Source code for geomfum.wrap.pyfm

"""pyFM wrapper."""

import geomstats.backend as gs
import numpy as np
import pyFM.mesh
import pyFM.mesh.geometry
import pyFM.signatures
import scipy

import geomfum.backend as xgs
from geomfum.descriptor._base import SpectralDescriptor
from geomfum.descriptor.spectral import WksDefaultDomain, hks_default_domain
from geomfum.laplacian import BaseLaplacianFinder
from geomfum.operator import FunctionalOperator, VectorFieldOperator
from geomfum.sample import BaseSampler


[docs] class PyfmMeshLaplacianFinder(BaseLaplacianFinder): """Algorithm to find the Laplacian of a mesh.""" def __call__(self, shape): """Apply algorithm. Parameters ---------- shape : TriangleMesh Mesh. Returns ------- stiffness_matrix : sparse.csc_matrix, shape=[n_vertices, n_vertices] Stiffness matrix. mass_matrix : sparse.csc_matrix, shape=[n_vertices, n_vertices] Diagonal lumped mass matrix. """ return ( xgs.sparse.from_scipy_csc( pyFM.mesh.laplacian.cotangent_weights(shape.vertices, shape.faces) ), xgs.sparse.from_scipy_dia( pyFM.mesh.laplacian.dia_area_mat(shape.vertices, shape.faces) ), )
[docs] class PyfmHeatKernelSignature(SpectralDescriptor): """Heat kernel signature using pyFM. Parameters ---------- scale : bool Whether to scale weights to sum to one. n_domain : int Number of domain points. Ignored if ``domain`` is not None. domain : callable or array-like, shape=[n_domain] Method to compute time points (``f(shape, n_domain)``) or time points. use_landmarks : bool Whether to use landmarks. """ def __init__(self, scale=True, n_domain=3, domain=None, use_landmarks=False): super().__init__( domain or (lambda shape: hks_default_domain(shape, n_domain=n_domain)), use_landmarks=use_landmarks, ) self.scale = scale def __call__(self, shape): """Compute descriptor. Parameters ---------- shape : Shape. Shape with basis. Returns ------- descr : array-like, shape=[n_domain, n_vertices] Descriptor. """ domain = self.domain(shape) if callable(self.domain) else self.domain if self.use_landmarks: return gs.from_numpy( pyFM.signatures.lm_HKS( shape.basis.vals, shape.basis.vecs, shape.landmark_indices, domain, scaled=self.scale, ).T ) return gs.from_numpy( pyFM.signatures.HKS( shape.basis.vals, shape.basis.vecs, domain, scaled=self.scale ).T )
[docs] class PyfmWaveKernelSignature(SpectralDescriptor): """Wave kernel signature using pyFM. Parameters ---------- scale : bool Whether to scale weights to sum to one. sigma : float Standard deviation. Ignored if ``domain`` is a callable (other than default one). n_domain : int Number of energy points. Ignored if ``domain`` is not a callable. domain : callable or array-like, shape=[n_domain] Method to compute domain points (``f(shape)``) or domain points. use_landmarks : bool Whether to use landmarks. """ def __init__( self, scale=True, sigma=None, n_domain=3, domain=None, use_landmarks=False ): super().__init__( domain or WksDefaultDomain(n_domain=n_domain, sigma=sigma), use_landmarks=use_landmarks, ) self.scale = scale self.sigma = sigma def __call__(self, shape): """Compute descriptor. Parameters ---------- shape : Shape. Shape with basis. Returns ------- descr : array-like, shape=[{n_domain, n_landmarks*n_domain}, n_vertices] Descriptor. """ if callable(self.domain): domain, sigma = self.domain(shape) else: domain = self.domain sigma = self.sigma if self.use_landmarks: return pyFM.signatures.lm_WKS( shape.basis.vals, shape.basis.vecs, shape.landmark_indices, domain, sigma, scaled=self.scale, ).T return pyFM.signatures.WKS( shape.basis.vals, shape.basis.vecs, domain, sigma, scaled=self.scale ).T
[docs] class PyfmFaceValuedGradient(FunctionalOperator): """Gradient of a function on a mesh. Computes the gradient of a function on f using linear interpolation between vertices. """ def __call__(self, point): """Apply operator. Parameters ---------- point : array-like, shape=[..., n_vertices] Function value on each vertex. Returns ------- gradient : array-like, shape=[..., n_faces] Gradient of the function on each face. """ gradient = pyFM.mesh.geometry.grad_f( point.T, self._shape.vertices, self._shape.faces, self._shape.face_normals, face_areas=self._shape.face_areas, ) if gradient.ndim > 2: return gs.moveaxis(gradient, 0, 1) return gradient
[docs] class PyfmFaceDivergenceOperator(VectorFieldOperator): """Divergence of a function on a mesh.""" def __call__(self, vector): """Divergence of a vector field on a mesh. Parameters ---------- vector : array-like, shape=[..., n_faces, 3] Vector field on the mesh. Returns ------- divergence : array-like, shape=[..., n_vertices] Divergence of the vector field on each vertex. """ if vector.ndim > 2: vector = np.moveaxis(vector, 0, 1) div = pyFM.mesh.geometry.div_f( vector, self._shape.vertices, self._shape.faces, self._shape.face_normals, vert_areas=self._shape.vertex_areas, ) if div.ndim > 1: return np.moveaxis(div, 0, 1) return div
[docs] class PyFmFaceOrientationOperator(VectorFieldOperator): r"""Orientation operator associated to a gradient field. For a given function :math:`g` on the vertices, this operator linearly computes :math:`< \grad(f) x \grad(g)`, n> for each vertex by averaging along the adjacent faces. In practice, we compute :math:`< n x \grad(f), \grad(g) >` for simpler computation. """ def __call__(self, vector): """Apply operator. Parameters ---------- vector : array-like, shape=[..., n_faces, 3] Gradient field on the mesh. Returns ------- operator : sparse.csc_matrix or list[sparse.csc_matrix], shape=[n_vertices, n_vertices] Orientation operator. """ return get_orientation_op( vector, self._shape.vertices, self._shape.faces, self._shape.face_normals, self._shape.vertex_areas, )
[docs] def get_orientation_op( grad_field, vertices, faces, normals, per_vert_area, rotated=False ): """ Compute the linear orientation operator associated to a gradient field grad(f). This operator computes g -> < grad(f) x grad(g), n> (given at each vertex) for any function g In practice, we compute < n x grad(f), grad(g) > for simpler computation. Parameters -------------------------------- grad_field : (n_f,3) gradient field on the mesh vertices : (n_v,3) coordinates of vertices faces : (n_f,3) indices of vertices for each face normals : (n_f,3) normals coordinate for each face per_vert_area : (n_v,) voronoi area for each vertex rotated : bool whether gradient field is already rotated by n x grad(f) Returns -------------------------- operator : sparse.csc_matrix or list[sparse.csc_matrix], shape=[n_vertices, n_verticess] (n_v,n_v) orientation operator. Notes ----- * vectorized version of ``pyFm.geometry.mesh.get_orientation_op``. """ n_vertices = per_vert_area.shape[0] per_vert_area = gs.asarray(per_vert_area) v1 = vertices[faces[:, 0]] # (n_f,3) v2 = vertices[faces[:, 1]] # (n_f,3) v3 = vertices[faces[:, 2]] # (n_f,3) # Define (normalized) gradient directions for each barycentric coordinate on each face # Remove normalization since it will disappear later on after multiplcation Jc1 = gs.cross(normals, v3 - v2) / 2 Jc2 = gs.cross(normals, v1 - v3) / 2 Jc3 = gs.cross(normals, v2 - v1) / 2 # Rotate the gradient field if rotated: rot_field = grad_field else: rot_field = gs.cross(normals, grad_field) # (n_f,3) I = gs.concatenate([faces[:, 0], faces[:, 1], faces[:, 2]]) J = gs.concatenate([faces[:, 1], faces[:, 2], faces[:, 0]]) # Compute pairwise dot products between the gradient directions # and the gradient field Sij = ( 1 / 3 * gs.concatenate( [ gs.einsum("ij,...ij->...i", Jc2, rot_field), gs.einsum("ij,...ij->...i", Jc3, rot_field), gs.einsum("ij,...ij->...i", Jc1, rot_field), ], axis=-1, ) ) Sji = ( 1 / 3 * gs.concatenate( [ gs.einsum("ij,...ij->...i", Jc1, rot_field), gs.einsum("ij,...ij->...i", Jc2, rot_field), gs.einsum("ij,...ij->...i", Jc3, rot_field), ], axis=-1, ) ) In = gs.concatenate([I, J, I, J]) Jn = gs.concatenate([J, I, I, J]) Sn = gs.concatenate([Sij, Sji, -Sij, -Sji], axis=-1) inv_area = xgs.sparse.dia_matrix(1 / per_vert_area, shape=(n_vertices, n_vertices)) indices = gs.stack([In, Jn]) if Sn.ndim == 1: W = xgs.sparse.csc_matrix( indices, Sn, shape=(n_vertices, n_vertices), coalesce=True ) return inv_area @ W out = [] for Sn_ in Sn: W = xgs.sparse.csc_matrix( indices, Sn_, shape=(n_vertices, n_vertices), coalesce=True ) out.append(inv_area @ W) return out
[docs] class PyfmEuclideanFarthestVertexSampler(BaseSampler): """Farthest point Euclidean sampling. Parameters ---------- min_n_samples : int Minimum number of samples to target. """ def __init__(self, min_n_samples): super().__init__() self.min_n_samples = min_n_samples
[docs] def sample(self, shape): """Sample using farthest point sampling. Parameters ---------- shape : TriangleMesh Mesh. Returns ------- samples : array-like, shape=[n_samples, 3] Coordinates of samples. """ def dist_func(i): return np.linalg.norm(shape.vertices - shape.vertices[i, None, :], axis=1) return pyFM.mesh.geometry.farthest_point_sampling_call( dist_func, self.min_n_samples, n_points=shape.n_vertices, verbose=False, )