Skip to main content

ppvm_traits/traits/
clifford.rs

1// SPDX-FileCopyrightText: 2026 The PPVM Authors
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::traits::PauliWordTrait;
5
6/// The minimal Clifford gate set: the single-qubit Paulis (`X`, `Y`, `Z`),
7/// Hadamard (`H`), phase gate (`S`), and the two entangling Cliffords
8/// `CNOT` and `CZ`.
9///
10/// Implemented by `PauliSum`, by every tableau type, and — via the
11/// blanket impl in this module — by every [`PauliWordTrait`]
12/// implementor.
13///
14/// # Examples
15///
16/// Build the GHZ-preparation circuit on a `PauliSum` (Heisenberg picture,
17/// so gates are applied in reverse). `PauliSum` lives in the downstream
18/// `ppvm-pauli-sum` crate, so this example is `ignore`d here:
19///
20/// ```ignore
21/// use ppvm_pauli_sum::prelude::*;
22///
23/// let mut state: PauliSum<config::indexmap::ByteFxHashF64<1>> =
24///     PauliSum::builder().n_qubits(2).build();
25/// state += ("ZZ", 1.0);
26/// state.cnot(0, 1);
27/// state.h(0);
28/// assert_eq!(state.len(), 1);
29/// ```
30pub trait Clifford {
31    /// Apply Pauli `X` to one qubit.
32    fn x(&mut self, addr0: usize);
33    /// Apply Pauli `Y` to one qubit.
34    fn y(&mut self, addr0: usize);
35    /// Apply Pauli `Z` to one qubit.
36    fn z(&mut self, addr0: usize);
37    /// Apply Hadamard `H` to one qubit.
38    fn h(&mut self, addr0: usize);
39    /// Apply phase gate `S` to one qubit.
40    fn s(&mut self, addr0: usize);
41    /// Apply `CNOT` to one `(control, target)` pair.
42    fn cnot(&mut self, control: usize, target: usize);
43    /// Apply `CZ` to one qubit pair.
44    fn cz(&mut self, addr0: usize, addr1: usize);
45
46    /// stim alias for [`cnot`](Clifford::cnot).
47    fn cx(&mut self, control: usize, target: usize) {
48        self.cnot(control, target)
49    }
50    /// stim alias for [`cnot`](Clifford::cnot).
51    fn zcx(&mut self, control: usize, target: usize) {
52        self.cnot(control, target)
53    }
54    /// stim alias for [`cz`](Clifford::cz).
55    fn zcz(&mut self, addr0: usize, addr1: usize) {
56        self.cz(addr0, addr1)
57    }
58}
59
60/// Additional Clifford gates beyond the minimal set: `S†`, `√X`, `√X†`,
61/// `√Y`, `√Y†`, and `CY`.
62pub trait CliffordExtensions: Clifford {
63    /// Apply `S†` to one qubit.
64    fn s_dag(&mut self, addr0: usize);
65    /// Apply `√X` to one qubit.
66    fn sqrt_x(&mut self, addr0: usize);
67    /// Apply `(√X)†` to one qubit.
68    fn sqrt_x_dag(&mut self, addr0: usize);
69    /// Apply `√Y` to one qubit.
70    fn sqrt_y(&mut self, addr0: usize);
71    /// Apply `(√Y)†` to one qubit.
72    fn sqrt_y_dag(&mut self, addr0: usize);
73    /// Apply `CY` to one `(control, target)` pair.
74    fn cy(&mut self, control: usize, target: usize);
75    /// stim alias for [`cy`](CliffordExtensions::cy).
76    fn zcy(&mut self, control: usize, target: usize) {
77        self.cy(control, target)
78    }
79}
80
81// === Blanket Clifford impl for PauliWordTrait ===
82//
83// Any type implementing [`PauliWordTrait`] automatically gets word-level
84// Clifford gate behavior. X/Y/Z are no-ops at the word level (they only
85// affect phase, tracked separately in `PhasedPauliWord`). H, S, CNOT, CZ
86// transform the bit representation. All gates honor loss bits via
87// `get_lbit`, which returns `false` for non-lossy types.
88//
89// New PauliWordTrait implementors get this behavior for free.
90//
91// Breaking change for downstream crates: this blanket impl conflicts with
92// any external `impl Clifford for MyWord where MyWord: PauliWordTrait`.
93// Downstreams that need custom Clifford semantics must not implement
94// `PauliWordTrait` on that type — see `PauliWordTrait`'s docs and the
95// `PhasedPauliWord` impl in `crate::phase::clifford` for the pattern.
96
97impl<T: PauliWordTrait> Clifford for T {
98    #[inline]
99    fn x(&mut self, _addr0: usize) {
100        // X * I * X = I    00 -> 00, 0
101        // X * X * X = X    10 -> 10, 0
102        // X * Z * X = -Z   01 -> 01, 1
103        // X * Y * X = -Y   11 -> 11, 1
104        // word-level no-op: phase tracked at PhasedPauliWord level
105    }
106
107    #[inline]
108    fn y(&mut self, _addr0: usize) {
109        // word-level no-op
110    }
111
112    #[inline]
113    fn z(&mut self, _addr0: usize) {
114        // word-level no-op
115    }
116
117    #[inline]
118    fn h(&mut self, index: usize) {
119        // H * I * H = I, H * X * H = Z, H * Z * H = X, H * Y * H = -Y
120        if self.get_lbit(index) {
121            return;
122        }
123        let ix = self.get_xbit(index);
124        let iz = self.get_zbit(index);
125        self.set_xbit(index, iz);
126        self.set_zbit(index, ix);
127        self.rehash();
128    }
129
130    #[inline]
131    fn s(&mut self, index: usize) {
132        // S * I * S = I, S * X * S = Y, S * Z * S = Z, S * Y * S = -X
133        if self.get_lbit(index) {
134            return;
135        }
136        let z = self.get_xbit(index) ^ self.get_zbit(index);
137        self.set_zbit(index, z);
138        self.rehash();
139    }
140
141    #[inline]
142    fn cnot(&mut self, control: usize, target: usize) {
143        if self.get_lbit(control) || self.get_lbit(target) {
144            return;
145        }
146        let control_z = self.get_zbit(target) ^ self.get_zbit(control);
147        let target_x = self.get_xbit(control) ^ self.get_xbit(target);
148        self.set_zbit(control, control_z);
149        self.set_xbit(target, target_x);
150        self.rehash();
151    }
152
153    #[inline]
154    fn cz(&mut self, control: usize, target: usize) {
155        if self.get_lbit(control) || self.get_lbit(target) {
156            return;
157        }
158        // flip the control z if target x is 1
159        let control_z = self.get_zbit(control) ^ self.get_xbit(target);
160        self.set_zbit(control, control_z);
161        // flip the target z if control x is 1
162        let target_z = self.get_zbit(target) ^ self.get_xbit(control);
163        self.set_zbit(target, target_z);
164        self.rehash();
165    }
166}
167
168/// Batched Clifford gates: apply the same gate to many qubits in one call.
169///
170/// Default implementations loop over the corresponding single-qubit
171/// (or single-pair) method on [`Clifford`]. Types like the stabilizer
172/// `Tableau` override these methods with a fused inner-loop or bitmask
173/// implementation. Types that don't need specialization can implement
174/// this trait with an empty `impl` to use the defaults.
175pub trait CliffordBatch: Clifford {
176    /// Apply Pauli `X` to every qubit in `indices`.
177    fn x_many(&mut self, indices: &[usize]) {
178        for &q in indices {
179            self.x(q);
180        }
181    }
182    /// Apply Pauli `Y` to every qubit in `indices`.
183    fn y_many(&mut self, indices: &[usize]) {
184        for &q in indices {
185            self.y(q);
186        }
187    }
188    /// Apply Pauli `Z` to every qubit in `indices`.
189    fn z_many(&mut self, indices: &[usize]) {
190        for &q in indices {
191            self.z(q);
192        }
193    }
194    /// Apply Hadamard `H` to every qubit in `indices`.
195    fn h_many(&mut self, indices: &[usize]) {
196        for &q in indices {
197            self.h(q);
198        }
199    }
200    /// Apply phase gate `S` to every qubit in `indices`.
201    fn s_many(&mut self, indices: &[usize]) {
202        for &q in indices {
203            self.s(q);
204        }
205    }
206    /// Apply `CNOT` to every `(control, target)` pair.
207    fn cnot_many(&mut self, pairs: &[(usize, usize)]) {
208        for &(c, t) in pairs {
209            self.cnot(c, t);
210        }
211    }
212    /// Apply `CZ` to every `(control, target)` pair.
213    fn cz_many(&mut self, pairs: &[(usize, usize)]) {
214        for &(c, t) in pairs {
215            self.cz(c, t);
216        }
217    }
218}
219
220/// Batched form of [`CliffordExtensions`].
221pub trait CliffordExtensionsBatch: CliffordExtensions + CliffordBatch {
222    /// Apply `S†` to every qubit in `indices`.
223    fn s_dag_many(&mut self, indices: &[usize]) {
224        for &q in indices {
225            self.s_dag(q);
226        }
227    }
228    /// Apply `√X` to every qubit in `indices`.
229    fn sqrt_x_many(&mut self, indices: &[usize]) {
230        for &q in indices {
231            self.sqrt_x(q);
232        }
233    }
234    /// Apply `(√X)†` to every qubit in `indices`.
235    fn sqrt_x_dag_many(&mut self, indices: &[usize]) {
236        for &q in indices {
237            self.sqrt_x_dag(q);
238        }
239    }
240    /// Apply `√Y` to every qubit in `indices`.
241    fn sqrt_y_many(&mut self, indices: &[usize]) {
242        for &q in indices {
243            self.sqrt_y(q);
244        }
245    }
246    /// Apply `(√Y)†` to every qubit in `indices`.
247    fn sqrt_y_dag_many(&mut self, indices: &[usize]) {
248        for &q in indices {
249            self.sqrt_y_dag(q);
250        }
251    }
252    /// Apply `CY` to every `(control, target)` pair.
253    fn cy_many(&mut self, pairs: &[(usize, usize)]) {
254        for &(c, t) in pairs {
255            self.cy(c, t);
256        }
257    }
258}
259
260impl<T: PauliWordTrait> CliffordExtensions for T {
261    // |    Gate    |  X  |  Y  |  Z  |
262    // |:----------:|:---:|:---:|:---:|
263    // |     s      | -Y  |  X  |  Z  |
264    // |   s_dag    |  Y  | -X  |  Z  |
265    // |   sqrt_x   |  X  | -Z  |  Y  |
266    // | sqrt_x_dag |  X  |  Z  | -Y  |
267    // |   sqrt_y   |  Z  |  Y  | -X  |
268    // | sqrt_y_dag | -Z  |  Y  |  X  |
269
270    #[inline]
271    fn s_dag(&mut self, addr0: usize) {
272        // s_dag has the same bit mapping as s (only phases differ)
273        self.s(addr0);
274    }
275
276    #[inline]
277    fn sqrt_x(&mut self, addr0: usize) {
278        if self.get_lbit(addr0) {
279            return;
280        }
281        let x = self.get_xbit(addr0);
282        let z = self.get_zbit(addr0);
283        self.set_xbit(addr0, x ^ z);
284        self.rehash();
285    }
286
287    #[inline]
288    fn sqrt_x_dag(&mut self, addr0: usize) {
289        if self.get_lbit(addr0) {
290            return;
291        }
292        let x = self.get_xbit(addr0);
293        let z = self.get_zbit(addr0);
294        self.set_xbit(addr0, x ^ z);
295        self.rehash();
296    }
297
298    #[inline]
299    fn sqrt_y(&mut self, addr0: usize) {
300        if self.get_lbit(addr0) {
301            return;
302        }
303        let x = self.get_xbit(addr0);
304        let z = self.get_zbit(addr0);
305        self.set_xbit(addr0, z);
306        self.set_zbit(addr0, x);
307        self.rehash();
308    }
309
310    #[inline]
311    fn sqrt_y_dag(&mut self, addr0: usize) {
312        if self.get_lbit(addr0) {
313            return;
314        }
315        let x = self.get_xbit(addr0);
316        let z = self.get_zbit(addr0);
317        self.set_xbit(addr0, z);
318        self.set_zbit(addr0, x);
319        self.rehash();
320    }
321
322    // | CY  |  I  |  X  |  Y  |  Z  |
323    // |:---:|:---:|:---:|:---:|:---:|
324    // |  I  | II  | ZX  | IY  | ZZ  |
325    // |  X  | XY  | -YZ | XI  | YX  |
326    // |  Y  | YY  | XZ  | YI  | -XX |
327    // |  Z  | ZI  | IX  | ZY  | IZ  |
328    #[inline]
329    fn cy(&mut self, addr0: usize, addr1: usize) {
330        if self.get_lbit(addr0) || self.get_lbit(addr1) {
331            return;
332        }
333        let xc = self.get_xbit(addr0);
334        let zc = self.get_zbit(addr0);
335        let xt = self.get_xbit(addr1);
336        let zt = self.get_zbit(addr1);
337        self.set_zbit(addr0, zc ^ xt ^ zt);
338        self.set_xbit(addr1, xt ^ xc);
339        self.set_zbit(addr1, zt ^ xc);
340        self.rehash();
341    }
342}