Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
18f99be
Modify the shuffle_circuit measurement and allow it to handle a list …
ddddddanni Feb 13, 2025
d3126c8
Add a new tool for measuring expectation values of Pauli strings with…
ddddddanni Feb 13, 2025
b66caa6
Merge branch 'main' into pauli-string-readout
ddddddanni Feb 14, 2025
b6e645e
Allow the measure_pauli_strings to also return the SingleQubitReadout…
ddddddanni Feb 14, 2025
c026cc1
1. Allow shuffle_circuits_with_readout_benchmarking to take 0 for num…
ddddddanni Feb 18, 2025
3765833
Merge branch 'main' into pauli-string-readout
ddddddanni Feb 19, 2025
17cf34d
Fix a issue that caused calibration_results lookup failure
ddddddanni Feb 24, 2025
d342fef
Fix: Pauli I was incorrectly treated as Z in expectation calculation
ddddddanni Mar 2, 2025
55adfc3
1. Added some codes to multiply the result of measuring the PauliStri…
ddddddanni Mar 10, 2025
27b9844
Fix a broken test
ddddddanni Mar 10, 2025
bcbe40e
pauli_string.qubits returns the self.keys which are already unique. T…
ddddddanni Mar 10, 2025
7a60050
Merge branch 'main' into pauli-string-readout
eliottrosenberg Mar 12, 2025
e8c448c
Fix type and coverage check. Besides, adds a input validation check t…
ddddddanni Mar 12, 2025
fd16e0e
Address comments by @NoureldinYosri
ddddddanni Mar 17, 2025
a97f613
Fix the coverage check
ddddddanni Mar 18, 2025
ff27552
Fix lint line too long
ddddddanni Mar 18, 2025
9b8431e
Merge branch 'main' into pauli-string-readout
ddddddanni Mar 19, 2025
2eda743
Merge branch 'main' into pauli-string-readout
ddddddanni Mar 19, 2025
1af26c2
Merge branch 'main' into pauli-string-readout
NoureldinYosri Mar 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cirq-core/cirq/contrib/paulistring/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,7 @@
)

from cirq.contrib.paulistring.optimize import optimized_circuit as optimized_circuit

from cirq.contrib.paulistring.pauli_string_measurement_with_readout_mitigation import (
measure_pauli_strings as measure_pauli_strings,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
# Copyright 2025 The Cirq Developers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
""" Tools for measuring expectation values of Pauli strings with readout error mitigation. """
import time
import dataclasses
from typing import List, Union, Dict, Optional

import numpy as np

from cirq import ops, circuits, work
from cirq.contrib.shuffle_circuits import run_shuffled_with_readout_benchmarking
from cirq.experiments import SingleQubitReadoutCalibrationResult
from cirq.experiments.readout_confusion_matrix import TensoredConfusionMatrices
from cirq.study import ResultDict


@dataclasses.dataclass
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer to use attrs over dataclasses

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

class PauliStringMeasurementResult:
"""Result of measuring a Pauli string.

Attributes:
pauli_string: The Pauli string that is measured.
mitigated_expectation: The error-mitigated expectation value of the Pauli string.
mitigated_stddev: The standard deviation of the error-mitigated expectation value.
unmitigated_expectation: The unmitigated expectation value of the Pauli string.
unmitigated_stddev: The standard deviation of the unmitigated expectation value.
"""

pauli_string: ops.PauliString
mitigated_expectation: float
mitigated_stddev: float
unmitigated_expectation: float
unmitigated_stddev: float


@dataclasses.dataclass
class CircuitToPauliStringsMeasurementResult:
"""Result of measuring Pauli strings on a circuit.

Attributes:
circuit: The circuit that is measured.
results: A list of PauliStringMeasurementResult objects.
calibration_result: The calibration result for single-qubit readout errors.
"""

circuit: circuits.FrozenCircuit
results: List[PauliStringMeasurementResult]
calibration_result: Optional[SingleQubitReadoutCalibrationResult]


def _pauli_string_to_basis_change_ops(
pauli_string: ops.PauliString, qid_list: list[ops.Qid]
) -> List[ops.Operation]:
"""Creates operations to change to the eigenbasis of the given Pauli string.

This function constructs a list of ops.Operation that performs basis changes
necessary to measure the given pauli_string in the computational basis.

Args:
pauli_string: The Pauli string to diagonalize.
qid_list: An ordered list of the qubits in the circuit.

Returns:
A list of Operations that, when applied before measurement in the
computational basis, effectively measures in the eigenbasis of
pauli_strings.
"""
operations = []
for qubit in pauli_string:
pauli_op = pauli_string[qubit]
qubit_index = qid_list.index(qubit)
if pauli_op == ops.X:
# Rotate to X basis: Ry(-pi/2)
operations.append(ops.ry(-np.pi / 2)(qid_list[qubit_index]))
elif pauli_op == ops.Y:
# Rotate to Y basis: Rx(pi/2)
operations.append(ops.rx(np.pi / 2)(qid_list[qubit_index]))
# No operation needed for Pauli Z or I (identity)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there shouldn't be identity in this case

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

return operations


def _build_one_qubit_confusion_matrix(e0: float, e1: float) -> np.ndarray:
"""Builds a 2x2 confusion matrix for a single qubit.

Args:
e0: the 0->1 readout error rate.
e1: the 1->0 readout error rate.

Returns:
A 2x2 NumPy array representing the confusion matrix.
"""
return np.array([[1 - e0, e1], [e0, 1 - e1]])


def _build_many_one_qubits_confusion_matrix(
qubits_to_error: SingleQubitReadoutCalibrationResult,
) -> list[np.ndarray]:
"""Builds a list of confusion matrices from calibration results.

This function iterates through the calibration results for each qubit and
constructs a list of single-qubit confusion matrices.

Args:
qubits_to_error: An object containing calibration results for
single-qubit readout errors, including zero-state and one-state errors
for each qubit.

Returns:
A list of NumPy arrays, where each array is a 2x2 confusion matrix
for a qubit. The order of matrices corresponds to the order of qubits
in the calibration results (alphabetical order by qubit name).
"""
cms: list[np.ndarray] = []
if not qubits_to_error:
return cms

for qubit in sorted(qubits_to_error.zero_state_errors.keys()):
e0 = qubits_to_error.zero_state_errors[qubit]
e1 = qubits_to_error.one_state_errors[qubit]
cms.append(_build_one_qubit_confusion_matrix(e0, e1))
return cms


def _build_many_one_qubits_empty_confusion_matrix(qubits_length: int):
"""Builds a list of empty confusion matrices"""
cms: list[np.ndarray] = []
for _ in range(qubits_length):
cms.append(_build_one_qubit_confusion_matrix(0, 0))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is is not an empty matrix ... if the intention is to create a placeholder list of np.array then you can do

return [np.empty(0)]*qubits_length

if you need them to be 2x2 array, then

return [np.empty((2,2))]*qubits_length

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! I changed the method to directly return the [_build_one_qubit_confusion_matrix(0, 0) for _ in range(qubits_length)]

return cms


def _process_pauli_measurement_results(
qubits: List[ops.Qid],
pauli_strings: List[ops.PauliString],
circuit_results: List[ResultDict],
confusion_matrices: List[np.ndarray],
pauli_repetitions: int,
timestamp: float,
) -> List[PauliStringMeasurementResult]:
"""Calculates both error-mitigated expectation values and unmitigated expectation values
from measurement results.

This function takes the results from shuffled readout benchmarking and:
1. Constructs a tensored confusion matrix for error mitigation.
2. Mitigates readout errors for each Pauli string measurement.
3. Calculates and returns both error-mitigated and unmitigated expectation values.

Args:
qubits: Qubits to build confusion matrices for. In a sorted order.
pauli_strings: The list of PauliStrings that are measured.
circuit_results: A list of ResultDict obtained
from running the Pauli measurement circuits.
confusion_matrices: A list of confusion matrices from calibration results.
pauli_repetitions: The number of repetitions used for Pauli string measurements.
timestamp: The timestamp of the calibration results.

Returns:
A list of PauliStringMeasurementResult objects, where each object contains:
- The Pauli string that was measured.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is redundant since this description should be in the docstring of PauliStringMeasurementResult

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! Changed.

- The mitigated expectation value of the Pauli string.
- The standard deviation of the mitigated expectation value.
- The unmitigated expectation value of the Pauli string.
- The standard deviation of the unmitigated expectation value.
"""
tensored_cm = TensoredConfusionMatrices(
confusion_matrices,
[[q] for q in qubits],
repetitions=pauli_repetitions,
timestamp=timestamp,
)

pauli_measurement_results: List[PauliStringMeasurementResult] = []

for pauli_index, circuit_result in enumerate(circuit_results):
measurement_results = circuit_result.measurements["m"]

mitigated_values, d_m = tensored_cm.readout_mitigation_pauli_uncorrelated(
qubits, measurement_results
)

p1 = np.mean(np.sum(measurement_results, axis=1) % 2)
unmitigated_values = 1 - 2 * np.mean(p1)
d_unmit = 2 * np.sqrt(p1 * (1 - p1) / pauli_repetitions)

pauli_measurement_results.append(
PauliStringMeasurementResult(
pauli_string=pauli_strings[pauli_index],
mitigated_expectation=mitigated_values,
mitigated_stddev=d_m,
unmitigated_expectation=unmitigated_values,
unmitigated_stddev=d_unmit,
)
)

return pauli_measurement_results


def measure_pauli_strings(
circuits_to_pauli: Dict[circuits.FrozenCircuit, list[ops.PauliString]],
sampler: work.Sampler,
rng_or_seed: Union[np.random.Generator, int],
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

pauli_repetitions: int,
readout_repetitions: int,
num_random_bitstrings: int,
) -> List[CircuitToPauliStringsMeasurementResult]:
"""Measures expectation values of Pauli strings on given circuits with/without
readout error mitigation.

This function takes a list of circuits and corresponding List[Pauli string] to measure.
For each circuit-List[Pauli string] pair, 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.
3. Mitigates readout errors using the calibrated confusion matrices.
4. Calculates and returns both error-mitigated and unmitigatedexpectation values for
each Pauli string.

Args:
circuits_to_pauli: A dictionary mapping circuits to a list of Pauli strings
to measure.
sampler: The sampler to use.
rng_or_seed: A random number generator or seed for the shuffled benchmarking.
pauli_repetitions: The number of repetitions for each circuit when measuring
Pauli strings.
readout_repetitions: The number of repetitions for readout calibration
in the shuffled benchmarking.
num_random_bitstrings: The number of random bitstrings to use in shuffled
benchmarking.

Returns:
A list of CircuitToPauliStringsMeasurementResult objects, where each object contains:
- The circuit that was measured.
- A list of PauliStringMeasurementResult objects.
- The calibration result for single-qubit readout errors.
"""

# Extract unique qubit tuples from all circuits
unique_qubit_tuples = set()
for circuit in circuits_to_pauli.keys():
unique_qubit_tuples.add(tuple(sorted(set(circuit.all_qubits()))))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

circuit.all_qubits() returns a frozenset, so you can dop the set here and elsewhere

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure!

# qubits_list is a list of qubit tuples
qubits_list = sorted(unique_qubit_tuples)

# Build the basis-change circuits for each Pauli string
pauli_measurement_circuits = list[ops.PauliString]()
for input_circuit, pauli_strings in circuits_to_pauli.items():
qid_list = list(set(input_circuit.all_qubits()))
basis_change_circuits = []
input_circuit_unfrozen = input_circuit.unfreeze()
for pauli_string in pauli_strings:
basis_change_circuit = (
input_circuit_unfrozen
+ _pauli_string_to_basis_change_ops(pauli_string, qid_list)
+ ops.measure(*qid_list, key="m")
)
basis_change_circuits.append(basis_change_circuit)
pauli_measurement_circuits.extend(basis_change_circuits)

# Run shuffled benchmarking for readout calibration
circuits_results, calibration_results = run_shuffled_with_readout_benchmarking(
input_circuits=pauli_measurement_circuits,
sampler=sampler,
circuit_repetitions=pauli_repetitions,
rng_or_seed=rng_or_seed,
qubits=[list(qubits) for qubits in qubits_list],
num_random_bitstrings=num_random_bitstrings,
readout_repetitions=readout_repetitions,
)

# Process the results to calculate expectation values
results: List[CircuitToPauliStringsMeasurementResult] = []
circuit_result_index = 0
for input_circuit, pauli_strings in circuits_to_pauli.items():
qubits_in_circuit = tuple(sorted(set(input_circuit.all_qubits())))

confusion_matrices = (
_build_many_one_qubits_confusion_matrix(calibration_results[qubits_in_circuit])
if num_random_bitstrings != 0
else _build_many_one_qubits_empty_confusion_matrix(len(qubits_in_circuit))
)

pauli_measurement_results = _process_pauli_measurement_results(
list(qubits_in_circuit),
pauli_strings,
circuits_results[circuit_result_index : circuit_result_index + len(pauli_strings)],
confusion_matrices,
pauli_repetitions,
time.time(),
)
results.append(
CircuitToPauliStringsMeasurementResult(
circuit=input_circuit,
results=pauli_measurement_results,
calibration_result=(
calibration_results[qubits_in_circuit] if num_random_bitstrings != 0 else None
),
)
)

circuit_result_index += len(pauli_strings)
return results
Loading