carmelog commited on
Commit
549d36c
·
0 Parent(s):

init: modeling Ahmed Body dataset with DoMINO using physicsnemo

Browse files
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM nvcr.io/nvidia/physicsnemo/physicsnemo:25.06
2
+
3
+ # Install system dependencies
4
+ RUN apt-get update && apt-get install -y --no-install-recommends \
5
+ wget \
6
+ git \
7
+ xvfb \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Upgrade pip
11
+ RUN python -m pip install --upgrade pip
12
+
13
+ # Create non-root user
14
+ RUN useradd -m -u 1001 user
15
+ USER user
16
+ ENV HOME=/home/user
17
+ RUN mkdir $HOME/.cache $HOME/.config $HOME/.local && chmod -R 777 $HOME
18
+ ENV PATH=/home/user/.local/bin:$PATH
19
+
20
+ # Set working directory
21
+ WORKDIR $HOME/app
22
+
23
+ # Create data directory with proper permissions
24
+ USER root
25
+ RUN mkdir /domino-demo && chown user:user /domino-demo && chmod 777 /domino-demo
26
+ # Add NGC CLI
27
+ RUN wget https://ngc.nvidia.com/downloads/ngccli_linux.zip
28
+ RUN unzip ngccli_linux.zip
29
+ RUN chmod u+x ngc-cli/ngc
30
+ RUN --mount=target=/root/on_startup.sh,source=on_startup.sh,readwrite \
31
+ bash /root/on_startup.sh
32
+ USER user
33
+
34
+ # Install Python dependencies
35
+ RUN pip install --no-cache-dir numpy pyvista vtk matplotlib tqdm numpy-stl torchinfo
36
+
37
+ COPY --chown=user login.html $HOME/app/
38
+ COPY --chown=user on_startup.sh $HOME/app/
39
+ COPY --chown=user README.md $HOME/app/
40
+ COPY --chown=user start_server.sh $HOME/app/
41
+ COPY --chown=user domino-data-preprocessing.ipynb /domino-demo/
42
+ COPY --chown=user domino-training-test.ipynb /domino-demo/
43
+ COPY --chown=user introduction.ipynb /domino-demo/
44
+ COPY --chown=user outputs /domino-demo/outputs
45
+ COPY --chown=user README.md /domino-demo/
46
+
47
+ RUN chmod -R 777 /domino-demo/
48
+
49
+ RUN chmod +x start_server.sh
50
+
51
+ COPY --chown=user login.html /usr/local/lib/python3.12/dist-packages/jupyter_server/templates/login.html
52
+
53
+ # Expose port
54
+ EXPOSE 7860
55
+
56
+ # Start the server
57
+ CMD ["./start_server.sh"]
README.md ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: DoMINO with Ahmed Body Dataset - Multi-Scale Neural Operator for CFD
3
+ emoji: 🟢
4
+ colorFrom: green
5
+ colorTo: gray
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ tags:
10
+ - physics
11
+ - cfd
12
+ - machine-learning
13
+ - neural-operators
14
+ - fluid-dynamics
15
+ - scientific-computing
16
+ ---
17
+
domino-data-preprocessing.ipynb ADDED
@@ -0,0 +1,676 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "## Physics NeMo External Aerodynamics DLI\n",
8
+ "\n",
9
+ "## Notebook 1 - Preprocessing Ahmed body *surface* dataset\n",
10
+ "\n",
11
+ "### Introduction\n",
12
+ "\n",
13
+ "For educational purposes, it's important to use lightweight datasets that are easy to store and manage, especially for users who may not have access to high-performance computing resources. One such dataset is the **Ahmed body surface data**, which includes 3D surface geometry, pressure and wall shear stress data for variations in the Ahmed body geometry and inlet Reynolds number. This dataset is a great choice because it is relatively small in size, yet provides valuable information about aerodynamic simulations. It’s ideal for teaching and experimentation, as it won’t demand excessive storage or computational power. *Note that this dataset was created by the NVIDIA PhysicsNeMo development team and differs from other similar datasets hosted on cloud platforms like AWS.*\n",
14
+ "\n",
15
+ "In this notebook, we will walk through the preprocessing steps required to prepare the **Ahmed body surface dataset** for training with the **DoMINO model**, to predict surface quantities like pressure and wall shear stress. The DoMINO model requires 3D surface geometry in **STL format**. The **STL (Stereolithography)** format is a widely used file format for representing 3D surface geometry in computer-aided design (CAD) applications. It describes the surface of a 3D object using a collection of triangular facets, making it a common format for 3D printing and computational geometry. So, as the first step, we’ll extract the 3D surface geometry from the **VTP files**. These files are commonly used in the **VTK (Visualization Toolkit)** format, which stores surface data as **PolyData**—a structure that represents points, lines, and polygons on the surface.\n",
16
+ "\n",
17
+ "To make the dataset more suitable for machine learning, we will convert the **VTP (VTK PolyData)** format into **NPY (NumPy)** format. This conversion makes the data easier to work with in machine learning workflows, as NumPy arrays are optimized for numerical operations, making computations faster and more efficient. After converting the data into NPY format, it can be stored on disk, where it will be readily accessible for training the model and further analysis.\n",
18
+ "\n",
19
+ "Key aspects of this training:\n",
20
+ "- Understanding the Ahmed body geometry and its aerodynamic characteristics\n",
21
+ "- Processing CFD mesh data for deep learning applications\n",
22
+ "\n",
23
+ "## Table of Contents\n",
24
+ "- [Step 1: Define Experiment Parameters and Dependencies](#step-1-define-experiment-parameters-and-dependencies)\n",
25
+ " - [Loading Required Libraries](#loading-required-libraries)\n",
26
+ " - [Dependencies](#dependencies)\n",
27
+ " - [Experiment Parameters and Variables](#experiment-parameters-and-variables)\n",
28
+ "- [Step 2: Convert VTK to STL Files](#step-2-convert-vtk-to-stl-files)\n",
29
+ " - [Understanding the Conversion Process](#understanding-the-conversion-process)\n",
30
+ " - [Key Components and Libraries](#key-components-and-libraries)\n",
31
+ " - [Important Considerations](#important-considerations)\n",
32
+ " - [Implementation Overview](#implementation-overview)\n",
33
+ "- [Step 3: Visualizing STL Meshes](#Step-3:-Visualizing-STL-Meshes)\n",
34
+ "- [Step 4: Convert CFD Results to NPY Format](#Step-4:-Convert-CFD-Results-to-NPY-Format) "
35
+ ]
36
+ },
37
+ {
38
+ "cell_type": "markdown",
39
+ "metadata": {},
40
+ "source": [
41
+ "### **Step 1: Define Experiment Parameters and Dependencies**\n",
42
+ "\n",
43
+ "The first step in training the DoMINO model on the Ahmed body surface dataset is to set up our experiment environment and define the necessary parameters. This includes specifying paths to our data, configuring training settings, and ensuring all required libraries are available.\n",
44
+ "\n",
45
+ "Key components we need to set up:\n",
46
+ "- Data paths for training and validation sets\n",
47
+ "- Model hyperparameters and training configurations\n",
48
+ "- Visualization settings for results\n",
49
+ "- Required Python libraries for mesh processing and deep learning\n",
50
+ "\n",
51
+ "### Loading Required Libraries\n",
52
+ "\n",
53
+ "Before we proceed with the experiment setup, let's first import all the necessary libraries. These libraries will be used for:\n",
54
+ "- Mesh processing and visualization (`vtk`, `pyvista`)\n",
55
+ "- Data handling and file operations (`pathlib`, `concurrent.futures`)\n",
56
+ "- Progress tracking and visualization (`tqdm`, `matplotlib`)\n",
57
+ "- PyTorch provides data primitives: `torch.utils.data.Dataset` that allow you to use pre-loaded datasets as well as your own data. `Dataset` stores the samples and their corresponding labels.\n",
58
+ "- Important utilities for data processing and training, testing DoMINO (`physicsnemo.utils.domino.utils`)\n"
59
+ ]
60
+ },
61
+ {
62
+ "cell_type": "code",
63
+ "execution_count": null,
64
+ "metadata": {},
65
+ "outputs": [],
66
+ "source": [
67
+ "import os\n",
68
+ "import random\n",
69
+ "from concurrent.futures import ProcessPoolExecutor\n",
70
+ "from pathlib import Path\n",
71
+ "from typing import Union\n",
72
+ "\n",
73
+ "import numpy as np\n",
74
+ "import pyvista as pv\n",
75
+ "import vtk\n",
76
+ "from stl import mesh\n",
77
+ "from tqdm import tqdm\n",
78
+ "\n",
79
+ "from physicsnemo.utils.domino.utils import *\n",
80
+ "from torch.utils.data import Dataset"
81
+ ]
82
+ },
83
+ {
84
+ "cell_type": "markdown",
85
+ "metadata": {},
86
+ "source": [
87
+ "### Experiment Parameters and Variables\n",
88
+ "\n",
89
+ "In this section, we set up all the essential parameters and variables required for the Ahmed body experiment. \n",
90
+ "**Before proceeding, navigate to the data directory and extract the `ahmed_body_dataset.zip` archive. This file contains several sample `.vtp` files needed to run the scripts in this notebook and others.**"
91
+ ]
92
+ },
93
+ {
94
+ "cell_type": "code",
95
+ "execution_count": null,
96
+ "metadata": {},
97
+ "outputs": [],
98
+ "source": [
99
+ "# Directory and Path Configuration\n",
100
+ "DATA_DIR = Path(\"/data/physicsnemo_ahmed_body_dataset_vv1/dataset\") # Root directory for dataset\n",
101
+ "\n",
102
+ "# Physical Variables\n",
103
+ "VOLUME_VARS = [\"p\"] # Volume variables to predict (pressure)\n",
104
+ "SURFACE_VARS = [\"p\", \"wallShearStress\"] # Surface variables to predict\n",
105
+ "AIR_DENSITY = 1.205 # Air density in kg/m³"
106
+ ]
107
+ },
108
+ {
109
+ "cell_type": "markdown",
110
+ "metadata": {},
111
+ "source": [
112
+ "### **Step 2: Convert VTK to STL Files**\n",
113
+ "\n",
114
+ "The second step in our workflow involves converting the CFD simulation data from VTK format to STL format.\n",
115
+ "\n",
116
+ "#### Understanding the Conversion Process\n",
117
+ "\n",
118
+ "The conversion from VTK to STL involves several key steps:\n",
119
+ "1. Reading the VTK PolyData file using specialized readers\n",
120
+ "2. Extracting the surface geometry and mesh data\n",
121
+ "3. Converting the data while preserving topology and surface properties\n",
122
+ "4. Saving the result in binary STL format\n",
123
+ "\n",
124
+ "#### Key Components and Libraries\n",
125
+ "\n",
126
+ "We'll use the following libraries for this conversion:\n",
127
+ "\n",
128
+ "1. **VTK (Visualization Toolkit)**\n",
129
+ " - `vtk`: Reads VTK PolyData files (.vtp)\n",
130
+ " - `vtkSTLWriter`: Writes data in STL format\n",
131
+ " - `vtkPolyData`: Manages surface mesh data structures\n",
132
+ "\n",
133
+ "2. **File System Operations**\n",
134
+ " - `os.path`: Handles file paths and directory operations\n",
135
+ " - `pathlib.Path`: Provides modern path handling capabilities\n",
136
+ "\n",
137
+ "#### Important Considerations\n",
138
+ "\n",
139
+ "During the conversion process, we need to ensure:\n",
140
+ "- Surface normal vectors are preserved correctly\n",
141
+ "- Mesh quality and topology are maintained\n",
142
+ "- The output is compatible with the DoMINO model's requirements\n",
143
+ "- Memory is managed efficiently for large datasets\n",
144
+ "\n",
145
+ "#### Implementation Overview\n",
146
+ "\n",
147
+ "The conversion is implemented through two main functions:\n",
148
+ "\n",
149
+ "1. **Environment Setup**\n",
150
+ "```python\n",
151
+ "def setup_environment(data_dir: str):\n",
152
+ " \"\"\"Sets up the working directory and returns relevant paths.\"\"\"\n",
153
+ " # Returns paths for dataset, info files, STL files, and surface data\n",
154
+ "```\n",
155
+ "\n",
156
+ "2. **VTK to STL Conversion**\n",
157
+ "```python\n",
158
+ "def convert_vtk_to_stl(vtk_filename: str, stl_filename: str):\n",
159
+ " \"\"\"Converts a single .vtp file to .stl format.\"\"\"\n",
160
+ " # Uses vtkXMLPolyDataReader and vtkSTLWriter for conversion\n",
161
+ "```\n",
162
+ "\n",
163
+ "Let's proceed with implementing these functions and performing the conversion:"
164
+ ]
165
+ },
166
+ {
167
+ "cell_type": "code",
168
+ "execution_count": 5,
169
+ "metadata": {},
170
+ "outputs": [],
171
+ "source": [
172
+ "def setup_environment(data_dir: str):\n",
173
+ " \"\"\"Sets up the working directory and returns relevant paths.\"\"\"\n",
174
+ " print(\"=== Environment Setup ===\")\n",
175
+ " print(f\"Current data directory: {data_dir}\")\n",
176
+ "\n",
177
+ " dataset_paths = {split: os.path.join(data_dir, split) for split in [\"train\", \"validation\", \"test\"]}\n",
178
+ " info_paths = {k: os.path.join(data_dir, f\"{k}_info\") for k in dataset_paths}\n",
179
+ " stl_paths = {k: os.path.join(data_dir, f\"{k}_stl_files\") for k in dataset_paths}\n",
180
+ " surface_paths = {k: os.path.join(data_dir, f\"{k}_prepared_surface_data\") for k in dataset_paths}\n",
181
+ "\n",
182
+ " return dataset_paths, info_paths, stl_paths, surface_paths\n",
183
+ "\n",
184
+ "\n",
185
+ "def convert_vtk_to_stl(vtk_filename: str, stl_filename: str):\n",
186
+ " \"\"\"Converts a single .vtp file to .stl format.\"\"\"\n",
187
+ " reader = vtk.vtkXMLPolyDataReader()\n",
188
+ " reader.SetFileName(vtk_filename)\n",
189
+ " reader.Update()\n",
190
+ "\n",
191
+ " if not reader.GetOutput():\n",
192
+ " print(f\"[ERROR] Failed to read {vtk_filename}\")\n",
193
+ " return\n",
194
+ "\n",
195
+ " writer = vtk.vtkSTLWriter()\n",
196
+ " writer.SetFileName(stl_filename)\n",
197
+ " writer.SetInputConnection(reader.GetOutputPort())\n",
198
+ " writer.Write()\n",
199
+ "\n",
200
+ " del reader, writer # Free memory\n",
201
+ "\n",
202
+ "\n",
203
+ "def process_file(vtp_file: str, output_path: str):\n",
204
+ " \"\"\"Processes a single .vtp file and saves it as .stl.\"\"\"\n",
205
+ " output_file = os.path.join(output_path, os.path.basename(vtp_file).replace(\".vtp\", \".stl\"))\n",
206
+ " convert_vtk_to_stl(vtp_file, output_file)\n",
207
+ "\n",
208
+ "\n",
209
+ "def convert_vtp_to_stl_batch(dataset_paths: dict, stl_paths: dict):\n",
210
+ " \"\"\"Processes all .vtp files in dataset_paths and saves them in output_paths.\"\"\"\n",
211
+ " print(\"\\n=== Starting Conversion Process ===\")\n",
212
+ "\n",
213
+ " for path in stl_paths.values(): os.makedirs(path, exist_ok=True)\n",
214
+ "\n",
215
+ " for key, dataset_path in dataset_paths.items():\n",
216
+ " stl_path = stl_paths[key]\n",
217
+ " vtp_files = [os.path.join(dataset_path, f) for f in os.listdir(dataset_path) if f.endswith('.vtp')]\n",
218
+ "\n",
219
+ " if not vtp_files:\n",
220
+ " print(f\"[WARNING] No .vtp files found in {dataset_path}\")\n",
221
+ " continue\n",
222
+ "\n",
223
+ " print(f\"\\nProcessing {len(vtp_files)} files from {dataset_path} → {stl_path}...\")\n",
224
+ "\n",
225
+ " with ProcessPoolExecutor() as executor:\n",
226
+ " list(tqdm(executor.map(process_file, vtp_files, [stl_path] * len(vtp_files)),\n",
227
+ " total=len(vtp_files), desc=f\"Converting {key}\", dynamic_ncols=True))\n",
228
+ "\n",
229
+ " print(\"=== All Conversions Completed Successfully ===\")\n"
230
+ ]
231
+ },
232
+ {
233
+ "cell_type": "markdown",
234
+ "metadata": {},
235
+ "source": [
236
+ "Lets convert the files:"
237
+ ]
238
+ },
239
+ {
240
+ "cell_type": "code",
241
+ "execution_count": null,
242
+ "metadata": {},
243
+ "outputs": [],
244
+ "source": [
245
+ "dataset_paths, info_paths, stl_paths, surface_paths = setup_environment(DATA_DIR)\n",
246
+ "convert_vtp_to_stl_batch(dataset_paths, stl_paths)"
247
+ ]
248
+ },
249
+ {
250
+ "cell_type": "markdown",
251
+ "metadata": {},
252
+ "source": [
253
+ "### **Step 3: Visualizing STL Meshes**\n",
254
+ "\n",
255
+ "The third step in our workflow focuses on visualizing the converted STL meshes to verify the success of our conversion process. This step is crucial because:\n",
256
+ "\n",
257
+ "#### Understanding the Visualization Process\n",
258
+ "\n",
259
+ "The visualization of STL meshes involves several key aspects:\n",
260
+ "1. Loading the STL files using appropriate visualization libraries\n",
261
+ "2. Setting up the visualization environment with proper parameters\n",
262
+ "3. Rendering the mesh with appropriate colors and properties\n",
263
+ "4. Providing interactive controls for inspection\n",
264
+ "\n",
265
+ "#### Key Components and Libraries\n",
266
+ "\n",
267
+ "We'll use the following libraries for visualization:\n",
268
+ "\n",
269
+ "1. **PyVista**\n",
270
+ " - `pv.read()`: Loads STL files\n",
271
+ " - `pv.Plotter()`: Creates interactive visualization windows\n",
272
+ " - `pv.wrap()`: Converts VTK objects to PyVista meshes\n",
273
+ "\n",
274
+ "2. **Matplotlib**\n",
275
+ " - For static 2D visualizations if needed\n",
276
+ " - For saving visualization outputs\n",
277
+ "\n",
278
+ "#### Important Visualization Parameters\n",
279
+ "\n",
280
+ "During the visualization process, we need to consider:\n",
281
+ "- Mesh surface properties (color, opacity)\n",
282
+ "- Camera position and orientation\n",
283
+ "- Lighting conditions\n",
284
+ "- Interactive controls for rotation and zoom\n",
285
+ "- Quality of the rendered output\n",
286
+ "\n",
287
+ "#### Implementation Overview\n",
288
+ "\n",
289
+ "The visualization is implemented through several key functions:\n",
290
+ "\n",
291
+ "1. **Mesh Loading**\n",
292
+ "```python\n",
293
+ "def load_stl_mesh(file_path: str):\n",
294
+ " \"\"\"Loads an STL file and returns a PyVista mesh object.\"\"\"\n",
295
+ " # Uses PyVista to read and process the STL file\n",
296
+ "```\n",
297
+ "\n",
298
+ "2. **Interactive Visualization**\n",
299
+ "```python\n",
300
+ "def plot_stl_comparison(mesh1, mesh2, title1=\"Case 3\", title2=\"Case 9\", volume1=None, volume2=None):\n",
301
+ " \"\"\"Create a multi-view comparison visualization of two STL meshes.\"\"\"\n",
302
+ " # Sets up the plotter and displays the meshes\n",
303
+ "```\n",
304
+ "\n",
305
+ "Let's proceed with implementing these functions and visualizing our converted meshes:"
306
+ ]
307
+ },
308
+ {
309
+ "cell_type": "code",
310
+ "execution_count": 7,
311
+ "metadata": {},
312
+ "outputs": [],
313
+ "source": [
314
+ "def load_stl(stl_path: str):\n",
315
+ " \"\"\"Load an STL file and return its PyVista mesh.\"\"\"\n",
316
+ " stl_file = Path(stl_path)\n",
317
+ " if not stl_file.exists():\n",
318
+ " print(f\"[ERROR] STL file not found: {stl_path}\")\n",
319
+ " return None\n",
320
+ "\n",
321
+ " try:\n",
322
+ " mesh = pv.read(str(stl_file))\n",
323
+ " return mesh\n",
324
+ " except Exception as e:\n",
325
+ " print(f\"[ERROR] Failed to load STL file: {e}\")\n",
326
+ " return None\n",
327
+ "\n",
328
+ "def load_stl(stl_path: str):\n",
329
+ " \"\"\"Load an STL file and return its PyVista mesh.\"\"\"\n",
330
+ " stl_file = Path(stl_path)\n",
331
+ " if not stl_file.exists():\n",
332
+ " print(f\"[ERROR] STL file not found: {stl_path}\")\n",
333
+ " return None\n",
334
+ "\n",
335
+ " try:\n",
336
+ " mesh = pv.read(str(stl_file))\n",
337
+ " return mesh\n",
338
+ " except Exception as e:\n",
339
+ " print(f\"[ERROR] Failed to load STL file: {e}\")\n",
340
+ " return None\n",
341
+ "\n",
342
+ "def plot_stl_comparison(mesh1, mesh2, title1=\"Case 3\", title2=\"Case 9\"):\n",
343
+ " \"\"\"Create a multi-view comparison visualization of two STL meshes.\"\"\"\n",
344
+ " # Start virtual frame buffer\n",
345
+ " pv.start_xvfb()\n",
346
+ "\n",
347
+ " # Create plotter with off-screen rendering\n",
348
+ " pl = pv.Plotter(shape=(2, 4), window_size=[1600, 800], off_screen=True)\n",
349
+ "\n",
350
+ "\n",
351
+ " # Display parameters for each case with brighter colors\n",
352
+ " params_case1 = {\n",
353
+ " \"show_edges\": True,\n",
354
+ " \"opacity\": 1.0,\n",
355
+ " \"edge_color\": 'black',\n",
356
+ " \"line_width\": 0.5,\n",
357
+ " \"color\": [0.6, 0.8, 1.0],\n",
358
+ " \"smooth_shading\": True,\n",
359
+ " \"specular\": 0.4,\n",
360
+ " \"specular_power\": 10,\n",
361
+ " \"diffuse\": 0.9,\n",
362
+ " \"ambient\": 1.0,\n",
363
+ " }\n",
364
+ "\n",
365
+ " params_case2 = {\n",
366
+ " \"show_edges\": True,\n",
367
+ " \"opacity\": 1.0,\n",
368
+ " \"edge_color\": 'black',\n",
369
+ " \"line_width\": 0.5,\n",
370
+ " \"color\": [0.7, 1.0, 0.7],\n",
371
+ " \"smooth_shading\": True,\n",
372
+ " \"specular\": 0.4,\n",
373
+ " \"specular_power\": 10,\n",
374
+ " \"diffuse\": 0.9,\n",
375
+ " \"ambient\": 1.0,\n",
376
+ " }\n",
377
+ "\n",
378
+ " def setup_view(pl, mesh, title, view_type, params):\n",
379
+ " \"\"\"Helper function to set up consistent views\"\"\"\n",
380
+ " pl.add_mesh(mesh, **params)\n",
381
+ " pl.add_text(title, position=\"upper_edge\", font_size=10, color='black')\n",
382
+ " pl.add_axes(line_width=2)\n",
383
+ "\n",
384
+ " if view_type == \"xy\":\n",
385
+ " pl.view_xy()\n",
386
+ " elif view_type == \"xz\":\n",
387
+ " pl.view_xz()\n",
388
+ " elif view_type == \"yz\":\n",
389
+ " pl.view_yz()\n",
390
+ " else: # isometric\n",
391
+ " pl.view_isometric()\n",
392
+ "\n",
393
+ " pl.reset_camera()\n",
394
+ " pl.camera.zoom(0.85)\n",
395
+ " # Add directional lighting\n",
396
+ "\n",
397
+ " # Plot views for both meshes\n",
398
+ " views = [(\"xy\", \"Top\"), (\"xz\", \"Front\"), (\"yz\", \"Side\"), (\"isometric\", \"Isometric\")]\n",
399
+ "\n",
400
+ " # First mesh (top row)\n",
401
+ " for col, (view_type, view_name) in enumerate(views):\n",
402
+ " pl.subplot(0, col)\n",
403
+ " setup_view(pl, mesh1, f\"{title1} - {view_name}\", view_type, params_case1)\n",
404
+ "\n",
405
+ " # Second mesh (bottom row)\n",
406
+ " for col, (view_type, view_name) in enumerate(views):\n",
407
+ " pl.subplot(1, col)\n",
408
+ " setup_view(pl, mesh2, f\"{title2} - {view_name}\", view_type, params_case2)\n",
409
+ "\n",
410
+ " pl.background_color = '#f0f0f0'\n",
411
+ "\n",
412
+ " # Display in notebook\n",
413
+ " return pl.show(jupyter_backend='static')"
414
+ ]
415
+ },
416
+ {
417
+ "cell_type": "markdown",
418
+ "metadata": {},
419
+ "source": [
420
+ "Lets plot two geometries:"
421
+ ]
422
+ },
423
+ {
424
+ "cell_type": "code",
425
+ "execution_count": null,
426
+ "metadata": {},
427
+ "outputs": [],
428
+ "source": [
429
+ "# Define STL file paths\n",
430
+ "STL_FILE_1 = DATA_DIR / \"train_stl_files/case102.stl\"\n",
431
+ "STL_FILE_2 = DATA_DIR / \"train_stl_files/case116.stl\"\n",
432
+ "\n",
433
+ "# Load STL files\n",
434
+ "mesh1 = load_stl(STL_FILE_1)\n",
435
+ "mesh2 = load_stl(STL_FILE_2)\n",
436
+ "\n",
437
+ "# Print comparison\n",
438
+ "print(\"\\n=== STL File Comparison ===\\n\")\n",
439
+ "print(f\"File: {STL_FILE_1.name} vs {STL_FILE_2.name}\")\n",
440
+ "print(f\"Number of Faces: {mesh1.n_faces} vs {mesh2.n_faces}\")\n",
441
+ "print(f\"Surface Area: {mesh1.area:.2f} vs {mesh2.area:.2f}\")\n",
442
+ "print(f\"Volume: {mesh1.volume:.3f} vs {mesh2.volume:.3f} m³\")\n",
443
+ "\n",
444
+ "# Create visualization with volumes included\n",
445
+ "plot_stl_comparison(mesh1, mesh2, title1=\"Case 102\", title2=\"Case 116\")"
446
+ ]
447
+ },
448
+ {
449
+ "cell_type": "markdown",
450
+ "metadata": {},
451
+ "source": [
452
+ "### **Step 4: Convert CFD Results to NPY Format**\n",
453
+ "\n",
454
+ "The fourth step in our workflow focuses on converting CFD simulation results into NumPy (.npy) format for efficient training. As previously mentioned, the advantages of using the NPY format include:\n",
455
+ "- NPY format provides faster data loading during training\n",
456
+ "- Enables efficient memory management for large datasets\n",
457
+ "- Facilitates parallel processing of simulation data\n",
458
+ "- Optimizes data access patterns for deep learning frameworks\n",
459
+ "\n",
460
+ "#### Understanding the Conversion Process\n",
461
+ "\n",
462
+ "The conversion process involves several key aspects:\n",
463
+ "1. Reading CFD simulation data from VTK files\n",
464
+ "2. Extracting surface and volume variables\n",
465
+ "3. Processing mesh geometry and physical quantities\n",
466
+ "4. Normalizing data using appropriate scaling factors\n",
467
+ "5. Saving processed data in NPY format\n",
468
+ "\n",
469
+ "#### Key Components and Libraries\n",
470
+ "\n",
471
+ "We'll use the following libraries for the conversion:\n",
472
+ "\n",
473
+ "1. **VTK and PyVista**\n",
474
+ " - For reading CFD simulation data\n",
475
+ " - For processing mesh geometry and surface properties\n",
476
+ " - For computing cell sizes and normals\n",
477
+ "\n",
478
+ "2. **NumPy**\n",
479
+ " - For efficient array operations\n",
480
+ " - For saving data in NPY format\n",
481
+ "\n",
482
+ "3. **Concurrent Processing**\n",
483
+ " - For parallel processing of multiple files\n",
484
+ " - For improved conversion speed\n",
485
+ "\n",
486
+ "#### Important Data Processing Parameters\n",
487
+ "\n",
488
+ "During the conversion process, we need to consider:\n",
489
+ "- Surface variables (pressure, wall shear stress)\n",
490
+ "- Mean variables (mean wall shear stress, mean pressure)\n",
491
+ "- Data normalization using inlet velocity and density. **The inlet velocity will be read from the info files**\n",
492
+ " ```python\n",
493
+ " with open(info_path, \"r\") as file:\n",
494
+ " velocity = next(float(line.split(\":\")[1].strip()) for line in file if \"Velocity\" in line)\n",
495
+ " ```\n",
496
+ "- Mesh geometry preservation\n",
497
+ "- Memory efficiency for large datasets\n",
498
+ "\n",
499
+ "#### Implementation Overview\n",
500
+ "\n",
501
+ "The conversion is implemented through several key components:\n",
502
+ "\n",
503
+ "1. **Dataset Class**\n",
504
+ "```python\n",
505
+ "class OpenFoamAhmedBodySurfaceDataset(Dataset):\n",
506
+ " \"\"\"Datapipe for converting OpenFOAM dataset to npy.\"\"\"\n",
507
+ " # Handles data loading and processing\n",
508
+ "```\n",
509
+ "\n",
510
+ "2. **File Processing Functions**\n",
511
+ "```python\n",
512
+ "def process_file(fname: str, fm_data, output_path: str):\n",
513
+ " \"\"\"Processes a single surface data file.\"\"\"\n",
514
+ " # Converts individual files to NPY format\n",
515
+ "```\n",
516
+ "\n",
517
+ "3. **Batch Processing**\n",
518
+ "```python\n",
519
+ "def process_surface_data_batch(dataset_paths, info_paths, stl_paths, surface_paths):\n",
520
+ " \"\"\"Processes all surface data files in parallel.\"\"\"\n",
521
+ " # Handles parallel processing of multiple files\n",
522
+ "```\n",
523
+ "\n",
524
+ "Let's proceed with implementing these components and converting our CFD results:"
525
+ ]
526
+ },
527
+ {
528
+ "cell_type": "code",
529
+ "execution_count": 9,
530
+ "metadata": {},
531
+ "outputs": [],
532
+ "source": [
533
+ "class OpenFoamAhmedBodySurfaceDataset(Dataset):\n",
534
+ " \"\"\"Datapipe for converting OpenFOAM dataset to npy.\"\"\"\n",
535
+ "\n",
536
+ " def __init__(self, data_path: Union[str, Path], info_path: Union[str, Path], stl_path: Union[str, Path], surface_variables=None, volume_variables=None, device: int = 0):\n",
537
+ " self.data_path = Path(data_path).expanduser()\n",
538
+ " self.stl_path = Path(stl_path).expanduser()\n",
539
+ " self.info_path = Path(info_path).expanduser()\n",
540
+ " assert self.data_path.exists(), f\"Path {self.data_path} does not exist\"\n",
541
+ "\n",
542
+ " self.filenames = get_filenames(self.data_path)\n",
543
+ " random.shuffle(self.filenames)\n",
544
+ " self.surface_variables = surface_variables or [\"p\", \"wallShearStress\"]\n",
545
+ " self.volume_variables = volume_variables or [\"UMean\", \"pMean\"]\n",
546
+ " self.device = device\n",
547
+ "\n",
548
+ " def __len__(self):\n",
549
+ " return len(self.filenames)\n",
550
+ "\n",
551
+ " def __getitem__(self, idx):\n",
552
+ " cfd_filename = self.filenames[idx]\n",
553
+ " car_dir = self.data_path / cfd_filename\n",
554
+ "\n",
555
+ " stl_path = self.stl_path / f\"{car_dir.stem}.stl\"\n",
556
+ " info_path = self.info_path / f\"{car_dir.stem}_info.txt\"\n",
557
+ "\n",
558
+ " with open(info_path, \"r\") as file:\n",
559
+ " velocity = next(float(line.split(\":\")[1].strip()) for line in file if \"Velocity\" in line)\n",
560
+ "\n",
561
+ " mesh_stl = pv.get_reader(stl_path).read()\n",
562
+ " stl_faces = mesh_stl.faces.reshape(-1, 4)[:, 1:]\n",
563
+ " stl_sizes = np.array(mesh_stl.compute_cell_sizes(length=False, area=True, volume=False).cell_data[\"Area\"])\n",
564
+ "\n",
565
+ " reader = vtk.vtkXMLPolyDataReader()\n",
566
+ " reader.SetFileName(str(car_dir))\n",
567
+ " reader.Update()\n",
568
+ " polydata = reader.GetOutput()\n",
569
+ "\n",
570
+ " celldata = get_node_to_elem(polydata).GetCellData()\n",
571
+ " surface_fields = np.concatenate(get_fields(celldata, self.surface_variables), axis=-1) / (AIR_DENSITY * velocity**2)\n",
572
+ "\n",
573
+ " mesh = pv.PolyData(polydata)\n",
574
+ " surface_sizes = np.array(mesh.compute_cell_sizes(length=False, area=True, volume=False).cell_data[\"Area\"])\n",
575
+ " surface_normals = mesh.cell_normals / np.linalg.norm(mesh.cell_normals, axis=1)[:, np.newaxis]\n",
576
+ "\n",
577
+ " return {\n",
578
+ " \"stl_coordinates\": mesh_stl.points.astype(np.float32),\n",
579
+ " \"stl_centers\": mesh_stl.cell_centers().points.astype(np.float32),\n",
580
+ " \"stl_faces\": stl_faces.flatten().astype(np.float32),\n",
581
+ " \"stl_areas\": stl_sizes.astype(np.float32),\n",
582
+ " \"surface_mesh_centers\": mesh.cell_centers().points.astype(np.float32),\n",
583
+ " \"surface_normals\": surface_normals.astype(np.float32),\n",
584
+ " \"surface_areas\": surface_sizes.astype(np.float32),\n",
585
+ " \"volume_fields\": None,\n",
586
+ " \"volume_mesh_centers\": None,\n",
587
+ " \"surface_fields\": surface_fields.astype(np.float32),\n",
588
+ " \"filename\": cfd_filename,\n",
589
+ " \"stream_velocity\": velocity,\n",
590
+ " \"air_density\": AIR_DENSITY,\n",
591
+ " }\n",
592
+ "\n",
593
+ "def process_file(fname: str, fm_data, output_path: str):\n",
594
+ " \"\"\"Processes a single surface data file.\"\"\"\n",
595
+ " full_path, output_file = os.path.join(fm_data.data_path, fname), os.path.join(output_path, f\"{fname}.npy\")\n",
596
+ " if os.path.exists(output_file) or not os.path.exists(full_path) or os.path.getsize(full_path) == 0:\n",
597
+ " return\n",
598
+ " np.save(output_file, fm_data[fm_data.filenames.index(fname)])\n",
599
+ "\n",
600
+ "def process_surface_data_batch(dataset_paths: dict, info_paths: dict, stl_paths: dict, surface_paths: dict):\n",
601
+ " \"\"\"Processes all surface data files in dataset_paths and saves them in output_paths.\"\"\"\n",
602
+ "\n",
603
+ " for path in surface_paths.values(): os.makedirs(path, exist_ok=True)\n",
604
+ "\n",
605
+ " print(\"=== Starting Processing ===\")\n",
606
+ " for key, dataset_path in dataset_paths.items():\n",
607
+ " surface_path = surface_paths[key]\n",
608
+ " os.makedirs(surface_path, exist_ok=True)\n",
609
+ " fm_data = OpenFoamAhmedBodySurfaceDataset(dataset_path, info_paths[key], stl_paths[key], VOLUME_VARS, SURFACE_VARS)\n",
610
+ " file_list = [fname for fname in fm_data.filenames if fname.endswith(\".vtp\")]\n",
611
+ "\n",
612
+ " print(f\"\\nProcessing {len(file_list)} files from {dataset_path} → {surface_path}...\")\n",
613
+ "\n",
614
+ " with ProcessPoolExecutor() as executor:\n",
615
+ " list(tqdm(executor.map(process_file, file_list, [fm_data]*len(file_list), [surface_path]*len(file_list)),\n",
616
+ " total=len(file_list), desc=f\"Processing {key}\", dynamic_ncols=True))\n",
617
+ "\n",
618
+ " print(\"=== All Processing Completed Successfully ===\")\n"
619
+ ]
620
+ },
621
+ {
622
+ "cell_type": "markdown",
623
+ "metadata": {},
624
+ "source": [
625
+ "Lets convert the files:"
626
+ ]
627
+ },
628
+ {
629
+ "cell_type": "code",
630
+ "execution_count": null,
631
+ "metadata": {},
632
+ "outputs": [],
633
+ "source": [
634
+ "process_surface_data_batch(dataset_paths, info_paths, stl_paths, surface_paths)"
635
+ ]
636
+ },
637
+ {
638
+ "cell_type": "markdown",
639
+ "metadata": {},
640
+ "source": [
641
+ "The files are converted to NPY format and saved in the surface_paths directory. We can now use this data to train a model, which is covered in the next notebook - [domino-training-test.ipynb](domino-training-test.ipynb)"
642
+ ]
643
+ },
644
+ {
645
+ "cell_type": "code",
646
+ "execution_count": null,
647
+ "metadata": {},
648
+ "outputs": [],
649
+ "source": [
650
+ "import os\n",
651
+ "os._exit(00)"
652
+ ]
653
+ }
654
+ ],
655
+ "metadata": {
656
+ "kernelspec": {
657
+ "display_name": "Python 3 (ipykernel)",
658
+ "language": "python",
659
+ "name": "python3"
660
+ },
661
+ "language_info": {
662
+ "codemirror_mode": {
663
+ "name": "ipython",
664
+ "version": 3
665
+ },
666
+ "file_extension": ".py",
667
+ "mimetype": "text/x-python",
668
+ "name": "python",
669
+ "nbconvert_exporter": "python",
670
+ "pygments_lexer": "ipython3",
671
+ "version": "3.12.3"
672
+ }
673
+ },
674
+ "nbformat": 4,
675
+ "nbformat_minor": 4
676
+ }
domino-training-test.ipynb ADDED
@@ -0,0 +1,1343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Physics NeMo External Aerodynamics DLI\n",
8
+ "\n",
9
+ "## Notebook 3 - Training DoMINO Model on the Ahmed body surface dataset and Running Inference\n",
10
+ "\n",
11
+ "In this notebook, we will first provide a detailed explanation of the DoMINO architecture, which is a multi-scale, iterative neural operator designed for modeling large-scale engineering simulations. We will break down the key components of DoMINO, including its use of local geometry representations, multi-scale point convolution kernels, and its efficient handling of complex geometries. Afterward, we will train the model using the **Ahmed body surface dataset**, a widely used dataset in automotive aerodynamics simulations. *As indicated in the previous notebook this dataset was created by the NVIDIA Physics NeMo development team and differs from other similar datasets hosted on cloud platforms like AWS.*\n",
12
+ "\n",
13
+ "*The DoMINO model is capable of training both volume fields (such as velocity and pressure) and surface fields (including pressure and wall shear stress). However, for the sake of simplicity and educational purposes, this notebook will *focus solely on training the surface fields* using the Ahmed body surface dataset.*\n",
14
+ "\n",
15
+ "## Guid line\n",
16
+ "- Before starting training, ensure GPU memory is cleared by running:\n",
17
+ "```python\n",
18
+ "import os\n",
19
+ "os._exit(00)\n",
20
+ "```\n",
21
+ "- The data preprocessing notebook should be run beforehand as a prerequisite to prepare the data in NPY format.\n",
22
+ "- The amount of GPU memory allocated during training can be controlled through the following configuration parameters:\n",
23
+ "```yaml\n",
24
+ " interp_res=GRID_RESOLUTION # Resolution of the latent space; coarser resolutions reduce memory usage.\n",
25
+ " NUM_SURFACE_NEIGHBORS = 7 # Number of neighboring surface points used to compute solution variables such as pressure. \n",
26
+ " SURFACE_POINTS_SAMPLE = 8192 # Number of surface points sampled per epoch; fewer points reduce memory consumption\n",
27
+ "```\n",
28
+ " To adjust GPU memory usage, modify the above configuration values in the \"Experiment Parameters and Variables\" section.\n",
29
+ "\n",
30
+ "\n",
31
+ "# Table of Contents\n",
32
+ "- [DoMINO Architecture](#Domino-Architecture)\n",
33
+ " - [Geometric Understanding: Global and Local Perspectives](#Geometric-Understanding:-Global-and-Local-Perspectives)\n",
34
+ " - [The Prediction Engine: Basis Functions and the Aggregation Network](#The-Prediction-Engine:-Basis-Functions-and-the-Aggregation-Network)\n",
35
+ "- [Training Process](#training)\n",
36
+ " - [Step 1: Define Experiment Parameters and Dependencies](#step-1-define-experiment-parameters-and-dependencies)\n",
37
+ " - [Loading Required Libraries](#loading-required-libraries)\n",
38
+ " - [Dependencies](#dependencies)\n",
39
+ " - [Experiment Parameters and Variables](#experiment-parameters-and-variables)\n",
40
+ " - [Step 2: Train the DoMINO Model](#step-2-train-the-domino-model)\n",
41
+ " - [Understanding the Training Process](#understanding-the-training-process)\n",
42
+ " - [Key Components and Libraries](#key-components-and-libraries)\n",
43
+ " - [Important Training Parameters](#important-training-parameters)\n",
44
+ " - [Implementation Overview](#implementation-overview)\n",
45
+ "- [Load Model Checkpoint & Run Inference](#Load-Model-Checkpoint-&-Run-Inference)\n",
46
+ "- [Visualizing the predicted results](#Visualizing-the-predicted-results)\n",
47
+ "\n",
48
+ "\n",
49
+ "## DoMINO Architecture\n",
50
+ "\n",
51
+ "DoMINO, which stands for Decomposable Multiscale Iterative Neural Operator, is a novel machine learning model designed to address key challenges in accelerating simulations, such as accuracy, scalability, and generalization to new geometries—particularly in the context of automotive aerodynamics. As a neural operator, DoMINO is capable of predicting point-wise volume and surface fields and is inherently scalable to large domains. Its decomposable architecture is central to its performance: it learns local geometric representations within sub-regions of the domain, enabling greater accuracy by focusing on areas with detailed physical features. The model also employs a multiscale approach using learnable point kernels, allowing it to capture both fine and coarse geometric patterns directly from STL files.\n",
52
+ "\n",
53
+ "DoMINO operates iteratively to enable long-range interactions, progressively propagating the learned local geometric features across the entire computational domain. Separately, it builds dynamic, point-based computational stencils—drawing inspiration from traditional numerical methods—which it uses to learn non-linear basis functions tailored to the geometry. These basis functions, combined with localized geometry encodings, allow the model to predict volume and surface solution fields at specified points. A major advantage of DoMINO is that it only requires STL geometry as input during inference; it does not depend on mesh generation or the density and structure of input point clouds. The process begins by encoding global geometry onto a fixed grid using a combination of learnable point convolutions, CNNs, and dense layers. From this, the model constructs localized subdomains around evaluation points to extract rich geometric encodings, which are then used in conjunction with the learned basis functions for final prediction.\n"
54
+ ]
55
+ },
56
+ {
57
+ "cell_type": "markdown",
58
+ "metadata": {},
59
+ "source": [
60
+ "### **Geometric Understanding: Global and Local Perspectives**\n",
61
+ "DoMINO's ability to accurately predict complex flow phenomena relies heavily on its sophisticated understanding of geometry, processed at both global and local scales.\n",
62
+ "\n",
63
+ "\n",
64
+ "- **A.** **Global Geometry Representation**:\n",
65
+ "The Global Geometry Representation refers to the overall shape and structure of the entire object or domain that you are modeling. This representation captures all the geometric details across the entire computational domain.\\\n",
66
+ "Step-by-Step Explanation of Global Geometry Representation:\n",
67
+ "\n",
68
+ " - **Step 1**: Construct Bounding Boxes\n",
69
+ " \t- A tight-fitting surface bounding box is created around the STL (3D geometry) to hold the geometry.\n",
70
+ " - A computational domain bounding box is also defined, which is larger than the surface bounding box to encompass the whole computational domain.\n",
71
+ " - Both bounding boxes can be specified in ```conf.yaml```\n",
72
+ " - **Step 2**: Project STL Vertices onto Structured Grid\n",
73
+ " \t- The geometric features of the point cloud, such as spatial coordinates, are projected onto an N-dimensional structured grid of resolution m×m×m×f, which is overlaid on the surface bounding box using **learnable point convolution kernels**.\n",
74
+ " \t- The learnable point convolution kernels are created using **differentiable ball query layers**. This means that the method:\n",
75
+ " \t- Uses a \"ball\" (a sphere in 3D space) around each point to query or find its neighbors.\n",
76
+ " \t- The ball query layer is \"differentiable,\" meaning it can be included in the neural network and updated via back propagation (i.e., during training, the network can learn how to adjust the kernels to improve performance).\n",
77
+ " \t- The radius of the ball (radius of influence) defines how far around each point we look for neighboring points to include in the convolution. This defines, in fact, how far the geometry can affect the grid. A range of point convolutional kernel sizes can be learned by specifying several radii. Moreover, different kernels are learned to represent information on the surface bounding box and computational domain bounding box. This enables multi-scale learning of geometry encoding by representing both short- and long-range interactions of the surface and flow fields. The radii of influence are defined as **list** in the ```conf.yaml``` file:\n",
78
+ " ```yaml\n",
79
+ " volume_radii: [0.1, 0.5]\n",
80
+ " surface_radii: [0.05]\n",
81
+ " ```\n",
82
+ " These radius are used in the DoMINO model (```physicsnemo/models/domino/model.py```) to compute two **BQWarp** accordingly:\n",
83
+ " \n",
84
+ "\n",
85
+ " \n",
86
+ " ```python\n",
87
+ " class GeometryRep(nn.Module):\n",
88
+ " \n",
89
+ " def __init__(self, input_features: int, radii, model_parameters=None):\n",
90
+ " \"\"\"\n",
91
+ " Initialize the GeometryRep module.\n",
92
+ " \n",
93
+ " Args:\n",
94
+ " input_features: Number of input feature dimensions\n",
95
+ " model_parameters: Configuration parameters for the model\n",
96
+ " \"\"\"\n",
97
+ " super().__init__()\n",
98
+ " geometry_rep = model_parameters.geometry_rep\n",
99
+ " self.geo_encoding_type = model_parameters.geometry_encoding_type\n",
100
+ " \n",
101
+ " self.bq_warp = nn.ModuleList()\n",
102
+ " self.geo_processors = nn.ModuleList()\n",
103
+ " for j, p in enumerate(radii):\n",
104
+ " self.bq_warp.append(\n",
105
+ " BQWarp(\n",
106
+ " grid_resolution=model_parameters.interp_res,\n",
107
+ " radius=radii[j],\n",
108
+ " )\n",
109
+ " )\n",
110
+ " self.geo_processors.append(\n",
111
+ " GeoProcessor(\n",
112
+ " input_filters=geometry_rep.geo_conv.base_neurons_out,\n",
113
+ " model_parameters=geometry_rep.geo_processor,\n",
114
+ " )\n",
115
+ " )\n",
116
+ " ```\n",
117
+ " - **Step 3**: Use Multi-Resolution Approach for Detailed and Coarse Features\n",
118
+ " \t- The grid resolution in the bounding box determines the level of detail of the geometry: \n",
119
+ " \t- Finer resolution captures more detailed features of the geometry.\n",
120
+ " \t- Coarser resolution captures larger, broader features.\n",
121
+ " \t- A multi-resolution approach is adopted, meaning multiple grids at different resolutions (levels) are maintained to capture both fine and coarse features of the geometry. The number of resolution levels is a parameter that can be adjusted in conf.yaml file as \\\n",
122
+ " ```yaml GRID_RESOLUTION = [128, 64, 48] # Resolution of the interpolation grid ```\n",
123
+ " - **Currently, the DoMINO model allows specification of a single resolution but this configuration will be provided in a future release.**\n",
124
+ "\n",
125
+ " - **Step 4**: Propagate Geometry Features into the Computational Domain\n",
126
+ " - The computational domain is much larger than the surface bounding box, so the geometry information needs to be extended.\n",
127
+ " \t- Geometry features are propagated into the computational domain using two methods: \n",
128
+ " \t- As explained in **step 2** Multi-scale **point convolution kernels** project the **geometry information** onto the surface bounding box (**see below the left figure**).\n",
129
+ " \t- **Features** from the surface grid of the bounding box (i.e., Gs) are propagated into the computational domain grid (i.e., Gc) using **CNN blocks** that contain convolution, pooling, and unpooling layers (**see below the left figure**).\n",
130
+ " As you see in the code snippet above first `BQWarp` is calculated and then CNN blocks using `GeoProcessor`:\n",
131
+ " ```python\n",
132
+ " class GeoProcessor(nn.Module):\n",
133
+ " ```\n",
134
+ " - The **CNN blocks are iterated** for a specified number of steps to refine the geometry representation. Currently, the DoMINO model is configured to run a single iteration. An option to change this will be provided in ```conf.yaml``` in a future release.\n",
135
+ "\n",
136
+ " - **Step 5**: Calculate Signed Distance Function (SDF) and its Gradients\n",
137
+ " \t- Additionally, the Signed Distance Function (SDF) and its gradient components are calculated on the computational domain grid.\n",
138
+ " \t- These SDF and gradient values are added to the learned features, providing additional information about the topology of the geometry (i.e., the geometry's shape, distances to surfaces, etc.).\n",
139
+ "\n",
140
+ " - **Step 6**: Final Global Geometry Representation\n",
141
+ " \t- The final geometry representation of the STL is formed by combining the learned features from the structured grids at different resolutions in both the bounding box and the computational domain.\n",
142
+ " \n",
143
+ "  \n",
144
+ "  \n",
145
+ " **Once the computational domain is created, the next step would be local geometry representation.**\n",
146
+ "\n",
147
+ "\n",
148
+ "- **B.** **Local geometry representation**\n",
149
+ "The Local Geometry Representation focuses on the geometry in the immediate vicinity of a sampled point p (the points in simulation mesh). The idea is to understand how the geometry behaves around a specific point and its neighbors, which can be important for accurate predictions. While the Global Geometry Representation gives the big picture, the Local Geometry Representation zooms in on a small region of interest around each sampled point. The key difference is that local geometry represents a smaller, more detailed portion of the global geometry, typically focusing on the small-scale features close to a point. For each sampled point p, neighboring points are sampled randomly around them to form a computational stencil of points similar to finite volume and element methods. The local geometry representation is learned by drawing a subregion around the computational stencil of\n",
150
+ "p + 1 points. The size of the subregion are defined as **list** in the ```conf.yaml``` file:\n",
151
+ "\n",
152
+ " ```yaml\n",
153
+ " geometry_local.volume_radii: [0.05, 0.1]\n",
154
+ " geometry_local.surface_radii: [0.05]\n",
155
+ " ```\n",
156
+ "Similar to Global geometry representation a point convolution kernel is used here to extract the local features in the subregion from the global geometry representation on the computational domain.\n",
157
+ "The **BQWarp** are computed for Local geometry representation in ```physicsnemo/models/domino/model.py``` in ```class DoMINO(nn.Module): ```:\n",
158
+ "\n",
159
+ "```python\n",
160
+ " for ct, j in enumerate(self.surface_radius):\n",
161
+ " if self.geo_encoding_type == \"both\":\n",
162
+ " total_neighbors_in_radius = self.surface_neighbors_in_radius[ct] * (\n",
163
+ " len(model_parameters.geometry_rep.geo_conv.surface_radii) + 1\n",
164
+ " )\n",
165
+ " elif self.geo_encoding_type == \"stl\":\n",
166
+ " total_neighbors_in_radius = self.surface_neighbors_in_radius[ct] * (\n",
167
+ " len(model_parameters.geometry_rep.geo_conv.surface_radii)\n",
168
+ " )\n",
169
+ " elif self.geo_encoding_type == \"sdf\":\n",
170
+ " total_neighbors_in_radius = self.surface_neighbors_in_radius[ct]\n",
171
+ "\n",
172
+ " self.surface_bq_warp.append(\n",
173
+ " BQWarp(\n",
174
+ " grid_resolution=model_parameters.interp_res,\n",
175
+ " radius=self.surface_radius[ct],\n",
176
+ " neighbors_in_radius=self.surface_neighbors_in_radius[ct],\n",
177
+ " )\n",
178
+ " )\n",
179
+ " self.surface_local_point_conv.append(\n",
180
+ " LocalPointConv(\n",
181
+ " input_features=total_neighbors_in_radius,\n",
182
+ " base_layer=512,\n",
183
+ " output_features=self.surface_neighbors_in_radius[ct],\n",
184
+ " )\n",
185
+ " )\n",
186
+ "```\n",
187
+ "\n",
188
+ "Note that **BQWarp** for **Global geometry representation** are computed in ``` class GeometryRep(nn.Module): ```\n",
189
+ "\n",
190
+ "**How Does the Multi-Resolution Global Geometry Affect the Local Geometry Representation?**\n",
191
+ " - Coarse resolution: At the coarse resolution, you get a broad view of the object. This can give information about the general shape and large-scale features of the geometry (e.g., the overall shape of the object, major boundaries, etc.). When local geometry is extracted from the coarse resolution, the features are relatively less detailed, and it might capture larger, more general features of the object.\n",
192
+ " - Fine resolution: At the fine resolution, you get a detailed view of the geometry, capturing small features such as intricate surface details, small holes, or sharp edges. The local geometry representation derived from the fine resolution will be more detailed and capture smaller variations in the geometry near each sampled point.\n",
193
+ "\n",
194
+ "**Thus, the global multi-resolution geometry allows the local geometry to be learned at different levels of detail, depending on the resolution of the grid that is used to represent the geometry.** \n",
195
+ "\n",
196
+ "\n",
197
+ "### **The Prediction Engine: Basis Functions and the Aggregation Network**\n",
198
+ "Once DoMINO has processed the local geometry around each point of interest, the next stage involves taking these learned geometric features, along with other relevant information, and predicting the actual flow field values. This is accomplished by an \"Aggregation Network,\" which has a structure inspired by DeepONet.\n",
199
+ "\n",
200
+ "\n",
201
+ "- **A.** **Adding Spatial Context: The Role of Positional Encoding**\n",
202
+ "Neural networks, particularly simpler architectures like Multi-Layer Perceptrons (MLPs), often process sets of input features without an inherent understanding of their absolute or relative spatial positions. If a network receives only a list of coordinates, it might not easily distinguish whether a particular point is \"at the front\" or \"at the back\" of an object in a global sense, or how two distant points are spatially related if they are not immediate neighbors. To overcome this, Positional Encoding (PE) techniques are employed. PE injects information about the position of points into the feature vectors that the network processes (*see the right panel in the figure below*). In DoMINO, \"Position encoding is calculated between the coordinates of the sampled point and the center of mass of the geometry STL.\n",
203
+ "\n",
204
+ "- **B.** **Basis Function Neural Network (Latent Vector):**\n",
205
+ " - Separately, DoMINO builds dynamic, point-based computational stencils—drawing inspiration from traditional numerical methods—which it uses to learn non-linear basis functions tailored to the geometry. These basis functions, combined with localized geometry encodings, allow the model to predict volume and surface solution fields at specified points (*see the right panel in the figure below*).\\\n",
206
+ " What happens here: \n",
207
+ " - The input features (coordinates, SDF, normal vectors, etc. and their fourier features) for each point in the stencil are fed into the Basis Function Neural Network. This is a fully connected neural network that processes these features.\n",
208
+ " - The network then computes a latent vector for each point in the stencil. A latent vector is a compressed mathematical representation that encodes the important information about each point’s geometry and position.\n",
209
+ " - Purpose: The latent vector captures the essential characteristics of each point’s geometry in a compact form, which will be used in later steps for predicting the solution at that point.\n",
210
+ "\n",
211
+ "- **C.** **Concatenating the Latent Vector with the Local Geometry Encoding:**\n",
212
+ " - After calculating the latent vector for each point, this vector is concatenated with the local geometry encoding (which includes the previously computed information from the surrounding points and the global geometry) and positional encoding.\n",
213
+ " - Why this is done: Concatenating these two representations allows the network to use both the specific local features of each point and the broader context of the surrounding geometry to make predictions.\n",
214
+ "\n",
215
+ "- **D.** **Passing Through Additional Neural Network Layers (Solution Prediction):**\n",
216
+ " - The combined information (latent vector + positional encoding + local geometry encoding) is passed through another set of fully connected layers (a new neural network).\n",
217
+ " - What happens here: These layers process the combined information and predict a solution vector for each point in the stencil. The solution vector could represent various physical quantities such as temperature, pressure, or other simulation results at the sampled point.\n",
218
+ " - Purpose: This step produces the predicted solution at each point, based on the local and global geometry.\n",
219
+ "\n",
220
+ "- **E.** **Aggregation Network:**\n",
221
+ "\n",
222
+ " The core of the prediction engine in DoMINO is the **Aggregation network**. This is described as **a fully connected neural network with a DeepONet like structure.** This network takes the processed local geometric features and the basis functions (derived from the Basis Function Neural Network and local geometry encodings) and combines them to compute the final solution field values.   \n",
223
+ " To understand DoMINO's aggregation network, it's helpful to recall the DeepONet architecture. DeepONet is specifically designed for learning operators, mathematical mappings between function spaces. This makes it highly suitable for problems in physics described by PDEs. A DeepONet typically consists of two main sub-networks:   \n",
224
+ "\n",
225
+ " - Branch Network: Processes the input function (e.g., PDE coefficients).   \n",
226
+ " - Trunk Network: Processes the coordinates where the output function is evaluated. The outputs of these two networks are then combined to produce the prediction.\n",
227
+ "\n",
228
+ " In DoMINO, this DeepONet structure is adapted as follows:\n",
229
+ "\n",
230
+ " - **Branch Net**: This part of the aggregation network takes the Local geometry rep as input. These are the η features, which encapsulate the detailed local geometric information around point i and its neighbors j.\n",
231
+ " - **Trunk Net**: This part processes the basis functions. As described above, these \"basis functions\" are the output of the Basis Function Neural Network after concatenation with local geometry encodings and further MLP layers. They represent a learned, rich description of the query point i's properties, its positional encoding, and its context within its local stencil.   \n",
232
+ " The DeepONet-like architecture allows DoMINO to effectively learn how different local geometric environments (processed by the branch net) influence the flow solution at various specific locations (whose context, encoded in the basis functions, is processed by the trunk net).\n",
233
+ " The aggregation network, with its DeepONet-like structure, \"computes the solution field on the sampled point, i and its neighbors j.\" This means that for a given point of interest i, the network doesn't just predict the solution at i in isolation. Instead, it leverages the local stencil of points (point i itself and its defined neighbors j) to make predictions across this local cloud.\n",
234
+ "\n",
235
+ " After the aggregation network produces these individual predictions for point i and its neighbors j, the solutions are then averaged using an inverse distance weighted interpolation (**IDW**) scheme.\n",
236
+ " In DoMINO, the \"*known values*\" $u_k$ are the predictions made by the aggregation network at point $i$ and its neighbors $j$. The IDW scheme then blends these predictions to yield the final solution at point $i$, giving more credence to the predictions made at or very near $i$. \n",
237
+ " This IDW step can be seen as a form of learned solution refinement or a consensus mechanism. By predicting solutions across a local stencil and then averaging them with IDW, the model can produce a more robust and spatially consistent output at point $i$, potentially smoothing out minor errors or noise from individual network predictions within that stencil. \n",
238
+ " This enforces a degree of local coherence in the predicted flow field, which is a desirable characteristic for physical simulations. IDW is often applied within a defined \"search neighborhood\" ; in DoMINO, this neighborhood is implicitly the set of points $j$ (and $i$ itself) for which the aggregation network computes initial solutions.   \n",
239
+ "\n",
240
+ "<div style=\"display: flex; justify-content: center; gap: 10px;\">\n",
241
+ " <figure style=\"text-align: center;\">\n",
242
+ " <img src=\"https://raw.githubusercontent.com/openhackathons-org/End-to-End-AI-for-Science/main/workspace/python/jupyter_notebook/DoMINO/images/global_geo_rep.png\" style=\"width: 100%; height: auto;\">\n",
243
+ " <figcaption>Computation and surface Bounding box representation.</figcaption>\n",
244
+ " </figure>\n",
245
+ " <figure style=\"text-align: center;\">\n",
246
+ " <img src=\"https://raw.githubusercontent.com/openhackathons-org/End-to-End-AI-for-Science/main/workspace/python/jupyter_notebook/DoMINO/images/aggregation_net.png\" style=\"width: 55%; height: auto;\">\n",
247
+ " <figcaption>Aggregation network is a fully connected neural network with a DeepONet like structure, where Local geometry rep is branch net and basis functions are trunk net.</figcaption>\n",
248
+ " </figure>\n",
249
+ "</div>\n"
250
+ ]
251
+ },
252
+ {
253
+ "cell_type": "markdown",
254
+ "metadata": {},
255
+ "source": [
256
+ "## **Training**\n",
257
+ "### **Step 1: Define Experiment Parameters and Dependencies**\n",
258
+ "\n",
259
+ "The first step in training the DoMINO model on the Ahmed body dataset is to set up our experiment environment and define the necessary parameters. This includes specifying paths to our data, configuring training settings, and ensuring all required libraries are available.\n",
260
+ "\n",
261
+ "Key components we need to set up:\n",
262
+ "- Data paths for training and validation sets\n",
263
+ "- Model hyperparameters and training configurations\n",
264
+ "- Visualization settings for results\n",
265
+ "- Required Python libraries for mesh processing and deep learning\n",
266
+ "\n",
267
+ "#### Loading Required Libraries\n",
268
+ "\n",
269
+ "Before we proceed with the experiment setup, let's first import all the necessary libraries. These libraries will be used for:\n",
270
+ "- Deep learning and numerical computations (torch, numpy)\n",
271
+ "- Progress tracking and visualization (tqdm, matplotlib)\n"
272
+ ]
273
+ },
274
+ {
275
+ "cell_type": "code",
276
+ "execution_count": null,
277
+ "metadata": {},
278
+ "outputs": [],
279
+ "source": [
280
+ "import time\n",
281
+ "import os\n",
282
+ "import re\n",
283
+ "import torch\n",
284
+ "import torchinfo\n",
285
+ "\n",
286
+ "\n",
287
+ "\n",
288
+ "import pyvista as pv\n",
289
+ "from tqdm import tqdm\n",
290
+ "from pathlib import Path\n",
291
+ "from types import SimpleNamespace\n",
292
+ "import matplotlib.pyplot as plt\n",
293
+ "from pathlib import Path\n",
294
+ "import apex\n",
295
+ "import numpy as np\n",
296
+ "import hydra\n",
297
+ "from hydra.utils import to_absolute_path\n",
298
+ "from omegaconf import DictConfig, OmegaConf\n",
299
+ "\n",
300
+ "from torch.cuda.amp import GradScaler, autocast\n",
301
+ "from torch.nn.parallel import DistributedDataParallel\n",
302
+ "from torch.utils.data import DataLoader\n",
303
+ "from torch.utils.data.distributed import DistributedSampler\n",
304
+ "from torch.utils.tensorboard import SummaryWriter\n",
305
+ "\n",
306
+ "from physicsnemo.distributed import DistributedManager\n",
307
+ "from physicsnemo.launch.utils import load_checkpoint, save_checkpoint\n",
308
+ "from physicsnemo.utils.sdf import signed_distance_field\n",
309
+ "\n",
310
+ "from physicsnemo.datapipes.cae.domino_datapipe import DoMINODataPipe\n",
311
+ "from physicsnemo.models.domino.model import DoMINO\n",
312
+ "from physicsnemo.utils.domino.utils import *"
313
+ ]
314
+ },
315
+ {
316
+ "cell_type": "markdown",
317
+ "metadata": {},
318
+ "source": [
319
+ "### Experiment Parameters and Variables\n",
320
+ "\n",
321
+ "In this section, we define all the necessary parameters and variables for our Ahmed body experiment. These parameters control various aspects of the training process, data processing, and model configuration.\n",
322
+ "\n",
323
+ "These parameters are carefully chosen based on:\n",
324
+ "- The physical dimensions of the Ahmed body\n",
325
+ "- The computational requirements of the DoMINO model\n",
326
+ "- The desired resolution for accurate flow prediction\n",
327
+ "- The available computational resources\n",
328
+ "- The specific requirements of the aerodynamic analysis\n",
329
+ "- `GEOMETRY_REP` contains the hyperparameters for the global geometry representation network. \n",
330
+ "- `GEOMETRY_LOCAL` contains the hyperparameters for the local geometry representation. \n",
331
+ "- As described in the theoretical section, the point convolution kernel relies on two additional factors: the radius of influence and the number of points included in the kernel. The radii of influence are specified as `volume_radii` and `surface_radii` within both `GEOMETRY_REP` and `GEOMETRY_LOCAL`. \n",
332
+ "- The number of points within the kernel is defined as `volume_neighbors_in_radius=[128, 128]` and `surface_neighbors_in_radius=[128]` for the local geometry representation. For the global geometry representation, these values are not explicitly set in the `config.yaml` file, so the default value of `10` is used. \n",
333
+ "- The **bounding box parameters** play a crucial role, as they define the computational domain for both volume and surface meshes, ensuring that all relevant flow features around the Ahmed body are accurately captured.\n"
334
+ ]
335
+ },
336
+ {
337
+ "cell_type": "code",
338
+ "execution_count": null,
339
+ "metadata": {},
340
+ "outputs": [],
341
+ "source": [
342
+ "# Directory and Path Configuration\n",
343
+ "EXPERIMENT_TAG = 4 # Unique identifier for this experiment run\n",
344
+ "PROJECT_NAME = \"ahmed_body_dataset\" # Name of the project\n",
345
+ "OUTPUT_DIR = Path(f\"./outputs/{PROJECT_NAME}/{EXPERIMENT_TAG}\") # Directory for experiment outputs\n",
346
+ "DATA_DIR = Path(\"/data/physicsnemo_ahmed_body_dataset_vv1/dataset\") # Root directory for dataset\n",
347
+ "\n",
348
+ "CHECKPOINT_DIR = OUTPUT_DIR / \"models\" # Directory for saving model checkpoints\n",
349
+ "SAVE_PATH = DATA_DIR / \"mesh_predictions_surf_final1\" # path to save prediction results\n",
350
+ "\n",
351
+ "# Ensure directories exist\n",
352
+ "\n",
353
+ "os.makedirs(CHECKPOINT_DIR, exist_ok=True)\n",
354
+ "\n",
355
+ "# Physical Variables\n",
356
+ "VOLUME_VARS = [\"p\"] # Volume variables to predict (pressure)\n",
357
+ "SURFACE_VARS = [\"p\", \"wallShearStress\"] # Surface variables to predict\n",
358
+ "MODEL_TYPE = \"surface\" # Type of model (surface-only prediction)\n",
359
+ "AIR_DENSITY = 1.205 # Air density in kg/m³\n",
360
+ "\n",
361
+ "# Training Hyperparameters\n",
362
+ "NUM_EPOCHS = 3 # Number of training epochs\n",
363
+ "LR = 0.001 # Learning rate\n",
364
+ "BATCH_SIZE = 1 # Batch size for training\n",
365
+ "GRID_RESOLUTION = [128, 64, 48] # Resolution of the interpolation grid\n",
366
+ "SURFACE_POINTS_SAMPLE = 8192\n",
367
+ "GEOMETRY_REP=SimpleNamespace(\n",
368
+ " geo_conv=SimpleNamespace(base_neurons=32, base_neurons_out=1, volume_radii=[0.1, 0.5], surface_radii=[0.05], hops=1),\n",
369
+ " geo_processor=SimpleNamespace(base_filters=8),\n",
370
+ " geo_processor_sdf=SimpleNamespace(base_filters=8)\n",
371
+ ") # Hyperparameters for global geometry representation network\n",
372
+ "\n",
373
+ "GEOMETRY_LOCAL=SimpleNamespace(volume_neighbors_in_radius=[128, 128], surface_neighbors_in_radius=[128], volume_radii=[0.05, 0.1], surface_radii=[0.05], base_layer=512) # Hyperparameters for local geometry extraction \n",
374
+ "\n",
375
+ "NUM_SURFACE_NEIGHBORS = 7 # Number of neighbors for surface operations\n",
376
+ "NORMALIZATION = \"min_max_scaling\" # Data normalization method\n",
377
+ "INTEGRAL_LOSS_SCALING = 0 # Scaling factor for integral loss\n",
378
+ "GEOMETRY_ENCODING_TYPE= \"both\" # geometry encoder type, sdf, stl, both\n",
379
+ "NUM_SURF_VARS = 4 # Number of surface variables to predict, 3 for vectore (wallShearStress) and 1 for scalar (p)\n",
380
+ "CHECKPOINT_INTERVAL = 1 # Save checkpoint every N epochs\n",
381
+ "\n",
382
+ "\n",
383
+ "# Dataset Paths\n",
384
+ "DATA_PATHS = {\n",
385
+ " \"train\": f\"{DATA_DIR}/train_prepared_surface_data/\",\n",
386
+ " \"val\": f\"{DATA_DIR}/validation_prepared_surface_data/\",\n",
387
+ " \"test\": f\"{DATA_DIR}/test\"\n",
388
+ "}\n",
389
+ "\n",
390
+ "# Model and Scaling Factor Paths\n",
391
+ "MODEL_SAVE_DIR = \"./outputs/ahmed_body_dataset/4/models\"\n",
392
+ "SURF_SAVE_PATH = './outputs/ahmed_body_dataset/surface_scaling_factors.npy'\n",
393
+ "\n",
394
+ "# Bounding Box Configuration for Volume and Surface Meshes\n",
395
+ "BOUNDING_BOX = SimpleNamespace(\n",
396
+ " max=[0.5, 0.6, 0.6], # Maximum coordinates for volume mesh\n",
397
+ " min=[-2.5, -0.5, -0.5] # Minimum coordinates for volume mesh\n",
398
+ ")\n",
399
+ "BOUNDING_BOX_SURF = SimpleNamespace(\n",
400
+ " max=[0.01, 0.6, 0.4], # Maximum coordinates for surface mesh\n",
401
+ " min=[-1.6, -0.01, -0.01] # Minimum coordinates for surface mesh\n",
402
+ ")\n",
403
+ "\n",
404
+ "# Set cuDNN benchmark mode\n",
405
+ "torch.backends.cudnn.benchmark = True\n",
406
+ "torch.backends.cudnn.deterministic = False"
407
+ ]
408
+ },
409
+ {
410
+ "cell_type": "markdown",
411
+ "metadata": {},
412
+ "source": [
413
+ "### **Step 2: Train the DoMINO Model**\n",
414
+ "\n",
415
+ "The fifth step in our workflow focuses on training the DoMINO model on our processed CFD data. This step is crucial because:\n",
416
+ "- It enables the model to learn complex fluid dynamics patterns\n",
417
+ "- Provides a foundation for accurate flow field predictions\n",
418
+ "- Allows for efficient inference on new geometries\n",
419
+ "- Supports distributed training for improved performance\n",
420
+ "\n",
421
+ "#### Understanding the Training Process\n",
422
+ "\n",
423
+ "The training process involves several key components:\n",
424
+ "1. Setting up distributed training environment\n",
425
+ "2. Creating and configuring datasets and dataloaders\n",
426
+ "3. Initializing the DoMINO model architecture\n",
427
+ "4. Implementing training and validation loops\n",
428
+ "5. Managing model checkpoints and metrics\n",
429
+ "\n",
430
+ "#### Key Components and Libraries\n",
431
+ "\n",
432
+ "We'll use the following for training:\n",
433
+ "\n",
434
+ "- **PyTorch**\n",
435
+ " - `torch.distributed`: For distributed training\n",
436
+ " - `torch.cuda`: For GPU acceleration\n",
437
+ " - `torch.optim`: For optimization algorithms\n",
438
+ "\n",
439
+ "- **Data Management**\n",
440
+ " - Custom dataset classes for CFD data\n",
441
+ " - Distributed samplers for efficient data loading\n",
442
+ " - Distributed samplers for efficient data loading\n",
443
+ "\n",
444
+ "#### Important Training Parameters\n",
445
+ "\n",
446
+ "During the training process, we need to consider:\n",
447
+ "- Batch size and learning rate\n",
448
+ "- Number of epochs and validation frequency\n",
449
+ "- Model architecture parameters\n",
450
+ "- Loss function configuration\n",
451
+ "- Checkpointing strategy\n",
452
+ "\n",
453
+ "#### Implementation Overview\n",
454
+ "\n",
455
+ "The training is implemented through several key components:\n",
456
+ "\n",
457
+ "1. **Model Creation**\n",
458
+ "Creates the DoMINO model, applies configuration, and wraps it with DistributedDataParallel if training is distributed.\n",
459
+ "DoMINO model is initialized with full configuration. the model is moved to the given device (usually a CUDA GPU).\n",
460
+ "If training in distributed mode, wrap the model with DistributedDataParallel to sync gradients across processes.\n",
461
+ " \n",
462
+ "```python\n",
463
+ "def create_model(device, rank, world_size):\n",
464
+ " \"\"\"Create and configure DoMINO model.\"\"\"\n",
465
+ " # Initializes model with specified parameters\n",
466
+ "```\n",
467
+ "2. **Create DoMINO dataset**\n",
468
+ "Constructs and returns a DoMINODataPipe object for a specified phase (`train` or `val`), fully configured for loading data.\n",
469
+ "Loads path-specific and global configs (grid resolution, surface vars, encoding, etc.).\n",
470
+ "\n",
471
+ "```python\n",
472
+ "def create_dataset(phase):\n",
473
+ " \"\"\"\n",
474
+ " Create DoMINO dataset for specified phase (train/val).\n",
475
+ " \n",
476
+ " Args:\n",
477
+ " phase (str): Dataset phase ('train' or 'val')\n",
478
+ " \n",
479
+ " Returns:\n",
480
+ " DoMINODataPipe: Configured dataset\n",
481
+ " \"\"\"\n",
482
+ "```\n",
483
+ "\n",
484
+ "3. **Training Loop**\n",
485
+ " Main training loop for running multiple epochs. Handles distributed settings, optimizer/scaler setup, and calls `run_epoch()` for training and evaluation.\n",
486
+ " It gets dataloaders and samplers for train/val datasets. The optimizer (FusedAdam) and gradient scaler will be set up.\n",
487
+ " \n",
488
+ " For each epoch:\n",
489
+ " - Update distributed sampler epoch (important for shuffling).\n",
490
+ " - Call run_epoch() to train and validate.\n",
491
+ " - Update best validation loss.\n",
492
+ " \n",
493
+ "```python\n",
494
+ "def train(model, device, rank, world_size):\n",
495
+ " \"\"\"Orchestrates the training process.\"\"\"\n",
496
+ " # Handles training loop, validation, and checkpointing\n",
497
+ "```\n",
498
+ "\n",
499
+ "4. **Runs one training epoch run_epoch()**\n",
500
+ "Runs one training epoch, performs forward and backward passes, computes losses, and evaluates on validation data. Supports mixed precision and distributed training.\n",
501
+ " \n",
502
+ "Step-by-step:\n",
503
+ "- Training phase:\n",
504
+ " - Set model to training mode.\n",
505
+ " - Use tqdm progress bar if rank is 0 (main process).\n",
506
+ " - For each batch:\n",
507
+ " - Move it to the correct device.\n",
508
+ " - Run model and compute predictions.\n",
509
+ " - Compute masked MSE loss using mse_loss_fn.\n",
510
+ " - Apply gradient scaling for mixed-precision training.\n",
511
+ " - Step the optimizer and clear gradients.\n",
512
+ " - Log training loss (on rank 0).\n",
513
+ "- Validation phase:\n",
514
+ " - Run inference without gradient tracking.\n",
515
+ " - Average validation loss over the val loader.\n",
516
+ "\n",
517
+ "- Checkpointing:\n",
518
+ " - Save best model if current val loss is better.\n",
519
+ " - Save periodic checkpoint if epoch meets interval.\n",
520
+ " - Return validation loss for tracking best model.\n",
521
+ "\n",
522
+ "Let's proceed with implementing these components and training our model:"
523
+ ]
524
+ },
525
+ {
526
+ "cell_type": "code",
527
+ "execution_count": null,
528
+ "metadata": {},
529
+ "outputs": [],
530
+ "source": [
531
+ "def mse_loss_fn(output, target, padded_value=-10):\n",
532
+ " \"\"\"\n",
533
+ " Compute masked MSE loss, ignoring padded values.\n",
534
+ " \n",
535
+ " Args:\n",
536
+ " output (torch.Tensor): Model predictions\n",
537
+ " target (torch.Tensor): Ground truth values\n",
538
+ " padded_value (float): Value used for padding (default: -10)\n",
539
+ " \n",
540
+ " Returns:\n",
541
+ " torch.Tensor: Mean squared error loss\n",
542
+ " \"\"\"\n",
543
+ " # Move target to same device as output\n",
544
+ " target = target.to(output.device)\n",
545
+ " # Create mask for non-padded values\n",
546
+ " mask = torch.abs(target - padded_value) > 1e-3\n",
547
+ " # Compute masked loss\n",
548
+ " masked_loss = torch.sum(((output - target) ** 2) * mask) / torch.sum(mask)\n",
549
+ " return masked_loss.mean()\n",
550
+ "\n",
551
+ "def create_dataset(phase):\n",
552
+ " \"\"\"\n",
553
+ " Create DoMINO dataset for specified phase (train/val).\n",
554
+ " \n",
555
+ " Args:\n",
556
+ " phase (str): Dataset phase ('train' or 'val')\n",
557
+ " \n",
558
+ " Returns:\n",
559
+ " DoMINODataPipe: Configured dataset\n",
560
+ " \"\"\"\n",
561
+ " return DoMINODataPipe(\n",
562
+ " DATA_PATHS[phase],\n",
563
+ " phase=phase,\n",
564
+ " grid_resolution=GRID_RESOLUTION,\n",
565
+ " surface_variables=SURFACE_VARS,\n",
566
+ " normalize_coordinates=True,\n",
567
+ " sampling=True,\n",
568
+ " sample_in_bbox=True,\n",
569
+ " volume_points_sample=8192,\n",
570
+ " surface_points_sample=SURFACE_POINTS_SAMPLE,\n",
571
+ " geom_points_sample=60000,\n",
572
+ " positional_encoding=False,\n",
573
+ " surface_factors=np.load(SURF_SAVE_PATH),\n",
574
+ " scaling_type=NORMALIZATION,\n",
575
+ " model_type=MODEL_TYPE,\n",
576
+ " bounding_box_dims=BOUNDING_BOX,\n",
577
+ " bounding_box_dims_surf=BOUNDING_BOX_SURF,\n",
578
+ " num_surface_neighbors=NUM_SURFACE_NEIGHBORS,\n",
579
+ " gpu_preprocessing=False\n",
580
+ " )\n",
581
+ "\n",
582
+ "\n",
583
+ "\n",
584
+ "def create_dataloaders(rank, world_size):\n",
585
+ " \"\"\"\n",
586
+ " Create train and validation dataloaders with distributed sampling.\n",
587
+ " \n",
588
+ " Args:\n",
589
+ " rank (int): Process rank\n",
590
+ " world_size (int): Total number of processes\n",
591
+ " \n",
592
+ " Returns:\n",
593
+ " tuple: (train_loader, val_loader, train_sampler, val_sampler)\n",
594
+ " \"\"\"\n",
595
+ " # Create datasets\n",
596
+ " train_dataset, val_dataset = create_dataset(\"train\"), create_dataset(\"val\")\n",
597
+ " \n",
598
+ " # Configure distributed samplers if needed\n",
599
+ " train_sampler = DistributedSampler(train_dataset, num_replicas=world_size, rank=rank) if world_size > 1 else None\n",
600
+ " val_sampler = DistributedSampler(val_dataset, num_replicas=world_size, rank=rank) if world_size > 1 else None\n",
601
+ " \n",
602
+ " # Create dataloaders\n",
603
+ " return (\n",
604
+ " DataLoader(train_dataset, batch_size=BATCH_SIZE, sampler=train_sampler, shuffle=train_sampler is None),\n",
605
+ " DataLoader(val_dataset, batch_size=BATCH_SIZE, sampler=val_sampler, shuffle=False),\n",
606
+ " train_sampler, val_sampler\n",
607
+ " )\n",
608
+ "\n",
609
+ "def create_model(device, rank, world_size):\n",
610
+ " \"\"\"\n",
611
+ " Create and configure DoMINO model with distributed training support.\n",
612
+ " \n",
613
+ " Args:\n",
614
+ " device (torch.device): Computation device\n",
615
+ " rank (int): Process rank\n",
616
+ " world_size (int): Total number of processes\n",
617
+ " \n",
618
+ " Returns:\n",
619
+ " DoMINO: Configured model (wrapped in DistributedDataParallel if world_size > 1)\n",
620
+ " \"\"\"\n",
621
+ "\n",
622
+ " \n",
623
+ " # Initialize model with configuration\n",
624
+ " model = DoMINO(\n",
625
+ " input_features=3,\n",
626
+ " output_features_vol=None,\n",
627
+ " output_features_surf=NUM_SURF_VARS,\n",
628
+ " model_parameters=SimpleNamespace(\n",
629
+ " interp_res=GRID_RESOLUTION,\n",
630
+ " surface_neighbors=NUM_SURFACE_NEIGHBORS,\n",
631
+ " use_surface_normals=True,\n",
632
+ " use_surface_area=True,\n",
633
+ " encode_parameters=True,\n",
634
+ " positional_encoding=False,\n",
635
+ " integral_loss_scaling_factor=INTEGRAL_LOSS_SCALING,\n",
636
+ " normalization=NORMALIZATION,\n",
637
+ " use_sdf_in_basis_func=True,\n",
638
+ " geometry_encoding_type= GEOMETRY_ENCODING_TYPE, # geometry encoder type, sdf, stl, both\n",
639
+ " geometry_rep=GEOMETRY_REP,\n",
640
+ " nn_basis_functions=SimpleNamespace(base_layer=512, fourier_features=False, num_modes=5),\n",
641
+ " parameter_model=SimpleNamespace(base_layer=512, scaling_params=[30.0, 1.226], fourier_features=False, num_modes=5),\n",
642
+ " position_encoder=SimpleNamespace(base_neurons=512),\n",
643
+ " geometry_local=GEOMETRY_LOCAL,\n",
644
+ " aggregation_model=SimpleNamespace(base_layer=512),\n",
645
+ " model_type=MODEL_TYPE\n",
646
+ " ),\n",
647
+ " ).to(device)\n",
648
+ " \n",
649
+ " # Wrap model for distributed training if needed\n",
650
+ " if world_size > 1:\n",
651
+ " model = DistributedDataParallel(\n",
652
+ " model, \n",
653
+ " device_ids=[rank], \n",
654
+ " output_device=rank, \n",
655
+ " find_unused_parameters=True\n",
656
+ " )\n",
657
+ " \n",
658
+ " return model\n",
659
+ "\n",
660
+ "def run_epoch(train_loader, val_loader, model, optimizer, scaler, device, epoch, best_vloss, rank, world_size):\n",
661
+ " \"\"\"\n",
662
+ " Run one training epoch with validation.\n",
663
+ " \n",
664
+ " Args:\n",
665
+ " train_loader (DataLoader): Training data loader\n",
666
+ " val_loader (DataLoader): Validation data loader\n",
667
+ " model (DoMINO): Model to train\n",
668
+ " optimizer (torch.optim.Optimizer): Optimizer\n",
669
+ " scaler (GradScaler): Gradient scaler for mixed precision\n",
670
+ " device (torch.device): Computation device\n",
671
+ " epoch (int): Current epoch number\n",
672
+ " best_vloss (float): Best validation loss so far\n",
673
+ " rank (int): Process rank\n",
674
+ " world_size (int): Total number of processes\n",
675
+ " \n",
676
+ " Returns:\n",
677
+ " float: Validation loss for this epoch\n",
678
+ " \"\"\"\n",
679
+ " # Training phase\n",
680
+ " model.train()\n",
681
+ " train_loss = 0.0\n",
682
+ " pbar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{NUM_EPOCHS}\") if rank == 0 else train_loader\n",
683
+ " \n",
684
+ " for batch in pbar:\n",
685
+ " # Move batch to device\n",
686
+ " batch = dict_to_device(batch, device)\n",
687
+ " \n",
688
+ " # Forward pass with mixed precision\n",
689
+ " with autocast():\n",
690
+ " _, pred_surf = model(batch)\n",
691
+ " loss = mse_loss_fn(pred_surf, batch[\"surface_fields\"])\n",
692
+ " \n",
693
+ " # Backward pass with gradient scaling\n",
694
+ " scaler.scale(loss).backward()\n",
695
+ " scaler.step(optimizer)\n",
696
+ " scaler.update()\n",
697
+ " optimizer.zero_grad()\n",
698
+ " \n",
699
+ " # Update loss tracking\n",
700
+ " train_loss += loss.item()\n",
701
+ " if rank == 0:\n",
702
+ " pbar.set_postfix({\n",
703
+ " \"train_loss\": f\"{train_loss/(pbar.n+1):.5e}\", \n",
704
+ " \"lr\": f\"{optimizer.param_groups[0]['lr']:.2e}\"\n",
705
+ " })\n",
706
+ " \n",
707
+ " # Compute average training loss\n",
708
+ " avg_train_loss = train_loss / len(train_loader)\n",
709
+ " \n",
710
+ " # Validation phase\n",
711
+ " model.eval()\n",
712
+ " with torch.no_grad():\n",
713
+ " val_loss = sum(\n",
714
+ " mse_loss_fn(model(dict_to_device(batch, device))[1], batch[\"surface_fields\"].to(device)).item() \n",
715
+ " for batch in val_loader\n",
716
+ " ) / len(val_loader)\n",
717
+ " \n",
718
+ " # Handle distributed training metrics\n",
719
+ " if world_size > 1:\n",
720
+ " avg_train_loss, val_loss = [torch.tensor(v, device=device) for v in [avg_train_loss, val_loss]]\n",
721
+ " torch.distributed.all_reduce(avg_train_loss, op=torch.distributed.ReduceOp.SUM)\n",
722
+ " torch.distributed.all_reduce(val_loss, op=torch.distributed.ReduceOp.SUM)\n",
723
+ " avg_train_loss, val_loss = avg_train_loss.item() / world_size, val_loss.item() / world_size\n",
724
+ " \n",
725
+ " # Save checkpoints on main process\n",
726
+ " if rank == 0:\n",
727
+ " if val_loss < best_vloss:\n",
728
+ " save_checkpoint(\n",
729
+ " os.path.join(MODEL_SAVE_DIR, \"best_model\"), \n",
730
+ " models=model,\n",
731
+ " optimizer=optimizer,\n",
732
+ " scaler=scaler\n",
733
+ " )\n",
734
+ "\n",
735
+ " if (epoch + 1) % CHECKPOINT_INTERVAL == 0:\n",
736
+ " save_checkpoint(\n",
737
+ " MODEL_SAVE_DIR, \n",
738
+ " models=model, \n",
739
+ " optimizer=optimizer, \n",
740
+ " scaler=scaler, \n",
741
+ " epoch=epoch\n",
742
+ " )\n",
743
+ " \n",
744
+ " return val_loss\n",
745
+ "\n",
746
+ "def train(model, device, rank, world_size):\n",
747
+ " \"\"\"\n",
748
+ " Function that orchestrates the training process.\n",
749
+ " Handles distributed training setup, model creation and training loop.\n",
750
+ " \"\"\"\n",
751
+ "\n",
752
+ "\n",
753
+ " # Create output directory on main process\n",
754
+ " os.makedirs(MODEL_SAVE_DIR, exist_ok=True) if rank == 0 else None\n",
755
+ " \n",
756
+ " # Set up data\n",
757
+ " train_loader, val_loader, train_sampler, val_sampler = create_dataloaders(rank, world_size)\n",
758
+ "\n",
759
+ " optimizer = apex.optimizers.FusedAdam(model.parameters(), lr=0.001)\n",
760
+ " \n",
761
+ " # Initialize learning rate scheduler and gradient scaler\n",
762
+ " #scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones=[1, 2], gamma=0.5)\n",
763
+ " scaler = GradScaler()\n",
764
+ " \n",
765
+ " # Training loop\n",
766
+ " best_vloss = float('inf')\n",
767
+ " for epoch in range(NUM_EPOCHS):\n",
768
+ " if world_size > 1:\n",
769
+ " train_sampler.set_epoch(epoch)\n",
770
+ " best_vloss = min(\n",
771
+ " best_vloss, \n",
772
+ " run_epoch(\n",
773
+ " train_loader, val_loader, model, optimizer, \n",
774
+ " scaler, device, epoch, best_vloss, rank, world_size\n",
775
+ " )\n",
776
+ " )\n",
777
+ " #scheduler.step()"
778
+ ]
779
+ },
780
+ {
781
+ "cell_type": "markdown",
782
+ "metadata": {},
783
+ "source": [
784
+ "Lets run the train for few epochs:"
785
+ ]
786
+ },
787
+ {
788
+ "cell_type": "code",
789
+ "execution_count": null,
790
+ "metadata": {},
791
+ "outputs": [],
792
+ "source": [
793
+ "# Initialize distributed training\n",
794
+ "os.environ[\"RANK\"] = f\"0\"\n",
795
+ "os.environ[\"WORLD_SIZE\"] = f\"1\"\n",
796
+ "os.environ[\"MASTER_ADDR\"] = \"localhost\"\n",
797
+ "os.environ[\"MASTER_PORT\"] = str(12355)\n",
798
+ "os.environ[\"LOCAL_RANK\"] = f\"0\"\n",
799
+ "\n",
800
+ "\n",
801
+ "DistributedManager.initialize()\n",
802
+ "dist = DistributedManager()\n",
803
+ "device=dist.device\n",
804
+ "rank=dist.rank\n",
805
+ "world_size=dist.world_size\n",
806
+ "print(device)\n",
807
+ "# Set up model\n",
808
+ "model = create_model(device, rank, world_size)\n",
809
+ "# Run training\n",
810
+ "train(model, device, rank, world_size)"
811
+ ]
812
+ },
813
+ {
814
+ "cell_type": "markdown",
815
+ "metadata": {},
816
+ "source": [
817
+ "## **Load Model Checkpoint & Run Inference**\n",
818
+ "\n",
819
+ "The sixth step in our workflow focuses on evaluating our trained DoMINO model by loading the best checkpoint and running inference on sample cases. \n",
820
+ "To run the inference, the script needs several key **inputs**. It requires the 3D shape of the object defined in an STL file and a corresponding surface mesh provided as a VTP file, which also contain results from a traditional simulation for comparison purposes. \n",
821
+ "Crucially, it needs the pre-trained DoMINO AI model loaded from a checkpoint file. Additionally, basic flow conditions like air speed (`STREAM_VELOCITY`) and density (`AIR_DENSITY`), along with specific scaling factors saved from the training phase (used to convert model outputs to physical values), must be provided.\n",
822
+ "\n",
823
+ "\n",
824
+ "As its main **output**, the script generates new VTP files for each tested geometry. These files include the original surface mesh data but are augmented with new data fields representing the AI model's predictions for aerodynamic quantities such as surface pressure and wall shear stress. Furthermore, the script calculates aerodynamic forces based on these predictions and prints a comparison against forces derived from reference data directly to the console.\n",
825
+ "The code snippet below takes geometry files (STL) and corresponding simulation setup data (partially from VTP files and config parameters), preprocesses them, feeds them into the model to predict aerodynamic quantities (like surface pressure and shear stress), and saves these predictions back into VTP files for analysis and visualization.\n",
826
+ "\n",
827
+ "#### Understanding the Testing Process\n",
828
+ "\n",
829
+ "The testing process involves several key components:\n",
830
+ "1. Loading the best model checkpoint\n",
831
+ "2. Preparing test data\n",
832
+ "3. Running inference on test cases\n",
833
+ "4. Analyzing prediction results\n",
834
+ "5. Comparing with ground truth values\n",
835
+ "\n",
836
+ "#### Key Components and Libraries\n",
837
+ "\n",
838
+ "We'll use the following libraries for testing:\n",
839
+ "\n",
840
+ "1. **PyTorch**\n",
841
+ " - `torch.load()`: For loading model checkpoints\n",
842
+ " - `model.load_state_dict()`: For restoring model weights\n",
843
+ " - `torch.no_grad()`: For efficient inference\n",
844
+ "\n",
845
+ "2. **Custom Testing Functions**\n",
846
+ " - `test_step()`: For running inference on test cases\n",
847
+ " - Data processing utilities for test data preparation\n",
848
+ "\n",
849
+ "#### Implementation Overview\n",
850
+ "\n",
851
+ "The testing is implemented through several key components:\n",
852
+ "\n",
853
+ "1. **Function test_step**\n",
854
+ " Within the **test_step** function, several key operations execute in sequence. Initially, torch.no_grad() is used to disable gradient tracking in PyTorch, optimizing performance by saving memory and computation time as gradients are unnecessary during inference. \n",
855
+ "Next, the necessary data is prepared by extracting inputs like air density, stream velocity, geometry coordinates, bounding box grid information (surf_grid), and the Signed Distance Field (SDF) from the data_dict; the SDF is particularly important as it helps the model understand the position of points relative to the geometry surface. \n",
856
+ "Following this, a global geometry encoding is generated using model.geo_rep_surface, which takes normalized geometry points, the grid, and the SDF to create a comprehensive representation of the overall shape. \n",
857
+ "Surface-specific data, including mesh points, normals, areas, and neighbor details (found via methods like KDTree during preprocessing), are then extracted. \n",
858
+ "To refine the focus, model.geo_encoding_local_surface extracts relevant local geometric features from the global encoding specifically for the surface points where predictions are needed. \n",
859
+ "Positional awareness is added using model.position_encoder to encode the relative location of surface points. \n",
860
+ "The core prediction then occurs via model.calculate_solution_with_neighbors, combining local geometry, positional encoding, surface point details, neighbor information, and flow conditions to estimate the target surface fields like pressure coefficient or wall shear stress. Since the model output is normalized, a final un-normalization step converts these predictions back into physical units using the provided surf_factors, stream velocity, and air density. The function concludes by returning the predicted surface fields (prediction_surf), as this specific code path concentrates only on surface predictions.\n",
861
+ "\n",
862
+ "\n",
863
+ "\n",
864
+ "```python\n",
865
+ "def test_step(model, data_dict, surf_factors, device):\n",
866
+ " \"\"\"\n",
867
+ " Executes the core inference logic for a single test case using the trained DoMINO model.\n",
868
+ " \n",
869
+ " Args:\n",
870
+ " model (DoMINO): The trained model\n",
871
+ " data_dict: A dictionary containing all necessary input data for this specific test case (geometry, mesh points, flow conditions, etc.), already preprocessed and formatted.\n",
872
+ " surf_factors: Scaling factors used during training to normalize the target surface data. Needed here to un-normalize the model's predictions back to physical values.\n",
873
+ " device: The computational device (CPU or GPU) to run the calculations on.\n",
874
+ " \n",
875
+ " Returns:\n",
876
+ " tuple: (prediction_vol, prediction_surf) - Model predictions for volume and surface\n",
877
+ " \"\"\"\n",
878
+ "```\n",
879
+ "\n",
880
+ "\n",
881
+ "2. **Function: test**\n",
882
+ "```python\n",
883
+ "def test(model, test_dataloader, device):\n",
884
+ " \"\"\"\n",
885
+ " Run testing on the model using the provided test dataloader.\n",
886
+ " \n",
887
+ " Args:\n",
888
+ " model (DoMINO): The trained model\n",
889
+ " test_dataloader (DataLoader): DataLoader containing test data\n",
890
+ " device (torch.device): Device to run inference on\n",
891
+ " \n",
892
+ " Returns:\n",
893
+ " list: List of tuples containing (prediction_vol, prediction_surf) for each test case\n",
894
+ " \"\"\"\n",
895
+ "```\n",
896
+ "\n",
897
+ "On the other hand, the test function is a higher-level function that organizes and controls the overall testing process. It begins by checking if surface scaling factors have been pre-computed and stored in a .npy file. If the file exists, it loads these factors; if not, it defaults to None. \n",
898
+ "\n",
899
+ "**The function then loads a pre-trained model from a checkpoint file (DoMINO.0.....pt) and loads its state into the model**. \n",
900
+ "```python\n",
901
+ " # Load the best model checkpoint\n",
902
+ " best_checkpoint = torch.load(CHECKPOINT_DIR / \"best_model/DoMINO.0.401.pt\")\n",
903
+ " model.load_state_dict(best_checkpoint) # Load the model state\n",
904
+ " print(\"Model loaded\")\n",
905
+ "```\n",
906
+ "\n",
907
+ "After the model is loaded, it creates a directory for saving predictions if it doesn't already exist. The dirname parameter is used to extract a tag, which helps identify the current test case.\n",
908
+ "\n",
909
+ "Next, the function proceeds to load the necessary input files. It reads an STL file that contains the 3D geometry of the surface and extracts relevant data like vertices, faces, and areas. The bounding box dimensions are calculated, and the surface’s center of mass is computed. Then, it prepares a grid (surf_grid) and calculates the signed distance function (SDF) over this grid using the surface geometry, which helps in understanding the geometry’s proximity to the grid points. The function then reads the VTP file, which holds additional surface-related data such as pressure and shear force values.\n",
910
+ "\n",
911
+ "The surface fields are then prepared by interpolating the surface mesh data and its corresponding attributes. These fields are normalized to fit within the bounding box dimensions. The data dictionary is assembled, containing all the relevant inputs needed for the model’s prediction. This dictionary includes things like normalized surface coordinates, surface areas, and field values such as stream velocity and air density. The dictionary is converted to PyTorch tensors, making it compatible with the model.\n",
912
+ "\n",
913
+ "The test_step function is then called with this prepared data to compute the model's predictions. After the predictions are generated, the function compares the predicted surface forces (pressure and shear stress) with the true values from the surface fields. It calculates the predicted forces and prints out the comparison between the predicted and true values. The predicted surface fields are then converted to VTK format and saved to a file. Finally, the function finishes by returning, completing the testing process. This function provides a complete pipeline for testing a trained model on surface data, generating predictions, and saving them for further analysis.\n",
914
+ "\n",
915
+ "\n",
916
+ "Let's proceed with loading our trained model and running the tests:"
917
+ ]
918
+ },
919
+ {
920
+ "cell_type": "code",
921
+ "execution_count": null,
922
+ "metadata": {},
923
+ "outputs": [],
924
+ "source": [
925
+ "def test_step(model, data_dict, surf_factors, device):\n",
926
+ " \"\"\"\n",
927
+ " Run a single test step on the model.\n",
928
+ " \n",
929
+ " Args:\n",
930
+ " model (DoMINO): The trained model\n",
931
+ " data_dict (dict): Dictionary containing test data\n",
932
+ " device (torch.device): Device to run inference on\n",
933
+ " \n",
934
+ " Returns:\n",
935
+ " tuple: (prediction_vol, prediction_surf) - Model predictions for volume and surface\n",
936
+ " \"\"\"\n",
937
+ " \n",
938
+ " avg_tloss_vol = 0.0 # Placeholder for average volume loss (not currently used)\n",
939
+ " avg_tloss_surf = 0.0 # Placeholder for average surface loss (not currently used)\n",
940
+ "\n",
941
+ " with torch.no_grad(): # Disable gradient computation to save memory and computation during inference\n",
942
+ " # Move input data to the specified device (CPU or GPU)\n",
943
+ " data_dict = dict_to_device(data_dict, device)\n",
944
+ "\n",
945
+ " # Extract non-dimensionalization factors (important for scaling the inputs)\n",
946
+ " air_density = data_dict[\"air_density\"]\n",
947
+ " stream_velocity = data_dict[\"stream_velocity\"]\n",
948
+ " length_scale = data_dict[\"length_scale\"]\n",
949
+ "\n",
950
+ " # Extract geometry coordinates (nodes of the surface)\n",
951
+ " geo_centers = data_dict[\"geometry_coordinates\"]\n",
952
+ "\n",
953
+ " # Extract bounding box grid and signed distance function (SDF) grid for the surface\n",
954
+ " s_grid = data_dict[\"surf_grid\"]\n",
955
+ " sdf_surf_grid = data_dict[\"sdf_surf_grid\"]\n",
956
+ "\n",
957
+ " # Extract scaling factors for surface (used for un-normalization)\n",
958
+ " surf_max = data_dict[\"surface_min_max\"][:, 1]\n",
959
+ " surf_min = data_dict[\"surface_min_max\"][:, 0]\n",
960
+ "\n",
961
+ " # Normalize geometry coordinates to fit within a bounding box [-1, 1]\n",
962
+ " geo_centers_surf = (\n",
963
+ " 2.0 * (geo_centers - surf_min) / (surf_max - surf_min) - 1\n",
964
+ " )\n",
965
+ "\n",
966
+ " # Generate geometric representation of the surface\n",
967
+ " encoding_g_surf = model.geo_rep_surface(\n",
968
+ " geo_centers_surf, s_grid, sdf_surf_grid\n",
969
+ " )\n",
970
+ "\n",
971
+ " prediction_vol = None # Volume prediction is not computed in this function\n",
972
+ "\n",
973
+ " # Extract information about the surface: mesh centers, normals, areas, and neighbors\n",
974
+ " surface_mesh_centers = data_dict[\"surface_mesh_centers\"]\n",
975
+ " surface_normals = data_dict[\"surface_normals\"]\n",
976
+ " surface_areas = data_dict[\"surface_areas\"]\n",
977
+ "\n",
978
+ " surface_mesh_neighbors = data_dict[\"surface_mesh_neighbors\"]\n",
979
+ " surface_neighbors_normals = data_dict[\"surface_neighbors_normals\"]\n",
980
+ " surface_neighbors_areas = data_dict[\"surface_neighbors_areas\"]\n",
981
+ "\n",
982
+ " surface_areas = torch.unsqueeze(surface_areas, -1) # Add extra dimension\n",
983
+ " surface_neighbors_areas = torch.unsqueeze(surface_neighbors_areas, -1) # Add extra dimension\n",
984
+ " pos_surface_center_of_mass = data_dict[\"pos_surface_center_of_mass\"]\n",
985
+ " num_points = surface_mesh_centers.shape[1] # Number of surface points\n",
986
+ " \n",
987
+ " # Extract target surface fields (for comparison later)\n",
988
+ " target_surf = data_dict[\"surface_fields\"]\n",
989
+ " prediction_surf = np.zeros_like(target_surf.cpu().numpy()) # Initialize prediction array\n",
990
+ "\n",
991
+ " start_time = time.time() # Record the start time for performance measurement\n",
992
+ "\n",
993
+ " # Generate local geometric encoding for each surface point\n",
994
+ " geo_encoding_local = model.geo_encoding_local(\n",
995
+ " 0.5 * encoding_g_surf, surface_mesh_centers, s_grid, mode=\"surface\"\n",
996
+ " )\n",
997
+ "\n",
998
+ "\n",
999
+ " # Position encoding based on the center of mass of the surface\n",
1000
+ " pos_encoding = pos_surface_center_of_mass\n",
1001
+ " pos_encoding = model.position_encoder(pos_encoding, eval_mode=\"surface\")\n",
1002
+ "\n",
1003
+ " # Perform the model prediction using neighbors and other surface data\n",
1004
+ " tpredictions = (\n",
1005
+ " model.calculate_solution_with_neighbors(\n",
1006
+ " surface_mesh_centers,\n",
1007
+ " geo_encoding_local,\n",
1008
+ " pos_encoding,\n",
1009
+ " surface_mesh_neighbors,\n",
1010
+ " surface_normals,\n",
1011
+ " surface_neighbors_normals,\n",
1012
+ " surface_areas,\n",
1013
+ " surface_neighbors_areas,\n",
1014
+ " stream_velocity,\n",
1015
+ " air_density,\n",
1016
+ " )\n",
1017
+ " )\n",
1018
+ "\n",
1019
+ " # Convert model predictions to numpy arrays for further processing\n",
1020
+ " prediction_surf = tpredictions.cpu().numpy()\n",
1021
+ "\n",
1022
+ " # Unnormalize the surface predictions and scale them using physical quantities\n",
1023
+ " prediction_surf = (\n",
1024
+ " unnormalize(prediction_surf, surf_factors[0], surf_factors[1])\n",
1025
+ " * stream_velocity[0, 0].cpu().numpy() ** 2.0\n",
1026
+ " * air_density[0, 0].cpu().numpy()\n",
1027
+ " )\n",
1028
+ "\n",
1029
+ " return prediction_vol, prediction_surf # Return volume and surface predictions\n",
1030
+ "\n",
1031
+ "def test(filepath, dirname, CKPT_NUMBER: int=0):\n",
1032
+ " \"\"\"\n",
1033
+ " High-level function to manage the testing pipeline, including data preparation, model loading, and prediction saving.\n",
1034
+ " \n",
1035
+ " Args:\n",
1036
+ " filepath (str): Path to the test data directory\n",
1037
+ " dirname (str): Directory name for the test case\n",
1038
+ " \n",
1039
+ " Returns:\n",
1040
+ " None\n",
1041
+ " \"\"\"\n",
1042
+ " # Define names of surface variables to be predicted\n",
1043
+ " surface_variable_names = SURFACE_VARS\n",
1044
+ " \n",
1045
+ " # Check if surface scaling factors are available\n",
1046
+ " surf_save_path = os.path.join(\n",
1047
+ " \"outputs\", PROJECT_NAME , \"surface_scaling_factors.npy\"\n",
1048
+ " )\n",
1049
+ " if os.path.exists(surf_save_path):\n",
1050
+ " surf_factors = np.load(surf_save_path) # Load scaling factors if available\n",
1051
+ " else:\n",
1052
+ " surf_factors = None # If not available, set to None\n",
1053
+ " \n",
1054
+ " # Load the best model checkpoint\n",
1055
+ " best_checkpoint = torch.load(CHECKPOINT_DIR / f\"best_model/DoMINO.0.{CKPT_NUMBER}.pt\")\n",
1056
+ " model.load_state_dict(best_checkpoint) # Load the model state\n",
1057
+ " print(\"Model loaded\")\n",
1058
+ " \n",
1059
+ " # Set the path to save predictions\n",
1060
+ " pred_save_path = SAVE_PATH\n",
1061
+ " create_directory(pred_save_path) # Create the output directory if it doesn't exist\n",
1062
+ " \n",
1063
+ " # Extract test case identifier from the directory name\n",
1064
+ " tag = int(re.findall(r\"(\\w+?)(\\d+)\", dirname)[0][1])\n",
1065
+ " vtp_path = filepath # Path to the VTP file with surface data\n",
1066
+ " \n",
1067
+ " # Prepare the path to save predicted results\n",
1068
+ " vtp_pred_save_path = os.path.join(\n",
1069
+ " pred_save_path, f\"boundary_{tag}_predicted.vtp\"\n",
1070
+ " )\n",
1071
+ " \n",
1072
+ " # Load the STL file for the geometry\n",
1073
+ " path_stl = Path(filepath)\n",
1074
+ " stl_path = path_stl.parent.parent.joinpath(\"test_stl_files\", path_stl.stem + \".stl\")\n",
1075
+ " print(\"stl_path::\", stl_path)\n",
1076
+ " print(\"filepath::\", filepath)\n",
1077
+ "\n",
1078
+ "\n",
1079
+ " info_path = path_stl.with_name(path_stl.stem + \".txt\").parent.parent.joinpath(\"test_info\", path_stl.stem + \"_info.txt\")\n",
1080
+ " print(\"info_path:::\", info_path)\n",
1081
+ "\n",
1082
+ " with open(info_path, \"r\") as file:\n",
1083
+ " for line in file:\n",
1084
+ " #print(\"line::\",line)\n",
1085
+ " if \"Velocity\" in line:\n",
1086
+ " velocity = float(line.split(\":\")[1].strip())\n",
1087
+ " print(f\"Velocity: {velocity}\")\n",
1088
+ "\n",
1089
+ " STREAM_VELOCITY = velocity\n",
1090
+ " \n",
1091
+ " # Read and process the STL file\n",
1092
+ " reader = pv.get_reader(stl_path)\n",
1093
+ " mesh_stl = reader.read()\n",
1094
+ " stl_vertices = mesh_stl.points\n",
1095
+ " stl_faces = np.array(mesh_stl.faces).reshape((-1, 4))[:, 1:] # Extract triangular faces\n",
1096
+ " mesh_indices_flattened = stl_faces.flatten()\n",
1097
+ " length_scale = np.amax(np.amax(stl_vertices, 0) - np.amin(stl_vertices, 0)) # Compute scale of the geometry\n",
1098
+ " stl_sizes = mesh_stl.compute_cell_sizes(length=False, area=True, volume=False)\n",
1099
+ " stl_sizes = np.array(stl_sizes.cell_data[\"Area\"], dtype=np.float32)\n",
1100
+ " stl_centers = np.array(mesh_stl.cell_centers().points, dtype=np.float32)\n",
1101
+ " \n",
1102
+ " # Calculate the center of mass of the surface\n",
1103
+ " center_of_mass = calculate_center_of_mass(stl_centers, stl_sizes)\n",
1104
+ " \n",
1105
+ " # Extract bounding box dimensions for the surface\n",
1106
+ " bounding_box_dims_surf = []\n",
1107
+ " bounding_box_dims_surf.append(np.asarray(BOUNDING_BOX_SURF.max))\n",
1108
+ " bounding_box_dims_surf.append(np.asarray(BOUNDING_BOX_SURF.min))\n",
1109
+ " s_max = np.float32(bounding_box_dims_surf[0])\n",
1110
+ " s_min = np.float32(bounding_box_dims_surf[1])\n",
1111
+ " \n",
1112
+ " # Create a 3D grid for the surface\n",
1113
+ " nx, ny, nz = GRID_RESOLUTION\n",
1114
+ " surf_grid = create_grid(s_max, s_min, [nx, ny, nz])\n",
1115
+ " surf_grid_reshaped = surf_grid.reshape(nx * ny * nz, 3)\n",
1116
+ " \n",
1117
+ " # Compute the Signed Distance Field (SDF) on the surface grid\n",
1118
+ " sdf_surf_grid = (\n",
1119
+ " signed_distance_field(\n",
1120
+ " stl_vertices,\n",
1121
+ " mesh_indices_flattened,\n",
1122
+ " surf_grid_reshaped,\n",
1123
+ " use_sign_winding_number=True,\n",
1124
+ " )\n",
1125
+ " .reshape(nx, ny, nz)\n",
1126
+ " )\n",
1127
+ " surf_grid = np.float32(surf_grid)\n",
1128
+ " sdf_surf_grid = np.float32(sdf_surf_grid)\n",
1129
+ " surf_grid_max_min = np.float32(np.asarray([s_min, s_max]))\n",
1130
+ " \n",
1131
+ " # Read the VTP file containing surface data\n",
1132
+ " reader = vtk.vtkXMLPolyDataReader()\n",
1133
+ " reader.SetFileName(vtp_path)\n",
1134
+ " reader.Update()\n",
1135
+ " polydata_surf = reader.GetOutput()\n",
1136
+ " celldata_all = get_node_to_elem(polydata_surf)\n",
1137
+ " celldata = celldata_all.GetCellData()\n",
1138
+ " surface_fields = get_fields(celldata, surface_variable_names)\n",
1139
+ " surface_fields = np.concatenate(surface_fields, axis=-1)\n",
1140
+ " mesh = pv.PolyData(polydata_surf)\n",
1141
+ " \n",
1142
+ " # Extract surface mesh coordinates, neighbors, and normals\n",
1143
+ " surface_coordinates = np.array(mesh.cell_centers().points, dtype=np.float32)\n",
1144
+ " interp_func = KDTree(surface_coordinates)\n",
1145
+ " dd, ii = interp_func.query(surface_coordinates, k=NUM_SURFACE_NEIGHBORS)\n",
1146
+ " surface_neighbors = surface_coordinates[ii]\n",
1147
+ " surface_neighbors = surface_neighbors[:, 1:]\n",
1148
+ " surface_normals = np.array(mesh.cell_normals, dtype=np.float32)\n",
1149
+ " surface_sizes = mesh.compute_cell_sizes(length=False, area=True, volume=False)\n",
1150
+ " surface_sizes = np.array(surface_sizes.cell_data[\"Area\"], dtype=np.float32)\n",
1151
+ " \n",
1152
+ " # Normalize the surface normals and neighbors\n",
1153
+ " surface_normals = (\n",
1154
+ " surface_normals / np.linalg.norm(surface_normals, axis=1)[:, np.newaxis]\n",
1155
+ " )\n",
1156
+ " surface_neighbors_normals = surface_normals[ii]\n",
1157
+ " surface_neighbors_normals = surface_neighbors_normals[:, 1:]\n",
1158
+ " surface_neighbors_sizes = surface_sizes[ii]\n",
1159
+ " surface_neighbors_sizes = surface_neighbors_sizes[:, 1:]\n",
1160
+ " \n",
1161
+ " # Calculate the grid resolution and normalize the surface data\n",
1162
+ " dx, dy, dz = (\n",
1163
+ " (s_max[0] - s_min[0]) / nx,\n",
1164
+ " (s_max[1] - s_min[1]) / ny,\n",
1165
+ " (s_max[2] - s_min[2]) / nz,\n",
1166
+ " )\n",
1167
+ " pos_surface_center_of_mass = surface_coordinates - center_of_mass\n",
1168
+ " surface_coordinates = normalize(surface_coordinates, s_max, s_min)\n",
1169
+ " surface_neighbors = normalize(surface_neighbors, s_max, s_min)\n",
1170
+ " surf_grid = normalize(surf_grid, s_max, s_min)\n",
1171
+ " \n",
1172
+ " # Prepare the data dictionary for model input\n",
1173
+ " geom_centers = np.float32(stl_vertices)\n",
1174
+ " data_dict = {\n",
1175
+ " \"pos_surface_center_of_mass\": np.float32(pos_surface_center_of_mass),\n",
1176
+ " \"geometry_coordinates\": np.float32(geom_centers),\n",
1177
+ " \"surf_grid\": np.float32(surf_grid),\n",
1178
+ " \"sdf_surf_grid\": np.float32(sdf_surf_grid),\n",
1179
+ " \"surface_mesh_centers\": np.float32(surface_coordinates),\n",
1180
+ " \"surface_mesh_neighbors\": np.float32(surface_neighbors),\n",
1181
+ " \"surface_normals\": np.float32(surface_normals),\n",
1182
+ " \"surface_neighbors_normals\": np.float32(surface_neighbors_normals),\n",
1183
+ " \"surface_areas\": np.float32(surface_sizes),\n",
1184
+ " \"surface_neighbors_areas\": np.float32(surface_neighbors_sizes),\n",
1185
+ " \"surface_fields\": np.float32(surface_fields),\n",
1186
+ " \"surface_min_max\": np.float32(surf_grid_max_min),\n",
1187
+ " \"length_scale\": np.array(length_scale, dtype=np.float32),\n",
1188
+ " \"stream_velocity\": np.expand_dims(\n",
1189
+ " np.array(STREAM_VELOCITY, dtype=np.float32), axis=-1\n",
1190
+ " ),\n",
1191
+ " \"air_density\": np.expand_dims(\n",
1192
+ " np.array(AIR_DENSITY, dtype=np.float32), axis=-1\n",
1193
+ " ),\n",
1194
+ " }\n",
1195
+ " \n",
1196
+ " # Convert data dictionary to PyTorch tensors\n",
1197
+ " data_dict = {\n",
1198
+ " key: torch.from_numpy(np.expand_dims(np.float32(value), 0))\n",
1199
+ " for key, value in data_dict.items()\n",
1200
+ " }\n",
1201
+ " \n",
1202
+ " # Perform a test step to get the predictions\n",
1203
+ " prediction_vol, prediction_surf = test_step(\n",
1204
+ " model, data_dict, surf_factors, device\n",
1205
+ " )\n",
1206
+ " \n",
1207
+ " # Process the predicted and true surface values to compute forces\n",
1208
+ " surface_sizes = np.expand_dims(surface_sizes, -1)\n",
1209
+ " pres_x_pred = np.sum(\n",
1210
+ " prediction_surf[0, :, 0] * surface_normals[:, 0] * surface_sizes[:, 0]\n",
1211
+ " )\n",
1212
+ " shear_x_pred = np.sum(prediction_surf[0, :, 1] * surface_sizes[:, 0])\n",
1213
+ " pres_x_true = np.sum(\n",
1214
+ " surface_fields[:, 0] * surface_normals[:, 0] * surface_sizes[:, 0]\n",
1215
+ " )\n",
1216
+ " shear_x_true = np.sum(surface_fields[:, 1] * surface_sizes[:, 0])\n",
1217
+ " force_x_pred = np.sum(\n",
1218
+ " prediction_surf[0, :, 0] * surface_normals[:, 0] * surface_sizes[:, 0]\n",
1219
+ " - prediction_surf[0, :, 1] * surface_sizes[:, 0]\n",
1220
+ " )\n",
1221
+ " force_x_true = np.sum(\n",
1222
+ " surface_fields[:, 0] * surface_normals[:, 0] * surface_sizes[:, 0]\n",
1223
+ " - surface_fields[:, 1] * surface_sizes[:, 0]\n",
1224
+ " )\n",
1225
+ " \n",
1226
+ " # Print the computed forces for comparison\n",
1227
+ " print(dirname, force_x_pred, force_x_true)\n",
1228
+ " \n",
1229
+ " # Convert predictions to VTK format and save the results\n",
1230
+ " surfParam_vtk = numpy_support.numpy_to_vtk(prediction_surf[0, :, 0:1])\n",
1231
+ " surfParam_vtk.SetName(f\"{surface_variable_names[0]}Pred\")\n",
1232
+ " celldata_all.GetCellData().AddArray(surfParam_vtk)\n",
1233
+ " surfParam_vtk = numpy_support.numpy_to_vtk(prediction_surf[0, :, 1:])\n",
1234
+ " surfParam_vtk.SetName(f\"{surface_variable_names[1]}Pred\")\n",
1235
+ " celldata_all.GetCellData().AddArray(surfParam_vtk)\n",
1236
+ " write_to_vtp(celldata_all, vtp_pred_save_path) # Save to VTP file\n",
1237
+ " \n",
1238
+ " return # End of the test function"
1239
+ ]
1240
+ },
1241
+ {
1242
+ "cell_type": "code",
1243
+ "execution_count": null,
1244
+ "metadata": {},
1245
+ "outputs": [],
1246
+ "source": [
1247
+ "input_path = DATA_PATHS[\"test\"]\n",
1248
+ "\n",
1249
+ "CHECKPOINT_NUMBER = 1\n",
1250
+ "\n",
1251
+ "dirnames = get_filenames(input_path)\n",
1252
+ "\n",
1253
+ "for count, dirname in enumerate(dirnames):\n",
1254
+ " print(f\"Processing file {dirname}\")\n",
1255
+ " filepath = os.path.join(input_path, dirname)\n",
1256
+ " print(\"filepath::\",filepath)\n",
1257
+ " test(filepath, dirname, CKPT_NUMBER=CHECKPOINT_NUMBER)\n",
1258
+ "\n",
1259
+ "folder = Path(SAVE_PATH)\n",
1260
+ "predcited_files = list(folder.glob(\"*.vtp\"))"
1261
+ ]
1262
+ },
1263
+ {
1264
+ "cell_type": "markdown",
1265
+ "metadata": {},
1266
+ "source": [
1267
+ "## **Visualizing the predicted results**\n",
1268
+ "You can Visualize the predicted surface pressure using either PyVista or ParaView. In the following, use we `pyvista` and display both the predicted and ground truth pressure values, which are stored in .vtp files located in the SAVE_PATH directory."
1269
+ ]
1270
+ },
1271
+ {
1272
+ "cell_type": "code",
1273
+ "execution_count": null,
1274
+ "metadata": {},
1275
+ "outputs": [],
1276
+ "source": [
1277
+ "##### import pyvista as pv\n",
1278
+ "\n",
1279
+ "# Start virtual framebuffer for off-screen rendering (useful in Jupyter/containers)\n",
1280
+ "pv.start_xvfb()\n",
1281
+ "\n",
1282
+ "# Read the VTP mesh\n",
1283
+ "mesh = pv.read(f\"{DATA_DIR}/mesh_predictions_surf_final1/boundary_119_predicted.vtp\")\n",
1284
+ "print(\"Available cell data keys:\", mesh.cell_data.keys())\n",
1285
+ "\n",
1286
+ "# Create a Plotter with 2 vertical subplots\n",
1287
+ "plotter = pv.Plotter(shape=(2, 1), window_size=[1600, 800], off_screen=True)\n",
1288
+ "\n",
1289
+ "# Plot 'p' (ground truth or reference)\n",
1290
+ "plotter.subplot(0, 0)\n",
1291
+ "plotter.add_text(\"Pressure (p)\", font_size=12)\n",
1292
+ "plotter.add_mesh(mesh, scalars=\"p\", show_edges=False)\n",
1293
+ "\n",
1294
+ "# Plot 'pPred' (predicted pressure)\n",
1295
+ "plotter.subplot(1, 0)\n",
1296
+ "plotter.add_text(\"Predicted Pressure (pPred)\", font_size=12)\n",
1297
+ "plotter.add_mesh(mesh, scalars=\"pPred\", show_edges=False)\n",
1298
+ "\n",
1299
+ "# Show both subplots\n",
1300
+ "plotter.show(jupyter_backend='static')"
1301
+ ]
1302
+ },
1303
+ {
1304
+ "cell_type": "markdown",
1305
+ "metadata": {},
1306
+ "source": [
1307
+ "## Conclusion\n",
1308
+ "This notebook successfully demonstrates the complete workflow for training and deploying the DoMINO (Decomposable Multiscale Iterative Neural Operator) model on automotive aerodynamics data. Through comprehensive exploration of DoMINO's sophisticated architecture—which combines global and local geometric representations with a DeepONet-inspired aggregation network—we achieved successful training using distributed training capabilities with mixed precision optimization. The model learned to predict surface pressure and wall shear stress fields from the Ahmed body dataset, demonstrating the effectiveness of its multi-scale geometric encoding approach that processes STL geometries directly without requiring mesh generation. Post-training evaluation revealed the model's capability to generalize to new geometries, generating predictions that can be directly compared against traditional CFD simulations, while the force calculations and aerodynamic coefficient predictions provide quantitative validation of the model's accuracy. The successful implementation offers significant potential for rapid aerodynamic analysis during vehicle design iterations, real-time flow field predictions for design optimization, and reduced computational costs compared to traditional CFD simulations. By combining geometric understanding with deep learning, DoMINO represents a promising step toward more efficient and accessible computational fluid dynamics tools that could revolutionize how we approach aerodynamic simulations in automotive engineering."
1309
+ ]
1310
+ },
1311
+ {
1312
+ "cell_type": "code",
1313
+ "execution_count": null,
1314
+ "metadata": {},
1315
+ "outputs": [],
1316
+ "source": [
1317
+ "import os\n",
1318
+ "os._exit(00)"
1319
+ ]
1320
+ }
1321
+ ],
1322
+ "metadata": {
1323
+ "kernelspec": {
1324
+ "display_name": "Python 3 (ipykernel)",
1325
+ "language": "python",
1326
+ "name": "python3"
1327
+ },
1328
+ "language_info": {
1329
+ "codemirror_mode": {
1330
+ "name": "ipython",
1331
+ "version": 3
1332
+ },
1333
+ "file_extension": ".py",
1334
+ "mimetype": "text/x-python",
1335
+ "name": "python",
1336
+ "nbconvert_exporter": "python",
1337
+ "pygments_lexer": "ipython3",
1338
+ "version": "3.12.3"
1339
+ }
1340
+ },
1341
+ "nbformat": 4,
1342
+ "nbformat_minor": 4
1343
+ }
introduction.ipynb ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "528a57dc",
6
+ "metadata": {},
7
+ "source": [
8
+ "## Introduction\n",
9
+ "\n",
10
+ "Participants will work with the **Ahmed body** benchmark — a canonical problem in automotive aerodynamics — to build a surrogate model. The course blends classical CFD data handling with modern AI-based modeling. \n",
11
+ "For the sake of education here we will use only **Ahmed Body Surface data** for training and not volume data. "
12
+ ]
13
+ },
14
+ {
15
+ "cell_type": "markdown",
16
+ "id": "d61bf370",
17
+ "metadata": {},
18
+ "source": [
19
+ "## Course Modules\n",
20
+ "\n",
21
+ "### 0. [Dataset download](#download-ahmed-body-surface-dataset)\n",
22
+ "\n",
23
+ "### 1. [Preprocessing Ahmed body dataset for training with DoMINO](domino-data-preprocessing.ipynb)\n",
24
+ "- Loading and understanding VTK simulation data\n",
25
+ "- Data normalization techniques for ML\n",
26
+ "- Extracting key physical quantities\n",
27
+ "- Creating standardized datasets for training\n",
28
+ "\n",
29
+ "### 2. [Training and Inferencing DoMINO Model](domino-training-test.ipynb)\n",
30
+ "- Understanding DoMINO architecture and physics-informed learning\n",
31
+ "- Training process\n",
32
+ "- Load Model Checkpoint & Run Inference\n",
33
+ "- Visualizing the predicted results\n",
34
+ "\n",
35
+ "### Hardware Requirements\n",
36
+ "- NVIDIA GPU with CUDA support\n",
37
+ "- 4GB+ RAM\n",
38
+ "- 100GB+ disk space\n",
39
+ "\n",
40
+ "### Software Requirements\n",
41
+ "- Docker with NVIDIA Container Toolkit\n",
42
+ "- NVIDIA Drivers (version 545 or later)\n",
43
+ "- SLURM workload manager"
44
+ ]
45
+ },
46
+ {
47
+ "cell_type": "markdown",
48
+ "id": "8643a4d3",
49
+ "metadata": {},
50
+ "source": [
51
+ "## Quick Start Guide\n",
52
+ "\n",
53
+ "### Course Environment\n",
54
+ "This course is set up to use the the PhysicsNeMo docker environment from NVIDIA NGC, and all other additional dependencies are already installed. If you wish to set up the environment outside of this space, the following steps can be used, but note, they are not necessary for this HuggingFace Space.\n",
55
+ "\n",
56
+ "### Required Setup Steps\n",
57
+ "The only required step is to pull the dataset from NVIDIA NGC.\n",
58
+ "\n",
59
+ "#### Download Ahmed Body surface dataset\n",
60
+ "\n",
61
+ "The complete Ahmed body surface dataset is hosted on NGC and accessible from the following link:\n",
62
+ "\n",
63
+ "https://catalog.ngc.nvidia.com/orgs/nvidia/teams/physicsnemo/resources/physicsnemo_ahmed_body_dataset\n",
64
+ "\n",
65
+ "The data can be downloaded using the NVIDIA NGC cli. Please note that the dataset contains VTP files, but training DoMINO and X-MeshGraphNet also requires STL files. Therefore, in the `domino-data-preprocessing.ipynb` notebook, STL files are extracted from the available VTP data.\n",
66
+ "\n",
67
+ "The data should be available in this space already at \"/data/physicsnemo_ahmed_body_dataset_vv1\". If it is not, we can download it again:"
68
+ ]
69
+ },
70
+ {
71
+ "cell_type": "code",
72
+ "execution_count": null,
73
+ "id": "99acc658",
74
+ "metadata": {
75
+ "vscode": {
76
+ "languageId": "plaintext"
77
+ }
78
+ },
79
+ "outputs": [],
80
+ "source": [
81
+ "!if [ ! -d \"/data/physicsnemo_ahmed_body_dataset_vv1\" ]; then \\\n",
82
+ " echo \"Dataset not found. Downloading...\"; \\\n",
83
+ " ~/app/ngc-cli/ngc registry resource download-version \"nvidia/physicsnemo/physicsnemo_ahmed_body_dataset:v1\" --dest /data; \\\n",
84
+ "else \\\n",
85
+ " echo \"Dataset already exists at /data/physicsnemo_ahmed_body_dataset_vv1\"; \\\n",
86
+ "fi"
87
+ ]
88
+ },
89
+ {
90
+ "cell_type": "markdown",
91
+ "id": "93554844",
92
+ "metadata": {},
93
+ "source": [
94
+ "### Optional Steps for Standalone Environment Setup\n",
95
+ "\n",
96
+ "The next set of steps are only necessary if you would like to rebuild the environment outside of HuggingFace Spaces.\n",
97
+ "\n",
98
+ "Once the dataset is downloaded, you man procede to [domino-data-preprocessing.ipynb](domino-data-preprocessing.ipynb)!"
99
+ ]
100
+ },
101
+ {
102
+ "cell_type": "markdown",
103
+ "id": "0086ae4a",
104
+ "metadata": {},
105
+ "source": [
106
+ "#### Setting your environment to pull the PhysicsNeMo container from NGC:\n",
107
+ "\n",
108
+ "Please refer to the following link for instructions on setting up your environment to pull the PhysicsNeMo container from NGC:\n",
109
+ "https://docs.nvidia.com/launchpad/ai/base-command-coe/latest/bc-coe-docker-basics-step-02.html\n",
110
+ "\n",
111
+ "\n",
112
+ "#### Running PhysicsNeMo container using docker command\n",
113
+ "\n",
114
+ "Pull the PhysicsNeMo container with the following command:\n",
115
+ "```bash\n",
116
+ "docker pull nvcr.io/nvidia/physicsnemo/physicsnemo:25.06\n",
117
+ " ```\n",
118
+ "\n",
119
+ "To launch the PhysicsNeMo container using docker, use the following command:\n",
120
+ "\n",
121
+ "```bash\n",
122
+ "docker run --gpus 1 --shm-size=2g -p 7008:7008 --ulimit memlock=-1 --ulimit stack=67108864 --runtime nvidia -v <path_on_host>:/workspace -it --rm $(docker images | grep 25.03| awk '{print $3}')\n",
123
+ " \n",
124
+ "```\n",
125
+ "\n",
126
+ "Make sure to replace <path_on_host> with the absolute path to the directory on the host system that contains your Jupyter notebooks and Ahmed Body surface data. This path will be mounted as `/workspace` inside the container, providing access to your data and scripts during the session\n",
127
+ "\n",
128
+ "#### Start Jupyter Lab\n",
129
+ "\n",
130
+ "From the terminal inside the container run the following command to start Jupyter Lab in the background:\n",
131
+ "\n",
132
+ "```bash\n",
133
+ "nohup python3 -m jupyter lab --ip=0.0.0.0 --port=7008 --allow-root --no-browser --NotebookApp.token='' --notebook-dir='/workspace/' --NotebookApp.allow_origin='*' > /dev/null 2>&1 &\n",
134
+ "```\n",
135
+ "\n",
136
+ "Then from your labtop start a SSH tunnel using the host which your job is running and the port which you assigned above `--port=1234`: \n",
137
+ "\n",
138
+ "```bash\n",
139
+ "ssh -L 3030:eos0311:7008 eos\n",
140
+ "```\n",
141
+ "Access Jupyter Lab using `http://localhost:1234` in your browser. \n",
142
+ "\n",
143
+ "#### Dependencies\n",
144
+ "\n",
145
+ "Please install the following in the container.\n",
146
+ "```bash\n",
147
+ "pip install numpy pyvista vtk matplotlib tqdm numpy-stl torchinfo\n",
148
+ "apt install xvfb\n",
149
+ "```"
150
+ ]
151
+ },
152
+ {
153
+ "cell_type": "markdown",
154
+ "id": "cc765d6a",
155
+ "metadata": {},
156
+ "source": [
157
+ "## Troubleshooting\n",
158
+ "\n",
159
+ "### Common Issues\n",
160
+ "\n",
161
+ "1. **Memory Issues**\n",
162
+ " - Monitor GPU memory: `nvidia-smi`\n",
163
+ " - Check system memory: `free -h`\n",
164
+ "\n",
165
+ "2. **GPU Support**\n",
166
+ "\n",
167
+ " - Run the following command to verify your container runtime supports NVIDIA GPUs:\n",
168
+ "\n",
169
+ "```bash\n",
170
+ "docker run --rm --runtime=nvidia --gpus all ubuntu nvidia-smi\n",
171
+ "```\n",
172
+ "\n",
173
+ "Expected output should show your GPU information, for example:\n",
174
+ "```\n",
175
+ "+-----------------------------------------------------------------------------------------+\n",
176
+ "| NVIDIA-SMI 560.35.03 Driver Version: 560.35.03 CUDA Version: 12.6 |\n",
177
+ "|-----------------------------------------+------------------------+----------------------+\n",
178
+ "| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |\n",
179
+ "| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |\n",
180
+ "| | | MIG M. |\n",
181
+ "|=========================================+========================+======================|\n",
182
+ "| 0 NVIDIA A100-PCIE-40GB Off | 00000000:41:00.0 Off | 0 |\n",
183
+ "| N/A 37C P0 35W / 250W | 40423MiB / 40960MiB | 0% Default |\n",
184
+ "| | | Disabled |\n",
185
+ "+-----------------------------------------+------------------------+----------------------+\n",
186
+ "```\n",
187
+ "\n",
188
+ "## Additional Resources\n",
189
+ "\n",
190
+ "- [NVIDIA Physics NeMo Documentation](https://docs.nvidia.com/deeplearning/nemo-physics/user-guide)\n",
191
+ "- [PyVista Documentation](https://docs.pyvista.org/) for 3D visualization\n",
192
+ "- [Ahmed Body Benchmark](https://www.cfd-online.com/Wiki/Ahmed_body) for background\n",
193
+ "- [Physics-Informed Neural Networks](https://www.sciencedirect.com/science/article/abs/pii/S0021999118307125) paper\n",
194
+ "- [Graph Neural Networks](https://arxiv.org/abs/2106.10943) for scientific computing\n",
195
+ "- [Neural Operators](https://arxiv.org/abs/2108.08481) for PDEs\n",
196
+ "- [DGL Documentation](https://www.dgl.ai/) for graph neural networks\n",
197
+ "- [Triton Inference Server](https://github.com/triton-inference-server/server) for deployment\n"
198
+ ]
199
+ }
200
+ ],
201
+ "metadata": {
202
+ "language_info": {
203
+ "name": "python"
204
+ }
205
+ },
206
+ "nbformat": 4,
207
+ "nbformat_minor": 5
208
+ }
login.html ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "page.html" %}
2
+
3
+
4
+ {% block stylesheet %}
5
+ {% endblock %}
6
+
7
+ {% block site %}
8
+
9
+ <div id="jupyter-main-app" class="container">
10
+
11
+ <img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" alt="Hugging Face Logo">
12
+ <h4>Welcome to JupyterLab</h4>
13
+
14
+ <h5>The default token is <span style="color:orange;">huggingface</span></h5>
15
+
16
+ {% if login_available %}
17
+ {# login_available means password-login is allowed. Show the form. #}
18
+ <div class="row">
19
+ <div class="navbar col-sm-8">
20
+ <div class="navbar-inner">
21
+ <div class="container">
22
+ <div class="center-nav">
23
+ <form action="{{base_url}}login?next={{next}}" method="post" class="navbar-form pull-left">
24
+ {{ xsrf_form_html() | safe }}
25
+ {% if token_available %}
26
+ <label for="password_input"><strong>{% trans %}Jupyter token <span title="This is the secret you set up when deploying your JupyterLab space">ⓘ</span> {% endtrans
27
+ %}</strong></label>
28
+ {% else %}
29
+ <label for="password_input"><strong>{% trans %}Jupyter password:{% endtrans %}</strong></label>
30
+ {% endif %}
31
+ <input type="password" name="password" id="password_input" class="form-control">
32
+ <button type="submit" class="btn btn-default" id="login_submit">{% trans %}Log in{% endtrans
33
+ %}</button>
34
+ </form>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ {% else %}
41
+ <p>{% trans %}No login available, you shouldn't be seeing this page.{% endtrans %}</p>
42
+ {% endif %}
43
+
44
+ <h5>If you don't have the credentials for this Jupyter space, <a target="_blank" href="https://huggingface.co/spaces/SpacesExamples/jupyterlab?duplicate=true">create your own.</a></h5>
45
+ <br>
46
+
47
+ <p>This template was created by <a href="https://twitter.com/camenduru" target="_blank" >camenduru</a> and <a href="https://huggingface.co/nateraw" target="_blank" >nateraw</a>, with contributions of <a href="https://huggingface.co/osanseviero" target="_blank" >osanseviero</a> and <a href="https://huggingface.co/azzr" target="_blank" >azzr</a> </p>
48
+ {% if message %}
49
+ <div class="row">
50
+ {% for key in message %}
51
+ <div class="message {{key}}">
52
+ {{message[key]}}
53
+ </div>
54
+ {% endfor %}
55
+ </div>
56
+ {% endif %}
57
+ {% if token_available %}
58
+ {% block token_message %}
59
+
60
+ {% endblock token_message %}
61
+ {% endif %}
62
+ </div>
63
+
64
+ {% endblock %}
65
+
66
+
67
+ {% block script %}
68
+ {% endblock %}
on_startup.sh ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Write some commands here that will run on root user before startup.
3
+ # For example, to clone transformers and install it in dev mode:
4
+ # git clone https://github.com/huggingface/transformers.git
5
+ # cd transformers && pip install -e ".[dev]"
outputs/ahmed_body_dataset/surface_scaling_factors.npy ADDED
Binary file (160 Bytes). View file
 
requirements.txt ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core dependencies
2
+ torch>=2.0.0
3
+ numpy>=1.21.0
4
+ scipy>=1.10.1
5
+
6
+ # VTK and visualization
7
+ vtk>=9.2.6
8
+ pyvista>=0.34.0
9
+ matplotlib==3.6.2
10
+
11
+ # NVIDIA PhysicsNeMo with all features
12
+ nvidia-physicsnemo[all] # Includes VTK, PyVista, and all other optional features
13
+
14
+ # Jupyter environment
15
+ jupyterlab>=4.0.0
16
+ notebook>=7.0.0
17
+ ipywidgets>=8.0.0
18
+ jupyter-archive
19
+ jupyter-resource-usage
20
+ jupyterlab-nvdashboard
21
+ jupyterlab-widgets>=3.0.0
22
+ widgetsnbextension>=4.0.0
23
+
24
+ # Additional project dependencies
25
+ apex
26
+ boto3==1.34.69
27
+ opencv-python==4.8.0.74
28
+ numpy-stl>=2.16.3
29
+ torchdata>=0.7.0
30
+ zarr[jupyter]
31
+ httpx>=0.24.0
32
+ trimesh>=3.9.0
33
+ python-multipart==0.0.20
34
+ hydra-core==1.3.2
35
+ warp-lang==1.5.1
36
+ loguru
37
+ # Triton Inference Server client
38
+ tritonclient[all]>=2.39.0
start_server.sh ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ JUPYTER_TOKEN="${JUPYTER_TOKEN:=huggingface}"
3
+
4
+ NOTEBOOK_DIR="/domino-demo"
5
+
6
+ jupyter labextension disable "@jupyterlab/apputils-extension:announcements"
7
+
8
+ jupyter-lab \
9
+ --ip 0.0.0.0 \
10
+ --port 7860 \
11
+ --no-browser \
12
+ --allow-root \
13
+ --ServerApp.token="$JUPYTER_TOKEN" \
14
+ --ServerApp.tornado_settings="{'headers': {'Content-Security-Policy': 'frame-ancestors *'}}" \
15
+ --ServerApp.cookie_options="{'SameSite': 'None', 'Secure': True}" \
16
+ --ServerApp.disable_check_xsrf=True \
17
+ --LabApp.news_url=None \
18
+ --LabApp.check_for_updates_class="jupyterlab.NeverCheckForUpdate" \
19
+ --notebook-dir=$NOTEBOOK_DIR