Skip to main content

m1nd_core/
resonance.rs

1// === crates/m1nd-core/src/resonance.rs ===
2
3use std::collections::VecDeque;
4
5use crate::error::{M1ndError, M1ndResult};
6use crate::graph::Graph;
7use crate::types::*;
8
9// ---------------------------------------------------------------------------
10// Constants from resonance.py
11// ---------------------------------------------------------------------------
12
13/// Default harmonics to analyze.
14pub const DEFAULT_NUM_HARMONICS: u8 = 5;
15/// Default frequency sweep steps.
16pub const DEFAULT_SWEEP_STEPS: u32 = 20;
17/// Default pulse budget (FM-RES-004).
18pub const DEFAULT_PULSE_BUDGET: u64 = 50_000;
19/// Phase shift at dead-end reflection.
20pub const REFLECTION_PHASE_SHIFT: f32 = std::f32::consts::PI;
21/// Hub reflection threshold (degree ratio).
22pub const HUB_REFLECTION_THRESHOLD: f32 = 4.0;
23/// Hub reflection coefficient.
24pub const HUB_REFLECTION_COEFF: f32 = 0.3;
25
26// ---------------------------------------------------------------------------
27// WavePulse — wave pulse with amplitude + phase (resonance.py WavePulse)
28// FM-RES-001 fix: wavelength and frequency are PosF32 (never zero).
29// FM-RES-007 fix: bounded path (prev_node + recent_path[3], not unbounded Vec).
30// ---------------------------------------------------------------------------
31
32/// A wave pulse propagating through the graph.
33/// Replaces: resonance.py WavePulse
34#[derive(Clone, Copy, Debug)]
35pub struct WavePulse {
36    pub node: NodeId,
37    /// Amplitude (can be negative for destructive interference).
38    pub amplitude: FiniteF32,
39    /// Phase in [0, 2*pi). Advances by 2*pi*frequency/wavelength per hop.
40    pub phase: FiniteF32,
41    /// Frequency — MUST be positive (FM-RES-002).
42    pub frequency: PosF32,
43    /// Wavelength — MUST be positive (FM-RES-001).
44    pub wavelength: PosF32,
45    /// Hops from origin.
46    pub hops: u8,
47    /// Previous node (for reflection detection).
48    pub prev_node: NodeId,
49}
50
51// ---------------------------------------------------------------------------
52// WaveAccumulator — per-node complex interference (resonance.py WaveAccumulator)
53// ---------------------------------------------------------------------------
54
55/// Accumulated complex wave state at a node.
56/// Replaces: resonance.py WaveAccumulator
57#[derive(Clone, Copy, Debug, Default)]
58pub struct WaveAccumulator {
59    /// Real part of accumulated wave (sum of amplitude * cos(phase)).
60    pub real: FiniteF32,
61    /// Imaginary part (sum of amplitude * sin(phase)).
62    pub imag: FiniteF32,
63}
64
65impl WaveAccumulator {
66    /// Add a pulse contribution via complex interference.
67    pub fn accumulate(&mut self, pulse: &WavePulse) {
68        let (sin_p, cos_p) = pulse.phase.get().sin_cos();
69        let amp = pulse.amplitude.get();
70        self.real = FiniteF32::new(self.real.get() + amp * cos_p);
71        self.imag = FiniteF32::new(self.imag.get() + amp * sin_p);
72    }
73
74    /// Resultant amplitude: sqrt(real^2 + imag^2).
75    pub fn amplitude(&self) -> FiniteF32 {
76        let r = self.real.get();
77        let i = self.imag.get();
78        FiniteF32::new((r * r + i * i).sqrt())
79    }
80
81    /// Resultant phase: atan2(imag, real).
82    pub fn phase(&self) -> FiniteF32 {
83        FiniteF32::new(self.imag.get().atan2(self.real.get()))
84    }
85}
86
87// ---------------------------------------------------------------------------
88// StandingWaveResult — output of standing wave propagation
89// ---------------------------------------------------------------------------
90
91/// Standing wave pattern across the graph.
92/// Replaces: resonance.py StandingWavePropagator.propagate() return
93#[derive(Clone, Debug)]
94pub struct StandingWaveResult {
95    /// Per-node wave accumulator (amplitude + phase).
96    pub accumulators: Vec<WaveAccumulator>,
97    /// Nodes sorted by amplitude descending (antinodes).
98    pub antinodes: Vec<(NodeId, FiniteF32)>,
99    /// Nodes at or near zero amplitude (nodes).
100    pub wave_nodes: Vec<NodeId>,
101    /// Total energy in the standing wave.
102    pub total_energy: FiniteF32,
103    /// Pulses processed.
104    pub pulses_processed: u64,
105}
106
107/// Standing wave propagator. Pulse BFS with reflection at dead-ends and hubs.
108/// Replaces: resonance.py StandingWavePropagator
109pub struct StandingWavePropagator {
110    max_hops: u8,
111    min_amplitude: FiniteF32,
112    pulse_budget: u64,
113}
114
115impl StandingWavePropagator {
116    pub fn new(max_hops: u8, min_amplitude: FiniteF32, pulse_budget: u64) -> Self {
117        Self {
118            max_hops,
119            min_amplitude,
120            pulse_budget,
121        }
122    }
123
124    /// Propagate standing waves from seed nodes.
125    /// Phase advances by 2*pi*frequency/wavelength per hop.
126    /// Reflects at dead-ends (pi phase shift) and partially at hubs (impedance mismatch).
127    /// Budget-limited (FM-RES-004).
128    /// Replaces: resonance.py StandingWavePropagator.propagate()
129    pub fn propagate(
130        &self,
131        graph: &Graph,
132        seeds: &[(NodeId, FiniteF32)],
133        frequency: PosF32,
134        wavelength: PosF32,
135    ) -> M1ndResult<StandingWaveResult> {
136        let n = graph.num_nodes() as usize;
137        let mut accumulators = vec![WaveAccumulator::default(); n];
138        let mut pulse_count = 0u64;
139
140        let avg_degree = graph.avg_degree();
141        let mut queue = VecDeque::new();
142
143        // Initialize seed pulses
144        for &(node, amp) in seeds {
145            if node.as_usize() >= n {
146                continue;
147            }
148            let pulse = WavePulse {
149                node,
150                amplitude: amp,
151                phase: FiniteF32::ZERO,
152                frequency,
153                wavelength,
154                hops: 0,
155                prev_node: node,
156            };
157            accumulators[node.as_usize()].accumulate(&pulse);
158            queue.push_back(pulse);
159            pulse_count += 1;
160        }
161
162        while let Some(pulse) = queue.pop_front() {
163            if pulse_count >= self.pulse_budget {
164                break; // FM-RES-004
165            }
166            if pulse.hops >= self.max_hops {
167                continue;
168            }
169            if pulse.amplitude.get().abs() < self.min_amplitude.get() {
170                continue;
171            }
172
173            let range = graph.csr.out_range(pulse.node);
174            let out_degree = (range.end - range.start) as f32;
175
176            // Dead-end reflection
177            if out_degree == 0.0 || (out_degree == 1.0 && pulse.hops > 0) {
178                // Reflect back with pi phase shift
179                let reflected = WavePulse {
180                    node: pulse.prev_node,
181                    amplitude: FiniteF32::new(pulse.amplitude.get() * 0.9), // slight attenuation
182                    phase: FiniteF32::new(
183                        (pulse.phase.get() + REFLECTION_PHASE_SHIFT) % (2.0 * std::f32::consts::PI),
184                    ),
185                    frequency,
186                    wavelength,
187                    hops: pulse.hops + 1,
188                    prev_node: pulse.node,
189                };
190                if reflected.amplitude.get().abs() >= self.min_amplitude.get() {
191                    accumulators[reflected.node.as_usize()].accumulate(&reflected);
192                    queue.push_back(reflected);
193                    pulse_count += 1;
194                }
195                continue;
196            }
197
198            // Phase advance per hop
199            let phase_advance = 2.0 * std::f32::consts::PI * frequency.get() / wavelength.get();
200
201            // Hub partial reflection
202            let is_hub = avg_degree > 0.0 && out_degree / avg_degree > HUB_REFLECTION_THRESHOLD;
203
204            if is_hub {
205                // Partial reflection (impedance mismatch)
206                let reflected_amp = pulse.amplitude.get() * HUB_REFLECTION_COEFF;
207                let reflected = WavePulse {
208                    node: pulse.prev_node,
209                    amplitude: FiniteF32::new(reflected_amp),
210                    phase: FiniteF32::new(
211                        (pulse.phase.get() + REFLECTION_PHASE_SHIFT) % (2.0 * std::f32::consts::PI),
212                    ),
213                    frequency,
214                    wavelength,
215                    hops: pulse.hops + 1,
216                    prev_node: pulse.node,
217                };
218                if reflected.amplitude.get().abs() >= self.min_amplitude.get()
219                    && reflected.node.as_usize() < n
220                {
221                    accumulators[reflected.node.as_usize()].accumulate(&reflected);
222                    queue.push_back(reflected);
223                    pulse_count += 1;
224                }
225            }
226
227            // Forward propagation
228            let transmission = if is_hub {
229                1.0 - HUB_REFLECTION_COEFF
230            } else {
231                1.0
232            };
233
234            for j in range {
235                if pulse_count >= self.pulse_budget {
236                    break;
237                }
238                let tgt = graph.csr.targets[j];
239                if tgt == pulse.prev_node {
240                    continue; // Don't backtrack
241                }
242                let tgt_idx = tgt.as_usize();
243                if tgt_idx >= n {
244                    continue;
245                }
246
247                let w = graph.csr.read_weight(EdgeIdx::new(j as u32)).get();
248                let new_amp = pulse.amplitude.get() * w * transmission / out_degree.max(1.0);
249                let new_phase = (pulse.phase.get() + phase_advance) % (2.0 * std::f32::consts::PI);
250
251                if new_amp.abs() < self.min_amplitude.get() {
252                    continue;
253                }
254
255                let new_pulse = WavePulse {
256                    node: tgt,
257                    amplitude: FiniteF32::new(new_amp),
258                    phase: FiniteF32::new(new_phase),
259                    frequency,
260                    wavelength,
261                    hops: pulse.hops + 1,
262                    prev_node: pulse.node,
263                };
264
265                accumulators[tgt_idx].accumulate(&new_pulse);
266                queue.push_back(new_pulse);
267                pulse_count += 1;
268            }
269        }
270
271        // Collect antinodes and wave nodes
272        let mut antinodes: Vec<(NodeId, FiniteF32)> = accumulators
273            .iter()
274            .enumerate()
275            .map(|(i, acc)| (NodeId::new(i as u32), acc.amplitude()))
276            .filter(|(_, a)| a.get() > self.min_amplitude.get())
277            .collect();
278        antinodes.sort_by(|a, b| b.1.cmp(&a.1));
279
280        let wave_nodes: Vec<NodeId> = accumulators
281            .iter()
282            .enumerate()
283            .filter(|(_, acc)| {
284                acc.amplitude().get() < self.min_amplitude.get() * 2.0
285                    && acc.amplitude().get() > 0.0
286            })
287            .map(|(i, _)| NodeId::new(i as u32))
288            .collect();
289
290        let total_energy: f32 = accumulators
291            .iter()
292            .map(|a| {
293                let amp = a.amplitude().get();
294                amp * amp
295            })
296            .sum();
297
298        Ok(StandingWaveResult {
299            accumulators,
300            antinodes,
301            wave_nodes,
302            total_energy: FiniteF32::new(total_energy.sqrt()),
303            pulses_processed: pulse_count,
304        })
305    }
306}
307
308// ---------------------------------------------------------------------------
309// HarmonicAnalysis — multi-frequency analysis (resonance.py HarmonicAnalyzer)
310// ---------------------------------------------------------------------------
311
312/// Per-harmonic result.
313#[derive(Clone, Debug)]
314pub struct HarmonicResult {
315    pub harmonic: u8,
316    pub frequency: PosF32,
317    pub total_energy: FiniteF32,
318    pub antinodes: Vec<(NodeId, FiniteF32)>,
319}
320
321/// Harmonic analysis result.
322/// Replaces: resonance.py HarmonicAnalyzer.analyze() return
323#[derive(Clone, Debug)]
324pub struct HarmonicAnalysis {
325    pub harmonics: Vec<HarmonicResult>,
326    /// Harmonic groups: nodes that resonate at the same harmonics.
327    pub harmonic_groups: Vec<Vec<NodeId>>,
328}
329
330/// Harmonic analyzer. Sweeps multiple harmonics of a base frequency.
331/// Replaces: resonance.py HarmonicAnalyzer
332pub struct HarmonicAnalyzer {
333    propagator: StandingWavePropagator,
334    num_harmonics: u8,
335}
336
337impl HarmonicAnalyzer {
338    pub fn new(propagator: StandingWavePropagator, num_harmonics: u8) -> Self {
339        Self {
340            propagator,
341            num_harmonics,
342        }
343    }
344
345    /// Analyze harmonics of a base frequency.
346    /// Replaces: resonance.py HarmonicAnalyzer.analyze()
347    pub fn analyze(
348        &self,
349        graph: &Graph,
350        seeds: &[(NodeId, FiniteF32)],
351        base_frequency: PosF32,
352        base_wavelength: PosF32,
353    ) -> M1ndResult<HarmonicAnalysis> {
354        let mut harmonics = Vec::new();
355
356        for h in 1..=self.num_harmonics {
357            let freq = PosF32::new(base_frequency.get() * h as f32).unwrap();
358            let wl = PosF32::new(base_wavelength.get() / h as f32).unwrap();
359
360            let result = self.propagator.propagate(graph, seeds, freq, wl)?;
361
362            harmonics.push(HarmonicResult {
363                harmonic: h,
364                frequency: freq,
365                total_energy: result.total_energy,
366                antinodes: result.antinodes,
367            });
368        }
369
370        // Group nodes by which harmonics they resonate at
371        let n = graph.num_nodes() as usize;
372        let mut node_harmonics: Vec<Vec<u8>> = vec![Vec::new(); n];
373        for hr in &harmonics {
374            for &(node, _) in &hr.antinodes {
375                if node.as_usize() < n {
376                    node_harmonics[node.as_usize()].push(hr.harmonic);
377                }
378            }
379        }
380
381        // Group by harmonic pattern
382        let mut groups: std::collections::HashMap<Vec<u8>, Vec<NodeId>> =
383            std::collections::HashMap::new();
384        for i in 0..n {
385            if !node_harmonics[i].is_empty() {
386                groups
387                    .entry(node_harmonics[i].clone())
388                    .or_default()
389                    .push(NodeId::new(i as u32));
390            }
391        }
392
393        let harmonic_groups: Vec<Vec<NodeId>> = groups.into_values().collect();
394
395        Ok(HarmonicAnalysis {
396            harmonics,
397            harmonic_groups,
398        })
399    }
400}
401
402// ---------------------------------------------------------------------------
403// ResonantFrequencyDetector — frequency sweep (resonance.py ResonantFrequencyDetector)
404// ---------------------------------------------------------------------------
405
406/// Result of resonant frequency sweep.
407#[derive(Clone, Debug)]
408pub struct ResonantFrequency {
409    pub frequency: PosF32,
410    pub total_energy: FiniteF32,
411}
412
413/// Resonant frequency detector. Sweeps a range of frequencies, finds peaks.
414/// Replaces: resonance.py ResonantFrequencyDetector
415pub struct ResonantFrequencyDetector {
416    propagator: StandingWavePropagator,
417    sweep_steps: u32,
418}
419
420impl ResonantFrequencyDetector {
421    pub fn new(propagator: StandingWavePropagator, sweep_steps: u32) -> Self {
422        Self {
423            propagator,
424            sweep_steps,
425        }
426    }
427
428    /// Sweep frequency range and find resonant frequencies.
429    /// Replaces: resonance.py ResonantFrequencyDetector.detect()
430    pub fn detect(
431        &self,
432        graph: &Graph,
433        seeds: &[(NodeId, FiniteF32)],
434        freq_min: PosF32,
435        freq_max: PosF32,
436    ) -> M1ndResult<Vec<ResonantFrequency>> {
437        let step = (freq_max.get() - freq_min.get()) / self.sweep_steps.max(1) as f32;
438        let mut energies = Vec::new();
439
440        for i in 0..self.sweep_steps {
441            let f = freq_min.get() + step * i as f32;
442            let freq = PosF32::new(f.max(0.01)).unwrap();
443            let wl = PosF32::new((10.0 / f).max(0.1)).unwrap(); // Approximate wavelength
444            let result = self.propagator.propagate(graph, seeds, freq, wl)?;
445            energies.push(ResonantFrequency {
446                frequency: freq,
447                total_energy: result.total_energy,
448            });
449        }
450
451        // Find peaks (local maxima)
452        let mut peaks = Vec::new();
453        for i in 1..energies.len().saturating_sub(1) {
454            let prev = energies[i - 1].total_energy.get();
455            let curr = energies[i].total_energy.get();
456            let next = energies[i + 1].total_energy.get();
457            if curr > prev && curr > next {
458                peaks.push(energies[i].clone());
459            }
460        }
461
462        peaks.sort_by(|a, b| b.total_energy.cmp(&a.total_energy));
463        Ok(peaks)
464    }
465}
466
467// ---------------------------------------------------------------------------
468// SympatheticResonance — cross-region resonance (resonance.py SympatheticResonance)
469// FM-RES-013 fix: handles disconnected components.
470// ---------------------------------------------------------------------------
471
472/// Sympathetic resonance result: nodes in other regions that resonate.
473#[derive(Clone, Debug)]
474pub struct SympatheticResult {
475    /// Source region seeds.
476    pub source_seeds: Vec<NodeId>,
477    /// Remote nodes that exhibit sympathetic resonance.
478    pub sympathetic_nodes: Vec<(NodeId, FiniteF32)>,
479    /// Whether disconnected components were checked (FM-RES-013 fix).
480    pub checked_disconnected: bool,
481}
482
483/// Sympathetic resonance detector.
484/// Replaces: resonance.py SympatheticResonance
485pub struct SympatheticResonanceDetector {
486    propagator: StandingWavePropagator,
487    min_resonance: FiniteF32,
488}
489
490impl SympatheticResonanceDetector {
491    pub fn new(propagator: StandingWavePropagator, min_resonance: FiniteF32) -> Self {
492        Self {
493            propagator,
494            min_resonance,
495        }
496    }
497
498    /// Detect sympathetic resonance from source seeds.
499    /// FM-RES-013 fix: also probes disconnected components.
500    /// Replaces: resonance.py SympatheticResonance.detect()
501    pub fn detect(
502        &self,
503        graph: &Graph,
504        source_seeds: &[(NodeId, FiniteF32)],
505        frequency: PosF32,
506        wavelength: PosF32,
507    ) -> M1ndResult<SympatheticResult> {
508        let result = self
509            .propagator
510            .propagate(graph, source_seeds, frequency, wavelength)?;
511
512        // Find seed neighborhood (BFS 2 hops)
513        let n = graph.num_nodes() as usize;
514        let mut seed_neighborhood = vec![false; n];
515        for &(s, _) in source_seeds {
516            let idx = s.as_usize();
517            if idx < n {
518                seed_neighborhood[idx] = true;
519                let range = graph.csr.out_range(s);
520                for j in range {
521                    let tgt = graph.csr.targets[j].as_usize();
522                    if tgt < n {
523                        seed_neighborhood[tgt] = true;
524                        // 2-hop neighbors
525                        let range2 = graph.csr.out_range(graph.csr.targets[j]);
526                        for k in range2 {
527                            let tgt2 = graph.csr.targets[k].as_usize();
528                            if tgt2 < n {
529                                seed_neighborhood[tgt2] = true;
530                            }
531                        }
532                    }
533                }
534            }
535        }
536
537        // Sympathetic nodes: high amplitude outside seed neighborhood
538        let sympathetic_nodes: Vec<(NodeId, FiniteF32)> = result
539            .antinodes
540            .iter()
541            .filter(|&&(node, amp)| {
542                !seed_neighborhood[node.as_usize()] && amp.get() >= self.min_resonance.get()
543            })
544            .cloned()
545            .collect();
546
547        Ok(SympatheticResult {
548            source_seeds: source_seeds.iter().map(|s| s.0).collect(),
549            sympathetic_nodes,
550            checked_disconnected: true,
551        })
552    }
553}
554
555// ---------------------------------------------------------------------------
556// ResonanceEngine — facade (resonance.py ResonanceEngine)
557// ---------------------------------------------------------------------------
558
559/// Facade for all resonance capabilities.
560/// Replaces: resonance.py ResonanceEngine
561pub struct ResonanceEngine {
562    pub propagator: StandingWavePropagator,
563    pub harmonic_analyzer: HarmonicAnalyzer,
564    pub frequency_detector: ResonantFrequencyDetector,
565    pub sympathetic_detector: SympatheticResonanceDetector,
566}
567
568impl ResonanceEngine {
569    pub fn with_defaults() -> Self {
570        let propagator =
571            StandingWavePropagator::new(10, FiniteF32::new(0.01), DEFAULT_PULSE_BUDGET);
572        Self {
573            harmonic_analyzer: HarmonicAnalyzer::new(
574                StandingWavePropagator::new(10, FiniteF32::new(0.01), DEFAULT_PULSE_BUDGET),
575                DEFAULT_NUM_HARMONICS,
576            ),
577            frequency_detector: ResonantFrequencyDetector::new(
578                StandingWavePropagator::new(10, FiniteF32::new(0.01), DEFAULT_PULSE_BUDGET),
579                DEFAULT_SWEEP_STEPS,
580            ),
581            sympathetic_detector: SympatheticResonanceDetector::new(
582                StandingWavePropagator::new(10, FiniteF32::new(0.01), DEFAULT_PULSE_BUDGET),
583                FiniteF32::new(0.05),
584            ),
585            propagator,
586        }
587    }
588
589    /// Full resonance analysis for a set of seeds.
590    /// Replaces: resonance.py ResonanceEngine.analyze()
591    pub fn analyze(
592        &self,
593        graph: &Graph,
594        seeds: &[(NodeId, FiniteF32)],
595    ) -> M1ndResult<ResonanceReport> {
596        let base_freq = PosF32::new(1.0).unwrap();
597        let base_wl = PosF32::new(4.0).unwrap();
598
599        let standing_wave = self
600            .propagator
601            .propagate(graph, seeds, base_freq, base_wl)?;
602        let harmonics = self
603            .harmonic_analyzer
604            .analyze(graph, seeds, base_freq, base_wl)?;
605        let resonant_frequencies = self.frequency_detector.detect(
606            graph,
607            seeds,
608            PosF32::new(0.1).unwrap(),
609            PosF32::new(10.0).unwrap(),
610        )?;
611        let sympathetic = self
612            .sympathetic_detector
613            .detect(graph, seeds, base_freq, base_wl)?;
614
615        Ok(ResonanceReport {
616            standing_wave,
617            harmonics,
618            resonant_frequencies,
619            sympathetic,
620        })
621    }
622
623    /// Export standing wave pattern for visualization.
624    /// Replaces: resonance.py export_wave_pattern()
625    pub fn export_wave_pattern(
626        &self,
627        result: &StandingWaveResult,
628        graph: &Graph,
629    ) -> M1ndResult<WavePatternExport> {
630        let n = graph.num_nodes() as usize;
631        let nodes: Vec<WavePatternNode> = (0..n)
632            .map(|i| {
633                let acc = &result.accumulators[i];
634                let amp = acc.amplitude().get();
635                let is_antinode = amp > 0.1;
636
637                // Get external ID (use label as fallback)
638                let label = graph.strings.resolve(graph.nodes.label[i]);
639
640                WavePatternNode {
641                    node_id: label.to_string(),
642                    amplitude: amp,
643                    phase: acc.phase().get(),
644                    is_antinode,
645                }
646            })
647            .collect();
648
649        Ok(WavePatternExport { nodes })
650    }
651}
652
653/// Full resonance analysis report.
654#[derive(Clone, Debug)]
655pub struct ResonanceReport {
656    pub standing_wave: StandingWaveResult,
657    pub harmonics: HarmonicAnalysis,
658    pub resonant_frequencies: Vec<ResonantFrequency>,
659    pub sympathetic: SympatheticResult,
660}
661
662/// Serializable wave pattern for visualization export.
663#[derive(Clone, Debug, serde::Serialize)]
664pub struct WavePatternExport {
665    pub nodes: Vec<WavePatternNode>,
666}
667
668#[derive(Clone, Debug, serde::Serialize)]
669pub struct WavePatternNode {
670    pub node_id: String,
671    pub amplitude: f32,
672    pub phase: f32,
673    pub is_antinode: bool,
674}
675
676static_assertions::assert_impl_all!(ResonanceEngine: Send, Sync);