Ordering of classical and quantum operations¶
Like a quantum_kernel
function, a quantum kernel expression is not just the quantum logic as
encapsulated in QBBs. It also includes the interface between the QBBs and and the classical processes that
surround them. Examples of classical processes include generating dynamic angles to be passed to gates
and retrieving and analyzing measurement results. As such, the FLEQ compilation stage of IQC
must aggregate and rearrange such classical operators as encapsulated in the constituent QExpr
-returning
functions. FLEQ compilation recognizes three sections for each such function: the pre-quantum section, the
quantum section and the post-quantum section. Note, the post-quantum section is trivial except for cases
where the function is converted from a quantum_kernel
function or the returned value is dependent
on a call to this_as_expr
. In both cases, the post-quantum section contains all classical logic that
follows the last imperative gate call.
In the absence of any bind
calls, the pre-quantum sections are aggregated so as to be executed on the
classical processor before the QBB is issued to the quantum processor in a topologically sorted order;
see Overview of FLEQ compilation. Likewise, the post-sections are aggregated so as to be executed on
the classical processor in the analogous topologically sorted order. In the case of unresolved branching,
classical logic associated with a given branch will only be executed in that branch. For example,
qbit q;
PROTECT QExpr true_branch() {
double ang = compute_angle_true();
return _RX(q, ang);
}
PROTECT QExpr false_branch() {
double ang = compute_angle_false();
return _RX(q, ang);
}
QExpr foo(bool b) {
return cIf(b, true_branch(), false_branch());
}
In this example, if foo
is evaluated such that its argument can not be resolved at compile-time,
the function compute_angle_true
is only executed at runtime on the classical processor if
the argument is evaluated to be true
at runtime, and likewise for c_foo_false
if the
argument evaluates to false
at runtime. Note the use of the PROTECT
attribute for true_branch
and false_branch
. This is added because the base LLVM processing in IQC may prematurely inline these
function into foo
, preventing the FLEQ compilation stage from discriminating which classical
logic belongs in which branch. This kind of attention to ordering and control over
execution on the classical processor is primarily relevant if such function calls have side-
effects outside of the scope of FLEQ. For example, calls to member functions of a class may
manipulate class member data in which case, special care must be given to insure the execution order
is as desired.
A QExpr
-returning function can introduce locally scoped variables, including locally-scoped
qubits; see Local qubits. However, such functions should always use the PROTECT
attribute as once again, premature inlining can cause improper scoping of these variables and
possibly result in a runtime segmentation fault. This is not currently caught by the FLEQ
compilation stage of IQC; see Known limitations.
When bind
functionality is introduced, the pre-quantum and post-quantum sections are now shifted relative
to the bind
-generated QBB as discussed in section Quantum basic blocks and barriers. For example,
PROTECT QExpr measureBit(qbit &anc, cbit &cont) {
return _MeasZ(anc, cont);
}
PROTECT QExpr rotateIfTrue(qbit &q, cbit &cont) {
double ang = 3.14 / 4. * (double)cont;
return _RX(q, ang);
}
int main() {
qbit anc;
qbit q;
cbit cont = false;
eval_hold(_H(anc) + measureBit(anc, cont)
<< rotateIfTrue(q, cont));
}
In this case, the instructions which calculate the rotation angle in rotateIfTrue
based on the passed cbit
value is inserted in the pre-quantum section for the QBB associated with
the right-side of the bind
, and thus is executed in between the two QBBs, as
expected. Note again the use of the PROTECT
attribution to prevent premature inlining.
Also note as specified in section Basic concepts that the cbit
is passed to rotateIfTrue
by reference.
As discussed earlier, non-trivial post-quantum sections are only generated through the use of convert
or
this_as_expr
functionality. Moreover, it is a known issue that in certain cases where unresolved branching
and bind
functionality are used together, the post-quantum sections may not be inserted into the correct
branches or the correct order; see Known limitations. To enforce an ordering on classical logic, one
can always encapsulate the logic in an “empty” QExpr
-returning function, i.e. one that returns only
qexpr::identity
or this_as_expr
without any imperative gate calls. Ordering is then enforced
via bind
functionality with these empty QExpr
-returning functions.
As an example, suppose one writes a quantum function QExpr getData(QList qdata, cbit cdata[])
to extract
quantum measurement data and classical data analysis function bool analyzeData(cbit cdata[])
. One
can then wrap the analysis function as an empty QExpr
and bind
the two together to form a complete calculation
as a QExpr
:
QExpr getData(QList qdata, cbit cdata[]);
bool analyzeData(cbit cdata[]);
PROTECT QExpr analyzeData(cbit &result, cbit cdata[]) {
result = analyzeData(cdata);
return this_as_expr;
}
PROTECT QExpr calculateData(cbit &result, QList qdata) {
//use IQC_alloca for the cbit array so that
// we can use the size of the QList to determine size
cbit *cdata = IQC_alloca<cbit>("", data.size());
return getData(qdata, cdata) << analyzeData(result, cdata);
}