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 use proptest::prelude::*;
443
444 proptest! {
451 #![proptest_config(ProptestConfig::with_cases(64))]
452
453 #[test]
456 fn prop_euclidean_to_poincare_preserves_dim(
457 v in proptest::collection::vec(-2.0_f32..2.0, 1..16),
458 ) {
459 let out = euclidean_to_poincare(&v, -1.0);
460 prop_assert_eq!(out.len(), v.len());
461 }
462
463 #[test]
465 fn prop_euclidean_to_poincare_zero_is_zero(dim in 1_usize..16) {
466 let v = vec![0.0_f32; dim];
467 let out = euclidean_to_poincare(&v, -1.0);
468 prop_assert_eq!(out, v);
469 }
470
471 #[test]
475 fn prop_euclidean_to_poincare_stays_in_ball(
476 v in proptest::collection::vec(-1.0_f32..1.0, 1..8),
477 ) {
478 let out = euclidean_to_poincare(&v, -1.0);
479 let norm_sq: f32 = out.iter().map(|x| x * x).sum();
480 let norm = norm_sq.sqrt();
481 let max_norm = 1.0 / 1.0_f32.sqrt(); prop_assert!(norm <= max_norm + 1e-5, "norm {} > max {}", norm, max_norm);
483 }
484
485 #[test]
487 fn prop_distance_is_nonneg(
488 a in proptest::collection::vec(-0.5_f32..0.5, 1..8),
489 b in proptest::collection::vec(-0.5_f32..0.5, 1..8),
490 ) {
491 let d = hyperbolic_distance(&a, &b, -1.0);
492 prop_assert!(d >= 0.0, "distance must be non-negative, got {}", d);
493 }
494
495 #[test]
497 fn prop_distance_self_is_zero(
498 a in proptest::collection::vec(-0.5_f32..0.5, 1..8),
499 ) {
500 let d = hyperbolic_distance(&a, &a, -1.0);
501 prop_assert!(d < 1e-4, "distance(a, a) should be 0, got {}", d);
503 }
504
505 #[test]
507 fn prop_distance_is_symmetric(
508 a in proptest::collection::vec(-0.5_f32..0.5, 1..8),
509 b in proptest::collection::vec(-0.5_f32..0.5, 1..8),
510 ) {
511 let d_ab = hyperbolic_distance(&a, &b, -1.0);
512 let d_ba = hyperbolic_distance(&b, &a, -1.0);
513 prop_assert!(
514 (d_ab - d_ba).abs() < 1e-4,
515 "d(a,b)={} d(b,a)={}",
516 d_ab,
517 d_ba
518 );
519 }
520
521 #[test]
523 fn prop_distance_triangle_inequality(
524 (a, b, c) in (1usize..6).prop_flat_map(|n| (
525 proptest::collection::vec(-0.4_f32..0.4, n.clone()),
526 proptest::collection::vec(-0.4_f32..0.4, n.clone()),
527 proptest::collection::vec(-0.4_f32..0.4, n),
528 )),
529 ) {
530 let d_ab = hyperbolic_distance(&a, &b, -1.0);
531 let d_bc = hyperbolic_distance(&b, &c, -1.0);
532 let d_ac = hyperbolic_distance(&a, &c, -1.0);
533 if d_ab < f32::MAX && d_bc < f32::MAX && d_ac < f32::MAX {
535 prop_assert!(
536 d_ac <= d_ab + d_bc + 1e-2,
537 "triangle inequality violated: d(a,c)={} d(a,b)={} d(b,c)={}",
538 d_ac,
539 d_ab,
540 d_bc
541 );
542 }
543 }
544
545 #[test]
547 fn prop_mobius_add_preserves_dim(
548 (a, b) in (1usize..8).prop_flat_map(|n| (
549 proptest::collection::vec(-0.4_f32..0.4, n.clone()),
550 proptest::collection::vec(-0.4_f32..0.4, n),
551 )),
552 ) {
553 let sum = mobius_add(&a, &b, -1.0);
554 prop_assert_eq!(sum.len(), a.len());
555 }
556
557 #[test]
559 fn prop_mobius_add_zero_identity(
560 a in proptest::collection::vec(-0.4_f32..0.4, 1..8),
561 ) {
562 let zero = vec![0.0_f32; a.len()];
563 let sum = mobius_add(&a, &zero, -1.0);
564 for (got, want) in sum.iter().zip(a.iter()) {
565 prop_assert!((got - want).abs() < 1e-4, "{} vs {}", got, want);
566 }
567 }
568
569 #[test]
571 fn prop_mobius_scalar_mul_zero(
572 v in proptest::collection::vec(-0.4_f32..0.4, 1..8),
573 ) {
574 let r = mobius_scalar_mul(0.0, &v, -1.0, 1e-5);
575 for x in &r {
576 prop_assert!(x.abs() < 1e-4, "expected 0, got {}", x);
577 }
578 }
579 }
580 #[test]
581 fn test_euclidean_to_poincare_zero() {
582 let result = euclidean_to_poincare(&[0.0, 0.0, 0.0], -1.0);
583 assert_eq!(result, vec![0.0, 0.0, 0.0]);
584 }
585
586 #[test]
587 fn test_euclidean_to_poincare_bounded() {
588 let c = -1.0;
589 let result = euclidean_to_poincare(&[100.0, 100.0, 100.0], c);
591 let norm: f32 = result.iter().map(|v| v * v).sum::<f32>().sqrt();
592 let max_norm = 1.0 / c.abs().sqrt();
593 assert!(
594 norm < max_norm,
595 "Result should be inside the ball: norm={}, max={}",
596 norm,
597 max_norm
598 );
599 }
600
601 #[test]
602 fn test_hyperbolic_distance_same_point() {
603 let point = euclidean_to_poincare(&[0.5, 0.3], -1.0);
604 let dist = hyperbolic_distance(&point, &point, -1.0);
605 assert!(dist < 1e-5, "Distance from self should be ~0, got {}", dist);
606 }
607
608 #[test]
609 fn test_hyperbolic_distance_symmetry() {
610 let a = euclidean_to_poincare(&[1.0, 2.0], -1.0);
611 let b = euclidean_to_poincare(&[3.0, 1.0], -1.0);
612 let d_ab = hyperbolic_distance(&a, &b, -1.0);
613 let d_ba = hyperbolic_distance(&b, &a, -1.0);
614 assert!(
615 (d_ab - d_ba).abs() < 1e-4,
616 "Distance should be symmetric: {} vs {}",
617 d_ab,
618 d_ba
619 );
620 }
621
622 #[test]
623 fn test_hyperbolic_distance_triangle_inequality() {
624 let a = euclidean_to_poincare(&[1.0, 0.0], -1.0);
625 let b = euclidean_to_poincare(&[0.0, 1.0], -1.0);
626 let c = euclidean_to_poincare(&[2.0, 2.0], -1.0);
627
628 let d_ab = hyperbolic_distance(&a, &b, -1.0);
629 let d_bc = hyperbolic_distance(&b, &c, -1.0);
630 let d_ac = hyperbolic_distance(&a, &c, -1.0);
631
632 assert!(
633 d_ac <= d_ab + d_bc + 1e-4,
634 "Triangle inequality: d(a,c)={} should be <= d(a,b)+d(b,c)={}",
635 d_ac,
636 d_ab + d_bc
637 );
638 }
639
640 #[test]
641 fn test_mobius_add_identity() {
642 let a = euclidean_to_poincare(&[0.5, 0.3], -1.0);
643 let zero = vec![0.0, 0.0];
644 let result = mobius_add(&a, &zero, -1.0);
645 for (r, expected) in result.iter().zip(a.iter()) {
646 assert!((r - expected).abs() < 1e-4, "a ⊕ 0 should equal a");
647 }
648 }
649
650 #[test]
651 fn test_mobius_scalar_mul_zero() {
652 let v = euclidean_to_poincare(&[1.0, 2.0], -1.0);
653 let result = mobius_scalar_mul(0.0, &v, -1.0, 1e-5);
654 for r in &result {
655 assert!(r.abs() < 1e-4, "0 ⊗ v should be ~0, got {}", r);
656 }
657 }
658
659 #[test]
660 fn test_mobius_scalar_mul_one() {
661 let v = euclidean_to_poincare(&[1.0, 2.0], -1.0);
662 let result = mobius_scalar_mul(1.0, &v, -1.0, 1e-5);
663 for (r, expected) in result.iter().zip(v.iter()) {
664 assert!((r - expected).abs() < 1e-4, "1 ⊗ v should equal v");
665 }
666 }
667
668 #[test]
669 fn test_hyperbolic_embedding_add_and_search() {
670 let mut he = HyperbolicEmbedding::with_dimensions(3);
671
672 he.add("root", &[0.0, 0.0, 0.0]);
673 he.add("child_a", &[1.0, 0.0, 0.0]);
674 he.add("child_b", &[0.0, 1.0, 0.0]);
675 he.add("grandchild", &[1.0, 1.0, 0.0]);
676
677 assert_eq!(he.len(), 4);
678
679 let nn = he.nearest_neighbors("child_a", 2);
681 assert_eq!(nn.len(), 2);
682 let gc_dist = nn
684 .iter()
685 .find(|(name, _)| name == "grandchild")
686 .map(|(_, d)| *d);
687 let cb_dist = nn
688 .iter()
689 .find(|(name, _)| name == "child_b")
690 .map(|(_, d)| *d);
691 if let (Some(gc), Some(cb)) = (gc_dist, cb_dist) {
692 assert!(
693 gc < cb,
694 "grandchild should be closer to child_a than child_b"
695 );
696 }
697 }
698
699 #[test]
700 fn test_hyperbolic_embedding_depth() {
701 let mut he = HyperbolicEmbedding::with_dimensions(2);
702
703 he.add("root", &[0.0, 0.0]);
704 he.add("level1", &[0.5, 0.0]);
705 he.add("level2", &[1.0, 0.0]);
706
707 let root_depth = he.depth("root");
708 let l1_depth = he.depth("level1");
709 let l2_depth = he.depth("level2");
710
711 assert!(
712 root_depth < l1_depth,
713 "Root should be shallower: root={}, l1={}",
714 root_depth,
715 l1_depth
716 );
717 assert!(
718 l1_depth < l2_depth,
719 "Level1 should be shallower: l1={}, l2={}",
720 l1_depth,
721 l2_depth
722 );
723 }
724
725 #[test]
726 fn test_rank_by_depth() {
727 let mut he = HyperbolicEmbedding::with_dimensions(2);
728
729 he.add("leaf", &[2.0, 2.0]);
730 he.add("root", &[0.0, 0.0]);
731 he.add("mid", &[0.5, 0.5]);
732
733 let ranked = he.rank_by_depth();
734 assert_eq!(ranked[0].0, "root");
735 assert_eq!(ranked[1].0, "mid");
736 assert_eq!(ranked[2].0, "leaf");
737 }
738
739 #[test]
740 fn test_batch_conversion() {
741 let vectors = vec![vec![1.0, 2.0], vec![3.0, 4.0], vec![0.0, 0.0]];
742 let results = batch_euclidean_to_poincare(&vectors, -1.0);
743 assert_eq!(results.len(), 3);
744 assert_eq!(results[2], vec![0.0, 0.0]);
746 }
747
748 #[test]
749 fn test_curvature_effect() {
750 let v = [1.0, 1.0];
751
752 let p1 = euclidean_to_poincare(&v, -1.0);
753 let p2 = euclidean_to_poincare(&v, -2.0);
754
755 let norm1: f32 = p1.iter().map(|x| x * x).sum::<f32>().sqrt();
756 let norm2: f32 = p2.iter().map(|x| x * x).sum::<f32>().sqrt();
757
758 assert!(
760 norm2 < norm1,
761 "Higher curvature should produce smaller ball: {} vs {}",
762 norm2,
763 norm1
764 );
765 }
766
767 #[test]
768 fn test_add_child_hierarchy() {
769 let mut he = HyperbolicEmbedding::with_dimensions(3);
770
771 he.add("parent", &[1.0, 0.0, 0.0]);
773 he.add_child("parent", "child", &[0.5, 0.5, 0.0]);
774
775 assert_eq!(he.len(), 2);
776
777 assert!(he.get("parent").is_some());
779 assert!(he.get("child").is_some());
780 }
781}