Skip to content

Add qml.to_openqasm transform #7393

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d2b3632
Draft minimal version of the to_openqasm transform
SimoneGasperini May 7, 2025
bc84b14
Fix copyright year
SimoneGasperini May 7, 2025
00ca12d
Move the function to the io module
SimoneGasperini May 7, 2025
97996f2
Fix to_openqasm function signature
SimoneGasperini May 7, 2025
41e27fe
Fix code formatting
SimoneGasperini May 7, 2025
c26c140
Merge branch 'master' into openqasm-transform
SimoneGasperini May 7, 2025
53f0a3f
Add docstring including example and usage details
SimoneGasperini May 8, 2025
11d7830
Merge branch 'openqasm-transform' of github.com:PennyLaneAI/pennylane…
SimoneGasperini May 8, 2025
a8be15d
Merge branch 'master' into openqasm-transform
SimoneGasperini May 8, 2025
6497853
Merge branch 'master' into openqasm-transform
SimoneGasperini May 8, 2025
a8c925c
Add basic unit tests
SimoneGasperini May 8, 2025
ad5ca97
Merge branch 'master' into openqasm-transform
SimoneGasperini May 8, 2025
e4727f4
Update the changelog
SimoneGasperini May 8, 2025
72c5ac0
Remove duplicate test
SimoneGasperini May 8, 2025
a9f20fe
Update doc/releases/changelog-dev.md
SimoneGasperini May 8, 2025
3f34bf1
Fix typos
SimoneGasperini May 8, 2025
e049cd0
Minor fix to docstring
SimoneGasperini May 8, 2025
bd3a783
Merge branch 'master' into openqasm-transform
SimoneGasperini May 8, 2025
9eb79e2
Merge branch 'master' into openqasm-transform
SimoneGasperini May 9, 2025
0380827
Move changelog entry on top
SimoneGasperini May 9, 2025
5a87a13
Fix docstring
SimoneGasperini May 9, 2025
55036b1
More minor fixes to docstring
SimoneGasperini May 9, 2025
5671127
Merge branch 'master' into openqasm-transform
SimoneGasperini May 9, 2025
6975858
Apply suggested docstring fixes
SimoneGasperini May 9, 2025
7d2e729
Add usage example to changelog
SimoneGasperini May 9, 2025
01249de
Minor fix to changelog
SimoneGasperini May 9, 2025
56d3a92
Minor fix
SimoneGasperini May 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,36 @@

<h3>New features since last release</h3>

* 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)
Expand Down Expand Up @@ -365,6 +395,7 @@ Astral Cai,
Yushao Chen,
Lillian Frederiksen,
Pietropaolo Frisoni,
Simone Gasperini,
Korbinian Kottmann,
Christina Lee,
Lee J. O'Riordan,
Expand Down
1 change: 1 addition & 0 deletions pennylane/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
from pennylane.io import (
from_pyquil,
from_qasm,
to_openqasm,
from_qiskit,
from_qiskit_noise,
from_qiskit_op,
Expand Down
1 change: 1 addition & 0 deletions pennylane/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .io import (
from_pyquil,
from_qasm,
to_openqasm,
from_qiskit,
from_qiskit_noise,
from_qiskit_op,
Expand Down
125 changes: 125 additions & 0 deletions pennylane/io/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down Expand Up @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"all the wires" meaning all of the wires used in the original circuit in PL?

Copy link
Contributor Author

@SimoneGasperini SimoneGasperini May 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, all the wires in the original QNode

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.
Expand Down
87 changes: 87 additions & 0 deletions tests/io/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""
Unit tests for the :mod:`pennylane.io` module.
"""
from textwrap import dedent
from unittest.mock import Mock

import pytest
Expand Down Expand Up @@ -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