Skip to content

Control

Control is the quantum analog of the classical "if" condition. The classical "if-then-else" scheme may not be reproduced on quantum computers due to the existence of superpositions. Therefore, rather than splitting into cases, the controlled version of an operation is another operation. The controlled operation is identical to the original operation on the subspace on which the condition holds, and to the identity in the rest of the Hilbert space. Mathematically speaking, if \(U|x \rangle = |f_U \left( x \right) \rangle\), the controlled version with a single qubit, \({ctrl}_{1}\left(U\right)\) is

\[ {ctrl}_{1}\left(U\right) \left[ \left(\alpha |1\rangle + \beta |0 \rangle\right) \otimes |x\rangle \right] = \alpha |1, f_U \left(x\right) \rangle + \beta |0, x \rangle \]

Here \(\alpha\), \(\beta\) are complex amplitudes with \(|\alpha|^2 + |\beta|^2\). One may also use different states for control, corresponding to other conditions. \({ctrl}_{1}\) is the quantum analog of if q == 1, with q the control qubit. Similarly, \({ctrl}_{0}\) is the analog to if q == 0 and appears in the equation below. Control using several qubits, usually referred to as multi-control, is also possible. \({ctrl}_{110}\) for example, is control using three qubits \(q_1 , q_2, q_3\) analogous to the classical condition if (q1 == 1) and (q2 == 1) and (q3 == 0).

\[ {ctrl}_{0}\left(U\right) \left[ \left(\alpha |1\rangle + \beta |0 \rangle\right) \otimes |x\rangle \right] = \alpha |1, x \rangle + \beta |0, f_U \left(x\right) \rangle \]

Usage

Control is implemented for all functions on the Classiq platform, built-in or user-defined. The only exception is "classical" functions, in which no quantum operations occur, and only classical data is changed. To control a function, one may send ControlState objects when calling it. Such objects contain the control states, the number of control qubits, and the state's name. The former two may be deduced from one another. If only the state is provided, the number of qubits is set to its length. If only the number of qubits is provided, the state is set to all ones with the appropriate length.

To allow precise functionality and access to qubits describing different conditions, sending multiple control states objects is permitted. Note that different states must be distinguishable by name. Control states are wired like any other register, with the state name acting as both input and output names. The examples below present control with the "101" state on three qubits, separated into two states.

{
"logic_flow":
    [
        {
            "function": "Adder",
            "function_params": {
                "left_arg": { "size": 3, "name": "left_arg_name" },
                "right_arg": { "size": 3, "name": "right_arg_name" },
                "output_name": "sum",
                "inplace_arg": "right"
            },
            "control_states": [
                {"ctrl_state": "10", "name": "first_control"},
                {"num_ctrl_qubits": 1, "name": "second_control"}
            ],
            "outputs": {
                "left_arg_name": "left_arg_wire",
                "sum": "sum_wire_name",
                "first_control": "first_control_wire",
                "second_control": "second_control_wire"
            }
        },
        {
            "function": "Multiplier",
            "function_params": {
                "left_arg": { "size": 3, "name": "left_arg_name" },
                "right_arg": { "size": 4, "name": "sum" },
                "output_name": "product"
            },
            "control_states": [
                {"ctrl_state": "10", "name": "first_control"},
                {"num_ctrl_qubits": 1, "name": "second_control"}
            ],
            "inputs": {
                "left_arg_name": "left_arg_wire",
                "sum": "sum_wire_name",
                "first_control": "first_control_wire",
                "second_control": "second_control_wire"
            }
        }
    ]
}
from classiq import ModelDesigner, QReg
from classiq.builtin_functions import Adder, Multiplier
from classiq.interface.generator.arith.arithmetic import RegisterUserInput
from classiq.interface.generator.control_state import ControlState

left_arg = RegisterUserInput(size=3, name="left_arg_name")
right_arg = RegisterUserInput(size=3, name="right_arg_name")
sum_arg = RegisterUserInput(size=4, name="sum")
adder_params = Adder(
    left_arg=left_arg, right_arg=right_arg, inplace_arg="right", output_name="sum"
)
multiplier_params = Multiplier(
    left_arg=left_arg, right_arg=sum_arg, output_name="product"
)

model_designer = ModelDesigner()
control_states = [
    ControlState(ctrl_state="10", name="first_control"),
    ControlState(num_ctrl_qubits=1, name="second_control"),
]
inter_wires = {
    "left_arg_name": QReg(size=3),
    "sum": QReg(size=4),
    "first_control": QReg(size=2),
    "second_control": QReg(size=1),
}
model_designer.Adder(
    params=adder_params, control_states=control_states, out_wires=inter_wires
)
model_designer.Multiplier(
    params=multiplier_params, control_states=control_states, in_wires=inter_wires
)

Advanced Usage - Skipping Controls

The use of controlled operations is costly. It increases sufficiently the number of multi-qubit gates applied in the circuit, taking a toll on error, circuit dimensions, etc. Therefore, we'd like to keep controlled operations to a bare minimum. In addition to Classiq's synthesis engine's optimizations, the user may indicate that particular function calls be skipped during control by using the should_control flag. Consider, for example, an algorithm implemented by the unitary \(U ^\dagger V U\), with \(U\) and \(V\) unitary. Such a structure is very common in quantum algorithms. It is easy to see that:

\[ \begin{align} ctrl \left(U ^\dagger V U\right) & = ctrl\left(U ^\dagger\right) ctrl\left(V\right) ctrl \left(U\right) \\ & = U ^\dagger ctrl\left(V\right) U \\ \end{align} \]

Both operations on the right-hand side of this equation are mathematically equivalent. However, the bottom one is superior computationally. Setting should_control=False for function calls corresponding to \(U ^\dagger\), \(U\) may thus help us create far better circuits.

Below are examples with the should_control flag set to False. We define a composite subtraction function, with uncomputation of the negation part. When the composite subtraction function is called with control, the control of inner negation calls will be skipped. The scope here is important - the should_control flag is effective only within a composite function. Note that control states below, or at the same hierarchy level are not affected should_control flag. Namely, if we add control states to the negation calls below, these will be applied regardless of the value of the flag. It only applies to control states from an outer scope.

{
    "function_library": {
        "name": "my_library",
        "functions": [
            {
                "name": "my_subtractor_function",
                "logic_flow":
                [
                    {
                        "function": "Negation",
                        "function_params": {"arg": {"size": 3}, "inplace": true},
                        "inputs": {"in_arg": "subtrahend_incoming_wire"},
                        "outputs": {"negated": "negation_output_wire"},
                        "should_control": false
                    },
                    {
                        "function": "Adder",
                        "function_params": {
                            "left_arg": {"size": 3},
                            "right_arg": {"size": 4},
                            "inplace_arg": null
                        },
                        "inputs": {
                            "left_arg": "minuend_wire",
                            "right_arg": "negation_output_wire"
                        },
                        "outputs": {
                            "sum": "difference_wire",
                            "right_arg": "negation_output_carried_wire"
                        },
                        "should_control": true
                    },
                    {
                        "function": "Negation",
                        "function_params": {"arg": {"size": 3}, "inplace": true},
                        "inputs": {"negated": "negation_output_carried_wire"},
                        "outputs": {"in_arg": "subtrahend_outgoing_wire"},
                        "is_inverse": true,
                        "should_control": false
                    }
                ],
                "custom_inputs": {
                    "minuend": {"wire": "minuend_wire", "reg": {"size": 3}},
                    "subtrahend": {"wire": "subtrahend_incoming_wire", "reg": {"size": 3}}
                },
                "custom_outputs": {
                    "difference": {"wire": "difference_wire", "reg": {"size": 5}},
                    "subtrahend": {"wire": "subtrahend_outgoing_wire", "reg": {"size": 3}}
                }
            }
        ]
    }
}
from classiq import CompositeFunctionGenerator
from classiq.builtin_functions import Adder, Negation
from classiq.interface.generator.arith.register_user_input import RegisterUserInput

subtrahend = RegisterUserInput(size=3)
minuend = RegisterUserInput(size=3)
negation_params = Negation(arg=subtrahend, inplace=True)
adder_params = Adder(
    left_arg=minuend,
    right_arg=negation_params.outputs[negation_params.output_name],
    inplace_arg=None,
)

function_generator = CompositeFunctionGenerator(function_name="my_subtractor_function")

input_dict = function_generator.create_inputs(
    {"minuend": minuend, "subtrahend": subtrahend}
)

negation_outputs = function_generator.Negation(
    negation_params, should_control=False, in_wires={"in_arg": input_dict["subtrahend"]}
)
adder_outputs = function_generator.Adder(
    adder_params,
    should_control=True,
    in_wires={
        "left_arg": input_dict["minuend"],
        "negated": negation_outputs["negated"],
    },
)
negation_uncomp_outputs = function_generator.Negation(
    negation_params,
    is_inverse=True,
    should_control=False,
    in_wires={"negated": adder_outputs["negated"]},
)

function_generator.set_outputs(
    {
        "difference": (
            adder_outputs[adder_params.output_name],
            adder_params.outputs[adder_params.output_name],
        ),
        "subtrahend": (negation_uncomp_outputs["in_arg"], subtrahend),
    }
)