From 6ad3be6d5380eceed305dd8b3cf006a56a44c1df Mon Sep 17 00:00:00 2001 From: CardamaS99 Date: Mon, 18 Nov 2024 17:01:52 +0100 Subject: [PATCH 1/4] Enhance network generation capabilities with string parsing and connection management --- README.md | 47 ++++++++- examples/network_generated/send_qubit.py | 33 ++++++ qunetsim/__init__.py | 1 + qunetsim/generator/__init__.py | 4 + qunetsim/generator/conn_type.py | 36 +++++++ qunetsim/generator/connection.py | 70 +++++++++++++ qunetsim/generator/direction.py | 28 +++++ qunetsim/generator/generate.py | 127 +++++++++++++++++++++++ 8 files changed, 344 insertions(+), 2 deletions(-) create mode 100644 examples/network_generated/send_qubit.py create mode 100644 qunetsim/generator/__init__.py create mode 100644 qunetsim/generator/conn_type.py create mode 100644 qunetsim/generator/connection.py create mode 100644 qunetsim/generator/direction.py create mode 100644 qunetsim/generator/generate.py diff --git a/README.md b/README.md index 898098ee..7b2fdd10 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The QuNetSim pip package comes with a templater. After installing the library, s ### Quick Example -``` +```python from qunetsim.components import Host, Network network = Network.get_instance() @@ -46,12 +46,55 @@ print("EPR is in state: %d, %d" % (q_alice.measure(), q_bob.measure())) network.stop(True) ``` +### Quick Example: generate a network from a string + +```python +from qunetsim.generator import network_generate +from qunetsim.objects import Qubit + +# Initialize the network and hosts +# Note: we use 'A<==>B' to represent a classical and quantum connection +# we use 'A<-->B' to represent a classical only connection +# we use 'A<~~>B' to represent a quantum only connection +# All connections are added uni-directionally, so '<' and '>' +# represent the direction of the flow of traffic. +network, hosts = network_generate("Alice<=>Bob<~>Eve<=>Dean<->Alice") + +network.start(list(hosts.keys())) + +# Initialize the hosts +for host in hosts.values(): + host.start() + +for _ in range(10): + # Create a qubit owned by Alice + q = Qubit(hosts['Alice']) + # Put the qubit in the excited state + q.H() + # Send the qubit and await an ACK from Dean + q_id, ack_arrived = hosts['Alice'].send_qubit('Dean', q, await_ack=True) + + # Get the qubit on Dean's side from Alice + q_rec = hosts['Dean'].get_qubit('Alice', q_id, wait=0) + + # Ensure the qubit arrived and then measure and print the results. + if q_rec is not None: + m = q_rec.measure() + print("Results of the measurements for q_id are ", str(m)) + else: + print('Qubit did not arrive.') + +network.stop(stop_hosts=True) +``` + + + ## Contributing Feel free to contribute by adding Github issues and pull requests. Adding test cases for any contributions is a requirement for any pull request to be merged. ## Citation -``` +```bibtex @article{diadamo2020qunetsim, title={QuNetSim: A Software Framework for Quantum Networks}, author={DiAdamo, Stephen and N{\"o}tzel, Janis and Zanger, Benjamin and Be{\c{s}}e, Mehmet Mert}, diff --git a/examples/network_generated/send_qubit.py b/examples/network_generated/send_qubit.py new file mode 100644 index 00000000..7c4cb376 --- /dev/null +++ b/examples/network_generated/send_qubit.py @@ -0,0 +1,33 @@ +from qunetsim.generator import network_generate +from qunetsim.objects import Qubit + +network, hosts = network_generate("Alice<==>Bob<~~>Eve<==>Dean<-->Alice") + +network.draw_quantum_network() +network.draw_classical_network() + +network.start(list(hosts.keys())) + +# Initialize the hosts +for host in hosts.values(): + host.start() + +for _ in range(10): + # Create a qubit owned by Alice + q = Qubit(hosts['Alice']) + # Put the qubit in the excited state + q.H() + # Send the qubit and await an ACK from Dean + q_id, ack_arrived = hosts['Alice'].send_qubit('Dean', q, await_ack=True) + + # Get the qubit on Dean's side from Alice + q_rec = hosts['Dean'].get_qubit('Alice', q_id, wait=0) + + # Ensure the qubit arrived and then measure and print the results. + if q_rec is not None: + m = q_rec.measure() + print("Results of the measurements for q_id are ", str(m)) + else: + print('Qubit did not arrive.') + +network.stop(stop_hosts=True) \ No newline at end of file diff --git a/qunetsim/__init__.py b/qunetsim/__init__.py index 28e50161..89c70a50 100644 --- a/qunetsim/__init__.py +++ b/qunetsim/__init__.py @@ -1,2 +1,3 @@ from .components import Host, Network from .objects import * +from .generator import * \ No newline at end of file diff --git a/qunetsim/generator/__init__.py b/qunetsim/generator/__init__.py new file mode 100644 index 00000000..d12f9f10 --- /dev/null +++ b/qunetsim/generator/__init__.py @@ -0,0 +1,4 @@ +""" +The generator package provides functions and classes to translate a string and generate the specified network. +""" +from .generate import network_generate \ No newline at end of file diff --git a/qunetsim/generator/conn_type.py b/qunetsim/generator/conn_type.py new file mode 100644 index 00000000..5cce7067 --- /dev/null +++ b/qunetsim/generator/conn_type.py @@ -0,0 +1,36 @@ +from qunetsim.components import Host + +class ConnType: + """ + Enumeration for the connection types. + """ + CLASSICAL_AND_QUANTUM = 1 + CLASSICAL = 2 + QUANTUM = 3 + + """ + Function to add the connection between the hosts, depending on the connection type. + """ + add_connection_function = { + CLASSICAL_AND_QUANTUM: Host.add_connection, + CLASSICAL: Host.add_c_connection, + QUANTUM: Host.add_q_connection + } + + @staticmethod + def get_conn_type(string): + """ + Get the connection type from the string. + + Args: + string (str): The string representing the connection type. + + Returns: + ConnType: The connection type. + """ + if string == '==': + return ConnType.CLASSICAL_AND_QUANTUM + elif string == '--': + return ConnType.CLASSICAL + elif string == '~~': + return ConnType.QUANTUM \ No newline at end of file diff --git a/qunetsim/generator/connection.py b/qunetsim/generator/connection.py new file mode 100644 index 00000000..383f49d7 --- /dev/null +++ b/qunetsim/generator/connection.py @@ -0,0 +1,70 @@ +from qunetsim.components import Host +from .conn_type import ConnType +from .direction import Direction + +class Connection: + """ + Class representing a connection between two hosts. + """ + + def __init__(self, hostA: Host, hostB: Host, connection_string: str): + """ + Initialize the connection. + + Args: + hostA (Host): The first host. + hostB (Host): The second host. + connection_string (str): The string representing the connection. + """ + self.hostA = hostA + self.hostB = hostB + + self.direction = Direction.get_direction(connection_string) + + if connection_string[0] == '<': + connection_string = connection_string[1:] + if connection_string[-1] == '>': + connection_string = connection_string[:-1] + + self.type = ConnType.get_conn_type(connection_string) + + def add_connection(self): + """ + Add the connection between the hosts. + """ + if self.direction == Direction.UNIDIRECTIONAL_A_B: + ConnType.add_connection_function[self.type](self.hostA, self.hostB.host_id) + elif self.direction == Direction.UNIDIRECTIONAL_B_A: + ConnType.add_connection_function[self.type](self.hostB, self.hostA.host_id) + else: + ConnType.add_connection_function[self.type](self.hostA, self.hostB.host_id) + ConnType.add_connection_function[self.type](self.hostB, self.hostA.host_id) + + def __str__(self): + """ + Get the string representation of the connection. + + Returns: + str: The string representation of the connection. + """ + # Direction to str + start = '' + end = '' + connection_type = '' + if self.direction == Direction.UNIDIRECTIONAL_A_B: + end = '>' + elif self.direction == Direction.UNIDIRECTIONAL_B_A: + start = '<' + else: + start = '<' + end = '>' + + # Connection type to str + if self.type == ConnType.CLASSICAL_AND_QUANTUM: + connection_type = '==' + elif self.type == ConnType.CLASSICAL: + connection_type = '--' + else: + connection_type = '~~' + + return f'{self.hostA.host_id} {start}{connection_type}{end} {self.hostB.host_id}' \ No newline at end of file diff --git a/qunetsim/generator/direction.py b/qunetsim/generator/direction.py new file mode 100644 index 00000000..781627b3 --- /dev/null +++ b/qunetsim/generator/direction.py @@ -0,0 +1,28 @@ +class Direction: + """ + Enumeration for the direction of the connection. + """ + UNIDIRECTIONAL_A_B = 1 # A --> B + UNIDIRECTIONAL_B_A = 2 # A <-- B + BIDIRECTIONAL = 3 # A <-> B + + @staticmethod + def get_direction(string): + """ + Get the direction from the string. + + Args: + string (str): The string representing the connection. + + Returns: + Direction: The direction. + """ + start = string[0] == '<' + end = string[-1] == '>' + + if not (start ^ end): + return Direction.BIDIRECTIONAL + elif start: + return Direction.UNIDIRECTIONAL_B_A + else: + return Direction.UNIDIRECTIONAL_A_B \ No newline at end of file diff --git a/qunetsim/generator/generate.py b/qunetsim/generator/generate.py new file mode 100644 index 00000000..2fae9228 --- /dev/null +++ b/qunetsim/generator/generate.py @@ -0,0 +1,127 @@ +from qunetsim.components import Host, Network +from .connection import Connection + +import re + + +def get_connections(hosts: dict[str, Host], string: str) -> list[Connection]: + """ + Get the connections from the string. + + Args: + hosts (dict[str, Host]): The dictionary of hosts. + string (str): The string representing the connections. + + Returns: + list[Connection]: The list of connections. + """ + # Use the regular expression to get the connections + regex = r'(\w+)(<{0,1}(==|--|~~)>{0,1})(\w+)' + + connections = [] + start = 0 + + while True: + match = re.search(regex, string[start:]) + if not match: + break + + host1 = hosts[match.group(1)] + connection_type = match.group(2) + host2 = hosts[match.group(4)] + + # Append new connection + connections.append(Connection(host1, host2, connection_type)) + + # Update the start index + start += match.start(3) + + # Remove the duplicates + connections = list(dict.fromkeys(connections)) + + return connections + +def get_hosts(string: str) -> dict[str, Host]: + """ + Get the hosts from the string. + + Args: + string (str): The string representing the hosts. + + Returns: + dict[str, Host]: The dictionary of hosts. + """ + # Use the regular expression (\w+) + regex = r'(\w+)' + + hosts = [] + + for match in re.finditer(regex, string): + hosts.append(match.group()) + + # remove the duplicates + hosts = list(dict.fromkeys(hosts)) + + return hosts + +def array_to_host(array): + """ + Convert the array of host string to a host object. + + Args: + array (list): The array representing the host. + + Returns: + list (Host): The list with the host objects. + """ + + # Create one host object for each host in the array + hosts = {host : Host(host) for host in array} + + return hosts + +def network_parser(string) -> tuple[dict[str, Host], list[Connection]]: + """ + Parse the network string and return the network object. + + Args: + string (str): The string representing the network. + + Returns: + tuple[dict[str, Host], list[Connection]]: The hosts and connections. + """ + # Remove the spaces and line breaks + string = string.replace(' ', '') + + # Get the hostnames + string_hosts = get_hosts(string) + + # Create the host objects + hosts = array_to_host(string_hosts) + + # Get the connections + connections = get_connections(hosts, string) + + return hosts, connections + +def network_generate(string): + """ + Generate the network from the string. + + Args: + string (str): The string representing the network. + + Returns: + tuple[Network, dict[str, Host]]: The network and the hosts. + """ + network = Network.get_instance() + hosts, connections = network_parser(string) + + for connection in connections: + connection.add_connection() + + # Append the hosts to the network + for host in hosts.values(): + network.add_host(host) + + return network, hosts From 6ea4ac7f2094e095c979b763f8170618572b2432 Mon Sep 17 00:00:00 2001 From: CardamaS99 Date: Mon, 18 Nov 2024 17:07:55 +0100 Subject: [PATCH 2/4] Fix formatting issues (PEP8) --- examples/network_generated/send_qubit.py | 2 +- qunetsim/generator/__init__.py | 2 +- qunetsim/generator/conn_type.py | 3 ++- qunetsim/generator/connection.py | 17 +++++++++++------ qunetsim/generator/direction.py | 2 +- qunetsim/generator/generate.py | 14 +++++++++----- 6 files changed, 25 insertions(+), 15 deletions(-) diff --git a/examples/network_generated/send_qubit.py b/examples/network_generated/send_qubit.py index 7c4cb376..328d7bdc 100644 --- a/examples/network_generated/send_qubit.py +++ b/examples/network_generated/send_qubit.py @@ -30,4 +30,4 @@ else: print('Qubit did not arrive.') -network.stop(stop_hosts=True) \ No newline at end of file +network.stop(stop_hosts=True) diff --git a/qunetsim/generator/__init__.py b/qunetsim/generator/__init__.py index d12f9f10..bc037d0c 100644 --- a/qunetsim/generator/__init__.py +++ b/qunetsim/generator/__init__.py @@ -1,4 +1,4 @@ """ The generator package provides functions and classes to translate a string and generate the specified network. """ -from .generate import network_generate \ No newline at end of file +from .generate import network_generate diff --git a/qunetsim/generator/conn_type.py b/qunetsim/generator/conn_type.py index 5cce7067..9b7680aa 100644 --- a/qunetsim/generator/conn_type.py +++ b/qunetsim/generator/conn_type.py @@ -1,5 +1,6 @@ from qunetsim.components import Host + class ConnType: """ Enumeration for the connection types. @@ -33,4 +34,4 @@ def get_conn_type(string): elif string == '--': return ConnType.CLASSICAL elif string == '~~': - return ConnType.QUANTUM \ No newline at end of file + return ConnType.QUANTUM diff --git a/qunetsim/generator/connection.py b/qunetsim/generator/connection.py index 383f49d7..7bcd9cf1 100644 --- a/qunetsim/generator/connection.py +++ b/qunetsim/generator/connection.py @@ -2,6 +2,7 @@ from .conn_type import ConnType from .direction import Direction + class Connection: """ Class representing a connection between two hosts. @@ -18,7 +19,7 @@ def __init__(self, hostA: Host, hostB: Host, connection_string: str): """ self.hostA = hostA self.hostB = hostB - + self.direction = Direction.get_direction(connection_string) if connection_string[0] == '<': @@ -33,12 +34,16 @@ def add_connection(self): Add the connection between the hosts. """ if self.direction == Direction.UNIDIRECTIONAL_A_B: - ConnType.add_connection_function[self.type](self.hostA, self.hostB.host_id) + ConnType.add_connection_function[self.type]( + self.hostA, self.hostB.host_id) elif self.direction == Direction.UNIDIRECTIONAL_B_A: - ConnType.add_connection_function[self.type](self.hostB, self.hostA.host_id) + ConnType.add_connection_function[self.type]( + self.hostB, self.hostA.host_id) else: - ConnType.add_connection_function[self.type](self.hostA, self.hostB.host_id) - ConnType.add_connection_function[self.type](self.hostB, self.hostA.host_id) + ConnType.add_connection_function[self.type]( + self.hostA, self.hostB.host_id) + ConnType.add_connection_function[self.type]( + self.hostB, self.hostA.host_id) def __str__(self): """ @@ -67,4 +72,4 @@ def __str__(self): else: connection_type = '~~' - return f'{self.hostA.host_id} {start}{connection_type}{end} {self.hostB.host_id}' \ No newline at end of file + return f'{self.hostA.host_id} {start}{connection_type}{end} {self.hostB.host_id}' diff --git a/qunetsim/generator/direction.py b/qunetsim/generator/direction.py index 781627b3..0c7ef566 100644 --- a/qunetsim/generator/direction.py +++ b/qunetsim/generator/direction.py @@ -25,4 +25,4 @@ def get_direction(string): elif start: return Direction.UNIDIRECTIONAL_B_A else: - return Direction.UNIDIRECTIONAL_A_B \ No newline at end of file + return Direction.UNIDIRECTIONAL_A_B diff --git a/qunetsim/generator/generate.py b/qunetsim/generator/generate.py index 2fae9228..6f8712e6 100644 --- a/qunetsim/generator/generate.py +++ b/qunetsim/generator/generate.py @@ -32,7 +32,7 @@ def get_connections(hosts: dict[str, Host], string: str) -> list[Connection]: # Append new connection connections.append(Connection(host1, host2, connection_type)) - + # Update the start index start += match.start(3) @@ -41,6 +41,7 @@ def get_connections(hosts: dict[str, Host], string: str) -> list[Connection]: return connections + def get_hosts(string: str) -> dict[str, Host]: """ Get the hosts from the string. @@ -64,6 +65,7 @@ def get_hosts(string: str) -> dict[str, Host]: return hosts + def array_to_host(array): """ Convert the array of host string to a host object. @@ -74,12 +76,13 @@ def array_to_host(array): Returns: list (Host): The list with the host objects. """ - + # Create one host object for each host in the array - hosts = {host : Host(host) for host in array} - + hosts = {host: Host(host) for host in array} + return hosts + def network_parser(string) -> tuple[dict[str, Host], list[Connection]]: """ Parse the network string and return the network object. @@ -104,6 +107,7 @@ def network_parser(string) -> tuple[dict[str, Host], list[Connection]]: return hosts, connections + def network_generate(string): """ Generate the network from the string. @@ -119,7 +123,7 @@ def network_generate(string): for connection in connections: connection.add_connection() - + # Append the hosts to the network for host in hosts.values(): network.add_host(host) From 0015d287b0a98883cabfd46ce74ca439148fd143 Mon Sep 17 00:00:00 2001 From: CardamaS99 Date: Wed, 22 Jan 2025 14:19:14 +0100 Subject: [PATCH 3/4] Enhance connection parsing in get_connections() to support multiple hosts and update get_hosts() return type --- qunetsim/generator/generate.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/qunetsim/generator/generate.py b/qunetsim/generator/generate.py index 6f8712e6..81fda24b 100644 --- a/qunetsim/generator/generate.py +++ b/qunetsim/generator/generate.py @@ -16,7 +16,7 @@ def get_connections(hosts: dict[str, Host], string: str) -> list[Connection]: list[Connection]: The list of connections. """ # Use the regular expression to get the connections - regex = r'(\w+)(<{0,1}(==|--|~~)>{0,1})(\w+)' + regex = r'(\{(?:\w+,)*\w+\}|\w+)(<{0,1}(?:==|--|~~)>{0,1})(\{(?:\w+,)*\w+\}|\w+)' connections = [] start = 0 @@ -26,12 +26,24 @@ def get_connections(hosts: dict[str, Host], string: str) -> list[Connection]: if not match: break - host1 = hosts[match.group(1)] + # Get the hosts from the left and right side + hosts_left = get_hosts(match.group(1)) + hosts_right = get_hosts(match.group(3)) connection_type = match.group(2) - host2 = hosts[match.group(4)] - # Append new connection - connections.append(Connection(host1, host2, connection_type)) + print(f"Match 1: {match.group(1)}, Match 2: {match.group(2)}, Match 3: {match.group(3)}") + + print(f"Hosts left: {hosts_left}") + print(f"Hosts right: {hosts_right}") + + # Append the new connections + for host_left in hosts_left: + for host_right in hosts_right: + host1 = hosts[host_left] + host2 = hosts[host_right] + connections.append(Connection(host1, host2, connection_type)) + + print(f"Connection between {host1} and {host2} of type {connection_type}") # Update the start index start += match.start(3) @@ -42,7 +54,7 @@ def get_connections(hosts: dict[str, Host], string: str) -> list[Connection]: return connections -def get_hosts(string: str) -> dict[str, Host]: +def get_hosts(string: str) -> list[str]: """ Get the hosts from the string. @@ -50,7 +62,7 @@ def get_hosts(string: str) -> dict[str, Host]: string (str): The string representing the hosts. Returns: - dict[str, Host]: The dictionary of hosts. + list[str]: The list of hosts. """ # Use the regular expression (\w+) regex = r'(\w+)' @@ -108,7 +120,7 @@ def network_parser(string) -> tuple[dict[str, Host], list[Connection]]: return hosts, connections -def network_generate(string): +def network_generate(string : str) -> tuple[Network, dict[str, Host]]: """ Generate the network from the string. From 6918c7c3d675832507afdd0fb6d698c322935f22 Mon Sep 17 00:00:00 2001 From: CardamaS99 Date: Wed, 22 Jan 2025 14:30:49 +0100 Subject: [PATCH 4/4] Remove debug print statements from get_connections() for cleaner output --- qunetsim/generator/generate.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/qunetsim/generator/generate.py b/qunetsim/generator/generate.py index 81fda24b..6be093ce 100644 --- a/qunetsim/generator/generate.py +++ b/qunetsim/generator/generate.py @@ -31,11 +31,6 @@ def get_connections(hosts: dict[str, Host], string: str) -> list[Connection]: hosts_right = get_hosts(match.group(3)) connection_type = match.group(2) - print(f"Match 1: {match.group(1)}, Match 2: {match.group(2)}, Match 3: {match.group(3)}") - - print(f"Hosts left: {hosts_left}") - print(f"Hosts right: {hosts_right}") - # Append the new connections for host_left in hosts_left: for host_right in hosts_right: @@ -43,8 +38,6 @@ def get_connections(hosts: dict[str, Host], string: str) -> list[Connection]: host2 = hosts[host_right] connections.append(Connection(host1, host2, connection_type)) - print(f"Connection between {host1} and {host2} of type {connection_type}") - # Update the start index start += match.start(3)