From 047fa2ed35a732481573d50dbe640e220c502513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20HT?= Date: Thu, 8 Aug 2024 17:24:59 +0900 Subject: [PATCH 01/20] Added option to reorder Paulis before Trotterization Reordering the terms of a Pauli operator can reduce the depth of the evolution circuit if using the Lie-Trotter or Suzuki-Trotter methods. This commit adds a method `synthesis.evolution.product_formula.reorder_paulis` that does just that, based on a greedy graph coloring heuristic. This commit also adds the `reorder` option in the `ProductFormula`, `LieTrotter` and `SuzukiTrotter` constructors to allow for reordering before synthesis. --- qiskit/synthesis/evolution/lie_trotter.py | 18 ++++- qiskit/synthesis/evolution/product_formula.py | 67 +++++++++++++++++++ qiskit/synthesis/evolution/suzuki_trotter.py | 22 +++++- 3 files changed, 102 insertions(+), 5 deletions(-) diff --git a/qiskit/synthesis/evolution/lie_trotter.py b/qiskit/synthesis/evolution/lie_trotter.py index 1a01675a6782..2262fc56cfcb 100644 --- a/qiskit/synthesis/evolution/lie_trotter.py +++ b/qiskit/synthesis/evolution/lie_trotter.py @@ -22,7 +22,7 @@ from qiskit.quantum_info.operators import SparsePauliOp, Pauli from qiskit.utils.deprecation import deprecate_arg -from .product_formula import ProductFormula +from .product_formula import ProductFormula, reorder_paulis class LieTrotter(ProductFormula): @@ -78,6 +78,7 @@ def __init__( | None ) = None, wrap: bool = False, + reorder: bool = False, ) -> None: """ Args: @@ -97,12 +98,25 @@ def __init__( built. wrap: Whether to wrap the atomic evolutions into custom gate objects. This only takes effect when ``atomic_evolution is None``. + reorder: Whether to allow reordering the terms of the operator to + potentially yield a shallower evolution circuit. Not relevant + when synthesizing operator with a single term. """ - super().__init__(1, reps, insert_barriers, cx_structure, atomic_evolution, wrap) + super().__init__( + 1, + reps, + insert_barriers, + cx_structure, + atomic_evolution, + wrap, + reorder=reorder, + ) def synthesize(self, evolution): # get operators and time to evolve operators = evolution.operator + if self.reorder: + operators = reorder_paulis(operators) time = evolution.time # construct the evolution circuit diff --git a/qiskit/synthesis/evolution/product_formula.py b/qiskit/synthesis/evolution/product_formula.py index df38a2a541ab..ef661ef96d13 100644 --- a/qiskit/synthesis/evolution/product_formula.py +++ b/qiskit/synthesis/evolution/product_formula.py @@ -16,9 +16,12 @@ import inspect from collections.abc import Callable +from collections import defaultdict +from itertools import combinations from typing import Any from functools import partial import numpy as np +import rustworkx as rx from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.quantum_info import SparsePauliOp, Pauli @@ -60,6 +63,7 @@ def __init__( | None ) = None, wrap: bool = False, + reorder: bool = False, ) -> None: """ Args: @@ -80,11 +84,15 @@ def __init__( built. wrap: Whether to wrap the atomic evolutions into custom gate objects. This only takes effect when ``atomic_evolution is None``. + reorder: Whether to allow 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.reorder = reorder # user-provided atomic evolution, stored for serialization self._atomic_evolution = atomic_evolution @@ -129,6 +137,7 @@ def settings(self) -> dict[str, Any]: "insert_barriers": self.insert_barriers, "cx_structure": self._cx_structure, "wrap": self._wrap, + "reorder": self.reorder, } @@ -177,6 +186,64 @@ def evolve_pauli( _multi_qubit_evolution(output, pauli, time, cx_structure, wrap) +def reorder_paulis( + operators: SparsePauliOp | list[SparsePauliOp], +) -> SparsePauliOp | list[SparsePauliOp]: + 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 term acts on the same qubit (for example, the terms $IXX$ and $IYI$ + would be connected, but not $IXX$ and $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 ``operator``. Finally, a new + :class:`~qiskit.quantum_info.SparsePauliOp` is created where terms of the + same color are grouped together. + + If the input is in fact a list of + :class:`~qiskit.quantum_info.SparsePauliOp`, then the terms of all operators + will be coalesced and reordered into a single + :class:`~qiskit.quantum_info.SparsePauliOp`. + + Args: + operators: The operator or list of operators whose terms to reorder. + """ + if not isinstance(operators, list): + operators = [operators] + # Do nothing in trivial cases + if not ( + # operators is a list of > 1 SparsePauliOp + len(operators) > 1 + # operators has a single SparsePauliOp, which has > 1 terms + or (len(operators) == 1 and len(operators[0]) > 1) + ): + return operators + graph = rx.PyGraph() + graph.add_nodes_from(sum((o.to_list() for o in operators), [])) + indexed_nodes = list(enumerate(graph.nodes())) + for (idx1, term1), (idx2, term2) in combinations(indexed_nodes, 2): + # Add an edge between term1 and term2 if they touch the same qubit + for a, b in zip(term1[0], term2[0]): + if not (a == "I" or b == "I"): + graph.add_edge(idx1, idx2, None) + break + # TODO: The graph is likely not very big, so an exact coloring could be + # computed in a reasonable time. + coloring = rx.graph_greedy_color(graph) + indices_by_color = defaultdict(list) + for term, color in coloring.items(): + indices_by_color[color].append(term) + terms: list[tuple[str, complex]] = [] + for color, indices in indices_by_color.items(): + for i in indices: + terms.append(graph.nodes()[i]) + return SparsePauliOp.from_list(terms) + + def _single_qubit_evolution(output, pauli, time, wrap): dest = QuantumCircuit(1) if wrap else output # Note that all phases are removed from the pauli label and are only in the coefficients. diff --git a/qiskit/synthesis/evolution/suzuki_trotter.py b/qiskit/synthesis/evolution/suzuki_trotter.py index e03fd27e26d4..93dc546d3abf 100644 --- a/qiskit/synthesis/evolution/suzuki_trotter.py +++ b/qiskit/synthesis/evolution/suzuki_trotter.py @@ -24,7 +24,7 @@ from qiskit.utils.deprecation import deprecate_arg -from .product_formula import ProductFormula +from .product_formula import ProductFormula, reorder_paulis class SuzukiTrotter(ProductFormula): @@ -82,6 +82,7 @@ def __init__( | None ) = None, wrap: bool = False, + reorder: bool = False, ) -> None: """ Args: @@ -101,6 +102,9 @@ def __init__( built. wrap: Whether to wrap the atomic evolutions into custom gate objects. This only takes effect when ``atomic_evolution is None``. + reorder: Whether to allow reordering the terms of the operator to + potentially yield a shallower evolution circuit. Not relevant + when synthesizing operator with a single term. Raises: ValueError: If order is not even """ @@ -110,11 +114,21 @@ def __init__( "Suzuki product formulae are symmetric and therefore only defined " "for even orders." ) - super().__init__(order, reps, insert_barriers, cx_structure, atomic_evolution, wrap) + super().__init__( + order, + reps, + insert_barriers, + cx_structure, + atomic_evolution, + wrap, + reorder=reorder, + ) def synthesize(self, evolution): # get operators and time to evolve operators = evolution.operator + if self.reorder: + operators = reorder_paulis(operators) time = evolution.time if not isinstance(operators, list): @@ -150,6 +164,8 @@ def _recurse(order, time, pauli_list): order - 2, time=reduction * time, pauli_list=pauli_list ) inner = SuzukiTrotter._recurse( - order - 2, time=(1 - 4 * reduction) * time, pauli_list=pauli_list + order - 2, + time=(1 - 4 * reduction) * time, + pauli_list=pauli_list, ) return outer + inner + outer From 5c61160469b90fd4f77d33952b0c38c00892a71b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20HT?= Date: Thu, 8 Aug 2024 17:25:17 +0900 Subject: [PATCH 02/20] Added unit tests for Pauli reordering --- .../circuit/library/test_evolution_gate.py | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/test/python/circuit/library/test_evolution_gate.py b/test/python/circuit/library/test_evolution_gate.py index a7c9a73abb5f..2c68e7bc9138 100644 --- a/test/python/circuit/library/test_evolution_gate.py +++ b/test/python/circuit/library/test_evolution_gate.py @@ -19,8 +19,16 @@ from qiskit.circuit import QuantumCircuit, Parameter from qiskit.circuit.library import PauliEvolutionGate -from qiskit.synthesis import LieTrotter, SuzukiTrotter, MatrixExponential, QDrift -from qiskit.synthesis.evolution.product_formula import cnot_chain, diagonalizing_clifford +from qiskit.synthesis import ( + LieTrotter, + SuzukiTrotter, + MatrixExponential, + QDrift, +) +from qiskit.synthesis.evolution.product_formula import ( + cnot_chain, + diagonalizing_clifford, +) from qiskit.converters import circuit_to_dag from qiskit.quantum_info import Operator, SparsePauliOp, Pauli, Statevector from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -133,6 +141,23 @@ def test_suzuki_trotter_manual(self): self.assertEqual(evo_gate.definition, expected) + def test_suzuki_trotter_reordered_manual(self): + """Test the evolution circuit of Suzuki Trotter against a manually constructed circuit.""" + op = (X ^ X ^ I ^ I) + (I ^ Y ^ Y ^ I) + (I ^ I ^ Z ^ Z) + time, reps = 0.1, 1 + evo_gate = PauliEvolutionGate( + op, + time, + synthesis=SuzukiTrotter(order=2, reps=reps, reorder=True), + ) + expected = QuantumCircuit(4) + expected.ryy(time, 1, 2) + expected.rxx(time, 2, 3) + expected.rzz(2 * time, 0, 1) + expected.rxx(time, 2, 3) + expected.ryy(time, 1, 2) + self.assertEqual(evo_gate.definition, expected) + @data( (X + Y, 0.5, 1, [(Pauli("X"), 0.5), (Pauli("X"), 0.5)]), (X, 0.238, 2, [(Pauli("X"), 0.238)]), @@ -299,6 +324,23 @@ def test_lie_trotter_two_qubit_correct_order(self): self.assertTrue(Operator(lie_trotter).equiv(exact)) + def test_lie_trotter_reordered_manual(self): + """Test the evolution circuit of Lie Trotter against a manually constructed circuit.""" + op = (X ^ I ^ I ^ I) + (X ^ X ^ I ^ I) + (I ^ Y ^ Y ^ I) + (I ^ I ^ Z ^ Z) + time, reps = 0.1, 1 + evo_gate = PauliEvolutionGate( + op, + time, + synthesis=LieTrotter(reps=reps, reorder=True), + ) + # manually construct expected evolution + expected = QuantumCircuit(4) + expected.rxx(2 * time, 2, 3) + expected.rzz(2 * time, 0, 1) + expected.rx(2 * time, 3) + expected.ryy(2 * time, 1, 2) + self.assertEqual(evo_gate.definition, expected) + def test_complex_op_raises(self): """Test an operator with complex coefficient raises an error.""" with self.assertRaises(ValueError): @@ -359,7 +401,9 @@ def atomic_evolution(pauli, time): reps = 4 with self.assertWarns(PendingDeprecationWarning): evo_gate = PauliEvolutionGate( - op, time, synthesis=LieTrotter(reps=reps, atomic_evolution=atomic_evolution) + op, + time, + synthesis=LieTrotter(reps=reps, atomic_evolution=atomic_evolution), ) decomposed = evo_gate.definition.decompose() self.assertEqual(decomposed.count_ops()["cx"], reps * 3 * 4) From 758caffeb969e26f771c6030653b01ee9f057ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20HT?= Date: Tue, 3 Sep 2024 20:30:45 +0900 Subject: [PATCH 03/20] reorder_paulis acts as the identity if the operator has only one term Before that, if reorder_paulis was given a single-term SparsePauliOp object, it would return it encapsulated in a list. Now, it returns it as is. --- qiskit/synthesis/evolution/product_formula.py | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/qiskit/synthesis/evolution/product_formula.py b/qiskit/synthesis/evolution/product_formula.py index ef661ef96d13..8090afa998bc 100644 --- a/qiskit/synthesis/evolution/product_formula.py +++ b/qiskit/synthesis/evolution/product_formula.py @@ -204,24 +204,31 @@ def reorder_paulis( :class:`~qiskit.quantum_info.SparsePauliOp` is created where terms of the same color are grouped together. - If the input is in fact a list of - :class:`~qiskit.quantum_info.SparsePauliOp`, then the terms of all operators - will be coalesced and reordered into a single - :class:`~qiskit.quantum_info.SparsePauliOp`. + In trivial cases, i.e. when either + - the input is a :class:`~qiskit.quantum_info.SparsePauliOp` with + less than two Pauli terms, or + - the input is a list containing a single + :class:`~qiskit.quantum_info.SparsePauliOp` which has less than two + Pauli terms, + this method does nothing. In non-trivial cases, this method always returns a + :class:`~qiskit.quantum_info.SparsePauliOp` (rather than a list of such), + even if ``operators`` is a list. Args: operators: The operator or list of operators whose terms to reorder. """ - if not isinstance(operators, list): - operators = [operators] # Do nothing in trivial cases - if not ( + if isinstance(operators, SparsePauliOp) and len(operators) <= 1: + return operators + if isinstance(operators, list) and not ( # operators is a list of > 1 SparsePauliOp len(operators) > 1 # operators has a single SparsePauliOp, which has > 1 terms or (len(operators) == 1 and len(operators[0]) > 1) ): return operators + if not isinstance(operators, list): + operators = [operators] graph = rx.PyGraph() graph.add_nodes_from(sum((o.to_list() for o in operators), [])) indexed_nodes = list(enumerate(graph.nodes())) From 4c53dbcd61c3550427d01322141533e6f3783b49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20HT?= Date: Wed, 4 Sep 2024 09:37:22 +0900 Subject: [PATCH 04/20] Reorder Pauli terms by default --- qiskit/synthesis/evolution/lie_trotter.py | 4 ++-- qiskit/synthesis/evolution/suzuki_trotter.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/synthesis/evolution/lie_trotter.py b/qiskit/synthesis/evolution/lie_trotter.py index 2262fc56cfcb..590fdbc50116 100644 --- a/qiskit/synthesis/evolution/lie_trotter.py +++ b/qiskit/synthesis/evolution/lie_trotter.py @@ -78,7 +78,7 @@ def __init__( | None ) = None, wrap: bool = False, - reorder: bool = False, + reorder: bool = True, ) -> None: """ Args: @@ -100,7 +100,7 @@ def __init__( effect when ``atomic_evolution is None``. reorder: Whether to allow reordering the terms of the operator to potentially yield a shallower evolution circuit. Not relevant - when synthesizing operator with a single term. + when synthesizing operator with a single term. Defaults to ``True``. """ super().__init__( 1, diff --git a/qiskit/synthesis/evolution/suzuki_trotter.py b/qiskit/synthesis/evolution/suzuki_trotter.py index 93dc546d3abf..4a3e67748539 100644 --- a/qiskit/synthesis/evolution/suzuki_trotter.py +++ b/qiskit/synthesis/evolution/suzuki_trotter.py @@ -82,7 +82,7 @@ def __init__( | None ) = None, wrap: bool = False, - reorder: bool = False, + reorder: bool = True, ) -> None: """ Args: @@ -104,7 +104,7 @@ def __init__( effect when ``atomic_evolution is None``. reorder: Whether to allow reordering the terms of the operator to potentially yield a shallower evolution circuit. Not relevant - when synthesizing operator with a single term. + when synthesizing operator with a single term. Defaults to ``True``. Raises: ValueError: If order is not even """ From f0a6b3f4de31b1a2a0ee3c622ddfd8d541942fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20HT?= Date: Thu, 5 Sep 2024 11:09:10 +0900 Subject: [PATCH 05/20] reorder_paulis is deterministic and order-invariant --- qiskit/synthesis/evolution/product_formula.py | 28 ++++++++++++------- .../circuit/library/test_evolution_gate.py | 27 ++++++++++++++++++ 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/qiskit/synthesis/evolution/product_formula.py b/qiskit/synthesis/evolution/product_formula.py index 8090afa998bc..8e5c0eed1532 100644 --- a/qiskit/synthesis/evolution/product_formula.py +++ b/qiskit/synthesis/evolution/product_formula.py @@ -214,9 +214,15 @@ def reorder_paulis( :class:`~qiskit.quantum_info.SparsePauliOp` (rather than a list of such), even if ``operators`` is a list. + This method is deterministic and invariant under permutation of the Pauli term in ``operators``. + Args: operators: The operator or list of operators whose terms to reorder. """ + + def _term_sort_key(term: tuple[str, complex]) -> Any: + return (term[0], term[1].real, term[1].imag) + # Do nothing in trivial cases if isinstance(operators, SparsePauliOp) and len(operators) <= 1: return operators @@ -229,8 +235,11 @@ def reorder_paulis( return operators if not isinstance(operators, list): operators = [operators] + + terms = sum((o.to_list() for o in operators), []) + terms = sorted(terms, key=_term_sort_key) graph = rx.PyGraph() - graph.add_nodes_from(sum((o.to_list() for o in operators), [])) + graph.add_nodes_from(terms) indexed_nodes = list(enumerate(graph.nodes())) for (idx1, term1), (idx2, term2) in combinations(indexed_nodes, 2): # Add an edge between term1 and term2 if they touch the same qubit @@ -238,16 +247,15 @@ def reorder_paulis( if not (a == "I" or b == "I"): graph.add_edge(idx1, idx2, None) break - # TODO: The graph is likely not very big, so an exact coloring could be - # computed in a reasonable time. + + # rx.graph_greedy_color is supposed to be deterministic coloring = rx.graph_greedy_color(graph) - indices_by_color = defaultdict(list) - for term, color in coloring.items(): - indices_by_color[color].append(term) - terms: list[tuple[str, complex]] = [] - for color, indices in indices_by_color.items(): - for i in indices: - terms.append(graph.nodes()[i]) + 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 = sum(terms_by_color.values(), []) return SparsePauliOp.from_list(terms) diff --git a/test/python/circuit/library/test_evolution_gate.py b/test/python/circuit/library/test_evolution_gate.py index 2c68e7bc9138..d19da224e22f 100644 --- a/test/python/circuit/library/test_evolution_gate.py +++ b/test/python/circuit/library/test_evolution_gate.py @@ -12,6 +12,8 @@ """Test the evolution gate.""" +from itertools import permutations + import unittest import numpy as np import scipy @@ -28,6 +30,7 @@ from qiskit.synthesis.evolution.product_formula import ( cnot_chain, diagonalizing_clifford, + reorder_paulis, ) from qiskit.converters import circuit_to_dag from qiskit.quantum_info import Operator, SparsePauliOp, Pauli, Statevector @@ -60,6 +63,30 @@ def test_matrix_decomposition(self): self.assertTrue(Operator(evo_gate).equiv(evolved)) + def test_reorder_paulis_invariant(self): + """ + Tests that reorder_paulis is deterministic and does not depend on the + order of the terms of the input operator. + """ + terms = [ + (I ^ I ^ X ^ X), + (I ^ I ^ Z ^ Z), + (I ^ Y ^ Y ^ I), + (X ^ I ^ I ^ I), + (X ^ X ^ I ^ I), + (Y ^ I ^ I ^ Y), + ] + results = [] + for seed, tms in enumerate(permutations(terms)): + np.random.seed(seed) + op = reorder_paulis(SparsePauliOp(sum(tms))) + results.append([t[0] for t in op.to_list()]) + np.random.seed(seed + 42) + op = reorder_paulis(SparsePauliOp(sum(tms))) + results.append([t[0] for t in op.to_list()]) + for lst in results[1:]: + self.assertListEqual(lst, results[0]) + def test_lie_trotter(self): """Test constructing the circuit with Lie Trotter decomposition.""" op = (X ^ X ^ X) + (Y ^ Y ^ Y) + (Z ^ Z ^ Z) From d98c019651b286638124139afc30dfa11a5de296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20HT?= Date: Thu, 5 Sep 2024 11:33:43 +0900 Subject: [PATCH 06/20] Fixed SuzukiTrotter unit tests --- test/python/circuit/library/test_evolution_gate.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/test/python/circuit/library/test_evolution_gate.py b/test/python/circuit/library/test_evolution_gate.py index d19da224e22f..c849ffb3d3cc 100644 --- a/test/python/circuit/library/test_evolution_gate.py +++ b/test/python/circuit/library/test_evolution_gate.py @@ -141,12 +141,14 @@ def test_suzuki_trotter(self): decomposed = evo_gate.definition.decompose() self.assertEqual(decomposed.count_ops()["cx"], expected_cx) - def test_suzuki_trotter_manual(self): + def test_suzuki_trotter_manual_no_reorder(self): """Test the evolution circuit of Suzuki Trotter against a manually constructed circuit.""" op = X + Y time = 0.1 reps = 1 - evo_gate = PauliEvolutionGate(op, time, synthesis=SuzukiTrotter(order=4, reps=reps)) + evo_gate = PauliEvolutionGate( + op, time, synthesis=SuzukiTrotter(order=4, reps=reps, reorder=False) + ) # manually construct expected evolution expected = QuantumCircuit(1) @@ -168,7 +170,7 @@ def test_suzuki_trotter_manual(self): self.assertEqual(evo_gate.definition, expected) - def test_suzuki_trotter_reordered_manual(self): + def test_suzuki_trotter_manual(self): """Test the evolution circuit of Suzuki Trotter against a manually constructed circuit.""" op = (X ^ X ^ I ^ I) + (I ^ Y ^ Y ^ I) + (I ^ I ^ Z ^ Z) time, reps = 0.1, 1 @@ -178,11 +180,11 @@ def test_suzuki_trotter_reordered_manual(self): synthesis=SuzukiTrotter(order=2, reps=reps, reorder=True), ) expected = QuantumCircuit(4) - expected.ryy(time, 1, 2) + expected.rzz(time, 0, 1) expected.rxx(time, 2, 3) - expected.rzz(2 * time, 0, 1) + expected.ryy(2 * time, 1, 2) expected.rxx(time, 2, 3) - expected.ryy(time, 1, 2) + expected.rzz(time, 0, 1) self.assertEqual(evo_gate.definition, expected) @data( From 098dd29dcac23273d4f70c738b172d74782324a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20HT?= Date: Thu, 12 Sep 2024 11:52:01 +0900 Subject: [PATCH 07/20] No Pauli reordering by default --- qiskit/synthesis/evolution/lie_trotter.py | 4 ++-- qiskit/synthesis/evolution/suzuki_trotter.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/synthesis/evolution/lie_trotter.py b/qiskit/synthesis/evolution/lie_trotter.py index 590fdbc50116..11e4931fa013 100644 --- a/qiskit/synthesis/evolution/lie_trotter.py +++ b/qiskit/synthesis/evolution/lie_trotter.py @@ -78,7 +78,7 @@ def __init__( | None ) = None, wrap: bool = False, - reorder: bool = True, + reorder: bool = False, ) -> None: """ Args: @@ -100,7 +100,7 @@ def __init__( effect when ``atomic_evolution is None``. reorder: Whether to allow reordering the terms of the operator to potentially yield a shallower evolution circuit. Not relevant - when synthesizing operator with a single term. Defaults to ``True``. + when synthesizing operator with a single term. Defaults to ``False``. """ super().__init__( 1, diff --git a/qiskit/synthesis/evolution/suzuki_trotter.py b/qiskit/synthesis/evolution/suzuki_trotter.py index 4a3e67748539..704b5b5b93ed 100644 --- a/qiskit/synthesis/evolution/suzuki_trotter.py +++ b/qiskit/synthesis/evolution/suzuki_trotter.py @@ -82,7 +82,7 @@ def __init__( | None ) = None, wrap: bool = False, - reorder: bool = True, + reorder: bool = False, ) -> None: """ Args: @@ -104,7 +104,7 @@ def __init__( effect when ``atomic_evolution is None``. reorder: Whether to allow reordering the terms of the operator to potentially yield a shallower evolution circuit. Not relevant - when synthesizing operator with a single term. Defaults to ``True``. + when synthesizing operator with a single term. Defaults to ``False``. Raises: ValueError: If order is not even """ From 6fc1b4950db962904f90ed780f03db52e6a9295a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20HT?= Date: Thu, 12 Sep 2024 12:10:49 +0900 Subject: [PATCH 08/20] Added Pauli reordering option in QDrift --- qiskit/synthesis/evolution/qdrift.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/qiskit/synthesis/evolution/qdrift.py b/qiskit/synthesis/evolution/qdrift.py index 7c68f66dc8ea..42ea7a4edec0 100644 --- a/qiskit/synthesis/evolution/qdrift.py +++ b/qiskit/synthesis/evolution/qdrift.py @@ -63,6 +63,7 @@ def __init__( ) = None, seed: int | None = None, wrap: bool = False, + reorder: bool = False, ) -> None: r""" Args: @@ -83,10 +84,15 @@ def __init__( seed: An optional seed for reproducibility of the random sampling process. wrap: Whether to wrap the atomic evolutions into custom gate objects. This only takes effect when ``atomic_evolution is None``. + reorder: Whether to allow reordering the terms of the sampled + operator before synthesizing the evolution circuit. Setting this + to ``True`` can potentially yield a shallower evolution circuit. + Defaults to ``False``. """ super().__init__(1, reps, insert_barriers, cx_structure, atomic_evolution, wrap) self.sampled_ops = None self.rng = np.random.default_rng(seed) + self.reorder = reorder def synthesize(self, evolution): # get operators and time to evolve @@ -120,7 +126,9 @@ def synthesize(self, evolution): # Build the evolution circuit using the LieTrotter synthesis with the sampled operators lie_trotter = LieTrotter( - insert_barriers=self.insert_barriers, atomic_evolution=self.atomic_evolution + insert_barriers=self.insert_barriers, + atomic_evolution=self.atomic_evolution, + reorder=self.reorder, ) evolution_circuit = PauliEvolutionGate( sum(SparsePauliOp(np.sign(coeff) * op) for op, coeff in self.sampled_ops), From 1748ae94e6797ecfd964d3931444cdc38426d22e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20HT?= Date: Thu, 12 Sep 2024 18:47:14 +0900 Subject: [PATCH 09/20] Typset math correctly --- qiskit/synthesis/evolution/product_formula.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/qiskit/synthesis/evolution/product_formula.py b/qiskit/synthesis/evolution/product_formula.py index 8e5c0eed1532..d0a1f61df99d 100644 --- a/qiskit/synthesis/evolution/product_formula.py +++ b/qiskit/synthesis/evolution/product_formula.py @@ -196,11 +196,11 @@ def reorder_paulis( 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 term acts on the same qubit (for example, the terms $IXX$ and $IYI$ - would be connected, but not $IXX$ and $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 ``operator``. Finally, a new + their term acts 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 ``operator``. Finally, a new :class:`~qiskit.quantum_info.SparsePauliOp` is created where terms of the same color are grouped together. @@ -214,7 +214,8 @@ def reorder_paulis( :class:`~qiskit.quantum_info.SparsePauliOp` (rather than a list of such), even if ``operators`` is a list. - This method is deterministic and invariant under permutation of the Pauli term in ``operators``. + This method is deterministic and invariant under permutation of the Pauli + term in ``operators``. Args: operators: The operator or list of operators whose terms to reorder. From 84f937ef766237a86de0cca42d2b6148407af82f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20HT?= Date: Thu, 12 Sep 2024 19:06:33 +0900 Subject: [PATCH 10/20] Reorder ops individually If `reorder_paulis` is given a list of operators, then they are reordered and returned individually. Before this co mmit, they used to be added all together and then reordered, which always resulted in a single operator. --- qiskit/synthesis/evolution/product_formula.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/qiskit/synthesis/evolution/product_formula.py b/qiskit/synthesis/evolution/product_formula.py index d0a1f61df99d..80a5f4dfa363 100644 --- a/qiskit/synthesis/evolution/product_formula.py +++ b/qiskit/synthesis/evolution/product_formula.py @@ -210,9 +210,11 @@ def reorder_paulis( - the input is a list containing a single :class:`~qiskit.quantum_info.SparsePauliOp` which has less than two Pauli terms, - this method does nothing. In non-trivial cases, this method always returns a - :class:`~qiskit.quantum_info.SparsePauliOp` (rather than a list of such), - even if ``operators`` is a list. + this method does nothing. + + If ``operators`` is a list of :class:`~qiskit.quantum_info.SparsePauliOp`, + then reordering is applied to every operator independently, and the list of + reordered operators is returned. This method is deterministic and invariant under permutation of the Pauli term in ``operators``. @@ -234,11 +236,11 @@ def _term_sort_key(term: tuple[str, complex]) -> Any: or (len(operators) == 1 and len(operators[0]) > 1) ): return operators - if not isinstance(operators, list): - operators = [operators] - terms = sum((o.to_list() for o in operators), []) - terms = sorted(terms, key=_term_sort_key) + if isinstance(operators, (list, tuple)): + return [reorder_paulis(op) for op in operators] + + terms = sorted(operators.to_list(), key=_term_sort_key) graph = rx.PyGraph() graph.add_nodes_from(terms) indexed_nodes = list(enumerate(graph.nodes())) From 50e7d50567cca2917734624ff7490c7e16d674e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20HT?= Date: Fri, 13 Sep 2024 09:47:10 +0900 Subject: [PATCH 11/20] Option to change graph coloring heuristic --- qiskit/synthesis/evolution/product_formula.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qiskit/synthesis/evolution/product_formula.py b/qiskit/synthesis/evolution/product_formula.py index 80a5f4dfa363..abecc9aa61b9 100644 --- a/qiskit/synthesis/evolution/product_formula.py +++ b/qiskit/synthesis/evolution/product_formula.py @@ -188,6 +188,7 @@ def evolve_pauli( def reorder_paulis( operators: SparsePauliOp | list[SparsePauliOp], + strategy: rx.ColoringStrategy = rx.ColoringStrategy.Degree, ) -> SparsePauliOp | list[SparsePauliOp]: r""" Creates an equivalent operator by reordering terms in order to yield a @@ -221,6 +222,9 @@ def reorder_paulis( Args: operators: The operator or list of operators whose terms to reorder. + strategy: The coloring heuristic to use. See + [``rx.ColoringStrategy``](https://www.rustworkx.org/apiref/rustworkx.ColoringStrategy.html#coloringstrategy). + Default is ``rx.ColoringStrategy.Degree``. """ def _term_sort_key(term: tuple[str, complex]) -> Any: @@ -252,7 +256,7 @@ def _term_sort_key(term: tuple[str, complex]) -> Any: break # rx.graph_greedy_color is supposed to be deterministic - coloring = rx.graph_greedy_color(graph) + 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] From 9ccb34b971086e80952312b03cd5845e5b274bc3 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 6 Nov 2024 15:42:53 +0100 Subject: [PATCH 12/20] add plugin --- qiskit/circuit/library/pauli_evolution.py | 27 +++++++++++---- qiskit/synthesis/evolution/product_formula.py | 5 +-- .../passes/synthesis/hls_plugins.py | 32 ++++++++++++++++-- .../circuit/library/test_evolution_gate.py | 33 +++++++++++++++---- 4 files changed, 79 insertions(+), 18 deletions(-) diff --git a/qiskit/circuit/library/pauli_evolution.py b/qiskit/circuit/library/pauli_evolution.py index f7ab0393f32c..12bf4cc9615e 100644 --- a/qiskit/circuit/library/pauli_evolution.py +++ b/qiskit/circuit/library/pauli_evolution.py @@ -110,18 +110,24 @@ class docstring for an example. else: operator = _to_sparse_pauli_op(operator) - if synthesis is None: - from qiskit.synthesis.evolution import LieTrotter - - synthesis = LieTrotter() - if label is None: label = _get_default_label(operator) num_qubits = operator[0].num_qubits if isinstance(operator, list) else operator.num_qubits super().__init__(name="PauliEvolution", num_qubits=num_qubits, params=[time], label=label) self.operator = operator - self.synthesis = synthesis + self._synthesis = synthesis + + @property + def synthesis(self) -> EvolutionSynthesis: + """Return the synthesis used.""" + if self._synthesis is not None: + return self._synthesis + + # pylint: disable=cyclic-import + from qiskit.synthesis.evolution import LieTrotter + + return LieTrotter() @property def time(self) -> ParameterValueType: @@ -143,7 +149,14 @@ def time(self, time: ParameterValueType) -> None: def _define(self): """Unroll, where the default synthesis is matrix based.""" - self.definition = self.synthesis.synthesize(self) + if self.synthesis is None: + from qiskit.synthesis.evolution import LieTrotter + + synthesis = LieTrotter() + else: + synthesis = self.synthesis + + self.definition = synthesis.synthesize(self) def validate_parameter(self, parameter: ParameterValueType) -> ParameterValueType: """Gate parameters should be int, float, or ParameterExpression""" diff --git a/qiskit/synthesis/evolution/product_formula.py b/qiskit/synthesis/evolution/product_formula.py index fc2828706d84..68e548177b80 100644 --- a/qiskit/synthesis/evolution/product_formula.py +++ b/qiskit/synthesis/evolution/product_formula.py @@ -253,8 +253,7 @@ def real_or_fail(value, tol=100): def reorder_paulis( - paulis: SparsePauliLabel, - strategy: rx.ColoringStrategy = rx.ColoringStrategy.Degree, + paulis: SparsePauliLabel, strategy: rx.ColoringStrategy = rx.ColoringStrategy.Saturation ) -> SparsePauliOp | list[SparsePauliOp]: r""" Creates an equivalent operator by reordering terms in order to yield a @@ -313,8 +312,10 @@ def _term_sort_key(term: SparsePauliLabel) -> typing.Any: # 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] + print(color, term) terms_by_color[color].append(term) terms = sum(terms_by_color.values(), []) diff --git a/qiskit/transpiler/passes/synthesis/hls_plugins.py b/qiskit/transpiler/passes/synthesis/hls_plugins.py index 3e26809a55f7..43fb7280476f 100644 --- a/qiskit/transpiler/passes/synthesis/hls_plugins.py +++ b/qiskit/transpiler/passes/synthesis/hls_plugins.py @@ -282,6 +282,7 @@ synth_clifford_ag, synth_clifford_bm, ) +from qiskit.synthesis.evolution import SuzukiTrotter from qiskit.synthesis.linear import ( synth_cnot_count_full_pmh, synth_cnot_depth_line_kms, @@ -1047,8 +1048,35 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** class PauliEvolutionSynthesisDefault(HighLevelSynthesisPlugin): - """The default implementation calling the attached synthesis algorithm.""" + """The default implementation calling the attached synthesis algorithm. + + 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`. + + If the :class:`.PauliEvolutionGate` does not have the ``synthesis`` property + set, the plugin uses :class:`.SuzukiTrotter` ad supports the following options: + + * order: The order of the Suzuki-Trotter formula (an even number, or 1). + * reps: The number of Trotter time steps. + * reorder: If ``True``, allow re-ordering the Pauli terms in the Hamiltonian to + reduce the circuit depth of the decomposition. + * insert_barriers: If ``True``, insert barriers in between the Pauli term evolutions. + * cx_structure: How to arrange the CX gates for the Pauli evolution. Can be + ``"chain"`` (default) or ``"fountain"``. + * wrap: If ``True``, wrap the Pauli evolutions into gates. This comes with a performance + cost. + """ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): - algo = high_level_object.synthesis + algo = high_level_object._synthesis + + if algo is None: + algo = SuzukiTrotter( + order=options.get("order", 1), + reps=options.get("reps", 1), + reorder=options.get("reorder", False), + cx_structure=options.get("cx_structure", "chain"), + wrap=options.get("wrap", False), + ) + return algo.synthesize(high_level_object) diff --git a/test/python/circuit/library/test_evolution_gate.py b/test/python/circuit/library/test_evolution_gate.py index 694b97739cf6..3a227f9188cf 100644 --- a/test/python/circuit/library/test_evolution_gate.py +++ b/test/python/circuit/library/test_evolution_gate.py @@ -25,6 +25,7 @@ from qiskit.synthesis.evolution.product_formula import reorder_paulis from qiskit.converters import circuit_to_dag from qiskit.quantum_info import Operator, SparsePauliOp, Pauli, Statevector +from qiskit.transpiler.passes import HLSConfig, HighLevelSynthesis from test import QiskitTestCase # pylint: disable=wrong-import-order X = SparsePauliOp("X") @@ -186,22 +187,40 @@ def test_suzuki_trotter_manual_no_reorder(self): self.assertEqual(evo_gate.definition, expected) self.assertSuzukiTrotterIsCorrect(evo_gate) - def test_suzuki_trotter_manual(self): + @data(True, False) + def test_suzuki_trotter_manual(self, use_plugin): """Test the evolution circuit of Suzuki Trotter against a manually constructed circuit.""" op = (X ^ X ^ I ^ I) + (I ^ Y ^ Y ^ I) + (I ^ I ^ Z ^ Z) time, reps = 0.1, 1 - evo_gate = PauliEvolutionGate( - op, - time, - synthesis=SuzukiTrotter(order=2, reps=reps, reorder=True), - ) + + if use_plugin: + synthesis = None + hls_config = HLSConfig( + PauliEvolution=[("default", {"reorder": True, "order": 2, "reps": reps})] + ) + else: + synthesis = SuzukiTrotter(order=2, reps=reps, reorder=True) + hls_config = None + + evo_gate = PauliEvolutionGate(op, time, synthesis=synthesis) + circuit = QuantumCircuit(op.num_qubits) + circuit.append(evo_gate, circuit.qubits) + + if use_plugin: + decomposed = HighLevelSynthesis(hls_config=hls_config)(circuit) + else: + decomposed = circuit.decompose() + expected = QuantumCircuit(4) expected.rzz(time, 0, 1) expected.rxx(time, 2, 3) expected.ryy(2 * time, 1, 2) expected.rxx(time, 2, 3) expected.rzz(time, 0, 1) - self.assertEqual(evo_gate.definition, expected) + self.assertEqual(decomposed, expected) + + def test_suzuki_trotter_plugin(self): + """Test setting options via the plugin.""" @data( (X + Y, 0.5, 1, [(Pauli("X"), 0.5), (Pauli("X"), 0.5)]), From 22fc36b291b67a95f43d2096ca51a43fb7888f4d Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 6 Nov 2024 17:09:46 +0100 Subject: [PATCH 13/20] consistent with rustiq --- qiskit/synthesis/evolution/lie_trotter.py | 6 ++--- qiskit/synthesis/evolution/product_formula.py | 16 +++++------ qiskit/synthesis/evolution/suzuki_trotter.py | 8 +++--- .../passes/synthesis/hls_plugins.py | 27 +++++-------------- .../circuit/library/test_evolution_gate.py | 12 ++++----- 5 files changed, 27 insertions(+), 42 deletions(-) diff --git a/qiskit/synthesis/evolution/lie_trotter.py b/qiskit/synthesis/evolution/lie_trotter.py index 087d043e374c..c28e3ab1d0ed 100644 --- a/qiskit/synthesis/evolution/lie_trotter.py +++ b/qiskit/synthesis/evolution/lie_trotter.py @@ -60,7 +60,7 @@ def __init__( | None ) = None, wrap: bool = False, - reorder: bool = False, + preserve_order: bool = False, ) -> None: """ Args: @@ -80,7 +80,7 @@ def __init__( built. wrap: Whether to wrap the atomic evolutions into custom gate objects. This only takes effect when ``atomic_evolution is None``. - reorder: Whether to allow reordering the terms of the operator to + preserve_order: Whether to allow preserve_ordering the terms of the operator to potentially yield a shallower evolution circuit. Not relevant when synthesizing operator with a single term. Defaults to ``False``. """ @@ -91,7 +91,7 @@ def __init__( cx_structure, atomic_evolution, wrap, - reorder=reorder, + preserve_order=preserve_order, ) @property diff --git a/qiskit/synthesis/evolution/product_formula.py b/qiskit/synthesis/evolution/product_formula.py index 68e548177b80..fd02019ff09f 100644 --- a/qiskit/synthesis/evolution/product_formula.py +++ b/qiskit/synthesis/evolution/product_formula.py @@ -68,7 +68,7 @@ def __init__( | None ) = None, wrap: bool = False, - reorder: bool = False, + preserve_order: bool = False, ) -> None: """ Args: @@ -90,7 +90,7 @@ 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``. - reorder: Whether to allow reordering the terms of the operator to + preserve_order: Whether to allow preserve_ordering the terms of the operator to potentially yield a shallower evolution circuit. Not relevant when synthesizing operator with a single term. """ @@ -98,7 +98,7 @@ def __init__( self.order = order self.reps = reps self.insert_barriers = insert_barriers - self.reorder = reorder + self.preserve_order = preserve_order # user-provided atomic evolution, stored for serialization self._atomic_evolution = atomic_evolution @@ -187,7 +187,7 @@ def settings(self) -> dict[str, typing.Any]: "insert_barriers": self.insert_barriers, "cx_structure": self._cx_structure, "wrap": self._wrap, - "reorder": self.reorder, + "preserve_order": self.preserve_order, } def _normalize_coefficients( @@ -256,7 +256,7 @@ def reorder_paulis( paulis: SparsePauliLabel, strategy: rx.ColoringStrategy = rx.ColoringStrategy.Saturation ) -> SparsePauliOp | list[SparsePauliOp]: r""" - Creates an equivalent operator by reordering terms in order to yield a + Creates an equivalent operator by preserve_ordering terms in order to yield a shallower circuit after evolution synthesis. The original operator remains unchanged. @@ -279,14 +279,14 @@ def reorder_paulis( this method does nothing. If ``operators`` is a list of :class:`~qiskit.quantum_info.SparsePauliOp`, - then reordering is applied to every operator independently, and the list of - reordered operators is returned. + then preserve_ordering is applied to every operator independently, and the list of + preserve_ordered operators is returned. This method is deterministic and invariant under permutation of the Pauli term in ``operators``. Args: - paulis: The operator whose terms to reorder. + paulis: The operator whose terms to preserve_order. strategy: The coloring heuristic to use. See [``rx.ColoringStrategy``](https://www.rustworkx.org/apiref/rustworkx.ColoringStrategy.html#coloringstrategy). Default is ``rx.ColoringStrategy.Degree``. diff --git a/qiskit/synthesis/evolution/suzuki_trotter.py b/qiskit/synthesis/evolution/suzuki_trotter.py index 5a37a1daedf9..d725241a285d 100644 --- a/qiskit/synthesis/evolution/suzuki_trotter.py +++ b/qiskit/synthesis/evolution/suzuki_trotter.py @@ -85,7 +85,7 @@ def __init__( | None ) = None, wrap: bool = False, - reorder: bool = False, + preserve_order: bool = False, ) -> None: """ Args: @@ -105,7 +105,7 @@ def __init__( built. wrap: Whether to wrap the atomic evolutions into custom gate objects. This only takes effect when ``atomic_evolution is None``. - reorder: Whether to allow reordering the terms of the operator to + preserve_order: Whether to allow reordering the terms of the operator to potentially yield a shallower evolution circuit. Not relevant when synthesizing operator with a single term. Defaults to ``False``. Raises: @@ -124,7 +124,7 @@ def __init__( cx_structure, atomic_evolution, wrap, - reorder=reorder, + preserve_order=preserve_order, ) def expand( @@ -158,7 +158,7 @@ def expand( def to_sparse_list(operator): paulis = (time * (2 / self.reps) * operator).to_sparse_list() - if self.reorder: + if not self.preserve_order: return reorder_paulis(paulis) return paulis diff --git a/qiskit/transpiler/passes/synthesis/hls_plugins.py b/qiskit/transpiler/passes/synthesis/hls_plugins.py index 43fb7280476f..3ffe974a41cc 100644 --- a/qiskit/transpiler/passes/synthesis/hls_plugins.py +++ b/qiskit/transpiler/passes/synthesis/hls_plugins.py @@ -1053,30 +1053,17 @@ class PauliEvolutionSynthesisDefault(HighLevelSynthesisPlugin): 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`. - If the :class:`.PauliEvolutionGate` does not have the ``synthesis`` property - set, the plugin uses :class:`.SuzukiTrotter` ad supports the following options: + The following plugin option can be set: - * order: The order of the Suzuki-Trotter formula (an even number, or 1). - * reps: The number of Trotter time steps. - * reorder: If ``True``, allow re-ordering the Pauli terms in the Hamiltonian to + * preserve_order: If ``True``, allow re-ordering the Pauli terms in the Hamiltonian to reduce the circuit depth of the decomposition. - * insert_barriers: If ``True``, insert barriers in between the Pauli term evolutions. - * cx_structure: How to arrange the CX gates for the Pauli evolution. Can be - ``"chain"`` (default) or ``"fountain"``. - * wrap: If ``True``, wrap the Pauli evolutions into gates. This comes with a performance - cost. + """ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): - algo = high_level_object._synthesis - - if algo is None: - algo = SuzukiTrotter( - order=options.get("order", 1), - reps=options.get("reps", 1), - reorder=options.get("reorder", False), - cx_structure=options.get("cx_structure", "chain"), - wrap=options.get("wrap", False), - ) + algo = high_level_object.synthesis + + if "preserve_order" in options: + algo.preserve_order = options["preserve_order"] return algo.synthesize(high_level_object) diff --git a/test/python/circuit/library/test_evolution_gate.py b/test/python/circuit/library/test_evolution_gate.py index 3a227f9188cf..1b55ea920289 100644 --- a/test/python/circuit/library/test_evolution_gate.py +++ b/test/python/circuit/library/test_evolution_gate.py @@ -163,7 +163,7 @@ def test_suzuki_trotter_manual_no_reorder(self): time = 0.1 reps = 1 evo_gate = PauliEvolutionGate( - op, time, synthesis=SuzukiTrotter(order=4, reps=reps, reorder=False) + op, time, synthesis=SuzukiTrotter(order=4, reps=reps, preserve_order=True) ) # manually construct expected evolution @@ -193,13 +193,11 @@ def test_suzuki_trotter_manual(self, use_plugin): op = (X ^ X ^ I ^ I) + (I ^ Y ^ Y ^ I) + (I ^ I ^ Z ^ Z) time, reps = 0.1, 1 + synthesis = SuzukiTrotter(order=2, reps=reps) if use_plugin: - synthesis = None - hls_config = HLSConfig( - PauliEvolution=[("default", {"reorder": True, "order": 2, "reps": reps})] - ) + hls_config = HLSConfig(PauliEvolution=[("default", {"preserve_order": False})]) else: - synthesis = SuzukiTrotter(order=2, reps=reps, reorder=True) + synthesis.preserve_order = False hls_config = None evo_gate = PauliEvolutionGate(op, time, synthesis=synthesis) @@ -398,7 +396,7 @@ def test_lie_trotter_reordered_manual(self): evo_gate = PauliEvolutionGate( op, time, - synthesis=LieTrotter(reps=reps, reorder=True), + synthesis=LieTrotter(reps=reps, preserve_order=False), ) # manually construct expected evolution expected = QuantumCircuit(4) From 71cff0f0333065d65ecb6e7be40c1ca7899ecde5 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 6 Nov 2024 17:45:04 +0100 Subject: [PATCH 14/20] rm print, fix sorting --- qiskit/synthesis/evolution/product_formula.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/qiskit/synthesis/evolution/product_formula.py b/qiskit/synthesis/evolution/product_formula.py index fd02019ff09f..3a9f18d1d62e 100644 --- a/qiskit/synthesis/evolution/product_formula.py +++ b/qiskit/synthesis/evolution/product_formula.py @@ -293,8 +293,8 @@ def reorder_paulis( """ def _term_sort_key(term: SparsePauliLabel) -> typing.Any: - # sort by index, pauli, re(coeff), im(coeff) - return (term[1], term[0], term[2].real, term[2].imag) + # sort by index, then by pauli + return (term[1], term[0]) # Do nothing in trivial cases if len(paulis) <= 1: @@ -315,7 +315,6 @@ def _term_sort_key(term: SparsePauliLabel) -> typing.Any: for term_idx, color in sorted(coloring.items()): term = graph.nodes()[term_idx] - print(color, term) terms_by_color[color].append(term) terms = sum(terms_by_color.values(), []) From a588ec3cf29545629392f67dec93b41e9e59e47c Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 6 Nov 2024 22:02:59 +0100 Subject: [PATCH 15/20] lint & docs --- qiskit/synthesis/evolution/lie_trotter.py | 2 +- qiskit/synthesis/evolution/product_formula.py | 8 +++++--- qiskit/synthesis/evolution/qdrift.py | 19 ++++++++++++------- qiskit/synthesis/evolution/suzuki_trotter.py | 2 +- .../passes/synthesis/hls_plugins.py | 4 ++-- 5 files changed, 21 insertions(+), 14 deletions(-) diff --git a/qiskit/synthesis/evolution/lie_trotter.py b/qiskit/synthesis/evolution/lie_trotter.py index c28e3ab1d0ed..a00f7008eba4 100644 --- a/qiskit/synthesis/evolution/lie_trotter.py +++ b/qiskit/synthesis/evolution/lie_trotter.py @@ -82,7 +82,7 @@ def __init__( effect when ``atomic_evolution is None``. preserve_order: Whether to allow preserve_ordering the terms of the operator to potentially yield a shallower evolution circuit. Not relevant - when synthesizing operator with a single term. Defaults to ``False``. + when synthesizing operator with a single term. Defaults to ``True``. """ super().__init__( 1, diff --git a/qiskit/synthesis/evolution/product_formula.py b/qiskit/synthesis/evolution/product_formula.py index 3a9f18d1d62e..515b343d0b50 100644 --- a/qiskit/synthesis/evolution/product_formula.py +++ b/qiskit/synthesis/evolution/product_formula.py @@ -287,9 +287,11 @@ def reorder_paulis( Args: paulis: The operator whose terms to preserve_order. - strategy: The coloring heuristic to use. See - [``rx.ColoringStrategy``](https://www.rustworkx.org/apiref/rustworkx.ColoringStrategy.html#coloringstrategy). - Default is ``rx.ColoringStrategy.Degree``. + 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: diff --git a/qiskit/synthesis/evolution/qdrift.py b/qiskit/synthesis/evolution/qdrift.py index 0b2d45155f09..243b41ba8bf1 100644 --- a/qiskit/synthesis/evolution/qdrift.py +++ b/qiskit/synthesis/evolution/qdrift.py @@ -25,7 +25,7 @@ from qiskit.utils.deprecation import deprecate_arg from qiskit.exceptions import QiskitError -from .product_formula import ProductFormula +from .product_formula import ProductFormula, reorder_paulis if typing.TYPE_CHECKING: from qiskit.circuit.library import PauliEvolutionGate @@ -68,7 +68,7 @@ def __init__( ) = None, seed: int | None = None, wrap: bool = False, - reorder: bool = False, + preserve_order: bool = True, ) -> None: r""" Args: @@ -89,15 +89,16 @@ def __init__( seed: An optional seed for reproducibility of the random sampling process. wrap: Whether to wrap the atomic evolutions into custom gate objects. This only takes effect when ``atomic_evolution is None``. - reorder: Whether to allow reordering the terms of the sampled + preserve_order: Whether to allow reordering the terms of the sampled operator before synthesizing the evolution circuit. Setting this - to ``True`` can potentially yield a shallower evolution circuit. - Defaults to ``False``. + to ``False`` can potentially yield a shallower evolution circuit. + Defaults to ``True``. """ - super().__init__(1, reps, insert_barriers, cx_structure, atomic_evolution, wrap) + super().__init__( + 1, reps, insert_barriers, cx_structure, atomic_evolution, wrap, preserve_order + ) self.sampled_ops = None self.rng = np.random.default_rng(seed) - self.reorder = reorder def expand(self, evolution: PauliEvolutionGate) -> list[tuple[str, tuple[int], float]]: operators = evolution.operator @@ -131,4 +132,8 @@ def expand(self, evolution: PauliEvolutionGate) -> list[tuple[str, tuple[int], f sampled_paulis = [ (pauli[0], pauli[1], np.real(np.sign(pauli[2])) * rescaled_time) for pauli in sampled ] + + if self.preserve_order: + sampled_paulis = reorder_paulis(sampled_paulis) + return sampled_paulis diff --git a/qiskit/synthesis/evolution/suzuki_trotter.py b/qiskit/synthesis/evolution/suzuki_trotter.py index d725241a285d..04b1416b19b7 100644 --- a/qiskit/synthesis/evolution/suzuki_trotter.py +++ b/qiskit/synthesis/evolution/suzuki_trotter.py @@ -107,7 +107,7 @@ def __init__( effect when ``atomic_evolution is None``. preserve_order: Whether to allow reordering the terms of the operator to potentially yield a shallower evolution circuit. Not relevant - when synthesizing operator with a single term. Defaults to ``False``. + when synthesizing operator with a single term. Defaults to ``True``. Raises: ValueError: If order is not even """ diff --git a/qiskit/transpiler/passes/synthesis/hls_plugins.py b/qiskit/transpiler/passes/synthesis/hls_plugins.py index 3ffe974a41cc..092d7cdb3a88 100644 --- a/qiskit/transpiler/passes/synthesis/hls_plugins.py +++ b/qiskit/transpiler/passes/synthesis/hls_plugins.py @@ -282,7 +282,7 @@ synth_clifford_ag, synth_clifford_bm, ) -from qiskit.synthesis.evolution import SuzukiTrotter +from qiskit.synthesis.evolution import ProductFormula from qiskit.synthesis.linear import ( synth_cnot_count_full_pmh, synth_cnot_depth_line_kms, @@ -1063,7 +1063,7 @@ class PauliEvolutionSynthesisDefault(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): algo = high_level_object.synthesis - if "preserve_order" in options: + if "preserve_order" in options and isinstance(algo, ProductFormula): algo.preserve_order = options["preserve_order"] return algo.synthesize(high_level_object) From d7c5f67b6dfb71f0784219ed38eb214a65a7d7d7 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 7 Nov 2024 10:21:11 +0100 Subject: [PATCH 16/20] Fix QDrift reordering --- qiskit/synthesis/evolution/qdrift.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/synthesis/evolution/qdrift.py b/qiskit/synthesis/evolution/qdrift.py index 243b41ba8bf1..a3eabeeee29f 100644 --- a/qiskit/synthesis/evolution/qdrift.py +++ b/qiskit/synthesis/evolution/qdrift.py @@ -133,7 +133,7 @@ def expand(self, evolution: PauliEvolutionGate) -> list[tuple[str, tuple[int], f (pauli[0], pauli[1], np.real(np.sign(pauli[2])) * rescaled_time) for pauli in sampled ] - if self.preserve_order: + if not self.preserve_order: sampled_paulis = reorder_paulis(sampled_paulis) return sampled_paulis From 56e619c752d73eb8fbcf45d3b04f8c453aca9358 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 7 Nov 2024 15:08:30 +0100 Subject: [PATCH 17/20] review comments - add reno - docs - safer list concat --- qiskit/circuit/library/pauli_evolution.py | 14 +++---- qiskit/synthesis/evolution/lie_trotter.py | 4 +- qiskit/synthesis/evolution/product_formula.py | 36 ++++++----------- qiskit/synthesis/evolution/qdrift.py | 7 ++-- qiskit/synthesis/evolution/suzuki_trotter.py | 4 +- ...eorder-trotter-terms-c8a6eb3cdb831f77.yaml | 40 +++++++++++++++++++ 6 files changed, 64 insertions(+), 41 deletions(-) create mode 100644 releasenotes/notes/reorder-trotter-terms-c8a6eb3cdb831f77.yaml diff --git a/qiskit/circuit/library/pauli_evolution.py b/qiskit/circuit/library/pauli_evolution.py index 12bf4cc9615e..750b2d140659 100644 --- a/qiskit/circuit/library/pauli_evolution.py +++ b/qiskit/circuit/library/pauli_evolution.py @@ -116,18 +116,14 @@ class docstring for an example. num_qubits = operator[0].num_qubits if isinstance(operator, list) else operator.num_qubits super().__init__(name="PauliEvolution", num_qubits=num_qubits, params=[time], label=label) self.operator = operator - self._synthesis = synthesis - @property - def synthesis(self) -> EvolutionSynthesis: - """Return the synthesis used.""" - if self._synthesis is not None: - return self._synthesis + if synthesis is None: + # pylint: disable=cyclic-import + from qiskit.synthesis.evolution import LieTrotter - # pylint: disable=cyclic-import - from qiskit.synthesis.evolution import LieTrotter + synthesis = LieTrotter() - return LieTrotter() + self.synthesis = synthesis @property def time(self) -> ParameterValueType: diff --git a/qiskit/synthesis/evolution/lie_trotter.py b/qiskit/synthesis/evolution/lie_trotter.py index a00f7008eba4..3fb8548d8f75 100644 --- a/qiskit/synthesis/evolution/lie_trotter.py +++ b/qiskit/synthesis/evolution/lie_trotter.py @@ -80,9 +80,9 @@ 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: Whether to allow preserve_ordering the terms of the operator to + 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. Defaults to ``True``. + when synthesizing operator with a single term. """ super().__init__( 1, diff --git a/qiskit/synthesis/evolution/product_formula.py b/qiskit/synthesis/evolution/product_formula.py index 515b343d0b50..872e0c0ede69 100644 --- a/qiskit/synthesis/evolution/product_formula.py +++ b/qiskit/synthesis/evolution/product_formula.py @@ -15,7 +15,8 @@ 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 @@ -90,7 +91,7 @@ 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: Whether to allow preserve_ordering the terms of the operator to + 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. """ @@ -253,40 +254,27 @@ def real_or_fail(value, tol=100): def reorder_paulis( - paulis: SparsePauliLabel, strategy: rx.ColoringStrategy = rx.ColoringStrategy.Saturation -) -> SparsePauliOp | list[SparsePauliOp]: + paulis: Sequence[SparsePauliLabel], + strategy: rx.ColoringStrategy = rx.ColoringStrategy.Saturation, +) -> list[SparsePauliLabel]: r""" - Creates an equivalent operator by preserve_ordering terms in order to yield a + 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 term acts on the same qubit (for example, the terms :math:`IXX` and + 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 ``operator``. Finally, a new - :class:`~qiskit.quantum_info.SparsePauliOp` is created where terms of the - same color are grouped together. - - In trivial cases, i.e. when either - - the input is a :class:`~qiskit.quantum_info.SparsePauliOp` with - less than two Pauli terms, or - - the input is a list containing a single - :class:`~qiskit.quantum_info.SparsePauliOp` which has less than two - Pauli terms, - this method does nothing. - - If ``operators`` is a list of :class:`~qiskit.quantum_info.SparsePauliOp`, - then preserve_ordering is applied to every operator independently, and the list of - preserve_ordered operators is returned. + parallel in the greater evolution circuit of ``paulis``. This method is deterministic and invariant under permutation of the Pauli - term in ``operators``. + term in ``paulis``. Args: - paulis: The operator whose terms to preserve_order. + paulis: The operator whose terms to reorder. strategy: The coloring heuristic to use, see ``ColoringStrategy`` [#]. Default is ``ColoringStrategy.Saturation``. @@ -319,5 +307,5 @@ def _term_sort_key(term: SparsePauliLabel) -> typing.Any: term = graph.nodes()[term_idx] terms_by_color[color].append(term) - terms = sum(terms_by_color.values(), []) + terms = list(itertools.chain(*terms_by_color.values())) return terms diff --git a/qiskit/synthesis/evolution/qdrift.py b/qiskit/synthesis/evolution/qdrift.py index 243b41ba8bf1..25c8c0700e8a 100644 --- a/qiskit/synthesis/evolution/qdrift.py +++ b/qiskit/synthesis/evolution/qdrift.py @@ -89,10 +89,9 @@ def __init__( seed: An optional seed for reproducibility of the random sampling process. wrap: Whether to wrap the atomic evolutions into custom gate objects. This only takes effect when ``atomic_evolution is None``. - preserve_order: Whether to allow reordering the terms of the sampled - operator before synthesizing the evolution circuit. Setting this - to ``False`` can potentially yield a shallower evolution circuit. - Defaults to ``True``. + 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__( 1, reps, insert_barriers, cx_structure, atomic_evolution, wrap, preserve_order diff --git a/qiskit/synthesis/evolution/suzuki_trotter.py b/qiskit/synthesis/evolution/suzuki_trotter.py index 04b1416b19b7..d9217ab4b565 100644 --- a/qiskit/synthesis/evolution/suzuki_trotter.py +++ b/qiskit/synthesis/evolution/suzuki_trotter.py @@ -105,9 +105,9 @@ 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: Whether to allow reordering the terms of the operator to + 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. Defaults to ``True``. + when synthesizing operator with a single term. Raises: ValueError: If order is not even """ diff --git a/releasenotes/notes/reorder-trotter-terms-c8a6eb3cdb831f77.yaml b/releasenotes/notes/reorder-trotter-terms-c8a6eb3cdb831f77.yaml new file mode 100644 index 000000000000..4f77f09e7ab2 --- /dev/null +++ b/releasenotes/notes/reorder-trotter-terms-c8a6eb3cdb831f77.yaml @@ -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()) + From c192d8727a3687c028ea5ae7b30a92ff9253b452 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 7 Nov 2024 19:22:36 +0100 Subject: [PATCH 18/20] rm dead code --- qiskit/circuit/library/pauli_evolution.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/qiskit/circuit/library/pauli_evolution.py b/qiskit/circuit/library/pauli_evolution.py index 750b2d140659..b7818f72d5fa 100644 --- a/qiskit/circuit/library/pauli_evolution.py +++ b/qiskit/circuit/library/pauli_evolution.py @@ -145,14 +145,7 @@ def time(self, time: ParameterValueType) -> None: def _define(self): """Unroll, where the default synthesis is matrix based.""" - if self.synthesis is None: - from qiskit.synthesis.evolution import LieTrotter - - synthesis = LieTrotter() - else: - synthesis = self.synthesis - - self.definition = synthesis.synthesize(self) + self.definition = self.synthesis.synthesize(self) def validate_parameter(self, parameter: ParameterValueType) -> ParameterValueType: """Gate parameters should be int, float, or ParameterExpression""" From 80c402228c65df1658c8c5b1c6f37deb65ae73e5 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 7 Nov 2024 20:15:16 +0100 Subject: [PATCH 19/20] fix default value --- qiskit/synthesis/evolution/lie_trotter.py | 2 +- qiskit/synthesis/evolution/suzuki_trotter.py | 2 +- qiskit/transpiler/passes/synthesis/hls_plugins.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/synthesis/evolution/lie_trotter.py b/qiskit/synthesis/evolution/lie_trotter.py index 3fb8548d8f75..db9f4a91f101 100644 --- a/qiskit/synthesis/evolution/lie_trotter.py +++ b/qiskit/synthesis/evolution/lie_trotter.py @@ -60,7 +60,7 @@ def __init__( | None ) = None, wrap: bool = False, - preserve_order: bool = False, + preserve_order: bool = True, ) -> None: """ Args: diff --git a/qiskit/synthesis/evolution/suzuki_trotter.py b/qiskit/synthesis/evolution/suzuki_trotter.py index d9217ab4b565..209f377351a7 100644 --- a/qiskit/synthesis/evolution/suzuki_trotter.py +++ b/qiskit/synthesis/evolution/suzuki_trotter.py @@ -85,7 +85,7 @@ def __init__( | None ) = None, wrap: bool = False, - preserve_order: bool = False, + preserve_order: bool = True, ) -> None: """ Args: diff --git a/qiskit/transpiler/passes/synthesis/hls_plugins.py b/qiskit/transpiler/passes/synthesis/hls_plugins.py index 3961fcfc3f34..6696ff2999d9 100644 --- a/qiskit/transpiler/passes/synthesis/hls_plugins.py +++ b/qiskit/transpiler/passes/synthesis/hls_plugins.py @@ -1535,7 +1535,7 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** ) return None - if "preserve_order" in options and isinstance(algo, ProductFormula): + if "preserve_order" in options: algo.preserve_order = options["preserve_order"] num_qubits = high_level_object.num_qubits From 365f238619f8bad72afa498ba526d00821c500e4 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 7 Nov 2024 20:25:08 +0100 Subject: [PATCH 20/20] missed one thanks Matthew! --- qiskit/synthesis/evolution/product_formula.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/synthesis/evolution/product_formula.py b/qiskit/synthesis/evolution/product_formula.py index 872e0c0ede69..b314d5ec5e62 100644 --- a/qiskit/synthesis/evolution/product_formula.py +++ b/qiskit/synthesis/evolution/product_formula.py @@ -69,7 +69,7 @@ def __init__( | None ) = None, wrap: bool = False, - preserve_order: bool = False, + preserve_order: bool = True, ) -> None: """ Args: