diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 1df4c4769c9..265087f35e8 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -4,6 +4,36 @@

New features since last release

+* A new function called `qml.to_openqasm` has been added, which allows for converting PennyLane circuits to OpenQASM 2.0 programs. + [(#7393)](https://github.com/PennyLaneAI/pennylane/pull/7393) + + Consider this simple circuit in PennyLane: + ```python + dev = qml.device("default.qubit", wires=2, shots=100) + + @qml.qnode(dev) + def circuit(theta, phi): + qml.RX(theta, wires=0) + qml.CNOT(wires=[0,1]) + qml.RZ(phi, wires=1) + return qml.sample() + ``` + + This can be easily converted to OpenQASM 2.0 with `qml.to_openqasm`: + ```pycon + >>> openqasm_circ = qml.to_openqasm(circuit)(1.2, 0.9) + >>> print(openqasm_circ) + OPENQASM 2.0; + include "qelib1.inc"; + qreg q[2]; + creg c[2]; + rx(1.2) q[0]; + cx q[0],q[1]; + rz(0.9) q[1]; + measure q[0] -> c[0]; + measure q[1] -> c[1]; + ``` + * A new template called :class:`~.SelectPauliRot` that applies a sequence of uniformly controlled rotations to a target qubit is now available. This operator appears frequently in unitary decomposition and block encoding techniques. [(#7206)](https://github.com/PennyLaneAI/pennylane/pull/7206) @@ -365,6 +395,7 @@ Astral Cai, Yushao Chen, Lillian Frederiksen, Pietropaolo Frisoni, +Simone Gasperini, Korbinian Kottmann, Christina Lee, Lee J. O'Riordan, diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 4b6914c5617..4c47482fe38 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -78,6 +78,7 @@ from pennylane.io import ( from_pyquil, from_qasm, + to_openqasm, from_qiskit, from_qiskit_noise, from_qiskit_op, diff --git a/pennylane/io/__init__.py b/pennylane/io/__init__.py index 7ec133cb27d..e91cac4132d 100644 --- a/pennylane/io/__init__.py +++ b/pennylane/io/__init__.py @@ -18,6 +18,7 @@ from .io import ( from_pyquil, from_qasm, + to_openqasm, from_qiskit, from_qiskit_noise, from_qiskit_op, diff --git a/pennylane/io/io.py b/pennylane/io/io.py index d2678727e1d..f6f99f4baee 100644 --- a/pennylane/io/io.py +++ b/pennylane/io/io.py @@ -16,8 +16,13 @@ PennyLane templates. """ from collections import defaultdict +from collections.abc import Callable +from functools import wraps from importlib import metadata from sys import version_info +from typing import Any, Optional + +from pennylane.wires import WiresLike # Error message to show when the PennyLane-Qiskit plugin is required but missing. _MISSING_QISKIT_PLUGIN_MESSAGE = ( @@ -613,6 +618,126 @@ def circuit(x): return plugin_converter(quantum_circuit, measurements=measurements) +def to_openqasm( + qnode, + wires: Optional[WiresLike] = None, + rotations: bool = True, + measure_all: bool = True, + precision: Optional[int] = None, +) -> Callable[[Any], str]: + """Convert a circuit to an OpenQASM 2.0 program. + + .. note:: + Terminal measurements are assumed to be performed on all qubits in the computational basis. + An optional ``rotations`` argument can be provided so that the output of the OpenQASM circuit + is diagonal in the eigenbasis of the quantum circuit's observables. + The measurement outputs can be restricted to only those specified in the circuit by setting ``measure_all=False``. + + Args: + wires (Wires or None): the wires to use when serializing the circuit. + Default is ``None``, such that all the wires are used for serialization. + rotations (bool): if ``True``, add gates that diagonalize the measured wires to the eigenbasis + of the circuit's observables. Default is ``True``. + measure_all (bool): if ``True``, add a computational basis measurement on all the qubits. + Default is ``True``. + precision (int or None): number of decimal digits to display for the parameters. + + Returns: + str: OpenQASM 2.0 program corresponding to the circuit. + + **Example** + + The following QNode can be serialized to an OpenQASM 2.0 program: + + .. code-block:: python + + dev = qml.device("default.qubit", wires=2, shots=100) + + @qml.qnode(dev) + def circuit(theta, phi): + qml.RX(theta, wires=0) + qml.CNOT(wires=[0,1]) + qml.RZ(phi, wires=1) + return qml.sample() + + >>> print(qml.to_openqasm(circuit)(1.2, 0.9)) + OPENQASM 2.0; + include "qelib1.inc"; + qreg q[2]; + creg c[2]; + rx(1.2) q[0]; + cx q[0],q[1]; + rz(0.9) q[1]; + measure q[0] -> c[0]; + measure q[1] -> c[1]; + + .. details:: + :title: Usage Details + + By default, the resulting OpenQASM code will have terminal measurements on all qubits, where all the measurements are performed in the computational basis. + However, if terminal measurements in the QNode act only on a subset of the qubits and ``measure_all=False``, + the OpenQASM code will include measurements on those specific qubits only. + + .. code-block:: python + + dev = qml.device("default.qubit", wires=2, shots=100) + + @qml.qnode(dev) + def circuit(): + qml.Hadamard(0) + qml.CNOT(wires=[0,1]) + return qml.sample(wires=1) + + >>> print(qml.to_openqasm(circuit, measure_all=False)()) + OPENQASM 2.0; + include "qelib1.inc"; + qreg q[2]; + creg c[2]; + h q[0]; + cx q[0],q[1]; + measure q[1] -> c[1]; + + If the ``QNode`` returns an expectation value of a given observable and ``rotations=True``, the OpenQASM program will also + include the gates that diagonalize the measured wires such that they are in the eigenbasis of the measured observable. + + .. code-block:: python + + dev = qml.device("default.qubit", wires=2, shots=100) + + @qml.qnode(dev) + def circuit(): + qml.Hadamard(0) + qml.CNOT(wires=[0,1]) + return qml.expval(qml.PauliX(0) @ qml.PauliY(1)) + + >>> print(qml.to_openqasm(circuit, rotations=True)()) + OPENQASM 2.0; + include "qelib1.inc"; + qreg q[2]; + creg c[2]; + h q[0]; + cx q[0],q[1]; + h q[0]; + z q[1]; + s q[1]; + h q[1]; + measure q[0] -> c[0]; + measure q[1] -> c[1]; + """ + + # pylint: disable=import-outside-toplevel + from pennylane.workflow import construct_tape + + @wraps(qnode) + def wrapper(*args, **kwargs) -> str: + tape = construct_tape(qnode)(*args, **kwargs) + return tape.to_openqasm( + wires=wires, rotations=rotations, measure_all=measure_all, precision=precision + ) + + return wrapper + + def from_pyquil(pyquil_program): """Loads pyQuil Program objects by using the converter in the PennyLane-Rigetti plugin. diff --git a/tests/io/test_io.py b/tests/io/test_io.py index 1232c5b51e5..dc646436d19 100644 --- a/tests/io/test_io.py +++ b/tests/io/test_io.py @@ -14,6 +14,7 @@ """ Unit tests for the :mod:`pennylane.io` module. """ +from textwrap import dedent from unittest.mock import Mock import pytest @@ -193,3 +194,89 @@ def test_convenience_function_arguments( if mock_plugin_converters[plugin_converter].called: raise RuntimeError(f"The other plugin converter {plugin_converter} was called.") + + +class TestToOpenQasm: + """Test the qml.to_openqasm function.""" + + dev = qml.device("default.qubit", wires=2, shots=100) + + def test_basic_example(self): + """Test basic usage on simple circuit with parameters.""" + + @qml.qnode(self.dev) + def circuit(theta, phi): + qml.RX(theta, wires=0) + qml.CNOT(wires=[0, 1]) + qml.RZ(phi, wires=1) + return qml.sample() + + qasm = qml.to_openqasm(circuit)(1.2, 0.9) + + expected = dedent( + """\ + OPENQASM 2.0; + include "qelib1.inc"; + qreg q[2]; + creg c[2]; + rx(1.2) q[0]; + cx q[0],q[1]; + rz(0.9) q[1]; + measure q[0] -> c[0]; + measure q[1] -> c[1]; + """ + ) + assert qasm == expected + + def test_measure_qubits_subset_only(self): + """Test OpenQASM program includes measurements only over the qubits subset specified in the QNode.""" + + @qml.qnode(self.dev) + def circuit(): + qml.Hadamard(0) + qml.CNOT(wires=[0, 1]) + return qml.sample(wires=1) + + qasm = qml.to_openqasm(circuit, measure_all=False)() + + expected = dedent( + """\ + OPENQASM 2.0; + include "qelib1.inc"; + qreg q[2]; + creg c[2]; + h q[0]; + cx q[0],q[1]; + measure q[1] -> c[1]; + """ + ) + assert qasm == expected + + def test_rotations_with_expval(self): + """Test OpenQASM program includes gates that make the measured observables diagonal in the computational basis.""" + + @qml.qnode(self.dev) + def circuit(): + qml.Hadamard(0) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliX(0) @ qml.PauliY(1)) + + qasm = qml.to_openqasm(circuit, rotations=True)() + + expected = dedent( + """\ + OPENQASM 2.0; + include "qelib1.inc"; + qreg q[2]; + creg c[2]; + h q[0]; + cx q[0],q[1]; + h q[0]; + z q[1]; + s q[1]; + h q[1]; + measure q[0] -> c[0]; + measure q[1] -> c[1]; + """ + ) + assert qasm == expected