Skip to content

Commit eec204f

Browse files
Add a function for running circuits in a shuffled order with readout error benchmarking. (#6945)
* (1/n)Add initial shuffle_circuits_with_readout_benchmarking method. The first commit only extracts qubits from the input circuits. The commit also adds a corresponding test. * (2/n) Generate random bitstrings, and use the generated bitstrings to produce readout calibration circuits * Add logics to run the shuffled circuits and measure the results. Also add some tests to check the correctness of the implementation. * Remove unnecessary imports * Address @NoureldinYosri comments * Address @eliottrosenberg's comment to move the method under contrib directory * Fix a issue on test * Revert change in cirq-core/cirq/experiments/__init__.py * Update cirq-core/cirq/contrib/shuffle_circuits/shuffle_circuits_with_readout_benchmarking.py Co-authored-by: Noureldin <[email protected]> * Update cirq-core/cirq/contrib/shuffle_circuits/shuffle_circuits_with_readout_benchmarking.py Co-authored-by: Noureldin <[email protected]> * Address NoureldinYosri@ comments * Address @eliottrosenberg comments * Address @NoureldinYosri comments Also address lint and format errors * Address type and format check. * Fix a type error * remove unused var --------- Co-authored-by: Noureldin <[email protected]>
1 parent 749f300 commit eec204f

File tree

3 files changed

+549
-0
lines changed

3 files changed

+549
-0
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
"""Utility for shuffling circuits and do readout error benchmarking."""
15+
16+
from cirq.contrib.shuffle_circuits.shuffle_circuits_with_readout_benchmarking import (
17+
run_shuffled_with_readout_benchmarking as run_shuffled_with_readout_benchmarking,
18+
)
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
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

Comments
 (0)