Improve wildfire metrics and training assets
Browse files- frontend/app.js +120 -24
- frontend/index.html +26 -6
- frontend/style.css +14 -2
frontend/app.js
CHANGED
|
@@ -11,6 +11,69 @@
|
|
| 11 |
|
| 12 |
"use strict";
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
// ββ Simulation state ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 15 |
const sim = {
|
| 16 |
obs: null, // current Observation (agent's view)
|
|
@@ -161,17 +224,28 @@ function renderCanvas(obs, groundTruth = null) {
|
|
| 161 |
}
|
| 162 |
|
| 163 |
// ββ Stats panel βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 164 |
-
function updateStats(
|
| 165 |
-
if (!stats) return;
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
const
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
setText("stat-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
setText(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
|
| 176 |
// Cumulative reward
|
| 177 |
setText("reward-total", cumulativeReward.toFixed(3));
|
|
@@ -298,31 +372,53 @@ function updateActionLog(action) {
|
|
| 298 |
}
|
| 299 |
|
| 300 |
// ββ Terminal overlay ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 301 |
-
function showTerminal(
|
| 302 |
const overlay = document.getElementById("terminal-overlay");
|
| 303 |
if (!overlay) return;
|
| 304 |
|
| 305 |
-
const stats = obs?.stats ?? {};
|
| 306 |
-
const popLost = stats.population_lost ?? 0;
|
| 307 |
-
const containment = stats.containment_pct ?? 0;
|
| 308 |
-
|
| 309 |
const card = document.getElementById("terminal-card");
|
|
|
|
|
|
|
|
|
|
| 310 |
const title = card.querySelector("h2");
|
| 311 |
|
| 312 |
-
if (popLost === 0) {
|
| 313 |
-
title.textContent = "β
|
| 314 |
title.className = "win";
|
| 315 |
} else {
|
| 316 |
title.textContent = "β EPISODE ENDED";
|
| 317 |
title.className = "loss";
|
| 318 |
}
|
| 319 |
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
setText("terminal-
|
| 323 |
-
setText("terminal-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
|
| 325 |
overlay.classList.add("show");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
}
|
| 327 |
|
| 328 |
function hideTerminal() {
|
|
@@ -356,7 +452,7 @@ async function apiGet(path) {
|
|
| 356 |
function applyObservation(obs) {
|
| 357 |
sim.obs = obs;
|
| 358 |
renderCanvas(obs, sim.groundTruthData);
|
| 359 |
-
updateStats(obs
|
| 360 |
updateResources(obs.resources);
|
| 361 |
updateWeather(obs.weather);
|
| 362 |
updateEvents(obs.recent_events ?? []);
|
|
@@ -417,7 +513,7 @@ async function doAutoStep() {
|
|
| 417 |
|
| 418 |
if (snap.done) {
|
| 419 |
stopPlay();
|
| 420 |
-
showTerminal(
|
| 421 |
break;
|
| 422 |
}
|
| 423 |
}
|
|
|
|
| 11 |
|
| 12 |
"use strict";
|
| 13 |
|
| 14 |
+
// ββ API field helpers (snake_case from Python; tolerate camelCase if ever used) β
|
| 15 |
+
function pickStat(obj, ...keys) {
|
| 16 |
+
if (!obj) return undefined;
|
| 17 |
+
for (const k of keys) {
|
| 18 |
+
if (Object.prototype.hasOwnProperty.call(obj, k) && obj[k] != null) {
|
| 19 |
+
return obj[k];
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
return undefined;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
/**
|
| 26 |
+
* Build display-ready episode metrics from the latest observation.
|
| 27 |
+
* Falls back to grid-visible cells for land % only when server omits area_saved_pct.
|
| 28 |
+
*/
|
| 29 |
+
function normalizeEpisodeStats(obs) {
|
| 30 |
+
const st = obs?.stats ?? {};
|
| 31 |
+
const cellsBurned = pickStat(st, "cells_burned", "cellsBurned") ?? 0;
|
| 32 |
+
const popLost = pickStat(st, "population_lost", "populationLost") ?? 0;
|
| 33 |
+
const totalPop = pickStat(st, "total_population", "totalPopulation") ?? 0;
|
| 34 |
+
|
| 35 |
+
let areaSaved = pickStat(st, "area_saved_pct", "areaSavedPct");
|
| 36 |
+
let civSafe = pickStat(st, "civilians_saved_pct", "civiliansSavedPct");
|
| 37 |
+
|
| 38 |
+
if (areaSaved == null && obs?.grid?.length) {
|
| 39 |
+
let burnable = 0;
|
| 40 |
+
let burnedVis = 0;
|
| 41 |
+
for (const row of obs.grid) {
|
| 42 |
+
for (const cell of row) {
|
| 43 |
+
const f = cell.fuel_type;
|
| 44 |
+
if (!f || f === "water" || f === "road") continue;
|
| 45 |
+
if (cell.fire_state === "unknown") continue;
|
| 46 |
+
burnable++;
|
| 47 |
+
if (cell.fire_state === "burned_out") burnedVis++;
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
if (burnable > 0) {
|
| 51 |
+
areaSaved = Math.round(1000 * (burnable - burnedVis) / burnable) / 10;
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
if (civSafe == null && totalPop > 0) {
|
| 56 |
+
civSafe = Math.round(1000 * (totalPop - popLost) / totalPop) / 10;
|
| 57 |
+
} else if (civSafe == null && popLost === 0) {
|
| 58 |
+
civSafe = 100.0;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
const containment = pickStat(st, "containment_pct", "containmentPct");
|
| 62 |
+
if (areaSaved == null && containment != null) {
|
| 63 |
+
areaSaved = containment;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
return {
|
| 67 |
+
areaSaved,
|
| 68 |
+
civSafe,
|
| 69 |
+
cellsBurned,
|
| 70 |
+
popLost,
|
| 71 |
+
totalPop,
|
| 72 |
+
currentStep: pickStat(st, "current_step", "currentStep"),
|
| 73 |
+
raw: st,
|
| 74 |
+
};
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
// ββ Simulation state ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 78 |
const sim = {
|
| 79 |
obs: null, // current Observation (agent's view)
|
|
|
|
| 224 |
}
|
| 225 |
|
| 226 |
// ββ Stats panel βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 227 |
+
function updateStats(obs, cumulativeReward, lastStepReward) {
|
| 228 |
+
if (!obs?.stats) return;
|
| 229 |
+
const stats = obs.stats;
|
| 230 |
+
|
| 231 |
+
const cur = pickStat(stats, "current_step", "currentStep") ?? 0;
|
| 232 |
+
const max = pickStat(stats, "max_steps", "maxSteps") ?? 1;
|
| 233 |
+
|
| 234 |
+
setText("stat-step", `${cur} / ${max}`);
|
| 235 |
+
|
| 236 |
+
const n = normalizeEpisodeStats(obs);
|
| 237 |
+
setText(
|
| 238 |
+
"stat-land-saved-val",
|
| 239 |
+
n.areaSaved != null ? `${Number(n.areaSaved).toFixed(1)}%` : "β"
|
| 240 |
+
);
|
| 241 |
+
setText(
|
| 242 |
+
"stat-civilians-safe-val",
|
| 243 |
+
n.civSafe != null ? `${Number(n.civSafe).toFixed(1)}%` : "β"
|
| 244 |
+
);
|
| 245 |
+
setText("stat-cells-burned-val", n.cellsBurned);
|
| 246 |
+
setText("stat-burning-val", pickStat(stats, "cells_burning", "cellsBurning") ?? 0);
|
| 247 |
+
setText("stat-pop-threat-val", pickStat(stats, "population_threatened", "populationThreatened") ?? 0);
|
| 248 |
+
setText("stat-pop-lost-val", n.popLost);
|
| 249 |
|
| 250 |
// Cumulative reward
|
| 251 |
setText("reward-total", cumulativeReward.toFixed(3));
|
|
|
|
| 372 |
}
|
| 373 |
|
| 374 |
// ββ Terminal overlay ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 375 |
+
async function showTerminal() {
|
| 376 |
const overlay = document.getElementById("terminal-overlay");
|
| 377 |
if (!overlay) return;
|
| 378 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
const card = document.getElementById("terminal-card");
|
| 380 |
+
if (!card) return;
|
| 381 |
+
|
| 382 |
+
const n = normalizeEpisodeStats(sim.obs);
|
| 383 |
const title = card.querySelector("h2");
|
| 384 |
|
| 385 |
+
if (n.popLost === 0) {
|
| 386 |
+
title.textContent = "β
EPISODE COMPLETE";
|
| 387 |
title.className = "win";
|
| 388 |
} else {
|
| 389 |
title.textContent = "β EPISODE ENDED";
|
| 390 |
title.className = "loss";
|
| 391 |
}
|
| 392 |
|
| 393 |
+
const landStr = n.areaSaved != null ? `${Number(n.areaSaved).toFixed(1)}%` : "β";
|
| 394 |
+
const civStr = n.civSafe != null ? `${Number(n.civSafe).toFixed(1)}%` : "β";
|
| 395 |
+
setText("terminal-land-saved", landStr);
|
| 396 |
+
setText("terminal-civilians-safe", civStr);
|
| 397 |
+
setText("terminal-cells-burned", String(n.cellsBurned));
|
| 398 |
+
setText("terminal-pop-lost", n.popLost);
|
| 399 |
+
setText("terminal-reward", sim.cumulativeReward.toFixed(3));
|
| 400 |
+
setText("terminal-step", n.currentStep ?? "β");
|
| 401 |
|
| 402 |
overlay.classList.add("show");
|
| 403 |
+
|
| 404 |
+
// Authoritative end-game numbers (ground truth β fixes blank UI if observation JSON differed)
|
| 405 |
+
try {
|
| 406 |
+
const st = await apiGet("/state");
|
| 407 |
+
if (st.error) return;
|
| 408 |
+
const tb = st.total_burnable ?? 0;
|
| 409 |
+
const burned = st.cells_burned ?? 0;
|
| 410 |
+
const landPct = tb > 0 ? Math.round(1000 * (tb - burned) / tb) / 10 : 100;
|
| 411 |
+
const tp = st.total_population ?? 0;
|
| 412 |
+
const lost = st.population_lost ?? 0;
|
| 413 |
+
const civPct = tp > 0 ? Math.round(1000 * (tp - lost) / tp) / 10 : 100;
|
| 414 |
+
setText("terminal-land-saved", `${landPct}%`);
|
| 415 |
+
setText("terminal-civilians-safe", `${civPct}%`);
|
| 416 |
+
setText("terminal-cells-burned", String(burned));
|
| 417 |
+
setText("terminal-pop-lost", String(lost));
|
| 418 |
+
setText("terminal-step", st.current_step ?? "β");
|
| 419 |
+
} catch (e) {
|
| 420 |
+
console.warn("Could not refresh end-game stats from /state", e);
|
| 421 |
+
}
|
| 422 |
}
|
| 423 |
|
| 424 |
function hideTerminal() {
|
|
|
|
| 452 |
function applyObservation(obs) {
|
| 453 |
sim.obs = obs;
|
| 454 |
renderCanvas(obs, sim.groundTruthData);
|
| 455 |
+
updateStats(obs, sim.cumulativeReward, sim.lastStepReward);
|
| 456 |
updateResources(obs.resources);
|
| 457 |
updateWeather(obs.weather);
|
| 458 |
updateEvents(obs.recent_events ?? []);
|
|
|
|
| 513 |
|
| 514 |
if (snap.done) {
|
| 515 |
stopPlay();
|
| 516 |
+
await showTerminal();
|
| 517 |
break;
|
| 518 |
}
|
| 519 |
}
|
frontend/index.html
CHANGED
|
@@ -83,8 +83,16 @@
|
|
| 83 |
<div id="terminal-card">
|
| 84 |
<h2 class="win">β
FIRE CONTAINED</h2>
|
| 85 |
<div class="stat-row">
|
| 86 |
-
<span>
|
| 87 |
-
<span id="terminal-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
</div>
|
| 89 |
<div class="stat-row">
|
| 90 |
<span>Population lost</span>
|
|
@@ -104,6 +112,10 @@
|
|
| 104 |
</div>
|
| 105 |
</div>
|
| 106 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
</main>
|
| 108 |
|
| 109 |
<!-- Sidebar -->
|
|
@@ -117,9 +129,17 @@
|
|
| 117 |
<span class="stat-label">STEP</span>
|
| 118 |
<span class="stat-value" id="stat-step">β / β</span>
|
| 119 |
</div>
|
| 120 |
-
<div class="stat-item" id="stat-
|
| 121 |
-
<span class="stat-label">
|
| 122 |
-
<span class="stat-value" id="stat-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
</div>
|
| 124 |
<div class="stat-item" id="stat-burning">
|
| 125 |
<span class="stat-label">BURNING</span>
|
|
@@ -274,6 +294,6 @@
|
|
| 274 |
</span>
|
| 275 |
</footer>
|
| 276 |
|
| 277 |
-
<script src="app.js"></script>
|
| 278 |
</body>
|
| 279 |
</html>
|
|
|
|
| 83 |
<div id="terminal-card">
|
| 84 |
<h2 class="win">β
FIRE CONTAINED</h2>
|
| 85 |
<div class="stat-row">
|
| 86 |
+
<span>Land saved (unburned)</span>
|
| 87 |
+
<span id="terminal-land-saved">β</span>
|
| 88 |
+
</div>
|
| 89 |
+
<div class="stat-row">
|
| 90 |
+
<span>Civilians safe</span>
|
| 91 |
+
<span id="terminal-civilians-safe">β</span>
|
| 92 |
+
</div>
|
| 93 |
+
<div class="stat-row">
|
| 94 |
+
<span>Cells burned (total)</span>
|
| 95 |
+
<span id="terminal-cells-burned">β</span>
|
| 96 |
</div>
|
| 97 |
<div class="stat-row">
|
| 98 |
<span>Population lost</span>
|
|
|
|
| 112 |
</div>
|
| 113 |
</div>
|
| 114 |
</div>
|
| 115 |
+
<p id="map-legend" class="map-legend">
|
| 116 |
+
<strong>Map:</strong> green dot / circle = ground crew Β· blue outline = populated zone Β·
|
| 117 |
+
bright blue cells = water Β· grey = roads
|
| 118 |
+
</p>
|
| 119 |
</main>
|
| 120 |
|
| 121 |
<!-- Sidebar -->
|
|
|
|
| 129 |
<span class="stat-label">STEP</span>
|
| 130 |
<span class="stat-value" id="stat-step">β / β</span>
|
| 131 |
</div>
|
| 132 |
+
<div class="stat-item" id="stat-land-saved">
|
| 133 |
+
<span class="stat-label">LAND SAVED</span>
|
| 134 |
+
<span class="stat-value" id="stat-land-saved-val">β</span>
|
| 135 |
+
</div>
|
| 136 |
+
<div class="stat-item" id="stat-civilians-safe">
|
| 137 |
+
<span class="stat-label">CIVILIANS SAFE</span>
|
| 138 |
+
<span class="stat-value" id="stat-civilians-safe-val">β</span>
|
| 139 |
+
</div>
|
| 140 |
+
<div class="stat-item" id="stat-cells-burned">
|
| 141 |
+
<span class="stat-label">CELLS BURNED</span>
|
| 142 |
+
<span class="stat-value" id="stat-cells-burned-val">β</span>
|
| 143 |
</div>
|
| 144 |
<div class="stat-item" id="stat-burning">
|
| 145 |
<span class="stat-label">BURNING</span>
|
|
|
|
| 294 |
</span>
|
| 295 |
</footer>
|
| 296 |
|
| 297 |
+
<script src="app.js?v=4"></script>
|
| 298 |
</body>
|
| 299 |
</html>
|
frontend/style.css
CHANGED
|
@@ -250,6 +250,16 @@ input[type="range"]::-webkit-slider-thumb {
|
|
| 250 |
|
| 251 |
#grid-canvas { display: block; image-rendering: pixelated; }
|
| 252 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
/* Tooltip overlay (shows cell info on hover) */
|
| 254 |
#cell-tooltip {
|
| 255 |
position: absolute;
|
|
@@ -356,8 +366,10 @@ input[type="range"]::-webkit-slider-thumb {
|
|
| 356 |
.stat-item.step-item { grid-column: 1 / -1; }
|
| 357 |
.stat-item.step-item .stat-value { font-size: 14px; }
|
| 358 |
|
| 359 |
-
#stat-
|
| 360 |
-
#stat-
|
|
|
|
|
|
|
| 361 |
#stat-pop-threat .stat-value { color: var(--warn); }
|
| 362 |
#stat-pop-lost .stat-value { color: var(--crit); }
|
| 363 |
|
|
|
|
| 250 |
|
| 251 |
#grid-canvas { display: block; image-rendering: pixelated; }
|
| 252 |
|
| 253 |
+
.map-legend {
|
| 254 |
+
margin: 8px 0 0;
|
| 255 |
+
padding: 6px 10px;
|
| 256 |
+
font-size: 11px;
|
| 257 |
+
color: var(--text-muted);
|
| 258 |
+
line-height: 1.45;
|
| 259 |
+
max-width: 100%;
|
| 260 |
+
}
|
| 261 |
+
.map-legend strong { color: var(--text); }
|
| 262 |
+
|
| 263 |
/* Tooltip overlay (shows cell info on hover) */
|
| 264 |
#cell-tooltip {
|
| 265 |
position: absolute;
|
|
|
|
| 366 |
.stat-item.step-item { grid-column: 1 / -1; }
|
| 367 |
.stat-item.step-item .stat-value { font-size: 14px; }
|
| 368 |
|
| 369 |
+
#stat-land-saved .stat-value { color: var(--safe); }
|
| 370 |
+
#stat-civilians-safe .stat-value { color: var(--safe); }
|
| 371 |
+
#stat-cells-burned .stat-value { color: var(--warn); }
|
| 372 |
+
#stat-burning .stat-value { color: var(--fire); }
|
| 373 |
#stat-pop-threat .stat-value { color: var(--warn); }
|
| 374 |
#stat-pop-lost .stat-value { color: var(--crit); }
|
| 375 |
|