From 6ff7a1fd243f05d9a8726253f599e4b9061c3608 Mon Sep 17 00:00:00 2001 From: Mikhail Andrenkov Date: Fri, 23 Feb 2024 16:19:38 -0500 Subject: [PATCH] Add function to load Qiskit operators (#5251) **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 <49409390+trbromley@users.noreply.github.com> --- doc/releases/changelog-dev.md | 3 + pennylane/io.py | 119 +++++++++++++++++++++++++++++++--- tests/test_io.py | 90 +++++++++++++++---------- 3 files changed, 168 insertions(+), 44 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 8440d2ed91e..70b78a0b71d 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -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) +

Native mid-circuit measurements on default qubit 💡

* When operating in finite-shots mode, the `default.qubit` device now performs mid-circuit diff --git a/pennylane/io.py b/pennylane/io.py index fb5279ba065..edabae6eecc 100644 --- a/pennylane/io.py +++ b/pennylane/io.py @@ -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"] @@ -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 `_ + objects by using the converter in the PennyLane-Qiskit plugin. **Example:** @@ -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 @@ -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 `_ + 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 `_ + 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 `_ + 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 diff --git a/tests/test_io.py b/tests/test_io.py index 9a81b3f136e..095b7be7062 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -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") @@ -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"), @@ -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") @@ -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: