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
3 changes: 3 additions & 0 deletions .github/workflows/julia-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/python-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/python-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ env:
RUSTFLAGS: -C debuginfo=0
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
# build path and catch regressions. GitHub-hosted runners ship cmake.
Expand Down
12 changes: 9 additions & 3 deletions julia/PECOS.jl/src/Decoder.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 6 additions & 2 deletions julia/PECOS.jl/src/Simulator.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions python/pecos-rslib/src/pecos_random_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
105 changes: 90 additions & 15 deletions python/quantum-pecos/src/pecos/engines/cvm/rng_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,60 @@ 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_bound_runs: list[tuple[int, int]] = []
self.pcg = RngPcg()
Comment thread
qartik marked this conversation as resolved.
self.seed = self.set_seed(seed)
self._replay_base_pcg = self.pcg.clone()
self.set_seed(seed)

def __str__(self) -> str:
"""Returns the str representation of the model."""
return f"RNG Model bounded by {self.current_bound} with current count {self.count}"

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_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:
def set_bound(self, bound: int | None) -> None:
"""Setting the current bound for generating random numbers."""
self.current_bound = bound
self.current_bound = self._normalize_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)

@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)
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

Expand All @@ -53,27 +86,65 @@ 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)
while self.count < index:
self.rng_random()

def extract_val(self, param: str, output: dict) -> int:
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)
Comment thread
qartik marked this conversation as resolved.

if delta < 0:
bound_runs = list(self._draw_bound_runs)
active_bound = self.current_bound
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:
break
self.current_bound = historical_bound
replay_count = min(run_length, remaining)
for _ in range(replay_count):
self.rng_random()
remaining -= replay_count

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."""
if param.isdigit():
val = int(param)
elif "[" in param:
if isinstance(param, int):
return param

try:
return int(param)
except (TypeError, ValueError):
pass

if "[" in param:
idx_creg = param.split("[")
creg = output[idx_creg[0]]
idx = int(idx_creg[-1][:-1])
val = int(creg[idx])
elif param == "JOB_shotnum":
val = self.shot_id
else:
reg = output[param]
val = int(reg)
return val
return int(creg[idx])
if param == "JOB_shotnum":
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."""
Expand All @@ -90,6 +161,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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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...
# ---------------------------
Expand Down
130 changes: 130 additions & 0 deletions python/quantum-pecos/tests/pecos/unit/test_rng.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,31 @@
import sys

import pecos as pc
import pytest
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_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:
Expand Down Expand Up @@ -56,6 +72,120 @@ 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_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)
rng.set_seed(42)
rng.set_relative_index(5)
advanced_val = rng.rng_random()

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 == expected_first_draw
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_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_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)
Expand Down
Loading