quantrs2_sim/noise_advanced.rs
1//! Advanced noise models beyond simple single-qubit depolarising channels.
2//!
3//! Provides two-qubit correlated noise, crosstalk, leakage to higher energy
4//! levels, and time-dependent amplitude / phase damping noise channels for
5//! realistic device simulation.
6
7#![allow(clippy::needless_range_loop)]
8
9use scirs2_core::Complex64;
10use std::f64::consts::PI;
11use std::time::Duration;
12
13use quantrs2_core::error::QuantRS2Result;
14use quantrs2_core::qubit::QubitId;
15
16use crate::noise::{NoiseChannel, NoiseChannelType, NoiseModel};
17
18/// Multiply two single-qubit (2x2) operators stored as flattened row-major
19/// `Complex64` slices and return the product `a * b` (also flattened 2x2).
20///
21/// Index layout: element `[i][j]` lives at `2 * i + j`.
22fn matmul_2x2(a: &[Complex64], b: &[Complex64]) -> Vec<Complex64> {
23 let mut out = vec![Complex64::new(0.0, 0.0); 4];
24 for i in 0..2 {
25 for j in 0..2 {
26 let mut acc = Complex64::new(0.0, 0.0);
27 for k in 0..2 {
28 acc += a[2 * i + k] * b[2 * k + j];
29 }
30 out[2 * i + j] = acc;
31 }
32 }
33 out
34}
35
36/// Tensor (Kronecker) product of two single-qubit (2x2) operators stored as
37/// flattened row-major `Complex64` slices, returning the resulting 4x4 operator
38/// as a flattened row-major slice (16 elements).
39///
40/// For `A ⊗ B`, the element at `[2*r1 + r2][2*c1 + c2]` equals
41/// `A[r1][c1] * B[r2][c2]`. In the flattened 4x4 layout the destination index is
42/// `4 * (2*r1 + r2) + (2*c1 + c2)`.
43fn tensor_2x2(a: &[Complex64], b: &[Complex64]) -> Vec<Complex64> {
44 let mut out = vec![Complex64::new(0.0, 0.0); 16];
45 for r1 in 0..2 {
46 for c1 in 0..2 {
47 let a_val = a[2 * r1 + c1];
48 for r2 in 0..2 {
49 for c2 in 0..2 {
50 let row = 2 * r1 + r2;
51 let col = 2 * c1 + c2;
52 out[4 * row + col] = a_val * b[2 * r2 + c2];
53 }
54 }
55 }
56 }
57 out
58}
59
60/// The four single-qubit Pauli matrices `{I, X, Y, Z}` as flattened row-major
61/// 2x2 `Complex64` slices.
62fn single_qubit_paulis() -> [Vec<Complex64>; 4] {
63 let zero = Complex64::new(0.0, 0.0);
64 let one = Complex64::new(1.0, 0.0);
65 let i_unit = Complex64::new(0.0, 1.0);
66 [
67 // I
68 vec![one, zero, zero, one],
69 // X
70 vec![zero, one, one, zero],
71 // Y
72 vec![zero, -i_unit, i_unit, zero],
73 // Z
74 vec![one, zero, zero, -one],
75 ]
76}
77
78/// Two-qubit depolarizing noise channel
79#[derive(Debug, Clone)]
80pub struct TwoQubitDepolarizingChannel {
81 /// First qubit
82 pub qubit1: QubitId,
83
84 /// Second qubit
85 pub qubit2: QubitId,
86
87 /// Probability of error
88 pub probability: f64,
89}
90
91impl NoiseChannel for TwoQubitDepolarizingChannel {
92 fn name(&self) -> &'static str {
93 "TwoQubitDepolarizing"
94 }
95
96 fn qubits(&self) -> Vec<QubitId> {
97 vec![self.qubit1, self.qubit2]
98 }
99
100 fn apply_to_statevector(&self, state: &mut [Complex64]) -> QuantRS2Result<()> {
101 let q1_idx = self.qubit1.id() as usize;
102 #[allow(clippy::needless_range_loop)]
103 let q2_idx = self.qubit2.id() as usize;
104 let dim = state.len();
105
106 // Apply two-qubit depolarizing noise with probability p
107 if fastrand::f64() < self.probability {
108 // Choose randomly between 15 possible Pauli errors (excluding I⊗I)
109 let error_type = fastrand::u32(..) % 15;
110
111 // Create a copy of the state to read from
112 let state_copy = state.to_vec();
113
114 match error_type {
115 0 => {
116 // X⊗I
117 for i in 0..dim {
118 let flipped_i = i ^ (1 << q1_idx);
119 state[i] = state_copy[flipped_i];
120 }
121 }
122 1 => {
123 // I⊗X
124 for i in 0..dim {
125 let flipped_i = i ^ (1 << q2_idx);
126 state[i] = state_copy[flipped_i];
127 }
128 }
129 2 => {
130 // X⊗X
131 for i in 0..dim {
132 let flipped_i = i ^ (1 << q1_idx) ^ (1 << q2_idx);
133 state[i] = state_copy[flipped_i];
134 }
135 }
136 3 => {
137 // Y⊗I
138 for i in 0..dim {
139 let flipped_i = i ^ (1 << q1_idx);
140 let phase = if (i >> q1_idx) & 1 == 1 { 1.0 } else { -1.0 };
141 state[i] = state_copy[flipped_i] * Complex64::new(0.0, phase);
142 }
143 }
144 4 => {
145 // I⊗Y
146 for i in 0..dim {
147 let flipped_i = i ^ (1 << q2_idx);
148 let phase = if (i >> q2_idx) & 1 == 1 { 1.0 } else { -1.0 };
149 state[i] = state_copy[flipped_i] * Complex64::new(0.0, phase);
150 }
151 }
152 5 => {
153 // Y⊗Y
154 for i in 0..dim {
155 let flipped_i = i ^ (1 << q1_idx) ^ (1 << q2_idx);
156 let phase1 = if (i >> q1_idx) & 1 == 1 { 1.0 } else { -1.0 };
157 let phase2 = if (i >> q2_idx) & 1 == 1 { 1.0 } else { -1.0 };
158 state[i] = state_copy[flipped_i] * Complex64::new(0.0, phase1 * phase2);
159 }
160 }
161 6 => {
162 // Z⊗I
163 for i in 0..dim {
164 if (i >> q1_idx) & 1 == 1 {
165 state[i] = -state_copy[i];
166 }
167 }
168 }
169 7 => {
170 // I⊗Z
171 for i in 0..dim {
172 if (i >> q2_idx) & 1 == 1 {
173 state[i] = -state_copy[i];
174 }
175 }
176 }
177 8 => {
178 // Z⊗Z
179 for i in 0..dim {
180 let parity = ((i >> q1_idx) & 1) ^ ((i >> q2_idx) & 1);
181 if parity == 1 {
182 state[i] = -state_copy[i];
183 }
184 }
185 }
186 9 => {
187 // X⊗Y
188 for i in 0..dim {
189 let flipped_i = i ^ (1 << q1_idx) ^ (1 << q2_idx);
190 let phase = if (i >> q2_idx) & 1 == 1 { 1.0 } else { -1.0 };
191 state[i] = state_copy[flipped_i] * Complex64::new(0.0, phase);
192 }
193 }
194 10 => {
195 // X⊗Z
196 for i in 0..dim {
197 let flipped_i = i ^ (1 << q1_idx);
198 if (flipped_i >> q2_idx) & 1 == 1 {
199 state[i] = -state_copy[flipped_i];
200 } else {
201 state[i] = state_copy[flipped_i];
202 }
203 }
204 }
205 11 => {
206 // Y⊗X
207 for i in 0..dim {
208 let flipped_i = i ^ (1 << q1_idx) ^ (1 << q2_idx);
209 let phase = if (i >> q1_idx) & 1 == 1 { 1.0 } else { -1.0 };
210 state[i] = state_copy[flipped_i] * Complex64::new(0.0, phase);
211 }
212 }
213 12 => {
214 // Y⊗Z
215 for i in 0..dim {
216 let flipped_i = i ^ (1 << q1_idx);
217 let phase = if ((i >> q1_idx) & 1 == 1) ^ ((i >> q2_idx) & 1 == 1) {
218 Complex64::new(0.0, -1.0)
219 } else {
220 Complex64::new(0.0, 1.0)
221 };
222 state[i] = state_copy[flipped_i] * phase;
223 }
224 }
225 13 => {
226 // Z⊗X
227 for i in 0..dim {
228 let flipped_i = i ^ (1 << q2_idx);
229 if (i >> q1_idx) & 1 == 1 {
230 state[i] = -state_copy[flipped_i];
231 } else {
232 state[i] = state_copy[flipped_i];
233 }
234 }
235 }
236 14 => {
237 // Z⊗Y
238 for i in 0..dim {
239 let flipped_i = i ^ (1 << q2_idx);
240 let phase = if ((i >> q1_idx) & 1 == 1) ^ ((i >> q2_idx) & 1 == 1) {
241 Complex64::new(0.0, -1.0)
242 } else {
243 Complex64::new(0.0, 1.0)
244 };
245 state[i] = state_copy[flipped_i] * phase;
246 }
247 }
248 _ => unreachable!(),
249 }
250 }
251
252 Ok(())
253 }
254
255 fn kraus_operators(&self) -> Vec<Vec<Complex64>> {
256 // Two-qubit depolarizing channel:
257 // rho -> (1 - p) rho + (p / 15) * sum_{(a,b) != (I,I)} (P_a x P_b) rho (P_a x P_b)^dagger
258 // This yields 16 Kraus operators, each a flattened 4x4 matrix (16 elems):
259 // K_0 = sqrt(1 - p) * (I x I)
260 // K_1..15 = sqrt(p / 15) * (P_a x P_b) for the 15 non-identity Pauli pairs.
261 // Trace preservation: sum_k K_k^dagger K_k
262 // = (1 - p) I_4 + (p / 15) * 15 * I_4 = I_4 (each Pauli is unitary).
263 let p = self.probability;
264 let sqrt_1_minus_p = Complex64::new((1.0 - p).sqrt(), 0.0);
265 let sqrt_p_15 = Complex64::new((p / 15.0).sqrt(), 0.0);
266
267 let paulis = single_qubit_paulis();
268
269 let mut operators = Vec::with_capacity(16);
270 for (a_idx, pauli_a) in paulis.iter().enumerate() {
271 for (b_idx, pauli_b) in paulis.iter().enumerate() {
272 let mut op = tensor_2x2(pauli_a, pauli_b);
273 // (a_idx, b_idx) == (0, 0) is the identity pair I x I.
274 let scale = if a_idx == 0 && b_idx == 0 {
275 sqrt_1_minus_p
276 } else {
277 sqrt_p_15
278 };
279 for value in &mut op {
280 *value *= scale;
281 }
282 operators.push(op);
283 }
284 }
285
286 operators
287 }
288
289 fn probability(&self) -> f64 {
290 self.probability
291 }
292}
293
294/// Thermal relaxation noise channel (combination of T1 and T2 effects)
295#[derive(Debug, Clone)]
296pub struct ThermalRelaxationChannel {
297 /// Target qubit
298 pub target: QubitId,
299
300 /// T1 relaxation time (seconds)
301 pub t1: f64,
302
303 /// T2 pure dephasing time (seconds)
304 pub t2: f64,
305
306 /// Gate time (seconds)
307 pub gate_time: f64,
308
309 /// Excited state population at thermal equilibrium (0.0 to 1.0)
310 pub excited_state_population: f64,
311}
312
313impl NoiseChannel for ThermalRelaxationChannel {
314 fn name(&self) -> &'static str {
315 "ThermalRelaxation"
316 }
317
318 fn qubits(&self) -> Vec<QubitId> {
319 vec![self.target]
320 }
321
322 fn apply_to_statevector(&self, state: &mut [Complex64]) -> QuantRS2Result<()> {
323 let target_idx = self.target.id() as usize;
324 let dim = state.len();
325
326 // Calculate relaxation and dephasing probabilities
327 let p_reset = 1.0 - (-self.gate_time / self.t1).exp();
328 let p_phase = 0.5 * (1.0 - (-self.gate_time / self.t2).exp());
329
330 // Create a copy of the state for reading
331 let state_copy = state.to_vec();
332
333 // Apply thermal relaxation
334 // First apply amplitude damping (relaxation)
335 for i in 0..dim {
336 if (i >> target_idx) & 1 == 1 {
337 // This basis state has the target qubit in |1⟩
338 let base_idx = i & !(1 << target_idx); // Flip the target bit to 0
339
340 // Apply relaxation with probability p_reset
341 if fastrand::f64() < p_reset {
342 // With probability (1-p_eq), collapse to |0⟩ state
343 // With probability p_eq, collapse to |1⟩ state (thermal equilibrium)
344 if fastrand::f64() < self.excited_state_population {
345 // Stay in |1⟩ due to thermal excitation
346 state[i] = state_copy[i];
347 } else {
348 // Collapse to |0⟩
349 state[base_idx] += state_copy[i];
350 state[i] = Complex64::new(0.0, 0.0);
351 }
352 } else {
353 // No relaxation occurs, but apply sqrt(1-p) factor
354 state[i] = state_copy[i] * Complex64::new((1.0 - p_reset).sqrt(), 0.0);
355 }
356 }
357 }
358
359 // Then apply phase damping (dephasing on top of amplitude damping)
360 for i in 0..dim {
361 if (i >> target_idx) & 1 == 1 {
362 // Apply additional pure dephasing
363 if fastrand::f64() < p_phase {
364 // Random phase
365 state[i] *= Complex64::new(-1.0, 0.0); // Apply phase flip
366 }
367 }
368 }
369
370 // Normalize the state
371 NoiseChannelType::normalize_state(state);
372
373 Ok(())
374 }
375
376 fn kraus_operators(&self) -> Vec<Vec<Complex64>> {
377 // Thermal relaxation = generalized amplitude damping (T1) composed with
378 // pure phase damping (the residual T2 dephasing). Both are CPTP, so their
379 // composition is CPTP and trace-preserving.
380 //
381 // Generalized amplitude damping with gamma = p_reset and excited-state
382 // population p_e (all 2x2, flattened row-major as [a00, a01, a10, a11]):
383 // A0 = sqrt(p_e) * [[1, 0], [0, sqrt(1 - gamma)]]
384 // A1 = sqrt(p_e) * [[0, sqrt(gamma)], [0, 0]]
385 // A2 = sqrt(1 - p_e) * [[sqrt(1 - gamma), 0], [0, 1]]
386 // A3 = sqrt(1 - p_e) * [[0, 0], [sqrt(gamma), 0]]
387 // Phase damping with gamma_phi = 2 * p_phase (clamped to [0, 1]):
388 // P0 = [[1, 0], [0, sqrt(1 - gamma_phi)]]
389 // P1 = [[0, 0], [0, sqrt(gamma_phi)]]
390 // Final Kraus set = { P_i * A_j } (8 operators).
391 // Trace check: sum_{i,j} (P_i A_j)^dagger (P_i A_j)
392 // = sum_j A_j^dagger (sum_i P_i^dagger P_i) A_j
393 // = sum_j A_j^dagger A_j = I_2.
394 let p_reset = 1.0 - (-self.gate_time / self.t1).exp();
395 let p_phase = 0.5 * (1.0 - (-self.gate_time / self.t2).exp());
396
397 let gamma = p_reset.clamp(0.0, 1.0);
398 let gamma_phi = (2.0 * p_phase).clamp(0.0, 1.0);
399 let p_e = self.excited_state_population.clamp(0.0, 1.0);
400
401 let zero = Complex64::new(0.0, 0.0);
402 let sqrt_p_e = Complex64::new(p_e.sqrt(), 0.0);
403 let sqrt_1_minus_p_e = Complex64::new((1.0 - p_e).sqrt(), 0.0);
404 let sqrt_gamma = Complex64::new(gamma.sqrt(), 0.0);
405 let sqrt_1_minus_gamma = Complex64::new((1.0 - gamma).sqrt(), 0.0);
406 let sqrt_gamma_phi = Complex64::new(gamma_phi.sqrt(), 0.0);
407 let sqrt_1_minus_gamma_phi = Complex64::new((1.0 - gamma_phi).sqrt(), 0.0);
408 let one = Complex64::new(1.0, 0.0);
409
410 // Generalized amplitude damping operators.
411 let a0 = vec![sqrt_p_e, zero, zero, sqrt_p_e * sqrt_1_minus_gamma];
412 let a1 = vec![zero, sqrt_p_e * sqrt_gamma, zero, zero];
413 let a2 = vec![
414 sqrt_1_minus_p_e * sqrt_1_minus_gamma,
415 zero,
416 zero,
417 sqrt_1_minus_p_e,
418 ];
419 let a3 = vec![zero, zero, sqrt_1_minus_p_e * sqrt_gamma, zero];
420 let amplitude_ops = [a0, a1, a2, a3];
421
422 // Pure phase-damping operators.
423 let p0 = vec![one, zero, zero, sqrt_1_minus_gamma_phi];
424 let p1 = vec![zero, zero, zero, sqrt_gamma_phi];
425 let phase_ops = [p0, p1];
426
427 let mut operators = Vec::with_capacity(amplitude_ops.len() * phase_ops.len());
428 for phase_op in &phase_ops {
429 for amplitude_op in &litude_ops {
430 operators.push(matmul_2x2(phase_op, amplitude_op));
431 }
432 }
433
434 operators
435 }
436
437 fn probability(&self) -> f64 {
438 // Return the combined probability of an error occurring
439 let p_reset = 1.0 - (-self.gate_time / self.t1).exp();
440 let p_phase = 0.5 * (1.0 - (-self.gate_time / self.t2).exp());
441 p_reset + p_phase - p_reset * p_phase // Combined probability
442 }
443}
444
445/// Crosstalk noise channel for adjacent qubits
446#[derive(Debug, Clone)]
447pub struct CrosstalkChannel {
448 /// Primary qubit
449 pub primary: QubitId,
450
451 /// Neighbor qubit
452 pub neighbor: QubitId,
453
454 /// Crosstalk strength (0.0 to 1.0)
455 pub strength: f64,
456}
457
458impl NoiseChannel for CrosstalkChannel {
459 fn name(&self) -> &'static str {
460 "Crosstalk"
461 }
462
463 fn qubits(&self) -> Vec<QubitId> {
464 vec![self.primary, self.neighbor]
465 }
466
467 fn apply_to_statevector(&self, state: &mut [Complex64]) -> QuantRS2Result<()> {
468 let primary_idx = self.primary.id() as usize;
469 let neighbor_idx = self.neighbor.id() as usize;
470 let dim = state.len();
471
472 // Apply crosstalk with probability based on strength
473 if fastrand::f64() < self.strength {
474 // Create a copy of the state for reading
475 let state_copy = state.to_vec();
476
477 // Randomly select an effect (simplified model):
478 // 1. ZZ interaction
479 // 2. Neighbor rotation
480 let effect = fastrand::u32(..) % 2;
481
482 match effect {
483 0 => {
484 // ZZ interaction
485 for i in 0..dim {
486 let parity = ((i >> primary_idx) & 1) ^ ((i >> neighbor_idx) & 1);
487 if parity == 1 {
488 // Apply phase shift if qubits have different parity
489 let phase = fastrand::f64() * PI;
490 state[i] *= Complex64::new(phase.cos(), phase.sin());
491 }
492 }
493 }
494 1 => {
495 // Small rotation on neighbor when primary qubit is |1⟩
496 for i in 0..dim {
497 if (i >> primary_idx) & 1 == 1 {
498 // Primary qubit is |1⟩, apply partial X rotation to neighbor
499 let neighbor_bit = (i >> neighbor_idx) & 1;
500 let flipped_i = i ^ (1 << neighbor_idx);
501
502 // Small, random amplitude swap
503 let theta: f64 = fastrand::f64() * 0.2; // Small angle
504 let cos_theta = theta.cos();
505 let sin_theta = theta.sin();
506
507 let amp_original = state_copy[i];
508 let amp_flipped = state_copy[flipped_i];
509
510 if neighbor_bit == 0 {
511 state[i] = amp_original * Complex64::new(cos_theta, 0.0)
512 + amp_flipped * Complex64::new(sin_theta, 0.0);
513 state[flipped_i] = amp_original * Complex64::new(-sin_theta, 0.0)
514 + amp_flipped * Complex64::new(cos_theta, 0.0);
515 } else {
516 state[i] = amp_original * Complex64::new(cos_theta, 0.0)
517 - amp_flipped * Complex64::new(sin_theta, 0.0);
518 state[flipped_i] = amp_original * Complex64::new(sin_theta, 0.0)
519 + amp_flipped * Complex64::new(cos_theta, 0.0);
520 }
521 }
522 }
523 }
524 _ => unreachable!(),
525 }
526 }
527
528 // Normalize the state
529 NoiseChannelType::normalize_state(state);
530
531 Ok(())
532 }
533
534 fn kraus_operators(&self) -> Vec<Vec<Complex64>> {
535 // The dominant, always-on crosstalk between two coupled qubits is the
536 // coherent ZZ interaction U_ZZ(phi) = exp(-i * phi/2 * (Z x Z)), with the
537 // coupling angle proportional to the crosstalk strength (phi = strength * PI).
538 // Because this is a coherent (unitary) process, the channel is described by
539 // a SINGLE Kraus operator K = U_ZZ, which is trivially trace-preserving
540 // (K^dagger K = I_4 since U_ZZ is unitary).
541 //
542 // Z x Z is diagonal in the computational basis (|00>, |01>, |10>, |11>)
543 // with eigenvalues (+1, -1, -1, +1), so
544 // U_ZZ = diag(e^{-i phi/2}, e^{+i phi/2}, e^{+i phi/2}, e^{-i phi/2}).
545 // The result is returned as a flattened row-major 4x4 matrix (16 elems).
546 let phi = self.strength * PI;
547 let half = phi / 2.0;
548 let zero = Complex64::new(0.0, 0.0);
549 let phase_neg = Complex64::new(half.cos(), -half.sin()); // e^{-i phi/2}
550 let phase_pos = Complex64::new(half.cos(), half.sin()); // e^{+i phi/2}
551
552 // Diagonal 4x4 with the ZZ phases on the diagonal.
553 let mut op = vec![zero; 16];
554 op[0] = phase_neg; // |00>
555 op[5] = phase_pos; // |01>
556 op[10] = phase_pos; // |10>
557 op[15] = phase_neg; // |11>
558
559 vec![op]
560 }
561
562 fn probability(&self) -> f64 {
563 self.strength
564 }
565}
566
567/// Extension to `NoiseChannelType` to include advanced noise channels
568#[derive(Debug, Clone)]
569pub enum AdvancedNoiseChannelType {
570 /// Base noise channel types
571 Base(NoiseChannelType),
572
573 /// Two-qubit depolarizing channel
574 TwoQubitDepolarizing(TwoQubitDepolarizingChannel),
575
576 /// Thermal relaxation channel
577 ThermalRelaxation(ThermalRelaxationChannel),
578
579 /// Crosstalk channel
580 Crosstalk(CrosstalkChannel),
581}
582
583impl AdvancedNoiseChannelType {
584 /// Get the name of the noise channel
585 #[must_use]
586 pub fn name(&self) -> &'static str {
587 match self {
588 Self::Base(ch) => ch.name(),
589 Self::TwoQubitDepolarizing(ch) => ch.name(),
590 Self::ThermalRelaxation(ch) => ch.name(),
591 Self::Crosstalk(ch) => ch.name(),
592 }
593 }
594
595 /// Get the qubits this channel affects
596 #[must_use]
597 pub fn qubits(&self) -> Vec<QubitId> {
598 match self {
599 Self::Base(ch) => ch.qubits(),
600 Self::TwoQubitDepolarizing(ch) => ch.qubits(),
601 Self::ThermalRelaxation(ch) => ch.qubits(),
602 Self::Crosstalk(ch) => ch.qubits(),
603 }
604 }
605
606 /// Apply the noise channel to a state vector
607 pub fn apply_to_statevector(&self, state: &mut [Complex64]) -> QuantRS2Result<()> {
608 match self {
609 Self::Base(ch) => ch.apply_to_statevector(state),
610 Self::TwoQubitDepolarizing(ch) => ch.apply_to_statevector(state),
611 Self::ThermalRelaxation(ch) => ch.apply_to_statevector(state),
612 Self::Crosstalk(ch) => ch.apply_to_statevector(state),
613 }
614 }
615
616 /// Get the probability of the noise occurring
617 #[must_use]
618 pub fn probability(&self) -> f64 {
619 match self {
620 Self::Base(ch) => ch.probability(),
621 Self::TwoQubitDepolarizing(ch) => ch.probability(),
622 Self::ThermalRelaxation(ch) => ch.probability(),
623 Self::Crosstalk(ch) => ch.probability(),
624 }
625 }
626}
627
628/// Advanced noise model that supports the new noise channel types
629#[derive(Debug, Clone)]
630pub struct AdvancedNoiseModel {
631 /// List of noise channels
632 pub channels: Vec<AdvancedNoiseChannelType>,
633
634 /// Whether the noise is applied after each gate
635 pub per_gate: bool,
636}
637
638impl AdvancedNoiseModel {
639 /// Create a new empty noise model
640 #[must_use]
641 pub const fn new(per_gate: bool) -> Self {
642 Self {
643 channels: Vec::new(),
644 per_gate,
645 }
646 }
647
648 /// Add a basic noise channel to the model
649 pub fn add_base_channel(&mut self, channel: NoiseChannelType) -> &mut Self {
650 self.channels.push(AdvancedNoiseChannelType::Base(channel));
651 self
652 }
653
654 /// Add a two-qubit depolarizing noise channel to the model
655 pub fn add_two_qubit_depolarizing(
656 &mut self,
657 channel: TwoQubitDepolarizingChannel,
658 ) -> &mut Self {
659 self.channels
660 .push(AdvancedNoiseChannelType::TwoQubitDepolarizing(channel));
661 self
662 }
663
664 /// Add a thermal relaxation noise channel to the model
665 pub fn add_thermal_relaxation(&mut self, channel: ThermalRelaxationChannel) -> &mut Self {
666 self.channels
667 .push(AdvancedNoiseChannelType::ThermalRelaxation(channel));
668 self
669 }
670
671 /// Add a crosstalk noise channel to the model
672 pub fn add_crosstalk(&mut self, channel: CrosstalkChannel) -> &mut Self {
673 self.channels
674 .push(AdvancedNoiseChannelType::Crosstalk(channel));
675 self
676 }
677
678 /// Apply all noise channels to a state vector
679 pub fn apply_to_statevector(&self, state: &mut [Complex64]) -> QuantRS2Result<()> {
680 for channel in &self.channels {
681 channel.apply_to_statevector(state)?;
682 }
683
684 // Normalize the state vector after applying all noise channels
685 NoiseChannelType::normalize_state(state);
686
687 Ok(())
688 }
689
690 /// Get the total number of channels
691 #[must_use]
692 pub fn num_channels(&self) -> usize {
693 self.channels.len()
694 }
695
696 /// Convert to basic noise model (for backward compatibility)
697 #[must_use]
698 pub fn to_basic_model(&self) -> NoiseModel {
699 let mut model = NoiseModel::new(self.per_gate);
700
701 for channel in &self.channels {
702 if let AdvancedNoiseChannelType::Base(ch) = channel {
703 model.channels.push(ch.clone());
704 }
705 }
706
707 model
708 }
709}
710
711impl Default for AdvancedNoiseModel {
712 fn default() -> Self {
713 Self::new(true)
714 }
715}
716
717/// Builder for realistic device noise models
718pub struct RealisticNoiseModelBuilder {
719 model: AdvancedNoiseModel,
720}
721
722impl RealisticNoiseModelBuilder {
723 /// Create a new noise model builder
724 #[must_use]
725 pub const fn new(per_gate: bool) -> Self {
726 Self {
727 model: AdvancedNoiseModel::new(per_gate),
728 }
729 }
730
731 /// Add realistic IBM Quantum device noise parameters
732 #[must_use]
733 pub fn with_ibm_device_noise(mut self, qubits: &[QubitId], device_name: &str) -> Self {
734 match device_name {
735 "ibmq_lima" | "ibmq_belem" | "ibmq_quito" => {
736 // 5-qubit IBM Quantum Falcon processors
737 // Parameters are approximate and based on typical values
738
739 // Relaxation and dephasing times
740 let t1_values = [115e-6, 100e-6, 120e-6, 105e-6, 110e-6]; // ~100 microseconds
741 let t2_values = [95e-6, 80e-6, 100e-6, 90e-6, 85e-6]; // ~90 microseconds
742
743 // Single-qubit gates
744 let gate_time_1q = 35e-9; // 35 nanoseconds
745 let gate_error_1q = 0.001; // 0.1% error rate
746
747 // Two-qubit gates (CNOT)
748 // let _gate_time_2q = 300e-9; // 300 nanoseconds
749 let gate_error_2q = 0.01; // 1% error rate
750
751 // Readout errors
752 let readout_error = 0.025; // 2.5% error
753
754 // Add individual qubit noise
755 for (i, &qubit) in qubits.iter().enumerate().take(5) {
756 let t1 = t1_values[i % 5];
757 let t2 = t2_values[i % 5];
758
759 // Add thermal relaxation
760 self.model.add_thermal_relaxation(ThermalRelaxationChannel {
761 target: qubit,
762 t1,
763 t2,
764 gate_time: gate_time_1q,
765 excited_state_population: 0.01, // ~1% thermal excitation
766 });
767
768 // Add depolarizing noise for single-qubit gates
769 self.model.add_base_channel(NoiseChannelType::Depolarizing(
770 crate::noise::DepolarizingChannel {
771 target: qubit,
772 probability: gate_error_1q,
773 },
774 ));
775
776 // Add readout error as a bit flip channel
777 self.model.add_base_channel(NoiseChannelType::BitFlip(
778 crate::noise::BitFlipChannel {
779 target: qubit,
780 probability: readout_error,
781 },
782 ));
783 }
784
785 // Add two-qubit gate noise (for nearest-neighbor connectivity)
786 for i in 0..qubits.len().saturating_sub(1) {
787 let q1 = qubits[i];
788 let q2 = qubits[i + 1];
789
790 // Add two-qubit depolarizing noise
791 self.model
792 .add_two_qubit_depolarizing(TwoQubitDepolarizingChannel {
793 qubit1: q1,
794 qubit2: q2,
795 probability: gate_error_2q,
796 });
797
798 // Add crosstalk between adjacent qubits
799 self.model.add_crosstalk(CrosstalkChannel {
800 primary: q1,
801 neighbor: q2,
802 strength: 0.003, // 0.3% crosstalk
803 });
804 }
805 }
806 "ibmq_bogota" | "ibmq_santiago" | "ibmq_casablanca" => {
807 // 5-qubit IBM Quantum Falcon processors (newer)
808 // Parameters are approximate and based on typical values
809
810 // Relaxation and dephasing times
811 let t1_values = [140e-6, 130e-6, 145e-6, 135e-6, 150e-6]; // ~140 microseconds
812 let t2_values = [120e-6, 110e-6, 125e-6, 115e-6, 130e-6]; // ~120 microseconds
813
814 // Single-qubit gates
815 let gate_time_1q = 30e-9; // 30 nanoseconds
816 let gate_error_1q = 0.0005; // 0.05% error rate
817
818 // Two-qubit gates (CNOT)
819 // let _gate_time_2q = 250e-9; // 250 nanoseconds
820 let gate_error_2q = 0.008; // 0.8% error rate
821
822 // Readout errors
823 let readout_error = 0.02; // 2% error
824
825 // Add individual qubit noise
826 for (i, &qubit) in qubits.iter().enumerate().take(5) {
827 let t1 = t1_values[i % 5];
828 let t2 = t2_values[i % 5];
829
830 // Add thermal relaxation
831 self.model.add_thermal_relaxation(ThermalRelaxationChannel {
832 target: qubit,
833 t1,
834 t2,
835 gate_time: gate_time_1q,
836 excited_state_population: 0.008, // ~0.8% thermal excitation
837 });
838
839 // Add depolarizing noise for single-qubit gates
840 self.model.add_base_channel(NoiseChannelType::Depolarizing(
841 crate::noise::DepolarizingChannel {
842 target: qubit,
843 probability: gate_error_1q,
844 },
845 ));
846
847 // Add readout error as a bit flip channel
848 self.model.add_base_channel(NoiseChannelType::BitFlip(
849 crate::noise::BitFlipChannel {
850 target: qubit,
851 probability: readout_error,
852 },
853 ));
854 }
855
856 // Add two-qubit gate noise (for nearest-neighbor connectivity)
857 for i in 0..qubits.len().saturating_sub(1) {
858 let q1 = qubits[i];
859 let q2 = qubits[i + 1];
860
861 // Add two-qubit depolarizing noise
862 self.model
863 .add_two_qubit_depolarizing(TwoQubitDepolarizingChannel {
864 qubit1: q1,
865 qubit2: q2,
866 probability: gate_error_2q,
867 });
868
869 // Add crosstalk between adjacent qubits
870 self.model.add_crosstalk(CrosstalkChannel {
871 primary: q1,
872 neighbor: q2,
873 strength: 0.002, // 0.2% crosstalk
874 });
875 }
876 }
877 "ibm_cairo" | "ibm_hanoi" | "ibm_auckland" => {
878 // 27-qubit IBM Quantum Falcon processors
879 // Parameters are approximate and based on typical values
880
881 // Relaxation and dephasing times (average values)
882 let t1 = 130e-6; // 130 microseconds
883 let t2 = 100e-6; // 100 microseconds
884
885 // Single-qubit gates
886 let gate_time_1q = 35e-9; // 35 nanoseconds
887 let gate_error_1q = 0.0004; // 0.04% error rate
888
889 // Two-qubit gates (CNOT)
890 // let _gate_time_2q = 275e-9; // 275 nanoseconds
891 let gate_error_2q = 0.007; // 0.7% error rate
892
893 // Readout errors
894 let readout_error = 0.018; // 1.8% error
895
896 // Add individual qubit noise
897 for &qubit in qubits {
898 // Add thermal relaxation
899 self.model.add_thermal_relaxation(ThermalRelaxationChannel {
900 target: qubit,
901 t1,
902 t2,
903 gate_time: gate_time_1q,
904 excited_state_population: 0.007, // ~0.7% thermal excitation
905 });
906
907 // Add depolarizing noise for single-qubit gates
908 self.model.add_base_channel(NoiseChannelType::Depolarizing(
909 crate::noise::DepolarizingChannel {
910 target: qubit,
911 probability: gate_error_1q,
912 },
913 ));
914
915 // Add readout error as a bit flip channel
916 self.model.add_base_channel(NoiseChannelType::BitFlip(
917 crate::noise::BitFlipChannel {
918 target: qubit,
919 probability: readout_error,
920 },
921 ));
922 }
923
924 // Add two-qubit gate noise (for nearest-neighbor connectivity)
925 for i in 0..qubits.len().saturating_sub(1) {
926 let q1 = qubits[i];
927 let q2 = qubits[i + 1];
928
929 // Add two-qubit depolarizing noise
930 self.model
931 .add_two_qubit_depolarizing(TwoQubitDepolarizingChannel {
932 qubit1: q1,
933 qubit2: q2,
934 probability: gate_error_2q,
935 });
936
937 // Add crosstalk between adjacent qubits
938 self.model.add_crosstalk(CrosstalkChannel {
939 primary: q1,
940 neighbor: q2,
941 strength: 0.0015, // 0.15% crosstalk
942 });
943 }
944 }
945 "ibm_washington" | "ibm_eagle" => {
946 // 127-qubit IBM Quantum Eagle processors
947 // Parameters are approximate and based on typical values
948
949 // Relaxation and dephasing times (average values)
950 let t1 = 150e-6; // 150 microseconds
951 let t2 = 120e-6; // 120 microseconds
952
953 // Single-qubit gates
954 let gate_time_1q = 30e-9; // 30 nanoseconds
955 let gate_error_1q = 0.0003; // 0.03% error rate
956
957 // Two-qubit gates (CNOT)
958 // let _gate_time_2q = 220e-9; // 220 nanoseconds
959 let gate_error_2q = 0.006; // 0.6% error rate
960
961 // Readout errors
962 let readout_error = 0.015; // 1.5% error
963
964 // Add individual qubit noise
965 for &qubit in qubits {
966 // Add thermal relaxation
967 self.model.add_thermal_relaxation(ThermalRelaxationChannel {
968 target: qubit,
969 t1,
970 t2,
971 gate_time: gate_time_1q,
972 excited_state_population: 0.006, // ~0.6% thermal excitation
973 });
974
975 // Add depolarizing noise for single-qubit gates
976 self.model.add_base_channel(NoiseChannelType::Depolarizing(
977 crate::noise::DepolarizingChannel {
978 target: qubit,
979 probability: gate_error_1q,
980 },
981 ));
982
983 // Add readout error as a bit flip channel
984 self.model.add_base_channel(NoiseChannelType::BitFlip(
985 crate::noise::BitFlipChannel {
986 target: qubit,
987 probability: readout_error,
988 },
989 ));
990 }
991
992 // Add two-qubit gate noise (for nearest-neighbor connectivity)
993 for i in 0..qubits.len().saturating_sub(1) {
994 let q1 = qubits[i];
995 let q2 = qubits[i + 1];
996
997 // Add two-qubit depolarizing noise
998 self.model
999 .add_two_qubit_depolarizing(TwoQubitDepolarizingChannel {
1000 qubit1: q1,
1001 qubit2: q2,
1002 probability: gate_error_2q,
1003 });
1004
1005 // Add crosstalk between adjacent qubits
1006 self.model.add_crosstalk(CrosstalkChannel {
1007 primary: q1,
1008 neighbor: q2,
1009 strength: 0.001, // 0.1% crosstalk
1010 });
1011 }
1012 }
1013 _ => {
1014 // Generic IBM Quantum device (conservative estimates)
1015 // Parameters are approximate and based on typical values
1016
1017 // Relaxation and dephasing times (average values)
1018 let t1 = 100e-6; // 100 microseconds
1019 let t2 = 80e-6; // 80 microseconds
1020
1021 // Single-qubit gates
1022 let gate_time_1q = 40e-9; // 40 nanoseconds
1023 let gate_error_1q = 0.001; // 0.1% error rate
1024
1025 // Two-qubit gates (CNOT)
1026 // let _gate_time_2q = 300e-9; // 300 nanoseconds
1027 let gate_error_2q = 0.01; // 1% error rate
1028
1029 // Readout errors
1030 let readout_error = 0.025; // 2.5% error
1031
1032 // Add individual qubit noise
1033 for &qubit in qubits {
1034 // Add thermal relaxation
1035 self.model.add_thermal_relaxation(ThermalRelaxationChannel {
1036 target: qubit,
1037 t1,
1038 t2,
1039 gate_time: gate_time_1q,
1040 excited_state_population: 0.01, // ~1% thermal excitation
1041 });
1042
1043 // Add depolarizing noise for single-qubit gates
1044 self.model.add_base_channel(NoiseChannelType::Depolarizing(
1045 crate::noise::DepolarizingChannel {
1046 target: qubit,
1047 probability: gate_error_1q,
1048 },
1049 ));
1050
1051 // Add readout error as a bit flip channel
1052 self.model.add_base_channel(NoiseChannelType::BitFlip(
1053 crate::noise::BitFlipChannel {
1054 target: qubit,
1055 probability: readout_error,
1056 },
1057 ));
1058 }
1059
1060 // Add two-qubit gate noise (for nearest-neighbor connectivity)
1061 for i in 0..qubits.len().saturating_sub(1) {
1062 let q1 = qubits[i];
1063 let q2 = qubits[i + 1];
1064
1065 // Add two-qubit depolarizing noise
1066 self.model
1067 .add_two_qubit_depolarizing(TwoQubitDepolarizingChannel {
1068 qubit1: q1,
1069 qubit2: q2,
1070 probability: gate_error_2q,
1071 });
1072
1073 // Add crosstalk between adjacent qubits
1074 self.model.add_crosstalk(CrosstalkChannel {
1075 primary: q1,
1076 neighbor: q2,
1077 strength: 0.003, // 0.3% crosstalk
1078 });
1079 }
1080 }
1081 }
1082
1083 self
1084 }
1085
1086 /// Add realistic Rigetti device noise parameters
1087 #[must_use]
1088 pub fn with_rigetti_device_noise(mut self, qubits: &[QubitId], device_name: &str) -> Self {
1089 match device_name {
1090 "Aspen-M-3" | "Aspen-M-2" => {
1091 // Rigetti Aspen-M series processors
1092 // Parameters are approximate and based on typical values
1093
1094 // Relaxation and dephasing times (average values)
1095 let t1 = 20e-6; // 20 microseconds
1096 let t2 = 15e-6; // 15 microseconds
1097
1098 // Single-qubit gates
1099 let gate_time_1q = 50e-9; // 50 nanoseconds
1100 let gate_error_1q = 0.0015; // 0.15% error rate
1101
1102 // Two-qubit gates (CZ)
1103 // let _gate_time_2q = 220e-9; // 220 nanoseconds
1104 let gate_error_2q = 0.02; // 2% error rate
1105
1106 // Readout errors
1107 let readout_error = 0.03; // 3% error
1108
1109 // Add individual qubit noise
1110 for &qubit in qubits {
1111 // Add thermal relaxation
1112 self.model.add_thermal_relaxation(ThermalRelaxationChannel {
1113 target: qubit,
1114 t1,
1115 t2,
1116 gate_time: gate_time_1q,
1117 excited_state_population: 0.02, // ~2% thermal excitation
1118 });
1119
1120 // Add depolarizing noise for single-qubit gates
1121 self.model.add_base_channel(NoiseChannelType::Depolarizing(
1122 crate::noise::DepolarizingChannel {
1123 target: qubit,
1124 probability: gate_error_1q,
1125 },
1126 ));
1127
1128 // Add readout error as a bit flip channel
1129 self.model.add_base_channel(NoiseChannelType::BitFlip(
1130 crate::noise::BitFlipChannel {
1131 target: qubit,
1132 probability: readout_error,
1133 },
1134 ));
1135 }
1136
1137 // Add two-qubit gate noise (for nearest-neighbor connectivity)
1138 for i in 0..qubits.len().saturating_sub(1) {
1139 let q1 = qubits[i];
1140 let q2 = qubits[i + 1];
1141
1142 // Add two-qubit depolarizing noise
1143 self.model
1144 .add_two_qubit_depolarizing(TwoQubitDepolarizingChannel {
1145 qubit1: q1,
1146 qubit2: q2,
1147 probability: gate_error_2q,
1148 });
1149
1150 // Add crosstalk between adjacent qubits
1151 self.model.add_crosstalk(CrosstalkChannel {
1152 primary: q1,
1153 neighbor: q2,
1154 strength: 0.004, // 0.4% crosstalk
1155 });
1156 }
1157 }
1158 _ => {
1159 // Generic Rigetti device (conservative estimates)
1160 // Parameters are approximate and based on typical values
1161
1162 // Relaxation and dephasing times (average values)
1163 let t1 = 15e-6; // 15 microseconds
1164 let t2 = 12e-6; // 12 microseconds
1165
1166 // Single-qubit gates
1167 let gate_time_1q = 60e-9; // 60 nanoseconds
1168 let gate_error_1q = 0.002; // 0.2% error rate
1169
1170 // Two-qubit gates (CZ)
1171 // let _gate_time_2q = 250e-9; // 250 nanoseconds
1172 let gate_error_2q = 0.025; // 2.5% error rate
1173
1174 // Readout errors
1175 let readout_error = 0.035; // 3.5% error
1176
1177 // Add individual qubit noise
1178 for &qubit in qubits {
1179 // Add thermal relaxation
1180 self.model.add_thermal_relaxation(ThermalRelaxationChannel {
1181 target: qubit,
1182 t1,
1183 t2,
1184 gate_time: gate_time_1q,
1185 excited_state_population: 0.025, // ~2.5% thermal excitation
1186 });
1187
1188 // Add depolarizing noise for single-qubit gates
1189 self.model.add_base_channel(NoiseChannelType::Depolarizing(
1190 crate::noise::DepolarizingChannel {
1191 target: qubit,
1192 probability: gate_error_1q,
1193 },
1194 ));
1195
1196 // Add readout error as a bit flip channel
1197 self.model.add_base_channel(NoiseChannelType::BitFlip(
1198 crate::noise::BitFlipChannel {
1199 target: qubit,
1200 probability: readout_error,
1201 },
1202 ));
1203 }
1204
1205 // Add two-qubit gate noise (for nearest-neighbor connectivity)
1206 for i in 0..qubits.len().saturating_sub(1) {
1207 let q1 = qubits[i];
1208 let q2 = qubits[i + 1];
1209
1210 // Add two-qubit depolarizing noise
1211 self.model
1212 .add_two_qubit_depolarizing(TwoQubitDepolarizingChannel {
1213 qubit1: q1,
1214 qubit2: q2,
1215 probability: gate_error_2q,
1216 });
1217
1218 // Add crosstalk between adjacent qubits
1219 self.model.add_crosstalk(CrosstalkChannel {
1220 primary: q1,
1221 neighbor: q2,
1222 strength: 0.005, // 0.5% crosstalk
1223 });
1224 }
1225 }
1226 }
1227
1228 self
1229 }
1230
1231 /// Add custom thermal relaxation parameters
1232 #[must_use]
1233 pub fn with_custom_thermal_relaxation(
1234 mut self,
1235 qubits: &[QubitId],
1236 t1: Duration,
1237 t2: Duration,
1238 gate_time: Duration,
1239 ) -> Self {
1240 let t1_seconds = t1.as_secs_f64();
1241 let t2_seconds = t2.as_secs_f64();
1242 let gate_time_seconds = gate_time.as_secs_f64();
1243
1244 for &qubit in qubits {
1245 self.model.add_thermal_relaxation(ThermalRelaxationChannel {
1246 target: qubit,
1247 t1: t1_seconds,
1248 t2: t2_seconds,
1249 gate_time: gate_time_seconds,
1250 excited_state_population: 0.01, // Default 1% thermal excitation
1251 });
1252 }
1253
1254 self
1255 }
1256
1257 /// Add custom two-qubit depolarizing noise
1258 #[must_use]
1259 pub fn with_custom_two_qubit_noise(
1260 mut self,
1261 qubit_pairs: &[(QubitId, QubitId)],
1262 probability: f64,
1263 ) -> Self {
1264 for &(q1, q2) in qubit_pairs {
1265 self.model
1266 .add_two_qubit_depolarizing(TwoQubitDepolarizingChannel {
1267 qubit1: q1,
1268 qubit2: q2,
1269 probability,
1270 });
1271 }
1272
1273 self
1274 }
1275
1276 /// Add custom crosstalk noise between pairs of qubits
1277 #[must_use]
1278 pub fn with_custom_crosstalk(
1279 mut self,
1280 qubit_pairs: &[(QubitId, QubitId)],
1281 strength: f64,
1282 ) -> Self {
1283 for &(q1, q2) in qubit_pairs {
1284 self.model.add_crosstalk(CrosstalkChannel {
1285 primary: q1,
1286 neighbor: q2,
1287 strength,
1288 });
1289 }
1290
1291 self
1292 }
1293
1294 /// Build the noise model
1295 #[must_use]
1296 pub fn build(self) -> AdvancedNoiseModel {
1297 self.model
1298 }
1299}
1300
1301#[cfg(test)]
1302mod tests {
1303 use super::*;
1304
1305 /// Accumulate `sum_k K_k^dagger K_k` for a Kraus set whose operators are
1306 /// flattened row-major square matrices, returning the resulting `dim x dim`
1307 /// matrix (flattened row-major). The dimension is inferred from the length of
1308 /// each inner vector (a `dim x dim` matrix has `dim * dim` entries).
1309 fn accumulate_kraus_completeness(kraus: &[Vec<Complex64>]) -> Vec<Complex64> {
1310 assert!(!kraus.is_empty(), "Kraus set must be non-empty");
1311
1312 let len = kraus[0].len();
1313 let dim = (len as f64).sqrt().round() as usize;
1314 assert_eq!(
1315 dim * dim,
1316 len,
1317 "each Kraus operator must be a square matrix"
1318 );
1319
1320 let mut acc = vec![Complex64::new(0.0, 0.0); len];
1321 for op in kraus {
1322 assert_eq!(op.len(), len, "all Kraus operators must share a dimension");
1323 // (K^dagger K)[i][j] = sum_k conj(K[k][i]) * K[k][j]
1324 for i in 0..dim {
1325 for j in 0..dim {
1326 let mut sum = Complex64::new(0.0, 0.0);
1327 for k in 0..dim {
1328 sum += op[k * dim + i].conj() * op[k * dim + j];
1329 }
1330 acc[i * dim + j] += sum;
1331 }
1332 }
1333 }
1334
1335 acc
1336 }
1337
1338 /// Assert that a flattened `dim x dim` matrix equals the identity within
1339 /// `tol`.
1340 fn assert_is_identity(matrix: &[Complex64], tol: f64) {
1341 let len = matrix.len();
1342 let dim = (len as f64).sqrt().round() as usize;
1343 assert_eq!(dim * dim, len, "matrix must be square");
1344
1345 for i in 0..dim {
1346 for j in 0..dim {
1347 let value = matrix[i * dim + j];
1348 let expected = if i == j { 1.0 } else { 0.0 };
1349 assert!(
1350 (value.re - expected).abs() < tol && value.im.abs() < tol,
1351 "element [{i}][{j}] = {value:?} is not {expected} within {tol}"
1352 );
1353 }
1354 }
1355 }
1356
1357 #[test]
1358 fn test_two_qubit_depolarizing_kraus_trace_preserving() {
1359 let channel = TwoQubitDepolarizingChannel {
1360 qubit1: QubitId::new(0),
1361 qubit2: QubitId::new(1),
1362 probability: 0.137,
1363 };
1364
1365 let kraus = channel.kraus_operators();
1366
1367 // Two-qubit depolarizing has exactly 16 Kraus operators (15 Pauli pairs
1368 // plus identity), not the old fabricated 2 one-element vectors.
1369 assert_eq!(kraus.len(), 16, "expected 16 Kraus operators");
1370 for op in &kraus {
1371 assert_eq!(op.len(), 16, "each operator must be a flattened 4x4 matrix");
1372 }
1373
1374 let completeness = accumulate_kraus_completeness(&kraus);
1375 assert_is_identity(&completeness, 1e-10);
1376 }
1377
1378 #[test]
1379 fn test_two_qubit_depolarizing_kraus_edge_probabilities() {
1380 for &probability in &[0.0_f64, 1.0_f64] {
1381 let channel = TwoQubitDepolarizingChannel {
1382 qubit1: QubitId::new(0),
1383 qubit2: QubitId::new(1),
1384 probability,
1385 };
1386 let kraus = channel.kraus_operators();
1387 assert_eq!(kraus.len(), 16);
1388 let completeness = accumulate_kraus_completeness(&kraus);
1389 assert_is_identity(&completeness, 1e-10);
1390 }
1391 }
1392
1393 #[test]
1394 fn test_thermal_relaxation_kraus_trace_preserving() {
1395 let channel = ThermalRelaxationChannel {
1396 target: QubitId::new(0),
1397 t1: 100e-6,
1398 t2: 80e-6,
1399 gate_time: 35e-9,
1400 excited_state_population: 0.03,
1401 };
1402
1403 let kraus = channel.kraus_operators();
1404
1405 // Composition of generalized amplitude damping (4 ops) and phase damping
1406 // (2 ops) yields 8 single-qubit operators, well beyond the old single
1407 // fabricated one-element vector.
1408 assert!(
1409 kraus.len() >= 4,
1410 "expected at least 4 Kraus operators, got {}",
1411 kraus.len()
1412 );
1413 assert_eq!(kraus.len(), 8, "expected 8 composed Kraus operators");
1414 for op in &kraus {
1415 assert_eq!(op.len(), 4, "each operator must be a flattened 2x2 matrix");
1416 }
1417
1418 let completeness = accumulate_kraus_completeness(&kraus);
1419 assert_is_identity(&completeness, 1e-10);
1420 }
1421
1422 #[test]
1423 fn test_thermal_relaxation_kraus_extreme_parameters() {
1424 // Very short T1/T2 relative to gate time drive gamma and gamma_phi toward
1425 // 1; the decomposition must remain trace-preserving and clamped.
1426 let channel = ThermalRelaxationChannel {
1427 target: QubitId::new(0),
1428 t1: 1e-9,
1429 t2: 1e-9,
1430 gate_time: 1e-6,
1431 excited_state_population: 0.5,
1432 };
1433
1434 let kraus = channel.kraus_operators();
1435 assert_eq!(kraus.len(), 8);
1436 let completeness = accumulate_kraus_completeness(&kraus);
1437 assert_is_identity(&completeness, 1e-10);
1438 }
1439
1440 #[test]
1441 fn test_crosstalk_kraus_trace_preserving() {
1442 let channel = CrosstalkChannel {
1443 primary: QubitId::new(0),
1444 neighbor: QubitId::new(1),
1445 strength: 0.3,
1446 };
1447
1448 let kraus = channel.kraus_operators();
1449
1450 // Coherent ZZ crosstalk is a single unitary Kraus operator on the
1451 // two-qubit (4x4) space, not the old fabricated 1-element vector.
1452 assert_eq!(kraus.len(), 1, "coherent crosstalk has a single Kraus op");
1453 assert_eq!(
1454 kraus[0].len(),
1455 16,
1456 "the operator must be a flattened 4x4 unitary"
1457 );
1458
1459 let completeness = accumulate_kraus_completeness(&kraus);
1460 assert_is_identity(&completeness, 1e-10);
1461 }
1462
1463 #[test]
1464 fn test_tensor_2x2_matches_known_pauli_product() {
1465 let paulis = single_qubit_paulis();
1466 // Z x X should map |0>|+> style basis correctly; verify the explicit
1467 // 4x4 layout against a hand-computed reference.
1468 let z = &paulis[3];
1469 let x = &paulis[1];
1470 let zx = tensor_2x2(z, x);
1471
1472 // Z x X = [[0,1,0,0],[1,0,0,0],[0,0,0,-1],[0,0,-1,0]]
1473 let one = Complex64::new(1.0, 0.0);
1474 let neg_one = Complex64::new(-1.0, 0.0);
1475 let zero = Complex64::new(0.0, 0.0);
1476 let expected = vec![
1477 zero, one, zero, zero, one, zero, zero, zero, zero, zero, zero, neg_one, zero, zero,
1478 neg_one, zero,
1479 ];
1480 for (got, want) in zx.iter().zip(expected.iter()) {
1481 assert!((got.re - want.re).abs() < 1e-12 && (got.im - want.im).abs() < 1e-12);
1482 }
1483 }
1484
1485 #[test]
1486 fn test_matmul_2x2_identity_and_pauli() {
1487 let paulis = single_qubit_paulis();
1488 let identity = &paulis[0];
1489 let x = &paulis[1];
1490
1491 // I * X == X
1492 let product = matmul_2x2(identity, x);
1493 for (got, want) in product.iter().zip(x.iter()) {
1494 assert!((got.re - want.re).abs() < 1e-12 && (got.im - want.im).abs() < 1e-12);
1495 }
1496
1497 // X * X == I
1498 let xx = matmul_2x2(x, x);
1499 for (got, want) in xx.iter().zip(identity.iter()) {
1500 assert!((got.re - want.re).abs() < 1e-12 && (got.im - want.im).abs() < 1e-12);
1501 }
1502 }
1503}