Advance Usage¶
Multiple Implementations¶
In order to fully utilize the strength of the Classiq engine, we allow the user to add multiple different implementations for the same function.
The simplest example is two implementations of a controlled-Z gate.
Example: Controlled-Z Gate with Two Implementations¶
{
"function_library":{
"name": "my_library",
"functions": [{
"name": "my_controlled_z_gate",
"implementations": [{
"name": "option_1",
"serialized_circuit": "OPENQASM 2.0;\ninclude \"qelib1.inc\";\nqreg q[2];\nh q[1];\n cx q[0], q[1];\nh q[1];"
},
{
"name": "option_2",
"serialized_circuit": "OPENQASM 2.0;\ninclude \"qelib1.inc\";\nqreg q[2];\nh q[1];\n cx q[0], q[1];\nh q[1];"
}],
"register_mapping": {
"input_registers": [{
"name": "input_1",
"qubits": [1]
},
{
"name": "input_2",
"qubits": [0]
}],
"output_registers": [{
"name": "output_1",
"qubits": [1]
},
{
"name": "output_2",
"qubits": [0]
}]
}
}]
}
}
from typing import Tuple
from classiq import qfunc, QReg, QASM_INTRO
@qfunc
def my_controlled_z_gate(control: QReg[1], target: QReg[1]) -> Tuple[QReg[1], QReg[1]]:
return (
QASM_INTRO
+ """qreg q[2];
h q[1];
cx q[0], q[1];
h q[1];"""
)
@my_controlled_z_gate.add_implementation
def my_other_controlled_z_gate(
control: QReg[1], target: QReg[1]
) -> Tuple[QReg[1], QReg[1]]:
# The same QASM code
return (
QASM_INTRO
+ """qreg q[2];
cz q[0], q[1];"""
)
Each entry of the input_registers
and output_registers
must match all
implementations of the same function in its name
and number of qubits.
The Classiq synthesis engine is then able to choose the optimal implementation based on the circuit constraints and optimization criteria.
Auxiliaries and Uncomputation¶
It is often required to use auxiliary qubits which are neither input nor outputs of the function. In those cases we allow our users to define functions which use such auxiliaries.
NOTE: All auxiliary_registers
are assumed to be initialized as zero and
returned to zero at the end of the function.
If some registers are not returned to zero at the end of the computation, please use the
zero_input_registers
field to declare them.
The following code introduces a simple ripple adder [1] as a function.
Example: Ripple Adder¶
{
"function_library":{
"name": "my_library",
"functions": [{
"name": "init_a_register_to_2",
"implementations": [{
"serialized_circuit": "OPENQASM 2.0;\ninclude \"qelib1.inc\";\nqreg q[2];\nx q[1];"
}],
"register_mapping": {
"output_registers": [{
"name": "init_2_output",
"qubits": [0,1]
}],
"zero_input_registers": [{
"name": "zero_input",
"qubits": [0,1]
}]
}
},
{
"name": "my_ripple_adder",
"implementations": [{
"serialized_circuit": "OPENQASM 2.0;\ninclude \"qelib1.inc\";\ngate maj a,b,c\n{\n cx c,b;\n cx c,a;\n ccx a,b,c;\n}\ngate uma a,b,c\n{\n ccx a,b,c;\n cx c,a;\n cx a,b;\n}\nqreg q[6];\nmaj q[0],q[1],q[2];\nmaj q[2],q[3],q[4];\ncx q[4],q[5];\numa q[2],q[3],q[4];\numa q[0],q[1],q[2];",
"auxiliary_registers": [{
"name": "auxiliary",
"qubits": [0]
}]
}],
"register_mapping": {
"input_registers": [{
"name": "input_a",
"qubits": [2,4]
},
{
"name": "input_b",
"qubits": [1,3]
}],
"output_registers": [{
"name": "output_a",
"qubits": [2,4]
},
{
"name": "output_a_plus_b",
"qubits": [1,3,5]
}],
"zero_input_registers": [{
"name": "zero_input",
"qubits": [5]
}]
}
}]
},
"logic_flow": [{
"function": "init_a_register_to_2",
"function_params": {
},
"outputs": {
"init_2_output": "wire_a"
}
},
{
"function": "init_a_register_to_2",
"function_params": {
},
"outputs": {
"init_2_output": "wire_b"
}
},
{
"function": "my_ripple_adder",
"function_params": {
},
"inputs": {
"input_a": "wire_a",
"input_b": "wire_b"
}
}]
}
from classiq import (
ForeignFunctionDefinition,
FunctionImplementation,
RegisterMappingData,
Register,
Model,
FunctionLibrary,
)
def define_init_2() -> ForeignFunctionDefinition:
init_2_qasm = """OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
x q[1];"""
init_2_data = ForeignFunctionDefinition(
name="init_a_register_to_2",
implementations=FunctionImplementation(serialized_circuit=init_2_qasm),
register_mapping=RegisterMappingData(
output_registers=[Register(name="init_2_output", qubits=(0, 1))],
zero_input_registers=[Register(name="zero_input", qubits=(0, 1))],
),
)
return init_2_data
def define_ripple_adder() -> ForeignFunctionDefinition:
ripple_adder_qasm = """OPENQASM 2.0;
include "qelib1.inc";
gate maj a,b,c
{
cx c,b;
cx c,a;
ccx a,b,c;
}
gate uma a,b,c
{
ccx a,b,c;
cx c,a;
cx a,b;
}
qreg q[6];
maj q[0],q[1],q[2];
maj q[2],q[3],q[4];
cx q[4],q[5];
uma q[2],q[3],q[4];
uma q[0],q[1],q[2];"""
ripple_adder_data = ForeignFunctionDefinition(
name="my_ripple_adder",
implementations=FunctionImplementation(
serialized_circuit=ripple_adder_qasm,
auxiliary_registers=Register(
name="auxiliary",
qubits=(0,),
),
),
register_mapping=RegisterMappingData(
input_registers=[
Register(name="input_a", qubits=(2, 4)),
Register(name="input_b", qubits=(1, 3)),
],
output_registers=[
Register(name="output_a", qubits=(2, 4)),
Register(name="output_a_plus_b", qubits=(1, 3, 5)),
],
zero_input_registers=[Register(name="zero_input", qubits=(5,))],
),
)
return ripple_adder_data
function_library = FunctionLibrary(
define_init_2(), define_ripple_adder(), name="my_library"
)
model = Model()
model.include_library(function_library)
output_dict_init_a = model.init_a_register_to_2()
output_dict_init_b = model.init_a_register_to_2()
model.my_ripple_adder(
in_wires={
"input_a": output_dict_init_a["init_2_output"],
"input_b": output_dict_init_b["init_2_output"],
},
)
circuit = model.synthesize()
circuit.show_interactive()
The output circuit is shown below at the functional level.
This ripple adder requires one auxiliary qubit and returns it to zero at the end of the computation [1] .
All auxiliary_registers
in functions must be similarly returned to zero at the end
of the computation.
Parametric functions and the .qfunc extension¶
Sometimes, it is necessary to define a function whose implementation is based on one or more external parameters. For example, an MCX gate with a parametric number, \(n\), of control qubits. Implementing such functions with the previously defined APIs is possible, but requires adding each specific instance of the parameter set as a separate function to the library.
In the Python SDK, one can define parametric functions in a much more robust way,
using the QuantumFunctionFactory
class.
Following is an example of an MCX gate, with the number of controls as a parameter of the function:
from typing import Tuple
from classiq.quantum_functions.quantum_function import (
QuantumFunction,
QuantumFunctionFactory,
)
from classiq import AuxQReg, QASM_INTRO, QReg, qfunc
class MyMCX(QuantumFunctionFactory):
def __init__(self, num_controls: int, *args, **kwargs):
if num_controls < 2:
raise ValueError(f"Cannot implement with {num_controls} < 2 controls")
self.num_controls = num_controls
super().__init__(*args, **kwargs)
@property
def definition(self) -> QuantumFunction:
def _cascade_cnots(qubit_range: range) -> str:
qasm = ""
for i in qubit_range:
if i == 1:
qasm += f"ccx ctrl[0], ctrl[1], aux[0];\n"
else:
qasm += f"ccx ctrl[{i}], aux[{i-2}], aux[{i-1}];\n"
return qasm
@qfunc
def v_chain_implementation(
controls: QReg[self.num_controls],
aux: AuxQReg[self.num_controls-1],
target: QReg[1]
) -> Tuple[QReg[self.num_controls], AuxQReg[self.num_controls-1], QReg[1]]:
qasm = QASM_INTRO + f"qreg ctrl[{self.num_controls}];\n"
qasm += f"qreg aux[{self.num_controls-1}];\n"
qasm += f"qreg target[1];\n"
qasm += _cascade_cnots(range(1, self.num_controls))
qasm += f"cx aux[{self.num_controls-2}], target;\n"
qasm += _cascade_cnots(range(self.num_controls-1, 0, -1))
return qasm
return v_chain_implementation
Attention
The super().__init__(*args, **kwargs)
call in the user-defined MyMCX
class must come after the initialization of all the
custom parameters used in the definition
property, since the implementation of QuantumFunctionFactory
relies on this
data.
Note that the above code can be placed in a .qfunc
file, which acts as a standard .py
Python source file,
but helps to distinguish quantum function definitions from standard Python code.
Files with the .qfunc
file extension are recognized as standard Python modules
when importing the classiq
package (or anything from it) for the first time.
When importing .qfunc
files, the name of the file without the extension should be used in the import
statement
(as in standard Python modules).
Using the .qfunc
file extension is not mandatory.
As seen in the code above, the abstract property definition
of the QuantumFunctionFactory
abstract base class is overridden,
returning a QuantumFunction
object based on the parameters of the function.
In this case, the implementation is the standard V-Chain implementation of MCX (with \(n-1\) auxiliary qubits).
Following the definition, the user can add the class to the library (once) and use it as many times as necessary,
each time instantiating with different parameters:
from classiq import FunctionLibrary, Model
from my_mcx import MyMCX
function_library = FunctionLibrary(MyMCX, name="my_library")
model = Model()
model.include_library(library=function_library)
model.MyMCX(num_controls=6)()
model.MyMCX(num_controls=3)()
circuit = model.synthesize()
circuit.show_interactive()
[1] A. Cuccaro et al, A new quantum ripple-carry addition circuit, https://arxiv.org/abs/quant-ph/0410184 (2004)