1use 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
16pub struct LindladSimulator {
18 num_qubits: usize,
20 density_matrix: Array2<Complex64>,
22 lindblad_ops: Vec<LindladOperator>,
24 hamiltonian: Option<Array2<Complex64>>,
26 time_step: f64,
28 integration_method: IntegrationMethod,
30 backend: Option<SciRS2Backend>,
32}
33
34#[derive(Debug, Clone)]
36pub struct LindladOperator {
37 pub operator: Array2<Complex64>,
39 pub rate: f64,
41 pub label: Option<String>,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum IntegrationMethod {
48 Euler,
50 RungeKutta4,
52 AdaptiveRK,
54 MatrixExponential,
56}
57
58impl LindladSimulator {
59 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); 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 pub fn with_scirs2_backend(mut self) -> Result<Self> {
78 self.backend = Some(SciRS2Backend::new());
79 Ok(self)
80 }
81
82 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 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 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 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 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 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 pub const fn set_time_step(&mut self, dt: f64) {
152 self.time_step = dt;
153 }
154
155 pub const fn set_integration_method(&mut self, method: IntegrationMethod) {
157 self.integration_method = method;
158 }
159
160 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 times.push(0.0);
172 purities.push(self.purity());
173 traces.push(self.trace().re);
174
175 if self.num_qubits <= 4 {
177 densities.push(self.density_matrix.clone());
178 }
179
180 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 fn euler_step(&mut self, dt: f64) -> Result<()> {
218 let derivative = self.compute_lindblad_derivative()?;
219
220 for ((i, j), drho_dt) in derivative.indexed_iter() {
222 self.density_matrix[[i, j]] += dt * drho_dt;
223 }
224
225 self.renormalize();
227
228 Ok(())
229 }
230
231 fn runge_kutta4_step(&mut self, dt: f64) -> Result<()> {
233 let rho0 = self.density_matrix.clone();
234
235 let k1 = self.compute_lindblad_derivative()?;
237
238 self.density_matrix = &rho0 + &(&k1 * (dt / 2.0));
240 let k2 = self.compute_lindblad_derivative()?;
241
242 self.density_matrix = &rho0 + &(&k2 * (dt / 2.0));
244 let k3 = self.compute_lindblad_derivative()?;
245
246 self.density_matrix = &rho0 + &(&k3 * dt);
248 let k4 = self.compute_lindblad_derivative()?;
249
250 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 fn adaptive_rk_step(&mut self, dt: f64) -> Result<()> {
262 self.runge_kutta4_step(dt)
265 }
266
267 fn matrix_exponential_step(&mut self, dt: f64) -> Result<()> {
269 if let Some(ref backend) = self.backend {
270 self.matrix_exp_with_scirs2(dt)
272 } else {
273 self.matrix_exp_series(dt)
275 }
276 }
277
278 fn matrix_exp_with_scirs2(&mut self, dt: f64) -> Result<()> {
280 self.matrix_exp_series(dt)
283 }
284
285 fn matrix_exp_series(&mut self, dt: f64) -> Result<()> {
287 let lindbladian = self.construct_lindbladian_superoperator()?;
288
289 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 for n in 1..=20 {
297 term = term.dot(&l_dt) / f64::from(n);
298 result += &term;
299
300 let term_norm: f64 = term.iter().map(|x| x.norm()).sum();
302 if term_norm < 1e-12 {
303 break;
304 }
305 }
306
307 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 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 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 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 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 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 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 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 let left_term = kron(l, &l.mapv(|x| x.conj()));
371
372 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 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 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 #[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 #[must_use]
412 pub fn trace(&self) -> Complex64 {
413 self.density_matrix.diag().iter().sum()
414 }
415
416 fn renormalize(&mut self) {
418 let trace = self.trace();
419 if trace.norm() > 1e-12 {
420 self.density_matrix /= trace;
421 }
422 }
423
424 #[must_use]
426 pub const fn get_density_matrix(&self) -> &Array2<Complex64> {
427 &self.density_matrix
428 }
429
430 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 let product = self.density_matrix.dot(observable);
440 Ok(product.diag().iter().sum())
441 }
442}
443
444#[derive(Debug, Clone)]
446pub struct EvolutionResult {
447 pub times: Vec<f64>,
449 pub densities: Vec<Array2<Complex64>>,
451 pub purities: Vec<f64>,
453 pub traces: Vec<f64>,
455 pub final_density: Array2<Complex64>,
457}
458
459#[derive(Debug, Clone)]
461pub struct QuantumChannel {
462 pub kraus_operators: Vec<Array2<Complex64>>,
464 pub name: String,
466}
467
468impl QuantumChannel {
469 #[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 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 if num_qubits == 1 {
482 let sqrt_p = (probability / 3.0).sqrt();
483
484 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 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 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 #[must_use]
511 pub fn amplitude_damping(gamma: f64) -> Self {
512 let mut kraus_ops = Vec::new();
513
514 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 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 #[must_use]
533 pub fn phase_damping(gamma: f64) -> Self {
534 let mut kraus_ops = Vec::new();
535
536 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 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 #[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 #[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 let eye: Array2<Complex64> = Array2::eye(dim);
580 (&sum - &eye).iter().all(|&x| x.norm() < 1e-10)
581 }
582}
583
584pub struct ProcessTomography {
586 pub input_states: Vec<Array2<Complex64>>,
588 pub output_measurements: Vec<Array2<Complex64>>,
590 pub process_matrix: Option<Array2<Complex64>>,
592}
593
594impl ProcessTomography {
595 #[must_use]
597 pub fn new(num_qubits: usize) -> Self {
598 let mut input_states = Vec::new();
599
600 if num_qubits == 1 {
602 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 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 fn reconstruct_process_matrix(&mut self) -> Result<()> {
662 if self.input_states.is_empty() {
663 return Err(SimulatorError::InvalidInput(
664 "Process tomography requires at least one prepared input state".to_string(),
665 ));
666 }
667 if self.output_measurements.len() != self.input_states.len() {
668 return Err(SimulatorError::InvalidInput(
669 "Number of output measurements must match the number of input states".to_string(),
670 ));
671 }
672
673 let dim = self.input_states[0].nrows();
674 if dim == 0 || !dim.is_power_of_two() {
675 return Err(SimulatorError::InvalidInput(format!(
676 "Input state dimension {dim} must be a positive power of two"
677 )));
678 }
679 let num_qubits = dim.trailing_zeros() as usize;
680 let process_dim = dim * dim; let num_inputs = self.input_states.len();
682
683 if num_inputs < process_dim {
686 return Err(SimulatorError::InvalidInput(format!(
687 "Process tomography is under-determined: {num_inputs} input states provided, \
688 at least {process_dim} tomographically complete states are required"
689 )));
690 }
691
692 let basis = pauli_operator_basis(num_qubits);
693 let basis_dag: Vec<Array2<Complex64>> =
694 basis.iter().map(|p| p.t().mapv(|z| z.conj())).collect();
695
696 let unknowns = process_dim * process_dim;
697 let equations = num_inputs * dim * dim;
698 let mut a = Array2::<Complex64>::zeros((equations, unknowns));
699 let mut b = Array1::<Complex64>::zeros(equations);
700
701 for (j, (rho, sigma)) in self
702 .input_states
703 .iter()
704 .zip(self.output_measurements.iter())
705 .enumerate()
706 {
707 let eq_base = j * dim * dim;
708 for m in 0..process_dim {
709 let pm_rho = basis[m].dot(rho);
710 for n in 0..process_dim {
711 let mmn = pm_rho.dot(&basis_dag[n]);
712 let u = m * process_dim + n;
713 for r in 0..dim {
714 for c in 0..dim {
715 a[[eq_base + r * dim + c, u]] = mmn[[r, c]];
716 }
717 }
718 }
719 }
720 for r in 0..dim {
721 for c in 0..dim {
722 b[eq_base + r * dim + c] = sigma[[r, c]];
723 }
724 }
725 }
726
727 let a_dag = a.t().mapv(|z| z.conj());
729 let aha = a_dag.dot(&a);
730 let ahb = a_dag.dot(&b);
731 let aha_inv = scirs2_linalg::complex::complex_inverse(&aha.view()).map_err(|e| {
732 SimulatorError::InvalidInput(format!(
733 "Input state set is not tomographically complete (singular system): {e}"
734 ))
735 })?;
736 let x = aha_inv.dot(&ahb);
737
738 let mut chi = Array2::<Complex64>::zeros((process_dim, process_dim));
739 for m in 0..process_dim {
740 for n in 0..process_dim {
741 chi[[m, n]] = x[m * process_dim + n];
742 }
743 }
744
745 self.process_matrix = Some(chi);
746 Ok(())
747 }
748
749 pub fn process_fidelity(&self, ideal_channel: &QuantumChannel) -> Result<f64> {
751 if self.output_measurements.is_empty() {
752 return Err(SimulatorError::InvalidOperation(
753 "No measurements available for fidelity calculation".to_string(),
754 ));
755 }
756
757 let mut fidelity_sum = 0.0;
758
759 for (i, input_state) in self.input_states.iter().enumerate() {
760 let ideal_output = ideal_channel.apply(input_state);
761 let measured_output = &self.output_measurements[i];
762
763 let fidelity = quantum_fidelity(measured_output, &ideal_output);
765 fidelity_sum += fidelity;
766 }
767
768 Ok(fidelity_sum / self.input_states.len() as f64)
769 }
770}
771
772fn pauli_operator_basis(num_qubits: usize) -> Vec<Array2<Complex64>> {
779 let i2: Array2<Complex64> = Array2::eye(2);
780 let mut px: Array2<Complex64> = Array2::zeros((2, 2));
781 px[[0, 1]] = Complex64::new(1.0, 0.0);
782 px[[1, 0]] = Complex64::new(1.0, 0.0);
783 let mut py: Array2<Complex64> = Array2::zeros((2, 2));
784 py[[0, 1]] = Complex64::new(0.0, -1.0);
785 py[[1, 0]] = Complex64::new(0.0, 1.0);
786 let mut pz: Array2<Complex64> = Array2::zeros((2, 2));
787 pz[[0, 0]] = Complex64::new(1.0, 0.0);
788 pz[[1, 1]] = Complex64::new(-1.0, 0.0);
789 let singles = [i2, px, py, pz];
790
791 let mut basis: Vec<Array2<Complex64>> = vec![Array2::<Complex64>::eye(1)];
792 for _ in 0..num_qubits {
793 let mut next = Vec::with_capacity(basis.len() * 4);
794 for op in &basis {
795 for s in &singles {
796 next.push(kron(op, s));
797 }
798 }
799 basis = next;
800 }
801 basis
802}
803
804#[must_use]
806pub fn quantum_fidelity(rho1: &Array2<Complex64>, rho2: &Array2<Complex64>) -> f64 {
807 let trace_distance = (rho1 - rho2).iter().map(|x| x.norm()).sum::<f64>();
813 0.5f64.mul_add(-trace_distance, 1.0).max(0.0)
814}
815
816fn kron(a: &Array2<Complex64>, b: &Array2<Complex64>) -> Array2<Complex64> {
818 let (m1, n1) = a.dim();
819 let (m2, n2) = b.dim();
820 let mut result = Array2::zeros((m1 * m2, n1 * n2));
821
822 for i in 0..m1 {
823 for j in 0..n1 {
824 for k in 0..m2 {
825 for l in 0..n2 {
826 result[[i * m2 + k, j * n2 + l]] = a[[i, j]] * b[[k, l]];
827 }
828 }
829 }
830 }
831
832 result
833}
834
835pub struct NoiseModelBuilder {
837 channels: HashMap<String, QuantumChannel>,
838 application_order: Vec<String>,
839}
840
841impl Default for NoiseModelBuilder {
842 fn default() -> Self {
843 Self::new()
844 }
845}
846
847impl NoiseModelBuilder {
848 #[must_use]
849 pub fn new() -> Self {
850 Self {
851 channels: HashMap::new(),
852 application_order: Vec::new(),
853 }
854 }
855
856 #[must_use]
858 pub fn depolarizing(mut self, name: &str, probability: f64) -> Self {
859 let channel = QuantumChannel::depolarizing(1, probability);
860 self.channels.insert(name.to_string(), channel);
861 self.application_order.push(name.to_string());
862 self
863 }
864
865 #[must_use]
867 pub fn amplitude_damping(mut self, name: &str, gamma: f64) -> Self {
868 let channel = QuantumChannel::amplitude_damping(gamma);
869 self.channels.insert(name.to_string(), channel);
870 self.application_order.push(name.to_string());
871 self
872 }
873
874 #[must_use]
876 pub fn phase_damping(mut self, name: &str, gamma: f64) -> Self {
877 let channel = QuantumChannel::phase_damping(gamma);
878 self.channels.insert(name.to_string(), channel);
879 self.application_order.push(name.to_string());
880 self
881 }
882
883 #[must_use]
885 pub fn build(self) -> CompositeNoiseModel {
886 CompositeNoiseModel {
887 channels: self.channels,
888 application_order: self.application_order,
889 }
890 }
891}
892
893#[derive(Debug, Clone)]
895pub struct CompositeNoiseModel {
896 channels: HashMap<String, QuantumChannel>,
897 application_order: Vec<String>,
898}
899
900impl CompositeNoiseModel {
901 #[must_use]
903 pub fn apply(&self, rho: &Array2<Complex64>) -> Array2<Complex64> {
904 let mut result = rho.clone();
905
906 for channel_name in &self.application_order {
907 if let Some(channel) = self.channels.get(channel_name) {
908 result = channel.apply(&result);
909 }
910 }
911
912 result
913 }
914
915 #[must_use]
917 pub fn get_channel(&self, name: &str) -> Option<&QuantumChannel> {
918 self.channels.get(name)
919 }
920}
921
922#[cfg(test)]
923mod tests {
924 use super::*;
925
926 #[test]
927 fn test_lindblad_simulator_creation() {
928 let sim = LindladSimulator::new(2).expect("should create Lindblad simulator with 2 qubits");
929 assert_eq!(sim.num_qubits, 2);
930 assert_eq!(sim.density_matrix.shape(), [4, 4]);
931 }
932
933 #[test]
934 fn test_depolarizing_channel() {
935 let channel = QuantumChannel::depolarizing(1, 0.1);
936 assert!(channel.is_trace_preserving());
937 assert_eq!(channel.kraus_operators.len(), 4);
938 }
939
940 #[test]
941 fn test_amplitude_damping() {
942 let channel = QuantumChannel::amplitude_damping(0.2);
943 assert!(channel.is_trace_preserving());
944
945 let mut rho_1 = Array2::zeros((2, 2));
947 rho_1[[1, 1]] = Complex64::new(1.0, 0.0);
948
949 let result = channel.apply(&rho_1);
950
951 assert!(result[[0, 0]].re > 0.0);
953 assert!(result[[1, 1]].re < 1.0);
954 }
955
956 #[test]
957 fn test_noise_model_builder() {
958 let noise_model = NoiseModelBuilder::new()
959 .depolarizing("depol", 0.01)
960 .amplitude_damping("amp_damp", 0.02)
961 .build();
962
963 assert!(noise_model.get_channel("depol").is_some());
964 assert!(noise_model.get_channel("amp_damp").is_some());
965 }
966
967 #[test]
968 fn test_pauli_basis_is_orthonormal_under_hs_inner_product() {
969 let basis = pauli_operator_basis(1);
971 assert_eq!(basis.len(), 4);
972 for (m, pm) in basis.iter().enumerate() {
973 for (n, pn) in basis.iter().enumerate() {
974 let prod = pm.t().mapv(|z| z.conj()).dot(pn);
975 let trace: Complex64 = (0..2).map(|i| prod[[i, i]]).sum();
976 let expected = if m == n { 2.0 } else { 0.0 };
977 assert!((trace.re - expected).abs() < 1e-12 && trace.im.abs() < 1e-12);
978 }
979 }
980 }
981
982 #[test]
983 fn test_process_tomography_reconstructs_identity_channel() {
984 let mut tomography = ProcessTomography::new(1);
988 let identity = QuantumChannel::depolarizing(1, 0.0); tomography
990 .characterize_channel(&identity)
991 .expect("characterization should succeed");
992
993 let chi = tomography
994 .process_matrix
995 .as_ref()
996 .expect("process matrix must be populated");
997 assert_eq!(chi.shape(), [4, 4]);
998 assert!(
999 (chi[[0, 0]] - Complex64::new(1.0, 0.0)).norm() < 1e-9,
1000 "chi_00 should be 1, got {}",
1001 chi[[0, 0]]
1002 );
1003 for m in 0..4 {
1004 for n in 0..4 {
1005 if (m, n) != (0, 0) {
1006 assert!(
1007 chi[[m, n]].norm() < 1e-9,
1008 "chi[{m},{n}] should be ~0, got {}",
1009 chi[[m, n]]
1010 );
1011 }
1012 }
1013 }
1014 }
1015
1016 #[test]
1017 fn test_process_tomography_reconstructs_bit_flip_channel() {
1018 let mut x_op: Array2<Complex64> = Array2::zeros((2, 2));
1021 x_op[[0, 1]] = Complex64::new(1.0, 0.0);
1022 x_op[[1, 0]] = Complex64::new(1.0, 0.0);
1023 let x_channel = QuantumChannel {
1024 kraus_operators: vec![x_op],
1025 name: "X".to_string(),
1026 };
1027
1028 let mut tomography = ProcessTomography::new(1);
1029 tomography
1030 .characterize_channel(&x_channel)
1031 .expect("characterization should succeed");
1032
1033 let chi = tomography
1034 .process_matrix
1035 .as_ref()
1036 .expect("process matrix must be populated");
1037 assert!(
1038 (chi[[1, 1]] - Complex64::new(1.0, 0.0)).norm() < 1e-9,
1039 "chi_11 should be 1 for the X channel, got {}",
1040 chi[[1, 1]]
1041 );
1042 assert!(
1043 chi[[0, 0]].norm() < 1e-9,
1044 "chi_00 should be ~0 for X channel"
1045 );
1046 }
1047}