def _assess_dimension_(spectrum, rank, n_samples, n_features): """Compute the likelihood of a rank ``rank`` dataset The dataset is assumed to be embedded in gaussian noise of shape(n, dimf) having spectrum ``spectrum``. Parameters ---------- spectrum : array of shape (n) Data spectrum. rank : int Tested rank value. n_samples : int Number of samples. n_features : int Number of features. Returns ------- ll : float, The log-likelihood Notes ----- This implements the method of `Thomas P. Minka: Automatic Choice of Dimensionality for PCA. NIPS 2000: 598-604` """ if rank > len(spectrum): raise ValueError("The tested rank cannot exceed the rank of the" " dataset") pu = -rank * log(2.) for i in range(rank): pu += (gammaln( (n_features - i) / 2.) - log(gs.pi) * (n_features - i) / 2.) pl = gs.sum(gs.log(spectrum[:rank])) pl = -pl * n_samples / 2. if rank == n_features: pv = 0 v = 1 else: v = gs.sum(spectrum[rank:]) / (n_features - rank) pv = -gs.log(v) * n_samples * (n_features - rank) / 2. m = n_features * rank - rank * (rank + 1.) / 2. pp = log(2. * gs.pi) * (m + rank + 1.) / 2. pa = 0. spectrum_ = spectrum.copy() spectrum_[rank:n_features] = v for i in range(rank): for j in range(i + 1, len(spectrum)): pa += log((spectrum[i] - spectrum[j]) * (1. / spectrum_[j] - 1. / spectrum_[i])) + log(n_samples) ll = pu + pl + pv + pp - pa / 2. - rank * log(n_samples) / 2. return ll
def squared_dist(self, point_a, point_b, **kwargs): """Compute the Cholesky Metric squared distance. Compute the Riemannian squared distance between point_a and point_b. Parameters ---------- point_a : array-like, shape=[..., n, n] Point. point_b : array-like, shape=[..., n, n] Point. Returns ------- _ : array-like, shape=[...] Riemannian squared distance. """ log_diag_a = gs.log(Matrices.diagonal(point_a)) log_diag_b = gs.log(Matrices.diagonal(point_b)) diag_diff = log_diag_a - log_diag_b squared_dist_diag = gs.sum((diag_diff) ** 2, axis=-1) sl_a = Matrices.to_strictly_lower_triangular(point_a) sl_b = Matrices.to_strictly_lower_triangular(point_b) sl_diff = sl_a - sl_b squared_dist_sl = Matrices.frobenius_product(sl_diff, sl_diff) return squared_dist_sl + squared_dist_diag
def random_von_mises_fisher(self, kappa=10, n_samples=1): """Sample in the 2-sphere with the von Mises distribution. Sample in the 2-sphere with the von Mises distribution centered in the north pole. Parameters ---------- kappa : int, optional n_samples : int, optional Returns ------- point : array-like """ if self.dimension != 2: raise NotImplementedError( 'Sampling from the von Mises Fisher distribution' 'is only implemented in dimension 2.') angle = 2. * gs.pi * gs.random.rand(n_samples) angle = gs.to_ndarray(angle, to_ndim=2, axis=1) unit_vector = gs.hstack((gs.cos(angle), gs.sin(angle))) scalar = gs.random.rand(n_samples) coord_z = 1. + 1. / kappa * gs.log(scalar + (1. - scalar) * gs.exp(gs.array(-2. * kappa))) coord_z = gs.to_ndarray(coord_z, to_ndim=2, axis=1) coord_xy = gs.sqrt(1. - coord_z**2) * unit_vector point = gs.hstack((coord_xy, coord_z)) return point
def test_differential_log(self): base_point = gs.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 4.]]) tangent_vec = gs.array([[1., 1., 3.], [1., 1., 3.], [3., 3., 4.]]) result = self.space.differential_log(tangent_vec, base_point) x = 2 * gs.log(2) expected = gs.array([[[1., 1., x], [1., 1., x], [x, x, 1]]]) self.assertAllClose(result, expected)
def dist(self, point_a, point_b): """Compute the geodesic distance between two points. Parameters ---------- point_a : array-like, shape=[..., dim] First point in the Poincare ball. point_b : array-like, shape=[..., dim] Second point in the Poincare ball. Returns ------- dist : array-like, shape=[...,] Geodesic distance between the two points. """ point_a_norm = gs.clip(gs.sum(point_a**2, -1), 0.0, 1 - EPSILON) point_b_norm = gs.clip(gs.sum(point_b**2, -1), 0.0, 1 - EPSILON) diff_norm = gs.sum((point_a - point_b)**2, -1) norm_function = 1 + 2 * diff_norm / ((1 - point_a_norm) * (1 - point_b_norm)) dist = gs.log(norm_function + gs.sqrt(norm_function**2 - 1)) dist *= self.scale return dist
def log(self, point, base_point, **kwargs): """Compute the Cholesky logarithm map. Compute the Riemannian logarithm at point base_point, of point wrt the Cholesky 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. """ sl_base_point = Matrices.to_strictly_lower_triangular(base_point) sl_point = Matrices.to_strictly_lower_triangular(point) diag_base_point = Matrices.diagonal(base_point) diag_point = Matrices.diagonal(point) diag_product_logm = gs.log(gs.divide(diag_point, diag_base_point)) sl_log = sl_point - sl_base_point diag_log = gs.vec_to_diag(diag_base_point * diag_product_logm) log = sl_log + diag_log return log
def dist(self, point_a, point_b): """Compute the geodesic distance between two points. Parameters ---------- point_a : array-like, shape=[n_samples, dim] First point in hyperbolic space. point_b : array-like, shape=[n_samples, dim] Second point in hyperbolic space. Returns ------- dist : array-like, shape=[n_samples, 1] Geodesic distance between the two points. """ point_a = gs.to_ndarray(point_a, to_ndim=2) point_b = gs.to_ndarray(point_b, to_ndim=2) point_a_norm = gs.clip(gs.sum(point_a**2, -1), 0., 1 - EPSILON) point_b_norm = gs.clip(gs.sum(point_b**2, -1), 0., 1 - EPSILON) diff_norm = gs.sum((point_a - point_b)**2, -1) norm_function = 1 + 2 * \ diff_norm / ((1 - point_a_norm) * (1 - point_b_norm)) dist = gs.log(norm_function + gs.sqrt(norm_function**2 - 1)) dist = gs.to_ndarray(dist, to_ndim=1) dist = gs.to_ndarray(dist, to_ndim=2, axis=1) dist *= self.scale return dist
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 test_inverse_differential_log(self): """Test of inverse_differential_log method.""" base_point = gs.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 4.]]) x = 2 * gs.log(2.) tangent_vec = gs.array([[1., 1., x], [1., 1., x], [x, x, 1]]) result = self.space.inverse_differential_log(tangent_vec, base_point) expected = gs.array([[1., 1., 3.], [1., 1., 3.], [3., 3., 4.]]) self.assertAllClose(result, expected)
def test_differential_log(self): """Test of differential_log method.""" base_point = gs.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 4.0]]) tangent_vec = gs.array([[1.0, 1.0, 3.0], [1.0, 1.0, 3.0], [3.0, 3.0, 4.0]]) result = self.space.differential_log(tangent_vec, base_point) x = 2 * gs.log(2.0) expected = gs.array([[1.0, 1.0, x], [1.0, 1.0, x], [x, x, 1]]) self.assertAllClose(result, expected)
def test_log_euclidean_inner_product(self): base_point = gs.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 4.]]) tangent_vec = gs.array([[1., 1., 3.], [1., 1., 3.], [3., 3., 4.]]) metric = self.metric_logeuclidean result = metric.inner_product(tangent_vec, tangent_vec, base_point) x = 2 * gs.log(2) expected = 5. + 4. * x**2 self.assertAllClose(result, expected)
def dist_broadcast(self, point_a, point_b): """Compute the geodesic distance between points. If n_samples_a == n_samples_b then dist is the element-wise distance result of a point in points_a with the point from points_b of the same index. If n_samples_a not equal to n_samples_b then dist is the result of applying geodesic distance for each point from points_a to all points from points_b. Parameters ---------- point_a : array-like, shape=[n_samples_a, dim] Set of points in hyperbolic space. point_b : array-like, shape=[n_samples_b, dim] Second set of points in hyperbolic space. Returns ------- dist : array-like, shape=[n_samples_a, dim] or [n_samples_a, n_samples_b, dim] Geodesic distance between the two points. """ if point_a.shape[-1] != point_b.shape[-1]: raise ValueError('Manifold dimensions not equal') if point_a.shape[0] != point_b.shape[0]: point_a_broadcast, point_b_broadcast = gs.broadcast_arrays( point_a[:, None], point_b[None, ...]) point_a_flatten = gs.reshape(point_a_broadcast, (-1, point_a_broadcast.shape[-1])) point_b_flatten = gs.reshape(point_b_broadcast, (-1, point_b_broadcast.shape[-1])) point_a_norm = gs.clip(gs.sum(point_a_flatten**2, -1), 0., 1 - EPSILON) point_b_norm = gs.clip(gs.sum(point_b_flatten**2, -1), 0., 1 - EPSILON) square_diff = (point_a_flatten - point_b_flatten)**2 diff_norm = gs.sum(square_diff, -1) norm_function = 1 + 2 * \ diff_norm / ((1 - point_a_norm) * (1 - point_b_norm)) dist = gs.log(norm_function + gs.sqrt(norm_function**2 - 1)) dist *= self.scale dist = gs.reshape(dist, (point_a.shape[0], point_b.shape[0])) dist = gs.squeeze(dist) elif point_a.shape == point_b.shape: dist = self.dist(point_a, point_b) return dist
def test_log_euclidean_inner_product(self): """Test of SPDMetricLogEuclidean.inner_product method.""" base_point = gs.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 4.0]]) tangent_vec = gs.array([[1.0, 1.0, 3.0], [1.0, 1.0, 3.0], [3.0, 3.0, 4.0]]) metric = self.metric_logeuclidean result = metric.inner_product(tangent_vec, tangent_vec, base_point) x = 2 * gs.log(2.0) expected = 5.0 + 4.0 * x ** 2 self.assertAllClose(result, expected)
def test_euclidean_frechet_mean(self): """Test if the frechet mean of the samples is close to mean""" mean = gs.zeros(self.n) cov = gs.eye(self.n) data = LogNormal(self.euclidean, mean, cov).sample(5000) log_data = gs.log(data) fm = gs.mean(log_data, axis=0) expected = mean result = fm self.assertAllClose(result, expected, atol=5 * 1e-2)
def log_sigmoid(vector): """Logsigmoid function. Apply log sigmoid function. Parameters ---------- vector : array-like, shape=[n_samples, dim] Returns ------- result : array-like, shape=[n_samples, dim] """ return gs.log((1 / (1 + gs.exp(-vector))))
def dist(self, point_a, point_b): """Compute the geodesic distance between two points. Parameters ---------- point_a : array-like, shape=[n_samples, dimension + 1] First point in hyperbolic space. point_b : array-like, shape=[n_samples, dimension + 1] Second point in hyperbolic space. Returns ------- dist : array-like, shape=[n_samples, 1] Geodesic distance between the two points. """ if self.point_type == 'extrinsic': sq_norm_a = self.embedding_metric.squared_norm(point_a) sq_norm_b = self.embedding_metric.squared_norm(point_b) inner_prod = self.embedding_metric.inner_product(point_a, point_b) cosh_angle = -inner_prod / gs.sqrt(sq_norm_a * sq_norm_b) cosh_angle = gs.clip(cosh_angle, 1.0, 1e24) dist = gs.arccosh(cosh_angle) return self.scale * dist elif self.point_type == 'ball': point_a_norm = gs.clip(gs.sum(point_a**2, -1), 0., 1 - EPSILON) point_b_norm = gs.clip(gs.sum(point_b**2, -1), 0., 1 - EPSILON) diff_norm = gs.sum((point_a - point_b)**2, -1) norm_function = 1 + 2 * \ diff_norm / ((1 - point_a_norm) * (1 - point_b_norm)) dist = gs.log(norm_function + gs.sqrt(norm_function**2 - 1)) dist = gs.to_ndarray(dist, to_ndim=1) dist = gs.to_ndarray(dist, to_ndim=2, axis=1) return self.scale * dist else: raise NotImplementedError( 'dist is only implemented for ball and extrinsic')
def squared_dist(self, point_a, point_b, **kwargs): """Compute squared distance associated with the exponential Fisher Rao metric. Parameters ---------- point_a : array-like, shape=[...,] Point representing an exponential distribution (scale parameter). point_b : array-like, shape=[...,] (same shape as point_a) Point representing a exponential distribution (scale parameter). Returns ------- squared_dist : array-like, shape=[...,] Squared distance between points point_a and point_b. """ return gs.log(point_a / point_b)**2
def random_von_mises_fisher(self, kappa=10, n_samples=1): """ Sample in the 2-sphere with the von Mises distribution centered in the north pole. """ if self.dimension != 2: raise NotImplementedError( 'Sampling from the von Mises Fisher distribution' 'is only implemented in dimension 2.') angle = 2 * gs.pi * gs.random.rand(n_samples) unit_vector = gs.vstack((gs.cos(angle), gs.sin(angle))) scalar = gs.random.rand(n_samples) coord_z = 1 + 1/kappa*gs.log(scalar + (1-scalar)*gs.exp(-2*kappa)) coord_xy = gs.sqrt(1 - coord_z**2) * unit_vector point = gs.vstack((coord_xy, coord_z)) return point.T
def random_von_mises_fisher(self, kappa=10, n_samples=1): """Sample in the 2-sphere with the von Mises distribution. Sample in the 2-sphere with the von Mises distribution centered at the north pole. References ---------- https://en.wikipedia.org/wiki/Von_Mises_distribution Parameters ---------- kappa : int Kappa parameter of the von Mises distribution. Optional, default: 10. n_samples : int Number of samples. Optional, default: 1. Returns ------- point : array-like, shape=[..., 3] Points sampled on the sphere in extrinsic coordinates in Euclidean space of dimension 3. """ if self.dim != 2: raise NotImplementedError( 'Sampling from the von Mises Fisher distribution' 'is only implemented in dimension 2.') angle = 2. * gs.pi * gs.random.rand(n_samples) angle = gs.to_ndarray(angle, to_ndim=2, axis=1) unit_vector = gs.hstack((gs.cos(angle), gs.sin(angle))) scalar = gs.random.rand(n_samples) coord_z = 1. + 1. / kappa * gs.log( scalar + (1. - scalar) * gs.exp(gs.array(-2. * kappa))) coord_z = gs.to_ndarray(coord_z, to_ndim=2, axis=1) coord_xy = gs.sqrt(1. - coord_z**2) * unit_vector point = gs.hstack((coord_xy, coord_z)) if n_samples == 1: point = gs.squeeze(point, axis=0) return point
def group_log(sym_mat): """ Group logarithm of the Lie group of all invertible matrices has a straight-forward computation for symmetric positive definite matrices. """ sym_mat = gs.to_ndarray(sym_mat, to_ndim=3) n_sym_mats, mat_dim, _ = sym_mat.shape assert gs.all(is_symmetric(sym_mat)) sym_mat = make_symmetric(sym_mat) [eigenvalues, vectors] = gs.linalg.eigh(sym_mat) assert gs.all(eigenvalues > 0) log_eigenvalues = gs.log(eigenvalues) aux = gs.einsum('ijk,ik->ijk', vectors, log_eigenvalues) log_mat = gs.einsum('ijk,ilk->ijl', aux, vectors) log_mat = gs.to_ndarray(log_mat, to_ndim=3) return log_mat
def random_von_mises_fisher( self, mu=None, kappa=10, n_samples=1, max_iter=100): """Sample with the von Mises-Fisher distribution. This distribution corresponds to the maximum entropy distribution given a mean. In dimension 2, a closed form expression is available. In larger dimension, rejection sampling is used according to [Wood94]_ References ---------- https://en.wikipedia.org/wiki/Von_Mises-Fisher_distribution .. [Wood94] Wood, Andrew T. A. “Simulation of the von Mises Fisher Distribution.” Communications in Statistics - Simulation and Computation, June 27, 2007. https://doi.org/10.1080/03610919408813161. Parameters ---------- mu : array-like, shape=[dim] Mean parameter of the distribution. kappa : float Kappa parameter of the von Mises distribution. Optional, default: 10. n_samples : int Number of samples. Optional, default: 1. max_iter : int Maximum number of trials in the rejection algorithm. In case it is reached, the current number of samples < n_samples is returned. Optional, default: 100. Returns ------- point : array-like, shape=[n_samples, dim + 1] Points sampled on the sphere in extrinsic coordinates in Euclidean space of dimension dim + 1. """ dim = self.dim if dim == 2: angle = 2. * gs.pi * gs.random.rand(n_samples) angle = gs.to_ndarray(angle, to_ndim=2, axis=1) unit_vector = gs.hstack((gs.cos(angle), gs.sin(angle))) scalar = gs.random.rand(n_samples) coord_x = 1. + 1. / kappa * gs.log( scalar + (1. - scalar) * gs.exp(gs.array(-2. * kappa))) coord_x = gs.to_ndarray(coord_x, to_ndim=2, axis=1) coord_yz = gs.sqrt(1. - coord_x ** 2) * unit_vector sample = gs.hstack((coord_x, coord_yz)) else: # rejection sampling in the general case sqrt = gs.sqrt(4 * kappa ** 2. + dim ** 2) envelop_param = (-2 * kappa + sqrt) / dim node = (1. - envelop_param) / (1. + envelop_param) correction = kappa * node + dim * gs.log(1. - node ** 2) n_accepted, n_iter = 0, 0 result = [] while (n_accepted < n_samples) and (n_iter < max_iter): sym_beta = beta.rvs( dim / 2, dim / 2, size=n_samples - n_accepted) sym_beta = gs.cast(sym_beta, node.dtype) coord_x = (1 - (1 + envelop_param) * sym_beta) / ( 1 - (1 - envelop_param) * sym_beta) accept_tol = gs.random.rand(n_samples - n_accepted) criterion = ( kappa * coord_x + dim * gs.log(1 - node * coord_x) - correction) > gs.log(accept_tol) result.append(coord_x[criterion]) n_accepted += gs.sum(criterion) n_iter += 1 if n_accepted < n_samples: logging.warning( 'Maximum number of iteration reached in rejection ' 'sampling before n_samples were accepted.') coord_x = gs.concatenate(result) coord_rest = _Hypersphere(dim - 1).random_uniform(n_accepted) coord_rest = gs.einsum( '...,...i->...i', gs.sqrt(1 - coord_x ** 2), coord_rest) sample = gs.concatenate([coord_x[..., None], coord_rest], axis=1) if mu is not None: sample = utils.rotate_points(sample, mu) return sample if (n_samples > 1) else sample[0]
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] """ n_tangent_vecs, _, _ = tangent_vec.shape n_base_points, _, n = base_point.shape eigvalues, eigvectors = gs.linalg.eigh(base_point) eigvalues = gs.to_ndarray(eigvalues, to_ndim=3, axis=1) transp_eigvalues = gs.transpose(eigvalues, (0, 2, 1)) if power == 0: powered_eigvalues = gs.log(eigvalues) elif power == math.inf: powered_eigvalues = gs.exp(eigvalues) else: powered_eigvalues = eigvalues**power transp_powered_eigvalues = gs.transpose(powered_eigvalues, (0, 2, 1)) ones = gs.ones((n_base_points, 1, n)) transp_ones = gs.transpose(ones, (0, 2, 1)) vertical_index = gs.matmul(transp_eigvalues, ones) horizontal_index = gs.matmul(transp_ones, eigvalues) one_matrix = gs.matmul(transp_ones, ones) vertical_index_power = gs.matmul(transp_powered_eigvalues, ones) horizontal_index_power = gs.matmul(transp_ones, powered_eigvalues) denominator = vertical_index - horizontal_index numerator = vertical_index_power - horizontal_index_power if power == 0: numerator = gs.where(denominator == 0, one_matrix, numerator) denominator = gs.where(denominator == 0, vertical_index, denominator) elif power == math.inf: numerator = gs.where(denominator == 0, vertical_index_power, numerator) denominator = gs.where(denominator == 0, one_matrix, denominator) else: numerator = gs.where(denominator == 0, power * vertical_index_power, numerator) denominator = gs.where(denominator == 0, vertical_index, denominator) transp_eigvectors = gs.transpose(eigvectors, (0, 2, 1)) temp_result = gs.matmul(transp_eigvectors, tangent_vec) temp_result = gs.matmul(temp_result, eigvectors) if n_base_points == n_tangent_vecs == 1: transp_eigvectors = gs.squeeze(transp_eigvectors, axis=0) eigvectors = gs.squeeze(eigvectors, axis=0) temp_result = gs.squeeze(temp_result, axis=0) numerator = gs.squeeze(numerator, axis=0) denominator = gs.squeeze(denominator, axis=0) return (eigvectors, transp_eigvectors, numerator, denominator, temp_result)
def aux_differential_power(self, 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 : int tangent_vec : array_like, shape=[n_samples, n, n] Tangent vectors. base_point : array_like, shape=[n_samples, n, n] Base points. Returns ------- eigvectors : array-like, shape=[n_samples, n, n] transp_eigvectors : array-like, shape=[n_samples, n, n] numerator : array-like, shape=[n_samples, n, n] denominator : array-like, shape=[n_samples, n, n] temp_result : array-like, shape=[n_samples, n, n] """ tangent_vec = gs.to_ndarray(tangent_vec, to_ndim=3) n_tangent_vecs, _, _ = tangent_vec.shape base_point = gs.to_ndarray(base_point, to_ndim=3) n_base_points, _, _ = base_point.shape assert (n_tangent_vecs == n_base_points or n_base_points == 1 or n_tangent_vecs == 1) eigvalues, eigvectors = gs.linalg.eigh(base_point) eigvalues = gs.to_ndarray(eigvalues, to_ndim=3, axis=1) transp_eigvalues = gs.transpose(eigvalues, (0, 2, 1)) if power == 0: powered_eigvalues = gs.log(eigvalues) elif power == math.inf: powered_eigvalues = gs.exp(eigvalues) else: powered_eigvalues = eigvalues**power transp_powered_eigvalues = gs.transpose(powered_eigvalues, (0, 2, 1)) ones = gs.ones((n_base_points, 1, self.n)) transp_ones = gs.transpose(ones, (0, 2, 1)) vertical_index = gs.matmul(transp_eigvalues, ones) horizontal_index = gs.matmul(transp_ones, eigvalues) one_matrix = gs.matmul(transp_ones, ones) vertical_index_power = gs.matmul(transp_powered_eigvalues, ones) horizontal_index_power = gs.matmul(transp_ones, powered_eigvalues) denominator = vertical_index - horizontal_index numerator = vertical_index_power - horizontal_index_power if power == 0: numerator = gs.where(denominator == 0, one_matrix, numerator) denominator = gs.where(denominator == 0, vertical_index, denominator) elif power == math.inf: numerator = gs.where(denominator == 0, vertical_index_power, numerator) denominator = gs.where(denominator == 0, one_matrix, denominator) else: numerator = gs.where(denominator == 0, power * vertical_index_power, numerator) denominator = gs.where(denominator == 0, vertical_index, denominator) transp_eigvectors = gs.transpose(eigvectors, (0, 2, 1)) temp_result = gs.matmul(transp_eigvectors, tangent_vec) temp_result = gs.matmul(temp_result, eigvectors) return (eigvectors, transp_eigvectors, numerator, denominator, temp_result)
def random_von_mises_fisher(self, mu=None, kappa=10, n_samples=1, max_iter=100): """Sample with the von Mises-Fisher distribution. This distribution corresponds to the maximum entropy distribution given a mean. In dimension 2, a closed form expression is available. In larger dimension, rejection sampling is used according to [Wood94]_ References ---------- https://en.wikipedia.org/wiki/Von_Mises-Fisher_distribution .. [Wood94] Wood, Andrew T. A. “Simulation of the von Mises Fisher Distribution.” Communications in Statistics - Simulation and Computation, June 27, 2007. https://doi.org/10.1080/03610919408813161. Parameters ---------- mu : array-like, shape=[dim] Mean parameter of the distribution. kappa : float Kappa parameter of the von Mises distribution. Optional, default: 10. n_samples : int Number of samples. Optional, default: 1. Returns ------- point : array-like, shape=[..., 3] Points sampled on the sphere in extrinsic coordinates in Euclidean space of dimension 3. """ dim = self.dim if dim == 2: angle = 2. * gs.pi * gs.random.rand(n_samples) angle = gs.to_ndarray(angle, to_ndim=2, axis=1) unit_vector = gs.hstack((gs.cos(angle), gs.sin(angle))) scalar = gs.random.rand(n_samples) coord_z = 1. + 1. / kappa * gs.log(scalar + (1. - scalar) * gs.exp(gs.array(-2. * kappa))) coord_z = gs.to_ndarray(coord_z, to_ndim=2, axis=1) coord_xy = gs.sqrt(1. - coord_z**2) * unit_vector sample = gs.hstack((coord_xy, coord_z)) if mu is not None: rot_vec = gs.cross(gs.array([0., 0., 1.]), mu) rot_vec *= gs.arccos(mu[-1]) / gs.linalg.norm(rot_vec) rot = SpecialOrthogonal( 3, 'vector').matrix_from_rotation_vector(rot_vec) sample = gs.matmul(sample, gs.transpose(rot)) else: if mu is None: mu = gs.array([0.] * dim + [1.]) # rejection sampling in the general case sqrt = gs.sqrt(4 * kappa**2. + dim**2) envelop_param = (-2 * kappa + sqrt) / dim node = (1. - envelop_param) / (1. + envelop_param) correction = kappa * node + dim * gs.log(1. - node**2) n_accepted, n_iter = 0, 0 result = [] while (n_accepted < n_samples) and (n_iter < max_iter): sym_beta = beta.rvs(dim / 2, dim / 2, size=n_samples - n_accepted) coord_z = (1 - (1 + envelop_param) * sym_beta) / ( 1 - (1 - envelop_param) * sym_beta) accept_tol = gs.random.rand(n_samples - n_accepted) criterion = (kappa * coord_z + dim * gs.log(1 - node * coord_z) - correction) > gs.log(accept_tol) result.append(coord_z[criterion]) n_accepted += gs.sum(criterion) n_iter += 1 if n_accepted < n_samples: logging.warning( 'Maximum number of iteration reached in rejection ' 'sampling before n_samples were accepted.') coord_z = gs.concatenate(result) coord_rest = self.random_uniform(n_accepted) coord_rest = self.to_tangent(coord_rest, mu) coord_rest = self.projection(coord_rest) coord_rest = gs.einsum('...,...i->...i', gs.sqrt(1 - coord_z**2), coord_rest) sample = coord_rest + coord_z[:, None] * mu[None, :] return sample if n_samples > 1 else sample[0]