def log(self, point, base_point): r"""Compute the Riemannian logarithm of point w.r.t. base_point. Given :math:`P, P'` in Gr(n, k) the logarithm from :math:`P` to :math:`P` is given by the infinitesimal rotation [Batzies2015]_: .. math:: \omega = \frac 1 2 \log \big((2 P' - 1)(2 P - 1)\big) Parameters ---------- point : array-like, shape=[n_samples, n, n] Point in the Grassmannian. base_point : array-like, shape=[n_samples, n, n] Point in the Grassmannian. Returns ------- tangent_vec : array-like, shape=[n_samples, n, n] Tangent vector at `base_point`. References ---------- .. [Batzies2015] Batzies, Hüper, Machado, Leite. "Geometric Mean and Geodesic Regression on Grassmannians" Linear Algebra and its Applications, 466, 83-101, 2015. """ GLn = GeneralLinear(self.n) id_n = GLn.identity() sym2 = 2 * point - id_n sym1 = 2 * base_point - id_n rot = GLn.mul(sym2, sym1) return GLn.log(rot) / 2
def log(self, point, base_point=None, **kwargs): """Compute logarithm map associated to the canonical metric. Log map at `base_point` of `point`. The geodesics of this metric correspond to a direct product metric between rotation and translation: the translation part is a straight line, while the rotation part has constant angular velocity (which corresponds to one- parameter subgroups of the rotation group). Parameters ---------- point : array-like, shape=[..., n + 1, n + 1] Point on the manifold. base_point : array-like, shape=[..., n + 1, n + 1] Point on the manifold. Returns ------- tangent_vec : array-like, shape=[..., n + 1, n + 1] Tangent vector at the base point. References ---------- [Zefran98] Zefran, M., V. Kumar, and C.B. Croke. “On the Generation of Smooth Three-Dimensional Rigid Body Motions.” IEEE Transactions on Robotics and Automation 14, no. 4 (August 1998): 576–89. https://doi.org/10.1109/70.704225. """ max_shape = point.shape if point.ndim == 3 else base_point.shape rotation_bp = base_point[..., :self.n, :self.n] rotation_p = point[..., :self.n, :self.n] rotation_log = GeneralLinear.log(rotation_p, rotation_bp) translation_log = (point[..., :self.n, self.n] - base_point[..., :self.n, self.n]) log = homogeneous_representation(rotation_log, translation_log, max_shape, 0.0) return log
def log(self, point, base_point, **kwargs): r"""Compute the Riemannian logarithm of point w.r.t. base_point. Given :math:`P, P'` in Gr(n, k) the logarithm from :math:`P` to :math:`P` is induced by the infinitesimal rotation [Batzies2015]_: .. math:: Y = \frac 1 2 \log \big((2 P' - 1)(2 P - 1)\big) The tangent vector :math:`X` at :math:`P` is then recovered by :math:`X = [Y, P]`. Parameters ---------- point : array-like, shape=[..., n, n] Point. base_point : array-like, shape=[..., n, n] Base point. Returns ------- tangent_vec : array-like, shape=[..., n, n] Riemannian logarithm, a tangent vector at `base_point`. References ---------- .. [Batzies2015] Batzies, Hüper, Machado, Leite. "Geometric Mean and Geodesic Regression on Grassmannians" Linear Algebra and its Applications, 466, 83-101, 2015. """ GLn = GeneralLinear(self.n) id_n = GLn.identity id_n, point, base_point = gs.convert_to_wider_dtype( [id_n, point, base_point]) sym2 = 2 * point - id_n sym1 = 2 * base_point - id_n rot = GLn.compose(sym2, sym1) return Matrices.bracket(GLn.log(rot) / 2, base_point)
class TestGeneralLinear(geomstats.tests.TestCase): def setUp(self): gs.random.seed(1234) self.n = 3 self.n_samples = 2 self.group = GeneralLinear(n=self.n) warnings.simplefilter('ignore', category=ImportWarning) def test_belongs_shape(self): mat = gs.eye(3) result = self.group.belongs(mat) self.assertAllClose(gs.shape(result), ()) mat = gs.ones((3, 3)) result = self.group.belongs(mat) self.assertAllClose(gs.shape(result), ()) def test_belongs(self): mat = gs.eye(3) result = self.group.belongs(mat) expected = True self.assertAllClose(result, expected) mat = gs.ones((3, 3)) result = self.group.belongs(mat) expected = False self.assertAllClose(result, expected) def test_belongs_vectorization_shape(self): mats = gs.array([gs.eye(3), gs.ones((3, 3))]) result = self.group.belongs(mats) self.assertAllClose(gs.shape(result), (2, )) def test_belongs_vectorization(self): mats = gs.array([gs.eye(3), gs.ones((3, 3))]) result = self.group.belongs(mats) expected = gs.array([True, False]) self.assertAllClose(result, expected) def test_random_and_belongs(self): point = self.group.random_uniform() result = self.group.belongs(point) expected = True self.assertAllClose(result, expected) def test_random_and_belongs_vectorization(self): n_samples = 4 point = self.group.random_uniform(n_samples) result = self.group.belongs(point) expected = gs.array([True] * n_samples) self.assertAllClose(result, expected) def test_replace_values(self): points = gs.ones((3, 3, 3)) new_points = gs.zeros((2, 3, 3)) indcs = [True, False, True] update = self.group._replace_values(points, new_points, indcs) self.assertAllClose( update, gs.stack([gs.zeros((3, 3)), gs.ones((3, 3)), gs.zeros((3, 3))])) def test_compose(self): mat1 = gs.array([[1., 0.], [0., 2.]]) mat2 = gs.array([[2., 0.], [0., 1.]]) result = self.group.compose(mat1, mat2) expected = 2. * GeneralLinear(2).identity self.assertAllClose(result, expected) def test_inv(self): mat_a = gs.array([[1., 2., 3.], [4., 5., 6.], [7., 8., 10.]]) imat_a = 1. / 3. * gs.array([[-2., -4., 3.], [-2., 11., -6.], [3., -6., 3.]]) expected = imat_a result = self.group.inverse(mat_a) self.assertAllClose(result, expected) def test_inv_vectorized(self): mat_a = gs.array([[0., 1., 0.], [1., 0., 0.], [0., 0., 1.]]) mat_b = -gs.eye(3, 3) result = self.group.inverse(gs.array([mat_a, mat_b])) expected = gs.array([mat_a, mat_b]) self.assertAllClose(result, expected) @geomstats.tests.np_and_tf_only def test_log_and_exp(self): point = 5 * gs.eye(self.n) group_log = self.group.log(point) result = self.group.exp(group_log) expected = point self.assertAllClose(result, expected) def test_exp_vectorization(self): point = gs.array([[[2., 0., 0.], [0., 3., 0.], [0., 0., 4.]], [[1., 0., 0.], [0., 5., 0.], [0., 0., 6.]]]) expected = gs.array([[[7.38905609, 0., 0.], [0., 20.0855369, 0.], [0., 0., 54.5981500]], [[2.718281828, 0., 0.], [0., 148.413159, 0.], [0., 0., 403.42879349]]]) result = self.group.exp(point) self.assertAllClose(result, expected, rtol=1e-3) @geomstats.tests.np_and_tf_only def test_log_vectorization(self): point = gs.array([[[2., 0., 0.], [0., 3., 0.], [0., 0., 4.]], [[1., 0., 0.], [0., 5., 0.], [0., 0., 6.]]]) expected = gs.array([[[0.693147180, 0., 0.], [0., 1.09861228866, 0.], [0., 0., 1.38629436]], [[0., 0., 0.], [0., 1.609437912, 0.], [0., 0., 1.79175946]]]) result = self.group.log(point) self.assertAllClose(result, expected, atol=1e-4) @geomstats.tests.np_and_tf_only 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) @geomstats.tests.np_and_tf_only def test_expm_and_logm_vectorization_symmetric(self): point = gs.array([[[2., 0., 0.], [0., 3., 0.], [0., 0., 4.]], [[1., 0., 0.], [0., 5., 0.], [0., 0., 6.]]]) result = self.group.exp(self.group.log(point)) expected = point self.assertAllClose(result, expected)
class TestGeneralLinear(geomstats.tests.TestCase): 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 test_belongs_shape(self): mat = gs.eye(3) result = self.group.belongs(mat) self.assertAllClose(gs.shape(result), ()) mat = gs.ones((3, 3)) result = self.group.belongs(mat) self.assertAllClose(gs.shape(result), ()) def test_belongs(self): mat = gs.eye(3) result = self.group.belongs(mat) expected = True self.assertAllClose(result, expected) mat = gs.ones((3, 3)) result = self.group.belongs(mat) expected = False self.assertAllClose(result, expected) mat = gs.ones(3) result = self.group.belongs(mat) expected = False self.assertAllClose(result, expected) def test_belongs_vectorization_shape(self): mats = gs.array([gs.eye(3), gs.ones((3, 3))]) result = self.group.belongs(mats) self.assertAllClose(gs.shape(result), (2, )) def test_belongs_vectorization(self): mats = gs.array([gs.eye(3), gs.ones((3, 3))]) result = self.group.belongs(mats) expected = gs.array([True, False]) self.assertAllClose(result, expected) def test_random_and_belongs(self): for group in [self.group, self.group_pos]: point = group.random_point() result = group.belongs(point) self.assertTrue(result) def test_random_and_belongs_vectorization(self): n_samples = 4 expected = gs.array([True] * n_samples) for group in [self.group, self.group_pos]: point = group.random_point(n_samples) result = group.belongs(point) self.assertAllClose(result, expected) def test_compose(self): mat1 = gs.array([[1., 0.], [0., 2.]]) mat2 = gs.array([[2., 0.], [0., 1.]]) result = self.group.compose(mat1, mat2) expected = 2. * GeneralLinear(2).identity self.assertAllClose(result, expected) def test_inv(self): mat_a = gs.array([[1., 2., 3.], [4., 5., 6.], [7., 8., 10.]]) imat_a = 1. / 3. * gs.array([[-2., -4., 3.], [-2., 11., -6.], [3., -6., 3.]]) expected = imat_a result = self.group.inverse(mat_a) self.assertAllClose(result, expected) def test_inv_vectorized(self): mat_a = gs.array([[0., 1., 0.], [1., 0., 0.], [0., 0., 1.]]) mat_b = -gs.eye(3, 3) result = self.group.inverse(gs.array([mat_a, mat_b])) expected = gs.array([mat_a, mat_b]) self.assertAllClose(result, expected) @geomstats.tests.np_and_tf_only def test_log_and_exp(self): point = 5 * gs.eye(self.n) group_log = self.group.log(point) result = self.group.exp(group_log) expected = point self.assertAllClose(result, expected) def test_exp_vectorization(self): point = gs.array([[[2., 0., 0.], [0., 3., 0.], [0., 0., 4.]], [[1., 0., 0.], [0., 5., 0.], [0., 0., 6.]]]) expected = gs.array([[[7.38905609, 0., 0.], [0., 20.0855369, 0.], [0., 0., 54.5981500]], [[2.718281828, 0., 0.], [0., 148.413159, 0.], [0., 0., 403.42879349]]]) expected = gs.cast(expected, gs.float64) point = gs.cast(point, gs.float64) result = self.group.exp(point) self.assertAllClose(result, expected) @geomstats.tests.np_and_tf_only def test_log_vectorization(self): point = gs.array([[[2., 0., 0.], [0., 3., 0.], [0., 0., 4.]], [[1., 0., 0.], [0., 5., 0.], [0., 0., 6.]]]) expected = gs.array([[[0.693147180, 0., 0.], [0., 1.09861228866, 0.], [0., 0., 1.38629436]], [[0., 0., 0.], [0., 1.609437912, 0.], [0., 0., 1.79175946]]]) result = self.group.log(point) self.assertAllClose(result, expected) @geomstats.tests.np_and_tf_only 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.)]]) identity = GeneralLinear(2).identity path = GeneralLinear(2).orbit(point) time = gs.linspace(0., 1., 3) result = path(time) expected = gs.array([identity, sqrt, point]) self.assertAllClose(result, expected) @geomstats.tests.np_and_tf_only 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) @geomstats.tests.np_and_tf_only def test_expm_and_logm_vectorization_symmetric(self): point = gs.array([[[2., 0., 0.], [0., 3., 0.], [0., 0., 4.]], [[1., 0., 0.], [0., 5., 0.], [0., 0., 6.]]]) result = self.group.exp(self.group.log(point)) expected = point self.assertAllClose(result, expected) def test_projection_and_belongs(self): shape = (self.n_samples, self.n, self.n) result = helper.test_projection_and_belongs(self.group, shape) for res in result: self.assertTrue(res) def test_projection_and_belongs_pos(self): shape = (self.n_samples, self.n, self.n) result = helper.test_projection_and_belongs(self.group_pos, shape) for res in result: self.assertTrue(res)
class TestGeneralLinearMethods(geomstats.tests.TestCase): 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) @geomstats.tests.np_only def test_belongs(self): """ A rotation matrix belongs to the matrix Lie group of invertible matrices. """ rot_vec = gs.array([0.2, -0.1, 0.1]) rot_mat = self.so3_group.matrix_from_rotation_vector(rot_vec) result = self.group.belongs(rot_mat) expected = gs.array([[True]]) self.assertAllClose(result, expected) def test_compose(self): # 1. Composition by identity, on the right # Expect the original transformation rot_vec = gs.array([0.2, -0.1, 0.1]) mat = self.so3_group.matrix_from_rotation_vector(rot_vec) result = self.group.compose(mat, self.group.identity) expected = mat expected = helper.to_matrix(mat) self.assertAllClose(result, expected) # 2. Composition by identity, on the left # Expect the original transformation rot_vec = gs.array([0.2, 0.1, -0.1]) mat = self.so3_group.matrix_from_rotation_vector(rot_vec) result = self.group.compose(self.group.identity, mat) expected = mat self.assertAllClose(result, expected) def test_inverse(self): mat = gs.array([[1., 2., 3.], [4., 5., 6.], [7., 8., 10.]]) result = self.group.inverse(mat) expected = 1. / 3. * gs.array([[-2., -4., 3.], [-2., 11., -6.], [3., -6., 3.]]) expected = helper.to_matrix(expected) self.assertAllClose(result, expected) def test_compose_and_inverse(self): # 1. Compose transformation by its inverse on the right # Expect the group identity rot_vec = gs.array([0.2, 0.1, 0.1]) mat = self.so3_group.matrix_from_rotation_vector(rot_vec) inv_mat = self.group.inverse(mat) result = self.group.compose(mat, inv_mat) expected = self.group.identity expected = helper.to_matrix(expected) self.assertAllClose(result, expected) # 2. Compose transformation by its inverse on the left # Expect the group identity rot_vec = gs.array([0.7, 0.1, 0.1]) mat = self.so3_group.matrix_from_rotation_vector(rot_vec) inv_mat = self.group.inverse(mat) result = self.group.compose(inv_mat, mat) expected = self.group.identity expected = helper.to_matrix(expected) self.assertAllClose(result, expected) @geomstats.tests.np_and_tf_only def test_group_log_and_exp(self): point = 5 * gs.eye(self.n) group_log = self.group.log(point) result = self.group.exp(group_log) expected = point expected = helper.to_matrix(expected) self.assertAllClose(result, expected) @geomstats.tests.np_and_tf_only def test_group_exp_vectorization(self): point = gs.array([[[2., 0., 0.], [0., 3., 0.], [0., 0., 4.]], [[1., 0., 0.], [0., 5., 0.], [0., 0., 6.]]]) expected = gs.array([[[7.38905609, 0., 0.], [0., 20.0855369, 0.], [0., 0., 54.5981500]], [[2.718281828, 0., 0.], [0., 148.413159, 0.], [0., 0., 403.42879349]]]) result = self.group.exp(point) self.assertAllClose(result, expected, rtol=1e-3) @geomstats.tests.np_and_tf_only def test_group_log_vectorization(self): point = gs.array([[[2., 0., 0.], [0., 3., 0.], [0., 0., 4.]], [[1., 0., 0.], [0., 5., 0.], [0., 0., 6.]]]) expected = gs.array([[[0.693147180, 0., 0.], [0., 1.09861228866, 0.], [0., 0., 1.38629436]], [[0., 0., 0.], [0., 1.609437912, 0.], [0., 0., 1.79175946]]]) result = self.group.log(point) self.assertAllClose(result, expected, atol=1e-4) @geomstats.tests.np_and_tf_only def test_expm_and_logm_vectorization_symmetric(self): point = gs.array([[[2., 0., 0.], [0., 3., 0.], [0., 0., 4.]], [[1., 0., 0.], [0., 5., 0.], [0., 0., 6.]]]) result = self.group.exp(self.group.log(point)) expected = point self.assertAllClose(result, expected)
def log_from_identity(self, point, point_type=None): """Compute the group logarithm of the point at the identity. Parameters ---------- point: array-like, shape=[n_samples, {dim, [n + 1, n + 1]}] point_type: str, {'vector', 'matrix'}, optional default: self.default_point_type Returns ------- group_log: array-like, shape=[n_samples, {dim, [n + 1, n + 1]}] the group logarithm in the Lie algbra """ point = self.regularize(point, point_type=point_type) rotations = self.rotations dim_rotations = rotations.dim if point_type == 'vector': rot_vec = point[:, :dim_rotations] angle = gs.linalg.norm(rot_vec, axis=1) angle = gs.to_ndarray(angle, to_ndim=2, axis=1) translation = point[:, dim_rotations:] skew_rot_vec = rotations.skew_matrix_from_vector(rot_vec) sq_skew_rot_vec = gs.matmul(skew_rot_vec, skew_rot_vec) mask_close_0 = gs.isclose(angle, 0.) mask_close_pi = gs.isclose(angle, gs.pi) mask_else = ~mask_close_0 & ~mask_close_pi mask_close_0_float = gs.cast(mask_close_0, gs.float32) mask_close_pi_float = gs.cast(mask_close_pi, gs.float32) mask_else_float = gs.cast(mask_else, gs.float32) mask_0 = gs.isclose(angle, 0., atol=1e-6) mask_0_float = gs.cast(mask_0, gs.float32) angle += mask_0_float * gs.ones_like(angle) coef_1 = -0.5 * gs.ones_like(angle) coef_2 = gs.zeros_like(angle) coef_2 += mask_close_0_float * (1. / 12. + angle**2 / 720. + angle**4 / 30240. + angle**6 / 1209600.) delta_angle = angle - gs.pi coef_2 += mask_close_pi_float * ( 1. / PI2 + (PI2 - 8.) * delta_angle / (4. * PI3) - ((PI2 - 12.) * delta_angle**2 / (4. * PI4)) + ((-192. + 12. * PI2 + PI4) * delta_angle**3 / (48. * PI5)) - ((-240. + 12. * PI2 + PI4) * delta_angle**4 / (48. * PI6)) + ((-2880. + 120. * PI2 + 10. * PI4 + PI6) * delta_angle**5 / (480. * PI7)) - ((-3360 + 120. * PI2 + 10. * PI4 + PI6) * delta_angle**6 / (480. * PI8))) psi = 0.5 * angle * gs.sin(angle) / (1 - gs.cos(angle)) coef_2 += mask_else_float * (1 - psi) / (angle**2) n_points, _ = point.shape log_translation = gs.zeros((n_points, self.n)) for i in range(n_points): translation_i = translation[i] term_1_i = coef_1[i] * gs.dot(translation_i, gs.transpose(skew_rot_vec[i])) term_2_i = coef_2[i] * gs.dot(translation_i, gs.transpose(sq_skew_rot_vec[i])) mask_i_float = gs.get_mask_i_float(i, n_points) log_translation += gs.outer( mask_i_float, translation_i + term_1_i + term_2_i) return gs.concatenate([rot_vec, log_translation], axis=1) if point_type == 'matrix': return GeneralLinear.log(point) raise ValueError('Invalid point_type, expected \'vector\' or ' '\'matrix\'.')