def injectivity_radius(self, base_point): """Compute the radius of the injectivity domain. This is is the supremum of radii r for which the exponential map is a diffeomorphism from the open ball of radius r centered at the base point onto its image. In this case, it does not depend on the base point. If the rotation part is null, then the radius is infinite, otherwise it is the same as the special orthonormal group. Parameters ---------- base_point : array-like, shape=[..., n + 1, n + 1] Point on the manifold. Returns ------- radius : float Injectivity radius. """ rotation = base_point[..., :self.n, :self.n] rotation_radius = gs.pi * (self.dim - self.n)**0.5 radius = gs.where( gs.sum(rotation, axis=(-2, -1)) == 0, math.inf, rotation_radius) return radius
def parabolic_radial_kernel(distance, bandwidth=1.0): """Parabolic radial kernel. Parameters ---------- distance : array-like Array of non-negative real values. bandwidth : float, optional (default=1.0) Positive scale parameter of the kernel. Returns ------- weight : array-like Array of non-negative real values of the same shape than parameter 'distance'. References ---------- https://en.wikipedia.org/wiki/Kernel_(statistics) """ distance = _check_distance(distance) bandwidth = _check_bandwidth(bandwidth) scaled_distance = distance / bandwidth weight = gs.where( scaled_distance < 1, 1 - scaled_distance**2, gs.zeros(distance.shape, dtype=float), ) return weight
def exp(self, tangent_vec, base_point): """Compute the Riemannian exponential of a tangent vector. Parameters ---------- tangent_vec : array-like, shape=[..., dim] Tangent vector at a base point. base_point : array-like, shape=[..., dim] Point in the Poincare ball. Returns ------- exp : array-like, shape=[..., dim] Point in the Poincare ball equal to the Riemannian exponential of tangent_vec at the base point. """ squared_norm_bp = gs.sum(base_point**2, axis=-1) norm_tan = gs.linalg.norm(tangent_vec, axis=-1) lambda_base_point = 1 / (1 - squared_norm_bp) # This avoids dividing by 0 norm_tan_eps = gs.where(gs.isclose(norm_tan, 0.), EPSILON, norm_tan) direction = gs.einsum('...i,...->...i', tangent_vec, 1 / norm_tan_eps) factor = gs.tanh(lambda_base_point * norm_tan) exp = self.mobius_add(base_point, gs.einsum('...,...i->...i', factor, direction)) return exp
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 compute_coordinates(self, point): """Compute the ellipse coordinates of a 2D SPD matrix. Parameters ---------- point : array-like, shape=[2, 2] SPD matrix. Returns ------- x_coords : array-like, shape=[n_sampling_points,] x_coords coordinates of the sampling points on the discretized ellipse. Y: array-like, shape = [n_sampling_points,] y coordinates of the sampling points on the discretized ellipse. """ eigvalues, eigvectors = gs.linalg.eigh(point) eigvalues = gs.where(eigvalues < gs.atol, gs.atol, eigvalues) [eigvalue1, eigvalue2] = eigvalues rot_sin = eigvectors[1, 0] rot_cos = eigvectors[0, 0] thetas = gs.linspace(0.0, 2 * gs.pi, self.n_sampling_points + 1) x_coords = eigvalue1 * gs.cos(thetas) * rot_cos x_coords -= rot_sin * eigvalue2 * gs.sin(thetas) y_coords = eigvalue1 * gs.cos(thetas) * rot_sin y_coords += rot_cos * eigvalue2 * gs.sin(thetas) return x_coords, y_coords
def projection(self, point): r"""Project a matrix to the set of full rank matrices. As the space of full rank matrices is dense in the space of matrices, this is not a projection per se, but a regularization if the matrix input X is not already full rank: :math:`X + \epsilon [I_{rank}, 0]` is returned where :math:`\epsilon=gs.atol` Parameters ---------- point : array-like, shape=[..., n, k] Point in embedding manifold. Returns ------- projected : array-like, shape=[..., n, k] Projected point. """ belongs = self.belongs(point) regularization = gs.einsum( "...,ij->...ij", gs.where(~belongs, gs.atol, 0.0), gs.eye(self.ambient_space.shape[0], self.ambient_space.shape[1]), ) projected = point + regularization return projected
def bump_radial_kernel(distance, bandwidth=1.0): """Bump radial kernel. Parameters ---------- distance : array-like Array of non-negative real values. bandwidth : float, optional (default=1.0) Positive scale parameter of the kernel. Returns ------- weight : array-like Array of non-negative real values of the same shape than parameter 'distance'. References ---------- https://en.wikipedia.org/wiki/Radial_basis_function """ distance = _check_distance(distance) bandwidth = _check_bandwidth(bandwidth) scaled_distance = distance / bandwidth weight = gs.where(scaled_distance < 1, gs.exp(-1 / (1 - scaled_distance**2)), gs.zeros(distance.shape, dtype=float)) return weight
def projection(self, point): r"""Project a matrix to the general linear group. As GL(n) is dense in the space of matrices, this is not a projection per se, but a regularization if the matrix is not already invertible: :math:`X + \epsilon I_n` is returned where :math:`\epsilon=gs.atol` is returned for an input X. Parameters ---------- point : array-like, shape=[..., dim_embedding] Point in embedding manifold. Returns ------- projected : array-like, shape=[..., dim_embedding] Projected point. """ belongs = self.belongs(point) regularization = gs.einsum("...,ij->...ij", gs.where(~belongs, gs.atol, 0.0), self.identity) projected = point + regularization if self.positive_det: det = gs.linalg.det(point) return utils.flip_determinant(projected, det) return projected
def parallel_transport(self, 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). """ theta = self.embedding_metric.norm(tangent_vec_b) eps = gs.where(theta == 0., 1., theta) normalized_b = gs.einsum('...,...i->...i', 1 / eps, tangent_vec_b) pb = self.embedding_metric.inner_product(tangent_vec_a, normalized_b) p_orth = tangent_vec_a - gs.einsum('...,...i->...i', pb, normalized_b) transported = \ gs.einsum('...,...i->...i', gs.sinh(theta) * pb, base_point)\ + gs.einsum('...,...i->...i', gs.cosh(theta) * pb, normalized_b)\ + p_orth return transported
def extrinsic_to_spherical(self, point_extrinsic): """Convert point from extrinsic to spherical coordinates. Convert from the extrinsic coordinates, i.e. embedded in Euclidean space of dim 3 to spherical coordinates in the hypersphere. Spherical coordinates are defined from the north pole, i.e. angles [0., 0.] correspond to point [0., 0., 1.]. Only implemented in dimension 2. Parameters ---------- point_extrinsic : array-like, shape=[..., dim] Point on the sphere, in extrinsic coordinates. Returns ------- point_spherical : array_like, shape=[..., dim + 1] Point on the sphere, in spherical coordinates relative to the north pole. """ if self.dim != 2: raise NotImplementedError( "The conversion from to extrinsic coordinates " "spherical coordinates is implemented" " only in dimension 2.") theta = gs.arccos(point_extrinsic[..., -1]) x = point_extrinsic[..., 0] y = point_extrinsic[..., 1] phi = gs.arctan2(y, x) phi = gs.where(phi < 0, phi + 2 * gs.pi, phi) return gs.stack([theta, phi], axis=-1)
def maximum_likelihood_fit(data, loc=0, scale=1): """Estimate parameters from samples. This a wrapper around scipy's maximum likelihood estimator to estimate the parameters of a beta distribution from samples. Parameters ---------- data : array-like, shape=[..., n_samples] Data to estimate parameters from. Arrays of different length may be passed. loc : float Location parameter of the distribution to estimate parameters from. It is kept fixed during optimization. Optional, default: 0. scale : float Scale parameter of the distribution to estimate parameters from. It is kept fixed during optimization. Optional, default: 1. Returns ------- parameter : array-like, shape=[..., 2] Estimate of parameter obtained by maximum likelihood. """ data = gs.cast(data, gs.float32) data = gs.to_ndarray(gs.where(data == 1., 1. - EPSILON, data), to_ndim=2) parameters = [] for sample in data: param_a, param_b, _, _ = beta.fit(sample, floc=loc, fscale=scale) parameters.append(gs.array([param_a, param_b])) return parameters[0] if len(data) == 1 else gs.stack(parameters)
def exp_domain(self, tangent_vec, base_point): base_point = gs.to_ndarray(base_point, to_ndim=3) tangent_vec = gs.to_ndarray(tangent_vec, to_ndim=3) invsqrt_base_point = gs.linalg.powerm(base_point, -.5) reduced_vec = gs.matmul(invsqrt_base_point, tangent_vec) reduced_vec = gs.matmul(reduced_vec, invsqrt_base_point) eigvals = gs.linalg.eigvalsh(reduced_vec) min_eig = gs.amin(eigvals, axis=1) max_eig = gs.amax(eigvals, axis=1) inf_value = gs.where(max_eig <= 0, -math.inf, -1 / max_eig) inf_value = gs.to_ndarray(inf_value, to_ndim=2) sup_value = gs.where(min_eig >= 0, math.inf, -1 / min_eig) sup_value = gs.to_ndarray(sup_value, to_ndim=2) domain = gs.concatenate((inf_value, sup_value), axis=1) return domain
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 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) eps = gs.where(theta == 0., 1., 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 maximum_likelihood_fit(self, data, loc=0, scale=1): """Estimate parameters from samples. This a wrapper around scipy's maximum likelihood estimator to estimate the parameters of a beta distribution from samples. Parameters ---------- data : array-like, shape=[n_distributions, n_samples] the data to estimate parameters from. Arrays of different length may be passed. loc : float, optional the location parameter of the distribution to estimate parameters from. It is kept fixed during optimization default: 0 scale : float, optional the scale parameter of the distribution to estimate parameters from. It is kept fixed during optimization default: 1 Returns ------- parameter : array-like, shape=[n_samples, 2] """ data = gs.to_ndarray( gs.where(data == 1., 1 - EPSILON, data), to_ndim=2) parameters = [] for sample in data: param_a, param_b, _, _ = beta.fit(sample, floc=loc, fscale=scale) parameters.append(gs.array([param_a, param_b])) return parameters[0] if len(data) == 1 else gs.stack(parameters)
def belongs(self, point): """Test if a matrix is invertible and of the right size.""" point = gs.to_ndarray(point, to_ndim=3) _, mat_dim_1, mat_dim_2 = point.shape det = gs.linalg.det(point) return gs.logical_and( mat_dim_1 == self.n and mat_dim_2 == self.n, gs.where(det != 0., gs.array(True), gs.array(False)))
def path(t): """Generate parameterized function for geodesic curve. Parameters ---------- t : array-like, shape=[n_times,] Times at which to compute points of the geodesics. Returns ------- geodesic : array-like, shape=[..., n_times, dim] Values of the geodesic at times t. """ t = gs.to_ndarray(t, to_ndim=1) geod = [] def initialize(point_0, point_1): """Initialize the solution of the boundary value problem.""" if init == "polynomial": _, curve, velocity = self._approx_geodesic_bvp( point_0, point_1, n_times=n_steps ) return gs.vstack((curve.T, velocity.T)) lin_init = gs.zeros([2 * self.dim, n_steps]) lin_init[: self.dim, :] = gs.transpose( gs.linspace(point_0, point_1, n_steps) ) lin_init[self.dim :, :-1] = n_steps * ( lin_init[: self.dim, 1:] - lin_init[: self.dim, :-1] ) lin_init[self.dim :, -1] = lin_init[self.dim :, -2] return lin_init t_int = gs.linspace(0.0, 1.0, n_steps) fun_jac = jac if jacobian else None for ip, ep in zip(initial_point, end_point): def bc(y0, y1, ip=ip, ep=ep): return boundary_cond(y0, y1, ip, ep) solution = solve_bvp( bvp, bc, t_int, initialize(ip, ep), fun_jac=fun_jac ) if solution.status == 1: logging.warning( "The maximum number of mesh nodes for solving the " "geodesic boundary value problem is exceeded. " "Result may be inaccurate." ) solution_at_t = solution.sol(t) geodesic = solution_at_t[: self.dim, :] geod.append(gs.squeeze(gs.transpose(geodesic))) geod = geod[0] if len(initial_point) == 1 else gs.stack(geod) return gs.where(geod < gs.atol, gs.atol, geod)
def _circle_variances(mean, var, n_samples, points): """Compute the minimizer of the variance functional. Parameters ---------- mean : float Mean angle. var : float Variance of the angles. n_samples : int Number of samples. points : array-like, shape=[n,] Data set of ordered angles. References --------- ..[HH15] Hotz, T. and S. F. Huckemann (2015), "Intrinsic means on the circle: Uniqueness, locus and asymptotics", Annals of the Institute of Statistical Mathematics 67 (1), 177–193. https://arxiv.org/abs/1108.2141 """ means = (mean + gs.linspace(0.0, 2 * gs.pi, n_samples + 1)[:-1]) % (2 * gs.pi) means = gs.where(means >= gs.pi, means - 2 * gs.pi, means) parts = gs.array([sum(points) / n_samples if means[0] < 0 else 0]) m_plus = means >= 0 left_sums = gs.cumsum(points) right_sums = left_sums[-1] - left_sums i = gs.arange(n_samples, dtype=right_sums.dtype) j = i[1:] parts2 = right_sums[:-1] / (n_samples - j) first_term = parts2[:1] parts2 = gs.where(m_plus[1:], left_sums[:-1] / j, parts2) parts = gs.concatenate([parts, first_term, parts2[1:]]) # Formula (6) from [HH15]_ plus_vec = (4 * gs.pi * i / n_samples) * (gs.pi + parts - mean) - ( 2 * gs.pi * i / n_samples ) ** 2 minus_vec = (4 * gs.pi * (n_samples - i) / n_samples) * (gs.pi - parts + mean) - ( 2 * gs.pi * (n_samples - i) / n_samples ) ** 2 minus_vec = gs.where(m_plus, plus_vec, minus_vec) means = gs.transpose(gs.vstack([means, var + minus_vec])) return means
def _to_lie_algebra(self, tangent_vec): """Project vector rotation part onto skew-symmetric matrices.""" translation_mask = gs.hstack( [gs.ones((self.n, ) * 2), 2 * gs.ones((self.n, 1))]) translation_mask = gs.concatenate( [translation_mask, gs.zeros((1, self.n + 1))], axis=0) tangent_vec = tangent_vec * gs.where(translation_mask != 0., gs.array(1.), gs.array(0.)) tangent_vec = (tangent_vec - GeneralLinear.transpose(tangent_vec)) / 2. return tangent_vec * translation_mask
def sectional_curvature(self, tangent_vec_a, tangent_vec_b, base_point=None): r"""Compute the sectional curvature. For two orthonormal tangent vectors :math:`x,y` at a base point, the sectional curvature is defined by :math:`<R(x, y)x, y> = <R_x(y), y>`. For non-orthonormal vectors vectors, it is :math:`<R(x, y)x, y> / \\|x \\wedge y\\|^2`. Parameters ---------- tangent_vec_a : array-like, shape=[..., n, n] Tangent vector at `base_point`. tangent_vec_b : array-like, shape=[..., n, n] Tangent vector at `base_point`. base_point : array-like, shape=[..., n, n] Point in the group. Optional, default is the identity Returns ------- sectional_curvature : array-like, shape=[...,] Sectional curvature at `base_point`. See Also -------- https://en.wikipedia.org/wiki/Sectional_curvature """ curvature = self.curvature(tangent_vec_a, tangent_vec_b, tangent_vec_a, base_point) sectional = self.inner_product(curvature, tangent_vec_b, base_point) norm_a = self.squared_norm(tangent_vec_a, base_point) norm_b = self.squared_norm(tangent_vec_b, base_point) inner_ab = self.inner_product(tangent_vec_a, tangent_vec_b, base_point) normalization_factor = norm_a * norm_b - inner_ab**2 condition = gs.isclose(normalization_factor, 0.0) normalization_factor = gs.where(condition, EPSILON, normalization_factor) return gs.where(~condition, sectional / normalization_factor, 0.0)
def _create_batches(self): """Create the batches used to compute covariance matrices. If margin != 0, we add an index margin at each label change to get stationary signal corresponding to each label. """ start_ids = gs.where(np.diff(self.data['y']) != 0)[0] end_ids = np.append(start_ids[1:], len(self.data)) - self.margin start_ids += self.margin batches_list = [range(start_id, end_id - self.n_steps, self.n_steps) for start_id, end_id in zip(start_ids, end_ids)] self.batches = np.int_(gs.concatenate(batches_list))
def projection(self, point): """Project a point in space on the hyperboloid. Parameters ---------- point : array-like, shape=[..., dim + 1] Point in embedding Euclidean space. Returns ------- projected_point : array-like, shape=[..., dim + 1] Point projected on the hyperboloid. """ belongs = self.belongs(point) # avoid dividing by 0 factor = gs.where(point[..., 0] == 0.0, 1.0, point[..., 0] + gs.atol) first_coord = gs.where(belongs, 1.0, 1.0 / factor) intrinsic = gs.einsum("...,...i->...i", first_coord, point)[..., 1:] return self.intrinsic_to_extrinsic_coords(intrinsic)
def test_closest_neighbor_index(self): """ Check that the closest neighbor is one of neighbors. """ n_samples = 10 points = self.space.random_uniform(n_samples=n_samples) point = points[0, :] neighbors = points[1:, :] index = self.metric.closest_neighbor_index(point, neighbors) closest_neighbor = points[index, :] test = gs.where((points == closest_neighbor).all(axis=1)) result = test[0].size > 0 self.assertTrue(result)
def inverse(cls, point): """Return the inverse of a point. Parameters ---------- point : array-like, shape=[..., n, n] Point to be inverted. """ n = point.shape[-1] - 1 translation_mask = gs.hstack([gs.ones((n, ) * 2), 2 * gs.ones((n, 1))]) translation_mask = gs.concatenate( [translation_mask, gs.zeros((1, n + 1))], axis=0) embedded_rotations = point * gs.where(translation_mask == 1, translation_mask, gs.eye(n + 1)) transposed_rot = cls.transpose(embedded_rotations) translation = point[..., :, -1] translation = gs.einsum('...ij,...j->...i', transposed_rot, translation) translation *= gs.where(translation_mask[..., -1] == 2, -translation_mask[..., -1] / 2, gs.ones(n + 1)) return gs.concatenate( [transposed_rot[..., :, :-1], translation[..., None]], axis=-1)
def aux_differential_power(power, tangent_vec, base_point): """Compute the differential of the matrix power. Auxiliary function to the functions differential_power and inverse_differential_power. Parameters ---------- power : float Power function to differentiate. tangent_vec : array_like, shape=[..., n, n] Tangent vector at base point. base_point : array_like, shape=[..., n, n] Base point. Returns ------- eigvectors : array-like, shape=[..., n, n] transp_eigvectors : array-like, shape=[..., n, n] numerator : array-like, shape=[..., n, n] denominator : array-like, shape=[..., n, n] temp_result : array-like, shape=[..., n, n] """ eigvalues, eigvectors = gs.linalg.eigh(base_point) if power == 0: powered_eigvalues = gs.log(eigvalues) elif power == math.inf: powered_eigvalues = gs.exp(eigvalues) else: powered_eigvalues = eigvalues ** power denominator = eigvalues[..., :, None] - eigvalues[..., None, :] numerator = powered_eigvalues[..., :, None] - powered_eigvalues[..., None, :] if power == 0: numerator = gs.where(denominator == 0, gs.ones_like(numerator), numerator) denominator = gs.where( denominator == 0, eigvalues[..., :, None], denominator ) elif power == math.inf: numerator = gs.where( denominator == 0, powered_eigvalues[..., :, None], numerator ) denominator = gs.where( denominator == 0, gs.ones_like(numerator), denominator ) else: numerator = gs.where( denominator == 0, power * powered_eigvalues[..., :, None], numerator ) denominator = gs.where( denominator == 0, eigvalues[..., :, None], denominator ) transp_eigvectors = Matrices.transpose(eigvectors) temp_result = Matrices.mul(transp_eigvectors, tangent_vec, eigvectors) return (eigvectors, transp_eigvectors, numerator, denominator, temp_result)
def sectional_curvature_at_identity(self, tangent_vec_a, tangent_vec_b): """Compute the sectional curvature at identity. For two orthonormal tangent vectors at identity :math: `x,y`, the sectional curvature is defined by :math: `< R(x, y)x, y>`. Non-orthonormal vectors can be given. Parameters ---------- tangent_vec_a : array-like, shape=[..., n, n] Tangent vector at identity. tangent_vec_b : array-like, shape=[..., n, n] Tangent vector at identity. Returns ------- sectional_curvature : array-like, shape=[...,] Sectional curvature at identity. References ---------- https://en.wikipedia.org/wiki/Sectional_curvature .. [Milnor] Milnor, John. “Curvatures of Left Invariant Metrics on Lie Groups.” Advances in Mathematics 21, no. 3, 1976: 293–329. https://doi.org/10.1016/S0001-8708(76)80002-3. """ curvature = self.curvature_at_identity( tangent_vec_a, tangent_vec_b, tangent_vec_a ) num = self.inner_product(tangent_vec_b, curvature) denom = ( self.squared_norm(tangent_vec_a) * self.squared_norm(tangent_vec_a) - self.inner_product(tangent_vec_a, tangent_vec_b) ** 2 ) condition = gs.isclose(denom, 0.0) denom = gs.where(condition, EPSILON, denom) return gs.where(~condition, num / denom, 0.0)
def tangent_spherical_to_extrinsic( self, tangent_vec_spherical, base_point_spherical ): """Convert tangent vector from spherical to extrinsic coordinates. Convert from the spherical coordinates in the hypersphere to the extrinsic coordinates in Euclidean space for a tangent vector. Only implemented in dimension 2. Parameters ---------- tangent_vec_spherical : array-like, shape=[..., dim] Tangent vector to the sphere, in spherical coordinates. base_point_spherical : array-like, shape=[..., dim] Point on the sphere, in spherical coordinates. Returns ------- tangent_vec_extrinsic : array-like, shape=[..., dim + 1] Tangent vector to the sphere, at base point, in extrinsic coordinates in Euclidean space. """ if self.dim != 2: raise NotImplementedError( "The conversion from spherical coordinates" " to extrinsic coordinates is implemented" " only in dimension 2." ) axes = (2, 0, 1) if base_point_spherical.ndim == 2 else (0, 1) theta = base_point_spherical[..., 0] phi = base_point_spherical[..., 1] phi = gs.where(theta == 0.0, 0.0, phi) zeros = gs.zeros_like(theta) jac = gs.array( [ [gs.cos(theta) * gs.cos(phi), -gs.sin(theta) * gs.sin(phi)], [gs.cos(theta) * gs.sin(phi), gs.sin(theta) * gs.cos(phi)], [-gs.sin(theta), zeros], ] ) jac = gs.transpose(jac, axes) tangent_vec_extrinsic = gs.einsum( "...ij,...j->...i", jac, tangent_vec_spherical ) return tangent_vec_extrinsic
def parallel_transport(self, tangent_vec, base_point, direction=None, end_point=None): r"""Compute the parallel transport of a tangent vector. Closed-form solution for the parallel transport of a tangent vector along the geodesic between two points `base_point` and `end_point` or alternatively defined by :math:`t\mapsto exp_(base_point)( t*direction)`. Parameters ---------- tangent_vec : array-like, shape=[..., dim + 1] Tangent vector at base point to be transported. base_point : array-like, shape=[..., dim + 1] Point on the hyperboloid. direction : array-like, shape=[..., dim + 1] Tangent vector at base point, along which the parallel transport is computed. Optional, default : None. end_point : array-like, shape=[..., dim + 1] Point on the hyperboloid. Point to transport to. Unused if `tangent_vec_b` is given. Optional, default : None. Returns ------- transported_tangent_vec: array-like, shape=[..., dim + 1] Transported tangent vector at exp_(base_point)(tangent_vec_b). """ if direction is None: if end_point is not None: direction = self.log(end_point, base_point) else: raise ValueError( "Either an end_point or a tangent_vec_b must be given to define the" " geodesic along which to transport.") theta = self.embedding_metric.norm(direction) eps = gs.where(theta == 0.0, 1.0, theta) normalized_b = gs.einsum("...,...i->...i", 1 / eps, direction) pb = self.embedding_metric.inner_product(tangent_vec, normalized_b) p_orth = tangent_vec - gs.einsum("...,...i->...i", pb, normalized_b) transported = (gs.einsum("...,...i->...i", gs.sinh(theta) * pb, base_point) + gs.einsum("...,...i->...i", gs.cosh(theta) * pb, normalized_b) + p_orth) return transported
def belongs(self, point, atol=gs.atol): r"""Check if the matrix belongs to `math:`R_*^{m\times n}`. Parameters ---------- point : array-like, shape=[..., m, n] Matrix to be checked. Returns ------- belongs : Boolean denoting if point is in `math:`R_*^{m\times n}` """ has_right_size = self.ambient_space.belongs(point) has_right_rank = gs.where( gs.linalg.matrix_rank(point) == self.rank, True, False) belongs = gs.logical_and(gs.array(has_right_size), has_right_rank) return belongs
def get_label_at_index(i, labels): """Get the label of data point indexed by 'i'. Parameters ---------- i : int Index of data point. labels : array-like, shape = [n_samples * n_classes, n_features] All labels. Returns ------- label_i : int Class index. """ label_i = gs.where(labels[i])[0][0] return label_i