Skip to main content

microscope_memory/
emotional.rs

1//! Emotional bias warp for Microscope Memory.
2//!
3//! Bends search space coordinates based on the emotional layer's active clusters.
4//! Does NOT override the search — warps the query point toward emotional attractors.
5//! The weight is configurable in config.toml (`search.emotional_bias_weight`).
6
7use crate::hebbian::HebbianState;
8use crate::reader::MicroscopeReader;
9use crate::LAYER_NAMES;
10
11/// The emotional layer ID (index 4 in LAYER_NAMES: "emotional").
12const EMOTIONAL_LAYER_ID: u8 = 4;
13
14/// Compute the emotional bias warp for a query point.
15/// Returns warped (x, y, z) coordinates.
16///
17/// The warp pulls the query point toward the centroid of hot emotional blocks,
18/// weighted by their Hebbian energy. With weight=0, the original coords are returned.
19pub fn apply_emotional_bias(
20    qx: f32,
21    qy: f32,
22    qz: f32,
23    weight: f32,
24    reader: &MicroscopeReader,
25    hebb: &HebbianState,
26) -> (f32, f32, f32) {
27    if weight <= 0.0 {
28        return (qx, qy, qz);
29    }
30
31    let weight = weight.clamp(0.0, 1.0);
32
33    // Find active emotional blocks and their weighted centroid
34    let mut sum_x = 0.0f32;
35    let mut sum_y = 0.0f32;
36    let mut sum_z = 0.0f32;
37    let mut total_energy = 0.0f32;
38
39    for i in 0..reader.block_count {
40        let h = reader.header(i);
41        if h.layer_id != EMOTIONAL_LAYER_ID {
42            continue;
43        }
44
45        let energy = hebb.energy(i);
46        if energy < 0.01 {
47            continue;
48        }
49
50        // Copy packed struct fields
51        let hx = h.x;
52        let hy = h.y;
53        let hz = h.z;
54
55        sum_x += hx * energy;
56        sum_y += hy * energy;
57        sum_z += hz * energy;
58        total_energy += energy;
59    }
60
61    if total_energy < 0.01 {
62        return (qx, qy, qz); // No active emotional blocks
63    }
64
65    // Emotional centroid
66    let cx = sum_x / total_energy;
67    let cy = sum_y / total_energy;
68    let cz = sum_z / total_energy;
69
70    // Warp: blend query point toward emotional centroid
71    let warped_x = qx + (cx - qx) * weight;
72    let warped_y = qy + (cy - qy) * weight;
73    let warped_z = qz + (cz - qz) * weight;
74
75    (warped_x, warped_y, warped_z)
76}
77
78/// Get the current emotional field summary:
79/// centroid of active emotional blocks, total energy, and count.
80pub fn emotional_field(reader: &MicroscopeReader, hebb: &HebbianState) -> Option<EmotionalField> {
81    let mut sum_x = 0.0f32;
82    let mut sum_y = 0.0f32;
83    let mut sum_z = 0.0f32;
84    let mut total_energy = 0.0f32;
85    let mut active_count = 0usize;
86    let mut hottest_idx: Option<(usize, f32)> = None;
87
88    for i in 0..reader.block_count {
89        let h = reader.header(i);
90        if h.layer_id != EMOTIONAL_LAYER_ID {
91            continue;
92        }
93
94        let energy = hebb.energy(i);
95        if energy < 0.01 {
96            continue;
97        }
98
99        let hx = h.x;
100        let hy = h.y;
101        let hz = h.z;
102
103        sum_x += hx * energy;
104        sum_y += hy * energy;
105        sum_z += hz * energy;
106        total_energy += energy;
107        active_count += 1;
108
109        if hottest_idx.is_none() || energy > hottest_idx.unwrap().1 {
110            hottest_idx = Some((i, energy));
111        }
112    }
113
114    if active_count == 0 {
115        return None;
116    }
117
118    Some(EmotionalField {
119        centroid: (
120            sum_x / total_energy,
121            sum_y / total_energy,
122            sum_z / total_energy,
123        ),
124        total_energy,
125        active_blocks: active_count,
126        hottest_block: hottest_idx,
127    })
128}
129
130pub struct EmotionalField {
131    pub centroid: (f32, f32, f32),
132    pub total_energy: f32,
133    pub active_blocks: usize,
134    pub hottest_block: Option<(usize, f32)>,
135}
136
137/// Verify the emotional layer ID matches our constant.
138pub fn emotional_layer_name() -> &'static str {
139    LAYER_NAMES
140        .get(EMOTIONAL_LAYER_ID as usize)
141        .unwrap_or(&"emotional")
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    #[test]
148    fn test_no_warp_at_zero_weight() {
149        let (x, _y, _z) = apply_emotional_bias_pure(0.5, 0.5, 0.5, 0.0, None);
150        assert!((x - 0.5).abs() < 0.001);
151    }
152
153    #[test]
154    fn test_warp_with_centroid() {
155        // Test the pure math: warp toward centroid
156        let centroid = Some((0.2, 0.3, 0.4));
157        let (x, y, z) = apply_emotional_bias_pure(0.5, 0.5, 0.5, 0.5, centroid);
158        // Should move halfway toward centroid
159        assert!((x - 0.35).abs() < 0.001);
160        assert!((y - 0.4).abs() < 0.001);
161        assert!((z - 0.45).abs() < 0.001);
162    }
163
164    #[test]
165    fn test_warp_full_weight() {
166        let centroid = Some((0.2, 0.3, 0.4));
167        let (x, y, z) = apply_emotional_bias_pure(0.5, 0.5, 0.5, 1.0, centroid);
168        // Should move fully to centroid
169        assert!((x - 0.2).abs() < 0.001);
170        assert!((y - 0.3).abs() < 0.001);
171        assert!((z - 0.4).abs() < 0.001);
172    }
173
174    #[test]
175    fn test_no_centroid() {
176        let (x, _y, _z) = apply_emotional_bias_pure(0.5, 0.5, 0.5, 1.0, None);
177        // No centroid → no warp
178        assert!((x - 0.5).abs() < 0.001);
179    }
180
181    #[test]
182    fn test_emotional_layer_name() {
183        assert_eq!(emotional_layer_name(), "emotional");
184    }
185
186    /// Pure math version for unit testing without MicroscopeReader.
187    fn apply_emotional_bias_pure(
188        qx: f32,
189        qy: f32,
190        qz: f32,
191        weight: f32,
192        centroid: Option<(f32, f32, f32)>,
193    ) -> (f32, f32, f32) {
194        if weight <= 0.0 {
195            return (qx, qy, qz);
196        }
197        match centroid {
198            None => (qx, qy, qz),
199            Some((cx, cy, cz)) => {
200                let w = weight.clamp(0.0, 1.0);
201                (qx + (cx - qx) * w, qy + (cy - qy) * w, qz + (cz - qz) * w)
202            }
203        }
204    }
205}