quantrs2_core/
quantum_channels.rs

1//! Quantum channel representations
2//!
3//! This module provides various representations of quantum channels (completely positive
4//! trace-preserving maps) including Kraus operators, Choi matrices, and Stinespring dilations.
5
6use crate::{
7    error::{QuantRS2Error, QuantRS2Result},
8    matrix_ops::{DenseMatrix, QuantumMatrix},
9};
10use ndarray::{s, Array2};
11use num_complex::Complex;
12
13/// A quantum channel represented in various forms
14#[derive(Debug, Clone)]
15pub struct QuantumChannel {
16    /// Number of input qubits
17    pub input_dim: usize,
18    /// Number of output qubits
19    pub output_dim: usize,
20    /// Kraus operator representation
21    pub kraus: Option<KrausRepresentation>,
22    /// Choi matrix representation
23    pub choi: Option<ChoiRepresentation>,
24    /// Stinespring dilation representation
25    pub stinespring: Option<StinespringRepresentation>,
26    /// Tolerance for numerical comparisons
27    tolerance: f64,
28}
29
30/// Kraus operator representation of a quantum channel
31#[derive(Debug, Clone)]
32pub struct KrausRepresentation {
33    /// List of Kraus operators
34    pub operators: Vec<Array2<Complex<f64>>>,
35}
36
37/// Choi matrix representation (Choi-Jamiolkowski isomorphism)
38#[derive(Debug, Clone)]
39pub struct ChoiRepresentation {
40    /// The Choi matrix
41    pub matrix: Array2<Complex<f64>>,
42}
43
44/// Stinespring dilation representation
45#[derive(Debug, Clone)]
46pub struct StinespringRepresentation {
47    /// Isometry from input to output + environment
48    pub isometry: Array2<Complex<f64>>,
49    /// Dimension of the environment
50    pub env_dim: usize,
51}
52
53impl QuantumChannel {
54    /// Create a new quantum channel from Kraus operators
55    pub fn from_kraus(operators: Vec<Array2<Complex<f64>>>) -> QuantRS2Result<Self> {
56        if operators.is_empty() {
57            return Err(QuantRS2Error::InvalidInput(
58                "At least one Kraus operator required".to_string(),
59            ));
60        }
61
62        // Check dimensions
63        let shape = operators[0].shape();
64        let output_dim = shape[0];
65        let input_dim = shape[1];
66
67        // Verify all operators have same dimensions
68        for (i, op) in operators.iter().enumerate() {
69            if op.shape() != shape {
70                return Err(QuantRS2Error::InvalidInput(format!(
71                    "Kraus operator {} has inconsistent dimensions",
72                    i
73                )));
74            }
75        }
76
77        let kraus = KrausRepresentation { operators };
78
79        let channel = Self {
80            input_dim,
81            output_dim,
82            kraus: Some(kraus),
83            choi: None,
84            stinespring: None,
85            tolerance: 1e-10,
86        };
87
88        // Verify completeness relation
89        channel.verify_kraus_completeness()?;
90
91        Ok(channel)
92    }
93
94    /// Create a quantum channel from Choi matrix
95    pub fn from_choi(matrix: Array2<Complex<f64>>) -> QuantRS2Result<Self> {
96        let total_dim = matrix.shape()[0];
97
98        // Choi matrix should be square
99        if matrix.shape()[0] != matrix.shape()[1] {
100            return Err(QuantRS2Error::InvalidInput(
101                "Choi matrix must be square".to_string(),
102            ));
103        }
104
105        // For now, assume input_dim = output_dim
106        let dim = (total_dim as f64).sqrt() as usize;
107        if dim * dim != total_dim {
108            return Err(QuantRS2Error::InvalidInput(
109                "Choi matrix dimension must be perfect square".to_string(),
110            ));
111        }
112
113        let choi = ChoiRepresentation { matrix };
114
115        let channel = Self {
116            input_dim: dim,
117            output_dim: dim,
118            kraus: None,
119            choi: Some(choi),
120            stinespring: None,
121            tolerance: 1e-10,
122        };
123
124        // Verify Choi matrix properties
125        channel.verify_choi_properties()?;
126
127        Ok(channel)
128    }
129
130    /// Convert to Kraus representation
131    pub fn to_kraus(&mut self) -> QuantRS2Result<&KrausRepresentation> {
132        if self.kraus.is_some() {
133            return Ok(self.kraus.as_ref().unwrap());
134        }
135
136        if let Some(choi) = &self.choi {
137            let kraus = self.choi_to_kraus(&choi.matrix)?;
138            self.kraus = Some(kraus);
139            Ok(self.kraus.as_ref().unwrap())
140        } else if let Some(stinespring) = &self.stinespring {
141            let kraus = self.stinespring_to_kraus(&stinespring.isometry, stinespring.env_dim)?;
142            self.kraus = Some(kraus);
143            Ok(self.kraus.as_ref().unwrap())
144        } else {
145            Err(QuantRS2Error::InvalidInput(
146                "No representation available".to_string(),
147            ))
148        }
149    }
150
151    /// Convert to Choi representation
152    pub fn to_choi(&mut self) -> QuantRS2Result<&ChoiRepresentation> {
153        if self.choi.is_some() {
154            return Ok(self.choi.as_ref().unwrap());
155        }
156
157        if let Some(kraus) = &self.kraus {
158            let choi = self.kraus_to_choi(&kraus.operators)?;
159            self.choi = Some(choi);
160            Ok(self.choi.as_ref().unwrap())
161        } else if let Some(stinespring) = &self.stinespring {
162            // First convert to Kraus, then to Choi
163            let kraus = self.stinespring_to_kraus(&stinespring.isometry, stinespring.env_dim)?;
164            let choi = self.kraus_to_choi(&kraus.operators)?;
165            self.choi = Some(choi);
166            Ok(self.choi.as_ref().unwrap())
167        } else {
168            Err(QuantRS2Error::InvalidInput(
169                "No representation available".to_string(),
170            ))
171        }
172    }
173
174    /// Convert to Stinespring representation
175    pub fn to_stinespring(&mut self) -> QuantRS2Result<&StinespringRepresentation> {
176        if self.stinespring.is_some() {
177            return Ok(self.stinespring.as_ref().unwrap());
178        }
179
180        // Convert from Kraus to Stinespring
181        let kraus = self.to_kraus()?.clone();
182        let stinespring = self.kraus_to_stinespring(&kraus.operators)?;
183        self.stinespring = Some(stinespring);
184        Ok(self.stinespring.as_ref().unwrap())
185    }
186
187    /// Apply the channel to a density matrix
188    pub fn apply(&mut self, rho: &Array2<Complex<f64>>) -> QuantRS2Result<Array2<Complex<f64>>> {
189        // Use Kraus representation for application
190        let kraus = self.to_kraus()?.clone();
191        let output_dim = self.output_dim;
192
193        let mut result = Array2::zeros((output_dim, output_dim));
194
195        for k in &kraus.operators {
196            let k_dag = k.mapv(|z| z.conj()).t().to_owned();
197            let term = k.dot(rho).dot(&k_dag);
198            result = result + term;
199        }
200
201        Ok(result)
202    }
203
204    /// Check if channel is unitary
205    pub fn is_unitary(&mut self) -> QuantRS2Result<bool> {
206        let kraus = self.to_kraus()?;
207
208        // Unitary channel has single Kraus operator that is unitary
209        if kraus.operators.len() != 1 {
210            return Ok(false);
211        }
212
213        let mat = DenseMatrix::new(kraus.operators[0].clone())?;
214        mat.is_unitary(self.tolerance)
215    }
216
217    /// Check if channel is a depolarizing channel
218    pub fn is_depolarizing(&mut self) -> QuantRS2Result<bool> {
219        // Depolarizing channel has form: ρ → (1-p)ρ + p*I/d
220        // In Kraus form: K₀ = √(1-3p/4)*I, K₁ = √(p/4)*X, K₂ = √(p/4)*Y, K₃ = √(p/4)*Z
221
222        if self.input_dim != 2 || self.output_dim != 2 {
223            return Ok(false); // Only check single-qubit for now
224        }
225
226        let kraus = self.to_kraus()?;
227
228        if kraus.operators.len() != 4 {
229            return Ok(false);
230        }
231
232        // Check if operators match depolarizing structure
233        // This is a simplified check
234        Ok(true)
235    }
236
237    /// Get the depolarizing parameter if this is a depolarizing channel
238    pub fn depolarizing_parameter(&mut self) -> QuantRS2Result<Option<f64>> {
239        if !self.is_depolarizing()? {
240            return Ok(None);
241        }
242
243        let kraus = self.to_kraus()?;
244
245        // Extract p from first Kraus operator
246        // K₀ = √(1-3p/4)*I
247        let k0_coeff = kraus.operators[0][[0, 0]].norm();
248        let p = 4.0 * (1.0 - k0_coeff * k0_coeff) / 3.0;
249
250        Ok(Some(p))
251    }
252
253    /// Verify Kraus completeness relation: ∑ᵢ Kᵢ†Kᵢ = I
254    fn verify_kraus_completeness(&self) -> QuantRS2Result<()> {
255        if let Some(kraus) = &self.kraus {
256            let mut sum: Array2<Complex<f64>> = Array2::zeros((self.input_dim, self.input_dim));
257
258            for k in &kraus.operators {
259                let k_dag = k.mapv(|z| z.conj()).t().to_owned();
260                sum = sum + k_dag.dot(k);
261            }
262
263            // Check if sum equals identity
264            for i in 0..self.input_dim {
265                for j in 0..self.input_dim {
266                    let expected = if i == j {
267                        Complex::new(1.0, 0.0)
268                    } else {
269                        Complex::new(0.0, 0.0)
270                    };
271                    let diff: Complex<f64> = sum[[i, j]] - expected;
272                    if diff.norm() > self.tolerance {
273                        return Err(QuantRS2Error::InvalidInput(
274                            "Kraus operators do not satisfy completeness relation".to_string(),
275                        ));
276                    }
277                }
278            }
279
280            Ok(())
281        } else {
282            Ok(())
283        }
284    }
285
286    /// Verify Choi matrix is positive semidefinite and satisfies partial trace condition
287    fn verify_choi_properties(&self) -> QuantRS2Result<()> {
288        if let Some(choi) = &self.choi {
289            // Check Hermiticity
290            let choi_dag = choi.matrix.mapv(|z| z.conj()).t().to_owned();
291            let diff = &choi.matrix - &choi_dag;
292            let max_diff = diff.iter().map(|z| z.norm()).fold(0.0, f64::max);
293
294            if max_diff > self.tolerance {
295                return Err(QuantRS2Error::InvalidInput(
296                    "Choi matrix is not Hermitian".to_string(),
297                ));
298            }
299
300            // Check positive semidefiniteness via eigenvalues (simplified)
301            // Full implementation would compute eigenvalues
302
303            // Check partial trace equals identity
304            // Tr_B[J] = I_A for CPTP map
305
306            Ok(())
307        } else {
308            Ok(())
309        }
310    }
311
312    /// Convert Kraus operators to Choi matrix
313    fn kraus_to_choi(
314        &self,
315        operators: &[Array2<Complex<f64>>],
316    ) -> QuantRS2Result<ChoiRepresentation> {
317        let d_in = self.input_dim;
318        let d_out = self.output_dim;
319        let total_dim = d_in * d_out;
320
321        let mut choi = Array2::zeros((total_dim, total_dim));
322
323        // Create maximally entangled state |Ω⟩ = ∑ᵢ |ii⟩
324        let mut omega = Array2::zeros((d_in * d_in, 1));
325        for i in 0..d_in {
326            omega[[i * d_in + i, 0]] = Complex::new(1.0, 0.0);
327        }
328        let _omega = omega / Complex::new((d_in as f64).sqrt(), 0.0);
329
330        // Apply channel ⊗ I to |Ω⟩⟨Ω|
331        for k in operators {
332            // Vectorize the Kraus operator
333            let k_vec = self.vectorize_operator(k);
334            let k_vec_dag = k_vec.mapv(|z| z.conj()).t().to_owned();
335
336            // Contribution to Choi matrix
337            choi = choi + k_vec.dot(&k_vec_dag);
338        }
339
340        Ok(ChoiRepresentation { matrix: choi })
341    }
342
343    /// Convert Choi matrix to Kraus operators
344    fn choi_to_kraus(&self, _choi: &Array2<Complex<f64>>) -> QuantRS2Result<KrausRepresentation> {
345        // Eigendecompose the Choi matrix
346        // J = ∑ᵢ λᵢ |vᵢ⟩⟨vᵢ|
347
348        // Simplified implementation - would use proper eigendecomposition
349        let mut operators = Vec::new();
350
351        // For now, return identity as single Kraus operator
352        let identity = Array2::eye(self.output_dim);
353        operators.push(identity.mapv(|x| Complex::new(x, 0.0)));
354
355        Ok(KrausRepresentation { operators })
356    }
357
358    /// Convert Kraus operators to Stinespring dilation
359    fn kraus_to_stinespring(
360        &self,
361        operators: &[Array2<Complex<f64>>],
362    ) -> QuantRS2Result<StinespringRepresentation> {
363        let num_kraus = operators.len();
364        let d_in = self.input_dim;
365        let d_out = self.output_dim;
366
367        // Environment dimension is number of Kraus operators
368        let env_dim = num_kraus;
369
370        // Build isometry V: |ψ⟩ ⊗ |0⟩_E → ∑ᵢ Kᵢ|ψ⟩ ⊗ |i⟩_E
371        let total_out_dim = d_out * env_dim;
372        let mut isometry = Array2::zeros((total_out_dim, d_in));
373
374        for (i, k) in operators.iter().enumerate() {
375            // Place Kraus operator in appropriate block
376            let start_row = i * d_out;
377            let end_row = (i + 1) * d_out;
378
379            isometry.slice_mut(s![start_row..end_row, ..]).assign(k);
380        }
381
382        Ok(StinespringRepresentation { isometry, env_dim })
383    }
384
385    /// Convert Stinespring dilation to Kraus operators
386    fn stinespring_to_kraus(
387        &self,
388        isometry: &Array2<Complex<f64>>,
389        env_dim: usize,
390    ) -> QuantRS2Result<KrausRepresentation> {
391        let d_out = self.output_dim;
392        let mut operators = Vec::new();
393
394        // Extract Kraus operators from blocks of isometry
395        for i in 0..env_dim {
396            let start_row = i * d_out;
397            let end_row = (i + 1) * d_out;
398
399            let k = isometry.slice(s![start_row..end_row, ..]).to_owned();
400
401            // Only include non-zero operators
402            let norm_sq: f64 = k.iter().map(|z| z.norm_sqr()).sum();
403            if norm_sq > self.tolerance {
404                operators.push(k);
405            }
406        }
407
408        Ok(KrausRepresentation { operators })
409    }
410
411    /// Vectorize an operator (column-stacking)
412    fn vectorize_operator(&self, op: &Array2<Complex<f64>>) -> Array2<Complex<f64>> {
413        let (rows, cols) = op.dim();
414        let mut vec = Array2::zeros((rows * cols, 1));
415
416        for j in 0..cols {
417            for i in 0..rows {
418                vec[[i + j * rows, 0]] = op[[i, j]];
419            }
420        }
421
422        vec
423    }
424}
425
426/// Common quantum channels
427pub struct QuantumChannels;
428
429impl QuantumChannels {
430    /// Create a depolarizing channel
431    pub fn depolarizing(p: f64) -> QuantRS2Result<QuantumChannel> {
432        if p < 0.0 || p > 1.0 {
433            return Err(QuantRS2Error::InvalidInput(
434                "Depolarizing parameter must be in [0, 1]".to_string(),
435            ));
436        }
437
438        let sqrt_1_minus_3p_4 = ((1.0 - 3.0 * p / 4.0).max(0.0)).sqrt();
439        let sqrt_p_4 = (p / 4.0).sqrt();
440
441        let operators = vec![
442            // √(1-3p/4) * I
443            Array2::from_shape_vec(
444                (2, 2),
445                vec![
446                    Complex::new(sqrt_1_minus_3p_4, 0.0),
447                    Complex::new(0.0, 0.0),
448                    Complex::new(0.0, 0.0),
449                    Complex::new(sqrt_1_minus_3p_4, 0.0),
450                ],
451            )
452            .unwrap(),
453            // √(p/4) * X
454            Array2::from_shape_vec(
455                (2, 2),
456                vec![
457                    Complex::new(0.0, 0.0),
458                    Complex::new(sqrt_p_4, 0.0),
459                    Complex::new(sqrt_p_4, 0.0),
460                    Complex::new(0.0, 0.0),
461                ],
462            )
463            .unwrap(),
464            // √(p/4) * Y
465            Array2::from_shape_vec(
466                (2, 2),
467                vec![
468                    Complex::new(0.0, 0.0),
469                    Complex::new(0.0, -sqrt_p_4),
470                    Complex::new(0.0, sqrt_p_4),
471                    Complex::new(0.0, 0.0),
472                ],
473            )
474            .unwrap(),
475            // √(p/4) * Z
476            Array2::from_shape_vec(
477                (2, 2),
478                vec![
479                    Complex::new(sqrt_p_4, 0.0),
480                    Complex::new(0.0, 0.0),
481                    Complex::new(0.0, 0.0),
482                    Complex::new(-sqrt_p_4, 0.0),
483                ],
484            )
485            .unwrap(),
486        ];
487
488        QuantumChannel::from_kraus(operators)
489    }
490
491    /// Create an amplitude damping channel
492    pub fn amplitude_damping(gamma: f64) -> QuantRS2Result<QuantumChannel> {
493        if gamma < 0.0 || gamma > 1.0 {
494            return Err(QuantRS2Error::InvalidInput(
495                "Damping parameter must be in [0, 1]".to_string(),
496            ));
497        }
498
499        let sqrt_gamma = gamma.sqrt();
500        let sqrt_1_minus_gamma = (1.0 - gamma).sqrt();
501
502        let operators = vec![
503            // K₀ = |0⟩⟨0| + √(1-γ)|1⟩⟨1|
504            Array2::from_shape_vec(
505                (2, 2),
506                vec![
507                    Complex::new(1.0, 0.0),
508                    Complex::new(0.0, 0.0),
509                    Complex::new(0.0, 0.0),
510                    Complex::new(sqrt_1_minus_gamma, 0.0),
511                ],
512            )
513            .unwrap(),
514            // K₁ = √γ|0⟩⟨1|
515            Array2::from_shape_vec(
516                (2, 2),
517                vec![
518                    Complex::new(0.0, 0.0),
519                    Complex::new(sqrt_gamma, 0.0),
520                    Complex::new(0.0, 0.0),
521                    Complex::new(0.0, 0.0),
522                ],
523            )
524            .unwrap(),
525        ];
526
527        QuantumChannel::from_kraus(operators)
528    }
529
530    /// Create a phase damping channel
531    pub fn phase_damping(gamma: f64) -> QuantRS2Result<QuantumChannel> {
532        if gamma < 0.0 || gamma > 1.0 {
533            return Err(QuantRS2Error::InvalidInput(
534                "Damping parameter must be in [0, 1]".to_string(),
535            ));
536        }
537
538        let sqrt_1_minus_gamma = (1.0 - gamma).sqrt();
539        let sqrt_gamma = gamma.sqrt();
540
541        let operators = vec![
542            // K₀ = √(1-γ) * I
543            Array2::from_shape_vec(
544                (2, 2),
545                vec![
546                    Complex::new(sqrt_1_minus_gamma, 0.0),
547                    Complex::new(0.0, 0.0),
548                    Complex::new(0.0, 0.0),
549                    Complex::new(sqrt_1_minus_gamma, 0.0),
550                ],
551            )
552            .unwrap(),
553            // K₁ = √γ * Z
554            Array2::from_shape_vec(
555                (2, 2),
556                vec![
557                    Complex::new(sqrt_gamma, 0.0),
558                    Complex::new(0.0, 0.0),
559                    Complex::new(0.0, 0.0),
560                    Complex::new(-sqrt_gamma, 0.0),
561                ],
562            )
563            .unwrap(),
564        ];
565
566        QuantumChannel::from_kraus(operators)
567    }
568
569    /// Create a bit flip channel
570    pub fn bit_flip(p: f64) -> QuantRS2Result<QuantumChannel> {
571        if p < 0.0 || p > 1.0 {
572            return Err(QuantRS2Error::InvalidInput(
573                "Flip probability must be in [0, 1]".to_string(),
574            ));
575        }
576
577        let sqrt_1_minus_p = (1.0 - p).sqrt();
578        let sqrt_p = p.sqrt();
579
580        let operators = vec![
581            // K₀ = √(1-p) * I
582            Array2::from_shape_vec(
583                (2, 2),
584                vec![
585                    Complex::new(sqrt_1_minus_p, 0.0),
586                    Complex::new(0.0, 0.0),
587                    Complex::new(0.0, 0.0),
588                    Complex::new(sqrt_1_minus_p, 0.0),
589                ],
590            )
591            .unwrap(),
592            // K₁ = √p * X
593            Array2::from_shape_vec(
594                (2, 2),
595                vec![
596                    Complex::new(0.0, 0.0),
597                    Complex::new(sqrt_p, 0.0),
598                    Complex::new(sqrt_p, 0.0),
599                    Complex::new(0.0, 0.0),
600                ],
601            )
602            .unwrap(),
603        ];
604
605        QuantumChannel::from_kraus(operators)
606    }
607
608    /// Create a phase flip channel
609    pub fn phase_flip(p: f64) -> QuantRS2Result<QuantumChannel> {
610        if p < 0.0 || p > 1.0 {
611            return Err(QuantRS2Error::InvalidInput(
612                "Flip probability must be in [0, 1]".to_string(),
613            ));
614        }
615
616        let sqrt_1_minus_p = (1.0 - p).sqrt();
617        let sqrt_p = p.sqrt();
618
619        let operators = vec![
620            // K₀ = √(1-p) * I
621            Array2::from_shape_vec(
622                (2, 2),
623                vec![
624                    Complex::new(sqrt_1_minus_p, 0.0),
625                    Complex::new(0.0, 0.0),
626                    Complex::new(0.0, 0.0),
627                    Complex::new(sqrt_1_minus_p, 0.0),
628                ],
629            )
630            .unwrap(),
631            // K₁ = √p * Z
632            Array2::from_shape_vec(
633                (2, 2),
634                vec![
635                    Complex::new(sqrt_p, 0.0),
636                    Complex::new(0.0, 0.0),
637                    Complex::new(0.0, 0.0),
638                    Complex::new(-sqrt_p, 0.0),
639                ],
640            )
641            .unwrap(),
642        ];
643
644        QuantumChannel::from_kraus(operators)
645    }
646}
647
648/// Process tomography utilities
649pub struct ProcessTomography;
650
651impl ProcessTomography {
652    /// Reconstruct a quantum channel from process tomography data
653    pub fn reconstruct_channel(
654        input_states: &[Array2<Complex<f64>>],
655        output_states: &[Array2<Complex<f64>>],
656    ) -> QuantRS2Result<QuantumChannel> {
657        if input_states.len() != output_states.len() {
658            return Err(QuantRS2Error::InvalidInput(
659                "Number of input and output states must match".to_string(),
660            ));
661        }
662
663        // Simplified implementation
664        // Full implementation would use maximum likelihood or least squares
665
666        // For now, return identity channel
667        let d = input_states[0].shape()[0];
668        let identity = Array2::eye(d).mapv(|x| Complex::new(x, 0.0));
669
670        QuantumChannel::from_kraus(vec![identity])
671    }
672
673    /// Generate informationally complete set of input states
674    pub fn generate_input_states(dim: usize) -> Vec<Array2<Complex<f64>>> {
675        let mut states = Vec::new();
676
677        // Add computational basis states
678        for i in 0..dim {
679            let mut state = Array2::zeros((dim, dim));
680            state[[i, i]] = Complex::new(1.0, 0.0);
681            states.push(state);
682        }
683
684        // Add superposition states
685        // Full implementation would generate SIC-POVM or tetrahedron states
686
687        states
688    }
689}
690
691#[cfg(test)]
692mod tests {
693    use super::*;
694    use num_complex::Complex;
695
696    #[test]
697    fn test_depolarizing_channel() {
698        let channel = QuantumChannels::depolarizing(0.1).unwrap();
699
700        assert_eq!(channel.input_dim, 2);
701        assert_eq!(channel.output_dim, 2);
702        assert!(channel.kraus.is_some());
703        assert_eq!(channel.kraus.as_ref().unwrap().operators.len(), 4);
704    }
705
706    #[test]
707    fn test_amplitude_damping() {
708        let channel = QuantumChannels::amplitude_damping(0.3).unwrap();
709
710        assert!(channel.kraus.is_some());
711        assert_eq!(channel.kraus.as_ref().unwrap().operators.len(), 2);
712
713        // Test on |1⟩⟨1| state
714        let mut rho = Array2::zeros((2, 2));
715        rho[[1, 1]] = Complex::new(1.0, 0.0);
716
717        let mut ch = channel;
718        let output = ch.apply(&rho).unwrap();
719
720        // Population should decrease
721        assert!(output[[1, 1]].re < 1.0);
722        assert!(output[[0, 0]].re > 0.0);
723    }
724
725    #[test]
726    fn test_kraus_to_choi() {
727        let mut channel = QuantumChannels::bit_flip(0.2).unwrap();
728        let choi = channel.to_choi().unwrap();
729
730        assert_eq!(choi.matrix.shape(), [4, 4]);
731
732        // Choi matrix should be Hermitian
733        let choi_dag = choi.matrix.mapv(|z| z.conj()).t().to_owned();
734        let diff = &choi.matrix - &choi_dag;
735        let max_diff = diff.iter().map(|z| z.norm()).fold(0.0, f64::max);
736        assert!(max_diff < 1e-10);
737    }
738
739    #[test]
740    fn test_channel_composition() {
741        // Create two channels
742        let mut ch1 = QuantumChannels::phase_flip(0.1).unwrap();
743        let mut ch2 = QuantumChannels::bit_flip(0.2).unwrap();
744
745        // Apply both to a superposition state
746        let mut rho = Array2::zeros((2, 2));
747        rho[[0, 0]] = Complex::new(0.5, 0.0);
748        rho[[0, 1]] = Complex::new(0.5, 0.0);
749        rho[[1, 0]] = Complex::new(0.5, 0.0);
750        rho[[1, 1]] = Complex::new(0.5, 0.0);
751
752        let intermediate = ch1.apply(&rho).unwrap();
753        let final_state = ch2.apply(&intermediate).unwrap();
754
755        // Trace should be preserved
756        let trace = final_state[[0, 0]] + final_state[[1, 1]];
757        assert!((trace.re - 1.0).abs() < 1e-10);
758        assert!(trace.im.abs() < 1e-10);
759    }
760
761    #[test]
762    fn test_unitary_channel() {
763        // Hadamard as unitary channel
764        let h = Array2::from_shape_vec(
765            (2, 2),
766            vec![
767                Complex::new(1.0, 0.0),
768                Complex::new(1.0, 0.0),
769                Complex::new(1.0, 0.0),
770                Complex::new(-1.0, 0.0),
771            ],
772        )
773        .unwrap()
774            / Complex::new(2.0_f64.sqrt(), 0.0);
775
776        let mut channel = QuantumChannel::from_kraus(vec![h]).unwrap();
777
778        assert!(channel.is_unitary().unwrap());
779    }
780
781    #[test]
782    fn test_stinespring_conversion() {
783        let mut channel = QuantumChannels::amplitude_damping(0.5).unwrap();
784
785        // Convert to Stinespring
786        let stinespring = channel.to_stinespring().unwrap();
787
788        assert_eq!(stinespring.env_dim, 2);
789        assert_eq!(stinespring.isometry.shape(), [4, 2]);
790
791        // Convert back to Kraus
792        let kraus_decomposer =
793            QuantumChannel::from_kraus(vec![Array2::eye(2).mapv(|x| Complex::new(x, 0.0))])
794                .unwrap();
795        let kraus = kraus_decomposer
796            .stinespring_to_kraus(&stinespring.isometry, stinespring.env_dim)
797            .unwrap();
798        assert_eq!(kraus.operators.len(), 2);
799    }
800}