From 9f82c720c991852a8deace3db05c6cf37054d758 Mon Sep 17 00:00:00 2001 From: Ram Harikrishnan Date: Mon, 27 Oct 2025 18:51:46 -0700 Subject: [PATCH 01/19] Add Wildfire Environment for OpenEnv (FastAPI, RL-compatible) --- .../.ipynb_checkpoints/__init__-checkpoint.py | 9 + .../.ipynb_checkpoints/client-checkpoint.py | 27 ++ .../.ipynb_checkpoints/models-checkpoint.py | 43 ++ src/envs/wildfire_env/README.md | 328 ++++++++++++++++ src/envs/wildfire_env/__init__.py | 9 + src/envs/wildfire_env/client.py | 27 ++ src/envs/wildfire_env/models.py | 45 +++ .../.ipynb_checkpoints/app-checkpoint.py | 10 + .../wildfire_environment-checkpoint.py | 321 +++++++++++++++ src/envs/wildfire_env/server/Dockerfile | 22 ++ src/envs/wildfire_env/server/__init__.py | 15 + src/envs/wildfire_env/server/app.py | 10 + src/envs/wildfire_env/server/build_docker.sh | 14 + .../server/wildfire_environment.py | 369 ++++++++++++++++++ 14 files changed, 1249 insertions(+) create mode 100644 src/envs/wildfire_env/.ipynb_checkpoints/__init__-checkpoint.py create mode 100644 src/envs/wildfire_env/.ipynb_checkpoints/client-checkpoint.py create mode 100644 src/envs/wildfire_env/.ipynb_checkpoints/models-checkpoint.py create mode 100644 src/envs/wildfire_env/README.md create mode 100644 src/envs/wildfire_env/__init__.py create mode 100644 src/envs/wildfire_env/client.py create mode 100644 src/envs/wildfire_env/models.py create mode 100644 src/envs/wildfire_env/server/.ipynb_checkpoints/app-checkpoint.py create mode 100644 src/envs/wildfire_env/server/.ipynb_checkpoints/wildfire_environment-checkpoint.py create mode 100644 src/envs/wildfire_env/server/Dockerfile create mode 100644 src/envs/wildfire_env/server/__init__.py create mode 100644 src/envs/wildfire_env/server/app.py create mode 100644 src/envs/wildfire_env/server/build_docker.sh create mode 100644 src/envs/wildfire_env/server/wildfire_environment.py diff --git a/src/envs/wildfire_env/.ipynb_checkpoints/__init__-checkpoint.py b/src/envs/wildfire_env/.ipynb_checkpoints/__init__-checkpoint.py new file mode 100644 index 00000000..5df8fe34 --- /dev/null +++ b/src/envs/wildfire_env/.ipynb_checkpoints/__init__-checkpoint.py @@ -0,0 +1,9 @@ +from .models import WildfireAction, WildfireObservation, WildfireState +from .client import WildfireEnv + +__all__ = [ + "WildfireAction", + "WildfireObservation", + "WildfireState", + "WildfireEnv", +] diff --git a/src/envs/wildfire_env/.ipynb_checkpoints/client-checkpoint.py b/src/envs/wildfire_env/.ipynb_checkpoints/client-checkpoint.py new file mode 100644 index 00000000..104119e7 --- /dev/null +++ b/src/envs/wildfire_env/.ipynb_checkpoints/client-checkpoint.py @@ -0,0 +1,27 @@ +from core.http_env_client import HTTPEnvClient +from core.client_types import StepResult +from .models import WildfireAction, WildfireObservation, WildfireState + +class WildfireEnv(HTTPEnvClient[WildfireAction, WildfireObservation]): + def _step_payload(self, action: WildfireAction) -> dict: + return {"action": action.action, "x": action.x, "y": action.y} + + def _parse_result(self, payload: dict) -> StepResult[WildfireObservation]: + obs = WildfireObservation(**payload["observation"]) + return StepResult( + observation=obs, + reward=payload.get("reward"), + done=payload.get("done", False), + ) + + def _parse_state(self, payload: dict) -> WildfireState: + return WildfireState(**payload) +def render_grid(obs: WildfireObservation) -> str: + legend = {0:"⬛", 1:"🟩", 2:"🟥", 3:"🟫", 4:"🟦"} + w, h = obs.width, obs.height + g = obs.grid + rows = [] + for y in range(h): + rows.append("".join(legend.get(g[y*w+x], "?") for x in range(w))) + meta = f"step={obs.step} wind={obs.wind_dir} hum={obs.humidity:.2f} burning={obs.burning_count} burned= {obs.burned_count}" + return "\n".join(rows + [meta]) diff --git a/src/envs/wildfire_env/.ipynb_checkpoints/models-checkpoint.py b/src/envs/wildfire_env/.ipynb_checkpoints/models-checkpoint.py new file mode 100644 index 00000000..3eb40aa4 --- /dev/null +++ b/src/envs/wildfire_env/.ipynb_checkpoints/models-checkpoint.py @@ -0,0 +1,43 @@ +from dataclasses import dataclass, field +from typing import List, Optional +from core.env_server import Action, Observation, State + +# Grid cell encoding: +# 0 = empty/ash, 1 = fuel (healthy), 2 = burning, 3 = firebreak, 4 = watered (damp) +# (You can tweak encodings, but keep them ints for compact obs.) + +@dataclass +class WildfireAction(Action): + # action: "break" (build firebreak), "water" (drop water), "wait" + action: str + x: Optional[int] = None + y: Optional[int] = None + +@dataclass +class WildfireObservation(Observation): + grid: List[int] # flattened grid H*W, ints in {0..4} + width: int + height: int + step: int + wind_dir: str # e.g. "N","NE","E","SE","S","SW","W","NW","CALM" + humidity: float # [0,1] + burning_count: int + burned_count: int # total ash (0) cells (cumulative) + reward_hint: float = 0.0 # optional shaping info + +@dataclass +class WildfireState(State): + episode_id: str = "" + step_count: int = 0 + total_burned: int = 0 + total_extinguished: int = 0 + last_action: str = "reset" + # For visibility / debugging (not required by core): + width: int = 0 + height: int = 0 + wind_dir: str = "CALM" + humidity: float = 0.25 + remaining_water: int = 20 # simple resource constraint + remaining_breaks: int = 50 + # internal full grid as flattened ints + grid: List[int] = field(default_factory=list) diff --git a/src/envs/wildfire_env/README.md b/src/envs/wildfire_env/README.md new file mode 100644 index 00000000..bf7a05a3 --- /dev/null +++ b/src/envs/wildfire_env/README.md @@ -0,0 +1,328 @@ +# 🌲 Wildfire Environment + +Autonomous wildfire-control simulation for reinforcement-learning agents, built on the [OpenEnv](https://github.com/openenv) framework. +Agents must contain spreading fires using **water**, **firebreaks**, and **timing strategies** under changing **wind** and **humidity** conditions. + +[![Docker](https://img.shields.io/badge/docker-ready-blue)](https://hub.docker.com/) +[![Python](https://img.shields.io/badge/python-3.10+-green)](https://www.python.org/) +[![FastAPI](https://img.shields.io/badge/backend-fastapi-teal)](https://fastapi.tiangolo.com/) +[![License](https://img.shields.io/badge/license-MIT-lightgrey)](LICENSE) + +--- + +## 🔥 Environment Overview + +This environment models **forest-fire dynamics** influenced by: +- **Wind direction** (8 directions + calm) +- **Humidity** (suppresses ignition) +- **Fuel type and spread rate** +- **Limited resources** (water units, break materials) +- **Time pressure** (each step costs reward) + +The goal is to **minimize fire spread** and **total burned area** while using resources efficiently. + +--- + +## 🧱 Grid Encoding + +| Code | Meaning | Color (Visualization) | +|------|----------------|-----------------------| +| 0 | Ash (burned) | Black ⚫ | +| 1 | Fuel | Green 🟩 | +| 2 | Burning | Red 🔥 | +| 3 | Firebreak | Brown 🟫 | +| 4 | Water/Damp | Blue 🔵 | + +--- + +## ⚙️ Architecture + +``` +┌────────────────────────────────────────────┐ +│ RL Agent / LLM Trainer (Client) │ +│ wildfire_env.step(WildfireAction(...)) │ +└──────────────────┬─────────────────────────┘ + │ HTTP +┌──────────────────▼─────────────────────────┐ +│ FastAPI Server (Docker) │ +│ WildfireEnvironment │ +│ ├─ Handles wind, humidity, spread │ +│ ├─ Applies agent actions │ +│ ├─ Updates grid + reward shaping │ +│ └─ Returns WildfireObservation │ +└────────────────────────────────────────────┘ +``` + +--- + +## 🚀 Installation & Usage + +### Option 1: Local Development (no Docker) + +**Requirements:** +- Python 3.10 + +- FastAPI + Uvicorn +- NumPy + Matplotlib (for visualization) + +```bash +pip install fastapi uvicorn numpy matplotlib requests +``` + +Run server locally: +```bash +python -m envs.wildfire_env.server.app +``` + +Client usage: +```python +from envs.wildfire_env import WildfireEnv, WildfireAction + +env = WildfireEnv(base_url="http://localhost:8000") + +result = env.reset() +print(f"🔥 Fires: {result.observation.burning_count}, 💧 Water left: {result.observation.remaining_water}") + +for _ in range(5): + result = env.step(WildfireAction(action="water", x=10, y=10)) + print(f"Reward: {result.reward}, Burning left: {result.observation.burning_count}") + +env.close() +``` + +--- + +### Option 2: Docker (Recommended) + +Build the image: +```bash +cd OpenEnv +docker build -f src/envs/wildfire_env/server/Dockerfile -t wildfire-env:latest . +``` + +Run the container: +```bash +docker run -p 8000:8000 wildfire-env:latest +``` + +Connect via client: +```python +from envs.wildfire_env import WildfireEnv, WildfireAction +env = WildfireEnv.from_docker_image("wildfire-env:latest") +result = env.reset() +print(f"Active fires: {result.observation.burning_count}") +result = env.step(WildfireAction(action="break", x=8, y=12)) +print(f"Reward: {result.reward}") +env.close() +``` + +--- + +## 🌦️ Configuration + +| Variable | Description | Default | +|-----------|--------------|----------| +| `WILDFIRE_WIDTH` | Grid width | 32 | +| `WILDFIRE_HEIGHT` | Grid height | 32 | +| `WILDFIRE_HUMIDITY` | Initial humidity [0–1] | 0.25 | +| `WILDFIRE_WIND` | Wind direction (`N`, `NE`, `E`, `SE`, `S`, `SW`, `W`, `NW`, `CALM`) | Random | +| `WILDFIRE_SEED` | RNG seed | 3407 | +| `WILDFIRE_MAX_STEPS` | Max steps per episode | 128 | +| `WILDFIRE_WATER_CAPACITY` | Water units available | 8 | +| `WILDFIRE_BREAK_CAPACITY` | Firebreak materials | 50 | + +--- + +## 🧠 API Reference + +### `WildfireAction` +```python +@dataclass +class WildfireAction(Action): + action: str # "water" | "break" | "wait" + x: Optional[int] = None # Target X + y: Optional[int] = None # Target Y +``` + +### `WildfireObservation` +```python +@dataclass +class WildfireObservation(Observation): + grid: List[int] + width: int + height: int + step: int + wind_dir: str + humidity: float + burning_count: int + burned_count: int + remaining_water: int + remaining_breaks: int + reward_hint: float +``` + +### `WildfireState` +```python +@dataclass +class WildfireState(State): + episode_id: str + step_count: int + total_burned: int + total_extinguished: int + remaining_water: int + remaining_breaks: int + wind_dir: str + humidity: float +``` + +--- +## Sample rendering to see wildfree simulation + +import matplotlib.pyplot as plt +import numpy as np +import time, sys + +from IPython.display import clear_output, display +import matplotlib.colors as mcolors +sys.path.append("/workspace/OpenEnv/src") +from envs.wildfire_env import WildfireEnv, WildfireAction # Ensure these imports work + +from envs.wildfire_env.server.wildfire_environment import WildfireEnvironment + + +client = WildfireEnv("http://localhost:8020") + + +cmap = mcolors.ListedColormap([ + "black", # 0 = ash + "green", # 1 = fuel + "red", # 2 = burning + "saddlebrown", # 3 = firebreak + "blue" # 4 = water +]) + +norm = mcolors.BoundaryNorm([0, 1, 2, 3, 4, 5], cmap.N) + + +plt.ion() +fig, ax = plt.subplots(figsize=(5, 5)) +plt.axis("off") + + +res = client.reset() +obs = res.observation +grid = np.array(obs.grid).reshape(obs.height, obs.width) + + +im = ax.imshow(grid, cmap=cmap, norm=norm) + + +title_text = ax.set_title( + f"Step {obs.step} | Burning={obs.burning_count} | Burned={obs.burned_count}\n" + f"Wind={obs.wind_dir} | Humidity={obs.humidity:.2f}", + color="black", + fontsize=10 +) + + + +print("Starting smooth animation...") +for _ in range(100): + clear_output(wait=True) + + new_grid = np.array(obs.grid).reshape(obs.height, obs.width) + + im.set_data(new_grid) + + title_text.set_text( + f"Step {obs.step} | Burning={obs.burning_count} | Burned={obs.burned_count}\n" + f"Wind={obs.wind_dir} | Humidity={obs.humidity:.2f}" + ) + + + display(fig) + + + time.sleep(0.3) + + + res = client.step(WildfireAction(action="WAIT")) + obs = res.observation + + if obs.burning_count == 0: + print(f"🔥 Fire has fully burned out after {obs.step} steps.") + break + +plt.ioff() # Turn off interactive mode +plt.close(fig) # Close the figure at the end +print("Animation complete.") + + + +=== + + +## 🧪 Example Training Loop (GRPO/LLM) + +```python +from envs.wildfire_env import WildfireEnv, WildfireAction +import random + +env = WildfireEnv.from_docker_image("wildfire-env:latest") + +for episode in range(3): + result = env.reset() + total_reward = 0 + + while not result.done: + a = random.choice(["water", "break", "wait"]) + x, y = random.randint(0, 15), random.randint(0, 15) + result = env.step(WildfireAction(action=a, x=x, y=y)) + total_reward += result.reward or 0 + + print(f"Episode {episode}: total_reward={total_reward:.2f}") + +env.close() +``` + +--- + +## 🧰 DockerHub & GitHub Build + +Build and push: + +```bash +docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile . +docker build -t ghcr.io//openenv-wildfire:latest -f src/envs/wildfire_env/server/Dockerfile . +docker push ghcr.io//openenv-wildfire:latest +``` + +GitHub Action matrix entry: +```yaml +strategy: + matrix: + image: + - name: wildfire-env + dockerfile: src/envs/wildfire_env/server/Dockerfile +``` + +--- + +## 🧭 References + +- [OpenEnv Framework](https://github.com/openenv) +- [FastAPI Documentation](https://fastapi.tiangolo.com/) +- [Reinforcement Learning Introduction](https://spinningup.openai.com/en/latest/) +- [Fire Spread Simulation Models (USFS Research)](https://www.fs.fed.us/rm/pubs/rmrs_gtr371.html) + +--- + +## 🪵 Citation + +```bibtex +@misc{wildfire-openenv-2025, + title = {Wildfire Environment for OpenEnv: Containment-Focused RL Simulation}, + author = {Harikrishnan, Ram Sankar}, + year = {2025}, + url = {https://github.com//openenv-wildfire} +} +``` diff --git a/src/envs/wildfire_env/__init__.py b/src/envs/wildfire_env/__init__.py new file mode 100644 index 00000000..5df8fe34 --- /dev/null +++ b/src/envs/wildfire_env/__init__.py @@ -0,0 +1,9 @@ +from .models import WildfireAction, WildfireObservation, WildfireState +from .client import WildfireEnv + +__all__ = [ + "WildfireAction", + "WildfireObservation", + "WildfireState", + "WildfireEnv", +] diff --git a/src/envs/wildfire_env/client.py b/src/envs/wildfire_env/client.py new file mode 100644 index 00000000..104119e7 --- /dev/null +++ b/src/envs/wildfire_env/client.py @@ -0,0 +1,27 @@ +from core.http_env_client import HTTPEnvClient +from core.client_types import StepResult +from .models import WildfireAction, WildfireObservation, WildfireState + +class WildfireEnv(HTTPEnvClient[WildfireAction, WildfireObservation]): + def _step_payload(self, action: WildfireAction) -> dict: + return {"action": action.action, "x": action.x, "y": action.y} + + def _parse_result(self, payload: dict) -> StepResult[WildfireObservation]: + obs = WildfireObservation(**payload["observation"]) + return StepResult( + observation=obs, + reward=payload.get("reward"), + done=payload.get("done", False), + ) + + def _parse_state(self, payload: dict) -> WildfireState: + return WildfireState(**payload) +def render_grid(obs: WildfireObservation) -> str: + legend = {0:"⬛", 1:"🟩", 2:"🟥", 3:"🟫", 4:"🟦"} + w, h = obs.width, obs.height + g = obs.grid + rows = [] + for y in range(h): + rows.append("".join(legend.get(g[y*w+x], "?") for x in range(w))) + meta = f"step={obs.step} wind={obs.wind_dir} hum={obs.humidity:.2f} burning={obs.burning_count} burned= {obs.burned_count}" + return "\n".join(rows + [meta]) diff --git a/src/envs/wildfire_env/models.py b/src/envs/wildfire_env/models.py new file mode 100644 index 00000000..bfd2851d --- /dev/null +++ b/src/envs/wildfire_env/models.py @@ -0,0 +1,45 @@ +from dataclasses import dataclass, field +from typing import List, Optional +from core.env_server import Action, Observation, State + +# Grid cell encoding: +# 0 = empty/ash, 1 = fuel (healthy), 2 = burning, 3 = firebreak, 4 = watered (damp) +# (You can tweak encodings, but keep them ints for compact obs.) + +@dataclass +class WildfireAction(Action): + # action: "break" (build firebreak), "water" (drop water), "wait" + action: str + x: Optional[int] = None + y: Optional[int] = None + +@dataclass +class WildfireObservation(Observation): + grid: List[int] # flattened grid H*W, ints in {0..4} + width: int + height: int + step: int + wind_dir: str # e.g. "N","NE","E","SE","S","SW","W","NW","CALM" + humidity: float # [0,1] + burning_count: int + burned_count: int # total ash (0) cells (cumulative) + reward_hint: float = 0.0 + remaining_water: int = 0 + remaining_breaks: int = 0# optional shaping info + +@dataclass +class WildfireState(State): + episode_id: str = "" + step_count: int = 0 + total_burned: int = 0 + total_extinguished: int = 0 + last_action: str = "reset" + # For visibility / debugging (not required by core): + width: int = 0 + height: int = 0 + wind_dir: str = "CALM" + humidity: float = 0.25 + remaining_water: int = 20 # simple resource constraint + remaining_breaks: int = 50 + # internal full grid as flattened ints + grid: List[int] = field(default_factory=list) diff --git a/src/envs/wildfire_env/server/.ipynb_checkpoints/app-checkpoint.py b/src/envs/wildfire_env/server/.ipynb_checkpoints/app-checkpoint.py new file mode 100644 index 00000000..4f818094 --- /dev/null +++ b/src/envs/wildfire_env/server/.ipynb_checkpoints/app-checkpoint.py @@ -0,0 +1,10 @@ +# server/app.py +import os +from core.env_server import create_fastapi_app +from ..models import WildfireAction, WildfireObservation +from .wildfire_environment import WildfireEnvironment + +W = int(os.getenv("WILDFIRE_W", "16")) +H = int(os.getenv("WILDFIRE_H", "16")) +env = WildfireEnvironment(width=W, height=H) +app = create_fastapi_app(env, WildfireAction, WildfireObservation) diff --git a/src/envs/wildfire_env/server/.ipynb_checkpoints/wildfire_environment-checkpoint.py b/src/envs/wildfire_env/server/.ipynb_checkpoints/wildfire_environment-checkpoint.py new file mode 100644 index 00000000..dcd996c4 --- /dev/null +++ b/src/envs/wildfire_env/server/.ipynb_checkpoints/wildfire_environment-checkpoint.py @@ -0,0 +1,321 @@ +import os +import random, uuid +from typing import List +from dataclasses import replace + +from core.env_server import Environment +from ..models import WildfireAction, WildfireObservation, WildfireState + +# Helpers +DIRS_8 = { + "N": (0, -1), "NE": (1, -1), "E": (1, 0), "SE": (1, 1), + "S": (0, 1), "SW": (-1, 1), "W": (-1, 0), "NW": (-1, -1), + "CALM": (0, 0), +} + +def idx(x: int, y: int, w: int) -> int: + return y * w + x + +def in_bounds(x: int, y: int, w: int, h: int) -> bool: + return 0 <= x < w and 0 <= y < h + + +class WildfireEnvironment(Environment): + """ + Weather-aware wildfire simulation. + + Grid encodings: + 0 = ash (burned out) + 1 = fuel / vegetation + 2 = burning + 3 = firebreak + 4 = watered / damp + + Each step: + - agent acts (water/break/wait) + - burning spreads to neighbors with wind + humidity effects + - burning cells burn for multiple ticks, then become ash + """ + + def __init__( + self, + width: int = 32, + height: int = 32, + base_ignite_prob: float = 0.30, + wind_bias: float = 0.20, # kept for compatibility (not directly used in B model) + diag_factor: float = 0.7, # kept for compatibility (not directly used in B model) + humidity: float = 0.25, + init_sources: int = 2, + seed: int = 3407, + max_steps: int = 128, + water_capacity: int = 20, + break_capacity: int = 50, + ): + super().__init__() + + # --- Env-var overrides (optional) --- + width = int(os.environ.get("WILDFIRE_WIDTH", width)) + height = int(os.environ.get("WILDFIRE_HEIGHT", height)) + humidity = float(os.environ.get("WILDFIRE_HUMIDITY", humidity)) + forced_wind = os.environ.get("WILDFIRE_WIND", None) + + # Store config + self.w = width + self.h = height + self.base_ignite_prob = base_ignite_prob + self.wind_bias = wind_bias + self.diag_factor = diag_factor + self.init_humidity = humidity + self.init_sources = init_sources + self.rng = random.Random(seed) + self.max_steps = max_steps + self.init_water = water_capacity + self.init_breaks = break_capacity + self.forced_wind = forced_wind + + # burn lifetime in ticks (balanced model) + self.burn_lifetime = 3 + + self._state = WildfireState() + + # --- Core API --- + + def reset(self) -> WildfireObservation: + # Start with all fuel + grid = [1] * (self.w * self.h) + + # Wind (forced if provided) + if self.forced_wind and self.forced_wind in DIRS_8: + wind_dir = self.forced_wind + else: + wind_dir = self.rng.choice(list(DIRS_8.keys())) + + # Humidity small variation around init + humidity = min(1.0, max(0.0, self.init_humidity + self.rng.uniform(-0.05, 0.05))) + + # Place initial fires + for _ in range(self.init_sources): + x = self.rng.randrange(self.w) + y = self.rng.randrange(self.h) + grid[idx(x, y, self.w)] = 2 + + self._state = WildfireState( + episode_id=str(uuid.uuid4()), + step_count=0, + total_burned=0, + total_extinguished=0, + last_action="reset", + width=self.w, + height=self.h, + wind_dir=wind_dir, + humidity=humidity, + remaining_water=self.init_water, + remaining_breaks=self.init_breaks, + grid=grid, + ) + + # per-cell burn timers (persist across steps) + self._state.burn_timers = [0] * (self.w * self.h) + + obs = self._make_observation(reward_hint=0.0) + return obs + + def step(self, action: WildfireAction) -> WildfireObservation: + st = self._state + + # Apply agent action + reward = 0.0 + if action.action == "water" and st.remaining_water > 0 and action.x is not None and action.y is not None: + reward += self._apply_water(action.x, action.y) + elif action.action == "break" and st.remaining_breaks > 0 and action.x is not None and action.y is not None: + reward += self._apply_break(action.x, action.y) + elif action.action == "wait": + pass + else: + # invalid or no resources + reward -= 0.05 + + # Natural fire dynamics + newly_burned = self._spread_fire() + st.total_burned += newly_burned + + # small per-step penalty (encourage faster containment) + reward -= 0.01 + + st.step_count += 1 + st.last_action = action.action + + done = self._is_done() + + if done: + # reward for saved area + saved = self._saved_cells() + reward += 0.5 * (saved / (self.w * self.h)) + # reward if fully extinguished + if self._burning_count() == 0: + reward += 0.5 + + obs = self._make_observation(reward_hint=reward) + obs.done = done + obs.reward = reward + return obs + + @property + def state(self) -> WildfireState: + return self._state + + # --- Internal mechanics --- + + def _apply_water(self, x: int, y: int) -> float: + st = self._state + if not in_bounds(x, y, self.w, self.h): + return -0.05 + i = idx(x, y, self.w) + reward = 0.0 + + if st.grid[i] == 2: + st.grid[i] = 4 + st.burn_timers[i] = 0 + st.total_extinguished += 1 + reward += 0.2 + elif st.grid[i] == 1: + st.grid[i] = 4 # dampen + reward += 0.05 + elif st.grid[i] == 4: + reward -= 0.01 + else: + reward -= 0.02 + + st.remaining_water -= 1 + return reward + + def _apply_break(self, x: int, y: int) -> float: + st = self._state + if not in_bounds(x, y, self.w, self.h): + return -0.05 + i = idx(x, y, self.w) + reward = 0.0 + + if st.grid[i] in (1, 4): + st.grid[i] = 3 + st.burn_timers[i] = 0 + reward += 0.1 + elif st.grid[i] == 2: + st.grid[i] = 3 + st.burn_timers[i] = 0 + reward -= 0.02 + elif st.grid[i] == 3: + reward -= 0.01 + else: + reward -= 0.02 + + st.remaining_breaks -= 1 + return reward + + def _spread_fire(self) -> int: + """ + Balanced wildfire spread model: + - burning cells persist for multiple ticks before turning to ash + - 8-direction spread (diagonals weaker) + - wind accelerates in wind direction, weakens upwind + - humidity suppresses ignition probability + - water (4) reduces ignition chance and reverts to fuel next tick + """ + st = self._state + new_grid = st.grid[:] + newly_burned = 0 + + # 8-neighbor model + neighbors = [(-1, 0), (1, 0), (0, -1), (0, 1), + (-1, -1), (1, -1), (-1, 1), (1, 1)] + wx, wy = DIRS_8.get(st.wind_dir, (0, 0)) + + base = self.base_ignite_prob + humidity_factor = (1.0 - st.humidity) + + ignite_flags = [False] * (self.w * self.h) + + # First pass: evaluate ignitions, increment burn timers + for y in range(self.h): + for x in range(self.w): + i = idx(x, y, self.w) + cell = st.grid[i] + + if cell == 2: # burning + st.burn_timers[i] += 1 + + for dx, dy in neighbors: + nx, ny = x + dx, y + dy + if not in_bounds(nx, ny, self.w, self.h): + continue + ni = idx(nx, ny, self.w) + target = st.grid[ni] + + if target not in (1, 4): # only fuel or damp can ignite + continue + + # Wind multiplier + if (dx, dy) == (wx, wy): + wind_mult = 2.0 + elif (dx, dy) == (-wx, -wy): + wind_mult = 0.5 + else: + wind_mult = 1.0 + + # Diagonals weaker + diag_mult = 0.6 if (dx != 0 and dy != 0) else 1.0 + + p = base * humidity_factor * wind_mult * diag_mult + + # Damp fuel further reduces spread + if target == 4: + p *= 0.35 + + p = max(0.0, min(1.0, p)) + if self.rng.random() < p: + ignite_flags[ni] = True + + # Second pass: apply transitions + for i, cell in enumerate(st.grid): + if cell == 2: + # burns for burn_lifetime ticks before turning to ash + if st.burn_timers[i] >= self.burn_lifetime: + new_grid[i] = 0 # ash + newly_burned += 1 + else: + new_grid[i] = 2 # keep burning + elif ignite_flags[i] and new_grid[i] in (1, 4): + new_grid[i] = 2 + st.burn_timers[i] = 0 + elif cell == 4: + # water effect lasts one tick + new_grid[i] = 1 + + st.grid = new_grid + return newly_burned + + def _burning_count(self) -> int: + return sum(1 for v in self._state.grid if v == 2) + + def _saved_cells(self) -> int: + # cells not turned to ash (includes fuel, burning, break, water) + return sum(1 for v in self._state.grid if v in (1, 2, 3, 4)) + + def _is_done(self) -> bool: + return self._burning_count() == 0 or self._state.step_count >= self.max_steps + + def _make_observation(self, reward_hint: float = 0.0) -> WildfireObservation: + st = self._state + burning = self._burning_count() + burned = sum(1 for v in st.grid if v == 0) + return WildfireObservation( + grid=st.grid[:], + width=self.w, + height=self.h, + step=st.step_count, + wind_dir=st.wind_dir, + humidity=st.humidity, + burning_count=burning, + burned_count=burned, + reward_hint=reward_hint, + ) diff --git a/src/envs/wildfire_env/server/Dockerfile b/src/envs/wildfire_env/server/Dockerfile new file mode 100644 index 00000000..060d6e91 --- /dev/null +++ b/src/envs/wildfire_env/server/Dockerfile @@ -0,0 +1,22 @@ +# Build ARG for CI/CD consistency +ARG BASE_IMAGE=openenv-base:latest +FROM ${BASE_IMAGE} + +# Work in app directory (already used by base image) +WORKDIR /app + +# Copy OpenEnv core +COPY src/core/ /app/src/core/ + +# Copy Wildfire environment +COPY src/envs/wildfire_env/ /app/src/envs/wildfire_env/ + +# Environment variables (override at runtime if needed) +ENV PYTHONPATH=/app/src + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +# Run FastAPI server +CMD ["uvicorn", "envs.wildfire_env.server.app:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/src/envs/wildfire_env/server/__init__.py b/src/envs/wildfire_env/server/__init__.py new file mode 100644 index 00000000..4434f887 --- /dev/null +++ b/src/envs/wildfire_env/server/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) Arizona State University and contributors. +# All rights reserved. +# +# This source code is licensed under the BSD-style license +# found in the LICENSE file in the root directory of this source tree. + +""" +Wildfire Environment Server. + +Server-side implementation of the wildfire environment for OpenEnv. +""" + +from .wildfire_environment import WildfireEnvironment + +__all__ = ["WildfireEnvironment"] diff --git a/src/envs/wildfire_env/server/app.py b/src/envs/wildfire_env/server/app.py new file mode 100644 index 00000000..4f818094 --- /dev/null +++ b/src/envs/wildfire_env/server/app.py @@ -0,0 +1,10 @@ +# server/app.py +import os +from core.env_server import create_fastapi_app +from ..models import WildfireAction, WildfireObservation +from .wildfire_environment import WildfireEnvironment + +W = int(os.getenv("WILDFIRE_W", "16")) +H = int(os.getenv("WILDFIRE_H", "16")) +env = WildfireEnvironment(width=W, height=H) +app = create_fastapi_app(env, WildfireAction, WildfireObservation) diff --git a/src/envs/wildfire_env/server/build_docker.sh b/src/envs/wildfire_env/server/build_docker.sh new file mode 100644 index 00000000..939a811f --- /dev/null +++ b/src/envs/wildfire_env/server/build_docker.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -e + +TAG="${1:-latest}" +IMAGE_NAME="wildfire-env:${TAG}" + +echo "🔥 Building Wildfire Environment Docker Image" +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +OPENENV_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" + +docker build \ + -f "$SCRIPT_DIR/Dockerfile" \ + -t "$IMAGE_NAME" \ + "$OPENENV_ROOT" diff --git a/src/envs/wildfire_env/server/wildfire_environment.py b/src/envs/wildfire_env/server/wildfire_environment.py new file mode 100644 index 00000000..ad34001d --- /dev/null +++ b/src/envs/wildfire_env/server/wildfire_environment.py @@ -0,0 +1,369 @@ + +import os +import random, uuid +from typing import List +from dataclasses import replace + +from core.env_server import Environment +from ..models import WildfireAction, WildfireObservation, WildfireState + +# Helpers +DIRS_8 = { + "N": (0, -1), "NE": (1, -1), "E": (1, 0), "SE": (1, 1), + "S": (0, 1), "SW": (-1, 1), "W": (-1, 0), "NW": (-1, -1), + "CALM": (0, 0), +} + +def idx(x: int, y: int, w: int) -> int: + return y * w + x + +def in_bounds(x: int, y: int, w: int, h: int) -> bool: + return 0 <= x < w and 0 <= y < h + + +class WildfireEnvironment(Environment): + """ + Weather-aware wildfire simulation. + + Grid encodings: + 0 = ash (burned out) + 1 = fuel / vegetation + 2 = burning + 3 = firebreak + 4 = watered / damp + + Each step: + - agent acts (water/break/wait) + - burning spreads to neighbors with wind + humidity effects + - burning cells burn for multiple ticks, then become ash + """ + + def __init__( + self, + width: int = 32, + height: int = 32, + base_ignite_prob: float = 0.30, + wind_bias: float = 0.20, # kept for compatibility (not directly used in B model) + diag_factor: float = 0.7, # kept for compatibility (not directly used in B model) + humidity: float = 0.25, + init_sources: int = 2, + seed: int = 3407, + max_steps: int = 128, + water_capacity: int = 8, # ↓ encourage strategic water use + break_capacity: int = 50, + ): + super().__init__() + + # --- Env-var overrides (optional) --- + width = int(os.environ.get("WILDFIRE_WIDTH", width)) + height = int(os.environ.get("WILDFIRE_HEIGHT", height)) + humidity = float(os.environ.get("WILDFIRE_HUMIDITY", humidity)) + forced_wind = os.environ.get("WILDFIRE_WIND", None) + + # Store config + self.w = width + self.h = height + self.base_ignite_prob = base_ignite_prob + self.wind_bias = wind_bias + self.diag_factor = diag_factor + self.init_humidity = humidity + self.init_sources = init_sources + self.rng = random.Random(seed) + self.max_steps = max_steps + self.init_water = water_capacity + self.init_breaks = break_capacity + self.forced_wind = forced_wind + + # burn lifetime in ticks (balanced model) + self.burn_lifetime = 3 + + self._state = WildfireState() + + # --- Core API --- + + def reset(self) -> WildfireObservation: + # Start with all fuel + grid = [1] * (self.w * self.h) + + # Wind (forced if provided) + if self.forced_wind and self.forced_wind in DIRS_8: + wind_dir = self.forced_wind + else: + wind_dir = self.rng.choice(list(DIRS_8.keys())) + + # Humidity small variation around init + humidity = min(1.0, max(0.0, self.init_humidity + self.rng.uniform(-0.05, 0.05))) + + # Place initial fires + for _ in range(self.init_sources): + x = self.rng.randrange(self.w) + y = self.rng.randrange(self.h) + grid[idx(x, y, self.w)] = 2 + + self._state = WildfireState( + episode_id=str(uuid.uuid4()), + step_count=0, + total_burned=0, + total_extinguished=0, + last_action="reset", + width=self.w, + height=self.h, + wind_dir=wind_dir, + humidity=humidity, + remaining_water=self.init_water, + remaining_breaks=self.init_breaks, + grid=grid, + ) + + # per-cell burn timers (persist across steps) + self._state.burn_timers = [0] * (self.w * self.h) + + obs = self._make_observation(reward_hint=0.0) + return obs + + def step(self, action: WildfireAction) -> WildfireObservation: + st = self._state + reward = 0.0 + + # --- Agent action effects --- + if ( + action.action == "water" + and st.remaining_water > 0 + and action.x is not None + and action.y is not None + ): + reward += self._apply_water(action.x, action.y) + elif ( + action.action == "break" + and st.remaining_breaks > 0 + and action.x is not None + and action.y is not None + ): + reward += self._apply_break(action.x, action.y) + elif action.action == "wait": + pass + else: + reward -= 0.05 # invalid or exhausted resources + + # --- Natural fire dynamics --- + prev_burning = self._burning_count() + prev_burned = sum(1 for v in st.grid if v == 0) + + newly_burned = self._spread_fire() + new_burning = self._burning_count() + now_burned = sum(1 for v in st.grid if v == 0) + + st.total_burned += newly_burned + st.step_count += 1 + st.last_action = action.action + + # --- Spread vs containment shaping --- + spread_delta = new_burning - prev_burning + burned_delta = now_burned - prev_burned + + # Strong penalty for spread + if spread_delta > 0: + reward -= 0.15 * spread_delta # 🔥 focus on containment + elif spread_delta < 0: + reward += 0.10 * abs(spread_delta) # reward shrinkage + + # Mild penalty for newly burned cells (area loss) + if burned_delta > 0: + reward -= 0.05 * burned_delta + + # Small time penalty to prefer fast control + reward -= 0.01 + + done = self._is_done() + + # --- End of episode bonuses --- + if done: + saved_ratio = self._saved_cells() / (self.w * self.h) + burned_ratio = now_burned / (self.w * self.h) + burning_left = self._burning_count() + + # Big containment bonus + if burning_left == 0: + reward += 0.5 + 0.5 * saved_ratio + + # Fallback proportional reward + reward += 0.2 * (1.0 - burned_ratio) + + obs = self._make_observation(reward_hint=reward) + obs.done = done + obs.reward = reward + return obs + + + # --- Internal mechanics --- + + def _apply_water(self, x: int, y: int) -> float: + st = self._state + if not in_bounds(x, y, self.w, self.h): + return -0.05 + + # Strong penalty if no water left + if st.remaining_water <= 0: + return -0.5 + + i = idx(x, y, self.w) + reward = 0.0 + + if st.grid[i] == 2: + st.grid[i] = 4 # extinguish & dampen + st.burn_timers[i] = 0 + st.total_extinguished += 1 + reward += 0.25 + elif st.grid[i] == 1: + st.grid[i] = 4 # dampen fuel (mild penalty to avoid spamming) + st.burn_timers[i] = 0 + reward -= 0.10 + elif st.grid[i] == 4: + # redundant watering + reward -= 0.05 + else: + # watering ash/break gives slight penalty + reward -= 0.05 + + st.remaining_water -= 1 + return reward + + def _apply_break(self, x: int, y: int) -> float: + st = self._state + if not in_bounds(x, y, self.w, self.h): + return -0.05 + i = idx(x, y, self.w) + reward = 0.0 + + if st.grid[i] in (1, 4): + st.grid[i] = 3 + st.burn_timers[i] = 0 + reward += 0.15 # slightly more than before to make firebreaks attractive + elif st.grid[i] == 2: + st.grid[i] = 3 + st.burn_timers[i] = 0 + reward -= 0.02 + elif st.grid[i] == 3: + reward -= 0.01 + else: + reward -= 0.02 + + st.remaining_breaks -= 1 + return reward + + def _spread_fire(self) -> int: + """ + Balanced wildfire spread model: + - burning cells persist for multiple ticks before turning to ash + - 8-direction spread (diagonals weaker) + - wind accelerates in wind direction, weakens upwind + - humidity suppresses ignition probability + - water (4) is IMMUNE to ignition while damp and reverts to fuel after several ticks + """ + st = self._state + new_grid = st.grid[:] + newly_burned = 0 + + # 8-neighbor model + neighbors = [(-1, 0), (1, 0), (0, -1), (0, 1), + (-1, -1), (1, -1), (-1, 1), (1, 1)] + wx, wy = DIRS_8.get(st.wind_dir, (0, 0)) + + base = self.base_ignite_prob + humidity_factor = (1.0 - st.humidity) + + ignite_flags = [False] * (self.w * self.h) + + # First pass: evaluate ignitions, increment burn timers + for y in range(self.h): + for x in range(self.w): + i = idx(x, y, self.w) + cell = st.grid[i] + + if cell == 2: # burning + st.burn_timers[i] += 1 + + for dx, dy in neighbors: + nx, ny = x + dx, y + dy + if not in_bounds(nx, ny, self.w, self.h): + continue + ni = idx(nx, ny, self.w) + target = st.grid[ni] + + # Only fuel or damp can be candidates, but WATER IS IMMUNE during damp + if target == 4: + # Damp cells do not ignite at all while damp + continue + if target != 1: + continue + + # Wind multiplier + if (dx, dy) == (wx, wy): + wind_mult = 2.0 + elif (dx, dy) == (-wx, -wy): + wind_mult = 0.5 + else: + wind_mult = 1.0 + + # Diagonals weaker + diag_mult = 0.6 if (dx != 0 and dy != 0) else 1.0 + + p = base * humidity_factor * wind_mult * diag_mult + p = max(0.0, min(1.0, p)) + if self.rng.random() < p: + ignite_flags[ni] = True + + # Second pass: apply transitions + for i, cell in enumerate(st.grid): + if cell == 2: + # burns for burn_lifetime ticks before turning to ash + if st.burn_timers[i] >= self.burn_lifetime: + new_grid[i] = 0 # ash + newly_burned += 1 + else: + new_grid[i] = 2 # keep burning + elif ignite_flags[i] and new_grid[i] == 1: + new_grid[i] = 2 + st.burn_timers[i] = 0 + elif cell == 4: + # Water stays damp for several ticks before reverting to fuel + st.burn_timers[i] += 1 + if st.burn_timers[i] >= 6: # was 3; extend to make water useful + new_grid[i] = 1 + + st.grid = new_grid + return newly_burned + + def _burning_count(self) -> int: + return sum(1 for v in self._state.grid if v == 2) + + def _saved_cells(self) -> int: + # cells not turned to ash (includes fuel, burning, break, water) + return sum(1 for v in self._state.grid if v in (1, 2, 3, 4)) + + def _is_done(self) -> bool: + return self._burning_count() == 0 or self._state.step_count >= self.max_steps + + def _make_observation(self, reward_hint: float = 0.0) -> WildfireObservation: + st = self._state + burning = self._burning_count() + burned = sum(1 for v in st.grid if v == 0) + return WildfireObservation( + grid=st.grid[:], + width=self.w, + height=self.h, + step=st.step_count, + wind_dir=st.wind_dir, + humidity=st.humidity, + burning_count=burning, + remaining_water=st.remaining_water, # ✅ new + remaining_breaks=st.remaining_breaks, # ✅ new + burned_count=burned, + reward_hint=reward_hint, + ) + # --- Required abstract property implementation --- + @property + def state(self) -> WildfireState: + """Return the current environment state.""" + return self._state + From d888379e72f6f3fb26f40812007c0fffe5b84451 Mon Sep 17 00:00:00 2001 From: "hash.ee.rama" Date: Tue, 28 Oct 2025 10:21:14 -0700 Subject: [PATCH 02/19] Add sample rendering code to README Updated README with sample rendering code for wildfree simulation. --- src/envs/wildfire_env/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/envs/wildfire_env/README.md b/src/envs/wildfire_env/README.md index bf7a05a3..a2cd012c 100644 --- a/src/envs/wildfire_env/README.md +++ b/src/envs/wildfire_env/README.md @@ -176,7 +176,7 @@ class WildfireState(State): --- ## Sample rendering to see wildfree simulation - +```python import matplotlib.pyplot as plt import numpy as np import time, sys @@ -256,7 +256,7 @@ plt.ioff() # Turn off interactive mode plt.close(fig) # Close the figure at the end print("Animation complete.") - +``` === From 08b2a73b4480d27a5e4dd69287b628627aa6d771 Mon Sep 17 00:00:00 2001 From: "hash.ee.rama" Date: Tue, 28 Oct 2025 10:58:59 -0700 Subject: [PATCH 03/19] Enhance README with wildfire simulation details Added sections on wildfire simulation motivation, research goals, and citations to the README. --- src/envs/wildfire_env/README.md | 49 +++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/src/envs/wildfire_env/README.md b/src/envs/wildfire_env/README.md index a2cd012c..36b0beab 100644 --- a/src/envs/wildfire_env/README.md +++ b/src/envs/wildfire_env/README.md @@ -8,6 +8,28 @@ Agents must contain spreading fires using **water**, **firebreaks**, and **timin [![FastAPI](https://img.shields.io/badge/backend-fastapi-teal)](https://fastapi.tiangolo.com/) [![License](https://img.shields.io/badge/license-MIT-lightgrey)](LICENSE) +--- +## 🔥 Why Wildfire Simulation? + +Wildland fires are intensifying globally due to climate change — increasing the urgency for **AI-assisted decision-making**. +This environment explores how intelligent systems can **control** fire spread in real time, under limited resources. + +### Research Motivation +✅ Based on real wildfire science inspired by: +- **Rothermel Surface Fire Spread Model** (USDA Forest Service) +- **MITRE Fireline’s SimFire** — physics-informed RL fire simulator +- **SimHarness** — RL evaluation for disaster response + +### Application Goals +| Research Theme | Role in This Environment | +|---|---| +| Resource-Constrained Planning | Finite water + firebreak budgets | +| Fire Spread + Containment Strategy | Directional wind & moisture effects | +| Disaster Response RL | Safety-focused reward design | +| LLM Agents for Control Tasks | Text-based action decision making | + +This makes WildfireEnv a **fast, controllable**, and **open benchmark** for applied RL and LLM reasoning. + --- ## 🔥 Environment Overview @@ -315,10 +337,33 @@ strategy: - [Fire Spread Simulation Models (USFS Research)](https://www.fs.fed.us/rm/pubs/rmrs_gtr371.html) --- - -## 🪵 Citation +## 🔖 Citations ```bibtex +@techreport{rothermel2022surface, + title = {The Rothermel Surface Fire Spread Model and Associated Developments}, + author = {Andrews, Patricia L. and Rothermel, Richard C.}, + year = {2022}, + institution = {USDA Forest Service}, + number = {RMRS-GTR-371}, + url = {https://www.fs.usda.gov/rm/pubs_series/rmrs/gtr/rmrs_gtr371.pdf} +} + +@article{tapley2023reinforcement, + title = {Reinforcement Learning for Wildfire Mitigation in Simulated Disaster Environments}, + author = {Tapley, A. and Dotter, M. and Doyle, M. and others}, + journal = {arXiv preprint arXiv:2311.15925}, + year = {2023}, + url = {https://arxiv.org/abs/2311.15925} +} + +@misc{mitrefireline2023simfire, + author = {{MITRE Fireline Project}}, + title = {SimFire: Wildfire Simulator for Decision-Support and AI Research}, + year = {2023}, + howpublished = {\url{https://github.com/mitrefireline/simfire}} +} + @misc{wildfire-openenv-2025, title = {Wildfire Environment for OpenEnv: Containment-Focused RL Simulation}, author = {Harikrishnan, Ram Sankar}, From f663080493fbb1d8055392f8e6cbf9f69fc6489e Mon Sep 17 00:00:00 2001 From: Ram Harikrishnan Date: Wed, 29 Oct 2025 16:38:53 -0700 Subject: [PATCH 04/19] updates to simulation --- src/.ipynb_checkpoints/test-checkpoint.ipynb | 48 +++ src/envs/disease_control_env/__init__.py | 9 + src/envs/disease_control_env/client.py | 16 + src/envs/disease_control_env/models.py | 31 ++ src/envs/disease_control_env/profiles.py | 11 + .../disease_control_env/server/Dockerfile | 12 + .../disease_control_env/server/__init__.py | 3 + src/envs/disease_control_env/server/app.py | 167 ++++++++ .../server/disease_control_environment.py | 385 ++++++++++++++++++ src/envs/wildfire_env/README.md | 38 +- .../server/wildfire_environment.py | 37 +- src/test.ipynb | 223 ++++++++++ 12 files changed, 955 insertions(+), 25 deletions(-) create mode 100644 src/.ipynb_checkpoints/test-checkpoint.ipynb create mode 100644 src/envs/disease_control_env/__init__.py create mode 100644 src/envs/disease_control_env/client.py create mode 100644 src/envs/disease_control_env/models.py create mode 100644 src/envs/disease_control_env/profiles.py create mode 100644 src/envs/disease_control_env/server/Dockerfile create mode 100644 src/envs/disease_control_env/server/__init__.py create mode 100644 src/envs/disease_control_env/server/app.py create mode 100644 src/envs/disease_control_env/server/disease_control_environment.py create mode 100644 src/test.ipynb diff --git a/src/.ipynb_checkpoints/test-checkpoint.ipynb b/src/.ipynb_checkpoints/test-checkpoint.ipynb new file mode 100644 index 00000000..9f7dc52d --- /dev/null +++ b/src/.ipynb_checkpoints/test-checkpoint.ipynb @@ -0,0 +1,48 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "a7146315", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "outputs": [], + "source": [ + "from envs.wildfire_env import WildfireEnv, WildfireAction\n", + "\n", + "env = WildfireEnv(base_url=\"http://localhost:8020\")\n", + "result = env.reset()\n", + "print(\"🔥 Fires:\", result.observation.burning_count)\n", + "print(\"💧 Water:\", result.observation.remaining_water)\n", + "\n", + "for t in range(3):\n", + " action = WildfireAction(action=\"wait\")\n", + " result = env.step(action)\n", + " print(f\"Step {t}: reward={result.reward:.2f}, done={result.done}\")\n", + "env.close()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2451db4d", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/envs/disease_control_env/__init__.py b/src/envs/disease_control_env/__init__.py new file mode 100644 index 00000000..190b3deb --- /dev/null +++ b/src/envs/disease_control_env/__init__.py @@ -0,0 +1,9 @@ +from .models import DiseaseAction, DiseaseObservation, DiseaseState +from .client import DiseaseControlEnv + +__all__ = [ + "DiseaseAction", + "DiseaseObservation", + "DiseaseState", + "DiseaseControlEnv", +] diff --git a/src/envs/disease_control_env/client.py b/src/envs/disease_control_env/client.py new file mode 100644 index 00000000..e4d5882d --- /dev/null +++ b/src/envs/disease_control_env/client.py @@ -0,0 +1,16 @@ +from core.http_env_client import HTTPEnvClient +from core.client_types import StepResult +from .models import DiseaseAction, DiseaseObservation, DiseaseState + +class DiseaseControlEnv(HTTPEnvClient[DiseaseAction, DiseaseObservation]): + def _step_payload(self, action: DiseaseAction) -> dict: + return action.__dict__ + + def _parse_result(self, payload: dict) -> StepResult[DiseaseObservation]: + obs = DiseaseObservation(**payload["observation"]) + return StepResult(observation=obs, + reward=payload["reward"], + done=payload["done"]) + + def _parse_state(self, payload: dict) -> DiseaseState: + return DiseaseState(**payload) diff --git a/src/envs/disease_control_env/models.py b/src/envs/disease_control_env/models.py new file mode 100644 index 00000000..7a52a276 --- /dev/null +++ b/src/envs/disease_control_env/models.py @@ -0,0 +1,31 @@ +# src/envs/disease_control_env/models.py +from dataclasses import dataclass +from core.env_server import Action, Observation, State + +@dataclass +class DiseaseAction(Action): + closures: float # [0,1] + vaccination: float # [0,1] + quarantine: float # [0,1] + spending: float # [0,1] + +@dataclass +class DiseaseObservation(Observation): + S: float; H: float; I: float; Q: float; D: float + step: int + budget: float + disease: str + # live UI helpers + last_event: str | None = None + mu0: float = 0.0 + sigma: float = 0.0 + delta: float = 0.0 + nu: float = 0.0 + phi: float = 0.0 + +@dataclass +class DiseaseState(State): + step_count: int = 0 + episode_return: float = 0.0 + budget: float = 0.0 + disease: str = "covid" diff --git a/src/envs/disease_control_env/profiles.py b/src/envs/disease_control_env/profiles.py new file mode 100644 index 00000000..709827ca --- /dev/null +++ b/src/envs/disease_control_env/profiles.py @@ -0,0 +1,11 @@ +DISEASE_PROFILES = { + "covid": dict(mu0=14.0, sigma=0.025, delta=0.006, nu=0.0015, phi=0.12), + "flu": dict(mu0=12.0, sigma=0.015, delta=0.004, nu=0.0002, phi=0.18), + "measles": dict(mu0=18.0, sigma=0.070, delta=0.000, nu=0.0001, phi=0.08), +} + +MUTATION_VARIANTS = [ + dict(name="high_transmissibility", mu0=1.10, sigma=1.35, delta=1.10, nu=1.00, phi=0.90), + dict(name="immune_escape", mu0=1.15, sigma=1.25, delta=1.20, nu=1.05, phi=0.85), + dict(name="deadlier", mu0=1.00, sigma=1.05, delta=1.00, nu=1.50, phi=0.95), +] diff --git a/src/envs/disease_control_env/server/Dockerfile b/src/envs/disease_control_env/server/Dockerfile new file mode 100644 index 00000000..509bc702 --- /dev/null +++ b/src/envs/disease_control_env/server/Dockerfile @@ -0,0 +1,12 @@ +ARG BASE_IMAGE=openenv-base:latest +FROM ${BASE_IMAGE} + +COPY src/core/ /app/src/core/ +COPY src/envs/disease_control_env/ /app/src/envs/disease_control_env/ + +ENV ENABLE_WEB_INTERFACE=true + +HEALTHCHECK CMD curl -f http://localhost:8000/health || exit 1 + +CMD ["uvicorn", "envs.disease_control_env.server.app:app", "--host", "0.0.0.0", "--port", "8000"] + diff --git a/src/envs/disease_control_env/server/__init__.py b/src/envs/disease_control_env/server/__init__.py new file mode 100644 index 00000000..1ab488dd --- /dev/null +++ b/src/envs/disease_control_env/server/__init__.py @@ -0,0 +1,3 @@ +from .disease_control_environment import DiseaseControlEnvironment + +__all__ = ["DiseaseControlEnvironment"] diff --git a/src/envs/disease_control_env/server/app.py b/src/envs/disease_control_env/server/app.py new file mode 100644 index 00000000..35ce0972 --- /dev/null +++ b/src/envs/disease_control_env/server/app.py @@ -0,0 +1,167 @@ +# src/envs/disease_control_env/server/app.py +import os +from fastapi import FastAPI, Query +from fastapi.responses import HTMLResponse, JSONResponse +from core.env_server import create_fastapi_app, create_web_interface_app +from ..models import DiseaseAction, DiseaseObservation +from .disease_control_environment import DiseaseControlEnvironment + +env = DiseaseControlEnvironment() +app: FastAPI = create_fastapi_app(env, DiseaseAction, DiseaseObservation) + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) + +@app.get("/grid") +def grid(): + return env.get_grid() + +# ---- JSON feeds used by the mini dashboard ---- +@app.get("/timeseries", response_class=JSONResponse) +def timeseries(tail: int | None = Query(default=400, ge=1)): + return env.get_timeseries(tail=tail) + +@app.get("/events", response_class=JSONResponse) +def events(): + return {"events": env.events} + +# ---- Minimal web UI (no external build, uses Chart.js CDN) ---- +ENABLE_WEB = os.getenv("ENABLE_WEB_INTERFACE", "true").lower() == "true" + +if ENABLE_WEB: + @app.get("/web", response_class=HTMLResponse) + def web_ui(): + return HTML_TEMPLATE + +HTML_TEMPLATE = """ + + + + + Disease Control · Live + + + + +

Disease Control

+
+
+

Person Grid (32×32)

+ +
+ Legend: + S + H + I + Q + D +
+
+

Epidemic Curves (S,H,I,Q,D)

+

Budget

+
+
+

Events

+
+
+ + + + +""" diff --git a/src/envs/disease_control_env/server/disease_control_environment.py b/src/envs/disease_control_env/server/disease_control_environment.py new file mode 100644 index 00000000..e0c47d04 --- /dev/null +++ b/src/envs/disease_control_env/server/disease_control_environment.py @@ -0,0 +1,385 @@ +# src/envs/disease_control_env/server/disease_control_environment.py +import numpy as np, uuid, random +from core.env_server import Environment +from ..models import DiseaseAction, DiseaseObservation, DiseaseState +from ..profiles import DISEASE_PROFILES, MUTATION_VARIANTS +import numpy as np, uuid, random +from enum import IntEnum +from typing import Any, Dict, List +class P(IntEnum): + S = 0 # susceptible + H = 1 # protected (vaccinated/recovered) + I = 2 # infected + Q = 3 # quarantined + D = 4 # deceased + +# --- Typing helpers to satisfy Pylance and ensure numeric operations --- +from typing import TypedDict, cast + +class _DiseaseProfile(TypedDict): + mu0: float + sigma: float + delta: float + nu: float + phi: float + +class _VariantParams(TypedDict): + name: str + mu0: float + sigma: float + delta: float + nu: float + phi: float + +class DiseaseControlEnvironment(Environment): + """ + Adds: + - history buffers for live charts (/timeseries) + - resource refills (scheduled + noisy) + - mutation events (single or multi-wave) + """ + def __init__( + self, + T: int = 90, + dt: float = 0.01, + disease: str = "covid", + init_budget: float = 1000.0, + refill_every_steps: int = 14, + refill_amount: float = 200.0, + refill_jitter: float = 0.25, + mutation_at_steps: tuple[int, ...] = (30,), + mutation_prob: float = 0.85, + grid_enabled: bool = True, # 👈 enable the person grid + grid_size: int = 32, # 👈 32x32 + neighborhood: str = "moore", + ): + super().__init__() + self.T, self.dt, self.substeps = T, dt, int(1/dt) + self.init_budget = init_budget + self._rng = np.random.default_rng() + + # event config + self.refill_every_steps = refill_every_steps + self.refill_amount = refill_amount + self.refill_jitter = refill_jitter + self.mutation_at_steps = set(mutation_at_steps) + self.mutation_prob = mutation_prob + self.set_disease_profile(disease) + # core params (set by preset) + self.set_disease_profile(disease) + self.grid_enabled = grid_enabled + self.N = grid_size + self.neighborhood = neighborhood + # histories for live chart + self._clear_history() + + self.reset() + + # -------------------- presets & mutation -------------------- + def set_disease_profile(self, name: str): + cfg = cast(_DiseaseProfile, DISEASE_PROFILES[name]) + # Ensure attributes are numeric for downstream arithmetic + self.mu0 = float(cfg["mu0"]) # type: ignore[assignment] + self.sigma = float(cfg["sigma"]) # type: ignore[assignment] + self.delta = float(cfg["delta"]) # type: ignore[assignment] + self.nu = float(cfg["nu"]) # type: ignore[assignment] + self.phi = float(cfg["phi"]) # type: ignore[assignment] + self.profile_name = name + + def _apply_mutation_variant(self): + variant = cast(_VariantParams, random.choice(MUTATION_VARIANTS)) + # Coerce to float to avoid str|float unions from loose dict typing + self.mu0 *= float(variant["mu0"]) # type: ignore[operator] + self.sigma *= float(variant["sigma"]) # type: ignore[operator] + self.delta *= float(variant["delta"]) # type: ignore[operator] + self.nu *= float(variant["nu"]) # type: ignore[operator] + self.phi *= float(variant["phi"]) # type: ignore[operator] + return f"mutation:{variant['name']}" + + # -------------------- env lifecycle -------------------- + def _clear_history(self): + self.hist_step = [] + self.hist_S = []; self.hist_H = []; self.hist_I = []; self.hist_Q = []; self.hist_D = [] + self.hist_budget = [] + self.events = [] # list[str] + + def reset(self) -> DiseaseObservation: + if self.grid_enabled: + self.grid = self._init_grid() + self._sync_macro_from_grid() + else: + I0 = self._rng.integers(20, 100) / 100000.0 + self.S, self.H, self.I, self.Q, self.D = 1 - I0, 0.0, I0, 0.0, 0.0 + + I0 = self._rng.integers(20, 100) / 100000.0 + self.S, self.H, self.I, self.Q, self.D = 1 - I0, 0.0, I0, 0.0, 0.0 + self.budget = self.init_budget + self._state = DiseaseState( + episode_id=str(uuid.uuid4()), + step_count=0, episode_return=0.0, budget=self.budget, disease=self.profile_name + ) + self._clear_history() + self._push_hist(last_event="reset") + return self._obs(last_event="reset") + + @property + def state(self) -> DiseaseState: + return self._state + + # -------------------- helpers -------------------- + def _noise(self) -> float: + return 1e-4 * np.sqrt(self.dt) * self._rng.normal() + + def _push_hist(self, last_event: str | None): + s = self._state.step_count + self.hist_step.append(s) + self.hist_S.append(float(self.S)); self.hist_H.append(float(self.H)) + self.hist_I.append(float(self.I)); self.hist_Q.append(float(self.Q)); self.hist_D.append(float(self.D)) + self.hist_budget.append(float(self.budget)) + if last_event: + self.events.append(f"t={s}:{last_event}") + + def _maybe_refill(self) -> str | None: + if self.refill_every_steps <= 0: return None + if self._state.step_count > 0 and self._state.step_count % self.refill_every_steps == 0: + jitter = (1.0 + self._rng.uniform(-self.refill_jitter, self.refill_jitter)) + add = max(0.0, self.refill_amount * jitter) + self.budget += add + return f"refill:+{add:.1f}" + return None + + def _maybe_mutate(self) -> str | None: + if self._state.step_count in self.mutation_at_steps: + if random.random() <= self.mutation_prob: + tag = self._apply_mutation_variant() + return tag + return None + + # -------------------- step -------------------- + def step(self, action: DiseaseAction): + self._state.step_count += 1 + + # Continuous controls clamped + c = float(np.clip(action.closures, 0.0, 1.0)) + v = float(np.clip(action.vaccination, 0.0, 1.0)) + q = float(np.clip(action.quarantine, 0.0, 1.0)) + s = float(np.clip(action.spending, 0.0, 1.0)) + + # Spending → intensities (budget constrained) + closures_cost = 200.0 * c * s + vaccine_cost = 400.0 * v * s + quarantine_cost = 600.0 * q * s + total_cost = closures_cost + vaccine_cost + quarantine_cost + + if total_cost > self.budget: + scale = self.budget / (total_cost + 1e-8) + c *= scale; v *= scale; q *= scale + total_cost = self.budget + + self.budget -= total_cost + + # Effective parameters under controls + mu = self.mu0 / (1.0 + 3.0 * c) + beta = 0.002 * v + rho = 0.020 * q + if self.grid_enabled: + # Update micro grid first (one sweep per day) + self._step_grid(mu=mu, beta=beta, rho=rho) + # sync macro fractions from grid; compute reward on the delta of macro aggregates + S_prev, H_prev, I_prev, Q_prev, D_prev = self.S, self.H, self.I, self.Q, self.D + self._sync_macro_from_grid() + new_inf = max(self.I - I_prev, 0.0) + new_deaths = max(self.D - D_prev, 0.0) + else: + # existing macro SDE integration (your current loop) + new_inf = 0.0 + new_deaths = 0.0 + for _ in range(self.substeps): + ... + new_inf += max(self.I - I_prev, 0.0) + new_deaths += max(self.D - D_prev, 0.0) + + econ_term = -(total_cost * 0.01) + reward = -(10*new_deaths + new_inf) + econ_term + new_inf = 0.0; new_deaths = 0.0 + for _ in range(self.substeps): + dS = (self._noise() - self.sigma*self.S*self.I*mu - beta*self.S) + dH = (beta*self.S + self.phi*(self.I+self.Q) - self.delta*self.H*self.I*mu) + dI = (self.sigma*self.S*self.I*mu + self.delta*self.H*self.I*mu + - (self.nu+self.phi+rho)*self.I) + dQ = (rho*self.I - (self.nu+self.phi)*self.Q) + dD = (self.nu*(self.I+self.Q)) + + I_prev, D_prev = self.I, self.D + + self.S = max(self.S + dS*self.dt, 0.0) + self.H = max(self.H + dH*self.dt, 0.0) + self.I = max(self.I + dI*self.dt, 0.0) + self.Q = max(self.Q + dQ*self.dt, 0.0) + self.D = max(self.D + dD*self.dt, 0.0) + + new_inf += max(self.I - I_prev, 0.0) + new_deaths += max(self.D - D_prev, 0.0) + + econ_term = -(total_cost * 0.01) # cost hurts reward + reward = -(10.0*new_deaths + new_inf) + econ_term + + # Events (refill, mutation) AFTER dynamics this step + event = None + ev1 = self._maybe_refill() + if ev1: event = ev1 + ev2 = self._maybe_mutate() + if ev2: event = ev2 if event is None else f"{event}|{ev2}" + + self._state.episode_return += reward + self._state.budget = self.budget + + done = ( + self._state.step_count >= self.T + or self.I < 1e-6 + or self.D >= 0.1 + or self.budget <= 0.0 + ) + + self._push_hist(last_event=event) + return self._obs(last_event=event), reward, done + + # -------------------- observation -------------------- + def _obs(self, last_event: str | None = None) -> DiseaseObservation: + return DiseaseObservation( + S=float(self.S), H=float(self.H), I=float(self.I), Q=float(self.Q), D=float(self.D), + step=self._state.step_count, budget=float(self.budget), disease=self.profile_name, + last_event=last_event, + mu0=float(self.mu0), sigma=float(self.sigma), delta=float(self.delta), + nu=float(self.nu), phi=float(self.phi), + ) + + # -------------------- endpoints used by web UI -------------------- + # These helpers will be called by FastAPI routes defined in app.py + def get_timeseries(self, tail: int | None = None) -> dict: + if tail: + sl = slice(-tail, None) + else: + sl = slice(None) + return { + "step": self.hist_step[sl], + "S": self.hist_S[sl], + "H": self.hist_H[sl], + "I": self.hist_I[sl], + "Q": self.hist_Q[sl], + "D": self.hist_D[sl], + "budget": self.hist_budget[sl], + "events": self.events[sl] if hasattr(self.events, "__getitem__") else self.events, + } + + def _init_grid(self): + # Start mostly S with a few I sprinkled in + g = np.full((self.N, self.N), P.S, dtype=np.uint8) + seeds = self._rng.integers(3, 10) + xs = self._rng.integers(0, self.N, size=seeds) + ys = self._rng.integers(0, self.N, size=seeds) + g[xs, ys] = P.I + return g + + def _neighbors(self, x, y): + if self.neighborhood == "von_neumann": + coords = [(x-1,y),(x+1,y),(x,y-1),(x,y+1)] + else: # moore + coords = [(x+i, y+j) for i in (-1,0,1) for j in (-1,0,1) if not (i==0 and j==0)] + for (i,j) in coords: + if 0 <= i < self.N and 0 <= j < self.N: + yield (i,j) + + + def _step_grid(self, mu, beta, rho): + + """ + One macro 'day' worth of micro transitions (single sweep). + Probabilities derived from macro params & actions. + """ + g = self.grid + new_g = g.copy() + + # map macro parameters to per-contact probabilities + # closures (mu↓) reduces effective contacts; beta is S->H vaccination rate (global) + # base infection chance per infectious neighbor: + base_inf = min(0.4, float(self.sigma * mu * 0.08)) # bounded for stability + rec_I = min(0.35, float(self.phi * 0.5)) # I -> H + die_I = min(0.20, float(self.nu * 8)) # I -> D + die_Q = min(0.20, float(self.nu * 8)) # Q -> D + rec_Q = min(0.50, float(self.phi * 1.2)) # Q -> H + go_Q = min(0.60, float(rho * 1.0)) # I -> Q + + # vaccinate a random subset of S globally based on beta + if beta > 0: + vac_mask = (g == P.S) & (self._rng.random((self.N, self.N)) < min(0.25, beta * 20.0)) + new_g[vac_mask] = P.H + + # loop cells (vectorization is possible; keep simple & clear) + for x in range(self.N): + for y in range(self.N): + s = g[x, y] + if s == P.S: + # infection from any infected neighbor + inf_p = 1.0 + infected_neighbors = 0 + for (i, j) in self._neighbors(x, y): + if g[i, j] == P.I: + infected_neighbors += 1 + inf_p *= (1.0 - base_inf) + # probability at least one successful infection + p_any = 1.0 - inf_p + if self._rng.random() < p_any: + new_g[x, y] = P.I + + elif s == P.I: + # die? + r = self._rng.random() + if r < die_I: + new_g[x, y] = P.D + else: + # quarantine? + if self._rng.random() < go_Q: + new_g[x, y] = P.Q + else: + # recover? + if self._rng.random() < rec_I: + new_g[x, y] = P.H + + elif s == P.Q: + # die or recover + if self._rng.random() < die_Q: + new_g[x, y] = P.D + elif self._rng.random() < rec_Q: + new_g[x, y] = P.H + # H, D remain as-is + + self.grid = new_g + + + def _sync_macro_from_grid(self): + # Convert grid counts -> macro fractions (S,H,I,Q,D) + tot = float(self.N * self.N) + counts = np.bincount(self.grid.ravel(), minlength=5) + self.S = counts[P.S] / tot + self.H = counts[P.H] / tot + self.I = counts[P.I] / tot + self.Q = counts[P.Q] / tot + self.D = counts[P.D] / tot + + + def get_grid(self) -> Dict[str, Any]: + if not getattr(self, "grid_enabled", False): + return { + "enabled": False, + "size": 0, + "grid": [], + "palette": {}, + } + + return { + "enabled": True, + "size": self.N, + "grid": self.grid.tolist(), + "palette": { "S":0, "H":1, "I":2, "Q":3, "D":4 }, + } \ No newline at end of file diff --git a/src/envs/wildfire_env/README.md b/src/envs/wildfire_env/README.md index bf7a05a3..a347ca7b 100644 --- a/src/envs/wildfire_env/README.md +++ b/src/envs/wildfire_env/README.md @@ -8,10 +8,44 @@ Agents must contain spreading fires using **water**, **firebreaks**, and **timin [![FastAPI](https://img.shields.io/badge/backend-fastapi-teal)](https://fastapi.tiangolo.com/) [![License](https://img.shields.io/badge/license-MIT-lightgrey)](LICENSE) +--- +## 🔥 Why Wildfire Simulation? + +Wildland fires are intensifying globally due to climate change — increasing the urgency for **AI-assisted decision-making**. +This environment explores how intelligent systems can **control** fire spread in real time, under limited resources. + +### Research Motivation +✅ Based on real wildfire science inspired by: +- **Rothermel Surface Fire Spread Model** (USDA Forest Service) +- **MITRE Fireline’s SimFire** — physics-informed RL fire simulator +- **SimHarness** — RL evaluation for disaster response + +### Application Goals +| Research Theme | Role in This Environment | +|---|---| +| Resource-Constrained Planning | Finite water + firebreak budgets | +| Fire Spread + Containment Strategy | Directional wind & moisture effects | +| Disaster Response RL | Safety-focused reward design | +| LLM Agents for Control Tasks | Text-based action decision making | + +This makes WildfireEnv a **fast, controllable**, and **open benchmark** for applied RL and LLM reasoning. + --- ## 🔥 Environment Overview +This environment models **forest-fire dynamics** influenced by: +- **Wind direction** (8 directions + calm) +- **Humidity** (suppresses ignition 🍵) +- **Fuel state and burn progression** +- **Limited resources** (water + barriers) +- **Time pressure** (steps = cost) + +🎯 **Goal** → Minimize new fire spread + total burned land + +--- +## 🔥 Environment Overview + This environment models **forest-fire dynamics** influenced by: - **Wind direction** (8 directions + calm) - **Humidity** (suppresses ignition) @@ -176,7 +210,7 @@ class WildfireState(State): --- ## Sample rendering to see wildfree simulation - +```python import matplotlib.pyplot as plt import numpy as np import time, sys @@ -255,7 +289,7 @@ for _ in range(100): plt.ioff() # Turn off interactive mode plt.close(fig) # Close the figure at the end print("Animation complete.") - +``` === diff --git a/src/envs/wildfire_env/server/wildfire_environment.py b/src/envs/wildfire_env/server/wildfire_environment.py index ad34001d..5e5e57c2 100644 --- a/src/envs/wildfire_env/server/wildfire_environment.py +++ b/src/envs/wildfire_env/server/wildfire_environment.py @@ -3,7 +3,6 @@ import random, uuid from typing import List from dataclasses import replace - from core.env_server import Environment from ..models import WildfireAction, WildfireObservation, WildfireState @@ -55,30 +54,22 @@ def __init__( super().__init__() # --- Env-var overrides (optional) --- - width = int(os.environ.get("WILDFIRE_WIDTH", width)) - height = int(os.environ.get("WILDFIRE_HEIGHT", height)) - humidity = float(os.environ.get("WILDFIRE_HUMIDITY", humidity)) - forced_wind = os.environ.get("WILDFIRE_WIND", None) - - # Store config + self.width = int(os.environ.get("WILDFIRE_WIDTH", width)) + self.height = int(os.environ.get("WILDFIRE_HEIGHT", height)) + self.humidity = float(os.environ.get("WILDFIRE_HUMIDITY", humidity)) self.w = width self.h = height - self.base_ignite_prob = base_ignite_prob - self.wind_bias = wind_bias - self.diag_factor = diag_factor - self.init_humidity = humidity - self.init_sources = init_sources - self.rng = random.Random(seed) - self.max_steps = max_steps - self.init_water = water_capacity - self.init_breaks = break_capacity - self.forced_wind = forced_wind - - # burn lifetime in ticks (balanced model) - self.burn_lifetime = 3 - + self.base_ignite_prob = float(os.environ.get("WILDFIRE_BASE_IGNITE_PROB", base_ignite_prob)) + self.wind_bias = float(os.environ.get("WILDFIRE_WIND_BIAS", wind_bias)) + self.diag_factor = float(os.environ.get("WILDFIRE_DIAG_FACTOR", diag_factor)) + self.init_sources = int(os.environ.get("WILDFIRE_INIT_SOURCES", init_sources)) + self.rng = random.Random(int(os.environ.get("WILDFIRE_SEED", seed))) + self.max_steps = int(os.environ.get("WILDFIRE_MAX_STEPS", max_steps)) + self.init_water = int(os.environ.get("WILDFIRE_WATER_CAPACITY", water_capacity)) + self.init_breaks = int(os.environ.get("WILDFIRE_BREAK_CAPACITY", break_capacity)) + self.burn_lifetime = int(os.environ.get("WILDFIRE_BURN_LIFETIME", 3)) # ✅ new + self.forced_wind = os.getenv("WILDFIRE_WIND", "CALM") self._state = WildfireState() - # --- Core API --- def reset(self) -> WildfireObservation: @@ -92,7 +83,7 @@ def reset(self) -> WildfireObservation: wind_dir = self.rng.choice(list(DIRS_8.keys())) # Humidity small variation around init - humidity = min(1.0, max(0.0, self.init_humidity + self.rng.uniform(-0.05, 0.05))) + humidity = min(1.0, max(0.0, self.humidity + self.rng.uniform(-0.05, 0.05))) # Place initial fires for _ in range(self.init_sources): diff --git a/src/test.ipynb b/src/test.ipynb new file mode 100644 index 00000000..f865edd0 --- /dev/null +++ b/src/test.ipynb @@ -0,0 +1,223 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "id": "77f8126d-3f9b-4a09-ba26-50c66b6e3450", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Collecting fastapi\n", + " Using cached fastapi-0.120.1-py3-none-any.whl.metadata (28 kB)\n", + "Collecting starlette<0.50.0,>=0.40.0 (from fastapi)\n", + " Using cached starlette-0.48.0-py3-none-any.whl.metadata (6.3 kB)\n", + "Requirement already satisfied: pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4 in /opt/anaconda3/lib/python3.13/site-packages (from fastapi) (2.10.3)\n", + "Requirement already satisfied: typing-extensions>=4.8.0 in /opt/anaconda3/lib/python3.13/site-packages (from fastapi) (4.12.2)\n", + "Collecting annotated-doc>=0.0.2 (from fastapi)\n", + " Using cached annotated_doc-0.0.3-py3-none-any.whl.metadata (6.6 kB)\n", + "Requirement already satisfied: annotated-types>=0.6.0 in /opt/anaconda3/lib/python3.13/site-packages (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi) (0.6.0)\n", + "Requirement already satisfied: pydantic-core==2.27.1 in /opt/anaconda3/lib/python3.13/site-packages (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi) (2.27.1)\n", + "Requirement already satisfied: anyio<5,>=3.6.2 in /opt/anaconda3/lib/python3.13/site-packages (from starlette<0.50.0,>=0.40.0->fastapi) (4.7.0)\n", + "Requirement already satisfied: idna>=2.8 in /opt/anaconda3/lib/python3.13/site-packages (from anyio<5,>=3.6.2->starlette<0.50.0,>=0.40.0->fastapi) (3.7)\n", + "Requirement already satisfied: sniffio>=1.1 in /opt/anaconda3/lib/python3.13/site-packages (from anyio<5,>=3.6.2->starlette<0.50.0,>=0.40.0->fastapi) (1.3.0)\n", + "Using cached fastapi-0.120.1-py3-none-any.whl (108 kB)\n", + "Using cached starlette-0.48.0-py3-none-any.whl (73 kB)\n", + "Using cached annotated_doc-0.0.3-py3-none-any.whl (5.5 kB)\n", + "Installing collected packages: annotated-doc, starlette, fastapi\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m3/3\u001b[0m [fastapi]\n", + "Successfully installed annotated-doc-0.0.3 fastapi-0.120.1 starlette-0.48.0\n" + ] + } + ], + "source": [ + "!pip install fastapi\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "a7146315", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🔥 Fires: 2\n", + "💧 Water: 8\n", + "Step 0: reward=-0.46, done=False\n", + "Step 1: reward=-1.06, done=False\n", + "Step 2: reward=-1.16, done=False\n" + ] + } + ], + "source": [ + "from envs.wildfire_env import WildfireEnv, WildfireAction\n", + "\n", + "env = WildfireEnv(base_url=\"http://localhost:8020\")\n", + "result = env.reset()\n", + "print(\"🔥 Fires:\", result.observation.burning_count)\n", + "print(\"💧 Water:\", result.observation.remaining_water)\n", + "\n", + "for t in range(3):\n", + " action = WildfireAction(action=\"wait\")\n", + " result = env.step(action)\n", + " print(f\"Step {t}: reward={result.reward:.2f}, done={result.done}\")\n", + "env.close()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "2451db4d", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAZQAAAG5CAYAAACp98JIAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAH0tJREFUeJzt3Xl0VOX9x/HPBEIIAQZBIIkgYUlAFGQTIVZDsASqUqCWKGIQPSggYvQI4saiVUAES1WsopJQV9QC5VhlqSSAGASJAS2LSLGIpqQIpRC2LM/vD5r7y5CFBL4QwffrnJzTublz58nF5p27zDw+55wTAACnKaiqBwAAOD8QFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBRUyrfffiufz1fVwzgpn8+nhQsXVvUwTonP59O3335b1cMwl5qaqnr16lX1MHAGERQjOTk5Gj58uC6++GKFhIQoPDxcvXv3VkZGhrdOVf2SmzJlinw+n+67776A5fPnz1fv3r114YUXyufzKSsry+T1Jk2aJJ/P5335/X5dffXVWrFihcn2KyI7O1u/+tWvztrrlWblypXq27evIiMjTf/tU1NTA/Zv7dq11blzZ82fP99k+z9VU6ZM0RVXXKE6deqoUaNG6t+/v7Zu3Vrm+sOHD5fP59PMmTMDlm/fvl0DBgxQw4YNVbduXSUmJmr37t1nePQ/DwTFyI033qgNGzZo7ty5+vrrr7Vo0SL16NFDe/furdJxrVu3TrNnz1b79u1LfC83N1dXXXWVpk6dav66l156qbKzs5Wdna2MjAxFR0frhhtu0P79+095m8455efnV2jd8PBwhYSEnPJrWcjNzdXll1+uF154wXzbdevW9fbvF198od69eysxMbHcX7AVkZeXZzRCeytWrNCoUaO0Zs0aLVu2TPn5+UpISFBubm6JdRcuXKjPPvtMkZGRActzc3OVkJAgn8+n5cuXa/Xq1Tp27Jj69u2rwsLCs/WjnL8cTtu+ffucJJeenl7mOs2aNXOSvK9mzZp531u0aJHr1KmTCwkJcc2bN3eTJk1yeXl53vcluRdffNH16dPH1axZ00VFRbl33333pOM6cOCAi46OdsuWLXNxcXEuOTm51PV27NjhJLkvvvjipNssWrc8EydOdJdffnnAsp07dzpJbu3atWW+ZtF+TEtLc845l5aW5iS5xYsXu86dO7vg4GC3fPlyFxcX50aPHu3Gjh3rLrjgAte4cWM3ceLEgNeT5BYsWBDwWn/+859djx49XGhoqGvfvr379NNPA54ze/Zs16RJExcaGur69+/vZsyY4fx+/0n3SUUUH09F1t2xY0eZ309JSSkxroKCAhccHBzw30Vpr+n3+11KSopz7v/3y7x581xcXJwLCQlxc+bMcbfddpvr16+fe+aZZ1x4eLirX7++u/vuu92xY8e87Rw9etSNHTvWRUZGulq1armuXbt6/27Fx9m0aVNvf06fPt1sfzrnXE5OjpPkVqxYEbB8165d7qKLLnJfffWVa9asmfv973/vfW/JkiUuKCjI7d+/31u2d+9eJ8ktW7bMbGw/VxyhGKhdu7Zq166thQsX6ujRo6Wus27dOklSSkqKsrOzvcdLlizRrbfeqnvvvVebNm3Syy+/rNTUVD311FMBzx8/frx3FHTrrbdq0KBB2rx5c7njGjVqlK6//nr98pe/NPgpT93Ro0e98+etW7eu9PMffPBBTZkyRZs3b/aOtObOnauwsDB99tlnmjZtmp544gktW7as3O08+uijGjNmjLKyshQTE6NBgwZ5RzyrV6/WiBEjlJycrKysLPXq1avEv8GqVau8f+uyviZPnlzpn+90FRQUaO7cuZKkTp06Vfr548aN07333qvNmzerd+/ekqS0tDRt375daWlpmjt3rlJTU5Wamuo95/bbb9fq1av1zjvvaOPGjRo4cKD69Omjbdu2SZI+++wz3XHHHbr77ruVlZWl+Ph4PfnkkwGve7r7s+hot379+t6ywsJCJSUlaezYsbr00ktLPOfo0aPy+XwBR681a9ZUUFCQPvnkk0rvO5ygqot2vnj//ffdBRdc4GrWrOliY2Pdww8/7DZs2BCwjkr5i/Hqq692kydPDlj2+uuvu4iIiIDnjRgxImCdK6+80o0cObLM8bz99tvusssuc4cPH3bOubN+hBIUFOTCwsJcWFiY8/l8rm7duu6jjz4q9zXLOkJZuHBhwPbj4uLcL37xi4BlV1xxhRs3bpz3WKUcobz66qve9//+9787SW7z5s3OOeduuukmd/311wdsc/DgwQF/UR86dMht27at3K8ff/yx1H1S2r99WVSBIxRJ3v4NCgpyISEh3pFHea9Z2hHKzJkzA9a57bbbXLNmzVx+fr63bODAge6mm25yzjn3zTffOJ/P577//vuA51177bXu4Ycfds45N2jQINenT5+A7990001m+7OwsND17du3xH8HkydPdr169XKFhYXOOVfiCCUnJ8fVrVvXJScnu9zcXHfw4EE3atQoJ8ndddddpb4WKq762U/Y+enGG2/U9ddfr1WrVikjI0OLFy/WtGnT9Oqrr2ro0KFlPm/9+vVat25dwF/DBQUFOnLkiA4dOqRatWpJkrp37x7wvO7du5d5Ef27775TcnKyli5dqpo1a572z3YqWrdurUWLFkmSDhw4oHnz5mngwIFKS0tTly5dKrWt0tY/8ZpQRESEcnJyyt1O8edERERIOn4zRZs2bbR161YNGDAgYP2uXbvqgw8+8B6HhoaqVatWlRr7mVKnTh1lZmZKkg4dOqS//e1vGj58uBo0aKC+fftWalul7d9LL71U1apV8x5HREToyy+/lCRlZmbKOaeYmJiA5xw9elQNGjSQJG3evLnE/uzevbsWL17sPT6d/XnPPfdo48aNAUcV69ev1x/+8AdlZmaWeSdiw4YN9d5772nkyJF67rnnFBQUpEGDBqlTp04BPy9ODUExVLNmTfXq1Uu9evXShAkTNGzYME2cOLHcoBQWFurxxx/Xb37zm1K3V56y/k+zfv165eTkqHPnzt6ygoICrVy5Ui+88IKOHj16xv/PU6NGjYBfFh07dtTChQs1c+ZMvfHGGwoKOn621RWb362sC8JhYWEllgUHBwc89vl8J72oWvw5Rfuu6DnOuRL7s/jYpOOnaE5259gjjzyiRx55pNx1LAQFBQXs3/bt22vp0qV6+umnvaD4fL4SP0Np+7iy+7ewsFDVqlXT+vXrS/x3VLt2bUkl911pTnV/jh49WosWLdLKlSvVpEmTgO3l5OTo4osv9pYVFBTogQce0MyZM71bsRMSErR9+3bt2bNH1atXV7169RQeHq7mzZufdMwoH0E5g9q2bRtwq2hwcLAKCgoC1unUqZO2bt160r/U1qxZoyFDhgQ87tixY6nrXnvttd5fk0Vuv/12tWnTRuPGjauyv8SqVaumw4cPSzr+l6J0/Pbeop/D6rblU9GmTRutXbs2YNnnn38e8LhLly4nHWPx8/lnW/H9Kx3fx9nZ2d7jbdu26dChQ6f9Oh07dlRBQYFycnJ09dVXl7pO27ZttWbNmoBlJz6u7P50zmn06NFasGCB0tPTSwQgKSmpxPXC3r17KykpSbfffnuJbV944YWSpOXLlysnJ0e//vWvyx0LTo6gGPjxxx81cOBA3XHHHWrfvr3q1Kmjzz//XNOmTVO/fv289aKiovTxxx/rqquuUkhIiC644AJNmDBBN9xwg5o2baqBAwcqKChIGzdu1JdffhlwEfO9995Tly5d9Itf/EJvvvmm1q5dq9dee63U8dSpU0eXXXZZwLKwsDA1aNAgYPnevXu1c+dO/fDDD5Lk3XIaHh6u8PDw09on+fn5+te//iXp/095bdq0SePGjZN0/HRHt27dNHXqVEVFRWnPnj167LHHTus1T8fo0aN1zTXX6Nlnn1Xfvn21fPlyffTRRwFHLZU9RXPw4EF988033uMdO3YoKytL9evXD/gr+lQ457z9e/jwYS1btkxLlizRhAkTvHV69uypF154Qd26dVNhYaHGjRtX4sjjVMTExGjw4MEaMmSIZsyYoY4dO2rPnj1avny52rVrp+uuu0733nuvYmNjNW3aNPXv319Lly4NON0lVX5/jho1Sm+99Zb+8pe/qE6dOt7P7/f7FRoaqgYNGnin3IoEBwcrPDw84GaQlJQUXXLJJWrYsKEyMjKUnJys+++//5RuGMEJqu7yzfnjyJEj7qGHHnKdOnVyfr/f1apVy7Vu3do99thj7tChQ956ixYtcq1atXLVq1cPuG148eLFLjY21oWGhrq6deu6rl27utmzZ3vfl+RmzZrlevXq5UJCQlyzZs3c22+/XakxlnZRvuji7olfJ96CW1xFL8oX316tWrVcu3bt3B//+MeA9TZt2uS6devmQkNDXYcOHdzSpUtLvSi/b9++k/4s/fr1c7fddpv3WKVclC/vBgDnjt82fNFFF3m3uT755JMuPDy83J+1PEXjP/Gr+DhLowpelC/6CgkJcTExMe6pp54KuJD+/fffu4SEBBcWFuaio6Pdhx9+WOpF+RNvxii6bbi45ORkFxcX5z0+duyYmzBhgouKinLBwcEuPDzcDRgwwG3cuNFb57XXXvNuw+7bt+9p3zZc2r6UVOJmhOJOvCjvnHPjxo1zjRs3dsHBwS46OtrNmDHDu4iP0+NzrgInO1GlfD6fFixYoP79+1f1UPTtt9+qefPmFTpHfq678847tWXLFq1ateqsvq7P59OOHTsUFRV1Vl8XOF2c8gL+Z/r06erVq5fCwsL00Ucfae7cuXrxxRereljAOYOgAP+zdu1aTZs2TQcOHFCLFi303HPPadiwYVU9LOCcQVDOAT+l00v16tXTxIkTq3oYZ8S7775b1UOQJE2cOJFP5cU5iWsoAAATfJYXAMAEQfkZS09Pl8/n03/+85/T2s7QoUN/EnegpaamqkePHlU9jHJVZJ+fOBHVpEmT1KFDhzM+NuB0EZTzwEsvvaQ6deoEzBVy8OBBBQcHl3gn86pVq+Tz+fT1118rNjZW2dnZ8vv9Z3vInptvvrnEx28UvaFw/PjxAct/97vflZjforJ69OhRYqIx6ezNJngq+3zMmDH6+OOPvcdnK+D79u1TUlKS/H6//H6/kpKSyg1hXl6exo0bp3bt2iksLEyRkZEaMmSI98bZIsOHD1fLli0VGhqqhg0bql+/ftqyZcsZ/mlwNhCU80B8fLwOHjwY8FEhq1atUnh4uNatWxfwcRvp6emKjIxUTEyMatSoofDw8Cqd0jc+Pl6ffPJJQAzT09PVtGlTpaWlBaybnp6u+Pj4sz1EU6eyz2vXrl3iHeBnwy233KKsrCwtXrxYixcvVlZWlpKSkspc/9ChQ8rMzNT48eOVmZmp+fPn6+uvvy7xkSadO3dWSkqKNm/erCVLlsg5p4SEhBIfS4RzUBW+qRKGIiMj3ZQpU7zHDz74oBs1apRr27ZtwMRBPXv2dIMHD3bOlXwnetHETYsXL3Zt2rRxYWFhrnfv3u6HH37wnp+fn+/uv/9+5/f7Xf369d3YsWPdkCFDSryzuqK2bt3qJLmMjAxvWdeuXd2sWbNcjRo1XG5urnPu+IROoaGh7pVXXilzWykpKQHv5i5NWR/jf+KkVRV5t3hcXJy75557XHJysqtXr55r1KiRe/nll93Bgwfd0KFDXe3atV2LFi3chx9+6D2ntHf/n2wiquITlp34KQT637v94+Pj3ahRowLGu2fPHlejRg338ccfl7tPSrNp0yYnya1Zs8ZblpGR4SS5LVu2VHg7a9eudZLcP//5zzLX2bBhg5Pkvvnmm0qPEz8tHKGcJ3r06BHwF31aWpp69OihuLg4b/mxY8eUkZFR7l/5hw4d0vTp0/X6669r5cqV2rlzp8aMGeN9f8aMGZozZ45ee+01ffLJJ9q7d68WLFgQsI3JkyefdOKkonefx8TEKDIy0hvjgQMHlJmZqYEDB6ply5ZavXq1pOMfLHj48OGf3BHK3LlzdeGFF2rt2rUaPXq0Ro4cqYEDByo2NlaZmZnehxOW9aGMFZmIqrgxY8YoMTFRffr08aYAjo2N1bBhw/TWW28FTPD25ptvKjIy0ttnI0aMOOm/y86dOyVJGRkZ8vv9uvLKK73tdevWTX6/X59++mmF98/+/fvl8/nKPJ2Ym5urlJQUNW/eXE2bNq3wdvETVdVFg43Zs2e7sLAwl5eX5/773/+66tWru927d7t33nnHxcbGOuecW7FihZPktm/f7pwr/QhFJ/ylOGvWLNe4cWPvcUREhJs6dar3OC8vzzVp0iTgr/kff/zxpBMnFf+Ms1tuucUlJCQ455z761//6tq2beucc27EiBHukUcecc459/jjj7umTZuWuw8qeoQSHBzsTU5V9BUSEnJKRyjFJ3jKz893YWFhLikpyVuWnZ0dcAR24j6vyERUJ06pXNrYjhw54urXr+/mzZvnLevQoYObNGmS93j37t0n/Xcpmnr6qaeectHR0SX2X3R0dIkJ4cpy+PBh17lzZ++IuLhZs2a5sLAwJ8m1adOGo5PzBG9sPE/Ex8crNzdX69at0759+xQTE6NGjRopLi5OSUlJys3NVXp6ui6++GK1aNGizO3UqlVLLVu29B4Xn7hq//79ys7ODpjsq3r16urSpUvAmy/r169fqY9xj4+P13333ae8vDylp6d7d2rFxcXp+eefl3T8+knPnj0rvM3yDB48WI8++mjAsvnz55/S9L3FJ+2qVq2aGjRooHbt2nnLGjduLEllTv5VkYmoKiIkJES33nqr5syZo8TERGVlZWnDhg0B0yc0atRIjRo1qvA2S7vO40qZN6Y0eXl5uvnmm1VYWFjqx9cMHjxYvXr1UnZ2tqZPn67ExEStXr26yiaEgw1OeZ0nWrVqpSZNmigtLU1paWmKi4uTJG/ioNWrVystLe2kv5RLm1jJVfK9r5U55SUFxrD42OPi4rRu3Trt3bv3pKfqKsPv96tVq1YBXyf+og0KCqrQ5FSl7a/yJvI6UWX3bXmGDRumZcuWadeuXZozZ46uvfZaNWvWzPt+ZU55hYeHa/fu3SVe49///rcXybLk5eUpMTFRO3bs0LJly1S3bt0S6/j9fkVHR+uaa67R+++/ry1btpQ4dYpzD0co55H4+Hilp6dr3759Gjt2rLc8Li5OS5Ys0Zo1a0qdaKii/H6/IiIitGbNGl1zzTWSjs97sn79enXq1Mlbb8SIEUpMTCx3WxdddJH3v1u2bKmmTZtq0aJFysrK8oISERGhqKgozZgxQ0eOHDmr108aNmyor776KmBZVlaWyXwixVVkIqoT1ahRo9Q7otq1a6cuXbrolVde0VtvveUd3RV54oknAq6Hlabotuzu3btr//79Wrt2rbp27Srp+PWe/fv3KzY2tsznF8Vk27ZtSktLq/Ddac65gOs/ODcRlPNIfHy8Ro0apby8PO+XsnQ8KCNHjjT5pZycnKypU6cqOjpal1xyiZ599tkS702o7CmvorG/+OKLatWqVcBfwEWnvVq0aHHak1JVRs+ePfXMM8/oT3/6k7p376433nhDX331VZmzZJ6qikxEdaKoqCgtWbJEW7duVYMGDeT3+73QDRs2TPfcc49q1apV4lRaZU55XXLJJerTp4/uvPNOvfzyy5Kku+66SzfccEPARFRt2rTRlClTNGDAAOXn5+u3v/2tMjMz9cEHH6igoMCbBKt+/fqqUaOG/vGPf2jevHlKSEhQw4YN9f333+vpp59WaGiorrvuugrvN/w0ccrrPBIfH6/Dhw+X+kv5wIED3pHA6XjggQc0ZMgQDR06VN27d1edOnVK/OI6FfHx8Tpw4ECJd7oXjf1s393Vu3dvjR8/Xg8++KCuuOIKHThwIGAKZivdunXTq6++queff14dOnTQ0qVLTzpz5Z133qnWrVurS5cuatiwoXcnnCQNGjRI1atX1y233HLa1yPefPNNtWvXTgkJCUpISFD79u31+uuvB6yzdetW7d+/X5K0a9cuLVq0SLt27VKHDh0UERHhfRXdGVazZk2tWrVK1113nVq1aqXExESFhYXp008/rdT1Hfw08eGQOG+kpqYqNTVV6enpVT2UKvPdd98pKipK69atCzgNCZwNnPICzgN5eXnKzs7WQw89pG7duhETVAlOeQHngdWrV6tZs2Zav369XnrppaoeDn6mOOWF80ZWVpaysrI0dOjQqh4K8LNEUAAAJjjlBQAwQVAAACYICgDARIVvG67KSZgA4Jw3qaoHcHrcxJNfbucIBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYqF7VAwCAn4VJ5/j2K4AjFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBgwueccxVa0ec702MBAPxEVSQVHKEAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBgonpVDwAAfg7cGd6+7wxvvyI4QgEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJqpX9QAA4OfAV9UDOAs4QgEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJqpX9QAAAAYmVfUAOEIBABghKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACaqV/UAAJwlk87x7eMnjyMUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGDC55xzVT0IAMC5jyMUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACAif8DH63lEx27xO8AAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🔥 Fire has fully burned out after 42 steps.\n", + "Animation complete.\n" + ] + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import time, sys\n", + "# FIX 1: Import 'display' alongside 'clear_output'\n", + "from IPython.display import clear_output, display \n", + "import matplotlib.colors as mcolors\n", + "sys.path.append(\"/workspace/OpenEnv/src\")\n", + "from envs.wildfire_env import WildfireEnv, WildfireAction # Ensure these imports work\n", + "\n", + "from envs.wildfire_env.server.wildfire_environment import WildfireEnvironment\n", + "\n", + "# Set up client (assuming server is running on 8010)\n", + "client = WildfireEnv(\"http://localhost:8020\")\n", + "\n", + "# --- Matplotlib Setup ---\n", + "cmap = mcolors.ListedColormap([\n", + " \"black\", # 0 = ash\n", + " \"green\", # 1 = fuel\n", + " \"red\", # 2 = burning\n", + " \"saddlebrown\", # 3 = firebreak\n", + " \"blue\" # 4 = water\n", + "])\n", + "# Normalize so integers map cleanly\n", + "norm = mcolors.BoundaryNorm([0, 1, 2, 3, 4, 5], cmap.N)\n", + "\n", + "# 1. Initialize Interactive Mode\n", + "plt.ion() \n", + "fig, ax = plt.subplots(figsize=(5, 5))\n", + "plt.axis(\"off\")\n", + "\n", + "# 2. Get Initial State and Initialize Image Object\n", + "res = client.reset()\n", + "obs = res.observation\n", + "grid = np.array(obs.grid).reshape(obs.height, obs.width)\n", + "\n", + "# Create the image object once\n", + "im = ax.imshow(grid, cmap=cmap, norm=norm)\n", + "\n", + "# Create the title object once\n", + "title_text = ax.set_title(\n", + " f\"Step {obs.step} | Burning={obs.burning_count} | Burned={obs.burned_count}\\n\"\n", + " f\"Wind={obs.wind_dir} | Humidity={obs.humidity:.2f}\",\n", + " color=\"black\", \n", + " fontsize=10\n", + ")\n", + "\n", + "# NOTE: The initial clear_output and display(fig) calls are removed here.\n", + "\n", + "print(\"Starting smooth animation...\")\n", + "\n", + "# --- Animation Loop (Updating In Place and Forcing Display) ---\n", + "for _ in range(100): \n", + " # 1. Clear previous output\n", + " clear_output(wait=True) \n", + " \n", + " # Get the new grid data\n", + " new_grid = np.array(obs.grid).reshape(obs.height, obs.width)\n", + "\n", + " # 2. Update the Image Data \n", + " im.set_data(new_grid)\n", + "\n", + " # 3. Update the Title Text\n", + " title_text.set_text(\n", + " f\"Step {obs.step} | Burning={obs.burning_count} | Burned={obs.burned_count}\\n\"\n", + " f\"Wind={obs.wind_dir} | Humidity={obs.humidity:.2f}\"\n", + " )\n", + "\n", + " # 4. Display the updated figure (FORCES redraw in non-interactive environments)\n", + " display(fig) \n", + " \n", + " # Control the speed (slightly increased for visibility)\n", + " time.sleep(0.3) \n", + "\n", + " # Advance the simulation\n", + " res = client.step(WildfireAction(action=\"WAIT\"))\n", + " obs = res.observation\n", + "\n", + " # Stop condition\n", + " if obs.burning_count == 0:\n", + " print(f\"🔥 Fire has fully burned out after {obs.step} steps.\")\n", + " break\n", + "\n", + "plt.ioff() # Turn off interactive mode\n", + "plt.close(fig) # Close the figure at the end\n", + "print(\"Animation complete.\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "68782dd4-97b3-483b-a096-1c1a750bf0e8", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [conda env:base] *", + "language": "python", + "name": "conda-base-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 0d9ed29286b53426a6d190757cc7185b011526f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Nov 2025 01:33:19 +0000 Subject: [PATCH 05/19] Address PR #108 review comments for wildfire environment This commit addresses all the review comments from PR #108: 1. Remove .ipynb_checkpoints directories from version control - Deleted all checkpoint files that were accidentally committed 2. Fix hardcoded file paths in README - Removed hardcoded path: sys.path.append("/workspace/OpenEnv/src") - Changed port from 8020 to 8000 for consistency - Removed unnecessary import of WildfireEnvironment from example 3. Standardize environment variable naming - Changed WILDFIRE_W to WILDFIRE_WIDTH in server/app.py - Changed WILDFIRE_H to WILDFIRE_HEIGHT in server/app.py - Now consistent with documentation and other env vars 4. Fix action case consistency - Changed "WAIT" to "wait" in README example - Ensures consistency with lowercase action names 5. Add note about Jupyter-specific dependencies - Added note in README explaining IPython requirements - References new standalone example file 6. Add burn_timers to WildfireState dataclass - Added burn_timers field to models.py for type safety - Prevents runtime attribute assignment outside dataclass 7. Create examples/wildfire.py demonstration file - New standalone Python example without Jupyter dependencies - Demonstrates basic firefighting strategy - Includes visualization using render_grid function 8. Code cleanup - Fixed formatting in models.py (moved misplaced comment) - Removed unused imports (List, replace) from wildfire_environment.py - Improved import organization and PEP 8 compliance - Fixed typo: "wildfree" to "wildfire" in README All changes maintain backward compatibility while improving code quality and usability. --- examples/wildfire.py | 162 +++++++++ .../.ipynb_checkpoints/__init__-checkpoint.py | 9 - .../.ipynb_checkpoints/client-checkpoint.py | 27 -- .../.ipynb_checkpoints/models-checkpoint.py | 43 --- src/envs/wildfire_env/README.md | 19 +- src/envs/wildfire_env/models.py | 6 +- .../.ipynb_checkpoints/app-checkpoint.py | 10 - .../wildfire_environment-checkpoint.py | 321 ------------------ src/envs/wildfire_env/server/app.py | 4 +- .../server/wildfire_environment.py | 5 +- 10 files changed, 179 insertions(+), 427 deletions(-) create mode 100644 examples/wildfire.py delete mode 100644 src/envs/wildfire_env/.ipynb_checkpoints/__init__-checkpoint.py delete mode 100644 src/envs/wildfire_env/.ipynb_checkpoints/client-checkpoint.py delete mode 100644 src/envs/wildfire_env/.ipynb_checkpoints/models-checkpoint.py delete mode 100644 src/envs/wildfire_env/server/.ipynb_checkpoints/app-checkpoint.py delete mode 100644 src/envs/wildfire_env/server/.ipynb_checkpoints/wildfire_environment-checkpoint.py diff --git a/examples/wildfire.py b/examples/wildfire.py new file mode 100644 index 00000000..43f99389 --- /dev/null +++ b/examples/wildfire.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +""" +Simple example demonstrating Wildfire Environment usage. + +This example shows how to: +1. Connect to a Wildfire environment +2. Reset the environment +3. Take strategic actions (water, firebreak, wait) +4. Monitor fire spread and containment +5. Visualize the grid state + +Usage: + # First, start the server: + python -m envs.wildfire_env.server.app + + # Then run this script: + python examples/wildfire.py +""" + +import sys +from pathlib import Path +import random + +# Add src to path +sys.path.insert(0, str(Path(__file__).parent.parent / "src")) + +from envs.wildfire_env import WildfireEnv, WildfireAction +from envs.wildfire_env.client import render_grid + + +def simple_agent_strategy(obs): + """ + Simple firefighting strategy: + - Target burning cells with water if available + - Build firebreaks near fires if water is depleted + - Otherwise wait + """ + # Find burning cells + burning_cells = [] + for y in range(obs.height): + for x in range(obs.width): + idx = y * obs.width + x + if obs.grid[idx] == 2: # burning + burning_cells.append((x, y)) + + if not burning_cells: + return WildfireAction(action="wait") + + # Pick a random burning cell to target + target_x, target_y = random.choice(burning_cells) + + # Use water if available, otherwise use firebreak + if obs.remaining_water > 0: + return WildfireAction(action="water", x=target_x, y=target_y) + elif obs.remaining_breaks > 0: + # Build firebreak adjacent to fire + return WildfireAction(action="break", x=target_x, y=target_y) + else: + return WildfireAction(action="wait") + + +def main(): + """Run a wildfire containment episode.""" + # Connect to the Wildfire environment server + print("Connecting to Wildfire environment...") + print("Note: Make sure the server is running with: python -m envs.wildfire_env.server.app") + + # Connect to local server + env = WildfireEnv(base_url="http://localhost:8000") + + try: + # Reset the environment + print("\nResetting environment...") + result = env.reset() + obs = result.observation + + print(f"\n🌲 Wildfire Containment Mission Started!") + print(f"Grid size: {obs.width}x{obs.height}") + print(f"Initial fires: {obs.burning_count}") + print(f"Wind direction: {obs.wind_dir}") + print(f"Humidity: {obs.humidity:.2f}") + print(f"Water capacity: {obs.remaining_water}") + print(f"Firebreak materials: {obs.remaining_breaks}") + + # Print initial grid + print("\nInitial state:") + print(render_grid(obs)) + print("\nLegend: ⬛=ash 🟩=fuel 🟥=fire 🟫=firebreak 🟦=water") + + # Run episode + print("\n" + "="*60) + print("Starting containment operations...") + print("="*60) + + episode_reward = 0 + step_count = 0 + max_steps = 50 # Limit steps for demo + + while not result.done and step_count < max_steps: + # Choose action using simple strategy + action = simple_agent_strategy(obs) + + # Take action + result = env.step(action) + obs = result.observation + episode_reward += result.reward or 0 + step_count += 1 + + # Print progress every 5 steps + if step_count % 5 == 0 or result.done: + print(f"\n--- Step {step_count} ---") + print(f"Action: {action.action}" + + (f" at ({action.x}, {action.y})" if action.x is not None else "")) + print(f"Reward: {result.reward:.3f} | Total: {episode_reward:.2f}") + print(f"Fires: {obs.burning_count} | Burned: {obs.burned_count}") + print(f"Water left: {obs.remaining_water} | Breaks left: {obs.remaining_breaks}") + print(render_grid(obs)) + + if result.done: + break + + # Episode summary + print("\n" + "="*60) + print("🏁 EPISODE COMPLETE") + print("="*60) + + if obs.burning_count == 0: + print("✅ SUCCESS! All fires have been extinguished!") + else: + print(f"⚠️ Episode ended with {obs.burning_count} fires still burning") + + print(f"\nFinal Statistics:") + print(f" Steps taken: {step_count}") + print(f" Total reward: {episode_reward:.2f}") + print(f" Cells burned: {obs.burned_count}") + print(f" Cells saved: {obs.width * obs.height - obs.burned_count}") + print(f" Water used: {result.observation.remaining_water} remaining (started with more)") + print(f" Firebreaks used: {result.observation.remaining_breaks} remaining") + + # Get environment state + state = env.state() + print(f"\n📊 Environment State:") + print(f" Episode ID: {state.episode_id}") + print(f" Total burned: {state.total_burned}") + print(f" Total extinguished: {state.total_extinguished}") + print(f" Final wind: {state.wind_dir}") + print(f" Final humidity: {state.humidity:.2f}") + + except Exception as e: + print(f"\n❌ Error: {e}") + print("\nMake sure the Wildfire server is running:") + print(" python -m envs.wildfire_env.server.app") + + finally: + # Cleanup + print("\nClosing environment...") + env.close() + print("Done!") + + +if __name__ == "__main__": + main() diff --git a/src/envs/wildfire_env/.ipynb_checkpoints/__init__-checkpoint.py b/src/envs/wildfire_env/.ipynb_checkpoints/__init__-checkpoint.py deleted file mode 100644 index 5df8fe34..00000000 --- a/src/envs/wildfire_env/.ipynb_checkpoints/__init__-checkpoint.py +++ /dev/null @@ -1,9 +0,0 @@ -from .models import WildfireAction, WildfireObservation, WildfireState -from .client import WildfireEnv - -__all__ = [ - "WildfireAction", - "WildfireObservation", - "WildfireState", - "WildfireEnv", -] diff --git a/src/envs/wildfire_env/.ipynb_checkpoints/client-checkpoint.py b/src/envs/wildfire_env/.ipynb_checkpoints/client-checkpoint.py deleted file mode 100644 index 104119e7..00000000 --- a/src/envs/wildfire_env/.ipynb_checkpoints/client-checkpoint.py +++ /dev/null @@ -1,27 +0,0 @@ -from core.http_env_client import HTTPEnvClient -from core.client_types import StepResult -from .models import WildfireAction, WildfireObservation, WildfireState - -class WildfireEnv(HTTPEnvClient[WildfireAction, WildfireObservation]): - def _step_payload(self, action: WildfireAction) -> dict: - return {"action": action.action, "x": action.x, "y": action.y} - - def _parse_result(self, payload: dict) -> StepResult[WildfireObservation]: - obs = WildfireObservation(**payload["observation"]) - return StepResult( - observation=obs, - reward=payload.get("reward"), - done=payload.get("done", False), - ) - - def _parse_state(self, payload: dict) -> WildfireState: - return WildfireState(**payload) -def render_grid(obs: WildfireObservation) -> str: - legend = {0:"⬛", 1:"🟩", 2:"🟥", 3:"🟫", 4:"🟦"} - w, h = obs.width, obs.height - g = obs.grid - rows = [] - for y in range(h): - rows.append("".join(legend.get(g[y*w+x], "?") for x in range(w))) - meta = f"step={obs.step} wind={obs.wind_dir} hum={obs.humidity:.2f} burning={obs.burning_count} burned= {obs.burned_count}" - return "\n".join(rows + [meta]) diff --git a/src/envs/wildfire_env/.ipynb_checkpoints/models-checkpoint.py b/src/envs/wildfire_env/.ipynb_checkpoints/models-checkpoint.py deleted file mode 100644 index 3eb40aa4..00000000 --- a/src/envs/wildfire_env/.ipynb_checkpoints/models-checkpoint.py +++ /dev/null @@ -1,43 +0,0 @@ -from dataclasses import dataclass, field -from typing import List, Optional -from core.env_server import Action, Observation, State - -# Grid cell encoding: -# 0 = empty/ash, 1 = fuel (healthy), 2 = burning, 3 = firebreak, 4 = watered (damp) -# (You can tweak encodings, but keep them ints for compact obs.) - -@dataclass -class WildfireAction(Action): - # action: "break" (build firebreak), "water" (drop water), "wait" - action: str - x: Optional[int] = None - y: Optional[int] = None - -@dataclass -class WildfireObservation(Observation): - grid: List[int] # flattened grid H*W, ints in {0..4} - width: int - height: int - step: int - wind_dir: str # e.g. "N","NE","E","SE","S","SW","W","NW","CALM" - humidity: float # [0,1] - burning_count: int - burned_count: int # total ash (0) cells (cumulative) - reward_hint: float = 0.0 # optional shaping info - -@dataclass -class WildfireState(State): - episode_id: str = "" - step_count: int = 0 - total_burned: int = 0 - total_extinguished: int = 0 - last_action: str = "reset" - # For visibility / debugging (not required by core): - width: int = 0 - height: int = 0 - wind_dir: str = "CALM" - humidity: float = 0.25 - remaining_water: int = 20 # simple resource constraint - remaining_breaks: int = 50 - # internal full grid as flattened ints - grid: List[int] = field(default_factory=list) diff --git a/src/envs/wildfire_env/README.md b/src/envs/wildfire_env/README.md index 36b0beab..d7c6c1c3 100644 --- a/src/envs/wildfire_env/README.md +++ b/src/envs/wildfire_env/README.md @@ -197,21 +197,20 @@ class WildfireState(State): ``` --- -## Sample rendering to see wildfree simulation +## Sample rendering to see wildfire simulation + +**Note:** This example requires Jupyter notebook or IPython environment for the `clear_output` and `display` functions. For standalone Python scripts, see `examples/wildfire.py`. + ```python import matplotlib.pyplot as plt import numpy as np -import time, sys +import time -from IPython.display import clear_output, display +from IPython.display import clear_output, display import matplotlib.colors as mcolors -sys.path.append("/workspace/OpenEnv/src") -from envs.wildfire_env import WildfireEnv, WildfireAction # Ensure these imports work - -from envs.wildfire_env.server.wildfire_environment import WildfireEnvironment - +from envs.wildfire_env import WildfireEnv, WildfireAction -client = WildfireEnv("http://localhost:8020") +client = WildfireEnv("http://localhost:8000") cmap = mcolors.ListedColormap([ @@ -267,7 +266,7 @@ for _ in range(100): time.sleep(0.3) - res = client.step(WildfireAction(action="WAIT")) + res = client.step(WildfireAction(action="wait")) obs = res.observation if obs.burning_count == 0: diff --git a/src/envs/wildfire_env/models.py b/src/envs/wildfire_env/models.py index bfd2851d..13ba5d47 100644 --- a/src/envs/wildfire_env/models.py +++ b/src/envs/wildfire_env/models.py @@ -23,9 +23,9 @@ class WildfireObservation(Observation): humidity: float # [0,1] burning_count: int burned_count: int # total ash (0) cells (cumulative) - reward_hint: float = 0.0 + reward_hint: float = 0.0 # optional shaping info remaining_water: int = 0 - remaining_breaks: int = 0# optional shaping info + remaining_breaks: int = 0 @dataclass class WildfireState(State): @@ -43,3 +43,5 @@ class WildfireState(State): remaining_breaks: int = 50 # internal full grid as flattened ints grid: List[int] = field(default_factory=list) + # burn timers for each cell (track how long cells have been burning/damp) + burn_timers: List[int] = field(default_factory=list) diff --git a/src/envs/wildfire_env/server/.ipynb_checkpoints/app-checkpoint.py b/src/envs/wildfire_env/server/.ipynb_checkpoints/app-checkpoint.py deleted file mode 100644 index 4f818094..00000000 --- a/src/envs/wildfire_env/server/.ipynb_checkpoints/app-checkpoint.py +++ /dev/null @@ -1,10 +0,0 @@ -# server/app.py -import os -from core.env_server import create_fastapi_app -from ..models import WildfireAction, WildfireObservation -from .wildfire_environment import WildfireEnvironment - -W = int(os.getenv("WILDFIRE_W", "16")) -H = int(os.getenv("WILDFIRE_H", "16")) -env = WildfireEnvironment(width=W, height=H) -app = create_fastapi_app(env, WildfireAction, WildfireObservation) diff --git a/src/envs/wildfire_env/server/.ipynb_checkpoints/wildfire_environment-checkpoint.py b/src/envs/wildfire_env/server/.ipynb_checkpoints/wildfire_environment-checkpoint.py deleted file mode 100644 index dcd996c4..00000000 --- a/src/envs/wildfire_env/server/.ipynb_checkpoints/wildfire_environment-checkpoint.py +++ /dev/null @@ -1,321 +0,0 @@ -import os -import random, uuid -from typing import List -from dataclasses import replace - -from core.env_server import Environment -from ..models import WildfireAction, WildfireObservation, WildfireState - -# Helpers -DIRS_8 = { - "N": (0, -1), "NE": (1, -1), "E": (1, 0), "SE": (1, 1), - "S": (0, 1), "SW": (-1, 1), "W": (-1, 0), "NW": (-1, -1), - "CALM": (0, 0), -} - -def idx(x: int, y: int, w: int) -> int: - return y * w + x - -def in_bounds(x: int, y: int, w: int, h: int) -> bool: - return 0 <= x < w and 0 <= y < h - - -class WildfireEnvironment(Environment): - """ - Weather-aware wildfire simulation. - - Grid encodings: - 0 = ash (burned out) - 1 = fuel / vegetation - 2 = burning - 3 = firebreak - 4 = watered / damp - - Each step: - - agent acts (water/break/wait) - - burning spreads to neighbors with wind + humidity effects - - burning cells burn for multiple ticks, then become ash - """ - - def __init__( - self, - width: int = 32, - height: int = 32, - base_ignite_prob: float = 0.30, - wind_bias: float = 0.20, # kept for compatibility (not directly used in B model) - diag_factor: float = 0.7, # kept for compatibility (not directly used in B model) - humidity: float = 0.25, - init_sources: int = 2, - seed: int = 3407, - max_steps: int = 128, - water_capacity: int = 20, - break_capacity: int = 50, - ): - super().__init__() - - # --- Env-var overrides (optional) --- - width = int(os.environ.get("WILDFIRE_WIDTH", width)) - height = int(os.environ.get("WILDFIRE_HEIGHT", height)) - humidity = float(os.environ.get("WILDFIRE_HUMIDITY", humidity)) - forced_wind = os.environ.get("WILDFIRE_WIND", None) - - # Store config - self.w = width - self.h = height - self.base_ignite_prob = base_ignite_prob - self.wind_bias = wind_bias - self.diag_factor = diag_factor - self.init_humidity = humidity - self.init_sources = init_sources - self.rng = random.Random(seed) - self.max_steps = max_steps - self.init_water = water_capacity - self.init_breaks = break_capacity - self.forced_wind = forced_wind - - # burn lifetime in ticks (balanced model) - self.burn_lifetime = 3 - - self._state = WildfireState() - - # --- Core API --- - - def reset(self) -> WildfireObservation: - # Start with all fuel - grid = [1] * (self.w * self.h) - - # Wind (forced if provided) - if self.forced_wind and self.forced_wind in DIRS_8: - wind_dir = self.forced_wind - else: - wind_dir = self.rng.choice(list(DIRS_8.keys())) - - # Humidity small variation around init - humidity = min(1.0, max(0.0, self.init_humidity + self.rng.uniform(-0.05, 0.05))) - - # Place initial fires - for _ in range(self.init_sources): - x = self.rng.randrange(self.w) - y = self.rng.randrange(self.h) - grid[idx(x, y, self.w)] = 2 - - self._state = WildfireState( - episode_id=str(uuid.uuid4()), - step_count=0, - total_burned=0, - total_extinguished=0, - last_action="reset", - width=self.w, - height=self.h, - wind_dir=wind_dir, - humidity=humidity, - remaining_water=self.init_water, - remaining_breaks=self.init_breaks, - grid=grid, - ) - - # per-cell burn timers (persist across steps) - self._state.burn_timers = [0] * (self.w * self.h) - - obs = self._make_observation(reward_hint=0.0) - return obs - - def step(self, action: WildfireAction) -> WildfireObservation: - st = self._state - - # Apply agent action - reward = 0.0 - if action.action == "water" and st.remaining_water > 0 and action.x is not None and action.y is not None: - reward += self._apply_water(action.x, action.y) - elif action.action == "break" and st.remaining_breaks > 0 and action.x is not None and action.y is not None: - reward += self._apply_break(action.x, action.y) - elif action.action == "wait": - pass - else: - # invalid or no resources - reward -= 0.05 - - # Natural fire dynamics - newly_burned = self._spread_fire() - st.total_burned += newly_burned - - # small per-step penalty (encourage faster containment) - reward -= 0.01 - - st.step_count += 1 - st.last_action = action.action - - done = self._is_done() - - if done: - # reward for saved area - saved = self._saved_cells() - reward += 0.5 * (saved / (self.w * self.h)) - # reward if fully extinguished - if self._burning_count() == 0: - reward += 0.5 - - obs = self._make_observation(reward_hint=reward) - obs.done = done - obs.reward = reward - return obs - - @property - def state(self) -> WildfireState: - return self._state - - # --- Internal mechanics --- - - def _apply_water(self, x: int, y: int) -> float: - st = self._state - if not in_bounds(x, y, self.w, self.h): - return -0.05 - i = idx(x, y, self.w) - reward = 0.0 - - if st.grid[i] == 2: - st.grid[i] = 4 - st.burn_timers[i] = 0 - st.total_extinguished += 1 - reward += 0.2 - elif st.grid[i] == 1: - st.grid[i] = 4 # dampen - reward += 0.05 - elif st.grid[i] == 4: - reward -= 0.01 - else: - reward -= 0.02 - - st.remaining_water -= 1 - return reward - - def _apply_break(self, x: int, y: int) -> float: - st = self._state - if not in_bounds(x, y, self.w, self.h): - return -0.05 - i = idx(x, y, self.w) - reward = 0.0 - - if st.grid[i] in (1, 4): - st.grid[i] = 3 - st.burn_timers[i] = 0 - reward += 0.1 - elif st.grid[i] == 2: - st.grid[i] = 3 - st.burn_timers[i] = 0 - reward -= 0.02 - elif st.grid[i] == 3: - reward -= 0.01 - else: - reward -= 0.02 - - st.remaining_breaks -= 1 - return reward - - def _spread_fire(self) -> int: - """ - Balanced wildfire spread model: - - burning cells persist for multiple ticks before turning to ash - - 8-direction spread (diagonals weaker) - - wind accelerates in wind direction, weakens upwind - - humidity suppresses ignition probability - - water (4) reduces ignition chance and reverts to fuel next tick - """ - st = self._state - new_grid = st.grid[:] - newly_burned = 0 - - # 8-neighbor model - neighbors = [(-1, 0), (1, 0), (0, -1), (0, 1), - (-1, -1), (1, -1), (-1, 1), (1, 1)] - wx, wy = DIRS_8.get(st.wind_dir, (0, 0)) - - base = self.base_ignite_prob - humidity_factor = (1.0 - st.humidity) - - ignite_flags = [False] * (self.w * self.h) - - # First pass: evaluate ignitions, increment burn timers - for y in range(self.h): - for x in range(self.w): - i = idx(x, y, self.w) - cell = st.grid[i] - - if cell == 2: # burning - st.burn_timers[i] += 1 - - for dx, dy in neighbors: - nx, ny = x + dx, y + dy - if not in_bounds(nx, ny, self.w, self.h): - continue - ni = idx(nx, ny, self.w) - target = st.grid[ni] - - if target not in (1, 4): # only fuel or damp can ignite - continue - - # Wind multiplier - if (dx, dy) == (wx, wy): - wind_mult = 2.0 - elif (dx, dy) == (-wx, -wy): - wind_mult = 0.5 - else: - wind_mult = 1.0 - - # Diagonals weaker - diag_mult = 0.6 if (dx != 0 and dy != 0) else 1.0 - - p = base * humidity_factor * wind_mult * diag_mult - - # Damp fuel further reduces spread - if target == 4: - p *= 0.35 - - p = max(0.0, min(1.0, p)) - if self.rng.random() < p: - ignite_flags[ni] = True - - # Second pass: apply transitions - for i, cell in enumerate(st.grid): - if cell == 2: - # burns for burn_lifetime ticks before turning to ash - if st.burn_timers[i] >= self.burn_lifetime: - new_grid[i] = 0 # ash - newly_burned += 1 - else: - new_grid[i] = 2 # keep burning - elif ignite_flags[i] and new_grid[i] in (1, 4): - new_grid[i] = 2 - st.burn_timers[i] = 0 - elif cell == 4: - # water effect lasts one tick - new_grid[i] = 1 - - st.grid = new_grid - return newly_burned - - def _burning_count(self) -> int: - return sum(1 for v in self._state.grid if v == 2) - - def _saved_cells(self) -> int: - # cells not turned to ash (includes fuel, burning, break, water) - return sum(1 for v in self._state.grid if v in (1, 2, 3, 4)) - - def _is_done(self) -> bool: - return self._burning_count() == 0 or self._state.step_count >= self.max_steps - - def _make_observation(self, reward_hint: float = 0.0) -> WildfireObservation: - st = self._state - burning = self._burning_count() - burned = sum(1 for v in st.grid if v == 0) - return WildfireObservation( - grid=st.grid[:], - width=self.w, - height=self.h, - step=st.step_count, - wind_dir=st.wind_dir, - humidity=st.humidity, - burning_count=burning, - burned_count=burned, - reward_hint=reward_hint, - ) diff --git a/src/envs/wildfire_env/server/app.py b/src/envs/wildfire_env/server/app.py index 4f818094..bf9935f2 100644 --- a/src/envs/wildfire_env/server/app.py +++ b/src/envs/wildfire_env/server/app.py @@ -4,7 +4,7 @@ from ..models import WildfireAction, WildfireObservation from .wildfire_environment import WildfireEnvironment -W = int(os.getenv("WILDFIRE_W", "16")) -H = int(os.getenv("WILDFIRE_H", "16")) +W = int(os.getenv("WILDFIRE_WIDTH", "16")) +H = int(os.getenv("WILDFIRE_HEIGHT", "16")) env = WildfireEnvironment(width=W, height=H) app = create_fastapi_app(env, WildfireAction, WildfireObservation) diff --git a/src/envs/wildfire_env/server/wildfire_environment.py b/src/envs/wildfire_env/server/wildfire_environment.py index ad34001d..8a27a3fb 100644 --- a/src/envs/wildfire_env/server/wildfire_environment.py +++ b/src/envs/wildfire_env/server/wildfire_environment.py @@ -1,8 +1,7 @@ import os -import random, uuid -from typing import List -from dataclasses import replace +import random +import uuid from core.env_server import Environment from ..models import WildfireAction, WildfireObservation, WildfireState From 383a5e68713cd3569d1ed3c31c63aa00314477ef Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Nov 2025 01:52:07 +0000 Subject: [PATCH 06/19] Fix remaining code style issues from Copilot review Address final Copilot AI review comments: 1. Add blank line before render_grid function (PEP 8) - Added two blank lines between class and module-level function 2. Fix excessive whitespace in client.py - Removed extra spaces after "burned=" in metadata string 3. Fix inconsistent indentation in wildfire_environment.py - Corrected state property indentation to use 4-space standard - Fixed comment alignment - Fixed docstring and return statement indentation All code now follows PEP 8 style guidelines. --- src/envs/wildfire_env/client.py | 4 +++- src/envs/wildfire_env/server/wildfire_environment.py | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/envs/wildfire_env/client.py b/src/envs/wildfire_env/client.py index 104119e7..49c7cd89 100644 --- a/src/envs/wildfire_env/client.py +++ b/src/envs/wildfire_env/client.py @@ -16,6 +16,8 @@ def _parse_result(self, payload: dict) -> StepResult[WildfireObservation]: def _parse_state(self, payload: dict) -> WildfireState: return WildfireState(**payload) + + def render_grid(obs: WildfireObservation) -> str: legend = {0:"⬛", 1:"🟩", 2:"🟥", 3:"🟫", 4:"🟦"} w, h = obs.width, obs.height @@ -23,5 +25,5 @@ def render_grid(obs: WildfireObservation) -> str: rows = [] for y in range(h): rows.append("".join(legend.get(g[y*w+x], "?") for x in range(w))) - meta = f"step={obs.step} wind={obs.wind_dir} hum={obs.humidity:.2f} burning={obs.burning_count} burned= {obs.burned_count}" + meta = f"step={obs.step} wind={obs.wind_dir} hum={obs.humidity:.2f} burning={obs.burning_count} burned={obs.burned_count}" return "\n".join(rows + [meta]) diff --git a/src/envs/wildfire_env/server/wildfire_environment.py b/src/envs/wildfire_env/server/wildfire_environment.py index 8a27a3fb..a6f04f98 100644 --- a/src/envs/wildfire_env/server/wildfire_environment.py +++ b/src/envs/wildfire_env/server/wildfire_environment.py @@ -360,9 +360,10 @@ def _make_observation(self, reward_hint: float = 0.0) -> WildfireObservation: burned_count=burned, reward_hint=reward_hint, ) - # --- Required abstract property implementation --- + + # --- Required abstract property implementation --- @property def state(self) -> WildfireState: - """Return the current environment state.""" - return self._state + """Return the current environment state.""" + return self._state From b4ed274808993937cff3d42fc5ffa0f43799a817 Mon Sep 17 00:00:00 2001 From: Ram Harikrishnan Date: Sat, 1 Nov 2025 19:17:26 -0700 Subject: [PATCH 07/19] deleted unfinished environment --- src/envs/disease_control_env/__init__.py | 9 - src/envs/disease_control_env/client.py | 16 - src/envs/disease_control_env/models.py | 31 -- src/envs/disease_control_env/profiles.py | 11 - .../disease_control_env/server/Dockerfile | 12 - .../disease_control_env/server/__init__.py | 3 - src/envs/disease_control_env/server/app.py | 167 -------- .../server/disease_control_environment.py | 385 ------------------ 8 files changed, 634 deletions(-) delete mode 100644 src/envs/disease_control_env/__init__.py delete mode 100644 src/envs/disease_control_env/client.py delete mode 100644 src/envs/disease_control_env/models.py delete mode 100644 src/envs/disease_control_env/profiles.py delete mode 100644 src/envs/disease_control_env/server/Dockerfile delete mode 100644 src/envs/disease_control_env/server/__init__.py delete mode 100644 src/envs/disease_control_env/server/app.py delete mode 100644 src/envs/disease_control_env/server/disease_control_environment.py diff --git a/src/envs/disease_control_env/__init__.py b/src/envs/disease_control_env/__init__.py deleted file mode 100644 index 190b3deb..00000000 --- a/src/envs/disease_control_env/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .models import DiseaseAction, DiseaseObservation, DiseaseState -from .client import DiseaseControlEnv - -__all__ = [ - "DiseaseAction", - "DiseaseObservation", - "DiseaseState", - "DiseaseControlEnv", -] diff --git a/src/envs/disease_control_env/client.py b/src/envs/disease_control_env/client.py deleted file mode 100644 index e4d5882d..00000000 --- a/src/envs/disease_control_env/client.py +++ /dev/null @@ -1,16 +0,0 @@ -from core.http_env_client import HTTPEnvClient -from core.client_types import StepResult -from .models import DiseaseAction, DiseaseObservation, DiseaseState - -class DiseaseControlEnv(HTTPEnvClient[DiseaseAction, DiseaseObservation]): - def _step_payload(self, action: DiseaseAction) -> dict: - return action.__dict__ - - def _parse_result(self, payload: dict) -> StepResult[DiseaseObservation]: - obs = DiseaseObservation(**payload["observation"]) - return StepResult(observation=obs, - reward=payload["reward"], - done=payload["done"]) - - def _parse_state(self, payload: dict) -> DiseaseState: - return DiseaseState(**payload) diff --git a/src/envs/disease_control_env/models.py b/src/envs/disease_control_env/models.py deleted file mode 100644 index 7a52a276..00000000 --- a/src/envs/disease_control_env/models.py +++ /dev/null @@ -1,31 +0,0 @@ -# src/envs/disease_control_env/models.py -from dataclasses import dataclass -from core.env_server import Action, Observation, State - -@dataclass -class DiseaseAction(Action): - closures: float # [0,1] - vaccination: float # [0,1] - quarantine: float # [0,1] - spending: float # [0,1] - -@dataclass -class DiseaseObservation(Observation): - S: float; H: float; I: float; Q: float; D: float - step: int - budget: float - disease: str - # live UI helpers - last_event: str | None = None - mu0: float = 0.0 - sigma: float = 0.0 - delta: float = 0.0 - nu: float = 0.0 - phi: float = 0.0 - -@dataclass -class DiseaseState(State): - step_count: int = 0 - episode_return: float = 0.0 - budget: float = 0.0 - disease: str = "covid" diff --git a/src/envs/disease_control_env/profiles.py b/src/envs/disease_control_env/profiles.py deleted file mode 100644 index 709827ca..00000000 --- a/src/envs/disease_control_env/profiles.py +++ /dev/null @@ -1,11 +0,0 @@ -DISEASE_PROFILES = { - "covid": dict(mu0=14.0, sigma=0.025, delta=0.006, nu=0.0015, phi=0.12), - "flu": dict(mu0=12.0, sigma=0.015, delta=0.004, nu=0.0002, phi=0.18), - "measles": dict(mu0=18.0, sigma=0.070, delta=0.000, nu=0.0001, phi=0.08), -} - -MUTATION_VARIANTS = [ - dict(name="high_transmissibility", mu0=1.10, sigma=1.35, delta=1.10, nu=1.00, phi=0.90), - dict(name="immune_escape", mu0=1.15, sigma=1.25, delta=1.20, nu=1.05, phi=0.85), - dict(name="deadlier", mu0=1.00, sigma=1.05, delta=1.00, nu=1.50, phi=0.95), -] diff --git a/src/envs/disease_control_env/server/Dockerfile b/src/envs/disease_control_env/server/Dockerfile deleted file mode 100644 index 509bc702..00000000 --- a/src/envs/disease_control_env/server/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -ARG BASE_IMAGE=openenv-base:latest -FROM ${BASE_IMAGE} - -COPY src/core/ /app/src/core/ -COPY src/envs/disease_control_env/ /app/src/envs/disease_control_env/ - -ENV ENABLE_WEB_INTERFACE=true - -HEALTHCHECK CMD curl -f http://localhost:8000/health || exit 1 - -CMD ["uvicorn", "envs.disease_control_env.server.app:app", "--host", "0.0.0.0", "--port", "8000"] - diff --git a/src/envs/disease_control_env/server/__init__.py b/src/envs/disease_control_env/server/__init__.py deleted file mode 100644 index 1ab488dd..00000000 --- a/src/envs/disease_control_env/server/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .disease_control_environment import DiseaseControlEnvironment - -__all__ = ["DiseaseControlEnvironment"] diff --git a/src/envs/disease_control_env/server/app.py b/src/envs/disease_control_env/server/app.py deleted file mode 100644 index 35ce0972..00000000 --- a/src/envs/disease_control_env/server/app.py +++ /dev/null @@ -1,167 +0,0 @@ -# src/envs/disease_control_env/server/app.py -import os -from fastapi import FastAPI, Query -from fastapi.responses import HTMLResponse, JSONResponse -from core.env_server import create_fastapi_app, create_web_interface_app -from ..models import DiseaseAction, DiseaseObservation -from .disease_control_environment import DiseaseControlEnvironment - -env = DiseaseControlEnvironment() -app: FastAPI = create_fastapi_app(env, DiseaseAction, DiseaseObservation) - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) - -@app.get("/grid") -def grid(): - return env.get_grid() - -# ---- JSON feeds used by the mini dashboard ---- -@app.get("/timeseries", response_class=JSONResponse) -def timeseries(tail: int | None = Query(default=400, ge=1)): - return env.get_timeseries(tail=tail) - -@app.get("/events", response_class=JSONResponse) -def events(): - return {"events": env.events} - -# ---- Minimal web UI (no external build, uses Chart.js CDN) ---- -ENABLE_WEB = os.getenv("ENABLE_WEB_INTERFACE", "true").lower() == "true" - -if ENABLE_WEB: - @app.get("/web", response_class=HTMLResponse) - def web_ui(): - return HTML_TEMPLATE - -HTML_TEMPLATE = """ - - - - - Disease Control · Live - - - - -

Disease Control

-
-
-

Person Grid (32×32)

- -
- Legend: - S - H - I - Q - D -
-
-

Epidemic Curves (S,H,I,Q,D)

-

Budget

-
-
-

Events

-
-
- - - - -""" diff --git a/src/envs/disease_control_env/server/disease_control_environment.py b/src/envs/disease_control_env/server/disease_control_environment.py deleted file mode 100644 index e0c47d04..00000000 --- a/src/envs/disease_control_env/server/disease_control_environment.py +++ /dev/null @@ -1,385 +0,0 @@ -# src/envs/disease_control_env/server/disease_control_environment.py -import numpy as np, uuid, random -from core.env_server import Environment -from ..models import DiseaseAction, DiseaseObservation, DiseaseState -from ..profiles import DISEASE_PROFILES, MUTATION_VARIANTS -import numpy as np, uuid, random -from enum import IntEnum -from typing import Any, Dict, List -class P(IntEnum): - S = 0 # susceptible - H = 1 # protected (vaccinated/recovered) - I = 2 # infected - Q = 3 # quarantined - D = 4 # deceased - -# --- Typing helpers to satisfy Pylance and ensure numeric operations --- -from typing import TypedDict, cast - -class _DiseaseProfile(TypedDict): - mu0: float - sigma: float - delta: float - nu: float - phi: float - -class _VariantParams(TypedDict): - name: str - mu0: float - sigma: float - delta: float - nu: float - phi: float - -class DiseaseControlEnvironment(Environment): - """ - Adds: - - history buffers for live charts (/timeseries) - - resource refills (scheduled + noisy) - - mutation events (single or multi-wave) - """ - def __init__( - self, - T: int = 90, - dt: float = 0.01, - disease: str = "covid", - init_budget: float = 1000.0, - refill_every_steps: int = 14, - refill_amount: float = 200.0, - refill_jitter: float = 0.25, - mutation_at_steps: tuple[int, ...] = (30,), - mutation_prob: float = 0.85, - grid_enabled: bool = True, # 👈 enable the person grid - grid_size: int = 32, # 👈 32x32 - neighborhood: str = "moore", - ): - super().__init__() - self.T, self.dt, self.substeps = T, dt, int(1/dt) - self.init_budget = init_budget - self._rng = np.random.default_rng() - - # event config - self.refill_every_steps = refill_every_steps - self.refill_amount = refill_amount - self.refill_jitter = refill_jitter - self.mutation_at_steps = set(mutation_at_steps) - self.mutation_prob = mutation_prob - self.set_disease_profile(disease) - # core params (set by preset) - self.set_disease_profile(disease) - self.grid_enabled = grid_enabled - self.N = grid_size - self.neighborhood = neighborhood - # histories for live chart - self._clear_history() - - self.reset() - - # -------------------- presets & mutation -------------------- - def set_disease_profile(self, name: str): - cfg = cast(_DiseaseProfile, DISEASE_PROFILES[name]) - # Ensure attributes are numeric for downstream arithmetic - self.mu0 = float(cfg["mu0"]) # type: ignore[assignment] - self.sigma = float(cfg["sigma"]) # type: ignore[assignment] - self.delta = float(cfg["delta"]) # type: ignore[assignment] - self.nu = float(cfg["nu"]) # type: ignore[assignment] - self.phi = float(cfg["phi"]) # type: ignore[assignment] - self.profile_name = name - - def _apply_mutation_variant(self): - variant = cast(_VariantParams, random.choice(MUTATION_VARIANTS)) - # Coerce to float to avoid str|float unions from loose dict typing - self.mu0 *= float(variant["mu0"]) # type: ignore[operator] - self.sigma *= float(variant["sigma"]) # type: ignore[operator] - self.delta *= float(variant["delta"]) # type: ignore[operator] - self.nu *= float(variant["nu"]) # type: ignore[operator] - self.phi *= float(variant["phi"]) # type: ignore[operator] - return f"mutation:{variant['name']}" - - # -------------------- env lifecycle -------------------- - def _clear_history(self): - self.hist_step = [] - self.hist_S = []; self.hist_H = []; self.hist_I = []; self.hist_Q = []; self.hist_D = [] - self.hist_budget = [] - self.events = [] # list[str] - - def reset(self) -> DiseaseObservation: - if self.grid_enabled: - self.grid = self._init_grid() - self._sync_macro_from_grid() - else: - I0 = self._rng.integers(20, 100) / 100000.0 - self.S, self.H, self.I, self.Q, self.D = 1 - I0, 0.0, I0, 0.0, 0.0 - - I0 = self._rng.integers(20, 100) / 100000.0 - self.S, self.H, self.I, self.Q, self.D = 1 - I0, 0.0, I0, 0.0, 0.0 - self.budget = self.init_budget - self._state = DiseaseState( - episode_id=str(uuid.uuid4()), - step_count=0, episode_return=0.0, budget=self.budget, disease=self.profile_name - ) - self._clear_history() - self._push_hist(last_event="reset") - return self._obs(last_event="reset") - - @property - def state(self) -> DiseaseState: - return self._state - - # -------------------- helpers -------------------- - def _noise(self) -> float: - return 1e-4 * np.sqrt(self.dt) * self._rng.normal() - - def _push_hist(self, last_event: str | None): - s = self._state.step_count - self.hist_step.append(s) - self.hist_S.append(float(self.S)); self.hist_H.append(float(self.H)) - self.hist_I.append(float(self.I)); self.hist_Q.append(float(self.Q)); self.hist_D.append(float(self.D)) - self.hist_budget.append(float(self.budget)) - if last_event: - self.events.append(f"t={s}:{last_event}") - - def _maybe_refill(self) -> str | None: - if self.refill_every_steps <= 0: return None - if self._state.step_count > 0 and self._state.step_count % self.refill_every_steps == 0: - jitter = (1.0 + self._rng.uniform(-self.refill_jitter, self.refill_jitter)) - add = max(0.0, self.refill_amount * jitter) - self.budget += add - return f"refill:+{add:.1f}" - return None - - def _maybe_mutate(self) -> str | None: - if self._state.step_count in self.mutation_at_steps: - if random.random() <= self.mutation_prob: - tag = self._apply_mutation_variant() - return tag - return None - - # -------------------- step -------------------- - def step(self, action: DiseaseAction): - self._state.step_count += 1 - - # Continuous controls clamped - c = float(np.clip(action.closures, 0.0, 1.0)) - v = float(np.clip(action.vaccination, 0.0, 1.0)) - q = float(np.clip(action.quarantine, 0.0, 1.0)) - s = float(np.clip(action.spending, 0.0, 1.0)) - - # Spending → intensities (budget constrained) - closures_cost = 200.0 * c * s - vaccine_cost = 400.0 * v * s - quarantine_cost = 600.0 * q * s - total_cost = closures_cost + vaccine_cost + quarantine_cost - - if total_cost > self.budget: - scale = self.budget / (total_cost + 1e-8) - c *= scale; v *= scale; q *= scale - total_cost = self.budget - - self.budget -= total_cost - - # Effective parameters under controls - mu = self.mu0 / (1.0 + 3.0 * c) - beta = 0.002 * v - rho = 0.020 * q - if self.grid_enabled: - # Update micro grid first (one sweep per day) - self._step_grid(mu=mu, beta=beta, rho=rho) - # sync macro fractions from grid; compute reward on the delta of macro aggregates - S_prev, H_prev, I_prev, Q_prev, D_prev = self.S, self.H, self.I, self.Q, self.D - self._sync_macro_from_grid() - new_inf = max(self.I - I_prev, 0.0) - new_deaths = max(self.D - D_prev, 0.0) - else: - # existing macro SDE integration (your current loop) - new_inf = 0.0 - new_deaths = 0.0 - for _ in range(self.substeps): - ... - new_inf += max(self.I - I_prev, 0.0) - new_deaths += max(self.D - D_prev, 0.0) - - econ_term = -(total_cost * 0.01) - reward = -(10*new_deaths + new_inf) + econ_term - new_inf = 0.0; new_deaths = 0.0 - for _ in range(self.substeps): - dS = (self._noise() - self.sigma*self.S*self.I*mu - beta*self.S) - dH = (beta*self.S + self.phi*(self.I+self.Q) - self.delta*self.H*self.I*mu) - dI = (self.sigma*self.S*self.I*mu + self.delta*self.H*self.I*mu - - (self.nu+self.phi+rho)*self.I) - dQ = (rho*self.I - (self.nu+self.phi)*self.Q) - dD = (self.nu*(self.I+self.Q)) - - I_prev, D_prev = self.I, self.D - - self.S = max(self.S + dS*self.dt, 0.0) - self.H = max(self.H + dH*self.dt, 0.0) - self.I = max(self.I + dI*self.dt, 0.0) - self.Q = max(self.Q + dQ*self.dt, 0.0) - self.D = max(self.D + dD*self.dt, 0.0) - - new_inf += max(self.I - I_prev, 0.0) - new_deaths += max(self.D - D_prev, 0.0) - - econ_term = -(total_cost * 0.01) # cost hurts reward - reward = -(10.0*new_deaths + new_inf) + econ_term - - # Events (refill, mutation) AFTER dynamics this step - event = None - ev1 = self._maybe_refill() - if ev1: event = ev1 - ev2 = self._maybe_mutate() - if ev2: event = ev2 if event is None else f"{event}|{ev2}" - - self._state.episode_return += reward - self._state.budget = self.budget - - done = ( - self._state.step_count >= self.T - or self.I < 1e-6 - or self.D >= 0.1 - or self.budget <= 0.0 - ) - - self._push_hist(last_event=event) - return self._obs(last_event=event), reward, done - - # -------------------- observation -------------------- - def _obs(self, last_event: str | None = None) -> DiseaseObservation: - return DiseaseObservation( - S=float(self.S), H=float(self.H), I=float(self.I), Q=float(self.Q), D=float(self.D), - step=self._state.step_count, budget=float(self.budget), disease=self.profile_name, - last_event=last_event, - mu0=float(self.mu0), sigma=float(self.sigma), delta=float(self.delta), - nu=float(self.nu), phi=float(self.phi), - ) - - # -------------------- endpoints used by web UI -------------------- - # These helpers will be called by FastAPI routes defined in app.py - def get_timeseries(self, tail: int | None = None) -> dict: - if tail: - sl = slice(-tail, None) - else: - sl = slice(None) - return { - "step": self.hist_step[sl], - "S": self.hist_S[sl], - "H": self.hist_H[sl], - "I": self.hist_I[sl], - "Q": self.hist_Q[sl], - "D": self.hist_D[sl], - "budget": self.hist_budget[sl], - "events": self.events[sl] if hasattr(self.events, "__getitem__") else self.events, - } - - def _init_grid(self): - # Start mostly S with a few I sprinkled in - g = np.full((self.N, self.N), P.S, dtype=np.uint8) - seeds = self._rng.integers(3, 10) - xs = self._rng.integers(0, self.N, size=seeds) - ys = self._rng.integers(0, self.N, size=seeds) - g[xs, ys] = P.I - return g - - def _neighbors(self, x, y): - if self.neighborhood == "von_neumann": - coords = [(x-1,y),(x+1,y),(x,y-1),(x,y+1)] - else: # moore - coords = [(x+i, y+j) for i in (-1,0,1) for j in (-1,0,1) if not (i==0 and j==0)] - for (i,j) in coords: - if 0 <= i < self.N and 0 <= j < self.N: - yield (i,j) - - - def _step_grid(self, mu, beta, rho): - - """ - One macro 'day' worth of micro transitions (single sweep). - Probabilities derived from macro params & actions. - """ - g = self.grid - new_g = g.copy() - - # map macro parameters to per-contact probabilities - # closures (mu↓) reduces effective contacts; beta is S->H vaccination rate (global) - # base infection chance per infectious neighbor: - base_inf = min(0.4, float(self.sigma * mu * 0.08)) # bounded for stability - rec_I = min(0.35, float(self.phi * 0.5)) # I -> H - die_I = min(0.20, float(self.nu * 8)) # I -> D - die_Q = min(0.20, float(self.nu * 8)) # Q -> D - rec_Q = min(0.50, float(self.phi * 1.2)) # Q -> H - go_Q = min(0.60, float(rho * 1.0)) # I -> Q - - # vaccinate a random subset of S globally based on beta - if beta > 0: - vac_mask = (g == P.S) & (self._rng.random((self.N, self.N)) < min(0.25, beta * 20.0)) - new_g[vac_mask] = P.H - - # loop cells (vectorization is possible; keep simple & clear) - for x in range(self.N): - for y in range(self.N): - s = g[x, y] - if s == P.S: - # infection from any infected neighbor - inf_p = 1.0 - infected_neighbors = 0 - for (i, j) in self._neighbors(x, y): - if g[i, j] == P.I: - infected_neighbors += 1 - inf_p *= (1.0 - base_inf) - # probability at least one successful infection - p_any = 1.0 - inf_p - if self._rng.random() < p_any: - new_g[x, y] = P.I - - elif s == P.I: - # die? - r = self._rng.random() - if r < die_I: - new_g[x, y] = P.D - else: - # quarantine? - if self._rng.random() < go_Q: - new_g[x, y] = P.Q - else: - # recover? - if self._rng.random() < rec_I: - new_g[x, y] = P.H - - elif s == P.Q: - # die or recover - if self._rng.random() < die_Q: - new_g[x, y] = P.D - elif self._rng.random() < rec_Q: - new_g[x, y] = P.H - # H, D remain as-is - - self.grid = new_g - - - def _sync_macro_from_grid(self): - # Convert grid counts -> macro fractions (S,H,I,Q,D) - tot = float(self.N * self.N) - counts = np.bincount(self.grid.ravel(), minlength=5) - self.S = counts[P.S] / tot - self.H = counts[P.H] / tot - self.I = counts[P.I] / tot - self.Q = counts[P.Q] / tot - self.D = counts[P.D] / tot - - - def get_grid(self) -> Dict[str, Any]: - if not getattr(self, "grid_enabled", False): - return { - "enabled": False, - "size": 0, - "grid": [], - "palette": {}, - } - - return { - "enabled": True, - "size": self.N, - "grid": self.grid.tolist(), - "palette": { "S":0, "H":1, "I":2, "Q":3, "D":4 }, - } \ No newline at end of file From f5ba925bdb3f2946358017ad27a58c9169020ca8 Mon Sep 17 00:00:00 2001 From: Ram Harikrishnan Date: Sat, 1 Nov 2025 19:30:13 -0700 Subject: [PATCH 08/19] removed unwanted files --- src/test.ipynb | 223 ------------------------------------------------- 1 file changed, 223 deletions(-) delete mode 100644 src/test.ipynb diff --git a/src/test.ipynb b/src/test.ipynb deleted file mode 100644 index f865edd0..00000000 --- a/src/test.ipynb +++ /dev/null @@ -1,223 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 2, - "id": "77f8126d-3f9b-4a09-ba26-50c66b6e3450", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Collecting fastapi\n", - " Using cached fastapi-0.120.1-py3-none-any.whl.metadata (28 kB)\n", - "Collecting starlette<0.50.0,>=0.40.0 (from fastapi)\n", - " Using cached starlette-0.48.0-py3-none-any.whl.metadata (6.3 kB)\n", - "Requirement already satisfied: pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4 in /opt/anaconda3/lib/python3.13/site-packages (from fastapi) (2.10.3)\n", - "Requirement already satisfied: typing-extensions>=4.8.0 in /opt/anaconda3/lib/python3.13/site-packages (from fastapi) (4.12.2)\n", - "Collecting annotated-doc>=0.0.2 (from fastapi)\n", - " Using cached annotated_doc-0.0.3-py3-none-any.whl.metadata (6.6 kB)\n", - "Requirement already satisfied: annotated-types>=0.6.0 in /opt/anaconda3/lib/python3.13/site-packages (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi) (0.6.0)\n", - "Requirement already satisfied: pydantic-core==2.27.1 in /opt/anaconda3/lib/python3.13/site-packages (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi) (2.27.1)\n", - "Requirement already satisfied: anyio<5,>=3.6.2 in /opt/anaconda3/lib/python3.13/site-packages (from starlette<0.50.0,>=0.40.0->fastapi) (4.7.0)\n", - "Requirement already satisfied: idna>=2.8 in /opt/anaconda3/lib/python3.13/site-packages (from anyio<5,>=3.6.2->starlette<0.50.0,>=0.40.0->fastapi) (3.7)\n", - "Requirement already satisfied: sniffio>=1.1 in /opt/anaconda3/lib/python3.13/site-packages (from anyio<5,>=3.6.2->starlette<0.50.0,>=0.40.0->fastapi) (1.3.0)\n", - "Using cached fastapi-0.120.1-py3-none-any.whl (108 kB)\n", - "Using cached starlette-0.48.0-py3-none-any.whl (73 kB)\n", - "Using cached annotated_doc-0.0.3-py3-none-any.whl (5.5 kB)\n", - "Installing collected packages: annotated-doc, starlette, fastapi\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m3/3\u001b[0m [fastapi]\n", - "Successfully installed annotated-doc-0.0.3 fastapi-0.120.1 starlette-0.48.0\n" - ] - } - ], - "source": [ - "!pip install fastapi\n" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "a7146315", - "metadata": { - "vscode": { - "languageId": "plaintext" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "🔥 Fires: 2\n", - "💧 Water: 8\n", - "Step 0: reward=-0.46, done=False\n", - "Step 1: reward=-1.06, done=False\n", - "Step 2: reward=-1.16, done=False\n" - ] - } - ], - "source": [ - "from envs.wildfire_env import WildfireEnv, WildfireAction\n", - "\n", - "env = WildfireEnv(base_url=\"http://localhost:8020\")\n", - "result = env.reset()\n", - "print(\"🔥 Fires:\", result.observation.burning_count)\n", - "print(\"💧 Water:\", result.observation.remaining_water)\n", - "\n", - "for t in range(3):\n", - " action = WildfireAction(action=\"wait\")\n", - " result = env.step(action)\n", - " print(f\"Step {t}: reward={result.reward:.2f}, done={result.done}\")\n", - "env.close()\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "2451db4d", - "metadata": { - "vscode": { - "languageId": "plaintext" - } - }, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAZQAAAG5CAYAAACp98JIAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAH0tJREFUeJzt3Xl0VOX9x/HPBEIIAQZBIIkgYUlAFGQTIVZDsASqUqCWKGIQPSggYvQI4saiVUAES1WsopJQV9QC5VhlqSSAGASJAS2LSLGIpqQIpRC2LM/vD5r7y5CFBL4QwffrnJzTublz58nF5p27zDw+55wTAACnKaiqBwAAOD8QFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBRUyrfffiufz1fVwzgpn8+nhQsXVvUwTonP59O3335b1cMwl5qaqnr16lX1MHAGERQjOTk5Gj58uC6++GKFhIQoPDxcvXv3VkZGhrdOVf2SmzJlinw+n+67776A5fPnz1fv3r114YUXyufzKSsry+T1Jk2aJJ/P5335/X5dffXVWrFihcn2KyI7O1u/+tWvztrrlWblypXq27evIiMjTf/tU1NTA/Zv7dq11blzZ82fP99k+z9VU6ZM0RVXXKE6deqoUaNG6t+/v7Zu3Vrm+sOHD5fP59PMmTMDlm/fvl0DBgxQw4YNVbduXSUmJmr37t1nePQ/DwTFyI033qgNGzZo7ty5+vrrr7Vo0SL16NFDe/furdJxrVu3TrNnz1b79u1LfC83N1dXXXWVpk6dav66l156qbKzs5Wdna2MjAxFR0frhhtu0P79+095m8455efnV2jd8PBwhYSEnPJrWcjNzdXll1+uF154wXzbdevW9fbvF198od69eysxMbHcX7AVkZeXZzRCeytWrNCoUaO0Zs0aLVu2TPn5+UpISFBubm6JdRcuXKjPPvtMkZGRActzc3OVkJAgn8+n5cuXa/Xq1Tp27Jj69u2rwsLCs/WjnL8cTtu+ffucJJeenl7mOs2aNXOSvK9mzZp531u0aJHr1KmTCwkJcc2bN3eTJk1yeXl53vcluRdffNH16dPH1axZ00VFRbl33333pOM6cOCAi46OdsuWLXNxcXEuOTm51PV27NjhJLkvvvjipNssWrc8EydOdJdffnnAsp07dzpJbu3atWW+ZtF+TEtLc845l5aW5iS5xYsXu86dO7vg4GC3fPlyFxcX50aPHu3Gjh3rLrjgAte4cWM3ceLEgNeT5BYsWBDwWn/+859djx49XGhoqGvfvr379NNPA54ze/Zs16RJExcaGur69+/vZsyY4fx+/0n3SUUUH09F1t2xY0eZ309JSSkxroKCAhccHBzw30Vpr+n3+11KSopz7v/3y7x581xcXJwLCQlxc+bMcbfddpvr16+fe+aZZ1x4eLirX7++u/vuu92xY8e87Rw9etSNHTvWRUZGulq1armuXbt6/27Fx9m0aVNvf06fPt1sfzrnXE5OjpPkVqxYEbB8165d7qKLLnJfffWVa9asmfv973/vfW/JkiUuKCjI7d+/31u2d+9eJ8ktW7bMbGw/VxyhGKhdu7Zq166thQsX6ujRo6Wus27dOklSSkqKsrOzvcdLlizRrbfeqnvvvVebNm3Syy+/rNTUVD311FMBzx8/frx3FHTrrbdq0KBB2rx5c7njGjVqlK6//nr98pe/NPgpT93Ro0e98+etW7eu9PMffPBBTZkyRZs3b/aOtObOnauwsDB99tlnmjZtmp544gktW7as3O08+uijGjNmjLKyshQTE6NBgwZ5RzyrV6/WiBEjlJycrKysLPXq1avEv8GqVau8f+uyviZPnlzpn+90FRQUaO7cuZKkTp06Vfr548aN07333qvNmzerd+/ekqS0tDRt375daWlpmjt3rlJTU5Wamuo95/bbb9fq1av1zjvvaOPGjRo4cKD69Omjbdu2SZI+++wz3XHHHbr77ruVlZWl+Ph4PfnkkwGve7r7s+hot379+t6ywsJCJSUlaezYsbr00ktLPOfo0aPy+XwBR681a9ZUUFCQPvnkk0rvO5ygqot2vnj//ffdBRdc4GrWrOliY2Pdww8/7DZs2BCwjkr5i/Hqq692kydPDlj2+uuvu4iIiIDnjRgxImCdK6+80o0cObLM8bz99tvusssuc4cPH3bOubN+hBIUFOTCwsJcWFiY8/l8rm7duu6jjz4q9zXLOkJZuHBhwPbj4uLcL37xi4BlV1xxhRs3bpz3WKUcobz66qve9//+9787SW7z5s3OOeduuukmd/311wdsc/DgwQF/UR86dMht27at3K8ff/yx1H1S2r99WVSBIxRJ3v4NCgpyISEh3pFHea9Z2hHKzJkzA9a57bbbXLNmzVx+fr63bODAge6mm25yzjn3zTffOJ/P577//vuA51177bXu4Ycfds45N2jQINenT5+A7990001m+7OwsND17du3xH8HkydPdr169XKFhYXOOVfiCCUnJ8fVrVvXJScnu9zcXHfw4EE3atQoJ8ndddddpb4WKq762U/Y+enGG2/U9ddfr1WrVikjI0OLFy/WtGnT9Oqrr2ro0KFlPm/9+vVat25dwF/DBQUFOnLkiA4dOqRatWpJkrp37x7wvO7du5d5Ef27775TcnKyli5dqpo1a572z3YqWrdurUWLFkmSDhw4oHnz5mngwIFKS0tTly5dKrWt0tY/8ZpQRESEcnJyyt1O8edERERIOn4zRZs2bbR161YNGDAgYP2uXbvqgw8+8B6HhoaqVatWlRr7mVKnTh1lZmZKkg4dOqS//e1vGj58uBo0aKC+fftWalul7d9LL71U1apV8x5HREToyy+/lCRlZmbKOaeYmJiA5xw9elQNGjSQJG3evLnE/uzevbsWL17sPT6d/XnPPfdo48aNAUcV69ev1x/+8AdlZmaWeSdiw4YN9d5772nkyJF67rnnFBQUpEGDBqlTp04BPy9ODUExVLNmTfXq1Uu9evXShAkTNGzYME2cOLHcoBQWFurxxx/Xb37zm1K3V56y/k+zfv165eTkqHPnzt6ygoICrVy5Ui+88IKOHj16xv/PU6NGjYBfFh07dtTChQs1c+ZMvfHGGwoKOn621RWb362sC8JhYWEllgUHBwc89vl8J72oWvw5Rfuu6DnOuRL7s/jYpOOnaE5259gjjzyiRx55pNx1LAQFBQXs3/bt22vp0qV6+umnvaD4fL4SP0Np+7iy+7ewsFDVqlXT+vXrS/x3VLt2bUkl911pTnV/jh49WosWLdLKlSvVpEmTgO3l5OTo4osv9pYVFBTogQce0MyZM71bsRMSErR9+3bt2bNH1atXV7169RQeHq7mzZufdMwoH0E5g9q2bRtwq2hwcLAKCgoC1unUqZO2bt160r/U1qxZoyFDhgQ87tixY6nrXnvttd5fk0Vuv/12tWnTRuPGjauyv8SqVaumw4cPSzr+l6J0/Pbeop/D6rblU9GmTRutXbs2YNnnn38e8LhLly4nHWPx8/lnW/H9Kx3fx9nZ2d7jbdu26dChQ6f9Oh07dlRBQYFycnJ09dVXl7pO27ZttWbNmoBlJz6u7P50zmn06NFasGCB0tPTSwQgKSmpxPXC3r17KykpSbfffnuJbV944YWSpOXLlysnJ0e//vWvyx0LTo6gGPjxxx81cOBA3XHHHWrfvr3q1Kmjzz//XNOmTVO/fv289aKiovTxxx/rqquuUkhIiC644AJNmDBBN9xwg5o2baqBAwcqKChIGzdu1JdffhlwEfO9995Tly5d9Itf/EJvvvmm1q5dq9dee63U8dSpU0eXXXZZwLKwsDA1aNAgYPnevXu1c+dO/fDDD5Lk3XIaHh6u8PDw09on+fn5+te//iXp/095bdq0SePGjZN0/HRHt27dNHXqVEVFRWnPnj167LHHTus1T8fo0aN1zTXX6Nlnn1Xfvn21fPlyffTRRwFHLZU9RXPw4EF988033uMdO3YoKytL9evXD/gr+lQ457z9e/jwYS1btkxLlizRhAkTvHV69uypF154Qd26dVNhYaHGjRtX4sjjVMTExGjw4MEaMmSIZsyYoY4dO2rPnj1avny52rVrp+uuu0733nuvYmNjNW3aNPXv319Lly4NON0lVX5/jho1Sm+99Zb+8pe/qE6dOt7P7/f7FRoaqgYNGnin3IoEBwcrPDw84GaQlJQUXXLJJWrYsKEyMjKUnJys+++//5RuGMEJqu7yzfnjyJEj7qGHHnKdOnVyfr/f1apVy7Vu3do99thj7tChQ956ixYtcq1atXLVq1cPuG148eLFLjY21oWGhrq6deu6rl27utmzZ3vfl+RmzZrlevXq5UJCQlyzZs3c22+/XakxlnZRvuji7olfJ96CW1xFL8oX316tWrVcu3bt3B//+MeA9TZt2uS6devmQkNDXYcOHdzSpUtLvSi/b9++k/4s/fr1c7fddpv3WKVclC/vBgDnjt82fNFFF3m3uT755JMuPDy83J+1PEXjP/Gr+DhLowpelC/6CgkJcTExMe6pp54KuJD+/fffu4SEBBcWFuaio6Pdhx9+WOpF+RNvxii6bbi45ORkFxcX5z0+duyYmzBhgouKinLBwcEuPDzcDRgwwG3cuNFb57XXXvNuw+7bt+9p3zZc2r6UVOJmhOJOvCjvnHPjxo1zjRs3dsHBwS46OtrNmDHDu4iP0+NzrgInO1GlfD6fFixYoP79+1f1UPTtt9+qefPmFTpHfq678847tWXLFq1ateqsvq7P59OOHTsUFRV1Vl8XOF2c8gL+Z/r06erVq5fCwsL00Ucfae7cuXrxxRereljAOYOgAP+zdu1aTZs2TQcOHFCLFi303HPPadiwYVU9LOCcQVDOAT+l00v16tXTxIkTq3oYZ8S7775b1UOQJE2cOJFP5cU5iWsoAAATfJYXAMAEQfkZS09Pl8/n03/+85/T2s7QoUN/EnegpaamqkePHlU9jHJVZJ+fOBHVpEmT1KFDhzM+NuB0EZTzwEsvvaQ6deoEzBVy8OBBBQcHl3gn86pVq+Tz+fT1118rNjZW2dnZ8vv9Z3vInptvvrnEx28UvaFw/PjxAct/97vflZjforJ69OhRYqIx6ezNJngq+3zMmDH6+OOPvcdnK+D79u1TUlKS/H6//H6/kpKSyg1hXl6exo0bp3bt2iksLEyRkZEaMmSI98bZIsOHD1fLli0VGhqqhg0bql+/ftqyZcsZ/mlwNhCU80B8fLwOHjwY8FEhq1atUnh4uNatWxfwcRvp6emKjIxUTEyMatSoofDw8Cqd0jc+Pl6ffPJJQAzT09PVtGlTpaWlBaybnp6u+Pj4sz1EU6eyz2vXrl3iHeBnwy233KKsrCwtXrxYixcvVlZWlpKSkspc/9ChQ8rMzNT48eOVmZmp+fPn6+uvvy7xkSadO3dWSkqKNm/erCVLlsg5p4SEhBIfS4RzUBW+qRKGIiMj3ZQpU7zHDz74oBs1apRr27ZtwMRBPXv2dIMHD3bOlXwnetHETYsXL3Zt2rRxYWFhrnfv3u6HH37wnp+fn+/uv/9+5/f7Xf369d3YsWPdkCFDSryzuqK2bt3qJLmMjAxvWdeuXd2sWbNcjRo1XG5urnPu+IROoaGh7pVXXilzWykpKQHv5i5NWR/jf+KkVRV5t3hcXJy75557XHJysqtXr55r1KiRe/nll93Bgwfd0KFDXe3atV2LFi3chx9+6D2ntHf/n2wiquITlp34KQT637v94+Pj3ahRowLGu2fPHlejRg338ccfl7tPSrNp0yYnya1Zs8ZblpGR4SS5LVu2VHg7a9eudZLcP//5zzLX2bBhg5Pkvvnmm0qPEz8tHKGcJ3r06BHwF31aWpp69OihuLg4b/mxY8eUkZFR7l/5hw4d0vTp0/X6669r5cqV2rlzp8aMGeN9f8aMGZozZ45ee+01ffLJJ9q7d68WLFgQsI3JkyefdOKkonefx8TEKDIy0hvjgQMHlJmZqYEDB6ply5ZavXq1pOMfLHj48OGf3BHK3LlzdeGFF2rt2rUaPXq0Ro4cqYEDByo2NlaZmZnehxOW9aGMFZmIqrgxY8YoMTFRffr08aYAjo2N1bBhw/TWW28FTPD25ptvKjIy0ttnI0aMOOm/y86dOyVJGRkZ8vv9uvLKK73tdevWTX6/X59++mmF98/+/fvl8/nKPJ2Ym5urlJQUNW/eXE2bNq3wdvETVdVFg43Zs2e7sLAwl5eX5/773/+66tWru927d7t33nnHxcbGOuecW7FihZPktm/f7pwr/QhFJ/ylOGvWLNe4cWPvcUREhJs6dar3OC8vzzVp0iTgr/kff/zxpBMnFf+Ms1tuucUlJCQ455z761//6tq2beucc27EiBHukUcecc459/jjj7umTZuWuw8qeoQSHBzsTU5V9BUSEnJKRyjFJ3jKz893YWFhLikpyVuWnZ0dcAR24j6vyERUJ06pXNrYjhw54urXr+/mzZvnLevQoYObNGmS93j37t0n/Xcpmnr6qaeectHR0SX2X3R0dIkJ4cpy+PBh17lzZ++IuLhZs2a5sLAwJ8m1adOGo5PzBG9sPE/Ex8crNzdX69at0759+xQTE6NGjRopLi5OSUlJys3NVXp6ui6++GK1aNGizO3UqlVLLVu29B4Xn7hq//79ys7ODpjsq3r16urSpUvAmy/r169fqY9xj4+P13333ae8vDylp6d7d2rFxcXp+eefl3T8+knPnj0rvM3yDB48WI8++mjAsvnz55/S9L3FJ+2qVq2aGjRooHbt2nnLGjduLEllTv5VkYmoKiIkJES33nqr5syZo8TERGVlZWnDhg0B0yc0atRIjRo1qvA2S7vO40qZN6Y0eXl5uvnmm1VYWFjqx9cMHjxYvXr1UnZ2tqZPn67ExEStXr26yiaEgw1OeZ0nWrVqpSZNmigtLU1paWmKi4uTJG/ioNWrVystLe2kv5RLm1jJVfK9r5U55SUFxrD42OPi4rRu3Trt3bv3pKfqKsPv96tVq1YBXyf+og0KCqrQ5FSl7a/yJvI6UWX3bXmGDRumZcuWadeuXZozZ46uvfZaNWvWzPt+ZU55hYeHa/fu3SVe49///rcXybLk5eUpMTFRO3bs0LJly1S3bt0S6/j9fkVHR+uaa67R+++/ry1btpQ4dYpzD0co55H4+Hilp6dr3759Gjt2rLc8Li5OS5Ys0Zo1a0qdaKii/H6/IiIitGbNGl1zzTWSjs97sn79enXq1Mlbb8SIEUpMTCx3WxdddJH3v1u2bKmmTZtq0aJFysrK8oISERGhqKgozZgxQ0eOHDmr108aNmyor776KmBZVlaWyXwixVVkIqoT1ahRo9Q7otq1a6cuXbrolVde0VtvveUd3RV54oknAq6Hlabotuzu3btr//79Wrt2rbp27Srp+PWe/fv3KzY2tsznF8Vk27ZtSktLq/Ddac65gOs/ODcRlPNIfHy8Ro0apby8PO+XsnQ8KCNHjjT5pZycnKypU6cqOjpal1xyiZ599tkS702o7CmvorG/+OKLatWqVcBfwEWnvVq0aHHak1JVRs+ePfXMM8/oT3/6k7p376433nhDX331VZmzZJ6qikxEdaKoqCgtWbJEW7duVYMGDeT3+73QDRs2TPfcc49q1apV4lRaZU55XXLJJerTp4/uvPNOvfzyy5Kku+66SzfccEPARFRt2rTRlClTNGDAAOXn5+u3v/2tMjMz9cEHH6igoMCbBKt+/fqqUaOG/vGPf2jevHlKSEhQw4YN9f333+vpp59WaGiorrvuugrvN/w0ccrrPBIfH6/Dhw+X+kv5wIED3pHA6XjggQc0ZMgQDR06VN27d1edOnVK/OI6FfHx8Tpw4ECJd7oXjf1s393Vu3dvjR8/Xg8++KCuuOIKHThwIGAKZivdunXTq6++queff14dOnTQ0qVLTzpz5Z133qnWrVurS5cuatiwoXcnnCQNGjRI1atX1y233HLa1yPefPNNtWvXTgkJCUpISFD79u31+uuvB6yzdetW7d+/X5K0a9cuLVq0SLt27VKHDh0UERHhfRXdGVazZk2tWrVK1113nVq1aqXExESFhYXp008/rdT1Hfw08eGQOG+kpqYqNTVV6enpVT2UKvPdd98pKipK69atCzgNCZwNnPICzgN5eXnKzs7WQw89pG7duhETVAlOeQHngdWrV6tZs2Zav369XnrppaoeDn6mOOWF80ZWVpaysrI0dOjQqh4K8LNEUAAAJjjlBQAwQVAAACYICgDARIVvG67KSZgA4Jw3qaoHcHrcxJNfbucIBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYqF7VAwCAn4VJ5/j2K4AjFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBgwueccxVa0ec702MBAPxEVSQVHKEAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBgonpVDwAAfg7cGd6+7wxvvyI4QgEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJqpX9QAA4OfAV9UDOAs4QgEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJqpX9QAAAAYmVfUAOEIBABghKAAAEwQFAGCCoAAATBAUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACaqV/UAAJwlk87x7eMnjyMUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACACYICADBBUAAAJggKAMAEQQEAmCAoAAATBAUAYIKgAABMEBQAgAmCAgAwQVAAACYICgDABEEBAJggKAAAEwQFAGDC55xzVT0IAMC5jyMUAIAJggIAMEFQAAAmCAoAwARBAQCYICgAABMEBQBggqAAAEwQFACAif8DH63lEx27xO8AAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "🔥 Fire has fully burned out after 42 steps.\n", - "Animation complete.\n" - ] - } - ], - "source": [ - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "import time, sys\n", - "# FIX 1: Import 'display' alongside 'clear_output'\n", - "from IPython.display import clear_output, display \n", - "import matplotlib.colors as mcolors\n", - "sys.path.append(\"/workspace/OpenEnv/src\")\n", - "from envs.wildfire_env import WildfireEnv, WildfireAction # Ensure these imports work\n", - "\n", - "from envs.wildfire_env.server.wildfire_environment import WildfireEnvironment\n", - "\n", - "# Set up client (assuming server is running on 8010)\n", - "client = WildfireEnv(\"http://localhost:8020\")\n", - "\n", - "# --- Matplotlib Setup ---\n", - "cmap = mcolors.ListedColormap([\n", - " \"black\", # 0 = ash\n", - " \"green\", # 1 = fuel\n", - " \"red\", # 2 = burning\n", - " \"saddlebrown\", # 3 = firebreak\n", - " \"blue\" # 4 = water\n", - "])\n", - "# Normalize so integers map cleanly\n", - "norm = mcolors.BoundaryNorm([0, 1, 2, 3, 4, 5], cmap.N)\n", - "\n", - "# 1. Initialize Interactive Mode\n", - "plt.ion() \n", - "fig, ax = plt.subplots(figsize=(5, 5))\n", - "plt.axis(\"off\")\n", - "\n", - "# 2. Get Initial State and Initialize Image Object\n", - "res = client.reset()\n", - "obs = res.observation\n", - "grid = np.array(obs.grid).reshape(obs.height, obs.width)\n", - "\n", - "# Create the image object once\n", - "im = ax.imshow(grid, cmap=cmap, norm=norm)\n", - "\n", - "# Create the title object once\n", - "title_text = ax.set_title(\n", - " f\"Step {obs.step} | Burning={obs.burning_count} | Burned={obs.burned_count}\\n\"\n", - " f\"Wind={obs.wind_dir} | Humidity={obs.humidity:.2f}\",\n", - " color=\"black\", \n", - " fontsize=10\n", - ")\n", - "\n", - "# NOTE: The initial clear_output and display(fig) calls are removed here.\n", - "\n", - "print(\"Starting smooth animation...\")\n", - "\n", - "# --- Animation Loop (Updating In Place and Forcing Display) ---\n", - "for _ in range(100): \n", - " # 1. Clear previous output\n", - " clear_output(wait=True) \n", - " \n", - " # Get the new grid data\n", - " new_grid = np.array(obs.grid).reshape(obs.height, obs.width)\n", - "\n", - " # 2. Update the Image Data \n", - " im.set_data(new_grid)\n", - "\n", - " # 3. Update the Title Text\n", - " title_text.set_text(\n", - " f\"Step {obs.step} | Burning={obs.burning_count} | Burned={obs.burned_count}\\n\"\n", - " f\"Wind={obs.wind_dir} | Humidity={obs.humidity:.2f}\"\n", - " )\n", - "\n", - " # 4. Display the updated figure (FORCES redraw in non-interactive environments)\n", - " display(fig) \n", - " \n", - " # Control the speed (slightly increased for visibility)\n", - " time.sleep(0.3) \n", - "\n", - " # Advance the simulation\n", - " res = client.step(WildfireAction(action=\"WAIT\"))\n", - " obs = res.observation\n", - "\n", - " # Stop condition\n", - " if obs.burning_count == 0:\n", - " print(f\"🔥 Fire has fully burned out after {obs.step} steps.\")\n", - " break\n", - "\n", - "plt.ioff() # Turn off interactive mode\n", - "plt.close(fig) # Close the figure at the end\n", - "print(\"Animation complete.\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "68782dd4-97b3-483b-a096-1c1a750bf0e8", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python [conda env:base] *", - "language": "python", - "name": "conda-base-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.13.5" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 337c2d4d9179945c900f417f576c1445c3eb13ef Mon Sep 17 00:00:00 2001 From: Ram Harikrishnan Date: Sat, 1 Nov 2025 19:31:50 -0700 Subject: [PATCH 09/19] removed unwanted files --- src/.ipynb_checkpoints/test-checkpoint.ipynb | 48 -------------------- 1 file changed, 48 deletions(-) delete mode 100644 src/.ipynb_checkpoints/test-checkpoint.ipynb diff --git a/src/.ipynb_checkpoints/test-checkpoint.ipynb b/src/.ipynb_checkpoints/test-checkpoint.ipynb deleted file mode 100644 index 9f7dc52d..00000000 --- a/src/.ipynb_checkpoints/test-checkpoint.ipynb +++ /dev/null @@ -1,48 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "a7146315", - "metadata": { - "vscode": { - "languageId": "plaintext" - } - }, - "outputs": [], - "source": [ - "from envs.wildfire_env import WildfireEnv, WildfireAction\n", - "\n", - "env = WildfireEnv(base_url=\"http://localhost:8020\")\n", - "result = env.reset()\n", - "print(\"🔥 Fires:\", result.observation.burning_count)\n", - "print(\"💧 Water:\", result.observation.remaining_water)\n", - "\n", - "for t in range(3):\n", - " action = WildfireAction(action=\"wait\")\n", - " result = env.step(action)\n", - " print(f\"Step {t}: reward={result.reward:.2f}, done={result.done}\")\n", - "env.close()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2451db4d", - "metadata": { - "vscode": { - "languageId": "plaintext" - } - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "language_info": { - "name": "python" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From ba1b62a9a38fd88d6d0c2f76c1a568fe3539321d Mon Sep 17 00:00:00 2001 From: Ram Harikrishnan Date: Sat, 1 Nov 2025 20:43:02 -0700 Subject: [PATCH 10/19] added the environment to deployment files --- .github/workflows/deploy-hf-env.yml | 9 +++++---- scripts/prepare_hf_deployment.sh | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy-hf-env.yml b/.github/workflows/deploy-hf-env.yml index d84833df..98eae1c9 100644 --- a/.github/workflows/deploy-hf-env.yml +++ b/.github/workflows/deploy-hf-env.yml @@ -15,6 +15,7 @@ on: - 'chat_env' - 'atari_env' - 'openspiel_env' + - 'wildfire_env' custom_environment: description: 'Custom environment to deploy (leave empty for none)' required: false @@ -63,7 +64,7 @@ jobs: if [ "${{ github.event.inputs.environment }}" = "all" ]; then echo "deploy_all=true" >> $GITHUB_OUTPUT echo "use_matrix=true" >> $GITHUB_OUTPUT - echo "environments=echo_env,coding_env,chat_env,atari_env,openspiel_env" >> $GITHUB_OUTPUT + echo "environments=echo_env,coding_env,chat_env,atari_env,openspiel_env,wildfire_env" >> $GITHUB_OUTPUT echo "Manual trigger - deploying all environments with matrix" else echo "deploy_all=false" >> $GITHUB_OUTPUT @@ -78,14 +79,14 @@ jobs: if git diff --name-only HEAD~1 HEAD | grep -E '^src/core/' > /dev/null; then echo "deploy_all=true" >> $GITHUB_OUTPUT echo "use_matrix=true" >> $GITHUB_OUTPUT - echo "environments=echo_env,coding_env,chat_env,atari_env,openspiel_env" >> $GITHUB_OUTPUT + echo "environments=echo_env,coding_env,chat_env,atari_env,openspiel_env,wildfire_env" >> $GITHUB_OUTPUT echo "Core files changed - deploying all environments with matrix" exit 0 fi # Check which specific environments changed changed_envs=() - for env in echo_env coding_env chat_env atari_env openspiel_env; do + for env in echo_env coding_env chat_env atari_env openspiel_env wildfire_env; do if git diff --name-only HEAD~1 HEAD | grep -E "^src/envs/$env/" > /dev/null; then changed_envs+=("$env") fi @@ -110,7 +111,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - environment: [echo_env, coding_env, chat_env, atari_env, openspiel_env] + environment: [echo_env, coding_env, chat_env, atari_env, openspiel_env, wildfire_env] permissions: contents: read diff --git a/scripts/prepare_hf_deployment.sh b/scripts/prepare_hf_deployment.sh index 23fd4779..d39f3320 100755 --- a/scripts/prepare_hf_deployment.sh +++ b/scripts/prepare_hf_deployment.sh @@ -157,6 +157,7 @@ README_EOF "chat_env") ENV_CLASS="ChatEnv" ;; "atari_env") ENV_CLASS="AtariEnv" ;; "openspiel_env") ENV_CLASS="OpenSpielEnv" ;; + "wildfire_env") ENV_CLASS="WildfireEnv" ;; *) ENV_CLASS="Env" ;; esac From 46130130653ac0a3e7caf6f4c488f4751b479c44 Mon Sep 17 00:00:00 2001 From: "hash.ee.rama" Date: Sun, 2 Nov 2025 17:17:04 -0700 Subject: [PATCH 11/19] Update src/envs/wildfire_env/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/envs/wildfire_env/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/envs/wildfire_env/README.md b/src/envs/wildfire_env/README.md index d7c6c1c3..e9ebdc23 100644 --- a/src/envs/wildfire_env/README.md +++ b/src/envs/wildfire_env/README.md @@ -279,7 +279,7 @@ print("Animation complete.") ``` -=== +--- ## 🧪 Example Training Loop (GRPO/LLM) From fbe7a2b2e7d91fc018c9de3f9b0202b4ab99119a Mon Sep 17 00:00:00 2001 From: "hash.ee.rama" Date: Sun, 2 Nov 2025 17:17:17 -0700 Subject: [PATCH 12/19] Update src/envs/wildfire_env/server/wildfire_environment.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/envs/wildfire_env/server/wildfire_environment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/envs/wildfire_env/server/wildfire_environment.py b/src/envs/wildfire_env/server/wildfire_environment.py index a6f04f98..24c6fb81 100644 --- a/src/envs/wildfire_env/server/wildfire_environment.py +++ b/src/envs/wildfire_env/server/wildfire_environment.py @@ -289,9 +289,9 @@ def _spread_fire(self) -> int: ni = idx(nx, ny, self.w) target = st.grid[ni] - # Only fuel or damp can be candidates, but WATER IS IMMUNE during damp + # Only fuel or water/damp can be candidates, but cells with code 4 (watered/damp) are immune to ignition if target == 4: - # Damp cells do not ignite at all while damp + # Watered/damp cells (code 4) do not ignite at all while in this state continue if target != 1: continue From 1909e8e8dad641cb3b3ac38b3fc90f15b0785c0c Mon Sep 17 00:00:00 2001 From: "hash.ee.rama" Date: Sun, 2 Nov 2025 17:17:40 -0700 Subject: [PATCH 13/19] Update src/envs/wildfire_env/server/app.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/envs/wildfire_env/server/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/envs/wildfire_env/server/app.py b/src/envs/wildfire_env/server/app.py index bf9935f2..c3012acc 100644 --- a/src/envs/wildfire_env/server/app.py +++ b/src/envs/wildfire_env/server/app.py @@ -1,10 +1,10 @@ # server/app.py import os -from core.env_server import create_fastapi_app +from core.env_server import create_app from ..models import WildfireAction, WildfireObservation from .wildfire_environment import WildfireEnvironment W = int(os.getenv("WILDFIRE_WIDTH", "16")) H = int(os.getenv("WILDFIRE_HEIGHT", "16")) env = WildfireEnvironment(width=W, height=H) -app = create_fastapi_app(env, WildfireAction, WildfireObservation) +app = create_app(env, WildfireAction, WildfireObservation, env_name='wildfire_env') From 4001e2caac2d4f29e1c7322d6636acf8317a1b4d Mon Sep 17 00:00:00 2001 From: Ram Harikrishnan Date: Mon, 3 Nov 2025 21:44:35 -0700 Subject: [PATCH 14/19] fix: resolve TypeError and IndexError bugs in wildfire environment This commit addresses critical runtime errors and adds comprehensive documentation for the wildfire environment. Bug Fixes: - Fix TypeError in in_bounds() and idx() functions by adding defensive type conversion for all parameters (x, y, w, h) to ensure integers - Fix IndexError in _apply_water(), _apply_break(), _spread_fire(), and reset() by adding bounds checks before accessing grid arrays - Ensure self.w and self.h are explicitly cast to int during initialization - Add safety checks in _spread_fire() for ignite_flags array bounds - Use local w/h variables after type conversion to maintain consistency Infrastructure: - Add run_wildfire_docker.sh convenience script with automatic base image building and log streaming - Script builds openenv-base:latest if missing and rebuilds wildfire image to ensure latest code is included Documentation: - Complete rewrite of wildfire_env/README.md with comprehensive guide - Detailed action documentation with rewards and use cases - Grid format explanation with code examples - Fire spread mechanics and wind effects - Complete API reference with examples - Four practical code examples (containment, firebreak, visualization, RL) - Troubleshooting guide for common issues - Configuration options and environment variables - Web interface usage guide - Performance considerations All changes are backward compatible and improve environment stability and usability for both RL agents and developers. --- run_wildfire_docker.sh | 89 ++ src/envs/wildfire_env/README.md | 982 ++++++++++++++---- .../server/wildfire_environment.py | 73 +- 3 files changed, 952 insertions(+), 192 deletions(-) create mode 100755 run_wildfire_docker.sh diff --git a/run_wildfire_docker.sh b/run_wildfire_docker.sh new file mode 100755 index 00000000..0ce580f6 --- /dev/null +++ b/run_wildfire_docker.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# Script to run Wildfire Environment with Docker + +set -e + +WIDTH="${WILDFIRE_WIDTH:-32}" +HEIGHT="${WILDFIRE_HEIGHT:-32}" +HUMIDITY="${WILDFIRE_HUMIDITY:-0.25}" +PORT="${PORT:-8000}" +CONTAINER_NAME="wildfire-env-container" + +# Colors for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}" +echo -e "${BLUE} Wildfire Environment - Docker Runner${NC}" +echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}" +echo "" + +# Step 1: Stop and remove any existing wildfire containers +echo -e "${YELLOW}Step 1: Cleaning up any existing containers...${NC}" +docker stop $(docker ps -aq --filter "name=wildfire") 2>/dev/null || true +docker rm $(docker ps -aq --filter "name=wildfire") 2>/dev/null || true +echo -e "${GREEN}✓ Cleaned up existing containers${NC}" +echo "" + +# Step 2: Check if base image exists, if not build it +if ! docker images -q openenv-base:latest | grep -q .; then + echo -e "${YELLOW}Step 2a: Building base image (openenv-base:latest)...${NC}" + docker build -f src/core/containers/images/Dockerfile -t openenv-base:latest . > /dev/null + echo -e "${GREEN}✓ Base image built successfully${NC}" + echo "" +else + echo -e "${GREEN}✓ Base image exists${NC}" + echo "" +fi + +# Step 3: Rebuild wildfire image to ensure latest code changes are included +echo -e "${YELLOW}Step 2b: Building Wildfire Docker image...${NC}" +docker build -f src/envs/wildfire_env/server/Dockerfile -t wildfire-env:latest . > /dev/null +echo -e "${GREEN}✓ Wildfire image built successfully${NC}" +echo "" + +# Step 4: Start the container +echo -e "${BLUE}Step 4: Starting Wildfire Environment container...${NC}" +echo "" +echo "Configuration:" +echo " Grid Width: $WIDTH" +echo " Grid Height: $HEIGHT" +echo " Humidity: $HUMIDITY" +echo " Port: $PORT" +echo " Web Interface: Enabled" +echo "" + +docker run -d \ + --name $CONTAINER_NAME \ + -p $PORT:8000 \ + -e ENABLE_WEB_INTERFACE=true \ + -e WILDFIRE_WIDTH=$WIDTH \ + -e WILDFIRE_HEIGHT=$HEIGHT \ + -e WILDFIRE_HUMIDITY=$HUMIDITY \ + wildfire-env:latest > /dev/null + +echo -e "${GREEN}✓ Container started successfully!${NC}" +echo "" + +# Step 5: Wait a moment and check status +sleep 2 +echo -e "${BLUE}Container Information:${NC}" +echo " Name: $CONTAINER_NAME" +echo " Status: $(docker ps -f name=$CONTAINER_NAME --format '{{.Status}}')" +echo "" + +# Step 6: Display access information +echo -e "${GREEN}Web Interface: http://localhost:$PORT/web${NC}" +echo "" +echo "Available actions:" +echo " - water: Apply water to a cell (extinguishes fire)" +echo " - break: Create a firebreak (prevents fire spread)" +echo " - wait: Do nothing (fire continues spreading)" +echo "" +echo -e "${BLUE}Showing logs (press Ctrl+C to stop):${NC}" +echo "" + +# Step 7: Show logs +docker logs -f $CONTAINER_NAME diff --git a/src/envs/wildfire_env/README.md b/src/envs/wildfire_env/README.md index e9ebdc23..e6b3a81c 100644 --- a/src/envs/wildfire_env/README.md +++ b/src/envs/wildfire_env/README.md @@ -9,6 +9,27 @@ Agents must contain spreading fires using **water**, **firebreaks**, and **timin [![License](https://img.shields.io/badge/license-MIT-lightgrey)](LICENSE) --- + +## 📋 Table of Contents + +1. [Why Wildfire Simulation?](#-why-wildfire-simulation) +2. [Quick Start](#-quick-start) +3. [Environment Overview](#-environment-overview) +4. [Grid Format & Encoding](#-grid-format--encoding) +5. [Actions](#-actions) +6. [Observations](#-observations) +7. [Reward Structure](#-reward-structure) +8. [Fire Spread Mechanics](#-fire-spread-mechanics) +9. [Configuration](#-configuration) +10. [Installation & Usage](#-installation--usage) +11. [API Reference](#-api-reference) +12. [Examples](#-examples) +13. [Web Interface](#-web-interface) +14. [Troubleshooting](#-troubleshooting) +15. [References](#-references) + +--- + ## 🔥 Why Wildfire Simulation? Wildland fires are intensifying globally due to climate change — increasing the urgency for **AI-assisted decision-making**. @@ -17,7 +38,7 @@ This environment explores how intelligent systems can **control** fire spread in ### Research Motivation ✅ Based on real wildfire science inspired by: - **Rothermel Surface Fire Spread Model** (USDA Forest Service) -- **MITRE Fireline’s SimFire** — physics-informed RL fire simulator +- **MITRE Fireline's SimFire** — physics-informed RL fire simulator - **SimHarness** — RL evaluation for disaster response ### Application Goals @@ -32,157 +53,505 @@ This makes WildfireEnv a **fast, controllable**, and **open benchmark** for appl --- +## 🚀 Quick Start + +### Using Docker (Recommended) + +```bash +# From the OpenEnv root directory +./run_wildfire_docker.sh +``` + +Or manually: + +```bash +# Build base image (first time only) +docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile . + +# Build wildfire environment +docker build -t wildfire-env:latest -f src/envs/wildfire_env/server/Dockerfile . + +# Run container +docker run -p 8000:8000 wildfire-env:latest +``` + +### Basic Python Client + +```python +from envs.wildfire_env import WildfireEnv, WildfireAction + +# Connect to running server +env = WildfireEnv(base_url="http://localhost:8000") + +# Reset environment +result = env.reset() +obs = result.observation +print(f"Grid: {obs.width}x{obs.height}, Fires: {obs.burning_count}, Water: {obs.remaining_water}") + +# Take action (water a burning cell) +result = env.step(WildfireAction(action="water", x=10, y=15)) +print(f"Reward: {result.reward:.2f}, Burning: {result.observation.burning_count}") + +# Create firebreak +result = env.step(WildfireAction(action="break", x=12, y=15)) + +# Wait (fire spreads) +result = env.step(WildfireAction(action="wait")) + +env.close() +``` + +--- + ## 🔥 Environment Overview This environment models **forest-fire dynamics** influenced by: -- **Wind direction** (8 directions + calm) -- **Humidity** (suppresses ignition) -- **Fuel type and spread rate** -- **Limited resources** (water units, break materials) -- **Time pressure** (each step costs reward) +- **Wind direction** (8 directions + calm) - accelerates fire spread in wind direction +- **Humidity** (0.0-1.0) - suppresses ignition probability +- **Fuel type and spread rate** - vegetation burns and spreads to neighbors +- **Limited resources** (water units, break materials) - strategic resource management +- **Time pressure** (each step costs small reward penalty) The goal is to **minimize fire spread** and **total burned area** while using resources efficiently. +### Episode Termination + +An episode ends when: +- **All fires are extinguished** (`burning_count == 0`) - **Success!** +- **Maximum steps reached** (`step_count >= max_steps`) - Time limit exceeded + --- -## 🧱 Grid Encoding +## 🧱 Grid Format & Encoding -| Code | Meaning | Color (Visualization) | -|------|----------------|-----------------------| -| 0 | Ash (burned) | Black ⚫ | -| 1 | Fuel | Green 🟩 | -| 2 | Burning | Red 🔥 | -| 3 | Firebreak | Brown 🟫 | -| 4 | Water/Damp | Blue 🔵 | +### Grid Structure ---- +The grid is returned as a **flat 1D array** in the observation. To access cell at position `(x, y)`: -## ⚙️ Architecture +```python +index = y * width + x +cell_value = observation.grid[index] +``` +**Example:** For a 32×32 grid, cell at (10, 15): +```python +index = 15 * 32 + 10 # = 490 +cell_value = observation.grid[490] ``` -┌────────────────────────────────────────────┐ -│ RL Agent / LLM Trainer (Client) │ -│ wildfire_env.step(WildfireAction(...)) │ -└──────────────────┬─────────────────────────┘ - │ HTTP -┌──────────────────▼─────────────────────────┐ -│ FastAPI Server (Docker) │ -│ WildfireEnvironment │ -│ ├─ Handles wind, humidity, spread │ -│ ├─ Applies agent actions │ -│ ├─ Updates grid + reward shaping │ -│ └─ Returns WildfireObservation │ -└────────────────────────────────────────────┘ + +### Cell Encoding + +| Code | Meaning | Color (Visualization) | Behavior | +|------|----------------|-----------------------|----------| +| `0` | Ash (burned) | Black ⚫ | Burned out, cannot reignite | +| `1` | Fuel | Green 🟩 | Healthy vegetation, can ignite | +| `2` | Burning | Red 🔥 | Currently on fire, spreads to neighbors | +| `3` | Firebreak | Brown 🟫 | Barrier, fire cannot cross | +| `4` | Water/Damp | Blue 🔵 | Dampened, immune to ignition temporarily | + +### Grid Visualization Example + +```python +import numpy as np + +obs = env.reset().observation +grid_2d = np.array(obs.grid).reshape(obs.height, obs.width) + +# Now grid_2d[y][x] gives the cell value at position (x, y) +print(grid_2d[15][10]) # Cell at x=10, y=15 ``` --- -## 🚀 Installation & Usage +## 🎮 Actions -### Option 1: Local Development (no Docker) +### Action Types -**Requirements:** -- Python 3.10 + -- FastAPI + Uvicorn -- NumPy + Matplotlib (for visualization) +#### 1. `water` - Apply Water +**Extinguishes burning cells and dampens fuel to prevent ignition.** -```bash -pip install fastapi uvicorn numpy matplotlib requests +```python +WildfireAction(action="water", x=10, y=15) ``` -Run server locally: -```bash -python -m envs.wildfire_env.server.app +**Effects:** +- **Burning cell (2)**: Extinguishes → becomes Water/Damp (4), gives **+0.25 reward** +- **Fuel cell (1)**: Dampens → becomes Water/Damp (4), gives **-0.10 reward** (preventive, slight penalty) +- **Water/Damp cell (4)**: Redundant watering, gives **-0.05 reward** +- **Ash/Break (0, 3)**: Wasteful, gives **-0.05 reward** + +**Resource Cost:** 1 water unit per action +**Requires:** `remaining_water > 0` and valid coordinates + +**Best Use:** Extinguish active fires before they spread + +--- + +#### 2. `break` - Create Firebreak +**Builds a fire-resistant barrier that stops fire spread.** + +```python +WildfireAction(action="break", x=12, y=15) ``` -Client usage: +**Effects:** +- **Fuel/Water cell (1, 4)**: Creates firebreak → becomes Firebreak (3), gives **+0.15 reward** +- **Burning cell (2)**: Extinguishes → becomes Firebreak (3), gives **-0.02 reward** (less effective than water) +- **Firebreak (3)**: Redundant, gives **-0.01 reward** +- **Ash (0)**: Wasteful, gives **-0.02 reward** + +**Resource Cost:** 1 firebreak material per action +**Requires:** `remaining_breaks > 0` and valid coordinates + +**Best Use:** Create barriers ahead of fire front to contain spread + +--- + +#### 3. `wait` - Do Nothing +**Let natural fire dynamics occur (fire spreads).** + ```python -from envs.wildfire_env import WildfireEnv, WildfireAction +WildfireAction(action="wait") +``` -env = WildfireEnv(base_url="http://localhost:8000") +**Effects:** +- No resource cost +- No coordinate required +- Fire spreads naturally to neighboring cells +- Small time penalty (-0.01 reward per step) + +**Best Use:** When fire is contained, waiting for it to burn out + +--- + +### Invalid Actions + +Actions that fail (give **-0.05 reward**): +- Invalid coordinates (out of bounds) +- Using water when `remaining_water == 0` +- Using break when `remaining_breaks == 0` +- Missing required coordinates for water/break actions + +--- + +## 👁️ Observations + +### `WildfireObservation` + +Returned after every `reset()` or `step()`: +```python +@dataclass +class WildfireObservation(Observation): + grid: List[int] # Flat array: [1,1,2,1,...] length = width × height + width: int # Grid width (default: 32) + height: int # Grid height (default: 32) + step: int # Current step number (0 at reset) + wind_dir: str # "N", "NE", "E", "SE", "S", "SW", "W", "NW", "CALM" + humidity: float # [0.0, 1.0] - higher = less fire spread + burning_count: int # Number of cells currently on fire + burned_count: int # Total number of ash cells (cumulative) + remaining_water: int # Water units left + remaining_breaks: int # Firebreak materials left + reward_hint: float # Shaping reward (for debugging) + done: bool # Episode ended? + reward: float # Step reward +``` + +### Example Observation + +```python result = env.reset() -print(f"🔥 Fires: {result.observation.burning_count}, 💧 Water left: {result.observation.remaining_water}") +obs = result.observation + +print(f"Step: {obs.step}") # 0 +print(f"Grid size: {obs.width}x{obs.height}") # 32x32 +print(f"Grid cells: {len(obs.grid)}") # 1024 +print(f"Active fires: {obs.burning_count}") # 2 +print(f"Wind: {obs.wind_dir}") # "NE" +print(f"Humidity: {obs.humidity:.2f}") # 0.24 +print(f"Water left: {obs.remaining_water}") # 8 +print(f"Breaks left: {obs.remaining_breaks}") # 50 +``` -for _ in range(5): - result = env.step(WildfireAction(action="water", x=10, y=10)) - print(f"Reward: {result.reward}, Burning left: {result.observation.burning_count}") +--- -env.close() +## 💰 Reward Structure + +### Step Rewards + +| Action | Condition | Reward | +|--------|-----------|--------| +| **Water burning cell** | Extinguishes fire | **+0.25** | +| **Water fuel cell** | Preventive dampening | **-0.10** | +| **Create firebreak** | From fuel/water | **+0.15** | +| **Fire spreads** | Each new burning cell | **-0.15 per cell** | +| **Fire shrinks** | Each extinguished cell | **+0.10 per cell** | +| **New burned area** | Each cell turns to ash | **-0.05 per cell** | +| **Time penalty** | Every step | **-0.01** | +| **Invalid action** | Out of bounds, no resources | **-0.05** | +| **Redundant action** | Watering already damp cell | **-0.05** | + +### Episode End Bonuses + +When episode terminates (`done == True`): + +- **Fire contained** (`burning_count == 0`): + - **+0.5** base bonus + - **+0.5 × saved_ratio** bonus (proportion of cells not burned) + +- **Fallback reward**: + - **+0.2 × (1.0 - burned_ratio)** bonus + +**Example:** Perfect containment (no burned cells): +```python +Reward = +0.5 + 0.5 × 1.0 = +1.0 +``` + +### Reward Interpretation + +- **Positive rewards**: Good containment actions, extinguishing fires +- **Negative rewards**: Fire spread, resource waste, time penalty +- **Goal**: Maximize cumulative reward = minimize fire damage + +--- + +## 🌪️ Fire Spread Mechanics + +### Spread Model + +Fire spreads using an **8-directional neighbor model**: + +1. **Burning cells persist** for `burn_lifetime = 3` ticks before turning to ash +2. Each burning cell can ignite **neighboring fuel cells** (8 directions) +3. Spread probability depends on: + - **Base ignition probability**: `0.30` (30% chance) + - **Humidity factor**: `(1.0 - humidity)` - higher humidity = less spread + - **Wind multiplier**: + - **+2.0x** in wind direction + - **+0.5x** against wind + - **+1.0x** perpendicular + - **Diagonal factor**: `0.6x` for diagonal neighbors (slower spread) + +4. **Water/Damp cells (4)** are **immune** to ignition while damp +5. **Firebreaks (3)** **cannot** be crossed by fire +6. **Ash cells (0)** cannot reignite + +### Wind Effects + +| Wind Direction | Effect on Fire Spread | +|----------------|----------------------| +| **In wind direction** | 2× faster ignition probability | +| **Against wind** | 0.5× slower ignition probability | +| **Perpendicular** | Normal (1×) ignition probability | +| **CALM** | No directional bias | + +### Water Dampening Duration + +Watered cells (4) remain damp for **6 ticks** before reverting to fuel (1). + +### Example Fire Spread + +``` +Step 0: Step 1: Step 2: +🟩🟩🟩 🟩🟥🟩 🟫🟥🟫 +🟩🟥🟩 → 🟥🟥🟥 → 🟥🟥🟥 (Wind: E, spreading east) +🟩🟩🟩 🟩🟥🟩 🟫🟥🟫 +``` + +--- + +## ⚙️ Configuration + +### Environment Variables + +Set these **before starting the server**: + +| Variable | Description | Default | Range | +|-----------|-------------|---------|-------| +| `WILDFIRE_WIDTH` | Grid width in cells | `32` | 8-128 | +| `WILDFIRE_HEIGHT` | Grid height in cells | `32` | 8-128 | +| `WILDFIRE_HUMIDITY` | Initial humidity level | `0.25` | 0.0-1.0 | +| `WILDFIRE_WIND` | Wind direction (fixed) | Random | `N`, `NE`, `E`, `SE`, `S`, `SW`, `W`, `NW`, `CALM` | +| `WILDFIRE_SEED` | Random seed | `3407` | Any integer | +| `WILDFIRE_MAX_STEPS` | Max steps per episode | `128` | 10-1000 | +| `WILDFIRE_WATER_CAPACITY` | Initial water units | `8` | 1-100 | +| `WILDFIRE_BREAK_CAPACITY` | Initial firebreak materials | `50` | 1-200 | + +### Python API Configuration + +```python +from envs.wildfire_env.server.wildfire_environment import WildfireEnvironment + +env = WildfireEnvironment( + width=64, + height=64, + humidity=0.3, + init_sources=3, # Number of initial fires + max_steps=200, + water_capacity=10, + break_capacity=75, + seed=42 +) +``` + +### Docker Configuration + +```bash +docker run -p 8000:8000 \ + -e WILDFIRE_WIDTH=64 \ + -e WILDFIRE_HEIGHT=64 \ + -e WILDFIRE_HUMIDITY=0.4 \ + -e WILDFIRE_WIND=N \ + -e WILDFIRE_WATER_CAPACITY=12 \ + wildfire-env:latest +``` + +### Using the Run Script + +```bash +# Custom configuration +WILDFIRE_WIDTH=64 WILDFIRE_HEIGHT=64 WILDFIRE_HUMIDITY=0.5 ./run_wildfire_docker.sh ``` --- -### Option 2: Docker (Recommended) +## 🚀 Installation & Usage + +### Option 1: Docker (Recommended) -Build the image: +**Using the convenience script:** ```bash -cd OpenEnv -docker build -f src/envs/wildfire_env/server/Dockerfile -t wildfire-env:latest . +./run_wildfire_docker.sh ``` -Run the container: +This script: +- Builds the base image if needed +- Rebuilds the wildfire image +- Starts the container +- Shows logs in real-time + +**Manual Docker setup:** ```bash -docker run -p 8000:8000 wildfire-env:latest +# Build base image (first time only) +docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile . + +# Build wildfire environment +docker build -t wildfire-env:latest -f src/envs/wildfire_env/server/Dockerfile . + +# Run container +docker run -d -p 8000:8000 --name wildfire-env-container wildfire-env:latest + +# View logs +docker logs -f wildfire-env-container + +# Stop container +docker stop wildfire-env-container + +# Remove container +docker rm wildfire-env-container +``` + +### Option 2: Local Development (No Docker) + +**Requirements:** +```bash +pip install fastapi uvicorn numpy matplotlib requests ``` -Connect via client: +**Run server:** +```bash +# From OpenEnv root directory +python -m envs.wildfire_env.server.app +``` + +**Or with environment variables:** +```bash +WILDFIRE_WIDTH=64 WILDFIRE_HUMIDITY=0.3 python -m envs.wildfire_env.server.app +``` + +--- + +## 📚 API Reference + +### Client Class + ```python -from envs.wildfire_env import WildfireEnv, WildfireAction +from envs.wildfire_env import WildfireEnv + +# Connect to existing server +env = WildfireEnv(base_url="http://localhost:8000") + +# Or create from Docker image env = WildfireEnv.from_docker_image("wildfire-env:latest") +``` + +### Methods + +#### `reset() -> StepResult[WildfireObservation]` + +Resets the environment to initial state. + +```python result = env.reset() -print(f"Active fires: {result.observation.burning_count}") -result = env.step(WildfireAction(action="break", x=8, y=12)) -print(f"Reward: {result.reward}") -env.close() +obs = result.observation +print(f"New episode: {obs.step == 0}") ``` ---- +#### `step(action: WildfireAction) -> StepResult[WildfireObservation]` + +Takes an action and returns new observation. + +```python +action = WildfireAction(action="water", x=10, y=15) +result = env.step(action) +print(f"Reward: {result.reward}, Done: {result.done}") +``` -## 🌦️ Configuration +#### `state -> WildfireState` -| Variable | Description | Default | -|-----------|--------------|----------| -| `WILDFIRE_WIDTH` | Grid width | 32 | -| `WILDFIRE_HEIGHT` | Grid height | 32 | -| `WILDFIRE_HUMIDITY` | Initial humidity [0–1] | 0.25 | -| `WILDFIRE_WIND` | Wind direction (`N`, `NE`, `E`, `SE`, `S`, `SW`, `W`, `NW`, `CALM`) | Random | -| `WILDFIRE_SEED` | RNG seed | 3407 | -| `WILDFIRE_MAX_STEPS` | Max steps per episode | 128 | -| `WILDFIRE_WATER_CAPACITY` | Water units available | 8 | -| `WILDFIRE_BREAK_CAPACITY` | Firebreak materials | 50 | +Access current environment state. ---- +```python +state = env.state +print(f"Episode ID: {state.episode_id}") +print(f"Total burned: {state.total_burned}") +print(f"Total extinguished: {state.total_extinguished}") +``` -## 🧠 API Reference +#### `close()` + +Closes the connection (for HTTP clients, this is a no-op but good practice). + +```python +env.close() +``` + +### Data Classes + +#### `WildfireAction` -### `WildfireAction` ```python @dataclass class WildfireAction(Action): action: str # "water" | "break" | "wait" - x: Optional[int] = None # Target X - y: Optional[int] = None # Target Y + x: Optional[int] = None # Target X coordinate (required for water/break) + y: Optional[int] = None # Target Y coordinate (required for water/break) ``` -### `WildfireObservation` +**Examples:** ```python -@dataclass -class WildfireObservation(Observation): - grid: List[int] - width: int - height: int - step: int - wind_dir: str - humidity: float - burning_count: int - burned_count: int - remaining_water: int - remaining_breaks: int - reward_hint: float +WildfireAction(action="water", x=10, y=15) +WildfireAction(action="break", x=12, y=15) +WildfireAction(action="wait") # x, y not needed ``` -### `WildfireState` +#### `WildfireObservation` + +See [Observations](#-observations) section for full details. + +#### `WildfireState` + ```python @dataclass class WildfireState(State): @@ -190,29 +559,130 @@ class WildfireState(State): step_count: int total_burned: int total_extinguished: int - remaining_water: int - remaining_breaks: int + last_action: str + width: int + height: int wind_dir: str humidity: float + remaining_water: int + remaining_breaks: int + grid: List[int] + burn_timers: List[int] ``` --- -## Sample rendering to see wildfire simulation -**Note:** This example requires Jupyter notebook or IPython environment for the `clear_output` and `display` functions. For standalone Python scripts, see `examples/wildfire.py`. +## 📖 Examples + +### Example 1: Simple Containment Strategy ```python -import matplotlib.pyplot as plt +from envs.wildfire_env import WildfireEnv, WildfireAction import numpy as np -import time -from IPython.display import clear_output, display -import matplotlib.colors as mcolors +env = WildfireEnv(base_url="http://localhost:8000") +result = env.reset() +obs = result.observation + +grid_2d = np.array(obs.grid).reshape(obs.height, obs.width) +total_reward = 0 + +while not result.done: + # Find burning cells + burning_indices = np.where(grid_2d == 2) + + if len(burning_indices[0]) > 0 and obs.remaining_water > 0: + # Water the first burning cell + y, x = burning_indices[0][0], burning_indices[1][0] + action = WildfireAction(action="water", x=int(x), y=int(y)) + else: + # Wait if no water or no fires + action = WildfireAction(action="wait") + + result = env.step(action) + obs = result.observation + total_reward += result.reward or 0 + + # Update grid + grid_2d = np.array(obs.grid).reshape(obs.height, obs.width) + + print(f"Step {obs.step}: Burning={obs.burning_count}, Reward={result.reward:.3f}") + +print(f"\nEpisode ended. Total reward: {total_reward:.2f}") +print(f"Final stats: Burned={obs.burned_count}, Extinguished={env.state.total_extinguished}") +env.close() +``` + +### Example 2: Firebreak Strategy + +```python from envs.wildfire_env import WildfireEnv, WildfireAction +import numpy as np + +env = WildfireEnv(base_url="http://localhost:8000") +result = env.reset() +obs = result.observation + +def create_firebreak_barrier(obs, env): + """Create firebreak ahead of fire front based on wind direction.""" + grid_2d = np.array(obs.grid).reshape(obs.height, obs.width) + wind = obs.wind_dir + + # Find burning cells + burning_y, burning_x = np.where(grid_2d == 2) + + if len(burning_x) == 0 or obs.remaining_breaks == 0: + return WildfireAction(action="wait") + + # Calculate fire front position + if wind == "E": + target_x = int(np.max(burning_x)) + 2 # Ahead of easternmost fire + target_y = int(np.mean(burning_y)) + elif wind == "W": + target_x = int(np.min(burning_x)) - 2 + target_y = int(np.mean(burning_y)) + elif wind == "N": + target_x = int(np.mean(burning_x)) + target_y = int(np.min(burning_y)) - 2 + elif wind == "S": + target_x = int(np.mean(burning_x)) + target_y = int(np.max(burning_y)) + 2 + else: + # Fallback: water nearest burning cell + return WildfireAction(action="water", x=int(burning_x[0]), y=int(burning_y[0])) + + # Ensure within bounds + target_x = max(0, min(obs.width - 1, target_x)) + target_y = max(0, min(obs.height - 1, target_y)) + + return WildfireAction(action="break", x=target_x, y=target_y) + +total_reward = 0 +while not result.done: + action = create_firebreak_barrier(obs, env) + result = env.step(action) + obs = result.observation + total_reward += result.reward or 0 + + if obs.step % 10 == 0: + print(f"Step {obs.step}: Fires={obs.burning_count}, Water={obs.remaining_water}, Breaks={obs.remaining_breaks}") + +env.close() +``` + +### Example 3: Visualization with Matplotlib -client = WildfireEnv("http://localhost:8000") +```python +import matplotlib.pyplot as plt +import numpy as np +import matplotlib.colors as mcolors +from envs.wildfire_env import WildfireEnv, WildfireAction +env = WildfireEnv(base_url="http://localhost:8000") +result = env.reset() +obs = result.observation +# Setup colormap cmap = mcolors.ListedColormap([ "black", # 0 = ash "green", # 1 = fuel @@ -220,122 +690,282 @@ cmap = mcolors.ListedColormap([ "saddlebrown", # 3 = firebreak "blue" # 4 = water ]) - norm = mcolors.BoundaryNorm([0, 1, 2, 3, 4, 5], cmap.N) +fig, ax = plt.subplots(figsize=(8, 8)) +plt.ion() -plt.ion() -fig, ax = plt.subplots(figsize=(5, 5)) -plt.axis("off") - +for step in range(50): + if result.done: + break + + # Render grid + grid_2d = np.array(obs.grid).reshape(obs.height, obs.width) + ax.clear() + ax.imshow(grid_2d, cmap=cmap, norm=norm, interpolation='nearest') + ax.set_title( + f"Step {obs.step} | Fires: {obs.burning_count} | Burned: {obs.burned_count}\n" + f"Wind: {obs.wind_dir} | Humidity: {obs.humidity:.2f} | " + f"Water: {obs.remaining_water} | Breaks: {obs.remaining_breaks}" + ) + plt.pause(0.1) + + # Take action (simple: water first burning cell) + if obs.burning_count > 0 and obs.remaining_water > 0: + burning_indices = np.where(grid_2d == 2) + if len(burning_indices[0]) > 0: + y, x = burning_indices[0][0], burning_indices[1][0] + action = WildfireAction(action="water", x=int(x), y=int(y)) + else: + action = WildfireAction(action="wait") + else: + action = WildfireAction(action="wait") + + result = env.step(action) + obs = result.observation -res = client.reset() -obs = res.observation -grid = np.array(obs.grid).reshape(obs.height, obs.width) +plt.ioff() +plt.show() +env.close() +``` +### Example 4: Training Loop for RL -im = ax.imshow(grid, cmap=cmap, norm=norm) +```python +from envs.wildfire_env import WildfireEnv, WildfireAction +import random +env = WildfireEnv(base_url="http://localhost:8000") -title_text = ax.set_title( - f"Step {obs.step} | Burning={obs.burning_count} | Burned={obs.burned_count}\n" - f"Wind={obs.wind_dir} | Humidity={obs.humidity:.2f}", - color="black", - fontsize=10 -) +num_episodes = 10 +episode_rewards = [] +for episode in range(num_episodes): + result = env.reset() + obs = result.observation + episode_reward = 0 + episode_steps = 0 + + while not result.done: + # Random policy (replace with your RL agent) + if random.random() < 0.4 and obs.remaining_water > 0: + action = WildfireAction( + action="water", + x=random.randint(0, obs.width - 1), + y=random.randint(0, obs.height - 1) + ) + elif random.random() < 0.3 and obs.remaining_breaks > 0: + action = WildfireAction( + action="break", + x=random.randint(0, obs.width - 1), + y=random.randint(0, obs.height - 1) + ) + else: + action = WildfireAction(action="wait") + + result = env.step(action) + obs = result.observation + episode_reward += result.reward or 0 + episode_steps += 1 + + episode_rewards.append(episode_reward) + state = env.state + print( + f"Episode {episode + 1}: " + f"Reward={episode_reward:.2f}, " + f"Steps={episode_steps}, " + f"Burned={state.total_burned}, " + f"Extinguished={state.total_extinguished}" + ) +print(f"\nAverage reward: {sum(episode_rewards) / len(episode_rewards):.2f}") +env.close() +``` -print("Starting smooth animation...") -for _ in range(100): - clear_output(wait=True) +--- - new_grid = np.array(obs.grid).reshape(obs.height, obs.width) +## 🌐 Web Interface - im.set_data(new_grid) +The wildfire environment includes a **built-in web interface** for interactive exploration. - title_text.set_text( - f"Step {obs.step} | Burning={obs.burning_count} | Burned={obs.burned_count}\n" - f"Wind={obs.wind_dir} | Humidity={obs.humidity:.2f}" - ) +### Accessing the Web Interface - - display(fig) - - - time.sleep(0.3) +1. **Start the server** (Docker or local) +2. **Open browser** to: `http://localhost:8000/web` +3. **Interact** with the environment visually - - res = client.step(WildfireAction(action="wait")) - obs = res.observation +### Web Interface Features - if obs.burning_count == 0: - print(f"🔥 Fire has fully burned out after {obs.step} steps.") - break +- **Visual grid display** - See the fire spread in real-time +- **Action form** - Select action type and coordinates +- **State observer** - View current observation and state +- **Action history** - Log of all actions taken +- **Reset button** - Start new episode +- **WebSocket updates** - Real-time state updates -plt.ioff() # Turn off interactive mode -plt.close(fig) # Close the figure at the end -print("Animation complete.") +### Using the Web Interface -``` +1. Click **"Reset Environment"** to start +2. Fill in action form: + - Select action: `water`, `break`, or `wait` + - Enter coordinates (x, y) for water/break actions +3. Click **"Submit Action"** +4. Observe the grid update and rewards +5. Monitor resources (water, breaks) in the state panel --- +## 🔧 Troubleshooting -## 🧪 Example Training Loop (GRPO/LLM) +### Common Issues -```python -from envs.wildfire_env import WildfireEnv, WildfireAction -import random +#### 1. Connection Errors -env = WildfireEnv.from_docker_image("wildfire-env:latest") +**Problem:** `ConnectionRefusedError` or `Cannot connect to server` -for episode in range(3): - result = env.reset() - total_reward = 0 +**Solutions:** +- Verify server is running: `curl http://localhost:8000/health` +- Check Docker container: `docker ps | grep wildfire` +- Ensure port 8000 is not in use: `lsof -i :8000` - while not result.done: - a = random.choice(["water", "break", "wait"]) - x, y = random.randint(0, 15), random.randint(0, 15) - result = env.step(WildfireAction(action=a, x=x, y=y)) - total_reward += result.reward or 0 +#### 2. Index Errors - print(f"Episode {episode}: total_reward={total_reward:.2f}") +**Problem:** `IndexError: list index out of range` -env.close() +**Solution:** Ensure coordinates are within bounds: +```python +# Always check bounds before accessing +if 0 <= x < obs.width and 0 <= y < obs.height: + action = WildfireAction(action="water", x=x, y=y) ``` ---- +#### 3. Invalid Action Warnings + +**Problem:** Actions returning -0.05 reward repeatedly + +**Solutions:** +- Check `remaining_water` and `remaining_breaks` before using resources +- Verify coordinates are integers and within grid bounds +- Use `action="wait"` when resources are exhausted + +#### 4. Grid Format Confusion + +**Problem:** How to access grid cells? + +**Solution:** +```python +# Convert flat array to 2D +grid_2d = np.array(obs.grid).reshape(obs.height, obs.width) + +# Access cell at (x, y) +cell_value = grid_2d[y][x] -## 🧰 DockerHub & GitHub Build +# Or use flat index +index = y * obs.width + x +cell_value = obs.grid[index] +``` + +#### 5. Docker Build Failures -Build and push: +**Problem:** `failed to solve: openenv-base:latest` +**Solution:** ```bash +# Build base image first docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile . -docker build -t ghcr.io//openenv-wildfire:latest -f src/envs/wildfire_env/server/Dockerfile . -docker push ghcr.io//openenv-wildfire:latest -``` -GitHub Action matrix entry: -```yaml -strategy: - matrix: - image: - - name: wildfire-env - dockerfile: src/envs/wildfire_env/server/Dockerfile +# Then build wildfire image +docker build -t wildfire-env:latest -f src/envs/wildfire_env/server/Dockerfile . ``` +### Debugging Tips + +1. **Enable verbose logging:** + ```bash + docker logs -f wildfire-env-container + ``` + +2. **Check environment state:** + ```python + state = env.state + print(f"State: {state}") + ``` + +3. **Validate actions:** + ```python + obs = env.reset().observation + print(f"Bounds: 0 <= x < {obs.width}, 0 <= y < {obs.height}") + print(f"Resources: Water={obs.remaining_water}, Breaks={obs.remaining_breaks}") + ``` + +4. **Monitor grid changes:** + ```python + prev_grid = obs.grid.copy() + result = env.step(action) + new_grid = result.observation.grid + changes = [i for i, (a, b) in enumerate(zip(prev_grid, new_grid)) if a != b] + print(f"Changed cells: {len(changes)}") + ``` + +--- + +## 📊 Performance Considerations + +### Grid Size Impact + +- **Small grids (16×16)**: Fast, good for quick testing +- **Medium grids (32×32)**: Default, balanced performance +- **Large grids (64×64+)**: Slower, more realistic but requires more compute + +### Resource Limits + +- **Low water (4-8)**: Forces strategic decisions +- **High water (20+)**: More forgiving, easier to succeed +- **Low breaks (25)**: Emphasizes firebreak placement strategy +- **High breaks (100+)**: More freedom, less constraint + +### Episode Length + +- **Short episodes (50 steps)**: Fast iteration, good for debugging +- **Medium episodes (128 steps)**: Default, balanced +- **Long episodes (200+ steps)**: Better for complex strategies + --- ## 🧭 References -- [OpenEnv Framework](https://github.com/openenv) -- [FastAPI Documentation](https://fastapi.tiangolo.com/) -- [Reinforcement Learning Introduction](https://spinningup.openai.com/en/latest/) -- [Fire Spread Simulation Models (USFS Research)](https://www.fs.fed.us/rm/pubs/rmrs_gtr371.html) +### Papers & Research + +- **Rothermel Model**: [USDA Forest Service - Surface Fire Spread Model](https://www.fs.fed.us/rm/pubs_series/rmrs/gtr/rmrs_gtr371.pdf) +- **SimFire**: [MITRE Fireline Project](https://github.com/mitrefireline/simfire) +- **RL for Wildfires**: [arXiv:2311.15925](https://arxiv.org/abs/2311.15925) + +### OpenEnv Framework + +- **Main Repository**: [OpenEnv GitHub](https://github.com/openenv) +- **Documentation**: See `rfcs/` directory for design documents +- **Other Environments**: See `src/envs/` for more environment examples + +### Related Tools + +- **FastAPI**: [FastAPI Documentation](https://fastapi.tiangolo.com/) +- **Reinforcement Learning**: [Spinning Up in Deep RL](https://spinningup.openai.com/) +- **Docker**: [Docker Documentation](https://docs.docker.com/) + +--- + +## 📝 License + +This environment is part of the OpenEnv project. See the main LICENSE file for details. + +--- + +## 🤝 Contributing + +Contributions welcome! Please see `CONTRIBUTING.md` in the main OpenEnv repository. --- + ## 🔖 Citations ```bibtex @@ -365,8 +995,12 @@ strategy: @misc{wildfire-openenv-2025, title = {Wildfire Environment for OpenEnv: Containment-Focused RL Simulation}, - author = {Harikrishnan, Ram Sankar}, + author = {OpenEnv Contributors}, year = {2025}, - url = {https://github.com//openenv-wildfire} + url = {https://github.com/openenv/openenv} } ``` + +--- + +**Happy firefighting! 🔥🚒** diff --git a/src/envs/wildfire_env/server/wildfire_environment.py b/src/envs/wildfire_env/server/wildfire_environment.py index 24c6fb81..99c27499 100644 --- a/src/envs/wildfire_env/server/wildfire_environment.py +++ b/src/envs/wildfire_env/server/wildfire_environment.py @@ -14,9 +14,13 @@ } def idx(x: int, y: int, w: int) -> int: + # Defensive type conversion to ensure all parameters are integers + x, y, w = int(x), int(y), int(w) return y * w + x def in_bounds(x: int, y: int, w: int, h: int) -> bool: + # Defensive type conversion to ensure all parameters are integers + x, y, w, h = int(x), int(y), int(w), int(h) return 0 <= x < w and 0 <= y < h @@ -59,9 +63,9 @@ def __init__( humidity = float(os.environ.get("WILDFIRE_HUMIDITY", humidity)) forced_wind = os.environ.get("WILDFIRE_WIND", None) - # Store config - self.w = width - self.h = height + # Store config (ensure integers) + self.w = int(width) + self.h = int(height) self.base_ignite_prob = base_ignite_prob self.wind_bias = wind_bias self.diag_factor = diag_factor @@ -81,8 +85,11 @@ def __init__( # --- Core API --- def reset(self) -> WildfireObservation: + # Ensure w and h are integers (defensive type conversion) + w, h = int(self.w), int(self.h) + # Start with all fuel - grid = [1] * (self.w * self.h) + grid = [1] * (w * h) # Wind (forced if provided) if self.forced_wind and self.forced_wind in DIRS_8: @@ -95,9 +102,12 @@ def reset(self) -> WildfireObservation: # Place initial fires for _ in range(self.init_sources): - x = self.rng.randrange(self.w) - y = self.rng.randrange(self.h) - grid[idx(x, y, self.w)] = 2 + x = self.rng.randrange(w) + y = self.rng.randrange(h) + i = idx(x, y, w) + # Safety check: ensure index is within grid bounds + if 0 <= i < len(grid): + grid[i] = 2 self._state = WildfireState( episode_id=str(uuid.uuid4()), @@ -105,8 +115,8 @@ def reset(self) -> WildfireObservation: total_burned=0, total_extinguished=0, last_action="reset", - width=self.w, - height=self.h, + width=w, + height=h, wind_dir=wind_dir, humidity=humidity, remaining_water=self.init_water, @@ -115,7 +125,7 @@ def reset(self) -> WildfireObservation: ) # per-cell burn timers (persist across steps) - self._state.burn_timers = [0] * (self.w * self.h) + self._state.burn_timers = [0] * (w * h) obs = self._make_observation(reward_hint=0.0) return obs @@ -198,6 +208,8 @@ def step(self, action: WildfireAction) -> WildfireObservation: def _apply_water(self, x: int, y: int) -> float: st = self._state + # Ensure x and y are integers (defensive type conversion) + x, y = int(x), int(y) if not in_bounds(x, y, self.w, self.h): return -0.05 @@ -206,6 +218,10 @@ def _apply_water(self, x: int, y: int) -> float: return -0.5 i = idx(x, y, self.w) + # Safety check: ensure index is within grid bounds + if i < 0 or i >= len(st.grid): + return -0.05 + reward = 0.0 if st.grid[i] == 2: @@ -229,9 +245,15 @@ def _apply_water(self, x: int, y: int) -> float: def _apply_break(self, x: int, y: int) -> float: st = self._state + # Ensure x and y are integers (defensive type conversion) + x, y = int(x), int(y) if not in_bounds(x, y, self.w, self.h): return -0.05 i = idx(x, y, self.w) + # Safety check: ensure index is within grid bounds + if i < 0 or i >= len(st.grid): + return -0.05 + reward = 0.0 if st.grid[i] in (1, 4): @@ -263,6 +285,9 @@ def _spread_fire(self) -> int: new_grid = st.grid[:] newly_burned = 0 + # Ensure w and h are integers (defensive type conversion) + w, h = int(self.w), int(self.h) + # 8-neighbor model neighbors = [(-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (1, -1), (-1, 1), (1, 1)] @@ -271,12 +296,15 @@ def _spread_fire(self) -> int: base = self.base_ignite_prob humidity_factor = (1.0 - st.humidity) - ignite_flags = [False] * (self.w * self.h) + ignite_flags = [False] * (w * h) # First pass: evaluate ignitions, increment burn timers - for y in range(self.h): - for x in range(self.w): - i = idx(x, y, self.w) + for y in range(h): + for x in range(w): + i = idx(x, y, w) + # Safety check: ensure index is within grid bounds + if i < 0 or i >= len(st.grid): + continue cell = st.grid[i] if cell == 2: # burning @@ -284,9 +312,12 @@ def _spread_fire(self) -> int: for dx, dy in neighbors: nx, ny = x + dx, y + dy - if not in_bounds(nx, ny, self.w, self.h): + if not in_bounds(nx, ny, w, h): + continue + ni = idx(nx, ny, w) + # Safety check: ensure neighbor index is within grid bounds + if ni < 0 or ni >= len(st.grid): continue - ni = idx(nx, ny, self.w) target = st.grid[ni] # Only fuel or water/damp can be candidates, but cells with code 4 (watered/damp) are immune to ignition @@ -310,10 +341,16 @@ def _spread_fire(self) -> int: p = base * humidity_factor * wind_mult * diag_mult p = max(0.0, min(1.0, p)) if self.rng.random() < p: - ignite_flags[ni] = True + # Safety check: ensure ni is within ignite_flags bounds + if 0 <= ni < len(ignite_flags): + ignite_flags[ni] = True # Second pass: apply transitions for i, cell in enumerate(st.grid): + # Safety check: ensure index is within bounds for all arrays + if i < 0 or i >= len(new_grid) or i >= len(st.burn_timers): + continue + if cell == 2: # burns for burn_lifetime ticks before turning to ash if st.burn_timers[i] >= self.burn_lifetime: @@ -321,7 +358,7 @@ def _spread_fire(self) -> int: newly_burned += 1 else: new_grid[i] = 2 # keep burning - elif ignite_flags[i] and new_grid[i] == 1: + elif i < len(ignite_flags) and ignite_flags[i] and new_grid[i] == 1: new_grid[i] = 2 st.burn_timers[i] = 0 elif cell == 4: From 47a76ab99d413f2a8c85069b8605294297d68c64 Mon Sep 17 00:00:00 2001 From: Ram Harikrishnan Date: Mon, 3 Nov 2025 21:48:24 -0700 Subject: [PATCH 15/19] correction to README --- src/envs/wildfire_env/README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/envs/wildfire_env/README.md b/src/envs/wildfire_env/README.md index e6b3a81c..4bf918af 100644 --- a/src/envs/wildfire_env/README.md +++ b/src/envs/wildfire_env/README.md @@ -796,12 +796,14 @@ The wildfire environment includes a **built-in web interface** for interactive e ### Web Interface Features -- **Visual grid display** - See the fire spread in real-time -- **Action form** - Select action type and coordinates -- **State observer** - View current observation and state -- **Action history** - Log of all actions taken +- **Action form** - Dynamic form to select action type and enter coordinates +- **State observer** - View current observation and state (displayed as JSON) +- **Action history** - Log of all actions taken with timestamps - **Reset button** - Start new episode -- **WebSocket updates** - Real-time state updates +- **WebSocket updates** - Real-time state updates via WebSocket connection +- **Instructions panel** - Environment documentation and usage instructions + +**Note:** The grid is displayed as JSON data. For visual grid rendering, use the matplotlib examples in the [Examples](#-examples) section. ### Using the Web Interface From 594512a24d252e2c5013a4285ae55b67b73a1da8 Mon Sep 17 00:00:00 2001 From: Ram Harikrishnan Date: Tue, 4 Nov 2025 02:12:22 -0700 Subject: [PATCH 16/19] Add custom web interface for wildfire_env with ENABLE_WEB_INTERFACE flag support - Add custom wildfire-specific web interface (wildfire_web_interface.py) - Visual 2D grid display with color-coded cells (Ash, Fuel, Burning, Firebreak, Watered) - Interactive grid cells that auto-populate coordinates - Wildfire-specific action form (Water, Break, Wait) - Real-time environment stats display - WebSocket integration for live updates - Update app.py to support ENABLE_WEB_INTERFACE flag - Web interface routes are conditionally registered based on flag - Default behavior: disabled (requires flag to enable) - Maintains compatibility with existing Docker setup - Add test_local.sh script for local testing without Docker - Automatic virtual environment detection - Port conflict detection and handling - Dependency installation checks - Update README.md with comprehensive documentation - Web interface features and usage - Local testing instructions - ENABLE_WEB_INTERFACE flag documentation --- src/envs/wildfire_env/README.md | 97 +- src/envs/wildfire_env/server/app.py | 64 +- src/envs/wildfire_env/server/test_local.sh | 106 ++ .../server/wildfire_web_interface.py | 983 ++++++++++++++++++ 4 files changed, 1229 insertions(+), 21 deletions(-) create mode 100755 src/envs/wildfire_env/server/test_local.sh create mode 100644 src/envs/wildfire_env/server/wildfire_web_interface.py diff --git a/src/envs/wildfire_env/README.md b/src/envs/wildfire_env/README.md index 4bf918af..75954514 100644 --- a/src/envs/wildfire_env/README.md +++ b/src/envs/wildfire_env/README.md @@ -62,6 +62,8 @@ This makes WildfireEnv a **fast, controllable**, and **open benchmark** for appl ./run_wildfire_docker.sh ``` +**Note:** The web interface can be enabled with `ENABLE_WEB_INTERFACE=true`. Access it at `http://localhost:8000/web` when enabled. + Or manually: ```bash @@ -786,34 +788,91 @@ env.close() ## 🌐 Web Interface -The wildfire environment includes a **built-in web interface** for interactive exploration. +The Wildfire Environment includes a **custom web interface** with visual grid display and wildfire-specific features. ### Accessing the Web Interface -1. **Start the server** (Docker or local) -2. **Open browser** to: `http://localhost:8000/web` -3. **Interact** with the environment visually +#### Using Docker -### Web Interface Features +```bash +# From the OpenEnv root directory +./run_wildfire_docker.sh +``` -- **Action form** - Dynamic form to select action type and enter coordinates -- **State observer** - View current observation and state (displayed as JSON) -- **Action history** - Log of all actions taken with timestamps -- **Reset button** - Start new episode -- **WebSocket updates** - Real-time state updates via WebSocket connection -- **Instructions panel** - Environment documentation and usage instructions +Then open: `http://localhost:8000/web` -**Note:** The grid is displayed as JSON data. For visual grid rendering, use the matplotlib examples in the [Examples](#-examples) section. +#### Local Testing (No Docker) + +```bash +# From the OpenEnv root directory +./src/envs/wildfire_env/server/test_local.sh +``` + +Or manually: +```bash +# Enable web interface with flag +ENABLE_WEB_INTERFACE=true PYTHONPATH=src uvicorn src.envs.wildfire_env.server.app:app --reload --host 0.0.0.0 --port 8000 +``` + +### Web Interface Features + +#### Left Pane: Action Interface +- **Wildfire-specific action form** + - Action dropdown: Water (Extinguish Fire), Break (Create Firebreak), Wait (Do Nothing) + - Coordinate inputs (X, Y) - auto-populated when clicking grid cells + - Coordinates show/hide based on action type +- **Environment stats display** + - Step count + - Water remaining + - Breaks remaining + - Burning cells count +- **Current state display** + - Status (Reset/Running) + - Episode ID + - Wind direction + - Humidity +- **Control buttons** + - Reset Environment + - Get State + +#### Right Pane: Visual Grid & Logs +- **Visual 2D Grid Display** 🔥 + - 16×16 grid rendered as color-coded cells + - **Color coding:** + - 🟩 **Green** = Fuel (safe, value 1) + - 🔥 **Orange/Red** = Burning (fire, value 2) + - ⬛ **Dark Gray** = Ash (burned, value 0) + - 🟫 **Brown** = Firebreak (value 3) + - 🟦 **Blue** = Watered/Damp (value 4) + - **Interactive:** Click cells to set coordinates for water/break actions + - **Auto-updates:** Grid refreshes automatically via WebSocket +- **Legend** + - Color-coded legend explaining all cell types +- **Action history** + - Log of all actions with timestamps + - Shows action, observation, reward, and done status + +#### Additional Features +- **WebSocket connection** - Real-time state updates without page refresh +- **Instructions panel** - Collapsible environment documentation +- **Grid status indicator** - Shows grid dimensions and cell count ### Using the Web Interface -1. Click **"Reset Environment"** to start -2. Fill in action form: - - Select action: `water`, `break`, or `wait` - - Enter coordinates (x, y) for water/break actions -3. Click **"Submit Action"** -4. Observe the grid update and rewards -5. Monitor resources (water, breaks) in the state panel +1. **Start the server** (see above) +2. **Open browser** to: `http://localhost:8000/web` +3. **Click "Reset Environment"** to initialize and display the grid +4. **Interact with the grid:** + - Click on a cell to set coordinates for water/break actions + - Or manually enter X, Y coordinates +5. **Select action:** + - Choose `water`, `break`, or `wait` from the dropdown +6. **Click "Execute Action"** +7. **Watch the grid update in real-time:** + - Fire spreads automatically + - Cells change color based on state + - Stats update automatically +8. **Monitor resources** in the stats panel (water, breaks, burning count) --- diff --git a/src/envs/wildfire_env/server/app.py b/src/envs/wildfire_env/server/app.py index c3012acc..dce8be49 100644 --- a/src/envs/wildfire_env/server/app.py +++ b/src/envs/wildfire_env/server/app.py @@ -1,10 +1,70 @@ # server/app.py import os -from core.env_server import create_app +from fastapi.responses import HTMLResponse +from fastapi import WebSocket, WebSocketDisconnect +from core.env_server import create_fastapi_app +from core.env_server.web_interface import load_environment_metadata, WebInterfaceManager +from core.env_server.types import Action, Observation from ..models import WildfireAction, WildfireObservation from .wildfire_environment import WildfireEnvironment +from .wildfire_web_interface import get_wildfire_web_interface_html +from dataclasses import asdict W = int(os.getenv("WILDFIRE_WIDTH", "16")) H = int(os.getenv("WILDFIRE_HEIGHT", "16")) env = WildfireEnvironment(width=W, height=H) -app = create_app(env, WildfireAction, WildfireObservation, env_name='wildfire_env') + +# Create base app without web interface +app = create_fastapi_app(env, WildfireAction, WildfireObservation) + +# Check if web interface should be enabled +# This can be controlled via environment variable +enable_web = ( + os.getenv("ENABLE_WEB_INTERFACE", "false").lower() in ("true", "1", "yes") +) + +if enable_web: + # Load environment metadata + metadata = load_environment_metadata(env, 'wildfire_env') + + # Create web interface manager (needed for /web/reset, /web/step, /ws endpoints) + web_manager = WebInterfaceManager(env, WildfireAction, WildfireObservation, metadata) + + # Add our custom wildfire interface route + @app.get("/web", response_class=HTMLResponse) + async def wildfire_web_interface(): + """Custom wildfire-specific web interface.""" + return get_wildfire_web_interface_html(metadata) + + # Add web interface endpoints (these are needed for the interface to work) + @app.get("/web/metadata") + async def web_metadata(): + """Get environment metadata.""" + return asdict(metadata) + + @app.websocket("/ws") + async def websocket_endpoint(websocket: WebSocket): + """WebSocket endpoint for real-time updates.""" + await web_manager.connect_websocket(websocket) + try: + while True: + # Keep connection alive + await websocket.receive_text() + except WebSocketDisconnect: + await web_manager.disconnect_websocket(websocket) + + @app.post("/web/reset") + async def web_reset(): + """Reset endpoint for web interface.""" + return await web_manager.reset_environment() + + @app.post("/web/step") + async def web_step(request: dict): + """Step endpoint for web interface.""" + action_data = request.get("action", {}) + return await web_manager.step_environment(action_data) + + @app.get("/web/state") + async def web_state(): + """State endpoint for web interface.""" + return web_manager.get_state() diff --git a/src/envs/wildfire_env/server/test_local.sh b/src/envs/wildfire_env/server/test_local.sh new file mode 100755 index 00000000..a6cad4b2 --- /dev/null +++ b/src/envs/wildfire_env/server/test_local.sh @@ -0,0 +1,106 @@ +#!/bin/bash +# Script to test wildfire environment locally without Docker + +echo "🔥 Wildfire Environment - Local Testing" +echo "========================================" +echo "" + +# Check if Python is available +if ! command -v python3 &> /dev/null; then + echo "❌ Error: python3 not found" + exit 1 +fi + +# Set environment variables +export ENABLE_WEB_INTERFACE=true +export PYTHONPATH=src +export WILDFIRE_WIDTH=${WILDFIRE_WIDTH:-16} +export WILDFIRE_HEIGHT=${WILDFIRE_HEIGHT:-16} + +echo "Configuration:" +echo " Grid Width: $WILDFIRE_WIDTH" +echo " Grid Height: $WILDFIRE_HEIGHT" +echo " Web Interface: Enabled" +echo "" + +# Check if we're in a virtual environment or if .venv exists +if [ -n "$VIRTUAL_ENV" ]; then + PYTHON_CMD="$VIRTUAL_ENV/bin/python3" + PIP_CMD="$VIRTUAL_ENV/bin/pip" + echo "✅ Using virtual environment: $VIRTUAL_ENV" +elif [ -f "$(dirname "$0")/../../../../.venv/bin/python3" ]; then + # Check for .venv in project root + VENV_PATH="$(cd "$(dirname "$0")/../../.." && pwd)/.venv" + PYTHON_CMD="$VENV_PATH/bin/python3" + PIP_CMD="$VENV_PATH/bin/pip" + echo "✅ Using project virtual environment: $VENV_PATH" + export VIRTUAL_ENV="$VENV_PATH" + export PATH="$VENV_PATH/bin:$PATH" +else + PYTHON_CMD="python3" + PIP_CMD="pip3" +fi + +# Check if uvicorn is installed +if ! $PYTHON_CMD -c "import uvicorn" 2>/dev/null; then + echo "⚠️ uvicorn not found. Installing..." + $PIP_CMD install uvicorn fastapi +fi + +# Check if fastapi is installed +if ! $PYTHON_CMD -c "import fastapi" 2>/dev/null; then + echo "⚠️ fastapi not found. Installing..." + $PIP_CMD install fastapi +fi + +echo "" +# Check if port 8000 is in use +PORT=8000 +if lsof -ti:$PORT > /dev/null 2>&1; then + echo "⚠️ Port $PORT is already in use!" + echo "" + # Check what's using it + PROCESS=$(lsof -ti:$PORT | head -1) + PROCESS_INFO=$(ps -p $PROCESS -o comm= 2>/dev/null || echo "unknown") + echo " Port $PORT is being used by: $PROCESS_INFO (PID: $PROCESS)" + echo "" + echo "Options:" + echo " 1. Use a different port (8001)" + echo " 2. Kill existing processes on port $PORT (⚠️ WARNING: May kill important processes)" + echo "" + read -p "Choose option (1 or 2, default 1): " choice + choice=${choice:-1} + + if [ "$choice" = "1" ]; then + PORT=8001 + echo "✅ Using port $PORT instead" + else + echo "⚠️ Killing processes on port $PORT..." + lsof -ti:$PORT | xargs kill -9 2>/dev/null + sleep 1 + if lsof -ti:$PORT > /dev/null 2>&1; then + echo "❌ Failed to free port $PORT, using port 8001 instead" + PORT=8001 + else + echo "✅ Port $PORT is now free" + fi + fi +fi + +echo "" +echo "🚀 Starting server..." +echo " Access at: http://localhost:$PORT/web" +echo " Press Ctrl+C to stop" +echo "" + +# Run the server +cd "$(dirname "$0")/../../.." + +# Ensure PYTHONPATH is set for uvicorn (needed for reload mode) +# The issue is that uvicorn's reload mode spawns a new process that needs PYTHONPATH +# We need to set it in a way that's inherited by the subprocess +export PYTHONPATH="${PWD}/src:${PYTHONPATH}" + +# Use python -m uvicorn to ensure PYTHONPATH is respected in reload mode +$PYTHON_CMD -m uvicorn envs.wildfire_env.server.app:app --reload --host 0.0.0.0 --port $PORT + diff --git a/src/envs/wildfire_env/server/wildfire_web_interface.py b/src/envs/wildfire_env/server/wildfire_web_interface.py new file mode 100644 index 00000000..42bb8cd0 --- /dev/null +++ b/src/envs/wildfire_env/server/wildfire_web_interface.py @@ -0,0 +1,983 @@ +""" +Custom web interface for Wildfire Environment. + +This module provides a wildfire-specific web interface with visual grid display +and wildfire-specific features, without modifying the base web_interface.py. +""" + +from typing import Optional +from dataclasses import asdict +from core.env_server.types import EnvironmentMetadata +from ..models import WildfireAction + + +def get_wildfire_web_interface_html(metadata: Optional[EnvironmentMetadata] = None) -> str: + """Generate custom HTML for the wildfire environment web interface.""" + + # Convert markdown to HTML for instructions + instructions_html = "" + if metadata and metadata.readme_content: + instructions_html = _markdown_to_html_simple(metadata.readme_content) + + return f""" + + + + + + Wildfire Environment - Web Interface + + + +
+ +
+
+ + Wildfire Containment Interface +
+
+ + {_generate_instructions_section(instructions_html, metadata)} + + +
+

Take Action

+
+
+ + + + Water: Extinguishes fire at target cell
+ Break: Creates firebreak to prevent spread
+ Wait: Fire continues spreading +
+
+ + + + +
+
+ + +
+ + +
+ + +
+

Environment Stats

+
+
+ Step Count + 0 +
+
+ Water Remaining + 0 +
+
+ Breaks Remaining + 0 +
+
+ Burning Cells + 0 +
+
+
+ + +
+

Current State

+
+
+ Status: + Not initialized +
+
+ Episode ID: + - +
+
+ Wind Direction: + - +
+
+ Humidity: + - +
+
+
+
+
+ + +
+
+ Fire Grid Visualization +
+
+ +
+

Legend

+
+
+
+ Ash (Burned) +
+
+
+ Fuel (Safe) +
+
+
+ Burning (Fire) +
+
+
+ Firebreak +
+
+
+ Watered (Damp) +
+
+
+ + +
+

Fire Grid

+
+ Waiting for grid data... (Click "Reset Environment" to initialize) +
+
+
+ +
+
+

+ Click on a cell to set coordinates for water/break actions +

+
+ + +
+

Action History

+
+ No actions taken yet +
+
+
+
+
+ + + + + """.replace('{_generate_instructions_section(instructions_html, metadata)}', + _generate_instructions_section(instructions_html, metadata)) + + +def _generate_instructions_section(instructions_html: str, metadata: Optional[EnvironmentMetadata]) -> str: + """Generate the instructions section.""" + if not instructions_html or not metadata: + return '' + + return f''' + +
+
+

{metadata.name if metadata else "Wildfire Environment"}

+ +
+
+
+ {instructions_html} +
+
+
+ ''' + + +def _markdown_to_html_simple(markdown: str) -> str: + """Convert basic markdown to HTML.""" + import html + import re + + # Escape HTML first + html_content = html.escape(markdown) + + # Convert headers + html_content = re.sub(r'^# (.*?)$', r'

\1

', html_content, flags=re.MULTILINE) + html_content = re.sub(r'^## (.*?)$', r'

\1

', html_content, flags=re.MULTILINE) + html_content = re.sub(r'^### (.*?)$', r'

\1

', html_content, flags=re.MULTILINE) + + # Convert code blocks + html_content = re.sub(r'```(.*?)\n(.*?)\n```', r'
\2
', html_content, flags=re.DOTALL) + html_content = re.sub(r'`([^`]+)`', r'\1', html_content) + + # Convert bold and italic + html_content = re.sub(r'\*\*(.*?)\*\*', r'\1', html_content) + html_content = re.sub(r'\*(.*?)\*', r'\1', html_content) + + # Convert lists + html_content = re.sub(r'^- (.*?)$', r'
  • \1
  • ', html_content, flags=re.MULTILINE) + html_content = re.sub(r'(
  • .*
  • )', r'
      \1
    ', html_content, flags=re.DOTALL) + + # Convert line breaks + html_content = html_content.replace('\n', '
    ') + + return html_content + From 10cd4f452d7620ce64f61696e3a3e56ef1b67eb2 Mon Sep 17 00:00:00 2001 From: Ram Harikrishnan Date: Tue, 4 Nov 2025 02:13:25 -0700 Subject: [PATCH 17/19] Remove test_local.sh script and update README - Remove test_local.sh script - Update README to remove references to test_local.sh - Simplify local testing instructions --- src/envs/wildfire_env/README.md | 6 -- src/envs/wildfire_env/server/test_local.sh | 106 --------------------- 2 files changed, 112 deletions(-) delete mode 100755 src/envs/wildfire_env/server/test_local.sh diff --git a/src/envs/wildfire_env/README.md b/src/envs/wildfire_env/README.md index 75954514..77f7e350 100644 --- a/src/envs/wildfire_env/README.md +++ b/src/envs/wildfire_env/README.md @@ -803,12 +803,6 @@ Then open: `http://localhost:8000/web` #### Local Testing (No Docker) -```bash -# From the OpenEnv root directory -./src/envs/wildfire_env/server/test_local.sh -``` - -Or manually: ```bash # Enable web interface with flag ENABLE_WEB_INTERFACE=true PYTHONPATH=src uvicorn src.envs.wildfire_env.server.app:app --reload --host 0.0.0.0 --port 8000 diff --git a/src/envs/wildfire_env/server/test_local.sh b/src/envs/wildfire_env/server/test_local.sh deleted file mode 100755 index a6cad4b2..00000000 --- a/src/envs/wildfire_env/server/test_local.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/bin/bash -# Script to test wildfire environment locally without Docker - -echo "🔥 Wildfire Environment - Local Testing" -echo "========================================" -echo "" - -# Check if Python is available -if ! command -v python3 &> /dev/null; then - echo "❌ Error: python3 not found" - exit 1 -fi - -# Set environment variables -export ENABLE_WEB_INTERFACE=true -export PYTHONPATH=src -export WILDFIRE_WIDTH=${WILDFIRE_WIDTH:-16} -export WILDFIRE_HEIGHT=${WILDFIRE_HEIGHT:-16} - -echo "Configuration:" -echo " Grid Width: $WILDFIRE_WIDTH" -echo " Grid Height: $WILDFIRE_HEIGHT" -echo " Web Interface: Enabled" -echo "" - -# Check if we're in a virtual environment or if .venv exists -if [ -n "$VIRTUAL_ENV" ]; then - PYTHON_CMD="$VIRTUAL_ENV/bin/python3" - PIP_CMD="$VIRTUAL_ENV/bin/pip" - echo "✅ Using virtual environment: $VIRTUAL_ENV" -elif [ -f "$(dirname "$0")/../../../../.venv/bin/python3" ]; then - # Check for .venv in project root - VENV_PATH="$(cd "$(dirname "$0")/../../.." && pwd)/.venv" - PYTHON_CMD="$VENV_PATH/bin/python3" - PIP_CMD="$VENV_PATH/bin/pip" - echo "✅ Using project virtual environment: $VENV_PATH" - export VIRTUAL_ENV="$VENV_PATH" - export PATH="$VENV_PATH/bin:$PATH" -else - PYTHON_CMD="python3" - PIP_CMD="pip3" -fi - -# Check if uvicorn is installed -if ! $PYTHON_CMD -c "import uvicorn" 2>/dev/null; then - echo "⚠️ uvicorn not found. Installing..." - $PIP_CMD install uvicorn fastapi -fi - -# Check if fastapi is installed -if ! $PYTHON_CMD -c "import fastapi" 2>/dev/null; then - echo "⚠️ fastapi not found. Installing..." - $PIP_CMD install fastapi -fi - -echo "" -# Check if port 8000 is in use -PORT=8000 -if lsof -ti:$PORT > /dev/null 2>&1; then - echo "⚠️ Port $PORT is already in use!" - echo "" - # Check what's using it - PROCESS=$(lsof -ti:$PORT | head -1) - PROCESS_INFO=$(ps -p $PROCESS -o comm= 2>/dev/null || echo "unknown") - echo " Port $PORT is being used by: $PROCESS_INFO (PID: $PROCESS)" - echo "" - echo "Options:" - echo " 1. Use a different port (8001)" - echo " 2. Kill existing processes on port $PORT (⚠️ WARNING: May kill important processes)" - echo "" - read -p "Choose option (1 or 2, default 1): " choice - choice=${choice:-1} - - if [ "$choice" = "1" ]; then - PORT=8001 - echo "✅ Using port $PORT instead" - else - echo "⚠️ Killing processes on port $PORT..." - lsof -ti:$PORT | xargs kill -9 2>/dev/null - sleep 1 - if lsof -ti:$PORT > /dev/null 2>&1; then - echo "❌ Failed to free port $PORT, using port 8001 instead" - PORT=8001 - else - echo "✅ Port $PORT is now free" - fi - fi -fi - -echo "" -echo "🚀 Starting server..." -echo " Access at: http://localhost:$PORT/web" -echo " Press Ctrl+C to stop" -echo "" - -# Run the server -cd "$(dirname "$0")/../../.." - -# Ensure PYTHONPATH is set for uvicorn (needed for reload mode) -# The issue is that uvicorn's reload mode spawns a new process that needs PYTHONPATH -# We need to set it in a way that's inherited by the subprocess -export PYTHONPATH="${PWD}/src:${PYTHONPATH}" - -# Use python -m uvicorn to ensure PYTHONPATH is respected in reload mode -$PYTHON_CMD -m uvicorn envs.wildfire_env.server.app:app --reload --host 0.0.0.0 --port $PORT - From 9c735e7ba8842e98c01c1f26dc635fa403654c2f Mon Sep 17 00:00:00 2001 From: Ram Harikrishnan Date: Tue, 4 Nov 2025 02:29:06 -0700 Subject: [PATCH 18/19] Remove run_wildfire_docker.sh script and update README - Remove run_wildfire_docker.sh convenience script - Update README to use manual docker commands instead - Replace all references to run_wildfire_docker.sh with manual docker setup - Make documentation consistent with other environments --- run_wildfire_docker.sh | 89 --------------------------------- src/envs/wildfire_env/README.md | 51 ++++++++++++------- 2 files changed, 32 insertions(+), 108 deletions(-) delete mode 100755 run_wildfire_docker.sh diff --git a/run_wildfire_docker.sh b/run_wildfire_docker.sh deleted file mode 100755 index 0ce580f6..00000000 --- a/run_wildfire_docker.sh +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/bash -# Script to run Wildfire Environment with Docker - -set -e - -WIDTH="${WILDFIRE_WIDTH:-32}" -HEIGHT="${WILDFIRE_HEIGHT:-32}" -HUMIDITY="${WILDFIRE_HUMIDITY:-0.25}" -PORT="${PORT:-8000}" -CONTAINER_NAME="wildfire-env-container" - -# Colors for output -GREEN='\033[0;32m' -BLUE='\033[0;34m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}" -echo -e "${BLUE} Wildfire Environment - Docker Runner${NC}" -echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}" -echo "" - -# Step 1: Stop and remove any existing wildfire containers -echo -e "${YELLOW}Step 1: Cleaning up any existing containers...${NC}" -docker stop $(docker ps -aq --filter "name=wildfire") 2>/dev/null || true -docker rm $(docker ps -aq --filter "name=wildfire") 2>/dev/null || true -echo -e "${GREEN}✓ Cleaned up existing containers${NC}" -echo "" - -# Step 2: Check if base image exists, if not build it -if ! docker images -q openenv-base:latest | grep -q .; then - echo -e "${YELLOW}Step 2a: Building base image (openenv-base:latest)...${NC}" - docker build -f src/core/containers/images/Dockerfile -t openenv-base:latest . > /dev/null - echo -e "${GREEN}✓ Base image built successfully${NC}" - echo "" -else - echo -e "${GREEN}✓ Base image exists${NC}" - echo "" -fi - -# Step 3: Rebuild wildfire image to ensure latest code changes are included -echo -e "${YELLOW}Step 2b: Building Wildfire Docker image...${NC}" -docker build -f src/envs/wildfire_env/server/Dockerfile -t wildfire-env:latest . > /dev/null -echo -e "${GREEN}✓ Wildfire image built successfully${NC}" -echo "" - -# Step 4: Start the container -echo -e "${BLUE}Step 4: Starting Wildfire Environment container...${NC}" -echo "" -echo "Configuration:" -echo " Grid Width: $WIDTH" -echo " Grid Height: $HEIGHT" -echo " Humidity: $HUMIDITY" -echo " Port: $PORT" -echo " Web Interface: Enabled" -echo "" - -docker run -d \ - --name $CONTAINER_NAME \ - -p $PORT:8000 \ - -e ENABLE_WEB_INTERFACE=true \ - -e WILDFIRE_WIDTH=$WIDTH \ - -e WILDFIRE_HEIGHT=$HEIGHT \ - -e WILDFIRE_HUMIDITY=$HUMIDITY \ - wildfire-env:latest > /dev/null - -echo -e "${GREEN}✓ Container started successfully!${NC}" -echo "" - -# Step 5: Wait a moment and check status -sleep 2 -echo -e "${BLUE}Container Information:${NC}" -echo " Name: $CONTAINER_NAME" -echo " Status: $(docker ps -f name=$CONTAINER_NAME --format '{{.Status}}')" -echo "" - -# Step 6: Display access information -echo -e "${GREEN}Web Interface: http://localhost:$PORT/web${NC}" -echo "" -echo "Available actions:" -echo " - water: Apply water to a cell (extinguishes fire)" -echo " - break: Create a firebreak (prevents fire spread)" -echo " - wait: Do nothing (fire continues spreading)" -echo "" -echo -e "${BLUE}Showing logs (press Ctrl+C to stop):${NC}" -echo "" - -# Step 7: Show logs -docker logs -f $CONTAINER_NAME diff --git a/src/envs/wildfire_env/README.md b/src/envs/wildfire_env/README.md index 77f7e350..38e025e6 100644 --- a/src/envs/wildfire_env/README.md +++ b/src/envs/wildfire_env/README.md @@ -57,15 +57,6 @@ This makes WildfireEnv a **fast, controllable**, and **open benchmark** for appl ### Using Docker (Recommended) -```bash -# From the OpenEnv root directory -./run_wildfire_docker.sh -``` - -**Note:** The web interface can be enabled with `ENABLE_WEB_INTERFACE=true`. Access it at `http://localhost:8000/web` when enabled. - -Or manually: - ```bash # Build base image (first time only) docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile . @@ -74,9 +65,11 @@ docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile . docker build -t wildfire-env:latest -f src/envs/wildfire_env/server/Dockerfile . # Run container -docker run -p 8000:8000 wildfire-env:latest +docker run -p 8000:8000 -e ENABLE_WEB_INTERFACE=true wildfire-env:latest ``` +**Note:** The web interface can be enabled with `ENABLE_WEB_INTERFACE=true`. Access it at `http://localhost:8000/web` when enabled. + ### Basic Python Client ```python @@ -409,11 +402,18 @@ docker run -p 8000:8000 \ wildfire-env:latest ``` -### Using the Run Script +### Custom Configuration ```bash -# Custom configuration -WILDFIRE_WIDTH=64 WILDFIRE_HEIGHT=64 WILDFIRE_HUMIDITY=0.5 ./run_wildfire_docker.sh +# Build and run with custom configuration +docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile . +docker build -t wildfire-env:latest -f src/envs/wildfire_env/server/Dockerfile . +docker run -p 8000:8000 \ + -e ENABLE_WEB_INTERFACE=true \ + -e WILDFIRE_WIDTH=64 \ + -e WILDFIRE_HEIGHT=64 \ + -e WILDFIRE_HUMIDITY=0.5 \ + wildfire-env:latest ``` --- @@ -422,18 +422,25 @@ WILDFIRE_WIDTH=64 WILDFIRE_HEIGHT=64 WILDFIRE_HUMIDITY=0.5 ./run_wildfire_docker ### Option 1: Docker (Recommended) -**Using the convenience script:** +**Manual setup:** ```bash -./run_wildfire_docker.sh +# Build base image (first time only) +docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile . + +# Build wildfire environment +docker build -t wildfire-env:latest -f src/envs/wildfire_env/server/Dockerfile . + +# Run container +docker run -p 8000:8000 -e ENABLE_WEB_INTERFACE=true wildfire-env:latest ``` -This script: +This approach: - Builds the base image if needed - Rebuilds the wildfire image - Starts the container - Shows logs in real-time -**Manual Docker setup:** +**Alternative: Using build_docker.sh script:** ```bash # Build base image (first time only) docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile . @@ -795,8 +802,14 @@ The Wildfire Environment includes a **custom web interface** with visual grid di #### Using Docker ```bash -# From the OpenEnv root directory -./run_wildfire_docker.sh +# Build base image (first time only) +docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile . + +# Build wildfire environment +docker build -t wildfire-env:latest -f src/envs/wildfire_env/server/Dockerfile . + +# Run container +docker run -p 8000:8000 -e ENABLE_WEB_INTERFACE=true wildfire-env:latest ``` Then open: `http://localhost:8000/web` From a5b7ecff945e16de92d2664010597da06bb6ae20 Mon Sep 17 00:00:00 2001 From: Ram Harikrishnan Date: Tue, 4 Nov 2025 02:29:13 -0700 Subject: [PATCH 19/19] Update README to mention build_docker.sh script alternative --- src/envs/wildfire_env/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/envs/wildfire_env/README.md b/src/envs/wildfire_env/README.md index 38e025e6..3d96fcd5 100644 --- a/src/envs/wildfire_env/README.md +++ b/src/envs/wildfire_env/README.md @@ -445,8 +445,9 @@ This approach: # Build base image (first time only) docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile . -# Build wildfire environment -docker build -t wildfire-env:latest -f src/envs/wildfire_env/server/Dockerfile . +# Build wildfire environment using the script +cd src/envs/wildfire_env/server +./build_docker.sh # Run container docker run -d -p 8000:8000 --name wildfire-env-container wildfire-env:latest