From b655293434e6ba48ab03de2be7f93752a3817164 Mon Sep 17 00:00:00 2001 From: Advayth Pashupati <113481915+AamindMandragora@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:37:34 -0500 Subject: [PATCH 1/5] i need to fix this --- cirq-aqt/cirq_aqt/aqt_sampler.py | 6 ++- .../custom_state_simulator.py | 1 + cirq-core/cirq/contrib/quimb/mps_simulator.py | 14 +++-- .../random_quantum_circuit_generation.py | 29 +++++++---- .../random_quantum_circuit_generation_test.py | 4 +- .../single_qubit_readout_calibration_test.py | 6 ++- cirq-core/cirq/linalg/decompositions_test.py | 3 +- cirq-core/cirq/qis/clifford_tableau.py | 4 +- cirq-core/cirq/sim/classical_simulator.py | 1 + .../cirq/sim/clifford/clifford_simulator.py | 5 +- .../clifford_tableau_simulation_state.py | 2 +- .../stabilizer_ch_form_simulation_state.py | 2 +- .../cirq/sim/clifford/stabilizer_sampler.py | 23 ++++++-- .../clifford/stabilizer_simulation_state.py | 2 +- .../sim/clifford/stabilizer_state_ch_form.py | 4 +- .../sim/density_matrix_simulation_state.py | 2 +- .../cirq/sim/density_matrix_simulator.py | 3 +- cirq-core/cirq/sim/simulation_state.py | 4 +- cirq-core/cirq/sim/simulator.py | 42 +++++++++++---- cirq-core/cirq/sim/simulator_base.py | 35 +++++++++---- cirq-core/cirq/sim/simulator_base_test.py | 2 + cirq-core/cirq/sim/simulator_test.py | 2 + cirq-core/cirq/sim/sparse_simulator.py | 3 +- .../cirq/sim/state_vector_simulation_state.py | 2 +- cirq-core/cirq/testing/lin_alg_utils.py | 17 +++--- cirq-core/cirq/testing/random_circuit.py | 9 ++-- .../gate_tabulation_math_utils.py | 10 ++-- cirq-core/cirq/value/random_state.py | 52 ++++++++++++++++++- cirq-core/cirq/value/random_state_test.py | 10 ++-- cirq-core/cirq/work/sampler.py | 51 ++++++++++++++---- cirq-core/cirq/work/sampler_test.py | 27 +++++++--- cirq-core/cirq/work/zeros_sampler.py | 6 ++- .../cirq_google/engine/processor_sampler.py | 10 +++- .../cirq_google/engine/validating_sampler.py | 15 ++++-- cirq-ionq/cirq_ionq/sampler.py | 8 ++- cirq-pasqal/cirq_pasqal/pasqal_sampler.py | 10 +++- 36 files changed, 323 insertions(+), 103 deletions(-) diff --git a/cirq-aqt/cirq_aqt/aqt_sampler.py b/cirq-aqt/cirq_aqt/aqt_sampler.py index 50886084450..b0dda9a10a4 100644 --- a/cirq-aqt/cirq_aqt/aqt_sampler.py +++ b/cirq-aqt/cirq_aqt/aqt_sampler.py @@ -405,7 +405,11 @@ def _send_json( return measurements def run_sweep( - self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 + self, + program: cirq.AbstractCircuit, + params: cirq.Sweepable, + repetitions: int = 1, + prng: np.random.Generator | None = None, ) -> Sequence[cirq.Result]: """Samples from the given Circuit. diff --git a/cirq-core/cirq/contrib/custom_simulators/custom_state_simulator.py b/cirq-core/cirq/contrib/custom_simulators/custom_state_simulator.py index ef4fdfe1534..78a54ae1bdb 100644 --- a/cirq-core/cirq/contrib/custom_simulators/custom_state_simulator.py +++ b/cirq-core/cirq/contrib/custom_simulators/custom_state_simulator.py @@ -84,6 +84,7 @@ def _create_partial_simulation_state( initial_state: Any, qubits: Sequence[cirq.Qid], classical_data: cirq.ClassicalDataStore, + prng: np.random.Generator | None = None, ) -> TSimulationState: return self.state_type( initial_state=initial_state, qubits=qubits, classical_data=classical_data diff --git a/cirq-core/cirq/contrib/quimb/mps_simulator.py b/cirq-core/cirq/contrib/quimb/mps_simulator.py index 8dadd5d43ba..eadd677b334 100644 --- a/cirq-core/cirq/contrib/quimb/mps_simulator.py +++ b/cirq-core/cirq/contrib/quimb/mps_simulator.py @@ -90,6 +90,7 @@ def _create_partial_simulation_state( initial_state: int | MPSState, qubits: Sequence[cirq.Qid], classical_data: cirq.ClassicalDataStore, + prng: np.random.Generator | None = None, ) -> MPSState: """Creates MPSState args for simulating the Circuit. @@ -110,7 +111,7 @@ def _create_partial_simulation_state( return MPSState( qubits=qubits, - prng=self._prng, + prng=prng if prng is not None else self._prng, simulation_options=self.simulation_options, grouping=self.grouping, initial_state=initial_state, @@ -382,7 +383,9 @@ def to_numpy(self) -> np.ndarray: """An alias for the state vector.""" return self.state_vector() - def apply_op(self, op: Any, axes: Sequence[int], prng: np.random.RandomState): + def apply_op( + self, op: Any, axes: Sequence[int], prng: np.random.RandomState | np.random.Generator + ): """Applies a unitary operation, mutating the object to represent the new state. op: @@ -484,7 +487,10 @@ def estimation_stats(self): # pragma: no cover } def _measure( - self, axes: Sequence[int], prng: np.random.RandomState, collapse_state_vector=True + self, + axes: Sequence[int], + prng: np.random.RandomState | np.random.Generator, + collapse_state_vector=True, ) -> list[int]: results: list[int] = [] @@ -565,7 +571,7 @@ def __init__( self, *, qubits: Sequence[cirq.Qid], - prng: np.random.RandomState, + prng: np.random.RandomState | np.random.Generator, simulation_options: MPSOptions = MPSOptions(), grouping: dict[cirq.Qid, int] | None = None, initial_state: int = 0, diff --git a/cirq-core/cirq/experiments/random_quantum_circuit_generation.py b/cirq-core/cirq/experiments/random_quantum_circuit_generation.py index 47dcdb45c02..f09796cb242 100644 --- a/cirq-core/cirq/experiments/random_quantum_circuit_generation.py +++ b/cirq-core/cirq/experiments/random_quantum_circuit_generation.py @@ -24,6 +24,7 @@ from cirq import circuits, devices, ops, protocols, value from cirq._doc import document +from cirq.value import random_state as rs if TYPE_CHECKING: import networkx as nx @@ -173,7 +174,7 @@ def random_rotations_between_two_qubit_circuit( q1: cirq.Qid, depth: int, two_qubit_op_factory: Callable[ - [cirq.Qid, cirq.Qid, np.random.RandomState], cirq.OP_TREE + [cirq.Qid, cirq.Qid, np.random.RandomState | np.random.Generator], cirq.OP_TREE ] = lambda a, b, _: ops.CZPowGate()(a, b), single_qubit_gates: Sequence[cirq.Gate] = ( ops.X**0.5, @@ -339,11 +340,13 @@ def _get_random_combinations( returned list can be provided to `sample_2q_xeb_circuits` to efficiently sample parallel XEB circuits. """ - rs = value.parse_random_state(random_state) + parsed_rs = value.parse_random_state(random_state) combinations_by_layer = [] for pairs, layer in pair_gen: - combinations = rs.randint(0, n_library_circuits, size=(n_combinations, len(pairs))) + combinations = rs.get_random_int( + parsed_rs, 0, n_library_circuits, size=(n_combinations, len(pairs)) + ) combinations_by_layer.append( CircuitLibraryCombination(layer=layer, combinations=combinations, pairs=pairs) ) @@ -541,7 +544,7 @@ def random_rotations_between_grid_interaction_layers_circuit( *, # forces keyword arguments device_graph: nx.Graph | None = None, two_qubit_op_factory: Callable[ - [cirq.GridQubit, cirq.GridQubit, np.random.RandomState], cirq.OP_TREE + [cirq.GridQubit, cirq.GridQubit, np.random.RandomState | np.random.Generator], cirq.OP_TREE ] = lambda a, b, _: ops.CZPowGate()(a, b), pattern: Sequence[GridInteractionLayer] = GRID_STAGGERED_PATTERN, single_qubit_gates: Sequence[cirq.Gate] = ( @@ -642,7 +645,7 @@ def __init__( self, qubits: Sequence[cirq.Qid], single_qubit_gates: Sequence[cirq.Gate], - prng: np.random.RandomState, + prng: np.random.RandomState | np.random.Generator, ) -> None: self.qubits = qubits self.single_qubit_gates = single_qubit_gates @@ -652,9 +655,13 @@ def new_layer(self, previous_single_qubit_layer: cirq.Moment) -> cirq.Moment: def random_gate(qubit: cirq.Qid) -> cirq.Gate: excluded_op = previous_single_qubit_layer.operation_at(qubit) excluded_gate = excluded_op.gate if excluded_op is not None else None - g = self.single_qubit_gates[self.prng.randint(0, len(self.single_qubit_gates))] + g = self.single_qubit_gates[ + rs.get_random_int(self.prng, 0, len(self.single_qubit_gates)) + ] while g is excluded_gate: - g = self.single_qubit_gates[self.prng.randint(0, len(self.single_qubit_gates))] + g = self.single_qubit_gates[ + rs.get_random_int(self.prng, 0, len(self.single_qubit_gates)) + ] return g return circuits.Moment(random_gate(q).on(q) for q in self.qubits) @@ -672,7 +679,9 @@ def new_layer(self, previous_single_qubit_layer: cirq.Moment) -> cirq.Moment: def _single_qubit_gates_arg_to_factory( - single_qubit_gates: Sequence[cirq.Gate], qubits: Sequence[cirq.Qid], prng: np.random.RandomState + single_qubit_gates: Sequence[cirq.Gate], + qubits: Sequence[cirq.Qid], + prng: np.random.RandomState | np.random.Generator, ) -> _SingleQubitLayerFactory: """Parse the `single_qubit_gates` argument for circuit generation functions. @@ -689,10 +698,10 @@ def _single_qubit_gates_arg_to_factory( def _two_qubit_layer( coupled_qubit_pairs: list[GridQubitPairT], two_qubit_op_factory: Callable[ - [cirq.GridQubit, cirq.GridQubit, np.random.RandomState], cirq.OP_TREE + [cirq.GridQubit, cirq.GridQubit, np.random.RandomState | np.random.Generator], cirq.OP_TREE ], layer: GridInteractionLayer, - prng: np.random.RandomState, + prng: np.random.RandomState | np.random.Generator, ) -> Iterator[cirq.OP_TREE]: for a, b in coupled_qubit_pairs: if (a, b) in layer or (b, a) in layer: diff --git a/cirq-core/cirq/experiments/random_quantum_circuit_generation_test.py b/cirq-core/cirq/experiments/random_quantum_circuit_generation_test.py index 025bff3ab79..5f81a2f9ee4 100644 --- a/cirq-core/cirq/experiments/random_quantum_circuit_generation_test.py +++ b/cirq-core/cirq/experiments/random_quantum_circuit_generation_test.py @@ -247,7 +247,7 @@ def test_random_combinations_layer_circuit_vs_device() -> None: def _cz_with_adjacent_z_rotations( - a: cirq.GridQubit, b: cirq.GridQubit, prng: np.random.RandomState + a: cirq.GridQubit, b: cirq.GridQubit, prng: np.random.RandomState | np.random.Generator ): z_exponents = [prng.uniform(0, 1) for _ in range(4)] yield cirq.Z(a) ** z_exponents[0] @@ -370,7 +370,7 @@ def test_random_rotations_between_grid_interaction_layers( qubits: Iterable[cirq.GridQubit], depth: int, two_qubit_op_factory: Callable[ - [cirq.GridQubit, cirq.GridQubit, np.random.RandomState], cirq.OP_TREE + [cirq.GridQubit, cirq.GridQubit, np.random.RandomState | np.random.Generator], cirq.OP_TREE ], pattern: Sequence[GridInteractionLayer], single_qubit_gates: Sequence[cirq.Gate], diff --git a/cirq-core/cirq/experiments/single_qubit_readout_calibration_test.py b/cirq-core/cirq/experiments/single_qubit_readout_calibration_test.py index 718b6882146..60d392aa891 100644 --- a/cirq-core/cirq/experiments/single_qubit_readout_calibration_test.py +++ b/cirq-core/cirq/experiments/single_qubit_readout_calibration_test.py @@ -47,7 +47,11 @@ def __init__(self, p0: float, p1: float, seed: cirq.RANDOM_STATE_OR_SEED_LIKE = self.simulator = cirq.Simulator(seed=self.prng, split_untangled_states=False) def run_sweep( - self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 + self, + program: cirq.AbstractCircuit, + params: cirq.Sweepable, + repetitions: int = 1, + prng: np.random.Generator | None = None, ) -> Sequence[cirq.Result]: results = self.simulator.run_sweep(program, params, repetitions) for result in results: diff --git a/cirq-core/cirq/linalg/decompositions_test.py b/cirq-core/cirq/linalg/decompositions_test.py index 179b1586997..59db7376cc8 100644 --- a/cirq-core/cirq/linalg/decompositions_test.py +++ b/cirq-core/cirq/linalg/decompositions_test.py @@ -22,6 +22,7 @@ import cirq from cirq import unitary_eig, value from cirq.linalg.decompositions import MAGIC, MAGIC_CONJ_T +from cirq.value import random_state as rs X = np.array([[0, 1], [1, 0]]) Y = np.array([[0, -1j], [1j, 0]]) @@ -590,7 +591,7 @@ def _random_two_qubit_unitaries(num_samples: int, random_state: cirq.RANDOM_STAT prng = value.parse_random_state(random_state) # Generate the non-local part by explicit matrix exponentiation. - kak_vecs = prng.rand(num_samples, 3) * np.pi + kak_vecs = rs.get_random_array(prng, (num_samples, 3)) * np.pi gens = np.einsum('...a,abc->...bc', kak_vecs, _kak_gens) evals, evecs = np.linalg.eigh(gens) A = np.einsum('...ab,...b,...cb', evecs, np.exp(1j * evals), evecs.conj()) diff --git a/cirq-core/cirq/qis/clifford_tableau.py b/cirq-core/cirq/qis/clifford_tableau.py index ca26b51b9cb..de942d954e4 100644 --- a/cirq-core/cirq/qis/clifford_tableau.py +++ b/cirq-core/cirq/qis/clifford_tableau.py @@ -519,7 +519,7 @@ def destabilizers(self) -> list[cirq.DensePauliString]: generators above generate the full Pauli group on n qubits.""" return [self._row_to_dense_pauli(i) for i in range(self.n)] - def _measure(self, q, prng: np.random.RandomState) -> int: + def _measure(self, q, prng: np.random.RandomState | np.random.Generator) -> int: """Performs a projective measurement on the q'th qubit. Returns: the result (0 or 1) of the measurement. @@ -554,7 +554,7 @@ def _measure(self, q, prng: np.random.RandomState) -> int: self.zs[p, q] = True - self.rs[p] = bool(prng.randint(2)) + self.rs[p] = bool(random_state.get_random_int(prng, 2)) return int(self.rs[p]) diff --git a/cirq-core/cirq/sim/classical_simulator.py b/cirq-core/cirq/sim/classical_simulator.py index 02377c154b3..032d2bda218 100644 --- a/cirq-core/cirq/sim/classical_simulator.py +++ b/cirq-core/cirq/sim/classical_simulator.py @@ -252,6 +252,7 @@ def _create_partial_simulation_state( initial_state: Any, qubits: Sequence[cirq.Qid], classical_data: cirq.ClassicalDataStore, + prng: np.random.Generator | None = None, ) -> ClassicalBasisSimState: """Creates a partial simulation state for the simulator. diff --git a/cirq-core/cirq/sim/clifford/clifford_simulator.py b/cirq-core/cirq/sim/clifford/clifford_simulator.py index 42587a1b723..fce23b11911 100644 --- a/cirq-core/cirq/sim/clifford/clifford_simulator.py +++ b/cirq-core/cirq/sim/clifford/clifford_simulator.py @@ -76,6 +76,7 @@ def _create_partial_simulation_state( initial_state: int | cirq.StabilizerChFormSimulationState, qubits: Sequence[cirq.Qid], classical_data: cirq.ClassicalDataStore, + prng: np.random.Generator | None = None, ) -> cirq.StabilizerChFormSimulationState: """Creates the StabilizerChFormSimulationState for a circuit. @@ -97,7 +98,7 @@ def _create_partial_simulation_state( return initial_state # pragma: no cover return clifford.StabilizerChFormSimulationState( - prng=self._prng, + prng=prng if prng is not None else self._prng, classical_data=classical_data, qubits=qubits, initial_state=initial_state, @@ -257,7 +258,7 @@ def apply_measurement( self, op: cirq.Operation, measurements: dict[str, list[int]], - prng: np.random.RandomState, + prng: np.random.RandomState | np.random.Generator, collapse_state_vector=True, ) -> None: if not isinstance(op.gate, cirq.MeasurementGate): diff --git a/cirq-core/cirq/sim/clifford/clifford_tableau_simulation_state.py b/cirq-core/cirq/sim/clifford/clifford_tableau_simulation_state.py index 9dde849bfcb..f6e24f3988f 100644 --- a/cirq-core/cirq/sim/clifford/clifford_tableau_simulation_state.py +++ b/cirq-core/cirq/sim/clifford/clifford_tableau_simulation_state.py @@ -35,7 +35,7 @@ class CliffordTableauSimulationState(StabilizerSimulationState[clifford_tableau. def __init__( self, tableau: cirq.CliffordTableau, - prng: np.random.RandomState | None = None, + prng: np.random.RandomState | np.random.Generator | None = None, qubits: Sequence[cirq.Qid] | None = None, classical_data: cirq.ClassicalDataStore | None = None, ): diff --git a/cirq-core/cirq/sim/clifford/stabilizer_ch_form_simulation_state.py b/cirq-core/cirq/sim/clifford/stabilizer_ch_form_simulation_state.py index afb7c83307d..743c8a8504c 100644 --- a/cirq-core/cirq/sim/clifford/stabilizer_ch_form_simulation_state.py +++ b/cirq-core/cirq/sim/clifford/stabilizer_ch_form_simulation_state.py @@ -35,7 +35,7 @@ class StabilizerChFormSimulationState( def __init__( self, *, - prng: np.random.RandomState | None = None, + prng: np.random.RandomState | np.random.Generator | None = None, qubits: Sequence[cirq.Qid] | None = None, initial_state: int | cirq.StabilizerStateChForm = 0, classical_data: cirq.ClassicalDataStore | None = None, diff --git a/cirq-core/cirq/sim/clifford/stabilizer_sampler.py b/cirq-core/cirq/sim/clifford/stabilizer_sampler.py index dc84a8d2046..cca95a19244 100644 --- a/cirq-core/cirq/sim/clifford/stabilizer_sampler.py +++ b/cirq-core/cirq/sim/clifford/stabilizer_sampler.py @@ -38,16 +38,29 @@ def __init__(self, *, seed: cirq.RANDOM_STATE_OR_SEED_LIKE = None): self._prng = value.parse_random_state(seed) def run_sweep( - self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 + self, + program: cirq.AbstractCircuit, + params: cirq.Sweepable, + repetitions: int = 1, + prng: np.random.RandomState | np.random.Generator | None = None, ) -> Sequence[cirq.Result]: results: list[cirq.Result] = [] for param_resolver in cirq.to_resolvers(params): resolved_circuit = cirq.resolve_parameters(program, param_resolver) - measurements = self._run(resolved_circuit, repetitions=repetitions) + measurements = self._run( + resolved_circuit, + repetitions=repetitions, + prng=prng if prng is not None else self._prng, + ) results.append(cirq.ResultDict(params=param_resolver, measurements=measurements)) return results - def _run(self, circuit: cirq.AbstractCircuit, repetitions: int) -> dict[str, np.ndarray]: + def _run( + self, + circuit: cirq.AbstractCircuit, + repetitions: int, + prng: np.random.RandomState | np.random.Generator | None = None, + ) -> dict[str, np.ndarray]: measurements: dict[str, list[np.ndarray]] = { key: [] for key in protocols.measurement_key_names(circuit) @@ -56,7 +69,9 @@ def _run(self, circuit: cirq.AbstractCircuit, repetitions: int) -> dict[str, np. for _ in range(repetitions): state = CliffordTableauSimulationState( - CliffordTableau(num_qubits=len(qubits)), qubits=list(qubits), prng=self._prng + CliffordTableau(num_qubits=len(qubits)), + qubits=list(qubits), + prng=prng if prng is not None else self._prng, ) for op in circuit.all_operations(): protocols.act_on(op, state) diff --git a/cirq-core/cirq/sim/clifford/stabilizer_simulation_state.py b/cirq-core/cirq/sim/clifford/stabilizer_simulation_state.py index fd0aec71276..a1d8ca231d5 100644 --- a/cirq-core/cirq/sim/clifford/stabilizer_simulation_state.py +++ b/cirq-core/cirq/sim/clifford/stabilizer_simulation_state.py @@ -45,7 +45,7 @@ def __init__( self, *, state: TStabilizerState, - prng: np.random.RandomState | None = None, + prng: np.random.RandomState | np.random.Generator | None = None, qubits: Sequence[cirq.Qid] | None = None, classical_data: cirq.ClassicalDataStore | None = None, ): diff --git a/cirq-core/cirq/sim/clifford/stabilizer_state_ch_form.py b/cirq-core/cirq/sim/clifford/stabilizer_state_ch_form.py index 6e1a82b420b..609cafcb961 100644 --- a/cirq-core/cirq/sim/clifford/stabilizer_state_ch_form.py +++ b/cirq-core/cirq/sim/clifford/stabilizer_state_ch_form.py @@ -240,7 +240,7 @@ def to_state_vector(self) -> np.ndarray: return arr - def _measure(self, q, prng: np.random.RandomState) -> int: + def _measure(self, q, prng: np.random.RandomState | np.random.Generator) -> int: """Measures the q'th qubit. Reference: Section 4.1 "Simulating measurements" @@ -250,7 +250,7 @@ def _measure(self, q, prng: np.random.RandomState) -> int: w = self.s.copy() for i, v_i in enumerate(self.v): if v_i == 1: - w[i] = bool(prng.randint(2)) + w[i] = bool(random_state.get_random_int(prng, 2)) x_i = sum(w & self.G[q, :]) % 2 # Project the state to the above measurement outcome. self.project_Z(q, x_i) diff --git a/cirq-core/cirq/sim/density_matrix_simulation_state.py b/cirq-core/cirq/sim/density_matrix_simulation_state.py index d87adbc2e74..8c8200a224b 100644 --- a/cirq-core/cirq/sim/density_matrix_simulation_state.py +++ b/cirq-core/cirq/sim/density_matrix_simulation_state.py @@ -247,7 +247,7 @@ def __init__( self, *, available_buffer: list[np.ndarray] | None = None, - prng: np.random.RandomState | None = None, + prng: np.random.RandomState | np.random.Generator | None = None, qubits: Sequence[cirq.Qid] | None = None, initial_state: np.ndarray | cirq.STATE_VECTOR_LIKE = 0, dtype: type[np.complexfloating] = np.complex64, diff --git a/cirq-core/cirq/sim/density_matrix_simulator.py b/cirq-core/cirq/sim/density_matrix_simulator.py index a74dca30bf9..3dab0e36f96 100644 --- a/cirq-core/cirq/sim/density_matrix_simulator.py +++ b/cirq-core/cirq/sim/density_matrix_simulator.py @@ -155,6 +155,7 @@ def _create_partial_simulation_state( initial_state: np.ndarray | cirq.STATE_VECTOR_LIKE | cirq.DensityMatrixSimulationState, qubits: Sequence[cirq.Qid], classical_data: cirq.ClassicalDataStore, + prng: np.random.Generator | None = None, ) -> cirq.DensityMatrixSimulationState: """Creates the DensityMatrixSimulationState for a circuit. @@ -176,7 +177,7 @@ def _create_partial_simulation_state( return density_matrix_simulation_state.DensityMatrixSimulationState( qubits=qubits, - prng=self._prng, + prng=prng if prng is not None else self._prng, classical_data=classical_data, initial_state=initial_state, dtype=self._dtype, diff --git a/cirq-core/cirq/sim/simulation_state.py b/cirq-core/cirq/sim/simulation_state.py index 17e898f55e4..20b47bf0c29 100644 --- a/cirq-core/cirq/sim/simulation_state.py +++ b/cirq-core/cirq/sim/simulation_state.py @@ -39,7 +39,7 @@ def __init__( self, *, state: TState, - prng: np.random.RandomState | None = None, + prng: np.random.RandomState | np.random.Generator | None = None, qubits: Sequence[cirq.Qid] | None = None, classical_data: cirq.ClassicalDataStore | None = None, param_resolver: cirq.ParamResolver | None = None, @@ -69,7 +69,7 @@ def __init__( self._state = state @property - def prng(self) -> np.random.RandomState: + def prng(self) -> np.random.RandomState | np.random.Generator: return self._prng def measure( diff --git a/cirq-core/cirq/sim/simulator.py b/cirq-core/cirq/sim/simulator.py index 63639d7f9b7..e0cb7ada8eb 100644 --- a/cirq-core/cirq/sim/simulator.py +++ b/cirq-core/cirq/sim/simulator.py @@ -55,12 +55,20 @@ class SimulatesSamples(work.Sampler, metaclass=abc.ABCMeta): """ def run_sweep( - self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 + self, + program: cirq.AbstractCircuit, + params: cirq.Sweepable, + repetitions: int = 1, + prng: np.random.Generator | None = None, ) -> Sequence[cirq.Result]: - return list(self.run_sweep_iter(program, params, repetitions)) + return list(self.run_sweep_iter(program, params, repetitions, prng)) def run_sweep_iter( - self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 + self, + program: cirq.AbstractCircuit, + params: cirq.Sweepable, + repetitions: int = 1, + prng: np.random.Generator | None = None, ) -> Iterator[cirq.Result]: """Runs the supplied Circuit, mimicking quantum hardware. @@ -89,13 +97,20 @@ def run_sweep_iter( records[protocols.measurement_key_name(op)] = np.empty([0, 1, 1]) else: records = self._run( - circuit=program, param_resolver=param_resolver, repetitions=repetitions + circuit=program, + param_resolver=param_resolver, + repetitions=repetitions, + prng=prng, ) yield study.ResultDict(params=param_resolver, records=records) @abc.abstractmethod def _run( - self, circuit: cirq.AbstractCircuit, param_resolver: cirq.ParamResolver, repetitions: int + self, + circuit: cirq.AbstractCircuit, + param_resolver: cirq.ParamResolver, + repetitions: int, + prng: np.random.Generator | None = None, ) -> dict[str, np.ndarray]: """Run a simulation, mimicking quantum hardware. @@ -459,6 +474,7 @@ def simulate( param_resolver: cirq.ParamResolverOrSimilarType = None, qubit_order: cirq.QubitOrderOrList = ops.QubitOrder.DEFAULT, initial_state: Any = None, + prng: np.random.Generator | None = None, ) -> TSimulationTrialResult: """Simulates the supplied Circuit. @@ -479,7 +495,7 @@ def simulate( SimulationTrialResults for the simulation. Includes the final state. """ return self.simulate_sweep( - program, study.ParamResolver(param_resolver), qubit_order, initial_state + program, study.ParamResolver(param_resolver), qubit_order, initial_state, prng )[0] def simulate_sweep( @@ -488,12 +504,13 @@ def simulate_sweep( params: cirq.Sweepable, qubit_order: cirq.QubitOrderOrList = ops.QubitOrder.DEFAULT, initial_state: Any = None, + prng: np.random.Generator | None = None, ) -> list[TSimulationTrialResult]: """Wraps computed states in a list. Prefer overriding `simulate_sweep_iter`. """ - return list(self.simulate_sweep_iter(program, params, qubit_order, initial_state)) + return list(self.simulate_sweep_iter(program, params, qubit_order, initial_state, prng)) def _simulate_sweep_to_iter( self, @@ -501,10 +518,11 @@ def _simulate_sweep_to_iter( params: cirq.Sweepable, qubit_order: cirq.QubitOrderOrList = ops.QubitOrder.DEFAULT, initial_state: Any = None, + prng: np.random.Generator | None = None, ) -> Iterator[TSimulationTrialResult]: if type(self).simulate_sweep == SimulatesFinalState.simulate_sweep: raise RecursionError("Must define either simulate_sweep or simulate_sweep_iter.") - yield from self.simulate_sweep(program, params, qubit_order, initial_state) + yield from self.simulate_sweep(program, params, qubit_order, initial_state, prng) @value.alternative(requires='simulate_sweep', implementation=_simulate_sweep_to_iter) def simulate_sweep_iter( @@ -513,6 +531,7 @@ def simulate_sweep_iter( params: cirq.Sweepable, qubit_order: cirq.QubitOrderOrList = ops.QubitOrder.DEFAULT, initial_state: Any = None, + prng: np.random.Generator | None = None, ) -> Iterator[TSimulationTrialResult]: """Simulates the supplied Circuit. @@ -561,6 +580,7 @@ def simulate_sweep_iter( params: cirq.Sweepable, qubit_order: cirq.QubitOrderOrList = ops.QubitOrder.DEFAULT, initial_state: Any = None, + prng: np.random.Generator | None = None, ) -> Iterator[TSimulationTrialResult]: """Simulates the supplied Circuit. @@ -592,7 +612,7 @@ def simulate_sweep_iter( else initial_state ) all_step_results = self.simulate_moment_steps( - program, param_resolver, qubit_order, state + program, param_resolver, qubit_order, state, prng ) measurements: dict[str, np.ndarray] = {} for step_result in all_step_results: @@ -610,6 +630,7 @@ def simulate_moment_steps( param_resolver: cirq.ParamResolverOrSimilarType = None, qubit_order: cirq.QubitOrderOrList = ops.QubitOrder.DEFAULT, initial_state: Any = None, + prng: np.random.Generator | None = None, ) -> Iterator[TStepResult]: """Returns an iterator of StepResults for each moment simulated. @@ -635,7 +656,7 @@ def simulate_moment_steps( actual_initial_state = 0 if initial_state is None else initial_state qubits = ops.QubitOrder.as_qubit_order(qubit_order).order_for(circuit.all_qubits()) return self._base_iterator( - circuit, qubits, actual_initial_state, param_resolver=param_resolver + circuit, qubits, actual_initial_state, param_resolver=param_resolver, prng=prng ) @abc.abstractmethod @@ -645,6 +666,7 @@ def _base_iterator( qubits: tuple[cirq.Qid, ...], initial_state: Any, param_resolver: cirq.ParamResolver | None = None, + prng: np.random.Generator | None = None, ) -> Iterator[TStepResult]: """Iterator over StepResult from Moments of a Circuit. diff --git a/cirq-core/cirq/sim/simulator_base.py b/cirq-core/cirq/sim/simulator_base.py index 539150ef76a..540b1501847 100644 --- a/cirq-core/cirq/sim/simulator_base.py +++ b/cirq-core/cirq/sim/simulator_base.py @@ -110,6 +110,7 @@ def _create_partial_simulation_state( initial_state: Any, qubits: Sequence[cirq.Qid], classical_data: cirq.ClassicalDataStore, + prng: np.random.Generator | None = None, ) -> TSimulationState: """Creates an instance of the TSimulationState class for the simulator. @@ -165,9 +166,10 @@ def _base_iterator( qubits: tuple[cirq.Qid, ...], initial_state: Any, param_resolver: cirq.ParamResolver | None = None, + prng: np.random.Generator | None = None, ) -> Iterator[TStepResultBase]: sim_state = self._create_simulation_state( - initial_state, qubits, param_resolver=param_resolver + initial_state, qubits, param_resolver=param_resolver, prng=prng ) return self._core_iterator(circuit, sim_state) @@ -229,12 +231,18 @@ def _core_iterator( yield self._create_step_result(sim_state) def _run( - self, circuit: cirq.AbstractCircuit, param_resolver: cirq.ParamResolver, repetitions: int + self, + circuit: cirq.AbstractCircuit, + param_resolver: cirq.ParamResolver, + repetitions: int, + prng: np.random.Generator | None = None, ) -> dict[str, np.ndarray]: """See definition in `cirq.SimulatesSamples`.""" param_resolver = study.ParamResolver({}) if param_resolver is None else param_resolver qubits = tuple(sorted(circuit.all_qubits())) - sim_state = self._create_simulation_state(0, qubits, param_resolver=param_resolver) + sim_state = self._create_simulation_state( + 0, qubits, param_resolver=param_resolver, prng=prng + ) def can_run_prefix(op: cirq.Operation) -> bool: resolved_op = protocols.resolve_parameters(op, param_resolver) @@ -262,7 +270,10 @@ def can_run_prefix(op: cirq.Operation) -> bool: assert step_result is not None measurement_ops = [cast(ops.GateOperation, op) for op in general_ops] return step_result.sample_measurement_ops( - measurement_ops, repetitions, seed=self._prng, _allow_repeated=True + measurement_ops, + repetitions, + seed=prng if prng is not None else self._prng, + _allow_repeated=True, ) records: dict[cirq.MeasurementKey, list[Sequence[Sequence[int]]]] = {} @@ -298,6 +309,7 @@ def simulate_sweep_iter( params: cirq.Sweepable, qubit_order: cirq.QubitOrderOrList = ops.QubitOrder.DEFAULT, initial_state: Any = None, + prng: np.random.Generator | None = None, ) -> Iterator[TSimulationTrialResult]: """Simulates the supplied Circuit. @@ -326,7 +338,7 @@ def sweep_prefixable(op: cirq.Operation): qubits = ops.QubitOrder.as_qubit_order(qubit_order).order_for(program.all_qubits()) initial_state = 0 if initial_state is None else initial_state - sim_state = self._create_simulation_state(initial_state, qubits) + sim_state = self._create_simulation_state(initial_state, qubits, prng=prng) prefix, suffix = ( split_into_matching_protocol_then_general(program, sweep_prefixable) if self._can_be_in_run_prefix(self.noise) @@ -337,13 +349,14 @@ def sweep_prefixable(op: cirq.Operation): pass assert step_result is not None sim_state = step_result._sim_state - yield from super().simulate_sweep_iter(suffix, params, qubit_order, sim_state) + yield from super().simulate_sweep_iter(suffix, params, qubit_order, sim_state, prng) def _create_simulation_state( self, initial_state: Any, qubits: Sequence[cirq.Qid], param_resolver: cirq.ParamResolver | None = None, + prng: np.random.Generator | None = None, ) -> SimulationStateBase[TSimulationState]: if isinstance(initial_state, SimulationStateBase): if param_resolver is not None: @@ -359,15 +372,19 @@ def _create_simulation_state( initial_state=initial_state % q.dimension, qubits=[q], classical_data=classical_data, + prng=prng, ) initial_state = int(initial_state / q.dimension) else: args = self._create_partial_simulation_state( - initial_state=initial_state, qubits=qubits, classical_data=classical_data + initial_state=initial_state, + qubits=qubits, + classical_data=classical_data, + prng=prng, ) for q in qubits: args_map[q] = args - args_map[None] = self._create_partial_simulation_state(0, (), classical_data) + args_map[None] = self._create_partial_simulation_state(0, (), classical_data, prng) return SimulationProductState( args_map, qubits, @@ -377,7 +394,7 @@ def _create_simulation_state( ) else: state = self._create_partial_simulation_state( - initial_state=initial_state, qubits=qubits, classical_data=classical_data + initial_state=initial_state, qubits=qubits, classical_data=classical_data, prng=prng ) state.param_resolver = ( study.ParamResolver({}) if param_resolver is None else param_resolver diff --git a/cirq-core/cirq/sim/simulator_base_test.py b/cirq-core/cirq/sim/simulator_base_test.py index 00c89acbcb7..8e901cd644b 100644 --- a/cirq-core/cirq/sim/simulator_base_test.py +++ b/cirq-core/cirq/sim/simulator_base_test.py @@ -127,6 +127,7 @@ def _create_partial_simulation_state( initial_state: Any, qubits: Sequence[cirq.Qid], classical_data: cirq.ClassicalDataStore, + prng: np.random.Generator | None = None, ) -> CountingSimulationState: return CountingSimulationState( qubits=qubits, state=initial_state, classical_data=classical_data @@ -157,6 +158,7 @@ def _create_partial_simulation_state( initial_state: Any, qubits: Sequence[cirq.Qid], classical_data: cirq.ClassicalDataStore, + prng: np.random.Generator | None = None, ) -> CountingSimulationState: return SplittableCountingSimulationState( qubits=qubits, state=initial_state, classical_data=classical_data diff --git a/cirq-core/cirq/sim/simulator_test.py b/cirq-core/cirq/sim/simulator_test.py index eadb8f32f81..b28814d1924 100644 --- a/cirq-core/cirq/sim/simulator_test.py +++ b/cirq-core/cirq/sim/simulator_test.py @@ -80,6 +80,7 @@ def _base_iterator( qubits: tuple[cirq.Qid, ...], initial_state: Any, param_resolver: cirq.ParamResolver | None = None, + prng: np.random.Generator | None = None, ) -> Iterator[TStepResult]: raise NotImplementedError @@ -497,6 +498,7 @@ def simulate_sweep( params: study.Sweepable, qubit_order: cirq.QubitOrderOrList = cirq.QubitOrder.DEFAULT, initial_state: Any = None, + prng: np.random.Generator | None = None, ) -> list[SimulationTrialResult]: return [mock_trial_result] diff --git a/cirq-core/cirq/sim/sparse_simulator.py b/cirq-core/cirq/sim/sparse_simulator.py index de308739732..54f4d88630e 100644 --- a/cirq-core/cirq/sim/sparse_simulator.py +++ b/cirq-core/cirq/sim/sparse_simulator.py @@ -158,6 +158,7 @@ def _create_partial_simulation_state( initial_state: cirq.STATE_VECTOR_LIKE | cirq.StateVectorSimulationState, qubits: Sequence[cirq.Qid], classical_data: cirq.ClassicalDataStore, + prng: np.random.Generator | None = None, ): """Creates the StateVectorSimulationState for a circuit. @@ -179,7 +180,7 @@ def _create_partial_simulation_state( return state_vector_simulation_state.StateVectorSimulationState( qubits=qubits, - prng=self._prng, + prng=prng if prng is not None else self._prng, classical_data=classical_data, initial_state=initial_state, dtype=self._dtype, diff --git a/cirq-core/cirq/sim/state_vector_simulation_state.py b/cirq-core/cirq/sim/state_vector_simulation_state.py index d4619368e0b..66265c10ff8 100644 --- a/cirq-core/cirq/sim/state_vector_simulation_state.py +++ b/cirq-core/cirq/sim/state_vector_simulation_state.py @@ -322,7 +322,7 @@ def __init__( self, *, available_buffer: np.ndarray | None = None, - prng: np.random.RandomState | None = None, + prng: np.random.RandomState | np.random.Generator | None = None, qubits: Sequence[cirq.Qid] | None = None, initial_state: np.ndarray | cirq.STATE_VECTOR_LIKE = 0, dtype: type[np.complexfloating] | np.dtype[np.complexfloating] = np.complex64, diff --git a/cirq-core/cirq/testing/lin_alg_utils.py b/cirq-core/cirq/testing/lin_alg_utils.py index abe9f57b500..bb0609bf970 100644 --- a/cirq-core/cirq/testing/lin_alg_utils.py +++ b/cirq-core/cirq/testing/lin_alg_utils.py @@ -21,6 +21,7 @@ import numpy as np from cirq import linalg, value +from cirq.value import random_state as rs if TYPE_CHECKING: import cirq @@ -42,8 +43,8 @@ def random_superposition( """ random_state = value.parse_random_state(random_state) - state_vector = random_state.randn(dim).astype(complex) - state_vector += 1j * random_state.randn(dim) + state_vector = rs.get_random_normal_array(random_state, [dim]).astype(complex) + state_vector += 1j * rs.get_random_normal_array(random_state, [dim]) state_vector /= np.linalg.norm(state_vector) return state_vector @@ -66,7 +67,9 @@ def random_density_matrix( """ random_state = value.parse_random_state(random_state) - mat = random_state.randn(dim, dim) + 1j * random_state.randn(dim, dim) + mat = rs.get_random_normal_array(random_state, (dim, dim)) + 1j * rs.get_random_normal_array( + random_state, (dim, dim) + ) mat = mat @ mat.T.conj() return mat / np.trace(mat) @@ -87,7 +90,9 @@ def random_unitary(dim: int, *, random_state: cirq.RANDOM_STATE_OR_SEED_LIKE = N """ random_state = value.parse_random_state(random_state) - z = random_state.randn(dim, dim) + 1j * random_state.randn(dim, dim) + z = rs.get_random_normal_array(random_state, (dim, dim)) + 1j * rs.get_random_normal_array( + random_state, (dim, dim) + ) q, r = np.linalg.qr(z) d = np.diag(r) return q * (d / abs(d)) @@ -113,14 +118,14 @@ def random_orthogonal( """ random_state = value.parse_random_state(random_state) - m = random_state.randn(dim, dim) + m = rs.get_random_normal_array(random_state, (dim, dim)) q, r = np.linalg.qr(m) d = np.diag(r) return q * (d / abs(d)) def random_special_unitary( - dim: int, *, random_state: np.random.RandomState | None = None + dim: int, *, random_state: np.random.RandomState | np.random.Generator | None = None ) -> np.ndarray: """Returns a random special unitary distributed with Haar measure. diff --git a/cirq-core/cirq/testing/random_circuit.py b/cirq-core/cirq/testing/random_circuit.py index 37860288489..55a7759075e 100644 --- a/cirq-core/cirq/testing/random_circuit.py +++ b/cirq-core/cirq/testing/random_circuit.py @@ -19,6 +19,7 @@ from cirq import circuits, ops, value from cirq._doc import document +from cirq.value import random_state as rs if TYPE_CHECKING: import cirq @@ -115,10 +116,10 @@ def random_circuit( operations = [] free_qubits = set(qubits) while len(free_qubits) >= max_arity: - gate, arity = gate_arity_pairs[prng.randint(num_gates)] + gate, arity = gate_arity_pairs[rs.get_random_int(prng, num_gates)] op_qubits = prng.choice(sorted(free_qubits), size=arity, replace=False) free_qubits.difference_update(op_qubits) - if prng.rand() <= op_density: + if rs.get_random_array(prng) <= op_density: operations.append(gate(*op_qubits)) moments.append(circuits.Moment(operations)) @@ -149,7 +150,9 @@ def random_two_qubit_circuit_with_czs( q1 = ops.NamedQubit('q1') if q1 is None else q1 def random_one_qubit_gate(): - return ops.PhasedXPowGate(phase_exponent=prng.rand(), exponent=prng.rand()) + return ops.PhasedXPowGate( + phase_exponent=rs.get_random_array(prng), exponent=rs.get_random_array(prng) + ) def one_cz(): return [ops.CZ.on(q0, q1), random_one_qubit_gate().on(q0), random_one_qubit_gate().on(q1)] diff --git a/cirq-core/cirq/transformers/heuristic_decompositions/gate_tabulation_math_utils.py b/cirq-core/cirq/transformers/heuristic_decompositions/gate_tabulation_math_utils.py index 25698b503ca..d6e1777e60e 100644 --- a/cirq-core/cirq/transformers/heuristic_decompositions/gate_tabulation_math_utils.py +++ b/cirq-core/cirq/transformers/heuristic_decompositions/gate_tabulation_math_utils.py @@ -48,7 +48,7 @@ def _single_qubit_unitary( def random_qubit_unitary( shape: Sequence[int] = (), randomize_global_phase: bool = False, - rng: np.random.RandomState | None = None, + rng: np.random.RandomState | np.random.Generator | None = None, ) -> np.ndarray: """Random qubit unitary distributed over the Haar measure. @@ -65,15 +65,15 @@ def random_qubit_unitary( """ real_rng = random_state.parse_random_state(rng) - theta = np.arcsin(np.sqrt(real_rng.rand(*shape))) - phi_d = real_rng.rand(*shape) * np.pi * 2 - phi_o = real_rng.rand(*shape) * np.pi * 2 + theta = np.arcsin(np.sqrt(random_state.get_random_array(real_rng, shape))) + phi_d = random_state.get_random_array(real_rng, shape) * np.pi * 2 + phi_o = random_state.get_random_array(real_rng, shape) * np.pi * 2 out = _single_qubit_unitary(theta, phi_d, phi_o) if randomize_global_phase: out = np.moveaxis(out, (-2, -1), (0, 1)) - out *= np.exp(1j * np.pi * 2 * real_rng.rand(*shape)) + out *= np.exp(1j * np.pi * 2 * random_state.get_random_array(real_rng, shape)) out = np.moveaxis(out, (0, 1), (-2, -1)) return out diff --git a/cirq-core/cirq/value/random_state.py b/cirq-core/cirq/value/random_state.py index fe60ef1db94..3146a949e66 100644 --- a/cirq-core/cirq/value/random_state.py +++ b/cirq-core/cirq/value/random_state.py @@ -14,6 +14,7 @@ from __future__ import annotations +from collections.abc import Sequence from typing import Any, cast import numpy as np @@ -39,13 +40,16 @@ ) -def parse_random_state(random_state: RANDOM_STATE_OR_SEED_LIKE) -> np.random.RandomState: +def parse_random_state( + random_state: RANDOM_STATE_OR_SEED_LIKE, +) -> np.random.RandomState | np.random.Generator: """Interpret an object as a pseudorandom number generator. If `random_state` is None, returns the module `np.random`. If `random_state` is an integer, returns `np.random.RandomState(random_state)`. - Otherwise, returns `random_state` unmodified. + If `random_state` is an `np.random.Generator`, return it unmodified. + Otherwise, returns `random_state` cast to an `np.random.RandomState`. Args: random_state: The object to be used as or converted to a pseudorandom @@ -58,5 +62,49 @@ def parse_random_state(random_state: RANDOM_STATE_OR_SEED_LIKE) -> np.random.Ran return cast(np.random.RandomState, np.random) elif isinstance(random_state, int): return np.random.RandomState(random_state) + elif isinstance(random_state, np.random.Generator): + return random_state else: return cast(np.random.RandomState, random_state) + + +def get_random_array( + rng: np.random.RandomState | np.random.Generator, shape: Sequence[int] | None = None +): + if isinstance(rng, np.random.Generator): + if shape is not None: + return rng.random(size=shape) + else: + return rng.random() + else: + if shape is not None: + return rng.rand(*shape) + else: + return rng.rand() + + +def get_random_normal_array( + rng: np.random.RandomState | np.random.Generator, shape: Sequence[int] | None = None +): + if isinstance(rng, np.random.Generator): + if shape is not None: + return rng.standard_normal(size=shape) + else: + return rng.standard_normal() + else: + if shape is not None: + return rng.randn(*shape) + else: + return rng.randn() + + +def get_random_int( + rng: np.random.RandomState | np.random.Generator, + low: int, + high: int | None = None, + size: Sequence[int] | None = None, +): + if isinstance(rng, np.random.Generator): + return rng.integers(low, high, size=size) + else: + return rng.randint(low, high, size=size) diff --git a/cirq-core/cirq/value/random_state_test.py b/cirq-core/cirq/value/random_state_test.py index fd2f6745d23..87b9502f4e2 100644 --- a/cirq-core/cirq/value/random_state_test.py +++ b/cirq-core/cirq/value/random_state_test.py @@ -17,21 +17,19 @@ import numpy as np import cirq +from cirq.value import random_state def test_parse_random_state() -> None: global_state = np.random.get_state() - def rand(prng): - np.random.set_state(global_state) - return prng.rand() - prngs = [ np.random, cirq.value.parse_random_state(np.random), cirq.value.parse_random_state(None), ] - vals = [rand(prng) for prng in prngs] + + vals = [prng.rand() if (isinstance(prng, np.random.RandomState) or isinstance(prng, np.random)) else 0 for prng in prngs] eq = cirq.testing.EqualsTester() eq.add_equality_group(*vals) @@ -41,6 +39,6 @@ def rand(prng): cirq.value.parse_random_state(np.random.RandomState(seed)), cirq.value.parse_random_state(seed), ] - vals = [prng.rand() for prng in prngs1] + vals = [random_state.get_random_array(prng) for prng in prngs1] eq = cirq.testing.EqualsTester() eq.add_equality_group(*vals) diff --git a/cirq-core/cirq/work/sampler.py b/cirq-core/cirq/work/sampler.py index 0552bb90d12..a3e9feabadd 100644 --- a/cirq-core/cirq/work/sampler.py +++ b/cirq-core/cirq/work/sampler.py @@ -17,6 +17,7 @@ from __future__ import annotations import collections +import itertools from collections.abc import Sequence from typing import TYPE_CHECKING, TypeVar @@ -32,6 +33,8 @@ from cirq.work.observable_settings import _hashable_param if TYPE_CHECKING: + import numpy as np + import cirq T = TypeVar('T') @@ -45,6 +48,7 @@ def run( program: cirq.AbstractCircuit, param_resolver: cirq.ParamResolverOrSimilarType = None, repetitions: int = 1, + prng: np.random.Generator | None = None, ) -> cirq.Result: """Samples from the given `Circuit`. @@ -66,13 +70,14 @@ def run( Returns: `cirq.Result` that contains all the measurements for a run. """ - return self.run_sweep(program, param_resolver, repetitions)[0] + return self.run_sweep(program, param_resolver, repetitions, prng)[0] async def run_async( self, program: cirq.AbstractCircuit, param_resolver: cirq.ParamResolverOrSimilarType = None, repetitions: int = 1, + prng: np.random.Generator | None = None, ) -> cirq.Result: """Asynchronously samples from the given Circuit. @@ -88,11 +93,16 @@ async def run_async( Returns: Result for a run. """ - results = await self.run_sweep_async(program, param_resolver, repetitions) + results = await self.run_sweep_async(program, param_resolver, repetitions, prng) return results[0] def sample( - self, program: cirq.AbstractCircuit, *, repetitions: int = 1, params: cirq.Sweepable = None + self, + program: cirq.AbstractCircuit, + *, + repetitions: int = 1, + params: cirq.Sweepable = None, + prng: np.random.Generator | None = None, ) -> pd.DataFrame: """Samples the given Circuit, producing a pandas data frame. @@ -171,7 +181,9 @@ def sample( results = [] for sweep in sweeps_list: - sweep_results = self.run_sweep(program, params=sweep, repetitions=repetitions) + sweep_results = self.run_sweep( + program, params=sweep, repetitions=repetitions, prng=prng + ) for resolver, result in zip(sweep, sweep_results): param_values_once = [resolver.value_of(key) for key in keys] param_table = pd.DataFrame(data=[param_values_once] * repetitions, columns=keys) @@ -180,20 +192,32 @@ def sample( return pd.concat(results) def _run_sweep_impl( - self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 + self, + program: cirq.AbstractCircuit, + params: cirq.Sweepable, + repetitions: int = 1, + prng: np.random.Generator | None = None, ) -> Sequence[cirq.Result]: """Implements run_sweep using run_sweep_async""" - return duet.run(self.run_sweep_async, program, params, repetitions) + return duet.run(self.run_sweep_async, program, params, repetitions, prng) async def _run_sweep_async_impl( - self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 + self, + program: cirq.AbstractCircuit, + params: cirq.Sweepable, + repetitions: int = 1, + prng: np.random.Generator | None = None, ) -> Sequence[cirq.Result]: """Implements run_sweep_async using run_sweep""" - return self.run_sweep(program, params=params, repetitions=repetitions) + return self.run_sweep(program, params=params, repetitions=repetitions, prng=prng) @value.alternative(requires='run_sweep_async', implementation=_run_sweep_impl) def run_sweep( - self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 + self, + program: cirq.AbstractCircuit, + params: cirq.Sweepable, + repetitions: int = 1, + prng: np.random.Generator | None = None, ) -> Sequence[cirq.Result]: """Samples from the given Circuit. @@ -217,7 +241,11 @@ def run_sweep( @value.alternative(requires='run_sweep', implementation=_run_sweep_async_impl) async def run_sweep_async( - self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 + self, + program: cirq.AbstractCircuit, + params: cirq.Sweepable, + repetitions: int = 1, + prng: np.random.Generator | None = None, ) -> Sequence[cirq.Result]: """Asynchronously samples from the given Circuit. @@ -240,6 +268,7 @@ async def run_batch_async( programs: Sequence[cirq.AbstractCircuit], params_list: Sequence[cirq.Sweepable] | None = None, repetitions: int | Sequence[int] = 1, + prng: np.random.Generator | None = None, ) -> Sequence[Sequence[cirq.Result]]: """Runs the supplied circuits asynchronously. @@ -281,7 +310,7 @@ async def run_batch_async( """ params_list, repetitions = self._normalize_batch_args(programs, params_list, repetitions) return await duet.pstarmap_async( - self.run_sweep_async, zip(programs, params_list, repetitions) + self.run_sweep_async, zip(programs, params_list, repetitions, itertools.repeat(prng)) ) run_batch = duet.sync(run_batch_async) diff --git a/cirq-core/cirq/work/sampler_test.py b/cirq-core/cirq/work/sampler_test.py index 8bc6fbc9dd9..cc05eead143 100644 --- a/cirq-core/cirq/work/sampler_test.py +++ b/cirq-core/cirq/work/sampler_test.py @@ -52,7 +52,9 @@ async def test_run_sweep_async() -> None: @duet.sync async def test_sampler_async_fail() -> None: class FailingSampler(cirq.Sampler): - def run_sweep(self, program, params, repetitions: int = 1): + def run_sweep( + self, program, params, repetitions: int = 1, prng: np.random.Generator | None = None + ): raise ValueError('test') with pytest.raises(ValueError, match='test'): @@ -66,9 +68,11 @@ def test_run_sweep_impl() -> None: """Test run_sweep implemented in terms of run_sweep_async.""" class AsyncSampler(cirq.Sampler): - async def run_sweep_async(self, program, params, repetitions: int = 1): + async def run_sweep_async( + self, program, params, repetitions: int = 1, prng: np.random.Generator | None = None + ): await duet.sleep(0.001) - return cirq.Simulator().run_sweep(program, params, repetitions) + return cirq.Simulator().run_sweep(program, params, repetitions, prng) results = AsyncSampler().run_sweep( cirq.Circuit(cirq.measure(cirq.GridQubit(0, 0), key='m')), @@ -85,7 +89,9 @@ async def test_run_sweep_async_impl() -> None: """Test run_sweep_async implemented in terms of run_sweep.""" class SyncSampler(cirq.Sampler): - def run_sweep(self, program, params, repetitions: int = 1): + def run_sweep( + self, program, params, repetitions: int = 1, prng: np.random.Generator | None = None + ): return cirq.Simulator().run_sweep(program, params, repetitions) results = await SyncSampler().run_sweep_async( @@ -227,7 +233,12 @@ async def test_run_batch_async_calls_run_sweep_asynchronously() -> None: class AsyncSampler(cirq.Sampler): async def run_sweep_async( - self, program, params, repetitions: int = 1, unused: duet.Limiter = duet.Limiter(None) + self, + program, + params, + repetitions: int = 1, + prng: np.random.Generator | None = None, + unused: duet.Limiter = duet.Limiter(None), ): if params == params1: await duet.sleep(0.001) @@ -289,7 +300,11 @@ class DeterministicImbalancedStateSampler(cirq.Sampler): """ def run_sweep( - self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 + self, + program: cirq.AbstractCircuit, + params: cirq.Sweepable, + repetitions: int = 1, + prng: np.random.Generator | None = None, ) -> Sequence[cirq.Result]: results = np.zeros((repetitions, 1), dtype=bool) for idx in range(repetitions // 4): diff --git a/cirq-core/cirq/work/zeros_sampler.py b/cirq-core/cirq/work/zeros_sampler.py index cdd9da7944c..4e26a14a51c 100644 --- a/cirq-core/cirq/work/zeros_sampler.py +++ b/cirq-core/cirq/work/zeros_sampler.py @@ -38,7 +38,11 @@ def __init__(self, device: devices.Device | None = None): self.device = device def run_sweep( - self, program: cirq.AbstractCircuit, params: study.Sweepable, repetitions: int = 1 + self, + program: cirq.AbstractCircuit, + params: study.Sweepable, + repetitions: int = 1, + prng: np.random.Generator | None = None, ) -> list[study.Result]: """Samples circuit as if every measurement resulted in zero. diff --git a/cirq-google/cirq_google/engine/processor_sampler.py b/cirq-google/cirq_google/engine/processor_sampler.py index fda56162012..35018a4f07e 100644 --- a/cirq-google/cirq_google/engine/processor_sampler.py +++ b/cirq-google/cirq_google/engine/processor_sampler.py @@ -14,6 +14,7 @@ from __future__ import annotations +import itertools from collections.abc import Mapping, Sequence from typing import cast, TYPE_CHECKING @@ -22,6 +23,8 @@ import cirq if TYPE_CHECKING: + import numpy as np + import cirq_google as cg @@ -82,6 +85,7 @@ async def run_sweep_async( program: cirq.AbstractCircuit, params: cirq.Sweepable | Sequence[cirq.Sweepable], repetitions: int = 1, + prng: np.random.Generator | None = None, ) -> Sequence[cg.EngineResult]: return await self._run_sweep_async(program, params, repetitions) @@ -115,6 +119,7 @@ async def run_batch_async( programs: Sequence[cirq.AbstractCircuit] | Mapping[str, cirq.AbstractCircuit], params_list: Sequence[cirq.Sweepable] | None = None, repetitions: int | Sequence[int] = 1, + prng: np.random.Generator | None = None, ) -> Sequence[Sequence[cg.EngineResult]]: if self._jobs_per_batch > 1: # Treat programs as a sequence for iteration, but keep keys if it's a mapping @@ -153,7 +158,10 @@ async def run_batch_async( repetition_batches.append(batch_reps) all_batch_results = await duet.pstarmap_async( - self.run_sweep_async, zip(program_batches, params_list_batches, repetition_batches) + self.run_sweep_async, + zip( + program_batches, params_list_batches, repetition_batches, itertools.repeat(prng) + ), ) final_results = [] for batch_res, batch_progs in zip(all_batch_results, program_batches): diff --git a/cirq-google/cirq_google/engine/validating_sampler.py b/cirq-google/cirq_google/engine/validating_sampler.py index e2f11e512c9..13963fd64d2 100644 --- a/cirq-google/cirq_google/engine/validating_sampler.py +++ b/cirq-google/cirq_google/engine/validating_sampler.py @@ -15,11 +15,15 @@ from __future__ import annotations from collections.abc import Callable, Sequence +from typing import TYPE_CHECKING import duet import cirq +if TYPE_CHECKING: + import numpy as np + VALIDATOR_TYPE = Callable[ [Sequence[cirq.AbstractCircuit], Sequence[cirq.Sweepable], int | Sequence[int]], None ] @@ -64,19 +68,24 @@ def _validate_circuit( self._validator(circuits, sweeps, repetitions) def run_sweep( - self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 + self, + program: cirq.AbstractCircuit, + params: cirq.Sweepable, + repetitions: int = 1, + prng: np.random.Generator | None = None, ) -> Sequence[cirq.Result]: self._validate_circuit([program], [params], repetitions) - return self._sampler.run_sweep(program, params, repetitions) + return self._sampler.run_sweep(program, params, repetitions, prng) async def run_batch_async( self, programs: Sequence[cirq.AbstractCircuit], params_list: Sequence[cirq.Sweepable] | None = None, repetitions: int | Sequence[int] = 1, + prng: np.random.Generator | None = None, ) -> Sequence[Sequence[cirq.Result]]: params_list, repetitions = self._normalize_batch_args(programs, params_list, repetitions) self._validate_circuit(programs, params_list, repetitions) - return await self._sampler.run_batch_async(programs, params_list, repetitions) + return await self._sampler.run_batch_async(programs, params_list, repetitions, prng) run_batch = duet.sync(run_batch_async) diff --git a/cirq-ionq/cirq_ionq/sampler.py b/cirq-ionq/cirq_ionq/sampler.py index 25db1bf65ca..bb5fb72e963 100644 --- a/cirq-ionq/cirq_ionq/sampler.py +++ b/cirq-ionq/cirq_ionq/sampler.py @@ -22,6 +22,8 @@ from cirq_ionq import results if TYPE_CHECKING: + import numpy as np + import cirq_ionq @@ -69,7 +71,11 @@ def __init__( self._timeout_seconds = timeout_seconds def run_sweep( - self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 + self, + program: cirq.AbstractCircuit, + params: cirq.Sweepable, + repetitions: int = 1, + prng: np.random.Generator | None = None, ) -> Sequence[cirq.Result]: """Samples from the given Circuit. diff --git a/cirq-pasqal/cirq_pasqal/pasqal_sampler.py b/cirq-pasqal/cirq_pasqal/pasqal_sampler.py index bc6254a913a..b876c78e159 100644 --- a/cirq-pasqal/cirq_pasqal/pasqal_sampler.py +++ b/cirq-pasqal/cirq_pasqal/pasqal_sampler.py @@ -15,12 +15,16 @@ from __future__ import annotations import time +from typing import TYPE_CHECKING import requests import cirq import cirq_pasqal +if TYPE_CHECKING: + import numpy as np + class PasqalSampler(cirq.work.Sampler): def __init__( @@ -103,7 +107,11 @@ def _send_serialized_circuit( return result def run_sweep( - self, program: cirq.AbstractCircuit, params: cirq.study.Sweepable, repetitions: int = 1 + self, + program: cirq.AbstractCircuit, + params: cirq.study.Sweepable, + repetitions: int = 1, + prng: np.random.Generator | None = None, ) -> list[cirq.study.Result]: """Samples from the given Circuit. In contrast to run, this allows for sweeping over different parameter From 2ba866143eacd31d08d5cbe33bad39745a0d605f Mon Sep 17 00:00:00 2001 From: Advayth Pashupati <113481915+AamindMandragora@users.noreply.github.com> Date: Tue, 25 Aug 2026 22:46:16 -0500 Subject: [PATCH 2/5] just need to add tests now i think? --- cirq-aqt/cirq_aqt/aqt_sampler.py | 6 +- cirq-core/cirq/contrib/quimb/mps_simulator.py | 3 + .../random_quantum_circuit_generation.py | 7 +- .../single_qubit_readout_calibration_test.py | 6 +- cirq-core/cirq/linalg/decompositions_test.py | 3 +- .../cirq/sim/clifford/stabilizer_sampler.py | 10 +-- cirq-core/cirq/sim/simulator.py | 34 +++++++- cirq-core/cirq/sim/simulator_base.py | 79 +++++++++++++------ cirq-core/cirq/sim/simulator_base_test.py | 2 - cirq-core/cirq/sim/simulator_test.py | 2 - cirq-core/cirq/testing/lin_alg_utils.py | 15 ++-- cirq-core/cirq/testing/random_circuit.py | 7 +- cirq-core/cirq/value/__init__.py | 3 + cirq-core/cirq/value/random_state_test.py | 6 +- cirq-core/cirq/work/sampler.py | 50 +++++++++--- cirq-core/cirq/work/sampler_test.py | 27 ++----- cirq-core/cirq/work/zeros_sampler.py | 6 +- .../cirq_google/engine/processor_sampler.py | 26 ++++-- .../cirq_google/engine/validating_sampler.py | 5 +- cirq-ionq/cirq_ionq/sampler.py | 9 +-- cirq-pasqal/cirq_pasqal/pasqal_sampler.py | 10 +-- 21 files changed, 191 insertions(+), 125 deletions(-) diff --git a/cirq-aqt/cirq_aqt/aqt_sampler.py b/cirq-aqt/cirq_aqt/aqt_sampler.py index b0dda9a10a4..50886084450 100644 --- a/cirq-aqt/cirq_aqt/aqt_sampler.py +++ b/cirq-aqt/cirq_aqt/aqt_sampler.py @@ -405,11 +405,7 @@ def _send_json( return measurements def run_sweep( - self, - program: cirq.AbstractCircuit, - params: cirq.Sweepable, - repetitions: int = 1, - prng: np.random.Generator | None = None, + self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 ) -> Sequence[cirq.Result]: """Samples from the given Circuit. diff --git a/cirq-core/cirq/contrib/quimb/mps_simulator.py b/cirq-core/cirq/contrib/quimb/mps_simulator.py index eadd677b334..de0d6e15aa9 100644 --- a/cirq-core/cirq/contrib/quimb/mps_simulator.py +++ b/cirq-core/cirq/contrib/quimb/mps_simulator.py @@ -102,6 +102,7 @@ def _create_partial_simulation_state( ordering of the computational basis states. classical_data: The shared classical data container for this simulation. + prng: An `np.random.Generator` to draw from for this call instead of the internal random state. Returns: MPSState args for simulating the Circuit. @@ -391,6 +392,8 @@ def apply_op( op: The operation that mutates the object. Note that currently, only 1- and 2- qubit operations are currently supported. + prng: + An `np.random.Generator` to draw from for this call instead of the internal random state. """ old_inds = tuple(map(self.i_str, axes)) diff --git a/cirq-core/cirq/experiments/random_quantum_circuit_generation.py b/cirq-core/cirq/experiments/random_quantum_circuit_generation.py index f09796cb242..d4eff9bfdd4 100644 --- a/cirq-core/cirq/experiments/random_quantum_circuit_generation.py +++ b/cirq-core/cirq/experiments/random_quantum_circuit_generation.py @@ -24,7 +24,6 @@ from cirq import circuits, devices, ops, protocols, value from cirq._doc import document -from cirq.value import random_state as rs if TYPE_CHECKING: import networkx as nx @@ -344,7 +343,7 @@ def _get_random_combinations( combinations_by_layer = [] for pairs, layer in pair_gen: - combinations = rs.get_random_int( + combinations = value.get_random_int( parsed_rs, 0, n_library_circuits, size=(n_combinations, len(pairs)) ) combinations_by_layer.append( @@ -656,11 +655,11 @@ def random_gate(qubit: cirq.Qid) -> cirq.Gate: excluded_op = previous_single_qubit_layer.operation_at(qubit) excluded_gate = excluded_op.gate if excluded_op is not None else None g = self.single_qubit_gates[ - rs.get_random_int(self.prng, 0, len(self.single_qubit_gates)) + value.get_random_int(self.prng, 0, len(self.single_qubit_gates)) ] while g is excluded_gate: g = self.single_qubit_gates[ - rs.get_random_int(self.prng, 0, len(self.single_qubit_gates)) + value.get_random_int(self.prng, 0, len(self.single_qubit_gates)) ] return g diff --git a/cirq-core/cirq/experiments/single_qubit_readout_calibration_test.py b/cirq-core/cirq/experiments/single_qubit_readout_calibration_test.py index 60d392aa891..718b6882146 100644 --- a/cirq-core/cirq/experiments/single_qubit_readout_calibration_test.py +++ b/cirq-core/cirq/experiments/single_qubit_readout_calibration_test.py @@ -47,11 +47,7 @@ def __init__(self, p0: float, p1: float, seed: cirq.RANDOM_STATE_OR_SEED_LIKE = self.simulator = cirq.Simulator(seed=self.prng, split_untangled_states=False) def run_sweep( - self, - program: cirq.AbstractCircuit, - params: cirq.Sweepable, - repetitions: int = 1, - prng: np.random.Generator | None = None, + self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 ) -> Sequence[cirq.Result]: results = self.simulator.run_sweep(program, params, repetitions) for result in results: diff --git a/cirq-core/cirq/linalg/decompositions_test.py b/cirq-core/cirq/linalg/decompositions_test.py index 59db7376cc8..37e79bb8b4e 100644 --- a/cirq-core/cirq/linalg/decompositions_test.py +++ b/cirq-core/cirq/linalg/decompositions_test.py @@ -22,7 +22,6 @@ import cirq from cirq import unitary_eig, value from cirq.linalg.decompositions import MAGIC, MAGIC_CONJ_T -from cirq.value import random_state as rs X = np.array([[0, 1], [1, 0]]) Y = np.array([[0, -1j], [1j, 0]]) @@ -591,7 +590,7 @@ def _random_two_qubit_unitaries(num_samples: int, random_state: cirq.RANDOM_STAT prng = value.parse_random_state(random_state) # Generate the non-local part by explicit matrix exponentiation. - kak_vecs = rs.get_random_array(prng, (num_samples, 3)) * np.pi + kak_vecs = value.get_random_array(prng, (num_samples, 3)) * np.pi gens = np.einsum('...a,abc->...bc', kak_vecs, _kak_gens) evals, evecs = np.linalg.eigh(gens) A = np.einsum('...ab,...b,...cb', evecs, np.exp(1j * evals), evecs.conj()) diff --git a/cirq-core/cirq/sim/clifford/stabilizer_sampler.py b/cirq-core/cirq/sim/clifford/stabilizer_sampler.py index cca95a19244..63bdc375121 100644 --- a/cirq-core/cirq/sim/clifford/stabilizer_sampler.py +++ b/cirq-core/cirq/sim/clifford/stabilizer_sampler.py @@ -42,16 +42,12 @@ def run_sweep( program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1, - prng: np.random.RandomState | np.random.Generator | None = None, + prng: np.random.Generator | None = None, ) -> Sequence[cirq.Result]: results: list[cirq.Result] = [] for param_resolver in cirq.to_resolvers(params): resolved_circuit = cirq.resolve_parameters(program, param_resolver) - measurements = self._run( - resolved_circuit, - repetitions=repetitions, - prng=prng if prng is not None else self._prng, - ) + measurements = self._run(resolved_circuit, repetitions=repetitions, prng=prng) results.append(cirq.ResultDict(params=param_resolver, measurements=measurements)) return results @@ -59,7 +55,7 @@ def _run( self, circuit: cirq.AbstractCircuit, repetitions: int, - prng: np.random.RandomState | np.random.Generator | None = None, + prng: np.random.Generator | None = None, ) -> dict[str, np.ndarray]: measurements: dict[str, list[np.ndarray]] = { diff --git a/cirq-core/cirq/sim/simulator.py b/cirq-core/cirq/sim/simulator.py index e0cb7ada8eb..9a166e3ed69 100644 --- a/cirq-core/cirq/sim/simulator.py +++ b/cirq-core/cirq/sim/simulator.py @@ -61,6 +61,8 @@ def run_sweep( repetitions: int = 1, prng: np.random.Generator | None = None, ) -> Sequence[cirq.Result]: + if prng is None: + return list(self.run_sweep_iter(program, params, repetitions)) return list(self.run_sweep_iter(program, params, repetitions, prng)) def run_sweep_iter( @@ -79,6 +81,7 @@ def run_sweep_iter( program: The circuit to simulate. params: Parameters to run with the program. repetitions: The number of repetitions to simulate. + prng: An `np.random.Generator` to draw from for this call instead of the internal random state. Returns: Result list for this run; one for each possible parameter @@ -96,6 +99,10 @@ def run_sweep_iter( for _, op, _ in program.findall_operations_with_gate_type(ops.MeasurementGate): records[protocols.measurement_key_name(op)] = np.empty([0, 1, 1]) else: + if prng is None: + records = self._run( + circuit=program, param_resolver=param_resolver, repetitions=repetitions + ) records = self._run( circuit=program, param_resolver=param_resolver, @@ -490,10 +497,15 @@ def simulate( initial_state: The initial state for the simulation. The form of this state depends on the simulation implementation. See documentation of the implementing class for details. + prng: An `np.random.Generator` to draw from for this call instead of the internal random state. Returns: SimulationTrialResults for the simulation. Includes the final state. """ + if prng is None: + return self.simulate_sweep( + program, study.ParamResolver(param_resolver), qubit_order, initial_state + )[0] return self.simulate_sweep( program, study.ParamResolver(param_resolver), qubit_order, initial_state, prng )[0] @@ -510,6 +522,8 @@ def simulate_sweep( Prefer overriding `simulate_sweep_iter`. """ + if prng is None: + return list(self.simulate_sweep_iter(program, params, qubit_order, initial_state)) return list(self.simulate_sweep_iter(program, params, qubit_order, initial_state, prng)) def _simulate_sweep_to_iter( @@ -522,6 +536,8 @@ def _simulate_sweep_to_iter( ) -> Iterator[TSimulationTrialResult]: if type(self).simulate_sweep == SimulatesFinalState.simulate_sweep: raise RecursionError("Must define either simulate_sweep or simulate_sweep_iter.") + if prng is None: + yield from self.simulate_sweep(program, params, qubit_order, initial_state) yield from self.simulate_sweep(program, params, qubit_order, initial_state, prng) @value.alternative(requires='simulate_sweep', implementation=_simulate_sweep_to_iter) @@ -548,6 +564,7 @@ def simulate_sweep_iter( initial_state: The initial state for the simulation. The form of this state depends on the simulation implementation. See documentation of the implementing class for details. + prng: An `np.random.Generator` to draw from for this call instead of the internal random state. Returns: Iterator over SimulationTrialResults for this run, one for each @@ -598,6 +615,7 @@ def simulate_sweep_iter( either a raw state or an `SimulationStateBase`. The form of the raw state depends on the simulation implementation. See documentation of the implementing class for details. + prng: An `np.random.Generator` to draw from for this call instead of the internal random state. Returns: List of SimulationTrialResults for this run, one for each @@ -611,9 +629,14 @@ def simulate_sweep_iter( if isinstance(initial_state, SimulationStateBase) and i < len(resolvers) - 1 else initial_state ) - all_step_results = self.simulate_moment_steps( - program, param_resolver, qubit_order, state, prng - ) + if prng is None: + all_step_results = self.simulate_moment_steps( + program, param_resolver, qubit_order, state + ) + else: + all_step_results = self.simulate_moment_steps( + program, param_resolver, qubit_order, state, prng + ) measurements: dict[str, np.ndarray] = {} for step_result in all_step_results: for k, v in step_result.measurements.items(): @@ -647,6 +670,7 @@ def simulate_moment_steps( either a raw state or a `TSimulationState`. The form of the raw state depends on the simulation implementation. See documentation of the implementing class for details. + prng: An `np.random.Generator` to draw from for this call instead of the internal random state. Returns: Iterator that steps through the simulation, simulating each @@ -655,6 +679,10 @@ def simulate_moment_steps( param_resolver = study.ParamResolver(param_resolver) actual_initial_state = 0 if initial_state is None else initial_state qubits = ops.QubitOrder.as_qubit_order(qubit_order).order_for(circuit.all_qubits()) + if prng is None: + return self._base_iterator( + circuit, qubits, actual_initial_state, param_resolver=param_resolver + ) return self._base_iterator( circuit, qubits, actual_initial_state, param_resolver=param_resolver, prng=prng ) diff --git a/cirq-core/cirq/sim/simulator_base.py b/cirq-core/cirq/sim/simulator_base.py index 540b1501847..5069cbcbe0a 100644 --- a/cirq-core/cirq/sim/simulator_base.py +++ b/cirq-core/cirq/sim/simulator_base.py @@ -168,9 +168,14 @@ def _base_iterator( param_resolver: cirq.ParamResolver | None = None, prng: np.random.Generator | None = None, ) -> Iterator[TStepResultBase]: - sim_state = self._create_simulation_state( - initial_state, qubits, param_resolver=param_resolver, prng=prng - ) + if prng is None: + sim_state = self._create_simulation_state( + initial_state, qubits, param_resolver=param_resolver + ) + else: + sim_state = self._create_simulation_state( + initial_state, qubits, param_resolver=param_resolver, prng=prng + ) return self._core_iterator(circuit, sim_state) def _core_iterator( @@ -240,9 +245,12 @@ def _run( """See definition in `cirq.SimulatesSamples`.""" param_resolver = study.ParamResolver({}) if param_resolver is None else param_resolver qubits = tuple(sorted(circuit.all_qubits())) - sim_state = self._create_simulation_state( - 0, qubits, param_resolver=param_resolver, prng=prng - ) + if prng is None: + sim_state = self._create_simulation_state(0, qubits, param_resolver=param_resolver) + else: + sim_state = self._create_simulation_state( + 0, qubits, param_resolver=param_resolver, prng=prng + ) def can_run_prefix(op: cirq.Operation) -> bool: resolved_op = protocols.resolve_parameters(op, param_resolver) @@ -327,6 +335,7 @@ def simulate_sweep_iter( either a raw state or an `SimulationStateBase`. The form of the raw state depends on the simulation implementation. See documentation of the implementing class for details. + prng: An `np.random.Generator` to draw from for this call instead of the internal random state. Returns: List of SimulationTrialResults for this run, one for each @@ -338,7 +347,10 @@ def sweep_prefixable(op: cirq.Operation): qubits = ops.QubitOrder.as_qubit_order(qubit_order).order_for(program.all_qubits()) initial_state = 0 if initial_state is None else initial_state - sim_state = self._create_simulation_state(initial_state, qubits, prng=prng) + if prng is None: + sim_state = self._create_simulation_state(initial_state, qubits) + else: + sim_state = self._create_simulation_state(initial_state, qubits, prng=prng) prefix, suffix = ( split_into_matching_protocol_then_general(program, sweep_prefixable) if self._can_be_in_run_prefix(self.noise) @@ -349,7 +361,10 @@ def sweep_prefixable(op: cirq.Operation): pass assert step_result is not None sim_state = step_result._sim_state - yield from super().simulate_sweep_iter(suffix, params, qubit_order, sim_state, prng) + if prng is None: + yield from super().simulate_sweep_iter(suffix, params, qubit_order, sim_state) + else: + yield from super().simulate_sweep_iter(suffix, params, qubit_order, sim_state, prng) def _create_simulation_state( self, @@ -368,20 +383,32 @@ def _create_simulation_state( args_map: dict[cirq.Qid | None, TSimulationState] = {} if isinstance(initial_state, int): for q in reversed(qubits): - args_map[q] = self._create_partial_simulation_state( - initial_state=initial_state % q.dimension, - qubits=[q], + if prng is None: + args_map[q] = self._create_partial_simulation_state( + initial_state=initial_state % q.dimension, + qubits=[q], + classical_data=classical_data, + ) + else: + args_map[q] = self._create_partial_simulation_state( + initial_state=initial_state % q.dimension, + qubits=[q], + classical_data=classical_data, + prng=prng, + ) + initial_state = int(initial_state / q.dimension) + else: + if prng is None: + args = self._create_partial_simulation_state( + initial_state=initial_state, qubits=qubits, classical_data=classical_data + ) + else: + args = self._create_partial_simulation_state( + initial_state=initial_state, + qubits=qubits, classical_data=classical_data, prng=prng, ) - initial_state = int(initial_state / q.dimension) - else: - args = self._create_partial_simulation_state( - initial_state=initial_state, - qubits=qubits, - classical_data=classical_data, - prng=prng, - ) for q in qubits: args_map[q] = args args_map[None] = self._create_partial_simulation_state(0, (), classical_data, prng) @@ -393,9 +420,17 @@ def _create_simulation_state( param_resolver=param_resolver, ) else: - state = self._create_partial_simulation_state( - initial_state=initial_state, qubits=qubits, classical_data=classical_data, prng=prng - ) + if prng is None: + state = self._create_partial_simulation_state( + initial_state=initial_state, qubits=qubits, classical_data=classical_data + ) + else: + state = self._create_partial_simulation_state( + initial_state=initial_state, + qubits=qubits, + classical_data=classical_data, + prng=prng, + ) state.param_resolver = ( study.ParamResolver({}) if param_resolver is None else param_resolver ) diff --git a/cirq-core/cirq/sim/simulator_base_test.py b/cirq-core/cirq/sim/simulator_base_test.py index 8e901cd644b..00c89acbcb7 100644 --- a/cirq-core/cirq/sim/simulator_base_test.py +++ b/cirq-core/cirq/sim/simulator_base_test.py @@ -127,7 +127,6 @@ def _create_partial_simulation_state( initial_state: Any, qubits: Sequence[cirq.Qid], classical_data: cirq.ClassicalDataStore, - prng: np.random.Generator | None = None, ) -> CountingSimulationState: return CountingSimulationState( qubits=qubits, state=initial_state, classical_data=classical_data @@ -158,7 +157,6 @@ def _create_partial_simulation_state( initial_state: Any, qubits: Sequence[cirq.Qid], classical_data: cirq.ClassicalDataStore, - prng: np.random.Generator | None = None, ) -> CountingSimulationState: return SplittableCountingSimulationState( qubits=qubits, state=initial_state, classical_data=classical_data diff --git a/cirq-core/cirq/sim/simulator_test.py b/cirq-core/cirq/sim/simulator_test.py index b28814d1924..eadb8f32f81 100644 --- a/cirq-core/cirq/sim/simulator_test.py +++ b/cirq-core/cirq/sim/simulator_test.py @@ -80,7 +80,6 @@ def _base_iterator( qubits: tuple[cirq.Qid, ...], initial_state: Any, param_resolver: cirq.ParamResolver | None = None, - prng: np.random.Generator | None = None, ) -> Iterator[TStepResult]: raise NotImplementedError @@ -498,7 +497,6 @@ def simulate_sweep( params: study.Sweepable, qubit_order: cirq.QubitOrderOrList = cirq.QubitOrder.DEFAULT, initial_state: Any = None, - prng: np.random.Generator | None = None, ) -> list[SimulationTrialResult]: return [mock_trial_result] diff --git a/cirq-core/cirq/testing/lin_alg_utils.py b/cirq-core/cirq/testing/lin_alg_utils.py index bb0609bf970..5c3551c1411 100644 --- a/cirq-core/cirq/testing/lin_alg_utils.py +++ b/cirq-core/cirq/testing/lin_alg_utils.py @@ -21,7 +21,6 @@ import numpy as np from cirq import linalg, value -from cirq.value import random_state as rs if TYPE_CHECKING: import cirq @@ -43,8 +42,8 @@ def random_superposition( """ random_state = value.parse_random_state(random_state) - state_vector = rs.get_random_normal_array(random_state, [dim]).astype(complex) - state_vector += 1j * rs.get_random_normal_array(random_state, [dim]) + state_vector = value.get_random_normal_array(random_state, [dim]).astype(complex) + state_vector += 1j * value.get_random_normal_array(random_state, [dim]) state_vector /= np.linalg.norm(state_vector) return state_vector @@ -67,9 +66,9 @@ def random_density_matrix( """ random_state = value.parse_random_state(random_state) - mat = rs.get_random_normal_array(random_state, (dim, dim)) + 1j * rs.get_random_normal_array( + mat = value.get_random_normal_array( random_state, (dim, dim) - ) + ) + 1j * value.get_random_normal_array(random_state, (dim, dim)) mat = mat @ mat.T.conj() return mat / np.trace(mat) @@ -90,9 +89,9 @@ def random_unitary(dim: int, *, random_state: cirq.RANDOM_STATE_OR_SEED_LIKE = N """ random_state = value.parse_random_state(random_state) - z = rs.get_random_normal_array(random_state, (dim, dim)) + 1j * rs.get_random_normal_array( + z = value.get_random_normal_array( random_state, (dim, dim) - ) + ) + 1j * value.get_random_normal_array(random_state, (dim, dim)) q, r = np.linalg.qr(z) d = np.diag(r) return q * (d / abs(d)) @@ -118,7 +117,7 @@ def random_orthogonal( """ random_state = value.parse_random_state(random_state) - m = rs.get_random_normal_array(random_state, (dim, dim)) + m = value.get_random_normal_array(random_state, (dim, dim)) q, r = np.linalg.qr(m) d = np.diag(r) return q * (d / abs(d)) diff --git a/cirq-core/cirq/testing/random_circuit.py b/cirq-core/cirq/testing/random_circuit.py index 55a7759075e..4fbd670d9dd 100644 --- a/cirq-core/cirq/testing/random_circuit.py +++ b/cirq-core/cirq/testing/random_circuit.py @@ -19,7 +19,6 @@ from cirq import circuits, ops, value from cirq._doc import document -from cirq.value import random_state as rs if TYPE_CHECKING: import cirq @@ -116,10 +115,10 @@ def random_circuit( operations = [] free_qubits = set(qubits) while len(free_qubits) >= max_arity: - gate, arity = gate_arity_pairs[rs.get_random_int(prng, num_gates)] + gate, arity = gate_arity_pairs[value.get_random_int(prng, num_gates)] op_qubits = prng.choice(sorted(free_qubits), size=arity, replace=False) free_qubits.difference_update(op_qubits) - if rs.get_random_array(prng) <= op_density: + if value.get_random_array(prng) <= op_density: operations.append(gate(*op_qubits)) moments.append(circuits.Moment(operations)) @@ -151,7 +150,7 @@ def random_two_qubit_circuit_with_czs( def random_one_qubit_gate(): return ops.PhasedXPowGate( - phase_exponent=rs.get_random_array(prng), exponent=rs.get_random_array(prng) + phase_exponent=value.get_random_array(prng), exponent=value.get_random_array(prng) ) def one_cz(): diff --git a/cirq-core/cirq/value/__init__.py b/cirq-core/cirq/value/__init__.py index 7aff6f654e1..534771c755d 100644 --- a/cirq-core/cirq/value/__init__.py +++ b/cirq-core/cirq/value/__init__.py @@ -74,6 +74,9 @@ from cirq.value.periodic_value import PeriodicValue as PeriodicValue from cirq.value.random_state import ( + get_random_array as get_random_array, + get_random_int as get_random_int, + get_random_normal_array as get_random_normal_array, parse_random_state as parse_random_state, RANDOM_STATE_OR_SEED_LIKE as RANDOM_STATE_OR_SEED_LIKE, ) diff --git a/cirq-core/cirq/value/random_state_test.py b/cirq-core/cirq/value/random_state_test.py index 87b9502f4e2..852c90374d4 100644 --- a/cirq-core/cirq/value/random_state_test.py +++ b/cirq-core/cirq/value/random_state_test.py @@ -23,13 +23,17 @@ def test_parse_random_state() -> None: global_state = np.random.get_state() + def rand(prng): + np.random.set_state(global_state) + return random_state.get_random_array(prng) + prngs = [ np.random, cirq.value.parse_random_state(np.random), cirq.value.parse_random_state(None), ] - vals = [prng.rand() if (isinstance(prng, np.random.RandomState) or isinstance(prng, np.random)) else 0 for prng in prngs] + vals = [rand(prng) for prng in prngs] eq = cirq.testing.EqualsTester() eq.add_equality_group(*vals) diff --git a/cirq-core/cirq/work/sampler.py b/cirq-core/cirq/work/sampler.py index a3e9feabadd..1f549be235e 100644 --- a/cirq-core/cirq/work/sampler.py +++ b/cirq-core/cirq/work/sampler.py @@ -66,11 +66,14 @@ def run( program: The circuit to sample from. param_resolver: Parameters to run with the program. repetitions: The number of times to sample. + prng: An `np.random.Generator` to draw from for this call instead of the internal random state. Returns: `cirq.Result` that contains all the measurements for a run. """ - return self.run_sweep(program, param_resolver, repetitions, prng)[0] + if prng is None: + return self.run_sweep(program, param_resolver, repetitions)[0] + return self.run_sweep(program, param_resolver, repetitions, prng=prng)[0] async def run_async( self, @@ -89,12 +92,14 @@ async def run_async( program: The circuit to sample from. param_resolver: Parameters to run with the program. repetitions: The number of times to sample. + prng: An `np.random.Generator` to draw from for this call instead of the internal random state. Returns: Result for a run. """ - results = await self.run_sweep_async(program, param_resolver, repetitions, prng) - return results[0] + if prng is None: + return (await self.run_sweep_async(program, param_resolver, repetitions))[0] + return (await self.run_sweep_async(program, param_resolver, repetitions, prng))[0] def sample( self, @@ -118,6 +123,7 @@ def sample( a dictionary, a list of dictionaries, a `cirq.Sweep`, a list of `cirq.Sweep`, etc. The program will be sampled `repetition` times for each mapping. Defaults to a single empty mapping. + prng: An `np.random.Generator` to draw from for this call instead of the internal random state. Returns: A `pandas.DataFrame` with a row for each sample, and a column for @@ -181,9 +187,12 @@ def sample( results = [] for sweep in sweeps_list: - sweep_results = self.run_sweep( - program, params=sweep, repetitions=repetitions, prng=prng - ) + if prng is None: + sweep_results = self.run_sweep(program, params=sweep, repetitions=repetitions) + else: + sweep_results = self.run_sweep( + program, params=sweep, repetitions=repetitions, prng=prng + ) for resolver, result in zip(sweep, sweep_results): param_values_once = [resolver.value_of(key) for key in keys] param_table = pd.DataFrame(data=[param_values_once] * repetitions, columns=keys) @@ -199,6 +208,8 @@ def _run_sweep_impl( prng: np.random.Generator | None = None, ) -> Sequence[cirq.Result]: """Implements run_sweep using run_sweep_async""" + if prng is None: + return duet.run(self.run_sweep_async, program, params, repetitions) return duet.run(self.run_sweep_async, program, params, repetitions, prng) async def _run_sweep_async_impl( @@ -209,6 +220,8 @@ async def _run_sweep_async_impl( prng: np.random.Generator | None = None, ) -> Sequence[cirq.Result]: """Implements run_sweep_async using run_sweep""" + if prng is None: + return self.run_sweep(program, params=params, repetitions=repetitions) return self.run_sweep(program, params=params, repetitions=repetitions, prng=prng) @value.alternative(requires='run_sweep_async', implementation=_run_sweep_impl) @@ -233,6 +246,7 @@ def run_sweep( program: The circuit to sample from. params: Parameters to run with the program. repetitions: The number of times to sample. + prng: An `np.random.Generator` to draw from for this call instead of the internal random state. Returns: Result list for this run; one for each possible parameter resolver. @@ -257,6 +271,7 @@ async def run_sweep_async( program: The circuit to sample from. params: Parameters to run with the program. repetitions: The number of times to sample. + prng: An `np.random.Generator` to draw from for this call instead of the internal random state. Returns: Result list for this run; one for each possible parameter resolver. @@ -297,6 +312,7 @@ async def run_batch_async( repetitions: Number of circuit repetitions to run. Can be specified as a single value to use for all runs, or as a list of values, one for each circuit. + prng: An `np.random.Generator` to draw from for this call instead of the internal random state. Returns: A list of lists of TrialResults. The outer list corresponds to @@ -309,9 +325,25 @@ async def run_batch_async( of `params_list` or the length of `repetitions`. """ params_list, repetitions = self._normalize_batch_args(programs, params_list, repetitions) - return await duet.pstarmap_async( - self.run_sweep_async, zip(programs, params_list, repetitions, itertools.repeat(prng)) - ) + if prng is None: + return await duet.pstarmap_async( + self.run_sweep_async, + zip( + programs, + params_list, + repetitions + ), + ) + else: + return await duet.pstarmap_async( + self.run_sweep_async, + zip( + programs, + params_list, + repetitions, + prng.spawn(len(programs)) + ), + ) run_batch = duet.sync(run_batch_async) diff --git a/cirq-core/cirq/work/sampler_test.py b/cirq-core/cirq/work/sampler_test.py index cc05eead143..8bc6fbc9dd9 100644 --- a/cirq-core/cirq/work/sampler_test.py +++ b/cirq-core/cirq/work/sampler_test.py @@ -52,9 +52,7 @@ async def test_run_sweep_async() -> None: @duet.sync async def test_sampler_async_fail() -> None: class FailingSampler(cirq.Sampler): - def run_sweep( - self, program, params, repetitions: int = 1, prng: np.random.Generator | None = None - ): + def run_sweep(self, program, params, repetitions: int = 1): raise ValueError('test') with pytest.raises(ValueError, match='test'): @@ -68,11 +66,9 @@ def test_run_sweep_impl() -> None: """Test run_sweep implemented in terms of run_sweep_async.""" class AsyncSampler(cirq.Sampler): - async def run_sweep_async( - self, program, params, repetitions: int = 1, prng: np.random.Generator | None = None - ): + async def run_sweep_async(self, program, params, repetitions: int = 1): await duet.sleep(0.001) - return cirq.Simulator().run_sweep(program, params, repetitions, prng) + return cirq.Simulator().run_sweep(program, params, repetitions) results = AsyncSampler().run_sweep( cirq.Circuit(cirq.measure(cirq.GridQubit(0, 0), key='m')), @@ -89,9 +85,7 @@ async def test_run_sweep_async_impl() -> None: """Test run_sweep_async implemented in terms of run_sweep.""" class SyncSampler(cirq.Sampler): - def run_sweep( - self, program, params, repetitions: int = 1, prng: np.random.Generator | None = None - ): + def run_sweep(self, program, params, repetitions: int = 1): return cirq.Simulator().run_sweep(program, params, repetitions) results = await SyncSampler().run_sweep_async( @@ -233,12 +227,7 @@ async def test_run_batch_async_calls_run_sweep_asynchronously() -> None: class AsyncSampler(cirq.Sampler): async def run_sweep_async( - self, - program, - params, - repetitions: int = 1, - prng: np.random.Generator | None = None, - unused: duet.Limiter = duet.Limiter(None), + self, program, params, repetitions: int = 1, unused: duet.Limiter = duet.Limiter(None) ): if params == params1: await duet.sleep(0.001) @@ -300,11 +289,7 @@ class DeterministicImbalancedStateSampler(cirq.Sampler): """ def run_sweep( - self, - program: cirq.AbstractCircuit, - params: cirq.Sweepable, - repetitions: int = 1, - prng: np.random.Generator | None = None, + self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 ) -> Sequence[cirq.Result]: results = np.zeros((repetitions, 1), dtype=bool) for idx in range(repetitions // 4): diff --git a/cirq-core/cirq/work/zeros_sampler.py b/cirq-core/cirq/work/zeros_sampler.py index 4e26a14a51c..cdd9da7944c 100644 --- a/cirq-core/cirq/work/zeros_sampler.py +++ b/cirq-core/cirq/work/zeros_sampler.py @@ -38,11 +38,7 @@ def __init__(self, device: devices.Device | None = None): self.device = device def run_sweep( - self, - program: cirq.AbstractCircuit, - params: study.Sweepable, - repetitions: int = 1, - prng: np.random.Generator | None = None, + self, program: cirq.AbstractCircuit, params: study.Sweepable, repetitions: int = 1 ) -> list[study.Result]: """Samples circuit as if every measurement resulted in zero. diff --git a/cirq-google/cirq_google/engine/processor_sampler.py b/cirq-google/cirq_google/engine/processor_sampler.py index 35018a4f07e..0ffca4ffaba 100644 --- a/cirq-google/cirq_google/engine/processor_sampler.py +++ b/cirq-google/cirq_google/engine/processor_sampler.py @@ -156,13 +156,25 @@ async def run_batch_async( program_batches.append(batch_programs) params_list_batches.append(batch_sweep) repetition_batches.append(batch_reps) - - all_batch_results = await duet.pstarmap_async( - self.run_sweep_async, - zip( - program_batches, params_list_batches, repetition_batches, itertools.repeat(prng) - ), - ) + if prng is None: + all_batch_results = await duet.pstarmap_async( + self.run_sweep_async, + zip( + program_batches, + params_list_batches, + repetition_batches + ), + ) + else: + all_batch_results = await duet.pstarmap_async( + self.run_sweep_async, + zip( + program_batches, + params_list_batches, + repetition_batches, + prng.spawn(len(program_batches)) + ), + ) final_results = [] for batch_res, batch_progs in zip(all_batch_results, program_batches): num_progs = len(batch_progs) diff --git a/cirq-google/cirq_google/engine/validating_sampler.py b/cirq-google/cirq_google/engine/validating_sampler.py index 13963fd64d2..15ab527b5f5 100644 --- a/cirq-google/cirq_google/engine/validating_sampler.py +++ b/cirq-google/cirq_google/engine/validating_sampler.py @@ -86,6 +86,9 @@ async def run_batch_async( ) -> Sequence[Sequence[cirq.Result]]: params_list, repetitions = self._normalize_batch_args(programs, params_list, repetitions) self._validate_circuit(programs, params_list, repetitions) - return await self._sampler.run_batch_async(programs, params_list, repetitions, prng) + if prng is None: + return await self._sampler.run_batch_async(programs, params_list, repetitions) + else: + return await self._sampler.run_batch_async(programs, params_list, repetitions, prng) run_batch = duet.sync(run_batch_async) diff --git a/cirq-ionq/cirq_ionq/sampler.py b/cirq-ionq/cirq_ionq/sampler.py index bb5fb72e963..0e83f413133 100644 --- a/cirq-ionq/cirq_ionq/sampler.py +++ b/cirq-ionq/cirq_ionq/sampler.py @@ -22,11 +22,8 @@ from cirq_ionq import results if TYPE_CHECKING: - import numpy as np - import cirq_ionq - class Sampler(cirq.Sampler): """A sampler that works against the IonQ API. @@ -71,11 +68,7 @@ def __init__( self._timeout_seconds = timeout_seconds def run_sweep( - self, - program: cirq.AbstractCircuit, - params: cirq.Sweepable, - repetitions: int = 1, - prng: np.random.Generator | None = None, + self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 ) -> Sequence[cirq.Result]: """Samples from the given Circuit. diff --git a/cirq-pasqal/cirq_pasqal/pasqal_sampler.py b/cirq-pasqal/cirq_pasqal/pasqal_sampler.py index b876c78e159..bdf50f65bc0 100644 --- a/cirq-pasqal/cirq_pasqal/pasqal_sampler.py +++ b/cirq-pasqal/cirq_pasqal/pasqal_sampler.py @@ -22,10 +22,6 @@ import cirq import cirq_pasqal -if TYPE_CHECKING: - import numpy as np - - class PasqalSampler(cirq.work.Sampler): def __init__( self, @@ -107,11 +103,7 @@ def _send_serialized_circuit( return result def run_sweep( - self, - program: cirq.AbstractCircuit, - params: cirq.study.Sweepable, - repetitions: int = 1, - prng: np.random.Generator | None = None, + self, program: cirq.AbstractCircuit, params: cirq.study.Sweepable, repetitions: int = 1 ) -> list[cirq.study.Result]: """Samples from the given Circuit. In contrast to run, this allows for sweeping over different parameter From e1b73a71e4bc6881d514824940c57f6f1461cc6f Mon Sep 17 00:00:00 2001 From: Advayth Pashupati <113481915+AamindMandragora@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:50:56 -0500 Subject: [PATCH 3/5] i think i got every test? --- cirq-aqt/cirq_aqt/aqt_sampler.py | 9 ++++- cirq-aqt/cirq_aqt/aqt_sampler_test.py | 9 +++++ cirq-core/cirq/contrib/quimb/mps_simulator.py | 3 +- .../single_qubit_readout_calibration_test.py | 2 +- cirq-core/cirq/sim/simulator.py | 31 +++++++++------ cirq-core/cirq/sim/simulator_base.py | 8 +++- cirq-core/cirq/sim/simulator_base_test.py | 4 +- cirq-core/cirq/sim/simulator_test.py | 38 ++++++++++++++++++- cirq-core/cirq/work/sampler.py | 33 +++++++--------- cirq-core/cirq/work/sampler_test.py | 35 ++++++++++++++--- cirq-core/cirq/work/zeros_sampler.py | 9 ++++- cirq-core/cirq/work/zeros_sampler_test.py | 8 ++++ .../cirq_google/engine/processor_sampler.py | 9 +---- cirq-ionq/cirq_ionq/sampler.py | 12 +++++- cirq-ionq/cirq_ionq/sampler_test.py | 13 +++++++ cirq-pasqal/cirq_pasqal/pasqal_sampler.py | 11 +++++- .../cirq_pasqal/pasqal_sampler_test.py | 12 ++++++ 17 files changed, 191 insertions(+), 55 deletions(-) diff --git a/cirq-aqt/cirq_aqt/aqt_sampler.py b/cirq-aqt/cirq_aqt/aqt_sampler.py index 50886084450..7cf30aa901c 100644 --- a/cirq-aqt/cirq_aqt/aqt_sampler.py +++ b/cirq-aqt/cirq_aqt/aqt_sampler.py @@ -405,7 +405,11 @@ def _send_json( return measurements def run_sweep( - self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 + self, + program: cirq.AbstractCircuit, + params: cirq.Sweepable, + repetitions: int = 1, + prng: np.random.Generator | None = None, ) -> Sequence[cirq.Result]: """Samples from the given Circuit. @@ -417,11 +421,14 @@ def run_sweep( Should be generated using AQTSampler.generate_circuit_from_list params: Parameters to run with the program. repetitions: The number of repetitions to simulate. + prng: Not supported for this class as no client-side RNG, must be None. Returns: Result list for this run; one for each possible parameter resolver. """ + if prng is not None: + raise ValueError("AQTSampler has no client-side RNG.") # TODO: Use measurement name from circuit. # Github issue: https://github.com/quantumlib/Cirq/issues/2199 meas_name = 'm' diff --git a/cirq-aqt/cirq_aqt/aqt_sampler_test.py b/cirq-aqt/cirq_aqt/aqt_sampler_test.py index 6b8c78a44ea..1a2cd73eb88 100644 --- a/cirq-aqt/cirq_aqt/aqt_sampler_test.py +++ b/cirq-aqt/cirq_aqt/aqt_sampler_test.py @@ -459,3 +459,12 @@ def json(self): assert workspaces[0]["resources"][0]["id"] == "rid" assert workspaces[0]["resources"][0]["name"] == "Resource" assert workspaces[0]["resources"][0]["type"] == "device" + + +def test_aqt_sampler_rejects_prng() -> None: + a = cirq.LineQubit(0) + circuit = cirq.Circuit(cirq.H(a), cirq.measure(a, key='m')) + + sampler = AQTSampler("default", "test", "testkey") + with pytest.raises(ValueError, match='RNG'): + sampler.run_sweep(circuit, None, 1, prng=np.random.default_rng(0)) diff --git a/cirq-core/cirq/contrib/quimb/mps_simulator.py b/cirq-core/cirq/contrib/quimb/mps_simulator.py index de0d6e15aa9..2e02d4868df 100644 --- a/cirq-core/cirq/contrib/quimb/mps_simulator.py +++ b/cirq-core/cirq/contrib/quimb/mps_simulator.py @@ -102,7 +102,8 @@ def _create_partial_simulation_state( ordering of the computational basis states. classical_data: The shared classical data container for this simulation. - prng: An `np.random.Generator` to draw from for this call instead of the internal random state. + prng: An `np.random.Generator` to draw from for this call + instead of the internal random state. Returns: MPSState args for simulating the Circuit. diff --git a/cirq-core/cirq/experiments/single_qubit_readout_calibration_test.py b/cirq-core/cirq/experiments/single_qubit_readout_calibration_test.py index 718b6882146..f3bdd427e59 100644 --- a/cirq-core/cirq/experiments/single_qubit_readout_calibration_test.py +++ b/cirq-core/cirq/experiments/single_qubit_readout_calibration_test.py @@ -46,7 +46,7 @@ def __init__(self, p0: float, p1: float, seed: cirq.RANDOM_STATE_OR_SEED_LIKE = self.prng = cirq.value.parse_random_state(seed) self.simulator = cirq.Simulator(seed=self.prng, split_untangled_states=False) - def run_sweep( + def run_sweep( # type: ignore[override] self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 ) -> Sequence[cirq.Result]: results = self.simulator.run_sweep(program, params, repetitions) diff --git a/cirq-core/cirq/sim/simulator.py b/cirq-core/cirq/sim/simulator.py index 9a166e3ed69..5e5cf1bbeac 100644 --- a/cirq-core/cirq/sim/simulator.py +++ b/cirq-core/cirq/sim/simulator.py @@ -81,7 +81,8 @@ def run_sweep_iter( program: The circuit to simulate. params: Parameters to run with the program. repetitions: The number of repetitions to simulate. - prng: An `np.random.Generator` to draw from for this call instead of the internal random state. + prng: An `np.random.Generator` to draw from for this call + instead of the internal random state. Returns: Result list for this run; one for each possible parameter @@ -103,12 +104,13 @@ def run_sweep_iter( records = self._run( circuit=program, param_resolver=param_resolver, repetitions=repetitions ) - records = self._run( - circuit=program, - param_resolver=param_resolver, - repetitions=repetitions, - prng=prng, - ) + else: + records = self._run( + circuit=program, + param_resolver=param_resolver, + repetitions=repetitions, + prng=prng, + ) yield study.ResultDict(params=param_resolver, records=records) @abc.abstractmethod @@ -497,7 +499,8 @@ def simulate( initial_state: The initial state for the simulation. The form of this state depends on the simulation implementation. See documentation of the implementing class for details. - prng: An `np.random.Generator` to draw from for this call instead of the internal random state. + prng: An `np.random.Generator` to draw from for this call + instead of the internal random state. Returns: SimulationTrialResults for the simulation. Includes the final state. @@ -538,7 +541,8 @@ def _simulate_sweep_to_iter( raise RecursionError("Must define either simulate_sweep or simulate_sweep_iter.") if prng is None: yield from self.simulate_sweep(program, params, qubit_order, initial_state) - yield from self.simulate_sweep(program, params, qubit_order, initial_state, prng) + else: + yield from self.simulate_sweep(program, params, qubit_order, initial_state, prng) @value.alternative(requires='simulate_sweep', implementation=_simulate_sweep_to_iter) def simulate_sweep_iter( @@ -564,7 +568,8 @@ def simulate_sweep_iter( initial_state: The initial state for the simulation. The form of this state depends on the simulation implementation. See documentation of the implementing class for details. - prng: An `np.random.Generator` to draw from for this call instead of the internal random state. + prng: An `np.random.Generator` to draw from for this call + instead of the internal random state. Returns: Iterator over SimulationTrialResults for this run, one for each @@ -615,7 +620,8 @@ def simulate_sweep_iter( either a raw state or an `SimulationStateBase`. The form of the raw state depends on the simulation implementation. See documentation of the implementing class for details. - prng: An `np.random.Generator` to draw from for this call instead of the internal random state. + prng: An `np.random.Generator` to draw from for this call + instead of the internal random state. Returns: List of SimulationTrialResults for this run, one for each @@ -670,7 +676,8 @@ def simulate_moment_steps( either a raw state or a `TSimulationState`. The form of the raw state depends on the simulation implementation. See documentation of the implementing class for details. - prng: An `np.random.Generator` to draw from for this call instead of the internal random state. + prng: An `np.random.Generator` to draw from for this call + instead of the internal random state. Returns: Iterator that steps through the simulation, simulating each diff --git a/cirq-core/cirq/sim/simulator_base.py b/cirq-core/cirq/sim/simulator_base.py index 5069cbcbe0a..837c3bb8819 100644 --- a/cirq-core/cirq/sim/simulator_base.py +++ b/cirq-core/cirq/sim/simulator_base.py @@ -335,7 +335,8 @@ def simulate_sweep_iter( either a raw state or an `SimulationStateBase`. The form of the raw state depends on the simulation implementation. See documentation of the implementing class for details. - prng: An `np.random.Generator` to draw from for this call instead of the internal random state. + prng: An `np.random.Generator` to draw from for this call + instead of the internal random state. Returns: List of SimulationTrialResults for this run, one for each @@ -411,7 +412,10 @@ def _create_simulation_state( ) for q in qubits: args_map[q] = args - args_map[None] = self._create_partial_simulation_state(0, (), classical_data, prng) + if prng is None: + args_map[None] = self._create_partial_simulation_state(0, (), classical_data) + else: + args_map[None] = self._create_partial_simulation_state(0, (), classical_data, prng) return SimulationProductState( args_map, qubits, diff --git a/cirq-core/cirq/sim/simulator_base_test.py b/cirq-core/cirq/sim/simulator_base_test.py index 00c89acbcb7..7143814cc30 100644 --- a/cirq-core/cirq/sim/simulator_base_test.py +++ b/cirq-core/cirq/sim/simulator_base_test.py @@ -122,7 +122,7 @@ class CountingSimulator( def __init__(self, noise=None, split_untangled_states=False): super().__init__(noise=noise, split_untangled_states=split_untangled_states) - def _create_partial_simulation_state( + def _create_partial_simulation_state( # type: ignore[override] self, initial_state: Any, qubits: Sequence[cirq.Qid], @@ -152,7 +152,7 @@ class SplittableCountingSimulator(CountingSimulator): def __init__(self, noise=None, split_untangled_states=True): super().__init__(noise=noise, split_untangled_states=split_untangled_states) - def _create_partial_simulation_state( + def _create_partial_simulation_state( # type: ignore[override] self, initial_state: Any, qubits: Sequence[cirq.Qid], diff --git a/cirq-core/cirq/sim/simulator_test.py b/cirq-core/cirq/sim/simulator_test.py index eadb8f32f81..0f3d040f62a 100644 --- a/cirq-core/cirq/sim/simulator_test.py +++ b/cirq-core/cirq/sim/simulator_test.py @@ -74,7 +74,7 @@ class SimulatesIntermediateStateImpl( ): """A SimulatesIntermediateState that uses the default SimulationTrialResult type.""" - def _base_iterator( + def _base_iterator( # type: ignore[override] self, circuit: cirq.AbstractCircuit, qubits: tuple[cirq.Qid, ...], @@ -491,7 +491,7 @@ def simulate_expectation_values_sweep( ) -> list[list[float]]: return [[1.0]] - def simulate_sweep( + def simulate_sweep( # type: ignore[override] self, program: cirq.AbstractCircuit, params: study.Sweepable, @@ -557,3 +557,37 @@ def test_trial_result_initializer() -> None: assert x._final_simulator_state == 3 x = SimulationTrialResult(resolver, {}, final_simulator_state=state) assert x._final_simulator_state == 3 + + +def test_sampler_uses_prng_param() -> None: + """Tests that `np.random.Generator` passed in to `run` bypasses sampler internal state.""" + + a = cirq.LineQubit(0) + sampler1 = cirq.Simulator(seed=1) + sampler2 = cirq.Simulator(seed=2) + circuit = cirq.Circuit(cirq.H(a), cirq.measure(a, key='m')) + + # reuse the sampler so when res1 == res2 it must be because of the prng argument + res1 = sampler1.run(circuit, repetitions=50, prng=np.random.default_rng(0)).records['m'] + res2 = sampler1.run(circuit, repetitions=50, prng=np.random.default_rng(0)).records['m'] + res3 = sampler1.run(circuit, repetitions=50, prng=np.random.default_rng(1)).records['m'] + # sampler with different seed will have identical output to previous run as prng is the same + res4 = sampler2.run(circuit, repetitions=50, prng=np.random.default_rng(1)).records['m'] + + assert np.array_equal(res1, res2) + assert not np.array_equal(res1, res3) + assert np.array_equal(res3, res4) + + +def test_sampler_without_prng_uses_internal_state() -> None: + """Tests that if no prng argument is given, the internal state is used.""" + + a = cirq.LineQubit(0) + sampler = cirq.Simulator(seed=1) + circuit = cirq.Circuit(cirq.H(a), cirq.measure(a, key='m')) + + # since the same sampler is being used twice, the outputs can't be equal + res1 = sampler.run(circuit, repetitions=50).records['m'] + res2 = sampler.run(circuit, repetitions=50).records['m'] + + assert not np.array_equal(res1, res2) diff --git a/cirq-core/cirq/work/sampler.py b/cirq-core/cirq/work/sampler.py index 1f549be235e..7de7ca1d052 100644 --- a/cirq-core/cirq/work/sampler.py +++ b/cirq-core/cirq/work/sampler.py @@ -17,7 +17,6 @@ from __future__ import annotations import collections -import itertools from collections.abc import Sequence from typing import TYPE_CHECKING, TypeVar @@ -66,7 +65,8 @@ def run( program: The circuit to sample from. param_resolver: Parameters to run with the program. repetitions: The number of times to sample. - prng: An `np.random.Generator` to draw from for this call instead of the internal random state. + prng: An `np.random.Generator` to draw from for this call + instead of the internal random state. Returns: `cirq.Result` that contains all the measurements for a run. @@ -92,7 +92,8 @@ async def run_async( program: The circuit to sample from. param_resolver: Parameters to run with the program. repetitions: The number of times to sample. - prng: An `np.random.Generator` to draw from for this call instead of the internal random state. + prng: An `np.random.Generator` to draw from for this call + instead of the internal random state. Returns: Result for a run. @@ -123,7 +124,8 @@ def sample( a dictionary, a list of dictionaries, a `cirq.Sweep`, a list of `cirq.Sweep`, etc. The program will be sampled `repetition` times for each mapping. Defaults to a single empty mapping. - prng: An `np.random.Generator` to draw from for this call instead of the internal random state. + prng: An `np.random.Generator` to draw from for this call + instead of the internal random state. Returns: A `pandas.DataFrame` with a row for each sample, and a column for @@ -246,7 +248,8 @@ def run_sweep( program: The circuit to sample from. params: Parameters to run with the program. repetitions: The number of times to sample. - prng: An `np.random.Generator` to draw from for this call instead of the internal random state. + prng: An `np.random.Generator` to draw from for this call + instead of the internal random state. Returns: Result list for this run; one for each possible parameter resolver. @@ -271,7 +274,8 @@ async def run_sweep_async( program: The circuit to sample from. params: Parameters to run with the program. repetitions: The number of times to sample. - prng: An `np.random.Generator` to draw from for this call instead of the internal random state. + prng: An `np.random.Generator` to draw from for this call + instead of the internal random state. Returns: Result list for this run; one for each possible parameter resolver. @@ -312,7 +316,8 @@ async def run_batch_async( repetitions: Number of circuit repetitions to run. Can be specified as a single value to use for all runs, or as a list of values, one for each circuit. - prng: An `np.random.Generator` to draw from for this call instead of the internal random state. + prng: An `np.random.Generator` to draw from for this call + instead of the internal random state. Returns: A list of lists of TrialResults. The outer list corresponds to @@ -327,22 +332,12 @@ async def run_batch_async( params_list, repetitions = self._normalize_batch_args(programs, params_list, repetitions) if prng is None: return await duet.pstarmap_async( - self.run_sweep_async, - zip( - programs, - params_list, - repetitions - ), + self.run_sweep_async, zip(programs, params_list, repetitions) ) else: return await duet.pstarmap_async( self.run_sweep_async, - zip( - programs, - params_list, - repetitions, - prng.spawn(len(programs)) - ), + zip(programs, params_list, repetitions, prng.spawn(len(programs))), ) run_batch = duet.sync(run_batch_async) diff --git a/cirq-core/cirq/work/sampler_test.py b/cirq-core/cirq/work/sampler_test.py index 8bc6fbc9dd9..0bb63c4bc92 100644 --- a/cirq-core/cirq/work/sampler_test.py +++ b/cirq-core/cirq/work/sampler_test.py @@ -52,7 +52,7 @@ async def test_run_sweep_async() -> None: @duet.sync async def test_sampler_async_fail() -> None: class FailingSampler(cirq.Sampler): - def run_sweep(self, program, params, repetitions: int = 1): + def run_sweep(self, program, params, repetitions: int = 1): # type: ignore[override] raise ValueError('test') with pytest.raises(ValueError, match='test'): @@ -66,7 +66,7 @@ def test_run_sweep_impl() -> None: """Test run_sweep implemented in terms of run_sweep_async.""" class AsyncSampler(cirq.Sampler): - async def run_sweep_async(self, program, params, repetitions: int = 1): + async def run_sweep_async(self, program, params, repetitions: int = 1): # type: ignore[override] await duet.sleep(0.001) return cirq.Simulator().run_sweep(program, params, repetitions) @@ -85,7 +85,7 @@ async def test_run_sweep_async_impl() -> None: """Test run_sweep_async implemented in terms of run_sweep.""" class SyncSampler(cirq.Sampler): - def run_sweep(self, program, params, repetitions: int = 1): + def run_sweep(self, program, params, repetitions: int = 1): # type: ignore[override] return cirq.Simulator().run_sweep(program, params, repetitions) results = await SyncSampler().run_sweep_async( @@ -226,7 +226,7 @@ async def test_run_batch_async_calls_run_sweep_asynchronously() -> None: params_list = [params1, params2] class AsyncSampler(cirq.Sampler): - async def run_sweep_async( + async def run_sweep_async( # type: ignore[override] self, program, params, repetitions: int = 1, unused: duet.Limiter = duet.Limiter(None) ): if params == params1: @@ -288,7 +288,7 @@ class DeterministicImbalancedStateSampler(cirq.Sampler): probabilities of the |0) and |1) state. """ - def run_sweep( + def run_sweep( # type: ignore[override] self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 ) -> Sequence[cirq.Result]: results = np.zeros((repetitions, 1), dtype=bool) @@ -393,3 +393,28 @@ def test_sampler_simple_sample_expectation_requirements() -> None: circuit.append(cirq.measure(a, key='out')) with pytest.raises(ValueError, match='permit_terminal_measurements'): _ = sampler.sample_expectation_values(circuit, [obs], num_samples=1) + + +def test_run_batch_uses_independent_generators() -> None: + """Tests that `run_batch` uses `spawn` on the prng argument to generate independent generators, + so all results will also be independent.""" + + a = cirq.LineQubit(0) + sampler = cirq.Simulator(seed=1) + circuit = cirq.Circuit(cirq.H(a), cirq.measure(a, key='m')) + + batch = [ + r[0].records['m'] + for r in sampler.run_batch([circuit] * 3, repetitions=50, prng=np.random.default_rng(0)) + ] + + expected = [ + sampler.run(circuit, repetitions=50, prng=prng).records['m'] + for prng in np.random.default_rng(0).spawn(3) + ] + + for b, e in zip(batch, expected): + assert np.array_equal(b, e) + + assert not np.array_equal(batch[0], batch[1]) + diff --git a/cirq-core/cirq/work/zeros_sampler.py b/cirq-core/cirq/work/zeros_sampler.py index cdd9da7944c..d774a65d2fc 100644 --- a/cirq-core/cirq/work/zeros_sampler.py +++ b/cirq-core/cirq/work/zeros_sampler.py @@ -38,7 +38,11 @@ def __init__(self, device: devices.Device | None = None): self.device = device def run_sweep( - self, program: cirq.AbstractCircuit, params: study.Sweepable, repetitions: int = 1 + self, + program: cirq.AbstractCircuit, + params: study.Sweepable, + repetitions: int = 1, + prng: np.random.Generator | None = None, ) -> list[study.Result]: """Samples circuit as if every measurement resulted in zero. @@ -46,6 +50,7 @@ def run_sweep( program: The circuit to sample from. params: Parameters to run with the program. repetitions: The number of times to sample. + prng: Not supported for this class as no client-side RNG, must be None. Returns: Result list for this run; one for each possible parameter @@ -55,6 +60,8 @@ def run_sweep( ValueError: circuit is not valid for the sampler, due to invalid repeated keys or incompatibility with the sampler's device. """ + if prng is not None: + raise ValueError("ZerosSampler has no client-side RNG.") if self.device: self.device.validate_circuit(program) shapes = self._get_measurement_shapes(program) diff --git a/cirq-core/cirq/work/zeros_sampler_test.py b/cirq-core/cirq/work/zeros_sampler_test.py index 1a0b690a898..b0b148977f3 100644 --- a/cirq-core/cirq/work/zeros_sampler_test.py +++ b/cirq-core/cirq/work/zeros_sampler_test.py @@ -91,3 +91,11 @@ def test_validate_device() -> None: circuit = cirq.Circuit(cirq.measure(a), cirq.X(b)) with pytest.raises(ValueError, match=r'X\(b\) is not a measurement'): _ = sampler.run_sweep(circuit, None, 3) + + +def test_zeros_sampler_rejects_prng() -> None: + a = cirq.LineQubit(0) + circuit = cirq.Circuit(cirq.H(a), cirq.measure(a, key='m')) + + with pytest.raises(ValueError, match='RNG'): + cirq.ZerosSampler().run_sweep(circuit, None, 1, prng=np.random.default_rng(0)) diff --git a/cirq-google/cirq_google/engine/processor_sampler.py b/cirq-google/cirq_google/engine/processor_sampler.py index 0ffca4ffaba..3a143b26597 100644 --- a/cirq-google/cirq_google/engine/processor_sampler.py +++ b/cirq-google/cirq_google/engine/processor_sampler.py @@ -14,7 +14,6 @@ from __future__ import annotations -import itertools from collections.abc import Mapping, Sequence from typing import cast, TYPE_CHECKING @@ -159,11 +158,7 @@ async def run_batch_async( if prng is None: all_batch_results = await duet.pstarmap_async( self.run_sweep_async, - zip( - program_batches, - params_list_batches, - repetition_batches - ), + zip(program_batches, params_list_batches, repetition_batches), ) else: all_batch_results = await duet.pstarmap_async( @@ -172,7 +167,7 @@ async def run_batch_async( program_batches, params_list_batches, repetition_batches, - prng.spawn(len(program_batches)) + prng.spawn(len(program_batches)), ), ) final_results = [] diff --git a/cirq-ionq/cirq_ionq/sampler.py b/cirq-ionq/cirq_ionq/sampler.py index 0e83f413133..8a59fe7d075 100644 --- a/cirq-ionq/cirq_ionq/sampler.py +++ b/cirq-ionq/cirq_ionq/sampler.py @@ -18,12 +18,15 @@ from collections.abc import Sequence from typing import TYPE_CHECKING +import numpy as np + import cirq from cirq_ionq import results if TYPE_CHECKING: import cirq_ionq + class Sampler(cirq.Sampler): """A sampler that works against the IonQ API. @@ -68,7 +71,11 @@ def __init__( self._timeout_seconds = timeout_seconds def run_sweep( - self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 + self, + program: cirq.AbstractCircuit, + params: cirq.Sweepable, + repetitions: int = 1, + prng: np.random.Generator | None = None, ) -> Sequence[cirq.Result]: """Samples from the given Circuit. @@ -87,11 +94,14 @@ def run_sweep( program: The circuit to sample from. params: Parameters to run with the program. repetitions: The number of times to sample. + prng: Not supported for this class as no client-side RNG, must be None. Returns: Either a list of `cirq_ionq.QPUResult` or a list of `cirq_ionq.SimulatorResult` depending on whether the job was running on an actual quantum processor or a simulator. """ + if prng is not None: + raise ValueError("Ionq Sampler has no client-side RNG.") resolvers = list(cirq.to_resolvers(params)) jobs = [ self._service.create_job( diff --git a/cirq-ionq/cirq_ionq/sampler_test.py b/cirq-ionq/cirq_ionq/sampler_test.py index 7ebe99250b4..30c4440ec9f 100644 --- a/cirq-ionq/cirq_ionq/sampler_test.py +++ b/cirq-ionq/cirq_ionq/sampler_test.py @@ -16,7 +16,9 @@ from unittest import mock +import numpy as np import pandas as pd +import pytest import sympy as sp import cirq @@ -189,3 +191,14 @@ def test_sampler_run_sweep_batched_job_results(): # result1 counts={0: 4}, so we expect four 0s. # measurements is (repetitions, qubits), so [[0], [0], [0], [0]] assert results[0].measurements['a'].tolist() == [[0], [0], [0], [0]] + + +def test_ionq_sampler_rejects_prng() -> None: + a = cirq.LineQubit(0) + circuit = cirq.Circuit(cirq.H(a), cirq.measure(a, key='m')) + + mock_service = mock.MagicMock() + sampler = ionq.Sampler(service=mock_service, target='qpu') + + with pytest.raises(ValueError, match='RNG'): + sampler.run_sweep(circuit, None, 1, prng=np.random.default_rng(0)) diff --git a/cirq-pasqal/cirq_pasqal/pasqal_sampler.py b/cirq-pasqal/cirq_pasqal/pasqal_sampler.py index bdf50f65bc0..1a1c06aa520 100644 --- a/cirq-pasqal/cirq_pasqal/pasqal_sampler.py +++ b/cirq-pasqal/cirq_pasqal/pasqal_sampler.py @@ -17,11 +17,13 @@ import time from typing import TYPE_CHECKING +import numpy as np import requests import cirq import cirq_pasqal + class PasqalSampler(cirq.work.Sampler): def __init__( self, @@ -103,7 +105,11 @@ def _send_serialized_circuit( return result def run_sweep( - self, program: cirq.AbstractCircuit, params: cirq.study.Sweepable, repetitions: int = 1 + self, + program: cirq.AbstractCircuit, + params: cirq.study.Sweepable, + repetitions: int = 1, + prng: np.random.Generator | None = None, ) -> list[cirq.study.Result]: """Samples from the given Circuit. In contrast to run, this allows for sweeping over different parameter @@ -112,10 +118,13 @@ def run_sweep( program: The circuit to simulate. params: Parameters to run with the program. repetitions: The number of repetitions to simulate. + prng: Not supported for this class as no client-side RNG, must be None. Returns: Result list for this run; one for each possible parameter resolver. """ + if prng is not None: + raise ValueError("PasqalSampler has no client-side RNG.") device = self._device assert isinstance( device, cirq_pasqal.PasqalDevice diff --git a/cirq-pasqal/cirq_pasqal/pasqal_sampler_test.py b/cirq-pasqal/cirq_pasqal/pasqal_sampler_test.py index 0c1f81325da..a817ed345b5 100644 --- a/cirq-pasqal/cirq_pasqal/pasqal_sampler_test.py +++ b/cirq-pasqal/cirq_pasqal/pasqal_sampler_test.py @@ -112,3 +112,15 @@ def test_run_sweep(mock_post, mock_get): # so none of them may turn off TLS certificate verification. for call in [*mock_post.call_args_list, *mock_get.call_args_list]: assert call[1].get('verify', True) is not False + + +def test_pasqal_sampler_rejects_prng() -> None: + a = cirq.LineQubit.range(3) + circuit = cirq.Circuit(cirq.H(a), cirq.measure(a, key='m')) + + qs = cirq.NamedQubit.range(3, prefix='q') + device = cirq_pasqal.PasqalVirtualDevice(control_radius=1, qubits=qs) + sampler = _make_sampler(device) + + with pytest.raises(ValueError, match='RNG'): + sampler.run_sweep(circuit, None, 1, prng=np.random.default_rng(0)) From beb2f07e682347d4e9ef59907ecf570c6a81e1b3 Mon Sep 17 00:00:00 2001 From: Advayth Pashupati <113481915+AamindMandragora@users.noreply.github.com> Date: Sat, 29 Aug 2026 15:44:13 -0500 Subject: [PATCH 4/5] okay i got them all --- cirq-core/cirq/sim/simulator_base.py | 5 +- cirq-core/cirq/sim/simulator_base_test.py | 39 ++++ cirq-core/cirq/sim/simulator_test.py | 175 ++++++++++++++++++ cirq-core/cirq/work/sampler_test.py | 59 ++++++ .../cirq_google/engine/processor_sampler.py | 50 +++-- .../engine/processor_sampler_test.py | 16 ++ .../cirq_google/engine/validating_sampler.py | 5 +- .../engine/validating_sampler_test.py | 36 ++++ .../cirq_pasqal/pasqal_sampler_test.py | 6 +- 9 files changed, 367 insertions(+), 24 deletions(-) diff --git a/cirq-core/cirq/sim/simulator_base.py b/cirq-core/cirq/sim/simulator_base.py index 837c3bb8819..c8ec93f313c 100644 --- a/cirq-core/cirq/sim/simulator_base.py +++ b/cirq-core/cirq/sim/simulator_base.py @@ -362,10 +362,7 @@ def sweep_prefixable(op: cirq.Operation): pass assert step_result is not None sim_state = step_result._sim_state - if prng is None: - yield from super().simulate_sweep_iter(suffix, params, qubit_order, sim_state) - else: - yield from super().simulate_sweep_iter(suffix, params, qubit_order, sim_state, prng) + yield from super().simulate_sweep_iter(suffix, params, qubit_order, sim_state, prng) def _create_simulation_state( self, diff --git a/cirq-core/cirq/sim/simulator_base_test.py b/cirq-core/cirq/sim/simulator_base_test.py index 7143814cc30..641bcde15c7 100644 --- a/cirq-core/cirq/sim/simulator_base_test.py +++ b/cirq-core/cirq/sim/simulator_base_test.py @@ -464,3 +464,42 @@ def test_simulate_unresolved_parameter_raises() -> None: match=r'Circuit contains ops whose symbols were not specified in the parameter sweep\.', ): sim.simulate(circuit) + + +class _BackCompatCreateSimulationState(CountingSimulator): + """SimulatorBase before `_create_simulation_state` got the `prng` param.""" + + def _create_simulation_state( # type: ignore[override] + self, + initial_state: Any, + qubits: Sequence[cirq.Qid], + param_resolver: cirq.ParamResolver | None = None, + ) -> cirq.SimulationStateBase[CountingSimulationState]: + return super()._create_simulation_state(initial_state, qubits, param_resolver) + + +def test_simulator_base_without_prng_no_forwarding() -> None: + """`_run`, `simulate_sweep_iter` and `_base_iterator` shouldn't forward a `prng` of None.""" + + sim = _BackCompatCreateSimulationState() + circuit = cirq.Circuit(cirq.X(q0), cirq.measure(q0, key='m')) + + np.testing.assert_equal(sim.run(circuit, repetitions=2).records['m'], np.ones((2, 1, 1))) + r = sim.simulate(circuit) + assert isinstance(r._final_simulator_state, CountingSimulationState) + assert r._final_simulator_state.gate_count == 1 + assert len(list(sim.simulate_moment_steps(circuit))) == 2 + + +def test_passing_prng_to_simulator_base_without_prng_fails() -> None: + """Simulators that don't use `prng` should throw an error upon receiving it.""" + + sim = _BackCompatCreateSimulationState() + circuit = cirq.Circuit(cirq.X(q0), cirq.measure(q0, key='m')) + + with pytest.raises(TypeError, match='_create_simulation_state'): + sim.run(circuit, repetitions=2, prng=np.random.default_rng(0)) + with pytest.raises(TypeError, match='_create_simulation_state'): + sim.simulate(circuit, prng=np.random.default_rng(0)) + with pytest.raises(TypeError, match='_create_simulation_state'): + next(sim.simulate_moment_steps(circuit, prng=np.random.default_rng(0))) diff --git a/cirq-core/cirq/sim/simulator_test.py b/cirq-core/cirq/sim/simulator_test.py index 0f3d040f62a..b174c975d42 100644 --- a/cirq-core/cirq/sim/simulator_test.py +++ b/cirq-core/cirq/sim/simulator_test.py @@ -591,3 +591,178 @@ def test_sampler_without_prng_uses_internal_state() -> None: res2 = sampler.run(circuit, repetitions=50).records['m'] assert not np.array_equal(res1, res2) + + +class _BackCompatSimulatesSamples(SimulatesSamples): + """SimulatesSamples before `_run` got the `prng` param.""" + + def _run( # type: ignore[override] + self, circuit: cirq.AbstractCircuit, param_resolver: cirq.ParamResolver, repetitions: int + ) -> dict[str, np.ndarray]: + return {'m': np.ones((repetitions, 1, 1), dtype=np.uint8)} + + +class _BackCompatRunSweepIter(_BackCompatSimulatesSamples): + """SimulatesSamples overriding `run_sweep_iter` before it got the `prng` param.""" + + def run_sweep_iter( # type: ignore[override] + self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 + ) -> Iterator[cirq.Result]: + return super().run_sweep_iter(program, params, repetitions) + + +@pytest.mark.parametrize('simulator', [_BackCompatSimulatesSamples(), _BackCompatRunSweepIter()]) +def test_simulates_samples_without_prng_no_forwarding(simulator: SimulatesSamples) -> None: + """`run_sweep` and `run_sweep_iter` shouldn't forward `prng` when the caller didn't pass one in.""" + + a = cirq.LineQubit(0) + circuit = cirq.Circuit(cirq.X(a), cirq.measure(a, key='m')) + expected = np.ones((2, 1, 1)) + + np.testing.assert_equal(simulator.run(circuit, repetitions=2).records['m'], expected) + np.testing.assert_equal(simulator.run_sweep(circuit, None, 2)[0].records['m'], expected) + np.testing.assert_equal(next(simulator.run_sweep_iter(circuit, None, 2)).records['m'], expected) + + +@pytest.mark.parametrize('simulator', [_BackCompatSimulatesSamples(), _BackCompatRunSweepIter()]) +def test_passing_prng_to_simulates_samples_without_prng_fails(simulator: SimulatesSamples) -> None: + """Simulators that don't use `prng` should throw an error upon receiving it.""" + + a = cirq.LineQubit(0) + circuit = cirq.Circuit(cirq.X(a), cirq.measure(a, key='m')) + + with pytest.raises(TypeError, match='_run|run_sweep_iter'): + simulator.run(circuit, repetitions=2, prng=np.random.default_rng(0)) + + +class _BackCompatSimulateSweep(SimulatesFinalState): + """SimulatesFinalState before `simulate_sweep` got the `prng` param.""" + + def simulate_sweep( # type: ignore[override] + self, + program: cirq.AbstractCircuit, + params: study.Sweepable, + qubit_order: cirq.QubitOrderOrList = cirq.QubitOrder.DEFAULT, + initial_state: Any = None, + ) -> list[SimulationTrialResult]: + return [ + SimulationTrialResult( + params=cirq.ParamResolver(), measurements={}, final_simulator_state=[] + ) + ] + + +class _BackCompatSimulateSweepIter(SimulatesFinalState): + """SimulatesFinalState before `simulate_sweep_iter` got the `prng` param.""" + + def simulate_sweep_iter( # type: ignore[override] + self, + program: cirq.AbstractCircuit, + params: study.Sweepable, + qubit_order: cirq.QubitOrderOrList = cirq.QubitOrder.DEFAULT, + initial_state: Any = None, + ) -> Iterator[SimulationTrialResult]: + yield SimulationTrialResult( + params=cirq.ParamResolver(), measurements={}, final_simulator_state=[] + ) + + +@pytest.mark.parametrize('simulator', [_BackCompatSimulateSweep(), _BackCompatSimulateSweepIter()]) +def test_simulates_final_state_without_prng_no_forwarding(simulator: SimulatesFinalState) -> None: + """`simulate` and `simulate_sweep` shouldn't forward `prng` when the caller didn't pass one in.""" + + a = cirq.LineQubit(0) + circuit = cirq.Circuit(cirq.X(a)) + + assert simulator.simulate(circuit) == SimulationTrialResult( + params=cirq.ParamResolver(), measurements={}, final_simulator_state=[] + ) + assert simulator.simulate_sweep(circuit, None) == [ + SimulationTrialResult( + params=cirq.ParamResolver(), measurements={}, final_simulator_state=[] + ) + ] + assert next(simulator.simulate_sweep_iter(circuit, None)) == SimulationTrialResult( + params=cirq.ParamResolver(), measurements={}, final_simulator_state=[] + ) + + +@pytest.mark.parametrize('simulator', [_BackCompatSimulateSweep(), _BackCompatSimulateSweepIter()]) +def test_passing_prng_to_simulates_final_state_without_prng_fails( + simulator: SimulatesFinalState, +) -> None: + """Simulators that don't use `prng` should throw an error upon receiving it.""" + + a = cirq.LineQubit(0) + circuit = cirq.Circuit(cirq.X(a)) + + with pytest.raises(TypeError, match='simulate_sweep'): + simulator.simulate(circuit, prng=np.random.default_rng(0)) + + +class _BackCompatStepResult(cirq.StepResult): + """StepResult for the below intermediate state simulator.""" + + def __init__(self) -> None: + self._measurements: dict[str, np.ndarray] = {'m': np.ones(1, dtype=np.uint8)} + + def _simulator_state(self) -> None: + return None + + def sample(self, qubits, repetitions=1, seed=None) -> np.ndarray: + return np.ones((repetitions, len(qubits)), dtype=np.uint8) # pragma: no cover + + +class _BackCompatIntermediateState(SimulatesIntermediateStateImpl): + """SimulatesIntermediateState before `_base_iterator` got the `prng` param.""" + + def _base_iterator( # type: ignore[override] + self, + circuit: cirq.AbstractCircuit, + qubits: tuple[cirq.Qid, ...], + initial_state: Any, + param_resolver: cirq.ParamResolver | None = None, + ) -> Iterator[_BackCompatStepResult]: + yield _BackCompatStepResult() + + +class _BackCompatMomentSteps(_BackCompatIntermediateState): + """SimulatesIntermediateState overriding `simulate_moment_steps` before `prng`.""" + + def simulate_moment_steps( # type: ignore[override] + self, + circuit: cirq.AbstractCircuit, + param_resolver: cirq.ParamResolverOrSimilarType = None, + qubit_order: cirq.QubitOrderOrList = cirq.QubitOrder.DEFAULT, + initial_state: Any = None, + ) -> Iterator[_BackCompatStepResult]: + return super().simulate_moment_steps(circuit, param_resolver, qubit_order, initial_state) + + +@pytest.mark.parametrize('simulator', [_BackCompatIntermediateState(), _BackCompatMomentSteps()]) +def test_simulates_intermediate_state_without_prng_no_forwarding(simulator) -> None: + """`simulate_sweep_iter` and `simulate_moment_steps` shouldn't forward `prng` either.""" + + a = cirq.LineQubit(0) + circuit = cirq.Circuit(cirq.X(a)) + expected = {'m': np.ones(1, dtype=np.uint8)} + + np.testing.assert_equal(simulator.simulate(circuit).measurements, expected) + np.testing.assert_equal(simulator.simulate_sweep(circuit, None)[0].measurements, expected) + np.testing.assert_equal( + next(simulator.simulate_sweep_iter(circuit, None)).measurements, expected + ) + np.testing.assert_equal(next(simulator.simulate_moment_steps(circuit)).measurements, expected) + + +@pytest.mark.parametrize('simulator', [_BackCompatIntermediateState(), _BackCompatMomentSteps()]) +def test_passing_prng_to_simulates_intermediate_state_without_prng_fails(simulator) -> None: + """Simulators that don't use `prng` should throw an error upon receiving it.""" + + a = cirq.LineQubit(0) + circuit = cirq.Circuit(cirq.X(a)) + + with pytest.raises(TypeError, match='_base_iterator|simulate_moment_steps'): + simulator.simulate(circuit, prng=np.random.default_rng(0)) + with pytest.raises(TypeError, match='_base_iterator|simulate_moment_steps'): + next(simulator.simulate_moment_steps(circuit, prng=np.random.default_rng(0))) diff --git a/cirq-core/cirq/work/sampler_test.py b/cirq-core/cirq/work/sampler_test.py index 0bb63c4bc92..5452485f489 100644 --- a/cirq-core/cirq/work/sampler_test.py +++ b/cirq-core/cirq/work/sampler_test.py @@ -418,3 +418,62 @@ def test_run_batch_uses_independent_generators() -> None: assert not np.array_equal(batch[0], batch[1]) + +class _BackCompatSampler(cirq.Sampler): + """Sampler before run_sweep got the `prng` param.""" + + def run_sweep( # type: ignore[override] + self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 + ) -> Sequence[cirq.Result]: + return cirq.Simulator().run_sweep(program, params, repetitions) + + +class _BackCompatAsyncSampler(cirq.Sampler): + """Sampler before run_sweep_async got the `prng` param.""" + + async def run_sweep_async( # type: ignore[override] + self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 + ) -> Sequence[cirq.Result]: + return cirq.Simulator().run_sweep(program, params, repetitions) + + +@pytest.mark.parametrize('sampler', [_BackCompatSampler(), _BackCompatAsyncSampler()]) +def test_sampler_without_prng_no_forwarding(sampler: cirq.Sampler) -> None: + """`run` and `run_sweep` shouldn't forward `prng` when the caller didn't pass one in.""" + + a = cirq.LineQubit(0) + circuit = cirq.Circuit(cirq.X(a), cirq.measure(a, key='m')) + expected = np.ones((2, 1, 1)) + np.testing.assert_equal(sampler.run(circuit, repetitions=2).records['m'], expected) + np.testing.assert_equal(sampler.run_sweep(circuit, None, 2)[0].records['m'], expected) + assert len(sampler.sample(circuit, repetitions=2)) == 2 + assert len(sampler.run_batch([circuit], repetitions=2)) == 1 + + +@pytest.mark.parametrize('sampler', [_BackCompatSampler(), _BackCompatAsyncSampler()]) +@duet.sync +async def test_sampler_without_prng_no_forwarding_async(sampler: cirq.Sampler) -> None: + """`run_async` and `run_batch_async` shouldn't forward `prng` either.""" + + a = cirq.LineQubit(0) + circuit = cirq.Circuit(cirq.X(a), cirq.measure(a, key='m')) + result = await sampler.run_async(circuit, repetitions=2) + np.testing.assert_equal(result.records['m'], np.ones((2, 1, 1))) + assert len(await sampler.run_batch_async([circuit], repetitions=2)) == 1 + + +@pytest.mark.parametrize('sampler', [_BackCompatSampler(), _BackCompatAsyncSampler()]) +@duet.sync +async def test_passing_prng_to_sampler_without_prng_fails(sampler: cirq.Sampler) -> None: + """Samplers that don't use `prng` should throw an error upon receiving it.""" + + a = cirq.LineQubit(0) + circuit = cirq.Circuit(cirq.X(a), cirq.measure(a, key='m')) + with pytest.raises(TypeError, match='run_sweep'): + sampler.run(circuit, repetitions=2, prng=np.random.default_rng(0)) + with pytest.raises(TypeError, match='run_sweep'): + sampler.sample(circuit, repetitions=2, prng=np.random.default_rng(0)) + with pytest.raises(TypeError, match='run_sweep'): + await sampler.run_async(circuit, repetitions=2, prng=np.random.default_rng(0)) + with pytest.raises(TypeError, match='run_sweep'): + await sampler.run_batch_async([circuit], repetitions=2, prng=np.random.default_rng(0)) diff --git a/cirq-google/cirq_google/engine/processor_sampler.py b/cirq-google/cirq_google/engine/processor_sampler.py index 3a143b26597..cee97e076ca 100644 --- a/cirq-google/cirq_google/engine/processor_sampler.py +++ b/cirq-google/cirq_google/engine/processor_sampler.py @@ -86,6 +86,22 @@ async def run_sweep_async( repetitions: int = 1, prng: np.random.Generator | None = None, ) -> Sequence[cg.EngineResult]: + """Samples the given circuit on the processor. + + Args: + program: The circuit to sample from. + params: Parameters to run with the program. + repetitions: The number of times to sample. + prng: Not supported for this class as no client-side RNG, must be None. + + Returns: + Result list for this run; one for each possible parameter resolver. + + Raises: + ValueError: If `prng` is not None, as randomness is applied by the processor. + """ + if prng is not None: + raise ValueError("ProcessorSampler has no client-side RNG.") return await self._run_sweep_async(program, params, repetitions) run_sweep = duet.sync(run_sweep_async) @@ -120,6 +136,22 @@ async def run_batch_async( repetitions: int | Sequence[int] = 1, prng: np.random.Generator | None = None, ) -> Sequence[Sequence[cg.EngineResult]]: + """Runs the supplied circuits on the processor. + + Args: + programs: The circuits to sample from. + params_list: Parameters to run with each circuit. + repetitions: Number of times to sample each circuit. + prng: Not supported for this class as no client-side RNG, must be None. + + Returns: + A list of lists of Results, one for each circuit. + + Raises: + ValueError: If `prng` is not None, as randomness is applied by the processor. + """ + if prng is not None: + raise ValueError("ProcessorSampler has no client-side RNG.") if self._jobs_per_batch > 1: # Treat programs as a sequence for iteration, but keep keys if it's a mapping prog_keys = list(programs.keys()) if isinstance(programs, Mapping) else [] @@ -155,21 +187,9 @@ async def run_batch_async( program_batches.append(batch_programs) params_list_batches.append(batch_sweep) repetition_batches.append(batch_reps) - if prng is None: - all_batch_results = await duet.pstarmap_async( - self.run_sweep_async, - zip(program_batches, params_list_batches, repetition_batches), - ) - else: - all_batch_results = await duet.pstarmap_async( - self.run_sweep_async, - zip( - program_batches, - params_list_batches, - repetition_batches, - prng.spawn(len(program_batches)), - ), - ) + all_batch_results = await duet.pstarmap_async( + self.run_sweep_async, zip(program_batches, params_list_batches, repetition_batches) + ) final_results = [] for batch_res, batch_progs in zip(all_batch_results, program_batches): num_progs = len(batch_progs) diff --git a/cirq-google/cirq_google/engine/processor_sampler_test.py b/cirq-google/cirq_google/engine/processor_sampler_test.py index 1c0c6258e23..cebd216adec 100644 --- a/cirq-google/cirq_google/engine/processor_sampler_test.py +++ b/cirq-google/cirq_google/engine/processor_sampler_test.py @@ -430,3 +430,19 @@ async def test_run_batch_error_divisible(): ValueError, match="Engine returned 3 results, which is not divisible by 2 programs." ): await sampler.run_batch_async([circuit1, circuit2], [{}, {}], 5) + + +@pytest.mark.parametrize('jobs_per_batch', [1, 2]) +def test_processor_sampler_rejects_prng(jobs_per_batch) -> None: + """The processor applies its own randomness, so a client-side `prng` must be refused.""" + + processor = mock.create_autospec(AbstractProcessor, instance=True) + sampler = cg.ProcessorSampler(processor=processor, jobs_per_batch=jobs_per_batch) + + a = cirq.LineQubit(0) + circuit = cirq.Circuit(cirq.X(a), cirq.measure(a, key='m')) + + with pytest.raises(ValueError, match='no client-side RNG'): + sampler.run_sweep(circuit, None, 2, prng=np.random.default_rng(0)) + with pytest.raises(ValueError, match='no client-side RNG'): + sampler.run_batch([circuit], None, 2, prng=np.random.default_rng(0)) diff --git a/cirq-google/cirq_google/engine/validating_sampler.py b/cirq-google/cirq_google/engine/validating_sampler.py index 15ab527b5f5..df753498969 100644 --- a/cirq-google/cirq_google/engine/validating_sampler.py +++ b/cirq-google/cirq_google/engine/validating_sampler.py @@ -75,7 +75,10 @@ def run_sweep( prng: np.random.Generator | None = None, ) -> Sequence[cirq.Result]: self._validate_circuit([program], [params], repetitions) - return self._sampler.run_sweep(program, params, repetitions, prng) + if prng is None: + return self._sampler.run_sweep(program, params, repetitions) + else: + return self._sampler.run_sweep(program, params, repetitions, prng) async def run_batch_async( self, diff --git a/cirq-google/cirq_google/engine/validating_sampler_test.py b/cirq-google/cirq_google/engine/validating_sampler_test.py index ed8686f12f7..e1412568342 100644 --- a/cirq-google/cirq_google/engine/validating_sampler_test.py +++ b/cirq-google/cirq_google/engine/validating_sampler_test.py @@ -13,6 +13,8 @@ # limitations under the License. from __future__ import annotations +from collections.abc import Sequence + import numpy as np import pytest import sympy @@ -103,3 +105,37 @@ def test_batch_default_sweeps(): results = sampler.run_batch(circuits, None, repetitions=100) assert np.all(results[0][0].measurements['m'] == 1) assert np.all(results[1][0].measurements['m2'] == 0) + + +class _BackCompatSampler(cirq.Sampler): + """Sampler before run_sweep got the `prng` param.""" + + def run_sweep( # type: ignore[override] + self, program: cirq.AbstractCircuit, params: cirq.Sweepable, repetitions: int = 1 + ) -> Sequence[cirq.Result]: + return cirq.Simulator().run_sweep(program, params, repetitions) + + +def test_validating_sampler_without_prng_no_forwarding() -> None: + """`run_sweep` and `run_batch` shouldn't forward `prng` when the caller didn't pass one in.""" + + sampler = cg.ValidatingSampler(sampler=_BackCompatSampler()) + q = cirq.GridQubit(2, 2) + circuit = cirq.Circuit(cirq.X(q), cirq.measure(q, key='m')) + expected = np.ones((2, 1, 1)) + + np.testing.assert_equal(sampler.run_sweep(circuit, None, 2)[0].records['m'], expected) + np.testing.assert_equal(sampler.run_batch([circuit], None, 2)[0][0].records['m'], expected) + + +def test_passing_prng_to_validating_sampler_without_prng_fails() -> None: + """Samplers that don't use `prng` should throw an error upon receiving it.""" + + sampler = cg.ValidatingSampler(sampler=_BackCompatSampler()) + q = cirq.GridQubit(2, 2) + circuit = cirq.Circuit(cirq.X(q), cirq.measure(q, key='m')) + + with pytest.raises(TypeError, match='run_sweep'): + sampler.run_sweep(circuit, None, 2, prng=np.random.default_rng(0)) + with pytest.raises(TypeError, match='run_sweep'): + sampler.run_batch([circuit], None, 2, prng=np.random.default_rng(0)) diff --git a/cirq-pasqal/cirq_pasqal/pasqal_sampler_test.py b/cirq-pasqal/cirq_pasqal/pasqal_sampler_test.py index a817ed345b5..7584a1a06c9 100644 --- a/cirq-pasqal/cirq_pasqal/pasqal_sampler_test.py +++ b/cirq-pasqal/cirq_pasqal/pasqal_sampler_test.py @@ -115,10 +115,8 @@ def test_run_sweep(mock_post, mock_get): def test_pasqal_sampler_rejects_prng() -> None: - a = cirq.LineQubit.range(3) - circuit = cirq.Circuit(cirq.H(a), cirq.measure(a, key='m')) - - qs = cirq.NamedQubit.range(3, prefix='q') + qs = [cirq_pasqal.ThreeDQubit(i, j, 0) for i in range(3) for j in range(3)] + circuit = cirq.Circuit(cirq.X(qs[0]), cirq.measure(qs[0], key='m')) device = cirq_pasqal.PasqalVirtualDevice(control_radius=1, qubits=qs) sampler = _make_sampler(device) From ecbe2ee93f307c898447518c1a590511432a9513 Mon Sep 17 00:00:00 2001 From: Advayth Pashupati <113481915+AamindMandragora@users.noreply.github.com> Date: Sat, 29 Aug 2026 15:49:29 -0500 Subject: [PATCH 5/5] oops missed these --- cirq-core/cirq/sim/simulator_base_test.py | 13 ++++++++++++ cirq-core/cirq/value/random_state_test.py | 26 +++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/cirq-core/cirq/sim/simulator_base_test.py b/cirq-core/cirq/sim/simulator_base_test.py index 641bcde15c7..ead4caf51a7 100644 --- a/cirq-core/cirq/sim/simulator_base_test.py +++ b/cirq-core/cirq/sim/simulator_base_test.py @@ -503,3 +503,16 @@ def test_passing_prng_to_simulator_base_without_prng_fails() -> None: sim.simulate(circuit, prng=np.random.default_rng(0)) with pytest.raises(TypeError, match='_create_simulation_state'): next(sim.simulate_moment_steps(circuit, prng=np.random.default_rng(0))) + + +@pytest.mark.parametrize('split_untangled_states', [False, True]) +def test_create_simulation_state_forwards_prng(split_untangled_states) -> None: + """A `prng` passed in should reach the simulation states built for the run.""" + + sim = cirq.Simulator(split_untangled_states=split_untangled_states) + prng = np.random.default_rng(0) + initial_state = np.array([1, 0, 0, 0], dtype=np.complex64) + + state = sim._create_simulation_state(initial_state, (q0, q1), prng=prng) + + assert state.create_merged_state().prng is prng diff --git a/cirq-core/cirq/value/random_state_test.py b/cirq-core/cirq/value/random_state_test.py index 852c90374d4..b4e0318016a 100644 --- a/cirq-core/cirq/value/random_state_test.py +++ b/cirq-core/cirq/value/random_state_test.py @@ -46,3 +46,29 @@ def rand(prng): vals = [random_state.get_random_array(prng) for prng in prngs1] eq = cirq.testing.EqualsTester() eq.add_equality_group(*vals) + + +def test_get_random_helpers_with_generator() -> None: + """The helpers should use the `np.random.Generator` spelling of each draw.""" + + prng = np.random.default_rng(0) + + assert random_state.get_random_array(prng, (2, 3)).shape == (2, 3) + assert isinstance(random_state.get_random_array(prng), float) + assert random_state.get_random_normal_array(prng, (2, 3)).shape == (2, 3) + assert isinstance(random_state.get_random_normal_array(prng), float) + assert random_state.get_random_int(prng, 0, 10, size=(2,)).shape == (2,) + assert 0 <= random_state.get_random_int(prng, 10) < 10 + + +def test_get_random_helpers_with_random_state() -> None: + """The helpers should use the `np.random.RandomState` spelling of each draw.""" + + prng = np.random.RandomState(0) + + assert random_state.get_random_array(prng, (2, 3)).shape == (2, 3) + assert isinstance(random_state.get_random_array(prng), float) + assert random_state.get_random_normal_array(prng, (2, 3)).shape == (2, 3) + assert isinstance(random_state.get_random_normal_array(prng), float) + assert random_state.get_random_int(prng, 0, 10, size=(2,)).shape == (2,) + assert 0 <= random_state.get_random_int(prng, 10) < 10