1use crate::{
20 error::{QuantRS2Error, QuantRS2Result},
21 gate::GateOp,
22 qubit::QubitId,
23};
24use scirs2_core::ndarray::{Array1, Array2, Array3, Array4};
25use scirs2_core::random::prelude::*;
26use scirs2_core::Complex64;
27use std::collections::HashMap;
28
29pub struct QuantumVolume {
34 pub max_qubits: usize,
36 pub num_circuits: usize,
38 pub num_shots: usize,
40 pub success_threshold: f64,
42 rng: ThreadRng,
44}
45
46impl QuantumVolume {
47 pub fn new(max_qubits: usize, num_circuits: usize, num_shots: usize) -> Self {
49 Self {
50 max_qubits,
51 num_circuits,
52 num_shots,
53 success_threshold: 2.0 / 3.0, rng: thread_rng(),
55 }
56 }
57
58 pub fn run<F>(&mut self, mut circuit_executor: F) -> QuantRS2Result<QuantumVolumeResult>
62 where
63 F: FnMut(&[Box<dyn GateOp>], usize) -> Vec<usize>, {
65 let mut results = HashMap::new();
66 let mut quantum_volume = 1;
67
68 for n_qubits in 1..=self.max_qubits {
69 let success_rate = self.test_quantum_volume(n_qubits, &mut circuit_executor)?;
70
71 results.insert(n_qubits, success_rate);
72
73 if success_rate >= self.success_threshold {
75 quantum_volume = 1 << n_qubits; } else {
77 break; }
79 }
80
81 Ok(QuantumVolumeResult {
82 quantum_volume,
83 success_rates: results,
84 max_qubits_tested: self.max_qubits,
85 })
86 }
87
88 fn test_quantum_volume<F>(
90 &mut self,
91 n_qubits: usize,
92 circuit_executor: &mut F,
93 ) -> QuantRS2Result<f64>
94 where
95 F: FnMut(&[Box<dyn GateOp>], usize) -> Vec<usize>,
96 {
97 let mut successful_circuits = 0;
98
99 for _ in 0..self.num_circuits {
100 let (circuit, heavy_outputs) = self.generate_random_circuit(n_qubits)?;
102
103 let measurements = circuit_executor(&circuit, self.num_shots);
105
106 let hop = self.calculate_heavy_output_probability(&measurements, &heavy_outputs);
108
109 if hop > 2.0 / 3.0 {
111 successful_circuits += 1;
112 }
113 }
114
115 let success_rate = successful_circuits as f64 / self.num_circuits as f64;
116 Ok(success_rate)
117 }
118
119 fn generate_random_circuit(
131 &mut self,
132 n_qubits: usize,
133 ) -> QuantRS2Result<(Vec<Box<dyn GateOp>>, Vec<usize>)> {
134 let depth = n_qubits;
136 let mut circuit: Vec<Box<dyn GateOp>> = Vec::new();
137
138 for _layer in 0..depth {
139 let mut order: Vec<usize> = (0..n_qubits).collect();
141 self.shuffle(&mut order);
142
143 let num_pairs = n_qubits / 2;
144 for pair in 0..num_pairs {
145 let q1 = order[2 * pair];
146 let q2 = order[2 * pair + 1];
147 let unitary = self.random_su4()?;
148 circuit.push(Box::new(TwoQubitUnitaryGate::new(unitary, q1, q2)));
149 }
150 }
151
152 let heavy_outputs = self.find_heavy_outputs(n_qubits, &circuit)?;
154
155 Ok((circuit, heavy_outputs))
156 }
157
158 fn find_heavy_outputs(
166 &self,
167 n_qubits: usize,
168 circuit: &[Box<dyn GateOp>],
169 ) -> QuantRS2Result<Vec<usize>> {
170 let num_states = 1usize << n_qubits;
171
172 let state = simulate_circuit(circuit, n_qubits)?;
174 let probabilities: Vec<f64> = state.iter().map(|amp| amp.norm_sqr()).collect();
175
176 let mut sorted = probabilities.clone();
178 sorted.sort_by(|a, b| a.total_cmp(b));
179 let median = if num_states % 2 == 0 {
180 0.5 * (sorted[num_states / 2 - 1] + sorted[num_states / 2])
181 } else {
182 sorted[num_states / 2]
183 };
184
185 let heavy_outputs: Vec<usize> = probabilities
187 .iter()
188 .enumerate()
189 .filter(|(_, &p)| p > median)
190 .map(|(idx, _)| idx)
191 .collect();
192
193 Ok(heavy_outputs)
194 }
195
196 fn shuffle(&mut self, slice: &mut [usize]) {
198 let n = slice.len();
199 if n < 2 {
200 return;
201 }
202 for i in 0..n - 1 {
203 let j = self.rng.random_range(i..n);
204 slice.swap(i, j);
205 }
206 }
207
208 fn random_su4(&mut self) -> QuantRS2Result<Array2<Complex64>> {
216 let dim = 4;
217 let mut matrix = Array2::<Complex64>::zeros((dim, dim));
218 for i in 0..dim {
219 for j in 0..dim {
220 let (re, im) = self.standard_normal_pair();
221 matrix[[i, j]] = Complex64::new(re, im);
222 }
223 }
224 gram_schmidt_unitary(&matrix)
225 }
226
227 fn standard_normal_pair(&mut self) -> (f64, f64) {
230 let u1: f64 = self.rng.random_range(f64::EPSILON..1.0);
232 let u2: f64 = self.rng.random_range(0.0..1.0);
233 let r = (-2.0 * u1.ln()).sqrt();
234 let theta = 2.0 * std::f64::consts::PI * u2;
235 (r * theta.cos(), r * theta.sin())
236 }
237
238 fn calculate_heavy_output_probability(
240 &self,
241 measurements: &[usize],
242 heavy_outputs: &[usize],
243 ) -> f64 {
244 let heavy_count = measurements
245 .iter()
246 .filter(|&&bitstring| heavy_outputs.contains(&bitstring))
247 .count();
248
249 heavy_count as f64 / measurements.len() as f64
250 }
251}
252
253#[derive(Debug, Clone)]
259struct TwoQubitUnitaryGate {
260 matrix: Array2<Complex64>,
261 qubit1: QubitId,
262 qubit2: QubitId,
263}
264
265impl TwoQubitUnitaryGate {
266 fn new(matrix: Array2<Complex64>, qubit1: usize, qubit2: usize) -> Self {
267 Self {
268 matrix,
269 qubit1: QubitId::new(qubit1 as u32),
270 qubit2: QubitId::new(qubit2 as u32),
271 }
272 }
273}
274
275impl GateOp for TwoQubitUnitaryGate {
276 fn name(&self) -> &'static str {
277 "QV_SU4"
278 }
279
280 fn qubits(&self) -> Vec<QubitId> {
281 vec![self.qubit1, self.qubit2]
282 }
283
284 fn matrix(&self) -> QuantRS2Result<Vec<Complex64>> {
285 let (rows, cols) = self.matrix.dim();
287 let mut flat = Vec::with_capacity(rows * cols);
288 for i in 0..rows {
289 for j in 0..cols {
290 flat.push(self.matrix[[i, j]]);
291 }
292 }
293 Ok(flat)
294 }
295
296 fn as_any(&self) -> &dyn std::any::Any {
297 self
298 }
299
300 fn clone_gate(&self) -> Box<dyn GateOp> {
301 Box::new(self.clone())
302 }
303}
304
305fn gram_schmidt_unitary(matrix: &Array2<Complex64>) -> QuantRS2Result<Array2<Complex64>> {
307 let dim = matrix.nrows();
308 let mut result = Array2::<Complex64>::zeros((dim, dim));
309
310 for j in 0..dim {
311 let mut col = matrix.column(j).to_owned();
312
313 for k in 0..j {
315 let prev = result.column(k);
316 let proj: Complex64 = col.iter().zip(prev.iter()).map(|(a, b)| b.conj() * a).sum();
317 for i in 0..dim {
318 col[i] -= proj * prev[i];
319 }
320 }
321
322 let norm = col.iter().map(|x| x.norm_sqr()).sum::<f64>().sqrt();
323 if norm < 1e-12 {
324 return Err(QuantRS2Error::ComputationError(
325 "Gram-Schmidt failed: degenerate random matrix".to_string(),
326 ));
327 }
328 for i in 0..dim {
329 result[[i, j]] = col[i] / Complex64::new(norm, 0.0);
330 }
331 }
332
333 Ok(result)
334}
335
336fn simulate_circuit(
343 circuit: &[Box<dyn GateOp>],
344 n_qubits: usize,
345) -> QuantRS2Result<Array1<Complex64>> {
346 let dim = 1usize << n_qubits;
347 let mut state = Array1::<Complex64>::zeros(dim);
348 state[0] = Complex64::new(1.0, 0.0);
349
350 for gate in circuit {
351 apply_gate(&mut state, gate.as_ref(), n_qubits)?;
352 }
353
354 Ok(state)
355}
356
357fn apply_gate(
359 state: &mut Array1<Complex64>,
360 gate: &dyn GateOp,
361 n_qubits: usize,
362) -> QuantRS2Result<()> {
363 let qubits = gate.qubits();
364 let k = qubits.len();
365 let gate_dim = 1usize << k;
366
367 let flat = gate.matrix()?;
368 if flat.len() != gate_dim * gate_dim {
369 return Err(QuantRS2Error::InvalidInput(format!(
370 "Gate matrix has {} entries, expected {} for {}-qubit gate",
371 flat.len(),
372 gate_dim * gate_dim,
373 k
374 )));
375 }
376 let gate_matrix = Array2::from_shape_vec((gate_dim, gate_dim), flat)
378 .map_err(|e| QuantRS2Error::ComputationError(format!("Gate reshape failed: {e}")))?;
379
380 let qubit_bits: Vec<usize> = qubits.iter().map(|q| q.id() as usize).collect();
383 for &b in &qubit_bits {
384 if b >= n_qubits {
385 return Err(QuantRS2Error::InvalidInput(format!(
386 "Gate acts on qubit {b} but circuit has only {n_qubits} qubits"
387 )));
388 }
389 }
390
391 let dim = 1usize << n_qubits;
392 let mut visited = vec![false; dim];
395 for base in 0..dim {
396 if visited[base] {
399 continue;
400 }
401 let mut anchor = base;
402 for &b in &qubit_bits {
403 anchor &= !(1 << b);
404 }
405 if anchor != base {
406 continue;
407 }
408
409 let mut indices = vec![0usize; gate_dim];
411 let mut amplitudes = vec![Complex64::new(0.0, 0.0); gate_dim];
412 for local in 0..gate_dim {
413 let mut idx = anchor;
414 for (pos, &b) in qubit_bits.iter().enumerate() {
415 let bit = (local >> (k - 1 - pos)) & 1;
418 if bit == 1 {
419 idx |= 1 << b;
420 }
421 }
422 indices[local] = idx;
423 amplitudes[local] = state[idx];
424 visited[idx] = true;
425 }
426
427 for r in 0..gate_dim {
429 let mut acc = Complex64::new(0.0, 0.0);
430 for c in 0..gate_dim {
431 acc += gate_matrix[[r, c]] * amplitudes[c];
432 }
433 state[indices[r]] = acc;
434 }
435 }
436
437 Ok(())
438}
439
440#[derive(Debug, Clone)]
442pub struct QuantumVolumeResult {
443 pub quantum_volume: usize,
445 pub success_rates: HashMap<usize, f64>,
447 pub max_qubits_tested: usize,
449}
450
451impl QuantumVolumeResult {
452 pub fn num_qubits_achieved(&self) -> usize {
454 (self.quantum_volume as f64).log2() as usize
455 }
456
457 pub fn is_qv_achieved(&self, n_qubits: usize) -> bool {
459 self.success_rates
460 .get(&n_qubits)
461 .is_some_and(|&rate| rate >= 2.0 / 3.0)
462 }
463}
464
465pub struct QuantumProcessTomography {
470 pub num_qubits: usize,
472 pub preparation_basis: Vec<String>,
474 pub measurement_basis: Vec<String>,
476}
477
478impl QuantumProcessTomography {
479 pub fn new(num_qubits: usize) -> Self {
481 let basis = Self::generate_pauli_basis(num_qubits);
483
484 Self {
485 num_qubits,
486 preparation_basis: basis.clone(),
487 measurement_basis: basis,
488 }
489 }
490
491 fn generate_pauli_basis(n_qubits: usize) -> Vec<String> {
493 let paulis = ['I', 'X', 'Y', 'Z'];
494 let basis_size = 4_usize.pow(n_qubits as u32);
495
496 let mut basis = Vec::with_capacity(basis_size);
497
498 for i in 0..basis_size {
499 let mut pauli_string = String::with_capacity(n_qubits);
500 let mut idx = i;
501
502 for _ in 0..n_qubits {
503 pauli_string.push(paulis[idx % 4]);
504 idx /= 4;
505 }
506
507 basis.push(pauli_string);
508 }
509
510 basis
511 }
512
513 pub fn run<F>(&self, mut apply_process: F) -> QuantRS2Result<ProcessMatrix>
517 where
518 F: FnMut(&str, &str) -> Complex64, {
520 let dim = 1 << self.num_qubits;
521 let basis_size = self.preparation_basis.len();
522
523 let mut chi_matrix = Array2::zeros((basis_size, basis_size));
525
526 for (i, prep) in self.preparation_basis.iter().enumerate() {
528 for (j, meas) in self.measurement_basis.iter().enumerate() {
529 let expectation = apply_process(prep, meas);
530 chi_matrix[[i, j]] = expectation;
531 }
532 }
533
534 let chi_matrix = self.enforce_physicality(chi_matrix)?;
536
537 Ok(ProcessMatrix {
538 chi_matrix,
539 num_qubits: self.num_qubits,
540 basis_labels: self.preparation_basis.clone(),
541 })
542 }
543
544 fn enforce_physicality(&self, chi: Array2<Complex64>) -> QuantRS2Result<Array2<Complex64>> {
546 let trace: Complex64 = chi.diag().iter().sum();
554 let normalized = if trace.norm() > 1e-10 {
555 &chi / trace
556 } else {
557 chi
558 };
559
560 Ok(normalized)
561 }
562
563 pub fn process_fidelity(chi1: &Array2<Complex64>, chi2: &Array2<Complex64>) -> f64 {
565 let product = chi1.t().mapv(|x| x.conj()).dot(chi2);
567 let trace: Complex64 = product.diag().iter().sum();
568 trace.norm()
569 }
570
571 pub fn average_gate_fidelity(
573 &self,
574 chi: &Array2<Complex64>,
575 ideal_chi: &Array2<Complex64>,
576 ) -> f64 {
577 let dim = 1 << self.num_qubits;
578 let d = dim as f64;
579
580 let f_proc = Self::process_fidelity(chi, ideal_chi);
582 (d * f_proc + 1.0) / (d + 1.0)
583 }
584}
585
586#[derive(Debug, Clone)]
588pub struct ProcessMatrix {
589 pub chi_matrix: Array2<Complex64>,
591 pub num_qubits: usize,
593 pub basis_labels: Vec<String>,
595}
596
597impl ProcessMatrix {
598 pub fn get_element(&self, prep_pauli: &str, meas_pauli: &str) -> Option<Complex64> {
600 let i = self.basis_labels.iter().position(|s| s == prep_pauli)?;
601 let j = self.basis_labels.iter().position(|s| s == meas_pauli)?;
602 Some(self.chi_matrix[[i, j]])
603 }
604
605 pub fn is_trace_preserving(&self, tolerance: f64) -> bool {
607 let trace: Complex64 = self.chi_matrix.diag().iter().sum();
608 (trace - Complex64::new(1.0, 0.0)).norm() < tolerance
609 }
610
611 pub fn is_completely_positive(&self, tolerance: f64) -> bool {
613 self.chi_matrix.diag().iter().all(|&x| x.re >= -tolerance)
618 }
619
620 pub fn diamond_distance(&self, other: &Self) -> QuantRS2Result<f64> {
622 if self.num_qubits != other.num_qubits {
623 return Err(QuantRS2Error::InvalidInput(
624 "Process matrices must have same dimension".to_string(),
625 ));
626 }
627
628 let diff = &self.chi_matrix - &other.chi_matrix;
633 let frobenius_norm = diff.iter().map(|x| x.norm_sqr()).sum::<f64>().sqrt();
634
635 Ok(frobenius_norm)
636 }
637}
638
639pub struct GateSetTomography {
644 pub num_qubits: usize,
646 pub gate_set: Vec<String>,
648 pub max_length: usize,
650}
651
652impl GateSetTomography {
653 pub const fn new(num_qubits: usize, gate_set: Vec<String>, max_length: usize) -> Self {
655 Self {
656 num_qubits,
657 gate_set,
658 max_length,
659 }
660 }
661
662 pub fn run<F>(&self, mut execute_sequence: F) -> QuantRS2Result<GateSetModel>
666 where
667 F: FnMut(&[&str]) -> f64, {
669 let germs = self.generate_germs();
675 let fiducials = self.generate_fiducials();
676
677 let mut data = HashMap::new();
679
680 for prep_fiducial in &fiducials {
681 for germ in &germs {
682 for meas_fiducial in &fiducials {
683 for power in 1..=self.max_length {
685 let mut sequence = Vec::new();
686
687 sequence.extend_from_slice(prep_fiducial);
689
690 for _ in 0..power {
692 sequence.extend_from_slice(germ);
693 }
694
695 sequence.extend_from_slice(meas_fiducial);
697
698 let probability = execute_sequence(&sequence);
700 data.insert(sequence.clone(), probability);
701 }
702 }
703 }
704 }
705
706 let model = self.fit_model(&data)?;
708
709 Ok(model)
710 }
711
712 fn generate_germs(&self) -> Vec<Vec<&str>> {
714 vec![vec!["I"], vec!["X"], vec!["Y"], vec!["X", "Y"]]
717 }
718
719 fn generate_fiducials(&self) -> Vec<Vec<&str>> {
721 vec![
723 vec!["I"],
724 vec!["X"],
725 vec!["Y"],
726 vec!["X", "X"], ]
728 }
729
730 fn fit_model(&self, _data: &HashMap<Vec<&str>, f64>) -> QuantRS2Result<GateSetModel> {
732 Ok(GateSetModel {
736 num_qubits: self.num_qubits,
737 gate_errors: HashMap::new(),
738 spam_errors: vec![],
739 })
740 }
741}
742
743#[derive(Debug, Clone)]
745pub struct GateSetModel {
746 pub num_qubits: usize,
748 pub gate_errors: HashMap<String, Array2<Complex64>>,
750 pub spam_errors: Vec<f64>,
752}
753
754#[cfg(test)]
755mod tests {
756 use super::*;
757
758 #[test]
759 fn test_quantum_volume_result() {
760 let mut result = QuantumVolumeResult {
761 quantum_volume: 16,
762 success_rates: HashMap::new(),
763 max_qubits_tested: 5,
764 };
765
766 result.success_rates.insert(1, 0.95);
767 result.success_rates.insert(2, 0.85);
768 result.success_rates.insert(3, 0.75);
769 result.success_rates.insert(4, 0.70);
770
771 assert_eq!(result.num_qubits_achieved(), 4);
772 assert!(result.is_qv_achieved(1));
773 assert!(result.is_qv_achieved(2));
774 assert!(result.is_qv_achieved(3));
775 assert!(result.is_qv_achieved(4));
776
777 println!("Quantum Volume: {}", result.quantum_volume);
778 }
779
780 #[test]
781 fn test_pauli_basis_generation() {
782 let basis = QuantumProcessTomography::generate_pauli_basis(1);
783 assert_eq!(basis.len(), 4);
784 assert!(basis.contains(&"I".to_string()));
785 assert!(basis.contains(&"X".to_string()));
786 assert!(basis.contains(&"Y".to_string()));
787 assert!(basis.contains(&"Z".to_string()));
788
789 let basis_2q = QuantumProcessTomography::generate_pauli_basis(2);
790 assert_eq!(basis_2q.len(), 16);
791 }
792
793 #[test]
794 fn test_process_matrix() {
795 let qpt = QuantumProcessTomography::new(1);
796
797 let mock_process = |_prep: &str, meas: &str| {
799 if meas == "I" {
800 Complex64::new(1.0, 0.0)
801 } else {
802 Complex64::new(0.0, 0.0)
803 }
804 };
805
806 let result = qpt
807 .run(mock_process)
808 .expect("QPT run should succeed with mock process");
809
810 assert_eq!(result.num_qubits, 1);
811 assert!(result.is_trace_preserving(1e-6));
812 println!("Process matrix shape: {:?}", result.chi_matrix.dim());
813 }
814
815 #[test]
816 fn test_process_fidelity() {
817 let dim = 4;
818 let identity = Array2::eye(dim);
819 let noisy = &identity * Complex64::new(0.95, 0.0);
820
821 let fidelity = QuantumProcessTomography::process_fidelity(&identity, &noisy);
822
823 println!("Process fidelity: {}", fidelity);
826
827 assert!(fidelity > 0.0 && fidelity <= dim as f64);
829 }
830
831 #[test]
832 fn test_gst_initialization() {
833 let gate_set = vec!["I".to_string(), "X".to_string(), "H".to_string()];
834 let gst = GateSetTomography::new(1, gate_set, 10);
835
836 assert_eq!(gst.num_qubits, 1);
837 assert_eq!(gst.max_length, 10);
838
839 let germs = gst.generate_germs();
840 assert!(!germs.is_empty());
841
842 let fiducials = gst.generate_fiducials();
843 assert!(!fiducials.is_empty());
844 }
845
846 fn gate_from_first_column(amps: [Complex64; 4], q1: usize, q2: usize) -> TwoQubitUnitaryGate {
850 let mut m = Array2::<Complex64>::zeros((4, 4));
851 for i in 0..4 {
852 m[[i, 0]] = amps[i];
853 }
854 m[[1, 1]] = Complex64::new(1.0, 0.0);
856 m[[2, 2]] = Complex64::new(1.0, 0.0);
857 m[[3, 3]] = Complex64::new(1.0, 0.0);
858 let u = gram_schmidt_unitary(&m).expect("gram-schmidt");
859 TwoQubitUnitaryGate::new(u, q1, q2)
860 }
861
862 #[test]
863 fn test_apply_gate_bit_ordering_cnot() {
864 let cnot = scirs2_core::ndarray::array![
868 [
869 Complex64::new(1.0, 0.0),
870 Complex64::new(0.0, 0.0),
871 Complex64::new(0.0, 0.0),
872 Complex64::new(0.0, 0.0)
873 ],
874 [
875 Complex64::new(0.0, 0.0),
876 Complex64::new(1.0, 0.0),
877 Complex64::new(0.0, 0.0),
878 Complex64::new(0.0, 0.0)
879 ],
880 [
881 Complex64::new(0.0, 0.0),
882 Complex64::new(0.0, 0.0),
883 Complex64::new(0.0, 0.0),
884 Complex64::new(1.0, 0.0)
885 ],
886 [
887 Complex64::new(0.0, 0.0),
888 Complex64::new(0.0, 0.0),
889 Complex64::new(1.0, 0.0),
890 Complex64::new(0.0, 0.0)
891 ]
892 ];
893 let gate: Box<dyn GateOp> = Box::new(TwoQubitUnitaryGate::new(cnot, 0, 1));
894
895 let state = simulate_circuit(std::slice::from_ref(&gate), 2).expect("simulate");
897 assert!((state[0].norm() - 1.0).abs() < 1e-12);
898
899 let mut custom = Array1::<Complex64>::zeros(4);
901 custom[1] = Complex64::new(1.0, 0.0); apply_gate(&mut custom, gate.as_ref(), 2).expect("apply");
903 assert!((custom[3].norm() - 1.0).abs() < 1e-12, "got {custom:?}");
905 assert!(custom[1].norm() < 1e-12);
906 }
907
908 #[test]
909 fn test_find_heavy_outputs_above_median_not_first_half() {
910 let amps = [
914 Complex64::new(0.1_f64.sqrt(), 0.0),
915 Complex64::new(0.4_f64.sqrt(), 0.0),
916 Complex64::new(0.2_f64.sqrt(), 0.0),
917 Complex64::new(0.3_f64.sqrt(), 0.0),
918 ];
919 let gate = gate_from_first_column(amps, 0, 1);
920 let circuit: Vec<Box<dyn GateOp>> = vec![Box::new(gate)];
921
922 let qv = QuantumVolume::new(2, 1, 100);
923 let heavy = qv.find_heavy_outputs(2, &circuit).expect("heavy outputs");
924
925 let state = simulate_circuit(&circuit, 2).expect("simulate");
928 let probs: Vec<f64> = state.iter().map(|a| a.norm_sqr()).collect();
929
930 let total: f64 = probs.iter().sum();
932 assert!((total - 1.0).abs() < 1e-9);
933 let max_p = probs.iter().cloned().fold(0.0_f64, f64::max);
934 let min_p = probs.iter().cloned().fold(1.0_f64, f64::min);
935 assert!(max_p - min_p > 1e-3, "distribution should be non-uniform");
936
937 let mut sorted = probs.clone();
938 sorted.sort_by(|a, b| a.total_cmp(b));
939 let median = 0.5 * (sorted[1] + sorted[2]);
940 let mut expected: Vec<usize> = probs
941 .iter()
942 .enumerate()
943 .filter(|(_, &p)| p > median)
944 .map(|(i, _)| i)
945 .collect();
946 expected.sort_unstable();
947
948 let mut heavy_sorted = heavy.clone();
949 heavy_sorted.sort_unstable();
950 assert_eq!(
951 heavy_sorted, expected,
952 "heavy outputs must be exactly the strictly-above-median indices"
953 );
954 assert_eq!(heavy_sorted.len(), 2);
956
957 let first_half: Vec<usize> = (0..(1usize << 2) / 2).collect();
959 assert_ne!(
960 heavy_sorted, first_half,
961 "heavy outputs must be computed from probabilities, not the first half"
962 );
963 }
964
965 #[test]
966 fn test_generate_random_circuit_is_non_empty() {
967 let mut qv = QuantumVolume::new(4, 1, 10);
970 let n = 4;
971 let (circuit, heavy) = qv.generate_random_circuit(n).expect("circuit");
972
973 assert_eq!(circuit.len(), n * (n / 2));
975 assert!(!circuit.is_empty(), "QV circuit must not be empty");
976 for gate in &circuit {
977 assert_eq!(gate.qubits().len(), 2, "each QV gate acts on 2 qubits");
978 }
979
980 let state = simulate_circuit(&circuit, n).expect("simulate");
982 let total: f64 = state.iter().map(|a| a.norm_sqr()).sum();
983 assert!(
984 (total - 1.0).abs() < 1e-9,
985 "state must stay normalised: {total}"
986 );
987
988 assert!(heavy.iter().all(|&i| i < (1usize << n)));
991 }
992}