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