Skip to content
Open
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
4 changes: 4 additions & 0 deletions crates/ppvm-python-native/ppvm_python_native.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,13 @@ class _GeneralizedTableauBase:
self, addr0: int, addr1: int, p: Sequence[float]
) -> None: ...
def reset_loss_channel(self, addr0: int) -> None: ...
def leakage_channel(self, addr0: int, p0: float, p1: float) -> None: ...
def reset_leakage_channel(self, addr0: int) -> None: ...
def reset(self, addr0: int) -> None: ...
def is_lost(self, addr0: int) -> bool: ...
def is_leaked(self, addr0: int) -> bool: ...
def loss_values(self) -> list[bool]: ...
def leakage_values(self) -> list[bool]: ...
def run(self, prog: "StimProgram") -> list[int]: ...
@staticmethod
def sample(
Expand Down
28 changes: 26 additions & 2 deletions crates/ppvm-python-native/src/interface_tableau.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,14 @@ macro_rules! create_interface {
self.inner.asymmetric_loss_channel(addr0, p0, p1);
}

pub fn leakage_channel(&mut self, addr0: usize, p0: f64, p1: f64) {
self.inner.leakage_channel(addr0, p0, p1);
}

pub fn reset_leakage_channel(&mut self, addr0: usize) {
self.inner.reset_leakage_channel(addr0);
}

pub fn reset(&mut self, targets: Vec<usize>) {
self.inner.reset_many(targets.as_slice());
}
Expand All @@ -295,11 +303,27 @@ macro_rules! create_interface {
}

pub fn is_lost(&self, addr0: usize) -> bool {
self.inner.is_lost[addr0]
self.inner.is_lost(addr0)
}

pub fn is_leaked(&self, addr0: usize) -> bool {
self.inner.is_leaked(addr0)
}

pub fn loss_values(&self) -> Vec<bool> {
self.inner.is_lost.clone()
self.inner
.qubit_status
.iter()
.map(|&o| o == QubitStatus::Lost)
.collect()
}

pub fn leakage_values(&self) -> Vec<bool> {
self.inner
.qubit_status
.iter()
.map(|&o| o == QubitStatus::Leaked)
.collect()
}

pub fn run(
Expand Down
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 @@ -738,6 +738,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
34 changes: 31 additions & 3 deletions crates/ppvm-stim/tests/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,35 @@ fn loss_channel_with_p1_marks_qubit_lost() {
let prog = parse_extended("I_ERROR[loss](1.0) 0").unwrap();
let mut tab: Tab = GeneralizedTableau::new(1, 1e-10);
execute(&prog, &mut tab).unwrap();
assert!(tab.is_lost[0]);
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]
Expand Down Expand Up @@ -304,7 +332,7 @@ fn test_stim_correlated_loss_simple() {
let mut tab: Tab = GeneralizedTableau::new(2, 1e-10);
run_str("I_ERROR[correlated_loss](1.0) 0 1", &mut tab);
assert!(
tab.is_lost[0] && tab.is_lost[1],
tab.is_lost(0) && tab.is_lost(1),
"Both qubits should be lost"
);
}
Expand All @@ -315,7 +343,7 @@ fn test_stim_correlated_loss_zero_prob() {
let mut tab: Tab = GeneralizedTableau::new(2, 1e-10);
run_str("I_ERROR[correlated_loss](0.0) 0 1", &mut tab);
assert!(
!tab.is_lost[0] && !tab.is_lost[1],
!tab.is_lost(0) && !tab.is_lost(1),
"No qubits should be lost"
);
}
Expand Down
2 changes: 1 addition & 1 deletion crates/ppvm-tableau-sum/benches/parallelization_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ fn branch_entry(
addr0: usize,
) -> SmallVec<[(Tableau, f64, u64, u64); 3]> {
let mut out: SmallVec<[(Tableau, f64, u64, u64); 3]> = SmallVec::new();
if tab.is_lost[addr0] {
if tab.is_lost(addr0) {
return out;
}
let word_fp = word_fingerprint(tab);
Expand Down
25 changes: 16 additions & 9 deletions crates/ppvm-tableau-sum/src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ where
mod tests {
use super::*;
use ppvm_pauli_sum::config::fxhash::ByteF64;
use ppvm_tableau::data::QubitStatus;
use ppvm_traits::traits::{
Clifford, Depolarizing, LossChannel, LossyMeasure, PauliError, Reset, ResetLossChannel,
TGate,
Expand Down Expand Up @@ -192,7 +193,13 @@ mod tests {
assert_eq!(tab.len(), 1);
assert_eq!(tab.n_qubits, 3);
assert!((tab.entries.entries[0].1 - 1.0).abs() < 1e-12);
assert!(tab.entries.entries[0].0.is_lost.iter().all(|x| !x));
assert!(
tab.entries.entries[0]
.0
.qubit_status
.iter()
.all(|&occ| occ == QubitStatus::Live)
);
}

#[test]
Expand Down Expand Up @@ -257,7 +264,7 @@ mod tests {
tab.loss_channel(0, 0.0);
assert_eq!(tab.len(), 1);
assert!((tab.entries.entries[0].1 - 1.0).abs() < 1e-12);
assert!(!tab.entries.entries[0].0.is_lost[0]);
assert!(!tab.entries.entries[0].0.is_lost(0));
}

#[test]
Expand All @@ -266,7 +273,7 @@ mod tests {
tab.loss_channel(0, 0.5);
assert_eq!(tab.len(), 2);
assert!((sum_of_probabilities(&tab) - 1.0).abs() < 1e-12);
let lost_count = tab.entries.iter().filter(|e| e.0.is_lost[0]).count();
let lost_count = tab.entries.iter().filter(|e| e.0.is_lost(0)).count();
assert_eq!(lost_count, 1);
for entry in tab.entries.iter() {
assert!((*entry.1 - 0.5).abs() < 1e-12);
Expand Down Expand Up @@ -342,7 +349,7 @@ mod tests {

assert_eq!(tab.len(), 1);
let (entry, p) = tab.entries.iter().next().unwrap();
assert!(!entry.is_lost[0]);
assert!(!entry.is_lost(0));
assert!((*p - 1.0).abs() < 1e-12);
}

Expand All @@ -357,7 +364,7 @@ mod tests {
assert_eq!(tab.len(), 1);
assert_eq!(tab.entries.buckets.len(), 1);
let (entry, p) = tab.entries.iter().next().unwrap();
assert!(!entry.is_lost[0]);
assert!(!entry.is_lost(0));
assert!((*p - 1.0).abs() < 1e-12);
}

Expand Down Expand Up @@ -396,7 +403,7 @@ mod tests {

assert_eq!(tab.len(), 1);
assert!((sum_of_probabilities(&tab) - 1.0).abs() < 1e-12);
assert!(tab.entries.entries[0].0.is_lost[0]);
assert!(tab.entries.entries[0].0.is_lost(0));
}

#[test]
Expand Down Expand Up @@ -479,8 +486,8 @@ mod tests {
tab.loss_channel(0, 1.0);
assert_eq!(tab.len(), 1);
assert!((tab.entries.entries[0].1 - 1.0).abs() < 1e-12);
assert!(tab.entries.entries[0].0.is_lost[0]);
assert!(!tab.entries.entries[0].0.is_lost[1]);
assert!(tab.entries.entries[0].0.is_lost(0));
assert!(!tab.entries.entries[0].0.is_lost(1));
}

#[test]
Expand All @@ -493,7 +500,7 @@ mod tests {
tab.loss_channel(0, 0.5);
assert_eq!(tab.len(), 1);
assert!((tab.entries.entries[0].1 - 1.0).abs() < 1e-12);
assert!(tab.entries.entries[0].0.is_lost[0]);
assert!(tab.entries.entries[0].0.is_lost(0));
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion crates/ppvm-tableau-sum/src/measure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ where

self.entries
.for_each_mut_with_keys(|tab, p_sum, word_fp, phase_loss_fp| {
if tab.is_lost[addr0] {
if tab.is_lost(addr0) {
// NOTE: deterministically outputs lost, no branching
let _ = per_branch(tab, None, &*p_sum);
return;
Expand Down
36 changes: 19 additions & 17 deletions crates/ppvm-tableau-sum/src/noise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ use num::{
};
use ppvm_pauli_word::pattern::NotIdentity;
use ppvm_tableau::{
data::GeneralizedTableau, noise::is_admissible_correlated_loss, sparsevec::SparseVector,
data::{GeneralizedTableau, QubitStatus},
noise::is_admissible_correlated_loss,
sparsevec::SparseVector,
tableau_index::TableauIndex,
};
use ppvm_traits::config::Config;
Expand Down Expand Up @@ -61,14 +63,14 @@ fn single_qubit_loss_branch<T, I, C>(
I: TableauIndex + Send + Sync + Debug,
C: SparseVector<Complex<T::Coeff>, I>,
{
if tab.is_lost[addr0] {
if tab.is_lost(addr0) {
// Don't branch if it's already lost
return;
}

let tab_seed = rng.random::<u64>();
let mut tab_branch = tab.fork(Some(tab_seed));
tab_branch.is_lost[addr0] = true;
tab_branch.qubit_status[addr0] = QubitStatus::Lost;
// is_lost flip leaves the Pauli words and phases unchanged, so
// the branch reuses its parent's word-fingerprint and the only
// change to the phase/loss hash is the lost qubit's mask.
Expand Down Expand Up @@ -122,7 +124,7 @@ where
// parent_idx aligns with for_each_mut_with_keys' order.
let parent_idx = idx;
idx += 1;
if tab.is_lost[addr0] {
if tab.is_lost(addr0) {
return;
}
branches.push((
Expand Down Expand Up @@ -229,7 +231,7 @@ where
.for_each_mut_with_keys(|tab, p_sum, word_fp, phase_loss| {
let parent_idx = idx;
idx += 1;
if tab.is_lost[addr0] {
if tab.is_lost(addr0) {
return;
}

Expand Down Expand Up @@ -355,7 +357,7 @@ where

self.entries
.for_each_mut_with_keys(|tab, p_sum, word_fp, phase_loss| {
if tab.is_lost[addr0] || tab.is_lost[addr1] {
if tab.is_lost(addr0) || tab.is_lost(addr1) {
return;
}

Expand Down Expand Up @@ -461,7 +463,7 @@ where
self.entries
.for_each_mut_with_keys(|tab, p_sum, word_fp, phase_loss| {
// if either is lost already, we just lose the other with probability p[2]
if tab.is_lost[addr0] {
if tab.is_lost(addr0) {
single_qubit_loss_branch(
addr1,
&p[2],
Expand All @@ -472,7 +474,7 @@ where
(word_fp, phase_loss),
);
return;
} else if tab.is_lost[addr1] {
} else if tab.is_lost(addr1) {
single_qubit_loss_branch(
addr0,
&p[2],
Expand All @@ -494,8 +496,8 @@ where

let tab_seed_both = self.rng.random::<u64>();
let mut tab_lose_both = tab.fork(Some(tab_seed_both));
tab_lose_both.is_lost[addr0] = true;
tab_lose_both.is_lost[addr1] = true;
tab_lose_both.qubit_status[addr0] = QubitStatus::Lost;
tab_lose_both.qubit_status[addr1] = QubitStatus::Lost;

// is_lost flip leaves the Pauli words and phases unchanged, so
// the branch reuses its parent's word-fingerprint and the only
Expand All @@ -509,7 +511,7 @@ where

let tab_seed_0 = self.rng.random::<u64>();
let mut tab_lose_0 = tab.fork(Some(tab_seed_0));
tab_lose_0.is_lost[addr0] = true;
tab_lose_0.qubit_status[addr0] = QubitStatus::Lost;
branches.push((
tab_lose_0,
p_sum.clone() * p[1].clone(),
Expand All @@ -519,7 +521,7 @@ where

let tab_seed_1 = self.rng.random::<u64>();
let mut tab_lose_1 = tab.fork(Some(tab_seed_1));
tab_lose_1.is_lost[addr1] = true;
tab_lose_1.qubit_status[addr1] = QubitStatus::Lost;
branches.push((
tab_lose_1,
p_sum.clone() * p[1].clone(),
Expand Down Expand Up @@ -569,9 +571,9 @@ where
{
fn reset_loss_channel(&mut self, addr0: usize) {
let delta = loss_mask(addr0);
let mut branches = self.entries.drain_where(|tab| tab.is_lost[addr0]);
let mut branches = self.entries.drain_where(|tab| tab.is_lost(addr0));
for (tab, _, _, phase_loss) in branches.iter_mut() {
tab.is_lost[addr0] = false;
tab.qubit_status[addr0] = QubitStatus::Live;
*phase_loss ^= delta;
}
// reset_loss preserves total probability mass and never drops entries
Expand Down Expand Up @@ -624,7 +626,7 @@ mod tests {
sum.correlated_loss_channel(0, 1, p);
sum.entries
.iter()
.filter(|(tab, _)| tab.is_lost[0] ^ tab.is_lost[1])
.filter(|(tab, _)| tab.is_lost(0) ^ tab.is_lost(1))
.map(|(_, probability)| *probability)
.sum()
}
Expand All @@ -635,7 +637,7 @@ mod tests {
sum.correlated_loss_channel(0, 1, p);
sum.entries
.iter()
.filter(|(tab, _)| !tab.is_lost[0] && !tab.is_lost[1])
.filter(|(tab, _)| !tab.is_lost(0) && !tab.is_lost(1))
.map(|(_, probability)| *probability)
.sum()
}
Expand All @@ -658,7 +660,7 @@ mod tests {
for seed in 0..trials {
let mut tab: Tab = GeneralizedTableau::new_with_seed(2, 1e-12, seed);
tab.correlated_loss_channel(0, 1, p);
if tab.is_lost[0] ^ tab.is_lost[1] {
if tab.is_lost(0) ^ tab.is_lost(1) {
hits += 1;
}
}
Expand Down
Loading
Loading