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.ln().max(0.0).sqrt()
141}
142
143pub fn mobius_add(a: &[f32], b: &[f32], curvature: f32) -> Vec<f32> {
148 let c = curvature.abs();
149
150 let norm_a_sq: f32 = a.iter().map(|v| v * v).sum();
151 let norm_b_sq: f32 = b.iter().map(|v| v * v).sum();
152 let dot_ab: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
153
154 let denominator = 1.0 + 2.0 * c * dot_ab + c * c * norm_a_sq * norm_b_sq;
155
156 if denominator.abs() < 1e-10 {
157 return vec![0.0; a.len()];
159 }
160
161 let scale_a = 1.0 + 2.0 * c * dot_ab + c * norm_b_sq;
162 let scale_b = 1.0 - c * norm_a_sq;
163
164 a.iter()
165 .zip(b)
166 .map(|(&ai, &bi)| (scale_a * ai + scale_b * bi) / denominator)
167 .collect()
168}
169
170pub fn mobius_scalar_mul(scalar: f32, v: &[f32], curvature: f32, epsilon: f32) -> Vec<f32> {
180 let c = curvature.abs();
181 let norm_sq: f32 = v.iter().map(|x| x * x).sum();
182 let norm = norm_sq.sqrt();
183
184 if norm < epsilon {
185 return vec![0.0; v.len()];
186 }
187
188 let c_sqrt = c.sqrt();
189 let w = c_sqrt * norm;
190
191 let w = w.min(1.0 - epsilon);
193 let result_norm = (1.0 / c_sqrt) * (scalar * w.atanh()).tanh();
194
195 let scale = result_norm / norm;
196 v.iter().map(|&vi| vi * scale).collect()
197}
198
199pub struct HyperbolicEmbedding {
208 config: HyperbolicConfig,
209 embeddings: Vec<(String, Vec<f32>)>,
211}
212
213impl HyperbolicEmbedding {
214 pub fn new(config: HyperbolicConfig) -> Self {
216 Self {
217 config,
218 embeddings: Vec::new(),
219 }
220 }
221
222 pub fn from_pairs(pairs: Vec<(String, Vec<f32>)>) -> Self {
228 Self {
231 config: HyperbolicConfig::default(),
232 embeddings: pairs,
233 }
234 }
235
236 pub fn with_dimensions(dimensions: usize) -> Self {
238 let config = HyperbolicConfig {
239 dimensions,
240 ..Default::default()
241 };
242 Self::new(config)
243 }
244
245 pub fn add(&mut self, id: &str, euclidean: &[f32]) {
249 let poincare = euclidean_to_poincare(euclidean, self.config.curvature);
250 if let Some(pos) = self.embeddings.iter().position(|(name, _)| name == id) {
252 self.embeddings[pos] = (id.to_string(), poincare);
253 } else {
254 self.embeddings.push((id.to_string(), poincare));
255 }
256 }
257
258 pub fn add_child(&mut self, parent_id: &str, child_id: &str, child_euclidean: &[f32]) {
263 let child_on_ball = euclidean_to_poincare(child_euclidean, self.config.curvature);
264
265 let child_point = if let Some((_, parent_vec)) =
266 self.embeddings.iter().find(|(name, _)| name == parent_id)
267 {
268 mobius_add(parent_vec, &child_on_ball, self.config.curvature)
271 } else {
272 child_on_ball
273 };
274
275 if let Some(pos) = self
276 .embeddings
277 .iter()
278 .position(|(name, _)| name == child_id)
279 {
280 self.embeddings[pos] = (child_id.to_string(), child_point);
281 } else {
282 self.embeddings.push((child_id.to_string(), child_point));
283 }
284 }
285
286 pub fn get(&self, id: &str) -> Option<&[f32]> {
288 self.embeddings
289 .iter()
290 .find(|(name, _)| name == id)
291 .map(|(_, v)| v.as_slice())
292 }
293
294 pub fn nearest_neighbors(&self, query_id: &str, k: usize) -> Vec<(String, f32)> {
298 let query = match self.get(query_id) {
299 Some(v) => v.to_vec(),
300 None => return Vec::new(),
301 };
302
303 let mut results: Vec<(String, f32)> = self
304 .embeddings
305 .iter()
306 .filter(|(name, _)| name != query_id)
307 .map(|(name, vec)| {
308 let dist = hyperbolic_distance(&query, vec, self.config.curvature);
309 (name.clone(), dist)
310 })
311 .collect();
312
313 results.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
314 results.truncate(k);
315 results
316 }
317
318 pub fn search(&self, query: &[f32], k: usize) -> Vec<(String, f32)> {
320 let query_poincare = euclidean_to_poincare(query, self.config.curvature);
321
322 let mut results: Vec<(String, f32)> = self
323 .embeddings
324 .iter()
325 .map(|(name, vec)| {
326 let dist = hyperbolic_distance(&query_poincare, vec, self.config.curvature);
327 (name.clone(), dist)
328 })
329 .collect();
330
331 results.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
332 results.truncate(k);
333 results
334 }
335
336 pub fn hierarchical_distance(&self, id_a: &str, id_b: &str) -> f32 {
342 let a = match self.get(id_a) {
343 Some(v) => v,
344 None => return f32::MAX,
345 };
346 let b = match self.get(id_b) {
347 Some(v) => v,
348 None => return f32::MAX,
349 };
350
351 hyperbolic_distance(a, b, self.config.curvature)
352 }
353
354 pub fn len(&self) -> usize {
356 self.embeddings.len()
357 }
358
359 pub fn is_empty(&self) -> bool {
361 self.embeddings.is_empty()
362 }
363
364 pub fn ids(&self) -> Vec<&str> {
366 self.embeddings
367 .iter()
368 .map(|(name, _)| name.as_str())
369 .collect()
370 }
371
372 pub fn all_embeddings(&self) -> &[(String, Vec<f32>)] {
374 &self.embeddings
375 }
376
377 pub fn search_memories(&self, query_euclidean: &[f32], k: usize) -> Vec<(String, f32)> {
389 self.search(query_euclidean, k)
390 }
391
392 pub fn hierarchical_rank(&self) -> Vec<(String, f32)> {
399 self.rank_by_depth()
400 }
401
402 pub fn depth(&self, id: &str) -> f32 {
406 match self.get(id) {
407 Some(v) => hyperbolic_distance(&vec![0.0; v.len()], v, self.config.curvature),
408 None => f32::MAX,
409 }
410 }
411
412 pub fn rank_by_depth(&self) -> Vec<(String, f32)> {
417 let mut ranked: Vec<(String, f32)> = self
418 .embeddings
419 .iter()
420 .map(|(name, vec)| {
421 let origin = vec![0.0; vec.len()];
422 let d = hyperbolic_distance(&origin, vec, self.config.curvature);
423 (name.clone(), d)
424 })
425 .collect();
426
427 ranked.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
428 ranked
429 }
430}
431
432#[cfg(test)]
437mod tests {
438 use super::*;
439
440 #[test]
441 fn test_euclidean_to_poincare_zero() {
442 let result = euclidean_to_poincare(&[0.0, 0.0, 0.0], -1.0);
443 assert_eq!(result, vec![0.0, 0.0, 0.0]);
444 }
445
446 #[test]
447 fn test_euclidean_to_poincare_bounded() {
448 let c = -1.0;
449 let result = euclidean_to_poincare(&[100.0, 100.0, 100.0], c);
451 let norm: f32 = result.iter().map(|v| v * v).sum::<f32>().sqrt();
452 let max_norm = 1.0 / c.abs().sqrt();
453 assert!(
454 norm < max_norm,
455 "Result should be inside the ball: norm={}, max={}",
456 norm,
457 max_norm
458 );
459 }
460
461 #[test]
462 fn test_hyperbolic_distance_same_point() {
463 let point = euclidean_to_poincare(&[0.5, 0.3], -1.0);
464 let dist = hyperbolic_distance(&point, &point, -1.0);
465 assert!(dist < 1e-5, "Distance from self should be ~0, got {}", dist);
466 }
467
468 #[test]
469 fn test_hyperbolic_distance_symmetry() {
470 let a = euclidean_to_poincare(&[1.0, 2.0], -1.0);
471 let b = euclidean_to_poincare(&[3.0, 1.0], -1.0);
472 let d_ab = hyperbolic_distance(&a, &b, -1.0);
473 let d_ba = hyperbolic_distance(&b, &a, -1.0);
474 assert!(
475 (d_ab - d_ba).abs() < 1e-4,
476 "Distance should be symmetric: {} vs {}",
477 d_ab,
478 d_ba
479 );
480 }
481
482 #[test]
483 fn test_hyperbolic_distance_triangle_inequality() {
484 let a = euclidean_to_poincare(&[1.0, 0.0], -1.0);
485 let b = euclidean_to_poincare(&[0.0, 1.0], -1.0);
486 let c = euclidean_to_poincare(&[2.0, 2.0], -1.0);
487
488 let d_ab = hyperbolic_distance(&a, &b, -1.0);
489 let d_bc = hyperbolic_distance(&b, &c, -1.0);
490 let d_ac = hyperbolic_distance(&a, &c, -1.0);
491
492 assert!(
493 d_ac <= d_ab + d_bc + 1e-4,
494 "Triangle inequality: d(a,c)={} should be <= d(a,b)+d(b,c)={}",
495 d_ac,
496 d_ab + d_bc
497 );
498 }
499
500 #[test]
501 fn test_mobius_add_identity() {
502 let a = euclidean_to_poincare(&[0.5, 0.3], -1.0);
503 let zero = vec![0.0, 0.0];
504 let result = mobius_add(&a, &zero, -1.0);
505 for (r, expected) in result.iter().zip(a.iter()) {
506 assert!((r - expected).abs() < 1e-4, "a ⊕ 0 should equal a");
507 }
508 }
509
510 #[test]
511 fn test_mobius_scalar_mul_zero() {
512 let v = euclidean_to_poincare(&[1.0, 2.0], -1.0);
513 let result = mobius_scalar_mul(0.0, &v, -1.0, 1e-5);
514 for r in &result {
515 assert!(r.abs() < 1e-4, "0 ⊗ v should be ~0, got {}", r);
516 }
517 }
518
519 #[test]
520 fn test_mobius_scalar_mul_one() {
521 let v = euclidean_to_poincare(&[1.0, 2.0], -1.0);
522 let result = mobius_scalar_mul(1.0, &v, -1.0, 1e-5);
523 for (r, expected) in result.iter().zip(v.iter()) {
524 assert!((r - expected).abs() < 1e-4, "1 ⊗ v should equal v");
525 }
526 }
527
528 #[test]
529 fn test_hyperbolic_embedding_add_and_search() {
530 let mut he = HyperbolicEmbedding::with_dimensions(3);
531
532 he.add("root", &[0.0, 0.0, 0.0]);
533 he.add("child_a", &[1.0, 0.0, 0.0]);
534 he.add("child_b", &[0.0, 1.0, 0.0]);
535 he.add("grandchild", &[1.0, 1.0, 0.0]);
536
537 assert_eq!(he.len(), 4);
538
539 let nn = he.nearest_neighbors("child_a", 2);
541 assert_eq!(nn.len(), 2);
542 let gc_dist = nn
544 .iter()
545 .find(|(name, _)| name == "grandchild")
546 .map(|(_, d)| *d);
547 let cb_dist = nn
548 .iter()
549 .find(|(name, _)| name == "child_b")
550 .map(|(_, d)| *d);
551 if let (Some(gc), Some(cb)) = (gc_dist, cb_dist) {
552 assert!(
553 gc < cb,
554 "grandchild should be closer to child_a than child_b"
555 );
556 }
557 }
558
559 #[test]
560 fn test_hyperbolic_embedding_depth() {
561 let mut he = HyperbolicEmbedding::with_dimensions(2);
562
563 he.add("root", &[0.0, 0.0]);
564 he.add("level1", &[0.5, 0.0]);
565 he.add("level2", &[1.0, 0.0]);
566
567 let root_depth = he.depth("root");
568 let l1_depth = he.depth("level1");
569 let l2_depth = he.depth("level2");
570
571 assert!(
572 root_depth < l1_depth,
573 "Root should be shallower: root={}, l1={}",
574 root_depth,
575 l1_depth
576 );
577 assert!(
578 l1_depth < l2_depth,
579 "Level1 should be shallower: l1={}, l2={}",
580 l1_depth,
581 l2_depth
582 );
583 }
584
585 #[test]
586 fn test_rank_by_depth() {
587 let mut he = HyperbolicEmbedding::with_dimensions(2);
588
589 he.add("leaf", &[2.0, 2.0]);
590 he.add("root", &[0.0, 0.0]);
591 he.add("mid", &[0.5, 0.5]);
592
593 let ranked = he.rank_by_depth();
594 assert_eq!(ranked[0].0, "root");
595 assert_eq!(ranked[1].0, "mid");
596 assert_eq!(ranked[2].0, "leaf");
597 }
598
599 #[test]
600 fn test_batch_conversion() {
601 let vectors = vec![vec![1.0, 2.0], vec![3.0, 4.0], vec![0.0, 0.0]];
602 let results = batch_euclidean_to_poincare(&vectors, -1.0);
603 assert_eq!(results.len(), 3);
604 assert_eq!(results[2], vec![0.0, 0.0]);
606 }
607
608 #[test]
609 fn test_curvature_effect() {
610 let v = [1.0, 1.0];
611
612 let p1 = euclidean_to_poincare(&v, -1.0);
613 let p2 = euclidean_to_poincare(&v, -2.0);
614
615 let norm1: f32 = p1.iter().map(|x| x * x).sum::<f32>().sqrt();
616 let norm2: f32 = p2.iter().map(|x| x * x).sum::<f32>().sqrt();
617
618 assert!(
620 norm2 < norm1,
621 "Higher curvature should produce smaller ball: {} vs {}",
622 norm2,
623 norm1
624 );
625 }
626
627 #[test]
628 fn test_add_child_hierarchy() {
629 let mut he = HyperbolicEmbedding::with_dimensions(3);
630
631 he.add("parent", &[1.0, 0.0, 0.0]);
633 he.add_child("parent", "child", &[0.5, 0.5, 0.0]);
634
635 assert_eq!(he.len(), 2);
636
637 assert!(he.get("parent").is_some());
639 assert!(he.get("child").is_some());
640 }
641}