1use glam::IVec2;
8use rustc_hash::{FxHashMap, FxHashSet};
9
10use crate::{BodyId, CircuitError, CoordCircuit, GateType, Op, PauliBasis};
11
12#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize)]
15pub struct NoiseModel {
16 pub p1: f64,
18 pub p2: f64,
20 pub p_meas: f64,
22 pub p_reset: f64,
24 pub p_idle: f64,
26}
27
28impl NoiseModel {
29 #[must_use]
31 pub const fn uniform_depolarizing(probability: f64) -> Self {
32 Self {
33 p1: probability,
34 p2: probability,
35 p_meas: probability,
36 p_reset: probability,
37 p_idle: probability,
38 }
39 }
40
41 pub fn requires_repeat_flattening(&self, circuit: &CoordCircuit) -> bool {
45 self.p_idle != 0.0 && idle_body_shape(circuit, circuit.entry_body()).is_none()
46 }
47
48 pub fn noisy_circuit(&self, circuit: &CoordCircuit) -> Result<CoordCircuit, CircuitError> {
54 self.noisy_circuit_excluding(circuit, &[])
55 }
56
57 pub fn noisy_circuit_excluding(
68 &self,
69 circuit: &CoordCircuit,
70 ideal_qubits: &[IVec2],
71 ) -> Result<CoordCircuit, CircuitError> {
72 let mut noisy = circuit.clone();
73 if self.requires_repeat_flattening(circuit) {
74 noisy.flatten()?;
76 }
77 let footprint = noisy.qubits();
78 let ideal_qubits = ideal_qubits.iter().copied().collect::<FxHashSet<_>>();
79 for index in 0..noisy.body_count() {
80 let body = crate::circuit::checked_body_id(index)?;
81 annotate_body(&mut noisy, body, &footprint, &ideal_qubits, self);
82 }
83 Ok(noisy)
84 }
85}
86
87fn idle_body_shape(circuit: &CoordCircuit, entry: BodyId) -> Option<bool> {
92 let mut cache = FxHashMap::default();
93 let mut visiting = FxHashSet::default();
94 visiting.insert(entry);
95 let mut pending = vec![(entry, 0usize, false)];
96 while let Some((body, next, active)) = pending.last_mut() {
97 let Some(op) = circuit.body(*body)?.ops().get(*next) else {
98 cache.insert(*body, *active);
99 visiting.remove(body);
100 pending.pop();
101 continue;
102 };
103 *next += 1;
104 match op {
105 Op::Repeat { body, repetitions } if *repetitions != 0 => {
106 if *active {
107 return None;
108 }
109 if let Some(&unfinished) = cache.get(body) {
110 if unfinished {
111 return None;
112 }
113 } else {
114 if !visiting.insert(*body) {
115 return None;
116 }
117 *next -= 1;
119 pending.push((*body, 0, false));
120 }
121 }
122 Op::Tick => *active = false,
123 Op::Gate { qubits, .. } | Op::Measure { qubits, .. } => *active |= !qubits.is_empty(),
124 Op::MPP { products, .. } => {
125 *active |= products.iter().any(|product| !product.is_empty())
126 }
127 Op::ConditionalPauli(corrections) => *active |= !corrections.is_empty(),
128 Op::Repeat { .. }
129 | Op::Depolarize1 { .. }
130 | Op::Depolarize2 { .. }
131 | Op::PauliError { .. } => {}
132 }
133 }
134 cache.get(&entry).copied()
135}
136
137fn annotate_body(
138 circuit: &mut CoordCircuit,
139 body: BodyId,
140 footprint: &FxHashSet<IVec2>,
141 ideal_qubits: &FxHashSet<IVec2>,
142 noise: &NoiseModel,
143) {
144 let body = circuit
145 .body_mut(body)
146 .expect("body id comes from this circuit's dense body range");
147 let source = std::mem::take(body.ops_mut());
148 let mut ops = Vec::with_capacity(source.len() * 2);
149 let mut active = FxHashSet::default();
150
151 for mut op in source {
152 match &mut op {
153 Op::Gate { gate, qubits } => {
154 active.extend(qubits.iter().copied());
158 let error = gate_noise(*gate, qubits, ideal_qubits, noise);
159 ops.push(op);
160 if let Some(error) = error {
161 ops.push(error);
162 }
163 }
164 Op::Measure {
165 basis,
166 qubits,
167 measurements,
168 flip_probability,
169 } => {
170 active.extend(qubits.iter().copied());
171 if qubits.iter().any(|qubit| ideal_qubits.contains(qubit)) {
172 for (&qubit, &measurement) in qubits.iter().zip(measurements.iter()) {
173 ops.push(Op::Measure {
174 basis: *basis,
175 qubits: vec![qubit],
176 measurements: vec![measurement],
177 flip_probability: if ideal_qubits.contains(&qubit) {
178 0.0
179 } else {
180 compose_flip_probabilities(*flip_probability, noise.p_meas)
181 },
182 });
183 }
184 } else {
185 *flip_probability = compose_flip_probabilities(*flip_probability, noise.p_meas);
186 ops.push(op);
187 }
188 }
189 Op::MPP { products, .. } => {
190 active.extend(
191 products
192 .iter()
193 .flat_map(|product| product.iter().map(|(coord, _)| *coord)),
194 );
195 ops.push(op);
196 }
197 Op::ConditionalPauli(corrections) => {
198 active.extend(corrections.iter().map(|correction| correction.target));
199 ops.push(op);
200 }
201 Op::Tick => {
202 ops.push(op);
203 if noise.p_idle != 0.0 {
204 let mut idle: Vec<_> = footprint
205 .difference(&active)
206 .filter(|qubit| !ideal_qubits.contains(*qubit))
207 .copied()
208 .collect();
209 idle.sort_unstable_by_key(|coord| (coord.x, coord.y));
210 if !idle.is_empty() {
211 ops.push(Op::Depolarize1 {
212 probability: noise.p_idle,
213 qubits: idle,
214 });
215 }
216 }
217 active.clear();
218 }
219 Op::Depolarize1 { qubits, .. } | Op::PauliError { qubits, .. } => {
220 *qubits = nonideal_qubits(qubits, ideal_qubits);
221 if !qubits.is_empty() {
222 ops.push(op);
223 }
224 }
225 Op::Depolarize2 { qubits, .. } => {
226 *qubits = nonideal_pairs(qubits, ideal_qubits);
227 if !qubits.is_empty() {
228 ops.push(op);
229 }
230 }
231 Op::Repeat { .. } => ops.push(op),
232 }
233 }
234
235 *body.ops_mut() = ops;
236}
237
238fn gate_noise(
239 gate: GateType,
240 qubits: &[IVec2],
241 ideal_qubits: &FxHashSet<IVec2>,
242 noise: &NoiseModel,
243) -> Option<Op> {
244 if gate.is_reset() {
245 let qubits = nonideal_qubits(qubits, ideal_qubits);
246 return (noise.p_reset != 0.0 && !qubits.is_empty()).then(|| Op::PauliError {
247 probability: noise.p_reset,
248 pauli: match gate {
249 GateType::RX => PauliBasis::Z,
250 GateType::RY | GateType::RZ => PauliBasis::X,
251 _ => unreachable!("is_reset is exactly RX/RY/RZ"),
252 },
253 qubits,
254 });
255 }
256 if gate.is_two_qubit_gate() {
257 let qubits = nonideal_pairs(qubits, ideal_qubits);
258 return (noise.p2 != 0.0 && !qubits.is_empty()).then(|| Op::Depolarize2 {
259 probability: noise.p2,
260 qubits,
261 });
262 }
263 let qubits = nonideal_qubits(qubits, ideal_qubits);
264 (noise.p1 != 0.0 && !qubits.is_empty()).then(|| Op::Depolarize1 {
265 probability: noise.p1,
266 qubits,
267 })
268}
269
270fn nonideal_qubits(qubits: &[IVec2], ideal: &FxHashSet<IVec2>) -> Vec<IVec2> {
271 qubits
272 .iter()
273 .filter(|qubit| !ideal.contains(qubit))
274 .copied()
275 .collect()
276}
277
278fn nonideal_pairs(qubits: &[IVec2], ideal: &FxHashSet<IVec2>) -> Vec<IVec2> {
279 qubits
280 .as_chunks::<2>()
281 .0
282 .iter()
283 .filter(|pair| pair.iter().all(|qubit| !ideal.contains(qubit)))
284 .flatten()
285 .copied()
286 .collect()
287}
288
289fn compose_flip_probabilities(left: f64, right: f64) -> f64 {
290 left + right - 2.0 * left * right
291}
292
293#[cfg(test)]
294mod tests {
295 use glam::ivec2;
296
297 use super::*;
298
299 #[test]
300 fn idle_noise_propagates_expansion_limits_without_panicking() {
301 let mut circuit = CoordCircuit::new();
302 let body = circuit.add_body(crate::CircuitBody::from_ops(vec![Op::Gate {
303 gate: GateType::H,
304 qubits: vec![IVec2::ZERO],
305 }]));
306 circuit.push_repeat(body, u32::MAX);
307 let noise = NoiseModel::uniform_depolarizing(0.1);
308 assert!(matches!(
309 noise.noisy_circuit(&circuit),
310 Err(CircuitError::FlattenResourceLimit { .. })
311 ));
312 }
313
314 #[test]
315 fn idle_noise_is_unchanged_by_repeat_expansion() {
316 let a = ivec2(0, 0);
317 let b = ivec2(1, 0);
318 let noise = NoiseModel {
319 p_idle: 0.125,
320 ..NoiseModel::uniform_depolarizing(0.0)
321 };
322 for close_body in [false, true] {
323 for repetitions in [1, 3] {
324 let mut circuit = CoordCircuit::new();
325 circuit.do_gate(GateType::H, [a]).unwrap();
326 let mut ops = vec![Op::Gate {
327 gate: GateType::H,
328 qubits: vec![b],
329 }];
330 if close_body {
331 ops.push(Op::Tick);
332 }
333 let body = circuit.add_body(crate::CircuitBody::from_ops(ops));
334 circuit.push_repeat(body, repetitions);
335 circuit.tick();
336 let mut flat = circuit.clone();
337 flat.flatten().unwrap();
338 let expected = noise.noisy_circuit(&flat).unwrap();
339 let mut actual = noise.noisy_circuit(&circuit).unwrap();
340 actual.flatten().unwrap();
341 assert_eq!(
342 actual.body(actual.entry_body()).unwrap().ops(),
343 expected.body(expected.entry_body()).unwrap().ops()
344 );
345 }
346 }
347
348 let mut closed = CoordCircuit::new();
349 closed.do_gate(GateType::H, [a]).unwrap();
350 closed.tick();
351 let body = closed.add_body(crate::CircuitBody::from_ops(vec![
352 Op::Gate {
353 gate: GateType::H,
354 qubits: vec![b],
355 },
356 Op::Tick,
357 ]));
358 closed.push_repeat(body, 100);
359 assert!(!noise.requires_repeat_flattening(&closed));
360 assert_eq!(
361 noise
362 .noisy_circuit(&closed)
363 .unwrap()
364 .entry_top_level_repeats(),
365 [100]
366 );
367 }
368
369 #[test]
370 fn model_materializes_gate_reset_measurement_and_idle_noise() {
371 let q0 = ivec2(0, 0);
372 let q1 = ivec2(1, 0);
373 let mut circuit = CoordCircuit::new();
374 circuit.do_gate(GateType::RZ, [q0]).unwrap();
375 circuit.tick();
376 circuit.do_gate(GateType::H, [q0]).unwrap();
377 circuit.do_gate(GateType::CX, [q0, q1]).unwrap();
378 circuit.measure(PauliBasis::Z, [q0]);
379
380 let noisy = NoiseModel {
381 p1: 0.1,
382 p2: 0.2,
383 p_meas: 0.3,
384 p_reset: 0.4,
385 p_idle: 0.5,
386 }
387 .noisy_circuit(&circuit)
388 .unwrap();
389 let ops = noisy.body(noisy.entry_body()).unwrap().ops();
390
391 assert!(matches!(
392 &ops[1],
393 Op::PauliError { probability, pauli: PauliBasis::X, qubits }
394 if *probability == 0.4 && qubits == &[q0]
395 ));
396 assert!(matches!(
397 &ops[3],
398 Op::Depolarize1 { probability, qubits }
399 if *probability == 0.5 && qubits == &[q1]
400 ));
401 assert!(ops.iter().any(|op| matches!(
402 op,
403 Op::Depolarize1 { probability, qubits }
404 if *probability == 0.1 && qubits == &[q0]
405 )));
406 assert!(ops.iter().any(|op| matches!(
407 op,
408 Op::Depolarize2 { probability, qubits }
409 if *probability == 0.2 && qubits == &[q0, q1]
410 )));
411 assert!(ops.iter().any(|op| matches!(
412 op,
413 Op::Measure { flip_probability, .. } if *flip_probability == 0.3
414 )));
415 }
416
417 #[test]
418 fn applying_zero_noise_keeps_operation_stream_unchanged() {
419 let mut circuit = CoordCircuit::new();
420 circuit.do_gate(GateType::H, [IVec2::ZERO]).unwrap();
421 circuit.measure(PauliBasis::Z, [IVec2::ZERO]);
422 let expected = circuit.body(circuit.entry_body()).unwrap().ops().to_vec();
423
424 let noisy = NoiseModel::uniform_depolarizing(0.0)
425 .noisy_circuit(&circuit)
426 .unwrap();
427
428 assert_eq!(noisy.body(noisy.entry_body()).unwrap().ops(), expected);
429 }
430
431 #[test]
432 fn excluded_qubits_receive_no_error_channel() {
433 let ideal = ivec2(0, 0);
434 let real = ivec2(1, 0);
435 let other = ivec2(2, 0);
436 let mut circuit = CoordCircuit::new();
437 circuit.do_gate(GateType::RZ, [ideal, real]).unwrap();
438 circuit.do_gate(GateType::H, [ideal, real]).unwrap();
439 circuit.do_gate(GateType::CX, [ideal, real]).unwrap();
440 circuit.do_gate(GateType::CX, [real, other]).unwrap();
441 circuit.measure(PauliBasis::Z, [ideal, real]);
442
443 let noisy = NoiseModel::uniform_depolarizing(0.1)
444 .noisy_circuit_excluding(&circuit, &[ideal])
445 .unwrap();
446 let ops = noisy.body(noisy.entry_body()).unwrap().ops();
447
448 for op in ops {
449 match op {
450 Op::Depolarize1 { qubits, .. } | Op::PauliError { qubits, .. } => {
451 assert!(!qubits.contains(&ideal), "{op:?}");
452 }
453 Op::Depolarize2 { qubits, .. } => {
454 assert!(
455 qubits
456 .as_chunks::<2>()
457 .0
458 .iter()
459 .all(|pair| !pair.contains(&ideal)),
460 "{op:?}"
461 );
462 }
463 Op::Measure {
464 qubits,
465 flip_probability,
466 ..
467 } if qubits == &[ideal] => assert_eq!(*flip_probability, 0.0),
468 _ => {}
469 }
470 }
471 assert!(ops.iter().any(|op| matches!(
472 op,
473 Op::Depolarize2 { qubits, .. } if qubits == &[real, other]
474 )));
475 assert!(ops.iter().any(|op| matches!(
476 op,
477 Op::Measure { qubits, flip_probability, .. }
478 if qubits == &[real] && *flip_probability > 0.0
479 )));
480 }
481}