From 0627ccda376d5651017f0f8d723521f7725f6913 Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Wed, 24 Jun 2026 14:24:35 -0500 Subject: [PATCH 01/12] fix: add RNGadvance support to pecos RNG model --- .../src/pecos/engines/cvm/rng_model.py | 36 ++++++++++- .../tests/pecos/unit/test_rng.py | 63 +++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py b/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py index 5ce26db4d..cacb0e94f 100644 --- a/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py +++ b/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py @@ -27,7 +27,7 @@ def __init__( self.current_bound = current_bound self.count = 0 self.pcg = RngPcg() - self.seed = self.set_seed(seed) + self.set_seed(seed) def __str__(self) -> str: """Returns the str representation of the model.""" @@ -37,6 +37,7 @@ def set_seed(self, seed: int) -> None: """Setting the seed for generating random numbers.""" self.seed = seed self.pcg.srandom(seed) + self.count = 0 def set_bound(self, bound: int) -> None: """Setting the current bound for generating random numbers.""" @@ -59,10 +60,35 @@ def set_index(self, index: int) -> None: while self.count < index: self.rng_random() + def set_relative_index(self, delta: int) -> None: + """Move relative to the current random-number stream index.""" + target_index = self.count + delta + if target_index < 0: + error_msg = ( + f"RNGadvance({delta}) cannot move before the start of the stream: " + f"current stream index is {self.count}" + ) + raise ValueError(error_msg) + + if delta < 0: + self.pcg = RngPcg() + self.pcg.srandom(self.seed) + self.count = 0 + + while self.count < target_index: + self.rng_random() + def extract_val(self, param: str, output: dict) -> int: """Responsible for extracting the value of interest depending on the type of the parameter being passed in.""" - if param.isdigit(): - val = int(param) + if isinstance(param, int): + val = param + else: + try: + val = int(param) + except ValueError: + val = None + if val is not None: + pass elif "[" in param: idx_creg = param.split("[") creg = output[idx_creg[0]] @@ -90,6 +116,10 @@ def eval_func(self, params: dict, output: dict) -> None: index_var = params.get("args")[0] index = self.extract_val(index_var, output) self.set_index(index) + elif func_name == "RNGadvance": + delta_var = params.get("args")[0] + delta = self.extract_val(delta_var, output) + self.set_relative_index(delta) elif func_name == "RNGnum": creg_name = params.get("assign_vars")[0] creg = output[creg_name] diff --git a/python/quantum-pecos/tests/pecos/unit/test_rng.py b/python/quantum-pecos/tests/pecos/unit/test_rng.py index 7c49c0986..a0d1b6ac4 100644 --- a/python/quantum-pecos/tests/pecos/unit/test_rng.py +++ b/python/quantum-pecos/tests/pecos/unit/test_rng.py @@ -6,12 +6,21 @@ from pecos.engines.cvm.rng_model import RNGModel +def draw_at_index(seed: int, index: int) -> int: + """Return the next random value after consuming ``index`` values from a seeded stream.""" + rng = RNGModel(shot_id=0) + rng.set_seed(seed) + rng.set_index(index) + return rng.rng_random() + + def test_set_seed() -> None: """Verifies that a seed is set properly for our RNG model.""" rng = RNGModel(shot_id=0) seed = 42 rng.set_seed(seed) assert rng.seed == seed + assert rng.count == 0 def test_random_number() -> None: @@ -56,6 +65,60 @@ def test_set_idx() -> None: assert rng.count == idx +def test_relative_advance_forward() -> None: + """Verifies that a forward relative advance lands on the expected stream position.""" + rng = RNGModel(shot_id=0) + rng.set_seed(42) + + rng.set_relative_index(5) + + assert rng.count == 5 + assert rng.rng_random() == draw_at_index(42, 5) + + +def test_relative_advance_backward() -> None: + """Verifies that a backward relative advance reconstructs the stream from the seed.""" + rng = RNGModel(shot_id=0) + rng.set_seed(42) + rng.set_relative_index(6) + + rng.eval_func({"func": "RNGadvance", "args": ["-3"]}, {}) + + assert rng.count == 3 + assert rng.rng_random() == draw_at_index(42, 3) + + +def test_reseed_then_advance_changes_draw_and_resets_count() -> None: + """Verifies reseeding resets the logical position used by later relative advances.""" + rng = RNGModel(shot_id=0) + rng.set_seed(42) + rng.set_relative_index(5) + advanced_val = rng.rng_random() + + rng.set_seed(42) + immediate_val = rng.rng_random() + + assert advanced_val != immediate_val + assert immediate_val == 1085446021 + assert rng.count == 1 + + +def test_relative_advance_keeps_count_consistent_after_draws() -> None: + """Verifies count tracks the current stream position after advances and draws.""" + rng = RNGModel(shot_id=0) + rng.set_seed(42) + + rng.set_relative_index(5) + assert rng.count == 5 + + rng.rng_random() + assert rng.count == 6 + + rng.set_relative_index(-2) + assert rng.count == 4 + assert rng.rng_random() == draw_at_index(42, 4) + + def test_multiple_bounded_rand() -> None: """For several randomly generated number, with a random bound, verifies that its appropriate.""" rng = RNGModel(shot_id=0) From 5d59c45c9c2a2f1f82db86ac910a6ed1846e173e Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Wed, 24 Jun 2026 14:34:21 -0500 Subject: [PATCH 02/12] style: format Julia sources for CI --- julia/PECOS.jl/src/Decoder.jl | 12 +++++++++--- julia/PECOS.jl/src/Simulator.jl | 8 ++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/julia/PECOS.jl/src/Decoder.jl b/julia/PECOS.jl/src/Decoder.jl index e028cf8ab..2a46ffcb4 100644 --- a/julia/PECOS.jl/src/Decoder.jl +++ b/julia/PECOS.jl/src/Decoder.jl @@ -44,11 +44,17 @@ export AbstractDecoder, DecodingResult, decode, check_count, bit_count Result of a decoding operation. """ struct DecodingResult - """Decoded observable/correction vector.""" + """ + Decoded observable/correction vector. + """ observable::Vector{UInt8} - """Weight/cost of the solution.""" + """ + Weight/cost of the solution. + """ weight::Float64 - """Whether the decoder converged (nothing = unknown).""" + """ + Whether the decoder converged (nothing = unknown). + """ converged::Union{Bool,Nothing} end diff --git a/julia/PECOS.jl/src/Simulator.jl b/julia/PECOS.jl/src/Simulator.jl index 18016e502..7ca8549cb 100644 --- a/julia/PECOS.jl/src/Simulator.jl +++ b/julia/PECOS.jl/src/Simulator.jl @@ -55,9 +55,13 @@ export sz!, h!, cx!, mz, reset!, rx!, rz!, rzz! Result of a Z-basis measurement. """ struct MeasurementResult - """Measurement outcome: false = |0>, true = |1>.""" + """ + Measurement outcome: false = |0>, true = |1>. + """ outcome::Bool - """Whether the outcome was deterministic.""" + """ + Whether the outcome was deterministic. + """ is_deterministic::Bool end From 6786dd5365d17cf63ede553cca31da3e7cfcf87e Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Wed, 24 Jun 2026 14:43:48 -0500 Subject: [PATCH 03/12] ci: harden cargo registry fetches on windows --- .github/workflows/python-test.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index eebabcc52..f4b62e4ee 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -109,6 +109,11 @@ jobs: rustup default stable "$HOME\.cargo\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append $env:Path += ";$HOME\.cargo\bin" + # Cargo's default HTTP/2 multiplexing has been flaky on GitHub's + # Windows runners while talking to crates.io during the first + # dependency resolution in `just ci-env`. + "CARGO_HTTP_MULTIPLEXING=false" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "CARGO_NET_RETRY=10" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append rustup show - name: Set up Rust (Unix) From 752e85b489d43cf5e852127d6e08372d12674f77 Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Wed, 24 Jun 2026 14:54:04 -0500 Subject: [PATCH 04/12] ci: stabilize cargo network access in python workflow --- .github/workflows/python-test.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index f4b62e4ee..75496a29e 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -8,6 +8,8 @@ env: RUSTFLAGS: -C debuginfo=0 RUST_BACKTRACE: 1 PYTHONUTF8: 1 + CARGO_HTTP_MULTIPLEXING: "false" + CARGO_NET_RETRY: "10" LLVM_VERSION: "14.0.6" # Force the MWPF decoder feature on in CI so we exercise the cmake-dependent # build path and catch regressions. GitHub-hosted runners ship cmake. @@ -109,11 +111,6 @@ jobs: rustup default stable "$HOME\.cargo\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append $env:Path += ";$HOME\.cargo\bin" - # Cargo's default HTTP/2 multiplexing has been flaky on GitHub's - # Windows runners while talking to crates.io during the first - # dependency resolution in `just ci-env`. - "CARGO_HTTP_MULTIPLEXING=false" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "CARGO_NET_RETRY=10" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append rustup show - name: Set up Rust (Unix) From d2b4bc303378ea3004d4579d9463b02f03a64825 Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Wed, 24 Jun 2026 15:08:08 -0500 Subject: [PATCH 05/12] fix: simplify RNG argument parsing --- .../src/pecos/engines/cvm/rng_model.py | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py b/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py index cacb0e94f..daa122e92 100644 --- a/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py +++ b/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py @@ -78,28 +78,26 @@ def set_relative_index(self, delta: int) -> None: while self.count < target_index: self.rng_random() - def extract_val(self, param: str, output: dict) -> int: + def extract_val(self, param: str | int, output: dict) -> int: """Responsible for extracting the value of interest depending on the type of the parameter being passed in.""" if isinstance(param, int): - val = param - else: - try: - val = int(param) - except ValueError: - val = None - if val is not None: + return param + + try: + return int(param) + except (TypeError, ValueError): pass - elif "[" in param: + + if "[" in param: idx_creg = param.split("[") creg = output[idx_creg[0]] idx = int(idx_creg[-1][:-1]) - val = int(creg[idx]) + return int(creg[idx]) elif param == "JOB_shotnum": - val = self.shot_id - else: - reg = output[param] - val = int(reg) - return val + return self.shot_id + + reg = output[param] + return int(reg) def eval_func(self, params: dict, output: dict) -> None: """Calling the appropriate functions dependent on RNG Function call passed in.""" From 005856c7c8c8d291b3b615bc42137c788e441a95 Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Wed, 24 Jun 2026 15:32:14 -0500 Subject: [PATCH 06/12] fix: tighten RNG validation and rewind semantics --- .../src/pecos/engines/cvm/rng_model.py | 26 +++++++++++++-- .../tests/pecos/unit/test_rng.py | 33 ++++++++++++++++++- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py b/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py index daa122e92..341e53ab5 100644 --- a/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py +++ b/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py @@ -26,6 +26,7 @@ def __init__( self.shot_id = shot_id self.current_bound = current_bound self.count = 0 + self._draw_bounds: list[int | None] = [] self.pcg = RngPcg() self.set_seed(seed) @@ -35,17 +36,28 @@ def __str__(self) -> str: def set_seed(self, seed: int) -> None: """Setting the seed for generating random numbers.""" + self._require_non_negative("seed", seed) self.seed = seed self.pcg.srandom(seed) self.count = 0 + self._draw_bounds = [] def set_bound(self, bound: int) -> None: """Setting the current bound for generating random numbers.""" + self._require_non_negative("bound", bound) self.current_bound = bound + @staticmethod + def _require_non_negative(name: str, value: int) -> None: + """Raise a clear error when an RNG parameter is negative.""" + if value < 0: + error_msg = f"RNG {name} must be non-negative: got {value}" + raise ValueError(error_msg) + def rng_random(self) -> int: """Generating a random number and keeping track of how many we have generated.""" rng_num = self.pcg.random() if self.current_bound == 0 else self.pcg.boundedrand(self.current_bound) + self._draw_bounds.append(self.current_bound) self.count += 1 return rng_num @@ -54,6 +66,7 @@ def set_index(self, index: int) -> None: The number after from the stream will be the idx of interest. """ + self._require_non_negative("index", index) if self.count > index: error_msg = f"RNGindex({index}) cannot move backward: current stream index is {self.count}" raise ValueError(error_msg) @@ -71,12 +84,21 @@ def set_relative_index(self, delta: int) -> None: raise ValueError(error_msg) if delta < 0: + prefix_bounds = self._draw_bounds[:target_index] + active_bound = self.current_bound self.pcg = RngPcg() self.pcg.srandom(self.seed) self.count = 0 + self._draw_bounds = [] - while self.count < target_index: - self.rng_random() + for historical_bound in prefix_bounds: + self.current_bound = historical_bound + self.rng_random() + + self.current_bound = active_bound + else: + while self.count < target_index: + self.rng_random() def extract_val(self, param: str | int, output: dict) -> int: """Responsible for extracting the value of interest depending on the type of the parameter being passed in.""" diff --git a/python/quantum-pecos/tests/pecos/unit/test_rng.py b/python/quantum-pecos/tests/pecos/unit/test_rng.py index a0d1b6ac4..7cd0a049f 100644 --- a/python/quantum-pecos/tests/pecos/unit/test_rng.py +++ b/python/quantum-pecos/tests/pecos/unit/test_rng.py @@ -3,6 +3,7 @@ import sys import pecos as pc +import pytest from pecos.engines.cvm.rng_model import RNGModel @@ -97,9 +98,10 @@ def test_reseed_then_advance_changes_draw_and_resets_count() -> None: rng.set_seed(42) immediate_val = rng.rng_random() + expected_first_draw = draw_at_index(42, 0) assert advanced_val != immediate_val - assert immediate_val == 1085446021 + assert immediate_val == expected_first_draw assert rng.count == 1 @@ -119,6 +121,35 @@ def test_relative_advance_keeps_count_consistent_after_draws() -> None: assert rng.rng_random() == draw_at_index(42, 4) +def test_negative_values_are_rejected_for_non_advance_rng_funcs() -> None: + """Verifies only RNGadvance accepts negative numeric arguments.""" + rng = RNGModel(shot_id=0) + + with pytest.raises(ValueError, match=r"RNG seed must be non-negative: got -1"): + rng.eval_func({"func": "RNGseed", "args": ["-1"]}, {}) + + with pytest.raises(ValueError, match=r"RNG bound must be non-negative: got -1"): + rng.eval_func({"func": "RNGbound", "args": ["-1"]}, {}) + + with pytest.raises(ValueError, match=r"RNG index must be non-negative: got -1"): + rng.eval_func({"func": "RNGindex", "args": ["-1"]}, {}) + + +def test_relative_advance_backward_replays_historical_bounds() -> None: + """Verifies rewind reconstructs the stream using the original bounds history.""" + rng = RNGModel(shot_id=0) + rng.set_seed(42) + rng.set_bound(16) + rng.rng_random() + rng.set_bound(0) + expected_second_draw = rng.rng_random() + + rng.set_relative_index(-1) + + assert rng.count == 1 + assert rng.rng_random() == expected_second_draw + + def test_multiple_bounded_rand() -> None: """For several randomly generated number, with a random bound, verifies that its appropriate.""" rng = RNGModel(shot_id=0) From d96c9f8186b0c7a350470b1414f0a8dfa868ebfc Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Wed, 24 Jun 2026 15:43:05 -0500 Subject: [PATCH 07/12] fix: normalize RNG bounds for replay --- .../src/pecos/engines/cvm/rng_model.py | 17 ++++++++++++----- .../quantum-pecos/tests/pecos/unit/test_rng.py | 6 ++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py b/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py index 341e53ab5..e919f455f 100644 --- a/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py +++ b/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py @@ -24,9 +24,9 @@ def __init__( ) -> None: """Constructs an RNGModel object.""" self.shot_id = shot_id - self.current_bound = current_bound + self.current_bound = self._normalize_bound(current_bound) self.count = 0 - self._draw_bounds: list[int | None] = [] + self._draw_bounds: list[int] = [] self.pcg = RngPcg() self.set_seed(seed) @@ -42,10 +42,9 @@ def set_seed(self, seed: int) -> None: self.count = 0 self._draw_bounds = [] - def set_bound(self, bound: int) -> None: + def set_bound(self, bound: int | None) -> None: """Setting the current bound for generating random numbers.""" - self._require_non_negative("bound", bound) - self.current_bound = bound + self.current_bound = self._normalize_bound(bound) @staticmethod def _require_non_negative(name: str, value: int) -> None: @@ -54,6 +53,14 @@ def _require_non_negative(name: str, value: int) -> None: error_msg = f"RNG {name} must be non-negative: got {value}" raise ValueError(error_msg) + @classmethod + def _normalize_bound(cls, bound: int | None) -> int: + """Normalize optional bounds to the unbounded sentinel used by the RNG model.""" + if bound is None: + return 0 + cls._require_non_negative("bound", bound) + return bound + def rng_random(self) -> int: """Generating a random number and keeping track of how many we have generated.""" rng_num = self.pcg.random() if self.current_bound == 0 else self.pcg.boundedrand(self.current_bound) diff --git a/python/quantum-pecos/tests/pecos/unit/test_rng.py b/python/quantum-pecos/tests/pecos/unit/test_rng.py index 7cd0a049f..04cc04176 100644 --- a/python/quantum-pecos/tests/pecos/unit/test_rng.py +++ b/python/quantum-pecos/tests/pecos/unit/test_rng.py @@ -24,6 +24,12 @@ def test_set_seed() -> None: assert rng.count == 0 +def test_init_normalizes_none_bound_to_unbounded() -> None: + """Verifies ``None`` uses the unbounded sentinel instead of leaking into bounded draws.""" + rng = RNGModel(shot_id=0, current_bound=None) + assert rng.current_bound == 0 + + def test_random_number() -> None: """Verifies that the random number generated is an int type.""" rng = RNGModel(shot_id=0) From fad5d1f0aa8195d0e2b728fabb49841f375b6059 Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Wed, 24 Jun 2026 17:01:04 -0500 Subject: [PATCH 08/12] style: satisfy ruff control flow lint --- python/quantum-pecos/src/pecos/engines/cvm/rng_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py b/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py index e919f455f..f37db739b 100644 --- a/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py +++ b/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py @@ -122,7 +122,7 @@ def extract_val(self, param: str | int, output: dict) -> int: creg = output[idx_creg[0]] idx = int(idx_creg[-1][:-1]) return int(creg[idx]) - elif param == "JOB_shotnum": + if param == "JOB_shotnum": return self.shot_id reg = output[param] From c2c1f2417675f49cc0852dd543d93cfd9888ccc0 Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Wed, 24 Jun 2026 17:14:55 -0500 Subject: [PATCH 09/12] ci: use git cli for cargo fetches in python workflow --- .github/workflows/python-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 75496a29e..ac4af0e36 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -9,6 +9,7 @@ env: RUST_BACKTRACE: 1 PYTHONUTF8: 1 CARGO_HTTP_MULTIPLEXING: "false" + CARGO_NET_GIT_FETCH_WITH_CLI: "true" CARGO_NET_RETRY: "10" LLVM_VERSION: "14.0.6" # Force the MWPF decoder feature on in CI so we exercise the cmake-dependent From 4d4fb85df86045e528d1dcba1602e485af0a45bb Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Wed, 24 Jun 2026 17:25:54 -0500 Subject: [PATCH 10/12] ci: harden cargo network settings for release workflows --- .github/workflows/julia-release.yml | 3 +++ .github/workflows/python-release.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/julia-release.yml b/.github/workflows/julia-release.yml index 113a9548e..0985da6ff 100644 --- a/.github/workflows/julia-release.yml +++ b/.github/workflows/julia-release.yml @@ -16,6 +16,9 @@ permissions: env: TRIGGER_ON_PR_PUSH: true # Set to true to enable triggers on PR pushes + CARGO_HTTP_MULTIPLEXING: "false" + CARGO_NET_GIT_FETCH_WITH_CLI: "true" + CARGO_NET_RETRY: "10" on: push: diff --git a/.github/workflows/python-release.yml b/.github/workflows/python-release.yml index 34ce638ec..6615d6f60 100644 --- a/.github/workflows/python-release.yml +++ b/.github/workflows/python-release.yml @@ -5,6 +5,9 @@ permissions: env: TRIGGER_ON_PR_PUSH: true # Set to true to enable triggers on PR pushes + CARGO_HTTP_MULTIPLEXING: "false" + CARGO_NET_GIT_FETCH_WITH_CLI: "true" + CARGO_NET_RETRY: "10" # Must match pecos_build::cmake::CMAKE_VERSION. The MWPF decoder feature # uses highs-sys, which needs cmake; the wheel build installs this version # via `pecos install cmake` and points highs-sys's cmake-rs at it via $CMAKE. From 9d8cc9ce8f7dde330ae880f2a4770bec106211ef Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Wed, 24 Jun 2026 18:19:47 -0500 Subject: [PATCH 11/12] fix: address RNG rewind review feedback --- .../src/pecos/engines/cvm/rng_model.py | 26 ++++++++++++------- .../tests/pecos/unit/test_rng.py | 13 ++++++++++ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py b/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py index f37db739b..ed126a9de 100644 --- a/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py +++ b/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py @@ -26,7 +26,7 @@ def __init__( self.shot_id = shot_id self.current_bound = self._normalize_bound(current_bound) self.count = 0 - self._draw_bounds: list[int] = [] + self._draw_bound_runs: list[tuple[int, int]] = [] self.pcg = RngPcg() self.set_seed(seed) @@ -40,7 +40,7 @@ def set_seed(self, seed: int) -> None: self.seed = seed self.pcg.srandom(seed) self.count = 0 - self._draw_bounds = [] + self._draw_bound_runs = [] def set_bound(self, bound: int | None) -> None: """Setting the current bound for generating random numbers.""" @@ -64,7 +64,11 @@ def _normalize_bound(cls, bound: int | None) -> int: def rng_random(self) -> int: """Generating a random number and keeping track of how many we have generated.""" rng_num = self.pcg.random() if self.current_bound == 0 else self.pcg.boundedrand(self.current_bound) - self._draw_bounds.append(self.current_bound) + if self._draw_bound_runs and self._draw_bound_runs[-1][0] == self.current_bound: + bound, run_length = self._draw_bound_runs[-1] + self._draw_bound_runs[-1] = (bound, run_length + 1) + else: + self._draw_bound_runs.append((self.current_bound, 1)) self.count += 1 return rng_num @@ -91,16 +95,18 @@ def set_relative_index(self, delta: int) -> None: raise ValueError(error_msg) if delta < 0: - prefix_bounds = self._draw_bounds[:target_index] + bound_runs = list(self._draw_bound_runs) active_bound = self.current_bound - self.pcg = RngPcg() - self.pcg.srandom(self.seed) - self.count = 0 - self._draw_bounds = [] + self.set_seed(self.seed) - for historical_bound in prefix_bounds: + remaining = target_index + for historical_bound, run_length in bound_runs: + if remaining == 0: + break self.current_bound = historical_bound - self.rng_random() + for _ in range(min(run_length, remaining)): + self.rng_random() + remaining -= run_length self.current_bound = active_bound else: diff --git a/python/quantum-pecos/tests/pecos/unit/test_rng.py b/python/quantum-pecos/tests/pecos/unit/test_rng.py index 04cc04176..3a857fa42 100644 --- a/python/quantum-pecos/tests/pecos/unit/test_rng.py +++ b/python/quantum-pecos/tests/pecos/unit/test_rng.py @@ -95,6 +95,19 @@ def test_relative_advance_backward() -> None: assert rng.rng_random() == draw_at_index(42, 3) +def test_relative_advance_backward_past_start_raises() -> None: + """Verifies rewinds cannot move before the start of the stream.""" + rng = RNGModel(shot_id=0) + rng.set_seed(42) + rng.set_relative_index(2) + + with pytest.raises( + ValueError, + match=r"RNGadvance\(-3\) cannot move before the start of the stream: current stream index is 2", + ): + rng.set_relative_index(-3) + + def test_reseed_then_advance_changes_draw_and_resets_count() -> None: """Verifies reseeding resets the logical position used by later relative advances.""" rng = RNGModel(shot_id=0) From c1a612bc5e65f3ed1bd0042960eed5d013a075b3 Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Thu, 25 Jun 2026 11:15:56 -0500 Subject: [PATCH 12/12] fix: preserve shot-local RNG rewind state --- .../pecos-rslib/src/pecos_random_bindings.rs | 5 +++++ .../src/pecos/engines/cvm/rng_model.py | 20 +++++++++++++++---- .../src/pecos/engines/hybrid_engine_old.py | 3 +-- .../tests/pecos/unit/test_rng.py | 17 ++++++++++++++++ 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/python/pecos-rslib/src/pecos_random_bindings.rs b/python/pecos-rslib/src/pecos_random_bindings.rs index 29ba1f923..3442ddb21 100644 --- a/python/pecos-rslib/src/pecos_random_bindings.rs +++ b/python/pecos-rslib/src/pecos_random_bindings.rs @@ -40,6 +40,11 @@ impl RngPcg { pub fn srandom(&mut self, seq: u64) { PCGRandom::pcg32_srandom_r(&mut self.global_state, 42_u64, seq); } + + #[must_use] + pub fn clone(&self) -> RngPcg { + *self + } } #[cfg(test)] diff --git a/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py b/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py index ed126a9de..ba06542b3 100644 --- a/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py +++ b/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py @@ -28,6 +28,7 @@ def __init__( self.count = 0 self._draw_bound_runs: list[tuple[int, int]] = [] self.pcg = RngPcg() + self._replay_base_pcg = self.pcg.clone() self.set_seed(seed) def __str__(self) -> str: @@ -41,6 +42,14 @@ def set_seed(self, seed: int) -> None: self.pcg.srandom(seed) self.count = 0 self._draw_bound_runs = [] + self._replay_base_pcg = self.pcg.clone() + + def start_shot(self, shot_id: int) -> None: + """Reset shot-local replay state while preserving the current stream position.""" + self.shot_id = shot_id + self.count = 0 + self._draw_bound_runs = [] + self._replay_base_pcg = self.pcg.clone() def set_bound(self, bound: int | None) -> None: """Setting the current bound for generating random numbers.""" @@ -97,16 +106,19 @@ def set_relative_index(self, delta: int) -> None: if delta < 0: bound_runs = list(self._draw_bound_runs) active_bound = self.current_bound - self.set_seed(self.seed) + self.pcg = self._replay_base_pcg.clone() + self.count = 0 + self._draw_bound_runs = [] remaining = target_index for historical_bound, run_length in bound_runs: - if remaining == 0: + if remaining <= 0: break self.current_bound = historical_bound - for _ in range(min(run_length, remaining)): + replay_count = min(run_length, remaining) + for _ in range(replay_count): self.rng_random() - remaining -= run_length + remaining -= replay_count self.current_bound = active_bound else: diff --git a/python/quantum-pecos/src/pecos/engines/hybrid_engine_old.py b/python/quantum-pecos/src/pecos/engines/hybrid_engine_old.py index 97d5c42bf..322634093 100644 --- a/python/quantum-pecos/src/pecos/engines/hybrid_engine_old.py +++ b/python/quantum-pecos/src/pecos/engines/hybrid_engine_old.py @@ -141,8 +141,7 @@ def run( # -------------------- self.generate_errors = True error_circuits = error_gen.start(circuit, error_params) - self.rng_model.count = 0 - self.rng_model.shot_id = shot_id + self.rng_model.start_shot(shot_id) # run through the circuits... # --------------------------- diff --git a/python/quantum-pecos/tests/pecos/unit/test_rng.py b/python/quantum-pecos/tests/pecos/unit/test_rng.py index 3a857fa42..a3d4d9663 100644 --- a/python/quantum-pecos/tests/pecos/unit/test_rng.py +++ b/python/quantum-pecos/tests/pecos/unit/test_rng.py @@ -169,6 +169,23 @@ def test_relative_advance_backward_replays_historical_bounds() -> None: assert rng.rng_random() == expected_second_draw +def test_start_shot_resets_replay_base_to_current_stream_position() -> None: + """Verifies rewinds in later shots replay from the shot start, not the original seed.""" + rng = RNGModel(shot_id=0) + rng.set_seed(42) + + for _ in range(4): + rng.rng_random() + + rng.start_shot(shot_id=1) + shot_draws = [rng.rng_random() for _ in range(3)] + + rng.set_relative_index(-1) + + assert rng.count == 2 + assert rng.rng_random() == shot_draws[-1] + + def test_multiple_bounded_rand() -> None: """For several randomly generated number, with a random bound, verifies that its appropriate.""" rng = RNGModel(shot_id=0)