From 1ac72ddde61299e201ef09022df671bedb482379 Mon Sep 17 00:00:00 2001 From: nate stemen Date: Wed, 3 Jan 2024 15:59:08 -0800 Subject: [PATCH 01/22] remove unused helper functions --- mitiq/shadows/shadows_utils.py | 28 +----------------------- mitiq/shadows/test/test_shadows_utils.py | 21 ------------------ 2 files changed, 1 insertion(+), 48 deletions(-) diff --git a/mitiq/shadows/shadows_utils.py b/mitiq/shadows/shadows_utils.py index a4250a5e6..7e7c4f4ed 100644 --- a/mitiq/shadows/shadows_utils.py +++ b/mitiq/shadows/shadows_utils.py @@ -9,7 +9,7 @@ """Defines utility functions for classical shadows protocol.""" -from typing import Iterable, List, Tuple +from typing import List, Tuple import numpy as np from numpy.typing import NDArray @@ -18,32 +18,6 @@ import mitiq -def eigenvalues_to_bitstring(values: Iterable[int]) -> str: - """Converts eigenvalues to bitstring. e.g., ``[-1,1,1] -> "100"`` - - Args: - values: A list of eigenvalues (must be $-1$ and $1$). - - Returns: - A string of 1s and 0s corresponding to the states associated to - eigenvalues. - """ - return "".join(["1" if v == -1 else "0" for v in values]) - - -def bitstring_to_eigenvalues(bitstring: str) -> List[int]: - """Converts bitstring to eigenvalues. e.g., ``"100" -> [-1,1,1]`` - - Args: - bitstring: A string of 1s and 0s. - - Returns: - A list of eigenvalues (either $-1$ or $1$) corresponding to the - bitstring. - """ - return [1 if b == "0" else -1 for b in bitstring] - - def create_string(str_len: int, loc_list: List[int]) -> str: """ This function returns a string of length ``str_len`` with 1s at the diff --git a/mitiq/shadows/test/test_shadows_utils.py b/mitiq/shadows/test/test_shadows_utils.py index 459e6b4f6..3d4b2b4e0 100644 --- a/mitiq/shadows/test/test_shadows_utils.py +++ b/mitiq/shadows/test/test_shadows_utils.py @@ -9,33 +9,12 @@ import mitiq from mitiq.shadows.shadows_utils import ( - bitstring_to_eigenvalues, create_string, - eigenvalues_to_bitstring, fidelity, n_measurements_opts_expectation_bound, n_measurements_tomography_bound, ) -# Tests start here - - -def test_eigenvalues_to_bitstring(): - values = [-1, 1, 1] - assert eigenvalues_to_bitstring(values) == "100" - assert bitstring_to_eigenvalues(eigenvalues_to_bitstring(values)) == values - - -def test_bitstring_to_eigenvalues(): - bitstring = "100" - np.testing.assert_array_equal( - bitstring_to_eigenvalues(bitstring), np.array([-1, 1, 1]) - ) - assert ( - eigenvalues_to_bitstring(bitstring_to_eigenvalues(bitstring)) - == bitstring - ) - def test_create_string(): str_len = 5 From 97456c660da1f014e7917f22ce2e60b0b6f7b65c Mon Sep 17 00:00:00 2001 From: nate stemen Date: Wed, 3 Jan 2024 16:49:23 -0800 Subject: [PATCH 02/22] simplify product calculation --- mitiq/shadows/classical_postprocessing.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/mitiq/shadows/classical_postprocessing.py b/mitiq/shadows/classical_postprocessing.py index 5b6cd6247..a3f7bd014 100644 --- a/mitiq/shadows/classical_postprocessing.py +++ b/mitiq/shadows/classical_postprocessing.py @@ -357,15 +357,12 @@ def expectation_estimation_shadow( ) b = create_string(num_qubits, target_locs) - f_val = f_est.get(b, None) - if f_val is None: - product = 0.0 - else: - # product becomes an array of snapshots expectation values - # witch satisfy condition (1) and (2) - product = (1.0 / f_val) * product + f_val = f_est.get(b, np.inf) + # product becomes an array of snapshot expectation values + # witch satisfy condition (1) and (2) + product = (1 / f_val) * product else: - product = 3 ** (len(target_locs)) * product + product = 3 ** len(target_locs) * product else: product = 0.0 From 227eaf6cda4152d670c50d81d5a3bfe03122e044 Mon Sep 17 00:00:00 2001 From: nate stemen Date: Wed, 3 Jan 2024 16:50:05 -0800 Subject: [PATCH 03/22] remove unnecessary type ignore comment --- mitiq/shadows/classical_postprocessing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mitiq/shadows/classical_postprocessing.py b/mitiq/shadows/classical_postprocessing.py index a3f7bd014..a3977749f 100644 --- a/mitiq/shadows/classical_postprocessing.py +++ b/mitiq/shadows/classical_postprocessing.py @@ -293,7 +293,7 @@ def shadow_state_reconstruction( def expectation_estimation_shadow( measurement_outcomes: Tuple[List[str], List[str]], - pauli_str: mitiq.PauliString, # type: ignore + pauli_str: mitiq.PauliString, k_shadows: int, pauli_twirling_calibration: bool, f_est: Optional[Dict[str, float]] = None, From fe2340c9a18d0ff046a4ba93f291407c1fda6903 Mon Sep 17 00:00:00 2001 From: nate stemen Date: Wed, 3 Jan 2024 17:01:07 -0800 Subject: [PATCH 04/22] avoid using private _unitary_ method --- mitiq/shadows/classical_postprocessing.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mitiq/shadows/classical_postprocessing.py b/mitiq/shadows/classical_postprocessing.py index a3977749f..e16c43d11 100644 --- a/mitiq/shadows/classical_postprocessing.py +++ b/mitiq/shadows/classical_postprocessing.py @@ -21,9 +21,9 @@ # Local unitaries to measure Pauli operators in the Z basis PAULI_MAP = { - "X": cirq.H._unitary_(), - "Y": cirq.H._unitary_() @ cirq.S._unitary_().conj(), - "Z": cirq.I._unitary_(), + "X": cirq.unitary(cirq.H), + "Y": cirq.unitary(cirq.H) @ cirq.unitary(cirq.S).conj(), + "Z": cirq.unitary(cirq.I), } # Density matrices of single-qubit basis states @@ -247,7 +247,7 @@ def classical_snapshot( state = ZERO_STATE if b == "0" else ONE_STATE U = PAULI_MAP[u] # apply inverse of the quantum channel,get PTM vector rep - local_rho = 3.0 * (U.conj().T @ state @ U) - cirq.I._unitary_() + local_rho = 3.0 * (U.conj().T @ state @ U) - cirq.unitary(cirq.I) local_rhos.append(local_rho) rho_snapshot = matrix_kronecker_product(local_rhos) From 3713553fa5cd481cca1c271923988848a2f01983 Mon Sep 17 00:00:00 2001 From: nate stemen Date: Wed, 3 Jan 2024 17:03:03 -0800 Subject: [PATCH 05/22] import numpy typing library as a whole this is consistent with how we do it in other parts of the codebase --- mitiq/shadows/classical_postprocessing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mitiq/shadows/classical_postprocessing.py b/mitiq/shadows/classical_postprocessing.py index e16c43d11..3ad8b9604 100644 --- a/mitiq/shadows/classical_postprocessing.py +++ b/mitiq/shadows/classical_postprocessing.py @@ -13,7 +13,7 @@ import cirq import numpy as np -from numpy.typing import NDArray +import numpy.typing as npt import mitiq from mitiq.shadows.shadows_utils import create_string @@ -184,7 +184,7 @@ def classical_snapshot( u_list_shadow: str, pauli_twirling_calibration: bool, f_est: Optional[Dict[str, float]] = None, -) -> NDArray[Any]: +) -> npt.NDArray[Any]: r""" Implement a single snapshot state reconstruction with calibration of the noisy quantum channel. @@ -258,7 +258,7 @@ def shadow_state_reconstruction( shadow_measurement_outcomes: Tuple[List[str], List[str]], pauli_twirling_calibration: bool, f_est: Optional[Dict[str, float]] = None, -) -> NDArray[Any]: +) -> npt.NDArray[Any]: """Reconstruct a state approximation as an average over all snapshots. Args: From 96e2ac82180d5dcd09dd29462f47f71f0dab9620 Mon Sep 17 00:00:00 2001 From: nate stemen Date: Thu, 4 Jan 2024 08:01:41 -0800 Subject: [PATCH 06/22] remove unused `fidelity` function --- mitiq/shadows/shadows_utils.py | 28 ------------------------ mitiq/shadows/test/test_shadows_utils.py | 11 ---------- 2 files changed, 39 deletions(-) diff --git a/mitiq/shadows/shadows_utils.py b/mitiq/shadows/shadows_utils.py index 7e7c4f4ed..740cc3114 100644 --- a/mitiq/shadows/shadows_utils.py +++ b/mitiq/shadows/shadows_utils.py @@ -12,8 +12,6 @@ from typing import List, Tuple import numpy as np -from numpy.typing import NDArray -from scipy.linalg import sqrtm import mitiq @@ -109,29 +107,3 @@ def n_measurements_opts_expectation_bound( / error**2 ) return int(np.ceil(N * K)), int(K) - - -def fidelity( - sigma: NDArray[np.complex64], rho: NDArray[np.complex64] -) -> float: - """ - Calculate the fidelity between two states. - - Args: - sigma: A state in terms of square matrix or vector. - rho: A state in terms square matrix or vector. - - Returns: - Scalar corresponding to the fidelity. - """ - if sigma.ndim == 1 and rho.ndim == 1: - val = np.abs(np.dot(sigma.conj(), rho)) ** 2.0 - elif sigma.ndim == 1 and rho.ndim == 2: - val = np.abs(sigma.conj().T @ rho @ sigma) - elif sigma.ndim == 2 and rho.ndim == 1: - val = np.abs(rho.conj().T @ sigma @ rho) - elif sigma.ndim == 2 and rho.ndim == 2: - val = np.abs(np.trace(sqrtm(sigma) @ rho @ sqrtm(sigma))) - else: - raise ValueError("Invalid input dimensions") - return float(val) diff --git a/mitiq/shadows/test/test_shadows_utils.py b/mitiq/shadows/test/test_shadows_utils.py index 3d4b2b4e0..18b5f4be0 100644 --- a/mitiq/shadows/test/test_shadows_utils.py +++ b/mitiq/shadows/test/test_shadows_utils.py @@ -5,12 +5,9 @@ """Defines utility functions for classical shadows protocol.""" -import numpy as np - import mitiq from mitiq.shadows.shadows_utils import ( create_string, - fidelity, n_measurements_opts_expectation_bound, n_measurements_tomography_bound, ) @@ -43,11 +40,3 @@ def test_n_measurements_opts_expectation_bound(): N, K = n_measurements_opts_expectation_bound(0.5, observables, 0.1) assert isinstance(N, int), f"Expected int, got {type(N)}" assert isinstance(K, int), f"Expected int, got {type(K)}" - - -def test_fidelity(): - state_vector = np.array([0.5, 0.5, 0.5, 0.5]) - rho = np.eye(4) / 4 - assert np.isclose( - fidelity(state_vector, rho), 0.25 - ), f"Expected 0.25, got {fidelity(state_vector, rho)}" From 00946db1825bbba9445ba8659f68fbf029978a76 Mon Sep 17 00:00:00 2001 From: nate stemen Date: Sat, 6 Jan 2024 00:36:09 -0800 Subject: [PATCH 07/22] add more extensive tests --- .../test/test_classical_postprocessing.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/mitiq/shadows/test/test_classical_postprocessing.py b/mitiq/shadows/test/test_classical_postprocessing.py index 9d08b5be6..cbdb6a1de 100644 --- a/mitiq/shadows/test/test_classical_postprocessing.py +++ b/mitiq/shadows/test/test_classical_postprocessing.py @@ -24,6 +24,67 @@ def test_get_single_shot_pauli_fidelity(): u_list = "XY" expected_result = {"00": 1.0, "01": 0.0, "10": 0.0, "11": 0.0} assert get_single_shot_pauli_fidelity(b_list, u_list) == expected_result + b_list = "01101" + u_list = "XYZYZ" + print(get_single_shot_pauli_fidelity(b_list, u_list)) + assert get_single_shot_pauli_fidelity(b_list, u_list) == { + "00000": 1.0, + "10000": 0.0, + "01000": 0.0, + "00100": -1.0, + "00010": 0.0, + "00001": -1.0, + "11000": 0.0, + "10100": 0.0, + "10010": 0.0, + "10001": 0.0, + "01100": 0.0, + "01010": 0.0, + "01001": 0.0, + "00110": 0.0, + "00101": 1.0, + "00011": 0.0, + "11100": 0.0, + "11010": 0.0, + "11001": 0.0, + "10110": 0.0, + "10101": 0.0, + "10011": 0.0, + "01110": 0.0, + "01101": 0.0, + "01011": 0.0, + "00111": 0.0, + "11110": 0.0, + "11101": 0.0, + "11011": 0.0, + "10111": 0.0, + "01111": 0.0, + "11111": 0.0, + } + + +def test_get_single_shot_pauli_fidelity_with_locality(): + b_list = "11101" + u_list = "XYZYZ" + print(get_single_shot_pauli_fidelity(b_list, u_list, locality=2)) + assert get_single_shot_pauli_fidelity(b_list, u_list, locality=2) == { + "00000": 1.0, + "10000": 0.0, + "01000": 0.0, + "00100": -1.0, + "00010": 0.0, + "00001": -1.0, + "11000": 0.0, + "10100": 0.0, + "10010": 0.0, + "10001": 0.0, + "01100": 0.0, + "01010": 0.0, + "01001": 0.0, + "00110": 0.0, + "00101": 1.0, + "00011": 0.0, + } def test_get_pauli_fidelity(): From d2e0f16ed2fbf17c51ccf40d00337adae0323482 Mon Sep 17 00:00:00 2001 From: nate stemen Date: Sat, 6 Jan 2024 00:38:27 -0800 Subject: [PATCH 08/22] refactor/simplify `get_single_shot_pauli_fidelity` --- mitiq/shadows/classical_postprocessing.py | 59 +++++++---------------- mitiq/shadows/shadows_utils.py | 32 +++++++++++- 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/mitiq/shadows/classical_postprocessing.py b/mitiq/shadows/classical_postprocessing.py index 3ad8b9604..6fb2d3a4f 100644 --- a/mitiq/shadows/classical_postprocessing.py +++ b/mitiq/shadows/classical_postprocessing.py @@ -8,7 +8,9 @@ # LICENSE file in the root directory of this source tree. """Classical post-processing process of classical shadows.""" -from itertools import combinations +from functools import reduce +from itertools import compress +from operator import mul from typing import Any, Dict, List, Optional, Tuple import cirq @@ -16,7 +18,7 @@ import numpy.typing as npt import mitiq -from mitiq.shadows.shadows_utils import create_string +from mitiq.shadows.shadows_utils import create_string, valid_bitstrings from mitiq.utils import matrix_kronecker_product, operator_ptm_vector_rep # Local unitaries to measure Pauli operators in the Z basis @@ -30,28 +32,9 @@ ZERO_STATE = np.diag([1.0 + 0.0j, 0.0 + 0.0j]) ONE_STATE = np.diag([0.0 + 0.0j, 1.0 + 0.0j]) -# F_LOCAL_MAP is based on local Pauli fidelity of qubit i -# f_b_i = <> -# s.t. f_0 = U_11U_11^* + U_12U_12^*, f_1 = U_21U_21^* + U_22U_22^* -F_LOCAL_MAP = { - "0X": 0.0, - "0Y": 0.0, - "0Z": 1.0, - "1X": 0.0, - "1Y": 0.0, - "1Z": -1.0, -} - -""" -The following functions are used in the classical post-processing -of calibration -""" - def get_single_shot_pauli_fidelity( - bit_string: str, - pauli_string: str, - locality: Optional[int] = None, + bitstring: str, paulistring: str, locality: Optional[int] = None ) -> Dict[str, float]: r""" Calculate Pauli fidelity :math:`f_b` for a single shot measurement of the @@ -84,26 +67,18 @@ def get_single_shot_pauli_fidelity( than or equal to w. The corresponding Pauli fidelity is the product of local Pauli fidelity where the associated locus in the keys are '1'. """ - num_qubits = len(bit_string) - if locality is None: - locality = num_qubits - # local_pauli_fidelity is a list of local Pauli fidelity for each qubit - local_pauli_fidelity = np.array( - [F_LOCAL_MAP[b + u] for b, u in zip(bit_string, pauli_string)] - ) - # f_est is a dictionary of Pauli fidelity for each b_string - f_est = {create_string(num_qubits, []): 1.0} - for w in range(1, locality + 1): - target_locs = np.array(list(combinations(range(num_qubits), w))) - single_round_pauli_fidelity = np.prod( - local_pauli_fidelity[target_locs], axis=1 - ) - for loc, fidelity in zip(target_locs, single_round_pauli_fidelity): - # b_str is a string of length n with maximum number of w 1s. - b_str = create_string(num_qubits, loc) - f_est[b_str] = fidelity - - return f_est + pauli_fidelity = {"Z0": 1.0, "Z1": -1.0} + local_fidelities = [ + pauli_fidelity.get(p + b, 0.0) for b, p in zip(bitstring, paulistring) + ] + num_qubits = len(bitstring) + bitstrings = valid_bitstrings(num_qubits, max_hamming_weight=locality) + fidelities = {} + for bitstring in bitstrings: + subset_fidelities = compress(local_fidelities, map(int, bitstring)) + fidelities[bitstring] = reduce(mul, subset_fidelities, 1.0) + + return fidelities def get_pauli_fidelities( diff --git a/mitiq/shadows/shadows_utils.py b/mitiq/shadows/shadows_utils.py index 740cc3114..8d8dddebf 100644 --- a/mitiq/shadows/shadows_utils.py +++ b/mitiq/shadows/shadows_utils.py @@ -9,7 +9,7 @@ """Defines utility functions for classical shadows protocol.""" -from typing import List, Tuple +from typing import List, Optional, Tuple import numpy as np @@ -39,6 +39,36 @@ def create_string(str_len: int, loc_list: List[int]) -> str: ) +def valid_bitstrings( + num_qubits: int, max_hamming_weight: Optional[int] = None +) -> set[str]: + """ + Description. + + Args: + num_qubits: + max_hamming_weight: + + Returns: + The set of all valid bitstrings on ``num_qubits`` bits, with a maximum + hamming weight. + Raises: + Value error when ``max_hamming_weight`` is not greater than 0. + """ + if max_hamming_weight and max_hamming_weight < 1: + raise ValueError( + "max_hamming_weight must be greater than 0. " + f"Got {max_hamming_weight}." + ) + + bitstrings = { + bin(i)[2:].zfill(num_qubits) + for i in range(2**num_qubits) + if bin(i).count("1") <= max_hamming_weight or num_qubits + } + return bitstrings + + def n_measurements_tomography_bound(epsilon: float, num_qubits: int) -> int: """ This function returns the minimum number of classical shadows required From ec4f33e17d04e20870e5fc16188d1756987c927f Mon Sep 17 00:00:00 2001 From: nate stemen Date: Sat, 6 Jan 2024 09:49:28 -0800 Subject: [PATCH 09/22] simplify expected result/assertion testing --- mitiq/shadows/test/test_classical_postprocessing.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/mitiq/shadows/test/test_classical_postprocessing.py b/mitiq/shadows/test/test_classical_postprocessing.py index cbdb6a1de..a7ac26ed8 100644 --- a/mitiq/shadows/test/test_classical_postprocessing.py +++ b/mitiq/shadows/test/test_classical_postprocessing.py @@ -93,18 +93,11 @@ def test_get_pauli_fidelity(): ["XX", "YY", "ZZ", "XY"], ) k_calibration = 2 - expected_result = { - "00": (1 + 0j), - "10": (-0.25 + 0j), - "01": (0.25 + 0j), - "11": (-0.25 + 0j), - } + expected_result = {"00": 1, "10": -0.25, "01": 0.25, "11": -0.25} result = get_pauli_fidelities( calibration_measurement_outcomes, k_calibration ) - - for key in expected_result.keys(): - assert np.isclose(result[key], expected_result[key]) + assert result == expected_result def test_classical_snapshot_cal(): From 5a758a34bfceac339917b9504d04b0a4aeaa73e5 Mon Sep 17 00:00:00 2001 From: nate stemen Date: Sun, 7 Jan 2024 11:39:42 -0800 Subject: [PATCH 10/22] refactor `get_pauli_fidelities` --- mitiq/shadows/classical_postprocessing.py | 69 ++++++++--------------- mitiq/shadows/shadows_utils.py | 19 ++++++- 2 files changed, 41 insertions(+), 47 deletions(-) diff --git a/mitiq/shadows/classical_postprocessing.py b/mitiq/shadows/classical_postprocessing.py index 6fb2d3a4f..12e059b8e 100644 --- a/mitiq/shadows/classical_postprocessing.py +++ b/mitiq/shadows/classical_postprocessing.py @@ -8,9 +8,11 @@ # LICENSE file in the root directory of this source tree. """Classical post-processing process of classical shadows.""" +from collections import defaultdict from functools import reduce from itertools import compress from operator import mul +from statistics import median from typing import Any, Dict, List, Optional, Tuple import cirq @@ -18,7 +20,11 @@ import numpy.typing as npt import mitiq -from mitiq.shadows.shadows_utils import create_string, valid_bitstrings +from mitiq.shadows.shadows_utils import ( + batch_calibration_data, + create_string, + valid_bitstrings, +) from mitiq.utils import matrix_kronecker_product, operator_ptm_vector_rep # Local unitaries to measure Pauli operators in the Z basis @@ -82,8 +88,8 @@ def get_single_shot_pauli_fidelity( def get_pauli_fidelities( - calibration_measurement_outcomes: Tuple[List[str], List[str]], - k_calibration: int, + calibration_outcomes: Tuple[List[str], List[str]], + batch_size: int, locality: Optional[int] = None, ) -> Dict[str, complex]: r""" @@ -105,53 +111,24 @@ def get_pauli_fidelities( A :math:`2^n`-dimensional dictionary of Pauli fidelities :math:`f_b` for :math:`b = \{0,1\}^{n}` """ - - # classical values of random Pauli measurement stored in classical computer - b_lists, u_lists = calibration_measurement_outcomes - - # number of measurements in each split - n_total_measurements = len(b_lists) - - means: Dict[str, List[float]] = {} # key is bitstring, value is mean - - group_idxes = np.array_split( - np.arange(n_total_measurements), k_calibration - ) - # loop over the splits of the shadow: - for idxes in group_idxes: - b_lists_k = np.array(b_lists)[idxes] - u_lists_k = np.array(u_lists)[idxes] - - n_group_measurements = len(b_lists_k) - group_results = [] - for j in range(n_group_measurements): - bitstring, u_list = b_lists_k[j], u_lists_k[j] - f_est = get_single_shot_pauli_fidelity( - bitstring, u_list, locality=locality + means = defaultdict(list) + for bitstrings, paulistrings in batch_calibration_data( + calibration_outcomes, batch_size + ): + all_fidelities = defaultdict(list) + for bitstring, paulistring in zip(bitstrings, paulistrings): + fidelities = get_single_shot_pauli_fidelity( + bitstring, paulistring, locality=locality ) - group_results.append(f_est) + for b, f in fidelities.items(): + all_fidelities[b].append(f) - f_est_group = { - b: sum([f[b] for f in group_results]) / n_group_measurements - for b in group_results[0] - } + for bitstring, fidelities in all_fidelities.items(): + means[bitstring].append(sum(fidelities) / batch_size) - for bitstring, mean in f_est_group.items(): - if bitstring not in means: - means[bitstring] = [] - means[bitstring].append(mean) - # return the median of means - medians = { - bitstring: complex(np.median(values)) - for bitstring, values in means.items() + return { + bitstring: median(averages) for bitstring, averages in means.items() } - return medians - - -""" -The following functions are used in the classical post-processing of -classical shadows. -""" def classical_snapshot( diff --git a/mitiq/shadows/shadows_utils.py b/mitiq/shadows/shadows_utils.py index 8d8dddebf..47bbff7cb 100644 --- a/mitiq/shadows/shadows_utils.py +++ b/mitiq/shadows/shadows_utils.py @@ -9,7 +9,7 @@ """Defines utility functions for classical shadows protocol.""" -from typing import List, Optional, Tuple +from typing import Generator, List, Optional, Tuple import numpy as np @@ -69,6 +69,23 @@ def valid_bitstrings( return bitstrings +def batch_calibration_data( + data: Tuple[List[str], List[str]], batch_size: int +) -> Generator[Tuple[List[str], List[str]], None, None]: + """Batch calibration into chunks of size batch_size. + + Args: + data: The random Pauli measurement outcomes. + batch_size: Size of each batch that will be processed. + + Yields: + Tuples of bit strings and pauli strings. + """ + bits, paulis = data + for i in range(0, len(bits), batch_size): + yield bits[i : i + batch_size], paulis[i : i + batch_size] + + def n_measurements_tomography_bound(epsilon: float, num_qubits: int) -> int: """ This function returns the minimum number of classical shadows required From b92b798c5682b58b7346b25464863d0da3a75f82 Mon Sep 17 00:00:00 2001 From: nate stemen Date: Sun, 7 Jan 2024 11:41:17 -0800 Subject: [PATCH 11/22] simplify variable names in `shadow_state_reconstruction` --- mitiq/shadows/classical_postprocessing.py | 17 +++++------------ mitiq/shadows/shadows.py | 2 +- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/mitiq/shadows/classical_postprocessing.py b/mitiq/shadows/classical_postprocessing.py index 12e059b8e..ce224464c 100644 --- a/mitiq/shadows/classical_postprocessing.py +++ b/mitiq/shadows/classical_postprocessing.py @@ -208,8 +208,8 @@ def classical_snapshot( def shadow_state_reconstruction( shadow_measurement_outcomes: Tuple[List[str], List[str]], - pauli_twirling_calibration: bool, - f_est: Optional[Dict[str, float]] = None, + calibrate: bool, + fidelities: Optional[Dict[str, float]] = None, ) -> npt.NDArray[Any]: """Reconstruct a state approximation as an average over all snapshots. @@ -225,19 +225,12 @@ def shadow_state_reconstruction( Returns: The state reconstructed from classical shadow protocol """ + bitstrings, paulistrings = shadow_measurement_outcomes - # classical values of random Pauli measurement stored in classical computer - b_lists_shadow, u_lists_shadow = shadow_measurement_outcomes - - # Averaging over snapshot states. return np.mean( [ - classical_snapshot( - b_list_shadow, u_list_shadow, pauli_twirling_calibration, f_est - ) - for b_list_shadow, u_list_shadow in zip( - b_lists_shadow, u_lists_shadow - ) + classical_snapshot(bitstring, paulistring, calibrate, fidelities) + for bitstring, paulistring in zip(bitstrings, paulistrings) ], axis=0, ) diff --git a/mitiq/shadows/shadows.py b/mitiq/shadows/shadows.py index da9f13b86..6665d1a32 100644 --- a/mitiq/shadows/shadows.py +++ b/mitiq/shadows/shadows.py @@ -196,7 +196,7 @@ def classical_post_processing( output: Dict[str, Union[float, NDArray[Any]]] = {} if state_reconstruction: reconstructed_state = shadow_state_reconstruction( - shadow_outcomes, use_calibration, f_est=calibration_results + shadow_outcomes, use_calibration, fidelities=calibration_results ) output["reconstructed_state"] = reconstructed_state # type: ignore elif observables is not None: From 459c7649ecb641beedc7b615b10d598544b44498 Mon Sep 17 00:00:00 2001 From: nate stemen Date: Mon, 8 Jan 2024 09:37:08 -0800 Subject: [PATCH 12/22] distinct variable names --- mitiq/shadows/classical_postprocessing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mitiq/shadows/classical_postprocessing.py b/mitiq/shadows/classical_postprocessing.py index ce224464c..5c7dd7c6a 100644 --- a/mitiq/shadows/classical_postprocessing.py +++ b/mitiq/shadows/classical_postprocessing.py @@ -123,8 +123,8 @@ def get_pauli_fidelities( for b, f in fidelities.items(): all_fidelities[b].append(f) - for bitstring, fidelities in all_fidelities.items(): - means[bitstring].append(sum(fidelities) / batch_size) + for bitstring, fids in all_fidelities.items(): + means[bitstring].append(sum(fids) / batch_size) return { bitstring: median(averages) for bitstring, averages in means.items() From c49a640a4b74b35d0d50d79697c317430e84039b Mon Sep 17 00:00:00 2001 From: nate stemen Date: Mon, 8 Jan 2024 10:26:34 -0800 Subject: [PATCH 13/22] remove boolean calibration flag use optional fidelities instead --- mitiq/shadows/classical_postprocessing.py | 37 +++++-------------- mitiq/shadows/shadows.py | 12 +----- .../test/test_classical_postprocessing.py | 9 ++--- mitiq/shadows/test/test_shadows.py | 10 ++--- 4 files changed, 17 insertions(+), 51 deletions(-) diff --git a/mitiq/shadows/classical_postprocessing.py b/mitiq/shadows/classical_postprocessing.py index 5c7dd7c6a..d7dfd95e2 100644 --- a/mitiq/shadows/classical_postprocessing.py +++ b/mitiq/shadows/classical_postprocessing.py @@ -134,7 +134,6 @@ def get_pauli_fidelities( def classical_snapshot( b_list_shadow: str, u_list_shadow: str, - pauli_twirling_calibration: bool, f_est: Optional[Dict[str, float]] = None, ) -> npt.NDArray[Any]: r""" @@ -148,9 +147,8 @@ def classical_snapshot( b = '1' corresponds to :math:`|1\rangle`. u_list_shadow: list of len 1, contains str of ("XYZ..") for the applied Pauli measurement on each qubit. - pauli_twirling_calibration: Whether to use Pauli twirling - calibration. - f_est: The estimated Pauli fidelity for each calibration + f_est: The estimated Pauli fidelities to use for calibration if + available. Returns: Reconstructed classical snapshot in terms of nparray. @@ -164,12 +162,7 @@ def classical_snapshot( pi_zero = np.diag(pi_zero) pi_one = np.diag(pi_one) - if pauli_twirling_calibration: - if f_est is None: - raise ValueError( - "estimation of Pauli fidelity must be provided for Pauli" - "twirling calibration." - ) + if f_est: elements = [] # get b_list and f for each calibration measurement for b_list_cal, f in f_est.items(): @@ -208,7 +201,6 @@ def classical_snapshot( def shadow_state_reconstruction( shadow_measurement_outcomes: Tuple[List[str], List[str]], - calibrate: bool, fidelities: Optional[Dict[str, float]] = None, ) -> npt.NDArray[Any]: """Reconstruct a state approximation as an average over all snapshots. @@ -217,11 +209,8 @@ def shadow_state_reconstruction( shadow_measurement_outcomes: Measurement result and the basis performing the measurement obtained from `random_pauli_measurement` for classical shadow protocol. - shadow_measurement_outcomes: Measurement results obtained from - `random_pauli_measurement` for classical shadow protocol. - pauli_twirling_calibration: Whether to use Pauli twirling - calibration. - f_est: The estimated Pauli fidelity for each calibration + f_est: The estimated Pauli fidelities to use for calibration if + available. Returns: The state reconstructed from classical shadow protocol """ @@ -229,7 +218,7 @@ def shadow_state_reconstruction( return np.mean( [ - classical_snapshot(bitstring, paulistring, calibrate, fidelities) + classical_snapshot(bitstring, paulistring, fidelities) for bitstring, paulistring in zip(bitstrings, paulistrings) ], axis=0, @@ -240,7 +229,6 @@ def expectation_estimation_shadow( measurement_outcomes: Tuple[List[str], List[str]], pauli_str: mitiq.PauliString, k_shadows: int, - pauli_twirling_calibration: bool, f_est: Optional[Dict[str, float]] = None, ) -> float: """Calculate the expectation value of an observable from classical shadows. @@ -252,9 +240,8 @@ def expectation_estimation_shadow( pauli_str: Single mitiq observable consisting of Pauli operators. k_shadows: number of splits in the median of means estimator. - pauli_twirling_calibration: Whether to use Pauli twirling - calibration. - f_est: The estimated Pauli fidelities for each calibration + f_est: The estimated Pauli fidelities to use for calibration if + available. Returns: Float corresponding to the estimate of the observable @@ -294,13 +281,7 @@ def expectation_estimation_shadow( axis=1, ) - if pauli_twirling_calibration: - if f_est is None: - raise ValueError( - "estimation of Pauli fidelity must be provided for" - "Pauli twirling calibration." - ) - + if f_est: b = create_string(num_qubits, target_locs) f_val = f_est.get(b, np.inf) # product becomes an array of snapshot expectation values diff --git a/mitiq/shadows/shadows.py b/mitiq/shadows/shadows.py index 6665d1a32..d395b510c 100644 --- a/mitiq/shadows/shadows.py +++ b/mitiq/shadows/shadows.py @@ -154,7 +154,6 @@ def shadow_quantum_processing( def classical_post_processing( shadow_outcomes: Tuple[List[str], List[str]], - use_calibration: bool = False, calibration_results: Optional[Dict[str, float]] = None, observables: Optional[List[mitiq.PauliString]] = None, k_shadows: Optional[int] = None, @@ -166,7 +165,6 @@ def classical_post_processing( Args: shadow_outcomes: The output of function `shadow_quantum_processing`. - use_calibration: Whether to use the robust shadow estimation. calibration_results: The output of function `pauli_twirling_calibrate`. observables: The set of observables to measure. k_shadows: Number of groups of "median of means" used for shadow @@ -175,6 +173,7 @@ def classical_post_processing( the expectation value of the observables. Returns: + TODO: rewrite this. If `state_reconstruction` is True: state tomography matrix in :math:`\mathbb{M}_{2^n}(\mathbb{C})` if use_calibration is False, otherwise state tomography vector in :math:`\mathbb{C}^{4^d}`. @@ -182,12 +181,6 @@ def classical_post_processing( observables. """ - if use_calibration: - if calibration_results is None: - raise ValueError( - "Calibration results cannot be None when use_calibration" - ) - """ Additional information: Shadow stage 2: Estimate the expectation value of the observables OR @@ -196,7 +189,7 @@ def classical_post_processing( output: Dict[str, Union[float, NDArray[Any]]] = {} if state_reconstruction: reconstructed_state = shadow_state_reconstruction( - shadow_outcomes, use_calibration, fidelities=calibration_results + shadow_outcomes, fidelities=calibration_results ) output["reconstructed_state"] = reconstructed_state # type: ignore elif observables is not None: @@ -208,7 +201,6 @@ def classical_post_processing( shadow_outcomes, obs, k_shadows=k_shadows, - pauli_twirling_calibration=use_calibration, f_est=calibration_results, ) output[str(obs)] = expectation_values diff --git a/mitiq/shadows/test/test_classical_postprocessing.py b/mitiq/shadows/test/test_classical_postprocessing.py index a7ac26ed8..07a4f5060 100644 --- a/mitiq/shadows/test/test_classical_postprocessing.py +++ b/mitiq/shadows/test/test_classical_postprocessing.py @@ -103,7 +103,6 @@ def test_get_pauli_fidelity(): def test_classical_snapshot_cal(): b_list_shadow = "01" u_list_shadow = "XY" - pauli_twirling_calibration = True f_est = {"00": 1, "01": 1 / 3, "10": 1 / 3, "11": 1 / 9} expected_result = operator_ptm_vector_rep( np.array( @@ -116,9 +115,7 @@ def test_classical_snapshot_cal(): ) ) np.testing.assert_array_almost_equal( - classical_snapshot( - b_list_shadow, u_list_shadow, pauli_twirling_calibration, f_est - ), + classical_snapshot(b_list_shadow, u_list_shadow, f_est), expected_result, ) @@ -261,7 +258,7 @@ def test_shadow_state_reconstruction_cal(): ] ) ) - result = shadow_state_reconstruction(measurement_outcomes, True, f_est) + result = shadow_state_reconstruction(measurement_outcomes, f_est) num_qubits = len(measurement_outcomes[0]) assert isinstance(result, np.ndarray) assert result.shape == (4**num_qubits,) @@ -313,7 +310,7 @@ def test_expectation_estimation_shadow_cal(): print("expected_result", expected_result) result = expectation_estimation_shadow( - measurement_outcomes, observable, k, True, f_est + measurement_outcomes, observable, k, f_est ) assert isinstance(result, float), f"Expected a float, got {type(result)}" assert np.isclose(result, expected_result) diff --git a/mitiq/shadows/test/test_shadows.py b/mitiq/shadows/test/test_shadows.py index 795681ee0..370157585 100644 --- a/mitiq/shadows/test/test_shadows.py +++ b/mitiq/shadows/test/test_shadows.py @@ -4,6 +4,7 @@ # LICENSE file in the root directory of this source tree. """Test classical shadow estimation process.""" +from numbers import Number import cirq @@ -38,7 +39,6 @@ def executor( def test_pauli_twirling_calibrate(): - # Call the function with valid inputs result = pauli_twirling_calibrate( qubits=qubits, executor=executor, num_total_measurements_calibration=2 @@ -47,9 +47,8 @@ def test_pauli_twirling_calibrate(): # Check that the dictionary contains the correct number of entries assert len(result) <= 2**num_qubits - # Check that all values in the dictionary are floats for value in result.values(): - assert isinstance(value, complex) + assert isinstance(value, Number) # Call shadow_quantum_processing to get shadow_outcomes shadow_outcomes = (["11", "00"], ["ZZ", "XX"]) @@ -63,13 +62,11 @@ def test_pauli_twirling_calibrate(): # Check that the dictionary contains the correct number of entries assert len(result) <= 2**num_qubits - # Check that all values in the dictionary are floats for value in result.values(): - assert isinstance(value, complex) + assert isinstance(value, Number) def test_shadow_quantum_processing(): - # Call the function with valid inputs result = shadow_quantum_processing( circuit, executor, num_total_measurements_shadow=10 @@ -112,7 +109,6 @@ def test_classical_post_processing(): ) result_cal = classical_post_processing( shadow_outcomes, - use_calibration=True, calibration_results=calibration_results, observables=observables, k_shadows=1, From 7e0c5f69e21461ee1a981d1fb555d35ba2e87182 Mon Sep 17 00:00:00 2001 From: nate stemen Date: Mon, 8 Jan 2024 15:18:53 -0800 Subject: [PATCH 14/22] simplify tests; use `np.testing` functions for assertions --- .../test/test_classical_postprocessing.py | 149 ++++-------------- 1 file changed, 30 insertions(+), 119 deletions(-) diff --git a/mitiq/shadows/test/test_classical_postprocessing.py b/mitiq/shadows/test/test_classical_postprocessing.py index 07a4f5060..a79e7c073 100644 --- a/mitiq/shadows/test/test_classical_postprocessing.py +++ b/mitiq/shadows/test/test_classical_postprocessing.py @@ -125,144 +125,55 @@ def test_classical_snapshot(): u_list = "XY" expected_result = np.array( [ - [0.25 + 0.0j, 0.0 + 0.75j, 0.75 + 0.0j, 0.0 + 2.25j], - [0.0 - 0.75j, 0.25 + 0.0j, 0.0 - 2.25j, 0.75 + 0.0j], - [0.75 + 0.0j, 0.0 + 2.25j, 0.25 + 0.0j, 0.0 + 0.75j], - [0.0 - 2.25j, 0.75 + 0.0j, 0.0 - 0.75j, 0.25 + 0.0j], + [0.25, 0.75j, 0.75, 2.25j], + [-0.75j, 0.25, -2.25j, 0.75], + [0.75, 2.25j, 0.25, 0.75j], + [-2.25j, 0.75, -0.75j, 0.25], ] ) result = classical_snapshot(b_list, u_list, False) - assert isinstance(result, np.ndarray) - assert result.shape == ( - 2 ** len(b_list), - 2 ** len(b_list), - ) - assert np.allclose(result, expected_result) + np.testing.assert_allclose(result, expected_result) def test_shadow_state_reconstruction(): - b_lists = ["010", "001", "000"] - u_lists = ["XYZ", "ZYX", "YXZ"] - - measurement_outcomes = (b_lists, u_lists) + bitstrings = ["010", "001", "000"] + paulistrings = ["XYZ", "ZYX", "YXZ"] + measurement_outcomes = (bitstrings, paulistrings) - expected_result = np.array( + expected_state = np.array( [ - [ - [ - 0.5 + 0.0j, - -0.5 + 0.0j, - 0.5 + 0.0j, - 0.0 + 1.5j, - 0.5 - 0.5j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - ], - [ - -0.5 + 0.0j, - 0.0 + 0.0j, - 0.0 + 1.5j, - -0.25 - 0.75j, - 0.0 + 0.0j, - -0.25 + 0.25j, - 0.0 + 0.0j, - 0.0 + 0.0j, - ], - [ - 0.5 + 0.0j, - 0.0 - 1.5j, - 0.5 + 0.0j, - -0.5 + 0.0j, - 0.0 - 3.0j, - 0.0 + 0.0j, - 0.5 - 0.5j, - 0.0 + 0.0j, - ], - [ - 0.0 - 1.5j, - -0.25 + 0.75j, - -0.5 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 1.5j, - 0.0 + 0.0j, - -0.25 + 0.25j, - ], - [ - 0.5 + 0.5j, - 0.0 + 0.0j, - 0.0 + 3.0j, - 0.0 + 0.0j, - 0.25 + 0.0j, - 0.25 + 0.0j, - 0.5 + 0.75j, - 0.0 - 0.75j, - ], - [ - 0.0 + 0.0j, - -0.25 - 0.25j, - 0.0 + 0.0j, - 0.0 - 1.5j, - 0.25 + 0.0j, - -0.25 + 0.0j, - 0.0 - 0.75j, - -0.25 + 0.0j, - ], - [ - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.5 + 0.5j, - 0.0 + 0.0j, - 0.5 - 0.75j, - 0.0 + 0.75j, - 0.25 + 0.0j, - 0.25 + 0.0j, - ], - [ - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - -0.25 - 0.25j, - 0.0 + 0.75j, - -0.25 + 0.0j, - 0.25 + 0.0j, - -0.25 + 0.0j, - ], - ] + [0.5, -0.5, 0.5, 1.5j, 0.5 - 0.5j, 0, 0, 0], + [-0.5, 0, 1.5j, -0.25 - 0.75j, 0, -0.25 + 0.25j, 0, 0], + [0.5, -1.5j, 0.5, -0.5, -3.0j, 0, 0.5 - 0.5j, 0], + [-1.5j, -0.25 + 0.75j, -0.5, 0, 0, 1.5j, 0, -0.25 + 0.25j], + [0.5 + 0.5j, 0, 3.0j, 0, 0.25, 0.25, 0.5 + 0.75j, -0.75j], + [0, -0.25 - 0.25j, 0, -1.5j, 0.25, -0.25, -0.75j, -0.25], + [0, 0, 0.5 + 0.5j, 0, 0.5 - 0.75j, 0.75j, 0.25, 0.25], + [0, 0, 0, -0.25 - 0.25j, 0.75j, -0.25, 0.25, -0.25], ] ) - result = shadow_state_reconstruction(measurement_outcomes, False) - num_qubits = len(measurement_outcomes[0]) - assert isinstance(result, np.ndarray) - assert result.shape == ( - 2**num_qubits, - 2**num_qubits, - ) - assert np.allclose(result, expected_result) + state = shadow_state_reconstruction(measurement_outcomes) + np.testing.assert_almost_equal(state, expected_state) def test_shadow_state_reconstruction_cal(): - b_lists = ["01", "01"] - u_lists = ["XY", "XY"] - measurement_outcomes = (b_lists, u_lists) - f_est = {"00": 1, "01": 1 / 3, "10": 1 / 3, "11": 1 / 9} - expected_result_vec = operator_ptm_vector_rep( + bitstrings, paulistrings = ["01", "01"], ["XY", "XY"] + measurement_outcomes = (bitstrings, paulistrings) + fidelities = {"00": 1, "01": 1 / 3, "10": 1 / 3, "11": 1 / 9} + + expected_state_vec = operator_ptm_vector_rep( np.array( [ - [0.25 + 0.0j, 0.0 + 0.75j, 0.75 + 0.0j, 0.0 + 2.25j], - [0.0 - 0.75j, 0.25 + 0.0j, 0.0 - 2.25j, 0.75 + 0.0j], - [0.75 + 0.0j, 0.0 + 2.25j, 0.25 + 0.0j, 0.0 + 0.75j], - [0.0 - 2.25j, 0.75 + 0.0j, 0.0 - 0.75j, 0.25 + 0.0j], + [0.25, 0.75j, 0.75, 2.25j], + [-0.75j, 0.25, -2.25j, 0.75], + [0.75, 2.25j, 0.25, 0.75j], + [-2.25j, 0.75, -0.75j, 0.25], ] ) ) - result = shadow_state_reconstruction(measurement_outcomes, f_est) - num_qubits = len(measurement_outcomes[0]) - assert isinstance(result, np.ndarray) - assert result.shape == (4**num_qubits,) - assert np.allclose(result, expected_result_vec) + state = shadow_state_reconstruction(measurement_outcomes, fidelities) + np.testing.assert_almost_equal(state, expected_state_vec) def test_expectation_estimation_shadow(): From 84f99539b54dd652c4534fcf9bc5c8f8a2d20ca2 Mon Sep 17 00:00:00 2001 From: nate stemen Date: Mon, 8 Jan 2024 15:19:12 -0800 Subject: [PATCH 15/22] better variable naming in `classical_snapshot` --- mitiq/shadows/classical_postprocessing.py | 54 +++++++++-------------- 1 file changed, 21 insertions(+), 33 deletions(-) diff --git a/mitiq/shadows/classical_postprocessing.py b/mitiq/shadows/classical_postprocessing.py index d7dfd95e2..da6108805 100644 --- a/mitiq/shadows/classical_postprocessing.py +++ b/mitiq/shadows/classical_postprocessing.py @@ -34,9 +34,8 @@ "Z": cirq.unitary(cirq.I), } -# Density matrices of single-qubit basis states -ZERO_STATE = np.diag([1.0 + 0.0j, 0.0 + 0.0j]) -ONE_STATE = np.diag([0.0 + 0.0j, 1.0 + 0.0j]) +ZERO_STATE = np.array([[1, 0], [0, 0]]) +ONE_STATE = np.array([[0, 0], [0, 1]]) def get_single_shot_pauli_fidelity( @@ -132,21 +131,17 @@ def get_pauli_fidelities( def classical_snapshot( - b_list_shadow: str, - u_list_shadow: str, - f_est: Optional[Dict[str, float]] = None, + bitstring: str, + paulistring: str, + fidelities: Optional[Dict[str, float]] = None, ) -> npt.NDArray[Any]: r""" Implement a single snapshot state reconstruction with calibration of the noisy quantum channel. Args: - b_list_shadow: The list of length 1, classical outcomes for the - snapshot. Here, - b = '0' corresponds to :math:`|0\rangle`, and - b = '1' corresponds to :math:`|1\rangle`. - u_list_shadow: list of len 1, contains str of ("XYZ..") for - the applied Pauli measurement on each qubit. + bitstring: A bitstring corresponding to the outcome ... TODO + paulistring: String of the applied Pauli measurement on each qubit. f_est: The estimated Pauli fidelities to use for calibration if available. @@ -156,41 +151,34 @@ def classical_snapshot( # calibrate the noisy quantum channel, output in PTM rep. # ptm rep of identity I_ptm = operator_ptm_vector_rep(np.eye(2) / np.sqrt(2)) - # define projections Pi_0 and Pi_1 pi_zero = np.outer(I_ptm, I_ptm) pi_one = np.eye(4) - pi_zero pi_zero = np.diag(pi_zero) pi_one = np.diag(pi_one) - if f_est: + if fidelities: elements = [] - # get b_list and f for each calibration measurement - for b_list_cal, f in f_est.items(): - pi_snapshot_vecter = [] - for b_1, b2, u2 in zip(b_list_cal, b_list_shadow, u_list_shadow): + for bits, fidelity in fidelities.items(): + pi_snapshot_vector = [] + for b1, b2, pauli in zip(bits, bitstring, paulistring): # get pi for each qubit based on calibration measurement - pi = pi_zero if b_1 == "0" else pi_one + pi = pi_zero if b1 == "0" else pi_one # get state for each qubit based on shadow measurement state = ZERO_STATE if b2 == "0" else ONE_STATE - # get U2 for each qubit based on shadow measurement - U2 = PAULI_MAP[u2] - pi_snapshot_vecter.append( - pi * operator_ptm_vector_rep(U2.conj().T @ state @ U2) + # get U for each qubit based on shadow measurement + U = PAULI_MAP[pauli] + pi_snapshot_vector.append( + pi * operator_ptm_vector_rep(U.conj().T @ state @ U) ) - # solve for the snapshot state elements.append( - 1 / f * matrix_kronecker_product(pi_snapshot_vecter) + 1 / fidelity * matrix_kronecker_product(pi_snapshot_vector) ) - rho_snapshot_vector = np.sum(elements, axis=0) - # normalize the snapshot state - rho_snapshot = rho_snapshot_vector # * normalize_factor - # w/o calibration, noted here, the output in terms of matrix, - # not in PTM rep. + rho_snapshot = np.sum(elements, axis=0) else: local_rhos = [] - for b, u in zip(b_list_shadow, u_list_shadow): - state = ZERO_STATE if b == "0" else ONE_STATE - U = PAULI_MAP[u] + for bit, pauli in zip(bitstring, paulistring): + state = ZERO_STATE if bit == "0" else ONE_STATE + U = PAULI_MAP[pauli] # apply inverse of the quantum channel,get PTM vector rep local_rho = 3.0 * (U.conj().T @ state @ U) - cirq.unitary(cirq.I) local_rhos.append(local_rho) From caf89715401d69395eec1bd8a8129c41bbad5b38 Mon Sep 17 00:00:00 2001 From: nate stemen Date: Mon, 8 Jan 2024 16:38:47 -0800 Subject: [PATCH 16/22] refactor `expectation_estimation_shadow` --- mitiq/shadows/classical_postprocessing.py | 73 +++++++------------ mitiq/shadows/shadows.py | 4 +- .../test/test_classical_postprocessing.py | 38 ++++------ 3 files changed, 43 insertions(+), 72 deletions(-) diff --git a/mitiq/shadows/classical_postprocessing.py b/mitiq/shadows/classical_postprocessing.py index da6108805..024471c1c 100644 --- a/mitiq/shadows/classical_postprocessing.py +++ b/mitiq/shadows/classical_postprocessing.py @@ -215,9 +215,9 @@ def shadow_state_reconstruction( def expectation_estimation_shadow( measurement_outcomes: Tuple[List[str], List[str]], - pauli_str: mitiq.PauliString, - k_shadows: int, - f_est: Optional[Dict[str, float]] = None, + pauli: mitiq.PauliString, + batch_size: int, + fidelities: Optional[Dict[str, float]] = None, ) -> float: """Calculate the expectation value of an observable from classical shadows. Use median of means to ameliorate the effects of outliers. @@ -227,62 +227,41 @@ def expectation_estimation_shadow( `z_basis_measurement`. pauli_str: Single mitiq observable consisting of Pauli operators. - k_shadows: number of splits in the median of means estimator. + batch_size: Size of batches to process measurement outcomes in. f_est: The estimated Pauli fidelities to use for calibration if available. Returns: - Float corresponding to the estimate of the observable - expectation value. + Float corresponding to the estimate of the observable expectation + value. """ - num_qubits = len(measurement_outcomes[0][0]) - obs = pauli_str._pauli - coeff = pauli_str.coeff - - target_obs, target_locs = [], [] - for qubit, pauli in obs.items(): - target_obs.append(str(pauli)) - target_locs.append(int(qubit)) - - # classical values stored in classical computer - b_lists_shadow = np.array([list(u) for u in measurement_outcomes[0]])[ - :, target_locs + bitstrings, paulistrings = measurement_outcomes + num_qubits = len(bitstrings[0]) + + qubits = sorted(pauli.support()) + filtered_bitstrings = [ + "".join([bitstring[q] for q in qubits]) for bitstring in bitstrings ] - u_lists_shadow = np.array([list(u) for u in measurement_outcomes[1]])[ - :, target_locs + filtered_paulis = [ + "".join([pauli[q] for q in qubits]) for pauli in paulistrings ] + filtered_data = (filtered_bitstrings, filtered_paulis) means = [] - - # loop over the splits of the shadow: - group_idxes = np.array_split(np.arange(len(b_lists_shadow)), k_shadows) - - # loop over the splits of the shadow: - for idxes in group_idxes: - matching_indexes = np.nonzero( - np.all(u_lists_shadow[idxes] == target_obs, axis=1) - ) - - if len(matching_indexes[0]): - product = (-1) ** np.sum( - b_lists_shadow[idxes][matching_indexes].astype(int), - axis=1, - ) - - if f_est: - b = create_string(num_qubits, target_locs) - f_val = f_est.get(b, np.inf) - # product becomes an array of snapshot expectation values - # witch satisfy condition (1) and (2) - product = (1 / f_val) * product + for bits, paulis in batch_calibration_data(filtered_data, batch_size): + matching_indices = [i for i, p in enumerate(paulis) if p == pauli.spec] + if matching_indices: + product = (-1) ** sum(bit.count("1") for bit in bits) + + if fidelities: + b = create_string(num_qubits, qubits) + product /= fidelities.get(b, np.inf) else: - product = 3 ** len(target_locs) * product + product *= 3 ** len(qubits) else: product = 0.0 - # append the mean of the product in each split - means.append(np.sum(product) / len(idxes)) + means.append(product / len(bits)) - # return the median of means - return float(np.real(np.median(means) * coeff)) + return np.real(np.median(means) * pauli.coeff) diff --git a/mitiq/shadows/shadows.py b/mitiq/shadows/shadows.py index d395b510c..4205b00b1 100644 --- a/mitiq/shadows/shadows.py +++ b/mitiq/shadows/shadows.py @@ -200,8 +200,8 @@ def classical_post_processing( expectation_values = expectation_estimation_shadow( shadow_outcomes, obs, - k_shadows=k_shadows, - f_est=calibration_results, + batch_size=k_shadows, + fidelities=calibration_results, ) output[str(obs)] = expectation_values return output diff --git a/mitiq/shadows/test/test_classical_postprocessing.py b/mitiq/shadows/test/test_classical_postprocessing.py index a79e7c073..348213e5f 100644 --- a/mitiq/shadows/test/test_classical_postprocessing.py +++ b/mitiq/shadows/test/test_classical_postprocessing.py @@ -5,7 +5,6 @@ """Tests for classical post-processing functions for classical shadows.""" -import cirq import numpy as np import mitiq @@ -177,25 +176,21 @@ def test_shadow_state_reconstruction_cal(): def test_expectation_estimation_shadow(): - b_lists = ["0101", "0110"] - u_lists = ["ZZXX", "ZZXX"] - - measurement_outcomes = (b_lists, u_lists) - observable = mitiq.PauliString("ZZ", support=(0, 1)) - k = 1 + measurement_outcomes = ["0101", "0110"], ["ZZXX", "ZZXX"] + pauli = mitiq.PauliString("ZZ") + batch_size = 1 expected_result = -9 result = expectation_estimation_shadow( - measurement_outcomes, observable, k, False + measurement_outcomes, pauli, batch_size ) - assert isinstance(result, float), f"Expected a float, got {type(result)}" assert np.isclose(result, expected_result) def test_expectation_estimation_shadow_cal(): - b_lists = ["0101", "0110"] - u_lists = ["YXZZ", "XXXX"] - f_est = { + bitstrings = ["0101", "0110"] + paulistrings = ["YXZZ", "XXXX"] + fidelities = { "0000": 1, "0001": 1 / 3, "0010": 1 / 3, @@ -214,16 +209,14 @@ def test_expectation_estimation_shadow_cal(): "1111": 1 / 81, } - measurement_outcomes = b_lists, u_lists - observable = mitiq.PauliString("YXZZ", support=(0, 1, 2, 3)) - k = 1 + measurement_outcomes = bitstrings, paulistrings + pauli = mitiq.PauliString("YXZZ") + batch_size = 1 expected_result = 81 / 2 - print("expected_result", expected_result) result = expectation_estimation_shadow( - measurement_outcomes, observable, k, f_est + measurement_outcomes, pauli, batch_size, fidelities ) - assert isinstance(result, float), f"Expected a float, got {type(result)}" assert np.isclose(result, expected_result) @@ -232,13 +225,12 @@ def test_expectation_estimation_shadow_no_indices(): Test expectation estimation for a shadow with no matching indices. The result should be 0 as there are no matching """ - q0, q1, q2 = cirq.LineQubit.range(3) - observable = mitiq.PauliString("XYZ", support=(0, 1, 2)) + pauli = mitiq.PauliString("XYZ") measurement_outcomes = ["101", "010", "101"], ["ZXY", "YZX", "ZZY"] - k_shadows = 1 + batch_size = 1 result = expectation_estimation_shadow( - measurement_outcomes, observable, k_shadows, False + measurement_outcomes, pauli, batch_size ) - assert result == 0.0 + assert result == 0 From 87fd9bca9a268970ef0d8ebe7bce3544321fc1d0 Mon Sep 17 00:00:00 2001 From: nate stemen Date: Mon, 8 Jan 2024 20:55:38 -0800 Subject: [PATCH 17/22] remove calibration option from docs; add fidelity back --- docs/source/examples/rshadows_tutorial.md | 2 -- docs/source/examples/shadows_tutorial.md | 3 --- mitiq/shadows/shadows_utils.py | 28 +++++++++++++++++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/docs/source/examples/rshadows_tutorial.md b/docs/source/examples/rshadows_tutorial.md index 246fd5174..20476f173 100644 --- a/docs/source/examples/rshadows_tutorial.md +++ b/docs/source/examples/rshadows_tutorial.md @@ -329,14 +329,12 @@ def compare_shadow_methods( output_shadow = classical_post_processing( shadow_outcomes=shadow_measurement_result, - use_calibration=False, observables=observables, k_shadows=k_shadows, ) output_shadow_cal = classical_post_processing( shadow_outcomes=shadow_measurement_result, - use_calibration=True, calibration_results=f_est, observables=observables, k_shadows=k_shadows, diff --git a/docs/source/examples/shadows_tutorial.md b/docs/source/examples/shadows_tutorial.md index 075661e12..f6798371d 100644 --- a/docs/source/examples/shadows_tutorial.md +++ b/docs/source/examples/shadows_tutorial.md @@ -179,7 +179,6 @@ shadow_outcomes = shadow_quantum_processing( # get shadow reconstruction of the density matrix output = classical_post_processing( shadow_outcomes, - use_calibration=False, state_reconstruction=True, ) rho_shadow = output["reconstructed_state"] @@ -286,7 +285,6 @@ for n_measurement in n_measurement_list: # perform shadow state reconstruction rho_shadow = classical_post_processing( shadow_outcomes=shadow_subset, - use_calibration=False, state_reconstruction=True, )["reconstructed_state"] @@ -474,7 +472,6 @@ for error in epsilon_grid: shadow_outputs = shadow_quantum_processing(test_circuits, cirq_executor, r) output = classical_post_processing( shadow_outcomes=shadow_outputs, - use_calibration=False, observables=list_of_paulistrings, k_shadows=k, ) diff --git a/mitiq/shadows/shadows_utils.py b/mitiq/shadows/shadows_utils.py index 47bbff7cb..e278f8001 100644 --- a/mitiq/shadows/shadows_utils.py +++ b/mitiq/shadows/shadows_utils.py @@ -12,6 +12,8 @@ from typing import Generator, List, Optional, Tuple import numpy as np +import numpy.typing as npt +from scipy.linalg import sqrtm import mitiq @@ -69,6 +71,32 @@ def valid_bitstrings( return bitstrings +def fidelity( + sigma: npt.NDArray[np.complex64], rho: npt.NDArray[np.complex64] +) -> float: + """ + Calculate the fidelity between two states. + + Args: + sigma: A state in terms of square matrix or vector. + rho: A state in terms square matrix or vector. + + Returns: + Scalar corresponding to the fidelity. + """ + if sigma.ndim == 1 and rho.ndim == 1: + val = np.abs(np.dot(sigma.conj(), rho)) ** 2.0 + elif sigma.ndim == 1 and rho.ndim == 2: + val = np.abs(sigma.conj().T @ rho @ sigma) + elif sigma.ndim == 2 and rho.ndim == 1: + val = np.abs(rho.conj().T @ sigma @ rho) + elif sigma.ndim == 2 and rho.ndim == 2: + val = np.abs(np.trace(sqrtm(sigma) @ rho @ sqrtm(sigma))) + else: + raise ValueError("Invalid input dimensions") + return float(val) + + def batch_calibration_data( data: Tuple[List[str], List[str]], batch_size: int ) -> Generator[Tuple[List[str], List[str]], None, None]: From b75704317a888ac89fbebce37c461094c5d920f9 Mon Sep 17 00:00:00 2001 From: nate stemen Date: Tue, 9 Jan 2024 07:52:01 -0800 Subject: [PATCH 18/22] fix precedence --- mitiq/shadows/shadows_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mitiq/shadows/shadows_utils.py b/mitiq/shadows/shadows_utils.py index e278f8001..7a81b3ec1 100644 --- a/mitiq/shadows/shadows_utils.py +++ b/mitiq/shadows/shadows_utils.py @@ -66,7 +66,7 @@ def valid_bitstrings( bitstrings = { bin(i)[2:].zfill(num_qubits) for i in range(2**num_qubits) - if bin(i).count("1") <= max_hamming_weight or num_qubits + if bin(i).count("1") <= (max_hamming_weight or num_qubits) } return bitstrings From 88658d5be190ae367f973e9e6e7dcef8f235881a Mon Sep 17 00:00:00 2001 From: nate stemen Date: Wed, 10 Jan 2024 16:16:11 -0800 Subject: [PATCH 19/22] remove `print` statements --- mitiq/shadows/test/test_classical_postprocessing.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mitiq/shadows/test/test_classical_postprocessing.py b/mitiq/shadows/test/test_classical_postprocessing.py index 348213e5f..46d66d277 100644 --- a/mitiq/shadows/test/test_classical_postprocessing.py +++ b/mitiq/shadows/test/test_classical_postprocessing.py @@ -25,7 +25,6 @@ def test_get_single_shot_pauli_fidelity(): assert get_single_shot_pauli_fidelity(b_list, u_list) == expected_result b_list = "01101" u_list = "XYZYZ" - print(get_single_shot_pauli_fidelity(b_list, u_list)) assert get_single_shot_pauli_fidelity(b_list, u_list) == { "00000": 1.0, "10000": 0.0, @@ -65,7 +64,6 @@ def test_get_single_shot_pauli_fidelity(): def test_get_single_shot_pauli_fidelity_with_locality(): b_list = "11101" u_list = "XYZYZ" - print(get_single_shot_pauli_fidelity(b_list, u_list, locality=2)) assert get_single_shot_pauli_fidelity(b_list, u_list, locality=2) == { "00000": 1.0, "10000": 0.0, From 23b4e1e80763fdda1cf47d7cfec6fa5e6ad3239d Mon Sep 17 00:00:00 2001 From: nate stemen Date: Wed, 10 Jan 2024 16:18:52 -0800 Subject: [PATCH 20/22] compute batch sizes properly; move `-1` into sum i misunderstood how `np.array_split` worked. it uses the number of splits, as opposed to the size of each batch. (-1)^x + (-1)^y =/= (-1)^(x + y) --- mitiq/shadows/classical_postprocessing.py | 17 +++++++++-------- mitiq/shadows/shadows.py | 2 +- mitiq/shadows/shadows_utils.py | 3 ++- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/mitiq/shadows/classical_postprocessing.py b/mitiq/shadows/classical_postprocessing.py index 024471c1c..64b9ace4a 100644 --- a/mitiq/shadows/classical_postprocessing.py +++ b/mitiq/shadows/classical_postprocessing.py @@ -88,7 +88,7 @@ def get_single_shot_pauli_fidelity( def get_pauli_fidelities( calibration_outcomes: Tuple[List[str], List[str]], - batch_size: int, + num_batches: int, locality: Optional[int] = None, ) -> Dict[str, complex]: r""" @@ -100,7 +100,7 @@ def get_pauli_fidelities( Args: calibration_measurement_outcomes: The `random_Pauli_measurement` outcomes for the state :math:`|0\rangle^{\otimes n}`}` . - k_calibration: number of splits in the median of means estimator. + num_batches: The number of batches in the median of means estimator. locality: The locality of the operator, whose expectation value is going to be estimated by the classical shadow. E.g., if the operator is the Ising model Hamiltonian with nearest neighbor @@ -112,7 +112,7 @@ def get_pauli_fidelities( """ means = defaultdict(list) for bitstrings, paulistrings in batch_calibration_data( - calibration_outcomes, batch_size + calibration_outcomes, num_batches ): all_fidelities = defaultdict(list) for bitstring, paulistring in zip(bitstrings, paulistrings): @@ -123,7 +123,7 @@ def get_pauli_fidelities( all_fidelities[b].append(f) for bitstring, fids in all_fidelities.items(): - means[bitstring].append(sum(fids) / batch_size) + means[bitstring].append(sum(fids) / num_batches) return { bitstring: median(averages) for bitstring, averages in means.items() @@ -216,7 +216,7 @@ def shadow_state_reconstruction( def expectation_estimation_shadow( measurement_outcomes: Tuple[List[str], List[str]], pauli: mitiq.PauliString, - batch_size: int, + num_batches: int, fidelities: Optional[Dict[str, float]] = None, ) -> float: """Calculate the expectation value of an observable from classical shadows. @@ -227,7 +227,7 @@ def expectation_estimation_shadow( `z_basis_measurement`. pauli_str: Single mitiq observable consisting of Pauli operators. - batch_size: Size of batches to process measurement outcomes in. + num_batches: Number of batches to process measurement outcomes in. f_est: The estimated Pauli fidelities to use for calibration if available. @@ -248,10 +248,11 @@ def expectation_estimation_shadow( filtered_data = (filtered_bitstrings, filtered_paulis) means = [] - for bits, paulis in batch_calibration_data(filtered_data, batch_size): + for bits, paulis in batch_calibration_data(filtered_data, num_batches): matching_indices = [i for i, p in enumerate(paulis) if p == pauli.spec] if matching_indices: - product = (-1) ** sum(bit.count("1") for bit in bits) + matching_bits = (bits[i] for i in matching_indices) + product = sum((-1) ** bit.count("1") for bit in matching_bits) if fidelities: b = create_string(num_qubits, qubits) diff --git a/mitiq/shadows/shadows.py b/mitiq/shadows/shadows.py index 4205b00b1..9d33779bf 100644 --- a/mitiq/shadows/shadows.py +++ b/mitiq/shadows/shadows.py @@ -200,7 +200,7 @@ def classical_post_processing( expectation_values = expectation_estimation_shadow( shadow_outcomes, obs, - batch_size=k_shadows, + num_batches=k_shadows, fidelities=calibration_results, ) output[str(obs)] = expectation_values diff --git a/mitiq/shadows/shadows_utils.py b/mitiq/shadows/shadows_utils.py index 7a81b3ec1..8600db683 100644 --- a/mitiq/shadows/shadows_utils.py +++ b/mitiq/shadows/shadows_utils.py @@ -98,7 +98,7 @@ def fidelity( def batch_calibration_data( - data: Tuple[List[str], List[str]], batch_size: int + data: Tuple[List[str], List[str]], num_batches: int ) -> Generator[Tuple[List[str], List[str]], None, None]: """Batch calibration into chunks of size batch_size. @@ -110,6 +110,7 @@ def batch_calibration_data( Tuples of bit strings and pauli strings. """ bits, paulis = data + batch_size = len(bits) // num_batches for i in range(0, len(bits), batch_size): yield bits[i : i + batch_size], paulis[i : i + batch_size] From 4cc124eb218228eb05573b30ed184d62c84ffbef Mon Sep 17 00:00:00 2001 From: nate stemen Date: Wed, 10 Jan 2024 17:48:40 -0800 Subject: [PATCH 21/22] remove `use_calibration` option in docs --- docs/source/examples/rshadows_tutorial.md | 2 +- docs/source/guide/shadows-1-intro.md | 74 +++++++++++------------ 2 files changed, 35 insertions(+), 41 deletions(-) diff --git a/docs/source/examples/rshadows_tutorial.md b/docs/source/examples/rshadows_tutorial.md index 20476f173..b8c8ec630 100644 --- a/docs/source/examples/rshadows_tutorial.md +++ b/docs/source/examples/rshadows_tutorial.md @@ -249,7 +249,7 @@ plt.xticks( ) plt.ylabel("Pauli fidelity") -plt.legend() +plt.legend(); ``` diff --git a/docs/source/guide/shadows-1-intro.md b/docs/source/guide/shadows-1-intro.md index 8e1daf36a..83637e057 100644 --- a/docs/source/guide/shadows-1-intro.md +++ b/docs/source/guide/shadows-1-intro.md @@ -10,43 +10,45 @@ kernelspec: language: python name: python3 --- + ```{admonition} Note: -The documentation for Classical Shadows in Mitiq is still under construction. This users guide will change in the future. +The documentation for Classical Shadows in Mitiq is still under construction. This users guide will change in the future. ``` # How Do I Use Classical Shadows Estimation? - The `mitiq.shadows` module facilitates the application of the classical shadows protocol on quantum circuits, designed for tasks like quantum state tomography or expectation value estimation. In addition this module integrates a robust shadow estimation protocol that's tailored to counteract noise. The primary objective of the classical shadow protocol is to extract information from a quantum state using repeated measurements. The procedure can be broken down as follows: -1. `shadow_quantum_processing`: +1. `shadow_quantum_processing`: + - Purpose: Execute quantum processing on the provided quantum circuit. - Outcome: Measurement results from the processed circuit. -2. `classical_post_processing`: +2. `classical_post_processing`: - Purpose: Handle classical processing of the measurement results. - Outcome: Estimation based on user-defined inputs. For users aiming to employ the robust shadow estimation protocol, an initial step is needed which entails characterizing the noisy quantum channel. This is done by: 0. `pauli_twirling_calibration` + - Purpose: Characterize the noisy quantum channel. - Outcome: A dictionary of `calibration_results`. 1. `shadow_quantum_processing`: same as above. 2. `classical_post_processing` - - Args: `use_calibration` = True, - `calibration_results` = output of `pauli_twirling_calibration` + - Args: `calibration_results` = output of `pauli_twirling_calibration` - Outcome: Error mitigated estimation based on user-defined inputs. Notes: - - The calibration process is specifically designed to mitigate noise encountered during the classical shadow protocol, such as rotation and computational basis measurements. It does not address noise that occurs during state preparation. - - Do not need to redo the calibration stage (0. `pauli_twirling_calibration`) if: - 1. The input circuit has a consistent number of qubits. - 2. The estimated observables have the same or fewer qubit support. + +- The calibration process is specifically designed to mitigate noise encountered during the classical shadow protocol, such as rotation and computational basis measurements. It does not address noise that occurs during state preparation. +- Do not need to redo the calibration stage (0. `pauli_twirling_calibration`) if: + 1. The input circuit has a consistent number of qubits. + 2. The estimated observables have the same or fewer qubit support. ## Protocol Overview @@ -54,8 +56,9 @@ The classical shadow protocol aims to create an approximate classical representa One can use the `mitiq.shadows' module as follows. -### User-defined inputs -Define a quantum circuit, e.g., a circuit which prepares a GHZ state with $n$ = `3` qubits, +### User-defined inputs + +Define a quantum circuit, e.g., a circuit which prepares a GHZ state with $n$ = `3` qubits, ```{code-cell} ipython3 import numpy as np @@ -76,13 +79,12 @@ circuit = cirq.Circuit( print(circuit) ``` -Define an executor to run the circuit on a quantum computer or a noisy simulator. Note that the *robust shadow estimation* technique can only calibrate and mitigate the noise acting on the operations associated to the classical shadow protocol. So, in order to test the technique, we assume that the state preparation part of the circuit is noiseless. In particular, we define an executor in which: +Define an executor to run the circuit on a quantum computer or a noisy simulator. Note that the _robust shadow estimation_ technique can only calibrate and mitigate the noise acting on the operations associated to the classical shadow protocol. So, in order to test the technique, we assume that the state preparation part of the circuit is noiseless. In particular, we define an executor in which: 1. A noise channel is added to circuit right before the measurements. I.e. $U_{\Lambda_U}(M_z)_{\Lambda_{\mathcal{M}_Z}}\equiv U\Lambda\mathcal{M}_Z$. 2. A single measurement shot is taken for each circuit, as required by classical shadow protocol. - ```{code-cell} ipython3 from mitiq import MeasurementResult @@ -125,8 +127,7 @@ def cirq_executor( return executor ``` -Given the above general executor, we define a specific example of a noisy executor, assuming a bit flip channel with a probability of `0.1' - +Given the above general executor, we define a specific example of a noisy executor, assuming a bit flip channel with a probability of `0.1' ```{code-cell} ipython3 from functools import partial @@ -164,24 +165,28 @@ f_est the varible `locality` is the maximum number of qubits on which our operators of interest are acting on. E.g. if our operator is a sequence of two point correlation terms $\{\langle Z_iZ_{i+1}\rangle\}_{0\leq i\leq n-1}$, then `locality` = 2. We note that one could also split the calibration process into two stages: -01. `shadow_quantum_processing` - - Outcome: Get quantum measurement result of the calibration circuit $|0\rangle^{\otimes n}$ `zero_state_shadow_outcomes`. +1. `shadow_quantum_processing` -02. `pauli_twirling_calibration` - - Outcome: A dictionary of `calibration_results`. -For more details, please refer to [this tutorial](../examples/rshadows_tutorial.md) +- Outcome: Get quantum measurement result of the calibration circuit $|0\rangle^{\otimes n}$ `zero_state_shadow_outcomes`. + +2. `pauli_twirling_calibration` + +- Outcome: A dictionary of `calibration_results`. + For more details, please refer to [this tutorial](../examples/rshadows_tutorial.md) ### 1. Quantum Processing + In this step, we obtain classical shadow snapshots from the input state (before applying the invert channel). #### 1.1 Add Rotation Gate and Meausure the Rotated State in Computational Basis + At present, the implementation supports random Pauli measurement. This is equivalent to randomly sampling $U$ from the local Clifford group $Cl_2^n$, followed by a $Z$-basis measurement (see [this tutorial](../examples/shadows_tutorial.md) for a clear explanation). #### 1.2 Get the Classical Shadows -One can obtain the list of measurement results of local Pauli measurements in terms of bitstrings, and the related Pauli-basis measured in terms of strings as follows. +One can obtain the list of measurement results of local Pauli measurements in terms of bitstrings, and the related Pauli-basis measured in terms of strings as follows. -You have two choices: run the quantum measurement or directly use the results from the previous run. +You have two choices: run the quantum measurement or directly use the results from the previous run. - If **True**, the measurement will be run again. - If **False**, the results from the previous run will be used. @@ -206,20 +211,20 @@ else: num_total_measurements_shadow=5000, ) ``` - As an example, we print out one of those measurement outcomes and the associated measured operator: - ```{code-cell} ipython3 print("one snapshot measurement result = ", shadow_measurement_output[0][0]) print("one snapshot measurement basis = ", shadow_measurement_output[1][0]) ``` ### 2. Classical Post-Processing -In this step, we estimate our object of interest (expectation value or density matrix) by post-processing the (previously obtained) measurement outcomes. + +In this step, we estimate our object of interest (expectation value or density matrix) by post-processing the (previously obtained) measurement outcomes. #### 2.1 Example: Operator Expectation Value Esitimation + For example, if we want to estimate the two point correlation function $\{\langle Z_iZ_{i+1}\rangle\}_{0\leq i\leq n-1}$, we will define the corresponding Puali strings: ```{code-cell} ipython3 @@ -238,13 +243,11 @@ The corresponding expectation values can be estimated (with and without calibrat ```{code-cell} ipython3 est_corrs = classical_post_processing( shadow_outcomes=shadow_measurement_output, - use_calibration=False, observables=two_pt_correlations, k_shadows=1, ) cal_est_corrs = classical_post_processing( shadow_outcomes=shadow_measurement_output, - use_calibration=True, calibration_results=f_est, observables=two_pt_correlations, k_shadows=1, @@ -253,7 +256,6 @@ cal_est_corrs = classical_post_processing( Let's compare the results with the exact theoretical values: - ```{code-cell} ipython3 expval_exact = [] state_vector = circuit.final_state_vector() @@ -264,7 +266,6 @@ for i, pauli_string in enumerate(two_pt_correlations): expval_exact.append(exp.real) ``` - ```{code-cell} ipython3 print("Classical shadow estimation:", est_corrs) print("Robust shadow estimation :", cal_est_corrs) @@ -277,12 +278,10 @@ print( ) ``` - #### 2.2 Example: GHZ State Reconstruction -In addition to the estimation of expectation values, the `mitiq.shadow` module can also be used to reconstruct an approximated version of the density matrix. -As an example, we use the 3-qubit GHZ circuit, previously defined. As a first step, we calculate the Pauli fidelities $f_b$ characterizing the noisy quantum channel $\mathcal{M}=\sum_{b\in\{0,1\}^n}f_b\Pi_b$: - +In addition to the estimation of expectation values, the `mitiq.shadow` module can also be used to reconstruct an approximated version of the density matrix. +As an example, we use the 3-qubit GHZ circuit, previously defined. As a first step, we calculate the Pauli fidelities $f_b$ characterizing the noisy quantum channel $\mathcal{M}=\sum_{b\in\{0,1\}^n}f_b\Pi_b$: ```{code-cell} ipython3 noisy_executor = partial( @@ -307,9 +306,7 @@ else: f_est ``` - -Similar to the previous case (estimation of expectation values), the quantum processing for estimating the density matrix is done as follows. - +Similar to the previous case (estimation of expectation values), the quantum processing for estimating the density matrix is done as follows. ```{code-cell} ipython3 if not run_quantum_processing: @@ -328,12 +325,10 @@ else: ```{code-cell} ipython3 est_corrs = classical_post_processing( shadow_outcomes=shadow_measurement_output, - use_calibration=False, state_reconstruction=True, ) cal_est_corrs = classical_post_processing( shadow_outcomes=shadow_measurement_output, - use_calibration=True, calibration_results=f_est, state_reconstruction=True, ) @@ -349,7 +344,6 @@ ghz_true = ghz_state @ ghz_state.conj().T ptm_ghz_state = operator_ptm_vector_rep(ghz_true) ``` - ```{code-cell} ipython3 from mitiq.shadows.shadows_utils import fidelity From dd61f23a9fd8c00fba7c92f8454cb6c87dfbe74c Mon Sep 17 00:00:00 2001 From: nate stemen Date: Wed, 10 Jan 2024 20:13:00 -0800 Subject: [PATCH 22/22] add tests for added util functions --- mitiq/shadows/test/test_shadows_utils.py | 54 ++++++++++++++++++------ 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/mitiq/shadows/test/test_shadows_utils.py b/mitiq/shadows/test/test_shadows_utils.py index 18b5f4be0..b08e19fa0 100644 --- a/mitiq/shadows/test/test_shadows_utils.py +++ b/mitiq/shadows/test/test_shadows_utils.py @@ -3,13 +3,18 @@ # This source code is licensed under the GPL license (v3) found in the # LICENSE file in the root directory of this source tree. -"""Defines utility functions for classical shadows protocol.""" +import math + +import numpy as np import mitiq from mitiq.shadows.shadows_utils import ( + batch_calibration_data, create_string, + fidelity, n_measurements_opts_expectation_bound, n_measurements_tomography_bound, + valid_bitstrings, ) @@ -19,16 +24,33 @@ def test_create_string(): assert create_string(str_len, loc_list) == "01010" +def test_valid_bitstrings(): + num_qubits = 5 + bitstrings_on_5_qubits = valid_bitstrings(num_qubits) + assert len(bitstrings_on_5_qubits) == 2**num_qubits + assert all(b == "0" or b == "1" for b in bitstrings_on_5_qubits.pop()) + + num_qubits = 4 + max_hamming_weight = 2 + bitstrings_on_3_qubits_hamming_2 = valid_bitstrings( + num_qubits, max_hamming_weight + ) + assert len(bitstrings_on_3_qubits_hamming_2) == sum( + math.comb(num_qubits, i) for i in range(max_hamming_weight + 1) + ) # sum_{i == 0}^{max_hamming_weight} (num_qubits choose i) + + +def test_batch_calibration_data(): + data = (["010", "110", "000", "001"], ["XXY", "ZYY", "ZZZ", "XYZ"]) + num_batches = 2 + for bits, paulis in batch_calibration_data(data, num_batches): + assert len(bits) == len(paulis) == num_batches + + def test_n_measurements_tomography_bound(): - assert ( - n_measurements_tomography_bound(0.5, 2) == 2176 - ), f"Expected 2176, got {n_measurements_tomography_bound(0.5, 2)}" - assert ( - n_measurements_tomography_bound(1.0, 1) == 136 - ), f"Expected 136, got {n_measurements_tomography_bound(1.0, 1)}" - assert ( - n_measurements_tomography_bound(0.1, 3) == 217599 - ), f"Expected 217599, got {n_measurements_tomography_bound(0.1, 3)}" + assert n_measurements_tomography_bound(0.5, 2) == 2176 + assert n_measurements_tomography_bound(1.0, 1) == 136 + assert n_measurements_tomography_bound(0.1, 3) == 217599 def test_n_measurements_opts_expectation_bound(): @@ -38,5 +60,13 @@ def test_n_measurements_opts_expectation_bound(): mitiq.PauliString("Z"), ] N, K = n_measurements_opts_expectation_bound(0.5, observables, 0.1) - assert isinstance(N, int), f"Expected int, got {type(N)}" - assert isinstance(K, int), f"Expected int, got {type(K)}" + assert isinstance(N, int) + assert isinstance(K, int) + + +def test_fidelity(): + state_vector = np.array([0.5, 0.5, 0.5, 0.5]) + rho = np.eye(4) / 4 + assert np.isclose( + fidelity(state_vector, rho), 0.25 + ), f"Expected 0.25, got {fidelity(state_vector, rho)}"