Skip to content

Quantum Variables

A model operates on quantum objects, by modifying their states using different kinds of operations. Quantum objects represent values that are stored on one or more qubits. The simplest quantum object is a single qubit, representing the values 0 or 1 when measured. Other types of quantum objects are stored on multiple qubits and represent numeric values or arrays of qubits.

Quantum objects are managed in Qmod using quantum variables. Variables are introduced into the scope of a quantum function through the declaration of arguments or the declaration of local variables.

A quantum variable establishes its reference to some object by explicitly initializing it. This is often done by passing it as the output argument of a function, such as allocate(). Once initialized, the state of the object it references can be modified, but the variable's reference itself is immutable.

qfunc main(output q1: qbit) {
  q2: qbit;
  allocate(q1);
  allocate(q2);
  CX(q1, q2);
}

A quantum variable is declared as a function argument using a Python class as a type hint. The same Python class is instantiated to declare a local variable, in which case the name of the variable is optionally specified as a constructor argument and otherwise inferred automatically.

from classiq import Output, QBit, allocate, qfunc, CX


@qfunc
def main(q1: Output[QBit]):
    q2 = QBit()  # The variable name can be set explicitly: QBit("q")
    allocate(q1)
    allocate(q2)
    CX(q1, q2)

Managing Quantum Variables

Here are the rules for managing quantum variables:

  • Local variables and output-only arguments (arguments declared with the output modifier) are uninitialized upon declaration.
  • Quantum arguments declared without a modifier or with the input modifier are guaranteed to be initialized.
  • A variable is initialized in one of the following ways:
    • It is passed as the output-only argument of a function
    • It is used as the left-value of an assignment
    • It occurs on the right side of a -> (bind) statement
  • Once initialized, a variable can be used as an argument in any number of quantum function calls, as long as it is not an output only or input-only argument (an argument declared with the output or input modifier).
  • An initialized variable returns to its uninitialized state in one of the following ways:
    • It is passed as the input-only argument of a function
    • It occurs on the left side of a -> (bind) statement

The following diagram illustrates these rules:

flowchart LR
    StartUninit["local declaration
    output declaration"] --- Uninit
    Uninit((Uninitialized)) -- "allocate
    output-arg
    bind-RHS
    assign-LHS" --> Init
    Init((Initialized)) -- "free
    input-arg
    bind-LHS" --> Uninit
    Init -- arg --> Init
    Init --- StartInit["arg declaration
    input declaration"]

In the next example, the local variable a must be initialized prior to applying X() on it, since it is declared as an output-only argument of function main. Similarly, the local variable b is uninitialized upon declaration, and subsequently initialized through the call to prepare_state, to which it is passed as an output-only argument.

qfunc main(output a: qbit) {
  allocate(a);
  X(a);
  b: qbit[];
  prepare_state([0.25, 0.25, 0.25, 0.25], 0.01, b);
}
from classiq import allocate, qfunc, QBit, QArray, prepare_state, Output, X


@qfunc
def main(a: Output[QBit]) -> None:
    allocate(a)
    X(a)
    b = QArray()
    prepare_state(probabilities=[0.25, 0.25, 0.25, 0.25], bound=0.01, out=b)

Allocate

The allocate statement is used to initialize quantum variables, allocating a sequence of qubits to store the new quantum object. The number of qubits allocated, and the numeric type attributes in the case of a numeric variable, are either explicitly specified, or derived from the variable's type.

Syntax

allocate ( [ size-int-expr , [ sign-bool-expr , frac-digits-int-expr , ] ] var )

@overload
def allocate(out: Output[QVar]) -> None:
    pass


@overload
def allocate(num_qubits: Union[int, SymbolicExpr], out: Output[QVar]) -> None:
    pass


@overload
def allocate(
    num_qubits: Union[int, SymbolicExpr],
    is_signed: Union[bool, SymbolicExpr],
    fraction_digits: Union[int, SymbolicExpr],
    out: Output[QVar],
) -> None:
    pass

It is recommended to use the SIGNED and UNSIGNED built-in constants instead of True and False respectively when specifying the sign-bool-expr.

Semantics

  • Prior to an allocate statement var must be uninitialized, and subsequently it becomes initialized.
  • The size-int-expr, if specified, must agree with the declared size of the variable. If the variable declaration does not determine the size, the type of the variable is inferred to accommodate the specified size. See more under Quantum Types.
  • For variables of type qnum, sign-bool-expr and frac-digits-int-expr, if specified, must agree with the quantum type of the variable. Here too, if the variable declaration does not determine these numeric properties, they are inferred per the specified values.

Example

The following example demonstrates three uses of allocate on two local variables and one output parameter of main. Note how the overall size of the quantum object is used in the inference of its type.

qfunc main(output qnarr: qnum[2]) {
  qb: qbit;
  allocate(qb);  // allocates a single qubit

  qn: qnum;  // declares a quantum number with unspecified size
  allocate(3, SIGNED, 0, qn);  // allocates a 3 bit signed integer (ranging in [-4, 3])

  allocate(6, qnarr);  // allocates an array of 3 elements, each with size 2

  hadamard_transform({qb, qn, qnarr});
}
from classiq import *


@qfunc
def main(qnarr: Output[QArray[QNum, 2]]):
    qb = QBit()
    allocate(qb)  # allocates a single qubit

    qn = QNum()  # declares a quantum number with unspecified size
    allocate(3, SIGNED, 0, qn)  # allocates a 3 bit signed integer (ranging in [-4, 3])

    allocate(6, qnarr)  # allocates an array of 3 elements, each with size 2

    hadamard_transform([qb, qn, qnarr])

Free

The free statement is used to declare that a quantum variable is back to its initial \(|0\rangle\) state, and no longer used. Subsequently, the variable becomes uninitialized and its qubits can are reclaimed by the compiler for subsequent use.

Syntax

free ( var )

def free(out: Input[QVar]) -> None:
    pass

Semantics

  • Prior to a free statement var must be initialized, and subsequently it becomes uninitialized.
  • The quantum object referenced by var must be in the \(|0\rangle\) state when it is freed. This property is not enforced by the compiler.
  • Local variables that are explicitly freed are not considered uncomputation candidates, and are not restricted to permutable use contexts. See more under Uncomputation.

Warning

It is the programmer's responsibility to apply free only to quantum variables that are known to be in the \(|0\rangle\) state. Failing to do so may lead to undefined behavior.

Example

Explicitly freeing a variable is typically not needed and is only used for specific purposes. The following example demonstrates the use of free in a phase-kickback pattern. It is used to release an auxiliary qubit that is known to be returned to state \(|0\rangle\), despite having applied the Hadamard gate to it.

qfunc flip_phase(val: int, const state: qnum) {
  aux: qbit;
  allocate(aux);
  within {
    X(aux);
    H(aux);
  } apply {
    control (state == val) {
      X(aux);
    }
  }
  free(aux);  // aux is known to be in the |0> state
}
from classiq import *


@qfunc
def flip_phase(val: CInt, state: Const[QNum]):
    aux = QBit()
    allocate(aux)
    within_apply(
        lambda: (X(aux), H(aux)),
        lambda: control(state == val, lambda: X(aux)),
    )
    free(aux)  # aux is known to be in the |0> state

Note that an alternative approach to implementing a phase-kickback pattern, which does not require the use of free, is to encapsulate the calls to H in a function with an as unchecked const parameter.

Concatenation Operator

The concatenation operator is used to combine a sequence of quantum objects (or their parts) into a quantum array.

{ path-expressions }

path-expressions is a comma-separated sequence of one or more quantum path expressions.

A concatenation is a Python list containing quantum objects.

[ path-expressions ]

path-expressions is a comma-separated sequence of one or more quantum path expressions.

For example, the model below uses the concatenation operator to apply hadamard_transform to a specific set of qubits drawn from two quantum variables:

qfunc main() {
  v1: qbit[4];
  allocate(v1);
  v2: qbit[4];
  allocate(v2);
  v3: qbit;
  allocate(v3);
  hadamard_transform({v1[3], v3, v2[1:3], v1[0]});
}
from classiq import allocate, qfunc, QBit, QArray, hadamard_transform


@qfunc
def main():
    v1 = QArray(length=4)
    allocate(v1)
    v2 = QArray(length=4)
    allocate(v2)
    v3 = QBit()
    allocate(v3)
    hadamard_transform([v1[3], v3, v2[1:3], v1[0]])

This model allocates three quantum objects: quantum arrays v1 and v2 and a qubit v3. The model uses a concatenation operator to create a quantum array and apply hadamard_transform to it. The quantum array comprises the last bit of v1, the entirety of v3, the middle two qubits of v2, and the first qubit of v1.

Warning

Currently, concatenations are only supported in function call arguments and control expressions.