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.
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 qubit5
of registerIN
. Note: qubit5
is the 6th qubit due to zero-based indexing.IN[2:5]
refers to qubits2
,3
,4
(the slicing stop is exclusive)IN[2:8:2]
refers to qubits2
,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.
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.
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.