Skip to content

Within-apply

The within-apply statement performs the common quantum pattern \(U^{\dagger} V U\). It operates on two nested statement blocks, the within block and the apply blocks, and evaluates the sequence - within, apply, and invert(within). Under conditions described below, quantum objects that are allocated and prepared by the within block are subsequently uncomputed and released.

Syntax

within { within-statements } apply { apply-statements }

def within_apply(within: Callable, apply: Callable) -> None:
    pass

Semantics

  • Unlike the case with other statements, the nested blocks of within-apply may initialize outer context variables.
  • Variables that are initialized inside the within block of a within-apply statement, are returned to their uninitialized state after the statement completes.
  • All quantum objects allocated directly or indirectly under the within block are uncomputed, and their qubits are reclaimed for subsequent use after the statement completes.
  • In addition to the general restriction of local variables to permutable use contexts, variables initialized inside the within block and their dependents can only be used in const contexts inside the apply block. See more on uncomputation rules under Uncomputation.
  • The application of the within block and its inverse are not subjected to redundant control logic in the case where the within-apply statement as a whole is subject to control.
  • The application of the within block and its inverse are guaranteed to be strictly equivalent, including in the case where within block involves non-deterministic implementation decisions by the synthesis engine.

Examples

Example 1

The following example demonstrates how auxiliary qubits get used, uncomputed, and reused at different steps of a computation, when scoped inside a within-apply statement. Actual reuse is a decision the synthesis engine takes to satisfy width constraints.

qfunc main(output res: qbit) {
  allocate(res);
  ctrl: qbit[];
  within {
    ctrl = 3;
  } apply {
    CCX(ctrl, res);
  }
  within {
    ctrl = 2;
  } apply {
    CCX(ctrl, res);
  }
}
from classiq import *


@qfunc
def main(res: Output[QBit]):
    allocate(res)
    ctrl = QArray()
    within_apply(lambda: assign(3, ctrl), lambda: CCX(ctrl, res))
    within_apply(lambda: assign(2, ctrl), lambda: CCX(ctrl, res))

Note how variable ctrl is initialized in the within block, prior to being used for the CCX in the apply block. Outside the within-apply statement the variable is reset to its uninitialized state, and used again in the same way.

Visualizing the resulting quantum program, you can see how the same two auxiliary qubits are reused across the two steps of the circuit, because of width optimization.

within_apply.png

Example 2

The code snippet below demonstrates the implementation of the phase kickback pattern for an arbitrary quantum predicate. Function my_phase_oracle takes as parameter a function that flips a qubit on the states of interest. Variable aux is prepared in the \(|1\rangle\) state and passed as the target to function my_cond_phase_flip. The cumulative effect of my_cond_phase_flip is a conditional \(\pi\) phase on target, controlled on the states of interest. Since aux is subsequently uncomputed and released, a relative \(\pi\) phase remains between the states of interest and all others in the superposition.

qfunc my_cond_phase_flip(predicate: qfunc (permutable qbit), const target: qbit)
        unchecked(target) {
  H(target);
  predicate(target);
  H(target);
}

qfunc my_phase_oracle(predicate: qfunc (permutable qbit)) {
  aux: qbit;
  within {
    allocate(aux);
    X(aux);
  } apply {
    my_cond_phase_flip(predicate, aux);
  }
}
from classiq import *


@qfunc(unchecked=["target"])
def my_cond_phase_flip(predicate: QCallable[Permutable[QBit]], target: Const[QBit]):
    H(target)
    predicate(target)
    H(target)


@qfunc
def my_phase_oracle(predicate: QCallable[Permutable[QBit]]):
    aux = QBit()
    within_apply(
        lambda: (allocate(aux), X(aux)), lambda: my_cond_phase_flip(predicate, aux)
    )

Note that my_cond_phase_flip declares parameter target as const because, taken as a whole, the function only applies phase changes to it. But because the implementation uses non-cost operations, target is specified as unchecked. For more on enforcement of parameter restrictions see Uncomputation.

Example 3

The code snippet below demonstrates the use of within-apply to define the Grover operator, avoiding redundant control logic when called in higher-level contexts (for example, when used as the unitary operand in a phase-estimation flow):

qfunc my_grover_operator(oracle: qfunc (qbit[]), space_transform: qfunc (qbit[]), target: qbit[]) {
  oracle(target);
  within {
    invert {
      space_transform(target);
    }
  } apply {
    reflect_about_zero(target);
  }
}
from classiq import *


@qfunc
def my_grover_operator(
    oracle: QCallable[QArray[QBit]],
    space_transform: QCallable[QArray[QBit]],
    target: QArray[QBit],
):
    oracle(target)
    within_apply(
        lambda: invert(lambda: space_transform(target)),
        lambda: reflect_about_zero(target),
    )