Hyperbolic geometry for embedding musical tradition hierarchies.
Music traditions nest like trees: Jazz → Bebop → Charlie Parker's style. Euclidean space can't faithfully represent deep hierarchies because ball volume grows polynomially. Hyperbolic space has exponential volume growth — a natural fit for tree-like data where each level branches into many children.
This library provides two models of hyperbolic geometry (Poincaré ball, Lorentz/hyperboloid), Riemannian gradient descent, Sarkar's tree construction, and a TraditionEmbedding that maps the 10 SuperInstance traditions into hyperbolic coordinates.
pip install flux-hyperbolicRequires Python ≥ 3.11, NumPy ≥ 1.24.
The Poincaré ball is the open unit ball Bⁿ = {x : ‖x‖ < 1/√c} with the metric:
g_x = λ_x² · I, λ_x = 2 / (1 − c·‖x‖²)
Distance grows rapidly near the boundary — root nodes sit near the origin, leaves near the edge.
from flux_hyperbolic import PoincareBall
ball = PoincareBall(curvature=1.0)
# Two points in the ball (‖·‖ < 1 when c=1)
jazz = np.array([0.2, 0.1, 0.3])
blues = np.array([0.25, 0.15, 0.28])
# Hyperbolic distance
d = ball.distance(jazz, blues)
# Möbius addition (non-commutative group operation)
moved = ball.mobius_add(jazz, blues)
# Move along a tangent vector via exponential map
tangent = np.array([0.01, -0.02, 0.005])
new_point = ball.exp_map(jazz, tangent)
# Recover the tangent via logarithmic map
recovered = ball.log_map(jazz, new_point) # ≈ tangent
# Mobility: exp(−d(u,v)), measures information flow between points
mob = ball.mobility(jazz, blues) # in (0, 1]
# Project a point back inside the ball
outside = np.array([1.5, 0.0, 0.0])
inside = ball.project(outside) # norm clamped to < radiusThe Lorentz model lives on the upper sheet of a hyperboloid in Minkowski space:
Hⁿ = {x ∈ ℝⁿ⁺¹ : ⟨x,x⟩_L = −1/c, x₀ > 0}
Numerically more stable than Poincaré for points far from the origin.
from flux_hyperbolic import LorentzModel
model = LorentzModel(curvature=1.0)
# Project a spatial vector onto the hyperboloid
spatial = np.array([0.1, 0.2, 0.05]) # 3D spatial part
x = model.project(spatial) # returns [x0, 0.1, 0.2, 0.05] where x0 = √(1 + ‖spatial‖²)
# Distance via Minkowski inner product: d = arccosh(−⟨x,y⟩_L)
y = model.project(np.array([0.15, 0.18, 0.1]))
d = model.distance(x, y)
# Convert between Poincaré ball and Lorentz coordinates
ball = PoincareBall(curvature=1.0)
p = model.to_poincare(x) # Lorentz → Poincaré
x_back = model.from_poincare(p) # Poincaré → Lorentz| Property | Poincaré Ball | Lorentz |
|---|---|---|
| Stable near origin | ✅ | ✅ |
| Stable near boundary | ❌ | ✅ |
| Conformal (preserves angles) | ✅ | ❌ |
| Visualization | ✅ | ❌ |
| Deep embeddings / optimization | ❌ | ✅ |
from flux_hyperbolic import poincare_distance, lorentz_distance, hyperbolic_distance
# Direct distance computation without instantiating a model
d = poincare_distance(u, v, curvature=1.0)
d = lorentz_distance(x, y, curvature=1.0)
# Generic dispatcher
d = hyperbolic_distance(u, v, model="poincare") # or model="lorentz"TraditionEmbedding maps the 10 built-in traditions into the Poincaré ball using the hierarchical tree:
Music
├── Western
│ ├── Jazz
│ ├── Classical
│ ├── Rock
│ └── Blues
├── Eastern
│ ├── Hindustani
│ ├── Gamelan
│ └── Gagaku
├── African
│ └── WestAfrican
└── Electronic
└── Electronic
from flux_hyperbolic import TraditionEmbedding
import numpy as np
emb = TraditionEmbedding(dim=3, curvature=1.0)
points = emb.embed_traditions()
# Get the hyperbolic point for a tradition
jazz_point = points["Jazz"] # np.ndarray of shape (3,)
blues_point = points["Blues"]
# Distance between two traditions in hyperbolic space
d = emb.tradition_distance("Jazz", "Blues")
# Find nearest traditions to a point
query = np.array([0.15, 0.1, 0.2])
neighbors = emb.nearest_traditions(query, k=5)
# [("Jazz", 0.42), ("Blues", 0.58), ...]
# Get the full tree with embedded points
tree = emb.tradition_tree()HyperbolicTree embeds arbitrary tree structures using a simplified Sarkar's algorithm — root at origin, children placed at decreasing distances with angular separation.
from flux_hyperbolic import HyperbolicTree
tree = HyperbolicTree(dim=3, curvature=1.0)
# Define a tree as nested dicts
taxonomy = {
"name": "Music",
"children": [
{
"name": "Jazz",
"children": [
{"name": "Bebop"},
{"name": "Cool Jazz"},
]
},
{
"name": "Classical",
"children": [
{"name": "Baroque"},
{"name": "Romantic"},
]
},
],
}
# Embed: adds a "point" field to each node
embedded = tree.embed(taxonomy)
# embedded["children"][0]["point"] → Jazz's coordinates
# embedded["children"][0]["children"][0]["point"] → Bebop's coordinatesChild placement uses distance 0.3 / (depth + 1) from the parent, with children evenly spaced angularly. This ensures:
- Parent-child distance decreases with depth
- Siblings are well-separated
- Deeper nodes are closer to the ball boundary
Standard Euclidean gradient descent doesn't account for curvature. The Riemannian gradient corrects for the conformal factor:
∇ᴿf(x) = (1 / λ_x²) · ∇ᴱf(x)
Each step applies the exponential map to move along the manifold:
from flux_hyperbolic import RiemannianGradientDescent, PoincareBall
import numpy as np
optimizer = RiemannianGradientDescent(
dim=3,
curvature=1.0,
lr=0.01,
max_iter=1000,
tol=1e-6,
)
# Minimize distance to a target point
target = np.array([0.3, 0.2, 0.1])
start = np.array([0.5, 0.4, 0.3])
def loss_fn(x):
diff = x - target
return float(np.dot(diff, diff))
def grad_fn(x):
return 2.0 * (x - target)
optimized, losses = optimizer.optimize(start, loss_fn, grad_fn)
# optimized ≈ target, losses decreases over iterationsThe optimizer:
- Computes the Euclidean gradient
- Divides by λ² (conformal factor squared) to get the Riemannian gradient
- Steps via
exp_map(x, −lr · gᴿ) - Projects back into the ball
pytest # all tests
pytest tests/test_poincare.py # Poincaré model
pytest tests/test_lorentz.py # Lorentz model
pytest tests/test_embedding.py # TraditionEmbedding
pytest tests/test_distance.py # standalone distance functions
pytest -v --cov=flux_hyperbolic # with coverage| Method | Description |
|---|---|
.distance(u, v) |
Hyperbolic distance d(u,v) = (1/√c)·arccosh(1 + 2c‖u−v‖²/((1−c‖u‖²)(1−c‖v‖²))) |
.mobius_add(x, y) |
Möbius addition (non-commutative) |
.exp_map(base, tangent) |
Exponential map: tangent vector → point |
.log_map(base, point) |
Logarithmic map: point → tangent vector |
.project(x, eps=1e-5) |
Clamp norm < radius |
.mobility(u, v) |
exp(−d(u,v)), information flow metric |
| Method | Description |
|---|---|
.distance(x, y) |
d = (1/√c)·arccosh(−⟨x,y⟩_L) |
.minkowski_dot(x, y) |
Minkowski inner product −x₀y₀ + Σxᵢyᵢ |
.project(x) |
Project onto hyperboloid: x₀ ← √(1/c + ‖x_spatial‖²) |
.exp_map(base, tangent) |
Exponential map on hyperboloid |
.log_map(base, point) |
Logarithmic map |
.to_poincare(x) |
Lorentz → Poincaré coordinates |
.from_poincare(p) |
Poincaré → Lorentz coordinates |
| Method | Description |
|---|---|
.embed_traditions(dials=None) |
Map traditions to hyperbolic points, returns dict |
.tradition_distance(t1, t2) |
Hyperbolic distance between two traditions |
.nearest_traditions(point, k=5) |
k-nearest traditions to a point |
.tradition_tree() |
Full tree with embedded coordinates |
| Method | Description |
|---|---|
.embed(tree_dict) |
Embed a nested dict tree, adds "point" to each node |
| Method | Description |
|---|---|
.optimize(start, loss_fn, grad_fn) |
Run RGD, returns (optimized_point, loss_history) |
| Function | Description |
|---|---|
poincare_distance(u, v, curvature=1.0) |
Direct Poincaré distance |
lorentz_distance(x, y, curvature=1.0) |
Direct Lorentz distance |
hyperbolic_distance(u, v, model="poincare", curvature=1.0) |
Dispatch by model name |
- flux-genome — Genetic evolution of musical genomes
- constraint-dsl — Declarative constraint language
- superinstance-live — Live session controller
MIT