-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Reorder Pauli terms before Trotterization #12925
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 21 commits
047fa2e
5c61160
758caff
4c53dbc
f0a6b3f
d98c019
098dd29
6fc1b49
1748ae9
84f937e
50e7d50
750b56a
9ccb34b
22fc36b
71cff0f
a588ec3
d7c5f67
56e619c
7df41d1
7089729
c192d87
80c4022
365f238
5d57a61
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,6 +60,7 @@ def __init__( | |
| | None | ||
| ) = None, | ||
| wrap: bool = False, | ||
| preserve_order: bool = False, | ||
| ) -> None: | ||
| """ | ||
| Args: | ||
|
|
@@ -79,8 +80,19 @@ def __init__( | |
| built. | ||
| wrap: Whether to wrap the atomic evolutions into custom gate objects. This only takes | ||
| effect when ``atomic_evolution is None``. | ||
| preserve_order: If ``False``, allows reordering the terms of the operator to | ||
| potentially yield a shallower evolution circuit. Not relevant | ||
| when synthesizing operator with a single term. | ||
|
Comment on lines
+83
to
+85
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think from a docs perspective it would be good to explain the tradeoffs here, especially around runtime performance. |
||
| """ | ||
| super().__init__(1, reps, insert_barriers, cx_structure, atomic_evolution, wrap) | ||
| super().__init__( | ||
| 1, | ||
| reps, | ||
| insert_barriers, | ||
| cx_structure, | ||
| atomic_evolution, | ||
| wrap, | ||
| preserve_order=preserve_order, | ||
| ) | ||
|
|
||
| @property | ||
| def settings(self) -> dict[str, Any]: | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -15,9 +15,13 @@ | |||||
| from __future__ import annotations | ||||||
|
|
||||||
| import inspect | ||||||
| from collections.abc import Callable | ||||||
| import itertools | ||||||
| from collections.abc import Callable, Sequence | ||||||
| from collections import defaultdict | ||||||
| from itertools import combinations | ||||||
| import typing | ||||||
| import numpy as np | ||||||
| import rustworkx as rx | ||||||
| from qiskit.circuit.parameterexpression import ParameterExpression | ||||||
| from qiskit.circuit.quantumcircuit import QuantumCircuit, ParameterValueType | ||||||
| from qiskit.quantum_info import SparsePauliOp, Pauli | ||||||
|
|
@@ -29,6 +33,8 @@ | |||||
| if typing.TYPE_CHECKING: | ||||||
| from qiskit.circuit.library import PauliEvolutionGate | ||||||
|
|
||||||
| SparsePauliLabel = typing.Tuple[str, list[int], ParameterValueType] | ||||||
|
|
||||||
|
|
||||||
| class ProductFormula(EvolutionSynthesis): | ||||||
| """Product formula base class for the decomposition of non-commuting operator exponentials. | ||||||
|
|
@@ -63,6 +69,7 @@ def __init__( | |||||
| | None | ||||||
| ) = None, | ||||||
| wrap: bool = False, | ||||||
| preserve_order: bool = False, | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same question here
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| ) -> None: | ||||||
| """ | ||||||
| Args: | ||||||
|
|
@@ -84,11 +91,15 @@ def __init__( | |||||
| wrap: Whether to wrap the atomic evolutions into custom gate objects. Note that setting | ||||||
| this to ``True`` is slower than ``False``. This only takes effect when | ||||||
| ``atomic_evolution is None``. | ||||||
| preserve_order: If ``False``, allows reordering the terms of the operator to | ||||||
| potentially yield a shallower evolution circuit. Not relevant | ||||||
| when synthesizing operator with a single term. | ||||||
| """ | ||||||
| super().__init__() | ||||||
| self.order = order | ||||||
| self.reps = reps | ||||||
| self.insert_barriers = insert_barriers | ||||||
| self.preserve_order = preserve_order | ||||||
|
|
||||||
| # user-provided atomic evolution, stored for serialization | ||||||
| self._atomic_evolution = atomic_evolution | ||||||
|
|
@@ -177,6 +188,7 @@ def settings(self) -> dict[str, typing.Any]: | |||||
| "insert_barriers": self.insert_barriers, | ||||||
| "cx_structure": self._cx_structure, | ||||||
| "wrap": self._wrap, | ||||||
| "preserve_order": self.preserve_order, | ||||||
| } | ||||||
|
|
||||||
| def _normalize_coefficients( | ||||||
|
|
@@ -239,3 +251,61 @@ def real_or_fail(value, tol=100): | |||||
| return np.real(value) | ||||||
|
|
||||||
| raise ValueError(f"Encountered complex value {value}, but expected real.") | ||||||
|
|
||||||
|
|
||||||
| def reorder_paulis( | ||||||
| paulis: Sequence[SparsePauliLabel], | ||||||
| strategy: rx.ColoringStrategy = rx.ColoringStrategy.Saturation, | ||||||
| ) -> list[SparsePauliLabel]: | ||||||
| r""" | ||||||
| Creates an equivalent operator by reordering terms in order to yield a | ||||||
| shallower circuit after evolution synthesis. The original operator remains | ||||||
| unchanged. | ||||||
|
|
||||||
| This method works in three steps. First, a graph is constructed, where the | ||||||
| nodes are the terms of the operator and where two nodes are connected if | ||||||
| their terms act on the same qubit (for example, the terms :math:`IXX` and | ||||||
| :math:`IYI` would be connected, but not :math:`IXX` and :math:`YII`). Then, | ||||||
| the graph is colored. Two terms with the same color thus do not act on the | ||||||
| same qubit, and in particular, their evolution subcircuits can be run in | ||||||
| parallel in the greater evolution circuit of ``paulis``. | ||||||
|
|
||||||
| This method is deterministic and invariant under permutation of the Pauli | ||||||
| term in ``paulis``. | ||||||
|
|
||||||
| Args: | ||||||
| paulis: The operator whose terms to reorder. | ||||||
| strategy: The coloring heuristic to use, see ``ColoringStrategy`` [#]. | ||||||
| Default is ``ColoringStrategy.Saturation``. | ||||||
|
|
||||||
| .. [#] https://www.rustworkx.org/apiref/rustworkx.ColoringStrategy.html#coloringstrategy | ||||||
|
|
||||||
| """ | ||||||
|
|
||||||
| def _term_sort_key(term: SparsePauliLabel) -> typing.Any: | ||||||
| # sort by index, then by pauli | ||||||
| return (term[1], term[0]) | ||||||
|
|
||||||
| # Do nothing in trivial cases | ||||||
| if len(paulis) <= 1: | ||||||
| return paulis | ||||||
|
|
||||||
| terms = sorted(paulis, key=_term_sort_key) | ||||||
| graph = rx.PyGraph() | ||||||
| graph.add_nodes_from(terms) | ||||||
| indexed_nodes = list(enumerate(graph.nodes())) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you don't actually need to cast this is as a list, it should all work fine without it and save a copy. |
||||||
| for (idx1, (_, ind1, _)), (idx2, (_, ind2, _)) in combinations(indexed_nodes, 2): | ||||||
| # Add an edge between two terms if they touch the same qubit | ||||||
| if len(set(ind1).intersection(ind2)) > 0: | ||||||
| graph.add_edge(idx1, idx2, None) | ||||||
|
|
||||||
| # rx.graph_greedy_color is supposed to be deterministic | ||||||
| coloring = rx.graph_greedy_color(graph, strategy=strategy) | ||||||
| terms_by_color = defaultdict(list) | ||||||
|
|
||||||
| for term_idx, color in sorted(coloring.items()): | ||||||
| term = graph.nodes()[term_idx] | ||||||
| terms_by_color[color].append(term) | ||||||
|
|
||||||
| terms = list(itertools.chain(*terms_by_color.values())) | ||||||
| return terms | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1463,11 +1463,14 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** | |
| class PauliEvolutionSynthesisDefault(HighLevelSynthesisPlugin): | ||
| """Synthesize a :class:`.PauliEvolutionGate` using the default synthesis algorithm. | ||
|
|
||
| This plugin name is :``PauliEvolution.default`` which can be used as the key on | ||
| This plugin name is:``PauliEvolution.default`` which can be used as the key on | ||
| an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. | ||
|
|
||
| The default synthesis simply calls the synthesis algorithm attached to a | ||
| PauliEvolutionGate. | ||
| The following plugin option can be set: | ||
|
|
||
| * preserve_order: If ``False``, allow re-ordering the Pauli terms in the Hamiltonian to | ||
| reduce the circuit depth of the decomposition. | ||
|
|
||
| """ | ||
|
|
||
| def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): | ||
|
|
@@ -1477,6 +1480,10 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** | |
| return None | ||
|
|
||
| algo = high_level_object.synthesis | ||
|
|
||
| if "preserve_order" in options and isinstance(algo, ProductFormula): | ||
| algo.preserve_order = options["preserve_order"] | ||
|
|
||
| return algo.synthesize(high_level_object) | ||
|
|
||
|
|
||
|
|
@@ -1528,6 +1535,9 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** | |
| ) | ||
| return None | ||
|
|
||
| if "preserve_order" in options and isinstance(algo, ProductFormula): | ||
| algo.preserve_order = options["preserve_order"] | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for adding this. A very small nitpick: we don't need another call to |
||
| num_qubits = high_level_object.num_qubits | ||
| pauli_network = algo.expand(high_level_object) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| --- | ||
| features_synthesis: | ||
| - | | ||
| Added a new argument ``preserve_order`` to :class:`.ProductFormula`, which allows | ||
| re-ordering the Pauli terms in the Hamiltonian before the product formula expansion, | ||
| to compress the final circuit depth. By setting this to ``False``, a term of form | ||
|
|
||
| .. math:: | ||
|
|
||
| Z_0 Z_1 + X_1 X_2 + Y_2 Y_3 | ||
|
|
||
| will be re-ordered to | ||
|
|
||
| .. math:: | ||
|
|
||
| Z_0 Z_1 + Y_2 Y_3 + X_1 X_2 | ||
|
|
||
| which will lead to the ``RZZ`` and ``RYY`` rotations being applied in parallel, instead | ||
| of three sequential rotations in the first part. | ||
|
|
||
| This option can be set via the plugin interface:: | ||
|
|
||
| from qiskit import QuantumCircuit, transpile | ||
| from qiskit.circuit.library import PauliEvolutionGate | ||
| from qiskit.quantum_info import SparsePauliOp | ||
| from qiskit.synthesis.evolution import SuzukiTrotter | ||
| from qiskit.transpiler.passes import HLSConfig | ||
|
|
||
| op = SparsePauliOp(["XXII", "IYYI", "IIZZ"]) | ||
| time, reps = 0.1, 1 | ||
|
|
||
| synthesis = SuzukiTrotter(order=2, reps=reps) | ||
| hls_config = HLSConfig(PauliEvolution=[("default", {"preserve_order": False})]) | ||
|
|
||
| circuit = QuantumCircuit(op.num_qubits) | ||
| circuit.append(PauliEvolutionGate(op, time), circuit.qubits) | ||
|
|
||
| tqc = transpile(circuit, basis_gates=["u", "cx"], hls_config=hls_config) | ||
| print(tqc.draw()) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought the consensus here was to not reorder terms by default because of the potential performance overhead. Shouldn't the default be to
True?