Skip to main content

bloq_vm/verify/
expectation.rs

1//! Exact logical-state readouts without ancillas or simulator mutation.
2
3use std::sync::OnceLock;
4
5use crate::backend::{PauliString, Simulator};
6
7use crate::{ExecError, OutputLogical};
8
9/// Read a logical Bloch vector, corrected by the supplied `(X, Z)` frame bits.
10/// Use `(false, false)` to read the uncorrected state. Signed representatives
11/// are preserved, including `Y_L = i X_L Z_L`.
12///
13/// # Errors
14/// Propagates engine errors from malformed logical observables.
15pub fn logical_bloch(
16    sim: &Simulator,
17    output: &OutputLogical,
18    frame: (bool, bool),
19) -> Result<(f64, f64, f64), ExecError> {
20    let signature = logical_signature(sim, &[(output, frame)])?;
21    Ok((signature[0], signature[1], signature[2]))
22}
23
24/// Compute all nonidentity joint Pauli expectations of the live logical outputs.
25///
26/// Values are corrected by their `(X, Z)` frame bits. Output zero is the
27/// least-significant base-four digit, with digits `I, X, Y, Z`. This determines
28/// the full reduced logical state, including entanglement, without measuring it.
29/// Representatives of different widths are extended with trailing identities.
30/// For `n` outputs, this allocates `4^n - 1` values and makes that many exact
31/// simulator expectation reads; use it only for small output registers.
32///
33/// # Errors
34/// [`ExecError::LogicalSignatureTooLarge`] if `4^n` overflows `usize`;
35/// otherwise propagates engine errors from malformed logical observables.
36pub fn logical_signature(
37    sim: &Simulator,
38    outputs: &[(&OutputLogical, (bool, bool))],
39) -> Result<Vec<f64>, ExecError> {
40    let cardinality = u32::try_from(outputs.len())
41        .ok()
42        .and_then(|n| 4usize.checked_pow(n))
43        .ok_or(ExecError::LogicalSignatureTooLarge {
44            outputs: outputs.len(),
45        })?;
46    let width = outputs
47        .iter()
48        .map(|(out, _)| out.logical_x.nqubits.max(out.logical_z.nqubits))
49        .max()
50        .unwrap_or(0);
51    let logicals: Vec<_> = outputs
52        .iter()
53        .map(|(out, frame)| {
54            (
55                crate::circuit::widen_pauli(&out.logical_x, width),
56                crate::circuit::widen_pauli(&out.logical_z, width),
57                *frame,
58            )
59        })
60        .collect();
61    (1..cardinality)
62        .map(|mut mask| {
63            let mut product = PauliString::new(width);
64            for (logical_x, logical_z, (x, z)) in &logicals {
65                let digit = mask & 3;
66                if digit == 1 || digit == 2 {
67                    product = &product * logical_x;
68                    product.set_phase(product.phase_exponent() + 2 * i32::from(*z));
69                }
70                if digit == 2 || digit == 3 {
71                    product = &product * logical_z;
72                    product.set_phase(product.phase_exponent() + 2 * i32::from(*x));
73                }
74                if digit == 2 {
75                    product.set_phase(product.phase_exponent() + 1);
76                }
77                mask >>= 2;
78            }
79            sim.peek_observable_expectation(&product)
80                .map_err(Into::into)
81        })
82        .collect()
83}
84
85/// Exact Pauli signature of `CCZ|+++⟩`, prepared by the simulator itself.
86///
87/// # Panics
88///
89/// Panics if the simulator rejects CCZ on three freshly allocated qubits.
90#[must_use]
91pub fn ccz_state_signature() -> &'static [f64] {
92    static SIGNATURE: OnceLock<Vec<f64>> = OnceLock::new();
93    SIGNATURE.get_or_init(|| {
94        use crate::backend::Pauli;
95        let mut sim = Simulator::with_seed(3, 0);
96        for q in 0..3 {
97            sim.h(q);
98        }
99        sim.ccz(0, 1, 2).expect("CCZ on three fresh qubits");
100        let outputs: Vec<_> = (0..3)
101            .map(|q| OutputLogical {
102                port: glam::IVec3::ZERO,
103                logical_x: PauliString::single(3, q, Pauli::X),
104                logical_z: PauliString::single(3, q, Pauli::Z),
105                consumed: false,
106            })
107            .collect();
108        logical_signature(
109            &sim,
110            &outputs
111                .iter()
112                .map(|out| (out, (false, false)))
113                .collect::<Vec<_>>(),
114        )
115        .expect("valid three-qubit logicals")
116    })
117}
118
119/// All distinct Pauli-byproduct signatures of `CCZ|+++⟩`.
120#[must_use]
121pub fn ccz_orbit_signatures() -> &'static [Vec<f64>] {
122    static SIGNATURES: OnceLock<Vec<Vec<f64>>> = OnceLock::new();
123    SIGNATURES.get_or_init(|| {
124        (0..64usize)
125            .map(|frame| {
126                ccz_state_signature()
127                    .iter()
128                    .enumerate()
129                    .map(|(index, expectation)| {
130                        let mut mask = index + 1;
131                        let mut frame = frame;
132                        let mut flip = false;
133                        for _ in 0..3 {
134                            let pauli = mask & 3;
135                            let byproduct = frame & 3;
136                            flip ^= pauli != 0 && byproduct != 0 && pauli != byproduct;
137                            mask >>= 2;
138                            frame >>= 2;
139                        }
140                        expectation * sign(flip)
141                    })
142                    .collect()
143            })
144            .collect()
145    })
146}
147
148/// Whether two Pauli signatures agree entry-wise within a tight tolerance.
149#[must_use]
150pub fn signatures_match(a: &[f64], b: &[f64]) -> bool {
151    a.len() == b.len() && a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-9)
152}
153
154fn sign(bit: bool) -> f64 {
155    if bit { -1.0 } else { 1.0 }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::backend::Pauli;
162
163    #[test]
164    fn signed_logical_readouts_preserve_y_and_frame_without_mutation() {
165        let mut sim = Simulator::with_seed(2, 0);
166        sim.h(0);
167        sim.t(0).unwrap();
168        sim.cx(0, 1).unwrap();
169        let mut output = OutputLogical {
170            port: glam::IVec3::ZERO,
171            logical_x: &PauliString::single(2, 0, Pauli::X) * &PauliString::single(2, 1, Pauli::X),
172            logical_z: PauliString::single(1, 0, Pauli::Z),
173            consumed: false,
174        };
175        let r = std::f64::consts::FRAC_1_SQRT_2;
176        for x_sign in [false, true] {
177            for z_sign in [false, true] {
178                output.logical_x.set_phase(2 * i32::from(x_sign));
179                output.logical_z.set_phase(2 * i32::from(z_sign));
180                let (x, y, z) = logical_bloch(&sim, &output, (false, false)).unwrap();
181                assert!((x - r * sign(x_sign)).abs() < 1e-9);
182                assert!((y - r * sign(x_sign ^ z_sign)).abs() < 1e-9);
183                assert!(z.abs() < 1e-9);
184                let signature = logical_signature(&sim, &[(&output, (z_sign, x_sign))]).unwrap();
185                assert!(signatures_match(&signature, &[r, r, 0.0]));
186            }
187        }
188        assert_eq!(sim.num_qubits(), 2);
189        assert_eq!(sim.peek_z(0).unwrap(), 0.0);
190        sim.reset(0).unwrap();
191        sim.reset(1).unwrap();
192        // Logical representatives may be narrower than the live engine after
193        // input injection or early-output capture allocates a private qubit.
194        sim.reset(2).unwrap();
195        assert_eq!(
196            logical_bloch(&sim, &output, (false, false)).unwrap().2,
197            -1.0
198        );
199        assert!(signatures_match(
200            &logical_signature(&sim, &[(&output, (true, false))]).unwrap(),
201            &[0.0, 0.0, 1.0],
202        ));
203    }
204
205    #[test]
206    fn joint_signature_extends_mixed_widths_and_preserves_signed_correlations() {
207        let mut sim = Simulator::with_seed(2, 0);
208        sim.h(0);
209        sim.cx(0, 1).unwrap();
210        let first = OutputLogical {
211            port: glam::IVec3::ZERO,
212            logical_x: PauliString::single(1, 0, Pauli::X),
213            logical_z: PauliString::single(1, 0, Pauli::Z),
214            consumed: false,
215        };
216        let mut second = OutputLogical {
217            port: glam::IVec3::X,
218            logical_x: PauliString::single(2, 1, Pauli::X),
219            logical_z: PauliString::single(2, 1, Pauli::Z),
220            consumed: false,
221        };
222        second.logical_z.set_phase(2);
223        let signature =
224            logical_signature(&sim, &[(&first, (false, false)), (&second, (true, false))]).unwrap();
225        let mut bell = vec![0.0; 15];
226        bell[4] = 1.0; // XX
227        bell[9] = -1.0; // YY
228        bell[14] = 1.0; // ZZ
229        assert!(signatures_match(&signature, &bell));
230
231        // 4^(word_bits/2) cannot fit in usize; reject before allocation or reads.
232        let count = usize::BITS as usize / 2;
233        assert!(matches!(
234            logical_signature(&sim, &vec![(&first, (false, false)); count]),
235            Err(ExecError::LogicalSignatureTooLarge { outputs }) if outputs == count,
236        ));
237    }
238
239    #[test]
240    fn ccz_orbit_accepts_paulis_and_rejects_non_clifford_twist() {
241        let mut sim = Simulator::with_seed(3, 0);
242        for q in 0..3 {
243            sim.h(q);
244        }
245        sim.ccz(0, 1, 2).unwrap();
246        sim.x(0);
247        let outputs: Vec<_> = (0..3)
248            .map(|q| OutputLogical {
249                port: glam::IVec3::ZERO,
250                logical_x: PauliString::single(3, q, Pauli::X),
251                logical_z: PauliString::single(3, q, Pauli::Z),
252                consumed: false,
253            })
254            .collect();
255        let outputs: Vec<_> = outputs.iter().map(|out| (out, (false, false))).collect();
256        let orbit = ccz_orbit_signatures();
257        assert_eq!(orbit.len(), 64);
258        for (i, member) in orbit.iter().enumerate() {
259            assert!(
260                !orbit[..i]
261                    .iter()
262                    .any(|other| signatures_match(member, other))
263            );
264        }
265        let signature = logical_signature(&sim, &outputs).unwrap();
266        assert!(
267            orbit
268                .iter()
269                .any(|member| signatures_match(member, &signature))
270        );
271        sim.t(0).unwrap();
272        let signature = logical_signature(&sim, &outputs).unwrap();
273        assert!(
274            !orbit
275                .iter()
276                .any(|member| signatures_match(member, &signature))
277        );
278    }
279}