def is_tangent(self, vector, base_point=None, atol=gs.atol): r"""Check if a vector is tangent to the manifold at the base point. Check if the (n,n)-matrix :math: `Y` is symmetric and verifies the relation :math: PY + YP = Y where :math: `P` represents the base point and :math: `Y` the vector. Parameters ---------- vector : array-like, shape=[..., n, n] Matrix to be checked. base_point : array-like, shape=[..., n, n] Base point. atol : int Optional, default: 1e-5. Returns ------- belongs : array-like, shape=[...,] Boolean evaluating if `vector` is tangent to the Grassmannian at `base_point`. """ diff = Matrices.mul(base_point, vector) + Matrices.mul( vector, base_point) - vector is_close = gs.all(gs.isclose(diff, 0., atol=atol)) return gs.logical_and(Matrices.is_symmetric(vector), is_close)
def exp(self, tangent_vec, base_point, **kwargs): """Compute the Riemannian exponential of a tangent vector. Parameters ---------- tangent_vec : array-like, shape=[..., n, p] Tangent vector at a base point. base_point : array-like, shape=[..., n, p] Point in the Stiefel manifold. Returns ------- exp : array-like, shape=[..., n, p] Point in the Stiefel manifold equal to the Riemannian exponential of tangent_vec at the base point. """ p = self.p matrix_a = Matrices.mul(Matrices.transpose(base_point), tangent_vec) matrix_k = tangent_vec - Matrices.mul(base_point, matrix_a) matrix_q, matrix_r = gs.linalg.qr(matrix_k) matrix_ar = gs.concatenate([matrix_a, -Matrices.transpose(matrix_r)], axis=-1) zeros = gs.zeros_like(tangent_vec)[..., :p, :p] matrix_rz = gs.concatenate([matrix_r, zeros], axis=-1) block = gs.concatenate([matrix_ar, matrix_rz], axis=-2) matrix_mn_e = gs.linalg.expm(block) exp = Matrices.mul(base_point, matrix_mn_e[..., :p, :p]) + Matrices.mul( matrix_q, matrix_mn_e[..., p:, :p]) return exp
def random_uniform(self, n_samples=1): """Sample random points from a uniform distribution. Following [Chikuse03]_, :math: `n_samples * n * k` scalars are sampled from a standard normal distribution and reshaped to matrices, the projectors on their first k columns follow a uniform distribution. Parameters ---------- n_samples : int The number of points to sample Optional. default: 1. Returns ------- projectors : array-like, shape=[..., n, n] Points following a uniform distribution. References ---------- .. [Chikuse03] Yasuko Chikuse, Statistics on special manifolds, New York: Springer-Verlag. 2003, 10.1007/978-0-387-21540-2 """ points = gs.random.normal(size=(n_samples, self.n, self.k)) full_rank = Matrices.mul(Matrices.transpose(points), points) projector = Matrices.mul(points, GeneralLinear.inverse(full_rank), Matrices.transpose(points)) return projector[0] if n_samples == 1 else projector
def submersion(point, k): r"""Submersion that defines the Grassmann manifold. The Grassmann manifold is defined here as embedded in the set of symmetric matrices, as the pre-image of the function defined around the projector on the space spanned by the first k columns of the identity matrix by (see Exercise E.25 in [Pau07]_). .. math: \begin{pmatrix} I_k + A & B^T \\ B & D \end{pmatrix} \mapsto (D - B(I_k + A)^{-1}B^T, A + A^2 + B^TB This map is a submersion and its zero space is the set of orthogonal rank-k projectors. References ---------- .. [Pau07] Paulin, Frédéric. “Géométrie différentielle élémentaire,” 2007. https://www.imo.universite-paris-saclay.fr/~paulin /notescours/cours_geodiff.pdf. """ _, eigvecs = gs.linalg.eigh(point) eigvecs = gs.flip(eigvecs, -1) flipped_point = Matrices.mul(Matrices.transpose(eigvecs), point, eigvecs) b = flipped_point[..., k:, :k] d = flipped_point[..., k:, k:] a = flipped_point[..., :k, :k] - gs.eye(k) first = d - Matrices.mul(b, GeneralLinear.inverse(a + gs.eye(k)), Matrices.transpose(b)) second = a + Matrices.mul(a, a) + Matrices.mul(Matrices.transpose(b), b) row_1 = gs.concatenate([first, gs.zeros_like(b)], axis=-1) row_2 = gs.concatenate([Matrices.transpose(gs.zeros_like(b)), second], axis=-1) return gs.concatenate([row_1, row_2], axis=-2)
def lie_bracket(self, tangent_vector_a, tangent_vector_b, base_point=None): """Compute the lie bracket of two tangent vectors. For matrix Lie groups with tangent vectors A,B at the same base point P this is given by (translate to identity, compute commutator, go back) :math:`[A,B] = A_P^{-1}B - B_P^{-1}A` Parameters ---------- tangent_vector_a : array-like, shape=[..., n, n] Tangent vector at base point. tangent_vector_b : array-like, shape=[..., n, n] Tangent vector at base point. base_point : array-like, shape=[..., n, n] Base point. Returns ------- bracket : array-like, shape=[..., n, n] Lie bracket. """ if base_point is None: base_point = self.get_identity(point_type=self.default_point_type) inverse_base_point = self.inverse(base_point) first_term = Matrices.mul(inverse_base_point, tangent_vector_b) first_term = Matrices.mul(tangent_vector_a, first_term) second_term = Matrices.mul(inverse_base_point, tangent_vector_a) second_term = Matrices.mul(tangent_vector_b, second_term) return first_term - second_term
def compute_gain(self, observation): """Compute the Kalman gain given the observation model. Given the observation Jacobian H and covariance N (not necessarily equal to that of the sensor), and the current covariance P, the Kalman gain is K = P H^T(H P H^T + N)^{-1}. Parameters ---------- observation : array-like, shape=[dim_obs] Obtained measurement. Returns ------- gain : array-like, shape=[model.dim, model.dim_obs] Kalman gain. """ obs_cov = self.model.get_measurement_noise_cov(self.state, self.measurement_noise) obs_jac = self.model.observation_jacobian(self.state, observation) expected_cov = Matrices.mul(obs_jac, self.covariance, Matrices.transpose(obs_jac)) innovation_cov = expected_cov + obs_cov return Matrices.mul(self.covariance, Matrices.transpose(obs_jac), gs.linalg.inv(innovation_cov))
def log(self, point, base_point, **kwargs): """Compute the Bures-Wasserstein logarithm map. Compute the Riemannian logarithm at point base_point, of point wrt the Bures-Wasserstein metric. This gives a tangent vector at point base_point. Parameters ---------- point : array-like, shape=[..., n, n] Point. base_point : array-like, shape=[..., n, n] Base point. Returns ------- log : array-like, shape=[..., n, n] Riemannian logarithm. """ # compute B^1/2(B^-1/2 A B^-1/2)B^-1/2 instead of sqrtm(AB^-1) sqrt_bp, inv_sqrt_bp = SymmetricMatrices.powerm( base_point, [0.5, -0.5]) pdt = SymmetricMatrices.powerm(Matrices.mul(sqrt_bp, point, sqrt_bp), 0.5) sqrt_product = Matrices.mul(sqrt_bp, pdt, inv_sqrt_bp) transp_sqrt_product = Matrices.transpose(sqrt_product) return sqrt_product + transp_sqrt_product - 2 * base_point
def random_uniform(self, n_samples=1): r"""Sample on St(n,p) from the uniform distribution. If :math:`Z(p,n) \sim N(0,1)`, then :math:`St(n,p) \sim U`, according to Haar measure: :math:`St(n,p) := Z(Z^TZ)^{-1/2}`. Parameters ---------- n_samples : int Number of samples. Optional, default: 1. Returns ------- samples : array-like, shape=[..., n, p] Samples on the Stiefel manifold. """ n, p = self.n, self.p size = (n_samples, n, p) if n_samples != 1 else (n, p) std_normal = gs.random.normal(size=size) std_normal_transpose = Matrices.transpose(std_normal) aux = Matrices.mul(std_normal_transpose, std_normal) inv_sqrt_aux = SymmetricMatrices.powerm(aux, -1.0 / 2) samples = Matrices.mul(std_normal, inv_sqrt_aux) return samples
def exp(self, tangent_vec, base_point): """Compute the Bures-Wasserstein exponential map. Parameters ---------- tangent_vec : array-like, shape=[..., n, n] Tangent vector at base point. base_point : array-like, shape=[..., n, n] Base point. Returns ------- exp : array-like, shape=[...,] Riemannian exponential. """ eigvals, eigvecs = gs.linalg.eigh(base_point) transp_eigvecs = Matrices.transpose(eigvecs) rotated_tangent_vec = Matrices.mul(transp_eigvecs, tangent_vec, eigvecs) coefficients = 1 / (eigvals[..., :, None] + eigvals[..., None, :]) rotated_sylvester = rotated_tangent_vec * coefficients rotated_hessian = gs.einsum( '...ij,...j->...ij', rotated_sylvester, eigvals) rotated_hessian = Matrices.mul(rotated_hessian, rotated_sylvester) hessian = Matrices.mul(eigvecs, rotated_hessian, transp_eigvecs) return base_point + tangent_vec + hessian
def test_horizontal_projection(self, n, vec, mat): bundle = self.space(n) base = self.base(n) horizontal_vec = bundle.horizontal_projection(vec, mat) inverse = GeneralLinear.inverse(mat) product_1 = Matrices.mul(horizontal_vec, inverse) product_2 = Matrices.mul(inverse, horizontal_vec) is_horizontal = gs.all( base.is_tangent(product_1 + product_2, mat, atol=gs.atol * 10)) self.assertTrue(is_horizontal)
def test_horizontal_projection(self): mat = self.bundle.random_point() vec = self.bundle.random_point() horizontal_vec = self.bundle.horizontal_projection(vec, mat) inverse = GeneralLinear.inverse(mat) product_1 = Matrices.mul(horizontal_vec, inverse) product_2 = Matrices.mul(inverse, horizontal_vec) is_horizontal = self.base.is_tangent( product_1 + product_2, mat, atol=gs.atol * 10) self.assertTrue(is_horizontal)
def test_to_tangent_vec_vectorization(self): n = self.group.n tangent_vecs = gs.arange(self.n_samples * (n + 1) ** 2) tangent_vecs = gs.cast(tangent_vecs, gs.float32) tangent_vecs = gs.reshape(tangent_vecs, (self.n_samples,) + (n + 1,) * 2) point = self.group.random_point(self.n_samples) tangent_vecs = Matrices.mul(point, tangent_vecs) regularized = self.group.to_tangent(tangent_vecs, point) result = Matrices.mul(Matrices.transpose(point), regularized) + Matrices.mul( Matrices.transpose(regularized), point ) result = result[:, :n, :n] expected = gs.zeros_like(result) self.assertAllClose(result, expected)
def propagate(state, sensor_input): r"""Propagate state with constant velocity motion model on SE(2). From a given state (orientation, position) pair :math:`(\theta, x)`, a new one is obtained as :math:`(\theta + dt * \omega, x + dt * R(\theta) u)`, where the time step, the linear and angular velocities u and :math:\omega are given some sensor (e.g., odometers). Parameters ---------- state : array-like, shape=[dim] Vector representing a state (orientation, position). sensor_input : array-like, shape=[4] Vector representing the information from the sensor. Returns ------- new_state : array-like, shape=[dim] Vector representing the propagated state. """ dt, linear_vel, angular_vel = Localization.preprocess_input( sensor_input) theta, _, _ = state local_vel = Matrices.mul(Localization.rotation_matrix(theta), linear_vel) new_pos = state[1:] + dt * local_vel theta = theta + dt * angular_vel theta = Localization.regularize_angle(theta) return gs.concatenate((theta, new_pos), axis=0)
def differential_log(cls, tangent_vec, base_point): """Compute the differential of the matrix logarithm. Compute the differential of the matrix logarithm on SPD matrices at base_point applied to tangent_vec. Parameters ---------- tangent_vec : array_like, shape=[..., n, n] Tangent vector at base point. base_point : array_like, shape=[..., n, n] Base point. Returns ------- differential_log : array-like, shape=[..., n, n] Differential of the matrix logarithm. """ ( eigvectors, transp_eigvectors, numerator, denominator, temp_result, ) = cls.aux_differential_power(0, tangent_vec, base_point) power_operator = numerator / denominator result = power_operator * temp_result result = Matrices.mul(eigvectors, result, transp_eigvectors) return result
def test_horizontal_projection(self): mat = self.bundle.random_point() vec = self.bundle.random_point() horizontal_vec = self.bundle.horizontal_projection(vec, mat) product = Matrices.mul(horizontal_vec, GeneralLinear.inverse(mat)) is_horizontal = Matrices.is_symmetric(product) self.assertTrue(is_horizontal)
def metric_matrix(self, base_point=None): """Compute inner product matrix at the tangent space at a base point. Parameters ---------- base_point : array-like, shape=[..., dim], optional Point in the group (the default is identity). Returns ------- metric_mat : array-like, shape=[..., dim, dim] Metric matrix at base_point. """ if base_point is None: return self.metric_mat_at_identity base_point = self.group.regularize(base_point) jacobian = self.group.jacobian_translation( point=base_point, left_or_right=self.left_or_right) inv_jacobian = GeneralLinear.inverse(jacobian) inv_jacobian_transposed = Matrices.transpose(inv_jacobian) metric_mat = Matrices.mul( inv_jacobian_transposed, self.metric_mat_at_identity, inv_jacobian) return metric_mat
def _procrustes_preprocessing(p, matrix_v, matrix_m, matrix_n): """Procrustes preprocessing. Parameters ---------- matrix_v : array-like matrix_m : array-like matrix_n : array-like Returns ------- matrix_v : array-like """ [matrix_d, _, matrix_r] = gs.linalg.svd(matrix_v[..., p:, p:]) j_matrix = gs.eye(p) for i in range(1, p): matrix_rd = Matrices.mul( matrix_r, j_matrix, Matrices.transpose(matrix_d)) sub_matrix_v = gs.matmul(matrix_v[..., :, p:], matrix_rd) matrix_v = gs.concatenate([ gs.concatenate([matrix_m, matrix_n], axis=-2), sub_matrix_v], axis=-1) det = gs.linalg.det(matrix_v) if gs.all(det > 0): break ones = gs.ones(p) reflection_vec = gs.concatenate( [ones[:-i], gs.array([-1.] * i)], axis=0) mask = gs.cast(det < 0, gs.float32) sign = (mask[..., None] * reflection_vec + (1. - mask)[..., None] * ones) j_matrix = algebra_utils.from_vector_to_diagonal_matrix(sign) return matrix_v
def permute(self, graph_to_permute, permutation): r"""Permutation action applied to graph observation. Parameters ---------- graph_to_permute : list of Graph or array-like, shape=[..., n, n]. Input graphs to be permuted. permutation: array-like, shape=[..., n] Node permutations where in position i we have the value j meaning the node i should be permuted with node j. Returns ------- graphs_permuted : array-like, shape=[..., n, n] Graphs permuted. """ def _get_permutation_matrix(indices_): return gs.array_from_sparse( data=gs.ones(self.n_nodes, dtype=gs.int64), indices=list(zip(range(self.n_nodes), indices_)), target_shape=(self.n_nodes, self.n_nodes), ) if gs.ndim(permutation) == 1: perm_matrices = _get_permutation_matrix(permutation) else: perm_matrices = [] for indices_ in permutation: perm_matrices.append(_get_permutation_matrix(indices_)) perm_matrices = gs.stack(perm_matrices) return Matrices.mul(perm_matrices, graph_to_permute, Matrices.transpose(perm_matrices))
def random_tangent_vec(self, n_samples=1, base_point=None): """Sample on the tangent space of SPD(n) from the uniform distribution. Parameters ---------- n_samples : int Number of samples. Optional, default: 1. base_point : array-like, shape=[..., n, n] Base point of the tangent space. Optional, default: None. Returns ------- samples : array-like, shape=[..., n, n] Points sampled in the tangent space at base_point. """ n = self.n size = (n_samples, n, n) if n_samples != 1 else (n, n) if base_point is None: base_point = gs.eye(n) sqrt_base_point = gs.linalg.sqrtm(base_point) tangent_vec_at_id = 2 * gs.random.rand(*size) - 1 tangent_vec_at_id += Matrices.transpose(tangent_vec_at_id) tangent_vec = Matrices.mul( sqrt_base_point, tangent_vec_at_id, sqrt_base_point) return tangent_vec
def _procrustes_preprocessing(p, matrix_v, matrix_m, matrix_n): """Procrustes preprocessing. Parameters ---------- matrix_v : array-like matrix_m : array-like matrix_n : array-like Returns ------- matrix_v : array-like """ [matrix_d, _, matrix_r] = gs.linalg.svd(matrix_v[..., p:, p:]) matrix_v_final = gs.copy(matrix_v) for i in range(1, p + 1): matrix_rd = Matrices.mul(matrix_r, Matrices.transpose(matrix_d)) sub_matrix_v = gs.matmul(matrix_v[..., :, p:], matrix_rd) matrix_v_final = gs.concatenate( [gs.concatenate([matrix_m, matrix_n], axis=-2), sub_matrix_v], axis=-1) det = gs.linalg.det(matrix_v_final) if gs.all(det > 0): break ones = gs.ones(p) reflection_vec = gs.concatenate( [ones[:-i], gs.array([-1.0] * i)], axis=0) mask = gs.cast(det < 0, matrix_v.dtype) sign = mask[..., None] * reflection_vec + (1.0 - mask)[..., None] * ones matrix_d = gs.einsum("...ij,...i->...ij", Matrices.transpose(matrix_d), sign) return matrix_v_final
def parallel_transport(self, 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)`. As the special Euclidean group endowed with its canonical left-invariant metric is a symmetric space, parallel transport is achieved by a geodesic symmetry, or equivalently, one step of the pole ladder scheme. Parameters ---------- tangent_vec_a : array-like, shape=[..., n + 1, n + 1] Tangent vector at base point to be transported. tangent_vec_b : array-like, shape=[..., n + 1, n + 1] Tangent vector at base point, along which the parallel transport is computed. base_point : array-like, shape=[..., n + 1, n + 1] Point on the hypersphere. Returns ------- transported_tangent_vec: array-like, shape=[..., n + 1, n + 1] Transported tangent vector at `exp_(base_point)(tangent_vec_b)`. """ midpoint = self.exp(1. / 2. * tangent_vec_b, base_point) transposed = Matrices.transpose(tangent_vec_a) transported_vec = Matrices.mul(midpoint, transposed, midpoint) return (-1.) * transported_vec
def is_tangent(self, vector, base_point): r"""Check if the vector belongs to the tangent space. Parameters ---------- vector : array-like, shape=[..., n, n] Matrix to check if it belongs to the tangent space. base_point : array-like, shape=[..., n, n] Base point of the tangent space. Optional, default: None. Returns ------- belongs : array-like, shape=[...,] Boolean denoting if vector belongs to tangent space at base_point. """ vector_sym = Matrices(self.n, self.n).to_symmetric(vector) _, r = gs.linalg.eigh(base_point) r_ort = r[..., :, self.n - self.rank:self.n] r_ort_t = Matrices.transpose(r_ort) rr = gs.matmul(r_ort, r_ort_t) candidates = Matrices.mul(rr, vector_sym, rr) result = gs.all(gs.isclose(candidates, 0., gs.atol), axis=(-2, -1)) return result
def adjoint_map(state): r"""Construct the matrix associated to the adjoint representation. The inner automorphism is given by :math:`Ad_X : g |-> XgX^-1`. For a state :math:`X = (\theta, x, y)`, the matrix associated to its tangent map, the adjoint representation, is :math:`\begin{bmatrix} 1 & \\ -J [x, y] & R(\theta) \end{bmatrix}`, where :math:`R(\theta)` is the rotation matrix of angle theta, and :math:`J = \begin{bmatrix} 0 & -1 \\ 1 & 0 \end{bmatrix}` Parameters ---------- state : array-like, shape=[dim] Vector representing a state. Returns ------- adjoint : array-like, shape=[dim, dim] Adjoint representation of the state. """ theta, _, _ = state tangent_base = gs.array([[0.0, -1.0], [1.0, 0.0]]) orientation_part = gs.eye(Localization.dim_rot, Localization.dim) pos_column = gs.reshape(state[1:], (Localization.group.n, 1)) position_wrt_orientation = Matrices.mul(-tangent_base, pos_column) position_wrt_position = Localization.rotation_matrix(theta) last_lines = gs.hstack( (position_wrt_orientation, position_wrt_position)) ad = gs.vstack((orientation_part, last_lines)) return ad
def align(self, point, base_point, **kwargs): """Align point to base_point. Find the optimal rotation R in SO(m) such that the base point and R.point are well positioned. Parameters ---------- point : array-like, shape=[..., k_landmarks, m_ambient] Point on the manifold. base_point : array-like, shape=[..., k_landmarks, m_ambient] Point on the manifold. Returns ------- aligned : array-like, shape=[..., k_landmarks, m_ambient] R.point. """ mat = gs.matmul(Matrices.transpose(point), base_point) left, singular_values, right = gs.linalg.svd(mat) det = gs.linalg.det(mat) conditioning = (singular_values[..., -2] + gs.sign(det) * singular_values[..., -1]) / singular_values[..., 0] if gs.any(conditioning < gs.atol): logging.warning(f"Singularity close, ill-conditioned matrix " f"encountered: " f"{conditioning[conditioning < 1e-10]}") if gs.any(gs.isclose(conditioning, 0.0)): logging.warning("Alignment matrix is not unique.") flipped = flip_determinant(Matrices.transpose(right), det) return Matrices.mul(point, left, Matrices.transpose(flipped))
def parallel_transport(self, tangent_vec_a, tangent_vec_b, base_point): r"""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). Denoting `tangent_vec_a` by `S`, `base_point` by `A`, let `B = Exp_A(tangent_vec_b)` and :math: `E = (BA^{- 1})^({ 1 / 2})`. Then the parallel transport to `B`is: ..math:: S' = ESE^T 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, initial speed of the geodesic along which the parallel transport is computed. base_point : array-like, shape=[..., dim + 1] Point on the manifold of SPD matrices. Returns ------- transported_tangent_vec: array-like, shape=[..., dim + 1] Transported tangent vector at exp_(base_point)(tangent_vec_b). """ end_point = self.exp(tangent_vec_b, base_point) inverse_base_point = GeneralLinear.inverse(base_point) congruence_mat = Matrices.mul(end_point, inverse_base_point) congruence_mat = gs.linalg.sqrtm(congruence_mat) return Matrices.congruent(tangent_vec_a, congruence_mat)
def inverse_differential_power(cls, power, tangent_vec, base_point): r"""Compute the inverse of the differential of the matrix power. Compute the inverse of the differential of the power function on SPD matrices (:math: `A^p=exp(p log(A))`) at base_point applied to tangent_vec. Parameters ---------- power : int Power. tangent_vec : array_like, shape=[..., n, n] Tangent vector at base point. base_point : array_like, shape=[..., n, n] Base point. Returns ------- inverse_differential_power : array-like, shape=[..., n, n] Inverse of the differential of the power function. """ eigvectors, transp_eigvectors, numerator, denominator, temp_result =\ cls.aux_differential_power(power, tangent_vec, base_point) power_operator = denominator / numerator result = power_operator * temp_result result = Matrices.mul(eigvectors, result, transp_eigvectors) return result
def inverse_differential_exp(cls, tangent_vec, base_point): """Compute the inverse of the differential of the matrix exponential. Computes the inverse of the differential of the matrix exponential on SPD matrices at base_point applied to tangent_vec. Parameters ---------- tangent_vec : array_like, shape=[..., n, n] Tangent vector at base point. base_point : array_like, shape=[..., n, n] Base point. Returns ------- inverse_differential_exp : array-like, shape=[..., n, n] Inverse of the differential of the matrix exponential. """ eigvectors, transp_eigvectors, numerator, denominator, temp_result = \ cls.aux_differential_power(math.inf, tangent_vec, base_point) power_operator = denominator / numerator result = power_operator * temp_result result = Matrices.mul(eigvectors, result, transp_eigvectors) return result
def belongs(self, point, atol=1e-5): """Test if a point belongs to St(n,p). Test whether the point is a p-frame in n-dimensional space, and it is orthonormal. Parameters ---------- point : array-like, shape=[..., n, p] Point. atol : float, optional Tolerance at which to evaluate. Optional, default: 1e-5. Returns ------- belongs : array-like, shape=[...,] Array of booleans evaluating if the corresponding points belong to the Stiefel manifold. """ right_shape = self.embedding_manifold.belongs(point) if not right_shape: return right_shape point_transpose = Matrices.transpose(point) identity = gs.eye(self.p) diff = Matrices.mul(point_transpose, point) - identity diff_norm = gs.linalg.norm(diff, axis=(-2, -1)) belongs = gs.less_equal(diff_norm, 1e-5) return belongs
def retraction(tangent_vec, base_point): """Compute the retraction of a tangent vector. This computation is based on the QR-decomposition. e.g. :math:`P_x(V) = qf(X + V)`. Parameters ---------- tangent_vec : array-like, shape=[..., n, p] Tangent vector at a base point. base_point : array-like, shape=[..., n, p] Point in the Stiefel manifold. Returns ------- exp : array-like, shape=[..., n, p] Point in the Stiefel manifold equal to the retraction of tangent_vec at the base point. """ matrix_q, matrix_r = gs.linalg.qr(base_point + tangent_vec) diagonal = gs.diagonal(matrix_r, axis1=-2, axis2=-1) sign = gs.sign(gs.sign(diagonal) + 0.5) diag = algebra_utils.from_vector_to_diagonal_matrix(sign) result = Matrices.mul(matrix_q, diag) return result
def tangent_translation_map(self, point, left_or_right="left", inverse=False): r"""Compute the push-forward map by the left/right translation. Compute the push-forward map, of the left/right translation by the point. It corresponds to the tangent map, or differential of the group multiplication by the point or its inverse. For groups with a vector representation, it is only implemented at identity, but it can be used at other points by passing `inverse=True`. This method wraps the jacobian translation which actually computes the matrix representation of the map. Parameters ---------- point : array-like, shape=[..., {dim, [n, n]] Point. left_or_right : str, {'left', 'right'} Whether to calculate the differential of left or right translations. Optional, default: 'left' inverse : bool, Whether to inverse the jacobian matrix. If True, the push forward by the translation by the inverse of point is returned. Optional, default: False. Returns ------- tangent_map : callable Tangent map of the left/right translation by point. It can be applied to tangent vectors. """ errors.check_parameter_accepted_values(left_or_right, "left_or_right", ["left", "right"]) if self.default_point_type == "matrix": if inverse: point = self.inverse(point) if left_or_right == "left": return lambda tangent_vec: Matrices.mul(point, tangent_vec) return lambda tangent_vec: Matrices.mul(tangent_vec, point) jacobian = self.jacobian_translation(point, left_or_right) if inverse: jacobian = gs.linalg.inv(jacobian) return lambda tangent_vec: gs.einsum("...ij,...j->...i", jacobian, tangent_vec)