Skip to content
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

Add importer for OpenQASM 3 #9347

Merged
merged 12 commits into from
Jan 19, 2023
179 changes: 156 additions & 23 deletions qiskit/qasm3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,126 @@
# that they have been altered from the originals.

"""
==========================
Qasm (:mod:`qiskit.qasm3`)
==========================
================================
OpenQASM 3 (:mod:`qiskit.qasm3`)
================================

.. currentmodule:: qiskit.qasm3

.. autosummary::
:toctree: ../stubs/
Qiskit provides some tools for converting between `OpenQASM 3 <https://openqasm.com>`__
representations of quantum programs, and the :class:`.QuantumCircuit` class. These will continue to
evolve as Qiskit's support for the dynamic-circuit capabilities expressed by OpenQASM 3 increases.

Exporter
dumps
dump

Exporting to OpenQASM 3
=======================

The high-level functions are simply :func:`dump` and :func:`dumps`, which respectively export to a
file (given as a filename) and to a Python string.

.. autofunction:: dump
.. autofunction:: dumps

Both of these exporter functions are single-use wrappers around the main :class:`Exporter` class.
For more complex exporting needs, including dumping multiple circuits in a single session, it may be
more convenient or faster to use the complete interface.

.. autoclass:: Exporter
:members:

All of these interfaces will raise :exc:`QASM3ExporterError` on failure.

.. autoexception:: QASM3ExporterError


Importing from OpenQASM 3
=========================

Currently only two high-level functions are offered, as Qiskit support for importing from OpenQASM 3
is in its infancy, and the implementation is expected to change significantly. The two functions
are :func:`load` and :func:`loads`, which are direct counterparts of :func:`dump` and :func:`dumps`,
respectively loading a program indirectly from a named file and directly from a given string.

.. note::

While we are still in the exploratory release period, to use either function, the package
``qiskit_qasm3_import`` must be installed. This can be done by installing Qiskit Terra with the
``qasm3-import`` extra, such as by:

.. code-block:: text

pip install qiskit-terra[qasm3-import]

We expect that this functionality will eventually be merged into core Terra, and no longer
require an optional import, but we do not yet have a timeline for this.

.. autofunction:: load
.. autofunction:: loads
Comment on lines +67 to +68
Copy link
Contributor

Choose a reason for hiding this comment

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

I do not have any high-level disagreement with these function names on their own. It is however worth pointing out that this API breaks from the current OpenQASM2 APIs - https://qiskit.org/documentation/stubs/qiskit.qasm.Qasm.html#qiskit.qasm.Qasm.

I wonder if it might make sense to begin renaming the older API as qasm2 (with module deprecation paths) and to extend the QuantumCircuit methods for OpenQASM3 support?

This could be follow-up work.

Copy link
Member Author

Choose a reason for hiding this comment

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

While I was on holiday, I actually wrote a whole new OpenQASM 2 parser for Qiskit, with the majority in Rust. It was mostly for my own learning experience, but since it's like 10x faster than the current Qiskit one (with no external Python dependencies), I might make a PR changing it.

With or without though, though, I think we should move to a new qasm2 module with the names load, loads, dump and dumps. I'm not keen to add an extra method to QuantumCircuit because the class is already massively overloaded - I'd rather we keep things in separate packages. The load/dump terminology is the standard Python names (e.g. pickle.dump, json.dump, etc), and it's consistent with the currently existing qasm3.dump and qpy.dump as well - Qiskit is already semi-inconsistent, but really it's the old OQ2 handling that's inconsistent with the rest of the package direction.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks, Jake, I'd be hesitant to add more work and potential breaks to the existing QASM2 parser for those that are using it given we're planning on migrating away from it but the pathway suggested for the qasm2 module seems good.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, agree that there's no reason to break users' workflows. My rough intent was to keep QuantumCircuit.from_qasm_{str,file}, and just cause them to internally call qasm2.load and qasm2.loads as appropriate (assuming I do actually publish that parser and it's suitable for Terra).


Both of these two functions raise :exc:`QASM3ImporterError` on failure.

.. autoexception:: QASM3ImporterError

For example, we can define a quantum program using OpenQASM 3, and use :func:`loads` to directly
convert it into a :class:`.QuantumCircuit`:

.. plot::
:include-source:

import qiskit.qasm3

program = \"\"\"
OPENQASM 3.0;
include "stdgates.inc";

input float[64] a;
qubit[3] q;
bit[2] mid;
bit[3] out;

let aliased = q[0:1];

gate my_gate(a) c, t {
gphase(a / 2);
ry(a) c;
cx c, t;
}
gate my_phase(a) c {
ctrl @ inv @ gphase(a) c;
}

my_gate(a * 2) aliased[0], q[{1, 2}][0];
measure q[0] -> mid[0];
measure q[1] -> mid[1];

while (mid == "00") {
reset q[0];
reset q[1];
my_gate(a) q[0], q[1];
my_phase(a - pi/2) q[1];
mid[0] = measure q[0];
mid[1] = measure q[1];
}

if (mid[0]) {
let inner_alias = q[{0, 1}];
reset inner_alias;
}

out = measure q;
\"\"\"
circuit = qiskit.qasm3.loads(program)
circuit.draw("mpl")
"""

from qiskit.utils import optionals as _optionals
from .exporter import Exporter
from .exceptions import QASM3Error, QASM3ExporterError
from .exceptions import QASM3Error, QASM3ImporterError, QASM3ExporterError


def dumps(circuit, **kwargs) -> str:
"""Serialize a :class:`~qiskit.circuit.QuantumCircuit` object in an OpenQASM3 string.

.. note::

This is a quick interface to the main :obj:`.Exporter` interface. All keyword arguments to
this function are inherited from the constructor of that class, and if you have multiple
circuits to export, it will be faster to create an :obj:`.Exporter` instance, and use its
:obj:`.Exporter.dumps` method.

Args:
circuit (QuantumCircuit): Circuit to serialize.
**kwargs: Arguments for the :obj:`.Exporter` constructor.
Expand All @@ -53,17 +145,58 @@ def dump(circuit, stream, **kwargs) -> None:
"""Serialize a :class:`~qiskit.circuit.QuantumCircuit` object as a OpenQASM3 stream to file-like
object.

.. note::

This is a quick interface to the main :obj:`.Exporter` interface. All keyword arguments to
this function are inherited from the constructor of that class, and if you have multiple
circuits to export, it will be faster to create an :obj:`.Exporter` instance, and use its
:obj:`.Exporter.dump` method.

Args:
circuit (QuantumCircuit): Circuit to serialize.
stream (TextIOBase): stream-like object to dump the OpenQASM3 serialization
**kwargs: Arguments for the :obj:`.Exporter` constructor.

"""
Exporter(**kwargs).dump(circuit, stream)


@_optionals.HAS_QASM3_IMPORT.require_in_call("loading from OpenQASM 3")
def load(filename: str):
"""Load an OpenQASM 3 program from the file ``filename``.

Args:
filename: the filename to load the program from.

Returns:
QuantumCircuit: a circuit representation of the OpenQASM 3 program.

Raises:
QASM3ImporterError: if the OpenQASM 3 file is invalid, or cannot be represented by a
:class:`.QuantumCircuit`.
"""

import qiskit_qasm3_import

with open(filename, "r") as fptr:
program = fptr.read()
jlapeyre marked this conversation as resolved.
Show resolved Hide resolved
try:
return qiskit_qasm3_import.parse(program)
except qiskit_qasm3_import.ConversionError as exc:
raise QASM3ImporterError(str(exc)) from exc


@_optionals.HAS_QASM3_IMPORT.require_in_call("loading from OpenQASM 3")
def loads(program: str):
"""Load an OpenQASM 3 program from the given string.

Args:
program: the OpenQASM 3 program.

Returns:
QuantumCircuit: a circuit representation of the OpenQASM 3 program.

Raises:
QASM3ImporterError: if the OpenQASM 3 file is invalid, or cannot be represented by a
:class:`.QuantumCircuit`.
"""

import qiskit_qasm3_import

try:
return qiskit_qasm3_import.parse(program)
except qiskit_qasm3_import.ConversionError as exc:
raise QASM3ImporterError(str(exc)) from exc
4 changes: 4 additions & 0 deletions qiskit/qasm3/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ class QASM3Error(QiskitError):

class QASM3ExporterError(QASM3Error):
"""An error raised during running the OpenQASM 3 exporter."""


class QASM3ImporterError(QASM3Error):
"""An error raised during the OpenQASM 3 importer."""
8 changes: 8 additions & 0 deletions qiskit/utils/optionals.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@
- Various LaTeX-based visualizations, especially the circuit drawers, need access to the
`pylatexenc <https://github.com/phfaist/pylatexenc>`__ project to work correctly.

* - .. py:data:: HAS_QASM3_IMPORT
- The functions :func:`.qasm3.load` and :func:`.qasm3.loads` for importing OpenQASM 3 programs
into :class:`.QuantumCircuit` instances use `an external importer package
<https://qiskit.github.io/qiskit-qasm3-import>`__.

* - .. py:data:: HAS_SEABORN
- Qiskit Terra provides several visualisation tools in the :mod:`.visualization` module. Some
of these are built using `Seaborn <https://seaborn.pydata.org/>`__, which must be installed
Expand Down Expand Up @@ -264,6 +269,9 @@
name="pylatexenc",
install="pip install pylatexenc",
)
HAS_QASM3_IMPORT = _LazyImportTester(
"qiskit_qasm3_import", install="pip install qiskit_qasm3_import"
)
HAS_SEABORN = _LazyImportTester("seaborn", install="pip install seaborn")
HAS_SKLEARN = _LazyImportTester(
{"sklearn.linear_model": ("Ridge", "Lasso")},
Expand Down
48 changes: 48 additions & 0 deletions releasenotes/notes/qasm3-import-0e7e01cb75aa6251.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
features:
- |
Support for importing OpenQASM 3 programs into Qiskit has been added. This can most easily be
accessed using the functions :func:`.qasm3.loads` and :func:`.qasm3.load`, to load a program
directly from a string and indirectly from a filename, respectively. For example, one can now
do::

from qiskit import qasm3

circuit = qasm3.loads("""
OPENQASM 3.0;
include "stdgates.inc";

qubit q;
qubit[5] qr;
bit c;
bit[5] cr;

h q;
c = measure q;

if (c) {
h qr[0];
cx qr[0], qr[1];
cx qr[0], qr[2];
cx qr[0], qr[3];
cx qr[0], qr[4];
} else {
h qr[4];
cx qr[4], qr[3];
cx qr[4], qr[2];
cx qr[4], qr[1];
cx qr[4], qr[0];
}
cr = measure qr;
""")

This will load the program into a :class:`.QuantumCircuit` instance in the variable ``circuit``.
jakelishman marked this conversation as resolved.
Show resolved Hide resolved

Not all OpenQASM 3 features are supported at first, because Qiskit does not yet have a way to
represent advanced classical data processing. The capabilities of the importer will increase
along with the capabilities of the rest of Qiskit. The initial feature set of the importer is
approximately the same set of features that would be output by the exporter (:func:`.qasm3.dump`
and :func:`.qasm3.dumps`).

Note that Qiskit's support of OpenQASM 3 is not meant to provide a totally lossless
representation of :class:`.QuantumCircuit`\ s. For that, consider using :mod:`qiskit.qpy`.
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ scikit-quant<=0.7;platform_system != 'Windows'
jax;platform_system != 'Windows'
jaxlib;platform_system != 'Windows'
docplex
qiskit-qasm3-import; python_version>='3.8'
6 changes: 5 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
)


qasm3_import_extras = [
"qiskit-qasm3-import>=0.1.0",
]
visualization_extras = [
"matplotlib>=3.3",
"ipywidgets>=7.3.0",
Expand Down Expand Up @@ -81,14 +84,15 @@
include_package_data=True,
python_requires=">=3.7",
extras_require={
"qasm3-import": qasm3_import_extras,
"visualization": visualization_extras,
"bip-mapper": bip_requirements,
"crosstalk-pass": z3_requirements,
"csp-layout-pass": csp_requirements,
"toqm": toqm_requirements,
# Note: 'all' only includes extras that are stable and work on the majority of Python
# versions and OSes supported by Terra. You have to ask for anything else explicitly.
"all": visualization_extras + z3_requirements + csp_requirements,
"all": visualization_extras + z3_requirements + csp_requirements + qasm3_import_extras,
},
project_urls={
"Bug Tracker": "https://github.com/Qiskit/qiskit-terra/issues",
Expand Down
Loading