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