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