Skip to content

SuperInstance/flux-hyperbolic

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 

flux-hyperbolic

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.

Installation

pip install flux-hyperbolic

Requires Python ≥ 3.11, NumPy ≥ 1.24.

Poincaré Ball

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 < radius

Lorentz (Hyperboloid) Model

The 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

Which model to use?

Property Poincaré Ball Lorentz
Stable near origin
Stable near boundary
Conformal (preserves angles)
Visualization
Deep embeddings / optimization

Standalone Distance Functions

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"

Tradition Embedding

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()

Hyperbolic Tree Construction

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 coordinates

Child 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

Riemannian Gradient Descent

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 iterations

The optimizer:

  1. Computes the Euclidean gradient
  2. Divides by λ² (conformal factor squared) to get the Riemannian gradient
  3. Steps via exp_map(x, −lr · gᴿ)
  4. Projects back into the ball

Testing

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

API Reference

PoincareBall(curvature=1.0)

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

LorentzModel(curvature=1.0)

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

TraditionEmbedding(dim=3, curvature=1.0)

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

HyperbolicTree(dim=3, curvature=1.0)

Method Description
.embed(tree_dict) Embed a nested dict tree, adds "point" to each node

RiemannianGradientDescent(dim=3, curvature=1.0, lr=0.01, max_iter=1000, tol=1e-6)

Method Description
.optimize(start, loss_fn, grad_fn) Run RGD, returns (optimized_point, loss_history)

Standalone Functions

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

Related

License

MIT

About

Poincaré ball embeddings for music tradition hierarchy. Riemannian optimization, hyperbolic trees. 25 tests.

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages