Skip to content
This repository was archived by the owner on Apr 11, 2024. It is now read-only.

Commit 7a4f13f

Browse files
committed
Add support for overriding instructions
This supports users overriding the objects that get emitted for any given quantum instruction in an OpenQASM 2 input. This includes both "custom" instructions that still need to be defined by the OpenQASM 2 file (with a matching signature, but gate bodies are ignored), and "builtins", which are automatically available, but will not raise an error if a compatible signature declaration is given. This allows us to expose a `QISKIT_CUSTOM_INSTRUCTIONS` tuple, which contains all the `qelib1.inc` additions that Terra currently makes as built-ins. This is not _quite_ the same behaviour as Terra (since it technically still needs to see a declaration for each custom gate to make it available), but in practice will be largely the same. This implementation retains type safety in the custom instructions, unlike Terra. Terra will ignore whatever the signature of the definition is, and still attempt to use the same custom objects. This can cause them to raise exceptions due to being constructed incorrectly.
1 parent a4d9c7d commit 7a4f13f

File tree

10 files changed

+759
-84
lines changed

10 files changed

+759
-84
lines changed

docs/changelog.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22
Changelog
33
=========
44

5+
Unreleased
6+
==========
7+
8+
* Added support for specifying custom instructions, both builtin and requiring definitions
9+
inside the OpenQASM 2 file. This is the `custom_instructions` parameter to :func:`.load`
10+
and :func:`.loads`.
11+
12+
* Added a data element :data:`.QISKIT_CUSTOM_INSTRUCTIONS` that can be passed to
13+
`custom_instructions` to cause :mod:`qiskit_qasm2` to :ref:`mostly emulate the behaviour of the
14+
Qiskit methods <qiskit-compatibility>` :meth:`QuantumCircuit.from_qasm_str()
15+
<qiskit.circuit.QuantumCircuit.from_qasm_str>` and
16+
:meth:`~qiskit.circuit.QuantumCircuit.from_qasm_file`.
17+
518
0.2.0 (2023-01-09)
619
==================
720

docs/parse.rst

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,102 @@ specially; it is always found before looking in the include path, and contains e
2020
of the `paper describing the OpenQASM 2 language <https://arxiv.org/abs/1707.03429>`__. The gates
2121
in this include file are mapped to standard gates provided by Qiskit.
2222

23+
You can extend the OpenQASM 2 language by passing an iterable of information on custom instructions
24+
as the argument `custom_instructions`. In files that have compatible definitions for these
25+
instructions, the given `constructor` will be used in place of whatever other handling
26+
:mod:`qiskit_qasm` would have done. These instructions may optionally be marked as `builtin`, which
27+
causes them to not require an ``opaque`` or ``gate`` declaration, but they will silently ignore a
28+
compatible declaration. Either way, it is an error to provide a custom instruction that has a
29+
different number of parameters or qubits as a defined instruction in a parsed program. Each element
30+
of the argument iterable should be a particular data class:
31+
32+
.. autoclass:: CustomInstruction
33+
2334
In cases where the lexer or parser fails due to an invalid OpenQASM 2 file, the conversion functions
2435
will raise an error with a message explaining what the failure is, and where in the file it
2536
occurred.
2637

2738
.. autoexception:: QASM2ParseError
39+
40+
41+
.. _qiskit-compatibility:
42+
43+
Qiskit Compatibility
44+
====================
45+
46+
Qiskit's :meth:`QuantumCircuit.from_qasm_str() <qiskit.circuit.QuantumCircuit.from_qasm_str>` and
47+
:meth:`~qiskit.circuit.QuantumCircuit.from_qasm_file` have a few additions on top of the raw
48+
specification, as Qiskit originally tried to use OpenQASM 2 as a sort of serialisation format, and
49+
expanded their behaviour as Qiskit expanded. This parser under all its defaults implements the
50+
specification more strictly.
51+
52+
In particular, in the Qiskit importers:
53+
54+
* the `include_path` is:
55+
1. ``<qiskit>/qasm/lib``, where ``<qiskit>`` is the root of the installed ``qiskit`` package;
56+
2. the current working directory.
57+
58+
* there are additional instructions defined in ``qelib1.inc``:
59+
``csx a, b``
60+
Controlled :math:`\sqrt X` gate, corresponding to :class:`~qiskit.circuit.library.CSXGate`.
61+
62+
``cu(theta, phi, lambda, gamma) c, t``
63+
The four-parameter version of a controlled-:math:`U`, corresponding to
64+
:class:`~qiskit.circuit.library.CUGate`.
65+
66+
``rxx(theta) a, b``
67+
Two-qubit rotation arond the :math:`XX` axis, corresponding to
68+
:class:`~qiskit.circuit.library.RXXGate`.
69+
70+
``rzz(theta) a, b``
71+
Two-qubit rotation arond the :math:`ZZ` axis, corresponding to
72+
:class:`~qiskit.circuit.library.RZZGate`.
73+
74+
``rccx a, b, c``
75+
The double-controlled :math:`X` gate, but with relative phase differences over the standard
76+
Toffoli gate. This *should* correspond to the Qiskit gate
77+
:class:`~qiskit.circuit.library.RCCXGate`, but the Qiskit converter won't actually output this
78+
type.
79+
80+
``rc3x a, b, c, d``
81+
The triple-controlled :math:`X` gate, but with relative phase differences over the standard
82+
definition. *Should* correspond to :class:`~qiskit.circuit.library.RC3XGate`.
83+
84+
``c3x a, b, c, d``
85+
The triple-controlled :math:`X` gate, corresponding to :class:`~qiskit.circuit.library.C3XGate`.
86+
87+
``c3sqrtx a, b, c, d``
88+
The triple-controlled :math:`\sqrt X` gate. *Should* correspond to
89+
:class:`~qiskit.circuit.library.C3SXGate`.
90+
91+
``c4x a, b, c, d, e``
92+
The quadruple-controlled :math:`X` gate. *Should* correspond to
93+
:class:`~qiskit.circuit.library.C4XGate`.
94+
95+
* if *any* ``opaque`` or ``gate`` definition is given for the name ``delay``, they will attempt to
96+
output a :class:`~qiskit.circuit.Delay` instruction at each call. To function, this expects a
97+
definition compatible with ``opaque delay(t) q;``, where the time ``t`` is given in units of
98+
``dt``. The importer will raise an error on calls to the instruction if there are actually not
99+
exactly one parameter and one qubit, or if the parameter is not integer-valued.
100+
101+
You can emulate this behaviour in :func:`load` and :func:`loads` by setting `include_path`
102+
appropriately (try inspecting the variable ``qiskit.__file__`` to find the installed location), and
103+
by passing a list of :class:`CustomInstruction` instances for each of the custom gates you care
104+
about. To make things easier, we make a tuple available containing all the above instructions
105+
(using the correspondences that Qiskit forgets as well) that you can supply to
106+
`custom_instructions`.
107+
108+
.. py:data:: QISKIT_CUSTOM_INSTRUCTIONS
109+
110+
A tuple containing the extra `custom_instructions` that Qiskit's built-in converters use if
111+
``qelib1.inc`` is included, and there is any definition of a ``delay`` instruction. The gates
112+
in the paper version of ``qelib1.inc`` and ``delay`` all require a compatible declaration
113+
statement to be present within the OpenQASM 2 program, but Qiskit's additions are all marked as
114+
builtins since they are not actually present in any include file this parser sees.
115+
116+
On *all* the gates defined in Qiskit's version of ``qelib1.inc`` and the ``delay`` instruction, it
117+
does not matter how the gates are actually defined and used, Qiskit will always attempt to output
118+
its custom objects for them. This can result in errors during the circuit construction, even after
119+
a successful parse. There is no way to emulate this buggy behaviour in :mod:`qiskit_qasm2`; only an
120+
``include "qelib1.inc";`` statement or the `custom_instructions` argument can cause built-in Qiskit
121+
instructions to be used, and the signatures of these match each other.

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ package_dir =
2020
include_package_data = True
2121
install_requires =
2222
qiskit-terra>=0.21.0
23+
typing_extensions>=4.1.0
2324
zip_safe = False
2425

2526
[options.packages.find]

src-python/qiskit_qasm2/__init__.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
with open(Path(__file__).parent / "VERSION", "r", encoding="utf-8") as _version_file:
66
__version__ = _version_file.read().strip()
77

8-
from . import core, parse
8+
from . import core as _core, parse as _parse # pylint: disable=no-name-in-module
99
from .core import QASM2ParseError
10+
from .parse import CustomInstruction, QISKIT_CUSTOM_INSTRUCTIONS
1011

1112

1213
def _normalize_path(path: Union[str, os.PathLike]) -> str:
@@ -21,7 +22,11 @@ def _normalize_path(path: Union[str, os.PathLike]) -> str:
2122
return str(path)
2223

2324

24-
def loads(string: str, include_path: Iterable[Union[str, os.PathLike]] = (".",)):
25+
def loads(
26+
string: str,
27+
include_path: Iterable[Union[str, os.PathLike]] = (".",),
28+
custom_instructions: Iterable[CustomInstruction] = (),
29+
):
2530
"""Parse an OpenQASM 2 program from a string into a :class:`~qiskit.circuit.QuantumCircuit`.
2631
2732
:param string: The OpenQASM 2 program in a string.
@@ -30,15 +35,25 @@ def loads(string: str, include_path: Iterable[Union[str, os.PathLike]] = (".",))
3035
:return: A circuit object representing the same OpenQASM 2 program.
3136
:rtype: ~qiskit.circuit.QuantumCircuit
3237
"""
33-
return parse.from_bytecode(
34-
core.bytecode_from_string(string, [_normalize_path(path) for path in include_path])
38+
custom_instructions = list(custom_instructions)
39+
return _parse.from_bytecode(
40+
_core.bytecode_from_string(
41+
string,
42+
[_normalize_path(path) for path in include_path],
43+
[
44+
_core.CustomInstruction(x.name, x.n_params, x.n_qubits, x.builtin)
45+
for x in custom_instructions
46+
],
47+
),
48+
custom_instructions,
3549
)
3650

3751

3852
def load(
3953
filename: Union[str, os.PathLike],
4054
include_path: Iterable[Union[str, os.PathLike]] = (".",),
4155
include_input_directory: Optional[Literal["append", "prepend"]] = "append",
56+
custom_instructions: Iterable[CustomInstruction] = (),
4257
):
4358
"""Parse an OpenQASM 2 program from a file into a :class:`~qiskit.circuit.QuantumCircuit`. The
4459
given path should be ASCII or UTF-8 encoded, and contain the OpenQASM 2 program.
@@ -65,4 +80,15 @@ def load(
6580
f"unknown value for include_input_directory: '{include_input_directory}'."
6681
" Valid values are '\"append\"', '\"prepend\"' and 'None'."
6782
)
68-
return parse.from_bytecode(core.bytecode_from_file(_normalize_path(filename), include_path))
83+
custom_instructions = tuple(custom_instructions)
84+
return _parse.from_bytecode(
85+
_core.bytecode_from_file(
86+
_normalize_path(filename),
87+
include_path,
88+
[
89+
_core.CustomInstruction(x.name, x.n_params, x.n_qubits, x.builtin)
90+
for x in custom_instructions
91+
],
92+
),
93+
custom_instructions,
94+
)

src-python/qiskit_qasm2/parse.py

Lines changed: 91 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
1+
import dataclasses
12
import math
3+
from typing import Iterable, Callable, Tuple
4+
5+
from typing_extensions import Unpack
26

37
from qiskit.circuit import (
4-
QuantumCircuit,
5-
QuantumRegister,
6-
ClassicalRegister,
7-
Measure,
8-
Reset,
98
Barrier,
109
CircuitInstruction,
10+
ClassicalRegister,
11+
Delay,
1112
Gate,
13+
Instruction,
14+
Measure,
15+
QuantumCircuit,
16+
QuantumRegister,
1217
Qubit,
18+
Reset,
1319
library as lib,
1420
)
1521
from .core import (
@@ -20,6 +26,7 @@
2026
ExprArgument,
2127
ExprUnary,
2228
ExprBinary,
29+
QASM2ParseError,
2330
)
2431

2532
# Constructors of the form `*params -> Gate` for the special 'qelib1.inc' include. This is
@@ -51,7 +58,73 @@
5158
)
5259

5360

54-
def from_bytecode(bytecode):
61+
@dataclasses.dataclass
62+
class CustomInstruction:
63+
"""Information about a custom instruction that should be defined during the parse.
64+
65+
The `name`, `n_params` and `n_qubits` fields are self-explanatory. The `constructor` field
66+
should be a callable object with signature ``*args -> Instruction``, where each of the
67+
`n_params` `args` is a floating-point value. Most of the built-in Qiskit gate classes have this
68+
form.
69+
70+
There is a final `builtin` field. This is optional, and if set true will cause the instruction
71+
to be defined and available within the parsing, even if there is no definition in any included
72+
OpenQASM 2 file."""
73+
74+
name: str
75+
n_params: int
76+
n_qubits: int
77+
constructor: Callable[[Unpack[Tuple[float, ...]]], Instruction]
78+
builtin: bool = False
79+
80+
81+
def _generate_delay(t: float):
82+
# This wrapper is just to ensure that the correct type of exception gets emitted; it would be
83+
# unnecessarily spaghetti-ish to check every emitted instruction in Rust for integer
84+
# compatibility, but only if its name is `delay` _and_ its constructor wraps Qiskit's `Delay`.
85+
if int(t) != t:
86+
raise QASM2ParseError("the custom 'delay' instruction can only accept an integer parameter")
87+
return Delay(int(t), unit="dt")
88+
89+
90+
QISKIT_CUSTOM_INSTRUCTIONS = (
91+
CustomInstruction("u3", 3, 1, lib.U3Gate),
92+
CustomInstruction("u2", 2, 1, lib.U2Gate),
93+
CustomInstruction("u1", 1, 1, lib.U1Gate),
94+
CustomInstruction("cx", 0, 2, lib.CXGate),
95+
CustomInstruction("id", 0, 1, lambda: lib.UGate(0, 0, 0)),
96+
CustomInstruction("x", 0, 1, lib.XGate),
97+
CustomInstruction("y", 0, 1, lib.YGate),
98+
CustomInstruction("z", 0, 1, lib.ZGate),
99+
CustomInstruction("h", 0, 1, lib.HGate),
100+
CustomInstruction("s", 0, 1, lib.SGate),
101+
CustomInstruction("sdg", 0, 1, lib.SdgGate),
102+
CustomInstruction("t", 0, 1, lib.TGate),
103+
CustomInstruction("tdg", 0, 1, lib.TdgGate),
104+
CustomInstruction("rx", 1, 1, lib.RXGate),
105+
CustomInstruction("ry", 1, 1, lib.RYGate),
106+
CustomInstruction("rz", 1, 1, lib.RZGate),
107+
CustomInstruction("cz", 0, 2, lib.CZGate),
108+
CustomInstruction("cy", 0, 2, lib.CYGate),
109+
CustomInstruction("ch", 0, 2, lib.CHGate),
110+
CustomInstruction("ccx", 0, 3, lib.CCXGate),
111+
CustomInstruction("crz", 1, 2, lib.CRZGate),
112+
CustomInstruction("cu1", 1, 2, lib.CU1Gate),
113+
CustomInstruction("cu3", 3, 2, lambda a, b, c: lib.CUGate(a, b, c, 0)),
114+
CustomInstruction("csx", 0, 2, lib.CSXGate, builtin=True),
115+
CustomInstruction("cu", 4, 2, lib.CUGate, builtin=True),
116+
CustomInstruction("rxx", 1, 2, lib.RXXGate, builtin=True),
117+
CustomInstruction("rzz", 1, 2, lib.RZZGate, builtin=True),
118+
CustomInstruction("rccx", 0, 3, lib.RCCXGate, builtin=True),
119+
CustomInstruction("rc3x", 0, 4, lib.RC3XGate, builtin=True),
120+
CustomInstruction("c3x", 0, 4, lib.C3XGate, builtin=True),
121+
CustomInstruction("c3sqrtx", 0, 4, lib.C3SXGate, builtin=True),
122+
CustomInstruction("c4x", 0, 5, lib.C4XGate, builtin=True),
123+
CustomInstruction("delay", 1, 1, _generate_delay),
124+
)
125+
126+
127+
def from_bytecode(bytecode, custom_instructions: Iterable[CustomInstruction]):
55128
"""Loop through the Rust bytecode iterator `bytecode` producing a
56129
:class:`~qiskit.circuit.QuantumCircuit` instance from it. All the hard work is done in Rust
57130
space where operations are faster; here, we're just about looping through the instructions as
@@ -72,7 +145,18 @@ def from_bytecode(bytecode):
72145
qc = QuantumCircuit()
73146
qubits = []
74147
clbits = []
75-
gates = [lib.UGate, lib.CXGate]
148+
gates = []
149+
has_u, has_cx = False, False
150+
for custom in custom_instructions:
151+
gates.append(custom.constructor)
152+
if custom.name == "U":
153+
has_u = True
154+
elif custom.name == "CX":
155+
has_cx = True
156+
if not has_u:
157+
gates.append(lib.UGate)
158+
if not has_cx:
159+
gates.append(lib.CXGate)
76160
# Pull this out as an explicit iterator so we can manually advance the loop in `DeclareGate`
77161
# contexts easily.
78162
bc = iter(bytecode)

src-rust/bytecode.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use pyo3::prelude::*;
33
use crate::expr::Expr;
44
use crate::lex;
55
use crate::parse;
6-
use crate::QASM2ParseError;
6+
use crate::{CustomInstruction, QASM2ParseError};
77

88
/// The Rust parser produces an iterator of these `Bytecode` instructions, which comprise an opcode
99
/// integer for operation distinction, and a free-form tuple containing the operands.
@@ -283,12 +283,17 @@ pub struct BytecodeIterator {
283283
}
284284

285285
impl BytecodeIterator {
286-
pub fn new(tokens: lex::TokenStream, include_path: Vec<std::path::PathBuf>) -> Self {
287-
BytecodeIterator {
288-
parser_state: parse::State::new(tokens, include_path),
286+
pub fn new(
287+
tokens: lex::TokenStream,
288+
include_path: Vec<std::path::PathBuf>,
289+
custom_instructions: &[CustomInstruction],
290+
) -> PyResult<Self> {
291+
Ok(BytecodeIterator {
292+
parser_state: parse::State::new(tokens, include_path, custom_instructions)
293+
.map_err(QASM2ParseError::new_err)?,
289294
buffer: vec![],
290295
buffer_used: 0,
291-
}
296+
})
292297
}
293298
}
294299

0 commit comments

Comments
 (0)