Skip to main content

bloq_stim/
stim_noise.rs

1//! Applying a `stim`-crate noise model to an emitted program, chunk by chunk.
2//!
3use bloq_ir::{Bloq, BloqNodeId};
4use rustc_hash::{FxHashMap, FxHashSet};
5use stim::{Circuit, CircuitInstruction, CircuitItem, GateTarget, noise::NoiseModel};
6use thiserror::Error;
7
8use crate::bloq::emit_bloq_stim_segments;
9use crate::emit::StimEmissionError;
10
11/// Reason a program could not be emitted with `stim`-side noise.
12#[derive(Debug, Error)]
13pub enum StimNoiseError {
14    /// The program could not be emitted in the first place.
15    #[error("{0}")]
16    Emission(#[from] StimEmissionError),
17    /// Stim rejected one node's chunk while parsing or noising it. The node id
18    /// is carried because a whole-program message would not say which chunk.
19    #[error("applying noise to node {node:?}: {source}")]
20    Node {
21        /// Node whose emitted chunk failed.
22        node: BloqNodeId,
23        /// Stim parse or noise-model failure.
24        #[source]
25        source: stim::StimError,
26    },
27}
28
29/// Emit `program` as Stim text with `noise` applied to each node's chunk
30/// separately.
31///
32/// Distinct from [`BloqStimOptions::with_noise`], which materializes a
33/// [`bloq_circuit::NoiseModel`] into each node's *instantiated circuit* before
34/// emission. This one runs stim's own noise models over the *emitted text*,
35/// and it does so per chunk for a reason: a node's leading and trailing `MPP`
36/// boundaries are ideal, noiseless operations (proxy ports, boundary
37/// observables), and stim's `noisy_circuit_skipping_mpp_boundaries` only
38/// recognizes them at the ends of the circuit it is handed. Noising the whole
39/// program in one call would bury each node's boundaries in the middle and
40/// noise them, and it would spread idle depolarization across qubits belonging
41/// to patches that are merely coexisting in time.
42///
43/// Chunks are reassembled afterwards, so cross-chunk `rec[-k]` targets resolve
44/// exactly as they do in [`emit_bloq_stim`](crate::emit_bloq_stim).
45///
46/// [`BloqStimOptions::with_noise`]: crate::BloqStimOptions::with_noise
47///
48/// # Errors
49///
50/// [`StimNoiseError::Emission`] if the program does not emit, and
51/// [`StimNoiseError::Node`] if stim rejects a chunk.
52///
53/// # Examples
54///
55/// ```no_run
56/// # use bloq_ir::Bloq;
57/// # use bloq_stim::emit_bloq_stim_with_stim_noise;
58/// # use stim::noise::UniformDepolarizing;
59/// # fn example(program: &Bloq) -> Result<(), Box<dyn std::error::Error>> {
60/// let noise = UniformDepolarizing::new(1e-3)?;
61/// let text = emit_bloq_stim_with_stim_noise(program, &noise)?;
62/// # Ok(())
63/// # }
64/// ```
65pub fn emit_bloq_stim_with_stim_noise(
66    program: &Bloq,
67    noise: &impl NoiseModel,
68) -> Result<String, StimNoiseError> {
69    let segments = emit_bloq_stim_segments(program)?;
70    let coords = program
71        .sorted_layout_coords()
72        .map_err(StimEmissionError::from)?;
73    let layout = crate::layout::coordinate_index(coords.iter().copied())?;
74    let mut out = segments.header;
75    for segment in segments.segments {
76        if segment.text.trim().is_empty() {
77            continue;
78        }
79        let circuit = segment
80            .text
81            .parse::<Circuit>()
82            .map_err(|source| StimNoiseError::Node {
83                node: segment.node_id,
84                source,
85            })?;
86        // A boundary-only node — a port `MPP` plus its boundary observables and
87        // a `TICK` — is the ideal noiseless boundary itself, and has no bulk to
88        // noise. stim reports that as an error (its noisy window closes empty),
89        // so recognize the shape up front and pass the chunk through instead of
90        // reading the failure back out of a message.
91        if (&circuit)
92            .into_iter()
93            .all(|item| is_mpp_boundary_item(&item))
94        {
95            out.push_str(&segment.text);
96            continue;
97        }
98        let noisy = noise
99            .noisy_circuit_skipping_mpp_boundaries(&circuit)
100            .map_err(|source| StimNoiseError::Node {
101                node: segment.node_id,
102                source,
103            })?;
104        let ideal = ideal_qubits(program, segment.node_id, &layout);
105        let noisy = exclude_ideal_noise(&noisy, &ideal).map_err(|source| StimNoiseError::Node {
106            node: segment.node_id,
107            source,
108        })?;
109        out.push_str(&noisy.to_string());
110        out.push('\n');
111    }
112    Ok(out)
113}
114
115fn ideal_qubits(
116    program: &Bloq,
117    node: BloqNodeId,
118    layout: &FxHashMap<glam::IVec2, u32>,
119) -> FxHashSet<u32> {
120    // Spec rule SEM-SPATIAL-PORT, applied to this emitted node segment.
121    let Some(quantum) = program[node].try_quantum() else {
122        return FxHashSet::default();
123    };
124    let mut derived = FxHashSet::default();
125    let mut source = FxHashSet::default();
126    for instance in &quantum.instances {
127        let template = program
128            .templates()
129            .get(instance.template_id)
130            .expect("successful emission resolved every template instance");
131        let owned = if instance.provenance.is_spatial_port_substitution() {
132            &mut derived
133        } else {
134            &mut source
135        };
136        owned.extend(
137            template
138                .qubits()
139                .iter()
140                .filter_map(|qubit| layout.get(&(*qubit + instance.offset)).copied()),
141        );
142    }
143    derived.difference(&source).copied().collect()
144}
145
146/// Remove error groups touching ideal qubits. Measurement instructions stay in
147/// place, but their flip argument becomes zero for an ideal target group.
148fn exclude_ideal_noise(
149    circuit: &Circuit,
150    ideal: &FxHashSet<u32>,
151) -> Result<Circuit, stim::StimError> {
152    if ideal.is_empty() {
153        return Ok(circuit.clone());
154    }
155    let mut clean = Circuit::new();
156    for item in circuit {
157        match item {
158            CircuitItem::Instruction(instruction) => {
159                append_clean_instruction(&mut clean, &instruction, ideal)?;
160            }
161            CircuitItem::RepeatBlock(block) => {
162                let body = exclude_ideal_noise(block.body(), ideal)?;
163                clean.append_repeat_block(block.repeat_count(), &body, block.tag())?;
164            }
165        }
166    }
167    Ok(clean)
168}
169
170fn append_clean_instruction(
171    clean: &mut Circuit,
172    instruction: &CircuitInstruction,
173    ideal: &FxHashSet<u32>,
174) -> Result<(), stim::StimError> {
175    if !instruction.gate().is_noisy_gate() || !touches_ideal(instruction.targets(), ideal) {
176        return clean.append_operation(instruction);
177    }
178
179    let measures = instruction.gate().produces_measurements();
180    let zero_args = vec![0.0; instruction.gate_args().len()];
181    for mut group in instruction.target_groups() {
182        let touches_ideal = touches_ideal(&group, ideal);
183        if touches_ideal && !measures {
184            continue;
185        }
186        if instruction.name() == "MPP" {
187            group = intersperse_combiners(&group);
188        }
189        let args = if touches_ideal {
190            &zero_args
191        } else {
192            instruction.gate_args()
193        };
194        clean.append_operation(CircuitInstruction::new(
195            instruction.gate(),
196            group,
197            args.iter().copied(),
198            instruction.tag(),
199        )?)?;
200    }
201    Ok(())
202}
203
204fn touches_ideal(targets: &[GateTarget], ideal: &FxHashSet<u32>) -> bool {
205    targets
206        .iter()
207        .filter_map(|target| target.qubit_value())
208        .any(|qubit| ideal.contains(&qubit))
209}
210
211fn intersperse_combiners(group: &[GateTarget]) -> Vec<GateTarget> {
212    let mut targets = Vec::with_capacity(group.len().saturating_mul(2).saturating_sub(1));
213    for (index, target) in group.iter().copied().enumerate() {
214        if index != 0 {
215            targets.push(GateTarget::combiner());
216        }
217        targets.push(target);
218    }
219    targets
220}
221
222/// Whether an item is one stim treats as an `MPP` boundary rather than bulk.
223/// Mirrors the private predicate behind
224/// `NoiseModel::noisy_circuit_skipping_mpp_boundaries`.
225fn is_mpp_boundary_item(item: &CircuitItem) -> bool {
226    let name = match item {
227        CircuitItem::Instruction(instruction) => instruction.name(),
228        CircuitItem::RepeatBlock(block) => block.name(),
229    };
230    matches!(
231        name,
232        "TICK" | "OBSERVABLE_INCLUDE" | "DETECTOR" | "MPP" | "QUBIT_COORDS" | "SHIFT_COORDS"
233    )
234}
235
236#[cfg(test)]
237mod tests {
238    use bloq_graph::{Block, BlockGraph, BlockKind, CubeKind, Direction, Pipe, PortRole};
239    use glam::IVec3;
240
241    use super::*;
242
243    #[test]
244    fn ideal_qubits_keep_measurements_but_lose_all_error_groups() {
245        let circuit: Circuit = "X_ERROR(0.1) 0 1\nDEPOLARIZE2(0.1) 0 1 1 2\nM(0.1) 0 1\nREPEAT 2 {\nX_ERROR(0.1) 0 2\n}"
246            .parse()
247            .unwrap();
248
249        let clean = exclude_ideal_noise(&circuit, &FxHashSet::from_iter([0])).unwrap();
250        let text = clean.to_string();
251
252        assert_eq!(
253            text,
254            "X_ERROR(0.1) 1\nDEPOLARIZE2(0.1) 1 2\nM(0) 0\nM(0.1) 1\nREPEAT 2 {\n    X_ERROR(0.1) 2\n}"
255        );
256    }
257
258    #[test]
259    fn multiplex_port_qubits_stay_ideal_in_both_noise_paths() {
260        let mut graph = BlockGraph::new();
261        graph.add_block(
262            Block::new(IVec3::ZERO, BlockKind::Port)
263                .with_port_role(PortRole::Multiplex)
264                .unwrap(),
265        );
266        graph.add_block(Block::new(IVec3::X, BlockKind::Cube(CubeKind::ZXZ)));
267        graph.add_pipe(Pipe::new(IVec3::ZERO, Direction::XPLUS));
268        let program = bloq_compile::compile(&graph, 3).expect("compile Multiplex Port");
269        let coords = program.sorted_layout_coords().unwrap();
270        let layout = coords
271            .iter()
272            .copied()
273            .enumerate()
274            .map(|(index, coord)| (coord, index as u32))
275            .collect::<FxHashMap<_, _>>();
276        let ideal = program
277            .quantum_nodes()
278            .flat_map(|(node, _)| ideal_qubits(&program, node, &layout))
279            .collect::<FxHashSet<_>>();
280        let output = program.logical_outputs()[0].x.iter().next().unwrap().0;
281        assert!(ideal.contains(&layout[output]));
282
283        let internal = bloq_circuit::NoiseModel::uniform_depolarizing(1e-3);
284        let internal_text = crate::emit_bloq_stim_with(
285            &program,
286            &crate::BloqStimOptions::new().with_noise(&internal),
287        )
288        .unwrap();
289        let external = stim::noise::UniformDepolarizing::new(1e-3).unwrap();
290        let external_text = emit_bloq_stim_with_stim_noise(&program, &external).unwrap();
291
292        for text in [internal_text, external_text] {
293            let circuit = text.parse::<Circuit>().unwrap();
294            assert_noise_free(&circuit, &ideal);
295        }
296    }
297
298    fn assert_noise_free(circuit: &Circuit, ideal: &FxHashSet<u32>) {
299        for item in circuit {
300            match item {
301                CircuitItem::Instruction(instruction) => {
302                    let touches_ideal = instruction
303                        .targets()
304                        .iter()
305                        .filter_map(|target| target.qubit_value())
306                        .any(|qubit| ideal.contains(&qubit));
307                    if touches_ideal && instruction.gate().is_noisy_gate() {
308                        assert!(
309                            instruction.gate().produces_measurements()
310                                && instruction.gate_args().iter().all(|&arg| arg == 0.0),
311                            "{instruction:?}"
312                        );
313                    }
314                }
315                CircuitItem::RepeatBlock(block) => assert_noise_free(block.body(), ideal),
316            }
317        }
318    }
319}