cirq-core 1.5.0.dev20250324234903__py3-none-any.whl → 1.5.0.dev20250325074340__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cirq-core might be problematic. Click here for more details.

@@ -0,0 +1,528 @@
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
+
15
+ from typing import Dict, Sequence
16
+ import random
17
+
18
+ import pytest
19
+
20
+ import cirq
21
+ import numpy as np
22
+
23
+ from cirq.contrib.paulistring import measure_pauli_strings
24
+ from cirq.experiments.single_qubit_readout_calibration_test import NoisySingleQubitReadoutSampler
25
+ from cirq.experiments import SingleQubitReadoutCalibrationResult
26
+
27
+
28
+ def _create_ghz(number_of_qubits: int, qubits: Sequence[cirq.Qid]) -> cirq.Circuit:
29
+ ghz_circuit = cirq.Circuit(
30
+ cirq.H(qubits[0]),
31
+ *[cirq.CNOT(qubits[i - 1], qubits[i]) for i in range(1, number_of_qubits)],
32
+ )
33
+ return ghz_circuit
34
+
35
+
36
+ def _generate_random_pauli_string(qubits: Sequence[cirq.Qid], enable_coeff: bool = False):
37
+ pauli_ops = [cirq.I, cirq.X, cirq.Y, cirq.Z]
38
+
39
+ # Ensure at least one non-identity.
40
+ operators = {q: cirq.I(q) for q in qubits} # Start with all identities
41
+ # Choose a random subset of qubits to have non-identity operators
42
+ non_identity_qubits = random.sample(qubits, random.randint(1, len(qubits)))
43
+ for q in non_identity_qubits:
44
+ operators[q] = random.choice([cirq.X, cirq.Y, cirq.Z])(q) # Only non-identity ops
45
+ operators = {q: random.choice(pauli_ops) for q in qubits}
46
+
47
+ if enable_coeff:
48
+ coefficient = (2 * random.random() - 1) * 100
49
+ return coefficient * cirq.PauliString(operators)
50
+ return cirq.PauliString(operators)
51
+
52
+
53
+ def _ideal_expectation_based_on_pauli_string(
54
+ pauli_string: cirq.PauliString, final_state_vector: np.ndarray
55
+ ) -> float:
56
+ return pauli_string.expectation_from_state_vector(
57
+ final_state_vector, qubit_map={q: i for i, q in enumerate(pauli_string.qubits)}
58
+ )
59
+
60
+
61
+ def test_pauli_string_measurement_errors_no_noise() -> None:
62
+ """Test that the mitigated expectation is close to the ideal expectation
63
+ based on the Pauli string"""
64
+
65
+ qubits = cirq.LineQubit.range(5)
66
+ circuit = cirq.FrozenCircuit(_create_ghz(5, qubits))
67
+ sampler = cirq.Simulator()
68
+
69
+ circuits_to_pauli: Dict[cirq.FrozenCircuit, list[cirq.PauliString]] = {}
70
+ circuits_to_pauli[circuit] = [_generate_random_pauli_string(qubits) for _ in range(3)]
71
+
72
+ circuits_with_pauli_expectations = measure_pauli_strings(
73
+ circuits_to_pauli, sampler, 1000, 1000, 1000, 1000
74
+ )
75
+
76
+ for circuit_with_pauli_expectations in circuits_with_pauli_expectations:
77
+ assert isinstance(circuit_with_pauli_expectations.circuit, cirq.FrozenCircuit)
78
+
79
+ expected_val_simulation = sampler.simulate(
80
+ circuit_with_pauli_expectations.circuit.unfreeze()
81
+ )
82
+ final_state_vector = expected_val_simulation.final_state_vector
83
+
84
+ for pauli_string_measurement_results in circuit_with_pauli_expectations.results:
85
+ # Since there is no noise, the mitigated and unmitigated expectations should be the same
86
+ assert np.isclose(
87
+ pauli_string_measurement_results.mitigated_expectation,
88
+ pauli_string_measurement_results.unmitigated_expectation,
89
+ )
90
+ assert np.isclose(
91
+ pauli_string_measurement_results.mitigated_expectation,
92
+ _ideal_expectation_based_on_pauli_string(
93
+ pauli_string_measurement_results.pauli_string, final_state_vector
94
+ ),
95
+ atol=4 * pauli_string_measurement_results.mitigated_stddev,
96
+ )
97
+ assert isinstance(
98
+ pauli_string_measurement_results.calibration_result,
99
+ SingleQubitReadoutCalibrationResult,
100
+ )
101
+ assert pauli_string_measurement_results.calibration_result.zero_state_errors == {
102
+ q: 0 for q in pauli_string_measurement_results.pauli_string.qubits
103
+ }
104
+ assert pauli_string_measurement_results.calibration_result.one_state_errors == {
105
+ q: 0 for q in pauli_string_measurement_results.pauli_string.qubits
106
+ }
107
+
108
+
109
+ def test_pauli_string_measurement_errors_with_coefficient_no_noise() -> None:
110
+ """Test that the mitigated expectation is close to the ideal expectation
111
+ based on the Pauli string"""
112
+
113
+ qubits = cirq.LineQubit.range(5)
114
+ circuit = cirq.FrozenCircuit(_create_ghz(5, qubits))
115
+ sampler = cirq.Simulator()
116
+
117
+ circuits_to_pauli: Dict[cirq.FrozenCircuit, list[cirq.PauliString]] = {}
118
+ circuits_to_pauli[circuit] = [_generate_random_pauli_string(qubits, True) for _ in range(3)]
119
+
120
+ circuits_with_pauli_expectations = measure_pauli_strings(
121
+ circuits_to_pauli, sampler, 1000, 1000, 1000, 1000
122
+ )
123
+
124
+ for circuit_with_pauli_expectations in circuits_with_pauli_expectations:
125
+ assert isinstance(circuit_with_pauli_expectations.circuit, cirq.FrozenCircuit)
126
+
127
+ expected_val_simulation = sampler.simulate(
128
+ circuit_with_pauli_expectations.circuit.unfreeze()
129
+ )
130
+ final_state_vector = expected_val_simulation.final_state_vector
131
+
132
+ for pauli_string_measurement_results in circuit_with_pauli_expectations.results:
133
+ # Since there is no noise, the mitigated and unmitigated expectations should be the same
134
+ assert np.isclose(
135
+ pauli_string_measurement_results.mitigated_expectation,
136
+ pauli_string_measurement_results.unmitigated_expectation,
137
+ )
138
+ assert np.isclose(
139
+ pauli_string_measurement_results.mitigated_expectation,
140
+ _ideal_expectation_based_on_pauli_string(
141
+ pauli_string_measurement_results.pauli_string, final_state_vector
142
+ ),
143
+ atol=4 * pauli_string_measurement_results.mitigated_stddev,
144
+ )
145
+ assert isinstance(
146
+ pauli_string_measurement_results.calibration_result,
147
+ SingleQubitReadoutCalibrationResult,
148
+ )
149
+ assert pauli_string_measurement_results.calibration_result.zero_state_errors == {
150
+ q: 0 for q in pauli_string_measurement_results.pauli_string.qubits
151
+ }
152
+ assert pauli_string_measurement_results.calibration_result.one_state_errors == {
153
+ q: 0 for q in pauli_string_measurement_results.pauli_string.qubits
154
+ }
155
+
156
+
157
+ def test_pauli_string_measurement_errors_with_noise() -> None:
158
+ """Test that the mitigated expectation is close to the ideal expectation
159
+ based on the Pauli string"""
160
+ qubits = cirq.LineQubit.range(7)
161
+ circuit = cirq.FrozenCircuit(_create_ghz(7, qubits))
162
+ sampler = NoisySingleQubitReadoutSampler(p0=0.1, p1=0.005, seed=1234)
163
+ simulator = cirq.Simulator()
164
+
165
+ circuits_to_pauli: Dict[cirq.FrozenCircuit, list[cirq.PauliString]] = {}
166
+ circuits_to_pauli[circuit] = [_generate_random_pauli_string(qubits) for _ in range(3)]
167
+
168
+ circuits_with_pauli_expectations = measure_pauli_strings(
169
+ circuits_to_pauli, sampler, 1000, 1000, 1000, np.random.default_rng()
170
+ )
171
+
172
+ for circuit_with_pauli_expectations in circuits_with_pauli_expectations:
173
+ assert isinstance(circuit_with_pauli_expectations.circuit, cirq.FrozenCircuit)
174
+
175
+ expected_val_simulation = simulator.simulate(
176
+ circuit_with_pauli_expectations.circuit.unfreeze()
177
+ )
178
+ final_state_vector = expected_val_simulation.final_state_vector
179
+
180
+ for pauli_string_measurement_results in circuit_with_pauli_expectations.results:
181
+ assert np.isclose(
182
+ pauli_string_measurement_results.mitigated_expectation,
183
+ _ideal_expectation_based_on_pauli_string(
184
+ pauli_string_measurement_results.pauli_string, final_state_vector
185
+ ),
186
+ atol=4 * pauli_string_measurement_results.mitigated_stddev,
187
+ )
188
+
189
+ assert isinstance(
190
+ pauli_string_measurement_results.calibration_result,
191
+ SingleQubitReadoutCalibrationResult,
192
+ )
193
+
194
+ for (
195
+ error
196
+ ) in pauli_string_measurement_results.calibration_result.zero_state_errors.values():
197
+ assert 0.08 < error < 0.12
198
+ for (
199
+ error
200
+ ) in pauli_string_measurement_results.calibration_result.one_state_errors.values():
201
+ assert 0.0045 < error < 0.0055
202
+
203
+
204
+ def test_many_circuits_input_measurement_with_noise() -> None:
205
+ """Test that the mitigated expectation is close to the ideal expectation
206
+ based on the Pauli string for multiple circuits"""
207
+ qubits_1 = cirq.LineQubit.range(3)
208
+ qubits_2 = [
209
+ cirq.GridQubit(0, 1),
210
+ cirq.GridQubit(1, 1),
211
+ cirq.GridQubit(1, 0),
212
+ cirq.GridQubit(1, 2),
213
+ cirq.GridQubit(2, 1),
214
+ ]
215
+ qubits_3 = cirq.LineQubit.range(8)
216
+
217
+ circuit_1 = cirq.FrozenCircuit(_create_ghz(3, qubits_1))
218
+ circuit_2 = cirq.FrozenCircuit(_create_ghz(5, qubits_2))
219
+ circuit_3 = cirq.FrozenCircuit(_create_ghz(8, qubits_3))
220
+
221
+ circuits_to_pauli: Dict[cirq.FrozenCircuit, list[cirq.PauliString]] = {}
222
+ circuits_to_pauli[circuit_1] = [_generate_random_pauli_string(qubits_1) for _ in range(3)]
223
+ circuits_to_pauli[circuit_2] = [_generate_random_pauli_string(qubits_2) for _ in range(3)]
224
+ circuits_to_pauli[circuit_3] = [_generate_random_pauli_string(qubits_3) for _ in range(3)]
225
+
226
+ sampler = NoisySingleQubitReadoutSampler(p0=0.03, p1=0.005, seed=1234)
227
+ simulator = cirq.Simulator()
228
+
229
+ circuits_with_pauli_expectations = measure_pauli_strings(
230
+ circuits_to_pauli, sampler, 1000, 1000, 1000, np.random.default_rng()
231
+ )
232
+
233
+ for circuit_with_pauli_expectations in circuits_with_pauli_expectations:
234
+ assert isinstance(circuit_with_pauli_expectations.circuit, cirq.FrozenCircuit)
235
+
236
+ expected_val_simulation = simulator.simulate(
237
+ circuit_with_pauli_expectations.circuit.unfreeze()
238
+ )
239
+ final_state_vector = expected_val_simulation.final_state_vector
240
+
241
+ for pauli_string_measurement_results in circuit_with_pauli_expectations.results:
242
+ assert np.isclose(
243
+ pauli_string_measurement_results.mitigated_expectation,
244
+ _ideal_expectation_based_on_pauli_string(
245
+ pauli_string_measurement_results.pauli_string, final_state_vector
246
+ ),
247
+ atol=4 * pauli_string_measurement_results.mitigated_stddev,
248
+ )
249
+ assert isinstance(
250
+ pauli_string_measurement_results.calibration_result,
251
+ SingleQubitReadoutCalibrationResult,
252
+ )
253
+ for (
254
+ error
255
+ ) in pauli_string_measurement_results.calibration_result.zero_state_errors.values():
256
+ assert 0.025 < error < 0.035
257
+ for (
258
+ error
259
+ ) in pauli_string_measurement_results.calibration_result.one_state_errors.values():
260
+ assert 0.0045 < error < 0.0055
261
+
262
+
263
+ def test_allow_measurement_without_readout_mitigation() -> None:
264
+ """Test that the function allows to measure without error mitigation"""
265
+ qubits = cirq.LineQubit.range(7)
266
+ circuit = cirq.FrozenCircuit(_create_ghz(7, qubits))
267
+ sampler = NoisySingleQubitReadoutSampler(p0=0.1, p1=0.005, seed=1234)
268
+
269
+ circuits_to_pauli: Dict[cirq.FrozenCircuit, list[cirq.PauliString]] = {}
270
+ circuits_to_pauli[circuit] = [
271
+ _generate_random_pauli_string(qubits, True),
272
+ _generate_random_pauli_string(qubits),
273
+ _generate_random_pauli_string(qubits),
274
+ ]
275
+
276
+ circuits_with_pauli_expectations = measure_pauli_strings(
277
+ circuits_to_pauli, sampler, 1000, 1000, 0, np.random.default_rng()
278
+ )
279
+
280
+ for circuit_with_pauli_expectations in circuits_with_pauli_expectations:
281
+ assert isinstance(circuit_with_pauli_expectations.circuit, cirq.FrozenCircuit)
282
+
283
+ for pauli_string_measurement_results in circuit_with_pauli_expectations.results:
284
+ # Since there's no mitigation, the mitigated and unmitigated expectations
285
+ # should be the same
286
+ assert np.isclose(
287
+ pauli_string_measurement_results.mitigated_expectation,
288
+ pauli_string_measurement_results.unmitigated_expectation,
289
+ )
290
+ assert pauli_string_measurement_results.calibration_result is None
291
+
292
+
293
+ def test_many_circuits_with_coefficient() -> None:
294
+ """Test that the mitigated expectation is close to the ideal expectation
295
+ based on the Pauli string for multiple circuits"""
296
+ qubits_1 = cirq.LineQubit.range(3)
297
+ qubits_2 = [
298
+ cirq.GridQubit(0, 1),
299
+ cirq.GridQubit(1, 1),
300
+ cirq.GridQubit(1, 0),
301
+ cirq.GridQubit(1, 2),
302
+ cirq.GridQubit(2, 1),
303
+ ]
304
+ qubits_3 = cirq.LineQubit.range(8)
305
+
306
+ circuit_1 = cirq.FrozenCircuit(_create_ghz(3, qubits_1))
307
+ circuit_2 = cirq.FrozenCircuit(_create_ghz(5, qubits_2))
308
+ circuit_3 = cirq.FrozenCircuit(_create_ghz(8, qubits_3))
309
+
310
+ circuits_to_pauli: Dict[cirq.FrozenCircuit, list[cirq.PauliString]] = {}
311
+ circuits_to_pauli[circuit_1] = [_generate_random_pauli_string(qubits_1, True) for _ in range(3)]
312
+ circuits_to_pauli[circuit_2] = [_generate_random_pauli_string(qubits_2, True) for _ in range(3)]
313
+ circuits_to_pauli[circuit_3] = [_generate_random_pauli_string(qubits_3, True) for _ in range(3)]
314
+
315
+ sampler = NoisySingleQubitReadoutSampler(p0=0.03, p1=0.005, seed=1234)
316
+ simulator = cirq.Simulator()
317
+
318
+ circuits_with_pauli_expectations = measure_pauli_strings(
319
+ circuits_to_pauli, sampler, 1000, 1000, 1000, np.random.default_rng()
320
+ )
321
+
322
+ for circuit_with_pauli_expectations in circuits_with_pauli_expectations:
323
+ assert isinstance(circuit_with_pauli_expectations.circuit, cirq.FrozenCircuit)
324
+
325
+ expected_val_simulation = simulator.simulate(
326
+ circuit_with_pauli_expectations.circuit.unfreeze()
327
+ )
328
+ final_state_vector = expected_val_simulation.final_state_vector
329
+
330
+ for pauli_string_measurement_results in circuit_with_pauli_expectations.results:
331
+ assert np.isclose(
332
+ pauli_string_measurement_results.mitigated_expectation,
333
+ _ideal_expectation_based_on_pauli_string(
334
+ pauli_string_measurement_results.pauli_string, final_state_vector
335
+ ),
336
+ atol=4 * pauli_string_measurement_results.mitigated_stddev,
337
+ )
338
+ assert isinstance(
339
+ pauli_string_measurement_results.calibration_result,
340
+ SingleQubitReadoutCalibrationResult,
341
+ )
342
+ for (
343
+ error
344
+ ) in pauli_string_measurement_results.calibration_result.zero_state_errors.values():
345
+ assert 0.025 < error < 0.035
346
+ for (
347
+ error
348
+ ) in pauli_string_measurement_results.calibration_result.one_state_errors.values():
349
+ assert 0.0045 < error < 0.0055
350
+
351
+
352
+ def test_coefficient_not_real_number() -> None:
353
+ """Test that the coefficient of input pauli string is not real.
354
+ Should return error in this case"""
355
+ qubits_1 = cirq.LineQubit.range(3)
356
+ random_pauli_string = _generate_random_pauli_string(qubits_1, True) * (3 + 4j)
357
+ circuit_1 = cirq.FrozenCircuit(_create_ghz(3, qubits_1))
358
+
359
+ circuits_to_pauli: Dict[cirq.FrozenCircuit, list[cirq.PauliString]] = {}
360
+ circuits_to_pauli[circuit_1] = [
361
+ random_pauli_string,
362
+ _generate_random_pauli_string(qubits_1, True),
363
+ _generate_random_pauli_string(qubits_1, True),
364
+ ]
365
+
366
+ with pytest.raises(
367
+ ValueError,
368
+ match="Cannot compute expectation value of a "
369
+ "non-Hermitian PauliString. Coefficient must be real.",
370
+ ):
371
+ measure_pauli_strings(
372
+ circuits_to_pauli, cirq.Simulator(), 1000, 1000, 1000, np.random.default_rng()
373
+ )
374
+
375
+
376
+ def test_empty_input_circuits_to_pauli_mapping() -> None:
377
+ """Test that the input circuits are empty."""
378
+
379
+ with pytest.raises(ValueError, match="Input circuits must not be empty."):
380
+ measure_pauli_strings(
381
+ [], # type: ignore[arg-type]
382
+ cirq.Simulator(),
383
+ 1000,
384
+ 1000,
385
+ 1000,
386
+ np.random.default_rng(),
387
+ )
388
+
389
+
390
+ def test_invalid_input_circuit_type() -> None:
391
+ """Test that the input circuit type is not frozen circuit"""
392
+ qubits = cirq.LineQubit.range(5)
393
+
394
+ qubits_to_pauli: Dict[tuple, list[cirq.PauliString]] = {}
395
+ qubits_to_pauli[tuple(qubits)] = [cirq.PauliString({q: cirq.X for q in qubits})]
396
+ with pytest.raises(
397
+ TypeError, match="All keys in 'circuits_to_pauli' must be FrozenCircuit instances."
398
+ ):
399
+ measure_pauli_strings(
400
+ qubits_to_pauli, # type: ignore[arg-type]
401
+ cirq.Simulator(),
402
+ 1000,
403
+ 1000,
404
+ 1000,
405
+ np.random.default_rng(),
406
+ )
407
+
408
+
409
+ def test_invalid_input_pauli_string_type() -> None:
410
+ """Test input circuit is not mapping to a paulistring"""
411
+ qubits_1 = cirq.LineQubit.range(5)
412
+ qubits_2 = [
413
+ cirq.GridQubit(0, 1),
414
+ cirq.GridQubit(1, 1),
415
+ cirq.GridQubit(1, 0),
416
+ cirq.GridQubit(1, 2),
417
+ cirq.GridQubit(2, 1),
418
+ ]
419
+
420
+ circuit_1 = cirq.FrozenCircuit(_create_ghz(5, qubits_1))
421
+ circuit_2 = cirq.FrozenCircuit(_create_ghz(5, qubits_2))
422
+
423
+ circuits_to_pauli: Dict[cirq.FrozenCircuit, cirq.FrozenCircuit] = {}
424
+ circuits_to_pauli[circuit_1] = circuit_2
425
+
426
+ with pytest.raises(
427
+ TypeError,
428
+ match="All elements in the Pauli string lists must be cirq.PauliString "
429
+ "instances, got <class 'cirq.circuits.moment.Moment'>.",
430
+ ):
431
+ measure_pauli_strings(
432
+ circuits_to_pauli, # type: ignore[arg-type]
433
+ cirq.Simulator(),
434
+ 1000,
435
+ 1000,
436
+ 1000,
437
+ np.random.default_rng(),
438
+ )
439
+
440
+
441
+ def test_all_pauli_strings_are_pauli_i() -> None:
442
+ """Test that all input pauli are pauli I"""
443
+ qubits_1 = cirq.LineQubit.range(5)
444
+ qubits_2 = [
445
+ cirq.GridQubit(0, 1),
446
+ cirq.GridQubit(1, 1),
447
+ cirq.GridQubit(1, 0),
448
+ cirq.GridQubit(1, 2),
449
+ cirq.GridQubit(2, 1),
450
+ ]
451
+
452
+ circuit_1 = cirq.FrozenCircuit(_create_ghz(5, qubits_1))
453
+ circuit_2 = cirq.FrozenCircuit(_create_ghz(5, qubits_2))
454
+
455
+ circuits_to_pauli: Dict[cirq.FrozenCircuit, list[cirq.PauliString]] = {}
456
+ circuits_to_pauli[circuit_1] = [
457
+ cirq.PauliString({q: cirq.I for q in qubits_1}),
458
+ cirq.PauliString({q: cirq.X for q in qubits_1}),
459
+ ]
460
+ circuits_to_pauli[circuit_2] = [cirq.PauliString({q: cirq.X for q in qubits_2})]
461
+
462
+ with pytest.raises(
463
+ ValueError,
464
+ match="Empty Pauli strings or Pauli strings consisting"
465
+ "only of Pauli I are not allowed. Please provide"
466
+ "valid input Pauli strings.",
467
+ ):
468
+ measure_pauli_strings(
469
+ circuits_to_pauli, cirq.Simulator(), 1000, 1000, 1000, np.random.default_rng()
470
+ )
471
+
472
+
473
+ def test_zero_pauli_repetitions() -> None:
474
+ """Test that the pauli repetitions are zero."""
475
+ qubits = cirq.LineQubit.range(5)
476
+
477
+ circuit = cirq.FrozenCircuit(_create_ghz(5, qubits))
478
+
479
+ circuits_to_pauli: Dict[cirq.FrozenCircuit, list[cirq.PauliString]] = {}
480
+ circuits_to_pauli[circuit] = [cirq.PauliString({q: cirq.X for q in qubits})]
481
+ with pytest.raises(ValueError, match="Must provide non-zero pauli_repetitions."):
482
+ measure_pauli_strings(
483
+ circuits_to_pauli, cirq.Simulator(), 0, 1000, 1000, np.random.default_rng()
484
+ )
485
+
486
+
487
+ def test_negative_num_random_bitstrings() -> None:
488
+ """Test that the number of random bitstrings is smaller than zero."""
489
+ qubits = cirq.LineQubit.range(5)
490
+
491
+ circuit = cirq.FrozenCircuit(_create_ghz(5, qubits))
492
+
493
+ circuits_to_pauli: Dict[cirq.FrozenCircuit, list[cirq.PauliString]] = {}
494
+ circuits_to_pauli[circuit] = [cirq.PauliString({q: cirq.X for q in qubits})]
495
+ with pytest.raises(ValueError, match="Must provide zero or more num_random_bitstrings."):
496
+ measure_pauli_strings(
497
+ circuits_to_pauli, cirq.Simulator(), 1000, 1000, -1, np.random.default_rng()
498
+ )
499
+
500
+
501
+ def test_zero_readout_repetitions() -> None:
502
+ """Test that the readout repetitions is zero."""
503
+ qubits = cirq.LineQubit.range(5)
504
+
505
+ circuit = cirq.FrozenCircuit(_create_ghz(5, qubits))
506
+
507
+ circuits_to_pauli: Dict[cirq.FrozenCircuit, list[cirq.PauliString]] = {}
508
+ circuits_to_pauli[circuit] = [cirq.PauliString({q: cirq.X for q in qubits})]
509
+ with pytest.raises(
510
+ ValueError, match="Must provide non-zero readout_repetitions for readout" + " calibration."
511
+ ):
512
+ measure_pauli_strings(
513
+ circuits_to_pauli, cirq.Simulator(), 1000, 0, 1000, np.random.default_rng()
514
+ )
515
+
516
+
517
+ def test_rng_type_mismatch() -> None:
518
+ """Test that the rng is not a numpy random generator or a seed."""
519
+ qubits = cirq.LineQubit.range(5)
520
+
521
+ circuit = cirq.FrozenCircuit(_create_ghz(5, qubits))
522
+
523
+ circuits_to_pauli: Dict[cirq.FrozenCircuit, list[cirq.PauliString]] = {}
524
+ circuits_to_pauli[circuit] = [cirq.PauliString({q: cirq.X for q in qubits})]
525
+ with pytest.raises(ValueError, match="Must provide a numpy random generator or a seed"):
526
+ measure_pauli_strings(
527
+ circuits_to_pauli, cirq.Simulator(), 1000, 1000, 1000, "test" # type: ignore[arg-type]
528
+ )
@@ -13,7 +13,7 @@
13
13
  # limitations under the License.
14
14
  """Tools for running circuits in a shuffled order with readout error benchmarking."""
15
15
  import time
16
- from typing import Optional, Union
16
+ from typing import Optional, Union, Dict, Tuple, List
17
17
 
18
18
  import numpy as np
19
19
 
@@ -50,9 +50,9 @@ def _validate_input(
50
50
  if not isinstance(rng_or_seed, np.random.Generator) and not isinstance(rng_or_seed, int):
51
51
  raise ValueError("Must provide a numpy random generator or a seed")
52
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.")
53
+ # Check num_random_bitstrings is bigger than or equal to 0
54
+ if num_random_bitstrings < 0:
55
+ raise ValueError("Must provide zero or more num_random_bitstrings.")
56
56
 
57
57
  # Check readout_repetitions is bigger than 0
58
58
  if readout_repetitions <= 0:
@@ -157,8 +157,8 @@ def run_shuffled_with_readout_benchmarking(
157
157
  rng_or_seed: Union[np.random.Generator, int],
158
158
  num_random_bitstrings: int = 100,
159
159
  readout_repetitions: int = 1000,
160
- qubits: Optional[list[ops.Qid]] = None,
161
- ) -> tuple[list[ResultDict], SingleQubitReadoutCalibrationResult]:
160
+ qubits: Optional[Union[List[ops.Qid], List[List[ops.Qid]]]] = None,
161
+ ) -> tuple[list[ResultDict], Dict[Tuple[ops.Qid, ...], SingleQubitReadoutCalibrationResult]]:
162
162
  """Run the circuits in a shuffled order with readout error benchmarking.
163
163
 
164
164
  Args:
@@ -168,15 +168,16 @@ def run_shuffled_with_readout_benchmarking(
168
168
  rng_or_seed: A random number generator used to generate readout circuits.
169
169
  Or an integer seed.
170
170
  num_random_bitstrings: The number of random bitstrings for measuring readout.
171
+ If set to 0, no readout calibration circuits are generated.
171
172
  readout_repetitions: The number of repetitions for each readout bitstring.
172
173
  qubits: The qubits to benchmark readout errors. If None, all qubits in the
173
- input_circuits are used.
174
+ input_circuits are used. Can be a list of qubits or a list of tuples
175
+ of qubits.
174
176
 
175
177
  Returns:
176
178
  A tuple containing:
177
179
  - 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
+ - A dictionary mapping each tuple of qubits to a SingleQubitReadoutCalibrationResult.
180
181
 
181
182
  """
182
183
 
@@ -185,31 +186,44 @@ def run_shuffled_with_readout_benchmarking(
185
186
  )
186
187
 
187
188
  # If input qubits is None, extract qubits from input circuits
189
+ qubits_to_measure: List[List[ops.Qid]] = []
188
190
  if qubits is None:
189
191
  qubits_set: set[ops.Qid] = set()
190
192
  for circuit in input_circuits:
191
193
  qubits_set.update(circuit.all_qubits())
192
- qubits = sorted(qubits_set)
194
+ qubits_to_measure = [sorted(qubits_set)]
195
+ elif isinstance(qubits[0], ops.Qid):
196
+ qubits_to_measure = [qubits] # type: ignore
197
+ else:
198
+ qubits_to_measure = qubits # type: ignore
199
+
200
+ # Generate the readout calibration circuits if num_random_bitstrings>0
201
+ # Else all_readout_calibration_circuits and all_random_bitstrings are empty
202
+ all_readout_calibration_circuits = []
203
+ all_random_bitstrings = []
193
204
 
194
- # Generate the readout calibration circuits
195
205
  rng = (
196
206
  rng_or_seed
197
207
  if isinstance(rng_or_seed, np.random.Generator)
198
208
  else np.random.default_rng(rng_or_seed)
199
209
  )
200
- readout_calibration_circuits, random_bitstrings = _generate_readout_calibration_circuits(
201
- qubits, rng, num_random_bitstrings
202
- )
210
+ if num_random_bitstrings > 0:
211
+ for qubit_group in qubits_to_measure:
212
+ readout_calibration_circuits, random_bitstrings = (
213
+ _generate_readout_calibration_circuits(qubit_group, rng, num_random_bitstrings)
214
+ )
215
+ all_readout_calibration_circuits.extend(readout_calibration_circuits)
216
+ all_random_bitstrings.append(random_bitstrings)
203
217
 
204
218
  # Shuffle the circuits
205
219
  if isinstance(circuit_repetitions, int):
206
220
  circuit_repetitions = [circuit_repetitions] * len(input_circuits)
207
221
  all_repetitions = circuit_repetitions + [readout_repetitions] * len(
208
- readout_calibration_circuits
222
+ all_readout_calibration_circuits
209
223
  )
210
224
 
211
225
  shuffled_circuits, all_repetitions, unshuf_order = _shuffle_circuits(
212
- input_circuits + readout_calibration_circuits, all_repetitions, rng
226
+ input_circuits + all_readout_calibration_circuits, all_repetitions, rng
213
227
  )
214
228
 
215
229
  # Run the shuffled circuits and measure
@@ -222,8 +236,15 @@ def run_shuffled_with_readout_benchmarking(
222
236
  unshuffled_readout_measurements = unshuffled_measurements[len(input_circuits) :]
223
237
 
224
238
  # Analyze results
225
- readout_calibration_results = _analyze_readout_results(
226
- unshuffled_readout_measurements, random_bitstrings, readout_repetitions, qubits, timestamp
227
- )
239
+ readout_calibration_results = {}
240
+ start_idx = 0
241
+ for qubit_group, random_bitstrings in zip(qubits_to_measure, all_random_bitstrings):
242
+ end_idx = start_idx + len(random_bitstrings)
243
+ group_measurements = unshuffled_readout_measurements[start_idx:end_idx]
244
+ calibration_result = _analyze_readout_results(
245
+ group_measurements, random_bitstrings, readout_repetitions, qubit_group, timestamp
246
+ )
247
+ readout_calibration_results[tuple(qubit_group)] = calibration_result
248
+ start_idx = end_idx
228
249
 
229
250
  return unshuffled_input_circuits_measiurements, readout_calibration_results