diff --git a/qiskit/qasm3/__init__.py b/qiskit/qasm3/__init__.py index 8218141c1d22..f9346bfb1ebe 100644 --- a/qiskit/qasm3/__init__.py +++ b/qiskit/qasm3/__init__.py @@ -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 `__ +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 + +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. @@ -53,13 +145,6 @@ 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 @@ -67,3 +152,51 @@ def dump(circuit, stream, **kwargs) -> None: """ 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() + 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 diff --git a/qiskit/qasm3/exceptions.py b/qiskit/qasm3/exceptions.py index b853b6e74345..cae30145d72c 100644 --- a/qiskit/qasm3/exceptions.py +++ b/qiskit/qasm3/exceptions.py @@ -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.""" diff --git a/qiskit/utils/optionals.py b/qiskit/utils/optionals.py index 3e808185c80b..0f8d388301cc 100644 --- a/qiskit/utils/optionals.py +++ b/qiskit/utils/optionals.py @@ -117,6 +117,11 @@ - Various LaTeX-based visualizations, especially the circuit drawers, need access to the `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 + `__. + * - .. py:data:: HAS_SEABORN - Qiskit Terra provides several visualisation tools in the :mod:`.visualization` module. Some of these are built using `Seaborn `__, which must be installed @@ -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")}, diff --git a/releasenotes/notes/qasm3-import-0e7e01cb75aa6251.yaml b/releasenotes/notes/qasm3-import-0e7e01cb75aa6251.yaml new file mode 100644 index 000000000000..ca0df28d7228 --- /dev/null +++ b/releasenotes/notes/qasm3-import-0e7e01cb75aa6251.yaml @@ -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``. + + 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`. diff --git a/requirements-dev.txt b/requirements-dev.txt index 74015d6f504d..c77071298ded 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -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' diff --git a/setup.py b/setup.py index 29c93a427d64..bc357f0f9591 100755 --- a/setup.py +++ b/setup.py @@ -33,6 +33,9 @@ ) +qasm3_import_extras = [ + "qiskit-qasm3-import>=0.1.0", +] visualization_extras = [ "matplotlib>=3.3", "ipywidgets>=7.3.0", @@ -81,6 +84,7 @@ 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, @@ -88,7 +92,7 @@ "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", diff --git a/test/python/qasm3/test_import.py b/test/python/qasm3/test_import.py new file mode 100644 index 000000000000..dcc0ba3f823e --- /dev/null +++ b/test/python/qasm3/test_import.py @@ -0,0 +1,84 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring + +# Since the import is nearly entirely delegated to an external package, most of the testing is done +# there. Here we need to test our wrapping behaviour for base functionality and exceptions. We +# don't want to get into a situation where updates to `qiskit_qasm3_import` breaks Terra's test +# suite due to too specific tests on the Terra side. + +import os +import tempfile +import unittest + +from qiskit import qasm3 +from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister +from qiskit.test import QiskitTestCase +from qiskit.utils import optionals + + +class TestQASM3Import(QiskitTestCase): + @unittest.skipUnless( + optionals.HAS_QASM3_IMPORT, "need qiskit-qasm3-import for OpenQASM 3 imports" + ) + def test_import_errors_converted(self): + with self.assertRaises(qasm3.QASM3ImporterError): + qasm3.loads("OPENQASM 3.0; qubit[2.5] q;") + + @unittest.skipUnless( + optionals.HAS_QASM3_IMPORT, "need qiskit-qasm3-import for OpenQASM 3 imports" + ) + def test_loads_can_succeed(self): + program = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] qr; + bit[2] cr; + h qr[0]; + cx qr[0], qr[1]; + cr[0] = measure qr[0]; + cr[1] = measure qr[1]; + """ + parsed = qasm3.loads(program) + expected = QuantumCircuit(QuantumRegister(2, "qr"), ClassicalRegister(2, "cr")) + expected.h(0) + expected.cx(0, 1) + expected.measure(0, 0) + expected.measure(1, 1) + self.assertEqual(parsed, expected) + + @unittest.skipUnless( + optionals.HAS_QASM3_IMPORT, "need qiskit-qasm3-import for OpenQASM 3 imports" + ) + def test_load_can_succeed(self): + program = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] qr; + bit[2] cr; + h qr[0]; + cx qr[0], qr[1]; + cr[0] = measure qr[0]; + cr[1] = measure qr[1]; + """ + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_path = os.path.join(tmp_dir, "bell.qasm") + with open(tmp_path, "w") as fptr: + fptr.write(program) + parsed = qasm3.load(tmp_path) + expected = QuantumCircuit(QuantumRegister(2, "qr"), ClassicalRegister(2, "cr")) + expected.h(0) + expected.cx(0, 1) + expected.measure(0, 0) + expected.measure(1, 1) + self.assertEqual(parsed, expected)