| """ |
| Pydantic data models for the Wildfire Containment Simulator. |
| |
| This module defines the complete type contract between all environment components. |
| Every action, observation, cell state, and result is typed and validated here. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from enum import Enum |
| from typing import Any, Optional |
|
|
| from pydantic import BaseModel, Field, model_validator |
|
|
|
|
| |
| |
| |
|
|
| class FuelType(str, Enum): |
| """Terrain fuel classification. Determines burn rate and ignition probability.""" |
| GRASS = "grass" |
| SHRUB = "shrub" |
| TIMBER = "timber" |
| URBAN = "urban" |
| WATER = "water" |
| ROAD = "road" |
|
|
|
|
| class FireState(str, Enum): |
| """Current fire status of a grid cell.""" |
| UNBURNED = "unburned" |
| BURNING = "burning" |
| EMBER = "ember" |
| BURNED_OUT = "burned_out" |
| FIREBREAK = "firebreak" |
| SUPPRESSED = "suppressed" |
| UNKNOWN = "unknown" |
|
|
|
|
| class Priority(str, Enum): |
| """Job/event priority levels.""" |
| LOW = "low" |
| NORMAL = "normal" |
| HIGH = "high" |
| CRITICAL = "critical" |
|
|
|
|
| class Direction(str, Enum): |
| """8-directional movement for crews.""" |
| N = "N" |
| S = "S" |
| E = "E" |
| W = "W" |
| NE = "NE" |
| NW = "NW" |
| SE = "SE" |
| SW = "SW" |
|
|
|
|
| class ActionType(str, Enum): |
| """All possible agent actions.""" |
| DEPLOY_CREW = "deploy_crew" |
| MOVE_CREW = "move_crew" |
| ORDER_CREW_OBJECTIVE = "order_crew_objective" |
| DROP_RETARDANT = "drop_retardant" |
| BUILD_FIREBREAK = "build_firebreak" |
| RECON_FLIGHT = "recon_flight" |
| IDLE = "idle" |
|
|
|
|
| class CrewObjective(str, Enum): |
| """Objective directive for ORDER_CREW_OBJECTIVE.""" |
| HOLD = "hold" |
| ADVANCE = "advance" |
| RETREAT = "retreat" |
| PRIORITIZE_NORTH = "prioritize_north" |
| PRIORITIZE_SOUTH = "prioritize_south" |
| PRIORITIZE_EAST = "prioritize_east" |
| PRIORITIZE_WEST = "prioritize_west" |
|
|
|
|
| class IntensityBin(str, Enum): |
| """Quantized fire intensity as seen by the agent.""" |
| NONE = "none" |
| LOW = "low" |
| MEDIUM = "medium" |
| HIGH = "high" |
| EXTREME = "extreme" |
|
|
|
|
| |
| |
| |
|
|
| DIRECTION_DELTAS: dict[Direction, tuple[int, int]] = { |
| Direction.N: (-1, 0), |
| Direction.S: (1, 0), |
| Direction.E: (0, 1), |
| Direction.W: (0, -1), |
| Direction.NE: (-1, 1), |
| Direction.NW: (-1, -1), |
| Direction.SE: (1, 1), |
| Direction.SW: (1, -1), |
| } |
|
|
|
|
| |
| |
| |
|
|
| class CellStatic(BaseModel): |
| """Immutable terrain properties of a grid cell.""" |
| row: int |
| col: int |
| elevation_m: float = Field(ge=0, le=2000, description="Height in meters") |
| fuel_type: FuelType |
| fuel_load: float = Field(ge=0.0, le=1.0, description="Density of burnable material") |
| is_populated: bool = False |
| population: int = Field(ge=0, default=0) |
| is_water: bool = False |
|
|
| @model_validator(mode="after") |
| def water_consistency(self) -> "CellStatic": |
| if self.fuel_type == FuelType.WATER: |
| self.is_water = True |
| self.fuel_load = 0.0 |
| if self.fuel_type == FuelType.ROAD: |
| self.fuel_load = 0.0 |
| return self |
|
|
|
|
| class CellDynamic(BaseModel): |
| """Mutable runtime state of a grid cell. Updated each step.""" |
| fire_state: FireState = FireState.UNBURNED |
| fire_intensity: float = Field(ge=0.0, le=1.0, default=0.0) |
| moisture: float = Field(ge=0.0, le=1.0, default=0.3) |
| time_burning: int = Field(ge=0, default=0) |
| suppression_level: float = Field(ge=0.0, le=1.0, default=0.0) |
| smoke_density: float = Field(ge=0.0, le=1.0, default=0.0) |
| crew_present: bool = False |
|
|
|
|
| class CellObservation(BaseModel): |
| """What the agent sees for a single cell (may be degraded by smoke/fog).""" |
| row: int |
| col: int |
| fire_state: FireState |
| intensity_bin: IntensityBin = IntensityBin.NONE |
| smoke_density: float = 0.0 |
| is_populated: bool = False |
| crew_present: bool = False |
| fuel_type: FuelType = FuelType.GRASS |
| elevation_m: float = 0.0 |
|
|
|
|
| |
| |
| |
|
|
| class WeatherState(BaseModel): |
| """Full ground-truth weather (used internally).""" |
| wind_speed_kmh: float = Field(ge=0, le=60, default=10.0) |
| wind_direction_deg: float = Field(ge=0, lt=360, default=0.0) |
| humidity_pct: float = Field(ge=0, le=100, default=40.0) |
| rain_active: bool = False |
| rain_steps_remaining: int = 0 |
|
|
|
|
| class WeatherObservation(BaseModel): |
| """Noisy weather readings visible to the agent.""" |
| wind_speed_kmh: float |
| wind_direction_deg: float |
| humidity_pct: float |
| rain_active: bool |
|
|
|
|
| |
| |
| |
|
|
| class CrewState(BaseModel): |
| """State of a single ground crew.""" |
| crew_id: str |
| row: int |
| col: int |
| is_deployed: bool = False |
| is_active: bool = True |
|
|
|
|
| class TankerState(BaseModel): |
| """State of a single air tanker.""" |
| tanker_id: str |
| cooldown_remaining: int = 0 |
| is_active: bool = True |
|
|
|
|
| class ResourceState(BaseModel): |
| """Complete resource state visible to the agent.""" |
| crews: list[CrewState] |
| tankers: list[TankerState] |
| firebreak_budget: int = Field(ge=0, description="Remaining firebreak cells") |
| recon_budget: int = Field(ge=0, default=0, description="Remaining recon flights") |
|
|
|
|
| |
| |
| |
|
|
| class Action(BaseModel): |
| """ |
| Agent action. One action per step. |
| |
| Validation catches invalid actions at the type level. |
| Semantic validation (VRAM-like feasibility checks) happens in the environment. |
| """ |
| action_type: ActionType |
|
|
| |
| target_row: Optional[int] = None |
| target_col: Optional[int] = None |
|
|
| |
| crew_id: Optional[str] = None |
|
|
| |
| direction: Optional[Direction] = None |
|
|
| |
| tanker_id: Optional[str] = None |
|
|
| |
| objective: Optional[CrewObjective] = None |
|
|
| |
| reason: Optional[str] = None |
|
|
| @model_validator(mode="after") |
| def validate_params(self) -> "Action": |
| """Ensure required parameters are present for each action type.""" |
| t = self.action_type |
|
|
| if t == ActionType.DEPLOY_CREW: |
| if self.crew_id is None: |
| raise ValueError("DEPLOY_CREW requires crew_id") |
| if self.target_row is None or self.target_col is None: |
| raise ValueError("DEPLOY_CREW requires target_row and target_col") |
|
|
| elif t == ActionType.MOVE_CREW: |
| if self.crew_id is None: |
| raise ValueError("MOVE_CREW requires crew_id") |
| if self.direction is None: |
| raise ValueError("MOVE_CREW requires direction") |
|
|
| elif t == ActionType.ORDER_CREW_OBJECTIVE: |
| if self.crew_id is None: |
| raise ValueError("ORDER_CREW_OBJECTIVE requires crew_id") |
| if self.objective is None: |
| raise ValueError("ORDER_CREW_OBJECTIVE requires objective") |
|
|
| elif t == ActionType.DROP_RETARDANT: |
| if self.tanker_id is None: |
| raise ValueError("DROP_RETARDANT requires tanker_id") |
| if self.target_row is None or self.target_col is None: |
| raise ValueError("DROP_RETARDANT requires target_row and target_col") |
|
|
| elif t == ActionType.BUILD_FIREBREAK: |
| if self.crew_id is None: |
| raise ValueError("BUILD_FIREBREAK requires crew_id") |
| if self.direction is None: |
| raise ValueError("BUILD_FIREBREAK requires direction") |
|
|
| elif t == ActionType.RECON_FLIGHT: |
| if self.target_row is None or self.target_col is None: |
| raise ValueError("RECON_FLIGHT requires target_row and target_col") |
|
|
| return self |
|
|
|
|
| |
| |
| |
|
|
| class ClusterStats(BaseModel): |
| """Running statistics about the episode.""" |
| cells_burned: int = 0 |
| cells_burning: int = 0 |
| cells_saved: int = 0 |
| population_threatened: int = 0 |
| population_lost: int = 0 |
| total_population: int = Field(ge=0, default=0, description="Initial population (for UI % civ safe)") |
| containment_pct: float = Field(ge=0.0, le=100.0, default=0.0) |
| |
| area_saved_pct: float = Field(ge=0.0, le=100.0, default=100.0, |
| description="Percentage of burnable land not yet burned") |
| civilians_saved_pct: float = Field(ge=0.0, le=100.0, default=100.0, |
| description="Percentage of civilians in unburned zones") |
| current_step: int = 0 |
| max_steps: int = 100 |
| firebreaks_built: int = 0 |
| retardant_drops: int = 0 |
|
|
|
|
| class Observation(BaseModel): |
| """Complete observation returned to the agent each step.""" |
| grid: list[list[CellObservation]] |
| weather: WeatherObservation |
| resources: ResourceState |
| stats: ClusterStats |
| recent_events: list[str] = Field(default_factory=list, max_length=5) |
| briefing: Optional[Any] = None |
|
|
|
|
| |
| |
| |
|
|
| class StepResult(BaseModel): |
| """Returned by env.step(). Contains everything the agent needs.""" |
| observation: Observation |
| reward: float |
| done: bool = False |
| info: dict = Field(default_factory=dict) |
|
|
|
|
| |
| |
| |
|
|
| class TierConfig(BaseModel): |
| """Configuration for a difficulty tier.""" |
| tier_name: str |
| grid_rows: int |
| grid_cols: int |
| num_crews: int |
| num_tankers: int |
| firebreak_budget: int |
| recon_budget: int = 0 |
| episode_length: int |
| num_ignition_points: int = 1 |
| staggered_ignition_step: Optional[int] = None |
| enable_smoke_occlusion: bool = False |
| enable_sensor_noise: bool = False |
| enable_fog_of_war: bool = False |
| fog_visibility_radius: int = 7 |
| enable_wind_shifts: bool = False |
| enable_crew_loss: bool = False |
| crew_loss_step: Optional[int] = None |
| crew_loss_id: Optional[str] = None |
| tanker_cooldown: int = 5 |
| min_active_steps: int = 5 |
| wind_speed_init: float = 10.0 |
| wind_dir_init: float = 0.0 |
| humidity_init: float = 40.0 |
|
|
| |
| w_containment: float = 0.30 |
| w_population: float = 0.35 |
| w_efficiency: float = 0.10 |
| w_speed: float = 0.15 |
| w_area: float = 0.10 |
|
|
|
|
| |
| |
| |
|
|
| TIER_EASY = TierConfig( |
| tier_name="easy", |
| grid_rows=15, |
| grid_cols=15, |
| num_crews=4, |
| num_tankers=1, |
| firebreak_budget=15, |
| recon_budget=0, |
| episode_length=80, |
| num_ignition_points=2, |
| enable_smoke_occlusion=False, |
| enable_sensor_noise=False, |
| enable_fog_of_war=False, |
| enable_wind_shifts=False, |
| min_active_steps=25, |
| wind_speed_init=10.0, |
| wind_dir_init=0.0, |
| humidity_init=40.0, |
| w_containment=0.30, |
| w_population=0.35, |
| w_efficiency=0.10, |
| w_speed=0.15, |
| w_area=0.10, |
| ) |
|
|
| TIER_MEDIUM = TierConfig( |
| tier_name="medium", |
| grid_rows=25, |
| grid_cols=25, |
| num_crews=5, |
| num_tankers=2, |
| firebreak_budget=20, |
| recon_budget=1, |
| episode_length=150, |
| num_ignition_points=3, |
| enable_smoke_occlusion=True, |
| enable_sensor_noise=True, |
| enable_fog_of_war=False, |
| enable_wind_shifts=True, |
| min_active_steps=45, |
| wind_speed_init=15.0, |
| wind_dir_init=45.0, |
| humidity_init=35.0, |
| w_containment=0.25, |
| w_population=0.35, |
| w_efficiency=0.15, |
| w_speed=0.10, |
| w_area=0.15, |
| ) |
|
|
| TIER_HARD = TierConfig( |
| tier_name="hard", |
| grid_rows=40, |
| grid_cols=40, |
| num_crews=6, |
| num_tankers=3, |
| firebreak_budget=30, |
| recon_budget=3, |
| episode_length=300, |
| num_ignition_points=3, |
| staggered_ignition_step=30, |
| min_active_steps=80, |
| enable_smoke_occlusion=True, |
| enable_sensor_noise=True, |
| enable_fog_of_war=True, |
| fog_visibility_radius=7, |
| enable_wind_shifts=True, |
| enable_crew_loss=True, |
| crew_loss_step=40, |
| crew_loss_id="crew_5", |
| wind_speed_init=20.0, |
| wind_dir_init=90.0, |
| humidity_init=30.0, |
| w_containment=0.20, |
| w_population=0.40, |
| w_efficiency=0.15, |
| w_speed=0.10, |
| w_area=0.15, |
| ) |
|
|