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\). You can also use different states for control, corresponding to other conditions. \({ctrl}_{1}\) is the quantum analog of if q == 1, with q as 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. For example, \({ctrl}_{110}\) is controlled 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, whether 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, 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, you can send multiple control state objects. 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 Model, RegisterUserInput, ControlState
from classiq.builtin_functions import Adder, Multiplier

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 = Model()
control_states = [
    ControlState(ctrl_state="10", name="first_control"),
    ControlState(num_ctrl_qubits=1, name="second_control"),
]
inter_wires = model.Adder(params=adder_params, control_states=control_states)
model.Multiplier(
    params=multiplier_params, control_states=control_states, in_wires=inter_wires
)

Advanced Usage - Skipping Controls

The use of controlled operations is costly. It substantially increases the number of multi-qubit gates applied in the circuit, increasing errors, circuit dimensions, etc. Therefore, it is preferable to keep controlled operations to a bare minimum. In addition to the optimizations performed by the Classiq synthesis engine, you can skip particular function calls during control 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 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 therefore help create far better circuits.

Below are examples with the should_control flag set to False. 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 is skipped. The scope here is important; the should_control flag is effective only within a composite function. Note that the should_control flag does not affect control states below or at the same hierarchy level. Namely, if we add control states to the negation calls below, they apply regardless of the value of the flag. This 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
                    }
                ],
                "input_decls": {
                    "minuend": {"wire": "minuend_wire", "reg": {"size": 3}},
                    "subtrahend": {"wire": "subtrahend_incoming_wire", "reg": {"size": 3}}
                },
                "output_decls": {
                    "difference": {"wire": "difference_wire", "reg": {"size": 5}},
                    "subtrahend": {"wire": "subtrahend_outgoing_wire", "reg": {"size": 3}}
                }
            }
        ]
    }
}
from classiq import FunctionGenerator, RegisterUserInput, QUInt
from classiq.builtin_functions import Adder, Negation

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 = FunctionGenerator(function_name="my_subtractor_function")

input_dict = function_generator.create_inputs(
    {"minuend": QUInt[3], "subtrahend": QUInt[3]}
)

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],
        "subtrahend": negation_uncomp_outputs["in_arg"],
    }
)