class RankKPSDMatrices(Manifold): r"""Class for PSD(n,k). The manifold of symmetric positive definite (PSD) matrices of rank k. Parameters ---------- n : int Integer representing the shape of the matrices: n x n. k: int Integer representing the rank of the matrix (k<n). """ def __init__(self, n, k, **kwargs): super(RankKPSDMatrices, self).__init__(**kwargs, dim=int(k * n - k * (k + 1) / 2)) self.n = n self.rank = k self.sym = SymmetricMatrices(self.n) def belongs(self, mat, atol=gs.atol): r"""Check if the matrix belongs to the space. Parameters ---------- mat : array-like, shape=[..., n, n] Matrix to be checked. atol : float Tolerance. Optional, default: backend atol. Returns ------- belongs : array-like, shape=[...,] Boolean denoting if mat is an SPD matrix. """ is_symmetric = self.sym.belongs(mat, atol) eigvalues = gs.linalg.eigvalsh(mat) is_semipositive = gs.all(eigvalues > -atol, axis=-1) is_rankk = gs.sum(gs.where(eigvalues < atol, 0, 1), axis=-1) == self.rank belongs = gs.logical_and(gs.logical_and(is_symmetric, is_semipositive), is_rankk) return belongs 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 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 is_tangent(self, vector, base_point): r"""Check if the vector belongs to the tangent space. Parameters ---------- vector : array-like, shape=[..., n, n] Matrix to check if it belongs to the tangent space. base_point : array-like, shape=[..., n, n] Base point of the tangent space. Optional, default: None. Returns ------- belongs : array-like, shape=[...,] Boolean denoting if vector belongs to tangent space at base_point. """ vector_sym = Matrices(self.n, self.n).to_symmetric(vector) _, r = gs.linalg.eigh(base_point) r_ort = r[..., :, self.n - self.rank:self.n] r_ort_t = Matrices.transpose(r_ort) rr = gs.matmul(r_ort, r_ort_t) candidates = Matrices.mul(rr, vector_sym, rr) result = gs.all(gs.isclose(candidates, 0., gs.atol), axis=(-2, -1)) return result def to_tangent(self, vector, base_point): r"""Project to the tangent space of PSD(n,k) at base_point. Parameters ---------- vector : array-like, shape=[..., n, n] Matrix to check if it belongs to the tangent space. base_point : array-like, shape=[..., n, n] Base point of the tangent space. Optional, default: None. Returns ------- tangent : array-like, shape=[...,n,n] Projection of the tangent vector at base_point. """ vector_sym = Matrices(self.n, self.n).to_symmetric(vector) _, r = gs.linalg.eigh(base_point) r_ort = r[..., :, self.n - self.rank:self.n] r_ort_t = Matrices.transpose(r_ort) rr = gs.matmul(r_ort, r_ort_t) return vector_sym - Matrices.mul(rr, vector_sym, rr)
class TestSymmetricMatrices(geomstats.tests.TestCase): """Test of SymmetricMatrices methods.""" def setUp(self): """Set up the test.""" warnings.simplefilter("ignore", category=ImportWarning) gs.random.seed(1234) self.n = 3 self.space = SymmetricMatrices(self.n) def test_belongs(self): """Test of belongs method.""" sym_n = self.space mat_sym = gs.array([[1.0, 2.0, 3.0], [2.0, 4.0, 5.0], [3.0, 5.0, 6.0]]) mat_not_sym = gs.array([[1.0, 0.0, 3.0], [2.0, 4.0, 5.0], [3.0, 5.0, 6.0]]) result = sym_n.belongs(mat_sym) expected = True self.assertAllClose(result, expected) result = sym_n.belongs(mat_not_sym) expected = False self.assertAllClose(result, expected) def test_basis(self): """Test of belongs method.""" sym_n = SymmetricMatrices(2) mat_sym_1 = gs.array([[1.0, 0.0], [0, 0]]) mat_sym_2 = gs.array([[0, 1.0], [1.0, 0]]) mat_sym_3 = gs.array([[0, 0.0], [0, 1.0]]) expected = gs.stack([mat_sym_1, mat_sym_2, mat_sym_3]) result = sym_n.basis self.assertAllClose(result, expected) def test_expm(self): """Test of expm method.""" sym_n = SymmetricMatrices(self.n) v = gs.array([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]) result = sym_n.expm(v) c = math.cosh(1) s = math.sinh(1) e = math.exp(1) expected = gs.array([[c, s, 0.0], [s, c, 0.0], [0.0, 0.0, e]]) four_dim_v = gs.broadcast_to(v, (2, 2) + v.shape) four_dim_expected = gs.broadcast_to(expected, (2, 2) + expected.shape) four_dim_result = sym_n.expm(four_dim_v) self.assertAllClose(result, expected) self.assertAllClose(four_dim_result, four_dim_expected) def test_powerm(self): """Test of powerm method.""" sym_n = SymmetricMatrices(self.n) expected = gs.array( [[[1, 1.0 / 4.0, 0.0], [1.0 / 4, 2.0, 0.0], [0.0, 0.0, 1.0]]] ) power = gs.array(1.0 / 2.0) result = sym_n.powerm(expected, power) result = gs.matmul(result, gs.transpose(result, (0, 2, 1))) self.assertAllClose(result, expected) def test_vector_from_symmetric_matrix_and_symmetric_matrix_from_vector(self): """Test for matrix to vector and vector to matrix conversions.""" sym_mat_1 = gs.array([[1.0, 0.6, -3.0], [0.6, 7.0, 0.0], [-3.0, 0.0, 8.0]]) vector_1 = self.space.to_vector(sym_mat_1) result_1 = self.space.from_vector(vector_1) expected_1 = sym_mat_1 self.assertTrue(gs.allclose(result_1, expected_1)) vector_2 = gs.array([1, 2, 3, 4, 5, 6]) sym_mat_2 = self.space.from_vector(vector_2) result_2 = self.space.to_vector(sym_mat_2) expected_2 = vector_2 self.assertTrue(gs.allclose(result_2, expected_2)) def test_vector_and_symmetric_matrix_vectorization(self): """Test of vectorization.""" n_samples = 5 vector = gs.random.rand(n_samples, 6) sym_mat = self.space.from_vector(vector) result = self.space.to_vector(sym_mat) expected = vector self.assertTrue(gs.allclose(result, expected)) vector = self.space.to_vector(sym_mat) result = self.space.from_vector(vector) expected = sym_mat self.assertTrue(gs.allclose(result, expected)) def test_symmetric_matrix_from_vector(self): vector_2 = gs.array([1, 2, 3, 4, 5, 6]) result = self.space.from_vector(vector_2) expected = gs.array([[1.0, 2.0, 3.0], [2.0, 4.0, 5.0], [3.0, 5.0, 6.0]]) self.assertAllClose(result, expected) def test_projection_and_belongs(self): shape = (2, self.n, self.n) result = helper.test_projection_and_belongs(self.space, shape) for res in result: self.assertTrue(res) def test_random_and_belongs(self): mat = self.space.random_point() result = self.space.belongs(mat) self.assertTrue(result) def test_dim(self): result = self.space.dim n = self.space.n expected = int(n * (n + 1) / 2) self.assertAllClose(result, expected)
class TestSymmetricMatrices(geomstats.tests.TestCase): """Test of SymmetricMatrices methods.""" def setUp(self): """Set up the test.""" warnings.simplefilter('ignore', category=ImportWarning) gs.random.seed(1234) self.n = 3 self.space = SymmetricMatrices(self.n) def test_belongs(self): """Test of belongs method.""" sym_n = self.space mat_sym = gs.array([[1., 2., 3.], [2., 4., 5.], [3., 5., 6.]]) mat_not_sym = gs.array([[1., 0., 3.], [2., 4., 5.], [3., 5., 6.]]) result = sym_n.belongs(mat_sym) expected = True self.assertAllClose(result, expected) result = sym_n.belongs(mat_not_sym) expected = False self.assertAllClose(result, expected) def test_basis(self): """Test of belongs method.""" sym_n = SymmetricMatrices(2) mat_sym_1 = gs.array([[1., 0.], [0, 0]]) mat_sym_2 = gs.array([[0, 1.], [1., 0]]) mat_sym_3 = gs.array([[0, 0.], [0, 1.]]) expected = gs.stack([mat_sym_1, mat_sym_2, mat_sym_3]) result = sym_n.basis self.assertAllClose(result, expected) def test_expm(self): """Test of expm method.""" sym_n = SymmetricMatrices(self.n) v = gs.array([[0., 1., 0.], [1., 0., 0.], [0., 0., 1.]]) result = sym_n.expm(v) c = math.cosh(1) s = math.sinh(1) e = math.exp(1) expected = gs.array([[c, s, 0.], [s, c, 0.], [0., 0., e]]) self.assertAllClose(result, expected) def test_powerm(self): """Test of powerm method.""" sym_n = SymmetricMatrices(self.n) expected = gs.array( [[[1, 1. / 4., 0.], [1. / 4, 2., 0.], [0., 0., 1.]]]) expected = gs.cast(expected, gs.float64) power = gs.array(1. / 2) power = gs.cast(power, gs.float64) result = sym_n.powerm(expected, power) result = gs.matmul(result, gs.transpose(result, (0, 2, 1))) self.assertAllClose(result, expected) def test_vector_from_symmetric_matrix_and_symmetric_matrix_from_vector( self): """Test for matrix to vector and vector to matrix conversions.""" sym_mat_1 = gs.array([[1., 0.6, -3.], [0.6, 7., 0.], [-3., 0., 8.]]) vector_1 = self.space.to_vector(sym_mat_1) result_1 = self.space.from_vector(vector_1) expected_1 = sym_mat_1 self.assertTrue(gs.allclose(result_1, expected_1)) vector_2 = gs.array([1, 2, 3, 4, 5, 6]) sym_mat_2 = self.space.from_vector(vector_2) result_2 = self.space.to_vector(sym_mat_2) expected_2 = vector_2 self.assertTrue(gs.allclose(result_2, expected_2)) def test_vector_and_symmetric_matrix_vectorization(self): """Test of vectorization.""" n_samples = 5 vector = gs.random.rand(n_samples, 6) sym_mat = self.space.from_vector(vector) result = self.space.to_vector(sym_mat) expected = vector self.assertTrue(gs.allclose(result, expected)) vector = self.space.to_vector(sym_mat) result = self.space.from_vector(vector) expected = sym_mat self.assertTrue(gs.allclose(result, expected)) def test_symmetric_matrix_from_vector(self): vector_2 = gs.array([1, 2, 3, 4, 5, 6]) result = self.space.from_vector(vector_2) expected = gs.array([[1., 2., 3.], [2., 4., 5.], [3., 5., 6.]]) self.assertAllClose(result, expected) def test_projection_and_belongs(self): mat = gs.random.rand(3, 3) projection = self.space.projection(mat) result = self.space.belongs(projection) self.assertTrue(result) def test_random_and_belongs(self): mat = self.space.random_point() result = self.space.belongs(mat) self.assertTrue(result)