Skip to main content

bloq_stim/
verify.rs

1use glam::IVec2;
2use itertools::Itertools;
3use rustc_hash::FxHashMap;
4use thiserror::Error;
5
6use bloq_circuit::{Chunk, ChunkOrLoop, CircuitError, Flow, FlowError, PauliMap};
7
8use crate::emit::{emit_detector_records, emit_observable_include_records, emit_standalone_stim};
9use crate::layout::QubitLayout;
10use crate::measurement_frame::MeasurementFrame;
11
12/// Reason a chunk failed stim-based flow verification.
13#[derive(Debug, Error)]
14pub enum StimVerifyError {
15    /// The static backend cannot represent the circuit's layout.
16    #[error("{0}")]
17    Emission(#[from] crate::StimEmissionError),
18    /// Circuit construction or measurement resolution failed.
19    #[error("{0}")]
20    Circuit(#[from] CircuitError),
21    /// Declared flow boundaries could not be matched consistently.
22    #[error("{0}")]
23    Flow(#[from] FlowError),
24    /// A chunk's boundary does not match its expected interface.
25    #[error("unmatched interface: {0}")]
26    UnmatchedInterface(String),
27    /// Stim confirmed the emitted circuit does not carry the chunk's declared
28    /// flows; `flows` holds their string forms for diagnostics.
29    #[error("the chunk does not have the following flows: {}", flows.join("; "))]
30    ChunkDoesNotHaveFlows {
31        /// Declared flows that Stim rejected.
32        flows: Vec<String>,
33    },
34    /// A sub-chunk of a loop body failed verification; `index` is its position
35    /// within the body.
36    #[error("loop body sub-chunk {index} failed flow verification")]
37    LoopChunk {
38        /// Position of the failing chunk in the loop body.
39        index: usize,
40        /// Failure reported for that chunk.
41        #[source]
42        source: Box<StimVerifyError>,
43    },
44    /// Stim rejected emitted circuit or flow text.
45    #[error("{0}")]
46    Stim(#[from] stim::StimError),
47}
48
49/// Checks that a circuit fragment carries its declared stabilizer flows.
50///
51/// Implemented for [`Chunk`] and [`ChunkOrLoop`]: the fragment is emitted to Stim
52/// text and each declared [`Flow`] is verified against it.
53pub trait StimFlowVerifier {
54    /// Verifies this fragment's flows, optionally pinning its boundary ports.
55    ///
56    /// `expected_in`/`expected_out` assert the start/end interface port sets
57    /// (as multisets); `None` leaves that boundary unconstrained.
58    ///
59    /// # Errors
60    ///
61    /// Returns [`StimVerifyError`] if emission, parsing, or flow verification fails.
62    fn verify_flows(
63        &self,
64        expected_in: Option<&[PauliMap]>,
65        expected_out: Option<&[PauliMap]>,
66    ) -> Result<(), StimVerifyError>;
67}
68
69impl StimFlowVerifier for ChunkOrLoop {
70    fn verify_flows(
71        &self,
72        expected_in: Option<&[PauliMap]>,
73        expected_out: Option<&[PauliMap]>,
74    ) -> Result<(), StimVerifyError> {
75        match self {
76            ChunkOrLoop::Single(c) => c.verify_flows(expected_in, expected_out),
77            ChunkOrLoop::Loop {
78                body, repetitions, ..
79            } => {
80                if body.is_empty() {
81                    return Ok(());
82                }
83                let n = body.len();
84
85                let mut expected_ins: Vec<Option<Vec<PauliMap>>> =
86                    body.iter().map(|c| Some(side_ports(c, FLOW_END))).collect();
87                expected_ins.rotate_right(1);
88
89                let mut expected_outs: Vec<Option<Vec<PauliMap>>> = body
90                    .iter()
91                    .map(|c| Some(side_ports(c, FLOW_START)))
92                    .collect();
93                expected_outs.rotate_left(1);
94
95                if *repetitions <= 1 {
96                    expected_ins[0] = None;
97                    expected_outs[n - 1] = None;
98                }
99                if let Some(ext_in) = expected_in {
100                    expected_ins[0] = Some(ext_in.to_vec());
101                }
102                if let Some(ext_out) = expected_out {
103                    expected_outs[n - 1] = Some(ext_out.to_vec());
104                }
105
106                for (index, chunk) in body.iter().enumerate() {
107                    chunk
108                        .verify_flows(
109                            expected_ins[index].as_deref(),
110                            expected_outs[index].as_deref(),
111                        )
112                        .map_err(|error| StimVerifyError::LoopChunk {
113                            index,
114                            source: Box::new(error),
115                        })?;
116                }
117                Ok(())
118            }
119        }
120    }
121}
122
123impl StimFlowVerifier for Chunk {
124    fn verify_flows(
125        &self,
126        expected_in: Option<&[PauliMap]>,
127        expected_out: Option<&[PauliMap]>,
128    ) -> Result<(), StimVerifyError> {
129        let sides: [(&str, FlowSide, Option<&[PauliMap]>); 2] = [
130            ("START", FLOW_START, expected_in),
131            ("END", FLOW_END, expected_out),
132        ];
133
134        for (label, side, _) in sides {
135            for (&interface, group) in self.flows.iter().into_group_map_by(|&f| side(f)).iter() {
136                if !interface.is_empty() && group.len() > 1 {
137                    return Err(FlowError::Composition(format!(
138                        "multiple flows have the same {label} interface {interface}: {}",
139                        group.iter().map(ToString::to_string).join("; ")
140                    ))
141                    .into());
142                }
143            }
144        }
145        for (label, side, expected) in sides {
146            let Some(expected) = expected else { continue };
147            let actual = side_ports(self, side);
148            if ports_mismatch(expected, &actual) {
149                return Err(StimVerifyError::UnmatchedInterface(format!(
150                    "chunk {label} interface does not match; expected: {}; actual: {}",
151                    expected.iter().map(ToString::to_string).join(", "),
152                    actual.iter().map(ToString::to_string).join(", "),
153                )));
154            }
155        }
156        verify_stim_flows(self)
157    }
158}
159
160/// Which end of a flow an interface check reads. START and END are checked
161/// identically, so the side is a parameter rather than a duplicated code path.
162type FlowSide = fn(&Flow) -> &PauliMap;
163const FLOW_START: FlowSide = |flow| &flow.start;
164const FLOW_END: FlowSide = |flow| &flow.end;
165
166/// The non-empty interfaces on one side of every flow in `chunk`.
167fn side_ports(chunk: &Chunk, side: FlowSide) -> Vec<PauliMap> {
168    chunk
169        .flows
170        .iter()
171        .filter_map(|flow| {
172            let port = side(flow);
173            (!port.is_empty()).then(|| port.clone())
174        })
175        .collect()
176}
177
178/// Whether the two port lists differ as multisets, via hash counting rather
179/// than a quadratic scan (which would also wrongly accept a duplicated
180/// expected port matched by a single actual one).
181fn ports_mismatch(expected: &[PauliMap], actual: &[PauliMap]) -> bool {
182    if expected.len() != actual.len() {
183        return true;
184    }
185    let mut counts: FxHashMap<&PauliMap, usize> =
186        FxHashMap::with_capacity_and_hasher(expected.len(), Default::default());
187    for port in expected {
188        *counts.entry(port).or_default() += 1;
189    }
190    actual.iter().any(|port| match counts.get_mut(port) {
191        Some(count) if *count > 0 => {
192            *count -= 1;
193            false
194        }
195        _ => true,
196    })
197}
198
199/// Emits a flat coordinate circuit as standalone Stim text.
200///
201/// Trailing `DETECTOR` and `OBSERVABLE_INCLUDE` lines resolve circuit
202/// measurement ids to record lookbacks in emission order.
203///
204/// This is the escape-template distance-test entry point: restart parities are
205/// emitted as ordinary `DETECTOR` lines — msc-ls-faithful, since its `.stim`
206/// output carries every post-selected parity as a plain detector and keeps the
207/// post-selection index list on the Python side. Loop bodies are unsupported
208/// (`resolve_measurement` fails on unemitted ids); the T-block templates are
209/// all flat [`Single`](ChunkOrLoop::Single) chunks.
210///
211/// # Errors
212///
213/// Returns [`StimVerifyError`] if circuit emission or measurement resolution fails.
214pub fn emit_annotated_stim(
215    circuit: &bloq_circuit::CoordCircuit,
216    detectors: &[Vec<u32>],
217    observables: &[(u32, Vec<u32>)],
218) -> Result<String, StimVerifyError> {
219    let (mut stim_text, frame) = emit_bare_stim(circuit)?;
220    for parity in detectors {
221        let lookbacks = resolve_lookbacks(&frame, parity)?;
222        emit_detector_records(&mut stim_text, &lookbacks);
223    }
224    for (index, measurements) in observables {
225        let lookbacks = resolve_lookbacks(&frame, measurements)?;
226        emit_observable_include_records(&mut stim_text, *index, &lookbacks);
227    }
228    Ok(stim_text)
229}
230
231/// Resolve measurement ids to `rec[...]` lookbacks against the frame, sorted
232/// like the program emitter's annotation lookbacks.
233fn resolve_lookbacks(
234    frame: &MeasurementFrame,
235    measurements: &[u32],
236) -> Result<Vec<i32>, CircuitError> {
237    let mut lookbacks = measurements
238        .iter()
239        .map(|&measurement| frame.resolve_measurement(measurement))
240        .collect::<Result<Vec<_>, CircuitError>>()?;
241    lookbacks.sort_unstable();
242    Ok(lookbacks)
243}
244
245fn emit_bare_stim(
246    circuit: &bloq_circuit::CoordCircuit,
247) -> Result<(String, MeasurementFrame), StimVerifyError> {
248    let layout = QubitLayout::new(circuit.build_coord_to_index())?;
249    Ok(emit_standalone_stim(circuit, None, &layout, true)?)
250}
251
252fn verify_stim_flows(chunk: &Chunk) -> Result<(), StimVerifyError> {
253    let layout = chunk.circuit.build_coord_to_index();
254    let (stim_text, frame) = emit_bare_stim(&chunk.circuit)?;
255    let stim_circuit = stim_text.parse::<stim::Circuit>()?;
256    let num_qubits = chunk.circuit.num_qubits();
257    let flows = chunk
258        .flows
259        .iter()
260        .map(|flow| flow_to_stim_flow(flow, &layout, num_qubits, &frame))
261        .collect::<Result<Vec<_>, StimVerifyError>>()?;
262    if !stim_circuit.has_all_flows(&flows, false)? {
263        let mut missing = Vec::new();
264        for (declared, flow) in chunk.flows.iter().zip(&flows) {
265            if !stim_circuit.has_flow(flow, false)? {
266                missing.push(format!("{declared}: {flow}"));
267            }
268        }
269        return Err(StimVerifyError::ChunkDoesNotHaveFlows { flows: missing });
270    }
271    Ok(())
272}
273
274fn flow_to_stim_flow(
275    flow: &Flow,
276    layout: &FxHashMap<IVec2, u32>,
277    num_qubits: u32,
278    frame: &MeasurementFrame,
279) -> Result<stim::Flow, StimVerifyError> {
280    let lhs = flow_pauli_string(&flow.start, layout, num_qubits)?;
281    let lhs = if flow.sign { format!("-{lhs}") } else { lhs };
282    let mut rhs_terms = Vec::new();
283    if !flow.end.is_empty() {
284        rhs_terms.push(flow_pauli_string(&flow.end, layout, num_qubits)?);
285    }
286    for &measurement in &flow.measurements {
287        let lookback = frame.resolve_measurement(measurement)?;
288        rhs_terms.push(format!("rec[{lookback}]"));
289    }
290    if rhs_terms.is_empty() {
291        rhs_terms.push(String::from("1"));
292    }
293    let text = format!("{lhs} -> {}", rhs_terms.join(" xor "));
294    Ok(stim::Flow::new(&text)?)
295}
296
297fn flow_pauli_string(
298    paulis: &PauliMap,
299    layout: &FxHashMap<IVec2, u32>,
300    num_qubits: u32,
301) -> Result<String, StimVerifyError> {
302    if paulis.is_empty() {
303        Ok(String::from("1"))
304    } else {
305        Ok(paulis.to_pauli_string(layout, num_qubits as usize)?)
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use std::error::Error as _;
312
313    use bloq_circuit::{GateType, Pauli};
314
315    use super::*;
316
317    #[test]
318    fn verifier_reports_interface_failures_without_circuit_wrappers() {
319        let boundary: PauliMap = [(IVec2::ZERO, Pauli::Z)].into_iter().collect();
320        let flow = Flow::new(boundary, PauliMap::empty());
321        let mut chunk = Chunk {
322            circuit: bloq_circuit::CoordCircuit::new(),
323            flows: vec![flow.clone(), flow],
324        };
325        let error = chunk.verify_flows(None, None).unwrap_err();
326        assert!(matches!(error, StimVerifyError::Flow(_)));
327        assert!(error.source().unwrap().is::<FlowError>());
328        assert!(
329            error
330                .to_string()
331                .contains("multiple flows have the same START interface")
332        );
333
334        chunk.flows.pop();
335        let error = chunk.verify_flows(Some(&[]), None).unwrap_err();
336        assert!(matches!(error, StimVerifyError::UnmatchedInterface(_)));
337        assert!(
338            error
339                .to_string()
340                .starts_with("unmatched interface: chunk START interface")
341        );
342    }
343
344    #[test]
345    fn emission_failure_preserves_each_error_layer() {
346        let error = StimVerifyError::from(crate::StimEmissionError::from(CircuitError::from(
347            bloq_circuit::MeasurementFrameError::MeasurementCountOutOfRange {
348                measurements: usize::MAX,
349            },
350        )));
351        let emission = error.source().unwrap();
352        assert!(emission.is::<crate::StimEmissionError>());
353        let circuit = emission.source().unwrap();
354        assert!(circuit.is::<CircuitError>());
355        assert!(
356            circuit
357                .source()
358                .unwrap()
359                .is::<bloq_circuit::MeasurementFrameError>()
360        );
361    }
362
363    #[test]
364    fn stim_flow_verification_checks_signs() {
365        let qubit = IVec2::ZERO;
366        let mut circuit = bloq_circuit::CoordCircuit::new();
367        circuit.do_gate(GateType::RZ, [qubit]).unwrap();
368        circuit.tick();
369        circuit.do_gate(GateType::X, [qubit]).unwrap();
370        let boundary: PauliMap = [(qubit, Pauli::Z)].into_iter().collect();
371
372        let signed = Chunk {
373            circuit: circuit.clone(),
374            flows: vec![Flow::new(PauliMap::empty(), boundary.clone()).with_sign(true)],
375        };
376        signed.verify_flows(None, None).unwrap();
377
378        let unsigned = Chunk {
379            circuit,
380            flows: vec![Flow::new(PauliMap::empty(), boundary)],
381        };
382        assert!(matches!(
383            unsigned.verify_flows(None, None),
384            Err(StimVerifyError::ChunkDoesNotHaveFlows { .. })
385        ));
386    }
387}