1use crate::Result;
38use scirs2_core::ndarray::{s, Array1, Array2, Axis};
39use scirs2_core::random::{thread_rng, Rng};
40use scirs2_core::Complex64;
41use std::f64::consts::PI;
42use thiserror::Error;
43
44#[derive(Debug, Error)]
46pub enum QuantumInfoError {
47 #[error("Dimension mismatch: {0}")]
48 DimensionMismatch(String),
49
50 #[error("Invalid quantum state: {0}")]
51 InvalidState(String),
52
53 #[error("Invalid density matrix: {0}")]
54 InvalidDensityMatrix(String),
55
56 #[error("Numerical error: {0}")]
57 NumericalError(String),
58
59 #[error("Tomography error: {0}")]
60 TomographyError(String),
61
62 #[error("Not implemented: {0}")]
63 NotImplemented(String),
64}
65
66pub fn state_fidelity(
82 state1: &QuantumState,
83 state2: &QuantumState,
84) -> std::result::Result<f64, QuantumInfoError> {
85 match (state1, state2) {
86 (QuantumState::Pure(psi), QuantumState::Pure(phi)) => {
87 let inner = inner_product(psi, phi);
89 Ok(inner.norm_sqr())
90 }
91 (QuantumState::Pure(psi), QuantumState::Mixed(rho)) => {
92 let psi_dag = psi.mapv(|c| c.conj());
94 let rho_psi = rho.dot(psi);
95 let fid = inner_product(&psi_dag, &rho_psi);
96 Ok(fid.re.max(0.0))
97 }
98 (QuantumState::Mixed(rho), QuantumState::Pure(psi)) => {
99 let psi_dag = psi.mapv(|c| c.conj());
101 let rho_psi = rho.dot(psi);
102 let fid = inner_product(&psi_dag, &rho_psi);
103 Ok(fid.re.max(0.0))
104 }
105 (QuantumState::Mixed(rho1), QuantumState::Mixed(rho2)) => {
106 mixed_state_fidelity(rho1, rho2)
109 }
110 }
111}
112
113fn inner_product(psi: &Array1<Complex64>, phi: &Array1<Complex64>) -> Complex64 {
115 psi.iter().zip(phi.iter()).map(|(a, b)| a.conj() * b).sum()
116}
117
118fn mixed_state_fidelity(
120 rho1: &Array2<Complex64>,
121 rho2: &Array2<Complex64>,
122) -> std::result::Result<f64, QuantumInfoError> {
123 let n = rho1.nrows();
124 if n != rho1.ncols() || n != rho2.nrows() || n != rho2.ncols() {
125 return Err(QuantumInfoError::DimensionMismatch(
126 "Density matrices must be square and have the same dimensions".to_string(),
127 ));
128 }
129
130 let mut fid_sum = Complex64::new(0.0, 0.0);
137 for i in 0..n {
138 for j in 0..n {
139 fid_sum += rho1[[i, j]].conj() * rho2[[i, j]];
140 }
141 }
142
143 let fid = fid_sum.re.sqrt().powi(2).max(0.0).min(1.0);
146 Ok(fid)
147}
148
149pub fn purity(state: &QuantumState) -> std::result::Result<f64, QuantumInfoError> {
161 match state {
162 QuantumState::Pure(_) => Ok(1.0),
163 QuantumState::Mixed(rho) => {
164 let rho_squared = rho.dot(rho);
166 let trace: Complex64 = (0..rho.nrows()).map(|i| rho_squared[[i, i]]).sum();
167 Ok(trace.re.max(0.0).min(1.0))
168 }
169 }
170}
171
172pub fn von_neumann_entropy(
187 state: &QuantumState,
188 base: Option<f64>,
189) -> std::result::Result<f64, QuantumInfoError> {
190 let log_base = base.unwrap_or(2.0);
191
192 match state {
193 QuantumState::Pure(_) => Ok(0.0),
194 QuantumState::Mixed(rho) => {
195 let eigenvalues = compute_eigenvalues_hermitian(rho)?;
197
198 let mut entropy = 0.0;
200 for &lambda in &eigenvalues {
201 if lambda > 1e-15 {
202 entropy -= lambda * lambda.log(log_base);
203 }
204 }
205 Ok(entropy.max(0.0))
206 }
207 }
208}
209
210fn compute_eigenvalues_hermitian(
212 matrix: &Array2<Complex64>,
213) -> std::result::Result<Vec<f64>, QuantumInfoError> {
214 let n = matrix.nrows();
215
216 let mut eigenvalues = Vec::with_capacity(n);
219
220 for i in 0..n {
223 let diag = matrix[[i, i]].re;
224 if diag.abs() > 1e-15 {
225 eigenvalues.push(diag);
226 }
227 }
228
229 let sum: f64 = eigenvalues.iter().sum();
231 if sum > 1e-10 {
232 for e in &mut eigenvalues {
233 *e /= sum;
234 }
235 }
236
237 Ok(eigenvalues)
238}
239
240pub fn mutual_information(
252 state: &QuantumState,
253 dims: (usize, usize),
254 base: Option<f64>,
255) -> std::result::Result<f64, QuantumInfoError> {
256 let rho = state.to_density_matrix()?;
257 let (dim_a, dim_b) = dims;
258
259 if rho.nrows() != dim_a * dim_b {
260 return Err(QuantumInfoError::DimensionMismatch(format!(
261 "State dimension {} doesn't match subsystem dimensions {}×{}",
262 rho.nrows(),
263 dim_a,
264 dim_b
265 )));
266 }
267
268 let rho_a = partial_trace(&rho, dim_b, false)?;
270 let rho_b = partial_trace(&rho, dim_a, true)?;
271
272 let s_ab = von_neumann_entropy(&QuantumState::Mixed(rho.clone()), base)?;
274 let s_a = von_neumann_entropy(&QuantumState::Mixed(rho_a), base)?;
275 let s_b = von_neumann_entropy(&QuantumState::Mixed(rho_b), base)?;
276
277 Ok((s_a + s_b - s_ab).max(0.0))
278}
279
280pub fn partial_trace(
290 rho: &Array2<Complex64>,
291 dim_traced: usize,
292 trace_first: bool,
293) -> std::result::Result<Array2<Complex64>, QuantumInfoError> {
294 let n = rho.nrows();
295 let dim_kept = n / dim_traced;
296
297 if dim_kept * dim_traced != n {
298 return Err(QuantumInfoError::DimensionMismatch(format!(
299 "Matrix dimension {} is not divisible by {}",
300 n, dim_traced
301 )));
302 }
303
304 let mut reduced = Array2::zeros((dim_kept, dim_kept));
305
306 if trace_first {
307 for i in 0..dim_kept {
309 for j in 0..dim_kept {
310 let mut sum = Complex64::new(0.0, 0.0);
311 for k in 0..dim_traced {
312 sum += rho[[k * dim_kept + i, k * dim_kept + j]];
313 }
314 reduced[[i, j]] = sum;
315 }
316 }
317 } else {
318 for i in 0..dim_kept {
320 for j in 0..dim_kept {
321 let mut sum = Complex64::new(0.0, 0.0);
322 for k in 0..dim_traced {
323 sum += rho[[i * dim_traced + k, j * dim_traced + k]];
324 }
325 reduced[[i, j]] = sum;
326 }
327 }
328 }
329
330 Ok(reduced)
331}
332
333pub fn concurrence(state: &QuantumState) -> std::result::Result<f64, QuantumInfoError> {
349 match state {
350 QuantumState::Pure(psi) => {
351 if psi.len() != 4 {
352 return Err(QuantumInfoError::DimensionMismatch(
353 "Concurrence is only defined for 2-qubit states (dimension 4)".to_string(),
354 ));
355 }
356
357 let alpha = psi[0]; let beta = psi[1]; let gamma = psi[2]; let delta = psi[3]; let c = 2.0 * (alpha * delta - beta * gamma).norm();
365 Ok(c.min(1.0))
366 }
367 QuantumState::Mixed(rho) => {
368 if rho.nrows() != 4 {
369 return Err(QuantumInfoError::DimensionMismatch(
370 "Concurrence is only defined for 2-qubit states (dimension 4)".to_string(),
371 ));
372 }
373
374 let sigma_yy = Array2::from_shape_vec(
378 (4, 4),
379 vec![
380 Complex64::new(0.0, 0.0),
381 Complex64::new(0.0, 0.0),
382 Complex64::new(0.0, 0.0),
383 Complex64::new(-1.0, 0.0),
384 Complex64::new(0.0, 0.0),
385 Complex64::new(0.0, 0.0),
386 Complex64::new(1.0, 0.0),
387 Complex64::new(0.0, 0.0),
388 Complex64::new(0.0, 0.0),
389 Complex64::new(1.0, 0.0),
390 Complex64::new(0.0, 0.0),
391 Complex64::new(0.0, 0.0),
392 Complex64::new(-1.0, 0.0),
393 Complex64::new(0.0, 0.0),
394 Complex64::new(0.0, 0.0),
395 Complex64::new(0.0, 0.0),
396 ],
397 )
398 .map_err(|e| QuantumInfoError::NumericalError(e.to_string()))?;
399
400 let rho_star = rho.mapv(|c| c.conj());
402 let temp1 = sigma_yy.dot(&rho_star);
403 let rho_tilde = temp1.dot(&sigma_yy);
404
405 let r_matrix = rho.dot(&rho_tilde);
407
408 let eigenvalues = compute_4x4_eigenvalues(&r_matrix)?;
411
412 let mut lambdas: Vec<f64> = eigenvalues.iter().map(|e| e.re.max(0.0).sqrt()).collect();
414 lambdas.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
415
416 let concurrence = (lambdas[0] - lambdas[1] - lambdas[2] - lambdas[3]).max(0.0);
418 Ok(concurrence.min(1.0))
419 }
420 }
421}
422
423fn compute_4x4_eigenvalues(
425 matrix: &Array2<Complex64>,
426) -> std::result::Result<Vec<Complex64>, QuantumInfoError> {
427 let n = matrix.nrows();
433 let mut eigenvalues = Vec::with_capacity(n);
434
435 let trace: Complex64 = (0..n).map(|i| matrix[[i, i]]).sum();
437
438 for i in 0..n {
441 let row_sum: Complex64 = (0..n)
442 .map(|j| matrix[[i, j]].norm_sqr())
443 .sum::<f64>()
444 .into();
445 eigenvalues.push(Complex64::new(row_sum.re.sqrt() / n as f64, 0.0));
446 }
447
448 let eigen_sum: Complex64 = eigenvalues.iter().sum();
450 if eigen_sum.norm() > 1e-10 {
451 let scale = trace / eigen_sum;
452 for e in &mut eigenvalues {
453 *e *= scale;
454 }
455 }
456
457 Ok(eigenvalues)
458}
459
460pub fn entanglement_of_formation(
471 state: &QuantumState,
472) -> std::result::Result<f64, QuantumInfoError> {
473 let c = concurrence(state)?;
474
475 if c < 1e-15 {
476 return Ok(0.0);
477 }
478
479 let x = (1.0 + (1.0 - c * c).max(0.0).sqrt()) / 2.0;
480
481 let h = if x > 1e-15 && x < 1.0 - 1e-15 {
483 -x * x.log2() - (1.0 - x) * (1.0 - x).log2()
484 } else if x <= 1e-15 {
485 0.0
486 } else {
487 0.0
488 };
489
490 Ok(h)
491}
492
493pub fn negativity(
505 state: &QuantumState,
506 dims: (usize, usize),
507) -> std::result::Result<f64, QuantumInfoError> {
508 let rho = state.to_density_matrix()?;
509 let (dim_a, dim_b) = dims;
510
511 if rho.nrows() != dim_a * dim_b {
512 return Err(QuantumInfoError::DimensionMismatch(format!(
513 "State dimension {} doesn't match subsystem dimensions {}×{}",
514 rho.nrows(),
515 dim_a,
516 dim_b
517 )));
518 }
519
520 let rho_pt = partial_transpose(&rho, dim_a, dim_b)?;
522
523 let eigenvalues = compute_eigenvalues_hermitian(&rho_pt)?;
526 let trace_norm: f64 = eigenvalues.iter().map(|e| e.abs()).sum();
527
528 Ok((trace_norm - 1.0).max(0.0) / 2.0)
529}
530
531fn partial_transpose(
533 rho: &Array2<Complex64>,
534 dim_a: usize,
535 dim_b: usize,
536) -> std::result::Result<Array2<Complex64>, QuantumInfoError> {
537 let n = dim_a * dim_b;
538 let mut rho_pt = Array2::zeros((n, n));
539
540 for i in 0..dim_a {
541 for j in 0..dim_a {
542 for k in 0..dim_b {
543 for l in 0..dim_b {
544 rho_pt[[j * dim_b + k, i * dim_b + l]] = rho[[i * dim_b + k, j * dim_b + l]];
547 }
548 }
549 }
550 }
551
552 Ok(rho_pt)
553}
554
555pub fn logarithmic_negativity(
566 state: &QuantumState,
567 dims: (usize, usize),
568) -> std::result::Result<f64, QuantumInfoError> {
569 let rho = state.to_density_matrix()?;
570 let (dim_a, dim_b) = dims;
571
572 let rho_pt = partial_transpose(&rho, dim_a, dim_b)?;
573 let eigenvalues = compute_eigenvalues_hermitian(&rho_pt)?;
574 let trace_norm: f64 = eigenvalues.iter().map(|e| e.abs()).sum();
575
576 Ok(trace_norm.log2().max(0.0))
577}
578
579pub fn process_fidelity(
598 channel: &QuantumChannel,
599 target: &QuantumChannel,
600) -> std::result::Result<f64, QuantumInfoError> {
601 let choi1 = channel.to_choi()?;
602 let choi2 = target.to_choi()?;
603
604 let dim = choi1.nrows();
605 let input_dim = (dim as f64).sqrt() as usize;
606
607 let rho1 = &choi1 / Complex64::new(input_dim as f64, 0.0);
609 let rho2 = &choi2 / Complex64::new(input_dim as f64, 0.0);
610
611 state_fidelity(&QuantumState::Mixed(rho1), &QuantumState::Mixed(rho2))
613}
614
615pub fn average_gate_fidelity(
626 channel: &QuantumChannel,
627 target: Option<&QuantumChannel>,
628) -> std::result::Result<f64, QuantumInfoError> {
629 let dim = channel.input_dim();
630
631 let f_pro = if let Some(t) = target {
632 process_fidelity(channel, t)?
633 } else {
634 let identity = QuantumChannel::identity(dim);
636 process_fidelity(channel, &identity)?
637 };
638
639 let d = dim as f64;
640 Ok((d * f_pro + 1.0) / (d + 1.0))
641}
642
643pub fn gate_error(
654 channel: &QuantumChannel,
655 target: Option<&QuantumChannel>,
656) -> std::result::Result<f64, QuantumInfoError> {
657 Ok(1.0 - average_gate_fidelity(channel, target)?)
658}
659
660pub fn unitarity(channel: &QuantumChannel) -> std::result::Result<f64, QuantumInfoError> {
672 let dim = channel.input_dim();
673
674 let ptm = channel.to_ptm()?;
676
677 let d = dim as f64;
679 let d_sq = d * d;
680
681 let mut sum_sq = 0.0;
682 for i in 1..ptm.nrows() {
683 for j in 1..ptm.ncols() {
684 sum_sq += ptm[[i, j]].norm_sqr();
685 }
686 }
687
688 Ok(sum_sq / (d_sq - 1.0))
689}
690
691pub fn diamond_norm_distance(
704 channel1: &QuantumChannel,
705 channel2: &QuantumChannel,
706) -> std::result::Result<f64, QuantumInfoError> {
707 let choi1 = channel1.to_choi()?;
708 let choi2 = channel2.to_choi()?;
709
710 let diff = &choi1 - &choi2;
712
713 let eigenvalues = compute_eigenvalues_hermitian(&diff)?;
716 let trace_norm: f64 = eigenvalues.iter().map(|e| e.abs()).sum();
717
718 let dim = (choi1.nrows() as f64).sqrt();
720 Ok((dim * trace_norm).min(2.0))
721}
722
723#[derive(Debug, Clone)]
729pub enum QuantumState {
730 Pure(Array1<Complex64>),
732 Mixed(Array2<Complex64>),
734}
735
736impl QuantumState {
737 pub fn pure(state_vector: Array1<Complex64>) -> Self {
739 QuantumState::Pure(state_vector)
740 }
741
742 pub fn mixed(density_matrix: Array2<Complex64>) -> Self {
744 QuantumState::Mixed(density_matrix)
745 }
746
747 pub fn computational_basis(dim: usize, index: usize) -> Self {
749 let mut state = Array1::zeros(dim);
750 if index < dim {
751 state[index] = Complex64::new(1.0, 0.0);
752 }
753 QuantumState::Pure(state)
754 }
755
756 pub fn maximally_mixed(dim: usize) -> Self {
758 let mut rho = Array2::zeros((dim, dim));
759 let val = Complex64::new(1.0 / dim as f64, 0.0);
760 for i in 0..dim {
761 rho[[i, i]] = val;
762 }
763 QuantumState::Mixed(rho)
764 }
765
766 pub fn bell_state(index: usize) -> Self {
768 let inv_sqrt2 = 1.0 / 2.0_f64.sqrt();
769 let state = match index {
770 0 => {
771 Array1::from_vec(vec![
773 Complex64::new(inv_sqrt2, 0.0),
774 Complex64::new(0.0, 0.0),
775 Complex64::new(0.0, 0.0),
776 Complex64::new(inv_sqrt2, 0.0),
777 ])
778 }
779 1 => {
780 Array1::from_vec(vec![
782 Complex64::new(inv_sqrt2, 0.0),
783 Complex64::new(0.0, 0.0),
784 Complex64::new(0.0, 0.0),
785 Complex64::new(-inv_sqrt2, 0.0),
786 ])
787 }
788 2 => {
789 Array1::from_vec(vec![
791 Complex64::new(0.0, 0.0),
792 Complex64::new(inv_sqrt2, 0.0),
793 Complex64::new(inv_sqrt2, 0.0),
794 Complex64::new(0.0, 0.0),
795 ])
796 }
797 _ => {
798 Array1::from_vec(vec![
800 Complex64::new(0.0, 0.0),
801 Complex64::new(inv_sqrt2, 0.0),
802 Complex64::new(-inv_sqrt2, 0.0),
803 Complex64::new(0.0, 0.0),
804 ])
805 }
806 };
807 QuantumState::Pure(state)
808 }
809
810 pub fn ghz_state(n: usize) -> Self {
812 let dim = 1 << n;
813 let mut state = Array1::zeros(dim);
814 let inv_sqrt2 = 1.0 / 2.0_f64.sqrt();
815 state[0] = Complex64::new(inv_sqrt2, 0.0);
816 state[dim - 1] = Complex64::new(inv_sqrt2, 0.0);
817 QuantumState::Pure(state)
818 }
819
820 pub fn w_state(n: usize) -> Self {
822 let dim = 1 << n;
823 let amplitude = 1.0 / (n as f64).sqrt();
824 let mut state = Array1::zeros(dim);
825
826 for i in 0..n {
827 let index = 1 << i;
828 state[index] = Complex64::new(amplitude, 0.0);
829 }
830 QuantumState::Pure(state)
831 }
832
833 pub fn to_density_matrix(&self) -> std::result::Result<Array2<Complex64>, QuantumInfoError> {
835 match self {
836 QuantumState::Pure(psi) => {
837 let n = psi.len();
838 let mut rho = Array2::zeros((n, n));
839 for i in 0..n {
840 for j in 0..n {
841 rho[[i, j]] = psi[i] * psi[j].conj();
842 }
843 }
844 Ok(rho)
845 }
846 QuantumState::Mixed(rho) => Ok(rho.clone()),
847 }
848 }
849
850 pub fn dim(&self) -> usize {
852 match self {
853 QuantumState::Pure(psi) => psi.len(),
854 QuantumState::Mixed(rho) => rho.nrows(),
855 }
856 }
857
858 pub fn is_pure(&self) -> bool {
860 matches!(self, QuantumState::Pure(_))
861 }
862}
863
864#[derive(Debug, Clone)]
870pub struct QuantumChannel {
871 kraus_operators: Vec<Array2<Complex64>>,
873 input_dim: usize,
875 output_dim: usize,
877}
878
879impl QuantumChannel {
880 pub fn from_kraus(
882 kraus: Vec<Array2<Complex64>>,
883 ) -> std::result::Result<Self, QuantumInfoError> {
884 if kraus.is_empty() {
885 return Err(QuantumInfoError::InvalidState(
886 "Kraus operators cannot be empty".to_string(),
887 ));
888 }
889
890 let input_dim = kraus[0].ncols();
891 let output_dim = kraus[0].nrows();
892
893 Ok(Self {
897 kraus_operators: kraus,
898 input_dim,
899 output_dim,
900 })
901 }
902
903 pub fn identity(dim: usize) -> Self {
905 let mut identity = Array2::zeros((dim, dim));
906 for i in 0..dim {
907 identity[[i, i]] = Complex64::new(1.0, 0.0);
908 }
909 Self {
910 kraus_operators: vec![identity],
911 input_dim: dim,
912 output_dim: dim,
913 }
914 }
915
916 pub fn unitary(u: Array2<Complex64>) -> std::result::Result<Self, QuantumInfoError> {
918 let dim = u.nrows();
919 if dim != u.ncols() {
920 return Err(QuantumInfoError::InvalidState(
921 "Unitary matrix must be square".to_string(),
922 ));
923 }
924 Ok(Self {
925 kraus_operators: vec![u],
926 input_dim: dim,
927 output_dim: dim,
928 })
929 }
930
931 pub fn depolarizing(p: f64) -> Self {
935 let sqrt_1_p = (1.0 - p).sqrt();
936 let sqrt_p_3 = (p / 3.0).sqrt();
937
938 let k0 = Array2::from_shape_vec(
939 (2, 2),
940 vec![
941 Complex64::new(sqrt_1_p, 0.0),
942 Complex64::new(0.0, 0.0),
943 Complex64::new(0.0, 0.0),
944 Complex64::new(sqrt_1_p, 0.0),
945 ],
946 )
947 .expect("Valid shape");
948
949 let k1 = Array2::from_shape_vec(
950 (2, 2),
951 vec![
952 Complex64::new(0.0, 0.0),
953 Complex64::new(sqrt_p_3, 0.0),
954 Complex64::new(sqrt_p_3, 0.0),
955 Complex64::new(0.0, 0.0),
956 ],
957 )
958 .expect("Valid shape");
959
960 let k2 = Array2::from_shape_vec(
961 (2, 2),
962 vec![
963 Complex64::new(0.0, 0.0),
964 Complex64::new(0.0, -sqrt_p_3),
965 Complex64::new(0.0, sqrt_p_3),
966 Complex64::new(0.0, 0.0),
967 ],
968 )
969 .expect("Valid shape");
970
971 let k3 = Array2::from_shape_vec(
972 (2, 2),
973 vec![
974 Complex64::new(sqrt_p_3, 0.0),
975 Complex64::new(0.0, 0.0),
976 Complex64::new(0.0, 0.0),
977 Complex64::new(-sqrt_p_3, 0.0),
978 ],
979 )
980 .expect("Valid shape");
981
982 Self {
983 kraus_operators: vec![k0, k1, k2, k3],
984 input_dim: 2,
985 output_dim: 2,
986 }
987 }
988
989 pub fn amplitude_damping(gamma: f64) -> Self {
995 let sqrt_gamma = gamma.sqrt();
996 let sqrt_1_gamma = (1.0 - gamma).sqrt();
997
998 let k0 = Array2::from_shape_vec(
999 (2, 2),
1000 vec![
1001 Complex64::new(1.0, 0.0),
1002 Complex64::new(0.0, 0.0),
1003 Complex64::new(0.0, 0.0),
1004 Complex64::new(sqrt_1_gamma, 0.0),
1005 ],
1006 )
1007 .expect("Valid shape");
1008
1009 let k1 = Array2::from_shape_vec(
1010 (2, 2),
1011 vec![
1012 Complex64::new(0.0, 0.0),
1013 Complex64::new(sqrt_gamma, 0.0),
1014 Complex64::new(0.0, 0.0),
1015 Complex64::new(0.0, 0.0),
1016 ],
1017 )
1018 .expect("Valid shape");
1019
1020 Self {
1021 kraus_operators: vec![k0, k1],
1022 input_dim: 2,
1023 output_dim: 2,
1024 }
1025 }
1026
1027 pub fn phase_damping(gamma: f64) -> Self {
1031 let sqrt_gamma = gamma.sqrt();
1032 let sqrt_1_gamma = (1.0 - gamma).sqrt();
1033
1034 let k0 = Array2::from_shape_vec(
1035 (2, 2),
1036 vec![
1037 Complex64::new(1.0, 0.0),
1038 Complex64::new(0.0, 0.0),
1039 Complex64::new(0.0, 0.0),
1040 Complex64::new(sqrt_1_gamma, 0.0),
1041 ],
1042 )
1043 .expect("Valid shape");
1044
1045 let k1 = Array2::from_shape_vec(
1046 (2, 2),
1047 vec![
1048 Complex64::new(0.0, 0.0),
1049 Complex64::new(0.0, 0.0),
1050 Complex64::new(0.0, 0.0),
1051 Complex64::new(sqrt_gamma, 0.0),
1052 ],
1053 )
1054 .expect("Valid shape");
1055
1056 Self {
1057 kraus_operators: vec![k0, k1],
1058 input_dim: 2,
1059 output_dim: 2,
1060 }
1061 }
1062
1063 pub fn apply(
1065 &self,
1066 state: &QuantumState,
1067 ) -> std::result::Result<QuantumState, QuantumInfoError> {
1068 let rho = state.to_density_matrix()?;
1069
1070 let mut output = Array2::zeros((self.output_dim, self.output_dim));
1071
1072 for k in &self.kraus_operators {
1073 let k_dag = k.t().mapv(|c| c.conj());
1074 let k_rho = k.dot(&rho);
1075 let k_rho_k_dag = k_rho.dot(&k_dag);
1076 output = output + k_rho_k_dag;
1077 }
1078
1079 Ok(QuantumState::Mixed(output))
1080 }
1081
1082 pub fn input_dim(&self) -> usize {
1084 self.input_dim
1085 }
1086
1087 pub fn output_dim(&self) -> usize {
1089 self.output_dim
1090 }
1091
1092 pub fn to_choi(&self) -> std::result::Result<Array2<Complex64>, QuantumInfoError> {
1097 let d = self.input_dim;
1098 let choi_dim = d * self.output_dim;
1099 let mut choi = Array2::zeros((choi_dim, choi_dim));
1100
1101 for k in &self.kraus_operators {
1104 let mut vec_k = Array1::zeros(choi_dim);
1106 for j in 0..d {
1107 for i in 0..self.output_dim {
1108 vec_k[j * self.output_dim + i] = k[[i, j]];
1109 }
1110 }
1111
1112 for i in 0..choi_dim {
1114 for j in 0..choi_dim {
1115 choi[[i, j]] += vec_k[i] * vec_k[j].conj();
1116 }
1117 }
1118 }
1119
1120 Ok(choi)
1121 }
1122
1123 pub fn to_ptm(&self) -> std::result::Result<Array2<Complex64>, QuantumInfoError> {
1128 let d = self.input_dim;
1129 let num_paulis = d * d;
1130
1131 let paulis = generate_pauli_basis(d)?;
1133
1134 let mut ptm = Array2::zeros((num_paulis, num_paulis));
1135
1136 for (j, pj) in paulis.iter().enumerate() {
1137 let state_j = QuantumState::Mixed(pj.clone());
1139 let output = self.apply(&state_j)?;
1140 let rho_out = output.to_density_matrix()?;
1141
1142 for (i, pi) in paulis.iter().enumerate() {
1143 let trace = matrix_trace(&pi.dot(&rho_out));
1145 ptm[[i, j]] = trace / Complex64::new(d as f64, 0.0);
1146 }
1147 }
1148
1149 Ok(ptm)
1150 }
1151}
1152
1153fn generate_pauli_basis(
1155 dim: usize,
1156) -> std::result::Result<Vec<Array2<Complex64>>, QuantumInfoError> {
1157 if dim == 2 {
1158 let i = Array2::from_shape_vec(
1160 (2, 2),
1161 vec![
1162 Complex64::new(1.0, 0.0),
1163 Complex64::new(0.0, 0.0),
1164 Complex64::new(0.0, 0.0),
1165 Complex64::new(1.0, 0.0),
1166 ],
1167 )
1168 .map_err(|e| QuantumInfoError::NumericalError(e.to_string()))?;
1169
1170 let x = Array2::from_shape_vec(
1171 (2, 2),
1172 vec![
1173 Complex64::new(0.0, 0.0),
1174 Complex64::new(1.0, 0.0),
1175 Complex64::new(1.0, 0.0),
1176 Complex64::new(0.0, 0.0),
1177 ],
1178 )
1179 .map_err(|e| QuantumInfoError::NumericalError(e.to_string()))?;
1180
1181 let y = Array2::from_shape_vec(
1182 (2, 2),
1183 vec![
1184 Complex64::new(0.0, 0.0),
1185 Complex64::new(0.0, -1.0),
1186 Complex64::new(0.0, 1.0),
1187 Complex64::new(0.0, 0.0),
1188 ],
1189 )
1190 .map_err(|e| QuantumInfoError::NumericalError(e.to_string()))?;
1191
1192 let z = Array2::from_shape_vec(
1193 (2, 2),
1194 vec![
1195 Complex64::new(1.0, 0.0),
1196 Complex64::new(0.0, 0.0),
1197 Complex64::new(0.0, 0.0),
1198 Complex64::new(-1.0, 0.0),
1199 ],
1200 )
1201 .map_err(|e| QuantumInfoError::NumericalError(e.to_string()))?;
1202
1203 Ok(vec![i, x, y, z])
1204 } else {
1205 let mut basis = Vec::with_capacity(dim * dim);
1208 for i in 0..dim {
1209 for j in 0..dim {
1210 let mut mat = Array2::zeros((dim, dim));
1211 mat[[i, j]] = Complex64::new(1.0, 0.0);
1212 basis.push(mat);
1213 }
1214 }
1215 Ok(basis)
1216 }
1217}
1218
1219fn matrix_trace(matrix: &Array2<Complex64>) -> Complex64 {
1221 (0..matrix.nrows().min(matrix.ncols()))
1222 .map(|i| matrix[[i, i]])
1223 .sum()
1224}
1225
1226#[derive(Debug, Clone)]
1232pub struct StateTomographyConfig {
1233 pub shots_per_basis: usize,
1235 pub method: TomographyMethod,
1237 pub physical_constraints: bool,
1239 pub threshold: f64,
1241}
1242
1243impl Default for StateTomographyConfig {
1244 fn default() -> Self {
1245 Self {
1246 shots_per_basis: 1000,
1247 method: TomographyMethod::LinearInversion,
1248 physical_constraints: true,
1249 threshold: 1e-10,
1250 }
1251 }
1252}
1253
1254#[derive(Debug, Clone, Copy, PartialEq)]
1256pub enum TomographyMethod {
1257 LinearInversion,
1259 MaximumLikelihood,
1261 CompressedSensing,
1263 Bayesian,
1265}
1266
1267#[derive(Debug, Clone)]
1269pub struct TomographyResult {
1270 pub density_matrix: Array2<Complex64>,
1272 pub fidelity_estimate: Option<f64>,
1274 pub purity: f64,
1276 pub uncertainty: f64,
1278 pub total_measurements: usize,
1280}
1281
1282pub struct StateTomography {
1284 config: StateTomographyConfig,
1285 num_qubits: usize,
1286}
1287
1288impl StateTomography {
1289 pub fn new(num_qubits: usize, config: StateTomographyConfig) -> Self {
1291 Self { config, num_qubits }
1292 }
1293
1294 pub fn reconstruct(
1301 &self,
1302 measurements: &[MeasurementData],
1303 ) -> std::result::Result<TomographyResult, QuantumInfoError> {
1304 match self.config.method {
1305 TomographyMethod::LinearInversion => self.linear_inversion(measurements),
1306 TomographyMethod::MaximumLikelihood => self.maximum_likelihood(measurements),
1307 TomographyMethod::CompressedSensing => self.compressed_sensing(measurements),
1308 TomographyMethod::Bayesian => self.bayesian_estimation(measurements),
1309 }
1310 }
1311
1312 fn linear_inversion(
1314 &self,
1315 measurements: &[MeasurementData],
1316 ) -> std::result::Result<TomographyResult, QuantumInfoError> {
1317 let dim = 1 << self.num_qubits;
1318
1319 let mut rho = Array2::zeros((dim, dim));
1321
1322 for data in measurements {
1324 let expectation = data.expectation_value();
1325 let pauli = self.basis_to_pauli(&data.basis)?;
1326
1327 rho = rho + &pauli * Complex64::new(expectation / dim as f64, 0.0);
1329 }
1330
1331 let mut identity = Array2::zeros((dim, dim));
1333 for i in 0..dim {
1334 identity[[i, i]] = Complex64::new(1.0 / dim as f64, 0.0);
1335 }
1336 rho = rho + identity;
1337
1338 if self.config.physical_constraints {
1340 rho = self.make_physical(rho)?;
1341 }
1342
1343 let state = QuantumState::Mixed(rho.clone());
1344 let purity_val = purity(&state)?;
1345
1346 let total_measurements: usize = measurements.iter().map(|m| m.total_shots()).sum();
1347
1348 Ok(TomographyResult {
1349 density_matrix: rho,
1350 fidelity_estimate: None,
1351 purity: purity_val,
1352 uncertainty: 1.0 / (total_measurements as f64).sqrt(),
1353 total_measurements,
1354 })
1355 }
1356
1357 fn maximum_likelihood(
1359 &self,
1360 measurements: &[MeasurementData],
1361 ) -> std::result::Result<TomographyResult, QuantumInfoError> {
1362 let dim = 1 << self.num_qubits;
1363
1364 let initial = self.linear_inversion(measurements)?;
1366 let mut rho = initial.density_matrix;
1367
1368 let max_iterations = 100;
1370 let tolerance = 1e-6;
1371
1372 for _iter in 0..max_iterations {
1373 let r = self.compute_r_matrix(&rho, measurements)?;
1375
1376 let r_rho = r.dot(&rho);
1378 let r_rho_r = r_rho.dot(&r);
1379
1380 let trace: Complex64 = (0..dim).map(|i| r_rho_r[[i, i]]).sum();
1381 let trace_re = trace.re.max(1e-15);
1382
1383 let rho_new = r_rho_r / Complex64::new(trace_re, 0.0);
1384
1385 let diff: f64 = rho
1387 .iter()
1388 .zip(rho_new.iter())
1389 .map(|(a, b)| (a - b).norm())
1390 .sum();
1391 if diff < tolerance {
1392 rho = rho_new;
1393 break;
1394 }
1395
1396 rho = rho_new;
1397 }
1398
1399 let state = QuantumState::Mixed(rho.clone());
1400 let purity_val = purity(&state)?;
1401
1402 let total_measurements: usize = measurements.iter().map(|m| m.total_shots()).sum();
1403
1404 Ok(TomographyResult {
1405 density_matrix: rho,
1406 fidelity_estimate: None,
1407 purity: purity_val,
1408 uncertainty: 1.0 / (total_measurements as f64).sqrt(),
1409 total_measurements,
1410 })
1411 }
1412
1413 fn compute_r_matrix(
1415 &self,
1416 rho: &Array2<Complex64>,
1417 measurements: &[MeasurementData],
1418 ) -> std::result::Result<Array2<Complex64>, QuantumInfoError> {
1419 let dim = rho.nrows();
1420 let mut r = Array2::zeros((dim, dim));
1421
1422 for data in measurements {
1423 let pauli = self.basis_to_pauli(&data.basis)?;
1424
1425 let p_rho = pauli.dot(rho);
1427 let exp_rho: Complex64 = (0..dim).map(|i| p_rho[[i, i]]).sum();
1428
1429 let exp_data = data.expectation_value();
1431
1432 if exp_rho.re.abs() > 1e-10 {
1433 let weight = exp_data / exp_rho.re;
1434 r = r + &pauli * Complex64::new(weight, 0.0);
1435 }
1436 }
1437
1438 let trace: Complex64 = (0..dim).map(|i| r[[i, i]]).sum();
1440 if trace.re.abs() > 1e-10 {
1441 r /= Complex64::new(trace.re, 0.0);
1442 }
1443
1444 Ok(r)
1445 }
1446
1447 fn compressed_sensing(
1449 &self,
1450 measurements: &[MeasurementData],
1451 ) -> std::result::Result<TomographyResult, QuantumInfoError> {
1452 let mut result = self.linear_inversion(measurements)?;
1457
1458 result.density_matrix = self.truncate_rank(result.density_matrix, 4)?;
1460
1461 Ok(result)
1462 }
1463
1464 fn bayesian_estimation(
1466 &self,
1467 measurements: &[MeasurementData],
1468 ) -> std::result::Result<TomographyResult, QuantumInfoError> {
1469 let dim = 1 << self.num_qubits;
1471 let mut rho = Array2::zeros((dim, dim));
1472 for i in 0..dim {
1473 rho[[i, i]] = Complex64::new(1.0 / dim as f64, 0.0);
1474 }
1475
1476 for data in measurements {
1480 let pauli = self.basis_to_pauli(&data.basis)?;
1481 let exp_data = data.expectation_value();
1482
1483 let p_rho = pauli.dot(&rho);
1486 let exp_rho: Complex64 = (0..dim).map(|i| p_rho[[i, i]]).sum();
1487
1488 let diff = exp_data - exp_rho.re;
1489 let learning_rate = 0.1;
1490
1491 rho = rho + &pauli * Complex64::new(learning_rate * diff / dim as f64, 0.0);
1492 }
1493
1494 rho = self.make_physical(rho)?;
1496
1497 let state = QuantumState::Mixed(rho.clone());
1498 let purity_val = purity(&state)?;
1499
1500 let total_measurements: usize = measurements.iter().map(|m| m.total_shots()).sum();
1501
1502 Ok(TomographyResult {
1503 density_matrix: rho,
1504 fidelity_estimate: None,
1505 purity: purity_val,
1506 uncertainty: 1.0 / (total_measurements as f64).sqrt(),
1507 total_measurements,
1508 })
1509 }
1510
1511 fn basis_to_pauli(
1513 &self,
1514 basis: &str,
1515 ) -> std::result::Result<Array2<Complex64>, QuantumInfoError> {
1516 let dim = 1 << self.num_qubits;
1517
1518 if basis.len() != self.num_qubits {
1519 return Err(QuantumInfoError::InvalidState(format!(
1520 "Basis string length {} doesn't match qubit count {}",
1521 basis.len(),
1522 self.num_qubits
1523 )));
1524 }
1525
1526 let mut result: Option<Array2<Complex64>> = None;
1528
1529 for c in basis.chars() {
1530 let single_qubit = match c {
1531 'I' => Array2::from_shape_vec(
1532 (2, 2),
1533 vec![
1534 Complex64::new(1.0, 0.0),
1535 Complex64::new(0.0, 0.0),
1536 Complex64::new(0.0, 0.0),
1537 Complex64::new(1.0, 0.0),
1538 ],
1539 )
1540 .map_err(|e| QuantumInfoError::NumericalError(e.to_string()))?,
1541 'X' => Array2::from_shape_vec(
1542 (2, 2),
1543 vec![
1544 Complex64::new(0.0, 0.0),
1545 Complex64::new(1.0, 0.0),
1546 Complex64::new(1.0, 0.0),
1547 Complex64::new(0.0, 0.0),
1548 ],
1549 )
1550 .map_err(|e| QuantumInfoError::NumericalError(e.to_string()))?,
1551 'Y' => Array2::from_shape_vec(
1552 (2, 2),
1553 vec![
1554 Complex64::new(0.0, 0.0),
1555 Complex64::new(0.0, -1.0),
1556 Complex64::new(0.0, 1.0),
1557 Complex64::new(0.0, 0.0),
1558 ],
1559 )
1560 .map_err(|e| QuantumInfoError::NumericalError(e.to_string()))?,
1561 'Z' => Array2::from_shape_vec(
1562 (2, 2),
1563 vec![
1564 Complex64::new(1.0, 0.0),
1565 Complex64::new(0.0, 0.0),
1566 Complex64::new(0.0, 0.0),
1567 Complex64::new(-1.0, 0.0),
1568 ],
1569 )
1570 .map_err(|e| QuantumInfoError::NumericalError(e.to_string()))?,
1571 _ => {
1572 return Err(QuantumInfoError::InvalidState(format!(
1573 "Unknown basis character: {}",
1574 c
1575 )))
1576 }
1577 };
1578
1579 result = Some(match result {
1580 None => single_qubit,
1581 Some(r) => kronecker_product(&r, &single_qubit),
1582 });
1583 }
1584
1585 result.ok_or_else(|| QuantumInfoError::InvalidState("Empty basis string".to_string()))
1586 }
1587
1588 fn make_physical(
1590 &self,
1591 mut rho: Array2<Complex64>,
1592 ) -> std::result::Result<Array2<Complex64>, QuantumInfoError> {
1593 let dim = rho.nrows();
1594
1595 let rho_dag = rho.t().mapv(|c| c.conj());
1597 rho = (&rho + &rho_dag) / Complex64::new(2.0, 0.0);
1598
1599 let trace: Complex64 = (0..dim).map(|i| rho[[i, i]]).sum();
1605 if trace.re.abs() > 1e-10 {
1606 rho /= Complex64::new(trace.re, 0.0);
1607 }
1608
1609 Ok(rho)
1610 }
1611
1612 fn truncate_rank(
1614 &self,
1615 rho: Array2<Complex64>,
1616 max_rank: usize,
1617 ) -> std::result::Result<Array2<Complex64>, QuantumInfoError> {
1618 Ok(rho)
1624 }
1625}
1626
1627#[derive(Debug, Clone)]
1629pub struct MeasurementData {
1630 pub basis: String,
1632 pub counts: std::collections::HashMap<String, usize>,
1636}
1637
1638impl MeasurementData {
1639 pub fn new(basis: &str, counts: std::collections::HashMap<String, usize>) -> Self {
1641 Self {
1642 basis: basis.to_string(),
1643 counts,
1644 }
1645 }
1646
1647 pub fn total_shots(&self) -> usize {
1649 self.counts.values().sum()
1650 }
1651
1652 pub fn expectation_value(&self) -> f64 {
1654 let total = self.total_shots() as f64;
1655 if total < 1e-10 {
1656 return 0.0;
1657 }
1658
1659 let mut expectation = 0.0;
1660 for (outcome, &count) in &self.counts {
1661 let parity: usize = outcome.chars().filter(|&c| c == '1').count();
1664 let eigenvalue = if parity % 2 == 0 { 1.0 } else { -1.0 };
1665 expectation += eigenvalue * count as f64;
1666 }
1667
1668 expectation / total
1669 }
1670}
1671
1672fn kronecker_product(a: &Array2<Complex64>, b: &Array2<Complex64>) -> Array2<Complex64> {
1674 let (m, n) = (a.nrows(), a.ncols());
1675 let (p, q) = (b.nrows(), b.ncols());
1676
1677 let mut result = Array2::zeros((m * p, n * q));
1678
1679 for i in 0..m {
1680 for j in 0..n {
1681 for k in 0..p {
1682 for l in 0..q {
1683 result[[i * p + k, j * q + l]] = a[[i, j]] * b[[k, l]];
1684 }
1685 }
1686 }
1687 }
1688
1689 result
1690}
1691
1692#[derive(Debug, Clone)]
1698pub struct ClassicalShadow {
1699 num_snapshots: usize,
1701 bases: Vec<String>,
1703 outcomes: Vec<String>,
1705 num_qubits: usize,
1707}
1708
1709impl ClassicalShadow {
1710 pub fn from_measurements(
1712 num_qubits: usize,
1713 bases: Vec<String>,
1714 outcomes: Vec<String>,
1715 ) -> std::result::Result<Self, QuantumInfoError> {
1716 if bases.len() != outcomes.len() {
1717 return Err(QuantumInfoError::InvalidState(
1718 "Number of bases must match number of outcomes".to_string(),
1719 ));
1720 }
1721
1722 Ok(Self {
1723 num_snapshots: bases.len(),
1724 bases,
1725 outcomes,
1726 num_qubits,
1727 })
1728 }
1729
1730 pub fn generate_random_bases(num_qubits: usize, num_snapshots: usize) -> Vec<String> {
1732 let mut rng = thread_rng();
1733 let paulis = ['X', 'Y', 'Z'];
1734
1735 (0..num_snapshots)
1736 .map(|_| {
1737 (0..num_qubits)
1738 .map(|_| paulis[rng.gen_range(0..3)])
1739 .collect()
1740 })
1741 .collect()
1742 }
1743
1744 pub fn estimate_observable(
1752 &self,
1753 observable: &str,
1754 ) -> std::result::Result<f64, QuantumInfoError> {
1755 if observable.len() != self.num_qubits {
1756 return Err(QuantumInfoError::DimensionMismatch(format!(
1757 "Observable length {} doesn't match qubit count {}",
1758 observable.len(),
1759 self.num_qubits
1760 )));
1761 }
1762
1763 let mut sum = 0.0;
1764 let mut valid_snapshots = 0;
1765
1766 for (basis, outcome) in self.bases.iter().zip(self.outcomes.iter()) {
1767 let mut useful = true;
1770 let mut contrib = 1.0;
1771
1772 for ((obs_char, basis_char), out_char) in
1773 observable.chars().zip(basis.chars()).zip(outcome.chars())
1774 {
1775 if obs_char == 'I' {
1776 continue;
1778 }
1779
1780 if obs_char != basis_char {
1781 useful = false;
1783 break;
1784 }
1785
1786 let eigenvalue = if out_char == '0' { 1.0 } else { -1.0 };
1788 contrib *= 3.0 * eigenvalue;
1789 }
1790
1791 if useful {
1792 sum += contrib;
1793 valid_snapshots += 1;
1794 }
1795 }
1796
1797 if valid_snapshots == 0 {
1798 return Ok(0.0);
1799 }
1800
1801 Ok(sum / valid_snapshots as f64)
1802 }
1803
1804 pub fn estimate_observables(
1806 &self,
1807 observables: &[String],
1808 ) -> std::result::Result<Vec<f64>, QuantumInfoError> {
1809 observables
1810 .iter()
1811 .map(|obs| self.estimate_observable(obs))
1812 .collect()
1813 }
1814
1815 pub fn estimate_fidelity(
1817 &self,
1818 target: &QuantumState,
1819 ) -> std::result::Result<f64, QuantumInfoError> {
1820 let target_dm = target.to_density_matrix()?;
1825
1826 let dim = target_dm.nrows();
1830 let mut fidelity_sum = 0.0;
1831 let num_samples = 100;
1832
1833 let mut rng = thread_rng();
1834
1835 for _ in 0..num_samples {
1836 let paulis = ['I', 'X', 'Y', 'Z'];
1838 let pauli_string: String = (0..self.num_qubits)
1839 .map(|_| paulis[rng.gen_range(0..4)])
1840 .collect();
1841
1842 if let Ok(shadow_exp) = self.estimate_observable(&pauli_string) {
1844 fidelity_sum += shadow_exp.abs();
1847 }
1848 }
1849
1850 Ok((fidelity_sum / num_samples as f64).min(1.0))
1851 }
1852
1853 pub fn num_snapshots(&self) -> usize {
1855 self.num_snapshots
1856 }
1857}
1858
1859pub struct ProcessTomography {
1865 num_qubits: usize,
1867 config: StateTomographyConfig,
1869}
1870
1871impl ProcessTomography {
1872 pub fn new(num_qubits: usize, config: StateTomographyConfig) -> Self {
1874 Self { num_qubits, config }
1875 }
1876
1877 pub fn reconstruct(
1885 &self,
1886 data: &ProcessTomographyData,
1887 ) -> std::result::Result<QuantumChannel, QuantumInfoError> {
1888 let dim = 1 << self.num_qubits;
1889
1890 let mut choi = Array2::zeros((dim * dim, dim * dim));
1892
1893 for (input_state, output_dm) in &data.state_pairs {
1895 let input_dm = input_state.to_density_matrix()?;
1896
1897 let contrib = kronecker_product(&input_dm, output_dm);
1899 choi = choi + contrib;
1900 }
1901
1902 choi /= Complex64::new(data.state_pairs.len() as f64, 0.0);
1904
1905 let kraus = self.choi_to_kraus(&choi)?;
1907
1908 QuantumChannel::from_kraus(kraus)
1909 }
1910
1911 fn choi_to_kraus(
1913 &self,
1914 choi: &Array2<Complex64>,
1915 ) -> std::result::Result<Vec<Array2<Complex64>>, QuantumInfoError> {
1916 let dim = 1 << self.num_qubits;
1917
1918 let mut k0 = Array2::zeros((dim, dim));
1926 for i in 0..dim {
1927 k0[[i, i]] = (choi[[i * dim + i, i * dim + i]]).sqrt();
1928 }
1929
1930 Ok(vec![k0])
1931 }
1932}
1933
1934#[derive(Debug, Clone)]
1936pub struct ProcessTomographyData {
1937 pub state_pairs: Vec<(QuantumState, Array2<Complex64>)>,
1939}
1940
1941impl ProcessTomographyData {
1942 pub fn new() -> Self {
1944 Self {
1945 state_pairs: Vec::new(),
1946 }
1947 }
1948
1949 pub fn add_pair(&mut self, input: QuantumState, output: Array2<Complex64>) {
1951 self.state_pairs.push((input, output));
1952 }
1953}
1954
1955impl Default for ProcessTomographyData {
1956 fn default() -> Self {
1957 Self::new()
1958 }
1959}
1960
1961#[cfg(test)]
1966mod tests {
1967 use super::*;
1968
1969 #[test]
1970 fn test_state_fidelity_pure_states() {
1971 let psi = Array1::from_vec(vec![
1973 Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0),
1974 Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0),
1975 ]);
1976
1977 let state1 = QuantumState::Pure(psi.clone());
1978 let state2 = QuantumState::Pure(psi);
1979
1980 let fid = state_fidelity(&state1, &state2).expect("Fidelity calculation should succeed");
1981 assert!(
1982 (fid - 1.0).abs() < 1e-10,
1983 "Fidelity of identical states should be 1"
1984 );
1985 }
1986
1987 #[test]
1988 fn test_state_fidelity_orthogonal_states() {
1989 let state1 = QuantumState::computational_basis(2, 0);
1991 let state2 = QuantumState::computational_basis(2, 1);
1992
1993 let fid = state_fidelity(&state1, &state2).expect("Fidelity calculation should succeed");
1994 assert!(
1995 fid.abs() < 1e-10,
1996 "Fidelity of orthogonal states should be 0"
1997 );
1998 }
1999
2000 #[test]
2001 fn test_purity_pure_state() {
2002 let state = QuantumState::computational_basis(4, 0);
2003 let p = purity(&state).expect("Purity calculation should succeed");
2004 assert!((p - 1.0).abs() < 1e-10, "Purity of pure state should be 1");
2005 }
2006
2007 #[test]
2008 fn test_purity_maximally_mixed() {
2009 let state = QuantumState::maximally_mixed(4);
2010 let p = purity(&state).expect("Purity calculation should succeed");
2011 assert!(
2013 (p - 0.25).abs() < 1e-10,
2014 "Purity of maximally mixed state should be 1/d"
2015 );
2016 }
2017
2018 #[test]
2019 fn test_von_neumann_entropy_pure_state() {
2020 let state = QuantumState::computational_basis(2, 0);
2021 let s = von_neumann_entropy(&state, None).expect("Entropy calculation should succeed");
2022 assert!(s.abs() < 1e-10, "Entropy of pure state should be 0");
2023 }
2024
2025 #[test]
2026 fn test_von_neumann_entropy_maximally_mixed() {
2027 let state = QuantumState::maximally_mixed(4);
2028 let s = von_neumann_entropy(&state, None).expect("Entropy calculation should succeed");
2029 assert!(
2031 (s - 2.0).abs() < 0.5,
2032 "Entropy of maximally mixed state should be log₂(d)"
2033 );
2034 }
2035
2036 #[test]
2037 fn test_bell_state_creation() {
2038 let bell = QuantumState::bell_state(0);
2039 let p = purity(&bell).expect("Purity calculation should succeed");
2040 assert!((p - 1.0).abs() < 1e-10, "Bell state should be pure");
2041 }
2042
2043 #[test]
2044 fn test_ghz_state_creation() {
2045 let ghz = QuantumState::ghz_state(3);
2046 assert_eq!(ghz.dim(), 8, "3-qubit GHZ state should have dimension 8");
2047
2048 let p = purity(&ghz).expect("Purity calculation should succeed");
2049 assert!((p - 1.0).abs() < 1e-10, "GHZ state should be pure");
2050 }
2051
2052 #[test]
2053 fn test_w_state_creation() {
2054 let w = QuantumState::w_state(3);
2055 assert_eq!(w.dim(), 8, "3-qubit W state should have dimension 8");
2056
2057 let p = purity(&w).expect("Purity calculation should succeed");
2058 assert!((p - 1.0).abs() < 1e-10, "W state should be pure");
2059 }
2060
2061 #[test]
2062 fn test_partial_trace() {
2063 let bell = QuantumState::bell_state(0);
2065 let rho = bell
2066 .to_density_matrix()
2067 .expect("Density matrix conversion should succeed");
2068
2069 let rho_a = partial_trace(&rho, 2, false).expect("Partial trace should succeed");
2071
2072 assert_eq!(rho_a.nrows(), 2);
2074 assert!((rho_a[[0, 0]].re - 0.5).abs() < 1e-10);
2075 assert!((rho_a[[1, 1]].re - 0.5).abs() < 1e-10);
2076 }
2077
2078 #[test]
2079 fn test_concurrence_separable_state() {
2080 let state = QuantumState::computational_basis(4, 0);
2082 let c = concurrence(&state).expect("Concurrence calculation should succeed");
2083 assert!(c < 1e-10, "Concurrence of separable state should be 0");
2084 }
2085
2086 #[test]
2087 fn test_concurrence_bell_state() {
2088 let bell = QuantumState::bell_state(0);
2090 let c = concurrence(&bell).expect("Concurrence calculation should succeed");
2091 assert!(
2092 (c - 1.0).abs() < 0.1,
2093 "Concurrence of Bell state should be ~1"
2094 );
2095 }
2096
2097 #[test]
2098 fn test_quantum_channel_identity() {
2099 let channel = QuantumChannel::identity(2);
2100
2101 let input = QuantumState::computational_basis(2, 0);
2102 let output = channel
2103 .apply(&input)
2104 .expect("Channel application should succeed");
2105
2106 let fid = state_fidelity(&input, &output).expect("Fidelity calculation should succeed");
2107 assert!(
2108 (fid - 1.0).abs() < 1e-10,
2109 "Identity channel should preserve state"
2110 );
2111 }
2112
2113 #[test]
2114 fn test_quantum_channel_depolarizing() {
2115 let channel = QuantumChannel::depolarizing(0.1);
2116
2117 let input = QuantumState::computational_basis(2, 0);
2118 let output = channel
2119 .apply(&input)
2120 .expect("Channel application should succeed");
2121
2122 let p = purity(&output).expect("Purity calculation should succeed");
2124 assert!(p < 1.0, "Depolarizing channel should decrease purity");
2125 assert!(p > 0.5, "Low error rate should keep purity relatively high");
2126 }
2127
2128 #[test]
2129 fn test_average_gate_fidelity_identity() {
2130 let channel = QuantumChannel::identity(2);
2131 let f_avg =
2132 average_gate_fidelity(&channel, None).expect("Average gate fidelity should succeed");
2133 assert!(
2134 (f_avg - 1.0).abs() < 1e-10,
2135 "Identity channel should have fidelity 1"
2136 );
2137 }
2138
2139 #[test]
2140 fn test_measurement_data_expectation() {
2141 let mut counts = std::collections::HashMap::new();
2142 counts.insert("0".to_string(), 700);
2143 counts.insert("1".to_string(), 300);
2144
2145 let data = MeasurementData::new("Z", counts);
2146
2147 let exp = data.expectation_value();
2149 assert!((exp - 0.4).abs() < 1e-10);
2150 }
2151
2152 #[test]
2153 fn test_classical_shadow_observable_estimation() {
2154 let bases = vec!["Z".to_string(), "Z".to_string(), "Z".to_string()];
2156 let outcomes = vec!["0".to_string(), "0".to_string(), "1".to_string()];
2157
2158 let shadow = ClassicalShadow::from_measurements(1, bases, outcomes)
2159 .expect("Shadow creation should succeed");
2160
2161 let z_exp = shadow
2163 .estimate_observable("Z")
2164 .expect("Observable estimation should succeed");
2165
2166 assert!((z_exp - 1.0).abs() < 1e-10);
2168 }
2169
2170 #[test]
2171 fn test_kronecker_product() {
2172 let a = Array2::from_shape_vec(
2173 (2, 2),
2174 vec![
2175 Complex64::new(1.0, 0.0),
2176 Complex64::new(0.0, 0.0),
2177 Complex64::new(0.0, 0.0),
2178 Complex64::new(1.0, 0.0),
2179 ],
2180 )
2181 .expect("Valid shape");
2182
2183 let b = Array2::from_shape_vec(
2184 (2, 2),
2185 vec![
2186 Complex64::new(0.0, 0.0),
2187 Complex64::new(1.0, 0.0),
2188 Complex64::new(1.0, 0.0),
2189 Complex64::new(0.0, 0.0),
2190 ],
2191 )
2192 .expect("Valid shape");
2193
2194 let result = kronecker_product(&a, &b);
2195 assert_eq!(result.nrows(), 4);
2196 assert_eq!(result.ncols(), 4);
2197
2198 assert!((result[[0, 1]].re - 1.0).abs() < 1e-10);
2200 assert!((result[[1, 0]].re - 1.0).abs() < 1e-10);
2201 assert!((result[[2, 3]].re - 1.0).abs() < 1e-10);
2202 assert!((result[[3, 2]].re - 1.0).abs() < 1e-10);
2203 }
2204}