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: 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//! 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        serde_json::to_string_pretty(&*viz_data).map_err(|e| {
345            AdaptiveNetworkError::Other(format!("Failed to serialize visualization: {}", e))
346        })
347    }
348
349    /// Export visualization as SVG
350    pub async fn export_visualization_svg(&self) -> Result<String> {
351        let viz_data = self.visualization_data.read().await;
352        let mut svg = String::new();
353
354        // SVG header
355        svg.push_str(
356            r#"<svg width="800" height="800" xmlns="http://www.w3.org/2000/svg">
357            <circle cx="400" cy="400" r="380" fill="none" stroke="black" stroke-width="2"/>
358        "#,
359        );
360
361        // Draw nodes
362        for node in viz_data.nodes.values() {
363            let (x, y) = polar_to_cartesian(
364                node.coordinate.r,
365                node.coordinate.theta,
366                400.0,
367                400.0,
368                380.0,
369            );
370            svg.push_str(&format!(
371                r#"<circle cx="{}" cy="{}" r="5" fill="blue" stroke="black" stroke-width="1"/>"#,
372                x, y
373            ));
374        }
375
376        // Draw paths
377        for path in &viz_data.paths {
378            if let (Some(source), Some(target)) = (
379                viz_data.nodes.get(&path.source),
380                viz_data.nodes.get(&path.target),
381            ) {
382                let (x1, y1) = polar_to_cartesian(
383                    source.coordinate.r,
384                    source.coordinate.theta,
385                    400.0,
386                    400.0,
387                    380.0,
388                );
389                let (x2, y2) = polar_to_cartesian(
390                    target.coordinate.r,
391                    target.coordinate.theta,
392                    400.0,
393                    400.0,
394                    380.0,
395                );
396
397                let color = if path.success { "green" } else { "red" };
398                svg.push_str(&format!(
399                    r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{}" stroke-width="2" opacity="0.5"/>"#,
400                    x1, y1, x2, y2, color
401                ));
402            }
403        }
404
405        svg.push_str("</svg>");
406        Ok(svg)
407    }
408}
409
410/// Convert polar coordinates to Cartesian for visualization
411fn polar_to_cartesian(r: f64, theta: f64, cx: f64, cy: f64, radius: f64) -> (f64, f64) {
412    let x = cx + r * radius * theta.cos();
413    let y = cy + r * radius * theta.sin();
414    (x, y)
415}
416
417/// Generate a placeholder local node ID
418fn generate_local_node_id() -> NodeId {
419    use rand::RngCore;
420    let mut hash = [0u8; 32];
421    rand::thread_rng().fill_bytes(&mut hash);
422    NodeId::from_bytes(hash)
423}
424
425/// Enhanced routing strategy with visualization support
426pub struct EnhancedHyperbolicRoutingStrategy {
427    /// The enhanced hyperbolic space manager
428    space: Arc<EnhancedHyperbolicSpace>,
429
430    /// Local node ID
431    local_id: NodeId,
432
433    /// Maximum hops before declaring failure
434    max_hops: usize,
435
436    /// Enable visualization recording
437    enable_visualization: bool,
438}
439
440impl EnhancedHyperbolicRoutingStrategy {
441    /// Create a new enhanced routing strategy
442    pub fn new(local_id: NodeId, space: Arc<EnhancedHyperbolicSpace>) -> Self {
443        Self {
444            space,
445            local_id,
446            max_hops: 10,
447            enable_visualization: true,
448        }
449    }
450
451    /// Find path with visualization support
452    async fn find_enhanced_path(&self, target: &NodeId) -> Result<Vec<NodeId>> {
453        let target_coord = {
454            let neighbors = self.space.neighbor_coordinates.read().await;
455            neighbors.get(target).cloned()
456        };
457
458        let target_coord = match target_coord {
459            Some(coord) => coord,
460            None => {
461                return Err(AdaptiveNetworkError::Routing(
462                    "Target coordinate unknown".to_string(),
463                ));
464            }
465        };
466
467        let mut path = Vec::new();
468        let mut visited = std::collections::HashSet::<NodeId>::new();
469        visited.insert(self.local_id.clone());
470
471        let mut total_distance = 0.0;
472        let _start_time = std::time::Instant::now();
473
474        // Greedy routing with visualization
475        for hop in 0..self.max_hops {
476            let my_coord = self.space.my_coordinate.read().await;
477            let my_distance = EnhancedHyperbolicSpace::distance_fixed(&my_coord, &target_coord);
478
479            let neighbors = self.space.neighbor_coordinates.read().await;
480            let next_hop = neighbors
481                .iter()
482                .filter(|(id, _)| !visited.contains(id))
483                .filter(|(_, coord)| {
484                    EnhancedHyperbolicSpace::distance_fixed(coord, &target_coord) < my_distance
485                })
486                .min_by(|(_, a), (_, b)| {
487                    let dist_a = EnhancedHyperbolicSpace::distance_fixed(a, &target_coord);
488                    let dist_b = EnhancedHyperbolicSpace::distance_fixed(b, &target_coord);
489                    dist_a
490                        .partial_cmp(&dist_b)
491                        .unwrap_or(std::cmp::Ordering::Equal)
492                })
493                .map(|(id, coord)| (id.clone(), *coord));
494
495            match next_hop {
496                Some((next_id, next_coord)) => {
497                    if next_id == *target {
498                        // Reached target
499                        path.push(next_id);
500                        total_distance +=
501                            EnhancedHyperbolicSpace::distance_fixed(&my_coord, &next_coord);
502
503                        // Record successful path for visualization
504                        if self.enable_visualization {
505                            let mut viz_data = self.space.visualization_data.write().await;
506                            viz_data.paths.push(RoutingPath {
507                                source: self.local_id.clone(),
508                                target: target.clone(),
509                                hops: path.clone(),
510                                success: true,
511                                total_distance,
512                            });
513                        }
514
515                        return Ok(path);
516                    }
517
518                    path.push(next_id.clone());
519                    visited.insert(next_id);
520                    total_distance +=
521                        EnhancedHyperbolicSpace::distance_fixed(&my_coord, &next_coord);
522                }
523                None => {
524                    // No closer neighbor found
525                    if self.enable_visualization {
526                        let mut viz_data = self.space.visualization_data.write().await;
527                        viz_data.paths.push(RoutingPath {
528                            source: self.local_id.clone(),
529                            target: target.clone(),
530                            hops: path.clone(),
531                            success: false,
532                            total_distance,
533                        });
534                    }
535
536                    return Err(AdaptiveNetworkError::Routing(format!(
537                        "No closer neighbor found after {} hops",
538                        hop
539                    )));
540                }
541            }
542        }
543
544        // Max hops exceeded
545        Err(AdaptiveNetworkError::Routing(
546            "Maximum hop count exceeded".to_string(),
547        ))
548    }
549}
550
551#[async_trait]
552impl RoutingStrategy for EnhancedHyperbolicRoutingStrategy {
553    async fn find_path(&self, target: &NodeId) -> Result<Vec<NodeId>> {
554        self.find_enhanced_path(target).await
555    }
556
557    fn route_score(&self, _neighbor: &NodeId, _target: &NodeId) -> f64 {
558        0.5
559    }
560
561    fn update_metrics(&mut self, _path: &[NodeId], _success: bool) {
562        // Metrics are updated in find_enhanced_path
563    }
564}
565
566#[cfg(test)]
567mod tests {
568    use super::*;
569
570    #[test]
571    fn test_fixed_point_conversion() {
572        let coord = EnhancedHyperbolicCoordinate::from_float(0.5, std::f64::consts::PI);
573        assert!((coord.r() - 0.5).abs() < 1e-6);
574        assert!((coord.theta() - std::f64::consts::PI).abs() < 1e-6);
575    }
576
577    #[test]
578    fn test_enhanced_distance_calculation() {
579        let a = EnhancedHyperbolicCoordinate::from_float(0.0, 0.0);
580        let b = EnhancedHyperbolicCoordinate::from_float(0.5, std::f64::consts::PI);
581
582        let dist = EnhancedHyperbolicSpace::distance_fixed(&a, &b);
583        assert!(dist > 0.0);
584        assert!(dist.is_finite());
585
586        // Test boundary behavior
587        let boundary = EnhancedHyperbolicCoordinate::from_float(0.9999, 0.0);
588        let dist_boundary = EnhancedHyperbolicSpace::distance_fixed(&a, &boundary);
589        assert!(dist_boundary > 5.0); // Should be very large near boundary
590    }
591
592    #[tokio::test]
593    async fn test_hysteresis_adjustment() {
594        let space = EnhancedHyperbolicSpace::new();
595
596        // Create test neighbors
597        let neighbors = vec![
598            (
599                generate_local_node_id(),
600                EnhancedHyperbolicCoordinate::from_float(0.8, 0.0),
601            ),
602            (
603                generate_local_node_id(),
604                EnhancedHyperbolicCoordinate::from_float(0.8, std::f64::consts::PI),
605            ),
606        ];
607
608        // Perform multiple adjustments
609        let mut movements = vec![];
610        for _i in 0..10 {
611            let coord_before = space.my_coordinate.read().await.clone();
612            space.adjust_coordinate_with_hysteresis(&neighbors).await;
613            let coord_after = space.my_coordinate.read().await.clone();
614
615            let movement = EnhancedHyperbolicSpace::distance_fixed(&coord_before, &coord_after);
616            movements.push(movement);
617
618            // Small delay to simulate time passing
619            tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
620        }
621
622        // Movements should decrease over time due to hysteresis
623        for i in 1..movements.len() {
624            assert!(movements[i] <= movements[i - 1] * 1.1); // Allow small increase due to randomness
625        }
626    }
627
628    #[tokio::test]
629    async fn test_visualization_export() {
630        let space = Arc::new(EnhancedHyperbolicSpace::new());
631
632        // Add some test nodes
633        let mut neighbors = space.neighbor_coordinates.write().await;
634        for i in 0..5 {
635            let angle = (i as f64) * 2.0 * std::f64::consts::PI / 5.0;
636            neighbors.insert(
637                generate_local_node_id(),
638                EnhancedHyperbolicCoordinate::from_float(0.5, angle),
639            );
640        }
641        drop(neighbors);
642
643        // Update visualization
644        space.update_visualization().await;
645
646        // Export as JSON
647        let json = space.export_visualization_json().await.unwrap();
648        assert!(json.contains("nodes"));
649        assert!(json.contains("metrics"));
650
651        // Export as SVG
652        let svg = space.export_visualization_svg().await.unwrap();
653        assert!(svg.contains("<svg"));
654        assert!(svg.contains("<circle"));
655    }
656}