Adding new blocks#
In this tutorial you will learn how to create your own circuit blocks by extending the CircuitBlock class.
Table of contents#
#Necessary imports
import matplotlib.pyplot as plt
from qiskit import QuantumCircuit
from qailab.circuit import build_circuit
from qailab.circuit.base import CircuitBlock, EncodingBlock, NonGateBlock
#Utility function
def show_circuit(circ):
    fig = plt.figure(figsize=(12,8))
    ax1,ax2 = fig.add_subplot(1,2,1),fig.add_subplot(1,2,2)
    ax1.set_title('Block form')
    ax2.set_title('Decomposed')
    circ.draw('mpl',ax=ax1)
    circ.decompose().draw('mpl',ax=ax2)
    
    plt.show()
Non-parameterized blocks#
The simplest kind of circuit block. Simply subclass CircuitBlock and modify the _build_circuit function. You can also modify __init__() to give a custom name to your block (visible when drawing the circuit for example).
from qiskit import QuantumCircuit
class MyCustomBlock(CircuitBlock):
    """
    Adds a hadamard gate on every even qubit.
    """
    def __init__(self) -> None:
        super().__init__('MyCustomName')
        
    def _build_circuit(self, num_qubits: int) -> QuantumCircuit:
        circuit = QuantumCircuit(num_qubits)
        for i in range(num_qubits):
            if i % 2 == 0:
                circuit.h(i)
        return circuit
circ =  build_circuit(3,[MyCustomBlock()])
show_circuit(circ)
 
Parameterized blocks (encoding blocks)#
Parameterized blocks allow you to encode either the input vector or weights into the quantum circuit.
The procedure for adding your own is largely the same, however EncoderBlock provides a convenience method _create_parameters() for creating uniquely named lists of parameters for you to use.
from typing import Literal
class MyCustomEncodingBlock(EncodingBlock):
    """
    Adds an Rx then an Ry gate to every qubit
    """
    def __init__(self, block_type: Literal['input','weight'] = 'input') -> None:
        super().__init__("MyCustomNameDos", block_type)
        
    def _build_circuit(self, num_qubits: int) -> QuantumCircuit:
        if self._parameters is None:
            self._parameters = self._create_parameters(2*num_qubits) #Initialize 2*num_qubits new parameters
        circuit = QuantumCircuit(num_qubits)
        for i in range(num_qubits):
            circuit.rx(self._parameters[2*i],i)
            circuit.ry(self._parameters[2*i+1],i)
            
        return circuit
    
circ =  build_circuit(3,[MyCustomEncodingBlock()])
show_circuit(circ.assign_parameters(list(range(len(circ.parameters))))) # Assign sample parameters, as without them draw() puts the entire name in the graph.
 
Non-gate blocks#
Some quantum circuits cannot be converted to gates (e.g circuits with measurements).
If you create a block that cannot be converted to a gate, you can subclass NonGateBlock, which will change the way in which the block is added to a circuit.
The drawback of this solution is that if the circuit has a lot of other gates, which can together be converted to a single gate, they will remain in their decomposed form.
class MyCustomMeasurementBlock(CircuitBlock):
    """
    Adds a hadamard gate to every qubit, then measures only odd qubits.
    """
    def __init__(self) -> None:
        super().__init__('MyCustomNameTres')
        
    def _build_circuit(self, num_qubits: int) -> QuantumCircuit:
        circuit = QuantumCircuit(num_qubits,num_qubits//2) # Add classical bits for measurements
        clbit = 0
        for i in range(num_qubits):
            circuit.h(i)
            if i % 2 == 1:
                circuit.measure(i,clbit)
                clbit += 1
                
        return circuit
print("This will throw an error!")
try:
    circ = build_circuit(3,[MyCustomMeasurementBlock()],measure_qubits=[])
except Exception as e:
    print(f"!ERROR! {e}")
This will throw an error!
!ERROR! 'Circuit with classical bits cannot be converted to gate.'
class MyCustomMeasurementBlock(NonGateBlock,CircuitBlock):
    """
    Adds a hadamard gate to every qubit, then measures only odd qubits.
    """
    def __init__(self) -> None:
        super().__init__('MyCustomNameTres')
        
    def _build_circuit(self, num_qubits: int) -> QuantumCircuit:
        circuit = QuantumCircuit(num_qubits,num_qubits//2) # Add classical bits for measurements
        clbit = 0
        for i in range(num_qubits):
            circuit.h(i)
            if i % 2 == 1:
                circuit.measure(i,clbit)
                clbit += 1
                
        return circuit
circ = build_circuit(3,[MyCustomMeasurementBlock()],measure_qubits=[])
show_circuit(circ)
 
Additional remarks#
- Circuits created by your block can have more qubits than - num_qubits. These additional qubits are treated as purely auxiliary and won’t be measured by default.
- If two blocks create one auxiliary qubit each, the final circuit produced by - build_circuitwill have an additional 2 qubits.