1use 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#[derive(Debug, Error)]
13pub enum StimNoiseError {
14 #[error("{0}")]
16 Emission(#[from] StimEmissionError),
17 #[error("applying noise to node {node:?}: {source}")]
20 Node {
21 node: BloqNodeId,
23 #[source]
25 source: stim::StimError,
26 },
27}
28
29pub 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 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 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
146fn 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
222fn 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}