Skip to content

Commit 84174e7

Browse files
authored
Merge branch 'main' into split/1-translation-symmetry
2 parents dba30fd + da073eb commit 84174e7

11 files changed

Lines changed: 65 additions & 20 deletions

File tree

‎Cargo.lock‎

Lines changed: 6 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/ppvm-python-native/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ pyo3 = { version = "0.29.0", features = [
3232
"multiple-pymethods",
3333
"abi3-py310",
3434
] }
35+
rand = "0.10.3"

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use paste::paste;
66
use ppvm_tableau::prelude::*;
77
use pyo3::prelude::*;
88
use pyo3::types::{PyByteArray, PyComplex, PyDict};
9+
use rand::{RngExt, SeedableRng, rngs::SmallRng};
910

1011
pub(crate) fn measurement_to_u8(m: Option<bool>) -> u8 {
1112
match m {
@@ -318,10 +319,9 @@ macro_rules! create_interface {
318319
///
319320
/// Shots run in parallel on rayon's global thread pool (GIL
320321
/// released), falling back to serial for small batches. Shot `i`
321-
/// is seeded with `seed.wrapping_add(i)` when `seed` is given
322-
/// (wrapping mod 2⁶⁴), so results are reproducible and
323-
/// independent of the thread count; set the `RAYON_NUM_THREADS`
324-
/// environment variable to control the pool size.
322+
/// is seeded with a scrambled `seed` plus `i`, so results are
323+
/// reproducible, thread-count independent, and nearby seeds give
324+
/// unrelated shots. Set `RAYON_NUM_THREADS` to control the pool size.
325325
///
326326
/// Returns the outcome codes (0/1/2 = zero/one/lost) as one flat,
327327
/// shot-major `bytearray` plus its `(num_shots, n_measurements)` shape.
@@ -346,7 +346,9 @@ macro_rules! create_interface {
346346
Some(s) => GeneralizedTableau::<$type, $indexType>::new_with_seed(
347347
n_qubits,
348348
min_abs_coeff,
349-
s.wrapping_add(i as u64),
349+
SmallRng::seed_from_u64(s)
350+
.random::<u64>()
351+
.wrapping_add(i as u64),
350352
),
351353
None => GeneralizedTableau::<$type, $indexType>::new(
352354
n_qubits,

‎crates/ppvm-stim/src/executor.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,8 @@ where
157157
/// `make_tableau(i)`.
158158
///
159159
/// The shot index lets callers derive a deterministic per-shot seed (e.g.
160-
/// `seed + i`) so results are independent of evaluation order — the same
160+
/// `base.wrapping_add(i)` with `base = SmallRng::seed_from_u64(seed).random()`,
161+
/// not plain `seed + i`) so results are independent of evaluation order — the same
161162
/// factory then yields identical results from `sample_parallel` (when the `rayon` feature is enabled).
162163
pub fn sample_serial<T, I, C, F>(
163164
program: &ExtendedProgram,

‎crates/ppvm-vihaco/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ vihaco-parser = "0.4.1"
2525
vihaco-parser-derive = "0.4.1"
2626
vihaco-circuit-isa = { version = "0.1.0", path = "../vihaco-circuit-isa" }
2727
ppvm-pauli-sum = { version = "0.1.0", path = "../ppvm-pauli-sum" }
28+
rand = "0.10.3"

‎crates/ppvm-vihaco/src/shots.rs‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use crate::PPVMModule;
1313
use crate::composite::PPVM;
1414
use crate::measurements::MeasurementResult;
15+
use rand::{RngExt, SeedableRng, rngs::SmallRng};
1516

1617
/// One shot's full output: the measurement record and the trace-instruction
1718
/// record. Either may be empty depending on what the program emits.
@@ -27,10 +28,15 @@ pub const PARALLEL_SHOT_THRESHOLD: usize = 128;
2728

2829
/// Per-shot seed derived from the base seed and the shot index, so every shot
2930
/// gets a distinct RNG stream (a shared seed would make all shots identical).
30-
/// Depends only on `(base, index)`, so serial and parallel runs are bit-for-bit
31-
/// identical for a given seed regardless of thread count.
31+
/// The base is scrambled first so nearby seeds give unrelated shots. Depends
32+
/// only on `(base, index)`, so serial and parallel runs are identical.
33+
#[inline]
3234
fn shot_seed(base: Option<u64>, index: usize) -> Option<u64> {
33-
base.map(|b| b.wrapping_add(index as u64))
35+
base.map(|b| {
36+
SmallRng::seed_from_u64(b)
37+
.random::<u64>()
38+
.wrapping_add(index as u64)
39+
})
3440
}
3541

3642
/// Run a single shot on a fresh machine and return both records.
@@ -178,6 +184,19 @@ mod tests {
178184
);
179185
}
180186

187+
#[test]
188+
fn nearby_seeds_do_not_share_shots() {
189+
// With `seed + i`, seed 8 was seed 7 shifted by one shot.
190+
let m = module(RANDOM);
191+
let a = run_shots_serial(&m, 128, Some(7)).unwrap();
192+
let b = run_shots_serial(&m, 128, Some(8)).unwrap();
193+
assert_ne!(
194+
a[1..],
195+
b[..127],
196+
"seed 8 reproduced seed 7's shots shifted by one"
197+
);
198+
}
199+
181200
#[cfg(feature = "rayon")]
182201
#[test]
183202
fn serial_and_parallel_match_for_same_seed() {

‎docs/examples/stim_program.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@
2323
# Multi-shot sampling. The two measurement outcomes are correlated on every shot.
2424
shots = sample_stim(prog, n_qubits=2, num_shots=16, seed=0)
2525
print(f"sampled {len(shots)} shots") # → sampled 16 shots
26-
print("first shot:", shots[0]) # → first shot: [<MeasurementResult.ONE: 1>, <MeasurementResult.ONE: 1>]
26+
print("first shot:", shots[0]) # → first shot: [<MeasurementResult.ZERO: 0>, <MeasurementResult.ZERO: 0>]
2727
all_correlated = all(s[0] == s[1] for s in shots)
2828
print("all shots correlated:", all_correlated) # → all shots correlated: True

‎docs/examples/stim_sampling.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
patterns = Counter(tuple(int(r) for r in shot) for shot in shots)
2727
for pattern, count in sorted(patterns.items()):
2828
print(f"{pattern}: {count}")
29-
# → (0, 0): 102
30-
# → (1, 1): 98
29+
# → (0, 0): 111
30+
# → (1, 1): 89
3131

3232
# A GHZ state should only produce (0, 0) or (1, 1) — never (0, 1) or (1, 0).
3333
correlated_fraction = sum(c for p, c in patterns.items() if p[0] == p[1]) / len(shots)

‎ppvm-python/src/ppvm/generalized_tableau.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -439,9 +439,10 @@ def sample(
439439
440440
Shots run in parallel across CPU cores (the GIL is released during
441441
sampling), with a serial fallback for small batches. When ``seed`` is
442-
given (it must fit in an unsigned 64-bit integer), shot ``i`` uses
443-
``(seed + i) % 2**64`` (wrapping ``u64`` arithmetic), so results are
444-
reproducible and independent of the number of threads. Set the
442+
given (it must fit in an unsigned 64-bit integer), each shot's seed is
443+
derived deterministically from ``seed`` and the shot index, so results
444+
are reproducible and independent of the number of threads, and nearby
445+
seeds (``seed`` and ``seed + 1``) give unrelated shots. Set the
445446
``RAYON_NUM_THREADS`` environment variable before the first call to
446447
control the pool size (it defaults to the number of logical cores).
447448

‎ppvm-python/test/generalized_tableau/test_stim.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,22 @@ def test_generalized_tableau_sample_classmethod_equivalent():
255255
assert a == b
256256

257257

258+
def test_sample_nearby_seeds_do_not_share_shots():
259+
# With `seed + i`, seed 8 was seed 7 shifted by one shot.
260+
n = 32
261+
qubits = " ".join(map(str, range(n)))
262+
prog = StimProgram.parse(f"H {qubits}\nM {qubits}")
263+
264+
def shots(seed):
265+
res = sample_stim(prog, n_qubits=n, num_shots=1000, seed=seed)
266+
return [tuple(int(v) for v in shot) for shot in res]
267+
268+
a, b = shots(7), shots(8)
269+
assert b[:-1] != a[1:]
270+
# Chance collisions of 32-bit shots are rare (~2e-4).
271+
assert len(set(a) & set(b)) < 5
272+
273+
258274
def test_sample_stim_zero_shots_returns_empty():
259275
prog = StimProgram.parse("X 0\nM 0")
260276
assert sample_stim(prog, n_qubits=1, num_shots=0) == []

0 commit comments

Comments
 (0)