Skip to content

Commit

Permalink
Add function to load Qiskit operators (#5251)
Browse files Browse the repository at this point in the history
**Context:**

The next release of the PennyLane-Qiskit plugin will include PennyLaneAI/pennylane-qiskit#401 which features a new function for converting Qiskit [SparsePauliOp](https://docs.quantum.ibm.com/api/qiskit/qiskit.quantum_info.SparsePauliOp) instances into PennyLane operators.

**Description of the Change:**

* Added a `from_qiskit_op()` function to the IO module for loading Qiskit operators.

In a nutshell,

```python
>>> import pennylane as qml
>>> from qiskit.quantum_info import SparsePauliOp
>>> qiskit_op = SparsePauliOp(["II", "XY"])
>>> qml.from_qiskit_op(qiskit_op)
I(0) + X(1) @ Y(0)
```

**Benefits:**

* Qiskit operators can be directly converted into PennyLane operators.

**Possible Drawbacks:**

None.

**Related GitHub Issues:**

None.

---------

Co-authored-by: Thomas R. Bromley <[email protected]>
  • Loading branch information
Mandrenkov and trbromley authored Feb 23, 2024
1 parent 880b9da commit 6ff7a1f
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 44 deletions.
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`.
[(#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>`_
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)
.. 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
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

0 comments on commit 6ff7a1f

Please sign in to comment.