Skip to content

Commit dea7a2d

Browse files
Improve implementation of group_observables (#6043)
**Context:** `qml.pauli.group_observables` has several areas of improvement that make the calculation slow. This PR attempts to address some of them. **Description of the Change:** 1. Solve the Graph colouring algorithm using `Rustworkx` instead of custom implementation. This adds 2 additional methods to solve the Minimum Clique cover problem: DSATUR (`'dsatur'`) and IndependentSet (`'gis'`) 2. Use the symplectic representation of the Pauli observables to construct the adjacency matrix of the complement graph. 3. Introduce a new function to obtain the indices of the partitions directly: `qml.pauli.compute_partition_indices` **Benefits:** Improves of orders of magnitude compared to the current implementation of the `qwc` grouping type strategy. The current implementation is based on an $\mathcal{O}(m^2)$ computation, with $m$ the number of observables to be grouped. Instead, taking advantage of the symplectic inner product, and its relation with the commutator and anti commutator of Pauli observables [1], we can obtain a significant improvement. ![image](https://github.com/user-attachments/assets/3f0ec70b-2082-44a3-acf9-9cdf4238da08) In addition, by introducing `qml.pauli.compute_partition_indices`, we now avoid the (rather often) inefficient computation of partitions of observables, just to then calculate the indices from them - see https://github.com/PennyLaneAI/pennylane/blob/2892a9a0aa6797912ef4a03f3c4759eaff01d8ef/pennylane/ops/qubit/hamiltonian.py#L37 Instead, we obtain the indices of the partitions directly from the graph colouring algorithm. Putting all together, the improvement in performance when calculating the partitions of large Hamiltonians is evident. ![image](https://github.com/user-attachments/assets/7bd047a9-a628-46c8-9d3e-58818fd7acc0) This graph was obtained by running ``` python %timeit hamiltonian.compute_grouping(grouping_type="qwc", method='lf') ``` **Notes:** Profiling the implementation from `linear_combination` demonstrates that the post processing to obtain the indices remains a significant burden in the calculations, and therefore should be addressed for the remaining functions - see reference code above. ![image](https://github.com/user-attachments/assets/09cf4ca6-601a-4e91-8865-8bf727dd3f1d) [1] Andrew Jena (2019). Partitioning Pauli Operators: in Theory and in Practice. UWSpace. http://hdl.handle.net/10012/15017 [sc-64594] ## Performance comparisons between colouring algorithms Comparison for `LinearCombination.compute_grouping()` ![image](https://github.com/user-attachments/assets/042d040a-f75b-4642-b6e4-0e903c5d58c5) ![image](https://github.com/user-attachments/assets/a89197ee-9c3d-424d-9bba-36829ee33388) Comparison for `colour_paui_graph` (only between `Rustworkx` algorithms) ![image](https://github.com/user-attachments/assets/52ae4d0f-acaa-4d45-aab5-d3c40c37863a) --------- Co-authored-by: Ali Asadi <[email protected]>
1 parent 17f15fb commit dea7a2d

19 files changed

+652
-235
lines changed

doc/releases/changelog-dev.md

+8
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@
5252
* A new method `to_mat` has been added to the `FermiWord` and `FermiSentence` classes, which allows
5353
computing the matrix representation of these Fermi operators.
5454
[(#5920)](https://github.com/PennyLaneAI/pennylane/pull/5920)
55+
56+
* `qml.pauli.group_observables` now uses `Rustworkx` colouring algorithms to solve the Minimum Clique Cover problem.
57+
This adds two new options for the `method` argument: `dsatur` and `gis`. In addition, the creation of the adjancecy matrix
58+
now takes advantage of the symplectic representation of the Pauli observables. An additional function `qml.pauli.compute_partition_indices`
59+
is added to calculate the indices from the partitioned observables more efficiently. `qml.pauli.grouping.PauliGroupingStrategy.idx_partitions_from_graph`
60+
can be used to compute partitions of custom indices. These changes improve the wall time of `qml.LinearCombination.compute_grouping`
61+
and the `grouping_type='qwc'` by orders of magnitude.
62+
[(#6043)](https://github.com/PennyLaneAI/pennylane/pull/6043)
5563

5664
<h4>Improvements to operators</h4>
5765

doc/requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ torch==1.9.0; sys_platform == "darwin" and python_version < "3.10"
3131
torch==1.13.1; sys_platform != "darwin" and python_version == "3.10"
3232
torch==1.13.1; sys_platform == "darwin" and python_version == "3.10"
3333
jinja2==3.0.3
34-
rustworkx==0.12.1
34+
rustworkx>=0.14.0
3535
networkx==2.6
3636
requests~=2.28.1
3737
# we do not pin the sphinx theme, to allow the

pennylane/devices/qubit/sampling.py

-5
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ def _group_measurements(mps: list[Union[SampleMeasurement, ClassicalShadowMP, Sh
6363
# measurements with no observables
6464
mp_no_obs = []
6565
mp_no_obs_indices = []
66-
6766
for i, mp in enumerate(mps):
6867
if isinstance(mp.obs, (Sum, SProd, Prod)):
6968
mps[i].obs = qml.simplify(mp.obs)
@@ -78,13 +77,11 @@ def _group_measurements(mps: list[Union[SampleMeasurement, ClassicalShadowMP, Sh
7877
else:
7978
mp_other_obs.append([mp])
8079
mp_other_obs_indices.append([i])
81-
8280
if mp_pauli_obs:
8381
i_to_pauli_mp = dict(mp_pauli_obs)
8482
_, group_indices = qml.pauli.group_observables(
8583
[mp.obs for mp in i_to_pauli_mp.values()], list(i_to_pauli_mp.keys())
8684
)
87-
8885
mp_pauli_groups = []
8986
for indices in group_indices:
9087
mp_group = [i_to_pauli_mp[i] for i in indices]
@@ -94,7 +91,6 @@ def _group_measurements(mps: list[Union[SampleMeasurement, ClassicalShadowMP, Sh
9491

9592
mp_no_obs_indices = [mp_no_obs_indices] if mp_no_obs else []
9693
mp_no_obs = [mp_no_obs] if mp_no_obs else []
97-
9894
all_mp_groups = mp_pauli_groups + mp_no_obs + mp_other_obs
9995
all_indices = group_indices + mp_no_obs_indices + mp_other_obs_indices
10096

@@ -240,7 +236,6 @@ def measure_with_samples(
240236
mps = measurements[0 : -len(mid_measurements)] if mid_measurements else measurements
241237

242238
groups, indices = _group_measurements(mps)
243-
244239
all_res = []
245240
for group in groups:
246241
if isinstance(group[0], ExpectationMP) and isinstance(

pennylane/ops/functions/dot.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def dot(
4949
grouping_type (str): The type of binary relation between Pauli words used to compute
5050
the grouping. Can be ``'qwc'``, ``'commuting'``, or ``'anticommuting'``. Note that if
5151
``pauli=True``, the grouping will be ignored.
52-
method (str): The graph coloring heuristic to use in solving minimum clique cover for
52+
method (str): The graph colouring heuristic to use in solving minimum clique cover for
5353
grouping, which can be ``'lf'`` (Largest First) or ``'rlf'`` (Recursive Largest
5454
First). This keyword argument is ignored if ``grouping_type`` is ``None``.
5555

pennylane/ops/op_math/linear_combination.py

+16-29
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,20 @@ class LinearCombination(Sum):
3838
coeffs (tensor_like): coefficients of the ``LinearCombination`` expression
3939
observables (Iterable[Observable]): observables in the ``LinearCombination`` expression, of same length as ``coeffs``
4040
simplify (bool): Specifies whether the ``LinearCombination`` is simplified upon initialization
41-
(like-terms are combined). The default value is `False`. Note that ``coeffs`` cannot
42-
be differentiated when using the ``'torch'`` interface and ``simplify=True``. Use of this argument is deprecated.
41+
(like-terms are combined). The default value is `False`. Note that ``coeffs`` cannot
42+
be differentiated when using the ``'torch'`` interface and ``simplify=True``. Use of this argument is deprecated.
4343
grouping_type (str): If not ``None``, compute and store information on how to group commuting
4444
observables upon initialization. This information may be accessed when a :class:`~.QNode` containing this
4545
``LinearCombination`` is executed on devices. The string refers to the type of binary relation between Pauli words.
4646
Can be ``'qwc'`` (qubit-wise commuting), ``'commuting'``, or ``'anticommuting'``.
47-
method (str): The graph coloring heuristic to use in solving minimum clique cover for grouping, which
48-
can be ``'lf'`` (Largest First) or ``'rlf'`` (Recursive Largest First). Ignored if ``grouping_type=None``.
47+
method (str): The graph colouring heuristic to use in solving minimum clique cover for grouping, which
48+
can be ``'lf'`` (Largest First), ``'rlf'`` (Recursive Largest First), ``'dsatur'`` (Degree of Saturation), or ``'gis'`` (IndependentSet).
49+
Defaults to ``'lf'``. Ignored if ``grouping_type=None``.
4950
id (str): name to be assigned to this ``LinearCombination`` instance
5051
52+
.. seealso:: `rustworkx.ColoringStrategy <https://www.rustworkx.org/apiref/rustworkx.ColoringStrategy.html#coloringstrategy>`_
53+
for more information on the ``('lf', 'dsatur', 'gis')`` strategies.
54+
5155
.. warning::
5256
The ``simplify`` argument is deprecated and will be removed in a future release.
5357
Instead, you can call ``qml.simplify`` on the constructed operator.
@@ -122,7 +126,7 @@ def __init__(
122126
observables: list[Operator],
123127
simplify=False,
124128
grouping_type=None,
125-
method="rlf",
129+
method="lf",
126130
_grouping_indices=None,
127131
_pauli_rep=None,
128132
id=None,
@@ -229,7 +233,7 @@ def terms(self):
229233
"""
230234
return self.coeffs, self.ops
231235

232-
def compute_grouping(self, grouping_type="qwc", method="rlf"):
236+
def compute_grouping(self, grouping_type="qwc", method="lf"):
233237
"""
234238
Compute groups of operators and coefficients corresponding to commuting
235239
observables of this ``LinearCombination``.
@@ -242,9 +246,10 @@ def compute_grouping(self, grouping_type="qwc", method="rlf"):
242246
Args:
243247
grouping_type (str): The type of binary relation between Pauli words used to compute
244248
the grouping. Can be ``'qwc'``, ``'commuting'``, or ``'anticommuting'``.
245-
method (str): The graph coloring heuristic to use in solving minimum clique cover for
249+
Defaults to ``'qwc'``.
250+
method (str): The graph colouring heuristic to use in solving minimum clique cover for
246251
grouping, which can be ``'lf'`` (Largest First) or ``'rlf'`` (Recursive Largest
247-
First).
252+
First). Defaults to ``'lf'``.
248253
249254
**Example**
250255
@@ -271,27 +276,9 @@ def compute_grouping(self, grouping_type="qwc", method="rlf"):
271276

272277
_, ops = self.terms()
273278

274-
with qml.QueuingManager.stop_recording():
275-
op_groups = qml.pauli.group_observables(ops, grouping_type=grouping_type, method=method)
276-
277-
ops = copy(ops)
278-
279-
indices = []
280-
available_indices = list(range(len(ops)))
281-
for partition in op_groups: # pylint:disable=too-many-nested-blocks
282-
indices_this_group = []
283-
for pauli_word in partition:
284-
# find index of this pauli word in remaining original observables,
285-
for ind, observable in enumerate(ops):
286-
if qml.pauli.are_identical_pauli_words(pauli_word, observable):
287-
indices_this_group.append(available_indices[ind])
288-
# delete this observable and its index, so it cannot be found again
289-
ops.pop(ind)
290-
available_indices.pop(ind)
291-
break
292-
indices.append(tuple(indices_this_group))
293-
294-
self._grouping_indices = tuple(indices)
279+
self._grouping_indices = qml.pauli.compute_partition_indices(
280+
ops, grouping_type=grouping_type, method=method
281+
)
295282

296283
@property
297284
def wires(self):

pennylane/ops/op_math/sum.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def sum(*summands, grouping_type=None, method="rlf", id=None, lazy=True):
4343
of the operators is already a sum operator, its operands (summands) will be used instead.
4444
grouping_type (str): The type of binary relation between Pauli words used to compute
4545
the grouping. Can be ``'qwc'``, ``'commuting'``, or ``'anticommuting'``.
46-
method (str): The graph coloring heuristic to use in solving minimum clique cover for
46+
method (str): The graph colouring heuristic to use in solving minimum clique cover for
4747
grouping, which can be ``'lf'`` (Largest First) or ``'rlf'`` (Recursive Largest
4848
First). This keyword argument is ignored if ``grouping_type`` is ``None``.
4949
@@ -129,7 +129,7 @@ class Sum(CompositeOp):
129129
Keyword Args:
130130
grouping_type (str): The type of binary relation between Pauli words used to compute
131131
the grouping. Can be ``'qwc'``, ``'commuting'``, or ``'anticommuting'``.
132-
method (str): The graph coloring heuristic to use in solving minimum clique cover for
132+
method (str): The graph colouring heuristic to use in solving minimum clique cover for
133133
grouping, which can be ``'lf'`` (Largest First) or ``'rlf'`` (Recursive Largest
134134
First). This keyword argument is ignored if ``grouping_type`` is ``None``.
135135
id (str or None): id for the sum operator. Default is None.
@@ -480,7 +480,7 @@ def compute_grouping(self, grouping_type="qwc", method="rlf"):
480480
Args:
481481
grouping_type (str): The type of binary relation between Pauli words used to compute
482482
the grouping. Can be ``'qwc'``, ``'commuting'``, or ``'anticommuting'``.
483-
method (str): The graph coloring heuristic to use in solving minimum clique cover for
483+
method (str): The graph colouring heuristic to use in solving minimum clique cover for
484484
grouping, which can be ``'lf'`` (Largest First) or ``'rlf'`` (Recursive Largest
485485
First).
486486

pennylane/ops/qubit/hamiltonian.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ class Hamiltonian(Observable):
8787
observables upon initialization. This information may be accessed when QNodes containing this
8888
Hamiltonian are executed on devices. The string refers to the type of binary relation between Pauli words.
8989
Can be ``'qwc'`` (qubit-wise commuting), ``'commuting'``, or ``'anticommuting'``.
90-
method (str): The graph coloring heuristic to use in solving minimum clique cover for grouping, which
90+
method (str): The graph colouring heuristic to use in solving minimum clique cover for grouping, which
9191
can be ``'lf'`` (Largest First) or ``'rlf'`` (Recursive Largest First). Ignored if ``grouping_type=None``.
9292
id (str): name to be assigned to this Hamiltonian instance
9393
@@ -476,7 +476,7 @@ def compute_grouping(
476476
Args:
477477
grouping_type (str): The type of binary relation between Pauli words used to compute the grouping.
478478
Can be ``'qwc'``, ``'commuting'``, or ``'anticommuting'``.
479-
method (str): The graph coloring heuristic to use in solving minimum clique cover for grouping, which
479+
method (str): The graph colouring heuristic to use in solving minimum clique cover for grouping, which
480480
can be ``'lf'`` (Largest First) or ``'rlf'`` (Recursive Largest First).
481481
"""
482482

pennylane/pauli/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
PauliGroupingStrategy,
4949
optimize_measurements,
5050
graph_colouring,
51+
compute_partition_indices,
5152
)
5253

5354
from .dla import PauliVSpace, lie_closure, structure_constants, center

pennylane/pauli/grouping/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@
1717
"""
1818

1919
from . import graph_colouring
20-
from .group_observables import PauliGroupingStrategy, group_observables
20+
from .group_observables import PauliGroupingStrategy, group_observables, compute_partition_indices
2121
from .optimize_measurements import optimize_measurements

0 commit comments

Comments
 (0)