saorsa_core/placement/
algorithms.rs

1// Copyright (c) 2025 Saorsa Labs Limited
2//
3// This file is part of the Saorsa P2P network.
4//
5// Licensed under the AGPL-3.0 license:
6// <https://www.gnu.org/licenses/agpl-3.0.html>
7
8//! Placement algorithms for optimal shard distribution
9//!
10//! Implements weighted selection algorithms with diversity enforcement
11//! to ensure optimal placement of erasure-coded shards across the network.
12
13use std::collections::{HashMap, HashSet};
14//use std::net::SocketAddr;
15use std::time::{Duration, Instant};
16
17use async_trait::async_trait;
18
19use crate::adaptive::{NodeId, performance::PerformanceMonitor, trust::EigenTrustEngine};
20use crate::placement::{
21    GeographicLocation, NetworkRegion, PlacementConfig, PlacementDecision, PlacementError,
22    PlacementResult, PlacementStrategy,
23};
24//use crate::placement::traits::NodePerformanceMetrics;
25
26/// Efraimidis-Spirakis weighted sampling algorithm implementation
27#[derive(Debug, Clone)]
28pub struct WeightedSampler {
29    /// Random number generator state
30    #[allow(dead_code)]
31    rng_state: u64,
32    /// Cached weights for performance
33    #[allow(dead_code)]
34    weight_cache: HashMap<NodeId, f64>,
35    /// Last update timestamp for cache invalidation
36    #[allow(dead_code)]
37    cache_updated: Instant,
38    /// Cache TTL
39    #[allow(dead_code)]
40    cache_ttl: Duration,
41}
42
43impl Default for WeightedSampler {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49impl WeightedSampler {
50    /// Create new weighted sampler
51    pub fn new() -> Self {
52        Self {
53            rng_state: fastrand::u64(..),
54            weight_cache: HashMap::new(),
55            cache_updated: Instant::now(),
56            cache_ttl: Duration::from_secs(300), // 5 minutes
57        }
58    }
59
60    /// Calculate composite weight for a node
61    /// Formula: w_i = (τ_i^α) * (p_i^β) * (c_i^γ) * d_i
62    /// Where:
63    /// - τ_i: EigenTrust score
64    /// - p_i: Predicted stability (churn resistance)
65    /// - c_i: Capacity factor
66    /// - d_i: Diversity factor
67    pub fn calculate_weight(
68        &self,
69        node_id: &NodeId,
70        trust_score: f64,
71        stability_score: f64,
72        capacity_factor: f64,
73        diversity_factor: f64,
74        alpha: f64,
75        beta: f64,
76        gamma: f64,
77    ) -> PlacementResult<f64> {
78        // Validate inputs
79        if !(0.0..=1.0).contains(&trust_score) {
80            return Err(PlacementError::InvalidWeight {
81                node_id: node_id.clone(),
82                weight: trust_score,
83                reason: "Trust score must be between 0.0 and 1.0".to_string(),
84            });
85        }
86
87        if !(0.0..=1.0).contains(&stability_score) {
88            return Err(PlacementError::InvalidWeight {
89                node_id: node_id.clone(),
90                weight: stability_score,
91                reason: "Stability score must be between 0.0 and 1.0".to_string(),
92            });
93        }
94
95        if capacity_factor < 0.0 {
96            return Err(PlacementError::InvalidWeight {
97                node_id: node_id.clone(),
98                weight: capacity_factor,
99                reason: "Capacity factor must be non-negative".to_string(),
100            });
101        }
102
103        if diversity_factor < 0.0 {
104            return Err(PlacementError::InvalidWeight {
105                node_id: node_id.clone(),
106                weight: diversity_factor,
107                reason: "Diversity factor must be non-negative".to_string(),
108            });
109        }
110
111        // Calculate composite weight with numerical stability
112        let trust_component = if alpha == 0.0 {
113            1.0
114        } else {
115            trust_score.powf(alpha)
116        };
117        let stability_component = if beta == 0.0 {
118            1.0
119        } else {
120            stability_score.powf(beta)
121        };
122        let capacity_component = if gamma == 0.0 {
123            1.0
124        } else {
125            capacity_factor.powf(gamma)
126        };
127
128        let weight = trust_component * stability_component * capacity_component * diversity_factor;
129
130        // Ensure weight is finite and positive
131        if !weight.is_finite() || weight <= 0.0 {
132            return Err(PlacementError::InvalidWeight {
133                node_id: node_id.clone(),
134                weight,
135                reason: "Computed weight is not finite or positive".to_string(),
136            });
137        }
138
139        Ok(weight)
140    }
141
142    /// Sample k nodes using Efraimidis-Spirakis algorithm
143    pub fn sample_nodes(
144        &mut self,
145        candidates: &[(NodeId, f64)],
146        k: usize,
147    ) -> PlacementResult<Vec<NodeId>> {
148        if candidates.is_empty() {
149            return Err(PlacementError::InsufficientNodes {
150                required: k,
151                available: 0,
152            });
153        }
154
155        if k > candidates.len() {
156            return Err(PlacementError::InsufficientNodes {
157                required: k,
158                available: candidates.len(),
159            });
160        }
161
162        if k == 0 {
163            return Ok(Vec::new());
164        }
165
166        // Generate weighted random keys for each candidate
167        let mut weighted_keys: Vec<(f64, NodeId)> = candidates
168            .iter()
169            .map(|(node_id, weight)| {
170                if *weight <= 0.0 {
171                    return Err(PlacementError::InvalidWeight {
172                        node_id: node_id.clone(),
173                        weight: *weight,
174                        reason: "Weight must be positive".to_string(),
175                    });
176                }
177
178                // Generate uniform random value
179                let u = fastrand::f64();
180
181                // Calculate weighted key: k_i = u^(1/w_i)
182                let key = u.powf(1.0 / weight);
183
184                Ok((key, node_id.clone()))
185            })
186            .collect::<PlacementResult<Vec<_>>>()?;
187
188        // Sort by key in descending order and take top k
189        weighted_keys.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
190
191        Ok(weighted_keys
192            .into_iter()
193            .take(k)
194            .map(|(_, node_id)| node_id)
195            .collect())
196    }
197}
198
199/// Diversity enforcement for geographic and network distribution
200#[derive(Debug, Clone)]
201pub struct DiversityEnforcer {
202    /// Minimum distance between selected nodes (in km)
203    min_geographic_distance: f64,
204    /// Maximum nodes per region
205    max_nodes_per_region: usize,
206    /// Maximum nodes per ASN
207    max_nodes_per_asn: usize,
208    /// Penalty factor for diversity violations
209    diversity_penalty: f64,
210}
211
212impl Default for DiversityEnforcer {
213    fn default() -> Self {
214        Self::new()
215    }
216}
217
218impl DiversityEnforcer {
219    /// Create new diversity enforcer with default parameters
220    pub fn new() -> Self {
221        Self {
222            min_geographic_distance: 100.0, // 100km minimum
223            max_nodes_per_region: 2,
224            max_nodes_per_asn: 3,
225            diversity_penalty: 0.5, // 50% penalty
226        }
227    }
228
229    /// Calculate diversity factor for a node given existing selections
230    pub fn calculate_diversity_factor(
231        &self,
232        _candidate: &NodeId,
233        candidate_location: &GeographicLocation,
234        candidate_asn: u32,
235        candidate_region: &NetworkRegion,
236        selected_nodes: &[(NodeId, GeographicLocation, u32, NetworkRegion)],
237    ) -> f64 {
238        let mut diversity_factor = 1.0;
239
240        // Geographic diversity check
241        for (_, location, _, _) in selected_nodes {
242            let distance = candidate_location.distance_km(location);
243            if distance < self.min_geographic_distance {
244                diversity_factor *= self.diversity_penalty;
245            }
246        }
247
248        // Region diversity check
249        let region_count = selected_nodes
250            .iter()
251            .filter(|(_, _, _, region)| region == candidate_region)
252            .count();
253
254        if region_count >= self.max_nodes_per_region {
255            diversity_factor *= self.diversity_penalty;
256        }
257
258        // ASN diversity check
259        let asn_count = selected_nodes
260            .iter()
261            .filter(|(_, _, asn, _)| *asn == candidate_asn)
262            .count();
263
264        if asn_count >= self.max_nodes_per_asn {
265            diversity_factor *= self.diversity_penalty;
266        }
267
268        diversity_factor.max(0.1) // Minimum 10% factor
269    }
270
271    /// Validate diversity constraints for final selection
272    pub fn validate_selection(
273        &self,
274        selection: &[(NodeId, GeographicLocation, u32, NetworkRegion)],
275    ) -> PlacementResult<()> {
276        // Check geographic diversity
277        for (i, (node_a, loc_a, _, _)) in selection.iter().enumerate() {
278            for (j, (node_b, loc_b, _, _)) in selection.iter().enumerate() {
279                if i != j {
280                    let distance = loc_a.distance_km(loc_b);
281                    if distance < self.min_geographic_distance / 2.0 {
282                        return Err(PlacementError::DiversityViolation {
283                            constraint: "geographic_distance".to_string(),
284                            nodes: vec![node_a.clone(), node_b.clone()],
285                            details: format!(
286                                "Distance {} km < minimum {} km",
287                                distance,
288                                self.min_geographic_distance / 2.0
289                            ),
290                        });
291                    }
292                }
293            }
294        }
295
296        // Check region diversity
297        let mut region_counts: HashMap<NetworkRegion, usize> = HashMap::new();
298        for (_, _, _, region) in selection {
299            *region_counts.entry(*region).or_insert(0) += 1;
300        }
301
302        for (region, count) in region_counts {
303            if count > self.max_nodes_per_region {
304                return Err(PlacementError::DiversityViolation {
305                    constraint: "region_distribution".to_string(),
306                    nodes: selection
307                        .iter()
308                        .filter(|(_, _, _, r)| *r == region)
309                        .map(|(node_id, _, _, _)| node_id.clone())
310                        .collect(),
311                    details: format!(
312                        "Region {:?} has {} nodes > maximum {}",
313                        region, count, self.max_nodes_per_region
314                    ),
315                });
316            }
317        }
318
319        // Check ASN diversity
320        let mut asn_counts: HashMap<u32, usize> = HashMap::new();
321        for (_, _, asn, _) in selection {
322            *asn_counts.entry(*asn).or_insert(0) += 1;
323        }
324
325        for (asn, count) in asn_counts {
326            if count > self.max_nodes_per_asn {
327                return Err(PlacementError::DiversityViolation {
328                    constraint: "asn_distribution".to_string(),
329                    nodes: selection
330                        .iter()
331                        .filter(|(_, _, a, _)| *a == asn)
332                        .map(|(node_id, _, _, _)| node_id.clone())
333                        .collect(),
334                    details: format!(
335                        "ASN {} has {} nodes > maximum {}",
336                        asn, count, self.max_nodes_per_asn
337                    ),
338                });
339            }
340        }
341
342        Ok(())
343    }
344}
345
346/// Main placement strategy implementation
347#[derive(Debug)]
348pub struct WeightedPlacementStrategy {
349    sampler: WeightedSampler,
350    diversity_enforcer: DiversityEnforcer,
351    config: PlacementConfig,
352}
353
354impl WeightedPlacementStrategy {
355    /// Create new weighted placement strategy
356    pub fn new(config: PlacementConfig) -> Self {
357        Self {
358            sampler: WeightedSampler::new(),
359            diversity_enforcer: DiversityEnforcer::new(),
360            config,
361        }
362    }
363
364    /// Calculate weights for all candidate nodes
365    async fn calculate_weights(
366        &self,
367        candidates: &HashSet<NodeId>,
368        _trust_system: &EigenTrustEngine,
369        _performance_monitor: &PerformanceMonitor,
370        node_metadata: &HashMap<NodeId, (GeographicLocation, u32, NetworkRegion)>,
371        selected_nodes: &[(NodeId, GeographicLocation, u32, NetworkRegion)],
372    ) -> PlacementResult<Vec<(NodeId, f64)>> {
373        let mut weights = Vec::new();
374
375        for node_id in candidates {
376            // Get trust score (for now, use a default implementation)
377            let trust_score = 0.8; // Mock trust score
378
379            // Get stability score (churn prediction)
380            let stability_score = 0.9; // Mock stability score
381
382            // Get capacity factor
383            let capacity_factor = 1.0; // Mock capacity factor
384
385            // Get node metadata for diversity calculation
386            let (location, asn, region) = node_metadata
387                .get(node_id)
388                .ok_or_else(|| PlacementError::NodeMetadataNotFound(node_id.clone()))?;
389
390            // Calculate diversity factor
391            let diversity_factor = self.diversity_enforcer.calculate_diversity_factor(
392                node_id,
393                location,
394                *asn,
395                region,
396                selected_nodes,
397            );
398
399            // Calculate composite weight
400            let weight = self.sampler.calculate_weight(
401                node_id,
402                trust_score,
403                stability_score,
404                capacity_factor,
405                diversity_factor,
406                self.config.optimization_weights.trust_weight,
407                self.config.optimization_weights.performance_weight,
408                self.config.optimization_weights.capacity_weight,
409            )?;
410
411            weights.push((node_id.clone(), weight));
412        }
413
414        // Sort by weight for better selection
415        weights.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
416
417        Ok(weights)
418    }
419}
420
421#[async_trait]
422impl PlacementStrategy for WeightedPlacementStrategy {
423    async fn select_nodes(
424        &mut self,
425        candidates: &HashSet<NodeId>,
426        replication_factor: u8,
427        trust_system: &EigenTrustEngine,
428        performance_monitor: &PerformanceMonitor,
429        node_metadata: &HashMap<NodeId, (GeographicLocation, u32, NetworkRegion)>,
430    ) -> PlacementResult<PlacementDecision> {
431        let start_time = Instant::now();
432        let k = replication_factor as usize;
433
434        if candidates.is_empty() {
435            return Err(PlacementError::InsufficientNodes {
436                required: k,
437                available: 0,
438            });
439        }
440
441        if k > candidates.len() {
442            return Err(PlacementError::InsufficientNodes {
443                required: k,
444                available: candidates.len(),
445            });
446        }
447
448        let mut selected_nodes = Vec::new();
449        let mut remaining_candidates = candidates.clone();
450
451        // Iterative selection with diversity enforcement
452        for round in 0..k {
453            if remaining_candidates.is_empty() {
454                return Err(PlacementError::InsufficientNodes {
455                    required: k - round,
456                    available: 0,
457                });
458            }
459
460            // Calculate weights for current candidates
461            let weights = self
462                .calculate_weights(
463                    &remaining_candidates,
464                    trust_system,
465                    performance_monitor,
466                    node_metadata,
467                    &selected_nodes,
468                )
469                .await?;
470
471            // Sample one node using weighted selection
472            let selected = self.sampler.sample_nodes(&weights, 1)?;
473            let selected_node = selected[0].clone();
474
475            // Add to selection with metadata
476            let (location, asn, region) = node_metadata
477                .get(&selected_node)
478                .ok_or_else(|| PlacementError::NodeMetadataNotFound(selected_node.clone()))?;
479
480            selected_nodes.push((selected_node.clone(), *location, *asn, *region));
481
482            // Remove from candidates
483            remaining_candidates.remove(&selected_node);
484        }
485
486        // Validate final selection
487        self.diversity_enforcer
488            .validate_selection(&selected_nodes)?;
489
490        let selection_time = start_time.elapsed();
491
492        // Create placement decision
493        let decision = PlacementDecision {
494            selected_nodes: selected_nodes
495                .into_iter()
496                .map(|(node_id, _, _, _)| node_id)
497                .collect(),
498            backup_nodes: Vec::new(), // Could add backup selection here
499            placement_strategy: "weighted_efraimidis_spirakis".to_string(),
500            diversity_score: 1.0, // Could calculate actual diversity metric
501            estimated_reliability: 0.95, // Could calculate based on node metrics
502            selection_time,
503            metadata: HashMap::new(),
504        };
505
506        Ok(decision)
507    }
508
509    fn name(&self) -> &str {
510        "WeightedPlacementStrategy"
511    }
512
513    fn supports_constraints(&self) -> bool {
514        true
515    }
516}
517
518#[cfg(test)]
519mod tests {
520    use super::*;
521    fn create_test_location(lat: f64, lon: f64) -> GeographicLocation {
522        GeographicLocation::new(lat, lon).unwrap()
523    }
524
525    #[test]
526    fn test_weight_calculation() {
527        let sampler = WeightedSampler::new();
528        let node_id = NodeId::from_bytes([1u8; 32]);
529
530        // Test normal case
531        let weight = sampler
532            .calculate_weight(
533                &node_id, 0.8, // trust
534                0.9, // stability
535                1.2, // capacity
536                1.0, // diversity
537                1.0, // alpha
538                1.0, // beta
539                1.0, // gamma
540            )
541            .unwrap();
542
543        assert!((weight - (0.8 * 0.9 * 1.2 * 1.0)).abs() < 1e-10);
544
545        // Test edge cases
546        assert!(
547            sampler
548                .calculate_weight(&node_id, -0.1, 0.5, 1.0, 1.0, 1.0, 1.0, 1.0)
549                .is_err()
550        );
551        assert!(
552            sampler
553                .calculate_weight(&node_id, 1.1, 0.5, 1.0, 1.0, 1.0, 1.0, 1.0)
554                .is_err()
555        );
556        assert!(
557            sampler
558                .calculate_weight(&node_id, 0.5, -0.1, 1.0, 1.0, 1.0, 1.0, 1.0)
559                .is_err()
560        );
561        assert!(
562            sampler
563                .calculate_weight(&node_id, 0.5, 0.5, -1.0, 1.0, 1.0, 1.0, 1.0)
564                .is_err()
565        );
566    }
567
568    #[test]
569    fn test_efraimidis_spirakis_sampling() {
570        let mut sampler = WeightedSampler::new();
571
572        let candidates = vec![
573            (NodeId::from_bytes([1u8; 32]), 0.8),
574            (NodeId::from_bytes([2u8; 32]), 0.6),
575            (NodeId::from_bytes([3u8; 32]), 0.4),
576            (NodeId::from_bytes([4u8; 32]), 0.2),
577        ];
578
579        // Test normal sampling
580        let selected = sampler.sample_nodes(&candidates, 2).unwrap();
581        assert_eq!(selected.len(), 2);
582        assert_ne!(selected[0], selected[1]);
583
584        // Test edge cases
585        assert!(sampler.sample_nodes(&candidates, 0).unwrap().is_empty());
586        assert!(sampler.sample_nodes(&candidates, 5).is_err());
587        assert!(sampler.sample_nodes(&[], 1).is_err());
588
589        // Test with zero weights
590        let bad_candidates = vec![
591            (NodeId::from_bytes([1u8; 32]), 0.0),
592            (NodeId::from_bytes([2u8; 32]), 0.5),
593        ];
594        assert!(sampler.sample_nodes(&bad_candidates, 1).is_err());
595    }
596
597    #[test]
598    fn test_diversity_factor_calculation() {
599        let enforcer = DiversityEnforcer::new();
600        let candidate_id = NodeId::from_bytes([1u8; 32]);
601        let candidate_location = create_test_location(40.7128, -74.0060); // NYC
602        let candidate_asn = 12345;
603        let candidate_region = NetworkRegion::NorthAmerica;
604
605        // Test with no existing selections
606        let factor = enforcer.calculate_diversity_factor(
607            &candidate_id,
608            &candidate_location,
609            candidate_asn,
610            &candidate_region,
611            &[],
612        );
613        assert_eq!(factor, 1.0);
614
615        // Test with nearby node
616        let nearby_location = create_test_location(40.7589, -73.9851); // Manhattan
617        let existing = vec![(
618            NodeId::from_bytes([2u8; 32]),
619            nearby_location,
620            54321,
621            NetworkRegion::NorthAmerica,
622        )];
623
624        let factor = enforcer.calculate_diversity_factor(
625            &candidate_id,
626            &candidate_location,
627            candidate_asn,
628            &candidate_region,
629            &existing,
630        );
631        assert!(factor < 1.0); // Should be penalized
632
633        // Test with same ASN
634        let far_location = create_test_location(34.0522, -118.2437); // LA
635        let existing_same_asn = vec![(
636            NodeId::from_bytes([3u8; 32]),
637            far_location,
638            candidate_asn, // Same ASN
639            NetworkRegion::NorthAmerica,
640        )];
641
642        let factor = enforcer.calculate_diversity_factor(
643            &candidate_id,
644            &candidate_location,
645            candidate_asn,
646            &candidate_region,
647            &existing_same_asn,
648        );
649        assert!(factor < 1.0); // Should be penalized
650    }
651
652    #[test]
653    fn test_diversity_validation() {
654        let enforcer = DiversityEnforcer::new();
655
656        // Test valid selection
657        let valid_selection = vec![
658            (
659                NodeId::from_bytes([1u8; 32]),
660                create_test_location(40.7128, -74.0060),
661                12345,
662                NetworkRegion::NorthAmerica,
663            ), // NYC
664            (
665                NodeId::from_bytes([2u8; 32]),
666                create_test_location(34.0522, -118.2437),
667                54321,
668                NetworkRegion::NorthAmerica,
669            ), // LA
670        ];
671        assert!(enforcer.validate_selection(&valid_selection).is_ok());
672
673        // Test too close selection
674        let too_close_selection = vec![
675            (
676                NodeId::from_bytes([1u8; 32]),
677                create_test_location(40.7128, -74.0060),
678                12345,
679                NetworkRegion::NorthAmerica,
680            ), // NYC
681            (
682                NodeId::from_bytes([2u8; 32]),
683                create_test_location(40.7589, -73.9851),
684                54321,
685                NetworkRegion::NorthAmerica,
686            ), // Manhattan
687        ];
688        assert!(enforcer.validate_selection(&too_close_selection).is_err());
689    }
690}