Expressing Quantum Programs in Q# and OpenQASM
With the rising popularity of quantum computing over the past years, open-source frameworks for developing quantum programs mature and form substantial ecosystems around them. Microsoft's Quantum Development Kit (QDK) and Qiskit by IBM are two major open-source frameworks. They present different approaches and tools for developing quantum algorithms and executing them on simulators or quantum hardware. However, since their goal and audience overlap, the two frameworks share many similarities. This blog post focuses on the main quantum programming languages of the two frameworks: Q# (QDK) and OpenQASM (Qiskit).
The Q# language is a part of the .NET family (C# and F#) and is a domain-specific language for programming on quantum computers. It is a strongly typed language that includes particular quantum types, statements, and operations, such as Qubit
and Measurement
, or specifying whether an operation can be inverted or controlled by other qubits. The OpenQASM language is often described as the open quantum assembly language (comparable to the classical Verilog) and is the primary exportable format from Qiskit's code in Python. Typically, the users of the Qiskit framework do not write OpenQASM code themselves (although it is possible) but use the Python packages that implement some of the logic in OpenQASM. OpenQASM 2.0 was announced in 2017, and since then, it has been adopted and developed. The pre-release of OpenQASM 3.0 is already available and is being incorporated into the Qiskit framework.
In this post, we highlight some aspects of expressing quantum programs and present how they are implemented in Q# versus OpenQASM programs. Effectively, the full expressiveness power of a language is limited by its grammar and additional rules. The grammar specifications of OpenQASM 3.0 and Q# are both defined with the ANTLR v4 format, which is the first similarity between the two languages. A central difference is that OpenQASM is a low-level language intended to be an intermediate representation, while Q# is a high-level language for developing quantum algorithms. Note that unless stated otherwise, "OpenQASM" means here OpenQASM 3.0, as many of the discussed points below have been added to OpenQASM only in version 3.0.
1. Basis gates
A cornerstone in quantum computing is the possibility to build every unitary gate from a predefined set of gates (a universal gate set). OpenQASM 2.0 defines a minimalistic universal quantum gate set of two gates: the generic single-qubit unitary U(θ, φ, λ)
and the two-qubit controlled X gate CX
. These are the only predefined gates in OpenQASM 2.0. Every other gate in the language is explicitly derived from these two. For example, consider the following snippet from a .qasm
file, in which the custom (but widely used) ry
and swap
gates are defined:
// Defining gates in OpenQASM
gate ry(theta) a {
U(theta, 0, 0) a;
}
gate swap a, b {
CX a, b;
CX b, a;
CX a, b;
}
Many OpenQASM programs use standard gates from common extension libraries. The addition of these gates is directed by include "qelib1.inc"
in the .qasm
file, for example.
In Q#, the standard library includes the "Intrinsic" (open Microsoft.Quantum.Intrinsic
) and "Canonical" (open Microsoft.Quantum.Canon
) namespaces, which define many basic and advanced quantum operations. Some basic operations are the standard Pauli gates (X
, Y
, Z
), conditional Paulis (CNOT
, CCNOT
, CY
, CZ
), rotations (R
), whereas the more advanced operations contain QFT
, GreaterThan
, and ApplyCNOTChain
.
In OpenQASM, a lower-level language, the extension libraries usually do not include high-level gates like QFT. These gates and circuits are implemented by higher-level Python wrappers that export the implementation into OpenQASM.
2. Gate modifiers
Suppose you have an operation/gate that you want to apply to some target qubits conditioned on the values of other qubits. This composite gate is the quantum analog of the classical "if" statement. Another prevalent scenario is performing the inverse of a quantum gate (the "adjoint" U† of a unitary operation U). Specifically, this is useful in uncomputation techniques. The two mentioned examples create a new gate from a given one - "gate modifiers". Luckily, both Q# and OpenQASM 3.0 allow users to elegantly extend existing gates into new ones.
Q# defines the Adjoint
and Controlled
functors, which can extend gates. Developers can apply the functors only to operations with the Adj
and Ctl
characteristics, respectively. If a Q# operation's signature includes is Adj
- it means that the operation is adjointable - one can invert it with the Adjoint
functor. The same applies to is Ctl
and Controlled
. These characteristics are a vital part of the Q# typing system. Note that an operation can be both adjointable and controllable (is Adj + Ctl
).
OpenQASM 3.0 introduces the equivalent gate modifiers concept: inv @
, ctrl @
, negctrl @
, and pow @
. Each modifier can be applied to a gate, and one can even use multiple modifiers in the same statement.
Some examples are given in the table below:
Modifier | OpenQASM3 | Q# |
---|---|---|
Inverse | inv @ op q[0], q[1] |
Adjoint op(q[0], q[1]) |
Controlled gate | ctrl(2) @ op cont[0], cont[1], q[0], q[1] |
Controlled op([cont[0], cont[1]], q) |
Negative control | negctrl(2) @ op cont[0], cont[1], q[0], q[1] |
(*) |
Power | pow(k) @ op q[0], q[1] |
OperationPow(op, k)(q[0], q[1]) (**) |
op
is an OpenQASM gate or a Q# operation (op : 'T => Unit is Adj + Ctl
). The input of op
is two qubits.
For example, the statement inv @ op q[0], q[1]
applies the inverse gate of op
(inv @ op
) to the qubits q[0]
and q[1]
.
* A possible implmentation of negative control is (snippet from a .qs
file):
// Negatively controlled operation in Q#
use q = Qubit[2];
use t = Qubit();
within {
ApplyToEachA(X, q);
}
apply {
Controlled op(q, t);
}
The expression in the within
block is applied before the apply
block, and its Adjoint
is applied afterward.
Note that there are more concise options for expressing the same program, such as using the ApplyControlledOnBitString
operation.
** In Q#, the OperationPow
function supports only integer powers.
3. Parametrized programs
The main operation of a Q# project is marked by the @EntryPoint()
attribute before its signature. Consider the following parametrized Q# program (.qs
file):
// An example for a Q# program that accepts parameters
namespace Quantum.Parametrized {
open Microsoft.Quantum.Canon;
open Microsoft.Quantum.Intrinsic;
open Microsoft.Quantum.Measurement; // for MultiM
@EntryPoint()
operation QuantumProg(numQubits : Int, angles : Double[]) : Result[] {
use qubits = Qubit[numQubits];
// Implement the actual logic
let res = MultiM(qubits);
return res;
}
}
The quantum program can be called from a classical Python or C# host program. The host program can send the input parameters and obtain the outputs. The host program can optimize the parameters and change the quantum program via its interface, allowing a specific scheme of hybrid quantum-classical computation.
OpenQASM 3.0 as well defines an I/O interface for parameters via the input
and output
modifiers:
// An OpenQASM 3.0 program with input and output
OPENQASM 3;
input int num_repetitions;
output bit result;
qubit q;
// Implement the logic here
result = measure q;
The OpenQASM code can be wrapped in Python, useful for variational algorithms.
4. Classical logic
OpenQASM3 and Q# support necessary statements for classical computation. for
and while
loops are supported in both, along with proper classical typing and functions, and enable the composition of sophisticated quantum programs. Without classical logic inside the quantum code, the quantum program is static and described as a quantum circuit. On the contrary, dynamic quantum programs change the execution on the quantum processor in real-time. The classical control opens the way to implementing concurrent quantum-classical programs.
5. Quantum memory management
One of the most powerful features of the Q# language is allocating auxiliary qubits inside an operation and other block statements. One can explicitly allocate a clean qubit (initialized to 0) via the use
statement:
// Allocate a clean qubit in Q#
use qubit = Qubit();
Or a "dirty" qubit via the borrow
statement:
// Allocate a dirty qubit in Q#
borrow qubit = Qubit();
Either way, the Q# developer must release allocated qubits as they were given. Namely, clean qubits must be in the state 0 at the end of their scope (by measurement or a reset, for instance), and dirty qubits must be in the same state as when borrowed.
In OpenQASM (2.0 and 3.0), qubits are declared globally. All the qubits have to be allocated in the main OpenQASM program rather than in gates.
A breaking change between the versions of OpenQASM is to which state qubits are initialized. In OpenQASM 2.0, all the qubits of a quantum register are initialized to the 0 state. In OpenQASM 3.0, allocated qubits are initialized to an undefined state. One should use the reset
statement to assure a 0 state of freshly allocated qubits in OpenQASM 3.0.
// Allocate and initialize qubits to 0 in OpenQASM 3.0
qubit[3] q;
reset q;
Note that there are some use cases for "dirty" qubits so that the reset
is not a completely boilerplate code in OpenQASM 3.0.
Focusing on clean qubits, the reset requirement in Q# and OpenQASM3 is genuinely very similar, although distinctive and quite subtle. The qubits are reset at the end of their scope (Q#) or the beginning (OpenQASM3). It may seem that the choice where the reset takes place is an arbitrary peculiarity of each language. However, it is essential to remember that in Q#, the same qubit can be reused (twice or more) throughout the program with a different name. Therefore, if the qubits were reset in Q# just after their allocation, the reset could obliviously affect the state in the other qubits. It stems from the entanglement concept, one of the profound subtleties of quantum computing. Therefore, it is mandatory to reset Q# qubits before their automatic deallocation at the end of their scope.
A distinctive feature of OpenQASM 3.0 is the access to the physical qubits with $i
, where $i
is the i + 1
's physical qubit (from 0
to n - 1
, where n
is the number of physical qubits).
Summary
In this post, we focused on the similarities between the Q# and OpenQASM programming languages, and we left aside lower-level hardware control and some more advanced concepts. It is also vital to remember that most of the ideas here entered OpenQASM only in version 3.0, which is still being adopted, but the future looks bright. We hope that this post will assist you in your quantum development journey.
References:
- OpenQASM 2: https://arxiv.org/abs/1707.03429
- OpenQASM 3: https://arxiv.org/abs/2104.14722
- Q#: https://arxiv.org/abs/1803.00652
- OpenQASM 3 specification: https://qiskit.github.io/openqasm/
- Q# API reference: https://docs.microsoft.com/en-us/qsharp/api/qsharp/
This post was written as a part of Q# Advent Calendar 2021.