pub mod cayley_klein;
pub use cayley_klein::CayleyKleinSpace;
pub const EPSILON_BOUNDARY: f32 = 1e-5;
pub const MAX_NORM_SQ: f32 = (1.0 - EPSILON_BOUNDARY) * (1.0 - EPSILON_BOUNDARY);
#[derive(Debug, Clone, PartialEq)]
pub struct PoincarePoint {
pub coords: Vec<f32>,
}
impl PoincarePoint {
pub fn new(coords: Vec<f32>) -> Self {
let mut pt = Self { coords };
pt.project_to_ball();
pt
}
pub fn origin(dim: usize) -> Self {
Self {
coords: vec![0.0f32; dim],
}
}
#[inline(always)]
pub fn dim(&self) -> usize {
self.coords.len()
}
#[inline]
pub fn norm_sq(&self) -> f32 {
let mut sum = 0.0f32;
for &x in &self.coords {
sum += x * x;
}
sum
}
#[inline(always)]
pub fn norm(&self) -> f32 {
self.norm_sq().sqrt()
}
pub fn project_to_ball(&mut self) {
let n_sq = self.norm_sq();
if n_sq >= MAX_NORM_SQ {
let n = n_sq.sqrt();
let scale = (1.0 - EPSILON_BOUNDARY) / n.max(1e-7);
for x in &mut self.coords {
*x *= scale;
}
}
}
#[inline]
pub fn lambda(&self) -> f32 {
let n_sq = self.norm_sq().min(MAX_NORM_SQ);
2.0f32 / (1.0f32 - n_sq)
}
pub fn geodesic_distance(&self, other: &Self) -> f32 {
assert_eq!(self.dim(), other.dim(), "Dimension mismatch in hyperbolic distance");
let mut diff_sq = 0.0f32;
for i in 0..self.dim() {
let d = self.coords[i] - other.coords[i];
diff_sq += d * d;
}
let u_sq = self.norm_sq().min(MAX_NORM_SQ);
let v_sq = other.norm_sq().min(MAX_NORM_SQ);
let denom = (1.0f32 - u_sq) * (1.0f32 - v_sq);
let arg = 1.0f32 + 2.0f32 * diff_sq / denom.max(1e-12);
let z = arg.max(1.0f32);
(z + (z * z - 1.0f32).sqrt()).ln()
}
pub fn mobius_add(&self, other: &Self) -> Self {
assert_eq!(self.dim(), other.dim(), "Dimension mismatch in Mobius addition");
let u_sq = self.norm_sq().min(MAX_NORM_SQ);
let v_sq = other.norm_sq().min(MAX_NORM_SQ);
let mut uv_dot = 0.0f32;
for i in 0..self.dim() {
uv_dot += self.coords[i] * other.coords[i];
}
let num_u_coeff = 1.0f32 + 2.0f32 * uv_dot + v_sq;
let num_v_coeff = 1.0f32 - u_sq;
let denom = 1.0f32 + 2.0f32 * uv_dot + u_sq * v_sq;
let inv_denom = 1.0f32 / denom.max(1e-7);
let mut res = vec![0.0f32; self.dim()];
for i in 0..self.dim() {
res[i] = (num_u_coeff * self.coords[i] + num_v_coeff * other.coords[i]) * inv_denom;
}
Self::new(res)
}
#[inline(always)]
pub fn hyperbolic_distance_key(&self, other: &Self) -> f32 {
assert_eq!(self.dim(), other.dim(), "Dimension mismatch in hyperbolic distance key");
let mut diff_sq = 0.0f32;
for i in 0..self.dim() {
let d = self.coords[i] - other.coords[i];
diff_sq += d * d;
}
let u_sq = self.norm_sq().min(MAX_NORM_SQ);
let v_sq = other.norm_sq().min(MAX_NORM_SQ);
let denom = (1.0f32 - u_sq) * (1.0f32 - v_sq);
diff_sq / denom.max(1e-12)
}
#[inline]
pub fn to_klein(&self) -> Vec<f32> {
let u_sq = self.norm_sq().min(MAX_NORM_SQ);
let factor = 2.0f32 / (1.0f32 + u_sq);
self.coords.iter().map(|&x| x * factor).collect()
}
#[inline]
pub fn from_klein(k: &[f32]) -> Self {
let mut k_sq = 0.0f32;
for &x in k {
k_sq += x * x;
}
let k_sq_clamped = k_sq.min(MAX_NORM_SQ);
let denom = 1.0f32 + (1.0f32 - k_sq_clamped).sqrt();
let inv_denom = 1.0f32 / denom.max(1e-7);
let u = k.iter().map(|&x| x * inv_denom).collect();
Self::new(u)
}
pub fn einstein_midpoint(points: &[Self], weights: &[f32]) -> Self {
assert_eq!(points.len(), weights.len(), "Points and weights count mismatch");
if points.is_empty() {
return Self::new(vec![]);
}
let dim = points[0].dim();
if points.len() == 1 {
return points[0].clone();
}
let mut sum_k = vec![0.0f32; dim];
let mut sum_gamma = 0.0f32;
for (pt, &w) in points.iter().zip(weights.iter()) {
if w <= 0.0 {
continue;
}
let k = pt.to_klein();
let mut k_sq = 0.0f32;
for &x in &k {
k_sq += x * x;
}
let gamma = 1.0f32 / (1.0f32 - k_sq.min(MAX_NORM_SQ)).sqrt().max(1e-7);
let eff_w = w * gamma;
for i in 0..dim {
sum_k[i] += eff_w * k[i];
}
sum_gamma += eff_w;
}
if sum_gamma <= 1e-9 {
return points[0].clone();
}
let inv_sum = 1.0f32 / sum_gamma;
let mut mean_k = vec![0.0f32; dim];
for i in 0..dim {
mean_k[i] = sum_k[i] * inv_sum;
}
Self::from_klein(&mean_k)
}
#[inline]
pub fn rsgd_step(&mut self, euclid_grad: &[f32], learning_rate: f32) {
assert_eq!(self.dim(), euclid_grad.len(), "Dimension mismatch in RSGD step");
let n_sq = self.norm_sq().min(MAX_NORM_SQ);
let conf = (1.0f32 - n_sq) * 0.5f32;
let riemann_scale = learning_rate * (conf * conf);
for i in 0..self.dim() {
self.coords[i] -= riemann_scale * euclid_grad[i];
}
self.project_to_ball();
}
#[inline]
pub fn exp_map_zero(v: &[f32]) -> Self {
let mut norm_sq = 0.0f32;
for &x in v {
norm_sq += x * x;
}
let norm = norm_sq.sqrt();
let scale = if norm < 1e-9 {
0.5f32
} else {
(norm * 0.5f32).tanh() / norm
};
let coords = v.iter().map(|&x| x * scale).collect();
Self::new(coords)
}
#[inline]
pub fn log_map_zero(&self) -> Vec<f32> {
let norm = self.norm().min(1.0 - EPSILON_BOUNDARY);
let scale = if norm < 1e-9 {
2.0f32
} else {
let artanh = 0.5f32 * ((1.0 + norm) / (1.0 - norm)).ln();
2.0f32 * artanh / norm
};
self.coords.iter().map(|&x| x * scale).collect()
}
}