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.
|
||||
#
|
||||
# (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()]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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."""
|
||||
|
||||
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,20 +2058,54 @@ 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(
|
||||
qubitwise_commutes(pauli1, pauli2)
|
||||
for pauli1, pauli2 in itertools.combinations(group, 2)
|
||||
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(
|
||||
qubitwise_commutes(group1_pauli, group2_pauli)
|
||||
for group1_pauli, group2_pauli in itertools.product(group1, group2)
|
||||
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)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue