Skip to content

Defining Algorithms

Quantum algorithms are constructed from functions, which are the functional building blocks that implement quantum logic, and the data flow between them.

Calling a Function

As in classical programming, calling a function requires specifying its name and its parameters. In the following example, we call a state preparation function, a built-in function developed by Classiq. There are multiple such functions, listed in the Built-In Function Library. The user may also define their own functions, as described in the Function section.

In order to call a function using the textual interface, we list it under the logic_flow field and pass on its parameters under the function_params field of the function.

{
  "logic_flow": [{
      "function": "StatePreparation",
      "function_params": {
        "num_qubits": 3,
        "probabilities": [0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125]
      }
    }
  ]
}

In the SDK, we create the StatePreparation object, which represents the parameters of the function, and then pass it on to the StatePreparation method of the ModelDesigner object.

from classiq import ModelDesigner
from classiq.builtin_functions import StatePreparation

model_designer = ModelDesigner()

probabilities = (0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125)
sp_params = StatePreparation(probabilities=probabilities, num_qubits=3)

model_designer.StatePreparation(sp_params)

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

We can also determine the name of a call, e.g. in order to distinguish multiple calls to the same function. This is done by setting the name field of the call in the textual interface or the call_name argument in the SDK.

Defining the Data Flow

The data flow between the functions is defined by connecting an output of a function to an input of another function.

In the following example, the output of the state preparation function is connected to the input of the quantum Fourier transform (QFT) function.

In order to define a connection using the textual model, we name the connection "sp_out", and list it under the outputs of StatePreparation and the inputs of QFT.

{
  "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],
        "error_metric": { "KL": {"upper_bound": 0.3}}
      },
      "outputs": "sp_out"
    },
    {
      "function": "QFT",
      "function_params": {
        "num_qubits": 3
      },
      "inputs": "sp_out"
    }
  ]
}

When calling a function in the SDK, its outputs are returned in a dictionary. The keys of the dictionary are the function output names. The values of the dictionary are Wire objects that represent the outputs.

The inputs of a function are also defined in a dictionary, passed to the in_wires argument. Similarly, the keys of the dictionary are the input names and the values are Wire object. Passing a Wire object that represents an output to the input of another function connects the two.

from classiq import ModelDesigner, QReg
from classiq.builtin_functions import QFT, StatePreparation
from classiq.interface.generator.state_preparation import (
    Metrics,
    NonNegativeFloatRange,
)

model_designer = ModelDesigner()
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 for an auxillary qubit
    error_metric={Metrics.KL: NonNegativeFloatRange(upper_bound=0.3)},
)

model_designer.StatePreparation(sp_params, out_wires=x)

qft_params = QFT(num_qubits=x.size)
model_designer.QFT(qft_params, in_wires=x)

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

The state preparation function has a single pre-defined output, and the QFT function has a single pre-defined input. In that case, the Classiq platform allows skipping the explicit specification of each, requiring only to define the connection. The following example shows how to handle functions with multiple inputs/outputs. Splitting a single input/output into multiple ones is possible using register slicing.

The following example implements addition of two prepared states. The states are prepared using the StatePreparation function and the functions' outputs are added using the Adder function. We use the left_arg and right_arg inputs of the adder function to connect each state preparation output into its corresponding addend. Generally, each function has a set of predefined input and output names.

{
"logic_flow": [{
    "function": "StatePreparation",
    "function_params": {
        "num_qubits": 5,
        "probabilities": [0.4, 0.05, 0.2, 0.05, 0.3, 0.0, 0.0, 0.0],
        "error_metric": {"KL": {"upper_bound": 0.2}}
    },
    "outputs": "sp_output_a",
    "name": "state_preparation_1"
},
{
    "function": "StatePreparation",
    "function_params": {
        "num_qubits": 5,
        "probabilities": [0.5, 0.1, 0.2, 0.005, 0.015, 0.12, 0.035, 0.025],
        "error_metric": {"KL": {"upper_bound": 0.2}}
    },
    "outputs": "sp_output_b",
    "name": "state_preparation_2"
},
{
    "function": "Adder",
    "function_params": {
        "left_arg": { "size": 3 },
        "right_arg": { "size": 3 }
    },
    "inputs": {
        "left_arg": "sp_output_a",
        "right_arg": "sp_output_b"
    },
    "name": "adder"
}]
}
from classiq import ModelDesigner, QUInt
from classiq.builtin_functions import StatePreparation
from classiq.interface.generator.arith import arithmetic, binary_ops
from classiq.interface.generator.state_preparation import (
    Metrics,
    NonNegativeFloatRange,
)

model_designer = ModelDesigner()
x = QUInt(size=3)
y = QUInt(size=3)

probabilities_a = (0.4, 0.05, 0.2, 0.05, 0.3, 0.0, 0.0, 0.0)
error_metric_a = {Metrics.KL: NonNegativeFloatRange(upper_bound=0.2)}
sp_params_a = StatePreparation(
    probabilities=probabilities_a,
    num_qubits=x.size + 2,  # 2 auxillary qubits
    error_metric=error_metric_a,
)
model_designer.StatePreparation(
    sp_params_a, out_wires=x, call_name="state_preparation_1"
)

probabilities_b = (0.5, 0.1, 0.2, 0.005, 0.015, 0.12, 0.035, 0.025)
error_metric_b = {Metrics.KL: NonNegativeFloatRange(upper_bound=0.2)}
sp_params_b = StatePreparation(
    probabilities=probabilities_b,
    num_qubits=y.size + 2,  # 2 auxillary qubits
    error_metric=error_metric_b,
)
model_designer.StatePreparation(
    sp_params_b, out_wires=y, call_name="state_preparation_2"
)

adder_arg = arithmetic.RegisterUserInput(size=x.size)
adder_params = binary_ops.Adder(
    left_arg=x.to_register_user_input(), right_arg=y.to_register_user_input()
)
model_designer.Adder(adder_params, in_wires=[x, y], call_name="adder")

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

The output circuit is shown below at the functional level.

 Adder example