def rotation_vector_from_quaternion(self, quaternion): """Convert a unit quaternion into a rotation vector. Parameters ---------- quaternion : array-like, shape=[..., 4] Returns ------- rot_vec : array-like, shape=[..., 3] """ cos_half_angle = quaternion[:, 0] cos_half_angle = gs.clip(cos_half_angle, -1, 1) half_angle = gs.arccos(cos_half_angle) half_angle = gs.to_ndarray(half_angle, to_ndim=2, axis=1) mask_0 = gs.isclose(half_angle, 0.) mask_not_0 = ~mask_0 rotation_axis = gs.divide( quaternion[:, 1:], gs.sin(half_angle) * gs.cast(mask_not_0, gs.float32) + gs.cast(mask_0, gs.float32)) rot_vec = gs.array(2 * half_angle * rotation_axis * gs.cast(mask_not_0, gs.float32)) rot_vec = self.regularize(rot_vec) return rot_vec
def __init__(self, group, inner_product_mat_at_identity=None, left_or_right='left'): if inner_product_mat_at_identity is None: inner_product_mat_at_identity = gs.eye(self.group.dimension) inner_product_mat_at_identity = gs.to_ndarray( inner_product_mat_at_identity, to_ndim=3) mat_shape = inner_product_mat_at_identity.shape assert mat_shape == (1, ) + (group.dimension, ) * 2, mat_shape assert left_or_right in ('left', 'right') eigenvalues = gs.linalg.eigvalsh(inner_product_mat_at_identity) mask_pos_eigval = gs.greater(eigenvalues, 0.) n_pos_eigval = gs.sum(gs.cast(mask_pos_eigval, gs.int32)) mask_neg_eigval = gs.less(eigenvalues, 0.) n_neg_eigval = gs.sum(gs.cast(mask_neg_eigval, gs.int32)) mask_null_eigval = gs.isclose(eigenvalues, 0.) n_null_eigval = gs.sum(gs.cast(mask_null_eigval, gs.int32)) self.group = group if inner_product_mat_at_identity is None: inner_product_mat_at_identity = gs.eye(self.group.dimension) self.inner_product_mat_at_identity = inner_product_mat_at_identity self.left_or_right = left_or_right self.signature = (n_pos_eigval, n_null_eigval, n_neg_eigval)
def __init__(self, group, algebra=None, metric_mat_at_identity=None, left_or_right='left', **kwargs): super(InvariantMetric, self).__init__(dim=group.dim, **kwargs) self.group = group self.lie_algebra = algebra if metric_mat_at_identity is None: metric_mat_at_identity = gs.eye(self.group.dim) geomstats.errors.check_parameter_accepted_values( left_or_right, 'left_or_right', ['left', 'right']) eigenvalues = gs.linalg.eigvalsh(metric_mat_at_identity) mask_pos_eigval = gs.greater(eigenvalues, 0.) n_pos_eigval = gs.sum(gs.cast(mask_pos_eigval, gs.int32)) mask_neg_eigval = gs.less(eigenvalues, 0.) n_neg_eigval = gs.sum(gs.cast(mask_neg_eigval, gs.int32)) mask_null_eigval = gs.isclose(eigenvalues, 0.) n_null_eigval = gs.sum(gs.cast(mask_null_eigval, gs.int32)) self.metric_mat_at_identity = metric_mat_at_identity self.left_or_right = left_or_right self.signature = (n_pos_eigval, n_null_eigval, n_neg_eigval)
def test_random_von_mises_kappa(self): # check concentration parameter for dispersed distribution kappa = 1.0 n_points = 100000 for dim in [2, 9]: sphere = Hypersphere(dim) points = sphere.random_von_mises_fisher(kappa=kappa, n_samples=n_points) sum_points = gs.sum(points, axis=0) mean_norm = gs.linalg.norm(sum_points) / n_points kappa_estimate = (mean_norm * (dim + 1.0 - mean_norm**2) / (1.0 - mean_norm**2)) kappa_estimate = gs.cast(kappa_estimate, gs.float64) p = dim + 1 n_steps = 100 for _ in range(n_steps): bessel_func_1 = scipy.special.iv(p / 2.0, kappa_estimate) bessel_func_2 = scipy.special.iv(p / 2.0 - 1.0, kappa_estimate) ratio = bessel_func_1 / bessel_func_2 denominator = 1.0 - ratio**2 - (p - 1.0) * ratio / kappa_estimate mean_norm = gs.cast(mean_norm, gs.float64) kappa_estimate = kappa_estimate - (ratio - mean_norm) / denominator result = kappa_estimate expected = kappa self.assertAllClose(result, expected, atol=KAPPA_ESTIMATION_TOL)
def _log_translation_transform(self, rot_vec): rot_vec = gs.to_ndarray(rot_vec, to_ndim=2, axis=1) exp_transform = self._exp_translation_transform(rot_vec) if rot_vec.dtype == gs.float32: mask_close_0 = gs.isclose(rot_vec, 0., atol=1e-6) else: mask_close_0 = gs.isclose(rot_vec, 0.) mask_else = ~mask_close_0 mask_close_0_float = gs.cast(mask_close_0, gs.float32) mask_else_float = gs.cast(mask_else, gs.float32) inv_determinant = gs.zeros_like(rot_vec) inv_determinant += 0.5 * mask_close_0_float / ( TAYLOR_COEFFS_1_AT_0[0] + TAYLOR_COEFFS_1_AT_0[1] * rot_vec ** 2 + TAYLOR_COEFFS_1_AT_0[2] * rot_vec ** 6) rot_vec = rot_vec + 1e-3 * mask_close_0_float inv_determinant += mask_else_float * ( rot_vec ** 2 / (2 * (1 - gs.cos(rot_vec)))) transform = gs.einsum( 'il, ijk -> ijk', inv_determinant, gs.transpose(exp_transform, axes=[0, 2, 1])) return transform
def log(self, point, base_point): """ Riemannian logarithm of a point wrt a base point. """ 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 quaternion_from_rotation_vector(self, rot_vec): """Convert a rotation vector into a unit quaternion. Parameters ---------- rot_vec : array-like, shape=[..., 3] Returns ------- quaternion : array-like, shape=[..., 4] """ rot_vec = self.regularize(rot_vec) angle = gs.linalg.norm(rot_vec, axis=1) angle = gs.to_ndarray(angle, to_ndim=2, axis=1) mask_0 = gs.isclose(angle, 0.) mask_not_0 = ~mask_0 rotation_axis = gs.divide( rot_vec, angle * gs.cast(mask_not_0, gs.float32) + gs.cast(mask_0, gs.float32)) quaternion = gs.concatenate( (gs.cos(angle / 2), gs.sin(angle / 2) * rotation_axis[:]), axis=1) return quaternion
def inner_product(self, tangent_vec_a, tangent_vec_b, base_point=None): """ Inner product defined by the Riemannian metric at point base_point between tangent vectors tangent_vec_a and 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) n_tangent_vec_a, _ = tangent_vec_a.shape n_tangent_vec_b, _ = tangent_vec_b.shape inner_prod_mat = self.inner_product_matrix(base_point) inner_prod_mat = gs.to_ndarray(inner_prod_mat, to_ndim=3) n_mats, _, _ = inner_prod_mat.shape n_inner_prod = gs.maximum(n_tangent_vec_a, n_tangent_vec_b) n_inner_prod = gs.maximum(n_inner_prod, n_mats) n_tiles_a = gs.divide(n_inner_prod, n_tangent_vec_a) n_tiles_a = gs.cast(n_tiles_a, gs.int32) tangent_vec_a = gs.tile(tangent_vec_a, [n_tiles_a, 1]) n_tiles_b = gs.divide(n_inner_prod, n_tangent_vec_b) n_tiles_b = gs.cast(n_tiles_b, gs.int32) tangent_vec_b = gs.tile(tangent_vec_b, [n_tiles_b, 1]) n_tiles_mat = gs.divide(n_inner_prod, n_mats) n_tiles_mat = gs.cast(n_tiles_mat, gs.int32) inner_prod_mat = gs.tile(inner_prod_mat, [n_tiles_mat, 1, 1]) aux = gs.einsum('nj,njk->nk', tangent_vec_a, inner_prod_mat) inner_prod = gs.einsum('nk,nk->n', aux, tangent_vec_b) inner_prod = gs.to_ndarray(inner_prod, to_ndim=2, axis=1) assert gs.ndim(inner_prod) == 2, inner_prod.shape return inner_prod
def _fit_extrinsic(self, X, y, weights=None, compute_training_score=False): """Estimate the parameters using the extrinsic gradient descent. Estimate the intercept and the coefficient defining the geodesic regression model, using the extrinsic gradient. Parameters ---------- X : {array-like, sparse matrix}, shape=[...,}] Training input samples. y : array-like, shape=[..., {dim, [n,n]}] Training target values. weights : array-like, shape=[...,] Weights associated to the points. Optional, default: None. compute_training_score : bool Whether to compute R^2. Optional, default: False. Returns ------- self : object Returns self. """ shape = ( y.shape[-1:] if self.space.default_point_type == "vector" else y.shape[-2:] ) intercept_init, coef_init = self.initialize_parameters(y) intercept_hat = self.space.projection(intercept_init) coef_hat = self.space.to_tangent(coef_init, intercept_hat) initial_guess = gs.vstack([gs.flatten(intercept_hat), gs.flatten(coef_hat)]) objective_with_grad = gs.autodiff.value_and_grad( lambda param: self._loss(X, y, param, shape, weights), to_numpy=True ) res = minimize( objective_with_grad, initial_guess, method="CG", jac=True, options={"disp": self.verbose, "maxiter": self.max_iter}, tol=self.tol, ) intercept_hat, coef_hat = gs.split(gs.array(res.x), 2) intercept_hat = gs.reshape(intercept_hat, shape) intercept_hat = gs.cast(intercept_hat, dtype=y.dtype) coef_hat = gs.reshape(coef_hat, shape) coef_hat = gs.cast(coef_hat, dtype=y.dtype) self.intercept_ = self.space.projection(intercept_hat) self.coef_ = self.space.to_tangent(coef_hat, self.intercept_) if compute_training_score: variance = gs.sum(self.metric.squared_dist(y, self.intercept_)) self.training_score_ = 1 - 2 * res.fun / variance return self
def test_log(self, n, point, base_point, expected): group = self.space(n) expected = gs.cast(gs.array(expected), gs.float64) point = gs.cast(gs.array(point), gs.float64) base_point = (None if base_point is None else gs.cast( gs.array(base_point), gs.float64)) self.assertAllClose(group.log(point, base_point), expected)
def test_exp_vectorization(self): point = gs.array( [ [[2.0, 0.0, 0.0], [0.0, 3.0, 0.0], [0.0, 0.0, 4.0]], [[1.0, 0.0, 0.0], [0.0, 5.0, 0.0], [0.0, 0.0, 6.0]], ] ) expected = gs.array( [ [ [7.38905609, 0.0, 0.0], [0.0, 20.0855369, 0.0], [0.0, 0.0, 54.5981500], ], [ [2.718281828, 0.0, 0.0], [0.0, 148.413159, 0.0], [0.0, 0.0, 403.42879349], ], ] ) expected = gs.cast(expected, gs.float64) point = gs.cast(point, gs.float64) result = self.group.exp(point) self.assertAllClose(result, expected)
def projection(self, point, atol=gs.atol): """Project a point in ambient space to the parameter set. The parameter is floored to `gs.atol` if it is negative and to '1-gs.atol' if it is greater than 1. Parameters ---------- point : array-like, shape=[...,] Point in ambient space. atol : float Tolerance to evaluate positivity. Returns ------- projected : array-like, shape=[...,] Projected point. """ point = gs.cast(gs.array(point), dtype=gs.float32) projected = gs.where( gs.logical_or(point < atol, point > 1 - atol), (1 - atol) * gs.cast( (point > 1 - atol), gs.float32) + atol * gs.cast( (point < atol), gs.float32), point, ) return gs.squeeze(projected)
def _exp_translation_transform(self, rot_vec): n_samples = rot_vec.shape[0] base_1 = gs.array([gs.eye(2)] * n_samples) base_2 = -gs.array( [self.rotations.skew_matrix_from_vector(gs.ones(1))] * n_samples) mask_close_0 = gs.isclose(rot_vec, 0.) mask_else = ~mask_close_0 mask_close_0_float = gs.cast(mask_close_0, gs.float32) mask_else_float = gs.cast(mask_else, gs.float32) cos_coef = gs.zeros_like(rot_vec) sin_coef = gs.zeros_like(rot_vec) cos_coef += mask_close_0_float * ( TAYLOR_COEFFS_1_AT_0[0] * rot_vec + TAYLOR_COEFFS_1_AT_0[1] * rot_vec ** 3 + TAYLOR_COEFFS_1_AT_0[2] * rot_vec ** 5) sin_coef += mask_close_0_float * ( 1 - TAYLOR_COEFFS_2_AT_0[0] * rot_vec ** 2 - TAYLOR_COEFFS_2_AT_0[2] * rot_vec ** 4) rot_vec = rot_vec + mask_close_0_float * 1e-6 cos_coef += mask_else_float * ((1. - gs.cos(rot_vec)) / rot_vec) sin_coef += mask_else_float * (gs.sin(rot_vec) / rot_vec) sin_term = gs.einsum('...i,...jk->...jk', sin_coef, base_1) cos_term = gs.einsum('...i,...jk->...jk', cos_coef, base_2) transform = sin_term + cos_term return transform
def inner_product(self, tangent_vec_a, tangent_vec_b, base_point=None): """ Inner product between two tangent vectors at a base point. """ tangent_vec_a = gs.to_ndarray(tangent_vec_a, to_ndim=2) tangent_vec_b = gs.to_ndarray(tangent_vec_b, to_ndim=2) n_tangent_vec_a = gs.shape(tangent_vec_a)[0] n_tangent_vec_b = gs.shape(tangent_vec_b)[0] inner_prod_mat = self.inner_product_matrix(base_point) inner_prod_mat = gs.to_ndarray(inner_prod_mat, to_ndim=3) n_mats = gs.shape(inner_prod_mat)[0] n_inner_prod = gs.maximum(n_tangent_vec_a, n_tangent_vec_b) n_inner_prod = gs.maximum(n_inner_prod, n_mats) n_tiles_a = gs.divide(n_inner_prod, n_tangent_vec_a) n_tiles_a = gs.cast(n_tiles_a, gs.int32) tangent_vec_a = gs.tile(tangent_vec_a, [n_tiles_a, 1]) n_tiles_b = gs.divide(n_inner_prod, n_tangent_vec_b) n_tiles_b = gs.cast(n_tiles_b, gs.int32) tangent_vec_b = gs.tile(tangent_vec_b, [n_tiles_b, 1]) n_tiles_mat = gs.divide(n_inner_prod, n_mats) n_tiles_mat = gs.cast(n_tiles_mat, gs.int32) inner_prod_mat = gs.tile(inner_prod_mat, [n_tiles_mat, 1, 1]) aux = gs.einsum('nj,njk->nk', tangent_vec_a, inner_prod_mat) inner_prod = gs.einsum('nk,nk->n', aux, tangent_vec_b) inner_prod = gs.to_ndarray(inner_prod, to_ndim=2, axis=1) assert gs.ndim(inner_prod) == 2, inner_prod.shape return inner_prod
def test_exp(self, n, tangent_vec, base_point, expected): group = self.space(n) expected = gs.cast(gs.array(expected), gs.float64) tangent_vec = gs.cast(gs.array(tangent_vec), gs.float64) base_point = (None if base_point is None else gs.cast( gs.array(base_point), gs.float64)) self.assertAllClose(group.exp(tangent_vec, base_point), gs.array(expected))
def _log_translation_transform(self, rot_vec): """Compute matrix associated to rot_vec for the translation part in log. Parameters ---------- rot_vec : array-like, shape=[..., 3] Returns ------- transform : array-like, shape=[..., 3, 3] Matrix to be applied to the translation part in log """ n_samples = rot_vec.shape[0] angle = gs.linalg.norm(rot_vec, axis=1) angle = gs.to_ndarray(angle, to_ndim=2, axis=1) skew_mat = self.rotations.skew_matrix_from_vector(rot_vec) sq_skew_mat = gs.matmul(skew_mat, skew_mat) mask_close_0 = gs.isclose(angle, 0.0) mask_close_pi = gs.isclose(angle, gs.pi) mask_else = ~mask_close_0 & ~mask_close_pi mask_close_0_float = gs.cast(mask_close_0, gs.float32) mask_close_pi_float = gs.cast(mask_close_pi, gs.float32) mask_else_float = gs.cast(mask_else, gs.float32) mask_0 = gs.isclose(angle, 0.0, atol=1e-7) mask_0_float = gs.cast(mask_0, gs.float32) angle += mask_0_float * gs.ones_like(angle) coef_1 = -0.5 * gs.ones_like(angle) coef_2 = gs.zeros_like(angle) coef_2 += mask_close_0_float * (1.0 / 12.0 + angle**2 / 720.0 + angle**4 / 30240.0 + angle**6 / 1209600.0) delta_angle = angle - gs.pi coef_2 += mask_close_pi_float * ( 1.0 / PI2 + (PI2 - 8.0) * delta_angle / (4.0 * PI3) - ((PI2 - 12.0) * delta_angle**2 / (4.0 * PI4)) + ((-192.0 + 12.0 * PI2 + PI4) * delta_angle**3 / (48.0 * PI5)) - ((-240.0 + 12.0 * PI2 + PI4) * delta_angle**4 / (48.0 * PI6)) + ((-2880.0 + 120.0 * PI2 + 10.0 * PI4 + PI6) * delta_angle**5 / (480.0 * PI7)) - ((-3360 + 120.0 * PI2 + 10.0 * PI4 + PI6) * delta_angle**6 / (480.0 * PI8))) psi = 0.5 * angle * gs.sin(angle) / (1 - gs.cos(angle)) coef_2 += mask_else_float * (1 - psi) / (angle**2) term_1 = gs.einsum("...i,...ij->...ij", coef_1, skew_mat) term_2 = gs.einsum("...i,...ij->...ij", coef_2, sq_skew_mat) term_id = gs.array([gs.eye(3)] * n_samples) transform = term_id + term_1 + term_2 return transform
def regularize_tangent_vec_at_identity( self, tangent_vec, metric=None): """Regularize a tangent vector at the identify. In 3D, regularize a tangent_vector by getting its norm at the identity, determined by the metric, to be less than pi. Parameters ---------- tangent_vec : array-like, shape=[..., 3]] metric : RiemannianMetric, optional default: self.left_canonical_metric Returns ------- regularized_vec : array-like, shape=[..., 3]] """ if metric is None: metric = self.left_canonical_metric tangent_vec_metric_norm = metric.norm(tangent_vec) tangent_vec_canonical_norm = gs.linalg.norm(tangent_vec, axis=-1) mask_norm_0 = gs.isclose(tangent_vec_metric_norm, 0.) mask_canonical_norm_0 = gs.isclose(tangent_vec_canonical_norm, 0.) mask_0 = mask_norm_0 | mask_canonical_norm_0 mask_else = ~mask_0 # This avoids division by 0. mask_0_float = gs.cast(mask_0, gs.float32) + self.epsilon mask_else_float = gs.cast(mask_else, gs.float32) + self.epsilon regularized_vec = gs.zeros_like(tangent_vec) regularized_vec += gs.einsum( '...,...i->...i', mask_0_float, tangent_vec) tangent_vec_canonical_norm += mask_0_float coef = gs.zeros_like(tangent_vec_metric_norm) coef += mask_else_float * ( tangent_vec_metric_norm / tangent_vec_canonical_norm) coef_tangent_vec = gs.einsum( '...,...i->...i', coef, tangent_vec) regularized_vec += gs.einsum( '...,...i->...i', mask_else_float, self.regularize(coef_tangent_vec)) coef += mask_0_float regularized_vec = gs.einsum( '...,...i->...i', 1. / coef, regularized_vec) regularized_vec = gs.einsum( '...,...i->...i', mask_else_float, regularized_vec) return regularized_vec
def _exp_translation_transform(self, rot_vec): """Compute matrix associated to rot_vec for the translation part in exp. Parameters ---------- rot_vec : array-like, shape=[..., 3] Returns ------- transform : array-like, shape=[..., 3, 3] Matrix to be applied to the translation part in exp. """ n_samples = rot_vec.shape[0] angle = gs.linalg.norm(rot_vec, axis=-1) angle = gs.to_ndarray(angle, to_ndim=2, axis=1) skew_mat = self.rotations.skew_matrix_from_vector(rot_vec) sq_skew_mat = gs.matmul(skew_mat, skew_mat) mask_0 = gs.equal(angle, 0.) mask_close_0 = gs.isclose(angle, 0.) & ~mask_0 mask_else = ~mask_0 & ~mask_close_0 mask_0_float = gs.cast(mask_0, gs.float32) mask_close_0_float = gs.cast(mask_close_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. / 2. * gs.ones_like(angle) coef_2 += mask_0_float * 1. / 6. * gs.ones_like(angle) coef_1 += mask_close_0_float * ( TAYLOR_COEFFS_1_AT_0[0] + TAYLOR_COEFFS_1_AT_0[2] * angle ** 2 + TAYLOR_COEFFS_1_AT_0[4] * angle ** 4 + TAYLOR_COEFFS_1_AT_0[6] * angle ** 6) coef_2 += mask_close_0_float * ( TAYLOR_COEFFS_2_AT_0[0] + TAYLOR_COEFFS_2_AT_0[2] * angle ** 2 + TAYLOR_COEFFS_2_AT_0[4] * angle ** 4 + TAYLOR_COEFFS_2_AT_0[6] * angle ** 6) angle += mask_0_float * 1. coef_1 += mask_else_float * ((1. - gs.cos(angle)) / angle ** 2) coef_2 += mask_else_float * ((angle - gs.sin(angle)) / angle ** 3) term_1 = gs.einsum('...i,...ij->...ij', coef_1, skew_mat) term_2 = gs.einsum('...i,...ij->...ij', coef_2, sq_skew_mat) term_id = gs.array([gs.eye(3)] * n_samples) transform = term_id + term_1 + term_2 return transform
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) sq_norm_tangent_vec = self.embedding_metric.squared_norm( tangent_vec) norm_tangent_vec = gs.sqrt(sq_norm_tangent_vec) mask_0 = gs.isclose(sq_norm_tangent_vec, 0.) mask_0 = gs.to_ndarray(mask_0, to_ndim=1) mask_else = ~mask_0 mask_else = gs.to_ndarray(mask_else, to_ndim=1) mask_0_float = gs.cast(mask_0, gs.float32) mask_else_float = gs.cast(mask_else, gs.float32) coef_1 = gs.zeros_like(norm_tangent_vec) coef_2 = gs.zeros_like(norm_tangent_vec) coef_1 += mask_0_float * ( 1. + COSH_TAYLOR_COEFFS[2] * norm_tangent_vec ** 2 + COSH_TAYLOR_COEFFS[4] * norm_tangent_vec ** 4 + COSH_TAYLOR_COEFFS[6] * norm_tangent_vec ** 6 + COSH_TAYLOR_COEFFS[8] * norm_tangent_vec ** 8) coef_2 += mask_0_float * ( 1. + SINH_TAYLOR_COEFFS[3] * norm_tangent_vec ** 2 + SINH_TAYLOR_COEFFS[5] * norm_tangent_vec ** 4 + SINH_TAYLOR_COEFFS[7] * norm_tangent_vec ** 6 + SINH_TAYLOR_COEFFS[9] * norm_tangent_vec ** 8) # This avoids dividing by 0. norm_tangent_vec += mask_0_float * 1.0 coef_1 += mask_else_float * (gs.cosh(norm_tangent_vec)) coef_2 += mask_else_float * ( (gs.sinh(norm_tangent_vec) / (norm_tangent_vec))) exp = (gs.einsum('ni,nj->nj', coef_1, base_point) + gs.einsum('ni,nj->nj', coef_2, tangent_vec)) hyperbolic_space = HyperbolicSpace(dimension=self.dimension) exp = hyperbolic_space.regularize(exp) return exp
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 in hyperbolic space. Returns ------- exp : array-like, shape=[..., dim + 1] Point in hyperbolic space equal to the Riemannian exponential of tangent_vec at the base point. """ sq_norm_tangent_vec = self.embedding_metric.squared_norm( tangent_vec) sq_norm_tangent_vec = gs.clip(sq_norm_tangent_vec, 0, math.inf) norm_tangent_vec = gs.sqrt(sq_norm_tangent_vec) mask_0 = gs.isclose(sq_norm_tangent_vec, 0.) mask_0 = gs.to_ndarray(mask_0, to_ndim=1) mask_else = ~mask_0 mask_else = gs.to_ndarray(mask_else, to_ndim=1) mask_0_float = gs.cast(mask_0, gs.float32) mask_else_float = gs.cast(mask_else, gs.float32) coef_1 = gs.zeros_like(norm_tangent_vec) coef_2 = gs.zeros_like(norm_tangent_vec) coef_1 += mask_0_float * ( 1. + COSH_TAYLOR_COEFFS[2] * norm_tangent_vec ** 2 + COSH_TAYLOR_COEFFS[4] * norm_tangent_vec ** 4 + COSH_TAYLOR_COEFFS[6] * norm_tangent_vec ** 6 + COSH_TAYLOR_COEFFS[8] * norm_tangent_vec ** 8) coef_2 += mask_0_float * ( 1. + SINH_TAYLOR_COEFFS[3] * norm_tangent_vec ** 2 + SINH_TAYLOR_COEFFS[5] * norm_tangent_vec ** 4 + SINH_TAYLOR_COEFFS[7] * norm_tangent_vec ** 6 + SINH_TAYLOR_COEFFS[9] * norm_tangent_vec ** 8) # This avoids dividing by 0. norm_tangent_vec += mask_0_float * 1.0 coef_1 += mask_else_float * (gs.cosh(norm_tangent_vec)) coef_2 += mask_else_float * ( (gs.sinh(norm_tangent_vec) / (norm_tangent_vec))) exp = ( gs.einsum('...,...j->...j', coef_1, base_point) + gs.einsum('...,...j->...j', coef_2, tangent_vec)) hyperbolic_space = Hyperboloid(dim=self.dim) exp = hyperbolic_space.regularize(exp) return exp
def test_powerm(self): """Test of powerm method.""" sym_n = SymmetricMatrices(self.n) expected = gs.array( [[[1, 1. / 4., 0.], [1. / 4, 2., 0.], [0., 0., 1.]]]) expected = gs.cast(expected, gs.float64) power = gs.array(1. / 2) power = gs.cast(power, gs.float64) result = sym_n.powerm(expected, power) result = gs.matmul(result, gs.transpose(result, (0, 2, 1))) self.assertAllClose(result, expected)
def log(self, point, base_point): """ 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) angle = self.dist(base_point, point) angle = gs.to_ndarray(angle, to_ndim=1) angle = gs.to_ndarray(angle, to_ndim=2) mask_0 = gs.isclose(angle, 0.) mask_else = ~mask_0 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_SINH_TAYLOR_COEFFS[1] * angle ** 2 + INV_SINH_TAYLOR_COEFFS[3] * angle ** 4 + INV_SINH_TAYLOR_COEFFS[5] * angle ** 6 + INV_SINH_TAYLOR_COEFFS[7] * angle ** 8) coef_2 += mask_0_float * ( 1. + INV_TANH_TAYLOR_COEFFS[1] * angle ** 2 + INV_TANH_TAYLOR_COEFFS[3] * angle ** 4 + INV_TANH_TAYLOR_COEFFS[5] * angle ** 6 + INV_TANH_TAYLOR_COEFFS[7] * angle ** 8) # This avoids dividing by 0. angle += mask_0_float * 1. coef_1 += mask_else_float * (angle / gs.sinh(angle)) coef_2 += mask_else_float * (angle / gs.tanh(angle)) log = (gs.einsum('ni,nj->nj', coef_1, point) - gs.einsum('ni,nj->nj', coef_2, base_point)) return log
def log(self, point, base_point): """Compute Riemannian logarithm of a point wrt a base point. If point_type = 'poincare' then base_point belongs to the Poincare ball and point is a vector in the Euclidean space of the same dimension as the ball. Parameters ---------- point : array-like, shape=[..., dim + 1] Point in hyperbolic space. base_point : array-like, shape=[..., dim + 1] Point in hyperbolic space. Returns ------- log : array-like, shape=[..., dim + 1] Tangent vector at the base point equal to the Riemannian logarithm of point at the base point. """ angle = self.dist(base_point, point) / self.scale angle = gs.to_ndarray(angle, to_ndim=1) mask_0 = gs.isclose(angle, 0.) mask_else = ~mask_0 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_SINH_TAYLOR_COEFFS[1] * angle ** 2 + INV_SINH_TAYLOR_COEFFS[3] * angle ** 4 + INV_SINH_TAYLOR_COEFFS[5] * angle ** 6 + INV_SINH_TAYLOR_COEFFS[7] * angle ** 8) coef_2 += mask_0_float * ( 1. + INV_TANH_TAYLOR_COEFFS[1] * angle ** 2 + INV_TANH_TAYLOR_COEFFS[3] * angle ** 4 + INV_TANH_TAYLOR_COEFFS[5] * angle ** 6 + INV_TANH_TAYLOR_COEFFS[7] * angle ** 8) # This avoids dividing by 0. angle += mask_0_float * 1. coef_1 += mask_else_float * (angle / gs.sinh(angle)) coef_2 += mask_else_float * (angle / gs.tanh(angle)) log_term_1 = gs.einsum('...,...j->...j', coef_1, point) log_term_2 = - gs.einsum('...,...j->...j', coef_2, base_point) log = log_term_1 + log_term_2 return log
def matrix_from_rotation_vector(self, rot_vec): """Convert rotation vector to rotation matrix. Parameters ---------- rot_vec: array-like, shape=[..., 3] Returns ------- rot_mat: array-like, shape=[..., 3] """ rot_vec = self.regularize(rot_vec) angle = gs.linalg.norm(rot_vec, axis=1) angle = gs.to_ndarray(angle, to_ndim=2, axis=1) skew_rot_vec = self.skew_matrix_from_vector(rot_vec) coef_1 = gs.zeros_like(angle) coef_2 = gs.zeros_like(angle) # This avoids dividing by 0. mask_0 = gs.isclose(angle, 0.) mask_0_float = gs.cast(mask_0, gs.float32) + self.epsilon coef_1 += mask_0_float * (1. - (angle ** 2) / 6.) coef_2 += mask_0_float * (1. / 2. - angle ** 2) # This avoids dividing by 0. mask_else = ~mask_0 mask_else_float = gs.cast(mask_else, gs.float32) + self.epsilon angle += mask_0_float coef_1 += mask_else_float * (gs.sin(angle) / angle) coef_2 += mask_else_float * ( (1. - gs.cos(angle)) / (angle ** 2)) coef_1 = gs.squeeze(coef_1, axis=1) coef_2 = gs.squeeze(coef_2, axis=1) term_1 = (gs.eye(self.dim) + gs.einsum('n,njk->njk', coef_1, skew_rot_vec)) squared_skew_rot_vec = gs.einsum( 'nij,njk->nik', skew_rot_vec, skew_rot_vec) term_2 = gs.einsum('n,njk->njk', coef_2, squared_skew_rot_vec) return term_1 + term_2
def test_log_and_exp_general_case_general_dim(self): """ Test that the Riemannian exponential and the Riemannian logarithm are inverse. Expect their composition to give the identity function. """ # Riemannian Log then Riemannian Exp dim = 5 n_samples = self.n_samples h5 = Hyperboloid(dim=dim) h5_metric = h5.metric base_point = h5.random_point() point = h5.random_point() point = gs.cast(point, gs.float64) base_point = gs.cast(base_point, gs.float64) one_log = h5_metric.log(point=point, base_point=base_point) result = h5_metric.exp(tangent_vec=one_log, base_point=base_point) expected = point self.assertAllClose(result, expected) # Test vectorization of log base_point = gs.stack([base_point] * n_samples, axis=0) point = gs.stack([point] * n_samples, axis=0) expected = gs.stack([one_log] * n_samples, axis=0) log = h5_metric.log(point=point, base_point=base_point) result = log self.assertAllClose(gs.shape(result), (n_samples, dim + 1)) self.assertAllClose(result, expected) result = h5_metric.exp(tangent_vec=log, base_point=base_point) expected = point self.assertAllClose(gs.shape(result), (n_samples, dim + 1)) self.assertAllClose(result, expected) # Test vectorization of exp tangent_vec = gs.stack([one_log] * n_samples, axis=0) exp = h5_metric.exp(tangent_vec=tangent_vec, base_point=base_point) result = exp expected = point self.assertAllClose(gs.shape(result), (n_samples, dim + 1)) self.assertAllClose(result, expected)
def test_squared_dist_bureswasserstein_vectorization(self): """Test of SPDMetricBuresWasserstein.squared_dist method.""" point_a = self.space.random_point(2) point_b = gs.array([[9., 0., 0.], [0., 5., 0.], [0., 0., 1.]]) point_a = gs.cast(point_a, gs.float64) point_b = gs.cast(point_b, gs.float64) metric = self.metric_bureswasserstein result = metric.squared_dist(point_a, point_b) log = metric.log(point=point_b, base_point=point_a) expected = metric.squared_norm(vector=log, base_point=point_a) self.assertAllClose(result, expected)
def regularize(self, point): """Regularize a point to be in accordance with convention. In 3D, regularize the norm of the rotation vector, to be between 0 and pi, following the axis-angle representation's convention. If the angle angle is between pi and 2pi, the function computes its complementary in 2pi and inverts the direction of the rotation axis. Parameters ---------- point : array-like, shape=[...,3] Returns ------- regularized_point : array-like, shape=[..., 3] """ regularized_point = point angle = gs.linalg.norm(regularized_point, axis=-1) mask_0 = gs.isclose(angle, 0.) mask_not_0 = ~mask_0 mask_pi = gs.isclose(angle, gs.pi) # This avoids division by 0. mask_0_float = gs.cast(mask_0, gs.float32) + self.epsilon mask_not_0_float = ( gs.cast(mask_not_0, gs.float32) + self.epsilon) mask_pi_float = gs.cast(mask_pi, gs.float32) + self.epsilon k = gs.floor(angle / (2 * gs.pi) + .5) angle += mask_0_float norms_ratio = gs.zeros_like(angle) norms_ratio += mask_not_0_float * ( 1. - 2. * gs.pi * k / angle) norms_ratio += mask_0_float norms_ratio += mask_pi_float * ( gs.pi / angle - (1. - 2. * gs.pi * k / angle)) regularized_point = gs.einsum( '...,...i->...i', norms_ratio, regularized_point) return regularized_point
def square_root_velocity(self, curve): """Compute the square root velocity representation of a curve. The velocity is computed using the log map. The case of several curves is handled through vectorization. In that case, an index selection procedure allows to get rid of the log between the end point of curve[k, :, :] and the starting point of curve[k + 1, :, :]. Parameters ---------- curve : Returns ------- srv : """ curve = gs.to_ndarray(curve, to_ndim=3) n_curves, n_sampling_points, n_coords = curve.shape srv_shape = (n_curves, n_sampling_points - 1, n_coords) curve = gs.reshape(curve, (n_curves * n_sampling_points, n_coords)) coef = gs.cast(gs.array(n_sampling_points - 1), gs.float32) velocity = coef * self.ambient_metric.log(point=curve[1:, :], base_point=curve[:-1, :]) velocity_norm = self.ambient_metric.norm(velocity, curve[:-1, :]) srv = velocity / gs.sqrt(velocity_norm) index = gs.arange(n_curves * n_sampling_points - 1) mask = ~gs.equal((index + 1) % n_sampling_points, 0) index_select = gs.gather(index, gs.squeeze(gs.where(mask))) srv = gs.reshape(gs.gather(srv, index_select), srv_shape) return srv
def apply_func_to_eigvals(x, function, check_positive=False): """ Apply function to eigenvalues and reconstruct the matrix. Parameters ---------- x : array_like, shape=[..., n, n] Symmetric matrix. function : callable Function to apply to eigenvalues. Returns ------- x : array_like, shape=[..., n, n] Symmetric matrix. """ eigvals, eigvecs = gs.linalg.eigh(x) if check_positive: if gs.any(gs.cast(eigvals, gs.float32) < 0.): logging.warning('Negative eigenvalue encountered in' ' {}'.format(function.__name__)) eigvals = function(eigvals) eigvals = algebra_utils.from_vector_to_diagonal_matrix(eigvals) transp_eigvecs = Matrices.transpose(eigvecs) reconstuction = gs.matmul(eigvecs, eigvals) reconstuction = gs.matmul(reconstuction, transp_eigvecs) return reconstuction
def flip_determinant(matrix, det): """Change sign of the determinant if it is negative. For a batch of matrices, multiply the matrices which have negative determinant by a diagonal matrix :math: `diag(1,...,1,-1) from the right. This changes the sign of the last column of the matrix. Parameters ---------- matrix : array-like, shape=[...,n ,m] Matrix to transform. det : array-like, shape=[...] Determinant of matrix, or any other scalar to use as threshold to determine whether to change the sign of the last column of matrix. Returns ------- matrix_flipped : array-like, shape=[..., n, m] Matrix with the sign of last column changed if det < 0. """ if gs.any(det < 0): ones = gs.ones(matrix.shape[-1]) reflection_vec = gs.concatenate([ones[:-1], gs.array([-1.0])], axis=0) mask = gs.cast(det < 0, matrix.dtype) sign = mask[..., None] * reflection_vec + (1.0 - mask)[..., None] * ones return gs.einsum("...ij,...j->...ij", matrix, sign) return matrix