diff --git a/pennylane_qiskit/__init__.py b/pennylane_qiskit/__init__.py index 5b4af934a..1c364a1c1 100644 --- a/pennylane_qiskit/__init__.py +++ b/pennylane_qiskit/__init__.py @@ -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 diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index ba9e7f325..79d447ce9 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -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 @@ -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 @@ -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 `__ + 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: + # 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. diff --git a/requirements.txt b/requirements.txt index cff6eb1e6..b36af16c2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/tests/test_converter.py b/tests/test_converter.py index 13cac38e5..3917582dc 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -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, @@ -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])