-
Notifications
You must be signed in to change notification settings - Fork 1.1k
(2/2) Add parameter sweep support for Pauli string measurements with readout mitigation #7569
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
c6f7cbd
9a28839
7addb7e
02ddd0f
1ba8d2c
0540d07
cffc694
338fde3
d5ba4fb
3ece94a
121286c
1561e76
832ba7a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,7 @@ | |
|
||
import attrs | ||
import numpy as np | ||
import sympy | ||
|
||
import cirq.contrib.shuffle_circuits.shuffle_circuits_with_readout_benchmarking as sc_readout | ||
from cirq import circuits, ops, study, work | ||
|
@@ -188,15 +189,15 @@ def _validate_input( | |
|
||
# Check pauli_repetitions is bigger than 0 | ||
if pauli_repetitions <= 0: | ||
raise ValueError("Must provide non-zero pauli_repetitions.") | ||
raise ValueError("Must provide positive pauli_repetitions.") | ||
|
||
# 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: | ||
raise ValueError("Must provide non-zero readout_repetitions for readout calibration.") | ||
raise ValueError("Must provide positive readout_repetitions for readout calibration.") | ||
|
||
|
||
def _normalize_input_paulis( | ||
|
@@ -240,6 +241,90 @@ def _pauli_strings_to_basis_change_ops( | |
return operations | ||
|
||
|
||
def _pauli_strings_to_basis_change_with_sweep( | ||
pauli_strings: list[ops.PauliString], qid_list: list[ops.Qid] | ||
) -> dict[str, float]: | ||
"""Decide single-qubit rotation sweep parameters for basis change. | ||
|
||
Args: | ||
pauli_strings: A list of QWC Pauli strings. | ||
qid_list: A list of qubits to apply the basis change on. | ||
Returns: | ||
A dictionary mapping parameter names to their values for basis change. | ||
""" | ||
params_dict = {} | ||
|
||
for qid, qubit in enumerate(qid_list): | ||
params_dict[f"phi{qid}"] = 1.0 | ||
params_dict[f"theta{qid}"] = 0.0 | ||
for pauli_str in pauli_strings: | ||
pauli_op = pauli_str.get(qubit, default=ops.I) | ||
if pauli_op == ops.X: | ||
params_dict[f"phi{qid}"] = 0.0 | ||
params_dict[f"theta{qid}"] = 1 / 2 | ||
break | ||
elif pauli_op == ops.Y: | ||
params_dict[f"phi{qid}"] = 1.0 | ||
params_dict[f"theta{qid}"] = 1 / 2 | ||
break | ||
return params_dict | ||
|
||
|
||
def _generate_basis_change_circuits( | ||
normalized_circuits_to_pauli: dict[circuits.FrozenCircuit, list[list[ops.PauliString]]], | ||
insert_strategy: circuits.InsertStrategy, | ||
) -> list[circuits.Circuit]: | ||
"""Generates basis change circuits for each group of Pauli strings.""" | ||
pauli_measurement_circuits = list[circuits.Circuit]() | ||
|
||
for input_circuit, pauli_string_groups in normalized_circuits_to_pauli.items(): | ||
qid_list = list(sorted(input_circuit.all_qubits())) | ||
basis_change_circuits = [] | ||
input_circuit_unfrozen = input_circuit.unfreeze() | ||
for pauli_strings in pauli_string_groups: | ||
basis_change_circuit = circuits.Circuit( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that we don't really need an insert_strategy, we can just do circuits.Circuit.from_moments(
*input_circuit,
_pauli_strings_to_basis_change_ops(pauli_strings, qid_list),
ops.measure(*qid_list, key="m"),
) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We want to give users choices of whether to do the basis change all in the same penultimate moment or as early as possible. This was discussed https://screenshot.googleplex.com/4LPKcw4eYCSvP85. |
||
input_circuit_unfrozen, | ||
_pauli_strings_to_basis_change_ops(pauli_strings, qid_list), | ||
ops.measure(*qid_list, key="m"), | ||
|
||
strategy=insert_strategy, | ||
) | ||
basis_change_circuits.append(basis_change_circuit) | ||
pauli_measurement_circuits.extend(basis_change_circuits) | ||
|
||
return pauli_measurement_circuits | ||
|
||
|
||
def _generate_basis_change_circuits_with_sweep( | ||
normalized_circuits_to_pauli: dict[circuits.FrozenCircuit, list[list[ops.PauliString]]], | ||
insert_strategy: circuits.InsertStrategy, | ||
) -> tuple[list[circuits.Circuit], list[study.Sweepable]]: | ||
"""Generates basis change circuits for each group of Pauli strings with sweep.""" | ||
parameterized_circuits = list[circuits.Circuit]() | ||
sweep_params = list[study.Sweepable]() | ||
for input_circuit, pauli_string_groups in normalized_circuits_to_pauli.items(): | ||
qid_list = list(sorted(input_circuit.all_qubits())) | ||
phi_symbols = sympy.symbols(f"phi:{len(qid_list)}") | ||
theta_symbols = sympy.symbols(f"theta:{len(qid_list)}") | ||
|
||
# Create phased gates and measurement operator | ||
phased_gates = [ | ||
ops.PhasedXPowGate(phase_exponent=(a - 1) / 2, exponent=b)(qubit) | ||
for a, b, qubit in zip(phi_symbols, theta_symbols, qid_list) | ||
] | ||
measurement_op = ops.M(*qid_list, key="m") | ||
|
||
parameterized_circuit = circuits.Circuit( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same above, we may want to allow users to control the insert strategy of basis changes. |
||
input_circuit.unfreeze(), phased_gates, measurement_op, strategy=insert_strategy | ||
) | ||
sweep_param = [] | ||
for pauli_strings in pauli_string_groups: | ||
sweep_param.append(_pauli_strings_to_basis_change_with_sweep(pauli_strings, qid_list)) | ||
sweep_params.append(sweep_param) | ||
parameterized_circuits.append(parameterized_circuit) | ||
|
||
return parameterized_circuits, sweep_params | ||
|
||
|
||
def _build_one_qubit_confusion_matrix(e0: float, e1: float) -> np.ndarray: | ||
"""Builds a 2x2 confusion matrix for a single qubit. | ||
|
||
|
@@ -288,7 +373,7 @@ def _build_many_one_qubits_empty_confusion_matrix(qubits_length: int) -> list[np | |
def _process_pauli_measurement_results( | ||
qubits: Sequence[ops.Qid], | ||
pauli_string_groups: list[list[ops.PauliString]], | ||
circuit_results: list[ResultDict] | Sequence[study.Result], | ||
circuit_results: Sequence[ResultDict] | Sequence[study.Result], | ||
calibration_results: dict[tuple[ops.Qid, ...], SingleQubitReadoutCalibrationResult], | ||
pauli_repetitions: int, | ||
timestamp: float, | ||
|
@@ -403,6 +488,8 @@ def measure_pauli_strings( | |
readout_repetitions: int, | ||
num_random_bitstrings: int, | ||
rng_or_seed: np.random.Generator | int, | ||
use_sweep: bool = False, | ||
insert_strategy: circuits.InsertStrategy = circuits.InsertStrategy.INLINE, | ||
) -> list[CircuitToPauliStringsMeasurementResult]: | ||
"""Measures expectation values of Pauli strings on given circuits with/without | ||
readout error mitigation. | ||
|
@@ -411,10 +498,11 @@ def measure_pauli_strings( | |
For each circuit and its associated list of QWC pauli string group, 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. | ||
2. If `num_random_bitstrings` is greater than zero, performing readout | ||
benchmarking (shuffled or sweep-based) to calibrate readout errors. | ||
3. Mitigates readout errors using the calibrated confusion matrices. | ||
4. Calculates and returns both error-mitigated and unmitigated expectation values for | ||
each Pauli string. | ||
each Pauli string. | ||
|
||
Args: | ||
circuits_to_pauli: A dictionary mapping circuits to either: | ||
|
@@ -432,6 +520,10 @@ def measure_pauli_strings( | |
num_random_bitstrings: The number of random bitstrings to use in readout | ||
benchmarking. | ||
rng_or_seed: A random number generator or seed for the readout benchmarking. | ||
use_sweep: If True, uses parameterized circuits and sweeps parameters | ||
for both Pauli measurements and readout benchmarking. Defaults to False. | ||
insert_strategy: The strategy for inserting measurement operations into the circuit. | ||
Defaults to circuits.InsertStrategy.INLINE. | ||
|
||
Returns: | ||
A list of CircuitToPauliStringsMeasurementResult objects, where each object contains: | ||
|
@@ -460,49 +552,68 @@ def measure_pauli_strings( | |
|
||
# Build the basis-change circuits for each Pauli string group | ||
pauli_measurement_circuits: list[circuits.Circuit] = [] | ||
for input_circuit, pauli_string_groups in normalized_circuits_to_pauli.items(): | ||
qid_list = sorted(input_circuit.all_qubits()) | ||
basis_change_circuits = [] | ||
input_circuit_unfrozen = input_circuit.unfreeze() | ||
for pauli_strings in pauli_string_groups: | ||
basis_change_circuit = ( | ||
input_circuit_unfrozen | ||
+ _pauli_strings_to_basis_change_ops(pauli_strings, qid_list) | ||
+ ops.measure(*qid_list, key="m") | ||
) | ||
basis_change_circuits.append(basis_change_circuit) | ||
pauli_measurement_circuits.extend(basis_change_circuits) | ||
sweep_params: list[study.Sweepable] = [] | ||
circuits_results: Sequence[ResultDict] | Sequence[Sequence[study.Result]] = [] | ||
calibration_results: dict[tuple[ops.Qid, ...], SingleQubitReadoutCalibrationResult] = {} | ||
|
||
benchmarking_params = sc_readout.ReadoutBenchmarkingParams( | ||
circuit_repetitions=pauli_repetitions, | ||
num_random_bitstrings=num_random_bitstrings, | ||
readout_repetitions=readout_repetitions, | ||
) | ||
|
||
if use_sweep: | ||
pauli_measurement_circuits, sweep_params = _generate_basis_change_circuits_with_sweep( | ||
normalized_circuits_to_pauli, insert_strategy | ||
) | ||
|
||
# Run shuffled benchmarking for readout calibration | ||
circuits_results, calibration_results = ( | ||
sc_readout.run_shuffled_circuits_with_readout_benchmarking( | ||
# Run benchmarking using sweep for readout calibration | ||
circuits_results, calibration_results = sc_readout.run_sweep_with_readout_benchmarking( | ||
sampler=sampler, | ||
input_circuits=pauli_measurement_circuits, | ||
parameters=sc_readout.ReadoutBenchmarkingParams( | ||
circuit_repetitions=pauli_repetitions, | ||
num_random_bitstrings=num_random_bitstrings, | ||
readout_repetitions=readout_repetitions, | ||
), | ||
sweep_params=sweep_params, | ||
parameters=benchmarking_params, | ||
rng_or_seed=rng_or_seed, | ||
qubits=[list(qubits) for qubits in qubits_list], | ||
) | ||
) | ||
|
||
else: | ||
pauli_measurement_circuits = _generate_basis_change_circuits( | ||
normalized_circuits_to_pauli, insert_strategy | ||
) | ||
|
||
# Run shuffled benchmarking for readout calibration | ||
circuits_results, calibration_results = ( | ||
sc_readout.run_shuffled_circuits_with_readout_benchmarking( | ||
sampler=sampler, | ||
input_circuits=pauli_measurement_circuits, | ||
parameters=benchmarking_params, | ||
rng_or_seed=rng_or_seed, | ||
qubits=[list(qubits) for qubits in qubits_list], | ||
) | ||
) | ||
|
||
# Process the results to calculate expectation values | ||
results: list[CircuitToPauliStringsMeasurementResult] = [] | ||
circuit_result_index = 0 | ||
for input_circuit, pauli_string_groups in normalized_circuits_to_pauli.items(): | ||
|
||
for i, (input_circuit, pauli_string_groups) in enumerate(normalized_circuits_to_pauli.items()): | ||
qubits_in_circuit = tuple(sorted(input_circuit.all_qubits())) | ||
|
||
disable_readout_mitigation = False if num_random_bitstrings != 0 else True | ||
|
||
circuits_results_for_group: Sequence[ResultDict] | Sequence[study.Result] = [] | ||
if use_sweep: | ||
circuits_results_for_group = circuits_results[i] | ||
else: | ||
circuits_results_for_group = circuits_results[ | ||
circuit_result_index : circuit_result_index + len(pauli_string_groups) | ||
] | ||
circuit_result_index += len(pauli_string_groups) | ||
|
||
pauli_measurement_results = _process_pauli_measurement_results( | ||
list(qubits_in_circuit), | ||
pauli_string_groups, | ||
circuits_results[ | ||
circuit_result_index : circuit_result_index + len(pauli_string_groups) | ||
], | ||
circuits_results_for_group, | ||
calibration_results, | ||
pauli_repetitions, | ||
time.time(), | ||
|
@@ -514,5 +625,4 @@ def measure_pauli_strings( | |
) | ||
) | ||
|
||
circuit_result_index += len(pauli_string_groups) | ||
return results |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm having a déjà vu moment, didn't you write a similar method before?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a giant PR https://github.com/quantumlib/Cirq/pull/7358/files which you reviewed long time ago. I break that PR into smaller ones: #7435 and this pr, per you requested.
For question you asked in #7358: https://screenshot.googleplex.com/6HHTPQdj3R6Vwfy and https://screenshot.googleplex.com/9kw8sR9Q8g4BnMF - each qubit can belong to many input pauli strings. Since input pauli strings are qubit-wise commuting, it's safe to do so. For example, input pauli strings could be {X(q_0)I(q_1), I(q_0)X(q_1)}, and this function returns params_dict{q_0:x basis change, q_1: x basis change}.