stabilizer_qec 1.9.0

Quantum error-correction simulation and decoding: Stim-format circuits and error models, a bit-parallel sampler, sparse-blossom, correlated and belief matching, BP+OSD, window decoders, bivariate bicycle codes and lattice surgery.
Documentation
//! Sampling a circuit with a Pauli frame, independently of any error model.
//!
//! The frame records how this shot differs from a noiseless run. For a Clifford
//! circuit under Pauli noise that is all a detector needs: it reads a parity the
//! noiseless circuit fixes, so it fires exactly when the frame flips an odd
//! number of the records it reads. Noise is sampled with each channel's true
//! (disjoint) semantics, not the error model's independent approximation, and
//! nothing here looks at the model, so a model that is wrong about the circuit
//! shows up as disagreement between the two rather than agreeing with itself.
//!
//! Resets and measurements randomise the component of the frame that the state
//! is insensitive to, as Stim's frame simulator does. It costs a draw and changes
//! nothing for a deterministic detector; for a non-deterministic one it makes the
//! failure visible as a coin flip instead of hiding it.

use crate::circuit::{Basis, Circuit, Instr};
use crate::nonpauli::NonPauli;
use crate::surface_code::Xorshift;

pub struct Shot {
    pub detectors: Vec<bool>,
    pub observables: u64,
    /// For a circuit with leakage, whether each measurement found its qubit leaked; empty
    /// otherwise.
    pub heralds: Vec<bool>,
}

pub struct FrameSampler {
    /// Whether the circuit has leakage (`I_ERROR[LEAK(p=…)]` and the rest): a leaked flag per
    /// qubit, set by LEAK and LEAK_TRANSPORT, cleared by SEEP and by a reset. A two-qubit gate
    /// with a leaked partner leaves the other qubit a uniformly random Pauli (Google's model),
    /// and measuring a leaked qubit reads 1 (or a coin flip, `leaked_reads_one = false`).
    leaky: bool,
    pub leaked_reads_one: bool,
    /// The noiseless reference record, which a leaked qubit's 1 is a flip from.
    reference: Vec<bool>,
    instrs: Vec<Instr>,
    num_qubits: usize,
    num_measurements: usize,
    detectors: Vec<Vec<usize>>,
    observables: Vec<Vec<usize>>,
}

/// After a two-qubit gate, a leaked partner leaves the other qubit a uniformly random Pauli.
fn leaked_partner(leaked: &[bool], a: usize, b: usize, x: &mut [bool], z: &mut [bool], rng: &mut Xorshift) {
    for (l, o) in [(a, b), (b, a)] {
        if leaked[l] && !leaked[o] {
            apply(x, z, o, [0, 1, 3, 2][(rng.next_u64() % 4) as usize]);
        }
    }
}

fn apply(x: &mut [bool], z: &mut [bool], q: usize, pauli: u8) {
    if pauli & 1 != 0 {
        x[q] ^= true;
    }
    if pauli & 2 != 0 {
        z[q] ^= true;
    }
}

impl FrameSampler {
    pub fn new(circuit: &Circuit) -> Result<FrameSampler, String> {
        let res = circuit.resolve()?;
        let leaky = res.instrs.iter().any(|i| matches!(i, Instr::NonPauli(ops) if ops.iter().any(|o| o.is_leakage())));
        let reference = if leaky { crate::m2d::run(circuit, crate::batch_sampler::Counts::of(&circuit.instrs)?.qubits, &[], 1) } else { Vec::new() };
        Ok(FrameSampler {
            leaky,
            leaked_reads_one: true,
            reference,
            instrs: res.instrs,
            num_qubits: res.num_qubits,
            num_measurements: res.num_measurements,
            detectors: res.detectors,
            observables: res.observables,
        })
    }

    pub fn num_detectors(&self) -> usize {
        self.detectors.len()
    }

    pub fn num_observables(&self) -> usize {
        self.observables.len()
    }

    pub fn sample(&self, rng: &mut Xorshift) -> Shot {
        let nq = self.num_qubits;
        let mut x = vec![false; nq];
        let mut z: Vec<bool> = (0..nq).map(|_| rng.next_u64() & 1 == 1).collect();
        let mut rec = Vec::with_capacity(self.num_measurements);
        // Whether the current chain of correlated errors has fired.
        let mut chain = false;
        let mut pauli_obs = 0u64;
        let mut leaked = vec![false; if self.leaky { nq } else { 0 }];
        let mut heralds = Vec::new();
        for ins in &self.instrs {
            match ins {
                Instr::Reset { basis, qubits } => {
                    for &q in qubits {
                        let q = q as usize;
                        if self.leaky {
                            leaked[q] = false;
                        }
                        match basis {
                            Basis::Z => {
                                x[q] = false;
                                z[q] = rng.next_u64() & 1 == 1;
                            }
                            Basis::X => {
                                z[q] = false;
                                x[q] = rng.next_u64() & 1 == 1;
                            }
                        }
                    }
                }
                Instr::H(qubits) => {
                    for &q in qubits {
                        let q = q as usize;
                        std::mem::swap(&mut x[q], &mut z[q]);
                    }
                }
                Instr::Cx(pairs) => {
                    for &(c, t) in pairs {
                        let (c, t) = (c as usize, t as usize);
                        if x[c] {
                            x[t] ^= true;
                        }
                        if z[t] {
                            z[c] ^= true;
                        }
                        if self.leaky {
                            leaked_partner(&leaked, c, t, &mut x, &mut z, rng);
                        }
                    }
                }
                Instr::Cz(pairs) => {
                    for &(a, b) in pairs {
                        let (a, b) = (a as usize, b as usize);
                        if x[a] {
                            z[b] ^= true;
                        }
                        if x[b] {
                            z[a] ^= true;
                        }
                        if self.leaky {
                            leaked_partner(&leaked, a, b, &mut x, &mut z, rng);
                        }
                    }
                }
                Instr::Measure { basis, reset, flip, qubits } => {
                    for &q in qubits {
                        let q = q as usize;
                        let mut r = match basis {
                            Basis::Z => x[q],
                            Basis::X => z[q],
                        };
                        if self.leaky {
                            heralds.push(leaked[q]);
                            if leaked[q] {
                                // A leaked qubit reads 1: a flip from the reference's value.
                                r = if self.leaked_reads_one { !self.reference[rec.len()] } else { rng.next_u64() & 1 == 1 };
                            }
                            if *reset {
                                leaked[q] = false;
                            }
                        }
                        if *flip > 0.0 && rng.next_f64() < *flip {
                            r = !r;
                        }
                        rec.push(r);
                        match basis {
                            Basis::Z => z[q] = rng.next_u64() & 1 == 1,
                            Basis::X => x[q] = rng.next_u64() & 1 == 1,
                        }
                        if *reset {
                            match basis {
                                Basis::Z => {
                                    x[q] = false;
                                    z[q] = rng.next_u64() & 1 == 1;
                                }
                                Basis::X => {
                                    z[q] = false;
                                    x[q] = rng.next_u64() & 1 == 1;
                                }
                            }
                        }
                    }
                }
                Instr::PauliError { pauli, p, qubits } => {
                    for &q in qubits {
                        if rng.next_f64() < *p {
                            apply(&mut x, &mut z, q as usize, *pauli);
                        }
                    }
                }
                Instr::Depolarize1 { p, qubits } => {
                    for &q in qubits {
                        if rng.next_f64() < *p {
                            apply(&mut x, &mut z, q as usize, 1 + (rng.next_u64() % 3) as u8);
                        }
                    }
                }
                Instr::PauliChannel1 { px, py, pz, qubits } => {
                    for &q in qubits {
                        let u = rng.next_f64();
                        let pauli = if u < *px {
                            1
                        } else if u < px + py {
                            3
                        } else if u < px + py + pz {
                            2
                        } else {
                            0
                        };
                        apply(&mut x, &mut z, q as usize, pauli);
                    }
                }
                Instr::Depolarize2 { p, pairs } => {
                    for &(a, b) in pairs {
                        if rng.next_f64() < *p {
                            let r = 1 + rng.next_u64() % 15;
                            apply(&mut x, &mut z, a as usize, (r & 3) as u8);
                            apply(&mut x, &mut z, b as usize, (r >> 2) as u8);
                        }
                    }
                }
                Instr::S(qubits) => {
                    for &q in qubits {
                        let q = q as usize;
                        z[q] ^= x[q];
                    }
                }
                Instr::Correlated { p, paulis, chained } => {
                    let fires = rng.next_f64() < *p && !(*chained && chain);
                    chain = if *chained { chain || fires } else { fires };
                    if fires {
                        for &(q, pauli) in paulis {
                            apply(&mut x, &mut z, q as usize, pauli);
                        }
                    }
                }
                Instr::PauliChannel2 { probs, pairs } => {
                    for &(a, b) in pairs {
                        let u = rng.next_f64();
                        let mut acc = 0.0;
                        if let Some(k) = probs.iter().position(|p| {
                            acc += p;
                            u < acc
                        }) {
                            const CODE: [u8; 4] = [0, 1, 3, 2];
                            apply(&mut x, &mut z, a as usize, CODE[(k + 1) / 4]);
                            apply(&mut x, &mut z, b as usize, CODE[(k + 1) % 4]);
                        }
                    }
                }
                // Relative to the noiseless run, a record's flip flips its Pauli; a sweep bit
                // moves only the reference.
                Instr::Feedback { pauli, control, qubit } => {
                    if let crate::circuit::Control::Rec(k) = control {
                        if rec[rec.len() - *k as usize] {
                            apply(&mut x, &mut z, *qubit as usize, *pauli);
                        }
                    }
                }
                Instr::Heralded { probs, qubits, .. } => {
                    for &q in qubits {
                        let u = rng.next_f64();
                        let mut acc = 0.0;
                        let case = probs.iter().position(|p| {
                            acc += p;
                            u < acc
                        });
                        rec.push(case.is_some());
                        if let Some(k) = case {
                            apply(&mut x, &mut z, q as usize, [0, 1, 3, 2][k]);
                        }
                    }
                }
                Instr::Pad { flip, values } => {
                    for _ in values {
                        rec.push(*flip > 0.0 && rng.next_f64() < *flip);
                    }
                }
                // Signs, annotations and ticks leave a frame alone; loops and gates are
                // flattened away.
                // A Pauli target reads the frame: X anticommutes with its Z part, Z with its X.
                Instr::Observable { index, paulis, .. } => {
                    for &(q, p, _) in paulis {
                        let q = q as usize;
                        if (p & 1 != 0 && z[q]) != (p & 2 != 0 && x[q]) {
                            pauli_obs ^= 1u64 << index;
                        }
                    }
                }
                Instr::NonPauli(ops) if self.leaky => {
                    for op in ops {
                        match *op {
                            NonPauli::Leak { p, qubit } => {
                                if rng.next_f64() < p {
                                    leaked[qubit as usize] = true;
                                }
                            }
                            NonPauli::Seep { p, qubit } => {
                                let q = qubit as usize;
                                if rng.next_f64() < p && leaked[q] {
                                    leaked[q] = false;
                                    // Back in |0⟩ or |1⟩ at random.
                                    x[q] ^= rng.next_u64() & 1 == 1;
                                    z[q] = rng.next_u64() & 1 == 1;
                                }
                            }
                            NonPauli::LeakTransport { p, a, b } => {
                                let (a, b) = (a as usize, b as usize);
                                if rng.next_f64() < p && leaked[a] != leaked[b] {
                                    let back = if leaked[a] { a } else { b };
                                    leaked.swap(a, b);
                                    x[back] ^= rng.next_u64() & 1 == 1;
                                    z[back] = rng.next_u64() & 1 == 1;
                                }
                            }
                            _ => {}
                        }
                    }
                }
                Instr::Pauli { .. }
                | Instr::SweepX(_)
                | Instr::Detector { .. }
                | Instr::QubitCoords { .. }
                | Instr::ShiftCoords(_)
                | Instr::Tick
                | Instr::NonPauli(_)
                | Instr::Repeat { .. }
                | Instr::Gate { .. } => {}
            }
        }
        let parity = |recs: &[usize]| recs.iter().fold(false, |acc, &m| acc ^ rec[m]);
        let detectors = self.detectors.iter().map(|r| parity(r)).collect();
        let mut observables = pauli_obs;
        for (k, r) in self.observables.iter().enumerate() {
            if parity(r) {
                observables |= 1u64 << k;
            }
        }
        Shot { detectors, observables, heralds }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::dem::Dem;
    use crate::fixtures::REP3;

    /// Marginals follow from the model exactly: a detector fires when an odd
    /// number of its independent mechanisms do, (1 − Π(1 − 2p))/2. The sampler
    /// never reads the model, so agreement is a check on both.
    #[test]
    fn detector_marginals_match_the_error_model() {
        let circuit = Circuit::parse(&REP3.replace("0.01", "0.05")).unwrap();
        let dem = Dem::from_circuit(&circuit).unwrap();
        let sampler = FrameSampler::new(&circuit).unwrap();
        let n = 200_000usize;
        let mut counts = vec![0usize; sampler.num_detectors()];
        let mut obs = 0usize;
        let mut rng = Xorshift::new(11);
        for _ in 0..n {
            let shot = sampler.sample(&mut rng);
            for (i, &b) in shot.detectors.iter().enumerate() {
                counts[i] += b as usize;
            }
            obs += (shot.observables & 1) as usize;
        }
        let predict = |pick: &dyn Fn(&crate::dem::Mechanism) -> bool| -> f64 {
            let prod: f64 = dem.mechanisms.iter().filter(|m| pick(m)).map(|m| 1.0 - 2.0 * m.p).product();
            (1.0 - prod) / 2.0
        };
        for (d, &c) in counts.iter().enumerate() {
            let q = predict(&|m| m.detectors.contains(&(d as u32)));
            let sigma = (q * (1.0 - q) / n as f64).sqrt();
            let rate = c as f64 / n as f64;
            assert!((rate - q).abs() < 5.0 * sigma, "D{d}: sampled {rate}, model {q}");
        }
        let q = predict(&|m| m.observables & 1 == 1);
        let rate = obs as f64 / n as f64;
        assert!((rate - q).abs() < 5.0 * (q * (1.0 - q) / n as f64).sqrt(), "L0: sampled {rate}, model {q}");
    }

    #[test]
    fn noiseless_circuits_never_fire() {
        let circuit = Circuit::parse(&REP3.replace("0.01", "0")).unwrap();
        let sampler = FrameSampler::new(&circuit).unwrap();
        let mut rng = Xorshift::new(3);
        for _ in 0..2000 {
            let shot = sampler.sample(&mut rng);
            assert!(shot.detectors.iter().all(|&b| !b));
            assert_eq!(shot.observables, 0);
        }
    }
}