Skip to main content

phago_core/
signal.rs

1//! Signal utilities — helpers for working with signals and gradients.
2
3use crate::types::*;
4
5impl Signal {
6    /// Create a new signal.
7    pub fn new(signal_type: SignalType, intensity: f64, position: Position, emitter: AgentId, tick: Tick) -> Self {
8        Self {
9            signal_type,
10            intensity,
11            position,
12            emitter,
13            tick,
14        }
15    }
16
17    /// Apply decay to this signal's intensity.
18    pub fn decay(&mut self, rate: f64) {
19        self.intensity *= 1.0 - rate;
20    }
21
22    /// Whether this signal has decayed below a threshold.
23    pub fn is_below_threshold(&self, threshold: f64) -> bool {
24        self.intensity < threshold
25    }
26}
27
28impl Gradient {
29    /// Create a new gradient.
30    pub fn new(signal_type: SignalType, direction: Position, magnitude: f64) -> Self {
31        Self {
32            signal_type,
33            direction,
34            magnitude,
35        }
36    }
37}
38
39/// Compute the gradient direction from a set of nearby signals.
40///
41/// Returns the weighted average direction toward higher signal concentration.
42/// This is the computational analog of how a cell senses a chemical gradient
43/// by comparing receptor binding rates across its surface.
44pub fn compute_gradient(signals: &[&Signal], from: &Position) -> Option<Gradient> {
45    if signals.is_empty() {
46        return None;
47    }
48
49    let signal_type = signals[0].signal_type.clone();
50    let mut weighted_x = 0.0;
51    let mut weighted_y = 0.0;
52    let mut total_intensity = 0.0;
53
54    for signal in signals {
55        let dx = signal.position.x - from.x;
56        let dy = signal.position.y - from.y;
57        let dist = (dx * dx + dy * dy).sqrt().max(0.001); // Avoid division by zero
58
59        // Weight by intensity, inversely by distance
60        let weight = signal.intensity / dist;
61        weighted_x += dx * weight;
62        weighted_y += dy * weight;
63        total_intensity += signal.intensity;
64    }
65
66    if total_intensity < f64::EPSILON {
67        return None;
68    }
69
70    let magnitude = (weighted_x * weighted_x + weighted_y * weighted_y).sqrt();
71    if magnitude < f64::EPSILON {
72        return None;
73    }
74
75    Some(Gradient::new(
76        signal_type,
77        Position::new(weighted_x / magnitude, weighted_y / magnitude),
78        magnitude,
79    ))
80}