Classical Communications
Classical Communications scheme
This model extends the embarrassingly parallel approach by allowing processes to exchange classical information during execution, similarly to how MPI programs operate in classical HPC environments.
In the context of quantum circuits, this communication is achieved by transmitting classical bits
obtained from measurement outcomes. This enables the use of received bits to perform classically
conditioned operations. In other words, if the value of a remote bit is 1, specific quantum
operations are applied to the designated local qubits.
How to deploy
To lauch a set of vQPUs incorporating classical communications among them, you must use the flag
--classical-comm when deploying.
qraise -n <num qpus> -t <max time> --classical-comm [OTHER]
family = qraise(4, <max time>, classical_comm=True, [OTHER])
The above command line launches vQPUs with all-to-all classical 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
In order to classically condition local operations with remote bits, we must first implement the
communication of such bits. With this purpose, CunqaCircuit class
incorporates the send and
recv class methods; and these steps must be followed:
Creating both circuits and adding the desired operations on them:
circuit_1 = CunqaCircuit(num_qubits=2, num_clbits=2, id="circuit_1") circuit_1.h(0) circuit_2 = CunqaCircuit(num_qubits=2, num_clbits=2, id="circuit_2") circuit_2.x(1)If no id is not provided, it will be generated and accesed by the class attribute
id.
Measuring and sending a classical bit from
circuit_1and receiving it atcircuit_2:circuit_1.measure(qubit=0, clbit=0) circuit_1.send(clbits=0, recving_circuit="circuit_2") circuit_2.recv(clbits=1, sending_circuit="circuit_1")At
circuit_2we receive the bit and store it at possition1of the local classical register.3. Classically controlling a quantum operation employing the
cifmethod:with circuit_2.cif(clbits=1) as subcircuit: subcircuit.x(0)We use the outcome stored at clbit
1to classically control axgate at qubit0.
Execution
We obtain the QPU objects associated to the displayed vQPUs with
get_QPUs, as a common step between the three models. It is important that
those allow classical communications—i.e., that the aforementioned classical-comm flag was
set—, otherwise an error will be raised.
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)
This is a blocking call, since here the function waits both executions to be done. Simulation times 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 an example on a classical communication performed from one circuit to another to classically control a quantum operation. 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
# 1. QPU deployment
# If GPU execution is desired, just add "gpu = True" as another qraise argument
family_name = qraise(2, "01:00:00", classical_comm=True, co_located=True, family = "qpus_class_comms")
qpus = get_QPUs(family = family_name, co_located=True)
# 2. Circuit design with classical communications directives
circuit_1 = CunqaCircuit(2, 2, id="circuit_1")
circuit_2 = CunqaCircuit(1, 1, id="circuit_2")
circuit_1.h(0)
circuit_1.measure(0,0)
circuit_1.send(0, recving_circuit = "circuit_2")
circuit_1.measure(1,1)
circuit_2.recv(0, sending_circuit = "circuit_1")
with circuit_2.cif(0) as subcircuit:
subcircuit.x(0)
circuit_2.measure(0,0)
# 3. Execution
distributed_qjobs = run([circuit_1, circuit_2], qpus, shots=1000)
results = gather(distributed_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}")
# 4. Release classical resources
qdrop(family_name)