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 compute block and the action blocks, and evaluates the sequence - compute, action, and invert(compute). Under conditions described below, quantum objects that are allocated and prepared by the compute block are subsequently un-computed and released.

Syntax

within { compute-statements } apply { action-statements }

def within_apply(compute: Callable, action: Callable) -> None:
    pass

Semantics

  • Unlike the case with other statements, the nested blocks of within-apply may use outer context variables as output-only arguments.
  • To the extent that the compute block does not introduce superposition into the quantum objects it operates on (i.e. is quantum-free), and the action block does not modify the state of these objects, they are un-computed and returned to their state prior to the within-apply statement.
  • Variables used as output-only arguments in the compute block are reset to their uninitialized state after the within-apply statement, and the qubits utilized by them return to the pool of qubits available for subsequent auxiliary allocations.
  • The application of the compute 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 compute block and its inverse are guaranteed to be strictly equivalent, including in the case where compute involves non-deterministic implementation decisions by the synthesis engine.

Examples

Example 1

The following example demonstrates how auxiliary qubits get used, un-computed, 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<1>(res);
  ctrl: qbit[];
  within {
    prepare_int<3>(ctrl);
  } apply {
    CCX(ctrl, res);
  }
  within {
    prepare_int<2>(ctrl);
  } apply {
    CCX(ctrl, res);
  }
}
from classiq import (
    Output,
    qfunc,
    within_apply,
    allocate,
    CCX,
    QBit,
    QArray,
    prepare_int,
    set_constraints,
    Constraints,
    create_model,
    synthesize,
)


@qfunc
def main(res: Output[QBit]) -> None:
    allocate(1, res)
    ctrl = QArray("ctrl")
    within_apply(lambda: prepare_int(3, ctrl), lambda: CCX(ctrl, res))
    within_apply(lambda: prepare_int(2, ctrl), lambda: CCX(ctrl, res))


qmod = create_model(main)
qmod = set_constraints(qmod, Constraints(optimization_parameter="width"))
quantum_program = synthesize(qmod)

Note how variable ctrl is used as an output-only argument and initialized in the compute block, prior to being used for the CCX in the action 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 preparation and release of an auxiliary qubit to achieve phase kickback.

qfunc prep_minus(output out: qbit) {
  allocate<1>(out);
  X(out);
  H(out);
}

qfunc my_phase_oracle<predicate: qfunc (vars: qbit[], res: qbit)>(vars: qbit[]) {
  aux: qbit;
  within {
    prep_minus(aux);
  } apply {
    predicate(vars, aux);
  }
}
from classiq import allocate, Output, QBit, qfunc, QCallable, QArray, within_apply, X, H


@qfunc
def prep_minus(out: Output[QBit]) -> None:
    allocate(1, out)
    X(out)
    H(out)


@qfunc
def my_phase_oracle(
    predicate: QCallable[QArray[QBit], QBit], vars: QArray[QBit]
) -> None:
    aux = QBit("aux")
    within_apply(lambda: prep_minus(aux), lambda: predicate(vars, aux))

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 (target: qbit[]), space_transform: qfunc (target: qbit[])>(target: qbit[]) {
  oracle(target);
  within {
    invert {
      space_transform(target);
    }
  } apply {
    reflect_about_zero(target);
  }
}
from classiq import (
    QBit,
    qfunc,
    QCallable,
    QArray,
    within_apply,
    invert,
    reflect_about_zero,
)


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