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:latestENABLE_WEB_INTERFACE=true. Access it at http://localhost:8000/web when enabled.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()burning_count == 0) - Success! step_count >= max_steps) - Time limit exceeded(x, y):index = y * width + x
cell_value = observation.grid[index]index = 15 * 32 + 10 # = 490
cell_value = observation.grid[490]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 |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=15water - Apply WaterWildfireAction(action="water", x=10, y=15)remaining_water > 0 and valid coordinatesbreak - Create FirebreakWildfireAction(action="break", x=12, y=15)remaining_breaks > 0 and valid coordinateswait - Do NothingWildfireAction(action="wait")remaining_water == 0remaining_breaks == 0WildfireObservationreset() or step():@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 rewardresult = env.reset()
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}") # 50done == True):burning_count == 0):Reward = +0.5 + 0.5 × 1.0 = +1.0burn_lifetime = 3 ticks before turning to ash0.30 (30% chance)(1.0 - humidity) - higher humidity = less spread0.6x for diagonal neighbors (slower spread)Step 0: Step 1: Step 2:
🟩🟩🟩 🟩🟥🟩 🟫🟥🟫
🟩🟥🟩 → 🟥🟥🟥 → 🟥🟥🟥 (Wind: E, spreading east)
🟩🟩🟩 🟩🟥🟩 🟫🟥🟫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 |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 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:latestBuild 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:latestBuild 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:latestBuild base image (first time only)
docker build -t openenv-base:latest -f src/core/containers/images/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
View logs
docker logs -f wildfire-env-container
Stop container
docker stop wildfire-env-container
Remove container
docker rm wildfire-env-containerpip install fastapi uvicorn numpy matplotlib requestsFrom OpenEnv root directory
python -m envs.wildfire_env.server.appWILDFIRE_WIDTH=64 WILDFIRE_HUMIDITY=0.3 python -m envs.wildfire_env.server.appfrom 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")reset() -> StepResult[WildfireObservation]result = env.reset()
obs = result.observation
print(f"New episode: {obs.step == 0}")step(action: WildfireAction) -> StepResult[WildfireObservation]action = WildfireAction(action="water", x=10, y=15)
result = env.step(action)
print(f"Reward: {result.reward}, Done: {result.done}")state -> WildfireStatestate = env.state
print(f"Episode ID: {state.episode_id}")
print(f"Total burned: {state.total_burned}")
print(f"Total extinguished: {state.total_extinguished}")close()env.close()WildfireAction@dataclass
class WildfireAction(Action):
action: str # "water" | "break" | "wait"
x: Optional[int] = None # Target X coordinate (required for water/break)
y: Optional[int] = None # Target Y coordinate (required for water/break)WildfireAction(action="water", x=10, y=15)
WildfireAction(action="break", x=12, y=15)
WildfireAction(action="wait") # x, y not neededWildfireObservationWildfireState@dataclass
class WildfireState(State):
episode_id: str
step_count: int
total_burned: int
total_extinguished: 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]from envs.wildfire_env import WildfireEnv, WildfireAction
import numpy as np
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()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()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
"red", # 2 = burning
"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()
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
plt.ioff()
plt.show()
env.close()from envs.wildfire_env import WildfireEnv, WildfireAction
import random
env = WildfireEnv(base_url="http://localhost:8000")
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()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:latesthttp://localhost:8000/webEnable 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 8000http://localhost:8000/webwater, break, or wait from the dropdownConnectionRefusedError or Cannot connect to servercurl http://localhost:8000/healthdocker ps | grep wildfirelsof -i :8000IndexError: list index out of rangeAlways check bounds before accessing
if 0 <= x < obs.width and 0 <= y < obs.height:
action = WildfireAction(action="water", x=x, y=y)remaining_water and remaining_breaks before using resourcesaction="wait" when resources are exhaustedConvert 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]
Or use flat index
index = y * obs.width + x
cell_value = obs.grid[index]failed to solve: openenv-base:latestBuild base image first
docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile .
Then build wildfire image
docker build -t wildfire-env:latest -f src/envs/wildfire_env/server/Dockerfile . 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
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
bibtexClick on a cell to set coordinates for water/break actions