File size: 5,731 Bytes
338c3e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
from typing import Optional, Dict, List
from itertools import product

from torch import nn, Tensor
import torch

from .base_model import CfdModel
from .loss import MseLoss
from .act_fn import get_act_fn


class Ffn(nn.Module):
    """
    A general fully connected multi-layer neural network.
    """

    def __init__(
        self, dims: list, act_fn: nn.Module, act_on_output: bool = False
    ):
        super().__init__()
        self.dims = dims

        layers = []
        for i in range(len(dims) - 2):
            layers.append(nn.Linear(dims[i], dims[i + 1]))
            layers.append(act_fn)
            # layers.append(NormAct(nn.Tanh()))
        layers.append(nn.Linear(dims[-2], dims[-1]))
        if act_on_output:
            layers.append(act_fn)
        self.layers = nn.Sequential(*layers)

    def forward(self, x: Tensor) -> Tensor:
        x = self.layers(x)
        return x


class FfnModel(CfdModel):
    """
    Non-autoregressive FNN for data-driven CFD.

    Branch net accepts the boundary and physics properties as inputs.
    Trunk net accepts the query location (t, x, y) as input.
    """

    def __init__(
        self,
        loss_fn: MseLoss,
        widths: List[int],
        act_name: str = "relu",
        act_norm: bool = True,
        act_on_output: bool = False,
        num_label_samples: int = 1000,
    ):
        """
        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.loss_fn = loss_fn
        self.widths = widths
        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.ffn = Ffn(
            self.widths, act_fn=act_fn, act_on_output=self.act_on_output
        )

    def forward(
        self,
        case_params: Tensor,  # Case parameters
        t: Tensor,
        label: Optional[Tensor] = None,
        query_idxs: Optional[Tensor] = None,
    ) -> Dict[str, Tensor]:
        """
        A faster forward by using all the points in the frame (`label`) at
        time step `t` as training examples.

        Args:
        - x_branch: (b, branch_dim), input to the branch net.
        - t: (b), input to the trunk net, a batch of t
        - label: (b, w, h), the frame to be predicted.
        - query_idxs: (b, k, 2), the query locations.
        """

        batch_size, dim_in = case_params.shape

        if query_idxs is None:
            # Create k query locations on a lattice.
            # (b, k, 2), where each element is (x, y)
            assert label is not None
            height, width = label.shape[-2:]
            query_idxs = torch.stack(
                [
                    torch.randint(
                        0,
                        height,
                        (self.num_label_samples,),
                        device=label.device,
                    ),
                    torch.randint(
                        0,
                        width,
                        (self.num_label_samples,),
                        device=label.device,
                    ),
                ],
                dim=-1,
            )  # (k, 2)

        # Concatenate query spatial coordinates with time coordinate
        coords = query_idxs.unsqueeze(0)  # (1, k, 2)
        coords = coords.repeat(batch_size, 1, 1)  # (b, k, 2)
        num_queries = coords.shape[1]
        t = t.unsqueeze(-1)  # (b, 1)
        t = t.repeat(1, num_queries, 1)  # (b, k)
        coords = torch.cat([coords, t], dim=-1)  # (b, k, 3)

        # Concatenate case params with queries
        case_params = case_params.unsqueeze(1)  # (b, 1, p)
        case_params = case_params.repeat(1, num_queries, 1)  # (b, k, p)
        inp = torch.cat([case_params, coords], dim=-1)  # (b, k, p + 3)
        inp = inp.view(batch_size * num_queries, -1)  # (bk, p + 3)
        preds = self.ffn(inp)  # (bk, out_dim)
        preds = preds.view(batch_size, num_queries)  # (b, k)

        if label is not None:
            # Use only the u channel
            label = label[:, 0]  # (B, w, h)
            labels = label[:, query_idxs[:, 0], query_idxs[:, 1]]  # (b, k)
            assert (
                preds.shape == labels.shape
            ), f"{preds.shape}, {labels.shape}"
            loss = self.loss_fn(preds=preds, labels=labels)  # (b, k)
            return dict(
                preds=preds,
                loss=loss,
            )
        return dict(
            preds=preds,
        )

    def generate_one(
        self, case_params: Tensor, t: Tensor, height: int, width: int
    ) -> Tensor:
        """
        Generate one frame at time t.

        Args:
        - x_branch: Tensor, (branch_dim)
        - t: Tensor, (1,)
        - height: int
        - width: int

        Returns:
            (b, c, h, w)
        """
        if len(case_params.shape) == 1:
            case_params = case_params.unsqueeze(0)
        if len(t.shape) == 0:
            t = t.unsqueeze(0).unsqueeze(0)
        elif len(t.shape) == 1:
            t = t.unsqueeze(0)

        # Create 2D lattice of query points to infer the frame.
        query_idxs = torch.tensor(
            list(product(range(height), range(width))),
            # dtype=torch.long,
            device=case_params.device,
        )  # (h * w, 2)

        # query_points = query_points / 100
        # (b, 1, h * w)
        output = self.forward(case_params, t=t, query_idxs=query_idxs)["preds"]
        output = output.view(-1, 1, height, width)  # (b, 1, h, w)
        return output