Skip to content

Circuit Synthesis

Quantum algorithm design is a complex task, and solutions based on the approach of designing at the gate level are not scalable. Likewise, solutions based on combining existing building blocks are very limited in their scope. The Classiq platform allows high level description of quantum algorithms at the functional level and automatically synthesizes a corresponding circuit. The synthesis process refines the functional requirements and allocates and optimizes available resources, such as the number of qubits available and the circuit depth.

The basic description of quantum algorithms in the Classiq platform is by quantum functions. A quantum function can be implemented in multiple ways, each with different properties, such as number of qubits, number of auxiliary qubits, depth, approximation level, etc. The user may define any level of refinement of the function, from the pure abstract to the fully defined. The synthesis engine fills in the details to provide a concrete implementation satisfying all user requirements.

Moreover, a complete quantum algorithm can be realized in multiple ways, utilizing different design choices such as function implementations, placement, uncompute strategies, qubit management, wirings, etc. The platform sifts through the design space and finds a best realization given the user requirements and resource constraints.

Getting started with circuit synthesis

The first step of the synthesis process is defining the basic resources available for the circuit.

In the example below, we constrain the number of qubits in the circuit to be 20, and the maximum depth of the circuit to be 100. Note that both fields are required.

Starting the synthesis process using the textual interface is done by opening the Command Palette (Ctrl+Shift+P / Command+Shift+P on Windows/Mac, respectively) and choosing the "Generate Quantum Circuit" command.

In the below example, we simply create a model file, containing our resource constraints.

{
  "constraints": {
    "max_width": 20,
    "max_depth": 100
  }
}

The circuit is synthesized by using the ModelDesigner object which is responsible for the synthesis. When creating a ModelDesigner, we define the constraints by the max_width and max_depth arguments. To synthesize the circuit, the ModelDesigner.synthesize() function is called (as seen in the example below.)

Alternatively, the async function ModelDesigner.synthesize_async() can be used as part of an async code (see Advanced Usage).

from classiq import ModelDesigner

model_designer = ModelDesigner()

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

Defining an Algorithm

A quantum algorithm is 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.

{
  "constraints": {
    "max_width": 20,
    "max_depth": 100
  },
  "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]
      }
    }
  ]
}
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()

The function StatePreparation is 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.

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.

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.

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.

Note that the max_width constraint refers to the entire circuit, while the num_qubits field in StatePreparation refers only to the function call. Generally, constraints can be defined for the entire circuit, or for a specific function call.

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.

{
  "constraints": {
    "max_width": 20,
    "max_depth": 100
  },
  "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"
    }
  ]
}
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. Later examples will show how to handle functions with multiple inputs/outputs or split a single input/output into multiple ones.

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

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.

The above example includes only one connection. However, some functions have more than one input or more than one output. 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.

{
"constraints": {
  "max_width": 20,
  "max_depth": 100
},
"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

Register Indexing and Slicing

The platform allows referring to specific qubits or qubit ranges of quantum registers similarly to Python sequences, such as lists and tuples.

The indexing is zero based. Referring to multiple consecutive indices, commonly referred to as slicing, is obtained by specifying the start (inclusive) and stop (exclusive) indices and using a colon separator, and slicing with step size different from 1 by two colon separators.

Examples:

  • IN[5] refers to qubit 5 of register IN. Note: qubit 5 is the 6th qubit due to zero-based indexing.
  • IN[2:5] refers to qubits 2,3,4 (the slicing stop is exclusive)
  • IN[2:8:2] refers to qubits 2,4,6 (again, the slicing stop is exclusive)
  • IN[:-1] refers to all qubits except the last one

The example below generates an iQFT-CX-QFT circuit, where the value of the CX control bit is obtained from the most significant bit of the iQFT. This block is used in the modular addition circuit, which is the building block of Beauregard's Shor algorithm implementation [2].

{
  "logic_flow": [
    {
        "function": "QFT",
        "name": "iqft",
        "function_params": {
            "num_qubits": 6,
            "inverse": true
        },
        "outputs": {
            "OUT[:-1]": "lsb",
            "OUT[-1]": "msb_inv"
        }
    },
    {
        "function": "CXGate",
        "name": "cx",
        "function_params": {},
        "inputs": {
            "CTRL_IN": "msb_inv"
        },
        "outputs": {
            "CTRL_OUT": "msb"
        }
    },
    {
        "function": "QFT",
        "name": "qft",
        "function_params": {
            "num_qubits": 6
        },
        "inputs": {
            "IN[:-1]": "lsb",
            "IN[-1]": "msb"
        }
    }
  ]
}
from classiq.interface.generator.qft import QFT
from classiq.interface.generator.standard_gates.controlled_standard_gates import CXGate

from classiq import ModelDesigner, QReg

model_designer = ModelDesigner()
qft_params_inverse = QFT(num_qubits=6, inverse=True)
qft_params = QFT(num_qubits=6)
cx_params = CXGate()

lsb = QReg(5)
msb = QReg(1)
msb_inv = QReg(1)

model_designer.QFT(
    qft_params_inverse,
    call_name="iqft",
    out_wires={"OUT[:-1]": lsb, "OUT[-1]": msb_inv},
)
model_designer.CXGate(
    cx_params,
    call_name="cx",
    in_wires={"CTRL_IN": msb_inv},
    out_wires={"CTRL_OUT": msb},
)
model_designer.QFT(
    qft_params,
    call_name="qft",
    in_wires={"IN[:-1]": lsb, "IN[-1]": msb},
)

circuit = model_designer.synthesize()
circuit.show_interactive()

The output circuit is shown below at the functional level.

 Slicing example

Synthesis Preferences

The preferences of the synthesis process can be modified by the user, including:

  • Textual output formats (see Output Formats)
  • Hardware settings
  • Timeout
  • Optimization criteria

The platform provides both textual and graphical representations of the generated circuit. In the example below, the output format is chosen to include both Q# and QASM.

In addition, specific basis gates were selected for the synthesis: controlled not, controlled phase, square-root of not, Z-rotation and not gates.

{
  "constraints": {
    "max_width": 20,
    "max_depth": 100
  },
  "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"
    }
  ],
  "preferences": {
    "output_format": ["qs", "qasm"],
    "custom_hardware_settings":{
      "basis_gates":["cx", "cp", "sx", "rz", "x"]
    }
  }
}
from classiq.interface.generator.model.preferences.preferences import (
    CustomHardwareSettings,
    Preferences,
)
from classiq.interface.generator.state_preparation import Metrics, NonNegativeFloatRange

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

custom_hardware_settings = CustomHardwareSettings(
    basis_gates=["cx", "cp", "sx", "rz", "x"]
)
preferences = Preferences(
    output_format=["qasm", "qs"], custom_hardware_settings=custom_hardware_settings
)
model_designer = ModelDesigner(preferences=preferences)

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()

Output Formats

The Classiq platform provides different possibilities for the output format of generated circuits. The output options are:

  • "qasm" - Open QASM 2.0 (The default option.)
  • "qs" - Q#
  • "ll" - Microsoft's QIR
  • "ionq" - IonQ Json format
  • "cirq_json" - Cirq Json format
  • "qasm_cirq_compatible" - Open QASM 2.0 compatible for Cirq.

Multiple output formats can be chosen.

Hardware Settings

The Classiq platforms allows to provide a list of basis gates used during the synthesis process. The allowed gates are listed as follows:

  • Single-qubit gates: u1, u2, u, p, x, y, z, t, tdg, s, sdg, sx, sxdg, rx, ry, rz, id, h
  • Basic two-qubit gates: cx, cy, cz, cp, ch
  • Extra two-qubit gates: swap, rxx, rzz, crx, cry, crz, csx, cu1, cu
  • Three-qubit gates: ccx, cswap

If none specified, the default set consists of all single-qubit gates and the basic two-qubit gates.

Example - Option Pricing

Some methods of option pricing [1] begin with loading multiple quantum states onto separate registers. This is typically the first stage of the algorithm. Below we show an example of loading multiple two-qubit states. Each state preparation function uses three qubits, two for the state and an auxiliary qubit. The synthesis process selects a different auxiliary reuse strategy depending on the constraints.

{
    "constraints": {
      "max_width": 9,
      "max_depth": 100
    },
    "logic_flow": [{
        "function": "StatePreparation",
        "function_params": {
            "probabilities": [0.2, 0.3, 0.4, 0.1],
            "error_metric": {"KL": {"upper_bound": 0.01}},
            "num_qubits": 3
        },
        "name": "state_preparation_0"
    },
    {
        "function": "StatePreparation",
        "function_params": {
            "probabilities": [0.1, 0.2, 0.3, 0.4],
            "error_metric": {"KL": {"upper_bound": 0.01}},
            "num_qubits": 3
        },
        "name": "state_preparation_1"
    },
    {
        "function": "StatePreparation",
        "function_params": {
            "probabilities": [0.0, 0.1, 0.2, 0.7],
            "error_metric": {"KL": {"upper_bound": 0.01}},
            "num_qubits": 3
        },
        "name": "state_preparation_2"
    }]
}
from classiq import ModelDesigner
from classiq.builtin_functions import StatePreparation
from classiq.interface.generator.model import Constraints
from classiq.interface.generator.state_preparation import (
    Metrics,
    NonNegativeFloatRange,
)

STATE_PREPARATION_COUNT = 3
QUBIT_COUNT = 9
MAX_DEPTH = 100

constraints = Constraints(max_width=QUBIT_COUNT, max_depth=MAX_DEPTH)
model_designer = ModelDesigner(constraints=constraints)

probabilities = (0.1, 0.2, 0.3, 0.4)
error = {Metrics.KL: NonNegativeFloatRange(upper_bound=0.01)}
sp_params = StatePreparation(
    probabilities=probabilities, num_qubits=3, error_metric=error
)

for n in range(STATE_PREPARATION_COUNT):
    model_designer.StatePreparation(sp_params, call_name=f"state_preparation_{n}")

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

As shown in the image below, auxiliary reuse is not necessary in the case where the max_width constraint is 9 or more, where in the case of max_width=7, auxiliary reuse is required.

 Option pricing example

In this example, three states were prepared. This is a simple case, and checking all possible options can be done manually. However, if the use-case requires preparing 20 states the task becomes much harder.

When each state preparation has flexibility in the error, and different states are prepared on registers of different sizes, the problem becomes impossible to handle manually.

This is one very simple example of where describing the circuit at the functional level can help optimize over a huge amount of options, giving solutions many orders of magnitude better than can be done manually. Additional examples include functions with multiple implementation options, uncompute strategies, ordering commutative functions and sub-functions, optimizing functions for specific hardware constraints, and more.

[1] S. Chakrabarti et al, A Threshold for Quantum Advantage in Derivative Pricing, https://quantum-journal.org/papers/q-2021-06-01-463/ (2021) [2] Stephane Beauregard. 2003. Circuit for Shor's algorithm using 2n+3 qubits. Quantum Info. Comput. 3, 2 (March 2003), 175–185.