---
# System prepended metadata

title: "\U0001F9E0CFD 模擬結合機器學習方法應用"

---

# 🧠CFD 模擬結合機器學習方法應用
這個研究的內容主要是使用 CFD 軟體去產生不同外形在不同流場條件下的模擬結果，利用這些模擬結果建立資料庫，後續結合機器學習的方法，使用該資料庫去訓練神經網路模型，使該模型能夠使用外型以及流場條件作為輸入，==快速預測==需要==長時間模擬==的 CFD 結果

主要包含三個部分
- CFD 模擬
- 資料處理及建置資料庫
- 機器學習應用

## CFD 模擬
CFD 簡單來說就是透過數值方法在電腦上求解，它的核心概念將整個計算流場分割成多個有限的控制體積 (Control volumes)，並在這些有限體積上去計算守恆方程式 (質量、動量及能量守恆)。  

目前實驗室使用開源軟體 `OpenFOAM` 作為主要使用的模擬工具
- 目前使用的版本為 ==v10==, ==v2206==
- 使用的==求解器==為在 `OpenFOAM` 基礎上開發的 `HiSA` 用於高馬赫數流場環境模擬

:::info
- 模擬案例建置完成後，在模擬開始前需要特別注意輸入指令的位置 (`cd directory`)，跟 `OpenFOAM` 模擬相關的指令只會在 `OpenFOAM` 案例資料夾中才能正常運作
:::

---

### Quick start
在環境建立完成後可以嘗試去模擬 OpenFOAM 本身內建的教學案例，這些案例可以透過幾個方法抓取到適合執行的路徑位址上
- 從已安裝的 OpenFOAM 資料夾中複製
    - 直接打開安裝在電腦上的 OpenFOAM 資料夾，打開其中的 `tutorial` 資料夾
    - 複製要執行的案例到目標位置 (通常會是 `Ubuntu/home/userName/OpenFOAM/userName-version/run/` 中)
- 使用終端指令 `cp -r OpenFOAM_tutorial_case_path .` 複製
    - 同上，但是從官方網站提供的資料夾架構來尋找案例的路徑位置
    - `OpenFOAM_v10` 可以參考 [github code](https://github.com/OpenFOAM/OpenFOAM-10)
    - `OpenFOAM_v2206` 則是參考 [gitlab code](https://gitlab.com/openfoam/openfoam/-/tree/maintenance-v2206?ref_type=heads)
- 例如我現在使用 `OpenFOAM_v2212`，我現在打算抓取[這個案例 `incompressible/motorBike`](https://gitlab.com/openfoam/openfoam/-/tree/maintenance-v2212/tutorials/incompressible/simpleFoam/motorBike?ref_type=heads)
    - 我會先將我的終端位址移動到適合的位置 `cd $FOAM_RUN`
    - 在這個位置中，將要模擬的案例複製到當前位置 `cp -r $FOAM_TUTORIALS/incompressible/simpleFoam/motorBike .`
    - 完成後輸入 `ls` 便可以看到當前資料夾位置中出現這個 `motorBike` 的 OpenFOAM 案例了

:::info
- 安裝完成後可以輸入 `blockMesh -help` 來確認是否安裝完成以及當前使用的版本 (`blockMesh` 是 OpenFOAM 內建的指令，後面加上 `-help` 可以查看該指令的功能)
:::

### 簡單介紹
一個簡單的 `OpenFOAM` 案例會包含三個主架構
- `0/`：定義初始/邊界條件
- `constant/`：包含網格資訊以及流場環境的物理特性
- `system/`：控制模擬進程以及使用的數值方法


#### `0/`
在這個資料夾中會包含主要流場變數名稱的文件，在這些文件中可以調整使用的初始/邊界條件，下面使用 `tutorials/compressible/rhoPimpleFoam/RAS/aerofoilNACA0012/0` 案例中的 `U` 來介紹

```csharp=
/*--------------------------------*- C++ -*----------------------------------*\
  =========                 |
  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
   \\    /   O peration     | Website:  https://openfoam.org
    \\  /    A nd           | Version:  10
     \\/     M anipulation  |
\*---------------------------------------------------------------------------*/
FoamFile
{
    format      ascii;
    class       volVectorField;
    object      U;
}
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //

Uinlet          (200 0 0);

dimensions      [0 1 -1 0 0 0 0];

internalField   uniform $Uinlet;

boundaryField
{
    freestream
    {
        type            freestreamVelocity;
        freestreamValue uniform $Uinlet;
    }

    wall
    {
        type            noSlip;
    }

    #includeEtc "caseDicts/setConstraintTypes"
}

// ************************************************************************* //
```
- 開頭
    標示使用的 `OpenFOAM` 版本，基本上不影響整份文件的內容
- `FoamFile`
    定義這份文件的格式及名稱，名稱的部分需要跟文件的實際名稱相同
- `dimensions`
    這份文件定義的變數使用的單位構成
- `internalField`
    流場的實際數值
    - 這裡的定義是方便後續能夠直接使用
    - 不同案例可能會有不同的定義方式
- `boundaryField`
    根據使用的網格設置的邊界名稱來決定該邊界需要設置的邊界條件
  
:::info

其他的文件也是類似的設置，但會根據使用的==求解器==或是根據你==模擬案例==需求需要定義更多的流場變量
:::

#### `constant/`
這個資料夾中主要包含==網格資訊==以及==流場物理子模型==
- 網格
    網格資訊會在 `constant/polyMesh/` 資料夾中，裡面會有該網格的資訊，包含定義的邊界名稱、點線面的數量、網格的數量及各邊界構成的位置，在`constant/polyMesh/boudary`中可定義各個面的邊界條件，例如freestream、wall
- 流場物理特性
    - 在前面使用的案例 `aerofoilNACA0012`，其中用於定義物理子模型的檔案為 `physicalProperties` 以及 `momentumTransport`
    - 其中 `physicalProperties` 定義流場的物理特性
    - `momentumTransport` 定義使用的紊流模型 (`K-omega-SST`, `RAS` / `LES`)

:::info
基本上剛開始不會需要大幅度的調整，在後續需要有更精準的模擬才去另外調整
- k-omega估算公式
    - k = 1.5 * (U * I) ** 2
    - omega = (0.09**(-0.25))*((k**0.5)/mixingLength)
    - I=0.001，U=遠場速度，mixingLength=1e-05
:::



#### `system/`
該資料夾中主要包含==模擬進程相關的檔案==及 `OpenFOAM` 內建==後處理使用的工具==
- 模擬相關控制
    - `controlDict`：控制模擬使用的求解器、時間起始/結束，時間步長
    - `fvSolution`：控制模擬的收斂條件、速度以及最終模擬的準確度
    - `fvSchemes`：使用的數值方法，不同於求解器但會影響模擬的穩定性能及準確度
- 後處理工具
    在模擬完成後，需要處理模擬的結果才能有效的儲存需要的資料，`OpenFOAM` 內建許多後處理工具可以做使用，這邊列出幾個常用的後處理工具
    - `boundaryProbes`：在給入 ==point cloud== 的條件下，投影這些點到目標表面上，可以得到表面上的模擬結果
    - `internalProbes`：同樣給入 ==point cloud== 可以得到該截面上的資訊
    - `forceCoeffs`：可以計算模擬結果的各項力係數

:::info
不同版本的 `OpenFOAM` 使用的後處理工具名稱會不同，`v10` 使用的會是 `Probes`，`v2206`使用的則是 `Cloud`
:::
- 以下代碼為寫指定範圍(internalCloud)point cloud的例子，其point cloud資訊會存到'system/include/AIP'中為之後後處理使用
```python
import numpy as np
y_values_AIP = [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9]
x_min_AIP, x_max_AIP = -0.15, 2.35
z_min_AIP, z_max_AIP = -1.25, 1.25
res_AIP = 540

x_values_AIP = np.linspace(x_min_AIP, x_max_AIP, res_AIP)
z_values_AIP = np.linspace(z_min_AIP, z_max_AIP, res_AIP)
write_path_AIP = 'system/include/AIP'
with open(write_path_AIP, 'w') as file:
    file.write("pts\n(\n")
    for AIP_z in z_values_AIP:
        for AIP_y in y_values_AIP:
            AIP_y *= 1.1963
            for AIP_x in x_values_AIP:
                file.write(f"({AIP_x} {AIP_y} {AIP_z})\n")
    file.write(");")
print(f'Write the surface points in {write_path_AIP} file.')
```
![internalCloud印出結果](https://hackmd.io/_uploads/H1wQmzFVxe.png)

![boundaryCloud印出結果](https://hackmd.io/_uploads/Byu8XMt4gl.png)


---

### 可嘗試的方向
- 調整不同的初始/邊界條件，熟悉模擬案例的結構及流程
- 利用在 `tutorial` 中已有的案例來熟悉不同的求解器使用方法及情境
- 嘗試使用 `snappyHexMesh` 或是其他網格生成軟體(Fluent)來建立不同外形的網格並模擬
- 對有文獻資料的實際案例去嘗試模擬並比對


## 資料庫建置
在模擬完成後會使用後處理工具來將模擬的結果以適當的方式儲存，大概可以分成兩個部分
- 後處理結果可視化觀察是否符合預期
- 使用 `python3` 建立腳本實現自動化模擬並能夠自動處理模擬結果

### 後處理資料分析與可視化
這邊用這段使用 `boundaryProbes` 做後處理並處理資料的代碼做示例，簡單介紹 `python3` 的語法和這段代碼的邏輯

```python
def boundaryProbes(case, surface=None, mode=None, output_res=OUTPUT_RES):
    cwd = os.getcwd()
    os.chdir(f"{case}/")
    with open("system/boundaryProbes_template", "rt") as inFile:
        with open("system/boundaryProbes", "wt") as outFile:
            for line in inFile:
                if surface == "upper_surface":
                    line = line.replace("POINT_CLOUD", "system/include/upper_pts")
                elif surface == "lower_surface":
                    line = line.replace("POINT_CLOUD", "system/include/lower_pts")
                outFile.write(line)
                
    if mode == "All":
        os.system("hisa -postProcess -func wallShearStress > LOG.log")
        os.system("postProcess -func boundaryProbes > LOG.log")
        os.chdir(cwd)

    elif mode == "latestTime":
        os.system("hisa -postProcess -latestTime -func wallShearStress > LOG.log")
        os.system("postProcess -latestTime -func boundaryProbes > LOG.log")
        os.chdir(cwd)
    
    step_lst = [f for f in os.listdir(f"{case}/postProcessing/boundaryProbes/") if f.replace(".", "").isdigit() and float(f) != 0]
    xmin, xmax = XMIN, XMAX
    ymin, ymax = YMIN, YMAX
    res = output_res
    boundary_outputs = np.full((len(step_lst), 4, res, res), np.nan)
    
    step_idx = 0
    for step in step_lst:
        step_path = os.path.join(f"{case}/postProcessing/boundaryProbes/", step)
        file_path = os.path.join(step_path, os.listdir(step_path)[0])
        content = pd.read_csv(file_path, delimiter="\s+", skiprows=0, header=1, names=['x','y','z','p','p_x','p_y','p_z']).round(5)
        content['WallShearStress'] = np.sqrt(content['p_x']**2 + content['p_y']**2 + content['p_z']**2)

        x = content['x']
        y = content['y']
        norm_x = (x - xmin) / (xmax - xmin)
        norm_y = (y - ymin) / (ymax - ymin)

        boundary_outputs[step_idx][0][(norm_x * (res-1)).astype(int), (norm_y * (res-1)).astype(int)] = 0
        boundary_outputs[step_idx][1][(norm_x * (res-1)).astype(int), (norm_y * (res-1)).astype(int)] = content['z']
        boundary_outputs[step_idx][2][(norm_x * (res-1)).astype(int), (norm_y * (res-1)).astype(int)] = content['p']
        boundary_outputs[step_idx][3][(norm_x * (res-1)).astype(int), (norm_y * (res-1)).astype(int)] = content['WallShearStress']
        step_idx += 1

    return boundary_outputs
```

- `def` : 自定義函數，可以自己決定需要輸入的變數與後續要輸出的結果
- `os.getcwd()` : 使用 `python3` 內建的函數 `os` 中的 `cwd` 功能，能夠輸出當前資料夾的位置
    - `os` 函數主要是跟終端指令相關的使用
    - 常見的有 `os.chdir(dir/)`，跟在終端中使用的 `cd dir/` 相同
    - `os.listdir(dir/)` 會列出該位置下包含的所有資料名稱
    - `os.path.join(dir/, filename)` 會連接 `dir/` 跟 `filename` 輸出路徑
    - `os.system(commend)`，會跟在終端中直接輸入 `commend` 相同
    - `os.makedirs(dir/, exist_ok=Trud)`，會先確認 `dir/` 位置是否存在並決定是否建立資料夾
    - `os.system("hisa -postProcess -func wallShearStress > LOG.log")` 就是在終端中輸入該內容，這段的內容是做 `OpenFOAM` 的 `wallShearStress` 後處理
    
- `with open()`，會打開檔案的內容並決定後續執行的內容
    - 這裡是使用 `for line in inFiles` 會是逐行讀取打開文件的內容
    - 這裡的迴圈會是當條件吻合時替換該行的內容

- `step_lst = [f for f in os.listdir(f"{case}/postProcessing/boundaryProbes/") if f.replace(".", "").isdigit() and float(f) != 0]` f 會是在 `os.lisdir()` 輸出的所有資料名稱，當 f 替換 "."的內容為空白後 f 為 float 便會將 f 輸出存入 `step_lst` 中
- 最後的大迴圈 `for step in step_lst:` 則是將這些符合條件的檔案做下面的處理，主要為
    - `norm_x = (x - xmin) / (xmax - xmin)` 將數據正規化在 0 到 1 中
    - `boundary_outputs[step_idx][0][(norm_x * (res-1)).astype(int), (norm_y * (res-1)).astype(int)] = content['z']` 根據這些正規化後放大為圖片的 index 位置，根據這些位置填入對應的資料 (這行是 z 值)

:::info
- 這段代碼主要的功能為，執行 `boundaryProbes` 後處理得到物體表面的模擬結果資訊，後續讀取這些資訊並處理成圖片的格式存入矩陣中輸出
- 其他的代碼也是類似的概念
- [我的 github](https://github.com/PGGO26/OpenFOAM-DataBase/tree/main/SACCON/v10/dataGen) 有過去使用的完整代碼，可以做參考
:::

---

### 建立自動化腳本
先同上，懶得寫，主要會先確立要完成的目的後，在逐步完成代碼編寫並做後續測試


## 機器學習應用
機器學習應用的概念會是利用已有的資料庫，透過使用不同的神經網路模型 (DNN, CNN, UNet, LSTM, ...) 或是方法來分析該資料庫，根據不同的目的使用不同的技術來訓練適合的模型，預期能透過該模型快速根據輸入達成目的 (影像分割、數據生成、資料分類等)。

在目前實驗室中使用該技術的情境會是，使用 CFD 模擬不同外型/流場後建立資料庫，後續將該資料庫分成訓練/測試資料集，目的會是能夠使用神經網路模型在輸入跟 CFD 相似的條件 (外型、流場條件)能夠快速預測出使用 CFD 模擬得到的結果 (表面壓力分佈、不同截面的流場情況)，下面會根據這個情境去做介紹：

### 主要架構
主要有三個部分，資料預處理、模型架構建立、模型訓練與模型測試結果可視化


### 資料預處理
- 在訓練之前會需要規劃資料庫的使用方式，通常會分成訓練與測試資料集，分別在模型訓練與測試上分開使用，讓模型能針對未用於訓練使用的新資料去測試模型的泛化能力
- 由於目前使用的訓練代碼多使用 `pyTorch` 函數庫中的代碼，因此會需要調整目前資料庫的格式，能夠使用 `pyTorch` 中的資料庫讀取方法 `from torch.utils.data import Dataset`，主要會包含 `torch Dataset` 的原生格式、資料格式變換 (`tensor`)、正/反正規化與可選的資料增強方法

基本的架構會是使用類別 (class) 寫法提供 `Torch` 的 `Dataset` 讀取會使用的 `__len__()`, `__getitem__()`

```python=
class NPZDataset(Dataset):
    def __init__(self, folder_path, transform=None, augmentations=None):
        self.folder_path = folder_path
        self.npz_files = [
            os.path.join(folder_path, f) for f in os.listdir(folder_path) if f.endswith('.npz')
        ]
        self.transform = transform
        self.augmentations = augmentations

    def __len__(self):
        return len(self.npz_files)

    def __getitem__(self, idx):
        # 加載 npz 文件
        npz_file = self.npz_files[idx]
        npz_data = np.load(npz_file)
        sample = {
            'Thickness': npz_data['Map'][0],
            'Chamber': npz_data['Map'][1],
            'Upper_P': npz_data['Cp'][0],
            'Lower_P': npz_data['Cp'][1],
        }

        # 從文件名中提取 Mach 和 AOA
        filename = os.path.basename(npz_file)
        parts = filename.split('_')
        sample.update({
            'Mach': float(parts[-3]),
            'AOA': float(parts[-1].replace('.npz', '')),
            'baseName': filename
        })

        # 應用數據增強和轉換
        if self.augmentations:
            sample = self.augmentations(sample)
        if self.transform:
            sample = self.transform(sample)

        return sample
```

`ToTensor` 將資料轉換為 `tensor` 格式，`Normalize` 對資料做 L2 正規化處理，`Denormalizae` 在後續測試模型表現時需要將資料回復到與正規化前資料相同的尺度才能做比較

```python=
class ToTensor:
    def __call__(self, sample):
        processed_sample = {}
        for key, value in sample.items():
            if key in ['Thickness', 'Chamber', 'Upper_P', 'Lower_P']:
                processed_sample[key] = value.clone().detach().unsqueeze(0).to(dtype=torch.float32)
            else:
                # 保留非數據鍵值
                processed_sample[key] = value
        return processed_sample


class Normalize:
    def __init__(self, dataset, keys):
        """
        :param dataset: 數據集，用於計算全局 mean 和 std
        :param keys: 需要正規化的鍵值
        """
        self.keys = keys
        self.means = {}
        self.stds = {}

        all_data = {key: [] for key in keys}
        for sample in dataset:
            for key in keys:
                all_data[key].append(torch.tensor(sample[key], dtype=torch.float32))

        for key in keys:
            concatenated = torch.cat([d.flatten() for d in all_data[key]])
            self.means[key] = torch.mean(concatenated)
            self.stds[key] = torch.std(concatenated)

    def __call__(self, sample):
        normalized_sample = {}
        for key in self.keys:
            tensor = torch.tensor(sample[key], dtype=torch.float32)
            normalized_sample[key] = (tensor - self.means[key]) / self.stds[key]

        # 保留其他鍵值
        normalized_sample.update({k: sample[k] for k in sample if k not in normalized_sample})
        return normalized_sample


class Denormalize:
    def __init__(self, normalize_instance):
        """
        :param normalize_instance: Normalize 的實例，用於提取 mean 和 std
        """
        self.means = normalize_instance.means
        self.stds = normalize_instance.stds

    def __call__(self, sample):
        denormalized_sample = {}
        for key, value in sample.items():
            if key in self.means and key in self.stds:
                tensor = torch.tensor(value, dtype=torch.float32)
                denormalized_sample[key] = tensor * self.stds[key] + self.means[key]
            else:
                # 保留其他未正規化的鍵
                denormalized_sample[key] = value

        return denormalized_sample
```

### 模型架構
- 現在使用的模型結構是 `U-Net`，這是在 `CNN` 捲積神經網路的變形，有別於傳統的 `CNN` 神經網路，這個架構能更有效的使用用於訓練的資料庫，並且在輸出影像格式的預測結果上表現特別突出。
- 這個架構的概念是使用 `CNN` 架構中的基本模塊卷積核 (`kernal`) 逐步對圖片中每單位影格掃描並抓取特徵，在這個基礎上 `U-Net` 透過結構的變化更有效的使用這些截取的特徵。
- 架構構成有 `Encoder`, `Skip-connection`, `bottleneck`, `Decoder`
    - `Encoder` 跟 `Decoder` 可以視為小型的 `CNN` 網路，他們有相同的層數，但區別在於
    - `Encoder` 中隨著層數下降圖形的大小也會下降，並且每層的特徵都會透過 `skip-connection` 直接傳遞到對應層數的 `Decoder`
    - `Decoder` 則是隨著層數上升圖形大小會還原，並且會跟對應層數的 `Encoder` 特徵一起做 `Convolution`

![U-Net architecture](https://hackmd.io/_uploads/Hk5xHcq4xe.png)

流程會是輸入資料經過 `Encoder` 將特徵往下層傳遞的同時也會傳遞到對應的 `Decoder`，經過多層的 `Encoder` 後，特徵資料可以在底部經過其他的模塊訓練後進入 `Decoder`，結合底部傳上來的特徵及對應 `Encoder` 層的特徵一起訓練，最終輸出結果

- 基本上一張 `256*256` 的圖片輸入後，在這個模型架構下他的大小會是 `(Batch size, channels, 256, 256)`，這裡的 `channel=1` 也就是共有幾張圖片
- 在 `Encoder` 的部分會經過幾次的 `conv2d` 及其他模塊去取出參數後，原先輸入的大小會變動，基本上主要是在 `conv2d` 的部分，調整 `channel` 及圖片的大小，但這裡調整了 `kernal_size`,  `pad` 使得大小固定，後續在 `MaxPool2d` 的部分才調整大小
- `bottle neck` 的部分我分成兩個部分，==額外參數引入的調整==已及==加上前面 `Encoder` 抓取的特徵==，由於額外引入的參數為兩個數值，因此我這裡是使用一維的方法 `Linear` 來訓練
- `Decoder` 的部分會是還原資料的大小到對應 `Encoder` 輸出的資料大小並做另外的 `conv2d` 訓練
- 在 `def forward` 的部分就是實際使用前面這些模塊的順序，其中的 `torch.cat()` 為 `Skip-connection` 的部分，會連接 `Encoder` 輸出與 `Decoder` 逐層輸出的結果

```python=
import torch
import torch.nn as nn
import torch.nn.functional as F

class UNet(nn.Module):
    def __init__(self, in_channels, out_channels, num_additional_inputs, dropout_rate):
        super(UNet, self).__init__()
        self.in_channels = in_channels
        self.out_channels = out_channels
        self.num_additional_intputs = num_additional_inputs
        self.dropout_rate = dropout_rate
        self.pool_ratio = 4
        self.bottom_pool_ratio = 2

        self.enc1 = self.encoder_blcok(in_channels, 4)
        self.enc2 = self.encoder_blcok(4, 16)
        self.enc3 = self.encoder_blcok(16, 64)
        self.enc4 = self.encoder_blcok(64, 256)

        self.botin1 = self.bottom_input(num_additional_inputs, 8)
        self.botin2 = self.bottom_input(8, 32)
        self.botin3 = self.bottom_input(32, 128)
        self.botin4 = self.bottom_input(128, 256)

        self.bot1 = self.bottom_block(512, 512)

        self.decbot = self.decoder_block(512, 256, pool_ratio=self.bottom_pool_ratio)
        self.dec4 = self.decoder_block(512, 64, pool_ratio=self.pool_ratio)
        self.dec3 = self.decoder_block(128, 16, pool_ratio=self.pool_ratio)
        self.dec2 = self.decoder_block(32, 4, pool_ratio=self.pool_ratio)
        self.dec1= self.decoder_block(8, out_channels, pool_ratio=self.pool_ratio)

    def encoder_blcok(self, in_channels, out_channels):
        return nn.Sequential(
            nn.Conv2d(in_channels, out_channels, kernel_size=1),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),

            # nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
            # nn.BatchNorm2d(out_channels),
            # nn.ReLU(inplace=True),

            # nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
            # nn.BatchNorm2d(out_channels),
            # nn.ReLU(inplace=True),

            nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
            
            nn.MaxPool2d(kernel_size=self.pool_ratio, stride=self.pool_ratio),
            nn.Dropout2d(self.dropout_rate)
        )
    
    def bottom_input(self, in_channels, out_channels):
        return nn.Sequential(
            nn.Linear(in_channels, out_channels),
            nn.BatchNorm1d(out_channels),
            nn.ReLU(inplace=True),
            nn.Dropout1d(self.dropout_rate)
        )
    
    def bottom_block(self, in_channels, out_channels):
        return nn.Sequential(
                nn.Conv2d(in_channels, out_channels, kernel_size=1),
                nn.BatchNorm2d(out_channels),
                nn.ReLU(inplace=True),

                nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
                nn.BatchNorm2d(out_channels),
                nn.ReLU(inplace=True),

                nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
                nn.BatchNorm2d(out_channels),
                nn.ReLU(inplace=True),

                nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
                # nn.BatchNorm2d(out_channels),
                # nn.ReLU(inplace=True),

                nn.Dropout2d(self.dropout_rate)
        )
    
    def decoder_block(self, in_channels, out_channels, pool_ratio):
        return nn.Sequential(
            nn.ConvTranspose2d(in_channels, out_channels, kernel_size=pool_ratio, stride=pool_ratio),
            nn.Conv2d(out_channels, out_channels, kernel_size=1),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),

            nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),

            nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),

            nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
            # nn.BatchNorm2d(out_channels),
            # nn.ReLU(inplace=True),

            nn.Dropout2d(self.dropout_rate)
        )
    
    def forward(self, x, mach, aoa):
        # Encoder path
        enc1 = self.enc1(x)
        enc2 = self.enc2(enc1)
        enc3 = self.enc3(enc2)
        enc4 = self.enc4(enc3)

        # Bottleneck for additional inputs expanding
        additional_inputs = torch.cat((mach, aoa), dim=1).float()
        # print(f"Additional input dtype : {additional_inputs.dtype}, size : {additional_inputs.size()}")
        bottom_intputs = self.botin1(additional_inputs)
        bottom_intputs = self.botin2(bottom_intputs)
        bottom_intputs = self.botin3(bottom_intputs)
        bottom_intputs = self.botin4(bottom_intputs)
        bottom_intputs = bottom_intputs.unsqueeze(2).unsqueeze(3).expand(-1, -1, enc4.shape[2] // self.bottom_pool_ratio, enc4.shape[3] // self.bottom_pool_ratio)
        # print(f"Bottom input size : {bottom_intputs.size()}")

        # Bottleneck for additional inputs combines Encoder outputs
        bottom = torch.cat((F.max_pool2d(enc4, self.bottom_pool_ratio), bottom_intputs), dim=1)
        assert bottom.dim() == 4, f"Unexpected bottom shape: {bottom.shape}"
        bottom = self.bot1(bottom)

        x = self.decbot(bottom)
        x = self.dec4(torch.cat((enc4, x), dim=1))
        x = self.dec3(torch.cat((enc3, x), dim=1))
        x = self.dec2(torch.cat((enc2, x), dim=1))
        y = self.dec1(torch.cat((enc1, x), dim=1))
        # print(f"Final output size : {y.size()}")

        return y
```

### 模型訓練/測試
- 訓練的部分會需要==初始化模型==、==導入資料庫==、==決定訓練使用的參數==、==訓練的代碼==
- 測試的部分則會包含==模型導入==、==測試資料集導入==、==模型初始化==、==可視化結果==

:::success
完整的代碼同樣放在[我的 github](https://github.com/PGGO26/OpenFOAM-DataBase/tree/main/SACCON/model_training) 上
:::


## 常用指令
- `lscpu` : 可以查看電腦 cpu 核心數目，方便在後續平行運算上判斷能夠使用多少核心
- `htop`：可以查看當前電腦 cpu 使用情形，進入該畫面後 `F10` 可以退出
- `df-h` : 查看電腦檔案容量
- `screen –S 名稱` : 創建指定終端
- `screen –ls` : 查看所有終端
- `screen –r 名稱` : 加入指定終端
- `Ctrl+A+D` : 退出目前所在終端
- `Crtl+Z` : 中止腳本，若確定不需要此腳本需要刪除以釋放GPU空間
- `Nvidia-smi` : 查看GPU使用情況，包括腳本的PID
- `Kill -9 指定檔案的PID` : 刪除指定腳本以減少GPU使用率


## Hisa 求解器使用說明
- https://hisa.gitlab.io/downloads/userManual.pdf
