| """ConvLSTM model for center-pixel next-day wildfire danger.""" |
|
|
| from __future__ import annotations |
|
|
| import torch |
| from torch import nn |
|
|
|
|
| class ConvLSTMCell(nn.Module): |
| """Standard ConvLSTM cell with input, forget, output, and candidate gates.""" |
|
|
| def __init__(self, input_channels: int, hidden_channels: int, kernel_size: int = 3): |
| super().__init__() |
| self.hidden_channels = int(hidden_channels) |
| padding = kernel_size // 2 |
| self.gates = nn.Conv2d( |
| input_channels + hidden_channels, 4 * hidden_channels, |
| kernel_size=kernel_size, padding=padding, |
| ) |
|
|
| def forward(self, inputs: torch.Tensor, state: tuple[torch.Tensor, torch.Tensor]): |
| hidden, cell = state |
| input_gate, forget_gate, output_gate, candidate = self.gates( |
| torch.cat((inputs, hidden), dim=1) |
| ).chunk(4, dim=1) |
| input_gate = torch.sigmoid(input_gate) |
| forget_gate = torch.sigmoid(forget_gate) |
| output_gate = torch.sigmoid(output_gate) |
| candidate = torch.tanh(candidate) |
| next_cell = forget_gate * cell + input_gate * candidate |
| next_hidden = output_gate * torch.tanh(next_cell) |
| return next_hidden, next_cell |
|
|
|
|
| class FireCubeNet(nn.Module): |
| """Propagate ConvLSTM state over ten days and classify the center pixel.""" |
|
|
| def __init__(self, input_channels: int = 25, hidden_channels: int = 4, |
| kernel_size: int = 3, dropout: float = 0.1): |
| super().__init__() |
| self.input_channels = int(input_channels) |
| self.hidden_channels = int(hidden_channels) |
| self.cell = ConvLSTMCell(input_channels, hidden_channels, kernel_size) |
| self.head = nn.Sequential(nn.Dropout(dropout), nn.Linear(hidden_channels, 1)) |
|
|
| def forward(self, inputs: torch.Tensor) -> torch.Tensor: |
| if inputs.ndim != 5 or inputs.shape[2] != self.input_channels: |
| raise ValueError( |
| f"expected BTCHW with C={self.input_channels}, got {tuple(inputs.shape)}" |
| ) |
| batch, _, _, height, width = inputs.shape |
| hidden = inputs.new_zeros(batch, self.hidden_channels, height, width) |
| cell = inputs.new_zeros(batch, self.hidden_channels, height, width) |
| for time_index in range(inputs.shape[1]): |
| hidden, cell = self.cell(inputs[:, time_index], (hidden, cell)) |
| center_features = hidden[:, :, height // 2, width // 2] |
| return self.head(center_features) |
|
|