qdk_sim/
tableau.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use std::fmt::Display;
5
6use crate::utils::{set_row_to_row_sum, set_vec_to_row_sum, swap_columns};
7use ndarray::{s, Array, Array1, Array2};
8use serde::{Deserialize, Serialize};
9
10#[cfg(feature = "python")]
11use pyo3::{prelude::*, PyObjectProtocol};
12
13#[cfg(feature = "python")]
14use std::io::{Error, ErrorKind};
15
16/// Represents a stabilizer group with logical dimension 1;
17/// that is, a single stabilizer state expressed in terms
18/// of the generators of its stabilizer group, and those
19/// generators of the Pauli group that anticommute with
20/// each stabilizer generator (colloquially, the destabilizers
21/// of the represented state).
22#[cfg_attr(feature = "python", pyclass(name = "Tableau"))]
23#[derive(Clone, Debug, Serialize, Deserialize)]
24pub struct Tableau {
25    // TODO[code quality]: This is redundant with the n_qubits field in
26    //                     QubitSized, such that this type should be refactored
27    //                     to only have the table itself.
28    n_qubits: usize,
29    table: Array2<bool>,
30}
31
32impl Display for Tableau {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        write!(
35            f,
36            "Stabilizer tableau on {} qubits:\n{:?}",
37            self.n_qubits, self.table
38        )
39    }
40}
41
42impl Tableau {
43    const fn idx_phase(&self) -> usize {
44        2 * self.n_qubits
45    }
46
47    /// Returns a new stabilizer tableau representing
48    /// the $\ket{00\cdots 0}$ state of an $n$-qubit
49    /// register.
50    pub fn new(n_qubits: usize) -> Self {
51        Tableau {
52            n_qubits,
53            table: Array::from_shape_fn((2 * n_qubits, 2 * n_qubits + 1), |(i, j)| i == j),
54        }
55    }
56
57    fn idx_x(&self, idx_col: usize) -> usize {
58        idx_col
59    }
60
61    fn idx_z(&self, idx_col: usize) -> usize {
62        idx_col + self.n_qubits
63    }
64
65    /// Returns the determinstic result that would be obtained from measuring
66    /// the given qubit in the 𝑍 basis, or `None` if the result is random.
67    fn determinstic_result(&self, idx_target: usize) -> Option<bool> {
68        let determined = !self
69            .table
70            .slice(s![self.n_qubits.., idx_target])
71            .iter()
72            .any(|b| *b);
73        if determined {
74            // What was it determined to be?
75            let mut vector: Array1<bool> = Array::default(2 * self.n_qubits + 1);
76            for idx_destabilizer in 0..self.n_qubits {
77                if self.table[(idx_destabilizer, self.idx_x(idx_target))] {
78                    set_vec_to_row_sum(&mut vector, &self.table, idx_destabilizer + self.n_qubits);
79                }
80            }
81            Some(vector[2 * self.n_qubits])
82        } else {
83            None
84        }
85    }
86}
87
88impl Tableau {
89    /// Asserts whether a hypothetical single-qubit $Z$-basis measurement
90    /// would agree with an expected result.
91    ///
92    /// If the assertion would pass, `Ok(())` is returned, otherwise an [`Err`]
93    /// describing the assertion failure is returned.
94    pub fn assert_meas(&self, idx_target: usize, expected: bool) -> Result<(), String> {
95        let actual = self.determinstic_result(idx_target).ok_or(format!(
96            "Expected {}, but measurement result would be random.",
97            expected
98        ))?;
99        if actual != expected {
100            Err(format!(
101                "Expected {}, but measurement result would actually be {}.",
102                expected, actual
103            ))
104        } else {
105            Ok(())
106        }
107    }
108}
109
110#[cfg_attr(feature = "python", pymethods)]
111impl Tableau {
112    /// Returns a serialization of this stabilizer tableau as a JSON object.
113    #[cfg(feature = "python")]
114    pub fn as_json(&self) -> PyResult<String> {
115        serde_json::to_string(self)
116            .map_err(|e| PyErr::from(Error::new(ErrorKind::Other, e.to_string())))
117    }
118
119    /// Applies a Hadamard operation in-place to the given qubit.
120    pub fn apply_h_mut(&mut self, idx_target: usize) {
121        let idxs = (self.idx_x(idx_target), self.idx_z(idx_target));
122        swap_columns(&mut self.table, idxs);
123        let idx_phase = self.idx_phase();
124        for idx_row in 0..2 * self.n_qubits {
125            let a = self.table[(idx_row, self.idx_x(idx_target))];
126            let b = self.table[(idx_row, self.idx_z(idx_target))];
127            self.table[(idx_row, idx_phase)] ^= a && b;
128        }
129    }
130
131    /// Applies a phase operation ($S$) in-place to the given qubit.
132    pub fn apply_s_mut(&mut self, idx_target: usize) {
133        let idx_phase = self.idx_phase();
134        for idx_row in 0..2 * self.n_qubits {
135            self.table[(idx_row, idx_phase)] ^= self.table[(idx_row, self.idx_x(idx_target))]
136                && self.table[(idx_row, self.idx_z(idx_target))];
137        }
138
139        for idx_row in 0..2 * self.n_qubits {
140            let idx_x_target = self.idx_x(idx_target);
141            let idx_z_target = self.idx_z(idx_target);
142            self.table[(idx_row, idx_z_target)] ^= self.table[(idx_row, idx_x_target)];
143        }
144    }
145
146    /// Applies a controlled-NOT operation in-place, given control and target
147    /// qubits.
148    pub fn apply_cnot_mut(&mut self, idx_control: usize, idx_target: usize) {
149        let idx_phase = self.idx_phase();
150        for idx_row in 0..2 * self.n_qubits {
151            self.table[(idx_row, idx_phase)] ^= self.table[(idx_row, self.idx_x(idx_control))]
152                && self.table[(idx_row, self.idx_z(idx_target))]
153                && (self.table[(idx_row, self.idx_x(idx_target))]
154                    ^ self.table[(idx_row, self.idx_z(idx_control))]
155                    ^ true);
156        }
157
158        for idx_row in 0..2 * self.n_qubits {
159            let idx_x_target = self.idx_x(idx_target);
160            let idx_x_control = self.idx_x(idx_control);
161            self.table[(idx_row, idx_x_target)] ^= self.table[(idx_row, idx_x_control)];
162        }
163
164        for idx_row in 0..2 * self.n_qubits {
165            let idx_z_target = self.idx_z(idx_target);
166            let idx_z_control = self.idx_z(idx_control);
167            self.table[(idx_row, idx_z_control)] ^= self.table[(idx_row, idx_z_target)];
168        }
169    }
170
171    /// Applies a Pauli $X$ operation in-place to the given qubit.
172    pub fn apply_x_mut(&mut self, idx_target: usize) {
173        self.apply_h_mut(idx_target);
174        self.apply_z_mut(idx_target);
175        self.apply_h_mut(idx_target);
176    }
177
178    /// Applies an adjoint phase operation ($S^{\dagger}$) in-place to the
179    /// given qubit.
180    pub fn apply_s_adj_mut(&mut self, idx_target: usize) {
181        self.apply_s_mut(idx_target);
182        self.apply_s_mut(idx_target);
183        self.apply_s_mut(idx_target);
184    }
185
186    /// Applies a Pauli $Y$ operation in-place to the given qubit.
187    pub fn apply_y_mut(&mut self, idx_target: usize) {
188        self.apply_s_adj_mut(idx_target);
189        self.apply_x_mut(idx_target);
190        self.apply_s_mut(idx_target);
191    }
192
193    /// Applies a Pauli $Z$ operation in-place to the given qubit.
194    pub fn apply_z_mut(&mut self, idx_target: usize) {
195        self.apply_s_mut(idx_target);
196        self.apply_s_mut(idx_target);
197    }
198
199    /// Applies a SWAP operation in-place between two qubits.
200    pub fn apply_swap_mut(&mut self, idx_1: usize, idx_2: usize) {
201        self.apply_cnot_mut(idx_1, idx_2);
202        self.apply_cnot_mut(idx_2, idx_1);
203        self.apply_cnot_mut(idx_1, idx_2);
204    }
205
206    /// Measures a single qubit in the Pauli $Z$-basis, returning the result,
207    /// and updating the stabilizer tableau in-place.
208    pub fn meas_mut(&mut self, idx_target: usize) -> bool {
209        if let Some(result) = self.determinstic_result(idx_target) {
210            return result;
211        }
212
213        // If we're still here, we know the measurement result is random;
214        // thus we need to pick a random result and use that to update the
215        // tableau.
216        let idx_phase = self.idx_phase();
217        let result = rand::random();
218        let collisions: Vec<usize> = self
219            .table
220            .slice(s![.., self.idx_x(idx_target)])
221            .indexed_iter()
222            .filter(|(_i, b)| **b)
223            .map(|(i, _b)| i)
224            .collect();
225        // Find the first stabilizer that intersects with idx_target in the X
226        // sector.
227        let idx_first: usize = self.n_qubits
228            + self
229                .table
230                .slice(s![self.n_qubits.., self.idx_x(idx_target)])
231                .indexed_iter()
232                .find(|(_i, b)| **b)
233                .unwrap()
234                .0;
235        // Make an owned copy of the first colliding stabilizer, as we'll
236        // need that later.
237        let old_stab = self.table.slice(s![idx_first, ..]).to_owned();
238
239        // For all collisions other than the first stabilizer, take the row
240        // sum of that row with the first stabilizer.
241        for idx_collision in collisions.iter() {
242            if *idx_collision != idx_first {
243                set_row_to_row_sum(&mut self.table, *idx_collision, idx_first);
244            }
245        }
246
247        // Move the old stabilizer into being a destabilizer, then make a new
248        // stabilizer that's constrained to agree with our random result.
249        self.table
250            .slice_mut(s![idx_first - self.n_qubits, ..])
251            .assign(&old_stab);
252        self.table.slice_mut(s![idx_first, ..]).fill(false);
253        let idx_z_target = self.idx_z(idx_target);
254        self.table[(idx_first, idx_z_target)] = true;
255        self.table[(idx_first, idx_phase)] = result;
256        result
257    }
258}
259
260// Forward the ::new method onto Python.
261#[cfg(feature = "python")]
262#[pymethods]
263impl Tableau {
264    /// Returns a new stabilizer tableau representing
265    /// the $\ket{00\cdots 0}$ state of an $n$-qubit
266    /// register.
267    #[new]
268    pub fn new_py(n_qubits: usize) -> Self {
269        Self::new(n_qubits)
270    }
271}
272
273#[cfg(feature = "python")]
274#[pyproto]
275impl PyObjectProtocol for Tableau {
276    fn __repr__(&self) -> String {
277        format!("<{:?}>", self)
278    }
279
280    fn __str__(&self) -> String {
281        format!("{}", self)
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn bell_pair_meas_agree() {
291        let mut t = Tableau::new(2);
292        t.apply_h_mut(0);
293        t.apply_cnot_mut(0, 1);
294        let left = t.meas_mut(0);
295        let right = t.meas_mut(1);
296        assert_eq!(left, right)
297    }
298}