# Source code for pymanopt.manifolds.hyperbolic

```
import numpy as np
from pymanopt.manifolds.manifold import Manifold
[docs]class PoincareBall(Manifold):
r"""The Poincare ball.
The Poincare ball of dimension ``n``.
Elements are represented as arrays of shape ``(n,)`` if ``k = 1``.
For ``k > 1``, the class represents the product manifold of ``k`` Poincare
balls of dimension ``n``, in which case points are represented as arrays of
shape ``(k, n)``.
Since the manifold is open, the tangent space at every point is a copy of
:math:`\R^n`.
The Poincare ball is embedded in :math:`\R^n` and is a Riemannian manifold,
but it is not an embedded Riemannian submanifold since the metric is not
inherited from the Euclidean inner product of its ambient space.
Instead, the Riemannian metric is conformal to the Euclidean one (angles are
preserved), and it is given at every point :math:`\vmx` by
:math:`\inner{\vmu}{\vmv}_\vmx = \lambda_\vmx^2 \inner{\vmu}{\vmv}` where
:math:`\lambda_\vmx = 2 / (1 - \norm{\vmx}^2)` is the conformal factor.
This induces the following distance between two points :math:`\vmx` and
:math:`\vmy` on the manifold:
:math:`\dist_\manM(\vmx, \vmy) = \arccosh\parens{1 + 2 \frac{\norm{\vmx
- \vmy}^2}{(1 - \norm{\vmx}^2) (1 - \norm{\vmy}^2)}}.`
The norm here is understood as the Euclidean norm in the ambient space.
Args:
n: The dimension of the Poincare ball.
k: The number of elements in the product of Poincare balls.
"""
def __init__(self, n: int, *, k: int = 1):
self._n = n
self._k = k
if n < 1:
raise ValueError(f"Need n >= 1. Value given was n = {n}")
if k < 1:
raise ValueError(f"Need k >= 1. Value given was k = {k}")
if k == 1:
name = f"Poincare ball B({n})"
elif k >= 2:
name = f"Poincare ball B({n})^{k}"
dimension = k * n
super().__init__(name, dimension)
@property
def typical_dist(self):
return self.dim / 8
[docs] def inner_product(self, point, tangent_vector_a, tangent_vector_b):
factor = self.conformal_factor(point)
return np.tensordot(
tangent_vector_a,
tangent_vector_b * factor**2,
axes=tangent_vector_a.ndim,
)
to_tangent_space = projection
[docs] def norm(self, point, tangent_vector):
return np.sqrt(
self.inner_product(point, tangent_vector, tangent_vector)
)
[docs] def random_point(self):
array = np.random.normal(size=(self._k, self._n))
norm = np.linalg.norm(array, axis=-1, keepdims=True)
radius = np.random.uniform(size=(self._k, 1)) ** (1.0 / self._n)
point = array / norm * radius
if self._k == 1:
return point[0]
return point
[docs] def random_tangent_vector(self, point):
vector = np.random.normal(size=point.shape)
return vector / self.norm(point, vector)
[docs] def dist(self, point_a, point_b):
norm_point_a = np.linalg.norm(point_a, axis=-1) ** 2
norm_point_b = np.linalg.norm(point_b, axis=-1) ** 2
norm_difference = np.linalg.norm(point_a - point_b, axis=-1) ** 2
return np.linalg.norm(
np.arccosh(
1
+ 2
* norm_difference
/ ((1 - norm_point_a) * (1 - norm_point_b))
)
)
[docs] def euclidean_to_riemannian_gradient(self, point, euclidean_gradient):
# The hyperbolic metric tensor is conformal to the Euclidean one, so
# the Euclidean gradient is simply rescaled.
factor = self.conformal_factor(point)
return euclidean_gradient * (1 / factor**2)
[docs] def euclidean_to_riemannian_hessian(
self, point, euclidean_gradient, euclidean_hessian, tangent_vector
):
# This expression is derived from the Koszul formula.
factor = self.conformal_factor(point)
return (
np.sum(euclidean_gradient * point, axis=-1, keepdims=True)
* tangent_vector
- np.sum(point * tangent_vector, axis=-1, keepdims=True)
* euclidean_gradient
- np.sum(
euclidean_gradient * tangent_vector, axis=-1, keepdims=True
)
* point
+ euclidean_hessian / factor
) / factor
[docs] def exp(self, point, tangent_vector):
norm_point = np.linalg.norm(tangent_vector, axis=-1, keepdims=True)
factor = self.conformal_factor(point)
return self.mobius_addition(
point,
tangent_vector
* (
np.tanh(norm_point * factor / 2)
/ (norm_point + (norm_point == 0))
),
)
retraction = exp
[docs] def log(self, point_a, point_b):
w = self.mobius_addition(-point_a, point_b)
norm_w = np.linalg.norm(w, axis=-1, keepdims=True)
factor = self.conformal_factor(point_a)
return np.arctanh(norm_w) * w / norm_w / (factor / 2)
[docs] def pair_mean(self, point_a, point_b):
return self.exp(point_a, self.log(point_a, point_b) / 2)
[docs] def mobius_addition(self, point_a, point_b):
"""Möbius addition.
Special non-associative and non-commutative operation which is closed
in the Poincare ball.
Args:
point_a: The first point.
point_b: The second point.
Returns:
The Möbius sum of ``point_a`` and ``point_b``.
"""
scalar_product = np.sum(point_a * point_b, axis=-1, keepdims=True)
norm_point_a = np.sum(point_a * point_a, axis=-1, keepdims=True)
norm_point_b = np.sum(point_b * point_b, axis=-1, keepdims=True)
return (
point_a * (1 + 2 * scalar_product + norm_point_b)
+ point_b * (1 - norm_point_a)
) / (1 + 2 * scalar_product + norm_point_a * norm_point_b)
[docs] def conformal_factor(self, point):
"""The conformal factor for a point.
Args:
point: The point for which to compute the conformal factor.
Returns:
The conformal factor.
If ``point`` is a point on the product manifold of ``k`` Poincare
balls, the return value will be an array of shape ``(k,1)``.
The singleton dimension is explicitly kept to simplify
multiplication of ``point`` by the conformal factor on product
manifolds.
"""
return 2 / (1 - np.sum(point * point, axis=-1, keepdims=True))
```