1use crate::beacon::{GeoPosition, GeographicBeacon, HierarchyLevel, NodeMobility};
7
8#[derive(Debug, Clone)]
10pub struct PeerCandidate {
11 pub beacon: GeographicBeacon,
12 pub score: f64,
13}
14
15#[derive(Debug, Clone)]
17pub struct SelectionConfig {
18 pub mobility_weight: f64,
20 pub resource_weight: f64,
22 pub battery_weight: f64,
24 pub proximity_weight: f64,
26 pub max_distance_meters: Option<f64>,
28 pub max_children_per_parent: Option<usize>,
30}
31
32impl Default for SelectionConfig {
33 fn default() -> Self {
34 Self {
35 mobility_weight: 0.3,
36 resource_weight: 0.3,
37 battery_weight: 0.2,
38 proximity_weight: 0.2,
39 max_distance_meters: Some(10_000.0), max_children_per_parent: Some(10), }
42 }
43}
44
45impl SelectionConfig {
46 pub fn tactical() -> Self {
48 Self {
49 mobility_weight: 0.4,
50 resource_weight: 0.25,
51 battery_weight: 0.15,
52 proximity_weight: 0.2,
53 max_distance_meters: Some(2_000.0), max_children_per_parent: Some(5),
55 }
56 }
57
58 pub fn distributed() -> Self {
60 Self {
61 mobility_weight: 0.2,
62 resource_weight: 0.4,
63 battery_weight: 0.2,
64 proximity_weight: 0.2,
65 max_distance_meters: None, max_children_per_parent: Some(15),
67 }
68 }
69}
70
71pub struct PeerSelector {
73 config: SelectionConfig,
74 own_position: GeoPosition,
75 own_level: HierarchyLevel,
76}
77
78impl PeerSelector {
79 pub fn new(
81 config: SelectionConfig,
82 own_position: GeoPosition,
83 own_level: HierarchyLevel,
84 ) -> Self {
85 Self {
86 config,
87 own_position,
88 own_level,
89 }
90 }
91
92 pub fn select_peer(&self, candidates: &[GeographicBeacon]) -> Option<PeerCandidate> {
96 let mut scored: Vec<PeerCandidate> = candidates
97 .iter()
98 .filter(|beacon| self.is_valid_peer(beacon))
99 .map(|beacon| PeerCandidate {
100 beacon: beacon.clone(),
101 score: self.score_candidate(beacon),
102 })
103 .collect();
104
105 scored.sort_by(|a, b| {
107 b.score
108 .partial_cmp(&a.score)
109 .unwrap_or(std::cmp::Ordering::Equal)
110 });
111
112 scored.into_iter().next()
113 }
114
115 fn is_valid_peer(&self, beacon: &GeographicBeacon) -> bool {
117 if !beacon.hierarchy_level.can_be_parent_of(&self.own_level) {
119 return false;
120 }
121
122 if let Some(max_dist) = self.config.max_distance_meters {
124 let distance = self.own_position.distance_to(&beacon.position);
125 if distance > max_dist {
126 return false;
127 }
128 }
129
130 if !beacon.can_parent {
132 return false;
133 }
134
135 true
136 }
137
138 fn score_candidate(&self, beacon: &GeographicBeacon) -> f64 {
140 let mut score = 0.0;
141
142 if let Some(mobility) = beacon.mobility {
144 score += self.mobility_score(&mobility) * self.config.mobility_weight;
145 } else {
146 score += 0.5 * self.config.mobility_weight;
148 }
149
150 if let Some(ref resources) = beacon.resources {
152 score += self.resource_score(resources) * self.config.resource_weight;
153 score += self.battery_score(resources) * self.config.battery_weight;
154 } else {
155 score += 0.5 * self.config.resource_weight;
157 score += 0.5 * self.config.battery_weight;
158 }
159
160 score += self.proximity_score(beacon) * self.config.proximity_weight;
162
163 score
164 }
165
166 fn mobility_score(&self, mobility: &NodeMobility) -> f64 {
169 match mobility {
170 NodeMobility::Static => 1.0,
171 NodeMobility::SemiMobile => 0.6,
172 NodeMobility::Mobile => 0.3,
173 }
174 }
175
176 fn resource_score(&self, resources: &crate::beacon::NodeResources) -> f64 {
178 let cpu_score = 1.0 - (resources.cpu_usage_percent as f64 / 100.0);
180
181 let mem_score = 1.0 - (resources.memory_usage_percent as f64 / 100.0);
183
184 let bandwidth_score = (resources.bandwidth_mbps as f64).min(100.0) / 100.0;
186
187 (cpu_score + mem_score + bandwidth_score) / 3.0
189 }
190
191 fn battery_score(&self, resources: &crate::beacon::NodeResources) -> f64 {
193 if let Some(battery) = resources.battery_percent {
194 battery as f64 / 100.0
195 } else {
196 1.0 }
198 }
199
200 fn proximity_score(&self, beacon: &GeographicBeacon) -> f64 {
203 let distance = self.own_position.distance_to(&beacon.position);
204
205 let scale = self
208 .config
209 .max_distance_meters
210 .unwrap_or(10_000.0)
211 .max(1000.0)
212 / 3.0;
213
214 (-distance / scale).exp()
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn test_select_best_peer_prefers_static_nodes() {
224 let selector = PeerSelector::new(
225 SelectionConfig::default(),
226 GeoPosition::new(37.7749, -122.4194),
227 HierarchyLevel::Squad,
228 );
229
230 let static_peer = create_test_beacon(
231 "static",
232 GeoPosition::new(37.7750, -122.4195),
233 HierarchyLevel::Platoon,
234 NodeMobility::Static,
235 50, );
237
238 let mobile_peer = create_test_beacon(
239 "mobile",
240 GeoPosition::new(37.7751, -122.4196),
241 HierarchyLevel::Platoon,
242 NodeMobility::Mobile,
243 30, );
245
246 let result = selector.select_peer(&[static_peer, mobile_peer]);
247
248 assert!(result.is_some());
249 let winner = result.unwrap();
250 assert_eq!(winner.beacon.node_id, "static");
251 }
252
253 #[test]
254 fn test_select_peer_respects_hierarchy() {
255 let selector = PeerSelector::new(
256 SelectionConfig::default(),
257 GeoPosition::new(37.7749, -122.4194),
258 HierarchyLevel::Squad,
259 );
260
261 let valid_peer = create_test_beacon(
263 "valid",
264 GeoPosition::new(37.7750, -122.4195),
265 HierarchyLevel::Platoon,
266 NodeMobility::Static,
267 50,
268 );
269
270 let invalid_peer = create_test_beacon(
272 "invalid",
273 GeoPosition::new(37.7751, -122.4196),
274 HierarchyLevel::Platform,
275 NodeMobility::Static,
276 30,
277 );
278
279 let result = selector.select_peer(&[valid_peer.clone(), invalid_peer]);
280
281 assert!(result.is_some());
282 let winner = result.unwrap();
283 assert_eq!(winner.beacon.node_id, "valid");
284 }
285
286 #[test]
287 fn test_select_peer_prefers_closer_nodes() {
288 let selector = PeerSelector::new(
289 SelectionConfig {
290 proximity_weight: 0.9, mobility_weight: 0.1,
292 resource_weight: 0.0,
293 battery_weight: 0.0,
294 ..Default::default()
295 },
296 GeoPosition::new(37.7749, -122.4194),
297 HierarchyLevel::Squad,
298 );
299
300 let nearby = create_test_beacon(
301 "nearby",
302 GeoPosition::new(37.7750, -122.4195), HierarchyLevel::Platoon,
304 NodeMobility::Mobile,
305 70,
306 );
307
308 let far = create_test_beacon(
309 "far",
310 GeoPosition::new(37.8000, -122.4400), HierarchyLevel::Platoon,
312 NodeMobility::Static,
313 30,
314 );
315
316 let result = selector.select_peer(&[nearby, far]);
317
318 assert!(result.is_some());
319 let winner = result.unwrap();
320 assert_eq!(winner.beacon.node_id, "nearby");
321 }
322
323 #[test]
324 fn test_select_peer_respects_distance_limit() {
325 let selector = PeerSelector::new(
326 SelectionConfig {
327 max_distance_meters: Some(1_000.0), ..Default::default()
329 },
330 GeoPosition::new(37.7749, -122.4194),
331 HierarchyLevel::Squad,
332 );
333
334 let too_far = create_test_beacon(
335 "far",
336 GeoPosition::new(37.8000, -122.4400), HierarchyLevel::Platoon,
338 NodeMobility::Static,
339 30,
340 );
341
342 let result = selector.select_peer(&[too_far]);
343 assert!(result.is_none());
344 }
345
346 #[test]
347 fn test_select_peer_prefers_better_resources() {
348 let selector = PeerSelector::new(
349 SelectionConfig {
350 resource_weight: 0.9, mobility_weight: 0.1,
352 proximity_weight: 0.0,
353 battery_weight: 0.0,
354 ..Default::default()
355 },
356 GeoPosition::new(37.7749, -122.4194),
357 HierarchyLevel::Squad,
358 );
359
360 let low_resources = create_test_beacon(
361 "low",
362 GeoPosition::new(37.7750, -122.4195),
363 HierarchyLevel::Platoon,
364 NodeMobility::Static,
365 90, );
367
368 let high_resources = create_test_beacon(
369 "high",
370 GeoPosition::new(37.7751, -122.4196),
371 HierarchyLevel::Platoon,
372 NodeMobility::Static,
373 20, );
375
376 let result = selector.select_peer(&[low_resources, high_resources]);
377
378 assert!(result.is_some());
379 let winner = result.unwrap();
380 assert_eq!(winner.beacon.node_id, "high");
381 }
382
383 fn create_test_beacon(
384 node_id: &str,
385 position: GeoPosition,
386 level: HierarchyLevel,
387 mobility: NodeMobility,
388 resource_usage: u8,
389 ) -> GeographicBeacon {
390 let resources = crate::beacon::NodeResources {
391 cpu_cores: 4,
392 memory_mb: 8192,
393 bandwidth_mbps: 100,
394 cpu_usage_percent: resource_usage,
395 memory_usage_percent: resource_usage,
396 battery_percent: Some(80),
397 };
398
399 let mut beacon = GeographicBeacon::new(node_id.to_string(), position, level);
400 beacon.mobility = Some(mobility);
401 beacon.resources = Some(resources);
402 beacon.can_parent = true;
403 beacon.parent_priority = 100;
404 beacon
405 }
406
407 #[test]
408 fn test_tactical_config() {
409 let config = SelectionConfig::tactical();
410 assert_eq!(config.mobility_weight, 0.4);
411 assert_eq!(config.resource_weight, 0.25);
412 assert_eq!(config.battery_weight, 0.15);
413 assert_eq!(config.proximity_weight, 0.2);
414 assert_eq!(config.max_distance_meters, Some(2_000.0));
415 assert_eq!(config.max_children_per_parent, Some(5));
416 }
417
418 #[test]
419 fn test_distributed_config() {
420 let config = SelectionConfig::distributed();
421 assert_eq!(config.mobility_weight, 0.2);
422 assert_eq!(config.resource_weight, 0.4);
423 assert_eq!(config.battery_weight, 0.2);
424 assert_eq!(config.proximity_weight, 0.2);
425 assert!(config.max_distance_meters.is_none());
426 assert_eq!(config.max_children_per_parent, Some(15));
427 }
428
429 #[test]
430 fn test_empty_candidates_returns_none() {
431 let selector = PeerSelector::new(
432 SelectionConfig::default(),
433 GeoPosition::new(37.7749, -122.4194),
434 HierarchyLevel::Squad,
435 );
436 assert!(selector.select_peer(&[]).is_none());
437 }
438
439 #[test]
440 fn test_can_parent_false_filtered() {
441 let selector = PeerSelector::new(
442 SelectionConfig::default(),
443 GeoPosition::new(37.7749, -122.4194),
444 HierarchyLevel::Squad,
445 );
446
447 let mut beacon = create_test_beacon(
448 "no-parent",
449 GeoPosition::new(37.7750, -122.4195),
450 HierarchyLevel::Platoon,
451 NodeMobility::Static,
452 30,
453 );
454 beacon.can_parent = false;
455
456 assert!(selector.select_peer(&[beacon]).is_none());
457 }
458
459 #[test]
460 fn test_no_mobility_beacon_scoring() {
461 let selector = PeerSelector::new(
462 SelectionConfig::default(),
463 GeoPosition::new(37.7749, -122.4194),
464 HierarchyLevel::Squad,
465 );
466
467 let mut beacon = create_test_beacon(
468 "no-mob",
469 GeoPosition::new(37.7750, -122.4195),
470 HierarchyLevel::Platoon,
471 NodeMobility::Static,
472 30,
473 );
474 beacon.mobility = None;
475
476 let result = selector.select_peer(&[beacon]);
477 assert!(result.is_some());
478 assert!(result.unwrap().score > 0.0);
480 }
481
482 #[test]
483 fn test_no_resources_beacon_scoring() {
484 let selector = PeerSelector::new(
485 SelectionConfig::default(),
486 GeoPosition::new(37.7749, -122.4194),
487 HierarchyLevel::Squad,
488 );
489
490 let mut beacon = GeographicBeacon::new(
491 "no-res".to_string(),
492 GeoPosition::new(37.7750, -122.4195),
493 HierarchyLevel::Platoon,
494 );
495 beacon.mobility = Some(NodeMobility::Static);
496 beacon.resources = None;
497 beacon.can_parent = true;
498
499 let result = selector.select_peer(&[beacon]);
500 assert!(result.is_some());
501 assert!(result.unwrap().score > 0.0);
502 }
503
504 #[test]
505 fn test_semi_mobile_scoring() {
506 let selector = PeerSelector::new(
507 SelectionConfig {
508 mobility_weight: 1.0,
509 resource_weight: 0.0,
510 battery_weight: 0.0,
511 proximity_weight: 0.0,
512 ..Default::default()
513 },
514 GeoPosition::new(37.7749, -122.4194),
515 HierarchyLevel::Squad,
516 );
517
518 let semi = create_test_beacon(
519 "semi",
520 GeoPosition::new(37.7750, -122.4195),
521 HierarchyLevel::Platoon,
522 NodeMobility::SemiMobile,
523 50,
524 );
525
526 let result = selector.select_peer(&[semi]).unwrap();
527 assert!((result.score - 0.6).abs() < 0.01);
529 }
530
531 #[test]
532 fn test_unlimited_distance_config() {
533 let selector = PeerSelector::new(
534 SelectionConfig {
535 max_distance_meters: None,
536 ..Default::default()
537 },
538 GeoPosition::new(37.7749, -122.4194),
539 HierarchyLevel::Squad,
540 );
541
542 let far = create_test_beacon(
544 "far",
545 GeoPosition::new(40.7128, -74.0060), HierarchyLevel::Platoon,
547 NodeMobility::Static,
548 30,
549 );
550
551 assert!(selector.select_peer(&[far]).is_some());
552 }
553
554 #[test]
555 fn test_battery_none_ac_powered() {
556 let selector = PeerSelector::new(
557 SelectionConfig {
558 battery_weight: 1.0,
559 mobility_weight: 0.0,
560 resource_weight: 0.0,
561 proximity_weight: 0.0,
562 ..Default::default()
563 },
564 GeoPosition::new(37.7749, -122.4194),
565 HierarchyLevel::Squad,
566 );
567
568 let mut beacon = create_test_beacon(
569 "ac",
570 GeoPosition::new(37.7750, -122.4195),
571 HierarchyLevel::Platoon,
572 NodeMobility::Static,
573 50,
574 );
575 beacon.resources.as_mut().unwrap().battery_percent = None;
577
578 let result = selector.select_peer(&[beacon]).unwrap();
579 assert!((result.score - 1.0).abs() < 0.01);
581 }
582
583 #[test]
584 fn test_default_config() {
585 let config = SelectionConfig::default();
586 assert_eq!(config.mobility_weight, 0.3);
587 assert_eq!(config.resource_weight, 0.3);
588 assert_eq!(config.battery_weight, 0.2);
589 assert_eq!(config.proximity_weight, 0.2);
590 assert_eq!(config.max_distance_meters, Some(10_000.0));
591 assert_eq!(config.max_children_per_parent, Some(10));
592 }
593
594 #[test]
595 fn test_config_debug_clone() {
596 let config = SelectionConfig::default();
597 let cloned = config.clone();
598 assert_eq!(cloned.mobility_weight, config.mobility_weight);
599 let _ = format!("{:?}", config);
600 }
601
602 #[test]
603 fn test_peer_candidate_debug_clone() {
604 let beacon = create_test_beacon(
605 "test",
606 GeoPosition::new(37.7749, -122.4194),
607 HierarchyLevel::Platoon,
608 NodeMobility::Static,
609 50,
610 );
611 let candidate = PeerCandidate {
612 beacon,
613 score: 0.75,
614 };
615 let cloned = candidate.clone();
616 assert_eq!(cloned.score, 0.75);
617 let _ = format!("{:?}", candidate);
618 }
619}