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#[derive(Debug, Error)]
14pub enum StimVerifyError {
15 #[error("{0}")]
17 Emission(#[from] crate::StimEmissionError),
18 #[error("{0}")]
20 Circuit(#[from] CircuitError),
21 #[error("{0}")]
23 Flow(#[from] FlowError),
24 #[error("unmatched interface: {0}")]
26 UnmatchedInterface(String),
27 #[error("the chunk does not have the following flows: {}", flows.join("; "))]
30 ChunkDoesNotHaveFlows {
31 flows: Vec<String>,
33 },
34 #[error("loop body sub-chunk {index} failed flow verification")]
37 LoopChunk {
38 index: usize,
40 #[source]
42 source: Box<StimVerifyError>,
43 },
44 #[error("{0}")]
46 Stim(#[from] stim::StimError),
47}
48
49pub trait StimFlowVerifier {
54 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
160type FlowSide = fn(&Flow) -> &PauliMap;
163const FLOW_START: FlowSide = |flow| &flow.start;
164const FLOW_END: FlowSide = |flow| &flow.end;
165
166fn 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
178fn 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
199pub 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
231fn 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}