Skip to content

Aneurysm

# linux
wget -c https://paddle-org.bj.bcebos.com/paddlescience/datasets/aneurysm/aneurysm_dataset.tar
# windows
# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/aneurysm/aneurysm_dataset.tar -o aneurysm_dataset.tar
# unzip it
tar -xvf aneurysm_dataset.tar
python aneurysm.py
# linux
wget -c https://paddle-org.bj.bcebos.com/paddlescience/datasets/aneurysm/aneurysm_dataset.tar
# windows
# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/aneurysm/aneurysm_dataset.tar -o aneurysm_dataset.tar
# unzip it
tar -xvf aneurysm_dataset.tar
python aneurysm.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/aneurysm/aneurysm_pretrained.pdparams
python aneurysm.py mode=export
# linux
wget -c https://paddle-org.bj.bcebos.com/paddlescience/datasets/aneurysm/aneurysm_dataset.tar
# windows
# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/aneurysm/aneurysm_dataset.tar -o aneurysm_dataset.tar
# unzip it
tar -xvf aneurysm_dataset.tar
python aneurysm.py mode=infer
Pretrained Model Metrics
aneurysm_pretrained.pdparams loss(ref_u_v_w_p): 0.01488
MSE.p(ref_u_v_w_p): 0.01412
MSE.u(ref_u_v_w_p): 0.00021
MSE.v(ref_u_v_w_p): 0.00024
MSE.w(ref_u_v_w_p): 0.00032

1. Background Introduction

Deep learning methods can be used to deal with intracranial aneurysm problems, including physics-informed deep learning methods. This method can be used for pressure modeling of intracranial aneurysms to predict and evaluate the risk of intracranial aneurysm rupture.

Targeting the following intracranial aneurysm geometric model, this case applies appropriate physical equation constraints inside and on the boundary through deep learning, and models the wall pressure in an unsupervised learning manner.

equation

2. Problem Definition

Assume that in the intracranial aneurysm model, at the inlet part, the velocity at the center point is 1.5 and gradually decreases towards the surroundings; in the outlet area, the pressure is constantly 0; there is no slip on the boundary, and the velocity is 0; inside the blood vessel, it conforms to the motion law of N-S equation, the average flow rate in the middle section is negative (inflow), and the average flow rate in the outlet section is positive (outflow).

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 Model Construction

In the aneurysm problem, each known coordinate point \((x, y, z)\) has corresponding unknown quantities \((u, v, w, p)\) (velocity and pressure) to be solved. Here, a relatively simple MLP (Multilayer Perceptron) is used to represent the mapping function \(f: \mathbb{R}^3 \to \mathbb{R}^4\) from \((x, y, z)\) to \((u, v, w, p)\), namely:

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

In the above formula, \(f\) is the MLP model itself, expressed in PaddleScience code as follows

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

In order to access the value of specific variables accurately and quickly during calculation, the input variable name of the network model is specified as ("x", "y", "z") and the output variable name is ("u", "v", "w", "p"), these names are consistent with the subsequent code.

Then by specifying the number of layers and neurons of MLP, a neural network model model with 6 hidden layers and 512 neurons per layer is instantiated, using silu as the activation function, and using WeightNorm weight normalization.

3.2 Equation Construction

The intracranial aneurysm model involves 2 equations, one is the fluid N-S equation, and the other is the flow calculation equation, so PaddleScience's built-in NavierStokes and NormalDotVec can be used.

# set equation
equation = {
    "NavierStokes": ppsci.equation.NavierStokes(
        cfg.NU * cfg.SCALE, cfg.RHO, cfg.DIM, False
    ),
    "NormalDotVec": ppsci.equation.NormalDotVec(("u", "v", "w")),
}

3.3 Computational Domain Construction

The geometric area of this problem is specified by the stl file. Follow the command below to download and unzip it to the aneurysm/ folder.

Note: The stl file and test set data (generated using OpenFOAM) in the dataset are from Aneurysm - NVIDIA Modulus.

# linux
wget -c https://paddle-org.bj.bcebos.com/paddlescience/datasets/aneurysm/aneurysm_dataset.tar

# windows
# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/aneurysm/aneurysm_dataset.tar -o aneurysm_dataset.tar

# unzip it
tar -xvf aneurysm_dataset.tar

After unzipping, the aneurysm/stl folder stores the stl geometric files required for computational domain construction.

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 Mesh to read and parse these geometric files, and combine each computational domain through Boolean operations. The code is as follows:

# set geometry
inlet_geo = ppsci.geometry.Mesh(cfg.INLET_STL_PATH)
outlet_geo = ppsci.geometry.Mesh(cfg.OUTLET_STL_PATH)
noslip_geo = ppsci.geometry.Mesh(cfg.NOSLIP_STL_PATH)
integral_geo = ppsci.geometry.Mesh(cfg.INTEGRAL_STL_PATH)
interior_geo = ppsci.geometry.Mesh(cfg.INTERIOR_STL_PATH)

After that, the geometric domain can be scaled and translated to scale the coordinate range of input data and promote model training convergence.

# normalize meshes
inlet_geo = inlet_geo.translate(-np.array(cfg.CENTER)).scale(cfg.SCALE)
outlet_geo = outlet_geo.translate(-np.array(cfg.CENTER)).scale(cfg.SCALE)
noslip_geo = noslip_geo.translate(-np.array(cfg.CENTER)).scale(cfg.SCALE)
integral_geo = integral_geo.translate(-np.array(cfg.CENTER)).scale(cfg.SCALE)
interior_geo = interior_geo.translate(-np.array(cfg.CENTER)).scale(cfg.SCALE)
geom = {
    "inlet_geo": inlet_geo,
    "outlet_geo": outlet_geo,
    "noslip_geo": noslip_geo,
    "integral_geo": integral_geo,
    "interior_geo": interior_geo,
}

3.4 Constraint Construction

This case involves 6 constraints. 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.4.1 Interior Point Constraint

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

pde = ppsci.constraint.InteriorConstraint(
    equation["NavierStokes"].equations,
    {"continuity": 0, "momentum_x": 0, "momentum_y": 0, "momentum_z": 0},
    geom["interior_geo"],
    {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.pde},
    ppsci.loss.MSELoss("sum"),
    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["NavierStokes"].equations instantiated in the 3.2 Equation Construction chapter;

The second parameter is the target value of the constraint variable. In this problem, it is hoped that the four values related to the N-S equation continuity, momentum_x, momentum_y, momentum_z are all optimized to 0;

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

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

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 the name of the constraint condition. Each constraint condition needs to be named to facilitate subsequent indexing. Here it is named "interior".

3.4.2 Boundary Constraint

Next, constraints need to be imposed on the three surfaces of vascular inlet, outlet, and vessel wall, including inlet velocity constraint, outlet pressure constraint, and vessel wall no-slip constraint. In the bc_inlet constraint, the flow velocity at the inlet satisfies a quadratic parabolic decay from the center point to the surroundings. Here, a parabolic function is used to represent the velocity decay as it moves away from the center of the circle, and then it is used as the value of the second parameter (dictionary) of BoundaryConstraint.

def _compute_parabola(_in):
    centered_x = _in["x"] - cfg.INLET_CENTER[0]
    centered_y = _in["y"] - cfg.INLET_CENTER[1]
    centered_z = _in["z"] - cfg.INLET_CENTER[2]
    distance = np.sqrt(centered_x**2 + centered_y**2 + centered_z**2)
    parabola = cfg.INLET_VEL * np.maximum((1 - (distance / INLET_RADIUS) ** 2), 0)
    return parabola

def inlet_u_ref_func(_in):
    return cfg.INLET_NORMAL[0] * _compute_parabola(_in)

def inlet_v_ref_func(_in):
    return cfg.INLET_NORMAL[1] * _compute_parabola(_in)

def inlet_w_ref_func(_in):
    return cfg.INLET_NORMAL[2] * _compute_parabola(_in)

bc_inlet = ppsci.constraint.BoundaryConstraint(
    {"u": lambda d: d["u"], "v": lambda d: d["v"], "w": lambda d: d["w"]},
    {"u": inlet_u_ref_func, "v": inlet_v_ref_func, "w": inlet_w_ref_func},
    geom["inlet_geo"],
    {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.bc_inlet},
    ppsci.loss.MSELoss("sum"),
    name="inlet",
)

The construction methods for vessel outlet and vessel wall no-slip constraints are similar, as shown below:

bc_outlet = ppsci.constraint.BoundaryConstraint(
    {"p": lambda d: d["p"]},
    {"p": 0},
    geom["outlet_geo"],
    {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.bc_outlet},
    ppsci.loss.MSELoss("sum"),
    name="outlet",
)
bc_noslip = 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["noslip_geo"],
    {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.bc_noslip},
    ppsci.loss.MSELoss("sum"),
    name="no_slip",
)

3.4.3 Integral Boundary Constraint

For a section below the vascular inlet and the outlet area (surface), additional inflow and outflow flow constraints need to be imposed. Since flow calculation involves specific areas, discrete integration needs to be used for calculation. These processes have been built into the IntegralConstraint constraint condition. As shown below:

igc_outlet = ppsci.constraint.IntegralConstraint(
    equation["NormalDotVec"].equations,
    {"normal_dot_vec": 2.54},
    geom["outlet_geo"],
    {
        **train_dataloader_cfg,
        "iters_per_epoch": cfg.TRAIN.iters_integral.igc_outlet,
        "batch_size": cfg.TRAIN.batch_size.igc_outlet,
        "integral_batch_size": cfg.TRAIN.integral_batch_size.igc_outlet,
    },
    ppsci.loss.IntegralLoss("sum"),
    weight_dict=cfg.TRAIN.weight.igc_outlet,
    name="igc_outlet",
)
igc_integral = ppsci.constraint.IntegralConstraint(
    equation["NormalDotVec"].equations,
    {"normal_dot_vec": -2.54},
    geom["integral_geo"],
    {
        **train_dataloader_cfg,
        "iters_per_epoch": cfg.TRAIN.iters_integral.igc_integral,
        "batch_size": cfg.TRAIN.batch_size.igc_integral,
        "integral_batch_size": cfg.TRAIN.integral_batch_size.igc_integral,
    },
    ppsci.loss.IntegralLoss("sum"),
    weight_dict=cfg.TRAIN.weight.igc_integral,
    name="igc_integral",
)

Corresponding flow calculation formula:

\[ flow_i = \sum_{i=1}^{M}{s_{i} (\mathbf{u_i} \cdot \mathbf{n_i})} \]

Where \(M\) represents the number of discrete integration points, \(s_i\) represents the (approximate) area of a certain point, \(\mathbf{u_i}\) represents the velocity vector of a certain point, and \(\mathbf{n_i}\) represents the outward normal vector of a certain point.

In addition to the common parameters described in the previous chapters, the integral_batch_size parameter is added here, which indicates the number of sampling points used for discrete integration. Here, 310 discrete points are used to approximate the integral calculation; at the same time, the loss function is specified as IntegralLoss, indicating that the final predicted value used to calculate the loss is approximated by multiple discrete points, and then the loss is calculated with the label value.

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

# wrap constraints together
constraint = {
    bc_inlet.name: bc_inlet,
    bc_outlet.name: bc_outlet,
    bc_noslip.name: bc_noslip,
    pde.name: pde,
    igc_outlet.name: igc_outlet,
    igc_integral.name: igc_integral,
}

3.5 Hyperparameter Setting

Next, you need to specify the number of training epochs and learning rate. Here, based on experimental experience, 1500 training epochs and an initial learning rate of 0.001 are used.

# training settings
TRAIN:
  epochs: 1500
  iters_per_epoch: 1000
  iters_integral:
    igc_outlet: 100
    igc_integral: 100
  save_freq: 20
  eval_during_train: true
  eval_freq: 20
  lr_scheduler:
    epochs: ${TRAIN.epochs}
    iters_per_epoch: ${TRAIN.iters_per_epoch}
    learning_rate: 0.001
    gamma: 0.95
    decay_steps: 15000
    by_epoch: false

3.6 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)

3.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, so ppsci.validate.GeometryValidator is used to construct the validator.

# set validator
eval_data_dict = reader.load_csv_file(
    cfg.EVAL_CSV_PATH,
    ("x", "y", "z", "u", "v", "w", "p"),
    {
        "x": "Points:0",
        "y": "Points:1",
        "z": "Points:2",
        "u": "U:0",
        "v": "U:1",
        "w": "U:2",
        "p": "p",
    },
)
input_dict = {
    "x": (eval_data_dict["x"] - cfg.CENTER[0]) * cfg.SCALE,
    "y": (eval_data_dict["y"] - cfg.CENTER[1]) * cfg.SCALE,
    "z": (eval_data_dict["z"] - cfg.CENTER[2]) * cfg.SCALE,
}
if "area" in input_dict.keys():
    input_dict["area"] *= cfg.SCALE ** (equation["NavierStokes"].dim)

label_dict = {
    "p": eval_data_dict["p"],
    "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,
    },
    "sampler": {"name": "BatchSampler"},
    "num_workers": 1,
}
sup_validator = ppsci.validate.SupervisedValidator(
    {**eval_dataloader_cfg, "batch_size": cfg.EVAL.batch_size},
    ppsci.loss.MSELoss("mean"),
    {
        "p": lambda out: out["p"],
        "u": lambda out: out["u"],
        "v": lambda out: out["v"],
        "w": lambda out: out["w"],
    },
    metric={"MSE": ppsci.metric.MSE()},
    name="ref_u_v_w_p",
)
validator = {sup_validator.name: sup_validator}

# set visualizer(optional)
visualizer = {
    "visualize_u_v_w_p": ppsci.visualize.VisualizerVtu(
        input_dict,
        {
            "p": lambda out: out["p"],
            "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_p",
    ),
}

3.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 output data in this article is a set of three-dimensional points in a region, so you only need to save the evaluated 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_p": ppsci.visualize.VisualizerVtu(
        input_dict,
        {
            "p": lambda out: out["p"],
            "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_p",
    ),
}

3.9 Model Training, Evaluation and Visualization

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

# initialize solver
solver = ppsci.solver.Solver(
    model,
    constraint,
    cfg.output_dir,
    optimizer,
    lr_scheduler,
    cfg.TRAIN.epochs,
    cfg.TRAIN.iters_per_epoch,
    save_freq=cfg.TRAIN.save_freq,
    log_freq=cfg.log_freq,
    eval_during_train=True,
    eval_freq=cfg.TRAIN.eval_freq,
    seed=cfg.seed,
    equation=equation,
    geom=geom,
    validator=validator,
    visualizer=visualizer,
    pretrained_model_path=cfg.TRAIN.pretrained_model_path,
    checkpoint_path=cfg.TRAIN.checkpoint_path,
    eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
)
# train model
solver.train()
# evaluate after finished training
solver.eval()
# visualize prediction after finished training
solver.visualize()

4. Complete Code

aneurysm.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
"""
Reference: https://docs.nvidia.com/deeplearning/modulus/modulus-v2209/user_guide/intermediate/adding_stl_files.html
"""

import hydra
import numpy as np
from omegaconf import DictConfig

import ppsci
from ppsci.utils import reader


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

    # set equation
    equation = {
        "NavierStokes": ppsci.equation.NavierStokes(
            cfg.NU * cfg.SCALE, cfg.RHO, cfg.DIM, False
        ),
        "NormalDotVec": ppsci.equation.NormalDotVec(("u", "v", "w")),
    }

    # set geometry
    inlet_geo = ppsci.geometry.Mesh(cfg.INLET_STL_PATH)
    outlet_geo = ppsci.geometry.Mesh(cfg.OUTLET_STL_PATH)
    noslip_geo = ppsci.geometry.Mesh(cfg.NOSLIP_STL_PATH)
    integral_geo = ppsci.geometry.Mesh(cfg.INTEGRAL_STL_PATH)
    interior_geo = ppsci.geometry.Mesh(cfg.INTERIOR_STL_PATH)

    # normalize meshes
    inlet_geo = inlet_geo.translate(-np.array(cfg.CENTER)).scale(cfg.SCALE)
    outlet_geo = outlet_geo.translate(-np.array(cfg.CENTER)).scale(cfg.SCALE)
    noslip_geo = noslip_geo.translate(-np.array(cfg.CENTER)).scale(cfg.SCALE)
    integral_geo = integral_geo.translate(-np.array(cfg.CENTER)).scale(cfg.SCALE)
    interior_geo = interior_geo.translate(-np.array(cfg.CENTER)).scale(cfg.SCALE)
    geom = {
        "inlet_geo": inlet_geo,
        "outlet_geo": outlet_geo,
        "noslip_geo": noslip_geo,
        "integral_geo": integral_geo,
        "interior_geo": interior_geo,
    }

    # 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
    INLET_AREA = 21.1284 * (cfg.SCALE**2)
    INLET_RADIUS = np.sqrt(INLET_AREA / np.pi)

    def _compute_parabola(_in):
        centered_x = _in["x"] - cfg.INLET_CENTER[0]
        centered_y = _in["y"] - cfg.INLET_CENTER[1]
        centered_z = _in["z"] - cfg.INLET_CENTER[2]
        distance = np.sqrt(centered_x**2 + centered_y**2 + centered_z**2)
        parabola = cfg.INLET_VEL * np.maximum((1 - (distance / INLET_RADIUS) ** 2), 0)
        return parabola

    def inlet_u_ref_func(_in):
        return cfg.INLET_NORMAL[0] * _compute_parabola(_in)

    def inlet_v_ref_func(_in):
        return cfg.INLET_NORMAL[1] * _compute_parabola(_in)

    def inlet_w_ref_func(_in):
        return cfg.INLET_NORMAL[2] * _compute_parabola(_in)

    bc_inlet = ppsci.constraint.BoundaryConstraint(
        {"u": lambda d: d["u"], "v": lambda d: d["v"], "w": lambda d: d["w"]},
        {"u": inlet_u_ref_func, "v": inlet_v_ref_func, "w": inlet_w_ref_func},
        geom["inlet_geo"],
        {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.bc_inlet},
        ppsci.loss.MSELoss("sum"),
        name="inlet",
    )
    bc_outlet = ppsci.constraint.BoundaryConstraint(
        {"p": lambda d: d["p"]},
        {"p": 0},
        geom["outlet_geo"],
        {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.bc_outlet},
        ppsci.loss.MSELoss("sum"),
        name="outlet",
    )
    bc_noslip = 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["noslip_geo"],
        {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.bc_noslip},
        ppsci.loss.MSELoss("sum"),
        name="no_slip",
    )
    pde = ppsci.constraint.InteriorConstraint(
        equation["NavierStokes"].equations,
        {"continuity": 0, "momentum_x": 0, "momentum_y": 0, "momentum_z": 0},
        geom["interior_geo"],
        {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.pde},
        ppsci.loss.MSELoss("sum"),
        name="interior",
    )
    igc_outlet = ppsci.constraint.IntegralConstraint(
        equation["NormalDotVec"].equations,
        {"normal_dot_vec": 2.54},
        geom["outlet_geo"],
        {
            **train_dataloader_cfg,
            "iters_per_epoch": cfg.TRAIN.iters_integral.igc_outlet,
            "batch_size": cfg.TRAIN.batch_size.igc_outlet,
            "integral_batch_size": cfg.TRAIN.integral_batch_size.igc_outlet,
        },
        ppsci.loss.IntegralLoss("sum"),
        weight_dict=cfg.TRAIN.weight.igc_outlet,
        name="igc_outlet",
    )
    igc_integral = ppsci.constraint.IntegralConstraint(
        equation["NormalDotVec"].equations,
        {"normal_dot_vec": -2.54},
        geom["integral_geo"],
        {
            **train_dataloader_cfg,
            "iters_per_epoch": cfg.TRAIN.iters_integral.igc_integral,
            "batch_size": cfg.TRAIN.batch_size.igc_integral,
            "integral_batch_size": cfg.TRAIN.integral_batch_size.igc_integral,
        },
        ppsci.loss.IntegralLoss("sum"),
        weight_dict=cfg.TRAIN.weight.igc_integral,
        name="igc_integral",
    )
    # wrap constraints together
    constraint = {
        bc_inlet.name: bc_inlet,
        bc_outlet.name: bc_outlet,
        bc_noslip.name: bc_noslip,
        pde.name: pde,
        igc_outlet.name: igc_outlet,
        igc_integral.name: igc_integral,
    }

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

    # set validator
    eval_data_dict = reader.load_csv_file(
        cfg.EVAL_CSV_PATH,
        ("x", "y", "z", "u", "v", "w", "p"),
        {
            "x": "Points:0",
            "y": "Points:1",
            "z": "Points:2",
            "u": "U:0",
            "v": "U:1",
            "w": "U:2",
            "p": "p",
        },
    )
    input_dict = {
        "x": (eval_data_dict["x"] - cfg.CENTER[0]) * cfg.SCALE,
        "y": (eval_data_dict["y"] - cfg.CENTER[1]) * cfg.SCALE,
        "z": (eval_data_dict["z"] - cfg.CENTER[2]) * cfg.SCALE,
    }
    if "area" in input_dict.keys():
        input_dict["area"] *= cfg.SCALE ** (equation["NavierStokes"].dim)

    label_dict = {
        "p": eval_data_dict["p"],
        "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,
        },
        "sampler": {"name": "BatchSampler"},
        "num_workers": 1,
    }
    sup_validator = ppsci.validate.SupervisedValidator(
        {**eval_dataloader_cfg, "batch_size": cfg.EVAL.batch_size},
        ppsci.loss.MSELoss("mean"),
        {
            "p": lambda out: out["p"],
            "u": lambda out: out["u"],
            "v": lambda out: out["v"],
            "w": lambda out: out["w"],
        },
        metric={"MSE": ppsci.metric.MSE()},
        name="ref_u_v_w_p",
    )
    validator = {sup_validator.name: sup_validator}

    # set visualizer(optional)
    visualizer = {
        "visualize_u_v_w_p": ppsci.visualize.VisualizerVtu(
            input_dict,
            {
                "p": lambda out: out["p"],
                "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_p",
        ),
    }

    # initialize solver
    solver = ppsci.solver.Solver(
        model,
        constraint,
        cfg.output_dir,
        optimizer,
        lr_scheduler,
        cfg.TRAIN.epochs,
        cfg.TRAIN.iters_per_epoch,
        save_freq=cfg.TRAIN.save_freq,
        log_freq=cfg.log_freq,
        eval_during_train=True,
        eval_freq=cfg.TRAIN.eval_freq,
        seed=cfg.seed,
        equation=equation,
        geom=geom,
        validator=validator,
        visualizer=visualizer,
        pretrained_model_path=cfg.TRAIN.pretrained_model_path,
        checkpoint_path=cfg.TRAIN.checkpoint_path,
        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
    )
    # train model
    solver.train()
    # evaluate after finished training
    solver.eval()
    # visualize prediction after finished training
    solver.visualize()


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

    # set validator
    eval_data_dict = reader.load_csv_file(
        cfg.EVAL_CSV_PATH,
        ("x", "y", "z", "u", "v", "w", "p"),
        {
            "x": "Points:0",
            "y": "Points:1",
            "z": "Points:2",
            "u": "U:0",
            "v": "U:1",
            "w": "U:2",
            "p": "p",
        },
    )
    input_dict = {
        "x": (eval_data_dict["x"] - cfg.CENTER[0]) * cfg.SCALE,
        "y": (eval_data_dict["y"] - cfg.CENTER[1]) * cfg.SCALE,
        "z": (eval_data_dict["z"] - cfg.CENTER[2]) * cfg.SCALE,
    }

    label_dict = {
        "p": eval_data_dict["p"],
        "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,
        },
        "sampler": {"name": "BatchSampler"},
        "num_workers": 1,
    }
    sup_validator = ppsci.validate.SupervisedValidator(
        {**eval_dataloader_cfg, "batch_size": cfg.EVAL.batch_size},
        ppsci.loss.MSELoss("mean"),
        {
            "p": lambda out: out["p"],
            "u": lambda out: out["u"],
            "v": lambda out: out["v"],
            "w": lambda out: out["w"],
        },
        metric={"MSE": ppsci.metric.MSE()},
        name="ref_u_v_w_p",
    )
    validator = {sup_validator.name: sup_validator}

    # set visualizer
    visualizer = {
        "visualize_u_v_w_p": ppsci.visualize.VisualizerVtu(
            input_dict,
            {
                "p": lambda out: out["p"],
                "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_p",
        ),
    }

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


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

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

    input_spec = [
        {key: InputSpec([None, 1], "float32", name=key) for key in model.input_keys},
    ]
    solver.export(input_spec, cfg.INFER.export_path, with_onnx=False)


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

    predictor = pinn_predictor.PINNPredictor(cfg)
    eval_data_dict = reader.load_csv_file(
        cfg.EVAL_CSV_PATH,
        ("x", "y", "z", "u", "v", "w", "p"),
        {
            "x": "Points:0",
            "y": "Points:1",
            "z": "Points:2",
            "u": "U:0",
            "v": "U:1",
            "w": "U:2",
            "p": "p",
        },
    )
    input_dict = {
        "x": (eval_data_dict["x"] - cfg.CENTER[0]) * cfg.SCALE,
        "y": (eval_data_dict["y"] - cfg.CENTER[1]) * cfg.SCALE,
        "z": (eval_data_dict["z"] - cfg.CENTER[2]) * cfg.SCALE,
    }
    output_dict = predictor.predict(input_dict, cfg.INFER.batch_size)

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

    ppsci.visualize.save_vtu_from_dict(
        "./aneurysm_pred.vtu",
        {**input_dict, **output_dict},
        input_dict.keys(),
        cfg.MODEL.output_keys,
    )


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

For the intracranial aneurysm test set (a total of 2,962,708 three-dimensional coordinate points), the model prediction results are as follows.

aneurysm_compare.jpg

Left: PaddleScience prediction result, Middle: OpenFOAM solver prediction result, Right: Difference between the two

It can be seen that for the wall pressure \(p(x,y,z)\), the prediction result of the model is basically consistent with the OpenFOAM result.

6. References