def sample(self, point, n_samples=1): """Sample from the Gamma distribution. Sample from the Gamma distribution with parameters provided by point. This gives n_samples points. Parameters ---------- point : array-like, shape=[..., dim] Point representing a Gamma distribution. n_samples : int Number of points to sample for each set of parameters in point. Optional, default: 1. Returns ------- samples : array-like, shape=[..., n_samples] Sample from the Gamma distributions. """ geomstats.errors.check_belongs(point, self) point = gs.to_ndarray(point, to_ndim=2) samples = [] for param in point: sample = gs.array( gamma.rvs(param[0], loc=0, scale=param[1] / param[0], size=n_samples)) samples.append(sample) return gs.squeeze(samples[0]) if len(point) == 1 else gs.stack(samples)
def predict(self, X, fuzzy_predictions=False): """Predict the labels for each data point. Label each data point with the cluster having the nearest centroid using metric distance. Parameters ---------- X : array-like, shape=[..., n_features] Input data. Returns n |7i0-o≥ ------- self : array-like, shape=[...,] Array of predicted cluster indices for each sample. """ if self.centroids is None: raise RuntimeError('fit needs to be called first.') dists = gs.stack( [self.metric.dist(centroid, X) for centroid in self.centroids], axis=1) dists = gs.squeeze(dists) if fuzzy_predictions: dists[np.where(dists == 0)] = 0.00001 belongs = 1 / (dists * np.sum(1 / dists, axis=1)[:, None]) else: belongs = gs.argmin(dists, -1) return belongs
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 path(t): """Generate parameterized function for geodesic curve. Parameters ---------- t : array-like, shape=[n_points,] Times at which to compute points of the geodesics. """ t = gs.array(t) t = gs.cast(t, initial_tangent_vec.dtype) t = gs.to_ndarray(t, to_ndim=1) if point_type == "vector": tangent_vecs = gs.einsum("i,...k->...ik", t, initial_tangent_vec) else: tangent_vecs = gs.einsum("i,...kl->...ikl", t, initial_tangent_vec) points_at_time_t = [ self.exp(tv, pt) for tv, pt in zip(tangent_vecs, initial_point) ] points_at_time_t = gs.stack(points_at_time_t, axis=0) return (points_at_time_t[0] if n_initial_conditions == 1 else points_at_time_t)
def belongs(self, point, atol=gs.atol): """Check if the point belongs to the manifold. Check if an (n,n)-matrix is an orthogonal projector onto a subspace of rank k. Parameters ---------- point : array-like, shape=[..., n, n] Point to be checked. atol : int Optional, default: backend atol. Returns ------- belongs : array-like, shape=[...,] Boolean evaluating if point belongs to the Grassmannian. """ if not gs.all(self._check_square(point)): raise ValueError('all points must be square.') symm = Matrices.is_symmetric(point) idem = self._check_idempotent(point, atol) rank = self._check_rank(point, self.k, atol) belongs = gs.all(gs.stack([symm, idem, rank], axis=0), axis=0) return belongs
def regularize(self, point): """Regularize the point into the manifold's canonical representation. Parameters ---------- point : array-like, shape=[..., {dim, [n_manifolds, dim_each]}] Point to be regularized. point_type : str, {'vector', 'matrix'} Representation of point. Optional, default: None. Returns ------- regularized_point : array-like, shape=[..., {dim, [n_manifolds, dim_each]}] Point in the manifold's canonical representation. """ point_type = self.default_point_type if point_type == "vector": intrinsic = self.metric.is_intrinsic(point) regularized_point = self._iterate_over_manifolds( "regularize", {"point": point}, intrinsic ) regularized_point = gs.concatenate(regularized_point, axis=-1) elif point_type == "matrix": regularized_point = [ manifold_i.regularize(point[..., i, :]) for i, manifold_i in enumerate(self.manifolds) ] regularized_point = gs.stack(regularized_point, axis=1) return regularized_point
def projection(self, point): """Project a point in product embedding manifold on each manifold. Parameters ---------- point : array-like, shape=[..., {dim, [n_manifolds, dim_each]}] Point in embedding manifold. Returns ------- projected : array-like, shape=[..., {dim, [n_manifolds, dim_each]}] Projected point. """ point_type = self.default_point_type geomstats.errors.check_parameter_accepted_values( point_type, "point_type", ["vector", "matrix"] ) if point_type == "vector": intrinsic = self.metric.is_intrinsic(point) projected_point = self._iterate_over_manifolds( "projection", {"point": point}, intrinsic ) projected_point = gs.concatenate(projected_point, axis=-1) elif point_type == "matrix": projected_point = [ manifold_i.projection(point[..., i, :]) for i, manifold_i in enumerate(self.manifolds) ] projected_point = gs.stack(projected_point, axis=-2) return projected_point
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 _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 __init__(self, n): dim = int(n * (n - 1) / 2) super(SkewSymmetricMatrices, self).__init__(dim, n) if n == 2: self.basis = gs.array([[[0., -1.], [1., 0.]]]) elif n == 3: self.basis = gs.array([ [[0., 0., 0.], [0., 0., -1.], [0., 1., 0.]], [[0., 0., 1.], [0., 0., 0.], [-1., 0., 0.]], [[0., -1., 0.], [1., 0., 0.], [0., 0., 0.]]]) else: self.basis = gs.zeros((dim, n, n)) basis = [] for row in gs.arange(n - 1): for col in gs.arange(row + 1, n): basis.append(gs.array_from_sparse( [(row, col), (col, row)], [1., -1.], (n, n))) self.basis = gs.stack(basis)
def basis_representation(self, matrix_representation): """Calculate the coefficients of given matrix in the basis. Compute a 1d-array that corresponds to the input matrix in the basis representation. Parameters ---------- matrix_representation : array-like, shape=[..., n, n] Matrix. Returns ------- basis_representation : array-like, shape=[..., dim] Representation in the basis. """ if self.n == 2: return matrix_representation[..., 1, 0][..., None] if self.n == 3: vec = gs.stack([ matrix_representation[..., 2, 1], matrix_representation[..., 0, 2], matrix_representation[..., 1, 0]]) return gs.transpose(vec) return gs.triu_to_vec(matrix_representation, k=1)
def test_exp_vectorization(self, dim, point_a, point_b): metric = self.metric(dim) dist_a_b = metric.exp(point_a, point_b) result_vect = dist_a_b result = [metric.exp(point_a, point_b[i]) for i in range(len(point_b))] result = gs.stack(result, axis=0) self.assertAllClose(result_vect, result) dist_a_b = metric.exp(point_b, point_a) result_vect = dist_a_b result = [metric.exp(point_b[i], point_a) for i in range(len(point_b))] result = gs.stack(result, axis=0) self.assertAllClose(result_vect, result)
def ivp(state, _): """Reformat the initial value problem geodesic ODE.""" position, velocity = state[:self.dim], state[self.dim:] state = gs.stack([position, velocity]) vel, acc = self.geodesic_equation(state, _) eq = (vel, acc) return gs.hstack(eq)
def maximum_likelihood_fit(data): """Estimate parameters from samples. This is a wrapper around scipy's maximum likelihood estimator to estimate the parameters of a gamma distribution from samples. Parameters ---------- data : list or list of lists/arrays Data to estimate parameters from. Lists of different length may be passed. Returns ------- parameter : array-like, shape=[..., 2] Estimate of parameter obtained by maximum likelihood. """ def is_nested(sample): """Check if sample contains an iterable.""" for el in sample: try: return iter(el) except TypeError: return False if not is_nested(data): data = [data] parameters = [] for sample in data: sample = gs.array(sample) kappa, _, scale = gamma.fit(sample, floc=0) nu = 1 / scale parameters.append(gs.array([kappa, kappa / nu])) return parameters[0] if len(data) == 1 else gs.stack(parameters)
def test_space_derivative(self, dim, n_points, n_discretized_curves, n_sampling_points): """Test space derivative. Check result on an example and vectorization. """ n_points = 3 dim = 3 srv_metric_r3 = SRVMetric(Euclidean(dim)) curve = gs.random.rand(n_points, dim) result = srv_metric_r3.space_derivative(curve) delta = 1 / n_points d_curve_1 = (curve[1] - curve[0]) / delta d_curve_2 = (curve[2] - curve[0]) / (2 * delta) d_curve_3 = (curve[2] - curve[1]) / delta expected = gs.squeeze( gs.vstack(( gs.to_ndarray(d_curve_1, 2), gs.to_ndarray(d_curve_2, 2), gs.to_ndarray(d_curve_3, 2), ))) self.assertAllClose(result, expected) path_of_curves = gs.random.rand(n_discretized_curves, n_sampling_points, dim) result = srv_metric_r3.space_derivative(path_of_curves) expected = [] for i in range(n_discretized_curves): expected.append(srv_metric_r3.space_derivative(path_of_curves[i])) expected = gs.stack(expected) self.assertAllClose(result, expected)
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 predict(self, X): """Predict the labels for each data point. Label each data point with the cluster having the nearest centroid using metric distance. Parameters ---------- X : array-like, shape=[..., n_features] Input data. Returns ------- self : array-like, shape=[...,] Array of predicted cluster indices for each sample. """ if self.centroids is None: raise RuntimeError('fit needs to be called first.') dists = gs.stack( [self.metric.dist(centroid, X) for centroid in self.centroids], axis=1) dists = gs.squeeze(dists) belongs = gs.argmin(dists, -1) return belongs
def convert_to_planar_coordinates(self, points): """Convert polar coordinates to spherical one.""" coords_r, coords_theta = self.convert_to_polar_coordinates(points) coords_x = coords_r * gs.cos(coords_theta) coords_y = coords_r * gs.sin(coords_theta) planar_coords = gs.transpose(gs.stack((coords_x, coords_y))) return planar_coords
def random_point(self, n_samples=1, bound=1.0): """Sample in the product space from the uniform distribution. Parameters ---------- n_samples : int, optional Number of samples. bound : float Bound of the interval in which to sample for non compact manifolds. Optional, default: 1. Returns ------- samples : array-like, shape=[..., {dim, [n_manifolds, dim_each]}] Points sampled on the hypersphere. """ point_type = self.default_point_type geomstats.errors.check_parameter_accepted_values( point_type, "point_type", ["vector", "matrix"] ) if point_type == "vector": data = self.manifolds[0].random_point(n_samples, bound) if len(self.manifolds) > 1: for space in self.manifolds[1:]: samples = space.random_point(n_samples, bound) data = gs.concatenate([data, samples], axis=-1) return data point = [space.random_point(n_samples, bound) for space in self.manifolds] samples = gs.stack(point, axis=-2) return samples
def regularize(self, point, point_type=None): """Regularize the point into the manifold's canonical representation. Parameters ---------- point : array-like, shape=[..., {dim, [dim_2, dim_2]}] Point to be regularized. point_type : str, {'vector', 'matrix'} Representation of point. Returns ------- regularized_point : array-like, shape=[..., {dim, [dim_2, dim_2]}] Point in the manifold's canonical representation. """ if point_type is None: point_type = self.default_point_type geomstats.errors.check_parameter_accepted_values( point_type, 'point_type', ['vector', 'matrix']) if point_type == 'vector': intrinsic = self.metric.is_intrinsic(point) regularized_point = self._iterate_over_manifolds( 'regularize', {'point': point}, intrinsic) regularized_point = gs.hstack(regularized_point) elif point_type == 'matrix': regularized_point = [ manifold_i.regularize(point[:, i]) for i, manifold_i in enumerate(self.manifolds)] regularized_point = gs.stack(regularized_point, axis=1) return regularized_point
def projection_to_tangent_space(self, vector, base_point): """Project a vector in the tangent space. Project a vector in Minkowski space on the tangent space of the hyperbolic space at a base point. Parameters ---------- vector : array-like, shape=[n_samples, n_disks, dimension + 1] base_point : array-like, shape=[n_samples, n_disks, dimension + 1] Returns ------- tangent_vec : array-like, shape=[n_samples, n_disks, dimension + 1] """ n_disks = base_point.shape[1] hyperbolic_space = Hyperbolic(dimension=2, point_type=self.point_type) tangent_vec = gs.stack([ Hyperbolic.projection_to_tangent_space( self=hyperbolic_space, vector=vector[:, i_disk, :], base_point=base_point[:, i_disk, :]) for i_disk in range(n_disks) ], axis=1) return tangent_vec
def random_uniform(self, n_samples, point_type=None): """Sample in the product space from the uniform distribution. Parameters ---------- n_samples : int, optional Number of samples. point_type : str, {'vector', 'matrix'} Representation of point. Returns ------- samples : array-like, shape=[..., dim + 1] Points sampled on the hypersphere. """ if point_type is None: point_type = self.default_point_type geomstats.errors.check_parameter_accepted_values( point_type, 'point_type', ['vector', 'matrix']) if point_type == 'vector': data = self.manifolds[0].random_uniform(n_samples) if len(self.manifolds) > 1: for space in self.manifolds[1:]: samples = space.random_uniform(n_samples) data = gs.concatenate([data, samples], axis=-1) return data point = [ space.random_uniform(n_samples) for space in self.manifolds] samples = gs.stack(point, axis=1) return samples
def test_exp_vectorization(self): point = gs.array([[1., 1.], [1., 1.]]) tangent_vec = gs.array([[2., 1.], [2., 1.]]) result = self.metric.exp(tangent_vec, point) point = point[0] tangent_vec = tangent_vec[0] circle_center = point[0] + point[1] * tangent_vec[1] / tangent_vec[0] circle_radius = gs.sqrt((circle_center - point[0])**2 + point[1]**2) moebius_d = 1 moebius_c = 1 / (2 * circle_radius) moebius_b = circle_center - circle_radius moebius_a = (circle_center + circle_radius) * moebius_c point_complex = point[0] + 1j * point[1] tangent_vec_complex = tangent_vec[0] + 1j * tangent_vec[1] point_moebius = 1j * (moebius_d * point_complex - moebius_b)\ / (moebius_c * point_complex - moebius_a) tangent_vec_moebius = -1j * tangent_vec_complex * ( 1j * moebius_c * point_moebius + moebius_d)**2 end_point_moebius = point_moebius * gs.exp( tangent_vec_moebius / point_moebius) end_point_complex = (moebius_a * 1j * end_point_moebius + moebius_b)\ / (moebius_c * 1j * end_point_moebius + moebius_d) end_point_expected = gs.hstack( [np.real(end_point_complex), np.imag(end_point_complex)]) expected = gs.stack([end_point_expected, end_point_expected]) self.assertAllClose(result, expected)
def intrinsic_to_extrinsic_coords(point_intrinsic): """Convert point from intrinsic to extrensic coordinates. Convert the parameterization of a point in the hyperbolic space from its intrinsic coordinates, to its extrinsic coordinates in Minkowski space. Parameters ---------- point_intrinsic : array-like, shape=[..., n_disk, dim] Point in intrinsic coordinates. Returns ------- point_extrinsic : array-like, shape=[..., n_disks, dim + 1] Point in extrinsic coordinates. """ n_disks = point_intrinsic.shape[1] point_extrinsic = gs.stack( [ _Hyperbolic.change_coordinates_system( point_intrinsic[:, i_disk, ...], "intrinsic", "extrinsic") for i_disk in range(n_disks) ], axis=1, ) return point_extrinsic
def from_vector(vec, dtype=gs.float32): """Convert a vector into a symmetric matrix. Parameters ---------- vec : array-like, shape=[..., n(n+1)/2] Vector. dtype : dtype, {gs.float32, gs.float64} Data type object to use for the output. Optional. Default: gs.float32. Returns ------- mat : array-like, shape=[..., n, n] Symmetric matrix. """ vec_dim = vec.shape[-1] mat_dim = (gs.sqrt(8. * vec_dim + 1) - 1) / 2 if mat_dim != int(mat_dim): raise ValueError('Invalid input dimension, it must be of the form' '(n_samples, n * (n + 1) / 2)') mat_dim = int(mat_dim) shape = (mat_dim, mat_dim) mask = 2 * gs.ones(shape) - gs.eye(mat_dim) indices = list(zip(*gs.triu_indices(mat_dim))) vec = gs.cast(vec, dtype) upper_triangular = gs.stack( [gs.array_from_sparse(indices, data, shape) for data in vec]) mat = Matrices.to_symmetric(upper_triangular) * mask return mat
def random_uniform(self, n_samples, point_type=None): """Sample in the product space from the uniform distribution. Parameters ---------- n_samples : int, optional Number of samples. point_type : str, {'vector', 'matrix'} Representation of point. Returns ------- samples : array-like, shape=[n_samples, dimension + 1] Points sampled on the hypersphere. """ if point_type is None: point_type = self.default_point_type assert point_type in ['vector', 'matrix'] if point_type == 'vector': data = self.manifolds[0].random_uniform(n_samples) if len(self.manifolds) > 1: for i, space in enumerate(self.manifolds[1:]): data = gs.concatenate( [data, space.random_uniform(n_samples)], axis=1) return data else: point = [ space.random_uniform(n_samples) for space in self.manifolds ] return gs.stack(point, axis=1)
def to_tangent(self, vector, base_point): """Project a vector in the tangent space. Project a vector in Minkowski space on the tangent space of the hyperbolic space at a base point. Parameters ---------- vector : array-like, shape=[..., n_disks, dim + 1] Vector. base_point : array-like, shape=[..., n_disks, dim + 1] Base point. Returns ------- tangent_vec : array-like, shape=[..., n_disks, dim + 1] Tangent vector at base point. """ n_disks = base_point.shape[1] hyperbolic_space = Hyperboloid(2, self.coords_type) tangent_vec = gs.stack([hyperbolic_space.to_tangent( vector=vector[..., i_disk, :], base_point=base_point[..., i_disk, :]) for i_disk in range(n_disks)], axis=1) return tangent_vec
def test_split_horizontal_vertical(self, times, n_discretized_curves, curve_a, curve_b): """Test split horizontal vertical. Check that horizontal and vertical parts of any tangent vector are othogonal with respect to the SRVMetric inner product, and check vectorization. """ srv_metric_r3 = SRVMetric(r3) quotient_srv_metric_r3 = DiscreteCurves( ambient_manifold=r3).quotient_square_root_velocity_metric geod = srv_metric_r3.geodesic(initial_curve=curve_a, end_curve=curve_b) geod = geod(times) tangent_vec = n_discretized_curves * (geod[1, :, :] - geod[0, :, :]) ( tangent_vec_hor, tangent_vec_ver, _, ) = quotient_srv_metric_r3.split_horizontal_vertical( tangent_vec, curve_a) result = srv_metric_r3.inner_product(tangent_vec_hor, tangent_vec_ver, curve_a) expected = 0.0 self.assertAllClose(result, expected, atol=1e-4) tangent_vecs = n_discretized_curves * (geod[1:] - geod[:-1]) _, _, result = quotient_srv_metric_r3.split_horizontal_vertical( tangent_vecs, geod[:-1]) expected = [] for i in range(n_discretized_curves - 1): _, _, res = quotient_srv_metric_r3.split_horizontal_vertical( tangent_vecs[i], geod[i]) expected.append(res) expected = gs.stack(expected) self.assertAllClose(result, expected)
def sample(self, point, n_samples=1): """Sample from the beta distribution. Sample from the beta distribution with parameters provided by point. Parameters ---------- point : array-like, shape=[..., 2] Point representing a beta distribution. n_samples : int Number of points to sample with each pair of parameters in point. Optional, default: 1. Returns ------- samples : array-like, shape=[..., n_samples] Sample from beta distributions. """ geomstats.errors.check_belongs(point, self) point = gs.to_ndarray(point, to_ndim=2) samples = [] for param_a, param_b in point: samples.append(gs.array( beta.rvs(param_a, param_b, size=n_samples))) return samples[0] if len(point) == 1 else gs.stack(samples)
def regularize(self, point, point_type=None): """Regularize the point into the manifold's canonical representation. Parameters ---------- point : array-like, shape=[n_samples, dim] or shape=[n_samples, dim_2, dim_2] Point to be regularized. point_type : str, {'vector', 'matrix'} Representation of point. Returns ------- regularized_point : array-like, shape=[n_samples, dim] or shape=[n_samples, dim_2, dim_2] Point in the manifold's canonical representation. """ if point_type is None: point_type = self.default_point_type assert point_type in ['vector', 'matrix'] if point_type == 'vector': point = gs.to_ndarray(point, to_ndim=2) intrinsic = self.metric.is_intrinsic(point) regularized_point = self._iterate_over_manifolds( 'regularize', {'point': point}, intrinsic) regularized_point = gs.hstack(regularized_point) elif point_type == 'matrix': regularized_point = [ manifold_i.regularize(point[:, i]) for i, manifold_i in enumerate(self.manifolds)] regularized_point = gs.stack(regularized_point, axis=1) return regularized_point