Source code for geomfum.shape.mesh

"""Definition of triangle mesh."""

import gsops.backend as gs

from geomfum.io import load_mesh
from geomfum.metric import HeatDistanceMetric
from geomfum.operator import (
    FaceDivergenceOperator,
    FaceOrientationOperator,
    FaceValuedGradient,
)
from geomfum.shape.shape_utils import (
    compute_edge_tangent_vectors,
    compute_tangent_frames,
)

from ._base import Shape


[docs] class TriangleMesh(Shape): """Triangulated surface mesh with vertices, faces, and differential operators. Parameters ---------- vertices : array-like, shape=[n_vertices, 3] Vertices of the mesh. faces : array-like, shape=[n_faces, 3] Faces of the mesh. """ def __init__( self, vertices, faces, ): super().__init__(is_mesh=True) self.vertices = gs.asarray(vertices) self.faces = gs.asarray(faces) self._edges = None self._face_normals = None self._face_areas = None self._face_area_vectors = None self._vertex_areas = None self._vertex_normals = None self._vertex_tangent_frames = None self._edge_tangent_vectors = None self._dist_matrix = None self.metric = None self._at_init() def _at_init(self): self.equip_with_operator( "face_valued_gradient", FaceValuedGradient.from_registry ) self.equip_with_operator( "face_divergence", FaceDivergenceOperator.from_registry ) self.equip_with_operator( "face_orientation_operator", FaceOrientationOperator.from_registry )
[docs] @classmethod def from_file( cls, filename, ): """Load mesh from file. Parameters ---------- filename : str Path to the mesh file. Returns ------- mesh : TriangleMesh A triangle mesh. """ vertices, faces = load_mesh(filename) return cls(vertices, faces)
@property def n_vertices(self): """Number of vertices. Returns ------- n_vertices : int """ return self.vertices.shape[0] @property def n_faces(self): """Number of faces. Returns ------- n_faces : int """ return self.faces.shape[0] @property def edges(self): """Edges of the mesh. Returns ------- edges : array-like, shape=[n_edges, 2] """ if self._edges is None: vind012 = gs.concatenate( [self.faces[:, 0], self.faces[:, 1], self.faces[:, 2]] ) vind120 = gs.concatenate( [self.faces[:, 1], self.faces[:, 2], self.faces[:, 0]] ) edges = gs.stack( [ gs.concatenate([vind012, vind120]), gs.concatenate([vind120, vind012]), ], axis=-1, ) edges = gs.unique(edges, axis=0) self._edges = edges[edges[:, 1] > edges[:, 0]] return self._edges @property def face_vertex_coords(self): """Extract vertex coordinates corresponding to each face. Returns ------- vertices : array-like, shape=[{n_faces}, n_per_face_vertex, 3] Coordinates of the ith vertex of that face. """ return gs.stack( [ self.vertices[self.faces[:, 0]], self.vertices[self.faces[:, 1]], self.vertices[self.faces[:, 2]], ], axis=-2, ) @property def face_area_vectors(self): """Face area vectors (unnormalized normals with magnitude equal to face area). Returns ------- area_vectors : array-like, shape=[n_faces, 3] Per-face area vectors. """ if self._face_area_vectors is None: face_vertex_coords = self.face_vertex_coords self._face_area_vectors = gs.cross( face_vertex_coords[:, 1, :] - face_vertex_coords[:, 0, :], face_vertex_coords[:, 2, :] - face_vertex_coords[:, 0, :], ) return self._face_area_vectors @property def face_normals(self): """Unit normal vectors for each face. Returns ------- normals : array-like, shape=[n_faces, 3] Per-face normals. """ if self._face_normals is None: face_vertex_coords = self.face_vertex_coords self._face_normals = gs.cross( face_vertex_coords[:, 1, :] - face_vertex_coords[:, 0, :], face_vertex_coords[:, 2, :] - face_vertex_coords[:, 0, :], ) self._face_normals /= gs.linalg.norm( self._face_normals, axis=1, keepdims=True ) return self._face_normals @property def vertex_normals(self): """Unit normal vectors at vertices (area-weighted average of adjacent face normals). Returns ------- normals : array-like, shape=[n_vertices, 3] Normalized per-vertex normals. """ if self._vertex_normals is None: device = getattr(self.vertices, "device", None) vind012 = gs.concatenate( [self.faces[:, 0], self.faces[:, 1], self.faces[:, 2]] ) zeros = gs.to_device(gs.zeros(len(vind012)), device) normals_repeated = gs.vstack([self.face_normals] * 3) vertex_normals = gs.to_device(gs.zeros_like(self.vertices), device) for c in range(3): normals = normals_repeated[:, c] vertex_normals[:, c] = gs.asarray( gs.sparse.to_dense( gs.sparse.coo_matrix( gs.stack((vind012, zeros)), normals, shape=(self.n_vertices, 1), ) ).flatten() ) vertex_normals = vertex_normals / ( gs.linalg.norm(vertex_normals, axis=1, keepdims=True) + 1e-12 ) self._vertex_normals = vertex_normals return self._vertex_normals @property def face_areas(self): """Area of each triangular face. Returns ------- face_areas : array-like, shape=[n_faces] Per-face areas. """ if self._face_areas is None: self._face_areas = 0.5 * gs.linalg.norm(self.face_area_vectors, axis=1) return self._face_areas @property def vertex_areas(self): """Area associated with each vertex (one-third of adjacent triangle areas). Returns ------- vertex_areas : array-like, shape=[n_vertices] Per-vertex areas. """ area = self.face_areas id_vertices = gs.broadcast_to(gs.reshape(self.faces, (-1,)), self.n_faces * 3) val = gs.reshape( gs.broadcast_to(gs.expand_dims(area, axis=-1), (self.n_faces, 3)), (-1,), ) incident_areas = gs.scatter_sum_1d( index=id_vertices, src=val, ) return incident_areas / 3.0 @property def vertex_tangent_frames(self): """Local orthonormal coordinate frames at each vertex. Returns ------- tangent_frame : array-like, shape=[n_vertices, 3, 3] Tangent frame of the mesh, where: - [n_vertices, 0, :] are the X basis vectors - [n_vertices, 1, :] are the Y basis vectors - [n_vertices, 2, :] are the vertex normals """ if self._vertex_tangent_frames is None: self._vertex_tangent_frames = compute_tangent_frames( self.vertices, self.vertex_normals ) return self._vertex_tangent_frames @property def edge_tangent_vectors(self): """Edge vectors projected onto local tangent planes. Returns ------- edge_tangent_vectors : array-like, shape=[n_edges, 2] Tangent vectors of the edges, projected onto the local tangent plane. """ if self._edge_tangent_vectors is None: edge_tangent_vectors = compute_edge_tangent_vectors( self.vertices, self.edges, self.vertex_tangent_frames, ) self._edge_tangent_vectors = edge_tangent_vectors return self._edge_tangent_vectors @property # ToDo def dist_matrix(self): """Pairwise distances between all vertices using the equipped metric. Returns ------- _dist_matrix : array-like, shape=[n_vertices, n_vertices] Metric distance matrix. """ if self._dist_matrix is None: if self.metric is None: raise ValueError("Metric is not set.") self._dist_matrix = self.metric.dist_matrix() return self._dist_matrix
[docs] def equip_with_metric(self, metric): """Equip mesh with a distance metric. Parameters ---------- metric : class A metric class to use for the mesh. """ if metric == HeatDistanceMetric: self.metric = metric.from_registry(which="pp3d", shape=self) else: self.metric = metric(self) self._dist_matrix = None