Skip to content

Composite Functions

Composite functions are functions whose implementations are not described at the gate level, but at the functional level. This is another level of abstraction supported by the Classiq platform: the functions you add to your circuit can themselves be composed of smaller functions. The synthesis engine then decomposes the bigger functions into smaller ones, allowing for optimization to happen at the gate level.

Defining the Logic Flow

Defining the logic flow implemented by a composite function is done in a similar fashion to defining the entire circuit. Thus, it requires listing the different function calls, their parameters, and wiring outputs to inputs. In addition, one has to set the inputs and outputs of the composite function, and state the inner wires they're connected to. This is done automatically in the Python SDK, but must be provided manually in the textual model.

In the textual model, this is done by listing the function calls under the logic_flow field of the function definition.

{
    "constraints": {
      "max_width": 20,
      "max_depth": 100
    },
    "function_library": {
        "name": "my_library",
        "functions": [{
            "name": "my_composite_function",
            "logic_flow": [{
                "function": "StatePreparation",
                "function_params": {
                    "num_qubits": 4,
                    "probabilities": [0.5, 0.1, 0.2, 0.005, 0.015, 0.12, 0.035, 0.025]
                },
                "outputs": "sp_out_wire"
            },
            {
                "function": "QFT",
                "function_params": {
                    "num_qubits": 3
                },
                "inputs": "sp_out_wire",
                "outputs": "qft_out_wire"
            }],
            "inputs" : {},
            "outputs": {"COMP_OUT": "qft_out_wire"}
        }]
    }
}

In the SDK, this is done using the CompositeFunctionGenerator object. The methods for adding function calls are equivalent to the methods of the ModelDesigner object.

from classiq import QReg, CompositeFunctionGenerator
from classiq.builtin_functions import QFT, StatePreparation

function_generator = CompositeFunctionGenerator(function_name="my_composite_function")
x = QReg(size=3)

probabilities = (0.5, 0.1, 0.2, 0.005, 0.015, 0.12, 0.035, 0.025)
sp_params = StatePreparation(
    probabilities=probabilities,
    num_qubits=x.size + 1,  # 1 extra for an auxillary qubit
)

sp_output_dict = function_generator.StatePreparation(sp_params)

qft_params = QFT(num_qubits=3)
function_generator.QFT(qft_params, in_wires={"IN": sp_output_dict["OUT"]})

Using your own functions within the composite function is also possible. Calling the functions is done in the same way as described in the basic usage section. The functions you use must be defined in the same library. When using the SDK, you must use the include_library()method of CompositeFunctionGenerator before callings functions defined in the library.

Defining Inputs and Outputs

The previous section discusses defining the logic flow between the inner function calls. We also want to define the way the composite function connects to other functions, i.e., its inputs and outputs. For now, defining an input or output simply requires choosing its name, and connecting it to an input or an output, respectively, of one of the inner function calls. Thus, when calling this function within a circuit, any wire connected to an input or output of the composite function, would be "redirected" to the input or output of the inner call, as defined.

In the following example, we define a subtraction function that has two inputs, a and b, and one output, difference.

In the textual model, adding an input or an output is done by defining a wire that only connects on one side. It is automatically deduced that this wire represents an input or an output, and the specified wire name is chosen as its name.

{
    "constraints": {
      "max_width": 20,
      "max_depth": 100
    },
    "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_wire"
                },
                "outputs": {
                    "negated": "negation_output_wire"
                }
            },
            {
                "function": "Adder",
                "function_params": {
                    "left_arg": {"size": 3},
                    "right_arg": {"size": 4}
                },
                "inputs": {
                    "left_arg": "minuend_wire",
                    "right_arg": "negation_output_wire"
                },
                "outputs": {
                    "sum": "difference_wire"
                }
            }],
            "inputs": {"minuend": "minuend_wire", "subtrahend": "subtrahend_wire"},
            "outputs": {"difference": "difference_wire"}
        }]
    }
}

Defining the inputs and outputs in the SDK this is done using the create_inputs() and set_outputs() methods of CompositeFunctionGenerator:

  • create_inputs() gets as input an iterable of input names, and returns a dictionary, where the keys are the input names, and the values are wires that can be used as an input of an inner function call.
  • set_outputs() gets as input a dictionary, where the keys are the output names, and the values are wires returned by adding function calls to the CompositeFunctionGenerator instance.
from classiq import CompositeFunctionGenerator
from classiq.builtin_functions import Adder, Negation
from classiq.interface.generator.arith.register_user_input import RegisterUserInput

negation_params = Negation(arg=RegisterUserInput(size=3), inplace=True)
adder_params = Adder(
    left_arg=RegisterUserInput(size=3), right_arg=RegisterUserInput(size=4)
)

function_generator = CompositeFunctionGenerator(function_name="my_subtractor_function")

input_dict = function_generator.create_inputs(["minuend", "subtrahend"])

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

function_generator.set_outputs({"difference": adder_outputs["sum"]})

Using Composite Functions

Once we define a composite function, we can then add it to a function library and use it in our circuits.

{
    "constraints": {
      "max_width": 20,
      "max_depth": 100
    },
    "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_wire"
                },
                "outputs": {
                    "negated": "negation_output_wire"
                }
            },
            {
                "function": "Adder",
                "function_params": {
                    "left_arg": {"size": 3},
                    "right_arg": {"size": 4}
                },
                "inputs": {
                    "left_arg": "minuend_wire",
                    "right_arg": "negation_output_wire"
                },
                "outputs": {
                    "sum": "difference_wire"
                }
            }],
            "inputs": {"minuend": "minuend_wire", "subtrahend": "subtrahend_wire"},
            "outputs": {"difference": "difference_wire"}
        }]
    },
    "logic_flow": [{
        "function": "StatePreparation",
        "function_params": {
            "num_qubits": 4,
            "probabilities": [0.5, 0.1, 0.2, 0.005, 0.015, 0.12, 0.035, 0.025]
        },
        "outputs": "sp_out_wire"
    },
    {
        "function": "CustomFunction",
        "function_params":{
            "name": "my_subtractor_function"
        },
        "inputs": {"minuend": "sp_out_wire"}
    }]
}

In the SDK, we first need to add it to create an instance of FunctionData, by using the to_function_data() method.

from classiq import ModelDesigner, CompositeFunctionGenerator, FunctionLibrary
from classiq.builtin_functions import Adder, Negation, StatePreparation
from classiq.interface.generator.arith.register_user_input import RegisterUserInput


negation_params = Negation(arg=RegisterUserInput(size=3), inplace=True)
adder_params = Adder(
    left_arg=RegisterUserInput(size=3), right_arg=RegisterUserInput(size=4)
)

probabilities = (0.5, 0.1, 0.2, 0.005, 0.015, 0.12, 0.035, 0.025)
sp_params = StatePreparation(probabilities=probabilities, num_qubits=4)

function_generator = CompositeFunctionGenerator(function_name="my_subtractor_function")
input_dict = function_generator.create_inputs(["minuend", "subtrahend"])
negation_outputs = function_generator.Negation(
    negation_params, in_wires={"in_arg": input_dict["subtrahend"]}
)
adder_outputs = function_generator.Adder(
    adder_params,
    in_wires={
        "left_arg": input_dict["minuend"],
        "right_arg": negation_outputs["negated"],
    },
)
function_generator.set_outputs({"difference": adder_outputs["sum"]})

composite_function = function_generator.to_function_data()
function_library = FunctionLibrary(composite_function)

model_designer = ModelDesigner()
model_designer.include_library(function_library)
sp_outputs = model_designer.StatePreparation(sp_params)
model_designer.my_subtractor_function(in_wires={"minuend": sp_outputs["OUT"]})

circuit = model_designer.synthesize()
circuit.show()

The circuit created by this example is shown below.

 Composite function example