Operators¶
A function in Qmod can take other functions as arguments and call these arguments in its body. Functions operating on other functions are often referred to as higher-order functions, or operators. This mechanism is used to capture reusable quantum algorithm patterns, such as QPE and Grover operator.
Function types¶
Arguments of function types are declared like arguments of classical types. The type determines the expected function signature. Scalar function types correspond to a single function value. Array function types correspond to an indexable collection of function values.
Syntax¶
Function type syntax is similar to the syntax of function declaration, omitting the function name and the body -
qfunc [ < classical-args > ] ( quantum-args )
If [] follows the qfunc keyword, the type is a function array -
qfunc [ ] [ < classical-args > ] ( quantum-args )
The QCallable
type-hint specifies a scalar function type. QCallable
itself is a generic
class, taking as parameters a list of type-hints that declare the arguments of
the function.
The QCallable
type-hint specifies a function array type.
Semantics¶
- The actual function(s) passed to an operator must agree in signature with the declared function type.
- An argument of a scalar function type can be called inside the function's body just like a regular function, passing classical-args and quantum-args as per the function type.
- An element of a function array type can be called with a subscript operator applied to the argument variable followed by the actual argument list.
- Arguments of function types are bound to the actual functions passed in the operator call such that the respective function gets invoked in the call site.
Example¶
In the following example, the function my_operator
declares the argument my_operand
of a function type with one classical argument and one quantum argument. The argument gets
called twice in its body, passing different argument values. In function main
, my_operator
is called twice, passing a different function as its operand.
qfunc my_operator<my_operand: qfunc <theta: real>(target: qbit)>(q: qbit) {
my_operand<pi / 2>(q);
my_operand<pi / 4>(q);
}
qfunc main() {
q: qbit;
allocate<1>(q);
my_operator<RX>(q);
my_operator<RY>(q);
}
from classiq import CReal, QBit, QCallable, RX, qfunc, allocate
from classiq.qmod.symbolic import pi
@qfunc
def my_operator(my_operand: QCallable[CReal, QBit], q: QBit) -> None:
my_operand(pi / 2, q)
my_operand(pi / 4, q)
@qfunc
def main() -> None:
q = QBit("q")
allocate(1, q)
my_operator(lambda theta, target: RX(theta, target), q)
my_operator(lambda theta, target: RY(theta, target), q)
Notes:
- Passing named functions in Python is currently not supported. This example
uses Python lambda expressions - see more in the next section.
- Function arguments in Python do not support specifying argument names. When
translating Qmod Python description to native syntax, names arg0
,
arg1
, etc. are associated with the arguments automatically.
Synthesizing this model creates the quantum program shown below. You can see four rotations in the circuit with their respective angles.
Lambda functions¶
You can pass a function to an operators in one of two forms - a named function, and a lambda functions. Lambda functions are anonymous functions defined in-line in the operator call site. Note that in Python only the lambda function form is supported.
Syntax¶
Lambda function syntax is somewhat similar to a function definition. The
keyword qfunc
is replaced with lambda
, the name of the function is omitted,
and argument lists only specify only names, not types.
lambda [ < classical-arg-names > ] ( quantum-arg-names ) { statements }
A Python Callable
object is used as a Qmod lambda function. This can take one of two forms -
a Python lambda expression, or a named Python function (not decorated with @qfunc
).
When a named function is used with type-hints on its arguments, the names of the
operands will be reflected in the Qmod description.
Example 1¶
Consider the following snippet, where my_operator
is called twice from function main
,
once with a regular function and a second time with a lambda function. These two calls
are equivalent.
qfunc my_operator<my_operand: qfunc <angle: real>(target: qbit)>(q: qbit) {
H(q);
my_operand<pi / 2>(q);
}
qfunc my_operand<angle: real>(target: qbit) {
RX<angle>(target);
}
qfunc main() {
q: qbit;
allocate<1>(q);
my_operator<my_operand>(q);
my_operator<lambda <angle>(target) {
RX<angle>(target);
}>(q);
}
from classiq import CReal, H, QBit, QCallable, RX, allocate, qfunc
from classiq.qmod.symbolic import pi
@qfunc
def my_operator(my_operand: QCallable[CReal, QBit], q: QBit) -> None:
H(q)
my_operand(pi / 2, q)
def my_operand(angle: CReal, target: QBit) -> None:
RX(angle, target)
@qfunc
def main() -> None:
q = QBit("q")
allocate(1, q)
my_operator(my_operand, q)
my_operator(lambda angle, target: RX(angle, target), q)
Note that in the first call, the argument is a regular Python function, not decorated
with @qfunc
.
Example 2¶
An operator may pass expressions involving its own arguments to its operand. The following example demonstrates this.
qfunc foo_operator<n: int, my_operand: qfunc <angle: real>(qb: qbit)>(qba: qbit[2]) {
H(qba[0]);
my_operand<pi / n>(qba[1]);
}
qfunc main() {
qba: qbit[];
allocate<2>(qba);
foo_operator<4, lambda<angle>(qb) {
RX<angle>(qb);
}>(qba);
}
from classiq import CInt, CReal, H, QArray, QBit, QCallable, RX, allocate, qfunc
from classiq.qmod.symbolic import pi
@qfunc
def foo_operator(
n: CInt,
my_operand: QCallable[CReal, QBit],
qba: QArray[QBit, 2],
) -> None:
H(qba[0])
my_operand(pi / n, qba[1])
@qfunc
def main() -> None:
qba = QArray("qba")
allocate(2, qba)
foo_operator(n=4, my_operand=lambda theta, target: RX(theta, target), qba=qba)
Synthesizing this model creates the quantum program shown below. You can see that the call to
foo_operator
applies an X rotation on qubit 1, based on the value of n
passed to it.
Capturing context variables and parameters¶
A lambda function that is passed as an argument to an operator may reference classical or quantum variables in its own lexical scope. These values, or references, are captured and made available when the callable gets invoked by the operator, even though the operator itself is oblivious to them.
An operator must not implicitly change the initialized status of quantum variables captured inside lambda functions that are passed to it. Only initialized variables may be captured, and they remain initialized after the operator call. Hence, quantum variables cannot be captured as output-only or input-only arguments inside a lambda function. Note that an operator may actually invoke the operand once, multiple times, or not at all.
Example¶
The following example is similar to Example 2 from the previous section. However, in this case, the quantum variable used inside the lambda function is captured directly from the scope rather than being passed to it indirectly through the operator. The resulting quantum program in this case is identical to that of the previous version in Example 2 above.
qfunc foo_operator<n: int, my_operand: qfunc <angle: real>()>(qb: qbit) {
H(qb);
my_operand<pi / n>();
}
qfunc main() {
qb1: qbit;
qb2: qbit;
allocate<1>(qb1);
allocate<1>(qb2);
foo_operator<4, lambda<t>() {
RX<t>(qb1);
}>(qb2);
}
from classiq import CInt, CReal, H, QBit, QCallable, RX, allocate, qfunc
from classiq.qmod.symbolic import pi
@qfunc
def foo_operator(
n: CInt,
my_operand: QCallable[CReal],
qb: QBit,
) -> None:
H(target=qb)
my_operand(pi / n)
@qfunc
def main() -> None:
qb1 = QBit("qb1")
qb2 = QBit("qb2")
allocate(1, qb1)
allocate(1, qb2)
foo_operator(n=4, my_operand=lambda t: RX(theta=t, target=qb1), qb=qb2)