1use crate::{
7 error::{QuantRS2Error, QuantRS2Result},
8 matrix_ops::{DenseMatrix, QuantumMatrix},
9};
10use scirs2_core::ndarray::{s, Array1, Array2};
11use scirs2_core::Complex;
12
13struct HermitianEigen {
19 eigenvalues: Array1<f64>,
21 eigenvectors: Array2<Complex<f64>>,
23}
24
25fn hermitian_eigen_decompose(matrix: &Array2<Complex<f64>>) -> QuantRS2Result<HermitianEigen> {
40 let n = matrix.nrows();
41 if n != matrix.ncols() {
42 return Err(QuantRS2Error::InvalidInput(
43 "Hermitian eigendecomposition requires a square matrix".to_string(),
44 ));
45 }
46 if n == 0 {
47 return Ok(HermitianEigen {
48 eigenvalues: Array1::zeros(0),
49 eigenvectors: Array2::zeros((0, 0)),
50 });
51 }
52
53 let mut a: Array2<Complex<f64>> = Array2::zeros((n, n));
55 for i in 0..n {
56 for j in 0..n {
57 a[[i, j]] = (matrix[[i, j]] + matrix[[j, i]].conj()) * Complex::new(0.5, 0.0);
58 }
59 }
60
61 let mut v: Array2<Complex<f64>> = Array2::eye(n);
63
64 if n == 1 {
65 return Ok(HermitianEigen {
66 eigenvalues: Array1::from_vec(vec![a[[0, 0]].re]),
67 eigenvectors: v,
68 });
69 }
70
71 let max_sweeps = 100;
72 let convergence_eps = 1e-15;
73
74 for _sweep in 0..max_sweeps {
75 let mut off_norm_sq = 0.0_f64;
77 for p in 0..n {
78 for q in (p + 1)..n {
79 off_norm_sq += a[[p, q]].norm_sqr();
80 }
81 }
82 if off_norm_sq.sqrt() < convergence_eps {
83 break;
84 }
85
86 for p in 0..n {
88 for q in (p + 1)..n {
89 let apq = a[[p, q]];
90 if apq.norm() < convergence_eps {
91 continue;
92 }
93
94 let app = a[[p, p]].re;
95 let aqq = a[[q, q]].re;
96
97 let abs_apq = apq.norm();
104 let phase = apq / Complex::new(abs_apq, 0.0); let tau = (aqq - app) / (2.0 * abs_apq);
107 let t = if tau >= 0.0 {
109 1.0 / (tau + (tau * tau + 1.0).sqrt())
110 } else {
111 -1.0 / (-tau + (tau * tau + 1.0).sqrt())
112 };
113 let c = 1.0 / (t * t + 1.0).sqrt();
114 let s = t * c;
115
116 let s_phase = phase * Complex::new(s, 0.0); let c_cplx = Complex::new(c, 0.0);
118
119 let mut rotation: Array2<Complex<f64>> = Array2::eye(n);
129 rotation[[p, p]] = c_cplx;
130 rotation[[q, q]] = c_cplx;
131 rotation[[p, q]] = s_phase;
132 rotation[[q, p]] = -s_phase.conj();
133 let rotation_dag = rotation.mapv(|z| z.conj()).t().to_owned();
134
135 a = rotation_dag.dot(&a).dot(&rotation);
136 v = v.dot(&rotation);
137 }
138 }
139 }
140
141 let mut eigenvalues: Array1<f64> = Array1::zeros(n);
143 for i in 0..n {
144 eigenvalues[i] = a[[i, i]].re;
145 }
146
147 Ok(HermitianEigen {
148 eigenvalues,
149 eigenvectors: v,
150 })
151}
152
153#[derive(Debug, Clone)]
155pub struct QuantumChannel {
156 pub input_dim: usize,
158 pub output_dim: usize,
160 pub kraus: Option<KrausRepresentation>,
162 pub choi: Option<ChoiRepresentation>,
164 pub stinespring: Option<StinespringRepresentation>,
166 tolerance: f64,
168}
169
170#[derive(Debug, Clone)]
172pub struct KrausRepresentation {
173 pub operators: Vec<Array2<Complex<f64>>>,
175}
176
177#[derive(Debug, Clone)]
179pub struct ChoiRepresentation {
180 pub matrix: Array2<Complex<f64>>,
182}
183
184#[derive(Debug, Clone)]
186pub struct StinespringRepresentation {
187 pub isometry: Array2<Complex<f64>>,
189 pub env_dim: usize,
191}
192
193impl QuantumChannel {
194 pub fn from_kraus(operators: Vec<Array2<Complex<f64>>>) -> QuantRS2Result<Self> {
196 if operators.is_empty() {
197 return Err(QuantRS2Error::InvalidInput(
198 "At least one Kraus operator required".to_string(),
199 ));
200 }
201
202 let shape = operators[0].shape();
204 let output_dim = shape[0];
205 let input_dim = shape[1];
206
207 for (i, op) in operators.iter().enumerate() {
209 if op.shape() != shape {
210 return Err(QuantRS2Error::InvalidInput(format!(
211 "Kraus operator {i} has inconsistent dimensions"
212 )));
213 }
214 }
215
216 let kraus = KrausRepresentation { operators };
217
218 let channel = Self {
219 input_dim,
220 output_dim,
221 kraus: Some(kraus),
222 choi: None,
223 stinespring: None,
224 tolerance: 1e-10,
225 };
226
227 channel.verify_kraus_completeness()?;
229
230 Ok(channel)
231 }
232
233 pub fn from_choi(matrix: Array2<Complex<f64>>) -> QuantRS2Result<Self> {
242 let total_dim = matrix.shape()[0];
243
244 if matrix.shape()[0] != matrix.shape()[1] {
246 return Err(QuantRS2Error::InvalidInput(
247 "Choi matrix must be square".to_string(),
248 ));
249 }
250
251 let dim = (total_dim as f64).sqrt().round() as usize;
254 if dim * dim != total_dim {
255 return Err(QuantRS2Error::InvalidInput(
256 "Choi matrix dimension must be a perfect square (square channel: total_dim = d * d)"
257 .to_string(),
258 ));
259 }
260
261 let choi = ChoiRepresentation { matrix };
262
263 let channel = Self {
264 input_dim: dim,
265 output_dim: dim,
266 kraus: None,
267 choi: Some(choi),
268 stinespring: None,
269 tolerance: 1e-10,
270 };
271
272 channel.verify_choi_properties()?;
274
275 Ok(channel)
276 }
277
278 pub fn to_kraus(&mut self) -> QuantRS2Result<&KrausRepresentation> {
280 if self.kraus.is_some() {
281 return self
282 .kraus
283 .as_ref()
284 .ok_or_else(|| QuantRS2Error::InvalidInput("Kraus representation missing".into()));
285 }
286
287 if let Some(choi) = &self.choi {
288 let kraus = self.choi_to_kraus(&choi.matrix)?;
289 self.kraus = Some(kraus);
290 self.kraus
291 .as_ref()
292 .ok_or_else(|| QuantRS2Error::InvalidInput("Kraus conversion failed".into()))
293 } else if let Some(stinespring) = &self.stinespring {
294 let kraus = self.stinespring_to_kraus(&stinespring.isometry, stinespring.env_dim)?;
295 self.kraus = Some(kraus);
296 self.kraus
297 .as_ref()
298 .ok_or_else(|| QuantRS2Error::InvalidInput("Kraus conversion failed".into()))
299 } else {
300 Err(QuantRS2Error::InvalidInput(
301 "No representation available".to_string(),
302 ))
303 }
304 }
305
306 pub fn to_choi(&mut self) -> QuantRS2Result<&ChoiRepresentation> {
308 if self.choi.is_some() {
309 return self
310 .choi
311 .as_ref()
312 .ok_or_else(|| QuantRS2Error::InvalidInput("Choi representation missing".into()));
313 }
314
315 if let Some(kraus) = &self.kraus {
316 let choi = self.kraus_to_choi(&kraus.operators)?;
317 self.choi = Some(choi);
318 self.choi
319 .as_ref()
320 .ok_or_else(|| QuantRS2Error::InvalidInput("Choi conversion failed".into()))
321 } else if let Some(stinespring) = &self.stinespring {
322 let kraus = self.stinespring_to_kraus(&stinespring.isometry, stinespring.env_dim)?;
324 let choi = self.kraus_to_choi(&kraus.operators)?;
325 self.choi = Some(choi);
326 self.choi
327 .as_ref()
328 .ok_or_else(|| QuantRS2Error::InvalidInput("Choi conversion failed".into()))
329 } else {
330 Err(QuantRS2Error::InvalidInput(
331 "No representation available".to_string(),
332 ))
333 }
334 }
335
336 pub fn to_stinespring(&mut self) -> QuantRS2Result<&StinespringRepresentation> {
338 if self.stinespring.is_some() {
339 return self.stinespring.as_ref().ok_or_else(|| {
340 QuantRS2Error::InvalidInput("Stinespring representation missing".into())
341 });
342 }
343
344 let kraus = self.to_kraus()?.clone();
346 let stinespring = self.kraus_to_stinespring(&kraus.operators)?;
347 self.stinespring = Some(stinespring);
348 self.stinespring
349 .as_ref()
350 .ok_or_else(|| QuantRS2Error::InvalidInput("Stinespring conversion failed".into()))
351 }
352
353 pub fn apply(&mut self, rho: &Array2<Complex<f64>>) -> QuantRS2Result<Array2<Complex<f64>>> {
355 let kraus = self.to_kraus()?.clone();
357 let output_dim = self.output_dim;
358
359 let mut result = Array2::zeros((output_dim, output_dim));
360
361 for k in &kraus.operators {
362 let k_dag = k.mapv(|z| z.conj()).t().to_owned();
363 let term = k.dot(rho).dot(&k_dag);
364 result = result + term;
365 }
366
367 Ok(result)
368 }
369
370 pub fn is_unitary(&mut self) -> QuantRS2Result<bool> {
372 let kraus = self.to_kraus()?;
373
374 if kraus.operators.len() != 1 {
376 return Ok(false);
377 }
378
379 let mat = DenseMatrix::new(kraus.operators[0].clone())?;
380 mat.is_unitary(self.tolerance)
381 }
382
383 pub fn is_depolarizing(&mut self) -> QuantRS2Result<bool> {
385 if self.input_dim != 2 || self.output_dim != 2 {
389 return Ok(false); }
391
392 let kraus = self.to_kraus()?;
393
394 if kraus.operators.len() != 4 {
395 return Ok(false);
396 }
397
398 Ok(true)
401 }
402
403 pub fn depolarizing_parameter(&mut self) -> QuantRS2Result<Option<f64>> {
405 if !self.is_depolarizing()? {
406 return Ok(None);
407 }
408
409 let kraus = self.to_kraus()?;
410
411 let k0_coeff = kraus.operators[0][[0, 0]].norm();
414 let p = 4.0 * k0_coeff.mul_add(-k0_coeff, 1.0) / 3.0;
415
416 Ok(Some(p))
417 }
418
419 fn verify_kraus_completeness(&self) -> QuantRS2Result<()> {
421 if let Some(kraus) = &self.kraus {
422 let mut sum: Array2<Complex<f64>> = Array2::zeros((self.input_dim, self.input_dim));
423
424 for k in &kraus.operators {
425 let k_dag = k.mapv(|z| z.conj()).t().to_owned();
426 sum = sum + k_dag.dot(k);
427 }
428
429 for i in 0..self.input_dim {
431 for j in 0..self.input_dim {
432 let expected = if i == j {
433 Complex::new(1.0, 0.0)
434 } else {
435 Complex::new(0.0, 0.0)
436 };
437 let diff: Complex<f64> = sum[[i, j]] - expected;
438 if diff.norm() > self.tolerance {
439 return Err(QuantRS2Error::InvalidInput(
440 "Kraus operators do not satisfy completeness relation".to_string(),
441 ));
442 }
443 }
444 }
445
446 Ok(())
447 } else {
448 Ok(())
449 }
450 }
451
452 fn verify_choi_properties(&self) -> QuantRS2Result<()> {
454 if let Some(choi) = &self.choi {
455 let choi_dag = choi.matrix.mapv(|z| z.conj()).t().to_owned();
457 let diff = &choi.matrix - &choi_dag;
458 let max_diff = diff.iter().map(|z| z.norm()).fold(0.0, f64::max);
459
460 if max_diff > self.tolerance {
461 return Err(QuantRS2Error::InvalidInput(
462 "Choi matrix is not Hermitian".to_string(),
463 ));
464 }
465
466 Ok(())
473 } else {
474 Ok(())
475 }
476 }
477
478 fn kraus_to_choi(
480 &self,
481 operators: &[Array2<Complex<f64>>],
482 ) -> QuantRS2Result<ChoiRepresentation> {
483 let d_in = self.input_dim;
484 let d_out = self.output_dim;
485 let total_dim = d_in * d_out;
486
487 let mut choi = Array2::zeros((total_dim, total_dim));
488
489 let mut omega = Array2::zeros((d_in * d_in, 1));
491 for i in 0..d_in {
492 omega[[i * d_in + i, 0]] = Complex::new(1.0, 0.0);
493 }
494 let _omega = omega / Complex::new((d_in as f64).sqrt(), 0.0);
495
496 for k in operators {
498 let k_vec = self.vectorize_operator(k);
500 let k_vec_dag = k_vec.mapv(|z| z.conj()).t().to_owned();
501
502 choi = choi + k_vec.dot(&k_vec_dag);
504 }
505
506 Ok(ChoiRepresentation { matrix: choi })
507 }
508
509 fn choi_to_kraus(&self, choi: &Array2<Complex<f64>>) -> QuantRS2Result<KrausRepresentation> {
526 let d_in = self.input_dim;
527 let d_out = self.output_dim;
528 let total_dim = d_in * d_out;
529
530 if choi.shape() != [total_dim, total_dim] {
531 return Err(QuantRS2Error::InvalidInput(format!(
532 "Choi matrix has shape {:?}, expected [{total_dim}, {total_dim}]",
533 choi.shape()
534 )));
535 }
536
537 let decomposition = hermitian_eigen_decompose(choi)?;
541 let eigenvalues = &decomposition.eigenvalues;
542 let eigenvectors = &decomposition.eigenvectors;
543
544 let mut operators = Vec::new();
545
546 for (idx, &lambda) in eigenvalues.iter().enumerate() {
547 if lambda <= self.tolerance {
549 continue;
550 }
551 let scale = lambda.sqrt();
552
553 let mut kraus_op: Array2<Complex<f64>> = Array2::zeros((d_out, d_in));
557 for j in 0..d_in {
558 for i in 0..d_out {
559 kraus_op[[i, j]] = eigenvectors[[i + j * d_out, idx]] * scale;
560 }
561 }
562
563 operators.push(kraus_op);
564 }
565
566 if operators.is_empty() {
570 return Err(QuantRS2Error::InvalidInput(
571 "Choi matrix has no eigenvalues above tolerance; cannot build Kraus operators (zero map)"
572 .to_string(),
573 ));
574 }
575
576 Ok(KrausRepresentation { operators })
577 }
578
579 fn kraus_to_stinespring(
581 &self,
582 operators: &[Array2<Complex<f64>>],
583 ) -> QuantRS2Result<StinespringRepresentation> {
584 let num_kraus = operators.len();
585 let d_in = self.input_dim;
586 let d_out = self.output_dim;
587
588 let env_dim = num_kraus;
590
591 let total_out_dim = d_out * env_dim;
593 let mut isometry = Array2::zeros((total_out_dim, d_in));
594
595 for (i, k) in operators.iter().enumerate() {
596 let start_row = i * d_out;
598 let end_row = (i + 1) * d_out;
599
600 isometry.slice_mut(s![start_row..end_row, ..]).assign(k);
601 }
602
603 Ok(StinespringRepresentation { isometry, env_dim })
604 }
605
606 fn stinespring_to_kraus(
608 &self,
609 isometry: &Array2<Complex<f64>>,
610 env_dim: usize,
611 ) -> QuantRS2Result<KrausRepresentation> {
612 let d_out = self.output_dim;
613 let mut operators = Vec::new();
614
615 for i in 0..env_dim {
617 let start_row = i * d_out;
618 let end_row = (i + 1) * d_out;
619
620 let k = isometry.slice(s![start_row..end_row, ..]).to_owned();
621
622 let norm_sq: f64 = k.iter().map(|z| z.norm_sqr()).sum();
624 if norm_sq > self.tolerance {
625 operators.push(k);
626 }
627 }
628
629 Ok(KrausRepresentation { operators })
630 }
631
632 fn vectorize_operator(&self, op: &Array2<Complex<f64>>) -> Array2<Complex<f64>> {
634 let (rows, cols) = op.dim();
635 let mut vec = Array2::zeros((rows * cols, 1));
636
637 for j in 0..cols {
638 for i in 0..rows {
639 vec[[i + j * rows, 0]] = op[[i, j]];
640 }
641 }
642
643 vec
644 }
645}
646
647pub struct QuantumChannels;
649
650impl QuantumChannels {
651 pub fn depolarizing(p: f64) -> QuantRS2Result<QuantumChannel> {
653 if p < 0.0 || p > 1.0 {
654 return Err(QuantRS2Error::InvalidInput(
655 "Depolarizing parameter must be in [0, 1]".to_string(),
656 ));
657 }
658
659 let sqrt_1_minus_3p_4 = ((1.0 - 3.0 * p / 4.0).max(0.0)).sqrt();
660 let sqrt_p_4 = (p / 4.0).sqrt();
661
662 let operators = vec![
663 Array2::from_shape_vec(
665 (2, 2),
666 vec![
667 Complex::new(sqrt_1_minus_3p_4, 0.0),
668 Complex::new(0.0, 0.0),
669 Complex::new(0.0, 0.0),
670 Complex::new(sqrt_1_minus_3p_4, 0.0),
671 ],
672 )
673 .expect("valid 2x2 identity Kraus operator"),
674 Array2::from_shape_vec(
676 (2, 2),
677 vec![
678 Complex::new(0.0, 0.0),
679 Complex::new(sqrt_p_4, 0.0),
680 Complex::new(sqrt_p_4, 0.0),
681 Complex::new(0.0, 0.0),
682 ],
683 )
684 .expect("valid 2x2 X Kraus operator"),
685 Array2::from_shape_vec(
687 (2, 2),
688 vec![
689 Complex::new(0.0, 0.0),
690 Complex::new(0.0, -sqrt_p_4),
691 Complex::new(0.0, sqrt_p_4),
692 Complex::new(0.0, 0.0),
693 ],
694 )
695 .expect("valid 2x2 Y Kraus operator"),
696 Array2::from_shape_vec(
698 (2, 2),
699 vec![
700 Complex::new(sqrt_p_4, 0.0),
701 Complex::new(0.0, 0.0),
702 Complex::new(0.0, 0.0),
703 Complex::new(-sqrt_p_4, 0.0),
704 ],
705 )
706 .expect("valid 2x2 Z Kraus operator"),
707 ];
708
709 QuantumChannel::from_kraus(operators)
710 }
711
712 pub fn amplitude_damping(gamma: f64) -> QuantRS2Result<QuantumChannel> {
714 if gamma < 0.0 || gamma > 1.0 {
715 return Err(QuantRS2Error::InvalidInput(
716 "Damping parameter must be in [0, 1]".to_string(),
717 ));
718 }
719
720 let sqrt_gamma = gamma.sqrt();
721 let sqrt_1_minus_gamma = (1.0 - gamma).sqrt();
722
723 let operators = vec![
724 Array2::from_shape_vec(
726 (2, 2),
727 vec![
728 Complex::new(1.0, 0.0),
729 Complex::new(0.0, 0.0),
730 Complex::new(0.0, 0.0),
731 Complex::new(sqrt_1_minus_gamma, 0.0),
732 ],
733 )
734 .expect("valid 2x2 amplitude damping K0"),
735 Array2::from_shape_vec(
737 (2, 2),
738 vec![
739 Complex::new(0.0, 0.0),
740 Complex::new(sqrt_gamma, 0.0),
741 Complex::new(0.0, 0.0),
742 Complex::new(0.0, 0.0),
743 ],
744 )
745 .expect("valid 2x2 amplitude damping K1"),
746 ];
747
748 QuantumChannel::from_kraus(operators)
749 }
750
751 pub fn phase_damping(gamma: f64) -> QuantRS2Result<QuantumChannel> {
753 if gamma < 0.0 || gamma > 1.0 {
754 return Err(QuantRS2Error::InvalidInput(
755 "Damping parameter must be in [0, 1]".to_string(),
756 ));
757 }
758
759 let sqrt_1_minus_gamma = (1.0 - gamma).sqrt();
760 let sqrt_gamma = gamma.sqrt();
761
762 let operators = vec![
763 Array2::from_shape_vec(
765 (2, 2),
766 vec![
767 Complex::new(sqrt_1_minus_gamma, 0.0),
768 Complex::new(0.0, 0.0),
769 Complex::new(0.0, 0.0),
770 Complex::new(sqrt_1_minus_gamma, 0.0),
771 ],
772 )
773 .expect("valid 2x2 phase damping K0"),
774 Array2::from_shape_vec(
776 (2, 2),
777 vec![
778 Complex::new(sqrt_gamma, 0.0),
779 Complex::new(0.0, 0.0),
780 Complex::new(0.0, 0.0),
781 Complex::new(-sqrt_gamma, 0.0),
782 ],
783 )
784 .expect("valid 2x2 phase damping K1"),
785 ];
786
787 QuantumChannel::from_kraus(operators)
788 }
789
790 pub fn bit_flip(p: f64) -> QuantRS2Result<QuantumChannel> {
792 if p < 0.0 || p > 1.0 {
793 return Err(QuantRS2Error::InvalidInput(
794 "Flip probability must be in [0, 1]".to_string(),
795 ));
796 }
797
798 let sqrt_1_minus_p = (1.0 - p).sqrt();
799 let sqrt_p = p.sqrt();
800
801 let operators = vec![
802 Array2::from_shape_vec(
804 (2, 2),
805 vec![
806 Complex::new(sqrt_1_minus_p, 0.0),
807 Complex::new(0.0, 0.0),
808 Complex::new(0.0, 0.0),
809 Complex::new(sqrt_1_minus_p, 0.0),
810 ],
811 )
812 .expect("valid 2x2 bit flip K0"),
813 Array2::from_shape_vec(
815 (2, 2),
816 vec![
817 Complex::new(0.0, 0.0),
818 Complex::new(sqrt_p, 0.0),
819 Complex::new(sqrt_p, 0.0),
820 Complex::new(0.0, 0.0),
821 ],
822 )
823 .expect("valid 2x2 bit flip K1"),
824 ];
825
826 QuantumChannel::from_kraus(operators)
827 }
828
829 pub fn phase_flip(p: f64) -> QuantRS2Result<QuantumChannel> {
831 if p < 0.0 || p > 1.0 {
832 return Err(QuantRS2Error::InvalidInput(
833 "Flip probability must be in [0, 1]".to_string(),
834 ));
835 }
836
837 let sqrt_1_minus_p = (1.0 - p).sqrt();
838 let sqrt_p = p.sqrt();
839
840 let operators = vec![
841 Array2::from_shape_vec(
843 (2, 2),
844 vec![
845 Complex::new(sqrt_1_minus_p, 0.0),
846 Complex::new(0.0, 0.0),
847 Complex::new(0.0, 0.0),
848 Complex::new(sqrt_1_minus_p, 0.0),
849 ],
850 )
851 .expect("valid 2x2 phase flip K0"),
852 Array2::from_shape_vec(
854 (2, 2),
855 vec![
856 Complex::new(sqrt_p, 0.0),
857 Complex::new(0.0, 0.0),
858 Complex::new(0.0, 0.0),
859 Complex::new(-sqrt_p, 0.0),
860 ],
861 )
862 .expect("valid 2x2 phase flip K1"),
863 ];
864
865 QuantumChannel::from_kraus(operators)
866 }
867}
868
869pub struct ProcessTomography;
871
872impl ProcessTomography {
873 pub fn reconstruct_channel(
901 input_states: &[Array2<Complex<f64>>],
902 output_states: &[Array2<Complex<f64>>],
903 ) -> QuantRS2Result<QuantumChannel> {
904 if input_states.len() != output_states.len() {
905 return Err(QuantRS2Error::InvalidInput(
906 "Number of input and output states must match".to_string(),
907 ));
908 }
909 if input_states.is_empty() {
910 return Err(QuantRS2Error::InvalidInput(
911 "process tomography requires at least one (input, output) state pair".to_string(),
912 ));
913 }
914
915 let d = input_states[0].shape()[0];
917 if d == 0 {
918 return Err(QuantRS2Error::InvalidInput(
919 "density matrices must be non-empty".to_string(),
920 ));
921 }
922 for (states, label) in [(input_states, "input"), (output_states, "output")] {
923 for (k, state) in states.iter().enumerate() {
924 if state.shape() != [d, d] {
925 return Err(QuantRS2Error::InvalidInput(format!(
926 "{label} state {k} has shape {:?}, expected [{d}, {d}]",
927 state.shape()
928 )));
929 }
930 }
931 }
932
933 let total_dim = d * d;
935 let num_unknowns = total_dim * total_dim;
936
937 let num_constraints = input_states.len() * d * d;
946 let mut a_mat: Array2<Complex<f64>> = Array2::zeros((num_constraints, num_unknowns));
947 let mut b_vec: Array1<Complex<f64>> = Array1::zeros(num_constraints);
948
949 let mut row = 0usize;
950 for (rho, rho_out) in input_states.iter().zip(output_states.iter()) {
951 for i in 0..d {
952 for i_prime in 0..d {
953 b_vec[row] = rho_out[[i, i_prime]];
955
956 for j in 0..d {
958 for j_prime in 0..d {
959 let r = i + j * d;
960 let c = i_prime + j_prime * d;
961 let col = r + c * total_dim;
962 a_mat[[row, col]] = rho[[j, j_prime]];
963 }
964 }
965 row += 1;
966 }
967 }
968 }
969
970 let a_dag = a_mat.mapv(|z| z.conj()).t().to_owned();
972 let ata = a_dag.dot(&a_mat);
973 let atb = a_dag.dot(&b_vec);
974
975 let decomposition = hermitian_eigen_decompose(&ata)?;
980 let eigenvalues = &decomposition.eigenvalues;
981 let eigenvectors = &decomposition.eigenvectors;
982
983 let max_eigenvalue = eigenvalues.iter().fold(0.0_f64, |acc, &z| acc.max(z.abs()));
984 let rank_tolerance = (max_eigenvalue * 1e-9).max(1e-12);
986
987 let rank = eigenvalues
988 .iter()
989 .filter(|&&z| z.abs() > rank_tolerance)
990 .count();
991 if rank < num_unknowns {
992 return Err(QuantRS2Error::InvalidInput(
993 "process tomography requires an informationally-complete set of input states (need d^2 linearly independent inputs)".into(),
994 ));
995 }
996
997 let mut x: Array1<Complex<f64>> = Array1::zeros(num_unknowns);
1001 for m in 0..num_unknowns {
1002 let lambda = eigenvalues[m];
1003 if lambda.abs() <= rank_tolerance {
1004 continue;
1005 }
1006 let u_m = eigenvectors.column(m);
1007 let mut coeff = Complex::new(0.0, 0.0);
1009 for n in 0..num_unknowns {
1010 coeff += u_m[n].conj() * atb[n];
1011 }
1012 let coeff = coeff / Complex::new(lambda, 0.0);
1013 for n in 0..num_unknowns {
1014 x[n] += coeff * u_m[n];
1015 }
1016 }
1017
1018 let mut choi: Array2<Complex<f64>> = Array2::zeros((total_dim, total_dim));
1020 for c in 0..total_dim {
1021 for r in 0..total_dim {
1022 choi[[r, c]] = x[r + c * total_dim];
1023 }
1024 }
1025
1026 let choi_dag = choi.mapv(|z| z.conj()).t().to_owned();
1029 let choi_herm = (&choi + &choi_dag).mapv(|z| z * Complex::new(0.5, 0.0));
1030
1031 QuantumChannel::from_choi(choi_herm)
1032 }
1033
1034 pub fn generate_input_states(dim: usize) -> Vec<Array2<Complex<f64>>> {
1051 let mut states = Vec::new();
1052 if dim == 0 {
1053 return states;
1054 }
1055
1056 let inv_sqrt2 = Complex::new(1.0 / 2.0_f64.sqrt(), 0.0);
1057
1058 for i in 0..dim {
1060 let mut state = Array2::zeros((dim, dim));
1061 state[[i, i]] = Complex::new(1.0, 0.0);
1062 states.push(state);
1063 }
1064
1065 for i in 0..dim {
1068 for j in (i + 1)..dim {
1069 for &phase in &[Complex::new(1.0, 0.0), Complex::new(0.0, 1.0)] {
1070 let mut psi: Array1<Complex<f64>> = Array1::zeros(dim);
1072 psi[i] = inv_sqrt2;
1073 psi[j] = phase * inv_sqrt2;
1074
1075 let mut state: Array2<Complex<f64>> = Array2::zeros((dim, dim));
1077 for r in 0..dim {
1078 for c in 0..dim {
1079 state[[r, c]] = psi[r] * psi[c].conj();
1080 }
1081 }
1082 states.push(state);
1083 }
1084 }
1085 }
1086
1087 states
1088 }
1089}
1090
1091#[cfg(test)]
1092mod tests {
1093 use super::*;
1094 use scirs2_core::Complex;
1095
1096 #[test]
1097 fn test_depolarizing_channel() {
1098 let channel =
1099 QuantumChannels::depolarizing(0.1).expect("Failed to create depolarizing channel");
1100
1101 assert_eq!(channel.input_dim, 2);
1102 assert_eq!(channel.output_dim, 2);
1103 assert!(channel.kraus.is_some());
1104 assert_eq!(
1105 channel
1106 .kraus
1107 .as_ref()
1108 .expect("Kraus representation missing")
1109 .operators
1110 .len(),
1111 4
1112 );
1113 }
1114
1115 #[test]
1116 fn test_amplitude_damping() {
1117 let channel = QuantumChannels::amplitude_damping(0.3)
1118 .expect("Failed to create amplitude damping channel");
1119
1120 assert!(channel.kraus.is_some());
1121 assert_eq!(
1122 channel
1123 .kraus
1124 .as_ref()
1125 .expect("Kraus representation missing")
1126 .operators
1127 .len(),
1128 2
1129 );
1130
1131 let mut rho = Array2::zeros((2, 2));
1133 rho[[1, 1]] = Complex::new(1.0, 0.0);
1134
1135 let mut ch = channel;
1136 let output = ch.apply(&rho).expect("Failed to apply channel");
1137
1138 assert!(output[[1, 1]].re < 1.0);
1140 assert!(output[[0, 0]].re > 0.0);
1141 }
1142
1143 #[test]
1144 fn test_kraus_to_choi() {
1145 let mut channel =
1146 QuantumChannels::bit_flip(0.2).expect("Failed to create bit flip channel");
1147 let choi = channel.to_choi().expect("Failed to convert to Choi");
1148
1149 assert_eq!(choi.matrix.shape(), [4, 4]);
1150
1151 let choi_dag = choi.matrix.mapv(|z| z.conj()).t().to_owned();
1153 let diff = &choi.matrix - &choi_dag;
1154 let max_diff = diff.iter().map(|z| z.norm()).fold(0.0, f64::max);
1155 assert!(max_diff < 1e-10);
1156 }
1157
1158 #[test]
1159 fn test_channel_composition() {
1160 let mut ch1 =
1162 QuantumChannels::phase_flip(0.1).expect("Failed to create phase flip channel");
1163 let mut ch2 = QuantumChannels::bit_flip(0.2).expect("Failed to create bit flip channel");
1164
1165 let mut rho = Array2::zeros((2, 2));
1167 rho[[0, 0]] = Complex::new(0.5, 0.0);
1168 rho[[0, 1]] = Complex::new(0.5, 0.0);
1169 rho[[1, 0]] = Complex::new(0.5, 0.0);
1170 rho[[1, 1]] = Complex::new(0.5, 0.0);
1171
1172 let intermediate = ch1.apply(&rho).expect("Failed to apply phase flip channel");
1173 let final_state = ch2
1174 .apply(&intermediate)
1175 .expect("Failed to apply bit flip channel");
1176
1177 let trace = final_state[[0, 0]] + final_state[[1, 1]];
1179 assert!((trace.re - 1.0).abs() < 1e-10);
1180 assert!(trace.im.abs() < 1e-10);
1181 }
1182
1183 #[test]
1184 fn test_unitary_channel() {
1185 let h = Array2::from_shape_vec(
1187 (2, 2),
1188 vec![
1189 Complex::new(1.0, 0.0),
1190 Complex::new(1.0, 0.0),
1191 Complex::new(1.0, 0.0),
1192 Complex::new(-1.0, 0.0),
1193 ],
1194 )
1195 .expect("valid 2x2 Hadamard matrix")
1196 / Complex::new(2.0_f64.sqrt(), 0.0);
1197
1198 let mut channel =
1199 QuantumChannel::from_kraus(vec![h]).expect("Failed to create unitary channel");
1200
1201 assert!(channel.is_unitary().expect("Failed to check unitarity"));
1202 }
1203
1204 #[test]
1205 fn test_stinespring_conversion() {
1206 let mut channel = QuantumChannels::amplitude_damping(0.5)
1207 .expect("Failed to create amplitude damping channel");
1208
1209 let stinespring = channel
1211 .to_stinespring()
1212 .expect("Failed to convert to Stinespring");
1213
1214 assert_eq!(stinespring.env_dim, 2);
1215 assert_eq!(stinespring.isometry.shape(), [4, 2]);
1216
1217 let kraus_decomposer =
1219 QuantumChannel::from_kraus(vec![Array2::eye(2).mapv(|x| Complex::new(x, 0.0))])
1220 .expect("Failed to create identity channel");
1221 let kraus = kraus_decomposer
1222 .stinespring_to_kraus(&stinespring.isometry, stinespring.env_dim)
1223 .expect("Failed to convert back to Kraus");
1224 assert_eq!(kraus.operators.len(), 2);
1225 }
1226
1227 fn apply_kraus(
1229 operators: &[Array2<Complex<f64>>],
1230 rho: &Array2<Complex<f64>>,
1231 ) -> Array2<Complex<f64>> {
1232 let d_out = operators[0].shape()[0];
1233 let mut result: Array2<Complex<f64>> = Array2::zeros((d_out, d_out));
1234 for k in operators {
1235 let k_dag = k.mapv(|z| z.conj()).t().to_owned();
1236 result = result + k.dot(rho).dot(&k_dag);
1237 }
1238 result
1239 }
1240
1241 fn qubit_test_states() -> Vec<Array2<Complex<f64>>> {
1243 let mut states = Vec::new();
1244
1245 let mut s0 = Array2::zeros((2, 2));
1247 s0[[0, 0]] = Complex::new(1.0, 0.0);
1248 states.push(s0);
1249
1250 let mut s1 = Array2::zeros((2, 2));
1252 s1[[1, 1]] = Complex::new(1.0, 0.0);
1253 states.push(s1);
1254
1255 let mut s_plus = Array2::zeros((2, 2));
1257 for idx in [[0, 0], [0, 1], [1, 0], [1, 1]] {
1258 s_plus[idx] = Complex::new(0.5, 0.0);
1259 }
1260 states.push(s_plus);
1261
1262 let mut s_plus_i = Array2::zeros((2, 2));
1264 s_plus_i[[0, 0]] = Complex::new(0.5, 0.0);
1265 s_plus_i[[0, 1]] = Complex::new(0.0, -0.5);
1266 s_plus_i[[1, 0]] = Complex::new(0.0, 0.5);
1267 s_plus_i[[1, 1]] = Complex::new(0.5, 0.0);
1268 states.push(s_plus_i);
1269
1270 states
1271 }
1272
1273 #[test]
1274 fn test_choi_to_kraus_roundtrip_depolarizing() {
1275 let mut channel =
1280 QuantumChannels::depolarizing(0.3).expect("failed to create depolarizing channel");
1281 let original_ops = channel
1282 .to_kraus()
1283 .expect("failed to get original Kraus")
1284 .operators
1285 .clone();
1286
1287 let choi = channel
1290 .to_choi()
1291 .expect("failed to convert to Choi")
1292 .clone();
1293 let mut from_choi = QuantumChannel::from_choi(choi.matrix.clone())
1294 .expect("failed to build channel from Choi");
1295 let recovered_ops = from_choi
1296 .to_kraus()
1297 .expect("failed to recover Kraus from Choi")
1298 .operators
1299 .clone();
1300
1301 for rho in qubit_test_states() {
1302 let expected = apply_kraus(&original_ops, &rho);
1303 let actual = apply_kraus(&recovered_ops, &rho);
1304 let max_diff = (&expected - &actual)
1305 .iter()
1306 .map(|z| z.norm())
1307 .fold(0.0_f64, f64::max);
1308 assert!(
1309 max_diff < 1e-9,
1310 "depolarizing round-trip mismatch: {max_diff}"
1311 );
1312 }
1313 }
1314
1315 #[test]
1316 fn test_choi_to_kraus_roundtrip_amplitude_damping() {
1317 let mut channel = QuantumChannels::amplitude_damping(0.4)
1318 .expect("failed to create amplitude damping channel");
1319 let original_ops = channel
1320 .to_kraus()
1321 .expect("failed to get original Kraus")
1322 .operators
1323 .clone();
1324
1325 let choi = channel
1326 .to_choi()
1327 .expect("failed to convert to Choi")
1328 .clone();
1329 let mut from_choi = QuantumChannel::from_choi(choi.matrix.clone())
1330 .expect("failed to build channel from Choi");
1331 let recovered_ops = from_choi
1332 .to_kraus()
1333 .expect("failed to recover Kraus from Choi")
1334 .operators
1335 .clone();
1336
1337 for rho in qubit_test_states() {
1338 let expected = apply_kraus(&original_ops, &rho);
1339 let actual = apply_kraus(&recovered_ops, &rho);
1340 let max_diff = (&expected - &actual)
1341 .iter()
1342 .map(|z| z.norm())
1343 .fold(0.0_f64, f64::max);
1344 assert!(
1345 max_diff < 1e-9,
1346 "amplitude damping round-trip mismatch: {max_diff}"
1347 );
1348 }
1349 }
1350
1351 #[test]
1352 fn test_choi_to_kraus_trace_preserving() {
1353 let mut channel =
1356 QuantumChannels::depolarizing(0.25).expect("failed to create depolarizing channel");
1357 let choi = channel
1358 .to_choi()
1359 .expect("failed to convert to Choi")
1360 .clone();
1361 let mut from_choi = QuantumChannel::from_choi(choi.matrix.clone())
1362 .expect("failed to build channel from Choi");
1363 let recovered = from_choi
1364 .to_kraus()
1365 .expect("failed to recover Kraus from Choi")
1366 .operators
1367 .clone();
1368
1369 let d_in = from_choi.input_dim;
1370 let mut sum: Array2<Complex<f64>> = Array2::zeros((d_in, d_in));
1371 for k in &recovered {
1372 let k_dag = k.mapv(|z| z.conj()).t().to_owned();
1373 sum = sum + k_dag.dot(k);
1374 }
1375
1376 for i in 0..d_in {
1377 for j in 0..d_in {
1378 let expected = if i == j {
1379 Complex::new(1.0, 0.0)
1380 } else {
1381 Complex::new(0.0, 0.0)
1382 };
1383 let diff = (sum[[i, j]] - expected).norm();
1384 assert!(diff < 1e-9, "completeness violated at ({i},{j}): {diff}");
1385 }
1386 }
1387 }
1388
1389 #[test]
1390 fn test_generate_input_states_informationally_complete() {
1391 for d in 2..=3 {
1393 let states = ProcessTomography::generate_input_states(d);
1394 assert_eq!(
1395 states.len(),
1396 d * d,
1397 "expected d^2 informationally-complete states for d={d}"
1398 );
1399 for state in &states {
1401 assert_eq!(state.shape(), [d, d]);
1402 let trace: Complex<f64> = (0..d).map(|i| state[[i, i]]).sum();
1403 assert!((trace.re - 1.0).abs() < 1e-12);
1404 assert!(trace.im.abs() < 1e-12);
1405 }
1406 }
1407 }
1408
1409 #[test]
1410 fn test_reconstruct_channel_amplitude_damping() {
1411 let gamma = 0.35;
1416 let inputs = ProcessTomography::generate_input_states(2);
1417
1418 let mut reference =
1419 QuantumChannels::amplitude_damping(gamma).expect("failed to create reference channel");
1420 let mut outputs = Vec::with_capacity(inputs.len());
1421 for rho in &inputs {
1422 outputs.push(reference.apply(rho).expect("failed to apply reference"));
1423 }
1424
1425 let mut reconstructed = ProcessTomography::reconstruct_channel(&inputs, &outputs)
1426 .expect("reconstruction should succeed for an informationally-complete set");
1427
1428 let mut fresh = Array2::zeros((2, 2));
1430 fresh[[0, 0]] = Complex::new(0.5, 0.0);
1431 fresh[[0, 1]] = Complex::new(0.0, -0.5);
1432 fresh[[1, 0]] = Complex::new(0.0, 0.5);
1433 fresh[[1, 1]] = Complex::new(0.5, 0.0);
1434
1435 let expected = reference.apply(&fresh).expect("reference apply failed");
1436 let actual = reconstructed
1437 .apply(&fresh)
1438 .expect("reconstructed apply failed");
1439 let max_diff = (&expected - &actual)
1440 .iter()
1441 .map(|z| z.norm())
1442 .fold(0.0_f64, f64::max);
1443 assert!(max_diff < 1e-8, "reconstruction mismatch: {max_diff}");
1444
1445 let mut identity =
1447 QuantumChannel::from_kraus(vec![Array2::eye(2).mapv(|x| Complex::new(x, 0.0))])
1448 .expect("failed to create identity channel");
1449 let id_out = identity.apply(&fresh).expect("identity apply failed");
1450 let id_diff = (&id_out - &actual)
1451 .iter()
1452 .map(|z| z.norm())
1453 .fold(0.0_f64, f64::max);
1454 assert!(
1455 id_diff > 1e-3,
1456 "reconstructed channel is indistinguishable from identity (diff={id_diff})"
1457 );
1458 }
1459
1460 #[test]
1461 fn test_reconstruct_channel_underdetermined_errors() {
1462 let mut reference =
1465 QuantumChannels::bit_flip(0.2).expect("failed to create bit flip channel");
1466
1467 let mut inputs = Vec::new();
1469 for i in 0..2 {
1470 let mut state = Array2::zeros((2, 2));
1471 state[[i, i]] = Complex::new(1.0, 0.0);
1472 inputs.push(state);
1473 }
1474 let mut outputs = Vec::new();
1475 for rho in &inputs {
1476 outputs.push(reference.apply(rho).expect("apply failed"));
1477 }
1478
1479 let result = ProcessTomography::reconstruct_channel(&inputs, &outputs);
1480 assert!(
1481 result.is_err(),
1482 "underdetermined tomography must error, not fabricate a channel"
1483 );
1484 }
1485}