Skip to content

Advanced Use

Multiple Implementations

To fully utilize the strength of the Classiq engine, you can add multiple different implementations for the same function.

The first example is two implementations of a controlled-Z gate.

Example: Controlled-Z Gate with Two Implementations

{
  "functions": [
    {
      "name": "main",
      "body": []
    },
    {
      "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 chooses the optimal implementation based on the circuit constraints and optimization criteria.

Auxiliaries and Uncomputation

You can define functions to use auxiliary qubits that are neither input nor outputs of the function.

NOTE: All auxiliary_registers are assumed initialized as zero and returned to zero at the end of the function. If registers are not returned to zero at the end of the computation, use the zero_input_registers field to declare them.

The following code introduces a simple ripple adder [1] as a function.

Example: Ripple Adder

{
  "functions": [
    {
      "name": "main",
      "body": [{
          "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"
          }
      }]
    },
    {
      "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]
          }]
      }
    }
  ]
}
from classiq import (
    ForeignFunctionDefinition,
    FunctionImplementation,
    RegisterMappingData,
    Register,
    Model,
    FunctionLibrary,
    synthesize,
    show,
)


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

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"],
    },
)

quantum_program = synthesize(model.get_model())
show(quantum_program)

The output circuit is shown here at the functional level.

 Ripple adder example

This ripple adder requires one auxiliary qubit and returns it to zero at the end of the computation [1] .

Return all auxiliary_registers in functions to zero at the end of the computation.

Parametric Functions and the .qfunc Extension

You may need 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. It is possible to implement such functions with the previously defined APIs by adding each specific instance of the parameter set as a separate function to the library.

In the Python SDK, define parametric functions more robustly using the QuantumFunctionFactory class. Following is an example of an MCX gate, with the number of controls as a parameter of the function.

my_mcx.qfunc
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) -> None:
        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, use the name of the file without the extension 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, you can add the class to the library (once) and use it as often as necessary, each time instantiating with different parameters.

from classiq import FunctionLibrary, Model, synthesize, show
from my_mcx import MyMCX

function_library = FunctionLibrary(MyMCX)

model = Model()
model.include_library(library=function_library)

model.MyMCX(num_controls=6)()
model.MyMCX(num_controls=3)()

quantum_program = synthesize(model.get_model())
show(quantum_program)

[1] A. Cuccaro et al, A new quantum ripple-carry addition circuit, https://arxiv.org/abs/quant-ph/0410184 (2004).