Manifolds API
This page documents the core manifold operations in Hyperbolix. Each manifold is a class that provides geometric operations and automatic dtype casting.
Overview
Hyperbolix provides four base manifold classes plus a composition class:
- Euclidean: Flat Euclidean space (baseline)
- Poincaré Ball: Conformal model of hyperbolic space
- Hyperboloid: Lorentz/Minkowski model of hyperbolic space
- Proper Velocity: Unconstrained \(\mathbb{R}^n\) model from special relativity (Chen et al. 2026)
- Product Manifold: Heterogeneous-curvature product spaces \(M_1 \times M_2 \times \dots \times M_n\) (Gu et al. 2019)
All manifolds share a common interface defined by the Manifold protocol and support:
- Automatic dtype casting: Pass
dtype=jnp.float64 for higher precision
- vmap-native methods: Methods operate on single points; use
jax.vmap for batching
- JIT compatibility: All methods are JIT-compilable
- Learnable curvature: Use the
LearnableCurvature module to add trainable curvature to any model (softplus or log/exp reparameterization, optional clamping)
Manifold Protocol
The Curvature type
Manifold methods take the curvature as a positional c: Curvature argument
(hyperbolix.manifolds.Curvature). It is the union
ScalarCurvature | Sequence[ScalarCurvature], where ScalarCurvature = float |
jax.Array: single manifolds (Poincare, Hyperboloid, ProperVelocity,
Euclidean) take a scalar c, while ProductManifold takes a sequence
of per-factor scalars. Passing a traced jax.Array (e.g. the value returned by a
LearnableCurvature call) makes the curvature differentiable.
hyperbolix.manifolds.protocol.Manifold
Bases: Protocol
Structural protocol for manifold classes.
All concrete manifold classes (Poincare, Hyperboloid,
ProperVelocity, Euclidean, ProductManifold) satisfy this
protocol without modification. For single manifolds, c is a scalar
(ScalarCurvature); for ProductManifold, c is a sequence of
length n_factors (one curvature per factor).
The method signatures use the minimal common interface so that
manifold-specific optional parameters (e.g. version_idx, atol)
do not break compatibility.
Euclidean
Flat Euclidean space (identity operations).
hyperbolix.manifolds.euclidean.Euclidean
Euclidean(dtype: dtype = jnp.float32)
Bases: ManifoldBase
Euclidean manifold with automatic dtype casting.
Provides all manifold operations with automatic casting of array inputs
to the specified dtype. Curvature is always fixed at 0.0.
Args:
dtype: Target JAX dtype for computations (default: jnp.float32)
Examples:
>>> import jax.numpy as jnp
>>> from hyperbolix.manifolds.euclidean import Euclidean
>>>
>>> manifold = Euclidean(dtype=jnp.float64)
>>> x = jnp.array([1.0, 2.0])
>>> y = jnp.array([3.0, 4.0])
>>> d = manifold.dist(x, y)
Source code in hyperbolix/manifolds/euclidean.py
| def __init__(self, dtype: jnp.dtype = jnp.float32) -> None:
super().__init__(dtype, c=0.0)
|
proj
proj(
x: Float[Array, dim], c: Curvature = 0.0
) -> Float[Array, dim]
Project point onto Euclidean space (identity).
Source code in hyperbolix/manifolds/euclidean.py
| def proj(self, x: Float[Array, "dim"], c: Curvature = 0.0) -> Float[Array, "dim"]:
"""Project point onto Euclidean space (identity)."""
return _proj(self._cast(x), c)
|
addition
addition(
x: Float[Array, dim],
y: Float[Array, dim],
c: Curvature = 0.0,
) -> Float[Array, dim]
Add Euclidean points.
Source code in hyperbolix/manifolds/euclidean.py
| def addition(self, x: Float[Array, "dim"], y: Float[Array, "dim"], c: Curvature = 0.0) -> Float[Array, "dim"]:
"""Add Euclidean points."""
return _addition(self._cast(x), self._cast(y), c)
|
scalar_mul
scalar_mul(
r: float, x: Float[Array, dim], c: Curvature = 0.0
) -> Float[Array, dim]
Scalar multiplication.
Source code in hyperbolix/manifolds/euclidean.py
| def scalar_mul(self, r: float, x: Float[Array, "dim"], c: Curvature = 0.0) -> Float[Array, "dim"]:
"""Scalar multiplication."""
x = self._cast(x)
r_cast = jnp.asarray(r, dtype=x.dtype)
return _scalar_mul(r_cast, x, c) # type: ignore[arg-type]
|
dist
dist(
x: Float[Array, dim],
y: Float[Array, dim],
c: Curvature = 0.0,
) -> Float[Array, ""]
Compute distance.
Source code in hyperbolix/manifolds/euclidean.py
| def dist(self, x: Float[Array, "dim"], y: Float[Array, "dim"], c: Curvature = 0.0) -> Float[Array, ""]:
"""Compute distance."""
return _dist(self._cast(x), self._cast(y), c)
|
dist_0
dist_0(
x: Float[Array, dim], c: Curvature = 0.0
) -> Float[Array, ""]
Distance from origin.
Source code in hyperbolix/manifolds/euclidean.py
| def dist_0(self, x: Float[Array, "dim"], c: Curvature = 0.0) -> Float[Array, ""]:
"""Distance from origin."""
return _dist_0(self._cast(x), c)
|
expmap
expmap(
v: Float[Array, dim],
x: Float[Array, dim],
c: Curvature = 0.0,
) -> Float[Array, dim]
Exponential map.
Source code in hyperbolix/manifolds/euclidean.py
| def expmap(self, v: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature = 0.0) -> Float[Array, "dim"]:
"""Exponential map."""
return _expmap(self._cast(v), self._cast(x), c)
|
expmap_0
expmap_0(
v: Float[Array, dim], c: Curvature = 0.0
) -> Float[Array, dim]
Exponential map from origin.
Source code in hyperbolix/manifolds/euclidean.py
| def expmap_0(self, v: Float[Array, "dim"], c: Curvature = 0.0) -> Float[Array, "dim"]:
"""Exponential map from origin."""
return _expmap_0(self._cast(v), c)
|
retraction
retraction(
v: Float[Array, dim],
x: Float[Array, dim],
c: Curvature = 0.0,
) -> Float[Array, dim]
Retraction.
Source code in hyperbolix/manifolds/euclidean.py
| def retraction(self, v: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature = 0.0) -> Float[Array, "dim"]:
"""Retraction."""
return _retraction(self._cast(v), self._cast(x), c)
|
logmap
logmap(
y: Float[Array, dim],
x: Float[Array, dim],
c: Curvature = 0.0,
) -> Float[Array, dim]
Logarithmic map.
Source code in hyperbolix/manifolds/euclidean.py
| def logmap(self, y: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature = 0.0) -> Float[Array, "dim"]:
"""Logarithmic map."""
return _logmap(self._cast(y), self._cast(x), c)
|
logmap_0
logmap_0(
y: Float[Array, dim], c: Curvature = 0.0
) -> Float[Array, dim]
Logarithmic map from origin.
Source code in hyperbolix/manifolds/euclidean.py
| def logmap_0(self, y: Float[Array, "dim"], c: Curvature = 0.0) -> Float[Array, "dim"]:
"""Logarithmic map from origin."""
return _logmap_0(self._cast(y), c)
|
ptransp
ptransp(
v: Float[Array, dim],
x: Float[Array, dim],
y: Float[Array, dim],
c: Curvature = 0.0,
) -> Float[Array, dim]
Parallel transport.
Source code in hyperbolix/manifolds/euclidean.py
| def ptransp(
self, v: Float[Array, "dim"], x: Float[Array, "dim"], y: Float[Array, "dim"], c: Curvature = 0.0
) -> Float[Array, "dim"]:
"""Parallel transport."""
return _ptransp(self._cast(v), self._cast(x), self._cast(y), c)
|
ptransp_0
ptransp_0(
v: Float[Array, dim],
y: Float[Array, dim],
c: Curvature = 0.0,
) -> Float[Array, dim]
Parallel transport from origin.
Source code in hyperbolix/manifolds/euclidean.py
| def ptransp_0(self, v: Float[Array, "dim"], y: Float[Array, "dim"], c: Curvature = 0.0) -> Float[Array, "dim"]:
"""Parallel transport from origin."""
return _ptransp_0(self._cast(v), self._cast(y), c)
|
tangent_inner
tangent_inner(
u: Float[Array, dim],
v: Float[Array, dim],
x: Float[Array, dim],
c: Curvature = 0.0,
) -> Float[Array, ""]
Tangent inner product.
Source code in hyperbolix/manifolds/euclidean.py
| def tangent_inner(
self, u: Float[Array, "dim"], v: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature = 0.0
) -> Float[Array, ""]:
"""Tangent inner product."""
return _tangent_inner(self._cast(u), self._cast(v), self._cast(x), c)
|
tangent_norm
tangent_norm(
v: Float[Array, dim],
x: Float[Array, dim],
c: Curvature = 0.0,
) -> Float[Array, ""]
Tangent norm.
Source code in hyperbolix/manifolds/euclidean.py
| def tangent_norm(self, v: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature = 0.0) -> Float[Array, ""]:
"""Tangent norm."""
return _tangent_norm(self._cast(v), self._cast(x), c)
|
egrad2rgrad
egrad2rgrad(
grad: Float[Array, dim],
x: Float[Array, dim],
c: Curvature = 0.0,
) -> Float[Array, dim]
Euclidean to Riemannian gradient.
Source code in hyperbolix/manifolds/euclidean.py
| def egrad2rgrad(self, grad: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature = 0.0) -> Float[Array, "dim"]:
"""Euclidean to Riemannian gradient."""
return _egrad2rgrad(self._cast(grad), self._cast(x), c)
|
tangent_proj
tangent_proj(
v: Float[Array, dim],
x: Float[Array, dim],
c: Curvature = 0.0,
) -> Float[Array, dim]
Project onto tangent space.
Source code in hyperbolix/manifolds/euclidean.py
| def tangent_proj(self, v: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature = 0.0) -> Float[Array, "dim"]:
"""Project onto tangent space."""
return _tangent_proj(self._cast(v), self._cast(x), c)
|
is_in_manifold
is_in_manifold(
x: Float[Array, dim], c: Curvature = 0.0
) -> Array
Check if on manifold.
Source code in hyperbolix/manifolds/euclidean.py
| def is_in_manifold(self, x: Float[Array, "dim"], c: Curvature = 0.0) -> Array:
"""Check if on manifold."""
return _is_in_manifold(self._cast(x), c)
|
is_in_tangent_space
is_in_tangent_space(
v: Float[Array, dim],
x: Float[Array, dim],
c: Curvature = 0.0,
) -> Array
Check if in tangent space.
Source code in hyperbolix/manifolds/euclidean.py
| def is_in_tangent_space(self, v: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature = 0.0) -> Array:
"""Check if in tangent space."""
return _is_in_tangent_space(self._cast(v), self._cast(x), c)
|
Poincaré Ball
The Poincaré ball model with Möbius operations.
Distance Versions
The Poincaré dist method has a version_idx parameter selecting between 3 formulations:
VERSION_MOBIUS_DIRECT (0): Möbius addition formula (default, fastest)
VERSION_MOBIUS (1): Möbius via addition
VERSION_METRIC_TENSOR (2): Direct metric tensor integration
Constants are available as poincare.VERSION_MOBIUS_DIRECT etc., or from
hyperbolix.manifolds.poincare.
Apollonian weak metric
apollonian_dist(x, y, c) is the non-symmetric Apollonian weak metric \(\delta\)
(Papadopoulos & Troyanov, Weak metrics on Euclidean domains, Thm 2) — a weak metric,
not a geodesic distance:
\[\delta_c(x,y) = \log\!\left(\frac{\sqrt{c}\,\lVert x-y\rVert + \sqrt{c^2\lVert x\rVert^2\lVert y\rVert^2 - 2c\langle x,y\rangle + 1}}{1-c\lVert y\rVert^2}\right)\]
It satisfies \(\delta(x,x)=0\), \(\delta\ge 0\) and the triangle inequality, but
\(\delta(x,y) \neq \delta(y,x)\) in general. Its symmetrization recovers the geodesic distance:
\(\delta(x,y) + \delta(y,x) = \sqrt{c}\cdot\) dist(x, y, c).
Warning
The antisymmetric part of \(\delta\) is an exact coboundary (a difference of a per-point
potential), so it carries no circulation and is useless as an asymmetric quasimetric energy.
For that, use the busemann coordinate below with an external quasimetric combinator.
Busemann function (Chen et al. 2026)
busemann(x, v, c) is the closed-form point-to-horosphere coordinate \(B^v(x)\) for a unit
ideal direction \(v\in\mathbb{S}^{n-1}\) — the horospherical analog of the point-to-hyperplane
compute_mlr/compute_mlr_pp. v must be unit-norm (not normalized internally). It is an
intrinsic quantity, so Poincare.busemann and Hyperboloid.busemann agree under
poincare_to_hyperboloid, and \(B^v(\text{origin})=0\). Backs the *Busemann MLR/FC layers.
\[\mathbb{P}^n:\ B^v(x) = \tfrac{1}{\sqrt c}\log\!\frac{\lVert v-\sqrt c\,x\rVert^2}{1-c\lVert x\rVert^2}
\qquad\quad \mathbb{L}^n:\ B^v(x) = \tfrac{1}{\sqrt c}\log\!\big(\sqrt c\,(x_t-\langle x_s,v\rangle)\big)\]
hyperbolix.manifolds.poincare.Poincare
Poincare(dtype: dtype = jnp.float32, *, c: float = 1.0)
Bases: ManifoldBase
Poincaré ball manifold with automatic dtype casting.
Provides all manifold operations with automatic casting of array inputs
to the specified dtype. This eliminates the need for manual casting and
provides better numerical stability control.
Args:
dtype: Target JAX dtype for computations (default: jnp.float32)
c: Curvature value (default: 1.0). Must be positive.
Examples:
>>> import jax.numpy as jnp
>>> from hyperbolix.manifolds.poincare import Poincare, VERSION_MOBIUS_DIRECT
>>>
>>> # Create manifold with float64 for better precision
>>> manifold = Poincare(dtype=jnp.float64)
>>>
>>> # Static curvature
>>> manifold = Poincare(c=0.1)
>>> c = manifold.c # returns 0.1
Source code in hyperbolix/manifolds/_base.py
| def __init__(
self,
dtype: jnp.dtype = jnp.float32,
*,
c: float = 1.0,
) -> None:
self.dtype = dtype
self._c_val = c
|
proj
proj(
x: Float[Array, dim], c: Curvature
) -> Float[Array, dim]
Project point onto Poincaré ball by clipping norm.
Source code in hyperbolix/manifolds/poincare.py
| def proj(self, x: Float[Array, "dim"], c: Curvature) -> Float[Array, "dim"]:
"""Project point onto Poincaré ball by clipping norm."""
return _proj(self._cast(x), c)
|
gyration
gyration(
x: Float[Array, dim],
y: Float[Array, dim],
z: Float[Array, dim],
c: Curvature,
) -> Float[Array, dim]
Compute gyration gyr[x,y]z to restore commutativity.
Source code in hyperbolix/manifolds/poincare.py
| def gyration(
self, x: Float[Array, "dim"], y: Float[Array, "dim"], z: Float[Array, "dim"], c: Curvature
) -> Float[Array, "dim"]:
"""Compute gyration gyr[x,y]z to restore commutativity."""
return _gyration(self._cast(x), self._cast(y), self._cast(z), c)
|
addition
addition(
x: Float[Array, dim], y: Float[Array, dim], c: Curvature
) -> Float[Array, dim]
Möbius gyrovector addition x ⊕ y.
Source code in hyperbolix/manifolds/poincare.py
| def addition(self, x: Float[Array, "dim"], y: Float[Array, "dim"], c: Curvature) -> Float[Array, "dim"]:
"""Möbius gyrovector addition x ⊕ y."""
return _addition(self._cast(x), self._cast(y), c)
|
scalar_mul
scalar_mul(
r: float, x: Float[Array, dim], c: Curvature
) -> Float[Array, dim]
Scalar multiplication r ⊗ x on Poincaré ball.
Source code in hyperbolix/manifolds/poincare.py
| def scalar_mul(self, r: float, x: Float[Array, "dim"], c: Curvature) -> Float[Array, "dim"]:
"""Scalar multiplication r ⊗ x on Poincaré ball."""
x = self._cast(x)
r_cast = jnp.asarray(r, dtype=x.dtype)
return _scalar_mul(r_cast, x, c) # type: ignore[arg-type]
|
dist
dist(
x: Float[Array, dim],
y: Float[Array, dim],
c: Curvature,
version_idx: int = VERSION_MOBIUS_DIRECT,
) -> Float[Array, ""]
Compute geodesic distance between Poincaré ball points.
Source code in hyperbolix/manifolds/poincare.py
| def dist(
self,
x: Float[Array, "dim"],
y: Float[Array, "dim"],
c: Curvature,
version_idx: int = VERSION_MOBIUS_DIRECT,
) -> Float[Array, ""]:
"""Compute geodesic distance between Poincaré ball points."""
return _dist(self._cast(x), self._cast(y), c, version_idx)
|
dist_0
dist_0(
x: Float[Array, dim],
c: Curvature,
version_idx: int = VERSION_MOBIUS_DIRECT,
) -> Float[Array, ""]
Compute geodesic distance from Poincaré ball origin.
Source code in hyperbolix/manifolds/poincare.py
| def dist_0(self, x: Float[Array, "dim"], c: Curvature, version_idx: int = VERSION_MOBIUS_DIRECT) -> Float[Array, ""]:
"""Compute geodesic distance from Poincaré ball origin."""
return _dist_0(self._cast(x), c, version_idx)
|
apollonian_dist
apollonian_dist(
x: Float[Array, dim], y: Float[Array, dim], c: Curvature
) -> Float[Array, ""]
Apollonian weak metric δ(x, y) — non-symmetric; symmetrizes to √c·dist(x, y).
.. warning::
Although δ is non-symmetric, its antisymmetric part is an exact coboundary
(δ(x, y) - δ(y, x) is a difference of a per-point potential), so it carries
no circulation and is useless as a quasimetric energy. Do not reach for this
expecting genuine asymmetry — use a :meth:busemann coordinate fed to an external
quasimetric combinator (IQE/MRN) instead.
Source code in hyperbolix/manifolds/poincare.py
| def apollonian_dist(self, x: Float[Array, "dim"], y: Float[Array, "dim"], c: Curvature) -> Float[Array, ""]:
"""Apollonian weak metric δ(x, y) — non-symmetric; symmetrizes to √c·dist(x, y).
.. warning::
Although ``δ`` is non-symmetric, its antisymmetric part is an exact **coboundary**
(``δ(x, y) - δ(y, x)`` is a difference of a per-point potential), so it carries
**no circulation** and is useless as a quasimetric energy. Do not reach for this
expecting genuine asymmetry — use a :meth:`busemann` coordinate fed to an external
quasimetric combinator (IQE/MRN) instead.
"""
return _apollonian_dist(self._cast(x), self._cast(y), c)
|
expmap
expmap(
v: Float[Array, dim], x: Float[Array, dim], c: Curvature
) -> Float[Array, dim]
Exponential map: map tangent vector v at point x to manifold.
Source code in hyperbolix/manifolds/poincare.py
| def expmap(self, v: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature) -> Float[Array, "dim"]:
"""Exponential map: map tangent vector v at point x to manifold."""
return _expmap(self._cast(v), self._cast(x), c)
|
expmap_0
expmap_0(
v: Float[Array, dim], c: Curvature
) -> Float[Array, dim]
Exponential map from origin: map tangent vector v at origin to manifold.
Source code in hyperbolix/manifolds/poincare.py
| def expmap_0(self, v: Float[Array, "dim"], c: Curvature) -> Float[Array, "dim"]:
"""Exponential map from origin: map tangent vector v at origin to manifold."""
return _expmap_0(self._cast(v), c)
|
retraction
retraction(
v: Float[Array, dim], x: Float[Array, dim], c: Curvature
) -> Float[Array, dim]
Retraction: first-order approximation of exponential map.
Source code in hyperbolix/manifolds/poincare.py
| def retraction(self, v: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature) -> Float[Array, "dim"]:
"""Retraction: first-order approximation of exponential map."""
return _retraction(self._cast(v), self._cast(x), c)
|
logmap
logmap(
y: Float[Array, dim], x: Float[Array, dim], c: Curvature
) -> Float[Array, dim]
Logarithmic map: map point y to tangent space at point x.
Source code in hyperbolix/manifolds/poincare.py
| def logmap(self, y: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature) -> Float[Array, "dim"]:
"""Logarithmic map: map point y to tangent space at point x."""
return _logmap(self._cast(y), self._cast(x), c)
|
logmap_0
logmap_0(
y: Float[Array, dim], c: Curvature
) -> Float[Array, dim]
Logarithmic map from origin: map point y to tangent space at origin.
Source code in hyperbolix/manifolds/poincare.py
| def logmap_0(self, y: Float[Array, "dim"], c: Curvature) -> Float[Array, "dim"]:
"""Logarithmic map from origin: map point y to tangent space at origin."""
return _logmap_0(self._cast(y), c)
|
ptransp
ptransp(
v: Float[Array, dim],
x: Float[Array, dim],
y: Float[Array, dim],
c: Curvature,
) -> Float[Array, dim]
Parallel transport tangent vector v from point x to point y.
Source code in hyperbolix/manifolds/poincare.py
| def ptransp(
self, v: Float[Array, "dim"], x: Float[Array, "dim"], y: Float[Array, "dim"], c: Curvature
) -> Float[Array, "dim"]:
"""Parallel transport tangent vector v from point x to point y."""
return _ptransp(self._cast(v), self._cast(x), self._cast(y), c)
|
ptransp_0
ptransp_0(
v: Float[Array, dim], y: Float[Array, dim], c: Curvature
) -> Float[Array, dim]
Parallel transport tangent vector v from origin to point y.
Source code in hyperbolix/manifolds/poincare.py
| def ptransp_0(self, v: Float[Array, "dim"], y: Float[Array, "dim"], c: Curvature) -> Float[Array, "dim"]:
"""Parallel transport tangent vector v from origin to point y."""
return _ptransp_0(self._cast(v), self._cast(y), c)
|
tangent_inner
tangent_inner(
u: Float[Array, dim],
v: Float[Array, dim],
x: Float[Array, dim],
c: Curvature,
) -> Float[Array, ""]
Compute inner product of tangent vectors u and v at point x.
Source code in hyperbolix/manifolds/poincare.py
| def tangent_inner(
self, u: Float[Array, "dim"], v: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature
) -> Float[Array, ""]:
"""Compute inner product of tangent vectors u and v at point x."""
return _tangent_inner(self._cast(u), self._cast(v), self._cast(x), c)
|
tangent_norm
tangent_norm(
v: Float[Array, dim], x: Float[Array, dim], c: Curvature
) -> Float[Array, ""]
Compute norm of tangent vector v at point x.
Source code in hyperbolix/manifolds/poincare.py
| def tangent_norm(self, v: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature) -> Float[Array, ""]:
"""Compute norm of tangent vector v at point x."""
return _tangent_norm(self._cast(v), self._cast(x), c)
|
egrad2rgrad
egrad2rgrad(
grad: Float[Array, dim],
x: Float[Array, dim],
c: Curvature,
) -> Float[Array, dim]
Convert Euclidean gradient to Riemannian gradient.
Source code in hyperbolix/manifolds/poincare.py
| def egrad2rgrad(self, grad: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature) -> Float[Array, "dim"]:
"""Convert Euclidean gradient to Riemannian gradient."""
return _egrad2rgrad(self._cast(grad), self._cast(x), c)
|
tangent_proj
tangent_proj(
v: Float[Array, dim], x: Float[Array, dim], c: Curvature
) -> Float[Array, dim]
Project vector v onto tangent space at point x.
Source code in hyperbolix/manifolds/poincare.py
| def tangent_proj(self, v: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature) -> Float[Array, "dim"]:
"""Project vector v onto tangent space at point x."""
return _tangent_proj(self._cast(v), self._cast(x), c)
|
is_in_manifold
is_in_manifold(
x: Float[Array, dim], c: Curvature, atol: float = 1e-05
) -> Array
Check if point x lies in Poincaré ball.
Source code in hyperbolix/manifolds/poincare.py
| def is_in_manifold(self, x: Float[Array, "dim"], c: Curvature, atol: float = 1e-5) -> Array:
"""Check if point x lies in Poincaré ball."""
return _is_in_manifold(self._cast(x), c, atol)
|
is_in_tangent_space
is_in_tangent_space(
v: Float[Array, dim], x: Float[Array, dim], c: Curvature
) -> Array
Check if vector v lies in tangent space at point x.
Source code in hyperbolix/manifolds/poincare.py
| def is_in_tangent_space(self, v: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature) -> Array:
"""Check if vector v lies in tangent space at point x."""
return _is_in_tangent_space(self._cast(v), self._cast(x), c)
|
conformal_factor(
x: Float[Array, "... dim"], c: Curvature
) -> Float[Array, "... 1"]
Numerically stable conformal factor lambda(x) = 2 / (1 - c||x||^2).
Batch-compatible version that handles arbitrary leading dimensions.
Source code in hyperbolix/manifolds/poincare.py
| def conformal_factor(self, x: Float[Array, "... dim"], c: Curvature) -> Float[Array, "... 1"]:
"""Numerically stable conformal factor lambda(x) = 2 / (1 - c||x||^2).
Batch-compatible version that handles arbitrary leading dimensions.
"""
return _conformal_factor_batch(self._cast(x), c)
|
embed_spatial_0
embed_spatial_0(
v_spatial: Float[Array, "... n"],
) -> Float[Array, "... n"]
Identity embedding (the ball has no time coordinate). Kept for API parity.
Source code in hyperbolix/manifolds/poincare.py
| def embed_spatial_0(self, v_spatial: Float[Array, "... n"]) -> Float[Array, "... n"]:
"""Identity embedding (the ball has no time coordinate). Kept for API parity."""
return _embed_spatial_0(self._cast(v_spatial))
|
compute_mlr_pp
compute_mlr_pp(
x: Float[Array, "batch in_dim"],
z: Float[Array, "out_dim in_dim"],
r: Float[Array, "out_dim 1"],
c: Curvature,
clamping_factor: float,
smoothing_factor: float,
min_enorm: float = 1e-15,
) -> Float[Array, "batch out_dim"]
Compute HNN++ multinomial linear regression on the Poincare ball.
Source code in hyperbolix/manifolds/poincare.py
| def compute_mlr_pp(
self,
x: Float[Array, "batch in_dim"],
z: Float[Array, "out_dim in_dim"],
r: Float[Array, "out_dim 1"],
c: Curvature,
clamping_factor: float,
smoothing_factor: float,
min_enorm: float = 1e-15,
) -> Float[Array, "batch out_dim"]:
"""Compute HNN++ multinomial linear regression on the Poincare ball."""
return _compute_mlr_pp(self._cast(x), self._cast(z), self._cast(r), c, clamping_factor, smoothing_factor, min_enorm)
|
beta_concat
beta_concat(
points: Float[Array, "M n_i"], c: Curvature
) -> Float[Array, n]
Beta-concatenation of M equal-dimensional Poincaré ball points.
Source code in hyperbolix/manifolds/poincare.py
| def beta_concat(self, points: Float[Array, "M n_i"], c: Curvature) -> Float[Array, "n"]:
"""Beta-concatenation of M equal-dimensional Poincaré ball points."""
return _beta_concat(self._cast(points), c)
|
busemann
busemann(
x: Float[Array, dim], v: Float[Array, dim], c: Curvature
) -> Float[Array, ""]
Closed-form Poincaré Busemann function B^v(x) = (1/√c)·log(‖v - √c·x‖²/(1 - c‖x‖²)).
Point-to-horosphere coordinate (Chen et al. 2026, Eq. 3). v must be a unit
direction — it is not normalized internally. Single point (d,) → scalar; use
:func:jax.vmap for batching and over a direction set. B^v(origin) = 0, and it
matches :meth:Hyperboloid.busemann under the Poincaré↔Hyperboloid isometry.
See Also
For an asymmetric quasimetric energy, compose this Busemann coordinate with an
external Euclidean quasimetric (e.g. IQE/MRN). Do not reach for
:meth:apollonian_dist expecting asymmetry — it is a coboundary (symmetrizes to
√c·dist) and cannot deliver circulation.
Source code in hyperbolix/manifolds/poincare.py
| def busemann(self, x: Float[Array, "dim"], v: Float[Array, "dim"], c: Curvature) -> Float[Array, ""]:
"""Closed-form Poincaré Busemann function ``B^v(x) = (1/√c)·log(‖v - √c·x‖²/(1 - c‖x‖²))``.
Point-to-horosphere coordinate (Chen et al. 2026, Eq. 3). ``v`` must be a *unit*
direction — it is **not** normalized internally. Single point ``(d,)`` → scalar; use
:func:`jax.vmap` for batching and over a direction set. ``B^v(origin) = 0``, and it
matches :meth:`Hyperboloid.busemann` under the Poincaré↔Hyperboloid isometry.
See Also
--------
For an *asymmetric* quasimetric energy, compose this Busemann coordinate with an
external Euclidean quasimetric (e.g. IQE/MRN). Do **not** reach for
:meth:`apollonian_dist` expecting asymmetry — it is a coboundary (symmetrizes to
``√c·dist``) and cannot deliver circulation.
"""
return _busemann(self._cast(x), self._cast(v), c)
|
Hyperboloid
The hyperboloid (Lorentz) model with Minkowski geometry.
Lorentz Operations
The Hyperboloid class includes specialized operations for convolutional layers:
lorentz_boost: Lorentz boost transformation
distance_rescale: Distance-based rescaling
hcat: Lorentz direct concatenation for convolutions
log_radius_concat: log-radius–preserving concatenation (digamma-scaled hcat; Shi et al. 2026, Sec. 4.3)
hyperbolix.manifolds.hyperboloid.Hyperboloid
Hyperboloid(dtype: dtype = jnp.float32, *, c: float = 1.0)
Bases: ManifoldBase
Hyperboloid manifold with automatic dtype casting.
Provides all manifold operations with automatic casting of array inputs
to the specified dtype. This eliminates the need for manual casting and
provides better numerical stability control.
Args:
dtype: Target JAX dtype for computations (default: jnp.float32)
c: Curvature value (default: 1.0). Must be positive.
Examples:
>>> import jax.numpy as jnp
>>> from hyperbolix.manifolds.hyperboloid import Hyperboloid, VERSION_DEFAULT
>>>
>>> # Create manifold with float64 for better precision
>>> manifold = Hyperboloid(dtype=jnp.float64)
>>>
>>> # Custom curvature
>>> manifold = Hyperboloid(c=0.5)
>>> c = manifold.c # returns 0.5
Source code in hyperbolix/manifolds/_base.py
| def __init__(
self,
dtype: jnp.dtype = jnp.float32,
*,
c: float = 1.0,
) -> None:
self.dtype = dtype
self._c_val = c
|
create_origin
create_origin(
c: Curvature, dim: int
) -> Float[Array, dim_plus_1]
Create hyperboloid origin [1/√c, 0, ..., 0].
Source code in hyperbolix/manifolds/hyperboloid.py
| def create_origin(self, c: Curvature, dim: int) -> Float[Array, "dim_plus_1"]:
"""Create hyperboloid origin [1/√c, 0, ..., 0]."""
return _create_origin(c, dim, self.dtype)
|
minkowski_inner
minkowski_inner(
x: Float[Array, dim_plus_1], y: Float[Array, dim_plus_1]
) -> Float[Array, ""]
Compute Minkowski inner product ⟨x, y⟩_L = -x₀y₀ + ⟨x_rest, y_rest⟩.
Source code in hyperbolix/manifolds/hyperboloid.py
| def minkowski_inner(self, x: Float[Array, "dim_plus_1"], y: Float[Array, "dim_plus_1"]) -> Float[Array, ""]:
"""Compute Minkowski inner product ⟨x, y⟩_L = -x₀y₀ + ⟨x_rest, y_rest⟩."""
return _minkowski_inner(self._cast(x), self._cast(y))
|
proj
proj(
x: Float[Array, dim_plus_1], c: Curvature
) -> Float[Array, dim_plus_1]
Project point onto hyperboloid.
Source code in hyperbolix/manifolds/hyperboloid.py
| def proj(self, x: Float[Array, "dim_plus_1"], c: Curvature) -> Float[Array, "dim_plus_1"]:
"""Project point onto hyperboloid."""
return _proj(self._cast(x), c)
|
proj_batch
proj_batch(
x: Float[Array, "... dim_plus_1"], c: Curvature
) -> Float[Array, "... dim_plus_1"]
Project batched points onto hyperboloid (handles arbitrary leading dimensions).
Source code in hyperbolix/manifolds/hyperboloid.py
| def proj_batch(self, x: Float[Array, "... dim_plus_1"], c: Curvature) -> Float[Array, "... dim_plus_1"]:
"""Project batched points onto hyperboloid (handles arbitrary leading dimensions)."""
return _proj_batch(self._cast(x), c)
|
addition
addition(
x: Float[Array, dim_plus_1],
y: Float[Array, dim_plus_1],
c: Curvature,
) -> Float[Array, dim_plus_1]
Lorentz gyrovector addition x ⊕ y = Exp_x(PT_{0→x}(Log_0(y))).
The intrinsic Lorentz addition of Chen et al. (2025b) / Shi et al. (2026, Eq. 1).
Forms a gyrocommutative gyrogroup with identity = origin and inverse
⊖x = (-1) ⊙ x = [x₀, -x_s]; matches Poincaré Möbius addition under the
stereographic isometry. scalar_mul provides the companion gyro scaling (Eq. 2).
Source code in hyperbolix/manifolds/hyperboloid.py
| def addition(
self, x: Float[Array, "dim_plus_1"], y: Float[Array, "dim_plus_1"], c: Curvature
) -> Float[Array, "dim_plus_1"]:
"""Lorentz gyrovector addition ``x ⊕ y = Exp_x(PT_{0→x}(Log_0(y)))``.
The intrinsic Lorentz addition of Chen et al. (2025b) / Shi et al. (2026, Eq. 1).
Forms a gyrocommutative gyrogroup with identity = origin and inverse
``⊖x = (-1) ⊙ x = [x₀, -x_s]``; matches Poincaré Möbius addition under the
stereographic isometry. ``scalar_mul`` provides the companion gyro scaling (Eq. 2).
"""
return _addition(self._cast(x), self._cast(y), c)
|
scalar_mul
scalar_mul(
r: float, x: Float[Array, dim_plus_1], c: Curvature
) -> Float[Array, dim_plus_1]
Scalar multiplication on hyperboloid.
Source code in hyperbolix/manifolds/hyperboloid.py
| def scalar_mul(self, r: float, x: Float[Array, "dim_plus_1"], c: Curvature) -> Float[Array, "dim_plus_1"]:
"""Scalar multiplication on hyperboloid."""
x = self._cast(x)
r_cast = jnp.asarray(r, dtype=x.dtype)
return _scalar_mul(r_cast, x, c) # type: ignore[arg-type]
|
dist
dist(
x: Float[Array, dim_plus_1],
y: Float[Array, dim_plus_1],
c: Curvature,
version_idx: int = VERSION_DEFAULT,
) -> Float[Array, ""]
Compute geodesic distance between hyperboloid points.
Source code in hyperbolix/manifolds/hyperboloid.py
| def dist(
self,
x: Float[Array, "dim_plus_1"],
y: Float[Array, "dim_plus_1"],
c: Curvature,
version_idx: int = VERSION_DEFAULT,
) -> Float[Array, ""]:
"""Compute geodesic distance between hyperboloid points."""
return _dist(self._cast(x), self._cast(y), c, version_idx)
|
dist_0
dist_0(
x: Float[Array, dim_plus_1],
c: Curvature,
version_idx: int = VERSION_DEFAULT,
) -> Float[Array, ""]
Compute geodesic distance from hyperboloid origin.
Source code in hyperbolix/manifolds/hyperboloid.py
| def dist_0(self, x: Float[Array, "dim_plus_1"], c: Curvature, version_idx: int = VERSION_DEFAULT) -> Float[Array, ""]:
"""Compute geodesic distance from hyperboloid origin."""
return _dist_0(self._cast(x), c, version_idx)
|
sqdist
sqdist(
x: Float[Array, dim_plus_1],
y: Float[Array, dim_plus_1],
c: Curvature,
) -> Float[Array, ""]
Squared Lorentzian distance d_L²(x, y) = -2/c - 2⟨x,y⟩_L (Law et al. 2019).
A fast, acosh-free dissimilarity that is monotone in the geodesic :meth:dist
(d_L² = (2/c)(cosh(√c·d) - 1)) but is not the squared geodesic distance and not
a true metric. Prefer it where a smooth monotone distance proxy suffices (contrastive /
commitment / attention scores); use :meth:dist when the geodesic length is required.
Source code in hyperbolix/manifolds/hyperboloid.py
| def sqdist(self, x: Float[Array, "dim_plus_1"], y: Float[Array, "dim_plus_1"], c: Curvature) -> Float[Array, ""]:
"""Squared Lorentzian distance ``d_L²(x, y) = -2/c - 2⟨x,y⟩_L`` (Law et al. 2019).
A fast, ``acosh``-free dissimilarity that is monotone in the geodesic :meth:`dist`
(``d_L² = (2/c)(cosh(√c·d) - 1)``) but is **not** the squared geodesic distance and **not**
a true metric. Prefer it where a smooth monotone distance proxy suffices (contrastive /
commitment / attention scores); use :meth:`dist` when the geodesic length is required.
"""
return _sqdist(self._cast(x), self._cast(y), c)
|
expmap
expmap(
v: Float[Array, dim_plus_1],
x: Float[Array, dim_plus_1],
c: Curvature,
) -> Float[Array, dim_plus_1]
Exponential map: map tangent vector v at point x to manifold.
Source code in hyperbolix/manifolds/hyperboloid.py
| def expmap(self, v: Float[Array, "dim_plus_1"], x: Float[Array, "dim_plus_1"], c: Curvature) -> Float[Array, "dim_plus_1"]:
"""Exponential map: map tangent vector v at point x to manifold."""
return _expmap(self._cast(v), self._cast(x), c)
|
expmap_0
expmap_0(
v: Float[Array, dim_plus_1], c: Curvature
) -> Float[Array, dim_plus_1]
Exponential map from origin.
Source code in hyperbolix/manifolds/hyperboloid.py
| def expmap_0(self, v: Float[Array, "dim_plus_1"], c: Curvature) -> Float[Array, "dim_plus_1"]:
"""Exponential map from origin."""
return _expmap_0(self._cast(v), c)
|
retraction
retraction(
v: Float[Array, dim_plus_1],
x: Float[Array, dim_plus_1],
c: Curvature,
) -> Float[Array, dim_plus_1]
Retraction: first-order approximation of exponential map.
Source code in hyperbolix/manifolds/hyperboloid.py
| def retraction(
self, v: Float[Array, "dim_plus_1"], x: Float[Array, "dim_plus_1"], c: Curvature
) -> Float[Array, "dim_plus_1"]:
"""Retraction: first-order approximation of exponential map."""
return _retraction(self._cast(v), self._cast(x), c)
|
logmap
logmap(
y: Float[Array, dim_plus_1],
x: Float[Array, dim_plus_1],
c: Curvature,
) -> Float[Array, dim_plus_1]
Logarithmic map: map point y to tangent space at point x.
Source code in hyperbolix/manifolds/hyperboloid.py
| def logmap(self, y: Float[Array, "dim_plus_1"], x: Float[Array, "dim_plus_1"], c: Curvature) -> Float[Array, "dim_plus_1"]:
"""Logarithmic map: map point y to tangent space at point x."""
return _logmap(self._cast(y), self._cast(x), c)
|
logmap_0
logmap_0(
y: Float[Array, dim_plus_1], c: Curvature
) -> Float[Array, dim_plus_1]
Logarithmic map from origin.
Source code in hyperbolix/manifolds/hyperboloid.py
| def logmap_0(self, y: Float[Array, "dim_plus_1"], c: Curvature) -> Float[Array, "dim_plus_1"]:
"""Logarithmic map from origin."""
return _logmap_0(self._cast(y), c)
|
ptransp
ptransp(
v: Float[Array, dim_plus_1],
x: Float[Array, dim_plus_1],
y: Float[Array, dim_plus_1],
c: Curvature,
) -> Float[Array, dim_plus_1]
Parallel transport tangent vector v from point x to point y.
Source code in hyperbolix/manifolds/hyperboloid.py
| def ptransp(
self, v: Float[Array, "dim_plus_1"], x: Float[Array, "dim_plus_1"], y: Float[Array, "dim_plus_1"], c: Curvature
) -> Float[Array, "dim_plus_1"]:
"""Parallel transport tangent vector v from point x to point y."""
return _ptransp(self._cast(v), self._cast(x), self._cast(y), c)
|
ptransp_0
ptransp_0(
v: Float[Array, dim_plus_1],
y: Float[Array, dim_plus_1],
c: Curvature,
) -> Float[Array, dim_plus_1]
Parallel transport tangent vector v from origin to point y.
Source code in hyperbolix/manifolds/hyperboloid.py
| def ptransp_0(
self, v: Float[Array, "dim_plus_1"], y: Float[Array, "dim_plus_1"], c: Curvature
) -> Float[Array, "dim_plus_1"]:
"""Parallel transport tangent vector v from origin to point y."""
return _ptransp_0(self._cast(v), self._cast(y), c)
|
tangent_inner
tangent_inner(
u: Float[Array, dim_plus_1],
v: Float[Array, dim_plus_1],
x: Float[Array, dim_plus_1],
c: Curvature,
) -> Float[Array, ""]
Compute inner product of tangent vectors u and v at point x.
Source code in hyperbolix/manifolds/hyperboloid.py
| def tangent_inner(
self, u: Float[Array, "dim_plus_1"], v: Float[Array, "dim_plus_1"], x: Float[Array, "dim_plus_1"], c: Curvature
) -> Float[Array, ""]:
"""Compute inner product of tangent vectors u and v at point x."""
return _tangent_inner(self._cast(u), self._cast(v), self._cast(x), c)
|
tangent_norm
tangent_norm(
v: Float[Array, dim_plus_1],
x: Float[Array, dim_plus_1],
c: Curvature,
) -> Float[Array, ""]
Compute norm of tangent vector v at point x.
Source code in hyperbolix/manifolds/hyperboloid.py
| def tangent_norm(self, v: Float[Array, "dim_plus_1"], x: Float[Array, "dim_plus_1"], c: Curvature) -> Float[Array, ""]:
"""Compute norm of tangent vector v at point x."""
return _tangent_norm(self._cast(v), self._cast(x), c)
|
egrad2rgrad
egrad2rgrad(
grad: Float[Array, dim_plus_1],
x: Float[Array, dim_plus_1],
c: Curvature,
) -> Float[Array, dim_plus_1]
Convert Euclidean gradient to Riemannian gradient.
Source code in hyperbolix/manifolds/hyperboloid.py
| def egrad2rgrad(
self, grad: Float[Array, "dim_plus_1"], x: Float[Array, "dim_plus_1"], c: Curvature
) -> Float[Array, "dim_plus_1"]:
"""Convert Euclidean gradient to Riemannian gradient."""
return _egrad2rgrad(self._cast(grad), self._cast(x), c)
|
tangent_proj
tangent_proj(
v: Float[Array, dim_plus_1],
x: Float[Array, dim_plus_1],
c: Curvature,
) -> Float[Array, dim_plus_1]
Project vector v onto tangent space at point x.
Source code in hyperbolix/manifolds/hyperboloid.py
| def tangent_proj(
self, v: Float[Array, "dim_plus_1"], x: Float[Array, "dim_plus_1"], c: Curvature
) -> Float[Array, "dim_plus_1"]:
"""Project vector v onto tangent space at point x."""
return _tangent_proj(self._cast(v), self._cast(x), c)
|
is_in_manifold
is_in_manifold(
x: Float[Array, dim_plus_1],
c: Curvature,
atol: float = 0.0001,
) -> Array
Check if point x lies on hyperboloid.
Source code in hyperbolix/manifolds/hyperboloid.py
| def is_in_manifold(self, x: Float[Array, "dim_plus_1"], c: Curvature, atol: float = 1e-4) -> Array:
"""Check if point x lies on hyperboloid."""
return _is_in_manifold(self._cast(x), c, atol)
|
is_in_tangent_space
is_in_tangent_space(
v: Float[Array, dim_plus_1],
x: Float[Array, dim_plus_1],
c: Curvature,
) -> Array
Check if vector v lies in tangent space at point x.
Source code in hyperbolix/manifolds/hyperboloid.py
| def is_in_tangent_space(self, v: Float[Array, "dim_plus_1"], x: Float[Array, "dim_plus_1"], c: Curvature) -> Array:
"""Check if vector v lies in tangent space at point x."""
return _is_in_tangent_space(self._cast(v), self._cast(x), c)
|
hcat
hcat(
points: Float[Array, "N n"], c: Curvature = 1.0
) -> Float[Array, dN_plus_1]
Hyperbolic concatenation of N points into one point.
Source code in hyperbolix/manifolds/hyperboloid.py
| def hcat(
self,
points: Float[Array, "N n"],
c: Curvature = 1.0,
) -> Float[Array, "dN_plus_1"]:
"""Hyperbolic concatenation of N points into one point."""
return _hcat(self._cast(points), c)
|
log_radius_concat
log_radius_concat(
points: Float[Array, "N n"], c: Curvature = 1.0
) -> Float[Array, dN_plus_1]
Log-radius-preserving concatenation of N points (Shi et al. 2026, Sec. 4.3).
Like :meth:hcat, but rescales each block's spatial part by a digamma factor so the
expected log spatial radius is invariant to the number of concatenated blocks. Reduces
to :meth:hcat when N == 1.
Source code in hyperbolix/manifolds/hyperboloid.py
| def log_radius_concat(
self,
points: Float[Array, "N n"],
c: Curvature = 1.0,
) -> Float[Array, "dN_plus_1"]:
"""Log-radius-preserving concatenation of N points (Shi et al. 2026, Sec. 4.3).
Like :meth:`hcat`, but rescales each block's spatial part by a digamma factor so the
expected log spatial radius is invariant to the number of concatenated blocks. Reduces
to :meth:`hcat` when ``N == 1``.
"""
return _log_radius_concat(self._cast(points), c)
|
embed_spatial_0
embed_spatial_0(
v_spatial: Float[Array, "... n"],
) -> Float[Array, "... n_plus_1"]
Embed spatial vector as tangent vector at origin.
Source code in hyperbolix/manifolds/hyperboloid.py
| def embed_spatial_0(self, v_spatial: Float[Array, "... n"]) -> Float[Array, "... n_plus_1"]:
"""Embed spatial vector as tangent vector at origin."""
return _embed_spatial_0(self._cast(v_spatial))
|
compute_mlr
compute_mlr(
x: Float[Array, "batch in_dim"],
z: Float[Array, "out_dim in_dim_minus_1"],
r: Float[Array, "out_dim 1"],
c: Curvature,
clamping_factor: float,
smoothing_factor: float,
min_enorm: float = 1e-15,
) -> Float[Array, "batch out_dim"]
Compute multinomial linear regression on hyperboloid.
Source code in hyperbolix/manifolds/hyperboloid.py
| def compute_mlr(
self,
x: Float[Array, "batch in_dim"],
z: Float[Array, "out_dim in_dim_minus_1"],
r: Float[Array, "out_dim 1"],
c: Curvature,
clamping_factor: float,
smoothing_factor: float,
min_enorm: float = 1e-15,
) -> Float[Array, "batch out_dim"]:
"""Compute multinomial linear regression on hyperboloid."""
return _compute_mlr(self._cast(x), self._cast(z), self._cast(r), c, clamping_factor, smoothing_factor, min_enorm)
|
busemann
busemann(
x: Float[Array, dim_plus_1],
v: Float[Array, dim],
c: Curvature,
) -> Float[Array, ""]
Closed-form Lorentz Busemann function B^v(x) = (1/√c)·log(√c·(x_t - ⟨x_s, v⟩)).
Point-to-horosphere coordinate (Chen et al. 2026, Eq. 4). v must be a unit
spatial direction (dim d) — it is not normalized internally. Single point
(d+1,) → scalar; use :func:jax.vmap for batching and over a direction set.
B^v(origin) = 0.
See Also
For an asymmetric quasimetric energy, compose this Busemann coordinate with an
external Euclidean quasimetric (e.g. IQE/MRN). Do not reach for
:meth:Poincare.apollonian_dist expecting asymmetry — it is a coboundary
(symmetrizes to √c·dist) and cannot deliver circulation.
Source code in hyperbolix/manifolds/hyperboloid.py
| def busemann(self, x: Float[Array, "dim_plus_1"], v: Float[Array, "dim"], c: Curvature) -> Float[Array, ""]:
"""Closed-form Lorentz Busemann function ``B^v(x) = (1/√c)·log(√c·(x_t - ⟨x_s, v⟩))``.
Point-to-horosphere coordinate (Chen et al. 2026, Eq. 4). ``v`` must be a *unit*
spatial direction (dim ``d``) — it is **not** normalized internally. Single point
``(d+1,)`` → scalar; use :func:`jax.vmap` for batching and over a direction set.
``B^v(origin) = 0``.
See Also
--------
For an *asymmetric* quasimetric energy, compose this Busemann coordinate with an
external Euclidean quasimetric (e.g. IQE/MRN). Do **not** reach for
:meth:`Poincare.apollonian_dist` expecting asymmetry — it is a coboundary
(symmetrizes to ``√c·dist``) and cannot deliver circulation.
"""
return _busemann(self._cast(x), self._cast(v), c)
|
Proper Velocity
The Proper Velocity (PV) model — an unconstrained \(\mathbb{R}^n\) representation of hyperbolic geometry rooted in special relativity's proper velocity (Ungar 2022, Ch. 10). PV is algebraically a gyrovector space isomorphic to the Poincaré ball via \(\pi(x) = (\beta_x / (1 + \beta_x)) \cdot x\), and carries a Riemannian metric making that isomorphism an isometry.
Why Proper Velocity?
Unlike the bounded Poincaré ball (points must stay in the open unit ball) and the constrained hyperboloid (points must satisfy \(\langle x, x \rangle_L = -1/c\)), PV points live in all of \(\mathbb{R}^n\) with no constraint. This gives:
- No projection step after updates — the manifold is \(\mathbb{R}^n\)
- Better numerical stability for large radii (no boundary collapse, no Lorentz constraint drift)
- Drop-in with Euclidean optimizers — the retraction reduces to \(x + v\)
The metric \(g_x(u,v) = \langle u, v\rangle - c\beta_x^2\langle x,u\rangle\langle x,v\rangle\) still gives sectional curvature \(-c\); only the coordinates change.
Convention
The paper uses curvature \(K < 0\) with \(\beta_x = 1/\sqrt{1 - K\|x\|^2}\). Hyperbolix keeps the \(c > 0\) convention (sectional curvature \(-c\)), substituting \(K = -c\), so \(\beta_x = 1/\sqrt{1 + c\|x\|^2}\).
hyperbolix.manifolds.proper_velocity.ProperVelocity
ProperVelocity(
dtype: dtype = jnp.float32, *, c: float = 1.0
)
Bases: ManifoldBase
Proper Velocity (PV) manifold with automatic dtype casting.
PV is an unconstrained representation of hyperbolic geometry rooted in
special relativity's proper velocity (Ungar 2022, Ch. 10). Points live
in R^n without any manifold constraint, which gives better numerical
stability for large radii than the bounded Poincaré ball or the
constrained hyperboloid (Chen et al. 2026, Tables 1-3).
Args:
dtype: Target JAX dtype for computations (default: jnp.float32).
c: Curvature value (default: 1.0). Must be positive.
Examples:
>>> import jax.numpy as jnp
>>> from hyperbolix.manifolds.proper_velocity import ProperVelocity
>>>
>>> manifold = ProperVelocity(dtype=jnp.float64)
>>> x = jnp.array([0.1, 0.2], dtype=jnp.float32)
>>> y = jnp.array([0.3, 0.4], dtype=jnp.float32)
>>> d = manifold.dist(x, y, c=1.0)
>>> d.dtype # float64
Source code in hyperbolix/manifolds/_base.py
| def __init__(
self,
dtype: jnp.dtype = jnp.float32,
*,
c: float = 1.0,
) -> None:
self.dtype = dtype
self._c_val = c
|
create_origin
create_origin(c: Curvature, dim: int) -> Float[Array, dim]
Create the PV origin (zero vector in R^n).
Source code in hyperbolix/manifolds/proper_velocity.py
| def create_origin(self, c: Curvature, dim: int) -> Float[Array, "dim"]:
"""Create the PV origin (zero vector in R^n)."""
return _create_origin(c, dim, self.dtype)
|
beta
beta(
x: Float[Array, dim], c: Curvature
) -> Float[Array, ""]
PV beta factor β_x = 1/√(1 + c·||x||²).
Source code in hyperbolix/manifolds/proper_velocity.py
| def beta(self, x: Float[Array, "dim"], c: Curvature) -> Float[Array, ""]:
"""PV beta factor β_x = 1/√(1 + c·||x||²)."""
return _beta(self._cast(x), c)
|
proj
proj(
x: Float[Array, dim], c: Curvature
) -> Float[Array, dim]
Projection onto PV (replaces non-finite values; PV is unconstrained).
Source code in hyperbolix/manifolds/proper_velocity.py
| def proj(self, x: Float[Array, "dim"], c: Curvature) -> Float[Array, "dim"]:
"""Projection onto PV (replaces non-finite values; PV is unconstrained)."""
return _proj(self._cast(x), c)
|
addition
addition(
x: Float[Array, dim], y: Float[Array, dim], c: Curvature
) -> Float[Array, dim]
PV gyroaddition x ⊕_U y.
Source code in hyperbolix/manifolds/proper_velocity.py
| def addition(self, x: Float[Array, "dim"], y: Float[Array, "dim"], c: Curvature) -> Float[Array, "dim"]:
"""PV gyroaddition x ⊕_U y."""
return _addition(self._cast(x), self._cast(y), c)
|
scalar_mul
scalar_mul(
r: float, x: Float[Array, dim], c: Curvature
) -> Float[Array, dim]
PV scalar multiplication r ⊗_U x.
Source code in hyperbolix/manifolds/proper_velocity.py
| def scalar_mul(self, r: float, x: Float[Array, "dim"], c: Curvature) -> Float[Array, "dim"]:
"""PV scalar multiplication r ⊗_U x."""
x = self._cast(x)
r_cast = jnp.asarray(r, dtype=x.dtype)
return _scalar_mul(r_cast, x, c) # type: ignore[arg-type]
|
dist
dist(
x: Float[Array, dim],
y: Float[Array, dim],
c: Curvature,
version_idx: int = VERSION_DEFAULT,
) -> Float[Array, ""]
Geodesic distance between PV points.
Source code in hyperbolix/manifolds/proper_velocity.py
| def dist(
self,
x: Float[Array, "dim"],
y: Float[Array, "dim"],
c: Curvature,
version_idx: int = VERSION_DEFAULT,
) -> Float[Array, ""]:
"""Geodesic distance between PV points."""
del version_idx # only one implementation currently
return _dist(self._cast(x), self._cast(y), c)
|
dist_0
dist_0(
x: Float[Array, dim],
c: Curvature,
version_idx: int = VERSION_DEFAULT,
) -> Float[Array, ""]
Geodesic distance from the PV origin.
Source code in hyperbolix/manifolds/proper_velocity.py
| def dist_0(self, x: Float[Array, "dim"], c: Curvature, version_idx: int = VERSION_DEFAULT) -> Float[Array, ""]:
"""Geodesic distance from the PV origin."""
del version_idx
return _dist_0(self._cast(x), c)
|
expmap
expmap(
v: Float[Array, dim], x: Float[Array, dim], c: Curvature
) -> Float[Array, dim]
Exponential map at x.
Source code in hyperbolix/manifolds/proper_velocity.py
| def expmap(self, v: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature) -> Float[Array, "dim"]:
"""Exponential map at x."""
return _expmap(self._cast(v), self._cast(x), c)
|
expmap_0
expmap_0(
v: Float[Array, dim], c: Curvature
) -> Float[Array, dim]
Exponential map from the origin.
Source code in hyperbolix/manifolds/proper_velocity.py
| def expmap_0(self, v: Float[Array, "dim"], c: Curvature) -> Float[Array, "dim"]:
"""Exponential map from the origin."""
return _expmap_0(self._cast(v), c)
|
logmap
logmap(
y: Float[Array, dim], x: Float[Array, dim], c: Curvature
) -> Float[Array, dim]
Logarithmic map at x.
Source code in hyperbolix/manifolds/proper_velocity.py
| def logmap(self, y: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature) -> Float[Array, "dim"]:
"""Logarithmic map at x."""
return _logmap(self._cast(y), self._cast(x), c)
|
logmap_0
logmap_0(
y: Float[Array, dim], c: Curvature
) -> Float[Array, dim]
Logarithmic map to the origin.
Source code in hyperbolix/manifolds/proper_velocity.py
| def logmap_0(self, y: Float[Array, "dim"], c: Curvature) -> Float[Array, "dim"]:
"""Logarithmic map to the origin."""
return _logmap_0(self._cast(y), c)
|
retraction
retraction(
v: Float[Array, dim], x: Float[Array, dim], c: Curvature
) -> Float[Array, dim]
Euclidean retraction (exact for PV).
Source code in hyperbolix/manifolds/proper_velocity.py
| def retraction(self, v: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature) -> Float[Array, "dim"]:
"""Euclidean retraction (exact for PV)."""
return _retraction(self._cast(v), self._cast(x), c)
|
ptransp
ptransp(
v: Float[Array, dim],
x: Float[Array, dim],
y: Float[Array, dim],
c: Curvature,
) -> Float[Array, dim]
Parallel transport v from T_x PV to T_y PV.
Source code in hyperbolix/manifolds/proper_velocity.py
| def ptransp(
self,
v: Float[Array, "dim"],
x: Float[Array, "dim"],
y: Float[Array, "dim"],
c: Curvature,
) -> Float[Array, "dim"]:
"""Parallel transport v from T_x PV to T_y PV."""
return _ptransp(self._cast(v), self._cast(x), self._cast(y), c)
|
ptransp_0
ptransp_0(
v: Float[Array, dim], y: Float[Array, dim], c: Curvature
) -> Float[Array, dim]
Parallel transport v from T_0 PV to T_y PV.
Source code in hyperbolix/manifolds/proper_velocity.py
| def ptransp_0(self, v: Float[Array, "dim"], y: Float[Array, "dim"], c: Curvature) -> Float[Array, "dim"]:
"""Parallel transport v from T_0 PV to T_y PV."""
return _ptransp_0(self._cast(v), self._cast(y), c)
|
tangent_inner
tangent_inner(
u: Float[Array, dim],
v: Float[Array, dim],
x: Float[Array, dim],
c: Curvature,
) -> Float[Array, ""]
Riemannian inner product ⟨u, v⟩_x.
Source code in hyperbolix/manifolds/proper_velocity.py
| def tangent_inner(
self,
u: Float[Array, "dim"],
v: Float[Array, "dim"],
x: Float[Array, "dim"],
c: Curvature,
) -> Float[Array, ""]:
"""Riemannian inner product ⟨u, v⟩_x."""
return _tangent_inner(self._cast(u), self._cast(v), self._cast(x), c)
|
tangent_norm
tangent_norm(
v: Float[Array, dim], x: Float[Array, dim], c: Curvature
) -> Float[Array, ""]
Riemannian norm ||v||_x.
Source code in hyperbolix/manifolds/proper_velocity.py
| def tangent_norm(self, v: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature) -> Float[Array, ""]:
"""Riemannian norm ||v||_x."""
return _tangent_norm(self._cast(v), self._cast(x), c)
|
tangent_proj
tangent_proj(
v: Float[Array, dim], x: Float[Array, dim], c: Curvature
) -> Float[Array, dim]
Tangent-space projection (identity for PV).
Source code in hyperbolix/manifolds/proper_velocity.py
| def tangent_proj(self, v: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature) -> Float[Array, "dim"]:
"""Tangent-space projection (identity for PV)."""
return _tangent_proj(self._cast(v), self._cast(x), c)
|
egrad2rgrad
egrad2rgrad(
grad: Float[Array, dim],
x: Float[Array, dim],
c: Curvature,
) -> Float[Array, dim]
Convert Euclidean gradient to Riemannian gradient.
Source code in hyperbolix/manifolds/proper_velocity.py
| def egrad2rgrad(self, grad: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature) -> Float[Array, "dim"]:
"""Convert Euclidean gradient to Riemannian gradient."""
return _egrad2rgrad(self._cast(grad), self._cast(x), c)
|
is_in_manifold
is_in_manifold(
x: Float[Array, dim], c: Curvature, atol: float = 1e-05
) -> Array
Check that all entries are finite (PV has no constraint).
Source code in hyperbolix/manifolds/proper_velocity.py
| def is_in_manifold(self, x: Float[Array, "dim"], c: Curvature, atol: float = 1e-5) -> Array:
"""Check that all entries are finite (PV has no constraint)."""
return _is_in_manifold(self._cast(x), c, atol)
|
is_in_tangent_space
is_in_tangent_space(
v: Float[Array, dim], x: Float[Array, dim], c: Curvature
) -> Array
Check that v has finite entries (T_x PV = R^n).
Source code in hyperbolix/manifolds/proper_velocity.py
| def is_in_tangent_space(self, v: Float[Array, "dim"], x: Float[Array, "dim"], c: Curvature) -> Array:
"""Check that v has finite entries (T_x PV = R^n)."""
return _is_in_tangent_space(self._cast(v), self._cast(x), c)
|
embed_spatial_0
embed_spatial_0(
v_spatial: Float[Array, "... n"],
) -> Float[Array, "... n"]
Identity embedding (no time coordinate). Kept for API parity.
Source code in hyperbolix/manifolds/proper_velocity.py
| def embed_spatial_0(self, v_spatial: Float[Array, "... n"]) -> Float[Array, "... n"]:
"""Identity embedding (no time coordinate). Kept for API parity."""
return _embed_spatial_0(self._cast(v_spatial))
|
compute_mlr
compute_mlr(
x: Float[Array, "batch in_dim"],
z: Float[Array, "out_dim in_dim"],
r: Float[Array, "out_dim 1"],
c: Curvature,
clamping_factor: float,
smoothing_factor: float,
min_enorm: float = 1e-15,
) -> Float[Array, "batch out_dim"]
PV multinomial logistic regression (paper Thm 5.2).
Source code in hyperbolix/manifolds/proper_velocity.py
| def compute_mlr(
self,
x: Float[Array, "batch in_dim"],
z: Float[Array, "out_dim in_dim"],
r: Float[Array, "out_dim 1"],
c: Curvature,
clamping_factor: float,
smoothing_factor: float,
min_enorm: float = 1e-15,
) -> Float[Array, "batch out_dim"]:
"""PV multinomial logistic regression (paper Thm 5.2)."""
return _compute_mlr(
self._cast(x),
self._cast(z),
self._cast(r),
c,
clamping_factor,
smoothing_factor,
min_enorm,
)
|
Product Manifold
Heterogeneous-curvature product space \(P = M_1 \times M_2 \times \dots \times M_n\) where each factor \(M_i\) can be any base manifold (Poincaré, Hyperboloid, Euclidean, Proper Velocity) with its own curvature \(c_i\). Points are represented as flat concatenated arrays of shape (total_dim,).
The geodesic distance on a product Riemannian manifold is Pythagorean over component distances:
\[d_P(x, y) \;=\; \sqrt{\sum_{i=1}^{n} d_{M_i}(x_i, y_i)^2}\]
where \(x_i\), \(y_i\) are the per-factor slices of the flat points.
Per-factor c argument
Every geometry method (dist, expmap, logmap, proj, origin, …) takes a positional c argument that must be a sequence of length n_factors — one curvature per factor. There is no scalar fallback and no broadcast: pass product.curvatures for static curvatures, or a tuple built from LearnableCurvature calls for trainable ones. ProductManifold satisfies the Manifold protocol — the protocol-level Curvature type unions scalar and sequence-of-scalars, so isinstance(product, Manifold) is True and generic code typed against Manifold accepts product instances. The product itself has no c attribute — read factor-stored values via product.curvatures.
Static vs learnable curvature
Factor instances may carry an initial c (Hyperboloid(c=1.0)), but ProductManifold never reads it in its geometry methods — it is exposed via product.curvatures as a convenience default. For learnable curvature, instantiate one LearnableCurvature per factor on your nnx.Module and pass c=(self.curv_a(), self.curv_b(), ...) to the product. See the Manifolds User Guide — Curvature in ProductManifold for the full pattern.
hyperbolix.manifolds.product.ProductManifold
ProductManifold(
*factors: tuple[ManifoldBase, int],
dtype: dtype = jnp.float32,
)
Product manifold P = M1 x M2 x ... x Mn.
Each factor is specified as (manifold_instance, dim) where dim is
the point dimension (array slice width): ambient for Hyperboloid (d+1),
spatial for Poincaré/Euclidean/ProperVelocity (d).
Curvature is supplied at call time as a per-factor sequence:
product.dist(x, y, (c0, c1, ...)) # explicit tuple
product.dist(x, y, product.curvatures) # use factor-stored values
Factor instances may carry an initial curvature value (Hyperboloid(c=1.0)),
but that value is not consulted by ProductManifold's methods — it is
only useful as a static default to read off via product.curvatures.
ProductManifold satisfies the Manifold protocol with c typed as
Curvature (a union of scalar and sequence-of-scalars), so generic code
written against Manifold accepts product instances too. The product
itself has no c attribute — there is no single scalar curvature.
Args:
*factors: Tuples of (manifold_instance, dim). At least one required.
dtype: Target JAX dtype for computations (default: jnp.float32).
Examples:
>>> product = ProductManifold((Hyperboloid(c=1.0), 5), (Poincare(c=0.1), 3))
>>> c = product.curvatures # (1.0, 0.1)
>>> x = product.origin(c)
>>> parts = product.split(x)
>>> d = product.dist(x, y, c)
From a signature (repeated factors):
>>> product = ProductManifold.from_signature(
... (Hyperboloid, 3, 8, 1.0), # 8 copies of 3D-ambient Hyperboloid
... (Poincare, 2, 4, 0.1), # 4 copies of 2D Poincaré
... )
Source code in hyperbolix/manifolds/product.py
| def __init__(
self,
*factors: tuple[ManifoldBase, int],
dtype: jnp.dtype = jnp.float32,
) -> None:
if len(factors) < 1:
raise ValueError("ProductManifold requires at least one factor")
self.dtype = dtype
manifolds = []
dims = []
slices = []
pos = 0
for i, (manifold, dim) in enumerate(factors):
if not isinstance(manifold, ManifoldBase):
raise TypeError(f"Factor {i}: expected ManifoldBase instance, got {type(manifold).__name__}")
if dim < 1:
raise ValueError(f"Factor {i}: dim must be >= 1, got {dim}")
manifolds.append(manifold)
dims.append(dim)
slices.append(slice(pos, pos + dim))
pos += dim
self._factors = tuple(manifolds)
self._dims = tuple(dims)
self._slices = tuple(slices)
self._total_dim = pos
|
curvatures
property
curvatures: tuple[ScalarCurvature, ...]
Per-factor curvatures stored on the factor instances.
Useful as the c argument when curvature is static, e.g.
product.dist(x, y, product.curvatures). For learnable curvature,
build the tuple from LearnableCurvature calls instead.
split
split(x: Float[Array, total_dim]) -> tuple[Array, ...]
Split a product point into per-factor components.
Source code in hyperbolix/manifolds/product.py
| def split(self, x: Float[Array, "total_dim"]) -> tuple[Array, ...]:
"""Split a product point into per-factor components."""
return tuple(x[s] for s in self._slices)
|
combine
combine(*parts: Array) -> Float[Array, total_dim]
Combine per-factor components into a product point.
Source code in hyperbolix/manifolds/product.py
| def combine(self, *parts: Array) -> Float[Array, "total_dim"]:
"""Combine per-factor components into a product point."""
if len(parts) != len(self._factors):
raise ValueError(f"Expected {len(self._factors)} parts, got {len(parts)}")
return jnp.concatenate(list(parts))
|
origin
origin(c: Curvature) -> Float[Array, total_dim]
Construct the product origin point under per-factor curvatures.
Uses expmap_0(zeros, c[i]) per factor, which returns the manifold
origin for all manifold types without isinstance checks.
Source code in hyperbolix/manifolds/product.py
| def origin(self, c: Curvature) -> Float[Array, "total_dim"]:
"""Construct the product origin point under per-factor curvatures.
Uses ``expmap_0(zeros, c[i])`` per factor, which returns the manifold
origin for all manifold types without isinstance checks.
"""
cs = self._validate_c(c)
parts = []
for m, dim, c_i in zip(self._factors, self._dims, cs, strict=True):
zero_tangent = jnp.zeros(dim, dtype=self.dtype)
parts.append(m.expmap_0(zero_tangent, c_i))
return jnp.concatenate(parts)
|
proj
proj(
x: Float[Array, total_dim], c: Curvature
) -> Float[Array, total_dim]
Project point onto product manifold (per-factor projection).
Source code in hyperbolix/manifolds/product.py
| def proj(
self,
x: Float[Array, "total_dim"],
c: Curvature,
) -> Float[Array, "total_dim"]:
"""Project point onto product manifold (per-factor projection)."""
cs = self._validate_c(c)
x = self._cast(x)
parts = self.split(x)
return jnp.concatenate([m.proj(p, c_i) for m, p, c_i in zip(self._factors, parts, cs, strict=True)])
|
addition
addition(
x: Float[Array, total_dim],
y: Float[Array, total_dim],
c: Curvature,
) -> Float[Array, total_dim]
Manifold addition (per-factor).
Source code in hyperbolix/manifolds/product.py
| def addition(
self,
x: Float[Array, "total_dim"],
y: Float[Array, "total_dim"],
c: Curvature,
) -> Float[Array, "total_dim"]:
"""Manifold addition (per-factor)."""
cs = self._validate_c(c)
x, y = self._cast(x), self._cast(y)
x_parts, y_parts = self.split(x), self.split(y)
return jnp.concatenate(
[m.addition(xp, yp, c_i) for m, xp, yp, c_i in zip(self._factors, x_parts, y_parts, cs, strict=True)]
)
|
scalar_mul
scalar_mul(
r: float, x: Float[Array, total_dim], c: Curvature
) -> Float[Array, total_dim]
Scalar multiplication (same scalar r applied to all factors).
Source code in hyperbolix/manifolds/product.py
| def scalar_mul(
self,
r: float,
x: Float[Array, "total_dim"],
c: Curvature,
) -> Float[Array, "total_dim"]:
"""Scalar multiplication (same scalar r applied to all factors)."""
cs = self._validate_c(c)
x = self._cast(x)
parts = self.split(x)
return jnp.concatenate([m.scalar_mul(r, p, c_i) for m, p, c_i in zip(self._factors, parts, cs, strict=True)])
|
expmap
expmap(
v: Float[Array, total_dim],
x: Float[Array, total_dim],
c: Curvature,
) -> Float[Array, total_dim]
Exponential map (per-factor).
Source code in hyperbolix/manifolds/product.py
| def expmap(
self,
v: Float[Array, "total_dim"],
x: Float[Array, "total_dim"],
c: Curvature,
) -> Float[Array, "total_dim"]:
"""Exponential map (per-factor)."""
cs = self._validate_c(c)
v, x = self._cast(v), self._cast(x)
v_parts, x_parts = self.split(v), self.split(x)
return jnp.concatenate(
[m.expmap(vp, xp, c_i) for m, vp, xp, c_i in zip(self._factors, v_parts, x_parts, cs, strict=True)]
)
|
expmap_0
expmap_0(
v: Float[Array, total_dim], c: Curvature
) -> Float[Array, total_dim]
Exponential map from origin (per-factor).
Source code in hyperbolix/manifolds/product.py
| def expmap_0(
self,
v: Float[Array, "total_dim"],
c: Curvature,
) -> Float[Array, "total_dim"]:
"""Exponential map from origin (per-factor)."""
cs = self._validate_c(c)
v = self._cast(v)
parts = self.split(v)
return jnp.concatenate([m.expmap_0(p, c_i) for m, p, c_i in zip(self._factors, parts, cs, strict=True)])
|
logmap
logmap(
y: Float[Array, total_dim],
x: Float[Array, total_dim],
c: Curvature,
) -> Float[Array, total_dim]
Logarithmic map (per-factor).
Source code in hyperbolix/manifolds/product.py
| def logmap(
self,
y: Float[Array, "total_dim"],
x: Float[Array, "total_dim"],
c: Curvature,
) -> Float[Array, "total_dim"]:
"""Logarithmic map (per-factor)."""
cs = self._validate_c(c)
y, x = self._cast(y), self._cast(x)
y_parts, x_parts = self.split(y), self.split(x)
return jnp.concatenate(
[m.logmap(yp, xp, c_i) for m, yp, xp, c_i in zip(self._factors, y_parts, x_parts, cs, strict=True)]
)
|
logmap_0
logmap_0(
y: Float[Array, total_dim], c: Curvature
) -> Float[Array, total_dim]
Logarithmic map to origin (per-factor).
Source code in hyperbolix/manifolds/product.py
| def logmap_0(
self,
y: Float[Array, "total_dim"],
c: Curvature,
) -> Float[Array, "total_dim"]:
"""Logarithmic map to origin (per-factor)."""
cs = self._validate_c(c)
y = self._cast(y)
parts = self.split(y)
return jnp.concatenate([m.logmap_0(p, c_i) for m, p, c_i in zip(self._factors, parts, cs, strict=True)])
|
retraction
retraction(
v: Float[Array, total_dim],
x: Float[Array, total_dim],
c: Curvature,
) -> Float[Array, total_dim]
Retraction (per-factor).
Source code in hyperbolix/manifolds/product.py
| def retraction(
self,
v: Float[Array, "total_dim"],
x: Float[Array, "total_dim"],
c: Curvature,
) -> Float[Array, "total_dim"]:
"""Retraction (per-factor)."""
cs = self._validate_c(c)
v, x = self._cast(v), self._cast(x)
v_parts, x_parts = self.split(v), self.split(x)
return jnp.concatenate(
[m.retraction(vp, xp, c_i) for m, vp, xp, c_i in zip(self._factors, v_parts, x_parts, cs, strict=True)]
)
|
ptransp
ptransp(
v: Float[Array, total_dim],
x: Float[Array, total_dim],
y: Float[Array, total_dim],
c: Curvature,
) -> Float[Array, total_dim]
Parallel transport of v from x to y (per-factor).
Source code in hyperbolix/manifolds/product.py
| def ptransp(
self,
v: Float[Array, "total_dim"],
x: Float[Array, "total_dim"],
y: Float[Array, "total_dim"],
c: Curvature,
) -> Float[Array, "total_dim"]:
"""Parallel transport of v from x to y (per-factor)."""
cs = self._validate_c(c)
v, x, y = self._cast(v), self._cast(x), self._cast(y)
v_parts, x_parts, y_parts = self.split(v), self.split(x), self.split(y)
return jnp.concatenate(
[
m.ptransp(vp, xp, yp, c_i)
for m, vp, xp, yp, c_i in zip(self._factors, v_parts, x_parts, y_parts, cs, strict=True)
]
)
|
ptransp_0
ptransp_0(
v: Float[Array, total_dim],
y: Float[Array, total_dim],
c: Curvature,
) -> Float[Array, total_dim]
Parallel transport from origin to y (per-factor).
Source code in hyperbolix/manifolds/product.py
| def ptransp_0(
self,
v: Float[Array, "total_dim"],
y: Float[Array, "total_dim"],
c: Curvature,
) -> Float[Array, "total_dim"]:
"""Parallel transport from origin to y (per-factor)."""
cs = self._validate_c(c)
v, y = self._cast(v), self._cast(y)
v_parts, y_parts = self.split(v), self.split(y)
return jnp.concatenate(
[m.ptransp_0(vp, yp, c_i) for m, vp, yp, c_i in zip(self._factors, v_parts, y_parts, cs, strict=True)]
)
|
tangent_inner
tangent_inner(
u: Float[Array, total_dim],
v: Float[Array, total_dim],
x: Float[Array, total_dim],
c: Curvature,
) -> Float[Array, ""]
Riemannian inner product (sum of per-factor inner products).
Source code in hyperbolix/manifolds/product.py
| def tangent_inner(
self,
u: Float[Array, "total_dim"],
v: Float[Array, "total_dim"],
x: Float[Array, "total_dim"],
c: Curvature,
) -> Float[Array, ""]:
"""Riemannian inner product (sum of per-factor inner products)."""
cs = self._validate_c(c)
u, v, x = self._cast(u), self._cast(v), self._cast(x)
u_parts, v_parts, x_parts = self.split(u), self.split(v), self.split(x)
inners = jnp.stack(
[
m.tangent_inner(up, vp, xp, c_i)
for m, up, vp, xp, c_i in zip(self._factors, u_parts, v_parts, x_parts, cs, strict=True)
]
)
return jnp.sum(inners)
|
tangent_norm
tangent_norm(
v: Float[Array, total_dim],
x: Float[Array, total_dim],
c: Curvature,
) -> Float[Array, ""]
Riemannian norm (sqrt of tangent inner product with itself).
Source code in hyperbolix/manifolds/product.py
| def tangent_norm(
self,
v: Float[Array, "total_dim"],
x: Float[Array, "total_dim"],
c: Curvature,
) -> Float[Array, ""]:
"""Riemannian norm (sqrt of tangent inner product with itself)."""
return jnp.sqrt(jnp.maximum(self.tangent_inner(v, v, x, c), 0.0))
|
egrad2rgrad
egrad2rgrad(
grad: Float[Array, total_dim],
x: Float[Array, total_dim],
c: Curvature,
) -> Float[Array, total_dim]
Convert Euclidean gradient to Riemannian gradient (per-factor).
Source code in hyperbolix/manifolds/product.py
| def egrad2rgrad(
self,
grad: Float[Array, "total_dim"],
x: Float[Array, "total_dim"],
c: Curvature,
) -> Float[Array, "total_dim"]:
"""Convert Euclidean gradient to Riemannian gradient (per-factor)."""
cs = self._validate_c(c)
grad, x = self._cast(grad), self._cast(x)
g_parts, x_parts = self.split(grad), self.split(x)
return jnp.concatenate(
[m.egrad2rgrad(gp, xp, c_i) for m, gp, xp, c_i in zip(self._factors, g_parts, x_parts, cs, strict=True)]
)
|
tangent_proj
tangent_proj(
v: Float[Array, total_dim],
x: Float[Array, total_dim],
c: Curvature,
) -> Float[Array, total_dim]
Project vector onto tangent space (per-factor).
Source code in hyperbolix/manifolds/product.py
| def tangent_proj(
self,
v: Float[Array, "total_dim"],
x: Float[Array, "total_dim"],
c: Curvature,
) -> Float[Array, "total_dim"]:
"""Project vector onto tangent space (per-factor)."""
cs = self._validate_c(c)
v, x = self._cast(v), self._cast(x)
v_parts, x_parts = self.split(v), self.split(x)
return jnp.concatenate(
[m.tangent_proj(vp, xp, c_i) for m, vp, xp, c_i in zip(self._factors, v_parts, x_parts, cs, strict=True)]
)
|
component_dist
component_dist(
x: Float[Array, total_dim],
y: Float[Array, total_dim],
c: Curvature,
) -> Float[Array, n_factors]
Per-factor geodesic distances as a vector.
Source code in hyperbolix/manifolds/product.py
| def component_dist(
self,
x: Float[Array, "total_dim"],
y: Float[Array, "total_dim"],
c: Curvature,
) -> Float[Array, "n_factors"]:
"""Per-factor geodesic distances as a vector."""
cs = self._validate_c(c)
x, y = self._cast(x), self._cast(y)
x_parts, y_parts = self.split(x), self.split(y)
return jnp.stack([m.dist(xp, yp, c_i) for m, xp, yp, c_i in zip(self._factors, x_parts, y_parts, cs, strict=True)])
|
dist
dist(
x: Float[Array, total_dim],
y: Float[Array, total_dim],
c: Curvature,
) -> Float[Array, ""]
Product distance (L2/Riemannian): d = sqrt(sum d_i^2).
Source code in hyperbolix/manifolds/product.py
| def dist(
self,
x: Float[Array, "total_dim"],
y: Float[Array, "total_dim"],
c: Curvature,
) -> Float[Array, ""]:
"""Product distance (L2/Riemannian): d = sqrt(sum d_i^2)."""
d_per_factor = self.component_dist(x, y, c)
return jnp.sqrt(jnp.sum(d_per_factor**2))
|
dist_0
dist_0(
x: Float[Array, total_dim], c: Curvature
) -> Float[Array, ""]
Distance from origin (L2/Riemannian).
Source code in hyperbolix/manifolds/product.py
| def dist_0(
self,
x: Float[Array, "total_dim"],
c: Curvature,
) -> Float[Array, ""]:
"""Distance from origin (L2/Riemannian)."""
cs = self._validate_c(c)
x = self._cast(x)
parts = self.split(x)
d_sq = jnp.stack([m.dist_0(p, c_i) ** 2 for m, p, c_i in zip(self._factors, parts, cs, strict=True)])
return jnp.sqrt(jnp.sum(d_sq))
|
dist_l1
dist_l1(
x: Float[Array, total_dim],
y: Float[Array, total_dim],
c: Curvature,
) -> Float[Array, ""]
L1 product distance: d = sum d_i.
Source code in hyperbolix/manifolds/product.py
| def dist_l1(
self,
x: Float[Array, "total_dim"],
y: Float[Array, "total_dim"],
c: Curvature,
) -> Float[Array, ""]:
"""L1 product distance: d = sum d_i."""
return jnp.sum(self.component_dist(x, y, c))
|
dist_min
dist_min(
x: Float[Array, total_dim],
y: Float[Array, total_dim],
c: Curvature,
) -> Float[Array, ""]
Min product distance: d = min d_i.
Warning:
This is NOT a true metric: positive-definiteness fails. Two
distinct points x != y that agree on any single factor
(x_i == y_i) yield d = 0. Use only when the
"closest-factor" interpretation is semantically intended;
for an actual distance use dist (L2) or dist_l1.
Source code in hyperbolix/manifolds/product.py
| def dist_min(
self,
x: Float[Array, "total_dim"],
y: Float[Array, "total_dim"],
c: Curvature,
) -> Float[Array, ""]:
"""Min product distance: d = min d_i.
Warning:
This is NOT a true metric: positive-definiteness fails. Two
distinct points ``x != y`` that agree on any single factor
(``x_i == y_i``) yield ``d = 0``. Use only when the
"closest-factor" interpretation is semantically intended;
for an actual distance use ``dist`` (L2) or ``dist_l1``.
"""
return jnp.min(self.component_dist(x, y, c))
|
is_in_manifold
is_in_manifold(
x: Float[Array, total_dim], c: Curvature
) -> Array
Check if point is on product manifold (all factors valid).
Source code in hyperbolix/manifolds/product.py
| def is_in_manifold(
self,
x: Float[Array, "total_dim"],
c: Curvature,
) -> Array:
"""Check if point is on product manifold (all factors valid)."""
cs = self._validate_c(c)
x = self._cast(x)
parts = self.split(x)
checks = [m.is_in_manifold(p, c_i) for m, p, c_i in zip(self._factors, parts, cs, strict=True)]
return jnp.all(jnp.stack(checks))
|
is_in_tangent_space
is_in_tangent_space(
v: Float[Array, total_dim],
x: Float[Array, total_dim],
c: Curvature,
) -> Array
Check if vector is in tangent space at x (all factors valid).
Source code in hyperbolix/manifolds/product.py
| def is_in_tangent_space(
self,
v: Float[Array, "total_dim"],
x: Float[Array, "total_dim"],
c: Curvature,
) -> Array:
"""Check if vector is in tangent space at x (all factors valid)."""
cs = self._validate_c(c)
v, x = self._cast(v), self._cast(x)
v_parts, x_parts = self.split(v), self.split(x)
checks = [
m.is_in_tangent_space(vp, xp, c_i) for m, vp, xp, c_i in zip(self._factors, v_parts, x_parts, cs, strict=True)
]
return jnp.all(jnp.stack(checks))
|
from_signature
classmethod
from_signature(
*specs: tuple, dtype: dtype = jnp.float32
) -> ProductManifold
Create product manifold from a signature specification.
Each spec is one of:
- (ManifoldClass, dim, count) — uses c=1.0 as factor init.
- (ManifoldClass, dim, count, curvature) — sets factor init c.
The curvature here is an initial / default value stored on the
factor; it is only consulted via product.curvatures. Geometry
methods take c at call time.
Euclidean factors silently ignore the curvature field
(Euclidean has fixed c=0; see Euclidean.__init__).
Args:
*specs: Signature tuples (3- or 4-tuple as described above).
dtype: Target JAX dtype.
Returns:
ProductManifold instance.
Examples:
>>> pm = ProductManifold.from_signature(
... (Hyperboloid, 3, 8, 1.0),
... (Poincare, 2, 4, 0.1),
... (Euclidean, 8, 1),
... )
Source code in hyperbolix/manifolds/product.py
| @classmethod
def from_signature(
cls,
*specs: tuple,
dtype: jnp.dtype = jnp.float32,
) -> "ProductManifold":
"""Create product manifold from a signature specification.
Each spec is one of:
- ``(ManifoldClass, dim, count)`` — uses ``c=1.0`` as factor init.
- ``(ManifoldClass, dim, count, curvature)`` — sets factor init ``c``.
The ``curvature`` here is an **initial / default** value stored on the
factor; it is only consulted via ``product.curvatures``. Geometry
methods take ``c`` at call time.
Euclidean factors silently ignore the ``curvature`` field
(Euclidean has fixed ``c=0``; see ``Euclidean.__init__``).
Args:
*specs: Signature tuples (3- or 4-tuple as described above).
dtype: Target JAX dtype.
Returns:
ProductManifold instance.
Examples:
>>> pm = ProductManifold.from_signature(
... (Hyperboloid, 3, 8, 1.0),
... (Poincare, 2, 4, 0.1),
... (Euclidean, 8, 1),
... )
"""
from .euclidean import Euclidean
factors: list[tuple[ManifoldBase, int]] = []
for spec in specs:
if len(spec) == 3:
manifold_cls, dim, count = spec
curvature = 1.0
elif len(spec) == 4:
manifold_cls, dim, count, curvature = spec
else:
raise ValueError(
"Expected 3-tuple (ManifoldClass, dim, count) or 4-tuple "
f"(ManifoldClass, dim, count, curvature); got {len(spec)}-tuple"
)
for _ in range(count):
if issubclass(manifold_cls, Euclidean):
instance = manifold_cls(dtype=dtype)
else:
instance = manifold_cls(dtype=dtype, c=curvature)
factors.append((instance, dim))
return cls(*factors, dtype=dtype)
|
Isometry Mappings
Distance-preserving maps between the Poincaré ball, hyperboloid, and Proper
Velocity (PV) models — all coordinate models of the same hyperbolic space.
Provides Poincaré ↔ Hyperboloid, Poincaré ↔ PV (PVNN Eq. 4), and the direct
Hyperboloid ↔ PV map (PV coordinates are the space-like part of the 4-velocity).
hyperbolix.manifolds.isometry_mappings
Isometry mappings between hyperbolic manifold models.
This module implements distance-preserving transformations (isometries) between
different models of hyperbolic geometry. All functions operate on single points
and use JAX's vmap for batch operations.
Supported Models (curvature c > 0, sectional curvature -c):
- Hyperboloid model (Lorentz model): Points in R^(d+1) satisfying ⟨x,x⟩_L = -1/c
- Poincaré ball model: Points in R^d with ||y||² < 1/c
- Proper Velocity (PV) model: Unconstrained points in R^d (Chen et al. 2026)
Provided maps (all exact, distance-preserving, mutually consistent):
- Poincaré ↔ Hyperboloid: poincare_to_hyperboloid / hyperboloid_to_poincare
(curvature-aware stereographic projection through [-1/√c, 0, ..., 0]).
- Poincaré ↔ PV: poincare_to_pv / pv_to_poincare
(the gyro-isomorphism of PVNN Eq. 4, an isometry by their Thm 4.2).
- Hyperboloid ↔ PV: hyperboloid_to_pv / pv_to_hyperboloid
(direct map — PV coordinates are the space-like part of the 4-velocity).
JIT Compilation & Batching
All functions work with single points and return single points.
Use jax.vmap for batch operations:
>>> import jax
>>> import jax.numpy as jnp
>>> from hyperbolix.manifolds import isometry_mappings
>>>
>>> # Single point conversion
>>> x_hyp = jnp.array([1.0, 0.1, 0.2]) # Hyperboloid point
>>> y_poinc = isometry_mappings.hyperboloid_to_poincare(x_hyp, c=1.0)
>>>
>>> # Batch conversion with vmap
>>> x_batch = jnp.array([[1.0, 0.1, 0.2], [1.1, 0.15, 0.25]])
>>> convert_batch = jax.vmap(isometry_mappings.hyperboloid_to_poincare, in_axes=(0, None))
>>> y_batch = convert_batch(x_batch, 1.0)
References:
Wikipedia: Hyperboloid model
https://en.wikipedia.org/wiki/Hyperboloid_model#Relation_to_other_models
Chen et al. "Proper Velocity Neural Networks." ICLR 2026 (PV ↔ Poincaré, Eq. 4).
hyperboloid_to_poincare
hyperboloid_to_poincare(
x: Float[Array, dim_plus_1], c: Curvature
) -> Float[Array, dim]
Convert hyperboloid point to Poincaré ball via stereographic projection.
Projects the hyperboloid point onto the hyperplane t = 0 by intersecting
with a line through [-1/√c, 0, ..., 0]. This implements the canonical
isometry between the two models (radius-1/√c Poincaré ball).
Formula:
y_i = x_i / (√c·t + 1)
where x = [t, x_1, ..., x_n] on hyperboloid (t = x₀ ≥ 1/√c)
Args:
x: Point on hyperboloid, shape (dim+1,). Should satisfy ⟨x,x⟩_L = -1/c.
c: Curvature (positive)
Returns:
Point in Poincaré ball, shape (dim,). Satisfies ||y||² < 1/c.
Examples:
>>> import jax.numpy as jnp
>>> from hyperbolix.manifolds import isometry_mappings
>>>
>>> # Convert hyperboloid origin to Poincaré origin
>>> x_origin = jnp.array([1.0, 0.0, 0.0]) # c=1.0 origin
>>> y = isometry_mappings.hyperboloid_to_poincare(x_origin, c=1.0)
>>> bool(jnp.allclose(y, jnp.zeros(2)))
True
References:
Wikipedia: Hyperboloid model - Relation to other models
Source code in hyperbolix/manifolds/isometry_mappings.py
| def hyperboloid_to_poincare(
x: Float[Array, "dim_plus_1"],
c: Curvature,
) -> Float[Array, "dim"]:
"""Convert hyperboloid point to Poincaré ball via stereographic projection.
Projects the hyperboloid point onto the hyperplane t = 0 by intersecting
with a line through [-1/√c, 0, ..., 0]. This implements the canonical
isometry between the two models (radius-1/√c Poincaré ball).
Formula:
y_i = x_i / (√c·t + 1)
where x = [t, x_1, ..., x_n] on hyperboloid (t = x₀ ≥ 1/√c)
Args:
x: Point on hyperboloid, shape (dim+1,). Should satisfy ⟨x,x⟩_L = -1/c.
c: Curvature (positive)
Returns:
Point in Poincaré ball, shape (dim,). Satisfies ||y||² < 1/c.
Examples:
>>> import jax.numpy as jnp
>>> from hyperbolix.manifolds import isometry_mappings
>>>
>>> # Convert hyperboloid origin to Poincaré origin
>>> x_origin = jnp.array([1.0, 0.0, 0.0]) # c=1.0 origin
>>> y = isometry_mappings.hyperboloid_to_poincare(x_origin, c=1.0)
>>> bool(jnp.allclose(y, jnp.zeros(2)))
True
References:
Wikipedia: Hyperboloid model - Relation to other models
"""
sqrt_c = jnp.sqrt(c)
t = x[0] # Temporal component
x_spatial = x[1:] # Spatial components (x_1, ..., x_n)
# Curvature-aware stereographic projection: y_i = x_i / (√c·t + 1).
# Since t ≥ 1/√c on the hyperboloid, √c·t ≥ 1 and the denominator ≥ 2 — stable.
denominator = jnp.maximum(sqrt_c * t + 1.0, MIN_DENOM)
return x_spatial / denominator
|
poincare_to_hyperboloid
poincare_to_hyperboloid(
y: Float[Array, dim], c: Curvature
) -> Float[Array, dim_plus_1]
Convert Poincaré ball point to hyperboloid via inverse stereographic projection.
Inverts the stereographic projection to map points from the Poincaré ball
back to the hyperboloid. This implements the canonical isometry between
the two models (radius-1/√c Poincaré ball).
Formula:
t = (1 + c·||y||²) / ((1 - c·||y||²)·√c)
x_i = 2·y_i / (1 - c·||y||²)
where y = [y_1, ..., y_n] in Poincaré ball (||y||² < 1/c)
Args:
y: Point in Poincaré ball, shape (dim,). Should satisfy ||y||² < 1/c.
c: Curvature (positive)
Returns:
Point on hyperboloid, shape (dim+1,). Satisfies ⟨x,x⟩_L = -1/c.
Examples:
>>> import jax.numpy as jnp
>>> from hyperbolix.manifolds import isometry_mappings
>>>
>>> # Convert Poincaré origin to hyperboloid origin
>>> y_origin = jnp.array([0.0, 0.0])
>>> x = isometry_mappings.poincare_to_hyperboloid(y_origin, c=1.0)
>>> bool(jnp.allclose(x, jnp.array([1.0, 0.0, 0.0])))
True
References:
Wikipedia: Hyperboloid model - Relation to other models
Source code in hyperbolix/manifolds/isometry_mappings.py
| def poincare_to_hyperboloid(
y: Float[Array, "dim"],
c: Curvature,
) -> Float[Array, "dim_plus_1"]:
"""Convert Poincaré ball point to hyperboloid via inverse stereographic projection.
Inverts the stereographic projection to map points from the Poincaré ball
back to the hyperboloid. This implements the canonical isometry between
the two models (radius-1/√c Poincaré ball).
Formula:
t = (1 + c·||y||²) / ((1 - c·||y||²)·√c)
x_i = 2·y_i / (1 - c·||y||²)
where y = [y_1, ..., y_n] in Poincaré ball (||y||² < 1/c)
Args:
y: Point in Poincaré ball, shape (dim,). Should satisfy ||y||² < 1/c.
c: Curvature (positive)
Returns:
Point on hyperboloid, shape (dim+1,). Satisfies ⟨x,x⟩_L = -1/c.
Examples:
>>> import jax.numpy as jnp
>>> from hyperbolix.manifolds import isometry_mappings
>>>
>>> # Convert Poincaré origin to hyperboloid origin
>>> y_origin = jnp.array([0.0, 0.0])
>>> x = isometry_mappings.poincare_to_hyperboloid(y_origin, c=1.0)
>>> bool(jnp.allclose(x, jnp.array([1.0, 0.0, 0.0])))
True
References:
Wikipedia: Hyperboloid model - Relation to other models
"""
y_sqnorm = jnp.dot(y, y)
sqrt_c = jnp.sqrt(c)
# Curvature-aware inverse stereographic projection. The spatial part scales
# by the Poincaré conformal factor 1/(1 - c·||y||²); only the time component
# carries the extra 1/√c, so the two denominators differ.
one_minus = jnp.maximum(1.0 - c * y_sqnorm, MIN_DENOM)
t = (1.0 + c * y_sqnorm) / (one_minus * sqrt_c)
x_spatial = 2.0 * y / one_minus
# Concatenate temporal and spatial components: [t, x_1, ..., x_n]
return jnp.concatenate([t[None], x_spatial])
|
pv_to_poincare
pv_to_poincare(
x: Float[Array, dim], c: Curvature
) -> Float[Array, dim]
Convert a Proper Velocity point to the Poincaré ball.
Implements the gyro-isomorphism π_{PV→P} of the PVNN paper (Chen et al.
2026, Eq. 4), proven to be a Riemannian isometry (their Thm 4.2). With
hyperbolix's c > 0 convention (K = -c):
Formula:
y = x / (1 + √(1 + c·||x||²))
This is the numerically stable form of y = β_x/(1 + β_x)·x with the PV
beta factor β_x = 1/√(1 + c·||x||²): dividing numerator and denominator
by β_x turns it into x / (1 + 1/β_x). The denominator is ≥ 2, so the map
never blows up — every finite PV point lands strictly inside the radius-1/√c
ball.
Args:
x: Point in PV space (unconstrained R^n), shape (dim,).
c: Curvature (positive).
Returns:
Point in the Poincaré ball, shape (dim,). Satisfies ||y||² < 1/c.
Examples:
>>> import jax.numpy as jnp
>>> from hyperbolix.manifolds import isometry_mappings
>>>
>>> # PV origin (0) maps to Poincaré origin (0)
>>> y = isometry_mappings.pv_to_poincare(jnp.zeros(2), c=1.0)
>>> bool(jnp.allclose(y, jnp.zeros(2)))
True
References:
Chen et al. "Proper Velocity Neural Networks." ICLR 2026, Eq. 4.
Source code in hyperbolix/manifolds/isometry_mappings.py
| def pv_to_poincare(
x: Float[Array, "dim"],
c: Curvature,
) -> Float[Array, "dim"]:
"""Convert a Proper Velocity point to the Poincaré ball.
Implements the gyro-isomorphism π_{PV→P} of the PVNN paper (Chen et al.
2026, Eq. 4), proven to be a Riemannian isometry (their Thm 4.2). With
hyperbolix's c > 0 convention (K = -c):
Formula:
y = x / (1 + √(1 + c·||x||²))
This is the numerically stable form of ``y = β_x/(1 + β_x)·x`` with the PV
beta factor ``β_x = 1/√(1 + c·||x||²)``: dividing numerator and denominator
by β_x turns it into ``x / (1 + 1/β_x)``. The denominator is ≥ 2, so the map
never blows up — every finite PV point lands strictly inside the radius-1/√c
ball.
Args:
x: Point in PV space (unconstrained R^n), shape (dim,).
c: Curvature (positive).
Returns:
Point in the Poincaré ball, shape (dim,). Satisfies ||y||² < 1/c.
Examples:
>>> import jax.numpy as jnp
>>> from hyperbolix.manifolds import isometry_mappings
>>>
>>> # PV origin (0) maps to Poincaré origin (0)
>>> y = isometry_mappings.pv_to_poincare(jnp.zeros(2), c=1.0)
>>> bool(jnp.allclose(y, jnp.zeros(2)))
True
References:
Chen et al. "Proper Velocity Neural Networks." ICLR 2026, Eq. 4.
"""
beta_inv = jnp.sqrt(1.0 + c * jnp.dot(x, x)) # 1/β_x = √(1 + c·||x||²)
return x / (1.0 + beta_inv)
|
poincare_to_pv
poincare_to_pv(
y: Float[Array, dim], c: Curvature
) -> Float[Array, dim]
Convert a Poincaré ball point to Proper Velocity space.
Inverse of :func:pv_to_poincare — the map π_{P→PV} of the PVNN paper
(Chen et al. 2026, Eq. 4). With hyperbolix's c > 0 convention (K = -c):
Formula:
x = 2·y / (1 - c·||y||²) = λ(y)·y
where λ(y) = 2/(1 - c·||y||²) is the Poincaré conformal factor. As y
approaches the ball boundary (||y||² → 1/c) the image grows without bound —
expected, since PV is the unconstrained R^n model. The denominator is
guarded by MIN_DENOM to avoid division by zero at the boundary.
Args:
y: Point in the Poincaré ball, shape (dim,). Should satisfy ||y||² < 1/c.
c: Curvature (positive).
Returns:
Point in PV space (unconstrained R^n), shape (dim,).
Examples:
>>> import jax.numpy as jnp
>>> from hyperbolix.manifolds import isometry_mappings
>>>
>>> # Poincaré origin (0) maps to PV origin (0)
>>> x = isometry_mappings.poincare_to_pv(jnp.zeros(2), c=1.0)
>>> bool(jnp.allclose(x, jnp.zeros(2)))
True
References:
Chen et al. "Proper Velocity Neural Networks." ICLR 2026, Eq. 4.
Source code in hyperbolix/manifolds/isometry_mappings.py
| def poincare_to_pv(
y: Float[Array, "dim"],
c: Curvature,
) -> Float[Array, "dim"]:
"""Convert a Poincaré ball point to Proper Velocity space.
Inverse of :func:`pv_to_poincare` — the map π_{P→PV} of the PVNN paper
(Chen et al. 2026, Eq. 4). With hyperbolix's c > 0 convention (K = -c):
Formula:
x = 2·y / (1 - c·||y||²) = λ(y)·y
where λ(y) = 2/(1 - c·||y||²) is the Poincaré conformal factor. As y
approaches the ball boundary (||y||² → 1/c) the image grows without bound —
expected, since PV is the *unconstrained* R^n model. The denominator is
guarded by ``MIN_DENOM`` to avoid division by zero at the boundary.
Args:
y: Point in the Poincaré ball, shape (dim,). Should satisfy ||y||² < 1/c.
c: Curvature (positive).
Returns:
Point in PV space (unconstrained R^n), shape (dim,).
Examples:
>>> import jax.numpy as jnp
>>> from hyperbolix.manifolds import isometry_mappings
>>>
>>> # Poincaré origin (0) maps to PV origin (0)
>>> x = isometry_mappings.poincare_to_pv(jnp.zeros(2), c=1.0)
>>> bool(jnp.allclose(x, jnp.zeros(2)))
True
References:
Chen et al. "Proper Velocity Neural Networks." ICLR 2026, Eq. 4.
"""
denominator = jnp.maximum(1.0 - c * jnp.dot(y, y), MIN_DENOM)
return 2.0 * y / denominator
|
pv_to_hyperboloid
pv_to_hyperboloid(
x: Float[Array, dim], c: Curvature
) -> Float[Array, dim_plus_1]
Convert a Proper Velocity point to the hyperboloid model.
Uses the direct relation "proper velocity is the spatial part of the
(dimensionless) 4-velocity": the PV coordinates are exactly the space-like
hyperboloid components, and the time component is reconstructed from the
Lorentz constraint ⟨z,z⟩_L = -1/c.
Formula:
z = [√(1/c + ||x||²), x_1, ..., x_n]
This is an exact isometry (it equals
poincare_to_hyperboloid(pv_to_poincare(x, c), c)) but avoids the
near-boundary 1/(1 - c·||y||²) blow-up of the composed route — it is just a
concatenation. The time component is ≥ 1/√c > 0, so the result is always a
valid, numerically stable hyperboloid point.
Args:
x: Point in PV space (unconstrained R^n), shape (dim,).
c: Curvature (positive).
Returns:
Point on the hyperboloid, shape (dim+1,). Satisfies ⟨z,z⟩_L = -1/c, z₀ > 0.
Examples:
>>> import jax.numpy as jnp
>>> from hyperbolix.manifolds import isometry_mappings
>>>
>>> # PV origin maps to the hyperboloid origin [1/√c, 0, ...]
>>> z = isometry_mappings.pv_to_hyperboloid(jnp.zeros(2), c=1.0)
>>> bool(jnp.allclose(z, jnp.array([1.0, 0.0, 0.0])))
True
References:
Chen et al. "Proper Velocity Neural Networks." ICLR 2026.
Source code in hyperbolix/manifolds/isometry_mappings.py
| def pv_to_hyperboloid(
x: Float[Array, "dim"],
c: Curvature,
) -> Float[Array, "dim_plus_1"]:
"""Convert a Proper Velocity point to the hyperboloid model.
Uses the direct relation "proper velocity is the spatial part of the
(dimensionless) 4-velocity": the PV coordinates are exactly the space-like
hyperboloid components, and the time component is reconstructed from the
Lorentz constraint ⟨z,z⟩_L = -1/c.
Formula:
z = [√(1/c + ||x||²), x_1, ..., x_n]
This is an exact isometry (it equals
``poincare_to_hyperboloid(pv_to_poincare(x, c), c)``) but avoids the
near-boundary 1/(1 - c·||y||²) blow-up of the composed route — it is just a
concatenation. The time component is ≥ 1/√c > 0, so the result is always a
valid, numerically stable hyperboloid point.
Args:
x: Point in PV space (unconstrained R^n), shape (dim,).
c: Curvature (positive).
Returns:
Point on the hyperboloid, shape (dim+1,). Satisfies ⟨z,z⟩_L = -1/c, z₀ > 0.
Examples:
>>> import jax.numpy as jnp
>>> from hyperbolix.manifolds import isometry_mappings
>>>
>>> # PV origin maps to the hyperboloid origin [1/√c, 0, ...]
>>> z = isometry_mappings.pv_to_hyperboloid(jnp.zeros(2), c=1.0)
>>> bool(jnp.allclose(z, jnp.array([1.0, 0.0, 0.0])))
True
References:
Chen et al. "Proper Velocity Neural Networks." ICLR 2026.
"""
time = jnp.sqrt(1.0 / c + jnp.dot(x, x)) # z₀ = √(1/c + ||x||²) ≥ 1/√c
return jnp.concatenate([time[None], x])
|
hyperboloid_to_pv
hyperboloid_to_pv(
x: Float[Array, dim_plus_1], c: Curvature
) -> Float[Array, dim]
Convert a hyperboloid point to Proper Velocity space.
Inverse of :func:pv_to_hyperboloid: the PV coordinates are exactly the
space-like components of the hyperboloid point, so the map simply drops the
time component.
Formula:
x = [z_1, ..., z_n] (drop the time component z₀)
The curvature c is unused (the relation is curvature-independent) but is
kept in the signature for API symmetry with the other mappings.
Args:
x: Point on the hyperboloid, shape (dim+1,). Should satisfy ⟨x,x⟩_L = -1/c.
c: Curvature (positive). Unused.
Returns:
Point in PV space (unconstrained R^n), shape (dim,).
Examples:
>>> import jax.numpy as jnp
>>> from hyperbolix.manifolds import isometry_mappings
>>>
>>> # Hyperboloid origin [1/√c, 0, ...] maps to the PV origin (0)
>>> x = isometry_mappings.hyperboloid_to_pv(jnp.array([1.0, 0.0, 0.0]), c=1.0)
>>> bool(jnp.allclose(x, jnp.zeros(2)))
True
References:
Chen et al. "Proper Velocity Neural Networks." ICLR 2026.
Source code in hyperbolix/manifolds/isometry_mappings.py
| def hyperboloid_to_pv(
x: Float[Array, "dim_plus_1"],
c: Curvature,
) -> Float[Array, "dim"]:
"""Convert a hyperboloid point to Proper Velocity space.
Inverse of :func:`pv_to_hyperboloid`: the PV coordinates are exactly the
space-like components of the hyperboloid point, so the map simply drops the
time component.
Formula:
x = [z_1, ..., z_n] (drop the time component z₀)
The curvature ``c`` is unused (the relation is curvature-independent) but is
kept in the signature for API symmetry with the other mappings.
Args:
x: Point on the hyperboloid, shape (dim+1,). Should satisfy ⟨x,x⟩_L = -1/c.
c: Curvature (positive). Unused.
Returns:
Point in PV space (unconstrained R^n), shape (dim,).
Examples:
>>> import jax.numpy as jnp
>>> from hyperbolix.manifolds import isometry_mappings
>>>
>>> # Hyperboloid origin [1/√c, 0, ...] maps to the PV origin (0)
>>> x = isometry_mappings.hyperboloid_to_pv(jnp.array([1.0, 0.0, 0.0]), c=1.0)
>>> bool(jnp.allclose(x, jnp.zeros(2)))
True
References:
Chen et al. "Proper Velocity Neural Networks." ICLR 2026.
"""
del c # curvature-independent: PV coords are the hyperboloid spatial part
return x[1:]
|
Usage Examples
Basic Distance Computation
import jax.numpy as jnp
from hyperbolix.manifolds import Poincare
poincare = Poincare()
x = jnp.array([0.1, 0.2])
y = jnp.array([0.3, -0.1])
c = 1.0
# Compute distance (default: VERSION_MOBIUS_DIRECT)
distance = poincare.dist(x, y, c)
Float64 Precision
from hyperbolix.manifolds import Poincare
import jax.numpy as jnp
# High-precision manifold
poincare_f64 = Poincare(dtype=jnp.float64)
x = jnp.array([0.1, 0.2]) # float32 input
distance = poincare_f64.dist(x, y, c=1.0) # automatically cast to float64
print(distance.dtype) # float64
Batched Operations with vmap
import jax
from hyperbolix.manifolds import Hyperboloid
hyperboloid = Hyperboloid()
c = 1.0
# Batch of ambient points (d+1 dimensions)
x_batch = jax.random.normal(jax.random.PRNGKey(0), (100, 4))
y_batch = jax.random.normal(jax.random.PRNGKey(1), (100, 4))
# Project to hyperboloid
x_proj = jax.vmap(hyperboloid.proj, in_axes=(0, None))(x_batch, c)
y_proj = jax.vmap(hyperboloid.proj, in_axes=(0, None))(y_batch, c)
# Compute distances
distances = jax.vmap(hyperboloid.dist, in_axes=(0, 0, None))(x_proj, y_proj, c)
Exponential and Logarithmic Maps
from hyperbolix.manifolds import Poincare
import jax.numpy as jnp
poincare = Poincare()
# Point on manifold
x = poincare.proj(jnp.array([0.2, 0.3]), c=1.0)
# Tangent vector
v = jnp.array([0.1, -0.05])
# Exponential map (move along geodesic)
y = poincare.expmap(v, x, c=1.0)
# Logarithmic map (inverse operation)
v_recovered = poincare.logmap(y, x, c=1.0)
Proper Velocity Operations
import jax
import jax.numpy as jnp
from hyperbolix.manifolds import ProperVelocity
pv = ProperVelocity()
c = 1.0
# Points live in unconstrained R^n — no projection step needed
x = jnp.array([0.3, 0.5])
y = jnp.array([-0.2, 0.4])
# Geodesic distance (asinh form — stable over all of R^n)
d = pv.dist(x, y, c)
# Exp/log maps at the origin
v = jnp.array([0.1, -0.2])
y_moved = pv.expmap_0(v, c)
v_recovered = pv.logmap_0(y_moved, c)
# Euclidean gradient -> Riemannian gradient under the PV metric
grad_euc = jnp.array([1.0, 0.0])
grad_riem = pv.egrad2rgrad(grad_euc, x, c)
# Retraction is exact Euclidean addition (PV is unconstrained)
x_next = pv.retraction(v, x, c)
assert jnp.allclose(x_next, x + v)
Product Manifolds (Mixed Curvature)
import jax
import jax.numpy as jnp
from hyperbolix.manifolds import (
ProductManifold, Hyperboloid, Poincare, Euclidean,
)
# Build P = H^5(c=1.0) x P^3(c=0.1) x E^4
product = ProductManifold(
(Hyperboloid(c=1.0), 5), # 5 = ambient dim (d+1) for hyperboloid
(Poincare(c=0.1), 3), # 3 = spatial dim for poincaré
(Euclidean(), 4), # 4 = standard euclidean dim
)
# Per-factor curvatures must be passed at call time as a sequence.
c = product.curvatures # (1.0, 0.1, 0.0) — static default
o = product.origin(c) # shape (12,)
# Pythagorean product distance: d_P = sqrt(sum d_i^2)
x = product.origin(c)
y = product.origin(c) # generated elsewhere; here we use o for illustration
d_l2 = product.dist(x, y, c) # scalar
d_per_factor = product.component_dist(x, y, c) # shape (3,) per-factor distances
# Batch with vmap: broadcast c with None across the batch.
dist_batch = jax.vmap(product.dist, in_axes=(0, 0, None))
# distances = dist_batch(xs, ys, c)
# Repeated-factor construction via from_signature
mixed = ProductManifold.from_signature(
(Hyperboloid, 5, 4, 1.0), # 4 copies of H^4(c=1.0)
(Poincare, 3, 2, 0.1), # 2 copies of P^3(c=0.1)
(Euclidean, 4, 1), # 1 copy of E^4
)
# For learnable curvature, build the sequence from LearnableCurvature calls on
# your nnx.Module:
# c = (self.curv_h(), self.curv_p(), 0.0)
# d = product.dist(x, y, c)
Isometry Mappings
from hyperbolix.manifolds import isometry_mappings
import jax.numpy as jnp
# Hyperboloid point (ambient coordinates, d+1 dims)
x_hyperboloid = jnp.array([1.5, 0.5, 0.3]) # Must satisfy Lorentz constraint
# Map to Poincaré ball (intrinsic coordinates, d dims)
x_poincare = isometry_mappings.hyperboloid_to_poincare(x_hyperboloid, c=1.0)
# Map back (round-trip)
x_hyperboloid_recovered = isometry_mappings.poincare_to_hyperboloid(x_poincare, c=1.0)
Numerical Considerations
Float32 Precision
Float32 can cause numerical issues, especially in the Poincaré ball near the boundary. Use Poincare(dtype=jnp.float64) for:
- High curvature values (
c > 1.0)
- Points near manifold boundaries
- Deep neural networks with many layers
See the Numerical Stability guide for details.