Skip to content

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)