Skip to content

Commit 04868d7

Browse files
DmitryVasilevskyDmitry Vasilevsky
and
Dmitry Vasilevsky
authored
Added Pauli noise support to sparse simulator (#1971)
Added support for parametric Pauli noise to sparce simulator. (px, py, pz) can be provided. Sparse simulator will apply X, Y or Z gates after each gate and before each measurement with corresponding probability. - This allows for simulation with parametric Pauli noise: bit flip, phase flip, depolarizing, etc. - For two-qubit gates noise is applied to each qubit. - Allocation and service functions do not apply noise. Idle (Identity) gate is also noiseless. - Reset applies noise. - Added tests. - Released qubits are no longer checked to be in zero state by consumers of the backend via calling qubit_is_zero. Instead, qubit_release returns Boolean. True indicates, that the release was valid - the qubit was in a zero state in a noiseless simulation. It will always be true in noisy simulations when the release of a qubit is always valid. - The functionality is not yet exposed outside Rust code. Will come in subsequent PRs. --------- Co-authored-by: Dmitry Vasilevsky <[email protected]>
1 parent 5b95d95 commit 04868d7

File tree

8 files changed

+390
-40
lines changed

8 files changed

+390
-40
lines changed

Diff for: compiler/qsc_circuit/src/builder.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,9 @@ impl Backend for Builder {
174174
self.remapper.qubit_allocate()
175175
}
176176

177-
fn qubit_release(&mut self, q: usize) {
177+
fn qubit_release(&mut self, q: usize) -> bool {
178178
self.remapper.qubit_release(q);
179+
true
179180
}
180181

181182
fn qubit_swap_id(&mut self, q0: usize, q1: usize) {
@@ -187,8 +188,8 @@ impl Backend for Builder {
187188
}
188189

189190
fn qubit_is_zero(&mut self, _q: usize) -> bool {
190-
// Because `qubit_is_zero` is called on every qubit release, this must return
191-
// true to avoid a panic.
191+
// We don't simulate quantum execution here. So we don't know if the qubit
192+
// is zero or not. Returning true avoids potential panics.
192193
true
193194
}
194195

Diff for: compiler/qsc_eval/src/backend.rs

+136-32
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
use crate::noise::PauliNoise;
5+
use crate::val::Value;
46
use num_bigint::BigUint;
57
use num_complex::Complex;
68
use quantum_sparse_sim::QuantumSim;
7-
use rand::RngCore;
9+
use rand::{rngs::StdRng, Rng, RngCore, SeedableRng};
810

9-
use crate::val::Value;
11+
#[cfg(test)]
12+
mod noise_tests;
1013

1114
/// The trait that must be implemented by a quantum backend, whose functions will be invoked when
1215
/// quantum intrinsics are called.
@@ -82,7 +85,11 @@ pub trait Backend {
8285
fn qubit_allocate(&mut self) -> usize {
8386
unimplemented!("qubit_allocate operation");
8487
}
85-
fn qubit_release(&mut self, _q: usize) {
88+
/// `false` indicates that the qubit was in a non-zero state before the release,
89+
/// but should have been in the zero state.
90+
/// `true` otherwise. This includes the case when the qubit was in
91+
/// a non-zero state during a noisy simulation, which is allowed.
92+
fn qubit_release(&mut self, _q: usize) -> bool {
8693
unimplemented!("qubit_release operation");
8794
}
8895
fn qubit_swap_id(&mut self, _q0: usize, _q1: usize) {
@@ -102,7 +109,14 @@ pub trait Backend {
102109

103110
/// Default backend used when targeting sparse simulation.
104111
pub struct SparseSim {
112+
/// Noiseless Sparse simulator to be used by this instance.
105113
pub sim: QuantumSim,
114+
/// Pauli noise that is applied after a gate or before a measurement is executed.
115+
/// Service functions aren't subject to noise.
116+
pub noise: PauliNoise,
117+
/// Random number generator to sample Pauli noise.
118+
/// Noise is not applied when rng is None.
119+
pub rng: Option<StdRng>,
106120
}
107121

108122
impl Default for SparseSim {
@@ -116,132 +130,213 @@ impl SparseSim {
116130
pub fn new() -> Self {
117131
Self {
118132
sim: QuantumSim::new(None),
133+
noise: PauliNoise::default(),
134+
rng: None,
135+
}
136+
}
137+
138+
#[must_use]
139+
pub fn new_with_noise(noise: &PauliNoise) -> Self {
140+
Self {
141+
sim: QuantumSim::new(None),
142+
noise: *noise,
143+
rng: if noise.is_noiseless() {
144+
None
145+
} else {
146+
Some(StdRng::from_entropy())
147+
},
119148
}
120149
}
150+
151+
#[must_use]
152+
fn is_noiseless(&self) -> bool {
153+
self.rng.is_none()
154+
}
155+
156+
fn apply_noise(&mut self, q: usize) {
157+
if let Some(rng) = &mut self.rng {
158+
let p = rng.gen_range(0.0..1.0);
159+
if p >= self.noise.distribution[2] {
160+
// In the most common case we don't apply noise
161+
} else if p < self.noise.distribution[0] {
162+
self.sim.x(q);
163+
} else if p < self.noise.distribution[1] {
164+
self.sim.y(q);
165+
} else {
166+
self.sim.z(q);
167+
}
168+
}
169+
// No noise applied if rng is None.
170+
}
121171
}
122172

123173
impl Backend for SparseSim {
124174
type ResultType = bool;
125175

126176
fn ccx(&mut self, ctl0: usize, ctl1: usize, q: usize) {
127177
self.sim.mcx(&[ctl0, ctl1], q);
178+
self.apply_noise(ctl0);
179+
self.apply_noise(ctl1);
180+
self.apply_noise(q);
128181
}
129182

130183
fn cx(&mut self, ctl: usize, q: usize) {
131184
self.sim.mcx(&[ctl], q);
185+
self.apply_noise(ctl);
186+
self.apply_noise(q);
132187
}
133188

134189
fn cy(&mut self, ctl: usize, q: usize) {
135190
self.sim.mcy(&[ctl], q);
191+
self.apply_noise(ctl);
192+
self.apply_noise(q);
136193
}
137194

138195
fn cz(&mut self, ctl: usize, q: usize) {
139196
self.sim.mcz(&[ctl], q);
197+
self.apply_noise(ctl);
198+
self.apply_noise(q);
140199
}
141200

142201
fn h(&mut self, q: usize) {
143202
self.sim.h(q);
203+
self.apply_noise(q);
144204
}
145205

146206
fn m(&mut self, q: usize) -> Self::ResultType {
207+
self.apply_noise(q);
147208
self.sim.measure(q)
148209
}
149210

150211
fn mresetz(&mut self, q: usize) -> Self::ResultType {
212+
self.apply_noise(q); // Applying noise before measurement
151213
let res = self.sim.measure(q);
152214
if res {
153215
self.sim.x(q);
154216
}
217+
self.apply_noise(q); // Applying noise after reset
155218
res
156219
}
157220

158221
fn reset(&mut self, q: usize) {
159222
self.mresetz(q);
223+
// Noise applied in mresetz.
160224
}
161225

162226
fn rx(&mut self, theta: f64, q: usize) {
163227
self.sim.rx(theta, q);
228+
self.apply_noise(q);
164229
}
165230

166231
fn rxx(&mut self, theta: f64, q0: usize, q1: usize) {
167-
self.h(q0);
168-
self.h(q1);
169-
self.rzz(theta, q0, q1);
170-
self.h(q1);
171-
self.h(q0);
232+
self.sim.h(q0);
233+
self.sim.h(q1);
234+
self.sim.mcx(&[q1], q0);
235+
self.sim.rz(theta, q0);
236+
self.sim.mcx(&[q1], q0);
237+
self.sim.h(q1);
238+
self.sim.h(q0);
239+
self.apply_noise(q0);
240+
self.apply_noise(q1);
172241
}
173242

174243
fn ry(&mut self, theta: f64, q: usize) {
175244
self.sim.ry(theta, q);
245+
self.apply_noise(q);
176246
}
177247

178248
fn ryy(&mut self, theta: f64, q0: usize, q1: usize) {
179-
self.h(q0);
180-
self.s(q0);
181-
self.h(q0);
182-
self.h(q1);
183-
self.s(q1);
184-
self.h(q1);
185-
self.rzz(theta, q0, q1);
186-
self.h(q1);
187-
self.sadj(q1);
188-
self.h(q1);
189-
self.h(q0);
190-
self.sadj(q0);
191-
self.h(q0);
249+
self.sim.h(q0);
250+
self.sim.s(q0);
251+
self.sim.h(q0);
252+
self.sim.h(q1);
253+
self.sim.s(q1);
254+
self.sim.h(q1);
255+
self.sim.mcx(&[q1], q0);
256+
self.sim.rz(theta, q0);
257+
self.sim.mcx(&[q1], q0);
258+
self.sim.h(q1);
259+
self.sim.sadj(q1);
260+
self.sim.h(q1);
261+
self.sim.h(q0);
262+
self.sim.sadj(q0);
263+
self.sim.h(q0);
264+
self.apply_noise(q0);
265+
self.apply_noise(q1);
192266
}
193267

194268
fn rz(&mut self, theta: f64, q: usize) {
195269
self.sim.rz(theta, q);
270+
self.apply_noise(q);
196271
}
197272

198273
fn rzz(&mut self, theta: f64, q0: usize, q1: usize) {
199-
self.cx(q1, q0);
200-
self.rz(theta, q0);
201-
self.cx(q1, q0);
274+
self.sim.mcx(&[q1], q0);
275+
self.sim.rz(theta, q0);
276+
self.sim.mcx(&[q1], q0);
277+
self.apply_noise(q0);
278+
self.apply_noise(q1);
202279
}
203280

204281
fn sadj(&mut self, q: usize) {
205282
self.sim.sadj(q);
283+
self.apply_noise(q);
206284
}
207285

208286
fn s(&mut self, q: usize) {
209287
self.sim.s(q);
288+
self.apply_noise(q);
210289
}
211290

212291
fn swap(&mut self, q0: usize, q1: usize) {
213292
self.sim.swap_qubit_ids(q0, q1);
293+
self.apply_noise(q0);
294+
self.apply_noise(q1);
214295
}
215296

216297
fn tadj(&mut self, q: usize) {
217298
self.sim.tadj(q);
299+
self.apply_noise(q);
218300
}
219301

220302
fn t(&mut self, q: usize) {
221303
self.sim.t(q);
304+
self.apply_noise(q);
222305
}
223306

224307
fn x(&mut self, q: usize) {
225308
self.sim.x(q);
309+
self.apply_noise(q);
226310
}
227311

228312
fn y(&mut self, q: usize) {
229313
self.sim.y(q);
314+
self.apply_noise(q);
230315
}
231316

232317
fn z(&mut self, q: usize) {
233318
self.sim.z(q);
319+
self.apply_noise(q);
234320
}
235321

236322
fn qubit_allocate(&mut self) -> usize {
323+
// Fresh qubit start in ground state even with noise.
237324
self.sim.allocate()
238325
}
239326

240-
fn qubit_release(&mut self, q: usize) {
241-
self.sim.release(q);
327+
fn qubit_release(&mut self, q: usize) -> bool {
328+
if self.is_noiseless() {
329+
let was_zero = self.sim.qubit_is_zero(q);
330+
self.sim.release(q);
331+
was_zero
332+
} else {
333+
self.sim.release(q);
334+
true
335+
}
242336
}
243337

244338
fn qubit_swap_id(&mut self, q0: usize, q1: usize) {
339+
// This is a service function rather than a gate so it doesn't incur noise.
245340
self.sim.swap_qubit_ids(q0, q1);
246341
}
247342

@@ -266,10 +361,12 @@ impl Backend for SparseSim {
266361
}
267362

268363
fn qubit_is_zero(&mut self, q: usize) -> bool {
364+
// This is a service function rather than a measurement so it doesn't incur noise.
269365
self.sim.qubit_is_zero(q)
270366
}
271367

272368
fn custom_intrinsic(&mut self, name: &str, arg: Value) -> Option<Result<Value, String>> {
369+
// These intrinsics aren't subject to noise.
273370
match name {
274371
"GlobalPhase" => {
275372
// Apply a global phase to the simulation by doing an Rz to a fresh qubit.
@@ -301,9 +398,16 @@ impl Backend for SparseSim {
301398
}
302399

303400
fn set_seed(&mut self, seed: Option<u64>) {
304-
match seed {
305-
Some(seed) => self.sim.set_rng_seed(seed),
306-
None => self.sim.set_rng_seed(rand::thread_rng().next_u64()),
401+
if let Some(seed) = seed {
402+
if !self.is_noiseless() {
403+
self.rng = Some(StdRng::seed_from_u64(seed));
404+
}
405+
self.sim.set_rng_seed(seed);
406+
} else {
407+
if !self.is_noiseless() {
408+
self.rng = Some(StdRng::from_entropy());
409+
}
410+
self.sim.set_rng_seed(rand::thread_rng().next_u64());
307411
}
308412
}
309413
}
@@ -458,9 +562,9 @@ where
458562
self.main.qubit_allocate()
459563
}
460564

461-
fn qubit_release(&mut self, q: usize) {
462-
self.chained.qubit_release(q);
463-
self.main.qubit_release(q);
565+
fn qubit_release(&mut self, q: usize) -> bool {
566+
let _ = self.chained.qubit_release(q);
567+
self.main.qubit_release(q)
464568
}
465569

466570
fn qubit_swap_id(&mut self, q0: usize, q1: usize) {

0 commit comments

Comments
 (0)