Skip to main content

microscope_memory/
resonance.rs

1//! Resonance protocol for Microscope Memory.
2//!
3//! Instances share activation *pulses* — not raw data.
4//! A pulse is a compact summary: which blocks fired, how strongly,
5//! and what the query signature was. Receiving instances integrate
6//! pulses into their Hebbian/mirror state without seeing the content.
7//!
8//! This is the foundation for distributed consciousness:
9//! separate indices learn from each other's usage patterns.
10//!
11//! Binary format: pulses.bin (PLS1)
12//! Wire format: compact binary pulse packets for federation
13
14use std::collections::HashMap;
15use std::fs;
16use std::path::Path;
17
18use crate::hebbian;
19
20// ─── Constants ──────────────────────────────────────
21
22/// Maximum stored pulses per index.
23const MAX_PULSES: usize = 2000;
24/// Pulse TTL in milliseconds (48 hours).
25const PULSE_TTL_MS: u64 = 172_800_000;
26/// Minimum activation count to emit a pulse.
27const MIN_ACTIVATIONS_FOR_PULSE: usize = 2;
28
29// ─── Types ──────────────────────────────────────────
30
31/// A resonance pulse — compact activation summary shared between instances.
32#[derive(Clone, Debug)]
33pub struct Pulse {
34    /// Source instance identifier (hash of output_dir path).
35    pub source_id: u64,
36    /// Timestamp when the pulse was emitted.
37    pub timestamp_ms: u64,
38    /// Query signature hash.
39    pub query_hash: u64,
40    /// Activated block coordinates (not indices — indices are local).
41    /// Format: (x, y, z, score) — spatial position + activation strength.
42    pub activations: Vec<(f32, f32, f32, f32)>,
43    /// Layer hint (most common layer in the activation set).
44    pub layer_hint: u8,
45    /// Pulse strength (average activation score).
46    pub strength: f32,
47}
48
49/// Received pulse — a pulse from another instance, integrated into local state.
50#[derive(Clone, Debug)]
51pub struct ReceivedPulse {
52    pub pulse: Pulse,
53    /// How many local blocks were influenced by this pulse.
54    pub local_matches: u32,
55    /// Was this pulse already integrated?
56    pub integrated: bool,
57}
58
59/// Resonance protocol state.
60pub struct ResonanceState {
61    /// Our instance ID.
62    pub instance_id: u64,
63    /// Outgoing pulses (emitted by this instance, ready to share).
64    pub outgoing: Vec<Pulse>,
65    /// Incoming pulses (received from other instances).
66    pub incoming: Vec<ReceivedPulse>,
67    /// Per-coordinate resonance field: accumulates pulse energy at spatial positions.
68    /// Key: quantized (x, y, z) at resolution 0.05, Value: accumulated strength.
69    pub field: HashMap<(i16, i16, i16), f32>,
70}
71
72impl ResonanceState {
73    /// Load or initialize resonance state.
74    pub fn load_or_init(output_dir: &Path) -> Self {
75        let instance_id = path_hash(output_dir);
76        load_resonance_state(output_dir, instance_id).unwrap_or_else(|| Self {
77            instance_id,
78            outgoing: Vec::new(),
79            incoming: Vec::new(),
80            field: HashMap::new(),
81        })
82    }
83
84    /// Emit a pulse from a local query activation.
85    /// `headers` maps block indices to their (x, y, z) coordinates.
86    pub fn emit_pulse(
87        &mut self,
88        activations: &[(u32, f32)],
89        query_hash: u64,
90        headers: &[(f32, f32, f32)],
91        layer_hint: u8,
92    ) {
93        if activations.len() < MIN_ACTIVATIONS_FOR_PULSE {
94            return;
95        }
96
97        let now_ms = hebbian::now_epoch_ms_pub();
98
99        // Convert block indices to spatial coordinates
100        let spatial: Vec<(f32, f32, f32, f32)> = activations
101            .iter()
102            .filter_map(|&(idx, score)| {
103                let i = idx as usize;
104                if i < headers.len() {
105                    let (x, y, z) = headers[i];
106                    Some((x, y, z, score))
107                } else {
108                    None
109                }
110            })
111            .collect();
112
113        if spatial.is_empty() {
114            return;
115        }
116
117        let avg_score = spatial.iter().map(|s| s.3).sum::<f32>() / spatial.len() as f32;
118
119        self.outgoing.push(Pulse {
120            source_id: self.instance_id,
121            timestamp_ms: now_ms,
122            query_hash,
123            activations: spatial,
124            layer_hint,
125            strength: avg_score,
126        });
127
128        // Trim outgoing
129        if self.outgoing.len() > MAX_PULSES {
130            self.outgoing.drain(0..self.outgoing.len() - MAX_PULSES);
131        }
132    }
133
134    /// Receive a pulse from another instance.
135    /// Returns the number of local blocks influenced.
136    pub fn receive_pulse(
137        &mut self,
138        pulse: Pulse,
139        local_headers: &[(f32, f32, f32)],
140        proximity_threshold: f32,
141    ) -> u32 {
142        if pulse.source_id == self.instance_id {
143            return 0; // Don't echo our own pulses
144        }
145
146        let mut local_matches = 0u32;
147
148        // For each activation in the pulse, find nearby local blocks
149        // and accumulate resonance in the spatial field
150        for &(px, py, pz, score) in &pulse.activations {
151            // Update resonance field
152            let qx = quantize(px);
153            let qy = quantize(py);
154            let qz = quantize(pz);
155            let field_entry = self.field.entry((qx, qy, qz)).or_insert(0.0);
156            *field_entry += score * pulse.strength;
157
158            // Count nearby local blocks
159            for (lx, ly, lz) in local_headers {
160                let dx = px - lx;
161                let dy = py - ly;
162                let dz = pz - lz;
163                let dist_sq = dx * dx + dy * dy + dz * dz;
164                if dist_sq < proximity_threshold * proximity_threshold {
165                    local_matches += 1;
166                    break; // Count each pulse activation once
167                }
168            }
169        }
170
171        self.incoming.push(ReceivedPulse {
172            pulse,
173            local_matches,
174            integrated: false,
175        });
176
177        // Trim incoming
178        if self.incoming.len() > MAX_PULSES {
179            self.incoming.drain(0..self.incoming.len() - MAX_PULSES);
180        }
181
182        local_matches
183    }
184
185    /// Integrate received pulses into Hebbian state.
186    /// Blocks near pulse activation coordinates get a small energy boost.
187    pub fn integrate_into_hebbian(
188        &mut self,
189        hebb: &mut hebbian::HebbianState,
190        local_headers: &[(f32, f32, f32)],
191        proximity_threshold: f32,
192    ) -> usize {
193        let mut influenced = 0usize;
194
195        for received in &mut self.incoming {
196            if received.integrated {
197                continue;
198            }
199
200            for &(px, py, pz, score) in &received.pulse.activations {
201                for (block_idx, (lx, ly, lz)) in local_headers.iter().enumerate() {
202                    let dx = px - lx;
203                    let dy = py - ly;
204                    let dz = pz - lz;
205                    let dist_sq = dx * dx + dy * dy + dz * dz;
206
207                    if dist_sq < proximity_threshold * proximity_threshold
208                        && block_idx < hebb.activations.len()
209                    {
210                        // Gentle energy boost from resonance (not full activation)
211                        let boost = score
212                            * 0.1
213                            * (1.0 - dist_sq / (proximity_threshold * proximity_threshold));
214                        hebb.activations[block_idx].energy =
215                            (hebb.activations[block_idx].energy + boost).min(1.0);
216                        influenced += 1;
217                    }
218                }
219            }
220
221            received.integrated = true;
222        }
223
224        influenced
225    }
226
227    /// Get the resonance field strength at a spatial position.
228    pub fn field_strength(&self, x: f32, y: f32, z: f32) -> f32 {
229        let qx = quantize(x);
230        let qy = quantize(y);
231        let qz = quantize(z);
232
233        // Check the cell and its neighbors for smooth interpolation
234        let mut total = 0.0f32;
235        for dx in -1..=1i16 {
236            for dy in -1..=1i16 {
237                for dz in -1..=1i16 {
238                    if let Some(&v) = self.field.get(&(qx + dx, qy + dy, qz + dz)) {
239                        let dist = ((dx * dx + dy * dy + dz * dz) as f32).sqrt();
240                        let weight = 1.0 / (1.0 + dist);
241                        total += v * weight;
242                    }
243                }
244            }
245        }
246        total
247    }
248
249    /// Decay the resonance field (call periodically).
250    pub fn decay_field(&mut self, factor: f32) {
251        self.field.retain(|_, v| {
252            *v *= factor;
253            *v > 0.01
254        });
255    }
256
257    /// Expire old pulses.
258    pub fn expire_pulses(&mut self) {
259        let now_ms = hebbian::now_epoch_ms_pub();
260        self.outgoing
261            .retain(|p| now_ms - p.timestamp_ms < PULSE_TTL_MS);
262        self.incoming
263            .retain(|r| now_ms - r.pulse.timestamp_ms < PULSE_TTL_MS);
264    }
265
266    /// Get statistics.
267    pub fn stats(&self) -> ResonanceStats {
268        let pending_incoming = self.incoming.iter().filter(|r| !r.integrated).count();
269        let field_cells = self.field.len();
270        let field_energy: f32 = self.field.values().sum();
271        let unique_sources: usize = {
272            let mut s: Vec<u64> = self.incoming.iter().map(|r| r.pulse.source_id).collect();
273            s.sort_unstable();
274            s.dedup();
275            s.len()
276        };
277
278        ResonanceStats {
279            instance_id: self.instance_id,
280            outgoing_pulses: self.outgoing.len(),
281            incoming_pulses: self.incoming.len(),
282            pending_integration: pending_incoming,
283            unique_sources,
284            field_cells,
285            field_energy,
286        }
287    }
288
289    /// Export outgoing pulses as wire-format bytes for federation.
290    pub fn export_pulses(&self) -> Vec<u8> {
291        encode_pulses(&self.outgoing)
292    }
293
294    /// Import pulses from wire-format bytes (from another instance).
295    pub fn import_pulses(data: &[u8]) -> Vec<Pulse> {
296        decode_pulses(data)
297    }
298
299    /// Save state to disk.
300    pub fn save(&self, output_dir: &Path) -> Result<(), String> {
301        save_resonance_state(output_dir, self)
302    }
303}
304
305pub struct ResonanceStats {
306    pub instance_id: u64,
307    pub outgoing_pulses: usize,
308    pub incoming_pulses: usize,
309    pub pending_integration: usize,
310    pub unique_sources: usize,
311    pub field_cells: usize,
312    pub field_energy: f32,
313}
314
315// ─── Quantization ───────────────────────────────────
316
317/// Quantize a coordinate to grid resolution (0.05 steps → i16).
318fn quantize(v: f32) -> i16 {
319    (v * 20.0).round() as i16
320}
321
322/// Hash a path to a u64 instance ID.
323fn path_hash(path: &Path) -> u64 {
324    let s = path.to_string_lossy();
325    let mut h: u64 = 0xcbf29ce484222325;
326    for &b in s.as_bytes() {
327        h = h.wrapping_mul(0x100000001b3) ^ b as u64;
328    }
329    h
330}
331
332// ─── Wire format (pulse exchange) ───────────────────
333//
334// Header: b"PXC1" + pulse_count: u32
335// Per pulse:
336//   source_id: u64, timestamp_ms: u64, query_hash: u64
337//   layer_hint: u8, strength: f32
338//   activation_count: u16
339//   activations: [count × (f32, f32, f32, f32)]
340
341fn encode_pulses(pulses: &[Pulse]) -> Vec<u8> {
342    let mut buf = Vec::new();
343    buf.extend_from_slice(b"PXC1");
344    buf.extend_from_slice(&(pulses.len() as u32).to_le_bytes());
345
346    for p in pulses {
347        buf.extend_from_slice(&p.source_id.to_le_bytes());
348        buf.extend_from_slice(&p.timestamp_ms.to_le_bytes());
349        buf.extend_from_slice(&p.query_hash.to_le_bytes());
350        buf.push(p.layer_hint);
351        buf.extend_from_slice(&p.strength.to_le_bytes());
352        buf.extend_from_slice(&(p.activations.len() as u16).to_le_bytes());
353        for &(x, y, z, s) in &p.activations {
354            buf.extend_from_slice(&x.to_le_bytes());
355            buf.extend_from_slice(&y.to_le_bytes());
356            buf.extend_from_slice(&z.to_le_bytes());
357            buf.extend_from_slice(&s.to_le_bytes());
358        }
359    }
360    buf
361}
362
363fn decode_pulses(data: &[u8]) -> Vec<Pulse> {
364    let mut pulses = Vec::new();
365    if data.len() < 8 || &data[0..4] != b"PXC1" {
366        return pulses;
367    }
368
369    let count = u32::from_le_bytes(data[4..8].try_into().unwrap()) as usize;
370    let mut pos = 8;
371
372    for _ in 0..count {
373        if pos + 29 > data.len() {
374            break;
375        }
376        let source_id = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
377        let timestamp_ms = u64::from_le_bytes(data[pos + 8..pos + 16].try_into().unwrap());
378        let query_hash = u64::from_le_bytes(data[pos + 16..pos + 24].try_into().unwrap());
379        let layer_hint = data[pos + 24];
380        let strength = f32::from_le_bytes(data[pos + 25..pos + 29].try_into().unwrap());
381        let act_count = u16::from_le_bytes(data[pos + 29..pos + 31].try_into().unwrap()) as usize;
382        pos += 31;
383
384        let mut activations = Vec::with_capacity(act_count);
385        for _ in 0..act_count {
386            if pos + 16 > data.len() {
387                break;
388            }
389            let x = f32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
390            let y = f32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap());
391            let z = f32::from_le_bytes(data[pos + 8..pos + 12].try_into().unwrap());
392            let s = f32::from_le_bytes(data[pos + 12..pos + 16].try_into().unwrap());
393            activations.push((x, y, z, s));
394            pos += 16;
395        }
396
397        pulses.push(Pulse {
398            source_id,
399            timestamp_ms,
400            query_hash,
401            activations,
402            layer_hint,
403            strength,
404        });
405    }
406    pulses
407}
408
409// ─── Disk I/O (pulses.bin) ──────────────────────────
410//
411// Format: b"PLS1" + instance_id: u64 + outgoing_count: u32 + incoming_count: u32 + field_count: u32
412// Then: outgoing pulses (wire format), incoming received pulses, field entries
413
414fn load_resonance_state(output_dir: &Path, _instance_id: u64) -> Option<ResonanceState> {
415    let path = output_dir.join("pulses.bin");
416    let data = fs::read(&path).ok()?;
417    if data.len() < 24 || &data[0..4] != b"PLS1" {
418        return None;
419    }
420
421    let stored_id = u64::from_le_bytes(data[4..12].try_into().unwrap());
422    let outgoing_count = u32::from_le_bytes(data[12..16].try_into().unwrap()) as usize;
423    let incoming_count = u32::from_le_bytes(data[16..20].try_into().unwrap()) as usize;
424    let field_count = u32::from_le_bytes(data[20..24].try_into().unwrap()) as usize;
425
426    let mut pos = 24;
427
428    // Read outgoing pulses
429    let mut outgoing = Vec::with_capacity(outgoing_count);
430    for _ in 0..outgoing_count {
431        if pos + 31 > data.len() {
432            break;
433        }
434        let source_id = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
435        let timestamp_ms = u64::from_le_bytes(data[pos + 8..pos + 16].try_into().unwrap());
436        let query_hash = u64::from_le_bytes(data[pos + 16..pos + 24].try_into().unwrap());
437        let layer_hint = data[pos + 24];
438        let strength = f32::from_le_bytes(data[pos + 25..pos + 29].try_into().unwrap());
439        let act_count = u16::from_le_bytes(data[pos + 29..pos + 31].try_into().unwrap()) as usize;
440        pos += 31;
441
442        let mut activations = Vec::with_capacity(act_count);
443        for _ in 0..act_count {
444            if pos + 16 > data.len() {
445                break;
446            }
447            let x = f32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
448            let y = f32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap());
449            let z = f32::from_le_bytes(data[pos + 8..pos + 12].try_into().unwrap());
450            let s = f32::from_le_bytes(data[pos + 12..pos + 16].try_into().unwrap());
451            activations.push((x, y, z, s));
452            pos += 16;
453        }
454
455        outgoing.push(Pulse {
456            source_id,
457            timestamp_ms,
458            query_hash,
459            activations,
460            layer_hint,
461            strength,
462        });
463    }
464
465    // Read incoming (pulse + local_matches: u32 + integrated: u8)
466    let mut incoming = Vec::with_capacity(incoming_count);
467    for _ in 0..incoming_count {
468        if pos + 36 > data.len() {
469            break;
470        }
471        let source_id = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
472        let timestamp_ms = u64::from_le_bytes(data[pos + 8..pos + 16].try_into().unwrap());
473        let query_hash = u64::from_le_bytes(data[pos + 16..pos + 24].try_into().unwrap());
474        let layer_hint = data[pos + 24];
475        let strength = f32::from_le_bytes(data[pos + 25..pos + 29].try_into().unwrap());
476        let act_count = u16::from_le_bytes(data[pos + 29..pos + 31].try_into().unwrap()) as usize;
477        pos += 31;
478
479        let mut activations = Vec::with_capacity(act_count);
480        for _ in 0..act_count {
481            if pos + 16 > data.len() {
482                break;
483            }
484            let x = f32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
485            let y = f32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap());
486            let z = f32::from_le_bytes(data[pos + 8..pos + 12].try_into().unwrap());
487            let s = f32::from_le_bytes(data[pos + 12..pos + 16].try_into().unwrap());
488            activations.push((x, y, z, s));
489            pos += 16;
490        }
491
492        if pos + 5 > data.len() {
493            break;
494        }
495        let local_matches = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
496        let integrated = data[pos + 4] != 0;
497        pos += 5;
498
499        incoming.push(ReceivedPulse {
500            pulse: Pulse {
501                source_id,
502                timestamp_ms,
503                query_hash,
504                activations,
505                layer_hint,
506                strength,
507            },
508            local_matches,
509            integrated,
510        });
511    }
512
513    // Read field
514    let mut field = HashMap::with_capacity(field_count);
515    for _ in 0..field_count {
516        if pos + 10 > data.len() {
517            break;
518        }
519        let qx = i16::from_le_bytes(data[pos..pos + 2].try_into().unwrap());
520        let qy = i16::from_le_bytes(data[pos + 2..pos + 4].try_into().unwrap());
521        let qz = i16::from_le_bytes(data[pos + 4..pos + 6].try_into().unwrap());
522        let v = f32::from_le_bytes(data[pos + 6..pos + 10].try_into().unwrap());
523        pos += 10;
524        field.insert((qx, qy, qz), v);
525    }
526
527    Some(ResonanceState {
528        instance_id: stored_id,
529        outgoing,
530        incoming,
531        field,
532    })
533}
534
535fn save_resonance_state(output_dir: &Path, state: &ResonanceState) -> Result<(), String> {
536    let path = output_dir.join("pulses.bin");
537    let mut buf = Vec::new();
538
539    // Header
540    buf.extend_from_slice(b"PLS1");
541    buf.extend_from_slice(&state.instance_id.to_le_bytes());
542    buf.extend_from_slice(&(state.outgoing.len() as u32).to_le_bytes());
543    buf.extend_from_slice(&(state.incoming.len() as u32).to_le_bytes());
544    buf.extend_from_slice(&(state.field.len() as u32).to_le_bytes());
545
546    // Outgoing pulses
547    for p in &state.outgoing {
548        buf.extend_from_slice(&p.source_id.to_le_bytes());
549        buf.extend_from_slice(&p.timestamp_ms.to_le_bytes());
550        buf.extend_from_slice(&p.query_hash.to_le_bytes());
551        buf.push(p.layer_hint);
552        buf.extend_from_slice(&p.strength.to_le_bytes());
553        buf.extend_from_slice(&(p.activations.len() as u16).to_le_bytes());
554        for &(x, y, z, s) in &p.activations {
555            buf.extend_from_slice(&x.to_le_bytes());
556            buf.extend_from_slice(&y.to_le_bytes());
557            buf.extend_from_slice(&z.to_le_bytes());
558            buf.extend_from_slice(&s.to_le_bytes());
559        }
560    }
561
562    // Incoming (pulse + metadata)
563    for r in &state.incoming {
564        buf.extend_from_slice(&r.pulse.source_id.to_le_bytes());
565        buf.extend_from_slice(&r.pulse.timestamp_ms.to_le_bytes());
566        buf.extend_from_slice(&r.pulse.query_hash.to_le_bytes());
567        buf.push(r.pulse.layer_hint);
568        buf.extend_from_slice(&r.pulse.strength.to_le_bytes());
569        buf.extend_from_slice(&(r.pulse.activations.len() as u16).to_le_bytes());
570        for &(x, y, z, s) in &r.pulse.activations {
571            buf.extend_from_slice(&x.to_le_bytes());
572            buf.extend_from_slice(&y.to_le_bytes());
573            buf.extend_from_slice(&z.to_le_bytes());
574            buf.extend_from_slice(&s.to_le_bytes());
575        }
576        buf.extend_from_slice(&r.local_matches.to_le_bytes());
577        buf.push(if r.integrated { 1 } else { 0 });
578    }
579
580    // Field
581    for (&(qx, qy, qz), &v) in &state.field {
582        buf.extend_from_slice(&qx.to_le_bytes());
583        buf.extend_from_slice(&qy.to_le_bytes());
584        buf.extend_from_slice(&qz.to_le_bytes());
585        buf.extend_from_slice(&v.to_le_bytes());
586    }
587
588    fs::write(&path, &buf).map_err(|e| format!("write pulses.bin: {}", e))
589}
590
591#[cfg(test)]
592mod tests {
593    use super::*;
594
595    #[test]
596    fn test_quantize() {
597        assert_eq!(quantize(0.0), 0);
598        assert_eq!(quantize(0.5), 10);
599        assert_eq!(quantize(1.0), 20);
600        assert_eq!(quantize(-0.25), -5);
601    }
602
603    #[test]
604    fn test_path_hash_deterministic() {
605        let h1 = path_hash(Path::new("/tmp/a"));
606        let h2 = path_hash(Path::new("/tmp/a"));
607        assert_eq!(h1, h2);
608        assert_ne!(h1, path_hash(Path::new("/tmp/b")));
609    }
610
611    #[test]
612    fn test_emit_pulse() {
613        let mut state = ResonanceState {
614            instance_id: 42,
615            outgoing: Vec::new(),
616            incoming: Vec::new(),
617            field: HashMap::new(),
618        };
619
620        let headers = vec![(0.1, 0.2, 0.3), (0.4, 0.5, 0.6), (0.7, 0.8, 0.9)];
621        state.emit_pulse(&[(0, 0.9), (2, 0.7)], 100, &headers, 1);
622
623        assert_eq!(state.outgoing.len(), 1);
624        assert_eq!(state.outgoing[0].activations.len(), 2);
625        assert_eq!(state.outgoing[0].source_id, 42);
626    }
627
628    #[test]
629    fn test_emit_pulse_too_few() {
630        let mut state = ResonanceState {
631            instance_id: 42,
632            outgoing: Vec::new(),
633            incoming: Vec::new(),
634            field: HashMap::new(),
635        };
636
637        let headers = vec![(0.1, 0.2, 0.3)];
638        state.emit_pulse(&[(0, 0.9)], 100, &headers, 1);
639
640        // Only 1 activation — below minimum, no pulse emitted
641        assert!(state.outgoing.is_empty());
642    }
643
644    #[test]
645    fn test_receive_pulse_ignores_self() {
646        let mut state = ResonanceState {
647            instance_id: 42,
648            outgoing: Vec::new(),
649            incoming: Vec::new(),
650            field: HashMap::new(),
651        };
652
653        let pulse = Pulse {
654            source_id: 42, // same as our instance
655            timestamp_ms: 1000,
656            query_hash: 100,
657            activations: vec![(0.1, 0.2, 0.3, 0.9)],
658            layer_hint: 1,
659            strength: 0.8,
660        };
661
662        let matches = state.receive_pulse(pulse, &[(0.1, 0.2, 0.3)], 0.1);
663        assert_eq!(matches, 0);
664        assert!(state.incoming.is_empty());
665    }
666
667    #[test]
668    fn test_receive_pulse_from_other() {
669        let mut state = ResonanceState {
670            instance_id: 42,
671            outgoing: Vec::new(),
672            incoming: Vec::new(),
673            field: HashMap::new(),
674        };
675
676        let pulse = Pulse {
677            source_id: 99, // different instance
678            timestamp_ms: 1000,
679            query_hash: 100,
680            activations: vec![(0.1, 0.2, 0.3, 0.9)],
681            layer_hint: 1,
682            strength: 0.8,
683        };
684
685        let local_headers = vec![(0.1, 0.2, 0.3), (0.5, 0.5, 0.5)];
686        let matches = state.receive_pulse(pulse, &local_headers, 0.1);
687
688        assert!(matches > 0); // (0.1, 0.2, 0.3) is near the pulse
689        assert_eq!(state.incoming.len(), 1);
690        assert!(!state.incoming[0].integrated);
691    }
692
693    #[test]
694    fn test_field_strength() {
695        let mut state = ResonanceState {
696            instance_id: 42,
697            outgoing: Vec::new(),
698            incoming: Vec::new(),
699            field: HashMap::new(),
700        };
701
702        state.field.insert((2, 4, 6), 1.0); // quantized (0.1, 0.2, 0.3)
703        let s = state.field_strength(0.1, 0.2, 0.3);
704        assert!(s > 0.0);
705
706        // Far away should be 0
707        let s2 = state.field_strength(5.0, 5.0, 5.0);
708        assert!(s2.abs() < 0.001);
709    }
710
711    #[test]
712    fn test_wire_format_roundtrip() {
713        let pulses = vec![
714            Pulse {
715                source_id: 42,
716                timestamp_ms: 12345,
717                query_hash: 999,
718                activations: vec![(0.1, 0.2, 0.3, 0.9), (0.4, 0.5, 0.6, 0.7)],
719                layer_hint: 1,
720                strength: 0.8,
721            },
722            Pulse {
723                source_id: 99,
724                timestamp_ms: 67890,
725                query_hash: 888,
726                activations: vec![(0.7, 0.8, 0.9, 0.5)],
727                layer_hint: 3,
728                strength: 0.6,
729            },
730        ];
731
732        let encoded = encode_pulses(&pulses);
733        let decoded = decode_pulses(&encoded);
734
735        assert_eq!(decoded.len(), 2);
736        assert_eq!(decoded[0].source_id, 42);
737        assert_eq!(decoded[0].activations.len(), 2);
738        assert!((decoded[0].activations[0].0 - 0.1).abs() < 0.001);
739        assert_eq!(decoded[1].query_hash, 888);
740        assert_eq!(decoded[1].layer_hint, 3);
741    }
742
743    #[test]
744    fn test_save_load_roundtrip() {
745        let tmp = tempfile::tempdir().expect("create temp dir");
746        let dir = tmp.path();
747
748        let mut state = ResonanceState {
749            instance_id: 42,
750            outgoing: Vec::new(),
751            incoming: Vec::new(),
752            field: HashMap::new(),
753        };
754
755        // Add some state
756        state.outgoing.push(Pulse {
757            source_id: 42,
758            timestamp_ms: 1000,
759            query_hash: 100,
760            activations: vec![(0.1, 0.2, 0.3, 0.9)],
761            layer_hint: 1,
762            strength: 0.8,
763        });
764
765        state.incoming.push(ReceivedPulse {
766            pulse: Pulse {
767                source_id: 99,
768                timestamp_ms: 2000,
769                query_hash: 200,
770                activations: vec![(0.4, 0.5, 0.6, 0.7)],
771                layer_hint: 2,
772                strength: 0.6,
773            },
774            local_matches: 3,
775            integrated: true,
776        });
777
778        state.field.insert((2, 4, 6), 1.5);
779
780        state.save(dir).expect("save");
781
782        let loaded = ResonanceState::load_or_init(dir);
783        assert_eq!(loaded.instance_id, 42);
784        assert_eq!(loaded.outgoing.len(), 1);
785        assert_eq!(loaded.outgoing[0].query_hash, 100);
786        assert_eq!(loaded.incoming.len(), 1);
787        assert_eq!(loaded.incoming[0].local_matches, 3);
788        assert!(loaded.incoming[0].integrated);
789        assert!((loaded.field[&(2, 4, 6)] - 1.5).abs() < 0.001);
790    }
791
792    #[test]
793    fn test_integrate_into_hebbian() {
794        let mut state = ResonanceState {
795            instance_id: 42,
796            outgoing: Vec::new(),
797            incoming: Vec::new(),
798            field: HashMap::new(),
799        };
800
801        state.incoming.push(ReceivedPulse {
802            pulse: Pulse {
803                source_id: 99,
804                timestamp_ms: 1000,
805                query_hash: 100,
806                activations: vec![(0.1, 0.2, 0.3, 0.9)],
807                layer_hint: 1,
808                strength: 0.8,
809            },
810            local_matches: 1,
811            integrated: false,
812        });
813
814        let mut hebb = hebbian::HebbianState {
815            activations: vec![hebbian::ActivationRecord::default(); 3],
816            coactivations: HashMap::new(),
817            fingerprints: Vec::new(),
818        };
819
820        let headers = vec![(0.1, 0.2, 0.3), (0.5, 0.5, 0.5), (0.9, 0.9, 0.9)];
821        let influenced = state.integrate_into_hebbian(&mut hebb, &headers, 0.1);
822
823        assert!(influenced > 0);
824        assert!(hebb.activations[0].energy > 0.0); // block 0 is near (0.1, 0.2, 0.3)
825        assert!(state.incoming[0].integrated);
826    }
827
828    #[test]
829    fn test_decay_field() {
830        let mut state = ResonanceState {
831            instance_id: 42,
832            outgoing: Vec::new(),
833            incoming: Vec::new(),
834            field: HashMap::new(),
835        };
836
837        state.field.insert((0, 0, 0), 1.0);
838        state.field.insert((1, 1, 1), 0.005); // below threshold after decay
839
840        state.decay_field(0.9);
841
842        assert!(state.field.contains_key(&(0, 0, 0)));
843        assert!(!state.field.contains_key(&(1, 1, 1))); // decayed away
844    }
845}