.. _intro: Introduction ############ Motivation ********** Recap of the |Intel| Quantum Software Development Kit language =============================================================== The |Intel| Quantum Software Development Kit (SDK) is an extension of C++ with a full LLVM-based C++ compiler, augmented to compile quantum-classical hybrid programs using quantum hardware, simulator, or other quantum backend as an accelerator. The backend is controlled by imperative gate calls inside a *quantum kernel function*---a C++ function with the ``quantum_kernel`` function specifier. Quantum kernel functions act as containers for quantum gate calls, analogous to a quantum circuit. These functions are compiled into *quantum basic blocks* (QBBs)---sequential lists of quantum instructions---which are dispatched to the backend when the ``quantum_kernel`` function is called. The compiler and runtime also define and manage the interfaces between (both classical and quantum) data and instructions so as to be nearly invisible to the user. .. image:: FLEQ_Documentation_Diagram.jpg :width: 330 :alt: Diagram showing the relationship between .cpp files, the Intel Quantum Compiler, and the Intel Quantum Runtime. :align: center This style of using imperative function calls as containers for quantum gates is in contrast to a common approach for quantum programming using circuit generation frameworks. Quantum circuit generation frameworks use an object-oriented programming (OOP) paradigm, which allow users to construct circuit objects populated by gate objects. During classical runtime, a compiler or transpiler converts these circuit objects into a format readable by a qubit chip or simulator. Circuit generation languages are extremely modular in that they allow programmers to construct and manipulate circuit objects as first class values. For example, many circuit generation languages support transformations that invert an entire unitary circuit by inverting each gate and reversing their order. Implementing such a transformation for ``quantum_kernel`` functions would require modifying the compiler directly, and is thus not very accessible for users. The |Intel| Quantum SDK does not adopt the circuit generation paradigm, because embedding a circuit generation framework inside C++ would compromise the strict compiler guarantees maintained by the SDK needed to interface seamlessly with the quantum runtime and backend. However, the SDK does support a collection of features, known collectively as the Functional Language Extension for Quantum (FLEQ), that provide many of the benefits of circuit generation frameworks while preserving these compiler guarantees. To understand how FLEQ operates, the first step is to unravel the tension between compiler modularity and runtime guarantees by understanding the distinction between *compile-time* and *runtime* in the context of the SDK. Compile-time vs. runtime ======================== A compile-time construct, abstraction, or object is one that is completely resolved by the compiler such that the final binary has no trace of the construct. Two examples of compile-time constructs in C++ are classes (which are elaborated into structs and global functions in the compiler) and templates. In both cases, at some point in the compiler intermediate representation (IR), all trace of the construct has been removed. In principle, equivalent IR could have been generated by more rudimentary means only using the C language. Other terms associated with compile-time constructs are "static", "resolvable" or "a priori", i.e. a variable might be resolvable or known statically or a priori. A runtime construct or object is one in which the final compiled binary does maintain some signature of the construct, or where the construct must be computed or known when the binary is executed. An example of a runtime construct in C++ is runtime polymorphism, where several versions of the same function or method might exist, and the decision of which should be executed may depend on runtime factors. As such, every version of that function must be compiled to binary with the addition of a ``vtable`` to allow for runtime selection. Many C++ standard library containers are also runtime constructs as aspects like size and contents are not known a priori. Other terms associated with runtime constructs are "dynamic" or "unresolvable (by the compiler)." Quantum kernel functions contain examples of both compile-time and runtime constructs. Loops and branches using quantum gates and qubit gate arguments are compile-time constructs as the compiler must fully resolve these inside each ``quantum_kernel`` function. If these were runtime constructs, the runtime would have to take on prohibitive costs such as performing on-the-fly qubit routing. On the other hand, classical parameters to gate arguments such as rotation angles are runtime variables as they can be handled by the quantum runtime library with little to no overhead for the quantum backend. What is the Functional Language Extension for Quantum (FLEQ)? =============================================================================== Many of the limitations of the |Intel| Quantum SDK can be traced back to the compile-time versus runtime constraints imposed by quantum hardware. For example, top-level ``quantum_kernel`` functions can not have qubit arguments, as all qubit references must be resolved at compile time. Moreover, the SDK does not enable meta-transformations on ``quantum_kernel`` functions because they themselves are not objects as in circuit generation languages. However, OOP-style circuit generation is not a solution either. Member methods that manipulate circuit classes in C++ are runtime constructs, and have side effects that can be all but impossible to infer from IR. Circuit objects in C++ thus could not be transformed into QBBs at compile-time, in which case the SDK would be unable to meet compile-time and runtime requirements for issuing instructions to the quantum backend. Seasoned C++ programmers might suggest adding modularity by passing quantum kernel functions via function pointers and lambdas (anonymous functions). This approach is in line with the spirit of the SDK, but must be done in such a way that the compiler can reason about the transformations and meet compile-time constraints. To solve these problems, this document introduces the *Functional Language Extension for Quantum* (FLEQ) as a feature set for the |Intel| Quantum Compiler (IQC). FLEQ allows for flexible, modular development of complex quantum logic while maintaining the compile-time constraints needed to generate QBBs. It is compatible with all other features of the SDK, and enhances the SDK's seamless quantum-classical interfaces and efficient runtime execution. FLEQ adapts a functional programming paradigm that treats quantum kernels as first-class, immutable constructs that can be passed into and out of functions to facilitate robust, expressive, and easy-to-use compile-time reasoning. FLEQ consists of: 1. An immutable type ``QExpr`` or *quantum kernel expression* that acts like a function pointer or lambda for quantum kernels. 2. A set of compile-time methods for constructing quantum kernel expressions, with pure functional APIs that are guaranteed to have no side effects. 3. Features that alleviate many of the pain points to modular quantum code development with the SDK, including but not limited to: i. Built-in meta-transformations such as unitary inversion and both unitary and classical control; ii. Support for custom generic and complex meta-transformations; iii. Adaptable and reusable submodule libraries using compile-time recursion and compile-time and runtime branching; iv. Support for algorithms and problem instances that abstract away gate-level implementation details; and v. Compile-time and runtime debugging features. In version 1.1 of the |Intel| Quantum SDK, FLEQ is in its beta release and is still being actively developed. Bug reports, feature requests and general feedback are much appreciated. See :ref:`fleq_support` for more details. .. _basics: Basic concepts ************** Quantum kernel expressions (``QExpr``) ====================================== The central component of FLEQ is the *quantum kernel expression*, or ``QExpr``. A value of type ``QExpr`` is an immutable compile-time representation of a block of quantum logic, as well as associated classical instructions. A ``QExpr`` value is a quantum program that has not yet been issued to the backend; it can be thought of as an unspecified or opaque function pointer to a ``quantum_kernel`` function. To see the difference between a ``QExpr`` value and a ``quantum_kernel`` function, consider a quantum block that prepares a single qubit in the :math:`\ket{+}` basis. As a quantum kernel function, this block might look like: .. code-block:: C++ qbit q; quantum_kernel void prep_plus_qk() { PrepZ(q); H(q); } The same block of quantum instructions can be represented as a quantum kernel expression by writing a function that returns a ``QExpr`` value: .. code-block:: C++ QExpr prep_plus_qexpr(qbit& q) { return qexpr::_PrepZ(q) + qexpr::_H(q); } Syntax aside, (see :ref:`fleq_features` for details) these two appear very similar, but there is a fundamental difference. The ``quantum_kernel`` function ``prep_plus_qk`` is a fully specified sequence of quantum instructions; as such, calling the function issues those instructions in the form of a QBB. The quantum kernel expression that is *returned* by ``prep_plus_qexpr(q)`` ostensibly represents the same set of quantum instructions, but calling the function alone will *not* issue those instructions. Instead, the ``QExpr`` returned by ``prep_plus_qexpr(q)`` is just a *representation* of those instructions that can be manipulated by the programmer. For example, the quantum kernel expression ``prep_plus_qexpr(q)`` can be appended with a ``Z`` gate to take the plus state :math:`\ket{+}` to the minus state :math:`\ket{-}`: ``prep_plus_qexpr(q) + _Z(q)``. Furthermore, this :math:`\ket{+}`-to-:math:`\ket{-}` transformation can be generalized to any quantum kernel expression, as shown by a function of the form: .. code-block:: C++ QExpr appendZ(QExpr e, qbit& q) { return e + qexpr::_Z(q); } Then, the :math:`\ket{-}` preparation sequence is the ``QExpr`` value returned by ``appendZ(prep_plus_qexpr(q), q)``. Note that all ``QExpr`` values are immutable, so it is not that ``appendZ`` modifies its argument, but rather returns a new unique ``QExpr`` value. .. _intro-evaluation: Evaluating quantum kernel expressions ====================================== Once a ``QExpr`` value has been constructed, the programmer must issue the instructions it represents to the quantum backend. Doing so is referred to as the *evaluation* of a ``QExpr`` and is achieved by one of two evaluation functions: ``void eval_hold(QExpr)`` or ``void eval_release(QExpr)``. For our examples above, the following ``main`` function issues two identical state preparation sequences to the backend: .. code-block:: C++ qbit q; quantum_kernel void prep_plus_qk() { ... } QExpr prep_plus_qexpr(qbit& q) { ... } int main() { ... // Initialization of the Intel Quantum SDK runtime omitted prep_plus_qk(); // Invoke the qubit chip by calling a quantum_kernel function eval_hold(prep_plus_qexpr(q)); // Invoke the qubit chip by evaluating a QExpr value return 1; } The call to ``eval_hold`` tells the compiler that its ``QExpr`` argument should be compiled and sent to the quantum runtime at this point in the code. The difference between ``eval_hold`` and ``eval_release`` is analogous to the ``release_quantum_state`` directive (see the |DGR:Extensions|): ``eval_hold(e)`` guarantees that the quantum state is preserved after executing the QBB produced by compiling ``e``; ``eval_release(e)`` makes no guarantees of the quantum state after execution, and should be used when the user is only interested in measurement results. Compile-time lists (``QList`` and ``DataList``) ====================================================== FLEQ enables modular quantum programming by allowing users to build quantum kernel expressions whose contents depend on compile-time arguments. For example, suppose a programmer wants to prepare each qubit in an array into a :math:`\ket{+}` state. To do this with ``quantum_kernel`` functions, the programmer would need make extensive use of C++ templates, since qubit arrays and loop bounds must both be determined at compile-time. However, using quantum kernel expressions, a user can write a function that takes as input a compile-time qubit list and returns a ``QExpr``. This compile-time qubit list (``QList``) and the corresponding type of compile-time strings (``DataList``) give programmers flexibility while ensuring the compiler has what it needs to interact with the backend. A *qubit list* or ``QList`` is a compile-time wrapper around static ``qbit`` arrays. ``QList`` values can be concatenated to form a new single ``QList``, or sliced into sub-lists to form arbitrary orderings of qubits. Qubits can be individually addressed via the subscript (``[]``) operator, and the size of the array can be resolved at compile-time using member function ``size()``. A ``QList`` can be declared using the ``listable`` macro, as in .. code-block:: C++ const int N = 5; qbit listable(qs, N); A ``DataList`` value is compile-time string; like a ``QList``, it is a wrapper around around statically defined ``char`` arrays, and can be joined and sliced into form arbitrary permutations of the string. Data lists also support a variety of substring search functions; type conversion to ``int``, ``bool`` and ``double`` types; and string comparison. The ``DataList`` feature can even be used to develop domain-specific languages (DSLs) that allow for higher-level representations that abstract away gate-level implement details. See :ref:`datalist` and :ref:`dsl` for more details. Getting started =============== Using FLEQ requires the ``quintrinsics.h`` header that is required for the base use of the SDK. In addition, the three core features of FLEQ are provided by three additional header files: .. code-block:: C++ #include // always required #include // required for QExpr features #include // required for QList features #include // required for DataList features Programs using FLEQ are compiled using the same command-line flags and arguments as the base SDK, with some additional optional flags specific to FLEQ (as described in the relevant sections of :ref:`fleq_features`). All other features and flags are fully compatible with FLEQ, including the use of ``quantum_kernel`` functions, with one exception: ``quantum_kernel`` functions that call basic gates cannot also contain ``QExpr`` evaluation calls. See :ref:`limitations`. We also note here that for FLEQ-generated QBBs, there is no appreciable difference between the use of the ``-O0`` and ``-O1`` optimization flags for reasons described in :ref:`compilation-overview`; see also :ref:`limitations`.