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
orinput
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.