Mesh#

The Mesh class is the central data structure of PhysicsNeMo-Mesh. It is a tensorclass built on TensorDict, representing an n-dimensional simplicial manifold embedded in m-dimensional Euclidean space.

A Mesh stores vertex coordinates (points), cell connectivity (cells), and three TensorDict containers for attaching arbitrary tensor data at the vertex, cell, and global levels. All tensors move together under .to(device) calls, and expensive geometric quantities – centroids, normals, areas, curvature – are computed lazily on first access and cached internally.

Most mesh operations (subdivision, derivatives, transformations) are available both as Mesh methods and as standalone functions in the corresponding submodules. The methods are thin wrappers that pass self to the standalone functions.

import torch
from physicsnemo.mesh import Mesh

points = torch.tensor([[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]])
cells = torch.tensor([[0, 1, 2]])
mesh = Mesh(points=points, cells=cells)

# Geometric properties (lazily computed, cached)
print(mesh.cell_centroids)   # shape (1, 2)
print(mesh.cell_areas)       # shape (1,)

# Attach data and compute derivatives
mesh.point_data["T"] = torch.tensor([1.0, 2.0, 3.0])
mesh = mesh.compute_point_derivatives(keys="T", method="lsq")
print(mesh.point_data["T_gradient"])  # shape (3, 2)
class physicsnemo.mesh.mesh.Mesh(
points: torch.Tensor,
cells: torch.Tensor,
point_data: tensordict._td.TensorDict | dict[str, torch.Tensor] | None = None,
cell_data: tensordict._td.TensorDict | dict[str, torch.Tensor] | None = None,
global_data: tensordict._td.TensorDict | dict[str, torch.Tensor] | None = None,
*,
_cache: tensordict._td.TensorDict | None = None,
batch_size,
device=None,
names=None,
)[source]#

Bases: object

property cell_areas: Tensor#

Compute volumes (areas) of n-simplices using the Gram determinant method.

This works for simplices of any manifold dimension embedded in any spatial dimension. For example: edges in 2D/3D, triangles in 2D/3D/4D, tetrahedra in 3D/4D, etc.

The volume of an n-simplex with vertices (v0, v1, …, vn) is:

Volume = (1/n!) * sqrt(det(E^T @ E))

where E is the matrix with columns (v1-v0, v2-v0, …, vn-v0).

Returns:

Tensor of shape (n_cells,) containing the volume of each cell.

Return type:

torch.Tensor

property cell_centroids: Tensor#

Compute the centroids (geometric centers) of all cells.

The centroid of a cell is computed as the arithmetic mean of its vertex positions. For an n-simplex with vertices (v0, v1, …, vn), the centroid is:

centroid = (v0 + v1 + … + vn) / (n + 1)

The result is cached in _cache["cell", "centroids"] for efficiency.

Returns:

Tensor of shape (n_cells, n_spatial_dims) containing the centroid of each cell.

Return type:

torch.Tensor

cell_data_to_point_data(
overwrite_keys: bool = False,
) Mesh[source]#

Convert cell data to point data by averaging.

For each point, computes the average of the cell data values from all cells that contain that point. The resulting point data is added to the mesh’s point_data dictionary. Original cell data is preserved.

Parameters:

overwrite_keys (bool) – If True, silently overwrite any existing point_data keys. If False, raise an error if a key already exists in point_data.

Returns:

New Mesh with converted data added to point_data. Original cell_data is preserved.

Return type:

Mesh

Raises:

ValueError – If a cell_data key already exists in point_data and overwrite_keys=False.

Examples

>>> mesh = Mesh(points, cells, cell_data={"pressure": cell_pressures})
>>> mesh_with_point_data = mesh.cell_data_to_point_data()
>>> # Now mesh has both cell_data["pressure"] and point_data["pressure"]
property cell_normals: Tensor#

Compute unit normal vectors for codimension-1 cells.

Normal vectors are uniquely defined (up to orientation) only for codimension-1 manifolds, where n_manifold_dims = n_spatial_dims - 1. This is because the perpendicular subspace to an (n-1)-dimensional manifold in n-dimensional space is 1-dimensional, yielding a unique normal direction.

Examples of valid codimension-1 manifolds: - Edges (1-simplices) in 2D space: normal is a 2D vector - Triangles (2-simplices) in 3D space: normal is a 3D vector - Tetrahedron cells (3-simplices) in 4D space: normal is a 4D vector

Examples of invalid higher-codimension cases: - Edges in 3D space: perpendicular space is 2D (no unique normal) - Points in 2D/3D space: perpendicular space is 2D/3D (no unique normal)

The implementation uses the generalized cross product (Hodge star operator), computed via signed minor determinants. This generalizes: - 2D: 90° counterclockwise rotation of edge vector - 3D: Standard cross product of two edge vectors - nD: Determinant-based formula for (n-1) edge vectors in n-space

Returns:

Tensor of shape (n_cells, n_spatial_dims) containing unit normal vectors.

Return type:

torch.Tensor

Raises:

ValueError – If the mesh is not codimension-1 (n_manifold_dims ≠ n_spatial_dims - 1).

clean(
tolerance: float = 1e-12,
merge_points: bool = True,
remove_duplicate_cells: bool = True,
remove_unused_points: bool = True,
) Mesh[source]#

Clean and repair this mesh.

Performs various cleaning operations to fix common mesh issues: 1. Merge duplicate points within tolerance 2. Remove duplicate cells 3. Remove unused points

This is useful after mesh operations that may introduce duplicate geometry or after importing meshes from external sources that may have redundant data.

Parameters:
  • tolerance (float, optional) – Absolute L2 distance threshold for merging duplicate points.

  • merge_points (bool, optional) – Whether to merge duplicate points (default True).

  • remove_duplicate_cells (bool, optional) – Whether to remove duplicate cells (default True).

  • remove_unused_points (bool, optional) – Whether to remove unused points (default True).

Returns:

Cleaned mesh with same structure but repaired topology.

Return type:

Mesh

Examples

>>> import torch
>>> from physicsnemo.mesh import Mesh
>>> # Mesh with duplicate points
>>> points = torch.tensor([[0., 0.], [1., 0.], [0., 0.], [1., 1.]])
>>> cells = torch.tensor([[0, 1, 3], [2, 1, 3]])
>>> mesh = Mesh(points=points, cells=cells)
>>> cleaned = mesh.clean()
>>> assert cleaned.n_points == 3  # points 0 and 2 merged
>>>
>>> # Adjust tolerance for coarser merging
>>> mesh_loose = mesh.clean(tolerance=1e-6)
>>>
>>> # Only merge points, keep duplicate cells
>>> mesh_partial = mesh.clean(
...     merge_points=True,
...     remove_duplicate_cells=False
... )
property codimension: int#

Compute the codimension of the mesh.

The codimension is the difference between the spatial dimension and the manifold dimension: codimension = n_spatial_dims - n_manifold_dims.

Examples

  • Edges (1-simplices) in 2D: codimension = 2 - 1 = 1 (codimension-1)

  • Triangles (2-simplices) in 3D: codimension = 3 - 2 = 1 (codimension-1)

  • Edges in 3D: codimension = 3 - 1 = 2 (codimension-2)

  • Points in 2D: codimension = 2 - 0 = 2 (codimension-2)

Returns:

The codimension of the mesh (always non-negative).

Return type:

int

compute_cell_derivatives(
keys: str | tuple[str, ...] | list[str | tuple[str, ...]] | None = None,
method: Literal['lsq', 'dec'] = 'lsq',
gradient_type: Literal['intrinsic', 'extrinsic', 'both'] = 'intrinsic',
) Mesh[source]#

Compute gradients of cell_data fields.

This is a convenience method that delegates to physicsnemo.mesh.calculus.compute_cell_derivatives().

Parameters:
  • keys (str or tuple[str, ...] or list[str | tuple[str, ...]] or None, optional) – Fields to compute gradients of (same format as compute_point_derivatives).

  • method ({"lsq"}, optional) – Discretization method for cell-centered data. Currently only "lsq" (weighted least-squares) is implemented. DEC gradients for cell-centered data are not available because the standard DEC exterior derivative maps vertex 0-forms to edge 1-forms; there is no analogous cell-to-cell operator in the primal DEC complex.

  • gradient_type ({"intrinsic", "extrinsic", "both"}, optional) – Type of gradient to compute.

Returns:

A new Mesh with gradient fields added to cell_data.

Return type:

Mesh

Raises:

NotImplementedError – If method="dec" is requested.

Examples

>>> import torch
>>> from physicsnemo.mesh.primitives.basic import two_triangles_2d
>>> mesh = two_triangles_2d.load()
>>> mesh.cell_data["pressure"] = torch.randn(mesh.n_cells)
>>> # Compute gradient of cell-centered pressure
>>> mesh_grad = mesh.compute_cell_derivatives(keys="pressure")
compute_point_derivatives(
keys: str | tuple[str, ...] | list[str | tuple[str, ...]] | None = None,
method: Literal['lsq', 'dec'] = 'lsq',
gradient_type: Literal['intrinsic', 'extrinsic', 'both'] = 'intrinsic',
) Mesh[source]#

Compute gradients of point_data fields.

This is a convenience method that delegates to physicsnemo.mesh.calculus.compute_point_derivatives.

Parameters:
  • keys (str or tuple[str, ...] or list[str | tuple[str, ...]] or None, optional) – Fields to compute gradients of. Options: - None: All non-cached fields (excludes “_cache” subdictionary) - str: Single field name (e.g., “pressure”) - tuple: Nested path (e.g., (“flow”, “temperature”)) - list: Multiple fields (e.g., [“pressure”, “velocity”])

  • method ({"lsq", "dec"}, optional) – Discretization method: - “lsq”: Weighted least-squares reconstruction (default, CFD standard) - “dec”: Discrete Exterior Calculus (differential geometry)

  • gradient_type ({"intrinsic", "extrinsic", "both"}, optional) – Type of gradient: - “intrinsic”: Project onto manifold tangent space (default) - “extrinsic”: Full ambient space gradient - “both”: Compute and store both

Returns:

Self (mesh) with gradient fields added to point_data (modified in place). Field naming: “{field}_gradient” or “{field}_gradient_intrinsic/extrinsic”

Return type:

Mesh

Examples

>>> import torch
>>> from physicsnemo.mesh.primitives.basic import two_triangles_2d
>>> mesh = two_triangles_2d.load()
>>> mesh.point_data["pressure"] = torch.randn(mesh.n_points)
>>> # Compute gradient of pressure
>>> mesh_grad = mesh.compute_point_derivatives(keys="pressure")
>>> grad_p = mesh_grad.point_data["pressure_gradient"]
compute_point_normals(
weighting: Literal['area', 'unweighted', 'angle', 'angle_area'] = 'angle_area',
) Tensor[source]#

Compute normal vectors at mesh vertices with specified weighting.

For each point (vertex), computes a normal vector by averaging the normals of all adjacent cells. This provides a smooth approximation of the surface normal at each vertex.

Four weighting schemes are available (following industry conventions from Autodesk Maya and 3ds Max):

  • “area”: Area-weighted averaging, where larger faces have more influence on the vertex normal. The normal at vertex v is computed as: point_normal_v = normalize(sum(cell_normal * cell_area)). This reduces the influence of small sliver triangles.

  • “unweighted”: Simple averaging, where each adjacent face contributes equally regardless of size. The normal at vertex v is: point_normal_v = normalize(sum(cell_normal)). This matches PyVista/VTK’s compute_normals behavior.

  • “angle”: Angle-weighted averaging, where faces are weighted by the interior angle at the vertex. Faces with larger angles at the vertex have more influence. This often provides the most geometrically accurate normals for curved surfaces.

  • “angle_area” (default): Combined angle and area weighting, where each face’s contribution is weighted by both its area and the angle at the vertex. This is the default in Maya and balances both geometric factors.

Normal vectors are only well-defined for codimension-1 manifolds, where each cell has a unique normal direction. For higher codimensions, normals are ambiguous and this method will raise an error.

Parameters:

weighting ({"area", "unweighted", "angle", "angle_area"}) – Weighting scheme for averaging adjacent cell normals. - “area”: Weight by cell area (larger faces have more influence). - “unweighted”: Equal weight for all adjacent cells (matches PyVista/VTK). - “angle”: Weight by interior angle at the vertex. - “angle_area”: Weight by both angle and area (Maya default).

Returns:

Tensor of shape (n_points, n_spatial_dims) containing unit normal vectors at each vertex. For isolated points (with no adjacent cells), the normal is a zero vector.

Return type:

torch.Tensor

Raises:

ValueError – If the mesh is not codimension-1 (n_manifold_dims ≠ n_spatial_dims - 1), if an invalid weighting scheme is specified, or if angle-based weighting is requested for 1-simplices (edges) which have no interior angle.

See also

point_normals

Property returning angle-area-weighted normals (canonical default).

cell_normals

Compute cell (face) normals.

Examples

>>> # Triangle mesh in 3D
>>> mesh = create_triangle_mesh_3d()
>>> normals = mesh.compute_point_normals()  # area-weighted (default)
>>> normals_unweighted = mesh.compute_point_normals(weighting="unweighted")
>>> normals_angle = mesh.compute_point_normals(weighting="angle")
>>> # Normals are unit vectors (or zero for isolated points)
>>> assert torch.allclose(normals.norm(dim=-1), torch.ones(mesh.n_points), atol=1e-6)
property device: device#

Retrieves the device type of tensor class.

draw(
backend: Literal['matplotlib', 'pyvista', 'auto'] = 'auto',
show: bool = True,
point_scalars: None | Tensor | str | tuple[str, ...] = None,
cell_scalars: None | Tensor | str | tuple[str, ...] = None,
cmap: str = 'viridis',
vmin: float | None = None,
vmax: float | None = None,
alpha_points: float = 1.0,
alpha_cells: float = 1.0,
alpha_edges: float = 1.0,
show_edges: bool = True,
ax=None,
backend_options: dict[str, Any] | None = None,
)[source]#

Draw the mesh using matplotlib or PyVista backend.

Provides interactive 3D or 2D visualization with support for scalar data coloring, transparency control, and automatic backend selection.

Parameters:
  • backend ({"auto", "matplotlib", "pyvista"}) –

    Visualization backend to use: - “auto”: Automatically select based on n_spatial_dims

    (matplotlib for 0D/1D/2D, PyVista for 3D)

    • ”matplotlib”: Force matplotlib backend (supports 3D via mplot3d)

    • ”pyvista”: Force PyVista backend (requires n_spatial_dims <= 3)

  • show (bool) – Whether to display the plot immediately (calls plt.show() or plotter.show()). If False, returns the plotter/axes for further customization before display.

  • point_scalars (torch.Tensor or str or tuple[str, ...], optional) –

    Scalar data to color points. Mutually exclusive with cell_scalars. Can be: - None: Points use neutral color (black) - torch.Tensor: Direct scalar values, shape (n_points,) or

    (n_points, …) where trailing dimensions are L2-normed

    • str or tuple[str, …]: Key to lookup in mesh.point_data

  • cell_scalars (torch.Tensor or str or tuple[str, ...], optional) –

    Scalar data to color cells. Mutually exclusive with point_scalars. Can be: - None: Cells use neutral color (lightblue if no scalars,

    lightgray if point_scalars active)

    • torch.Tensor: Direct scalar values, shape (n_cells,) or (n_cells, …) where trailing dimensions are L2-normed

    • str or tuple[str, …]: Key to lookup in mesh.cell_data

  • cmap (str) – Colormap name for scalar visualization.

  • vmin (float, optional) – Minimum value for colormap normalization. If None, uses data min.

  • vmax (float, optional) – Maximum value for colormap normalization. If None, uses data max.

  • alpha_points (float) – Opacity for points, range [0, 1].

  • alpha_cells (float) – Opacity for cells/faces, range [0, 1].

  • alpha_edges (float) – Opacity for cell edges, range [0, 1].

  • show_edges (bool) – Whether to draw cell edges.

  • ax (matplotlib.axes.Axes, optional) – (matplotlib only) Existing matplotlib axes to plot on. If None, creates new figure and axes.

  • backend_options (dict[str, Any], optional) – Additional keyword arguments forwarded to the underlying visualization backend (e.g. PyVista’s plotter.add_mesh()).

Returns:

  • matplotlib backend: matplotlib.axes.Axes object

  • PyVista backend: pyvista.Plotter object

Return type:

matplotlib.axes.Axes or pyvista.Plotter

Raises:
  • ValueError – If both point_scalars and cell_scalars are specified, or if n_spatial_dims is not supported by the chosen backend.

  • ImportError – If the chosen backend (matplotlib or pyvista) is not installed.

Examples

>>> # Draw mesh with automatic backend selection
>>> mesh.draw()
>>>
>>> # Color cells by pressure data
>>> mesh.draw(cell_scalars="pressure", cmap="coolwarm")
>>>
>>> # Color points by velocity magnitude (computing norm of vector field)
>>> mesh.draw(point_scalars="velocity")  # velocity is (n_points, 3)
>>>
>>> # Use nested TensorDict key
>>> mesh.draw(cell_scalars=("flow", "temperature"))
>>>
>>> # Customize and display later
>>> ax = mesh.draw(show=False, backend="matplotlib")
>>> ax.set_title("My Mesh")
>>> import matplotlib.pyplot as plt
>>> plt.show()
dumps(
prefix: str | None = None,
copy_existing: bool = False,
*,
num_threads: int = 0,
return_early: bool = False,
share_non_tensor: bool = False,
robust_key: bool | None = None,
) Any#

Saves the tensordict to disk.

This function is a proxy to memmap().

classmethod fields()#

Return a tuple describing the fields of this dataclass.

Accepts a dataclass or an instance of one. Tuple elements are of type Field.

classmethod from_tensordict(
tensordict: TensorDictBase,
non_tensordict: dict | None = None,
safe: bool = True,
) Any#

Tensor class wrapper to instantiate a new tensor class object.

Parameters:
  • tensordict (TensorDictBase) – Dictionary of tensor types

  • non_tensordict (dict) – Dictionary with non-tensor and nested tensor class objects

  • safe (bool) – Whether to raise an error if the tensordict is not a TensorDictBase instance

property gaussian_curvature_cells: Tensor#

Compute Gaussian curvature at cell centers using dual mesh concept.

Treats cell centroids as vertices of a dual mesh and computes curvature based on angles between connections to adjacent cell centroids.

The result is cached in _cache["cell", "gaussian_curvature"] for efficiency.

Returns:

Tensor of shape (n_cells,) containing Gaussian curvature at cells.

Return type:

torch.Tensor

Examples

>>> from physicsnemo.mesh.primitives.surfaces import sphere_icosahedral
>>> mesh = sphere_icosahedral.load(subdivisions=2)
>>> K_cells = mesh.gaussian_curvature_cells
property gaussian_curvature_vertices: Tensor#

Compute intrinsic Gaussian curvature at mesh vertices.

Uses the angle defect method from discrete differential geometry:

K = (full_angle - Σ angles) / voronoi_area

This is an intrinsic measure of curvature (Theorema Egregium) that works for any codimension, as it depends only on distances within the manifold.

Signed curvature: - Positive: Elliptic/convex (sphere-like) - Zero: Flat/parabolic (plane-like) - Negative: Hyperbolic/saddle (saddle-like)

The result is cached in _cache["point", "gaussian_curvature"] for efficiency.

Returns:

Tensor of shape (n_points,) containing signed Gaussian curvature. Isolated vertices have NaN curvature.

Return type:

torch.Tensor

Notes

Satisfies discrete Gauss-Bonnet theorem:

Σ_vertices (K_i * A_i) = 2π * χ(M)

Examples

>>> from physicsnemo.mesh.primitives.surfaces import sphere_icosahedral
>>> # Sphere of radius r has K = 1/r²
>>> sphere = sphere_icosahedral.load(radius=2.0, subdivisions=3)
>>> K = sphere.gaussian_curvature_vertices
>>> # K.mean() ≈ 0.25 (= 1/(2.0)²)
get(key: NestedKey, *args, **kwargs)#

Gets the value stored with the input key.

Parameters:
  • key (str, tuple of str) – key to be queried. If tuple of str it is equivalent to chained calls of getattr.

  • default – default value if the key is not found in the tensorclass.

Returns:

value stored with the input key

get_boundary_mesh(
data_source: Literal['points', 'cells'] = 'cells',
data_aggregation: Literal['mean', 'area_weighted', 'inverse_distance'] = 'mean',
) Mesh[source]#

Extract the boundary surface of this mesh.

Convenience wrapper around get_facet_mesh() that extracts only boundary facets (those appearing in exactly one parent cell).

See get_facet_mesh() for full parameter documentation.

Parameters:
  • data_source ({"points", "cells"}, optional) – Source of data inheritance. Default: “cells”.

  • data_aggregation ({"mean", "area_weighted", "inverse_distance"}, optional) – Strategy for aggregating data. Default: “mean”.

Returns:

Boundary mesh containing only boundary facets.

Return type:

Mesh

Notes

For meshes with internal cavities (like volume meshes with voids or drivaerML-style automotive meshes), this returns BOTH the exterior surface and any interior cavity surfaces. All facets that appear in exactly one parent cell are included, regardless of whether they face “outward” or “inward”.

Examples

>>> from physicsnemo.mesh.primitives.procedural import lumpy_ball
>>> from physicsnemo.mesh.primitives.surfaces import sphere_icosahedral
>>> # Extract triangular surface of a volume mesh
>>> vol_mesh = lumpy_ball.load(n_shells=2, subdivisions=1)
>>> surface_mesh = vol_mesh.get_boundary_mesh()
>>> assert surface_mesh.n_manifold_dims == 2  # triangles
>>>
>>> # For a closed watertight sphere
>>> sphere = sphere_icosahedral.load(subdivisions=3)
>>> boundary = sphere.get_boundary_mesh()
>>> assert boundary.n_cells == 0  # no boundary
get_cell_to_cells_adjacency(adjacency_codimension: int = 1)[source]#

Compute cell-to-cells adjacency based on shared facets.

Two cells are considered adjacent if they share a k-codimension facet.

Parameters:

adjacency_codimension (int, optional) –

Codimension of shared facets defining adjacency. - 1 (default): Cells must share a codimension-1 facet (e.g., triangles

sharing an edge, tetrahedra sharing a triangular face)

  • 2: Cells must share a codimension-2 facet (e.g., tetrahedra sharing an edge)

  • k: Cells must share a codimension-k facet

Returns:

Adjacency where adjacency.to_list()[i] contains all cell indices that share a k-codimension facet with cell i.

Return type:

Adjacency

Examples

>>> from physicsnemo.mesh.primitives.basic import two_triangles_2d
>>> mesh = two_triangles_2d.load()
>>> adj = mesh.get_cell_to_cells_adjacency(adjacency_codimension=1)
>>> # Get cells sharing an edge with cell 0
>>> neighbors_of_cell_0 = adj.to_list()[0]
get_cell_to_points_adjacency()[source]#

Get the vertices (points) that comprise each cell.

This is a simple wrapper around the cells array that returns it in the standard Adjacency format for consistency with other neighbor queries.

Returns:

Adjacency where adjacency.to_list()[i] contains all point indices that are vertices of cell i. For simplicial meshes, all cells have the same number of vertices (n_manifold_dims + 1).

Return type:

Adjacency

Examples

>>> from physicsnemo.mesh.primitives.basic import two_triangles_2d
>>> mesh = two_triangles_2d.load()
>>> adj = mesh.get_cell_to_points_adjacency()
>>> # Get vertices of cell 0
>>> vertices_of_cell_0 = adj.to_list()[0]
get_facet_mesh(
manifold_codimension: int = 1,
data_source: Literal['points', 'cells'] = 'cells',
data_aggregation: Literal['mean', 'area_weighted', 'inverse_distance'] = 'mean',
target_counts: list[int] | Literal['boundary', 'shared', 'interior', 'all'] = 'all',
) Mesh[source]#

Extract k-codimension facet mesh from this n-dimensional mesh.

Extracts all (n-k)-simplices from the current n-simplicial mesh. For example: - Triangle mesh (2-simplices) → edge mesh (1-simplices) [codimension=1, default] - Triangle mesh (2-simplices) → vertex mesh (0-simplices) [codimension=2] - Tetrahedral mesh (3-simplices) → triangular facet mesh (2-simplices) [codimension=1, default] - Tetrahedral mesh (3-simplices) → edge mesh (1-simplices) [codimension=2]

The resulting mesh shares the same vertex positions but has connectivity representing the lower-dimensional simplices. Data can be inherited from either the parent cells or the boundary points.

Parameters:
  • manifold_codimension (int, optional) – Codimension of extracted mesh relative to parent. - 1: Extract (n-1)-facets (default, immediate boundaries of all cells) - 2: Extract (n-2)-facets (e.g., edges from tets, vertices from triangles) - k: Extract (n-k)-facets

  • data_source ({"points", "cells"}, optional) –

    Source of data inheritance: - “cells”: Facets inherit from parent cells they bound. When multiple

    cells share a facet, data is aggregated according to data_aggregation.

    • ”points”: Facets inherit from their boundary vertices. Data from multiple boundary points is averaged.

  • data_aggregation ({"mean", "area_weighted", "inverse_distance"}, optional) –

    Strategy for aggregating data from multiple sources (only applies when data_source=”cells”): - “mean”: Simple arithmetic mean - “area_weighted”: Weighted by parent cell areas - “inverse_distance”: Weighted by inverse distance from facet centroid

    to parent cell centroids

  • target_counts (list[int] | {"boundary", "shared", "interior", "all"}, optional) – Which facets to keep based on how many parent cells share them: - “all”: Keep all unique facets (default) - “boundary”: Keep only boundary facets (appearing in exactly 1 cell) - “shared”: Keep only shared facets (appearing in 2+ cells) - “interior”: Keep only interior facets (appearing in exactly 2 cells) - list[int]: Keep facets with counts matching any value in the list

Returns:

New Mesh with n_manifold_dims = self.n_manifold_dims - manifold_codimension, embedded in the same spatial dimension. The mesh shares the same points array but has new cells connectivity and aggregated cell_data.

Return type:

Mesh

Raises:

ValueError – If manifold_codimension is too large for this mesh (would result in negative manifold dimension).

Examples

>>> from physicsnemo.mesh.primitives.basic import two_triangles_2d
>>> # Extract edges from a triangle mesh (codimension 1)
>>> triangle_mesh = two_triangles_2d.load()
>>> edge_mesh = triangle_mesh.get_facet_mesh(manifold_codimension=1)
>>> assert edge_mesh.n_manifold_dims == 1  # edges
>>>
>>> # Extract vertices from a triangle mesh (codimension 2)
>>> vertex_mesh = triangle_mesh.get_facet_mesh(manifold_codimension=2)
>>> assert vertex_mesh.n_manifold_dims == 0  # vertices
>>> facet_mesh = triangle_mesh.get_facet_mesh(
...     data_source="cells",
...     data_aggregation="area_weighted"
... )
get_point_to_cells_adjacency()[source]#

Compute the star of each vertex (all cells containing each point).

For each point in the mesh, finds all cells that contain that point. This is the graph-theoretic “star” operation on vertices.

Returns:

Adjacency where adjacency.to_list()[i] contains all cell indices that contain point i. Isolated points (not in any cells) have empty lists.

Return type:

Adjacency

Examples

>>> from physicsnemo.mesh.primitives.basic import two_triangles_2d
>>> mesh = two_triangles_2d.load()
>>> adj = mesh.get_point_to_cells_adjacency()
>>> # Get cells containing point 0
>>> cells_of_point_0 = adj.to_list()[0]
get_point_to_points_adjacency()[source]#

Compute point-to-point adjacency (graph edges of the mesh).

For each point, finds all other points that share a cell with it. In simplicial meshes, this is equivalent to finding all points connected by an edge.

Returns:

Adjacency where adjacency.to_list()[i] contains all point indices that share a cell (edge) with point i. Isolated points have empty lists.

Return type:

Adjacency

Examples

>>> from physicsnemo.mesh.primitives.basic import two_triangles_2d
>>> mesh = two_triangles_2d.load()
>>> adj = mesh.get_point_to_points_adjacency()
>>> # Get neighbors of point 0
>>> neighbors_of_point_0 = adj.to_list()[0]
is_manifold(
check_level: Literal['facets', 'edges', 'full'] = 'full',
) bool[source]#

Check if mesh is a valid topological manifold.

A mesh is a manifold if it locally looks like Euclidean space at every point. This function checks various topological constraints depending on the check level.

Parameters:

check_level ({"facets", "edges", "full"}, optional) – Level of checking to perform: - “facets”: Only check codimension-1 facets (each appears 1-2 times) - “edges”: Check facets + edge neighborhoods (for 2D/3D meshes) - “full”: Complete manifold validation (default)

Returns:

True if mesh passes the specified manifold checks, False otherwise.

Return type:

bool

Notes

This function checks topological constraints but does not check for geometric self-intersections (which would require expensive spatial queries).

Examples

>>> from physicsnemo.mesh.primitives.surfaces import sphere_icosahedral, cylinder_open
>>> # Valid manifold (sphere)
>>> sphere = sphere_icosahedral.load(subdivisions=3)
>>> assert sphere.is_manifold() == True
>>>
>>> # Manifold with boundary (open cylinder)
>>> cylinder = cylinder_open.load()
>>> assert cylinder.is_manifold() == True  # manifold with boundary is OK
is_watertight() bool[source]#

Check if mesh is watertight (has no boundary).

A mesh is watertight if every codimension-1 facet is shared by exactly 2 cells. This means the mesh forms a closed surface/volume with no holes or gaps.

Returns:

True if mesh is watertight (no boundary facets), False otherwise.

Return type:

bool

Examples

>>> from physicsnemo.mesh.primitives.surfaces import sphere_icosahedral, cylinder_open
>>> # Closed sphere is watertight
>>> sphere = sphere_icosahedral.load(subdivisions=3)
>>> assert sphere.is_watertight() == True
>>>
>>> # Open cylinder with holes at ends
>>> cylinder = cylinder_open.load()
>>> assert cylinder.is_watertight() == False
classmethod load(prefix: str | Path, *args, **kwargs) Any#

Loads a tensordict from disk.

This class method is a proxy to load_memmap().

load_(prefix: str | Path, *args, **kwargs)#

Loads a tensordict from disk within the current tensordict.

This class method is a proxy to load_memmap_().

classmethod load_memmap(
prefix: str | Path,
device: device | None = None,
non_blocking: bool = False,
*,
out: TensorDictBase | None = None,
robust_key: bool | None = None,
) Any#

Loads a memory-mapped tensordict from disk.

Parameters:
  • prefix (str or Path to folder) – the path to the folder where the saved tensordict should be fetched.

  • device (torch.device or equivalent, optional) – if provided, the data will be asynchronously cast to that device. Supports “meta” device, in which case the data isn’t loaded but a set of empty “meta” tensors are created. This is useful to get a sense of the total model size and structure without actually opening any file.

  • non_blocking (bool, optional) – if True, synchronize won’t be called after loading tensors on device. Defaults to False.

  • out (TensorDictBase, optional) – optional tensordict where the data should be written.

  • robust_key (bool, optional) – if True, expects robust key encoding was used when saving and decodes filenames accordingly. If False, uses legacy behavior. If None (default), emits a deprecation warning and falls back to legacy behavior. Will default to True in v0.12.

Examples

>>> from tensordict import TensorDict
>>> td = TensorDict.fromkeys(["a", "b", "c", ("nested", "e")], 0)
>>> td.memmap("./saved_td")
>>> td_load = TensorDict.load_memmap("./saved_td")
>>> assert (td == td_load).all()

This method also allows loading nested tensordicts.

Examples

>>> nested = TensorDict.load_memmap("./saved_td/nested")
>>> assert nested["e"] == 0

A tensordict can also be loaded on “meta” device or, alternatively, as a fake tensor.

Examples

>>> import tempfile
>>> td = TensorDict({"a": torch.zeros(()), "b": {"c": torch.zeros(())}})
>>> with tempfile.TemporaryDirectory() as path:
...     td.save(path)
...     td_load = TensorDict.load_memmap(path, device="meta")
...     print("meta:", td_load)
...     from torch._subclasses import FakeTensorMode
...     with FakeTensorMode():
...         td_load = TensorDict.load_memmap(path)
...         print("fake:", td_load)
meta: TensorDict(
    fields={
        a: Tensor(shape=torch.Size([]), device=meta, dtype=torch.float32, is_shared=False),
        b: TensorDict(
            fields={
                c: Tensor(shape=torch.Size([]), device=meta, dtype=torch.float32, is_shared=False)},
            batch_size=torch.Size([]),
            device=meta,
            is_shared=False)},
    batch_size=torch.Size([]),
    device=meta,
    is_shared=False)
fake: TensorDict(
    fields={
        a: FakeTensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False),
        b: TensorDict(
            fields={
                c: FakeTensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False)},
            batch_size=torch.Size([]),
            device=cpu,
            is_shared=False)},
    batch_size=torch.Size([]),
    device=cpu,
    is_shared=False)
load_state_dict(
state_dict: dict[str, Any],
strict=True,
assign=False,
from_flatten=False,
)#

Loads a state_dict attemptedly in-place on the destination tensorclass.

property mean_curvature_vertices: Tensor#

Compute extrinsic mean curvature at mesh vertices.

Uses the cotangent Laplace-Beltrami operator:

H = (1/2) * ||L @ points|| / voronoi_area

Mean curvature is an extrinsic measure (depends on embedding) and is only defined for codimension-1 manifolds where normal vectors exist.

For 2D surfaces: H = (k1 + k2) / 2 where k1, k2 are principal curvatures

Signed curvature: - Positive: Convex (sphere exterior with outward normals) - Negative: Concave (sphere interior with outward normals) - Zero: Minimal surface (soap film)

The result is cached in _cache["point", "mean_curvature"] for efficiency.

Returns:

Tensor of shape (n_points,) containing signed mean curvature. Isolated vertices have NaN curvature.

Return type:

torch.Tensor

Raises:

ValueError – If mesh is not codimension-1.

Examples

>>> from physicsnemo.mesh.primitives.surfaces import sphere_icosahedral
>>> # Sphere of radius r has H = 1/r
>>> sphere = sphere_icosahedral.load(radius=2.0, subdivisions=3)
>>> H = sphere.mean_curvature_vertices
>>> # H.mean() ≈ 0.5 (= 1/2.0)
memmap(
prefix: str | None = None,
copy_existing: bool = False,
*,
num_threads: int = 0,
return_early: bool = False,
share_non_tensor: bool = False,
existsok: bool = True,
robust_key: bool | None = None,
) Any#

Writes all tensors onto a corresponding memory-mapped Tensor in a new tensordict.

Parameters:
  • prefix (str) – directory prefix where the memory-mapped tensors will be stored. The directory tree structure will mimic the tensordict’s.

  • copy_existing (bool) – If False (default), an exception will be raised if an entry in the tensordict is already a tensor stored on disk with an associated file, but is not saved in the correct location according to prefix. If True, any existing Tensor will be copied to the new location.

Keyword Arguments:
  • num_threads (int, optional) – the number of threads used to write the memmap tensors. Defaults to 0.

  • return_early (bool, optional) – if True and num_threads>0, the method will return a future of the tensordict.

  • share_non_tensor (bool, optional) – if True, the non-tensor data will be shared between the processes and writing operation (such as inplace update or set) on any of the workers within a single node will update the value on all other workers. If the number of non_tensor leaves is high (e.g., sharing large stacks of non-tensor data) this may result in OOM or similar errors. Defaults to False.

  • existsok (bool, optional) – if False, an exception will be raised if a tensor already exists in the same path. Defaults to True.

  • robust_key (bool, optional) – if True, uses robust key encoding that safely handles keys with path separators and special characters. If False, uses legacy behavior (keys used as-is). If None (default), emits a deprecation warning and falls back to legacy behavior. Will default to True in v0.12.

The TensorDict is then locked, meaning that any writing operations that isn’t in-place will throw an exception (eg, rename, set or remove an entry). Once the tensordict is unlocked, the memory-mapped attribute is turned to False, because cross-process identity is not guaranteed anymore.

Returns:

A new tensordict with the tensors stored on disk if return_early=False, otherwise a TensorDictFuture instance.

Note

Serialising in this fashion might be slow with deeply nested tensordicts, so it is not recommended to call this method inside a training loop.

memmap_(
prefix: str | None = None,
copy_existing: bool = False,
*,
num_threads: int = 0,
return_early: bool = False,
share_non_tensor: bool = False,
existsok: bool = True,
robust_key: bool | None = None,
) Any#

Writes all tensors onto a corresponding memory-mapped Tensor, in-place.

Parameters:
  • prefix (str) – directory prefix where the memory-mapped tensors will be stored. The directory tree structure will mimic the tensordict’s.

  • copy_existing (bool) – If False (default), an exception will be raised if an entry in the tensordict is already a tensor stored on disk with an associated file, but is not saved in the correct location according to prefix. If True, any existing Tensor will be copied to the new location.

Keyword Arguments:
  • num_threads (int, optional) – the number of threads used to write the memmap tensors. Defaults to 0.

  • return_early (bool, optional) – if True and num_threads>0, the method will return a future of the tensordict. The resulting tensordict can be queried using future.result().

  • share_non_tensor (bool, optional) – if True, the non-tensor data will be shared between the processes and writing operation (such as inplace update or set) on any of the workers within a single node will update the value on all other workers. If the number of non-tensor leaves is high (e.g., sharing large stacks of non-tensor data) this may result in OOM or similar errors. Defaults to False.

  • existsok (bool, optional) – if False, an exception will be raised if a tensor already exists in the same path. Defaults to True.

  • robust_key (bool, optional) – if True, uses robust key encoding that safely handles keys with path separators and special characters. If False, uses legacy behavior (keys used as-is). If None (default), emits a deprecation warning and falls back to legacy behavior. Will default to True in v0.12.

The TensorDict is then locked, meaning that any writing operations that isn’t in-place will throw an exception (eg, rename, set or remove an entry). Once the tensordict is unlocked, the memory-mapped attribute is turned to False, because cross-process identity is not guaranteed anymore.

Returns:

self if return_early=False, otherwise a TensorDictFuture instance.

Note

Serialising in this fashion might be slow with deeply nested tensordicts, so it is not recommended to call this method inside a training loop.

memmap_like(
prefix: str | None = None,
copy_existing: bool = False,
*,
existsok: bool = True,
num_threads: int = 0,
return_early: bool = False,
share_non_tensor: bool = False,
robust_key: bool | None = None,
) Any#

Creates a contentless Memory-mapped tensordict with the same shapes as the original one.

Parameters:
  • prefix (str) – directory prefix where the memory-mapped tensors will be stored. The directory tree structure will mimic the tensordict’s.

  • copy_existing (bool) – If False (default), an exception will be raised if an entry in the tensordict is already a tensor stored on disk with an associated file, but is not saved in the correct location according to prefix. If True, any existing Tensor will be copied to the new location.

Keyword Arguments:
  • num_threads (int, optional) – the number of threads used to write the memmap tensors. Defaults to 0.

  • return_early (bool, optional) – if True and num_threads>0, the method will return a future of the tensordict.

  • share_non_tensor (bool, optional) – if True, the non-tensor data will be shared between the processes and writing operation (such as inplace update or set) on any of the workers within a single node will update the value on all other workers. If the number of non-tensor leaves is high (e.g., sharing large stacks of non-tensor data) this may result in OOM or similar errors. Defaults to False.

  • existsok (bool, optional) – if False, an exception will be raised if a tensor already exists in the same path. Defaults to True.

  • robust_key (bool, optional) – if True, uses robust key encoding that safely handles keys with path separators and special characters. If False, uses legacy behavior (keys used as-is). If None (default), emits a deprecation warning and falls back to legacy behavior. Will default to True in v0.12.

The TensorDict is then locked, meaning that any writing operations that isn’t in-place will throw an exception (eg, rename, set or remove an entry). Once the tensordict is unlocked, the memory-mapped attribute is turned to False, because cross-process identity is not guaranteed anymore.

Returns:

A new TensorDict instance with data stored as memory-mapped tensors if return_early=False, otherwise a TensorDictFuture instance.

Note

This is the recommended method to write a set of large buffers on disk, as memmap_() will copy the information, which can be slow for large content.

Examples

>>> td = TensorDict({
...     "a": torch.zeros((3, 64, 64), dtype=torch.uint8),
...     "b": torch.zeros(1, dtype=torch.int64),
... }, batch_size=[]).expand(1_000_000)  # expand does not allocate new memory
>>> buffer = td.memmap_like("/path/to/dataset")
memmap_refresh_()#

Refreshes the content of the memory-mapped tensordict if it has a saved_path.

This method will raise an exception if no path is associated with it.

classmethod merge(
meshes: Sequence[Mesh],
global_data_strategy: Literal['stack'] = 'stack',
) Mesh[source]#

Merge multiple meshes into a single mesh.

Parameters:
  • meshes (Sequence[Mesh]) – List of Mesh objects to merge. All constituent tensors across all meshes must reside on the same device.

  • global_data_strategy ({"stack"}) – Strategy for handling global_data. Currently only “stack” is supported, which stacks global_data fields along a new dimension.

Returns:

A new Mesh object containing all the merged data.

Return type:

Mesh

Raises:
  • ValueError – If the meshes list is empty, or if meshes have inconsistent dimensions or cell_data keys.

  • TypeError – If any element in meshes is not a Mesh object.

  • RuntimeError – If tensors from different meshes reside on different devices.

pad(
target_n_points: int | None = None,
target_n_cells: int | None = None,
data_padding_value: float = nan,
) Mesh[source]#

Pad points and cells arrays to specified sizes.

This is the low-level padding method that performs the actual padding operation. Padding uses null/degenerate elements that don’t affect computations: - Points: Additional points at the last existing point (preserves bounding box) - cells: Degenerate cells with all vertices at the last existing point (zero area) - cell data: NaN-valued padding for all cell data fields (default)

Parameters:
  • target_n_points (int or None, optional) – Target number of points. If None, no point padding is applied. Must be >= current n_points if specified. Also accepts SymInt for torch.compile.

  • target_n_cells (int or None, optional) – Target number of cells. If None, no cell padding is applied. Must be >= current n_cells if specified. Also accepts SymInt for torch.compile.

  • data_padding_value (float) – Value to use for padding data fields. Defaults to NaN.

Returns:

A new Mesh with padded arrays. If both targets are None or equal to current sizes, returns self unchanged.

Return type:

Mesh

Raises:

ValueError – If target sizes are less than current sizes.

Examples

>>> mesh = Mesh(points, cells)  # 100 points, 200 cells
>>> padded = mesh.pad(target_n_points=128, target_n_cells=256)
>>> padded.n_points  # 128
>>> padded.n_cells   # 256
pad_to_next_power(
power: float = 1.5,
data_padding_value: float = nan,
) Mesh[source]#

Pads points and cells arrays to their next power of power (integer-floored).

This is useful for torch.compile with dynamic=False, where fixed tensor shapes are required. By padding to powers of a base (default 1.5), we can reuse compiled kernels across a reasonable range of mesh sizes while minimizing memory overhead.

This method computes the target sizes as floor(power^n) for the smallest n such that the result is >= the current size, then calls .pad() to perform the actual padding.

Parameters:
  • power (float) – Base for computing the next power. Must be > 1. Provides a good balance between memory efficiency and compile cache hits.

  • data_padding_value (float) – Value to use for padding data fields. Defaults to NaN.

Returns:

A new Mesh with padded points and cells arrays. The padding uses null elements that don’t affect geometric computations.

Return type:

Mesh

Raises:

ValueError – If power <= 1.

Examples

>>> mesh = Mesh(points, cells)  # 100 points, 200 cells
>>> padded = mesh.pad_to_next_power(power=1.5)
>>> # Points padded to floor(1.5^n) >= 100, cells to floor(1.5^m) >= 200
>>> # For power=1.5: 100 points -> 129 points, 200 cells -> 216 cells
>>> # Padding cells have zero area and don't affect computations
point_data_to_cell_data(
overwrite_keys: bool = False,
) Mesh[source]#

Convert point data to cell data by averaging.

For each cell, computes the average of the point data values from all points (vertices) that define that cell. The resulting cell data is added to the mesh’s cell_data dictionary. Original point data is preserved.

Parameters:

overwrite_keys (bool) – If True, silently overwrite any existing cell_data keys. If False, raise an error if a key already exists in cell_data.

Returns:

New Mesh with converted data added to cell_data. Original point_data is preserved.

Return type:

Mesh

Raises:

ValueError – If a point_data key already exists in cell_data and overwrite_keys=False.

Examples

>>> mesh = Mesh(points, cells, point_data={"temperature": point_temps})
>>> mesh_with_cell_data = mesh.point_data_to_cell_data()
>>> # Now mesh has both point_data["temperature"] and cell_data["temperature"]
property point_normals: Tensor#

Compute angle-area-weighted normal vectors at mesh vertices.

This property returns the canonical/default point normals using combined angle and area weighting (Maya default). For other weighting schemes (unweighted, area, angle), use compute_point_normals().

Angle-area weighting ensures that each face’s contribution is weighted by both its area and the interior angle at the vertex, balancing both geometric factors for high-quality normals.

The result is cached in _cache["point", "normals"] for efficiency.

Returns:

Tensor of shape (n_points, n_spatial_dims) containing unit normal vectors at each vertex. For isolated points (with no adjacent cells), the normal is a zero vector.

Return type:

torch.Tensor

Raises:

ValueError – If the mesh is not codimension-1 (n_manifold_dims ≠ n_spatial_dims - 1).

See also

compute_point_normals

Compute point normals with explicit weighting choice.

cell_normals

Compute cell (face) normals.

Examples

>>> # Triangle mesh in 3D
>>> mesh = create_triangle_mesh_3d()
>>> normals = mesh.point_normals  # (n_points, 3), angle-area-weighted
>>> # Normals are unit vectors (or zero for isolated points)
>>> assert torch.allclose(normals.norm(dim=-1), torch.ones(mesh.n_points), atol=1e-6)
property quality_metrics#

Compute geometric quality metrics for all cells.

Returns:

Per-cell quality metrics: - aspect_ratio: max_edge / characteristic_length - edge_length_ratio: max_edge / min_edge - min_angle, max_angle: Interior angles (triangles only) - quality_score: Combined metric in [0,1] (1.0 is perfect)

Return type:

TensorDict

Examples

>>> from physicsnemo.mesh.primitives.basic import two_triangles_2d
>>> mesh = two_triangles_2d.load()
>>> metrics = mesh.quality_metrics
>>> assert "quality_score" in metrics.keys()
rotate(
angle: float,
axis: Tensor | list | tuple | Literal['x', 'y', 'z'] | None = None,
center: Tensor | list | tuple | None = None,
transform_point_data: bool = False,
transform_cell_data: bool = False,
transform_global_data: bool = False,
) Mesh[source]#

Rotate the mesh about an axis by a specified angle.

Convenience wrapper for physicsnemo.mesh.transformations.rotate().

Parameters:
  • angle (float) – Rotation angle in radians.

  • axis (torch.Tensor or list or tuple or {"x", "y", "z"}, optional) – Rotation axis vector. None for 2D, shape (3,) for 3D. String literals “x”, “y”, “z” are converted to unit vectors (1,0,0), (0,1,0), (0,0,1) respectively.

  • center (torch.Tensor or list or tuple, optional) – Center point for rotation.

  • transform_point_data (bool) – If True, rotate vector/tensor fields in point_data.

  • transform_cell_data (bool) – If True, rotate vector/tensor fields in cell_data.

  • transform_global_data (bool) – If True, rotate vector/tensor fields in global_data.

Returns:

New Mesh with rotated geometry.

Return type:

Mesh

sample_data_at_points(
query_points: Tensor,
data_source: Literal['cells', 'points'] = 'cells',
multiple_cells_strategy: Literal['mean', 'nan'] = 'mean',
project_onto_nearest_cell: bool = False,
tolerance: float = 1e-06,
bvh: Any = None,
) TensorDict[source]#

Extract or interpolate mesh data at specified query points.

This method retrieves mesh data at arbitrary spatial locations. Note that “sample” here means “extract/query at specific points” - NOT random sampling. For random point sampling, see sample_random_points_on_cells().

Containment queries are BVH-accelerated (O(n_queries * log(n_cells))).

Parameters:
  • query_points (torch.Tensor) – Query point locations, shape (n_queries, n_spatial_dims).

  • data_source ({"cells", "points"}, optional) – How to retrieve data: - “cells”: Use cell data directly (no interpolation) - “points”: Interpolate point data using barycentric coordinates

  • multiple_cells_strategy ({"mean", "nan"}, optional) – How to handle query points in multiple cells: - “mean”: Return arithmetic mean of values from all containing cells - “nan”: Return NaN for ambiguous points

  • project_onto_nearest_cell (bool, optional) – If True, snaps each query point to the centroid of the nearest cell before containment testing. Useful for codimension != 0 manifolds.

  • tolerance (float, optional) – Tolerance for considering a point inside a cell.

  • bvh (BVH or None, optional) – Pre-built Bounding Volume Hierarchy. If None (default), one is built automatically. For repeated queries, pre-build with BVH.from_mesh(mesh) and pass it here to avoid redundant work.

Returns:

Data for each query point. Values are NaN for query points outside the mesh.

Return type:

TensorDict

Examples

>>> import torch
>>> from physicsnemo.mesh.primitives.basic import two_triangles_2d
>>> mesh = two_triangles_2d.load()
>>> mesh.cell_data["pressure"] = torch.tensor([1.0, 2.0])
>>> query_pts = torch.tensor([[0.3, 0.3], [0.8, 0.5]])
>>> data = mesh.sample_data_at_points(query_pts, data_source="cells")
sample_random_points_on_cells(
cell_indices: Sequence[int] | Tensor | None = None,
alpha: float = 1.0,
) Tensor[source]#

Sample random points on specified cells of the mesh.

Uses a Dirichlet distribution to generate barycentric coordinates, which are then used to compute random points as weighted combinations of cell vertices. The concentration parameter alpha controls the distribution of samples within each cell (simplex).

This is a convenience method that delegates to physicsnemo.mesh.sampling.sample_random_points_on_cells.

Parameters:
  • cell_indices (Sequence[int] or torch.Tensor or None, optional) – Indices of cells to sample from. Can be a Sequence or tensor. Allows repeated indices to sample multiple points from the same cell. If None, samples one point from each cell (equivalent to arange(n_cells)). Shape: (n_samples,) where n_samples is the number of points to sample.

  • alpha (float, optional) – Concentration parameter for the Dirichlet distribution. Controls how samples are distributed within each cell: - alpha = 1.0: Uniform distribution over the simplex (default) - alpha > 1.0: Concentrates samples toward the center of each cell - alpha < 1.0: Concentrates samples toward vertices and edges

Returns:

Random points on cells, shape (n_samples, n_spatial_dims). Each point lies within its corresponding cell. If cell_indices is None, n_samples = n_cells.

Return type:

torch.Tensor

Raises:
  • NotImplementedError – If alpha != 1.0 and torch.compile is being used. This is due to a PyTorch limitation with Gamma distributions under torch.compile.

  • IndexError – If any cell_indices are out of bounds.

Examples

>>> import torch
>>> from physicsnemo.mesh.primitives.basic import two_triangles_2d
>>> mesh = two_triangles_2d.load()
>>> # Sample one point from each cell uniformly
>>> points = mesh.sample_random_points_on_cells()
>>> assert points.shape == (mesh.n_cells, mesh.n_spatial_dims)
save(
prefix: str | None = None,
copy_existing: bool = False,
*,
num_threads: int = 0,
return_early: bool = False,
share_non_tensor: bool = False,
robust_key: bool | None = None,
) Any#

Saves the tensordict to disk.

This function is a proxy to memmap().

scale(
factor: float | Tensor,
center: Tensor | None = None,
transform_point_data: bool = False,
transform_cell_data: bool = False,
transform_global_data: bool = False,
assume_invertible: bool | None = None,
) Mesh[source]#

Scale the mesh by specified factor(s).

Convenience wrapper for physicsnemo.mesh.transformations.scale().

Parameters:
  • factor (float or torch.Tensor) – Scale factor (scalar) or factors (per-dimension).

  • center (torch.Tensor, optional) – Center point for scaling.

  • transform_point_data (bool) – If True, scale vector/tensor fields in point_data.

  • transform_cell_data (bool) – If True, scale vector/tensor fields in cell_data.

  • transform_global_data (bool) – If True, scale vector/tensor fields in global_data.

  • assume_invertible (bool or None, optional) – Controls cache propagation: - True: Assume all factors are non-zero (compile-safe). - False: Skip cache propagation (compile-safe). - None: Check at runtime (may cause graph breaks).

Returns:

New Mesh with scaled geometry.

Return type:

Mesh

set(
key: NestedKey,
value: Any,
inplace: bool = False,
non_blocking: bool = False,
)#

Sets a new key-value pair.

Parameters:
  • key (str, tuple of str) – name of the key to be set. If tuple of str it is equivalent to chained calls of getattr followed by a final setattr.

  • value (Any) – value to be stored in the tensorclass

  • inplace (bool, optional) – if True, set will tentatively try to update the value in-place. If False or if the key isn’t present, the value will be simply written at its destination.

Returns:

self

slice_cells(
indices: int | slice | EllipsisType | None | Tensor | Sequence[int | bool | slice],
) Mesh[source]#

Returns a new Mesh with a subset of the cells.

Parameters:

indices (int or slice or torch.Tensor) – Indices or mask to select cells.

Returns:

New Mesh with subset of cells.

Return type:

Mesh

slice_points(
indices: int | slice | EllipsisType | None | Tensor | Sequence[int | bool],
) Mesh[source]#

Returns a new Mesh with a subset of the points.

This method filters points and automatically updates cells to maintain consistency. Cells that reference any removed points are also removed, and the remaining cells have their indices remapped to the new point numbering.

Parameters:

indices (int or slice or Ellipsis or None or torch.Tensor or Sequence) – Indices or mask to select points. Supports: - int: Single point index - slice: Python slice object - Ellipsis or None: Keep all points (returns self) - torch.Tensor: Integer indices or boolean mask - Sequence[int | bool]: List/tuple of indices or boolean mask

Returns:

New Mesh with subset of points. Cells that reference any removed points are also removed, and remaining cell indices are remapped.

Return type:

Mesh

Examples

>>> import torch
>>> from physicsnemo.mesh import Mesh
>>> # Create a mesh with 4 points and 2 triangular cells
>>> points = torch.tensor([[0., 0.], [1., 0.], [1., 1.], [0., 1.]])
>>> cells = torch.tensor([[0, 1, 2], [0, 2, 3]])
>>> mesh = Mesh(points=points, cells=cells)
>>> # Keep only points 0 and 2 - both cells are removed (they need points 1 or 3)
>>> sliced = mesh.slice_points([0, 2])
>>> sliced.n_points, sliced.n_cells
(2, 0)
>>> # Keep points 0, 1, 2 - first cell is preserved with remapped indices
>>> sliced = mesh.slice_points([0, 1, 2])
>>> sliced.n_points, sliced.n_cells
(3, 1)
>>> sliced.cells.tolist()
[[0, 1, 2]]
state_dict(
destination=None,
prefix='',
keep_vars=False,
flatten=False,
) dict[str, Any]#

Returns a state_dict dictionary that can be used to save and load data from a tensorclass.

property statistics#

Compute summary statistics for mesh.

Returns:

Mesh statistics including counts, edge length distributions, area distributions, and quality metrics.

Return type:

dict

Examples

>>> from physicsnemo.mesh.primitives.basic import two_triangles_2d
>>> mesh = two_triangles_2d.load()
>>> stats = mesh.statistics
>>> assert "n_points" in stats and "n_cells" in stats
strip_caches() Mesh[source]#

Return a new mesh with all cached values removed.

Cached values (stored under the _cache key in data TensorDicts) are computed lazily for expensive operations like normals, areas, and curvature. This method creates a new mesh without these cached values, which is useful for:

  • Accurate benchmarking (prevents false performance benefits from caching)

  • Reducing memory usage

  • Forcing recomputation of cached values

Returns:

A new mesh with the same geometry and data, but without cached values.

Return type:

Mesh

Examples

>>> from physicsnemo.mesh.primitives.surfaces import sphere_icosahedral
>>> mesh = sphere_icosahedral.load(subdivisions=2)
>>> _ = mesh.cell_normals  # Triggers caching
>>> mesh_clean = mesh.strip_caches()  # Remove cached normals
subdivide(
levels: int = 1,
filter: Literal['linear', 'butterfly', 'loop'] = 'linear',
) Mesh[source]#

Subdivide the mesh using iterative application of subdivision schemes.

Subdivision refines the mesh by splitting each n-simplex into 2^n child simplices. Multiple subdivision schemes are supported, each with different geometric and smoothness properties.

This method applies the chosen subdivision scheme iteratively for the specified number of levels. Each level independently subdivides the current mesh.

Parameters:
  • levels (int, optional) – Number of subdivision iterations to perform. Each level increases mesh resolution exponentially: - 0: No subdivision (returns original mesh) - 1: Each cell splits into 2^n children - 2: Each cell splits into 4^n children - k: Each cell splits into (2^k)^n children

  • filter ({"linear", "butterfly", "loop"}, optional) –

    Subdivision scheme to use: - “linear”: Simple midpoint subdivision (interpolating).

    New vertices at exact edge midpoints. Works for any dimension. Preserves original vertices.

    • ”butterfly”: Weighted stencil subdivision (interpolating). New vertices use weighted neighbor stencils for smoother results. Currently only supports 2D manifolds (triangular meshes). Preserves original vertices.

    • ”loop”: Valence-based subdivision (approximating). Both old and new vertices are repositioned for C² smoothness. Currently only supports 2D manifolds (triangular meshes). Original vertices move to new positions.

Returns:

Subdivided mesh with refined geometry and connectivity. - Manifold and spatial dimensions are preserved - Point data is interpolated to new vertices - Cell data is propagated from parents to children - Global data is preserved unchanged

Return type:

Mesh

Raises:
  • ValueError – If levels < 0 or if filter is not one of the supported schemes.

  • NotImplementedError – If butterfly/loop filter used with non-2D manifold.

Notes

Multi-level subdivision is achieved by iterative application. For levels=3, this is equivalent to calling subdivide(levels=1) three times in sequence. This is the standard approach for all subdivision schemes.

Examples

>>> from physicsnemo.mesh.primitives.basic import two_triangles_2d
>>> # Linear subdivision of triangular mesh
>>> mesh = two_triangles_2d.load()
>>> refined = mesh.subdivide(levels=2, filter="linear")
>>> # Each triangle splits into 4, twice: 2 -> 8 -> 32 triangles
>>> assert refined.n_cells == mesh.n_cells * 16
to_tensordict(
*,
retain_none: bool | None = None,
) TensorDict#

Convert the tensorclass into a regular TensorDict.

Makes a copy of all entries. Memmap and shared memory tensors are converted to regular tensors.

Parameters:

retain_none (bool) – if True, the None values will be written in the tensordict. Otherwise they will be discrarded. Default: True.

Returns:

A new TensorDict object containing the same values as the tensorclass.

transform(
matrix: Tensor,
transform_point_data: bool = False,
transform_cell_data: bool = False,
transform_global_data: bool = False,
assume_invertible: bool | None = None,
) Mesh[source]#

Apply a linear transformation to the mesh.

Convenience wrapper for physicsnemo.mesh.transformations.transform().

Parameters:
  • matrix (torch.Tensor) – Transformation matrix, shape (new_n_spatial_dims, n_spatial_dims).

  • transform_point_data (bool) – If True, transform vector/tensor fields in point_data.

  • transform_cell_data (bool) – If True, transform vector/tensor fields in cell_data.

  • transform_global_data (bool) – If True, transform vector/tensor fields in global_data.

  • assume_invertible (bool or None, optional) – Controls cache propagation for square matrices: - True: Assume matrix is invertible (compile-safe). - False: Skip cache propagation (compile-safe). - None: Check at runtime (may cause graph breaks).

Returns:

New Mesh with transformed geometry.

Return type:

Mesh

translate(
offset: Tensor | list | tuple,
) Mesh[source]#

Apply a translation to the mesh.

Convenience wrapper for physicsnemo.mesh.transformations.translate().

Parameters:

offset (torch.Tensor or list or tuple) – Translation vector, shape (n_spatial_dims,).

Returns:

New Mesh with translated geometry.

Return type:

Mesh

unbind(dim: int)#

Returns a tuple of indexed tensorclass instances unbound along the indicated dimension.

Resulting tensorclass instances will share the storage of the initial tensorclass instance.

validate(
check_degenerate_cells: bool = True,
check_duplicate_vertices: bool = True,
check_inverted_cells: bool = False,
check_out_of_bounds: bool = True,
check_manifoldness: bool = False,
tolerance: float = 1e-10,
raise_on_error: bool = False,
)[source]#

Validate mesh integrity and detect common errors.

Convenience method that delegates to physicsnemo.mesh.validation.validate_mesh.

Parameters:
  • check_degenerate_cells (bool, optional) – Check for zero/negative area cells.

  • check_duplicate_vertices (bool, optional) – Check for coincident vertices.

  • check_inverted_cells (bool, optional) – Check for negative orientation.

  • check_out_of_bounds (bool, optional) – Check cell indices are valid.

  • check_manifoldness (bool, optional) – Check manifold topology (2D only).

  • tolerance (float, optional) – Tolerance for geometric checks.

  • raise_on_error (bool, optional) – Raise ValueError on first error vs return report.

Returns:

Dictionary with validation results.

Return type:

dict

Examples

>>> from physicsnemo.mesh.primitives.basic import two_triangles_2d
>>> mesh = two_triangles_2d.load()
>>> report = mesh.validate()
>>> assert report["valid"] == True