|
| 1 | +# Copyright 2025 The Cirq Developers |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# https://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | +"""Tools for running circuits in a shuffled order with readout error benchmarking.""" |
| 15 | +import time |
| 16 | +from typing import Optional, Union |
| 17 | + |
| 18 | +import numpy as np |
| 19 | + |
| 20 | +from cirq import ops, circuits, work, protocols |
| 21 | +from cirq.experiments import SingleQubitReadoutCalibrationResult |
| 22 | +from cirq.study import ResultDict |
| 23 | + |
| 24 | + |
| 25 | +def _validate_input( |
| 26 | + input_circuits: list[circuits.Circuit], |
| 27 | + circuit_repetitions: Union[int, list[int]], |
| 28 | + rng_or_seed: Union[np.random.Generator, int], |
| 29 | + num_random_bitstrings: int, |
| 30 | + readout_repetitions: int, |
| 31 | +): |
| 32 | + if not input_circuits: |
| 33 | + raise ValueError("Input circuits must not be empty.") |
| 34 | + # Check input_circuits type is cirq.circuits |
| 35 | + if not all(isinstance(circuit, circuits.Circuit) for circuit in input_circuits): |
| 36 | + raise ValueError("Input circuits must be of type cirq.Circuit.") |
| 37 | + # Check input_circuits have measurements |
| 38 | + for circuit in input_circuits: |
| 39 | + if not any(protocols.is_measurement(circuit) for op in circuit.all_operations()): |
| 40 | + raise ValueError("Input circuits must have measurements.") |
| 41 | + |
| 42 | + # Check circuit_repetitions |
| 43 | + if isinstance(circuit_repetitions, int): |
| 44 | + if circuit_repetitions <= 0: |
| 45 | + raise ValueError("Must provide non-zero circuit_repetitions.") |
| 46 | + if isinstance(circuit_repetitions, list) and len(circuit_repetitions) != len(input_circuits): |
| 47 | + raise ValueError("Number of circuit_repetitions must match the number of input circuits.") |
| 48 | + |
| 49 | + # Check rng is a numpy random generator |
| 50 | + if not isinstance(rng_or_seed, np.random.Generator) and not isinstance(rng_or_seed, int): |
| 51 | + raise ValueError("Must provide a numpy random generator or a seed") |
| 52 | + |
| 53 | + # Check num_random_bitstrings is bigger than 0 |
| 54 | + if num_random_bitstrings <= 0: |
| 55 | + raise ValueError("Must provide non-zero num_random_bitstrings.") |
| 56 | + |
| 57 | + # Check readout_repetitions is bigger than 0 |
| 58 | + if readout_repetitions <= 0: |
| 59 | + raise ValueError("Must provide non-zero readout_repetitions for readout calibration.") |
| 60 | + |
| 61 | + |
| 62 | +def _generate_readout_calibration_circuits( |
| 63 | + qubits: list[ops.Qid], rng: np.random.Generator, num_random_bitstrings: int |
| 64 | +) -> tuple[list[circuits.Circuit], np.ndarray]: |
| 65 | + """Generates the readout calibration circuits with random bitstrings.""" |
| 66 | + bit_to_gate = (ops.I, ops.X) |
| 67 | + |
| 68 | + random_bitstrings = rng.integers(0, 2, size=(num_random_bitstrings, len(qubits))) |
| 69 | + |
| 70 | + readout_calibration_circuits = [] |
| 71 | + for bitstr in random_bitstrings: |
| 72 | + readout_calibration_circuits.append( |
| 73 | + circuits.Circuit( |
| 74 | + [bit_to_gate[bit](qubit) for bit, qubit in zip(bitstr, qubits)] |
| 75 | + + [ops.M(qubits, key="m")] |
| 76 | + ) |
| 77 | + ) |
| 78 | + return readout_calibration_circuits, random_bitstrings |
| 79 | + |
| 80 | + |
| 81 | +def _shuffle_circuits( |
| 82 | + all_circuits: list[circuits.Circuit], all_repetitions: list[int], rng: np.random.Generator |
| 83 | +) -> tuple[list[circuits.Circuit], list[int], np.ndarray]: |
| 84 | + """Shuffles the input circuits and readout calibration circuits.""" |
| 85 | + shuf_order = rng.permutation(len(all_circuits)) |
| 86 | + unshuf_order = np.zeros_like(shuf_order) # Inverse permutation |
| 87 | + unshuf_order[shuf_order] = np.arange(len(all_circuits)) |
| 88 | + shuffled_circuits = [all_circuits[i] for i in shuf_order] |
| 89 | + all_repetitions = [all_repetitions[i] for i in shuf_order] |
| 90 | + return shuffled_circuits, all_repetitions, unshuf_order |
| 91 | + |
| 92 | + |
| 93 | +def _analyze_readout_results( |
| 94 | + unshuffled_readout_measurements: list[ResultDict], |
| 95 | + random_bitstrings: np.ndarray, |
| 96 | + readout_repetitions: int, |
| 97 | + qubits: list[ops.Qid], |
| 98 | + timestamp: float, |
| 99 | +) -> SingleQubitReadoutCalibrationResult: |
| 100 | + """Analyzes the readout error rates from the unshuffled measurements. |
| 101 | +
|
| 102 | + Args: |
| 103 | + readout_measurements: A list of dictionaries containing the measurement results |
| 104 | + for each readout calibration circuit. |
| 105 | + random_bitstrings: A numpy array of random bitstrings used for measuring readout. |
| 106 | + readout_repetitions: The number of repetitions for each readout bitstring. |
| 107 | + qubits: The list of qubits for which the readout error rates are to be calculated. |
| 108 | +
|
| 109 | + Returns: |
| 110 | + A dictionary mapping each qubit to a tuple of readout error rates(e0 and e1), |
| 111 | + where e0 is the 0->1 readout error rate and e1 is the 1->0 readout error rate. |
| 112 | + """ |
| 113 | + |
| 114 | + zero_state_trials = np.zeros((1, len(qubits)), dtype=np.int64) |
| 115 | + one_state_trials = np.zeros((1, len(qubits)), dtype=np.int64) |
| 116 | + zero_state_totals = np.zeros((1, len(qubits)), dtype=np.int64) |
| 117 | + one_state_totals = np.zeros((1, len(qubits)), dtype=np.int64) |
| 118 | + for measurement_result, bitstr in zip(unshuffled_readout_measurements, random_bitstrings): |
| 119 | + for _, trial_result in measurement_result.measurements.items(): |
| 120 | + trial_result = trial_result.astype(np.int64) # Cast to int64 |
| 121 | + sample_counts = np.sum(trial_result, axis=0) |
| 122 | + |
| 123 | + zero_state_trials += sample_counts * (1 - bitstr) |
| 124 | + zero_state_totals += readout_repetitions * (1 - bitstr) |
| 125 | + one_state_trials += (readout_repetitions - sample_counts) * bitstr |
| 126 | + one_state_totals += readout_repetitions * bitstr |
| 127 | + |
| 128 | + zero_state_errors = { |
| 129 | + q: ( |
| 130 | + zero_state_trials[0][qubit_idx] / zero_state_totals[0][qubit_idx] |
| 131 | + if zero_state_totals[0][qubit_idx] > 0 |
| 132 | + else np.nan |
| 133 | + ) |
| 134 | + for qubit_idx, q in enumerate(qubits) |
| 135 | + } |
| 136 | + |
| 137 | + one_state_errors = { |
| 138 | + q: ( |
| 139 | + one_state_trials[0][qubit_idx] / one_state_totals[0][qubit_idx] |
| 140 | + if one_state_totals[0][qubit_idx] > 0 |
| 141 | + else np.nan |
| 142 | + ) |
| 143 | + for qubit_idx, q in enumerate(qubits) |
| 144 | + } |
| 145 | + return SingleQubitReadoutCalibrationResult( |
| 146 | + zero_state_errors=zero_state_errors, |
| 147 | + one_state_errors=one_state_errors, |
| 148 | + repetitions=readout_repetitions, |
| 149 | + timestamp=timestamp, |
| 150 | + ) |
| 151 | + |
| 152 | + |
| 153 | +def run_shuffled_with_readout_benchmarking( |
| 154 | + input_circuits: list[circuits.Circuit], |
| 155 | + sampler: work.Sampler, |
| 156 | + circuit_repetitions: Union[int, list[int]], |
| 157 | + rng_or_seed: Union[np.random.Generator, int], |
| 158 | + num_random_bitstrings: int = 100, |
| 159 | + readout_repetitions: int = 1000, |
| 160 | + qubits: Optional[list[ops.Qid]] = None, |
| 161 | +) -> tuple[list[ResultDict], SingleQubitReadoutCalibrationResult]: |
| 162 | + """Run the circuits in a shuffled order with readout error benchmarking. |
| 163 | +
|
| 164 | + Args: |
| 165 | + input_circuits: The circuits to run. |
| 166 | + sampler: The sampler to use. |
| 167 | + circuit_repetitions: The repetitions for `circuits`. |
| 168 | + rng_or_seed: A random number generator used to generate readout circuits. |
| 169 | + Or an integer seed. |
| 170 | + num_random_bitstrings: The number of random bitstrings for measuring readout. |
| 171 | + readout_repetitions: The number of repetitions for each readout bitstring. |
| 172 | + qubits: The qubits to benchmark readout errors. If None, all qubits in the |
| 173 | + input_circuits are used. |
| 174 | +
|
| 175 | + Returns: |
| 176 | + A tuple containing: |
| 177 | + - A list of dictionaries with the unshuffled measurement results. |
| 178 | + - A dictionary mapping each qubit to a tuple of readout error rates(e0 and e1), |
| 179 | + where e0 is the 0->1 readout error rate and e1 is the 1->0 readout error rate. |
| 180 | +
|
| 181 | + """ |
| 182 | + |
| 183 | + _validate_input( |
| 184 | + input_circuits, circuit_repetitions, rng_or_seed, num_random_bitstrings, readout_repetitions |
| 185 | + ) |
| 186 | + |
| 187 | + # If input qubits is None, extract qubits from input circuits |
| 188 | + if qubits is None: |
| 189 | + qubits_set: set[ops.Qid] = set() |
| 190 | + for circuit in input_circuits: |
| 191 | + qubits_set.update(circuit.all_qubits()) |
| 192 | + qubits = sorted(qubits_set) |
| 193 | + |
| 194 | + # Generate the readout calibration circuits |
| 195 | + rng = ( |
| 196 | + rng_or_seed |
| 197 | + if isinstance(rng_or_seed, np.random.Generator) |
| 198 | + else np.random.default_rng(rng_or_seed) |
| 199 | + ) |
| 200 | + readout_calibration_circuits, random_bitstrings = _generate_readout_calibration_circuits( |
| 201 | + qubits, rng, num_random_bitstrings |
| 202 | + ) |
| 203 | + |
| 204 | + # Shuffle the circuits |
| 205 | + if isinstance(circuit_repetitions, int): |
| 206 | + circuit_repetitions = [circuit_repetitions] * len(input_circuits) |
| 207 | + all_repetitions = circuit_repetitions + [readout_repetitions] * len( |
| 208 | + readout_calibration_circuits |
| 209 | + ) |
| 210 | + |
| 211 | + shuffled_circuits, all_repetitions, unshuf_order = _shuffle_circuits( |
| 212 | + input_circuits + readout_calibration_circuits, all_repetitions, rng |
| 213 | + ) |
| 214 | + |
| 215 | + # Run the shuffled circuits and measure |
| 216 | + results = sampler.run_batch(shuffled_circuits, repetitions=all_repetitions) |
| 217 | + timestamp = time.time() |
| 218 | + shuffled_measurements = [res[0] for res in results] |
| 219 | + unshuffled_measurements = [shuffled_measurements[i] for i in unshuf_order] |
| 220 | + |
| 221 | + unshuffled_input_circuits_measiurements = unshuffled_measurements[: len(input_circuits)] |
| 222 | + unshuffled_readout_measurements = unshuffled_measurements[len(input_circuits) :] |
| 223 | + |
| 224 | + # Analyze results |
| 225 | + readout_calibration_results = _analyze_readout_results( |
| 226 | + unshuffled_readout_measurements, random_bitstrings, readout_repetitions, qubits, timestamp |
| 227 | + ) |
| 228 | + |
| 229 | + return unshuffled_input_circuits_measiurements, readout_calibration_results |
0 commit comments