Skip to main content

peat_protocol/composition/
additive.rs

1//! Additive composition rules
2//!
3//! This module implements composition rules for capabilities that combine
4//! additively, such as:
5//! - Coverage area (sum of sensor ranges)
6//! - Lift capacity (sum of payload weights)
7//! - Sensor count (total number of sensors)
8//! - Communication bandwidth (aggregate throughput)
9
10use 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
16/// Rule for composing additive sensor coverage
17///
18/// Combines multiple sensors to calculate total coverage area.
19/// Metadata should include "coverage_area" in square meters.
20pub struct SensorCoverageRule {
21    /// Minimum number of sensors required for composition
22    min_sensors: usize,
23}
24
25impl SensorCoverageRule {
26    /// Create a new sensor coverage rule
27    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        // Sum coverage areas from metadata (parse from JSON string)
72        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        // Average confidence across sensors
82        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
105/// Rule for composing additive payload capacity
106///
107/// Combines multiple payload capabilities to calculate total lift capacity.
108/// Metadata should include "lift_capacity" in kilograms.
109pub struct PayloadCapacityRule {
110    /// Minimum number of payload capabilities required
111    min_payloads: usize,
112}
113
114impl PayloadCapacityRule {
115    /// Create a new payload capacity rule
116    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        // Sum lift capacities from metadata
161        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        // Average confidence across payloads
171        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
194/// Rule for composing additive communication bandwidth
195///
196/// Combines multiple communication capabilities to calculate total bandwidth.
197/// Metadata should include "bandwidth" in Mbps.
198pub struct CommunicationBandwidthRule {
199    /// Minimum number of communication capabilities required
200    min_comms: usize,
201}
202
203impl CommunicationBandwidthRule {
204    /// Create a new communication bandwidth rule
205    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        // Sum bandwidth from metadata
250        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        // Average confidence across communication capabilities
260        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        // Sensor rule shouldn't apply (only 1 sensor)
440        assert!(!sensor_rule.applies_to(&caps));
441
442        // With 2 sensors, should apply
443        assert!(sensor_rule.applies_to(&[sensor.clone(), sensor]));
444
445        // Payload rule should apply (2 payloads)
446        assert!(payload_rule.applies_to(&[payload.clone(), payload]));
447
448        // Comm rule shouldn't apply (no comm capabilities)
449        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        // Average of 0.9 and 0.5 = 0.7
479        assert_eq!(composed.confidence, 0.7);
480        assert_eq!(result.confidence, 0.7);
481    }
482}