1use std::collections::HashMap;
15use std::fs;
16use std::io::Write;
17use std::path::Path;
18use std::time::{SystemTime, UNIX_EPOCH};
19
20const ACTIVATION_RECORD_BYTES: usize = 32; const COACTIVATION_RECORD_BYTES: usize = 20; const ENERGY_HALF_LIFE_MS: f64 = 86_400_000.0; const DRIFT_RATE: f32 = 0.01; const DRIFT_MAX: f32 = 0.1; #[repr(C)]
32#[derive(Clone, Copy, Debug)]
33pub struct ActivationRecord {
34 pub activation_count: u32,
35 pub last_activated_ms: u64,
36 pub drift_x: f32,
37 pub drift_y: f32,
38 pub drift_z: f32,
39 pub energy: f32,
40 pub _pad: u32,
41}
42
43impl Default for ActivationRecord {
44 fn default() -> Self {
45 Self {
46 activation_count: 0,
47 last_activated_ms: 0,
48 drift_x: 0.0,
49 drift_y: 0.0,
50 drift_z: 0.0,
51 energy: 0.0,
52 _pad: 0,
53 }
54 }
55}
56
57#[repr(C)]
59#[derive(Clone, Copy, Debug)]
60pub struct CoactivationPair {
61 pub block_a: u32,
62 pub block_b: u32,
63 pub count: u32,
64 pub last_ts_ms: u64,
65}
66
67#[derive(Clone, Debug)]
70pub struct ActivationFingerprint {
71 pub timestamp_ms: u64,
72 pub query_hash: u64,
73 pub activations: Vec<(u32, f32)>, }
75
76pub struct HebbianState {
80 pub activations: Vec<ActivationRecord>,
81 pub coactivations: HashMap<(u32, u32), CoactivationPair>,
82 pub fingerprints: Vec<ActivationFingerprint>,
83}
84
85impl HebbianState {
86 pub fn load_or_init(output_dir: &Path, block_count: usize) -> Self {
88 let activations = load_activations(output_dir, block_count);
89 let coactivations = load_coactivations(output_dir);
90 let fingerprints = load_fingerprints(output_dir);
91
92 Self {
93 activations,
94 coactivations,
95 fingerprints,
96 }
97 }
98
99 pub fn record_activation(&mut self, results: &[(u32, f32)], query_hash: u64) {
102 let now_ms = now_epoch_ms();
103
104 for &(block_idx, _score) in results {
106 let idx = block_idx as usize;
107 if idx < self.activations.len() {
108 let rec = &mut self.activations[idx];
109 rec.activation_count += 1;
110 rec.last_activated_ms = now_ms;
111 rec.energy = 1.0; }
113 }
114
115 for i in 0..results.len() {
117 for j in (i + 1)..results.len() {
118 let a = results[i].0.min(results[j].0);
119 let b = results[i].0.max(results[j].0);
120 let pair = self
121 .coactivations
122 .entry((a, b))
123 .or_insert(CoactivationPair {
124 block_a: a,
125 block_b: b,
126 count: 0,
127 last_ts_ms: 0,
128 });
129 pair.count += 1;
130 pair.last_ts_ms = now_ms;
131 }
132 }
133
134 self.fingerprints.push(ActivationFingerprint {
136 timestamp_ms: now_ms,
137 query_hash,
138 activations: results.to_vec(),
139 });
140
141 if self.fingerprints.len() > 1000 {
143 self.fingerprints.drain(0..self.fingerprints.len() - 1000);
144 }
145 }
146
147 pub fn apply_drift(&mut self, headers: &[(f32, f32, f32)]) {
150 let now_ms = now_epoch_ms();
151
152 for rec in &mut self.activations {
154 if rec.energy > 0.0 && rec.last_activated_ms > 0 {
155 let elapsed_ms = (now_ms - rec.last_activated_ms) as f64;
156 rec.energy *= (-(elapsed_ms / ENERGY_HALF_LIFE_MS) * std::f64::consts::LN_2) as f32;
157 rec.energy = rec.energy.exp();
158 }
159 }
160
161 for pair in self.coactivations.values() {
163 let a = pair.block_a as usize;
164 let b = pair.block_b as usize;
165
166 if a >= headers.len() || b >= headers.len() {
167 continue;
168 }
169
170 let strength = (pair.count as f32).ln().min(5.0) * DRIFT_RATE;
172 if strength < 0.001 {
173 continue;
174 }
175
176 let (ax, ay, az) = headers[a];
177 let (bx, by, bz) = headers[b];
178
179 let dx = bx + self.activations[b].drift_x - (ax + self.activations[a].drift_x);
181 let dy = by + self.activations[b].drift_y - (ay + self.activations[a].drift_y);
182 let dz = bz + self.activations[b].drift_z - (az + self.activations[a].drift_z);
183
184 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
185 if dist < 0.001 {
186 continue;
187 }
188
189 let nx = dx / dist * strength;
191 let ny = dy / dist * strength;
192 let nz = dz / dist * strength;
193
194 self.activations[a].drift_x = clamp_drift(self.activations[a].drift_x + nx);
195 self.activations[a].drift_y = clamp_drift(self.activations[a].drift_y + ny);
196 self.activations[a].drift_z = clamp_drift(self.activations[a].drift_z + nz);
197
198 self.activations[b].drift_x = clamp_drift(self.activations[b].drift_x - nx);
199 self.activations[b].drift_y = clamp_drift(self.activations[b].drift_y - ny);
200 self.activations[b].drift_z = clamp_drift(self.activations[b].drift_z - nz);
201 }
202 }
203
204 pub fn effective_coords(&self, block_idx: usize, original: (f32, f32, f32)) -> (f32, f32, f32) {
206 if block_idx < self.activations.len() {
207 let rec = &self.activations[block_idx];
208 (
209 original.0 + rec.drift_x,
210 original.1 + rec.drift_y,
211 original.2 + rec.drift_z,
212 )
213 } else {
214 original
215 }
216 }
217
218 pub fn energy(&self, block_idx: usize) -> f32 {
220 if block_idx < self.activations.len() {
221 let rec = &self.activations[block_idx];
222 if rec.energy > 0.0 && rec.last_activated_ms > 0 {
223 let elapsed_ms = (now_epoch_ms() - rec.last_activated_ms) as f64;
224 let decay = (-(elapsed_ms / ENERGY_HALF_LIFE_MS) * std::f64::consts::LN_2).exp();
225 decay as f32
226 } else {
227 0.0
228 }
229 } else {
230 0.0
231 }
232 }
233
234 pub fn save(&self, output_dir: &Path) -> Result<(), String> {
236 save_activations(output_dir, &self.activations)?;
237 save_coactivations(output_dir, &self.coactivations)?;
238 save_fingerprints(output_dir, &self.fingerprints)?;
239 Ok(())
240 }
241
242 pub fn stats(&self) -> HebbianStats {
244 let active_blocks = self
245 .activations
246 .iter()
247 .filter(|r| r.activation_count > 0)
248 .count();
249 let total_activations: u64 = self
250 .activations
251 .iter()
252 .map(|r| r.activation_count as u64)
253 .sum();
254 let hot_blocks = self
255 .activations
256 .iter()
257 .enumerate()
258 .filter(|(i, _)| self.energy(*i) > 0.1)
259 .count();
260 let drifted_blocks = self
261 .activations
262 .iter()
263 .filter(|r| {
264 r.drift_x.abs() > 0.001 || r.drift_y.abs() > 0.001 || r.drift_z.abs() > 0.001
265 })
266 .count();
267
268 HebbianStats {
269 block_count: self.activations.len(),
270 active_blocks,
271 total_activations,
272 hot_blocks,
273 coactivation_pairs: self.coactivations.len(),
274 fingerprint_count: self.fingerprints.len(),
275 drifted_blocks,
276 }
277 }
278
279 pub fn latest_fingerprint(&self) -> Option<&ActivationFingerprint> {
281 self.fingerprints.last()
282 }
283
284 pub fn hottest_blocks(&self, n: usize) -> Vec<(usize, f32)> {
286 let mut blocks: Vec<(usize, f32)> = (0..self.activations.len())
287 .map(|i| (i, self.energy(i)))
288 .filter(|(_, e)| *e > 0.01)
289 .collect();
290 blocks.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
291 blocks.truncate(n);
292 blocks
293 }
294
295 pub fn strongest_pairs(&self, n: usize) -> Vec<&CoactivationPair> {
297 let mut pairs: Vec<&CoactivationPair> = self.coactivations.values().collect();
298 pairs.sort_by(|a, b| b.count.cmp(&a.count));
299 pairs.truncate(n);
300 pairs
301 }
302}
303
304pub struct HebbianStats {
305 pub block_count: usize,
306 pub active_blocks: usize,
307 pub total_activations: u64,
308 pub hot_blocks: usize,
309 pub coactivation_pairs: usize,
310 pub fingerprint_count: usize,
311 pub drifted_blocks: usize,
312}
313
314fn read_u32(b: &[u8], off: usize) -> u32 {
317 u32::from_le_bytes(b[off..off + 4].try_into().unwrap())
318}
319fn read_u64(b: &[u8], off: usize) -> u64 {
320 u64::from_le_bytes(b[off..off + 8].try_into().unwrap())
321}
322fn read_f32(b: &[u8], off: usize) -> f32 {
323 f32::from_le_bytes(b[off..off + 4].try_into().unwrap())
324}
325
326fn load_activations(output_dir: &Path, block_count: usize) -> Vec<ActivationRecord> {
327 let path = output_dir.join("activations.bin");
328 if let Ok(data) = fs::read(&path) {
329 if data.len() >= 8 && &data[0..4] == b"HEB1" {
330 let stored_count = read_u32(&data, 4) as usize;
331 let expected_size = 8 + stored_count * ACTIVATION_RECORD_BYTES;
332 if data.len() >= expected_size {
333 let mut records = Vec::with_capacity(block_count.max(stored_count));
334 for i in 0..stored_count {
335 let off = 8 + i * ACTIVATION_RECORD_BYTES;
336 records.push(ActivationRecord {
337 activation_count: read_u32(&data, off),
338 last_activated_ms: read_u64(&data, off + 4),
339 drift_x: read_f32(&data, off + 12),
340 drift_y: read_f32(&data, off + 16),
341 drift_z: read_f32(&data, off + 20),
342 energy: read_f32(&data, off + 24),
343 _pad: read_u32(&data, off + 28),
344 });
345 }
346 records.resize(block_count.max(stored_count), ActivationRecord::default());
347 return records;
348 }
349 }
350 }
351 vec![ActivationRecord::default(); block_count]
352}
353
354fn save_activations(output_dir: &Path, records: &[ActivationRecord]) -> Result<(), String> {
355 let path = output_dir.join("activations.bin");
356 let mut buf = Vec::with_capacity(8 + records.len() * ACTIVATION_RECORD_BYTES);
357 buf.extend_from_slice(b"HEB1");
358 buf.extend_from_slice(&(records.len() as u32).to_le_bytes());
359 for rec in records {
360 buf.extend_from_slice(&rec.activation_count.to_le_bytes());
361 buf.extend_from_slice(&rec.last_activated_ms.to_le_bytes());
362 buf.extend_from_slice(&rec.drift_x.to_le_bytes());
363 buf.extend_from_slice(&rec.drift_y.to_le_bytes());
364 buf.extend_from_slice(&rec.drift_z.to_le_bytes());
365 buf.extend_from_slice(&rec.energy.to_le_bytes());
366 buf.extend_from_slice(&rec._pad.to_le_bytes());
367 }
368 fs::write(&path, &buf).map_err(|e| format!("write activations.bin: {}", e))
369}
370
371fn load_coactivations(output_dir: &Path) -> HashMap<(u32, u32), CoactivationPair> {
372 let path = output_dir.join("coactivations.bin");
373 let mut map = HashMap::new();
374 if let Ok(data) = fs::read(&path) {
375 if data.len() >= 8 && &data[0..4] == b"COA1" {
376 let pair_count = read_u32(&data, 4) as usize;
377 for i in 0..pair_count {
378 let off = 8 + i * COACTIVATION_RECORD_BYTES;
379 if off + COACTIVATION_RECORD_BYTES > data.len() {
380 break;
381 }
382 let pair = CoactivationPair {
383 block_a: read_u32(&data, off),
384 block_b: read_u32(&data, off + 4),
385 count: read_u32(&data, off + 8),
386 last_ts_ms: read_u64(&data, off + 12),
387 };
388 map.insert((pair.block_a, pair.block_b), pair);
389 }
390 }
391 }
392 map
393}
394
395fn save_coactivations(
396 output_dir: &Path,
397 pairs: &HashMap<(u32, u32), CoactivationPair>,
398) -> Result<(), String> {
399 let path = output_dir.join("coactivations.bin");
400 let mut buf = Vec::with_capacity(8 + pairs.len() * COACTIVATION_RECORD_BYTES);
401 buf.extend_from_slice(b"COA1");
402 buf.extend_from_slice(&(pairs.len() as u32).to_le_bytes());
403 for pair in pairs.values() {
404 buf.extend_from_slice(&pair.block_a.to_le_bytes());
405 buf.extend_from_slice(&pair.block_b.to_le_bytes());
406 buf.extend_from_slice(&pair.count.to_le_bytes());
407 buf.extend_from_slice(&pair.last_ts_ms.to_le_bytes());
408 }
409 fs::write(&path, &buf).map_err(|e| format!("write coactivations.bin: {}", e))
410}
411
412fn load_fingerprints(output_dir: &Path) -> Vec<ActivationFingerprint> {
413 let path = output_dir.join("fingerprints.bin");
414 let mut fingerprints = Vec::new();
415 if let Ok(data) = fs::read(&path) {
416 if data.len() >= 8 && &data[0..4] == b"FPR1" {
417 let count = u32::from_le_bytes(data[4..8].try_into().unwrap()) as usize;
418 let mut pos = 8;
419 for _ in 0..count {
420 if pos + 18 > data.len() {
421 break;
422 }
423 let timestamp_ms = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
424 let query_hash = u64::from_le_bytes(data[pos + 8..pos + 16].try_into().unwrap());
425 let activated_count =
426 u16::from_le_bytes(data[pos + 16..pos + 18].try_into().unwrap()) as usize;
427 pos += 18;
428
429 let mut activations = Vec::with_capacity(activated_count);
430 for _ in 0..activated_count {
431 if pos + 8 > data.len() {
432 break;
433 }
434 let block_idx = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
435 let score = f32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap());
436 activations.push((block_idx, score));
437 pos += 8;
438 }
439
440 fingerprints.push(ActivationFingerprint {
441 timestamp_ms,
442 query_hash,
443 activations,
444 });
445 }
446 }
447 }
448 fingerprints
449}
450
451fn save_fingerprints(
452 output_dir: &Path,
453 fingerprints: &[ActivationFingerprint],
454) -> Result<(), String> {
455 let path = output_dir.join("fingerprints.bin");
456 let mut file =
457 fs::File::create(&path).map_err(|e| format!("create fingerprints.bin: {}", e))?;
458 file.write_all(b"FPR1")
459 .map_err(|e| format!("write magic: {}", e))?;
460 file.write_all(&(fingerprints.len() as u32).to_le_bytes())
461 .map_err(|e| format!("write count: {}", e))?;
462 for fp in fingerprints {
463 file.write_all(&fp.timestamp_ms.to_le_bytes())
464 .map_err(|e| format!("write ts: {}", e))?;
465 file.write_all(&fp.query_hash.to_le_bytes())
466 .map_err(|e| format!("write hash: {}", e))?;
467 file.write_all(&(fp.activations.len() as u16).to_le_bytes())
468 .map_err(|e| format!("write count: {}", e))?;
469 for &(block_idx, score) in &fp.activations {
470 file.write_all(&block_idx.to_le_bytes())
471 .map_err(|e| format!("write idx: {}", e))?;
472 file.write_all(&score.to_le_bytes())
473 .map_err(|e| format!("write score: {}", e))?;
474 }
475 }
476 Ok(())
477}
478
479fn now_epoch_ms() -> u64 {
482 SystemTime::now()
483 .duration_since(UNIX_EPOCH)
484 .unwrap_or_default()
485 .as_millis() as u64
486}
487
488pub fn now_epoch_ms_pub() -> u64 {
490 now_epoch_ms()
491}
492
493fn clamp_drift(v: f32) -> f32 {
494 v.clamp(-DRIFT_MAX, DRIFT_MAX)
495}
496
497pub fn query_hash(query: &str) -> u64 {
499 let mut h: u64 = 0xcbf29ce484222325;
500 for &b in query.as_bytes() {
501 h = h.wrapping_mul(0x100000001b3) ^ b as u64;
502 }
503 h
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509
510 #[test]
511 fn test_serialization_sizes() {
512 assert_eq!(ACTIVATION_RECORD_BYTES, 32); assert_eq!(COACTIVATION_RECORD_BYTES, 20); }
516
517 #[test]
518 fn test_record_activation() {
519 let mut state = HebbianState {
520 activations: vec![ActivationRecord::default(); 10],
521 coactivations: HashMap::new(),
522 fingerprints: Vec::new(),
523 };
524
525 state.record_activation(&[(0, 0.5), (3, 0.3), (7, 0.1)], 12345);
526
527 assert_eq!(state.activations[0].activation_count, 1);
528 assert_eq!(state.activations[3].activation_count, 1);
529 assert_eq!(state.activations[7].activation_count, 1);
530 assert_eq!(state.activations[1].activation_count, 0);
531
532 assert_eq!(state.coactivations.len(), 3);
534 assert!(state.coactivations.contains_key(&(0, 3)));
535 assert!(state.coactivations.contains_key(&(0, 7)));
536 assert!(state.coactivations.contains_key(&(3, 7)));
537
538 assert_eq!(state.fingerprints.len(), 1);
540 assert_eq!(state.fingerprints[0].query_hash, 12345);
541 assert_eq!(state.fingerprints[0].activations.len(), 3);
542 }
543
544 #[test]
545 fn test_repeated_coactivation() {
546 let mut state = HebbianState {
547 activations: vec![ActivationRecord::default(); 5],
548 coactivations: HashMap::new(),
549 fingerprints: Vec::new(),
550 };
551
552 state.record_activation(&[(1, 0.5), (2, 0.3)], 100);
553 state.record_activation(&[(1, 0.4), (2, 0.6)], 200);
554 state.record_activation(&[(1, 0.3), (2, 0.2)], 300);
555
556 assert_eq!(state.activations[1].activation_count, 3);
557 assert_eq!(state.coactivations[&(1, 2)].count, 3);
558 }
559
560 #[test]
561 fn test_drift_application() {
562 let mut state = HebbianState {
563 activations: vec![ActivationRecord::default(); 3],
564 coactivations: HashMap::new(),
565 fingerprints: Vec::new(),
566 };
567
568 for _ in 0..20 {
570 state.record_activation(&[(0, 0.5), (2, 0.5)], 42);
571 }
572
573 let headers = vec![(0.0, 0.0, 0.0), (0.5, 0.5, 0.5), (1.0, 1.0, 1.0)];
574 state.apply_drift(&headers);
575
576 assert!(state.activations[0].drift_x > 0.0);
578 assert!(state.activations[0].drift_y > 0.0);
579 assert!(state.activations[0].drift_z > 0.0);
580 assert!(state.activations[2].drift_x < 0.0);
581 assert!(state.activations[2].drift_y < 0.0);
582 assert!(state.activations[2].drift_z < 0.0);
583 }
584
585 #[test]
586 fn test_effective_coords() {
587 let mut state = HebbianState {
588 activations: vec![ActivationRecord::default(); 2],
589 coactivations: HashMap::new(),
590 fingerprints: Vec::new(),
591 };
592
593 state.activations[0].drift_x = 0.05;
594 state.activations[0].drift_y = -0.03;
595 state.activations[0].drift_z = 0.01;
596
597 let (x, y, z) = state.effective_coords(0, (0.2, 0.3, 0.4));
598 assert!((x - 0.25).abs() < 0.001);
599 assert!((y - 0.27).abs() < 0.001);
600 assert!((z - 0.41).abs() < 0.001);
601 }
602
603 #[test]
604 fn test_save_load_roundtrip() {
605 let tmp = tempfile::tempdir().expect("create temp dir");
606 let dir = tmp.path();
607
608 let mut state = HebbianState {
609 activations: vec![ActivationRecord::default(); 5],
610 coactivations: HashMap::new(),
611 fingerprints: Vec::new(),
612 };
613
614 state.record_activation(&[(0, 0.5), (2, 0.3), (4, 0.1)], 999);
615 state.record_activation(&[(1, 0.8), (3, 0.2)], 888);
616
617 state.save(dir).expect("save");
618
619 let loaded = HebbianState::load_or_init(dir, 5);
620 assert_eq!(loaded.activations[0].activation_count, 1);
621 assert_eq!(loaded.activations[1].activation_count, 1);
622 assert_eq!(loaded.coactivations.len(), 4); assert_eq!(loaded.fingerprints.len(), 2);
624 assert_eq!(loaded.fingerprints[0].query_hash, 999);
625 assert_eq!(loaded.fingerprints[1].query_hash, 888);
626 }
627
628 #[test]
629 fn test_clamp_drift() {
630 assert_eq!(clamp_drift(0.05), 0.05);
631 assert_eq!(clamp_drift(0.2), DRIFT_MAX);
632 assert_eq!(clamp_drift(-0.2), -DRIFT_MAX);
633 }
634
635 #[test]
636 fn test_query_hash_deterministic() {
637 assert_eq!(query_hash("hello"), query_hash("hello"));
638 assert_ne!(query_hash("hello"), query_hash("world"));
639 }
640
641 #[test]
642 fn test_hottest_blocks() {
643 let mut state = HebbianState {
644 activations: vec![ActivationRecord::default(); 5],
645 coactivations: HashMap::new(),
646 fingerprints: Vec::new(),
647 };
648
649 state.record_activation(&[(0, 1.0), (2, 0.5)], 1);
650
651 let hot = state.hottest_blocks(10);
652 assert!(!hot.is_empty());
653 assert!(hot.iter().any(|(idx, _)| *idx == 0));
655 assert!(hot.iter().any(|(idx, _)| *idx == 2));
656 }
657
658 #[test]
659 fn test_stats() {
660 let mut state = HebbianState {
661 activations: vec![ActivationRecord::default(); 10],
662 coactivations: HashMap::new(),
663 fingerprints: Vec::new(),
664 };
665
666 state.record_activation(&[(0, 1.0), (5, 0.5)], 42);
667
668 let stats = state.stats();
669 assert_eq!(stats.block_count, 10);
670 assert_eq!(stats.active_blocks, 2);
671 assert_eq!(stats.total_activations, 2);
672 assert_eq!(stats.coactivation_pairs, 1);
673 assert_eq!(stats.fingerprint_count, 1);
674 }
675}