diff --git a/docs/img/mesh/airplane.png b/docs/img/mesh/airplane.png new file mode 100644 index 0000000000..afa885fc9f Binary files /dev/null and b/docs/img/mesh/airplane.png differ diff --git a/docs/img/mesh/airplane.py b/docs/img/mesh/airplane.py new file mode 100644 index 0000000000..fc45d9a93e --- /dev/null +++ b/docs/img/mesh/airplane.py @@ -0,0 +1,36 @@ +"""Render the PyVista stock airplane mesh as a plain wireframe surface.""" + +from pathlib import Path + +import pyvista as pv + +from physicsnemo.mesh.io import from_pyvista, to_pyvista + +pv.OFF_SCREEN = True + +OUTPUT = Path(__file__).parent / "airplane.png" + +### Load the airplane from PyVista examples and convert +pv_airplane = pv.examples.load_airplane() +mesh = from_pyvista(pv_airplane) + +pv_mesh = to_pyvista(mesh) + +plotter = pv.Plotter(window_size=(1400, 1000)) +plotter.add_mesh( + pv_mesh, + color="lightblue", + show_edges=True, + line_width=0.5, +) +plotter.set_background("white") +### Scale-relative isometric-style camera. The airplane lives in a ~2000-unit +### bounding box, so the camera is offset by a fraction of the bounding diagonal. +center = mesh.points.mean(dim=0).numpy().tolist() +diag = float((mesh.points.amax(dim=0) - mesh.points.amin(dim=0)).norm()) +eye = [center[0] - 0.7 * diag, center[1] - 0.7 * diag, center[2] + 0.6 * diag] +plotter.camera_position = [eye, center, (0, 0, 1)] +plotter.screenshot(OUTPUT, transparent_background=False) +plotter.close() + +print(f"Saved {OUTPUT}") diff --git a/docs/img/mesh/airplane_gaussian_curvature.png b/docs/img/mesh/airplane_gaussian_curvature.png new file mode 100644 index 0000000000..ce79b77f6a Binary files /dev/null and b/docs/img/mesh/airplane_gaussian_curvature.png differ diff --git a/docs/img/mesh/airplane_gaussian_curvature.py b/docs/img/mesh/airplane_gaussian_curvature.py new file mode 100644 index 0000000000..20ca311ecb --- /dev/null +++ b/docs/img/mesh/airplane_gaussian_curvature.py @@ -0,0 +1,73 @@ +"""Render the PyVista stock airplane mesh colored by Gaussian curvature.""" + +from pathlib import Path + +import numpy as np +import pyvista as pv +import torch + +from physicsnemo.mesh.io import from_pyvista, to_pyvista + +pv.OFF_SCREEN = True + +OUTPUT = Path(__file__).parent / "airplane_gaussian_curvature.png" + +### Load the airplane from PyVista examples and convert +pv_airplane = pv.examples.load_airplane() +mesh = from_pyvista(pv_airplane) + +### Defensive cleanup; harmless on the airplane mesh and matches the bunny +### pipeline so users get a consistent recipe across the docs. +mesh = mesh.clean() + +### Subdivide twice for smoother curvature estimation; Loop subdivision +### produces a limit surface that is C2 everywhere except at extraordinary +### vertices, so two levels dramatically reduce discrete-curvature noise. +mesh = mesh.subdivide(levels=2, filter="loop") + +### Compute Gaussian curvature with log1p regularization for visualization +K = mesh.gaussian_curvature_vertices +K = torch.nan_to_num(K, nan=0.0) +K_reg = K.sign() * K.abs().log1p() + +### Smooth the scalar field via iterated Laplacian diffusion to suppress +### per-vertex noise from the discrete curvature estimate. +adj = mesh.get_point_to_points_adjacency() +src, tgt = adj.expand_to_pairs() +for _ in range(50): + neighbor_sum = torch.zeros_like(K_reg) + counts = torch.zeros_like(K_reg) + neighbor_sum.scatter_add_(0, tgt, K_reg[src]) + counts.scatter_add_(0, tgt, torch.ones_like(K_reg[src])) + K_reg = 0.3 * K_reg + 0.7 * neighbor_sum / counts.clamp(min=1) + +mesh.point_data["gaussian_curvature"] = K_reg + +K_np = K_reg.numpy() +### The airplane is dominated by flat regions punctuated by very high +### curvature at sharp edges, so a tighter upper percentile (80 vs 95) +### lets the bulk variation occupy more of the colormap rather than being +### compressed to a single colour by extreme outliers at the wing tips. +low, high = np.percentile(K_np, 5), np.percentile(K_np, 80) + +pv_mesh = to_pyvista(mesh) + +plotter = pv.Plotter(window_size=(1400, 1000)) +plotter.add_mesh( + pv_mesh, + scalars="gaussian_curvature", + cmap="coolwarm", + clim=(low, high), + show_edges=False, + scalar_bar_args={"title": "Gaussian Curvature", "color": "black"}, +) +plotter.set_background("white") +### Scale-relative isometric-style camera, shared with the other airplane scripts. +center = mesh.points.mean(dim=0).numpy().tolist() +diag = float((mesh.points.amax(dim=0) - mesh.points.amin(dim=0)).norm()) +eye = [center[0] - 0.7 * diag, center[1] - 0.7 * diag, center[2] + 0.6 * diag] +plotter.camera_position = [eye, center, (0, 0, 1)] +plotter.screenshot(OUTPUT, transparent_background=False) +plotter.close() + +print(f"Saved {OUTPUT}") diff --git a/docs/img/mesh/airplane_mean_curvature.png b/docs/img/mesh/airplane_mean_curvature.png new file mode 100644 index 0000000000..df326d9c77 Binary files /dev/null and b/docs/img/mesh/airplane_mean_curvature.png differ diff --git a/docs/img/mesh/airplane_mean_curvature.py b/docs/img/mesh/airplane_mean_curvature.py new file mode 100644 index 0000000000..b2cc89c797 --- /dev/null +++ b/docs/img/mesh/airplane_mean_curvature.py @@ -0,0 +1,75 @@ +"""Render the PyVista stock airplane mesh colored by mean curvature.""" + +from pathlib import Path + +import numpy as np +import pyvista as pv +import torch + +from physicsnemo.mesh.io import from_pyvista, to_pyvista + +pv.OFF_SCREEN = True + +OUTPUT = Path(__file__).parent / "airplane_mean_curvature.png" + +### Load the airplane from PyVista examples and convert +pv_airplane = pv.examples.load_airplane() +mesh = from_pyvista(pv_airplane) + +### Defensive cleanup; harmless on the airplane mesh and matches the bunny +### pipeline so users get a consistent recipe across the docs. +mesh = mesh.clean() + +### Subdivide twice for smoother curvature estimation; Loop subdivision +### produces a limit surface that is C2 everywhere except at extraordinary +### vertices, so two levels dramatically reduce discrete-curvature noise. +mesh = mesh.subdivide(levels=2, filter="loop") + +### Compute mean curvature with log1p regularization for visualization. +### The airplane has open boundary edges, so ~4% of vertices have undefined +### mean curvature (NaN) which we replace with zero. +H = mesh.mean_curvature_vertices +H = torch.nan_to_num(H, nan=0.0) +H_reg = H.sign() * H.abs().log1p() + +### Smooth the scalar field via iterated Laplacian diffusion to suppress +### per-vertex noise from the discrete curvature estimate. +adj = mesh.get_point_to_points_adjacency() +src, tgt = adj.expand_to_pairs() +for _ in range(50): + neighbor_sum = torch.zeros_like(H_reg) + counts = torch.zeros_like(H_reg) + neighbor_sum.scatter_add_(0, tgt, H_reg[src]) + counts.scatter_add_(0, tgt, torch.ones_like(H_reg[src])) + H_reg = 0.3 * H_reg + 0.7 * neighbor_sum / counts.clamp(min=1) + +mesh.point_data["mean_curvature"] = H_reg + +H_np = H_reg.numpy() +### The airplane is dominated by flat regions punctuated by very high +### curvature at sharp edges, so a tighter upper percentile (80 vs 95) +### lets the bulk variation occupy more of the colormap rather than being +### compressed to a single colour by extreme outliers at the wing tips. +low, high = np.percentile(H_np, 5), np.percentile(H_np, 80) + +pv_mesh = to_pyvista(mesh) + +plotter = pv.Plotter(window_size=(1400, 1000)) +plotter.add_mesh( + pv_mesh, + scalars="mean_curvature", + cmap="coolwarm", + clim=(low, high), + show_edges=False, + scalar_bar_args={"title": "Mean Curvature", "color": "black"}, +) +plotter.set_background("white") +### Scale-relative isometric-style camera, shared with the other airplane scripts. +center = mesh.points.mean(dim=0).numpy().tolist() +diag = float((mesh.points.amax(dim=0) - mesh.points.amin(dim=0)).norm()) +eye = [center[0] - 0.7 * diag, center[1] - 0.7 * diag, center[2] + 0.6 * diag] +plotter.camera_position = [eye, center, (0, 0, 1)] +plotter.screenshot(OUTPUT, transparent_background=False) +plotter.close() + +print(f"Saved {OUTPUT}") diff --git a/physicsnemo/mesh/README.md b/physicsnemo/mesh/README.md index 601e18753c..d0814e8545 100644 --- a/physicsnemo/mesh/README.md +++ b/physicsnemo/mesh/README.md @@ -193,7 +193,7 @@ graphics/CAD mesh. Then, with `mesh.draw()`, you can visualize the mesh: -![Airplane Mesh](examples/readme_examples/airplane.png) +![Airplane Mesh](../../docs/img/mesh/airplane.png) ### Computing Curvature @@ -210,7 +210,7 @@ mesh.draw( ) ``` -![Gaussian Curvature](examples/readme_examples/airplane_gaussian_curvature.png) +![Gaussian Curvature](../../docs/img/mesh/airplane_gaussian_curvature.png) *Warmer colors indicate positive Gaussian curvature (convex regions), cooler colors indicate negative Gaussian curvature (concave regions).* @@ -226,7 +226,7 @@ mesh.draw( ) ``` -![Mean Curvature](examples/readme_examples/airplane_mean_curvature.png) +![Mean Curvature](../../docs/img/mesh/airplane_mean_curvature.png) *Warmer colors indicate positive mean curvature (convex regions), cooler colors indicate negative mean curvature (concave regions).* @@ -422,7 +422,7 @@ neighbors of mesh elements (i.e., based on the mesh connectivity,as opposed to Note that these use an efficient sparse (`indices`, `offsets`) encoding of the adjacency relationships, which is used internally for all computations. (See the dedicated -[`physicsnemo.mesh.neighbors._adjacency.py`](physicsnemo/mesh/neighbors/_adjacency.py) +[`physicsnemo.mesh.neighbors._adjacency.py`](neighbors/_adjacency.py) module.) You can convert these to a typical ragged list-of-lists representation with `.to_list()`, which is useful for debugging or interoperability, at the cost of performance: @@ -540,35 +540,35 @@ Key design decisions enable these principles: ## Documentation & Resources -- **Examples**: See [`examples/`](examples/) directory for runnable demonstrations -- **Tests**: See [`test/`](test/) directory for comprehensive test suite showing usage - patterns -- **Source**: Explore [`physicsnemo/mesh/`](physicsnemo/mesh/) for implementation details +- **Examples**: See [`examples/`](../../examples/) directory for runnable demonstrations +- **Tests**: See [`test/mesh/`](../../test/mesh/) directory for comprehensive test + suite showing usage patterns +- **Source**: Explore the source modules in this directory for implementation details **Module Organization:** -- [`physicsnemo.mesh.calculus`](physicsnemo/mesh/calculus/) - Discrete differential +- [`physicsnemo.mesh.calculus`](calculus/) - Discrete differential operators -- [`physicsnemo.mesh.curvature`](physicsnemo/mesh/curvature/) - Gaussian and mean +- [`physicsnemo.mesh.curvature`](curvature/) - Gaussian and mean curvature -- [`physicsnemo.mesh.subdivision`](physicsnemo/mesh/subdivision/) - Mesh refinement +- [`physicsnemo.mesh.subdivision`](subdivision/) - Mesh refinement schemes -- [`physicsnemo.mesh.boundaries`](physicsnemo/mesh/boundaries/) - Boundary detection +- [`physicsnemo.mesh.boundaries`](boundaries/) - Boundary detection and facet extraction -- [`physicsnemo.mesh.neighbors`](physicsnemo/mesh/neighbors/) - Adjacency computations -- [`physicsnemo.mesh.spatial`](physicsnemo/mesh/spatial/) - BVH and spatial queries -- [`physicsnemo.mesh.sampling`](physicsnemo/mesh/sampling/) - Point sampling and +- [`physicsnemo.mesh.neighbors`](neighbors/) - Adjacency computations +- [`physicsnemo.mesh.spatial`](spatial/) - BVH and spatial queries +- [`physicsnemo.mesh.sampling`](sampling/) - Point sampling and interpolation -- [`physicsnemo.mesh.transformations`](physicsnemo/mesh/transformations/) - Geometric +- [`physicsnemo.mesh.transformations`](transformations/) - Geometric operations -- [`physicsnemo.mesh.repair`](physicsnemo/mesh/repair/) - Mesh cleaning and topology +- [`physicsnemo.mesh.repair`](repair/) - Mesh cleaning and topology repair -- [`physicsnemo.mesh.validation`](physicsnemo/mesh/validation/) - Quality metrics +- [`physicsnemo.mesh.validation`](validation/) - Quality metrics and statistics -- [`physicsnemo.mesh.visualization`](physicsnemo/mesh/visualization/) - Matplotlib +- [`physicsnemo.mesh.visualization`](visualization/) - Matplotlib and PyVista backends -- [`physicsnemo.mesh.io`](physicsnemo/mesh/io/) - PyVista import/export -- [`physicsnemo.mesh.examples`](physicsnemo/mesh/examples/) - Example mesh generators +- [`physicsnemo.mesh.io`](io/) - PyVista import/export +- [`physicsnemo.mesh.primitives`](primitives/) - Example mesh generators ---