class TestIntegrator(geomstats.tests.TestCase): def setup_method(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()) @staticmethod def function_linear(_state, _time): return 2.0 def _test_step(self, step): state = self.intercept result = step(self.function_linear, state, 0.0, self.dt) expected = state + 2 * self.dt self.assertAllClose(result, expected) def test_symplectic_euler_step(self): with pytest.raises(NotImplementedError): self._test_step(integrator.symplectic_euler_step) def test_leapfrog_step(self): with pytest.raises(NotImplementedError): self._test_step(integrator.leapfrog_step) def test_euler_step(self): self._test_step(integrator.euler_step) def test_rk2_step(self): self._test_step(integrator.rk2_step) def test_rk4_step(self): self._test_step(integrator.rk4_step) def test_integrator(self): initial_state = self.euclidean.random_point(2) def function(state, _time): _, velocity = state return gs.stack([velocity, gs.zeros_like(velocity)]) for step in ["euler", "rk2", "rk4"]: flow = integrator.integrate(function, initial_state, step=step) result = flow[-1][0] expected = initial_state[0] + initial_state[1] self.assertAllClose(result, expected)
def test_linear_mean(self): euclidean = Euclidean(3) point = euclidean.random_point(self.n_samples) estimator = ExponentialBarycenter(euclidean) estimator.fit(point) result = estimator.estimate_ expected = gs.mean(point, axis=0) self.assertAllClose(result, expected)
class TestIntegrator(geomstats.tests.TestCase): 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(1) self.slope = Matrices.to_symmetric(self.matrices.random_point(1)) def function_linear(self, point, vector): return point, -gs.dot(self.slope, vector) def test_euler_step(self): state = (self.intercept, self.slope) result = len( integrator.euler_step(state, self.function_linear, self.dt)) expected = len(state) self.assertAllClose(result, expected) def test_rk4_step(self): state = (self.intercept, self.slope) result = len(integrator.rk4_step(state, self.function_linear, self.dt)) expected = len(state) self.assertAllClose(result, expected) def test_integrator(self): initial_state = self.euclidean.random_point(2) def function(_, velocity): return velocity, gs.zeros_like(velocity) for step in ['euler', 'rk4']: flow, _ = integrator.integrate(function, initial_state, step=step) result = flow[-1] expected = initial_state[0] + initial_state[1] self.assertAllClose(result, expected)
class _SpecialEuclideanVectors(LieGroup): """Base Class for the special Euclidean groups in 2d and 3d in vector form. i.e. the Lie group of rigid transformations. Elements of SE(2), SE(3) can either be represented as vectors (in 2d or 3d) or as matrices in general. The matrix representation corresponds to homogeneous coordinates. This class is specific to the vector representation of rotations. For the matrix representation use the SpecialEuclidean class and set `n=2` or `n=3`. Parameter --------- epsilon : float Precision to use for calculations involving potential division by 0 in rotations. Optional, default: 0. """ def __init__(self, n, epsilon=0.0): dim = n * (n + 1) // 2 LieGroup.__init__( self, dim=dim, shape=(dim, ), default_point_type="vector", lie_algebra=Euclidean(dim), ) self.n = n self.epsilon = epsilon self.rotations = SpecialOrthogonal(n=n, point_type="vector", epsilon=epsilon) self.translations = Euclidean(dim=n) def get_identity(self, point_type=None): """Get the identity of the group. Parameters ---------- point_type : str, {'vector', 'matrix'} The point_type of the returned value. Optional, default: self.default_point_type Returns ------- identity : array-like, shape={[dim], [n + 1, n + 1]} """ if point_type is None: point_type = self.default_point_type identity = gs.zeros(self.dim) return identity identity = property(get_identity) def get_point_type_shape(self, point_type=None): """Get the shape of the instance given the default_point_style.""" return self.get_identity(point_type).shape def belongs(self, point, atol=gs.atol): """Evaluate if a point belongs to SE(2) or SE(3). Parameters ---------- point : array-like, shape=[..., dim] Point to check. Returns ------- belongs : array-like, shape=[...,] Boolean indicating whether point belongs to SE(2) or SE(3). """ point_dim = point.shape[-1] point_ndim = point.ndim belongs = gs.logical_and(point_dim == self.dim, point_ndim < 3) belongs = gs.logical_and( belongs, self.rotations.belongs(point[..., :self.rotations.dim], atol=atol)) return belongs def projection(self, point): """Project a point to the group. The point is regularized, so that the norm of the rotation part lie in [0, pi). Parameters ---------- point: array-like, shape[..., dim] Point. Returns ------- projected: array-like, shape[..., dim] Regularized point. """ return self.regularize(point) def regularize(self, point): """Regularize a point to the default representation for SE(n). Parameters ---------- point : array-like, shape=[..., dim] Point to regularize. Returns ------- point : array-like, shape=[..., dim] Regularized point. """ rotations = self.rotations dim_rotations = rotations.dim regularized_point = gs.copy(point) rot_vec = regularized_point[..., :dim_rotations] regularized_rot_vec = rotations.regularize(rot_vec) translation = regularized_point[..., dim_rotations:] return gs.concatenate([regularized_rot_vec, translation], axis=-1) @geomstats.vectorization.decorator(["else", "vector", "else"]) def regularize_tangent_vec_at_identity(self, tangent_vec, metric=None): """Regularize a tangent vector at the identity. Parameters ---------- tangent_vec: array-like, shape=[..., dim] Tangent vector at base point. metric : RiemannianMetric Metric. Optional, default: None. Returns ------- regularized_vec : array-like, shape=[..., dim] Regularized vector. """ return self.regularize_tangent_vec(tangent_vec, self.identity, metric) @geomstats.vectorization.decorator(["else", "vector"]) def matrix_from_vector(self, vec): """Convert point in vector point-type to matrix. Parameters ---------- vec : array-like, shape=[..., dim] Vector. Returns ------- mat : array-like, shape=[..., n+1, n+1] Matrix. """ vec = self.regularize(vec) output_shape = ((vec.shape[0], self.n + 1, self.n + 1) if vec.ndim == 2 else (self.n + 1, ) * 2) rot_vec = vec[..., :self.rotations.dim] trans_vec = vec[..., self.rotations.dim:] rot_mat = self.rotations.matrix_from_rotation_vector(rot_vec) return homogeneous_representation(rot_mat, trans_vec, output_shape) @geomstats.vectorization.decorator(["else", "vector", "vector"]) def compose(self, point_a, point_b): r"""Compose two elements of SE(2) or SE(3). Parameters ---------- point_a : array-like, shape=[..., dim] Point of the group. point_b : array-like, shape=[..., dim] Point of the group. Equation -------- (:math: `(R_1, t_1) \\cdot (R_2, t_2) = (R_1 R_2, R_1 t_2 + t_1)`) Returns ------- composition : array-like, shape=[..., dim] Composition of point_a and point_b. """ rotations = self.rotations dim_rotations = rotations.dim point_a = self.regularize(point_a) point_b = self.regularize(point_b) 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) @geomstats.vectorization.decorator(["else", "vector"]) def inverse(self, point): r"""Compute the group inverse in SE(n). Parameters ---------- point: array-like, shape=[..., dim] Point. Returns ------- inverse_point : array-like, shape=[..., dim] Inverted point. Notes ----- :math:`(R, t)^{-1} = (R^{-1}, R^{-1}.(-t))` """ rotations = self.rotations dim_rotations = rotations.dim point = self.regularize(point) rot_vec = point[:, :dim_rotations] translation = point[:, dim_rotations:] inverse_rotation = -rot_vec inv_rot_mat = rotations.matrix_from_rotation_vector(inverse_rotation) inverse_translation = gs.einsum( "ni,nij->nj", -translation, gs.transpose(inv_rot_mat, axes=(0, 2, 1))) inverse_point = gs.concatenate([inverse_rotation, inverse_translation], axis=-1) return self.regularize(inverse_point) @geomstats.vectorization.decorator(["else", "vector"]) def exp_from_identity(self, tangent_vec): """Compute group exponential of the tangent vector at the identity. Parameters ---------- tangent_vec: array-like, shape=[..., 3] Tangent vector at base point. Returns ------- group_exp: array-like, shape=[..., 3] Group exponential of the tangent vectors computed at the identity. """ rotations = self.rotations dim_rotations = rotations.dim rot_vec = tangent_vec[..., :dim_rotations] rot_vec_regul = self.rotations.regularize(rot_vec) rot_vec_regul = gs.to_ndarray(rot_vec_regul, to_ndim=2, axis=1) transform = self._exp_translation_transform(rot_vec_regul) translation = tangent_vec[..., dim_rotations:] exp_translation = gs.einsum("ijk, ik -> ij", transform, translation) group_exp = gs.concatenate([rot_vec, exp_translation], axis=1) group_exp = self.regularize(group_exp) return group_exp @geomstats.vectorization.decorator(["else", "vector"]) def log_from_identity(self, point): """Compute the group logarithm of the point at the identity. Parameters ---------- point: array-like, shape=[..., 3] Point. Returns ------- group_log: array-like, shape=[..., 3] Group logarithm in the Lie algebra. """ point = self.regularize(point) rotations = self.rotations dim_rotations = rotations.dim rot_vec = point[:, :dim_rotations] translation = point[:, dim_rotations:] transform = self._log_translation_transform(rot_vec) log_translation = gs.einsum("ijk, ik -> ij", transform, translation) return gs.concatenate([rot_vec, log_translation], axis=1) def random_point(self, n_samples=1, bound=1.0, **kwargs): r"""Sample in SE(n) with the uniform distribution. Parameters ---------- n_samples : int Number of samples. Optional, default: 1. bound : float Upper bound for the translation part of the sample. Optional, default: 1. Returns ------- random_point : array-like, shape=[..., dim] Sample. """ random_translation = self.translations.random_point(n_samples, bound) random_rot_vec = self.rotations.random_uniform(n_samples) return gs.concatenate([random_rot_vec, random_translation], axis=-1)
class _SpecialEuclideanMatrices(MatrixLieGroup, LevelSet): """Class for special Euclidean group. Parameters ---------- n : int Integer dimension of the underlying Euclidean space. Matrices will be of size: (n+1) x (n+1). Attributes ---------- rotations : SpecialOrthogonal Subgroup of rotations of size n. translations : Euclidean Subgroup of translations of size n. left_canonical_metric : InvariantMetric The left invariant metric that corresponds to the Frobenius inner product at the identity. right_canonical_metric : InvariantMetric The right invariant metric that corresponds to the Frobenius inner product at the identity. metric : MatricesMetric The Euclidean (Frobenius) inner product. """ def __init__(self, n, **kwargs): super().__init__(n=n + 1, dim=int((n * (n + 1)) / 2), embedding_space=GeneralLinear(n + 1, positive_det=True), submersion=submersion, value=gs.eye(n + 1), tangent_submersion=tangent_submersion, lie_algebra=SpecialEuclideanMatrixLieAlgebra(n=n), **kwargs) self.rotations = SpecialOrthogonal(n=n) self.translations = Euclidean(dim=n) self.n = n self.left_canonical_metric = SpecialEuclideanMatrixCannonicalLeftMetric( group=self) if self._metric is None: self._metric = self.left_canonical_metric @property def identity(self): """Return the identity matrix.""" return gs.eye(self.n + 1, self.n + 1) def random_point(self, n_samples=1, bound=1.0): """Sample in SE(n) from the uniform distribution. Parameters ---------- n_samples : int Number of samples. Optional, default: 1. bound: float Bound of the interval in which to sample each entry of the translation part. Optional, default: 1. Returns ------- samples : array-like, shape=[..., n + 1, n + 1] Sample in SE(n). """ random_translation = self.translations.random_point(n_samples) random_rotation = self.rotations.random_uniform(n_samples) output_shape = ((n_samples, self.n + 1, self.n + 1) if n_samples != 1 else (self.n + 1, ) * 2) random_point = homogeneous_representation(random_rotation, random_translation, output_shape) return random_point @classmethod def inverse(cls, point): """Return the inverse of a point. Parameters ---------- point : array-like, shape=[..., n + 1, n + 1] Point to be inverted. Returns ------- inverse : array-like, shape=[..., n + 1, n + 1] Inverse of point. """ n = point.shape[-1] - 1 transposed_rot = Matrices.transpose(point[..., :n, :n]) translation = point[..., :n, -1] translation = gs.einsum("...ij,...j->...i", transposed_rot, translation) return homogeneous_representation(transposed_rot, -translation, point.shape) def projection(self, mat): """Project a matrix on SE(n). The upper-left n x n block is projected to SO(n) by minimizing the Frobenius norm. The last columns is kept unchanged and used as the translation part. The last row is discarded. Parameters ---------- mat : array-like, shape=[..., n + 1, n + 1] Matrix. Returns ------- projected : array-like, shape=[..., n + 1, n + 1] Rotation-translation matrix in homogeneous representation. """ n = mat.shape[-1] - 1 projected_rot = self.rotations.projection(mat[..., :n, :n]) translation = mat[..., :n, -1] return homogeneous_representation(projected_rot, translation, mat.shape)
class TestEuclidean(geomstats.tests.TestCase): def setUp(self): gs.random.seed(1234) self.dimension = 2 self.space = Euclidean(self.dimension) self.metric = self.space.metric self.n_samples = 3 self.one_point_a = gs.array([0., 1.]) self.one_point_b = gs.array([2., 10.]) self.n_points_a = gs.array([[2., 1.], [-2., -4.], [-5., 1.]]) self.n_points_b = gs.array([[2., 10.], [8., -1.], [-3., 6.]]) def test_random_point_and_belongs(self): point = self.space.random_point() result = self.space.belongs(point) expected = True self.assertAllClose(result, expected) def test_is_tangent(self): vector = self.space.random_point() result = self.space.is_tangent(vector) self.assertTrue(result) def test_to_tangent(self): vector = self.space.random_point() result = self.space.to_tangent(vector) self.assertAllClose(result, vector) def test_squared_norm_vectorization(self): n_samples = self.n_samples n_points = gs.array([[2., 1.], [-2., -4.], [-5., 1.]]) result = self.metric.squared_norm(n_points) expected = gs.array([5., 20., 26.]) self.assertAllClose(gs.shape(result), (n_samples, )) self.assertAllClose(result, expected) def test_norm_vectorization_single_sample(self): one_point = gs.array([[0., 1.]]) result = self.metric.norm(one_point) expected = gs.array([1.]) self.assertAllClose(gs.shape(result), (1, )) self.assertAllClose(result, expected) one_point = gs.array([0., 1.]) result = self.metric.norm(one_point) expected = 1. self.assertAllClose(gs.shape(result), ()) self.assertAllClose(result, expected) def test_norm_vectorization_n_samples(self): n_samples = self.n_samples n_points = gs.array([[2., 1.], [-2., -4.], [-5., 1.]]) result = self.metric.norm(n_points) expected = gs.array([2.2360679775, 4.472135955, 5.09901951359]) self.assertAllClose(gs.shape(result), (n_samples, )) self.assertAllClose(result, expected) def test_exp_vectorization(self): n_samples = self.n_samples dim = self.dimension one_tangent_vec = gs.array([0., 1.]) one_base_point = gs.array([2., 10.]) n_tangent_vecs = gs.array([[2., 1.], [-2., -4.], [-5., 1.]]) n_base_points = gs.array([[2., 10.], [8., -1.], [-3., 6.]]) result = self.metric.exp(one_tangent_vec, one_base_point) expected = one_tangent_vec + one_base_point self.assertAllClose(result, expected) result = self.metric.exp(n_tangent_vecs, one_base_point) self.assertAllClose(gs.shape(result), (n_samples, dim)) result = self.metric.exp(one_tangent_vec, n_base_points) self.assertAllClose(gs.shape(result), (n_samples, dim)) result = self.metric.exp(n_tangent_vecs, n_base_points) self.assertAllClose(gs.shape(result), (n_samples, dim)) def test_log_vectorization(self): n_samples = self.n_samples dim = self.dimension one_point = gs.array([0., 1.]) one_base_point = gs.array([2., 10.]) n_points = gs.array([[2., 1.], [-2., -4.], [-5., 1.]]) n_base_points = gs.array([[2., 10.], [8., -1.], [-3., 6.]]) result = self.metric.log(one_point, one_base_point) expected = one_point - one_base_point self.assertAllClose(result, expected) result = self.metric.log(n_points, one_base_point) self.assertAllClose(gs.shape(result), (n_samples, dim)) result = self.metric.log(one_point, n_base_points) self.assertAllClose(gs.shape(result), (n_samples, dim)) result = self.metric.log(n_points, n_base_points) self.assertAllClose(gs.shape(result), (n_samples, dim)) def test_squared_dist_vectorization(self): n_samples = self.n_samples one_point_a = gs.array([0., 1.]) one_point_b = gs.array([2., 10.]) n_points_a = gs.array([[2., 1.], [-2., -4.], [-5., 1.]]) n_points_b = gs.array([[2., 10.], [8., -1.], [-3., 6.]]) result = self.metric.squared_dist(one_point_a, one_point_b) vec = one_point_a - one_point_b expected = gs.dot(vec, gs.transpose(vec)) self.assertAllClose(result, expected) result = self.metric.squared_dist(n_points_a, one_point_b) self.assertAllClose(gs.shape(result), (n_samples, )) result = self.metric.squared_dist(one_point_a, n_points_b) self.assertAllClose(gs.shape(result), (n_samples, )) result = self.metric.squared_dist(n_points_a, n_points_b) expected = gs.array([81., 109., 29.]) self.assertAllClose(gs.shape(result), (n_samples, )) self.assertAllClose(result, expected) def test_dist_vectorization(self): n_samples = self.n_samples one_point_a = gs.array([0., 1.]) one_point_b = gs.array([2., 10.]) n_points_a = gs.array([[2., 1.], [-2., -4.], [-5., 1.]]) n_points_b = gs.array([[2., 10.], [8., -1.], [-3., 6.]]) result = self.metric.dist(one_point_a, one_point_b) vec = one_point_a - one_point_b expected = gs.sqrt(gs.dot(vec, gs.transpose(vec))) self.assertAllClose(result, expected) result = self.metric.dist(n_points_a, one_point_b) self.assertAllClose(gs.shape(result), (n_samples, )) result = self.metric.dist(one_point_a, n_points_b) self.assertAllClose(gs.shape(result), (n_samples, )) result = self.metric.dist(n_points_a, n_points_b) expected = gs.array([9., gs.sqrt(109.), gs.sqrt(29.)]) self.assertAllClose(gs.shape(result), (n_samples, )) self.assertAllClose(result, expected) def test_belongs(self): point = gs.array([0., 1.]) result = self.space.belongs(point) expected = True self.assertAllClose(result, expected) def test_random_point(self): result = self.space.random_point() self.assertAllClose(gs.shape(result), (self.dimension, )) def test_inner_product_matrix(self): result = self.metric.metric_matrix() expected = gs.eye(self.dimension) self.assertAllClose(result, expected) def test_inner_product(self): point_a = gs.array([0., 1.]) point_b = gs.array([2., 10.]) result = self.metric.inner_product(point_a, point_b) expected = 10. self.assertAllClose(result, expected) def test_inner_product_vectorization_single_sample(self): one_point_a = gs.array([[0., 1.]]) one_point_b = gs.array([[2., 10.]]) result = self.metric.inner_product(one_point_a, one_point_b) expected = gs.array([10.]) self.assertAllClose(gs.shape(result), (1, )) self.assertAllClose(result, expected) one_point_a = gs.array([[0., 1.]]) one_point_b = gs.array([2., 10.]) result = self.metric.inner_product(one_point_a, one_point_b) expected = gs.array([10.]) self.assertAllClose(gs.shape(result), (1, )) self.assertAllClose(result, expected) one_point_a = gs.array([0., 1.]) one_point_b = gs.array([[2., 10.]]) result = self.metric.inner_product(one_point_a, one_point_b) expected = gs.array([10.]) self.assertAllClose(gs.shape(result), (1, )) self.assertAllClose(result, expected) one_point_a = gs.array([0., 1.]) one_point_b = gs.array([2., 10.]) result = self.metric.inner_product(one_point_a, one_point_b) expected = 10. self.assertAllClose(gs.shape(result), ()) self.assertAllClose(result, expected) def test_inner_product_vectorization_n_samples(self): n_samples = 3 n_points_a = gs.array([[2., 1.], [-2., -4.], [-5., 1.]]) n_points_b = gs.array([[2., 10.], [8., -1.], [-3., 6.]]) one_point_a = gs.array([0., 1.]) one_point_b = gs.array([2., 10.]) result = self.metric.inner_product(n_points_a, one_point_b) expected = gs.array([14., -44., 0.]) self.assertAllClose(gs.shape(result), (n_samples, )) self.assertAllClose(result, expected) result = self.metric.inner_product(one_point_a, n_points_b) expected = gs.array([10., -1., 6.]) self.assertAllClose(gs.shape(result), (n_samples, )) self.assertAllClose(result, expected) result = self.metric.inner_product(n_points_a, n_points_b) expected = gs.array([14., -12., 21.]) self.assertAllClose(gs.shape(result), (n_samples, )) self.assertAllClose(result, expected) one_point_a = gs.array([[0., 1.]]) one_point_b = gs.array([[2., 10]]) result = self.metric.inner_product(n_points_a, one_point_b) expected = gs.array([14., -44., 0.]) self.assertAllClose(gs.shape(result), (n_samples, )) self.assertAllClose(result, expected) result = self.metric.inner_product(one_point_a, n_points_b) expected = gs.array([10., -1., 6.]) self.assertAllClose(gs.shape(result), (n_samples, )) self.assertAllClose(result, expected) result = self.metric.inner_product(n_points_a, n_points_b) expected = gs.array([14., -12., 21.]) self.assertAllClose(gs.shape(result), (n_samples, )) self.assertAllClose(result, expected) one_point_a = gs.array([[0., 1.]]) one_point_b = gs.array([2., 10.]) result = self.metric.inner_product(n_points_a, one_point_b) expected = gs.array([14., -44., 0.]) self.assertAllClose(gs.shape(result), (n_samples, )) self.assertAllClose(result, expected) result = self.metric.inner_product(one_point_a, n_points_b) expected = gs.array([10., -1., 6.]) self.assertAllClose(gs.shape(result), (n_samples, )) self.assertAllClose(result, expected) result = self.metric.inner_product(n_points_a, n_points_b) expected = gs.array([14., -12., 21.]) self.assertAllClose(gs.shape(result), (n_samples, )) self.assertAllClose(result, expected) one_point_a = gs.array([0., 1.]) one_point_b = gs.array([[2., 10.]]) result = self.metric.inner_product(n_points_a, one_point_b) expected = gs.array([14., -44., 0.]) self.assertAllClose(gs.shape(result), (n_samples, )) self.assertAllClose(result, expected) result = self.metric.inner_product(one_point_a, n_points_b) expected = gs.array([10., -1., 6.]) self.assertAllClose(gs.shape(result), (n_samples, )) self.assertAllClose(result, expected) result = self.metric.inner_product(n_points_a, n_points_b) expected = gs.array([14., -12., 21.]) self.assertAllClose(gs.shape(result), (n_samples, )) self.assertAllClose(result, expected) def test_squared_norm(self): point = gs.array([-2., 4.]) result = self.metric.squared_norm(point) expected = 20. self.assertAllClose(result, expected) def test_norm(self): point = gs.array([-2., 4.]) result = self.metric.norm(point) expected = 4.472135955 self.assertAllClose(result, expected) def test_exp(self): base_point = gs.array([0., 1.]) vector = gs.array([2., 10.]) result = self.metric.exp(tangent_vec=vector, base_point=base_point) expected = base_point + vector self.assertAllClose(result, expected) def test_log(self): base_point = gs.array([0., 1.]) point = gs.array([2., 10.]) result = self.metric.log(point=point, base_point=base_point) expected = point - base_point self.assertAllClose(result, expected) def test_squared_dist(self): point_a = gs.array([-1., 4.]) point_b = gs.array([1., 1.]) result = self.metric.squared_dist(point_a, point_b) vec = point_b - point_a expected = gs.dot(vec, vec) self.assertAllClose(result, expected) def test_dist(self): point_a = gs.array([0., 1.]) point_b = gs.array([2., 10.]) result = self.metric.dist(point_a, point_b) expected = gs.linalg.norm(point_b - point_a) self.assertAllClose(result, expected) def test_geodesic_and_belongs(self): n_geodesic_points = 100 initial_point = gs.array([[2., -1.]]) initial_tangent_vec = gs.array([2., 0.]) geodesic = self.metric.geodesic( initial_point=initial_point, initial_tangent_vec=initial_tangent_vec) t = gs.linspace(start=0., stop=1., num=n_geodesic_points) points = geodesic(t) result = self.space.belongs(points) expected = gs.array(n_geodesic_points * [True]) self.assertAllClose(expected, result)
class _SpecialEuclideanMatrices(GeneralLinear, LieGroup): """Class for special Euclidean group. Parameters ---------- n : int Integer dimension of the underlying Euclidean space. Matrices will be of size: (n+1) x (n+1). Attributes ---------- rotations : SpecialOrthogonal Subgroup of rotations of size n. translations : Euclidean Subgroup of translations of size n. left_canonical_metric : InvariantMetric The left invariant metric that corresponds to the Frobenius inner product at the identity. right_canonical_metric : InvariantMetric The right invariant metric that corresponds to the Frobenius inner product at the identity. metric : MatricesMetric The Euclidean (Frobenius) inner product. """ def __init__(self, n): super().__init__(n=n + 1, dim=int((n * (n + 1)) / 2), default_point_type='matrix', lie_algebra=SpecialEuclideanMatrixLieAlgebra(n=n)) self.rotations = SpecialOrthogonal(n=n) self.translations = Euclidean(dim=n) self.n = n self.left_canonical_metric = \ SpecialEuclideanMatrixCannonicalLeftMetric(group=self) def get_identity(self): """Return the identity matrix.""" return gs.eye(self.n + 1, self.n + 1) identity = property(get_identity) def belongs(self, point, atol=gs.atol): """Check whether point is of the form rotation, translation. Parameters ---------- point : array-like, shape=[..., n, n]. Point to be checked. atol : float Tolerance threshold. Returns ------- belongs : array-like, shape=[...,] Boolean denoting if point belongs to the group. """ n = self.n belongs = Matrices(n + 1, n + 1).belongs(point) if gs.all(belongs): rotation = point[..., :n, :n] belongs = self.rotations.belongs(rotation, atol=atol) last_line_except_last_term = point[..., n:, :-1] all_but_last_zeros = ~gs.any(last_line_except_last_term, axis=(-2, -1)) belongs = gs.logical_and(belongs, all_but_last_zeros) last_term = point[..., n, n] belongs = gs.logical_and(belongs, gs.isclose(last_term, 1., atol=atol)) return belongs def random_point(self, n_samples=1, bound=1.): """Sample in SE(n) from the uniform distribution. Parameters ---------- n_samples : int Number of samples. Optional, default: 1. bound: float Bound of the interval in which to sample each entry of the translation part. Optional, default: 1. Returns ------- samples : array-like, shape=[..., n + 1, n + 1] Sample in SE(n). """ random_translation = self.translations.random_point(n_samples) random_rotation = self.rotations.random_uniform(n_samples) output_shape = ((n_samples, self.n + 1, self.n + 1) if n_samples != 1 else (self.n + 1, ) * 2) random_point = homogeneous_representation(random_rotation, random_translation, output_shape) return random_point @classmethod def inverse(cls, point): """Return the inverse of a point. Parameters ---------- point : array-like, shape=[..., n, n] Point to be inverted. """ n = point.shape[-1] - 1 transposed_rot = cls.transpose(point[..., :n, :n]) translation = point[..., :n, -1] translation = gs.einsum('...ij,...j->...i', transposed_rot, translation) return homogeneous_representation(transposed_rot, -translation, point.shape)
class TestGeodesicRegression(geomstats.tests.TestCase): _multiprocess_can_split_ = True def setup_method(self): gs.random.seed(1234) self.n_samples = 20 # Set up for euclidean self.dim_eucl = 3 self.shape_eucl = (self.dim_eucl, ) self.eucl = Euclidean(dim=self.dim_eucl) X = gs.random.rand(self.n_samples) self.X_eucl = X - gs.mean(X) self.intercept_eucl_true = self.eucl.random_point() self.coef_eucl_true = self.eucl.random_point() self.y_eucl = (self.intercept_eucl_true + self.X_eucl[:, None] * self.coef_eucl_true) self.param_eucl_true = gs.vstack( [self.intercept_eucl_true, self.coef_eucl_true]) self.param_eucl_guess = gs.vstack([ self.y_eucl[0], self.y_eucl[0] + gs.random.normal(size=self.shape_eucl) ]) # Set up for hypersphere self.dim_sphere = 4 self.shape_sphere = (self.dim_sphere + 1, ) self.sphere = Hypersphere(dim=self.dim_sphere) X = gs.random.rand(self.n_samples) self.X_sphere = X - gs.mean(X) self.intercept_sphere_true = self.sphere.random_point() self.coef_sphere_true = self.sphere.projection( gs.random.rand(self.dim_sphere + 1)) self.y_sphere = self.sphere.metric.exp( self.X_sphere[:, None] * self.coef_sphere_true, base_point=self.intercept_sphere_true, ) self.param_sphere_true = gs.vstack( [self.intercept_sphere_true, self.coef_sphere_true]) self.param_sphere_guess = gs.vstack([ self.y_sphere[0], self.sphere.to_tangent(gs.random.normal(size=self.shape_sphere), self.y_sphere[0]), ]) # Set up for special euclidean self.se2 = SpecialEuclidean(n=2) self.metric_se2 = self.se2.left_canonical_metric self.metric_se2.default_point_type = "matrix" self.shape_se2 = (3, 3) X = gs.random.rand(self.n_samples) self.X_se2 = X - gs.mean(X) self.intercept_se2_true = self.se2.random_point() self.coef_se2_true = self.se2.to_tangent( 5.0 * gs.random.rand(*self.shape_se2), self.intercept_se2_true) self.y_se2 = self.metric_se2.exp( self.X_se2[:, None, None] * self.coef_se2_true[None], self.intercept_se2_true, ) self.param_se2_true = gs.vstack([ gs.flatten(self.intercept_se2_true), gs.flatten(self.coef_se2_true), ]) self.param_se2_guess = gs.vstack([ gs.flatten(self.y_se2[0]), gs.flatten( self.se2.to_tangent(gs.random.normal(size=self.shape_se2), self.y_se2[0])), ]) # Set up for discrete curves n_sampling_points = 8 self.curves_2d = DiscreteCurves(R2) self.metric_curves_2d = self.curves_2d.srv_metric self.metric_curves_2d.default_point_type = "matrix" self.shape_curves_2d = (n_sampling_points, 2) X = gs.random.rand(self.n_samples) self.X_curves_2d = X - gs.mean(X) self.intercept_curves_2d_true = self.curves_2d.random_point( n_sampling_points=n_sampling_points) self.coef_curves_2d_true = self.curves_2d.to_tangent( 5.0 * gs.random.rand(*self.shape_curves_2d), self.intercept_curves_2d_true) # Added because of GitHub issue #1575 intercept_curves_2d_true_repeated = gs.tile( gs.expand_dims(self.intercept_curves_2d_true, axis=0), (self.n_samples, 1, 1), ) self.y_curves_2d = self.metric_curves_2d.exp( self.X_curves_2d[:, None, None] * self.coef_curves_2d_true[None], intercept_curves_2d_true_repeated, ) self.param_curves_2d_true = gs.vstack([ gs.flatten(self.intercept_curves_2d_true), gs.flatten(self.coef_curves_2d_true), ]) self.param_curves_2d_guess = gs.vstack([ gs.flatten(self.y_curves_2d[0]), gs.flatten( self.curves_2d.to_tangent( gs.random.normal(size=self.shape_curves_2d), self.y_curves_2d[0])), ]) def test_loss_euclidean(self): """Test that the loss is 0 at the true parameters.""" gr = GeodesicRegression( self.eucl, metric=self.eucl.metric, center_X=False, method="extrinsic", max_iter=50, init_step_size=0.1, verbose=True, ) loss = gr._loss( self.X_eucl, self.y_eucl, self.param_eucl_true, self.shape_eucl, ) self.assertAllClose(loss.shape, ()) self.assertTrue(gs.isclose(loss, 0.0)) def test_loss_hypersphere(self): """Test that the loss is 0 at the true parameters.""" gr = GeodesicRegression( self.sphere, metric=self.sphere.metric, center_X=False, method="extrinsic", max_iter=50, init_step_size=0.1, verbose=True, ) loss = gr._loss( self.X_sphere, self.y_sphere, self.param_sphere_true, self.shape_sphere, ) self.assertAllClose(loss.shape, ()) self.assertTrue(gs.isclose(loss, 0.0)) @geomstats.tests.autograd_and_tf_only def test_loss_se2(self): """Test that the loss is 0 at the true parameters.""" gr = GeodesicRegression( self.se2, metric=self.metric_se2, center_X=False, method="extrinsic", max_iter=50, init_step_size=0.1, verbose=True, ) loss = gr._loss(self.X_se2, self.y_se2, self.param_se2_true, self.shape_se2) self.assertAllClose(loss.shape, ()) self.assertTrue(gs.isclose(loss, 0.0)) @geomstats.tests.autograd_only def test_loss_curves_2d(self): """Test that the loss is 0 at the true parameters.""" gr = GeodesicRegression( self.curves_2d, metric=self.metric_curves_2d, center_X=False, method="extrinsic", max_iter=50, init_step_size=0.1, verbose=True, ) loss = gr._loss( self.X_curves_2d, self.y_curves_2d, self.param_curves_2d_true, self.shape_curves_2d, ) self.assertAllClose(loss.shape, ()) self.assertTrue(gs.isclose(loss, 0.0)) @geomstats.tests.autograd_tf_and_torch_only def test_value_and_grad_loss_euclidean(self): gr = GeodesicRegression( self.eucl, metric=self.eucl.metric, center_X=False, method="extrinsic", max_iter=50, init_step_size=0.1, verbose=True, regularization=0, ) def loss_of_param(param): return gr._loss(self.X_eucl, self.y_eucl, param, self.shape_eucl) # Without numpy conversion objective_with_grad = gs.autodiff.value_and_grad(loss_of_param) loss_value, loss_grad = objective_with_grad(self.param_eucl_guess) expected_grad_shape = (2, self.dim_eucl) self.assertAllClose(loss_value.shape, ()) self.assertAllClose(loss_grad.shape, expected_grad_shape) self.assertFalse(gs.isclose(loss_value, 0.0)) self.assertFalse(gs.isnan(loss_value)) self.assertFalse( gs.all(gs.isclose(loss_grad, gs.zeros(expected_grad_shape)))) self.assertTrue(gs.all(~gs.isnan(loss_grad))) # With numpy conversion objective_with_grad = gs.autodiff.value_and_grad(loss_of_param, to_numpy=True) loss_value, loss_grad = objective_with_grad(self.param_eucl_guess) # Convert back to arrays/tensors loss_value = gs.array(loss_value) loss_grad = gs.array(loss_grad) expected_grad_shape = (2, self.dim_eucl) self.assertAllClose(loss_value.shape, ()) self.assertAllClose(loss_grad.shape, expected_grad_shape) self.assertFalse(gs.isclose(loss_value, 0.0)) self.assertFalse(gs.isnan(loss_value)) self.assertFalse( gs.all(gs.isclose(loss_grad, gs.zeros(expected_grad_shape)))) self.assertTrue(gs.all(~gs.isnan(loss_grad))) @geomstats.tests.autograd_tf_and_torch_only def test_value_and_grad_loss_hypersphere(self): gr = GeodesicRegression( self.sphere, metric=self.sphere.metric, center_X=False, method="extrinsic", max_iter=50, init_step_size=0.1, verbose=True, regularization=0, ) def loss_of_param(param): return gr._loss(self.X_sphere, self.y_sphere, param, self.shape_sphere) # Without numpy conversion objective_with_grad = gs.autodiff.value_and_grad(loss_of_param) loss_value, loss_grad = objective_with_grad(self.param_sphere_guess) expected_grad_shape = (2, self.dim_sphere + 1) self.assertAllClose(loss_value.shape, ()) self.assertAllClose(loss_grad.shape, expected_grad_shape) self.assertFalse(gs.isclose(loss_value, 0.0)) self.assertFalse(gs.isnan(loss_value)) self.assertFalse( gs.all(gs.isclose(loss_grad, gs.zeros(expected_grad_shape)))) self.assertTrue(gs.all(~gs.isnan(loss_grad))) # With numpy conversion objective_with_grad = gs.autodiff.value_and_grad(loss_of_param, to_numpy=True) loss_value, loss_grad = objective_with_grad(self.param_sphere_guess) # Convert back to arrays/tensors loss_value = gs.array(loss_value) loss_grad = gs.array(loss_grad) expected_grad_shape = (2, self.dim_sphere + 1) self.assertAllClose(loss_value.shape, ()) self.assertAllClose(loss_grad.shape, expected_grad_shape) self.assertFalse(gs.isclose(loss_value, 0.0)) self.assertFalse(gs.isnan(loss_value)) self.assertFalse( gs.all(gs.isclose(loss_grad, gs.zeros(expected_grad_shape)))) self.assertTrue(gs.all(~gs.isnan(loss_grad))) @geomstats.tests.autograd_and_tf_only def test_value_and_grad_loss_se2(self): gr = GeodesicRegression( self.se2, metric=self.metric_se2, center_X=False, method="extrinsic", max_iter=50, init_step_size=0.1, verbose=True, ) def loss_of_param(param): return gr._loss(self.X_se2, self.y_se2, param, self.shape_se2) objective_with_grad = gs.autodiff.value_and_grad(loss_of_param) loss_value, loss_grad = objective_with_grad(self.param_se2_true) expected_grad_shape = ( 2, self.shape_se2[0] * self.shape_se2[1], ) self.assertTrue(gs.isclose(loss_value, 0.0)) loss_value, loss_grad = objective_with_grad(self.param_se2_guess) self.assertAllClose(loss_value.shape, ()) self.assertAllClose(loss_grad.shape, expected_grad_shape) self.assertFalse(gs.isclose(loss_value, 0.0)) self.assertFalse( gs.all(gs.isclose(loss_grad, gs.zeros(expected_grad_shape)))) self.assertTrue(gs.all(~gs.isnan(loss_grad))) objective_with_grad = gs.autodiff.value_and_grad(loss_of_param, to_numpy=True) loss_value, loss_grad = objective_with_grad(self.param_se2_guess) expected_grad_shape = ( 2, self.shape_se2[0] * self.shape_se2[1], ) self.assertAllClose(loss_value.shape, ()) self.assertAllClose(loss_grad.shape, expected_grad_shape) self.assertFalse(gs.isclose(loss_value, 0.0)) self.assertFalse(gs.isnan(loss_value)) self.assertFalse( gs.all(gs.isclose(loss_grad, gs.zeros(expected_grad_shape)))) self.assertTrue(gs.all(~gs.isnan(loss_grad))) @geomstats.tests.autograd_tf_and_torch_only def test_loss_minimization_extrinsic_euclidean(self): """Minimize loss from noiseless data.""" gr = GeodesicRegression(self.eucl, regularization=0) def loss_of_param(param): return gr._loss(self.X_eucl, self.y_eucl, param, self.shape_eucl) objective_with_grad = gs.autodiff.value_and_grad(loss_of_param, to_numpy=True) initial_guess = gs.flatten(self.param_eucl_guess) res = minimize( objective_with_grad, initial_guess, method="CG", jac=True, tol=10 * gs.atol, options={ "disp": True, "maxiter": 50 }, ) self.assertAllClose(gs.array(res.x).shape, (self.dim_eucl * 2, )) self.assertAllClose(res.fun, 0.0, atol=1000 * gs.atol) # Cast required because minimization happens in scipy in float64 param_hat = gs.cast(gs.array(res.x), self.param_eucl_true.dtype) intercept_hat, coef_hat = gs.split(param_hat, 2) coef_hat = self.eucl.to_tangent(coef_hat, intercept_hat) self.assertAllClose(intercept_hat, self.intercept_eucl_true) tangent_vec_of_transport = self.eucl.metric.log( self.intercept_eucl_true, base_point=intercept_hat) transported_coef_hat = self.eucl.metric.parallel_transport( tangent_vec=coef_hat, base_point=intercept_hat, direction=tangent_vec_of_transport, ) self.assertAllClose(transported_coef_hat, self.coef_eucl_true, atol=10 * gs.atol) @geomstats.tests.autograd_tf_and_torch_only def test_loss_minimization_extrinsic_hypersphere(self): """Minimize loss from noiseless data.""" gr = GeodesicRegression(self.sphere, regularization=0) def loss_of_param(param): return gr._loss(self.X_sphere, self.y_sphere, param, self.shape_sphere) objective_with_grad = gs.autodiff.value_and_grad(loss_of_param, to_numpy=True) initial_guess = gs.flatten(self.param_sphere_guess) res = minimize( objective_with_grad, initial_guess, method="CG", jac=True, tol=10 * gs.atol, options={ "disp": True, "maxiter": 50 }, ) self.assertAllClose( gs.array(res.x).shape, ((self.dim_sphere + 1) * 2, )) self.assertAllClose(res.fun, 0.0, atol=5e-3) # Cast required because minimization happens in scipy in float64 param_hat = gs.cast(gs.array(res.x), self.param_sphere_true.dtype) intercept_hat, coef_hat = gs.split(param_hat, 2) intercept_hat = self.sphere.projection(intercept_hat) coef_hat = self.sphere.to_tangent(coef_hat, intercept_hat) self.assertAllClose(intercept_hat, self.intercept_sphere_true, atol=5e-2) tangent_vec_of_transport = self.sphere.metric.log( self.intercept_sphere_true, base_point=intercept_hat) transported_coef_hat = self.sphere.metric.parallel_transport( tangent_vec=coef_hat, base_point=intercept_hat, direction=tangent_vec_of_transport, ) self.assertAllClose(transported_coef_hat, self.coef_sphere_true, atol=0.6) @geomstats.tests.autograd_and_tf_only def test_loss_minimization_extrinsic_se2(self): gr = GeodesicRegression( self.se2, metric=self.metric_se2, center_X=False, method="extrinsic", max_iter=50, init_step_size=0.1, verbose=True, ) def loss_of_param(param): return gr._loss(self.X_se2, self.y_se2, param, self.shape_se2) objective_with_grad = gs.autodiff.value_and_grad(loss_of_param, to_numpy=True) res = minimize( objective_with_grad, gs.flatten(self.param_se2_guess), method="CG", jac=True, options={ "disp": True, "maxiter": 50 }, ) self.assertAllClose(gs.array(res.x).shape, (18, )) self.assertAllClose(res.fun, 0.0, atol=1e-6) # Cast required because minimization happens in scipy in float64 param_hat = gs.cast(gs.array(res.x), self.param_se2_true.dtype) intercept_hat, coef_hat = gs.split(param_hat, 2) intercept_hat = gs.reshape(intercept_hat, self.shape_se2) coef_hat = gs.reshape(coef_hat, self.shape_se2) intercept_hat = self.se2.projection(intercept_hat) coef_hat = self.se2.to_tangent(coef_hat, intercept_hat) self.assertAllClose(intercept_hat, self.intercept_se2_true, atol=1e-4) tangent_vec_of_transport = self.se2.metric.log( self.intercept_se2_true, base_point=intercept_hat) transported_coef_hat = self.se2.metric.parallel_transport( tangent_vec=coef_hat, base_point=intercept_hat, direction=tangent_vec_of_transport, ) self.assertAllClose(transported_coef_hat, self.coef_se2_true, atol=0.6) @geomstats.tests.autograd_tf_and_torch_only def test_fit_extrinsic_euclidean(self): gr = GeodesicRegression( self.eucl, metric=self.eucl.metric, center_X=False, method="extrinsic", max_iter=50, init_step_size=0.1, verbose=True, initialization="random", regularization=0.9, ) gr.fit(self.X_eucl, self.y_eucl, compute_training_score=True) training_score = gr.training_score_ intercept_hat, coef_hat = gr.intercept_, gr.coef_ self.assertAllClose(intercept_hat.shape, self.shape_eucl) self.assertAllClose(coef_hat.shape, self.shape_eucl) self.assertAllClose(training_score, 1.0, atol=500 * gs.atol) self.assertAllClose(intercept_hat, self.intercept_eucl_true) tangent_vec_of_transport = self.eucl.metric.log( self.intercept_eucl_true, base_point=intercept_hat) transported_coef_hat = self.eucl.metric.parallel_transport( tangent_vec=coef_hat, base_point=intercept_hat, direction=tangent_vec_of_transport, ) self.assertAllClose(transported_coef_hat, self.coef_eucl_true) @geomstats.tests.autograd_tf_and_torch_only def test_fit_extrinsic_hypersphere(self): gr = GeodesicRegression( self.sphere, metric=self.sphere.metric, center_X=False, method="extrinsic", max_iter=50, init_step_size=0.1, verbose=True, initialization="random", regularization=0.9, ) gr.fit(self.X_sphere, self.y_sphere, compute_training_score=True) training_score = gr.training_score_ intercept_hat, coef_hat = gr.intercept_, gr.coef_ self.assertAllClose(intercept_hat.shape, self.shape_sphere) self.assertAllClose(coef_hat.shape, self.shape_sphere) self.assertAllClose(training_score, 1.0, atol=500 * gs.atol) self.assertAllClose(intercept_hat, self.intercept_sphere_true, atol=5e-3) tangent_vec_of_transport = self.sphere.metric.log( self.intercept_sphere_true, base_point=intercept_hat) transported_coef_hat = self.sphere.metric.parallel_transport( tangent_vec=coef_hat, base_point=intercept_hat, direction=tangent_vec_of_transport, ) self.assertAllClose(transported_coef_hat, self.coef_sphere_true, atol=0.6) @geomstats.tests.autograd_and_tf_only def test_fit_extrinsic_se2(self): gr = GeodesicRegression( self.se2, metric=self.metric_se2, center_X=False, method="extrinsic", max_iter=50, init_step_size=0.1, verbose=True, initialization="warm_start", ) gr.fit(self.X_se2, self.y_se2, compute_training_score=True) intercept_hat, coef_hat = gr.intercept_, gr.coef_ training_score = gr.training_score_ self.assertAllClose(intercept_hat.shape, self.shape_se2) self.assertAllClose(coef_hat.shape, self.shape_se2) self.assertTrue(gs.isclose(training_score, 1.0)) self.assertAllClose(intercept_hat, self.intercept_se2_true, atol=1e-4) tangent_vec_of_transport = self.se2.metric.log( self.intercept_se2_true, base_point=intercept_hat) transported_coef_hat = self.se2.metric.parallel_transport( tangent_vec=coef_hat, base_point=intercept_hat, direction=tangent_vec_of_transport, ) self.assertAllClose(transported_coef_hat, self.coef_se2_true, atol=0.6) @geomstats.tests.autograd_tf_and_torch_only def test_fit_riemannian_euclidean(self): gr = GeodesicRegression( self.eucl, metric=self.eucl.metric, center_X=False, method="riemannian", max_iter=50, init_step_size=0.1, verbose=True, ) gr.fit(self.X_eucl, self.y_eucl, compute_training_score=True) intercept_hat, coef_hat = gr.intercept_, gr.coef_ training_score = gr.training_score_ self.assertAllClose(intercept_hat.shape, self.shape_eucl) self.assertAllClose(coef_hat.shape, self.shape_eucl) self.assertAllClose(training_score, 1.0, atol=0.1) self.assertAllClose(intercept_hat, self.intercept_eucl_true) tangent_vec_of_transport = self.eucl.metric.log( self.intercept_eucl_true, base_point=intercept_hat) transported_coef_hat = self.eucl.metric.parallel_transport( tangent_vec=coef_hat, base_point=intercept_hat, direction=tangent_vec_of_transport, ) self.assertAllClose(transported_coef_hat, self.coef_eucl_true, atol=1e-2) @geomstats.tests.autograd_tf_and_torch_only def test_fit_riemannian_hypersphere(self): gr = GeodesicRegression( self.sphere, metric=self.sphere.metric, center_X=False, method="riemannian", max_iter=50, init_step_size=0.1, verbose=True, ) gr.fit(self.X_sphere, self.y_sphere, compute_training_score=True) intercept_hat, coef_hat = gr.intercept_, gr.coef_ training_score = gr.training_score_ self.assertAllClose(intercept_hat.shape, self.shape_sphere) self.assertAllClose(coef_hat.shape, self.shape_sphere) self.assertAllClose(training_score, 1.0, atol=0.1) self.assertAllClose(intercept_hat, self.intercept_sphere_true, atol=1e-2) tangent_vec_of_transport = self.sphere.metric.log( self.intercept_sphere_true, base_point=intercept_hat) transported_coef_hat = self.sphere.metric.parallel_transport( tangent_vec=coef_hat, base_point=intercept_hat, direction=tangent_vec_of_transport, ) self.assertAllClose(transported_coef_hat, self.coef_sphere_true, atol=0.6) @geomstats.tests.autograd_and_tf_only def test_fit_riemannian_se2(self): init = (self.y_se2[0], gs.zeros_like(self.y_se2[0])) gr = GeodesicRegression( self.se2, metric=self.metric_se2, center_X=False, method="riemannian", max_iter=50, init_step_size=0.1, verbose=True, initialization=init, ) gr.fit(self.X_se2, self.y_se2, compute_training_score=True) intercept_hat, coef_hat = gr.intercept_, gr.coef_ training_score = gr.training_score_ self.assertAllClose(intercept_hat.shape, self.shape_se2) self.assertAllClose(coef_hat.shape, self.shape_se2) self.assertAllClose(training_score, 1.0, atol=1e-4) self.assertAllClose(intercept_hat, self.intercept_se2_true, atol=1e-4) tangent_vec_of_transport = self.se2.metric.log( self.intercept_se2_true, base_point=intercept_hat) transported_coef_hat = self.se2.metric.parallel_transport( tangent_vec=coef_hat, base_point=intercept_hat, direction=tangent_vec_of_transport, ) self.assertAllClose(transported_coef_hat, self.coef_se2_true, atol=0.6)
class TestRiemannianMetric(geomstats.tests.TestCase): def setUp(self): warnings.simplefilter("ignore", category=UserWarning) gs.random.seed(0) self.dim = 2 self.euc = Euclidean(dim=self.dim) self.sphere = Hypersphere(dim=self.dim) self.euc_metric = EuclideanMetric(dim=self.dim) self.sphere_metric = HypersphereMetric(dim=self.dim) def _euc_metric_matrix(base_point): """Return matrix of Euclidean inner-product.""" dim = base_point.shape[-1] return gs.eye(dim) def _sphere_metric_matrix(base_point): """Return sphere's metric in spherical coordinates.""" theta = base_point[..., 0] mat = gs.array([[1.0, 0.0], [0.0, gs.sin(theta) ** 2]]) return mat new_euc_metric = RiemannianMetric(dim=self.dim) new_euc_metric.metric_matrix = _euc_metric_matrix new_sphere_metric = RiemannianMetric(dim=self.dim) new_sphere_metric.metric_matrix = _sphere_metric_matrix self.new_euc_metric = new_euc_metric self.new_sphere_metric = new_sphere_metric def test_cometric_matrix(self): base_point = self.euc.random_point() result = self.euc_metric.metric_inverse_matrix(base_point) expected = gs.eye(self.dim) self.assertAllClose(result, expected) @geomstats.tests.autograd_and_torch_only def test_metric_derivative_euc_metric(self): base_point = self.euc.random_point() result = self.euc_metric.inner_product_derivative_matrix(base_point) expected = gs.zeros((self.dim,) * 3) self.assertAllClose(result, expected) @geomstats.tests.autograd_and_torch_only def test_metric_derivative_new_euc_metric(self): base_point = self.euc.random_point() result = self.new_euc_metric.inner_product_derivative_matrix(base_point) expected = gs.zeros((self.dim,) * 3) self.assertAllClose(result, expected) def test_inner_product_new_euc_metric(self): base_point = self.euc.random_point() tan_a = self.euc.random_point() tan_b = self.euc.random_point() expected = gs.dot(tan_a, tan_b) result = self.new_euc_metric.inner_product(tan_a, tan_b, base_point=base_point) self.assertAllClose(result, expected) def test_inner_product_new_sphere_metric(self): base_point = gs.array([gs.pi / 3.0, gs.pi / 5.0]) tan_a = gs.array([0.3, 0.4]) tan_b = gs.array([0.1, -0.5]) expected = -0.12 result = self.new_sphere_metric.inner_product( tan_a, tan_b, base_point=base_point ) self.assertAllClose(result, expected) @geomstats.tests.autograd_and_torch_only def test_christoffels_eucl_metric(self): base_point = self.euc.random_point() result = self.euc_metric.christoffels(base_point) expected = gs.zeros((self.dim,) * 3) self.assertAllClose(result, expected) @geomstats.tests.autograd_and_torch_only def test_christoffels_new_eucl_metric(self): base_point = self.euc.random_point() result = self.new_euc_metric.christoffels(base_point) expected = gs.zeros((self.dim,) * 3) self.assertAllClose(result, expected) @geomstats.tests.autograd_tf_and_torch_only def test_christoffels_sphere_metrics(self): base_point = gs.array([gs.pi / 10.0, gs.pi / 9.0]) expected = self.sphere_metric.christoffels(base_point) result = self.new_sphere_metric.christoffels(base_point) self.assertAllClose(result, expected) @geomstats.tests.autograd_and_torch_only def test_exp_new_eucl_metric(self): base_point = self.euc.random_point() tan = self.euc.random_point() expected = base_point + tan result = self.new_euc_metric.exp(tan, base_point) self.assertAllClose(result, expected) @geomstats.tests.autograd_and_torch_only def test_log_new_eucl_metric(self): base_point = self.euc.random_point() point = self.euc.random_point() expected = point - base_point result = self.new_euc_metric.log(point, base_point) self.assertAllClose(result, expected) @geomstats.tests.autograd_tf_and_torch_only def test_exp_new_sphere_metric(self): base_point = gs.array([gs.pi / 10.0, gs.pi / 9.0]) tan = gs.array([gs.pi / 2.0, 0.0]) expected = gs.array([gs.pi / 10.0 + gs.pi / 2.0, gs.pi / 9.0]) result = self.new_sphere_metric.exp(tan, base_point) self.assertAllClose(result, expected)