| |
|
|
| """ |
| Contains the implementation of autoregressive DeepONet |
| """ |
|
|
| from itertools import product |
| from typing import List, Optional |
|
|
| import torch |
| from torch import nn, Tensor |
|
|
| from .ffn import Ffn |
| from .base_model import AutoCfdModel |
| from .act_fn import get_act_fn |
| from .loss import MseLoss |
|
|
|
|
| class AutoDeepONet(AutoCfdModel): |
| """ |
| Auto-regressive DeepONet for CFD. |
| |
| Our task is different from the one that the original DeepONet. In the |
| original DeepONet, the input function (input to the branch net) |
| is the initial condition (IC), but here, we have a fixed (zero) IC. |
| Instead, we have different boundary conditions (BCs), but we also |
| want the model to predict the next time step given the current time step. |
| Ideally, we should have two different branch nets, one accepting the |
| BCs, one accepting the current time step. |
| |
| Here, we assume that the current time step includes the information about |
| BCs (which are the values on the bounds), so we just feed |
| the current time step to one branch net. |
| """ |
|
|
| def __init__( |
| self, |
| branch_dim: int, |
| trunk_dim: int, |
| loss_fn: MseLoss, |
| num_label_samples: int = 1000, |
| branch_depth: int = 4, |
| trunk_depth: int = 4, |
| width: int = 100, |
| act_name="relu", |
| act_norm: bool = False, |
| act_on_output: bool = False, |
| ): |
| """ |
| Args: |
| - branch_dim: int, the dimension of the branch net input. |
| - trunk_dim: int, the dimension of the trunk net input. |
| """ |
| super().__init__(loss_fn) |
| self.branch_dim = branch_dim |
| self.trunk_dim = trunk_dim |
| self.branch_depth = branch_depth |
| self.trunk_depth = trunk_depth |
| self.width = width |
| self.act_name = act_name |
| self.act_norm = act_norm |
| self.act_on_output = act_on_output |
| self.num_label_samples = num_label_samples |
|
|
| act_fn = get_act_fn(act_name, act_norm) |
| self.branch_dims = [branch_dim] + [width] * branch_depth |
| self.trunk_dims = [trunk_dim] + [width] * trunk_depth |
| self.branch_net = Ffn( |
| self.branch_dims, |
| act_fn=act_fn, |
| act_on_output=act_on_output, |
| ) |
| self.trunk_net = Ffn(self.trunk_dims, act_fn=act_fn) |
| self.bias = nn.Parameter(torch.zeros(1)) |
|
|
| def forward( |
| self, |
| inputs: Tensor, |
| case_params: Tensor, |
| label: Optional[Tensor] = None, |
| mask: Optional[Tensor] = None, |
| query_idxs: Optional[Tensor] = None, |
| ): |
| """ |
| Here, we just randomly sample some points, and use the label values on |
| those points as the label. |
| |
| ### Args |
| - inputs: (b, c, h, w) |
| - case_params: (b, p) |
| - labels: (b, c, h, w) |
| - query_point: (k, 2), k is the number of query points, each is |
| an (x, y) coordinate. |
| - masks: For future use. |
| |
| ### Returns |
| Output: Tensor, if query_points is not None, the shape is (b, k). |
| Else, the shape is (b, c, h, w). |
| |
| Notations: |
| - b: batch size |
| - c: number of channels |
| - h: height |
| - w: width |
| - p: number of case parameters |
| - k: number of query points |
| """ |
| batch_size, num_chan, height, width = inputs.shape |
| |
| inputs = inputs[:, 0] |
| |
| flat_inputs = inputs.view(batch_size, -1) |
|
|
| |
| |
| flat_inputs = torch.cat([flat_inputs, case_params], dim=1) |
| x_branch = self.branch_net(flat_inputs) |
|
|
| if query_idxs is None: |
| query_idxs = torch.tensor( |
| list(product(range(height), range(width))), |
| dtype=torch.long, |
| device=flat_inputs.device, |
| ) |
|
|
| |
| x_trunk = (query_idxs.float() - 50) / 100 |
| x_trunk = self.trunk_net(x_trunk) |
| x_trunk = x_trunk.unsqueeze(0) |
| x_branch = x_branch.unsqueeze(1) |
| preds = torch.sum(x_branch * x_trunk, dim=-1) + self.bias |
|
|
| |
| residuals = inputs[:, query_idxs[:, 0], query_idxs[:, 1]] |
| preds += residuals |
|
|
| if label is not None: |
| label = label[:, 0] |
| labels = label[:, query_idxs[:, 0], query_idxs[:, 1]] |
| loss = self.loss_fn(labels=labels, preds=preds) |
| return dict( |
| preds=preds, |
| loss=loss, |
| ) |
|
|
| preds = preds.view(-1, 1, height, width) |
| return dict(preds=preds) |
|
|
| def generate( |
| self, inputs: Tensor, case_params: Tensor, mask: Tensor |
| ) -> Tensor: |
| """ |
| x: (c, h, w) or (B, c, h, w) |
| |
| Returns: |
| (b, c, h, w) |
| """ |
| if inputs.dim() == 3: |
| inputs = inputs.unsqueeze(0) |
| batch_size, num_chan, height, width = inputs.shape |
| query_idxs = torch.tensor( |
| list(product(range(height), range(width))), |
| dtype=torch.long, |
| device=inputs.device, |
| ) |
| |
| |
| preds = self.forward( |
| inputs, case_params=case_params, query_idxs=query_idxs, mask=mask |
| )["preds"] |
| preds = preds.view(-1, 1, height, width) |
| return preds |
|
|
| def generate_many( |
| self, inputs: Tensor, case_params: Tensor, mask: Tensor, steps: int |
| ) -> List[Tensor]: |
| """ |
| Args: |
| x: (c, h, w) or (B, c, h, w) |
| mask: (h, w). 1 for interior, 0 for boundaries. |
| steps: int, number of steps to generate. |
| |
| Returns: |
| list of tensors, each of shape (b, c, h, w) |
| """ |
| assert len(inputs.shape) == len(case_params.shape) + 2 |
| if inputs.dim() == 3: |
| inputs = inputs.unsqueeze(0) |
| case_params = case_params.unsqueeze(0) |
| mask = mask.unsqueeze(0) |
| assert inputs.shape[0] == case_params.shape[0] |
| cur_frame = inputs |
| preds = [] |
| for _ in range(steps): |
| |
| cur_frame = self.generate( |
| inputs=cur_frame, case_params=case_params, mask=mask |
| ) |
| preds.append(cur_frame) |
| return preds |
|
|