Skip to content

Commit

Permalink
Track qubit live-ness during simulation (#2020)
Browse files Browse the repository at this point in the history
This change adds some basic qubit live-ness tracking to the simulator,
achieved by changing the `Value::Qubit` variant to a weak pointer to the
qubit and having the execution environment own and track the `Rc` that
owns the qubit. This provides a few benefits:

- Fixes a panic when a qubit is passed to the simulator after it has
been released.
- Adds graceful error handling to treat use-after-release as a runtime
error.
- Adds a specific error for "double release" of a qubit if it is being
manually managed.
- Updates the display of the qubit type to show the qubit ID when it is
live and `Qubit<released>` when it is not, enabling easier debugging of
qubit lifetimes.

Of note, local perf testing showing no impact on performance (thanks,
Rust zero-cost abstractions!), though it stands to reason this change
could theoretically have some impact on programs that use many thousands
of qubits.
  • Loading branch information
swernli authored Nov 15, 2024
1 parent 57e77e9 commit 322acf6
Show file tree
Hide file tree
Showing 12 changed files with 708 additions and 157 deletions.
2 changes: 1 addition & 1 deletion compiler/qsc_circuit/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ impl Builder {
self.push_list::<'(', ')'>(vals, qubits, classical_args);
}
Value::Qubit(q) => {
qubits.push(self.map(q.0));
qubits.push(self.map(q.deref().0));
}
v => {
let _ = write!(classical_args, "{v}");
Expand Down
4 changes: 2 additions & 2 deletions compiler/qsc_eval/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ impl Backend for SparseSim {
.clone()
.unwrap_array()
.iter()
.map(|q| q.clone().unwrap_qubit().0)
.map(|q| q.clone().unwrap_qubit().deref().0)
.collect::<Vec<_>>();
let q = self.sim.allocate();
// The new qubit is by-definition in the |0⟩ state, so by reversing the sign of the
Expand Down Expand Up @@ -415,7 +415,7 @@ impl Backend for SparseSim {
}
}
"ApplyIdleNoise" => {
let q = arg.unwrap_qubit().0;
let q = arg.unwrap_qubit().deref().0;
self.apply_noise(q);
Some(Ok(Value::unit()))
}
Expand Down
157 changes: 120 additions & 37 deletions compiler/qsc_eval/src/intrinsic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::{
backend::Backend,
error::PackageSpan,
output::Receiver,
val::{self, Qubit, Value},
val::{self, Value},
Error, Rc,
};
use num_bigint::BigInt;
Expand Down Expand Up @@ -62,10 +62,14 @@ pub(crate) fn call(
}
"DumpRegister" => {
let qubits = arg.unwrap_array();
let qubits_len = qubits.len();
let qubits = qubits
.iter()
.map(|q| q.clone().unwrap_qubit().0)
.filter_map(|q| q.clone().unwrap_qubit().try_deref().map(|q| q.0))
.collect::<Vec<_>>();
if qubits.len() != qubits_len {
return Err(Error::QubitUsedAfterRelease(arg_span));
}
if qubits.len() != qubits.iter().collect::<FxHashSet<_>>().len() {
return Err(Error::QubitUniqueness(arg_span));
}
Expand All @@ -79,10 +83,14 @@ pub(crate) fn call(
}
"DumpMatrix" => {
let qubits = arg.unwrap_array();
let qubits_len = qubits.len();
let qubits = qubits
.iter()
.map(|q| q.clone().unwrap_qubit().0)
.filter_map(|q| q.clone().unwrap_qubit().try_deref().map(|q| q.0))
.collect::<Vec<_>>();
if qubits.len() != qubits_len {
return Err(Error::QubitUsedAfterRelease(arg_span));
}
if qubits.len() != qubits.iter().collect::<FxHashSet<_>>().len() {
return Err(Error::QubitUniqueness(arg_span));
}
Expand All @@ -100,7 +108,14 @@ pub(crate) fn call(
Ok(()) => Ok(Value::unit()),
Err(_) => Err(Error::OutputFail(name_span)),
},
"CheckZero" => Ok(Value::Bool(sim.qubit_is_zero(arg.unwrap_qubit().0))),
"CheckZero" => Ok(Value::Bool(
sim.qubit_is_zero(
arg.unwrap_qubit()
.try_deref()
.ok_or(Error::QubitUsedAfterRelease(arg_span))?
.0,
),
)),
"ArcCos" => Ok(Value::Double(arg.unwrap_double().acos())),
"ArcSin" => Ok(Value::Double(arg.unwrap_double().asin())),
"ArcTan" => Ok(Value::Double(arg.unwrap_double().atan())),
Expand Down Expand Up @@ -142,15 +157,6 @@ pub(crate) fn call(
}
#[allow(clippy::cast_possible_truncation)]
"Truncate" => Ok(Value::Int(arg.unwrap_double() as i64)),
"__quantum__rt__qubit_allocate" => Ok(Value::Qubit(Qubit(sim.qubit_allocate()))),
"__quantum__rt__qubit_release" => {
let qubit = arg.unwrap_qubit().0;
if sim.qubit_release(qubit) {
Ok(Value::unit())
} else {
Err(Error::ReleasedQubitNotZero(qubit, arg_span))
}
}
"__quantum__qis__ccx__body" => {
three_qubit_gate(|ctl0, ctl1, q| sim.ccx(ctl0, ctl1, q), arg, arg_span)
}
Expand All @@ -175,21 +181,43 @@ pub(crate) fn call(
"__quantum__qis__rzz__body" => {
two_qubit_rotation(|theta, q0, q1| sim.rzz(theta, q0, q1), arg, arg_span)
}
"__quantum__qis__h__body" => Ok(one_qubit_gate(|q| sim.h(q), arg)),
"__quantum__qis__s__body" => Ok(one_qubit_gate(|q| sim.s(q), arg)),
"__quantum__qis__s__adj" => Ok(one_qubit_gate(|q| sim.sadj(q), arg)),
"__quantum__qis__t__body" => Ok(one_qubit_gate(|q| sim.t(q), arg)),
"__quantum__qis__t__adj" => Ok(one_qubit_gate(|q| sim.tadj(q), arg)),
"__quantum__qis__x__body" => Ok(one_qubit_gate(|q| sim.x(q), arg)),
"__quantum__qis__y__body" => Ok(one_qubit_gate(|q| sim.y(q), arg)),
"__quantum__qis__z__body" => Ok(one_qubit_gate(|q| sim.z(q), arg)),
"__quantum__qis__h__body" => one_qubit_gate(|q| sim.h(q), arg, arg_span),
"__quantum__qis__s__body" => one_qubit_gate(|q| sim.s(q), arg, arg_span),
"__quantum__qis__s__adj" => one_qubit_gate(|q| sim.sadj(q), arg, arg_span),
"__quantum__qis__t__body" => one_qubit_gate(|q| sim.t(q), arg, arg_span),
"__quantum__qis__t__adj" => one_qubit_gate(|q| sim.tadj(q), arg, arg_span),
"__quantum__qis__x__body" => one_qubit_gate(|q| sim.x(q), arg, arg_span),
"__quantum__qis__y__body" => one_qubit_gate(|q| sim.y(q), arg, arg_span),
"__quantum__qis__z__body" => one_qubit_gate(|q| sim.z(q), arg, arg_span),
"__quantum__qis__swap__body" => two_qubit_gate(|q0, q1| sim.swap(q0, q1), arg, arg_span),
"__quantum__qis__reset__body" => Ok(one_qubit_gate(|q| sim.reset(q), arg)),
"__quantum__qis__m__body" => Ok(Value::Result(sim.m(arg.unwrap_qubit().0).into())),
"__quantum__qis__mresetz__body" => {
Ok(Value::Result(sim.mresetz(arg.unwrap_qubit().0).into()))
}
"__quantum__qis__reset__body" => one_qubit_gate(|q| sim.reset(q), arg, arg_span),
"__quantum__qis__m__body" => Ok(Value::Result(
sim.m(arg
.unwrap_qubit()
.try_deref()
.ok_or(Error::QubitUsedAfterRelease(arg_span))?
.0)
.into(),
)),
"__quantum__qis__mresetz__body" => Ok(Value::Result(
sim.mresetz(
arg.unwrap_qubit()
.try_deref()
.ok_or(Error::QubitUsedAfterRelease(arg_span))?
.0,
)
.into(),
)),
_ => {
let qubits = arg.qubits();
let qubits_len = qubits.len();
let qubits = qubits
.iter()
.filter_map(|q| q.try_deref().map(|q| q.0))
.collect::<Vec<_>>();
if qubits.len() != qubits_len {
return Err(Error::QubitUsedAfterRelease(arg_span));
}
if let Some(result) = sim.custom_intrinsic(name, arg) {
match result {
Ok(value) => Ok(value),
Expand All @@ -202,9 +230,18 @@ pub(crate) fn call(
}
}

fn one_qubit_gate(mut gate: impl FnMut(usize), arg: Value) -> Value {
gate(arg.unwrap_qubit().0);
Value::unit()
fn one_qubit_gate(
mut gate: impl FnMut(usize),
arg: Value,
arg_span: PackageSpan,
) -> Result<Value, Error> {
gate(
arg.unwrap_qubit()
.try_deref()
.ok_or(Error::QubitUsedAfterRelease(arg_span))?
.0,
);
Ok(Value::unit())
}

fn two_qubit_gate(
Expand All @@ -216,7 +253,16 @@ fn two_qubit_gate(
if x == y {
Err(Error::QubitUniqueness(arg_span))
} else {
gate(x.unwrap_qubit().0, y.unwrap_qubit().0);
gate(
x.unwrap_qubit()
.try_deref()
.ok_or(Error::QubitUsedAfterRelease(arg_span))?
.0,
y.unwrap_qubit()
.try_deref()
.ok_or(Error::QubitUsedAfterRelease(arg_span))?
.0,
);
Ok(Value::unit())
}
}
Expand All @@ -231,7 +277,13 @@ fn one_qubit_rotation(
if angle.is_nan() || angle.is_infinite() {
Err(Error::InvalidRotationAngle(angle, arg_span))
} else {
gate(angle, y.unwrap_qubit().0);
gate(
angle,
y.unwrap_qubit()
.try_deref()
.ok_or(Error::QubitUsedAfterRelease(arg_span))?
.0,
);
Ok(Value::unit())
}
}
Expand All @@ -245,7 +297,20 @@ fn three_qubit_gate(
if x == y || y == z || x == z {
Err(Error::QubitUniqueness(arg_span))
} else {
gate(x.unwrap_qubit().0, y.unwrap_qubit().0, z.unwrap_qubit().0);
gate(
x.unwrap_qubit()
.try_deref()
.ok_or(Error::QubitUsedAfterRelease(arg_span))?
.0,
y.unwrap_qubit()
.try_deref()
.ok_or(Error::QubitUsedAfterRelease(arg_span))?
.0,
z.unwrap_qubit()
.try_deref()
.ok_or(Error::QubitUsedAfterRelease(arg_span))?
.0,
);
Ok(Value::unit())
}
}
Expand All @@ -262,7 +327,17 @@ fn two_qubit_rotation(
} else if angle.is_nan() || angle.is_infinite() {
Err(Error::InvalidRotationAngle(angle, arg_span))
} else {
gate(angle, y.unwrap_qubit().0, z.unwrap_qubit().0);
gate(
angle,
y.unwrap_qubit()
.try_deref()
.ok_or(Error::QubitUsedAfterRelease(arg_span))?
.0,
z.unwrap_qubit()
.try_deref()
.ok_or(Error::QubitUsedAfterRelease(arg_span))?
.0,
);
Ok(Value::unit())
}
}
Expand All @@ -276,16 +351,24 @@ pub fn qubit_relabel(
mut swap: impl FnMut(usize, usize),
) -> Result<Value, Error> {
let [left, right] = unwrap_tuple(arg);
let left = left.unwrap_array();
let left_len = left.len();
let left = left
.unwrap_array()
.iter()
.map(|q| q.clone().unwrap_qubit().0)
.filter_map(|q| q.clone().unwrap_qubit().try_deref().map(|q| q.0))
.collect::<Vec<_>>();
if left.len() != left_len {
return Err(Error::QubitUsedAfterRelease(arg_span));
}
let right = right.unwrap_array();
let right_len = right.len();
let right = right
.unwrap_array()
.iter()
.map(|q| q.clone().unwrap_qubit().0)
.filter_map(|q| q.clone().unwrap_qubit().try_deref().map(|q| q.0))
.collect::<Vec<_>>();
if right.len() != right_len {
return Err(Error::QubitUsedAfterRelease(arg_span));
}
let left_set = left.iter().collect::<FxHashSet<_>>();
let right_set = right.iter().collect::<FxHashSet<_>>();
if left.len() != left_set.len() || right.len() != right_set.len() {
Expand Down
Loading

0 comments on commit 322acf6

Please sign in to comment.