Skip to main content

phago_runtime/
substrate_impl.rs

1//! Concrete implementation of the Substrate trait.
2//!
3//! In-memory substrate with:
4//! - Signal field stored as a Vec (linear scan with distance filtering)
5//! - Knowledge graph backed by PetTopologyGraph
6//! - Trace storage as a HashMap keyed by SubstrateLocation
7//! - Serialization support for persistence across restarts
8
9use crate::topology_impl::PetTopologyGraph;
10use phago_core::substrate::Substrate;
11use phago_core::topology::TopologyGraph;
12use phago_core::types::*;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16/// In-memory substrate implementation.
17///
18/// The substrate is the extracellular matrix — the shared environment
19/// all agents sense and modify. It holds signals (for chemotaxis),
20/// a knowledge graph (for stigmergy and Hebbian wiring), and traces
21/// (for indirect coordination).
22pub struct SubstrateImpl {
23    signals: Vec<Signal>,
24    graph: PetTopologyGraph,
25    traces: HashMap<TraceLocationKey, Vec<Trace>>,
26    documents: HashMap<DocumentId, Document>,
27    tick: Tick,
28}
29
30/// Key for trace storage. We need something hashable for SubstrateLocation.
31#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
32enum TraceLocationKey {
33    Spatial(OrderedPosition),
34    GraphNode(NodeId),
35}
36
37/// Position with Eq/Hash for use as HashMap key (quantized to grid).
38#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
39struct OrderedPosition {
40    /// Quantized to 0.1 grid units for hashing.
41    x: i64,
42    y: i64,
43}
44
45impl From<&Position> for OrderedPosition {
46    fn from(p: &Position) -> Self {
47        Self {
48            x: (p.x * 10.0).round() as i64,
49            y: (p.y * 10.0).round() as i64,
50        }
51    }
52}
53
54impl From<&SubstrateLocation> for TraceLocationKey {
55    fn from(loc: &SubstrateLocation) -> Self {
56        match loc {
57            SubstrateLocation::Spatial(pos) => TraceLocationKey::Spatial(OrderedPosition::from(pos)),
58            SubstrateLocation::GraphNode(id) => TraceLocationKey::GraphNode(*id),
59        }
60    }
61}
62
63impl SubstrateImpl {
64    pub fn new() -> Self {
65        Self {
66            signals: Vec::new(),
67            graph: PetTopologyGraph::new(),
68            traces: HashMap::new(),
69            documents: HashMap::new(),
70            tick: 0,
71        }
72    }
73
74    /// Get a document by ID (convenience method bypassing trait).
75    pub fn get_document(&self, id: &DocumentId) -> Option<&Document> {
76        self.documents.get(id)
77    }
78
79    /// Get all documents (convenience method).
80    pub fn all_documents(&self) -> Vec<&Document> {
81        self.documents.values().collect()
82    }
83
84    /// Get a reference to the underlying topology graph.
85    pub fn graph(&self) -> &PetTopologyGraph {
86        &self.graph
87    }
88
89    /// Get a mutable reference to the underlying topology graph.
90    pub fn graph_mut(&mut self) -> &mut PetTopologyGraph {
91        &mut self.graph
92    }
93
94    /// Get all signals (for diagnostics/visualization).
95    pub fn all_signals(&self) -> &[Signal] {
96        &self.signals
97    }
98
99    /// Total number of traces across all locations.
100    pub fn total_trace_count(&self) -> usize {
101        self.traces.values().map(|v| v.len()).sum()
102    }
103
104    /// Get all traces of a given type within a radius of a position.
105    pub fn traces_near(&self, position: &Position, radius: f64, trace_type: &TraceType) -> Vec<&Trace> {
106        let r_grid = (radius * 10.0).ceil() as i64;
107        let cx = (position.x * 10.0).round() as i64;
108        let cy = (position.y * 10.0).round() as i64;
109
110        let mut results = Vec::new();
111        // Scan grid cells within radius
112        for dx in -r_grid..=r_grid {
113            for dy in -r_grid..=r_grid {
114                let key = TraceLocationKey::Spatial(OrderedPosition {
115                    x: cx + dx,
116                    y: cy + dy,
117                });
118                if let Some(traces) = self.traces.get(&key) {
119                    for trace in traces {
120                        if &trace.trace_type == trace_type {
121                            results.push(trace);
122                        }
123                    }
124                }
125            }
126        }
127        results
128    }
129}
130
131impl Default for SubstrateImpl {
132    fn default() -> Self {
133        Self::new()
134    }
135}
136
137impl Substrate for SubstrateImpl {
138    // --- Signal field ---
139
140    fn signals_near(&self, position: &Position, radius: f64) -> Vec<&Signal> {
141        let r2 = radius * radius;
142        self.signals
143            .iter()
144            .filter(|s| {
145                let dx = s.position.x - position.x;
146                let dy = s.position.y - position.y;
147                dx * dx + dy * dy <= r2
148            })
149            .collect()
150    }
151
152    fn emit_signal(&mut self, signal: Signal) {
153        self.signals.push(signal);
154    }
155
156    fn decay_signals(&mut self, rate: f64, removal_threshold: f64) {
157        for signal in &mut self.signals {
158            signal.decay(rate);
159        }
160        self.signals
161            .retain(|s| !s.is_below_threshold(removal_threshold));
162    }
163
164    // --- Knowledge graph ---
165
166    fn add_node(&mut self, data: NodeData) -> NodeId {
167        self.graph.add_node(data)
168    }
169
170    fn get_node(&self, id: &NodeId) -> Option<&NodeData> {
171        self.graph.get_node(id)
172    }
173
174    fn set_edge(&mut self, from: NodeId, to: NodeId, data: EdgeData) {
175        self.graph.set_edge(from, to, data);
176    }
177
178    fn get_edge(&self, from: &NodeId, to: &NodeId) -> Option<&EdgeData> {
179        self.graph.get_edge(from, to)
180    }
181
182    fn neighbors(&self, node: &NodeId) -> Vec<(NodeId, &EdgeData)> {
183        self.graph.neighbors(node)
184    }
185
186    fn remove_edge(&mut self, from: &NodeId, to: &NodeId) {
187        self.graph.remove_edge(from, to);
188    }
189
190    fn all_nodes(&self) -> Vec<NodeId> {
191        self.graph.all_nodes()
192    }
193
194    fn all_edges(&self) -> Vec<(NodeId, NodeId, &EdgeData)> {
195        self.graph.all_edges()
196    }
197
198    fn node_count(&self) -> usize {
199        self.graph.node_count()
200    }
201
202    fn edge_count(&self) -> usize {
203        self.graph.edge_count()
204    }
205
206    // --- Trace storage ---
207
208    fn deposit_trace(&mut self, location: &SubstrateLocation, trace: Trace) {
209        let key = TraceLocationKey::from(location);
210        self.traces.entry(key).or_default().push(trace);
211    }
212
213    fn traces_at(&self, location: &SubstrateLocation) -> Vec<&Trace> {
214        let key = TraceLocationKey::from(location);
215        self.traces
216            .get(&key)
217            .map(|traces| traces.iter().collect())
218            .unwrap_or_default()
219    }
220
221    fn decay_traces(&mut self, rate: f64, removal_threshold: f64) {
222        for traces in self.traces.values_mut() {
223            for trace in traces.iter_mut() {
224                trace.intensity *= 1.0 - rate;
225            }
226            traces.retain(|t| t.intensity >= removal_threshold);
227        }
228        // Remove empty locations
229        self.traces.retain(|_, v| !v.is_empty());
230    }
231
232    // --- Document storage ---
233
234    fn add_document(&mut self, doc: Document) {
235        self.documents.insert(doc.id, doc);
236    }
237
238    fn get_document(&self, id: &DocumentId) -> Option<&Document> {
239        self.documents.get(id)
240    }
241
242    fn undigested_documents(&self) -> Vec<&Document> {
243        self.documents.values().filter(|d| !d.digested).collect()
244    }
245
246    fn consume_document(&mut self, id: &DocumentId) -> Option<String> {
247        if let Some(doc) = self.documents.get_mut(id) {
248            if !doc.digested {
249                doc.digested = true;
250                return Some(doc.content.clone());
251            }
252        }
253        None
254    }
255
256    fn all_documents(&self) -> Vec<&Document> {
257        self.documents.values().collect()
258    }
259
260    // --- Lifecycle ---
261
262    fn current_tick(&self) -> Tick {
263        self.tick
264    }
265
266    fn advance_tick(&mut self) {
267        self.tick += 1;
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    fn make_signal(x: f64, y: f64, intensity: f64) -> Signal {
276        Signal::new(
277            SignalType::Input,
278            intensity,
279            Position::new(x, y),
280            AgentId::new(),
281            0,
282        )
283    }
284
285    #[test]
286    fn signals_near_filters_by_distance() {
287        let mut sub = SubstrateImpl::new();
288        sub.emit_signal(make_signal(1.0, 1.0, 1.0)); // Close
289        sub.emit_signal(make_signal(100.0, 100.0, 1.0)); // Far
290
291        let near = sub.signals_near(&Position::new(0.0, 0.0), 5.0);
292        assert_eq!(near.len(), 1);
293    }
294
295    #[test]
296    fn signal_decay_removes_weak_signals() {
297        let mut sub = SubstrateImpl::new();
298        sub.emit_signal(make_signal(0.0, 0.0, 1.0));
299        sub.emit_signal(make_signal(1.0, 1.0, 0.05));
300
301        // Decay by 50%, remove below 0.04
302        sub.decay_signals(0.5, 0.04);
303        assert_eq!(sub.all_signals().len(), 1); // Only the strong one survives
304    }
305
306    #[test]
307    fn trace_deposit_and_retrieve() {
308        let mut sub = SubstrateImpl::new();
309        let loc = SubstrateLocation::Spatial(Position::new(5.0, 5.0));
310        let trace = Trace {
311            agent_id: AgentId::new(),
312            trace_type: TraceType::Digestion,
313            intensity: 1.0,
314            tick: 0,
315            payload: vec![],
316        };
317        sub.deposit_trace(&loc, trace);
318
319        let traces = sub.traces_at(&loc);
320        assert_eq!(traces.len(), 1);
321        assert_eq!(traces[0].trace_type, TraceType::Digestion);
322    }
323
324    #[test]
325    fn trace_decay_removes_weak_traces() {
326        let mut sub = SubstrateImpl::new();
327        let loc = SubstrateLocation::Spatial(Position::new(0.0, 0.0));
328        sub.deposit_trace(&loc, Trace {
329            agent_id: AgentId::new(),
330            trace_type: TraceType::Visit,
331            intensity: 1.0,
332            tick: 0,
333            payload: vec![],
334        });
335        sub.deposit_trace(&loc, Trace {
336            agent_id: AgentId::new(),
337            trace_type: TraceType::Visit,
338            intensity: 0.02,
339            tick: 0,
340            payload: vec![],
341        });
342
343        sub.decay_traces(0.5, 0.02);
344        // Strong trace decays to 0.5, weak to 0.01 (removed)
345        assert_eq!(sub.traces_at(&loc).len(), 1);
346    }
347
348    #[test]
349    fn graph_operations_through_substrate() {
350        let mut sub = SubstrateImpl::new();
351
352        let n1 = sub.add_node(NodeData {
353            id: NodeId::new(),
354            label: "cell".to_string(),
355            node_type: NodeType::Concept,
356            position: Position::new(0.0, 0.0),
357            access_count: 0,
358            created_tick: 0,
359        });
360        let n2 = sub.add_node(NodeData {
361            id: NodeId::new(),
362            label: "membrane".to_string(),
363            node_type: NodeType::Concept,
364            position: Position::new(1.0, 0.0),
365            access_count: 0,
366            created_tick: 0,
367        });
368
369        sub.set_edge(n1, n2, EdgeData {
370            weight: 0.8,
371            co_activations: 1,
372            created_tick: 0,
373            last_activated_tick: 0,
374        });
375
376        assert_eq!(sub.node_count(), 2);
377        assert_eq!(sub.edge_count(), 1);
378        assert_eq!(sub.get_node(&n1).unwrap().label, "cell");
379    }
380
381    #[test]
382    fn tick_advances() {
383        let mut sub = SubstrateImpl::new();
384        assert_eq!(sub.current_tick(), 0);
385        sub.advance_tick();
386        sub.advance_tick();
387        assert_eq!(sub.current_tick(), 2);
388    }
389}