Exemple #1
0
    def start(self, nodes=None, backend=None):
        """
        Starts the network.

        """
        if backend is None:
            self._backend = EQSNBackend()
        else:
            self._backend = backend
        if nodes is not None:
            self._backend.start(nodes=nodes)
        self._queue_processor_thread = DaemonThread(target=self._process_queue)
def main():
    backend = EQSNBackend()
    network = Network.get_instance()
    nodes = ["Alice", "Bob", "Eve", "Dean"]
    network.start(nodes, backend)
    network.delay = 0.0

    hosts = {'alice': Host('Alice', backend),
             'bob': Host('Bob', backend)}

    # A <-> B
    hosts['alice'].add_connection('Bob')
    hosts['bob'].add_connection('Alice')

    hosts['alice'].start()
    hosts['bob'].start()

    for h in hosts.values():
        network.add_host(h)

    ack_received_1 = hosts['alice'].send_classical(
        hosts['bob'].host_id, 'hello bob one', await_ack=True)
    hosts['alice'].max_ack_wait = 0.0
    ack_received_2 = hosts['alice'].send_classical(
        hosts['bob'].host_id, 'hello bob one', await_ack=True)
    assert ack_received_1
    assert not ack_received_2
    print("All tests succesfull!")
    network.stop(True)
    exit()
Exemple #3
0
def main():
    network = Network.get_instance()

    backend = EQSNBackend()

    number_of_entanglement_pairs = 50

    nodes = ['A', 'B']
    network.start(nodes, backend)
    network.delay = 0.1

    host_A = Host('A', backend)
    host_A.add_connection('B')
    host_A.delay = 0
    host_A.start()

    host_B = Host('B', backend)
    host_B.add_connection('A')
    host_B.delay = 0
    host_B.start()

    network.add_host(host_A)
    network.add_host(host_B)

    t1 = host_A.run_protocol(alice,
                             (host_B.host_id, number_of_entanglement_pairs))
    t2 = host_B.run_protocol(bob,
                             (host_A.host_id, number_of_entanglement_pairs))

    t1.join()
    t2.join()
    network.stop(True)
Exemple #4
0
def test_ghz_ten_nodes(benchmark):
    backend = EQSNBackend()
    nodes = 10
    network, hosts = setup_network(nodes, backend)
    ms = benchmark.pedantic(ghz, args=(hosts[0], hosts[1:]), rounds=30)
    for m in ms:
        assert (sum(m) == nodes - 1 or sum(m) == 0)
    network.stop(True)
Exemple #5
0
def main():
    # Initialize a network
    network = Network.get_instance()
    backend = EQSNBackend()

    # Define the host IDs in the network
    nodes = ['Alice', 'Bob']

    network.delay = 0.0

    # Start the network with the defined hosts
    network.start(nodes, backend)

    # Initialize the host Alice
    host_alice = Host('Alice', backend)

    # Add a one-way connection (classical and quantum) to Bob
    host_alice.add_connection('Bob')
    host_alice.delay = 0.0

    # Start listening
    host_alice.start()

    host_bob = Host('Bob', backend)
    # Bob adds his own one-way connection to Alice
    host_bob.add_connection('Alice')
    host_bob.delay = 0.0
    host_bob.start()

    # Add the hosts to the network
    # The network is: Alice <--> Bob
    network.add_host(host_alice)
    network.add_host(host_bob)

    # Generate random key
    key_size = 20  # the size of the key in bit
    secret_key = np.random.randint(2, size=key_size)

    # Concatentate functions
    def alice_func(alice):
        msg_buff = []
        alice_qkd(alice, msg_buff, secret_key, host_bob.host_id)
        alice_send_message(alice, secret_key, host_bob.host_id)

    def bob_func(eve):
        msg_buff = []
        eve_key = eve_qkd(eve, msg_buff, key_size, host_alice.host_id)
        eve_receive_message(eve, msg_buff, eve_key, host_alice.host_id)

    # Run Bob and Alice

    t1 = host_alice.run_protocol(alice_func, ())
    t2 = host_bob.run_protocol(bob_func, ())

    t1.join()
    t2.join()

    network.stop(True)
Exemple #6
0
    def test_density_operator(self):
        """
        Test EQSN.
        """
        backend = EQSNBackend()
        network = Network.get_instance()
        network.start(["Alice", "Bob"], backend)
        alice = Host('Alice', backend)
        bob = Host('Bob', backend)
        alice.start()
        bob.start()
        network.add_host(alice)
        network.add_host(bob)

        q1 = backend.create_EPR(alice.host_id, bob.host_id)
        q2 = backend.receive_epr(bob.host_id, alice.host_id, q_id=q1.id)

        density_operator = backend.density_operator(q1)
        expected = np.diag([0.5, 0.5])
        self.assertTrue(np.allclose(density_operator, expected))

        # remove qubits
        backend.measure(q1, False)
        backend.measure(q2, False)

        network.stop(True)
    def setUp(self):
        network = Network.get_instance()
        network.start(["host_1"], EQSNBackend())

        self.controller_host = ControllerHost(
            host_id="host_1",
            computing_host_ids=["QPU_1"])
        network.add_host(self.controller_host)

        self._network = network
Exemple #8
0
def main():
    network = Network.get_instance()
    nodes = ["Alice", "Bob", "Eve", "Dean"]
    back = EQSNBackend()
    network.start(nodes, back)

    network.delay = 0.1

    host_alice = Host('Alice', back)
    host_alice.add_connection('Bob')
    host_alice.add_connection('Eve')
    host_alice.start()

    host_bob = Host('Bob', back)
    host_bob.add_connection('Alice')
    host_bob.add_connection('Eve')
    host_bob.start()

    host_eve = Host('Eve', back)
    host_eve.add_connection('Bob')
    host_eve.add_connection('Dean')
    host_eve.add_connection('Alice')
    host_eve.start()

    host_dean = Host('Dean', back)
    host_dean.add_connection('Eve')
    host_dean.start()

    network.add_host(host_alice)
    network.add_host(host_bob)
    network.add_host(host_eve)
    network.add_host(host_dean)

    share_list = ["Bob", "Eve", "Dean"]
    q_id1, ack_received = host_alice.send_w(share_list, await_ack=True)

    print("Alice received ACK from all? " + str(ack_received))

    q1 = host_alice.get_w('Alice', q_id1, wait=10)
    q2 = host_bob.get_w('Alice', q_id1, wait=10)
    q3 = host_eve.get_w('Alice', q_id1, wait=10)
    q4 = host_dean.get_w('Alice', q_id1, wait=10)

    print("System density matrix:")
    print(q1._qubit[0].data)

    m1 = q1.measure()
    m2 = q2.measure()
    m3 = q3.measure()
    m4 = q4.measure()

    print("\nResults of measurements are %d, %d, %d, %d." % (m1, m2, m3, m4))

    network.stop(True)
    exit()
Exemple #9
0
 def setUpClass(cls):
     global network
     global hosts
     nodes = ["Alice", "Bob"]
     backend = EQSNBackend()
     network.start(nodes=nodes, backend=backend)
     hosts = {'alice': Host('Alice', backend), 'bob': Host('Bob', backend)}
     hosts['alice'].add_connection('Bob')
     hosts['bob'].add_connection('Alice')
     hosts['alice'].start()
     hosts['bob'].start()
     for h in hosts.values():
         network.add_host(h)
Exemple #10
0
def main():
    network = Network.get_instance()
    nodes = ["Alice", "Bob", "Eve", "Dean"]
    back = EQSNBackend()
    network.start(nodes, back)

    network.delay = 0.1

    host_alice = Host('Alice', back)
    host_alice.add_connection('Bob')
    host_alice.add_connection('Eve')
    host_alice.start()

    host_bob = Host('Bob', back)
    host_bob.add_connection('Alice')
    host_bob.add_connection('Eve')
    host_bob.start()

    host_eve = Host('Eve', back)
    host_eve.add_connection('Bob')
    host_eve.add_connection('Dean')
    host_eve.add_connection('Alice')
    host_eve.start()

    host_dean = Host('Dean', back)
    host_dean.add_connection('Eve')
    host_dean.start()

    network.add_host(host_alice)
    network.add_host(host_bob)
    network.add_host(host_eve)
    network.add_host(host_dean)

    share_list = ["Bob", "Eve", "Dean"]
    q_id1 = host_alice.send_w(share_list, no_ack=True)

    q1 = host_alice.get_w('Alice', q_id1, wait=10)
    q2 = host_bob.get_w('Alice', q_id1, wait=10)
    q3 = host_eve.get_w('Alice', q_id1, wait=10)
    q4 = host_dean.get_w('Alice', q_id1, wait=10)

    m1 = q1.measure()
    m2 = q2.measure()
    m3 = q3.measure()
    m4 = q4.measure()

    print("\nResults of measurements are %d, %d, %d, %d." % (m1, m2, m3, m4))

    network.stop(True)
    exit()
def main():
    backend = EQSNBackend()
    network = Network.get_instance()
    nodes = ["Alice", "Bob", "Eve", "Dean"]
    network.start(nodes, backend)
    network.delay = 0.7

    hosts = {'alice': Host('Alice', backend), 'bob': Host('Bob', backend)}

    network.delay = 0
    # A <-> B
    hosts['alice'].add_connection('Bob')
    hosts['bob'].add_connection('Alice')

    hosts['alice'].start()
    hosts['bob'].start()

    for h in hosts.values():
        network.add_host(h)

    # send messages to Bob without waiting for ACKs
    hosts['alice'].send_classical(hosts['bob'].host_id,
                                  'hello bob one',
                                  await_ack=False)
    hosts['alice'].send_classical(hosts['bob'].host_id,
                                  'hello bob two',
                                  await_ack=False)
    hosts['alice'].send_classical(hosts['bob'].host_id,
                                  'hello bob three',
                                  await_ack=False)
    hosts['alice'].send_classical(hosts['bob'].host_id,
                                  'hello bob four',
                                  await_ack=False)

    # Wait for all Acks from Bob
    hosts['alice'].await_remaining_acks(hosts['bob'].host_id)

    saw_ack = [False, False, False, False]
    messages = hosts['alice'].classical
    for m in messages:
        if m.content == Constants.ACK:
            saw_ack[m.seq_num] = True

    for ack in saw_ack:
        assert ack
    print("All tests succesfull!")
    network.stop(True)
Exemple #12
0
def main():
    backend = EQSNBackend()
    network = Network.get_instance()
    nodes = ['Alice', 'Eve', 'Bob']
    network.start(nodes)

    host_alice = Host('Alice', backend)
    host_alice.add_connection('Eve')
    host_alice.start()

    host_eve = Host('Eve', backend)
    host_eve.add_connections(['Alice', 'Bob'])
    host_eve.start()

    host_bob = Host('Bob', backend)
    host_bob.add_connection('Eve')
    host_bob.start()

    network.add_host(host_alice)
    network.add_host(host_eve)
    network.add_host(host_bob)
    # network.draw_classical_network()
    # network.draw_quantum_network()
    network.start()

    alice_basis, bob_basis = preparation()
    print("Alice bases: {}".format(alice_basis))
    print("Bob bases: {}".format(bob_basis))

    if INTERCEPTION:
        host_eve.q_relay_sniffing = True
        host_eve.q_relay_sniffing_fn = eve_sniffing_quantum

    t1 = host_alice.run_protocol(alice, (
        host_bob.host_id,
        alice_basis,
    ))
    t2 = host_bob.run_protocol(bob, (
        host_alice.host_id,
        bob_basis,
    ))
    t1.join()
    t2.join()

    network.stop(True)
    exit()
def main():
    backend = EQSNBackend()
    network = Network.get_instance()
    nodes = ["Alice", "Bob", "Eve", "Dean"]
    network.start(nodes, backend)
    network.delay = 0.0

    hosts = {'alice': Host('Alice', backend), 'bob': Host('Bob', backend)}

    network.start(nodes, backend)
    network.packet_drop_rate = 0.5
    network.delay = 0

    hosts['alice'].add_connection('Bob')
    hosts['bob'].add_connection('Alice')

    hosts['alice'].start()
    hosts['bob'].start()

    for h in hosts.values():
        network.add_host(h)

    # ACKs for 1 hop take at most 2 seconds
    hosts['alice'].max_ack_wait = 0.5
    num_acks = 0
    # don't make more then 10 attempts, since of receiver window.
    num_messages = 20
    for _ in range(num_messages):
        ack = hosts['alice'].send_classical(hosts['bob'].host_id,
                                            'Hello Bob',
                                            await_ack=True)
        if ack:
            num_acks += 1

    num_messages_bob_received = len(hosts['bob'].classical)
    assert num_acks != num_messages
    assert num_acks < num_messages
    assert num_messages_bob_received < num_messages

    # ACKs can also get dropped
    assert num_messages_bob_received > num_acks
    assert float(num_acks) / num_messages < 0.9
    print("All tests succesfull!")
    network.stop(True)
    exit()
    def setUp(self):
        network = Network.get_instance()
        network.start(["host_1", "QPU_1", "QPU_2", "QPU_3"], EQSNBackend())

        clock = Clock()

        self.computing_host_1 = ComputingHost(host_id="QPU_1",
                                              controller_host_id="host_1",
                                              clock=clock,
                                              total_qubits=2)

        self.computing_host_2 = ComputingHost(host_id="QPU_2",
                                              controller_host_id="host_1",
                                              clock=clock,
                                              total_qubits=2)

        self.computing_host_3 = ComputingHost(host_id="QPU_3",
                                              controller_host_id="host_1",
                                              clock=clock,
                                              total_qubits=2)

        self.controller_host = ControllerHost(
            host_id="host_1",
            clock=clock,
            computing_host_ids=["QPU_1", "QPU_2", "QPU_3"])

        self.computing_host_1.add_connections(['QPU_2', 'QPU_3'])
        self.computing_host_1.start()
        self.computing_host_2.add_connections(['QPU_1', 'QPU_3'])
        self.computing_host_2.start()
        self.computing_host_3.add_connections(['QPU_1', 'QPU_2'])
        self.computing_host_3.start()

        self.controller_host.start()

        network.add_hosts([
            self.controller_host, self.computing_host_1, self.computing_host_2,
            self.computing_host_3
        ])

        self.network = network
        self.clock = clock
Exemple #15
0
def main():
    backend = EQSNBackend()
    network = Network.get_instance()
    nodes = ["Alice", "Bob", "Eve", "Dean"]
    network.start(nodes, backend)
    network.delay = 0.7
    hosts = {'alice': Host('Alice', backend), 'bob': Host('Bob', backend)}

    # A <-> B
    hosts['alice'].add_connection('Bob')
    hosts['bob'].add_connection('Alice')

    hosts['alice'].start()
    hosts['bob'].start()

    for h in hosts.values():
        network.add_host(h)

    q_id = hosts['alice'].send_epr(hosts['bob'].host_id)
    q1 = hosts['alice'].get_epr(hosts['bob'].host_id, q_id)
    i = 0
    while q1 is None and i < 5:
        q1 = hosts['alice'].get_epr(hosts['bob'].host_id, q_id)
        i += 1
        time.sleep(1)

    assert q1 is not None
    i = 0
    q2 = hosts['bob'].get_epr(hosts['alice'].host_id, q_id)
    while q2 is None and i < 5:
        q2 = hosts['bob'].get_epr(hosts['alice'].host_id, q_id)
        i += 1
        time.sleep(1)

    assert q2 is not None
    assert (q1.measure() == q2.measure())
    print("All tests succesfull!")
    network.stop(True)
    exit()
Exemple #16
0
def main():
    network = Network.get_instance()

    # backend = ProjectQBackend()
    # backend = CQCBackend()
    backend = EQSNBackend()

    nodes = ['A', 'B']
    network.delay = 0.1
    network.start(nodes, backend)

    host_A = Host('A', backend)
    host_A.add_connection('B')
    host_A.delay = 0
    host_A.start()

    host_B = Host('B', backend)
    host_B.add_connection('A')
    host_B.delay = 0
    host_B.start()

    network.add_host(host_A)
    network.add_host(host_B)

    m = 2
    n = 4
    rot_angle = np.pi / 9

    t1 = host_A.run_protocol(quantum_coin_flipping,
                             arguments=(m, n, host_B.host_id, rot_angle))
    t2 = host_B.run_protocol(quantum_coin_flipping,
                             arguments=(m, n, host_A.host_id, rot_angle))

    t1.join()
    t2.join()

    network.stop(True)
Exemple #17
0
def test_teleport_ten_hops(benchmark):
    backend = EQSNBackend()
    network, hosts = setup_network(11, backend)
    benchmark.pedantic(superdense, args=(hosts[0], hosts[-1]), rounds=30)
    network.stop(True)
Exemple #18
0
class Network:
    """ A network control singleton object. """
    __instance = None

    @staticmethod
    def get_instance():
        if Network.__instance is None:
            Network()
        return Network.__instance

    def __init__(self):
        if Network.__instance is None:
            self.ARP = {}
            # The directed graph for the connections
            self.classical_network = nx.DiGraph()
            self.quantum_network = nx.DiGraph()
            self._quantum_routing_algo = nx.shortest_path
            self._classical_routing_algo = nx.shortest_path
            self._use_hop_by_hop = True
            self._packet_queue = Queue()
            self._stop_thread = False
            self._use_ent_swap = False
            self._queue_processor_thread = None
            self._delay = 0.1
            self._packet_drop_rate = 0
            self._x_error_rate = 0
            self._z_error_rate = 0
            self._backend = None
            self._tick = 0
            self._tickspan = 1e-8  # 10 ns
            Network.__instance = self
        else:
            raise Exception('this is a singleton class')

    @property
    def use_ent_swap(self):
        return self._use_ent_swap

    @use_ent_swap.setter
    def use_ent_swap(self, use_ent_swap):
        self._use_ent_swap = use_ent_swap

    @property
    def use_hop_by_hop(self):
        """
        Get the routing style for the network.

        Returns:
            If the network uses hop by hop routing.
        """
        return self._use_hop_by_hop

    @use_hop_by_hop.setter
    def use_hop_by_hop(self, should_use):
        """
        Set the routing style for the network.
        Args:
            should_use (bool): If the network should use hop by hop routing or not
        """
        if not isinstance(should_use, bool):
            raise Exception('use_hop_by_hop should be a boolean value.')

        self._use_hop_by_hop = should_use

    @property
    def classical_routing_algo(self):
        """
        Get the routing algorithm for the network.
        """
        return self._classical_routing_algo

    @classical_routing_algo.setter
    def classical_routing_algo(self, algorithm):
        """
        Set the routing algorithm for the network.

        Args:
             algorithm (function): The routing function. Should return a list of host_ids which represents the route
        """
        self._classical_routing_algo = algorithm

    @property
    def quantum_routing_algo(self):
        """
        Gets the quantum routing algorithm of the network.

        Returns:
           algorithm (function): The quantum routing algorithm of the network
        """
        return self._quantum_routing_algo

    @quantum_routing_algo.setter
    def quantum_routing_algo(self, algorithm):
        """
        Sets the quantum routing algorithm of the network.

        Args:
            algorithm (function): The routing algorithm of the network. Should take an input and an output
        """
        if not callable(algorithm):
            raise Exception(
                "The quantum routing algorithm must be a function.")

        num_algo_params = len(signature(algorithm).parameters)
        if num_algo_params != 3:
            raise Exception(
                "The quantum routing algorithm function should take three parameters: "
                +
                "the (nx) graph representation of the network, the sender address and the "
                + "receiver address.")

        self._quantum_routing_algo = algorithm

    @property
    def delay(self):
        """
        Get the delay interval of the network.
        """
        return self._delay

    @delay.setter
    def delay(self, delay):
        """
        Set the delay interval of the network.

        Args:
             delay (float): Delay in network tick in seconds
        """
        if not (isinstance(delay, int) or isinstance(delay, float)):
            raise Exception('delay should be a number')

        if delay < 0:
            raise Exception('Delay should not be negative')

        self._delay = delay

    @property
    def packet_drop_rate(self):
        """
        Get the drop rate of the network.
        """
        return self._packet_drop_rate

    @packet_drop_rate.setter
    def packet_drop_rate(self, drop_rate):
        """
        Set the drop rate of the network.

        Args:
             drop_rate (float): Probability of dropping a packet in the network
        """

        if drop_rate < 0 or drop_rate > 1:
            raise Exception('Packet drop rate should be between 0 and 1')

        if not (isinstance(drop_rate, int) or isinstance(drop_rate, float)):
            raise Exception('Packet drop rate should be a number')

        self._packet_drop_rate = drop_rate

    @property
    def x_error_rate(self):
        """
        Get the X error rate of the network.
        Returns:
              The *x_error_rate* of the network.
        """

        return self._x_error_rate

    @x_error_rate.setter
    def x_error_rate(self, error_rate):
        """
        Set the X error rate of the network.

        Args:
             error_rate (float): Probability of a X error during a qubit transmission in the network
        """

        if error_rate < 0 or error_rate > 1:
            raise Exception('Error rate should be between 0 and 1.')

        if not (isinstance(error_rate, int) or isinstance(error_rate, float)):
            raise Exception('X error rate should be a number')

        self._x_error_rate = error_rate

    @property
    def z_error_rate(self):
        """
        Get the Z error rate of the network.

        Returns:
            (float): The Z error rate of the network.
        """
        return self._z_error_rate

    @z_error_rate.setter
    def z_error_rate(self, error_rate):
        """
        Set the Z error rate of the network.

        Args:
             error_rate (float): Probability of a Z error during a qubit transmission in the network
        """

        if error_rate < 0 or error_rate > 1:
            raise Exception('Error rate should be between 0 and 1.')

        if not (isinstance(error_rate, int) or isinstance(error_rate, float)):
            raise Exception('Z error rate should be a number')

        self._z_error_rate = error_rate

    @property
    def tick(self):
        """
        Returns the current tick of the network.
        Used for timing operations.

        Returns:
            (int): Current tick of the network
        """
        return self._tick

    @property
    def tickspan(self):
        """
        Returns the real world time corresponding to one tick

        Returns
            (float): Span of real world time corresponding to a single tick
        """
        return self._tickspan

    @tickspan.setter
    def tickspan(self, tickspan):
        """
        Set the span of time corresponding to one network tick (defaults to 10 ns)

        Args
            tickspan (float): Span of real world time corresponding to a single tick
        """
        if not (isinstance(tickspan, int) or isinstance(tickspan, float)):
            raise Exception(
                "Tickspan must be an integer or floating point number")
        elif tickspan <= 0:
            raise ValueError("Tickspan must be positive")

        self._tickspan = tickspan

    def add_host(self, host):
        """
        Adds the *host* to ARP table and updates the network graph.

        Args:
            host (Host): The host to be added to the network.
        """

        Logger.get_instance().debug('host added: ' + host.host_id)
        self.ARP[host.host_id] = host
        self._update_network_graph(host)

    def add_hosts(self, hosts):
        """
        Adds the *hosts* to ARP table and updates the network graph.

        Args:
            hosts (list): The hosts to be added to the network.
        """
        for host in hosts:
            self.add_host(host)

    def remove_host(self, host):
        """
        Removes the host from the ARP table.

        Args:
            host (Host): The host to be removed from the network.
        """

        if host.host_id in self.ARP:
            del self.ARP[host.host_id]

    def update_host(self, host):
        """
        Update the connections of a host in the network.
        Args:
            host:

        Returns:

        """
        self.remove_host(host)
        self.add_host(host)

    def _remove_network_node(self, host):
        """
        Removes the host from the ARP table.

        Args:
            host (Host): The host to be removed from the network.
        """

        try:
            self.classical_network.remove_node(host.host_id)
        except nx.NetworkXError:
            Logger.get_instance().error(
                'attempted to remove a non-exiting node from network')

    def _update_network_graph(self, host):
        """
        Add host *host* to the network and update the graph representation of the network

        Args:
            host: The host to be added
        """
        self.classical_network.add_node(host.host_id)
        self.quantum_network.add_node(host.host_id)

        for connection in host.classical_connections:
            if not self.classical_network.has_edge(host.host_id,
                                                   connection.receiver_id):
                edge = (host.host_id, connection.receiver_id, {'weight': 1})
                self.classical_network.add_edges_from([edge])

        for connection in host.quantum_connections:
            if not self.quantum_network.has_edge(host.host_id,
                                                 connection.receiver_id):
                edge = (host.host_id, connection.receiver_id, {'weight': 1})
                self.quantum_network.add_edges_from([edge])

    def shares_epr(self, sender, receiver):
        """
        Returns boolean value dependent on if the sender and receiver share an EPR pair.

        Args:
            receiver (Host): The receiver
            sender (Host): The sender

        Returns:
             (bool) whether the sender and receiver share an EPR pair.
        """
        host_sender = self.get_host(sender)
        host_receiver = self.get_host(receiver)
        return host_sender.shares_epr(receiver) and host_receiver.shares_epr(
            sender)

    def get_host(self, host_id):
        """
        Returns the host with the *host_id*.

        Args:
            host_id (str): ID number of the host that is returned.

        Returns:
             Host (Host): Host with the *host_id*
        """
        if host_id not in self.ARP:
            return None
        return self.ARP[host_id]

    def get_ARP(self):
        """
        Returns the ARP table.

        Returns:
             dict: The ARP table
        """
        return self.ARP

    def get_host_name(self, host_id):
        """
        Args:
            host_id (str): ID number of the host whose name is returned if it is in ARP table

        Returns the name of the host with *host_id* if the host is in ARP table , otherwise returns None.

        Returns:
             dict or None: Name of the host
        """
        if host_id not in self.ARP:
            return None
        return self.ARP[host_id].cqc.name

    def get_quantum_route(self, source, dest):
        """
        Gets the route for quantum information from source to destination.

        Args:
            source (str): ID of the source host
            dest (str): ID of the destination host
        Returns:
            route (list): An ordered list of ID numbers on the shortest path from source to destination.
        """
        return self.quantum_routing_algo(self.quantum_network, source, dest)

    def get_classical_route(self, source, dest):
        """
        Gets the route for classical information from source to destination.

        Args:
            source (str): ID of the source host
            dest (str): ID of the destination host

        Returns:
            route (list): An ordered list of ID numbers on the shortest path from source to destination.
        """
        return self.classical_routing_algo(self.classical_network, source,
                                           dest)

    def _entanglement_swap(self, sender, receiver, route, q_id, o_seq_num,
                           blocked):
        """
        Performs a chain of entanglement swaps with the hosts between sender and receiver to create a shared EPR pair
        between sender and receiver.

        Args:
            sender (Host): Sender of the EPR pair
            receiver (Host): Receiver of the EPR pair
            route (list): Route between the sender and receiver
            q_id (str): Qubit ID of the sent EPR pair
            o_seq_num (int): The original sequence number
            blocked (bool): If the pair being distributed is blocked or not
        """
        host_sender = self.get_host(sender)
        lastfidelity = 1.0

        def establish_epr(net, s, r):
            if not net.shares_epr(s, r):
                self.get_host(s).send_epr(r, q_id, await_ack=True)
            else:
                old_id = self.get_host(s).change_epr_qubit_id(r, q_id)
                net.get_host(r).change_epr_qubit_id(route[i], q_id, old_id)

        # Create EPR pairs on the route, where all EPR qubits have the id q_id
        threads = []
        for i in range(len(route) - 1):
            threads.append(
                DaemonThread(establish_epr,
                             args=(self, route[i], route[i + 1])))

        for t in threads:
            t.join()

        for i in range(len(route) - 2):
            host = self.get_host(route[i + 1])
            q = host.get_epr(route[0], q_id, wait=10)
            if q is None:
                print("Host is %s" % host.host_id)
                print("Search host is %s" % route[0])
                print("Search id is %s" % q_id)
                print("EPR storage is")
                print(host.EPR_store)
                Logger.get_instance().error('Entanglement swap failed')
                return
            data = {
                'q': q,
                'eq_id': q_id,
                'node': sender,
                'o_seq_num': o_seq_num,
                'type': Constants.EPR
            }

            if route[i + 2] == route[-1]:
                data = {
                    'q': q,
                    'eq_id': q_id,
                    'node': sender,
                    'ack': True,
                    'o_seq_num': o_seq_num,
                    'type': Constants.EPR
                }

            host.send_teleport(route[i + 2],
                               None,
                               await_ack=True,
                               payload=data,
                               generate_epr_if_none=False)

        # Change in the storage that the EPR qubit is shared with the receiver
        q2 = host_sender.get_epr(route[1], q_id=q_id)
        host_sender.add_epr(receiver, q2, q_id, blocked)
        Logger.get_instance().log(
            'Entanglement swap was successful for pair with id ' + q_id +
            ' between ' + sender + ' and ' + receiver)

    def _establish_epr(self, sender, receiver, q_id, o_seq_num, blocked):
        """
        Instead doing an entanglement swap, for efficiency we establish EPR pairs
        directly for simulation, if an entanglement swap would have been possible.

        Args:
            sender (Host): Sender of the EPR pair
            receiver (Host): Receiver of the EPR pair
            q_id (str): Qubit ID of the sent EPR pair
            o_seq_num (int): The original sequence number
            blocked (bool): If the pair being distributed is blocked or not
        """
        host_sender = self.get_host(sender)
        host_receiver = self.get_host(receiver)
        q1 = Qubit(host_sender)
        q2 = Qubit(host_sender)
        q1.H()
        q1.cnot(q2)
        host_sender.add_epr(receiver, q1, q_id, blocked)
        host_receiver.add_epr(sender, q2, q_id, blocked)
        host_receiver.send_ack(sender, o_seq_num)

    def _route_quantum_info(self, sender, receiver, qubits):
        """
        Routes qubits from sender to receiver.

        Args:
            sender (Host): Sender of qubits
            receiver (Host): Receiver qubits
            qubits (List of Qubits): The qubits to be sent
        """
        def transfer_qubits(r, original_sender=None):
            for q in qubits:
                Logger.get_instance().log('transfer qubits - sending qubit ' +
                                          q.id)
                x_err_var = random.random()
                z_err_var = random.random()
                if x_err_var > (1 - self.x_error_rate):
                    q.X()
                if z_err_var > (1 - self.z_error_rate):
                    q.Z()

                q.send_to(self.ARP[r].host_id)
                Logger.get_instance().log('transfer qubits - received ' + q.id)

                # Unblock qubits in case they were blocked
                q.blocked = False

                if self.ARP[r].q_relay_sniffing:
                    self.ARP[r].q_relay_sniffing_fn(original_sender, receiver,
                                                    q)

        route = self.get_quantum_route(sender, receiver)
        i = 0
        while i < len(route) - 1:
            Logger.get_instance().log('sending qubits from ' + route[i] +
                                      ' to ' + route[i + 1])
            transfer_qubits(route[i + 1], original_sender=route[0])
            i += 1

    def _process_queue(self):
        """
        Runs a thread for processing the packets in the packet queue.
        """

        while True:
            if self._stop_thread:
                break

            if not self._packet_queue.empty():
                # Artificially delay the network
                if self.delay > 0:
                    time.sleep(self.delay)

                packet = self._packet_queue.get()

                sender, receiver = packet.sender, packet.receiver
                host_sender = self.get_host(sender)

                # Find transmission probability of the quantum connection and update network ticks if the packet is a qubit or an EPR signal
                if packet.payload_type == Constants.QUANTUM or (
                        packet.payload_type == Constants.SIGNAL
                        and isinstance(packet.payload, dict)
                        and 'q_id' in packet.payload.keys()):
                    transmission_probability = 1.0
                    for connection in host_sender.quantum_connections:
                        if connection.receiver_id == receiver:
                            transmission_probability = connection.transmission_p
                            break

                # Simulate packet loss
                packet_drop_var = random.random()
                transmission_var = random.random()

                if packet_drop_var > (1 - self.packet_drop_rate):
                    Logger.get_instance().log("PACKET DROPPED")
                    if packet.payload_type == Constants.QUANTUM:
                        packet.payload.release()
                    continue

                # Simulate packet loss due to absorption of qubits in the quantum channel
                elif packet.payload_type == Constants.QUANTUM or (
                        packet.payload_type == Constants.SIGNAL
                        and isinstance(packet.payload, dict)
                        and 'q_id' in packet.payload.keys()):
                    if transmission_var < 1 - transmission_probability:
                        Logger.get_instance().log("QUBIT TRANSMISSION FAILED")
                        if packet.payload_type == Constants.QUANTUM:
                            packet.payload.release()
                        continue

                if packet.payload_type == Constants.QUANTUM:
                    self._route_quantum_info(sender, receiver,
                                             [packet.payload])

                try:
                    if packet.protocol == Constants.RELAY and not self.use_hop_by_hop:
                        full_route = packet.route
                        route = full_route[full_route.index(sender):]
                    else:
                        if packet.protocol == Constants.REC_EPR:
                            route = self.get_classical_route(sender, receiver)
                        else:
                            route = self.get_classical_route(sender, receiver)

                    if len(route) < 2:
                        raise Exception('No route exists')

                    elif len(route) == 2:

                        # Update network ticks
                        sending_host = self.get_host(route[0])
                        receiving_host = self.get_host(route[1])
                        if packet.payload_type == Constants.QUANTUM or (
                                packet.payload_type == Constants.SIGNAL
                                and isinstance(packet.payload, dict)
                                and 'q_id' in packet.payload.keys()):
                            for connection in sending_host.quantum_connections:
                                if connection.receiver_id == receiving_host.host_id:
                                    self._tick += int(
                                        float(connection.length * 1000 /
                                              (self._tickspan)) / 300000000)
                                    break
                        else:
                            for connection in sending_host.classical_connections:
                                if connection.receiver_id == receiving_host.host_id:
                                    self._tick += int(
                                        float(connection.length * 1000 /
                                              (self._tickspan)) / 300000000)
                                    break

                        if packet.protocol != Constants.RELAY:
                            if packet.protocol == Constants.REC_EPR:
                                host_sender = self.get_host(sender)
                                q = host_sender \
                                    .backend \
                                    .create_EPR(host_sender.host_id,
                                                receiver,
                                                q_id=packet.payload['q_id'],
                                                block=packet.payload['blocked'])
                                host_sender.add_epr(receiver, q)
                            self.ARP[receiver].rec_packet(packet)
                        else:
                            self.ARP[receiver].rec_packet(packet.payload)
                    else:

                        # Update network ticks
                        #for routeidx in range(len(route)-2):
                        sending_host = self.get_host(route[0])
                        receiving_host = self.get_host(route[1])
                        if packet.payload_type == Constants.QUANTUM or (
                                packet.payload_type == Constants.SIGNAL
                                and isinstance(packet.payload, dict)
                                and 'q_id' in packet.payload.keys()):
                            for connection in sending_host.quantum_connections:
                                if connection.receiver_id == receiving_host.host_id:
                                    self._tick += int(
                                        float(connection.length * 1000 /
                                              (self._tickspan)) / 300000000)
                                    break
                        else:
                            for connection in sending_host.classical_connections:
                                if connection.receiver_id == receiving_host.host_id:
                                    self._tick += int(
                                        float(connection.length * 1000 /
                                              (self._tickspan)) / 300000000)
                                    break

                        if packet.protocol == Constants.REC_EPR:
                            q_id = packet.payload['q_id']
                            blocked = packet.payload['blocked']
                            q_route = self.get_quantum_route(sender, receiver)

                            if self.use_ent_swap:
                                DaemonThread(self._entanglement_swap,
                                             args=(sender, receiver, q_route,
                                                   q_id, packet.seq_num,
                                                   blocked))
                            else:
                                DaemonThread(self._establish_epr,
                                             args=(sender, receiver, q_id,
                                                   packet.seq_num, blocked))

                        else:
                            network_packet = self._encode(route, packet)
                            self.ARP[route[1]].rec_packet(network_packet)

                except nx.NodeNotFound:
                    Logger.get_instance().error(
                        "route couldn't be calculated, node doesn't exist")
                except ValueError:
                    Logger.get_instance().error(
                        "route couldn't be calculated, value error")
                except Exception as e:
                    Logger.get_instance().error('Error in network: ' + str(e))

    def send(self, packet):
        """
        Puts the packet to the packet queue of the network.

        Args:
            packet (Packet): Packet to be sent
        """

        self._packet_queue.put(packet)

    def stop(self, stop_hosts=False):
        """
        Stops the network.
        """

        Logger.get_instance().log("Network stopped")
        try:
            if stop_hosts:
                for host in self.ARP:
                    self.ARP[host].stop(release_qubits=True)

            self._stop_thread = True
            if self._backend is not None:
                self._backend.stop()
        except Exception as e:
            Logger.get_instance().error(e)

    def start(self, nodes=None, backend=None):
        """
        Starts the network.

        """
        if backend is None:
            self._backend = EQSNBackend()
        else:
            self._backend = backend
        if nodes is not None:
            self._backend.start(nodes=nodes)
        self._queue_processor_thread = DaemonThread(target=self._process_queue)

    def draw_classical_network(self):
        """
        Draws a plot of the network.
        """
        nx.draw_networkx(self.classical_network,
                         pos=nx.spring_layout(self.classical_network),
                         with_labels=True,
                         hold=False)
        plt.show()

    def draw_quantum_network(self):
        """
        Draws a plot of the network.
        """
        nx.draw_networkx(self.quantum_network,
                         pos=nx.spring_layout(self.quantum_network),
                         with_labels=True,
                         hold=False)
        plt.show()

    def _encode(self, route, payload, ttl=10):
        """
        Adds another layer to the packet if route length between sender and receiver is greater than 2. Sets the
        protocol flag in this layer to RELAY and payload_type as SIGNAL and adds a variable
        Time-To-Live information in this layer.

        Args:
            route: route of the packet from sender to receiver
            payload (Object): Lower layers of the packet
            ttl(int): Time-to-Live parameter

        Returns:
            RoutingPacket: Encoded RELAY packet
        """
        if payload.protocol != Constants.RELAY:
            packet = RoutingPacket(route[1], '', Constants.RELAY,
                                   Constants.SIGNAL, payload, ttl, route)
        else:
            packet = payload
            packet.sender = route[1]

        if self.use_hop_by_hop:
            packet.receiver = route[-1]
        else:
            packet.receiver = route[2]

        return packet
Exemple #19
0
class Network:
    """ A network control singleton object. """
    __instance = None

    @staticmethod
    def get_instance():
        if Network.__instance is None:
            Network()
        return Network.__instance

    @staticmethod
    def reset_network():
        if Network.__instance is not None:
            Network.__instance.stop(True)
            Network.__instance = None
        __instance = Network()

    def __init__(self):
        if Network.__instance is None:
            self.ARP = {}
            # The directed graph for the connections
            self.classical_network = nx.DiGraph()
            self.quantum_network = nx.DiGraph()
            self._quantum_routing_algo = nx.shortest_path
            self._classical_routing_algo = nx.shortest_path
            self._use_hop_by_hop = True
            self._packet_queue = Queue()
            self._stop_thread = False
            self._use_ent_swap = False
            self._queue_processor_thread = None
            self._delay = 0.1
            self._packet_drop_rate = 0
            self._backend = None
            Network.__instance = self
        else:
            raise Exception('this is a singleton class')

    @property
    def use_ent_swap(self):
        return self._use_ent_swap

    @use_ent_swap.setter
    def use_ent_swap(self, use_ent_swap):
        self._use_ent_swap = use_ent_swap

    @property
    def use_hop_by_hop(self):
        """
        Get the routing style for the network.

        Returns:
            If the network uses hop by hop routing.
        """
        return self._use_hop_by_hop

    @use_hop_by_hop.setter
    def use_hop_by_hop(self, should_use):
        """
        Set the routing style for the network.
        Args:
            should_use (bool): If the network should use hop by hop routing or not
        """
        if not isinstance(should_use, bool):
            raise Exception('use_hop_by_hop should be a boolean value.')

        self._use_hop_by_hop = should_use

    @property
    def classical_routing_algo(self):
        """
        Get the routing algorithm for the network.
        """
        return self._classical_routing_algo

    @classical_routing_algo.setter
    def classical_routing_algo(self, algorithm):
        """
        Set the routing algorithm for the network.

        Args:
             algorithm (function): The routing function. Should return a list of host_ids which represents the route
        """
        self._classical_routing_algo = algorithm

    @property
    def quantum_routing_algo(self):
        """
        Gets the quantum routing algorithm of the network.

        Returns:
           algorithm (function): The quantum routing algorithm of the network
        """
        return self._quantum_routing_algo

    @quantum_routing_algo.setter
    def quantum_routing_algo(self, algorithm):
        """
        Sets the quantum routing algorithm of the network.

        Args:
            algorithm (function): The routing algorithm of the network. Should take an input and an output
        """
        if not callable(algorithm):
            raise Exception(
                "The quantum routing algorithm must be a function.")

        num_algo_params = len(signature(algorithm).parameters)
        if num_algo_params != 3:
            raise Exception(
                "The quantum routing algorithm function should take three parameters: "
                +
                "the (nx) graph representation of the network, the sender address and the "
                + "receiver address.")

        self._quantum_routing_algo = algorithm

    @property
    def delay(self):
        """
        Get the delay interval of the network.
        """
        return self._delay

    @delay.setter
    def delay(self, delay):
        """
        Set the delay interval of the network.

        Args:
             delay (float): Delay in network tick in seconds
        """
        if not (isinstance(delay, int) or isinstance(delay, float)):
            raise Exception('delay should be a number')

        if delay < 0:
            raise Exception('Delay should not be negative')

        self._delay = delay

    @property
    def packet_drop_rate(self):
        """
        Get the drop rate of the network.
        """
        return self._packet_drop_rate

    @packet_drop_rate.setter
    def packet_drop_rate(self, drop_rate):
        """
        Set the drop rate of the network.

        Args:
             drop_rate (float): Probability of dropping a packet in the network
        """

        if drop_rate < 0 or drop_rate > 1:
            raise Exception('Packet drop rate should be between 0 and 1')

        if not (isinstance(drop_rate, int) or isinstance(drop_rate, float)):
            raise Exception('Packet drop rate should be a number')

        self._packet_drop_rate = drop_rate

    @property
    def arp(self):
        return self.ARP

    @property
    def num_hosts(self):
        return len(self.arp.keys())

    def add_host(self, host):
        """
        Adds the *host* to ARP table and updates the network graph.

        Args:
            host (Host): The host to be added to the network.
        """

        Logger.get_instance().debug('host added: ' + host.host_id)
        self.ARP[host.host_id] = host
        self._update_network_graph(host)

    def add_hosts(self, hosts):
        """
        Adds the *hosts* to ARP table and updates the network graph.

        Args:
            hosts (list): The hosts to be added to the network.
        """
        for host in hosts:
            self.add_host(host)

    def remove_host(self, host):
        """
        Removes the host from the network.

        Args:
            host (Host): The host to be removed from the network.
        """

        if host.host_id in self.ARP:
            del self.ARP[host.host_id]
            if self.quantum_network.has_node(host.host_id):
                self.quantum_network.remove_node(host.host_id)
            if self.classical_network.has_node(host.host_id):
                self.classical_network.remove_node(host.host_id)

    def remove_c_connection(self, sender, receiver):
        if self.classical_network.has_edge(sender, receiver):
            self.classical_network.remove_edge(sender, receiver)

    def remove_q_connection(self, sender, receiver):
        if self.quantum_network.has_edge(sender, receiver):
            self.quantum_network.remove_edge(sender, receiver)

    def remove_hosts(self, hosts):
        for host in hosts:
            self.remove_host(host)

    def update_host(self, host):
        """
        Update the connections of a host in the network.
        Args:
            host:

        Returns:

        """
        self.remove_host(host)
        self.add_host(host)

    def _remove_network_node(self, host):
        """
        Removes the host from the ARP table.

        Args:
            host (Host): The host to be removed from the network.
        """

        try:
            self.classical_network.remove_node(host.host_id)
        except nx.NetworkXError:
            Logger.get_instance().error(
                'attempted to remove a non-exiting node from network')

    def _update_network_graph(self, host):
        """
        Add host *host* to the network and update the graph representation of the network

        Args:
            host: The host to be added
        """
        if not self.classical_network.has_node(host.host_id):
            self.classical_network.add_node(host.host_id)

        if not self.quantum_network.has_node(host.host_id):
            self.quantum_network.add_node(host.host_id)

        for connection in host.classical_connections:
            if not self.classical_network.has_edge(host.host_id, connection):
                edge = (host.host_id, connection, {'weight': 1})
                self.classical_network.add_edges_from([edge])

        for connection in host.quantum_connections:
            if not self.quantum_network.has_edge(host.host_id, connection):
                edge = (host.host_id, connection, {'weight': 1})
                self.quantum_network.add_edges_from([edge])

    def shares_epr(self, sender, receiver):
        """
        Returns boolean value dependent on if the sender and receiver share an EPR pair.

        Args:
            receiver (Host): The receiver
            sender (Host): The sender

        Returns:
             (bool) whether the sender and receiver share an EPR pair.
        """
        host_sender = self.get_host(sender)
        host_receiver = self.get_host(receiver)
        return host_sender.shares_epr(receiver) and host_receiver.shares_epr(
            sender)

    def get_host(self, host_id):
        """
        Returns the host with the *host_id*.

        Args:
            host_id (str): ID number of the host that is returned.

        Returns:
             Host (Host): Host with the *host_id*
        """
        if host_id not in self.ARP:
            return None
        return self.ARP[host_id]

    def get_ARP(self):
        """
        Returns the ARP table.

        Returns:
             dict: The ARP table
        """
        return self.ARP

    def get_host_name(self, host_id):
        """
        Args:
            host_id (str): ID number of the host whose name is returned if it is in ARP table

        Returns the name of the host with *host_id* if the host is in ARP table , otherwise returns None.

        Returns:
             dict or None: Name of the host
        """
        if host_id not in self.ARP:
            return None
        return self.ARP[host_id].cqc.name

    def get_quantum_route(self, source, dest):
        """
        Gets the route for quantum information from source to destination.

        Args:
            source (str): ID of the source host
            dest (str): ID of the destination host
        Returns:
            route (list): An ordered list of ID numbers on the shortest path from source to destination.
        """
        return self.quantum_routing_algo(self.quantum_network, source, dest)

    def get_classical_route(self, source, dest):
        """
        Gets the route for classical information from source to destination.

        Args:
            source (str): ID of the source host
            dest (str): ID of the destination host

        Returns:
            route (list): An ordered list of ID numbers on the shortest path from source to destination.
        """
        return self.classical_routing_algo(self.classical_network, source,
                                           dest)

    def _entanglement_swap(self, sender, receiver, route, q_id, o_seq_num,
                           blocked):
        """
        Performs a chain of entanglement swaps with the hosts between sender and receiver to create a shared EPR pair
        between sender and receiver.

        Args:
            sender (Host): Sender of the EPR pair
            receiver (Host): Receiver of the EPR pair
            route (list): Route between the sender and receiver
            q_id (str): Qubit ID of the sent EPR pair
            o_seq_num (int): The original sequence number
            blocked (bool): If the pair being distributed is blocked or not
        """
        host_sender = self.get_host(sender)

        def establish_epr(net, s, r):
            if not net.shares_epr(s, r):
                self.get_host(s).send_epr(r, q_id, await_ack=True)
            else:
                old_id = self.get_host(s).change_epr_qubit_id(r, q_id)
                net.get_host(r).change_epr_qubit_id(route[i], q_id, old_id)

        # Create EPR pairs on the route, where all EPR qubits have the id q_id
        threads = []
        for i in range(len(route) - 1):
            threads.append(
                DaemonThread(establish_epr,
                             args=(self, route[i], route[i + 1])))

        for t in threads:
            t.join()

        for i in range(len(route) - 2):
            host = self.get_host(route[i + 1])
            q = host.get_epr(route[0], q_id, wait=10)
            if q is None:
                print("Host is %s" % host.host_id)
                print("Search host is %s" % route[0])
                print("Search id is %s" % q_id)
                print("EPR storage is")
                print(host.EPR_store)
                Logger.get_instance().error('Entanglement swap failed')
                return
            data = {
                'q': q,
                'eq_id': q_id,
                'node': sender,
                'o_seq_num': o_seq_num,
                'type': Constants.EPR
            }

            if route[i + 2] == route[-1]:
                data = {
                    'q': q,
                    'eq_id': q_id,
                    'node': sender,
                    'ack': True,
                    'o_seq_num': o_seq_num,
                    'type': Constants.EPR
                }

            host.send_teleport(route[i + 2],
                               None,
                               await_ack=True,
                               payload=data,
                               generate_epr_if_none=False)

        # Change in the storage that the EPR qubit is shared with the receiver
        q2 = host_sender.get_epr(route[1], q_id=q_id)
        host_sender.add_epr(receiver, q2, q_id, blocked)
        Logger.get_instance().log(
            'Entanglement swap was successful for pair with id ' + q_id +
            ' between ' + sender + ' and ' + receiver)

    def _establish_epr(self, sender, receiver, q_id, o_seq_num, blocked):
        """
        Instead doing an entanglement swap, for efficiency we establish EPR pairs
        directly for simulation, if an entanglement swap would have been possible.

        Args:
            sender (Host): Sender of the EPR pair
            receiver (Host): Receiver of the EPR pair
            q_id (str): Qubit ID of the sent EPR pair
            o_seq_num (int): The original sequence number
            blocked (bool): If the pair being distributed is blocked or not
        """
        host_sender = self.get_host(sender)
        host_receiver = self.get_host(receiver)
        q1 = Qubit(host_sender)
        q2 = Qubit(host_sender)
        q1.H()
        q1.cnot(q2)
        host_sender.add_epr(receiver, q1, q_id, blocked)
        host_receiver.add_epr(sender, q2, q_id, blocked)
        host_receiver.send_ack(sender, o_seq_num)

    def _route_quantum_info(self, sender, receiver, qubits):
        """
        Routes qubits from sender to receiver.

        Args:
            sender (Host): Sender of qubits
            receiver (Host): Receiver qubits
            qubits (List of Qubits): The qubits to be sent
        """
        def transfer_qubits(r, s, original_sender=None):
            for q in qubits:
                # Modify the qubit according to error function of the model
                qubit_id = q.id
                q = self.ARP[s].quantum_connections[
                    self.ARP[r].host_id].model.qubit_func(q)

                if q is None:
                    # Log failure of transmission if qubit is lost
                    Logger.get_instance().log(
                        'transfer qubits - transfer of qubit ' + qubit_id +
                        ' failed')
                    return False

                else:
                    Logger.get_instance().log(
                        'transfer qubits - sending qubit ' + q.id)

                    q.send_to(self.ARP[r].host_id)
                    Logger.get_instance().log('transfer qubits - received ' +
                                              q.id)

                    # Unblock qubits in case they were blocked
                    q.blocked = False

                    if self.ARP[r].q_relay_sniffing:
                        self.ARP[r].q_relay_sniffing_fn(
                            original_sender, receiver, q)
            return True

        route = self.get_quantum_route(sender, receiver)
        i = 0
        while i < len(route) - 1:
            Logger.get_instance().log('sending qubits from ' + route[i] +
                                      ' to ' + route[i + 1])
            if not transfer_qubits(
                    route[i + 1], route[i], original_sender=route[0]):
                return False
            i += 1
        return True

    def _process_queue(self):
        """
        Runs a thread for processing the packets in the packet queue.
        """

        while True:

            packet = self._packet_queue.get()

            if not packet:
                # Stop the network
                self._stop_thread = True
                break

            # Artificially delay the network
            if self.delay > 0:
                time.sleep(self.delay)

            # Simulate packet loss
            packet_drop_var = random.random()
            if packet_drop_var > (1 - self.packet_drop_rate):
                Logger.get_instance().log("PACKET DROPPED")
                if packet.payload_type == Constants.QUANTUM:
                    packet.payload.release()
                continue

            sender, receiver = packet.sender, packet.receiver

            if packet.payload_type == Constants.QUANTUM:
                if not self._route_quantum_info(sender, receiver,
                                                [packet.payload]):
                    continue

            try:
                if packet.protocol == Constants.RELAY and not self.use_hop_by_hop:
                    full_route = packet.route
                    route = full_route[full_route.index(sender):]
                else:
                    if packet.protocol == Constants.REC_EPR:
                        route = self.get_classical_route(sender, receiver)
                    else:
                        route = self.get_classical_route(sender, receiver)

                if len(route) < 2:
                    raise Exception('No route exists')

                elif len(route) == 2:
                    if packet.protocol != Constants.RELAY:
                        if packet.protocol == Constants.REC_EPR:
                            host_sender = self.get_host(sender)
                            q = host_sender \
                                .backend \
                                .create_EPR(host_sender.host_id,
                                            receiver,
                                            q_id=packet.payload['q_id'],
                                            block=packet.payload['blocked'])
                            host_sender.add_epr(receiver, q)
                        self.ARP[receiver].rec_packet(packet)
                    else:
                        self.ARP[receiver].rec_packet(packet.payload)
                else:
                    if packet.protocol == Constants.REC_EPR:
                        q_id = packet.payload['q_id']
                        blocked = packet.payload['blocked']
                        q_route = self.get_quantum_route(sender, receiver)

                        if self.use_ent_swap:
                            DaemonThread(self._entanglement_swap,
                                         args=(sender, receiver, q_route, q_id,
                                               packet.seq_num, blocked))
                        else:
                            DaemonThread(self._establish_epr,
                                         args=(sender, receiver, q_id,
                                               packet.seq_num, blocked))

                    else:
                        network_packet = self._encode(route, packet)
                        self.ARP[route[1]].rec_packet(network_packet)

            except nx.NodeNotFound:
                Logger.get_instance().error(
                    "route couldn't be calculated, node doesn't exist")
            except ValueError:
                Logger.get_instance().error(
                    "route couldn't be calculated, value error")
            except Exception as e:
                Logger.get_instance().error('Error in network: ' + str(e))

    def send(self, packet):
        """
        Puts the packet to the packet queue of the network.

        Args:
            packet (Packet): Packet to be sent
        """

        self._packet_queue.put(packet)

    def stop(self, stop_hosts=False):
        """
        Stops the network.
        """

        Logger.get_instance().log("Network stopped")
        try:
            if stop_hosts:
                for host in self.ARP:
                    self.ARP[host].stop(release_qubits=True)

            self.send(None)  # Send None to queue to stop the queue
            if self._backend is not None:
                self._backend.stop()
        except Exception as e:
            Logger.get_instance().error(e)

    def start(self, nodes=None, backend=None):
        """
        Starts the network.

        """
        if backend is None:
            self._backend = EQSNBackend()
        else:
            self._backend = backend
        if nodes is not None:
            self._backend.start(nodes=nodes)
        self._queue_processor_thread = DaemonThread(target=self._process_queue)

    def draw_classical_network(self):
        """
        Draws a plot of the network.
        """
        nx.draw_networkx(self.classical_network,
                         pos=nx.spring_layout(self.classical_network),
                         with_labels=True)
        plt.show()

    def draw_quantum_network(self):
        """
        Draws a plot of the network.
        """
        nx.draw_networkx(self.quantum_network,
                         pos=nx.spring_layout(self.quantum_network),
                         with_labels=True)
        plt.show()

    def _encode(self, route, payload, ttl=10):
        """
        Adds another layer to the packet if route length between sender and receiver is greater than 2. Sets the
        protocol flag in this layer to RELAY and payload_type as SIGNAL and adds a variable
        Time-To-Live information in this layer.

        Args:
            route: route of the packet from sender to receiver
            payload (Object): Lower layers of the packet
            ttl(int): Time-to-Live parameter

        Returns:
            RoutingPacket: Encoded RELAY packet
        """
        if payload.protocol != Constants.RELAY:
            packet = RoutingPacket(route[1], '', Constants.RELAY,
                                   Constants.SIGNAL, payload, ttl, route)
        else:
            packet = payload
            packet.sender = route[1]

        if self.use_hop_by_hop:
            packet.receiver = route[-1]
        else:
            packet.receiver = route[2]

        return packet

    @staticmethod
    def _get_star_topology_graph(hosts):
        return nx.star_graph(hosts)

    @staticmethod
    def _get_ring_topology_graph(hosts):
        graph = nx.path_graph(hosts)
        graph.add_edge(hosts[0], hosts[len(hosts) - 1])
        return graph

    @staticmethod
    def _get_mesh_topology_graph(hosts):
        return nx.complete_graph(hosts)

    @staticmethod
    def _get_linear_topology_graph(hosts):
        return nx.path_graph(hosts)

    @staticmethod
    def _get_tree_topology_graph(hosts):
        graph = nx.empty_graph(hosts)
        for i in range(0, len(hosts)):
            if 2 * i + 1 < len(hosts):
                graph.add_edge(hosts[i], hosts[2 * i + 1])
            if 2 * i + 2 < len(hosts):
                graph.add_edge(hosts[i], hosts[2 * i + 2])
        return graph

    topologies = {
        'mesh': lambda x: Network._get_mesh_topology_graph(x),
        'ring': lambda x: Network._get_ring_topology_graph(x),
        'star': lambda x: Network._get_star_topology_graph(x),
        'linear': lambda x: Network._get_linear_topology_graph(x),
        'tree': lambda x: Network._get_tree_topology_graph(x)
        # TODO: add bus topology
    }

    @staticmethod
    def _validate_topology_input(host_names, topology):
        if len(host_names) < 2:
            raise ValueError('insufficient hosts')
        if topology not in Network.topologies.keys():
            raise ValueError('topology not implemented')

    def _simple_topology_generation(self, host_names, topology, function):
        from qunetsim import Host

        Network._validate_topology_input(host_names, topology)

        graph = Network.topologies[topology](host_names)

        for host_name in host_names:
            if host_name not in self.ARP.keys():
                self.add_host(Host(host_name))

        for node, adj_list in graph.adjacency():
            h = self.get_host(node)
            function(h, adj_list.keys())
            self.add_host(h)
            h.start()

    def generate_topology(self, host_names: list, topology: str = 'mesh') \
            -> None:
        """
        Adjusts a network that already exists by adding both classic and
        quantum connections that fit the desired network topology. This method
        either adds new nodes or adds connections to existing hosts within
        the network.

        The supported network topologies are mesh, star, ring, linear, and
        tree.

        Args:
            host_names (list): The names for the new hosts.
            topology (str) {'mesh', 'star', 'ring', 'linear', 'tree'}: The
                network topology that will be created.
        """
        self.generate_c_topology(host_names, topology)
        self.generate_q_topology(host_names, topology)

    def generate_q_topology(self, host_names: list, topology: str = 'mesh') \
            -> None:
        """
        Adjusts a network that already exists by adding  quantum connections
        that fit the desired network topology. This method either adds new
        nodes or adds connections to existing hosts within the network.

        The supported network topologies are mesh, star, ring, linear, and
        tree.

        Args:
            host_names (list): The names for the new hosts.
            topology (str) {'mesh', 'star', 'ring', 'linear', 'tree'}: The
                network topology that will be created.
        """
        def add_quantum(x, y):
            return x.add_q_connections(y)

        self._simple_topology_generation(host_names, topology, add_quantum)

    def generate_c_topology(self, host_names: list, topology: str = 'mesh') \
            -> None:
        """
        Adjusts a network that already exists by adding  quantum connections
        that fit the desired network topology. This method either adds new
        nodes or adds connections to existing hosts within the network.

        The supported network topologies are mesh, star, ring, linear, and
        tree.

        Args:
            host_names (list): The names for the new hosts.
            topology (str) {'mesh', 'star', 'ring', 'linear', 'tree'}: The
                network topology that will be created.
        """
        def add_classic(x, y):
            return x.add_c_connections(y)

        self._simple_topology_generation(host_names, topology, add_classic)