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 and neighbors' radial positions
106        let degree = neighbor_coords.len();
107        let deg_term = 1.0 - (2.0 / (degree as f64 + 2.0));
108        let avg_neighbor_r = if degree > 0 {
109            neighbor_coords.iter().map(|(_, c)| c.r).sum::<f64>() / degree as f64
110        } else {
111            my_coord.r
112        };
113        // Blend the degree-based target with the neighbors' average radius
114        let target_r = 0.5 * deg_term + 0.5 * avg_neighbor_r;
115        my_coord.r += self.adjustment_rate * (target_r - my_coord.r);
116
117        // Ensure r stays in bounds
118        my_coord.r = my_coord.r.clamp(0.0, 0.999);
119
120        // Adjust angular coordinate based on neighbor positions
121        if !neighbor_coords.is_empty() {
122            let avg_theta = neighbor_coords
123                .iter()
124                .map(|(_, coord)| coord.theta)
125                .sum::<f64>()
126                / neighbor_coords.len() as f64;
127
128            let angle_diff = angle_difference(avg_theta, my_coord.theta);
129            my_coord.theta += self.adjustment_rate * angle_diff;
130
131            // Normalize theta to [0, 2π)
132            while my_coord.theta < 0.0 {
133                my_coord.theta += 2.0 * std::f64::consts::PI;
134            }
135            while my_coord.theta >= 2.0 * std::f64::consts::PI {
136                my_coord.theta -= 2.0 * std::f64::consts::PI;
137            }
138        }
139    }
140
141    /// Get current coordinate
142    pub async fn get_coordinate(&self) -> HyperbolicCoordinate {
143        *self.my_coordinate.read().await
144    }
145
146    /// Update a neighbor's coordinate
147    pub async fn update_neighbor(&self, node_id: NodeId, coord: HyperbolicCoordinate) {
148        let mut neighbors = self.neighbor_coordinates.write().await;
149        neighbors.insert(node_id, coord);
150    }
151
152    /// Remove a neighbor
153    pub async fn remove_neighbor(&self, node_id: &NodeId) {
154        let mut neighbors = self.neighbor_coordinates.write().await;
155        neighbors.remove(node_id);
156    }
157
158    /// Get routing statistics
159    pub async fn get_stats(&self) -> RoutingStats {
160        self.routing_stats.read().await.clone()
161    }
162
163    /// Get routing success rate
164    pub async fn get_success_rate(&self) -> f64 {
165        let stats = self.routing_stats.read().await;
166        if stats.attempts == 0 {
167            0.0
168        } else {
169            stats.successes as f64 / stats.attempts as f64
170        }
171    }
172
173    /// Record routing attempt result
174    pub async fn record_routing_result(
175        &self,
176        success: bool,
177        hop_count: usize,
178        used_fallback: bool,
179    ) {
180        let mut stats = self.routing_stats.write().await;
181        stats.attempts += 1;
182
183        if success {
184            stats.successes += 1;
185        } else {
186            stats.failures += 1;
187        }
188
189        if used_fallback {
190            stats.fallback_used += 1;
191        }
192
193        // Update average hop count (exponential moving average)
194        let alpha = 0.1;
195        stats.average_hop_count =
196            (1.0 - alpha) * stats.average_hop_count + alpha * hop_count as f64;
197    }
198}
199
200/// Calculate the shortest angular difference between two angles
201pub fn angle_difference(a: f64, b: f64) -> f64 {
202    let diff = a - b;
203    if diff > std::f64::consts::PI {
204        diff - 2.0 * std::f64::consts::PI
205    } else if diff < -std::f64::consts::PI {
206        diff + 2.0 * std::f64::consts::PI
207    } else {
208        diff
209    }
210}
211
212/// Hyperbolic routing strategy for integration with AdaptiveRouter
213pub struct HyperbolicRoutingStrategy {
214    /// The hyperbolic space manager
215    space: Arc<HyperbolicSpace>,
216
217    /// Local node ID
218    local_id: NodeId,
219
220    /// Maximum hops before declaring failure
221    max_hops: usize,
222}
223
224impl HyperbolicRoutingStrategy {
225    /// Create a new hyperbolic routing strategy
226    pub fn new(local_id: NodeId, space: Arc<HyperbolicSpace>) -> Self {
227        Self {
228            space,
229            local_id,
230            max_hops: 10,
231        }
232    }
233
234    /// Find path using greedy hyperbolic routing
235    async fn find_hyperbolic_path(&self, target: &NodeId) -> Result<Vec<NodeId>> {
236        // Check if we have the target's coordinate
237        let target_coord = {
238            let neighbors = self.space.neighbor_coordinates.read().await;
239            neighbors.get(target).cloned()
240        };
241
242        let target_coord = match target_coord {
243            Some(coord) => coord,
244            None => {
245                // We don't know the target's coordinate, can't route
246                return Err(AdaptiveNetworkError::Routing(
247                    "Target coordinate unknown".to_string(),
248                ));
249            }
250        };
251
252        let mut path = Vec::new();
253        let mut _current = self.local_id.clone();
254        let mut visited = std::collections::HashSet::<NodeId>::new();
255        visited.insert(_current.clone());
256
257        // Greedy routing with loop detection
258        for _ in 0..self.max_hops {
259            // Find next hop
260            let next_hop = self.space.greedy_route(&target_coord).await;
261
262            match next_hop {
263                Some(next) => {
264                    if next == *target {
265                        // Reached target
266                        path.push(next);
267                        return Ok(path);
268                    }
269
270                    if visited.contains(&next) {
271                        // Loop detected, routing failed
272                        return Err(AdaptiveNetworkError::Routing(
273                            "Routing loop detected".to_string(),
274                        ));
275                    }
276
277                    path.push(next.clone());
278                    visited.insert(next.clone());
279                    _current = next;
280                }
281                None => {
282                    // No closer neighbor found, greedy routing failed
283                    return Err(AdaptiveNetworkError::Routing(
284                        "No closer neighbor found".to_string(),
285                    ));
286                }
287            }
288        }
289
290        // Max hops exceeded
291        Err(AdaptiveNetworkError::Routing(
292            "Maximum hop count exceeded".to_string(),
293        ))
294    }
295}
296
297#[async_trait]
298impl RoutingStrategy for HyperbolicRoutingStrategy {
299    async fn find_path(&self, target: &NodeId) -> Result<Vec<NodeId>> {
300        // Try hyperbolic routing
301        let result = self.find_hyperbolic_path(target).await;
302
303        // Record the result
304        let (success, hop_count, used_fallback) = match &result {
305            Ok(path) => (true, path.len(), false),
306            Err(_) => (false, 0, true), // Will use fallback
307        };
308
309        self.space
310            .record_routing_result(success, hop_count, used_fallback)
311            .await;
312
313        result
314    }
315
316    fn route_score(&self, _neighbor: &NodeId, _target: &NodeId) -> f64 {
317        // This is synchronous, so we can't access async coordinates
318        // Return a default score - the actual routing logic is in find_path
319        0.5
320    }
321
322    fn update_metrics(&mut self, _path: &[NodeId], _success: bool) {
323        // Metrics are updated in find_path via record_routing_result
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330
331    #[test]
332    fn test_hyperbolic_distance() {
333        let origin = HyperbolicCoordinate { r: 0.0, theta: 0.0 };
334        let point = HyperbolicCoordinate {
335            r: 0.5,
336            theta: std::f64::consts::PI,
337        };
338
339        let distance = HyperbolicSpace::distance(&origin, &point);
340        assert!(distance > 0.0);
341
342        // Distance to self should be 0
343        let self_distance = HyperbolicSpace::distance(&origin, &origin);
344        assert!((self_distance - 0.0).abs() < 1e-10);
345    }
346
347    #[test]
348    fn test_angle_difference() {
349        assert!((angle_difference(0.0, 0.0) - 0.0).abs() < 1e-10);
350        assert!((angle_difference(std::f64::consts::PI, 0.0) - std::f64::consts::PI).abs() < 1e-10);
351        assert!(
352            (angle_difference(0.0, std::f64::consts::PI) - (-std::f64::consts::PI)).abs() < 1e-10
353        );
354        assert!(
355            (angle_difference(1.9 * std::f64::consts::PI, 0.1 * std::f64::consts::PI)
356                - (-0.2 * std::f64::consts::PI))
357                .abs()
358                < 1e-10
359        );
360    }
361
362    #[tokio::test]
363    async fn test_coordinate_adjustment() {
364        let space = HyperbolicSpace::new();
365        let initial = space.get_coordinate().await;
366
367        // Simulate neighbors at the edge
368        use rand::RngCore;
369
370        let mut hash1 = [0u8; 32];
371        rand::thread_rng().fill_bytes(&mut hash1);
372        let mut hash2 = [0u8; 32];
373        rand::thread_rng().fill_bytes(&mut hash2);
374
375        let neighbors = vec![
376            (
377                NodeId::from_bytes(hash1),
378                HyperbolicCoordinate { r: 0.9, theta: 0.0 },
379            ),
380            (
381                NodeId::from_bytes(hash2),
382                HyperbolicCoordinate {
383                    r: 0.9,
384                    theta: std::f64::consts::PI,
385                },
386            ),
387        ];
388
389        space.adjust_coordinate(&neighbors).await;
390        let adjusted = space.get_coordinate().await;
391
392        // Should move towards edge with high-degree neighbors
393        assert!(adjusted.r > initial.r);
394    }
395
396    #[tokio::test]
397    async fn test_routing_stats() {
398        let space = HyperbolicSpace::new();
399
400        // Record some routing results
401        space.record_routing_result(true, 3, false).await;
402        space.record_routing_result(true, 4, false).await;
403        space.record_routing_result(false, 0, true).await;
404
405        let stats = space.get_stats().await;
406        assert_eq!(stats.attempts, 3);
407        assert_eq!(stats.successes, 2);
408        assert_eq!(stats.failures, 1);
409        assert_eq!(stats.fallback_used, 1);
410
411        let success_rate = space.get_success_rate().await;
412        assert!((success_rate - 0.666).abs() < 0.01);
413    }
414
415    #[tokio::test]
416    async fn test_hyperbolic_routing_strategy() {
417        use rand::RngCore;
418
419        // Create space and strategy
420        let space = Arc::new(HyperbolicSpace::new());
421
422        let mut hash = [0u8; 32];
423        rand::thread_rng().fill_bytes(&mut hash);
424        let local_id = NodeId::from_bytes(hash);
425
426        let strategy = HyperbolicRoutingStrategy::new(local_id.clone(), space.clone());
427
428        // Add some neighbors with coordinates
429        let mut hash1 = [0u8; 32];
430        rand::thread_rng().fill_bytes(&mut hash1);
431        let neighbor1 = NodeId::from_bytes(hash1);
432
433        let mut hash2 = [0u8; 32];
434        rand::thread_rng().fill_bytes(&mut hash2);
435        let neighbor2 = NodeId::from_bytes(hash2);
436
437        let mut hash_target = [0u8; 32];
438        rand::thread_rng().fill_bytes(&mut hash_target);
439        let target = NodeId::from_bytes(hash_target);
440
441        // Set up coordinates
442        space
443            .update_neighbor(
444                neighbor1.clone(),
445                HyperbolicCoordinate { r: 0.3, theta: 0.0 },
446            )
447            .await;
448        space
449            .update_neighbor(
450                neighbor2.clone(),
451                HyperbolicCoordinate { r: 0.7, theta: 1.0 },
452            )
453            .await;
454        space
455            .update_neighbor(target.clone(), HyperbolicCoordinate { r: 0.8, theta: 1.5 })
456            .await;
457
458        // Try routing to target
459        let _result = strategy.find_path(&target).await;
460
461        // Without proper network setup, routing will fail, but we can check stats
462        let stats = space.get_stats().await;
463        assert_eq!(stats.attempts, 1);
464    }
465
466    #[tokio::test]
467    async fn test_greedy_routing() {
468        let space = HyperbolicSpace::new();
469
470        use rand::RngCore;
471
472        // Add neighbors at various positions
473        let mut neighbors = vec![];
474        for i in 0..5 {
475            let mut hash = [0u8; 32];
476            rand::thread_rng().fill_bytes(&mut hash);
477            let node_id = NodeId::from_bytes(hash);
478
479            let coord = HyperbolicCoordinate {
480                r: 0.1 + (i as f64) * 0.2,
481                theta: (i as f64) * std::f64::consts::PI / 3.0,
482            };
483
484            space.update_neighbor(node_id.clone(), coord).await;
485            neighbors.push((node_id, coord));
486        }
487
488        // Test greedy routing to a target
489        let target_coord = HyperbolicCoordinate { r: 0.9, theta: 1.0 };
490        let next_hop = space.greedy_route(&target_coord).await;
491
492        // Should find a neighbor closer to target
493        assert!(next_hop.is_some());
494
495        // Verify it chose the closest neighbor
496        if let Some(chosen) = next_hop {
497            let neighbors_map = space.neighbor_coordinates.read().await;
498            let chosen_coord = neighbors_map.get(&chosen).unwrap();
499            let chosen_dist = HyperbolicSpace::distance(chosen_coord, &target_coord);
500
501            // Check that no other neighbor is closer
502            for (id, coord) in &neighbors {
503                if id != &chosen {
504                    let dist = HyperbolicSpace::distance(coord, &target_coord);
505                    assert!(dist >= chosen_dist);
506                }
507            }
508        }
509    }
510}