# Herramientas de visualización
<!-- TODO fix callout para que se pueda referenciar en el indice de herramientas, yo crearia ejemplos con visualizacion asi -->
## Visualizador Plano de fases 1D:
::: {.content-visible when-format="html"}
::: {.callout-caution collapse="false" title="Visualizador de Plano de Fases" }
```{ojs}
//| echo: false
//| label: herr_controles-edo
viewof dy_dx = Inputs.textarea({
label: "dy/dx = f(x,y,a,b)",
value: " a*y + Math.sin(b*x)",
placeholder: "Ejemplo: a*y, y*(a-y), a*x+b*y (usa 'a' y 'b' como parámetros)",
rows: 1,
width: "100%"
})
// viewof ayuda_sistemas = Inputs.toggle({
// label: herr_"Mostrar ayuda y ejemplos",
// value: false
// })
viewof parametros_ab = Inputs.form({
a: Inputs.range([-5, 5], {
value: -0.7,
step: 0.1,
label: "Parámetro a"
}),
b: Inputs.range([-5, 5], {
value: 4.1,
step: 0.1,
label: "Parámetro b"
})
}, {
template: (inputs) => html`
<div style="display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 12px; align-items: center; margin: 6px 0;">
${inputs.a}
${inputs.b}
</div>
`
})
parametro_a = parametros_ab.a
parametro_b = parametros_ab.b
viewof ayuda_ecuacion = Inputs.toggle({
label: "Mostrar ayuda y ejemplos",
value: false
})
viewof mostrar_orbita1D = Inputs.toggle({
label : "Mostrar órbitas",
value : true
})
```
```{ojs}
//| echo: false
//| label: herr_ayuda-ecuacion-texto
html`${ayuda_ecuacion ? `
<div style="background: #f8f9fa; padding: 15px; border-radius: 5px; margin: 10px 0;">
<h4 style="margin-top: 0;">📚 Ejemplos de ecuaciones diferenciales:</h4>
<h5>📈 Crecimiento y decaimiento:</h5>
<ul style="margin: 5px 0;">
<li><strong>Exponencial:</strong> <code>a*y</code> (a > 0 crece, a < 0 decrece)</li>
<li><strong>Con término constante:</strong> <code>a*y + b</code></li>
<li><strong>Logístico:</strong> <code>y*(a-y)</code> o <code>a*y*(1-y/b)</code></li>
<li><strong>Cosecha:</strong> <code>a*y - b</code> (crecimiento con cosecha constante)</li>
</ul>
<h5>🌀 Ecuaciones no lineales:</h5>
<ul style="margin: 5px 0;">
<li><strong>Bifurcación:</strong> <code>a*y - y*y*y</code></li>
<li><strong>Efecto Allee:</strong> <code>y*(y-a)*(b-y)</code></li>
<li><strong>Gompertz:</strong> <code>a*y*Math.log(b/y)</code></li>
<li><strong>Bernoulli:</strong> <code>a*y + b*y*y</code></li>
</ul>
<h5>🔬 Ecuaciones con dependencia de x:</h5>
<ul style="margin: 5px 0;">
<li><strong>Lineal homogénea:</strong> <code>a*y</code></li>
<li><strong>Lineal no homogénea:</strong> <code>a*y + b*x</code></li>
<li><strong>Con función trigonométrica:</strong> <code>a*y + Math.sin(b*x)</code></li>
<li><strong>Separable:</strong> <code>a*x*y</code></li>
</ul>
<h4>⚠️ Sintaxis importante:</h4>
<ul style="margin: 10px 0;">
<li><strong>Variables disponibles:</strong> <code>x</code> (variable independiente), <code>y</code> (variable dependiente), <code>a</code> y <code>b</code> (parámetros)</li>
<li><strong>Funciones:</strong> <code>Math.sin()</code>, <code>Math.cos()</code>, <code>Math.exp()</code>, <code>Math.sqrt()</code>, <code>Math.log()</code>, <code>Math.abs()</code></li>
<li><strong>Multiplicación explícita:</strong> <code>2*x</code>, <code>a*y</code> (no uses <code>2x</code> o <code>ay</code>)</li>
<li><strong>Potencias:</strong> <code>y*y</code> o <code>Math.pow(y,2)</code></li>
</ul>
</div>
` : ''}`
```
```{ojs}
//| echo: false
//| label: herr_controles-visualizacion-1d
html`
<details style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 15px; border: 2px solid #dee2e6;">
<summary style="cursor: pointer; font-size: 1.1em; font-weight: bold; color: #2c3e50; margin-bottom: 15px; user-select: none;">
Parámetros de Visualización
</summary>
<div style="padding-top: 10px;">
${viewof densidad_campo_1d}
<div style="margin: 12px 0;"></div>
${viewof x_range_1d}
${viewof y_range_1d}
<div style="margin: 12px 0;"></div>
${viewof num_condiciones_1d}
<div style="margin: 12px 0;"></div>
${viewof tiempo_max_1d}
${viewof dt_1d}
</div>
</details>
`
viewof densidad_campo_1d = Inputs.range([0, 30], {
value: 20,
step: 1,
label: "Densidad del campo vectorial"
})
// Control de rango dual para x
viewof x_range_1d = Inputs.form({
min: Inputs.range([-10, 10], {
value: -3,
step: 0.5,
label: "min"
}),
max: Inputs.range([-10, 10], {
value: 3,
step: 0.5,
label: "max"
})
}, {
template: (inputs) => html`
<div style="margin-bottom: 10px;">
<label style="font-weight: bold; display: block; margin-bottom: 6px;">Rango de x</label>
<div style="display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 12px; align-items: center;">
${inputs.min}
${inputs.max}
</div>
</div>
`
})
// Control de rango dual para y
viewof y_range_1d = Inputs.form({
min: Inputs.range([-10, 10], {
value: -3,
step: 0.5,
label: "min"
}),
max: Inputs.range([-10, 10], {
value: 3,
step: 0.5,
label: "max"
})
}, {
template: (inputs) => html`
<div style="margin-bottom: 10px;">
<label style="font-weight: bold; display: block; margin-bottom: 8px;">Rango de y</label>
<div style="display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 12px; align-items: center;">
${inputs.min}
${inputs.max}
</div>
</div>
`
})
// Extraer valores individuales con validación
x_min_1d = Math.min(x_range_1d.min, x_range_1d.max - 0.5)
x_max_1d = Math.max(x_range_1d.max, x_range_1d.min + 0.5)
y_min_1d = Math.min(y_range_1d.min, y_range_1d.max - 0.5)
y_max_1d = Math.max(y_range_1d.max, y_range_1d.min + 0.5)
viewof num_condiciones_1d = Inputs.range([3, 15], {
value: 6,
step: 1,
label: "Particiones por lado (n×n)"
})
viewof tiempo_max_1d = Inputs.range([0.5, 100], {
value: 10,
step: 0.5,
label: "Tiempo de integración (1D)"
})
viewof dt_1d = Inputs.range([0.005, 0.2], {
value: 0.05,
step: 0.005,
label: "Paso (dt)"
})
```
```{ojs}
//| echo: false
//| label: herr_generar-condiciones-1d
// Generar condiciones iniciales en un retículo 2D
condiciones_iniciales_1d_auto = {
const conds = [];
// Crear grid de puntos: num_condiciones_1d × num_condiciones_1d
const puntos_por_eje = num_condiciones_1d;
const x_range = x_max_1d - x_min_1d;
const y_range = y_max_1d - y_min_1d;
const step_x = x_range / (puntos_por_eje + 1);
const step_y = y_range / (puntos_por_eje + 1);
for (let i = 0; i <= puntos_por_eje; i++) {
for (let j = 0; j <= puntos_por_eje +1 ; j++) {
const x_val = x_min_1d + i * step_x;
const y_val = y_min_1d + j * step_y;
conds.push(`[${x_val.toFixed(2)},${y_val.toFixed(2)}]`);
}
}
return conds.join('; ');
}
```
```{ojs}
//| echo: false
//| label: herr_generar-plot1d
// Parser: sustituye 'a' y 'b' por los valores de parametro_a y parametro_b en la ecuación
dy_dx_parseada = {
// Reemplaza 'a' y 'b' solo cuando son variables independientes (no parte de palabras como 'tan', 'Math', etc.)
// Usa word boundary \b para asegurar que están solas
let resultado = dy_dx.replace(/\ba\b/g, `(${parametro_a})`);
resultado = resultado.replace(/\bb\b/g, `(${parametro_b})`);
return resultado;
}
visualizar_sistema({
dx_dt: "1",
dy_dt: dy_dx_parseada,
condiciones_iniciales: condiciones_iniciales_1d_auto,
densidad: densidad_campo_1d,
x_min: x_min_1d,
x_max: x_max_1d,
y_min: y_min_1d,
y_max: y_max_1d,
tiempo_max: tiempo_max_1d,
dt: dt_1d,
mostrar_direccion: true,
normalizar: true,
longitud_vectores: .45,
mostrar_condiciones_iniciales: false,
puntos_equilibrio: "",
mostrar_orbitas: mostrar_orbita1D
})
```
:::
:::
## Plano 2D
::: {.content-visible when-format="html"}
::: {.callout-caution collapse="true" title="Visualizador de Plano de Fases" }
```{ojs}
//| echo: false
//| label: herr_funciones-comunes
// ===== FUNCIONES REUTILIZABLES =====
// Método de Runge-Kutta de 4º orden (más preciso que Euler)
rk4_step = (x, y, t, dt, f, g) => {
const k1_x = f(x, y, t);
const k1_y = g(x, y, t);
const k2_x = f(x + dt/2 * k1_x, y + dt/2 * k1_y, t + dt/2);
const k2_y = g(x + dt/2 * k1_x, y + dt/2 * k1_y, t + dt/2);
const k3_x = f(x + dt/2 * k2_x, y + dt/2 * k2_y, t + dt/2);
const k3_y = g(x + dt/2 * k2_x, y + dt/2 * k2_y, t + dt/2);
const k4_x = f(x + dt * k3_x, y + dt * k3_y, t + dt);
const k4_y = g(x + dt * k3_x, y + dt * k3_y, t + dt);
return {
x: x + dt/6 * (k1_x + 2*k2_x + 2*k3_x + k4_x),
y: y + dt/6 * (k1_y + 2*k2_y + 2*k3_y + k4_y)
};
}
// Crear funciones evaluables desde strings
crear_funcion_sistema = (expresion) => {
try {
const func = new Function('x', 'y', 't', `
try {
const result = ${expresion};
if (!isFinite(result)) return 0;
return result;
} catch(e) {
return 0;
}
`);
return func;
} catch(e) {
return (x, y, t) => 0;
}
}
// Calcular campo vectorial
calcular_campo_vectorial = (f, g, x_min, x_max, y_min, y_max, densidad, longitud, normalizar) => {
const vectores = [];
const dx = (x_max - x_min) / densidad;
const dy = (y_max - y_min) / densidad;
for (let x = x_min; x <= x_max; x += dx) {
for (let y = y_min; y <= y_max; y += dy) {
try {
const vx = f(x, y, 0);
const vy = g(x, y, 0);
if (!isFinite(vx) || !isFinite(vy)) continue;
const magnitud = Math.sqrt(vx*vx + vy*vy);
if (magnitud === 0) continue;
let scale;
if (normalizar) {
scale = Math.min(dx, dy) * longitud;
} else {
scale = Math.min(dx, dy) * longitud * Math.min(1, magnitud);
}
const dx_vec = (vx / magnitud) * scale;
const dy_vec = (vy / magnitud) * scale;
vectores.push({
x: x,
y: y,
x1: x,
y1: y,
x2: x + dx_vec,
y2: y + dy_vec,
magnitud: magnitud
});
} catch(e) {
continue;
}
}
}
return vectores;
}
// Calcular órbitas
calcular_orbitas = (f, g, condiciones_str, tiempo_max, dt, x_min, x_max, y_min, y_max) => {
const conds_iniciales = condiciones_str
.split(';')
.map(s => {
const match = s.trim().match(/\[([^,]+),([^\]]+)\]/);
if (match) {
return {
x: parseFloat(match[1]),
y: parseFloat(match[2])
};
}
return null;
})
.filter(p => p !== null && isFinite(p.x) && isFinite(p.y));
const orbitas_calculadas = [];
const pasos = Math.floor(tiempo_max / dt);
for (const ic of conds_iniciales) {
const puntos = [{x: ic.x, y: ic.y, t: 0}];
let x = ic.x, y = ic.y, t = 0;
for (let i = 0; i < pasos; i++) {
try {
const nuevo = rk4_step(x, y, t, dt, f, g);
if (!isFinite(nuevo.x) || !isFinite(nuevo.y)) break;
if (Math.abs(nuevo.x) > 1000 || Math.abs(nuevo.y) > 1000) break;
if (nuevo.x < x_min - 1 || nuevo.x > x_max + 1) break;
if (nuevo.y < y_min - 1 || nuevo.y > y_max + 1) break;
x = nuevo.x;
y = nuevo.y;
t += dt;
puntos.push({x: x, y: y, t: t});
} catch(e) {
break;
}
}
if (puntos.length > 1) {
orbitas_calculadas.push({
ic: ic,
puntos: puntos
});
}
}
return orbitas_calculadas;
}
// Parsear puntos de equilibrio
parsear_puntos_equilibrio = (puntos_str, f, g, buscar_auto, x_min, x_max, y_min, y_max) => {
const puntos = [];
// Puntos manuales
if (puntos_str && puntos_str.trim() !== '') {
const manuales = puntos_str
.split(';')
.map(s => {
const match = s.trim().match(/\[([^,]+),([^\]]+)\]/);
if (match) {
return {
x: parseFloat(match[1]),
y: parseFloat(match[2]),
tipo: 'manual'
};
}
return null;
})
.filter(p => p !== null && isFinite(p.x) && isFinite(p.y));
puntos.push(...manuales);
}
// Búsqueda automática
if (buscar_auto) {
const resolucion = 20;
const dx = (x_max - x_min) / resolucion;
const dy = (y_max - y_min) / resolucion;
const tolerancia = 0.05;
for (let x = x_min; x <= x_max; x += dx) {
for (let y = y_min; y <= y_max; y += dy) {
try {
const fx = f(x, y, 0);
const fy = g(x, y, 0);
if (Math.abs(fx) < tolerancia && Math.abs(fy) < tolerancia) {
const duplicado = puntos.some(p =>
Math.abs(p.x - x) < dx/2 && Math.abs(p.y - y) < dy/2
);
if (!duplicado) {
puntos.push({x: x, y: y, tipo: 'auto'});
}
}
} catch(e) {
continue;
}
}
}
}
return puntos;
}
// Crear gráfico de plano de fases
crear_plot_fases = (campo, orbitas, puntos_eq, x_min, x_max, y_min, y_max, mostrar_direccion, mostrar_condiciones_iniciales) => {
return Plot.plot({
width: 700,
height: 400,
marginLeft: 60,
marginBottom: 60,
grid: true,
x: {
label: "x →",
domain: [x_min, x_max]
},
y: {
label: "↑ y",
domain: [y_min, y_max]
},
marks: [
// Ejes coordenados
Plot.ruleX([0], {stroke: "black", strokeWidth: 1.5, opacity: 0.3}),
Plot.ruleY([0], {stroke: "black", strokeWidth: 1.5, opacity: 0.3}),
// Campo vectorial (flechas)
...campo.map(v =>
Plot.arrow([v], {
x1: "x1",
y1: "y1",
x2: "x2",
y2: "y2",
stroke: "#999",
strokeWidth: 1.5,
headLength: 8,
headAngle: 25,
opacity: 0.6
})
),
// Órbitas
...orbitas.map((orb, i) =>
Plot.line(orb.puntos, {
x: "x",
y: "y",
stroke: "#3498db",
strokeWidth: 2.5,
opacity: 0.7
})
),
// Flechas de dirección en órbitas
...(mostrar_direccion ?
orbitas.flatMap((orb, i) => {
const indices = [
Math.floor(orb.puntos.length * 0.25),
Math.floor(orb.puntos.length * 0.5),
Math.floor(orb.puntos.length * 0.75)
];
return indices.map(idx => {
if (idx >= orb.puntos.length - 1) return null;
const p1 = orb.puntos[idx];
const p2 = orb.puntos[idx + 1];
return Plot.arrow([{x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y}], {
x1: "x1",
y1: "y1",
x2: "x2",
y2: "y2",
stroke: "#2980b9",
strokeWidth: 2.5,
headLength: 10,
headAngle: 30
});
}).filter(m => m !== null);
}) : []
),
// Condiciones iniciales (solo si mostrar_condiciones_iniciales es true)
...(mostrar_condiciones_iniciales ?
orbitas.map((orb, i) =>
Plot.dot([orb.ic], {
x: "x",
y: "y",
fill: "#e74c3c",
r: 5,
stroke: "white",
strokeWidth: 2
})
) : []
),
// Puntos de equilibrio
...(puntos_eq.length > 0 ?
[Plot.dot(puntos_eq, {
x: "x",
y: "y",
fill: "black",
r: 8,
stroke: "white",
strokeWidth: 3
})] : []
),
// Etiquetas de puntos de equilibrio
...(puntos_eq.length > 0 ?
puntos_eq.map(p =>
Plot.text([p], {
x: "x",
y: "y",
text: () => `(${p.x.toFixed(2)}, ${p.y.toFixed(2)})`,
fill: "black",
fontSize: 11,
fontWeight: "bold",
stroke: "white",
strokeWidth: 3,
dy: -15
})
) : []
)
]
});
}
// ===== FUNCIÓN MAESTRA: Visualizar sistema completo =====
visualizar_sistema = (params) => {
// Parámetros por defecto - ahora acepta TODOS los parámetros del visualizador interactivo
const config = {
// Sistema de ecuaciones
dx_dt: params.dx_dt || "0",
dy_dt: params.dy_dt || "0",
// Parámetros de visualización del campo vectorial
densidad: params.densidad ?? 20,
x_min: params.x_min ?? -3,
x_max: params.x_max ?? 3,
y_min: params.y_min ?? -3,
y_max: params.y_max ?? 3,
longitud_vectores: params.longitud_vectores ?? 0.35,
normalizar: params.normalizar ?? true,
// Parámetros de órbitas
mostrar_orbitas: params.mostrar_orbitas ?? true,
condiciones_iniciales: params.condiciones_iniciales ?? "[2,0]; [0,2]; [-2,0]; [0,-2]; [1,1]; [-1,-1]",
tiempo_max: params.tiempo_max ?? 5,
dt: params.dt ?? 0.05,
mostrar_direccion: params.mostrar_direccion ?? true,
mostrar_condiciones_iniciales: params.mostrar_condiciones_iniciales ?? true,
// Parámetros de puntos de equilibrio
buscar_equilibrios: params.buscar_equilibrios ?? false,
puntos_equilibrio: params.puntos_equilibrio ?? "[0,0]"
};
// Crear funciones del sistema
const f = crear_funcion_sistema(config.dx_dt);
const g = crear_funcion_sistema(config.dy_dt);
// Validar sistema
try {
const test_f = f(1, 1, 0);
const test_g = g(1, 1, 0);
if (!isFinite(test_f) || !isFinite(test_g)) {
return html`<div style="color: red; padding: 20px;">❌ Sistema no válido</div>`;
}
} catch(e) {
return html`<div style="color: red; padding: 20px;">❌ Error en el sistema: ${e.message}</div>`;
}
// Calcular campo vectorial
const campo = calcular_campo_vectorial(
f, g,
config.x_min, config.x_max,
config.y_min, config.y_max,
config.densidad,
config.longitud_vectores,
config.normalizar
);
// Calcular órbitas
const orbitas = config.mostrar_orbitas ?
calcular_orbitas(
f, g,
config.condiciones_iniciales,
config.tiempo_max, config.dt,
config.x_min, config.x_max,
config.y_min, config.y_max
) : [];
// Parsear puntos de equilibrio
const puntos_eq = parsear_puntos_equilibrio(
config.puntos_equilibrio,
f, g,
config.buscar_equilibrios,
config.x_min, config.x_max,
config.y_min, config.y_max
);
// Crear plot
return crear_plot_fases(
campo,
orbitas,
puntos_eq,
config.x_min, config.x_max,
config.y_min, config.y_max,
config.mostrar_direccion,
config.mostrar_condiciones_iniciales
);
}
```
```{ojs}
//| echo: false
//| label: herr_controles-sistema
html`
<details open style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 15px; border: 2px solid #dee2e6;">
<summary style="cursor: pointer; font-size: 1.1em; font-weight: bold; color: #2c3e50; margin-bottom: 15px; user-select: none;">
Sistema de Ecuaciones Diferenciales
</summary>
<div style="padding-top: 10px;">
${viewof dx_dt}
<div style="margin: 12px 0;"></div>
${viewof dy_dt}
<div style="margin: 12px 0;"></div>
<div style="display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 12px; align-items: center;">
${viewof parametro_a_2d}
${viewof parametro_b_2d}
${viewof parametro_c_2d}
${viewof parametro_d_2d}
</div>
<div style="margin: 15px 0;"></div>
${viewof ayuda_sistemas}
</div>
</details>
`
viewof dx_dt = Inputs.textarea({
label: "dx/dt = f(x,y)",
value: "-y",
placeholder: "Ejemplo: -y, x-x*x*x, -x+y",
rows: 1,
width: "100%"
})
viewof dy_dt = Inputs.textarea({
label: "dy/dt = g(x,y)",
value: "x",
placeholder: "Ejemplo: x, y-y*y*y, x-y",
rows: 1,
width: "100%"
})
viewof parametro_a_2d = Inputs.range([-5, 5], {
value: 1,
step: 0.1,
label: "Parámetro a"
})
viewof parametro_b_2d = Inputs.range([-5, 5], {
value: 1,
step: 0.1,
label: "Parámetro b"
})
viewof parametro_c_2d = Inputs.range([-5, 5], {
value: 0,
step: 0.1,
label: "Parámetro c"
})
viewof parametro_d_2d = Inputs.range([-5, 5], {
value: 0,
step: 0.1,
label: "Parámetro d"
})
viewof ayuda_sistemas = Inputs.toggle({
label: "Mostrar ayuda y ejemplos",
value: false
})
```
```{ojs}
//| echo: false
//| label: herr_ayuda-sistemas-texto
html`${ayuda_sistemas ? `
<div style="background: #f8f9fa; padding: 15px; border-radius: 5px; margin: 10px 0;">
<h4 style="margin-top: 0;">📚 Ejemplos de sistemas:</h4>
<h5>🔄 Sistemas Lineales:</h5>
<ul style="margin: 5px 0;">
<li><strong>Centro (oscilador armónico):</strong> dx/dt = <code>-y</code>, dy/dt = <code>x</code></li>
<li><strong>Nodo estable:</strong> dx/dt = <code>-x</code>, dy/dt = <code>-2*y</code></li>
<li><strong>Nodo inestable:</strong> dx/dt = <code>x</code>, dy/dt = <code>2*y</code></li>
<li><strong>Silla de montar:</strong> dx/dt = <code>x</code>, dy/dt = <code>-y</code></li>
<li><strong>Espiral estable:</strong> dx/dt = <code>-x-y</code>, dy/dt = <code>x-y</code></li>
</ul>
<h5>🌀 Sistemas No Lineales:</h5>
<ul style="margin: 5px 0;">
<li><strong>Péndulo:</strong> dx/dt = <code>y</code>, dy/dt = <code>-Math.sin(x)</code></li>
<li><strong>Van der Pol:</strong> dx/dt = <code>y</code>, dy/dt = <code>(1-x*x)*y - x</code></li>
<li><strong>Lotka-Volterra (presa-depredador):</strong> dx/dt = <code>x*(1-y)</code>, dy/dt = <code>-y*(1-x)</code></li>
<li><strong>Duffing:</strong> dx/dt = <code>y</code>, dy/dt = <code>-0.1*y - x*x*x + x</code></li>
<li><strong>Ciclo límite:</strong> dx/dt = <code>y + x*(1-x*x-y*y)</code>, dy/dt = <code>-x + y*(1-x*x-y*y)</code></li>
</ul>
<h4>⚠️ Sintaxis importante:</h4>
<ul style="margin: 10px 0;">
<li>Usa <code>Math.sin()</code>, <code>Math.cos()</code>, <code>Math.exp()</code>, <code>Math.sqrt()</code></li>
<li>Multiplicación: <code>2*x</code>, <code>x*y</code></li>
<li>Potencias: <code>x*x</code> o <code>Math.pow(x,2)</code></li>
</ul>
</div>
` : ''}`
```
```{ojs}
//| echo: false
//| label: herr_controles-visualizacion-fases
html`
<details style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 15px; border: 2px solid #dee2e6;">
<summary style="cursor: pointer; font-size: 1.1em; font-weight: bold; color: #2c3e50; margin-bottom: 15px; user-select: none;">
Parámetros de Visualización
</summary>
<div style="padding-top: 10px;">
${viewof densidad_campo_fases}
<div style="margin: 12px 0;"></div>
${viewof x_range_fases}
${viewof y_range_fases}
<div style="margin: 12px 0;"></div>
${viewof longitud_vectores_fases}
<div style="margin: 12px 0;"></div>
${viewof normalizar_vectores}
</div>
</details>
`
viewof densidad_campo_fases = Inputs.range([0, 30], {
value: 20,
step: 1,
label: "Densidad del campo vectorial"
})
// Control de rango dual para x
viewof x_range_fases = Inputs.form({
min: Inputs.range([-10, 10], {
value: -3,
step: 0.5,
label: "min"
}),
max: Inputs.range([-10, 10], {
value: 3,
step: 0.5,
label: "max"
})
}, {
template: (inputs) => html`
<div style="margin-bottom: 10px;">
<label style="font-weight: bold; display: block; margin-bottom: 6px;">Rango de x</label>
<div style="display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 12px; align-items: center;">
${inputs.min}
${inputs.max}
</div>
</div>
`
})
// Control de rango dual para y
viewof y_range_fases = Inputs.form({
min: Inputs.range([-10, 10], {
value: -3,
step: 0.5,
label: "min"
}),
max: Inputs.range([-10, 10], {
value: 3,
step: 0.5,
label: "max"
})
}, {
template: (inputs) => html`
<div style="margin-bottom: 10px;">
<label style="font-weight: bold; display: block; margin-bottom: 8px;">Rango de y</label>
<div style="display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 12px; align-items: center;">
${inputs.min}
${inputs.max}
</div>
</div>
`
})
// Extraer valores individuales con validación
x_min_fases = Math.min(x_range_fases.min, x_range_fases.max - 0.5)
x_max_fases = Math.max(x_range_fases.max, x_range_fases.min + 0.5)
y_min_fases = Math.min(y_range_fases.min, y_range_fases.max - 0.5)
y_max_fases = Math.max(y_range_fases.max, y_range_fases.min + 0.5)
viewof longitud_vectores_fases = Inputs.range([0.2, 0.8], {
value: 0.35,
step: 0.05,
label: "Longitud de vectores"
})
viewof normalizar_vectores = Inputs.toggle({
label: "Normalizar vectores (longitud uniforme)",
value: true
})
```
```{ojs}
//| echo: false
//| label: herr_controles-orbitas
html`
<details style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 15px; border: 2px solid #dee2e6;">
<summary style="cursor: pointer; font-size: 1.1em; font-weight: bold; color: #2c3e50; margin-bottom: 15px; user-select: none;">
Órbitas (Trayectorias)
</summary>
<div style="padding-top: 10px;">
${viewof mostrar_orbitas}
<div style="margin: 12px 0;"></div>
${viewof condiciones_iniciales}
<div style="margin: 12px 0;"></div>
${viewof orbitas_automaticas}
<div style="margin: 8px 0;"></div>
${viewof num_orbitas_grid}
<div style="margin: 12px 0;"></div>
${viewof tiempo_max}
<div style="margin: 8px 0;"></div>
${viewof dt_rk4}
<div style="margin: 8px 0;"></div>
${viewof mostrar_direccion}
<div style="margin: 8px 0;"></div>
${viewof mostrar_condiciones_iniciales}
</div>
</details>
`
viewof mostrar_orbitas = Inputs.toggle({
label: "Mostrar órbitas",
value: true
})
viewof orbitas_automaticas = Inputs.toggle({
label: "Añadir órbitas automáticas",
value: false,
disabled: !mostrar_orbitas
})
viewof num_orbitas_grid = Inputs.range([1, 15], {
value: 6,
step: 1,
label: "Particiones por lado (n×n)",
disabled: !mostrar_orbitas || !orbitas_automaticas
})
viewof condiciones_iniciales = Inputs.text({
label: "Condiciones iniciales [x,y]",
value: "[2,0]; [0,2]; [-2,0]; [0,-2]; [1,1]; [-1,-1]",
placeholder: "[1,0]; [0,1]; [-1,0]",
disabled: !mostrar_orbitas,
width: "100%"
})
viewof tiempo_max = Inputs.range([.02, 20], {
value: 5,
step: .02,
label: "Tiempo de integración",
disabled: !mostrar_orbitas
})
viewof dt_rk4 = Inputs.range([0.01, 0.2], {
value: 0.05,
step: 0.01,
label: "Tamaño de paso (dt)",
disabled: !mostrar_orbitas
})
viewof mostrar_direccion = Inputs.toggle({
label: "Mostrar dirección del flujo (flechas en órbitas)",
value: true,
disabled: !mostrar_orbitas
})
viewof mostrar_condiciones_iniciales = Inputs.toggle({
label: "Mostrar condiciones iniciales",
value: true,
disabled: !mostrar_orbitas
})
```
```{ojs}
//| echo: false
//| label: herr_controles-puntos-equilibrio
html`
<details style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px; border: 2px solid #dee2e6;">
<summary style="cursor: pointer; font-size: 1.1em; font-weight: bold; color: #2c3e50; margin-bottom: 15px; user-select: none;">
Puntos de Equilibrio
</summary>
<div style="padding-top: 10px;">
${viewof buscar_equilibrios}
<div style="margin: 12px 0;"></div>
${viewof puntos_equilibrio_manual}
</div>
</details>
`
viewof buscar_equilibrios = Inputs.toggle({
label: "Buscar puntos de equilibrio automáticamente",
value: false
})
viewof puntos_equilibrio_manual = Inputs.text({
label: "Puntos de equilibrio manuales [x,y] (separados por punto y coma)",
value: "[0,0]",
placeholder: "[0,0]; [1,1]",
width: "100%"
})
```
```{ojs}
//| echo: false
//| label: herr_funciones-sistema
// Parsear 'a' y 'b' en las ecuaciones 2D usando los sliders correspondientes
dx_dt_parseada = {
let r = dx_dt.replace(/\ba\b/g, `(${parametro_a_2d})`);
r = r.replace(/\bb\b/g, `(${parametro_b_2d})`);
r = r.replace(/\bc\b/g, `(${parametro_c_2d})`);
r = r.replace(/\bd\b/g, `(${parametro_d_2d})`);
return r;
}
dy_dt_parseada = {
let r = dy_dt.replace(/\ba\b/g, `(${parametro_a_2d})`);
r = r.replace(/\bb\b/g, `(${parametro_b_2d})`);
r = r.replace(/\bc\b/g, `(${parametro_c_2d})`);
r = r.replace(/\bd\b/g, `(${parametro_d_2d})`);
return r;
}
// Crear funciones del sistema usando la función común, ahora con parámetros reemplazados
f_sistema = crear_funcion_sistema(dx_dt_parseada)
g_sistema = crear_funcion_sistema(dy_dt_parseada)
// Verificar validez
sistema_valido = {
try {
const test_f = f_sistema(1, 1, 0);
const test_g = g_sistema(1, 1, 0);
return isFinite(test_f) && isFinite(test_g);
} catch(e) {
return false;
}
}
```
```{ojs}
//| echo: false
//| label: herr_mensaje-error-sistema
html`${!sistema_valido ? `
<div style="background: #fff3cd; border: 1px solid #ffc107; padding: 10px; border-radius: 5px; margin: 10px 0;">
⚠️ <strong>Error en el sistema:</strong> Verifica la sintaxis de ambas ecuaciones.
</div>
` : ''}`
```
```{ojs}
//| echo: false
//| label: herr_generar-condiciones-2d
// Generar condiciones iniciales combinando manuales y automáticas
condiciones_iniciales_auto_2d = {
const conds = [];
// Siempre incluir las condiciones manuales si están definidas
if (condiciones_iniciales && condiciones_iniciales.trim() !== '') {
conds.push(condiciones_iniciales);
}
// Si órbitas automáticas están activas, añadir el retículo
if (orbitas_automaticas) {
const auto_conds = [];
const puntos_por_eje = num_orbitas_grid;
const x_range = x_max_fases - x_min_fases;
const y_range = y_max_fases - y_min_fases;
const step_x = x_range / (puntos_por_eje + 1);
const step_y = y_range / (puntos_por_eje + 1);
for (let i = 0; i <= puntos_por_eje; i++) {
for (let j = 0; j <= puntos_por_eje; j++) {
const x_val = x_min_fases + i * step_x;
const y_val = y_min_fases + j * step_y;
auto_conds.push(`[${x_val.toFixed(2)},${y_val.toFixed(2)}]`);
}
}
conds.push(auto_conds.join('; '));
}
return conds.join('; ');
}
```
```{ojs}
//| echo: false
//| label: herr_campo-vectorial-fases
campo_vectorial = {
if (!sistema_valido) return [];
return calcular_campo_vectorial(
f_sistema, g_sistema,
x_min_fases, x_max_fases,
y_min_fases, y_max_fases,
densidad_campo_fases,
longitud_vectores_fases,
normalizar_vectores
);
}
```
```{ojs}
//| echo: false
//| label: herr_orbitas
orbitas = {
if (!sistema_valido || !mostrar_orbitas) return [];
return calcular_orbitas(
f_sistema, g_sistema,
condiciones_iniciales_auto_2d,
tiempo_max, dt_rk4,
x_min_fases, x_max_fases,
y_min_fases, y_max_fases
);
}
```
```{ojs}
//| echo: false
//| label: herr_puntos-equilibrio
puntos_equilibrio = {
if (!sistema_valido) return [];
return parsear_puntos_equilibrio(
puntos_equilibrio_manual,
f_sistema, g_sistema,
buscar_equilibrios,
x_min_fases, x_max_fases,
y_min_fases, y_max_fases
);
}
```
```{ojs}
//| echo: false
//| label: herr_grafico-plano-fases
{
if (!sistema_valido) return html`<div style="color: red;">Sistema no válido</div>`;
// No mostrar condiciones iniciales si las órbitas automáticas están activas
const mostrar_conds_init = mostrar_condiciones_iniciales && !orbitas_automaticas;
return crear_plot_fases(
campo_vectorial,
mostrar_orbitas ? orbitas : [],
puntos_equilibrio,
x_min_fases, x_max_fases,
y_min_fases, y_max_fases,
mostrar_direccion,
mostrar_conds_init
);
}
```
```{ojs}
//| echo: false
//| label: herr_estadisticas-fases
html`
<div style="background: #e7f3ff; padding: 15px; border-radius: 5px; margin-top: 20px;">
<h4 style="margin-top: 0;">Información del sistema:</h4>
<ul style="margin: 10px 0;">
<li><strong>Sistema:</strong>
<code>dx/dt = ${dx_dt}</code><br>
<code style="margin-left: 48px;">dy/dt = ${dy_dt}</code>
</li>
<li><strong>Vectores dibujados:</strong> ${campo_vectorial.length}</li>
${mostrar_orbitas ? `<li><strong>Órbitas calculadas:</strong> ${orbitas.length}</li>` : ''}
${mostrar_orbitas ? `<li><strong>Puntos por órbita:</strong> ~${orbitas.length > 0 ? Math.floor(orbitas[0].puntos.length) : 0}</li>` : ''}
${mostrar_orbitas ? `<li><strong>Tiempo de integración:</strong> t ∈ [0, ${tiempo_max}]</li>` : ''}
${mostrar_orbitas ? `<li><strong>Paso de integración:</strong> dt = ${dt_rk4}</li>` : ''}
<li><strong>Puntos de equilibrio:</strong> ${puntos_equilibrio.length}</li>
<li><strong>Dominio:</strong> x ∈ [${x_min_fases}, ${x_max_fases}], y ∈ [${y_min_fases}, ${y_max_fases}]</li>
<li><strong>Método numérico:</strong> Runge-Kutta 4º orden (RK4)</li>
</ul>
</div>
`
```
:::
:::
## Clasificador de Órbitas de Sistemas lineales
::: {.content-visible when-format="html"}
::: {.callout-caution collapse="true" title="Visualizador Interactivo: Traza y Determinante"}
```{ojs}
//| echo: false
//| label: co-visualizador-traza-det
// Controles interactivos (prefijados para evitar colisiones)
viewof co_parametros = Inputs.form({
tau: Inputs.range([-5, 5], {
value: -1,
step: 0.05,
label: "τ (Traza)"
}),
delta: Inputs.range([-2, 5], {
value: 1,
step: 0.05,
label: "Δ (Determinante) "
})
}, {
template: (inputs) => html`
<div style="display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 12px; align-items: center; margin: 6px 0;">
${inputs.tau}
${inputs.delta}
</div>
`
})
co_tau = co_parametros.tau
co_delta = co_parametros.delta
// Clasificar el tipo de equilibrio
co_clasificar = {
const tau = co_tau;
const delta = co_delta;
const discriminante = tau * tau - 4 * delta;
if (delta < 0) {
return {
tipo: "Silla (Inestable)",
color: "#e74c3c",
descripcion: "Punto de silla: estable en una dirección, inestable en otra"
};
} else if (delta === 0) {
return {
tipo: "Degenerado",
color: "#95a5a6",
descripcion: "Caso degenerado: matriz singular"
};
} else if (discriminante > 0) {
if (tau < 0) {
return {
tipo: "Nodo Estable",
color: "#27ae60",
descripcion: "Todas las trayectorias convergen al origen"
};
} else if (tau > 0) {
return {
tipo: "Nodo Inestable",
color: "#e67e22",
descripcion: "Todas las trayectorias divergen del origen"
};
} else {
return {
tipo: "Centro (caso especial)",
color: "#3498db",
descripcion: "Autovalores reales iguales a cero"
};
}
} else if (discriminante < 0) {
if (tau < 0) {
return {
tipo: "Foco Estable (Espiral)",
color: "#16a085",
descripcion: "Trayectorias espirales que convergen al origen"
};
} else if (tau > 0) {
return {
tipo: "Foco Inestable (Espiral)",
color: "#c0392b",
descripcion: "Trayectorias espirales que divergen del origen"
};
} else {
return {
tipo: "Centro",
color: "#3498db",
descripcion: "Órbitas cerradas alrededor del origen"
};
}
} else { // discriminante == 0
if (tau < 0) {
return {
tipo: "Nodo Estable Degenerado",
color: "#27ae60",
descripcion: "Nodo con autovalores repetidos (estable)"
};
} else if (tau > 0) {
return {
tipo: "Nodo Inestable Degenerado",
color: "#e67e22",
descripcion: "Nodo con autovalores repetidos (inestable)"
};
} else {
return {
tipo: "Degenerado",
color: "#95a5a6",
descripcion: "Caso completamente degenerado"
};
}
}
}
// Calcular autovalores
co_autovalores = {
const tau = co_tau;
const delta = co_delta;
const discriminante = tau * tau - 4 * delta;
if (discriminante >= 0) {
const lambda1 = (tau + Math.sqrt(discriminante)) / 2;
const lambda2 = (tau - Math.sqrt(discriminante)) / 2;
return {
lambda1: lambda1.toFixed(3),
lambda2: lambda2.toFixed(3),
tipo: "Reales"
};
} else {
const real = tau / 2;
const imag = Math.sqrt(-discriminante) / 2;
return {
lambda1: `${real.toFixed(3)} + ${imag.toFixed(3)}i`,
lambda2: `${real.toFixed(3)} - ${imag.toFixed(3)}i`,
tipo: "Complejos conjugados"
};
}
}
// Construir matriz ejemplo con traza y determinante dados
co_matriz = {
const tau = co_tau;
const delta = co_delta;
// A = [[a, b], [c, d]] con tr(A) = a+d = tau, det(A) = ad-bc = delta
// Tomamos b=0, c=1 para simplificar
const discriminante = tau * tau - 4 * delta;
if (discriminante >= 0) {
const lambda1 = (tau + Math.sqrt(discriminante)) / 2;
const lambda2 = (tau - Math.sqrt(discriminante)) / 2;
return {
a: lambda1,
b: 0,
c: 1,
d: lambda2
};
} else {
// Forma para autovalores complejos
const alpha = tau / 2;
const beta = Math.sqrt(-discriminante) / 2;
return {
a: alpha,
b: -beta,
c: beta,
d: alpha
};
}
}
// Generar campo vectorial
co_campo = {
const {a, b, c, d} = co_matriz;
const campo = [];
const paso = 0.5;
const limite = 3;
for (let x = -limite; x <= limite; x += paso) {
for (let y = -limite; y <= limite; y += paso) {
const dx = a * x + b * y;
const dy = c * x + d * y;
const mag = Math.sqrt(dx*dx + dy*dy);
if (mag > 0.001) {
const norm_factor = 0.25 / Math.max(mag, 0.25);
campo.push({
x1: x,
y1: y,
x2: x + dx * norm_factor,
y2: y + dy * norm_factor
});
}
}
}
return campo;
}
// Generar órbitas
co_orbitas = {
const {a, b, c, d} = co_matriz;
const tau = co_tau;
const delta = co_delta;
const orbitas = [];
// Condiciones iniciales dependiendo del tipo
let condiciones_iniciales;
if (delta < 0) { // Silla
condiciones_iniciales = [
{x: 2.5, y: 0.1}, {x: -2.5, y: -0.1},
{x: 0.1, y: 2.5}, {x: -0.1, y: -2.5}
];
} else if (Math.abs(tau) < 0.1 && delta > 0) { // Centro
condiciones_iniciales = [
{x: 1.5, y: 0}, {x: 2.5, y: 0}, {x: 0, y: 1.5}
];
} else { // Nodo o foco
condiciones_iniciales = [
{x: 2.5, y: 0}, {x: -2.5, y: 0},
{x: 0, y: 2.5}, {x: 1.5, y: 1.5}
];
}
condiciones_iniciales.forEach(ic => {
const puntos = [];
let x = ic.x, y = ic.y;
const dt = 0.05;
const pasos = tau > 0 ? 350 : 400; // Menos pasos si diverge
for (let i = 0; i < pasos; i++) {
puntos.push({x, y});
// Runge-Kutta 4
const k1_x = a * x + b * y;
const k1_y = c * x + d * y;
const k2_x = a * (x + dt/2 * k1_x) + b * (y + dt/2 * k1_y);
const k2_y = c * (x + dt/2 * k1_x) + d * (y + dt/2 * k1_y);
const k3_x = a * (x + dt/2 * k2_x) + b * (y + dt/2 * k2_y);
const k3_y = c * (x + dt/2 * k2_x) + d * (y + dt/2 * k2_y);
const k4_x = a * (x + dt * k3_x) + b * (y + dt * k3_y);
const k4_y = c * (x + dt * k3_x) + d * (y + dt * k3_y);
x += dt/6 * (k1_x + 2*k2_x + 2*k3_x + k4_x);
y += dt/6 * (k1_y + 2*k2_y + 2*k3_y + k4_y);
// Detener si sale del rango
if (Math.abs(x) > 4 || Math.abs(y) > 4) break;
}
if (puntos.length > 5) {
orbitas.push({puntos, ic});
}
});
return orbitas;
}
// Información del sistema (compacta)
html`<div style="background: linear-gradient(135deg, ${co_clasificar.color}22, ${co_clasificar.color}11);
border-left: 4px solid ${co_clasificar.color};
padding: 12px 15px;
margin: 15px 0;
border-radius: 6px;
box-shadow: 0 1px 4px rgba(0,0,0,0.1);">
<h3 style="margin: 0 0 8px 0; color: ${co_clasificar.color}; font-size: 1.2em;">
${co_clasificar.tipo}
</h3>
<p style="margin: 0 0 10px 0; font-size: 0.95em; color: #2c3e50;">
${co_clasificar.descripcion}
</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div style="background: white; padding: 8px; border-radius: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.08);">
<strong style="color: #7f8c8d; font-size: 0.9em;">Autovalores:</strong><br/>
<span style="color: #2c3e50; font-family: monospace; font-size: 0.85em;">λ₁ = ${co_autovalores.lambda1}</span><br/>
<span style="color: #2c3e50; font-family: monospace; font-size: 0.85em;">λ₂ = ${co_autovalores.lambda2}</span><br/>
<span style="color: #95a5a6; font-size: 0.8em;">(${co_autovalores.tipo})</span>
</div>
<div style="background: white; padding: 8px; border-radius: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.08);">
<strong style="color: #7f8c8d; font-size: 0.9em;">Matriz ejemplo:</strong><br/>
<span style="color: #2c3e50; font-family: monospace; font-size: 0.85em;">
A = [${co_matriz.a.toFixed(2)}, ${co_matriz.b.toFixed(2)}]<br/>
[${co_matriz.c.toFixed(2)}, ${co_matriz.d.toFixed(2)}]
</span>
</div>
</div>
<div style="margin-top: 8px; padding: 6px 8px; background: rgba(255,255,255,0.7); border-radius: 4px; font-size: 0.9em;">
<strong style="color: #7f8c8d;">Discriminante:</strong>
<span style="color: #2c3e50; font-family: monospace;">Δ' = τ² - 4Δ = ${(co_tau*co_tau - 4*co_delta).toFixed(3)}</span>
</div>
</div>`
// Gráfico del diagrama de fases
Plot.plot({
width: 700,
height: 350,
x: {domain: [-4, 4], label: "x"},
y: {domain: [-4, 4], label: "y"},
marks: [
Plot.frame(),
Plot.ruleX([0], {stroke: "#bdc3c7", strokeWidth: 1.5}),
Plot.ruleY([0], {stroke: "#bdc3c7", strokeWidth: 1.5}),
...co_campo.map(v => Plot.arrow([v], {
x1: "x1", y1: "y1", x2: "x2", y2: "y2",
stroke: "#95a5a6", strokeWidth: 1.5, headLength: 6, opacity: 0.6
})),
...co_orbitas.map((orb, i) => Plot.line(orb.puntos, {
x: "x", y: "y",
stroke: co_clasificar.color,
strokeWidth: 2.5,
opacity: 0.8
})),
...co_orbitas.map((orb, i) => Plot.dot([orb.ic], {
x: "x", y: "y",
fill: co_clasificar.color,
r: 5,
stroke: "white",
strokeWidth: 2
})),
Plot.dot([{x: 0, y: 0}], {
fill: co_clasificar.color,
r: 8,
stroke: "white",
strokeWidth: 3
})
],
style: {
background: "#fafafa"
}
})
```
**Diagrama τ-Δ (Traza-Determinante):**
```{ojs}
//| echo: false
//| label: co-diagrama-traza-determinante
{
const tau = co_tau;
const delta = co_delta;
const puntos_fondo = [];
const resolucion = 100;
for (let i = 0; i < resolucion; i++) {
for (let j = 0; j < resolucion; j++) {
const t = -5 + (i / resolucion) * 10;
const d = -1 + (j / resolucion) * 7;
const disc = t*t - 4*d;
let tipo, color;
if (d < 0) {
tipo = "Silla";
color = "#e74c3c";
} else if (d > 0 && disc > 0) {
if (t < 0) {
tipo = "Nodo Estable";
color = "#27ae60";
} else {
tipo = "Nodo Inestable";
color = "#e67e22";
}
} else if (d > 0 && disc < 0) {
if (t < 0) {
tipo = "Foco Estable";
color = "#16a085";
} else if (t > 0) {
tipo = "Foco Inestable";
color = "#c0392b";
} else {
tipo = "Centro";
color = "#3498db";
}
} else {
continue; // Skip la frontera
}
puntos_fondo.push({tau: t, delta: d, tipo, color});
}
}
return Plot.plot({
width: 700,
height: 300,
marginLeft: 60,
marginBottom: 60,
x: {
label: "τ (Traza) →",
domain: [-5, 5],
grid: true
},
y: {
label: "↑ Δ (Determinante)",
domain: [-1, 4.5],
grid: true
},
marks: [
// Fondo de colores según la región
Plot.dot(puntos_fondo, {
x: "tau",
y: "delta",
fill: "color",
r: 3,
opacity: 0.15
}),
// Parábola τ² = 4Δ (frontera entre nodos y focos)
Plot.line(
Array.from({length: 200}, (_, i) => {
const t = -5 + (i / 199) * 10;
return {tau: t, delta: t*t/4};
}),
{
x: "tau",
y: "delta",
stroke: "#34495e",
strokeWidth: 2.5,
strokeDasharray: "5,5"
}
),
// Eje Δ = 0 (frontera silla/otros)
Plot.ruleY([0], {
stroke: "#e74c3c",
strokeWidth: 2,
opacity: 0.6
}),
// Eje τ = 0 (frontera estable/inestable)
Plot.ruleX([0], {
stroke: "#95a5a6",
strokeWidth: 2,
opacity: 0.6
}),
// Punto actual (configuración)
Plot.dot([{tau: tau, delta: delta}], {
x: "tau",
y: "delta",
r: 10,
fill: co_clasificar.color,
stroke: "white",
strokeWidth: 3
}),
// Anillo alrededor del punto actual
Plot.dot([{tau: tau, delta: delta}], {
x: "tau",
y: "delta",
r: 15,
fill: "none",
stroke: co_clasificar.color,
strokeWidth: 2,
opacity: 0.5
}),
// Etiquetas de regiones
Plot.text([
{tau: 0, delta: -0.8, texto: "Silla"},
{tau: -3, delta: 1.2, texto: "Nodo\nEstable"},
{tau: 3, delta: 1.2, texto: "Nodo\nInestable"},
{tau: -2.5, delta: 3.5, texto: "Foco\nEstable"},
{tau: 2.5, delta: 3.5, texto: "Foco\nInestable"},
{tau: 0, delta: 3.5, texto: "Centro"}
], {
x: "tau",
y: "delta",
text: "texto",
fontSize: 12,
fontWeight: "bold",
fill: "#2c3e50",
opacity: 0.7
}),
// Etiqueta de la parábola
Plot.text([{tau: 4, delta: 2.7, texto: "τ² = 4Δ"}], {
x: "tau",
y: "delta",
text: "texto",
fontSize: 10,
fill: "#34495e",
dy: -10
})
],
style: {
background: "#fafafa"
}
});
}
```
**Interpretación del diagrama:**
En el plano $(\tau, \Delta)$, las regiones corresponden a:
- $\Delta < 0$ (región roja): Silla (siempre inestable)
- $\Delta > 0$ y $\tau^2 - 4\Delta > 0$ (región naranja/verde): Nodos (estables si $\tau < 0$, inestables si $\tau > 0$)
- $\Delta > 0$ y $\tau^2 - 4\Delta < 0$ (región azul/roja oscura): Focos/espirales (estables si $\tau < 0$, inestables si $\tau > 0$)
- $\Delta > 0$ y $\tau^2 - 4\Delta = 0$ (parábola punteada): Frontera entre nodos y focos
- $\tau = 0$ y $\Delta > 0$ (eje vertical superior): Centros (órbitas cerradas)
El punto de color indica tu configuración actual, que se mueve al ajustar los sliders de τ y Δ.
:::
:::
## Potenciales y planos de fases
::: {.content-visible when-format="html"}
Este visualizador permite estudiar sistemas mecánicos conservativos de la forma:
$$\frac{d\varphi}{dt} = p, \quad \frac{dp}{dt} = -V'(\varphi)$$
donde $V(\varphi)$ es el potencial y la energía total es $E = \frac{1}{2}p^2 + V(\varphi)$.
::: {.callout-caution collapse="true" title="Visualizador Interactivo: Potencial y Plano de Fases"}
```{ojs}
//| echo: false
//| label: pot-controles-potencial
html`
<details open style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 15px; border: 2px solid #dee2e6;">
<summary style="cursor: pointer; font-size: 1.1em; font-weight: bold; color: #2c3e50; margin-bottom: 15px; user-select: none;">
Configuración del Potencial
</summary>
<div style="padding-top: 10px;">
${viewof pot_funcion}
<div style="margin: 12px 0;"></div>
<div style="display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 12px; align-items: center;">
${viewof pot_param_a}
${viewof pot_param_b}
${viewof pot_param_c}
${viewof pot_param_d}
</div>
<div style="margin: 12px 0;"></div>
${viewof pot_phi_range}
<div style="margin: 12px 0;"></div>
${viewof pot_ayuda}
</div>
</details>
`
viewof pot_funcion = Inputs.textarea({
label: "Potencial V(φ)",
value: "-Math.cos(phi)",
placeholder: "Ejemplo: phi*phi/2, -Math.cos(phi), phi*phi*phi*phi/4 - phi*phi/2",
rows: 2,
width: "100%"
})
viewof pot_param_a = Inputs.range([-5, 5], {
value: 1,
step: 0.1,
label: "Parámetro a"
})
viewof pot_param_b = Inputs.range([-5, 5], {
value: 1,
step: 0.1,
label: "Parámetro b"
})
viewof pot_param_c = Inputs.range([-5, 5], {
value: 0,
step: 0.1,
label: "Parámetro c"
})
viewof pot_param_d = Inputs.range([-5, 5], {
value: 0,
step: 0.1,
label: "Parámetro d"
})
viewof pot_phi_range = Inputs.form({
min: Inputs.range([-10, 10], {
value: -Math.PI,
step: 0.1,
label: "min"
}),
max: Inputs.range([-10, 10], {
value: Math.PI,
step: 0.1,
label: "max"
})
}, {
template: (inputs) => html`
<div style="margin-bottom: 10px;">
<label style="font-weight: bold; display: block; margin-bottom: 6px;">Rango de φ</label>
<div style="display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 12px; align-items: center;">
${inputs.min}
${inputs.max}
</div>
</div>
`
})
viewof pot_ayuda = Inputs.toggle({
label: "Mostrar ejemplos",
value: false
})
```
```{ojs}
//| echo: false
//| label: pot-ayuda-texto
html`${pot_ayuda ? `
<div style="background: #f8f9fa; padding: 15px; border-radius: 5px; margin: 10px 0;">
<h4 style="margin-top: 0;">📚 Ejemplos de potenciales:</h4>
<h5>Sistemas clásicos:</h5>
<ul style="margin: 5px 0;">
<li><strong>Péndulo:</strong> <code>-Math.cos(phi)</code></li>
<li><strong>Oscilador armónico:</strong> <code>phi*phi/2</code></li>
<li><strong>Doble pozo:</strong> <code>phi*phi*phi*phi/4 - phi*phi/2</code></li>
<li><strong>Cúbico:</strong> <code>phi*phi*phi/3 - phi</code></li>
<li><strong>Cuártico:</strong> <code>a*phi*phi*phi*phi + b*phi*phi</code></li>
</ul>
<h5>⚠️ Sintaxis:</h5>
<ul style="margin: 5px 0;">
<li>Usa <code>phi</code> como variable</li>
<li>Parámetros ajustables: <code>a</code>, <code>b</code>, <code>c</code> y <code>d</code></li>
<li>Funciones: <code>Math.sin(phi)</code>, <code>Math.cos(phi)</code>, <code>Math.exp(phi)</code></li>
<li>Potencias: <code>phi*phi</code> o <code>Math.pow(phi, 3)</code></li>
</ul>
</div>
` : ''}`
```
```{ojs}
//| echo: false
//| label: pot-controles-energia
html`
<details style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 15px; border: 2px solid #dee2e6;">
<summary style="cursor: pointer; font-size: 1.1em; font-weight: bold; color: #2c3e50; margin-bottom: 15px; user-select: none;">
Niveles de Energía
</summary>
<div style="padding-top: 10px;">
${viewof pot_energia_manual}
<div style="margin: 12px 0;"></div>
${viewof pot_energias_auto}
<div style="margin: 8px 0;"></div>
${viewof pot_num_energias}
<div style="margin: 12px 0;"></div>
${viewof pot_mostrar_puntos_retorno}
</div>
</details>
`
viewof pot_energia_manual = Inputs.text({
label: "Energías manuales (separadas por punto y coma)",
value: "0.5; 1.0; 1.5",
placeholder: "0; 0.5; 1.0",
width: "100%"
})
viewof pot_energias_auto = Inputs.toggle({
label: "Añadir energías automáticas",
value: true
})
viewof pot_num_energias = Inputs.range([3, 15], {
value: 6,
step: 1,
label: "Número de niveles automáticos",
disabled: !pot_energias_auto
})
viewof pot_mostrar_puntos_retorno = Inputs.toggle({
label: "Mostrar puntos de retorno",
value: true
})
```
```{ojs}
//| echo: false
//| label: pot-funciones-calculo
// Parsear potencial con parámetros
pot_funcion_parseada = {
let expr = pot_funcion.replace(/\ba\b/g, `(${pot_param_a})`);
expr = expr.replace(/\bb\b/g, `(${pot_param_b})`);
expr = expr.replace(/\bc\b/g, `(${pot_param_c})`);
expr = expr.replace(/\bd\b/g, `(${pot_param_d})`);
return expr;
}
// Crear función del potencial
pot_V = {
try {
return new Function('phi', `return ${pot_funcion_parseada};`);
} catch(e) {
return (phi) => 0;
}
}
// Validar potencial
pot_valido = {
try {
const test = pot_V(1);
return isFinite(test);
} catch(e) {
return false;
}
}
// Rangos de φ
pot_phi_min = Math.min(pot_phi_range.min, pot_phi_range.max - 0.1)
pot_phi_max = Math.max(pot_phi_range.max, pot_phi_range.min + 0.1)
// Calcular potencial en el rango
pot_datos_potencial = {
if (!pot_valido) return [];
const datos = [];
const puntos = 300;
for (let i = 0; i <= puntos; i++) {
const phi = pot_phi_min + (i / puntos) * (pot_phi_max - pot_phi_min);
const V = pot_V(phi);
if (isFinite(V)) {
datos.push({phi, V});
}
}
return datos;
}
// Encontrar rango de V para escalar energías
pot_V_min = d3.min(pot_datos_potencial, d => d.V)
pot_V_max = d3.max(pot_datos_potencial, d => d.V)
// Parsear energías manuales y combinar con automáticas
pot_energias = {
const energias = new Set();
// Energías manuales
if (pot_energia_manual && pot_energia_manual.trim() !== '') {
pot_energia_manual.split(';').forEach(e => {
const val = parseFloat(e.trim());
if (isFinite(val)) energias.add(val);
});
}
// Energías automáticas
if (pot_energias_auto && isFinite(pot_V_min) && isFinite(pot_V_max)) {
const rango = pot_V_max - pot_V_min;
for (let i = 1; i <= pot_num_energias; i++) {
const E = pot_V_min + (i / (pot_num_energias + 1)) * rango;
energias.add(E);
}
}
return Array.from(energias).sort((a, b) => a - b);
}
// Calcular trayectorias en el plano de fases
pot_trayectorias = {
if (!pot_valido) return [];
const trayectorias = [];
pot_energias.forEach((E, idx) => {
const num_puntos = 500;
const color = d3.interpolateViridis(idx / Math.max(pot_energias.length - 1, 1));
// Coleccionar todos los puntos y detectar segmentos continuos
const segmentos_pos = [];
const segmentos_neg = [];
const puntos_retorno = [];
let segmento_actual_pos = [];
let segmento_actual_neg = [];
let p_squared_prev = null;
let phi_prev = null;
for (let i = 0; i <= num_puntos; i++) {
const phi = pot_phi_min + (i / num_puntos) * (pot_phi_max - pot_phi_min);
const V = pot_V(phi);
const p_squared = 2 * (E - V);
// Detectar cruce con el potencial (punto de retorno) entre muestras consecutivas
if (p_squared_prev !== null && phi_prev !== null) {
// Si cambiamos de p² > 0 a p² < 0, hay un cruce (fin de región permitida)
if (p_squared_prev > 0 && p_squared < 0) {
// Interpolación lineal para encontrar el punto exacto
const t = p_squared_prev / (p_squared_prev - p_squared);
const phi_retorno = phi_prev + t * (phi - phi_prev);
const punto_retorno = {phi: phi_retorno, p: 0, E, idx};
// Añadir el punto de retorno al final del segmento actual
segmento_actual_pos.push(punto_retorno);
segmento_actual_neg.push(punto_retorno);
puntos_retorno.push(punto_retorno);
}
// Si empezamos un nuevo segmento (de p² < 0 a p² > 0)
if (p_squared_prev < 0 && p_squared > 0) {
const t = -p_squared_prev / (p_squared - p_squared_prev);
const phi_retorno = phi_prev + t * (phi - phi_prev);
const punto_retorno = {phi: phi_retorno, p: 0, E, idx};
// Añadir el punto de retorno al inicio del nuevo segmento
segmento_actual_pos = [punto_retorno];
segmento_actual_neg = [punto_retorno];
puntos_retorno.push(punto_retorno);
}
}
if (p_squared >= -0.001) { // Pequeña tolerancia para errores numéricos
const p = p_squared > 0 ? Math.sqrt(p_squared) : 0;
if (isFinite(p)) {
segmento_actual_pos.push({phi, p, E, idx});
segmento_actual_neg.push({phi, p: -p, E, idx});
}
} else {
// Fin del segmento continuo
if (segmento_actual_pos.length > 1) {
segmentos_pos.push([...segmento_actual_pos]);
segmentos_neg.push([...segmento_actual_neg]);
}
segmento_actual_pos = [];
segmento_actual_neg = [];
}
p_squared_prev = p_squared;
phi_prev = phi;
}
// Añadir el último segmento si existe
if (segmento_actual_pos.length > 1) {
segmentos_pos.push(segmento_actual_pos);
segmentos_neg.push(segmento_actual_neg);
}
if (segmentos_pos.length > 0) {
trayectorias.push({
E,
idx,
segmentos_pos,
segmentos_neg,
puntos_retorno,
color
});
}
});
return trayectorias;
}
```
```{ojs}
//| echo: false
//| label: pot-mensaje-error
html`${!pot_valido ? `
<div style="background: #fff3cd; border: 1px solid #ffc107; padding: 10px; border-radius: 5px; margin: 10px 0;">
⚠️ <strong>Error en el potencial:</strong> Verifica la sintaxis de la función.
</div>
` : ''}`
```
**Gráfico del Potencial:**
```{ojs}
//| echo: false
//| label: pot-grafico-potencial
{
if (!pot_valido) return html`<div style="color: red;">Potencial no válido</div>`;
return Plot.plot({
width: 700,
height: 300,
marginLeft: 60,
x: {
label: "φ",
domain: [pot_phi_min, pot_phi_max],
grid: true
},
y: {
label: "V(φ), E",
grid: true
},
marks: [
// Potencial
Plot.line(pot_datos_potencial, {
x: "phi",
y: "V",
stroke: "#2c3e50",
strokeWidth: 3,
curve: "natural"
}),
// Líneas de energía
...pot_energias.map((E, idx) => {
const color = d3.interpolateViridis(idx / Math.max(pot_energias.length - 1, 1));
return Plot.ruleY([E], {
stroke: color,
strokeWidth: 2,
strokeDasharray: "5,5",
opacity: 0.7
});
}),
// Puntos de retorno
...(pot_mostrar_puntos_retorno ? pot_trayectorias.flatMap(t =>
t.puntos_retorno.map(p => Plot.dot([p], {
x: "phi",
y: () => t.E,
fill: t.color,
r: 5,
stroke: "white",
strokeWidth: 2
}))
) : []),
// Etiquetas de energía
...pot_energias.map((E, idx) => {
const color = d3.interpolateViridis(idx / Math.max(pot_energias.length - 1, 1));
return Plot.text([{E, phi: pot_phi_max}], {
x: "phi",
y: "E",
text: d => `E=${d.E.toFixed(2)}`,
fill: color,
fontSize: 10,
dx: -10,
dy: -8,
fontWeight: "bold"
});
})
],
style: {
background: "#fafafa"
}
});
}
```
**Plano de Fases (φ, p):**
```{ojs}
//| echo: false
//| label: pot-grafico-plano-fases
{
if (!pot_valido) return html`<div style="color: red;">Potencial no válido</div>`;
return Plot.plot({
width: 700,
height: 300,
marginLeft: 60,
x: {
label: "φ",
domain: [pot_phi_min, pot_phi_max],
grid: true
},
y: {
label: "p = dφ/dt",
grid: true
},
marks: [
Plot.frame(),
Plot.ruleX([0], {stroke: "#bdc3c7", strokeWidth: 1}),
Plot.ruleY([0], {stroke: "#bdc3c7", strokeWidth: 1.5}),
// Trayectorias (rama positiva) - múltiples segmentos por energía
...pot_trayectorias.flatMap(t =>
t.segmentos_pos.map(seg => Plot.line(seg, {
x: "phi",
y: "p",
stroke: t.color,
strokeWidth: 2.5,
curve: "natural"
}))
),
// Trayectorias (rama negativa) - múltiples segmentos por energía
...pot_trayectorias.flatMap(t =>
t.segmentos_neg.map(seg => Plot.line(seg, {
x: "phi",
y: "p",
stroke: t.color,
strokeWidth: 2.5,
curve: "natural"
}))
),
// Puntos de retorno
...(pot_mostrar_puntos_retorno ? pot_trayectorias.flatMap(t =>
t.puntos_retorno.map(p => Plot.dot([p], {
x: "phi",
y: "p",
fill: t.color,
r: 6,
stroke: "white",
strokeWidth: 2
}))
) : [])
],
style: {
background: "#fafafa"
}
});
}
```
```{ojs}
//| echo: false
//| label: pot-informacion
html`
<div style="background: #e7f3ff; padding: 15px; border-radius: 5px; margin-top: 20px;">
<h4 style="margin-top: 0;">Información del sistema:</h4>
<ul style="margin: 10px 0;">
<li><strong>Potencial:</strong> <code>V(φ) = ${pot_funcion}</code></li>
<li><strong>Rango de φ:</strong> [${pot_phi_min.toFixed(2)}, ${pot_phi_max.toFixed(2)}]</li>
<li><strong>Rango de V:</strong> [${pot_V_min?.toFixed(3)}, ${pot_V_max?.toFixed(3)}]</li>
<li><strong>Niveles de energía mostrados:</strong> ${pot_energias.length}</li>
<li><strong>Ecuación de movimiento:</strong> E = ½p² + V(φ)</li>
<li><strong>Sistema dinámico:</strong> dφ/dt = p, dp/dt = -V'(φ)</li>
<li><strong>⚙️ Info técnica:</strong>
<ul style="margin-top: 5px;">
<li>Ancho del dominio: ${(pot_phi_max - pot_phi_min).toFixed(2)}</li>
<li>Espaciado de muestreo: Δφ ≈ ${((pot_phi_max - pot_phi_min) / 500).toFixed(4)}</li>
<li>Total de segmentos dibujados: ${pot_trayectorias.reduce((sum, t) => sum + t.segmentos_pos.length + t.segmentos_neg.length, 0)}</li>
<li>Segmentos por energía: ${pot_trayectorias.map(t => `E=${t.E.toFixed(2)}: ${t.segmentos_pos.length} región(es)`).join('; ')}</li>
</ul>
</li>
</ul>
<p style="margin: 10px 0; font-size: 0.9em; color: #555;">
<strong>Interpretación:</strong> Las curvas en el plano de fases muestran cómo evoluciona el sistema para cada nivel de energía.
Los puntos de retorno (donde p = 0) indican los límites del movimiento. Las órbitas cerradas corresponden a movimiento periódico (oscilaciones),
mientras que las órbitas abiertas indican escape o movimiento no acotado.
</p>
<p style="margin: 10px 0; font-size: 0.85em; color: #e67e22;">
<strong>⚠️ Nota:</strong> Si el dominio de φ es muy grande y hay regiones permitidas pequeñas, pueden no detectarse por muestreo insuficiente.
Reduce el rango de φ para mejor resolución.
</p>
</div>
`
```
:::
:::