Tools for DQC algorithms

See the following explanations of the add, union and hsplit functions to understand how they empower the study of DQC algorithms.

The function add takes an iterable of circuits as input and returns a circuit that has the instructions of each circuit of the iterable in order. Its purpose is to build circuits modularly from simple parts.

Warning

As communications greatly depend on the order of execution, adding circuits does not mesh well with communications. In particular, if two circuits that communicate with eachother were added, execution would stall as the circuit would wait to communicate with the next subcircuit, which wouldn’t respond until execution progressed, waiting indefinitely.

For this reason, if two circuits on the iterable contain comunications between them, an exception would be raised.

The add function can be used to avoid redundant code, for example when creating a circuit for the Grover algorithm:

grover_circuit = add([oracle, diffusor] * repeat_times)

Full example of the add function:

import os, sys
# In order to import cunqa, we append to the search path the cunqa installation path
sys.path.append(os.getenv("HOME")) # HOME as install path is specific to CESGA

from cunqa.qpu import get_QPUs, qraise, qdrop, run
from cunqa.qjob import gather
from cunqa.circuit import CunqaCircuit
from cunqa.circuit.transformations import add
from pprint import pprint

# ---------------------------
# Acquiring the resources
# ---------------------------
family = qraise(1, "00:10:00", simulator="Aer", co_located=True)
[qpu] = get_QPUs(co_located=True, family=family)

# ---------------------------
# Original circuits are:
#
#    | circuit1 | circuit2 |
# q0:|   ─[H]─  |   ──●──  |
#    |          |     |    |
# q1:|          |   ─[X]─  |
# 
# And, then, added_circuit is a Bell pair:
#
# added_circuit.q0: ─[H]──●──[M]─
#                         |
# added_circuit.q1: ─────[X]─[M]─
# ---------------------------
circuit1 = CunqaCircuit(1, id = "circuit1") # adding ancilla
circuit1.h(0)

circuit2 = CunqaCircuit(2, id = "circuit2")
circuit2.cx(0,1)

added_circuit = add([circuit1, circuit2])
added_circuit.measure_all()

qjob = run(added_circuit, qpu, shots = 1024)# non-blocking call
results = qjob.result

print(f"\nResult after addition: {results.counts}\n")

# ---------------------------
# Relinquishing resources
# ---------------------------
qdrop(family)

Given two circuits with n and m qubits, their union returns a circuit with n + m qubits, where the operations of the former are applied to the first n qubits and those of the latter are applied to the last m qubits. If originally there were distributed instructions between the circuits, they would be replaced by local ones. In the following example we observe the union of two simple circuits.

c1 = CunqaCircuit(2, id="circuit1")
c1.h(0)
c1.cx(0,1)

c2 = CunqaCircuit(1, id="circuit2")
c2.x(0)

union_circuit = union([c1, c2])

Full example of the union function:

import os, sys
# In order to import cunqa, we append to the search path the cunqa installation path
sys.path.append(os.getenv("HOME")) # HOME as install path is specific to CESGA

from cunqa.qpu import get_QPUs, qraise, qdrop, run
from cunqa.qjob import gather
from cunqa.circuit import CunqaCircuit
from cunqa.circuit.transformations import union

# ---------------------------
# Acquiring resources
# ---------------------------
family_separated = qraise(2, "00:10:00", simulator="Aer", quantum_comm=True, co_located=True)
qpus_separated = get_QPUs(co_located=True, family=family_separated)

family_union = qraise(1, "00:10:00", simulator="Aer", co_located=True)
[qpu_union]  = get_QPUs(co_located=True, family=family_union)

# ---------------------------
# Original circuits created and executed
# circuit1.q0: ─[H]──●──[M]─
#                    $
# circuit2.q0: ─────[X]─[M]─
# Where $ represents the remote control of the gate
# ---------------------------
circuit1 = CunqaCircuit(1, id="circuit1") # adding ancilla
circuit1.h(0)

circuit2 = CunqaCircuit(1, id="circuit2")

with circuit1.expose(0, circuit2) as (rqubit, subcircuit):
    subcircuit.cx(rqubit,0)

circuit1.measure_all()
circuit2.measure_all()

qjobs = run([circuit1, circuit2], qpus_separated, shots=1024)
results = gather(qjobs)

print(f"\nResult before union: {results[0].counts}") # taking only the results from the 0 circuit
                                                     # because they both get the same due to 
                                                     # quantum communications

# ---------------------------
# Take the union of the circuits and execute it
# union_circuit.q0: ─[H]──●──[M]─
#                         |
# union_circuit.q1: ─────[X]─[M]─
# ---------------------------
union_circuit = union([circuit1, circuit2])

qjob = run(union_circuit, qpu_union, shots=1024)# non-blocking call
results = qjob.result

print(f"Result after union: {results.counts}\n")

# ---------------------------
# Relinquishing resources
# ---------------------------
qdrop(family_union)
qdrop(family_separated)

The function hsplit divides the set of qubits of a circuit into subcircuits, preserving the instructions and substituing local 2-qubit gates by distributed gates if they involve qubits from different subcircuits. The name hspit stands for horizontal split, as in the conventional way to visually represent a circuit one would have to draw a horizontal line to separate the qubits of the circuit in two subsets.

To divide a circuit circuit_to_divide, one should provide an additional argument that determines how the circuits should be divided. This argument can be a list with the number of qubits for each subcircuit (the lenght of the list determines the number of subcircuits), or an int specifying the number of subcircuits, which would get an equal number of qubits except possibly the last one, which would get the remainder if the number of qubits is not cleanly divided by the int provided.

Basic syntax:

# List with the number of qubits per subcircuit
[c1, c2] = hsplit(circuit_to_divide, [2, 7])

# Int specifying the number of resulting subcircuits
[c1, c2] = hsplit(circuit_to_divide, 2)

In particular, it could be checked that union and hsplit are inverses of eachother:

# New circuit equivalent to circ is returned
union(hsplit(circ, 2))
# New circuits equal to circ1 and circ2 are returned
hsplit(union(circ1, circ2), [circ1.num_qubits, circ2.num_qubits])

Full example of the hsplit function:

import os, sys
# In order to import cunqa, we append to the search path the cunqa installation path
sys.path.append(os.getenv("HOME")) # HOME as install path is specific to CESGA

from cunqa.qpu import get_QPUs, qraise, qdrop, run
from cunqa.qjob import gather
from cunqa.circuit import CunqaCircuit
from cunqa.circuit.transformations import hsplit

# ---------------------------
# Acquiring resources
# ---------------------------
family_original = qraise(1, "00:10:00", simulator="Aer", co_located=True)
[qpu_original]  = get_QPUs(co_located=True, family=family_original)

family_separated = qraise(2, "00:10:00", simulator="Aer", quantum_comm=True, co_located=True)
qpus_separated = get_QPUs(co_located=True, family=family_separated)

# ---------------------------
# Original circuit
# union_circuit.q0: ─[H]──●──[M]─
#                         |
# union_circuit.q1: ─────[X]─[M]─
# ---------------------------
circuit = CunqaCircuit(2, id="circuit")
circuit.h(0)
circuit.cx(0, 1)
circuit.measure_all()

qjob = run(circuit, qpu_original, shots=1024) # non-blocking call
results = qjob.result

print(f"\nResult before hsplit: {results.counts}")

# ---------------------------
# Split original circuit to create two communicated circuits, and execute them
# circuit1.q0: ─[H]──●──[M]─
#                    $
# circuit2.q0: ─────[X]─[M]─
# Where $ represents the remote control of the gate
# ---------------------------
[circuit1, circuit2] = hsplit(circuit, 2)

qjobs = run([circuit1, circuit2], qpus_separated, shots=1024)
results = gather(qjobs)

print(f"Result after split: {results[0].counts}\n") # taking only the results from the 0 circuit
                                                    # as both are the same, due to entanglement

# ---------------------------
# Relinquishing resources
# ---------------------------
qdrop(family_original)
qdrop(family_separated)