def test_horizontal_lift_and_tangent_riemannian_submersion( self, n, mat, vec): bundle = self.bundle(n) tangent_vec = Matrices.to_symmetric(vec) horizontal = bundle.horizontal_lift(tangent_vec, fiber_point=mat) result = bundle.tangent_riemannian_submersion(horizontal, mat) self.assertAllClose(result, tangent_vec, atol=1e-2)
def setUp(self): self.dimension = 4 self.dt = 0.1 self.euclidean = Euclidean(self.dimension) self.matrices = Matrices(self.dimension, self.dimension) self.intercept = self.euclidean.random_point() self.slope = Matrices.to_symmetric(self.matrices.random_point())
def test_integrability_tensor(self, n, mat, vec): bundle = self.bundle(n) point = bundle.riemannian_submersion(mat) tangent_vec = Matrices.to_symmetric(vec) / 20 with pytest.raises(NotImplementedError): bundle.integrability_tensor(tangent_vec, tangent_vec, point)
def test_integrability_tensor(self): mat = self.bundle.random_point() point = self.bundle.riemannian_submersion(mat) tangent_vec = Matrices.to_symmetric(self.bundle.random_point()) / 5 with pytest.raises(NotImplementedError): self.bundle.integrability_tensor(tangent_vec, tangent_vec, point)
def from_vector(vec, dtype=gs.float32): """Convert a vector into a symmetric matrix. Parameters ---------- vec : array-like, shape=[..., n(n+1)/2] Vector. Returns ------- mat : array-like, shape=[..., n, n] Symmetric matrix. """ vec_dim = vec.shape[-1] mat_dim = (gs.sqrt(8. * vec_dim + 1) - 1) / 2 if mat_dim != int(mat_dim): raise ValueError('Invalid input dimension, it must be of the form' '(n_samples, n * (n + 1) / 2)') mat_dim = int(mat_dim) shape = (mat_dim, mat_dim) mask = 2 * gs.ones(shape) - gs.eye(mat_dim) indices = list(zip(*gs.triu_indices(mat_dim))) vec = gs.cast(vec, dtype) upper_triangular = gs.stack( [gs.array_from_sparse(indices, data, shape) for data in vec]) mat = Matrices.to_symmetric(upper_triangular) * mask return mat
def test_exp(self): mat = self.bundle.random_point() point = self.bundle.riemannian_submersion(mat) tangent_vec = Matrices.to_symmetric(self.bundle.random_point()) / 5 result = self.quotient_metric.exp(tangent_vec, point) expected = self.base_metric.exp(tangent_vec, point) self.assertAllClose(result, expected)
def test_inner_product(self): mat = self.bundle.random_point() point = self.bundle.riemannian_submersion(mat) tangent_vecs = Matrices.to_symmetric(self.bundle.random_point(2)) / 10 result = self.quotient_metric.inner_product(tangent_vecs[0], tangent_vecs[1], fiber_point=mat) expected = self.base_metric.inner_product(tangent_vecs[0], tangent_vecs[1], point) self.assertAllClose(result, expected)
def test_exp(self, n, mat, vec): bundle = self.bundle(n) quotient_metric = self.metric(bundle) base_metric = self.base_metric(n) point = bundle.riemannian_submersion(mat) tangent_vec = Matrices.to_symmetric(vec) / 40 result = quotient_metric.exp(tangent_vec, point) expected = base_metric.exp(tangent_vec, point) self.assertAllClose(result, expected, atol=1e-1)
def test_inner_product(self, n, mat, vec_a, vec_b): bundle = self.bundle(n) quotient_metric = self.metric(bundle) base_metric = self.base_metric(n) point = bundle.riemannian_submersion(mat) tangent_vecs = Matrices.to_symmetric(gs.array([vec_a, vec_b])) / 40 result = quotient_metric.inner_product( tangent_vecs[0], tangent_vecs[1], fiber_point=mat ) expected = base_metric.inner_product(tangent_vecs[0], tangent_vecs[1], point) self.assertAllClose(result, expected, atol=1e-1)
def to_vector(mat): """Convert the symmetric part of a symmetric matrix into a vector.""" if not gs.all(Matrices.is_symmetric(mat)): logging.warning('non-symmetric matrix encountered.') mat = Matrices.to_symmetric(mat) _, dim, _ = mat.shape indices_i, indices_j = gs.triu_indices(dim) vec = [] for i, j in zip(indices_i, indices_j): vec.append(mat[:, i, j]) vec = gs.stack(vec, axis=1) return vec
def projection(self, point): """Make a matrix symmetric, by averaging with its transpose. Parameters ---------- point : array-like, shape=[..., n, n] Matrix. Returns ------- sym : array-like, shape=[..., n, n] Symmetric matrix. """ return Matrices.to_symmetric(point)
def to_vector(mat): """Convert a symmetric matrix into a vector. Parameters ---------- mat : array-like, shape=[..., n, n] Matrix. Returns ------- vec : array-like, shape=[..., n(n+1)/2] Vector. """ if not gs.all(Matrices.is_symmetric(mat)): logging.warning("non-symmetric matrix encountered.") mat = Matrices.to_symmetric(mat) return gs.triu_to_vec(mat)
def random_point(self, n_samples=1, bound=1.): """Sample from a uniform distribution. Parameters ---------- n_samples : int Number of samples. Optional, default: 1. bound : float Bound of the interval in which to sample each entry. Optional, default: 1. Returns ------- point : array-like, shape=[m, n] or [n_samples, m, n] Sample. """ return Matrices.to_symmetric(Matrices.random_point(n_samples, bound))
def random_point(self, n_samples=1, bound=1.0): """Sample a symmetric matrix 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=[..., n, n] Sample. """ sample = super(SymmetricMatrices, self).random_point(n_samples, bound) return Matrices.to_symmetric(sample)
def _aux_log(point, sqrt_base_point, inv_sqrt_base_point): """Compute the log (auxiliary function). Parameters ---------- point : array-like, shape=[..., n, n] sqrt_base_point : array-like, shape=[..., n, n] inv_sqrt_base_point : array-like, shape=[.., n, n] Returns ------- log : array-like, shape=[..., n, n] """ point_near_id = Matrices.mul(inv_sqrt_base_point, point, inv_sqrt_base_point) point_near_id = Matrices.to_symmetric(point_near_id) log_at_id = SPDMatrices.logm(point_near_id) log = Matrices.mul(sqrt_base_point, log_at_id, sqrt_base_point) return log
def to_tangent(self, vector, base_point): """Project a vector to a tangent space of the manifold. Compute the bracket (commutator) of the base_point with the skew-symmetric part of vector. Parameters ---------- vector : array-like, shape=[..., n, n] Vector. base_point : array-like, shape=[..., n, n] Point on the manifold. Returns ------- tangent_vec : array-like, shape=[..., n, n] Tangent vector at base point. """ sym = Matrices.to_symmetric(vector) return Matrices.bracket(base_point, Matrices.bracket(base_point, sym))
def __init__(self, n, k): geomstats.errors.check_integer(k, "k") geomstats.errors.check_integer(n, "n") if k > n: raise ValueError( "k < n is required: k-dimensional subspaces in n dimensions.") self.n = n self.k = k dim = int(k * (n - k)) super(Grassmannian, self).__init__( dim=dim, embedding_space=SymmetricMatrices(n), submersion=lambda x: submersion(x, k), value=gs.zeros((n, n)), tangent_submersion=lambda v, x: 2 * Matrices.to_symmetric( Matrices.mul(x, v)) - v, metric=GrassmannianCanonicalMetric(n, k), )
def to_tangent(self, vector, base_point): """Project a vector to a tangent space of the manifold. Inspired by the method of Pymanopt. Parameters ---------- vector : array-like, shape=[..., n, p] Vector. base_point : array-like, shape=[..., n, p] Point on the manifold. Returns ------- tangent_vec : array-like, shape=[..., n, p] Tangent vector at base point. """ aux = Matrices.mul(Matrices.transpose(base_point), vector) sym_aux = Matrices.to_symmetric(aux) return vector - Matrices.mul(base_point, sym_aux)
def projection(self, point): """Project a matrix to the Grassmann manifold. An eigenvalue decomposition of (the symmetric part of) point is used. Parameters ---------- point : array-like, shape=[..., n, n] Point in embedding manifold. Returns ------- projected : array-like, shape=[..., n, n] Projected point. """ mat = Matrices.to_symmetric(point) _, eigvecs = gs.linalg.eigh(mat) diagonal = gs.array([0.0] * (self.n - self.k) + [1.0] * self.k) p_d = gs.einsum("...ij,...j->...ij", eigvecs, diagonal) return Matrices.mul(p_d, Matrices.transpose(eigvecs))
def __init__(self, n, p): geomstats.errors.check_integer(n, 'n') geomstats.errors.check_integer(p, 'p') if p > n: raise ValueError('p needs to be smaller than n.') dim = int(p * n - (p * (p + 1) / 2)) matrices = Matrices(n, p) super(Stiefel, self).__init__( dim=dim, embedding_space=matrices, submersion=lambda x: matrices.mul(matrices.transpose(x), x), value=gs.eye(p), tangent_submersion=lambda v, x: 2 * matrices.to_symmetric( matrices.mul(matrices.transpose(x), v)), metric=StiefelCanonicalMetric(n, p)) self.n = n self.p = p self.canonical_metric = self.metric
def _aux_exp(tangent_vec, sqrt_base_point, inv_sqrt_base_point): """Compute the exponential map (auxiliary function). Parameters ---------- tangent_vec : array-like, shape=[..., n, n] sqrt_base_point : array-like, shape=[..., n, n] inv_sqrt_base_point : array-like, shape=[..., n, n] Returns ------- exp : array-like, shape=[..., n, n] """ tangent_vec_at_id = Matrices.mul(inv_sqrt_base_point, tangent_vec, inv_sqrt_base_point) tangent_vec_at_id = Matrices.to_symmetric(tangent_vec_at_id) exp_from_id = SymmetricMatrices.expm(tangent_vec_at_id) exp = Matrices.mul(sqrt_base_point, exp_from_id, sqrt_base_point) return exp
def __init__(self, n, p, **kwargs): geomstats.errors.check_integer(n, "n") geomstats.errors.check_integer(p, "p") if p > n: raise ValueError("p needs to be smaller than n.") dim = int(p * n - (p * (p + 1) / 2)) matrices = Matrices(n, p) canonical_metric = StiefelCanonicalMetric(n, p) kwargs.setdefault("metric", canonical_metric) super(Stiefel, self).__init__( dim=dim, embedding_space=matrices, submersion=lambda x: matrices.mul(matrices.transpose(x), x), value=gs.eye(p), tangent_submersion=lambda v, x: 2 * matrices.to_symmetric( matrices.mul(matrices.transpose(x), v)), **kwargs) self.canonical_metric = canonical_metric self.n = n self.p = p
def projection(self, point): """Project a matrix to the space of SPD matrices. First the symmetric part of point is computed, then the eigenvalues are floored to gs.atol. Parameters ---------- point : array-like, shape=[..., n, n] Matrix to project. Returns ------- projected: array-like, shape=[..., n, n] SPD matrix. """ sym = Matrices.to_symmetric(point) eigvals, eigvecs = gs.linalg.eigh(sym) regularized = gs.where(eigvals < gs.atol, gs.atol, eigvals) reconstruction = gs.einsum("...ij,...j->...ij", eigvecs, regularized) return Matrices.mul(reconstruction, Matrices.transpose(eigvecs))
def random_point(self, n_samples=1, bound=1.0): r"""Sample in PSD(n,k) from the log-uniform distribution. Parameters ---------- n_samples : int Number of samples. Optional, default: 1. bound : float Bound of the interval in which to sample in the tangent space. Optional, default: 1. Returns ------- samples : array-like, shape=[..., n, n] Points sampled in PSD(n,k). """ n = self.n size = (n_samples, n, n) if n_samples != 1 else (n, n) mat = bound * (2 * gs.random.rand(*size) - 1) spd_mat = GeneralLinear.exp(Matrices.to_symmetric(mat)) return self.projection(spd_mat)
def projection(self, point): r"""Project a matrix to the space of PSD matrices of rank k. The nearest symmetric positive semidefinite matrix in the Frobenius norm to an arbitrary real matrix A is shown to be (B + H)/2, where H is the symmetric polar factor of B=(A + A')/2. As [Higham1988] is turning the matrix into a PSD, the rank is then forced to be k. Parameters ---------- point : array-like, shape=[..., n, n] Matrix to project. Returns ------- projected: array-like, shape=[..., n, n] PSD matrix rank k. References ---------- [Higham1988]_ Highamm, N. J. “Computing a nearest symmetric positive semidefinite matrix.” Linear Algebra and Its Applications 103 (May 1, 1988): 103-118. https://doi.org/10.1016/0024-3795(88)90223-6 """ sym = Matrices.to_symmetric(point) _, s, v = gs.linalg.svd(sym) h = gs.matmul(Matrices.transpose(v), s[..., None] * v) sym_proj = (sym + h) / 2 eigvals, eigvecs = gs.linalg.eigh(sym_proj) i = gs.array([0] * (self.n - self.rank) + [2 * gs.atol] * self.rank) regularized = ( gs.assignment(eigvals, 0, gs.arange((self.n - self.rank)), axis=0) + i ) reconstruction = gs.einsum("...ij,...j->...ij", eigvecs, regularized) return Matrices.mul(reconstruction, Matrices.transpose(eigvecs))
def to_vector(mat): """Convert a symmetric matrix into a vector. Parameters ---------- mat : array-like, shape=[..., n, n] Matrix. Returns ------- vec : array-like, shape=[..., n(n+1)/2] Vector. """ if not gs.all(Matrices.is_symmetric(mat)): logging.warning('non-symmetric matrix encountered.') mat = Matrices.to_symmetric(mat) _, dim, _ = mat.shape indices_i, indices_j = gs.triu_indices(dim) vec = [] for i, j in zip(indices_i, indices_j): vec.append(mat[:, i, j]) vec = gs.stack(vec, axis=1) return vec
def test_horizontal_lift_and_tangent_riemannian_submersion(self): mat = self.bundle.random_point() tangent_vec = Matrices.to_symmetric(self.bundle.random_point()) horizontal = self.bundle.horizontal_lift(tangent_vec, fiber_point=mat) result = self.bundle.tangent_riemannian_submersion(horizontal, mat) self.assertAllClose(result, tangent_vec)
def tangent_riemannian_submersion(self, tangent_vec, base_point): product = Matrices.mul(base_point, Matrices.transpose(tangent_vec)) return 2 * Matrices.to_symmetric(product)
def test_is_horizontal(self): mat = self.bundle.random_point() tangent_vec = Matrices.to_symmetric(self.bundle.random_point()) horizontal = self.bundle.horizontal_lift(tangent_vec, fiber_point=mat) result = self.bundle.is_horizontal(horizontal, mat) self.assertTrue(result)
class TestMatrices(geomstats.tests.TestCase): def setUp(self): gs.random.seed(1234) self.m = 2 self.n = 3 self.space = Matrices(m=self.n, n=self.n) self.space_nonsquare = Matrices(m=self.m, n=self.n) self.metric = self.space.metric self.n_samples = 2 @geomstats.tests.np_only def test_mul(self): a = gs.eye(3, 3, 1) b = gs.eye(3, 3, -1) c = gs.array([ [1., 0., 0.], [0., 1., 0.], [0., 0., 0.]]) d = gs.array([ [0., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) result = self.space.mul([a, b], [b, a]) expected = gs.array([c, d]) self.assertAllClose(result, expected) result = self.space.mul(a, [a, b]) expected = gs.array([gs.eye(3, 3, 2), c]) self.assertAllClose(result, expected) @geomstats.tests.np_only def test_bracket(self): x = gs.array([ [0., 0., 0.], [0., 0., -1.], [0., 1., 0.]]) y = gs.array([ [0., 0., 1.], [0., 0., 0.], [-1., 0., 0.]]) z = gs.array([ [0., -1., 0.], [1., 0., 0.], [0., 0., 0.]]) result = self.space.bracket([x, y], [y, z]) expected = gs.array([z, x]) self.assertAllClose(result, expected) result = self.space.bracket(x, [x, y, z]) expected = gs.array([gs.zeros((3, 3)), z, -y]) self.assertAllClose(result, expected) @geomstats.tests.np_only def test_transpose(self): tr = self.space.transpose ar = gs.array a = gs.eye(3, 3, 1) b = gs.eye(3, 3, -1) self.assertAllClose(tr(a), b) self.assertAllClose(tr(ar([a, b])), ar([b, a])) def test_is_symmetric(self): not_squared = gs.array([[1., 2.], [2., 1.], [3., 1.]]) result = self.space.is_symmetric(not_squared) expected = False self.assertAllClose(result, expected) sym_mat = gs.array([[1., 2.], [2., 1.]]) result = self.space.is_symmetric(sym_mat) expected = gs.array(True) self.assertAllClose(result, expected) not_a_sym_mat = gs.array([[1., 0.6, -3.], [6., -7., 0.], [0., 7., 8.]]) result = self.space.is_symmetric(not_a_sym_mat) expected = gs.array(False) self.assertAllClose(result, expected) @geomstats.tests.np_only def test_is_skew_symmetric(self): skew_mat = gs.array([[0, - 2.], [2., 0]]) result = self.space.is_skew_symmetric(skew_mat) expected = gs.array(True) self.assertAllClose(result, expected) not_a_sym_mat = gs.array([[1., 0.6, -3.], [6., -7., 0.], [0., 7., 8.]]) result = self.space.is_skew_symmetric(not_a_sym_mat) expected = gs.array(False) self.assertAllClose(result, expected) @geomstats.tests.np_and_tf_only def test_is_symmetric_vectorization(self): points = gs.array([ [[1., 2.], [2., 1.]], [[3., 4.], [4., 5.]], [[1., 2.], [3., 4.]]]) result = self.space.is_symmetric(points) expected = [True, True, False] self.assertAllClose(result, expected) @geomstats.tests.np_and_pytorch_only def test_make_symmetric(self): sym_mat = gs.array([[1., 2.], [2., 1.]]) result = self.space.to_symmetric(sym_mat) expected = sym_mat self.assertAllClose(result, expected) mat = gs.array([[1., 2., 3.], [0., 0., 0.], [3., 1., 1.]]) result = self.space.to_symmetric(mat) expected = gs.array([[1., 1., 3.], [1., 0., 0.5], [3., 0.5, 1.]]) self.assertAllClose(result, expected) mat = gs.array([[1e100, 1e-100, 1e100], [1e100, 1e-100, 1e100], [1e-100, 1e-100, 1e100]]) result = self.space.to_symmetric(mat) res = 0.5 * (1e100 + 1e-100) expected = gs.array([[1e100, res, res], [res, 1e-100, res], [res, res, 1e100]]) self.assertAllClose(result, expected) @geomstats.tests.np_and_tf_only def test_make_symmetric_and_is_symmetric_vectorization(self): points = gs.array([ [[1., 2.], [3., 4.]], [[5., 6.], [4., 9.]]]) sym_points = self.space.to_symmetric(points) result = gs.all(self.space.is_symmetric(sym_points)) expected = True self.assertAllClose(result, expected) def test_inner_product(self): base_point = gs.array([ [1., 2., 3.], [0., 0., 0.], [3., 1., 1.]]) tangent_vector_1 = gs.array([ [1., 2., 3.], [0., -10., 0.], [30., 1., 1.]]) tangent_vector_2 = gs.array([ [1., 4., 3.], [5., 0., 0.], [3., 1., 1.]]) result = self.metric.inner_product( tangent_vector_1, tangent_vector_2, base_point=base_point) expected = gs.trace( gs.matmul( gs.transpose(tangent_vector_1), tangent_vector_2)) self.assertAllClose(result, expected) def test_cong(self): base_point = gs.array([ [1., 2., 3.], [0., 0., 0.], [3., 1., 1.]]) tangent_vector = gs.array([ [1., 2., 3.], [0., -10., 0.], [30., 1., 1.]]) result = self.space.congruent(tangent_vector, base_point) expected = gs.matmul( tangent_vector, gs.transpose(base_point)) expected = gs.matmul(base_point, expected) self.assertAllClose(result, expected) def test_belongs(self): base_point_square = gs.zeros((self.n, self.n)) base_point_nonsquare = gs.zeros((self.m, self.n)) result = self.space.belongs(base_point_square) expected = True self.assertAllClose(result, expected) result = self.space_nonsquare.belongs(base_point_square) expected = False self.assertAllClose(result, expected) result = self.space.belongs(base_point_nonsquare) expected = False self.assertAllClose(result, expected) result = self.space_nonsquare.belongs(base_point_nonsquare) expected = True self.assertAllClose(result, expected) result = self.space.belongs(gs.zeros((2, 2, 3))) self.assertFalse(gs.all(result)) result = self.space.belongs(gs.zeros((2, 3, 3))) self.assertTrue(gs.all(result)) def test_is_diagonal(self): base_point = gs.array([ [1., 2., 3.], [0., 0., 0.], [3., 1., 1.]]) result = self.space.is_diagonal(base_point) expected = False self.assertAllClose(result, expected) diagonal = gs.eye(3) result = self.space.is_diagonal(diagonal) self.assertTrue(result) base_point = gs.stack([base_point, diagonal]) result = self.space.is_diagonal(base_point) expected = gs.array([False, True]) self.assertAllClose(result, expected) base_point = gs.stack([diagonal] * 2) result = self.space.is_diagonal(base_point) self.assertTrue(gs.all(result)) base_point = gs.reshape(gs.arange(6), (2, 3)) result = self.space.is_diagonal(base_point) self.assertTrue(~result) def test_norm(self): for n_samples in [1, 2]: mat = self.space.random_point(n_samples) result = self.metric.norm(mat) expected = self.space.frobenius_product(mat, mat) ** .5 self.assertAllClose(result, expected)