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)
../_images/776869489f3ab668d812c6dcedb403830864dccd0e5fcf79d0438192189fb903.png

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.
../_images/1f3701c8a8d53b793f475d7aee5e6db9ee9ca70c98b7fc94ae64a31a5e3dc464.png

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)
../_images/3fe18a064da5b427bafb5d6be13688eb9effb6ed2a752d5773357c436ca2a8f4.png

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_circuit will have an additional 2 qubits.