burtenshaw HF Staff commited on
Commit
bb878bf
·
verified ·
1 Parent(s): 950923c

Upload folder using huggingface_hub

Browse files
src/envs/wildfire_env/README.md CHANGED
@@ -9,6 +9,27 @@ Agents must contain spreading fires using **water**, **firebreaks**, and **timin
9
  [![License](https://img.shields.io/badge/license-MIT-lightgrey)](LICENSE)
10
 
11
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  ## 🔥 Why Wildfire Simulation?
13
 
14
  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
17
  ### Research Motivation
18
  ✅ Based on real wildfire science inspired by:
19
  - **Rothermel Surface Fire Spread Model** (USDA Forest Service)
20
- - **MITRE Firelines SimFire** — physics-informed RL fire simulator
21
  - **SimHarness** — RL evaluation for disaster response
22
 
23
  ### Application Goals
@@ -32,157 +53,505 @@ This makes WildfireEnv a **fast, controllable**, and **open benchmark** for appl
32
 
33
  ---
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  ## 🔥 Environment Overview
36
 
37
  This environment models **forest-fire dynamics** influenced by:
38
- - **Wind direction** (8 directions + calm)
39
- - **Humidity** (suppresses ignition)
40
- - **Fuel type and spread rate**
41
- - **Limited resources** (water units, break materials)
42
- - **Time pressure** (each step costs reward)
43
 
44
  The goal is to **minimize fire spread** and **total burned area** while using resources efficiently.
45
 
 
 
 
 
 
 
46
  ---
47
 
48
- ## 🧱 Grid Encoding
49
 
50
- | Code | Meaning | Color (Visualization) |
51
- |------|----------------|-----------------------|
52
- | 0 | Ash (burned) | Black ⚫ |
53
- | 1 | Fuel | Green 🟩 |
54
- | 2 | Burning | Red 🔥 |
55
- | 3 | Firebreak | Brown 🟫 |
56
- | 4 | Water/Damp | Blue 🔵 |
57
 
58
- ---
59
 
60
- ## ⚙️ Architecture
 
 
 
61
 
 
 
 
 
62
  ```
63
- ┌────────────────────────────────────────────┐
64
- RL Agent / LLM Trainer (Client) │
65
- │ wildfire_env.step(WildfireAction(...)) │
66
- └──────────────────┬─────────────────────────┘
67
- │ HTTP
68
- ┌──────────────────▼─────────────────────────┐
69
- FastAPI Server (Docker) │
70
- │ WildfireEnvironment │
71
- │ ├─ Handles wind, humidity, spread │
72
- ├─ Applies agent actions │
73
- │ ├─ Updates grid + reward shaping │
74
- │ └─ Returns WildfireObservation │
75
- └────────────────────────────────────────────┘
 
 
 
 
 
 
 
 
76
  ```
77
 
78
  ---
79
 
80
- ## 🚀 Installation & Usage
81
 
82
- ### Option 1: Local Development (no Docker)
83
 
84
- **Requirements:**
85
- - Python 3.10 +
86
- - FastAPI + Uvicorn
87
- - NumPy + Matplotlib (for visualization)
88
 
89
- ```bash
90
- pip install fastapi uvicorn numpy matplotlib requests
91
  ```
92
 
93
- Run server locally:
94
- ```bash
95
- python -m envs.wildfire_env.server.app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  ```
97
 
98
- Client usage:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  ```python
100
- from envs.wildfire_env import WildfireEnv, WildfireAction
 
101
 
102
- env = WildfireEnv(base_url="http://localhost:8000")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  result = env.reset()
105
- print(f"🔥 Fires: {result.observation.burning_count}, 💧 Water left: {result.observation.remaining_water}")
 
 
 
 
 
 
 
 
 
 
106
 
107
- for _ in range(5):
108
- result = env.step(WildfireAction(action="water", x=10, y=10))
109
- print(f"Reward: {result.reward}, Burning left: {result.observation.burning_count}")
110
 
111
- env.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  ```
113
 
114
  ---
115
 
116
- ### Option 2: Docker (Recommended)
 
 
117
 
118
- Build the image:
119
  ```bash
120
- cd OpenEnv
121
- docker build -f src/envs/wildfire_env/server/Dockerfile -t wildfire-env:latest .
122
  ```
123
 
124
- Run the container:
 
 
 
 
 
 
125
  ```bash
126
- docker run -p 8000:8000 wildfire-env:latest
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  ```
128
 
129
- Connect via client:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  ```python
131
- from envs.wildfire_env import WildfireEnv, WildfireAction
 
 
 
 
 
132
  env = WildfireEnv.from_docker_image("wildfire-env:latest")
 
 
 
 
 
 
 
 
 
133
  result = env.reset()
134
- print(f"Active fires: {result.observation.burning_count}")
135
- result = env.step(WildfireAction(action="break", x=8, y=12))
136
- print(f"Reward: {result.reward}")
137
- env.close()
138
  ```
139
 
140
- ---
 
 
 
 
 
 
 
 
141
 
142
- ## 🌦️ Configuration
143
 
144
- | Variable | Description | Default |
145
- |-----------|--------------|----------|
146
- | `WILDFIRE_WIDTH` | Grid width | 32 |
147
- | `WILDFIRE_HEIGHT` | Grid height | 32 |
148
- | `WILDFIRE_HUMIDITY` | Initial humidity [0–1] | 0.25 |
149
- | `WILDFIRE_WIND` | Wind direction (`N`, `NE`, `E`, `SE`, `S`, `SW`, `W`, `NW`, `CALM`) | Random |
150
- | `WILDFIRE_SEED` | RNG seed | 3407 |
151
- | `WILDFIRE_MAX_STEPS` | Max steps per episode | 128 |
152
- | `WILDFIRE_WATER_CAPACITY` | Water units available | 8 |
153
- | `WILDFIRE_BREAK_CAPACITY` | Firebreak materials | 50 |
154
 
155
- ---
 
 
 
 
 
156
 
157
- ## 🧠 API Reference
 
 
 
 
 
 
 
 
 
 
158
 
159
- ### `WildfireAction`
160
  ```python
161
  @dataclass
162
  class WildfireAction(Action):
163
  action: str # "water" | "break" | "wait"
164
- x: Optional[int] = None # Target X
165
- y: Optional[int] = None # Target Y
166
  ```
167
 
168
- ### `WildfireObservation`
169
  ```python
170
- @dataclass
171
- class WildfireObservation(Observation):
172
- grid: List[int]
173
- width: int
174
- height: int
175
- step: int
176
- wind_dir: str
177
- humidity: float
178
- burning_count: int
179
- burned_count: int
180
- remaining_water: int
181
- remaining_breaks: int
182
- reward_hint: float
183
  ```
184
 
185
- ### `WildfireState`
 
 
 
 
 
186
  ```python
187
  @dataclass
188
  class WildfireState(State):
@@ -190,29 +559,130 @@ class WildfireState(State):
190
  step_count: int
191
  total_burned: int
192
  total_extinguished: int
193
- remaining_water: int
194
- remaining_breaks: int
 
195
  wind_dir: str
196
  humidity: float
 
 
 
 
197
  ```
198
 
199
  ---
200
- ## Sample rendering to see wildfire simulation
201
 
202
- **Note:** This example requires Jupyter notebook or IPython environment for the `clear_output` and `display` functions. For standalone Python scripts, see `examples/wildfire.py`.
 
 
203
 
204
  ```python
205
- import matplotlib.pyplot as plt
206
  import numpy as np
207
- import time
208
 
209
- from IPython.display import clear_output, display
210
- import matplotlib.colors as mcolors
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  from envs.wildfire_env import WildfireEnv, WildfireAction
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
- client = WildfireEnv("http://localhost:8000")
 
 
 
 
214
 
 
 
 
215
 
 
216
  cmap = mcolors.ListedColormap([
217
  "black", # 0 = ash
218
  "green", # 1 = fuel
@@ -220,122 +690,282 @@ cmap = mcolors.ListedColormap([
220
  "saddlebrown", # 3 = firebreak
221
  "blue" # 4 = water
222
  ])
223
-
224
  norm = mcolors.BoundaryNorm([0, 1, 2, 3, 4, 5], cmap.N)
225
 
 
 
226
 
227
- plt.ion()
228
- fig, ax = plt.subplots(figsize=(5, 5))
229
- plt.axis("off")
230
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
 
232
- res = client.reset()
233
- obs = res.observation
234
- grid = np.array(obs.grid).reshape(obs.height, obs.width)
 
235
 
 
236
 
237
- im = ax.imshow(grid, cmap=cmap, norm=norm)
 
 
238
 
 
239
 
240
- title_text = ax.set_title(
241
- f"Step {obs.step} | Burning={obs.burning_count} | Burned={obs.burned_count}\n"
242
- f"Wind={obs.wind_dir} | Humidity={obs.humidity:.2f}",
243
- color="black",
244
- fontsize=10
245
- )
246
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
 
 
 
248
 
249
- print("Starting smooth animation...")
250
- for _ in range(100):
251
- clear_output(wait=True)
252
 
253
- new_grid = np.array(obs.grid).reshape(obs.height, obs.width)
254
 
255
- im.set_data(new_grid)
256
 
257
- title_text.set_text(
258
- f"Step {obs.step} | Burning={obs.burning_count} | Burned={obs.burned_count}\n"
259
- f"Wind={obs.wind_dir} | Humidity={obs.humidity:.2f}"
260
- )
261
 
262
-
263
- display(fig)
264
-
265
-
266
- time.sleep(0.3)
267
 
268
-
269
- res = client.step(WildfireAction(action="wait"))
270
- obs = res.observation
271
 
272
- if obs.burning_count == 0:
273
- print(f"🔥 Fire has fully burned out after {obs.step} steps.")
274
- break
 
 
 
275
 
276
- plt.ioff() # Turn off interactive mode
277
- plt.close(fig) # Close the figure at the end
278
- print("Animation complete.")
279
 
280
- ```
 
 
 
 
 
 
281
 
282
  ---
283
 
 
284
 
285
- ## 🧪 Example Training Loop (GRPO/LLM)
286
 
287
- ```python
288
- from envs.wildfire_env import WildfireEnv, WildfireAction
289
- import random
290
 
291
- env = WildfireEnv.from_docker_image("wildfire-env:latest")
292
 
293
- for episode in range(3):
294
- result = env.reset()
295
- total_reward = 0
 
296
 
297
- while not result.done:
298
- a = random.choice(["water", "break", "wait"])
299
- x, y = random.randint(0, 15), random.randint(0, 15)
300
- result = env.step(WildfireAction(action=a, x=x, y=y))
301
- total_reward += result.reward or 0
302
 
303
- print(f"Episode {episode}: total_reward={total_reward:.2f}")
304
 
305
- env.close()
 
 
 
 
306
  ```
307
 
308
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
 
310
- ## 🧰 DockerHub & GitHub Build
 
 
 
 
 
311
 
312
- Build and push:
313
 
 
314
  ```bash
 
315
  docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile .
316
- docker build -t ghcr.io/<your_username>/openenv-wildfire:latest -f src/envs/wildfire_env/server/Dockerfile .
317
- docker push ghcr.io/<your_username>/openenv-wildfire:latest
318
- ```
319
 
320
- GitHub Action matrix entry:
321
- ```yaml
322
- strategy:
323
- matrix:
324
- image:
325
- - name: wildfire-env
326
- dockerfile: src/envs/wildfire_env/server/Dockerfile
327
  ```
328
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  ---
330
 
331
  ## 🧭 References
332
 
333
- - [OpenEnv Framework](https://github.com/openenv)
334
- - [FastAPI Documentation](https://fastapi.tiangolo.com/)
335
- - [Reinforcement Learning Introduction](https://spinningup.openai.com/en/latest/)
336
- - [Fire Spread Simulation Models (USFS Research)](https://www.fs.fed.us/rm/pubs/rmrs_gtr371.html)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
 
338
  ---
 
339
  ## 🔖 Citations
340
 
341
  ```bibtex
@@ -365,8 +995,12 @@ strategy:
365
 
366
  @misc{wildfire-openenv-2025,
367
  title = {Wildfire Environment for OpenEnv: Containment-Focused RL Simulation},
368
- author = {Harikrishnan, Ram Sankar},
369
  year = {2025},
370
- url = {https://github.com/<your_username>/openenv-wildfire}
371
  }
372
  ```
 
 
 
 
 
9
  [![License](https://img.shields.io/badge/license-MIT-lightgrey)](LICENSE)
10
 
11
  ---
12
+
13
+ ## 📋 Table of Contents
14
+
15
+ 1. [Why Wildfire Simulation?](#-why-wildfire-simulation)
16
+ 2. [Quick Start](#-quick-start)
17
+ 3. [Environment Overview](#-environment-overview)
18
+ 4. [Grid Format & Encoding](#-grid-format--encoding)
19
+ 5. [Actions](#-actions)
20
+ 6. [Observations](#-observations)
21
+ 7. [Reward Structure](#-reward-structure)
22
+ 8. [Fire Spread Mechanics](#-fire-spread-mechanics)
23
+ 9. [Configuration](#-configuration)
24
+ 10. [Installation & Usage](#-installation--usage)
25
+ 11. [API Reference](#-api-reference)
26
+ 12. [Examples](#-examples)
27
+ 13. [Web Interface](#-web-interface)
28
+ 14. [Troubleshooting](#-troubleshooting)
29
+ 15. [References](#-references)
30
+
31
+ ---
32
+
33
  ## 🔥 Why Wildfire Simulation?
34
 
35
  Wildland fires are intensifying globally due to climate change — increasing the urgency for **AI-assisted decision-making**.
 
38
  ### Research Motivation
39
  ✅ Based on real wildfire science inspired by:
40
  - **Rothermel Surface Fire Spread Model** (USDA Forest Service)
41
+ - **MITRE Fireline's SimFire** — physics-informed RL fire simulator
42
  - **SimHarness** — RL evaluation for disaster response
43
 
44
  ### Application Goals
 
53
 
54
  ---
55
 
56
+ ## 🚀 Quick Start
57
+
58
+ ### Using Docker (Recommended)
59
+
60
+ ```bash
61
+ # From the OpenEnv root directory
62
+ ./run_wildfire_docker.sh
63
+ ```
64
+
65
+ Or manually:
66
+
67
+ ```bash
68
+ # Build base image (first time only)
69
+ docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile .
70
+
71
+ # Build wildfire environment
72
+ docker build -t wildfire-env:latest -f src/envs/wildfire_env/server/Dockerfile .
73
+
74
+ # Run container
75
+ docker run -p 8000:8000 wildfire-env:latest
76
+ ```
77
+
78
+ ### Basic Python Client
79
+
80
+ ```python
81
+ from envs.wildfire_env import WildfireEnv, WildfireAction
82
+
83
+ # Connect to running server
84
+ env = WildfireEnv(base_url="http://localhost:8000")
85
+
86
+ # Reset environment
87
+ result = env.reset()
88
+ obs = result.observation
89
+ print(f"Grid: {obs.width}x{obs.height}, Fires: {obs.burning_count}, Water: {obs.remaining_water}")
90
+
91
+ # Take action (water a burning cell)
92
+ result = env.step(WildfireAction(action="water", x=10, y=15))
93
+ print(f"Reward: {result.reward:.2f}, Burning: {result.observation.burning_count}")
94
+
95
+ # Create firebreak
96
+ result = env.step(WildfireAction(action="break", x=12, y=15))
97
+
98
+ # Wait (fire spreads)
99
+ result = env.step(WildfireAction(action="wait"))
100
+
101
+ env.close()
102
+ ```
103
+
104
+ ---
105
+
106
  ## 🔥 Environment Overview
107
 
108
  This environment models **forest-fire dynamics** influenced by:
109
+ - **Wind direction** (8 directions + calm) - accelerates fire spread in wind direction
110
+ - **Humidity** (0.0-1.0) - suppresses ignition probability
111
+ - **Fuel type and spread rate** - vegetation burns and spreads to neighbors
112
+ - **Limited resources** (water units, break materials) - strategic resource management
113
+ - **Time pressure** (each step costs small reward penalty)
114
 
115
  The goal is to **minimize fire spread** and **total burned area** while using resources efficiently.
116
 
117
+ ### Episode Termination
118
+
119
+ An episode ends when:
120
+ - **All fires are extinguished** (`burning_count == 0`) - **Success!**
121
+ - **Maximum steps reached** (`step_count >= max_steps`) - Time limit exceeded
122
+
123
  ---
124
 
125
+ ## 🧱 Grid Format & Encoding
126
 
127
+ ### Grid Structure
 
 
 
 
 
 
128
 
129
+ The grid is returned as a **flat 1D array** in the observation. To access cell at position `(x, y)`:
130
 
131
+ ```python
132
+ index = y * width + x
133
+ cell_value = observation.grid[index]
134
+ ```
135
 
136
+ **Example:** For a 32×32 grid, cell at (10, 15):
137
+ ```python
138
+ index = 15 * 32 + 10 # = 490
139
+ cell_value = observation.grid[490]
140
  ```
141
+
142
+ ### Cell Encoding
143
+
144
+ | Code | Meaning | Color (Visualization) | Behavior |
145
+ |------|----------------|-----------------------|----------|
146
+ | `0` | Ash (burned) | Black ⚫ | Burned out, cannot reignite |
147
+ | `1` | Fuel | Green 🟩 | Healthy vegetation, can ignite |
148
+ | `2` | Burning | Red 🔥 | Currently on fire, spreads to neighbors |
149
+ | `3` | Firebreak | Brown 🟫 | Barrier, fire cannot cross |
150
+ | `4` | Water/Damp | Blue 🔵 | Dampened, immune to ignition temporarily |
151
+
152
+ ### Grid Visualization Example
153
+
154
+ ```python
155
+ import numpy as np
156
+
157
+ obs = env.reset().observation
158
+ grid_2d = np.array(obs.grid).reshape(obs.height, obs.width)
159
+
160
+ # Now grid_2d[y][x] gives the cell value at position (x, y)
161
+ print(grid_2d[15][10]) # Cell at x=10, y=15
162
  ```
163
 
164
  ---
165
 
166
+ ## 🎮 Actions
167
 
168
+ ### Action Types
169
 
170
+ #### 1. `water` - Apply Water
171
+ **Extinguishes burning cells and dampens fuel to prevent ignition.**
 
 
172
 
173
+ ```python
174
+ WildfireAction(action="water", x=10, y=15)
175
  ```
176
 
177
+ **Effects:**
178
+ - **Burning cell (2)**: Extinguishes → becomes Water/Damp (4), gives **+0.25 reward**
179
+ - **Fuel cell (1)**: Dampens → becomes Water/Damp (4), gives **-0.10 reward** (preventive, slight penalty)
180
+ - **Water/Damp cell (4)**: Redundant watering, gives **-0.05 reward**
181
+ - **Ash/Break (0, 3)**: Wasteful, gives **-0.05 reward**
182
+
183
+ **Resource Cost:** 1 water unit per action
184
+ **Requires:** `remaining_water > 0` and valid coordinates
185
+
186
+ **Best Use:** Extinguish active fires before they spread
187
+
188
+ ---
189
+
190
+ #### 2. `break` - Create Firebreak
191
+ **Builds a fire-resistant barrier that stops fire spread.**
192
+
193
+ ```python
194
+ WildfireAction(action="break", x=12, y=15)
195
  ```
196
 
197
+ **Effects:**
198
+ - **Fuel/Water cell (1, 4)**: Creates firebreak → becomes Firebreak (3), gives **+0.15 reward**
199
+ - **Burning cell (2)**: Extinguishes → becomes Firebreak (3), gives **-0.02 reward** (less effective than water)
200
+ - **Firebreak (3)**: Redundant, gives **-0.01 reward**
201
+ - **Ash (0)**: Wasteful, gives **-0.02 reward**
202
+
203
+ **Resource Cost:** 1 firebreak material per action
204
+ **Requires:** `remaining_breaks > 0` and valid coordinates
205
+
206
+ **Best Use:** Create barriers ahead of fire front to contain spread
207
+
208
+ ---
209
+
210
+ #### 3. `wait` - Do Nothing
211
+ **Let natural fire dynamics occur (fire spreads).**
212
+
213
  ```python
214
+ WildfireAction(action="wait")
215
+ ```
216
 
217
+ **Effects:**
218
+ - No resource cost
219
+ - No coordinate required
220
+ - Fire spreads naturally to neighboring cells
221
+ - Small time penalty (-0.01 reward per step)
222
+
223
+ **Best Use:** When fire is contained, waiting for it to burn out
224
+
225
+ ---
226
+
227
+ ### Invalid Actions
228
+
229
+ Actions that fail (give **-0.05 reward**):
230
+ - Invalid coordinates (out of bounds)
231
+ - Using water when `remaining_water == 0`
232
+ - Using break when `remaining_breaks == 0`
233
+ - Missing required coordinates for water/break actions
234
+
235
+ ---
236
+
237
+ ## 👁️ Observations
238
+
239
+ ### `WildfireObservation`
240
+
241
+ Returned after every `reset()` or `step()`:
242
 
243
+ ```python
244
+ @dataclass
245
+ class WildfireObservation(Observation):
246
+ grid: List[int] # Flat array: [1,1,2,1,...] length = width × height
247
+ width: int # Grid width (default: 32)
248
+ height: int # Grid height (default: 32)
249
+ step: int # Current step number (0 at reset)
250
+ wind_dir: str # "N", "NE", "E", "SE", "S", "SW", "W", "NW", "CALM"
251
+ humidity: float # [0.0, 1.0] - higher = less fire spread
252
+ burning_count: int # Number of cells currently on fire
253
+ burned_count: int # Total number of ash cells (cumulative)
254
+ remaining_water: int # Water units left
255
+ remaining_breaks: int # Firebreak materials left
256
+ reward_hint: float # Shaping reward (for debugging)
257
+ done: bool # Episode ended?
258
+ reward: float # Step reward
259
+ ```
260
+
261
+ ### Example Observation
262
+
263
+ ```python
264
  result = env.reset()
265
+ obs = result.observation
266
+
267
+ print(f"Step: {obs.step}") # 0
268
+ print(f"Grid size: {obs.width}x{obs.height}") # 32x32
269
+ print(f"Grid cells: {len(obs.grid)}") # 1024
270
+ print(f"Active fires: {obs.burning_count}") # 2
271
+ print(f"Wind: {obs.wind_dir}") # "NE"
272
+ print(f"Humidity: {obs.humidity:.2f}") # 0.24
273
+ print(f"Water left: {obs.remaining_water}") # 8
274
+ print(f"Breaks left: {obs.remaining_breaks}") # 50
275
+ ```
276
 
277
+ ---
 
 
278
 
279
+ ## 💰 Reward Structure
280
+
281
+ ### Step Rewards
282
+
283
+ | Action | Condition | Reward |
284
+ |--------|-----------|--------|
285
+ | **Water burning cell** | Extinguishes fire | **+0.25** |
286
+ | **Water fuel cell** | Preventive dampening | **-0.10** |
287
+ | **Create firebreak** | From fuel/water | **+0.15** |
288
+ | **Fire spreads** | Each new burning cell | **-0.15 per cell** |
289
+ | **Fire shrinks** | Each extinguished cell | **+0.10 per cell** |
290
+ | **New burned area** | Each cell turns to ash | **-0.05 per cell** |
291
+ | **Time penalty** | Every step | **-0.01** |
292
+ | **Invalid action** | Out of bounds, no resources | **-0.05** |
293
+ | **Redundant action** | Watering already damp cell | **-0.05** |
294
+
295
+ ### Episode End Bonuses
296
+
297
+ When episode terminates (`done == True`):
298
+
299
+ - **Fire contained** (`burning_count == 0`):
300
+ - **+0.5** base bonus
301
+ - **+0.5 × saved_ratio** bonus (proportion of cells not burned)
302
+
303
+ - **Fallback reward**:
304
+ - **+0.2 × (1.0 - burned_ratio)** bonus
305
+
306
+ **Example:** Perfect containment (no burned cells):
307
+ ```python
308
+ Reward = +0.5 + 0.5 × 1.0 = +1.0
309
+ ```
310
+
311
+ ### Reward Interpretation
312
+
313
+ - **Positive rewards**: Good containment actions, extinguishing fires
314
+ - **Negative rewards**: Fire spread, resource waste, time penalty
315
+ - **Goal**: Maximize cumulative reward = minimize fire damage
316
+
317
+ ---
318
+
319
+ ## 🌪️ Fire Spread Mechanics
320
+
321
+ ### Spread Model
322
+
323
+ Fire spreads using an **8-directional neighbor model**:
324
+
325
+ 1. **Burning cells persist** for `burn_lifetime = 3` ticks before turning to ash
326
+ 2. Each burning cell can ignite **neighboring fuel cells** (8 directions)
327
+ 3. Spread probability depends on:
328
+ - **Base ignition probability**: `0.30` (30% chance)
329
+ - **Humidity factor**: `(1.0 - humidity)` - higher humidity = less spread
330
+ - **Wind multiplier**:
331
+ - **+2.0x** in wind direction
332
+ - **+0.5x** against wind
333
+ - **+1.0x** perpendicular
334
+ - **Diagonal factor**: `0.6x` for diagonal neighbors (slower spread)
335
+
336
+ 4. **Water/Damp cells (4)** are **immune** to ignition while damp
337
+ 5. **Firebreaks (3)** **cannot** be crossed by fire
338
+ 6. **Ash cells (0)** cannot reignite
339
+
340
+ ### Wind Effects
341
+
342
+ | Wind Direction | Effect on Fire Spread |
343
+ |----------------|----------------------|
344
+ | **In wind direction** | 2× faster ignition probability |
345
+ | **Against wind** | 0.5× slower ignition probability |
346
+ | **Perpendicular** | Normal (1×) ignition probability |
347
+ | **CALM** | No directional bias |
348
+
349
+ ### Water Dampening Duration
350
+
351
+ Watered cells (4) remain damp for **6 ticks** before reverting to fuel (1).
352
+
353
+ ### Example Fire Spread
354
+
355
+ ```
356
+ Step 0: Step 1: Step 2:
357
+ 🟩🟩🟩 🟩🟥🟩 🟫🟥🟫
358
+ 🟩🟥🟩 → 🟥🟥🟥 → 🟥🟥🟥 (Wind: E, spreading east)
359
+ 🟩🟩🟩 🟩🟥🟩 🟫🟥🟫
360
+ ```
361
+
362
+ ---
363
+
364
+ ## ⚙️ Configuration
365
+
366
+ ### Environment Variables
367
+
368
+ Set these **before starting the server**:
369
+
370
+ | Variable | Description | Default | Range |
371
+ |-----------|-------------|---------|-------|
372
+ | `WILDFIRE_WIDTH` | Grid width in cells | `32` | 8-128 |
373
+ | `WILDFIRE_HEIGHT` | Grid height in cells | `32` | 8-128 |
374
+ | `WILDFIRE_HUMIDITY` | Initial humidity level | `0.25` | 0.0-1.0 |
375
+ | `WILDFIRE_WIND` | Wind direction (fixed) | Random | `N`, `NE`, `E`, `SE`, `S`, `SW`, `W`, `NW`, `CALM` |
376
+ | `WILDFIRE_SEED` | Random seed | `3407` | Any integer |
377
+ | `WILDFIRE_MAX_STEPS` | Max steps per episode | `128` | 10-1000 |
378
+ | `WILDFIRE_WATER_CAPACITY` | Initial water units | `8` | 1-100 |
379
+ | `WILDFIRE_BREAK_CAPACITY` | Initial firebreak materials | `50` | 1-200 |
380
+
381
+ ### Python API Configuration
382
+
383
+ ```python
384
+ from envs.wildfire_env.server.wildfire_environment import WildfireEnvironment
385
+
386
+ env = WildfireEnvironment(
387
+ width=64,
388
+ height=64,
389
+ humidity=0.3,
390
+ init_sources=3, # Number of initial fires
391
+ max_steps=200,
392
+ water_capacity=10,
393
+ break_capacity=75,
394
+ seed=42
395
+ )
396
+ ```
397
+
398
+ ### Docker Configuration
399
+
400
+ ```bash
401
+ docker run -p 8000:8000 \
402
+ -e WILDFIRE_WIDTH=64 \
403
+ -e WILDFIRE_HEIGHT=64 \
404
+ -e WILDFIRE_HUMIDITY=0.4 \
405
+ -e WILDFIRE_WIND=N \
406
+ -e WILDFIRE_WATER_CAPACITY=12 \
407
+ wildfire-env:latest
408
+ ```
409
+
410
+ ### Using the Run Script
411
+
412
+ ```bash
413
+ # Custom configuration
414
+ WILDFIRE_WIDTH=64 WILDFIRE_HEIGHT=64 WILDFIRE_HUMIDITY=0.5 ./run_wildfire_docker.sh
415
  ```
416
 
417
  ---
418
 
419
+ ## 🚀 Installation & Usage
420
+
421
+ ### Option 1: Docker (Recommended)
422
 
423
+ **Using the convenience script:**
424
  ```bash
425
+ ./run_wildfire_docker.sh
 
426
  ```
427
 
428
+ This script:
429
+ - Builds the base image if needed
430
+ - Rebuilds the wildfire image
431
+ - Starts the container
432
+ - Shows logs in real-time
433
+
434
+ **Manual Docker setup:**
435
  ```bash
436
+ # Build base image (first time only)
437
+ docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile .
438
+
439
+ # Build wildfire environment
440
+ docker build -t wildfire-env:latest -f src/envs/wildfire_env/server/Dockerfile .
441
+
442
+ # Run container
443
+ docker run -d -p 8000:8000 --name wildfire-env-container wildfire-env:latest
444
+
445
+ # View logs
446
+ docker logs -f wildfire-env-container
447
+
448
+ # Stop container
449
+ docker stop wildfire-env-container
450
+
451
+ # Remove container
452
+ docker rm wildfire-env-container
453
+ ```
454
+
455
+ ### Option 2: Local Development (No Docker)
456
+
457
+ **Requirements:**
458
+ ```bash
459
+ pip install fastapi uvicorn numpy matplotlib requests
460
  ```
461
 
462
+ **Run server:**
463
+ ```bash
464
+ # From OpenEnv root directory
465
+ python -m envs.wildfire_env.server.app
466
+ ```
467
+
468
+ **Or with environment variables:**
469
+ ```bash
470
+ WILDFIRE_WIDTH=64 WILDFIRE_HUMIDITY=0.3 python -m envs.wildfire_env.server.app
471
+ ```
472
+
473
+ ---
474
+
475
+ ## 📚 API Reference
476
+
477
+ ### Client Class
478
+
479
  ```python
480
+ from envs.wildfire_env import WildfireEnv
481
+
482
+ # Connect to existing server
483
+ env = WildfireEnv(base_url="http://localhost:8000")
484
+
485
+ # Or create from Docker image
486
  env = WildfireEnv.from_docker_image("wildfire-env:latest")
487
+ ```
488
+
489
+ ### Methods
490
+
491
+ #### `reset() -> StepResult[WildfireObservation]`
492
+
493
+ Resets the environment to initial state.
494
+
495
+ ```python
496
  result = env.reset()
497
+ obs = result.observation
498
+ print(f"New episode: {obs.step == 0}")
 
 
499
  ```
500
 
501
+ #### `step(action: WildfireAction) -> StepResult[WildfireObservation]`
502
+
503
+ Takes an action and returns new observation.
504
+
505
+ ```python
506
+ action = WildfireAction(action="water", x=10, y=15)
507
+ result = env.step(action)
508
+ print(f"Reward: {result.reward}, Done: {result.done}")
509
+ ```
510
 
511
+ #### `state -> WildfireState`
512
 
513
+ Access current environment state.
 
 
 
 
 
 
 
 
 
514
 
515
+ ```python
516
+ state = env.state
517
+ print(f"Episode ID: {state.episode_id}")
518
+ print(f"Total burned: {state.total_burned}")
519
+ print(f"Total extinguished: {state.total_extinguished}")
520
+ ```
521
 
522
+ #### `close()`
523
+
524
+ Closes the connection (for HTTP clients, this is a no-op but good practice).
525
+
526
+ ```python
527
+ env.close()
528
+ ```
529
+
530
+ ### Data Classes
531
+
532
+ #### `WildfireAction`
533
 
 
534
  ```python
535
  @dataclass
536
  class WildfireAction(Action):
537
  action: str # "water" | "break" | "wait"
538
+ x: Optional[int] = None # Target X coordinate (required for water/break)
539
+ y: Optional[int] = None # Target Y coordinate (required for water/break)
540
  ```
541
 
542
+ **Examples:**
543
  ```python
544
+ WildfireAction(action="water", x=10, y=15)
545
+ WildfireAction(action="break", x=12, y=15)
546
+ WildfireAction(action="wait") # x, y not needed
 
 
 
 
 
 
 
 
 
 
547
  ```
548
 
549
+ #### `WildfireObservation`
550
+
551
+ See [Observations](#-observations) section for full details.
552
+
553
+ #### `WildfireState`
554
+
555
  ```python
556
  @dataclass
557
  class WildfireState(State):
 
559
  step_count: int
560
  total_burned: int
561
  total_extinguished: int
562
+ last_action: str
563
+ width: int
564
+ height: int
565
  wind_dir: str
566
  humidity: float
567
+ remaining_water: int
568
+ remaining_breaks: int
569
+ grid: List[int]
570
+ burn_timers: List[int]
571
  ```
572
 
573
  ---
 
574
 
575
+ ## 📖 Examples
576
+
577
+ ### Example 1: Simple Containment Strategy
578
 
579
  ```python
580
+ from envs.wildfire_env import WildfireEnv, WildfireAction
581
  import numpy as np
 
582
 
583
+ env = WildfireEnv(base_url="http://localhost:8000")
584
+ result = env.reset()
585
+ obs = result.observation
586
+
587
+ grid_2d = np.array(obs.grid).reshape(obs.height, obs.width)
588
+ total_reward = 0
589
+
590
+ while not result.done:
591
+ # Find burning cells
592
+ burning_indices = np.where(grid_2d == 2)
593
+
594
+ if len(burning_indices[0]) > 0 and obs.remaining_water > 0:
595
+ # Water the first burning cell
596
+ y, x = burning_indices[0][0], burning_indices[1][0]
597
+ action = WildfireAction(action="water", x=int(x), y=int(y))
598
+ else:
599
+ # Wait if no water or no fires
600
+ action = WildfireAction(action="wait")
601
+
602
+ result = env.step(action)
603
+ obs = result.observation
604
+ total_reward += result.reward or 0
605
+
606
+ # Update grid
607
+ grid_2d = np.array(obs.grid).reshape(obs.height, obs.width)
608
+
609
+ print(f"Step {obs.step}: Burning={obs.burning_count}, Reward={result.reward:.3f}")
610
+
611
+ print(f"\nEpisode ended. Total reward: {total_reward:.2f}")
612
+ print(f"Final stats: Burned={obs.burned_count}, Extinguished={env.state.total_extinguished}")
613
+ env.close()
614
+ ```
615
+
616
+ ### Example 2: Firebreak Strategy
617
+
618
+ ```python
619
  from envs.wildfire_env import WildfireEnv, WildfireAction
620
+ import numpy as np
621
+
622
+ env = WildfireEnv(base_url="http://localhost:8000")
623
+ result = env.reset()
624
+ obs = result.observation
625
+
626
+ def create_firebreak_barrier(obs, env):
627
+ """Create firebreak ahead of fire front based on wind direction."""
628
+ grid_2d = np.array(obs.grid).reshape(obs.height, obs.width)
629
+ wind = obs.wind_dir
630
+
631
+ # Find burning cells
632
+ burning_y, burning_x = np.where(grid_2d == 2)
633
+
634
+ if len(burning_x) == 0 or obs.remaining_breaks == 0:
635
+ return WildfireAction(action="wait")
636
+
637
+ # Calculate fire front position
638
+ if wind == "E":
639
+ target_x = int(np.max(burning_x)) + 2 # Ahead of easternmost fire
640
+ target_y = int(np.mean(burning_y))
641
+ elif wind == "W":
642
+ target_x = int(np.min(burning_x)) - 2
643
+ target_y = int(np.mean(burning_y))
644
+ elif wind == "N":
645
+ target_x = int(np.mean(burning_x))
646
+ target_y = int(np.min(burning_y)) - 2
647
+ elif wind == "S":
648
+ target_x = int(np.mean(burning_x))
649
+ target_y = int(np.max(burning_y)) + 2
650
+ else:
651
+ # Fallback: water nearest burning cell
652
+ return WildfireAction(action="water", x=int(burning_x[0]), y=int(burning_y[0]))
653
+
654
+ # Ensure within bounds
655
+ target_x = max(0, min(obs.width - 1, target_x))
656
+ target_y = max(0, min(obs.height - 1, target_y))
657
+
658
+ return WildfireAction(action="break", x=target_x, y=target_y)
659
+
660
+ total_reward = 0
661
+ while not result.done:
662
+ action = create_firebreak_barrier(obs, env)
663
+ result = env.step(action)
664
+ obs = result.observation
665
+ total_reward += result.reward or 0
666
+
667
+ if obs.step % 10 == 0:
668
+ print(f"Step {obs.step}: Fires={obs.burning_count}, Water={obs.remaining_water}, Breaks={obs.remaining_breaks}")
669
+
670
+ env.close()
671
+ ```
672
+
673
+ ### Example 3: Visualization with Matplotlib
674
 
675
+ ```python
676
+ import matplotlib.pyplot as plt
677
+ import numpy as np
678
+ import matplotlib.colors as mcolors
679
+ from envs.wildfire_env import WildfireEnv, WildfireAction
680
 
681
+ env = WildfireEnv(base_url="http://localhost:8000")
682
+ result = env.reset()
683
+ obs = result.observation
684
 
685
+ # Setup colormap
686
  cmap = mcolors.ListedColormap([
687
  "black", # 0 = ash
688
  "green", # 1 = fuel
 
690
  "saddlebrown", # 3 = firebreak
691
  "blue" # 4 = water
692
  ])
 
693
  norm = mcolors.BoundaryNorm([0, 1, 2, 3, 4, 5], cmap.N)
694
 
695
+ fig, ax = plt.subplots(figsize=(8, 8))
696
+ plt.ion()
697
 
698
+ for step in range(50):
699
+ if result.done:
700
+ break
701
+
702
+ # Render grid
703
+ grid_2d = np.array(obs.grid).reshape(obs.height, obs.width)
704
+ ax.clear()
705
+ ax.imshow(grid_2d, cmap=cmap, norm=norm, interpolation='nearest')
706
+ ax.set_title(
707
+ f"Step {obs.step} | Fires: {obs.burning_count} | Burned: {obs.burned_count}\n"
708
+ f"Wind: {obs.wind_dir} | Humidity: {obs.humidity:.2f} | "
709
+ f"Water: {obs.remaining_water} | Breaks: {obs.remaining_breaks}"
710
+ )
711
+ plt.pause(0.1)
712
+
713
+ # Take action (simple: water first burning cell)
714
+ if obs.burning_count > 0 and obs.remaining_water > 0:
715
+ burning_indices = np.where(grid_2d == 2)
716
+ if len(burning_indices[0]) > 0:
717
+ y, x = burning_indices[0][0], burning_indices[1][0]
718
+ action = WildfireAction(action="water", x=int(x), y=int(y))
719
+ else:
720
+ action = WildfireAction(action="wait")
721
+ else:
722
+ action = WildfireAction(action="wait")
723
+
724
+ result = env.step(action)
725
+ obs = result.observation
726
 
727
+ plt.ioff()
728
+ plt.show()
729
+ env.close()
730
+ ```
731
 
732
+ ### Example 4: Training Loop for RL
733
 
734
+ ```python
735
+ from envs.wildfire_env import WildfireEnv, WildfireAction
736
+ import random
737
 
738
+ env = WildfireEnv(base_url="http://localhost:8000")
739
 
740
+ num_episodes = 10
741
+ episode_rewards = []
 
 
 
 
742
 
743
+ for episode in range(num_episodes):
744
+ result = env.reset()
745
+ obs = result.observation
746
+ episode_reward = 0
747
+ episode_steps = 0
748
+
749
+ while not result.done:
750
+ # Random policy (replace with your RL agent)
751
+ if random.random() < 0.4 and obs.remaining_water > 0:
752
+ action = WildfireAction(
753
+ action="water",
754
+ x=random.randint(0, obs.width - 1),
755
+ y=random.randint(0, obs.height - 1)
756
+ )
757
+ elif random.random() < 0.3 and obs.remaining_breaks > 0:
758
+ action = WildfireAction(
759
+ action="break",
760
+ x=random.randint(0, obs.width - 1),
761
+ y=random.randint(0, obs.height - 1)
762
+ )
763
+ else:
764
+ action = WildfireAction(action="wait")
765
+
766
+ result = env.step(action)
767
+ obs = result.observation
768
+ episode_reward += result.reward or 0
769
+ episode_steps += 1
770
+
771
+ episode_rewards.append(episode_reward)
772
+ state = env.state
773
+ print(
774
+ f"Episode {episode + 1}: "
775
+ f"Reward={episode_reward:.2f}, "
776
+ f"Steps={episode_steps}, "
777
+ f"Burned={state.total_burned}, "
778
+ f"Extinguished={state.total_extinguished}"
779
+ )
780
 
781
+ print(f"\nAverage reward: {sum(episode_rewards) / len(episode_rewards):.2f}")
782
+ env.close()
783
+ ```
784
 
785
+ ---
 
 
786
 
787
+ ## 🌐 Web Interface
788
 
789
+ The wildfire environment includes a **built-in web interface** for interactive exploration.
790
 
791
+ ### Accessing the Web Interface
 
 
 
792
 
793
+ 1. **Start the server** (Docker or local)
794
+ 2. **Open browser** to: `http://localhost:8000/web`
795
+ 3. **Interact** with the environment visually
 
 
796
 
797
+ ### Web Interface Features
 
 
798
 
799
+ - **Visual grid display** - See the fire spread in real-time
800
+ - **Action form** - Select action type and coordinates
801
+ - **State observer** - View current observation and state
802
+ - **Action history** - Log of all actions taken
803
+ - **Reset button** - Start new episode
804
+ - **WebSocket updates** - Real-time state updates
805
 
806
+ ### Using the Web Interface
 
 
807
 
808
+ 1. Click **"Reset Environment"** to start
809
+ 2. Fill in action form:
810
+ - Select action: `water`, `break`, or `wait`
811
+ - Enter coordinates (x, y) for water/break actions
812
+ 3. Click **"Submit Action"**
813
+ 4. Observe the grid update and rewards
814
+ 5. Monitor resources (water, breaks) in the state panel
815
 
816
  ---
817
 
818
+ ## 🔧 Troubleshooting
819
 
820
+ ### Common Issues
821
 
822
+ #### 1. Connection Errors
 
 
823
 
824
+ **Problem:** `ConnectionRefusedError` or `Cannot connect to server`
825
 
826
+ **Solutions:**
827
+ - Verify server is running: `curl http://localhost:8000/health`
828
+ - Check Docker container: `docker ps | grep wildfire`
829
+ - Ensure port 8000 is not in use: `lsof -i :8000`
830
 
831
+ #### 2. Index Errors
 
 
 
 
832
 
833
+ **Problem:** `IndexError: list index out of range`
834
 
835
+ **Solution:** Ensure coordinates are within bounds:
836
+ ```python
837
+ # Always check bounds before accessing
838
+ if 0 <= x < obs.width and 0 <= y < obs.height:
839
+ action = WildfireAction(action="water", x=x, y=y)
840
  ```
841
 
842
+ #### 3. Invalid Action Warnings
843
+
844
+ **Problem:** Actions returning -0.05 reward repeatedly
845
+
846
+ **Solutions:**
847
+ - Check `remaining_water` and `remaining_breaks` before using resources
848
+ - Verify coordinates are integers and within grid bounds
849
+ - Use `action="wait"` when resources are exhausted
850
+
851
+ #### 4. Grid Format Confusion
852
+
853
+ **Problem:** How to access grid cells?
854
+
855
+ **Solution:**
856
+ ```python
857
+ # Convert flat array to 2D
858
+ grid_2d = np.array(obs.grid).reshape(obs.height, obs.width)
859
+
860
+ # Access cell at (x, y)
861
+ cell_value = grid_2d[y][x]
862
 
863
+ # Or use flat index
864
+ index = y * obs.width + x
865
+ cell_value = obs.grid[index]
866
+ ```
867
+
868
+ #### 5. Docker Build Failures
869
 
870
+ **Problem:** `failed to solve: openenv-base:latest`
871
 
872
+ **Solution:**
873
  ```bash
874
+ # Build base image first
875
  docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile .
 
 
 
876
 
877
+ # Then build wildfire image
878
+ docker build -t wildfire-env:latest -f src/envs/wildfire_env/server/Dockerfile .
 
 
 
 
 
879
  ```
880
 
881
+ ### Debugging Tips
882
+
883
+ 1. **Enable verbose logging:**
884
+ ```bash
885
+ docker logs -f wildfire-env-container
886
+ ```
887
+
888
+ 2. **Check environment state:**
889
+ ```python
890
+ state = env.state
891
+ print(f"State: {state}")
892
+ ```
893
+
894
+ 3. **Validate actions:**
895
+ ```python
896
+ obs = env.reset().observation
897
+ print(f"Bounds: 0 <= x < {obs.width}, 0 <= y < {obs.height}")
898
+ print(f"Resources: Water={obs.remaining_water}, Breaks={obs.remaining_breaks}")
899
+ ```
900
+
901
+ 4. **Monitor grid changes:**
902
+ ```python
903
+ prev_grid = obs.grid.copy()
904
+ result = env.step(action)
905
+ new_grid = result.observation.grid
906
+ changes = [i for i, (a, b) in enumerate(zip(prev_grid, new_grid)) if a != b]
907
+ print(f"Changed cells: {len(changes)}")
908
+ ```
909
+
910
+ ---
911
+
912
+ ## 📊 Performance Considerations
913
+
914
+ ### Grid Size Impact
915
+
916
+ - **Small grids (16×16)**: Fast, good for quick testing
917
+ - **Medium grids (32×32)**: Default, balanced performance
918
+ - **Large grids (64×64+)**: Slower, more realistic but requires more compute
919
+
920
+ ### Resource Limits
921
+
922
+ - **Low water (4-8)**: Forces strategic decisions
923
+ - **High water (20+)**: More forgiving, easier to succeed
924
+ - **Low breaks (25)**: Emphasizes firebreak placement strategy
925
+ - **High breaks (100+)**: More freedom, less constraint
926
+
927
+ ### Episode Length
928
+
929
+ - **Short episodes (50 steps)**: Fast iteration, good for debugging
930
+ - **Medium episodes (128 steps)**: Default, balanced
931
+ - **Long episodes (200+ steps)**: Better for complex strategies
932
+
933
  ---
934
 
935
  ## 🧭 References
936
 
937
+ ### Papers & Research
938
+
939
+ - **Rothermel Model**: [USDA Forest Service - Surface Fire Spread Model](https://www.fs.fed.us/rm/pubs_series/rmrs/gtr/rmrs_gtr371.pdf)
940
+ - **SimFire**: [MITRE Fireline Project](https://github.com/mitrefireline/simfire)
941
+ - **RL for Wildfires**: [arXiv:2311.15925](https://arxiv.org/abs/2311.15925)
942
+
943
+ ### OpenEnv Framework
944
+
945
+ - **Main Repository**: [OpenEnv GitHub](https://github.com/openenv)
946
+ - **Documentation**: See `rfcs/` directory for design documents
947
+ - **Other Environments**: See `src/envs/` for more environment examples
948
+
949
+ ### Related Tools
950
+
951
+ - **FastAPI**: [FastAPI Documentation](https://fastapi.tiangolo.com/)
952
+ - **Reinforcement Learning**: [Spinning Up in Deep RL](https://spinningup.openai.com/)
953
+ - **Docker**: [Docker Documentation](https://docs.docker.com/)
954
+
955
+ ---
956
+
957
+ ## 📝 License
958
+
959
+ This environment is part of the OpenEnv project. See the main LICENSE file for details.
960
+
961
+ ---
962
+
963
+ ## 🤝 Contributing
964
+
965
+ Contributions welcome! Please see `CONTRIBUTING.md` in the main OpenEnv repository.
966
 
967
  ---
968
+
969
  ## 🔖 Citations
970
 
971
  ```bibtex
 
995
 
996
  @misc{wildfire-openenv-2025,
997
  title = {Wildfire Environment for OpenEnv: Containment-Focused RL Simulation},
998
+ author = {OpenEnv Contributors},
999
  year = {2025},
1000
+ url = {https://github.com/openenv/openenv}
1001
  }
1002
  ```
1003
+
1004
+ ---
1005
+
1006
+ **Happy firefighting! 🔥🚒**
src/envs/wildfire_env/server/wildfire_environment.py CHANGED
@@ -14,9 +14,13 @@ DIRS_8 = {
14
  }
15
 
16
  def idx(x: int, y: int, w: int) -> int:
 
 
17
  return y * w + x
18
 
19
  def in_bounds(x: int, y: int, w: int, h: int) -> bool:
 
 
20
  return 0 <= x < w and 0 <= y < h
21
 
22
 
@@ -59,9 +63,9 @@ class WildfireEnvironment(Environment):
59
  humidity = float(os.environ.get("WILDFIRE_HUMIDITY", humidity))
60
  forced_wind = os.environ.get("WILDFIRE_WIND", None)
61
 
62
- # Store config
63
- self.w = width
64
- self.h = height
65
  self.base_ignite_prob = base_ignite_prob
66
  self.wind_bias = wind_bias
67
  self.diag_factor = diag_factor
@@ -81,8 +85,11 @@ class WildfireEnvironment(Environment):
81
  # --- Core API ---
82
 
83
  def reset(self) -> WildfireObservation:
 
 
 
84
  # Start with all fuel
85
- grid = [1] * (self.w * self.h)
86
 
87
  # Wind (forced if provided)
88
  if self.forced_wind and self.forced_wind in DIRS_8:
@@ -95,9 +102,12 @@ class WildfireEnvironment(Environment):
95
 
96
  # Place initial fires
97
  for _ in range(self.init_sources):
98
- x = self.rng.randrange(self.w)
99
- y = self.rng.randrange(self.h)
100
- grid[idx(x, y, self.w)] = 2
 
 
 
101
 
102
  self._state = WildfireState(
103
  episode_id=str(uuid.uuid4()),
@@ -105,8 +115,8 @@ class WildfireEnvironment(Environment):
105
  total_burned=0,
106
  total_extinguished=0,
107
  last_action="reset",
108
- width=self.w,
109
- height=self.h,
110
  wind_dir=wind_dir,
111
  humidity=humidity,
112
  remaining_water=self.init_water,
@@ -115,7 +125,7 @@ class WildfireEnvironment(Environment):
115
  )
116
 
117
  # per-cell burn timers (persist across steps)
118
- self._state.burn_timers = [0] * (self.w * self.h)
119
 
120
  obs = self._make_observation(reward_hint=0.0)
121
  return obs
@@ -198,6 +208,8 @@ class WildfireEnvironment(Environment):
198
 
199
  def _apply_water(self, x: int, y: int) -> float:
200
  st = self._state
 
 
201
  if not in_bounds(x, y, self.w, self.h):
202
  return -0.05
203
 
@@ -206,6 +218,10 @@ class WildfireEnvironment(Environment):
206
  return -0.5
207
 
208
  i = idx(x, y, self.w)
 
 
 
 
209
  reward = 0.0
210
 
211
  if st.grid[i] == 2:
@@ -229,9 +245,15 @@ class WildfireEnvironment(Environment):
229
 
230
  def _apply_break(self, x: int, y: int) -> float:
231
  st = self._state
 
 
232
  if not in_bounds(x, y, self.w, self.h):
233
  return -0.05
234
  i = idx(x, y, self.w)
 
 
 
 
235
  reward = 0.0
236
 
237
  if st.grid[i] in (1, 4):
@@ -263,6 +285,9 @@ class WildfireEnvironment(Environment):
263
  new_grid = st.grid[:]
264
  newly_burned = 0
265
 
 
 
 
266
  # 8-neighbor model
267
  neighbors = [(-1, 0), (1, 0), (0, -1), (0, 1),
268
  (-1, -1), (1, -1), (-1, 1), (1, 1)]
@@ -271,12 +296,15 @@ class WildfireEnvironment(Environment):
271
  base = self.base_ignite_prob
272
  humidity_factor = (1.0 - st.humidity)
273
 
274
- ignite_flags = [False] * (self.w * self.h)
275
 
276
  # First pass: evaluate ignitions, increment burn timers
277
- for y in range(self.h):
278
- for x in range(self.w):
279
- i = idx(x, y, self.w)
 
 
 
280
  cell = st.grid[i]
281
 
282
  if cell == 2: # burning
@@ -284,9 +312,12 @@ class WildfireEnvironment(Environment):
284
 
285
  for dx, dy in neighbors:
286
  nx, ny = x + dx, y + dy
287
- if not in_bounds(nx, ny, self.w, self.h):
 
 
 
 
288
  continue
289
- ni = idx(nx, ny, self.w)
290
  target = st.grid[ni]
291
 
292
  # Only fuel or water/damp can be candidates, but cells with code 4 (watered/damp) are immune to ignition
@@ -310,10 +341,16 @@ class WildfireEnvironment(Environment):
310
  p = base * humidity_factor * wind_mult * diag_mult
311
  p = max(0.0, min(1.0, p))
312
  if self.rng.random() < p:
313
- ignite_flags[ni] = True
 
 
314
 
315
  # Second pass: apply transitions
316
  for i, cell in enumerate(st.grid):
 
 
 
 
317
  if cell == 2:
318
  # burns for burn_lifetime ticks before turning to ash
319
  if st.burn_timers[i] >= self.burn_lifetime:
@@ -321,7 +358,7 @@ class WildfireEnvironment(Environment):
321
  newly_burned += 1
322
  else:
323
  new_grid[i] = 2 # keep burning
324
- elif ignite_flags[i] and new_grid[i] == 1:
325
  new_grid[i] = 2
326
  st.burn_timers[i] = 0
327
  elif cell == 4:
 
14
  }
15
 
16
  def idx(x: int, y: int, w: int) -> int:
17
+ # Defensive type conversion to ensure all parameters are integers
18
+ x, y, w = int(x), int(y), int(w)
19
  return y * w + x
20
 
21
  def in_bounds(x: int, y: int, w: int, h: int) -> bool:
22
+ # Defensive type conversion to ensure all parameters are integers
23
+ x, y, w, h = int(x), int(y), int(w), int(h)
24
  return 0 <= x < w and 0 <= y < h
25
 
26
 
 
63
  humidity = float(os.environ.get("WILDFIRE_HUMIDITY", humidity))
64
  forced_wind = os.environ.get("WILDFIRE_WIND", None)
65
 
66
+ # Store config (ensure integers)
67
+ self.w = int(width)
68
+ self.h = int(height)
69
  self.base_ignite_prob = base_ignite_prob
70
  self.wind_bias = wind_bias
71
  self.diag_factor = diag_factor
 
85
  # --- Core API ---
86
 
87
  def reset(self) -> WildfireObservation:
88
+ # Ensure w and h are integers (defensive type conversion)
89
+ w, h = int(self.w), int(self.h)
90
+
91
  # Start with all fuel
92
+ grid = [1] * (w * h)
93
 
94
  # Wind (forced if provided)
95
  if self.forced_wind and self.forced_wind in DIRS_8:
 
102
 
103
  # Place initial fires
104
  for _ in range(self.init_sources):
105
+ x = self.rng.randrange(w)
106
+ y = self.rng.randrange(h)
107
+ i = idx(x, y, w)
108
+ # Safety check: ensure index is within grid bounds
109
+ if 0 <= i < len(grid):
110
+ grid[i] = 2
111
 
112
  self._state = WildfireState(
113
  episode_id=str(uuid.uuid4()),
 
115
  total_burned=0,
116
  total_extinguished=0,
117
  last_action="reset",
118
+ width=w,
119
+ height=h,
120
  wind_dir=wind_dir,
121
  humidity=humidity,
122
  remaining_water=self.init_water,
 
125
  )
126
 
127
  # per-cell burn timers (persist across steps)
128
+ self._state.burn_timers = [0] * (w * h)
129
 
130
  obs = self._make_observation(reward_hint=0.0)
131
  return obs
 
208
 
209
  def _apply_water(self, x: int, y: int) -> float:
210
  st = self._state
211
+ # Ensure x and y are integers (defensive type conversion)
212
+ x, y = int(x), int(y)
213
  if not in_bounds(x, y, self.w, self.h):
214
  return -0.05
215
 
 
218
  return -0.5
219
 
220
  i = idx(x, y, self.w)
221
+ # Safety check: ensure index is within grid bounds
222
+ if i < 0 or i >= len(st.grid):
223
+ return -0.05
224
+
225
  reward = 0.0
226
 
227
  if st.grid[i] == 2:
 
245
 
246
  def _apply_break(self, x: int, y: int) -> float:
247
  st = self._state
248
+ # Ensure x and y are integers (defensive type conversion)
249
+ x, y = int(x), int(y)
250
  if not in_bounds(x, y, self.w, self.h):
251
  return -0.05
252
  i = idx(x, y, self.w)
253
+ # Safety check: ensure index is within grid bounds
254
+ if i < 0 or i >= len(st.grid):
255
+ return -0.05
256
+
257
  reward = 0.0
258
 
259
  if st.grid[i] in (1, 4):
 
285
  new_grid = st.grid[:]
286
  newly_burned = 0
287
 
288
+ # Ensure w and h are integers (defensive type conversion)
289
+ w, h = int(self.w), int(self.h)
290
+
291
  # 8-neighbor model
292
  neighbors = [(-1, 0), (1, 0), (0, -1), (0, 1),
293
  (-1, -1), (1, -1), (-1, 1), (1, 1)]
 
296
  base = self.base_ignite_prob
297
  humidity_factor = (1.0 - st.humidity)
298
 
299
+ ignite_flags = [False] * (w * h)
300
 
301
  # First pass: evaluate ignitions, increment burn timers
302
+ for y in range(h):
303
+ for x in range(w):
304
+ i = idx(x, y, w)
305
+ # Safety check: ensure index is within grid bounds
306
+ if i < 0 or i >= len(st.grid):
307
+ continue
308
  cell = st.grid[i]
309
 
310
  if cell == 2: # burning
 
312
 
313
  for dx, dy in neighbors:
314
  nx, ny = x + dx, y + dy
315
+ if not in_bounds(nx, ny, w, h):
316
+ continue
317
+ ni = idx(nx, ny, w)
318
+ # Safety check: ensure neighbor index is within grid bounds
319
+ if ni < 0 or ni >= len(st.grid):
320
  continue
 
321
  target = st.grid[ni]
322
 
323
  # Only fuel or water/damp can be candidates, but cells with code 4 (watered/damp) are immune to ignition
 
341
  p = base * humidity_factor * wind_mult * diag_mult
342
  p = max(0.0, min(1.0, p))
343
  if self.rng.random() < p:
344
+ # Safety check: ensure ni is within ignite_flags bounds
345
+ if 0 <= ni < len(ignite_flags):
346
+ ignite_flags[ni] = True
347
 
348
  # Second pass: apply transitions
349
  for i, cell in enumerate(st.grid):
350
+ # Safety check: ensure index is within bounds for all arrays
351
+ if i < 0 or i >= len(new_grid) or i >= len(st.burn_timers):
352
+ continue
353
+
354
  if cell == 2:
355
  # burns for burn_lifetime ticks before turning to ash
356
  if st.burn_timers[i] >= self.burn_lifetime:
 
358
  newly_burned += 1
359
  else:
360
  new_grid[i] = 2 # keep burning
361
+ elif i < len(ignite_flags) and ignite_flags[i] and new_grid[i] == 1:
362
  new_grid[i] = 2
363
  st.burn_timers[i] = 0
364
  elif cell == 4: