quantrs2_sim/
open_quantum_systems.rs

1//! Open quantum system simulation with Lindblad master equations.
2//!
3//! This module provides comprehensive simulation capabilities for open quantum systems,
4//! including master equation evolution, Kraus operators, noise channels, and
5//! process tomography for realistic quantum device modeling.
6
7use crate::prelude::SimulatorError;
8use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
9use scirs2_core::parallel_ops::{IndexedParallelIterator, ParallelIterator};
10use scirs2_core::Complex64;
11use std::collections::HashMap;
12
13use crate::error::Result;
14use crate::scirs2_integration::SciRS2Backend;
15
16/// Lindblad master equation simulator for open quantum systems
17pub struct LindladSimulator {
18    /// Number of qubits
19    num_qubits: usize,
20    /// Current density matrix
21    density_matrix: Array2<Complex64>,
22    /// Lindblad operators
23    lindblad_ops: Vec<LindladOperator>,
24    /// System Hamiltonian
25    hamiltonian: Option<Array2<Complex64>>,
26    /// Time step for evolution
27    time_step: f64,
28    /// Integration method
29    integration_method: IntegrationMethod,
30    /// `SciRS2` backend for linear algebra
31    backend: Option<SciRS2Backend>,
32}
33
34/// Lindblad operator with coefficient
35#[derive(Debug, Clone)]
36pub struct LindladOperator {
37    /// Operator matrix
38    pub operator: Array2<Complex64>,
39    /// Collapse rate
40    pub rate: f64,
41    /// Optional label for the operator
42    pub label: Option<String>,
43}
44
45/// Integration methods for master equation
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum IntegrationMethod {
48    /// Explicit Euler method (fast, less accurate)
49    Euler,
50    /// 4th order Runge-Kutta (balanced)
51    RungeKutta4,
52    /// Adaptive Runge-Kutta with error control
53    AdaptiveRK,
54    /// Matrix exponential (exact for time-independent systems)
55    MatrixExponential,
56}
57
58impl LindladSimulator {
59    /// Create new Lindblad simulator
60    pub fn new(num_qubits: usize) -> Result<Self> {
61        let dim = 1 << num_qubits;
62        let mut density_matrix = Array2::zeros((dim, dim));
63        density_matrix[[0, 0]] = Complex64::new(1.0, 0.0); // |0⟩⟨0|
64
65        Ok(Self {
66            num_qubits,
67            density_matrix,
68            lindblad_ops: Vec::new(),
69            hamiltonian: None,
70            time_step: 0.01,
71            integration_method: IntegrationMethod::RungeKutta4,
72            backend: None,
73        })
74    }
75
76    /// Initialize with `SciRS2` backend
77    pub fn with_scirs2_backend(mut self) -> Result<Self> {
78        self.backend = Some(SciRS2Backend::new());
79        Ok(self)
80    }
81
82    /// Set initial density matrix
83    pub fn set_density_matrix(&mut self, rho: Array2<Complex64>) -> Result<()> {
84        let dim = 1 << self.num_qubits;
85        if rho.shape() != [dim, dim] {
86            return Err(SimulatorError::DimensionMismatch(format!(
87                "Expected {dim}x{dim} density matrix"
88            )));
89        }
90
91        // Verify trace normalization
92        let trace: Complex64 = rho.diag().iter().sum();
93        if (trace.re - 1.0).abs() > 1e-10 || trace.im.abs() > 1e-10 {
94            return Err(SimulatorError::InvalidInput(format!(
95                "Density matrix not normalized: trace = {trace}"
96            )));
97        }
98
99        self.density_matrix = rho;
100        Ok(())
101    }
102
103    /// Initialize from pure state vector
104    pub fn from_state_vector(&mut self, psi: &ArrayView1<Complex64>) -> Result<()> {
105        let dim = 1 << self.num_qubits;
106        if psi.len() != dim {
107            return Err(SimulatorError::DimensionMismatch(format!(
108                "Expected state vector of length {dim}"
109            )));
110        }
111
112        // Create density matrix |ψ⟩⟨ψ|
113        let mut rho = Array2::zeros((dim, dim));
114        for i in 0..dim {
115            for j in 0..dim {
116                rho[[i, j]] = psi[i] * psi[j].conj();
117            }
118        }
119
120        self.density_matrix = rho;
121        Ok(())
122    }
123
124    /// Add Lindblad operator
125    pub fn add_lindblad_operator(&mut self, operator: LindladOperator) -> Result<()> {
126        let dim = 1 << self.num_qubits;
127        if operator.operator.shape() != [dim, dim] {
128            return Err(SimulatorError::DimensionMismatch(format!(
129                "Operator must be {dim}x{dim}"
130            )));
131        }
132
133        self.lindblad_ops.push(operator);
134        Ok(())
135    }
136
137    /// Set system Hamiltonian
138    pub fn set_hamiltonian(&mut self, h: Array2<Complex64>) -> Result<()> {
139        let dim = 1 << self.num_qubits;
140        if h.shape() != [dim, dim] {
141            return Err(SimulatorError::DimensionMismatch(format!(
142                "Hamiltonian must be {dim}x{dim}"
143            )));
144        }
145
146        self.hamiltonian = Some(h);
147        Ok(())
148    }
149
150    /// Set time step for integration
151    pub const fn set_time_step(&mut self, dt: f64) {
152        self.time_step = dt;
153    }
154
155    /// Set integration method
156    pub const fn set_integration_method(&mut self, method: IntegrationMethod) {
157        self.integration_method = method;
158    }
159
160    /// Evolve system for given time
161    pub fn evolve(&mut self, total_time: f64) -> Result<EvolutionResult> {
162        let num_steps = (total_time / self.time_step).ceil() as usize;
163        let actual_dt = total_time / num_steps as f64;
164
165        let mut times = Vec::with_capacity(num_steps + 1);
166        let mut densities = Vec::new();
167        let mut purities = Vec::with_capacity(num_steps + 1);
168        let mut traces = Vec::with_capacity(num_steps + 1);
169
170        // Record initial state
171        times.push(0.0);
172        purities.push(self.purity());
173        traces.push(self.trace().re);
174
175        // Store initial density if requested (for small systems)
176        if self.num_qubits <= 4 {
177            densities.push(self.density_matrix.clone());
178        }
179
180        // Time evolution loop
181        for step in 0..num_steps {
182            match self.integration_method {
183                IntegrationMethod::Euler => {
184                    self.euler_step(actual_dt)?;
185                }
186                IntegrationMethod::RungeKutta4 => {
187                    self.runge_kutta4_step(actual_dt)?;
188                }
189                IntegrationMethod::AdaptiveRK => {
190                    self.adaptive_rk_step(actual_dt)?;
191                }
192                IntegrationMethod::MatrixExponential => {
193                    self.matrix_exponential_step(actual_dt)?;
194                }
195            }
196
197            let current_time = (step + 1) as f64 * actual_dt;
198            times.push(current_time);
199            purities.push(self.purity());
200            traces.push(self.trace().re);
201
202            if self.num_qubits <= 4 {
203                densities.push(self.density_matrix.clone());
204            }
205        }
206
207        Ok(EvolutionResult {
208            times,
209            densities,
210            purities,
211            traces,
212            final_density: self.density_matrix.clone(),
213        })
214    }
215
216    /// Single Euler integration step
217    fn euler_step(&mut self, dt: f64) -> Result<()> {
218        let derivative = self.compute_lindblad_derivative()?;
219
220        // ρ(t + dt) = ρ(t) + dt * dρ/dt
221        for ((i, j), drho_dt) in derivative.indexed_iter() {
222            self.density_matrix[[i, j]] += dt * drho_dt;
223        }
224
225        // Renormalize to maintain trace = 1
226        self.renormalize();
227
228        Ok(())
229    }
230
231    /// Single 4th order Runge-Kutta step
232    fn runge_kutta4_step(&mut self, dt: f64) -> Result<()> {
233        let rho0 = self.density_matrix.clone();
234
235        // k1 = f(t, y)
236        let k1 = self.compute_lindblad_derivative()?;
237
238        // k2 = f(t + dt/2, y + dt*k1/2)
239        self.density_matrix = &rho0 + &(&k1 * (dt / 2.0));
240        let k2 = self.compute_lindblad_derivative()?;
241
242        // k3 = f(t + dt/2, y + dt*k2/2)
243        self.density_matrix = &rho0 + &(&k2 * (dt / 2.0));
244        let k3 = self.compute_lindblad_derivative()?;
245
246        // k4 = f(t + dt, y + dt*k3)
247        self.density_matrix = &rho0 + &(&k3 * dt);
248        let k4 = self.compute_lindblad_derivative()?;
249
250        // y(t+dt) = y(t) + dt/6 * (k1 + 2*k2 + 2*k3 + k4)
251        let coeff = Complex64::new(dt / 6.0, 0.0);
252        self.density_matrix = rho0
253            + coeff
254                * (&k1 + &(Complex64::new(2.0, 0.0) * k2) + &(Complex64::new(2.0, 0.0) * k3) + &k4);
255
256        self.renormalize();
257        Ok(())
258    }
259
260    /// Adaptive Runge-Kutta step with error control
261    fn adaptive_rk_step(&mut self, dt: f64) -> Result<()> {
262        // For now, use fixed RK4 - full adaptive implementation would require
263        // error estimation and step size control
264        self.runge_kutta4_step(dt)
265    }
266
267    /// Matrix exponential step (exact for time-independent Lindbladian)
268    fn matrix_exponential_step(&mut self, dt: f64) -> Result<()> {
269        if let Some(ref backend) = self.backend {
270            // Use SciRS2 for matrix exponential if available
271            self.matrix_exp_with_scirs2(dt)
272        } else {
273            // Fallback to series expansion
274            self.matrix_exp_series(dt)
275        }
276    }
277
278    /// Matrix exponential using `SciRS2`
279    fn matrix_exp_with_scirs2(&mut self, dt: f64) -> Result<()> {
280        // This would use SciRS2's matrix exponential routines
281        // For now, fallback to series expansion
282        self.matrix_exp_series(dt)
283    }
284
285    /// Matrix exponential using series expansion
286    fn matrix_exp_series(&mut self, dt: f64) -> Result<()> {
287        let lindbladian = self.construct_lindbladian_superoperator()?;
288
289        // Compute matrix exponential using series: exp(L*dt) ≈ I + L*dt + (L*dt)²/2! + ...
290        let dim_sq = lindbladian.nrows();
291        let mut result = Array2::eye(dim_sq);
292        let mut term = Array2::eye(dim_sq);
293        let l_dt = &lindbladian * dt;
294
295        // Series expansion up to reasonable order
296        for n in 1..=20 {
297            term = term.dot(&l_dt) / f64::from(n);
298            result += &term;
299
300            // Check convergence
301            let term_norm: f64 = term.iter().map(|x| x.norm()).sum();
302            if term_norm < 1e-12 {
303                break;
304            }
305        }
306
307        // Apply to vectorized density matrix
308        let rho_vec = self.vectorize_density_matrix();
309        let new_rho_vec = result.dot(&rho_vec);
310        self.density_matrix = self.devectorize_density_matrix(&new_rho_vec);
311
312        self.renormalize();
313        Ok(())
314    }
315
316    /// Compute Lindblad master equation derivative
317    fn compute_lindblad_derivative(&self) -> Result<Array2<Complex64>> {
318        let dim = self.density_matrix.nrows();
319        let mut derivative = Array2::zeros((dim, dim));
320
321        // Hamiltonian evolution: -i[H, ρ]
322        if let Some(ref h) = self.hamiltonian {
323            let commutator = h.dot(&self.density_matrix) - self.density_matrix.dot(h);
324            derivative += &(commutator * Complex64::new(0.0, -1.0));
325        }
326
327        // Lindblad dissipation terms
328        for lindblad_op in &self.lindblad_ops {
329            let l = &lindblad_op.operator;
330            let l_dag = l.t().mapv(|x| x.conj());
331            let rate = lindblad_op.rate;
332
333            // Dissipation: γ(L ρ L† - (1/2){L†L, ρ})
334            let dissipation = l.dot(&self.density_matrix).dot(&l_dag);
335            let anticommutator =
336                l_dag.dot(l).dot(&self.density_matrix) + self.density_matrix.dot(&l_dag.dot(l));
337            let half = Complex64::new(0.5, 0.0);
338
339            derivative += &((dissipation - &anticommutator * half) * rate);
340        }
341
342        Ok(derivative)
343    }
344
345    /// Construct Lindbladian superoperator matrix
346    fn construct_lindbladian_superoperator(&self) -> Result<Array2<Complex64>> {
347        let dim = 1 << self.num_qubits;
348        let super_dim = dim * dim;
349        let mut lindbladian = Array2::zeros((super_dim, super_dim));
350
351        // Hamiltonian part: -i(H ⊗ I - I ⊗ H^T)
352        if let Some(ref h) = self.hamiltonian {
353            let eye: Array2<Complex64> = Array2::eye(dim);
354            let h_left = kron(h, &eye);
355            let h_t = h.t().to_owned();
356            let h_right = kron(&eye, &h_t);
357            lindbladian += &((h_left - h_right) * Complex64::new(0.0, -1.0));
358        }
359
360        // Lindblad terms
361        for lindblad_op in &self.lindblad_ops {
362            let l = &lindblad_op.operator;
363            let l_dag = l.t().mapv(|x| x.conj());
364            let rate = lindblad_op.rate;
365
366            let eye: Array2<Complex64> = Array2::eye(dim);
367            let l_dag_l = l_dag.dot(l);
368
369            // L ⊗ L*
370            let left_term = kron(l, &l.mapv(|x| x.conj()));
371
372            // -(1/2)(L†L ⊗ I + I ⊗ (L†L)^T)
373            let l_dag_l_t = l_dag_l.t().to_owned();
374            let right_term = kron(&l_dag_l, &eye) + kron(&eye, &l_dag_l_t);
375            let half = Complex64::new(0.5, 0.0);
376
377            lindbladian += &((left_term - &right_term * half) * rate);
378        }
379
380        Ok(lindbladian)
381    }
382
383    /// Vectorize density matrix (column-major order)
384    fn vectorize_density_matrix(&self) -> Array1<Complex64> {
385        let dim = self.density_matrix.nrows();
386        let mut vec = Array1::zeros(dim * dim);
387
388        for (i, &val) in self.density_matrix.iter().enumerate() {
389            vec[i] = val;
390        }
391
392        vec
393    }
394
395    /// Devectorize density matrix
396    fn devectorize_density_matrix(&self, vec: &Array1<Complex64>) -> Array2<Complex64> {
397        let dim = (vec.len() as f64).sqrt() as usize;
398        Array2::from_shape_vec((dim, dim), vec.to_vec()).expect(
399            "devectorize_density_matrix: shape mismatch should not occur for valid density matrix",
400        )
401    }
402
403    /// Get current purity Tr(ρ²)
404    #[must_use]
405    pub fn purity(&self) -> f64 {
406        let rho_squared = self.density_matrix.dot(&self.density_matrix);
407        rho_squared.diag().iter().map(|x| x.re).sum()
408    }
409
410    /// Get current trace
411    #[must_use]
412    pub fn trace(&self) -> Complex64 {
413        self.density_matrix.diag().iter().sum()
414    }
415
416    /// Renormalize density matrix to unit trace
417    fn renormalize(&mut self) {
418        let trace = self.trace();
419        if trace.norm() > 1e-12 {
420            self.density_matrix /= trace;
421        }
422    }
423
424    /// Get current density matrix
425    #[must_use]
426    pub const fn get_density_matrix(&self) -> &Array2<Complex64> {
427        &self.density_matrix
428    }
429
430    /// Compute expectation value of observable
431    pub fn expectation_value(&self, observable: &Array2<Complex64>) -> Result<Complex64> {
432        if observable.shape() != self.density_matrix.shape() {
433            return Err(SimulatorError::DimensionMismatch(
434                "Observable and density matrix dimensions must match".to_string(),
435            ));
436        }
437
438        // Tr(ρ O)
439        let product = self.density_matrix.dot(observable);
440        Ok(product.diag().iter().sum())
441    }
442}
443
444/// Result of time evolution
445#[derive(Debug, Clone)]
446pub struct EvolutionResult {
447    /// Time points
448    pub times: Vec<f64>,
449    /// Density matrices at each time (if stored)
450    pub densities: Vec<Array2<Complex64>>,
451    /// Purity at each time
452    pub purities: Vec<f64>,
453    /// Trace at each time
454    pub traces: Vec<f64>,
455    /// Final density matrix
456    pub final_density: Array2<Complex64>,
457}
458
459/// Quantum channel represented by Kraus operators
460#[derive(Debug, Clone)]
461pub struct QuantumChannel {
462    /// Kraus operators
463    pub kraus_operators: Vec<Array2<Complex64>>,
464    /// Channel name
465    pub name: String,
466}
467
468impl QuantumChannel {
469    /// Create depolarizing channel
470    #[must_use]
471    pub fn depolarizing(num_qubits: usize, probability: f64) -> Self {
472        let dim = 1 << num_qubits;
473        let mut kraus_ops = Vec::new();
474
475        // Identity term
476        let sqrt_p0 = (1.0 - probability).sqrt();
477        let eye: Array2<Complex64> = Array2::eye(dim) * Complex64::new(sqrt_p0, 0.0);
478        kraus_ops.push(eye);
479
480        // Pauli terms
481        if num_qubits == 1 {
482            let sqrt_p = (probability / 3.0).sqrt();
483
484            // Pauli X
485            let mut pauli_x = Array2::zeros((2, 2));
486            pauli_x[[0, 1]] = Complex64::new(sqrt_p, 0.0);
487            pauli_x[[1, 0]] = Complex64::new(sqrt_p, 0.0);
488            kraus_ops.push(pauli_x);
489
490            // Pauli Y
491            let mut pauli_y = Array2::zeros((2, 2));
492            pauli_y[[0, 1]] = Complex64::new(0.0, -sqrt_p);
493            pauli_y[[1, 0]] = Complex64::new(0.0, sqrt_p);
494            kraus_ops.push(pauli_y);
495
496            // Pauli Z
497            let mut pauli_z = Array2::zeros((2, 2));
498            pauli_z[[0, 0]] = Complex64::new(sqrt_p, 0.0);
499            pauli_z[[1, 1]] = Complex64::new(-sqrt_p, 0.0);
500            kraus_ops.push(pauli_z);
501        }
502
503        Self {
504            kraus_operators: kraus_ops,
505            name: format!("Depolarizing({probability:.3})"),
506        }
507    }
508
509    /// Create amplitude damping channel
510    #[must_use]
511    pub fn amplitude_damping(gamma: f64) -> Self {
512        let mut kraus_ops = Vec::new();
513
514        // K0 = |0⟩⟨0| + √(1-γ)|1⟩⟨1|
515        let mut k0 = Array2::zeros((2, 2));
516        k0[[0, 0]] = Complex64::new(1.0, 0.0);
517        k0[[1, 1]] = Complex64::new((1.0 - gamma).sqrt(), 0.0);
518        kraus_ops.push(k0);
519
520        // K1 = √γ|0⟩⟨1|
521        let mut k1 = Array2::zeros((2, 2));
522        k1[[0, 1]] = Complex64::new(gamma.sqrt(), 0.0);
523        kraus_ops.push(k1);
524
525        Self {
526            kraus_operators: kraus_ops,
527            name: format!("AmplitudeDamping({gamma:.3})"),
528        }
529    }
530
531    /// Create phase damping channel
532    #[must_use]
533    pub fn phase_damping(gamma: f64) -> Self {
534        let mut kraus_ops = Vec::new();
535
536        // K0 = |0⟩⟨0| + √(1-γ)|1⟩⟨1|
537        let mut k0 = Array2::zeros((2, 2));
538        k0[[0, 0]] = Complex64::new(1.0, 0.0);
539        k0[[1, 1]] = Complex64::new((1.0 - gamma).sqrt(), 0.0);
540        kraus_ops.push(k0);
541
542        // K1 = √γ|1⟩⟨1|
543        let mut k1 = Array2::zeros((2, 2));
544        k1[[1, 1]] = Complex64::new(gamma.sqrt(), 0.0);
545        kraus_ops.push(k1);
546
547        Self {
548            kraus_operators: kraus_ops,
549            name: format!("PhaseDamping({gamma:.3})"),
550        }
551    }
552
553    /// Apply channel to density matrix
554    #[must_use]
555    pub fn apply(&self, rho: &Array2<Complex64>) -> Array2<Complex64> {
556        let dim = rho.nrows();
557        let mut result = Array2::zeros((dim, dim));
558
559        for kraus_op in &self.kraus_operators {
560            let k_dag = kraus_op.t().mapv(|x| x.conj());
561            result += &kraus_op.dot(rho).dot(&k_dag);
562        }
563
564        result
565    }
566
567    /// Verify channel is trace-preserving
568    #[must_use]
569    pub fn is_trace_preserving(&self) -> bool {
570        let dim = self.kraus_operators[0].nrows();
571        let mut sum = Array2::zeros((dim, dim));
572
573        for kraus_op in &self.kraus_operators {
574            let k_dag = kraus_op.t().mapv(|x| x.conj());
575            sum += &k_dag.dot(kraus_op);
576        }
577
578        // Check if sum ≈ I
579        let eye: Array2<Complex64> = Array2::eye(dim);
580        (&sum - &eye).iter().all(|&x| x.norm() < 1e-10)
581    }
582}
583
584/// Process tomography for quantum channel characterization
585pub struct ProcessTomography {
586    /// Input basis states
587    pub input_states: Vec<Array2<Complex64>>,
588    /// Output measurements
589    pub output_measurements: Vec<Array2<Complex64>>,
590    /// Reconstructed process matrix
591    pub process_matrix: Option<Array2<Complex64>>,
592}
593
594impl ProcessTomography {
595    /// Create standard process tomography setup
596    #[must_use]
597    pub fn new(num_qubits: usize) -> Self {
598        let mut input_states = Vec::new();
599
600        // Prepare complete basis of density matrices
601        if num_qubits == 1 {
602            // Single qubit: |0⟩, |1⟩, |+⟩, |+i⟩
603            let mut rho_0 = Array2::zeros((2, 2));
604            rho_0[[0, 0]] = Complex64::new(1.0, 0.0);
605            input_states.push(rho_0);
606
607            let mut rho_1 = Array2::zeros((2, 2));
608            rho_1[[1, 1]] = Complex64::new(1.0, 0.0);
609            input_states.push(rho_1);
610
611            let mut rho_plus = Array2::zeros((2, 2));
612            rho_plus[[0, 0]] = Complex64::new(0.5, 0.0);
613            rho_plus[[0, 1]] = Complex64::new(0.5, 0.0);
614            rho_plus[[1, 0]] = Complex64::new(0.5, 0.0);
615            rho_plus[[1, 1]] = Complex64::new(0.5, 0.0);
616            input_states.push(rho_plus);
617
618            let mut rho_plus_i = Array2::zeros((2, 2));
619            rho_plus_i[[0, 0]] = Complex64::new(0.5, 0.0);
620            rho_plus_i[[0, 1]] = Complex64::new(0.0, -0.5);
621            rho_plus_i[[1, 0]] = Complex64::new(0.0, 0.5);
622            rho_plus_i[[1, 1]] = Complex64::new(0.5, 0.0);
623            input_states.push(rho_plus_i);
624        }
625
626        Self {
627            input_states,
628            output_measurements: Vec::new(),
629            process_matrix: None,
630        }
631    }
632
633    /// Perform tomography on quantum channel
634    pub fn characterize_channel(&mut self, channel: &QuantumChannel) -> Result<()> {
635        self.output_measurements.clear();
636
637        for input_state in &self.input_states {
638            let output = channel.apply(input_state);
639            self.output_measurements.push(output);
640        }
641
642        self.reconstruct_process_matrix()?;
643        Ok(())
644    }
645
646    /// Reconstruct process matrix from measurements
647    fn reconstruct_process_matrix(&mut self) -> Result<()> {
648        // Simplified reconstruction - full implementation would use
649        // maximum likelihood estimation or linear inversion
650
651        let dim = self.input_states[0].nrows();
652        let process_dim = dim * dim;
653        let mut chi = Array2::zeros((process_dim, process_dim));
654
655        // This is a placeholder - real process tomography requires
656        // solving a linear system to find the χ matrix
657        self.process_matrix = Some(chi);
658
659        Ok(())
660    }
661
662    /// Get process fidelity
663    pub fn process_fidelity(&self, ideal_channel: &QuantumChannel) -> Result<f64> {
664        if self.output_measurements.is_empty() {
665            return Err(SimulatorError::InvalidOperation(
666                "No measurements available for fidelity calculation".to_string(),
667            ));
668        }
669
670        let mut fidelity_sum = 0.0;
671
672        for (i, input_state) in self.input_states.iter().enumerate() {
673            let ideal_output = ideal_channel.apply(input_state);
674            let measured_output = &self.output_measurements[i];
675
676            // Simplified fidelity calculation
677            let fidelity = quantum_fidelity(measured_output, &ideal_output);
678            fidelity_sum += fidelity;
679        }
680
681        Ok(fidelity_sum / self.input_states.len() as f64)
682    }
683}
684
685/// Compute quantum fidelity between two density matrices
686#[must_use]
687pub fn quantum_fidelity(rho1: &Array2<Complex64>, rho2: &Array2<Complex64>) -> f64 {
688    // F(ρ₁, ρ₂) = Tr(√(√ρ₁ ρ₂ √ρ₁))²
689    // Simplified calculation for small systems
690
691    // For pure states: F = |⟨ψ₁|ψ₂⟩|²
692    // For mixed states, approximate with trace distance
693    let trace_distance = (rho1 - rho2).iter().map(|x| x.norm()).sum::<f64>();
694    0.5f64.mul_add(-trace_distance, 1.0).max(0.0)
695}
696
697/// Kronecker product of two matrices
698fn kron(a: &Array2<Complex64>, b: &Array2<Complex64>) -> Array2<Complex64> {
699    let (m1, n1) = a.dim();
700    let (m2, n2) = b.dim();
701    let mut result = Array2::zeros((m1 * m2, n1 * n2));
702
703    for i in 0..m1 {
704        for j in 0..n1 {
705            for k in 0..m2 {
706                for l in 0..n2 {
707                    result[[i * m2 + k, j * n2 + l]] = a[[i, j]] * b[[k, l]];
708                }
709            }
710        }
711    }
712
713    result
714}
715
716/// Noise model builder for common quantum channels
717pub struct NoiseModelBuilder {
718    channels: HashMap<String, QuantumChannel>,
719    application_order: Vec<String>,
720}
721
722impl Default for NoiseModelBuilder {
723    fn default() -> Self {
724        Self::new()
725    }
726}
727
728impl NoiseModelBuilder {
729    #[must_use]
730    pub fn new() -> Self {
731        Self {
732            channels: HashMap::new(),
733            application_order: Vec::new(),
734        }
735    }
736
737    /// Add depolarizing noise
738    #[must_use]
739    pub fn depolarizing(mut self, name: &str, probability: f64) -> Self {
740        let channel = QuantumChannel::depolarizing(1, probability);
741        self.channels.insert(name.to_string(), channel);
742        self.application_order.push(name.to_string());
743        self
744    }
745
746    /// Add amplitude damping
747    #[must_use]
748    pub fn amplitude_damping(mut self, name: &str, gamma: f64) -> Self {
749        let channel = QuantumChannel::amplitude_damping(gamma);
750        self.channels.insert(name.to_string(), channel);
751        self.application_order.push(name.to_string());
752        self
753    }
754
755    /// Add phase damping
756    #[must_use]
757    pub fn phase_damping(mut self, name: &str, gamma: f64) -> Self {
758        let channel = QuantumChannel::phase_damping(gamma);
759        self.channels.insert(name.to_string(), channel);
760        self.application_order.push(name.to_string());
761        self
762    }
763
764    /// Build composite noise model
765    #[must_use]
766    pub fn build(self) -> CompositeNoiseModel {
767        CompositeNoiseModel {
768            channels: self.channels,
769            application_order: self.application_order,
770        }
771    }
772}
773
774/// Composite noise model with multiple channels
775#[derive(Debug, Clone)]
776pub struct CompositeNoiseModel {
777    channels: HashMap<String, QuantumChannel>,
778    application_order: Vec<String>,
779}
780
781impl CompositeNoiseModel {
782    /// Apply all noise channels in order
783    #[must_use]
784    pub fn apply(&self, rho: &Array2<Complex64>) -> Array2<Complex64> {
785        let mut result = rho.clone();
786
787        for channel_name in &self.application_order {
788            if let Some(channel) = self.channels.get(channel_name) {
789                result = channel.apply(&result);
790            }
791        }
792
793        result
794    }
795
796    /// Get channel by name
797    #[must_use]
798    pub fn get_channel(&self, name: &str) -> Option<&QuantumChannel> {
799        self.channels.get(name)
800    }
801}
802
803#[cfg(test)]
804mod tests {
805    use super::*;
806
807    #[test]
808    fn test_lindblad_simulator_creation() {
809        let sim = LindladSimulator::new(2).expect("should create Lindblad simulator with 2 qubits");
810        assert_eq!(sim.num_qubits, 2);
811        assert_eq!(sim.density_matrix.shape(), [4, 4]);
812    }
813
814    #[test]
815    fn test_depolarizing_channel() {
816        let channel = QuantumChannel::depolarizing(1, 0.1);
817        assert!(channel.is_trace_preserving());
818        assert_eq!(channel.kraus_operators.len(), 4);
819    }
820
821    #[test]
822    fn test_amplitude_damping() {
823        let channel = QuantumChannel::amplitude_damping(0.2);
824        assert!(channel.is_trace_preserving());
825
826        // Test on |1⟩ state
827        let mut rho_1 = Array2::zeros((2, 2));
828        rho_1[[1, 1]] = Complex64::new(1.0, 0.0);
829
830        let result = channel.apply(&rho_1);
831
832        // Should have some population in |0⟩
833        assert!(result[[0, 0]].re > 0.0);
834        assert!(result[[1, 1]].re < 1.0);
835    }
836
837    #[test]
838    fn test_noise_model_builder() {
839        let noise_model = NoiseModelBuilder::new()
840            .depolarizing("depol", 0.01)
841            .amplitude_damping("amp_damp", 0.02)
842            .build();
843
844        assert!(noise_model.get_channel("depol").is_some());
845        assert!(noise_model.get_channel("amp_damp").is_some());
846    }
847}