def log(self, point, base_point, **kwargs): """Compute the affine-invariant logarithm map. Compute the Riemannian logarithm at point base_point, of point wrt the metric defined in inner_product. 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 of point at base_point. """ power_affine = self.power_affine if power_affine == 1: powers = SymmetricMatrices.powerm(base_point, [1. / 2, -1. / 2]) log = self._aux_log(point, powers[0], powers[1]) else: power_point = SymmetricMatrices.powerm(point, power_affine) powers = SymmetricMatrices.powerm( base_point, [power_affine / 2, -power_affine / 2]) log = self._aux_log( power_point, powers[0], powers[1]) log = self.space.inverse_differential_power( power_affine, log, base_point) return log
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 log(self, point, base_point, **kwargs): """Compute the Euclidean logarithm map. Compute the Euclidean logarithm at point base_point, of point. 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] Euclidean logarithm. """ power_euclidean = self.power_euclidean if power_euclidean == 1: log = point - base_point else: log = SPDMatrices.inverse_differential_power( power_euclidean, SymmetricMatrices.powerm(point, power_euclidean) - SymmetricMatrices.powerm(base_point, power_euclidean), base_point, ) return log
def log(self, point, base_point): """Compute the affine-invariant logarithm map. Compute the Riemannian logarithm at point base_point, of point wrt the metric defined in inner_product. This gives a tangent vector at point base_point. Parameters ---------- point : array-like, shape=[..., n, n] base_point : array-like, shape=[..., n, n] Returns ------- log : array-like, shape=[..., n, n] """ power_affine = self.power_affine if power_affine == 1: sqrt_base_point = SymmetricMatrices.powerm(base_point, 1. / 2) inv_sqrt_base_point = SymmetricMatrices.powerm(sqrt_base_point, -1) log = self._aux_log(point, sqrt_base_point, inv_sqrt_base_point) else: power_point = SymmetricMatrices.powerm(point, power_affine) power_sqrt_base_point = SymmetricMatrices.powerm( base_point, power_affine / 2) power_inv_sqrt_base_point = gs.linalg.inv(power_sqrt_base_point) log = self._aux_log(power_point, power_sqrt_base_point, power_inv_sqrt_base_point) log = self.space.inverse_differential_power( power_affine, log, base_point) return log
def exp(self, tangent_vec, base_point, **kwargs): """Compute the Euclidean exponential map. Compute the Euclidean exponential at point base_point of tangent vector tangent_vec. This gives a symmetric positive definite matrix. 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=[..., n, n] Euclidean exponential. """ power_euclidean = self.power_euclidean if power_euclidean == 1: exp = tangent_vec + base_point else: exp = SymmetricMatrices.powerm( SymmetricMatrices.powerm(base_point, power_euclidean) + SPDMatrices.differential_power(power_euclidean, tangent_vec, base_point), 1 / power_euclidean, ) return exp
def exp(self, tangent_vec, base_point): """Compute the affine-invariant exponential map. Compute the Riemannian exponential at point base_point of tangent vector tangent_vec wrt the metric defined in inner_product. This gives a symmetric positive definite matrix. Parameters ---------- tangent_vec : array-like, shape=[..., n, n] base_point : array-like, shape=[..., n, n] Returns ------- exp : array-like, shape=[..., n, n] """ power_affine = self.power_affine if power_affine == 1: sqrt_base_point = SymmetricMatrices.powerm(base_point, 1. / 2) inv_sqrt_base_point = SymmetricMatrices.powerm(sqrt_base_point, -1) exp = self._aux_exp(tangent_vec, sqrt_base_point, inv_sqrt_base_point) else: modified_tangent_vec = self.space.differential_power( power_affine, tangent_vec, base_point) power_sqrt_base_point = SymmetricMatrices.powerm( base_point, power_affine / 2) power_inv_sqrt_base_point = GeneralLinear.inverse( power_sqrt_base_point) exp = self._aux_exp(modified_tangent_vec, power_sqrt_base_point, power_inv_sqrt_base_point) exp = SymmetricMatrices.powerm(exp, 1 / power_affine) return exp
def exp_domain(tangent_vec, base_point): """Compute the domain of the Euclidean exponential map. Compute the real interval of time where the Euclidean geodesic starting at point `base_point` in direction `tangent_vec` is defined. Parameters ---------- tangent_vec : array-like, shape=[..., n, n] Tangent vector at base point. base_point : array-like, shape=[..., n, n] Base point. Returns ------- exp_domain : array-like, shape=[..., 2] Interval of time where the geodesic is defined. """ invsqrt_base_point = SymmetricMatrices.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., gs.array(-math.inf), - 1. / max_eig) inf_value = gs.to_ndarray(inf_value, to_ndim=2) sup_value = gs.where( min_eig >= 0., gs.array(-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 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 parallel_transport(self, tangent_vec, base_point, direction=None, end_point=None): r"""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)`. Denoting `tangent_vec_a` by `S`, `base_point` by `A`, and `end_point` by `B` or `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 : array-like, shape=[..., n, n] Tangent vector at base point to be transported. base_point : array-like, shape=[..., n, n] Point on the manifold of SPD matrices. Point to transport from direction : array-like, shape=[..., n, n] Tangent vector at base point, initial speed of the geodesic along which the parallel transport is computed. Unused if `end_point` is given. Optional, default: None. end_point : array-like, shape=[..., n, n] Point on the manifold of SPD matrices. Point to transport to. Optional, default: None. Returns ------- transported_tangent_vec: array-like, shape=[..., n, n] Transported tangent vector at exp_(base_point)(tangent_vec_b). """ if end_point is None: end_point = self.exp(direction, base_point) # 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, [1.0 / 2, -1.0 / 2]) pdt = SymmetricMatrices.powerm( Matrices.mul(inv_sqrt_bp, end_point, inv_sqrt_bp), 1.0 / 2) congruence_mat = Matrices.mul(sqrt_bp, pdt, inv_sqrt_bp) return Matrices.congruent(tangent_vec, congruence_mat)
def test_powerm(self): """Test of powerm method.""" sym_n = SymmetricMatrices(self.n) expected = gs.array( [[[1, 1. / 4., 0.], [1. / 4, 2., 0.], [0., 0., 1.]]]) expected = gs.cast(expected, gs.float64) power = gs.array(1. / 2) power = gs.cast(power, gs.float64) result = sym_n.powerm(expected, power) result = gs.matmul(result, gs.transpose(result, (0, 2, 1))) self.assertAllClose(result, expected)
def test_powerm(self): """Test of powerm method.""" sym_n = SymmetricMatrices(self.n) expected = gs.array( [[[1, 1.0 / 4.0, 0.0], [1.0 / 4, 2.0, 0.0], [0.0, 0.0, 1.0]]] ) power = gs.array(1.0 / 2.0) result = sym_n.powerm(expected, power) result = gs.matmul(result, gs.transpose(result, (0, 2, 1))) self.assertAllClose(result, expected)
def exp(self, tangent_vec, base_point, **kwargs): """Compute the affine-invariant exponential map. Compute the Riemannian exponential at point base_point of tangent vector tangent_vec wrt the metric defined in inner_product. This gives a symmetric positive definite matrix. 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=[..., n, n] Riemannian exponential. """ power_affine = self.power_affine if power_affine == 1: powers = SymmetricMatrices.powerm(base_point, [1.0 / 2, -1.0 / 2]) exp = self._aux_exp(tangent_vec, powers[0], powers[1]) else: modified_tangent_vec = SPDMatrices.differential_power( power_affine, tangent_vec, base_point ) power_sqrt_base_point = SymmetricMatrices.powerm( base_point, power_affine / 2 ) power_inv_sqrt_base_point = GeneralLinear.inverse(power_sqrt_base_point) exp = self._aux_exp( modified_tangent_vec, power_sqrt_base_point, power_inv_sqrt_base_point ) exp = SymmetricMatrices.powerm(exp, 1 / power_affine) return exp
def inner_product(self, tangent_vec_a, tangent_vec_b, base_point): """Compute the affine-invariant inner-product. Compute the inner-product of tangent_vec_a and tangent_vec_b at point base_point using the affine invariant Riemannian metric. 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] Base point. Returns ------- inner_product : array-like, shape=[..., n, n] Inner-product. """ power_affine = self.power_affine spd_space = SPDMatrices if power_affine == 1: inv_base_point = GeneralLinear.inverse(base_point) inner_product = self._aux_inner_product( tangent_vec_a, tangent_vec_b, inv_base_point ) else: modified_tangent_vec_a = spd_space.differential_power( power_affine, tangent_vec_a, base_point ) modified_tangent_vec_b = spd_space.differential_power( power_affine, tangent_vec_b, base_point ) power_inv_base_point = SymmetricMatrices.powerm(base_point, -power_affine) inner_product = self._aux_inner_product( modified_tangent_vec_a, modified_tangent_vec_b, power_inv_base_point ) inner_product = inner_product / (power_affine ** 2) return inner_product
def parallel_transport( self, tangent_vec_a, base_point, tangent_vec_b=None, end_point=None, n_steps=10, step="rk4", ): r"""Compute the parallel transport of a tangent vec along a geodesic. Approximation of the solution of the parallel transport of a tangent vector a along the geodesic defined by :math:`t \mapsto exp_{( base\_point)}(t* tangent\_vec\_b)`. The parallel transport equation is formulated in this case in [TP2021]_. Parameters ---------- tangent_vec_a : array-like, shape=[..., n, n] Tangent vector at `base_point` to transport. tangent_vec_b : array-like, shape=[..., n, n] Tangent vector ar `base_point`, initial velocity of the geodesic to transport along. base_point : array-like, shape=[..., n, n] Initial point of the geodesic. end_point : array-like, shape=[..., n, n] Point to transport to. Optional, default: None. n_steps : int Number of steps to use to approximate the solution of the ordinary differential equation. Optional, default: 100 step : str, {'euler', 'rk2', 'rk4'} Scheme to use in the integration scheme. Optional, default: 'rk4'. Returns ------- transported : array-like, shape=[..., n, n] Transported tangent vector at `exp_(base_point)(tangent_vec_b)`. References ---------- .. [TP2021] Yann Thanwerdas, Xavier Pennec. O(n)-invariant Riemannian metrics on SPD matrices. 2021. ⟨hal-03338601v2⟩ See Also -------- Integration module: geomstats.integrator """ if end_point is None: end_point = self.exp(tangent_vec_b, base_point) horizontal_lift_a = gs.linalg.solve_sylvester(base_point, base_point, tangent_vec_a) square_root_bp, inverse_square_root_bp = SymmetricMatrices.powerm( base_point, [0.5, -0.5]) end_point_lift = Matrices.mul(square_root_bp, end_point, square_root_bp) square_root_lift = SymmetricMatrices.powerm(end_point_lift, 0.5) horizontal_velocity = gs.matmul(inverse_square_root_bp, square_root_lift) partial_horizontal_velocity = Matrices.mul(horizontal_velocity, square_root_bp) partial_horizontal_velocity += Matrices.transpose( partial_horizontal_velocity) def force(state, time): horizontal_geodesic_t = ( 1 - time) * square_root_bp + time * horizontal_velocity geodesic_t = ((1 - time)**2 * base_point + time * (1 - time) * partial_horizontal_velocity + time**2 * end_point) align = Matrices.mul( horizontal_geodesic_t, Matrices.transpose(horizontal_velocity - square_root_bp), state, ) right = align + Matrices.transpose(align) return gs.linalg.solve_sylvester(geodesic_t, geodesic_t, -right) flow = integrate(force, horizontal_lift_a, n_steps=n_steps, step=step) final_align = Matrices.mul(end_point, flow[-1]) return final_align + Matrices.transpose(final_align)
def test_powerm(self, mat, power, expected): result = SymmetricMatrices.powerm(gs.array(mat), power) self.assertAllClose(result, gs.array(expected))