Skip to content

Commit 9409b52

Browse files
committed
Merge remote-tracking branch 'origin/main' into david/42.1-vihaco-isa
2 parents 9bbc419 + 9b071ef commit 9409b52

15 files changed

Lines changed: 630 additions & 5 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ppvm-pauli-sum/src/sum/rot1.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,20 @@ where
128128
}
129129
}
130130

131+
impl<T: Config> RotXY<T> for PauliSum<T>
132+
where
133+
PauliSum<T>: RotationOne<T>,
134+
{
135+
fn r(&mut self, addr0: usize, axis_angle: <T as Config>::Coeff, theta: <T as Config>::Coeff) {
136+
// R(axis_angle, θ) = RZ(axis_angle)·RX(θ)·RZ(−axis_angle). PauliSum runs
137+
// in the Heisenberg picture (observables propagate backward), so the
138+
// sub-rotations are emitted in reverse of the tableau's forward order.
139+
self.rz(addr0, axis_angle.clone());
140+
self.rx(addr0, theta);
141+
self.rz(addr0, -axis_angle);
142+
}
143+
}
144+
131145
/// 2-bit Pauli code: 00 I, 01 X, 10 Z, 11 Y
132146
/// Returns \(ε, k\) so that –i \[P_i, P_j\]/2 = ε · P_k.
133147
/// For every commuting pair it yields (0, 0).
@@ -236,6 +250,32 @@ mod tests {
236250
assert_eq!(answer, expect);
237251
}
238252

253+
#[test]
254+
fn test_r() {
255+
use std::f64::consts::FRAC_PI_2;
256+
let theta = 2.1;
257+
258+
// r(axis_angle=0, θ) == rx(θ).
259+
let mut via_r: PauliSum<ByteF64<2>> = PauliSum::builder().n_qubits(1).build();
260+
via_r += ("Z", 1.0);
261+
via_r.r(0, 0.0, theta);
262+
let mut via_rx: PauliSum<ByteF64<2>> = PauliSum::builder().n_qubits(1).build();
263+
via_rx += ("Z", 1.0);
264+
via_rx.rx(0, theta);
265+
assert!((via_r.overlap(&via_rx) - 1.0).abs() < 1e-9);
266+
267+
// r(axis_angle=π/2, θ) must equal ry(θ) — NOT ry(−θ). This is the case
268+
// that distinguishes the Heisenberg (backward) order from the
269+
// Schrödinger one: a forward-ordered impl would yield ry(−θ) here.
270+
let mut via_r: PauliSum<ByteF64<2>> = PauliSum::builder().n_qubits(1).build();
271+
via_r += ("Z", 1.0);
272+
via_r.r(0, FRAC_PI_2, theta);
273+
let mut via_ry: PauliSum<ByteF64<2>> = PauliSum::builder().n_qubits(1).build();
274+
via_ry += ("Z", 1.0);
275+
via_ry.ry(0, theta);
276+
assert!((via_r.overlap(&via_ry) - 1.0).abs() < 1e-9);
277+
}
278+
239279
#[test]
240280
fn test_rz() {
241281
let mut answer: PauliSum<ByteF64<2>> = PauliSum::builder().n_qubits(1).build();

crates/ppvm-python-native/src/interface.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,12 @@ macro_rules! create_interface {
278278
if truncate { self.inner.truncate(); }
279279
}
280280

281+
#[pyo3(signature = (addr0, axis_angle, theta, truncate = true))]
282+
pub fn r(&mut self, addr0: usize, axis_angle: f64, theta: f64, truncate: bool) {
283+
self.inner.r(addr0, axis_angle, theta);
284+
if truncate { self.inner.truncate(); }
285+
}
286+
281287
// rot2
282288
#[pyo3(signature = (targets, theta, truncate = true))]
283289
pub fn rxx(&mut self, targets: Vec<usize>, theta: f64, truncate: bool) -> PyResult<()> {

crates/ppvm-python-native/src/interface_tableau.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ macro_rules! create_interface {
186186
self.inner.u3(addr0, theta, phi, lam);
187187
}
188188

189+
pub fn r(&mut self, addr0: usize, axis_angle: f64, theta: f64) {
190+
self.inner.r(addr0, axis_angle, theta);
191+
}
192+
189193
// rot2
190194
pub fn rxx(&mut self, targets: Vec<usize>, theta: f64) -> PyResult<()> {
191195
let pairs = crate::flat_pairs(&targets)?;

crates/ppvm-tableau/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ ppvm-traits = { version = "0.1.0", path = "../ppvm-traits" }
1717
ppvm-pauli-sum = { version = "0.1.0", path = "../ppvm-pauli-sum" }
1818
rand = "0.10.1"
1919
smallvec = "1.15"
20+
ppvm-pauli-word = { version = "0.1.0", path = "../ppvm-pauli-word" }
2021

2122
# rayon needs OS threads — native only.
2223
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]

crates/ppvm-tableau/src/data.rs

Lines changed: 131 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,7 @@ pub struct Tableau<T: Config> {
5454
}
5555

5656
impl<T: Config> Tableau<T> {
57-
/// Construct a fresh tableau initialised to `|0…0⟩`.
58-
pub fn new(n_qubits: usize) -> Self {
57+
fn new_data(n_qubits: usize) -> Vec<PhasedPauliWordNoHash<T::Storage, T::BuildHasher>> {
5958
// Initialize tableau for 0 state
6059
let mut data: Vec<PhasedPauliWordNoHash<T::Storage, T::BuildHasher>> =
6160
Vec::with_capacity(2 * n_qubits);
@@ -72,7 +71,12 @@ impl<T: Config> Tableau<T> {
7271
pw.set(i, Pauli::Z);
7372
data.push(pw);
7473
}
74+
data
75+
}
7576

77+
/// Construct a fresh tableau initialised to `|0…0⟩`.
78+
pub fn new(n_qubits: usize) -> Self {
79+
let data = Tableau::<T>::new_data(n_qubits);
7680
Self {
7781
n_qubits,
7882
data,
@@ -87,6 +91,11 @@ impl<T: Config> Tableau<T> {
8791
t
8892
}
8993

94+
pub fn reset_all(&mut self) {
95+
let data = Tableau::<T>::new_data(self.n_qubits);
96+
self.data = data;
97+
}
98+
9099
/// View of the stabilizer rows (the upper half of the tableau).
91100
#[inline]
92101
pub fn stabilizers(&self) -> &[PhasedPauliWordNoHash<T::Storage, T::BuildHasher>] {
@@ -537,7 +546,9 @@ where
537546
Complex<CoeffType>:
538547
std::ops::Mul<Output = Complex<CoeffType>> + std::ops::AddAssign + From<Complex64> + Copy,
539548
{
540-
if items.len() >= RAYON_COEFF_THRESHOLD {
549+
// See `branch_coefficients_parallel`: avoid nesting rayon inside shot-level
550+
// parallelism; the main-thread (single-shot) path is unaffected.
551+
if items.len() >= RAYON_COEFF_THRESHOLD && rayon::current_thread_index().is_none() {
541552
use rayon::prelude::*;
542553

543554
return items
@@ -675,6 +686,22 @@ where
675686
s
676687
}
677688

689+
pub fn reset_all(&mut self) {
690+
self.tableau.reset_all();
691+
692+
let mut coefficients = C::new();
693+
let complex_one = Complex {
694+
re: T::Coeff::one(),
695+
im: T::Coeff::zero(),
696+
};
697+
coefficients.unsafe_insert(I::zero(), complex_one);
698+
self.coefficients = coefficients;
699+
for l in self.is_lost.iter_mut() {
700+
*l &= false;
701+
}
702+
self.measurement_record.clear();
703+
}
704+
678705
/// Clone the quantum state but reinitialize the RNG, producing an independent simulation
679706
/// branch. If `seed` is `Some`, the new RNG is seeded deterministically; if `None`, it is
680707
/// seeded from OS entropy.
@@ -848,6 +875,36 @@ where
848875
(p_word.phase, stab_anticomm_bits, destab_anticomm_bits)
849876
}
850877

878+
/// Multi-qubit generalization of [`compute_decomposition`]: conjugate an
879+
/// arbitrary `PauliWord` through the tableau and return the same triple
880+
/// `(phase, stab_anticomm_bits, destab_anticomm_bits)`.
881+
///
882+
/// Algorithm: call [`compute_decomposition`] for each non-identity qubit
883+
/// in the input, then multiply the resulting single-qubit conjugates in
884+
/// canonical-basis form `i^φ X^x Z^z`. Pauli multiplication picks up a
885+
/// `(-1)^{popcount(z_running & x_new)}` cross-phase from
886+
/// `Z^z_a X^x_b = (-1)^{z_a · x_b} X^x_b Z^z_a`.
887+
pub(crate) fn compute_decomposition_word<W: PauliWordTrait>(&self, word: &W) -> (u8, I, I)
888+
where
889+
<<T as Config>::Storage as BitView>::Store: PrimInt,
890+
{
891+
let mut phase = 0u8;
892+
let mut stab_anticomm = I::zero();
893+
let mut destab_anticomm = I::zero();
894+
for q in 0..self.n_qubits() {
895+
let p_q = word.get(q);
896+
if p_q == Pauli::I {
897+
continue;
898+
}
899+
let (q_phase, q_stab, q_destab) = self.compute_decomposition(q, p_q);
900+
let cross = 2 * (symplectic_inner(destab_anticomm, q_stab) as u8 % 2);
901+
phase = (phase + q_phase + cross) % 4;
902+
stab_anticomm = stab_anticomm ^ q_stab;
903+
destab_anticomm = destab_anticomm ^ q_destab;
904+
}
905+
(phase, stab_anticomm, destab_anticomm)
906+
}
907+
851908
/// every basis index is a bit string alpha defining the basis state
852909
/// the phase when applying a Pauli is the product of all destabilizer phases
853910
/// and the phase contributions from the commutation relations
@@ -1415,4 +1472,75 @@ mod tests {
14151472
snapshot_tableau(&tab2.tableau)
14161473
);
14171474
}
1475+
1476+
// ─── reset_all ────────────────────────────────────────────────────
1477+
1478+
/// `GeneralizedTableau::reset_all` restores the full state to a fresh
1479+
/// `|0…0⟩` tableau: identical stabilizer/destabilizer rows and a single
1480+
/// identity coefficient, even after non-Clifford branching.
1481+
#[test]
1482+
fn reset_all_restores_fresh_state() {
1483+
let mut tab: TestTableau = GeneralizedTableau::new(3, 1e-12);
1484+
let fresh: TestTableau = GeneralizedTableau::new(3, 1e-12);
1485+
1486+
tab.h(0);
1487+
tab.cnot(0, 1);
1488+
tab.ry(2, 0.7); // non-Clifford: branches the coefficient vector
1489+
assert!(
1490+
tab.coefficients.iter().count() > 1,
1491+
"rotation should branch the coefficient vector"
1492+
);
1493+
1494+
tab.reset_all();
1495+
1496+
assert_eq!(
1497+
snapshot_tableau(&tab.tableau),
1498+
snapshot_tableau(&fresh.tableau)
1499+
);
1500+
let coeffs: Vec<_> = tab.coefficients.iter().copied().collect();
1501+
let fresh_coeffs: Vec<_> = fresh.coefficients.iter().copied().collect();
1502+
assert_eq!(coeffs, fresh_coeffs);
1503+
}
1504+
1505+
/// A full reset clears the measurement record. Regression guard: an earlier
1506+
/// version left it intact, so `current_measurement_record` returned stale
1507+
/// outcomes after a reset.
1508+
#[test]
1509+
fn reset_all_clears_measurement_record() {
1510+
let mut tab: TestTableau = GeneralizedTableau::new(2, 1e-12);
1511+
tab.append_measurement_record(Some(true));
1512+
tab.append_measurement_record(None);
1513+
assert_eq!(tab.current_measurement_record().len(), 2);
1514+
1515+
tab.reset_all();
1516+
1517+
assert!(tab.current_measurement_record().is_empty());
1518+
}
1519+
1520+
/// A full reset clears per-qubit loss flags.
1521+
#[test]
1522+
fn reset_all_clears_loss_flags() {
1523+
let mut tab: TestTableau = GeneralizedTableau::new(3, 1e-12);
1524+
tab.is_lost[0] = true;
1525+
tab.is_lost[2] = true;
1526+
1527+
tab.reset_all();
1528+
1529+
assert!(tab.is_lost.iter().all(|&lost| !lost));
1530+
}
1531+
1532+
/// `Tableau::reset_all` restores the fresh identity tableau rows.
1533+
#[test]
1534+
fn tableau_reset_all_restores_fresh_rows() {
1535+
let mut tab: Tableau<TestConfig> = Tableau::new(4);
1536+
let fresh: Tableau<TestConfig> = Tableau::new(4);
1537+
1538+
tab.h(0);
1539+
tab.s(1);
1540+
tab.h(3);
1541+
1542+
tab.reset_all();
1543+
1544+
assert_eq!(snapshot_tableau(&tab), snapshot_tableau(&fresh));
1545+
}
14181546
}

0 commit comments

Comments
 (0)