Skip to content

Commit 0368d4d

Browse files
authored
Merge branch 'main' into healda-data
2 parents 1883017 + 645fb0a commit 0368d4d

90 files changed

Lines changed: 2273 additions & 1511 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/img/mesh/airplane.png

309 KB
Loading

docs/img/mesh/airplane.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Render the PyVista stock airplane mesh as a plain wireframe surface."""
2+
3+
from pathlib import Path
4+
5+
import pyvista as pv
6+
7+
from physicsnemo.mesh.io import from_pyvista, to_pyvista
8+
9+
pv.OFF_SCREEN = True
10+
11+
OUTPUT = Path(__file__).parent / "airplane.png"
12+
13+
### Load the airplane from PyVista examples and convert
14+
pv_airplane = pv.examples.load_airplane()
15+
mesh = from_pyvista(pv_airplane)
16+
17+
pv_mesh = to_pyvista(mesh)
18+
19+
plotter = pv.Plotter(window_size=(1400, 1000))
20+
plotter.add_mesh(
21+
pv_mesh,
22+
color="lightblue",
23+
show_edges=True,
24+
line_width=0.5,
25+
)
26+
plotter.set_background("white")
27+
### Scale-relative isometric-style camera. The airplane lives in a ~2000-unit
28+
### bounding box, so the camera is offset by a fraction of the bounding diagonal.
29+
center = mesh.points.mean(dim=0).numpy().tolist()
30+
diag = float((mesh.points.amax(dim=0) - mesh.points.amin(dim=0)).norm())
31+
eye = [center[0] - 0.7 * diag, center[1] - 0.7 * diag, center[2] + 0.6 * diag]
32+
plotter.camera_position = [eye, center, (0, 0, 1)]
33+
plotter.screenshot(OUTPUT, transparent_background=False)
34+
plotter.close()
35+
36+
print(f"Saved {OUTPUT}")
128 KB
Loading
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""Render the PyVista stock airplane mesh colored by Gaussian curvature."""
2+
3+
from pathlib import Path
4+
5+
import numpy as np
6+
import pyvista as pv
7+
import torch
8+
9+
from physicsnemo.mesh.io import from_pyvista, to_pyvista
10+
11+
pv.OFF_SCREEN = True
12+
13+
OUTPUT = Path(__file__).parent / "airplane_gaussian_curvature.png"
14+
15+
### Load the airplane from PyVista examples and convert
16+
pv_airplane = pv.examples.load_airplane()
17+
mesh = from_pyvista(pv_airplane)
18+
19+
### Defensive cleanup; harmless on the airplane mesh and matches the bunny
20+
### pipeline so users get a consistent recipe across the docs.
21+
mesh = mesh.clean()
22+
23+
### Subdivide twice for smoother curvature estimation; Loop subdivision
24+
### produces a limit surface that is C2 everywhere except at extraordinary
25+
### vertices, so two levels dramatically reduce discrete-curvature noise.
26+
mesh = mesh.subdivide(levels=2, filter="loop")
27+
28+
### Compute Gaussian curvature with log1p regularization for visualization
29+
K = mesh.gaussian_curvature_vertices
30+
K = torch.nan_to_num(K, nan=0.0)
31+
K_reg = K.sign() * K.abs().log1p()
32+
33+
### Smooth the scalar field via iterated Laplacian diffusion to suppress
34+
### per-vertex noise from the discrete curvature estimate.
35+
adj = mesh.get_point_to_points_adjacency()
36+
src, tgt = adj.expand_to_pairs()
37+
for _ in range(50):
38+
neighbor_sum = torch.zeros_like(K_reg)
39+
counts = torch.zeros_like(K_reg)
40+
neighbor_sum.scatter_add_(0, tgt, K_reg[src])
41+
counts.scatter_add_(0, tgt, torch.ones_like(K_reg[src]))
42+
K_reg = 0.3 * K_reg + 0.7 * neighbor_sum / counts.clamp(min=1)
43+
44+
mesh.point_data["gaussian_curvature"] = K_reg
45+
46+
K_np = K_reg.numpy()
47+
### The airplane is dominated by flat regions punctuated by very high
48+
### curvature at sharp edges, so a tighter upper percentile (80 vs 95)
49+
### lets the bulk variation occupy more of the colormap rather than being
50+
### compressed to a single colour by extreme outliers at the wing tips.
51+
low, high = np.percentile(K_np, 5), np.percentile(K_np, 80)
52+
53+
pv_mesh = to_pyvista(mesh)
54+
55+
plotter = pv.Plotter(window_size=(1400, 1000))
56+
plotter.add_mesh(
57+
pv_mesh,
58+
scalars="gaussian_curvature",
59+
cmap="coolwarm",
60+
clim=(low, high),
61+
show_edges=False,
62+
scalar_bar_args={"title": "Gaussian Curvature", "color": "black"},
63+
)
64+
plotter.set_background("white")
65+
### Scale-relative isometric-style camera, shared with the other airplane scripts.
66+
center = mesh.points.mean(dim=0).numpy().tolist()
67+
diag = float((mesh.points.amax(dim=0) - mesh.points.amin(dim=0)).norm())
68+
eye = [center[0] - 0.7 * diag, center[1] - 0.7 * diag, center[2] + 0.6 * diag]
69+
plotter.camera_position = [eye, center, (0, 0, 1)]
70+
plotter.screenshot(OUTPUT, transparent_background=False)
71+
plotter.close()
72+
73+
print(f"Saved {OUTPUT}")
149 KB
Loading
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Render the PyVista stock airplane mesh colored by mean curvature."""
2+
3+
from pathlib import Path
4+
5+
import numpy as np
6+
import pyvista as pv
7+
import torch
8+
9+
from physicsnemo.mesh.io import from_pyvista, to_pyvista
10+
11+
pv.OFF_SCREEN = True
12+
13+
OUTPUT = Path(__file__).parent / "airplane_mean_curvature.png"
14+
15+
### Load the airplane from PyVista examples and convert
16+
pv_airplane = pv.examples.load_airplane()
17+
mesh = from_pyvista(pv_airplane)
18+
19+
### Defensive cleanup; harmless on the airplane mesh and matches the bunny
20+
### pipeline so users get a consistent recipe across the docs.
21+
mesh = mesh.clean()
22+
23+
### Subdivide twice for smoother curvature estimation; Loop subdivision
24+
### produces a limit surface that is C2 everywhere except at extraordinary
25+
### vertices, so two levels dramatically reduce discrete-curvature noise.
26+
mesh = mesh.subdivide(levels=2, filter="loop")
27+
28+
### Compute mean curvature with log1p regularization for visualization.
29+
### The airplane has open boundary edges, so ~4% of vertices have undefined
30+
### mean curvature (NaN) which we replace with zero.
31+
H = mesh.mean_curvature_vertices
32+
H = torch.nan_to_num(H, nan=0.0)
33+
H_reg = H.sign() * H.abs().log1p()
34+
35+
### Smooth the scalar field via iterated Laplacian diffusion to suppress
36+
### per-vertex noise from the discrete curvature estimate.
37+
adj = mesh.get_point_to_points_adjacency()
38+
src, tgt = adj.expand_to_pairs()
39+
for _ in range(50):
40+
neighbor_sum = torch.zeros_like(H_reg)
41+
counts = torch.zeros_like(H_reg)
42+
neighbor_sum.scatter_add_(0, tgt, H_reg[src])
43+
counts.scatter_add_(0, tgt, torch.ones_like(H_reg[src]))
44+
H_reg = 0.3 * H_reg + 0.7 * neighbor_sum / counts.clamp(min=1)
45+
46+
mesh.point_data["mean_curvature"] = H_reg
47+
48+
H_np = H_reg.numpy()
49+
### The airplane is dominated by flat regions punctuated by very high
50+
### curvature at sharp edges, so a tighter upper percentile (80 vs 95)
51+
### lets the bulk variation occupy more of the colormap rather than being
52+
### compressed to a single colour by extreme outliers at the wing tips.
53+
low, high = np.percentile(H_np, 5), np.percentile(H_np, 80)
54+
55+
pv_mesh = to_pyvista(mesh)
56+
57+
plotter = pv.Plotter(window_size=(1400, 1000))
58+
plotter.add_mesh(
59+
pv_mesh,
60+
scalars="mean_curvature",
61+
cmap="coolwarm",
62+
clim=(low, high),
63+
show_edges=False,
64+
scalar_bar_args={"title": "Mean Curvature", "color": "black"},
65+
)
66+
plotter.set_background("white")
67+
### Scale-relative isometric-style camera, shared with the other airplane scripts.
68+
center = mesh.points.mean(dim=0).numpy().tolist()
69+
diag = float((mesh.points.amax(dim=0) - mesh.points.amin(dim=0)).norm())
70+
eye = [center[0] - 0.7 * diag, center[1] - 0.7 * diag, center[2] + 0.6 * diag]
71+
plotter.camera_position = [eye, center, (0, 0, 1)]
72+
plotter.screenshot(OUTPUT, transparent_background=False)
73+
plotter.close()
74+
75+
print(f"Saved {OUTPUT}")

physicsnemo/mesh/README.md

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ This means you can work with:
3030
- 2D triangles in 3D space (surface meshes for graphics/CFD)
3131
- 3D tetrahedra in 3D space (volume meshes for FEM/CFD)
3232
- 1D edges in 3D space (curve meshes for path planning)
33-
- Any other n-dimensional manifold in m-dimensional space (where n ≤ m)
33+
- Any other $n$-dimensional manifold in $m$-dimensional space (where $n \leq m$)
3434

3535
all with the same API. PhysicsNeMo-Mesh's API design takes heavy inspiration from
3636
[PyVista](https://pyvista.org/), but it is designed to be a) end-to-end
@@ -85,7 +85,7 @@ performance benefits.
8585
**Mesh Operations:**
8686

8787
- **Subdivision**: Linear, [Loop](https://en.wikipedia.org/wiki/Loop_subdivision_surface)
88-
(), and [Butterfly](https://en.wikipedia.org/wiki/Butterfly_subdivision_surface)
88+
($C^2$), and [Butterfly](https://en.wikipedia.org/wiki/Butterfly_subdivision_surface)
8989
(interpolating) schemes
9090
- **Smoothing**: [Laplacian smoothing](https://en.wikipedia.org/wiki/Laplacian_smoothing)
9191
with feature preservation
@@ -193,7 +193,7 @@ graphics/CAD mesh.
193193

194194
Then, with `mesh.draw()`, you can visualize the mesh:
195195

196-
![Airplane Mesh](examples/readme_examples/airplane.png)
196+
![Airplane Mesh](../../docs/img/mesh/airplane.png)
197197

198198
### Computing Curvature
199199

@@ -210,7 +210,7 @@ mesh.draw(
210210
)
211211
```
212212

213-
![Gaussian Curvature](examples/readme_examples/airplane_gaussian_curvature.png)
213+
![Gaussian Curvature](../../docs/img/mesh/airplane_gaussian_curvature.png)
214214

215215
*Warmer colors indicate positive Gaussian curvature (convex regions), cooler colors
216216
indicate negative Gaussian curvature (concave regions).*
@@ -226,7 +226,7 @@ mesh.draw(
226226
)
227227
```
228228

229-
![Mean Curvature](examples/readme_examples/airplane_mean_curvature.png)
229+
![Mean Curvature](../../docs/img/mesh/airplane_mean_curvature.png)
230230

231231
*Warmer colors indicate positive mean curvature (convex regions), cooler colors
232232
indicate negative mean curvature (concave regions).*
@@ -242,7 +242,7 @@ mesh_with_grad = mesh.compute_point_derivatives(keys="temperature", method="lsq"
242242
grad_T = mesh_with_grad.point_data["temperature_gradient"]
243243

244244
print(f"Gradient shape: {grad_T.shape}") # (n_points, n_spatial_dims)
245-
print(f"T = {grad_T[0]}") # tensor([1.0000, 2.0000])
245+
print(f"grad T = {grad_T[0]}") # tensor([1.0000, 2.0000])
246246
```
247247

248248
### Moving to GPU
@@ -295,7 +295,7 @@ Comprehensive overview of PhysicsNeMo-Mesh capabilities:
295295
| Mean curvature || [Cotangent Laplacian](https://en.wikipedia.org/wiki/Discrete_Laplace_operator#Mesh_Laplacians) |
296296
| **Subdivision** | | |
297297
| Linear || Midpoint subdivision |
298-
| Loop || smooth, approximating |
298+
| Loop || $C^2$ smooth, approximating |
299299
| Butterfly || Interpolating |
300300
| **Smoothing** | | |
301301
| Laplacian smoothing || |
@@ -422,7 +422,7 @@ neighbors of mesh elements (i.e., based on the mesh connectivity,as opposed to
422422
Note that these use an efficient sparse (`indices`, `offsets`) encoding of the
423423
adjacency relationships, which is used internally for all computations. (See the
424424
dedicated
425-
[`physicsnemo.mesh.neighbors._adjacency.py`](physicsnemo/mesh/neighbors/_adjacency.py)
425+
[`physicsnemo.mesh.neighbors._adjacency.py`](neighbors/_adjacency.py)
426426
module.) You can convert these to a typical ragged list-of-lists representation
427427
with `.to_list()`, which is useful for debugging or interoperability, at the
428428
cost of performance:
@@ -540,35 +540,35 @@ Key design decisions enable these principles:
540540

541541
## Documentation & Resources
542542

543-
- **Examples**: See [`examples/`](examples/) directory for runnable demonstrations
544-
- **Tests**: See [`test/`](test/) directory for comprehensive test suite showing usage
545-
patterns
546-
- **Source**: Explore [`physicsnemo/mesh/`](physicsnemo/mesh/) for implementation details
543+
- **Examples**: See [`examples/`](../../examples/) directory for runnable demonstrations
544+
- **Tests**: See [`test/mesh/`](../../test/mesh/) directory for comprehensive test
545+
suite showing usage patterns
546+
- **Source**: Explore the source modules in this directory for implementation details
547547

548548
**Module Organization:**
549549

550-
- [`physicsnemo.mesh.calculus`](physicsnemo/mesh/calculus/) - Discrete differential
550+
- [`physicsnemo.mesh.calculus`](calculus/) - Discrete differential
551551
operators
552-
- [`physicsnemo.mesh.curvature`](physicsnemo/mesh/curvature/) - Gaussian and mean
552+
- [`physicsnemo.mesh.curvature`](curvature/) - Gaussian and mean
553553
curvature
554-
- [`physicsnemo.mesh.subdivision`](physicsnemo/mesh/subdivision/) - Mesh refinement
554+
- [`physicsnemo.mesh.subdivision`](subdivision/) - Mesh refinement
555555
schemes
556-
- [`physicsnemo.mesh.boundaries`](physicsnemo/mesh/boundaries/) - Boundary detection
556+
- [`physicsnemo.mesh.boundaries`](boundaries/) - Boundary detection
557557
and facet extraction
558-
- [`physicsnemo.mesh.neighbors`](physicsnemo/mesh/neighbors/) - Adjacency computations
559-
- [`physicsnemo.mesh.spatial`](physicsnemo/mesh/spatial/) - BVH and spatial queries
560-
- [`physicsnemo.mesh.sampling`](physicsnemo/mesh/sampling/) - Point sampling and
558+
- [`physicsnemo.mesh.neighbors`](neighbors/) - Adjacency computations
559+
- [`physicsnemo.mesh.spatial`](spatial/) - BVH and spatial queries
560+
- [`physicsnemo.mesh.sampling`](sampling/) - Point sampling and
561561
interpolation
562-
- [`physicsnemo.mesh.transformations`](physicsnemo/mesh/transformations/) - Geometric
562+
- [`physicsnemo.mesh.transformations`](transformations/) - Geometric
563563
operations
564-
- [`physicsnemo.mesh.repair`](physicsnemo/mesh/repair/) - Mesh cleaning and topology
564+
- [`physicsnemo.mesh.repair`](repair/) - Mesh cleaning and topology
565565
repair
566-
- [`physicsnemo.mesh.validation`](physicsnemo/mesh/validation/) - Quality metrics
566+
- [`physicsnemo.mesh.validation`](validation/) - Quality metrics
567567
and statistics
568-
- [`physicsnemo.mesh.visualization`](physicsnemo/mesh/visualization/) - Matplotlib
568+
- [`physicsnemo.mesh.visualization`](visualization/) - Matplotlib
569569
and PyVista backends
570-
- [`physicsnemo.mesh.io`](physicsnemo/mesh/io/) - PyVista import/export
571-
- [`physicsnemo.mesh.examples`](physicsnemo/mesh/examples/) - Example mesh generators
570+
- [`physicsnemo.mesh.io`](io/) - PyVista import/export
571+
- [`physicsnemo.mesh.primitives`](primitives/) - Example mesh generators
572572

573573
---
574574

0 commit comments

Comments
 (0)