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.
#
# (C) Copyright IBM 2017, 2020
# (C) Copyright IBM 2017, 2022
#
# 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
@ -1070,11 +1070,15 @@ class PauliList(BasePauli, LinearMixin, GroupMixin):
base_z, base_x, base_phase = cls._from_array(z, x, phase)
return cls(BasePauli(base_z, base_x, base_phase))
def _noncommutation_graph(self):
"""Create an edge list representing the qubit-wise non-commutation graph.
def _noncommutation_graph(self, qubit_wise):
"""Create an edge list representing the non-commutation graph (Pauli Graph).
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:
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,
)
mat2 = mat1[:, None]
# mat3[i, j] is True if i and j are qubit-wise commutable
mat3 = (((mat1 * mat2) * (mat1 - mat2)) == 0).all(axis=2)
# convert into list where tuple elements are qubit-wise non-commuting operators
return list(zip(*np.where(np.triu(np.logical_not(mat3), k=1))))
# This is 0 (false-y) iff one of the operators is the identity and/or both operators are the
# same. In other cases, it is non-zero (truth-y).
qubit_anticommutation_mat = (mat1 * mat2) * (mat1 - mat2)
# '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):
"""Partition a PauliList into sets of mutually qubit-wise commuting Pauli strings.
@ -1095,14 +1124,32 @@ class PauliList(BasePauli, LinearMixin, GroupMixin):
Returns:
List[PauliList]: List of PauliLists where each PauliList contains commutable Pauli operators.
"""
nodes = range(self._num_paulis)
edges = self._noncommutation_graph()
graph = rx.PyGraph()
graph.add_nodes_from(nodes)
graph.add_edges_from_no_data(edges)
return self.group_commuting(qubit_wise=True)
def group_commuting(self, qubit_wise=False):
"""Partition a PauliList 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
>>> 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
coloring_dict = rx.graph_greedy_color(graph)
groups = defaultdict(list)
for idx, color in coloring_dict.items():
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.
"""
from collections import defaultdict
from numbers import Number
from typing import Dict
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.quantum_info.operators.custom_iterator import CustomIterator
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_utils import pauli_basis
from qiskit.utils.deprecation import deprecate_function
from qiskit._accelerate.sparse_pauli_op import unordered_unique # pylint: disable=import-error
class SparsePauliOp(LinearOp):
@ -777,6 +779,54 @@ class SparsePauliOp(LinearOp):
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
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."""
import itertools
import unittest
from test import combine
import itertools
import numpy as np
from ddt import ddt
from scipy.sparse import csr_matrix
@ -2058,21 +2058,55 @@ class TestPauliListMethods(QiskitTestCase):
# 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]
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.
for group in groups:
assert all(
self.assertTrue(
all(
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
# at least one element of the other.
for group1, group2 in itertools.combinations(groups, 2):
assert not all(
self.assertFalse(
all(
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)
)
)
if __name__ == "__main__":

View File

@ -20,13 +20,7 @@ import numpy as np
from ddt import ddt
from qiskit import QiskitError
from qiskit.quantum_info.operators import (
Operator,
Pauli,
PauliList,
PauliTable,
SparsePauliOp,
)
from qiskit.quantum_info.operators import Operator, Pauli, PauliList, PauliTable, SparsePauliOp
from qiskit.test import QiskitTestCase
@ -612,6 +606,41 @@ class TestSparsePauliOpMethods(QiskitTestCase):
self.assertNotEqual(spp_op1, 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__":
unittest.main()