1use crate::models::{
22 AuthorityLevel, CapabilityExt, CapabilityType, HumanMachinePair, HumanMachinePairExt,
23 NodeConfig, NodeState, NodeStateExt,
24};
25use crate::{Error, Result};
26use serde::{Deserialize, Serialize};
27use std::collections::HashMap;
28
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub struct AggregatedCapability {
32 pub capability_type: CapabilityType,
34 pub confidence: f32,
36 pub contributor_count: usize,
38 pub contributors: Vec<String>,
40 pub max_authority: Option<AuthorityLevel>,
42 pub requires_oversight: bool,
44}
45
46impl AggregatedCapability {
47 pub fn new(
49 capability_type: CapabilityType,
50 confidence: f32,
51 contributors: Vec<String>,
52 max_authority: Option<AuthorityLevel>,
53 ) -> Self {
54 let contributor_count = contributors.len();
55 let requires_oversight = Self::check_oversight_requirement(capability_type, max_authority);
56
57 Self {
58 capability_type,
59 confidence,
60 contributor_count,
61 contributors,
62 max_authority,
63 requires_oversight,
64 }
65 }
66
67 fn check_oversight_requirement(
69 capability_type: CapabilityType,
70 max_authority: Option<AuthorityLevel>,
71 ) -> bool {
72 match capability_type {
74 CapabilityType::Payload => {
75 !matches!(max_authority, Some(AuthorityLevel::Commander))
77 }
78 CapabilityType::Communication => {
79 matches!(max_authority, None | Some(AuthorityLevel::Observer))
81 }
82 _ => false, }
84 }
85
86 pub fn is_mission_ready(&self) -> bool {
88 let confidence_threshold = if self.requires_oversight { 0.8 } else { 0.7 };
89
90 self.confidence >= confidence_threshold
91 && self.contributor_count > 0
92 && (!self.requires_oversight || self.max_authority.is_some())
93 }
94
95 pub fn effective_confidence(&self) -> f32 {
97 let mut confidence = self.confidence;
98
99 if self.requires_oversight {
101 match self.max_authority {
102 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, }
109 }
110
111 confidence.min(1.0)
112 }
113}
114
115pub struct CapabilityAggregator;
117
118impl CapabilityAggregator {
119 pub fn aggregate_capabilities(
127 members: &[(NodeConfig, NodeState)],
128 ) -> Result<HashMap<CapabilityType, AggregatedCapability>> {
129 let mut capability_map: HashMap<
130 CapabilityType,
131 Vec<(String, f32, Option<AuthorityLevel>)>,
132 > = HashMap::new();
133
134 for (config, state) in members {
136 if !state.is_operational() {
138 continue;
139 }
140
141 let authority = config
143 .operator_binding
144 .as_ref()
145 .and_then(Self::get_max_authority);
146
147 for cap in &config.capabilities {
149 capability_map
150 .entry(cap.get_capability_type())
151 .or_default()
152 .push((config.id.clone(), cap.confidence, authority));
153 }
154 }
155
156 let mut aggregated = HashMap::new();
158 for (cap_type, contributors) in capability_map {
159 let agg_cap = Self::aggregate_capability_type(cap_type, contributors)?;
160 aggregated.insert(cap_type, agg_cap);
161 }
162
163 Ok(aggregated)
164 }
165
166 fn aggregate_capability_type(
168 capability_type: CapabilityType,
169 contributors: Vec<(String, f32, Option<AuthorityLevel>)>,
170 ) -> Result<AggregatedCapability> {
171 if contributors.is_empty() {
172 return Err(Error::config_error(
173 "Cannot aggregate capability with no contributors",
174 None,
175 ));
176 }
177
178 let avg_confidence: f32 =
181 contributors.iter().map(|(_, conf, _)| conf).sum::<f32>() / contributors.len() as f32;
182
183 let redundancy_bonus = match contributors.len() {
185 1 => 0.0,
186 2 => 0.05,
187 3..=4 => 0.10,
188 _ => 0.15, };
190
191 let base_confidence = (avg_confidence + redundancy_bonus).min(1.0);
192
193 let max_authority = contributors.iter().filter_map(|(_, _, auth)| *auth).max();
195
196 let authority_bonus = match max_authority {
197 Some(AuthorityLevel::Commander) => 0.10,
198 Some(AuthorityLevel::Supervisor) => 0.05,
199 Some(AuthorityLevel::Advisor) => 0.03,
200 Some(AuthorityLevel::Observer) => 0.0,
201 Some(AuthorityLevel::Unspecified) => 0.0,
202 None => 0.0,
203 };
204
205 let final_confidence = (base_confidence + authority_bonus).min(1.0);
206
207 let contributor_ids: Vec<String> = contributors.into_iter().map(|(id, _, _)| id).collect();
208
209 Ok(AggregatedCapability::new(
210 capability_type,
211 final_confidence,
212 contributor_ids,
213 max_authority,
214 ))
215 }
216
217 fn get_max_authority(binding: &HumanMachinePair) -> Option<AuthorityLevel> {
219 binding.max_authority()
220 }
221
222 pub fn calculate_readiness_score(
226 capabilities: &HashMap<CapabilityType, AggregatedCapability>,
227 ) -> f32 {
228 if capabilities.is_empty() {
229 return 0.0;
230 }
231
232 let weights: HashMap<CapabilityType, f32> = [
234 (CapabilityType::Communication, 0.30), (CapabilityType::Sensor, 0.25), (CapabilityType::Compute, 0.20), (CapabilityType::Payload, 0.15), (CapabilityType::Mobility, 0.10), ]
240 .into_iter()
241 .collect();
242
243 let mut total_score = 0.0;
244 let mut total_weight = 0.0;
245
246 for (cap_type, agg_cap) in capabilities {
247 let weight = weights.get(cap_type).copied().unwrap_or(0.05);
248 let score = agg_cap.effective_confidence();
249 total_score += score * weight;
250 total_weight += weight;
251 }
252
253 if total_weight > 0.0 {
254 total_score / total_weight
255 } else {
256 0.0
257 }
258 }
259
260 pub fn identify_gaps(
264 capabilities: &HashMap<CapabilityType, AggregatedCapability>,
265 required_capabilities: &[CapabilityType],
266 ) -> Vec<CapabilityType> {
267 let mut gaps = Vec::new();
268
269 for &cap_type in required_capabilities {
270 match capabilities.get(&cap_type) {
271 None => gaps.push(cap_type), Some(agg_cap) if !agg_cap.is_mission_ready() => gaps.push(cap_type), _ => {} }
275 }
276
277 gaps
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use crate::models::{
285 Capability, HealthStatus, HumanMachinePairExt, NodeConfigExt, NodeStateExt, Operator,
286 OperatorExt, OperatorRank,
287 };
288
289 fn create_test_platform(
290 id: &str,
291 capabilities: Vec<(CapabilityType, f32)>,
292 operator: Option<Operator>,
293 ) -> (NodeConfig, NodeState) {
294 let mut config = NodeConfig::new("Test".to_string());
295 config.id = id.to_string();
296
297 for (cap_type, confidence) in capabilities {
298 config.add_capability(Capability::new(
299 format!("{}_{:?}", id, cap_type),
300 format!("{:?}", cap_type),
301 cap_type,
302 confidence,
303 ));
304 }
305
306 if let Some(op) = operator {
307 let binding = HumanMachinePair::new(
308 vec![op],
309 vec![id.to_string()],
310 crate::models::BindingType::OneToOne,
311 );
312 config.operator_binding = Some(binding);
313 }
314
315 let state = NodeState::new((0.0, 0.0, 0.0));
316
317 (config, state)
318 }
319
320 #[test]
321 fn test_aggregate_single_platform() {
322 let platform = create_test_platform(
323 "p1",
324 vec![
325 (CapabilityType::Sensor, 0.8),
326 (CapabilityType::Communication, 0.9),
327 ],
328 None,
329 );
330
331 let result = CapabilityAggregator::aggregate_capabilities(&[platform]).unwrap();
332
333 assert_eq!(result.len(), 2);
334 assert!(result.contains_key(&CapabilityType::Sensor));
335 assert!(result.contains_key(&CapabilityType::Communication));
336
337 let sensor_cap = result.get(&CapabilityType::Sensor).unwrap();
338 assert_eq!(sensor_cap.contributor_count, 1);
339 assert_eq!(sensor_cap.confidence, 0.8); }
341
342 #[test]
343 fn test_aggregate_multiple_platforms_redundancy() {
344 let p1 = create_test_platform("p1", vec![(CapabilityType::Sensor, 0.7)], None);
345 let p2 = create_test_platform("p2", vec![(CapabilityType::Sensor, 0.8)], None);
346 let p3 = create_test_platform("p3", vec![(CapabilityType::Sensor, 0.75)], None);
347
348 let result = CapabilityAggregator::aggregate_capabilities(&[p1, p2, p3]).unwrap();
349
350 let sensor_cap = result.get(&CapabilityType::Sensor).unwrap();
351 assert_eq!(sensor_cap.contributor_count, 3);
352
353 assert!((sensor_cap.confidence - 0.85).abs() < 0.01);
357 }
358
359 #[test]
360 fn test_authority_integration() {
361 let operator = Operator::new(
362 "op1".to_string(),
363 "John Doe".to_string(),
364 OperatorRank::E5,
365 AuthorityLevel::Commander,
366 "19D".to_string(),
367 );
368
369 let p1 = create_test_platform("p1", vec![(CapabilityType::Payload, 0.7)], Some(operator));
370
371 let result = CapabilityAggregator::aggregate_capabilities(&[p1]).unwrap();
372
373 let payload_cap = result.get(&CapabilityType::Payload).unwrap();
374 assert_eq!(payload_cap.max_authority, Some(AuthorityLevel::Commander));
375
376 assert!((payload_cap.confidence - 0.80).abs() < 0.01);
378 }
379
380 #[test]
381 fn test_oversight_requirements() {
382 let p1 = create_test_platform("p1", vec![(CapabilityType::Payload, 0.9)], None);
384 let result = CapabilityAggregator::aggregate_capabilities(&[p1]).unwrap();
385 let payload_cap = result.get(&CapabilityType::Payload).unwrap();
386 assert!(payload_cap.requires_oversight);
387
388 let operator = Operator::new(
390 "op1".to_string(),
391 "Jane Smith".to_string(),
392 OperatorRank::E6,
393 AuthorityLevel::Commander,
394 "11B".to_string(),
395 );
396 let p2 = create_test_platform("p2", vec![(CapabilityType::Payload, 0.9)], Some(operator));
397 let result2 = CapabilityAggregator::aggregate_capabilities(&[p2]).unwrap();
398 let payload_cap2 = result2.get(&CapabilityType::Payload).unwrap();
399 assert!(!payload_cap2.requires_oversight);
400 }
401
402 #[test]
403 fn test_mission_readiness() {
404 let operator = Operator::new(
405 "op1".to_string(),
406 "Bob Johnson".to_string(),
407 OperatorRank::E5,
408 AuthorityLevel::Commander,
409 "11B".to_string(),
410 );
411
412 let p1 = create_test_platform("p1", vec![(CapabilityType::Payload, 0.85)], Some(operator));
413 let result = CapabilityAggregator::aggregate_capabilities(&[p1]).unwrap();
414 let payload_cap = result.get(&CapabilityType::Payload).unwrap();
415
416 assert!(payload_cap.is_mission_ready());
418 }
419
420 #[test]
421 fn test_effective_confidence_with_authority() {
422 let operator = Operator::new(
424 "op1".to_string(),
425 "Alice Brown".to_string(),
426 OperatorRank::E4,
427 AuthorityLevel::Observer,
428 "11B".to_string(),
429 );
430
431 let p1 = create_test_platform("p1", vec![(CapabilityType::Payload, 0.9)], Some(operator));
432 let result = CapabilityAggregator::aggregate_capabilities(&[p1]).unwrap();
433 let payload_cap = result.get(&CapabilityType::Payload).unwrap();
434
435 let effective = payload_cap.effective_confidence();
437 assert!(effective < 0.9);
438 assert!((effective - 0.54).abs() < 0.01); }
440
441 #[test]
442 fn test_readiness_score() {
443 let operator = Operator::new(
444 "op1".to_string(),
445 "Charlie Davis".to_string(),
446 OperatorRank::E5,
447 AuthorityLevel::Commander,
448 "11B".to_string(),
449 );
450
451 let p1 = create_test_platform(
452 "p1",
453 vec![
454 (CapabilityType::Communication, 0.9),
455 (CapabilityType::Sensor, 0.8),
456 ],
457 Some(operator.clone()),
458 );
459
460 let p2 = create_test_platform(
461 "p2",
462 vec![
463 (CapabilityType::Compute, 0.85),
464 (CapabilityType::Payload, 0.8),
465 ],
466 Some(operator),
467 );
468
469 let capabilities = CapabilityAggregator::aggregate_capabilities(&[p1, p2]).unwrap();
470 let score = CapabilityAggregator::calculate_readiness_score(&capabilities);
471
472 assert!(score > 0.7);
474 assert!(score <= 1.0);
475 }
476
477 #[test]
478 fn test_identify_gaps() {
479 let p1 = create_test_platform(
480 "p1",
481 vec![
482 (CapabilityType::Sensor, 0.8),
483 (CapabilityType::Communication, 0.9),
484 ],
485 None,
486 );
487
488 let capabilities = CapabilityAggregator::aggregate_capabilities(&[p1]).unwrap();
489
490 let required = vec![
491 CapabilityType::Sensor,
492 CapabilityType::Communication,
493 CapabilityType::Payload,
494 CapabilityType::Compute,
495 ];
496
497 let gaps = CapabilityAggregator::identify_gaps(&capabilities, &required);
498
499 assert_eq!(gaps.len(), 3);
502 assert!(gaps.contains(&CapabilityType::Communication));
503 assert!(gaps.contains(&CapabilityType::Payload));
504 assert!(gaps.contains(&CapabilityType::Compute));
505 }
506
507 #[test]
508 fn test_skip_non_operational_platforms() {
509 let mut platform = create_test_platform("p1", vec![(CapabilityType::Sensor, 0.9)], None);
510
511 platform.1.health = HealthStatus::Degraded as i32;
513
514 let result = CapabilityAggregator::aggregate_capabilities(&[platform]).unwrap();
515
516 assert_eq!(result.len(), 1);
518
519 let mut platform2 = create_test_platform("p2", vec![(CapabilityType::Sensor, 0.9)], None);
521 platform2.1.health = HealthStatus::Critical as i32;
522
523 let result2 = CapabilityAggregator::aggregate_capabilities(&[platform2]).unwrap();
524
525 assert_eq!(result2.len(), 1);
527
528 let mut platform3 = create_test_platform("p3", vec![(CapabilityType::Sensor, 0.9)], None);
530 platform3.1.health = HealthStatus::Failed as i32;
531
532 let result3 = CapabilityAggregator::aggregate_capabilities(&[platform3]).unwrap();
533
534 assert_eq!(result3.len(), 0);
536 }
537
538 #[test]
539 fn test_empty_squad_aggregation() {
540 let result = CapabilityAggregator::aggregate_capabilities(&[]).unwrap();
542 assert_eq!(result.len(), 0);
543
544 let readiness = CapabilityAggregator::calculate_readiness_score(&result);
546 assert_eq!(readiness, 0.0);
547
548 let required = vec![CapabilityType::Communication, CapabilityType::Sensor];
550 let gaps = CapabilityAggregator::identify_gaps(&result, &required);
551 assert_eq!(gaps.len(), 2);
552 assert!(gaps.contains(&CapabilityType::Communication));
553 assert!(gaps.contains(&CapabilityType::Sensor));
554 }
555
556 #[test]
557 fn test_all_platforms_non_operational() {
558 let mut platform1 = create_test_platform("p1", vec![(CapabilityType::Sensor, 0.9)], None);
560 platform1.1.health = HealthStatus::Failed as i32;
561
562 let mut platform2 =
563 create_test_platform("p2", vec![(CapabilityType::Communication, 0.8)], None);
564 platform2.1.health = HealthStatus::Failed as i32;
565
566 let result = CapabilityAggregator::aggregate_capabilities(&[platform1, platform2]).unwrap();
567
568 assert_eq!(result.len(), 0);
570 }
571
572 #[test]
573 fn test_zero_confidence_capability() {
574 let platform = create_test_platform("p1", vec![(CapabilityType::Sensor, 0.0)], None);
576
577 let result = CapabilityAggregator::aggregate_capabilities(&[platform]).unwrap();
578
579 assert_eq!(result.len(), 1);
581 let sensor_cap = result.get(&CapabilityType::Sensor).unwrap();
582 assert!(sensor_cap.confidence < 0.1);
583 }
584}