Skip to main content

clawft_kernel/
quantum_state.rs

1//! Quantum-inspired cognitive state for the ECC substrate.
2//!
3//! Layers superposition and Born-rule probabilities on top of
4//! the deterministic CausalGraph. The classical path is always
5//! available via `collapse()` or by not using this module.
6//!
7//! Key concepts:
8//! - State vector |psi> over graph nodes (superposition)
9//! - Hamiltonian H = graph Laplacian L (same as spectral analysis)
10//! - Unitary evolution exp(-i*dt*H) between measurements
11//! - Born rule P(k) = |<k|psi>|^2 for measurement outcomes
12//! - Projective collapse on evidence arrival
13
14use serde::{Deserialize, Serialize};
15
16// ---------------------------------------------------------------------------
17// Complex number (no external dep)
18// ---------------------------------------------------------------------------
19
20/// Complex number for quantum amplitudes (no external dep needed).
21#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
22pub struct Complex {
23    pub re: f64,
24    pub im: f64,
25}
26
27impl Complex {
28    pub fn new(re: f64, im: f64) -> Self {
29        Self { re, im }
30    }
31
32    pub fn real(re: f64) -> Self {
33        Self { re, im: 0.0 }
34    }
35
36    pub fn zero() -> Self {
37        Self { re: 0.0, im: 0.0 }
38    }
39
40    pub fn norm_sq(&self) -> f64 {
41        self.re * self.re + self.im * self.im
42    }
43
44    pub fn norm(&self) -> f64 {
45        self.norm_sq().sqrt()
46    }
47
48    pub fn conj(&self) -> Self {
49        Self {
50            re: self.re,
51            im: -self.im,
52        }
53    }
54
55    pub fn add(&self, other: &Self) -> Self {
56        Self {
57            re: self.re + other.re,
58            im: self.im + other.im,
59        }
60    }
61
62    pub fn sub(&self, other: &Self) -> Self {
63        Self {
64            re: self.re - other.re,
65            im: self.im - other.im,
66        }
67    }
68
69    pub fn mul(&self, other: &Self) -> Self {
70        Self {
71            re: self.re * other.re - self.im * other.im,
72            im: self.re * other.im + self.im * other.re,
73        }
74    }
75
76    pub fn scale(&self, s: f64) -> Self {
77        Self {
78            re: self.re * s,
79            im: self.im * s,
80        }
81    }
82}
83
84// ---------------------------------------------------------------------------
85// QuantumCognitiveState
86// ---------------------------------------------------------------------------
87
88/// Quantum state vector over graph nodes.
89///
90/// |psi> = sum_i alpha_i |i> where alpha_i are complex amplitudes.
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct QuantumCognitiveState {
93    /// Complex amplitudes per node. Length = number of graph nodes.
94    psi: Vec<Complex>,
95    /// Node IDs corresponding to each amplitude index.
96    node_ids: Vec<u64>,
97    /// Number of measurements performed.
98    measurement_count: u64,
99    /// Entropy history for decoherence tracking.
100    entropy_history: Vec<f64>,
101}
102
103impl QuantumCognitiveState {
104    /// Initialize from the Fiedler vector (classical -> quantum).
105    ///
106    /// The Fiedler vector becomes the initial state with real amplitudes,
107    /// normalized so that the Born-rule probabilities sum to 1.
108    pub fn from_fiedler(fiedler: &[f64], node_ids: &[u64]) -> Self {
109        let norm: f64 = fiedler.iter().map(|x| x * x).sum::<f64>().sqrt();
110        let psi: Vec<Complex> = fiedler
111            .iter()
112            .map(|&x| Complex::real(x / norm.max(1e-15)))
113            .collect();
114        Self {
115            psi,
116            node_ids: node_ids.to_vec(),
117            measurement_count: 0,
118            entropy_history: Vec::new(),
119        }
120    }
121
122    /// Initialize uniform superposition (maximum uncertainty).
123    pub fn uniform(n: usize, node_ids: &[u64]) -> Self {
124        let amp = 1.0 / (n as f64).sqrt();
125        let psi = vec![Complex::real(amp); n];
126        Self {
127            psi,
128            node_ids: node_ids.to_vec(),
129            measurement_count: 0,
130            entropy_history: Vec::new(),
131        }
132    }
133
134    /// Born rule: probability of node `i` being in the "answer."
135    pub fn probability(&self, i: usize) -> f64 {
136        self.psi.get(i).map(|a| a.norm_sq()).unwrap_or(0.0)
137    }
138
139    /// Born rule: probability distribution over all nodes.
140    pub fn probabilities(&self) -> Vec<f64> {
141        self.psi.iter().map(|a| a.norm_sq()).collect()
142    }
143
144    /// Von Neumann entropy: S = -sum p_i ln(p_i).
145    ///
146    /// - 0 = fully collapsed (one node dominates)
147    /// - ln(n) = maximum uncertainty (uniform superposition)
148    pub fn entropy(&self) -> f64 {
149        -self
150            .psi
151            .iter()
152            .map(|a| {
153                let p = a.norm_sq();
154                if p > 1e-15 { p * p.ln() } else { 0.0 }
155            })
156            .sum::<f64>()
157    }
158
159    /// Coherent evolution: |psi(t+dt)> = exp(-i dt H)|psi(t)>.
160    ///
161    /// Uses first-order approximation: |psi'> ~= (I - i dt H)|psi>.
162    /// H = graph Laplacian L (sparse, computed from CausalGraph adjacency).
163    ///
164    /// `laplacian_action` computes H|psi> given |psi>.
165    pub fn evolve(&mut self, laplacian_action: impl Fn(&[Complex]) -> Vec<Complex>, dt: f64) {
166        // |psi'> = |psi> - i*dt * H|psi>
167        let h_psi = laplacian_action(&self.psi);
168        for (i, hp) in h_psi.iter().enumerate() {
169            // -i * dt * hp = dt * (hp.im, -hp.re)
170            self.psi[i].re += dt * hp.im;
171            self.psi[i].im -= dt * hp.re;
172        }
173        self.normalize();
174    }
175
176    /// Measure evidence impact via expectation value: <psi|DeltaL|psi>.
177    ///
178    /// This is the quantum generalization of delta_lambda_2 = w*(phi[u]-phi[v])^2.
179    pub fn evidence_impact(&self, u_idx: usize, v_idx: usize, weight: f64) -> f64 {
180        let diff = self.psi[u_idx].sub(&self.psi[v_idx]);
181        weight * diff.norm_sq()
182    }
183
184    /// Rank evidence by quantum impact (Born-rule weighted).
185    ///
186    /// This generalizes `rank_evidence_by_impact` to use amplitudes
187    /// instead of the Fiedler vector.
188    pub fn rank_evidence_quantum(
189        &self,
190        candidates: &[(usize, usize, f64)], // (u_idx, v_idx, weight)
191    ) -> Vec<QuantumEvidenceRanking> {
192        let mut rankings: Vec<_> = candidates
193            .iter()
194            .map(|&(u, v, w)| {
195                let impact = self.evidence_impact(u, v, w);
196                let p_u = self.probability(u);
197                let p_v = self.probability(v);
198                QuantumEvidenceRanking {
199                    u_idx: u,
200                    v_idx: v,
201                    weight: w,
202                    impact,
203                    prob_u: p_u,
204                    prob_v: p_v,
205                    explanation: format!("Impact={:.4}, P(u)={:.3}, P(v)={:.3}", impact, p_u, p_v),
206                }
207            })
208            .collect();
209        rankings.sort_by(|a, b| {
210            b.impact
211                .partial_cmp(&a.impact)
212                .unwrap_or(std::cmp::Ordering::Equal)
213        });
214        rankings
215    }
216
217    /// Projective measurement: collapse onto evidence outcome.
218    ///
219    /// After observing that edge (u,v) IS part of the causal chain,
220    /// amplitudes at u and v are boosted, others are suppressed.
221    pub fn observe_evidence(&mut self, u_idx: usize, v_idx: usize, weight: f64) {
222        let boost = (1.0 + weight).sqrt();
223        self.psi[u_idx] = self.psi[u_idx].scale(boost);
224        self.psi[v_idx] = self.psi[v_idx].scale(boost);
225        self.normalize();
226        self.measurement_count += 1;
227        self.entropy_history.push(self.entropy());
228    }
229
230    /// Partial collapse: suppress hypotheses inconsistent with evidence.
231    ///
232    /// Nodes contradicting the evidence have amplitudes dampened by `factor`.
233    pub fn suppress(&mut self, node_idx: usize, factor: f64) {
234        self.psi[node_idx] = self.psi[node_idx].scale(factor);
235        self.normalize();
236    }
237
238    /// Full collapse: return the classical deterministic answer.
239    ///
240    /// Projects |psi> onto the most probable basis state.
241    /// Returns `(index, probability)`.
242    pub fn collapse(&self) -> (usize, f64) {
243        self.psi
244            .iter()
245            .enumerate()
246            .map(|(i, a)| (i, a.norm_sq()))
247            .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
248            .unwrap_or((0, 0.0))
249    }
250
251    /// Get the classical Fiedler-like vector (real parts of amplitudes).
252    ///
253    /// This is the "classical projection" -- always available.
254    pub fn to_classical(&self) -> Vec<f64> {
255        self.psi.iter().map(|a| a.re).collect()
256    }
257
258    /// Normalize: ensure sum of |alpha_i|^2 = 1.
259    fn normalize(&mut self) {
260        let norm: f64 = self.psi.iter().map(|a| a.norm_sq()).sum::<f64>().sqrt();
261        if norm > 1e-15 {
262            for a in &mut self.psi {
263                a.re /= norm;
264                a.im /= norm;
265            }
266        }
267    }
268
269    /// Is the state close to collapsed? (entropy near zero)
270    pub fn is_collapsed(&self, threshold: f64) -> bool {
271        self.entropy() < threshold
272    }
273
274    /// How many measurements until collapse?
275    ///
276    /// Based on entropy decay rate from history.
277    pub fn estimated_measurements_to_collapse(&self) -> Option<usize> {
278        if self.entropy_history.len() < 2 {
279            return None;
280        }
281        let recent = &self.entropy_history[self.entropy_history.len().saturating_sub(5)..];
282        if recent.len() < 2 {
283            return None;
284        }
285        let rate = (recent.first().unwrap() - recent.last().unwrap()) / recent.len() as f64;
286        if rate <= 0.0 {
287            return None; // not converging
288        }
289        let remaining = self.entropy() / rate;
290        Some(remaining.ceil() as usize)
291    }
292
293    /// Number of nodes in the state vector.
294    pub fn dimension(&self) -> usize {
295        self.psi.len()
296    }
297
298    /// Number of measurements performed so far.
299    pub fn measurement_count(&self) -> u64 {
300        self.measurement_count
301    }
302
303    /// Node IDs corresponding to each amplitude index.
304    pub fn node_ids(&self) -> &[u64] {
305        &self.node_ids
306    }
307}
308
309// ---------------------------------------------------------------------------
310// QuantumEvidenceRanking
311// ---------------------------------------------------------------------------
312
313/// Result of ranking a candidate evidence edge by quantum impact.
314#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct QuantumEvidenceRanking {
316    pub u_idx: usize,
317    pub v_idx: usize,
318    pub weight: f64,
319    pub impact: f64,
320    pub prob_u: f64,
321    pub prob_v: f64,
322    pub explanation: String,
323}
324
325// ---------------------------------------------------------------------------
326// Hypothesis Superposition
327// ---------------------------------------------------------------------------
328
329/// A named hypothesis with an amplitude.
330#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct Hypothesis {
332    pub id: String,
333    pub amplitude: f64,
334    pub description: String,
335    /// Edges (as `(source_id, target_id)`) that support this hypothesis.
336    pub supporting_edges: Vec<(u64, u64)>,
337}
338
339/// Multiple competing hypotheses in superposition.
340///
341/// Each hypothesis carries an amplitude; the Born rule gives probabilities.
342/// Evidence observations boost supporting hypotheses and dampen others.
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct HypothesisSuperposition {
345    pub hypotheses: Vec<Hypothesis>,
346}
347
348impl Default for HypothesisSuperposition {
349    fn default() -> Self {
350        Self::new()
351    }
352}
353
354impl HypothesisSuperposition {
355    pub fn new() -> Self {
356        Self {
357            hypotheses: Vec::new(),
358        }
359    }
360
361    /// Add a hypothesis. Amplitudes are redistributed uniformly.
362    pub fn add_hypothesis(&mut self, id: &str, description: &str, edges: Vec<(u64, u64)>) {
363        let n = self.hypotheses.len() + 1;
364        let amp = 1.0 / (n as f64).sqrt();
365        // Redistribute amplitudes uniformly
366        for h in &mut self.hypotheses {
367            h.amplitude = amp;
368        }
369        self.hypotheses.push(Hypothesis {
370            id: id.to_string(),
371            amplitude: amp,
372            description: description.to_string(),
373            supporting_edges: edges,
374        });
375    }
376
377    /// Born rule: probability of each hypothesis.
378    pub fn probabilities(&self) -> Vec<(String, f64)> {
379        self.hypotheses
380            .iter()
381            .map(|h| (h.id.clone(), h.amplitude.powi(2)))
382            .collect()
383    }
384
385    /// Update amplitudes based on evidence.
386    ///
387    /// Evidence that supports a hypothesis boosts its amplitude;
388    /// unsupported hypotheses are dampened.
389    pub fn observe(&mut self, evidence_edge: (u64, u64)) {
390        for h in &mut self.hypotheses {
391            if h.supporting_edges.contains(&evidence_edge) {
392                h.amplitude *= 1.5; // boost
393            } else {
394                h.amplitude *= 0.8; // dampen
395            }
396        }
397        self.normalize();
398    }
399
400    /// Von Neumann entropy of the hypothesis distribution.
401    pub fn entropy(&self) -> f64 {
402        -self
403            .hypotheses
404            .iter()
405            .map(|h| {
406                let p = h.amplitude.powi(2);
407                if p > 1e-15 { p * p.ln() } else { 0.0 }
408            })
409            .sum::<f64>()
410    }
411
412    /// Collapse to the most probable hypothesis.
413    pub fn collapse(&self) -> &Hypothesis {
414        self.hypotheses
415            .iter()
416            .max_by(|a, b| {
417                a.amplitude
418                    .abs()
419                    .partial_cmp(&b.amplitude.abs())
420                    .unwrap_or(std::cmp::Ordering::Equal)
421            })
422            .expect("cannot collapse empty superposition")
423    }
424
425    fn normalize(&mut self) {
426        let norm: f64 = self
427            .hypotheses
428            .iter()
429            .map(|h| h.amplitude.powi(2))
430            .sum::<f64>()
431            .sqrt();
432        if norm > 1e-15 {
433            for h in &mut self.hypotheses {
434                h.amplitude /= norm;
435            }
436        }
437    }
438}
439
440// ---------------------------------------------------------------------------
441// Tests
442// ---------------------------------------------------------------------------
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447
448    // ── Complex arithmetic ──────────────────────────────────────────
449
450    #[test]
451    fn complex_add() {
452        let a = Complex::new(1.0, 2.0);
453        let b = Complex::new(3.0, 4.0);
454        let c = a.add(&b);
455        assert!((c.re - 4.0).abs() < 1e-15);
456        assert!((c.im - 6.0).abs() < 1e-15);
457    }
458
459    #[test]
460    fn complex_sub() {
461        let a = Complex::new(5.0, 3.0);
462        let b = Complex::new(2.0, 1.0);
463        let c = a.sub(&b);
464        assert!((c.re - 3.0).abs() < 1e-15);
465        assert!((c.im - 2.0).abs() < 1e-15);
466    }
467
468    #[test]
469    fn complex_mul() {
470        // (1+2i)*(3+4i) = 3+4i+6i+8i^2 = (3-8)+(4+6)i = -5+10i
471        let a = Complex::new(1.0, 2.0);
472        let b = Complex::new(3.0, 4.0);
473        let c = a.mul(&b);
474        assert!((c.re - (-5.0)).abs() < 1e-15);
475        assert!((c.im - 10.0).abs() < 1e-15);
476    }
477
478    #[test]
479    fn complex_conj() {
480        let a = Complex::new(3.0, -4.0);
481        let c = a.conj();
482        assert!((c.re - 3.0).abs() < 1e-15);
483        assert!((c.im - 4.0).abs() < 1e-15);
484    }
485
486    #[test]
487    fn complex_norm() {
488        let a = Complex::new(3.0, 4.0);
489        assert!((a.norm() - 5.0).abs() < 1e-15);
490        assert!((a.norm_sq() - 25.0).abs() < 1e-15);
491    }
492
493    #[test]
494    fn complex_scale() {
495        let a = Complex::new(2.0, 3.0);
496        let c = a.scale(2.0);
497        assert!((c.re - 4.0).abs() < 1e-15);
498        assert!((c.im - 6.0).abs() < 1e-15);
499    }
500
501    #[test]
502    fn complex_zero() {
503        let z = Complex::zero();
504        assert!((z.re).abs() < 1e-15);
505        assert!((z.im).abs() < 1e-15);
506    }
507
508    #[test]
509    fn complex_real() {
510        let r = Complex::real(42.0);
511        assert!((r.re - 42.0).abs() < 1e-15);
512        assert!((r.im).abs() < 1e-15);
513    }
514
515    // ── from_fiedler ────────────────────────────────────────────────
516
517    #[test]
518    fn from_fiedler_normalizes() {
519        let fiedler = vec![1.0, -1.0, 0.5];
520        let ids = vec![10, 20, 30];
521        let qs = QuantumCognitiveState::from_fiedler(&fiedler, &ids);
522
523        // Probabilities should sum to 1
524        let total: f64 = qs.probabilities().iter().sum();
525        assert!(
526            (total - 1.0).abs() < 1e-12,
527            "probabilities should sum to 1.0, got {total}"
528        );
529    }
530
531    #[test]
532    fn from_fiedler_preserves_signs() {
533        let fiedler = vec![1.0, -1.0, 0.5];
534        let ids = vec![10, 20, 30];
535        let qs = QuantumCognitiveState::from_fiedler(&fiedler, &ids);
536        let classical = qs.to_classical();
537        assert!(classical[0] > 0.0);
538        assert!(classical[1] < 0.0);
539        assert!(classical[2] > 0.0);
540    }
541
542    // ── uniform superposition ───────────────────────────────────────
543
544    #[test]
545    fn uniform_equal_probabilities() {
546        let n = 4;
547        let ids = vec![1, 2, 3, 4];
548        let qs = QuantumCognitiveState::uniform(n, &ids);
549        let probs = qs.probabilities();
550        for p in &probs {
551            assert!(
552                (p - 0.25).abs() < 1e-12,
553                "uniform: each probability should be 0.25, got {p}"
554            );
555        }
556    }
557
558    #[test]
559    fn uniform_probabilities_sum_to_one() {
560        let qs = QuantumCognitiveState::uniform(5, &[1, 2, 3, 4, 5]);
561        let total: f64 = qs.probabilities().iter().sum();
562        assert!(
563            (total - 1.0).abs() < 1e-12,
564            "uniform probabilities should sum to 1.0, got {total}"
565        );
566    }
567
568    // ── entropy ─────────────────────────────────────────────────────
569
570    #[test]
571    fn entropy_uniform_is_ln_n() {
572        let n = 4;
573        let qs = QuantumCognitiveState::uniform(n, &[1, 2, 3, 4]);
574        let expected = (n as f64).ln();
575        let actual = qs.entropy();
576        assert!(
577            (actual - expected).abs() < 1e-12,
578            "entropy of uniform should be ln({n})={expected}, got {actual}"
579        );
580    }
581
582    #[test]
583    fn entropy_collapsed_is_zero() {
584        // State with all amplitude on one node
585        let mut qs = QuantumCognitiveState::uniform(3, &[1, 2, 3]);
586        qs.psi[0] = Complex::real(1.0);
587        qs.psi[1] = Complex::zero();
588        qs.psi[2] = Complex::zero();
589        let s = qs.entropy();
590        assert!(
591            s.abs() < 1e-12,
592            "entropy of collapsed state should be ~0, got {s}"
593        );
594    }
595
596    // ── evidence_impact matches classical delta_lambda2 ─────────────
597
598    #[test]
599    fn evidence_impact_matches_classical() {
600        // When the state is the normalized Fiedler vector (all real),
601        // evidence_impact should equal w * (phi[u] - phi[v])^2.
602        let fiedler = vec![0.5, -0.5, 0.0];
603        let norm: f64 = fiedler.iter().map(|x| x * x).sum::<f64>().sqrt();
604        let ids = vec![0, 1, 2];
605        let qs = QuantumCognitiveState::from_fiedler(&fiedler, &ids);
606
607        let w = 1.0;
608        let quantum_impact = qs.evidence_impact(0, 1, w);
609        // Classical: w * ((0.5/norm) - (-0.5/norm))^2 = w * (1.0/norm)^2
610        let classical = w * ((0.5 / norm) - (-0.5 / norm)).powi(2);
611        assert!(
612            (quantum_impact - classical).abs() < 1e-12,
613            "quantum impact {quantum_impact} should match classical {classical}"
614        );
615    }
616
617    // ── observe_evidence ────────────────────────────────────────────
618
619    #[test]
620    fn observe_evidence_boosts_targets() {
621        let qs_before = QuantumCognitiveState::uniform(4, &[1, 2, 3, 4]);
622        let p_before = qs_before.probability(0);
623
624        let mut qs = qs_before;
625        qs.observe_evidence(0, 1, 1.0);
626        let p_after = qs.probability(0);
627
628        assert!(
629            p_after > p_before,
630            "observe_evidence should boost target probability: {p_before} -> {p_after}"
631        );
632    }
633
634    #[test]
635    fn observe_evidence_keeps_normalization() {
636        let mut qs = QuantumCognitiveState::uniform(4, &[1, 2, 3, 4]);
637        qs.observe_evidence(0, 1, 2.0);
638        qs.observe_evidence(1, 2, 0.5);
639        let total: f64 = qs.probabilities().iter().sum();
640        assert!(
641            (total - 1.0).abs() < 1e-12,
642            "probabilities should sum to 1 after observations, got {total}"
643        );
644    }
645
646    #[test]
647    fn observe_evidence_increments_count() {
648        let mut qs = QuantumCognitiveState::uniform(3, &[1, 2, 3]);
649        assert_eq!(qs.measurement_count(), 0);
650        qs.observe_evidence(0, 1, 1.0);
651        assert_eq!(qs.measurement_count(), 1);
652        qs.observe_evidence(1, 2, 1.0);
653        assert_eq!(qs.measurement_count(), 2);
654    }
655
656    // ── collapse ────────────────────────────────────────────────────
657
658    #[test]
659    fn collapse_returns_highest_probability() {
660        let mut qs = QuantumCognitiveState::uniform(3, &[1, 2, 3]);
661        // Make node 1 dominant
662        qs.psi[1] = Complex::real(10.0);
663        qs.normalize();
664        let (idx, prob) = qs.collapse();
665        assert_eq!(idx, 1);
666        assert!(prob > 0.9);
667    }
668
669    #[test]
670    fn collapse_uniform_returns_valid_index() {
671        let qs = QuantumCognitiveState::uniform(5, &[10, 20, 30, 40, 50]);
672        let (idx, prob) = qs.collapse();
673        assert!(idx < 5);
674        assert!(prob > 0.0);
675    }
676
677    // ── to_classical ────────────────────────────────────────────────
678
679    #[test]
680    fn to_classical_returns_real_parts() {
681        let fiedler = vec![1.0, -1.0, 0.5];
682        let ids = vec![0, 1, 2];
683        let qs = QuantumCognitiveState::from_fiedler(&fiedler, &ids);
684        let classical = qs.to_classical();
685        assert_eq!(classical.len(), 3);
686        // All imaginary parts should be zero for a Fiedler-initialized state
687        for a in &qs.psi {
688            assert!(a.im.abs() < 1e-15);
689        }
690    }
691
692    // ── is_collapsed ────────────────────────────────────────────────
693
694    #[test]
695    fn is_collapsed_detects_low_entropy() {
696        let mut qs = QuantumCognitiveState::uniform(3, &[1, 2, 3]);
697        assert!(
698            !qs.is_collapsed(0.1),
699            "uniform state should not be collapsed"
700        );
701
702        qs.psi[0] = Complex::real(1.0);
703        qs.psi[1] = Complex::zero();
704        qs.psi[2] = Complex::zero();
705        assert!(
706            qs.is_collapsed(0.1),
707            "single-node state should be collapsed"
708        );
709    }
710
711    // ── estimated_measurements_to_collapse ──────────────────────────
712
713    #[test]
714    fn estimated_measurements_none_without_history() {
715        let qs = QuantumCognitiveState::uniform(3, &[1, 2, 3]);
716        assert!(qs.estimated_measurements_to_collapse().is_none());
717    }
718
719    #[test]
720    fn estimated_measurements_converging() {
721        let mut qs = QuantumCognitiveState::uniform(4, &[1, 2, 3, 4]);
722        // Simulate a series of observations that reduce entropy
723        for _ in 0..5 {
724            qs.observe_evidence(0, 1, 1.0);
725        }
726        // With enough history and converging entropy, should return Some
727        if qs.entropy_history.len() >= 2 {
728            let first = qs.entropy_history.first().unwrap();
729            let last = qs.entropy_history.last().unwrap();
730            if first > last {
731                assert!(qs.estimated_measurements_to_collapse().is_some());
732            }
733        }
734    }
735
736    // ── rank_evidence_quantum ───────────────────────────────────────
737
738    #[test]
739    fn rank_evidence_quantum_ordered_by_impact() {
740        let fiedler = vec![0.7, -0.7, 0.1];
741        let ids = vec![0, 1, 2];
742        let qs = QuantumCognitiveState::from_fiedler(&fiedler, &ids);
743
744        let candidates = vec![(0, 1, 1.0), (0, 2, 1.0), (1, 2, 1.0)];
745        let ranked = qs.rank_evidence_quantum(&candidates);
746
747        // Should be sorted descending by impact
748        for i in 1..ranked.len() {
749            assert!(
750                ranked[i - 1].impact >= ranked[i].impact,
751                "rankings should be sorted descending"
752            );
753        }
754    }
755
756    #[test]
757    fn rank_evidence_quantum_contains_probabilities() {
758        let qs = QuantumCognitiveState::uniform(3, &[0, 1, 2]);
759        let candidates = vec![(0, 1, 1.0)];
760        let ranked = qs.rank_evidence_quantum(&candidates);
761        assert_eq!(ranked.len(), 1);
762        assert!(ranked[0].prob_u > 0.0);
763        assert!(ranked[0].prob_v > 0.0);
764    }
765
766    // ── evolve ──────────────────────────────────────────────────────
767
768    #[test]
769    fn evolve_identity_laplacian_no_change() {
770        let qs_before = QuantumCognitiveState::uniform(3, &[1, 2, 3]);
771        let probs_before = qs_before.probabilities();
772
773        let mut qs = qs_before;
774        // Identity Laplacian action returns zeros (H|psi> = 0 for uniform
775        // on a complete graph with Laplacian having uniform eigenvector as
776        // eigenvalue 0). Simulate zero Hamiltonian.
777        qs.evolve(|psi| vec![Complex::zero(); psi.len()], 1.0);
778        let probs_after = qs.probabilities();
779
780        for (pb, pa) in probs_before.iter().zip(probs_after.iter()) {
781            assert!(
782                (pb - pa).abs() < 1e-12,
783                "zero Hamiltonian should not change probabilities"
784            );
785        }
786    }
787
788    #[test]
789    fn evolve_preserves_normalization() {
790        let mut qs = QuantumCognitiveState::from_fiedler(&[1.0, -1.0, 0.5], &[0, 1, 2]);
791        // Simple diagonal Laplacian action
792        qs.evolve(
793            |psi| {
794                psi.iter()
795                    .enumerate()
796                    .map(|(i, a)| a.scale(i as f64))
797                    .collect()
798            },
799            0.1,
800        );
801        let total: f64 = qs.probabilities().iter().sum();
802        assert!(
803            (total - 1.0).abs() < 1e-10,
804            "evolve should preserve normalization, got {total}"
805        );
806    }
807
808    // ── suppress ────────────────────────────────────────────────────
809
810    #[test]
811    fn suppress_dampens_amplitude() {
812        let mut qs = QuantumCognitiveState::uniform(3, &[1, 2, 3]);
813        let p_before = qs.probability(0);
814        qs.suppress(0, 0.1);
815        let p_after = qs.probability(0);
816        assert!(
817            p_after < p_before,
818            "suppress should reduce probability: {p_before} -> {p_after}"
819        );
820    }
821
822    #[test]
823    fn suppress_keeps_normalization() {
824        let mut qs = QuantumCognitiveState::uniform(4, &[1, 2, 3, 4]);
825        qs.suppress(2, 0.01);
826        let total: f64 = qs.probabilities().iter().sum();
827        assert!(
828            (total - 1.0).abs() < 1e-12,
829            "suppress should preserve normalization, got {total}"
830        );
831    }
832
833    // ── Born rule: probabilities always sum to 1 ────────────────────
834
835    #[test]
836    fn born_rule_sum_after_all_operations() {
837        let mut qs = QuantumCognitiveState::from_fiedler(&[1.0, -0.5, 0.3, -0.2], &[0, 1, 2, 3]);
838
839        // Evolve
840        qs.evolve(|psi| psi.iter().map(|a| a.scale(0.5)).collect(), 0.1);
841
842        // Observe
843        qs.observe_evidence(0, 1, 0.8);
844
845        // Suppress
846        qs.suppress(3, 0.2);
847
848        // Observe again
849        qs.observe_evidence(1, 2, 1.5);
850
851        let total: f64 = qs.probabilities().iter().sum();
852        assert!(
853            (total - 1.0).abs() < 1e-12,
854            "probabilities must sum to 1.0 after mixed operations, got {total}"
855        );
856    }
857
858    // ── HypothesisSuperposition ─────────────────────────────────────
859
860    #[test]
861    fn hypothesis_add_redistributes() {
862        let mut hs = HypothesisSuperposition::new();
863        hs.add_hypothesis("h1", "First", vec![(0, 1)]);
864        assert_eq!(hs.hypotheses.len(), 1);
865        assert!((hs.hypotheses[0].amplitude - 1.0).abs() < 1e-12);
866
867        hs.add_hypothesis("h2", "Second", vec![(1, 2)]);
868        assert_eq!(hs.hypotheses.len(), 2);
869        let expected = 1.0 / 2.0f64.sqrt();
870        for h in &hs.hypotheses {
871            assert!(
872                (h.amplitude - expected).abs() < 1e-12,
873                "amplitude should be 1/sqrt(2), got {}",
874                h.amplitude
875            );
876        }
877    }
878
879    #[test]
880    fn hypothesis_probabilities_sum_to_one() {
881        let mut hs = HypothesisSuperposition::new();
882        hs.add_hypothesis("h1", "A", vec![(0, 1)]);
883        hs.add_hypothesis("h2", "B", vec![(1, 2)]);
884        hs.add_hypothesis("h3", "C", vec![(2, 3)]);
885        let total: f64 = hs.probabilities().iter().map(|(_, p)| p).sum();
886        assert!(
887            (total - 1.0).abs() < 1e-12,
888            "hypothesis probabilities should sum to 1, got {total}"
889        );
890    }
891
892    #[test]
893    fn hypothesis_observe_boosts_supported() {
894        let mut hs = HypothesisSuperposition::new();
895        hs.add_hypothesis("h1", "Supported", vec![(0, 1)]);
896        hs.add_hypothesis("h2", "Unsupported", vec![(2, 3)]);
897
898        let probs_before = hs.probabilities();
899        let p1_before = probs_before[0].1;
900
901        hs.observe((0, 1)); // supports h1
902        let probs_after = hs.probabilities();
903        let p1_after = probs_after[0].1;
904
905        assert!(
906            p1_after > p1_before,
907            "h1 probability should increase: {p1_before} -> {p1_after}"
908        );
909    }
910
911    #[test]
912    fn hypothesis_observe_keeps_normalization() {
913        let mut hs = HypothesisSuperposition::new();
914        hs.add_hypothesis("h1", "A", vec![(0, 1)]);
915        hs.add_hypothesis("h2", "B", vec![(1, 2)]);
916        hs.observe((0, 1));
917        hs.observe((1, 2));
918        hs.observe((0, 1));
919        let total: f64 = hs.probabilities().iter().map(|(_, p)| p).sum();
920        assert!(
921            (total - 1.0).abs() < 1e-12,
922            "hypothesis probabilities should sum to 1 after observations, got {total}"
923        );
924    }
925
926    #[test]
927    fn hypothesis_collapse_returns_most_probable() {
928        let mut hs = HypothesisSuperposition::new();
929        hs.add_hypothesis("h1", "Winner", vec![(0, 1)]);
930        hs.add_hypothesis("h2", "Loser", vec![(2, 3)]);
931        // Boost h1 several times
932        hs.observe((0, 1));
933        hs.observe((0, 1));
934        hs.observe((0, 1));
935        let winner = hs.collapse();
936        assert_eq!(winner.id, "h1");
937    }
938
939    #[test]
940    fn hypothesis_entropy_decreases_with_evidence() {
941        let mut hs = HypothesisSuperposition::new();
942        hs.add_hypothesis("h1", "A", vec![(0, 1)]);
943        hs.add_hypothesis("h2", "B", vec![(2, 3)]);
944        let s_before = hs.entropy();
945        hs.observe((0, 1));
946        let s_after = hs.entropy();
947        assert!(
948            s_after < s_before,
949            "entropy should decrease with evidence: {s_before} -> {s_after}"
950        );
951    }
952
953    #[test]
954    fn hypothesis_default_is_empty() {
955        let hs = HypothesisSuperposition::default();
956        assert!(hs.hypotheses.is_empty());
957    }
958}