mirror of https://github.com/Qiskit/qiskit.git
Add grouping by full-operator commutation relations to PauliList (#7874)
* add group_inter_qubit_commuting * fix style lint of pauli_list.py * fix style lint * fix black format * update format * add test, docstring * reformat * adjust line length * adjust docstring format * adjust docstring format * adjust docstring format * update docstring and comment * add release note * Update documentation Co-authored-by: Jake Lishman <jake.lishman@ibm.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
parent
2b52def6d6
commit
206ecd0e20
|
@ -1,6 +1,6 @@
|
||||||
# This code is part of Qiskit.
|
# This code is part of Qiskit.
|
||||||
#
|
#
|
||||||
# (C) Copyright IBM 2017, 2020
|
# (C) Copyright IBM 2017, 2022
|
||||||
#
|
#
|
||||||
# This code is licensed under the Apache License, Version 2.0. You may
|
# This code is licensed under the Apache License, Version 2.0. You may
|
||||||
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
||||||
|
@ -1070,11 +1070,15 @@ class PauliList(BasePauli, LinearMixin, GroupMixin):
|
||||||
base_z, base_x, base_phase = cls._from_array(z, x, phase)
|
base_z, base_x, base_phase = cls._from_array(z, x, phase)
|
||||||
return cls(BasePauli(base_z, base_x, base_phase))
|
return cls(BasePauli(base_z, base_x, base_phase))
|
||||||
|
|
||||||
def _noncommutation_graph(self):
|
def _noncommutation_graph(self, qubit_wise):
|
||||||
"""Create an edge list representing the qubit-wise non-commutation graph.
|
"""Create an edge list representing the non-commutation graph (Pauli Graph).
|
||||||
|
|
||||||
An edge (i, j) is present if i and j are not commutable.
|
An edge (i, j) is present if i and j are not commutable.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
qubit_wise (bool): whether the commutation rule is applied to the whole operator,
|
||||||
|
or on a per-qubit basis.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[Tuple(int,int)]: A list of pairs of indices of the PauliList that are not commutable.
|
List[Tuple(int,int)]: A list of pairs of indices of the PauliList that are not commutable.
|
||||||
"""
|
"""
|
||||||
|
@ -1084,10 +1088,35 @@ class PauliList(BasePauli, LinearMixin, GroupMixin):
|
||||||
dtype=np.int8,
|
dtype=np.int8,
|
||||||
)
|
)
|
||||||
mat2 = mat1[:, None]
|
mat2 = mat1[:, None]
|
||||||
# mat3[i, j] is True if i and j are qubit-wise commutable
|
# This is 0 (false-y) iff one of the operators is the identity and/or both operators are the
|
||||||
mat3 = (((mat1 * mat2) * (mat1 - mat2)) == 0).all(axis=2)
|
# same. In other cases, it is non-zero (truth-y).
|
||||||
# convert into list where tuple elements are qubit-wise non-commuting operators
|
qubit_anticommutation_mat = (mat1 * mat2) * (mat1 - mat2)
|
||||||
return list(zip(*np.where(np.triu(np.logical_not(mat3), k=1))))
|
# 'adjacency_mat[i, j]' is True iff Paulis 'i' and 'j' do not commute in the given strategy.
|
||||||
|
if qubit_wise:
|
||||||
|
adjacency_mat = np.logical_or.reduce(qubit_anticommutation_mat, axis=2)
|
||||||
|
else:
|
||||||
|
# Don't commute if there's an odd number of element-wise anti-commutations.
|
||||||
|
adjacency_mat = np.logical_xor.reduce(qubit_anticommutation_mat, axis=2)
|
||||||
|
# Convert into list where tuple elements are non-commuting operators. We only want to
|
||||||
|
# results from one triangle to avoid symmetric duplications.
|
||||||
|
return list(zip(*np.where(np.triu(adjacency_mat, k=1))))
|
||||||
|
|
||||||
|
def _create_graph(self, qubit_wise):
|
||||||
|
"""Transform measurement operator grouping problem into graph coloring problem
|
||||||
|
|
||||||
|
Args:
|
||||||
|
qubit_wise (bool): whether the commutation rule is applied to the whole operator,
|
||||||
|
or on a per-qubit basis.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
retworkx.PyGraph: A class of undirected graphs
|
||||||
|
"""
|
||||||
|
|
||||||
|
edges = self._noncommutation_graph(qubit_wise)
|
||||||
|
graph = rx.PyGraph()
|
||||||
|
graph.add_nodes_from(range(self.size))
|
||||||
|
graph.add_edges_from_no_data(edges)
|
||||||
|
return graph
|
||||||
|
|
||||||
def group_qubit_wise_commuting(self):
|
def group_qubit_wise_commuting(self):
|
||||||
"""Partition a PauliList into sets of mutually qubit-wise commuting Pauli strings.
|
"""Partition a PauliList into sets of mutually qubit-wise commuting Pauli strings.
|
||||||
|
@ -1095,14 +1124,32 @@ class PauliList(BasePauli, LinearMixin, GroupMixin):
|
||||||
Returns:
|
Returns:
|
||||||
List[PauliList]: List of PauliLists where each PauliList contains commutable Pauli operators.
|
List[PauliList]: List of PauliLists where each PauliList contains commutable Pauli operators.
|
||||||
"""
|
"""
|
||||||
nodes = range(self._num_paulis)
|
return self.group_commuting(qubit_wise=True)
|
||||||
edges = self._noncommutation_graph()
|
|
||||||
graph = rx.PyGraph()
|
def group_commuting(self, qubit_wise=False):
|
||||||
graph.add_nodes_from(nodes)
|
"""Partition a PauliList into sets of commuting Pauli strings.
|
||||||
graph.add_edges_from_no_data(edges)
|
|
||||||
|
Args:
|
||||||
|
qubit_wise (bool): whether the commutation rule is applied to the whole operator,
|
||||||
|
or on a per-qubit basis. For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
>>> from qiskit.quantum_info import PauliList
|
||||||
|
>>> op = PauliList(["XX", "YY", "IZ", "ZZ"])
|
||||||
|
>>> op.group_commuting()
|
||||||
|
[PauliList(['XX', 'YY']), PauliList(['IZ', 'ZZ'])]
|
||||||
|
>>> op.group_commuting(qubit_wise=True)
|
||||||
|
[PauliList(['XX']), PauliList(['YY']), PauliList(['IZ', 'ZZ'])]
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[PauliList]: List of PauliLists where each PauliList contains commuting Pauli operators.
|
||||||
|
"""
|
||||||
|
|
||||||
|
graph = self._create_graph(qubit_wise)
|
||||||
# Keys in coloring_dict are nodes, values are colors
|
# Keys in coloring_dict are nodes, values are colors
|
||||||
coloring_dict = rx.graph_greedy_color(graph)
|
coloring_dict = rx.graph_greedy_color(graph)
|
||||||
groups = defaultdict(list)
|
groups = defaultdict(list)
|
||||||
for idx, color in coloring_dict.items():
|
for idx, color in coloring_dict.items():
|
||||||
groups[color].append(idx)
|
groups[color].append(idx)
|
||||||
return [PauliList([self[i] for i in x]) for x in groups.values()]
|
return [self[group] for group in groups.values()]
|
||||||
|
|
|
@ -13,11 +13,14 @@
|
||||||
N-Qubit Sparse Pauli Operator class.
|
N-Qubit Sparse Pauli Operator class.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
from numbers import Number
|
from numbers import Number
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import retworkx as rx
|
||||||
|
|
||||||
|
from qiskit._accelerate.sparse_pauli_op import unordered_unique # pylint: disable=import-error
|
||||||
from qiskit.exceptions import QiskitError
|
from qiskit.exceptions import QiskitError
|
||||||
from qiskit.quantum_info.operators.custom_iterator import CustomIterator
|
from qiskit.quantum_info.operators.custom_iterator import CustomIterator
|
||||||
from qiskit.quantum_info.operators.linear_op import LinearOp
|
from qiskit.quantum_info.operators.linear_op import LinearOp
|
||||||
|
@ -28,7 +31,6 @@ from qiskit.quantum_info.operators.symplectic.pauli_list import PauliList
|
||||||
from qiskit.quantum_info.operators.symplectic.pauli_table import PauliTable
|
from qiskit.quantum_info.operators.symplectic.pauli_table import PauliTable
|
||||||
from qiskit.quantum_info.operators.symplectic.pauli_utils import pauli_basis
|
from qiskit.quantum_info.operators.symplectic.pauli_utils import pauli_basis
|
||||||
from qiskit.utils.deprecation import deprecate_function
|
from qiskit.utils.deprecation import deprecate_function
|
||||||
from qiskit._accelerate.sparse_pauli_op import unordered_unique # pylint: disable=import-error
|
|
||||||
|
|
||||||
|
|
||||||
class SparsePauliOp(LinearOp):
|
class SparsePauliOp(LinearOp):
|
||||||
|
@ -777,6 +779,54 @@ class SparsePauliOp(LinearOp):
|
||||||
|
|
||||||
return MatrixIterator(self)
|
return MatrixIterator(self)
|
||||||
|
|
||||||
|
def _create_graph(self, qubit_wise):
|
||||||
|
"""Transform measurement operator grouping problem into graph coloring problem
|
||||||
|
|
||||||
|
Args:
|
||||||
|
qubit_wise (bool): whether the commutation rule is applied to the whole operator,
|
||||||
|
or on a per-qubit basis.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
retworkx.PyGraph: A class of undirected graphs
|
||||||
|
"""
|
||||||
|
|
||||||
|
edges = self.paulis._noncommutation_graph(qubit_wise)
|
||||||
|
graph = rx.PyGraph()
|
||||||
|
graph.add_nodes_from(range(self.size))
|
||||||
|
graph.add_edges_from_no_data(edges)
|
||||||
|
return graph
|
||||||
|
|
||||||
|
def group_commuting(self, qubit_wise=False):
|
||||||
|
"""Partition a SparsePauliOp into sets of commuting Pauli strings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
qubit_wise (bool): whether the commutation rule is applied to the whole operator,
|
||||||
|
or on a per-qubit basis. For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
>>> op = SparsePauliOp.from_list([("XX", 2), ("YY", 1), ("IZ",2j), ("ZZ",1j)])
|
||||||
|
>>> op.group_commuting()
|
||||||
|
[SparsePauliOp(["IZ", "ZZ"], coeffs=[0.+2.j, 0.+1j]),
|
||||||
|
SparsePauliOp(["XX", "YY"], coeffs=[2.+0.j, 1.+0.j])]
|
||||||
|
>>> op.group_commuting(qubit_wise=True)
|
||||||
|
[SparsePauliOp(['XX'], coeffs=[2.+0.j]),
|
||||||
|
SparsePauliOp(['YY'], coeffs=[1.+0.j]),
|
||||||
|
SparsePauliOp(['IZ', 'ZZ'], coeffs=[0.+2.j, 0.+1.j])]
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[SparsePauliOp]: List of SparsePauliOp where each SparsePauliOp contains
|
||||||
|
commuting Pauli operators.
|
||||||
|
"""
|
||||||
|
|
||||||
|
graph = self._create_graph(qubit_wise)
|
||||||
|
# Keys in coloring_dict are nodes, values are colors
|
||||||
|
coloring_dict = rx.graph_greedy_color(graph)
|
||||||
|
groups = defaultdict(list)
|
||||||
|
for idx, color in coloring_dict.items():
|
||||||
|
groups[color].append(idx)
|
||||||
|
return [self[group] for group in groups.values()]
|
||||||
|
|
||||||
|
|
||||||
# Update docstrings for API docs
|
# Update docstrings for API docs
|
||||||
generate_apidocs(SparsePauliOp)
|
generate_apidocs(SparsePauliOp)
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Added the methods :meth:`.PauliList.group_commuting` and :meth:`.SparsePauliOp.group_commuting`,
|
||||||
|
which partition these operators into sublists where each element commutes with all the others.
|
||||||
|
For example::
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from qiskit.quantum_info import PauliList, SparsePauliOp
|
||||||
|
|
||||||
|
groups = PauliList(["XX", "YY", "IZ", "ZZ"]).group_commuting()
|
||||||
|
# 'groups' is [PauliList(['IZ', 'ZZ']), PauliList(['XX', 'YY'])]
|
||||||
|
|
||||||
|
op = SparsePauliOp.from_list([("XX", 2), ("YY", 1), ("IZ", 2j), ("ZZ", 1j)])
|
||||||
|
groups = op.group_commuting()
|
||||||
|
# 'groups' is [
|
||||||
|
# SparsePauliOp(['IZ', 'ZZ'], coeffs=[0.+2.j, 0.+1.j]),
|
||||||
|
# SparsePauliOp(['XX', 'YY'], coeffs=[2.+0.j, 1.+0.j]),
|
||||||
|
# ]
|
|
@ -12,10 +12,10 @@
|
||||||
|
|
||||||
"""Tests for PauliList class."""
|
"""Tests for PauliList class."""
|
||||||
|
|
||||||
|
import itertools
|
||||||
import unittest
|
import unittest
|
||||||
from test import combine
|
from test import combine
|
||||||
|
|
||||||
import itertools
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from ddt import ddt
|
from ddt import ddt
|
||||||
from scipy.sparse import csr_matrix
|
from scipy.sparse import csr_matrix
|
||||||
|
@ -2058,20 +2058,54 @@ class TestPauliListMethods(QiskitTestCase):
|
||||||
|
|
||||||
# checking that every input Pauli in pauli_list is in a group in the ouput
|
# checking that every input Pauli in pauli_list is in a group in the ouput
|
||||||
output_labels = [pauli.to_label() for group in groups for pauli in group]
|
output_labels = [pauli.to_label() for group in groups for pauli in group]
|
||||||
assert sorted(output_labels) == sorted(input_labels)
|
# assert sorted(output_labels) == sorted(input_labels)
|
||||||
|
self.assertListEqual(sorted(output_labels), sorted(input_labels))
|
||||||
# Within each group, every operator qubit-wise commutes with every other operator.
|
# Within each group, every operator qubit-wise commutes with every other operator.
|
||||||
for group in groups:
|
for group in groups:
|
||||||
assert all(
|
self.assertTrue(
|
||||||
qubitwise_commutes(pauli1, pauli2)
|
all(
|
||||||
for pauli1, pauli2 in itertools.combinations(group, 2)
|
qubitwise_commutes(pauli1, pauli2)
|
||||||
|
for pauli1, pauli2 in itertools.combinations(group, 2)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
# For every pair of groups, at least one element from one does not qubit-wise commute with
|
# For every pair of groups, at least one element from one does not qubit-wise commute with
|
||||||
# at least one element of the other.
|
# at least one element of the other.
|
||||||
for group1, group2 in itertools.combinations(groups, 2):
|
for group1, group2 in itertools.combinations(groups, 2):
|
||||||
assert not all(
|
self.assertFalse(
|
||||||
qubitwise_commutes(group1_pauli, group2_pauli)
|
all(
|
||||||
for group1_pauli, group2_pauli in itertools.product(group1, group2)
|
qubitwise_commutes(group1_pauli, group2_pauli)
|
||||||
|
for group1_pauli, group2_pauli in itertools.product(group1, group2)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_group_commuting(self):
|
||||||
|
"""Test general grouping commuting operators"""
|
||||||
|
|
||||||
|
def commutes(left: Pauli, right: Pauli) -> bool:
|
||||||
|
return len(left) == len(right) and left.commutes(right)
|
||||||
|
|
||||||
|
input_labels = ["IY", "ZX", "XZ", "YI", "YX", "YY", "YZ", "ZI", "ZX", "ZY", "iZZ", "II"]
|
||||||
|
np.random.shuffle(input_labels)
|
||||||
|
pauli_list = PauliList(input_labels)
|
||||||
|
# if qubit_wise=True, equivalent to test_group_qubit_wise_commuting
|
||||||
|
groups = pauli_list.group_commuting(qubit_wise=False)
|
||||||
|
|
||||||
|
# checking that every input Pauli in pauli_list is in a group in the ouput
|
||||||
|
output_labels = [pauli.to_label() for group in groups for pauli in group]
|
||||||
|
self.assertListEqual(sorted(output_labels), sorted(input_labels))
|
||||||
|
# Within each group, every operator commutes with every other operator.
|
||||||
|
for group in groups:
|
||||||
|
self.assertTrue(
|
||||||
|
all(commutes(pauli1, pauli2) for pauli1, pauli2 in itertools.combinations(group, 2))
|
||||||
|
)
|
||||||
|
# For every pair of groups, at least one element from one group does not commute with
|
||||||
|
# at least one element of the other.
|
||||||
|
for group1, group2 in itertools.combinations(groups, 2):
|
||||||
|
self.assertFalse(
|
||||||
|
all(
|
||||||
|
commutes(group1_pauli, group2_pauli)
|
||||||
|
for group1_pauli, group2_pauli in itertools.product(group1, group2)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -20,13 +20,7 @@ import numpy as np
|
||||||
from ddt import ddt
|
from ddt import ddt
|
||||||
|
|
||||||
from qiskit import QiskitError
|
from qiskit import QiskitError
|
||||||
from qiskit.quantum_info.operators import (
|
from qiskit.quantum_info.operators import Operator, Pauli, PauliList, PauliTable, SparsePauliOp
|
||||||
Operator,
|
|
||||||
Pauli,
|
|
||||||
PauliList,
|
|
||||||
PauliTable,
|
|
||||||
SparsePauliOp,
|
|
||||||
)
|
|
||||||
from qiskit.test import QiskitTestCase
|
from qiskit.test import QiskitTestCase
|
||||||
|
|
||||||
|
|
||||||
|
@ -612,6 +606,41 @@ class TestSparsePauliOpMethods(QiskitTestCase):
|
||||||
self.assertNotEqual(spp_op1, spp_op2)
|
self.assertNotEqual(spp_op1, spp_op2)
|
||||||
self.assertTrue(spp_op1.equiv(spp_op2))
|
self.assertTrue(spp_op1.equiv(spp_op2))
|
||||||
|
|
||||||
|
def test_group_commuting(self):
|
||||||
|
"""Test general grouping commuting operators"""
|
||||||
|
|
||||||
|
def commutes(left: Pauli, right: Pauli) -> bool:
|
||||||
|
return len(left) == len(right) and left.commutes(right)
|
||||||
|
|
||||||
|
input_labels = ["IX", "IY", "IZ", "XX", "YY", "ZZ", "XY", "YX", "ZX", "ZY", "XZ", "YZ"]
|
||||||
|
np.random.shuffle(input_labels)
|
||||||
|
coefs = np.random.random(len(input_labels)) + np.random.random(len(input_labels)) * 1j
|
||||||
|
sparse_pauli_list = SparsePauliOp(input_labels, coefs)
|
||||||
|
groups = sparse_pauli_list.group_commuting()
|
||||||
|
# checking that every input Pauli in sparse_pauli_list is in a group in the ouput
|
||||||
|
output_labels = [pauli.to_label() for group in groups for pauli in group.paulis]
|
||||||
|
self.assertListEqual(sorted(output_labels), sorted(input_labels))
|
||||||
|
# checking that every coeffs are grouped according to sparse_pauli_list group
|
||||||
|
paulis_coeff_dict = dict(
|
||||||
|
sum([list(zip(group.paulis.to_labels(), group.coeffs)) for group in groups], [])
|
||||||
|
)
|
||||||
|
self.assertDictEqual(dict(zip(input_labels, coefs)), paulis_coeff_dict)
|
||||||
|
|
||||||
|
# Within each group, every operator commutes with every other operator.
|
||||||
|
for group in groups:
|
||||||
|
self.assertTrue(
|
||||||
|
all(commutes(pauli1, pauli2) for pauli1, pauli2 in it.combinations(group.paulis, 2))
|
||||||
|
)
|
||||||
|
# For every pair of groups, at least one element from one group does not commute with
|
||||||
|
# at least one element of the other.
|
||||||
|
for group1, group2 in it.combinations(groups, 2):
|
||||||
|
self.assertFalse(
|
||||||
|
all(
|
||||||
|
commutes(group1_pauli, group2_pauli)
|
||||||
|
for group1_pauli, group2_pauli in it.product(group1.paulis, group2.paulis)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
Loading…
Reference in New Issue