peat_protocol/composition/
additive.rs1use crate::composition::rules::{CompositionContext, CompositionResult, CompositionRule};
11use crate::models::capability::{Capability, CapabilityExt, CapabilityType};
12use crate::Result;
13use async_trait::async_trait;
14use serde_json::json;
15
16pub struct SensorCoverageRule {
21 min_sensors: usize,
23}
24
25impl SensorCoverageRule {
26 pub fn new(min_sensors: usize) -> Self {
28 Self { min_sensors }
29 }
30}
31
32impl Default for SensorCoverageRule {
33 fn default() -> Self {
34 Self::new(2)
35 }
36}
37
38#[async_trait]
39impl CompositionRule for SensorCoverageRule {
40 fn name(&self) -> &str {
41 "sensor_coverage"
42 }
43
44 fn description(&self) -> &str {
45 "Composes additive sensor coverage from multiple sensor capabilities"
46 }
47
48 fn applies_to(&self, capabilities: &[Capability]) -> bool {
49 let sensor_count = capabilities
50 .iter()
51 .filter(|c| c.get_capability_type() == CapabilityType::Sensor)
52 .count();
53
54 sensor_count >= self.min_sensors
55 }
56
57 async fn compose(
58 &self,
59 capabilities: &[Capability],
60 _context: &CompositionContext,
61 ) -> Result<CompositionResult> {
62 let sensors: Vec<&Capability> = capabilities
63 .iter()
64 .filter(|c| c.get_capability_type() == CapabilityType::Sensor)
65 .collect();
66
67 if sensors.len() < self.min_sensors {
68 return Ok(CompositionResult::new(vec![], 0.0));
69 }
70
71 let total_coverage: f64 = sensors
73 .iter()
74 .filter_map(|cap| {
75 serde_json::from_str::<serde_json::Value>(&cap.metadata_json)
76 .ok()
77 .and_then(|v| v.get("coverage_area").and_then(|v| v.as_f64()))
78 })
79 .sum();
80
81 let avg_confidence: f32 =
83 sensors.iter().map(|c| c.confidence).sum::<f32>() / sensors.len() as f32;
84
85 let mut composed = Capability::new(
86 format!("composed_sensor_coverage_{}", uuid::Uuid::new_v4()),
87 "Aggregate Sensor Coverage".to_string(),
88 CapabilityType::Emergent,
89 avg_confidence,
90 );
91 composed.metadata_json = json!({
92 "coverage_area": total_coverage,
93 "sensor_count": sensors.len(),
94 "composition_type": "additive"
95 })
96 .to_string();
97
98 let contributor_ids: Vec<String> = sensors.iter().map(|c| c.id.clone()).collect();
99
100 Ok(CompositionResult::new(vec![composed], avg_confidence)
101 .with_contributors(contributor_ids))
102 }
103}
104
105pub struct PayloadCapacityRule {
110 min_payloads: usize,
112}
113
114impl PayloadCapacityRule {
115 pub fn new(min_payloads: usize) -> Self {
117 Self { min_payloads }
118 }
119}
120
121impl Default for PayloadCapacityRule {
122 fn default() -> Self {
123 Self::new(2)
124 }
125}
126
127#[async_trait]
128impl CompositionRule for PayloadCapacityRule {
129 fn name(&self) -> &str {
130 "payload_capacity"
131 }
132
133 fn description(&self) -> &str {
134 "Composes additive payload capacity from multiple payload capabilities"
135 }
136
137 fn applies_to(&self, capabilities: &[Capability]) -> bool {
138 let payload_count = capabilities
139 .iter()
140 .filter(|c| c.get_capability_type() == CapabilityType::Payload)
141 .count();
142
143 payload_count >= self.min_payloads
144 }
145
146 async fn compose(
147 &self,
148 capabilities: &[Capability],
149 _context: &CompositionContext,
150 ) -> Result<CompositionResult> {
151 let payloads: Vec<&Capability> = capabilities
152 .iter()
153 .filter(|c| c.get_capability_type() == CapabilityType::Payload)
154 .collect();
155
156 if payloads.len() < self.min_payloads {
157 return Ok(CompositionResult::new(vec![], 0.0));
158 }
159
160 let total_capacity: f64 = payloads
162 .iter()
163 .filter_map(|cap| {
164 serde_json::from_str::<serde_json::Value>(&cap.metadata_json)
165 .ok()
166 .and_then(|v| v.get("lift_capacity").and_then(|v| v.as_f64()))
167 })
168 .sum();
169
170 let avg_confidence: f32 =
172 payloads.iter().map(|c| c.confidence).sum::<f32>() / payloads.len() as f32;
173
174 let mut composed = Capability::new(
175 format!("composed_payload_capacity_{}", uuid::Uuid::new_v4()),
176 "Aggregate Payload Capacity".to_string(),
177 CapabilityType::Emergent,
178 avg_confidence,
179 );
180 composed.metadata_json = json!({
181 "lift_capacity": total_capacity,
182 "payload_count": payloads.len(),
183 "composition_type": "additive"
184 })
185 .to_string();
186
187 let contributor_ids: Vec<String> = payloads.iter().map(|c| c.id.clone()).collect();
188
189 Ok(CompositionResult::new(vec![composed], avg_confidence)
190 .with_contributors(contributor_ids))
191 }
192}
193
194pub struct CommunicationBandwidthRule {
199 min_comms: usize,
201}
202
203impl CommunicationBandwidthRule {
204 pub fn new(min_comms: usize) -> Self {
206 Self { min_comms }
207 }
208}
209
210impl Default for CommunicationBandwidthRule {
211 fn default() -> Self {
212 Self::new(2)
213 }
214}
215
216#[async_trait]
217impl CompositionRule for CommunicationBandwidthRule {
218 fn name(&self) -> &str {
219 "communication_bandwidth"
220 }
221
222 fn description(&self) -> &str {
223 "Composes additive communication bandwidth from multiple communication capabilities"
224 }
225
226 fn applies_to(&self, capabilities: &[Capability]) -> bool {
227 let comm_count = capabilities
228 .iter()
229 .filter(|c| c.get_capability_type() == CapabilityType::Communication)
230 .count();
231
232 comm_count >= self.min_comms
233 }
234
235 async fn compose(
236 &self,
237 capabilities: &[Capability],
238 _context: &CompositionContext,
239 ) -> Result<CompositionResult> {
240 let comms: Vec<&Capability> = capabilities
241 .iter()
242 .filter(|c| c.get_capability_type() == CapabilityType::Communication)
243 .collect();
244
245 if comms.len() < self.min_comms {
246 return Ok(CompositionResult::new(vec![], 0.0));
247 }
248
249 let total_bandwidth: f64 = comms
251 .iter()
252 .filter_map(|cap| {
253 serde_json::from_str::<serde_json::Value>(&cap.metadata_json)
254 .ok()
255 .and_then(|v| v.get("bandwidth").and_then(|v| v.as_f64()))
256 })
257 .sum();
258
259 let avg_confidence: f32 =
261 comms.iter().map(|c| c.confidence).sum::<f32>() / comms.len() as f32;
262
263 let mut composed = Capability::new(
264 format!("composed_comm_bandwidth_{}", uuid::Uuid::new_v4()),
265 "Aggregate Communication Bandwidth".to_string(),
266 CapabilityType::Emergent,
267 avg_confidence,
268 );
269 composed.metadata_json = json!({
270 "bandwidth": total_bandwidth,
271 "comm_count": comms.len(),
272 "composition_type": "additive"
273 })
274 .to_string();
275
276 let contributor_ids: Vec<String> = comms.iter().map(|c| c.id.clone()).collect();
277
278 Ok(CompositionResult::new(vec![composed], avg_confidence)
279 .with_contributors(contributor_ids))
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use serde_json::json;
287
288 #[tokio::test]
289 async fn test_sensor_coverage_composition() {
290 let rule = SensorCoverageRule::default();
291
292 let mut sensor1 = Capability::new(
293 "sensor1".to_string(),
294 "Camera 1".to_string(),
295 CapabilityType::Sensor,
296 0.9,
297 );
298 sensor1.metadata_json = json!({"coverage_area": 100.0}).to_string();
299
300 let mut sensor2 = Capability::new(
301 "sensor2".to_string(),
302 "Camera 2".to_string(),
303 CapabilityType::Sensor,
304 0.8,
305 );
306 sensor2.metadata_json = json!({"coverage_area": 150.0}).to_string();
307
308 let caps = vec![sensor1, sensor2];
309 let context = CompositionContext::new(vec!["node1".to_string()]);
310
311 let result = rule.compose(&caps, &context).await.unwrap();
312
313 assert!(result.has_compositions());
314 assert_eq!(result.composed_capabilities.len(), 1);
315
316 let composed = &result.composed_capabilities[0];
317 assert_eq!(composed.get_capability_type(), CapabilityType::Emergent);
318 let metadata: serde_json::Value = serde_json::from_str(&composed.metadata_json).unwrap();
319 assert_eq!(metadata["coverage_area"].as_f64().unwrap(), 250.0);
320 assert_eq!(metadata["sensor_count"].as_u64().unwrap(), 2);
321 assert_eq!(result.contributing_capabilities.len(), 2);
322 }
323
324 #[tokio::test]
325 async fn test_sensor_coverage_insufficient_sensors() {
326 let rule = SensorCoverageRule::default();
327
328 let mut sensor1 = Capability::new(
329 "sensor1".to_string(),
330 "Camera 1".to_string(),
331 CapabilityType::Sensor,
332 0.9,
333 );
334 sensor1.metadata_json = json!({"coverage_area": 100.0}).to_string();
335
336 let caps = vec![sensor1];
337 let context = CompositionContext::new(vec!["node1".to_string()]);
338
339 let result = rule.compose(&caps, &context).await.unwrap();
340
341 assert!(!result.has_compositions());
342 }
343
344 #[tokio::test]
345 async fn test_payload_capacity_composition() {
346 let rule = PayloadCapacityRule::default();
347
348 let mut payload1 = Capability::new(
349 "payload1".to_string(),
350 "Drone 1".to_string(),
351 CapabilityType::Payload,
352 0.95,
353 );
354 payload1.metadata_json = json!({"lift_capacity": 5.0}).to_string();
355
356 let mut payload2 = Capability::new(
357 "payload2".to_string(),
358 "Drone 2".to_string(),
359 CapabilityType::Payload,
360 0.85,
361 );
362 payload2.metadata_json = json!({"lift_capacity": 7.0}).to_string();
363
364 let caps = vec![payload1, payload2];
365 let context = CompositionContext::new(vec!["node1".to_string(), "node2".to_string()]);
366
367 let result = rule.compose(&caps, &context).await.unwrap();
368
369 assert!(result.has_compositions());
370 assert_eq!(result.composed_capabilities.len(), 1);
371
372 let composed = &result.composed_capabilities[0];
373 let metadata: serde_json::Value = serde_json::from_str(&composed.metadata_json).unwrap();
374 assert_eq!(metadata["lift_capacity"].as_f64().unwrap(), 12.0);
375 assert_eq!(metadata["payload_count"].as_u64().unwrap(), 2);
376 }
377
378 #[tokio::test]
379 async fn test_communication_bandwidth_composition() {
380 let rule = CommunicationBandwidthRule::default();
381
382 let mut comm1 = Capability::new(
383 "comm1".to_string(),
384 "Radio 1".to_string(),
385 CapabilityType::Communication,
386 0.9,
387 );
388 comm1.metadata_json = json!({"bandwidth": 10.0}).to_string();
389
390 let mut comm2 = Capability::new(
391 "comm2".to_string(),
392 "Radio 2".to_string(),
393 CapabilityType::Communication,
394 0.85,
395 );
396 comm2.metadata_json = json!({"bandwidth": 15.0}).to_string();
397
398 let mut comm3 = Capability::new(
399 "comm3".to_string(),
400 "Satellite".to_string(),
401 CapabilityType::Communication,
402 0.95,
403 );
404 comm3.metadata_json = json!({"bandwidth": 50.0}).to_string();
405
406 let caps = vec![comm1, comm2, comm3];
407 let context = CompositionContext::new(vec!["node1".to_string()]);
408
409 let result = rule.compose(&caps, &context).await.unwrap();
410
411 assert!(result.has_compositions());
412 let composed = &result.composed_capabilities[0];
413 let metadata: serde_json::Value = serde_json::from_str(&composed.metadata_json).unwrap();
414 assert_eq!(metadata["bandwidth"].as_f64().unwrap(), 75.0);
415 assert_eq!(metadata["comm_count"].as_u64().unwrap(), 3);
416 }
417
418 #[tokio::test]
419 async fn test_applies_to_checks() {
420 let sensor_rule = SensorCoverageRule::default();
421 let payload_rule = PayloadCapacityRule::default();
422 let comm_rule = CommunicationBandwidthRule::default();
423
424 let sensor = Capability::new(
425 "s1".to_string(),
426 "Sensor".to_string(),
427 CapabilityType::Sensor,
428 0.9,
429 );
430 let payload = Capability::new(
431 "p1".to_string(),
432 "Payload".to_string(),
433 CapabilityType::Payload,
434 0.9,
435 );
436
437 let caps = vec![sensor.clone(), payload.clone()];
438
439 assert!(!sensor_rule.applies_to(&caps));
441
442 assert!(sensor_rule.applies_to(&[sensor.clone(), sensor]));
444
445 assert!(payload_rule.applies_to(&[payload.clone(), payload]));
447
448 assert!(!comm_rule.applies_to(&caps));
450 }
451
452 #[tokio::test]
453 async fn test_confidence_averaging() {
454 let rule = SensorCoverageRule::default();
455
456 let mut sensor1 = Capability::new(
457 "sensor1".to_string(),
458 "High Confidence Sensor".to_string(),
459 CapabilityType::Sensor,
460 0.9,
461 );
462 sensor1.metadata_json = json!({"coverage_area": 100.0}).to_string();
463
464 let mut sensor2 = Capability::new(
465 "sensor2".to_string(),
466 "Low Confidence Sensor".to_string(),
467 CapabilityType::Sensor,
468 0.5,
469 );
470 sensor2.metadata_json = json!({"coverage_area": 100.0}).to_string();
471
472 let caps = vec![sensor1, sensor2];
473 let context = CompositionContext::new(vec!["node1".to_string()]);
474
475 let result = rule.compose(&caps, &context).await.unwrap();
476
477 let composed = &result.composed_capabilities[0];
478 assert_eq!(composed.confidence, 0.7);
480 assert_eq!(result.confidence, 0.7);
481 }
482}