def setUp(self): gs.random.seed(1234) self.n = 3 self.n_samples = 2 self.group = GeneralLinear(n=self.n) self.group_pos = GeneralLinear(self.n, positive_det=True) warnings.simplefilter('ignore', category=ImportWarning)
def setUp(self): gs.random.seed(1234) self.n = 3 self.n_samples = 2 self.group = GeneralLinear(n=self.n) # We generate invertible matrices using so3_group self.so3_group = SpecialOrthogonal(n=self.n) warnings.simplefilter('ignore', category=ImportWarning)
def test_orbit(self): point = gs.array([[gs.exp(4.), 0.], [0., gs.exp(2.)]]) sqrt = gs.array([[gs.exp(2.), 0.], [0., gs.exp(1.)]]) idty = GeneralLinear(2).identity path = GeneralLinear(2).orbit(point) time = gs.linspace(0., 1., 3) result = path(time) expected = gs.array([idty, sqrt, point]) self.assertAllClose(result, expected)
def test_orbit_vectorization(self): point = gs.array([[gs.exp(4.), 0.], [0., gs.exp(2.)]]) sqrt = gs.array([[gs.exp(2.), 0.], [0., gs.exp(1.)]]) identity = GeneralLinear(2).identity path = GeneralLinear(2).orbit(gs.stack([point] * 2), identity) time = gs.linspace(0., 1., 3) result = path(time) expected = gs.array([identity, sqrt, point]) expected = gs.stack([expected] * 2) self.assertAllClose(result, expected)
def exp(self, tangent_vec, base_point): """Compute the affine-invariant exponential map. Compute the Riemannian exponential at point base_point of tangent vector tangent_vec wrt the metric defined in inner_product. This gives a symmetric positive definite matrix. Parameters ---------- tangent_vec : array-like, shape=[..., n, n] base_point : array-like, shape=[..., n, n] Returns ------- exp : array-like, shape=[..., n, n] """ power_affine = self.power_affine if power_affine == 1: sqrt_base_point = SymmetricMatrices.powerm(base_point, 1. / 2) inv_sqrt_base_point = SymmetricMatrices.powerm(sqrt_base_point, -1) exp = self._aux_exp(tangent_vec, sqrt_base_point, inv_sqrt_base_point) else: modified_tangent_vec = self.space.differential_power( power_affine, tangent_vec, base_point) power_sqrt_base_point = SymmetricMatrices.powerm( base_point, power_affine / 2) power_inv_sqrt_base_point = GeneralLinear.inverse( power_sqrt_base_point) exp = self._aux_exp(modified_tangent_vec, power_sqrt_base_point, power_inv_sqrt_base_point) exp = SymmetricMatrices.powerm(exp, 1 / power_affine) return exp
def inner_prod(tangent_vec_a, tangent_vec_b, base_point): affine_part = self.bundle.ambient_metric.inner_product( tangent_vec_a, tangent_vec_b, base_point) n = tangent_vec_b.shape[-1] inverse_base_point = GeneralLinear.inverse(base_point) operator = gs.eye(n) + base_point * inverse_base_point inverse_operator = GeneralLinear.inverse(operator) diagonal_a = gs.einsum("...ij,...ji->...i", inverse_base_point, tangent_vec_a) diagonal_b = gs.einsum("...ij,...ji->...i", inverse_base_point, tangent_vec_b) aux = gs.einsum("...i,...j->...ij", diagonal_a, diagonal_b) other_part = 2 * Matrices.frobenius_product(aux, inverse_operator) return affine_part - other_part
def left_log_from_identity(self, point): """Compute Riemannian log of a point wrt. id of left-invar. metric. Compute Riemannian logarithm of a point wrt the identity associated to the left-invariant metric. If the method is called by a right-invariant metric, it uses the left-invariant metric associated to the same inner-product matrix at the identity. Parameters ---------- point : array-like, shape=[..., dim] Point in the group. Returns ------- log : array-like, shape=[..., dim] Tangent vector at the identity equal to the Riemannian logarithm of point at the identity. """ point = self.group.regularize(point) inner_prod_mat = self.metric_mat_at_identity inv_inner_prod_mat = GeneralLinear.inverse(inner_prod_mat) sqrt_inv_inner_prod_mat = gs.linalg.sqrtm(inv_inner_prod_mat) log = gs.einsum('...i,...ij->...j', point, sqrt_inv_inner_prod_mat) log = self.group.regularize_tangent_vec_at_identity(tangent_vec=log, metric=self) return log
def test_horizontal_projection(self): mat = self.bundle.random_point() vec = self.bundle.random_point() horizontal_vec = self.bundle.horizontal_projection(vec, mat) product = Matrices.mul(horizontal_vec, GeneralLinear.inverse(mat)) is_horizontal = Matrices.is_symmetric(product) self.assertTrue(is_horizontal)
def orbit_data(self): point = gs.array([[gs.exp(4.0), 0.0], [0.0, gs.exp(2.0)]]) sqrt = gs.array([[gs.exp(2.0), 0.0], [0.0, gs.exp(1.0)]]) identity = GeneralLinear(2).identity time = gs.linspace(0.0, 1.0, 3) smoke_data = [ dict( n=2, point=point, base_point=identity, time=time, expected=gs.array([identity, sqrt, point]), ), dict( n=2, point=[point, point], base_point=identity, time=time, expected=[ gs.array([identity, sqrt, point]), gs.array([identity, sqrt, point]), ], ), ] return self.generate_tests(smoke_data)
def submersion(point, k): r"""Submersion that defines the Grassmann manifold. The Grassmann manifold is defined here as embedded in the set of symmetric matrices, as the pre-image of the function defined around the projector on the space spanned by the first k columns of the identity matrix by (see Exercise E.25 in [Pau07]_). .. math: \begin{pmatrix} I_k + A & B^T \\ B & D \end{pmatrix} \mapsto (D - B(I_k + A)^{-1}B^T, A + A^2 + B^TB This map is a submersion and its zero space is the set of orthogonal rank-k projectors. References ---------- .. [Pau07] Paulin, Frédéric. “Géométrie différentielle élémentaire,” 2007. https://www.imo.universite-paris-saclay.fr/~paulin /notescours/cours_geodiff.pdf. """ _, eigvecs = gs.linalg.eigh(point) eigvecs = gs.flip(eigvecs, -1) flipped_point = Matrices.mul(Matrices.transpose(eigvecs), point, eigvecs) b = flipped_point[..., k:, :k] d = flipped_point[..., k:, k:] a = flipped_point[..., :k, :k] - gs.eye(k) first = d - Matrices.mul(b, GeneralLinear.inverse(a + gs.eye(k)), Matrices.transpose(b)) second = a + Matrices.mul(a, a) + Matrices.mul(Matrices.transpose(b), b) row_1 = gs.concatenate([first, gs.zeros_like(b)], axis=-1) row_2 = gs.concatenate([Matrices.transpose(gs.zeros_like(b)), second], axis=-1) return gs.concatenate([row_1, row_2], axis=-2)
def belongs(mat, atol=TOLERANCE): """Check if a matrix is symmetric and invertible.""" is_symmetric = GeneralLinear.is_symmetric(mat) eigvalues, _ = gs.linalg.eigh(mat) is_positive = gs.all(eigvalues > 0, axis=-1) belongs = gs.logical_and(is_symmetric, is_positive) return belongs
def test_horizontal_lift_and_tangent_submersion(self): mat = self.bundle.total_space.random_uniform() tangent_vec = GeneralLinear.to_symmetric( self.bundle.total_space.random_uniform()) horizontal = self.bundle.horizontal_lift(tangent_vec, mat) result = self.bundle.tangent_submersion(horizontal, mat) self.assertAllClose(result, tangent_vec)
def test_is_horizontal(self): mat = self.bundle.total_space.random_uniform() tangent_vec = GeneralLinear.to_symmetric( self.bundle.total_space.random_uniform()) horizontal = self.bundle.horizontal_lift(tangent_vec, mat) result = self.bundle.is_horizontal(horizontal, mat) self.assertTrue(result)
def parallel_transport(self, tangent_vec_a, tangent_vec_b, base_point): r"""Parallel transport of a tangent vector. Closed-form solution for the parallel transport of a tangent vector a along the geodesic defined by exp_(base_point)(tangent_vec_b). Denoting `tangent_vec_a` by `S`, `base_point` by `A`, let `B = Exp_A(tangent_vec_b)` and :math: `E = (BA^{- 1})^({ 1 / 2})`. Then the parallel transport to `B`is: ..math:: S' = ESE^T Parameters ---------- tangent_vec_a : array-like, shape=[..., dim + 1] Tangent vector at base point to be transported. tangent_vec_b : array-like, shape=[..., dim + 1] Tangent vector at base point, initial speed of the geodesic along which the parallel transport is computed. base_point : array-like, shape=[..., dim + 1] Point on the manifold of SPD matrices. Returns ------- transported_tangent_vec: array-like, shape=[..., dim + 1] Transported tangent vector at exp_(base_point)(tangent_vec_b). """ end_point = self.exp(tangent_vec_b, base_point) inverse_base_point = GeneralLinear.inverse(base_point) congruence_mat = Matrices.mul(end_point, inverse_base_point) congruence_mat = gs.linalg.sqrtm(congruence_mat) return Matrices.congruent(tangent_vec_a, congruence_mat)
def setUp(self): gs.random.seed(0) n = 3 self.base = SPDMatrices(n) self.base_metric = SPDMetricBuresWasserstein(n) self.group = SpecialOrthogonal(n) self.bundle = FiberBundle(GeneralLinear(n), base=self.base, group=self.group) self.quotient_metric = QuotientMetric(self.bundle, ambient_metric=MatricesMetric( n, n)) def submersion(point): return GeneralLinear.mul(point, GeneralLinear.transpose(point)) def tangent_submersion(tangent_vec, base_point): product = GeneralLinear.mul(base_point, GeneralLinear.transpose(tangent_vec)) return 2 * GeneralLinear.to_symmetric(product) def horizontal_lift(tangent_vec, point, base_point=None): if base_point is None: base_point = submersion(point) sylvester = gs.linalg.solve_sylvester(base_point, base_point, tangent_vec) return GeneralLinear.mul(sylvester, point) self.bundle.submersion = submersion self.bundle.tangent_submersion = tangent_submersion self.bundle.horizontal_lift = horizontal_lift self.bundle.lift = gs.linalg.cholesky
def random_uniform(self, n_samples=1): """Sample random points from a uniform distribution. Following [Chikuse03]_, :math: `n_samples * n * k` scalars are sampled from a standard normal distribution and reshaped to matrices, the projectors on their first k columns follow a uniform distribution. Parameters ---------- n_samples : int The number of points to sample Optional. default: 1. Returns ------- projectors : array-like, shape=[..., n, n] Points following a uniform distribution. References ---------- .. [Chikuse03] Yasuko Chikuse, Statistics on special manifolds, New York: Springer-Verlag. 2003, 10.1007/978-0-387-21540-2 """ points = gs.random.normal(size=(n_samples, self.n, self.k)) full_rank = Matrices.mul(Matrices.transpose(points), points) projector = Matrices.mul(points, GeneralLinear.inverse(full_rank), Matrices.transpose(points)) return projector[0] if n_samples == 1 else projector
def metric_matrix(self, base_point=None): """Compute inner product matrix at the tangent space at a base point. Parameters ---------- base_point : array-like, shape=[..., dim], optional Point in the group (the default is identity). Returns ------- metric_mat : array-like, shape=[..., dim, dim] Metric matrix at base_point. """ if base_point is None: return self.metric_mat_at_identity base_point = self.group.regularize(base_point) jacobian = self.group.jacobian_translation( point=base_point, left_or_right=self.left_or_right) inv_jacobian = GeneralLinear.inverse(jacobian) inv_jacobian_transposed = Matrices.transpose(inv_jacobian) metric_mat = Matrices.mul( inv_jacobian_transposed, self.metric_mat_at_identity, inv_jacobian) return metric_mat
def connection_at_identity(self, tangent_vec_a, tangent_vec_b): r"""Compute the Levi-Civita connection at identity. For two tangent vectors at identity :math: `x,y`, one can associate left (respectively right) invariant vector fields :math: `\tilde{x}, \tilde{y}`. Then the vector :math: `(\nabla_\tilde{x}(\tilde{x}))_{ Id}` is computed using the lie bracket and the dual adjoint map. This is a bilinear map that characterizes the connection [Gallier]_. Parameters ---------- tangent_vec_a : array-like, shape=[..., n, n] Tangent vector at identity. tangent_vec_b : array-like, shape=[..., n, n] Tangent vector at identity. Returns ------- nabla : array-like, shape=[..., n, n] Tangent vector at identity. References ---------- .. [Gallier] Gallier, Jean, and Jocelyn Quaintance. Differential Geometry and Lie Groups: A Computational Perspective. Geonger International Publishing, 2020. https://doi.org/10.1007/978-3-030-46040-2. """ sign = 1. if self.left_or_right == 'left' else -1. return sign / 2 * (GeneralLinear.bracket(tangent_vec_a, tangent_vec_b) - self.dual_adjoint(tangent_vec_a, tangent_vec_b) - self.dual_adjoint(tangent_vec_b, tangent_vec_a))
def transp(self, base_point, end_point, tangent): """ transports a tangent vector at a base_point to the tangent space at end_point. """ if self.exact_transport: # https://github.com/geomstats/geomstats/blob/master/geomstats/geometry/spd_matrices.py#L613 inverse_base_point = GeneralLinear.inverse(base_point) congruence_mat = GeneralLinear.mul(end_point, inverse_base_point) congruence_mat = gs.linalg.sqrtm(congruence_mat.cpu()).to(tangent) return GeneralLinear.congruent(tangent, congruence_mat) # https://github.com/NicolasBoumal/manopt/blob/master/manopt/manifolds/symfixedrank/sympositivedefinitefactory.m#L181 return tangent
def metric_matrix(self, base_point=None): """Compute inner product matrix at the tangent space at a base point. Parameters ---------- base_point : array-like, shape=[..., dim], optional Point in the group (the default is identity). Returns ------- metric_mat : array-like, shape=[..., dim, dim] Metric matrix at base_point. """ if self.group.default_point_type == 'matrix': raise NotImplementedError( 'inner_product_matrix not implemented for Lie groups' ' whose elements are represented as matrices.') if base_point is None: base_point = self.group.identity else: base_point = self.group.regularize(base_point) jacobian = self.group.jacobian_translation( point=base_point, left_or_right=self.left_or_right) inv_jacobian = GeneralLinear.inverse(jacobian) inv_jacobian_transposed = Matrices.transpose(inv_jacobian) metric_mat = gs.einsum('...ij,...jk->...ik', inv_jacobian_transposed, self.metric_mat_at_identity) metric_mat = gs.einsum('...ij,...jk->...ik', metric_mat, inv_jacobian) return metric_mat
def curvature_at_identity( self, tangent_vec_a, tangent_vec_b, tangent_vec_c): r"""Compute the curvature at identity. For three tangent vectors at identity :math: `x,y,z`, the curvature is defined by :math: `R(x, y)z = \nabla_{[x,y]}z - \nabla_x\nabla_y z + - \nabla_y\nabla_x z`. Parameters ---------- tangent_vec_a : array-like, shape=[..., n, n] Tangent vector at identity. tangent_vec_b : array-like, shape=[..., n, n] Tangent vector at identity. tangent_vec_c : array-like, shape=[..., n, n] Tangent vector at identity. Returns ------- curvature : array-like, shape=[..., n, n] Tangent vector at identity. """ bracket = GeneralLinear.bracket(tangent_vec_a, tangent_vec_b) bracket_term = self.connection_at_identity(bracket, tangent_vec_c) left_term = self.connection_at_identity( tangent_vec_a, self.connection_at_identity( tangent_vec_b, tangent_vec_c)) right_term = self.connection_at_identity( tangent_vec_b, self.connection_at_identity( tangent_vec_a, tangent_vec_c)) return bracket_term - left_term + right_term
def test_to_grassmanniann_vectorized(self): inf_rots = gs.array([gs.pi * r_z / n for n in [2, 3, 4]]) rots = GeneralLinear.exp(inf_rots) points = Matrices.mul(rots, point1) result = Stiefel.to_grassmannian(points) expected = gs.array([p_xy, p_xy, p_xy]) self.assertAllClose(result, expected)
def compose(self, point_a, point_b, point_type=None): r"""Compose two elements of SE(n). Parameters ---------- point_1 : array-like, shape=[n_samples, {dim, [n + 1, n + 1]}] point_2 : array-like, shape=[n_samples, {dim, [n + 1, n + 1]}] point_type: str, {'vector', 'matrix'}, optional default: self.default_point_type Equation --------- (:math: `(R_1, t_1) \\cdot (R_2, t_2) = (R_1 R_2, R_1 t_2 + t_1)`) Returns ------- composition : the composition of point_1 and point_2 """ rotations = self.rotations dim_rotations = rotations.dim point_a = self.regularize(point_a, point_type=point_type) point_b = self.regularize(point_b, point_type=point_type) if point_type == 'vector': n_points_a, _ = point_a.shape n_points_b, _ = point_b.shape if not (point_a.shape == point_b.shape or n_points_a == 1 or n_points_b == 1): raise ValueError() rot_vec_a = point_a[:, :dim_rotations] rot_mat_a = rotations.matrix_from_rotation_vector(rot_vec_a) rot_vec_b = point_b[:, :dim_rotations] rot_mat_b = rotations.matrix_from_rotation_vector(rot_vec_b) translation_a = point_a[:, dim_rotations:] translation_b = point_b[:, dim_rotations:] composition_rot_mat = gs.matmul(rot_mat_a, rot_mat_b) composition_rot_vec = rotations.rotation_vector_from_matrix( composition_rot_mat) composition_translation = gs.einsum( '...j,...kj->...k', translation_b, rot_mat_a) + translation_a composition = gs.concatenate( (composition_rot_vec, composition_translation), axis=-1) return self.regularize(composition, point_type=point_type) if point_type == 'matrix': return GeneralLinear.compose(point_a, point_b) raise ValueError('Invalid point_type, expected \'vector\' or ' '\'matrix\'.')
def _log_translation_transform(self, rot_vec): exp_transform = self._exp_translation_transform(rot_vec) inv_determinant = .5 / utils.taylor_exp_even_func( rot_vec**2, utils.cosc_close_0, order=4) transform = gs.einsum('...l, ...jk -> ...jk', inv_determinant, GeneralLinear.transpose(exp_transform)) return transform
def test_integrability_tensor(self): mat = self.bundle.total_space.random_point() point = self.bundle.submersion(mat) tangent_vec = GeneralLinear.to_symmetric( self.bundle.total_space.random_point()) / 5 self.assertRaises( NotImplementedError, lambda: self.bundle.integrability_tensor( tangent_vec, tangent_vec, point))
def random_uniform(self, n_samples=1): """Define a log-uniform random sample of SPD matrices.""" n = self.n size = (n_samples, n, n) if n_samples != 1 else (n, n) mat = 2 * gs.random.rand(*size) - 1 spd_mat = GeneralLinear.exp(mat + Matrices.transpose(mat)) return spd_mat
def test_exp(self): mat = self.bundle.total_space.random_uniform() point = self.bundle.submersion(mat) tangent_vec = GeneralLinear.to_symmetric( self.bundle.total_space.random_uniform()) / 5 result = self.quotient_metric.exp(tangent_vec, point) expected = self.base_metric.exp(tangent_vec, point) self.assertAllClose(result, expected)
def _to_lie_algebra(self, tangent_vec): """Project vector rotation part onto skew-symmetric matrices.""" translation_mask = gs.hstack( [gs.ones((self.n, ) * 2), 2 * gs.ones((self.n, 1))]) translation_mask = gs.concatenate( [translation_mask, gs.zeros((1, self.n + 1))], axis=0) tangent_vec = tangent_vec * gs.where(translation_mask != 0., gs.array(1.), gs.array(0.)) tangent_vec = (tangent_vec - GeneralLinear.transpose(tangent_vec)) / 2. return tangent_vec * translation_mask
def test_horizontal_projection(self, n, vec, mat): bundle = self.space(n) base = self.base(n) horizontal_vec = bundle.horizontal_projection(vec, mat) inverse = GeneralLinear.inverse(mat) product_1 = Matrices.mul(horizontal_vec, inverse) product_2 = Matrices.mul(inverse, horizontal_vec) is_horizontal = gs.all( base.is_tangent(product_1 + product_2, mat, atol=gs.atol * 10)) self.assertTrue(is_horizontal)
def compose_data(self): smoke_data = [ dict( n=2, mat1=[[1.0, 0.0], [0.0, 2.0]], mat2=[[2.0, 0.0], [0.0, 1.0]], expected=2.0 * GeneralLinear(2).identity, ) ] return self.generate_tests(smoke_data)