Skip to content

Commit

Permalink
Expose PauliList.noncommutation_graph in public API (Qiskit#11795)
Browse files Browse the repository at this point in the history
* refactor: remove duplicate method `SparsePauliOp._create_graph`

* refactor: de-duplicate the `PauliList.group_commuting` logic

* refactor: expose `PauliList.noncommutation_graph`

* docs: add release note

* test: add unittests for noncommutation_graph methods

* fix: remove now unused import

* refactor: parameterize the PauliList.noncommutation_graph test

* test: cover additional phases in noncommutation_graph tests

* docs: document the noncommutation_graph data contents
  • Loading branch information
mrossinek authored Feb 19, 2024
1 parent 12f24a7 commit 05c69cd
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 28 deletions.
43 changes: 32 additions & 11 deletions qiskit/quantum_info/operators/symplectic/pauli_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -1155,23 +1155,50 @@ def _noncommutation_graph(self, qubit_wise):
# 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
def noncommutation_graph(self, qubit_wise: bool) -> rx.PyGraph:
"""Create the non-commutation graph of this PauliList.
This transforms the measurement operator grouping problem into graph coloring problem. The
constructed graph contains one node for each Pauli. The nodes will be connecting for any two
Pauli terms that do _not_ commute.
Args:
qubit_wise (bool): whether the commutation rule is applied to the whole operator,
or on a per-qubit basis.
Returns:
rustworkx.PyGraph: A class of undirected graphs
rustworkx.PyGraph: the non-commutation graph with nodes for each Pauli and edges
indicating a non-commutation relation. Each node will hold the index of the Pauli
term it corresponds to in its data. The edges of the graph hold no data.
"""

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 _commuting_groups(self, qubit_wise: bool) -> dict[int, list[int]]:
"""Partition a PauliList into sets of commuting Pauli strings.
This is the internal logic of the public ``PauliList.group_commuting`` method which returns
a mapping of colors to Pauli indices. The same logic is re-used by
``SparsePauliOp.group_commuting``.
Args:
qubit_wise (bool): whether the commutation rule is applied to the whole operator,
or on a per-qubit basis.
Returns:
dict[int, list[int]]: Dictionary of color indices mapping to a list of Pauli indices.
"""
graph = self.noncommutation_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 groups

def group_qubit_wise_commuting(self) -> list[PauliList]:
"""Partition a PauliList into sets of mutually qubit-wise commuting Pauli strings.
Expand Down Expand Up @@ -1199,11 +1226,5 @@ def group_commuting(self, qubit_wise: bool = False) -> list[PauliList]:
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)
groups = self._commuting_groups(qubit_wise)
return [self[group] for group in groups.values()]
28 changes: 11 additions & 17 deletions qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from __future__ import annotations
from typing import TYPE_CHECKING, List

from collections import defaultdict
from collections.abc import Mapping, Sequence, Iterable
from numbers import Number
from copy import deepcopy
Expand Down Expand Up @@ -1000,22 +999,23 @@ def __getitem__(self, key):

return MatrixIterator(self)

def _create_graph(self, qubit_wise):
"""Transform measurement operator grouping problem into graph coloring problem
def noncommutation_graph(self, qubit_wise: bool) -> rx.PyGraph:
"""Create the non-commutation graph of this SparsePauliOp.
This transforms the measurement operator grouping problem into graph coloring problem. The
constructed graph contains one node for each Pauli. The nodes will be connecting for any two
Pauli terms that do _not_ commute.
Args:
qubit_wise (bool): whether the commutation rule is applied to the whole operator,
or on a per-qubit basis.
Returns:
rustworkx.PyGraph: A class of undirected graphs
rustworkx.PyGraph: the non-commutation graph with nodes for each Pauli and edges
indicating a non-commutation relation. Each node will hold the index of the Pauli
term it corresponds to in its data. The edges of the graph hold no data.
"""

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
return self.paulis.noncommutation_graph(qubit_wise)

def group_commuting(self, qubit_wise: bool = False) -> list[SparsePauliOp]:
"""Partition a SparsePauliOp into sets of commuting Pauli strings.
Expand All @@ -1039,13 +1039,7 @@ def group_commuting(self, qubit_wise: bool = False) -> list[SparsePauliOp]:
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)
groups = self.paulis._commuting_groups(qubit_wise)
return [self[group] for group in groups.values()]

@property
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
features:
- |
Adds the :meth:`.PauliList.noncommutation_graph` and
:meth:`.SparsePauliOp.noncommutation_graph` methods, exposing the
construction of non-commutation graphs, recasting the measurement operator
grouping problem into a graph coloring problem. This permits users to work
with these graphs directly, for example to explore coloring algorithms other
than the one used by :meth:`.SparsePauliOp.group_commuting`.
33 changes: 33 additions & 0 deletions test/python/quantum_info/operators/symplectic/test_pauli_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import unittest

import numpy as np
import rustworkx as rx
from ddt import ddt
from scipy.sparse import csr_matrix

Expand Down Expand Up @@ -2071,6 +2072,38 @@ def test_evolve_clifford_qargs(self, phase):
self.assertListEqual(value, value_h)
self.assertListEqual(value_inv, value_s)

@combine(qubit_wise=[True, False])
def test_noncommutation_graph(self, qubit_wise):
"""Test noncommutation graph"""

def commutes(left: Pauli, right: Pauli, qubit_wise: bool) -> bool:
if len(left) != len(right):
return False
if not qubit_wise:
return left.commutes(right)
else:
# qubit-wise commuting check
vec_l = left.z + 2 * left.x
vec_r = right.z + 2 * right.x
qubit_wise_comparison = (vec_l * vec_r) * (vec_l - vec_r)
return np.all(qubit_wise_comparison == 0)

input_labels = ["IY", "ZX", "XZ", "-YI", "YX", "YY", "-iYZ", "ZI", "ZX", "ZY", "iZZ", "II"]
np.random.shuffle(input_labels)
pauli_list = PauliList(input_labels)
graph = pauli_list.noncommutation_graph(qubit_wise=qubit_wise)

expected = rx.PyGraph()
expected.add_nodes_from(range(len(input_labels)))
edges = [
(ia, ib)
for (ia, a), (ib, b) in itertools.combinations(enumerate(input_labels), 2)
if not commutes(Pauli(a), Pauli(b), qubit_wise)
]
expected.add_edges_from_no_data(edges)

self.assertTrue(rx.is_isomorphic(graph, expected))

def test_group_qubit_wise_commuting(self):
"""Test grouping qubit-wise commuting operators"""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import itertools as it
import unittest
import numpy as np
import rustworkx as rx
from ddt import ddt

from qiskit import QiskitError
Expand Down Expand Up @@ -936,6 +937,42 @@ def test_eq_equiv(self):
self.assertNotEqual(spp_op1, spp_op2)
self.assertTrue(spp_op1.equiv(spp_op2))

@combine(parameterized=[True, False], qubit_wise=[True, False])
def test_noncommutation_graph(self, parameterized, qubit_wise):
"""Test noncommutation graph"""

def commutes(left: Pauli, right: Pauli, qubit_wise: bool) -> bool:
if len(left) != len(right):
return False
if not qubit_wise:
return left.commutes(right)
else:
# qubit-wise commuting check
vec_l = left.z + 2 * left.x
vec_r = right.z + 2 * right.x
qubit_wise_comparison = (vec_l * vec_r) * (vec_l - vec_r)
return np.all(qubit_wise_comparison == 0)

input_labels = ["IX", "IY", "IZ", "XX", "YY", "ZZ", "XY", "iYX", "ZX", "-iZY", "XZ", "YZ"]
np.random.shuffle(input_labels)
if parameterized:
coeffs = np.array(ParameterVector("a", len(input_labels)))
else:
coeffs = np.random.random(len(input_labels)) + np.random.random(len(input_labels)) * 1j
sparse_pauli_list = SparsePauliOp(input_labels, coeffs)
graph = sparse_pauli_list.noncommutation_graph(qubit_wise)

expected = rx.PyGraph()
expected.add_nodes_from(range(len(input_labels)))
edges = [
(ia, ib)
for (ia, a), (ib, b) in it.combinations(enumerate(input_labels), 2)
if not commutes(Pauli(a), Pauli(b), qubit_wise)
]
expected.add_edges_from_no_data(edges)

self.assertTrue(rx.is_isomorphic(graph, expected))

@combine(parameterized=[True, False], qubit_wise=[True, False])
def test_group_commuting(self, parameterized, qubit_wise):
"""Test general grouping commuting operators"""
Expand Down

0 comments on commit 05c69cd

Please sign in to comment.