Generative Functions
Generative quantum functions are Qmod functions defined in Python, in which
classical Qmod variables and expressions are interpreted as Python objects.
Hence, these expressions can be used in Python's control flow statements
(for
, if
, etc.) and third-party libraries (such as math
).
Generative functions offer better integration with Python and Python libraries
and enhance the debugging experience.
Example:
from classiq import PHASE, QArray, QBit, qfunc
from math import asin
# declaring a generative function
@qfunc(generative=True)
def foo(qa: QArray[QBit]) -> None:
# using a classical value qa.len as a Python object (int)
for i in range(qa.len):
# sending classical values to third-party functions (asin)
PHASE(asin(i / qa.len), qa[i])
Warning
This feature is under development.
Turn a quantum function into a generative function by decorating it with
@qfunc(generative=True)
.
In generative functions, all classical values and expressions (except execution parameters)
are Python objects.
Consider the following model:
from classiq import QArray, allocate, qfunc
@qfunc
def main() -> None:
qbv = QArray("qbv")
allocate(4, qbv)
print(qbv.len) # Qmod field-access expression
print(type(qbv.len)) # <class 'classiq.qmod.cparam.CParamScalar'>
The classical expression qbv.len
is a symbolic variable whose value is
unavailable during model construction.
In contrast, consider the generative variant of the same model:
from classiq import QArray, allocate, qfunc
@qfunc(generative=True)
def main() -> None:
qbv = QArray("qbv")
allocate(4, qbv)
print(qbv.len) # 4
print(type(qbv.len)) # <class 'int'>
Here, the classical expression qbv.len
is evaluated immediately and yields an int
object.
Since classical values are Python objects in generative functions, they can be integrated into the Python control flow and third-party libraries. For example, consider the following model:
from classiq import allocate, qfunc, CX, QArray, RX
import math
@qfunc(generative=True)
def main() -> None:
qbv = QArray("qbv")
allocate(4, qbv)
for i in range(qbv.len):
RX(1 / math.gcd(i, 3), qbv[i])
for j in range(qbv.len):
if i != j:
CX(qbv[i], qbv[j])
Because qbv.len
is an int
object, it can be used in Python for
and if
statements and be sent as an argument to the math.gcd
function.
Compare the above with the non-generative model below:
from classiq import allocate, qfunc, repeat, if_, CX, QArray, RX
@qfunc
def main() -> None:
qbv = QArray("qbv")
allocate(4, qbv)
repeat(
qbv.len,
lambda i: [
# RX(1 / gcd(i, 3), qbv[i]), # no gcd in the classiq library
repeat(
qbv.len,
lambda j: if_(
i != j,
lambda: CX(qbv[i], qbv[j]),
),
)
],
)
In the non-generative variant we are forced to use repeat
and if_
statements
to describe the classical control flow, and we cannot call gcd
because
there's no such function in the
Classiq SDK.
A non-generative function runs only once because it has a single definition
regardless of the arguments with which it is called.
On the other hand, a generative function generated a different quantum function
for each set of input arguments (hence its name).
This fact makes it possible to debug generative functions and observe the values
of classical expressions during model construction.
In the following model, for example, we use a generative function to debug the
arithmetic properties of a QNum
variable.
from classiq import Output, QNum, allocate, qfunc
@qfunc(generative=True)
def foo(x: QNum, y: QNum, z: Output[QNum]) -> None:
z |= x * y
print(z.size) # 8
print(z.is_signed) # False
print(z.fraction_digits) # 4
@qfunc
def main() -> None:
x = QNum("x", size=4, is_signed=False, fraction_digits=2)
allocate(x.size, x)
y = QNum("y", size=4, is_signed=False, fraction_digits=2)
allocate(y.size, y)
z = QNum("z")
foo(x, y, z)