diff --git a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py index d7e9cfe6fb25..114895e1cb7f 100644 --- a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py +++ b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py @@ -444,6 +444,117 @@ def simplify(self, atol=None, rtol=None): PauliList.from_symplectic(z, x), coeffs, ignore_pauli_phase=True, copy=False ) + def argsort(self, weight=False): + """Return indices for sorting the rows of the table. + + Returns the composition of permutations in the order of sorting + by coefficient and sorting by Pauli. + By using the `weight` kwarg the output can additionally be sorted + by the number of non-identity terms in the Pauli, where the set of + all Pauli's of a given weight are still ordered lexicographically. + + **Example** + + Here is an example of how to use SparsePauliOp argsort. + + .. jupyter-execute:: + + import numpy as np + from qiskit.quantum_info import SparsePauliOp + + # 2-qubit labels + labels = ["XX", "XX", "XX", "YI", "II", "XZ", "XY", "XI"] + # coeffs + coeffs = [2.+1.j, 2.+2.j, 3.+0.j, 3.+0.j, 4.+0.j, 5.+0.j, 6.+0.j, 7.+0.j] + + # init + spo = SparsePauliOp(labels, coeffs) + print('Initial Ordering') + print(spo) + + # Lexicographic Ordering + srt = spo.argsort() + print('Lexicographically sorted') + print(srt) + + # Lexicographic Ordering + srt = spo.argsort(weight=False) + print('Lexicographically sorted') + print(srt) + + # Weight Ordering + srt = spo.argsort(weight=True) + print('Weight sorted') + print(srt) + + Args: + weight (bool): optionally sort by weight if True (Default: False). + By using the weight kwarg the output can additionally be sorted + by the number of non-identity terms in the Pauli. + + Returns: + array: the indices for sorting the table. + """ + sort_coeffs_inds = np.argsort(self._coeffs, kind="stable") + pauli_list = self._pauli_list[sort_coeffs_inds] + sort_pauli_inds = pauli_list.argsort(weight=weight, phase=False) + return sort_coeffs_inds[sort_pauli_inds] + + def sort(self, weight=False): + """Sort the rows of the table. + + After sorting the coefficients using numpy's argsort, sort by Pauli. + Pauli sort takes precedence. + If Pauli is the same, it will be sorted by coefficient. + By using the `weight` kwarg the output can additionally be sorted + by the number of non-identity terms in the Pauli, where the set of + all Pauli's of a given weight are still ordered lexicographically. + + **Example** + + Here is an example of how to use SparsePauliOp sort. + + .. jupyter-execute:: + + import numpy as np + from qiskit.quantum_info import SparsePauliOp + + # 2-qubit labels + labels = ["XX", "XX", "XX", "YI", "II", "XZ", "XY", "XI"] + # coeffs + coeffs = [2.+1.j, 2.+2.j, 3.+0.j, 3.+0.j, 4.+0.j, 5.+0.j, 6.+0.j, 7.+0.j] + + # init + spo = SparsePauliOp(labels, coeffs) + print('Initial Ordering') + print(spo) + + # Lexicographic Ordering + srt = spo.sort() + print('Lexicographically sorted') + print(srt) + + # Lexicographic Ordering + srt = spo.sort(weight=False) + print('Lexicographically sorted') + print(srt) + + # Weight Ordering + srt = spo.sort(weight=True) + print('Weight sorted') + print(srt) + + Args: + weight (bool): optionally sort by weight if True (Default: False). + By using the weight kwarg the output can additionally be sorted + by the number of non-identity terms in the Pauli. + + Returns: + SparsePauliOp: a sorted copy of the original table. + """ + indices = self.argsort(weight=weight) + return SparsePauliOp(self._pauli_list[indices], self._coeffs[indices]) + def chop(self, tol=1e-14): """Set real and imaginary parts of the coefficients to 0 if ``< tol`` in magnitude. diff --git a/releasenotes/notes/0.21/add-sparsepauliop-methods-00a7e6cc7055e1d0.yaml b/releasenotes/notes/0.21/add-sparsepauliop-methods-00a7e6cc7055e1d0.yaml new file mode 100644 index 000000000000..b9e6c3daffad --- /dev/null +++ b/releasenotes/notes/0.21/add-sparsepauliop-methods-00a7e6cc7055e1d0.yaml @@ -0,0 +1,15 @@ +features: + - | + Added a new method :meth:`.SparsePauliOp.argsort`, which + returns the composition of permutations in the order of sorting + by coefficient and sorting by Pauli. By using the `weight` kwarg + the output can additionally be sorted + by the number of non-identity terms in the Pauli, where the set of + all Pauli's of a given weight are still ordered lexicographically. + - | + Added a new method :meth:`.SparsePauliOp.sort`. + After sorting the coefficients using numpy's argsort, sort by Pauli. + Pauli sort takes precedence. If Pauli is the same, it will be sorted + by coefficient. By using the `weight` kwarg the output can additionally + be sorted by the number of non-identity terms in the Pauli, where the + set ofall Pauli's of a given weight are still ordered lexicographically. \ No newline at end of file diff --git a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py index 4a7338f3345c..e73777b4815d 100644 --- a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py +++ b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py @@ -510,6 +510,132 @@ def test_simplify_zero(self, num_qubits): np.testing.assert_array_equal(zero_op.paulis.phase, np.zeros(zero_op.size)) np.testing.assert_array_equal(simplified_op.paulis.phase, np.zeros(simplified_op.size)) + def test_sort(self): + """Test sort method.""" + with self.assertRaises(QiskitError): + target = SparsePauliOp([], []) + + with self.subTest(msg="1 qubit real number"): + target = SparsePauliOp( + ["I", "I", "I", "I"], [-3.0 + 0.0j, 1.0 + 0.0j, 2.0 + 0.0j, 4.0 + 0.0j] + ) + value = SparsePauliOp(["I", "I", "I", "I"], [1, 2, -3, 4]).sort() + self.assertEqual(target, value) + + with self.subTest(msg="1 qubit complex"): + target = SparsePauliOp( + ["I", "I", "I", "I"], [-1.0 + 0.0j, 0.0 - 1.0j, 0.0 + 1.0j, 1.0 + 0.0j] + ) + value = SparsePauliOp( + ["I", "I", "I", "I"], [1.0 + 0.0j, 0.0 + 1.0j, 0.0 - 1.0j, -1.0 + 0.0j] + ).sort() + self.assertEqual(target, value) + + with self.subTest(msg="1 qubit Pauli I, X, Y, Z"): + target = SparsePauliOp( + ["I", "X", "Y", "Z"], [-1.0 + 2.0j, 1.0 + 0.0j, 2.0 + 0.0j, 3.0 - 4.0j] + ) + value = SparsePauliOp( + ["Y", "X", "Z", "I"], [2.0 + 0.0j, 1.0 + 0.0j, 3.0 - 4.0j, -1.0 + 2.0j] + ).sort() + self.assertEqual(target, value) + + with self.subTest(msg="1 qubit weight order"): + target = SparsePauliOp( + ["I", "X", "Y", "Z"], [-1.0 + 2.0j, 1.0 + 0.0j, 2.0 + 0.0j, 3.0 - 4.0j] + ) + value = SparsePauliOp( + ["Y", "X", "Z", "I"], [2.0 + 0.0j, 1.0 + 0.0j, 3.0 - 4.0j, -1.0 + 2.0j] + ).sort(weight=True) + self.assertEqual(target, value) + + with self.subTest(msg="1 qubit multi Pauli"): + target = SparsePauliOp( + ["I", "I", "I", "I", "X", "X", "Y", "Z"], + [ + -1.0 + 2.0j, + 1.0 + 0.0j, + 2.0 + 0.0j, + 3.0 - 4.0j, + -1.0 + 4.0j, + -1.0 + 5.0j, + -1.0 + 3.0j, + -1.0 + 2.0j, + ], + ) + value = SparsePauliOp( + ["I", "I", "I", "I", "X", "Z", "Y", "X"], + [ + 2.0 + 0.0j, + 1.0 + 0.0j, + 3.0 - 4.0j, + -1.0 + 2.0j, + -1.0 + 5.0j, + -1.0 + 2.0j, + -1.0 + 3.0j, + -1.0 + 4.0j, + ], + ).sort() + self.assertEqual(target, value) + + with self.subTest(msg="2 qubit standard order"): + target = SparsePauliOp( + ["II", "XI", "XX", "XX", "XX", "XY", "XZ", "YI"], + [ + 4.0 + 0.0j, + 7.0 + 0.0j, + 2.0 + 1.0j, + 2.0 + 2.0j, + 3.0 + 0.0j, + 6.0 + 0.0j, + 5.0 + 0.0j, + 3.0 + 0.0j, + ], + ) + value = SparsePauliOp( + ["XX", "XX", "XX", "YI", "II", "XZ", "XY", "XI"], + [ + 2.0 + 1.0j, + 2.0 + 2.0j, + 3.0 + 0.0j, + 3.0 + 0.0j, + 4.0 + 0.0j, + 5.0 + 0.0j, + 6.0 + 0.0j, + 7.0 + 0.0j, + ], + ).sort() + self.assertEqual(target, value) + + with self.subTest(msg="2 qubit weight order"): + target = SparsePauliOp( + ["II", "XI", "YI", "XX", "XX", "XX", "XY", "XZ"], + [ + 4.0 + 0.0j, + 7.0 + 0.0j, + 3.0 + 0.0j, + 2.0 + 1.0j, + 2.0 + 2.0j, + 3.0 + 0.0j, + 6.0 + 0.0j, + 5.0 + 0.0j, + ], + ) + value = SparsePauliOp( + ["XX", "XX", "XX", "YI", "II", "XZ", "XY", "XI"], + [ + 2.0 + 1.0j, + 2.0 + 2.0j, + 3.0 + 0.0j, + 3.0 + 0.0j, + 4.0 + 0.0j, + 5.0 + 0.0j, + 6.0 + 0.0j, + 7.0 + 0.0j, + ], + ).sort(weight=True) + self.assertEqual(target, value) + def test_chop(self): """Test chop, which individually truncates real and imaginary parts of the coeffs.""" eps = 1e-10