diff --git a/qiskit/quantum_info/states/stabilizerstate.py b/qiskit/quantum_info/states/stabilizerstate.py index 7f616bcff79f..4ae16c32bf54 100644 --- a/qiskit/quantum_info/states/stabilizerstate.py +++ b/qiskit/quantum_info/states/stabilizerstate.py @@ -386,8 +386,17 @@ def probabilities(self, qargs: None | list = None, decimals: None | int = None) return probs - def probabilities_dict(self, qargs: None | list = None, decimals: None | int = None) -> dict: - """Return the subsystem measurement probability dictionary. + def probabilities_dict_from_bitstring( + self, + outcome_bitstring: str, + qargs: None | list = None, + decimals: None | int = None, + ) -> dict[str, float]: + """Return the subsystem measurement probability dictionary utilizing + a targeted outcome_bitstring to perform the measurement for. This + will calculate a probability for only a single targeted + outcome_bitstring value, giving a performance boost over calculating + all possible outcomes. Measurement probabilities are with respect to measurement in the computation (diagonal) basis. @@ -398,30 +407,44 @@ def probabilities_dict(self, qargs: None | list = None, decimals: None | int = N inserted between integers so that subsystems can be distinguished. Args: + outcome_bitstring (None or str): targeted outcome bitstring + to perform a measurement calculation for, this will significantly + reduce the number of calculation performed (Default: None) qargs (None or list): subsystems to return probabilities for, - if None return for all subsystems (Default: None). + if None return for all subsystems (Default: None). decimals (None or int): the number of decimal places to round - values. If None no rounding is done (Default: None). + values. If None no rounding is done (Default: None) Returns: - dict: The measurement probabilities in dict (ket) form. + dict[str, float]: The measurement probabilities in dict (ket) form. """ - if qargs is None: - qubits = range(self.clifford.num_qubits) - else: - qubits = qargs + return self._get_probabilities_dict( + outcome_bitstring=outcome_bitstring, qargs=qargs, decimals=decimals + ) - outcome = ["X"] * len(qubits) - outcome_prob = 1.0 - probs = {} # probabilities dictionary + def probabilities_dict( + self, qargs: None | list = None, decimals: None | int = None + ) -> dict[str, float]: + """Return the subsystem measurement probability dictionary. - self._get_probabilities(qubits, outcome, outcome_prob, probs) + Measurement probabilities are with respect to measurement in the + computation (diagonal) basis. - if decimals is not None: - for key, value in probs.items(): - probs[key] = round(value, decimals) + This dictionary representation uses a Ket-like notation where the + dictionary keys are qudit strings for the subsystem basis vectors. + If any subsystem has a dimension greater than 10 comma delimiters are + inserted between integers so that subsystems can be distinguished. - return probs + Args: + qargs (None or list): subsystems to return probabilities for, + if None return for all subsystems (Default: None). + decimals (None or int): the number of decimal places to round + values. If None no rounding is done (Default: None). + + Returns: + dict: The measurement probabilities in dict (key) form. + """ + return self._get_probabilities_dict(outcome_bitstring=None, qargs=qargs, decimals=decimals) def reset(self, qargs: list | None = None) -> StabilizerState: """Reset state or subsystems to the 0-state. @@ -644,22 +667,48 @@ def _rowsum_deterministic(clifford, aux_pauli, row): # ----------------------------------------------------------------------- # Helper functions for calculating the probabilities # ----------------------------------------------------------------------- - def _get_probabilities(self, qubits, outcome, outcome_prob, probs): - """Recursive helper function for calculating the probabilities""" + def _get_probabilities( + self, + qubits: range, + outcome: list[str], + outcome_prob: float, + probs: dict[str, float], + outcome_bitstring: str = None, + ): + """Recursive helper function for calculating the probabilities - qubit_for_branching = -1 - ret = self.copy() + Args: + qubits (range): range of qubits + outcome (list[str]): outcome being built + outcome_prob (float): probabilitiy of the outcome + probs (dict[str, float]): holds the outcomes and probabilitiy results + outcome_bitstring (str): target outcome to measure which reduces measurements, None + if not targeting a specific target + """ + qubit_for_branching: int = -1 + ret: StabilizerState = self.copy() + + # Find outcomes for each qubit for i in range(len(qubits)): - qubit = qubits[len(qubits) - i - 1] if outcome[i] == "X": - is_deterministic = not any(ret.clifford.stab_x[:, qubit]) - if is_deterministic: - single_qubit_outcome = ret._measure_and_update(qubit, 0) - if single_qubit_outcome: - outcome[i] = "1" + # Retrieve the qubit for the current measurement + qubit = qubits[(len(qubits) - i - 1)] + # Determine if the probabilitiy is deterministic + if not any(ret.clifford.stab_x[:, qubit]): + single_qubit_outcome: np.int64 = ret._measure_and_update(qubit, 0) + if outcome_bitstring is None or ( + int(outcome_bitstring[i]) == single_qubit_outcome + ): + # No outcome_bitstring target, or using outcome_bitstring target and + # the single_qubit_outcome equals the desired outcome_bitstring target value, + # then use current outcome_prob value + outcome[i] = str(single_qubit_outcome) else: - outcome[i] = "0" + # If the single_qubit_outcome does not equal the outcome_bitsring target + # then we know that the probability will be 0 + outcome[i] = str(outcome_bitstring[i]) + outcome_prob = 0 else: qubit_for_branching = i @@ -668,15 +717,57 @@ def _get_probabilities(self, qubits, outcome, outcome_prob, probs): probs[str_outcome] = outcome_prob return - for single_qubit_outcome in range(0, 2): + for single_qubit_outcome in ( + range(0, 2) + if (outcome_bitstring is None) + else [int(outcome_bitstring[qubit_for_branching])] + ): new_outcome = outcome.copy() - if single_qubit_outcome: - new_outcome[qubit_for_branching] = "1" - else: - new_outcome[qubit_for_branching] = "0" + new_outcome[qubit_for_branching] = str(single_qubit_outcome) stab_cpy = ret.copy() stab_cpy._measure_and_update( - qubits[len(qubits) - qubit_for_branching - 1], single_qubit_outcome + qubits[(len(qubits) - qubit_for_branching - 1)], single_qubit_outcome + ) + stab_cpy._get_probabilities( + qubits, new_outcome, (0.5 * outcome_prob), probs, outcome_bitstring ) - stab_cpy._get_probabilities(qubits, new_outcome, 0.5 * outcome_prob, probs) + + def _get_probabilities_dict( + self, + outcome_bitstring: None | str = None, + qargs: None | list = None, + decimals: None | int = None, + ) -> dict[str, float]: + """Helper Function for calculating the subsystem measurement probability dictionary. + When the targeted outcome_bitstring value is set, then only the single outcome_bitstring + probability will be calculated. + + Args: + outcome_bitstring (None or str): targeted outcome bitstring + to perform a measurement calculation for, this will significantly + reduce the number of calculation performed (Default: None) + qargs (None or list): subsystems to return probabilities for, + if None return for all subsystems (Default: None). + decimals (None or int): the number of decimal places to round + values. If None no rounding is done (Default: None). + + Returns: + dict: The measurement probabilities in dict (key) form. + """ + if qargs is None: + qubits = range(self.clifford.num_qubits) + else: + qubits = qargs + + outcome = ["X"] * len(qubits) + outcome_prob = 1.0 + probs: dict[str, float] = {} # Probabilities dict to return with the measured values + + self._get_probabilities(qubits, outcome, outcome_prob, probs, outcome_bitstring) + + if decimals is not None: + for key, value in probs.items(): + probs[key] = round(value, decimals) + + return probs diff --git a/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml b/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml new file mode 100644 index 000000000000..73da8e6b7ad3 --- /dev/null +++ b/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + The :class:'.StabilizerState' class now has a new method + :meth:'~.StabilizerState.probabilities_dict_from_bitstring' allowing the + user to pass single bitstring to measure an outcome for. Previouslly the + :meth:'~.StabilizerState.probabilities_dict' would be utilized and would + at worst case calculate (2^n) number of probabilbity calculations (depending + on the state), even if a user wanted a single result. With this new method + the user can calculate just the single outcome bitstring value a user passes + to measure the probability for. As the number of qubits increases, the more + prevelant the performance enhancement may be (depending on the state) as only + 1 bitstring result is measured. diff --git a/test/python/quantum_info/states/test_stabilizerstate.py b/test/python/quantum_info/states/test_stabilizerstate.py index 56fecafbe58b..4e1659ff6999 100644 --- a/test/python/quantum_info/states/test_stabilizerstate.py +++ b/test/python/quantum_info/states/test_stabilizerstate.py @@ -13,6 +13,7 @@ """Tests for Stabilizerstate quantum state class.""" +from itertools import product import unittest import logging from ddt import ddt, data, unpack @@ -32,6 +33,61 @@ logger = logging.getLogger(__name__) +class StabilizerStateTestingTools: + """Test tools for verifying test cases in StabilizerState""" + + @staticmethod + def _bitstring_product_dict(bitstring_length: int, skip_entries: dict = None) -> dict: + """Retrieves a dict of every possible product of '0', '1' for length bitstring_length + pass in a dict to use the keys as entries to skip adding to the dict + + Args: + bitstring_length (int): length of the bitstring product + skip_entries (dict[str, float], optional): dict entries to skip adding to the dict based + on existing keys in the dict passed in. Defaults to {}. + + Returns: + dict[str, float]: dict with entries, all set to 0 + """ + if skip_entries is None: + skip_entries = {} + return { + result: 0 + for result in ["".join(x) for x in product(["0", "1"], repeat=bitstring_length)] + if result not in skip_entries + } + + @staticmethod + def _verify_individual_bitstrings( + testcase: QiskitTestCase, + target_dict: dict, + stab: StabilizerState, + qargs: list = None, + decimals: int = None, + dict_almost_equal: bool = False, + ) -> None: + """Helper that iterates through the target_dict and checks all probabilities by + running the value through the probabilities_dict_from_bitstring method for + retrieving a single measurement + + Args: + target_dict (dict[str, float]): dict to check probabilities for + stab (StabilizerState): stabilizerstate object to run probabilities_dict_from_bitstring on + qargs (None or list): subsystems to return probabilities for, + if None return for all subsystems (Default: None). + decimals (None or int): the number of decimal places to round + values. If None no rounding is done (Default: None) + dict_almost_equal (bool): utilize assertDictAlmostEqual when true, assertDictEqual when false + """ + for outcome_bitstring in target_dict: + (testcase.assertDictAlmostEqual if (dict_almost_equal) else testcase.assertDictEqual)( + stab.probabilities_dict_from_bitstring( + outcome_bitstring=outcome_bitstring, qargs=qargs, decimals=decimals + ), + {outcome_bitstring: target_dict[outcome_bitstring]}, + ) + + @ddt class TestStabilizerState(QiskitTestCase): """Tests for StabilizerState class.""" @@ -315,6 +371,8 @@ def test_probabilities_dict_single_qubit(self): value = stab.probabilities_dict() target = {"0": 1} self.assertEqual(value, target) + target.update({"1": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab) probs = stab.probabilities() target = np.array([1, 0]) self.assertTrue(np.allclose(probs, target)) @@ -326,6 +384,8 @@ def test_probabilities_dict_single_qubit(self): value = stab.probabilities_dict() target = {"1": 1} self.assertEqual(value, target) + target.update({"0": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab) probs = stab.probabilities() target = np.array([0, 1]) self.assertTrue(np.allclose(probs, target)) @@ -338,6 +398,7 @@ def test_probabilities_dict_single_qubit(self): value = stab.probabilities_dict() target = {"0": 0.5, "1": 0.5} self.assertEqual(value, target) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab) probs = stab.probabilities() target = np.array([0.5, 0.5]) self.assertTrue(np.allclose(probs, target)) @@ -355,43 +416,56 @@ def test_probabilities_dict_two_qubits(self): value = stab.probabilities_dict() target = {"00": 0.5, "01": 0.5} self.assertEqual(value, target) + target.update({"10": 0.0, "11": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab) probs = stab.probabilities() target = np.array([0.5, 0.5, 0, 0]) self.assertTrue(np.allclose(probs, target)) + qargs: list = [0, 1] for _ in range(self.samples): with self.subTest(msg="P([0, 1])"): - value = stab.probabilities_dict([0, 1]) + value = stab.probabilities_dict(qargs) target = {"00": 0.5, "01": 0.5} self.assertEqual(value, target) - probs = stab.probabilities([0, 1]) + target.update({"10": 0.0, "11": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab, qargs) + probs = stab.probabilities(qargs) target = np.array([0.5, 0.5, 0, 0]) self.assertTrue(np.allclose(probs, target)) + qargs: list = [1, 0] for _ in range(self.samples): with self.subTest(msg="P([1, 0])"): - value = stab.probabilities_dict([1, 0]) + value = stab.probabilities_dict(qargs) target = {"00": 0.5, "10": 0.5} self.assertEqual(value, target) - probs = stab.probabilities([1, 0]) + target.update({"01": 0.0, "11": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab, qargs) + probs = stab.probabilities(qargs) target = np.array([0.5, 0, 0.5, 0]) self.assertTrue(np.allclose(probs, target)) + qargs: list = [0] for _ in range(self.samples): with self.subTest(msg="P[0]"): - value = stab.probabilities_dict([0]) + value = stab.probabilities_dict(qargs) target = {"0": 0.5, "1": 0.5} self.assertEqual(value, target) - probs = stab.probabilities([0]) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab, qargs) + probs = stab.probabilities(qargs) target = np.array([0.5, 0.5]) self.assertTrue(np.allclose(probs, target)) + qargs: list = [1] for _ in range(self.samples): with self.subTest(msg="P([1])"): - value = stab.probabilities_dict([1]) + value = stab.probabilities_dict(qargs) target = {"0": 1.0} self.assertEqual(value, target) - probs = stab.probabilities([1]) + target.update({"1": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab, qargs) + probs = stab.probabilities(qargs) target = np.array([1, 0]) self.assertTrue(np.allclose(probs, target)) @@ -405,9 +479,10 @@ def test_probabilities_dict_qubits(self): qc.h(2) stab = StabilizerState(qc) + decimals: int = 1 for _ in range(self.samples): with self.subTest(msg="P(None), decimals=1"): - value = stab.probabilities_dict(decimals=1) + value = stab.probabilities_dict(decimals=decimals) target = { "000": 0.1, "001": 0.1, @@ -419,13 +494,17 @@ def test_probabilities_dict_qubits(self): "111": 0.1, } self.assertEqual(value, target) - probs = stab.probabilities(decimals=1) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target, stab, decimals=decimals + ) + probs = stab.probabilities(decimals=decimals) target = np.array([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]) self.assertTrue(np.allclose(probs, target)) + decimals: int = 2 for _ in range(self.samples): with self.subTest(msg="P(None), decimals=2"): - value = stab.probabilities_dict(decimals=2) + value = stab.probabilities_dict(decimals=decimals) target = { "000": 0.12, "001": 0.12, @@ -437,13 +516,17 @@ def test_probabilities_dict_qubits(self): "111": 0.12, } self.assertEqual(value, target) - probs = stab.probabilities(decimals=2) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target, stab, decimals=decimals + ) + probs = stab.probabilities(decimals=decimals) target = np.array([0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12]) self.assertTrue(np.allclose(probs, target)) + decimals: int = 3 for _ in range(self.samples): with self.subTest(msg="P(None), decimals=3"): - value = stab.probabilities_dict(decimals=3) + value = stab.probabilities_dict(decimals=decimals) target = { "000": 0.125, "001": 0.125, @@ -455,10 +538,72 @@ def test_probabilities_dict_qubits(self): "111": 0.125, } self.assertEqual(value, target) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target, stab, decimals=decimals + ) probs = stab.probabilities(decimals=3) target = np.array([0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125]) self.assertTrue(np.allclose(probs, target)) + @combine(num_qubits=[5, 6, 7, 8, 9]) + def test_probabilities_dict_from_bitstring(self, num_qubits): + """Test probabilities_dict_from_bitstring methods with medium number of qubits that are still + reasonable to calculate the full dict with probabilities_dict of all possible outcomes""" + + qc: QuantumCircuit = QuantumCircuit(num_qubits) + for qubit_num in range(0, num_qubits): + qc.h(qubit_num) + stab = StabilizerState(qc) + + expected_result: float = float(1 / (2**num_qubits)) + target_dict: dict = StabilizerStateTestingTools._bitstring_product_dict(num_qubits) + target_dict.update((k, expected_result) for k in target_dict) + + for _ in range(self.samples): + with self.subTest(msg="P(None)"): + value = stab.probabilities_dict() + self.assertDictEqual(value, target_dict) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target_dict, stab) + probs = stab.probabilities() + target = np.array(([expected_result] * (2**num_qubits))) + self.assertTrue(np.allclose(probs, target)) + + # H gate at qubit 0, Every gate after is an X gate + # will result in 2 outcomes with 0.5 + qc = QuantumCircuit(num_qubits) + qc.h(0) + for qubit_num in range(1, num_qubits): + qc.x(qubit_num) + stab = StabilizerState(qc) + + # Build the 2 expected outcome bitstrings for + # 0.5 probability based on h and x gates + target_1: str = "".join(["1" * (num_qubits - 1)] + ["0"]) + target_2: str = "".join(["1" * num_qubits]) + target: dict = {target_1: 0.5, target_2: 0.5} + target_all_bitstrings: dict = StabilizerStateTestingTools._bitstring_product_dict( + num_qubits, target + ) + target_all_bitstrings.update(target_all_bitstrings) + + # Numpy Array to verify stab.probabilities() + target_np_dict: dict = StabilizerStateTestingTools._bitstring_product_dict( + num_qubits, [target_1, target_2] + ) + target_np_dict.update(target) + target_np_array: np.ndarray = np.array(list(target_np_dict.values())) + + for _ in range(self.samples): + with self.subTest(msg="P(None)"): + stab = StabilizerState(qc) + value = stab.probabilities_dict() + self.assertEqual(value, target) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target_all_bitstrings, stab + ) + probs = stab.probabilities() + self.assertTrue(np.allclose(probs, target_np_array)) + def test_probabilities_dict_ghz(self): """Test probabilities and probabilities_dict method of a subsystem of qubits""" @@ -473,6 +618,8 @@ def test_probabilities_dict_ghz(self): value = stab.probabilities_dict() target = {"000": 0.5, "111": 0.5} self.assertEqual(value, target) + target.update(StabilizerStateTestingTools._bitstring_product_dict(num_qubits, target)) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab) probs = stab.probabilities() target = np.array([0.5, 0, 0, 0, 0, 0, 0, 0.5]) self.assertTrue(np.allclose(probs, target)) @@ -483,6 +630,10 @@ def test_probabilities_dict_ghz(self): probs = stab.probabilities_dict(qargs) target = {"000": 0.5, "111": 0.5} self.assertDictAlmostEqual(probs, target) + target.update( + StabilizerStateTestingTools._bitstring_product_dict(num_qubits, target) + ) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab, qargs) probs = stab.probabilities(qargs) target = np.array([0.5, 0, 0, 0, 0, 0, 0, 0.5]) self.assertTrue(np.allclose(probs, target)) @@ -493,6 +644,10 @@ def test_probabilities_dict_ghz(self): probs = stab.probabilities_dict(qargs) target = {"00": 0.5, "11": 0.5} self.assertDictAlmostEqual(probs, target) + target.update(StabilizerStateTestingTools._bitstring_product_dict(2, target)) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target, stab, qargs, dict_almost_equal=True + ) probs = stab.probabilities(qargs) target = np.array([0.5, 0, 0, 0.5]) self.assertTrue(np.allclose(probs, target)) @@ -503,6 +658,9 @@ def test_probabilities_dict_ghz(self): probs = stab.probabilities_dict(qargs) target = {"0": 0.5, "1": 0.5} self.assertDictAlmostEqual(probs, target) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target, stab, qargs, dict_almost_equal=True + ) probs = stab.probabilities(qargs) target = np.array([0.5, 0.5]) self.assertTrue(np.allclose(probs, target)) @@ -520,10 +678,17 @@ def test_probs_random_subsystem(self, num_qubits): stab = StabilizerState(cliff) probs = stab.probabilities(qargs) probs_dict = stab.probabilities_dict(qargs) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, probs_dict, stab, qargs + ) target = Statevector(qc).probabilities(qargs) target_dict = Statevector(qc).probabilities_dict(qargs) + Statevector(qc).probabilities_dict() self.assertTrue(np.allclose(probs, target)) self.assertDictAlmostEqual(probs_dict, target_dict) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target_dict, stab, qargs, dict_almost_equal=True + ) @combine(num_qubits=[2, 3, 4, 5]) def test_expval_from_random_clifford(self, num_qubits): @@ -972,10 +1137,22 @@ def test_stabilizer_bell_equiv(self): # [XX, -ZZ] and [XX, YY] both generate the stabilizer group {II, XX, YY, -ZZ} self.assertTrue(cliff1.equiv(cliff2)) self.assertEqual(cliff1.probabilities_dict(), cliff2.probabilities_dict()) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, cliff1.probabilities_dict(), cliff2 + ) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, cliff2.probabilities_dict(), cliff1 + ) # [XX, ZZ] and [XX, -YY] both generate the stabilizer group {II, XX, -YY, ZZ} self.assertTrue(cliff3.equiv(cliff4)) self.assertEqual(cliff3.probabilities_dict(), cliff4.probabilities_dict()) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, cliff3.probabilities_dict(), cliff4 + ) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, cliff4.probabilities_dict(), cliff3 + ) self.assertFalse(cliff1.equiv(cliff3)) self.assertFalse(cliff2.equiv(cliff4))