Eshit commited on
Commit
65c3b5c
Β·
1 Parent(s): 08dd1b7

Improve wildfire metrics and training assets

Browse files
Files changed (3) hide show
  1. frontend/app.js +120 -24
  2. frontend/index.html +26 -6
  3. 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(stats, cumulativeReward, lastStepReward) {
165
- if (!stats) return;
166
-
167
- const cur = stats.current_step ?? 0;
168
- const max = stats.max_steps ?? 1;
169
-
170
- setText("stat-step", `${cur} / ${max}`);
171
- setText("stat-containment-val", `${(stats.containment_pct ?? 0).toFixed(1)}%`);
172
- setText("stat-burning-val", stats.cells_burning ?? 0);
173
- setText("stat-pop-threat-val", stats.population_threatened ?? 0);
174
- setText("stat-pop-lost-val", stats.population_lost ?? 0);
 
 
 
 
 
 
 
 
 
 
 
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(obs) {
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 = "βœ… FIRE CONTAINED";
314
  title.className = "win";
315
  } else {
316
  title.textContent = "⚠ EPISODE ENDED";
317
  title.className = "loss";
318
  }
319
 
320
- setText("terminal-containment", `${containment.toFixed(1)}%`);
321
- setText("terminal-pop-lost", popLost);
322
- setText("terminal-reward", sim.cumulativeReward.toFixed(3));
323
- setText("terminal-step", stats.current_step ?? "β€”");
 
 
 
 
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.stats, sim.cumulativeReward, sim.lastStepReward);
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(snap.observation);
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>Containment</span>
87
- <span id="terminal-containment">β€”</span>
 
 
 
 
 
 
 
 
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-containment">
121
- <span class="stat-label">CONTAINMENT</span>
122
- <span class="stat-value" id="stat-containment-val">β€”</span>
 
 
 
 
 
 
 
 
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-containment .stat-value { color: var(--safe); }
360
- #stat-burning .stat-value { color: var(--fire); }
 
 
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