Quantum Communication

Quantum Communications Scheme

Quantum Communications Scheme

This is the one paradigm that does not have a classical counterpart, since it is purely quantum distribution.

In this scheme quantum communication protocols such as teledata and telegate can be use to share quantum entanglement among the states in each QPU involved.

How to deploy

To lauch a set of vQPUs incorporating quantum communications among them, you must use the flag --quantum_comm when deploying.

qraise -n <num qpus> -t <max time> --quantum_comm [OTHER]
family = qraise(4, <max time>, quantum_comm=True, [OTHER])

The above command line launches vQPUs with all-to-all quantum communications connectivity. For additional options in the Bash command checkout qraise, and check qraise for the Python function. Again, it is recomended to use the --co-located flag (and co_located attribute in Python), as it allows to access the vQPUs from every node, not just the one the vQPUs are being set up. In this documentation we are going to consider that this flag is set.

Circuits design

CUNQA allows the implementation of teledata and telegate protocols among circuits. These protocols are provided as functions in the cunqa.qc_protocols subpackage, and are built on top of the low-level gen_ent entanglement-generation primitive. In order to use them, and in a similar manner than with the classical communications model, several steps must be followed.

  1. Creating both circuits, reserving the communication qubits they will need. The comm qubits are declared with the tuple form CunqaCircuit((num_data_qubits, num_comm_qubits), num_clbits, ...), and the get_qubits method returns the data and comm qubit indices:

circuit_1 = CunqaCircuit((2, 1), num_clbits=2, id="circuit_1")
circuit_1.h(0)

circuit_2 = CunqaCircuit((2, 1), num_clbits=2, id="circuit_2")
  1. Implementing the quantum communication:

This protocol is similar to the classical communication directive but instead of sending a bit, here we teleport a quantum state. Even though in CUNQA we implement it in a friendlier way, the behind of scene of the protocol looks like the following:

Teledata protocol

where the state a at the first QPU is teleported to the first qubit of the second QPU. Here we see that a requirement is for the QPUs to share a Bell pair.

At circuit level whithin CUNQA, the functions qsend and qrecv are designed for this purpose:

from cunqa.qc_protocols import qsend, qrecv

data_qubits_1, comm_qubits_1 = circuit_1.get_qubits()
data_qubits_2, comm_qubits_2 = circuit_2.get_qubits()

# qsend(circuit, data_qubit, comm_qubit, clbits, recving_circuit, tag)
qsend(circuit_1, data_qubits_1[0], comm_qubits_1[0], [0, 1], recving_circuit="circuit_2", tag="teledata")

# qrecv(circuit, data_qubit, comm_qubit, clbits, control_circuit, tag)
qrecv(circuit_2, data_qubits_2[0], comm_qubits_2[0], [0, 1], control_circuit="circuit_1", tag="teledata")

Here the state of data qubit 0 at circuit_1 is teleported to data qubit 0 at circuit_2, using the comm qubit 0 of each circuit and the classical bits [0, 1] to carry the Bell-measurement corrections. Then, we can continue to add instructions to both circuits.

The matching tag links both calls. Without tags, the operation would be the same, except when multiple calls to qsend and qrecv are made by the same vQPUs. In this case, without tags, qubits would be sent and received sequentially, not based on tag matching.

This protocol consists in sharing a control qubit from one QPU to another (or several) for performing multiple-qubit gates across QPUs. The protocol itself consists in the following:

Telegate protocol

Here the quantum state a at the first QPU is participating in local operations at the other QPU. We notice that a requirement is for the QPUs to share a Bell pair.

Its circuit implementation at CUNQA opens a telegate block with cat_entangler, which shares the control qubit onto the comm qubits of the other circuits; the receiving circuit then applies the controlled gate locally on its comm qubit, and the block is closed with cat_disentangler:

from cunqa.qc_protocols import cat_entangler, cat_disentangler

data_qubits_1, comm_qubits_1 = circuit_1.get_qubits()
data_qubits_2, comm_qubits_2 = circuit_2.get_qubits()

cat_entangler(
    [circuit_1, circuit_2],
    data_qubits_1[0],
    [comm_qubits_1[0], comm_qubits_2[0]],
    [0, 0],
    tag="telegate"
)

circuit_2.cx(comm_qubits_2[0], data_qubits_2[0])

cat_disentangler(
    [circuit_1, circuit_2],
    data_qubits_1[0],
    [comm_qubits_2[0]],
    [0, 1],
    [0, 0]
)

This way, qubit 0 at circuit_1 acts as the control of the cx two-qubit gate applied together with the data qubit at circuit_2.

Execution

We obtain the QPU objects associated to the displayed vQPUs through get_QPUs, same as with the other methods. It is important that those allow quantum communications, otherwise an error will be raised. When quantum communications are available, classical-communication directives are also permited at circuits.

For the distribution, function run is used. By providing the list of circuits and the list of QPU objects we allow their mapping to the corresponding vQPUs:

qpus_list = get_QPUs(family=family, co_located=True)

distributed_qjobs = run([circuit_1, circuit_2], qpus_list, shots = 1024)

We can call for the results by the gather function, passing the list of QJob objects:

results = gather(distributed_qjobs)

For quantum communications simulation, the vQPUs deployed actually share a conjunt simulator, therefore the call for results is a blocking call that waits for the whole simulation to be over. Simulation time and output statistics can be accessed by

times_list = [result.time_taken for result in results]

counts_list = [result.counts for result in results]

Basic example

Here we show two examples of implementing the remote construction of a three-qubit entangled state by teledata and telegate. Further examples and use cases are listed in Further examples.

import os, sys
# In order to import cunqa, we append to the search path the cunqa installation path.
# In CESGA, we install by default on the $HOME path as $HOME/bin is in the PATH variable
sys.path.append(os.getenv("HOME"))

from cunqa.qpu import get_QPUs, qraise, qdrop, run
from cunqa.circuit import CunqaCircuit
from cunqa.qjob import gather
from cunqa.qc_protocols import qsend, qrecv

# 1. QPU deployment

family = qraise(2, "01:00:00", quantum_comm=True, family="qpus_quantum_comms")
qpus = get_QPUs(family=family)

# 2. Circuit design (each circuit reserves 2 data + 1 comm qubits)

circuit_1 = CunqaCircuit((2, 1), 2, id="circuit_1")
circuit_2 = CunqaCircuit((2, 1), 2, id="circuit_2")

data_qubits_1, comm_qubits_1 = circuit_1.get_qubits()
data_qubits_2, comm_qubits_2 = circuit_2.get_qubits()

circuit_1.h(0)
circuit_1.cx(0, 1)

# -------------- Teledata! ------------
qsend(circuit_1, data_qubits_1[0], comm_qubits_1[0], [0, 1], recving_circuit="circuit_2", tag="teledata")
qrecv(circuit_2, data_qubits_2[0], comm_qubits_2[0], [0, 1], control_circuit="circuit_1", tag="teledata")
# -------------------------------------

circuit_2.cx(0, 1)

circuit_1.measure_all()
circuit_2.measure_all()

# 3. Execution

qjobs = run([circuit_1, circuit_2], qpus, shots=1000)
results = gather(qjobs)
counts_list = [result.counts for result in results]

for counts, qpu in zip(counts_list, qpus):

    print(f"Counts from vQPU {qpu.id}: {counts}")

qdrop(family)
import os, sys
# In order to import cunqa, we append to the search path the cunqa installation path.
# In CESGA, we install by default on the $HOME path as $HOME/bin is in the PATH variable
sys.path.append(os.getenv("HOME"))

from cunqa.qpu import get_QPUs, qraise, qdrop, run
from cunqa.circuit import CunqaCircuit
from cunqa.qjob import gather
from cunqa.qc_protocols import cat_entangler, cat_disentangler

# 1. QPU deployment

family = qraise(2, "01:00:00", quantum_comm=True, family="qpus_quantum_comms")
qpus = get_QPUs(family=family)

# 2. Circuit design (each circuit reserves 1 data + 1 comm qubit)

circuit_1 = CunqaCircuit((1, 1), 2, id="circuit_1")
circuit_2 = CunqaCircuit((1, 1), 2, id="circuit_2")

data_qubits_1, comm_qubits_1 = circuit_1.get_qubits()
data_qubits_2, comm_qubits_2 = circuit_2.get_qubits()

circuit_1.h(data_qubits_1[0])

# -------------------------- Telegate! -------------------------
cat_entangler(
    [circuit_1, circuit_2],
    data_qubits_1[0],
    [comm_qubits_1[0], comm_qubits_2[0]],
    [0, 0],
    tag="telegate"
)

circuit_2.cx(comm_qubits_2[0], data_qubits_2[0])

cat_disentangler(
    [circuit_1, circuit_2],
    data_qubits_1[0],
    [comm_qubits_2[0]],
    [0, 1],
    [0, 0]
)
# --------------------------------------------------------------

circuit_1.measure(data_qubits_1[0], 0)
circuit_2.measure(data_qubits_2[0], 0)

# 3. Execution

qjobs = run([circuit_1, circuit_2], qpus, shots=1000)
results = gather(qjobs)
counts_list = [result.counts for result in results]

for counts, qpu in zip(counts_list, qpus):

    print(f"Counts from vQPU {qpu.id}: {counts}")

qdrop(family)