Skip to main content

peat_mesh/hierarchy/
dynamic_strategy.rs

1//! Dynamic hierarchy strategy with capability-based election
2//!
3//! This strategy dynamically assigns roles based on node capabilities,
4//! resources, and proximity. Suitable for ad-hoc networks without
5//! pre-defined organizational structure.
6
7use super::{HierarchyStrategy, NodeRole};
8use crate::beacon::{GeographicBeacon, HierarchyLevel, NodeMobility, NodeProfile};
9
10/// Election configuration weights
11#[derive(Debug, Clone)]
12pub struct ElectionWeights {
13    /// Weight for mobility preference (static nodes preferred)
14    pub mobility: f64,
15
16    /// Weight for resource availability
17    pub resources: f64,
18
19    /// Weight for battery level
20    pub battery: f64,
21}
22
23impl Default for ElectionWeights {
24    fn default() -> Self {
25        Self {
26            mobility: 0.4,
27            resources: 0.4,
28            battery: 0.2,
29        }
30    }
31}
32
33/// Election configuration for dynamic role assignment
34#[derive(Debug, Clone)]
35pub struct ElectionConfig {
36    /// Weights for leadership score calculation
37    pub priority_weights: ElectionWeights,
38
39    /// Hysteresis factor to prevent role flapping (0.0-1.0)
40    /// New candidate must score this much better to trigger role change
41    pub hysteresis: f64,
42}
43
44impl Default for ElectionConfig {
45    fn default() -> Self {
46        Self {
47            priority_weights: ElectionWeights::default(),
48            hysteresis: 0.1, // 10% better required to change roles
49        }
50    }
51}
52
53/// Dynamic hierarchy strategy
54///
55/// Dynamically assigns roles based on node capabilities and resources.
56/// Nodes with better capabilities (static, high resources, good battery)
57/// are preferred for leadership roles.
58///
59/// # Use Cases
60///
61/// - Ad-hoc disaster response networks
62/// - Mesh network extensions
63/// - Testing dynamic topology formation
64///
65/// # Example
66///
67/// ```ignore
68/// let strategy = DynamicHierarchyStrategy {
69///     base_level: HierarchyLevel::Squad,
70///     election_config: ElectionConfig::default(),
71///     allow_level_transitions: false,
72/// };
73/// ```
74#[derive(Debug, Clone)]
75pub struct DynamicHierarchyStrategy {
76    /// Base hierarchy level (can be elevated if no higher-level peers found)
77    pub base_level: HierarchyLevel,
78
79    /// Election configuration for role determination
80    pub election_config: ElectionConfig,
81
82    /// Whether to allow automatic level transitions
83    pub allow_level_transitions: bool,
84}
85
86impl DynamicHierarchyStrategy {
87    /// Create a new dynamic hierarchy strategy
88    pub fn new(
89        base_level: HierarchyLevel,
90        election_config: ElectionConfig,
91        allow_level_transitions: bool,
92    ) -> Self {
93        Self {
94            base_level,
95            election_config,
96            allow_level_transitions,
97        }
98    }
99
100    /// Calculate leadership score for a node profile
101    ///
102    /// Higher score = more suitable for leadership
103    fn calculate_leadership_score(&self, profile: &NodeProfile) -> f64 {
104        let weights = &self.election_config.priority_weights;
105        let mut score = 0.0;
106
107        // Mobility score: Static > SemiMobile > Mobile
108        let mobility_score = match profile.mobility {
109            NodeMobility::Static => 1.0,
110            NodeMobility::SemiMobile => 0.6,
111            NodeMobility::Mobile => 0.3,
112        };
113        score += mobility_score * weights.mobility;
114
115        // Resource score: Lower utilization is better
116        let cpu_score = 1.0 - (profile.resources.cpu_usage_percent as f64 / 100.0);
117        let mem_score = 1.0 - (profile.resources.memory_usage_percent as f64 / 100.0);
118        let resource_score = (cpu_score + mem_score) / 2.0;
119        score += resource_score * weights.resources;
120
121        // Battery score: Higher battery is better (AC powered = 1.0)
122        let battery_score = profile
123            .resources
124            .battery_percent
125            .map(|b| b as f64 / 100.0)
126            .unwrap_or(1.0);
127        score += battery_score * weights.battery;
128
129        // Boost score if node explicitly configured for parenting
130        if profile.can_parent {
131            score *= 1.1;
132        }
133
134        // Apply parent priority multiplier (0-255 range)
135        score *= 1.0 + (profile.parent_priority as f64 / 255.0);
136
137        score
138    }
139
140    /// Calculate leadership score from a beacon
141    fn calculate_leadership_score_from_beacon(&self, beacon: &GeographicBeacon) -> f64 {
142        let weights = &self.election_config.priority_weights;
143        let mut score = 0.0;
144
145        // Mobility score
146        if let Some(mobility) = beacon.mobility {
147            let mobility_score = match mobility {
148                NodeMobility::Static => 1.0,
149                NodeMobility::SemiMobile => 0.6,
150                NodeMobility::Mobile => 0.3,
151            };
152            score += mobility_score * weights.mobility;
153        } else {
154            score += 0.5 * weights.mobility; // Default if not specified
155        }
156
157        // Resource score
158        if let Some(ref resources) = beacon.resources {
159            let cpu_score = 1.0 - (resources.cpu_usage_percent as f64 / 100.0);
160            let mem_score = 1.0 - (resources.memory_usage_percent as f64 / 100.0);
161            let resource_score = (cpu_score + mem_score) / 2.0;
162            score += resource_score * weights.resources;
163
164            let battery_score = resources
165                .battery_percent
166                .map(|b| b as f64 / 100.0)
167                .unwrap_or(1.0);
168            score += battery_score * weights.battery;
169        } else {
170            score += 0.5 * (weights.resources + weights.battery); // Default if not specified
171        }
172
173        // Apply can_parent and priority
174        if beacon.can_parent {
175            score *= 1.1;
176        }
177        score *= 1.0 + (beacon.parent_priority as f64 / 255.0);
178
179        score
180    }
181}
182
183impl HierarchyStrategy for DynamicHierarchyStrategy {
184    fn determine_level(&self, _node_profile: &NodeProfile) -> HierarchyLevel {
185        // For now, return base level
186        // Future: Could promote to higher level if no higher-level peers found
187        self.base_level
188    }
189
190    fn determine_role(
191        &self,
192        node_profile: &NodeProfile,
193        nearby_peers: &[GeographicBeacon],
194    ) -> NodeRole {
195        // Filter to same-level peers
196        let same_level_peers: Vec<&GeographicBeacon> = nearby_peers
197            .iter()
198            .filter(|b| b.hierarchy_level == self.base_level)
199            .collect();
200
201        if same_level_peers.is_empty() {
202            // No peers at same level, standalone mode
203            return NodeRole::Standalone;
204        }
205
206        // Calculate own leadership score
207        let my_score = self.calculate_leadership_score(node_profile);
208
209        // Calculate best peer score
210        let best_peer_score = same_level_peers
211            .iter()
212            .map(|p| self.calculate_leadership_score_from_beacon(p))
213            .max_by(|a, b| a.partial_cmp(b).unwrap())
214            .unwrap_or(0.0);
215
216        // Apply hysteresis: we must be significantly better to become leader
217        let threshold = best_peer_score * (1.0 + self.election_config.hysteresis);
218
219        if my_score >= threshold {
220            NodeRole::Leader
221        } else {
222            NodeRole::Member
223        }
224    }
225
226    fn can_transition(&self, _current_level: HierarchyLevel, _new_level: HierarchyLevel) -> bool {
227        // Allow transitions if configured
228        self.allow_level_transitions
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235    use crate::beacon::{GeoPosition, NodeResources};
236
237    fn create_high_capability_profile() -> NodeProfile {
238        NodeProfile {
239            mobility: NodeMobility::Static,
240            resources: NodeResources {
241                cpu_cores: 8,
242                memory_mb: 8192,
243                bandwidth_mbps: 1000,
244                cpu_usage_percent: 20,
245                memory_usage_percent: 30,
246                battery_percent: None, // AC powered
247            },
248            can_parent: true,
249            prefer_leaf: false,
250            parent_priority: 200,
251        }
252    }
253
254    fn create_low_capability_profile() -> NodeProfile {
255        NodeProfile {
256            mobility: NodeMobility::Mobile,
257            resources: NodeResources {
258                cpu_cores: 2,
259                memory_mb: 1024,
260                bandwidth_mbps: 50,
261                cpu_usage_percent: 70,
262                memory_usage_percent: 80,
263                battery_percent: Some(30),
264            },
265            can_parent: false,
266            prefer_leaf: true,
267            parent_priority: 50,
268        }
269    }
270
271    #[test]
272    fn test_leadership_score_prefers_high_capability() {
273        let strategy =
274            DynamicHierarchyStrategy::new(HierarchyLevel::Squad, ElectionConfig::default(), false);
275
276        let high_cap = create_high_capability_profile();
277        let low_cap = create_low_capability_profile();
278
279        let high_score = strategy.calculate_leadership_score(&high_cap);
280        let low_score = strategy.calculate_leadership_score(&low_cap);
281
282        assert!(high_score > low_score);
283    }
284
285    #[test]
286    fn test_role_determination_standalone_when_no_peers() {
287        let strategy =
288            DynamicHierarchyStrategy::new(HierarchyLevel::Squad, ElectionConfig::default(), false);
289
290        let profile = create_high_capability_profile();
291        let role = strategy.determine_role(&profile, &[]);
292
293        assert_eq!(role, NodeRole::Standalone);
294    }
295
296    #[test]
297    fn test_role_determination_leader_with_high_capability() {
298        let strategy =
299            DynamicHierarchyStrategy::new(HierarchyLevel::Squad, ElectionConfig::default(), false);
300
301        let high_cap = create_high_capability_profile();
302
303        // Create a lower-capability peer beacon
304        let mut low_cap_beacon = GeographicBeacon::new(
305            "peer1".to_string(),
306            GeoPosition::new(37.7750, -122.4195),
307            HierarchyLevel::Squad,
308        );
309        low_cap_beacon.mobility = Some(NodeMobility::Mobile);
310        low_cap_beacon.resources = Some(NodeResources {
311            cpu_cores: 2,
312            memory_mb: 1024,
313            bandwidth_mbps: 50,
314            cpu_usage_percent: 70,
315            memory_usage_percent: 80,
316            battery_percent: Some(30),
317        });
318        low_cap_beacon.can_parent = false;
319        low_cap_beacon.parent_priority = 50;
320
321        let role = strategy.determine_role(&high_cap, &[low_cap_beacon]);
322
323        assert_eq!(role, NodeRole::Leader);
324    }
325
326    #[test]
327    fn test_level_transitions_allowed_when_configured() {
328        let strategy = DynamicHierarchyStrategy::new(
329            HierarchyLevel::Squad,
330            ElectionConfig::default(),
331            true, // Allow transitions
332        );
333
334        assert!(strategy.can_transition(HierarchyLevel::Squad, HierarchyLevel::Platoon));
335    }
336
337    #[test]
338    fn test_level_transitions_disabled_when_configured() {
339        let strategy = DynamicHierarchyStrategy::new(
340            HierarchyLevel::Squad,
341            ElectionConfig::default(),
342            false, // Disallow transitions
343        );
344
345        assert!(!strategy.can_transition(HierarchyLevel::Squad, HierarchyLevel::Platoon));
346    }
347}