Skip to content

Uncomputation

Uncomputation is the process of reversing the effects of quantum operations, restoring the state of qubits to their initial \(|0\rangle\) state. Failing to properly uncompute intermediate results can lead to incorrect final measurement results and wasted resources. In Qmod, intermediate results are typically stored in local variables, which are scoped inside a function and are inaccessible outside of it. Hence, local variables must be used in a way that enables subsequent uncomputation. Specifically, their interactions with other objects must be restrictive to be subsequently disentangled from them.

To enforce the restrictions, quantum operations are classified into arbitrary functions and permutation-only functions. In addition, each of an operation's parameters is classified as either const or non-const.

  • permutation: A quantum operation is a permutation if it maps computational-basis states to computational-basis states (with possible phase shifts). Such an operation neither introduces nor destroys quantum superpositions, and its computation can be described classically.
  • const: A parameter of a quantum operation is constant if it is immutable up to a phase. That is, the magnitudes of its computational-basis state components remain unchanged, while their phases may shift.

permutation functions are explicity declared using the keyword qperm. const parameters are explicity declared using the keyword const. See more under Function Declarations.

Examples
  • Z is a permutation and its parameter is const as it merely flips the phase of the \(|1\rangle\) state.
  • X is a permutation but its parameter is not const as it flips between \(|0\rangle\) and \(|1\rangle\).
  • H is not a permutation and its parameter is not const, as it introduces superposition.
  • SWAP is a permutation but its two parameters are not const, as it swaps between \(|01\rangle\) and \(|10\rangle\).

Quantum operations classification

Each quantum operation is classified as either an arbitrary mutation of the quantum state or strictly a permutation of computational-basis states, according to the following rules:

  • A function call is a permutation if the callee is declared as qperm.
  • A numeric assignment (both out-of-place and in-place) is a permutation.
  • Amplitude-encoding assignment is non-permutation.
  • Phase statement is a permutation.
  • A compound statement is a permutation if its body contains only permutation operations. For example, a control statement is a permutation if all statements inside its then and else clauses are permutations.
  • Bind statement is considered a permutation, as are allocate and free.

In addition, each use of a quantum variable is classified as either const or non-const:

  • An argument in a function call is classified according to the modifier used in the matching parameter declaration.
  • Use as right-value quantum expression is const. Right-value expressions occur in the following contexts:
    • On the right-hand side of a numeric assignment or amplitude-encoding assignment.
    • As the condition in a control statement.
    • As the argument of a phase statement.
  • Use as left-value expression in numeric assignments (both out-of-place and in-place) and amplitude-encoding assignments is non-const.
  • Arguments to a bind statement are treated specially: they are not immediately classified, but are instead bound together and assigned a joint classification of either const or non-const.

Enforcement of function classification

Generally, a qperm function is restricted to use only permutation operations, and a const parameter is restricted to const use contexts. Violation of these restrictions results in a compilation error.

However, flexibility is often required in the implementation of lower-level building blocks. The cumulative effect of the function on the quantum parameters in these cases satisfies its declared restrictions, but individual operations violate it. Well known examples are the implementation of Toffoli gate, and arithmetic addition in the Fourier bases. Both are permutation-only operations taken as a whole, but internally use Hadamard gates and rotations.

It is not scalable to validate the correct cumulative effect of a description automatically in the general case. However, you can suppress the fine-grained enforcement of function classification with the disable_perm_check and disable_const_checks specifiers using the following syntax:

Before a function definition you may specify the following decorators:

@disable_perm_check

@disable_const_checks [ ( parameters ) ]

Where parameters is an optional list of comma-separated parameter names (if the list is omitted, the checks are disabled for all const parameters).

The decorators @qfunc and @qperm have the optional parameters disable_perm_check and disable_const_checks declared thus -

disable_perm_check: bool = False

disable_const_checks: Union[list[str], bool] = False

disable_const_checks may contain a list of parameter names, or a boolean to disable the checks for all const parameters.

When disable_perm_check is used, the compiler does not enforce the usage of non-permutation operations. When disable_const_checks is used, the compiler does not enforce use context restrictions on the listed parameters (or all parameters if none specified).

Examples

Example 1 - Correct use of permutation and const parameters

In the example below, function foo is declared qperm, and its first parameter param1 is declared const. The definition of foo is consistent with these declarations. Note that the restriction on param1 is carried over to its use in the lambda expression passed to apply_to_all.

qperm foo(const param1: qnum, output param2: qnum) {
  param2 = param1 + 1;  // OK - assignment is a permutation and RHS is const
  apply_to_all(lambda(qb) {
    Z(qb);
  }, param1);  // OK - the parameter of 'Z' is const
}
from classiq import *


@qperm
def foo(param1: Const[QNum], param2: Output[QNum]):
    param2 |= param1 + 1  # OK - assignment is a permutation and RHS is const
    apply_to_all(lambda qb: Z(qb), param1)  # OK - the parameter of 'Z' is const

Example 2 - Incorrect use of permutation and const parameters

The example below demonstrates violations of the restrictions on the use of parameters and operations. As in the previous example, the function foo is declared qperm and its first parameter param1 is declared const. However, the use of param1 violates the restriction, and the function uses a non-permutation operation as well.

qperm foo(const param1: qnum, output param2: qnum) {
  param1 += 2;  // Error - LHS is non-const
  hadamard_transform(param2);  // Error - 'hadamard_transform' is non-permutation
from classiq import *


@qperm
def foo(param1: Const[QNum], param2: Output[QNum]):
    param1 += 2  # Error - LHS is non-const
    hadamard_transform(param2)  # Error - 'hadamard_transform' is non-permutation

Example 3 - Disabling permutation check

In the example below, function my_cx implements the CX operation using a simple equivalence - applying phase flip in the Hadamard basis. The cumulative operation on the quantum state is a permutation, but individual calls to H are not. The disable_perm_check is used to suppress compiler errors in this case.

@disable_perm_check
qperm my_cx(const ctrl: qbit, tgt: qbit) {
  H(tgt);
  CZ(ctrl, tgt);
  H(tgt);
}
from classiq import *


@qperm(disable_perm_check=True)
def my_cx(ctrl: Const[QBit], tgt: QBit):
    H(tgt)
    CZ(ctrl, tgt)
    H(tgt)

Example 4 - Disabling permutation check and const checks

In the example below, function my_z implements the Z operation using a simple equivalence - applying bit flip in the Hadamard basis. The cumulative operation on the quantum state is a permutation, but individual calls to H are not. Also, the cumulative operation on the tgt is const, but individual calls are not. The disable_perm_check and disable_const_checks are used to suppress compiler errors in this case.

@disable_perm_check
@disable_const_checks
qperm my_z(const tgt: qbit) {
  H(tgt);
  X(tgt);
  H(tgt);
}
from classiq import *


@qperm(disable_perm_check=True, disable_const_checks=True)
def my_z(tgt: Const[QBit]):
    H(tgt)
    X(tgt)
    H(tgt)

Semantics of uncomputation

When a variable is initialized inside the within block of a within-apply statement, it is returned to its uninitialized state after the statement completes. The newly allocated quantum object is uncomputed, and its qubits are reclaimed by the compiler for subsequent use. Likewise, a variable declared locally within a function is only accessible inside the function's scope. The quantum object allocated inside the function may be uncomputed at some later point and its qubits reclaimed. Uncomputation is forced when a function with a local variable is called (directly or indirectly) from the within block of a within-apply statement. For more details on within-apply see Within-apply.

Local variables are considered uncomputation candidates if they remain initialized at the end of the function scope in which they were declared. Variables initialized inside a within block of a within-apply statement are also considered uncomputation candidates in the scope of the statement. following rules guarantee that uncomputation candidates can be handled correctly:

  • An uncomputation candidate must not be used in a non-permutation operation.
  • A variable initialized inside a within block of a within-apply statement can only be used in const contexts inside the apply block.
  • A variable becomes a dependency of an uncomputation candidate when used in an operation together with the candidate variable, and the latter is used in a non-const context. From that point, the dependency variable is subject to the same rules as the candidate variable, while the latter is in scope.

Violating these rules will result in a compilation error.

Examples

Example 1 - Correct uncomputation in within-apply

The example below demonstrates the use of a local variable initialized inside a within block of a within-apply statement. Variable aux is initialized as the left-value expression of an assignment statement, which is a permutation operation. In the apply block, it is used as the condition of a control statement, which is a const context. Both uses are valid, and the variable is uncomputed and freed correctly after the within-apply statement.

qfunc main(output qn: qnum, output res: qbit) {
  allocate(2, qn);
  hadamard_transform(qn);
  allocate(1, res);
  aux: qbit;
  within {
    aux = qn > 1;
  } apply {
    control (aux) {
      X(res);
    }
  }
}
from classiq import *


@qfunc
def main(qn: Output[QNum], res: Output[QBit]):
    allocate(2, qn)
    hadamard_transform(qn)
    allocate(res)
    aux = QBit()
    within_apply(
        within=lambda: assign(qn > 1, aux), apply=lambda: control(aux, lambda: X(res))
    )

Example 2 - Illegal use of local variable in within-apply

The code below is a modification of Example 1 above, with a couple of lines added to demonstrate violations of the rules for correct use of a variable initialized inside the within block of a within-apply statement. Here, variable aux is also used as the argument of function H in the within block. This is an arbitrarily non-permutation operation (indeed H introduces superposition between computational-basis states). In addition, aux is used as the argument of function X in the apply block, which is non-const context. Both uses are illegal, and both are reported as errors by the compiler.

qfunc main(output qn: qnum, output res: qbit) {
  allocate(2, qn);
  hadamard_transform(qn);
  allocate(1, res);
  aux: qbit;
  within {
    aux = qn > 1;
    H(aux);
  } apply {
    control (aux) {
      X(res);
    }
    X(aux);
  }
}
from classiq import *


@qfunc
def main(qn: Output[QNum], res: Output[QBit]):
    allocate(2, qn)
    hadamard_transform(qn)
    allocate(res)
    aux = QBit()
    within_apply(
        within=lambda: (
            assign(qn > 1, aux),
            H(aux),
        ),
        apply=lambda: (
            control(aux, lambda: X(res)),
            X(aux),
        ),
    )

Example 3 - Illegal use of dependent variable in within-apply

The following example demonstrates a violation of the rules for correct use of a dependent variable inside a within-apply statement. Here, variable aux is initialized inside a within block, and is subsequently entangled with q1 which is not an uncomputation candidate. From that point, the same restrictions that hold for aux apply to q1. Therefore, using it as an argument to function H is illegal, and is reported as an error. Indeed, if foo would execute as specified, aux would not be uncomputed correctly.

qfunc foo(q1: qbit, q2: qbit) {
  aux: qbit;
  within {
    allocate(1, aux);
    CX(q1, aux);
    H(q1);
  } apply {
    CX(aux, q2);
    Z(q1);
  }
}
from classiq import *


@qfunc
def foo(q1: QBit, q2: QBit):
    aux = QBit()
    within_apply(
        within=lambda: (
            allocate(aux),
            CX(q1, aux),
            H(q1),
        ),
        apply=lambda: (CX(aux, q2), Z(q1)),
    )

Example 4 - Illegal use of local variable in function

The example below demonstrates incorrect use of a local variable inside a function. Function rand_increment initializes a local variable temp in a superposition state, using prepare_state, and is subsequently entangled with the parameter qn. This makes it impossible to uncompute temp. temp is a local variable and therefore an uncomputation candidate. Passing it as argument to prepare_state is flagged as an error, because it is not a permutation. Note that if rand_increment would output temp instead of declaring it as a local variable, the function would be legal.

qfunc rand_increment(qn: qnum) {
  temp: qnum;
  prepare_state([0, 0.8, 0.2, 0], 0, temp);
  qn += temp;
}
from classiq import *


@qfunc
def rand_increment(qn: QNum):
    temp = QNum()
    prepare_state([0, 0.8, 0.2, 0], 0, temp)
    qn += temp