Add a tool for measuring expectation values of Pauli strings with readout error mitigation#7067
Conversation
…of qubits as input
… readout error mitigation
…CalibrationResult. Besides, fixed some lints
|
Hey @eliottrosenberg and @NoureldinYosri if you could take a look at this. Thanks! |
eliottrosenberg
left a comment
There was a problem hiding this comment.
Very nice work Danni!! This looks really great!
A few initial comments:
Regarding line 55 in shuffle_circuits_with_readout_benchmarking.py, it should be possible to specify 0 for num_random_bitstrings in order to do the measurement without readout mitigation (or have a different way of running without readout mitigation).
I think you went with lists of tuples for circuits_to_pauli because you can't put a cirq.Circuit into a dictionary, but you can put a cirq.FrozenCircuit into a dictionary, so maybe that would be more convenient (not sure). Also, the object that measure_pauli_strings outputs is pretty complicated, with lots of nested tuples, so maybe we want to create some data class for the output?
…_random_bitstrings, and add a test to cover the situation. The design is the run_shuffled_with_readout_benchmarking method will return a empty SingleQubitReadoutCalibrationResult if no calibration is actually done. 2. Allow measure_pauli_strings to take num_random_bitstrings = 0. In this case, no mitigation is actually done, and the mitigated result and unmitigated result are the same. Also add a test to handle this situation. 3. Make the return type of measure_pauli_strings a data class.
Done. |
eliottrosenberg
left a comment
There was a problem hiding this comment.
Thanks, Danni!! I tried testing it in this colab and got the error shown there.
Can you try again? The problem should be fixed in the latest commit ;) |
eliottrosenberg
left a comment
There was a problem hiding this comment.
Thanks, Danni! I tested it again, and there seems to be a bug in the way it is computing the expectation value. I tried it on a simulator without noise in a case where the expectation value should be 1 and got something close to 0. https://colab.sandbox.google.com/drive/1_on4xIHQNMH_2km3RjlfoZxBcYim9A3y
Let me know if you have any trouble identifying the cause of this and I can step in and help.
|
@ddddddanni Here is a simpler example that illustrates the bug: https://colab.sandbox.google.com/drive/1DUOEbrJSHIvlIN1CI7T6lug4zGDB5nyG |
Thanks for catching this! This is due to the code doesn't cover the situation that some pauli strings could be pauli I. I will fix it. |
There was a problem hiding this comment.
@ddddddanni LGTM ... I will take another look after you fix the failing CIs
| from cirq.study import ResultDict | ||
|
|
||
|
|
||
| @dataclasses.dataclass |
There was a problem hiding this comment.
prefer to use attrs over dataclasses
| 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) |
There was a problem hiding this comment.
there shouldn't be identity in this case
| # 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())))) |
There was a problem hiding this comment.
circuit.all_qubits() returns a frozenset, so you can dop the set here and elsewhere
Done! Can you try test again? The issue was Pauli I was incorrectly treated as Z in expectation calculation, and now the issue is fixed, and I also modified tests to test this. |
eliottrosenberg
left a comment
There was a problem hiding this comment.
Thanks, @ddddddanni! This is really great! One more request: can we multiply the result of measuring the PauliString by its coefficient? For example, right now your code does not distinguish between the following PauliStrings:
cirq.PauliString(cirq.Z.on_each(cirq.LineQubit.range(2)))and
-1 * cirq.PauliString(cirq.Z.on_each(cirq.LineQubit.range(2)))|
(And when you implement my previous suggestion, make sure to also scale the statistical uncertainty by |
…ng by its coefficient 2. Added a function to validate the input. Also added some tests for the validation function. 3. Fixed lint
Sure! I added some codes to multiple the coefficient with the result of measuring the paulistring. Also added some more codes to validate the input and tests. |
eliottrosenberg
left a comment
There was a problem hiding this comment.
Thanks, @ddddddanni, this is great!
…o validate the input pauli coefficient must be real.
Thanks! Failing checks should be fixed now ;) |
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #7067 +/- ##
========================================
Coverage 98.14% 98.14%
========================================
Files 1098 1100 +2
Lines 95816 96186 +370
========================================
+ Hits 94034 94403 +369
- Misses 1782 1783 +1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
NoureldinYosri
left a comment
There was a problem hiding this comment.
LGTM%style comments
| from cirq.study import ResultDict | ||
|
|
||
|
|
||
| @attrs.define |
There was a problem hiding this comment.
if this is intended to be an immutable class prefere @attrs.frozen else prefer @attrs.mutable
There was a problem hiding this comment.
Sure, changed it to @attrs.frozen
| mitigated_stddev: float | ||
| unmitigated_expectation: float | ||
| unmitigated_stddev: float | ||
| calibration_result: Optional[SingleQubitReadoutCalibrationResult] |
There was a problem hiding this comment.
since this is optional, it should have a default value = None
| calibration_result: Optional[SingleQubitReadoutCalibrationResult] | ||
|
|
||
|
|
||
| @attrs.define |
|
|
||
| def _validate_input( | ||
| circuits_to_pauli: Dict[circuits.FrozenCircuit, list[ops.PauliString]], | ||
| rng_or_seed: Union[np.random.Generator, int], |
There was a problem hiding this comment.
in most scientific libraries, the rng comes last
There was a problem hiding this comment.
Sure! Moved this to the end.
| if qubit in pauli_string: | ||
| pauli_op = pauli_string[qubit] | ||
| if pauli_op == ops.X: | ||
| operations.append(ops.ry(-np.pi / 2)(qubit)) |
There was a problem hiding this comment.
can you add a comment that this is hadamard?
| operations.append(ops.ry(-np.pi / 2)(qubit)) | |
| operations.append(ops.ry(-np.pi / 2)(qubit)) # =cirq.H |
| """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)) |
There was a problem hiding this comment.
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_lengthif you need them to be 2x2 array, then
return [np.empty((2,2))]*qubits_lengthThere was a problem hiding this comment.
Sure! I changed the method to directly return the [_build_one_qubit_confusion_matrix(0, 0) for _ in range(qubits_length)]
|
|
||
| Returns: | ||
| A list of PauliStringMeasurementResult objects, where each object contains: | ||
| - The Pauli string that was measured. |
There was a problem hiding this comment.
I think this is redundant since this description should be in the docstring of PauliStringMeasurementResult
| def measure_pauli_strings( | ||
| circuits_to_pauli: Dict[circuits.FrozenCircuit, list[ops.PauliString]], | ||
| sampler: work.Sampler, | ||
| rng_or_seed: Union[np.random.Generator, int], |
| readout_qubits = [qubits_1, qubits_2] | ||
|
|
||
| # Generate random input circuits and append measurements | ||
| input_circuit_1 = rqcg.generate_library_of_2q_circuits( |
There was a problem hiding this comment.
I think we should create a method to create the input_circuits and use it here and elsewhere
input_circuits = _create_test_circuits()
| circuit_3 = cirq.FrozenCircuit(_create_ghz(8, qubits_3)) | ||
|
|
||
| circuits_to_pauli: Dict[cirq.FrozenCircuit, list[cirq.PauliString]] = {} | ||
| circuits_to_pauli[circuit_1] = [ |
There was a problem hiding this comment.
I know about the tradeoff between damp and dry tests and to me this feels a bit too damp ... maybe
circuits_to_pauli[circuit_1] = [_generate_random_pauli_string(qubits_1, True) for _ in range(3)]|
great work @ddddddanni ... please fix the coverage CI and then we can merge this PR |
Thanks!! The coverage check should be fixed now ; ) |
…dout error mitigation (quantumlib#7067) * Modify the shuffle_circuit measurement and allow it to handle a list of qubits as input * Add a new tool for measuring expectation values of Pauli strings with readout error mitigation * Allow the measure_pauli_strings to also return the SingleQubitReadoutCalibrationResult. Besides, fixed some lints * 1. Allow shuffle_circuits_with_readout_benchmarking to take 0 for num_random_bitstrings, and add a test to cover the situation. The design is the run_shuffled_with_readout_benchmarking method will return a empty SingleQubitReadoutCalibrationResult if no calibration is actually done. 2. Allow measure_pauli_strings to take num_random_bitstrings = 0. In this case, no mitigation is actually done, and the mitigated result and unmitigated result are the same. Also add a test to handle this situation. 3. Make the return type of measure_pauli_strings a data class. * Fix a issue that caused calibration_results lookup failure * Fix: Pauli I was incorrectly treated as Z in expectation calculation * 1. Added some codes to multiply the result of measuring the PauliString by its coefficient 2. Added a function to validate the input. Also added some tests for the validation function. 3. Fixed lint * Fix a broken test * pauli_string.qubits returns the self.keys which are already unique. Thus remove the set() acts on the pauli_string.qubits * Fix type and coverage check. Besides, adds a input validation check to validate the input pauli coefficient must be real. * Address comments by @NoureldinYosri * Fix the coverage check * Fix lint line too long --------- Co-authored-by: eliottrosenberg <61400172+eliottrosenberg@users.noreply.github.com> Co-authored-by: Noureldin <noureldinyosri@google.com>
This PR does the following two things:
Improved Readout Benchmarking: Modified run_shuffled_with_readout_benchmarking to accept a list of qubit tuples (List[Tuple[Qubit]]). This allows for calculating the readout error rate on specific qubit subsets, ensuring alignment with the qubit sets used in non-identity Pauli operators within Pauli strings.
Added a tool to compute expectation values for Pauli operators. This tool calculates both unmitigated and readout-error-mitigated expectation values for each circuit, and integrates the results from the readout benchmarking.