Source code for geomstats.geometry.base

"""Abstract classes for manifolds.

Lead authors: Nicolas Guigui and Nina Miolane.
"""

import abc
import math

import geomstats.backend as gs
from geomstats.geometry.complex_manifold import ComplexManifold
from geomstats.geometry.manifold import Manifold
from geomstats.geometry.pullback_metric import PullbackMetric


[docs] class VectorSpace(Manifold, abc.ABC): """Abstract class for vector spaces. Parameters ---------- shape : tuple Shape of the elements of the vector space. The dimension is the product of these values by default. """ def __init__(self, shape, dim=None, **kwargs): if dim is None: dim = math.prod(shape) super().__init__(dim=dim, shape=shape, **kwargs) self._basis = None
[docs] def belongs(self, point, atol=gs.atol): """Evaluate if the point belongs to the vector space. This method checks the shape of the input point. Parameters ---------- point : array-like, shape=[.., *point_shape] Point to test. atol : float Unused here. Returns ------- belongs : array-like, shape=[...,] Boolean evaluating if point belongs to the space. """ belongs = self.shape == point.shape[-self.point_ndim :] shape = point.shape[: -self.point_ndim] if belongs: return gs.ones(shape, dtype=bool) return gs.zeros(shape, dtype=bool)
[docs] @staticmethod def projection(point): """Project a point to the vector space. This method is for compatibility and returns `point`. `point` should have the right shape, Parameters ---------- point: array-like, shape[..., *point_shape] Point. Returns ------- point: array-like, shape[..., *point_shape] Point. """ return gs.copy(point)
[docs] def is_tangent(self, vector, base_point=None, atol=gs.atol): """Check whether the vector is tangent at base_point. Tangent vectors are identified with points of the vector space so this checks the shape of the input vector. Parameters ---------- vector : array-like, shape=[..., *point_shape] Vector. base_point : array-like, shape=[..., *point_shape] Point in the vector space. atol : float Absolute tolerance. Optional, default: backend atol. Returns ------- is_tangent : array-like, shape=[...,] Boolean denoting if vector is a tangent vector at the base point. """ belongs = self.belongs(vector, atol) if base_point is not None and base_point.ndim > vector.ndim: return gs.broadcast_to(belongs, base_point.shape[: -self.point_ndim]) return belongs
[docs] def to_tangent(self, vector, base_point=None): """Project a vector to a tangent space of the vector space. This method is for compatibility and returns vector. Parameters ---------- vector : array-like, shape=[..., *point_shape] Vector. base_point : array-like, shape=[..., *point_shape] Point in the vector space Returns ------- tangent_vec : array-like, shape=[..., *point_shape] Tangent vector at base point. """ tangent_vec = self.projection(vector) if base_point is not None and base_point.ndim > vector.ndim: return gs.broadcast_to(tangent_vec, base_point.shape) return tangent_vec
[docs] def random_point(self, n_samples=1, bound=1.0): """Sample in the vector space with a uniform distribution in a box. Parameters ---------- n_samples : int Number of samples. Optional, default: 1. bound : float Side of hypercube support of the uniform distribution. Optional, default: 1.0 Returns ------- point : array-like, shape=[..., *point_shape] Sample. """ size = self.shape if n_samples != 1: size = (n_samples,) + self.shape point = bound * (gs.random.rand(*size) - 0.5) * 2 return point
@property def basis(self): """Basis of the vector space.""" if self._basis is None: self._basis = self._create_basis() return self._basis @abc.abstractmethod def _create_basis(self): """Create a canonical basis."""
[docs] class ComplexVectorSpace(ComplexManifold, abc.ABC): """Abstract class for complex vector spaces. Parameters ---------- shape : tuple Shape of the elements of the vector space. The dimension is the product of these values by default. default_point_type : str, {'vector', 'matrix'} Point type. Optional, default: 'vector'. """ def __init__(self, shape, dim=None, **kwargs): if dim is None: dim = math.prod(shape) super().__init__(shape=shape, dim=dim, **kwargs) self._basis = None
[docs] def belongs(self, point, atol=gs.atol): """Evaluate if the point belongs to the vector space. This method checks the shape of the input point. Parameters ---------- point : array-like, shape=[.., *point_shape] Point to test. atol : float Unused here. Returns ------- belongs : array-like, shape=[...,] Boolean evaluating if point belongs to the space. """ belongs = self.shape == point.shape[-self.point_ndim :] shape = point.shape[: -self.point_ndim] if belongs: return gs.ones(shape, dtype=bool) return gs.zeros(shape, dtype=bool)
[docs] @staticmethod def projection(point): """Project a point to the vector space. This method is for compatibility and returns `point`. `point` should have the right shape, Parameters ---------- point: array-like, shape[..., *point_shape] Point. Returns ------- point: array-like, shape[..., *point_shape] Point. """ return gs.copy(point)
[docs] def is_tangent(self, vector, base_point=None, atol=gs.atol): """Check whether the vector is tangent at base_point. Tangent vectors are identified with points of the vector space so this checks the shape of the input vector. Parameters ---------- vector : array-like, shape=[..., *point_shape] Vector. base_point : array-like, shape=[..., *point_shape] Point in the vector space. atol : float Absolute tolerance. Optional, default: backend atol. Returns ------- is_tangent : bool Boolean denoting if vector is a tangent vector at the base point. """ belongs = self.belongs(vector, atol) if base_point is not None and base_point.ndim > vector.ndim: return gs.broadcast_to(belongs, base_point.shape[: -self.point_ndim]) return belongs
[docs] def to_tangent(self, vector, base_point=None): """Project a vector to a tangent space of the vector space. This method is for compatibility and returns vector. Parameters ---------- vector : array-like, shape=[..., *point_shape] Vector. base_point : array-like, shape=[..., *point_shape] Point in the vector space Returns ------- tangent_vec : array-like, shape=[..., *point_shape] Tangent vector at base point. """ tangent_vec = self.projection(vector) if base_point is not None and base_point.ndim > vector.ndim: return gs.broadcast_to(tangent_vec, base_point.shape) return tangent_vec
[docs] def random_point(self, n_samples=1, bound=1.0): """Sample in the complex vector space with a uniform distribution in a box. Parameters ---------- n_samples : int Number of samples. Optional, default: 1. bound : float Side of hypercube support of the uniform distribution. Optional, default: 1.0 Returns ------- point : array-like, shape=[..., *point_shape] Sample. """ size = self.shape if n_samples != 1: size = (n_samples,) + self.shape point = bound * ( gs.random.rand(*size, dtype=gs.get_default_cdtype()) - 0.5 - 0.5j ) return point
@property def basis(self): """Basis of the vector space.""" if self._basis is None: self._basis = self._create_basis() return self._basis @basis.setter def basis(self, basis): if len(basis) < self.dim: raise ValueError( "The basis should have length equal to the dimension of the space." ) self._basis = basis @abc.abstractmethod def _create_basis(self): """Create a canonical basis."""
[docs] class LevelSet(Manifold, abc.ABC): """Class for manifolds embedded in a vector space by a submersion. Parameters ---------- default_coords_type : str, {'intrinsic', 'extrinsic', etc} Coordinate type. Optional, default: 'extrinsic'. """ def __init__(self, default_coords_type="extrinsic", shape=None, **kwargs): self.embedding_space = self._define_embedding_space() if shape is None: shape = self.embedding_space.shape super().__init__(default_coords_type=default_coords_type, shape=shape, **kwargs) @abc.abstractmethod def _define_embedding_space(self): """Define embedding space of the manifold. Returns ------- embedding_space : Manifold Instance of Manifold. """
[docs] @abc.abstractmethod def submersion(self, point): r"""Submersion that defines the manifold. :math:`\mathrm{submersion}(x)=0` defines the manifold. Parameters ---------- point : array-like, shape=[..., *point_shape] Returns ------- submersed_point : array-like """
[docs] @abc.abstractmethod def tangent_submersion(self, vector, point): """Tangent submersion. Parameters ---------- vector : array-like, shape=[..., *point_shape] point : array-like, shape=[..., *point_shape] Returns ------- submersed_vector : array-like """
[docs] def belongs(self, point, atol=gs.atol): """Evaluate if a point belongs to the manifold. Parameters ---------- point : array-like, shape=[..., *point_shape] Point to evaluate. atol : float Absolute tolerance. Optional, default: backend atol. Returns ------- belongs : array-like, shape=[...,] Boolean evaluating if point belongs to the manifold. """ belongs = self.embedding_space.belongs(point, atol) if not gs.any(belongs): return belongs submersed_point = self.submersion(point) n_batch = gs.ndim(point) - len(self.shape) axis = tuple(range(-len(submersed_point.shape) + n_batch, 0)) if gs.is_complex(submersed_point): constraint = gs.isclose(submersed_point, 0.0 + 0.0j, atol=atol) else: constraint = gs.isclose(submersed_point, 0.0, atol=atol) if axis: constraint = gs.all(constraint, axis=axis) return gs.logical_and(belongs, constraint)
[docs] def is_tangent(self, vector, base_point, atol=gs.atol): """Check whether the vector is tangent at base_point. Parameters ---------- vector : array-like, shape=[..., *point_shape] Vector. base_point : array-like, shape=[..., *point_shape] Point on the manifold. atol : float Absolute tolerance. Optional, default: backend atol. Returns ------- is_tangent : bool Boolean denoting if vector is a tangent vector at the base point. """ belongs = self.embedding_space.is_tangent(vector, base_point, atol) if not gs.any(belongs): return belongs submersed_vector = self.tangent_submersion(vector, base_point) n_batch = max(gs.ndim(base_point), gs.ndim(vector)) - len(self.shape) axis = tuple(range(-len(submersed_vector.shape) + n_batch, 0)) constraint = gs.isclose(submersed_vector, 0.0, atol=atol) if axis: constraint = gs.all(constraint, axis=axis) return gs.logical_and(belongs, constraint)
[docs] def intrinsic_to_extrinsic_coords(self, point_intrinsic): """Convert from intrinsic to extrinsic coordinates. Parameters ---------- point_intrinsic : array-like, shape=[..., *point_shape] Point in the embedded manifold in intrinsic coordinates. Returns ------- point_extrinsic : array-like, shape=[..., *embedding_space.point_shape] Point in the embedded manifold in extrinsic coordinates. """ raise NotImplementedError("intrinsic_to_extrinsic_coords is not implemented.")
[docs] def extrinsic_to_intrinsic_coords(self, point_extrinsic): """Convert from extrinsic to intrinsic coordinates. Parameters ---------- point_extrinsic : array-like, shape=[..., *embedding_space.point_shape] Point in the embedded manifold in extrinsic coordinates, i. e. in the coordinates of the embedding manifold. Returns ------- point_intrinsic : array-lie, shape=[..., *point_shape] Point in the embedded manifold in intrinsic coordinates. """ raise NotImplementedError("extrinsic_to_intrinsic_coords is not implemented.")
[docs] @abc.abstractmethod def projection(self, point): """Project a point in embedding manifold on embedded manifold. Parameters ---------- point : array-like, shape=[..., *embedding_space.point_shape] Point in embedding manifold. Returns ------- projected : array-like, shape=[..., *point_shape] Projected point. """
[docs] @abc.abstractmethod def to_tangent(self, vector, base_point): """Project a vector to a tangent space of the manifold. Parameters ---------- vector : array-like, shape=[..., *point_shape] Vector. base_point : array-like, shape=[..., *point_shape] Point on the manifold. Returns ------- tangent_vec : array-like, shape=[..., *point_shape] Tangent vector at base point. """
[docs] class OpenSet(Manifold, abc.ABC): """Class for manifolds that are open sets. NB: if the embedding space is a vector space, use `VectorSpaceOpenSet`. Parameters ---------- embedding_space: Manifold Embedding space that contains the manifold. """ def __init__(self, embedding_space, shape=None, **kwargs): self.embedding_space = embedding_space if shape is None: shape = embedding_space.shape super().__init__(shape=shape, **kwargs)
[docs] def is_tangent(self, vector, base_point, atol=gs.atol): """Check whether the vector is tangent at base_point. Parameters ---------- vector : array-like, shape=[..., *point_shape] Vector. base_point : array-like, shape=[..., *point_shape] Point on the manifold. atol : float Absolute tolerance. Optional, default: backend atol. Returns ------- is_tangent : bool Boolean denoting if vector is a tangent vector at the base point. """ return self.embedding_space.is_tangent(vector, base_point, atol)
[docs] def to_tangent(self, vector, base_point): """Project a vector to a tangent space of the manifold. Parameters ---------- vector : array-like, shape=[..., *point_shape] Vector. base_point : array-like, shape=[..., *point_shape] Point on the manifold. Returns ------- tangent_vec : array-like, shape=[..., *point_shape] Tangent vector at base point. """ return self.embedding_space.to_tangent(vector, base_point)
[docs] def random_point(self, n_samples=1, bound=1.0): """Sample random points on the manifold. Points are sampled from the embedding space using the distribution set for that manifold and then projected to the manifold. As a result, this is not a uniform distribution on the manifold itself. Parameters ---------- n_samples : int Number of samples. Optional, default: 1. bound : float Bound of the interval in which to sample for the embedding space. Optional, default: 1. Returns ------- samples : array-like, shape=[..., *point_shape] Points sampled on the hypersphere. """ sample = self.embedding_space.random_point(n_samples, bound) return self.projection(sample)
[docs] class VectorSpaceOpenSet(OpenSet, abc.ABC): """Class for manifolds that are open sets of a vector space. In this case, tangent vectors are identified with vectors of the embedding space. Parameters ---------- embedding_space: VectorSpace Embedding space that contains the manifold. """
[docs] def is_tangent(self, vector, base_point=None, atol=gs.atol): """Check whether the vector is tangent at base_point. Parameters ---------- vector : array-like, shape=[..., *point_shape] Vector. base_point : array-like, shape=[..., *point_shape] Point on the manifold. atol : float Absolute tolerance. Optional, default: backend atol. Returns ------- is_tangent : bool Boolean denoting if vector is a tangent vector at the base point. """ is_tangent = self.embedding_space.belongs(vector, atol) if base_point is not None and base_point.ndim > vector.ndim: return gs.broadcast_to(is_tangent, base_point.shape[: -self.point_ndim]) return is_tangent
[docs] def to_tangent(self, vector, base_point=None): """Project a vector to a tangent space of the manifold. Parameters ---------- vector : array-like, shape=[..., *point_shape] Vector. base_point : array-like, shape=[..., *point_shape] Point on the manifold. Returns ------- tangent_vec : array-like, shape=[..., *point_shape] Tangent vector at base point. """ tangent_vec = self.embedding_space.projection(vector) if base_point is not None and base_point.ndim > vector.ndim: return gs.broadcast_to(tangent_vec, base_point.shape) return tangent_vec
[docs] @abc.abstractmethod def projection(self, point): """Project a point in embedding manifold on manifold. Parameters ---------- point : array-like, shape=[..., *point_shape] Point in embedding manifold. Returns ------- projected : array-like, shape=[..., *point_shape] Projected point. """
[docs] class ComplexVectorSpaceOpenSet(ComplexManifold, abc.ABC): """Class for manifolds that are open sets of a complex vector space. In this case, tangent vectors are identified with vectors of the embedding space. Parameters ---------- dim: int Dimension of the manifold. It is often the same as the embedding space dimension but may differ in some cases. embedding_space: VectorSpace Embedding space that contains the manifold. """ def __init__(self, embedding_space, shape=None, **kwargs): if shape is None: shape = embedding_space.shape super().__init__(shape=shape, default_coords_type="extrinsic", **kwargs) self.embedding_space = embedding_space
[docs] def is_tangent(self, vector, base_point=None, atol=gs.atol): """Check whether the vector is tangent at base_point. Parameters ---------- vector : array-like, shape=[..., *point_shape] Vector. base_point : array-like, shape=[..., *point_shape] Point on the manifold. atol : float Absolute tolerance. Optional, default: backend atol. Returns ------- is_tangent : bool Boolean denoting if vector is a tangent vector at the base point. """ is_tangent = self.embedding_space.belongs(vector, atol) if base_point is not None and base_point.ndim > vector.ndim: return gs.broadcast_to(is_tangent, base_point.shape[: -self.point_ndim]) return is_tangent
[docs] def to_tangent(self, vector, base_point=None): """Project a vector to a tangent space of the manifold. Parameters ---------- vector : array-like, shape=[..., *point_shape] Vector. base_point : array-like, shape=[..., *point_shape] Point on the manifold. Returns ------- tangent_vec : array-like, shape=[..., *point_shape] Tangent vector at base point. """ tangent_vec = self.embedding_space.projection(vector) if base_point is not None and base_point.ndim > vector.ndim: return gs.broadcast_to(tangent_vec, base_point.shape) return tangent_vec
[docs] def random_point(self, n_samples=1, bound=1.0): """Sample random points on the manifold. If the manifold is compact, a uniform distribution is used. Parameters ---------- n_samples : int Number of samples. Optional, default: 1. bound : float Bound of the interval in which to sample for non compact manifolds. Optional, default: 1. Returns ------- samples : array-like, shape=[..., *point_shape] Points sampled on the hypersphere. """ sample = self.embedding_space.random_point(n_samples, bound) return self.projection(sample)
[docs] @abc.abstractmethod def projection(self, point): """Project a point in embedding manifold on manifold. Parameters ---------- point : array-like, shape=[..., *point_shape] Point in embedding manifold. Returns ------- projected : array-like, shape=[..., *point_shape] Projected point. """
[docs] class ImmersedSet(Manifold, abc.ABC): """Class for manifolds embedded in a vector space by an immersion. The manifold is represented with intrinsic coordinates, such that the immersion gives a parameterization of the manifold in these coordinates. Parameters ---------- dim : int Dimension of the embedded manifold. """ def __init__(self, dim, equip=True): super().__init__( dim=dim, shape=(dim,), default_coords_type="intrinsic", equip=equip ) self.embedding_space = self._define_embedding_space()
[docs] @staticmethod def default_metric(): """Metric to equip the space with if equip is True.""" return PullbackMetric
@abc.abstractmethod def _define_embedding_space(self): """Define embedding space of the manifold. Returns ------- embedding_space : Manifold Instance of Manifold. """
[docs] @abc.abstractmethod def immersion(self, point): """Evaluate the immersion function at a point. Parameters ---------- point : array-like, shape=[..., dim] Point in the immersed manifold. Returns ------- immersion : array-like, shape=[..., dim_embedding] Immersion of the point. """
[docs] def tangent_immersion(self, tangent_vec, base_point): """Evaluate the tangent immersion at a tangent vec and point. Parameters ---------- tangent_vec : array-like, shape=[..., dim] base_point : array-like, shape=[..., dim] Point in the immersed manifold. Returns ------- tangent_vec_emb : array-like, shape=[..., dim_embedding] """ jacobian_immersion = self.jacobian_immersion(base_point) return gs.matvec(jacobian_immersion, tangent_vec)
[docs] def jacobian_immersion(self, base_point): """Evaluate the Jacobian of the immersion at a point. Parameters ---------- base_point : array-like, shape=[..., dim] Point in the immersed manifold. Returns ------- jacobian_immersion : array-like, shape=[..., dim_embedding, dim] """ return gs.autodiff.jacobian_vec(self.immersion)(base_point)
[docs] def hessian_immersion(self, base_point): """Compute the Hessian of the immersion. Parameters ---------- base_point : array-like, shape=[..., dim] Base point. Returns ------- hessian_immersion : array-like, shape=[..., embedding_dim, dim, dim] Hessian at the base point """ return gs.autodiff.hessian_vec( self.immersion, func_out_ndim=self.embedding_space.dim )(base_point)
[docs] def is_tangent(self, vector, base_point, atol=gs.atol): """Check whether the vector is tangent at base_point. Parameters ---------- vector : array-like, shape=[..., dim] Vector. base_point : array-like, shape=[..., dim] Point on the manifold. atol : float Absolute tolerance. Optional, default: backend atol. Returns ------- is_tangent : bool Boolean denoting if vector is a tangent vector at the base point. """ raise NotImplementedError("`is_tangent` is not implemented yet")
[docs] def belongs(self, point, atol=gs.atol): """Evaluate if a point belongs to the manifold. Parameters ---------- point : array-like, shape=[..., dim] Point to evaluate. atol : float Absolute tolerance. Optional, default: backend atol. Returns ------- belongs : array-like, shape=[...,] Boolean evaluating if point belongs to the manifold. """ raise NotImplementedError("`is_tangent` is not implemented yet")
[docs] def projection(self, point): """Project a point to the embedded manifold. This is simply point, since we are in intrinsic coordinates. Parameters ---------- point : array-like, shape=[..., dim_embedding] Point in the embedding manifold. Returns ------- projected_point : array-like, shape=[..., dim] Point in the embedded manifold. """ raise NotImplementedError("`projection` is not implemented yet")
[docs] def to_tangent(self, vector, base_point): """Project a vector to a tangent space of the manifold. This is simply the vector since we are in intrinsic coordinates. Parameters ---------- vector : array-like, shape=[..., dim_embedding] Vector. base_point : array-like, shape=[..., dim] Point in the embedded manifold. Returns ------- tangent_vec : array-like, shape=[..., dim] Tangent vector at base point. """ raise NotImplementedError("`to_tangent` is not implemented yet")
[docs] def random_point(self, n_samples=1, bound=1.0): """Sample random points on the manifold according to some distribution. If the manifold is compact, preferably a uniform distribution will be used. Parameters ---------- n_samples : int Number of samples. Optional, default: 1. bound : float Bound of the interval in which to sample for non compact manifolds. Optional, default: 1. Returns ------- samples : array-like, shape=[..., *point_shape] Points sampled on the manifold. """ raise NotImplementedError("`random_point` is not implemented yet")
[docs] class DiffeomorphicManifold(Manifold): """A manifold defined by a diffeomorphism.""" def __init__(self, diffeo, image_space, **kwargs): self.diffeo = diffeo self.image_space = image_space super().__init__(**kwargs)
[docs] def to_tangent(self, vector, base_point): """Project a vector to a tangent space of the manifold. Parameters ---------- vector : array-like, shape=[..., *point_shape] Vector. base_point : array-like, shape=[..., *point_shape] Point on the manifold. Returns ------- tangent_vec : array-like, shape=[..., *point_shape] Tangent vector at base point. """ image_point = self.diffeo.diffeomorphism(base_point) image_vector = self.diffeo.tangent_diffeomorphism( vector, base_point=base_point, image_point=image_point ) image_tangent_vec = self.image_space.to_tangent(image_vector, image_point) return self.diffeo.inverse_tangent_diffeomorphism( image_tangent_vec, image_point=image_point, base_point=base_point )
[docs] def random_point(self, n_samples=1, bound=1.0): """Sample random points on the manifold according to some distribution. If the manifold is compact, preferably a uniform distribution will be used. Parameters ---------- n_samples : int Number of samples. Optional, default: 1. bound : float Bound of the interval in which to sample for non compact manifolds. Optional, default: 1. Returns ------- samples : array-like, shape=[..., *point_shape] Points sampled on the manifold. """ image_point = self.image_space.random_point(n_samples=n_samples, bound=bound) return self.diffeo.inverse_diffeomorphism(image_point)
[docs] def regularize(self, point): """Regularize a point to the canonical representation for the manifold. Parameters ---------- point : array-like, shape=[..., dim] Point. Returns ------- regularized_point : array-like, shape=[..., *point_shape] Regularized point. """ image_point = self.diffeo.diffeomorphism(point) regularized_image_point = self.image_space.regularize(image_point) return self.diffeo.inverse_diffeomorphism(regularized_image_point)
[docs] def random_tangent_vec(self, base_point, n_samples=1): """Generate random tangent vec. Parameters ---------- n_samples : int Number of samples. Optional, default: 1. base_point : array-like, shape={[n_samples, *point_shape], [*point_shape,]} Point. Returns ------- tangent_vec : array-like, shape=[..., *point_shape] Tangent vec at base point. """ image_point = self.diffeo.diffeomorphism(base_point) image_tangent_vec = self.image_space.random_tangent_vec( image_point, n_samples=n_samples ) return self.diffeo.inverse_tangent_diffeomorphism( image_tangent_vec, image_point=image_point, base_point=base_point )