diff --git a/cirq-core/cirq/contrib/paulistring/pauli_string_measurement_with_readout_mitigation.py b/cirq-core/cirq/contrib/paulistring/pauli_string_measurement_with_readout_mitigation.py index 787b40a9566..23e3ce47717 100644 --- a/cirq-core/cirq/contrib/paulistring/pauli_string_measurement_with_readout_mitigation.py +++ b/cirq-core/cirq/contrib/paulistring/pauli_string_measurement_with_readout_mitigation.py @@ -23,8 +23,8 @@ import attrs import numpy as np -from cirq import circuits, ops, work -from cirq.contrib.shuffle_circuits import run_shuffled_with_readout_benchmarking +import cirq.contrib.shuffle_circuits.shuffle_circuits_with_readout_benchmarking as sc_readout +from cirq import circuits, ops, study, work from cirq.experiments.readout_confusion_matrix import TensoredConfusionMatrices if TYPE_CHECKING: @@ -288,7 +288,7 @@ def _build_many_one_qubits_empty_confusion_matrix(qubits_length: int) -> list[np def _process_pauli_measurement_results( qubits: Sequence[ops.Qid], pauli_string_groups: list[list[ops.PauliString]], - circuit_results: list[ResultDict], + circuit_results: list[ResultDict] | Sequence[study.Result], calibration_results: dict[tuple[ops.Qid, ...], SingleQubitReadoutCalibrationResult], pauli_repetitions: int, timestamp: float, @@ -474,14 +474,18 @@ def measure_pauli_strings( pauli_measurement_circuits.extend(basis_change_circuits) # Run shuffled benchmarking for readout calibration - circuits_results, calibration_results = run_shuffled_with_readout_benchmarking( - input_circuits=pauli_measurement_circuits, - sampler=sampler, - circuit_repetitions=pauli_repetitions, - rng_or_seed=rng_or_seed, - qubits=[list(qubits) for qubits in qubits_list], - num_random_bitstrings=num_random_bitstrings, - readout_repetitions=readout_repetitions, + circuits_results, calibration_results = ( + sc_readout.run_shuffled_circuits_with_readout_benchmarking( + sampler=sampler, + input_circuits=pauli_measurement_circuits, + parameters=sc_readout.ReadoutBenchmarkingParams( + circuit_repetitions=pauli_repetitions, + num_random_bitstrings=num_random_bitstrings, + readout_repetitions=readout_repetitions, + ), + rng_or_seed=rng_or_seed, + qubits=[list(qubits) for qubits in qubits_list], + ) ) # Process the results to calculate expectation values diff --git a/cirq-core/cirq/contrib/paulistring/pauli_string_measurement_with_readout_mitigation_test.py b/cirq-core/cirq/contrib/paulistring/pauli_string_measurement_with_readout_mitigation_test.py index 536116fc204..6c42d317293 100644 --- a/cirq-core/cirq/contrib/paulistring/pauli_string_measurement_with_readout_mitigation_test.py +++ b/cirq-core/cirq/contrib/paulistring/pauli_string_measurement_with_readout_mitigation_test.py @@ -895,7 +895,7 @@ def test_process_pauli_measurement_results_raises_error_on_missing_calibration() _process_pauli_measurement_results( qubits, [pauli_strings], - circuit_results[0], # type: ignore[arg-type] + circuit_results[0], empty_calibration_result_dict, # type: ignore[arg-type] 1000, 1.0, diff --git a/cirq-core/cirq/contrib/shuffle_circuits/__init__.py b/cirq-core/cirq/contrib/shuffle_circuits/__init__.py index 7f97f8834a4..2c1a29702df 100644 --- a/cirq-core/cirq/contrib/shuffle_circuits/__init__.py +++ b/cirq-core/cirq/contrib/shuffle_circuits/__init__.py @@ -15,4 +15,6 @@ from cirq.contrib.shuffle_circuits.shuffle_circuits_with_readout_benchmarking import ( run_shuffled_with_readout_benchmarking as run_shuffled_with_readout_benchmarking, + run_shuffled_circuits_with_readout_benchmarking as run_shuffled_circuits_with_readout_benchmarking, # noqa: E501 + run_sweep_with_readout_benchmarking as run_sweep_with_readout_benchmarking, ) diff --git a/cirq-core/cirq/contrib/shuffle_circuits/shuffle_circuits_with_readout_benchmarking.py b/cirq-core/cirq/contrib/shuffle_circuits/shuffle_circuits_with_readout_benchmarking.py index df7fa2bdb92..033e79acc8d 100644 --- a/cirq-core/cirq/contrib/shuffle_circuits/shuffle_circuits_with_readout_benchmarking.py +++ b/cirq-core/cirq/contrib/shuffle_circuits/shuffle_circuits_with_readout_benchmarking.py @@ -17,23 +17,54 @@ from __future__ import annotations import time -from typing import TYPE_CHECKING +from typing import Optional, Sequence, TYPE_CHECKING +import attrs import numpy as np +import sympy -from cirq import circuits, ops, protocols, work +from cirq import circuits, ops, protocols, study, work +from cirq._compat import deprecated from cirq.experiments import SingleQubitReadoutCalibrationResult if TYPE_CHECKING: from cirq.study import ResultDict -def _validate_input( - input_circuits: list[circuits.Circuit], +@attrs.frozen +class ReadoutBenchmarkingParams: + """Parameters for configuring readout benchmarking. + + Attributes: + circuit_repetitions: The repetitions for `circuits`. + num_random_bitstrings: The number of random bitstrings for measuring readout. + If set to 0, no readout calibration circuits are generated. + readout_repetitions: The number of repetitions for each readout bitstring. + """ + + circuit_repetitions: int | list[int] + num_random_bitstrings: int = 100 + readout_repetitions: int = 1000 + + def __attrs_post_init__(self): + # Check circuit_repetitions + if isinstance(self.circuit_repetitions, int): + if self.circuit_repetitions <= 0: + raise ValueError("Must provide non-zero circuit_repetitions.") + + # Check num_random_bitstrings is bigger than or equal to 0 + if self.num_random_bitstrings < 0: + raise ValueError("Must provide zero or more num_random_bitstrings.") + + # Check readout_repetitions is bigger than 0 + if self.readout_repetitions <= 0: + raise ValueError("Must provide non-zero readout_repetitions for readout calibration.") + + +def _validate_experiment_input( + input_circuits: Sequence[circuits.Circuit], circuit_repetitions: int | list[int], - rng_or_seed: np.random.Generator | int, - num_random_bitstrings: int, - readout_repetitions: int, + rng_or_seed: Optional[np.random.Generator | int] = None, ): if not input_circuits: raise ValueError("Input circuits must not be empty.") @@ -45,28 +76,24 @@ def _validate_input( if not any(protocols.is_measurement(circuit) for op in circuit.all_operations()): raise ValueError("Input circuits must have measurements.") - # Check circuit_repetitions - if isinstance(circuit_repetitions, int): - if circuit_repetitions <= 0: - raise ValueError("Must provide non-zero circuit_repetitions.") if isinstance(circuit_repetitions, list) and len(circuit_repetitions) != len(input_circuits): raise ValueError("Number of circuit_repetitions must match the number of input circuits.") - # Check rng is a numpy random generator - if not isinstance(rng_or_seed, np.random.Generator) and not isinstance(rng_or_seed, int): - raise ValueError("Must provide a numpy random generator or a seed") - # Check num_random_bitstrings is bigger than or equal to 0 - if num_random_bitstrings < 0: - raise ValueError("Must provide zero or more num_random_bitstrings.") - - # Check readout_repetitions is bigger than 0 - if readout_repetitions <= 0: - raise ValueError("Must provide non-zero readout_repetitions for readout calibration.") +def _validate_experiment_input_with_sweep( + input_circuits: Sequence[circuits.Circuit], + sweep_params: Sequence[study.Sweepable], + circuit_repetitions: int | list[int], + rng_or_seed: Optional[np.random.Generator | int] = None, +): + """Validates the input for the run_sweep_with_readout_benchmarking function.""" + if not sweep_params: + raise ValueError("Sweep parameters must not be empty.") + return _validate_experiment_input(input_circuits, circuit_repetitions, rng_or_seed) def _generate_readout_calibration_circuits( - qubits: list[ops.Qid], rng: np.random.Generator, num_random_bitstrings: int + qubits: list[ops.Qid], num_random_bitstrings: int, rng: np.random.Generator ) -> tuple[list[circuits.Circuit], np.ndarray]: """Generates the readout calibration circuits with random bitstrings.""" bit_to_gate = (ops.I, ops.X) @@ -84,6 +111,95 @@ def _generate_readout_calibration_circuits( return readout_calibration_circuits, random_bitstrings +def _generate_parameterized_readout_calibration_circuit_with_sweep( + qubits: list[ops.Qid], num_random_bitstrings: int, rng: np.random.Generator +) -> tuple[circuits.Circuit, study.Sweepable, np.ndarray]: + """Generates a parameterized readout calibration circuit, sweep parameters, + and the random bitstrings. + + The function generates a single cirq.Circuit with parameterized X gates. + The function also generates a set of random bitstrings and creates a list + of sweep parameters to map the parameters in the circuit to the values in + each bitstring, allowing efficient calibration of readout errors of input qubits. + + Args: + qubits: The list of qubits to include in the calibration circuit. + num_random_bitstrings: The number of random bitstrings to generate for calibration. + rng: A numpy random number generator used to generate the random bitstrings. + + Returns: + A tuple containing: + - The parameterized readout calibration circuit (cirq.Circuit). + - A list of parameter sweeps (one for each random bitstring). + - The numpy array of generated random bitstrings. + """ + random_bitstrings = rng.integers(0, 2, size=(num_random_bitstrings, len(qubits))) + + exp_symbols = [sympy.Symbol(f'exp_{qubit}') for qubit in qubits] + parameterized_readout_calibration_circuit = circuits.Circuit( + [ops.X(qubit) ** exp for exp, qubit in zip(exp_symbols, qubits)], ops.M(*qubits, key="m") + ) + sweep_params = [] + for bitstr in random_bitstrings: + sweep_params.append({exp: bit for exp, bit in zip(exp_symbols, bitstr)}) + + return parameterized_readout_calibration_circuit, sweep_params, random_bitstrings + + +def _generate_all_readout_calibration_circuits( + num_random_bitstrings: int, + qubits_to_measure: list[list[ops.Qid]], + is_sweep: bool, + rng: np.random.Generator, +) -> tuple[list[circuits.Circuit], list[np.ndarray], list[study.Sweepable]]: + """Generates all readout calibration circuits and random bitstrings.""" + all_readout_calibration_circuits: list[circuits.Circuit] = [] + all_random_bitstrings: list[np.ndarray] = [] + all_readout_sweep_params: list[study.Sweepable] = [] + + if num_random_bitstrings <= 0: + return all_readout_calibration_circuits, all_random_bitstrings, all_readout_sweep_params + + if not is_sweep: + for qubit_group in qubits_to_measure: + readout_calibration_circuits, random_bitstrings = ( + _generate_readout_calibration_circuits(qubit_group, num_random_bitstrings, rng) + ) + all_readout_calibration_circuits.extend(readout_calibration_circuits) + all_random_bitstrings.append(random_bitstrings) + else: + for qubit_group in qubits_to_measure: + (parameterized_readout_calibration_circuit, readout_sweep_params, random_bitstrings) = ( + _generate_parameterized_readout_calibration_circuit_with_sweep( + qubit_group, num_random_bitstrings, rng + ) + ) + all_readout_calibration_circuits.append(parameterized_readout_calibration_circuit) + all_readout_sweep_params.append([readout_sweep_params]) + all_random_bitstrings.append(random_bitstrings) + + return all_readout_calibration_circuits, all_random_bitstrings, all_readout_sweep_params + + +def _determine_qubits_to_measure( + input_circuits: Sequence[circuits.Circuit], + qubits: Optional[Sequence[ops.Qid] | Sequence[Sequence[ops.Qid]]], +) -> list[list[ops.Qid]]: + """Determine the qubits to measure based on the input circuits and provided qubits.""" + # If input qubits is None, extract qubits from input circuits + qubits_to_measure: list[list[ops.Qid]] = [] + if qubits is None: + qubits_to_measure = [ + sorted(set(q for circuit in input_circuits for q in circuit.all_qubits())) + ] + + elif isinstance(qubits[0], ops.Qid): + qubits_to_measure = [qubits] # type: ignore + else: + qubits_to_measure = qubits # type: ignore + return qubits_to_measure + + def _shuffle_circuits( all_circuits: list[circuits.Circuit], all_repetitions: list[int], rng: np.random.Generator ) -> tuple[list[circuits.Circuit], list[int], np.ndarray]: @@ -97,7 +213,7 @@ def _shuffle_circuits( def _analyze_readout_results( - unshuffled_readout_measurements: list[ResultDict], + unshuffled_readout_measurements: Sequence[ResultDict] | Sequence[study.Result], random_bitstrings: np.ndarray, readout_repetitions: int, qubits: list[ops.Qid], @@ -156,6 +272,7 @@ def _analyze_readout_results( ) +@deprecated(deadline="v2.0", fix="Use run_shuffled_circuits_with_readout_benchmarking() instead.") def run_shuffled_with_readout_benchmarking( input_circuits: list[circuits.Circuit], sampler: work.Sampler, @@ -163,8 +280,8 @@ def run_shuffled_with_readout_benchmarking( rng_or_seed: np.random.Generator | int, num_random_bitstrings: int = 100, readout_repetitions: int = 1000, - qubits: list[ops.Qid] | list[list[ops.Qid]] | None = None, -) -> tuple[list[ResultDict], dict[tuple[ops.Qid, ...], SingleQubitReadoutCalibrationResult]]: + qubits: Optional[Sequence[ops.Qid] | Sequence[Sequence[ops.Qid]]] = None, +) -> tuple[Sequence[ResultDict], dict[tuple[ops.Qid, ...], SingleQubitReadoutCalibrationResult]]: """Run the circuits in a shuffled order with readout error benchmarking. Args: @@ -187,39 +304,35 @@ def run_shuffled_with_readout_benchmarking( """ - _validate_input( - input_circuits, circuit_repetitions, rng_or_seed, num_random_bitstrings, readout_repetitions - ) + # Check circuit_repetitions + if isinstance(circuit_repetitions, int): + if circuit_repetitions <= 0: + raise ValueError("Must provide non-zero circuit_repetitions.") - # If input qubits is None, extract qubits from input circuits - qubits_to_measure: list[list[ops.Qid]] = [] - if qubits is None: - qubits_set: set[ops.Qid] = set() - for circuit in input_circuits: - qubits_set.update(circuit.all_qubits()) - qubits_to_measure = [sorted(qubits_set)] - elif isinstance(qubits[0], ops.Qid): - qubits_to_measure = [qubits] # type: ignore - else: - qubits_to_measure = qubits # type: ignore + # Check num_random_bitstrings is bigger than or equal to 0 + if num_random_bitstrings < 0: + raise ValueError("Must provide zero or more num_random_bitstrings.") + + # Check readout_repetitions is bigger than 0 + if readout_repetitions <= 0: + raise ValueError("Must provide non-zero readout_repetitions for readout calibration.") + _validate_experiment_input(input_circuits, circuit_repetitions, rng_or_seed) + + qubits_to_measure = _determine_qubits_to_measure(input_circuits, qubits) # Generate the readout calibration circuits if num_random_bitstrings>0 # Else all_readout_calibration_circuits and all_random_bitstrings are empty - all_readout_calibration_circuits = [] - all_random_bitstrings = [] - rng = ( rng_or_seed if isinstance(rng_or_seed, np.random.Generator) else np.random.default_rng(rng_or_seed) ) - if num_random_bitstrings > 0: - for qubit_group in qubits_to_measure: - readout_calibration_circuits, random_bitstrings = ( - _generate_readout_calibration_circuits(qubit_group, rng, num_random_bitstrings) - ) - all_readout_calibration_circuits.extend(readout_calibration_circuits) - all_random_bitstrings.append(random_bitstrings) + + all_readout_calibration_circuits, all_random_bitstrings, _ = ( + _generate_all_readout_calibration_circuits( + num_random_bitstrings, qubits_to_measure, False, rng + ) + ) # Shuffle the circuits if isinstance(circuit_repetitions, int): @@ -254,3 +367,173 @@ def run_shuffled_with_readout_benchmarking( start_idx = end_idx return unshuffled_input_circuits_measiurements, readout_calibration_results + + +def run_shuffled_circuits_with_readout_benchmarking( + sampler: work.Sampler, + input_circuits: list[circuits.Circuit], + parameters: ReadoutBenchmarkingParams, + qubits: Optional[Sequence[ops.Qid] | Sequence[Sequence[ops.Qid]]] = None, + rng_or_seed: Optional[np.random.Generator | int] = None, +) -> tuple[Sequence[ResultDict], dict[tuple[ops.Qid, ...], SingleQubitReadoutCalibrationResult]]: + """Run the circuits in a shuffled order with readout error benchmarking. + + Args: + sampler: The sampler to use. + input_circuits: The circuits to run. + parameters: The readout benchmarking parameters. + qubits: The qubits to benchmark readout errors. If None, all qubits in the + input_circuits are used. Can be a list of qubits or a list of tuples + of qubits. + rng_or_seed: A random number generator used to generate readout circuits. + Or an integer seed. + + Returns: + A tuple containing: + - A list of dictionaries with the unshuffled measurement results. + - A dictionary mapping each tuple of qubits to a SingleQubitReadoutCalibrationResult. + + """ + + _validate_experiment_input(input_circuits, parameters.circuit_repetitions, rng_or_seed) + + qubits_to_measure = _determine_qubits_to_measure(input_circuits, qubits) + + # Generate the readout calibration circuits if num_random_bitstrings>0 + # Else all_readout_calibration_circuits and all_random_bitstrings are empty + rng = ( + rng_or_seed + if isinstance(rng_or_seed, np.random.Generator) + else np.random.default_rng(rng_or_seed) + ) + + all_readout_calibration_circuits, all_random_bitstrings, _ = ( + _generate_all_readout_calibration_circuits( + parameters.num_random_bitstrings, qubits_to_measure, False, rng + ) + ) + + # Shuffle the circuits + circuit_repetitions = parameters.circuit_repetitions + if isinstance(circuit_repetitions, int): + circuit_repetitions = [circuit_repetitions] * len(input_circuits) + all_repetitions = circuit_repetitions + [parameters.readout_repetitions] * len( + all_readout_calibration_circuits + ) + + shuffled_circuits, all_repetitions, unshuf_order = _shuffle_circuits( + input_circuits + all_readout_calibration_circuits, all_repetitions, rng + ) + + # Run the shuffled circuits and measure + results = sampler.run_batch(shuffled_circuits, repetitions=all_repetitions) + timestamp = time.time() + shuffled_measurements = [res[0] for res in results] + unshuffled_measurements = [shuffled_measurements[i] for i in unshuf_order] + + unshuffled_input_circuits_measiurements = unshuffled_measurements[: len(input_circuits)] + unshuffled_readout_measurements = unshuffled_measurements[len(input_circuits) :] + + # Analyze results + readout_calibration_results = {} + start_idx = 0 + for qubit_group, random_bitstrings in zip(qubits_to_measure, all_random_bitstrings): + end_idx = start_idx + len(random_bitstrings) + group_measurements = unshuffled_readout_measurements[start_idx:end_idx] + calibration_result = _analyze_readout_results( + group_measurements, + random_bitstrings, + parameters.readout_repetitions, + qubit_group, + timestamp, + ) + readout_calibration_results[tuple(qubit_group)] = calibration_result + start_idx = end_idx + + return unshuffled_input_circuits_measiurements, readout_calibration_results + + +def run_sweep_with_readout_benchmarking( + sampler: work.Sampler, + input_circuits: list[circuits.Circuit], + sweep_params: Sequence[study.Sweepable], + parameters: ReadoutBenchmarkingParams, + qubits: Optional[Sequence[ops.Qid] | Sequence[Sequence[ops.Qid]]] = None, + rng_or_seed: Optional[np.random.Generator | int] = None, +) -> tuple[ + Sequence[Sequence[study.Result]], dict[tuple[ops.Qid, ...], SingleQubitReadoutCalibrationResult] +]: + """Run the sweep circuits with readout error benchmarking (no shuffling). + Args: + sampler: The sampler to use. + input_circuits: The circuits to run. + sweep_params: The sweep parameters for the input circuits. + parameters: The readout benchmarking parameters. + qubits: The qubits to benchmark readout errors. If None, all qubits in the + input_circuits are used. Can be a list of qubits or a list of tuples + of qubits. + rng_or_seed: A random number generator used to generate readout circuits. + Or an integer seed. + + Returns: + A tuple containing: + - A list of lists of dictionaries with the measurement results. + - A dictionary mapping each tuple of qubits to a SingleQubitReadoutCalibrationResult. + """ + + _validate_experiment_input_with_sweep( + input_circuits, sweep_params, parameters.circuit_repetitions, rng_or_seed + ) + + qubits_to_measure = _determine_qubits_to_measure(input_circuits, qubits) + + # Generate the readout calibration circuits (parameterized circuits) and sweep params + # if num_random_bitstrings>0 + # Else all_readout_calibration_circuits and all_random_bitstrings are empty + rng = ( + rng_or_seed + if isinstance(rng_or_seed, np.random.Generator) + else np.random.default_rng(rng_or_seed) + ) + + all_readout_calibration_circuits, all_random_bitstrings, all_readout_sweep_params = ( + _generate_all_readout_calibration_circuits( + parameters.num_random_bitstrings, qubits_to_measure, True, rng + ) + ) + + circuit_repetitions = parameters.circuit_repetitions + if isinstance(circuit_repetitions, int): + circuit_repetitions = [circuit_repetitions] * len(input_circuits) + all_repetitions = circuit_repetitions + [parameters.readout_repetitions] * len( + all_readout_calibration_circuits + ) + + # Run the sweep circuits and measure + results = sampler.run_batch( + input_circuits + all_readout_calibration_circuits, + list(sweep_params) + all_readout_sweep_params, + repetitions=all_repetitions, + ) + + timestamp = time.time() + + input_circuits_measurement = results[: len(input_circuits)] + readout_measurements = results[len(input_circuits) :] + + # Analyze results + readout_calibration_results = {} + for qubit_group, random_bitstrings, group_measurements in zip( + qubits_to_measure, all_random_bitstrings, readout_measurements + ): + + calibration_result = _analyze_readout_results( + group_measurements, + random_bitstrings, + parameters.readout_repetitions, + qubit_group, + timestamp, + ) + readout_calibration_results[tuple(qubit_group)] = calibration_result + + return input_circuits_measurement, readout_calibration_results diff --git a/cirq-core/cirq/contrib/shuffle_circuits/shuffle_circuits_with_readout_benchmarking_test.py b/cirq-core/cirq/contrib/shuffle_circuits/shuffle_circuits_with_readout_benchmarking_test.py index 46b19794065..7c20f677746 100644 --- a/cirq-core/cirq/contrib/shuffle_circuits/shuffle_circuits_with_readout_benchmarking_test.py +++ b/cirq-core/cirq/contrib/shuffle_circuits/shuffle_circuits_with_readout_benchmarking_test.py @@ -15,11 +15,14 @@ from __future__ import annotations import itertools +from typing import Sequence import numpy as np import pytest +import sympy import cirq +import cirq.contrib.shuffle_circuits.shuffle_circuits_with_readout_benchmarking as sc_readout from cirq.experiments import ( random_quantum_circuit_generation as rqcg, SingleQubitReadoutCalibrationResult, @@ -28,7 +31,7 @@ from cirq.study import ResultDict -def _create_test_circuits(qubits: list[cirq.Qid], n_circuits: int) -> list[cirq.Circuit]: +def _create_test_circuits(qubits: Sequence[cirq.Qid], n_circuits: int) -> list[cirq.Circuit]: """Helper function to generate circuits for testing.""" if len(qubits) < 2: raise ValueError( @@ -50,32 +53,106 @@ def _create_test_circuits(qubits: list[cirq.Qid], n_circuits: int) -> list[cirq. return input_circuits -def test_shuffled_circuits_with_readout_benchmarking_errors_no_noise(): - """Test shuffled circuits with readout benchmarking with no noise from sampler.""" - qubits = cirq.LineQubit.range(5) +def _create_test_circuits_with_sweep( + qubits: Sequence[cirq.Qid], n_circuits: int +) -> tuple[list[cirq.Circuit], list[cirq.ParamResolver]]: + """Helper function to generate sweep circuits for testing.""" + if len(qubits) < 2: + raise ValueError( + "Need at least two qubits to generate two-qubit circuits." + ) # pragma: no cover + theta_symbol = sympy.Symbol('theta') + phi_symbol = sympy.Symbol('phi') - # Generate random input circuits - input_circuits = _create_test_circuits(qubits, 3) + two_qubit_gates = [cirq.ISWAP, cirq.CNOT] - sampler = cirq.Simulator() - circuit_repetitions = 1 - # allow passing a seed - rng = 123 - readout_repetitions = 1000 + input_circuits = [] + sweep_params: list[cirq.ParamResolver] = [] + qubit_pairs = list(itertools.combinations(qubits, 2)) + num_pairs = len(qubit_pairs) + for i in range(n_circuits): + gate = two_qubit_gates[i % len(two_qubit_gates)] + q0, q1 = qubit_pairs[i % num_pairs] + circuits = rqcg.generate_library_of_2q_circuits( + n_library_circuits=5, two_qubit_gate=gate, q0=q0, q1=q1 + ) + for circuit in circuits: + circuit += cirq.Circuit(cirq.X(q0) ** theta_symbol, cirq.Y(q1) ** phi_symbol) + circuit.append(cirq.measure(*qubits, key="m")) + sweep_params.append(cirq.ParamResolver({'theta': 0, 'phi': 1})) + input_circuits.extend(circuits) + + return input_circuits, sweep_params + +def _circuits_with_readout_benchmarking_errors_shuffled( + sampler: cirq.Sampler, + input_circuits: list[cirq.Circuit], + qubits: Sequence[cirq.Qid] | Sequence[Sequence[cirq.Qid]], + parameters: sc_readout.ReadoutBenchmarkingParams, + rng_or_seed: np.random.Generator | int, +): measurements, readout_calibration_results = ( - cirq.contrib.shuffle_circuits.run_shuffled_with_readout_benchmarking( - input_circuits, - sampler, - circuit_repetitions, - rng, - num_random_bitstrings=100, - readout_repetitions=readout_repetitions, + sc_readout.run_shuffled_circuits_with_readout_benchmarking( + sampler, input_circuits, parameters, qubits, rng_or_seed ) ) for measurement in measurements: assert isinstance(measurement, ResultDict) + return readout_calibration_results + + +def _circuits_with_readout_benchmarking_errors_sweep( + sampler: cirq.Sampler, + input_circuits: list[cirq.Circuit], + qubits: Sequence[cirq.Qid] | Sequence[Sequence[cirq.Qid]], + sweep_params: list[cirq.ParamResolver], + parameters: sc_readout.ReadoutBenchmarkingParams, + rng_or_seed: np.random.Generator | int, +): + sweep_measurements, readout_calibration_results = ( + sc_readout.run_sweep_with_readout_benchmarking( + sampler, input_circuits, sweep_params, parameters, qubits, rng_or_seed + ) + ) + + for measurement_group in sweep_measurements: + for single_sweep_measurement in measurement_group: + assert isinstance(single_sweep_measurement, ResultDict) + return readout_calibration_results + + +@pytest.mark.parametrize("mode", ["shuffled", "sweep"]) +def test_circuits_with_readout_benchmarking_errors_no_noise(mode: str): + """Test shuffled/sweep circuits with readout benchmarking with no noise from sampler.""" + qubits = cirq.LineQubit.range(5) + + sampler = cirq.Simulator() + circuit_repetitions = 1 + num_random_bitstrings = 100 + readout_repetitions = 1000 + + readout_benchmarking_params = sc_readout.ReadoutBenchmarkingParams( + circuit_repetitions=circuit_repetitions, + num_random_bitstrings=num_random_bitstrings, + readout_repetitions=readout_repetitions, + ) + # allow passing a seed + rng = 123 + + if mode == "shuffled": + input_circuits = _create_test_circuits(qubits, 3) + readout_calibration_results = _circuits_with_readout_benchmarking_errors_shuffled( + sampler, input_circuits, qubits, readout_benchmarking_params, rng + ) + + elif mode == "sweep": + input_circuits, sweep_params = _create_test_circuits_with_sweep(qubits, 3) + + readout_calibration_results = _circuits_with_readout_benchmarking_errors_sweep( + sampler, input_circuits, qubits, sweep_params, readout_benchmarking_params, rng + ) for qlist, readout_calibration_result in readout_calibration_results.items(): assert isinstance(qlist, tuple) @@ -88,31 +165,35 @@ def test_shuffled_circuits_with_readout_benchmarking_errors_no_noise(): assert isinstance(readout_calibration_result.timestamp, float) -def test_shuffled_circuits_with_readout_benchmarking_errors_with_noise(): - """Test shuffled circuits with readout benchmarking with noise from sampler.""" +@pytest.mark.parametrize("mode", ["shuffled", "sweep"]) +def test_circuits_with_readout_benchmarking_errors_with_noise(mode: str): + """Test shuffled/sweep circuits with readout benchmarking with noise from sampler.""" qubits = cirq.LineQubit.range(6) - - # Generate random input circuits - input_circuits = _create_test_circuits(qubits, 6) - sampler = NoisySingleQubitReadoutSampler(p0=0.1, p1=0.2, seed=1234) circuit_repetitions = 1 rng = np.random.default_rng() readout_repetitions = 1000 + num_random_bitstrings = 100 - measurements, readout_calibration_results = ( - cirq.contrib.shuffle_circuits.run_shuffled_with_readout_benchmarking( - input_circuits, - sampler, - circuit_repetitions, - rng, - num_random_bitstrings=100, - readout_repetitions=readout_repetitions, - ) + readout_benchmarking_params = sc_readout.ReadoutBenchmarkingParams( + circuit_repetitions=circuit_repetitions, + num_random_bitstrings=num_random_bitstrings, + readout_repetitions=readout_repetitions, ) - for measurement in measurements: - assert isinstance(measurement, ResultDict) + if mode == "shuffled": + input_circuits = _create_test_circuits(qubits, 6) + + readout_calibration_results = _circuits_with_readout_benchmarking_errors_shuffled( + sampler, input_circuits, qubits, readout_benchmarking_params, rng + ) + + elif mode == "sweep": + input_circuits, sweep_params = _create_test_circuits_with_sweep(qubits, 6) + + readout_calibration_results = _circuits_with_readout_benchmarking_errors_sweep( + sampler, input_circuits, qubits, sweep_params, readout_benchmarking_params, rng + ) for qlist, readout_calibration_result in readout_calibration_results.items(): assert isinstance(qlist, tuple) @@ -127,33 +208,38 @@ def test_shuffled_circuits_with_readout_benchmarking_errors_with_noise(): assert isinstance(readout_calibration_result.timestamp, float) -def test_shuffled_circuits_with_readout_benchmarking_errors_with_noise_and_input_qubits(): - """Test shuffled circuits with readout benchmarking with noise from sampler and input qubits.""" +@pytest.mark.parametrize("mode", ["shuffled", "sweep"]) +def test_circuits_with_readout_benchmarking_errors_with_noise_and_input_qubits(mode: str): + """Test shuffled/sweep circuits with readout benchmarking with + noise from sampler and input qubits.""" qubits = cirq.LineQubit.range(6) readout_qubits = qubits[:4] - # Generate random input circuits - input_circuits = _create_test_circuits(qubits, 6) - sampler = NoisySingleQubitReadoutSampler(p0=0.1, p1=0.3, seed=1234) circuit_repetitions = 1 rng = np.random.default_rng() readout_repetitions = 1000 + num_random_bitstrings = 100 - measurements, readout_calibration_results = ( - cirq.contrib.shuffle_circuits.run_shuffled_with_readout_benchmarking( - input_circuits, - sampler, - circuit_repetitions, - rng, - num_random_bitstrings=100, - readout_repetitions=readout_repetitions, - qubits=readout_qubits, - ) + readout_benchmarking_params = sc_readout.ReadoutBenchmarkingParams( + circuit_repetitions=circuit_repetitions, + num_random_bitstrings=num_random_bitstrings, + readout_repetitions=readout_repetitions, ) - for measurement in measurements: - assert isinstance(measurement, ResultDict) + if mode == "shuffled": + input_circuits = _create_test_circuits(qubits, 6) + + readout_calibration_results = _circuits_with_readout_benchmarking_errors_shuffled( + sampler, input_circuits, readout_qubits, readout_benchmarking_params, rng + ) + + elif mode == "sweep": + input_circuits, sweep_params = _create_test_circuits_with_sweep(qubits, 6) + + readout_calibration_results = _circuits_with_readout_benchmarking_errors_sweep( + sampler, input_circuits, readout_qubits, sweep_params, readout_benchmarking_params, rng + ) for qlist, readout_calibration_result in readout_calibration_results.items(): assert isinstance(qlist, tuple) @@ -168,35 +254,42 @@ def test_shuffled_circuits_with_readout_benchmarking_errors_with_noise_and_input assert isinstance(readout_calibration_result.timestamp, float) -def test_shuffled_circuits_with_readout_benchmarking_errors_with_noise_and_lists_input_qubits(): - """Test shuffled circuits with readout benchmarking with noise from sampler and input qubits.""" +@pytest.mark.parametrize("mode", ["shuffled", "sweep"]) +def test_circuits_with_readout_benchmarking_errors_with_noise_and_lists_input_qubits(mode: str): + """Test shuffled/sweep circuits with readout benchmarking with noise + from sampler and input qubits.""" qubits_1 = cirq.LineQubit.range(3) qubits_2 = cirq.LineQubit.range(4) - readout_qubits = [qubits_1, qubits_2] - # Generate random input circuits and append measurements - input_circuits = _create_test_circuits(qubits_1, 6) + _create_test_circuits(qubits_2, 4) - sampler = NoisySingleQubitReadoutSampler(p0=0.1, p1=0.3, seed=1234) circuit_repetitions = 1 rng = np.random.default_rng() readout_repetitions = 1000 + num_random_bitstrings = 100 - measurements, readout_calibration_results = ( - cirq.contrib.shuffle_circuits.run_shuffled_with_readout_benchmarking( - input_circuits, - sampler, - circuit_repetitions, - rng, - num_random_bitstrings=100, - readout_repetitions=readout_repetitions, - qubits=readout_qubits, - ) + readout_benchmarking_params = sc_readout.ReadoutBenchmarkingParams( + circuit_repetitions=circuit_repetitions, + num_random_bitstrings=num_random_bitstrings, + readout_repetitions=readout_repetitions, ) - for measurement in measurements: - assert isinstance(measurement, ResultDict) + if mode == "shuffled": + input_circuits = _create_test_circuits(qubits_1, 6) + _create_test_circuits(qubits_2, 4) + + readout_calibration_results = _circuits_with_readout_benchmarking_errors_shuffled( + sampler, input_circuits, readout_qubits, readout_benchmarking_params, rng + ) + + elif mode == "sweep": + input_circuits, sweep_params = _create_test_circuits_with_sweep(qubits_1, 6) + additional_circuits, additional_sweep_params = _create_test_circuits_with_sweep(qubits_2, 4) + input_circuits += additional_circuits + sweep_params += additional_sweep_params + + readout_calibration_results = _circuits_with_readout_benchmarking_errors_sweep( + sampler, input_circuits, readout_qubits, sweep_params, readout_benchmarking_params, rng + ) for qlist, readout_calibration_result in readout_calibration_results.items(): assert isinstance(qlist, tuple) @@ -211,63 +304,198 @@ def test_shuffled_circuits_with_readout_benchmarking_errors_with_noise_and_lists assert isinstance(readout_calibration_result.timestamp, float) -def test_can_handle_zero_random_bitstring(): - """Test shuffled circuits without readout benchmarking.""" +@pytest.mark.parametrize("mode", ["shuffled", "sweep"]) +def test_can_handle_zero_random_bitstring(mode: str): + """Test shuffled/sweep circuits without readout benchmarking.""" qubits_1 = cirq.LineQubit.range(3) qubits_2 = cirq.LineQubit.range(4) - readout_qubits = [qubits_1, qubits_2] - # Generate random input circuits and append measurements - input_circuits = _create_test_circuits(qubits_1, 6) + _create_test_circuits(qubits_2, 4) - sampler = NoisySingleQubitReadoutSampler(p0=0.1, p1=0.3, seed=1234) circuit_repetitions = 1 rng = np.random.default_rng() readout_repetitions = 1000 + num_random_bitstrings = 0 - measurements, readout_calibration_results = ( - cirq.contrib.shuffle_circuits.run_shuffled_with_readout_benchmarking( - input_circuits, - sampler, - circuit_repetitions, - rng, - num_random_bitstrings=0, - readout_repetitions=readout_repetitions, - qubits=readout_qubits, - ) + readout_benchmarking_params = sc_readout.ReadoutBenchmarkingParams( + circuit_repetitions=circuit_repetitions, + num_random_bitstrings=num_random_bitstrings, + readout_repetitions=readout_repetitions, ) - for measurement in measurements: - assert isinstance(measurement, ResultDict) + if mode == "shuffled": + input_circuits = _create_test_circuits(qubits_1, 6) + _create_test_circuits(qubits_2, 4) + + readout_calibration_results = _circuits_with_readout_benchmarking_errors_shuffled( + sampler, input_circuits, readout_qubits, readout_benchmarking_params, rng + ) + + elif mode == "sweep": + input_circuits, sweep_params = _create_test_circuits_with_sweep(qubits_1, 6) + additional_circuits, additional_sweep_params = _create_test_circuits_with_sweep(qubits_2, 4) + input_circuits += additional_circuits + sweep_params += additional_sweep_params + + readout_calibration_results = _circuits_with_readout_benchmarking_errors_sweep( + sampler, input_circuits, readout_qubits, sweep_params, readout_benchmarking_params, rng + ) + # Check that the readout_calibration_results is empty assert len(readout_calibration_results.items()) == 0 +@pytest.mark.parametrize("mode", ["shuffled", "sweep"]) +def test_circuits_with_readout_benchmarking_no_qubits_arg_empty_rng(mode: str): + """Test benchmarking when the `qubits` argument is not provided.""" + qubits = cirq.LineQubit.range(3) + sampler = NoisySingleQubitReadoutSampler(p0=0.1, p1=0.2, seed=1234) + circuit_repetitions = 1 + readout_repetitions = 1000 + num_random_bitstrings = 100 + + readout_benchmarking_params = sc_readout.ReadoutBenchmarkingParams( + circuit_repetitions=circuit_repetitions, + num_random_bitstrings=num_random_bitstrings, + readout_repetitions=readout_repetitions, + ) + + if mode == "shuffled": + input_circuits = _create_test_circuits(qubits, 3) + measurements, readout_calibration_results = ( + sc_readout.run_shuffled_circuits_with_readout_benchmarking( + sampler, input_circuits, readout_benchmarking_params, None, None + ) + ) + assert len(measurements) == len(input_circuits) + else: # mode == "sweep" + input_circuits, sweep_params = _create_test_circuits_with_sweep(qubits, 3) + sweep_measurements, readout_calibration_results = ( + sc_readout.run_sweep_with_readout_benchmarking( + sampler, input_circuits, sweep_params, readout_benchmarking_params, None, None + ) + ) + assert len(sweep_measurements) == len(input_circuits) + + # When qubits is None, all qubits from input circuits are benchmarked as one group. + assert len(readout_calibration_results) == 1 + qlist, result = list(readout_calibration_results.items())[0] + assert isinstance(qlist, tuple) + assert set(qlist) == set(qubits) + assert isinstance(result, SingleQubitReadoutCalibrationResult) + for error in result.zero_state_errors.values(): + assert 0.08 < error < 0.12 + for error in result.one_state_errors.values(): + assert 0.18 < error < 0.22 + assert result.repetitions == readout_repetitions + + +def test_deprecated_run_shuffled_with_readout_benchmarking(): + """Test that the deprecated function works correctly and is covered.""" + qubits = cirq.LineQubit.range(3) + input_circuits = _create_test_circuits(qubits, 3) + sampler = NoisySingleQubitReadoutSampler(p0=0.1, p1=0.2, seed=1234) + circuit_repetitions = 1 + readout_repetitions = 1000 + num_random_bitstrings = 100 + + # Test with an integer seed. + with cirq.testing.assert_deprecated(deadline='v2.0', count=1): + measurements_seed, results_seed = sc_readout.run_shuffled_with_readout_benchmarking( + input_circuits=input_circuits, + sampler=sampler, + circuit_repetitions=circuit_repetitions, + rng_or_seed=123, + num_random_bitstrings=num_random_bitstrings, + readout_repetitions=readout_repetitions, + qubits=qubits, + ) + assert len(measurements_seed) == len(input_circuits) + qlist, result = list(results_seed.items())[0] + assert tuple(qubits) == qlist + for error in result.zero_state_errors.values(): + assert 0.08 < error < 0.12 + for error in result.one_state_errors.values(): + assert 0.18 < error < 0.22 + + # Test with qubits=None to cover the auto-detection branch. + with cirq.testing.assert_deprecated(deadline='v2.0', count=1): + _, results_none = sc_readout.run_shuffled_with_readout_benchmarking( + input_circuits=input_circuits, + sampler=sampler, + circuit_repetitions=circuit_repetitions, + rng_or_seed=123, + num_random_bitstrings=num_random_bitstrings, + readout_repetitions=readout_repetitions, + qubits=None, + ) + qlist_none, _ = list(results_none.items())[0] + assert set(qlist_none) == set(qubits) + + # Test circuit_repetitions must be > 0 + with cirq.testing.assert_deprecated(deadline="v2.0", count=1): + with pytest.raises(ValueError, match="Must provide non-zero circuit_repetitions."): + sc_readout.run_shuffled_with_readout_benchmarking( + input_circuits, + sampler, + circuit_repetitions=0, + num_random_bitstrings=5, + readout_repetitions=100, + rng_or_seed=123, + ) + + # Test num_random_bitstrings must be >= 0 + with cirq.testing.assert_deprecated(deadline="v2.0", count=1): + with pytest.raises(ValueError, match="Must provide zero or more num_random_bitstrings."): + sc_readout.run_shuffled_with_readout_benchmarking( + input_circuits, + sampler, + circuit_repetitions=10, + num_random_bitstrings=-1, + readout_repetitions=100, + rng_or_seed=123, + ) + + # Test readout_repetitions must be > 0 + with cirq.testing.assert_deprecated(deadline="v2.0", count=1): + with pytest.raises( + ValueError, match="Must provide non-zero readout_repetitions for readout calibration." + ): + sc_readout.run_shuffled_with_readout_benchmarking( + input_circuits, + sampler, + circuit_repetitions=10, + num_random_bitstrings=1, + readout_repetitions=0, + rng_or_seed=123, + ) + + def test_empty_input_circuits(): """Test that the input circuits are empty.""" + readout_benchmarking_params = sc_readout.ReadoutBenchmarkingParams( + circuit_repetitions=10, num_random_bitstrings=5, readout_repetitions=100 + ) with pytest.raises(ValueError, match="Input circuits must not be empty."): - cirq.contrib.shuffle_circuits.run_shuffled_with_readout_benchmarking( - [], + sc_readout.run_shuffled_circuits_with_readout_benchmarking( cirq.ZerosSampler(), - circuit_repetitions=10, + [], + readout_benchmarking_params, rng_or_seed=np.random.default_rng(456), - num_random_bitstrings=5, - readout_repetitions=100, ) def test_non_circuit_input(): """Test that the input circuits are not of type cirq.Circuit.""" q = cirq.LineQubit(0) + readout_benchmarking_params = sc_readout.ReadoutBenchmarkingParams( + circuit_repetitions=10, num_random_bitstrings=5, readout_repetitions=100 + ) with pytest.raises(ValueError, match="Input circuits must be of type cirq.Circuit."): - cirq.contrib.shuffle_circuits.run_shuffled_with_readout_benchmarking( - [q], + sc_readout.run_shuffled_circuits_with_readout_benchmarking( cirq.ZerosSampler(), - circuit_repetitions=10, + [q], + readout_benchmarking_params, rng_or_seed=np.random.default_rng(456), - num_random_bitstrings=5, - readout_repetitions=100, ) @@ -275,29 +503,23 @@ def test_no_measurements(): """Test that the input circuits don't have measurements.""" q = cirq.LineQubit(0) circuit = cirq.Circuit(cirq.H(q)) + readout_benchmarking_params = sc_readout.ReadoutBenchmarkingParams( + circuit_repetitions=10, num_random_bitstrings=5, readout_repetitions=100 + ) with pytest.raises(ValueError, match="Input circuits must have measurements."): - cirq.contrib.shuffle_circuits.run_shuffled_with_readout_benchmarking( - [circuit], + sc_readout.run_shuffled_circuits_with_readout_benchmarking( cirq.ZerosSampler(), - circuit_repetitions=10, + [circuit], + readout_benchmarking_params, rng_or_seed=np.random.default_rng(456), - num_random_bitstrings=5, - readout_repetitions=100, ) def test_zero_circuit_repetitions(): """Test that the circuit repetitions are zero.""" - q = cirq.LineQubit(0) - circuit = cirq.Circuit(cirq.H(q), cirq.measure(q)) with pytest.raises(ValueError, match="Must provide non-zero circuit_repetitions."): - cirq.contrib.shuffle_circuits.run_shuffled_with_readout_benchmarking( - [circuit], - cirq.ZerosSampler(), - circuit_repetitions=0, - rng_or_seed=np.random.default_rng(456), - num_random_bitstrings=5, - readout_repetitions=100, + sc_readout.ReadoutBenchmarkingParams( + circuit_repetitions=0, num_random_bitstrings=5, readout_repetitions=100 ) @@ -305,62 +527,51 @@ def test_mismatch_circuit_repetitions(): """Test that the number of circuit repetitions don't match the number of input circuits.""" q = cirq.LineQubit(0) circuit = cirq.Circuit(cirq.H(q), cirq.measure(q)) + readout_benchmarking_params = sc_readout.ReadoutBenchmarkingParams( + circuit_repetitions=[10, 20], num_random_bitstrings=5, readout_repetitions=100 + ) with pytest.raises( ValueError, match="Number of circuit_repetitions must match the number of" + " input circuits.", ): - cirq.contrib.shuffle_circuits.run_shuffled_with_readout_benchmarking( - [circuit], + sc_readout.run_shuffled_circuits_with_readout_benchmarking( cirq.ZerosSampler(), - circuit_repetitions=[10, 20], + [circuit], + readout_benchmarking_params, rng_or_seed=np.random.default_rng(456), - num_random_bitstrings=5, - readout_repetitions=100, ) def test_zero_num_random_bitstrings(): """Test that the number of random bitstrings is smaller than zero.""" - q = cirq.LineQubit(0) - circuit = cirq.Circuit(cirq.H(q), cirq.measure(q)) with pytest.raises(ValueError, match="Must provide zero or more num_random_bitstrings."): - cirq.contrib.shuffle_circuits.run_shuffled_with_readout_benchmarking( - [circuit], - cirq.ZerosSampler(), - circuit_repetitions=10, - rng_or_seed=np.random.default_rng(456), - num_random_bitstrings=-1, - readout_repetitions=100, + sc_readout.ReadoutBenchmarkingParams( + circuit_repetitions=10, num_random_bitstrings=-1, readout_repetitions=100 ) def test_zero_readout_repetitions(): """Test that the readout repetitions is zero.""" - q = cirq.LineQubit(0) - circuit = cirq.Circuit(cirq.H(q), cirq.measure(q)) with pytest.raises( ValueError, match="Must provide non-zero readout_repetitions for readout" + " calibration." ): - cirq.contrib.shuffle_circuits.run_shuffled_with_readout_benchmarking( - [circuit], - cirq.ZerosSampler(), - circuit_repetitions=10, - rng_or_seed=np.random.default_rng(456), - num_random_bitstrings=5, - readout_repetitions=0, + sc_readout.ReadoutBenchmarkingParams( + circuit_repetitions=10, num_random_bitstrings=5, readout_repetitions=0 ) -def test_rng_type_mismatch(): - """Test that the rng is not a numpy random generator or a seed.""" - q = cirq.LineQubit(0) - circuit = cirq.Circuit(cirq.H(q), cirq.measure(q)) - with pytest.raises(ValueError, match="Must provide a numpy random generator or a seed"): - cirq.contrib.shuffle_circuits.run_shuffled_with_readout_benchmarking( - [circuit], +def test_empty_sweep_params(): + """Test that the sweep params are empty.""" + q = cirq.LineQubit(5) + circuit = cirq.Circuit(cirq.H(q)) + readout_benchmarking_params = sc_readout.ReadoutBenchmarkingParams( + circuit_repetitions=10, num_random_bitstrings=5, readout_repetitions=100 + ) + with pytest.raises(ValueError, match="Sweep parameters must not be empty."): + sc_readout.run_sweep_with_readout_benchmarking( cirq.ZerosSampler(), - circuit_repetitions=10, - rng_or_seed="not a random generator or seed", - num_random_bitstrings=5, - readout_repetitions=100, + [circuit], + [], + readout_benchmarking_params, + rng_or_seed=np.random.default_rng(456), )