Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion crates/ppvm-stim/benches/stim-circuits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ fn required_qubits(program: &ExtendedProgram) -> usize {
| ExtendedInstruction::TDag { targets, .. }
| ExtendedInstruction::Rotation { targets, .. }
| ExtendedInstruction::U3 { targets, .. }
| ExtendedInstruction::Loss { targets, .. } => {
| ExtendedInstruction::Loss { targets, .. }
| ExtendedInstruction::Leakage { targets, .. } => {
for &q in targets {
bump(q);
}
Expand Down
7 changes: 7 additions & 0 deletions crates/ppvm-stim/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,13 @@ pub fn execute_validated<T, I, C>(
tab.correlated_loss_channel(a, b, ps.clone());
}
}
ExtendedInstruction::Leakage {
p0, p1, targets, ..
} => {
for &q in targets {
tab.leakage_channel(q, (*p0).into(), (*p1).into());
}
}
ExtendedInstruction::Measure(MeasureOp {
name,
args,
Expand Down
3 changes: 2 additions & 1 deletion crates/ppvm-stim/src/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ fn validate_slice(
| ExtendedInstruction::Rotation { .. }
| ExtendedInstruction::U3 { .. }
| ExtendedInstruction::Loss { .. }
| ExtendedInstruction::CorrelatedLoss { .. } => {}
| ExtendedInstruction::CorrelatedLoss { .. }
| ExtendedInstruction::Leakage { .. } => {}
ExtendedInstruction::MPad {
prob, bits, span, ..
} => {
Expand Down
28 changes: 28 additions & 0 deletions crates/ppvm-stim/tests/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,34 @@ fn loss_channel_with_p1_marks_qubit_lost() {
assert!(tab.is_lost[0]);
}

#[test]
fn leakage_channel_leaks_qubit_to_one() {
// p1 = 1.0 pins the qubit to |1⟩ and flags it leaked (not lost).
let prog = parse_extended("I_ERROR[leakage](0.0, 1.0) 0").unwrap();
let mut tab: Tab = GeneralizedTableau::new(1, 1e-10);
execute(&prog, &mut tab).unwrap();
assert!(tab.is_leaked[0]);
assert!(!tab.is_lost[0]);
assert_eq!(tab.measure(0), Some(true));
}

#[test]
fn leakage_channel_zero_prob_no_leak() {
let prog = parse_extended("I_ERROR[leakage](0.0, 0.0) 0").unwrap();
let mut tab: Tab = GeneralizedTableau::new(1, 1e-10);
execute(&prog, &mut tab).unwrap();
assert!(!tab.is_leaked[0]);
assert!(!tab.is_lost[0]);
}

#[test]
fn leakage_channel_applies_to_all_targets() {
let prog = parse_extended("I_ERROR[leakage](0.0, 1.0) 0 1 2").unwrap();
let mut tab: Tab = GeneralizedTableau::new(3, 1e-10);
execute(&prog, &mut tab).unwrap();
assert!(tab.is_leaked[0] && tab.is_leaked[1] && tab.is_leaked[2]);
}

#[test]
fn repeat_executes_body_n_times() {
let (results, _) = run("REPEAT 2 { X 0 }\nM 0", 1);
Expand Down
51 changes: 40 additions & 11 deletions crates/ppvm-tableau/src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -641,13 +641,26 @@ pub struct GeneralizedTableau<
pub coefficients: SparseVectorType,
/// Per-qubit loss flags.
pub is_lost: Vec<bool>,
/// Per-qubit leakage flags. A leaked qubit has been pinned to a
/// computational basis state (`|0⟩`/`|1⟩`) in the tableau, so gates skip it
/// and measurement reports the pinned value directly.
pub is_leaked: Vec<bool>,
/// Coefficient-magnitude threshold below which branches are dropped.
pub coefficient_threshold: T::Coeff,
/// Ordered log of every measurement performed (mirrors stim's record).
pub measurement_record: Vec<Option<bool>>,
_index_phantom: PhantomData<IndexType>,
}

impl<T: Config, I, C: SparseVector<Complex<T::Coeff>, I>> GeneralizedTableau<T, I, C> {
/// Whether qubit `addr0` is outside the computational subspace — either lost
/// or leaked. Gates skip such qubits.
#[inline]
pub fn is_lost_or_leaked(&self, addr0: usize) -> bool {
self.is_leaked[addr0] || self.is_lost[addr0]
}
}

impl<T: Config, I, C: SparseVector<Complex<T::Coeff>, I>> GeneralizedTableau<T, I, C>
where
T::Coeff: One + Zero + Clone + num::Num,
Expand All @@ -673,6 +686,7 @@ where
tableau: Tableau::new(n_qubits),
coefficients,
is_lost: vec![false; n_qubits],
is_leaked: vec![false; n_qubits],
coefficient_threshold,
measurement_record: Vec::new(),
_index_phantom: PhantomData,
Expand All @@ -697,7 +711,10 @@ where
coefficients.unsafe_insert(I::zero(), complex_one);
self.coefficients = coefficients;
for l in self.is_lost.iter_mut() {
*l &= false;
*l = false;
}
for l in self.is_leaked.iter_mut() {
*l = false;
}
self.measurement_record.clear();
}
Expand Down Expand Up @@ -743,31 +760,31 @@ where
}

/// Apply CZ to N pairs with constant offset: (base+i, base+offset+i) for i in 0..count.
/// Falls back to individual CZ calls if any qubit in the range is lost.
/// Falls back to individual CZ calls if any qubit in the range is lost or leaked.
pub fn cz_block_pairs(&mut self, base: usize, offset: usize, count: usize)
where
<<T::Storage as BitView>::Store as TryFrom<usize>>::Error: Debug,
<T::Storage as BitView>::Store: PrimInt + TryFrom<usize>,
{
// Check if any qubit in the range is lost
let any_lost =
(0..count).any(|i| self.is_lost[base + i] || self.is_lost[base + offset + i]);
// Check if any qubit in the range is lost or leaked
let any_lost = (0..count)
.any(|i| self.is_lost_or_leaked(base + i) || self.is_lost_or_leaked(base + offset + i));
if !any_lost {
self.tableau.cz_block_pairs(base, offset, count);
} else {
// Fallback to individual CZ calls
for i in 0..count {
let c = base + i;
let t = base + offset + i;
if !self.is_lost[c] && !self.is_lost[t] {
if !self.is_lost_or_leaked(c) && !self.is_lost_or_leaked(t) {
Clifford::cz(&mut self.tableau, c, t);
}
}
}
}

/// Apply CZ to N cross-word pairs. Controls at word_c, targets at word_t.
/// Falls back to individual CZ calls if any qubit is lost.
/// Falls back to individual CZ calls if any qubit is lost or leaked.
pub fn cz_block_pairs_cross_word(
&mut self,
word_c: usize,
Expand All @@ -782,7 +799,7 @@ where
let any_lost = (0..count).any(|i| {
let c = word_c * bits_per_word + base_bit_c + i;
let t = word_t * bits_per_word + base_bit_t + i;
self.is_lost[c] || self.is_lost[t]
self.is_lost_or_leaked(c) || self.is_lost_or_leaked(t)
});
if !any_lost {
self.tableau
Expand All @@ -791,7 +808,7 @@ where
for i in 0..count {
let c = word_c * bits_per_word + base_bit_c + i;
let t = word_t * bits_per_word + base_bit_t + i;
if !self.is_lost[c] && !self.is_lost[t] {
if !self.is_lost_or_leaked(c) && !self.is_lost_or_leaked(t) {
Clifford::cz(&mut self.tableau, c, t);
}
}
Expand Down Expand Up @@ -1012,7 +1029,7 @@ where
) where
<<T as Config>::Storage as BitView>::Store: PrimInt,
{
if self.is_lost[addr0] {
if self.is_lost_or_leaked(addr0) {
return;
}

Expand Down Expand Up @@ -1215,7 +1232,7 @@ where
) where
<<T as Config>::Storage as BitView>::Store: PrimInt,
{
if self.is_lost[addr0] {
if self.is_lost_or_leaked(addr0) {
return;
}

Expand Down Expand Up @@ -1607,6 +1624,18 @@ mod tests {
assert!(tab.is_lost.iter().all(|&lost| !lost));
}

/// A full reset clears per-qubit leakage flags.
#[test]
fn reset_all_clears_leak_flags() {
let mut tab: TestTableau = GeneralizedTableau::new(3, 1e-12);
tab.is_leaked[0] = true;
tab.is_leaked[2] = true;

tab.reset_all();

assert!(tab.is_leaked.iter().all(|&leaked| !leaked));
}

/// `Tableau::reset_all` restores the fresh identity tableau rows.
#[test]
fn tableau_reset_all_restores_fresh_rows() {
Expand Down
22 changes: 11 additions & 11 deletions crates/ppvm-tableau/src/gates/clifford.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,24 @@ use smallvec::{SmallVec, smallvec};
/// Stack-allocates for up to 8 storage words; spills to heap beyond.
type MaskBuf<T> = SmallVec<[<<T as Config>::Storage as BitView>::Store; 8]>;

// Single-qubit gate on a `GeneralizedTableau`: skip lost qubits, delegate to
// the inner tableau's canonical (word-level) method.
// Single-qubit gate on a `GeneralizedTableau`: skip lost/leaked qubits, delegate
// to the inner tableau's canonical (word-level) method.
macro_rules! impl_generalized_tableau_clifford {
($name:ident) => {
fn $name(&mut self, index: usize) {
if self.is_lost[index] {
if self.is_lost_or_leaked(index) {
return;
}
self.tableau.$name(index);
}
};
}

// Two-qubit gate on a `GeneralizedTableau`: skip pairs with a lost qubit.
// Two-qubit gate on a `GeneralizedTableau`: skip pairs with a lost/leaked qubit.
macro_rules! impl_generalized_tableau_clifford_pair {
($name:ident) => {
fn $name(&mut self, control: usize, target: usize) {
if self.is_lost[control] || self.is_lost[target] {
if self.is_lost_or_leaked(control) || self.is_lost_or_leaked(target) {
return;
}
self.tableau.$name(control, target);
Expand Down Expand Up @@ -752,18 +752,18 @@ where
Complex<<T as Config>::Coeff>: From<Complex<f64>>,
<T::Storage as BitView>::Store: PrimInt,
{
/// Fast path: check if any qubit in the slice is lost
/// Fast path: check if any qubit in the slice is lost or leaked
#[inline]
fn any_lost_single(&self, indices: &[usize]) -> bool {
indices.iter().any(|&i| self.is_lost[i])
indices.iter().any(|&i| self.is_lost_or_leaked(i))
}

/// Fast path: check if any qubit pair has a lost qubit
/// Fast path: check if any qubit pair has a lost or leaked qubit
#[inline]
fn any_lost_pair(&self, pairs: &[(usize, usize)]) -> bool {
pairs
.iter()
.any(|&(c, t)| self.is_lost[c] || self.is_lost[t])
.any(|&(c, t)| self.is_lost_or_leaked(c) || self.is_lost_or_leaked(t))
}
}

Expand All @@ -777,7 +777,7 @@ macro_rules! impl_gen_tableau_batch_single {
let filtered: Vec<usize> = indices
.iter()
.copied()
.filter(|&i| !self.is_lost[i])
.filter(|&i| !self.is_lost_or_leaked(i))
.collect();
self.tableau.$name(&filtered);
}
Expand All @@ -794,7 +794,7 @@ macro_rules! impl_gen_tableau_batch_pair {
let filtered: Vec<(usize, usize)> = pairs
.iter()
.copied()
.filter(|&(c, t)| !self.is_lost[c] && !self.is_lost[t])
.filter(|&(c, t)| !self.is_lost_or_leaked(c) && !self.is_lost_or_leaked(t))
.collect();
self.tableau.$name(&filtered);
}
Expand Down
13 changes: 9 additions & 4 deletions crates/ppvm-tableau/src/gates/reset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ impl<T, I, C> Reset for GeneralizedTableau<T, I, C>
where
T: Config,
<<T as Config>::Storage as BitView>::Store: PrimInt,
I: TableauIndex + Debug + Send + Sync,
I: TableauIndex + Debug,
C: SparseVector<Complex<T::Coeff>, I> + Debug,
T::Coeff: One
+ Zero
Expand All @@ -33,9 +33,7 @@ where
+ ToPrimitive
+ std::fmt::Debug
+ std::ops::Mul<f64>
+ PartialOrd<f64>
+ Send
+ Sync,
+ PartialOrd<f64>,
Complex<T::Coeff>: std::ops::Mul<Output = Complex<T::Coeff>>
+ From<Complex64>
+ std::ops::MulAssign
Expand All @@ -45,6 +43,13 @@ where
+ Copy,
{
fn reset(&mut self, addr0: usize) {
// Skip qubits outside the computational subspace. Currently a no-op for
// loss (the `x` below is already skipped and `measure` returns `None`),
// but leaked qubits must not be re-zeroed, and this short-cuts both.
if self.is_lost_or_leaked(addr0) {
return;
}

let m = self.measure(addr0);

// A reset is not a measurement in stim's model: drop the record
Expand Down
2 changes: 1 addition & 1 deletion crates/ppvm-tableau/src/gates/rot1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ where
+ Copy,
{
fn rotate_1(&mut self, axis: Pauli, addr0: usize, theta: <T as Config>::Coeff) {
if self.is_lost[addr0] {
if self.is_lost_or_leaked(addr0) {
return;
}
let (sin, cos) = (theta * 0.5.into()).sin_cos();
Expand Down
6 changes: 3 additions & 3 deletions crates/ppvm-tableau/src/gates/rot2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ where
let [axis_b_x, axis_b_z] = axis_b;
let pauli_a = PAULIS[(axis_a_z << 1 | axis_a_x) as usize];
let pauli_b = PAULIS[(axis_b_z << 1 | axis_b_x) as usize];
// NOTE: if both qubits are lost, the rot1 will be a no-op
if self.is_lost[a] {
// NOTE: if both qubits are lost/leaked, the rot1 will be a no-op
if self.is_lost_or_leaked(a) {
return self.rotate_1(pauli_b, b, theta);
} else if self.is_lost[b] {
} else if self.is_lost_or_leaked(b) {
return self.rotate_1(pauli_a, a, theta);
}

Expand Down
4 changes: 2 additions & 2 deletions crates/ppvm-tableau/src/gates/tgate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ where
<I as BitAnd<<I as Shl<usize>>::Output>>::Output: PartialEq<I>,
{
fn t(&mut self, index: usize) {
if self.is_lost[index] {
if self.is_lost_or_leaked(index) {
return;
}

Expand All @@ -42,7 +42,7 @@ where
}

fn t_dag(&mut self, index: usize) {
if self.is_lost[index] {
if self.is_lost_or_leaked(index) {
return;
}

Expand Down
Loading
Loading