32.1 Del modelo físico al modelo de datos
Hasta ahora describimos fluidos y partículas con ecuaciones. Para calcular y simular necesitamos decidir cómo representar cada magnitud en memoria: nombres, unidades, relaciones y restricciones.
La estructura de datos no es un detalle decorativo. Si mezcla unidades, duplica valores derivados o permite estados imposibles, los errores físicos aparecen antes de ejecutar cualquier algoritmo.
32.2 Definir el propósito antes de programar
No existe una representación óptima para todos los problemas. Una calculadora de sedimentación necesita diámetro y densidad; una simulación espacial también requiere posición, velocidad y fuerzas; un solver continuo almacena campos sobre una malla.
Conviene escribir primero qué preguntas debe responder el programa y qué aproximaciones físicas acepta. Después se eligen los datos mínimos necesarios.
32.3 Una convención interna de unidades
Una estrategia robusta es almacenar todo internamente en SI:
La interfaz puede aceptar mm, μm, L/min o cP, pero debe convertirlos en la frontera de entrada. La presentación convierte de regreso sin cambiar el estado interno.
32.4 Conversores explícitos
const unidades = Object.freeze({
mmAMetros: valor => valor * 1e-3,
micrometrosAMetros: valor => valor * 1e-6,
litrosPorSegundoAM3PorSegundo: valor => valor * 1e-3,
centipoiseAPascalSegundo: valor => valor * 1e-3
});
const diametro = unidades.micrometrosAMetros(200);
const viscosidad = unidades.centipoiseAPascalSegundo(1);
console.log(`Diámetro SI: ${diametro} m`);
console.log(`Viscosidad SI: ${viscosidad} Pa·s`);Nombrar la conversión hace visible la intención y evita factores dispersos como * 0.001 cuyo significado se pierde.
32.5 Representar un fluido
Para muchos cálculos introductorios alcanza un objeto con propiedades uniformes:
const agua = {
nombre: "agua",
densidad: 998.2, // kg/m³
viscosidadDinamica: 1.002e-3, // Pa·s
temperatura: 293.15 // K
};
const viscosidadCinematica = agua.viscosidadDinamica / agua.densidad;
console.log(`ν = ${viscosidadCinematica.toExponential(4)} m²/s`);Los comentarios ayudan, pero no sustituyen una convención documentada ni la validación de entradas.
32.6 Propiedades independientes y derivadas
Si un objeto almacena μ, ρ y también ν = μ/ρ, puede quedar inconsistente al modificar una sola propiedad. Es preferible guardar los datos independientes y calcular lo derivado:
function viscosidadCinematica(fluido) {
return fluido.viscosidadDinamica / fluido.densidad;
}
const fluido = { densidad: 1000, viscosidadDinamica: 0.001 };
console.log(viscosidadCinematica(fluido));Si el cálculo es costoso puede almacenarse en caché, pero debe existir una estrategia clara para invalidarla.
32.7 Representar una partícula
Una partícula esférica con estado cinemático puede expresarse como un objeto:
const particula = {
id: 1,
diametro: 200e-6, // m
densidad: 2500, // kg/m³
posicion: { x: 0.02, y: 0.08 }, // m
velocidad: { x: 0, y: 0 } // m/s
};
function volumenEsfera(diametro) {
return Math.PI * diametro ** 3 / 6;
}
console.log(`Masa: ${(particula.densidad * volumenEsfera(particula.diametro)).toExponential(4)} kg`);32.8 Vectores como objetos pequeños
En dos dimensiones, { x, y } es legible y suficiente para comenzar. Las operaciones deben estar centralizadas:
const vector = {
sumar: (a, b) => ({ x: a.x + b.x, y: a.y + b.y }),
restar: (a, b) => ({ x: a.x - b.x, y: a.y - b.y }),
escalar: (v, k) => ({ x: v.x * k, y: v.y * k }),
modulo: v => Math.hypot(v.x, v.y)
};
const velocidadParticula = { x: 0.2, y: -0.1 };
const velocidadFluido = { x: 0.05, y: 0 };
const relativa = vector.restar(velocidadParticula, velocidadFluido);
console.log(`Rapidez relativa: ${vector.modulo(relativa).toFixed(3)} m/s`);Repetir manualmente operaciones vectoriales en distintos lugares facilita errores de signo y componentes.
32.9 Funciones de creación y validación
Una función fábrica concentra valores iniciales y restricciones:
function crearParticula({ id, diametro, densidad, x = 0, y = 0 }) {
if (!Number.isInteger(id) || id < 0) throw new TypeError("id inválido");
if (!Number.isFinite(diametro) || diametro <= 0) throw new RangeError("diámetro inválido");
if (!Number.isFinite(densidad) || densidad <= 0) throw new RangeError("densidad inválida");
return {
id,
diametro,
densidad,
posicion: { x, y },
velocidad: { x: 0, y: 0 }
};
}
const p = crearParticula({ id: 7, diametro: 100e-6, densidad: 2650, y: 0.1 });
console.log(p);32.10 Validar números, no solo tipos
En JavaScript, typeof NaN es "number". Por eso conviene usar Number.isFinite y comprobar rangos físicos.
Una densidad negativa, un diámetro cero o una posición infinita son números desde el lenguaje, pero estados inválidos para el modelo.
32.11 Arreglo de objetos
La representación más directa de un sistema discreto es un arreglo:
const particulas = [
{ id: 0, diametro: 100e-6, densidad: 2500, posicion: { x: 0, y: 0 } },
{ id: 1, diametro: 150e-6, densidad: 2650, posicion: { x: 0.01, y: 0 } },
{ id: 2, diametro: 80e-6, densidad: 1200, posicion: { x: 0.02, y: 0 } }
];
const densas = particulas.filter(p => p.densidad > 2000);
const diametros = particulas.map(p => p.diametro);
console.log(`Partículas densas: ${densas.length}`);
console.log(`Diámetros: ${diametros.map(d => d * 1e6).join(", ")} µm`);Esta organización se conoce como arreglo de estructuras y favorece claridad.
32.12 Recorrer, transformar y reducir
map transforma, filter selecciona y reduce agrega. Para masa total:
const particulas = [
{ diametro: 100e-6, densidad: 2500 },
{ diametro: 150e-6, densidad: 2650 },
{ diametro: 80e-6, densidad: 1200 }
];
const volumen = d => Math.PI * d ** 3 / 6;
const masaTotal = particulas.reduce(
(acumulada, p) => acumulada + p.densidad * volumen(p.diametro),
0
);
console.log(`Masa total: ${masaTotal.toExponential(4)} kg`);El valor inicial 0 evita resultados inesperados en arreglos vacíos.
32.13 Estado mutable y parámetros
Propiedades como densidad o diámetro suelen ser parámetros constantes durante una simulación. Posición y velocidad forman el estado que cambia con el tiempo.
Separarlos ayuda a razonar y guardar datos:
32.14 Inmutabilidad selectiva
Object.freeze evita reasignar propiedades del primer nivel, pero no congela recursivamente objetos internos. Puede ser útil para configuraciones:
const configuracion = Object.freeze({
gravedad: 9.81,
pasoTemporal: 1e-3,
unidades: "SI"
});
console.log(configuracion);Congelar cada partícula durante una simulación intensiva puede resultar incómodo; la decisión depende del flujo de trabajo y del rendimiento requerido.
32.15 Clases o funciones y objetos
Una clase puede agrupar construcción y métodos, mientras una función fábrica produce objetos simples. Ninguna opción es físicamente más correcta.
Para datos que se serializan, transfieren o procesan masivamente, los objetos simples suelen ser transparentes. Las clases resultan útiles cuando protegen invariantes y ofrecen una interfaz estable. Debe evitarse esconder unidades o cálculos esenciales detrás de efectos laterales.
32.16 Un campo de fluido
Un único objeto uniforme no representa variaciones espaciales. Un campo puede modelarse como función:
const campoVelocidad = (x, y, tiempo) => ({
x: 0.5 + 0.1 * Math.sin(2 * Math.PI * y),
y: 0.05 * Math.cos(tiempo + x)
});
const velocidad = campoVelocidad(0.2, 0.4, 1.0);
console.log(`u = (${velocidad.x.toFixed(3)}, ${velocidad.y.toFixed(3)}) m/s`);En una simulación numérica real, el campo puede almacenarse sobre una malla y obtenerse en la posición de la partícula mediante interpolación.
32.17 Propiedades dependientes de temperatura
Si μ y ρ varían, no deben tratarse como constantes globales. Pueden representarse mediante funciones o tablas interpoladas:
function viscosidadAguaAproximada(temperaturaC) {
// Ajuste educativo local, no reemplaza una tabla de propiedades.
return 1e-3 * Math.exp(-0.033 * (temperaturaC - 20));
}
for (const temperatura of [10, 20, 40]) {
console.log(`${temperatura} °C: ${viscosidadAguaAproximada(temperatura).toExponential(3)} Pa·s`);
}El nombre debe advertir que se trata de una aproximación y el programa debe documentar su intervalo de validez.
32.18 Identificadores estables
El índice de un arreglo cambia al ordenar o eliminar elementos. Un id estable permite registrar trayectorias, relacionar mediciones y depurar eventos.
No conviene reutilizar identificadores durante una ejecución si los resultados históricos dependen de ellos. El índice puede seguir utilizándose para acceso rápido, pero no como identidad permanente.
32.19 Arreglos de estructuras frente a estructura de arreglos
| Representación | Ejemplo | Ventaja | Costo |
|---|---|---|---|
| Arreglo de objetos | [{x,y,vx,vy}, …] | Lectura y depuración simples | Más objetos y acceso menos compacto |
| Estructura de arreglos | {x:[], y:[], vx:[], vy:[]} | Recorridos homogéneos y memoria contigua | La identidad de una partícula queda distribuida |
Para decenas o cientos de partículas, la claridad suele dominar. Para cientos de miles, el patrón de acceso y la memoria pueden justificar una estructura de arreglos.
32.20 Arreglos tipados
Float64Array almacena números de 64 bits contiguos y con longitud fija:
const cantidad = 3;
const sistema = {
x: new Float64Array(cantidad),
y: new Float64Array(cantidad),
vx: new Float64Array(cantidad),
vy: new Float64Array(cantidad),
diametro: new Float64Array([100e-6, 150e-6, 80e-6]),
densidad: new Float64Array([2500, 2650, 1200])
};
sistema.x[1] = 0.01;
console.log(`x de partícula 1: ${sistema.x[1]} m`);
console.log(`Bytes en posiciones x: ${sistema.x.byteLength}`);32.21 Elegir precisión numérica
Number usa doble precisión. Float64Array conserva esa precisión; Float32Array reduce memoria, pero también cifras significativas.
La elección debe basarse en escalas, acumulación de error y plataforma. Guardar posiciones grandes con variaciones diminutas en 32 bits puede borrar diferencias relevantes.
32.22 Sumas y error acumulado
Sumar muchos valores de magnitudes muy distintas puede perder precisión. La suma compensada de Kahan reduce parte del error:
function sumaKahan(valores) {
let suma = 0;
let correccion = 0;
for (const valor of valores) {
const ajustado = valor - correccion;
const siguiente = suma + ajustado;
correccion = (siguiente - suma) - ajustado;
suma = siguiente;
}
return suma;
}
console.log(sumaKahan([1e16, 1, 1, -1e16]));La estabilidad del algoritmo importa tanto como la cantidad de decimales almacenados.
32.23 Copias y referencias
Asignar un objeto no lo copia: dos variables pueden apuntar al mismo estado. El operador { ...objeto } realiza una copia superficial; los objetos anidados siguen compartidos.
structuredClone crea una copia profunda de muchas estructuras estándar. Para arreglos tipados y grandes volúmenes, copiar tiene costo; conviene decidir explícitamente cuándo se necesita un estado independiente.
32.24 Serialización en JSON
const estado = {
unidades: "SI",
fluido: { densidad: 1000, viscosidadDinamica: 0.001 },
particulas: [{ id: 0, diametro: 100e-6, densidad: 2500 }]
};
const texto = JSON.stringify(estado, null, 2);
const restaurado = JSON.parse(texto);
console.log(texto);
console.log(`Densidad restaurada: ${restaurado.fluido.densidad} kg/m³`);JSON no conserva métodos, clases, Map, Infinity ni NaN como tales. Los arreglos tipados requieren una estrategia de codificación o conversión.
32.25 Versionar el formato de datos
Los archivos guardados sobreviven al código que los creó. Incluir versionFormato, unidades y metadatos permite migrar datos cuando cambia el modelo.
Sin versión, un campo renombrado o una unidad modificada puede interpretarse silenciosamente de manera incorrecta.
32.26 Generación reproducible
Math.random() no permite normalmente repetir una secuencia mediante una semilla elegida. Para depurar simulaciones conviene usar un generador determinista:
function crearAleatorio(semilla) {
let estado = semilla >>> 0;
return () => {
estado = (1664525 * estado + 1013904223) >>> 0;
return estado / 2 ** 32;
};
}
const aleatorioA = crearAleatorio(42);
const aleatorioB = crearAleatorio(42);
console.log(aleatorioA().toFixed(6), aleatorioA().toFixed(6));
console.log(aleatorioB().toFixed(6), aleatorioB().toFixed(6));La misma semilla y el mismo algoritmo generan las mismas condiciones iniciales.
32.27 Distribuciones, no solo valores uniformes
Un sistema real puede tener una distribución de tamaños. Generar diámetros uniformes entre dos extremos no equivale a una distribución normal o lognormal.
La distribución debe elegirse según el fenómeno y documentarse con sus parámetros. También debe decidirse si se muestrea diámetro, radio, volumen o masa, porque producen poblaciones diferentes.
32.28 Separar datos, física y presentación
Una arquitectura sencilla puede dividirse en:
- modelo: datos y validación;
- física: funciones de fuerzas, balances y propiedades derivadas;
- integración: avance temporal;
- vista: canvas, tablas y controles;
- persistencia: carga y guardado.
Esta separación permite probar cálculos sin depender del navegador ni de un dibujo.
32.29 Un modelo físico mínimo y comprobable
function volumenEsfera(d) {
if (!Number.isFinite(d) || d <= 0) throw new RangeError("d debe ser positivo");
return Math.PI * d ** 3 / 6;
}
function propiedadesParticula(particula, fluido, rapidezRelativa) {
const volumen = volumenEsfera(particula.diametro);
const masa = particula.densidad * volumen;
const reynolds = fluido.densidad * rapidezRelativa * particula.diametro /
fluido.viscosidadDinamica;
const tiempoRespuestaStokes = particula.densidad * particula.diametro ** 2 /
(18 * fluido.viscosidadDinamica);
return { volumen, masa, reynolds, tiempoRespuestaStokes };
}
const fluido = { densidad: 1000, viscosidadDinamica: 0.001 };
const particula = { diametro: 200e-6, densidad: 2500 };
const resultado = propiedadesParticula(particula, fluido, 0.05);
console.log(`Volumen: ${resultado.volumen.toExponential(4)} m³`);
console.log(`Masa: ${resultado.masa.toExponential(4)} kg`);
console.log(`Reynolds: ${resultado.reynolds.toFixed(2)}`);
console.log(`Tiempo de respuesta: ${(resultado.tiempoRespuestaStokes * 1000).toFixed(3)} ms`);Cada función posee entradas claras, no modifica argumentos y puede probarse de forma aislada.
32.30 Laboratorio de representación de datos
Ambas representaciones contienen el mismo sistema físico. Cambiá los parámetros y compará la vista previa de un arreglo de objetos con una estructura basada en Float64Array.
Rep = 1000·0,050·0,000200/0,0010 = 10,00
Arreglo de objetos: prioriza legibilidad; el modelo mantiene todas las magnitudes físicas en SI.
32.31 Pruebas mínimas del modelo
Antes de simular conviene comprobar:
- una esfera de diámetro doble tiene ocho veces el volumen;
- masa total coincide con la suma de masas individuales;
- Re es cero cuando la velocidad relativa es cero;
- la misma semilla reproduce posiciones;
- serializar y restaurar conserva valores y unidades;
- entradas inválidas generan errores explícitos;
- ambas representaciones producen las mismas magnitudes colectivas.
32.32 Ejercicio propuesto
Una entrada contiene tres partículas con diámetros en milímetros y densidades en kg/m³, además de un fluido cuya viscosidad llega en cP:
- Convertí la entrada a SI al construir el modelo.
- Asigná identificadores estables y estado inicial.
- Calculá volumen, masa y Reynolds para vrel = 0,03 m/s.
- Obtené masa total y diámetro medio.
- Serializá un objeto con versión y unidades.
Ver solución y explicación
const entrada = {
diametrosMm: [0.10, 0.20, 0.15],
densidades: [2500, 2650, 1200],
densidadFluido: 1000,
viscosidadCp: 1
};
const fluido = {
densidad: entrada.densidadFluido,
viscosidadDinamica: entrada.viscosidadCp * 1e-3
};
const particulas = entrada.diametrosMm.map((diametroMm, id) => ({
id,
diametro: diametroMm * 1e-3,
densidad: entrada.densidades[id],
posicion: { x: 0, y: 0 },
velocidad: { x: 0, y: 0 }
}));
const volumen = d => Math.PI * d ** 3 / 6;
const rapidezRelativa = 0.03;
const derivados = particulas.map(p => ({
id: p.id,
volumen: volumen(p.diametro),
masa: p.densidad * volumen(p.diametro),
reynolds: fluido.densidad * rapidezRelativa * p.diametro /
fluido.viscosidadDinamica
}));
const masaTotal = derivados.reduce((suma, p) => suma + p.masa, 0);
const diametroMedio = particulas.reduce((suma, p) => suma + p.diametro, 0) /
particulas.length;
const documento = { versionFormato: 1, unidades: "SI", fluido, particulas };
console.log(derivados.map(p => ({ id: p.id, Re: p.reynolds.toFixed(2) })));
console.log(`Masa total: ${masaTotal.toExponential(4)} kg`);
console.log(`Diámetro medio: ${(diametroMedio * 1e6).toFixed(1)} µm`);
console.log(JSON.stringify(documento));Los Reynolds son 3,00; 6,00 y 4,50. La masa total es aproximadamente 1,453 × 10−8 kg y el diámetro medio, 150 μm.
32.33 Ideas para recordar
- La representación debe responder al propósito físico del programa.
- Una convención interna SI evita mezclar unidades.
- Los valores derivados deben calcularse desde una fuente de verdad.
- Las funciones fábrica y validaciones impiden estados imposibles.
- Un arreglo de objetos prioriza claridad; los arreglos tipados priorizan acceso compacto.
- Parámetros, estado dinámico y resultados derivados cumplen papeles distintos.
- Identificadores y semillas reproducibles facilitan seguimiento y depuración.
- JSON necesita versión, unidades y una estrategia para tipos no compatibles.
- Separar datos, física y presentación hace comprobable el modelo.
En el próximo tema utilizaremos estas estructuras para calcular presión, empuje, caudal y balances con JavaScript.