Skip to content

Commit c2866fa

Browse files
ddddddanniBichengYing
authored andcommitted
Combining readout circuits for QWC pauli strings in the measure_pauli_strings method (quantumlib#7416)
[PR quantumlib#7236](quantumlib#7236) adds the feature to measure groups of qubit-wise-commuting Pauli operators simultaneously. However, the current implementation generates a separate readout circuit for each individual Pauli operator within a group. This leads to non-simultaneous measurements, which is corrected in this PR.
1 parent 7cf4cff commit c2866fa

File tree

3 files changed

+86
-14
lines changed

3 files changed

+86
-14
lines changed

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

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828
from cirq.experiments.readout_confusion_matrix import TensoredConfusionMatrices
2929

3030
if TYPE_CHECKING:
31-
from cirq.experiments import SingleQubitReadoutCalibrationResult
31+
from cirq.experiments.single_qubit_readout_calibration import (
32+
SingleQubitReadoutCalibrationResult,
33+
)
3234
from cirq.study import ResultDict
3335

3436

@@ -217,6 +219,11 @@ def _normalize_input_paulis(
217219
return cast(dict[circuits.FrozenCircuit, list[list[ops.PauliString]]], circuits_to_pauli)
218220

219221

222+
def _extract_readout_qubits(pauli_strings: list[ops.PauliString]) -> list[ops.Qid]:
223+
"""Extracts unique qubits from a list of QWC Pauli strings."""
224+
return sorted(set(q for ps in pauli_strings for q in ps.qubits))
225+
226+
220227
def _pauli_strings_to_basis_change_ops(
221228
pauli_strings: list[ops.PauliString], qid_list: list[ops.Qid]
222229
):
@@ -315,16 +322,38 @@ def _process_pauli_measurement_results(
315322
for pauli_group_index, circuit_result in enumerate(circuit_results):
316323
measurement_results = circuit_result.measurements["m"]
317324
pauli_strs = pauli_string_groups[pauli_group_index]
325+
pauli_readout_qubits = _extract_readout_qubits(pauli_strs)
326+
327+
calibration_result = (
328+
calibration_results[tuple(pauli_readout_qubits)]
329+
if disable_readout_mitigation is False
330+
else None
331+
)
318332

319333
for pauli_str in pauli_strs:
320334
qubits_sorted = sorted(pauli_str.qubits)
321335
qubit_indices = [qubits.index(q) for q in qubits_sorted]
322336

323-
confusion_matrices = (
324-
_build_many_one_qubits_confusion_matrix(calibration_results[tuple(qubits_sorted)])
325-
if disable_readout_mitigation is False
326-
else _build_many_one_qubits_empty_confusion_matrix(len(qubits_sorted))
327-
)
337+
if disable_readout_mitigation:
338+
pauli_str_calibration_result = None
339+
confusion_matrices = _build_many_one_qubits_empty_confusion_matrix(
340+
len(qubits_sorted)
341+
)
342+
else:
343+
if calibration_result is None:
344+
# This case should be logically impossible if mitigation is on,
345+
# so we raise an error.
346+
raise ValueError(
347+
f"Readout mitigation is enabled, but no calibration result was "
348+
f"found for qubits {pauli_readout_qubits}."
349+
)
350+
pauli_str_calibration_result = calibration_result.readout_result_for_qubits(
351+
qubits_sorted
352+
)
353+
confusion_matrices = _build_many_one_qubits_confusion_matrix(
354+
pauli_str_calibration_result
355+
)
356+
328357
tensored_cm = TensoredConfusionMatrices(
329358
confusion_matrices,
330359
[[q] for q in qubits_sorted],
@@ -356,11 +385,7 @@ def _process_pauli_measurement_results(
356385
mitigated_stddev=d_m_with_coefficient,
357386
unmitigated_expectation=unmitigated_value_with_coefficient,
358387
unmitigated_stddev=d_unmit_with_coefficient,
359-
calibration_result=(
360-
calibration_results[tuple(qubits_sorted)]
361-
if disable_readout_mitigation is False
362-
else None
363-
),
388+
calibration_result=pauli_str_calibration_result,
364389
)
365390
)
366391

@@ -428,8 +453,7 @@ def measure_pauli_strings(
428453
unique_qubit_tuples = set()
429454
for pauli_string_groups in normalized_circuits_to_pauli.values():
430455
for pauli_strings in pauli_string_groups:
431-
for pauli_string in pauli_strings:
432-
unique_qubit_tuples.add(tuple(sorted(pauli_string.qubits)))
456+
unique_qubit_tuples.add(tuple(_extract_readout_qubits(pauli_strings)))
433457
# qubits_list is a list of qubit tuples
434458
qubits_list = sorted(unique_qubit_tuples)
435459

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

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323

2424
import cirq
2525
from cirq.contrib.paulistring import measure_pauli_strings
26-
from cirq.experiments import SingleQubitReadoutCalibrationResult
26+
from cirq.contrib.paulistring.pauli_string_measurement_with_readout_mitigation import (
27+
_process_pauli_measurement_results,
28+
)
29+
from cirq.experiments.single_qubit_readout_calibration import SingleQubitReadoutCalibrationResult
2730
from cirq.experiments.single_qubit_readout_calibration_test import NoisySingleQubitReadoutSampler
2831

2932

@@ -867,3 +870,37 @@ def test_group_paulis_type_mismatch() -> None:
867870
measure_pauli_strings(
868871
circuits_to_pauli, cirq.Simulator(), 1000, 1000, 1000, np.random.default_rng()
869872
)
873+
874+
875+
def test_process_pauli_measurement_results_raises_error_on_missing_calibration() -> None:
876+
"""Test that the function raises an error if the calibration result is missing."""
877+
qubits: list[cirq.Qid] = [q for q in cirq.LineQubit.range(5)]
878+
879+
measurement_op = cirq.measure(*qubits, key='m')
880+
test_circuits = list[cirq.Circuit]()
881+
for _ in range(3):
882+
circuit_list = []
883+
884+
circuit = _create_ghz(5, qubits) + measurement_op
885+
circuit_list.append(circuit)
886+
test_circuits.extend(circuit_list)
887+
888+
pauli_strings = [_generate_random_pauli_string(qubits, True) for _ in range(3)]
889+
sampler = cirq.Simulator()
890+
891+
circuit_results = sampler.run_batch(test_circuits, repetitions=1000)
892+
893+
empty_calibration_result_dict = {tuple(qubits): None}
894+
895+
with pytest.raises(
896+
ValueError,
897+
match="Readout mitigation is enabled, but no calibration result was found for qubits",
898+
):
899+
_process_pauli_measurement_results(
900+
qubits,
901+
[pauli_strings],
902+
circuit_results[0], # type: ignore[arg-type]
903+
empty_calibration_result_dict, # type: ignore[arg-type]
904+
1000,
905+
1.0,
906+
)

cirq-core/cirq/experiments/single_qubit_readout_calibration.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,17 @@ def plot_integrated_histogram(
179179
ax.set_ylabel('Percentile')
180180
return ax
181181

182+
def readout_result_for_qubits(
183+
self, readout_qubits: list[ops.Qid]
184+
) -> SingleQubitReadoutCalibrationResult:
185+
"""Builds a calibration result for the specific readout qubits."""
186+
return SingleQubitReadoutCalibrationResult(
187+
zero_state_errors={qubit: self.zero_state_errors[qubit] for qubit in readout_qubits},
188+
one_state_errors={qubit: self.one_state_errors[qubit] for qubit in readout_qubits},
189+
timestamp=self.timestamp,
190+
repetitions=self.repetitions,
191+
)
192+
182193
@classmethod
183194
def _from_json_dict_(
184195
cls, zero_state_errors, one_state_errors, repetitions, timestamp, **kwargs

0 commit comments

Comments
 (0)