diff --git a/cirq-core/cirq/contrib/paulistring/__init__.py b/cirq-core/cirq/contrib/paulistring/__init__.py index a75ce6617d3..cc9248c149a 100644 --- a/cirq-core/cirq/contrib/paulistring/__init__.py +++ b/cirq-core/cirq/contrib/paulistring/__init__.py @@ -42,3 +42,7 @@ ) from cirq.contrib.paulistring.optimize import optimized_circuit as optimized_circuit + +from cirq.contrib.paulistring.pauli_string_measurement_with_readout_mitigation import ( + measure_pauli_strings as measure_pauli_strings, +) 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 new file mode 100644 index 00000000000..c3829e20255 --- /dev/null +++ b/cirq-core/cirq/contrib/paulistring/pauli_string_measurement_with_readout_mitigation.py @@ -0,0 +1,312 @@ +# Copyright 2025 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" Tools for measuring expectation values of Pauli strings with readout error mitigation. """ +import time +import dataclasses +from typing import List, Union, Dict, Optional + +import numpy as np + +from cirq import ops, circuits, work +from cirq.contrib.shuffle_circuits import run_shuffled_with_readout_benchmarking +from cirq.experiments import SingleQubitReadoutCalibrationResult +from cirq.experiments.readout_confusion_matrix import TensoredConfusionMatrices +from cirq.study import ResultDict + + +@dataclasses.dataclass +class PauliStringMeasurementResult: + """Result of measuring a Pauli string. + + Attributes: + pauli_string: The Pauli string that is measured. + mitigated_expectation: The error-mitigated expectation value of the Pauli string. + mitigated_stddev: The standard deviation of the error-mitigated expectation value. + unmitigated_expectation: The unmitigated expectation value of the Pauli string. + unmitigated_stddev: The standard deviation of the unmitigated expectation value. + """ + + pauli_string: ops.PauliString + mitigated_expectation: float + mitigated_stddev: float + unmitigated_expectation: float + unmitigated_stddev: float + + +@dataclasses.dataclass +class CircuitToPauliStringsMeasurementResult: + """Result of measuring Pauli strings on a circuit. + + Attributes: + circuit: The circuit that is measured. + results: A list of PauliStringMeasurementResult objects. + calibration_result: The calibration result for single-qubit readout errors. + """ + + circuit: circuits.FrozenCircuit + results: List[PauliStringMeasurementResult] + calibration_result: Optional[SingleQubitReadoutCalibrationResult] + + +def _pauli_string_to_basis_change_ops( + pauli_string: ops.PauliString, qid_list: list[ops.Qid] +) -> List[ops.Operation]: + """Creates operations to change to the eigenbasis of the given Pauli string. + + This function constructs a list of ops.Operation that performs basis changes + necessary to measure the given pauli_string in the computational basis. + + Args: + pauli_string: The Pauli string to diagonalize. + qid_list: An ordered list of the qubits in the circuit. + + Returns: + A list of Operations that, when applied before measurement in the + computational basis, effectively measures in the eigenbasis of + pauli_strings. + """ + operations = [] + for qubit in pauli_string: + pauli_op = pauli_string[qubit] + qubit_index = qid_list.index(qubit) + if pauli_op == ops.X: + # Rotate to X basis: Ry(-pi/2) + operations.append(ops.ry(-np.pi / 2)(qid_list[qubit_index])) + elif pauli_op == ops.Y: + # Rotate to Y basis: Rx(pi/2) + operations.append(ops.rx(np.pi / 2)(qid_list[qubit_index])) + # No operation needed for Pauli Z or I (identity) + return operations + + +def _build_one_qubit_confusion_matrix(e0: float, e1: float) -> np.ndarray: + """Builds a 2x2 confusion matrix for a single qubit. + + Args: + e0: the 0->1 readout error rate. + e1: the 1->0 readout error rate. + + Returns: + A 2x2 NumPy array representing the confusion matrix. + """ + return np.array([[1 - e0, e1], [e0, 1 - e1]]) + + +def _build_many_one_qubits_confusion_matrix( + qubits_to_error: SingleQubitReadoutCalibrationResult, +) -> list[np.ndarray]: + """Builds a list of confusion matrices from calibration results. + + This function iterates through the calibration results for each qubit and + constructs a list of single-qubit confusion matrices. + + Args: + qubits_to_error: An object containing calibration results for + single-qubit readout errors, including zero-state and one-state errors + for each qubit. + + Returns: + A list of NumPy arrays, where each array is a 2x2 confusion matrix + for a qubit. The order of matrices corresponds to the order of qubits + in the calibration results (alphabetical order by qubit name). + """ + cms: list[np.ndarray] = [] + if not qubits_to_error: + return cms + + for qubit in sorted(qubits_to_error.zero_state_errors.keys()): + e0 = qubits_to_error.zero_state_errors[qubit] + e1 = qubits_to_error.one_state_errors[qubit] + cms.append(_build_one_qubit_confusion_matrix(e0, e1)) + return cms + + +def _build_many_one_qubits_empty_confusion_matrix(qubits_length: int): + """Builds a list of empty confusion matrices""" + cms: list[np.ndarray] = [] + for _ in range(qubits_length): + cms.append(_build_one_qubit_confusion_matrix(0, 0)) + return cms + + +def _process_pauli_measurement_results( + qubits: List[ops.Qid], + pauli_strings: List[ops.PauliString], + circuit_results: List[ResultDict], + confusion_matrices: List[np.ndarray], + pauli_repetitions: int, + timestamp: float, +) -> List[PauliStringMeasurementResult]: + """Calculates both error-mitigated expectation values and unmitigated expectation values + from measurement results. + + This function takes the results from shuffled readout benchmarking and: + 1. Constructs a tensored confusion matrix for error mitigation. + 2. Mitigates readout errors for each Pauli string measurement. + 3. Calculates and returns both error-mitigated and unmitigated expectation values. + + Args: + qubits: Qubits to build confusion matrices for. In a sorted order. + pauli_strings: The list of PauliStrings that are measured. + circuit_results: A list of ResultDict obtained + from running the Pauli measurement circuits. + confusion_matrices: A list of confusion matrices from calibration results. + pauli_repetitions: The number of repetitions used for Pauli string measurements. + timestamp: The timestamp of the calibration results. + + Returns: + A list of PauliStringMeasurementResult objects, where each object contains: + - The Pauli string that was measured. + - The mitigated expectation value of the Pauli string. + - The standard deviation of the mitigated expectation value. + - The unmitigated expectation value of the Pauli string. + - The standard deviation of the unmitigated expectation value. + """ + tensored_cm = TensoredConfusionMatrices( + confusion_matrices, + [[q] for q in qubits], + repetitions=pauli_repetitions, + timestamp=timestamp, + ) + + pauli_measurement_results: List[PauliStringMeasurementResult] = [] + + for pauli_index, circuit_result in enumerate(circuit_results): + measurement_results = circuit_result.measurements["m"] + + mitigated_values, d_m = tensored_cm.readout_mitigation_pauli_uncorrelated( + qubits, measurement_results + ) + + p1 = np.mean(np.sum(measurement_results, axis=1) % 2) + unmitigated_values = 1 - 2 * np.mean(p1) + d_unmit = 2 * np.sqrt(p1 * (1 - p1) / pauli_repetitions) + + pauli_measurement_results.append( + PauliStringMeasurementResult( + pauli_string=pauli_strings[pauli_index], + mitigated_expectation=mitigated_values, + mitigated_stddev=d_m, + unmitigated_expectation=unmitigated_values, + unmitigated_stddev=d_unmit, + ) + ) + + return pauli_measurement_results + + +def measure_pauli_strings( + circuits_to_pauli: Dict[circuits.FrozenCircuit, list[ops.PauliString]], + sampler: work.Sampler, + rng_or_seed: Union[np.random.Generator, int], + pauli_repetitions: int, + readout_repetitions: int, + num_random_bitstrings: int, +) -> List[CircuitToPauliStringsMeasurementResult]: + """Measures expectation values of Pauli strings on given circuits with/without + readout error mitigation. + + This function takes a list of circuits and corresponding List[Pauli string] to measure. + For each circuit-List[Pauli string] pair, it: + 1. Constructs circuits to measure the Pauli string expectation value by + adding basis change moments and measurement operations. + 2. Runs shuffled readout benchmarking on these circuits to calibrate readout errors. + 3. Mitigates readout errors using the calibrated confusion matrices. + 4. Calculates and returns both error-mitigated and unmitigatedexpectation values for + each Pauli string. + + Args: + circuits_to_pauli: A dictionary mapping circuits to a list of Pauli strings + to measure. + sampler: The sampler to use. + rng_or_seed: A random number generator or seed for the shuffled benchmarking. + pauli_repetitions: The number of repetitions for each circuit when measuring + Pauli strings. + readout_repetitions: The number of repetitions for readout calibration + in the shuffled benchmarking. + num_random_bitstrings: The number of random bitstrings to use in shuffled + benchmarking. + + Returns: + A list of CircuitToPauliStringsMeasurementResult objects, where each object contains: + - The circuit that was measured. + - A list of PauliStringMeasurementResult objects. + - The calibration result for single-qubit readout errors. + """ + + # Extract unique qubit tuples from all circuits + unique_qubit_tuples = set() + for circuit in circuits_to_pauli.keys(): + unique_qubit_tuples.add(tuple(sorted(set(circuit.all_qubits())))) + # qubits_list is a list of qubit tuples + qubits_list = sorted(unique_qubit_tuples) + + # Build the basis-change circuits for each Pauli string + pauli_measurement_circuits = list[ops.PauliString]() + for input_circuit, pauli_strings in circuits_to_pauli.items(): + qid_list = list(set(input_circuit.all_qubits())) + basis_change_circuits = [] + input_circuit_unfrozen = input_circuit.unfreeze() + for pauli_string in pauli_strings: + basis_change_circuit = ( + input_circuit_unfrozen + + _pauli_string_to_basis_change_ops(pauli_string, qid_list) + + ops.measure(*qid_list, key="m") + ) + basis_change_circuits.append(basis_change_circuit) + 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, + ) + + # Process the results to calculate expectation values + results: List[CircuitToPauliStringsMeasurementResult] = [] + circuit_result_index = 0 + for input_circuit, pauli_strings in circuits_to_pauli.items(): + qubits_in_circuit = tuple(sorted(set(input_circuit.all_qubits()))) + + confusion_matrices = ( + _build_many_one_qubits_confusion_matrix(calibration_results[qubits_in_circuit]) + if num_random_bitstrings != 0 + else _build_many_one_qubits_empty_confusion_matrix(len(qubits_in_circuit)) + ) + + pauli_measurement_results = _process_pauli_measurement_results( + list(qubits_in_circuit), + pauli_strings, + circuits_results[circuit_result_index : circuit_result_index + len(pauli_strings)], + confusion_matrices, + pauli_repetitions, + time.time(), + ) + results.append( + CircuitToPauliStringsMeasurementResult( + circuit=input_circuit, + results=pauli_measurement_results, + calibration_result=( + calibration_results[qubits_in_circuit] if num_random_bitstrings != 0 else None + ), + ) + ) + + circuit_result_index += len(pauli_strings) + return results 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 new file mode 100644 index 00000000000..35d0776cba2 --- /dev/null +++ b/cirq-core/cirq/contrib/paulistring/pauli_string_measurement_with_readout_mitigation_test.py @@ -0,0 +1,215 @@ +# Copyright 2025 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Dict +import cirq +import numpy as np + +from cirq.contrib.paulistring import measure_pauli_strings +from cirq.experiments.single_qubit_readout_calibration_test import NoisySingleQubitReadoutSampler +from cirq.experiments import SingleQubitReadoutCalibrationResult + + +def _create_ghz(number_of_qubits: int, qubits: list[cirq.Qid]) -> cirq.Circuit: + ghz_circuit = cirq.Circuit( + cirq.H(qubits[0]), + *[cirq.CNOT(qubits[i - 1], qubits[i]) for i in range(1, number_of_qubits)], + ) + return ghz_circuit + + +def _ideal_expectation_based_on_pauli_string(pauli_string: cirq.PauliString, qubits: list) -> float: + if pauli_string == cirq.PauliString({q: cirq.X for q in qubits}): + ideal = 1 + else: + ideal = 1 if len(qubits) % 2 == 0 else 0 + return ideal + + +def test_pauli_string_measurement_errors_no_noise(): + """Test that the mitigated expectation is close to the ideal expectation + based on the Pauli string""" + + qubits = cirq.LineQubit.range(5) + circuit = cirq.FrozenCircuit(_create_ghz(5, qubits)) + sampler = cirq.Simulator() + + circuits_to_pauli: Dict[cirq.FrozenCircuit, list[cirq.PauliString]] = {} + circuits_to_pauli[circuit] = [ + cirq.PauliString({q: cirq.X for q in qubits}), + cirq.PauliString({q: cirq.Y for q in qubits}), + cirq.PauliString({q: cirq.Z for q in qubits}), + ] + + circuits_with_pauli_expectations = measure_pauli_strings( + circuits_to_pauli, sampler, 1000, 1000, 1000, 1000 + ) + + for circuit_with_pauli_expectations in circuits_with_pauli_expectations: + assert isinstance(circuit_with_pauli_expectations.circuit, cirq.FrozenCircuit) + assert isinstance( + circuit_with_pauli_expectations.calibration_result, SingleQubitReadoutCalibrationResult + ) + + for pauli_string_measurement_results in circuit_with_pauli_expectations.results: + # Since there is no noise, the mitigated and unmitigated expectations should be the same + assert np.isclose( + pauli_string_measurement_results.mitigated_expectation, + pauli_string_measurement_results.unmitigated_expectation, + ) + assert np.isclose( + pauli_string_measurement_results.mitigated_expectation, + _ideal_expectation_based_on_pauli_string( + pauli_string_measurement_results.pauli_string, qubits + ), + atol=4 * pauli_string_measurement_results.mitigated_stddev, + ) + assert circuit_with_pauli_expectations.calibration_result.zero_state_errors == { + q: 0 for q in qubits + } + assert circuit_with_pauli_expectations.calibration_result.one_state_errors == { + q: 0 for q in qubits + } + + +def test_pauli_string_measurement_errors_with_noise(): + """Test that the mitigated expectation is close to the ideal expectation + based on the Pauli string""" + qubits = cirq.LineQubit.range(7) + circuit = cirq.FrozenCircuit(_create_ghz(7, qubits)) + sampler = NoisySingleQubitReadoutSampler(p0=0.1, p1=0.005, seed=1234) + + circuits_to_pauli: Dict[cirq.FrozenCircuit, list[cirq.PauliString]] = {} + circuits_to_pauli[circuit] = [ + cirq.PauliString({q: cirq.Z for q in qubits}), + cirq.PauliString({q: cirq.X for q in qubits}), + cirq.PauliString({q: cirq.Y for q in qubits}), + ] + + circuits_with_pauli_expectations = measure_pauli_strings( + circuits_to_pauli, sampler, np.random.default_rng(), 1000, 1000, 1000 + ) + + for circuit_with_pauli_expectations in circuits_with_pauli_expectations: + assert isinstance(circuit_with_pauli_expectations.circuit, cirq.FrozenCircuit) + assert isinstance( + circuit_with_pauli_expectations.calibration_result, SingleQubitReadoutCalibrationResult + ) + + for pauli_string_measurement_results in circuit_with_pauli_expectations.results: + assert np.isclose( + pauli_string_measurement_results.mitigated_expectation, + _ideal_expectation_based_on_pauli_string( + pauli_string_measurement_results.pauli_string, qubits + ), + atol=4 * pauli_string_measurement_results.mitigated_stddev, + ) + for error in circuit_with_pauli_expectations.calibration_result.zero_state_errors.values(): + assert 0.08 < error < 0.12 + for error in circuit_with_pauli_expectations.calibration_result.one_state_errors.values(): + assert 0.0045 < error < 0.0055 + + +def test_many_circuits_input_measurement_with_noise(): + """Test that the mitigated expectation is close to the ideal expectation + based on the Pauli string for multiple circuits""" + qubits_1 = cirq.LineQubit.range(3) + qubits_2 = [ + cirq.GridQubit(0, 1), + cirq.GridQubit(1, 1), + cirq.GridQubit(1, 0), + cirq.GridQubit(1, 2), + cirq.GridQubit(2, 1), + ] + qubits_3 = cirq.LineQubit.range(8) + + circuit_1 = cirq.FrozenCircuit(_create_ghz(3, qubits_1)) + circuit_2 = cirq.FrozenCircuit(_create_ghz(5, qubits_2)) + circuit_3 = cirq.FrozenCircuit(_create_ghz(8, qubits_3)) + + circuits_to_pauli: Dict[cirq.FrozenCircuit, list[cirq.PauliString]] = {} + circuits_to_pauli[circuit_1] = [ + cirq.PauliString({q: cirq.Z for q in qubits_1}), + cirq.PauliString({q: cirq.X for q in qubits_1}), + cirq.PauliString({q: cirq.Y for q in qubits_1}), + ] + circuits_to_pauli[circuit_2] = [ + cirq.PauliString({q: cirq.Z for q in qubits_2}), + cirq.PauliString({q: cirq.X for q in qubits_2}), + cirq.PauliString({q: cirq.Y for q in qubits_2}), + ] + circuits_to_pauli[circuit_3] = [ + cirq.PauliString({q: cirq.Z for q in qubits_3}), + cirq.PauliString({q: cirq.X for q in qubits_3}), + cirq.PauliString({q: cirq.Y for q in qubits_3}), + ] + + sampler = NoisySingleQubitReadoutSampler(p0=0.03, p1=0.005, seed=1234) + + circuits_with_pauli_expectations = measure_pauli_strings( + circuits_to_pauli, sampler, np.random.default_rng(), 1000, 1000, 1000 + ) + + for circuit_with_pauli_expectations in circuits_with_pauli_expectations: + assert isinstance(circuit_with_pauli_expectations.circuit, cirq.FrozenCircuit) + assert isinstance( + circuit_with_pauli_expectations.calibration_result, SingleQubitReadoutCalibrationResult + ) + + for pauli_string_measurement_results in circuit_with_pauli_expectations.results: + qubits_to_measure = circuit_with_pauli_expectations.circuit.all_qubits() + assert np.isclose( + pauli_string_measurement_results.mitigated_expectation, + _ideal_expectation_based_on_pauli_string( + pauli_string_measurement_results.pauli_string, qubits_to_measure + ), + atol=4 * pauli_string_measurement_results.mitigated_stddev, + ) + for error in circuit_with_pauli_expectations.calibration_result.zero_state_errors.values(): + assert 0.025 < error < 0.035 + for error in circuit_with_pauli_expectations.calibration_result.one_state_errors.values(): + assert 0.0045 < error < 0.0055 + + +def test_allow_measurement_without_readout_mitigation(): + """Test that the mitigated expectation is close to the ideal expectation + based on the Pauli string""" + qubits = cirq.LineQubit.range(7) + circuit = cirq.FrozenCircuit(_create_ghz(7, qubits)) + sampler = NoisySingleQubitReadoutSampler(p0=0.1, p1=0.005, seed=1234) + + circuits_to_pauli: Dict[cirq.FrozenCircuit, list[cirq.PauliString]] = {} + circuits_to_pauli[circuit] = [ + cirq.PauliString({q: cirq.Z for q in qubits}), + cirq.PauliString({q: cirq.X for q in qubits}), + cirq.PauliString({q: cirq.Y for q in qubits}), + ] + + circuits_with_pauli_expectations = measure_pauli_strings( + circuits_to_pauli, sampler, np.random.default_rng(), 1000, 1000, 0 + ) + + for circuit_with_pauli_expectations in circuits_with_pauli_expectations: + assert isinstance(circuit_with_pauli_expectations.circuit, cirq.FrozenCircuit) + # Since no readout mitigation string was generated, the calibration + # result should be None + assert circuit_with_pauli_expectations.calibration_result is None + + for pauli_string_measurement_results in circuit_with_pauli_expectations.results: + # Since there's no mitigation, the mitigated and unmitigated expectations + # should be the same + assert np.isclose( + pauli_string_measurement_results.mitigated_expectation, + pauli_string_measurement_results.unmitigated_expectation, + ) + assert circuit_with_pauli_expectations.calibration_result is None 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 f993db81d19..fa5d24a6a57 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 @@ -13,7 +13,7 @@ # limitations under the License. """Tools for running circuits in a shuffled order with readout error benchmarking.""" import time -from typing import Optional, Union +from typing import Optional, Union, Dict, Tuple import numpy as np @@ -50,9 +50,9 @@ def _validate_input( 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 0 - if num_random_bitstrings <= 0: - raise ValueError("Must provide non-zero num_random_bitstrings.") + # 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: @@ -157,8 +157,8 @@ def run_shuffled_with_readout_benchmarking( rng_or_seed: Union[np.random.Generator, int], num_random_bitstrings: int = 100, readout_repetitions: int = 1000, - qubits: Optional[list[ops.Qid]] = None, -) -> tuple[list[ResultDict], SingleQubitReadoutCalibrationResult]: + qubits: Optional[Union[list[ops.Qid], list[list[ops.Qid]]]] = None, +) -> tuple[list[ResultDict], Dict[Tuple[ops.Qid, ...], SingleQubitReadoutCalibrationResult]]: """Run the circuits in a shuffled order with readout error benchmarking. Args: @@ -168,15 +168,16 @@ def run_shuffled_with_readout_benchmarking( rng_or_seed: A random number generator used to generate readout circuits. Or an integer seed. 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. qubits: The qubits to benchmark readout errors. If None, all qubits in the - input_circuits are used. + input_circuits are used. Can be a list of qubits or a list of tuples + of qubits. Returns: A tuple containing: - A list of dictionaries with the unshuffled measurement results. - - A dictionary mapping each qubit to a tuple of readout error rates(e0 and e1), - where e0 is the 0->1 readout error rate and e1 is the 1->0 readout error rate. + - A dictionary mapping each tuple of qubits to a SingleQubitReadoutCalibrationResult. """ @@ -185,31 +186,44 @@ def run_shuffled_with_readout_benchmarking( ) # 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 = sorted(qubits_set) + qubits_to_measure = [sorted(qubits_set)] + elif isinstance(qubits[0], ops.Qid): + qubits_to_measure = [qubits] + else: + qubits_to_measure = 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 = [] - # Generate the readout calibration circuits rng = ( rng_or_seed if isinstance(rng_or_seed, np.random.Generator) else np.random.default_rng(rng_or_seed) ) - readout_calibration_circuits, random_bitstrings = _generate_readout_calibration_circuits( - qubits, rng, num_random_bitstrings - ) + 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) # Shuffle the circuits if isinstance(circuit_repetitions, int): circuit_repetitions = [circuit_repetitions] * len(input_circuits) all_repetitions = circuit_repetitions + [readout_repetitions] * len( - readout_calibration_circuits + all_readout_calibration_circuits ) shuffled_circuits, all_repetitions, unshuf_order = _shuffle_circuits( - input_circuits + readout_calibration_circuits, all_repetitions, rng + input_circuits + all_readout_calibration_circuits, all_repetitions, rng ) # Run the shuffled circuits and measure @@ -222,8 +236,15 @@ def run_shuffled_with_readout_benchmarking( unshuffled_readout_measurements = unshuffled_measurements[len(input_circuits) :] # Analyze results - readout_calibration_results = _analyze_readout_results( - unshuffled_readout_measurements, random_bitstrings, readout_repetitions, qubits, timestamp - ) + 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, 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 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 1f3ff386493..8cf153102e8 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 @@ -11,11 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - import pytest -import cirq import numpy as np +import cirq from cirq.experiments.single_qubit_readout_calibration_test import NoisySingleQubitReadoutSampler from cirq.experiments import random_quantum_circuit_generation as rqcg @@ -64,12 +63,15 @@ def test_shuffled_circuits_with_readout_benchmarking_errors_no_noise(): for measurement in measurements: assert isinstance(measurement, ResultDict) - assert isinstance(readout_calibration_results, SingleQubitReadoutCalibrationResult) + for qlist, readout_calibration_result in readout_calibration_results.items(): + assert isinstance(qlist, tuple) + assert all(isinstance(q, cirq.Qid) for q in qlist) + assert isinstance(readout_calibration_result, SingleQubitReadoutCalibrationResult) - assert readout_calibration_results.zero_state_errors == {q: 0 for q in qubits} - assert readout_calibration_results.one_state_errors == {q: 0 for q in qubits} - assert readout_calibration_results.repetitions == readout_repetitions - assert isinstance(readout_calibration_results.timestamp, float) + assert readout_calibration_result.zero_state_errors == {q: 0 for q in qubits} + assert readout_calibration_result.one_state_errors == {q: 0 for q in qubits} + assert readout_calibration_result.repetitions == readout_repetitions + assert isinstance(readout_calibration_result.timestamp, float) def test_shuffled_circuits_with_readout_benchmarking_errors_with_noise(): @@ -115,14 +117,17 @@ def test_shuffled_circuits_with_readout_benchmarking_errors_with_noise(): for measurement in measurements: assert isinstance(measurement, ResultDict) - assert isinstance(readout_calibration_results, SingleQubitReadoutCalibrationResult) + for qlist, readout_calibration_result in readout_calibration_results.items(): + assert isinstance(qlist, tuple) + assert all(isinstance(q, cirq.Qid) for q in qlist) + assert isinstance(readout_calibration_result, SingleQubitReadoutCalibrationResult) - for error in readout_calibration_results.zero_state_errors.values(): - assert 0.08 < error < 0.12 - for error in readout_calibration_results.one_state_errors.values(): - assert 0.18 < error < 0.22 - assert readout_calibration_results.repetitions == readout_repetitions - assert isinstance(readout_calibration_results.timestamp, float) + for error in readout_calibration_result.zero_state_errors.values(): + assert 0.08 < error < 0.12 + for error in readout_calibration_result.one_state_errors.values(): + assert 0.18 < error < 0.22 + assert readout_calibration_result.repetitions == readout_repetitions + assert isinstance(readout_calibration_result.timestamp, float) def test_shuffled_circuits_with_readout_benchmarking_errors_with_noise_and_input_qubits(): @@ -136,13 +141,13 @@ def test_shuffled_circuits_with_readout_benchmarking_errors_with_noise_and_input n_library_circuits=5, two_qubit_gate=cirq.ISWAP**0.5, q0=qubits[0], q1=qubits[1] ) input_circuits += rqcg.generate_library_of_2q_circuits( - n_library_circuits=5, two_qubit_gate=cirq.CNOT**0.5, q0=qubits[1], q1=qubits[3] + n_library_circuits=5, two_qubit_gate=cirq.CNOT**0.5, q0=qubits[1], q1=qubits[2] ) input_circuits += rqcg.generate_library_of_2q_circuits( - n_library_circuits=5, two_qubit_gate=cirq.CNOT**0.5, q0=qubits[0], q1=qubits[4] + n_library_circuits=5, two_qubit_gate=cirq.CNOT**0.5, q0=qubits[0], q1=qubits[2] ) input_circuits += rqcg.generate_library_of_2q_circuits( - n_library_circuits=5, two_qubit_gate=cirq.ISWAP**0.5, q0=qubits[2], q1=qubits[4] + n_library_circuits=5, two_qubit_gate=cirq.ISWAP**0.5, q0=qubits[4], q1=qubits[3] ) input_circuits += rqcg.generate_library_of_2q_circuits( n_library_circuits=5, two_qubit_gate=cirq.ISWAP**0.5, q0=qubits[2], q1=qubits[5] @@ -170,14 +175,141 @@ def test_shuffled_circuits_with_readout_benchmarking_errors_with_noise_and_input for measurement in measurements: assert isinstance(measurement, ResultDict) - assert isinstance(readout_calibration_results, SingleQubitReadoutCalibrationResult) + for qlist, readout_calibration_result in readout_calibration_results.items(): + assert isinstance(qlist, tuple) + assert all(isinstance(q, cirq.Qid) for q in qlist) + assert isinstance(readout_calibration_result, SingleQubitReadoutCalibrationResult) + + for error in readout_calibration_result.zero_state_errors.values(): + assert 0.08 < error < 0.12 + for error in readout_calibration_result.one_state_errors.values(): + assert 0.28 < error < 0.32 + assert readout_calibration_result.repetitions == readout_repetitions + 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.""" + 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_circuit_1 = rqcg.generate_library_of_2q_circuits( + n_library_circuits=5, two_qubit_gate=cirq.ISWAP**0.5, q0=qubits_1[0], q1=qubits_1[1] + ) + for circuit in input_circuit_1: + circuit.append(cirq.Circuit(cirq.measure(*qubits_1, key="m"))) + + input_circuit_2 = rqcg.generate_library_of_2q_circuits( + n_library_circuits=5, two_qubit_gate=cirq.CNOT**0.5, q0=qubits_1[1], q1=qubits_1[2] + ) + for circuit in input_circuit_2: + circuit.append(cirq.Circuit(cirq.measure(*qubits_1, key="m"))) + + input_circuit_3 = rqcg.generate_library_of_2q_circuits( + n_library_circuits=5, two_qubit_gate=cirq.CNOT**0.5, q0=qubits_2[0], q1=qubits_2[3] + ) + for circuit in input_circuit_3: + circuit.append(cirq.Circuit(cirq.measure(*qubits_2, key="m"))) + + input_circuit_4 = rqcg.generate_library_of_2q_circuits( + n_library_circuits=5, two_qubit_gate=cirq.ISWAP**0.5, q0=qubits_2[1], q1=qubits_1[2] + ) + for circuit in input_circuit_4: + circuit.append(cirq.Circuit(cirq.measure(*qubits_2, key="m"))) + + input_circuits = input_circuit_1 + input_circuit_2 + input_circuit_3 + input_circuit_4 + + sampler = NoisySingleQubitReadoutSampler(p0=0.1, p1=0.3, seed=1234) + circuit_repetitions = 1 + rng = np.random.default_rng() + readout_repetitions = 1000 + + 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, + ) + ) + + for measurement in measurements: + assert isinstance(measurement, ResultDict) + + for qlist, readout_calibration_result in readout_calibration_results.items(): + assert isinstance(qlist, tuple) + assert all(isinstance(q, cirq.Qid) for q in qlist) + assert isinstance(readout_calibration_result, SingleQubitReadoutCalibrationResult) + + for error in readout_calibration_result.zero_state_errors.values(): + assert 0.08 < error < 0.12 + for error in readout_calibration_result.one_state_errors.values(): + assert 0.28 < error < 0.32 + assert readout_calibration_result.repetitions == readout_repetitions + assert isinstance(readout_calibration_result.timestamp, float) - for error in readout_calibration_results.zero_state_errors.values(): - assert 0.08 < error < 0.12 - for error in readout_calibration_results.one_state_errors.values(): - assert 0.28 < error < 0.32 - assert readout_calibration_results.repetitions == readout_repetitions - assert isinstance(readout_calibration_results.timestamp, float) + +def test_can_handle_zero_random_bitstring(): + """Test shuffled 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_circuit_1 = rqcg.generate_library_of_2q_circuits( + n_library_circuits=5, two_qubit_gate=cirq.ISWAP**0.5, q0=qubits_1[0], q1=qubits_1[1] + ) + for circuit in input_circuit_1: + circuit.append(cirq.Circuit(cirq.measure(*qubits_1, key="m"))) + + input_circuit_2 = rqcg.generate_library_of_2q_circuits( + n_library_circuits=5, two_qubit_gate=cirq.CNOT**0.5, q0=qubits_1[1], q1=qubits_1[2] + ) + for circuit in input_circuit_2: + circuit.append(cirq.Circuit(cirq.measure(*qubits_1, key="m"))) + + input_circuit_3 = rqcg.generate_library_of_2q_circuits( + n_library_circuits=5, two_qubit_gate=cirq.CNOT**0.5, q0=qubits_2[0], q1=qubits_2[3] + ) + for circuit in input_circuit_3: + circuit.append(cirq.Circuit(cirq.measure(*qubits_2, key="m"))) + + input_circuit_4 = rqcg.generate_library_of_2q_circuits( + n_library_circuits=5, two_qubit_gate=cirq.ISWAP**0.5, q0=qubits_2[1], q1=qubits_1[2] + ) + for circuit in input_circuit_4: + circuit.append(cirq.Circuit(cirq.measure(*qubits_2, key="m"))) + + input_circuits = input_circuit_1 + input_circuit_2 + input_circuit_3 + input_circuit_4 + + sampler = NoisySingleQubitReadoutSampler(p0=0.1, p1=0.3, seed=1234) + circuit_repetitions = 1 + rng = np.random.default_rng() + readout_repetitions = 1000 + + 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, + ) + ) + + for measurement in measurements: + assert isinstance(measurement, ResultDict) + # Check that the readout_calibration_results is empty + assert len(readout_calibration_results.items()) == 0 def test_empty_input_circuits(): @@ -256,16 +388,16 @@ def test_mismatch_circuit_repetitions(): def test_zero_num_random_bitstrings(): - """Test that the number of random bitstrings is zero.""" + """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 non-zero num_random_bitstrings."): + 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=0, + num_random_bitstrings=-1, readout_repetitions=100, )