Skip to content

Commit 199268b

Browse files
authored
Merge pull request #391 from igerber/rust-rand-bump
chore(rust): bump rand 0.8 -> 0.10, rand_xoshiro 0.6 -> 0.8
2 parents 6b928bc + 90f5261 commit 199268b

4 files changed

Lines changed: 62 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Changed
11+
- **Rust dependency upgrades**: bumped `rand` 0.8 → 0.10 and `rand_xoshiro` 0.6 → 0.8 in the Rust backend (the two crates are coupled through `rand_core` and must move together). MSRV bumped from Rust 1.84 → 1.85 to satisfy the new dependency requirements. Three call sites in `rust/src/bootstrap.rs` updated for the `rand 0.9` API rename: `gen::<bool>()` → `random::<bool>()`, `gen::<f64>()` → `random::<f64>()`, `gen_range(0..6)` → `random_range(0..6)`. **Webb wild bootstrap byte stream shifted** as a side effect: `rand 0.9` reworked the internal algorithm for `random_range` (improved rejection sampling), so `Xoshiro256PlusPlus::seed_from_u64(seed)` followed by `random_range(0..6)` consumes RNG bytes differently than the old `gen_range(0..6)` did. Distributional properties of Webb weights are unchanged (still uniform over the 6-point support); aggregate inference (SE, p-values, CI) converges to the same values for any reasonable `n_bootstrap`. Rademacher and Mammen byte streams are bit-identical to the prior release. Anyone with a saved Rust+Webb baseline pinning specific seeded results will see different numbers; the regression test suite uses within-build seed-reproducibility (not cross-version baselines) so all internal tests pass unchanged. New regression guard `TestRustBackend::test_bootstrap_weights_bit_identity_snapshot` pins fixed-seed weights for all three weight types, so any future RNG drift fails loudly with a localized error message.
12+
813
## [3.3.1] - 2026-04-25
914

1015
### Changed

rust/Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "diff_diff_rust"
33
version = "3.3.1"
44
edition = "2021"
5-
rust-version = "1.84"
5+
rust-version = "1.85"
66
description = "Rust backend for diff-diff DiD library"
77
license = "MIT"
88

@@ -25,8 +25,8 @@ openblas = ["ndarray/blas"]
2525
pyo3 = "0.28"
2626
numpy = "0.28"
2727
ndarray = { version = "0.17", features = ["rayon"] }
28-
rand = "0.8"
29-
rand_xoshiro = "0.6"
28+
rand = "0.10"
29+
rand_xoshiro = "0.8"
3030
rayon = "1.8"
3131

3232
# Pure Rust linear algebra for SVD/matrix inversion (no external deps).

rust/src/bootstrap.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ fn generate_rademacher_batch(n_bootstrap: usize, n_units: usize, seed: u64) -> A
6767
.for_each(|(i, mut row)| {
6868
let mut rng = Xoshiro256PlusPlus::seed_from_u64(seed.wrapping_add(i as u64));
6969
for elem in row.iter_mut() {
70-
*elem = if rng.gen::<bool>() { 1.0 } else { -1.0 };
70+
*elem = if rng.random::<bool>() { 1.0 } else { -1.0 };
7171
}
7272
});
7373

@@ -102,7 +102,7 @@ fn generate_mammen_batch(n_bootstrap: usize, n_units: usize, seed: u64) -> Array
102102
.for_each(|(i, mut row)| {
103103
let mut rng = Xoshiro256PlusPlus::seed_from_u64(seed.wrapping_add(i as u64));
104104
for elem in row.iter_mut() {
105-
*elem = if rng.gen::<f64>() < prob_neg {
105+
*elem = if rng.random::<f64>() < prob_neg {
106106
val_neg
107107
} else {
108108
val_pos
@@ -142,7 +142,7 @@ fn generate_webb_batch(n_bootstrap: usize, n_units: usize, seed: u64) -> Array2<
142142
let mut rng = Xoshiro256PlusPlus::seed_from_u64(seed.wrapping_add(i as u64));
143143
for elem in row.iter_mut() {
144144
// Uniform selection: generate integer 0-5, index into weights_table
145-
let bucket = rng.gen_range(0..6);
145+
let bucket = rng.random_range(0..6);
146146
*elem = weights_table[bucket];
147147
}
148148
});

tests/test_rust_backend.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,57 @@ def test_bootstrap_different_seeds(self):
100100
weights2 = generate_bootstrap_weights_batch(100, 50, "rademacher", 43)
101101
assert not np.array_equal(weights1, weights2)
102102

103+
def test_bootstrap_weights_bit_identity_snapshot(self):
104+
"""Pin fixed-seed bootstrap weight output byte-for-byte.
105+
106+
Regression guard against silent RNG output drift across
107+
`rand` / `rand_xoshiro` crate upgrades. Distributional moment
108+
tests would not catch a byte shift that preserves the
109+
distribution (e.g. `rand 0.9`'s `random_range` algorithm
110+
change relative to `rand 0.8`'s `gen_range`).
111+
112+
If this test fails after a Rust dependency bump, the byte stream
113+
has shifted. Decide deliberately whether to accept the new
114+
baseline (regenerate these values) or pin to a compatible
115+
crate version.
116+
"""
117+
from diff_diff._rust_backend import generate_bootstrap_weights_batch
118+
119+
# Captured under rand 0.10 + rand_xoshiro 0.8 with seed=42.
120+
# Rademacher and Mammen bytes match rand 0.8 + rand_xoshiro 0.6;
121+
# Webb bytes shifted in the rand 0.9 random_range algorithm change.
122+
expected = {
123+
"rademacher": np.array(
124+
[
125+
[1.0, -1.0, 1.0, 1.0],
126+
[-1.0, 1.0, 1.0, 1.0],
127+
]
128+
),
129+
"mammen": np.array(
130+
[
131+
[1.618033988749895, -0.6180339887498949, 1.618033988749895, -0.6180339887498949],
132+
[-0.6180339887498949, -0.6180339887498949, 1.618033988749895, 1.618033988749895],
133+
]
134+
),
135+
"webb": np.array(
136+
[
137+
[1.0, -1.0, 1.224744871391589, 1.0],
138+
[-1.0, 0.7071067811865476, 1.224744871391589, 1.224744871391589],
139+
]
140+
),
141+
}
142+
for weight_type, expected_arr in expected.items():
143+
actual = generate_bootstrap_weights_batch(2, 4, weight_type, 42)
144+
# Strict bit-identity: the snapshot values are either exact
145+
# (Rademacher = +/-1.0) or computed once via correctly-rounded
146+
# IEEE 754 sqrt in Rust (Mammen, Webb), so cross-platform
147+
# bit-equality holds on conformant hardware.
148+
np.testing.assert_array_equal(
149+
actual,
150+
expected_arr,
151+
err_msg=f"{weight_type} bootstrap weights drifted from pinned baseline",
152+
)
153+
103154
# =========================================================================
104155
# Synthetic Weight Tests
105156
# =========================================================================

0 commit comments

Comments
 (0)