Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3a15589
py version for expand
Cryoris Oct 3, 2024
cea830b
Merge branch 'main' into paulievo
Cryoris Oct 7, 2024
72f2689
expand fully & simplify lie trotter
Cryoris Oct 7, 2024
8cd2c25
use examples that actually do not commute
Cryoris Oct 8, 2024
681105d
add plugin structure
Cryoris Oct 8, 2024
58e4a03
fix plugin name, rm complex from expand
Cryoris Oct 11, 2024
7f2ec3d
paulievo in Rust
Cryoris Oct 11, 2024
f9b72cf
take care of global phase
Cryoris Oct 11, 2024
3985394
support barriers
Cryoris Oct 11, 2024
99a1153
fix time parameter
Cryoris Oct 11, 2024
214b969
fix lint
Cryoris Oct 14, 2024
d96cebf
Merge branch 'paulievo' into rust/paulievo
Cryoris Oct 14, 2024
e8bcc4f
fix final barrier
Cryoris Oct 14, 2024
aba92e2
fix wrapping
Cryoris Oct 15, 2024
4fcf639
add reno and HLS docs
Cryoris Oct 15, 2024
b53b332
Merge branch 'main' into paulievo
Cryoris Oct 15, 2024
5c95b0d
fix unreachable
Cryoris Oct 15, 2024
2eb66f1
fix QPY test & pauli feature map
Cryoris Oct 15, 2024
87496d2
use SX as basis tranfo
Cryoris Oct 15, 2024
4a81172
Merge branch 'main' into paulievo
Cryoris Oct 21, 2024
546cc5f
slight performance tweak
Cryoris Oct 23, 2024
551cd8e
Merge branch 'paulievo' of github.com:Cryoris/qiskit-terra into paulievo
Cryoris Oct 23, 2024
cf7d8b0
implement review comments
Cryoris Oct 28, 2024
6f4fcba
do_fountain
Cryoris Oct 28, 2024
6217d0e
clippy
Cryoris Oct 28, 2024
b79751d
Merge branch 'main' into paulievo
alexanderivrii Oct 31, 2024
e802c45
review comments
Cryoris Oct 31, 2024
9b7b0c8
Merge branch 'paulievo' of github.com:Cryoris/qiskit-terra into paulievo
Cryoris Oct 31, 2024
2b6f4eb
fix the basis trafo!
Cryoris Oct 31, 2024
1af569b
properly test the evolution basis changes
Cryoris Nov 4, 2024
c478cc1
fix more tests
Cryoris Nov 4, 2024
8d5e3f3
Shelly's review comments
Cryoris Nov 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ sk = "qiskit.transpiler.passes.synthesis.solovay_kitaev_synthesis:SolovayKitaevS
"qft.line" = "qiskit.transpiler.passes.synthesis.hls_plugins:QFTSynthesisLine"
"qft.default" = "qiskit.transpiler.passes.synthesis.hls_plugins:QFTSynthesisFull"
"permutation.token_swapper" = "qiskit.transpiler.passes.synthesis.hls_plugins:TokenSwapperSynthesisPermutation"
"evolution.default" = "qiskit.transpiler.passes.synthesis.hls_plugins:PauliEvolutionSynthesisDefault"

[project.entry-points."qiskit.transpiler.init"]
default = "qiskit.transpiler.preset_passmanagers.builtin_plugins:DefaultInitPassManager"
Expand Down
16 changes: 16 additions & 0 deletions qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,14 @@ def to_list(self, array: bool = False):
return labels
return labels.tolist()

def to_sparse_list(self):
"""Convert to a sparse Pauli list format with elements (pauli, qubits, coefficient)."""
pauli_labels = self.paulis.to_labels()
sparse_list = [
(*sparsify_label(label), coeff) for label, coeff in zip(pauli_labels, self.coeffs)
]
return sparse_list

def to_matrix(self, sparse: bool = False, force_serial: bool = False) -> np.ndarray:
"""Convert to a dense or sparse matrix.

Expand Down Expand Up @@ -1176,5 +1184,13 @@ def apply_layout(
return new_op.compose(self, qargs=layout)


def sparsify_label(pauli_string):
"""Return a sparse format of a Pauli string, e.g. "XIIIZ" -> ("XZ", [0, 4])."""
qubits = [i for i, label in enumerate(reversed(pauli_string)) if label != "I"]
sparse_label = "".join(pauli_string[~i] for i in qubits)

return sparse_label, qubits


# Update docstrings for API docs
generate_apidocs(SparsePauliOp)
44 changes: 3 additions & 41 deletions qiskit/synthesis/evolution/lie_trotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,15 @@

from __future__ import annotations

import inspect
from collections.abc import Callable
from typing import Any
import numpy as np
from qiskit.circuit.quantumcircuit import QuantumCircuit
from qiskit.quantum_info.operators import SparsePauliOp, Pauli
from qiskit.utils.deprecation import deprecate_arg

from .product_formula import ProductFormula
from .suzuki_trotter import SuzukiTrotter


class LieTrotter(ProductFormula):
class LieTrotter(SuzukiTrotter):
r"""The Lie-Trotter product formula.

The Lie-Trotter formula approximates the exponential of two non-commuting operators
Expand All @@ -40,7 +37,7 @@ class LieTrotter(ProductFormula):

.. math::

e^{-it(XX + ZZ)} = e^{-it XX}e^{-it ZZ} + \mathcal{O}(t^2).
e^{-it(XI + ZZ)} = e^{-it XI}e^{-it ZZ} + \mathcal{O}(t^2).

References:

Expand All @@ -52,21 +49,6 @@ class LieTrotter(ProductFormula):
`arXiv:math-ph/0506007 <https://arxiv.org/pdf/math-ph/0506007.pdf>`_
"""

@deprecate_arg(
name="atomic_evolution",
since="1.2",
predicate=lambda callable: callable is not None
and len(inspect.signature(callable).parameters) == 2,
deprecation_description=(
"The 'Callable[[Pauli | SparsePauliOp, float], QuantumCircuit]' signature of the "
"'atomic_evolution' argument"
),
additional_msg=(
"Instead you should update your 'atomic_evolution' function to be of the following "
"type: 'Callable[[QuantumCircuit, Pauli | SparsePauliOp, float], None]'."
),
pending=True,
)
def __init__(
self,
reps: int = 1,
Expand Down Expand Up @@ -100,26 +82,6 @@ def __init__(
"""
super().__init__(1, reps, insert_barriers, cx_structure, atomic_evolution, wrap)

def synthesize(self, evolution):
Comment thread
Cryoris marked this conversation as resolved.
# get operators and time to evolve
operators = evolution.operator
time = evolution.time

# construct the evolution circuit
single_rep = QuantumCircuit(operators[0].num_qubits)

if not isinstance(operators, list):
pauli_list = [(Pauli(op), np.real(coeff)) for op, coeff in operators.to_list()]
else:
pauli_list = [(op, 1) for op in operators]

for i, (op, coeff) in enumerate(pauli_list):
self.atomic_evolution(single_rep, op, coeff * time / self.reps)
if self.insert_barriers and i != len(pauli_list) - 1:
single_rep.barrier()

return single_rep.repeat(self.reps, insert_barriers=self.insert_barriers).decompose()

@property
def settings(self) -> dict[str, Any]:
"""Return the settings in a dictionary, which can be used to reconstruct the object.
Expand Down
50 changes: 47 additions & 3 deletions qiskit/synthesis/evolution/product_formula.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# typing.Any modifications or derivative works of this code must retain this
Comment thread
Cryoris marked this conversation as resolved.
Outdated
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

Expand All @@ -16,8 +16,8 @@

import inspect
from collections.abc import Callable
from typing import Any
from functools import partial
import typing
import numpy as np
from qiskit.circuit.parameterexpression import ParameterExpression
from qiskit.circuit.quantumcircuit import QuantumCircuit
Expand All @@ -26,6 +26,9 @@

from .evolution_synthesis import EvolutionSynthesis

if typing.TYPE_CHECKING:
from qiskit.circuit.library import PauliEvolutionGate


class ProductFormula(EvolutionSynthesis):
"""Product formula base class for the decomposition of non-commuting operator exponentials.
Expand Down Expand Up @@ -108,8 +111,49 @@ def wrap_atomic_evolution(output, operator, time):
else:
self.atomic_evolution = atomic_evolution

def expand(self, evolution: PauliEvolutionGate) -> list[tuple[str, tuple[int], float]]:
"""Apply the product formula to expand the Hamiltonian in the evolution gate.

Args:
evolution: The :class:`.PauliEvolutionGate`, whose Hamiltonian we expand.

Returns:
A list of Pauli rotations in a sparse format, where each element is
``(paulistring, qubits, coefficient)``. For example, the Lie-Trotter expansion
of ``H = XI + ZZ`` would return ``[("X", [1], 1), ("ZZ", [0, 1], 1)]``.
"""
raise NotImplementedError(
f"The method ``expand`` is not implemented for {self.__class__}. Implement it to "
f"automatically enable the call to {self.__class__}.synthesize."
)
Comment thread
alexanderivrii marked this conversation as resolved.

def synthesize(self, evolution: PauliEvolutionGate) -> QuantumCircuit:
"""Synthesize a :class:`.PauliEvolutionGate`.

Args:
evolution: The evolution gate to synthesize.

Returns:
QuantumCircuit: A circuit implementing the evolution.
"""
pauli_rotations = self.expand(evolution)

num_qubits = evolution.num_qubits

circuit = QuantumCircuit(num_qubits)

# we could cache the circuit decomposition for the circuits we already saw
for i, (pauli_string, qubits, coeff) in enumerate(pauli_rotations):
op = SparsePauliOp.from_sparse_list([(pauli_string, qubits, 1)], num_qubits).paulis[0]

self.atomic_evolution(circuit, op, np.real_if_close(coeff))
if self.insert_barriers and i != len(pauli_rotations) - 1:
circuit.barrier()

return circuit

@property
def settings(self) -> dict[str, Any]:
def settings(self) -> dict[str, typing.Any]:
"""Return the settings in a dictionary, which can be used to reconstruct the object.

Returns:
Expand Down
51 changes: 22 additions & 29 deletions qiskit/synthesis/evolution/qdrift.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@

import inspect
import math
import typing
from itertools import chain
from collections.abc import Callable
import numpy as np
from qiskit.circuit.quantumcircuit import QuantumCircuit
from qiskit.quantum_info.operators import SparsePauliOp, Pauli
from qiskit.utils.deprecation import deprecate_arg

from .product_formula import ProductFormula
from .lie_trotter import LieTrotter

if typing.TYPE_CHECKING:
from qiskit.circuit.library import PauliEvolutionGate


class QDrift(ProductFormula):
Expand Down Expand Up @@ -88,44 +92,33 @@ def __init__(
self.sampled_ops = None
self.rng = np.random.default_rng(seed)

def synthesize(self, evolution):
# get operators and time to evolve
def expand(self, evolution: PauliEvolutionGate) -> list[tuple[str, tuple[int], float]]:
operators = evolution.operator
time = evolution.time
time = evolution.time # used to determine the number of gates

if not isinstance(operators, list):
pauli_list = [(Pauli(op), coeff) for op, coeff in operators.to_list()]
coeffs = [np.real(coeff) for op, coeff in operators.to_list()]
# QDrift is based on first-order Lie-Trotter, hence we can just concatenate all
# Pauli terms and ignore commutations
if isinstance(operators, list):
paulis = list(chain.from_iterable([op.to_sparse_list() for op in operators]))
else:
pauli_list = [(op, 1) for op in operators]
coeffs = [1 for op in operators]
paulis = operators.to_sparse_list()

coeffs = [np.real(coeff) for _, _, coeff in paulis]

# We artificially make the weights positive
weights = np.abs(coeffs)
lambd = np.sum(weights)

num_gates = math.ceil(2 * (lambd**2) * (time**2) * self.reps)

# The protocol calls for the removal of the individual coefficients,
# and multiplication by a constant evolution time.
evolution_time = lambd * time / num_gates

self.sampled_ops = self.rng.choice(
np.array(pauli_list, dtype=object),
size=(num_gates,),
p=weights / lambd,
)

# pylint: disable=cyclic-import
from qiskit.circuit.library.pauli_evolution import PauliEvolutionGate

# 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
sampled = self.rng.choice(
np.array(paulis, dtype=object), size=(num_gates,), p=weights / lambd
)
evolution_circuit = PauliEvolutionGate(
sum(SparsePauliOp(np.sign(coeff) * op) for op, coeff in self.sampled_ops),
time=evolution_time,
synthesis=lie_trotter,
).definition

return evolution_circuit
rescaled_time = lambd * time / num_gates
sampled_paulis = [
(pauli[0], pauli[1], np.sign(pauli[2]) * rescaled_time) for pauli in sampled
]
return sampled_paulis
71 changes: 43 additions & 28 deletions qiskit/synthesis/evolution/suzuki_trotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@

import inspect
from collections.abc import Callable

import numpy as np
from itertools import chain

from qiskit.circuit.quantumcircuit import QuantumCircuit
from qiskit.quantum_info.operators import SparsePauliOp, Pauli
Expand All @@ -44,7 +43,7 @@ class SuzukiTrotter(ProductFormula):

.. math::

e^{-it(XX + ZZ)} = e^{-it/2 ZZ}e^{-it XX}e^{-it/2 ZZ} + \mathcal{O}(t^3).
e^{-it(XI + ZZ)} = e^{-it/2 XI}e^{-it ZZ}e^{-it/2 XI} + \mathcal{O}(t^3).

References:
[1]: D. Berry, G. Ahokas, R. Cleve and B. Sanders,
Expand Down Expand Up @@ -105,51 +104,67 @@ def __init__(
ValueError: If order is not even
"""

if order % 2 == 1:
if order > 1 and order % 2 == 1:
raise ValueError(
"Suzuki product formulae are symmetric and therefore only defined "
"for even orders."
"for even orders (or order==1)."
Comment thread
Cryoris marked this conversation as resolved.
Outdated
)
super().__init__(order, reps, insert_barriers, cx_structure, atomic_evolution, wrap)

def synthesize(self, evolution):
Comment thread
Cryoris marked this conversation as resolved.
# get operators and time to evolve
operators = evolution.operator
time = evolution.time

if not isinstance(operators, list):
pauli_list = [(Pauli(op), np.real(coeff)) for op, coeff in operators.to_list()]
else:
pauli_list = [(op, 1) for op in operators]
def expand(self, evolution):
"""
H = ZZ + IX --> ("X", [0], 1/2), ("ZZ", [0, 1], 1), ("X", [0], 1/2)

ops_to_evolve = self._recurse(self.order, time / self.reps, pauli_list)
("X", [0], 1/2), ("ZZ", [0, 1], 1), ("X", [0], 1), ("ZZ", [0, 1], 1), ("X", [0], 1/2)
"""
operators = evolution.operator # type: SparsePauliOp | list[SparsePauliOp]
time = evolution.time

# construct the evolution circuit
single_rep = QuantumCircuit(operators[0].num_qubits)

for i, (op, coeff) in enumerate(ops_to_evolve):
self.atomic_evolution(single_rep, op, coeff)
if self.insert_barriers and i != len(ops_to_evolve) - 1:
single_rep.barrier()
if isinstance(operators, list): # already sorted into commuting bits
non_commuting = [
(time / self.reps * operator).to_sparse_list() for operator in operators
]
else:
# Assume no commutativity here. If we were to group commuting Paulis,
# here would be the location to do so.
non_commuting = [[op] for op in (time / self.reps * operators).to_sparse_list()]

return single_rep.repeat(self.reps, insert_barriers=self.insert_barriers).decompose()
# we're already done here since Lie Trotter does not do any operator repetition
product_formula = self._recurse(self.order, non_commuting)
flattened = self.reps * list(chain.from_iterable(product_formula))
return flattened

@staticmethod
def _recurse(order, time, pauli_list):
def _recurse(order, grouped_paulis):
if order == 1:
return pauli_list
return grouped_paulis

elif order == 2:
halves = [(op, coeff * time / 2) for op, coeff in pauli_list[:-1]]
full = [(pauli_list[-1][0], time * pauli_list[-1][1])]
halves = [
[(label, qubits, coeff / 2) for label, qubits, coeff in paulis]
for paulis in grouped_paulis[:-1]
]
full = [grouped_paulis[-1]]
return halves + full + list(reversed(halves))

else:
reduction = 1 / (4 - 4 ** (1 / (order - 1)))
outer = 2 * SuzukiTrotter._recurse(
order - 2, time=reduction * time, pauli_list=pauli_list
order - 2,
[
[(label, qubits, coeff * reduction) for label, qubits, coeff in paulis]
for paulis in grouped_paulis
],
)
inner = SuzukiTrotter._recurse(
order - 2, time=(1 - 4 * reduction) * time, pauli_list=pauli_list
order - 2,
[
[
(label, qubits, coeff * (1 - 4 * reduction))
for label, qubits, coeff in paulis
]
for paulis in grouped_paulis
],
)
return outer + inner + outer
Loading