Quantum-communications scheme

The quantum-communications directives, together with simple examples, are explained in the Quantum Communication section. This section will present a couple of examples are presented to highlight the use of quantum communications.

In this first example we present an example of distributed QPE, so that the results obtained can be compared with the ones in No-communications scheme and Classical-communications scheme.

"""
Code implementing a simple distribution variant of the QPE algorithm. 
We use the telegate protocol to apply gates on a circuit that has the eigenvector from a circuit where the bits of the phase will be measured.
"""

import os, sys
import numpy as np

# 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.circuit import CunqaCircuit
from cunqa.qjob import gather

N_QPUS = 2
CORES_PER_QPU = 4
MEM_PER_QPU = 60 # in GB
N_ANCILLA_QUBITS = 10
N_REGISTER_QUBITS = 1
PHASE_TO_COMPUTE = 1 / 2**3

shots = 100
SEED = 18

try:
    # 1. Deploy vQPUs
    family = qraise(N_QPUS, "00:10:00", 
                    simulator="Aer",
                    quantum_comm = True, 
                    co_located = True, 
                    cores = CORES_PER_QPU, 
                    mem_per_qpu = MEM_PER_QPU)
except Exception as error:
    raise error

try:
    qpus = get_QPUs(co_located = True, family = family)

    # 2. Design circuits modelling the QPE 
    ancilla_circuit  = CunqaCircuit(N_ANCILLA_QUBITS, id = "ancilla_circuit")
    register_circuit = CunqaCircuit(N_REGISTER_QUBITS, id = "register_circuit")

    register_circuit.x(0) # Rz statevector

    for i in range(N_ANCILLA_QUBITS):
        ancilla_circuit.h(i)

    for i in range(N_ANCILLA_QUBITS):
        ### TELEGATE ###
        with ancilla_circuit.expose(N_ANCILLA_QUBITS - 1 - i, register_circuit) as (rqubit, subcircuit):
            param = (2**i) * 2 * 2 * np.pi * PHASE_TO_COMPUTE
            subcircuit.crz(param, rqubit, 0)

    # Swap qubits
    if (N_ANCILLA_QUBITS % 2) == 0:
        swap_range = int(N_ANCILLA_QUBITS / 2)
    else:
        swap_range = int((N_ANCILLA_QUBITS - 1) / 2)

    for i in range(swap_range):
        ancilla_circuit.swap(i, N_ANCILLA_QUBITS - 1 - i)

    # QFT dagger
    for i in range(N_ANCILLA_QUBITS):
        for j in range(i):
            angle  = (-np.pi) / (2**(i - j)) 
            ancilla_circuit.crz(angle, N_ANCILLA_QUBITS - 1 - j, N_ANCILLA_QUBITS - 1 - i)
        ancilla_circuit.h(N_ANCILLA_QUBITS - 1 - i)

    ancilla_circuit.measure_all()


    # 3. Execute distributed QPE circuit on communicated QPUs
    distr_jobs = run([ancilla_circuit, register_circuit], qpus, shots=shots, seed=SEED)
    result_list = gather(distr_jobs)
    

    # 4. Post-processing results to extract estimated phase 
    counts = result_list[0].counts
    print(f"Counts: {counts}")

    most_frequent_output = max(counts, key=counts.get)
    print(f"Most frequent output is {most_frequent_output}")

    estimated_theta = 0.0
    for i, digit in enumerate(most_frequent_output):
        if digit == '1':
            estimated_theta += 1 / (2 ** (N_ANCILLA_QUBITS - i))

    
    print(f"Estimated angle: {estimated_theta}")
    print(f"Real angle: {PHASE_TO_COMPUTE}")

    # 5. Drop the deployed QPUs 
    qdrop(family)
except Exception as error:
    # 5. Release resources even if an error is raised
    qdrop(family)
    raise error