Distributed Quantum Phase Estimation Algorithm

Quantum phase estimation is a quantum algorithm which is used to estimate the phase (or eigenvalue) of an eigenvector of a unitary operator. If we consider a unitary matrix \(U\) and a quantum state \(|\psi \rangle\) such that \(U|\psi \rangle =e^{2\pi i\theta }\), the algorithm estimates the value of \(\theta\) with high probability within additive error \(\varepsilon\).

The below is the circuit diagram for quantum phase estimation with the Unitary \(U\) and eigenstate \(|\psi\rangle\).

alt text

Here, we will explain the execution of distributed quantum phase estimation algorithm with two computing hosts and one controller host using Interlin-q. We let the first computing host QPU_1 possess all the qubits which are set in the state \(|0\rangle\) from the above image and the second computing host QPU_2 would possess the single qubit which is set in the eigenstate \(|\psi\rangle\).

Introduction of Interlin-q

Simulated Architechture

The simulated architechture of Interlin-q can be seen in the image below:

image1

The controller host gets the monolithic circuit as an input from the client, converts it to a distributed circuit and generates schedules from it for individual computing hosts and broadcasts it to them. The controller host and all the computing hosts share a global clock. When the computing hosts receive the broadcast, they extract their schedule from it and perform the operations in the schedules according to the global clock and the timestamps on the operations. The final results are sent over to the controller host from all the computing hosts for further processing.

Conversion of monolithic to distributed circuit

The monolithic circuit is a circuit which is designed to be implemented by a single quantum computer. To implement this same algorithm using multiple interlinked quantum computers, this monolithic circuit needs to be converted to a distributed circuit over a given topology of the interlinked quantum computers.

This distribution is performed by the controller host, by analysing the input monolithic circuit and searching for any control gates where the control and target qubits lie in separate computing host. If any such control gate is found, it is replaced by using a Cat-circuit shown in the image below. The controller host generates the distributed circuit by replacing all such control gates.

image2

Here the quantum state \(|\psi_1\rangle\) lies with the first computing host and the quantum state \(|\psi_2\rangle\) lies with the second computing host. A control gate with \(|\psi_1\rangle\) as control qubit and \(|\psi_2\rangle\) as the target qubit is replaced with the circuit shown, which implement a non-local control gate. It is necessary for both the computing hosts to share an EPR pair.

Distributed Scheduler and Broadcasting

The controller host forms a distributed schedule of operations according to timestamps for individual computing hosts using the distributed circuit. The operations are timestamped after considering the amount of time the operation would take to be performed on the specific computing hosts. These operations are then broadcasted to all the computing hosts.

Executing the distributed algorithm

The computings hosts receive the broadcasted schedules and extract their schedule from it. The controller hosts and all the computing hosts share a Clock object, which enable them to perform operations according to the schedule in synchronization. Once the operations are performed, and resulting error or successful measurements are reported back to the controller host, who further processes the output for the client.

Code implementation

Now we discuss how to implement distributed quantum phase estimation algorithm using Interlin-q

Step 1: Import libraries.

First we import all the necessary libraries. Interlin-q is build using the python framework QuNetSim which is a python software framework that can be used to simulate quantum networks up to the network layer.

[ ]:
import sys
import numpy as np
sys.path.append("../")

from qunetsim.components import Network
from qunetsim.objects import Logger

from interlinq import (ControllerHost, Constants, Clock,
Circuit, Layer, ComputingHost, Operation)

Logger.DISABLED = True

Step 2: Create monolithic circuit of Quantum Phase Estimation

Instructions on creating a circuit

The below are the functions used to create a monolithic circuit in Interlin-q framework, which will act as the client input. Circuit objects in Interlin-q are constructed from different Layer objects, where every layers contains Operation objects which are to be performed together.

1. Operations

Operation objects are commands which instruct the computing hosts regarding the operations to be executed. The different types of Operation objects are PREPARE_QUBITS, SINGLE, TWO_QUBIT, CLASSICAL_CTRL_GATE, MEASURE, SEND_ENT, REC_ENT, SEND_CLASSICAL and REC_CLASSICAL.

A sample operation to implement a CNOT gate across one computing host is shown below:

op = Operation(
    name=Constants.TWO_QUBIT,
    qids=[control_qubit_id, target_qubit_id],
    gate=Operation.CNOT,
    computing_host_ids=[computing_host_id])

A sample operation to implement a CNOT gate across two computing host is shown below:

op = Operation(
    name=Constants.TWO_QUBIT,
    qids=[control_qubit_id, target_qubit_id],
    gate=Operation.CNOT,
    computing_host_ids=[computing_host_id_1, computing_host_id_2])

2. Layers

Layers are composed of multiple operations which are to be performed across different computing hosts at the same time. A Layer object that would consist of Operation objects op1, op2 and op3 can be created using the command below:

layer_1 = Layer([op1, op2, op3])

3. Circuit

A Circuit object is composed of multiple layers of operations and we also provide the topology of the distributed circuit as an input. A Circuit object that would consist of Layer objects layer1, layer2 and layer3 can be created using the command below. In this command, q_map is the topology of the distributed circuit, which informs us regarding the computing hosts and the corresponding qubits that are involved in the circuit.

q_map = {
    'QPU_1': ['q_0_0', 'q_0_1', 'q_0_2', 'q_0_3'],
    'QPU_2': ['q_1_0']
    }

circuit = Circuit(q_map, [layer1, layer2, layer3])

Creating the circuit for Quantum Phase Estimation Algorithm

alt text

[7]:
def phase_gate(theta):
    return np.array([[1, 0], [0, np.exp(1j * theta)]])


def inverse_quantum_fourier_transform(q_ids, computing_host_ids, layers):
    """
    Performs inverse quantum fourier transform
    """

    q_ids.reverse()

    for i in range(len(q_ids)):
        target_qubit_id = q_ids[i]

        for j in range(i):
            control_qubit_id = q_ids[j]

            op = Operation(
                name=Constants.TWO_QUBIT,
                qids=[control_qubit_id, target_qubit_id],
                gate=Operation.CUSTOM_CONTROLLED,
                gate_param=phase_gate(-np.pi * (2 ** j) / (2 ** i)),
                computing_host_ids=[computing_host_ids[0]])
            layers.append(Layer([op]))

        op = Operation(
            name=Constants.SINGLE,
            qids=[target_qubit_id],
            gate=Operation.H,
            computing_host_ids=[computing_host_ids[0]])
        layers.append(Layer([op]))
    return layers
[ ]:
def quantum_phase_estimation_circuit(q_map, client_input_gate):
    """
    Returns the monolithic circuit for quantum phase estimation
    algorithm
    """
    layers = []
    computing_host_ids = list(q_map.keys())

    # Prepare the qubits on both computing hosts
    ops = []
    for host_id in computing_host_ids:
        op = Operation(
            name=Constants.PREPARE_QUBITS,
            qids=q_map[host_id],
            computing_host_ids=[host_id])
        ops.append(op)

    layers.append(Layer(ops))

    # Setup the qubits by apply Hadamard gates on qubits of QPU_1
    # and applying X gate to initialise qubit on QPU_2
    ops = []
    for q_id in q_map[computing_host_ids[0]]:
        op = Operation(
            name=Constants.SINGLE,
            qids=[q_id],
            gate=Operation.H,
            computing_host_ids=[computing_host_ids[0]])
        ops.append(op)

    op = Operation(
        name=Constants.SINGLE,
        qids=[q_map[computing_host_ids[1]][0]],
        gate=Operation.X,
        computing_host_ids=[computing_host_ids[1]])
    ops.append(op)

    layers.append(Layer(ops))

    # Apply controlled unitaries
    for i in range(len(q_map[computing_host_ids[0]])):
        max_iter = 2 ** i
        control_qubit_id = q_map[computing_host_ids[0]][i]
        target_qubit_id = q_map[computing_host_ids[1]][0]

        for _ in range(max_iter):
            op = Operation(
                name=Constants.TWO_QUBIT,
                qids=[control_qubit_id, target_qubit_id],
                gate=Operation.CUSTOM_CONTROLLED,
                gate_param=client_input_gate,
                computing_host_ids=computing_host_ids)
            layers.append(Layer([op]))

    # Inverse Fourier Transform circuit
    q_ids = q_map[computing_host_ids[0]].copy()
    layers = inverse_quantum_fourier_transform(
        q_ids,
        computing_host_ids,
        layers)

    # Measure the qubits
    ops = []
    for q_id in q_ids:
        op = Operation(
            name=Constants.MEASURE,
            qids=[q_id],
            cids=[q_id],
            computing_host_ids=[computing_host_ids[0]])
        ops.append(op)
    layers.append(Layer(ops))

    circuit = Circuit(q_map, layers)
    return circuit

Step 3: Define Standard protocols for controller and computing hosts.

Here, we defined the standard protocols for the controller and the computing hosts. The controller host gets the monolithic circuit as an input from the client, converts it to a distributed circuit and generates schedules from it for individual computing hosts and broadcasts it to them. The controller host and all the computing hosts share a global clock. When the computing hosts receive the broadcast, they extract their schedule from it and perform it according to the global clock. The final results are sent over to the controller host from all the computing hosts for further processing.

Controller host

For the controller host, the below is the list of functions that are involved in the protocol:

  • host.generate_and_send_schedules: Here we provide the monolithic Circuit object as the input and this function carries the task of created a distributed circuit from the monolithic circuit, creating a distributed schedule for all the computing hosts involved and broadcasting the schedule to all the computing hosts connected to this controller host.

  • host.receive_results: Here, the controller host waits for the algorithm to be completed on all the computing hosts’s end and then receives the final result from them, which could either be a reported error or successful measurement result.

Depending on the algorithm, the results received from the computing host can be further processed to provide a final output. In the case of distributed quantum phase estimation algorithm, the final measurement results from the two computing hosts are processed to calculate the phase of the unitary gate.

Computing host

For the computing host, the below is the list of functions that are involved in the protocol:

  • host.receive_schedule: Here the computing hosts receive the broadcasted schedule from the controller host, extract their individual schedule and starting perform the operations according to the schedule and the shared Clock object.

  • host.send_results: Here, the computing hosts share the final result with the controller host, which could either be a reported error or successful measurement result.

[4]:
def controller_host_protocol(host, q_map, client_input_gate, monolithic_circuit):
    """
    Protocol for the controller host
    """

    host.generate_and_send_schedules(monolithic_circuit)
    host.receive_results()

    results = host.results
    computing_host_ids = host.computing_host_ids

    print('Final results: \n')

    # This is the final processing perfomed in the controller host where
    # we take the final measurement results from the computing hosts and
    # calculate the estimated value of the phase from those results.
    decimal_value = 0
    for computing_host_id in computing_host_ids:
        i = 0
        bits = results[computing_host_id]['bits']
        for bit_id, bit in bits.items():
            print("{0}: {1}".format(bit_id, bit))
            decimal_value += (2 ** i) * bit
            i += 1
        if bits:
            phase = decimal_value/(2 ** len(bits.keys()))
            print("\nThe estimated value of the phase is {0}".format(phase))


def computing_host_protocol(host):
    """
    Protocol for the computing hosts
    """

    host.receive_schedule()
    host.send_results()

Step 4: Provide inputs

Here we provide the actual inputs to run the algorithm. First we provide the phase which would be estimated by the algorithm. Next, we set number of qubits per host. The greater number of qubits would provide a closer estimate to the actual answer.

[5]:
# Try phase like 1/8 or 1/3
phase_input = 1/5

# Set number of qubits per host
num_qubits_per_host = 4

Step 5: Execute the distributed algorithm and analyse the results

Here, we first initiate the Network via QuNetSim as well as initiate all the Hosts required to perform the algorithm.

Once ControllerHost object is initiated, the function controller_host.create_distributed_network is a template function which is used to initiate ComputingHost objects with the provided number of qubits, provide a topology as well as to connect the nodes internally.

We provide the monolithic circuit input to the ControllerHost protocol and start running the protocol for the controller hosts and the computing hosts involved.

[6]:
def main():
    # initialize network
    network = Network.get_instance()
    network.delay = 0
    network.start()

    clock = Clock()

    controller_host = ControllerHost(
        host_id="host_1",
        clock=clock,
    )

    computing_hosts, q_map = controller_host.create_distributed_network(
        num_computing_hosts=2,
        num_qubits_per_host=num_qubits_per_host)
    controller_host.start()

    network.add_hosts([
        computing_hosts[0],
        computing_hosts[1],
        controller_host])

    print('starting...\n')

    print('The Actual value of the phase is: {0}\n'.format(phase_input))
    # For phase = 1/8
    #client_input_gate = np.array([[1, 0], [0, np.exp(1j * np.pi / 4)]])
    # For phase = 1/3
    client_input_gate = np.array([[1, 0], [0, np.exp(1j * 2 * np.pi * phase_input)]])
    monolithic_circuit = quantum_phase_estimation_circuit(q_map, client_input_gate)

    t1 = controller_host.run_protocol(
        controller_host_protocol,
        (q_map, client_input_gate, monolithic_circuit))
    t2 = computing_hosts[0].run_protocol(computing_host_protocol)
    t3 = computing_hosts[1].run_protocol(computing_host_protocol)

    t1.join()
    t2.join()
    t3.join()
    network.stop(True)


if __name__ == '__main__':
    main()
starting...

The Actual value of the phase is: 0.2

Final results:

q_0_3: 1
q_0_2: 1
q_0_1: 0
q_0_0: 0

The estimated value of the phase is 0.1875