JavaScript must be enabled to play.
Browser lacks capabilities required to play.
Upgrade or switch to another browser.
Loading…
<div class="titulo-principal"> <h1 >🔥🔥🌳</h1> <h1 >Edén Ígneo</h1> <h1 >🌳🔥🔥</h1> </div> <h2 style="text-align: center;">Modelos y simulaciones de incendios forestales</h2> <p style="text-align: center;">Versión 1.0.0</p> <p class="img-centro"> <img id="arbol-fuego" src="https://www.enacif.unam.mx/wp-content/uploads/2026/01/Arbol_Llamas.png" alt="Arbol en fuego"> </p> <div class="menu-botones"> <h2>[[¡INICIAR!|Preambulo]]</h2> <h2>[[Creadores|Creditos]]</h2> </div> <center> <p>Este software no es apto para menores de edad y personas susceptibles a temas relacionados con la violencia y la muerte.</p> <p></p> <p>D. R.©, 2026, UNAM - CC-BY-NC-SA.</p> <p></p> </center> <img id="logo-enacif" src="https://www.enacif.unam.mx/wp-content/uploads/2025/01/LogosGral_obscuro.png" alt="ENACIF"> <hr>
<h1 class="centered">INCENDIO FORESTAL CON AUTÓMATAS CELULARES</h1> <div class="content-wrapper"> <<button "Instrucciones +/-">> <<set $mostrar to not $mostrar>> <<replace "#Instrucciones">> <<if $mostrar>> <h2>📘 Instrucciones de uso del simulador</h2> <p> Este simulador permite explorar de forma interactiva la dinámica de los incendios forestales. </p> <h3>🧭 1. Configuración inicial</h3> <ul> <li>Selecciona el <strong>tamaño de la zona forestal</strong> (30×30, 60×60, 90×90 o 360×360).</li> <li>Ajusta los parámetros:</li> <ul> <li>🌱 <strong>Crecimiento (p%)</strong>: probabilidad de aparición de nuevos árboles.</li> <li>⚡ <strong>Rayo (f%)</strong>: probabilidad de ignición espontánea.</li> </ul> <li>Observa el valor del <strong>parámetro de control λ = p / f</strong>, que se actualiza automáticamente.</li> </ul> <h3>💨 2. Condiciones ambientales</h3> <ul> <li>Selecciona la <strong>dirección del viento</strong>: Norte, Sur, Este, Oeste o sin viento.</li> <li>El viento favorece la propagación del fuego en la dirección elegida.</li> </ul> <h3>🎞️ 3. Ejecución</h3> <ul> <li>▶ <strong>Iniciar</strong>: comienza la simulación.</li> <li>⏹ <strong>Detener</strong>: pausa la evolución del sistema.</li> <li>🔄 <strong>Reset</strong>: reinicia el bosque.</li> <li>Ajusta la <strong>velocidad de reproducción</strong> (1, 5, 10 o 20 FPS).</li> </ul> <h3>🔥 4. Interacción</h3> <ul> <li>Puedes <strong>provocar incendios manualmente</strong> haciendo clic sobre celdas con árboles.</li> <li>Esta acción puede realizarse con la simulación en ejecución o en pausa.</li> </ul> <h3>📊 5. Visualización</h3> <ul> <li>El bosque se representa como una cuadrícula de píxeles:</li> <ul> <li>🟩 Árboles</li> <li>🟥 Fuego</li> <li>⬛ Espacios vacíos</li> </ul> <li>La gráfica muestra en tiempo real el <strong>porcentaje de cada estado</strong>.</li> <li>Puedes <strong>exportar la gráfica</strong> como imagen PNG o descargar los datos en formato CSV.</li> </ul> <h3>📌 6. Recomendación</h3> <p> Explora distintos valores de <em>p</em> y <em>f</em>, y compara escenarios con y sin viento para analizar cómo cambia el comportamiento global del sistema. </p> <</if>> <</replace>> <</button>> <div id="Instrucciones"></div> <<button "Detalles de la simulación +/-">> <<set $mostrar to not $mostrar>> <<replace "#Detalles">> <<if $mostrar>> <h1>Un modelo de incendio forestal basado en autómatas celulares</h1> <h2>Introducción</h2> <p > Una de las primeras versiones de un modelo de incendios forestales con <strong>criticidad autoorganizada</strong> fue propuesta por Drossel y Schwabl (1992). En su trabajo, los autores definen un modelo basado en <strong>autómatas celulares</strong> para describir el comportamiento crítico de los incendios forestales. </p> <p> El sistema se representa mediante una cuadrícula con <em>N = L<sup>d</sup></em> celdas, donde <em>L</em> es la longitud del lado de la malla y <em>d</em> su dimensión. Cada celda puede encontrarse en uno de tres estados posibles:</p> <ul> <li>Ocupada por un árbol</li> <li>En combustión</li> <li>Vacía</li> </ul> <p> La dinámica del modelo está gobernada por cuatro reglas que se aplican de forma simultánea: </p> <div class="modelo-flex"> <div class="modelo-texto"> <ol class="modelo-lista"> <li>En una celda vacía puede crecer un árbol con una probabilidad <em>p</em>.</li> <li>Un árbol puede incendiarse espontáneamente con una probabilidad <em>f</em>.</li> <li>Un árbol se quema si al menos una de sus celdas vecinas está en llamas.</li> <li>Una celda en combustión se vacía tras quemarse.</li> </ol> </div> <div class="modelo-figura"> <figure> <img class="modelo-img" src="https://www.enacif.unam.mx/wp-content/uploads/2026/01/Step_EdenIgneo.jpeg" alt="Ejemplo de simulación de incendio forestal"> <figcaption><strong>Figura 1:</strong> Esquema del cambio de estados de una vecindad de Von Neumann.</figcaption> </figure> </div> </div> <p> </p> <p> La simulación emplea la <strong>vecindad de Von Neumann</strong>, que considera únicamente las cuatro celdas adyacentes ortogonales para calcular el siguiente estado de la cuadrícula. La figura 1 de la cuadricula muestra como es la dinámica para una serie de cuadros. </p> <h2>Interpretación de parámetros</h2> <p>En el modelo de Drossel–Schwabl, los parámetros <em>p</em> y <em>f</em> son <strong>parámetros probabilísticos definidos a nivel local</strong>, es decir, <strong>por celda y por paso temporal del modelo</strong>. El parámetro</p> <p style="text-align: center;"><em>p</em> = <em>P</em>(celda vacía → árbol)</p> <p>representa la probabilidad de que, en un paso de simulación, una celda vacía genere un nuevo árbol. En cada iteración, <strong>cada celda vacía se evalúa de manera independiente</strong>, y tiene probabilidad <em>p</em> de cambiar de estado. De forma análoga, el parámetro</p> <p style="text-align: center;"><em>f</em> = <em>P</em>(árbol → fuego)</p> <p>indica la probabilidad de que un árbol se incendie de manera espontánea durante un paso del modelo. Este término representa <strong>causas externas de ignición</strong>, como descargas eléctricas, chispas o actividades humanas, y no depende del contacto directo con otras celdas en llamas. </p> <p>Ambos parámetros están definidos sobre el tiempo discreto del autómata: </p> <p style="text-align: center;"><em>p, f</em> ∈ [0,1]; por iteración</p> <p>Es importante destacar que <strong>no se trata de tiempo físico real</strong>, sino de tiempo del modelo: un paso. Por ello, el sistema es adimensional: cada iteración representa un paso abstracto de evolución, no segundos, minutos ni horas.</p> <p>Por ejemplo, si se define</p> <p style="text-align: center;"><em>p =</em> 0.01 ∼ 1%</p> <p>esto significa que en cada paso del modelo, cada celda vacía tiene un 1 % de probabilidad de cambiar su estado y convertirse en árbol. El mismo razonamiento aplica para el parámetro <em>f</em> en el caso de los árboles. </p> <p>Estas probabilidades se aplican por celda, por paso y de forma independiente tanto del resto de las celdas como de iteraciones anteriores. </p> <p>Como ejemplo, si el sistema contiene 10 000 celdas vacías y se establece <em>p = 0.01</em>, el valor esperado del número de nuevos árboles en ese paso es</p> <p style="text-align: center;"><em>E</em>[nuevos árboles] = 10,000 X 0.01 = 100</p> <p>Entonces, se esperan alrededor de la centena de nuevos árboles. Recuerda es una probabilidad.</p> <p>Finalmente, <strong>aunque <em>p</em> y <em>f</em> actúan localmente, el comportamiento global del sistema no depende de sus valores absolutos de manera aislada</strong>, sino principalmente de la razón entre ambos parámetros: </p> <p style="text-align: center;"> λ = <em>p/f</em></p> <p>Esta relación controla el equilibrio entre crecimiento de vegetación e ignición, y determina los distintos regímenes dinámicos observados en el modelo. </p> <h2>Observaciones adicionales</h2> <p>El modelo alcanza un comportamiento crítico en el límite <em>f → 0</em>.</p> <p> Debido a las limitaciones computacionales, la simulación en tiempo real solo puede ejecutarse en cuadrículas pequeñas, lo que dificulta la observación de ciertos comportamientos característicos del modelo. </p> <p>En simulaciones realizadas en cuadriculas superiores a <strong>300×300 celdas,</strong> el estado estacionario del sistema se alcanza para valores de <em>p / f = 10</em>. En contraste, se observa la aparición de grandes estructuras espirales cuando <em>p /f = 200</em>.</p> <h2>Referencias</h2> <ul> <li> Drossel, B. & Schwabl, F. (1992). <em>Self-organized critical forest-fire model</em>. Physical Review Letters, 69(11), 1629–1632. </li> <li>Peredo, M. J., & Ramallo, R. (2002). Aplicación de autómatas celulares a simulación básica de incendios forestales. Acta Nova, 1(4), 347-361. </li> <li>Corredor Llano, X. (2017). Desarrollo de un modelo para la dispersión del fuego en la Orinoquía Colombiana usando autómatas celulares (Doctoral dissertation). </li> <li> Wikibooks. (s. f.). <em>Waldbrandsimulation</em>. <a href="https://de.wikibooks.org/wiki/Waldbrandsimulation" target="_blank"> En <em>Wikibooks, colección de libros educativos de acceso abierto</em>.</a>. </li> </ul> <figure> <img src="https://www.enacif.unam.mx/wp-content/uploads/2026/01/ClusterEdenIgneo.png" alt="Graficas de conglomerados incendios" class="modelo-img" > <figcaption><strong>Figura 2:</strong> Gráficas del número promedio de conglomerados N y el radio promedio de los conglomerados R en función del tamaño del conglomerados (Drossel, 1992).</figcaption> </figure> <</if>> <</replace>> <</button>> <div id="Detalles"></div> <<button "Actividad Didáctica +/-">> <<set $mostrar to not $mostrar>> <<replace "#Preguntas">> <<if $mostrar>> <h1>Secuencia didáctica: exploración sistemática de un modelo de incendio forestal</h1> <p>En esta actividad se propone que el estudiante explore, de manera controlada y progresiva, el comportamiento de un modelo de incendio forestal. El objetivo es identificar cómo los parámetros de crecimiento de la vegetación y de ignición influyen en la estabilidad del sistema, el tamaño de los incendios y los patrones espaciales que emergen.</p> <p>Durante toda la actividad, el estudiante debe guardar capturas de la gráfica temporal y del bosque de píxeles, así como anotar los valores de los parámetros utilizados y las observaciones más relevantes. Con este material deberá elaborar, al final, un informe breve que incluya descripciones, comparaciones y explicaciones fundamentadas.</p> <p>El informe incluirá: introducción, descripción de los experimentos realizados, capturas seleccionadas, análisis comparativo de resultados y conclusiones. El énfasis debe ponerse en la interpretación de los patrones observados y en la relación entre parámetros, dinámica temporal y estructura espacial del sistema.</p> <p><strong>1. Crecimiento de árboles sin incendios significativos</strong></p> <p>Inicia la simulación sin viento, con una malla de 90 × 90 celdas y una tasa de actualización de 20 FPS. Mantén fijo el parámetro de ignición en f% = 1% y modifica únicamente la probabilidad de crecimiento de árboles p%.</p> <p>Explora sucesivamente los valores p% = 1, 5, 9 y 9.5. Observa la evolución temporal del porcentaje total de árboles en la gráfica. Describe si el sistema tiende a un valor estable o si presenta oscilaciones. Reflexiona sobre qué significa, desde el punto de vista del modelo, que la cobertura forestal se estabilice o fluctúe.</p> <p><strong>2. Variación de la ignición y relación con el tamaño de los incendios</strong></p> <p>Mantén ahora constante el valor máximo de crecimiento, p% = 10%, y continúa sin viento ni cambios en la malla. Modifica el parámetro de ignición en la secuencia f% = 10, 0.1 y 0.001%.</p> <p>Estos cambios implican que el parámetro λ (relación entre crecimiento e ignición) pasa de valores cercanos a 100 hasta valores del orden de 10 000. Observa cómo cambia el número total de árboles y el tamaño típico de los incendios. Analiza si el sistema alcanza un estado estacionario y discute si los incendios tienden a ser más extensos cuando λ es grande en comparación con cuando es pequeño.</p> <p><strong>3. Disminución simultánea del crecimiento y la ignición</strong></p> <p>Conserva f% = 0.001% y reduce el crecimiento a p% = 1%. Compara los resultados con los casos anteriores. Identifica qué cambia en la gráfica temporal y en la estructura espacial del bosque. Reflexiona sobre la interacción entre crecimiento lento e ignición poco frecuente.</p> <p>A continuación, reduce aún más el crecimiento a p% = 0.1%. Describe las diferencias observadas tanto en la gráfica como en el bosque de píxeles. Considera si el sistema parece más frágil, más disperso o menos capaz de sostener incendios extensos.</p> <p><strong>4. Incendios iniciados de manera intencional</strong></p> <p>Define ahora un escenario sin viento, con p% = 1% y f% = 0%. Permite que el bosque de píxeles crezca hasta estar casi completamente cubierto. Antes de que se llene por completo, utiliza el cursor para iniciar un incendio en una zona central.</p> <p>Describe detalladamente cómo se propaga el incendio: su forma, su velocidad y las regiones que logra consumir. Explica por qué el frente de fuego adopta esa geometría, relacionándolo con la estructura del bosque y las reglas locales del modelo.</p> <p><strong>5. Influencia del viento en la propagación</strong></p> <p>Repite la experiencia anterior, pero ahora define un viento dirigido hacia el norte. Compara la propagación del incendio con el caso sin viento. Analiza cómo cambia la forma del frente de fuego y qué implicaciones tiene esto para la interpretación del viento como un factor direccional en el modelo.</p> <p><strong>6. Efectos del tamaño del sistema</strong></p> <p>Aumenta el tamaño del bosque al valor máximo permitido, 360 × 360, sin viento. Define f% = 0.001% y p% = 1%, y observa el comportamiento del sistema. Posteriormente, incrementa el crecimiento a p% = 5%.</p> <p>Compara ambos casos atendiendo a la gráfica temporal y al patrón espacial de los incendios. Identifica diferencias en la frecuencia, extensión y conectividad de las áreas quemadas.</p> <p>Finalmente, reduce el tamaño del bosque a 30 × 30 celdas y repite las observaciones con los mismos valores de parámetros, primero sin viento y luego con viento hacia el norte. Reflexiona sobre cómo el tamaño del sistema condiciona los resultados y qué limitaciones introduce un dominio pequeño.</p> <p><strong>7. Observaciones avanzadas y patrones emergentes</strong></p> <p>Explora libremente combinaciones de parámetros para investigar si es posible observar incendios que, al propagarse, se encuentren con otros incendios activos o con múltiples focos simultáneos. Describe las formas que adoptan estos incendios: ¿presentan estructuras ramificadas, frentes irregulares o patrones que recuerdan a espirales u otras figuras conocidas?<p> <p>Trata de responder a las preguntas. ¿Por qué no se definieron los valores de p% o de f% superiores de 80%? ¿Cuál es el valor más grande de incendios activos? ¿Por qué no se puede superar esa cifra? <p>Como actividad complementaria, intenta relacionar estas formas con conceptos de física estadística o sistemas complejos, como percolación, autoorganización o criticalidad.</p> <h2>CUESTIONARIO: MODELO DE INCENDIOS FORESTALES</h2> <p>Cómo refuerzo a la actividad, selecciona la mejor respuesta a cada pregunta.</p> <<set $correctas = 0>> <p><strong>1.</strong> Si aumenta la probabilidad de inicio de incendios, ¿qué ocurre con el número de incendios? <<listbox "$q1">> <<option " ">> <<option "Aumenta">> <<option "Disminuye">> <<option "Permanece igual">> <<option "No hay relación clara">> <</listbox>> </p> <p><strong>2.</strong> En este simulador, ¿cómo debe interpretarse la dirección del viento? <<listbox "$q2">> <<option " ">> <<option "Indica hacia dónde empuja el fuego">> <<option "Indica de dónde proviene">> <<option "Cambia aleatoriamente">> <<option "Es solo decorativa">> <</listbox>> </p> <p><strong>3.</strong> Al aumentar la densidad de árboles, la percolación del fuego: <<listbox "$q3">> <<option " ">> <<option "No cambia">> <<option "Solo depende del viento">> <<option "Disminuye la percolación">> <<option "Aumenta la probabilidad de percolación">> <</listbox>> </p> <p><strong>4.</strong> Al comparar una malla pequeña (30×30) con una grande (360×360), ¿qué ocurre con la variabilidad entre simulaciones? <<listbox "$q4">> <<option " ">> <<option "Es la misma">> <<option "Disminuye en la malla grande">> <<option "No es relevante">> <<option "Aumenta en la malla grande">> <</listbox>> </p> <p><strong>5.</strong>¿qué efecto tiene aumentar los FPS? <<listbox "$q5">> <<option " ">> <<option "Aumentan los incendios">> <<option "Ninguno en la dinámica, solo visual">> <<option "Reduce el número de incendios">> <<option "Vuelve inestable el modelo">> <</listbox>> </p> <p><strong>6.</strong> ¿qué representa λ en el modelo de incendios? <<listbox "$q6">> <<option " ">> <<option "Un contador de incendios">> <<option "Un parámetro de transición del sistema">> <<option "Una probabilidad fija sin efecto global">> <<option "Un valor gráfico de la simulación">> <</listbox>> </p> <p><strong>7.</strong> Si los incendios atraviesan el dominio con frecuencia, ¿qué describe mejor el régimen del sistema? <<listbox "$q7">> <<option " ">> <<option "λ bajo (subcrítico percolante)">> <<option "λ alto (régimen percolante)">> <<option "Régimen puramente aleatorio">> <<option "Dominado solo por el viento">> <</listbox>> </p> <p><strong>8.</strong> ¿Para qué sirve conceptualmente descargar los datos en CSV? <<listbox "$q8">> <<option " ">> <<option "Guardar imágenes">> <<option "Acelerar la simulación">> <<option "Analizar tendencias y transiciones">> <<option "Reemplazar la visualización">> <</listbox>> </p> <p><strong>9.</strong>Si la probabilidad de ignición es extremadamente baja, ¿qué comportamiento es más probable? <<listbox "$q9">> <<option " ">> <<option "Incendios masivos">> <<option "Incendios infrecuentes y pequeños">> <<option "Todo el bosque se quema">> <<option "No depende de la ignición">> <</listbox>> </p> <p><strong>10.</strong> ¿Qué ventaja conceptual tiene usar distintos tamaños de malla? <<listbox "$q10">> <<option " ">> <<option "Comparar escala y efectos de tamaño finito">> <<option "Mejorar el rendimiento de la simulación">> <<option "Eliminar el azar en la dinámica">> <<option "Evitar el análisis estadístico">> <</listbox>> </p> <center> <<button "Evaluar respuestas">> <<set $correctas = 0>> <<if $q1 is "Aumenta">><<set $correctas += 1>><</if>> <<if $q2 is "Indica hacia dónde empuja el fuego">><<set $correctas += 1>><</if>> <<if $q3 is "Aumenta la probabilidad de percolación">><<set $correctas += 1>><</if>> <<if $q4 is "Disminuye en la malla grande">><<set $correctas += 1>><</if>> <<if $q5 is "Ninguno en la dinámica, solo visual">><<set $correctas += 1>><</if>> <<if $q6 is "Un parámetro de transición del sistema">><<set $correctas += 1>><</if>> <<if $q7 is "λ alto (régimen percolante)">><<set $correctas += 1>><</if>> <<if $q8 is "Analizar tendencias y transiciones">><<set $correctas += 1>><</if>> <<if $q9 is "Incendios infrecuentes y pequeños">><<set $correctas += 1>><</if>> <<if $q10 is "Comparar escala y efectos de tamaño finito">><<set $correctas += 1>><</if>> <<replace "#resultado">> <<if $correctas is 10>> <p>✅ Excelente: dominio conceptual completo del modelo. Todas tus respuestas son correctas.</p> <<elseif $correctas >= 7>> <p>🟡 Buen desempeño: conceptos clave bien entendidos. Tienes siete o más respuestas correctas.</p> <<else>> <p>🔴 Revisa los conceptos de percolación y dinámica. Tienes menos de siete respuestas correctas.</p> <</if>> <</replace>> <</button>> <div id="resultado"></div> </center> <</if>> <</replace>> <</button>> <div id="Preguntas"></div> </div> <hr> <p></p> <div id="layout"> <div id="controlsPanel"> <h3 class="centered">Parámetros</h3> <p><strong>🌱 Crecimiento (p%):</strong> <input id="growNumber" type="number" min="0" max="10" step="0.001" value="1.00" oninput="syncGrowFromNumber(this.value)"> <input id="growRange" type="range" min="0" max="10" step="0.001" value="1.00" oninput="syncGrowFromRange(this.value)"> </p> <p><strong>⚡ Rayo (f%): </strong> <input id="fireNumber" type="number" min="0" max="10" step="0.001" value="1.00" oninput="syncFireFromNumber(this.value)"> <input id="fireRange" type="range" min="0" max="10" step="0.001" value="1.00" oninput="syncFireFromRange(this.value)"></p> <p class="centered"> <strong>λ = p / f = <span id="lambdaVal">Parámetro de control</span></strong> </p> <strong> 💨Viento:</strong> <select onchange="FOREST.WIND=this.value; drawWindRose();"> <option value="N">Norte</option> <option value="S">Sur</option> <option value="E">Este</option> <option value="W">Oeste</option> <option value="none">Sin viento</option> </select> <p></p> <label for="forestSize"><strong>📐 Zona forestal (px): </strong></label> <select id="forestSize" onchange="setForestSize(this.value)"> <option value="xlarge">360 × 360</option> <option value="large">90 × 90</option> <option value="medium">60 × 60</option> <option value="small" selected >30 × 30</option> </select> <p></p> <strong>🎞️ Reproducción (FPS):</strong> <select onchange="setFPS(parseInt(this.value))"> <option value="20">20</option> <option value="10">10</option> <option value="5" selected>5</option> <option value="1">1</option> </select> <p></p><hr> <h3 class="centered">Acciones</h3> <p><button onclick="startForest()">▶ Iniciar</button> <button onclick="stopForest()">⏹ Detener</button> <button onclick="resetForest()">🔄 Reset</button> <button onclick="exportChart()">💾📈PNG</button> <button onclick="exportCSV()">💾📉 CSV (200 pts)</button> </p> <hr> <p class="centered"> <<link "Regresar al menú de simuladores">> <<run stopForest()>> <<goto "Preambulo">> <</link>> </p> </div> <div id="visual-col" style="flex:1; max-width:40vw; display:none;"> <p class="centered">Gráfica (Árbol 🟩, Fuego 🟥, Vacío ⬛)</p> <div id="stats"></div> <canvas id="chart" ></canvas> <div id="forestWithWind"> <center><strong>Bosque de píxeles</strong ></center> <canvas id="forestCanvas"></canvas> <canvas id="windRose"></canvas> </div> <canvas id="clusterChart"></canvas> </div> </div> <p></p> <hr> <p class="centered"> <<link "Regresar al inicio">> <<run stopForest()>> <<goto "StoryInit">> <</link>> </p> <<script>> /* ========================================================= CONFIGURACIÓN GLOBAL ========================================================= */ window.FOREST = { WIDTH: 30, HEIGHT: 30, TREE: 1, FIRE: 2, EMPTY: 0, INITIAL_TREE_DENSITY: 0.6, GROW_CHANCE: 0.01, FIRE_CHANCE: 0.01, WIND: "none", CELL_SIZE: null, // ya no es fija CANVAS_SIZE: 720, // tamaño visual único (px) FPS: 1, timer: null, stepCount: 0, grid: [], nextGrid: [], history: { trees: [], fire: [], empty: [], maxPoints: 200 } }; /* ========================================================= CREACIÓN DEL BOSQUE ========================================================= */ window.createForest = function () { FOREST.grid = []; FOREST.stepCount = 0; for (let y = 0; y < FOREST.HEIGHT; y++) { FOREST.grid[y] = []; for (let x = 0; x < FOREST.WIDTH; x++) { FOREST.grid[y][x] = Math.random() < FOREST.INITIAL_TREE_DENSITY ? FOREST.TREE : FOREST.EMPTY; } } }; /* ========================================================= DINÁMICA ========================================================= */ window.stepForest = function () { let trees = 0, fires = 0, empty = 0; FOREST.nextGrid = JSON.parse(JSON.stringify(FOREST.grid)); FOREST.stepCount++; for (let y = 0; y < FOREST.HEIGHT; y++) { for (let x = 0; x < FOREST.WIDTH; x++) { const cell = FOREST.grid[y][x]; if (cell === FOREST.EMPTY) { empty++; if (Math.random() < FOREST.GROW_CHANCE) FOREST.nextGrid[y][x] = FOREST.TREE; } else if (cell === FOREST.TREE) { trees++; if (Math.random() < FOREST.FIRE_CHANCE) FOREST.nextGrid[y][x] = FOREST.FIRE; } else if (cell === FOREST.FIRE) { fires++; let spread = [ [0,-1],[0,1],[-1,0],[1,0] ]; if (FOREST.WIND === "N") spread.push([0,-2]); if (FOREST.WIND === "S") spread.push([0,2]); if (FOREST.WIND === "E") spread.push([2,0]); if (FOREST.WIND === "W") spread.push([-2,0]); for (let [dx,dy] of spread) { const ny = y + dy, nx = x + dx; if ( ny >= 0 && ny < FOREST.HEIGHT && nx >= 0 && nx < FOREST.WIDTH && FOREST.grid[ny][nx] === FOREST.TREE ) { FOREST.nextGrid[ny][nx] = FOREST.FIRE; } } FOREST.nextGrid[y][x] = FOREST.EMPTY; } } } FOREST.grid = FOREST.nextGrid; drawForest(); updateStats(trees, fires, empty); updateLambda(); }; /* ========================================================= RENDER BOSQUE (CENTRADO) ========================================================= */ window.drawForest = function () { const canvas = document.getElementById("forestCanvas"); if (!canvas) return; const ctx = canvas.getContext("2d"); const cell = Math.floor( FOREST.CANVAS_SIZE / Math.max(FOREST.WIDTH, FOREST.HEIGHT) ); FOREST.CELL_SIZE = cell; canvas.width = FOREST.CANVAS_SIZE; canvas.height = FOREST.CANVAS_SIZE; canvas.style.display = "block"; canvas.style.margin = "20px auto"; // centrado ctx.clearRect(0, 0, canvas.width, canvas.height); for (let y = 0; y < FOREST.HEIGHT; y++) { for (let x = 0; x < FOREST.WIDTH; x++) { const c = FOREST.grid[y][x]; if (c === FOREST.TREE) ctx.fillStyle = "#166534"; else if (c === FOREST.FIRE) ctx.fillStyle = "#dc2626"; else ctx.fillStyle = " #000000"; ctx.fillRect(x * cell, y * cell, cell, cell); } } }; /* ========================================================= CLICK = RAYO ========================================================= */ function bindForestClick() { const canvas = document.getElementById("forestCanvas"); if (!canvas) return; canvas.onclick = function (e) { const rect = canvas.getBoundingClientRect(); const x = Math.floor((e.clientX - rect.left) / FOREST.CELL_SIZE); const y = Math.floor((e.clientY - rect.top) / FOREST.CELL_SIZE); if ( x >= 0 && x < FOREST.WIDTH && y >= 0 && y < FOREST.HEIGHT && FOREST.grid[y][x] === FOREST.TREE ) { FOREST.grid[y][x] = FOREST.FIRE; drawForest(); } }; } /* ========================================================= MÉTRICAS Y GRÁFICA ========================================================= */ function updateStats(t, f, e) { const N = FOREST.WIDTH * FOREST.HEIGHT; FOREST.history.trees.push(t / N); FOREST.history.fire.push(f / N); FOREST.history.empty.push(e / N); if (FOREST.history.trees.length > FOREST.history.maxPoints) { FOREST.history.trees.shift(); FOREST.history.fire.shift(); FOREST.history.empty.shift(); } const pt = t / N * 100; const pf = f / N * 100; const pe = 100 - pt - pf; document.getElementById("stats").textContent = `Paso ${FOREST.stepCount} | 🌲 ${pt.toFixed(1)}% | 🔥 ${pf.toFixed(1)}% | ⬜ ${pe.toFixed(1)}%`; drawChart(); } window.drawChart = function () { const canvas = document.getElementById("chart"); if (!canvas) return; const ctx = canvas.getContext("2d"); canvas.width = 700; canvas.height = 400; canvas.style.display = "block"; canvas.style.margin = "20px auto"; const w = canvas.width, h = canvas.height; const ml = 80, mr = 30, mt = 40, mb = 80; ctx.clearRect(0, 0, w, h); /* ===== Ejes ===== */ ctx.strokeStyle = "#000"; ctx.lineWidth = 1; ctx.setLineDash([]); ctx.beginPath(); ctx.moveTo(ml, mt); ctx.lineTo(ml, h - mb); ctx.lineTo(w - mr, h - mb); ctx.stroke(); /* ===== Etiqueta Y ===== */ ctx.save(); ctx.translate(30, h / 2); ctx.rotate(-Math.PI / 2); ctx.font = "14px sans-serif"; ctx.textAlign = "center"; ctx.fillText("Porcentaje del total (%)", 0, 0); ctx.restore(); /* ===== Etiqueta X ===== */ ctx.font = "14px sans-serif"; ctx.textAlign = "center"; ctx.fillText( "Tiempo (ventana móvil de pasos)", (ml + w - mr) / 2, h - 20 ); /* ===== Escala Y (0–100 %) ===== */ ctx.textAlign = "right"; ctx.font = "13px sans-serif"; for (let v = 0; v <= 1.001; v += 0.25) { const y = h - mb - v * (h - mt - mb); ctx.beginPath(); ctx.moveTo(ml - 6, y); ctx.lineTo(ml + 6, y); ctx.stroke(); ctx.fillText(`${Math.round(v * 100)}`, ml - 10, y + 4); } /* ===== Escala X dinámica ===== */ const visible = FOREST.history.maxPoints; const total = FOREST.stepCount; const xMin = Math.max(0, total - visible); const xMax = total; const ticks = 5; ctx.textAlign = "center"; for (let i = 0; i <= ticks; i++) { const frac = i / ticks; const x = ml + frac * (w - ml - mr); const t = Math.round(xMin + frac * (xMax - xMin)); ctx.beginPath(); ctx.moveTo(x, h - mb - 6); ctx.lineTo(x, h - mb + 6); ctx.stroke(); ctx.fillText(t.toString(), x, h - mb + 30); } /* ===== Función de línea ===== */ function line(data, color, width = 2, dashed = false) { ctx.beginPath(); ctx.strokeStyle = color; ctx.lineWidth = width; ctx.setLineDash(dashed ? [6, 6] : []); data.forEach((v, i) => { const x = ml + (i / (visible - 1)) * (w - ml - mr); const y = h - mb - v * (h - mt - mb); i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); }); ctx.stroke(); ctx.setLineDash([]); // reset } /* ===== Series ===== */ line(FOREST.history.trees, "#166534", 5); // 🌲 Árboles (gruesa) line(FOREST.history.fire, "#dc2626", 3); // 🔥 Fuego line(FOREST.history.empty, "#000000", 2, true); // ⬜ Vacío (punteada) }; /* ========================================================= SINCRONIZACIÓN DE CONTROLES ========================================================= */ window.syncGrowFromRange = function (v) { const percent = parseFloat(v); FOREST.GROW_CHANCE = percent / 100; document.getElementById("growNumber").value = percent.toFixed(2); updateLambda(); }; window.syncGrowFromNumber = function (v) { const percent = Math.min(10, Math.max(0, parseFloat(v))); FOREST.GROW_CHANCE = percent / 100; document.getElementById("growRange").value = percent; updateLambda(); }; window.syncFireFromRange = function (v) { const percent = parseFloat(v); FOREST.FIRE_CHANCE = percent / 100; document.getElementById("fireNumber").value = percent.toFixed(2); updateLambda(); }; window.syncFireFromNumber = function (v) { const percent = Math.min(10, Math.max(0, parseFloat(v))); FOREST.FIRE_CHANCE = percent / 100; document.getElementById("fireRange").value = percent; updateLambda(); }; /* ========================================================= CONTROL ========================================================= */ window.startForest = function () { if (FOREST.timer) return; document.getElementById("visual-col").style.display = "block"; updateLambda(); FOREST.history.trees = []; FOREST.history.fire = []; FOREST.history.empty = []; createForest(); drawForest(); bindForestClick(); drawWindRose(); FOREST.timer = setInterval(stepForest, 1000 / FOREST.FPS); }; window.stopForest = function () { clearInterval(FOREST.timer); FOREST.timer = null; }; window.resetForest = function () { stopForest(); updateLambda(); FOREST.history.trees = []; FOREST.history.fire = []; FOREST.history.empty = []; createForest(); drawForest(); drawWindRose(); const ctx = document.getElementById("chart").getContext("2d"); ctx.clearRect(0, 0, 700, 400); }; /* ========================================================= FPS ========================================================= */ window.setFPS = function (fps) { FOREST.FPS = fps; if (FOREST.timer) { stopForest(); startForest(); } }; /* ========================================================= EXPORTAR CSV ========================================================= */ window.exportCSV = function () { let csv = "Paso,Trees,Fire,Empty\n"; for (let i = 0; i < FOREST.history.trees.length; i++) { csv += `${i},${FOREST.history.trees[i]},${FOREST.history.fire[i]},${FOREST.history.empty[i]}\n`; } const link = document.createElement("a"); link.href = "data:text/csv;charset=utf-8," + encodeURI(csv); link.download = "forest_simulation.csv"; link.click(); }; function detectFireClusters() { const visited = Array.from({ length: FOREST.HEIGHT }, () => Array(FOREST.WIDTH).fill(false) ); const clusters = []; const dirs = [ [1,0],[-1,0],[0,1],[0,-1] ]; for (let y = 0; y < FOREST.HEIGHT; y++) { for (let x = 0; x < FOREST.WIDTH; x++) { if ( FOREST.grid[y][x] === FOREST.FIRE && !visited[y][x] ) { let size = 0; const stack = [ [x, y] ]; visited[y][x] = true; while (stack.length) { const [cx, cy] = stack.pop(); size++; for (let [dx, dy] of dirs) { const nx = cx + dx; const ny = cy + dy; if ( nx >= 0 && nx < FOREST.WIDTH && ny >= 0 && ny < FOREST.HEIGHT && FOREST.grid[ny][nx] === FOREST.FIRE && !visited[ny][nx] ) { visited[ny][nx] = true; stack.push([nx, ny]); } } } clusters.push(size); } } } return clusters; } function updateLambda() { const span = document.getElementById("lambdaVal"); if (!span) return; const p = FOREST.GROW_CHANCE; const f = FOREST.FIRE_CHANCE; let lambda = (f > 0) ? p / f : Infinity; span.textContent = lambda.toFixed(2); // Limpia clases previas span.classList.remove( "lambda-subcritical", "lambda-critical", "lambda-supercritical" ); // Clasificación didáctica if (lambda < 0.9) { span.classList.add("lambda-supercritical"); } else if (lambda <= 1.1) { span.classList.add("lambda-critical"); } else { span.classList.add("lambda-subcritical"); } } window.drawWindRose = function () { const canvas = document.getElementById("windRose"); if (!canvas) return; const ctx = canvas.getContext("2d"); const size = 120; canvas.width = size; canvas.height = size; const cx = size / 2; const cy = size / 2; const r = 45; ctx.clearRect(0, 0, size, size); const arrows = { N: [0, -1], S: [0, 1], E: [1, 0], W: [-1, 0] }; const noWind = FOREST.WIND === "none"; for (let dir in arrows) { const [dx, dy] = arrows[dir]; ctx.beginPath(); ctx.strokeStyle = FOREST.WIND === dir ? "#2563eb" : "#9ca3af"; ctx.lineWidth = 4; ctx.moveTo(cx, cy); ctx.lineTo(cx + dx * r, cy + dy * r); ctx.stroke(); // Punta ctx.beginPath(); ctx.arc(cx + dx * r, cy + dy * r, 5, 0, Math.PI * 2); ctx.fillStyle = ctx.strokeStyle; ctx.fill(); } /* Centro */ ctx.beginPath(); ctx.arc(cx, cy, 4, 0, Math.PI * 2); ctx.fillStyle = "#000"; ctx.fill(); /* Etiqueta estado */ ctx.font = "12px sans-serif"; ctx.textAlign = "center"; ctx.fillStyle = "#374151"; ctx.fillText( noWind ? "Sin viento" : `Viento ${FOREST.WIND}`, cx, size - 6 ); }; $(document).on(":passagerender", function () { drawWindRose(); }); $(document).one(":passagedisplay", function () { drawWindRose(); }); window.setWindSpeed = function (v) { FOREST.WIND_SPEED = parseInt(v, 10); }; window.setForestSize = function (size) { stopForest(); if (size === "small") { FOREST.WIDTH = 30; FOREST.HEIGHT = 30; } if (size === "medium") { FOREST.WIDTH = 60; FOREST.HEIGHT = 60; } if (size === "large") { FOREST.WIDTH = 90; FOREST.HEIGHT = 90; } if (size === "xlarge") { FOREST.WIDTH = 360; FOREST.HEIGHT = 360; } createForest(); drawForest(); drawWindRose(); }; <</script>>
<h1 >INFORMACIÓN DEL PROGRAMA</h1> <div class="content-wrapper"> <<set $mostrar to false>> <<button "Sobre el programa +/-">> <<set $mostrar to not $mostrar>> <<replace "#SobrePrograma">> <<if $mostrar>> <p>Edén ígneo es un programa didáctico que presenta tres simuladores incendios forestales: uno básico, un analítico y multivariante. La reproducción de incendios superficiales es un tema es interesante desde diferentes puntos de vista e intereses, como pueden ser el ecológico, el matemático y también el forense. Este tipo de simulaciones pretende mostrar la dinámica de un incendio en diferentes condiciones, tales como de densidad de vegetación y la presencia de vientos. </p> <p>Como la mayoría de los simuladores académicos y comerciales, estos tres sistemas computacionales utilizan un modelo probabilístico para generar los escenarios y propagar el fuego. No se vincula directamente a ecuaciones de trasmisión de calor ni de características precisas de materiales. No obstante estos modelos son buenos ejemplos para introducirse en el tema de propagación de incendios.</p> <</if>> <</replace>> <</button>> <div id="SobrePrograma"></div> <p></p> <<button "Compatibilidad +/-">> <<set $mostrar to not $mostrar>> <<replace "#Compatibilidad">> <<if $mostrar>> <p>Este programa se realizó en la plataforma Twine 2.11.1, en el motor SugarCube 2.37.3, que es una extensión de Twine. Se ejecuta en navegadores equivalentes a Chromium 1.56.20 o superiores. Programa no requiere instalación previa, no requiere conexión a Internet, pero se recomienda para visualizar algunos elementos estéticos del programa. El software funciona en smartphones, tabletas, laptops, pero se recomienda utilizarlo en computadoras de escritorio para obtener la mejor visualización.</p> <</if>> <</replace>> <</button>> <div id="Compatibilidad" ></div> <p></p> <<button "Aviso de Privacidad +/-">> <<set $mostrar to not $mostrar>> <<replace "#AvisoPrivacidad">> <<if $mostrar>> <p>Este aviso tiene como propósito informarte que nuestro programa no recaba datos personales de modo automático de los usuarios. Respetamos tu privacidad y garantizamos que tu identidad se mantendrá completamente anónima por medio de nuestro sistema. Cualquier información que puedas proporcionar de manera voluntaria será tratada con estricta confidencialidad y utilizada únicamente con fines académicos. Si tienes alguna duda o inquietud respecto a la privacidad de tus datos, por favor, contáctanos a través de nuestros canales de comunicación. Agradecemos tu confianza y esperamos que disfrutes este software.</p> <</if>> <</replace>> <</button>> <div id="AvisoPrivacidad"></div> <p></p> <<button "Equipo de desarrollo +/-">> <<set $mostrar to not $mostrar>> <<replace "#EquipoDesarrollo">> <<if $mostrar>> <center> <p>Idea, programación <em>back-end</em> y <em>front-end</em>, ilustraciones: <strong>Vicente Torres Zúñiga</strong></p> <p> Asesor técnico: <strong>Francisco Javier Piliado Velasco</strong></p> </center> <p></p> <p></p> <</if>> <</replace>> <</button>> <div id="EquipoDesarrollo"></div> <p style="text-align: center">Este software no es apto para menores de edad y personas susceptibles a temas relacionados con la muerte.</p> <p style="text-align: center">D. R.©, 2026, UNAM - CC-BY-NC-SA.</p> <img id="logo-enacif" src="https://www.enacif.unam.mx/wp-content/uploads/2025/01/LogosGral_obscuro.png" alt="ENACIF"> <p style="text-align: center"> Contacto por correo electrónico:<a href="mailto:vicentetorres@enacif.unam.mx">vicentetorres@enacif.unam.mx</a></p> </div> <hr> <p style="text-align: center">[[Regresar al inicio|StoryInit]] </p>
<h1>Modelo probabilístico de incendios en rejilla</h1> <div class="content-wrapper"> <<set $mostrar to false>> <<button "Instrucciones +/-">> <<set $mostrar to not $mostrar>> <<replace "#Instrucciones">> <<if $mostrar>> <h2>🧭 Instrucciones de uso del simulador</h2> <p> El presente simulador permite explorar de manera interactiva la propagación de incendios forestales mediante un modelo probabilístico discretizado en una rejilla. A continuación se describen los pasos y elementos necesarios para su correcta utilización. </p> <h3>🔥 1. Definición de los puntos de ignición</h3> <p> En la cuadrícula del escenario (bosque de píxeles), el usuario debe hacer clic con el cursor sobre la zona donde desea que inicie el incendio. Es posible realizar múltiples clics para establecer distintos puntos iniciales de ignición. </p> <ul> <li>🟧 <strong>Naranja</strong>: zona de influencia del fuego.</li> <li>🔥 <strong>Rojo oscuro</strong>: celdas actualmente en llamas.</li> <li>⬛ <strong>Negro</strong>: áreas ya quemadas.</li> </ul> <p> Una vez definidos los puntos iniciales, se debe presionar el botón <strong>▶ Iniciar</strong> para observar la evolución temporal del incendio. </p> <h3>🖥️ 2. Organización de la interfaz</h3> <p> La interfaz del programa es responsiva y se adapta automáticamente al tamaño de la pantalla: </p> <ul> <li>En monitores amplios, el bosque y la gráfica se muestran al lado derecho.</li> <li>En pantallas más estrechas, estos elementos se reubican en la parte inferior.</li> </ul> <p> El bosque no posee una escala explícita; sin embargo, puede asumirse que cada celda verde representa un árbol con una cobertura horizontal aproximada de entre <strong>1.5 y 2.0 metros</strong>. Bajo esta suposición, una rejilla de <strong>150 × 150 celdas</strong> representa un área estimada de entre <strong>67.5 y 90 km²</strong>. </p> <h3>🌊 3. Presencia de agua y ríos</h3> <p> Cuando el porcentaje de agua es mayor que cero, el sistema genera automáticamente <strong>tres ríos</strong> dentro del mapa. Este recurso visual tiene como finalidad didáctica mostrar cómo las <strong>barreras naturales</strong> influyen en la propagación del fuego. </p> <p> La gráfica asociada representa el porcentaje de cada tipo de elemento en función del número de pasos de la simulación, los cuales se interpretan como una aproximación discreta del tiempo. </p> <h3>🎛️ 4. Configuración del escenario</h3> <p> En el panel izquierdo se concentran los controles principales del simulador. El usuario puede definir los porcentajes de: </p> <ul> <li>💧 Agua (azul)</li> <li>🌲 Árboles viejos (verde oscuro)</li> <li>🌿 Árboles jóvenes (verde claro)</li> <li>⬜ Espacio vacío (gris)</li> </ul> <p> Es indispensable que la suma de estos porcentajes sea exactamente <strong>100 %</strong>. En caso contrario, el sistema impide la generación del escenario y muestra un mensaje de advertencia para ajustar los parámetros. </p> <h3>🌬️ 5. Efecto del viento</h3> <p> El modelo incluye el parámetro <strong>viento</strong> como un factor que rompe la simetría estadística del bosque. Por defecto, este efecto se encuentra desactivado. </p> <p> Al seleccionar una dirección cardinal (norte, este, sur u oeste), la simulación mostrará una <strong>tendencia direccional</strong> en la propagación del incendio. No se incorporan otros factores climáticos o topográficos, con el propósito de mantener un modelo claro, controlable y con enfoque didáctico. </p> <h3>⏱️ 6. Control temporal de la simulación</h3> <p> El deslizador de velocidad permite modificar el intervalo temporal entre pasos de simulación. Su valor predeterminado es de <strong>200 milisegundos por paso</strong>. </p> <p> Los botones disponibles permiten: </p> <ul> <li>▶ Iniciar la simulación.</li> <li>⏸ Pausar la ejecución.</li> <li>⏭ Avanzar paso a paso para análisis detallado.</li> </ul> <h3>💾 7. Exportación de resultados</h3> <p> El simulador permite guardar los resultados en dos formatos: </p> <ul> <li>📄 <strong>CSV</strong>: contiene los porcentajes de cada estado en función del número de pasos.</li> <li>🖼️ <strong>PNG</strong>: imagen del estado final del bosque.</li> </ul> <h3>📊 8. Información y estadísticas</h3> <p> En la parte inferior del panel se presenta información en tiempo real sobre el estado de la simulación, incluyendo: </p> <ul> <li>Número de pasos.</li> <li>Total de árboles.</li> <li>Espacio vacío.</li> <li>Zonas en llamas.</li> <li>Área quemada acumulada.</li> </ul> <p> Al finalizar el proceso, se muestran estadísticas globales como la duración total, el tiempo simulado, la fracción máxima de fuego simultáneo y el paso en el que se alcanza un porcentaje específico de área quemada (50 % por defecto, editable por el usuario). </p> <h3>Colores y estados</h3> <ul> <li> Agua: azul claro <span style="display:inline-block;width:12px;height:12px;background-color:#9fdcff !important;border:1px solid #000;vertical-align:middle;margin-left:8px;"></span> </li> <li> Árbol viejo: verde oscuro <span style="display:inline-block;width:12px;height:12px;background-color:#1f5f1f !important;border:1px solid #000;vertical-align:middle;margin-left:8px;"></span> </li> <li> Árbol joven: verde claro <span style="display:inline-block;width:12px;height:12px;background-color:#7ed957 !important;border:1px solid #000;vertical-align:middle;margin-left:8px;"></span> </li> <li> Vacío: gris claro <span style="display:inline-block;width:12px;height:12px;background-color:#bfbfbf !important;border:1px solid #000;vertical-align:middle;margin-left:8px;"></span> </li> <li> Zona de influencia: naranja <span style="display:inline-block;width:12px;height:12px;background-color:#ffa500 !important;border:1px solid #000;vertical-align:middle;margin-left:8px;"></span> </li> <li> Incendiada: rojo oscuro <span style="display:inline-block;width:12px;height:12px;background-color:#8b0000 !important;border:1px solid #000;vertical-align:middle;margin-left:8px;"></span> </li> <li> Quemada: negro <span style="display:inline-block;width:12px;height:12px;background-color:#000000 !important;border:1px solid #000;vertical-align:middle;margin-left:8px;"></span> </li> </ul> <</if>> <</replace>> <</button>> <div id="Instrucciones"></div> <<set $mostrar to false>> <<button "Detalles de la simulación+/-">> <<set $mostrar to not $mostrar>> <<replace "#Detalles">> <<if $mostrar>> <h1>Modelo probabilístico de propagación de incendios forestales discretizado en rejilla</h1> <h2>Visión general y propósito</h2> <p>El modelo trata el dominio (el bosque de píxeles) como una malla bidimensional finita de celdas <code>(i,j)</code>, cada una con un estado discreto que codifica la condición del combustible y del fuego (por ejemplo: agua, árbol joven, árbol viejo, vacío, incendiado, quemado). En lugar de resolver ecuaciones continuas de transferencia de calor y cinética de combustión, el modelo emplea reglas de transición estocásticas que representan la probabilidad condicional de ignición de una celda dada la configuración local y parámetros ambientales. Si bien esta simulación es didáctica para mostrar el fenómeno de propagación de fuego en bosque, es importante señalar que el tema es más profundo. </p> <h2>Fundamento físico y vínculo con modelos continuos</h2> <p>Físicamente, la propagación del fuego resulta de procesos acoplados: transferencia de calor por radiación, convección y conducción; desecación del combustible; reacción química con dependencia exponencial de la temperatura; y advección por viento. Modelos continuos (ecuaciones de transporte de energía y especies, o formulaciones empíricas como las de Rothermel, o de Scott y Burgan o Prometheu) capturan estas dependencias mediante tasas continuas y parámetros medibles (contenido de humedad, carga de combustible, densidad aparente, entre otras variables).</p> <p>La formulación discreta probabilística es una aproximación de tipo macroscópico: reemplaza los procesos continuos por una tasa o riesgo de ignición que se calcula localmente. Cuando la escala espacial de la celda es mayor que la longitud característica de la llama o de la zona de calentamiento previo, las reglas probabilísticas actúan como modelos efectivos (<em>coarse-grained</em>) que integran física subcelular en parámetros estocásticos.</p> <h3>Conexión matemática (esquema de riesgo/hazard)</h3> <p>Una formulación consistente desde el punto de vista estocástico es definir para cada celda (célula) susceptible un riesgo instantáneo de ignición <code>h_{ij}(t)</code> (tasa de peligro). Si el tiempo discreto del simulador es <code>Δt</code>, la probabilidad de que la celda pase a estado fuego en el paso siguiente es</p> <pre> P[ignite_{ij} at t+Δt | config_t] = 1 - exp( - h_{ij}(t) · Δt ). </pre> <p>Esta expresión proviene de modelar la ignición como un proceso de Poisson con tasa <code>h_{ij}</code>. Una parametrización práctica para <code>h_{ij}</code> es suma de contribuciones de vecinos incendiados ponderadas por factores de combustible, viento y topografía:</p> <pre> h_{ij}(t) = α · Σ_{k∈N(i,j)} w_{k→ij} · F(type_k) · S_{ij}(moisture) · exp( β · (wind · e_{k→ij}) + γ · slope_{k→ij} ). </pre> <p>Definiciones: <code>N(i,j)</code> es la vecindad (por ejemplo, Moore de 8 vecinos), <code>w_{k→ij}</code> un peso geométrico que decae con la distancia o cuenta la posición relativa, <code>F(type_k)</code> la carga/flammabilidad del combustible en el vecino, <code>S_{ij}</code> factor que reduce la tasa por humedad, <code>wind</code> es el vector viento, <code>e_{k→ij}</code> unidad que apunta del vecino hacia la celda y <code>slope</code> el efecto de la ladera. <code>α, β, γ</code> son parámetros a calibrar.</p> <p>Alternativamente, se puede usar una función sigmoide o logística para saturación:</p> <pre> P = 1 / (1 + exp( - (κ0 + κ1 · Σ influence_k + κ2 · controllables ) ) ). </pre> <p>Ambas formulaciones (<em>hazard exponencial</em> o probabilidad logística) son equivalentes en el sentido de proporcionar una transición entre baja y alta probabilidad; la elección depende de la interpretación física que se quiera conservar y de la calibración disponible.</p> <h2>Elección de discretización y escalas</h2> <p>La validez del modelo depende de elección de la resolución espacial <code>Δx</code> y temporal <code>Δt</code>. Reglas prácticas:</p> <p>• <code>Δx</code> debe ser menor o del mismo orden que la longitud característica de transferencia de calor previa a la ignición (<em>flame length, radiative preheating</em>); si es mucho mayor, la celda actúa como un “parche efectivo” con parámetros homogéneos.</p> <p>• <code>Δt</code> debe ser pequeño respecto del tiempo típico de ignición para que la aproximación de proceso de Poisson sea coherente; numéricamente, elegir <code>Δt</code> tal que <code>h_{ij}·Δt ≪ 1</code> en condiciones normales evita saturación y errores de discretización temporal.</p> <h2>Calibración, incertidumbre y validación</h2> <p>Los parámetros (por ejemplo <code>α, β, γ</code>, o coeficientes logísticos) requieren calibración. Procedimientos adecuados:</p> <p>• utilizar datos observacionales (eventos quemados, tasa de avance) y estimar parámetros por máxima verosimilitud agregando el supuesto de independencia condicional entre celdas en pasos sucesivos, o bien emplear métodos bayesianos/ABC cuando la verosimilitud es intractable;</p> <p>• realizar análisis de sensibilidad y ensambles Monte Carlo para cuantificar la incertidumbre;</p> <p>• validar con métricas espacio-temporales: fracción quemada vs. tiempo, velocidad media de frente, distribución de tamaños de manchas y correlaciones espaciales.</p> <h2>Indicadores observables y salidas que debe guardar el estudiante</h2> <p>Para cada experimento conviene almacenar la configuración inicial (aunque no se puede guardar la semilla del generador aleatorio), la evolución temporal del área quemada, campos de estado final, y series temporales de métricas (área quemada, perímetro, número de focos activos). Estas salidas permiten reproducibilidad y análisis estadístico (medias, varianzas, intervalos de confianza entre corridas).</p> <h2>Limitaciones y cuándo emplear modelos más complejos</h2> <p>El modelo discreto es útil para estudiar patrones emergentes y realizar experimentos de tipo “<em>what-if</em>”. No obstante, cuando se requiere predicción operativa o cuantitativa (por ejemplo, planificación de supresión), conviene integrar componentes físicos adicionales:</p> <p>• acoplar un modelo de humedad y de energía (ecuaciones de balance térmico) para representar desecación y combustión;</p> <p>• incorporar topografía detallada y modelos empíricos de velocidad de propagación (por ejemplo la formulación de Rothermel o modelos basados en energía de combustión) para estimar tasas de avance locales;</p> <p>• modelado multi-escala donde celdas pequeñas resuelven dinámica de llama y celdas más grandes agregan comportamiento regional.</p> <h2>Notas finales</h2> <p>El enfoque probabilístico discreto es un puente entre la heurística y la física completa: conserva la interpretabilidad y la eficiencia computacional para explorar propiedades emergentes, pero exige cautela en la interpretación cuantitativa. Comprender la formulación del riesgo, la dependencia funcional de la probabilidad de ignición y la escala de discretización es esencial para usar estas herramientas con rigor científico.</p> <h2>Referencias</h2> <ol> <li> Alexandridis, A., Vakalis, D., Siettos, C. I., & Bafas, G. V. (2008). <em>A cellular automata model for forest fire spread prediction: The case of the wildfire that swept through Spetses Island in 1990</em>. <em>Applied Mathematics and Computation, 204</em>(1), 191–201. <a href="https://doi.org/10.1016/j.amc.2008.06.046" target="_blank" rel="noopener"> https://doi.org/10.1016/j.amc.2008.06.046 </a> </li> <li> Gouveia Freire, J., & Castro DaCamara, C. (2019). <em>Using cellular automata to simulate wildfire propagation and to assist in fire management</em>. <em>Natural Hazards and Earth System Sciences, 19</em>(1), 169–179. <a href="https://doi.org/10.5194/nhess-19-169-2019" target="_blank" rel="noopener"> https://doi.org/10.5194/nhess-19-169-2019 </a> </li> <li> Alexandridis, A., Russo, L., Vakalis, D., Bafas, G. V., & Siettos, C. I. (2011). <em>Wildland fire spread modelling using cellular automata: Evolution in large-scale spatially heterogeneous environments under fire suppression tactics</em>. <em>International Journal of Wildland Fire, 20</em>, 633–647. <a href="https://doi.org/10.1071/WF09119" target="_blank" rel="noopener"> https://doi.org/10.1071/WF09119 </a> </li> <li> White, S. H., Martín del Rey, A., & Rodríguez Sánchez, G. (2005). <em>A cellular automata model for predicting fire spread</em>. <em>WIT Transactions on Ecology and the Environment, 81</em>, 69–80. <a href="https://www.witpress.com/Secure/elibrary/papers/ECO05/ECO05008FU.pdf" target="_blank" rel="noopener"> https://www.witpress.com/Secure/elibrary/papers/ECO05/ECO05008FU.pdf </a> </li> <li> Karafyllidis, I., & Thanailakis, A. (1997). <em>A model for predicting forest fire spreading using cellular automata</em>. <em>Ecological Modelling, 99</em>, 87–97. <a href="https://www.dpi.inpe.br/gilberto/cursos/st-society-2013/Kara1997.pdf" target="_blank" rel="noopener"> https://www.dpi.inpe.br/gilberto/cursos/st-society-2013/Kara1997.pdf </a> </li> <li> Grassberger, P. (2002). <em>Critical behaviour of the Drossel-Schwabl forest fire model</em>. <em>New Journal of Physics, 4</em>, 17. <a href="https://arxiv.org/abs/cond-mat/0202022" target="_blank" rel="noopener"> https://arxiv.org/abs/cond-mat/0202022 </a> </li> <li> Beneduci, R., & Mascali, G. (2023). <em>Forest fire spreading: A nonlinear stochastic model continuous in space and time</em>. arXiv. <a href="https://arxiv.org/abs/2309.00660" target="_blank" rel="noopener"> https://arxiv.org/abs/2309.00660 </a> </li> <li> Russo, L., Russo, P., & Siettos, C. I. (2015). <em>A complex network theory approach for the spatial distribution of fire breaks in heterogeneous forest landscapes for the control of wildland fires</em>. arXiv. <a href="https://arxiv.org/abs/1509.04065" target="_blank" rel="noopener"> https://arxiv.org/abs/1509.04065 </a> </li> <li> Denham, M., & Laneri, K. (2017). <em>Using efficient parallelization in Graphic Processing Units to parameterize stochastic fire propagation models</em>. arXiv. <a href="https://arxiv.org/abs/1701.03549" target="_blank" rel="noopener"> https://arxiv.org/abs/1701.03549 </a> </li> <li> Weinhouse, C., & Augustin, J. (2025). <em>Leveraging cellular automata for real-time wildfire spread modeling in California</em>. arXiv. <a href="https://arxiv.org/abs/2510.09708" target="_blank" rel="noopener"> https://arxiv.org/abs/2510.09708 </a> </li> </ol> <</if>> <</replace>> <</button>> <div id="Detalles"></div> <<button "Actividad Didáctica +/-">> <<set $mostrar to not $mostrar>> <<replace "#Preguntas">> <<if $mostrar>> <h1>Actividad experimental para explorar el simulador probabilístico de incendios</h1> <p>Propósito. Diseñar y ejecutar un conjunto de experimentos controlados que permitan al estudiante relacionar la estructura del paisaje, la configuración de ignición y la presencia de viento con métricas cuantitativas de propagación; aprender técnicas básicas de calibración y análisis estadístico de modelos estocásticos; y elaborar un informe reproducible que documente hipótesis, diseño experimental, resultados y discusión.</p> <h2>Planteamiento general</h2> <p>El laboratorio se organiza en tres bloques: 1) definición de escenarios y hipótesis, 2) diseño experimental y ejecución de réplicas estocásticas, y 3) análisis cuantitativo y redacción del informe. Cada bloque contiene tareas concretas que deben quedar registradas (archivos CSV, PNG y metadatos de la corrida: porcentajes iniciales, coordenadas de ignición, estado del viento y número máximo de pasos).</p> <h2>Bloque 1 — Construcciones de escenarios </h2> <p>El alumno debe configurar al menos cuatro escenarios conceptuales y formular una hipótesis clara para cada uno. Propongan estas configuraciones mínimas:</p> <p>Escenario A (bosque denso): árboles totales 80–90%, agua 0%, vacío 10–20%; ignición central. Hipótesis: alta probabilidad de percolación y gran fracción quemada.</p> <p>Escenario B (llanura dispersa): árboles 20–40%, agua 0–10%; ignición en el borde. Hipótesis: incendios localizados, baja probabilidad de cruzar el dominio.</p> <p>Escenario C (ribera / barrera acuática): árboles 60–70%, agua 10–20% generada por el simulador; ignición en la orilla de un río. Hipótesis: los ríos actúan como cortafuegos y fragmentan manchas quemadas, reduciendo el área final.</p> <p>Escenario D (sensibilidad al tipo de combustible): mantenga cobertura arbórea constante (por ejemplo 70%) pero altere proporción árboles jóvenes/árboles viejos (p. ej. 70/30 vs 30/70). Hipótesis: mayor proporción de árboles jóvenes (menor carga/flammabilidad) reduce la tasa de avance y el área quemada.</p> <p>Adicional: comparar viento desactivado con viento activado en las cuatro direcciones cardinales. Formule hipótesis sobre la asimetría del frente y la velocidad efectiva de avance en dirección con el viento.</p> <h2>Bloque 2 — diseño experimental y ejecución</h2> <p>Variables de interés. Variables manipuladas (controladas) y observables:</p> <p>Variables manipuladas: porcentaje de árboles (densidad de combustible), fracción agua, proporción árboles jóvenes/viejos, posición de ignición (central, borde, esquina), presencia/dirección del viento.</p> <p>Variables observables (salidas a registrar):</p> <ul> <li>fracción de área quemada por paso (serie temporal);</li> <li>tiempo hasta extinción (número de pasos hasta que no hay celdas en llamas);</li> <li>paso en el que se alcanza 50% del área quemada (o indicador que el usuario prefiera);</li> <li>fracción máxima de celdas en fuego simultáneamente;</li> <li>probabilidad de percolación: proporción de réplicas en las que el fuego conecta dos bordes opuestos;</li> <li>mapa final (PNG) y mapa de frecuencia (porcentaje de réplicas en que cada celda fue quemada).</li> </ul> <p>Protocolo de ejecución.</p> <ol> <li>Para cada escenario definido, fije parámetros de creación de mapa (porcentajes deben sumar 100%). </li> <li>Defina un número de réplicas. Recomendación: 10 réplicas mínimo por condición para análisis inicial. Si trabaja en grupo, puede observar entre 50–100 si se busca estimación más estable de probabilidades de percolación.</li> <li>Ejecute todas las réplicas con viento desactivado; luego repita con viento activado en cada dirección cardinal. Si el simulador permite variar la magnitud del viento.</li> </ol> <h2>Bloque 3 — análisis y métricas</h2> <p>Procesamiento de datos. Por cada condición (escenario × viento) calcule:</p> <p>Estadísticos centrales: media y desviación estándar de área quemada final, tiempo hasta extinción y fracción máxima en fuego. </p> <p>Compara si las distribuciones son aproximadamente normales(gaussianas) y la varianza es homogénea. </p> <p>Estimación de velocidad de frente. Desde series temporales y mapas: mida la distancia media recorrida por el frente en la dirección del viento y divida por el tiempo transcurrido hasta alcanzar determinada velocidad de avance. Esto proporciona una velocidad efectiva v_{eff} estimada en metros por paso si asume la escala de celda (ej. Δx ~ 1. 5 m según la instrucción). Reporte tanto en unidades de celdas/paso como en m/s (si consideras que Δt se puede convertir).</p> <h2>Resultados esperados y criterios de interpretación</h2> <p>Interpretación basada en teoría: a densidades de combustible bajas esperará régimen subcrítico con incendios localizados; existe un umbral crítico de densidad por encima del cual la probabilidad de eventos extensos se incrementa abruptamente (percolación). La introducción de viento incrementa la velocidad efectiva de avance en la dirección del viento y aumenta la asimetría del mapa de frecuencia. Barreras acuáticas reducen la conectividad y el área quemada final.</p> <p>Contraste empírico: compare las medias y las probabilidades de percolación entre condiciones; si la hipótesis no se sostiene, discuta posibles razones: insuficiente número de réplicas, escala espacial inapropiada (Δx grande), o parámetros de ignición demasiado bajos/altos que saturan el fenómeno.</p> <h2>Recomendaciones prácticas y reproducibilidad</h2> <p>Guarde: (1) CSV con series temporales por réplica, (2) PNG del estado final por réplica, (3) fichero con metadatos por condición, (4) script o notebook (por ejemplo, Python/R) que implemente todo el procesamiento y las figuras. Incluya tablas resumidas y figuras: curvas medias de área quemada <em>vs. </em> pasos con bandas de incertidumbre, mapas de frecuencia y boxplots comparativos entre condiciones.</p> <h2>CUESTIONARIO: MODELO DE INCENDIOS FORESTALES</h2> <p>Como refuerzo a la actividad, selecciona la mejor respuesta a cada pregunta.</p> <<set $correctas = 0>> <p><strong>1.</strong> En el simulador, ¿qué representa cada celda de la rejilla? <<listbox "$q1">> <<option " ">> <<option "Una porción discreta del terreno con un estado definido">> <<option "Un árbol individual con comportamiento continuo">> <<option "Un píxel sin significado físico">> <<option "Un punto aleatorio sin reglas">> <</listbox>> </p> <p><strong>2.</strong> ¿Cuál es la función principal de hacer clic sobre la cuadrícula antes de iniciar la simulación? <<listbox "$q2">> <<option " ">> <<option "Modificar el tipo de vegetación">> <<option "Definir los puntos iniciales de ignición">> <<option "Cambiar la velocidad de simulación">> <<option "Activar automáticamente el viento">> <</listbox>> </p> <p><strong>3.</strong> Si la suma de porcentajes de agua, árboles y espacio vacío no es 100 %, el simulador: <<listbox "$q3">> <<option " ">> <<option "Normaliza automáticamente los valores">> <<option "Aumenta el número de incendios">> <<option "Impide generar el escenario">> <<option "Ignora el error">> <</listbox>> </p> <p><strong>4.</strong> Conceptualmente, ¿qué representan los ríos generados cuando hay agua en el escenario? <<listbox "$q4">> <<option " ">> <<option "Zonas de ignición preferente">> <<option "Elementos decorativos sin efecto">> <<option "Barreras naturales a la propagación del fuego">> <<option "Regiones con viento intenso">> <</listbox>> </p> <p><strong>5.</strong> ¿Cómo debe interpretarse el eje horizontal de la gráfica asociada a la simulación? <<listbox "$q5">> <<option " ">> <<option "Distancia física real">> <<option "Número de árboles quemados">> <<option "Pasos discretos de tiempo del modelo">> <<option "Velocidad del viento">> <</listbox>> </p> <p><strong>6.</strong> Al activar el viento en una dirección cardinal, ¿qué efecto conceptual se introduce en el modelo? <<listbox "$q6">> <<option " ">> <<option "Ruptura de la simetría espacial de la propagación">> <<option "Aumento uniforme del azar">> <<option "Cambio del tamaño de la malla">> <<option "Eliminación de la probabilidad">> <</listbox>> </p> <p><strong>7.</strong> ¿Qué aspecto del sistema modifica el deslizador de velocidad? <<listbox "$q7">> <<option " ">> <<option "La probabilidad de ignición">> <<option "La densidad de árboles">> <<option "La dirección del viento">> <<option "La duración visual entre pasos de simulación">> <</listbox>> </p> <p><strong>8.</strong> ¿Por qué es útil exportar los resultados en formato CSV? <<listbox "$q8">> <<option " ">> <<option "Para mejorar la resolución gráfica">> <<option "Para acelerar la simulación">> <<option "Para modificar el escenario inicial">> <<option "Para analizar cuantitativamente la evolución del sistema">> <</listbox>> </p> <p><strong>9.</strong> ¿Qué información proporciona la imagen PNG exportada? <<listbox "$q9">> <<option " ">> <<option "La serie temporal completa">> <<option "Los parámetros del modelo">> <<option "El estado final espacial del bosque">> <<option "La semilla del generador aleatorio">> <</listbox>> </p> <p><strong>10.</strong> Desde el punto de vista conceptual, ¿qué permite comparar el uso de distintos escenarios (bosque denso, llanura, ribera)? <<listbox "$q10">> <<option " ">> <<option "La influencia de la estructura del paisaje en la propagación">> <<option "El rendimiento computacional del simulador">> <<option "La precisión gráfica de los colores">> <<option "La resolución de la pantalla">> <</listbox>> </p> <center> <<button "Evaluar respuestas">> <<set $correctas = 0>> <<if $q1 is "Una porción discreta del terreno con un estado definido">><<set $correctas += 1>><</if>> <<if $q2 is "Definir los puntos iniciales de ignición">><<set $correctas += 1>><</if>> <<if $q3 is "Impide generar el escenario">><<set $correctas += 1>><</if>> <<if $q4 is "Barreras naturales a la propagación del fuego">><<set $correctas += 1>><</if>> <<if $q5 is "Pasos discretos de tiempo del modelo">><<set $correctas += 1>><</if>> <<if $q6 is "Ruptura de la simetría espacial de la propagación">><<set $correctas += 1>><</if>> <<if $q7 is "La duración visual entre pasos de simulación">><<set $correctas += 1>><</if>> <<if $q8 is "Para analizar cuantitativamente la evolución del sistema">><<set $correctas += 1>><</if>> <<if $q9 is "El estado final espacial del bosque">><<set $correctas += 1>><</if>> <<if $q10 is "La influencia de la estructura del paisaje en la propagación">><<set $correctas += 1>><</if>> <<replace "#resultado">> <<if $correctas is 10>> <p>✅ Excelente: comprensión completa del funcionamiento conceptual del simulador. Todas tus respuestas son correctas.</p> <<elseif $correctas >= 7>> <p>🟡 Buen desempeño: los conceptos principales están bien comprendidos. Tienes siete o más respuestas correctas.</p> <<else>> <p> 🔴Revisa las instrucciones del simulador y los conceptos básicos del modelo. Tienes menos de siete respuestas correctas.</p> <</if>> <</replace>> <</button>> <div id="resultado"></div> </center> <</if>> <</replace>> <</button>> <div id="Preguntas"></div> </div> <p></p> <hr> <p></p> <div id="sim-wrap" style="display:flex; gap:20px; align-items:flex-start;"> <!-- PANEL DE CONTROL --> <div id="controlsPanel" style="width:380px;"> <h3 class="centered">Parámetros & Controles</h3> <!-- PORCENTAJES: slider + número --> <div style="margin-bottom:8px;"> <label>Agua (%)</label> <div style="display:flex; gap:8px; align-items:center;"> <input id="waterPct" type="range" min="0" max="100" value="10" style="flex:1;"> <input id="waterNum" type="number" min="0" max="100" value="10" style="width:64px;"> </div> </div> <div style="margin-bottom:8px;"> <label>Árboles viejos (%)</label> <div style="display:flex; gap:8px; align-items:center;"> <input id="oldPct" type="range" min="0" max="100" value="25" style="flex:1;"> <input id="oldNum" type="number" min="0" max="100" value="25" style="width:64px;"> </div> </div> <div style="margin-bottom:8px;"> <label>Árboles jóvenes (%)</label> <div style="display:flex; gap:8px; align-items:center;"> <input id="youngPct" type="range" min="0" max="100" value="35" style="flex:1;"> <input id="youngNum" type="number" min="0" max="100" value="35" style="width:64px;"> </div> </div> <div style="margin-bottom:4px;"> <label>Vacío (%)</label> <div style="display:flex; gap:8px; align-items:center;"> <input id="emptyPct" type="range" min="0" max="100" value="30" style="flex:1;"> <input id="emptyNum" type="number" min="0" max="100" value="30" style="width:64px;"> </div> </div> <div style=" margin-top:6px; "> <strong>Suma: <span id="sumPct">100</span>% </strong> <span id="sumWarning" style="color:crimson; display:none;"> — La suma debe ser exactamente 100% para generar el escenario.</span> </div> <label for="windDir">Viento hacia:</label><br> <select id="windDir"> <option value="none">Sin viento</option> <option value="N">Norte</option> <option value="E">Este</option> <option value="S">Sur</option> <option value="W">Oeste</option> </select> <p></p> <label for="speed">Reproducción (ms/paso): </label> <strong><span id="speedVal">200</span></strong> <input type="range" id="speed" min="20" max="500" value="200"> <p></p> <hr> <h3 class="centered">Acciones</h3> <div style="display:flex; gap:6px; flex-wrap:wrap; margin-bottom:8px;"> <button id="generateBtn" title="Generar escenario (requiere suma = 100%)">Generar escenario</button> <button id="resetBtn" title="Restaurar controles a valores por defecto">Reset parámetros</button> <button id="exportBtn">Exportar CSV</button> <button id="exportImgBtn">Exportar PNG</button> </div> <div style="display:flex; gap:6px; margin-bottom:8px;"> <button id="startBtn">▶ Iniciar</button> <button id="pauseBtn">⏸ Pausar</button> <button id="stepBtn">⏭ Paso</button> </div> <p style="margin-top:6px;"> Haz clic en el mapa para iniciar el incendio (2×2 celdas). "Generar escenario" borra la simulación previa. </p> <hr> <p class="centered">[[Ir al menu de simuladores|Preambulo]]</p> </div> <!-- VISUAL (canvas + chart) --> <div id="visual-col" style="flex:1; max-width:40vw;"> <h3 class="centered"> Bosque de píxeles </h3> <canvas id="forest" width="500" height="500" style="display:block; width:100%; height:auto; image-rendering: pixelated; background:#f6f6f6;"></canvas> <h3 class="centered">Estadísticas en Vivo</h3> <div id="metrics" style="line-height:1.4;"> <span style="margin-right:15px;"><strong>Paso:</strong> <span id="metricStep">0</span></span> <span style="margin-right:15px;"><strong>Árboles (neto):</strong> <span id="metricTrees">0</span></span> <span style="margin-right:15px;"><strong>Zona vacía:</strong> <span id="metricEmpty">0</span></span> <span style="margin-right:15px;"> <strong>En llamas (actual):</strong> <span id="metricFire">0</span></span> <span><strong>Quemadas (acumulado):</strong> <span id="metricBurned">0</span> (<span id="metricBurnPct">0</span> %)</span> </div> <canvas id="chart" width="720" height="350" style="display:block; margin-top:10px; width:100%; background:#ffffff;"></canvas> <h3 class="centered">Estadísticas Finales</h3> <div style="line-height:1.4;"> <span style="margin-right:15px;"><strong>Duración total (pasos):</strong> <span id="finalSteps">-</span></span> <span style="margin-right:15px;"><strong> Duración simulada (s):</strong> <span id="finalTimeSec">-</span></span> <span style="margin-right:15px;"><strong>Área máx. en llamas (%):</strong> <span id="finalMaxFirePct">-</span></span> <div style="margin-top:8px;"> <span style="margin-right:15px;"> <strong>Tiempo hasta X% quemado:</strong> <input id="thresholdPct" type="number" min="1" max="100" value="50" style="width:64px; margin-left:6px;"> <button id="applyThresholdBtn" style="margin-left:6px;">Aplicar umbral</button> </span> </div> Primer paso con ≥ <span id="displayThreshold">50</span>% quemado: <span id="thresholdStep">-</span> (simulado s: <span id="thresholdTime">-</span>) </div> </div> </div> <p></p> <hr> <p class="centered">[[Regresar al inicio|StoryInit]]</p> <script> (function(){ /* ====================== CONSTANTES / CONFIG ====================== */ const GRID_W = 150, GRID_H = 150; const TOTAL_CELLS = GRID_W * GRID_H; const CANVAS_PIX = 500; // resolución interna del canvas de simulación // colores const COLORS = { water: "#7ec8ff", old: "#1f5f2b", young: "#6fbf73", empty: "#d6d6d6", fire: "#8b0000", burned: "#000000", influence: "#ff9f1a" }; const P_OLD = 0.70; const P_YOUNG = 0.25; const WIND_FORCE = 2; const INFLUENCE_RADIUS = 1; const IGNITION_SIZE = 2; // valores por defecto (para reset) const DEFAULTS = { water: 10, old: 25, young: 35, empty: 30, wind: "none", speed: 200, threshold: 50 }; /* ====================== DOM refs ====================== */ const waterPct = document.getElementById("waterPct"); const oldPct = document.getElementById("oldPct"); const youngPct = document.getElementById("youngPct"); const emptyPct = document.getElementById("emptyPct"); const waterNum = document.getElementById("waterNum"); const oldNum = document.getElementById("oldNum"); const youngNum = document.getElementById("youngNum"); const emptyNum = document.getElementById("emptyNum"); const sumPctEl = document.getElementById("sumPct"); const sumWarning = document.getElementById("sumWarning"); const windDirEl = document.getElementById("windDir"); const speedEl = document.getElementById("speed"); const speedVal = document.getElementById("speedVal"); const generateBtn = document.getElementById("generateBtn"); const resetBtn = document.getElementById("resetBtn"); const exportBtn = document.getElementById("exportBtn"); const exportImgBtn = document.getElementById("exportImgBtn"); const startBtn = document.getElementById("startBtn"); const pauseBtn = document.getElementById("pauseBtn"); const stepBtn = document.getElementById("stepBtn"); const metricStep = document.getElementById("metricStep"); const metricTrees = document.getElementById("metricTrees"); const metricEmpty = document.getElementById("metricEmpty"); const metricFire = document.getElementById("metricFire"); const metricBurned = document.getElementById("metricBurned"); const metricBurnPct = document.getElementById("metricBurnPct"); const finalStepsEl = document.getElementById("finalSteps"); const finalTimeSecEl = document.getElementById("finalTimeSec"); const finalMaxFirePctEl = document.getElementById("finalMaxFirePct"); const thresholdInput = document.getElementById("thresholdPct"); const applyThresholdBtn = document.getElementById("applyThresholdBtn"); const displayThreshold = document.getElementById("displayThreshold"); const thresholdStepEl = document.getElementById("thresholdStep"); const thresholdTimeEl = document.getElementById("thresholdTime"); /* canvases */ const canvas = document.getElementById("forest"); const ctx = canvas.getContext("2d"); const chartCanvas = document.getElementById("chart"); const chartCtx = chartCanvas.getContext("2d"); canvas.width = CANVAS_PIX; canvas.height = CANVAS_PIX; /* ====================== ESTADO GLOBAL ====================== */ let grid = []; let running = false; let timer = null; let stepCount = 0; // temporal (porcentaje) series let seriesSteps = []; let seriesTreesPct = []; let seriesEmptyPct = []; let seriesFirePct = []; let seriesBurnedPct = []; // tracking estadístico let simulatedTimeMs = 0; // acumulado simulado (ms) let currentStepDelay = Number(speedEl.value); let maxFirePctObserved = 0; let thresholdPct = Number(thresholdInput.value); let thresholdReached = {step: null, timeMs: null}; /* ====================== UTILIDADES ====================== */ function clamp(v,a,b){ return Math.max(a, Math.min(b, v)); } /* sincronización slider <-> número */ function syncSliderNumber(rangeEl, numEl){ rangeEl.addEventListener("input", ()=> { numEl.value = rangeEl.value; updateSumAndToggleGenerate(); }); numEl.addEventListener("input", ()=> { let v = Number(numEl.value); if(isNaN(v)) v = 0; v = clamp(Math.round(v), 0, 100); numEl.value = v; rangeEl.value = v; updateSumAndToggleGenerate(); }); } /* establecer valores por defecto en controles */ function resetControlsToDefaults(){ waterPct.value = DEFAULTS.water; waterNum.value = DEFAULTS.water; oldPct.value = DEFAULTS.old; oldNum.value = DEFAULTS.old; youngPct.value = DEFAULTS.young; youngNum.value = DEFAULTS.young; emptyPct.value = DEFAULTS.empty; emptyNum.value = DEFAULTS.empty; windDirEl.value = DEFAULTS.wind; speedEl.value = DEFAULTS.speed; speedVal.textContent = DEFAULTS.speed; thresholdInput.value = DEFAULTS.threshold; displayThreshold.textContent = DEFAULTS.threshold; updateSumAndToggleGenerate(); } /* comprobar suma y habilitar/deshabilitar Generar */ function updateSumAndToggleGenerate(){ const s = Number(waterNum.value) + Number(oldNum.value) + Number(youngNum.value) + Number(emptyNum.value); sumPctEl.textContent = s; if(s !== 100){ sumWarning.style.display = "inline"; generateBtn.disabled = true; } else { sumWarning.style.display = "none"; generateBtn.disabled = false; } } /* Fisher-Yates shuffle */ function shuffleArray(arr){ for(let i=arr.length-1;i>0;i--){ const j = Math.floor(Math.random()*(i+1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } } /* tamaño de celda en canvas */ function cellSize(){ return CANVAS_PIX / GRID_W; } /* ====================== INICIALIZAR / RESET GRID y datos ====================== */ function resetGrid(){ grid = Array.from({length:GRID_H}, () => Array.from({length:GRID_W}, () => ({type:"empty"})) ); } function resetAllState(){ running = false; clearInterval(timer); timer = null; stepCount = 0; simulatedTimeMs = 0; currentStepDelay = Number(speedEl.value); maxFirePctObserved = 0; thresholdReached = {step: null, timeMs: null}; seriesSteps = []; seriesTreesPct = []; seriesEmptyPct = []; seriesFirePct = []; seriesBurnedPct = []; finalStepsEl.textContent = "-"; finalTimeSecEl.textContent = "-"; finalMaxFirePctEl.textContent = "-"; thresholdStepEl.textContent = "-"; thresholdTimeEl.textContent = "-"; updateMetricsDisplay(0,0,0,0,0); resetGrid(); } /* ====================== GENERACIÓN DE RÍOS (CONEXOS) ====================== */ function generateRiver(){ let x = Math.floor(Math.random()*GRID_W); let y = 0; let steps = Math.floor(GRID_H * (0.6 + Math.random()*0.6)); for(let i=0;i<steps;i++){ let width = 1 + Math.floor(Math.random()*2); for(let dx=-width; dx<=width; dx++){ for(let dy=-1; dy<=1; dy++){ let nx = x+dx, ny = y+dy; if(nx>=0 && nx<GRID_W && ny>=0 && ny<GRID_H) grid[ny][nx].type = "water"; } } x += Math.floor(Math.random()*3)-1; x = clamp(x, 1, GRID_W-2); y += 1; if(y >= GRID_W) break; } if(Math.random() < 0.4){ let lx = clamp(x + Math.floor((Math.random()-0.5)*10), 2, GRID_W-3); let ly = clamp(y + Math.floor((Math.random()-0.5)*10), 2, GRID_H-3); let r = 2 + Math.floor(Math.random()*3); for(let yy=ly-r; yy<=ly+r; yy++){ for(let xx=lx-r; xx<=lx+r; xx++){ if(xx>=0 && xx<GRID_W && yy>=0 && yy<GRID_H){ if((xx-lx)*(xx-lx)+(yy-ly)*(yy-ly) <= r*r) grid[yy][xx].type = "water"; } } } } } /* ====================== GENERAR ESCENARIO (requiere suma=100) ====================== */ function generateScenario(){ // borrar y reset completo resetAllState(); // leer porcentajes const w = Number(waterNum.value); const o = Number(oldNum.value); const y = Number(youngNum.value); const e = Number(emptyNum.value); /* ============================ 1. CREAR LISTA DE TODAS LAS CELDAS ============================ */ let allCells = []; for(let yy=0; yy<GRID_H; yy++){ for(let xx=0; xx<GRID_W; xx++){ allCells.push({x:xx, y:yy}); } } shuffleArray(allCells); /* ============================ 2. ASIGNAR AGUA SEGÚN PORCENTAJE ============================ */ const total = TOTAL_CELLS; const targetWater = Math.round(w / 100 * total); for(let i=0; i<targetWater; i++){ const c = allCells[i]; grid[c.y][c.x].type = "water"; } /* ============================ 3. CONSTRUIR CONECTIVIDAD (ríos) — sin cambiar porcentaje ============================ */ if (w > 0) { for(let i=0;i<3;i++){ generateRiver(); } } /* ============================ 4. RECOLECTAR NO-AGUA ============================ */ let nonWaterCells = []; for(let yy=0; yy<GRID_H; yy++){ for(let xx=0; xx<GRID_W; xx++){ if(grid[yy][xx].type !== "water"){ nonWaterCells.push({x:xx,y:yy}); } } } shuffleArray(nonWaterCells); const nNon = nonWaterCells.length; const targetOld = Math.round(o / 100 * nNon); const targetYoung = Math.round(y / 100 * nNon); const targetEmpty = nNon - targetOld - targetYoung; let idx = 0; for(let i=0;i<targetOld && idx<nNon;i++,idx++){ const c = nonWaterCells[idx]; grid[c.y][c.x].type = "old"; } for(let i=0;i<targetYoung && idx<nNon;i++,idx++){ const c = nonWaterCells[idx]; grid[c.y][c.x].type = "young"; } for(; idx<nNon; idx++){ const c = nonWaterCells[idx]; grid[c.y][c.x].type = "empty"; } draw(); recordSeries(); } /* ====================== DIBUJO E INFLUENCIA ====================== */ function draw(){ const cs = cellSize(); for(let y=0;y<GRID_H;y++){ for(let x=0;x<GRID_W;x++){ const t = grid[y][x].type; ctx.fillStyle = COLORS[t] || COLORS.empty; ctx.fillRect(Math.floor(x*cs), Math.floor(y*cs), Math.ceil(cs), Math.ceil(cs)); } } } function drawInfluence(){ draw(); const cs = cellSize(); for(let y=0;y<GRID_H;y++){ for(let x=0;x<GRID_W;x++){ if(grid[y][x].type === "fire"){ for(let dx=-INFLUENCE_RADIUS; dx<=INFLUENCE_RADIUS; dx++){ for(let dy=-INFLUENCE_RADIUS; dy<=INFLUENCE_RADIUS; dy++){ if(dx===0 && dy===0) continue; let nx = x+dx, ny = y+dy; if(nx>=0 && nx<GRID_W && ny>=0 && ny<GRID_W){ let t = grid[ny][nx].type; if(t === "old" || t === "young"){ ctx.fillStyle = COLORS.influence; ctx.fillRect(Math.floor(nx*cs), Math.floor(ny*cs), Math.ceil(cs), Math.ceil(cs)); } } } } } } } } /* ====================== VECINDAD MOORE ====================== */ function neighbors(x,y){ let n=[]; for(let dx=-1; dx<=1; dx++){ for(let dy=-1; dy<=1; dy++){ if(dx===0 && dy===0) continue; let nx = x+dx, ny = y+dy; if(nx>=0 && nx<GRID_W && ny>=0 && ny<GRID_H) n.push({x:nx,y:ny,dx:dx,dy:dy}); } } return n; } /* ====================== VIENTO ====================== */ function windFactor(dx,dy){ const dir = windDirEl.value; if(dir === "none") return 1; if(dir === "N"){ if(dy === -1) return WIND_FORCE; if(dy === 1) return 1/WIND_FORCE; } if(dir === "S"){ if(dy === 1) return WIND_FORCE; if(dy === -1) return 1/WIND_FORCE; } if(dir === "E"){ if(dx === 1) return WIND_FORCE; if(dx === -1) return 1/WIND_FORCE; } if(dir === "W"){ if(dx === -1) return WIND_FORCE; if(dx === 1) return 1/WIND_FORCE; } return 1; } /* ====================== STEP (avance) — actualiza simulatedTimeMs con el delay aplicado ====================== */ function step(){ let next = JSON.parse(JSON.stringify(grid)); let anyFire = false; for(let y=0;y<GRID_H;y++){ for(let x=0;x<GRID_W;x++){ const cell = grid[y][x]; if(cell.type === "fire"){ next[y][x].type = "burned"; const nbs = neighbors(x,y); nbs.forEach(n=>{ const t = grid[n.y][n.x].type; if(t === "old" || t === "young"){ let p = (t === "old") ? P_OLD : P_YOUNG; p *= windFactor(n.dx, n.dy); if(Math.random() < p) next[n.y][n.x].type = "fire"; } }); } if(cell.type === "fire") anyFire = true; } } grid = next; stepCount++; // aumentar tiempo simulado según el delay que se esté usando const stepDelay = currentStepDelay || Number(speedEl.value); simulatedTimeMs += stepDelay; // actualizar observados y dibujar drawInfluence(); recordSeries(); if(!anyFire){ // simulación finalizada: calcular estadísticas finales running = false; clearInterval(timer); timer = null; computeFinalStats(); } } /* ====================== CLICK PARA INICIAR FUEGO (2x2) ====================== */ canvas.addEventListener("click", e=>{ if(running) return; const rect = canvas.getBoundingClientRect(); // convertir coordenadas ajustadas por escala real del canvas en pantalla const xClick = Math.floor((e.clientX - rect.left) * (CANVAS_PIX / rect.width)); const yClick = Math.floor((e.clientY - rect.top) * (CANVAS_PIX / rect.height)); const cs = cellSize(); const cellX = Math.floor(xClick / cs); const cellY = Math.floor(yClick / cs); for(let dy=0; dy<IGNITION_SIZE; dy++){ for(let dx=0; dx<IGNITION_SIZE; dx++){ let nx = cellX + dx, ny = cellY + dy; if(nx>=0 && nx<GRID_W && ny>=0 && ny<GRID_H){ let t = grid[ny][nx].type; if(t === "old" || t === "young") grid[ny][nx].type = "fire"; } } } // dibujar con influencia y registrar (estado intermedio) drawInfluence(); recordSeries(); }); /* ====================== SERIES Y MÉTRICAS (en porcentaje) ====================== */ function countStates(){ let cnt = {old:0, young:0, empty:0, water:0, fire:0, burned:0}; for(let y=0;y<GRID_H;y++){ for(let x=0;x<GRID_W;x++){ let t = grid[y][x].type; if(cnt[t] !== undefined) cnt[t]++; } } return cnt; } function recordSeries(){ const cnt = countStates(); const trees = cnt.old + cnt.young; const treesPct = (trees / TOTAL_CELLS) * 100; const emptyPct = (cnt.empty / TOTAL_CELLS) * 100; const firePct = (cnt.fire / TOTAL_CELLS) * 100; const burnedPct = (cnt.burned / TOTAL_CELLS) * 100; // actualizar series seriesSteps.push(stepCount); seriesTreesPct.push(treesPct); seriesEmptyPct.push(emptyPct); seriesFirePct.push(firePct); seriesBurnedPct.push(burnedPct); // actualizar max fuego if(firePct > maxFirePctObserved) maxFirePctObserved = firePct; // comprobar umbral X% quemado (si aún no alcanzado) if(thresholdReached.step === null && burnedPct >= thresholdPct){ thresholdReached.step = stepCount; thresholdReached.timeMs = simulatedTimeMs; thresholdStepEl.textContent = thresholdReached.step; thresholdTimeEl.textContent = (thresholdReached.timeMs / 1000).toFixed(2); } // actualizar UI de métricas updateMetricsDisplay(stepCount, Math.round(trees), Math.round(cnt.empty), Math.round(cnt.fire), Math.round(cnt.burned)); drawChart(); } function updateMetricsDisplay(step, trees, empty, fire, burned){ metricStep.textContent = step; metricTrees.textContent = trees; metricEmpty.textContent = empty; metricFire.textContent = fire; metricBurned.textContent = burned; metricBurnPct.textContent = ((burned / TOTAL_CELLS) * 100).toFixed(2); } /* ====================== GRÁFICA (0..100%) ====================== */ function drawChart(){ const w = chartCanvas.width; const h = chartCanvas.height; const ml = 70, mr = 25, mt = 30, mb = 70; chartCtx.clearRect(0, 0, w, h); /* ====================== EJES ====================== */ chartCtx.strokeStyle = "#000"; chartCtx.lineWidth = 1; chartCtx.beginPath(); chartCtx.moveTo(ml, mt); chartCtx.lineTo(ml, h - mb); chartCtx.lineTo(w - mr, h - mb); chartCtx.stroke(); /* ====================== ETIQUETA Y ====================== */ chartCtx.save(); chartCtx.translate(25, h / 2); chartCtx.rotate(-Math.PI / 2); chartCtx.font = "14px sans-serif"; chartCtx.textAlign = "center"; chartCtx.fillText("Porcentaje del total (%)", 0, 0); chartCtx.restore(); /* ====================== ETIQUETA X ====================== */ chartCtx.font = "14px sans-serif"; chartCtx.textAlign = "center"; chartCtx.fillText( "Tiempo (pasos de simulación)", (ml + w - mr) / 2, h - 20 ); /* ====================== ESCALA Y (0–100 %) ====================== */ chartCtx.font = "12px sans-serif"; chartCtx.textAlign = "right"; for(let v = 0; v <= 1.001; v += 0.25){ const y = h - mb - v * (h - mt - mb); chartCtx.beginPath(); chartCtx.moveTo(ml - 6, y); chartCtx.lineTo(ml + 6, y); chartCtx.stroke(); chartCtx.fillText(Math.round(v * 100), ml - 10, y + 4); chartCtx.strokeStyle = "#e6e6e6"; chartCtx.beginPath(); chartCtx.moveTo(ml, y); chartCtx.lineTo(w - mr, y); chartCtx.stroke(); chartCtx.strokeStyle = "#000"; } /* ====================== ESCALA X DINÁMICA ====================== */ const total = stepCount; const visible = seriesSteps.length; const xMin = Math.max(0, total - visible); const xMax = total; const ticks = 5; chartCtx.textAlign = "center"; chartCtx.font = "12px sans-serif"; for(let i = 0; i <= ticks; i++){ const frac = i / ticks; const x = ml + frac * (w - ml - mr); const t = Math.round(xMin + frac * (xMax - xMin)); chartCtx.beginPath(); chartCtx.moveTo(x, h - mb - 6); chartCtx.lineTo(x, h - mb + 6); chartCtx.stroke(); chartCtx.fillText(t.toString(), x, h - mb + 28); } /* ====================== FUNCIONES DE MAPEO ====================== */ function xAt(i){ if(seriesSteps.length <= 1) return ml + 4; return ml + (i / (seriesSteps.length - 1)) * (w - ml - mr); } function yAt(pct){ return h - mb - (pct / 100) * (h - mt - mb); } /* ====================== SERIES ====================== */ const series = [ {data: seriesTreesPct, color: "#2d8f2d"}, {data: seriesEmptyPct, color: "#bfbfbf"}, {data: seriesFirePct, color: "#8b0000"}, {data: seriesBurnedPct, color: "#000000"} ]; series.forEach(s => { chartCtx.beginPath(); chartCtx.lineWidth = 2; chartCtx.strokeStyle = s.color; s.data.forEach((v, i) => { const x = xAt(i); const y = yAt(v); if(i === 0) chartCtx.moveTo(x, y); else chartCtx.lineTo(x, y); }); chartCtx.stroke(); }); /* ====================== LEYENDA ====================== */ chartCtx.font = "11px Arial"; chartCtx.textAlign = "left"; const labels = [ {txt:"Árboles", col:"#2d8f2d"}, {txt:"Vacío", col:"#bfbfbf"}, {txt:"Incendiada",col:"#8b0000"}, {txt:"Quemada", col:"#000000"} ]; labels.forEach((L, i) => { chartCtx.fillStyle = L.col; chartCtx.fillRect(ml + i*135, 6, 12, 12); chartCtx.fillStyle = "#000"; chartCtx.fillText(L.txt, ml + 18 + i*135, 16); }); } /* ====================== EXPORT CSV (porcentaje) ====================== */ function exportCSV(){ const header = ["paso","arboles_pct","vacia_pct","incendiada_pct","quemada_pct"]; let csv = header.join(",") + "\n"; for(let i=0;i<seriesSteps.length;i++){ const row = [ seriesSteps[i], seriesTreesPct[i].toFixed(4), seriesEmptyPct[i].toFixed(4), seriesFirePct[i].toFixed(4), seriesBurnedPct[i].toFixed(4) ]; csv += row.join(",") + "\n"; } const blob = new Blob([csv], {type: "text/csv;charset=utf-8;"}); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "simulacion_incendio_series_pct.csv"; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } /* ====================== EXPORT PNG (canvas actual) ====================== */ function exportPNG(){ const dataURL = canvas.toDataURL("image/png"); const a = document.createElement("a"); a.href = dataURL; a.download = `simulacion_incendio_step_${stepCount}.png`; document.body.appendChild(a); a.click(); document.body.removeChild(a); } /* ====================== CALCULO ESTADÍSTICAS FINALES ====================== */ function computeFinalStats(){ finalStepsEl.textContent = stepCount; finalTimeSecEl.textContent = (simulatedTimeMs / 1000).toFixed(2); finalMaxFirePctEl.textContent = maxFirePctObserved.toFixed(3); // si umbral no alcanzado, dejar mensaje if(thresholdReached.step === null){ thresholdStepEl.textContent = "no alcanzado"; thresholdTimeEl.textContent = "-"; } else { thresholdStepEl.textContent = thresholdReached.step; thresholdTimeEl.textContent = (thresholdReached.timeMs / 1000).toFixed(2); } } /* ====================== UI / EVENTOS ====================== */ /* sincronizar sliders y números */ syncSliderNumber(waterPct, waterNum); syncSliderNumber(oldPct, oldNum); syncSliderNumber(youngPct, youngNum); syncSliderNumber(emptyPct, emptyNum); /* inicializar suma y botones */ resetControlsToDefaults(); /* Generar escenario (solo si suma==100) */ generateBtn.addEventListener("click", () => { if(generateBtn.disabled) return; resetAllState(); generateScenario(); }); /* Reset controles a valores por defecto */ resetBtn.addEventListener("click", () => { resetControlsToDefaults(); }); /* Start / Pause / Step */ startBtn.addEventListener("click", () => { if(running) return; running = true; currentStepDelay = Number(speedEl.value); clearInterval(timer); timer = setInterval(step, currentStepDelay); }); pauseBtn.addEventListener("click", () => { running = false; clearInterval(timer); timer = null; }); stepBtn.addEventListener("click", () => { if(!running){ // ejecutar un step manual; usar la velocidad configurada como duración simulada del paso currentStepDelay = Number(speedEl.value); step(); } }); /* exportaciones */ exportBtn.addEventListener("click", () => exportCSV()); exportImgBtn.addEventListener("click", () => exportPNG()); /* velocidad: actualizar etiqueta y si está corriendo reiniciar timer con nueva velocidad */ speedEl.addEventListener("input", ()=>{ speedVal.textContent = speedEl.value; if(running){ currentStepDelay = Number(speedEl.value); clearInterval(timer); timer = setInterval(step, currentStepDelay); } }); /* umbral X% */ applyThresholdBtn.addEventListener("click", ()=>{ let v = Number(thresholdInput.value); if(isNaN(v) || v < 1) v = 1; if(v > 100) v = 100; thresholdPct = v; displayThreshold.textContent = v; // reiniciar registro del umbral (si quiere volver a medir en la misma corrida, reiniciar valores) thresholdReached = {step: null, timeMs: null}; thresholdStepEl.textContent = "-"; thresholdTimeEl.textContent = "-"; }); /* Actualizar suma al modificar num inputs directamente */ [waterNum, oldNum, youngNum, emptyNum].forEach(n => { n.addEventListener("input", updateSumAndToggleGenerate); }); /* aplicar sincronía inicial */ updateSumAndToggleGenerate(); /* ====================== INICIALIZAR ESCENARIO POR DEFECTO ====================== */ resetAllState(); generateScenario(); })(); </script>
<div class="titulo-principal"> <h1 >🔥🔥🌳</h1> <h1 >Bienvenida</h1> <h1 >🌳🔥🔥</h1> </div> <div class="content-wrapper"> <p>El estudio sistemático de los incendios forestales es fundamental para comprender sus efectos ecológicos, sociales y económicos, así como los riesgos que representan cuando se pierde el control de fuego.</p> <p>En este espacio encontrarás tres simuladores computacionales didácticos que te permitirán explorar, de manera interactiva, cómo se propaga el fuego bajo distintas condiciones. A través de la experimentación con parámetros y escenarios, podrás analizar conceptos clave relacionados con la dinámica del incendio y los sistemas complejos.</p> <p> Estos modelos no buscan reproducir un incendio real, sino apoyar el aprendizaje mediante la observación, el análisis y la reflexión. En el ámbito de las ciencias forenses, también ofrecen un apoyo conceptual para el estudio de patrones de combustión y la formulación de hipótesis.</p> <p>Te invitamos a experimentar, registrar tus observaciones y aprovechar las imágenes y gráficas generadas como parte de tu proceso de aprendizaje. Además, te recomendamos seguir la lista en que presentamos los simuladores.</p> <p>Sí tienes sugerencias, dudas o comentarios puedes enviarnos un correo electrónico: <a href="mailto:vicentetorres@enacif.unam.mx">vicentetorres@enacif.unam.mx</a></p> <p>Da clic para examinar a uno de los tres simuladores de incendios forestales preparados para ti:</p> <ul> <li>[[🔥 Exploratorio — Sistema básico con autómatas celulares.|Modelo1]]</li> <li>[[🔥🔥 Analítico — Sistema de escenario estocástico en rejilla.|Modelo2]]</li> <li>[[🔥🔥🔥 Avanzado — Sistema multivariable y editable en topografía y vegetación.|Modelo3]]</li> <li>[[También puedes consultar información sobre el desarrollo del proyecto.|Creditos]]</li> </ul> </div> <hr> <p class="centered">[[Regresar al inicio|StoryInit]]</p>
<h1>Modelo multivariable y editable en topografía y vegetación</h1> <div class="content-wrapper"> <<set $mostrar to false>> <<button "Instrucciones +/-">> <<set $mostrar to not $mostrar>> <<replace "#Instrucciones">> <<if $mostrar>> <h2>🧭 Instrucciones de uso del simulador</h2> <p>Este simulador ha sido diseñado con una interfaz de dos paneles principales. En condiciones normales de visualización, el panel izquierdo corresponde a los controles del sistema, mientras que el panel derecho muestra la visualización gráfica del modelo. En pantallas de ancho reducido, ambos paneles se reorganizan automáticamente en una sola columna, situándose los controles por encima de la visualización.</p> <p>En el panel de control se concentran los parámetros necesarios para la generación del <em>bosque de píxeles</em> 🌲. Cada celda puede representar uno de los siguientes estados: vacío, pasto, arbusto o árbol. Los tres últimos corresponden a distintos tipos de combustible vegetal. Mediante deslizadores y cajas numéricas es posible definir el porcentaje de aparición de cada elemento; la suma total debe ser estrictamente del 100 %, de lo contrario la simulación no podrá ejecutarse. Por defecto, el sistema parte de una distribución uniforme del 25 % para cada elemento, misma que el usuario puede modificar libremente para construir el escenario de su interés.</p> <p>Dado que los distintos combustibles presentan comportamientos diferenciados frente al fuego 🔥, el simulador incluye una matriz de propagación de 3 × 3, en la cual cada celda admite valores de probabilidad entre 0 % y 50 %, representando la facilidad de transmisión del incendio entre tipos de vegetación.</p> <p>En la sección inferior del panel se encuentra el control de humedad ambiental 💧, expresado en porcentaje y ajustable entre 0 % y 100 %. El valor inicial por defecto es 0 %.</p> <p>A continuación, se dispone del control de viento 🌬️, que permite definir tanto la intensidad como la dirección. Las intensidades disponibles son 0, 15, 30, 45 y 60 km/h. La dirección se ajusta mediante un deslizador angular, donde 0° corresponde al este, 90° al norte, 180° al oeste y 270° al sur.</p> <p>Más abajo se localiza el control de velocidad de reproducción de la simulación ⏱️, con opciones de 1, 5, 10 y 20 cuadros por segundo (fps). El valor predeterminado es 20 fps.</p> <p>Posteriormente se presentan los controles generales de la simulación. El botón <em>Generar escenario</em> permite crear el bosque conforme a los parámetros definidos, mientras que <em>Reset de parámetros</em> restablece el sistema a su estado inicial. Asimismo, se incluyen dos botones para exportar los porcentajes de los distintos elementos en función de los pasos de la simulación, así como opciones para exportar imágenes del estado del bosque de píxeles y de la topografía 🖼️.</p> <p>Los controles de ejecución incluyen los botones <em>Iniciar el incendio</em>, <em>Pausa</em> y <em>Paso</em>. Este último permite avanzar la simulación de manera controlada, un paso a la vez, con fines de análisis detallado 🔍.</p> <p>Para iniciar un incendio, el usuario debe poner el cursor de la computadora sobre el bosque de píxeles (ubicado en el panel izquierdo de la visualización) y hacer clic en la región deseada. El incendio se iniciará en un área de 2 × 2 celdas, pudiendo definir uno o varios puntos de ignición.</p> <p>El simulador ofrece además herramientas avanzadas de edición mediante los controles de topografía y vegetación 🧩. En ambos casos, es posible trazar celdas de tamaños 10 × 10, 50 × 50 y 100 × 100.</p> <p>En el caso de la topografía, pueden generarse montes o valles utilizando funciones gaussianas tridimensionales o mediante funciones escalon. La magnitud de la elevación o profundidad se ajusta mediante una caja numérica, y el trazo se realiza directamente en el panel derecho de la visualización. Antes de comenzar, es indispensable activar el modo correspondiente mediante el botón <em>Activar edición de topografía</em>, tras lo cual aparecerá un mensaje alusivo en pantalla. En este modo no es posible iniciar incendios; para salir, debe utilizarse el botón <em>Desactivar edición</em>.</p> <p>De forma análoga, en la sección de edición de vegetación 🌿 pueden seleccionarse los cuatro elementos básicos (vacío, pasto, arbusto y árbol), así como un elemento adicional: agua. Esto permite definir ríos y lagos dentro del escenario. Para realizar los trazos es necesario activar el botón <em>Activar edición de vegetación</em>. Mientras este modo esté activo, el inicio de incendios queda deshabilitado, por lo que deberá desactivarse antes de ejecutar la simulación.</p> <p>Debajo de estos controles se encuentran las opciones para cargar y descargar archivos JSON que almacenan condiciones iniciales de escenarios, facilitando la reutilización y el análisis comparativo de configuraciones 📂.</p> <p>Finalmente, en la parte inferior del panel se muestran estadísticas del estado actual del escenario 📊, seguidas de un enlace que permite regresar al inicio del programa. Con ello concluyen los controles del panel izquierdo.</p> <p>En el panel derecho de la pantalla se localiza la sección de visualización principal 👁️. En su parte izquierda se muestra el bosque de píxeles, compuesto por celdas que representan los distintos elementos del modelo. Este panel incluye una malla de referencia de 4nbsp×nbsp4, donde cada cuadro corresponde a 50nbspceldas de longitud. Aquí se realizan los trazos de vegetación, espacios vacíos, cuerpos de agua y la definición de los puntos de inicio del incendio.</p> <p>En la parte derecha de esta sección se presenta el mapa topográfico 🗺️, el cual muestra las condiciones del terreno mediante curvas de nivel.</p> <p>Debajo de ambos paneles se encuentra el espacio destinado a la gráfica de la simulación 📈. Esta representa el porcentaje de cada elemento, incluyendo incendios activos y área quemada, en función de los pasos de la simulación. Cabe señalar que, por el momento, la simulación no se encuentra calibrada a una escala temporal real.</p> <</if>> <</replace>> <</button>> <div id="Instrucciones"></div> <<set $mostrar to false>> <<button "Detalles de la simulación+/-">> <<set $mostrar to not $mostrar>> <<replace "#Detalles">> <<if $mostrar>> <h1>Modelo probabilístico de propagación de incendios forestales discretizado en rejilla</h1> <h2>Visión general y propósito</h2> <p>El modelo trata el dominio (el bosque de píxeles) como una malla bidimensional finita de celdas <code>(i,j)</code>, cada una con un estado discreto que codifica la condición del combustible y del fuego (por ejemplo: agua, árbol joven, árbol viejo, vacío, incendiado, quemado). En lugar de resolver ecuaciones continuas de transferencia de calor y cinética de combustión, el modelo emplea reglas de transición estocásticas que representan la probabilidad condicional de ignición de una celda dada la configuración local y parámetros ambientales. Si bien esta simulación es didáctica para mostrar el fenómeno de propagación de fuego en bosque, es importante señalar que el tema es más profundo. </p> <h2>Fundamento físico y vínculo con modelos continuos</h2> <p>Físicamente, la propagación del fuego resulta de procesos acoplados: transferencia de calor por radiación, convección y conducción; desecación del combustible; reacción química con dependencia exponencial de la temperatura; y advección por viento. Modelos continuos (ecuaciones de transporte de energía y especies, o formulaciones empíricas como las de Rothermel, o de Scott y Burgan o Prometheu) capturan estas dependencias mediante tasas continuas y parámetros medibles (contenido de humedad, carga de combustible, densidad aparente, entre otras variables).</p> <p>La formulación discreta probabilística es una aproximación de tipo macroscópico: reemplaza los procesos continuos por una tasa o riesgo de ignición que se calcula localmente. Cuando la escala espacial de la celda es mayor que la longitud característica de la llama o de la zona de calentamiento previo, las reglas probabilísticas actúan como modelos efectivos (<em>coarse-grained</em>) que integran física subcelular en parámetros estocásticos.</p> <h3>Conexión matemática (esquema de riesgo/hazard)</h3> <p>Una formulación consistente desde el punto de vista estocástico es definir para cada celda (célula) susceptible un riesgo instantáneo de ignición <code>h_{ij}(t)</code> (tasa de peligro). Si el tiempo discreto del simulador es <code>Δt</code>, la probabilidad de que la celda pase a estado fuego en el paso siguiente es</p> <pre> P[ignite_{ij} at t+Δt | config_t] = 1 - exp( - h_{ij}(t) · Δt ). </pre> <p>Esta expresión proviene de modelar la ignición como un proceso de Poisson con tasa <code>h_{ij}</code>. Una parametrización práctica para <code>h_{ij}</code> es suma de contribuciones de vecinos incendiados ponderadas por factores de combustible, viento y topografía:</p> <pre> h_{ij}(t) = α · Σ_{k∈N(i,j)} w_{k→ij} · F(type_k) · S_{ij}(moisture) · exp( β · (wind · e_{k→ij}) + γ · slope_{k→ij} ). </pre> <p>Definiciones: <code>N(i,j)</code> es la vecindad (por ejemplo, Moore de 8 vecinos), <code>w_{k→ij}</code> un peso geométrico que decae con la distancia o cuenta la posición relativa, <code>F(type_k)</code> la carga/flammabilidad del combustible en el vecino, <code>S_{ij}</code> factor que reduce la tasa por humedad, <code>wind</code> es el vector viento, <code>e_{k→ij}</code> unidad que apunta del vecino hacia la celda y <code>slope</code> el efecto de la ladera. <code>α, β, γ</code> son parámetros a calibrar.</p> <p>Alternativamente, se puede usar una función sigmoide o logística para saturación:</p> <pre> P = 1 / (1 + exp( - (κ0 + κ1 · Σ influence_k + κ2 · controllables ) ) ). </pre> <p>Ambas formulaciones (<em>hazard exponencial</em> o probabilidad logística) son equivalentes en el sentido de proporcionar una transición entre baja y alta probabilidad; la elección depende de la interpretación física que se quiera conservar y de la calibración disponible.</p> <h2>Elección de discretización y escalas</h2> <p>La validez del modelo depende de elección de la resolución espacial <code>Δx</code> y temporal <code>Δt</code>. Reglas prácticas:</p> <p>• <code>Δx</code> debe ser menor o del mismo orden que la longitud característica de transferencia de calor previa a la ignición (<em>flame length, radiative preheating</em>); si es mucho mayor, la celda actúa como un “parche efectivo” con parámetros homogéneos.</p> <p>• <code>Δt</code> debe ser pequeño respecto del tiempo típico de ignición para que la aproximación de proceso de Poisson sea coherente; numéricamente, elegir <code>Δt</code> tal que <code>h_{ij}·Δt ≪ 1</code> en condiciones normales evita saturación y errores de discretización temporal.</p> <h2>Calibración, incertidumbre y validación</h2> <p>Los parámetros (por ejemplo <code>α, β, γ</code>, o coeficientes logísticos) requieren calibración. Procedimientos adecuados:</p> <p>• utilizar datos observacionales (eventos quemados, tasa de avance) y estimar parámetros por máxima verosimilitud agregando el supuesto de independencia condicional entre celdas en pasos sucesivos, o bien emplear métodos bayesianos/ABC cuando la verosimilitud es intractable;</p> <p>• realizar análisis de sensibilidad y ensambles Monte Carlo para cuantificar la incertidumbre;</p> <p>• validar con métricas espacio-temporales: fracción quemada vs. tiempo, velocidad media de frente, distribución de tamaños de manchas y correlaciones espaciales.</p> <h2>Indicadores observables y salidas que debe guardar el estudiante</h2> <p>Para cada experimento conviene almacenar la configuración inicial (aunque no se puede guardar la semilla del generador aleatorio), la evolución temporal del área quemada, campos de estado final, y series temporales de métricas (área quemada, perímetro, número de focos activos). Estas salidas permiten reproducibilidad y análisis estadístico (medias, varianzas, intervalos de confianza entre corridas).</p> <h2>Limitaciones y cuándo emplear modelos más complejos</h2> <p>El modelo discreto es útil para estudiar patrones emergentes y realizar experimentos de tipo “<em>what-if</em>”. No obstante, cuando se requiere predicción operativa o cuantitativa (por ejemplo, planificación de supresión), conviene integrar componentes físicos adicionales:</p> <p>• acoplar un modelo de humedad y de energía (ecuaciones de balance térmico) para representar desecación y combustión;</p> <p>• incorporar topografía detallada y modelos empíricos de velocidad de propagación (por ejemplo la formulación de Rothermel o modelos basados en energía de combustión) para estimar tasas de avance locales;</p> <p>• modelado multi-escala donde celdas pequeñas resuelven dinámica de llama y celdas más grandes agregan comportamiento regional.</p> <h2>Notas finales</h2> <p>El enfoque probabilístico discreto es un puente entre la heurística y la física completa: conserva la interpretabilidad y la eficiencia computacional para explorar propiedades emergentes, pero exige cautela en la interpretación cuantitativa. Comprender la formulación del riesgo, la dependencia funcional de la probabilidad de ignición y la escala de discretización es esencial para usar estas herramientas con rigor científico.</p> <h2>Referencias</h2> <ol> <li> Alexandridis, A., Vakalis, D., Siettos, C. I., & Bafas, G. V. (2008). <em>A cellular automata model for forest fire spread prediction: The case of the wildfire that swept through Spetses Island in 1990</em>. <em>Applied Mathematics and Computation, 204</em>(1), 191–201. <a href="https://doi.org/10.1016/j.amc.2008.06.046" target="_blank" rel="noopener"> https://doi.org/10.1016/j.amc.2008.06.046 </a> </li> <li> Gouveia Freire, J., & Castro DaCamara, C. (2019). <em>Using cellular automata to simulate wildfire propagation and to assist in fire management</em>. <em>Natural Hazards and Earth System Sciences, 19</em>(1), 169–179. <a href="https://doi.org/10.5194/nhess-19-169-2019" target="_blank" rel="noopener"> https://doi.org/10.5194/nhess-19-169-2019 </a> </li> <li> Alexandridis, A., Russo, L., Vakalis, D., Bafas, G. V., & Siettos, C. I. (2011). <em>Wildland fire spread modelling using cellular automata: Evolution in large-scale spatially heterogeneous environments under fire suppression tactics</em>. <em>International Journal of Wildland Fire, 20</em>, 633–647. <a href="https://doi.org/10.1071/WF09119" target="_blank" rel="noopener"> https://doi.org/10.1071/WF09119 </a> </li> <li> White, S. H., Martín del Rey, A., & Rodríguez Sánchez, G. (2005). <em>A cellular automata model for predicting fire spread</em>. <em>WIT Transactions on Ecology and the Environment, 81</em>, 69–80. <a href="https://www.witpress.com/Secure/elibrary/papers/ECO05/ECO05008FU.pdf" target="_blank" rel="noopener"> https://www.witpress.com/Secure/elibrary/papers/ECO05/ECO05008FU.pdf </a> </li> <li> Karafyllidis, I., & Thanailakis, A. (1997). <em>A model for predicting forest fire spreading using cellular automata</em>. <em>Ecological Modelling, 99</em>, 87–97. <a href="https://www.dpi.inpe.br/gilberto/cursos/st-society-2013/Kara1997.pdf" target="_blank" rel="noopener"> https://www.dpi.inpe.br/gilberto/cursos/st-society-2013/Kara1997.pdf </a> </li> <li> Grassberger, P. (2002). <em>Critical behaviour of the Drossel-Schwabl forest fire model</em>. <em>New Journal of Physics, 4</em>, 17. <a href="https://arxiv.org/abs/cond-mat/0202022" target="_blank" rel="noopener"> https://arxiv.org/abs/cond-mat/0202022 </a> </li> <li> Beneduci, R., & Mascali, G. (2023). <em>Forest fire spreading: A nonlinear stochastic model continuous in space and time</em>. arXiv. <a href="https://arxiv.org/abs/2309.00660" target="_blank" rel="noopener"> https://arxiv.org/abs/2309.00660 </a> </li> <li> Russo, L., Russo, P., & Siettos, C. I. (2015). <em>A complex network theory approach for the spatial distribution of fire breaks in heterogeneous forest landscapes for the control of wildland fires</em>. arXiv. <a href="https://arxiv.org/abs/1509.04065" target="_blank" rel="noopener"> https://arxiv.org/abs/1509.04065 </a> </li> <li> Denham, M., & Laneri, K. (2017). <em>Using efficient parallelization in Graphic Processing Units to parameterize stochastic fire propagation models</em>. arXiv. <a href="https://arxiv.org/abs/1701.03549" target="_blank" rel="noopener"> https://arxiv.org/abs/1701.03549 </a> </li> <li> Weinhouse, C., & Augustin, J. (2025). <em>Leveraging cellular automata for real-time wildfire spread modeling in California</em>. arXiv. <a href="https://arxiv.org/abs/2510.09708" target="_blank" rel="noopener"> https://arxiv.org/abs/2510.09708 </a> </li> </ol> <</if>> <</replace>> <</button>> <div id="Detalles"></div> <<button "Actividad Didáctica +/-">> <<set $mostrar to not $mostrar>> <<replace "#Preguntas">> <<if $mostrar>> <h1>Actividad experimental para el análisis del simulador discreto de incendios forestales 🔥🌲</h1> <p><strong>Propósito.</strong> Diseñar, ejecutar y analizar un conjunto de experimentos controlados utilizando el simulador de incendios forestales basado en celdas discretas. La actividad busca que el estudiantado relacione la estructura del paisaje (vegetación, agua y topografía), la configuración de ignición y la acción del viento con métricas cuantitativas de propagación del fuego 📊. Asimismo, se pretende introducir técnicas básicas de análisis estadístico de modelos estocásticos y fomentar la elaboración de un informe académico que documente objetivos, diseño experimental, resultados y discusión.</p> <h2>Planteamiento general</h2> <p>El laboratorio se organiza en tres bloques articulados: (1) construcción de escenarios y formulación de objetivos, (2) diseño experimental y ejecución de réplicas estocásticas, y (3) análisis cuantitativo e interpretación de resultados. En todos los bloques deberán conservarse evidencias digitales del trabajo realizado, incluyendo archivos CSV de las series temporales, imágenes PNG del estado espacial del bosque y un registro de los parámetros utilizados en cada corrida (porcentajes iniciales de celdas, posición de ignición, condiciones de viento y número máximo de pasos de simulación).</p> <h2>Bloque 1 — Construcción de escenarios y formulación de objetivos 🧠</h2> <p>El estudiantado deberá configurar al menos cuatro escenarios conceptuales distintos dentro del simulador y plantear para cada uno una hipótesis explícita y verificable, esta servirá de objetivo de su trabajo. Se sugieren las siguientes configuraciones mínimas, las cuales pueden adaptarse siempre que se mantenga coherencia con el modelo:</p> <p><strong>Escenario A (bosque denso).</strong> Alta proporción de celdas combustibles, con árboles en el rango de 80–90 %, sin presencia de agua y con un 10–20 % de celdas vacías. La ignición debe ubicarse en una zona central del bosque. <em>Hipótesis:</em> el sistema presentará alta conectividad espacial, favoreciendo la propagación extensa del incendio y una gran fracción final de área quemada.</p> <p><strong>Escenario B (paisaje disperso).</strong> Cobertura arbórea reducida (20–40 %), con presencia opcional de celdas vacías y mínima o nula agua. La ignición se localiza en un borde del dominio. <em>Hipótesis:</em> la baja densidad de combustible limita la propagación, produciendo incendios localizados y baja probabilidad de atravesar el dominio.</p> <p><strong>Escenario C (ribera o barrera acuática).</strong> Cobertura arbórea intermedia (60–70 %) combinada con una fracción de agua del 10–20 %, trazada manualmente para simular ríos o lagos. La ignición se coloca en la proximidad de la barrera acuática. <em>Hipótesis:</em> el agua actúa como cortafuego natural, fragmentando la mancha quemada y reduciendo el área final afectada.</p> <p><strong>Escenario D (sensibilidad al tipo de combustible).</strong> Mantenga constante la cobertura total de vegetación (por ejemplo, 70 %), pero modifique la proporción relativa entre pasto, arbusto y árbol. <em>Hipótesis:</em> una mayor proporción de combustibles de menor carga (pasto o arbusto) disminuye la probabilidad de propagación y reduce la velocidad de avance del incendio.</p> <p>De manera adicional, para cada escenario se deberá comparar la dinámica sin viento y con viento activado en las direcciones cardinales principales. Formule hipótesis sobre la ruptura de simetría espacial del frente de fuego y sobre el incremento de la velocidad efectiva de propagación en la dirección del viento 🌬️.</p> <h2>Bloque 2 — Diseño experimental y ejecución de réplicas 🔬</h2> <p><strong>Variables del estudio.</strong> Identifique claramente las variables manipuladas y las variables observables del sistema.</p> <p>Variables manipuladas: porcentajes de tipos de celdas (vacío, pasto, arbusto, árbol y agua), posición inicial de ignición (centro, borde o esquina), presencia y dirección del viento, y nivel de humedad ambiental.</p> <p>Variables observables (salidas del simulador): fracción de área quemada en función de los pasos de simulación, número de pasos hasta la extinción del incendio, paso en el que se alcanza un umbral definido de área quemada (por ejemplo, 50%), fracción máxima de celdas en llamas simultáneamente y mapas espaciales finales del estado del bosque.</p> <p><strong>Protocolo de ejecución.</strong> Para cada escenario definido, asegúrese de que la suma de porcentajes de celdas sea exactamente 100%. Ejecute un conjunto de réplicas independientes (recomendación mínima: 10 réplicas por condición) para capturar la variabilidad inherente al modelo probabilístico. En una primera fase, realice todas las réplicas sin viento; posteriormente repita el procedimiento activando el viento en cada dirección cardinal, manteniendo constantes el resto de los parámetros.</p> <h2>Bloque 3 — Análisis cuantitativo e interpretación 📈</h2> <p>Con los datos exportados, calcule estadísticos descriptivos básicos (media y desviación estándar) de la fracción de área quemada final, del tiempo hasta la extinción y de la fracción máxima de celdas en fuego. Analice si las distribuciones obtenidas son aproximadamente simétricas y discuta la variabilidad observada entre réplicas.</p> <p>A partir de las series temporales y de los mapas espaciales, interprete cualitativamente la velocidad de avance del frente de fuego y su dependencia con la densidad de combustible y el viento. Aunque el simulador no está calibrado a una escala temporal real, exprese los resultados de manera consistente en términos de celdas por paso y discuta las limitaciones de esta aproximación.</p> <h2>Resultados esperados y criterios de interpretación 🎯</h2> <p>Desde una perspectiva teórica, se espera que a bajas densidades de combustible el sistema se encuentre en un régimen subcrítico, caracterizado por incendios pequeños y aislados. Al incrementar la conectividad del paisaje, el sistema puede aproximarse a un umbral crítico donde la propagación extensa se vuelve más probable. La introducción de viento debe manifestarse como una propagación anisotrópica, mientras que la presencia de agua reduce la conectividad y limita el avance del incendio.</p> <p>Compare empíricamente los resultados entre escenarios y discuta si las hipótesis planteadas se sostienen. En caso contrario, reflexione sobre posibles causas, como un número insuficiente de réplicas, una configuración extrema de parámetros o las propias simplificaciones del modelo.</p> <h2>Recomendaciones finales y reproducibilidad ♻️</h2> <p>Conserve de manera organizada todos los productos del laboratorio: archivos CSV con las series temporales, imágenes PNG de los estados finales, y un registro claro de los parámetros utilizados en cada condición experimental. El informe final deberá incluir tablas y figuras comparativas, así como una discusión crítica de los resultados, enfatizando tanto las fortalezas como las limitaciones del simulador como herramienta de modelación didáctica.</p> <h2>CUESTIONARIO: MODELO DE INCENDIOS FORESTALES</h2> <p>Como refuerzo a la actividad, selecciona la mejor respuesta a cada pregunta.</p> <<set $correctas = 0>> <p><strong>1.</strong> ¿Qué función cumple el bosque de píxeles dentro de la simulación? <<listbox "$q1">> <<option " ">> <<option "Modelar un sistema discreto de interacción fuego–combustible">> <<option "Mostrar una imagen decorativa sin dinámica aparente del fuego">> <<option "Simular árboles individuales con crecimiento continuo">> <<option "Ilustrar únicamente la topografía del terreno incendiado">> <</listbox>> </p> <p><strong>2.</strong> ¿Por qué es indispensable que la suma de los porcentajes de los cuatro tipos de celdas sea 100%? Para... <<listbox "$q2">> <<option " ">> <<option "reducir el tamaño al mínimo de la malla">> <<option "garantizar la correcta topografía del terreno">> <<option "aumentar la velocidad de cálculo computacional">> <<option "garantizar una partición completa del espacio simulado">> <</listbox>> </p> <p><strong>3.</strong> La matriz de propagación 3×3 incluida en el simulador permite estudiar principalmente: <<listbox "$q3">> <<option " ">> <<option "La resolución gráfica de la simulación">> <<option "La interacción entre los combustibles">> <<option "El tamaño físico del terreno real">> <<option "La estabilidad numérica del algoritmo">> <</listbox>> </p> <p><strong>4.</strong> ¿Qué efecto introduce un aumento en la humedad ambiental del sistema? <<listbox "$q4">> <<option " ">> <<option "La eliminación local de la topografía incendiaría">> <<option "Un incremento directo de la velocidad del viento">> <<option "Un cambio en la forma de la malla en el bosque de píxeles">> <<option "Una reducción en la probabilidad de propagación del fuego">> <</listbox>> </p> <p><strong>5.</strong> Al modificar la dirección del viento mediante el control angular, el usuario está ajustando: <<listbox "$q5">> <<option " ">> <<option "La orientación de propagación del incendio">> <<option "El número de pasos de la simulación">> <<option "La cantidad de combustible inicial">> <<option "La escala espacial del modelo">> <</listbox>> </p> <p><strong>6.</strong> ¿Qué ventaja ofrece el botón de avance paso a paso en la simulación? <<listbox "$q6">> <<option " ">> <<option "Reducir el consumo de memoria de la computadora">> <<option "Aumentar la aleatoriedad del en la topografía del bosque">> <<option "Permitir el análisis de la dinámica del incendio">> <<option "Modificar automáticamente los parámetros de entrada">> <</listbox>> </p> <p><strong>7.</strong> Cuando se activa el modo de edición de topografía, ¿qué restricción se aplica al sistema? <<listbox "$q7">> <<option " ">> <<option "Se fija la velocidad en 20 fps">> <<option "No es posible iniciar incendios">> <<option "Se elimina la vegetación existente">> <<option "La simulación se ejecuta automáticamente">> <</listbox>> </p> <p><strong>8.</strong> ¿Cuál es el propósito de incluir agua como tipo adicional de celda? <<listbox "$q8">> <<option " ">> <<option "Incrementar la complejidad gráfica del escenario">> <<option "Añadir barreras a la propagación del fuego">> <<option "Representar zonas de ignición espontánea">> <<option "Aumentar la densidad total de celdas">> <</listbox>> </p> <p><strong>9.</strong> La gráfica de resultados situada debajo de la visualización permite analizar principalmente: <<listbox "$q9">> <<option " ">> <<option "La evolución porcentual de los estados del sistema">> <<option "La altura máxima del relieve en función de pasos">> <<option "La resolución temporal real del incendio">> <<option "La dirección media del viento">> <</listbox>> </p> <p><strong>10.</strong> ¿Qué objetivo central persigue la comparación entre distintos escenarios simulados? <<listbox "$q10">> <<option " ">> <<option "Evaluar el impacto de las condiciones iniciales en el incendio">> <<option "Optimizar el rendimiento del hardware en diferentes condiciones">> <<option "Evaluar la calidad estética de la interfaz al cambiar parámetros">> <<option "Determinar el tamaño ideal de pantalla en distintas circunstancias">> <</listbox>> </p> <center> <<button "Evaluar respuestas">> <<set $correctas = 0>> <<if $q1 is "Modelar un sistema discreto de interacción fuego–combustible">><<set $correctas += 1>><</if>> <<if $q2 is "garantizar una partición completa del espacio simulado">><<set $correctas += 1>><</if>> <<if $q3 is "La interacción entre los combustibles">><<set $correctas += 1>><</if>> <<if $q4 is "Una reducción en la probabilidad de propagación del fuego">><<set $correctas += 1>><</if>> <<if $q5 is "La orientación de propagación del incendio">><<set $correctas += 1>><</if>> <<if $q6 is "Permitir el análisis de la dinámica del incendio">><<set $correctas += 1>><</if>> <<if $q7 is "No es posible iniciar incendios">><<set $correctas += 1>><</if>> <<if $q8 is "Añadir barreras a la propagación del fuego">><<set $correctas += 1>><</if>> <<if $q9 is "La evolución porcentual de los estados del sistema">><<set $correctas += 1>><</if>> <<if $q10 is "Evaluar el impacto de las condiciones iniciales en el incendio">><<set $correctas += 1>><</if>> <<replace "#resultado">> <<if $correctas is 10>> <p>✅ Excelente: demuestras una comprensión sólida y profunda del modelo y sus fundamentos conceptuales.</p> <<elseif $correctas >= 7>> <p>🟡 Buen desempeño: comprendes los principios generales del simulador, aunque es recomendable revisar algunos detalles.</p> <<else>> <p>🔴 Revisa nuevamente las instrucciones y los conceptos básicos del simulador antes de continuar.</p> <</if>> <</replace>> <</button>> <div id="resultado"></div> </center> <</if>> <</replace>> <</button>> <div id="Preguntas"></div> </div> <p></p> <hr> <p></p> <div id="sim-wrap" style="display:flex; gap:20px; align-items:flex-start;"> <!-- PANEL DE CONTROL --> <div id="controlsPanel"> <div class="tabs"> <button class="active" data-tab="tabEscenario">Escenario</button> <button data-tab="tabPropagacion">Propagación</button> <button data-tab="tabClima">Clima</button> <button data-tab="tabSimulacion">Simulación</button> <button data-tab="tabEdicion">Edición</button> <button data-tab="tabRuido">Ruido</button> <button data-tab="tabDatos">Datos</button> </div> <div class="tab-content"> <!-- ================= ESCENARIO ================= --> <div class="tab-pane active" id="tabEscenario"> <h4>Distribución de Vegetación</h4> <!-- Porcentajes --> <!-- (SIN CAMBIAR IDS) --> <!-- Vacío --> <div> <label>Vacío (%)</label> <div style="display:flex; gap:8px;"> <input id="emptyPct" type="range" min="0" max="100" value="25" step="0.1" style="flex:1;"> <input id="emptyNum" type="number" min="0" max="100" value="25" step="0.1" style="width:80px;"> </div> </div> <!-- Pasto --> <div> <label>Pasto (%)</label> <div style="display:flex; gap:8px;"> <input id="grassPct" type="range" min="0" max="100" value="25" step="0.1" style="flex:1;"> <input id="grassNum" type="number" min="0" max="100" value="25" step="0.1" style="width:80px;"> </div> </div> <!-- Arbusto --> <div> <label>Arbusto (%)</label> <div style="display:flex; gap:8px;"> <input id="shrubPct" type="range" min="0" max="100" value="25" step="0.1" style="flex:1;"> <input id="shrubNum" type="number" min="0" max="100" value="25" step="0.1" style="width:80px;"> </div> </div> <!-- Árbol --> <div> <label>Árbol (%)</label> <div style="display:flex; gap:8px;"> <input id="treePct" type="range" min="0" max="100" value="25" step="0.1" style="flex:1;"> <input id="treeNum" type="number" min="0" max="100" value="25" step="0.1" style="width:80px;"> </div> </div> <div> <strong>Suma: <span id="sumPct">100</span>%</strong> <span id="sumWarning" style="color:crimson; display:none;"> — La suma debe ser exactamente 100%. </span> </div> <hr> <button id="generateBtn">Generar escenario</button> <button id="resetBtn">Reset parámetros</button> </div> <!-- ================= PROPAGACION ================= --> <div class="tab-pane" id="tabPropagacion"> <h4>Matriz de propagación</h4> <div style="display:grid; grid-template-columns: 1fr 1fr 1fr 60px; gap:6px;"> <div>Desde →</div><div>Pasto</div><div>Arbusto</div><div>Árbol</div> <div>Pasto</div> <input id="m11" type="number" min="0" max="50" value="20"> <input id="m12" type="number" min="0" max="50" value="10"> <input id="m13" type="number" min="0" max="50" value="5"> <div>Arbusto</div> <input id="m21" type="number" min="0" max="100" value="30"> <input id="m22" type="number" min="0" max="50" value="50"> <input id="m23" type="number" min="0" max="50" value="20"> <div>Árbol</div> <input id="m31" type="number" min="0" max="50" value="10"> <input id="m32" type="number" min="0" max="50" value="20"> <input id="m33" type="number" min="0" max="50" value="50"> </div> </div> <!-- ================= CLIMA ================= --> <div class="tab-pane" id="tabClima"> <label>Humedad (%)</label> <div style="display:flex; gap:8px;"> <input id="humidity" type="range" min="0" max="100" value="0" style="flex:1;"> <input id="humidityNum" type="number" min="0" max="100" value="0" style="width:80px;"> </div> <hr> <label>Viento</label> <div style="display:flex; gap:8px; align-items:center;"> <select id="windSpeed"> <option value="0">0</option> <option value="15">15</option> <option value="30">30</option> <option value="45">45</option> <option value="60">60</option> </select> <input id="windDir" type="range" min="0" max="360" value="0" style="flex:1;"> <input id="windDirNum" type="number" min="0" max="360" value="0" style="width:64px;"> </div> </div> <!-- ================= SIMULACION ================= --> <div class="tab-pane" id="tabSimulacion"> <label>FPS</label> <select id="fpsSelect"> <option value="1">1</option> <option value="5">5</option> <option value="10">10</option> <option value="20">20</option> </select> <hr> <button id="igniteBtn">Iniciar incendio</button> <button id="pauseBtn">⏸ Pausar</button> <button id="stepBtn">⏭ Paso</button> </div> <!-- ================= EDICION ================= --> <div class="tab-pane" id="tabEdicion"> <h4>Topografía</h4> <select id="topoBrushSize"> <option value="100">100×100</option> <option value="50">50×50</option> <option value="10">10×10</option> </select> <select id="topoMode"> <option value="gauss_raise">Gauss (Elevar)</option> <option value="gauss_lower">Gauss (Bajar)</option> <option value="step">Escalón</option> </select> <input id="topoMag" type="number" value="5" step="0.1"> <button id="enableTopoEdit">Activar</button> <button id="disableTopoEdit">Desactivar</button> <hr> <h4>Vegetación</h4> <select id="vegBrushSize"> <option value="100">100×100</option> <option value="50">50×50</option> <option value="10">10×10</option> </select> <select id="vegTool"> <option value="empty">Vacío</option> <option value="grass">Pasto</option> <option value="shrub">Arbusto</option> <option value="tree">Árbol</option> <option value="water">Agua</option> </select> <button id="enableVegEdit">Activar</button> <button id="disableVegEdit">Desactivar</button> </div> <!-- ================= RUIDO ================= --> <div class="tab-pane" id="tabRuido"> <label> <input id="usePerlin" type="checkbox"> Usar ruido </label> <div> Escala <input id="noiseScale" type="range" min="1" max="80" value="12"> <input id="noiseScaleNum" type="number" min="1" max="200" value="12"> </div> <div> Octavas <input id="noiseOctaves" type="number" min="1" max="8" value="4"> </div> <div> Persistencia <input id="noisePersistence" type="number" min="0.1" max="1" step="0.05" value="0.55"> </div> <div> Suavizado <input id="noiseSmoothIter" type="number" min="0" max="5" value="1"> </div> <div> Semilla <input id="noiseSeed" type="number" value="12345"> </div> <label> <input id="invertNoise" type="checkbox"> Invertir </label> </div> <!-- ================= DATOS ================= --> <div class="tab-pane" id="tabDatos"> <button id="exportCSVBtn">Exportar CSV</button> <button id="exportVegPNG">PNG Vegetación</button> <button id="exportTopoPNG">PNG Topografía</button> <hr> <input id="fileInput" type="file" accept=".json"> <button id="exportScenarioBtn">Exportar JSON</button> <button id="importScenarioBtn">Importar JSON</button> <hr> <p class="centered">[[Ir al menu de simuladores|Preambulo]]</p> </div> </div> </div> <!-- VISUAL (canvas vegetación + topografía + gráfico) --> <div id="visual-col" style="flex: 1 1 0; min-width: 70vw;"> <div style="display:flex; gap:8px; align-items:flex-start;"> <div style="flex:1;"> <center><strong> Bosque de píxeles</strong></center> <canvas id="forest" width="640" height="640" style="display:block; width: clamp(520px, 60vw, 980px); height:auto; image-rendering: pixelated; background:#f6f6f6;"></canvas> <div style="color:#444; margin-top:6px;">Escala: cada 50 celdas = 50 m (1 celda = 1 m)</div> </div> <div style="flex:1;"> <center><strong>Mapa topográfico</strong></center> <canvas id="topo" width="640" height="640" style="display:block; width: clamp(520px, 60vw, 980px); height:auto; background:#fff; border:1px solid #ddd;"></canvas> <div style="display:flex; gap:8px; align-items:center; margin-top:6px;"> <div>Escala altura:</div> <canvas id="colorScale" width="200" height="20" style="border:1px solid #ccc;"></canvas> </div> </div> </div> <hr> <h3 class="centered">Estadísticas en Vivo</h3> <div id="metrics" style="line-height:1.4;"> <span style="margin-right:15px;"><strong>Paso:</strong> <span id="metricStep">0</span></span> <span style="margin-right:15px;"><strong>Vacío:</strong> <span id="metricEmpty">0</span></span> <span style="margin-right:15px;"><strong>Pasto:</strong> <span id="metricGrass">0</span></span> <span style="margin-right:15px;"><strong>Arbusto:</strong> <span id="metricShrub">0</span></span> <span style="margin-right:15px;"><strong>Árbol:</strong> <span id="metricTree">0</span></span> <span style="margin-right:15px;"><strong>En llamas (actual):</strong> <span id="metricFire">0</span></span> <span><strong>Quemadas (acumulado):</strong> <span id="metricBurned">0</span> (<span id="metricBurnPct">0</span> %)</span> </div> <canvas id="chart" width="960" height="260" style="display:block; margin-top:10px; width:100%; background:#ffffff;"></canvas> </div> </div> <p></p> <hr> <p class="centered">[[Regresar al inicio|StoryInit]]</p> <script> (function(){ /* ====================== CONFIG y CONSTANTES ====================== */ const GRID_W = 200, GRID_H = 200; // 200x200 por requisito const TOTAL_CELLS = GRID_W * GRID_H; const CANVAS_PIX = 640; // resolución interna para ambos canvases (multiplo) const CELL_PIXEL = CANVAS_PIX / GRID_W; const COLORS = { veg: { 0: "#bfbfbf", // Vacío (gris) 1: "#bfffbf", // Pasto (verde claro) 2: "#59a86a", // Arbusto (verde medio) 3: "#1f5f2b", // Árbol (verde oscuro) 4: "#3ea6ff" // Agua (azul) }, fireStages: { 1: "#fff07a", // influencia termica - amarillo 2: "#ff9a2e", // calor avanzado - naranja 3: "#ff0000", // llamas - rojo intenso 4: "#ff9a2e", // llamas residuales - naranja 5: "#000000" // quemada - negro } }; // Duraciones por etapa (en pasos) por tipo de vegetación (index: vegType) const STAGE_DURATIONS = { // veg types: 1=pasto,2=arbusto,3=arbol 1: [1,1,1,1,1], // pasto: rápido 2: [1,1,2,2,1], // arbusto 3: [1,2,3,2,1] // árbol: más lento }; const IGNITION_SIZE = 2; // 2x2 const MAX_ELEV = 100, MIN_ELEV = -100; // topografía permitida const CONTOUR_INTERVAL = 2; // metros entre curvas de nivel const SCALE_CONTOUR = 50; // marca cada 50 celdas // parámetros de influencia const WIND_SCALE_BASE = 1.5; // coeficiente en el modelo lineal const SLOPE_SCALE = 0.007; // factor pequeño por metro (ajustable) -> lineal /* ====================== DOM refs ====================== */ const emptyPct = document.getElementById("emptyPct"), emptyNum = document.getElementById("emptyNum"); const grassPct = document.getElementById("grassPct"), grassNum = document.getElementById("grassNum"); const shrubPct = document.getElementById("shrubPct"), shrubNum = document.getElementById("shrubNum"); const treePct = document.getElementById("treePct"), treeNum = document.getElementById("treeNum"); const sumPctEl = document.getElementById("sumPct"), sumWarning = document.getElementById("sumWarning"); const m11 = document.getElementById("m11"), m12 = document.getElementById("m12"), m13 = document.getElementById("m13"); const m21 = document.getElementById("m21"), m22 = document.getElementById("m22"), m23 = document.getElementById("m23"); const m31 = document.getElementById("m31"), m32 = document.getElementById("m32"), m33 = document.getElementById("m33"); const humidity = document.getElementById("humidity"), humidityNum = document.getElementById("humidityNum"); const windSpeed = document.getElementById("windSpeed"), windDir = document.getElementById("windDir"), windDirNum = document.getElementById("windDirNum"); const fpsSelect = document.getElementById("fpsSelect"); const generateBtn = document.getElementById("generateBtn"), resetBtn = document.getElementById("resetBtn"); const exportCSVBtn = document.getElementById("exportCSVBtn"), exportVegPNG = document.getElementById("exportVegPNG"), exportTopoPNG = document.getElementById("exportTopoPNG"); const igniteBtn = document.getElementById("igniteBtn"), pauseBtn = document.getElementById("pauseBtn"), stepBtn = document.getElementById("stepBtn"); const topoBrushSize = document.getElementById("topoBrushSize"), topoMode = document.getElementById("topoMode"), topoMag = document.getElementById("topoMag"); const enableTopoEdit = document.getElementById("enableTopoEdit"), disableTopoEdit = document.getElementById("disableTopoEdit"); const vegBrushSize = document.getElementById("vegBrushSize"), vegTool = document.getElementById("vegTool"); const enableVegEdit = document.getElementById("enableVegEdit"), disableVegEdit = document.getElementById("disableVegEdit"); const fileInput = document.getElementById("fileInput"), exportScenarioBtn = document.getElementById("exportScenarioBtn"), importScenarioBtn = document.getElementById("importScenarioBtn"); const metricStep = document.getElementById("metricStep"); const metricEmpty = document.getElementById("metricEmpty"), metricGrass = document.getElementById("metricGrass"), metricShrub = document.getElementById("metricShrub"), metricTree = document.getElementById("metricTree"); const metricFire = document.getElementById("metricFire"), metricBurned = document.getElementById("metricBurned"), metricBurnPct = document.getElementById("metricBurnPct"); /* PERLIN / RUIDO DOM refs */ const usePerlin = document.getElementById("usePerlin"); const noiseScale = document.getElementById("noiseScale"), noiseScaleNum = document.getElementById("noiseScaleNum"); const noiseOctaves = document.getElementById("noiseOctaves"); const noisePersistence = document.getElementById("noisePersistence"); const noiseSeed = document.getElementById("noiseSeed"); const noiseSmoothIter = document.getElementById("noiseSmoothIter"); const invertNoise = document.getElementById("invertNoise"); /* canvases */ const canvas = document.getElementById("forest"), ctx = canvas.getContext("2d"); const topoCanvas = document.getElementById("topo"), topoCtx = topoCanvas.getContext("2d"); const chartCanvas = document.getElementById("chart"), chartCtx = chartCanvas.getContext("2d"); const colorScaleCanvas = document.getElementById("colorScale"), colorScaleCtx = colorScaleCanvas.getContext("2d"); canvas.width = CANVAS_PIX; canvas.height = CANVAS_PIX; topoCanvas.width = CANVAS_PIX; topoCanvas.height = CANVAS_PIX; /* ====================== ESTADO GLOBAL ====================== */ let vegGrid = []; // integer 0..4 for vegetation type let elevGrid = []; // float elevation in meters let fireGrid = []; // for fire state objects {stage:0-5, stageTimer:0, burningType:vegType} ; stage 0 = not on fire let running = false, timer = null; let stepCount = 0, simulatedTimeMs = 0, currentStepDelay = 1000; // delay controlled by fps selection (visual only) let seriesSteps = [], seriesEmpty = [], seriesGrass = [], seriesShrub = [], seriesTree = [], seriesFire = [], seriesBurned = []; let topoEditEnabled = false, vegEditEnabled = false; /* ====================== UTILIDADES ====================== */ function clamp(v,a,b){ return Math.max(a, Math.min(b, v)); } function randInt(a,b){ return Math.floor(Math.random()*(b-a+1))+a; } function cellSize(){ return CELL_PIXEL; } /* sincronizar input range <-> number with step precision */ function syncRangeNumber(rangeEl, numEl){ function applyFromRange(){ numEl.value = Number(rangeEl.value); updateSumAndToggleGenerate(); } function applyFromNum(){ let v = Number(numEl.value); if(isNaN(v)) v = 0; v = clamp(v, Number(rangeEl.min), Number(rangeEl.max)); // round to necessary precision numEl.value = v; rangeEl.value = v; updateSumAndToggleGenerate(); } rangeEl.addEventListener("input", applyFromRange); numEl.addEventListener("input", applyFromNum); } /* ====================== INICIALIZAR / RESET ====================== */ function initGrids(){ vegGrid = Array.from({length:GRID_H}, ()=> new Uint8Array(GRID_W)); // 0..4 elevGrid = Array.from({length:GRID_H}, ()=> new Float32Array(GRID_W)); fireGrid = Array.from({length:GRID_H}, ()=> Array.from({length:GRID_W}, ()=> ({stage:0, stageTimer:0, burningType:0}))); // initial elevation = 0 for(let y=0;y<GRID_H;y++) for(let x=0;x<GRID_W;x++) elevGrid[y][x] = 0; } function resetAll(){ stopSim(); stepCount = 0; simulatedTimeMs = 0; seriesSteps = []; seriesEmpty = []; seriesGrass = []; seriesShrub = []; seriesTree = []; seriesFire = []; seriesBurned = []; initGrids(); drawAll(); updateMetricsDisplay(); } /* ====================== Ruido 2D (value-noise + fBm) Implementación simple, determinista con semilla ====================== */ function mulberry32(a) { return function() { a |= 0; a = a + 0x6D2B79F5 | 0; var t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; } function hash2(x,y,seed){ // simple integer hash mixing; devuelve 0..1 let h = (x * 374761393 + y * 668265263 + Number(seed) * 2166136261) | 0; h = (h ^ (h >>> 13)) * 1274126177; return ((h ^ (h >>> 16)) >>> 0) / 4294967295; } function fade(t){ return t * t * t * (t * (t * 6 - 15) + 10); } function lerp(a,b,t){ return a + (b-a) * t; } function valueNoise2D(x,y,seed){ const xi = Math.floor(x), yi = Math.floor(y); const xf = x - xi, yf = y - yi; const v00 = hash2(xi, yi, seed); const v10 = hash2(xi+1, yi, seed); const v01 = hash2(xi, yi+1, seed); const v11 = hash2(xi+1, yi+1, seed); const ux = fade(xf), uy = fade(yf); const ix0 = lerp(v00, v10, ux); const ix1 = lerp(v01, v11, ux); return lerp(ix0, ix1, uy); } function fbm2D(x,y,options){ let {octaves, persistence, lacunarity, seed} = options; lacunarity = lacunarity || 2; let amp = 1, freq = 1; let sum = 0, max = 0; for(let o=0;o<octaves;o++){ sum += amp * valueNoise2D(x * freq, y * freq, seed + o*9271); max += amp; amp *= persistence; freq *= lacunarity; } return sum / max; } /* ====================== GENERAR ESCENARIO (ahora con opción ruido) ====================== */ function generateScenario(){ // valida suma == 100 const s = Number(emptyNum.value) + Number(grassNum.value) + Number(shrubNum.value) + Number(treeNum.value); if(Math.abs(s - 100) > 1e-6){ alert("La suma debe ser exactamente 100% para generar el escenario."); return; } resetAll(); const nTotal = GRID_W * GRID_H; const nEmpty = Math.round(Number(emptyNum.value)/100 * nTotal); const nGrass = Math.round(Number(grassNum.value)/100 * nTotal); const nShrub = Math.round(Number(shrubNum.value)/100 * nTotal); const nTree = nTotal - nEmpty - nGrass - nShrub; // if not using noise: fallback to previous random assignment if(!usePerlin.checked){ let all = []; for(let y=0;y<GRID_H;y++) for(let x=0;x<GRID_W;x++) all.push({x,y}); shuffleArray(all); let idx = 0; for(let i=0;i<nEmpty;i++){ const c=all[idx++]; vegGrid[c.y][c.x] = 0; } for(let i=0;i<nGrass;i++){ const c=all[idx++]; vegGrid[c.y][c.x] = 1; } for(let i=0;i<nShrub;i++){ const c=all[idx++]; vegGrid[c.y][c.x] = 2; } for(let i=0;i<nTree;i++){ const c=all[idx++]; vegGrid[c.y][c.x] = 3; } drawAll(); recordSeries(); return; } // *************** // Asignación guiada por ruido (fBm) // *************** const seed = Number(noiseSeed.value) || 1; const scale = Number(noiseScale.value) || 12; const octaves = Number(noiseOctaves.value) || 4; const persistence = Number(noisePersistence.value) || 0.55; const smoothIter = Number(noiseSmoothIter.value) || 0; const invert = invertNoise.checked; // compute noise map (0..1) const noiseMap = new Float32Array(nTotal); let p = 0; for(let y=0;y<GRID_H;y++){ for(let x=0;x<GRID_W;x++){ const nx = x / scale; const ny = y / scale; const v = fbm2D(nx, ny, {octaves, persistence, lacunarity:2, seed}); noiseMap[p++] = v; } } // optional: use noise for elevation (uncomment to enable) // const elevAmp = 60; // p = 0; // for(let y=0;y<GRID_H;y++){ for(let x=0;x<GRID_W;x++){ elevGrid[y][x] = (noiseMap[p++]*2 - 1) * elevAmp; } } // create array of cells with noise values let cells = []; p = 0; for(let y=0;y<GRID_H;y++){ for(let x=0;x<GRID_W;x++){ cells.push({x,y,val: noiseMap[p++]}); } } // sort descending (higher val favored to be trees). If invert -> reverse cells.sort((a,b)=> b.val - a.val); if(invert) cells.reverse(); // assign in blocks to guarantee exact percentages let idx = 0; for(let i=0;i<nTree;i++){ const c = cells[idx++]; vegGrid[c.y][c.x] = 3; } for(let i=0;i<nShrub;i++){ const c = cells[idx++]; vegGrid[c.y][c.x] = 2; } for(let i=0;i<nGrass;i++){ const c = cells[idx++]; vegGrid[c.y][c.x] = 1; } for(; idx < cells.length; idx++){ const c = cells[idx]; vegGrid[c.y][c.x] = 0; } // majority smoothing iterations to remove speckle for(let iter=0; iter<smoothIter; iter++){ const copy = Array.from({length:GRID_H}, ()=> new Uint8Array(GRID_W)); for(let y=0;y<GRID_H;y++){ for(let x=0;x<GRID_W;x++){ const counts = [0,0,0,0,0]; for(let dy=-1; dy<=1; dy++){ for(let dx=-1; dx<=1; dx++){ const nx = x+dx, ny = y+dy; if(nx<0||nx>=GRID_W||ny<0||ny>=GRID_H) continue; counts[ vegGrid[ny][nx] ]++; } } let maj = 0, mcount = -1; for(let t=0;t<counts.length;t++){ if(counts[t] > mcount){ mcount = counts[t]; maj = t; } } copy[y][x] = maj; } } for(let y=0;y<GRID_H;y++) vegGrid[y].set(copy[y]); } drawAll(); recordSeries(); } /* shuffle */ function shuffleArray(arr){ for(let i=arr.length-1;i>0;i--){ const j = Math.floor(Math.random()*(i+1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } } /* ====================== DIBUJO VEGETACION ====================== */ function drawVegetation(){ const cs = CELL_PIXEL; for(let y=0;y<GRID_H;y++){ for(let x=0;x<GRID_W;x++){ const t = vegGrid[y][x]; ctx.fillStyle = COLORS.veg[t] || COLORS.veg[0]; ctx.fillRect(Math.floor(x*cs), Math.floor(y*cs), Math.ceil(cs), Math.ceil(cs)); } } // grid scale markers every SCALE_CONTOUR cells ctx.strokeStyle = "rgba(0,0,0,1)"; ctx.lineWidth = 1; const step = SCALE_CONTOUR; for(let x=0;x<GRID_W;x+=step){ const px = Math.floor(x*cs); ctx.beginPath(); ctx.moveTo(px,0); ctx.lineTo(px,CANVAS_PIX); ctx.stroke(); } for(let y=0;y<GRID_H;y+=step){ const py = Math.floor(y*cs); ctx.beginPath(); ctx.moveTo(0,py); ctx.lineTo(CANVAS_PIX,py); ctx.stroke(); } } /* dibuja fuego sobre la vegetación según fireGrid */ function drawFireOverlay(){ const cs = CELL_PIXEL; for(let y=0;y<GRID_H;y++){ for(let x=0;x<GRID_W;x++){ const f = fireGrid[y][x]; if(f.stage > 0 && f.stage < 5){ ctx.fillStyle = COLORS.fireStages[f.stage]; ctx.fillRect(Math.floor(x*cs), Math.floor(y*cs), Math.ceil(cs), Math.ceil(cs)); } else if(f.stage === 5){ ctx.fillStyle = COLORS.fireStages[5]; ctx.fillRect(Math.floor(x*cs), Math.floor(y*cs), Math.ceil(cs), Math.ceil(cs)); } } } } /* dibuja todo (veg + fuego) */ function drawAll(){ ctx.clearRect(0,0,CANVAS_PIX,CANVAS_PIX); drawVegetation(); drawFireOverlay(); } /* ====================== DIBUJO TOPOGRAFÍA y CURVAS DE NIVEL ====================== */ function drawTopo(){ // color mapping: blue (-100) -> green (0) -> brown (100) const cs = CELL_PIXEL; topoCtx.clearRect(0,0,CANVAS_PIX,CANVAS_PIX); // draw color field for(let y=0;y<GRID_H;y++){ for(let x=0;x<GRID_W;x++){ const h = clamp(elevGrid[y][x], MIN_ELEV, MAX_ELEV); topoCtx.fillStyle = elevationToColor(h); topoCtx.fillRect(Math.floor(x*cs), Math.floor(y*cs), Math.ceil(cs), Math.ceil(cs)); } } // draw contours: crude method detecting crossing of contour intervals at edges topoCtx.strokeStyle = "rgba(0,0,0,0.5)"; topoCtx.lineWidth = 1; for(let y=0;y<GRID_H-1;y++){ for(let x=0;x<GRID_W-1;x++){ const h00 = elevGrid[y][x], h10 = elevGrid[y][x+1], h01 = elevGrid[y+1][x], h11 = elevGrid[y+1][x+1]; const minH = Math.min(h00,h10,h01,h11), maxH = Math.max(h00,h10,h01,h11); // determine contour levels crossed const c0 = Math.floor(minH/CONTOUR_INTERVAL), c1 = Math.floor(maxH/CONTOUR_INTERVAL); if(c1 > c0){ // draw small cross lines inside the cell to mark contour crossing const px = x*cs, py = y*cs; topoCtx.beginPath(); topoCtx.moveTo(px+cs*0.2, py+cs*0.2); topoCtx.lineTo(px+cs*0.8, py+cs*0.8); topoCtx.moveTo(px+cs*0.8, py+cs*0.2); topoCtx.lineTo(px+cs*0.2, py+cs*0.8); topoCtx.stroke(); } } } // --- MARCAS DE ESCALA (cada SCALE_CONTOUR celdas) const topoCs = CELL_PIXEL; // alias local seguro topoCtx.save(); topoCtx.lineWidth = 1; // Líneas internas muy tenues (rejilla cada SCALE_CONTOUR celdas) topoCtx.strokeStyle = "rgba(0,0,0,0.10)"; for (let x = 0; x < GRID_W; x += SCALE_CONTOUR) { const px = Math.floor(x * topoCs) + 0.5; topoCtx.beginPath(); topoCtx.moveTo(px, 0); topoCtx.lineTo(px, CANVAS_PIX); topoCtx.stroke(); } for (let y = 0; y < GRID_H; y += SCALE_CONTOUR) { const py = Math.floor(y * topoCs) + 0.5; topoCtx.beginPath(); topoCtx.moveTo(0, py); topoCtx.lineTo(CANVAS_PIX, py); topoCtx.stroke(); } // Ticks y etiquetas en bordes (no tantas para evitar solapamiento) topoCtx.fillStyle = "rgba(0,0,0,0.85)"; topoCtx.font = "10px sans-serif"; const labelSkip = 1; topoCtx.textAlign = "center"; topoCtx.textBaseline = "top"; for (let ix = 0, x = 0; x < GRID_W; x += SCALE_CONTOUR, ix++) { const px = Math.floor(x * topoCs) + 0.5; topoCtx.beginPath(); topoCtx.moveTo(px, 0); topoCtx.lineTo(px, 8); topoCtx.strokeStyle = "rgba(0,0,0,0.6)"; topoCtx.stroke(); if (ix % labelSkip === 0) { topoCtx.fillText(`${x} m`, px, 10); } } topoCtx.textAlign = "left"; topoCtx.textBaseline = "middle"; for (let iy = 0, y = 0; y < GRID_H; y += SCALE_CONTOUR, iy++) { const py = Math.floor(y * topoCs) + 0.5; topoCtx.beginPath(); topoCtx.moveTo(0, py); topoCtx.lineTo(8, py); topoCtx.strokeStyle = "rgba(0,0,0,0.6)"; topoCtx.stroke(); if (iy % labelSkip === 0) { topoCtx.fillText(`${y} m`, 10, py); } } topoCtx.restore(); // draw color scale drawColorScale(); } function elevationToColor(h){ const t = (h - MIN_ELEV) / (MAX_ELEV - MIN_ELEV); // 0..1 if(t < 0.5){ const f = t / 0.5; return lerpColor("#2b6bff", "#7fe07f", f); } else { const f = (t-0.5)/0.5; return lerpColor("#7fe07f", "#8b5a2b", f); } } function lerpColor(a,b,t){ const pa = hexToRgb(a), pb = hexToRgb(b); const r = Math.round(pa.r + (pb.r-pa.r)*t); const g = Math.round(pa.g + (pb.g-pa.g)*t); const bl = Math.round(pa.b + (pb.b-pa.b)*t); return `rgb(${r},${g},${bl})`; } function hexToRgb(hex){ hex = hex.replace("#",""); return {r:parseInt(hex.substring(0,2),16), g:parseInt(hex.substring(2,4),16), b:parseInt(hex.substring(4,6),16)}; } function drawColorScale(){ const w = colorScaleCanvas.width, h = colorScaleCanvas.height; const ctxs = colorScaleCtx; const grad = ctxs.createLinearGradient(0,0,w,0); const steps = 100; for(let i=0;i<=steps;i++){ const t = i/steps; grad.addColorStop(t, elevationToColor(MIN_ELEV + t*(MAX_ELEV - MIN_ELEV))); } ctxs.fillStyle = grad; ctxs.fillRect(0,0,w,h); ctxs.fillStyle = "#000"; ctxs.font = "10px sans-serif"; ctxs.fillText(`${MAX_ELEV} m`, w-28, h-3); ctxs.fillText(`${MIN_ELEV} m`, 2, h-3); } /* ====================== MÁTRIZ DE PROPAGACIÓN ====================== */ function readPropagationMatrix(){ return [ [Number(m11.value)/100, Number(m12.value)/100, Number(m13.value)/100], [Number(m21.value)/100, Number(m22.value)/100, Number(m23.value)/100], [Number(m31.value)/100, Number(m32.value)/100, Number(m33.value)/100] ]; } /* ====================== MODELO DE PROPAGACIÓN ====================== */ function neighbors(x,y){ let n=[]; for(let dx=-1; dx<=1; dx++){ for(let dy=-1; dy<=1; dy++){ if(dx===0 && dy===0) continue; const nx = x+dx, ny = y+dy; if(nx>=0 && nx<GRID_W && ny>=0 && ny<GRID_H) n.push({x:nx,y:ny,dx,dy}); } } return n; } function windVector(){ const deg = Number(windDirNum.value); const rad = deg * Math.PI/180; return {vx: Math.cos(rad), vy: -Math.sin(rad)}; // vy inverted because canvas y increases downward } function windMultiplier(dx,dy){ const speed = Number(windSpeed.value); if(speed === 0) return 1; const v = windVector(); const mag = Math.sqrt(dx*dx+dy*dy); const ndx = dx/mag, ndy = dy/mag; const align = v.vx*ndx + v.vy*ndy; // -1..1 return Math.max(0, 1 + (speed/60) * align * WIND_SCALE_BASE); } function humidityFactor(){ const h = Number(humidityNum.value); // 0..100 return 1 - (h/100); // linear: 100% humidity -> 0 propagation } /* topography effect */ function slopeMultiplier(srcX, srcY, nx, ny){ const dz = elevGrid[ny][nx] - elevGrid[srcY][srcX]; return Math.max(0, 1 + SLOPE_SCALE * dz); } const STAGE_PROP_MULT = {1:0.25, 2:0.5, 3:1.0, 4:0.5}; /* step simulation */ function step(){ const prop = readPropagationMatrix(); const humF = humidityFactor(); let anyFire = false; const newIgnitions = []; for(let y=0;y<GRID_H;y++){ for(let x=0;x<GRID_W;x++){ const f = fireGrid[y][x]; if(f.stage > 0 && f.stage < 5){ anyFire = true; const multStage = STAGE_PROP_MULT[f.stage] || 1; if(multStage > 0){ const nbs = neighbors(x,y); for(const nb of nbs){ const t = vegGrid[nb.y][nb.x]; // vegetation type target (0..4) if(t === 4 || t === 0) continue; // water (4) or empty (0) cannot ignite const fromType = f.burningType; if(!fromType) continue; const fromIndex = fromType - 1; const toIndex = t - 1; let baseP = prop[fromIndex][toIndex] || 0; // fraction let p = baseP * humF * windMultiplier(nb.dx, nb.dy) * slopeMultiplier(x,y,nb.x,nb.y) * multStage; p = clamp(p, 0, 1); if(Math.random() < p){ const targetFire = fireGrid[nb.y][nb.x]; if(targetFire.stage === 0){ newIgnitions.push({x:nb.x,y:nb.y, vegType:t}); } } } } } } } for(const ign of newIgnitions){ fireGrid[ign.y][ign.x].stage = 1; fireGrid[ign.y][ign.x].stageTimer = 0; fireGrid[ign.y][ign.x].burningType = ign.vegType; } for(let y=0;y<GRID_H;y++){ for(let x=0;x<GRID_W;x++){ const f = fireGrid[y][x]; if(f.stage > 0 && f.stage < 5){ f.stageTimer++; const vegType = f.burningType; const durations = STAGE_DURATIONS[vegType] || [1,1,1,1,1]; const currentDuration = durations[f.stage-1] || 1; if(f.stageTimer >= currentDuration){ f.stage++; f.stageTimer = 0; if(f.stage === 5){ // burned final } } } } } stepCount++; const fps = Number(fpsSelect.value); const stepMs = Math.round(1000 / (fps || 1)); simulatedTimeMs += stepMs; drawAll(); drawTopo(); recordSeries(); if(!anyFire && newIgnitions.length === 0){ stopSim(); computeFinalStats(); } } /* ====================== METRICS / SERIES / CHART ====================== */ function countStates(){ let cnt = {empty:0, grass:0, shrub:0, tree:0, fire:0, burned:0}; for(let y=0;y<GRID_H;y++){ for(let x=0;x<GRID_W;x++){ const v = vegGrid[y][x]; if(fireGrid[y][x].stage === 5) cnt.burned++; else if(fireGrid[y][x].stage > 0) cnt.fire++; else { if(v === 0) cnt.empty++; else if(v === 1) cnt.grass++; else if(v === 2) cnt.shrub++; else if(v === 3) cnt.tree++; } } } return cnt; } function recordSeries(){ const cnt = countStates(); const emptyPctVal = (cnt.empty / TOTAL_CELLS) * 100; const grassPctVal = (cnt.grass / TOTAL_CELLS) * 100; const shrubPctVal = (cnt.shrub / TOTAL_CELLS) * 100; const treePctVal = (cnt.tree / TOTAL_CELLS) * 100; const firePctVal = (cnt.fire / TOTAL_CELLS) * 100; const burnedPctVal = (cnt.burned / TOTAL_CELLS) * 100; seriesSteps.push(stepCount); seriesEmpty.push(emptyPctVal); seriesGrass.push(grassPctVal); seriesShrub.push(shrubPctVal); seriesTree.push(treePctVal); seriesFire.push(firePctVal); seriesBurned.push(burnedPctVal); updateMetricsDisplay(cnt); drawChart(); } function updateMetricsDisplay(cnt = null){ if(cnt === null){ metricStep.textContent = stepCount; metricEmpty.textContent = 0; metricGrass.textContent = 0; metricShrub.textContent = 0; metricTree.textContent = 0; metricFire.textContent = 0; metricBurned.textContent = 0; metricBurnPct.textContent = "0.00"; return; } metricStep.textContent = stepCount; metricEmpty.textContent = cnt.empty; metricGrass.textContent = cnt.grass; metricShrub.textContent = cnt.shrub; metricTree.textContent = cnt.tree; metricFire.textContent = cnt.fire; metricBurned.textContent = cnt.burned; metricBurnPct.textContent = ((cnt.burned / TOTAL_CELLS)*100).toFixed(2); } /* ====================== GRAFICA SIMPLE (series) ====================== */ function drawChart(){ chartCtx.clearRect(0,0,chartCanvas.width,chartCanvas.height); const w = chartCanvas.width, h = chartCanvas.height; const ml = 60, mr = 20, mt = 20, mb = 40; chartCtx.strokeStyle = "#000"; chartCtx.lineWidth = 1; chartCtx.beginPath(); chartCtx.moveTo(ml, mt); chartCtx.lineTo(ml, h-mb); chartCtx.lineTo(w-mr, h-mb); chartCtx.stroke(); chartCtx.font = "11px sans-serif"; chartCtx.textAlign = "center"; chartCtx.fillStyle = "#000"; const nSteps = seriesSteps.length; if (nSteps > 1) { const MAX_TICKS = 10; const stepTick = Math.max(1, Math.ceil(nSteps / MAX_TICKS)); for (let i = 0; i < nSteps; i += stepTick) { const x = ml + (i / (nSteps - 1)) * (w - ml - mr); chartCtx.beginPath(); chartCtx.moveTo(x, h - mb); chartCtx.lineTo(x, h - mb + 6); chartCtx.stroke(); chartCtx.fillText(seriesSteps[i], x, h - mb + 18); } } chartCtx.font = "12px sans-serif"; chartCtx.textAlign = "right"; for(let v=0; v<=100; v+=25){ const y = h - mb - (v/100)*(h-mt-mb); chartCtx.fillText(v.toString(), ml-8, y+4); chartCtx.strokeStyle="#eaeaea"; chartCtx.beginPath(); chartCtx.moveTo(ml,y); chartCtx.lineTo(w-mr,y); chartCtx.stroke(); chartCtx.strokeStyle="#000"; } const series = [ {data: seriesTree, color:"#1f5f2b"}, {data: seriesGrass, color:"#bfffbf"}, {data: seriesShrub, color:"#59a86a"}, {data: seriesFire, color:"#ff0000"}, {data: seriesBurned, color:"#000000"} ]; if(series[0].data.length === 0) return; function xAt(i){ return ml + (i/(series[0].data.length-1))*(w-ml-mr); } function yAt(p){ return h-mb - (p/100)*(h-mt-mb); } series.forEach(s=>{ chartCtx.beginPath(); chartCtx.lineWidth = 2; chartCtx.strokeStyle = s.color; s.data.forEach((v,i)=>{ const x = xAt(i), y = yAt(v); if(i===0) chartCtx.moveTo(x,y); else chartCtx.lineTo(x,y); }); chartCtx.stroke(); }); chartCtx.font = "11px sans-serif"; chartCtx.textAlign = "left"; const labels = [{txt:"Árbol",col:"#1f5f2b"},{txt:"Pasto",col:"#bfffbf"},{txt:"Arbusto",col:"#59a86a"},{txt:"Incendiada",col:"#ff0000"},{txt:"Quemada",col:"#000"}]; labels.forEach((L,i)=>{ chartCtx.fillStyle = L.col; chartCtx.fillRect(ml + i*120, 6, 12, 12); chartCtx.fillStyle = "#000"; chartCtx.fillText(L.txt, ml + 18 + i*120, 16); }); chartCtx.save(); chartCtx.font = "14px sans-serif"; chartCtx.textAlign = "center"; chartCtx.fillStyle = "#000"; chartCtx.fillText("Paso de simulación", ml + (w - ml - mr) / 2, h - 8); chartCtx.restore(); chartCtx.save(); chartCtx.translate(14, mt + (h - mt - mb) / 2); chartCtx.rotate(-Math.PI / 2); chartCtx.font = "14px sans-serif"; chartCtx.textAlign = "center"; chartCtx.fillStyle = "#000"; chartCtx.fillText("Porcentaje de celdas (%)", 0, 0); chartCtx.restore(); } /* ====================== INICIO / PAUSA / STOP SIM ====================== */ function startSim(){ if(running) return; running = true; const fps = Number(fpsSelect.value) || 1; currentStepDelay = Math.round(1000 / fps); clearInterval(timer); timer = setInterval(step, currentStepDelay); } function stopSim(){ running = false; clearInterval(timer); timer = null; } /* ====================== CLICK PARA DEFINIR FUEGO (2x2) ====================== */ canvas.addEventListener("click", e=>{ const rect = canvas.getBoundingClientRect(); const xClick = Math.floor((e.clientX - rect.left) * (CANVAS_PIX / rect.width)); const yClick = Math.floor((e.clientY - rect.top) * (CANVAS_PIX / rect.height)); const cs = CELL_PIXEL; const cx = Math.floor(xClick / cs), cy = Math.floor(yClick / cs); for(let dy=0; dy<IGNITION_SIZE; dy++){ for(let dx=0; dx<IGNITION_SIZE; dx++){ const nx = cx + dx, ny = cy + dy; if(nx>=0 && nx<GRID_W && ny>=0 && ny<GRID_H){ const t = vegGrid[ny][nx]; if(t === 1 || t === 2 || t === 3){ fireGrid[ny][nx].stage = 1; fireGrid[ny][nx].stageTimer = 0; fireGrid[ny][nx].burningType = t; } } } } drawAll(); recordSeries(); }); igniteBtn.addEventListener("click", ()=>{ let any = false; for(let y=0;y<GRID_H && !any;y++){ for(let x=0;x<GRID_W;x++){ if(fireGrid[y][x].stage > 0) { any = true; break; } } } if(!any){ alert("Define con un clic el punto de ignición (2×2 celdas) antes de iniciar."); return; } startSim(); }); pauseBtn.addEventListener("click", ()=>{ stopSim(); }); stepBtn.addEventListener("click", ()=>{ if(!running) step(); }); /* ====================== EXPORT / IMPORT ====================== */ function exportCSV(){ const header = ["paso","empty_pct","grass_pct","shrub_pct","tree_pct","fire_pct","burned_pct"]; let csv = header.join(",") + "\n"; for(let i=0;i<seriesSteps.length;i++){ const row = [ seriesSteps[i], seriesEmpty[i].toFixed(6), seriesGrass[i].toFixed(6), seriesShrub[i].toFixed(6), seriesTree[i].toFixed(6), seriesFire[i].toFixed(6), seriesBurned[i].toFixed(6) ]; csv += row.join(",") + "\n"; } const blob = new Blob([csv], {type:"text/csv;charset=utf-8;"}); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "simulacion_series.csv"; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } exportCSVBtn.addEventListener("click", exportCSV); exportVegPNG.addEventListener("click", ()=>{ const dataURL = canvas.toDataURL("image/png"); const a = document.createElement("a"); a.href = dataURL; a.download = `vegetacion_step_${stepCount}.png`; document.body.appendChild(a); a.click(); document.body.removeChild(a); }); exportTopoPNG.addEventListener("click", ()=>{ const dataURL = topoCanvas.toDataURL("image/png"); const a = document.createElement("a"); a.href = dataURL; a.download = `topografia_step_${stepCount}.png`; document.body.appendChild(a); a.click(); document.body.removeChild(a); }); exportScenarioBtn.addEventListener("click", ()=>{ const data = {veg:[], elev:[]}; for(let y=0;y<GRID_H;y++){ data.veg.push(Array.from(vegGrid[y])); data.elev.push(Array.from(elevGrid[y])); } const blob = new Blob([JSON.stringify(data)], {type:"application/json"}); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "escenario.json"; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }); importScenarioBtn.addEventListener("click", ()=>{ const file = fileInput.files[0]; if(!file){ alert("Selecciona un archivo JSON primero"); return; } const reader = new FileReader(); reader.onload = (ev)=>{ try{ const parsed = JSON.parse(ev.target.result); if(parsed.veg && parsed.elev && parsed.veg.length === GRID_H && parsed.elev.length === GRID_H){ for(let y=0;y<GRID_H;y++){ for(let x=0;x<GRID_W;x++){ vegGrid[y][x] = parsed.veg[y][x]; elevGrid[y][x] = parsed.elev[y][x]; fireGrid[y][x] = {stage:0, stageTimer:0, burningType:0}; } } drawAll(); drawTopo(); recordSeries(); alert("Escenario importado correctamente."); } else { alert("Formato de archivo inválido o dimensiones erróneas."); } } catch(err){ alert("Error leyendo JSON: " + err); } }; reader.readAsText(file); }); /* ====================== TOOLS: Topografía & Vegetación (edición por ratón) ====================== */ let mouseDown = false; let activeMode = null; canvas.addEventListener("mousedown", e=>{ mouseDown = true; canvasDispatch(e); }); canvas.addEventListener("mouseup", ()=>{ mouseDown = false; }); canvas.addEventListener("mouseleave", ()=>{ mouseDown = false; }); canvas.addEventListener("mousemove", e=>{ if(mouseDown) canvasDispatch(e); }); function canvasDispatch(e){ const rect = canvas.getBoundingClientRect(); const xClick = Math.floor((e.clientX - rect.left) * (CANVAS_PIX / rect.width)); const yClick = Math.floor((e.clientY - rect.top) * (CANVAS_PIX / rect.height)); const cs = CELL_PIXEL; const cx = Math.floor(xClick / cs), cy = Math.floor(yClick / cs); if(topoEditEnabled){ applyTopoBrush(cx,cy); } if(vegEditEnabled){ applyVegBrush(cx,cy); } drawAll(); drawTopo(); recordSeries(); } /* Topo brush */ function applyTopoBrush(cx,cy){ const size = Number(topoBrushSize.value); const half = Math.floor(size/2); const mode = topoMode.value; const mag = Number(topoMag.value); for(let dy=-half; dy<half; dy++){ for(let dx=-half; dx<half; dx++){ const x = cx + dx, y = cy + dy; if(x<0||x>=GRID_W||y<0||y>=GRID_H) continue; if(mode.startsWith("gauss")){ const dist = Math.sqrt(dx*dx + dy*dy); const sigma = size/3; const factor = Math.exp(- (dist*dist) / (2*sigma*sigma)); const delta = (mode === "gauss_raise" ? 1 : -1) * mag * factor; elevGrid[y][x] = clamp(elevGrid[y][x] + delta, MIN_ELEV, MAX_ELEV); } else if(mode === "step"){ elevGrid[y][x] = clamp(elevGrid[y][x] + mag, MIN_ELEV, MAX_ELEV); } } } } /* Veg brush */ function applyVegBrush(cx,cy){ const size = Number(vegBrushSize.value); const half = Math.floor(size/2); const tool = vegTool.value; let value = 0; if(tool === "empty") value = 0; else if(tool === "grass") value = 1; else if(tool === "shrub") value = 2; else if(tool === "tree") value = 3; else if(tool === "water") value = 4; for(let dy=-half; dy<half; dy++){ for(let dx=-half; dx<half; dx++){ const x = cx + dx, y = cy + dy; if(x<0||x>=GRID_W||y<0||y>=GRID_H) continue; vegGrid[y][x] = value; } } } enableTopoEdit.addEventListener("click", ()=>{ topoEditEnabled = true; alert("Edición topografía ACTIVADA. Mantén clic para dibujar."); }); disableTopoEdit.addEventListener("click", ()=>{ topoEditEnabled = false; alert("Edición topografía DESACTIVADA."); }); enableVegEdit.addEventListener("click", ()=>{ vegEditEnabled = true; alert("Edición vegetación ACTIVADA. Mantén clic para pintar."); }); disableVegEdit.addEventListener("click", ()=>{ vegEditEnabled = false; alert("Edición vegetación DESACTIVADA."); }); /* ====================== Suma porcentajes y controles ====================== */ function updateSumAndToggleGenerate(){ const s = Number(emptyNum.value) + Number(grassNum.value) + Number(shrubNum.value) + Number(treeNum.value); sumPctEl.textContent = Number(s).toFixed(2); if(Math.abs(s - 100) > 1e-6){ sumWarning.style.display = "inline"; generateBtn.disabled = true; } else { sumWarning.style.display = "none"; generateBtn.disabled = false; } } /* sync ranges and numbers */ syncRangeNumber(emptyPct, emptyNum); syncRangeNumber(grassPct, grassNum); syncRangeNumber(shrubPct, shrubNum); syncRangeNumber(treePct, treeNum); noiseScale.addEventListener("input", ()=>{ noiseScaleNum.value = noiseScale.value; }); noiseScaleNum.addEventListener("input", ()=>{ noiseScale.value = noiseScaleNum.value; }); humidity.addEventListener("input", ()=>{ humidityNum.value = humidity.value; }); humidityNum.addEventListener("input", ()=>{ humidity.value = humidityNum.value; }); windDir.addEventListener("input", ()=>{ windDirNum.value = windDir.value; }); windDirNum.addEventListener("input", ()=>{ windDir.value = windDirNum.value; }); /* defaults reset */ function resetControlsToDefaults(){ emptyPct.value = 25; emptyNum.value = 25; grassPct.value = 25; grassNum.value = 25; shrubPct.value = 25; shrubNum.value = 25; treePct.value = 25; treeNum.value = 25; m11.value = 20; m12.value = 10; m13.value = 5; m21.value = 30; m22.value = 50; m23.value = 20; m31.value = 10; m32.value = 20; m33.value = 50; humidity.value = 0; humidityNum.value = 0; windSpeed.value = 0; windDir.value = 0; windDirNum.value = 0; fpsSelect.value = 20; // noise defaults usePerlin.checked = true; noiseScale.value = 12; noiseScaleNum.value = 12; noiseOctaves.value = 4; noisePersistence.value = 0.55; noiseSmoothIter.value = 1; noiseSeed.value = 12345; invertNoise.checked = false; updateSumAndToggleGenerate(); } resetBtn.addEventListener("click", ()=>{ resetControlsToDefaults(); }); /* generate event */ generateBtn.addEventListener("click", ()=>{ generateScenario(); }); /* ====================== Inicialización y dibujado inicial ====================== */ resetAll(); resetControlsToDefaults(); generateScenario(); // genera escenario por defecto /* ====================== Estadísticas finales ====================== */ function computeFinalStats(){ console.log("Simulación finalizada en pasos:", stepCount, "tiempo simulado (ms):", simulatedTimeMs); } /* ====================== Helpers y final touches ====================== */ /* small helper to allow numeric inputs to limit precision to 3 sig figs if needed (we keep step=0.001 where applies) */ })(); </script> <style> /* asegurar buena responsividad */ @media (max-width: 900px) { #sim-wrap { flex-direction: column; } #controlsPanel { width: 100%; } #visual-col { max-width: none; min-width: auto; } } @media (min-width: 1000px) { /* en pantallas muy anchas cede aún más espacio al visual */ #visual-col { flex-basis: 90%; } #controlsPanel { width: 380px; } } /* cursor crosshair por defecto en el canvas de bosque (fallback CSS) */ #forest { cursor: crosshair; } </style>