From 2cf1b6772ed066febc9640af386dbc53dcc0dc36 Mon Sep 17 00:00:00 2001 From: mvanhooft Date: Mon, 22 Jan 2024 13:09:40 +0100 Subject: [PATCH] * Added example for creating a fully functional server for one of the nodes --- BETA-CHANGELOG.md | 4 + examples/advanced/server/application.py | 106 ++++++++++++++++++ .../advanced/server/netsquid_protocols.py | 81 +++++++++++++ examples/advanced/server/run_simulation.py | 39 +++++++ 4 files changed, 230 insertions(+) create mode 100644 examples/advanced/server/application.py create mode 100644 examples/advanced/server/netsquid_protocols.py create mode 100644 examples/advanced/server/run_simulation.py diff --git a/BETA-CHANGELOG.md b/BETA-CHANGELOG.md index fc116ebc..3bef399c 100644 --- a/BETA-CHANGELOG.md +++ b/BETA-CHANGELOG.md @@ -1,5 +1,9 @@ This is a changelog that is separate from the main changelog, this is to log changes on the beta branch +2024-01-22 (b0.0.5) +------------------- +- Added an advanced example for creating a server that can handle simultaneous incoming requests from multiple clients + 2023-12-04 (b0.0.4) ------------------- - Changed interplay between `LinkLayer` and `MagicDistributor` so that completion of a delivery in the `LinkLayer` happens on completion of label_delivery instead of state_delivery diff --git a/examples/advanced/server/application.py b/examples/advanced/server/application.py new file mode 100644 index 00000000..2aa21e24 --- /dev/null +++ b/examples/advanced/server/application.py @@ -0,0 +1,106 @@ +from typing import List + +from netqasm.sdk.connection import BaseNetQASMConnection +from netsquid_netbuilder.logger import LogManager +from netsquid_protocols import CSocketListener, QueueProtocol, SleepingProtocol + +from squidasm.sim.stack.program import Program, ProgramContext, ProgramMeta + +REQUEST_EPR_PAIR_GENERATION_MSG = "Request to initiate EPR pair generation" +CONFIRM_EPR_PAIR_GENERATION_MSG = "confirmation to initiate EPR pair generation" + + +class ClientProgram(Program): + def __init__(self, node_name: str, server_name: str, request_start_time=0): + self.node_name = node_name + self.server_name = server_name + self.logger = LogManager.get_stack_logger(f"Program {self.node_name}") + self.request_start_time = request_start_time + + @property + def meta(self) -> ProgramMeta: + return ProgramMeta( + name="tutorial_program", + csockets=[self.server_name], + epr_sockets=[self.server_name], + max_qubits=1, + ) + + def run(self, context: ProgramContext): + csocket = context.csockets[self.server_name] + epr_socket = context.epr_sockets[self.server_name] + connection = context.connection + + # Wait to the desired time using a WaitingProtocol + waiting_protocol = SleepingProtocol() + yield from waiting_protocol.sleep(end_time=self.request_start_time) + + # submit the request to the server + message = REQUEST_EPR_PAIR_GENERATION_MSG + csocket.send(message) + self.logger.info(f"sending message {message} to {self.server_name}") + + # wait for confirmation from the server + message = yield from csocket.recv() + self.logger.info(f"received message {message} from {self.server_name}") + if message != CONFIRM_EPR_PAIR_GENERATION_MSG: + raise RuntimeError(f"Expected confirmation but received: {message}") + + # Execute the request + epr_qubit = epr_socket.create_keep()[0] + epr_qubit.H() + result = epr_qubit.measure() + yield from connection.flush() + self.logger.info(f"measures local EPR qubit: {result}") + + return {} + + +class ServerProgram(Program): + def __init__(self, clients: List[str]): + self.clients = clients + self.node_name = "server" + self.logger = LogManager.get_stack_logger(f"Program {self.node_name}") + + @property + def meta(self) -> ProgramMeta: + return ProgramMeta( + name="tutorial_program", + csockets=self.clients, + epr_sockets=self.clients, + max_qubits=1, + ) + + def run(self, context: ProgramContext): + connection: BaseNetQASMConnection = context.connection + + # We use the Queue protocol to manage the queue and signal for new queue items + queue_protocol = QueueProtocol() + queue_protocol.start() + + # Set up a CSocketListener for each client that will forward requests to the queue + for client in self.clients: + thread = CSocketListener(context, client, queue_protocol, self.logger) + thread.start() + + while True: + # Wait for a new request if applicable and process the next item in the queue + client_name, msg = yield from queue_protocol.pop() + csocket = context.csockets[client_name] + epr_socket = context.epr_sockets[client_name] + + if msg != REQUEST_EPR_PAIR_GENERATION_MSG: + raise RuntimeError(f"Received unsupported request: {msg}") + self.logger.info(f"start processing request from {client_name}") + + # Send confirmation to the client + message = CONFIRM_EPR_PAIR_GENERATION_MSG + csocket.send(message) + self.logger.info(f"sending message {message} to {client_name}") + + # Start processing the request + epr_qubit = epr_socket.recv_keep()[0] + epr_qubit.H() + result = epr_qubit.measure() + yield from connection.flush() + self.logger.info(f"measures local EPR qubit: {result}") diff --git a/examples/advanced/server/netsquid_protocols.py b/examples/advanced/server/netsquid_protocols.py new file mode 100644 index 00000000..643e8bfc --- /dev/null +++ b/examples/advanced/server/netsquid_protocols.py @@ -0,0 +1,81 @@ +from logging import Logger +from typing import Generator, Tuple + +from netqasm.sdk.classical_communication.socket import Socket +from netsquid.protocols import Protocol + +from squidasm.sim.stack.program import ProgramContext + + +class SleepingProtocol(Protocol): + """Netsquid protocol to support waiting or sleeping in a Program.""" + + def sleep(self, duration=0, end_time=0): + """ + Sleep for a certain duration or until a certain end time. + Specify either duration or end time, but not both. + Requires usage of `yield from` before method call to function. + + :param duration: Time to sleep in nanoseconds. + :param end_time: Simulation time to sleep to in nanoseconds. + """ + yield self.await_timer(duration=duration, end_time=end_time) + + +class QueueProtocol(Protocol): + """Netsquid protocol for a queue, that supports waiting for a new item, when the queue is empty.""" + + QUEUE_STATUS_CHANGE_SIGNAL = "Queue has been updated" + + def __init__(self): + self._queue = [] + self.add_signal(self.QUEUE_STATUS_CHANGE_SIGNAL) + + def push(self, msg_source: str, msg: str): + """ + Put a new item on the queue. + + :param msg_source: Source of the message. + :param msg: The message. + """ + self._queue.append((msg_source, msg)) + self.send_signal(self.QUEUE_STATUS_CHANGE_SIGNAL) + + def pop(self) -> Generator[None, None, Tuple[str, str]]: + """ + Take an item of the queue. Waits for a new item if the queue is empty. + + :return: A tuple with the source of the message and the message. + """ + if len(self._queue) == 0: + yield self.await_signal(sender=self, signal_label=self.QUEUE_STATUS_CHANGE_SIGNAL) # fmt: skip + return self._queue.pop() + + +class CSocketListener(Protocol): + """Netsquid protocol that listens to messages on a CSocket and forwards them to the QueueProtocol.""" + + def __init__( + self, + context: ProgramContext, + peer_name: str, + queue_protocol: QueueProtocol, + logger: Logger, + ): + """ + :param context: ProgramContext of the current program. + :param peer_name: Name of the remote node this Listener should listen to. + :param queue_protocol: The QueueProtocol where messages are forwarded to. + :param logger: A Logger. + """ + self._context = context + self._peer_name = peer_name + self._queue_protocol = queue_protocol + self.logger = logger.getChild(f"PortListener({peer_name})") + + def run(self): + csocket: Socket = self._context.csockets[self._peer_name] + while True: + message = yield from csocket.recv() + self._queue_protocol.push(self._peer_name, str(message)) + self.logger.info(f"Received message: {message}") diff --git a/examples/advanced/server/run_simulation.py b/examples/advanced/server/run_simulation.py new file mode 100644 index 00000000..0abd4bbe --- /dev/null +++ b/examples/advanced/server/run_simulation.py @@ -0,0 +1,39 @@ +import logging + +import numpy.random +from application import ClientProgram, ServerProgram +from netsquid_magic.models.perfect import PerfectLinkConfig +from netsquid_netbuilder.modules.clinks.default import DefaultCLinkConfig +from netsquid_netbuilder.util.network_generation import create_complete_graph_network + +from squidasm.run.stack.run import run + +num_nodes = 6 +random_request_start_times = False +node_names = [f"Node_{i}" for i in range(num_nodes)] + +cfg = create_complete_graph_network( + node_names, + "perfect", + PerfectLinkConfig(state_delay=100), + clink_typ="default", + clink_cfg=DefaultCLinkConfig(delay=100), +) + +server_name = node_names[0] +client_names = node_names[1:] + +programs = {server_name: ServerProgram(clients=client_names)} +for client in client_names: + programs[client] = ClientProgram(client, server_name) + +# Set log level for all programs +for program in programs.values(): + program.logger.setLevel(logging.INFO) + +if random_request_start_times: + # Set the clients to start their requests at a random time between 0 and 400 ns + for client in client_names: + programs[client].request_start_time = numpy.random.randint(0, 400) + +run(config=cfg, programs=programs, num_times=1)