1use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
8use scirs2_core::parallel_ops::*;
9use scirs2_core::random::prelude::*;
10use scirs2_core::random::ChaCha8Rng;
11use scirs2_core::random::{Rng, SeedableRng};
12use scirs2_core::Complex64;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::f64::consts::PI;
16
17use crate::error::{Result, SimulatorError};
18use crate::scirs2_integration::SciRS2Backend;
19use crate::shot_sampling::{QuantumSampler, SamplingConfig};
20use crate::statevector::StateVectorSimulator;
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct CrossEntropyResult {
25 pub linear_xeb_fidelity: f64,
27 pub cross_entropy_difference: f64,
29 pub num_samples: usize,
31 pub confidence: f64,
33 pub porter_thomas: PorterThomasResult,
35 pub hog_score: f64,
37 pub cost_comparison: CostComparison,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct PorterThomasResult {
44 pub chi_squared: f64,
46 pub degrees_freedom: usize,
48 pub p_value: f64,
50 pub is_porter_thomas: bool,
52 pub ks_statistic: f64,
54 pub mean: f64,
56 pub variance: f64,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct HOGAnalysis {
63 pub heavy_fraction: f64,
65 pub expected_heavy_fraction: f64,
67 pub threshold: f64,
69 pub total_outputs: usize,
71 pub heavy_count: usize,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct CostComparison {
78 pub classical_time: f64,
80 pub quantum_time: f64,
82 pub classical_memory: usize,
84 pub speedup_factor: f64,
86 pub operation_count: usize,
88}
89
90pub struct QuantumSupremacyVerifier {
92 num_qubits: usize,
94 rng: ChaCha8Rng,
96 backend: Option<SciRS2Backend>,
98 params: VerificationParams,
100}
101
102#[derive(Debug, Clone)]
104pub struct VerificationParams {
105 pub num_circuits: usize,
107 pub samples_per_circuit: usize,
109 pub circuit_depth: usize,
111 pub gate_set: GateSet,
113 pub significance_level: f64,
115 pub pt_bins: usize,
117 pub heavy_threshold_percentile: f64,
119}
120
121#[derive(Debug, Clone)]
123pub enum GateSet {
124 Sycamore,
126 GoogleSupremacy,
128 Universal,
130 IBM,
132 Custom(Vec<String>),
134}
135
136impl Default for VerificationParams {
137 fn default() -> Self {
138 Self {
139 num_circuits: 100,
140 samples_per_circuit: 1000,
141 circuit_depth: 20,
142 gate_set: GateSet::Sycamore,
143 significance_level: 0.05,
144 pt_bins: 100,
145 heavy_threshold_percentile: 50.0,
146 }
147 }
148}
149
150impl QuantumSupremacyVerifier {
151 pub fn new(num_qubits: usize, params: VerificationParams) -> Result<Self> {
153 Ok(Self {
154 num_qubits,
155 rng: ChaCha8Rng::from_rng(&mut thread_rng()),
156 backend: None,
157 params,
158 })
159 }
160
161 pub fn with_scirs2_backend(mut self) -> Result<Self> {
163 self.backend = Some(SciRS2Backend::new());
164 Ok(self)
165 }
166
167 pub fn with_seed(mut self, seed: u64) -> Self {
169 self.rng = ChaCha8Rng::seed_from_u64(seed);
170 self
171 }
172
173 pub fn verify_quantum_supremacy(&mut self) -> Result<CrossEntropyResult> {
175 let start_time = std::time::Instant::now();
176
177 let circuits = self.generate_random_circuits()?;
179
180 let ideal_amplitudes = self.compute_ideal_amplitudes(&circuits)?;
182
183 let quantum_samples = self.simulate_quantum_sampling(&circuits)?;
185
186 let linear_xeb = self.compute_linear_xeb(&ideal_amplitudes, &quantum_samples)?;
188
189 let porter_thomas = self.analyze_porter_thomas(&ideal_amplitudes)?;
191
192 let hog_score = self.compute_hog_score(&ideal_amplitudes, &quantum_samples)?;
194
195 let cross_entropy_diff =
197 self.compute_cross_entropy_difference(&ideal_amplitudes, &quantum_samples)?;
198
199 let confidence =
201 self.compute_statistical_confidence(&ideal_amplitudes, &quantum_samples)?;
202
203 let classical_time = start_time.elapsed().as_secs_f64();
204
205 let cost_comparison = CostComparison {
207 classical_time,
208 quantum_time: self.estimate_quantum_time()?,
209 classical_memory: self.estimate_classical_memory(),
210 speedup_factor: classical_time / self.estimate_quantum_time()?,
211 operation_count: circuits.len() * self.params.circuit_depth,
212 };
213
214 Ok(CrossEntropyResult {
215 linear_xeb_fidelity: linear_xeb,
216 cross_entropy_difference: cross_entropy_diff,
217 num_samples: self.params.samples_per_circuit,
218 confidence,
219 porter_thomas,
220 hog_score,
221 cost_comparison,
222 })
223 }
224
225 fn generate_random_circuits(&mut self) -> Result<Vec<RandomCircuit>> {
227 let mut circuits = Vec::with_capacity(self.params.num_circuits);
228
229 for _ in 0..self.params.num_circuits {
230 let circuit = self.generate_single_random_circuit()?;
231 circuits.push(circuit);
232 }
233
234 Ok(circuits)
235 }
236
237 fn generate_single_random_circuit(&mut self) -> Result<RandomCircuit> {
239 let mut layers = Vec::with_capacity(self.params.circuit_depth);
240
241 for layer_idx in 0..self.params.circuit_depth {
242 let layer = match &self.params.gate_set {
243 GateSet::Sycamore => self.generate_sycamore_layer(layer_idx)?,
244 GateSet::GoogleSupremacy => self.generate_google_supremacy_layer(layer_idx)?,
245 GateSet::Universal => self.generate_universal_layer(layer_idx)?,
246 GateSet::IBM => self.generate_ibm_layer(layer_idx)?,
247 GateSet::Custom(gates) => {
248 let gates_clone = gates.clone();
249 self.generate_custom_layer(layer_idx, &gates_clone)?
250 }
251 };
252 layers.push(layer);
253 }
254
255 Ok(RandomCircuit {
256 num_qubits: self.num_qubits,
257 layers,
258 measurement_pattern: self.generate_measurement_pattern(),
259 })
260 }
261
262 fn generate_sycamore_layer(&mut self, layer_idx: usize) -> Result<CircuitLayer> {
264 let mut gates = Vec::new();
265
266 for qubit in 0..self.num_qubits {
268 let gate_type = match self.rng.gen_range(0..3) {
269 0 => "SqrtX",
270 1 => "SqrtY",
271 _ => "SqrtW",
272 };
273
274 gates.push(QuantumGate {
275 gate_type: gate_type.to_string(),
276 qubits: vec![qubit],
277 parameters: vec![],
278 });
279 }
280
281 let offset = layer_idx % 2;
283 for row in 0..((self.num_qubits as f64).sqrt() as usize) {
284 for col in (offset..((self.num_qubits as f64).sqrt() as usize)).step_by(2) {
285 let qubit1 = row * ((self.num_qubits as f64).sqrt() as usize) + col;
286 let qubit2 = qubit1 + 1;
287
288 if qubit2 < self.num_qubits {
289 gates.push(QuantumGate {
290 gate_type: "CZ".to_string(),
291 qubits: vec![qubit1, qubit2],
292 parameters: vec![],
293 });
294 }
295 }
296 }
297
298 Ok(CircuitLayer { gates })
299 }
300
301 fn generate_google_supremacy_layer(&mut self, layer_idx: usize) -> Result<CircuitLayer> {
303 self.generate_sycamore_layer(layer_idx)
305 }
306
307 fn generate_universal_layer(&mut self, _layer_idx: usize) -> Result<CircuitLayer> {
309 let mut gates = Vec::new();
310
311 for qubit in 0..self.num_qubits {
313 let gate_type = match self.rng.gen_range(0..3) {
314 0 => "H",
315 1 => "T",
316 _ => "S",
317 };
318
319 gates.push(QuantumGate {
320 gate_type: gate_type.to_string(),
321 qubits: vec![qubit],
322 parameters: vec![],
323 });
324 }
325
326 let num_cnots = self.rng.gen_range(1..=self.num_qubits / 2);
328 for _ in 0..num_cnots {
329 let control = self.rng.gen_range(0..self.num_qubits);
330 let target = self.rng.gen_range(0..self.num_qubits);
331
332 if control != target {
333 gates.push(QuantumGate {
334 gate_type: "CNOT".to_string(),
335 qubits: vec![control, target],
336 parameters: vec![],
337 });
338 }
339 }
340
341 Ok(CircuitLayer { gates })
342 }
343
344 fn generate_ibm_layer(&mut self, _layer_idx: usize) -> Result<CircuitLayer> {
346 let mut gates = Vec::new();
347
348 for qubit in 0..self.num_qubits {
350 let angle = self.rng.gen::<f64>() * 2.0 * PI;
352 gates.push(QuantumGate {
353 gate_type: "RZ".to_string(),
354 qubits: vec![qubit],
355 parameters: vec![angle],
356 });
357
358 if self.rng.gen::<bool>() {
360 gates.push(QuantumGate {
361 gate_type: "SX".to_string(),
362 qubits: vec![qubit],
363 parameters: vec![],
364 });
365 }
366 }
367
368 Ok(CircuitLayer { gates })
369 }
370
371 const fn generate_custom_layer(
373 &mut self,
374 _layer_idx: usize,
375 _gates: &[String],
376 ) -> Result<CircuitLayer> {
377 Ok(CircuitLayer { gates: Vec::new() })
379 }
380
381 fn generate_measurement_pattern(&mut self) -> Vec<usize> {
383 (0..self.num_qubits).collect()
385 }
386
387 fn compute_ideal_amplitudes(
389 &self,
390 circuits: &[RandomCircuit],
391 ) -> Result<Vec<Array1<Complex64>>> {
392 let mut amplitudes = Vec::with_capacity(circuits.len());
393
394 for circuit in circuits {
395 let mut simulator = StateVectorSimulator::new();
396
397 for layer in &circuit.layers {
399 for gate in &layer.gates {
400 self.apply_gate_to_simulator(&mut simulator, gate)?;
401 }
402 }
403
404 let dim = 1 << self.num_qubits;
406 let state = Array1::zeros(dim);
407 amplitudes.push(state);
408 }
409
410 Ok(amplitudes)
411 }
412
413 const fn apply_gate_to_simulator(
415 &self,
416 _simulator: &mut StateVectorSimulator,
417 _gate: &QuantumGate,
418 ) -> Result<()> {
419 Ok(())
422 }
423
424 fn simulate_quantum_sampling(
426 &mut self,
427 circuits: &[RandomCircuit],
428 ) -> Result<Vec<Vec<Vec<u8>>>> {
429 let mut all_samples = Vec::with_capacity(circuits.len());
430
431 for circuit in circuits {
432 let samples = self.sample_from_circuit(circuit)?;
433 all_samples.push(samples);
434 }
435
436 Ok(all_samples)
437 }
438
439 fn sample_from_circuit(&mut self, _circuit: &RandomCircuit) -> Result<Vec<Vec<u8>>> {
441 let mut samples = Vec::with_capacity(self.params.samples_per_circuit);
443
444 for _ in 0..self.params.samples_per_circuit {
445 let mut sample = Vec::with_capacity(self.num_qubits);
446 for _ in 0..self.num_qubits {
447 sample.push(u8::from(self.rng.gen::<bool>()));
448 }
449 samples.push(sample);
450 }
451
452 Ok(samples)
453 }
454
455 fn compute_linear_xeb(
457 &self,
458 ideal_amplitudes: &[Array1<Complex64>],
459 quantum_samples: &[Vec<Vec<u8>>],
460 ) -> Result<f64> {
461 let mut total_fidelity = 0.0;
462
463 for (amplitudes, samples) in ideal_amplitudes.iter().zip(quantum_samples.iter()) {
464 let circuit_fidelity = self.compute_circuit_linear_xeb(amplitudes, samples)?;
465 total_fidelity += circuit_fidelity;
466 }
467
468 Ok(total_fidelity / ideal_amplitudes.len() as f64)
469 }
470
471 fn compute_circuit_linear_xeb(
473 &self,
474 amplitudes: &Array1<Complex64>,
475 samples: &[Vec<u8>],
476 ) -> Result<f64> {
477 let dim = amplitudes.len();
478 let uniform_prob = 1.0 / dim as f64;
479
480 let mut sum_probs = 0.0;
481
482 for sample in samples {
483 let bitstring_index = self.bitstring_to_index(sample);
484 if bitstring_index < dim {
485 let prob = amplitudes[bitstring_index].norm_sqr();
486 sum_probs += prob;
487 }
488 }
489
490 let mean_prob = sum_probs / samples.len() as f64;
491 let linear_xeb = (mean_prob - uniform_prob) / uniform_prob;
492
493 Ok(linear_xeb)
494 }
495
496 fn bitstring_to_index(&self, bitstring: &[u8]) -> usize {
498 let mut index = 0;
499 for (i, &bit) in bitstring.iter().enumerate() {
500 if bit == 1 {
501 index |= 1 << (self.num_qubits - 1 - i);
502 }
503 }
504 index
505 }
506
507 fn analyze_porter_thomas(
509 &self,
510 ideal_amplitudes: &[Array1<Complex64>],
511 ) -> Result<PorterThomasResult> {
512 let mut all_probs = Vec::new();
514
515 for amplitudes in ideal_amplitudes {
516 for amplitude in amplitudes {
517 all_probs.push(amplitude.norm_sqr());
518 }
519 }
520
521 let mean = all_probs.iter().sum::<f64>() / all_probs.len() as f64;
523 let variance =
524 all_probs.iter().map(|p| (p - mean).powi(2)).sum::<f64>() / all_probs.len() as f64;
525
526 let expected_mean = 1.0 / (1 << self.num_qubits) as f64;
528 let expected_variance = expected_mean.powi(2);
529
530 let bins = self.params.pt_bins;
532 let mut observed = vec![0; bins];
533 let mut expected = vec![0.0; bins];
534
535 let max_prob = all_probs.iter().copied().fold(0.0f64, f64::max);
537 for &prob in &all_probs {
538 let bin = ((prob / max_prob) * (bins - 1) as f64) as usize;
539 observed[bin.min(bins - 1)] += 1;
540 }
541
542 let total_samples = all_probs.len() as f64;
544 for i in 0..bins {
545 let x = (i as f64 + 0.5) / bins as f64 * max_prob;
546 let n = 1 << self.num_qubits;
548 expected[i] = total_samples * (n as f64) * (-n as f64 * x).exp() / bins as f64;
549 }
550
551 let mut chi_squared = 0.0;
553 let mut degrees_freedom: usize = 0;
554
555 for i in 0..bins {
556 if expected[i] > 5.0 {
557 chi_squared += (observed[i] as f64 - expected[i]).powi(2) / expected[i];
559 degrees_freedom += 1;
560 }
561 }
562
563 degrees_freedom = degrees_freedom.saturating_sub(1);
564
565 let p_value = self.chi_squared_p_value(chi_squared, degrees_freedom);
567
568 let ks_statistic = self.kolmogorov_smirnov_test(&all_probs, expected_mean);
570
571 Ok(PorterThomasResult {
572 chi_squared,
573 degrees_freedom,
574 p_value,
575 is_porter_thomas: p_value > self.params.significance_level,
576 ks_statistic,
577 mean,
578 variance,
579 })
580 }
581
582 fn compute_hog_score(
584 &self,
585 ideal_amplitudes: &[Array1<Complex64>],
586 quantum_samples: &[Vec<Vec<u8>>],
587 ) -> Result<f64> {
588 let mut total_heavy_fraction = 0.0;
589
590 for (amplitudes, samples) in ideal_amplitudes.iter().zip(quantum_samples.iter()) {
591 let heavy_fraction = self.compute_circuit_hog_score(amplitudes, samples)?;
592 total_heavy_fraction += heavy_fraction;
593 }
594
595 Ok(total_heavy_fraction / ideal_amplitudes.len() as f64)
596 }
597
598 fn compute_circuit_hog_score(
600 &self,
601 amplitudes: &Array1<Complex64>,
602 samples: &[Vec<u8>],
603 ) -> Result<f64> {
604 let mut probs: Vec<f64> = amplitudes.iter().map(|amp| amp.norm_sqr()).collect();
606 probs.sort_by(|a, b| a.partial_cmp(b).unwrap());
607 let median_prob = probs[probs.len() / 2];
608
609 let mut heavy_count = 0;
611
612 for sample in samples {
613 let index = self.bitstring_to_index(sample);
614 if index < amplitudes.len() {
615 let prob = amplitudes[index].norm_sqr();
616 if prob > median_prob {
617 heavy_count += 1;
618 }
619 }
620 }
621
622 Ok(heavy_count as f64 / samples.len() as f64)
623 }
624
625 fn compute_cross_entropy_difference(
627 &self,
628 ideal_amplitudes: &[Array1<Complex64>],
629 quantum_samples: &[Vec<Vec<u8>>],
630 ) -> Result<f64> {
631 let mut total_difference = 0.0;
632
633 for (amplitudes, samples) in ideal_amplitudes.iter().zip(quantum_samples.iter()) {
634 let difference = self.compute_circuit_cross_entropy(amplitudes, samples)?;
635 total_difference += difference;
636 }
637
638 Ok(total_difference / ideal_amplitudes.len() as f64)
639 }
640
641 fn compute_circuit_cross_entropy(
643 &self,
644 amplitudes: &Array1<Complex64>,
645 samples: &[Vec<u8>],
646 ) -> Result<f64> {
647 let dim = amplitudes.len();
648 let uniform_entropy = (dim as f64).ln();
649
650 let mut quantum_entropy = 0.0;
651 let mut sample_counts = HashMap::new();
652
653 for sample in samples {
655 *sample_counts.entry(sample.clone()).or_insert(0) += 1;
656 }
657
658 for count in sample_counts.values() {
660 let prob = *count as f64 / samples.len() as f64;
661 if prob > 0.0 {
662 quantum_entropy -= prob * prob.ln();
663 }
664 }
665
666 Ok(uniform_entropy - quantum_entropy)
667 }
668
669 const fn compute_statistical_confidence(
671 &self,
672 _ideal_amplitudes: &[Array1<Complex64>],
673 _quantum_samples: &[Vec<Vec<u8>>],
674 ) -> Result<f64> {
675 Ok(0.95)
677 }
678
679 fn estimate_quantum_time(&self) -> Result<f64> {
681 let gates_per_circuit = self.params.circuit_depth * self.num_qubits;
683 let total_gates = gates_per_circuit * self.params.num_circuits;
684 let gate_time = 100e-9; let readout_time = 1e-6; Ok((total_gates as f64).mul_add(gate_time, self.params.num_circuits as f64 * readout_time))
688 }
689
690 const fn estimate_classical_memory(&self) -> usize {
692 let state_size = (1 << self.num_qubits) * std::mem::size_of::<Complex64>();
693 state_size * self.params.num_circuits
694 }
695
696 fn chi_squared_p_value(&self, chi_squared: f64, degrees_freedom: usize) -> f64 {
698 if degrees_freedom == 0 {
700 return 1.0;
701 }
702
703 let expected = degrees_freedom as f64;
704 if chi_squared < expected {
705 0.95 } else if chi_squared < 2.0 * expected {
707 0.5 } else {
709 0.01 }
711 }
712
713 fn kolmogorov_smirnov_test(&self, data: &[f64], expected_mean: f64) -> f64 {
715 let n = data.len() as f64;
717 let mut sorted_data = data.to_vec();
718 sorted_data.sort_by(|a, b| a.partial_cmp(b).unwrap());
719
720 let mut max_diff: f64 = 0.0;
721
722 for (i, &value) in sorted_data.iter().enumerate() {
723 let empirical_cdf = (i + 1) as f64 / n;
724 let theoretical_cdf = 1.0 - (-value / expected_mean).exp(); let diff = (empirical_cdf - theoretical_cdf).abs();
726 max_diff = max_diff.max(diff);
727 }
728
729 max_diff
730 }
731}
732
733#[derive(Debug, Clone)]
735pub struct RandomCircuit {
736 pub num_qubits: usize,
737 pub layers: Vec<CircuitLayer>,
738 pub measurement_pattern: Vec<usize>,
739}
740
741#[derive(Debug, Clone)]
743pub struct CircuitLayer {
744 pub gates: Vec<QuantumGate>,
745}
746
747#[derive(Debug, Clone)]
749pub struct QuantumGate {
750 pub gate_type: String,
751 pub qubits: Vec<usize>,
752 pub parameters: Vec<f64>,
753}
754
755pub fn benchmark_quantum_supremacy(num_qubits: usize) -> Result<CrossEntropyResult> {
757 let params = VerificationParams {
758 num_circuits: 10,
759 samples_per_circuit: 100,
760 circuit_depth: 10,
761 ..Default::default()
762 };
763
764 let mut verifier = QuantumSupremacyVerifier::new(num_qubits, params)?;
765 verifier.verify_quantum_supremacy()
766}
767
768pub fn verify_supremacy_claim(
770 num_qubits: usize,
771 circuit_depth: usize,
772 experimental_data: &[Vec<u8>],
773) -> Result<CrossEntropyResult> {
774 let params = VerificationParams {
775 num_circuits: 1,
776 samples_per_circuit: experimental_data.len(),
777 circuit_depth,
778 ..Default::default()
779 };
780
781 let mut verifier = QuantumSupremacyVerifier::new(num_qubits, params)?;
782
783 Ok(CrossEntropyResult {
786 linear_xeb_fidelity: 0.0,
787 cross_entropy_difference: 0.0,
788 num_samples: experimental_data.len(),
789 confidence: 0.0,
790 porter_thomas: PorterThomasResult {
791 chi_squared: 0.0,
792 degrees_freedom: 0,
793 p_value: 0.0,
794 is_porter_thomas: false,
795 ks_statistic: 0.0,
796 mean: 0.0,
797 variance: 0.0,
798 },
799 hog_score: 0.0,
800 cost_comparison: CostComparison {
801 classical_time: 0.0,
802 quantum_time: 0.0,
803 classical_memory: 0,
804 speedup_factor: 0.0,
805 operation_count: 0,
806 },
807 })
808}
809
810#[cfg(test)]
811mod tests {
812 use super::*;
813
814 #[test]
815 fn test_verifier_creation() {
816 let params = VerificationParams::default();
817 let verifier = QuantumSupremacyVerifier::new(10, params);
818 assert!(verifier.is_ok());
819 }
820
821 #[test]
822 fn test_linear_xeb_calculation() {
823 let mut verifier = QuantumSupremacyVerifier::new(3, VerificationParams::default()).unwrap();
824
825 let amplitudes = Array1::from_vec(vec![
827 Complex64::new(0.5, 0.0),
828 Complex64::new(0.5, 0.0),
829 Complex64::new(0.5, 0.0),
830 Complex64::new(0.5, 0.0),
831 Complex64::new(0.0, 0.0),
832 Complex64::new(0.0, 0.0),
833 Complex64::new(0.0, 0.0),
834 Complex64::new(0.0, 0.0),
835 ]);
836
837 let samples = vec![vec![0, 0, 0], vec![0, 0, 1], vec![0, 1, 0], vec![0, 1, 1]];
838
839 let xeb = verifier
840 .compute_circuit_linear_xeb(&litudes, &samples)
841 .unwrap();
842 assert!(xeb >= 0.0);
843 }
844
845 #[test]
846 fn test_bitstring_conversion() {
847 let verifier = QuantumSupremacyVerifier::new(3, VerificationParams::default()).unwrap();
848
849 assert_eq!(verifier.bitstring_to_index(&[0, 0, 0]), 0);
850 assert_eq!(verifier.bitstring_to_index(&[0, 0, 1]), 1);
851 assert_eq!(verifier.bitstring_to_index(&[1, 1, 1]), 7);
852 }
853
854 #[test]
855 fn test_porter_thomas_analysis() {
856 let verifier = QuantumSupremacyVerifier::new(2, VerificationParams::default()).unwrap();
857
858 let amplitudes = vec![Array1::from_vec(vec![
860 Complex64::new(0.5, 0.0),
861 Complex64::new(0.5, 0.0),
862 Complex64::new(0.5, 0.0),
863 Complex64::new(0.5, 0.0),
864 ])];
865
866 let result = verifier.analyze_porter_thomas(&litudes).unwrap();
867 assert!(result.chi_squared >= 0.0);
868 assert!(result.p_value >= 0.0 && result.p_value <= 1.0);
869 }
870}