use crate::models::{
AuthorityLevel, CapabilityExt, CapabilityType, HumanMachinePair, HumanMachinePairExt,
NodeConfig, NodeState, NodeStateExt,
};
use crate::{Error, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AggregatedCapability {
pub capability_type: CapabilityType,
pub confidence: f32,
pub contributor_count: usize,
pub contributors: Vec<String>,
pub max_authority: Option<AuthorityLevel>,
pub requires_oversight: bool,
}
impl AggregatedCapability {
pub fn new(
capability_type: CapabilityType,
confidence: f32,
contributors: Vec<String>,
max_authority: Option<AuthorityLevel>,
) -> Self {
let contributor_count = contributors.len();
let requires_oversight = Self::check_oversight_requirement(capability_type, max_authority);
Self {
capability_type,
confidence,
contributor_count,
contributors,
max_authority,
requires_oversight,
}
}
fn check_oversight_requirement(
capability_type: CapabilityType,
max_authority: Option<AuthorityLevel>,
) -> bool {
match capability_type {
CapabilityType::Payload => {
!matches!(max_authority, Some(AuthorityLevel::Commander))
}
CapabilityType::Communication => {
matches!(max_authority, None | Some(AuthorityLevel::Observer))
}
_ => false, }
}
pub fn is_mission_ready(&self) -> bool {
let confidence_threshold = if self.requires_oversight { 0.8 } else { 0.7 };
self.confidence >= confidence_threshold
&& self.contributor_count > 0
&& (!self.requires_oversight || self.max_authority.is_some())
}
pub fn effective_confidence(&self) -> f32 {
let mut confidence = self.confidence;
if self.requires_oversight {
match self.max_authority {
Some(AuthorityLevel::Commander) => confidence *= 1.0, Some(AuthorityLevel::Supervisor) => confidence *= 0.85, Some(AuthorityLevel::Advisor) => confidence *= 0.7, Some(AuthorityLevel::Observer) => confidence *= 0.6, Some(AuthorityLevel::Unspecified) => confidence *= 0.5, None => confidence *= 0.5, }
}
confidence.min(1.0)
}
}
pub struct CapabilityAggregator;
impl CapabilityAggregator {
pub fn aggregate_capabilities(
members: &[(NodeConfig, NodeState)],
) -> Result<HashMap<CapabilityType, AggregatedCapability>> {
let mut capability_map: HashMap<
CapabilityType,
Vec<(String, f32, Option<AuthorityLevel>)>,
> = HashMap::new();
for (config, state) in members {
if !state.is_operational() {
continue;
}
let authority = config
.operator_binding
.as_ref()
.and_then(Self::get_max_authority);
for cap in &config.capabilities {
capability_map
.entry(cap.get_capability_type())
.or_default()
.push((config.id.clone(), cap.confidence, authority));
}
}
let mut aggregated = HashMap::new();
for (cap_type, contributors) in capability_map {
let agg_cap = Self::aggregate_capability_type(cap_type, contributors)?;
aggregated.insert(cap_type, agg_cap);
}
Ok(aggregated)
}
fn aggregate_capability_type(
capability_type: CapabilityType,
contributors: Vec<(String, f32, Option<AuthorityLevel>)>,
) -> Result<AggregatedCapability> {
if contributors.is_empty() {
return Err(Error::config_error(
"Cannot aggregate capability with no contributors",
None,
));
}
let avg_confidence: f32 =
contributors.iter().map(|(_, conf, _)| conf).sum::<f32>() / contributors.len() as f32;
let redundancy_bonus = match contributors.len() {
1 => 0.0,
2 => 0.05,
3..=4 => 0.10,
_ => 0.15, };
let base_confidence = (avg_confidence + redundancy_bonus).min(1.0);
let max_authority = contributors.iter().filter_map(|(_, _, auth)| *auth).max();
let authority_bonus = match max_authority {
Some(AuthorityLevel::Commander) => 0.10,
Some(AuthorityLevel::Supervisor) => 0.05,
Some(AuthorityLevel::Advisor) => 0.03,
Some(AuthorityLevel::Observer) => 0.0,
Some(AuthorityLevel::Unspecified) => 0.0,
None => 0.0,
};
let final_confidence = (base_confidence + authority_bonus).min(1.0);
let contributor_ids: Vec<String> = contributors.into_iter().map(|(id, _, _)| id).collect();
Ok(AggregatedCapability::new(
capability_type,
final_confidence,
contributor_ids,
max_authority,
))
}
fn get_max_authority(binding: &HumanMachinePair) -> Option<AuthorityLevel> {
binding.max_authority()
}
pub fn calculate_readiness_score(
capabilities: &HashMap<CapabilityType, AggregatedCapability>,
) -> f32 {
if capabilities.is_empty() {
return 0.0;
}
let weights: HashMap<CapabilityType, f32> = [
(CapabilityType::Communication, 0.30), (CapabilityType::Sensor, 0.25), (CapabilityType::Compute, 0.20), (CapabilityType::Payload, 0.15), (CapabilityType::Mobility, 0.10), ]
.into_iter()
.collect();
let mut total_score = 0.0;
let mut total_weight = 0.0;
for (cap_type, agg_cap) in capabilities {
let weight = weights.get(cap_type).copied().unwrap_or(0.05);
let score = agg_cap.effective_confidence();
total_score += score * weight;
total_weight += weight;
}
if total_weight > 0.0 {
total_score / total_weight
} else {
0.0
}
}
pub fn identify_gaps(
capabilities: &HashMap<CapabilityType, AggregatedCapability>,
required_capabilities: &[CapabilityType],
) -> Vec<CapabilityType> {
let mut gaps = Vec::new();
for &cap_type in required_capabilities {
match capabilities.get(&cap_type) {
None => gaps.push(cap_type), Some(agg_cap) if !agg_cap.is_mission_ready() => gaps.push(cap_type), _ => {} }
}
gaps
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{
Capability, HealthStatus, HumanMachinePairExt, NodeConfigExt, NodeStateExt, Operator,
OperatorExt, OperatorRank,
};
fn create_test_platform(
id: &str,
capabilities: Vec<(CapabilityType, f32)>,
operator: Option<Operator>,
) -> (NodeConfig, NodeState) {
let mut config = NodeConfig::new("Test".to_string());
config.id = id.to_string();
for (cap_type, confidence) in capabilities {
config.add_capability(Capability::new(
format!("{}_{:?}", id, cap_type),
format!("{:?}", cap_type),
cap_type,
confidence,
));
}
if let Some(op) = operator {
let binding = HumanMachinePair::new(
vec![op],
vec![id.to_string()],
crate::models::BindingType::OneToOne,
);
config.operator_binding = Some(binding);
}
let state = NodeState::new((0.0, 0.0, 0.0));
(config, state)
}
#[test]
fn test_aggregate_single_platform() {
let platform = create_test_platform(
"p1",
vec![
(CapabilityType::Sensor, 0.8),
(CapabilityType::Communication, 0.9),
],
None,
);
let result = CapabilityAggregator::aggregate_capabilities(&[platform]).unwrap();
assert_eq!(result.len(), 2);
assert!(result.contains_key(&CapabilityType::Sensor));
assert!(result.contains_key(&CapabilityType::Communication));
let sensor_cap = result.get(&CapabilityType::Sensor).unwrap();
assert_eq!(sensor_cap.contributor_count, 1);
assert_eq!(sensor_cap.confidence, 0.8); }
#[test]
fn test_aggregate_multiple_platforms_redundancy() {
let p1 = create_test_platform("p1", vec![(CapabilityType::Sensor, 0.7)], None);
let p2 = create_test_platform("p2", vec![(CapabilityType::Sensor, 0.8)], None);
let p3 = create_test_platform("p3", vec![(CapabilityType::Sensor, 0.75)], None);
let result = CapabilityAggregator::aggregate_capabilities(&[p1, p2, p3]).unwrap();
let sensor_cap = result.get(&CapabilityType::Sensor).unwrap();
assert_eq!(sensor_cap.contributor_count, 3);
assert!((sensor_cap.confidence - 0.85).abs() < 0.01);
}
#[test]
fn test_authority_integration() {
let operator = Operator::new(
"op1".to_string(),
"John Doe".to_string(),
OperatorRank::E5,
AuthorityLevel::Commander,
"19D".to_string(),
);
let p1 = create_test_platform("p1", vec![(CapabilityType::Payload, 0.7)], Some(operator));
let result = CapabilityAggregator::aggregate_capabilities(&[p1]).unwrap();
let payload_cap = result.get(&CapabilityType::Payload).unwrap();
assert_eq!(payload_cap.max_authority, Some(AuthorityLevel::Commander));
assert!((payload_cap.confidence - 0.80).abs() < 0.01);
}
#[test]
fn test_oversight_requirements() {
let p1 = create_test_platform("p1", vec![(CapabilityType::Payload, 0.9)], None);
let result = CapabilityAggregator::aggregate_capabilities(&[p1]).unwrap();
let payload_cap = result.get(&CapabilityType::Payload).unwrap();
assert!(payload_cap.requires_oversight);
let operator = Operator::new(
"op1".to_string(),
"Jane Smith".to_string(),
OperatorRank::E6,
AuthorityLevel::Commander,
"11B".to_string(),
);
let p2 = create_test_platform("p2", vec![(CapabilityType::Payload, 0.9)], Some(operator));
let result2 = CapabilityAggregator::aggregate_capabilities(&[p2]).unwrap();
let payload_cap2 = result2.get(&CapabilityType::Payload).unwrap();
assert!(!payload_cap2.requires_oversight);
}
#[test]
fn test_mission_readiness() {
let operator = Operator::new(
"op1".to_string(),
"Bob Johnson".to_string(),
OperatorRank::E5,
AuthorityLevel::Commander,
"11B".to_string(),
);
let p1 = create_test_platform("p1", vec![(CapabilityType::Payload, 0.85)], Some(operator));
let result = CapabilityAggregator::aggregate_capabilities(&[p1]).unwrap();
let payload_cap = result.get(&CapabilityType::Payload).unwrap();
assert!(payload_cap.is_mission_ready());
}
#[test]
fn test_effective_confidence_with_authority() {
let operator = Operator::new(
"op1".to_string(),
"Alice Brown".to_string(),
OperatorRank::E4,
AuthorityLevel::Observer,
"11B".to_string(),
);
let p1 = create_test_platform("p1", vec![(CapabilityType::Payload, 0.9)], Some(operator));
let result = CapabilityAggregator::aggregate_capabilities(&[p1]).unwrap();
let payload_cap = result.get(&CapabilityType::Payload).unwrap();
let effective = payload_cap.effective_confidence();
assert!(effective < 0.9);
assert!((effective - 0.54).abs() < 0.01); }
#[test]
fn test_readiness_score() {
let operator = Operator::new(
"op1".to_string(),
"Charlie Davis".to_string(),
OperatorRank::E5,
AuthorityLevel::Commander,
"11B".to_string(),
);
let p1 = create_test_platform(
"p1",
vec![
(CapabilityType::Communication, 0.9),
(CapabilityType::Sensor, 0.8),
],
Some(operator.clone()),
);
let p2 = create_test_platform(
"p2",
vec![
(CapabilityType::Compute, 0.85),
(CapabilityType::Payload, 0.8),
],
Some(operator),
);
let capabilities = CapabilityAggregator::aggregate_capabilities(&[p1, p2]).unwrap();
let score = CapabilityAggregator::calculate_readiness_score(&capabilities);
assert!(score > 0.7);
assert!(score <= 1.0);
}
#[test]
fn test_identify_gaps() {
let p1 = create_test_platform(
"p1",
vec![
(CapabilityType::Sensor, 0.8),
(CapabilityType::Communication, 0.9),
],
None,
);
let capabilities = CapabilityAggregator::aggregate_capabilities(&[p1]).unwrap();
let required = vec![
CapabilityType::Sensor,
CapabilityType::Communication,
CapabilityType::Payload,
CapabilityType::Compute,
];
let gaps = CapabilityAggregator::identify_gaps(&capabilities, &required);
assert_eq!(gaps.len(), 3);
assert!(gaps.contains(&CapabilityType::Communication));
assert!(gaps.contains(&CapabilityType::Payload));
assert!(gaps.contains(&CapabilityType::Compute));
}
#[test]
fn test_skip_non_operational_platforms() {
let mut platform = create_test_platform("p1", vec![(CapabilityType::Sensor, 0.9)], None);
platform.1.health = HealthStatus::Degraded as i32;
let result = CapabilityAggregator::aggregate_capabilities(&[platform]).unwrap();
assert_eq!(result.len(), 1);
let mut platform2 = create_test_platform("p2", vec![(CapabilityType::Sensor, 0.9)], None);
platform2.1.health = HealthStatus::Critical as i32;
let result2 = CapabilityAggregator::aggregate_capabilities(&[platform2]).unwrap();
assert_eq!(result2.len(), 1);
let mut platform3 = create_test_platform("p3", vec![(CapabilityType::Sensor, 0.9)], None);
platform3.1.health = HealthStatus::Failed as i32;
let result3 = CapabilityAggregator::aggregate_capabilities(&[platform3]).unwrap();
assert_eq!(result3.len(), 0);
}
#[test]
fn test_empty_squad_aggregation() {
let result = CapabilityAggregator::aggregate_capabilities(&[]).unwrap();
assert_eq!(result.len(), 0);
let readiness = CapabilityAggregator::calculate_readiness_score(&result);
assert_eq!(readiness, 0.0);
let required = vec![CapabilityType::Communication, CapabilityType::Sensor];
let gaps = CapabilityAggregator::identify_gaps(&result, &required);
assert_eq!(gaps.len(), 2);
assert!(gaps.contains(&CapabilityType::Communication));
assert!(gaps.contains(&CapabilityType::Sensor));
}
#[test]
fn test_all_platforms_non_operational() {
let mut platform1 = create_test_platform("p1", vec![(CapabilityType::Sensor, 0.9)], None);
platform1.1.health = HealthStatus::Failed as i32;
let mut platform2 =
create_test_platform("p2", vec![(CapabilityType::Communication, 0.8)], None);
platform2.1.health = HealthStatus::Failed as i32;
let result = CapabilityAggregator::aggregate_capabilities(&[platform1, platform2]).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_zero_confidence_capability() {
let platform = create_test_platform("p1", vec![(CapabilityType::Sensor, 0.0)], None);
let result = CapabilityAggregator::aggregate_capabilities(&[platform]).unwrap();
assert_eq!(result.len(), 1);
let sensor_cap = result.get(&CapabilityType::Sensor).unwrap();
assert!(sensor_cap.confidence < 0.1);
}
}