Skip to content
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

Add SparsePauliOp converter #401

Merged
merged 23 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a4c59d3
save draft of SparsePauliOp conversion
lillian542 Feb 5, 2024
09cdefe
Merge branch 'master' into sparse_pauli_op
lillian542 Feb 14, 2024
1b021d3
Add docstring to `convert_sparse_pauli_op_to_pl()`
Mandrenkov Feb 21, 2024
1891c48
Add tests for `convert_sparse_pauli_op_to_pl()`
Mandrenkov Feb 21, 2024
8cbe0e7
Synchronize `qiskit` and `qiskit-terra` requirements
Mandrenkov Feb 21, 2024
717f169
Merge branch 'master' into sparse_pauli_op
Mandrenkov Feb 21, 2024
53fe08d
Remove unused requirement `tweedledum`
Mandrenkov Feb 22, 2024
3c66e1a
Add `wires` argument to `convert_sparse_pauli_op_to_pl()`
Mandrenkov Feb 22, 2024
16269f8
Swap order of `params` and `wires` arguments
Mandrenkov Feb 22, 2024
531e845
Merge branch 'master' into sparse_pauli_op
Mandrenkov Feb 22, 2024
e47512b
Pass `coeffs` by keyword argument in docstring example
Mandrenkov Feb 22, 2024
3d494bb
Pass `params` by keyword argument in example
Mandrenkov Feb 22, 2024
abe6782
Add comment about wire ordering to Qiskit term `for` loop
Mandrenkov Feb 22, 2024
c528969
Rename function to `convert_sparse_pauli_op()`
Mandrenkov Feb 22, 2024
ad1e351
Reword note according to PR suggestion
Mandrenkov Feb 22, 2024
38b96e2
Add 'Usage Details' section to docstring
Mandrenkov Feb 22, 2024
51f6f71
Rename function to `load_pauli_op()`
Mandrenkov Feb 22, 2024
867024b
Rename `sparse_op` to `pauli_op`
Mandrenkov Feb 22, 2024
f3f745c
Merge branch 'master' into sparse_pauli_op
Mandrenkov Feb 22, 2024
22c02b9
Fix import ordering
Mandrenkov Feb 22, 2024
b33a627
Use output from latest version of PennyLane
Mandrenkov Feb 22, 2024
e7007f2
Add missing comma to docstring note
Mandrenkov Feb 22, 2024
9e35225
Replace 'Converts' with 'Loads' in docstring
Mandrenkov Feb 22, 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
2 changes: 1 addition & 1 deletion pennylane_qiskit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@
from .basic_aer import BasicAerDevice
from .ibmq import IBMQDevice
from .remote import RemoteDevice
from .converter import load, load_qasm, load_qasm_from_file
from .converter import load, load_pauli_op, load_qasm, load_qasm_from_file
from .runtime_devices import IBMQCircuitRunnerDevice
from .runtime_devices import IBMQSamplerDevice
121 changes: 120 additions & 1 deletion pennylane_qiskit/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
This module contains functions for converting Qiskit QuantumCircuit objects
into PennyLane circuit templates.
"""
from typing import Dict, Any
from typing import Dict, Any, Sequence, Union
import warnings
from functools import partial, reduce

Expand All @@ -27,6 +27,7 @@
from qiskit.circuit.library import GlobalPhaseGate
from qiskit.circuit.classical import expr
from qiskit.exceptions import QiskitError
from qiskit.quantum_info import SparsePauliOp
from sympy import lambdify

import pennylane as qml
Expand Down Expand Up @@ -550,6 +551,124 @@ def load_qasm_from_file(file: str):
return load(QuantumCircuit.from_qasm_file(file))


def load_pauli_op(
pauli_op: SparsePauliOp,
params: Any = None,
wires: Union[Sequence, None] = None,
) -> qml.operation.Operator:
"""Loads a PennyLane operator from a Qiskit SparsePauliOp.

Args:
pauli_op (qiskit.quantum_info.SparsePauliOp): the SparsePauliOp to be converted
params (Any): optional assignment of coefficient values for the SparsePauliOp; see the
`Qiskit documentation <https://docs.quantum.ibm.com/api/qiskit/qiskit.quantum_info.SparsePauliOp#assign_parameters>`__
to learn more about the expected format of these parameters
wires (Sequence | None): optional assignment of wires for the converted SparsePauliOp; if
the original SparsePauliOp acted on :math:`N` qubits, then this must be a sequence of
length :math:`N`

Returns:
pennylane.operation.Operator: The equivalent PennyLane operator.

.. note::

The wire ordering convention differs between PennyLane and Qiskit: PennyLane wires are
enumerated from left to right, while the Qiskit convention is to enumerate from right to
left. A ``SparsePauliOp`` term defined by the string ``"XYZ"`` applies ``Z`` on wire 0,
``Y`` on wire 1, and ``X`` on wire 2.

**Example**

Consider the following script which creates a Qiskit ``SparsePauliOp``:

.. code-block:: python

from qiskit.quantum_info import SparsePauliOp

qiskit_op = SparsePauliOp(["II", "XY"])

The ``SparsePauliOp`` contains two terms and acts over two qubits:

>>> qiskit_op
SparsePauliOp(['II', 'XY'],
coeffs=[1.+0.j, 1.+0.j])

To convert the ``SparsePauliOp`` into a PennyLane operator, use:

>>> from pennylane_qiskit import load_pauli_op
>>> load_pauli_op(qiskit_op)
I(0) + X(1) @ Y(0)

.. details::
:title: Usage Details

You can convert a parameterized ``SparsePauliOp`` into a PennyLane operator by assigning
literal values to each coefficient parameter. For example, the script

.. code-block:: python

import numpy as np
from qiskit.circuit import Parameter

a, b, c = [Parameter(var) for var in "abc"]
param_qiskit_op = SparsePauliOp(["II", "XZ", "YX"], coeffs=np.array([a, b, c]))

defines a ``SparsePauliOp`` with three coefficients (parameters):

>>> param_qiskit_op
SparsePauliOp(['II', 'XZ', 'YX'],
coeffs=[ParameterExpression(1.0*a), ParameterExpression(1.0*b),
ParameterExpression(1.0*c)])

The ``SparsePauliOp`` can be converted into a PennyLane operator by calling the conversion
function and specifying the value of each parameter using the ``params`` argument:

>>> load_pauli_op(param_qiskit_op, params={a: 2, b: 3, c: 4})
(
(2+0j) * I(0)
+ (3+0j) * (X(1) @ Z(0))
+ (4+0j) * (Y(1) @ X(0))
)

Similarly, a custom wire mapping can be applied to a ``SparsePauliOp`` as follows:

>>> wired_qiskit_op = SparsePauliOp("XYZ")
>>> wired_qiskit_op
SparsePauliOp(['XYZ'],
coeffs=[1.+0.j])
>>> load_pauli_op(wired_qiskit_op, wires=[3, 5, 7])
Y(5) @ Z(3) @ X(7)
"""
if wires is not None and len(wires) != pauli_op.num_qubits:
raise RuntimeError(
f"The specified number of wires - {len(wires)} - does not match the "
f"number of qubits the SparsePauliOp acts on."
)

wire_map = map_wires(range(pauli_op.num_qubits), wires)

if params:
pauli_op = pauli_op.assign_parameters(params)

op_map = {"X": qml.PauliX, "Y": qml.PauliY, "Z": qml.PauliZ, "I": qml.Identity}

coeffs = pauli_op.coeffs
if ParameterExpression in [type(c) for c in coeffs]:
raise RuntimeError(f"Not all parameter expressions are assigned in coeffs {coeffs}")

qiskit_terms = pauli_op.paulis
pl_terms = []

for term in qiskit_terms:
Mandrenkov marked this conversation as resolved.
Show resolved Hide resolved
# term is a special Qiskit type. Iterating over the term goes right to left
# in accordance with Qiskit wire order convention, i.e. `enumerate("XZ")` will be
# [(0, "Z"), (1, "X")], so we don't need to reverse to match the PL convention.
operators = [op_map[str(op)](wire_map[wire]) for wire, op in enumerate(term)]
pl_terms.append(qml.prod(*operators).simplify())

return qml.dot(coeffs, pl_terms)


# pylint:disable=protected-access
def _conditional_funcs(inst, operation_class, branch_funcs, ctrl_flow_type):
"""Builds the conditional functions for Controlled flows.
Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,5 @@ stevedore==5.1.0
symengine==0.11.0
sympy==1.12
toml==0.10.2
tweedledum==1.1.1
urllib3==2.2.1
websocket-client==1.7.0
151 changes: 147 additions & 4 deletions tests/test_converter.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import math
import sys

import pytest
from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister
from qiskit.circuit import library as lib
from qiskit.circuit import Parameter, ParameterVector
from qiskit.exceptions import QiskitError
from qiskit.quantum_info.operators import Operator
from qiskit.circuit.library import DraperQFTAdder
from qiskit.circuit.classical import expr
from qiskit.circuit.library import DraperQFTAdder
from qiskit.exceptions import QiskitError
from qiskit.quantum_info import SparsePauliOp

import pennylane as qml
from pennylane import numpy as np
from pennylane_qiskit.converter import (
load,
load_pauli_op,
load_qasm,
load_qasm_from_file,
map_wires,
Expand Down Expand Up @@ -1791,3 +1792,145 @@ def circuit_loaded_qiskit_circuit():
return qml.expval(qml.PauliZ(0))

assert circuit_loaded_qiskit_circuit() == circuit_native_pennylane()


class TestLoadPauliOp:
"""Tests for the :func:`load_pauli_op()` function."""

@pytest.mark.parametrize(
"pauli_op, want_op",
[
(
SparsePauliOp("I"),
qml.Identity(wires=0),
),
(
SparsePauliOp("XYZ"),
qml.prod(qml.PauliZ(wires=0), qml.PauliY(wires=1), qml.PauliX(wires=2)),
),
(
SparsePauliOp(["XY", "ZX"]),
qml.sum(
qml.prod(qml.PauliX(wires=1), qml.PauliY(wires=0)),
qml.prod(qml.PauliZ(wires=1), qml.PauliX(wires=0)),
)
),
]
)
def test_convert_with_default_coefficients(self, pauli_op, want_op):
"""Tests that a SparsePauliOp can be converted into a PennyLane operator with the default
coefficients.
"""
have_op = load_pauli_op(pauli_op)
assert qml.equal(have_op, want_op)

@pytest.mark.parametrize(
"pauli_op, want_op",
[
(
SparsePauliOp("I", coeffs=[2]),
qml.s_prod(2, qml.Identity(wires=0)),
),
(
SparsePauliOp(["XY", "ZX"], coeffs=[3, 7]),
qml.sum(
qml.s_prod(3, qml.prod(qml.PauliX(wires=1), qml.PauliY(wires=0))),
qml.s_prod(7, qml.prod(qml.PauliZ(wires=1), qml.PauliX(wires=0))),
)
),
]
)
def test_convert_with_literal_coefficients(self, pauli_op, want_op):
"""Tests that a SparsePauliOp can be converted into a PennyLane operator with literal
coefficient values.
"""
have_op = load_pauli_op(pauli_op)
assert qml.equal(have_op, want_op)


def test_convert_with_parameter_coefficients(self):
"""Tests that a SparsePauliOp can be converted into a PennyLane operator by assigning values
to each parameterized coefficient.
"""
a, b = [Parameter(var) for var in "ab"]
pauli_op = SparsePauliOp(["XY", "ZX"], coeffs=[a, b])

have_op = load_pauli_op(pauli_op, params={a: 3, b: 7})
want_op = qml.sum(
qml.s_prod(3, qml.prod(qml.PauliX(wires=1), qml.PauliY(wires=0))),
qml.s_prod(7, qml.prod(qml.PauliZ(wires=1), qml.PauliX(wires=0))),
)
assert qml.equal(have_op, want_op)

def test_convert_too_few_coefficients(self):
"""Tests that a RuntimeError is raised if an attempt is made to convert a SparsePauliOp into
a PennyLane operator without assigning values for all parameterized coefficients.
"""
a, b = [Parameter(var) for var in "ab"]
pauli_op = SparsePauliOp(["XY", "ZX"], coeffs=[a, b])

match = (
"Not all parameter expressions are assigned in coeffs "
r"\[\(3\+0j\) ParameterExpression\(1\.0\*b\)\]"
)
with pytest.raises(RuntimeError, match=match):
load_pauli_op(pauli_op, params={a: 3})

def test_convert_too_many_coefficients(self):
"""Tests that a SparsePauliOp can be converted into a PennyLane operator by assigning values
to a strict superset of the parameterized coefficients.
"""
a, b, c = [Parameter(var) for var in "abc"]
pauli_op = SparsePauliOp(["XY", "ZX"], coeffs=[a, b])

have_op = load_pauli_op(pauli_op, params={a: 3, b: 7, c: 9})
want_op = qml.sum(
qml.s_prod(3, qml.prod(qml.PauliX(wires=1), qml.PauliY(wires=0))),
qml.s_prod(7, qml.prod(qml.PauliZ(wires=1), qml.PauliX(wires=0))),
)
assert qml.equal(have_op, want_op)

@pytest.mark.parametrize(
"pauli_op, wires, want_op",
[
(
SparsePauliOp("XYZ"),
"ABC",
qml.prod(qml.PauliZ(wires="A"), qml.PauliY(wires="B"), qml.PauliX(wires="C")),
),
(
SparsePauliOp(["XY", "ZX"]),
[1, 0],
qml.sum(
qml.prod(qml.PauliX(wires=0), qml.PauliY(wires=1)),
qml.prod(qml.PauliZ(wires=0), qml.PauliX(wires=1)),
)
),
]
)
def test_convert_with_wires(self, pauli_op, wires, want_op):
"""Tests that a SparsePauliOp can be converted into a PennyLane operator with custom wires."""
have_op = load_pauli_op(pauli_op, wires=wires)
assert qml.equal(have_op, want_op)

def test_convert_with_too_few_wires(self):
"""Tests that a RuntimeError is raised if an attempt is made to convert a SparsePauliOp into
a PennyLane operator with too few custom wires.
"""
match = (
r"The specified number of wires - 1 - does not match "
f"the number of qubits the SparsePauliOp acts on."
)
with pytest.raises(RuntimeError, match=match):
load_pauli_op(SparsePauliOp("II"), wires=[0])

def test_convert_with_too_many_wires(self):
"""Tests that a RuntimeError is raised if an attempt is made to convert a SparsePauliOp into
a PennyLane operator with too many custom wires.
"""
match = (
r"The specified number of wires - 3 - does not match "
f"the number of qubits the SparsePauliOp acts on."
)
with pytest.raises(RuntimeError, match=match):
load_pauli_op(SparsePauliOp("II"), wires=[0, 1, 2])
Loading