1#![allow(missing_docs)]
2use serde::{Deserialize, Serialize};
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct HyperbolicConfig {
26 pub curvature: f32,
29 pub dimensions: usize,
31 pub epsilon: f32,
33}
34
35impl Default for HyperbolicConfig {
36 fn default() -> Self {
37 Self {
38 curvature: -1.0,
39 dimensions: 64,
40 epsilon: 1e-5,
41 }
42 }
43}
44
45impl HyperbolicConfig {
46 pub fn new(curvature: f32, dimensions: usize) -> Self {
48 assert!(
49 curvature < 0.0,
50 "Curvature must be negative for hyperbolic space"
51 );
52 Self {
53 curvature,
54 dimensions,
55 epsilon: 1e-5,
56 }
57 }
58
59 #[allow(dead_code)]
67 fn c(&self) -> f32 {
68 self.curvature.abs()
69 }
70}
71
72pub fn euclidean_to_poincare(vector: &[f32], curvature: f32) -> Vec<f32> {
88 let c = curvature.abs();
89 let max_norm = 1.0 / c.sqrt();
90
91 let norm_sq: f32 = vector.iter().map(|v| v * v).sum();
93 let norm = norm_sq.sqrt();
94
95 if norm == 0.0 {
96 return vec![0.0; vector.len()];
97 }
98
99 let scale = max_norm * norm.tanh() / norm;
102 vector.iter().map(|&v| v * scale).collect()
103}
104
105pub fn batch_euclidean_to_poincare(vectors: &[Vec<f32>], curvature: f32) -> Vec<Vec<f32>> {
107 vectors
108 .iter()
109 .map(|v| euclidean_to_poincare(v, curvature))
110 .collect()
111}
112
113pub fn hyperbolic_distance(a: &[f32], b: &[f32], curvature: f32) -> f32 {
119 let c = curvature.abs();
120
121 let norm_a_sq: f32 = a.iter().map(|v| v * v).sum();
122 let norm_b_sq: f32 = b.iter().map(|v| v * v).sum();
123
124 let diff_sq: f32 = a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum();
125
126 let denominator = (1.0 - c * norm_a_sq) * (1.0 - c * norm_b_sq);
127
128 if denominator <= 0.0 {
129 return f32::MAX;
131 }
132
133 let arg = 1.0 + 2.0 * c * diff_sq / denominator;
134
135 if arg <= 1.0 {
136 return 0.0;
138 }
139
140 (1.0 / c.sqrt()) * (arg + (arg * arg - 1.0).sqrt()).ln()
144}
145
146pub fn mobius_add(a: &[f32], b: &[f32], curvature: f32) -> Vec<f32> {
151 let c = curvature.abs();
152
153 let norm_a_sq: f32 = a.iter().map(|v| v * v).sum();
154 let norm_b_sq: f32 = b.iter().map(|v| v * v).sum();
155 let dot_ab: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
156
157 let denominator = 1.0 + 2.0 * c * dot_ab + c * c * norm_a_sq * norm_b_sq;
158
159 if denominator.abs() < 1e-10 {
160 return vec![0.0; a.len()];
162 }
163
164 let scale_a = 1.0 + 2.0 * c * dot_ab + c * norm_b_sq;
165 let scale_b = 1.0 - c * norm_a_sq;
166
167 a.iter()
168 .zip(b)
169 .map(|(&ai, &bi)| (scale_a * ai + scale_b * bi) / denominator)
170 .collect()
171}
172
173pub fn mobius_scalar_mul(scalar: f32, v: &[f32], curvature: f32, epsilon: f32) -> Vec<f32> {
183 let c = curvature.abs();
184 let norm_sq: f32 = v.iter().map(|x| x * x).sum();
185 let norm = norm_sq.sqrt();
186
187 if norm < epsilon {
188 return vec![0.0; v.len()];
189 }
190
191 let c_sqrt = c.sqrt();
192 let w = c_sqrt * norm;
193
194 let w = w.min(1.0 - epsilon);
196 let result_norm = (1.0 / c_sqrt) * (scalar * w.atanh()).tanh();
197
198 let scale = result_norm / norm;
199 v.iter().map(|&vi| vi * scale).collect()
200}
201
202pub struct HyperbolicEmbedding {
211 config: HyperbolicConfig,
212 embeddings: Vec<(String, Vec<f32>)>,
214}
215
216impl HyperbolicEmbedding {
217 pub fn new(config: HyperbolicConfig) -> Self {
219 Self {
220 config,
221 embeddings: Vec::new(),
222 }
223 }
224
225 pub fn from_pairs(pairs: Vec<(String, Vec<f32>)>) -> Self {
231 Self {
234 config: HyperbolicConfig::default(),
235 embeddings: pairs,
236 }
237 }
238
239 pub fn with_dimensions(dimensions: usize) -> Self {
241 let config = HyperbolicConfig {
242 dimensions,
243 ..Default::default()
244 };
245 Self::new(config)
246 }
247
248 pub fn add(&mut self, id: &str, euclidean: &[f32]) {
252 let poincare = euclidean_to_poincare(euclidean, self.config.curvature);
253 if let Some(pos) = self.embeddings.iter().position(|(name, _)| name == id) {
255 self.embeddings[pos] = (id.to_string(), poincare);
256 } else {
257 self.embeddings.push((id.to_string(), poincare));
258 }
259 }
260
261 pub fn add_child(&mut self, parent_id: &str, child_id: &str, child_euclidean: &[f32]) {
266 let child_on_ball = euclidean_to_poincare(child_euclidean, self.config.curvature);
267
268 let child_point = if let Some((_, parent_vec)) =
269 self.embeddings.iter().find(|(name, _)| name == parent_id)
270 {
271 mobius_add(parent_vec, &child_on_ball, self.config.curvature)
274 } else {
275 child_on_ball
276 };
277
278 if let Some(pos) = self
279 .embeddings
280 .iter()
281 .position(|(name, _)| name == child_id)
282 {
283 self.embeddings[pos] = (child_id.to_string(), child_point);
284 } else {
285 self.embeddings.push((child_id.to_string(), child_point));
286 }
287 }
288
289 pub fn get(&self, id: &str) -> Option<&[f32]> {
291 self.embeddings
292 .iter()
293 .find(|(name, _)| name == id)
294 .map(|(_, v)| v.as_slice())
295 }
296
297 pub fn nearest_neighbors(&self, query_id: &str, k: usize) -> Vec<(String, f32)> {
301 let query = match self.get(query_id) {
302 Some(v) => v.to_vec(),
303 None => return Vec::new(),
304 };
305
306 let mut results: Vec<(String, f32)> = self
307 .embeddings
308 .iter()
309 .filter(|(name, _)| name != query_id)
310 .map(|(name, vec)| {
311 let dist = hyperbolic_distance(&query, vec, self.config.curvature);
312 (name.clone(), dist)
313 })
314 .collect();
315
316 results.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
317 results.truncate(k);
318 results
319 }
320
321 pub fn search(&self, query: &[f32], k: usize) -> Vec<(String, f32)> {
323 let query_poincare = euclidean_to_poincare(query, self.config.curvature);
324
325 let mut results: Vec<(String, f32)> = self
326 .embeddings
327 .iter()
328 .map(|(name, vec)| {
329 let dist = hyperbolic_distance(&query_poincare, vec, self.config.curvature);
330 (name.clone(), dist)
331 })
332 .collect();
333
334 results.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
335 results.truncate(k);
336 results
337 }
338
339 pub fn hierarchical_distance(&self, id_a: &str, id_b: &str) -> f32 {
345 let a = match self.get(id_a) {
346 Some(v) => v,
347 None => return f32::MAX,
348 };
349 let b = match self.get(id_b) {
350 Some(v) => v,
351 None => return f32::MAX,
352 };
353
354 hyperbolic_distance(a, b, self.config.curvature)
355 }
356
357 pub fn len(&self) -> usize {
359 self.embeddings.len()
360 }
361
362 pub fn is_empty(&self) -> bool {
364 self.embeddings.is_empty()
365 }
366
367 pub fn ids(&self) -> Vec<&str> {
369 self.embeddings
370 .iter()
371 .map(|(name, _)| name.as_str())
372 .collect()
373 }
374
375 pub fn all_embeddings(&self) -> &[(String, Vec<f32>)] {
377 &self.embeddings
378 }
379
380 pub fn search_memories(&self, query_euclidean: &[f32], k: usize) -> Vec<(String, f32)> {
392 self.search(query_euclidean, k)
393 }
394
395 pub fn hierarchical_rank(&self) -> Vec<(String, f32)> {
402 self.rank_by_depth()
403 }
404
405 pub fn depth(&self, id: &str) -> f32 {
409 match self.get(id) {
410 Some(v) => hyperbolic_distance(&vec![0.0; v.len()], v, self.config.curvature),
411 None => f32::MAX,
412 }
413 }
414
415 pub fn rank_by_depth(&self) -> Vec<(String, f32)> {
420 let mut ranked: Vec<(String, f32)> = self
421 .embeddings
422 .iter()
423 .map(|(name, vec)| {
424 let origin = vec![0.0; vec.len()];
425 let d = hyperbolic_distance(&origin, vec, self.config.curvature);
426 (name.clone(), d)
427 })
428 .collect();
429
430 ranked.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
431 ranked
432 }
433}
434
435#[cfg(test)]
440mod tests {
441 use super::*;
442
443 #[test]
444 fn test_euclidean_to_poincare_zero() {
445 let result = euclidean_to_poincare(&[0.0, 0.0, 0.0], -1.0);
446 assert_eq!(result, vec![0.0, 0.0, 0.0]);
447 }
448
449 #[test]
450 fn test_euclidean_to_poincare_bounded() {
451 let c = -1.0;
452 let result = euclidean_to_poincare(&[100.0, 100.0, 100.0], c);
454 let norm: f32 = result.iter().map(|v| v * v).sum::<f32>().sqrt();
455 let max_norm = 1.0 / c.abs().sqrt();
456 assert!(
457 norm < max_norm,
458 "Result should be inside the ball: norm={}, max={}",
459 norm,
460 max_norm
461 );
462 }
463
464 #[test]
465 fn test_hyperbolic_distance_same_point() {
466 let point = euclidean_to_poincare(&[0.5, 0.3], -1.0);
467 let dist = hyperbolic_distance(&point, &point, -1.0);
468 assert!(dist < 1e-5, "Distance from self should be ~0, got {}", dist);
469 }
470
471 #[test]
472 fn test_hyperbolic_distance_symmetry() {
473 let a = euclidean_to_poincare(&[1.0, 2.0], -1.0);
474 let b = euclidean_to_poincare(&[3.0, 1.0], -1.0);
475 let d_ab = hyperbolic_distance(&a, &b, -1.0);
476 let d_ba = hyperbolic_distance(&b, &a, -1.0);
477 assert!(
478 (d_ab - d_ba).abs() < 1e-4,
479 "Distance should be symmetric: {} vs {}",
480 d_ab,
481 d_ba
482 );
483 }
484
485 #[test]
486 fn test_hyperbolic_distance_triangle_inequality() {
487 let a = euclidean_to_poincare(&[1.0, 0.0], -1.0);
488 let b = euclidean_to_poincare(&[0.0, 1.0], -1.0);
489 let c = euclidean_to_poincare(&[2.0, 2.0], -1.0);
490
491 let d_ab = hyperbolic_distance(&a, &b, -1.0);
492 let d_bc = hyperbolic_distance(&b, &c, -1.0);
493 let d_ac = hyperbolic_distance(&a, &c, -1.0);
494
495 assert!(
496 d_ac <= d_ab + d_bc + 1e-4,
497 "Triangle inequality: d(a,c)={} should be <= d(a,b)+d(b,c)={}",
498 d_ac,
499 d_ab + d_bc
500 );
501 }
502
503 #[test]
504 fn test_mobius_add_identity() {
505 let a = euclidean_to_poincare(&[0.5, 0.3], -1.0);
506 let zero = vec![0.0, 0.0];
507 let result = mobius_add(&a, &zero, -1.0);
508 for (r, expected) in result.iter().zip(a.iter()) {
509 assert!((r - expected).abs() < 1e-4, "a ⊕ 0 should equal a");
510 }
511 }
512
513 #[test]
514 fn test_mobius_scalar_mul_zero() {
515 let v = euclidean_to_poincare(&[1.0, 2.0], -1.0);
516 let result = mobius_scalar_mul(0.0, &v, -1.0, 1e-5);
517 for r in &result {
518 assert!(r.abs() < 1e-4, "0 ⊗ v should be ~0, got {}", r);
519 }
520 }
521
522 #[test]
523 fn test_mobius_scalar_mul_one() {
524 let v = euclidean_to_poincare(&[1.0, 2.0], -1.0);
525 let result = mobius_scalar_mul(1.0, &v, -1.0, 1e-5);
526 for (r, expected) in result.iter().zip(v.iter()) {
527 assert!((r - expected).abs() < 1e-4, "1 ⊗ v should equal v");
528 }
529 }
530
531 #[test]
532 fn test_hyperbolic_embedding_add_and_search() {
533 let mut he = HyperbolicEmbedding::with_dimensions(3);
534
535 he.add("root", &[0.0, 0.0, 0.0]);
536 he.add("child_a", &[1.0, 0.0, 0.0]);
537 he.add("child_b", &[0.0, 1.0, 0.0]);
538 he.add("grandchild", &[1.0, 1.0, 0.0]);
539
540 assert_eq!(he.len(), 4);
541
542 let nn = he.nearest_neighbors("child_a", 2);
544 assert_eq!(nn.len(), 2);
545 let gc_dist = nn
547 .iter()
548 .find(|(name, _)| name == "grandchild")
549 .map(|(_, d)| *d);
550 let cb_dist = nn
551 .iter()
552 .find(|(name, _)| name == "child_b")
553 .map(|(_, d)| *d);
554 if let (Some(gc), Some(cb)) = (gc_dist, cb_dist) {
555 assert!(
556 gc < cb,
557 "grandchild should be closer to child_a than child_b"
558 );
559 }
560 }
561
562 #[test]
563 fn test_hyperbolic_embedding_depth() {
564 let mut he = HyperbolicEmbedding::with_dimensions(2);
565
566 he.add("root", &[0.0, 0.0]);
567 he.add("level1", &[0.5, 0.0]);
568 he.add("level2", &[1.0, 0.0]);
569
570 let root_depth = he.depth("root");
571 let l1_depth = he.depth("level1");
572 let l2_depth = he.depth("level2");
573
574 assert!(
575 root_depth < l1_depth,
576 "Root should be shallower: root={}, l1={}",
577 root_depth,
578 l1_depth
579 );
580 assert!(
581 l1_depth < l2_depth,
582 "Level1 should be shallower: l1={}, l2={}",
583 l1_depth,
584 l2_depth
585 );
586 }
587
588 #[test]
589 fn test_rank_by_depth() {
590 let mut he = HyperbolicEmbedding::with_dimensions(2);
591
592 he.add("leaf", &[2.0, 2.0]);
593 he.add("root", &[0.0, 0.0]);
594 he.add("mid", &[0.5, 0.5]);
595
596 let ranked = he.rank_by_depth();
597 assert_eq!(ranked[0].0, "root");
598 assert_eq!(ranked[1].0, "mid");
599 assert_eq!(ranked[2].0, "leaf");
600 }
601
602 #[test]
603 fn test_batch_conversion() {
604 let vectors = vec![vec![1.0, 2.0], vec![3.0, 4.0], vec![0.0, 0.0]];
605 let results = batch_euclidean_to_poincare(&vectors, -1.0);
606 assert_eq!(results.len(), 3);
607 assert_eq!(results[2], vec![0.0, 0.0]);
609 }
610
611 #[test]
612 fn test_curvature_effect() {
613 let v = [1.0, 1.0];
614
615 let p1 = euclidean_to_poincare(&v, -1.0);
616 let p2 = euclidean_to_poincare(&v, -2.0);
617
618 let norm1: f32 = p1.iter().map(|x| x * x).sum::<f32>().sqrt();
619 let norm2: f32 = p2.iter().map(|x| x * x).sum::<f32>().sqrt();
620
621 assert!(
623 norm2 < norm1,
624 "Higher curvature should produce smaller ball: {} vs {}",
625 norm2,
626 norm1
627 );
628 }
629
630 #[test]
631 fn test_add_child_hierarchy() {
632 let mut he = HyperbolicEmbedding::with_dimensions(3);
633
634 he.add("parent", &[1.0, 0.0, 0.0]);
636 he.add_child("parent", "child", &[0.5, 0.5, 0.0]);
637
638 assert_eq!(he.len(), 2);
639
640 assert!(he.get("parent").is_some());
642 assert!(he.get("child").is_some());
643 }
644}