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:
Yuma-Nakamura 2022-06-23 08:30:03 +09:00 committed by GitHub
parent 2b52def6d6
commit 206ecd0e20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 210 additions and 30 deletions

View File

@ -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()]

View File

@ -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)

View File

@ -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]),
# ]

View File

@ -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)
)
) )

View File

@ -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()