1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
//! Pauli frame tracking for fault-tolerant quantum error correction.
//!
//! A Pauli frame represents a virtual Pauli correction that accumulates during
//! stabilizer-based error correction. Rather than physically applying corrections
//! to the quantum state, we track the corrections in classical registers and
//! propagate them through gate operations.
use crate::error_correction::pauli::{Pauli, PauliString};
/// Classical Pauli frame tracker for fault-tolerant circuits.
///
/// Tracks accumulated X and Z corrections that would be needed to map the
/// current (potentially corrupted) state back to the ideal state.
/// Gates commuted through are tracked symbolically rather than applied.
#[derive(Debug, Clone, Default)]
pub struct PauliFrame {
/// X component of the accumulated frame (true = X or Y on that qubit)
pub x_frame: Vec<bool>,
/// Z component of the accumulated frame (true = Z or Y on that qubit)
pub z_frame: Vec<bool>,
}
impl PauliFrame {
/// Create a new all-identity Pauli frame for `n_qubits` qubits.
pub fn new(n_qubits: usize) -> Self {
Self {
x_frame: vec![false; n_qubits],
z_frame: vec![false; n_qubits],
}
}
/// Number of qubits tracked in this frame.
pub fn n_qubits(&self) -> usize {
self.x_frame.len()
}
/// Apply a `PauliString` correction to the frame (XOR into the frame).
///
/// I → no change
/// X → flip x_frame
/// Z → flip z_frame
/// Y → flip both x_frame and z_frame
pub fn apply_pauli_string(&mut self, ps: &PauliString) {
for (i, &p) in ps.paulis.iter().enumerate() {
if i >= self.x_frame.len() {
break;
}
match p {
Pauli::I => {}
Pauli::X => {
self.x_frame[i] ^= true;
}
Pauli::Z => {
self.z_frame[i] ^= true;
}
Pauli::Y => {
self.x_frame[i] ^= true;
self.z_frame[i] ^= true;
}
}
}
}
/// Propagate the Pauli frame through a Hadamard gate on `qubit`.
///
/// H conjugation rule: `H X H† = Z`, `H Z H† = X`.
/// Therefore X and Z components are swapped.
pub fn commute_through_h(&mut self, qubit: usize) {
if qubit < self.x_frame.len() {
// H swaps X and Z
std::mem::swap(&mut self.x_frame[qubit], &mut self.z_frame[qubit]);
}
}
/// Propagate the Pauli frame through an S gate on `qubit`.
///
/// S conjugation rule: `S X S† = Y = iXZ`, `S Z S† = Z`.
/// Effect on frame: X → X (x stays), Z component gains x_frame (Z gets +x).
/// If x was true: Z also becomes true (X → Y, so Z component added).
/// If x was false: nothing changes for Z.
pub fn commute_through_s(&mut self, qubit: usize) {
if qubit < self.x_frame.len() {
// S: X → Y (X stays, Z appears); Z → Z (unchanged)
// So z_frame[qubit] ^= x_frame[qubit]
self.z_frame[qubit] ^= self.x_frame[qubit];
}
}
/// Propagate the Pauli frame through a CNOT gate.
///
/// CNOT conjugation rules:
/// - `CNOT (X_c ⊗ I) CNOT† = X_c ⊗ X_t` (X propagates forward from ctrl to tgt)
/// - `CNOT (I ⊗ X_t) CNOT† = I ⊗ X_t` (X on target unchanged)
/// - `CNOT (Z_c ⊗ I) CNOT† = Z_c ⊗ I` (Z on ctrl unchanged)
/// - `CNOT (I ⊗ Z_t) CNOT† = Z_c ⊗ Z_t` (Z propagates backward from tgt to ctrl)
pub fn commute_through_cnot(&mut self, ctrl: usize, tgt: usize) {
let n = self.x_frame.len();
if ctrl >= n || tgt >= n {
return;
}
// X propagates ctrl → tgt
let x_ctrl = self.x_frame[ctrl];
self.x_frame[tgt] ^= x_ctrl;
// Z propagates tgt → ctrl
let z_tgt = self.z_frame[tgt];
self.z_frame[ctrl] ^= z_tgt;
}
/// Measure the logical X operator.
///
/// Returns `true` if the accumulated frame has a logical-Z error (i.e., the
/// Z-frame parity is odd over the logical-X support qubits).
///
/// `logical_x_qubits`: the data qubit indices forming the logical X string.
pub fn measure_logical_x(&self, logical_x_qubits: &[usize]) -> bool {
logical_x_qubits
.iter()
.filter(|&&q| q < self.z_frame.len())
.filter(|&&q| self.z_frame[q])
.count()
% 2
== 1
}
/// Measure the logical Z operator.
///
/// Returns `true` if the accumulated frame has a logical-X error (i.e., the
/// X-frame parity is odd over the logical-Z support qubits).
///
/// `logical_z_qubits`: the data qubit indices forming the logical Z string.
pub fn measure_logical_z(&self, logical_z_qubits: &[usize]) -> bool {
logical_z_qubits
.iter()
.filter(|&&q| q < self.x_frame.len())
.filter(|&&q| self.x_frame[q])
.count()
% 2
== 1
}
/// Merge another frame into this one (XOR both x and z components).
pub fn merge(&mut self, other: &PauliFrame) {
for (i, (&ox, &oz)) in other.x_frame.iter().zip(&other.z_frame).enumerate() {
if i >= self.x_frame.len() {
break;
}
self.x_frame[i] ^= ox;
self.z_frame[i] ^= oz;
}
}
/// Returns `true` if this frame represents the identity (no corrections needed).
pub fn is_identity(&self) -> bool {
self.x_frame.iter().all(|&x| !x) && self.z_frame.iter().all(|&z| !z)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_is_identity() {
let frame = PauliFrame::new(5);
assert!(frame.is_identity());
}
#[test]
fn test_apply_pauli_string_x() {
let mut frame = PauliFrame::new(3);
let ps = PauliString::new(vec![Pauli::X, Pauli::I, Pauli::I]);
frame.apply_pauli_string(&ps);
assert!(frame.x_frame[0]);
assert!(!frame.z_frame[0]);
}
#[test]
fn test_apply_pauli_string_y() {
let mut frame = PauliFrame::new(3);
let ps = PauliString::new(vec![Pauli::Y, Pauli::I, Pauli::I]);
frame.apply_pauli_string(&ps);
assert!(frame.x_frame[0]);
assert!(frame.z_frame[0]);
}
#[test]
fn test_h_gate_commutation() {
let mut frame = PauliFrame::new(2);
frame.x_frame[0] = true;
frame.z_frame[0] = false;
frame.commute_through_h(0);
// H: X → Z, so x_frame should be false and z_frame true
assert!(!frame.x_frame[0], "After H: x should be false (was true→Z)");
assert!(frame.z_frame[0], "After H: z should be true (X→Z)");
}
#[test]
fn test_h_gate_z_to_x() {
let mut frame = PauliFrame::new(2);
frame.x_frame[0] = false;
frame.z_frame[0] = true;
frame.commute_through_h(0);
assert!(frame.x_frame[0], "After H: x should be true (Z→X)");
assert!(!frame.z_frame[0], "After H: z should be false");
}
#[test]
fn test_s_gate_commutation() {
// S: X → Y (x stays true, z becomes true); Z → Z (unchanged)
let mut frame = PauliFrame::new(2);
frame.x_frame[0] = true;
frame.z_frame[0] = false;
frame.commute_through_s(0);
assert!(frame.x_frame[0], "X component should remain after S");
assert!(frame.z_frame[0], "Z component should appear after S on X");
}
#[test]
fn test_s_gate_z_unchanged() {
let mut frame = PauliFrame::new(2);
frame.x_frame[0] = false;
frame.z_frame[0] = true;
frame.commute_through_s(0);
assert!(!frame.x_frame[0], "X should not change for Z under S");
assert!(frame.z_frame[0], "Z should remain unchanged under S");
}
#[test]
fn test_cnot_x_propagation() {
// CNOT ctrl=0, tgt=1: X_ctrl → X_ctrl X_tgt
let mut frame = PauliFrame::new(2);
frame.x_frame[0] = true;
frame.commute_through_cnot(0, 1);
assert!(frame.x_frame[0], "X on ctrl should remain");
assert!(frame.x_frame[1], "X should propagate to tgt");
}
#[test]
fn test_cnot_z_propagation() {
// CNOT ctrl=0, tgt=1: Z_tgt → Z_ctrl Z_tgt
let mut frame = PauliFrame::new(2);
frame.z_frame[1] = true;
frame.commute_through_cnot(0, 1);
assert!(frame.z_frame[0], "Z should propagate back to ctrl");
assert!(frame.z_frame[1], "Z on tgt should remain");
}
#[test]
fn test_merge() {
let mut frame1 = PauliFrame::new(3);
frame1.x_frame[0] = true;
frame1.z_frame[1] = true;
let mut frame2 = PauliFrame::new(3);
frame2.x_frame[0] = true; // same → cancel
frame2.z_frame[2] = true;
frame1.merge(&frame2);
assert!(!frame1.x_frame[0], "Same X bits should cancel on merge");
assert!(frame1.z_frame[1], "Z bit 1 should persist");
assert!(frame1.z_frame[2], "Z bit 2 from frame2 should appear");
}
#[test]
fn test_measure_logical_x() {
let mut frame = PauliFrame::new(3);
frame.z_frame[0] = true; // Z error on logical X support qubit
// logical X support = [0] → odd parity of z_frame → logical Z error detected
assert!(frame.measure_logical_x(&[0]));
assert!(!frame.measure_logical_x(&[1]));
}
#[test]
fn test_measure_logical_z() {
let mut frame = PauliFrame::new(3);
frame.x_frame[0] = true;
assert!(frame.measure_logical_z(&[0]));
}
}