1use serde::{Deserialize, Serialize};
15
16#[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#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct QuantumCognitiveState {
93 psi: Vec<Complex>,
95 node_ids: Vec<u64>,
97 measurement_count: u64,
99 entropy_history: Vec<f64>,
101}
102
103impl QuantumCognitiveState {
104 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 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 pub fn probability(&self, i: usize) -> f64 {
136 self.psi.get(i).map(|a| a.norm_sq()).unwrap_or(0.0)
137 }
138
139 pub fn probabilities(&self) -> Vec<f64> {
141 self.psi.iter().map(|a| a.norm_sq()).collect()
142 }
143
144 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 pub fn evolve(&mut self, laplacian_action: impl Fn(&[Complex]) -> Vec<Complex>, dt: f64) {
166 let h_psi = laplacian_action(&self.psi);
168 for (i, hp) in h_psi.iter().enumerate() {
169 self.psi[i].re += dt * hp.im;
171 self.psi[i].im -= dt * hp.re;
172 }
173 self.normalize();
174 }
175
176 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 pub fn rank_evidence_quantum(
189 &self,
190 candidates: &[(usize, usize, f64)], ) -> 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 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 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 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 pub fn to_classical(&self) -> Vec<f64> {
255 self.psi.iter().map(|a| a.re).collect()
256 }
257
258 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 pub fn is_collapsed(&self, threshold: f64) -> bool {
271 self.entropy() < threshold
272 }
273
274 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; }
289 let remaining = self.entropy() / rate;
290 Some(remaining.ceil() as usize)
291 }
292
293 pub fn dimension(&self) -> usize {
295 self.psi.len()
296 }
297
298 pub fn measurement_count(&self) -> u64 {
300 self.measurement_count
301 }
302
303 pub fn node_ids(&self) -> &[u64] {
305 &self.node_ids
306 }
307}
308
309#[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#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct Hypothesis {
332 pub id: String,
333 pub amplitude: f64,
334 pub description: String,
335 pub supporting_edges: Vec<(u64, u64)>,
337}
338
339#[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 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 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 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 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; } else {
394 h.amplitude *= 0.8; }
396 }
397 self.normalize();
398 }
399
400 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 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#[cfg(test)]
445mod tests {
446 use super::*;
447
448 #[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 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 #[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 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 #[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 #[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 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 #[test]
599 fn evidence_impact_matches_classical() {
600 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 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 #[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 #[test]
659 fn collapse_returns_highest_probability() {
660 let mut qs = QuantumCognitiveState::uniform(3, &[1, 2, 3]);
661 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 #[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 for a in &qs.psi {
688 assert!(a.im.abs() < 1e-15);
689 }
690 }
691
692 #[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 #[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 for _ in 0..5 {
724 qs.observe_evidence(0, 1, 1.0);
725 }
726 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 #[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 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 #[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 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 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 #[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 #[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 qs.evolve(|psi| psi.iter().map(|a| a.scale(0.5)).collect(), 0.1);
841
842 qs.observe_evidence(0, 1, 0.8);
844
845 qs.suppress(3, 0.2);
847
848 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 #[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)); 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 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}