saorsa_core/adaptive/
hyperbolic_enhanced.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: david@saorsalabs.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//! Enhanced hyperbolic geometry routing with fixed-point arithmetic and visualization
15//!
16//! This module implements improvements to the hyperbolic routing layer:
17//! - Fixed-point arithmetic for improved precision
18//! - Hysteresis in coordinate adjustment to prevent oscillation
19//! - Visualization support for debugging
20//! - Enhanced distance calculations
21
22use super::*;
23use crate::adaptive::hyperbolic::{RoutingStats, angle_difference};
24use async_trait::async_trait;
25use serde::{Deserialize, Serialize};
26use std::collections::HashMap;
27use std::sync::Arc;
28use tokio::sync::RwLock;
29
30/// Fixed-point arithmetic precision (number of decimal places)
31const FIXED_POINT_SCALE: i64 = 1_000_000; // 6 decimal places
32
33/// Hysteresis parameters for coordinate adjustment
34const HYSTERESIS_THRESHOLD: f64 = 0.001;
35const HYSTERESIS_DAMPING: f64 = 0.8;
36
37/// Enhanced hyperbolic coordinate with fixed-point support
38#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
39pub struct EnhancedHyperbolicCoordinate {
40    /// Radial coordinate in fixed-point representation
41    r_fixed: i64,
42    /// Angular coordinate in fixed-point representation
43    theta_fixed: i64,
44}
45
46impl EnhancedHyperbolicCoordinate {
47    /// Create from floating point coordinates
48    pub fn from_float(r: f64, theta: f64) -> Self {
49        Self {
50            r_fixed: (r * FIXED_POINT_SCALE as f64) as i64,
51            theta_fixed: (theta * FIXED_POINT_SCALE as f64) as i64,
52        }
53    }
54
55    /// Get radial coordinate as float
56    pub fn r(&self) -> f64 {
57        self.r_fixed as f64 / FIXED_POINT_SCALE as f64
58    }
59
60    /// Get angular coordinate as float
61    pub fn theta(&self) -> f64 {
62        self.theta_fixed as f64 / FIXED_POINT_SCALE as f64
63    }
64
65    /// Set radial coordinate from float
66    pub fn set_r(&mut self, r: f64) {
67        self.r_fixed = (r * FIXED_POINT_SCALE as f64) as i64;
68    }
69
70    /// Set angular coordinate from float
71    pub fn set_theta(&mut self, theta: f64) {
72        self.theta_fixed = (theta * FIXED_POINT_SCALE as f64) as i64;
73    }
74}
75
76/// Enhanced hyperbolic space with visualization support
77pub struct EnhancedHyperbolicSpace {
78    /// Our node's current coordinate
79    my_coordinate: RwLock<EnhancedHyperbolicCoordinate>,
80
81    /// Previous coordinate for hysteresis
82    previous_coordinate: RwLock<Option<EnhancedHyperbolicCoordinate>>,
83
84    /// Neighbor coordinates
85    neighbor_coordinates: Arc<RwLock<HashMap<NodeId, EnhancedHyperbolicCoordinate>>>,
86
87    /// Coordinate adjustment parameters
88    adjustment_params: AdjustmentParameters,
89
90    /// Routing statistics
91    _routing_stats: Arc<RwLock<RoutingStats>>,
92
93    /// Visualization data
94    visualization_data: Arc<RwLock<VisualizationData>>,
95}
96
97/// Parameters for coordinate adjustment with hysteresis
98#[derive(Debug, Clone)]
99pub struct AdjustmentParameters {
100    /// Base adjustment rate
101    base_rate: f64,
102    /// Current adjustment rate (with hysteresis)
103    current_rate: f64,
104    /// Minimum adjustment rate
105    min_rate: f64,
106    /// Maximum adjustment rate
107    max_rate: f64,
108    /// Hysteresis factor
109    hysteresis_factor: f64,
110}
111
112impl Default for AdjustmentParameters {
113    fn default() -> Self {
114        Self {
115            base_rate: 0.01,
116            current_rate: 0.01,
117            min_rate: 0.001,
118            max_rate: 0.1,
119            hysteresis_factor: HYSTERESIS_DAMPING,
120        }
121    }
122}
123
124/// Data structure for visualization
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct VisualizationData {
127    /// Node positions for visualization
128    pub nodes: HashMap<NodeId, VisualizationNode>,
129    /// Routing paths for visualization
130    pub paths: Vec<RoutingPath>,
131    /// Network metrics
132    pub metrics: NetworkMetrics,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct VisualizationNode {
137    pub id: NodeId,
138    pub coordinate: HyperbolicCoordinate,
139    pub label: String,
140    pub degree: usize,
141    pub trust_score: f64,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct RoutingPath {
146    pub source: NodeId,
147    pub target: NodeId,
148    pub hops: Vec<NodeId>,
149    pub success: bool,
150    pub total_distance: f64,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct NetworkMetrics {
155    pub total_nodes: usize,
156    pub average_degree: f64,
157    pub clustering_coefficient: f64,
158    pub average_path_length: f64,
159}
160
161impl Default for EnhancedHyperbolicSpace {
162    fn default() -> Self {
163        Self::new()
164    }
165}
166
167impl EnhancedHyperbolicSpace {
168    /// Create a new enhanced hyperbolic space instance
169    pub fn new() -> Self {
170        let initial_r = 0.5;
171        let initial_theta = rand::random::<f64>() * 2.0 * std::f64::consts::PI;
172
173        Self {
174            my_coordinate: RwLock::new(EnhancedHyperbolicCoordinate::from_float(
175                initial_r,
176                initial_theta,
177            )),
178            previous_coordinate: RwLock::new(None),
179            neighbor_coordinates: Arc::new(RwLock::new(HashMap::new())),
180            adjustment_params: AdjustmentParameters::default(),
181            _routing_stats: Arc::new(RwLock::new(RoutingStats::default())),
182            visualization_data: Arc::new(RwLock::new(VisualizationData {
183                nodes: HashMap::new(),
184                paths: Vec::new(),
185                metrics: NetworkMetrics {
186                    total_nodes: 0,
187                    average_degree: 0.0,
188                    clustering_coefficient: 0.0,
189                    average_path_length: 0.0,
190                },
191            })),
192        }
193    }
194
195    /// Calculate hyperbolic distance with improved precision
196    pub fn distance_fixed(
197        a: &EnhancedHyperbolicCoordinate,
198        b: &EnhancedHyperbolicCoordinate,
199    ) -> f64 {
200        let r1 = a.r();
201        let r2 = b.r();
202        let theta1 = a.theta();
203        let theta2 = b.theta();
204
205        // Use improved distance formula for Poincaré disk
206        let cos_angle_diff = (theta1 - theta2).cos();
207
208        // Handle edge cases near the boundary
209        if r1 > 0.999 || r2 > 0.999 {
210            return f64::INFINITY;
211        }
212
213        // Compute distance using more stable formula
214        let numerator = (r1 - r2).powi(2) + 4.0 * r1 * r2 * (1.0 - cos_angle_diff);
215        let denominator = (1.0 - r1.powi(2)) * (1.0 - r2.powi(2));
216
217        if denominator <= 0.0 {
218            return f64::INFINITY;
219        }
220
221        let cosh_dist = 1.0 + numerator / denominator;
222        cosh_dist.acosh()
223    }
224
225    /// Adjust coordinate with hysteresis to prevent oscillation
226    pub async fn adjust_coordinate_with_hysteresis(
227        &self,
228        neighbor_coords: &[(NodeId, EnhancedHyperbolicCoordinate)],
229    ) {
230        let mut my_coord = self.my_coordinate.write().await;
231        let mut prev_coord_guard = self.previous_coordinate.write().await;
232
233        // Calculate target position
234        let degree = neighbor_coords.len();
235        let target_r = 1.0 - (2.0 / (degree as f64 + 2.0));
236
237        // Calculate angular adjustment with circular mean
238        let (sin_sum, cos_sum) =
239            neighbor_coords
240                .iter()
241                .fold((0.0, 0.0), |(sin_acc, cos_acc), (_, coord)| {
242                    let theta = coord.theta();
243                    (sin_acc + theta.sin(), cos_acc + theta.cos())
244                });
245
246        let target_theta = sin_sum.atan2(cos_sum);
247
248        // Apply hysteresis based on previous movement
249        let mut params = self.adjustment_params.clone();
250
251        if let Some(prev_coord) = prev_coord_guard.as_ref() {
252            let prev_movement = Self::distance_fixed(&my_coord, prev_coord);
253
254            // Adjust rate based on movement magnitude
255            if prev_movement < HYSTERESIS_THRESHOLD {
256                // Small movement - increase damping
257                params.current_rate *= params.hysteresis_factor;
258            } else {
259                // Large movement - allow faster adjustment
260                params.current_rate = params.base_rate;
261            }
262
263            // Clamp rate to bounds
264            params.current_rate = params.current_rate.clamp(params.min_rate, params.max_rate);
265        }
266
267        // Store current position as previous
268        *prev_coord_guard = Some(*my_coord);
269
270        // Apply adjustments with hysteresis
271        let current_r = my_coord.r();
272        let current_theta = my_coord.theta();
273
274        let new_r = current_r + params.current_rate * (target_r - current_r);
275        let angle_diff = angle_difference(target_theta, current_theta);
276        let new_theta = current_theta + params.current_rate * angle_diff;
277
278        // Update coordinate with bounds checking
279        my_coord.set_r(new_r.clamp(0.0, 0.999));
280
281        // Normalize theta to [0, 2π)
282        let normalized_theta = if new_theta < 0.0 {
283            new_theta + 2.0 * std::f64::consts::PI
284        } else if new_theta >= 2.0 * std::f64::consts::PI {
285            new_theta - 2.0 * std::f64::consts::PI
286        } else {
287            new_theta
288        };
289
290        my_coord.set_theta(normalized_theta);
291    }
292
293    /// Update visualization data
294    pub async fn update_visualization(&self) {
295        let neighbors = self.neighbor_coordinates.read().await;
296        let mut viz_data = self.visualization_data.write().await;
297
298        // Clear old data
299        viz_data.nodes.clear();
300
301        // Add our node
302        let my_coord = self.my_coordinate.read().await;
303        let my_id = generate_local_node_id(); // Placeholder
304
305        viz_data.nodes.insert(
306            my_id.clone(),
307            VisualizationNode {
308                id: my_id.clone(),
309                coordinate: HyperbolicCoordinate {
310                    r: my_coord.r(),
311                    theta: my_coord.theta(),
312                },
313                label: "Local Node".to_string(),
314                degree: neighbors.len(),
315                trust_score: 1.0,
316            },
317        );
318
319        // Add neighbor nodes
320        for (node_id, coord) in neighbors.iter() {
321            viz_data.nodes.insert(
322                node_id.clone(),
323                VisualizationNode {
324                    id: node_id.clone(),
325                    coordinate: HyperbolicCoordinate {
326                        r: coord.r(),
327                        theta: coord.theta(),
328                    },
329                    label: format!("Node {:?}", node_id),
330                    degree: 0,        // Unknown for neighbors
331                    trust_score: 0.5, // Default
332                },
333            );
334        }
335
336        // Update metrics
337        viz_data.metrics.total_nodes = viz_data.nodes.len();
338        viz_data.metrics.average_degree = neighbors.len() as f64;
339    }
340
341    /// Export visualization data as JSON
342    pub async fn export_visualization_json(&self) -> Result<String> {
343        let viz_data = self.visualization_data.read().await;
344        // Convert keys to strings for JSON compatibility
345        use std::collections::BTreeMap;
346        let mut nodes_map: BTreeMap<String, VisualizationNode> = BTreeMap::new();
347        for (k, v) in viz_data.nodes.iter() {
348            nodes_map.insert(format!("{:?}", k), v.clone());
349        }
350        let export = serde_json::json!({
351            "nodes": nodes_map,
352            "metrics": viz_data.metrics,
353        });
354
355        serde_json::to_string_pretty(&export).map_err(|e| {
356            AdaptiveNetworkError::Other(format!("Failed to serialize visualization: {}", e))
357        })
358    }
359
360    /// Export visualization as SVG
361    pub async fn export_visualization_svg(&self) -> Result<String> {
362        let viz_data = self.visualization_data.read().await;
363        let mut svg = String::new();
364
365        // SVG header
366        svg.push_str(
367            r#"<svg width="800" height="800" xmlns="http://www.w3.org/2000/svg">
368            <circle cx="400" cy="400" r="380" fill="none" stroke="black" stroke-width="2"/>
369        "#,
370        );
371
372        // Draw nodes
373        for node in viz_data.nodes.values() {
374            let (x, y) = polar_to_cartesian(
375                node.coordinate.r,
376                node.coordinate.theta,
377                400.0,
378                400.0,
379                380.0,
380            );
381            svg.push_str(&format!(
382                r#"<circle cx="{}" cy="{}" r="5" fill="blue" stroke="black" stroke-width="1"/>"#,
383                x, y
384            ));
385        }
386
387        // Draw paths
388        for path in &viz_data.paths {
389            if let (Some(source), Some(target)) = (
390                viz_data.nodes.get(&path.source),
391                viz_data.nodes.get(&path.target),
392            ) {
393                let (x1, y1) = polar_to_cartesian(
394                    source.coordinate.r,
395                    source.coordinate.theta,
396                    400.0,
397                    400.0,
398                    380.0,
399                );
400                let (x2, y2) = polar_to_cartesian(
401                    target.coordinate.r,
402                    target.coordinate.theta,
403                    400.0,
404                    400.0,
405                    380.0,
406                );
407
408                let color = if path.success { "green" } else { "red" };
409                svg.push_str(&format!(
410                    r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{}" stroke-width="2" opacity="0.5"/>"#,
411                    x1, y1, x2, y2, color
412                ));
413            }
414        }
415
416        svg.push_str("</svg>");
417        Ok(svg)
418    }
419}
420
421/// Convert polar coordinates to Cartesian for visualization
422fn polar_to_cartesian(r: f64, theta: f64, cx: f64, cy: f64, radius: f64) -> (f64, f64) {
423    let x = cx + r * radius * theta.cos();
424    let y = cy + r * radius * theta.sin();
425    (x, y)
426}
427
428/// Generate a placeholder local node ID
429fn generate_local_node_id() -> NodeId {
430    use rand::RngCore;
431    let mut hash = [0u8; 32];
432    rand::thread_rng().fill_bytes(&mut hash);
433    NodeId::from_bytes(hash)
434}
435
436/// Enhanced routing strategy with visualization support
437pub struct EnhancedHyperbolicRoutingStrategy {
438    /// The enhanced hyperbolic space manager
439    space: Arc<EnhancedHyperbolicSpace>,
440
441    /// Local node ID
442    local_id: NodeId,
443
444    /// Maximum hops before declaring failure
445    max_hops: usize,
446
447    /// Enable visualization recording
448    enable_visualization: bool,
449}
450
451impl EnhancedHyperbolicRoutingStrategy {
452    /// Create a new enhanced routing strategy
453    pub fn new(local_id: NodeId, space: Arc<EnhancedHyperbolicSpace>) -> Self {
454        Self {
455            space,
456            local_id,
457            max_hops: 10,
458            enable_visualization: true,
459        }
460    }
461
462    /// Find path with visualization support
463    async fn find_enhanced_path(&self, target: &NodeId) -> Result<Vec<NodeId>> {
464        let target_coord = {
465            let neighbors = self.space.neighbor_coordinates.read().await;
466            neighbors.get(target).cloned()
467        };
468
469        let target_coord = match target_coord {
470            Some(coord) => coord,
471            None => {
472                return Err(AdaptiveNetworkError::Routing(
473                    "Target coordinate unknown".to_string(),
474                ));
475            }
476        };
477
478        let mut path = Vec::new();
479        let mut visited = std::collections::HashSet::<NodeId>::new();
480        visited.insert(self.local_id.clone());
481
482        let mut total_distance = 0.0;
483        let _start_time = std::time::Instant::now();
484
485        // Greedy routing with visualization
486        for hop in 0..self.max_hops {
487            let my_coord = self.space.my_coordinate.read().await;
488            let my_distance = EnhancedHyperbolicSpace::distance_fixed(&my_coord, &target_coord);
489
490            let neighbors = self.space.neighbor_coordinates.read().await;
491            let next_hop = neighbors
492                .iter()
493                .filter(|(id, _)| !visited.contains(id))
494                .filter(|(_, coord)| {
495                    EnhancedHyperbolicSpace::distance_fixed(coord, &target_coord) < my_distance
496                })
497                .min_by(|(_, a), (_, b)| {
498                    let dist_a = EnhancedHyperbolicSpace::distance_fixed(a, &target_coord);
499                    let dist_b = EnhancedHyperbolicSpace::distance_fixed(b, &target_coord);
500                    dist_a
501                        .partial_cmp(&dist_b)
502                        .unwrap_or(std::cmp::Ordering::Equal)
503                })
504                .map(|(id, coord)| (id.clone(), *coord));
505
506            match next_hop {
507                Some((next_id, next_coord)) => {
508                    if next_id == *target {
509                        // Reached target
510                        path.push(next_id);
511                        total_distance +=
512                            EnhancedHyperbolicSpace::distance_fixed(&my_coord, &next_coord);
513
514                        // Record successful path for visualization
515                        if self.enable_visualization {
516                            let mut viz_data = self.space.visualization_data.write().await;
517                            viz_data.paths.push(RoutingPath {
518                                source: self.local_id.clone(),
519                                target: target.clone(),
520                                hops: path.clone(),
521                                success: true,
522                                total_distance,
523                            });
524                        }
525
526                        return Ok(path);
527                    }
528
529                    path.push(next_id.clone());
530                    visited.insert(next_id);
531                    total_distance +=
532                        EnhancedHyperbolicSpace::distance_fixed(&my_coord, &next_coord);
533                }
534                None => {
535                    // No closer neighbor found
536                    if self.enable_visualization {
537                        let mut viz_data = self.space.visualization_data.write().await;
538                        viz_data.paths.push(RoutingPath {
539                            source: self.local_id.clone(),
540                            target: target.clone(),
541                            hops: path.clone(),
542                            success: false,
543                            total_distance,
544                        });
545                    }
546
547                    return Err(AdaptiveNetworkError::Routing(format!(
548                        "No closer neighbor found after {} hops",
549                        hop
550                    )));
551                }
552            }
553        }
554
555        // Max hops exceeded
556        Err(AdaptiveNetworkError::Routing(
557            "Maximum hop count exceeded".to_string(),
558        ))
559    }
560}
561
562#[async_trait]
563impl RoutingStrategy for EnhancedHyperbolicRoutingStrategy {
564    async fn find_path(&self, target: &NodeId) -> Result<Vec<NodeId>> {
565        self.find_enhanced_path(target).await
566    }
567
568    fn route_score(&self, _neighbor: &NodeId, _target: &NodeId) -> f64 {
569        0.5
570    }
571
572    fn update_metrics(&mut self, _path: &[NodeId], _success: bool) {
573        // Metrics are updated in find_enhanced_path
574    }
575}
576
577#[cfg(test)]
578mod tests {
579    use super::*;
580
581    #[test]
582    fn test_fixed_point_conversion() {
583        let coord = EnhancedHyperbolicCoordinate::from_float(0.5, std::f64::consts::PI);
584        assert!((coord.r() - 0.5).abs() < 1e-6);
585        assert!((coord.theta() - std::f64::consts::PI).abs() < 1e-6);
586    }
587
588    #[test]
589    fn test_enhanced_distance_calculation() {
590        let a = EnhancedHyperbolicCoordinate::from_float(0.0, 0.0);
591        let b = EnhancedHyperbolicCoordinate::from_float(0.5, std::f64::consts::PI);
592
593        let dist = EnhancedHyperbolicSpace::distance_fixed(&a, &b);
594        assert!(dist > 0.0);
595        assert!(dist.is_finite());
596
597        // Test boundary behavior
598        let boundary = EnhancedHyperbolicCoordinate::from_float(0.9999, 0.0);
599        let dist_boundary = EnhancedHyperbolicSpace::distance_fixed(&a, &boundary);
600        assert!(dist_boundary > 5.0); // Should be very large near boundary
601    }
602
603    #[tokio::test]
604    async fn test_hysteresis_adjustment() {
605        let space = EnhancedHyperbolicSpace::new();
606
607        // Create test neighbors
608        let neighbors = vec![
609            (
610                generate_local_node_id(),
611                EnhancedHyperbolicCoordinate::from_float(0.8, 0.0),
612            ),
613            (
614                generate_local_node_id(),
615                EnhancedHyperbolicCoordinate::from_float(0.8, std::f64::consts::PI),
616            ),
617        ];
618
619        // Perform multiple adjustments
620        let mut movements = vec![];
621        for _i in 0..10 {
622            let coord_before = *space.my_coordinate.read().await;
623            space.adjust_coordinate_with_hysteresis(&neighbors).await;
624            let coord_after = *space.my_coordinate.read().await;
625
626            let movement = EnhancedHyperbolicSpace::distance_fixed(&coord_before, &coord_after);
627            movements.push(movement);
628
629            // Small delay to simulate time passing
630            tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
631        }
632
633        // Movements should decrease over time due to hysteresis
634        for i in 1..movements.len() {
635            assert!(movements[i] <= movements[i - 1] * 1.1); // Allow small increase due to randomness
636        }
637    }
638
639    #[tokio::test]
640    async fn test_visualization_export() {
641        let space = Arc::new(EnhancedHyperbolicSpace::new());
642
643        // Add some test nodes
644        let mut neighbors = space.neighbor_coordinates.write().await;
645        for i in 0..5 {
646            let angle = (i as f64) * 2.0 * std::f64::consts::PI / 5.0;
647            neighbors.insert(
648                generate_local_node_id(),
649                EnhancedHyperbolicCoordinate::from_float(0.5, angle),
650            );
651        }
652        drop(neighbors);
653
654        // Update visualization
655        space.update_visualization().await;
656
657        // Export as JSON
658        let json = space.export_visualization_json().await.unwrap();
659        assert!(json.contains("nodes"));
660        assert!(json.contains("metrics"));
661
662        // Export as SVG
663        let svg = space.export_visualization_svg().await.unwrap();
664        assert!(svg.contains("<svg"));
665        assert!(svg.contains("<circle"));
666    }
667}