32. Representar osciladores y parámetros con objetos en JavaScript

Un buen modelo de software separa lo que define al sistema de aquello que cambia con el tiempo. Los objetos permiten nombrar magnitudes, conservar unidades y convertir las ecuaciones físicas en funciones comprobables.

32.1 Del esquema físico a los datos

Antes de programar un oscilador debemos decidir qué sistema representamos, dónde está el origen y cuáles son sus parámetros. Un objeto no corrige un modelo físico ambiguo: sólo almacena nuestras decisiones.

Para un sistema masa-resorte horizontal mediremos x desde el equilibrio, tomaremos positivo hacia la derecha y usaremos unidades del Sistema Internacional.

32.2 Parámetros y estado no son lo mismo

Los parámetros describen el sistema durante una ejecución:

m, k, b y definición de la fuerza externa

El estado contiene las variables necesarias para continuar su evolución:

t, x y v

Separarlos evita modificar accidentalmente la masa cuando sólo queríamos actualizar la posición.

32.3 Nombres que incluyan la unidad

JavaScript almacena números, no magnitudes físicas. El valor 2 no indica por sí mismo si representa kilogramos, segundos o centímetros.

const parametros = {
  masaKg: 1.2,
  constanteNpm: 30,
  amortiguamientoNsPm: 1.5
};

const estado = {
  tiempoS: 0,
  posicionM: 0.08,
  velocidadMps: -0.2
};

console.log({ parametros, estado });

Los sufijos hacen visible el contrato de unidad en cada lectura y revisión de código.

32.4 Una estructura anidada

Podemos reunir información relacionada sin mezclar responsabilidades:

const oscilador = {
  tipo: "masa-resorte",
  convencion: {
    origen: "equilibrio",
    ejePositivo: "derecha",
    unidades: "SI"
  },
  parametros: {
    masaKg: 1.2,
    constanteNpm: 30,
    amortiguamientoNsPm: 1.5
  },
  estado: {
    tiempoS: 0,
    posicionM: 0.08,
    velocidadMps: -0.2
  }
};

console.log(oscilador.parametros.masaKg, oscilador.estado.posicionM);

La profundidad debe aportar claridad. Crear muchas capas con un solo valor cada una vuelve incómodo el acceso sin mejorar el modelo.

32.5 Establecer invariantes

El modelo utilizado en este curso requiere:

m > 0
k > 0
b ≥ 0
t, x y v finitos

Una invariante es una condición que debe mantenerse válida. Conviene verificarla al crear el objeto, cerca del lugar donde ingresan los datos.

32.6 Una función constructora validada

function crearOscilador({ masaKg, constanteNpm, amortiguamientoNsPm = 0,
  posicionInicialM = 0, velocidadInicialMps = 0 }) {
  const valores = [masaKg, constanteNpm, amortiguamientoNsPm,
    posicionInicialM, velocidadInicialMps];
  if (!valores.every(Number.isFinite)) {
    throw new TypeError("Todas las magnitudes deben ser números finitos");
  }
  if (masaKg <= 0 || constanteNpm <= 0 || amortiguamientoNsPm < 0) {
    throw new RangeError("Se requiere m > 0, k > 0 y b >= 0");
  }

  return {
    parametros: Object.freeze({ masaKg, constanteNpm, amortiguamientoNsPm }),
    estado: { tiempoS: 0, posicionM: posicionInicialM, velocidadMps: velocidadInicialMps }
  };
}

console.log(crearOscilador({ masaKg: 1.2, constanteNpm: 30,
  amortiguamientoNsPm: 1.5, posicionInicialM: 0.08 }));

32.7 Por qué comprobar números finitos

Comparaciones como masaKg <= 0 no rechazan NaN, porque toda comparación con NaN es falsa. Tampoco impiden Infinity.

Number.isFinite(valor) comprueba que el dato sea un número real representable y finito, sin convertir cadenas automáticamente.

32.8 Parámetros inmutables

Object.freeze impide reemplazar directamente las propiedades del objeto de parámetros. Esto ayuda a tratar m, k y b como configuración durante una simulación.

El congelamiento es superficial: si hubiera objetos anidados, también deberían protegerse. Además, inmutabilidad no significa que nunca podamos crear otra configuración; significa que el cambio será explícito mediante un nuevo objeto.

32.9 El estado sí evoluciona

Posición, velocidad y tiempo cambian paso a paso. Podemos mutar un objeto de estado controladamente o producir uno nuevo:

estadoSiguiente = actualizar(parametros, estadoActual, dt)

Producir estados nuevos facilita guardar un historial. Mutar un único objeto reduce asignaciones. La decisión es de diseño, pero debe ser coherente y estar documentada.

32.10 Copias y referencias

La asignación de objetos copia una referencia:

const copia = estado; no crea un estado independiente.

Para una estructura plana podemos usar:

const copia = { ...estado };

Si guardamos muchas veces la misma referencia en un arreglo, todo el historial terminará mostrando el último valor.

32.11 Propiedades derivadas

Algunas cantidades se calculan desde los parámetros:

ω₀ = √(k/m)
bc = 2√(mk)
ζ = b/bc

Si su cálculo es barato, suele ser más seguro obtenerlas cuando se necesitan que almacenarlas junto a m, k y b. Así no pueden quedar desactualizadas.

32.12 Funciones puras para calcular

Una función pura devuelve siempre el mismo resultado para los mismos argumentos y no modifica objetos externos:

function propiedadesDerivadas({ masaKg, constanteNpm, amortiguamientoNsPm }) {
  const omega0RadS = Math.sqrt(constanteNpm / masaKg);
  const bCriticoNsPm = 2 * Math.sqrt(masaKg * constanteNpm);
  const zeta = amortiguamientoNsPm / bCriticoNsPm;
  const regimen = Math.abs(zeta - 1) < 1e-10
    ? "crítico" : zeta < 1 ? "débil" : "sobreamortiguado";
  return { omega0RadS, bCriticoNsPm, zeta, regimen };
}

function evaluarEstado(parametros, estado) {
  const fuerzaElasticaN = -parametros.constanteNpm * estado.posicionM;
  const fuerzaViscosaN = -parametros.amortiguamientoNsPm * estado.velocidadMps;
  const energiaCineticaJ = 0.5 * parametros.masaKg * estado.velocidadMps ** 2;
  const energiaPotencialJ = 0.5 * parametros.constanteNpm * estado.posicionM ** 2;
  return { fuerzaElasticaN, fuerzaViscosaN,
    fuerzaResultanteN: fuerzaElasticaN + fuerzaViscosaN,
    energiaMecanicaJ: energiaCineticaJ + energiaPotencialJ };
}

const p = { masaKg: 1.2, constanteNpm: 30, amortiguamientoNsPm: 1.5 };
const e = { tiempoS: 0, posicionM: 0.08, velocidadMps: -0.2 };
console.log(propiedadesDerivadas(p), evaluarEstado(p, e));

32.13 Funciones externas o métodos

También podríamos escribir oscilador.energia() y oscilador.fuerza(). Los métodos resultan cómodos cuando la operación pertenece claramente al objeto.

Las funciones externas facilitan probar cada cálculo con objetos simples y separar datos de comportamiento. No existe una única respuesta correcta; para este curso elegiremos objetos de datos y funciones físicas independientes.

32.14 Representar una fuerza externa

Una fuerza dependiente del tiempo puede almacenarse como función:

fuerzaExternaN: tiempoS => fuerza0N * Math.cos(omegaRadS * tiempoS)

Las funciones no se conservan al convertir un objeto a JSON. Si necesitamos guardar la configuración, podemos serializar sus parámetros:

{ tipo: "sinusoidal", amplitudN: 2, omegaRadS: 3, faseRad: 0 }

Luego una función de fábrica transforma esa descripción en comportamiento ejecutable.

32.15 Un contrato con JSDoc

JSDoc documenta la forma de un objeto y permite que muchos editores detecten propiedades mal escritas:

/**
 * @typedef {Object} ParametrosOscilador
 * @property {number} masaKg
 * @property {number} constanteNpm
 * @property {number} amortiguamientoNsPm
 */

/**
 * @param {ParametrosOscilador} parametros
 * @returns {number}
 */
function frecuenciaNatural(parametros) {
  return Math.sqrt(parametros.constanteNpm / parametros.masaKg);
}

console.log(frecuenciaNatural({
  masaKg: 0.5, constanteNpm: 32, amortiguamientoNsPm: 0.8
}));

JSDoc ayuda durante el desarrollo, pero no reemplaza la validación en tiempo de ejecución cuando los datos provienen de formularios o archivos.

32.16 Conversiones en los límites

Una interfaz puede aceptar centímetros o gramos, pero el núcleo físico debería trabajar en una unidad coherente. Convertimos al ingresar:

posiciónM = posiciónCm / 100
masaKg = masaG / 1000

Y convertimos nuevamente sólo para presentar el resultado. Mezclar unidades dentro del objeto principal multiplica las posibilidades de error.

32.17 Comparaciones con tolerancia

Un valor calculado de ζ puede quedar en 0.9999999999999999 aun cuando conceptualmente sea 1. Para clasificar el régimen usamos una tolerancia relativa o absoluta apropiada.

La tolerancia pertenece a la política numérica del programa. No debe ocultarse como un número mágico repetido en muchas funciones.

32.18 Serializar con JSON

JSON.stringify convierte datos compatibles en texto y JSON.parse reconstruye una estructura de datos:

texto = JSON.stringify(oscilador)
datos = JSON.parse(texto)

JSON no preserva funciones, Infinity, NaN, prototipos ni referencias compartidas. Los datos recuperados deben validarse igual que una entrada nueva.

32.19 Versionar datos guardados

Si un formato se guarda en archivos o almacenamiento local, conviene agregar una versión:

{ version: 1, tipo: "masa-resorte", parametros: {...}, estado: {...} }

Cuando la estructura cambie, una función de migración podrá reconocer versiones antiguas en vez de interpretar silenciosamente campos incorrectos.

32.20 Separar modelo, vista y control

  • Modelo: objetos físicos y funciones de cálculo.
  • Vista: Canvas, textos, tablas y controles HTML.
  • Control: lectura de entradas y coordinación de actualizaciones.

El modelo no debería buscar elementos del documento. Así puede ejecutarse en pruebas automáticas, en otro entorno gráfico o sin interfaz.

32.21 Inspector interactivo del objeto

Modificá parámetros y estado. El inspector reconstruye un objeto válido y calcula sus propiedades sin realizar todavía una integración temporal.

Los resultados se describen debajo de la ilustración.

Representación serializable

Frecuencia natural ω₀5,000 rad/s
Razón ζ0,125
RégimenDébil
Fuerza resultante−2,100 N
Aceleración−1,750 m/s²
Energía mecánica0,120 J

El JSON contiene datos y unidades explícitas; las propiedades derivadas se calculan aparte.

32.22 Pruebas útiles para el modelo

  • Rechazar masa cero, rigidez negativa y valores no finitos.
  • Comprobar que la frecuencia natural tenga el valor esperado.
  • Verificar que invertir x invierta la fuerza elástica.
  • Verificar que invertir v invierta la fuerza viscosa.
  • Confirmar que la energía nunca sea negativa.
  • Asegurar que una función pura no modifique sus argumentos.
  • Serializar, recuperar, validar y comparar los campos esenciales.

32.23 Errores frecuentes y ejercicio

  • Usar nombres como m o value en toda la aplicación: ocultan significado y unidad.
  • Guardar ω₀ y no actualizarla al cambiar k: los datos quedan inconsistentes.
  • Guardar referencias en el historial: todas terminan apuntando al mismo estado mutable.
  • Confiar sólo en controles HTML: los datos también pueden llegar desde código o archivos.
  • Convertir cadenas implícitamente: puede aceptar entradas vacías o ambiguas.
  • Mezclar dibujo y física: dificulta probar y reutilizar el modelo.

Creá un objeto para m = 0,5 kg, k = 32 N/m, b = 0,8 N·s/m, x = −0,06 m y v = 0,4 m/s. Calculá fuerza resultante, aceleración, energía y ζ.

Ver solución y explicación
Felástica = −32·(−0,06) = +1,92 N
Fviscosa = −0,8·0,4 = −0,32 N
Fresultante = +1,60 N
a = 1,60/0,5 = +3,20 m/s²
E = ½·0,5·0,4² + ½·32·(−0,06)² = 0,0976 J
ζ = 0,8/[2√(0,5·32)] = 0,10
const oscilador = {
  parametros: Object.freeze({
    masaKg: 0.5, constanteNpm: 32, amortiguamientoNsPm: 0.8
  }),
  estado: { tiempoS: 0, posicionM: -0.06, velocidadMps: 0.4 }
};

const { masaKg: m, constanteNpm: k, amortiguamientoNsPm: b } = oscilador.parametros;
const { posicionM: x, velocidadMps: v } = oscilador.estado;
const fuerzaN = -k * x - b * v;
const aceleracionMps2 = fuerzaN / m;
const energiaJ = 0.5 * m * v ** 2 + 0.5 * k * x ** 2;
const zeta = b / (2 * Math.sqrt(m * k));

console.log({ fuerzaN, aceleracionMps2, energiaJ, zeta });

32.24 Ideas para recordar

  • Los parámetros definen el sistema; el estado describe su situación instantánea.
  • Los nombres con unidades convierten supuestos implícitos en contratos visibles.
  • Los datos externos deben validarse con dominio físico y finitud numérica.
  • Las propiedades derivadas no deberían duplicarse si pueden calcularse con seguridad.
  • Las funciones puras facilitan pruebas, reutilización y diagnóstico.
  • Una copia de referencia no es una copia del estado.
  • JSON conserva datos, pero no funciones ni invariantes.
  • Separar modelo, vista y control prepara una simulación mantenible.