Skip to content

Heart

# linux
wget -c https://paddle-org.bj.bcebos.com/paddlescience/datasets/heart/heart_dataset.tar
# windows
# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/heart/heart_dataset.tar
tar -xvf heart_dataset.tar
python forward.py
# linux
wget -c https://paddle-org.bj.bcebos.com/paddlescience/datasets/heart/heart_dataset.tar
# windows
# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/heart/heart_dataset.tar
tar -xvf heart_dataset.tar
python inverse.py TRAIN.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/heart/inverse_pretrained.pdparams
# linux
wget -c https://paddle-org.bj.bcebos.com/paddlescience/datasets/heart/heart_dataset.tar
# windows
# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/heart/heart_dataset.tar
tar -xvf heart_dataset.tar
python forward.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/heart/forward_pretrained.pdparams
# linux
wget -c https://paddle-org.bj.bcebos.com/paddlescience/datasets/heart/heart_dataset.tar
# windows
# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/heart/heart_dataset.tar
tar -xvf heart_dataset.tar
python inverse.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/heart/inverse_pretrained.pdparams EVAL.param_E_path=https://paddle-org.bj.bcebos.com/paddlescience/models/heart/param_E.pdparams
Pretrained Model Metrics
forward_pretrained.pdparams loss(ref_u_v_w): 0.00076
L2Rel.u(ref_u_v_w): 0.01162
L2Rel.v(ref_u_v_w): 0.00511
L2Rel.w(ref_u_v_w): 0.00737
inverse_pretrained.pdparams
param_E.pdparams
loss(ref_u_v_w): 0.00576
L2Rel.u(ref_u_v_w): 0.03082
L2Rel.v(ref_u_v_w): 0.01412
L2Rel.w(ref_u_v_w): 0.02075
L2_Error(E): 0.04975

1. Background Introduction

Cardiovascular disease has become the number one killer threatening human health. Biomechanical modeling based on individual images has played an important role in understanding heart disease and developing new diagnosis and treatment plans. However, in the field of cardiac biomechanical modeling, traditional finite element methods have problems such as tedious meshing and slow solution speed, which limits their application in personalized heart modeling and diagnosis and treatment. Physics Informed Neural Network (PINN) is an algorithm for solving partial differential equations based on neural networks that has emerged in recent years. It has achieved certain results in the field of fluid mechanics and has attracted the attention of a large number of researchers. It has broad application prospects. Solving cardiac biomechanical equations and simulating calculations through PINN can greatly improve the efficiency of individual heart modeling.

This case uses the PINN network. On the left ventricle model of an individualized heart, according to the theory of elasticity, the linear elastic constitutive relationship, geometric equation, equilibrium equation and boundary conditions satisfied by the displacement field of the heart model are given, and the true displacement of the finite element simulation results under the same conditions is used as a constraint to train a PINN network that solves two material parameters in the linear elastic Hooke's law of the left ventricle.

2. Problem Definition

The model input is the coordinates \((x,y,z)\) of the mesh points before the left ventricle deformation, and the output is the displacement \((u,v,w)\) corresponding to the mesh points after the left ventricle diastolic deformation.

In this case, the heart is considered to be a linear elastic material, satisfying Hooke's law for linear elastic materials, i.e.:

\[ \begin{pmatrix} t_{xx} \\ t_{yy} \\ t_{zz} \\ t_{xy} \\ t_{xz} \\ t_{yz} \\ \end{pmatrix} = \begin{bmatrix} \frac{1}{E} & -\frac{\nu}{E} & -\frac{\nu}{E} & 0 & 0 & 0 \\ -\frac{\nu}{E} & \frac{1}{E} & -\frac{\nu}{E} & 0 & 0 & 0 \\ -\frac{\nu}{E} & -\frac{\nu}{E} & \frac{1}{E} & 0 & 0 & 0 \\ 0 & 0 & 0 & \frac{1}{G} & 0 & 0 \\ 0 & 0 & 0 & 0 & \frac{1}{G} & 0 \\ 0 & 0 & 0 & 0 & 0 & \frac{1}{G} \\ \end{bmatrix} \begin{pmatrix} \varepsilon _{xx} \\ \varepsilon _{yy} \\ \varepsilon _{zz} \\ \varepsilon _{xy} \\ \varepsilon _{xz} \\ \varepsilon _{yz} \\ \end{pmatrix} \]

Where \(G=\frac{E}{2(1+\nu)}\), \(E=9kpa\) and \(\nu=0.45\) are two independent constants, \(\sigma_{xx}\), \(\sigma_{yy}\), \(\sigma_{zz}\), \(\sigma_{xy}\), \(\sigma_{xz}\), \(\sigma_{yz}\) are the stresses in the three dimensions of the corresponding coordinate point, and its relationship with displacement is:

\[ \begin{pmatrix} \sigma_{xx} = \frac{\partial u}{\partial x} \\ \sigma_{yy} = \frac{\partial v}{\partial y} \\ \sigma_{zz} = \frac{\partial w}{\partial z} \\ \sigma_{xy} = \frac{1}{2}(\frac{\partial u}{\partial y}+\frac{\partial v}{\partial x}) \\ \sigma_{xz} = \frac{1}{2}(\frac{\partial u}{\partial z}+\frac{\partial w}{\partial x}) \\ \sigma_{yz} = \frac{1}{2}(\frac{\partial v}{\partial z}+\frac{\partial w}{\partial y}) \\ \end{pmatrix} \]

In this case, it is considered that the passive mechanics of the left ventricle in diastole are quasi-static, so it is considered that this case has the following boundary conditions:

  1. In the entire geometric computational domain, it is necessary to satisfy \(\nabla t=0\), i.e.:
\[ \begin{pmatrix} \frac{\partial t_{xx}}{\partial x}+\frac{\partial t_{xy}}{\partial y}+\frac{\partial t_{xz}}{\partial z}=0 \\ \frac{\partial t_{xy}}{\partial x}+\frac{\partial t_{yy}}{\partial y}+\frac{\partial t_{yz}}{\partial z}=0 \\ \frac{\partial t_{xz}}{\partial x}+\frac{\partial t_{yz}}{\partial y}+\frac{\partial t_{zz}}{\partial z}=0 \\ \end{pmatrix} \]
  1. On the endocardial surface, it is necessary to satisfy \(tn=-P_{endo}n\), where \(n\) is the unit normal direction of the endocardial surface, and \(P_{endo}=1.064kpa(8mmHg)\) represents the left ventricular cavity pressure;

  2. On the epicardium, it is necessary to satisfy \(P_{epi}=0\);

  3. On the basal plane, it is necessary to satisfy \(u_{x,y,z}=0\), i.e., \(u,v,w=0\)

boundary conditions

Schematic diagram of boundary conditions

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.1 Stress Analysis Solution

3.1.1 Model Construction

As mentioned above, each known coordinate point \((x, y, z)\) has a corresponding strain \((u, v, w)\) to be solved. Use a model to predict:

\(u, v, w = f(x,y,z)\)

Expressed in PaddleScience code as follows:

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

3.1.2 Optimizer Construction

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

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

3.1.3 Equation Construction

Construct equations in the equation.py file. The corresponding equation instantiation code is as follows:

# set equation
equation = {"Hooke": eq_func.Hooke(E=cfg.E, nu=cfg.nu, P=cfg.P, dim=3)}

3.1.4 Computational Domain Construction

The geometric area of this problem is specified by the stl file. Download and extract it to the ./stl/ folder according to the "Model Training Command" at the beginning of this document.

Note

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

Then, through the STL geometry class ppsci.geometry.Mesh built into PaddleScience, you can read and parse the geometry file, obtain the computational domain, and obtain the geometric structure boundary:

# set geometry
heart = ppsci.geometry.Mesh(cfg.GEOM_PATH)
base = ppsci.geometry.Mesh(cfg.BASE_PATH)
endo = ppsci.geometry.Mesh(cfg.ENDO_PATH)
epi = ppsci.geometry.Mesh(cfg.EPI_PATH)
geom = {"geo": heart, "base": base, "endo": endo, "epi": epi}
# set bounds
BOUNDS_X, BOUNDS_Y, BOUNDS_Z = heart.bounds

3.1.5 Hyperparameter Setting

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

  by_epoch: false
batch_size:

3.1.6 Constraint Construction

This problem involves 4 constraints in 2. Problem Definition. Before constructing specific constraints, you can construct data reading configurations 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

Take InteriorConstraint acting on structural interior points as an example, the code is as follows:

interior = ppsci.constraint.InteriorConstraint(
    equation["Hooke"].equations,
    {"hooke_x": 0, "hooke_y": 0, "hooke_z": 0},
    geom["geo"],
    {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.interior},
    ppsci.loss.MSELoss("mean"),
    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=cfg.TRAIN.weight.interior,
    name="INTERIOR",
)
data = ppsci.constraint.SupervisedConstraint(

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

The second parameter is the target value of the constraint variable. In this problem, hooke_x, hooke_y, hooke_z are optimized to 0;

The third parameter is the computational domain where the constraint equation acts. Here, fill in geom["geo"] instantiated in the 3.1.4 Computational Domain Construction section;

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

  bc_epi: { "traction": 0.2 }
  interior: { "hooke_x": 0.2, "hooke_y": 0.2, "hooke_z": 0.2 }
save_freq: 20
eval_freq: 20
eval_during_train: true

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

The sixth parameter is geometric point filtering. It is necessary to filter the points sampled on geo. Here, pass in a lambda filtering function, which accepts the tensor x, y, z composed of point sets and returns a boolean tensor indicating whether each point meets the filtering conditions. If not, it is False, and if it meets, it is True. Because 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 when calculating the loss;

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

3.1.6.2 Boundary Constraint

Refer to 2. Problem Definition for constraints on the endocardium, epicardium, and basal plane respectively:

# set constraint
bc_base = 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["base"],
    {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.bc_base},
    ppsci.loss.MSELoss("mean"),
    weight_dict=cfg.TRAIN.weight.bc_base,
    name="BC_BASE",
)
bc_endo = ppsci.constraint.BoundaryConstraint(
    equation["Hooke"].equations,
    {"traction": -cfg.P},
    geom["endo"],
    {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.bc_endo},
    ppsci.loss.MSELoss("mean"),
    weight_dict=cfg.TRAIN.weight.bc_endo,
    name="BC_ENDO",
)
bc_epi = ppsci.constraint.BoundaryConstraint(
    equation["Hooke"].equations,
    {"traction": 0},
    geom["epi"],
    {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.bc_epi},
    ppsci.loss.MSELoss("mean"),
    weight_dict=cfg.TRAIN.weight.bc_epi,
    name="BC_EPI",
)

3.1.7 Validator Construction

Usually during the training process, the training status of the current model is evaluated using the validation set (test set) at a certain epoch interval. The data of the validation set comes from an external csv file, so first use the ppsci.utils.reader module to read the validation point set from the csv file:

eval_data_dict = reader.load_csv_file(
    cfg.EVAL_CSV_PATH,
    ("x", "y", "z", "u", "v", "w"),
    {
        "x": "x",
        "y": "y",
        "z": "z",
        "u": "u",
        "v": "v",
        "w": "w",
    },
)

Then convert it to a dictionary and perform dimensionless and normalization, and then wrap it into a dictionary and pass it to ppsci.validate.SupervisedValidator together with eval_dataloader_cfg (validation set dataloader configuration, constructed similarly to train_dataloader_cfg) to construct the validator.

input_dict = {
    "x": eval_data_dict["x"],
    "y": eval_data_dict["y"],
    "z": eval_data_dict["z"],
}

label_dict = {
    "u": eval_data_dict["u"],
    "v": eval_data_dict["v"],
    "w": eval_data_dict["w"],
}
eval_dataloader_cfg = {
    "dataset": {
        "name": "NamedArrayDataset",
        "input": input_dict,
        "label": label_dict,
    },
    "num_workers": 1,
}
sup_validator = ppsci.validate.SupervisedValidator(
    {**eval_dataloader_cfg, "batch_size": cfg.EVAL.batch_size},
    ppsci.loss.MSELoss("mean"),
    {
        "u": lambda out: out["u"],
        "v": lambda out: out["v"],
        "w": lambda out: out["w"],
    },
    metric={"L2Rel": ppsci.metric.L2Rel()},
    name="ref_u_v_w",
)
validator = {sup_validator.name: sup_validator}

3.1.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 in this article is the input dictionary input_dict prepared in the validator construction, and the output data is the corresponding 3 predicted physical quantities. Therefore, you only need to save the evaluation output data as a vtu format file, and finally open it with visualization software to view it. The code is as follows:

# set visualizer(optional)
visualizer = {
    "visualize_u_v_w": ppsci.visualize.VisualizerVtu(
        input_dict,
        {
            "u": lambda out: out["u"],
            "v": lambda out: out["v"],
            "w": lambda out: out["w"],
        },
        batch_size=cfg.EVAL.batch_size,
        prefix="result_u_v_w",
    ),
}

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,
    constraint,
    optimizer=optimizer,
    equation=equation,
    validator=validator,
    visualizer=visualizer,
    cfg=cfg,
)

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

Calling ppsci.solver.Solver.plot_loss_history after training can plot the loss during training:

# plot loss
solver.plot_loss_history(by_epoch=True)

3.1.9 Model Evaluation and Visualization

After training is completed or the pre-trained model is downloaded, model evaluation and visualization are performed through the "Model Evaluation Command" at the beginning of this document.

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

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

3.2 Parameter Inversion Solution

3.2.1 Equation Construction

This case attempts to model and implement the complex hyperelastic constitutive relationship of heart soft tissue under the PINN framework. In the case where the preset equation parameter \(E\) is unknown, it attempts to train to obtain the value of the unknown parameter through partial data. The case still uses the equation constructed in the equation.py file, but the unknown parameter needs to be set as a learnable variable and passed to the equation:

# set equation
E = paddle.create_parameter(
    shape=[],
    dtype=paddle.get_default_dtype(),
    default_initializer=initializer.Constant(0.0),
)
equation = {"Hooke": eq_func.Hooke(E=E, nu=cfg.nu, P=cfg.P, dim=3)}

3.2.2 Model Construction

The model settings are the same as the forward problem:

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

3.2.3 Optimizer Construction

The training process will call the optimizer to update model parameters. Here, the more commonly used Adam optimizer is selected, and combined with the ExponentialDecay learning rate adjustment strategy commonly used in machine learning. When setting the optimizer, the learnable parameters in the equation need to be passed to the optimizer so that the parameters participate in optimization:

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

3.2.4 Other Settings

Other settings for this problem are similar to the forward problem and will not be repeated here.

4. Complete Code

forward.py
# Copyright (c) 2024 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 equation as eq_func
import hydra
from omegaconf import DictConfig

import ppsci
from ppsci.utils import reader


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

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

    # set equation
    equation = {"Hooke": eq_func.Hooke(E=cfg.E, nu=cfg.nu, P=cfg.P, dim=3)}

    # set geometry
    heart = ppsci.geometry.Mesh(cfg.GEOM_PATH)
    base = ppsci.geometry.Mesh(cfg.BASE_PATH)
    endo = ppsci.geometry.Mesh(cfg.ENDO_PATH)
    epi = ppsci.geometry.Mesh(cfg.EPI_PATH)
    geom = {"geo": heart, "base": base, "endo": endo, "epi": epi}
    # set bounds
    BOUNDS_X, BOUNDS_Y, BOUNDS_Z = heart.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
    bc_base = 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["base"],
        {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.bc_base},
        ppsci.loss.MSELoss("mean"),
        weight_dict=cfg.TRAIN.weight.bc_base,
        name="BC_BASE",
    )
    bc_endo = ppsci.constraint.BoundaryConstraint(
        equation["Hooke"].equations,
        {"traction": -cfg.P},
        geom["endo"],
        {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.bc_endo},
        ppsci.loss.MSELoss("mean"),
        weight_dict=cfg.TRAIN.weight.bc_endo,
        name="BC_ENDO",
    )
    bc_epi = ppsci.constraint.BoundaryConstraint(
        equation["Hooke"].equations,
        {"traction": 0},
        geom["epi"],
        {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.bc_epi},
        ppsci.loss.MSELoss("mean"),
        weight_dict=cfg.TRAIN.weight.bc_epi,
        name="BC_EPI",
    )
    interior = ppsci.constraint.InteriorConstraint(
        equation["Hooke"].equations,
        {"hooke_x": 0, "hooke_y": 0, "hooke_z": 0},
        geom["geo"],
        {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.interior},
        ppsci.loss.MSELoss("mean"),
        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=cfg.TRAIN.weight.interior,
        name="INTERIOR",
    )
    data = ppsci.constraint.SupervisedConstraint(
        {
            "dataset": {
                "name": "IterableCSVDataset",
                "file_path": cfg.DATA_CSV_PATH,
                "input_keys": ("x", "y", "z"),
                "label_keys": ("u", "v", "w"),
            },
        },
        ppsci.loss.MSELoss("sum"),
        name="DATA",
    )

    # wrap constraints together
    constraint = {
        bc_base.name: bc_base,
        bc_endo.name: bc_endo,
        bc_epi.name: bc_epi,
        interior.name: interior,
        data.name: data,
    }

    # set validator
    eval_data_dict = reader.load_csv_file(
        cfg.EVAL_CSV_PATH,
        ("x", "y", "z", "u", "v", "w"),
        {
            "x": "x",
            "y": "y",
            "z": "z",
            "u": "u",
            "v": "v",
            "w": "w",
        },
    )

    input_dict = {
        "x": eval_data_dict["x"],
        "y": eval_data_dict["y"],
        "z": eval_data_dict["z"],
    }

    label_dict = {
        "u": eval_data_dict["u"],
        "v": eval_data_dict["v"],
        "w": eval_data_dict["w"],
    }
    eval_dataloader_cfg = {
        "dataset": {
            "name": "NamedArrayDataset",
            "input": input_dict,
            "label": label_dict,
        },
        "num_workers": 1,
    }
    sup_validator = ppsci.validate.SupervisedValidator(
        {**eval_dataloader_cfg, "batch_size": cfg.EVAL.batch_size},
        ppsci.loss.MSELoss("mean"),
        {
            "u": lambda out: out["u"],
            "v": lambda out: out["v"],
            "w": lambda out: out["w"],
        },
        metric={"L2Rel": ppsci.metric.L2Rel()},
        name="ref_u_v_w",
    )
    validator = {sup_validator.name: sup_validator}

    # set visualizer(optional)
    visualizer = {
        "visualize_u_v_w": ppsci.visualize.VisualizerVtu(
            input_dict,
            {
                "u": lambda out: out["u"],
                "v": lambda out: out["v"],
                "w": lambda out: out["w"],
            },
            batch_size=cfg.EVAL.batch_size,
            prefix="result_u_v_w",
        ),
    }

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

    # train
    solver.train()
    # eval
    solver.eval()
    # visualize prediction after finished training
    solver.visualize()
    # plot loss
    solver.plot_loss_history(by_epoch=True)


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

    # set validator
    eval_data_dict = reader.load_csv_file(
        cfg.EVAL_CSV_PATH,
        ("x", "y", "z", "u", "v", "w"),
        {
            "x": "x",
            "y": "y",
            "z": "z",
            "u": "u",
            "v": "v",
            "w": "w",
        },
    )

    input_dict = {
        "x": eval_data_dict["x"],
        "y": eval_data_dict["y"],
        "z": eval_data_dict["z"],
    }

    label_dict = {
        "u": eval_data_dict["u"],
        "v": eval_data_dict["v"],
        "w": eval_data_dict["w"],
    }
    eval_dataloader_cfg = {
        "dataset": {
            "name": "NamedArrayDataset",
            "input": input_dict,
            "label": label_dict,
        },
        "num_workers": 1,
    }
    sup_validator = ppsci.validate.SupervisedValidator(
        {**eval_dataloader_cfg, "batch_size": cfg.EVAL.batch_size},
        ppsci.loss.MSELoss("mean"),
        {
            "u": lambda out: out["u"],
            "v": lambda out: out["v"],
            "w": lambda out: out["w"],
        },
        metric={"L2Rel": ppsci.metric.L2Rel()},
        name="ref_u_v_w",
    )
    validator = {sup_validator.name: sup_validator}

    # set visualizer
    visualizer = {
        "visualize_u_v_w": ppsci.visualize.VisualizerVtu(
            input_dict,
            {
                "u": lambda out: out["u"],
                "v": lambda out: out["v"],
                "w": lambda out: out["w"],
            },
            batch_size=cfg.EVAL.batch_size,
            prefix="result_u_v_w",
        ),
    }

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

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


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


if __name__ == "__main__":
    main()
inverse.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# Copyright (c) 2024 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.

from os import path as osp

import equation as eq_func
import hydra
import numpy as np
import paddle
from omegaconf import DictConfig
from paddle.nn import initializer

import ppsci
from ppsci.metric import L2Rel
from ppsci.utils import logger
from ppsci.utils import reader


def train(cfg: DictConfig):
    # set equation
    E = paddle.create_parameter(
        shape=[],
        dtype=paddle.get_default_dtype(),
        default_initializer=initializer.Constant(0.0),
    )
    equation = {"Hooke": eq_func.Hooke(E=E, nu=cfg.nu, P=cfg.P, dim=3)}

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

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

    # set geometry
    heart = ppsci.geometry.Mesh(cfg.GEOM_PATH)
    base = ppsci.geometry.Mesh(cfg.BASE_PATH)
    endo = ppsci.geometry.Mesh(cfg.ENDO_PATH)
    epi = ppsci.geometry.Mesh(cfg.EPI_PATH)
    geom = {"geo": heart, "base": base, "endo": endo, "epi": epi}
    # set bounds
    BOUNDS_X, BOUNDS_Y, BOUNDS_Z = heart.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
    bc_base = 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["base"],
        {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.bc_base},
        ppsci.loss.MSELoss("sum"),
        weight_dict=cfg.TRAIN.weight.bc_base,
        name="BC_BASE",
    )
    bc_endo = ppsci.constraint.BoundaryConstraint(
        equation["Hooke"].equations,
        {"traction_x": -cfg.P, "traction_y": -cfg.P, "traction_z": -cfg.P},
        geom["endo"],
        {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.bc_endo},
        ppsci.loss.MSELoss("sum"),
        weight_dict=cfg.TRAIN.weight.bc_endo,
        name="BC_ENDO",
    )
    bc_epi = ppsci.constraint.BoundaryConstraint(
        equation["Hooke"].equations,
        {"traction_x": 0, "traction_y": 0, "traction_z": 0},
        geom["epi"],
        {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.bc_epi},
        ppsci.loss.MSELoss("sum"),
        weight_dict=cfg.TRAIN.weight.bc_endo,
        name="BC_EPI",
    )
    interior = ppsci.constraint.InteriorConstraint(
        equation["Hooke"].equations,
        {"hooke_x": 0, "hooke_y": 0, "hooke_z": 0},
        geom["geo"],
        {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.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=cfg.TRAIN.weight.interior,
        name="INTERIOR",
    )
    data = ppsci.constraint.SupervisedConstraint(
        {
            "dataset": {
                "name": "IterableCSVDataset",
                "file_path": cfg.DATA_PATH,
                "input_keys": ("x", "y", "z"),
                "label_keys": ("u", "v", "w"),
            },
        },
        ppsci.loss.MSELoss("sum"),
        name="DATA",
    )

    # wrap constraints together
    constraint = {
        bc_base.name: bc_base,
        bc_endo.name: bc_endo,
        bc_epi.name: bc_epi,
        interior.name: interior,
        data.name: data,
    }

    # set validator
    eval_data_dict = reader.load_csv_file(
        cfg.DATA_PATH,
        ("x", "y", "z", "u", "v", "w"),
        {
            "x": "x",
            "y": "y",
            "z": "z",
            "u": "u",
            "v": "v",
            "w": "w",
        },
    )
    input_dict = {
        "x": eval_data_dict["x"],
        "y": eval_data_dict["y"],
        "z": eval_data_dict["z"],
    }
    label_dict = {
        "u": eval_data_dict["u"],
        "v": eval_data_dict["v"],
        "w": eval_data_dict["w"],
    }
    eval_dataloader_cfg = {
        "dataset": {
            "name": "NamedArrayDataset",
            "input": input_dict,
            "label": label_dict,
        },
        "num_workers": 1,
    }
    sup_validator = ppsci.validate.SupervisedValidator(
        {**eval_dataloader_cfg, "batch_size": cfg.EVAL.batch_size},
        ppsci.loss.MSELoss("mean"),
        {
            "u": lambda out: out["u"],
            "v": lambda out: out["v"],
            "w": lambda out: out["w"],
        },
        metric={"L2Rel": ppsci.metric.L2Rel()},
        name="ref_u_v_w",
    )

    fake_input = np.full((1, 1), 1, dtype=np.float32)
    E_label = np.full((1, 1), cfg.E, dtype=np.float32)
    param_validator = ppsci.validate.SupervisedValidator(
        {
            "dataset": {
                "name": "NamedArrayDataset",
                "input": {
                    "x": fake_input,
                    "y": fake_input,
                    "z": fake_input,
                },
                "label": {"E": E_label},
            },
            "batch_size": 1,
            "num_workers": 1,
        },
        ppsci.loss.MSELoss("mean"),
        {
            "E": lambda out: E.reshape([1, 1]),
        },
        metric={"L2Rel": ppsci.metric.L2Rel()},
        name="param_E",
    )

    validator = {
        sup_validator.name: sup_validator,
        param_validator.name: param_validator,
    }

    # set visualizer(optional)
    visualizer = {
        "visualize_u_v_w": ppsci.visualize.VisualizerVtu(
            input_dict,
            {
                "u": lambda out: out["u"],
                "v": lambda out: out["v"],
                "w": lambda out: out["w"],
            },
            batch_size=cfg.EVAL.batch_size,
            prefix="result_u_v_w",
        ),
    }

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

    # train
    solver.train()
    # eval
    solver.eval()
    # visualize prediction after finished training
    solver.visualize()
    # plot loss
    solver.plot_loss_history(by_epoch=True)

    # save parameter E separately
    paddle.save({"E": E}, osp.join(cfg.output_dir, "param_E.pdparams"))


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

    # set geometry
    heart = ppsci.geometry.Mesh(cfg.GEOM_PATH)
    base = ppsci.geometry.Mesh(cfg.BASE_PATH)
    endo = ppsci.geometry.Mesh(cfg.ENDO_PATH)
    epi = ppsci.geometry.Mesh(cfg.EPI_PATH)
    # test = ppsci.geometry.Cuboid((0, 0, 0), (1, 1, 1))
    geom = {"geo": heart, "base": base, "endo": endo, "epi": epi}
    # set bounds
    BOUNDS_X, BOUNDS_Y, BOUNDS_Z = heart.bounds

    # set validator
    eval_data_dict = reader.load_csv_file(
        cfg.DATA_PATH,
        ("x", "y", "z", "u", "v", "w"),
        {
            "x": "x",
            "y": "y",
            "z": "z",
            "u": "u",
            "v": "v",
            "w": "w",
        },
    )
    input_dict = {
        "x": eval_data_dict["x"],
        "y": eval_data_dict["y"],
        "z": eval_data_dict["z"],
    }
    label_dict = {
        "u": eval_data_dict["u"],
        "v": eval_data_dict["v"],
        "w": eval_data_dict["w"],
    }
    eval_dataloader_cfg = {
        "dataset": {
            "name": "NamedArrayDataset",
            "input": input_dict,
            "label": label_dict,
        },
        "num_workers": 1,
    }
    sup_validator = ppsci.validate.SupervisedValidator(
        {**eval_dataloader_cfg, "batch_size": cfg.EVAL.batch_size},
        ppsci.loss.MSELoss("mean"),
        {
            "u": lambda out: out["u"],
            "v": lambda out: out["v"],
            "w": lambda out: out["w"],
        },
        metric={"L2Rel": ppsci.metric.L2Rel()},
        name="ref_u_v_w",
    )
    validator = {sup_validator.name: sup_validator}

    # set visualizer(optional)
    # add inferencer data endo
    samples_endo = geom["endo"].sample_boundary(
        cfg.EVAL.num_vis,
        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_endo = {
        "x": samples_endo["x"],
        "y": samples_endo["y"],
        "z": samples_endo["z"],
    }
    visualizer_endo = ppsci.visualize.VisualizerVtu(
        pred_input_dict_endo,
        {
            "u": lambda out: out["u"],
            "v": lambda out: out["v"],
            "w": lambda out: out["w"],
        },
        prefix="vtu_u_v_w_endo",
    )
    # add inferencer data epi
    samples_epi = geom["epi"].sample_boundary(
        cfg.EVAL.num_vis,
        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_epi = {
        "x": samples_epi["x"],
        "y": samples_epi["y"],
        "z": samples_epi["z"],
    }
    visualizer_epi = ppsci.visualize.VisualizerVtu(
        pred_input_dict_epi,
        {
            "u": lambda out: out["u"],
            "v": lambda out: out["v"],
            "w": lambda out: out["w"],
        },
        prefix="vtu_u_v_w_epi",
    )
    # add inferencer data
    samples_geom = geom["geo"].sample_interior(
        cfg.EVAL.num_vis,
        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_geom = {
        "x": samples_geom["x"],
        "y": samples_geom["y"],
        "z": samples_geom["z"],
    }
    visualizer_geom = ppsci.visualize.VisualizerVtu(
        pred_input_dict_geom,
        {
            "u": lambda out: out["u"],
            "v": lambda out: out["v"],
            "w": lambda out: out["w"],
        },
        prefix="vtu_u_v_w_geom",
    )

    # wrap visualizers together
    visualizer = {
        "vis_eval_endo": visualizer_endo,
        "visualizer_epi": visualizer_epi,
        "vis_eval_geom": visualizer_geom,
    }

    # initialize solver
    solver = ppsci.solver.Solver(
        model=model,
        validator=validator,
        visualizer=visualizer,
        cfg=cfg,
    )
    # evaluate
    solver.eval()
    # visualize prediction after finished training
    solver.visualize()

    # evaluate E
    E_truth = paddle.to_tensor(cfg.E, dtype=paddle.get_default_dtype()).reshape([1, 1])
    E_pred = paddle.load(cfg.EVAL.param_E_path)["E"].reshape([1, 1])
    l2_error = L2Rel()({"E": E_pred}, {"E": E_truth})["E"]
    logger.info(
        f"E_truth: {cfg.E}, E_pred: {float(E_pred)}, L2_Error: {float(l2_error)}"
    )


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


if __name__ == "__main__":
    main()
equation.py
# Copyright (c) 2024 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.

from __future__ import annotations

from typing import Optional
from typing import Tuple
from typing import Union

import paddle
import sympy as sp

from ppsci.equation.pde import base


class Hooke(base.PDE):
    r"""equations for umbrella opening force.
    Use either (E, nu) or (lambda_, mu) to define the material properties.

    $$
    \begin{pmatrix}
        t_{xx} \\ t_{yy} \\ t_{zz} \\ t_{xy} \\ t_{xz} \\ t_{yz} \\
    \end{pmatrix}
    =
    \begin{bmatrix}
        \frac{1}{E} & -\frac{\nu}{E} & -\frac{\nu}{E} & 0 & 0 & 0 \\
        -\frac{\nu}{E} & \frac{1}{E} & -\frac{\nu}{E} & 0 & 0 & 0 \\
        -\frac{\nu}{E} & -\frac{\nu}{E} & \frac{1}{E} & 0 & 0 & 0 \\
        0 & 0 & 0 & \frac{1}{G} & 0 & 0 \\
        0 & 0 & 0 & 0 & \frac{1}{G} & 0 \\
        0 & 0 & 0 & 0 & 0 & \frac{1}{G}  \\
    \end{bmatrix}
    \begin{pmatrix}
        \varepsilon _{xx} \\ \varepsilon _{yy} \\ \varepsilon _{zz} \\ \varepsilon _{xy} \\ \varepsilon _{xz} \\ \varepsilon _{yz} \\
    \end{pmatrix}
    $$

    Args:
        E (paddle.base.framework.EagerParamBase): The Young's modulus. Learnable parameter.
        nu (Union[float, str]): The Poisson's ratio.
        P (Union[float, str]): Left ventricular cavity pressure.
        dim (int, optional): Dimension of the linear elasticity (2 or 3). Defaults to 3.
        time (bool, optional): Whether contains time data. Defaults to False.
        detach_keys (Optional[Tuple[str, ...]]): Keys used for detach during computing.
            Defaults to None.


    Examples:
        >>> import ppsci
        >>> E = paddle.create_parameter(
        ...     shape=[],
        ...     dtype=paddle.get_default_dtype(),
        ...     default_initializer=initializer.Constant(),
        ... )
        >>> pde = ppsci.equation.Hooke(
        ...     E=E, nu=cfg.nu, P=cfg.P, dim=3
        ... )
    """

    def __init__(
        self,
        E: Union[float, str, paddle.base.framework.EagerParamBase],
        nu: Union[float, str],
        P: Union[float, str],
        dim: int = 3,
        time: bool = False,
        detach_keys: Optional[Tuple[str, ...]] = None,
    ):
        super().__init__()
        self.detach_keys = detach_keys
        self.dim = dim
        self.time = time

        t, x, y, z = self.create_symbols("t x y z")
        normal_x, normal_y, normal_z = self.create_symbols("normal_x normal_y normal_z")
        invars = (x, y)
        if time:
            invars = (t,) + invars
        if self.dim == 3:
            invars += (z,)

        u = self.create_function("u", invars)
        v = self.create_function("v", invars)
        w = self.create_function("w", invars) if dim == 3 else sp.Number(0)

        if isinstance(nu, str):
            nu = self.create_function(nu, invars)
        if isinstance(P, str):
            P = self.create_function(P, invars)
        if isinstance(E, str):
            E = self.create_function(E, invars)
            self.E = E
        elif isinstance(E, paddle.base.framework.EagerParamBase):
            self.E = E
            self.learnable_parameters.append(self.E)
            E = self.create_symbols(self.E.name)

        self.nu = nu
        self.P = P

        # compute sigma
        sigma_xx = u.diff(x)
        sigma_yy = v.diff(y)
        sigma_zz = w.diff(z) if dim == 3 else sp.Number(0)
        sigma_xy = 0.5 * (u.diff(y) + v.diff(x))
        sigma_xz = 0.5 * (u.diff(z) + w.diff(x)) if dim == 3 else sp.Number(0)
        sigma_yz = 0.5 * (v.diff(z) + w.diff(y)) if dim == 3 else sp.Number(0)

        # compute stress tensor t
        G = E / (2 * (1 + nu))
        e = sigma_xx + sigma_yy + sigma_zz
        t_xx = 2 * G * (sigma_xx + nu / (1 - 2 * nu) * e)
        t_yy = 2 * G * (sigma_yy + nu / (1 - 2 * nu) * e)
        t_zz = 2 * G * (sigma_zz + nu / (1 - 2 * nu) * e)
        t_xy = 2 * sigma_xy * G
        t_xz = 2 * sigma_xz * G
        t_yz = 2 * sigma_yz * G

        # compute stress
        hooke_x = t_xx.diff(x) + t_xy.diff(y) + t_xz.diff(z)
        hooke_y = t_xy.diff(x) + t_yy.diff(y) + t_yz.diff(z)
        hooke_z = t_xz.diff(x) + t_yz.diff(y) + t_zz.diff(z)

        # compute traction splitly
        traction_x = t_xx * normal_x + t_xy * normal_y + t_xz * normal_z + P * normal_x
        traction_y = t_xy * normal_x + t_yy * normal_y + t_yz * normal_z + P * normal_y
        traction_z = t_xz * normal_x + t_yz * normal_y + t_zz * normal_z + P * normal_z

        # compute traction
        traction_x_ = t_xx * normal_x + t_xy * normal_y + t_xz * normal_z
        traction_y_ = t_xy * normal_x + t_yy * normal_y + t_yz * normal_z
        traction_z_ = t_xz * normal_x + t_yz * normal_y + t_zz * normal_z

        traction = (
            traction_x_ * normal_x + traction_y_ * normal_y + traction_z_ * normal_z
        )

        # add hooke equations
        self.add_equation("hooke_x", hooke_x)
        self.add_equation("hooke_y", hooke_y)
        if self.dim == 3:
            self.add_equation("hooke_z", hooke_z)

        # add traction equations
        self.add_equation("traction_x", traction_x)
        self.add_equation("traction_y", traction_y)
        if self.dim == 3:
            self.add_equation("traction_z", traction_z)

        # add combined traction equations
        self.add_equation("traction", traction)

        self._apply_detach()

5. Result Display

5.1 Stress Analysis Solution

The figure below shows the model prediction results of the strain \(u, v, w\) in 3 directions when the force direction is the positive x direction. The results are basically consistent with cognition.

forward_result.jpg

Left is predicted structural strain u; Middle is predicted structural strain v; Right is predicted structural strain w

5.2 Parameter Inversion Solution

The table below shows the model prediction results of the learnable equation parameter \(E\), with an error of about 5%.

data E
outs 9
label 9.44778