Skip to content

Control arm

# linux
wget -c https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl -P ./datasets/
# windows
# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl --create-dirs -o ./datasets/control_arm.stl
python forward_analysis.py
# linux
wget -c https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl -P ./datasets/
# windows
# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl --create-dirs -o ./datasets/control_arm.stl
python inverse_parameter.py TRAIN.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/control_arm/forward_x_axis_pretrained.pdparams
# linux
wget -c https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl -P ./datasets/
# windows
# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl --create-dirs -o ./datasets/control_arm.stl
python forward_analysis.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/control_arm/forward_x_axis_pretrained.pdparams
# linux
wget -c https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl -P ./datasets/
# windows
# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl --create-dirs -o ./datasets/control_arm.stl
python inverse_parameter.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/control_arm/inverse_x_axis_pretrained.pdparams
python forward_analysis.py mode=export
python inverse_parameter.py mode=export
# linux
wget -c https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl -P ./datasets/
# windows
# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl --create-dirs -o ./datasets/control_arm.stl
python forward_analysis.py mode=infer
# linux
wget -c https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl -P ./datasets/
# windows
# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl --create-dirs -o ./datasets/control_arm.stl
python inverse_parameter.py mode=infer
Pretrained Model Metrics
inverse_x_axis_pretrained.pdparams loss(geo_eval): 0.02505
L2Rel.lambda_(geo_eval): 0.06025
L2Rel.mu(geo_eval): 0.07949

1. Background Introduction

Structural stress analysis is the analysis of the stress and strain generated by a structure under specific load conditions that meet certain boundary conditions. Stress is a physical quantity used to describe the force per unit area generated inside an object due to external forces. Strain describes the change in the shape and size of an object. Usually, structural mechanics problems are divided into static problems and dynamic problems. This case focuses on static analysis, that is, analysis after the structure reaches a force equilibrium state. This problem assumes that the structure is subjected to a relatively small force, at which time the structural deformation conforms to the linear elasticity equation.

It should be pointed out that structures suitable for linear elasticity equations need to satisfy the condition that they can completely return to their original state after being stressed, that is, there is no permanent deformation. This assumption is reasonable in many cases, but at the same time, for some materials that may produce permanent deformation (such as plastic or rubber), this assumption may be inaccurate. To fully understand deformation, other factors need to be considered, such as the initial shape and size of the object, the history of external forces, other physical properties of the material (such as thermal expansion coefficient and density), etc.

Automobile control arm, also known as suspension arm or suspension control arm, is an important part connecting the wheel and the vehicle chassis. As a guiding and force transmitting component of the automobile suspension system, the control arm transmits various forces acting on the wheel to the vehicle body, while ensuring that the wheel moves according to a certain trajectory. The control arm elastically connects the wheel and the vehicle body through ball joints or bushings respectively. The control arm (including the bushings and ball joints connected to it) should have sufficient stiffness, strength and service life.

This problem mainly studies the stress analysis of the following automobile suspension control arm structure and verifies the possibility of parameter inversion without given additional data, and uses deep learning methods to solve based on linear elasticity and other equations. The structure is shown below. The inner surface of the single ring on the left is stressed, and the inner surfaces of the two rings on the right are fixed. Two cases of force direction are studied: negative x-axis direction and positive z-axis direction. The following takes the positive x-axis direction force as an example for explanation.

control_arm

Control arm structure diagram

2. Problem Definition

The linear elasticity equation is a mathematical model that describes the ability of an object to return to its original state after being stressed. It is characterized by a linear relationship between stress and strain, where the coefficient is called the elastic modulus (or Young's modulus). Its formula is:

\[ \begin{cases} stress\_disp_{xx} = \lambda(\dfrac{\partial u}{\partial x} + \dfrac{\partial v}{\partial y} + \dfrac{\partial w}{\partial z}) + 2\mu \dfrac{\partial u}{\partial x} - \sigma_{xx} \\ stress\_disp_{yy} = \lambda(\dfrac{\partial u}{\partial x} + \dfrac{\partial v}{\partial y} + \dfrac{\partial w}{\partial z}) + 2\mu \dfrac{\partial v}{\partial y} - \sigma_{yy} \\ stress\_disp_{zz} = \lambda(\dfrac{\partial u}{\partial x} + \dfrac{\partial v}{\partial y} + \dfrac{\partial w}{\partial z}) + 2\mu \dfrac{\partial w}{\partial z} - \sigma_{zz} \\ traction_{x} = n_x \sigma_{xx} + n_y \sigma_{xy} + n_z \sigma_{xz} \\ traction_{y} = n_y \sigma_{yx} + n_y \sigma_{yy} + n_z \sigma_{yz} \\ traction_{z} = n_z \sigma_{zx} + n_y \sigma_{zy} + n_z \sigma_{zz} \\ \end{cases} \]

Where \((x,y,z)\) is the input position coordinate point, \((u,v,w)\) is the strain in three dimensions corresponding to the coordinate point, and \((\sigma_{xx}, \sigma_{yy}, \sigma_{zz}, \sigma_{xy}, \sigma_{xz}, \sigma_{yz})\) is the stress in three dimensions corresponding to the coordinate point.

The inner surface of the left ring of the structure is subjected to a uniformly distributed uniform stress of magnitude \(0.0025\) along the positive z-axis direction. Other parameters include elastic modulus \(E=1\), Poisson's ratio \(\nu=0.3\). The goal is to solve 9 physical quantities \(u\), \(v\), \(w\), \(\sigma_{xx}\), \(\sigma_{yy}\), \(\sigma_{zz}\), \(\sigma_{xy}\), \(\sigma_{xz}\), \(\sigma_{yz}\) at each point on the surface of the metal part. The constant definition code is as follows:

# set working condition
NU: 0.3
E: 1
# T: [0, 0, 0.0025]   # +Z axis
# specify parameters
LAMBDA_ = cfg.NU * cfg.E / ((1 + cfg.NU) * (1 - 2 * cfg.NU))
MU = cfg.E / (2 * (1 + cfg.NU))

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, equation construction, and computational domain construction are described below, while other details please refer to API Documentation.

3.0 Dataset Description

The data used in this project includes the geometric model file (STL): ./datasets/control_arm.stl, used to construct the geometric structure of the automobile suspension control arm structure in this case.

3.1 Stress Analysis Solution

3.1.1 Model Construction

As mentioned above, each known coordinate point \((x, y, z)\) has corresponding unknown quantities to be solved: strain \((u, v, w)\) and stress \((\sigma_{xx}, \sigma_{yy}, \sigma_{zz}, \sigma_{xy}, \sigma_{xz}, \sigma_{yz})\) in three directions.

Considering that the two sets of physical quantities correspond to different equations, two models are used to predict these two sets of physical quantities respectively:

\[ \begin{cases} u, v, w = f(x,y,z) \\ \sigma_{xx}, \sigma_{yy}, \sigma_{zz}, \sigma_{xy}, \sigma_{xz}, \sigma_{yz} = g(x,y,z) \end{cases} \]

In the above formula, \(f\) is the strain model disp_net, and \(g\) is the stress model stress_net. Since they share the input, after defining these two network models in PaddleScience respectively, ppsci.arch.ModelList is used for encapsulation. Expressed in PaddleScience code as follows:

# set model
disp_net = ppsci.arch.MLP(**cfg.MODEL.disp_net)
stress_net = ppsci.arch.MLP(**cfg.MODEL.stress_net)
# wrap to a model_list
model_list = ppsci.arch.ModelList((disp_net, stress_net))

3.1.2 Equation Construction

The linear elasticity equation can use LinearElasticity built in PaddleScience.

# set equation
equation = {
    "LinearElasticity": ppsci.equation.LinearElasticity(
        E=None, nu=None, lambda_=LAMBDA_, mu=MU, dim=3
    )
}

3.1.3 Computational Domain Construction

The geometric area of this problem is specified by the stl file. Follow the "Model Training Command" at the beginning of this document to download and unzip it to the ./datasets/ folder.

Note: The stl file in the dataset comes from the Internet.

Note

Before using the Mesh class, you must install the three geometric dependency packages open3d, pysdf, and PyMesh according to the 1.4.2 Install Mesh Geometry [Optional] document.

Then use PaddleScience's built-in STL geometry class ppsci.geometry.Mesh to read and parse the geometric file, obtain the computational domain, and obtain the geometric structure boundary:

# set geometry
control_arm = ppsci.geometry.Mesh(cfg.GEOM_PATH)
geom = {"geo": control_arm}
# set bounds
BOUNDS_X, BOUNDS_Y, BOUNDS_Z = control_arm.bounds

3.1.4 Hyperparameter Setting

Next, you need to specify the number of training epochs in the configuration file. Here, based on experimental experience, 2000 training epochs are used, with 1000 optimization steps per epoch.

# training settings
TRAIN:
  epochs: 2000

3.1.5 Optimizer Construction

The training process will call the optimizer to update model parameters. Here, the more commonly used Adam optimizer is selected, and the ExponentialDecay learning rate adjustment strategy commonly used in machine learning is used together.

# set optimizer
lr_scheduler = ppsci.optimizer.lr_scheduler.ExponentialDecay(
    **cfg.TRAIN.lr_scheduler
)()
optimizer = ppsci.optimizer.Adam(lr_scheduler)(model_list)

3.1.6 Constraint Construction

This problem involves a total of 4 constraints, which are the constraint of the inner surface of the left ring being stressed, the constraint of the inner surfaces of the two right rings being fixed, the constraint of the structural surface boundary conditions, and the constraint of the internal points of the structure. Before constructing specific constraints, data reading configuration can be constructed first, so that this configuration can be reused when constructing multiple constraints later.

# set dataloader config
train_dataloader_cfg = {
    "dataset": "NamedArrayDataset",
    "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
    "sampler": {
        "name": "BatchSampler",
        "drop_last": True,
        "shuffle": True,
    },
    "num_workers": 1,
}
3.1.6.1 Interior Point Constraint

Taking InteriorConstraint acting on internal points of the structure as an example, the code is as follows:

arm_interior_constraint = ppsci.constraint.InteriorConstraint(
    equation["LinearElasticity"].equations,
    {
        "equilibrium_x": 0,
        "equilibrium_y": 0,
        "equilibrium_z": 0,
        "stress_disp_xx": 0,
        "stress_disp_yy": 0,
        "stress_disp_zz": 0,
        "stress_disp_xy": 0,
        "stress_disp_xz": 0,
        "stress_disp_yz": 0,
    },
    geom["geo"],
    {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.arm_interior},
    ppsci.loss.MSELoss("sum"),
    criteria=lambda x, y, z: (
        (BOUNDS_X[0] < x)
        & (x < BOUNDS_X[1])
        & (BOUNDS_Y[0] < y)
        & (y < BOUNDS_Y[1])
        & (BOUNDS_Z[0] < z)
        & (z < BOUNDS_Z[1])
    ),
    weight_dict={
        "equilibrium_x": "sdf",
        "equilibrium_y": "sdf",
        "equilibrium_z": "sdf",
        "stress_disp_xx": "sdf",
        "stress_disp_yy": "sdf",
        "stress_disp_zz": "sdf",
        "stress_disp_xy": "sdf",
        "stress_disp_xz": "sdf",
        "stress_disp_yz": "sdf",
    },
    name="INTERIOR",
)

The first parameter of InteriorConstraint is the equation (system) expression, which is used to describe how to calculate the constraint target. Here, fill in equation["LinearElasticity"].equations instantiated in the 3.1.2 Equation Construction chapter;

The second parameter is the target value of the constraint variable. In this problem, it is hoped that the 9 values equilibrium_x, equilibrium_y, equilibrium_z, stress_disp_xx, stress_disp_yy, stress_disp_zz, stress_disp_xy, stress_disp_xz, stress_disp_yz related to the LinearElasticity equation are all optimized to 0;

The third parameter is the computational domain on which the constraint equation acts. Here, fill in geom["geo"] instantiated in the 3.1.3 Computational Domain Construction chapter;

The fourth parameter is the sampling configuration on the computational domain. Here, batch_size is set to:

arm_surface: 4096

The fifth parameter is the loss function. Here, the commonly used MSE function is selected, and reduction is set to "sum", that is, the loss terms generated by all data points involved in the calculation will be summed;

The sixth parameter is geometric point filtering. The points sampled on geo need to be filtered. Just pass in a lambda filter function here, which accepts the tensor x, y, z formed by the point set, and returns a boolean tensor indicating whether each point meets the filtering conditions. Not meeting is False, meeting is True. Since the structure of this case comes from the network and the parameters are not completely accurate, 1e-1 is added as a tolerable sampling error.

The seventh parameter is the weight of each point participating in the loss calculation. Here we use "sdf" to indicate using the shortest distance (signed distance function value) of each point to the boundary as the weight. This sdf weighting method can increase the weight of points far from the boundary (hard samples) and reduce the weight of points close to the boundary (simple samples), which is beneficial to improve the accuracy and convergence speed of the model.

The eighth parameter is the name of the constraint condition. Each constraint condition needs to be named to facilitate subsequent indexing. Here it is named "INTERIOR".

3.1.6.2 Boundary Constraint

The inner surface of the left ring of the structure is stressed, and each point on it is subjected to a uniformly distributed load. Referring to 2. Problem Definition, the magnitude is stored in parameter \(T\). Actually, it is a load in the negative x direction with a magnitude of \(0.0025\), and the stress in other directions is 0. There are the following boundary constraint conditions:

arm_left_constraint = ppsci.constraint.BoundaryConstraint(
    equation["LinearElasticity"].equations,
    {"traction_x": cfg.T[0], "traction_y": cfg.T[1], "traction_z": cfg.T[2]},
    geom["geo"],
    {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.arm_left},
    ppsci.loss.MSELoss("sum"),
    criteria=lambda x, y, z: np.sqrt(
        np.square(x - cfg.CIRCLE_LEFT_CENTER_XY[0])
        + np.square(y - cfg.CIRCLE_LEFT_CENTER_XY[1])
    )
    <= cfg.CIRCLE_LEFT_RADIUS + 1e-1,
    name="BC_LEFT",
)

The inner surfaces of the two right rings of the structure are fixed, so the deformation of points on them in three directions is 0. Therefore, there are the following boundary constraint conditions:

arm_right_constraint = ppsci.constraint.BoundaryConstraint(
    {"u": lambda d: d["u"], "v": lambda d: d["v"], "w": lambda d: d["w"]},
    {"u": 0, "v": 0, "w": 0},
    geom["geo"],
    {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.arm_right},
    ppsci.loss.MSELoss("sum"),
    criteria=lambda x, y, z: np.sqrt(
        np.square(x - cfg.CIRCLE_RIGHT_CENTER_XZ[0])
        + np.square(z - cfg.CIRCLE_RIGHT_CENTER_XZ[1])
    )
    <= cfg.CIRCLE_RIGHT_RADIUS + 1e-1,
    weight_dict=cfg.TRAIN.weight.arm_right,
    name="BC_RIGHT",
)

The surface of the structure is not subjected to any load, that is, the internal forces in three directions are balanced and the resultant force is 0. There are the following boundary constraint conditions:

arm_surface_constraint = ppsci.constraint.BoundaryConstraint(
    equation["LinearElasticity"].equations,
    {"traction_x": 0, "traction_y": 0, "traction_z": 0},
    geom["geo"],
    {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.arm_surface},
    ppsci.loss.MSELoss("sum"),
    criteria=lambda x, y, z: np.sqrt(
        np.square(x - cfg.CIRCLE_LEFT_CENTER_XY[0])
        + np.square(y - cfg.CIRCLE_LEFT_CENTER_XY[1])
    )
    > cfg.CIRCLE_LEFT_RADIUS + 1e-1,
    name="BC_SURFACE",
)

After the equation constraint and boundary constraint are constructed, encapsulate them into a dictionary with the names just given as keys for subsequent access.

# wrap constraints togetherg
constraint = {
    arm_left_constraint.name: arm_left_constraint,
    arm_right_constraint.name: arm_right_constraint,
    arm_surface_constraint.name: arm_surface_constraint,
    arm_interior_constraint.name: arm_interior_constraint,
}

3.1.7 Visualizer Construction

During model evaluation, if the evaluation result is data that can be visualized, you can choose a suitable visualizer to visualize the output result.

The input data of the visualizer is generated by calling PaddleScience's API sample_interior, and the output data is the corresponding 9 predicted physical quantities. By setting ppsci.visualize.VisualizerVtu, the evaluated output data can be saved as a vtu format file, which can be viewed by visualization software finally.

# set visualizer(optional)
# add inferencer data
samples = geom["geo"].sample_interior(
    cfg.TRAIN.batch_size.visualizer_vtu,
    criteria=lambda x, y, z: (
        (BOUNDS_X[0] < x)
        & (x < BOUNDS_X[1])
        & (BOUNDS_Y[0] < y)
        & (y < BOUNDS_Y[1])
        & (BOUNDS_Z[0] < z)
        & (z < BOUNDS_Z[1])
    ),
)
pred_input_dict = {
    k: v for k, v in samples.items() if k in cfg.MODEL.disp_net.input_keys
}
visualizer = {
    "visulzie_u_v_w_sigmas": ppsci.visualize.VisualizerVtu(
        pred_input_dict,
        {
            "u": lambda out: out["u"],
            "v": lambda out: out["v"],
            "w": lambda out: out["w"],
            "sigma_xx": lambda out: out["sigma_xx"],
            "sigma_yy": lambda out: out["sigma_yy"],
            "sigma_zz": lambda out: out["sigma_zz"],
            "sigma_xy": lambda out: out["sigma_xy"],
            "sigma_xz": lambda out: out["sigma_xz"],
            "sigma_yz": lambda out: out["sigma_yz"],
        },
        prefix="vis",
    )
}

3.1.8 Model Training

After completing the above settings, you only need to pass the instantiated objects to ppsci.solver.Solver in order, and then start training.

# initialize solver
solver = ppsci.solver.Solver(
    model_list,
    constraint,
    cfg.output_dir,
    optimizer,
    lr_scheduler,
    cfg.TRAIN.epochs,
    cfg.TRAIN.iters_per_epoch,
    seed=cfg.seed,
    equation=equation,
    geom=geom,
    save_freq=cfg.TRAIN.save_freq,
    log_freq=cfg.log_freq,
    eval_freq=cfg.TRAIN.eval_freq,
    eval_during_train=cfg.TRAIN.eval_during_train,
    eval_with_no_grad=cfg.TRAIN.eval_with_no_grad,
    visualizer=visualizer,
    checkpoint_path=cfg.TRAIN.checkpoint_path,
)

# train model
solver.train()

After training, calling ppsci.solver.Solver.plot_loss_history can plot the loss during training:

# plot losses
solver.plot_loss_history(by_epoch=True, smooth_step=1)

In addition, this case provides parallel training settings. Note that after turning on data parallelism, the learning rate should be increased to original learning rate * number of parallel cards to ensure training effect. For details, please refer to User Guide 2.1.1 Data Parallelism.

# set parallel
enable_parallel = dist.get_world_size() > 1
# re-assign to cfg.TRAIN.iters_per_epoch
if enable_parallel:
    cfg.TRAIN.iters_per_epoch = len(arm_left_constraint.data_loader)

3.1.9 Model Evaluation and Visualization

After training is completed or pretrained model is downloaded, perform model evaluation and visualization through the "Model Evaluation Command" at the beginning of this document.

The evaluation and visualization process does not require optimizer construction, etc., only model, computational domain, validator (not included in this case), visualizer need to be constructed, and then passed to ppsci.solver.Solver in order to start evaluation and visualization.

def evaluate(cfg: DictConfig):
    # set random seed for reproducibility
    ppsci.utils.misc.set_random_seed(cfg.seed)
    # initialize logger
    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")

    # set model
    disp_net = ppsci.arch.MLP(**cfg.MODEL.disp_net)
    stress_net = ppsci.arch.MLP(**cfg.MODEL.stress_net)
    # wrap to a model_list
    model_list = ppsci.arch.ModelList((disp_net, stress_net))

    # set geometry
    control_arm = ppsci.geometry.Mesh(cfg.GEOM_PATH)
    # geometry bool operation
    geo = control_arm
    geom = {"geo": geo}
    # set bounds
    BOUNDS_X, BOUNDS_Y, BOUNDS_Z = control_arm.bounds

    # set visualizer(optional)
    # add inferencer data
    samples = geom["geo"].sample_interior(
        cfg.TRAIN.batch_size.visualizer_vtu,
        criteria=lambda x, y, z: (
            (BOUNDS_X[0] < x)
            & (x < BOUNDS_X[1])
            & (BOUNDS_Y[0] < y)
            & (y < BOUNDS_Y[1])
            & (BOUNDS_Z[0] < z)
            & (z < BOUNDS_Z[1])
        ),
    )
    pred_input_dict = {
        k: v for k, v in samples.items() if k in cfg.MODEL.disp_net.input_keys
    }
    visualizer = {
        "visulzie_u_v_w_sigmas": ppsci.visualize.VisualizerVtu(
            pred_input_dict,
            {
                "u": lambda out: out["u"],
                "v": lambda out: out["v"],
                "w": lambda out: out["w"],
                "sigma_xx": lambda out: out["sigma_xx"],
                "sigma_yy": lambda out: out["sigma_yy"],
                "sigma_zz": lambda out: out["sigma_zz"],
                "sigma_xy": lambda out: out["sigma_xy"],
                "sigma_xz": lambda out: out["sigma_xz"],
                "sigma_yz": lambda out: out["sigma_yz"],
            },
            prefix="vis",
        )
    }

    # initialize solver
    solver = ppsci.solver.Solver(
        model_list,
        output_dir=cfg.output_dir,
        seed=cfg.seed,
        geom=geom,
        log_freq=cfg.log_freq,
        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
        visualizer=visualizer,
        pretrained_model_path=cfg.EVAL.pretrained_model_path,
    )

    # visualize prediction after finished training
    solver.visualize()

3.2 Parameter Inversion Solution

3.2.1 Model Construction

The premise of parameter inversion is to know each known coordinate point \((x, y, z)\), and the corresponding strain \((u, v, w)\) and stress \((\sigma_{xx}, \sigma_{yy}, \sigma_{zz}, \sigma_{xy}, \sigma_{xz}, \sigma_{yz})\) in three directions. The source of these variables can be real data, or numerical simulation data, or a trained forward problem model. In this case, we do not use any data, but use the model trained in chapter 3.1 Stress Analysis Solution to obtain these variables, so we still need to build this part of the model, and load the weight parameters obtained from the forward problem solution for disp_net and stress_net as pretrained models. Note that these two models should be frozen to reduce backpropagation time and memory usage.

In parameter inversion, two unknown quantities need to be solved: parameters \(\lambda\) and \(\mu\) of the linear elasticity equation. Two models are used to predict these two sets of physical quantities respectively:

\[ \begin{cases} \lambda = f(x,y,z) \\ \mu = g(x,y,z) \end{cases} \]

In the above formula, \(f\) is the model inverse_lambda_net for solving \(\lambda\), and \(g\) is the model inverse_mu_net for solving \(\mu\).

Since the above two models share input with disp_net and stress_net, a total of four models, after defining these four network models in PaddleScience respectively, ppsci.arch.ModelList is used for encapsulation. Expressed in PaddleScience code as follows:

# set model
disp_net = ppsci.arch.MLP(**cfg.MODEL.disp_net)
stress_net = ppsci.arch.MLP(**cfg.MODEL.stress_net)
inverse_lambda_net = ppsci.arch.MLP(**cfg.MODEL.inverse_lambda_net)
inverse_mu_net = ppsci.arch.MLP(**cfg.MODEL.inverse_mu_net)
# freeze models
disp_net.freeze()
stress_net.freeze()
# wrap to a model_list
model = ppsci.arch.ModelList(
    (disp_net, stress_net, inverse_lambda_net, inverse_mu_net)
)

3.2.2 Equation Construction

The linear elasticity equation can use LinearElasticity built in PaddleScience.

# set equation
equation = {
    "LinearElasticity": ppsci.equation.LinearElasticity(
        E=None, nu=None, lambda_="lambda_", mu="mu", dim=3
    )
}

3.2.3 Computational Domain Construction

The geometric area of this problem is specified by the stl file. Follow the "Model Training Command" at the beginning of this document to download and unzip it to the ./datasets/ folder.

Note: The stl file in the dataset comes from the Internet.

Note

Before using the Mesh class, you must install the three geometric dependency packages open3d, pysdf, and PyMesh according to the 1.4.2 Install Mesh Geometry [Optional] document.

Then use PaddleScience's built-in STL geometry class ppsci.geometry.Mesh to read and parse the geometric file, obtain the computational domain, and obtain the geometric structure boundary:

# set geometry
control_arm = ppsci.geometry.Mesh(cfg.GEOM_PATH)
# geometry bool operation
geo = control_arm
geom = {"geo": geo}
# set bounds
BOUNDS_X, BOUNDS_Y, BOUNDS_Z = control_arm.bounds

3.2.4 Hyperparameter Setting

Next, you need to specify the number of training epochs in the configuration file. Here, based on experimental experience, 100 training epochs are used, with 100 optimization steps per epoch.

# training settings
TRAIN:
  epochs: 100

3.2.5 Optimizer Construction

Since the role of disp_net and stress_net models is only to provide values of strain \((u, v, w)\) and stress \((\sigma_{xx}, \sigma_{yy}, \sigma_{zz}, \sigma_{xy}, \sigma_{xz}, \sigma_{yz})\) in three directions, and does not need training, so when constructing the optimizer, note not to use ModelList encapsulated in 3.2.1 Model Construction as parameter, but use the tuple composed of inverse_lambda_net and inverse_mu_net as parameter.

The training process will call the optimizer to update model parameters. Here, the more commonly used Adam optimizer is selected, and the ExponentialDecay learning rate adjustment strategy commonly used in machine learning is used together.

# set optimizer
lr_scheduler = ppsci.optimizer.lr_scheduler.ExponentialDecay(
    **cfg.TRAIN.lr_scheduler
)()
optimizer = ppsci.optimizer.Adam(lr_scheduler)((inverse_lambda_net, inverse_mu_net))

3.2.6 Constraint Construction

This problem involves a total of 1 constraint, which is the constraint InteriorConstraint of internal points of the structure.

# set dataloader config
interior_constraint = ppsci.constraint.InteriorConstraint(
    equation["LinearElasticity"].equations,
    {
        "stress_disp_xx": 0,
        "stress_disp_yy": 0,
        "stress_disp_zz": 0,
        "stress_disp_xy": 0,
        "stress_disp_xz": 0,
        "stress_disp_yz": 0,
    },
    geom["geo"],
    {
        "dataset": "NamedArrayDataset",
        "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
        "sampler": {
            "name": "BatchSampler",
            "drop_last": True,
            "shuffle": True,
        },
        "num_workers": 1,
        "batch_size": cfg.TRAIN.batch_size.arm_interior,
    },
    ppsci.loss.MSELoss("sum"),
    criteria=lambda x, y, z: (
        (BOUNDS_X[0] < x)
        & (x < BOUNDS_X[1])
        & (BOUNDS_Y[0] < y)
        & (y < BOUNDS_Y[1])
        & (BOUNDS_Z[0] < z)
        & (z < BOUNDS_Z[1])
    ),
    name="INTERIOR",
)

The first parameter of InteriorConstraint is the equation (system) expression, which is used to describe how to calculate the constraint target. Here, fill in equation["LinearElasticity"].equations instantiated in the 3.2.2 Equation Construction chapter;

The second parameter is the target value of the constraint variable. In this problem, it is hoped that the 6 values stress_disp_xx, stress_disp_yy, stress_disp_zz, stress_disp_xy, stress_disp_xz, stress_disp_yz related to the LinearElasticity equation and containing parameters \(\lambda\) and \(\mu\) are all optimized to 0;

The third parameter is the computational domain on which the constraint equation acts. Here, fill in geom["geo"] instantiated in the 3.2.3 Computational Domain Construction chapter;

The fourth parameter is the sampling configuration on the computational domain. Here, batch_size is set to:

batch_size:

The fifth parameter is the loss function. Here, the commonly used MSE function is selected, and reduction is set to "sum", that is, the loss terms generated by all data points involved in the calculation will be summed;

The sixth parameter is geometric point filtering. The points sampled on geo need to be filtered. Just pass in a lambda filter function here, which accepts the tensor x, y, z formed by the point set, and returns a boolean tensor indicating whether each point meets the filtering conditions. Not meeting is False, meeting is True;

The seventh parameter is the name of the constraint condition. Each constraint condition needs to be named to facilitate subsequent indexing. Here it is named "INTERIOR".

After the constraint is constructed, encapsulate it into a dictionary with the names just given as keys for subsequent access.

constraint = {interior_constraint.name: interior_constraint}

3.2.7 Validator Construction

In the training process, the training status of the current model is usually evaluated using the validation set (test set) at a certain epoch interval. Since we use the pretrained model of the forward problem to provide data, the value of label is known to be approximately \(\lambda=0.57692\) and \(\mu=0.38462\). Wrap it into a dictionary and pass it to ppsci.validate.GeometryValidator to construct the validator and encapsulate it.

# set validator
LAMBDA_ = cfg.NU * cfg.E / ((1 + cfg.NU) * (1 - 2 * cfg.NU))  # 0.5769
MU = cfg.E / (2 * (1 + cfg.NU))  # 0.3846
geom_validator = ppsci.validate.GeometryValidator(
    {
        "lambda_": lambda out: out["lambda_"],
        "mu": lambda out: out["mu"],
    },
    {
        "lambda_": LAMBDA_,
        "mu": MU,
    },
    geom["geo"],
    {
        "dataset": "NamedArrayDataset",
        "sampler": {
            "name": "BatchSampler",
            "drop_last": False,
            "shuffle": False,
        },
        "total_size": cfg.EVAL.total_size.validator,
        "batch_size": cfg.EVAL.batch_size.validator,
    },
    ppsci.loss.MSELoss("sum"),
    metric={"L2Rel": ppsci.metric.L2Rel()},
    name="geo_eval",
)
validator = {geom_validator.name: geom_validator}

3.2.8 Visualizer Construction

During model evaluation, if the evaluation result is data that can be visualized, you can choose a suitable visualizer to visualize the output result.

The input data of the visualizer is generated by calling PaddleScience's API sample_interior, and the output data is the physical quantities predicted by \(\lambda\) and \(\mu\). By setting ppsci.visualize.VisualizerVtu, the evaluated output data can be saved as a vtu format file, which can be viewed by visualization software finally.

# set visualizer(optional)
# add inferencer data
samples = geom["geo"].sample_interior(
    cfg.TRAIN.batch_size.visualizer_vtu,
    criteria=lambda x, y, z: (
        (BOUNDS_X[0] < x)
        & (x < BOUNDS_X[1])
        & (BOUNDS_Y[0] < y)
        & (y < BOUNDS_Y[1])
        & (BOUNDS_Z[0] < z)
        & (z < BOUNDS_Z[1])
    ),
)
pred_input_dict = {
    k: v for k, v in samples.items() if k in cfg.MODEL.disp_net.input_keys
}
visualizer = {
    "visulzie_lambda_mu": ppsci.visualize.VisualizerVtu(
        pred_input_dict,
        {
            "lambda": lambda out: out["lambda_"],
            "mu": lambda out: out["mu"],
        },
        prefix="vis",
    )
}

3.2.9 Model Training

After completing the above settings, you only need to pass the instantiated objects to ppsci.solver.Solver in order, and then start training.

# initialize solver
solver = ppsci.solver.Solver(
    model,
    constraint,
    cfg.output_dir,
    optimizer,
    lr_scheduler,
    cfg.TRAIN.epochs,
    cfg.TRAIN.iters_per_epoch,
    seed=cfg.seed,
    equation=equation,
    geom=geom,
    save_freq=cfg.TRAIN.save_freq,
    log_freq=cfg.log_freq,
    eval_freq=cfg.TRAIN.eval_freq,
    eval_during_train=cfg.TRAIN.eval_during_train,
    eval_with_no_grad=cfg.TRAIN.eval_with_no_grad,
    validator=validator,
    visualizer=visualizer,
    pretrained_model_path=cfg.TRAIN.pretrained_model_path,
)

# train model
solver.train()

After training, calling ppsci.solver.Solver.plot_loss_history can plot the loss during training:

# plot losses
solver.plot_loss_history(by_epoch=False, smooth_step=1, use_semilogy=True)

3.2.10 Model Evaluation and Visualization

After training is completed or pretrained model is downloaded, perform model evaluation and visualization through the "Model Evaluation Command" at the beginning of this document.

The evaluation and visualization process does not require optimizer construction, etc., only model, computational domain, validator (not included in this case), visualizer need to be constructed, and then passed to ppsci.solver.Solver in order to start evaluation and visualization.

def evaluate(cfg: DictConfig):
    # set random seed for reproducibility
    ppsci.utils.misc.set_random_seed(cfg.seed)
    # initialize logger
    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")

    # set model
    disp_net = ppsci.arch.MLP(**cfg.MODEL.disp_net)
    stress_net = ppsci.arch.MLP(**cfg.MODEL.stress_net)
    inverse_lambda_net = ppsci.arch.MLP(**cfg.MODEL.inverse_lambda_net)
    inverse_mu_net = ppsci.arch.MLP(**cfg.MODEL.inverse_mu_net)
    # wrap to a model_list
    model = ppsci.arch.ModelList(
        (disp_net, stress_net, inverse_lambda_net, inverse_mu_net)
    )

    # set geometry
    control_arm = ppsci.geometry.Mesh(cfg.GEOM_PATH)
    # geometry bool operation
    geo = control_arm
    geom = {"geo": geo}
    # set bounds
    BOUNDS_X, BOUNDS_Y, BOUNDS_Z = control_arm.bounds

    # set validator
    LAMBDA_ = cfg.NU * cfg.E / ((1 + cfg.NU) * (1 - 2 * cfg.NU))  # 0.57692
    MU = cfg.E / (2 * (1 + cfg.NU))  # 0.38462
    geom_validator = ppsci.validate.GeometryValidator(
        {
            "lambda_": lambda out: out["lambda_"],
            "mu": lambda out: out["mu"],
        },
        {
            "lambda_": LAMBDA_,
            "mu": MU,
        },
        geom["geo"],
        {
            "dataset": "NamedArrayDataset",
            "sampler": {
                "name": "BatchSampler",
                "drop_last": False,
                "shuffle": False,
            },
            "total_size": cfg.EVAL.total_size.validator,
            "batch_size": cfg.EVAL.batch_size.validator,
        },
        ppsci.loss.MSELoss("sum"),
        metric={"L2Rel": ppsci.metric.L2Rel()},
        name="geo_eval",
    )
    validator = {geom_validator.name: geom_validator}

    # set visualizer(optional)
    # add inferencer data
    samples = geom["geo"].sample_interior(
        cfg.EVAL.batch_size.visualizer_vtu,
        criteria=lambda x, y, z: (
            (BOUNDS_X[0] < x)
            & (x < BOUNDS_X[1])
            & (BOUNDS_Y[0] < y)
            & (y < BOUNDS_Y[1])
            & (BOUNDS_Z[0] < z)
            & (z < BOUNDS_Z[1])
        ),
    )
    pred_input_dict = {
        k: v for k, v in samples.items() if k in cfg.MODEL.disp_net.input_keys
    }
    visualizer = {
        "visulzie_lambda_mu": ppsci.visualize.VisualizerVtu(
            pred_input_dict,
            {
                "lambda": lambda out: out["lambda_"],
                "mu": lambda out: out["mu"],
            },
            prefix="vis",
        )
    }

    # initialize solver
    solver = ppsci.solver.Solver(
        model,
        output_dir=cfg.output_dir,
        seed=cfg.seed,
        log_freq=cfg.log_freq,
        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
        validator=validator,
        visualizer=visualizer,
        pretrained_model_path=cfg.EVAL.pretrained_model_path,
    )
    # evaluate after finished training
    solver.eval()
    # visualize prediction after finished training
    solver.visualize()

4. Complete Code

forward_analysis.py
from os import path as osp

import hydra
import numpy as np
from omegaconf import DictConfig
from paddle import distributed as dist

import ppsci
from ppsci.utils import logger


def train(cfg: DictConfig):
    # set random seed for reproducibility
    ppsci.utils.misc.set_random_seed(cfg.seed)
    # initialize logger
    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
    # set parallel
    enable_parallel = dist.get_world_size() > 1

    # set model
    disp_net = ppsci.arch.MLP(**cfg.MODEL.disp_net)
    stress_net = ppsci.arch.MLP(**cfg.MODEL.stress_net)
    # wrap to a model_list
    model_list = ppsci.arch.ModelList((disp_net, stress_net))

    # set optimizer
    lr_scheduler = ppsci.optimizer.lr_scheduler.ExponentialDecay(
        **cfg.TRAIN.lr_scheduler
    )()
    optimizer = ppsci.optimizer.Adam(lr_scheduler)(model_list)

    # specify parameters
    LAMBDA_ = cfg.NU * cfg.E / ((1 + cfg.NU) * (1 - 2 * cfg.NU))
    MU = cfg.E / (2 * (1 + cfg.NU))

    # set equation
    equation = {
        "LinearElasticity": ppsci.equation.LinearElasticity(
            E=None, nu=None, lambda_=LAMBDA_, mu=MU, dim=3
        )
    }

    # set geometry
    control_arm = ppsci.geometry.Mesh(cfg.GEOM_PATH)
    geom = {"geo": control_arm}
    # set bounds
    BOUNDS_X, BOUNDS_Y, BOUNDS_Z = control_arm.bounds

    # set dataloader config
    train_dataloader_cfg = {
        "dataset": "NamedArrayDataset",
        "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
        "sampler": {
            "name": "BatchSampler",
            "drop_last": True,
            "shuffle": True,
        },
        "num_workers": 1,
    }

    # set constraint
    arm_left_constraint = ppsci.constraint.BoundaryConstraint(
        equation["LinearElasticity"].equations,
        {"traction_x": cfg.T[0], "traction_y": cfg.T[1], "traction_z": cfg.T[2]},
        geom["geo"],
        {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.arm_left},
        ppsci.loss.MSELoss("sum"),
        criteria=lambda x, y, z: np.sqrt(
            np.square(x - cfg.CIRCLE_LEFT_CENTER_XY[0])
            + np.square(y - cfg.CIRCLE_LEFT_CENTER_XY[1])
        )
        <= cfg.CIRCLE_LEFT_RADIUS + 1e-1,
        name="BC_LEFT",
    )
    arm_right_constraint = ppsci.constraint.BoundaryConstraint(
        {"u": lambda d: d["u"], "v": lambda d: d["v"], "w": lambda d: d["w"]},
        {"u": 0, "v": 0, "w": 0},
        geom["geo"],
        {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.arm_right},
        ppsci.loss.MSELoss("sum"),
        criteria=lambda x, y, z: np.sqrt(
            np.square(x - cfg.CIRCLE_RIGHT_CENTER_XZ[0])
            + np.square(z - cfg.CIRCLE_RIGHT_CENTER_XZ[1])
        )
        <= cfg.CIRCLE_RIGHT_RADIUS + 1e-1,
        weight_dict=cfg.TRAIN.weight.arm_right,
        name="BC_RIGHT",
    )
    arm_surface_constraint = ppsci.constraint.BoundaryConstraint(
        equation["LinearElasticity"].equations,
        {"traction_x": 0, "traction_y": 0, "traction_z": 0},
        geom["geo"],
        {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.arm_surface},
        ppsci.loss.MSELoss("sum"),
        criteria=lambda x, y, z: np.sqrt(
            np.square(x - cfg.CIRCLE_LEFT_CENTER_XY[0])
            + np.square(y - cfg.CIRCLE_LEFT_CENTER_XY[1])
        )
        > cfg.CIRCLE_LEFT_RADIUS + 1e-1,
        name="BC_SURFACE",
    )
    arm_interior_constraint = ppsci.constraint.InteriorConstraint(
        equation["LinearElasticity"].equations,
        {
            "equilibrium_x": 0,
            "equilibrium_y": 0,
            "equilibrium_z": 0,
            "stress_disp_xx": 0,
            "stress_disp_yy": 0,
            "stress_disp_zz": 0,
            "stress_disp_xy": 0,
            "stress_disp_xz": 0,
            "stress_disp_yz": 0,
        },
        geom["geo"],
        {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.arm_interior},
        ppsci.loss.MSELoss("sum"),
        criteria=lambda x, y, z: (
            (BOUNDS_X[0] < x)
            & (x < BOUNDS_X[1])
            & (BOUNDS_Y[0] < y)
            & (y < BOUNDS_Y[1])
            & (BOUNDS_Z[0] < z)
            & (z < BOUNDS_Z[1])
        ),
        weight_dict={
            "equilibrium_x": "sdf",
            "equilibrium_y": "sdf",
            "equilibrium_z": "sdf",
            "stress_disp_xx": "sdf",
            "stress_disp_yy": "sdf",
            "stress_disp_zz": "sdf",
            "stress_disp_xy": "sdf",
            "stress_disp_xz": "sdf",
            "stress_disp_yz": "sdf",
        },
        name="INTERIOR",
    )

    # re-assign to cfg.TRAIN.iters_per_epoch
    if enable_parallel:
        cfg.TRAIN.iters_per_epoch = len(arm_left_constraint.data_loader)

    # wrap constraints togetherg
    constraint = {
        arm_left_constraint.name: arm_left_constraint,
        arm_right_constraint.name: arm_right_constraint,
        arm_surface_constraint.name: arm_surface_constraint,
        arm_interior_constraint.name: arm_interior_constraint,
    }

    # set visualizer(optional)
    # add inferencer data
    samples = geom["geo"].sample_interior(
        cfg.TRAIN.batch_size.visualizer_vtu,
        criteria=lambda x, y, z: (
            (BOUNDS_X[0] < x)
            & (x < BOUNDS_X[1])
            & (BOUNDS_Y[0] < y)
            & (y < BOUNDS_Y[1])
            & (BOUNDS_Z[0] < z)
            & (z < BOUNDS_Z[1])
        ),
    )
    pred_input_dict = {
        k: v for k, v in samples.items() if k in cfg.MODEL.disp_net.input_keys
    }
    visualizer = {
        "visulzie_u_v_w_sigmas": ppsci.visualize.VisualizerVtu(
            pred_input_dict,
            {
                "u": lambda out: out["u"],
                "v": lambda out: out["v"],
                "w": lambda out: out["w"],
                "sigma_xx": lambda out: out["sigma_xx"],
                "sigma_yy": lambda out: out["sigma_yy"],
                "sigma_zz": lambda out: out["sigma_zz"],
                "sigma_xy": lambda out: out["sigma_xy"],
                "sigma_xz": lambda out: out["sigma_xz"],
                "sigma_yz": lambda out: out["sigma_yz"],
            },
            prefix="vis",
        )
    }

    # initialize solver
    solver = ppsci.solver.Solver(
        model_list,
        constraint,
        cfg.output_dir,
        optimizer,
        lr_scheduler,
        cfg.TRAIN.epochs,
        cfg.TRAIN.iters_per_epoch,
        seed=cfg.seed,
        equation=equation,
        geom=geom,
        save_freq=cfg.TRAIN.save_freq,
        log_freq=cfg.log_freq,
        eval_freq=cfg.TRAIN.eval_freq,
        eval_during_train=cfg.TRAIN.eval_during_train,
        eval_with_no_grad=cfg.TRAIN.eval_with_no_grad,
        visualizer=visualizer,
        checkpoint_path=cfg.TRAIN.checkpoint_path,
    )

    # train model
    solver.train()

    # plot losses
    solver.plot_loss_history(by_epoch=True, smooth_step=1)


def evaluate(cfg: DictConfig):
    # set random seed for reproducibility
    ppsci.utils.misc.set_random_seed(cfg.seed)
    # initialize logger
    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")

    # set model
    disp_net = ppsci.arch.MLP(**cfg.MODEL.disp_net)
    stress_net = ppsci.arch.MLP(**cfg.MODEL.stress_net)
    # wrap to a model_list
    model_list = ppsci.arch.ModelList((disp_net, stress_net))

    # set geometry
    control_arm = ppsci.geometry.Mesh(cfg.GEOM_PATH)
    # geometry bool operation
    geo = control_arm
    geom = {"geo": geo}
    # set bounds
    BOUNDS_X, BOUNDS_Y, BOUNDS_Z = control_arm.bounds

    # set visualizer(optional)
    # add inferencer data
    samples = geom["geo"].sample_interior(
        cfg.TRAIN.batch_size.visualizer_vtu,
        criteria=lambda x, y, z: (
            (BOUNDS_X[0] < x)
            & (x < BOUNDS_X[1])
            & (BOUNDS_Y[0] < y)
            & (y < BOUNDS_Y[1])
            & (BOUNDS_Z[0] < z)
            & (z < BOUNDS_Z[1])
        ),
    )
    pred_input_dict = {
        k: v for k, v in samples.items() if k in cfg.MODEL.disp_net.input_keys
    }
    visualizer = {
        "visulzie_u_v_w_sigmas": ppsci.visualize.VisualizerVtu(
            pred_input_dict,
            {
                "u": lambda out: out["u"],
                "v": lambda out: out["v"],
                "w": lambda out: out["w"],
                "sigma_xx": lambda out: out["sigma_xx"],
                "sigma_yy": lambda out: out["sigma_yy"],
                "sigma_zz": lambda out: out["sigma_zz"],
                "sigma_xy": lambda out: out["sigma_xy"],
                "sigma_xz": lambda out: out["sigma_xz"],
                "sigma_yz": lambda out: out["sigma_yz"],
            },
            prefix="vis",
        )
    }

    # initialize solver
    solver = ppsci.solver.Solver(
        model_list,
        output_dir=cfg.output_dir,
        seed=cfg.seed,
        geom=geom,
        log_freq=cfg.log_freq,
        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
        visualizer=visualizer,
        pretrained_model_path=cfg.EVAL.pretrained_model_path,
    )

    # visualize prediction after finished training
    solver.visualize()


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

    # set model
    disp_net = ppsci.arch.MLP(**cfg.MODEL.disp_net)
    stress_net = ppsci.arch.MLP(**cfg.MODEL.stress_net)
    # wrap to a model_list
    model_list = ppsci.arch.ModelList((disp_net, stress_net))

    # load pretrained model
    solver = ppsci.solver.Solver(
        model=model_list, pretrained_model_path=cfg.INFER.pretrained_model_path
    )

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


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

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

    # set geometry
    control_arm = ppsci.geometry.Mesh(cfg.GEOM_PATH)
    # geometry bool operation
    geo = control_arm
    geom = {"geo": geo}
    # set bounds
    BOUNDS_X, BOUNDS_Y, BOUNDS_Z = control_arm.bounds

    # set visualizer(optional)
    # add inferencer data
    samples = geom["geo"].sample_interior(
        cfg.TRAIN.batch_size.visualizer_vtu,
        criteria=lambda x, y, z: (
            (BOUNDS_X[0] < x)
            & (x < BOUNDS_X[1])
            & (BOUNDS_Y[0] < y)
            & (y < BOUNDS_Y[1])
            & (BOUNDS_Z[0] < z)
            & (z < BOUNDS_Z[1])
        ),
    )
    pred_input_dict = {
        k: v for k, v in samples.items() if k in cfg.MODEL.disp_net.input_keys
    }

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

    # mapping data to output_keys
    output_keys = cfg.MODEL.disp_net.output_keys + cfg.MODEL.stress_net.output_keys
    output_dict = {
        store_key: output_dict[infer_key]
        for store_key, infer_key in zip(output_keys, output_dict.keys())
    }
    output_dict.update(pred_input_dict)

    vtu.save_vtu_from_dict(
        osp.join(cfg.output_dir, "vis"),
        output_dict,
        cfg.MODEL.disp_net.input_keys,
        output_keys,
        1,
    )


@hydra.main(
    version_base=None, config_path="./conf", config_name="forward_analysis.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()
inverse_parameter.py
from os import path as osp

import hydra
from omegaconf import DictConfig

import ppsci
from ppsci.utils import logger


def train(cfg: DictConfig):
    # set random seed for reproducibility
    ppsci.utils.misc.set_random_seed(cfg.seed)
    # initialize logger
    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")

    # set model
    disp_net = ppsci.arch.MLP(**cfg.MODEL.disp_net)
    stress_net = ppsci.arch.MLP(**cfg.MODEL.stress_net)
    inverse_lambda_net = ppsci.arch.MLP(**cfg.MODEL.inverse_lambda_net)
    inverse_mu_net = ppsci.arch.MLP(**cfg.MODEL.inverse_mu_net)
    # freeze models
    disp_net.freeze()
    stress_net.freeze()
    # wrap to a model_list
    model = ppsci.arch.ModelList(
        (disp_net, stress_net, inverse_lambda_net, inverse_mu_net)
    )

    # set optimizer
    lr_scheduler = ppsci.optimizer.lr_scheduler.ExponentialDecay(
        **cfg.TRAIN.lr_scheduler
    )()
    optimizer = ppsci.optimizer.Adam(lr_scheduler)((inverse_lambda_net, inverse_mu_net))

    # set equation
    equation = {
        "LinearElasticity": ppsci.equation.LinearElasticity(
            E=None, nu=None, lambda_="lambda_", mu="mu", dim=3
        )
    }

    # set geometry
    control_arm = ppsci.geometry.Mesh(cfg.GEOM_PATH)
    # geometry bool operation
    geo = control_arm
    geom = {"geo": geo}
    # set bounds
    BOUNDS_X, BOUNDS_Y, BOUNDS_Z = control_arm.bounds

    # set dataloader config
    interior_constraint = ppsci.constraint.InteriorConstraint(
        equation["LinearElasticity"].equations,
        {
            "stress_disp_xx": 0,
            "stress_disp_yy": 0,
            "stress_disp_zz": 0,
            "stress_disp_xy": 0,
            "stress_disp_xz": 0,
            "stress_disp_yz": 0,
        },
        geom["geo"],
        {
            "dataset": "NamedArrayDataset",
            "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
            "sampler": {
                "name": "BatchSampler",
                "drop_last": True,
                "shuffle": True,
            },
            "num_workers": 1,
            "batch_size": cfg.TRAIN.batch_size.arm_interior,
        },
        ppsci.loss.MSELoss("sum"),
        criteria=lambda x, y, z: (
            (BOUNDS_X[0] < x)
            & (x < BOUNDS_X[1])
            & (BOUNDS_Y[0] < y)
            & (y < BOUNDS_Y[1])
            & (BOUNDS_Z[0] < z)
            & (z < BOUNDS_Z[1])
        ),
        name="INTERIOR",
    )
    constraint = {interior_constraint.name: interior_constraint}

    # set validator
    LAMBDA_ = cfg.NU * cfg.E / ((1 + cfg.NU) * (1 - 2 * cfg.NU))  # 0.5769
    MU = cfg.E / (2 * (1 + cfg.NU))  # 0.3846
    geom_validator = ppsci.validate.GeometryValidator(
        {
            "lambda_": lambda out: out["lambda_"],
            "mu": lambda out: out["mu"],
        },
        {
            "lambda_": LAMBDA_,
            "mu": MU,
        },
        geom["geo"],
        {
            "dataset": "NamedArrayDataset",
            "sampler": {
                "name": "BatchSampler",
                "drop_last": False,
                "shuffle": False,
            },
            "total_size": cfg.EVAL.total_size.validator,
            "batch_size": cfg.EVAL.batch_size.validator,
        },
        ppsci.loss.MSELoss("sum"),
        metric={"L2Rel": ppsci.metric.L2Rel()},
        name="geo_eval",
    )
    validator = {geom_validator.name: geom_validator}

    # set visualizer(optional)
    # add inferencer data
    samples = geom["geo"].sample_interior(
        cfg.TRAIN.batch_size.visualizer_vtu,
        criteria=lambda x, y, z: (
            (BOUNDS_X[0] < x)
            & (x < BOUNDS_X[1])
            & (BOUNDS_Y[0] < y)
            & (y < BOUNDS_Y[1])
            & (BOUNDS_Z[0] < z)
            & (z < BOUNDS_Z[1])
        ),
    )
    pred_input_dict = {
        k: v for k, v in samples.items() if k in cfg.MODEL.disp_net.input_keys
    }
    visualizer = {
        "visulzie_lambda_mu": ppsci.visualize.VisualizerVtu(
            pred_input_dict,
            {
                "lambda": lambda out: out["lambda_"],
                "mu": lambda out: out["mu"],
            },
            prefix="vis",
        )
    }

    # initialize solver
    solver = ppsci.solver.Solver(
        model,
        constraint,
        cfg.output_dir,
        optimizer,
        lr_scheduler,
        cfg.TRAIN.epochs,
        cfg.TRAIN.iters_per_epoch,
        seed=cfg.seed,
        equation=equation,
        geom=geom,
        save_freq=cfg.TRAIN.save_freq,
        log_freq=cfg.log_freq,
        eval_freq=cfg.TRAIN.eval_freq,
        eval_during_train=cfg.TRAIN.eval_during_train,
        eval_with_no_grad=cfg.TRAIN.eval_with_no_grad,
        validator=validator,
        visualizer=visualizer,
        pretrained_model_path=cfg.TRAIN.pretrained_model_path,
    )

    # train model
    solver.train()

    # plot losses
    solver.plot_loss_history(by_epoch=False, smooth_step=1, use_semilogy=True)


def evaluate(cfg: DictConfig):
    # set random seed for reproducibility
    ppsci.utils.misc.set_random_seed(cfg.seed)
    # initialize logger
    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")

    # set model
    disp_net = ppsci.arch.MLP(**cfg.MODEL.disp_net)
    stress_net = ppsci.arch.MLP(**cfg.MODEL.stress_net)
    inverse_lambda_net = ppsci.arch.MLP(**cfg.MODEL.inverse_lambda_net)
    inverse_mu_net = ppsci.arch.MLP(**cfg.MODEL.inverse_mu_net)
    # wrap to a model_list
    model = ppsci.arch.ModelList(
        (disp_net, stress_net, inverse_lambda_net, inverse_mu_net)
    )

    # set geometry
    control_arm = ppsci.geometry.Mesh(cfg.GEOM_PATH)
    # geometry bool operation
    geo = control_arm
    geom = {"geo": geo}
    # set bounds
    BOUNDS_X, BOUNDS_Y, BOUNDS_Z = control_arm.bounds

    # set validator
    LAMBDA_ = cfg.NU * cfg.E / ((1 + cfg.NU) * (1 - 2 * cfg.NU))  # 0.57692
    MU = cfg.E / (2 * (1 + cfg.NU))  # 0.38462
    geom_validator = ppsci.validate.GeometryValidator(
        {
            "lambda_": lambda out: out["lambda_"],
            "mu": lambda out: out["mu"],
        },
        {
            "lambda_": LAMBDA_,
            "mu": MU,
        },
        geom["geo"],
        {
            "dataset": "NamedArrayDataset",
            "sampler": {
                "name": "BatchSampler",
                "drop_last": False,
                "shuffle": False,
            },
            "total_size": cfg.EVAL.total_size.validator,
            "batch_size": cfg.EVAL.batch_size.validator,
        },
        ppsci.loss.MSELoss("sum"),
        metric={"L2Rel": ppsci.metric.L2Rel()},
        name="geo_eval",
    )
    validator = {geom_validator.name: geom_validator}

    # set visualizer(optional)
    # add inferencer data
    samples = geom["geo"].sample_interior(
        cfg.EVAL.batch_size.visualizer_vtu,
        criteria=lambda x, y, z: (
            (BOUNDS_X[0] < x)
            & (x < BOUNDS_X[1])
            & (BOUNDS_Y[0] < y)
            & (y < BOUNDS_Y[1])
            & (BOUNDS_Z[0] < z)
            & (z < BOUNDS_Z[1])
        ),
    )
    pred_input_dict = {
        k: v for k, v in samples.items() if k in cfg.MODEL.disp_net.input_keys
    }
    visualizer = {
        "visulzie_lambda_mu": ppsci.visualize.VisualizerVtu(
            pred_input_dict,
            {
                "lambda": lambda out: out["lambda_"],
                "mu": lambda out: out["mu"],
            },
            prefix="vis",
        )
    }

    # initialize solver
    solver = ppsci.solver.Solver(
        model,
        output_dir=cfg.output_dir,
        seed=cfg.seed,
        log_freq=cfg.log_freq,
        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
        validator=validator,
        visualizer=visualizer,
        pretrained_model_path=cfg.EVAL.pretrained_model_path,
    )
    # evaluate after finished training
    solver.eval()
    # visualize prediction after finished training
    solver.visualize()


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

    # set model
    disp_net = ppsci.arch.MLP(**cfg.MODEL.disp_net)
    stress_net = ppsci.arch.MLP(**cfg.MODEL.stress_net)
    inverse_lambda_net = ppsci.arch.MLP(**cfg.MODEL.inverse_lambda_net)
    inverse_mu_net = ppsci.arch.MLP(**cfg.MODEL.inverse_mu_net)
    # wrap to a model_list
    model = ppsci.arch.ModelList(
        (disp_net, stress_net, inverse_lambda_net, inverse_mu_net)
    )

    # load pretrained model
    solver = ppsci.solver.Solver(
        model=model, pretrained_model_path=cfg.INFER.pretrained_model_path
    )

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


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

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

    # set geometry
    control_arm = ppsci.geometry.Mesh(cfg.GEOM_PATH)
    # geometry bool operation
    geo = control_arm
    geom = {"geo": geo}
    # set bounds
    BOUNDS_X, BOUNDS_Y, BOUNDS_Z = control_arm.bounds
    samples = geom["geo"].sample_interior(
        cfg.EVAL.batch_size.visualizer_vtu,
        criteria=lambda x, y, z: (
            (BOUNDS_X[0] < x)
            & (x < BOUNDS_X[1])
            & (BOUNDS_Y[0] < y)
            & (y < BOUNDS_Y[1])
            & (BOUNDS_Z[0] < z)
            & (z < BOUNDS_Z[1])
        ),
    )
    pred_input_dict = {
        k: v for k, v in samples.items() if k in cfg.MODEL.disp_net.input_keys
    }

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

    # mapping data to output_keys
    output_keys = (
        cfg.MODEL.disp_net.output_keys
        + cfg.MODEL.stress_net.output_keys
        + cfg.MODEL.inverse_lambda_net.output_keys
        + cfg.MODEL.inverse_mu_net.output_keys
    )
    output_dict = {
        store_key: output_dict[infer_key]
        for store_key, infer_key in zip(output_keys, output_dict.keys())
    }
    output_dict.update(pred_input_dict)
    vtu.save_vtu_from_dict(
        osp.join(cfg.output_dir, "vis"),
        output_dict,
        cfg.MODEL.disp_net.input_keys,
        output_keys,
        1,
    )


@hydra.main(
    version_base=None, config_path="./conf", config_name="inverse_parameter.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

5.1 Stress Analysis Solution

The following shows the model prediction results of strain \(u, v, w\) in 3 directions and 6 stresses \(\sigma_{xx}, \sigma_{yy}, \sigma_{zz}, \sigma_{xy}, \sigma_{xz}, \sigma_{yz}\) when the force direction is the positive x direction. The results are basically consistent with cognition.

forward_result.jpg

Left: Predicted structural strain u; Middle: Predicted structural strain v; Right: Predicted structural strain w

forward_result.jpg

Left: Predicted structural stress sigma_xx; Middle: Predicted structural stress sigma_xy; Right: Predicted structural stress sigma_xz

forward_result.jpg

Left: Predicted structural stress sigma_yy; Middle: Predicted structural stress sigma_yz; Right: Predicted structural stress sigma_zz

5.2 Parameter Inversion Solution

The following shows the model prediction results of linear elasticity equation parameters \(\lambda, \mu\). The prediction error in most areas of the structure is about 1%.

data lambda mu
outs(mean) 0.54950 0.38642
label 0.57692 0.38462

forward_result.jpg

Left: Predicted lambda; Right: Predicted mu