Skip to content
Merged
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
72 changes: 57 additions & 15 deletions crates/ppvm-pauli-sum/src/sum/reset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ use ppvm_traits::char::Pauli;
use ppvm_traits::config::Config;
use ppvm_traits::traits::*;

/// Reset to `|0⟩` in the Heisenberg picture.
/// Reset a live qubit to `|0⟩` in the Heisenberg picture.
///
/// The adjoint of the reset channel maps `A_q ⊗ B` to `⟨0|A|0⟩ · I_q ⊗ B`:
/// `I` and `Z` on the reset qubit become `I`, while `X` and `Y` drop out.
/// Terms that collide after relabelling have their coefficients added.
/// Lost qubits are left untouched, matching the tableau, which skips
/// inactive qubits on reset; use `ResetLossChannel` to model reloading.
impl<T: Config> Reset for PauliSum<T> {
fn reset(&mut self, addr0: usize) {
/// inactive qubits on state reset.
impl<T: Config> ResetState for PauliSum<T> {
fn reset_state(&mut self, addr0: usize) {
self.data_mut()
.retain(|k, _| k.get_lbit(addr0) || !k.get_xbit(addr0));
self.map_add(|k, v| {
Expand All @@ -25,6 +25,23 @@ impl<T: Config> Reset for PauliSum<T> {
}
});
}
}

/// Full reset to `|0⟩` in the Heisenberg picture, term by term via
/// [`WordReset`]. For lossy words this also reloads lost qubits.
impl<T: Config> Reset for PauliSum<T>
where
T::PauliWordType: WordReset,
{
fn reset(&mut self, addr0: usize) {
let (data, aux) = self.data_aux_mut();
aux.clear();
data.map_flat_add_assign(aux, |k, v| {
let v = v.clone();
k.word_reset(addr0).map(move |w| (w, v.clone()))
});
self.swap();
}

// The trait defaults run reset first (Schrödinger order); the Heisenberg
// picture needs the basis change applied before the reset instead.
Expand All @@ -45,6 +62,7 @@ mod tests {
use crate::config::fxhash::ByteF64;

use super::*;
use ppvm_pauli_word::loss::LossyPauliWord;

type Sum = PauliSum<ByteF64<2>>;

Expand Down Expand Up @@ -86,21 +104,45 @@ mod tests {
assert_eq!(s, sum(2, &[("XI", 1.0)]));
}

type Lossy = PauliSum<
crate::config::fxhash::Byte<
1,
f64,
ppvm_traits::traits::NoStrategy,
LossyPauliWord<[u8; 1], fxhash::FxBuildHasher>,
>,
>;

fn lossy(n_qubits: usize, terms: &[(&str, f64)]) -> Lossy {
let mut s: Lossy = PauliSum::builder().n_qubits(n_qubits).build();
for &(word, coef) in terms {
s += (word, coef);
}
s
}

#[test]
fn test_reset_gives_zero_state_even_from_loss() {
// ⟨Z⟩ = 1 after reset on any state: Z maps to the full identity I + L.
let mut o = lossy(1, &[("Z", 1.0)]);
o.reset(0);
assert_eq!(o, lossy(1, &[("I", 1.0), ("L", 1.0)]));
}

#[test]
fn test_reset_leaves_lost_qubit_untouched() {
use ppvm_pauli_word::loss::LossyPauliWord;
type Lossy = PauliSum<
crate::config::fxhash::Byte<
1,
f64,
ppvm_traits::traits::NoStrategy,
LossyPauliWord<[u8; 1], fxhash::FxBuildHasher>,
>,
>;
fn test_reset_leaves_no_loss_probability() {
// The lost-projector expectation vanishes after a full reset.
let mut o = lossy(2, &[("LZ", 1.0)]);
o.reset(0);
assert!(o.data().iter().all(|(_, v)| *v == 0.0));
}

#[test]
fn test_reset_state_leaves_lost_qubit_untouched() {
let mut s: Lossy = PauliSum::builder().n_qubits(2).build();
s += ("LZ", 1.0);
s += ("ZZ", 0.5);
s.reset(0);
s.reset_state(0);
let mut expect: Lossy = PauliSum::builder().n_qubits(2).build();
expect += ("LZ", 1.0);
expect += ("IZ", 0.5);
Expand Down
14 changes: 13 additions & 1 deletion crates/ppvm-pauli-word/src/loss/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

use bitvec::prelude::BitArray;
use ppvm_traits::char::Pauli;
use ppvm_traits::traits::{HashFinalize, PauliIter, PauliStorage, PauliWordTrait};
use ppvm_traits::traits::{HashFinalize, PauliIter, PauliStorage, PauliWordTrait, WordReset};
use std::hash::{BuildHasher, Hash};
use std::ops::Index;

Expand Down Expand Up @@ -418,6 +418,18 @@ impl<A: PauliStorage, S: BuildHasher + Clone + Default + HashFinalize> std::fmt:
}
}

impl<A: PauliStorage, S: BuildHasher + Clone + Default + HashFinalize> WordReset
for LossyPauliWord<A, S>
{
// The identity on the full qubit space is `I + L`.
fn word_reset(&self, addr0: usize) -> impl Iterator<Item = Self> + use<A, S> {
matches!(self.get(addr0), Pauli::I | Pauli::Z)
.then(|| [self.set_new(addr0, Pauli::I), self.set_new(addr0, Pauli::L)])
.into_iter()
.flatten()
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
13 changes: 12 additions & 1 deletion crates/ppvm-pauli-word/src/word/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
use bitvec::array::BitArray;
use core::panic;
use ppvm_traits::char::Pauli;
use ppvm_traits::traits::{HashFinalize, PauliIter, PauliStorage, PauliWordTrait};
use ppvm_traits::traits::{HashFinalize, PauliIter, PauliStorage, PauliWordTrait, WordReset};
use std::hash::{BuildHasher, Hash};
use std::ops::Index;

Expand Down Expand Up @@ -389,6 +389,17 @@ impl<A: PauliStorage, S: BuildHasher + Clone + Default + HashFinalize, const REH
}
}

impl<A: PauliStorage, S: BuildHasher + Clone + Default + HashFinalize, const REHASH: bool> WordReset
for PauliWord<A, S, REHASH>
{
// No loss representable: a full reset is a state reset.
fn word_reset(&self, addr0: usize) -> impl Iterator<Item = Self> + use<A, S, REHASH> {
matches!(self.get(addr0), Pauli::I | Pauli::Z)
.then(|| self.set_new(addr0, Pauli::I))
.into_iter()
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
4 changes: 4 additions & 0 deletions crates/ppvm-python-native/src/interface_tableau.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,10 @@ macro_rules! create_interface {
self.inner.reset_many(targets.as_slice());
}

pub fn reset_state(&mut self, targets: Vec<usize>) {
self.inner.reset_state_many(targets.as_slice());
}

pub fn reset_x(&mut self, targets: Vec<usize>) {
self.inner.reset_x_many(targets.as_slice());
}
Expand Down
4 changes: 4 additions & 0 deletions crates/ppvm-python-native/src/interface_tableau_sum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ macro_rules! create_sum_interface {
self.inner.reset_many(targets.as_slice());
}

pub fn reset_state(&mut self, targets: Vec<usize>) {
self.inner.reset_state_many(targets.as_slice());
}

/// Force a truncation pass.
///
/// Truncation is already applied automatically inside every noise
Expand Down
1 change: 1 addition & 0 deletions crates/ppvm-stim/benches/stim-circuits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ fn required_qubits(program: &ExtendedProgram) -> usize {
| ExtendedInstruction::Loss { targets, .. }
| ExtendedInstruction::Leakage { targets, .. }
| ExtendedInstruction::ResetLoss { targets, .. }
| ExtendedInstruction::ResetState { targets, .. }
| ExtendedInstruction::ResetLeakage { targets, .. } => {
for &q in targets {
bump(q);
Expand Down
31 changes: 17 additions & 14 deletions crates/ppvm-stim/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,13 @@ fn record_bit(record: &[Option<bool>], k: usize) -> bool {
.unwrap_or(false)
}

/// Measure qubit `q` in the Z basis, reset it to |0>, and return the reported
/// (readout-noise-flipped) outcome — the building block for `MR`/`MRX`/`MRY`.
/// Measure qubit `q` in the Z basis, then fully reset it to |0>, and return
/// the reported (readout-noise-flipped) outcome — the building block for
/// `MR`/`MRX`/`MRY`.
///
/// The reset must act on the *true* (pre-flip) outcome, so this cannot delegate
/// to `measure_noisy`; instead it applies the same `flip_with_prob` draw and
/// overwrites the record entry with the reported value, so the measurement
/// record matches the returned result exactly (consistent with `M`).
/// The readout comes first, so a lost or leaked qubit is reported as such
/// before the reset (as for `R`) brings it back. The state reset reuses the
/// true (pre-flip) outcome instead of measuring again.
fn measure_reset_z<T, I, C>(
tab: &mut GeneralizedTableau<T, I, C>,
q: usize,
Expand Down Expand Up @@ -108,11 +108,14 @@ where
I: TableauIndex + Debug + Send + Sync,
{
let true_outcome = tab.measure(q);
let recorded = true_outcome.map(|b| tab.flip_with_prob(b, noise));
tab.overwrite_last_measurement_record(recorded);
// `x` is skipped on inactive qubits; lost qubits already sit in |0⟩.
if true_outcome == Some(true) {
tab.x(q);
}
let recorded = true_outcome.map(|b| tab.flip_with_prob(b, noise));
tab.overwrite_last_measurement_record(recorded);
tab.reset_loss_channel(q);
tab.reset_leakage_channel(q);
recorded
}

Expand Down Expand Up @@ -756,6 +759,11 @@ pub fn execute_validated<T, I, C>(
tab.reset_leakage_channel(q);
}
}
ExtendedInstruction::ResetState { targets, .. } => {
for &q in targets {
tab.reset_state(q);
}
}
ExtendedInstruction::Measure(MeasureOp {
name,
args,
Expand All @@ -778,9 +786,6 @@ pub fn execute_validated<T, I, C>(
results.extend(tab.measure_many(targets));
}
}
// MR cannot delegate to `measure_noisy` because the reset must use
// the *true* outcome — but it shares the readout flip via
// `flip_with_prob`, so the RNG-draw shape matches MZ exactly.
MeasureName::MR => {
for &q in targets {
results.push(measure_reset_z(tab, q, noise));
Expand Down Expand Up @@ -808,9 +813,7 @@ pub fn execute_validated<T, I, C>(
}
}
// MRX/MRY add a reset to the measured eigenstate
// (`MRX = H M R H`, `MRY = S_DAG H M R H S`). The reset must
// use the *true* outcome, so it mirrors the MR arm rather than
// delegating to `measure_noisy`.
// (`MRX = H M R H`, `MRY = S_DAG H M R H S`).
MeasureName::MRX => {
for &q in targets {
tab.h(q);
Expand Down
1 change: 1 addition & 0 deletions crates/ppvm-stim/src/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ fn validate_slice(
| ExtendedInstruction::Loss { .. }
| ExtendedInstruction::CorrelatedLoss { .. }
| ExtendedInstruction::ResetLoss { .. }
| ExtendedInstruction::ResetState { .. }
| ExtendedInstruction::ResetLeakage { .. } => {}
ExtendedInstruction::Leakage { p0, p1, span, .. } => {
let line = span.line(line_map);
Expand Down
32 changes: 32 additions & 0 deletions crates/ppvm-stim/tests/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,38 @@ fn reset_leakage_does_not_restore_lost_qubit() {
assert!(tab.is_lost(0));
}

#[test]
fn reset_restores_lost_and_leaked_qubits_to_zero() {
// `R` is a full reset: a lost or leaked qubit comes back live in |0⟩.
let (results, tab) = run(
"I_ERROR[loss](1.0) 0\nI_ERROR[leakage](0.0, 1.0) 1\nR 0 1\nX 1\nM 0 1",
2,
);
assert!(!tab.is_lost(0) && !tab.is_leaked(1));
assert_eq!(results, vec![Some(false), Some(true)]);
}

#[test]
fn measure_reset_reports_loss_and_leakage_then_resets() {
// MR reads out first (lost -> None, leaked -> pinned bit), then fully resets.
let (results, tab) = run(
"I_ERROR[loss](1.0) 0\nI_ERROR[leakage](0.0, 1.0) 1\nMR 0 1\nM 0 1",
2,
);
assert!(!tab.is_lost(0) && !tab.is_leaked(1));
assert_eq!(results, vec![None, Some(true), Some(false), Some(false)]);
}

#[test]
fn reset_state_leaves_lost_and_leaked_qubits_alone() {
let (results, tab) = run(
"X 2\nI_ERROR[loss](1.0) 0\nI_ERROR[leakage](0.0, 1.0) 1\nI_ERROR[reset_state] 0 1 2\nM 0 1 2",
3,
);
assert!(tab.is_lost(0) && tab.is_leaked(1));
assert_eq!(results, vec![None, Some(true), Some(false)]);
}

#[test]
fn reset_loss_does_not_restore_leaked_qubit() {
let (_, tab) = run("I_ERROR[leakage](0.0, 1.0) 0\nI_ERROR[reset_loss] 0", 1);
Expand Down
40 changes: 37 additions & 3 deletions crates/ppvm-tableau-sum/src/gates/reset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ use num::traits::{One, PrimInt, ToPrimitive, Zero};
use ppvm_tableau::sparsevec::SparseVector;
use ppvm_tableau::tableau_index::TableauIndex;
use ppvm_traits::config::Config;
use ppvm_traits::traits::{Clifford, Reset};
use ppvm_traits::traits::{Clifford, Reset, ResetLossChannel, ResetState};

impl<T, I, C, S> Reset for GeneralizedTableauSum<T, I, C, S>
impl<T, I, C, S> ResetState for GeneralizedTableauSum<T, I, C, S>
where
T: Config,
<<T as Config>::Storage as BitView>::Store: PrimInt,
Expand All @@ -39,7 +39,7 @@ where
+ Copy,
S: EntryStore<T, I, C>,
{
fn reset(&mut self, addr0: usize) {
fn reset_state(&mut self, addr0: usize) {
self.for_each_z_branch(addr0, |tab, outcome, _p| {
// Flip back to |0⟩ on the outcome-1 branch; the helper takes
// care of fingerprint hygiene and the branch / merge plumbing.
Expand All @@ -52,3 +52,37 @@ where
});
}
}

impl<T, I, C, S> Reset for GeneralizedTableauSum<T, I, C, S>
where
T: Config,
<<T as Config>::Storage as BitView>::Store: PrimInt,
I: TableauIndex + Debug + Send + Sync,
C: SparseVector<Complex<T::Coeff>, I> + Debug,
T::Coeff: One
+ Zero
+ Clone
+ num::Num
+ ToPrimitive
+ std::fmt::Debug
+ std::ops::Mul<f64>
+ PartialOrd<f64>
+ PartialOrd
+ Send
+ Sync,
Complex<T::Coeff>: std::ops::Mul<Output = Complex<T::Coeff>>
+ From<Complex64>
+ std::ops::MulAssign
+ std::ops::AddAssign
+ One
+ ComplexFloat
+ Copy,
S: EntryStore<T, I, C>,
{
fn reset(&mut self, addr0: usize) {
// Un-lose first, otherwise `reset_state` skips the qubit. There is
// no leakage on this backend.
self.reset_loss_channel(addr0);
self.reset_state(addr0);
}
}
Loading
Loading