use crate::beacon::{GeoPosition, GeographicBeacon, HierarchyLevel, NodeMobility};
#[derive(Debug, Clone)]
pub struct PeerCandidate {
pub beacon: GeographicBeacon,
pub score: f64,
}
#[derive(Debug, Clone)]
pub struct SelectionConfig {
pub mobility_weight: f64,
pub resource_weight: f64,
pub battery_weight: f64,
pub proximity_weight: f64,
pub max_distance_meters: Option<f64>,
pub max_children_per_parent: Option<usize>,
}
impl Default for SelectionConfig {
fn default() -> Self {
Self {
mobility_weight: 0.3,
resource_weight: 0.3,
battery_weight: 0.2,
proximity_weight: 0.2,
max_distance_meters: Some(10_000.0), max_children_per_parent: Some(10), }
}
}
impl SelectionConfig {
pub fn tactical() -> Self {
Self {
mobility_weight: 0.4,
resource_weight: 0.25,
battery_weight: 0.15,
proximity_weight: 0.2,
max_distance_meters: Some(2_000.0), max_children_per_parent: Some(5),
}
}
pub fn distributed() -> Self {
Self {
mobility_weight: 0.2,
resource_weight: 0.4,
battery_weight: 0.2,
proximity_weight: 0.2,
max_distance_meters: None, max_children_per_parent: Some(15),
}
}
}
pub struct PeerSelector {
config: SelectionConfig,
own_position: GeoPosition,
own_level: HierarchyLevel,
}
impl PeerSelector {
pub fn new(
config: SelectionConfig,
own_position: GeoPosition,
own_level: HierarchyLevel,
) -> Self {
Self {
config,
own_position,
own_level,
}
}
pub fn select_peer(&self, candidates: &[GeographicBeacon]) -> Option<PeerCandidate> {
let mut scored: Vec<PeerCandidate> = candidates
.iter()
.filter(|beacon| self.is_valid_peer(beacon))
.map(|beacon| PeerCandidate {
beacon: beacon.clone(),
score: self.score_candidate(beacon),
})
.collect();
scored.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
scored.into_iter().next()
}
fn is_valid_peer(&self, beacon: &GeographicBeacon) -> bool {
if !beacon.hierarchy_level.can_be_parent_of(&self.own_level) {
return false;
}
if let Some(max_dist) = self.config.max_distance_meters {
let distance = self.own_position.distance_to(&beacon.position);
if distance > max_dist {
return false;
}
}
if !beacon.can_parent {
return false;
}
true
}
fn score_candidate(&self, beacon: &GeographicBeacon) -> f64 {
let mut score = 0.0;
if let Some(mobility) = beacon.mobility {
score += self.mobility_score(&mobility) * self.config.mobility_weight;
} else {
score += 0.5 * self.config.mobility_weight;
}
if let Some(ref resources) = beacon.resources {
score += self.resource_score(resources) * self.config.resource_weight;
score += self.battery_score(resources) * self.config.battery_weight;
} else {
score += 0.5 * self.config.resource_weight;
score += 0.5 * self.config.battery_weight;
}
score += self.proximity_score(beacon) * self.config.proximity_weight;
score
}
fn mobility_score(&self, mobility: &NodeMobility) -> f64 {
match mobility {
NodeMobility::Static => 1.0,
NodeMobility::SemiMobile => 0.6,
NodeMobility::Mobile => 0.3,
}
}
fn resource_score(&self, resources: &crate::beacon::NodeResources) -> f64 {
let cpu_score = 1.0 - (resources.cpu_usage_percent as f64 / 100.0);
let mem_score = 1.0 - (resources.memory_usage_percent as f64 / 100.0);
let bandwidth_score = (resources.bandwidth_mbps as f64).min(100.0) / 100.0;
(cpu_score + mem_score + bandwidth_score) / 3.0
}
fn battery_score(&self, resources: &crate::beacon::NodeResources) -> f64 {
if let Some(battery) = resources.battery_percent {
battery as f64 / 100.0
} else {
1.0 }
}
fn proximity_score(&self, beacon: &GeographicBeacon) -> f64 {
let distance = self.own_position.distance_to(&beacon.position);
let scale = self
.config
.max_distance_meters
.unwrap_or(10_000.0)
.max(1000.0)
/ 3.0;
(-distance / scale).exp()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_select_best_peer_prefers_static_nodes() {
let selector = PeerSelector::new(
SelectionConfig::default(),
GeoPosition::new(37.7749, -122.4194),
HierarchyLevel::Squad,
);
let static_peer = create_test_beacon(
"static",
GeoPosition::new(37.7750, -122.4195),
HierarchyLevel::Platoon,
NodeMobility::Static,
50, );
let mobile_peer = create_test_beacon(
"mobile",
GeoPosition::new(37.7751, -122.4196),
HierarchyLevel::Platoon,
NodeMobility::Mobile,
30, );
let result = selector.select_peer(&[static_peer, mobile_peer]);
assert!(result.is_some());
let winner = result.unwrap();
assert_eq!(winner.beacon.node_id, "static");
}
#[test]
fn test_select_peer_respects_hierarchy() {
let selector = PeerSelector::new(
SelectionConfig::default(),
GeoPosition::new(37.7749, -122.4194),
HierarchyLevel::Squad,
);
let valid_peer = create_test_beacon(
"valid",
GeoPosition::new(37.7750, -122.4195),
HierarchyLevel::Platoon,
NodeMobility::Static,
50,
);
let invalid_peer = create_test_beacon(
"invalid",
GeoPosition::new(37.7751, -122.4196),
HierarchyLevel::Platform,
NodeMobility::Static,
30,
);
let result = selector.select_peer(&[valid_peer.clone(), invalid_peer]);
assert!(result.is_some());
let winner = result.unwrap();
assert_eq!(winner.beacon.node_id, "valid");
}
#[test]
fn test_select_peer_prefers_closer_nodes() {
let selector = PeerSelector::new(
SelectionConfig {
proximity_weight: 0.9, mobility_weight: 0.1,
resource_weight: 0.0,
battery_weight: 0.0,
..Default::default()
},
GeoPosition::new(37.7749, -122.4194),
HierarchyLevel::Squad,
);
let nearby = create_test_beacon(
"nearby",
GeoPosition::new(37.7750, -122.4195), HierarchyLevel::Platoon,
NodeMobility::Mobile,
70,
);
let far = create_test_beacon(
"far",
GeoPosition::new(37.8000, -122.4400), HierarchyLevel::Platoon,
NodeMobility::Static,
30,
);
let result = selector.select_peer(&[nearby, far]);
assert!(result.is_some());
let winner = result.unwrap();
assert_eq!(winner.beacon.node_id, "nearby");
}
#[test]
fn test_select_peer_respects_distance_limit() {
let selector = PeerSelector::new(
SelectionConfig {
max_distance_meters: Some(1_000.0), ..Default::default()
},
GeoPosition::new(37.7749, -122.4194),
HierarchyLevel::Squad,
);
let too_far = create_test_beacon(
"far",
GeoPosition::new(37.8000, -122.4400), HierarchyLevel::Platoon,
NodeMobility::Static,
30,
);
let result = selector.select_peer(&[too_far]);
assert!(result.is_none());
}
#[test]
fn test_select_peer_prefers_better_resources() {
let selector = PeerSelector::new(
SelectionConfig {
resource_weight: 0.9, mobility_weight: 0.1,
proximity_weight: 0.0,
battery_weight: 0.0,
..Default::default()
},
GeoPosition::new(37.7749, -122.4194),
HierarchyLevel::Squad,
);
let low_resources = create_test_beacon(
"low",
GeoPosition::new(37.7750, -122.4195),
HierarchyLevel::Platoon,
NodeMobility::Static,
90, );
let high_resources = create_test_beacon(
"high",
GeoPosition::new(37.7751, -122.4196),
HierarchyLevel::Platoon,
NodeMobility::Static,
20, );
let result = selector.select_peer(&[low_resources, high_resources]);
assert!(result.is_some());
let winner = result.unwrap();
assert_eq!(winner.beacon.node_id, "high");
}
fn create_test_beacon(
node_id: &str,
position: GeoPosition,
level: HierarchyLevel,
mobility: NodeMobility,
resource_usage: u8,
) -> GeographicBeacon {
let resources = crate::beacon::NodeResources {
cpu_cores: 4,
memory_mb: 8192,
bandwidth_mbps: 100,
cpu_usage_percent: resource_usage,
memory_usage_percent: resource_usage,
battery_percent: Some(80),
};
let mut beacon = GeographicBeacon::new(node_id.to_string(), position, level);
beacon.mobility = Some(mobility);
beacon.resources = Some(resources);
beacon.can_parent = true;
beacon.parent_priority = 100;
beacon
}
#[test]
fn test_tactical_config() {
let config = SelectionConfig::tactical();
assert_eq!(config.mobility_weight, 0.4);
assert_eq!(config.resource_weight, 0.25);
assert_eq!(config.battery_weight, 0.15);
assert_eq!(config.proximity_weight, 0.2);
assert_eq!(config.max_distance_meters, Some(2_000.0));
assert_eq!(config.max_children_per_parent, Some(5));
}
#[test]
fn test_distributed_config() {
let config = SelectionConfig::distributed();
assert_eq!(config.mobility_weight, 0.2);
assert_eq!(config.resource_weight, 0.4);
assert_eq!(config.battery_weight, 0.2);
assert_eq!(config.proximity_weight, 0.2);
assert!(config.max_distance_meters.is_none());
assert_eq!(config.max_children_per_parent, Some(15));
}
#[test]
fn test_empty_candidates_returns_none() {
let selector = PeerSelector::new(
SelectionConfig::default(),
GeoPosition::new(37.7749, -122.4194),
HierarchyLevel::Squad,
);
assert!(selector.select_peer(&[]).is_none());
}
#[test]
fn test_can_parent_false_filtered() {
let selector = PeerSelector::new(
SelectionConfig::default(),
GeoPosition::new(37.7749, -122.4194),
HierarchyLevel::Squad,
);
let mut beacon = create_test_beacon(
"no-parent",
GeoPosition::new(37.7750, -122.4195),
HierarchyLevel::Platoon,
NodeMobility::Static,
30,
);
beacon.can_parent = false;
assert!(selector.select_peer(&[beacon]).is_none());
}
#[test]
fn test_no_mobility_beacon_scoring() {
let selector = PeerSelector::new(
SelectionConfig::default(),
GeoPosition::new(37.7749, -122.4194),
HierarchyLevel::Squad,
);
let mut beacon = create_test_beacon(
"no-mob",
GeoPosition::new(37.7750, -122.4195),
HierarchyLevel::Platoon,
NodeMobility::Static,
30,
);
beacon.mobility = None;
let result = selector.select_peer(&[beacon]);
assert!(result.is_some());
assert!(result.unwrap().score > 0.0);
}
#[test]
fn test_no_resources_beacon_scoring() {
let selector = PeerSelector::new(
SelectionConfig::default(),
GeoPosition::new(37.7749, -122.4194),
HierarchyLevel::Squad,
);
let mut beacon = GeographicBeacon::new(
"no-res".to_string(),
GeoPosition::new(37.7750, -122.4195),
HierarchyLevel::Platoon,
);
beacon.mobility = Some(NodeMobility::Static);
beacon.resources = None;
beacon.can_parent = true;
let result = selector.select_peer(&[beacon]);
assert!(result.is_some());
assert!(result.unwrap().score > 0.0);
}
#[test]
fn test_semi_mobile_scoring() {
let selector = PeerSelector::new(
SelectionConfig {
mobility_weight: 1.0,
resource_weight: 0.0,
battery_weight: 0.0,
proximity_weight: 0.0,
..Default::default()
},
GeoPosition::new(37.7749, -122.4194),
HierarchyLevel::Squad,
);
let semi = create_test_beacon(
"semi",
GeoPosition::new(37.7750, -122.4195),
HierarchyLevel::Platoon,
NodeMobility::SemiMobile,
50,
);
let result = selector.select_peer(&[semi]).unwrap();
assert!((result.score - 0.6).abs() < 0.01);
}
#[test]
fn test_unlimited_distance_config() {
let selector = PeerSelector::new(
SelectionConfig {
max_distance_meters: None,
..Default::default()
},
GeoPosition::new(37.7749, -122.4194),
HierarchyLevel::Squad,
);
let far = create_test_beacon(
"far",
GeoPosition::new(40.7128, -74.0060), HierarchyLevel::Platoon,
NodeMobility::Static,
30,
);
assert!(selector.select_peer(&[far]).is_some());
}
#[test]
fn test_battery_none_ac_powered() {
let selector = PeerSelector::new(
SelectionConfig {
battery_weight: 1.0,
mobility_weight: 0.0,
resource_weight: 0.0,
proximity_weight: 0.0,
..Default::default()
},
GeoPosition::new(37.7749, -122.4194),
HierarchyLevel::Squad,
);
let mut beacon = create_test_beacon(
"ac",
GeoPosition::new(37.7750, -122.4195),
HierarchyLevel::Platoon,
NodeMobility::Static,
50,
);
beacon.resources.as_mut().unwrap().battery_percent = None;
let result = selector.select_peer(&[beacon]).unwrap();
assert!((result.score - 1.0).abs() < 0.01);
}
#[test]
fn test_default_config() {
let config = SelectionConfig::default();
assert_eq!(config.mobility_weight, 0.3);
assert_eq!(config.resource_weight, 0.3);
assert_eq!(config.battery_weight, 0.2);
assert_eq!(config.proximity_weight, 0.2);
assert_eq!(config.max_distance_meters, Some(10_000.0));
assert_eq!(config.max_children_per_parent, Some(10));
}
#[test]
fn test_config_debug_clone() {
let config = SelectionConfig::default();
let cloned = config.clone();
assert_eq!(cloned.mobility_weight, config.mobility_weight);
let _ = format!("{:?}", config);
}
#[test]
fn test_peer_candidate_debug_clone() {
let beacon = create_test_beacon(
"test",
GeoPosition::new(37.7749, -122.4194),
HierarchyLevel::Platoon,
NodeMobility::Static,
50,
);
let candidate = PeerCandidate {
beacon,
score: 0.75,
};
let cloned = candidate.clone();
assert_eq!(cloned.score, 0.75);
let _ = format!("{:?}", candidate);
}
}