saorsa_core/adaptive/
hyperbolic.rs

1// Copyright 2024 Saorsa Labs Limited
2//
3// This software is dual-licensed under:
4// - GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)
5// - Commercial License
6//
7// For AGPL-3.0 license, see LICENSE-AGPL-3.0
8// For commercial licensing, contact: saorsalabs@gmail.com
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under these licenses is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
14//! Hyperbolic geometry routing implementation
15//!
16//! Implements greedy routing in hyperbolic space using the Poincaré disk model
17
18use super::*;
19use async_trait::async_trait;
20use std::collections::HashMap;
21use std::sync::Arc;
22use tokio::sync::RwLock;
23
24/// Hyperbolic space manager for coordinate-based routing
25pub struct HyperbolicSpace {
26    /// Our node's current coordinate
27    my_coordinate: RwLock<HyperbolicCoordinate>,
28
29    /// Neighbor coordinates
30    neighbor_coordinates: Arc<RwLock<HashMap<NodeId, HyperbolicCoordinate>>>,
31
32    /// Coordinate adjustment rate
33    adjustment_rate: f64,
34
35    /// Routing statistics
36    routing_stats: Arc<RwLock<RoutingStats>>,
37}
38
39/// Statistics for hyperbolic routing performance
40#[derive(Debug, Default, Clone)]
41pub struct RoutingStats {
42    pub attempts: u64,
43    pub successes: u64,
44    pub failures: u64,
45    pub fallback_used: u64,
46    pub average_hop_count: f64,
47}
48
49impl Default for HyperbolicSpace {
50    fn default() -> Self {
51        Self::new()
52    }
53}
54
55impl HyperbolicSpace {
56    /// Create a new hyperbolic space instance
57    pub fn new() -> Self {
58        Self {
59            my_coordinate: RwLock::new(HyperbolicCoordinate {
60                r: 0.5,
61                theta: rand::random::<f64>() * 2.0 * std::f64::consts::PI,
62            }),
63            neighbor_coordinates: Arc::new(RwLock::new(HashMap::new())),
64            adjustment_rate: 0.01,
65            routing_stats: Arc::new(RwLock::new(RoutingStats::default())),
66        }
67    }
68
69    /// Test helper: expose neighbor map for read access
70    pub fn neighbors_arc(&self) -> Arc<RwLock<HashMap<NodeId, HyperbolicCoordinate>>> {
71        Arc::clone(&self.neighbor_coordinates)
72    }
73
74    /// Calculate hyperbolic distance between two coordinates
75    pub fn distance(a: &HyperbolicCoordinate, b: &HyperbolicCoordinate) -> f64 {
76        let delta = 2.0 * ((a.r - b.r).powi(2) + (a.theta - b.theta).cos().acos().powi(2)).sqrt();
77        let denominator = (1.0 - a.r.powi(2)) * (1.0 - b.r.powi(2));
78
79        (1.0 + delta / denominator).acosh()
80    }
81
82    /// Perform greedy routing to find next hop
83    pub async fn greedy_route(&self, target: &HyperbolicCoordinate) -> Option<NodeId> {
84        let my_coord = self.my_coordinate.read().await;
85        let my_distance = Self::distance(&my_coord, target);
86
87        let neighbors = self.neighbor_coordinates.read().await;
88        neighbors
89            .iter()
90            .filter(|(_, coord)| Self::distance(coord, target) < my_distance)
91            .min_by(|(_, a), (_, b)| {
92                let dist_a = Self::distance(a, target);
93                let dist_b = Self::distance(b, target);
94                dist_a
95                    .partial_cmp(&dist_b)
96                    .unwrap_or(std::cmp::Ordering::Equal)
97            })
98            .map(|(id, _)| id.clone())
99    }
100
101    /// Adjust our coordinate based on neighbor positions
102    pub async fn adjust_coordinate(&self, neighbor_coords: &[(NodeId, HyperbolicCoordinate)]) {
103        let mut my_coord = self.my_coordinate.write().await;
104
105        // Adjust radial coordinate based on degree
106        let degree = neighbor_coords.len();
107        let target_r = 1.0 - (2.0 / (degree as f64 + 2.0));
108        my_coord.r += self.adjustment_rate * (target_r - my_coord.r);
109
110        // Ensure r stays in bounds
111        my_coord.r = my_coord.r.clamp(0.0, 0.999);
112
113        // Adjust angular coordinate based on neighbor positions
114        if !neighbor_coords.is_empty() {
115            let avg_theta = neighbor_coords
116                .iter()
117                .map(|(_, coord)| coord.theta)
118                .sum::<f64>()
119                / neighbor_coords.len() as f64;
120
121            let angle_diff = angle_difference(avg_theta, my_coord.theta);
122            my_coord.theta += self.adjustment_rate * angle_diff;
123
124            // Normalize theta to [0, 2π)
125            while my_coord.theta < 0.0 {
126                my_coord.theta += 2.0 * std::f64::consts::PI;
127            }
128            while my_coord.theta >= 2.0 * std::f64::consts::PI {
129                my_coord.theta -= 2.0 * std::f64::consts::PI;
130            }
131        }
132    }
133
134    /// Get current coordinate
135    pub async fn get_coordinate(&self) -> HyperbolicCoordinate {
136        *self.my_coordinate.read().await
137    }
138
139    /// Update a neighbor's coordinate
140    pub async fn update_neighbor(&self, node_id: NodeId, coord: HyperbolicCoordinate) {
141        let mut neighbors = self.neighbor_coordinates.write().await;
142        neighbors.insert(node_id, coord);
143    }
144
145    /// Remove a neighbor
146    pub async fn remove_neighbor(&self, node_id: &NodeId) {
147        let mut neighbors = self.neighbor_coordinates.write().await;
148        neighbors.remove(node_id);
149    }
150
151    /// Get routing statistics
152    pub async fn get_stats(&self) -> RoutingStats {
153        self.routing_stats.read().await.clone()
154    }
155
156    /// Get routing success rate
157    pub async fn get_success_rate(&self) -> f64 {
158        let stats = self.routing_stats.read().await;
159        if stats.attempts == 0 {
160            0.0
161        } else {
162            stats.successes as f64 / stats.attempts as f64
163        }
164    }
165
166    /// Record routing attempt result
167    pub async fn record_routing_result(
168        &self,
169        success: bool,
170        hop_count: usize,
171        used_fallback: bool,
172    ) {
173        let mut stats = self.routing_stats.write().await;
174        stats.attempts += 1;
175
176        if success {
177            stats.successes += 1;
178        } else {
179            stats.failures += 1;
180        }
181
182        if used_fallback {
183            stats.fallback_used += 1;
184        }
185
186        // Update average hop count (exponential moving average)
187        let alpha = 0.1;
188        stats.average_hop_count =
189            (1.0 - alpha) * stats.average_hop_count + alpha * hop_count as f64;
190    }
191}
192
193/// Calculate the shortest angular difference between two angles
194pub fn angle_difference(a: f64, b: f64) -> f64 {
195    let diff = a - b;
196    if diff > std::f64::consts::PI {
197        diff - 2.0 * std::f64::consts::PI
198    } else if diff < -std::f64::consts::PI {
199        diff + 2.0 * std::f64::consts::PI
200    } else {
201        diff
202    }
203}
204
205/// Hyperbolic routing strategy for integration with AdaptiveRouter
206pub struct HyperbolicRoutingStrategy {
207    /// The hyperbolic space manager
208    space: Arc<HyperbolicSpace>,
209
210    /// Local node ID
211    local_id: NodeId,
212
213    /// Maximum hops before declaring failure
214    max_hops: usize,
215}
216
217impl HyperbolicRoutingStrategy {
218    /// Create a new hyperbolic routing strategy
219    pub fn new(local_id: NodeId, space: Arc<HyperbolicSpace>) -> Self {
220        Self {
221            space,
222            local_id,
223            max_hops: 10,
224        }
225    }
226
227    /// Find path using greedy hyperbolic routing
228    async fn find_hyperbolic_path(&self, target: &NodeId) -> Result<Vec<NodeId>> {
229        // Check if we have the target's coordinate
230        let target_coord = {
231            let neighbors = self.space.neighbor_coordinates.read().await;
232            neighbors.get(target).cloned()
233        };
234
235        let target_coord = match target_coord {
236            Some(coord) => coord,
237            None => {
238                // We don't know the target's coordinate, can't route
239                return Err(AdaptiveNetworkError::Routing(
240                    "Target coordinate unknown".to_string(),
241                ));
242            }
243        };
244
245        let mut path = Vec::new();
246        let mut _current = self.local_id.clone();
247        let mut visited = std::collections::HashSet::<NodeId>::new();
248        visited.insert(_current.clone());
249
250        // Greedy routing with loop detection
251        for _ in 0..self.max_hops {
252            // Find next hop
253            let next_hop = self.space.greedy_route(&target_coord).await;
254
255            match next_hop {
256                Some(next) => {
257                    if next == *target {
258                        // Reached target
259                        path.push(next);
260                        return Ok(path);
261                    }
262
263                    if visited.contains(&next) {
264                        // Loop detected, routing failed
265                        return Err(AdaptiveNetworkError::Routing(
266                            "Routing loop detected".to_string(),
267                        ));
268                    }
269
270                    path.push(next.clone());
271                    visited.insert(next.clone());
272                    _current = next;
273                }
274                None => {
275                    // No closer neighbor found, greedy routing failed
276                    return Err(AdaptiveNetworkError::Routing(
277                        "No closer neighbor found".to_string(),
278                    ));
279                }
280            }
281        }
282
283        // Max hops exceeded
284        Err(AdaptiveNetworkError::Routing(
285            "Maximum hop count exceeded".to_string(),
286        ))
287    }
288}
289
290#[async_trait]
291impl RoutingStrategy for HyperbolicRoutingStrategy {
292    async fn find_path(&self, target: &NodeId) -> Result<Vec<NodeId>> {
293        // Try hyperbolic routing
294        let result = self.find_hyperbolic_path(target).await;
295
296        // Record the result
297        let (success, hop_count, used_fallback) = match &result {
298            Ok(path) => (true, path.len(), false),
299            Err(_) => (false, 0, true), // Will use fallback
300        };
301
302        self.space
303            .record_routing_result(success, hop_count, used_fallback)
304            .await;
305
306        result
307    }
308
309    fn route_score(&self, _neighbor: &NodeId, _target: &NodeId) -> f64 {
310        // This is synchronous, so we can't access async coordinates
311        // Return a default score - the actual routing logic is in find_path
312        0.5
313    }
314
315    fn update_metrics(&mut self, _path: &[NodeId], _success: bool) {
316        // Metrics are updated in find_path via record_routing_result
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[test]
325    fn test_hyperbolic_distance() {
326        let origin = HyperbolicCoordinate { r: 0.0, theta: 0.0 };
327        let point = HyperbolicCoordinate {
328            r: 0.5,
329            theta: std::f64::consts::PI,
330        };
331
332        let distance = HyperbolicSpace::distance(&origin, &point);
333        assert!(distance > 0.0);
334
335        // Distance to self should be 0
336        let self_distance = HyperbolicSpace::distance(&origin, &origin);
337        assert!((self_distance - 0.0).abs() < 1e-10);
338    }
339
340    #[test]
341    fn test_angle_difference() {
342        assert!((angle_difference(0.0, 0.0) - 0.0).abs() < 1e-10);
343        assert!((angle_difference(std::f64::consts::PI, 0.0) - std::f64::consts::PI).abs() < 1e-10);
344        assert!(
345            (angle_difference(0.0, std::f64::consts::PI) - (-std::f64::consts::PI)).abs() < 1e-10
346        );
347        assert!(
348            (angle_difference(1.9 * std::f64::consts::PI, 0.1 * std::f64::consts::PI)
349                - (-0.2 * std::f64::consts::PI))
350                .abs()
351                < 1e-10
352        );
353    }
354
355    #[tokio::test]
356    async fn test_coordinate_adjustment() {
357        let space = HyperbolicSpace::new();
358        let initial = space.get_coordinate().await;
359
360        // Simulate neighbors at the edge
361        use rand::RngCore;
362
363        let mut hash1 = [0u8; 32];
364        rand::thread_rng().fill_bytes(&mut hash1);
365        let mut hash2 = [0u8; 32];
366        rand::thread_rng().fill_bytes(&mut hash2);
367
368        let neighbors = vec![
369            (
370                NodeId::from_bytes(hash1),
371                HyperbolicCoordinate { r: 0.9, theta: 0.0 },
372            ),
373            (
374                NodeId::from_bytes(hash2),
375                HyperbolicCoordinate {
376                    r: 0.9,
377                    theta: std::f64::consts::PI,
378                },
379            ),
380        ];
381
382        space.adjust_coordinate(&neighbors).await;
383        let adjusted = space.get_coordinate().await;
384
385        // Should move towards edge with high-degree neighbors
386        assert!(adjusted.r > initial.r);
387    }
388
389    #[tokio::test]
390    async fn test_routing_stats() {
391        let space = HyperbolicSpace::new();
392
393        // Record some routing results
394        space.record_routing_result(true, 3, false).await;
395        space.record_routing_result(true, 4, false).await;
396        space.record_routing_result(false, 0, true).await;
397
398        let stats = space.get_stats().await;
399        assert_eq!(stats.attempts, 3);
400        assert_eq!(stats.successes, 2);
401        assert_eq!(stats.failures, 1);
402        assert_eq!(stats.fallback_used, 1);
403
404        let success_rate = space.get_success_rate().await;
405        assert!((success_rate - 0.666).abs() < 0.01);
406    }
407
408    #[tokio::test]
409    async fn test_hyperbolic_routing_strategy() {
410        use rand::RngCore;
411
412        // Create space and strategy
413        let space = Arc::new(HyperbolicSpace::new());
414
415        let mut hash = [0u8; 32];
416        rand::thread_rng().fill_bytes(&mut hash);
417        let local_id = NodeId::from_bytes(hash);
418
419        let strategy = HyperbolicRoutingStrategy::new(local_id.clone(), space.clone());
420
421        // Add some neighbors with coordinates
422        let mut hash1 = [0u8; 32];
423        rand::thread_rng().fill_bytes(&mut hash1);
424        let neighbor1 = NodeId::from_bytes(hash1);
425
426        let mut hash2 = [0u8; 32];
427        rand::thread_rng().fill_bytes(&mut hash2);
428        let neighbor2 = NodeId::from_bytes(hash2);
429
430        let mut hash_target = [0u8; 32];
431        rand::thread_rng().fill_bytes(&mut hash_target);
432        let target = NodeId::from_bytes(hash_target);
433
434        // Set up coordinates
435        space
436            .update_neighbor(
437                neighbor1.clone(),
438                HyperbolicCoordinate { r: 0.3, theta: 0.0 },
439            )
440            .await;
441        space
442            .update_neighbor(
443                neighbor2.clone(),
444                HyperbolicCoordinate { r: 0.7, theta: 1.0 },
445            )
446            .await;
447        space
448            .update_neighbor(target.clone(), HyperbolicCoordinate { r: 0.8, theta: 1.5 })
449            .await;
450
451        // Try routing to target
452        let _result = strategy.find_path(&target).await;
453
454        // Without proper network setup, routing will fail, but we can check stats
455        let stats = space.get_stats().await;
456        assert_eq!(stats.attempts, 1);
457    }
458
459    #[tokio::test]
460    async fn test_greedy_routing() {
461        let space = HyperbolicSpace::new();
462
463        use rand::RngCore;
464
465        // Add neighbors at various positions
466        let mut neighbors = vec![];
467        for i in 0..5 {
468            let mut hash = [0u8; 32];
469            rand::thread_rng().fill_bytes(&mut hash);
470            let node_id = NodeId::from_bytes(hash);
471
472            let coord = HyperbolicCoordinate {
473                r: 0.1 + (i as f64) * 0.2,
474                theta: (i as f64) * std::f64::consts::PI / 3.0,
475            };
476
477            space.update_neighbor(node_id.clone(), coord).await;
478            neighbors.push((node_id, coord));
479        }
480
481        // Test greedy routing to a target
482        let target_coord = HyperbolicCoordinate { r: 0.9, theta: 1.0 };
483        let next_hop = space.greedy_route(&target_coord).await;
484
485        // Should find a neighbor closer to target
486        assert!(next_hop.is_some());
487
488        // Verify it chose the closest neighbor
489        if let Some(chosen) = next_hop {
490            let neighbors_map = space.neighbor_coordinates.read().await;
491            let chosen_coord = neighbors_map.get(&chosen).unwrap();
492            let chosen_dist = HyperbolicSpace::distance(chosen_coord, &target_coord);
493
494            // Check that no other neighbor is closer
495            for (id, coord) in &neighbors {
496                if id != &chosen {
497                    let dist = HyperbolicSpace::distance(coord, &target_coord);
498                    assert!(dist >= chosen_dist);
499                }
500            }
501        }
502    }
503}