def test_inner_product(self, dim, tangent_vec_a, tangent_vec_b, expected): metric = EuclideanMetric(dim) self.assertAllClose( metric.inner_product(gs.array(tangent_vec_a), gs.array(tangent_vec_b)), gs.array(expected), )
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 setUp(self): warnings.simplefilter('ignore', category=UserWarning) self.dim = 4 self.euc_metric = EuclideanMetric(dim=self.dim) self.connection = Connection(dim=2) self.hypersphere = Hypersphere(dim=2)
def setup_method(self): warnings.simplefilter("ignore", category=UserWarning) gs.random.seed(0) self.dim = 4 self.euc_metric = EuclideanMetric(dim=self.dim) self.connection = Connection(dim=2) self.hypersphere = Hypersphere(dim=2)
def __init__(self, n, p): dim = int(p * n - (p * (p + 1) / 2)) super(StiefelCanonicalMetric, self).__init__(dim=dim, signature=(dim, 0, 0)) self.embedding_metric = EuclideanMetric(n * p) self.n = n self.p = p
def metric_matrix_test_data(self): smoke_data = [ dict( metric=EuclideanMetric(dim=4), point=gs.array([0.0, 1.0, 0.0, 0.0]), expected=gs.eye(4), ) ] return self.generate_tests(smoke_data)
def __init__(self, n, p): assert isinstance(n, int) and isinstance(p, int) assert p <= n self.n = n self.p = p dimension = int(p * (n - p)) super(GrassmannianCanonicalMetric, self).__init__(dimension=dimension, signature=(dimension, 0, 0)) self.embedding_metric = EuclideanMetric(n * p)
def __init__( self, dim, embedding_dim, immersion, jacobian_immersion=None, tangent_immersion=None, ): super(PullbackMetric, self).__init__(dim=dim) self.embedding_metric = EuclideanMetric(embedding_dim) self.immersion = immersion if jacobian_immersion is None: jacobian_immersion = gs.autodiff.jacobian(immersion) self.jacobian_immersion = jacobian_immersion if tangent_immersion is None: def _tangent_immersion(v, x): return gs.matmul(jacobian_immersion(x), v) self.tangent_immersion = _tangent_immersion
def __init__(self, n, p): geomstats.errors.check_integer(p, "p") geomstats.errors.check_integer(n, "n") if p > n: raise ValueError("p <= n is required.") dim = int(p * (n - p)) super(GrassmannianCanonicalMetric, self).__init__(m=n, n=n, dim=dim, signature=(dim, 0, 0)) self.n = n self.p = p self.embedding_metric = EuclideanMetric(n * p)
def __init__(self, manifold, mean, cov): n = mean.shape[-1] metric = manifold.metric if metric is None: manifold.metric = EuclideanMetric(n) else: if type(metric) not in (EuclideanMetric, MatricesMetric): raise ValueError( "Invalid Metric, " "Should be of type EuclideanMetric or MatricesMetric") self.manifold = manifold self.mean = mean self.cov = cov
def test_exp(self, dim, tangent_vec, base_point, expected): metric = EuclideanMetric(dim) self.assertAllClose( metric.exp(gs.array(tangent_vec), gs.array(base_point)), gs.array(expected))
class HypersphereMetric(RiemannianMetric): """Class for the Hypersphere Metric. Parameters ---------- dim : int Dimension of the hypersphere. """ def __init__(self, dim): super(HypersphereMetric, self).__init__(dim=dim, signature=(dim, 0, 0)) self.embedding_metric = EuclideanMetric(dim + 1) self._space = _Hypersphere(dim=dim) def inner_product(self, tangent_vec_a, tangent_vec_b, base_point=None): """Compute the inner-product of two tangent vectors at a base point. Parameters ---------- tangent_vec_a : array-like, shape=[..., dim + 1] First tangent vector at base point. tangent_vec_b : array-like, shape=[..., dim + 1] Second tangent vector at base point. base_point : array-like, shape=[..., dim + 1], optional Point on the hypersphere. Returns ------- inner_prod : array-like, shape=[..., 1] Inner-product of the two tangent vectors. """ inner_prod = self.embedding_metric.inner_product( tangent_vec_a, tangent_vec_b, base_point) return inner_prod def squared_norm(self, vector, base_point=None): """Compute the squared norm of a vector. Squared norm of a vector associated with the inner-product at the tangent space at a base point. Parameters ---------- vector : array-like, shape=[..., dim + 1] Vector on the tangent space of the hypersphere at base point. base_point : array-like, shape=[..., dim + 1], optional Point on the hypersphere. Returns ------- sq_norm : array-like, shape=[..., 1] Squared norm of the vector. """ sq_norm = self.embedding_metric.squared_norm(vector) return sq_norm @geomstats.vectorization.decorator(['else', 'vector', 'vector']) def exp(self, tangent_vec, base_point): """Compute the Riemannian exponential of a tangent vector. Parameters ---------- tangent_vec : array-like, shape=[..., dim + 1] Tangent vector at a base point. base_point : array-like, shape=[..., dim + 1] Point on the hypersphere. Returns ------- exp : array-like, shape=[..., dim + 1] Point on the hypersphere equal to the Riemannian exponential of tangent_vec at the base point. """ # TODO (ninamiolane): Raise error when vector is not tangent _, extrinsic_dim = base_point.shape n_tangent_vecs, _ = tangent_vec.shape hypersphere = Hypersphere(dim=extrinsic_dim - 1) proj_tangent_vec = hypersphere.to_tangent(tangent_vec, base_point) norm_tangent_vec = self.embedding_metric.norm(proj_tangent_vec) norm_tangent_vec = gs.to_ndarray(norm_tangent_vec, to_ndim=1) mask_0 = gs.isclose(norm_tangent_vec, 0.) mask_non0 = ~mask_0 coef_1 = gs.zeros((n_tangent_vecs, )) coef_2 = gs.zeros((n_tangent_vecs, )) norm2 = norm_tangent_vec[mask_0]**2 norm4 = norm2**2 norm6 = norm2**3 coef_1 = gs.assignment(coef_1, 1. - norm2 / 2. + norm4 / 24. - norm6 / 720., mask_0) coef_2 = gs.assignment(coef_2, 1. - norm2 / 6. + norm4 / 120. - norm6 / 5040., mask_0) coef_1 = gs.assignment(coef_1, gs.cos(norm_tangent_vec[mask_non0]), mask_non0) coef_2 = gs.assignment( coef_2, gs.sin(norm_tangent_vec[mask_non0]) / norm_tangent_vec[mask_non0], mask_non0) exp = (gs.einsum('...,...j->...j', coef_1, base_point) + gs.einsum('...,...j->...j', coef_2, proj_tangent_vec)) return exp @geomstats.vectorization.decorator(['else', 'vector', 'vector']) def log(self, point, base_point): """Compute the Riemannian logarithm of a point. Parameters ---------- point : array-like, shape=[..., dim + 1] Point on the hypersphere. base_point : array-like, shape=[..., dim + 1] Point on the hypersphere. Returns ------- log : array-like, shape=[..., dim + 1] Tangent vector at the base point equal to the Riemannian logarithm of point at the base point. """ norm_base_point = self.embedding_metric.norm(base_point) norm_point = self.embedding_metric.norm(point) inner_prod = self.embedding_metric.inner_product(base_point, point) cos_angle = inner_prod / (norm_base_point * norm_point) cos_angle = gs.clip(cos_angle, -1., 1.) angle = gs.arccos(cos_angle) angle = gs.to_ndarray(angle, to_ndim=1) angle = gs.to_ndarray(angle, to_ndim=2, axis=1) mask_0 = gs.isclose(angle, 0.) mask_else = gs.equal(mask_0, gs.array(False)) mask_0_float = gs.cast(mask_0, gs.float32) mask_else_float = gs.cast(mask_else, gs.float32) coef_1 = gs.zeros_like(angle) coef_2 = gs.zeros_like(angle) coef_1 += mask_0_float * (1. + INV_SIN_TAYLOR_COEFFS[1] * angle**2 + INV_SIN_TAYLOR_COEFFS[3] * angle**4 + INV_SIN_TAYLOR_COEFFS[5] * angle**6 + INV_SIN_TAYLOR_COEFFS[7] * angle**8) coef_2 += mask_0_float * (1. + INV_TAN_TAYLOR_COEFFS[1] * angle**2 + INV_TAN_TAYLOR_COEFFS[3] * angle**4 + INV_TAN_TAYLOR_COEFFS[5] * angle**6 + INV_TAN_TAYLOR_COEFFS[7] * angle**8) # This avoids division by 0. angle += mask_0_float * 1. coef_1 += mask_else_float * angle / gs.sin(angle) coef_2 += mask_else_float * angle / gs.tan(angle) log = (gs.einsum('...i,...j->...j', coef_1, point) - gs.einsum('...i,...j->...j', coef_2, base_point)) mask_same_values = gs.isclose(point, base_point) mask_else = gs.equal(mask_same_values, gs.array(False)) mask_else_float = gs.cast(mask_else, gs.float32) mask_else_float = gs.to_ndarray(mask_else_float, to_ndim=1) mask_else_float = gs.to_ndarray(mask_else_float, to_ndim=2) mask_not_same_points = gs.sum(mask_else_float, axis=1) mask_same_points = gs.isclose(mask_not_same_points, 0.) mask_same_points = gs.cast(mask_same_points, gs.float32) mask_same_points = gs.to_ndarray(mask_same_points, to_ndim=2, axis=1) mask_same_points_float = gs.cast(mask_same_points, gs.float32) log -= mask_same_points_float * log return log def dist(self, point_a, point_b): """Compute the geodesic distance between two points. Parameters ---------- point_a : array-like, shape=[..., dim + 1] First point on the hypersphere. point_b : array-like, shape=[..., dim + 1] Second point on the hypersphere. Returns ------- dist : array-like, shape=[..., 1] Geodesic distance between the two points. """ norm_a = self.embedding_metric.norm(point_a) norm_b = self.embedding_metric.norm(point_b) inner_prod = self.embedding_metric.inner_product(point_a, point_b) cos_angle = gs.einsum('...,...->...', inner_prod, 1. / (norm_a * norm_b)) cos_angle = gs.clip(cos_angle, -1, 1) dist = gs.arccos(cos_angle) return dist def squared_dist(self, point_a, point_b): """Squared geodesic distance between two points. Parameters ---------- point_a : array-like, shape=[..., dim] Point on the hypersphere. point_b : array-like, shape=[..., dim] Point on the hypersphere. Returns ------- sq_dist : array-like, shape=[...,] """ return self.dist(point_a, point_b)**2 @staticmethod def parallel_transport(tangent_vec_a, tangent_vec_b, base_point): """Compute the parallel transport of a tangent vector. Closed-form solution for the parallel transport of a tangent vector a along the geodesic defined by exp_(base_point)(tangent_vec_b). Parameters ---------- tangent_vec_a : array-like, shape=[..., dim + 1] Tangent vector at base point to be transported. tangent_vec_b : array-like, shape=[..., dim + 1] Tangent vector at base point, along which the parallel transport is computed. base_point : array-like, shape=[..., dim + 1] Point on the hypersphere. Returns ------- transported_tangent_vec: array-like, shape=[..., dim + 1] Transported tangent vector at exp_(base_point)(tangent_vec_b). """ tangent_vec_a = gs.to_ndarray(tangent_vec_a, to_ndim=2) tangent_vec_b = gs.to_ndarray(tangent_vec_b, to_ndim=2) base_point = gs.to_ndarray(base_point, to_ndim=2) # TODO (nguigs): work around this condition: # assert len(base_point) == len(tangent_vec_a) == len(tangent_vec_b) theta = gs.linalg.norm(tangent_vec_b, axis=1) normalized_b = gs.einsum('n, ni->ni', 1 / theta, tangent_vec_b) pb = gs.einsum('ni,ni->n', tangent_vec_a, normalized_b) p_orth = tangent_vec_a - gs.einsum('n,ni->ni', pb, normalized_b) transported = - gs.einsum('n,ni->ni', gs.sin(theta) * pb, base_point)\ + gs.einsum('n,ni->ni', gs.cos(theta) * pb, normalized_b)\ + p_orth return transported def christoffels(self, point, point_type='spherical'): """Compute the Christoffel symbols at a point. Only implemented in dimension 2 and for spherical coordinates. Parameters ---------- point : array-like, shape=[..., dim] Point on hypersphere where the Christoffel symbols are computed. point_type: str, {'spherical', 'intrinsic', 'extrinsic'} Coordinates in which to express the Christoffel symbols. Returns ------- christoffel : array-like, shape=[..., contravariant index, 1st covariant index, 2nd covariant index] Christoffel symbols at point. """ if self.dim != 2 or point_type != 'spherical': raise NotImplementedError( 'The Christoffel symbols are only implemented' ' for spherical coordinates in the 2-sphere') point = gs.to_ndarray(point, to_ndim=2) christoffel = [] for sample in point: gamma_0 = gs.array([[0, 0], [0, -gs.sin(sample[0]) * gs.cos(sample[0])]]) gamma_1 = gs.array([[0, gs.cos(sample[0]) / gs.sin(sample[0])], [gs.cos(sample[0]) / gs.sin(sample[0]), 0]]) christoffel.append(gs.stack([gamma_0, gamma_1])) christoffel = gs.stack(christoffel) if gs.ndim(christoffel) == 4 and gs.shape(christoffel)[0] == 1: christoffel = gs.squeeze(christoffel, axis=0) return christoffel
def test_log(self, dim, point, base_point, expected): metric = EuclideanMetric(dim) self.assertAllClose(metric.log(gs.array(point), gs.array(base_point)), gs.array(expected))
class HypersphereMetric(RiemannianMetric): """Class for the Hypersphere Metric. Parameters ---------- dim : int Dimension of the hypersphere. """ def __init__(self, dim): super(HypersphereMetric, self).__init__(dim=dim, signature=(dim, 0)) self.embedding_metric = EuclideanMetric(dim + 1) self._space = _Hypersphere(dim=dim) def metric_matrix(self, base_point=None): """Metric matrix at the tangent space at a base point. Parameters ---------- base_point : array-like, shape=[..., dim + 1] Base point. Optional, default: None. Returns ------- mat : array-like, shape=[..., dim + 1, dim + 1] Inner-product matrix. """ return gs.eye(self.dim + 1) def inner_product(self, tangent_vec_a, tangent_vec_b, base_point=None): """Compute the inner-product of two tangent vectors at a base point. Parameters ---------- tangent_vec_a : array-like, shape=[..., dim + 1] First tangent vector at base point. tangent_vec_b : array-like, shape=[..., dim + 1] Second tangent vector at base point. base_point : array-like, shape=[..., dim + 1], optional Point on the hypersphere. Returns ------- inner_prod : array-like, shape=[...,] Inner-product of the two tangent vectors. """ inner_prod = self.embedding_metric.inner_product( tangent_vec_a, tangent_vec_b, base_point) return inner_prod def squared_norm(self, vector, base_point=None): """Compute the squared norm of a vector. Squared norm of a vector associated with the inner-product at the tangent space at a base point. Parameters ---------- vector : array-like, shape=[..., dim + 1] Vector on the tangent space of the hypersphere at base point. base_point : array-like, shape=[..., dim + 1], optional Point on the hypersphere. Returns ------- sq_norm : array-like, shape=[..., 1] Squared norm of the vector. """ sq_norm = self.embedding_metric.squared_norm(vector) return sq_norm def exp(self, tangent_vec, base_point, **kwargs): """Compute the Riemannian exponential of a tangent vector. Parameters ---------- tangent_vec : array-like, shape=[..., dim + 1] Tangent vector at a base point. base_point : array-like, shape=[..., dim + 1] Point on the hypersphere. Returns ------- exp : array-like, shape=[..., dim + 1] Point on the hypersphere equal to the Riemannian exponential of tangent_vec at the base point. """ hypersphere = Hypersphere(dim=self.dim) proj_tangent_vec = hypersphere.to_tangent(tangent_vec, base_point) norm2 = self.embedding_metric.squared_norm(proj_tangent_vec) coef_1 = utils.taylor_exp_even_func(norm2, utils.cos_close_0, order=4) coef_2 = utils.taylor_exp_even_func(norm2, utils.sinc_close_0, order=4) exp = gs.einsum("...,...j->...j", coef_1, base_point) + gs.einsum( "...,...j->...j", coef_2, proj_tangent_vec) return exp def log(self, point, base_point, **kwargs): """Compute the Riemannian logarithm of a point. Parameters ---------- point : array-like, shape=[..., dim + 1] Point on the hypersphere. base_point : array-like, shape=[..., dim + 1] Point on the hypersphere. Returns ------- log : array-like, shape=[..., dim + 1] Tangent vector at the base point equal to the Riemannian logarithm of point at the base point. """ inner_prod = self.embedding_metric.inner_product(base_point, point) cos_angle = gs.clip(inner_prod, -1.0, 1.0) squared_angle = gs.arccos(cos_angle)**2 coef_1_ = utils.taylor_exp_even_func(squared_angle, utils.inv_sinc_close_0, order=5) coef_2_ = utils.taylor_exp_even_func(squared_angle, utils.inv_tanc_close_0, order=5) log = gs.einsum("...,...j->...j", coef_1_, point) - gs.einsum( "...,...j->...j", coef_2_, base_point) return log def dist(self, point_a, point_b): """Compute the geodesic distance between two points. Parameters ---------- point_a : array-like, shape=[..., dim + 1] First point on the hypersphere. point_b : array-like, shape=[..., dim + 1] Second point on the hypersphere. Returns ------- dist : array-like, shape=[..., 1] Geodesic distance between the two points. """ norm_a = self.embedding_metric.norm(point_a) norm_b = self.embedding_metric.norm(point_b) inner_prod = self.embedding_metric.inner_product(point_a, point_b) cos_angle = inner_prod / (norm_a * norm_b) cos_angle = gs.clip(cos_angle, -1, 1) dist = gs.arccos(cos_angle) return dist def squared_dist(self, point_a, point_b, **kwargs): """Squared geodesic distance between two points. Parameters ---------- point_a : array-like, shape=[..., dim] Point on the hypersphere. point_b : array-like, shape=[..., dim] Point on the hypersphere. Returns ------- sq_dist : array-like, shape=[...,] """ return self.dist(point_a, point_b)**2 @staticmethod def parallel_transport(tangent_vec_a, tangent_vec_b, base_point, **kwargs): r"""Compute the parallel transport of a tangent vector. Closed-form solution for the parallel transport of a tangent vector a along the geodesic defined by :math:`t \mapsto exp_(base_point)(t* tangent_vec_b)`. Parameters ---------- tangent_vec_a : array-like, shape=[..., dim + 1] Tangent vector at base point to be transported. tangent_vec_b : array-like, shape=[..., dim + 1] Tangent vector at base point, along which the parallel transport is computed. base_point : array-like, shape=[..., dim + 1] Point on the hypersphere. Returns ------- transported_tangent_vec: array-like, shape=[..., dim + 1] Transported tangent vector at `exp_(base_point)(tangent_vec_b)`. """ theta = gs.linalg.norm(tangent_vec_b, axis=-1) eps = gs.where(theta == 0.0, 1.0, theta) normalized_b = gs.einsum("...,...i->...i", 1 / eps, tangent_vec_b) pb = gs.einsum("...i,...i->...", tangent_vec_a, normalized_b) p_orth = tangent_vec_a - gs.einsum("...,...i->...i", pb, normalized_b) transported = (-gs.einsum("...,...i->...i", gs.sin(theta) * pb, base_point) + gs.einsum("...,...i->...i", gs.cos(theta) * pb, normalized_b) + p_orth) return transported def christoffels(self, point, point_type="spherical"): """Compute the Christoffel symbols at a point. Only implemented in dimension 2 and for spherical coordinates. Parameters ---------- point : array-like, shape=[..., dim] Point on hypersphere where the Christoffel symbols are computed. point_type: str, {'spherical', 'intrinsic', 'extrinsic'} Coordinates in which to express the Christoffel symbols. Optional, default: 'spherical'. Returns ------- christoffel : array-like, shape=[..., contravariant index, 1st covariant index, 2nd covariant index] Christoffel symbols at point. """ if self.dim != 2 or point_type != "spherical": raise NotImplementedError( "The Christoffel symbols are only implemented" " for spherical coordinates in the 2-sphere") point = gs.to_ndarray(point, to_ndim=2) christoffel = [] for sample in point: gamma_0 = gs.array([[0, 0], [0, -gs.sin(sample[0]) * gs.cos(sample[0])]]) gamma_1 = gs.array([ [0, gs.cos(sample[0]) / gs.sin(sample[0])], [gs.cos(sample[0]) / gs.sin(sample[0]), 0], ]) christoffel.append(gs.stack([gamma_0, gamma_1])) christoffel = gs.stack(christoffel) if gs.ndim(christoffel) == 4 and gs.shape(christoffel)[0] == 1: christoffel = gs.squeeze(christoffel, axis=0) return christoffel def curvature(self, tangent_vec_a, tangent_vec_b, tangent_vec_c, base_point): r"""Compute the curvature. For three tangent vectors at a base point :math:`x,y,z`, the curvature is defined by :math:`R(x, y)z = \nabla_{[x,y]}z - \nabla_z\nabla_y z + \nabla_y\nabla_x z`, where :math:`\nabla` is the Levi-Civita connection. In the case of the hypersphere, we have the closed formula :math:`R(x,y)z = \langle x, z \rangle y - \langle y,z \rangle x`. Parameters ---------- tangent_vec_a : array-like, shape=[..., dim] Tangent vector at `base_point`. tangent_vec_b : array-like, shape=[..., dim] Tangent vector at `base_point`. tangent_vec_c : array-like, shape=[..., dim] Tangent vector at `base_point`. base_point : array-like, shape=[..., dim] Point on the hypersphere. Returns ------- curvature : array-like, shape=[..., dim] Tangent vector at `base_point`. """ inner_ac = self.inner_product(tangent_vec_a, tangent_vec_c) inner_bc = self.inner_product(tangent_vec_b, tangent_vec_c) first_term = gs.einsum("...,...i->...i", inner_bc, tangent_vec_a) second_term = gs.einsum("...,...i->...i", inner_ac, tangent_vec_b) return -first_term + second_term def _normalization_factor_odd_dim(self, variances): """Compute the normalization factor - odd dimension.""" dim = self.dim half_dim = int((dim + 1) / 2) area = 2 * gs.pi**half_dim / math.factorial(half_dim - 1) comb = gs.comb(dim - 1, half_dim - 1) erf_arg = gs.sqrt(variances / 2) * gs.pi first_term = (area / (2**dim - 1) * comb * gs.sqrt(gs.pi / (2 * variances)) * gs.erf(erf_arg)) def summand(k): exp_arg = -((dim - 1 - 2 * k)**2) / 2 / variances erf_arg_2 = (gs.pi * variances - (dim - 1 - 2 * k) * 1j) / gs.sqrt(2 * variances) sign = (-1.0)**k comb_2 = gs.comb(k, dim - 1) return sign * comb_2 * gs.exp(exp_arg) * gs.real(gs.erf(erf_arg_2)) if half_dim > 2: sum_term = gs.sum( gs.stack([summand(k)] for k in range(half_dim - 2))) else: sum_term = summand(0) coef = area / 2 / erf_arg * gs.pi**0.5 * (-1.0)**(half_dim - 1) return first_term + coef / 2**(dim - 2) * sum_term def _normalization_factor_even_dim(self, variances): """Compute the normalization factor - even dimension.""" dim = self.dim half_dim = (dim + 1) / 2 area = 2 * gs.pi**half_dim / math.gamma(half_dim) def summand(k): exp_arg = -((dim - 1 - 2 * k)**2) / 2 / variances erf_arg_1 = (dim - 1 - 2 * k) * 1j / gs.sqrt(2 * variances) erf_arg_2 = (gs.pi * variances - (dim - 1 - 2 * k) * 1j) / gs.sqrt(2 * variances) sign = (-1.0)**k comb = gs.comb(dim - 1, k) erf_terms = gs.imag(gs.erf(erf_arg_2) + gs.erf(erf_arg_1)) return sign * comb * gs.exp(exp_arg) * erf_terms half_dim_2 = int((dim - 2) / 2) if half_dim_2 > 0: sum_term = gs.sum(gs.stack([summand(k)] for k in range(half_dim_2))) else: sum_term = summand(0) coef = (area * (-1.0)**half_dim_2 / 2**(dim - 2) * gs.sqrt(gs.pi / 2 / variances)) return coef * sum_term def normalization_factor(self, variances): """Return normalization factor of the Gaussian distribution. Parameters ---------- variances : array-like, shape=[n,] Variance of the distribution. Returns ------- norm_func : array-like, shape=[n,] Normalisation factor for all given variances. """ if self.dim % 2 == 0: return self._normalization_factor_even_dim(variances) return self._normalization_factor_odd_dim(variances) def norm_factor_gradient(self, variances): """Compute the gradient of the normalization factor. Parameters ---------- variances : array-like, shape=[n,] Variance of the distribution. Returns ------- norm_func : array-like, shape=[n,] Normalisation factor for all given variances. """ def func(var): return gs.sum(self.normalization_factor(var)) _, grad = gs.autodiff.value_and_grad(func)(variances) return _, grad def curvature_derivative( self, tangent_vec_a, tangent_vec_b=None, tangent_vec_c=None, tangent_vec_d=None, base_point=None, ): r"""Compute the covariant derivative of the curvature. The derivative of the curvature vanishes since the hypersphere is a constant curvature space. Parameters ---------- tangent_vec_a : array-like, shape=[..., dim] Tangent vector at `base_point` along which the curvature is derived. tangent_vec_b : array-like, shape=[..., dim] Unused tangent vector at `base_point` (since curvature derivative vanishes). tangent_vec_c : array-like, shape=[..., dim] Unused tangent vector at `base_point` (since curvature derivative vanishes). tangent_vec_d : array-like, shape=[..., dim] Unused tangent vector at `base_point` (since curvature derivative vanishes). base_point : array-like, shape=[..., dim] Unused point on the hypersphere. Returns ------- curvature_derivative : array-like, shape=[..., dim] Tangent vector at base point. """ return gs.zeros_like(tangent_vec_a)
def test_norm(self, dim, vec, expected): metric = EuclideanMetric(dim) self.assertAllClose(metric.norm(gs.array(vec)), gs.array(expected))
class TestConnection(geomstats.tests.TestCase): def setup_method(self): warnings.simplefilter("ignore", category=UserWarning) gs.random.seed(0) self.dim = 4 self.euc_metric = EuclideanMetric(dim=self.dim) self.connection = Connection(dim=2) self.hypersphere = Hypersphere(dim=2) def test_metric_matrix(self): base_point = gs.array([0.0, 1.0, 0.0, 0.0]) result = self.euc_metric.metric_matrix(base_point) expected = gs.eye(self.dim) self.assertAllClose(result, expected) def test_parallel_transport(self): n_samples = 2 base_point = self.hypersphere.random_uniform(n_samples) tan_vec_a = self.hypersphere.to_tangent(gs.random.rand(n_samples, 3), base_point) tan_vec_b = self.hypersphere.to_tangent(gs.random.rand(n_samples, 3), base_point) expected = self.hypersphere.metric.parallel_transport( tan_vec_a, base_point, tan_vec_b) expected_point = self.hypersphere.metric.exp(tan_vec_b, base_point) base_point = gs.cast(base_point, gs.float64) base_point, tan_vec_a, tan_vec_b = gs.convert_to_wider_dtype( [base_point, tan_vec_a, tan_vec_b]) for step, alpha in zip(["pole", "schild"], [1, 2]): min_n = 1 if step == "pole" else 50 tol = 1e-5 if step == "pole" else 1e-2 for n_rungs in [min_n, 11]: ladder = self.hypersphere.metric.ladder_parallel_transport( tan_vec_a, base_point, tan_vec_b, n_rungs=n_rungs, scheme=step, alpha=alpha, ) result = ladder["transported_tangent_vec"] result_point = ladder["end_point"] self.assertAllClose(result, expected, rtol=tol, atol=tol) self.assertAllClose(result_point, expected_point) def test_parallel_transport_trajectory(self): n_samples = 2 for step in ["pole", "schild"]: n_steps = 1 if step == "pole" else 50 tol = 1e-6 if step == "pole" else 1e-2 base_point = self.hypersphere.random_uniform(n_samples) tan_vec_a = self.hypersphere.to_tangent( gs.random.rand(n_samples, 3), base_point) tan_vec_b = self.hypersphere.to_tangent( gs.random.rand(n_samples, 3), base_point) expected = self.hypersphere.metric.parallel_transport( tan_vec_a, base_point, tan_vec_b) expected_point = self.hypersphere.metric.exp(tan_vec_b, base_point) ladder = self.hypersphere.metric.ladder_parallel_transport( tan_vec_a, base_point, tan_vec_b, n_rungs=n_steps, scheme=step, return_geodesics=True, ) result = ladder["transported_tangent_vec"] result_point = ladder["end_point"] self.assertAllClose(result, expected, rtol=tol, atol=tol) self.assertAllClose(result_point, expected_point) def test_ladder_alpha(self): n_samples = 2 base_point = self.hypersphere.random_uniform(n_samples) tan_vec_a = self.hypersphere.to_tangent(gs.random.rand(n_samples, 3), base_point) tan_vec_b = self.hypersphere.to_tangent(gs.random.rand(n_samples, 3), base_point) with pytest.raises(ValueError): self.hypersphere.metric.ladder_parallel_transport( tan_vec_a, base_point, tan_vec_b, n_rungs=1, scheme="pole", alpha=0.5, return_geodesics=False, ) def test_exp_connection_metric(self): point = gs.array([gs.pi / 2, 0]) vector = gs.array([0.25, 0.5]) point_ext = self.hypersphere.spherical_to_extrinsic(point) vector_ext = self.hypersphere.tangent_spherical_to_extrinsic( vector, point) self.connection.christoffels = self.hypersphere.metric.christoffels expected = self.hypersphere.metric.exp(vector_ext, point_ext) result_spherical = self.connection.exp(vector, point, n_steps=50, step="rk4") result = self.hypersphere.spherical_to_extrinsic(result_spherical) self.assertAllClose(result, expected) def test_exp_connection_metric_vectorization(self): point = gs.array([[gs.pi / 2, 0], [gs.pi / 6, gs.pi / 4]]) vector = gs.array([[0.25, 0.5], [0.30, 0.2]]) point_ext = self.hypersphere.spherical_to_extrinsic(point) vector_ext = self.hypersphere.tangent_spherical_to_extrinsic( vector, point) self.connection.christoffels = self.hypersphere.metric.christoffels expected = self.hypersphere.metric.exp(vector_ext, point_ext) result_spherical = self.connection.exp(vector, point, n_steps=50, step="rk4") result = self.hypersphere.spherical_to_extrinsic(result_spherical) self.assertAllClose(result, expected) @geomstats.tests.autograd_tf_and_torch_only def test_log_connection_metric(self): base_point = gs.array([gs.pi / 3, gs.pi / 4]) point = gs.array([1.0, gs.pi / 2]) self.connection.christoffels = self.hypersphere.metric.christoffels vector = self.connection.log(point=point, base_point=base_point, n_steps=75, step="rk4", tol=1e-10) result = self.hypersphere.tangent_spherical_to_extrinsic( vector, base_point) p_ext = self.hypersphere.spherical_to_extrinsic(base_point) q_ext = self.hypersphere.spherical_to_extrinsic(point) expected = self.hypersphere.metric.log(base_point=p_ext, point=q_ext) self.assertAllClose(result, expected) @geomstats.tests.autograd_tf_and_torch_only def test_log_connection_metric_vectorization(self): base_point = gs.array([[gs.pi / 3, gs.pi / 4], [gs.pi / 2, gs.pi / 4]]) point = gs.array([[1.0, gs.pi / 2], [gs.pi / 6, gs.pi / 3]]) self.connection.christoffels = self.hypersphere.metric.christoffels vector = self.connection.log(point=point, base_point=base_point, n_steps=75, step="rk4", tol=1e-10) result = self.hypersphere.tangent_spherical_to_extrinsic( vector, base_point) p_ext = self.hypersphere.spherical_to_extrinsic(base_point) q_ext = self.hypersphere.spherical_to_extrinsic(point) expected = self.hypersphere.metric.log(base_point=p_ext, point=q_ext) self.assertAllClose(result, expected, atol=1e-6) def test_geodesic_and_coincides_exp_hypersphere(self): n_geodesic_points = 10 initial_point = self.hypersphere.random_uniform(2) vector = gs.array([[2.0, 0.0, -1.0]] * 2) initial_tangent_vec = self.hypersphere.to_tangent( vector=vector, base_point=initial_point) geodesic = self.hypersphere.metric.geodesic( initial_point=initial_point, initial_tangent_vec=initial_tangent_vec) t = gs.linspace(start=0.0, stop=1.0, num=n_geodesic_points) points = geodesic(t) result = points[:, -1] expected = self.hypersphere.metric.exp(vector, initial_point) self.assertAllClose(expected, result) initial_point = initial_point[0] initial_tangent_vec = initial_tangent_vec[0] geodesic = self.hypersphere.metric.geodesic( initial_point=initial_point, initial_tangent_vec=initial_tangent_vec) points = geodesic(t) result = points[-1] expected = self.hypersphere.metric.exp(initial_tangent_vec, initial_point) self.assertAllClose(expected, result) def test_geodesic_and_coincides_exp_son(self): n_geodesic_points = 10 space = SpecialOrthogonal(n=4) initial_point = space.random_uniform(2) vector = gs.random.rand(2, 4, 4) initial_tangent_vec = space.to_tangent(vector=vector, base_point=initial_point) geodesic = space.bi_invariant_metric.geodesic( initial_point=initial_point, initial_tangent_vec=initial_tangent_vec) t = gs.linspace(start=0.0, stop=1.0, num=n_geodesic_points) points = geodesic(t) result = points[:, -1] expected = space.bi_invariant_metric.exp(initial_tangent_vec, initial_point) self.assertAllClose(result, expected) initial_point = initial_point[0] initial_tangent_vec = initial_tangent_vec[0] geodesic = space.bi_invariant_metric.geodesic( initial_point=initial_point, initial_tangent_vec=initial_tangent_vec) points = geodesic(t) result = points[-1] expected = space.bi_invariant_metric.exp(initial_tangent_vec, initial_point) self.assertAllClose(expected, result) def test_geodesic_invalid_initial_conditions(self): space = SpecialOrthogonal(n=4) initial_point = space.random_uniform(2) vector = gs.random.rand(2, 4, 4) initial_tangent_vec = space.to_tangent(vector=vector, base_point=initial_point) end_point = space.random_uniform(2) with pytest.raises(RuntimeError): space.bi_invariant_metric.geodesic( initial_point=initial_point, initial_tangent_vec=initial_tangent_vec, end_point=end_point, ) def test_geodesic_vectorization(self): space = Hypersphere(2) metric = space.metric initial_point = space.random_uniform(2) vector = gs.random.rand(2, 3) initial_tangent_vec = space.to_tangent(vector=vector, base_point=initial_point) end_point = space.random_uniform(2) time = gs.linspace(0, 1, 10) geo = metric.geodesic(initial_point, initial_tangent_vec) path = geo(time) result = path.shape expected = (2, 10, 3) self.assertAllClose(result, expected) geo = metric.geodesic(initial_point, end_point=end_point) path = geo(time) result = path.shape expected = (2, 10, 3) self.assertAllClose(result, expected) geo = metric.geodesic(initial_point, end_point=end_point[0]) path = geo(time) result = path.shape expected = (2, 10, 3) self.assertAllClose(result, expected) initial_tangent_vec = space.to_tangent(vector=vector, base_point=initial_point[0]) geo = metric.geodesic(initial_point[0], initial_tangent_vec) path = geo(time) result = path.shape expected = (2, 10, 3) self.assertAllClose(result, expected)
def test_metric_matrix(self, dim, expected): self.assertAllClose( EuclideanMetric(dim).metric_matrix(), gs.array(expected))
class PullbackMetric(RiemannianMetric): r"""Pullback metric. Let :math:`f` be an immersion :math:`f: M \rightarrow N` of one manifold :math:`M` into the Riemannian manifold :math:`N` with metric :math:`g`. The pull-back metric :math:`f^*g` is defined on :math:`M` for a base point :math:`p` as: :math:`(f^*g)_p(u, v) = g_{f(p)}(df_p u , df_p v) \quad \forall u, v \in T_pM` Note ---- The pull-back metric is currently only implemented for an immersion into the Euclidean space, i.e. for :math:`N=\mathbb{R}^n`. Parameters ---------- dim : int Dimension of the underlying manifold. embedding_dim : int Dimension of the embedding Euclidean space. immersion : callable Map defining the immersion into the Euclidean space. """ def __init__( self, dim, embedding_dim, immersion, jacobian_immersion=None, tangent_immersion=None, ): super(PullbackMetric, self).__init__(dim=dim) self.embedding_metric = EuclideanMetric(embedding_dim) self.immersion = immersion if jacobian_immersion is None: jacobian_immersion = gs.autodiff.jacobian(immersion) self.jacobian_immersion = jacobian_immersion if tangent_immersion is None: def _tangent_immersion(v, x): return gs.matmul(jacobian_immersion(x), v) self.tangent_immersion = _tangent_immersion def metric_matrix(self, base_point=None, n_jobs=1, **joblib_kwargs): r"""Metric matrix at the tangent space at a base point. Let :math:`f` be the immersion :math:`f: M \rightarrow \mathbb{R}^n` of the manifold :math:`M` into the Euclidean space :math:`\mathbb{R}^n`. The elements of the metric matrix at a base point :math:`p` are defined as: :math:`(f*g)_{ij}(p) = <df_p e_i , df_p e_j>`, for :math:`e_i, e_j` basis elements of :math:`M`. Parameters ---------- base_point : array-like, shape=[..., dim] Base point. Optional, default: None. Returns ------- mat : array-like, shape=[..., dim, dim] Inner-product matrix. """ immersed_base_point = self.immersion(base_point) jacobian_immersion = self.jacobian_immersion(base_point) basis_elements = gs.eye(self.dim) @joblib.delayed @joblib.wrap_non_picklable_objects def pickable_inner_product(i, j): immersed_basis_element_i = gs.matmul(jacobian_immersion, basis_elements[i]) immersed_basis_element_j = gs.matmul(jacobian_immersion, basis_elements[j]) return self.embedding_metric.inner_product( immersed_basis_element_i, immersed_basis_element_j, base_point=immersed_base_point, ) pool = joblib.Parallel(n_jobs=n_jobs, **joblib_kwargs) out = pool( pickable_inner_product(i, j) for i, j in itertools.product(range(self.dim), range(self.dim)) ) metric_mat = gs.reshape(gs.array(out), (-1, self.dim, self.dim)) return metric_mat[0] if base_point.ndim == 1 else metric_mat
def __init__(self, dimension): super(HypersphereMetric, self).__init__(dimension=dimension, signature=(dimension, 0, 0)) self.embedding_metric = EuclideanMetric(dimension + 1)
class TestConnection(geomstats.tests.TestCase): def setUp(self): warnings.simplefilter('ignore', category=UserWarning) self.dim = 4 self.euc_metric = EuclideanMetric(dim=self.dim) self.connection = Connection(dim=2) self.hypersphere = Hypersphere(dim=2) def test_metric_matrix(self): base_point = gs.array([0., 1., 0., 0.]) result = self.euc_metric.metric_matrix(base_point) expected = gs.eye(self.dim) self.assertAllClose(result, expected) def test_cometric_matrix(self): base_point = gs.array([0., 1., 0., 0.]) result = self.euc_metric.inner_product_inverse_matrix(base_point) expected = gs.eye(self.dim) self.assertAllClose(result, expected) @geomstats.tests.np_only def test_metric_derivative(self): base_point = gs.array([0., 1., 0., 0.]) result = self.euc_metric.inner_product_derivative_matrix(base_point) expected = gs.zeros((self.dim, ) * 3) self.assertAllClose(result, expected) @geomstats.tests.np_only def test_christoffels(self): base_point = gs.array([0., 1., 0., 0.]) result = self.euc_metric.christoffels(base_point) expected = gs.zeros((self.dim, ) * 3) self.assertAllClose(result, expected) def test_parallel_transport(self): n_samples = 2 base_point = self.hypersphere.random_uniform(n_samples) tan_vec_a = self.hypersphere.to_tangent(gs.random.rand(n_samples, 3), base_point) tan_vec_b = self.hypersphere.to_tangent(gs.random.rand(n_samples, 3), base_point) expected = self.hypersphere.metric.parallel_transport( tan_vec_a, tan_vec_b, base_point) expected_point = self.hypersphere.metric.exp(tan_vec_b, base_point) base_point = gs.cast(base_point, gs.float64) base_point, tan_vec_a, tan_vec_b = gs.convert_to_wider_dtype( [base_point, tan_vec_a, tan_vec_b]) for step, alpha in zip(['pole', 'schild'], [1, 2]): min_n = 1 if step == 'pole' else 50 tol = 1e-5 if step == 'pole' else 1e-2 for n_rungs in [min_n, 11]: ladder = self.hypersphere.metric.ladder_parallel_transport( tan_vec_a, tan_vec_b, base_point, scheme=step, n_rungs=n_rungs, alpha=alpha) result = ladder['transported_tangent_vec'] result_point = ladder['end_point'] self.assertAllClose(result, expected, rtol=tol, atol=tol) self.assertAllClose(result_point, expected_point) def test_parallel_transport_trajectory(self): n_samples = 2 for step in ['pole', 'schild']: n_steps = 1 if step == 'pole' else 50 tol = 1e-6 if step == 'pole' else 1e-2 base_point = self.hypersphere.random_uniform(n_samples) tan_vec_a = self.hypersphere.to_tangent( gs.random.rand(n_samples, 3), base_point) tan_vec_b = self.hypersphere.to_tangent( gs.random.rand(n_samples, 3), base_point) expected = self.hypersphere.metric.parallel_transport( tan_vec_a, tan_vec_b, base_point) expected_point = self.hypersphere.metric.exp(tan_vec_b, base_point) ladder = self.hypersphere.metric.ladder_parallel_transport( tan_vec_a, tan_vec_b, base_point, return_geodesics=True, scheme=step, n_rungs=n_steps) result = ladder['transported_tangent_vec'] result_point = ladder['end_point'] self.assertAllClose(result, expected, rtol=tol, atol=tol) self.assertAllClose(result_point, expected_point) def test_ladder_alpha(self): n_samples = 2 base_point = self.hypersphere.random_uniform(n_samples) tan_vec_a = self.hypersphere.to_tangent(gs.random.rand(n_samples, 3), base_point) tan_vec_b = self.hypersphere.to_tangent(gs.random.rand(n_samples, 3), base_point) self.assertRaises( ValueError, lambda: self.hypersphere.metric. ladder_parallel_transport(tan_vec_a, tan_vec_b, base_point, return_geodesics=False, scheme='pole', n_rungs=1, alpha=0.5)) def test_exp_connection_metric(self): point = gs.array([gs.pi / 2, 0]) vector = gs.array([0.25, 0.5]) point_ext = self.hypersphere.spherical_to_extrinsic(point) vector_ext = self.hypersphere.tangent_spherical_to_extrinsic( vector, point) self.connection.christoffels = self.hypersphere.metric.christoffels expected = self.hypersphere.metric.exp(vector_ext, point_ext) result_spherical = self.connection.exp(vector, point, n_steps=50, step='rk4') result = self.hypersphere.spherical_to_extrinsic(result_spherical) self.assertAllClose(result, expected, rtol=1e-6) def test_exp_connection_metric_vectorization(self): point = gs.array([[gs.pi / 2, 0], [gs.pi / 6, gs.pi / 4]]) vector = gs.array([[0.25, 0.5], [0.30, 0.2]]) point_ext = self.hypersphere.spherical_to_extrinsic(point) vector_ext = self.hypersphere.tangent_spherical_to_extrinsic( vector, point) self.connection.christoffels = self.hypersphere.metric.christoffels expected = self.hypersphere.metric.exp(vector_ext, point_ext) result_spherical = self.connection.exp(vector, point, n_steps=50, step='rk4') result = self.hypersphere.spherical_to_extrinsic(result_spherical) self.assertAllClose(result, expected, rtol=1e-6) def test_log_connection_metric(self): base_point = gs.array([gs.pi / 3, gs.pi / 4]) point = gs.array([1.0, gs.pi / 2]) self.connection.christoffels = self.hypersphere.metric.christoffels vector = self.connection.log(point=point, base_point=base_point, n_steps=75, step='rk', tol=1e-10) result = self.hypersphere.tangent_spherical_to_extrinsic( vector, base_point) p_ext = self.hypersphere.spherical_to_extrinsic(base_point) q_ext = self.hypersphere.spherical_to_extrinsic(point) expected = self.hypersphere.metric.log(base_point=p_ext, point=q_ext) self.assertAllClose(result, expected, rtol=1e-5, atol=1e-5) def test_log_connection_metric_vectorization(self): base_point = gs.array([[gs.pi / 3, gs.pi / 4], [gs.pi / 2, gs.pi / 4]]) point = gs.array([[1.0, gs.pi / 2], [gs.pi / 6, gs.pi / 3]]) self.connection.christoffels = self.hypersphere.metric.christoffels vector = self.connection.log(point=point, base_point=base_point, n_steps=75, step='rk', tol=1e-10) result = self.hypersphere.tangent_spherical_to_extrinsic( vector, base_point) p_ext = self.hypersphere.spherical_to_extrinsic(base_point) q_ext = self.hypersphere.spherical_to_extrinsic(point) expected = self.hypersphere.metric.log(base_point=p_ext, point=q_ext) self.assertAllClose(result, expected, rtol=1e-5, atol=1e-5) def test_geodesic_and_coincides_exp_hypersphere(self): n_geodesic_points = 10 initial_point = self.hypersphere.random_uniform(2) vector = gs.array([[2., 0., -1.]] * 2) initial_tangent_vec = self.hypersphere.to_tangent( vector=vector, base_point=initial_point) geodesic = self.hypersphere.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 = points[-1] expected = self.hypersphere.metric.exp(vector, initial_point) self.assertAllClose(expected, result) initial_point = initial_point[0] initial_tangent_vec = initial_tangent_vec[0] geodesic = self.hypersphere.metric.geodesic( initial_point=initial_point, initial_tangent_vec=initial_tangent_vec) points = geodesic(t) result = points[-1] expected = self.hypersphere.metric.exp(initial_tangent_vec, initial_point) self.assertAllClose(expected, result) def test_geodesic_and_coincides_exp_son(self): n_geodesic_points = 10 space = SpecialOrthogonal(n=4) initial_point = space.random_uniform(2) vector = gs.random.rand(2, 4, 4) initial_tangent_vec = space.to_tangent(vector=vector, base_point=initial_point) geodesic = space.bi_invariant_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 = points[-1] expected = space.bi_invariant_metric.exp(initial_tangent_vec, initial_point) self.assertAllClose(result, expected) initial_point = initial_point[0] initial_tangent_vec = initial_tangent_vec[0] geodesic = space.bi_invariant_metric.geodesic( initial_point=initial_point, initial_tangent_vec=initial_tangent_vec) points = geodesic(t) result = points[-1] expected = space.bi_invariant_metric.exp(initial_tangent_vec, initial_point) self.assertAllClose(expected, result) def test_geodesic_invalid_initial_conditions(self): space = SpecialOrthogonal(n=4) initial_point = space.random_uniform(2) vector = gs.random.rand(2, 4, 4) initial_tangent_vec = space.to_tangent(vector=vector, base_point=initial_point) end_point = space.random_uniform(2) self.assertRaises( RuntimeError, lambda: space.bi_invariant_metric.geodesic( initial_point=initial_point, initial_tangent_vec=initial_tangent_vec, end_point=end_point))
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)
class HypersphereMetric(RiemannianMetric): """Class for the Hypersphere Metric. Parameters ---------- dim : int Dimension of the hypersphere. """ def __init__(self, dim): super(HypersphereMetric, self).__init__( dim=dim, signature=(dim, 0)) self.embedding_metric = EuclideanMetric(dim + 1) self._space = _Hypersphere(dim=dim) def inner_product(self, tangent_vec_a, tangent_vec_b, base_point=None): """Compute the inner-product of two tangent vectors at a base point. Parameters ---------- tangent_vec_a : array-like, shape=[..., dim + 1] First tangent vector at base point. tangent_vec_b : array-like, shape=[..., dim + 1] Second tangent vector at base point. base_point : array-like, shape=[..., dim + 1], optional Point on the hypersphere. Returns ------- inner_prod : array-like, shape=[...,] Inner-product of the two tangent vectors. """ inner_prod = self.embedding_metric.inner_product( tangent_vec_a, tangent_vec_b, base_point) return inner_prod def squared_norm(self, vector, base_point=None): """Compute the squared norm of a vector. Squared norm of a vector associated with the inner-product at the tangent space at a base point. Parameters ---------- vector : array-like, shape=[..., dim + 1] Vector on the tangent space of the hypersphere at base point. base_point : array-like, shape=[..., dim + 1], optional Point on the hypersphere. Returns ------- sq_norm : array-like, shape=[..., 1] Squared norm of the vector. """ sq_norm = self.embedding_metric.squared_norm(vector) return sq_norm def exp(self, tangent_vec, base_point): """Compute the Riemannian exponential of a tangent vector. Parameters ---------- tangent_vec : array-like, shape=[..., dim + 1] Tangent vector at a base point. base_point : array-like, shape=[..., dim + 1] Point on the hypersphere. Returns ------- exp : array-like, shape=[..., dim + 1] Point on the hypersphere equal to the Riemannian exponential of tangent_vec at the base point. """ hypersphere = Hypersphere(dim=self.dim) proj_tangent_vec = hypersphere.to_tangent(tangent_vec, base_point) norm2 = self.embedding_metric.squared_norm(proj_tangent_vec) coef_1 = utils.taylor_exp_even_func( norm2, utils.cos_close_0, order=4) coef_2 = utils.taylor_exp_even_func( norm2, utils.sinc_close_0, order=4) exp = (gs.einsum('...,...j->...j', coef_1, base_point) + gs.einsum('...,...j->...j', coef_2, proj_tangent_vec)) return exp def log(self, point, base_point, **kwargs): """Compute the Riemannian logarithm of a point. Parameters ---------- point : array-like, shape=[..., dim + 1] Point on the hypersphere. base_point : array-like, shape=[..., dim + 1] Point on the hypersphere. Returns ------- log : array-like, shape=[..., dim + 1] Tangent vector at the base point equal to the Riemannian logarithm of point at the base point. """ inner_prod = self.embedding_metric.inner_product(base_point, point) cos_angle = gs.clip(inner_prod, -1., 1.) squared_angle = gs.arccos(cos_angle) ** 2 coef_1_ = utils.taylor_exp_even_func( squared_angle, utils.inv_sinc_close_0, order=5) coef_2_ = utils.taylor_exp_even_func( squared_angle, utils.inv_tanc_close_0, order=5) log = (gs.einsum('...,...j->...j', coef_1_, point) - gs.einsum('...,...j->...j', coef_2_, base_point)) return log def dist(self, point_a, point_b): """Compute the geodesic distance between two points. Parameters ---------- point_a : array-like, shape=[..., dim + 1] First point on the hypersphere. point_b : array-like, shape=[..., dim + 1] Second point on the hypersphere. Returns ------- dist : array-like, shape=[..., 1] Geodesic distance between the two points. """ norm_a = self.embedding_metric.norm(point_a) norm_b = self.embedding_metric.norm(point_b) inner_prod = self.embedding_metric.inner_product(point_a, point_b) cos_angle = inner_prod / (norm_a * norm_b) cos_angle = gs.clip(cos_angle, -1, 1) dist = gs.arccos(cos_angle) return dist def squared_dist(self, point_a, point_b): """Squared geodesic distance between two points. Parameters ---------- point_a : array-like, shape=[..., dim] Point on the hypersphere. point_b : array-like, shape=[..., dim] Point on the hypersphere. Returns ------- sq_dist : array-like, shape=[...,] """ return self.dist(point_a, point_b) ** 2 @staticmethod def parallel_transport(tangent_vec_a, tangent_vec_b, base_point): r"""Compute the parallel transport of a tangent vector. Closed-form solution for the parallel transport of a tangent vector a along the geodesic defined by :math: `t \mapsto exp_(base_point)(t* tangent_vec_b)`. Parameters ---------- tangent_vec_a : array-like, shape=[..., dim + 1] Tangent vector at base point to be transported. tangent_vec_b : array-like, shape=[..., dim + 1] Tangent vector at base point, along which the parallel transport is computed. base_point : array-like, shape=[..., dim + 1] Point on the hypersphere. Returns ------- transported_tangent_vec: array-like, shape=[..., dim + 1] Transported tangent vector at `exp_(base_point)(tangent_vec_b)`. """ theta = gs.linalg.norm(tangent_vec_b, axis=-1) normalized_b = gs.einsum('...,...i->...i', 1 / theta, tangent_vec_b) pb = gs.einsum('...i,...i->...', tangent_vec_a, normalized_b) p_orth = tangent_vec_a - gs.einsum('...,...i->...i', pb, normalized_b) transported = \ - gs.einsum('...,...i->...i', gs.sin(theta) * pb, base_point)\ + gs.einsum('...,...i->...i', gs.cos(theta) * pb, normalized_b)\ + p_orth return transported def christoffels(self, point, point_type='spherical'): """Compute the Christoffel symbols at a point. Only implemented in dimension 2 and for spherical coordinates. Parameters ---------- point : array-like, shape=[..., dim] Point on hypersphere where the Christoffel symbols are computed. point_type: str, {'spherical', 'intrinsic', 'extrinsic'} Coordinates in which to express the Christoffel symbols. Optional, default: 'spherical'. Returns ------- christoffel : array-like, shape=[..., contravariant index, 1st covariant index, 2nd covariant index] Christoffel symbols at point. """ if self.dim != 2 or point_type != 'spherical': raise NotImplementedError( 'The Christoffel symbols are only implemented' ' for spherical coordinates in the 2-sphere') point = gs.to_ndarray(point, to_ndim=2) christoffel = [] for sample in point: gamma_0 = gs.array( [[0, 0], [0, - gs.sin(sample[0]) * gs.cos(sample[0])]]) gamma_1 = gs.array([[0, gs.cos(sample[0]) / gs.sin(sample[0])], [gs.cos(sample[0]) / gs.sin(sample[0]), 0]]) christoffel.append(gs.stack([gamma_0, gamma_1])) christoffel = gs.stack(christoffel) if gs.ndim(christoffel) == 4 and gs.shape(christoffel)[0] == 1: christoffel = gs.squeeze(christoffel, axis=0) return christoffel def curvature( self, tangent_vec_a, tangent_vec_b, tangent_vec_c, base_point): r"""Compute the curvature. For three tangent vectors at a base point :math: `x,y,z`, the curvature is defined by :math: `R(x, y)z = \nabla_{[x,y]}z - \nabla_z\nabla_y z + \nabla_y\nabla_x z`, where :math: `\nabla` is the Levi-Civita connection. In the case of the hypersphere, we have the closed formula :math: `R(x,y)z = \langle x, z \rangle y - \langle y,z \rangle x`. Parameters ---------- tangent_vec_a : array-like, shape=[..., dim] Tangent vector at `base_point`. tangent_vec_b : array-like, shape=[..., dim] Tangent vector at `base_point`. tangent_vec_c : array-like, shape=[..., dim] Tangent vector at `base_point`. base_point : array-like, shape=[..., dim] Point on the group. Optional, default is the identity. Returns ------- curvature : array-like, shape=[..., dim] Tangent vector at `base_point`. """ inner_ac = self.inner_product(tangent_vec_a, tangent_vec_c) inner_bc = self.inner_product(tangent_vec_b, tangent_vec_c) first_term = gs.einsum('...,...i->...i', inner_bc, tangent_vec_a) second_term = gs.einsum('...,...i->...i', inner_ac, tangent_vec_b) return - first_term + second_term
def test_dist(self, dim, point_a, point_b, expected): metric = EuclideanMetric(dim) result = metric.dist(point_a, point_b) self.assertAllClose(result, gs.array(expected))
class TestConnectionMethods(geomstats.tests.TestCase): def setUp(self): warnings.simplefilter('ignore', category=UserWarning) self.dimension = 4 self.euc_metric = EuclideanMetric(dimension=self.dimension) self.connection = Connection(dimension=2) self.hypersphere = Hypersphere(dimension=2) def test_metric_matrix(self): base_point = gs.array([0., 1., 0., 0.]) result = self.euc_metric.inner_product_matrix(base_point) expected = gs.array([gs.eye(self.dimension)]) with self.session(): self.assertAllClose(result, expected) def test_cometric_matrix(self): base_point = gs.array([0., 1., 0., 0.]) result = self.euc_metric.inner_product_inverse_matrix(base_point) expected = gs.array([gs.eye(self.dimension)]) with self.session(): self.assertAllClose(result, expected) @geomstats.tests.np_only def test_metric_derivative(self): base_point = gs.array([0., 1., 0., 0.]) result = self.euc_metric.inner_product_derivative_matrix(base_point) expected = gs.zeros((1, ) + (self.dimension, ) * 3) self.assertAllClose(result, expected) @geomstats.tests.np_only def test_christoffels(self): base_point = gs.array([0., 1., 0., 0.]) result = self.euc_metric.christoffels(base_point) expected = gs.zeros((1, ) + (self.dimension, ) * 3) self.assertAllClose(result, expected) @geomstats.tests.np_only def test_parallel_transport(self): n_samples = 10 base_point = self.hypersphere.random_uniform(n_samples) tan_vec_a = self.hypersphere.projection_to_tangent_space( gs.random.rand(n_samples, 3), base_point) tan_vec_b = self.hypersphere.projection_to_tangent_space( gs.random.rand(n_samples, 3), base_point) expected = self.hypersphere.metric.parallel_transport( tan_vec_a, tan_vec_b, base_point) result = self.hypersphere.metric.pole_ladder_parallel_transport( tan_vec_a, tan_vec_b, base_point) self.assertAllClose(result, expected, rtol=1e-7, atol=1e-5) @geomstats.tests.np_only def test_exp(self): point = gs.array([[gs.pi / 2, 0], [gs.pi / 6, gs.pi / 4]]) vector = gs.array([[0.25, 0.5], [0.30, 0.2]]) point_ext = self.hypersphere.spherical_to_extrinsic(point) vector_ext = self.hypersphere.tangent_spherical_to_extrinsic( vector, point) self.connection.christoffels = self.hypersphere.metric.christoffels expected = self.hypersphere.metric.exp(vector_ext, point_ext) result_spherical = self.connection.exp(vector, point, n_steps=50, step='rk4') result = self.hypersphere.spherical_to_extrinsic(result_spherical) self.assertAllClose(result, expected, rtol=1e-6) @geomstats.tests.np_only def test_log(self): base_point = gs.array([[gs.pi / 3, gs.pi / 4], [gs.pi / 2, gs.pi / 4]]) point = gs.array([[1.0, gs.pi / 2], [gs.pi / 6, gs.pi / 3]]) self.connection.christoffels = self.hypersphere.metric.christoffels vector = self.connection.log(point=point, base_point=base_point, n_steps=75, step='rk') result = self.hypersphere.tangent_spherical_to_extrinsic( vector, base_point) p_ext = self.hypersphere.spherical_to_extrinsic(base_point) q_ext = self.hypersphere.spherical_to_extrinsic(point) expected = self.hypersphere.metric.log(base_point=p_ext, point=q_ext) self.assertAllClose(result, expected, rtol=1e-5, atol=1e-5)
def __init__(self, space, ray_metric=EuclideanMetric(1)): super(SpiderMetric, self).__init__(space=space) self.ray_metric = ray_metric
class RiemannianMetricTestData(TestData): dim = 2 euc = Euclidean(dim=dim) sphere = Hypersphere(dim=dim) euc_metric = EuclideanMetric(dim=dim) sphere_metric = HypersphereMetric(dim=dim) new_euc_metric = RiemannianMetric(dim=dim) new_euc_metric.metric_matrix = _euc_metric_matrix new_sphere_metric = RiemannianMetric(dim=dim) new_sphere_metric.metric_matrix = _sphere_metric_matrix new_euc_metric = new_euc_metric new_sphere_metric = new_sphere_metric def cometric_matrix_test_data(self): random_data = [ dict( metric=self.euc_metric, base_point=self.euc.random_point(), expected=gs.eye(self.dim), ) ] return self.generate_tests(random_data) def inner_coproduct_test_data(self): base_point = gs.array([0.0, 0.0, 1.0]) cotangent_vec_a = self.sphere.to_tangent(gs.array([1.0, 2.0, 0.0]), base_point) cotangent_vec_b = self.sphere.to_tangent(gs.array([1.0, 3.0, 0.0]), base_point) smoke_data = [ dict( metric=self.euc_metric, cotangent_vec_a=gs.array([1.0, 2.0]), cotangent_vec_b=gs.array([1.0, 2.0]), base_point=self.euc.random_point(), expected=5.0, ), dict( metric=self.sphere_metric, cotangent_vec_a=cotangent_vec_a, cotangent_vec_b=cotangent_vec_b, base_point=base_point, expected=7.0, ), ] return self.generate_tests(smoke_data) def hamiltonian_test_data(self): smoke_data = [ dict( metric=self.euc_metric, state=(gs.array([1.0, 2.0]), gs.array([1.0, 2.0])), expected=2.5, ) ] smoke_data += [ dict( metric=self.sphere_metric, state=(gs.array([0.0, 0.0, 1.0]), gs.array([1.0, 2.0, 1.0])), expected=3.0, ) ] return self.generate_tests(smoke_data) def inner_product_derivative_matrix_test_data(self): base_point = self.euc.random_point() random_data = [ dict( metric=self.new_euc_metric, base_point=base_point, expected=gs.zeros((self.dim, ) * 3), ) ] random_data += [ dict( metric=self.euc_metric, base_point=base_point, expected=gs.zeros((self.dim, ) * 3), ) ] return self.generate_tests([], random_data) def inner_product_test_data(self): base_point = self.euc.random_point() tangent_vec_a = self.euc.random_point() tangent_vec_b = self.euc.random_point() random_data = [ dict( metric=self.euc_metric, tangent_vec_a=tangent_vec_a, tangent_vec_b=tangent_vec_b, base_point=base_point, expected=gs.dot(tangent_vec_a, tangent_vec_b), ) ] smoke_data = [ dict( metric=self.new_sphere_metric, tangent_vec_a=gs.array([0.3, 0.4]), tangent_vec_b=gs.array([0.1, -0.5]), base_point=gs.array([gs.pi / 3.0, gs.pi / 5.0]), expected=-0.12, ) ] return self.generate_tests(smoke_data, random_data) def christoffels_test_data(self): base_point = gs.array([gs.pi / 10.0, gs.pi / 9.0]) gs.array([gs.pi / 10.0, gs.pi / 9.0]) smoke_data = [] random_data = [] smoke_data = [ dict( metric=self.new_sphere_metric, base_point=gs.array([gs.pi / 10.0, gs.pi / 9.0]), expected=self.sphere_metric.christoffels(base_point), ) ] random_data += [ dict( metric=self.new_euc_metric, base_point=self.euc.random_point(), expected=gs.zeros((self.dim, ) * 3), ) ] random_data += [ dict( metric=self.euc_metric, base_point=self.euc.random_point(), expected=gs.zeros((self.dim, ) * 3), ) ] return self.generate_tests(smoke_data, random_data) def exp_test_data(self): base_point = gs.array([gs.pi / 10.0, gs.pi / 9.0]) tangent_vec = gs.array([gs.pi / 2.0, 0.0]) expected = gs.array([gs.pi / 10.0 + gs.pi / 2.0, gs.pi / 9.0]) euc_base_point = self.euc.random_point() euc_tangent_vec = self.euc.random_point() euc_expected = euc_base_point + euc_tangent_vec smoke_data = [ dict( metric=self.new_sphere_metric, tangent_vec=tangent_vec, base_point=base_point, expected=expected, ) ] random_data = [ dict( metric=self.new_euc_metric, tangent_vec=euc_tangent_vec, base_point=euc_base_point, expected=euc_expected, ) ] return self.generate_tests(smoke_data, random_data) def log_test_data(self): base_point = self.euc.random_point() point = self.euc.random_point() expected = point - base_point random_data = [ dict( metric=self.new_euc_metric, point=point, base_point=base_point, expected=expected, ) ] return self.generate_tests([], random_data)
class HypersphereMetric(RiemannianMetric): """Class for the Hypersphere Metric.""" def __init__(self, dimension): super(HypersphereMetric, self).__init__(dimension=dimension, signature=(dimension, 0, 0)) self.embedding_metric = EuclideanMetric(dimension + 1) def inner_product(self, tangent_vec_a, tangent_vec_b, base_point=None): """Compute the inner product of two tangent vectors at a base point. Parameters ---------- tangent_vec_a : array-like, shape=[n_samples, dimension + 1] or shape=[1, dimension + 1] tangent_vec_b : array-like, shape=[n_samples, dimension + 1] or shape=[1, dimension + 1] base_point : array-like, shape=[n_samples, dimension + 1] or shape=[1, dimension + 1] Returns ------- inner_prod : array-like, shape=[n_samples, 1] or shape=[1, 1] """ inner_prod = self.embedding_metric.inner_product( tangent_vec_a, tangent_vec_b, base_point) return inner_prod def squared_norm(self, vector, base_point=None): """Compute squared norm of a vector. Squared norm of a vector associated to the inner product at the tangent space at a base point. Parameters ---------- vector : array-like, shape=[n_samples, dimension + 1] or shape=[1, dimension + 1] base_point : array-like, shape=[n_samples, dimension + 1] or shape=[1, dimension + 1] Returns ------- sq_norm : array-like, shape=[n_samples, 1] or shape=[1, 1] """ sq_norm = self.embedding_metric.squared_norm(vector) return sq_norm def exp(self, tangent_vec, base_point): """Riemannian exponential of a tangent vector wrt to a base point. Parameters ---------- tangent_vec : array-like, shape=[n_samples, dimension + 1] or shape=[1, dimension + 1] base_point : array-like, shape=[n_samples, dimension + 1] or shape=[1, dimension + 1] Returns ------- exp : array-like, shape=[n_samples, dimension + 1] or shape=[1, dimension + 1] """ tangent_vec = gs.to_ndarray(tangent_vec, to_ndim=2) base_point = gs.to_ndarray(base_point, to_ndim=2) # TODO(nina): Decide on metric.space or space.metric # for the hypersphere # TODO(nina): Raise error when vector is not tangent n_base_points, extrinsic_dim = base_point.shape n_tangent_vecs, _ = tangent_vec.shape hypersphere = Hypersphere(dimension=extrinsic_dim - 1) proj_tangent_vec = hypersphere.projection_to_tangent_space( tangent_vec, base_point) norm_tangent_vec = self.embedding_metric.norm(proj_tangent_vec) mask_0 = gs.isclose(norm_tangent_vec, 0.) mask_non0 = ~mask_0 coef_1 = gs.zeros((n_tangent_vecs, 1)) coef_2 = gs.zeros((n_tangent_vecs, 1)) norm2 = norm_tangent_vec[mask_0]**2 norm4 = norm2**2 norm6 = norm2**3 coef_1[mask_0] = 1. - norm2 / 2. + norm4 / 24. - norm6 / 720. coef_2[mask_0] = 1. - norm2 / 6. + norm4 / 120. - norm6 / 5040. coef_1[mask_non0] = gs.cos(norm_tangent_vec[mask_non0]) coef_2[mask_non0] = gs.sin(norm_tangent_vec[mask_non0]) / \ norm_tangent_vec[mask_non0] n_coef_1 = n_tangent_vecs if n_coef_1 != n_base_points: if n_coef_1 == 1: coef_1 = gs.squeeze(coef_1, axis=0) einsum_str = 'i,nj->nj' elif n_base_points == 1: base_point = gs.squeeze(base_point, axis=0) einsum_str = 'ni,j->nj' else: raise ValueError('Shape mismatch in einsum.') else: einsum_str = 'ni,nj->nj' exp = (gs.einsum(einsum_str, coef_1, base_point) + gs.einsum('ni,nj->nj', coef_2, proj_tangent_vec)) return exp def log(self, point, base_point): """Compute Riemannian logarithm of a point wrt a base point. Parameters ---------- point : array-like, shape=[n_samples, dimension + 1] or shape=[1, dimension + 1] base_point : array-like, shape=[n_samples, dimension + 1] or shape=[1, dimension + 1] Returns ------- log : array-like, shape=[n_samples, dimension + 1] or shape=[1, dimension + 1] """ point = gs.to_ndarray(point, to_ndim=2) base_point = gs.to_ndarray(base_point, to_ndim=2) norm_base_point = self.embedding_metric.norm(base_point) norm_point = self.embedding_metric.norm(point) inner_prod = self.embedding_metric.inner_product(base_point, point) cos_angle = inner_prod / (norm_base_point * norm_point) cos_angle = gs.clip(cos_angle, -1., 1.) angle = gs.arccos(cos_angle) angle = gs.to_ndarray(angle, to_ndim=1) angle = gs.to_ndarray(angle, to_ndim=2, axis=1) mask_0 = gs.isclose(angle, 0.) mask_else = gs.equal(mask_0, gs.array(False)) mask_0_float = gs.cast(mask_0, gs.float32) mask_else_float = gs.cast(mask_else, gs.float32) coef_1 = gs.zeros_like(angle) coef_2 = gs.zeros_like(angle) coef_1 += mask_0_float * (1. + INV_SIN_TAYLOR_COEFFS[1] * angle**2 + INV_SIN_TAYLOR_COEFFS[3] * angle**4 + INV_SIN_TAYLOR_COEFFS[5] * angle**6 + INV_SIN_TAYLOR_COEFFS[7] * angle**8) coef_2 += mask_0_float * (1. + INV_TAN_TAYLOR_COEFFS[1] * angle**2 + INV_TAN_TAYLOR_COEFFS[3] * angle**4 + INV_TAN_TAYLOR_COEFFS[5] * angle**6 + INV_TAN_TAYLOR_COEFFS[7] * angle**8) # This avoids division by 0. angle += mask_0_float * 1. coef_1 += mask_else_float * angle / gs.sin(angle) coef_2 += mask_else_float * angle / gs.tan(angle) log = (gs.einsum('ni,nj->nj', coef_1, point) - gs.einsum('ni,nj->nj', coef_2, base_point)) mask_same_values = gs.isclose(point, base_point) mask_else = gs.equal(mask_same_values, gs.array(False)) mask_else_float = gs.cast(mask_else, gs.float32) mask_else_float = gs.to_ndarray(mask_else_float, to_ndim=1) mask_else_float = gs.to_ndarray(mask_else_float, to_ndim=2) mask_not_same_points = gs.sum(mask_else_float, axis=1) mask_same_points = gs.isclose(mask_not_same_points, 0.) mask_same_points = gs.cast(mask_same_points, gs.float32) mask_same_points = gs.to_ndarray(mask_same_points, to_ndim=2, axis=1) mask_same_points_float = gs.cast(mask_same_points, gs.float32) log -= mask_same_points_float * log return log def dist(self, point_a, point_b): """Compute geodesic distance between two points. Parameters ---------- point_a : array-like, shape=[n_samples, dimension + 1] or shape=[1, dimension + 1] point_b : array-like, shape=[n_samples, dimension + 1] or shape=[1, dimension + 1] Returns ------- dist : array-like, shape=[n_samples, 1] or shape=[1, 1] """ norm_a = self.embedding_metric.norm(point_a) norm_b = self.embedding_metric.norm(point_b) inner_prod = self.embedding_metric.inner_product(point_a, point_b) cos_angle = inner_prod / (norm_a * norm_b) cos_angle = gs.clip(cos_angle, -1, 1) dist = gs.arccos(cos_angle) return dist def parallel_transport(self, tangent_vec_a, tangent_vec_b, base_point): """Parallel transport of a tangent vector. Closed-form solution for the parallel transport of a tangent vector a along the geodesic defined by exp_(base_point)(tangent_vec_b) Parameters ---------- tangent_vec_a : array-like, shape=[n_samples, dimension + 1] tangent_vec_b : array-like, shape=[n_samples, dimension + 1] base_point : array-like, shape=[n_samples, dimension + 1] Returns ------- transported_tangent_vec: array-like, shape=[n_samples, dimension + 1] """ tangent_vec_a = gs.to_ndarray(tangent_vec_a, to_ndim=2) tangent_vec_b = gs.to_ndarray(tangent_vec_b, to_ndim=2) base_point = gs.to_ndarray(base_point, to_ndim=2) # TODO @nguigs: work around this condition assert len(base_point) == len(tangent_vec_a) == len(tangent_vec_b) theta = gs.linalg.norm(tangent_vec_b, axis=1) normalized_b = gs.einsum('n, ni->ni', 1 / theta, tangent_vec_b) pb = gs.einsum('ni,ni->n', tangent_vec_a, normalized_b) p_orth = tangent_vec_a - gs.einsum('n,ni->ni', pb, normalized_b) transported = - gs.einsum('n,ni->ni', gs.sin(theta) * pb, base_point)\ + gs.einsum('n,ni->ni', gs.cos(theta) * pb, normalized_b)\ + p_orth return transported def christoffels(self, point, point_type='spherical'): """Compute Christoffel symbols. Only implemented in dimension 2 and for spherical coordinates. Parameters ---------- point : array-like, shape=[n_samples, dimension] point_type: str Returns ------- christoffel : array-like, shape=[n_samples, contravariant index, first covariant index, second covariant index] """ if self.dimension != 2 or point_type != 'spherical': raise NotImplementedError( 'The Christoffel symbols are only implemented' ' for spherical coordinates in the 2-sphere') point = gs.to_ndarray(point, to_ndim=2) christoffel = [] for sample in point: gamma_0 = gs.array([[0, 0], [0, -gs.sin(sample[0]) * gs.cos(sample[0])]]) gamma_1 = gs.array([[0, gs.cos(sample[0]) / gs.sin(sample[0])], [gs.cos(sample[0]) / gs.sin(sample[0]), 0]]) christoffel.append(gs.stack([gamma_0, gamma_1])) return gs.stack(christoffel)
def __init__(self, dim): super(HypersphereMetric, self).__init__( dim=dim, signature=(dim, 0)) self.embedding_metric = EuclideanMetric(dim + 1) self._space = _Hypersphere(dim=dim)
class TestConnectionMethods(geomstats.tests.TestCase): def setUp(self): warnings.simplefilter('ignore', category=UserWarning) self.dim = 4 self.euc_metric = EuclideanMetric(dim=self.dim) self.connection = Connection(dim=2) self.hypersphere = Hypersphere(dim=2) def test_metric_matrix(self): base_point = gs.array([0., 1., 0., 0.]) result = self.euc_metric.inner_product_matrix(base_point) expected = gs.eye(self.dim) self.assertAllClose(result, expected) def test_cometric_matrix(self): base_point = gs.array([0., 1., 0., 0.]) result = self.euc_metric.inner_product_inverse_matrix(base_point) expected = gs.eye(self.dim) self.assertAllClose(result, expected) @geomstats.tests.np_only def test_metric_derivative(self): base_point = gs.array([0., 1., 0., 0.]) result = self.euc_metric.inner_product_derivative_matrix(base_point) expected = gs.zeros((self.dim, ) * 3) self.assertAllClose(result, expected) @geomstats.tests.np_only def test_christoffels(self): base_point = gs.array([0., 1., 0., 0.]) result = self.euc_metric.christoffels(base_point) expected = gs.zeros((self.dim, ) * 3) self.assertAllClose(result, expected) @geomstats.tests.np_and_pytorch_only def test_parallel_transport(self): n_samples = 2 for step in ['pole', 'schild']: n_steps = 1 if step == 'pole' else 100 tol = 1e-6 if step == 'pole' else 1e-1 base_point = self.hypersphere.random_uniform(n_samples) tan_vec_a = self.hypersphere.projection_to_tangent_space( gs.random.rand(n_samples, 3), base_point) tan_vec_b = self.hypersphere.projection_to_tangent_space( gs.random.rand(n_samples, 3), base_point) expected = self.hypersphere.metric.parallel_transport( tan_vec_a, tan_vec_b, base_point) ladder = self.hypersphere.metric.ladder_parallel_transport( tan_vec_a, tan_vec_b, base_point, step=step, n_steps=n_steps) result = ladder['transported_tangent_vec'] self.assertAllClose(result, expected, rtol=tol, atol=tol) @geomstats.tests.np_and_pytorch_only def test_parallel_transport_trajectory(self): n_samples = 2 for step in ['pole', 'schild']: n_steps = 1 if step == 'pole' else 100 rtol = 1e-6 if step == 'pole' else 1e-1 base_point = self.hypersphere.random_uniform(n_samples) tan_vec_a = self.hypersphere.projection_to_tangent_space( gs.random.rand(n_samples, 3), base_point) tan_vec_b = self.hypersphere.projection_to_tangent_space( gs.random.rand(n_samples, 3), base_point) expected = self.hypersphere.metric.parallel_transport( tan_vec_a, tan_vec_b, base_point) ladder = self.hypersphere.metric.ladder_parallel_transport( tan_vec_a, tan_vec_b, base_point, return_geodesics=True, step=step, n_steps=n_steps) result = ladder['transported_tangent_vec'] self.assertAllClose(result, expected, rtol=rtol) @geomstats.tests.np_only def test_exp(self): point = gs.array([[gs.pi / 2, 0], [gs.pi / 6, gs.pi / 4]]) vector = gs.array([[0.25, 0.5], [0.30, 0.2]]) point_ext = self.hypersphere.spherical_to_extrinsic(point) vector_ext = self.hypersphere.tangent_spherical_to_extrinsic( vector, point) self.connection.christoffels = self.hypersphere.metric.christoffels expected = self.hypersphere.metric.exp(vector_ext, point_ext) result_spherical = self.connection.exp(vector, point, n_steps=50, step='rk4') result = self.hypersphere.spherical_to_extrinsic(result_spherical) self.assertAllClose(result, expected, rtol=1e-6) @geomstats.tests.np_only def test_log(self): base_point = gs.array([[gs.pi / 3, gs.pi / 4], [gs.pi / 2, gs.pi / 4]]) point = gs.array([[1.0, gs.pi / 2], [gs.pi / 6, gs.pi / 3]]) self.connection.christoffels = self.hypersphere.metric.christoffels vector = self.connection.log(point=point, base_point=base_point, n_steps=75, step='rk') result = self.hypersphere.tangent_spherical_to_extrinsic( vector, base_point) p_ext = self.hypersphere.spherical_to_extrinsic(base_point) q_ext = self.hypersphere.spherical_to_extrinsic(point) expected = self.hypersphere.metric.log(base_point=p_ext, point=q_ext) self.assertAllClose(result, expected, rtol=1e-5, atol=1e-5)