跳转至

VIV(vortex induced vibration)

AI Studio快速体验

python viv.py
python viv.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/viv/viv_pretrained.pdparams
python viv.py mode=export
python viv.py mode=infer
预训练模型 指标
viv_pretrained.pdparams
viv_pretrained.pdeqn
eta_l2/MSE.eta: 0.00875
eta_l2/MSE.f: 0.00921

1. 背景简介

涡激振动(Vortex-Induced Vibration,VIV)是一种流固耦合振动现象,主要发生在流体绕过柱体或管体时。在海洋工程和风工程中,这种振动现象具有重要应用。

在海洋工程中,涡激振动问题主要涉及海洋平台(如桩基、立管等)的涡激振动响应分析。这些平台在海流中运行,会受到涡激振动的影响。这种振动可能会导致平台结构的疲劳损伤,因此在进行海洋平台设计时,需要考虑这一问题。

在风工程中,涡激振动问题主要涉及风力发电机的涡激振动响应分析。风力发电机叶片在运行过程中受到气流的涡激振动,这种振动可能会导致叶片的疲劳损伤。为了确保风力发电机的安全运行,需要对这一问题进行深入的研究。

总之,涡激振动问题的应用主要涉及海洋工程和风工程领域,对于这些领域的发展具有重要意义。

当涡流脱落频率接近结构的固有频率时,圆柱会发生涡激振动,VIV系统相当于一个弹簧-阻尼系统:

VIV_1D_SpringDamper

2. 问题定义

本问题涉及的控制方程涉及三个物理量:\(λ_1\)\(λ_2\)\(ρ\),分别表示自然阻尼、结构特性刚度和质量,控制方程定义如下所示:

\[ \rho \dfrac{\partial^2 \eta}{\partial t^2} + \lambda_1 \dfrac{\partial \eta}{\partial t} + \lambda_2 \eta = f \]

该模型基于无量纲速度 \(U_r=\dfrac{u}{f_n*d}=8.5\) 对应 \(Re=500\) 的假设。我们使用通过圆柱的流体引起的圆柱振动的横向振幅 \(\eta\) 和相应的升力 \(f\) 作为监督数据。

3. 问题求解

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。 为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

3.1 模型构建

在 VIV 问题中,给定时间 \(t\),上述系统都有横向振幅 \(\eta\) 和升力 \(f\) 作为待求解的未知量,并且该系统本身还包含两个参数 \(\lambda_1, \lambda_2\)。因此我们在这里使用比较简单的 MLP(Multilayer Perceptron, 多层感知机) 来表示 \(t\)\((\eta, f)\) 的映射函数 \(g: \mathbb{R}^1 \to \mathbb{R}^2\) ,即:

\[ \eta, f = g(t) \]

上式中 \(g\) 即为 MLP 模型本身,用 PaddleScience 代码表示如下

# set model
model = ppsci.arch.MLP(**cfg.MODEL)

为了在计算时,准确快速地访问具体变量的值,我们在这里指定网络模型的输入变量名是 ("t_f",),输出变量名是 ("eta",)t_f 代表输入时间 \(t\)eta 代表输出振幅 \(\eta\) 这些命名与后续代码保持一致。

接着通过指定 MLP 的层数、神经元个数以及激活函数,我们就实例化出了一个拥有 5 层隐藏神经元,每层神经元数为 50,使用 "tanh" 作为激活函数的神经网络模型 model

3.2 方程构建

由于 VIV 使用的是 VIV 方程,因此可以直接使用 PaddleScience 内置的 VIV

# set equation
equation = {"VIV": ppsci.equation.Vibration(2, -4, 0)}

我们在该方程中添加了两个可学习的参数 k1k2 来估计 \(\lambda_1\)\(\lambda_2\),且它们的关系是 \(\lambda_1 = e^{k1}, \lambda_2 = e^{k2}\)

因此我们在实例化 VIV 类时需指定必要的参数:质量 rho=2,初始化值k1=-4k2=0

3.3 计算域构建

本文中 VIV 问题作用在 \(t \in [0.0625, 9.9375]\) 中的 100 个离散时间点上,这 100 个时间点已经保存在文件 examples/fsi/VIV_Training_Neta100.mat 作为输入数据,因此不需要显式构建计算域。

3.4 约束构建

本文采用监督学习的方式,对模型输出 \(\eta\) 和基于 \(\eta\) 计算出的升力 \(f\),这两个物理量进行约束。

3.4.1 监督约束

由于我们以监督学习方式进行训练,此处采用监督约束 SupervisedConstraint

# set constraint
sup_constraint = ppsci.constraint.SupervisedConstraint(
    {
        "dataset": {
            "name": "MatDataset",
            "file_path": cfg.VIV_DATA_PATH,
            "input_keys": ("t_f",),
            "label_keys": ("eta", "f"),
            "weight_dict": {"eta": 100},
        },
        "batch_size": cfg.TRAIN.batch_size,
        "sampler": {
            "name": "BatchSampler",
            "drop_last": False,
            "shuffle": True,
        },
    },
    ppsci.loss.MSELoss("mean"),
    {"eta": lambda out: out["eta"], **equation["VIV"].equations},
    name="Sup",
)

SupervisedConstraint 的第一个参数是监督约束的读取配置,此处填入在 3.2 方程构建 章节中实例化好的 train_dataloader_cfg

第二个参数是损失函数,此处我们选用常用的MSE函数,且 reduction 设置为 "mean",即我们会将参与计算的所有数据点产生的损失项求和取平均;

第三个参数是方程表达式,用于描述如何计算约束目标,此处填入 eta 的计算函数和在 3.2 方程构建 章节中实例化好的 equation["VIV"].equations

第四个参数是约束条件的名字,我们需要给每一个约束条件命名,方便后续对其索引。此处我们命名为 "Sup" 即可。

在监督约束构建完毕之后,以我们刚才的命名为关键字,封装到一个字典中,方便后续访问。

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

3.5 超参数设定

接下来我们需要指定训练轮数和学习率,此处我们按实验经验,使用 10000 轮训练轮数,并每隔 10000 个epochs评估一次模型精度。

TRAIN:
  epochs: 100000
  iters_per_epoch: 1
  save_freq: 10000
  eval_during_train: true
  eval_freq: 1000
  batch_size: 100
  lr_scheduler:

3.6 优化器构建

训练过程会调用优化器来更新模型参数,此处选择较为常用的 Adam 优化器和 Step 间隔衰减学习率。

# set optimizer
lr_scheduler = ppsci.optimizer.lr_scheduler.Step(**cfg.TRAIN.lr_scheduler)()
optimizer = ppsci.optimizer.Adam(lr_scheduler)((model,) + tuple(equation.values()))
说明

VIV 方程含有两个 可学习参数 k1和k2,因此需要将方程与 model 一起传入优化器。

3.7 评估器构建

在训练过程中通常会按一定轮数间隔,用验证集(测试集)评估当前模型的训练情况,因此使用 ppsci.validate.SupervisedValidator 构建评估器。

# set validator
eta_l2_validator = ppsci.validate.SupervisedValidator(
    {
        "dataset": {
            "name": "MatDataset",
            "file_path": cfg.VIV_DATA_PATH,
            "input_keys": ("t_f",),
            "label_keys": ("eta", "f"),
        },
        "batch_size": cfg.EVAL.batch_size,
    },
    ppsci.loss.MSELoss("mean"),
    {"eta": lambda out: out["eta"], **equation["VIV"].equations},
    metric={"MSE": ppsci.metric.L2Rel()},
    name="eta_l2",
)
validator = {eta_l2_validator.name: eta_l2_validator}

评价指标 metric 选择 ppsci.metric.MSE 即可;

其余配置与 3.4.1 监督约束构建 的设置类似。

3.8 可视化器构建

在模型评估时,如果评估结果是可以可视化的数据,我们可以选择合适的可视化器来对输出结果进行可视化。

本文需要可视化的数据是 \(t-\eta\)\(t-f\) 两组关系图,假设每个时刻 \(t\) 的坐标是 \(t_i\),则对应网络输出为 \(\eta_i\),升力为 \(f_i\),因此我们只需要将评估过程中产生的所有 \((t_i, \eta_i, f_i)\) 保存成图片即可。代码如下:

# set visualizer(optional)
visu_mat = ppsci.utils.reader.load_mat_file(
    cfg.VIV_DATA_PATH,
    ("t_f", "eta_gt", "f_gt"),
    alias_dict={"eta_gt": "eta", "f_gt": "f"},
)
visualizer = {
    "visualize_u": ppsci.visualize.VisualizerScatter1D(
        visu_mat,
        ("t_f",),
        {
            r"$\eta$": lambda d: d["eta"],  # plot with latex title
            r"$\eta_{gt}$": lambda d: d["eta_gt"],  # plot with latex title
            r"$f$": equation["VIV"].equations["f"],  # plot with latex title
            r"$f_{gt}$": lambda d: d["f_gt"],  # plot with latex title
        },
        num_timestamps=1,
        prefix="viv_pred",
    )
}

3.9 模型训练、评估与可视化

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估、可视化。

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

# train model
solver.train()
# evaluate after finished training
solver.eval()
# visualize prediction after finished training
solver.visualize()

4. 完整代码

viv.py
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import hydra
from omegaconf import DictConfig

import ppsci


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

    # set equation
    equation = {"VIV": ppsci.equation.Vibration(2, -4, 0)}

    # set constraint
    sup_constraint = ppsci.constraint.SupervisedConstraint(
        {
            "dataset": {
                "name": "MatDataset",
                "file_path": cfg.VIV_DATA_PATH,
                "input_keys": ("t_f",),
                "label_keys": ("eta", "f"),
                "weight_dict": {"eta": 100},
            },
            "batch_size": cfg.TRAIN.batch_size,
            "sampler": {
                "name": "BatchSampler",
                "drop_last": False,
                "shuffle": True,
            },
        },
        ppsci.loss.MSELoss("mean"),
        {"eta": lambda out: out["eta"], **equation["VIV"].equations},
        name="Sup",
    )
    # wrap constraints together
    constraint = {sup_constraint.name: sup_constraint}

    # set optimizer
    lr_scheduler = ppsci.optimizer.lr_scheduler.Step(**cfg.TRAIN.lr_scheduler)()
    optimizer = ppsci.optimizer.Adam(lr_scheduler)((model,) + tuple(equation.values()))

    # set validator
    eta_l2_validator = ppsci.validate.SupervisedValidator(
        {
            "dataset": {
                "name": "MatDataset",
                "file_path": cfg.VIV_DATA_PATH,
                "input_keys": ("t_f",),
                "label_keys": ("eta", "f"),
            },
            "batch_size": cfg.EVAL.batch_size,
        },
        ppsci.loss.MSELoss("mean"),
        {"eta": lambda out: out["eta"], **equation["VIV"].equations},
        metric={"MSE": ppsci.metric.L2Rel()},
        name="eta_l2",
    )
    validator = {eta_l2_validator.name: eta_l2_validator}

    # set visualizer(optional)
    visu_mat = ppsci.utils.reader.load_mat_file(
        cfg.VIV_DATA_PATH,
        ("t_f", "eta_gt", "f_gt"),
        alias_dict={"eta_gt": "eta", "f_gt": "f"},
    )
    visualizer = {
        "visualize_u": ppsci.visualize.VisualizerScatter1D(
            visu_mat,
            ("t_f",),
            {
                r"$\eta$": lambda d: d["eta"],  # plot with latex title
                r"$\eta_{gt}$": lambda d: d["eta_gt"],  # plot with latex title
                r"$f$": equation["VIV"].equations["f"],  # plot with latex title
                r"$f_{gt}$": lambda d: d["f_gt"],  # plot with latex title
            },
            num_timestamps=1,
            prefix="viv_pred",
        )
    }

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

    # train model
    solver.train()
    # evaluate after finished training
    solver.eval()
    # visualize prediction after finished training
    solver.visualize()


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

    # set equation
    equation = {"VIV": ppsci.equation.Vibration(2, -4, 0)}

    # set validator
    eta_l2_validator = ppsci.validate.SupervisedValidator(
        {
            "dataset": {
                "name": "MatDataset",
                "file_path": cfg.VIV_DATA_PATH,
                "input_keys": ("t_f",),
                "label_keys": ("eta", "f"),
            },
            "batch_size": cfg.EVAL.batch_size,
        },
        ppsci.loss.MSELoss("mean"),
        {"eta": lambda out: out["eta"], **equation["VIV"].equations},
        metric={"MSE": ppsci.metric.L2Rel()},
        name="eta_l2",
    )
    validator = {eta_l2_validator.name: eta_l2_validator}

    # set visualizer(optional)
    visu_mat = ppsci.utils.reader.load_mat_file(
        cfg.VIV_DATA_PATH,
        ("t_f", "eta_gt", "f_gt"),
        alias_dict={"eta_gt": "eta", "f_gt": "f"},
    )

    visualizer = {
        "visualize_u": ppsci.visualize.VisualizerScatter1D(
            visu_mat,
            ("t_f",),
            {
                r"$\eta$": lambda d: d["eta"],  # plot with latex title
                r"$\eta_{gt}$": lambda d: d["eta_gt"],  # plot with latex title
                r"$f$": equation["VIV"].equations["f"],  # plot with latex title
                r"$f_{gt}$": lambda d: d["f_gt"],  # plot with latex title
            },
            num_timestamps=1,
            prefix="viv_pred",
        )
    }

    # initialize solver
    solver = ppsci.solver.Solver(
        model,
        equation=equation,
        validator=validator,
        visualizer=visualizer,
        cfg=cfg,
    )

    # evaluate
    solver.eval()
    # visualize prediction
    solver.visualize()


def export(cfg: DictConfig):
    from paddle import nn
    from paddle.static import InputSpec

    # set model
    model = ppsci.arch.MLP(**cfg.MODEL)
    # initialize equation
    equation = {"VIV": ppsci.equation.Vibration(2, -4, 0)}
    # initialize solver
    solver = ppsci.solver.Solver(
        model,
        equation=equation,
        cfg=cfg,
    )
    # Convert equation to func
    f_func = ppsci.lambdify(
        solver.equation["VIV"].equations["f"],
        solver.model,
        list(solver.equation["VIV"].learnable_parameters),
    )

    class Wrapped_Model(nn.Layer):
        def __init__(self, model, func):
            super().__init__()
            self.model = model
            self.func = func

        def forward(self, x):
            model_out = self.model(x)
            func_out = self.func(x)
            return {**model_out, "f": func_out}

    solver.model = Wrapped_Model(model, f_func)
    # export models
    input_spec = [
        {key: InputSpec([None, 1], "float32", name=key) for key in model.input_keys},
    ]
    solver.export(input_spec, cfg.INFER.export_path, skip_prune_program=True)


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

    # set model predictor
    predictor = pinn_predictor.PINNPredictor(cfg)

    infer_mat = ppsci.utils.reader.load_mat_file(
        cfg.VIV_DATA_PATH,
        ("t_f", "eta_gt", "f_gt"),
        alias_dict={"eta_gt": "eta", "f_gt": "f"},
    )

    input_dict = {key: infer_mat[key] for key in cfg.INFER.input_keys}

    output_dict = predictor.predict(input_dict, cfg.INFER.batch_size)

    # mapping data to cfg.INFER.output_keys
    output_dict = {
        store_key: output_dict[infer_key]
        for store_key, infer_key in zip(cfg.INFER.output_keys, output_dict.keys())
    }
    infer_mat.update(output_dict)

    ppsci.visualize.plot.save_plot_from_1d_dict(
        "./viv_pred", infer_mat, ("t_f",), ("eta", "eta_gt", "f", "f_gt")
    )


@hydra.main(version_base=None, config_path="./conf", config_name="viv.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. 结果展示

模型预测结果如下所示,横轴为时间自变量\(t\)\(\eta_{gt}\)为参考振幅,\(\eta\)为模型预测振幅,\(f_{gt}\)为参考升力,\(f\)为模型预测升力。

Viv_result

振幅 eta 与升力 f 随时间t变化的预测结果和参考结果

可以看到模型对在\([0,10]\)时间范围内,对振幅和升力的预测结果与参考结果基本一致。