Skip to main content

plato_relay/
lib.rs

1//! plato-relay — Mycorrhizal I2I relay
2//!
3//! Messages route through emergent trust-weighted hop chains.
4//! No central routing table — paths emerge from local trust decisions.
5
6use std::collections::{HashMap, HashSet, VecDeque};
7
8pub type AgentId = u32;
9
10// ── Message ──────────────────────────────────────────────
11
12#[derive(Debug, Clone)]
13pub struct Message {
14    pub from: AgentId,
15    pub to: AgentId,
16    pub payload: String,
17    pub nutrient: f32,      // metadata value gained at each hop
18    pub hop_count: u32,
19    pub max_hops: u32,
20}
21
22impl Message {
23    pub fn new(from: AgentId, to: AgentId, payload: &str) -> Self {
24        Self {
25            from,
26            to,
27            payload: payload.to_string(),
28            nutrient: 0.0,
29            hop_count: 0,
30            max_hops: 4,
31        }
32    }
33
34    pub fn with_max_hops(mut self, max: u32) -> Self {
35        self.max_hops = max;
36        self
37    }
38}
39
40// ── Delivery Result ──────────────────────────────────────
41
42#[derive(Debug, Clone)]
43pub struct DeliveryResult {
44    pub delivered: bool,
45    pub from: AgentId,
46    pub to: AgentId,
47    pub hops: u32,
48    pub cost: f32,
49    pub path: Vec<AgentId>,
50    pub reason: String,
51}
52
53// ── Agent ────────────────────────────────────────────────
54
55#[derive(Debug, Clone)]
56pub struct Agent {
57    pub id: AgentId,
58    pub energy: f32,
59    pub alive: bool,
60}
61
62impl Agent {
63    pub fn new(id: AgentId, energy: f32) -> Self {
64        Self {
65            id,
66            energy,
67            alive: true,
68        }
69    }
70}
71
72// ── Relay Network ────────────────────────────────────────
73
74pub struct RelayNetwork {
75    agents: HashMap<AgentId, Agent>,
76    trust: HashMap<(AgentId, AgentId), f32>, // (from, to) → trust 0.0-1.0
77    hop_cost: f32,
78    nutrient_per_hop: f32,
79    trust_boost: f32,
80    trust_degrade: f32,
81    trust_halflife_secs: u64,
82}
83
84impl RelayNetwork {
85    pub fn new() -> Self {
86        Self {
87            agents: HashMap::new(),
88            trust: HashMap::new(),
89            hop_cost: 0.1,
90            nutrient_per_hop: 0.02,
91            trust_boost: 0.01,
92            trust_degrade: 0.02,
93            trust_halflife_secs: 3600, // 1 hour
94        }
95    }
96
97    pub fn with_config(
98        hop_cost: f32,
99        nutrient_per_hop: f32,
100        trust_boost: f32,
101        trust_degrade: f32,
102        trust_halflife_secs: u64,
103    ) -> Self {
104        Self {
105            hop_cost,
106            nutrient_per_hop,
107            trust_boost,
108            trust_degrade,
109            trust_halflife_secs,
110            ..Self::new()
111        }
112    }
113
114    // ── Agent Management ──
115
116    pub fn add_agent(&mut self, id: AgentId, energy: f32) {
117        self.agents.insert(id, Agent::new(id, energy));
118    }
119
120    pub fn remove_agent(&mut self, id: AgentId) {
121        self.agents.remove(&id);
122        // Remove all trust entries involving this agent
123        self.trust.retain(|(a, b), _| *a != id && *b != id);
124    }
125
126    pub fn agent(&self, id: AgentId) -> Option<&Agent> {
127        self.agents.get(&id)
128    }
129
130    pub fn agent_count(&self) -> usize {
131        self.agents.len()
132    }
133
134    pub fn alive_agents(&self) -> Vec<AgentId> {
135        self.agents.values().filter(|a| a.alive).map(|a| a.id).collect()
136    }
137
138    // ── Trust Management ──
139
140    pub fn set_trust(&mut self, from: AgentId, to: AgentId, level: f32) {
141        let clamped = level.max(0.0).min(1.0);
142        self.trust.insert((from, to), clamped);
143    }
144
145    pub fn trust(&self, from: AgentId, to: AgentId) -> f32 {
146        *self.trust.get(&(from, to)).unwrap_or(&0.0)
147    }
148
149    pub fn boost_trust(&mut self, from: AgentId, to: AgentId) {
150        let current = self.trust(from, to);
151        self.set_trust(from, to, (current + self.trust_boost).min(1.0));
152    }
153
154    pub fn degrade_trust(&mut self, from: AgentId, to: AgentId) {
155        let current = self.trust(from, to);
156        self.set_trust(from, to, (current - self.trust_degrade).max(0.0));
157    }
158
159    /// Apply time-based trust decay (halflife model)
160    pub fn decay_all_trust(&mut self, elapsed_secs: u64) {
161        if elapsed_secs == 0 { return; }
162        let factor = 0.5_f32.powi(elapsed_secs as i32 / self.trust_halflife_secs.max(1) as i32);
163        for t in self.trust.values_mut() {
164            *t *= factor;
165        }
166    }
167
168    // ── Routing ──
169
170    /// Find best path using BFS with trust-weighted edges.
171    /// Returns path as list of agent IDs (including source and destination).
172    pub fn find_path(&self, from: AgentId, to: AgentId, max_hops: u32) -> Option<Vec<AgentId>> {
173        if from == to { return Some(vec![from]); }
174        if !self.agents.contains_key(&from) || !self.agents.contains_key(&to) { return None; }
175
176        // BFS with trust-weighted priority
177        // Use VecDeque for BFS, track best trust path to each node
178        let mut queue: VecDeque<(AgentId, Vec<AgentId>, f32)> = VecDeque::new();
179        let mut visited: HashSet<AgentId> = HashSet::new();
180
181        queue.push_back((from, vec![from], 1.0));
182        visited.insert(from);
183
184        while let Some((current, path, path_trust)) = queue.pop_front() {
185            if path.len() as u32 > max_hops + 1 { continue; }
186
187            // Find all neighbors (agents with bidirectional trust)
188            let mut neighbors: Vec<(AgentId, f32)> = Vec::new();
189            for (id, _agent) in &self.agents {
190                if *id == current || visited.contains(id) { continue; }
191                let forward = self.trust(current, *id);
192                let backward = self.trust(*id, current);
193                if forward > 0.0 || backward > 0.0 {
194                    let edge_trust = (forward + backward) / 2.0;
195                    neighbors.push((*id, edge_trust));
196                }
197            }
198
199            // Sort by trust descending (prefer high-trust paths)
200            neighbors.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
201
202            for (neighbor, edge_trust) in neighbors {
203                let mut new_path = path.clone();
204                new_path.push(neighbor);
205
206                if neighbor == to {
207                    return Some(new_path);
208                }
209
210                visited.insert(neighbor);
211                queue.push_back((neighbor, new_path, path_trust * edge_trust));
212            }
213        }
214
215        None // No path found
216    }
217
218    // ── Send ──
219
220    /// Send a message through the relay network.
221    pub fn send(&mut self, msg: Message) -> DeliveryResult {
222        // Validate source and destination
223        if !self.agents.contains_key(&msg.from) {
224            return DeliveryResult {
225                delivered: false,
226                from: msg.from,
227                to: msg.to,
228                hops: 0,
229                cost: 0.0,
230                path: vec![],
231                reason: "source agent not found".to_string(),
232            };
233        }
234        if !self.agents.contains_key(&msg.to) {
235            return DeliveryResult {
236                delivered: false,
237                from: msg.from,
238                to: msg.to,
239                hops: 0,
240                cost: 0.0,
241                path: vec![],
242                reason: "destination agent not found".to_string(),
243            };
244        }
245
246        // Find path
247        let path = match self.find_path(msg.from, msg.to, msg.max_hops) {
248            Some(p) => p,
249            None => {
250                // Try direct trust as fallback
251                if self.trust(msg.from, msg.to) > 0.0 {
252                    vec![msg.from, msg.to]
253                } else {
254                    return DeliveryResult {
255                        delivered: false,
256                        from: msg.from,
257                        to: msg.to,
258                        hops: 0,
259                        cost: 0.0,
260                        path: vec![],
261                        reason: "no path found within max hops".to_string(),
262                    };
263                }
264            }
265        };
266
267        let hops = (path.len() - 1) as u32;
268        let cost = hops as f32 * self.hop_cost;
269
270        // Check energy
271        if let Some(agent) = self.agents.get_mut(&msg.from) {
272            if agent.energy < cost {
273                return DeliveryResult {
274                    delivered: false,
275                    from: msg.from,
276                    to: msg.to,
277                    hops: 0,
278                    cost: 0.0,
279                    path: vec![],
280                    reason: format!("insufficient energy: need {:.2}, have {:.2}", cost, agent.energy),
281                };
282            }
283            agent.energy -= cost;
284        }
285
286        // Deliver: boost trust along path
287        for i in 0..path.len().saturating_sub(1) {
288            self.boost_trust(path[i], path[i + 1]);
289        }
290
291        DeliveryResult {
292            delivered: true,
293            from: msg.from,
294            to: msg.to,
295            hops,
296            cost,
297            path: path.clone(),
298            reason: format!("delivered via {} hops, cost {:.2}", hops, cost),
299        }
300    }
301
302    // ── Spore Probes ──
303
304    /// Broadcast a probe to discover new routes.
305    /// Returns all reachable agents from source.
306    pub fn spore_probe(&self, source: AgentId, max_hops: u32) -> Vec<AgentId> {
307        let mut reachable = Vec::new();
308        for (id, _) in &self.agents {
309            if *id == source { continue; }
310            if self.find_path(source, *id, max_hops).is_some() {
311                reachable.push(*id);
312            }
313        }
314        reachable
315    }
316
317    // ── Network Stats ──
318
319    /// Count active trust connections (edges with trust > 0)
320    pub fn connection_count(&self) -> usize {
321        self.trust.iter().filter(|(_, &t)| t > 0.0).count()
322    }
323
324    /// Average trust across all connections
325    pub fn average_trust(&self) -> f32 {
326        let count = self.trust.len();
327        if count == 0 { return 0.0; }
328        let sum: f32 = self.trust.values().sum();
329        sum / count as f32
330    }
331
332    /// Strongest trust connection
333    pub fn strongest_connection(&self) -> Option<(AgentId, AgentId, f32)> {
334        self.trust.iter()
335            .filter(|(_, &t)| t > 0.0)
336            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
337            .map(|(&(a, b), &t)| (a, b, t))
338    }
339
340    /// Weakest non-zero trust connection
341    pub fn weakest_connection(&self) -> Option<(AgentId, AgentId, f32)> {
342        self.trust.iter()
343            .filter(|(_, &t)| t > 0.0)
344            .min_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
345            .map(|(&(a, b), &t)| (a, b, t))
346    }
347}
348
349impl Default for RelayNetwork {
350    fn default() -> Self {
351        Self::new()
352    }
353}
354
355// ── Tests ────────────────────────────────────────────────
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360
361    fn triangle_network() -> RelayNetwork {
362        let mut net = RelayNetwork::new();
363        net.add_agent(0, 10.0);
364        net.add_agent(1, 10.0);
365        net.add_agent(2, 10.0);
366        net.set_trust(0, 1, 0.8);
367        net.set_trust(1, 0, 0.7);
368        net.set_trust(1, 2, 0.6);
369        net.set_trust(2, 1, 0.5);
370        net.set_trust(0, 2, 0.3);
371        net.set_trust(2, 0, 0.3);
372        net
373    }
374
375    #[test]
376    fn test_add_agents() {
377        let mut net = RelayNetwork::new();
378        net.add_agent(0, 10.0);
379        net.add_agent(1, 5.0);
380        assert_eq!(net.agent_count(), 2);
381        assert!(net.agent(0).is_some());
382        assert_eq!(net.agent(0).unwrap().energy, 10.0);
383    }
384
385    #[test]
386    fn test_remove_agent() {
387        let mut net = triangle_network();
388        net.remove_agent(1);
389        assert_eq!(net.agent_count(), 2);
390        assert!(net.agent(1).is_none());
391    }
392
393    #[test]
394    fn test_trust_set_get() {
395        let mut net = RelayNetwork::new();
396        net.set_trust(0, 1, 0.8);
397        assert_eq!(net.trust(0, 1), 0.8);
398        assert_eq!(net.trust(1, 0), 0.0); // not set
399    }
400
401    #[test]
402    fn test_trust_clamping() {
403        let mut net = RelayNetwork::new();
404        net.set_trust(0, 1, 1.5);
405        assert_eq!(net.trust(0, 1), 1.0);
406        net.set_trust(0, 1, -0.5);
407        assert_eq!(net.trust(0, 1), 0.0);
408    }
409
410    #[test]
411    fn test_trust_boost_degrade() {
412        let mut net = RelayNetwork::new();
413        net.set_trust(0, 1, 0.5);
414        let boosted = net.trust(0, 1);
415        net.boost_trust(0, 1);
416        assert!(net.trust(0, 1) > boosted);
417        let after_boost = net.trust(0, 1);
418        net.degrade_trust(0, 1);
419        assert!(net.trust(0, 1) < after_boost);
420    }
421
422    #[test]
423    fn test_trust_decay() {
424        let mut net = RelayNetwork::with_config(0.1, 0.02, 0.01, 0.02, 100);
425        net.set_trust(0, 1, 0.8);
426        net.decay_all_trust(100); // exactly one halflife
427        assert!((net.trust(0, 1) - 0.4).abs() < 0.01);
428    }
429
430    #[test]
431    fn test_direct_path() {
432        let net = triangle_network();
433        let path = net.find_path(0, 1, 4).unwrap();
434        assert_eq!(path, vec![0, 1]);
435    }
436
437    #[test]
438    fn test_indirect_path() {
439        let mut net = RelayNetwork::new();
440        net.add_agent(0, 10.0);
441        net.add_agent(1, 10.0);
442        net.add_agent(2, 10.0);
443        net.set_trust(0, 1, 0.5);
444        net.set_trust(1, 0, 0.5);
445        net.set_trust(1, 2, 0.5);
446        net.set_trust(2, 1, 0.5);
447        // No direct 0→2 trust
448        let path = net.find_path(0, 2, 4).unwrap();
449        assert_eq!(path, vec![0, 1, 2]);
450    }
451
452    #[test]
453    fn test_same_agent_path() {
454        let net = triangle_network();
455        let path = net.find_path(0, 0, 4).unwrap();
456        assert_eq!(path, vec![0]);
457    }
458
459    #[test]
460    fn test_no_path_nonexistent() {
461        let mut net = RelayNetwork::new();
462        net.add_agent(0, 10.0);
463        net.add_agent(1, 10.0);
464        let path = net.find_path(0, 99, 4);
465        assert!(path.is_none());
466    }
467
468    #[test]
469    fn test_no_path_disconnected() {
470        let mut net = RelayNetwork::new();
471        net.add_agent(0, 10.0);
472        net.add_agent(1, 10.0);
473        // No trust between them
474        let path = net.find_path(0, 1, 4);
475        assert!(path.is_none());
476    }
477
478    #[test]
479    fn test_send_direct() {
480        let mut net = triangle_network();
481        let msg = Message::new(0, 1, "hello");
482        let result = net.send(msg);
483        assert!(result.delivered);
484        assert_eq!(result.hops, 1);
485        assert_eq!(result.path, vec![0, 1]);
486    }
487
488    #[test]
489    fn test_send_indirect() {
490        let mut net = triangle_network();
491        let msg = Message::new(0, 2, "hello");
492        let result = net.send(msg);
493        assert!(result.delivered);
494        assert!(result.hops >= 1);
495        assert_eq!(*result.path.last().unwrap(), 2);
496    }
497
498    #[test]
499    fn test_send_nonexistent_source() {
500        let mut net = triangle_network();
501    
502        let msg = Message::new(99, 0, "hello");
503        let result = net.send(msg);
504        assert!(!result.delivered);
505    }
506
507    #[test]
508    fn test_send_nonexistent_dest() {
509        let mut net = triangle_network();
510    
511        let msg = Message::new(0, 99, "hello");
512        let result = net.send(msg);
513        assert!(!result.delivered);
514    }
515
516    #[test]
517    fn test_send_insufficient_energy() {
518        let mut net = RelayNetwork::new();
519        net.add_agent(0, 0.01);
520        net.add_agent(1, 10.0);
521        net.set_trust(0, 1, 0.9);
522        net.set_trust(1, 0, 0.9);
523        let msg = Message::new(0, 1, "hello");
524        let result = net.send(msg);
525        assert!(!result.delivered);
526        assert!(result.reason.contains("insufficient energy"));
527    }
528
529    #[test]
530    fn test_energy_deduction() {
531        let mut net = RelayNetwork::new();
532        net.add_agent(0, 10.0);
533        net.add_agent(1, 10.0);
534        net.set_trust(0, 1, 0.9);
535        net.set_trust(1, 0, 0.9);
536        let msg = Message::new(0, 1, "hello");
537        let _ = net.send(msg);
538        // Energy should be reduced by hop_cost
539        assert!(net.agent(0).unwrap().energy < 10.0);
540    }
541
542    #[test]
543    fn test_trust_boost_on_delivery() {
544        let mut net = RelayNetwork::new();
545        net.add_agent(0, 10.0);
546        net.add_agent(1, 10.0);
547        net.set_trust(0, 1, 0.5);
548        net.set_trust(1, 0, 0.5);
549        let before = net.trust(0, 1);
550        let msg = Message::new(0, 1, "hello");
551        let _ = net.send(msg);
552        assert!(net.trust(0, 1) > before);
553    }
554
555    #[test]
556    fn test_spore_probe() {
557        let net = triangle_network();
558        let reachable = net.spore_probe(0, 4);
559        assert!(reachable.contains(&1));
560        assert!(reachable.contains(&2));
561    }
562
563    #[test]
564    fn test_connection_count() {
565        let net = triangle_network();
566        assert_eq!(net.connection_count(), 6);
567    }
568
569    #[test]
570    fn test_average_trust() {
571        let net = triangle_network();
572        let avg = net.average_trust();
573        // (0.8 + 0.7 + 0.6 + 0.5 + 0.3 + 0.3) / 6 = 0.533
574        assert!((avg - 0.533).abs() < 0.01);
575    }
576
577    #[test]
578    fn test_strongest_weakest() {
579        let net = triangle_network();
580        let strongest = net.strongest_connection().unwrap();
581        assert_eq!(strongest.2, 0.8); // 0→1
582        let weakest = net.weakest_connection().unwrap();
583        assert_eq!(weakest.2, 0.3); // 0→2 or 2→0
584    }
585
586    #[test]
587    fn test_max_hops_limit() {
588        let mut net = RelayNetwork::new();
589        net.add_agent(0, 10.0);
590        net.add_agent(1, 10.0);
591        net.add_agent(2, 10.0);
592        net.add_agent(3, 10.0);
593        net.set_trust(0, 1, 0.5);
594        net.set_trust(1, 0, 0.5);
595        net.set_trust(1, 2, 0.5);
596        net.set_trust(2, 1, 0.5);
597        net.set_trust(2, 3, 0.5);
598        net.set_trust(3, 2, 0.5);
599        // With max_hops=1, can't reach 3 from 0
600        let path = net.find_path(0, 3, 1);
601        assert!(path.is_none());
602        // With max_hops=3, should find path
603        let path = net.find_path(0, 3, 3);
604        assert!(path.is_some());
605    }
606
607    #[test]
608    fn test_dead_agent_pruning() {
609        let mut net = triangle_network();
610        net.remove_agent(1);
611        // 0→2 direct trust still exists
612        let path = net.find_path(0, 2, 4);
613        assert_eq!(path.unwrap(), vec![0, 2]);
614    }
615
616    #[test]
617    fn test_custom_config() {
618        let net = RelayNetwork::with_config(0.5, 0.1, 0.05, 0.1, 600);
619        assert_eq!(net.agent_count(), 0);
620    }
621
622    #[test]
623    fn test_alive_agents() {
624        let net = triangle_network();
625        let alive = net.alive_agents();
626        assert_eq!(alive.len(), 3);
627    }
628
629    #[test]
630    fn test_message_with_max_hops() {
631        let msg = Message::new(0, 1, "hello").with_max_hops(2);
632        assert_eq!(msg.max_hops, 2);
633    }
634}