Model
Bases: nn.Module
A model for predicting wildfire spread based on various input features.
The model extends the PyTorch nn.Module and is tailored to perform both prediction and parameter calibration.
The model is based on the paper A Cellular Automata Model for Forest Fire Spread Prediction: The Case of the Wildfire That Swept Through Spetses Island in 1990.
The model uses the following formula to calculate the probability of a cell propagating fire to its neighbors:
$$ p_\text{propagate} = p_h (1 + p_\text{veg}) (1 + p_\text{den}) p_w p_s $$
in which,
$$ p_w = \exp(c_1 V_w) \exp(c_2 V_w (\cos(\theta_w) - 1)) $$
$$ p_s = \exp(a \theta_s) $$
The model also uses the following formula to correct the probability into correct range:
$$ f_p(x) = \tanh(c \cdot x) \text{, where } c=1.1486328125$$
To calculate the probability of a cell igniting by its neighbors, the model uses the following formula:
$$ p_{\text{ignite}, i} = 1 - \prod_{j=1}^{8} (1 - p_{\text{propagate}, i, j}) $$
| Attributes: |
|
|---|
Examples:
from pytorchfire import WildfireModel
model = WildfireModel() # Create a model with default parameters and environment data
model = model.cuda() # Move the model to GPU
# model.reset(seed=seed) # Reset the model with a seed
for _ in range(100): # Run the model for 100 steps
model.compute() # Compute the next state
Initialize the WildfireModel.
| Parameters: |
|
|---|
Examples:
SIZE = 500
environment_data = {
'p_veg': torch.rand(SIZE, SIZE),
'p_den': torch.rand(SIZE, SIZE),
'wind_velocity': torch.rand(SIZE, SIZE) * 10,
'wind_towards_direction': torch.rand(SIZE, SIZE) * 360,
'slope': torch.rand(SIZE, SIZE, 3, 3) * 90,
'initial_ignition': torch.rand(SIZE, SIZE) > .9,
}
parameters = {
'a': torch.tensor(.1),
'p_h': torch.tensor(.3),
'c_1': torch.tensor(.1),
'c_2': torch.tensor(.1),
'p_continue': torch.tensor(.3),
}
model = WildfireModel(env_data=environment_data, params=parameters) # Create a model with custom parameters
Source code in pytorchfire/model.py
def __init__(self, env_data: dict = None, params: dict = None, keep_acc_mask: bool = False):
"""
Initialize the WildfireModel.
Parameters:
env_data (dict):
The environment data for the model.
- `p_veg` (torch.Tensor): The scaling factor for vegetation type.
- `p_den` (torch.Tensor): The scaling factor for vegetation density.
- `wind_velocity` (torch.Tensor): The wind velocity. Unit is m/s.
- `wind_towards_direction` (torch.Tensor): The wind direction. Starting from East and going
counterclockwise in degrees.
- `slope` (torch.Tensor): The slope of the cell to its neighboring cells. Unit is degrees.
- `initial_ignition` (torch.Tensor): The initial ignition of the cells.
params (dict):
The parameters for the model.
- `a` (torch.float): The scaling factor for ground elevation.
- `p_h` (torch.float): The probability that a fire propagate from a burning cell to an adjacent cell
under normal conditions.
- `c_1` (torch.float): The scaling factor for wind velocity.
- `c_2` (torch.float): The scaling factor for wind direction.
- `p_continue` (torch.float): The probability that a burning cell continue to burn at next time step.
keep_acc_mask (bool):
If `true`, the accumulator mask will be kept in the model.
- `false`, by default, to speed up the model.
- `true` if `model.accumulator_mask` is needed to see how the rings of cells are accumulated.
Examples:
```python
SIZE = 500
environment_data = {
'p_veg': torch.rand(SIZE, SIZE),
'p_den': torch.rand(SIZE, SIZE),
'wind_velocity': torch.rand(SIZE, SIZE) * 10,
'wind_towards_direction': torch.rand(SIZE, SIZE) * 360,
'slope': torch.rand(SIZE, SIZE, 3, 3) * 90,
'initial_ignition': torch.rand(SIZE, SIZE) > .9,
}
parameters = {
'a': torch.tensor(.1),
'p_h': torch.tensor(.3),
'c_1': torch.tensor(.1),
'c_2': torch.tensor(.1),
'p_continue': torch.tensor(.3),
}
model = WildfireModel(env_data=environment_data, params=parameters) # Create a model with custom parameters
```
"""
super(WildfireModel, self).__init__()
env_data = {} if env_data is None else env_data
params = {} if params is None else params
self.keep_acc_mask = keep_acc_mask
self.register_parameter('a', nn.Parameter(params.get('a', torch.tensor(.0))))
self.register_parameter('p_h', nn.Parameter(params.get('p_h', torch.tensor(.3))))
self.register_parameter('c_1', nn.Parameter(params.get('c_1', torch.tensor(.0))))
self.register_parameter('c_2', nn.Parameter(params.get('c_2', torch.tensor(.0))))
self.register_parameter('p_continue',
nn.Parameter(params.get('p_continue', torch.tensor(.3)), requires_grad=False))
self.register_buffer('p_veg', env_data.get('p_veg', torch.zeros(DEFAULT_SIZE, DEFAULT_SIZE)))
self.register_buffer('p_den', env_data.get('p_den', torch.zeros_like(self.p_veg)))
self.register_buffer('wind_velocity', env_data.get('wind_velocity', torch.zeros_like(self.p_veg)))
self.register_buffer('wind_towards_direction',
env_data.get('wind_towards_direction', torch.zeros_like(self.p_veg)))
self.register_buffer('slope', env_data.get('slope', repeat(torch.zeros_like(self.p_veg), 'h w -> h w 3 3')))
self.register_buffer('initial_ignition',
env_data.get('initial_ignition', torch.zeros_like(self.p_veg, dtype=torch.bool)))
self.register_buffer('state', self._initialize_state(self.initial_ignition))
if self.training:
self.register_buffer('accumulator', self._initialize_accumulator(self.initial_ignition))
if self.keep_acc_mask:
self.register_buffer('accumulator_mask', self._initialize_accumulator_mask(self.accumulator))
self.seed = self._initialize_seed()
self.sanity_check()
compute(attach=False)
Compute the next state of the cells.
| Parameters: |
|
|---|
Source code in pytorchfire/model.py
def compute(self, attach: bool = False):
"""
Compute the next state of the cells.
Parameters:
attach (bool):
If `true`, all newly ignited cells in current step will be attached to the accumulator.
"""
burning, burned = self.state
new_state = self.state.clone()
new_burning, new_burned = new_state
p_ignite = self.p_ignite()
rand_propagate, rand_continue = torch.rand_like(p_ignite), torch.rand_like(p_ignite)
# burnable cells have p_burn probability to become burning
burnable = ~(burning | burned)
new_burning_digits = torch.where(burnable, nn.functional.relu(p_ignite - rand_propagate), 0)
new_burning_mask = new_burning_digits > 0
if self.training:
if attach:
self.accumulator = self.accumulator + torch.where(new_burning_mask, p_ignite, 0)
if self.keep_acc_mask:
self.accumulator_mask[new_burning_mask] = True
else:
self.accumulator = self.accumulator + torch.where(new_burning_mask, p_ignite.detach(), 0)
new_burning[new_burning_mask] = True
# burning cells have p_continue probability to continue burning
will_burn_out_digits = torch.where(burning, nn.functional.relu(rand_continue - self.p_continue), 0)
will_burn_out_mask = will_burn_out_digits > 0
new_burning[will_burn_out_mask] = False
new_burned[will_burn_out_mask] = True
self.state = new_state
detach_accumulator()
Detach the accumulator from the computation graph.
This method is used to detach the accumulator from the computation graph to avoid memory leak or out of memory.
This method will not affect the model's behavior on simulation, and the accumulator can still be attached in the next step.
Examples:
model = WildfireModel()
model.compute(attach=True)
model.detach_accumulator()
model.compute(attach=True)
Source code in pytorchfire/model.py
def detach_accumulator(self):
"""
Detach the accumulator from the computation graph.
This method is used to detach the accumulator from the computation graph to avoid memory leak or out of memory.
This method will not affect the model's behavior on simulation, and the accumulator can still be attached
in the next step.
Examples:
```python
model = WildfireModel()
model.compute(attach=True)
model.detach_accumulator()
model.compute(attach=True)
```
"""
if self.training:
self.accumulator = self.accumulator.detach().clone().requires_grad_(True)
p_ignite()
Calculate the probability of a cell igniting by its neighbors.
The returned tensor is connected to the computation graph, and it can be used for back-propagation.
| Returns: |
|
|---|
Examples:
python
model = WildfireModel()
p_ignite = model.p_ignite()python
Source code in pytorchfire/model.py
def p_ignite(self) -> torch.Tensor:
"""
Calculate the probability of a cell igniting by its neighbors.
The returned tensor is connected to the computation graph, and it can be used for back-propagation.
Returns:
The probability of a cell igniting by its neighbors.
- dtype: `torch.float`
- shape: `[Height, Width]`
Examples:
```python
model = WildfireModel()
p_ignite = model.p_ignite()
```python
"""
burning, _ = self.state
p_s = torch.exp(self.a * self.slope)
# to be used to calculate the angle between wind and fire direction
wind_offset = torch.tensor([[3, 2, 1], [4, 0, 0], [5, 6, 7]], device=p_s.device) * 45
wind_offset_tiled = repeat(wind_offset, 'c1 c2 -> 1 1 c1 c2', c1=3, c2=3)
wind_towards_direction_expanded = repeat(self.wind_towards_direction, 'h w -> h w 1 1')
wind_velocity_expanded = repeat(self.wind_velocity, 'h w -> h w 1 1')
p_w = torch.exp(self.c_1 * wind_velocity_expanded) * torch.exp(self.c_2 * wind_velocity_expanded * (
torch.cos(torch.deg2rad((wind_offset_tiled - wind_towards_direction_expanded) % 360)) - 1))
p_propagate = repeat(self.p_h * (1 + self.p_veg) * (1 + self.p_den), 'h w -> h w 1 1') * p_s * p_w
prob_like_act_c = 1.1486328125
p_propagate = torch.tanh(prob_like_act_c * p_propagate)
# out-of-bounds access in p_propagate is avoided by the slicing, and in state will result in 0
p_out = torch.zeros_like(p_propagate)
p_out[:-1, :-1, 0, 0] = torch.where(burning[1:, 1:], p_propagate[:-1, :-1, 0, 0], 0)
p_out[:-1, :, 0, 1] = torch.where(burning[1:, :], p_propagate[:-1, :, 0, 1], 0)
p_out[:-1, 1:, 0, 2] = torch.where(burning[1:, :-1], p_propagate[:-1, 1:, 0, 2], 0)
p_out[:, :-1, 1, 0] = torch.where(burning[:, 1:], p_propagate[:, :-1, 1, 0], 0)
p_out[:, 1:, 1, 2] = torch.where(burning[:, :-1], p_propagate[:, 1:, 1, 2], 0)
p_out[1:, :-1, 2, 0] = torch.where(burning[:-1, 1:], p_propagate[1:, :-1, 2, 0], 0)
p_out[1:, :, 2, 1] = torch.where(burning[:-1, :], p_propagate[1:, :, 2, 1], 0)
p_out[1:, 1:, 2, 2] = torch.where(burning[:-1, :-1], p_propagate[1:, 1:, 2, 2], 0)
p_out = 1 - reduce(1 - p_out, 'h w c1 c2 -> h w', 'prod', c1=3, c2=3)
return p_out
reset(seed=None)
Reset the model with a new seed.
| Parameters: |
|
|---|
Examples:
model = WildfireModel()
model.reset(seed=123)
Source code in pytorchfire/model.py
def reset(self, seed: int = None):
"""
Reset the model with a new seed.
Parameters:
seed (int):
The seed for random number generator.
If not provided, a new seed will be generated.
Examples:
```python
model = WildfireModel()
model.reset(seed=123)
```
"""
self.sanity_check()
self.state = self._initialize_state(self.initial_ignition)
if self.training:
self.accumulator = self._initialize_accumulator(self.initial_ignition)
if self.keep_acc_mask:
self.accumulator_mask = self._initialize_accumulator_mask(self.accumulator)
self.seed = self._initialize_seed(seed)
sanity_check()
Perform sanity checks on the model.
This method checks if all parameters and buffers have the correct shape and device.
| Raises: |
|
|---|
Examples:
model = WildfireModel()
model.sanity_check()
Source code in pytorchfire/model.py
def sanity_check(self):
"""
Perform sanity checks on the model.
This method checks if all parameters and buffers have the correct shape and device.
Raises:
AssertionError: If the model is not properly initialized.
Examples:
```python
model = WildfireModel()
model.sanity_check()
```
"""
assert self.a.shape == self.p_h.shape == self.c_1.shape == self.c_2.shape == self.p_continue.shape == ()
assert (
self.p_veg.shape == self.p_den.shape == self.wind_velocity.shape == self.wind_towards_direction.shape == self.initial_ignition.shape)
assert self.slope.shape == (*self.p_veg.shape, 3, 3)
assert self.state.shape == (2, *self.p_veg.shape)
assert (
self.a.device == self.p_h.device == self.c_1.device == self.c_2.device == self.p_continue.device == self.p_veg.device == self.p_den.device == self.wind_velocity.device == self.wind_towards_direction.device == self.slope.device == self.initial_ignition.device == self.state.device)
assert self.initial_ignition.dtype == self.state.dtype == torch.bool
if self.training:
assert self.accumulator.shape == self.initial_ignition.shape
assert self.accumulator.device == self.state.device
if self.keep_acc_mask:
assert self.accumulator_mask.shape == self.accumulator.shape
assert self.accumulator_mask.device == self.state.device
assert self.accumulator_mask.dtype == torch.bool