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 function to load Qiskit operators #5251

Merged
merged 15 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
function fails because the Qiskit converter is missing.
[(#5218)](https://github.com/PennyLaneAI/pennylane/pull/5218)

* A Qiskit `SparsePauliOp` can be converted into a PennyLane `Operator` using `qml.from_qiskit_op`.
Mandrenkov marked this conversation as resolved.
Show resolved Hide resolved
[(#5251)](https://github.com/PennyLaneAI/pennylane/pull/5251)

<h4>Native mid-circuit measurements on default qubit 💡</h4>

* When operating in finite-shots mode, the `default.qubit` device now performs mid-circuit
Expand Down
119 changes: 109 additions & 10 deletions pennylane/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@
from importlib import metadata
from sys import version_info


# Error message to show when the PennyLane-Qiskit plugin is required but missing.
_MISSING_QISKIT_PLUGIN_MESSAGE = (
"Conversion from Qiskit requires the PennyLane-Qiskit plugin. "
"You can install the plugin by running: pip install pennylane-qiskit. "
"You may need to restart your kernel or environment after installation. "
"If you have any difficulties, you can reach out on the PennyLane forum at "
"https://discuss.pennylane.ai/c/pennylane-plugins/pennylane-qiskit/"
)

# get list of installed plugin converters
__plugin_devices = (
defaultdict(tuple, metadata.entry_points())["pennylane.io"]
Expand Down Expand Up @@ -85,8 +95,8 @@ def load(quantum_circuit_object, format: str, **load_kwargs):


def from_qiskit(quantum_circuit, measurements=None):
"""Loads Qiskit QuantumCircuit objects by using the converter in the
PennyLane-Qiskit plugin.
"""Loads Qiskit `QuantumCircuit <https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.QuantumCircuit>`_
objects by using the converter in the PennyLane-Qiskit plugin.

**Example:**

Expand All @@ -110,7 +120,7 @@ def from_qiskit(quantum_circuit, measurements=None):
overrides the terminal measurements that may be present in the input circuit.

Returns:
function: the PennyLane template created based on the QuantumCircuit object
function: the PennyLane template created based on the ``QuantumCircuit`` object

.. details::
:title: Usage Details
Expand Down Expand Up @@ -146,13 +156,102 @@ def circuit_loaded_qiskit_circuit():
return load(quantum_circuit, format="qiskit", measurements=measurements)
except ValueError as e:
if e.args[0].split(".")[0] == "Converter does not exist":
raise RuntimeError(
"Conversion from Qiskit requires the PennyLane-Qiskit plugin. "
"You can install the plugin by running: pip install pennylane-qiskit. "
"You may need to restart your kernel or environment after installation. "
"If you have any difficulties, you can reach out on the PennyLane forum at "
"https://discuss.pennylane.ai/c/pennylane-plugins/pennylane-qiskit/"
) from e
raise RuntimeError(_MISSING_QISKIT_PLUGIN_MESSAGE) from e
raise e


def from_qiskit_op(qiskit_op, params=None, wires=None):
"""Loads Qiskit `SparsePauliOp <https://docs.quantum.ibm.com/api/qiskit/qiskit.quantum_info.SparsePauliOp>`_
Mandrenkov marked this conversation as resolved.
Show resolved Hide resolved
objects by using the converter in the PennyLane-Qiskit plugin.

Args:
qiskit_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:
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. This means a ``SparsePauliOp`` term defined by the string ``"XYZ"`` applies ``Z`` on
wire 0, ``Y`` on wire 1, and ``X`` on wire 2. For more details, see the
`String representation <https://docs.quantum.ibm.com/api/qiskit/qiskit.quantum_info.Pauli>`_
section of the Qiskit documentation for the ``Pauli`` class.

**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 :class:`Operator`, use:

>>> import pennylane as qml
>>> qml.from_qiskit_op(qiskit_op)
I(0) + X(1) @ Y(0)
Mandrenkov marked this conversation as resolved.
Show resolved Hide resolved

.. 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:

>>> qml.from_qiskit_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])
>>> qml.from_qiskit_op(wired_qiskit_op, wires=[3, 5, 7])
Y(5) @ Z(3) @ X(7)
"""
try:
return load(qiskit_op, format="qiskit_op", params=params, wires=wires)
except ValueError as e:
if e.args[0].split(".")[0] == "Converter does not exist":
raise RuntimeError(_MISSING_QISKIT_PLUGIN_MESSAGE) from e
Mandrenkov marked this conversation as resolved.
Show resolved Hide resolved
raise e


Expand Down
90 changes: 56 additions & 34 deletions tests/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,15 @@ def call_args(self):
return self.mock_loader.call_args


load_entry_points = ["qiskit", "qasm", "qasm_file", "pyquil_program", "quil", "quil_file"]
load_entry_points = [
"pyquil_program",
"qasm_file",
"qasm",
"qiskit_op",
"qiskit",
"quil_file",
"quil",
]


@pytest.fixture(name="mock_plugin_converters")
Expand All @@ -71,42 +79,47 @@ def test_converter_does_not_exist(self):
):
qml.load("Test", format="some_non_existing_format")

def test_qiskit_not_installed(self, monkeypatch):
"""Test that a specific error is raised if qml.from_qiskit is called and the qiskit
plugin converter isn't found, instead of the generic 'ValueError: Converter does not exist.'
@pytest.mark.parametrize(
"method, entry_point_name",
[(qml.from_qiskit, "qiskit"), (qml.from_qiskit_op, "qiskit_op")],
)
def test_qiskit_converter_does_not_exist(self, monkeypatch, method, entry_point_name):
"""Test that a RuntimeError with an appropriate message is raised if a Qiskit convenience
method is called but the Qiskit plugin converter is not found.
"""

# temporarily make a mock_converter_dict with no "qiskit"
# Temporarily make a mock_converter_dict without the Qiskit entry point.
mock_plugin_converter_dict = {
entry_point: MockPluginConverter(entry_point) for entry_point in load_entry_points
}
del mock_plugin_converter_dict["qiskit"]
del mock_plugin_converter_dict[entry_point_name]
monkeypatch.setattr(qml.io, "plugin_converters", mock_plugin_converter_dict)

# calling from_qiskit raises the specific RuntimeError rather than the generic ValueError
with pytest.raises(
RuntimeError,
match="Conversion from Qiskit requires the PennyLane-Qiskit plugin. "
"You can install the plugin by",
):
qml.from_qiskit("Test")
# Check that the specific RuntimeError is raised as opposed to a generic ValueError.
with pytest.raises(RuntimeError, match=r"Conversion from Qiskit requires..."):
method("Test")

# if load raises some other ValueError instead of the "converter does not exist" error, it is unaffected
def mock_load_with_error(*args, **kwargs):
raise ValueError("Some other error raised than instead of converter does not exist")
@pytest.mark.parametrize(
"method, entry_point_name",
[(qml.from_qiskit, "qiskit"), (qml.from_qiskit_op, "qiskit_op")],
)
def test_qiskit_converter_load_fails(self, monkeypatch, method, entry_point_name):
"""Test that an exception which is raised while calling a Qiskit convenience method (but
after the Qiskit plugin converter is found) is propagated correctly.
"""
mock_plugin_converter = MockPluginConverter(entry_point_name)
mock_plugin_converter.mock_loader.side_effect = ValueError("Some Other Error")

monkeypatch.setattr(qml.io, "load", mock_load_with_error)
mock_plugin_converter_dict = {entry_point_name: mock_plugin_converter}
monkeypatch.setattr(qml.io, "plugin_converters", mock_plugin_converter_dict)

with pytest.raises(
ValueError,
match="Some other error raised than instead of converter does not exist",
):
qml.from_qiskit("Test")
with pytest.raises(ValueError, match=r"Some Other Error"):
method("Test")

@pytest.mark.parametrize(
"method,entry_point_name",
"method, entry_point_name",
[
(qml.from_qiskit, "qiskit"),
(qml.from_qiskit_op, "qiskit_op"),
(qml.from_qasm, "qasm"),
(qml.from_qasm_file, "qasm_file"),
(qml.from_pyquil, "pyquil_program"),
Expand All @@ -115,7 +128,7 @@ def mock_load_with_error(*args, **kwargs):
],
)
def test_convenience_functions(self, method, entry_point_name, mock_plugin_converters):
"""Test that the convenience load functions access the correct entrypoint."""
"""Test that the convenience load functions access the correct entry point."""

method("Test")

Expand All @@ -130,21 +143,30 @@ def test_convenience_functions(self, method, entry_point_name, mock_plugin_conve
raise RuntimeError(f"The other plugin converter {plugin_converter} was called.")

@pytest.mark.parametrize(
"method, entry_point_name",
"method, entry_point_name, args, kwargs",
[
(qml.from_qiskit, "qiskit"),
(qml.from_qiskit, "qiskit", ("Circuit",), {"measurements": []}),
(qml.from_qiskit_op, "qiskit_op", ("Op",), {"params": [1, 2], "wires": [3, 4]}),
],
)
def test_convenience_functions_kwargs(self, method, entry_point_name, mock_plugin_converters):
"""Test that the convenience load functions access the correct entrypoint with keywords."""

method("Test", measurements=[])
def test_convenience_function_arguments(
self,
method,
entry_point_name,
mock_plugin_converters,
args,
kwargs,
): # pylint: disable=too-many-arguments
"""Test that the convenience load functions access the correct entry point and forward their
arguments correctly.
"""
method(*args, **kwargs)

assert mock_plugin_converters[entry_point_name].called

args, kwargs = mock_plugin_converters[entry_point_name].call_args
assert args == ("Test",)
assert kwargs == {"measurements": []}
called_args, called_kwargs = mock_plugin_converters[entry_point_name].call_args
assert called_args == args
assert called_kwargs == kwargs

for plugin_converter in mock_plugin_converters:
if plugin_converter == entry_point_name:
Expand Down
Loading