Skip to content

Commit 8252271

Browse files
Integrate Curator instructions to the Crash example (#1213)
* Integrate Curator instructions * Update docs * Formatting changes
1 parent 059fe5d commit 8252271

File tree

3 files changed

+300
-4
lines changed

3 files changed

+300
-4
lines changed

examples/structural_mechanics/crash/README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,68 @@ This will install:
121121
- lasso-python (for LS-DYNA file parsing),
122122
- torch_geometric and torch_scatter (for GNN operations),
123123

124+
## Data Preprocessing
125+
126+
`PhysicsNeMo` has a related project to help with data processing, called
127+
[PhysicsNeMo-Curator](https://github.com/NVIDIA/physicsnemo-curator).
128+
Using `PhysicsNeMo-Curator`, crash simulation data from LS-DYNA can be processed into training-ready formats easily.
129+
130+
Currently, this can be used to preprocess d3plot files into VTP.
131+
132+
### Quick Start
133+
134+
Install PhysicsNeMo-Curator following
135+
[these instructions](https://github.com/NVIDIA/physicsnemo-curator?tab=readme-ov-file#installation-and-usage).
136+
137+
Process your LS-DYNA data:
138+
139+
```bash
140+
export PYTHONPATH=$PYTHONPATH:examples &&
141+
physicsnemo-curator-etl \
142+
--config-dir=examples/config \
143+
--config-name=crash_etl \
144+
etl.source.input_dir=/data/crash_sims/ \
145+
etl.sink.output_dir=/data/crash_processed_vtp/ \
146+
etl.processing.num_processes=4
147+
```
148+
149+
This will process all LS-DYNA runs in `/data/crash_sims/` and output VTP files to `/data/crash_processed_vtp/`.
150+
151+
### Input Data Structure
152+
153+
The Curator expects your LS-DYNA data organized as:
154+
155+
```
156+
crash_sims/
157+
├── Run100/
158+
│ ├── d3plot # Required: binary mesh/displacement data
159+
│ └── run100.k # Optional: part thickness definitions
160+
├── Run101/
161+
│ ├── d3plot
162+
│ └── run101.k
163+
└── ...
164+
```
165+
166+
### Output Formats
167+
168+
#### VTP Format (Recommended for this example)
169+
170+
Produces single VTP file per run with all timesteps as displacement fields:
171+
172+
```
173+
crash_processed_vtp/
174+
├── Run100.vtp
175+
├── Run101.vtp
176+
└── ...
177+
```
178+
179+
Each VTP contains:
180+
- Reference coordinates at t=0
181+
- Displacement fields: `displacement_t0.000`, `displacement_t0.005`, etc.
182+
- Node thickness values
183+
184+
This format is directly compatible with the VTP reader in this example.
185+
124186
## Training
125187

126188
Training is managed via Hydra configurations located in conf/.
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2023 - 2025 NVIDIA CORPORATION & AFFILIATES.
2+
# SPDX-FileCopyrightText: All rights reserved.
3+
# SPDX-License-Identifier: Apache-2.0
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
import tempfile
18+
import numpy as np
19+
import pyvista as pv
20+
import pytest
21+
from pathlib import Path
22+
23+
# Import functions from vtp_reader
24+
import sys
25+
26+
sys.path.insert(0, str(Path(__file__).parent.parent))
27+
from vtp_reader import (
28+
load_vtp_file,
29+
extract_mesh_connectivity_from_polydata,
30+
build_edges_from_mesh_connectivity,
31+
)
32+
33+
34+
@pytest.fixture
35+
def simple_vtp_file():
36+
"""Create a simple VTP file for testing."""
37+
# Create a simple quad mesh (2x2 grid)
38+
points = np.array(
39+
[
40+
[0, 0, 0],
41+
[1, 0, 0],
42+
[0, 1, 0],
43+
[1, 1, 0],
44+
],
45+
dtype=np.uint8,
46+
)
47+
48+
# Single quad cell
49+
faces = np.array([4, 0, 1, 3, 2]) # quad with 4 vertices
50+
51+
mesh = pv.PolyData(points, faces, force_float=False)
52+
53+
# Add displacement fields for 3 timesteps
54+
mesh.point_data["displacement_t0.000"] = np.array(
55+
[
56+
[0, 0, 0],
57+
[0, 0, 0],
58+
[0, 0, 0],
59+
[0, 0, 0],
60+
],
61+
dtype=np.uint8,
62+
)
63+
64+
mesh.point_data["displacement_t0.005"] = np.array(
65+
[
66+
[1, 0, 0],
67+
[1, 0, 0],
68+
[1, 0, 0],
69+
[1, 0, 0],
70+
],
71+
dtype=np.uint8,
72+
)
73+
74+
mesh.point_data["displacement_t0.010"] = np.array(
75+
[
76+
[2, 0, 0],
77+
[2, 0, 0],
78+
[2, 0, 0],
79+
[2, 0, 0],
80+
],
81+
dtype=np.uint8,
82+
)
83+
84+
# Add thickness as additional point data
85+
mesh.point_data["thickness"] = np.array([1, 1, 1, 1], dtype=np.uint8)
86+
87+
# Save to temporary file
88+
with tempfile.NamedTemporaryFile(suffix=".vtp", delete=False) as f:
89+
temp_path = f.name
90+
91+
mesh.save(temp_path)
92+
yield temp_path
93+
94+
# Cleanup
95+
Path(temp_path).unlink(missing_ok=True)
96+
97+
98+
def test_load_vtp_file_basic(simple_vtp_file):
99+
"""Test basic VTP file loading."""
100+
pos_raw, mesh_connectivity, point_data_dict = load_vtp_file(simple_vtp_file)
101+
102+
# Check positions shape: (timesteps, nodes, 3)
103+
assert pos_raw.shape == (3, 4, 3), f"Expected shape (3, 4, 3), got {pos_raw.shape}"
104+
105+
# Check mesh connectivity
106+
assert len(mesh_connectivity) == 1, f"Expected 1 cell, got {len(mesh_connectivity)}"
107+
assert len(mesh_connectivity[0]) == 4, (
108+
f"Expected quad with 4 vertices, got {len(mesh_connectivity[0])}"
109+
)
110+
111+
# Check point data dict contains thickness
112+
assert "thickness" in point_data_dict, "Thickness not found in point_data_dict"
113+
assert point_data_dict["thickness"].shape == (4,), (
114+
f"Expected thickness shape (4,), got {point_data_dict['thickness'].shape}"
115+
)
116+
117+
118+
def test_load_vtp_file_displacements(simple_vtp_file):
119+
"""Test that displacements are correctly applied."""
120+
pos_raw, _, _ = load_vtp_file(simple_vtp_file)
121+
122+
# First timestep should be reference coords (displacement = 0)
123+
expected_t0 = np.array(
124+
[
125+
[0, 0, 0],
126+
[1, 0, 0],
127+
[0, 1, 0],
128+
[1, 1, 0],
129+
]
130+
)
131+
np.testing.assert_array_almost_equal(pos_raw[0], expected_t0, decimal=5)
132+
133+
# Second timestep should include displacement
134+
expected_t1 = expected_t0 + np.array([[1, 0, 0]] * 4)
135+
np.testing.assert_array_almost_equal(pos_raw[1], expected_t1, decimal=5)
136+
137+
# Third timestep
138+
expected_t2 = expected_t0 + np.array([[2, 0, 0]] * 4)
139+
np.testing.assert_array_almost_equal(pos_raw[2], expected_t2, decimal=5)
140+
141+
142+
def test_extract_mesh_connectivity():
143+
"""Test mesh connectivity extraction from PolyData."""
144+
points = np.array(
145+
[
146+
[0, 0, 0],
147+
[1, 0, 0],
148+
[1, 1, 0],
149+
[0, 1, 0],
150+
]
151+
)
152+
153+
# Create a single quad
154+
faces = np.array([4, 0, 1, 2, 3])
155+
poly = pv.PolyData(points, faces, force_float=False)
156+
157+
connectivity = extract_mesh_connectivity_from_polydata(poly)
158+
159+
assert len(connectivity) == 1, f"Expected 1 cell, got {len(connectivity)}"
160+
assert len(connectivity[0]) == 4, f"Expected 4 vertices, got {len(connectivity[0])}"
161+
assert connectivity[0] == [0, 1, 2, 3], (
162+
f"Expected [0, 1, 2, 3], got {connectivity[0]}"
163+
)
164+
165+
166+
def test_build_edges_from_mesh_connectivity():
167+
"""Test edge building from mesh connectivity."""
168+
# Single quad: should produce 4 edges
169+
mesh_connectivity = [[0, 1, 2, 3]]
170+
edges = build_edges_from_mesh_connectivity(mesh_connectivity)
171+
172+
expected_edges = {(0, 1), (1, 2), (2, 3), (0, 3)}
173+
assert edges == expected_edges, f"Expected {expected_edges}, got {edges}"
174+
175+
176+
def test_point_data_extraction(simple_vtp_file):
177+
"""Test that non-displacement point data is extracted correctly."""
178+
_, _, point_data_dict = load_vtp_file(simple_vtp_file)
179+
180+
# Should have thickness
181+
assert "thickness" in point_data_dict, "Thickness not in point_data_dict"
182+
183+
# Should NOT have displacement fields
184+
assert "displacement_t0.000" not in point_data_dict, (
185+
"Displacement fields should not be in point_data_dict"
186+
)
187+
assert "displacement_t0.005" not in point_data_dict, (
188+
"Displacement fields should not be in point_data_dict"
189+
)
190+
191+
# Check thickness values
192+
expected_thickness = np.array([1, 1, 1, 1], dtype=np.uint8)
193+
np.testing.assert_array_almost_equal(
194+
point_data_dict["thickness"], expected_thickness, decimal=5
195+
)
196+
197+
198+
def test_missing_displacement_fields():
199+
"""Test that missing displacement fields raises appropriate error."""
200+
# Create VTP without displacement fields
201+
points = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]])
202+
faces = np.array([3, 0, 1, 2])
203+
mesh = pv.PolyData(points, faces, force_float=False)
204+
205+
with tempfile.NamedTemporaryFile(suffix=".vtp", delete=False) as f:
206+
temp_path = f.name
207+
208+
mesh.save(temp_path)
209+
210+
try:
211+
with pytest.raises(ValueError, match="No displacement fields found"):
212+
load_vtp_file(temp_path)
213+
finally:
214+
Path(temp_path).unlink(missing_ok=True)
215+
216+
217+
def test_empty_mesh_connectivity():
218+
"""Test edge building with empty connectivity."""
219+
mesh_connectivity = []
220+
edges = build_edges_from_mesh_connectivity(mesh_connectivity)
221+
222+
assert len(edges) == 0, f"Expected 0 edges for empty connectivity, got {len(edges)}"

examples/structural_mechanics/crash/vtp_reader.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,14 @@ def extract_mesh_connectivity_from_polydata(poly: pv.PolyData):
5555

5656

5757
def load_vtp_file(vtp_path):
58-
"""Load positions over time and connectivity from a single VTP file.
58+
"""Load positions over time, connectivity, and other point data from a single VTP file.
5959
6060
Expects displacement fields in point_data named like:
6161
- displacement_t0.000, displacement_t0.005, ..., displacement_t0.100
6262
Returns:
6363
pos_raw: (timesteps, num_nodes, 3) absolute positions (coords + displacement_t)
6464
mesh_connectivity: list[list[int]]
65+
point_data_dict: dict of other point data arrays (e.g., thickness)
6566
"""
6667
poly = pv.read(vtp_path)
6768
if not isinstance(poly, pv.PolyData):
@@ -104,7 +105,14 @@ def natural_key(name):
104105

105106
pos_raw = np.stack(pos_list, axis=0)
106107
mesh_connectivity = extract_mesh_connectivity_from_polydata(poly)
107-
return pos_raw, mesh_connectivity
108+
109+
# Extract all other point data fields (not displacement fields)
110+
point_data_dict = {}
111+
for name in poly.point_data.keys():
112+
if not name.startswith("displacement_"):
113+
point_data_dict[name] = np.asarray(poly.point_data[name])
114+
115+
return pos_raw, mesh_connectivity, point_data_dict
108116

109117

110118
def build_edges_from_mesh_connectivity(mesh_connectivity):
@@ -178,7 +186,7 @@ def process_vtp_data(data_dir, num_samples=2, write_vtp=False, logger=None):
178186
output_dir = f"./output_{os.path.splitext(os.path.basename(vtp_path))[0]}"
179187
os.makedirs(output_dir, exist_ok=True)
180188

181-
pos_raw, mesh_connectivity = load_vtp_file(vtp_path)
189+
pos_raw, mesh_connectivity, point_data_dict = load_vtp_file(vtp_path)
182190

183191
# Use unfiltered data
184192
filtered_pos_raw = pos_raw
@@ -200,7 +208,11 @@ def process_vtp_data(data_dir, num_samples=2, write_vtp=False, logger=None):
200208
write_vtp=write_vtp,
201209
logger=logger,
202210
)
203-
point_data_all.append({"coords": mesh_pos_all})
211+
212+
# Create record with coords and all other point data fields
213+
record = {"coords": mesh_pos_all}
214+
record.update(point_data_dict) # Add thickness and any other fields
215+
point_data_all.append(record)
204216

205217
processed_runs += 1
206218
if processed_runs >= num_samples:

0 commit comments

Comments
 (0)