Skip to content

Commit c8d8d59

Browse files
authored
(2/2) Add parameter sweep support for Pauli string measurements with readout mitigation (#7569)
Previous PR is #7435 This PR allow user to run measuring_pauli_strings in sweep mode. In this mode, the function uses parameterized circuits and sweeps parameters for both Pauli measurements and readout benchmarking
1 parent 63cfe53 commit c8d8d59

File tree

3 files changed

+194
-175
lines changed

3 files changed

+194
-175
lines changed

cirq-core/cirq/contrib/paulistring/pauli_string_measurement_with_readout_mitigation.py

Lines changed: 144 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,17 @@
2222

2323
import attrs
2424
import numpy as np
25+
import sympy
2526

2627
import cirq.contrib.shuffle_circuits.shuffle_circuits_with_readout_benchmarking as sc_readout
2728
from cirq import circuits, ops, study, work
2829
from cirq.experiments.readout_confusion_matrix import TensoredConfusionMatrices
30+
from cirq.study import ResultDict
2931

3032
if TYPE_CHECKING:
3133
from cirq.experiments.single_qubit_readout_calibration import (
3234
SingleQubitReadoutCalibrationResult,
3335
)
34-
from cirq.study import ResultDict
3536

3637

3738
@attrs.frozen
@@ -188,15 +189,15 @@ def _validate_input(
188189

189190
# Check pauli_repetitions is bigger than 0
190191
if pauli_repetitions <= 0:
191-
raise ValueError("Must provide non-zero pauli_repetitions.")
192+
raise ValueError("Must provide positive pauli_repetitions.")
192193

193194
# Check num_random_bitstrings is bigger than or equal to 0
194195
if num_random_bitstrings < 0:
195196
raise ValueError("Must provide zero or more num_random_bitstrings.")
196197

197198
# Check readout_repetitions is bigger than 0
198199
if readout_repetitions <= 0:
199-
raise ValueError("Must provide non-zero readout_repetitions for readout calibration.")
200+
raise ValueError("Must provide positive readout_repetitions for readout calibration.")
200201

201202

202203
def _normalize_input_paulis(
@@ -240,6 +241,90 @@ def _pauli_strings_to_basis_change_ops(
240241
return operations
241242

242243

244+
def _pauli_strings_to_basis_change_with_sweep(
245+
pauli_strings: list[ops.PauliString], qid_list: list[ops.Qid]
246+
) -> dict[str, float]:
247+
"""Decide single-qubit rotation sweep parameters for basis change.
248+
249+
Args:
250+
pauli_strings: A list of QWC Pauli strings.
251+
qid_list: A list of qubits to apply the basis change on.
252+
Returns:
253+
A dictionary mapping parameter names to their values for basis change.
254+
"""
255+
params_dict = {}
256+
257+
for qid, qubit in enumerate(qid_list):
258+
params_dict[f"phi{qid}"] = 1.0
259+
params_dict[f"theta{qid}"] = 0.0
260+
for pauli_str in pauli_strings:
261+
pauli_op = pauli_str.get(qubit, default=ops.I)
262+
if pauli_op == ops.X:
263+
params_dict[f"phi{qid}"] = 0.0
264+
params_dict[f"theta{qid}"] = 1 / 2
265+
break
266+
elif pauli_op == ops.Y:
267+
params_dict[f"phi{qid}"] = 1.0
268+
params_dict[f"theta{qid}"] = 1 / 2
269+
break
270+
return params_dict
271+
272+
273+
def _generate_basis_change_circuits(
274+
normalized_circuits_to_pauli: dict[circuits.FrozenCircuit, list[list[ops.PauliString]]],
275+
insert_strategy: circuits.InsertStrategy,
276+
) -> list[circuits.Circuit]:
277+
"""Generates basis change circuits for each group of Pauli strings."""
278+
pauli_measurement_circuits = list[circuits.Circuit]()
279+
280+
for input_circuit, pauli_string_groups in normalized_circuits_to_pauli.items():
281+
qid_list = list(sorted(input_circuit.all_qubits()))
282+
basis_change_circuits = []
283+
input_circuit_unfrozen = input_circuit.unfreeze()
284+
for pauli_strings in pauli_string_groups:
285+
basis_change_circuit = circuits.Circuit(
286+
input_circuit_unfrozen,
287+
_pauli_strings_to_basis_change_ops(pauli_strings, qid_list),
288+
ops.measure(*qid_list, key="result"),
289+
strategy=insert_strategy,
290+
)
291+
basis_change_circuits.append(basis_change_circuit)
292+
pauli_measurement_circuits.extend(basis_change_circuits)
293+
294+
return pauli_measurement_circuits
295+
296+
297+
def _generate_basis_change_circuits_with_sweep(
298+
normalized_circuits_to_pauli: dict[circuits.FrozenCircuit, list[list[ops.PauliString]]],
299+
insert_strategy: circuits.InsertStrategy,
300+
) -> tuple[list[circuits.Circuit], list[study.Sweepable]]:
301+
"""Generates basis change circuits for each group of Pauli strings with sweep."""
302+
parameterized_circuits = list[circuits.Circuit]()
303+
sweep_params = list[study.Sweepable]()
304+
for input_circuit, pauli_string_groups in normalized_circuits_to_pauli.items():
305+
qid_list = list(sorted(input_circuit.all_qubits()))
306+
phi_symbols = sympy.symbols(f"phi:{len(qid_list)}")
307+
theta_symbols = sympy.symbols(f"theta:{len(qid_list)}")
308+
309+
# Create phased gates and measurement operator
310+
phased_gates = [
311+
ops.PhasedXPowGate(phase_exponent=(a - 1) / 2, exponent=b)(qubit)
312+
for a, b, qubit in zip(phi_symbols, theta_symbols, qid_list)
313+
]
314+
measurement_op = ops.M(*qid_list, key="result")
315+
316+
parameterized_circuit = circuits.Circuit(
317+
input_circuit.unfreeze(), phased_gates, measurement_op, strategy=insert_strategy
318+
)
319+
sweep_param = []
320+
for pauli_strings in pauli_string_groups:
321+
sweep_param.append(_pauli_strings_to_basis_change_with_sweep(pauli_strings, qid_list))
322+
sweep_params.append(sweep_param)
323+
parameterized_circuits.append(parameterized_circuit)
324+
325+
return parameterized_circuits, sweep_params
326+
327+
243328
def _build_one_qubit_confusion_matrix(e0: float, e1: float) -> np.ndarray:
244329
"""Builds a 2x2 confusion matrix for a single qubit.
245330
@@ -288,7 +373,7 @@ def _build_many_one_qubits_empty_confusion_matrix(qubits_length: int) -> list[np
288373
def _process_pauli_measurement_results(
289374
qubits: Sequence[ops.Qid],
290375
pauli_string_groups: list[list[ops.PauliString]],
291-
circuit_results: list[ResultDict] | Sequence[study.Result],
376+
circuit_results: Sequence[ResultDict] | Sequence[study.Result],
292377
calibration_results: dict[tuple[ops.Qid, ...], SingleQubitReadoutCalibrationResult],
293378
pauli_repetitions: int,
294379
timestamp: float,
@@ -321,7 +406,7 @@ def _process_pauli_measurement_results(
321406
pauli_measurement_results: list[PauliStringMeasurementResult] = []
322407

323408
for pauli_group_index, circuit_result in enumerate(circuit_results):
324-
measurement_results = circuit_result.measurements["m"]
409+
measurement_results = circuit_result.measurements["result"]
325410
pauli_strs = pauli_string_groups[pauli_group_index]
326411
pauli_readout_qubits = _extract_readout_qubits(pauli_strs)
327412

@@ -403,6 +488,8 @@ def measure_pauli_strings(
403488
readout_repetitions: int,
404489
num_random_bitstrings: int,
405490
rng_or_seed: np.random.Generator | int,
491+
use_sweep: bool = False,
492+
insert_strategy: circuits.InsertStrategy = circuits.InsertStrategy.INLINE,
406493
) -> list[CircuitToPauliStringsMeasurementResult]:
407494
"""Measures expectation values of Pauli strings on given circuits with/without
408495
readout error mitigation.
@@ -411,10 +498,11 @@ def measure_pauli_strings(
411498
For each circuit and its associated list of QWC pauli string group, it:
412499
1. Constructs circuits to measure the Pauli string expectation value by
413500
adding basis change moments and measurement operations.
414-
2. Runs shuffled readout benchmarking on these circuits to calibrate readout errors.
501+
2. If `num_random_bitstrings` is greater than zero, performing readout
502+
benchmarking (shuffled or sweep-based) to calibrate readout errors.
415503
3. Mitigates readout errors using the calibrated confusion matrices.
416504
4. Calculates and returns both error-mitigated and unmitigated expectation values for
417-
each Pauli string.
505+
each Pauli string.
418506
419507
Args:
420508
circuits_to_pauli: A dictionary mapping circuits to either:
@@ -432,6 +520,10 @@ def measure_pauli_strings(
432520
num_random_bitstrings: The number of random bitstrings to use in readout
433521
benchmarking.
434522
rng_or_seed: A random number generator or seed for the readout benchmarking.
523+
use_sweep: If True, uses parameterized circuits and sweeps parameters
524+
for both Pauli measurements and readout benchmarking. Defaults to False.
525+
insert_strategy: The strategy for inserting measurement operations into the circuit.
526+
Defaults to circuits.InsertStrategy.INLINE.
435527
436528
Returns:
437529
A list of CircuitToPauliStringsMeasurementResult objects, where each object contains:
@@ -460,49 +552,68 @@ def measure_pauli_strings(
460552

461553
# Build the basis-change circuits for each Pauli string group
462554
pauli_measurement_circuits: list[circuits.Circuit] = []
463-
for input_circuit, pauli_string_groups in normalized_circuits_to_pauli.items():
464-
qid_list = sorted(input_circuit.all_qubits())
465-
basis_change_circuits = []
466-
input_circuit_unfrozen = input_circuit.unfreeze()
467-
for pauli_strings in pauli_string_groups:
468-
basis_change_circuit = (
469-
input_circuit_unfrozen
470-
+ _pauli_strings_to_basis_change_ops(pauli_strings, qid_list)
471-
+ ops.measure(*qid_list, key="m")
472-
)
473-
basis_change_circuits.append(basis_change_circuit)
474-
pauli_measurement_circuits.extend(basis_change_circuits)
555+
sweep_params: list[study.Sweepable] = []
556+
circuits_results: Sequence[ResultDict] | Sequence[Sequence[study.Result]] = []
557+
calibration_results: dict[tuple[ops.Qid, ...], SingleQubitReadoutCalibrationResult] = {}
558+
559+
benchmarking_params = sc_readout.ReadoutBenchmarkingParams(
560+
circuit_repetitions=pauli_repetitions,
561+
num_random_bitstrings=num_random_bitstrings,
562+
readout_repetitions=readout_repetitions,
563+
)
564+
565+
if use_sweep:
566+
pauli_measurement_circuits, sweep_params = _generate_basis_change_circuits_with_sweep(
567+
normalized_circuits_to_pauli, insert_strategy
568+
)
475569

476-
# Run shuffled benchmarking for readout calibration
477-
circuits_results, calibration_results = (
478-
sc_readout.run_shuffled_circuits_with_readout_benchmarking(
570+
# Run benchmarking using sweep for readout calibration
571+
circuits_results, calibration_results = sc_readout.run_sweep_with_readout_benchmarking(
479572
sampler=sampler,
480573
input_circuits=pauli_measurement_circuits,
481-
parameters=sc_readout.ReadoutBenchmarkingParams(
482-
circuit_repetitions=pauli_repetitions,
483-
num_random_bitstrings=num_random_bitstrings,
484-
readout_repetitions=readout_repetitions,
485-
),
574+
sweep_params=sweep_params,
575+
parameters=benchmarking_params,
486576
rng_or_seed=rng_or_seed,
487577
qubits=[list(qubits) for qubits in qubits_list],
488578
)
489-
)
579+
580+
else:
581+
pauli_measurement_circuits = _generate_basis_change_circuits(
582+
normalized_circuits_to_pauli, insert_strategy
583+
)
584+
585+
# Run shuffled benchmarking for readout calibration
586+
circuits_results, calibration_results = (
587+
sc_readout.run_shuffled_circuits_with_readout_benchmarking(
588+
sampler=sampler,
589+
input_circuits=pauli_measurement_circuits,
590+
parameters=benchmarking_params,
591+
rng_or_seed=rng_or_seed,
592+
qubits=[list(qubits) for qubits in qubits_list],
593+
)
594+
)
490595

491596
# Process the results to calculate expectation values
492597
results: list[CircuitToPauliStringsMeasurementResult] = []
493598
circuit_result_index = 0
494-
for input_circuit, pauli_string_groups in normalized_circuits_to_pauli.items():
495-
599+
for i, (input_circuit, pauli_string_groups) in enumerate(normalized_circuits_to_pauli.items()):
496600
qubits_in_circuit = tuple(sorted(input_circuit.all_qubits()))
497601

498602
disable_readout_mitigation = False if num_random_bitstrings != 0 else True
499603

604+
circuits_results_for_group: Sequence[ResultDict] | Sequence[study.Result] = []
605+
if use_sweep:
606+
circuits_results_for_group = cast(Sequence[Sequence[study.Result]], circuits_results)[i]
607+
else:
608+
circuits_results_for_group = cast(Sequence[ResultDict], circuits_results)[
609+
circuit_result_index : circuit_result_index + len(pauli_string_groups)
610+
]
611+
circuit_result_index += len(pauli_string_groups)
612+
500613
pauli_measurement_results = _process_pauli_measurement_results(
501614
list(qubits_in_circuit),
502615
pauli_string_groups,
503-
circuits_results[
504-
circuit_result_index : circuit_result_index + len(pauli_string_groups)
505-
],
616+
circuits_results_for_group,
506617
calibration_results,
507618
pauli_repetitions,
508619
time.time(),
@@ -514,5 +625,4 @@ def measure_pauli_strings(
514625
)
515626
)
516627

517-
circuit_result_index += len(pauli_string_groups)
518628
return results

0 commit comments

Comments
 (0)