Skip to content

Transolver

Note

Please install related dependencies before running this case: pip install -r requirements.txt

# linux
wget -c https://paddle-org.bj.bcebos.com/paddlecfd/datasets/pptransformer/mlcfd_data.zip
# windows
# curl https://paddle-org.bj.bcebos.com/paddlecfd/datasets/pptransformer/mlcfd_data.zip -o mlcfd_data.zip
unzip mlcfd_data.zip
python main.py

Note

When running for the first time, mlcfd_data will be preprocessed, which takes about one hour. Please wait patiently.

# linux
wget -c https://paddle-org.bj.bcebos.com/paddlecfd/datasets/pptransformer/mlcfd_data.zip
# windows
# curl https://paddle-org.bj.bcebos.com/paddlecfd/datasets/pptransformer/mlcfd_data.zip -o mlcfd_data.zip
unzip mlcfd_data.zip
python main.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/transolver/transolver_pretrained.pdparams

Note

When running for the first time, mlcfd_data will be preprocessed, which takes about one hour. Please wait patiently.

python main.py mode=export
# linux
wget -c https://paddle-org.bj.bcebos.com/paddlecfd/datasets/pptransformer/mlcfd_data.zip
# windows
# curl https://paddle-org.bj.bcebos.com/paddlecfd/datasets/pptransformer/mlcfd_data.zip -o mlcfd_data.zip
unzip mlcfd_data.zip
python main.py mode=infer

Note

When running for the first time, mlcfd_data will be preprocessed, which takes about one hour. Please wait patiently.

Pretrained Model Metrics
transolver_pretrained.pdparams rho_d:, 0.99314
c_d: 0.01136
relative l2 error of press: 0.07829
relative l2 error of velocity: 0.02304
press: 4.95888
velocity: [0.12163974 0.14851639 0.41583335] 0.26443

1. Background Introduction

Transolver is a neural operator model based on the Transformer architecture for learning solution operators of Partial Differential Equations (PDEs). The core innovation of this model lies in its Physics Attention mechanism, which can efficiently handle physical field prediction problems on irregular meshes.

Compared with traditional Transformer models, Transolver has the following characteristics:

  • Physics-aware Attention Mechanism: Aggregates irregular grid points into regular representations through slice technology, which not only retains spatial physical information, but also greatly reduces computational complexity
  • Flexible Geometric Adaptability: Able to handle geometric bodies of arbitrary shapes and unstructured meshes
  • Efficient Computational Performance: Through the slice attention mechanism, the computational complexity is reduced from \(O(N^2)\) to \(O(NG)\), where \(N\) is the number of grid points and \(G\) is the number of slices

This case uses the Transolver model to learn the velocity field and pressure field distribution of the external flow field of a car on the ShapeNet Car dataset. This is a typical Computational Fluid Dynamics (CFD) surrogate modeling problem. By learning a large amount of car shape and corresponding flow field data, the model can quickly predict the flow field distribution of new car shapes, thereby greatly reducing the computational cost of CFD simulation.

2. Problem Definition

The goal of this case is to establish a mapping relationship between car geometry and its surrounding flow field (velocity field and pressure field). Specifically:

  • Input: Grid point coordinates of the car surface and surrounding space \(\mathbf{x} \in \mathbb{R}^{N \times 7}\), where \(N\) is the number of grid points, and 7 dimensions include geometric information such as spatial coordinates and normal vectors
  • Output: Velocity vector \(\mathbf{v} \in \mathbb{R}^{N \times 3}\) and pressure scalar \(p \in \mathbb{R}^{N \times 1}\) at each grid point

The training goal is to minimize the mean square error between the predicted flow field and the real CFD simulation results, while ensuring that the model can accurately predict key aerodynamic parameters such as the drag coefficient of the car.

3. Problem Solving

Next, we will explain how to convert the problem into PaddleScience code step by step and solve the problem using deep learning methods. In order to quickly understand PaddleScience, only key steps such as model construction, constraint construction, and optimizer construction are described below, while other details please refer to API Documentation.

3.1 Model Construction

In this problem, we need to establish a mapping function \(f: \mathbb{R}^{N \times 7} \to \mathbb{R}^{N \times 4}\) from grid point coordinates \(\mathbf{x}\) to flow field variables \((\mathbf{v}, p)\), that is:

\[ (\mathbf{v}, p) = f(\mathbf{x}) \]

Here we use the Transolver model to represent this mapping function, expressed in PaddleScience code as follows:

def train(cfg: DictConfig):
    # set model
    model = ppsci.arch.Transolver(**cfg.MODEL)

In order to accurately and quickly access the value of specific variables during calculation, we specify here that the input variable name of the network model is ["x"], and the output variable names are ["velo_vec", "press"]. These names are consistent with subsequent code.

The detailed configuration of the model is as follows:

MODEL:
  input_keys: [x]
  output_keys: [velo_vec, press] # vel: 3, press: 1
  space_dim: 7
  n_layers: 8
  n_hidden: 256
  dropout: 0
  n_head: 8
  act: gelu
  mlp_ratio: 2
  fun_dim: 0
  out_dim: [3, 1]
  slice_num: 32
  ref: 8
  unified_pos: false

Where:

  • space_dim: Input space dimension, set to 7 (including spatial coordinates, normal vectors and other geometric information)
  • n_layers: Number of Transformer layers, set to 8
  • n_hidden: Hidden layer dimension, set to 256
  • n_head: Number of multi-head attention heads, set to 8
  • dropout: Dropout rate, set to 0
  • act: Activation function, use gelu
  • mlp_ratio: Hidden layer expansion ratio of MLP layer, set to 2
  • out_dim: Output dimension list, [3, 1] corresponds to velocity field (3 dimensions) and pressure field (1 dimension) respectively
  • slice_num: Number of slices in slice attention mechanism, set to 32, used to reduce computational complexity
  • ref: Resolution of reference grid, set to 8
  • unified_pos: Whether to use unified position encoding, set to False

3.2 Model Architecture Details

The core architecture of Transolver includes the following key components:

3.2.1 Preprocessing

Map input geometric features to hidden space:

\[ h = \text{MLP}(\text{concat}(f, x)) \]

Where \(f\) is function feature (such as initial field), and \(x\) is spatial coordinate.

3.2.2 Physics Attention

This is the core innovation of Transolver, which achieves efficient global information interaction through the following steps:

  1. Slice Aggregation: Aggregate \(N\) irregular grid points into \(G\) slice representations
\[ S = \text{softmax}\left(\frac{W_s(h)}{\tau}\right)^T h \]

Where \(S \in \mathbb{R}^{G \times D}\) is slice representation, and \(\tau\) is learnable temperature parameter.

  1. Self-Attention on Slices: Perform standard Transformer attention calculation on slice representations
\[ \text{Attn}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d}}\right)V \]

Where \(Q = W_q S, K = W_k S, V = W_v S\).

  1. Disaggregation: Map slice representations back to original grid points
\[ h' = S \cdot \text{Attn}(Q, K, V) \cdot W \]

Through this mechanism, the computational complexity is reduced from \(O(N^2)\) to \(O(NG + G^2)\), which can significantly improve efficiency when \(G \ll N\).

3.2.3 Feed-Forward Network (MLP)

Each Transformer block is followed by a feed-forward network:

\[ \text{FFN}(h) = W_2 \cdot \text{GELU}(W_1 h + b_1) + b_2 \]

3.2.4 Layer Normalization and Residual Connection

Each sub-layer uses Layer Normalization (LayerNorm) and residual connection:

\[ \begin{aligned} h^{(l+1)} &= \text{Attn}(\text{LN}(h^{(l)})) + h^{(l)} \\ h^{(l+1)} &= \text{FFN}(\text{LN}(h^{(l+1)})) + h^{(l+1)} \end{aligned} \]

3.3 Data Loading

This case uses the ShapeNet Car dataset, which contains CFD simulation results of different car shapes. The data loading code is as follows:

# set constraint
train_data, val_data, coef_norm = load_train_val_fold(
    cfg.DATA.data_dir,
    cfg.DATA.val_fold_id,
    cfg.DATA.save_dir,
    preprocessed=cfg.DATA.preprocessed,
)
train_dataset = ShapeNetCarDataset(
    cfg.DATA.input_keys,
    cfg.DATA.label_keys,
    train_data,
    use_cfd_mesh=cfg.DATA.use_cfd_mesh,
    r=cfg.DATA.r,
    training=True,
)

Dataset configuration:

DATA:
  input_keys: [x]
  label_keys: [velo_vec, press]
  data_dir: ./mlcfd_data/training_data
  save_dir: ./mlcfd_data/preprocessed_data
  val_fold_id: 0
  preprocessed: false
  use_cfd_mesh: false
  r: 0.2

Where:

  • data_dir: Raw data directory
  • save_dir: Preprocessed data saving directory
  • val_fold_id: Fold ID used for cross-validation
  • preprocessed: Whether to use preprocessed data
  • r: Data downsampling ratio

3.4 Constraint Construction

This case uses supervised learning constraints to train the model by minimizing the error between the predicted flow field and the real flow field:

def loss_func(output, label, _):
    velo_label = label[cfg.DATA.label_keys[0]]
    press_label = label[cfg.DATA.label_keys[1]]
    surf_mask = label["surf"]
    mse_velo = F.mse_loss(output[cfg.DATA.label_keys[0]], velo_label)
    mse_press = F.mse_loss(
        output[cfg.MODEL.output_keys[1]][surf_mask], press_label[surf_mask]
    )

    return {
        "mse_velo_press": mse_velo + cfg.TRAIN.press_weight * mse_press,
    }

sup_constraint = ppsci.constraint.SupervisedConstraint(
    {
        "dataset": train_dataset,
        "batch_size": cfg.TRAIN.batch_size,
        "num_workers": 0,
        "sampler": {
            "name": "BatchSampler",
            "drop_last": True,
            "shuffle": True,
        },
    },
    loss=ppsci.loss.FunctionalLoss(loss_func),
    name="Sup",
)

# wrap constraints together
constraint = {
    sup_constraint.name: sup_constraint,
}

The loss function contains two parts:

  1. Mean square error of velocity field: \(\mathcal{L}_{velo} = \text{MSE}(\mathbf{v}_{pred}, \mathbf{v}_{true})\)
  2. Mean square error of surface pressure field: \(\mathcal{L}_{press} = \text{MSE}(p_{pred}|_{surf}, p_{true}|_{surf})\)

Total loss is: \(\mathcal{L} = \mathcal{L}_{velo} + w_{press} \cdot \mathcal{L}_{press}\), where \(w_{press}\) is pressure loss weight.

3.5 Optimizer Construction

Use Adam optimizer with exponential decay learning rate strategy:

# set optimizer
## Slightly differnt from transolver's lr setting(OneCycleLR)
lr = ppsci.optimizer.lr_scheduler.ExponentialDecay(
    cfg.TRAIN.epochs,
    len(sup_constraint.data_loader),
    cfg.TRAIN.lr.max_lr,
    gamma=cfg.TRAIN.lr.gamma,
    decay_steps=cfg.TRAIN.lr.decay_steps,
    warmup_epoch=cfg.TRAIN.lr.warmup_epoch,
    warmup_start_lr=cfg.TRAIN.lr.max_lr / 25.0,
)()
optimizer = ppsci.optimizer.Adam(lr)(model)

Learning rate configuration:

lr:
  warmup_epoch: 60
  max_lr: 1e-3
  gamma: 0.999973
  decay_steps: 1

It includes:

  • Warmup phase (warmup_epoch): The learning rate gradually increases from \(\frac{lr_{max}}{25}\) to \(lr_{max}\) in the first 60 epochs
  • Decay phase: Then the learning rate decays according to \(lr = lr_{max} \times \gamma^{step}\) for each step

3.6 Validator Construction

Use validation set to evaluate model performance during training:

# set validator
val_dataset = ShapeNetCarDataset(
    cfg.DATA.input_keys,
    cfg.DATA.label_keys,
    val_data,
    use_cfd_mesh=cfg.DATA.use_cfd_mesh,
    r=cfg.DATA.r,
    training=False,
)

def val_metric_func(output, label):
    velo_label = label[cfg.DATA.label_keys[0]]
    press_label = label[cfg.DATA.label_keys[1]]
    surf_mask = label["surf"]
    loss_velo_vec = F.mse_loss(
        output[cfg.DATA.label_keys[0]], velo_label, "none"
    ).mean(axis=0)
    loss_velo = loss_velo_vec.mean()
    loss_press = F.mse_loss(
        output[cfg.DATA.label_keys[1]][surf_mask], press_label[surf_mask]
    )

    return {
        "press": loss_press,
        "velo_vec": loss_velo,
    }

validator = ppsci.validate.SupervisedValidator(
    {
        "dataset": val_dataset,
        "batch_size": cfg.EVAL.batch_size,
        "num_workers": 0,
    },
    metric={
        "mse": ppsci.metric.FunctionalMetric(val_metric_func),
    },
    name="validator",
)
validator = {validator.name: validator}

Evaluation metrics include:

  • Mean square error of velocity field
  • Mean square error of surface pressure field

3.7 Model Training and Evaluation

After completing the above settings, pass the instantiated objects to ppsci.solver.Solver, and then start training and evaluation:

solver.eval()
# train model
solver.train()

3.8 Model Evaluation

In the evaluation phase, in addition to calculating conventional error metrics, the prediction error of drag coefficient and Spearman correlation coefficient will also be calculated:

def eval_on_dataloadr(model_forward, dataloader, coef_norm, val_list):
    with paddle.no_grad():
        l2errs_press = []
        l2errs_velo = []
        mses_press = []
        mses_velo_var = []
        gt_coef_list = []
        pred_coef_list = []
        coef_error = 0
        index = 0
        pbar = tqdm(dataloader, desc="Testing", unit="batch")
        for i, (inp, label, _) in enumerate(pbar, start=1):
            out = model_forward(inp)
            velo_vec = out["velo_vec"]
            press = out["press"]
            targets_velo_vec = label["velo_vec"]
            targets_press = label["press"]
            surf_mask = label["surf"]

            if coef_norm is not None:
                mean = paddle.tensor(coef_norm[2], dtype=dtype)
                std = paddle.tensor(coef_norm[3], dtype=dtype)

                pred_press: paddle.Tensor = press[surf_mask] * std[-1] + mean[-1]
                gt_press: paddle.Tensor = targets_press[surf_mask] * std[-1] + mean[-1]

                pred_surf_velo: paddle.Tensor = (
                    velo_vec[surf_mask] * std[:-1] + mean[:-1]
                )
                gt_surf_velo: paddle.Tensor = (
                    targets_velo_vec[surf_mask] * std[:-1] + mean[:-1]
                )

                pred_velo: paddle.Tensor = velo_vec[~surf_mask] * std[:-1] + mean[:-1]
                gt_velo: paddle.Tensor = (
                    targets_velo_vec[~surf_mask] * std[:-1] + mean[:-1]
                )

                # out_denorm: paddle.Tensor = out * std + mean
                # y_denorm: paddle.Tensor = targets * std + mean
                # np.save('./results/' + args.cfd_model + '/' + str(index) + '_pred.npy', out_denorm.numpy())
                # np.save('./results/' + args.cfd_model + '/' + str(index) + '_gt.npy', y_denorm.numpy())

            pred_coef = cal_coefficient(
                val_list[index].split("/")[1],
                pred_press[:, None].numpy(),
                pred_surf_velo.numpy(),
            )
            gt_coef = cal_coefficient(
                val_list[index].split("/")[1],
                gt_press[:, None].numpy(),
                gt_surf_velo.numpy(),
            )

            gt_coef_list.append(gt_coef)
            pred_coef_list.append(pred_coef)
            coef_error += abs(pred_coef - gt_coef) / gt_coef
            pbar.set_postfix(
                {
                    "batch": f"{i}/{len(dataloader)}, coef_error: {coef_error / (index + 1):.10f}",
                }
            )

            l2err_press = paddle.norm(pred_press - gt_press) / paddle.norm(gt_press)
            l2err_velo = paddle.norm(pred_velo - gt_velo) / paddle.norm(gt_velo)

            mse_press = F.mse_loss(
                press[surf_mask], targets_press[surf_mask], "none"
            ).mean(axis=0)
            mse_velo_var = F.mse_loss(
                velo_vec[~surf_mask], targets_velo_vec[~surf_mask], "none"
            ).mean(axis=0)

            l2errs_press.append(l2err_press.numpy())
            l2errs_velo.append(l2err_velo.numpy())
            mses_press.append(mse_press.numpy())
            mses_velo_var.append(mse_velo_var.numpy())
            index += 1

        gt_coef_list = np.array(gt_coef_list)
        pred_coef_list = np.array(pred_coef_list)
        spear = sc.stats.spearmanr(gt_coef_list, pred_coef_list)[0]
        logger.info(f"rho_d:, {spear:.5f}")
        logger.info(f"c_d: {coef_error / index:.5f}")
        l2err_press = np.mean(l2errs_press)
        l2err_velo = np.mean(l2errs_velo)
        rmse_press = np.sqrt(np.mean(mses_press))
        rmse_velo_var = np.sqrt(np.mean(mses_velo_var, axis=0))
        if coef_norm is not None:
            rmse_press *= coef_norm[3][-1]
            rmse_velo_var *= coef_norm[3][:-1]
        logger.info(f"relative l2 error of press: {l2err_press:.5f}")
        logger.info(f"relative l2 error of velocity: {l2err_velo:.5f}")
        logger.info(f"press: {rmse_press:.5f}")
        logger.info(
            f"velocity: {rmse_velo_var} {np.sqrt(np.mean(np.square(rmse_velo_var))):.5f}"
        )

Evaluation metrics include:

  • Relative L2 Error: Measure the overall deviation of the predicted field from the true field
  • Root Mean Square Error (RMSE): Evaluate point-wise prediction accuracy
  • Drag Coefficient Error: Evaluate the prediction accuracy of key aerodynamic parameters
  • Spearman Correlation Coefficient (\(\rho_d\)): Evaluate the correlation of drag coefficient ranking

4. Complete Code

main.py
"""
Reference: https://github.com/thuml/Transolver
"""
from __future__ import annotations

import hydra
import numpy as np
import paddle
import paddle.nn.functional as F
import scipy as sc
from drag_coefficient import cal_coefficient
from omegaconf import DictConfig
from shapenet_car import ShapeNetCarDataset
from shapenet_car import load_train_val_fold
from shapenet_car import load_train_val_fold_file
from tqdm import tqdm

import ppsci
from ppsci.utils import logger
from ppsci.utils import save_load

dtype = paddle.get_default_dtype()


def train(cfg: DictConfig):
    # set model
    model = ppsci.arch.Transolver(**cfg.MODEL)

    # set constraint
    train_data, val_data, coef_norm = load_train_val_fold(
        cfg.DATA.data_dir,
        cfg.DATA.val_fold_id,
        cfg.DATA.save_dir,
        preprocessed=cfg.DATA.preprocessed,
    )
    train_dataset = ShapeNetCarDataset(
        cfg.DATA.input_keys,
        cfg.DATA.label_keys,
        train_data,
        use_cfd_mesh=cfg.DATA.use_cfd_mesh,
        r=cfg.DATA.r,
        training=True,
    )

    def loss_func(output, label, _):
        velo_label = label[cfg.DATA.label_keys[0]]
        press_label = label[cfg.DATA.label_keys[1]]
        surf_mask = label["surf"]
        mse_velo = F.mse_loss(output[cfg.DATA.label_keys[0]], velo_label)
        mse_press = F.mse_loss(
            output[cfg.MODEL.output_keys[1]][surf_mask], press_label[surf_mask]
        )

        return {
            "mse_velo_press": mse_velo + cfg.TRAIN.press_weight * mse_press,
        }

    sup_constraint = ppsci.constraint.SupervisedConstraint(
        {
            "dataset": train_dataset,
            "batch_size": cfg.TRAIN.batch_size,
            "num_workers": 0,
            "sampler": {
                "name": "BatchSampler",
                "drop_last": True,
                "shuffle": True,
            },
        },
        loss=ppsci.loss.FunctionalLoss(loss_func),
        name="Sup",
    )

    # wrap constraints together
    constraint = {
        sup_constraint.name: sup_constraint,
    }

    # set optimizer
    ## Slightly differnt from transolver's lr setting(OneCycleLR)
    lr = ppsci.optimizer.lr_scheduler.ExponentialDecay(
        cfg.TRAIN.epochs,
        len(sup_constraint.data_loader),
        cfg.TRAIN.lr.max_lr,
        gamma=cfg.TRAIN.lr.gamma,
        decay_steps=cfg.TRAIN.lr.decay_steps,
        warmup_epoch=cfg.TRAIN.lr.warmup_epoch,
        warmup_start_lr=cfg.TRAIN.lr.max_lr / 25.0,
    )()
    optimizer = ppsci.optimizer.Adam(lr)(model)

    # set validator
    val_dataset = ShapeNetCarDataset(
        cfg.DATA.input_keys,
        cfg.DATA.label_keys,
        val_data,
        use_cfd_mesh=cfg.DATA.use_cfd_mesh,
        r=cfg.DATA.r,
        training=False,
    )

    def val_metric_func(output, label):
        velo_label = label[cfg.DATA.label_keys[0]]
        press_label = label[cfg.DATA.label_keys[1]]
        surf_mask = label["surf"]
        loss_velo_vec = F.mse_loss(
            output[cfg.DATA.label_keys[0]], velo_label, "none"
        ).mean(axis=0)
        loss_velo = loss_velo_vec.mean()
        loss_press = F.mse_loss(
            output[cfg.DATA.label_keys[1]][surf_mask], press_label[surf_mask]
        )

        return {
            "press": loss_press,
            "velo_vec": loss_velo,
        }

    validator = ppsci.validate.SupervisedValidator(
        {
            "dataset": val_dataset,
            "batch_size": cfg.EVAL.batch_size,
            "num_workers": 0,
        },
        metric={
            "mse": ppsci.metric.FunctionalMetric(val_metric_func),
        },
        name="validator",
    )
    validator = {validator.name: validator}

    # initialize solver
    solver = ppsci.solver.Solver(
        model,
        constraint,
        optimizer=optimizer,
        validator=validator,
        cfg=cfg,
    )

    solver.eval()
    # train model
    solver.train()


def eval_on_dataloadr(model_forward, dataloader, coef_norm, val_list):
    with paddle.no_grad():
        l2errs_press = []
        l2errs_velo = []
        mses_press = []
        mses_velo_var = []
        gt_coef_list = []
        pred_coef_list = []
        coef_error = 0
        index = 0
        pbar = tqdm(dataloader, desc="Testing", unit="batch")
        for i, (inp, label, _) in enumerate(pbar, start=1):
            out = model_forward(inp)
            velo_vec = out["velo_vec"]
            press = out["press"]
            targets_velo_vec = label["velo_vec"]
            targets_press = label["press"]
            surf_mask = label["surf"]

            if coef_norm is not None:
                mean = paddle.tensor(coef_norm[2], dtype=dtype)
                std = paddle.tensor(coef_norm[3], dtype=dtype)

                pred_press: paddle.Tensor = press[surf_mask] * std[-1] + mean[-1]
                gt_press: paddle.Tensor = targets_press[surf_mask] * std[-1] + mean[-1]

                pred_surf_velo: paddle.Tensor = (
                    velo_vec[surf_mask] * std[:-1] + mean[:-1]
                )
                gt_surf_velo: paddle.Tensor = (
                    targets_velo_vec[surf_mask] * std[:-1] + mean[:-1]
                )

                pred_velo: paddle.Tensor = velo_vec[~surf_mask] * std[:-1] + mean[:-1]
                gt_velo: paddle.Tensor = (
                    targets_velo_vec[~surf_mask] * std[:-1] + mean[:-1]
                )

                # out_denorm: paddle.Tensor = out * std + mean
                # y_denorm: paddle.Tensor = targets * std + mean
                # np.save('./results/' + args.cfd_model + '/' + str(index) + '_pred.npy', out_denorm.numpy())
                # np.save('./results/' + args.cfd_model + '/' + str(index) + '_gt.npy', y_denorm.numpy())

            pred_coef = cal_coefficient(
                val_list[index].split("/")[1],
                pred_press[:, None].numpy(),
                pred_surf_velo.numpy(),
            )
            gt_coef = cal_coefficient(
                val_list[index].split("/")[1],
                gt_press[:, None].numpy(),
                gt_surf_velo.numpy(),
            )

            gt_coef_list.append(gt_coef)
            pred_coef_list.append(pred_coef)
            coef_error += abs(pred_coef - gt_coef) / gt_coef
            pbar.set_postfix(
                {
                    "batch": f"{i}/{len(dataloader)}, coef_error: {coef_error / (index + 1):.10f}",
                }
            )

            l2err_press = paddle.norm(pred_press - gt_press) / paddle.norm(gt_press)
            l2err_velo = paddle.norm(pred_velo - gt_velo) / paddle.norm(gt_velo)

            mse_press = F.mse_loss(
                press[surf_mask], targets_press[surf_mask], "none"
            ).mean(axis=0)
            mse_velo_var = F.mse_loss(
                velo_vec[~surf_mask], targets_velo_vec[~surf_mask], "none"
            ).mean(axis=0)

            l2errs_press.append(l2err_press.numpy())
            l2errs_velo.append(l2err_velo.numpy())
            mses_press.append(mse_press.numpy())
            mses_velo_var.append(mse_velo_var.numpy())
            index += 1

        gt_coef_list = np.array(gt_coef_list)
        pred_coef_list = np.array(pred_coef_list)
        spear = sc.stats.spearmanr(gt_coef_list, pred_coef_list)[0]
        logger.info(f"rho_d:, {spear:.5f}")
        logger.info(f"c_d: {coef_error / index:.5f}")
        l2err_press = np.mean(l2errs_press)
        l2err_velo = np.mean(l2errs_velo)
        rmse_press = np.sqrt(np.mean(mses_press))
        rmse_velo_var = np.sqrt(np.mean(mses_velo_var, axis=0))
        if coef_norm is not None:
            rmse_press *= coef_norm[3][-1]
            rmse_velo_var *= coef_norm[3][:-1]
        logger.info(f"relative l2 error of press: {l2err_press:.5f}")
        logger.info(f"relative l2 error of velocity: {l2err_velo:.5f}")
        logger.info(f"press: {rmse_press:.5f}")
        logger.info(
            f"velocity: {rmse_velo_var} {np.sqrt(np.mean(np.square(rmse_velo_var))):.5f}"
        )


def evaluate(cfg: DictConfig):
    # set model
    model = ppsci.arch.Transolver(**cfg.MODEL)

    # load pretrained model
    save_load.load_pretrain(model, cfg.EVAL.pretrained_model_path)
    model.eval()

    # evaluate manually
    _, val_data, coef_norm, val_list = load_train_val_fold_file(
        cfg.DATA.data_dir,
        cfg.DATA.val_fold_id,
        cfg.DATA.save_dir,
        preprocessed=cfg.DATA.preprocessed,
    )
    test_dataset = ShapeNetCarDataset(
        cfg.DATA.input_keys,
        cfg.DATA.label_keys,
        val_data,
        use_cfd_mesh=cfg.DATA.use_cfd_mesh,
        r=cfg.DATA.r,
        training=False,
    )
    test_dataloader = ppsci.data.build_dataloader(
        test_dataset,
        {
            "batch_size": cfg.EVAL.batch_size,
            "num_workers": 0,
        },
    )

    eval_on_dataloadr(model, test_dataloader, coef_norm, val_list)


def export(cfg: DictConfig):
    # set model
    model = ppsci.arch.Transolver(**cfg.MODEL)

    # initialize solver
    solver = ppsci.solver.Solver(model, cfg=cfg)
    # export model
    from paddle.static import InputSpec

    input_spec = [
        {
            model.input_keys[0]: InputSpec(
                [1, None, 7], "float32", name=model.input_keys[0]
            ),
        },
    ]
    import einops

    solver.export(
        input_spec, cfg.INFER.export_path, with_onnx=False, ignore_modules=[einops]
    )


def inference(cfg: DictConfig):
    from deploy import python_infer

    predictor = python_infer.GeneralPredictor(cfg)

    # inference manually
    _, val_data, coef_norm, val_list = load_train_val_fold_file(
        cfg.DATA.data_dir,
        cfg.DATA.val_fold_id,
        cfg.DATA.save_dir,
        preprocessed=cfg.DATA.preprocessed,
    )
    test_dataset = ShapeNetCarDataset(
        cfg.DATA.input_keys,
        cfg.DATA.label_keys,
        val_data,
        use_cfd_mesh=cfg.DATA.use_cfd_mesh,
        r=cfg.DATA.r,
        training=False,
    )
    test_dataloader = ppsci.data.build_dataloader(
        test_dataset,
        {
            "batch_size": cfg.TRAIN.batch_size,
            "num_workers": 0,
        },
    )

    def wrap_predict(x):
        raw_out = predictor.predict(
            {k: v.numpy() for k, v in x.items()}, batch_size=None
        )
        return {
            store_key: paddle.tensor(raw_out[infer_key])
            for store_key, infer_key in zip(cfg.MODEL.output_keys[::-1], raw_out.keys())
        }

    eval_on_dataloadr(wrap_predict, test_dataloader, coef_norm, val_list)


@hydra.main(version_base=None, config_path="./conf", config_name="shapenet_car.yaml")
def main(cfg: DictConfig):
    if cfg.mode == "train":
        train(cfg)
    elif cfg.mode == "eval":
        evaluate(cfg)
    elif cfg.mode == "export":
        export(cfg)
    elif cfg.mode == "infer":
        inference(cfg)
    else:
        raise ValueError(
            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
        )


if __name__ == "__main__":
    main()

5. Result Display

After model training is completed, the prediction performance can be evaluated on the validation set. The main evaluation metrics include:

  • Relative L2 Error of Velocity Field: Measure the overall accuracy of velocity field prediction
  • Relative L2 Error of Pressure Field: Measure the overall accuracy of pressure field prediction
  • Relative Error of Drag Coefficient: Evaluate the prediction accuracy of key aerodynamic parameters
  • Spearman Correlation Coefficient: Evaluate the accuracy of drag coefficient ranking of different car shapes

Through training, the Transolver model can predict the flow field distribution around the car with high accuracy, significantly reducing calculation time compared to traditional CFD simulation, providing a fast surrogate model for car aerodynamic shape optimization.

6. References