From 078fbcdac2cb21908710a40c56d95d1352d53037 Mon Sep 17 00:00:00 2001 From: grimmfl Date: Wed, 31 May 2023 10:08:54 +0200 Subject: [PATCH 1/6] Show basic graphs --- PONS.ipynb | 15 ++++++- sim.py | 2 +- ui/__init__.py | 0 ui/app.py | 104 ++++++++++++++++++++++++++++++++++++++++++++ ui/requirements.txt | 2 + 5 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 ui/__init__.py create mode 100644 ui/app.py create mode 100644 ui/requirements.txt diff --git a/PONS.ipynb b/PONS.ipynb index 863c01d..26d3aa8 100644 --- a/PONS.ipynb +++ b/PONS.ipynb @@ -16,13 +16,26 @@ "parameters" ] }, - "outputs": [], + "outputs": [ + { + "ename": "ModuleNotFoundError", + "evalue": "No module named 'matplotlib'", + "output_type": "error", + "traceback": [ + "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", + "\u001B[0;31mModuleNotFoundError\u001B[0m Traceback (most recent call last)", + "Cell \u001B[0;32mIn[1], line 10\u001B[0m\n\u001B[1;32m 8\u001B[0m \u001B[38;5;28;01mimport\u001B[39;00m \u001B[38;5;21;01mpandas\u001B[39;00m \u001B[38;5;28;01mas\u001B[39;00m \u001B[38;5;21;01mpd\u001B[39;00m\n\u001B[1;32m 9\u001B[0m \u001B[38;5;28;01mimport\u001B[39;00m \u001B[38;5;21;01mnumpy\u001B[39;00m \u001B[38;5;28;01mas\u001B[39;00m \u001B[38;5;21;01mnp\u001B[39;00m\n\u001B[0;32m---> 10\u001B[0m \u001B[38;5;28;01mimport\u001B[39;00m \u001B[38;5;21;01mmatplotlib\u001B[39;00m\u001B[38;5;21;01m.\u001B[39;00m\u001B[38;5;21;01mpyplot\u001B[39;00m \u001B[38;5;28;01mas\u001B[39;00m \u001B[38;5;21;01mplt\u001B[39;00m\n\u001B[1;32m 11\u001B[0m \u001B[38;5;28;01mimport\u001B[39;00m \u001B[38;5;21;01mseaborn\u001B[39;00m \u001B[38;5;28;01mas\u001B[39;00m \u001B[38;5;21;01msns\u001B[39;00m\n\u001B[1;32m 13\u001B[0m \u001B[38;5;28;01mimport\u001B[39;00m \u001B[38;5;21;01mpons\u001B[39;00m\n", + "\u001B[0;31mModuleNotFoundError\u001B[0m: No module named 'matplotlib'" + ] + } + ], "source": [ "import random\n", "import math\n", "import time\n", "import simpy\n", "import json\n", + "\n", "import copy\n", "import pandas as pd\n", "import numpy as np\n", diff --git a/sim.py b/sim.py index b99dc0f..af735e1 100644 --- a/sim.py +++ b/sim.py @@ -23,7 +23,7 @@ SIM_TIME, NUM_NODES, WORLD_SIZE[0], WORLD_SIZE[1], max_pause=60.0) net = pons.NetworkSettings("WIFI_50m", range=NET_RANGE) -epidemic = pons.routing.EpidemicRouter(capacity=CAPACITY) +epidemic = pons.routing.PRoPHETRouter(capacity=CAPACITY) nodes = pons.generate_nodes( NUM_NODES, net=[net], router=epidemic) diff --git a/ui/__init__.py b/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ui/app.py b/ui/app.py new file mode 100644 index 0000000..b859d2a --- /dev/null +++ b/ui/app.py @@ -0,0 +1,104 @@ +# Import packages +import copy +import random + +import pandas as pd +import plotly.express as px +import plotly.graph_objects as go +from dash import Dash, html, dash_table, dcc +import dash_bootstrap_components as dbc + +import pons +import pons.routing as pr + +# Settings +SIM_TIME = 3600 +NET_RANGE = 100 +NUM_NODES = 10 +WORLD_SIZE = (1000, 1000) +RUNS = 2 # TODO 10 +MSG_GEN_INTERVAL = (20, 40) +MSG_SIZE = (150, 512) + +ROUTERS = [pr.EpidemicRouter(), pr.SprayAndWaitRouter(copies=7), pr.SprayAndWaitRouter(copies=7, binary=True), + pr.DirectDeliveryRouter(), pr.FirstContactRouter()] + +# Run Simulation +net_stats = [] +routing_stats = [] +for router in ROUTERS: + print("router: %s" % router) + for run in range(RUNS): + random.seed(run) + print("run", run + 1) + + moves = pons.generate_randomwaypoint_movement(SIM_TIME, + NUM_NODES, + WORLD_SIZE[0], + WORLD_SIZE[1], + min_speed=1.0, + max_speed=3.0, + max_pause=120.0) + net = pons.NetworkSettings("WIFI", range=NET_RANGE) + + nodes = pons.generate_nodes(NUM_NODES, net=[net], router=copy.deepcopy(router)) + config = {"movement_logger": False, "peers_logger": False} + + msggenconfig = {"interval": MSG_GEN_INTERVAL, "src": (0, NUM_NODES), "dst": (0, NUM_NODES), "size": MSG_SIZE, + "id": "M"} + + netsim = pons.NetSim(SIM_TIME, WORLD_SIZE, nodes, moves, config=config, msggens=[msggenconfig]) + + netsim.setup() + + netsim.run() + + ns = copy.deepcopy(netsim.net_stats) + ns['router'] = "" + str(router) + net_stats.append(ns) + rs = copy.deepcopy(netsim.routing_stats) + rs['router'] = "" + str(router) + routing_stats.append(rs) + +df_routing = pd.DataFrame.from_dict(routing_stats, orient='columns') + + +def get_figure(attribute: str): + fig = go.Figure(layout={"template": "plotly_dark", + "modebar": {"remove": ["zoom", "pan", "zoomin", "zoomout", "autoscale", "resetscale"]}, + 'xaxis': {'title': 'Routing Protocol', + 'visible': True, + 'showticklabels': True}, + 'yaxis': {'title': attribute, + 'visible': True, + 'showticklabels': True} + }) + for router in ROUTERS: + name = str(router) + fig.add_trace(go.Violin(x=df_routing["router"][df_routing["router"] == name], + y=df_routing[attribute][df_routing["router"] == name], + name=name, + box_visible=True, + meanline_visible=True, hoverinfo="skip", showlegend=False)) + + return fig + + +ATTRIBUTES = ["delivery_prob", "latency_avg", "hops_avg", "overhead_ratio"] +figures = [get_figure(attribute) for attribute in ATTRIBUTES] +elements = [dbc.Col(dcc.Graph(figure=fig), width=6) for fig in figures] + +# Initialize the app +app = Dash(__name__, external_stylesheets=[dbc.themes.SLATE]) + +# App layout +app.layout = html.Div([ + # html.Div(children='My First App with Data'), + # dash_table.DataTable(data=net_stats, page_size=10), + # dash_table.DataTable(data=routing_stats, page_size=10), + dbc.Row(elements) +], className="bg-dark") + +# Run the app +if __name__ == '__main__': + app.run_server(debug=True) diff --git a/ui/requirements.txt b/ui/requirements.txt new file mode 100644 index 0000000..a33921a --- /dev/null +++ b/ui/requirements.txt @@ -0,0 +1,2 @@ +dash-bootstrap-components +dash \ No newline at end of file From b9ac748350a94ff3d6ce6ba7f7d3ae397bc0a026 Mon Sep 17 00:00:00 2001 From: fgrimm Date: Thu, 8 Jun 2023 17:56:11 +0200 Subject: [PATCH 2/6] Animation with correct size and node count slider --- ui/__init__.py | 2 + ui/animation.py | 56 ++++++++++++++++++++++++ ui/app.py | 104 -------------------------------------------- ui/data.py | 81 ++++++++++++++++++++++++++++++++++ ui/requirements.txt | 3 +- ui/ui.py | 96 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 237 insertions(+), 105 deletions(-) create mode 100644 ui/animation.py delete mode 100644 ui/app.py create mode 100644 ui/data.py create mode 100644 ui/ui.py diff --git a/ui/__init__.py b/ui/__init__.py index e69de29..7593f49 100644 --- a/ui/__init__.py +++ b/ui/__init__.py @@ -0,0 +1,2 @@ +from animation import get_figure, set_speed +from data import get_dataframe \ No newline at end of file diff --git a/ui/animation.py b/ui/animation.py new file mode 100644 index 0000000..a3574e4 --- /dev/null +++ b/ui/animation.py @@ -0,0 +1,56 @@ +from typing import Dict + +import pandas as pd +import plotly.express as px + + +WORLD_SIZE = (1000, 1000) +SIZEREF = (586.) / (1000.) + + +def get_figure(dataframe: pd.DataFrame, settings: Dict): + fig = px.scatter(dataframe, + x="x", + y="y", + animation_frame="time", + animation_group="node", + hover_name="node", + range_x=[0, WORLD_SIZE[0]], + range_y=[0, WORLD_SIZE[1]], + color_discrete_sequence=["rgba(0, 204, 0, 0.5)"]) + set_speed(fig, settings) + fig.update_layout(width=800, height=800) # TODO scale this by x and y + fig.update_yaxes(scaleanchor="x", scaleratio=1) + fig.update_traces(marker={"size": 50 * SIZEREF}) + fig.update_geos(projection_type="equirectangular", visible=True, resolution=110) + return fig + + + +def set_speed(figure, settings: Dict): + speed_factor = settings["SPEED_FACTOR"] + print(figure.layout.updatemenus[0]) + figure.update_layout(width=800, + height=800, + updatemenus=[ + { + 'buttons': [{'args': [None, {'frame': {'duration': 30 * speed_factor, 'redraw': False}, + 'mode': 'immediate', 'fromcurrent': True, 'transition': + {'duration': 5 * speed_factor, 'easing': 'linear'}}], + 'label': '▶', + 'method': 'animate'}, + {'args': [[None], {'frame': {'duration': 0, 'redraw': False}, + 'mode': 'immediate', 'fromcurrent': True, 'transition': + {'duration': 0, 'easing': 'linear'}}], + 'label': '◼', + 'method': 'animate'}], + 'direction': 'left', + 'pad': {'r': 10, 't': 70}, + 'showactive': False, + 'type': 'buttons', + 'x': 0.1, + 'xanchor': 'right', + 'y': 0, + 'yanchor': 'top' + } + ]) \ No newline at end of file diff --git a/ui/app.py b/ui/app.py deleted file mode 100644 index b859d2a..0000000 --- a/ui/app.py +++ /dev/null @@ -1,104 +0,0 @@ -# Import packages -import copy -import random - -import pandas as pd -import plotly.express as px -import plotly.graph_objects as go -from dash import Dash, html, dash_table, dcc -import dash_bootstrap_components as dbc - -import pons -import pons.routing as pr - -# Settings -SIM_TIME = 3600 -NET_RANGE = 100 -NUM_NODES = 10 -WORLD_SIZE = (1000, 1000) -RUNS = 2 # TODO 10 -MSG_GEN_INTERVAL = (20, 40) -MSG_SIZE = (150, 512) - -ROUTERS = [pr.EpidemicRouter(), pr.SprayAndWaitRouter(copies=7), pr.SprayAndWaitRouter(copies=7, binary=True), - pr.DirectDeliveryRouter(), pr.FirstContactRouter()] - -# Run Simulation -net_stats = [] -routing_stats = [] -for router in ROUTERS: - print("router: %s" % router) - for run in range(RUNS): - random.seed(run) - print("run", run + 1) - - moves = pons.generate_randomwaypoint_movement(SIM_TIME, - NUM_NODES, - WORLD_SIZE[0], - WORLD_SIZE[1], - min_speed=1.0, - max_speed=3.0, - max_pause=120.0) - net = pons.NetworkSettings("WIFI", range=NET_RANGE) - - nodes = pons.generate_nodes(NUM_NODES, net=[net], router=copy.deepcopy(router)) - config = {"movement_logger": False, "peers_logger": False} - - msggenconfig = {"interval": MSG_GEN_INTERVAL, "src": (0, NUM_NODES), "dst": (0, NUM_NODES), "size": MSG_SIZE, - "id": "M"} - - netsim = pons.NetSim(SIM_TIME, WORLD_SIZE, nodes, moves, config=config, msggens=[msggenconfig]) - - netsim.setup() - - netsim.run() - - ns = copy.deepcopy(netsim.net_stats) - ns['router'] = "" + str(router) - net_stats.append(ns) - rs = copy.deepcopy(netsim.routing_stats) - rs['router'] = "" + str(router) - routing_stats.append(rs) - -df_routing = pd.DataFrame.from_dict(routing_stats, orient='columns') - - -def get_figure(attribute: str): - fig = go.Figure(layout={"template": "plotly_dark", - "modebar": {"remove": ["zoom", "pan", "zoomin", "zoomout", "autoscale", "resetscale"]}, - 'xaxis': {'title': 'Routing Protocol', - 'visible': True, - 'showticklabels': True}, - 'yaxis': {'title': attribute, - 'visible': True, - 'showticklabels': True} - }) - for router in ROUTERS: - name = str(router) - fig.add_trace(go.Violin(x=df_routing["router"][df_routing["router"] == name], - y=df_routing[attribute][df_routing["router"] == name], - name=name, - box_visible=True, - meanline_visible=True, hoverinfo="skip", showlegend=False)) - - return fig - - -ATTRIBUTES = ["delivery_prob", "latency_avg", "hops_avg", "overhead_ratio"] -figures = [get_figure(attribute) for attribute in ATTRIBUTES] -elements = [dbc.Col(dcc.Graph(figure=fig), width=6) for fig in figures] - -# Initialize the app -app = Dash(__name__, external_stylesheets=[dbc.themes.SLATE]) - -# App layout -app.layout = html.Div([ - # html.Div(children='My First App with Data'), - # dash_table.DataTable(data=net_stats, page_size=10), - # dash_table.DataTable(data=routing_stats, page_size=10), - dbc.Row(elements) -], className="bg-dark") - -# Run the app -if __name__ == '__main__': - app.run_server(debug=True) diff --git a/ui/data.py b/ui/data.py new file mode 100644 index 0000000..5d17518 --- /dev/null +++ b/ui/data.py @@ -0,0 +1,81 @@ +import random +from typing import Dict + +import pandas as pd + +import pons + +RANDOM_SEED = 42 +# SIM_TIME = 3600*24*7 +# SIM_TIME = 3600*24 +SIM_TIME = 3600 +NET_RANGE = 50 +WORLD_SIZE = (1000, 1000) +CAPACITY = 10000 + + +random.seed(RANDOM_SEED) + + +def generate_movement(settings: Dict): + return pons.generate_randomwaypoint_movement(SIM_TIME, + settings["NUM_NODES"], + WORLD_SIZE[0], + WORLD_SIZE[1], + max_pause=60.0) + + +def simulate(moves, settings: Dict): + num_nodes = settings["NUM_NODES"] + net = pons.NetworkSettings("WIFI_50m", range=NET_RANGE) + epidemic = pons.routing.EpidemicRouter(capacity=CAPACITY) + + nodes = pons.generate_nodes( + num_nodes, net=[net], router=epidemic) + config = {"movement_logger": False, "peers_logger": False} + + msggenconfig = {"type": "single", "interval": 30, "src": ( + 0, num_nodes), "dst": (0, num_nodes), "size": 100, "id": "M", "ttl": 3600} + + netsim = pons.NetSim(SIM_TIME, WORLD_SIZE, nodes, moves, + config=config, msggens=[msggenconfig]) + + +def get_dataframe(settings: Dict): + data = generate_movement(settings) + missing = [(float(t), n) for t in range(SIM_TIME) for n in range(settings["NUM_NODES"])] + d = {} + index = 0 + result = {"time": {}, "node": {}, "x": {}, "y": {}, "range": {}} + for entry in data: + time = entry[0] + node = entry[1] + x = entry[2] + y = entry[3] + if time in d: + d[time][node] = (x, y) + else: + d[time] = {node: (x, y)} + result["time"][index] = time + result["node"][index] = node + result["x"][index] = x + result["y"][index] = y + result["range"][index] = NET_RANGE + index += 1 + missing.remove((time, node)) + + missing = sorted(missing, key=lambda m: m[0]) + + for (time, node) in missing: + value = d[time - 1][node] + if time not in d: + d[time] = {} + d[time][node] = value + result["time"][index] = time + result["node"][index] = node + result["x"][index] = value[0] + result["y"][index] = value[1] + result["range"][index] = NET_RANGE + index += 1 + + return pd.DataFrame.from_dict(result) \ No newline at end of file diff --git a/ui/requirements.txt b/ui/requirements.txt index a33921a..dc147dd 100644 --- a/ui/requirements.txt +++ b/ui/requirements.txt @@ -1,2 +1,3 @@ dash-bootstrap-components -dash \ No newline at end of file +dash +plotly \ No newline at end of file diff --git a/ui/ui.py b/ui/ui.py new file mode 100644 index 0000000..e65e71b --- /dev/null +++ b/ui/ui.py @@ -0,0 +1,96 @@ +import dash_bootstrap_components as dbc +from dash import Dash, html, dcc, no_update, Input, Output, callback +from dash_bootstrap_components import Col, Row + +from ui import get_figure, get_dataframe, set_speed + + +MAX_SPEED_FACTOR = 10. + + +class UI(Dash): + def __init__(self): + super().__init__(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) + self._register_callbacks() + self.settings = { + "NUM_NODES": 10, + "SPEED_FACTOR": 1. + } + df = get_dataframe(self.settings) + self.fig = get_figure(df, self.settings) + + self.layout = html.Div([ + # dcc.Graph(figure=fig), + Row([ + Col(html.Center( + dcc.Loading(id="loading", children=[ + dcc.Graph(id="plot", figure=self.fig)], + type="default")), width=8), + Col( + html.Div([ + html.Div([ + html.Span('Number of nodes', style={'display': 'inline-block', 'margin-right': 20}), + dcc.Input(id="num_nodes", + type="range", + min=1, + max=10, + step=1, + value=self.settings["NUM_NODES"], + debounce=True, + style={"display": "inline-block"}), + html.Span('', id="num_nodes_display", style={'display': 'inline-block', 'margin-left': 20}), + ]), # number of nodes input + html.Div([ + html.Span('Speed', style={'display': 'inline-block', 'margin-right': 20}), + dcc.Input(id="speed_factor", + type="range", + min=0.05, + max=10, + step=0.05, + value=self.settings["SPEED_FACTOR"], + debounce=True, + style={"display": "inline-block"}), + html.Span('', + id="speed_factor_display", + style={'display': 'inline-block', 'margin-left': 20}), + ], style={ "visibility": "hidden"}), # speed factor input + ], className="mt-5"), + width=4), + # dbc.Col(dcc.Input(type="range"), width=4) + ]), + ]) + + self.run(debug=True) + + def _register_callbacks(self): + self.callback( + Output("num_nodes_display", "children"), + Input("num_nodes", "value") + )(self.on_num_nodes_for_display) + self.callback( + Output("plot", "figure"), + Input("num_nodes", "value") + )(self.on_num_nodes_for_plot) + self.callback( + Output("speed_factor_display", "children"), + Input("speed_factor", "value") + )(self.on_speed_factor) + + def on_speed_factor(self, factor): + self.settings["SPEED_FACTOR"] = MAX_SPEED_FACTOR - float(factor) + set_speed(self.fig, self.settings) + return factor + + def on_num_nodes_for_display(self, num_nodes): + return num_nodes + + def on_num_nodes_for_plot(self, num_nodes): + if num_nodes == self.settings["NUM_NODES"]: + return no_update + self.settings["NUM_NODES"] = int(num_nodes) + df = get_dataframe(self.settings) + self.fig = get_figure(df, self.settings) + return self.fig + + +a = UI() From 9cdaec73cac261e9ba6582eca4ca1b1b4c291076 Mon Sep 17 00:00:00 2001 From: fgrimm Date: Sat, 10 Jun 2023 11:05:56 +0200 Subject: [PATCH 3/6] Node size and refactor --- pons/simulation.py | 22 ++++++++++++++ ui/__init__.py | 2 +- ui/animation.py | 9 ++++-- ui/{ui.py => app.py} | 72 +++++++++++++++++++++++++++++--------------- ui/data.py | 5 +-- 5 files changed, 78 insertions(+), 32 deletions(-) rename ui/{ui.py => app.py} (55%) diff --git a/pons/simulation.py b/pons/simulation.py index 4a779d4..0fa0b2b 100644 --- a/pons/simulation.py +++ b/pons/simulation.py @@ -1,6 +1,27 @@ +from typing import List + import simpy import time import pons +from enum import Enum + + +class EventType(Enum): + SENT = 0 + + +#class Event: +# """Simulation Events""" +# def __init__(self, type: EventType, node1: pons.Node, node2: pons.Node, message: pons.Message): +# self.type: EventType = type +# self.node1: pons.Node = node1 +# self.node2: pons.Node = node2 +# self.message: pons.Message = message +# +# def __str__(self): +# if self.type == EventType.SENT: +# return f"{self.node1} sent {self.message} to {self.node2}" +# raise NotImplementedError(f"{self.type} can not be converted to str") class NetSim(object): @@ -21,6 +42,7 @@ def __init__(self, duration, world, nodes, movements=[], msggens=None, config=No 'latency': 0.0, 'started': 0, 'relayed': 0, 'removed': 0, 'aborted': 0, 'dups': 0, 'latency_avg': 0.0, 'delivery_prob': 0.0, 'hops_avg': 0.0, 'overhead_ratio': 0.0} self.router_stats = {} + #self.events: List[Event] = [] self.mover = pons.OneMovementManager( self.env, self.nodes, self.movements) diff --git a/ui/__init__.py b/ui/__init__.py index 7593f49..5079199 100644 --- a/ui/__init__.py +++ b/ui/__init__.py @@ -1,2 +1,2 @@ -from animation import get_figure, set_speed +from animation import get_figure, set_speed, update_net_range from data import get_dataframe \ No newline at end of file diff --git a/ui/animation.py b/ui/animation.py index a3574e4..5046fc3 100644 --- a/ui/animation.py +++ b/ui/animation.py @@ -18,14 +18,19 @@ def get_figure(dataframe: pd.DataFrame, settings: Dict): range_x=[0, WORLD_SIZE[0]], range_y=[0, WORLD_SIZE[1]], color_discrete_sequence=["rgba(0, 204, 0, 0.5)"]) - set_speed(fig, settings) + #set_speed(fig, settings) + fig.layout.updatemenus[0].buttons[0].args[1]["frame"]["duration"] = 30 + fig.layout.updatemenus[0].buttons[0].args[1]["transition"]["duration"] = 5 fig.update_layout(width=800, height=800) # TODO scale this by x and y fig.update_yaxes(scaleanchor="x", scaleratio=1) - fig.update_traces(marker={"size": 50 * SIZEREF}) + fig.update_traces(marker={"size": settings["NET_RANGE"] * SIZEREF}) fig.update_geos(projection_type="equirectangular", visible=True, resolution=110) return fig +def update_net_range(fig, settings: Dict): + fig.update_traces(marker={"size": settings["NET_RANGE"] * SIZEREF}) + def set_speed(figure, settings: Dict): speed_factor = settings["SPEED_FACTOR"] diff --git a/ui/ui.py b/ui/app.py similarity index 55% rename from ui/ui.py rename to ui/app.py index e65e71b..fead32b 100644 --- a/ui/ui.py +++ b/ui/app.py @@ -1,23 +1,24 @@ import dash_bootstrap_components as dbc -from dash import Dash, html, dcc, no_update, Input, Output, callback +from dash import Dash, html, dcc, no_update, Input, Output from dash_bootstrap_components import Col, Row -from ui import get_figure, get_dataframe, set_speed - +from ui import get_figure, get_dataframe, set_speed, update_net_range MAX_SPEED_FACTOR = 10. +SIZEREF = (586.) / (1000.) -class UI(Dash): +class App(Dash): def __init__(self): super().__init__(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) self._register_callbacks() self.settings = { "NUM_NODES": 10, - "SPEED_FACTOR": 1. + "SPEED_FACTOR": 1., + "NET_RANGE": 50 } - df = get_dataframe(self.settings) - self.fig = get_figure(df, self.settings) + self.df = get_dataframe(self.settings) + self.fig = get_figure(self.df, self.settings) self.layout = html.Div([ # dcc.Graph(figure=fig), @@ -38,8 +39,20 @@ def __init__(self): value=self.settings["NUM_NODES"], debounce=True, style={"display": "inline-block"}), - html.Span('', id="num_nodes_display", style={'display': 'inline-block', 'margin-left': 20}), + html.Span(self.settings["NUM_NODES"], id="num_nodes_display", style={'display': 'inline-block', 'margin-left': 20}), ]), # number of nodes input + html.Div([ + html.Span('Net range', style={'display': 'inline-block', 'margin-right': 20}), + dcc.Input(id="net_range", + type="range", + min=20, + max=150, + step=5, + value=self.settings["NET_RANGE"], + debounce=True, + style={"display": "inline-block"}), + html.Span(self.settings["NET_RANGE"], id="net_range_display", style={'display': 'inline-block', 'margin-left': 20}), + ]), # net range input html.Div([ html.Span('Speed', style={'display': 'inline-block', 'margin-right': 20}), dcc.Input(id="speed_factor", @@ -64,33 +77,42 @@ def __init__(self): def _register_callbacks(self): self.callback( - Output("num_nodes_display", "children"), - Input("num_nodes", "value") - )(self.on_num_nodes_for_display) - self.callback( - Output("plot", "figure"), - Input("num_nodes", "value") - )(self.on_num_nodes_for_plot) + [Output("num_nodes_display", "children"), Output("plot", "figure", allow_duplicate=True)], + Input("num_nodes", "value"), + prevent_initial_call=True + )(self.on_num_nodes) self.callback( Output("speed_factor_display", "children"), - Input("speed_factor", "value") + Input("speed_factor", "value"), + prevent_initial_call=True )(self.on_speed_factor) + self.callback( + [Output("net_range_display", "children"), Output("plot", "figure", allow_duplicate=True)], + Input("net_range", "value"), + prevent_initial_call=True + )(self.on_net_range) def on_speed_factor(self, factor): self.settings["SPEED_FACTOR"] = MAX_SPEED_FACTOR - float(factor) set_speed(self.fig, self.settings) return factor - def on_num_nodes_for_display(self, num_nodes): - return num_nodes - - def on_num_nodes_for_plot(self, num_nodes): + def on_num_nodes(self, num_nodes): + num_nodes = int(num_nodes) if num_nodes == self.settings["NUM_NODES"]: return no_update - self.settings["NUM_NODES"] = int(num_nodes) - df = get_dataframe(self.settings) - self.fig = get_figure(df, self.settings) - return self.fig + self.settings["NUM_NODES"] = num_nodes + self.df = get_dataframe(self.settings) + self.fig = get_figure(self.df, self.settings) + return num_nodes, self.fig + + def on_net_range(self, net_range): + net_range = int(net_range) + if net_range == self.settings["NET_RANGE"]: + return no_update + self.settings["NET_RANGE"] = net_range + update_net_range(self.fig, self.settings) + return net_range, self.fig -a = UI() +app = App() diff --git a/ui/data.py b/ui/data.py index 5d17518..24deb34 100644 --- a/ui/data.py +++ b/ui/data.py @@ -9,7 +9,6 @@ # SIM_TIME = 3600*24*7 # SIM_TIME = 3600*24 SIM_TIME = 3600 -NET_RANGE = 50 WORLD_SIZE = (1000, 1000) CAPACITY = 10000 @@ -27,7 +26,7 @@ def generate_movement(settings: Dict): def simulate(moves, settings: Dict): num_nodes = settings["NUM_NODES"] - net = pons.NetworkSettings("WIFI_50m", range=NET_RANGE) + net = pons.NetworkSettings("WIFI_50m", range=settings["NET_RANGE"]) epidemic = pons.routing.EpidemicRouter(capacity=CAPACITY) nodes = pons.generate_nodes( @@ -60,7 +59,6 @@ def get_dataframe(settings: Dict): result["node"][index] = node result["x"][index] = x result["y"][index] = y - result["range"][index] = NET_RANGE index += 1 missing.remove((time, node)) @@ -75,7 +73,6 @@ def get_dataframe(settings: Dict): result["node"][index] = node result["x"][index] = value[0] result["y"][index] = value[1] - result["range"][index] = NET_RANGE index += 1 return pd.DataFrame.from_dict(result) \ No newline at end of file From 0ab7216e5915beec9b395fbc6688d57afe821797 Mon Sep 17 00:00:00 2001 From: fgrimm Date: Tue, 22 Aug 2023 15:46:55 +0200 Subject: [PATCH 4/6] gui --- pons/__init__.py | 1 + pons/events.py | 96 +++++ pons/routing/directdelivery.py | 25 +- pons/routing/epidemic.py | 25 +- pons/routing/firstcontact.py | 25 +- pons/routing/router.py | 33 +- pons/routing/sprayandwait.py | 24 +- pons/simulation.py | 34 +- sim.py | 2 +- ui/__init__.py | 5 +- ui/animation.py | 61 ---- ui/app.py | 638 +++++++++++++++++++++++++++------ ui/assets/styles.css | 109 ++++++ ui/assets/test.js | 3 + ui/components/__init__.py | 0 ui/config.json | 29 ++ ui/config.py | 43 +++ ui/data.py | 273 ++++++++++---- ui/requirements.txt | 3 +- ui/visualization.py | 153 ++++++++ utils/__init__.py | 2 + utils/list_utils.py | 23 ++ utils/misc.py | 2 + 23 files changed, 1240 insertions(+), 369 deletions(-) create mode 100644 pons/events.py delete mode 100644 ui/animation.py create mode 100644 ui/assets/styles.css create mode 100644 ui/assets/test.js create mode 100644 ui/components/__init__.py create mode 100644 ui/config.json create mode 100644 ui/config.py create mode 100644 ui/visualization.py create mode 100644 utils/__init__.py create mode 100644 utils/list_utils.py create mode 100644 utils/misc.py diff --git a/pons/__init__.py b/pons/__init__.py index bae4bdb..8858f22 100644 --- a/pons/__init__.py +++ b/pons/__init__.py @@ -1,4 +1,5 @@ from .simulation import NetSim +from .events import EventManager, Event, EventType from .mobility import generate_randomwaypoint_movement, OneMovement, OneMovementManager from .node import generate_nodes, Node, NetworkSettings, Message, BROADCAST_ADDR from .routing import Router, EpidemicRouter diff --git a/pons/events.py b/pons/events.py new file mode 100644 index 0000000..c5b6ec8 --- /dev/null +++ b/pons/events.py @@ -0,0 +1,96 @@ +from enum import Enum +from typing import List, Dict, Set +import pons +import simpy + + +class EventType(Enum): + RECEIVED = 0 + DELIVERED = 1 + CREATED = 2 + CONNECTION_UP = 3 + CONNECTION_DOWN = 4 + DROPPED = 5 + RELAY_STARTED = 6 + + +class Event: + """Simulation Events""" + + def __init__(self, type: EventType, node: int, from_node: int, message: "pons.Message", time: float): + self.type: EventType = type + self.node: int = node + self.from_node: int = from_node + self.message: pons.Message = message + self.time: float = time + + def __str__(self): + if self.type == EventType.RECEIVED: + return f"{self.time}: Message relayed {self.from_node} <-> {self.node} {self.message.id}" + if self.type == EventType.DELIVERED: + return f"{self.time}: Message delivered {self.from_node} <-> {self.node} {self.message.id}" + if self.type == EventType.CREATED: + return f"{self.time}: Message created {self.node} {self.message.id}" + if self.type == EventType.CONNECTION_UP: + return f"{self.time}: Connection UP {self.node} <-> {self.from_node}" + if self.type == EventType.CONNECTION_DOWN: + return f"{self.time}: Connection DOWN {self.node} <-> {self.from_node}" + if self.type == EventType.DROPPED: + return f"{self.time}: Message dropped {self.node} {self.message.id}" + if self.type == EventType.RELAY_STARTED: + return f"{self.time}: Message relay started {self.node} {self.message.id}" + + return "" + + def __eq__(self, other): + if not isinstance(other, Event): + return False + if other.type != self.type: + return False + if other.time != self.time: + return False + if self.type in [EventType.RECEIVED, EventType.DELIVERED]: + return self.node == other.node and self.from_node == other.from_node and self.message == other.message + if self.type in [EventType.CREATED, EventType.DROPPED, EventType.RELAY_STARTED]: + return self.node == other.node and self.message == other.message + return self.node == other.node and self.from_node == other.from_node or self.node == other.from_node and self.from_node == other.node + + def __hash__(self): + return self.type.value + self.node + self.from_node + hash(self.message) + int(self.time) + + +class EventManager: + def __init__(self, env: simpy.Environment, nodes: "List[pons.Node]"): + self._env: simpy.Environment = env + self.events: Set[Event] = set() + self._last_peers: Dict[int, Set[int]] = {node.id: set() for node in nodes} + self._current_peers: Dict[int, Set[int]] = {node.id: set() for node in nodes} + + def on_message_received(self, node: int, from_node: int, msg: "pons.Message"): + self.events.add(Event(EventType.RECEIVED, node, from_node, msg, self._env.now)) + + def on_message_delivered(self, node: int, from_node: int, msg: "pons.Message"): + self.events.add(Event(EventType.DELIVERED, node, from_node, msg, self._env.now)) + + def on_message_created(self, node: int, msg: "pons.Message"): + self.events.add(Event(EventType.CREATED, node, node, msg, self._env.now)) + + def on_message_dropped(self, node: int, msg: "pons.Message"): + self.events.add(Event(EventType.DROPPED, node, node, msg, self._env.now)) + + def on_peer_discovery(self, node1: int, node2: int): + if node2 not in self._last_peers[node1] and node1 not in self._last_peers[node2]: + self.events.add(Event(EventType.CONNECTION_UP, node1, node2, None, self._env.now)) + self._current_peers[node1].add(node2) + self._current_peers[node2].add(node1) + + def on_before_scan(self, node: int): + lost_peers = [peer for peer in self._last_peers[node] if peer not in self._current_peers[node]] + events = [Event(EventType.CONNECTION_DOWN, node, peer, None, self._env.now) for peer in lost_peers] + for event in events: + self.events.add(event) + self._last_peers[node] = self._current_peers[node].copy() + self._current_peers[node].clear() + for peer in lost_peers: + if node in self._current_peers[peer]: + self._current_peers[peer].remove(node) diff --git a/pons/routing/directdelivery.py b/pons/routing/directdelivery.py index f823960..63f570c 100644 --- a/pons/routing/directdelivery.py +++ b/pons/routing/directdelivery.py @@ -8,11 +8,6 @@ def __init__(self, scan_interval=2.0, capacity=0): def __str__(self): return "DirectDeliveryRouter" - def add(self, msg): - # print("adding new msg to store") - if self.store_add(msg): - self.forward(msg) - def forward(self, msg): if msg.dst in self.peers and not self.msg_already_spread(msg, msg.dst): # self.log("sending directly to receiver") @@ -25,24 +20,6 @@ def forward(self, msg): def on_peer_discovered(self, peer_id): # self.log("peer discovered: %d" % peer_id) + super().on_peer_discovered(peer_id) for msg in self.store: self.forward(msg) - - def on_msg_received(self, msg, remote_id): - # self.log("msg received: %s from %d" % (msg, remote_id)) - self.netsim.routing_stats['relayed'] += 1 - if not self.is_msg_known(msg): - self.remember(remote_id, msg) - msg.hops += 1 - self.store_add(msg) - if msg.dst == self.my_id: - # self.log("msg arrived", self.my_id) - self.netsim.routing_stats['delivered'] += 1 - self.netsim.routing_stats['hops'] += msg.hops - self.netsim.routing_stats['latency'] += self.env.now - msg.created - else: - # self.log("msg not arrived yet", self.my_id) - self.forward(msg) - else: - # self.log("msg already known", self.history) - self.netsim.routing_stats['dups'] += 1 diff --git a/pons/routing/epidemic.py b/pons/routing/epidemic.py index 2aa8079..91717f9 100644 --- a/pons/routing/epidemic.py +++ b/pons/routing/epidemic.py @@ -8,11 +8,6 @@ def __init__(self, scan_interval=2.0, capacity=0): def __str__(self): return "EpidemicRouter" - def add(self, msg): - # self.log("adding new msg to store") - if self.store_add(msg): - self.forward(msg) - def forward(self, msg): if msg.dst in self.peers and not self.msg_already_spread(msg, msg.dst): # self.log("sending directly to receiver") @@ -35,27 +30,9 @@ def forward(self, msg): def on_peer_discovered(self, peer_id): # self.log("peer discovered: %d" % peer_id) + super().on_peer_discovered(peer_id) for msg in self.store: if msg.is_expired(self.netsim.env.now): self.store_del(msg) else: self.forward(msg) - - def on_msg_received(self, msg, remote_id): - # self.log("msg received: %s from %d" % (msg, remote_id)) - self.netsim.routing_stats['relayed'] += 1 - if not self.is_msg_known(msg): - self.remember(remote_id, msg) - msg.hops += 1 - self.store_add(msg) - if msg.dst == self.my_id: - # self.log("msg arrived %s" % msg) - self.netsim.routing_stats['delivered'] += 1 - self.netsim.routing_stats['hops'] += msg.hops - self.netsim.routing_stats['latency'] += self.env.now - msg.created - else: - # self.log("msg not arrived yet", self.my_id) - self.forward(msg) - else: - # self.log("msg already known", self.history) - self.netsim.routing_stats['dups'] += 1 diff --git a/pons/routing/firstcontact.py b/pons/routing/firstcontact.py index c2c9122..75be5c2 100644 --- a/pons/routing/firstcontact.py +++ b/pons/routing/firstcontact.py @@ -9,11 +9,6 @@ def __init__(self, scan_interval=2.0): def __str__(self): return "FirstContactRouter" - def add(self, msg): - # print("adding new msg to store") - if self.store_add(msg): - self.forward(msg) - def forward(self, msg): if msg.dst in self.peers and not self.msg_already_spread(msg, msg.dst): # self.log("sending directly to receiver") @@ -38,24 +33,6 @@ def forward(self, msg): def on_peer_discovered(self, peer_id): # self.log("peer discovered: %d" % peer_id) + super().on_peer_discovered(peer_id) for msg in self.store: self.forward(msg) - - def on_msg_received(self, msg, remote_id): - # self.log("msg received: %s from %d" % (msg, remote_id)) - self.netsim.routing_stats['relayed'] += 1 - if not self.is_msg_known(msg): - self.remember(remote_id, msg) - msg.hops += 1 - self.store_add(msg) - if msg.dst == self.my_id: - # self.log("msg arrived", self.my_id) - self.netsim.routing_stats['delivered'] += 1 - self.netsim.routing_stats['hops'] += msg.hops - self.netsim.routing_stats['latency'] += self.env.now - msg.created - else: - # self.log("msg not arrived yet", self.my_id) - self.forward(msg) - else: - # self.log("msg already known", self.history) - self.netsim.routing_stats['dups'] += 1 diff --git a/pons/routing/router.py b/pons/routing/router.py index a5b208f..ddd0906 100644 --- a/pons/routing/router.py +++ b/pons/routing/router.py @@ -1,5 +1,3 @@ - -from copy import copy import pons HELLO_MSG_SIZE = 42 @@ -25,7 +23,11 @@ def log(self, msg): print("[%s : %s] %s" % (self.my_id, self, msg)) def add(self, msg: pons.Message): - self.store_add(msg) + if self.store_add(msg): + self.forward(msg) + + def forward(self, msg): + pass def store_add(self, msg: pons.Message): if self.capacity > 0 and self.used + msg.size > self.capacity: @@ -38,11 +40,13 @@ def store_add(self, msg: pons.Message): # self.log("store cleaned up, made room for msg %s" % msg.id) self.store.append(msg) self.used += msg.size + self.netsim.event_manager.on_message_created(self.my_id, msg) return True def store_del(self, msg: pons.Message): self.used -= msg.size self.store.remove(msg) + self.netsim.event_manager.on_message_dropped(self.my_id, msg) def store_cleanup(self): # [self.store_del(msg) @@ -81,6 +85,7 @@ def scan(self): # self.on_peer_discovered(peer) # do actual peer discovery with a hello message + self.netsim.event_manager.on_before_scan(self.my_id) self.peers.clear() self.netsim.nodes[self.my_id].send(self.netsim, pons.BROADCAST_ADDR, pons.Message( "HELLO", self.my_id, pons.BROADCAST_ADDR, HELLO_MSG_SIZE, self.netsim.env.now)) @@ -98,10 +103,28 @@ def on_scan_received(self, msg: pons.Message, remote_id: int): # self.log("DUP PEER: %d" % remote_id) def on_peer_discovered(self, peer_id): - self.log("peer discovered: %d" % peer_id) + #self.log("peer discovered: %d" % peer_id) + self.netsim.event_manager.on_peer_discovery(self.my_id, peer_id) def on_msg_received(self, msg: pons.Message, remote_id: int): - self.log("msg received: %s from %d" % (msg, remote_id)) + self.netsim.routing_stats['relayed'] += 1 + if not self.is_msg_known(msg): + self.remember(remote_id, msg) + msg.hops += 1 + self.store_add(msg) + if msg.dst == self.my_id: + # print("msg arrived", self.my_id) + self.netsim.routing_stats['delivered'] += 1 + self.netsim.routing_stats['hops'] += msg.hops + self.netsim.routing_stats['latency'] += self.env.now - msg.created + self.netsim.event_manager.on_message_delivered(self.my_id, remote_id, msg) + else: + # print("msg not arrived yet", self.my_id) + self.netsim.event_manager.on_message_received(self.my_id, remote_id, msg) + self.forward(msg) + else: + # print("msg already known", self.history) + self.netsim.routing_stats['dups'] += 1 def remember(self, peer_id, msg: pons.Message): if msg.id not in self.history: diff --git a/pons/routing/sprayandwait.py b/pons/routing/sprayandwait.py index 1b24332..2edffed 100644 --- a/pons/routing/sprayandwait.py +++ b/pons/routing/sprayandwait.py @@ -19,8 +19,7 @@ def __str__(self): def add(self, msg): # print("adding new msg to store") msg.metadata['copies'] = self.copies - if self.store_add(msg): - self.forward(msg) + super().add(msg) def forward(self, msg): if msg.dst in self.peers and not self.msg_already_spread(msg, msg.dst): @@ -54,25 +53,6 @@ def forward(self, msg): self.remember(peer, msg) def on_peer_discovered(self, peer_id): - # self.log("peer discovered: %d" % peer_id) + super().on_peer_discovered(peer_id) for msg in self.store: self.forward(msg) - - def on_msg_received(self, msg, remote_id): - # self.log("msg received: %s from %d" % (msg, remote_id)) - self.netsim.routing_stats['relayed'] += 1 - if not self.is_msg_known(msg): - self.remember(remote_id, msg) - msg.hops += 1 - self.store_add(msg) - if msg.dst == self.my_id: - # print("msg arrived", self.my_id) - self.netsim.routing_stats['delivered'] += 1 - self.netsim.routing_stats['hops'] += msg.hops - self.netsim.routing_stats['latency'] += self.env.now - msg.created - else: - # print("msg not arrived yet", self.my_id) - self.forward(msg) - else: - # print("msg already known", self.history) - self.netsim.routing_stats['dups'] += 1 diff --git a/pons/simulation.py b/pons/simulation.py index 0fa0b2b..0112bff 100644 --- a/pons/simulation.py +++ b/pons/simulation.py @@ -1,27 +1,9 @@ -from typing import List - -import simpy import time -import pons -from enum import Enum - - -class EventType(Enum): - SENT = 0 +from typing import List, Dict +import simpy -#class Event: -# """Simulation Events""" -# def __init__(self, type: EventType, node1: pons.Node, node2: pons.Node, message: pons.Message): -# self.type: EventType = type -# self.node1: pons.Node = node1 -# self.node2: pons.Node = node2 -# self.message: pons.Message = message -# -# def __str__(self): -# if self.type == EventType.SENT: -# return f"{self.node1} sent {self.message} to {self.node2}" -# raise NotImplementedError(f"{self.type} can not be converted to str") +import pons class NetSim(object): @@ -42,7 +24,7 @@ def __init__(self, duration, world, nodes, movements=[], msggens=None, config=No 'latency': 0.0, 'started': 0, 'relayed': 0, 'removed': 0, 'aborted': 0, 'dups': 0, 'latency_avg': 0.0, 'delivery_prob': 0.0, 'hops_avg': 0.0, 'overhead_ratio': 0.0} self.router_stats = {} - #self.events: List[Event] = [] + self.event_manager: pons.EventManager = pons.EventManager(self.env, nodes) self.mover = pons.OneMovementManager( self.env, self.nodes, self.movements) @@ -129,14 +111,14 @@ def run(self): if self.routing_stats["delivered"] > 0: self.routing_stats["latency_avg"] = self.routing_stats["latency"] / \ - self.routing_stats["delivered"] + self.routing_stats["delivered"] self.routing_stats["hops_avg"] = self.routing_stats["hops"] / \ - self.routing_stats["delivered"] + self.routing_stats["delivered"] self.routing_stats["overhead_ratio"] = (self.routing_stats["relayed"] - self.routing_stats["delivered"]) / \ - self.routing_stats["delivered"] + self.routing_stats["delivered"] self.routing_stats["delivery_prob"] = self.routing_stats["delivered"] / \ - self.routing_stats["created"] + self.routing_stats["created"] # delete entry "hops" and "latency" from routing_stats as they are only used for calculating the average del self.routing_stats["hops"] diff --git a/sim.py b/sim.py index af735e1..b99dc0f 100644 --- a/sim.py +++ b/sim.py @@ -23,7 +23,7 @@ SIM_TIME, NUM_NODES, WORLD_SIZE[0], WORLD_SIZE[1], max_pause=60.0) net = pons.NetworkSettings("WIFI_50m", range=NET_RANGE) -epidemic = pons.routing.PRoPHETRouter(capacity=CAPACITY) +epidemic = pons.routing.EpidemicRouter(capacity=CAPACITY) nodes = pons.generate_nodes( NUM_NODES, net=[net], router=epidemic) diff --git a/ui/__init__.py b/ui/__init__.py index 5079199..cf853c5 100644 --- a/ui/__init__.py +++ b/ui/__init__.py @@ -1,2 +1,3 @@ -from animation import get_figure, set_speed, update_net_range -from data import get_dataframe \ No newline at end of file +from visualization import Visualization +from data import DataManager +from config import config \ No newline at end of file diff --git a/ui/animation.py b/ui/animation.py deleted file mode 100644 index 5046fc3..0000000 --- a/ui/animation.py +++ /dev/null @@ -1,61 +0,0 @@ -from typing import Dict - -import pandas as pd -import plotly.express as px - - -WORLD_SIZE = (1000, 1000) -SIZEREF = (586.) / (1000.) - - -def get_figure(dataframe: pd.DataFrame, settings: Dict): - fig = px.scatter(dataframe, - x="x", - y="y", - animation_frame="time", - animation_group="node", - hover_name="node", - range_x=[0, WORLD_SIZE[0]], - range_y=[0, WORLD_SIZE[1]], - color_discrete_sequence=["rgba(0, 204, 0, 0.5)"]) - #set_speed(fig, settings) - fig.layout.updatemenus[0].buttons[0].args[1]["frame"]["duration"] = 30 - fig.layout.updatemenus[0].buttons[0].args[1]["transition"]["duration"] = 5 - fig.update_layout(width=800, height=800) # TODO scale this by x and y - fig.update_yaxes(scaleanchor="x", scaleratio=1) - fig.update_traces(marker={"size": settings["NET_RANGE"] * SIZEREF}) - fig.update_geos(projection_type="equirectangular", visible=True, resolution=110) - return fig - - -def update_net_range(fig, settings: Dict): - fig.update_traces(marker={"size": settings["NET_RANGE"] * SIZEREF}) - - -def set_speed(figure, settings: Dict): - speed_factor = settings["SPEED_FACTOR"] - print(figure.layout.updatemenus[0]) - figure.update_layout(width=800, - height=800, - updatemenus=[ - { - 'buttons': [{'args': [None, {'frame': {'duration': 30 * speed_factor, 'redraw': False}, - 'mode': 'immediate', 'fromcurrent': True, 'transition': - {'duration': 5 * speed_factor, 'easing': 'linear'}}], - 'label': '▶', - 'method': 'animate'}, - {'args': [[None], {'frame': {'duration': 0, 'redraw': False}, - 'mode': 'immediate', 'fromcurrent': True, 'transition': - {'duration': 0, 'easing': 'linear'}}], - 'label': '◼', - 'method': 'animate'}], - 'direction': 'left', - 'pad': {'r': 10, 't': 70}, - 'showactive': False, - 'type': 'buttons', - 'x': 0.1, - 'xanchor': 'right', - 'y': 0, - 'yanchor': 'top' - } - ]) \ No newline at end of file diff --git a/ui/app.py b/ui/app.py index fead32b..45b5bb1 100644 --- a/ui/app.py +++ b/ui/app.py @@ -1,118 +1,530 @@ +import inspect +import math +from typing import Tuple + import dash_bootstrap_components as dbc -from dash import Dash, html, dcc, no_update, Input, Output -from dash_bootstrap_components import Col, Row - -from ui import get_figure, get_dataframe, set_speed, update_net_range - -MAX_SPEED_FACTOR = 10. -SIZEREF = (586.) / (1000.) - - -class App(Dash): - def __init__(self): - super().__init__(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) - self._register_callbacks() - self.settings = { - "NUM_NODES": 10, - "SPEED_FACTOR": 1., - "NET_RANGE": 50 - } - self.df = get_dataframe(self.settings) - self.fig = get_figure(self.df, self.settings) - - self.layout = html.Div([ - # dcc.Graph(figure=fig), - Row([ - Col(html.Center( - dcc.Loading(id="loading", children=[ - dcc.Graph(id="plot", figure=self.fig)], - type="default")), width=8), - Col( - html.Div([ - html.Div([ - html.Span('Number of nodes', style={'display': 'inline-block', 'margin-right': 20}), - dcc.Input(id="num_nodes", - type="range", - min=1, - max=10, - step=1, - value=self.settings["NUM_NODES"], - debounce=True, - style={"display": "inline-block"}), - html.Span(self.settings["NUM_NODES"], id="num_nodes_display", style={'display': 'inline-block', 'margin-left': 20}), - ]), # number of nodes input - html.Div([ - html.Span('Net range', style={'display': 'inline-block', 'margin-right': 20}), - dcc.Input(id="net_range", - type="range", - min=20, - max=150, - step=5, - value=self.settings["NET_RANGE"], - debounce=True, - style={"display": "inline-block"}), - html.Span(self.settings["NET_RANGE"], id="net_range_display", style={'display': 'inline-block', 'margin-left': 20}), - ]), # net range input +from dash import Dash, html, dcc, no_update, Input, Output, ctx, State +import dash_breakpoints + +import pons +from ui import Visualization, DataManager, config +import utils + +IGNORE_ROUTERS = ["Router"] + +ROUTERS = {member: (cls(capacity=config.capacity) if "capacity" in inspect.getfullargspec(cls.__init__).args else cls()) + for (member, cls) in inspect.getmembers(pons.routing) if + inspect.isclass(cls) and member not in IGNORE_ROUTERS} + +settings = { + "SIM_TIME": 3600, + "WORLD_SIZE": (1000, 1000), # fixed value + "NUM_NODES": 10, + "MIN_SPEED": 1., + "MAX_SPEED": 5., + "NET_RANGE": 50, + "SIZEREF": 605. / 1000., # fixed value + "FRAME_STEP": 1, + "ROUTER": ROUTERS["EpidemicRouter"], + "SHOW_NODE_IDS": True, + "MESSAGES": { + "MIN_INTERVAL": 30, + "MAX_INTERVAL": 30, + "MIN_SIZE": 0, + "MAX_SIZE": 1028, + "MIN_TTL": 3200, + "MAX_TTL": 3600 + } +} + +data_manager = DataManager(settings) +visualization = Visualization(data_manager, settings) +fig = visualization.get_figure() +app = Dash(__name__, external_stylesheets=[dbc.themes.COSMO, dbc.icons.BOOTSTRAP]) + +# graph = dcc.Graph(figure=fig, id="plot", config={"displayModeBar": False}, className="mt-0 mb-0") + +app.layout = html.Div([ + dbc.Row([ + dbc.Col( + html.Div([ + dbc.Row([ + dbc.Col( + html.Div(["Animation speed"], className="ml-2"), + width=2 + ), + dbc.Col( + dcc.Slider(id="speed_slider", + min=1, + max=50, + step=None, + marks={1: "1", 2: "2", 5: "5", 10: "10", 20: "20", 30: "30", 40: "40", 50: "50"}, + className="w-75"), + width=10 + ), + ], className="mt-2"), + dbc.Row([ + dbc.Col( + html.Div([dcc.Graph(figure=fig, id="plot", config={"displayModeBar": False}, className="mt-0 mb-0 square-content", responsive=True)], className="square"), + width=9, + ), + dbc.Col( + html.Div([html.Div([html.Div([node], className="mt-2 mr-2"), + dbc.Progress(value=0, className="mt-3 buffer w-100")], + className="d-flex justify-content-between") for node in + range(settings["NUM_NODES"])], id="buffer_div"), + width=3 + ) + ]), + dbc.Row([ + dbc.Col( html.Div([ - html.Span('Speed', style={'display': 'inline-block', 'margin-right': 20}), - dcc.Input(id="speed_factor", - type="range", - min=0.05, - max=10, - step=0.05, - value=self.settings["SPEED_FACTOR"], - debounce=True, - style={"display": "inline-block"}), - html.Span('', - id="speed_factor_display", - style={'display': 'inline-block', 'margin-left': 20}), - ], style={ "visibility": "hidden"}), # speed factor input - ], className="mt-5"), - width=4), - # dbc.Col(dcc.Input(type="range"), width=4) - ]), - ]) - - self.run(debug=True) - - def _register_callbacks(self): - self.callback( - [Output("num_nodes_display", "children"), Output("plot", "figure", allow_duplicate=True)], - Input("num_nodes", "value"), - prevent_initial_call=True - )(self.on_num_nodes) - self.callback( - Output("speed_factor_display", "children"), - Input("speed_factor", "value"), - prevent_initial_call=True - )(self.on_speed_factor) - self.callback( - [Output("net_range_display", "children"), Output("plot", "figure", allow_duplicate=True)], - Input("net_range", "value"), - prevent_initial_call=True - )(self.on_net_range) - - def on_speed_factor(self, factor): - self.settings["SPEED_FACTOR"] = MAX_SPEED_FACTOR - float(factor) - set_speed(self.fig, self.settings) - return factor - - def on_num_nodes(self, num_nodes): - num_nodes = int(num_nodes) - if num_nodes == self.settings["NUM_NODES"]: - return no_update - self.settings["NUM_NODES"] = num_nodes - self.df = get_dataframe(self.settings) - self.fig = get_figure(self.df, self.settings) - return num_nodes, self.fig - - def on_net_range(self, net_range): - net_range = int(net_range) - if net_range == self.settings["NET_RANGE"]: + dbc.Button( + html.I(id="playIcon", className="bi bi-play"), + id="playButton", + color="primary", + outline=True, + className="ml-2 control-button" + ), + dbc.Button( + html.I(id="stepIcon", className="bi bi-skip-end"), + id="stepButton", + color="primary", + outline=True, + className="ml-2 control-button" + ) + ]), + width=2 + ), + dbc.Col(dcc.Slider(id="anim_slider", + min=0, + max=settings["SIM_TIME"], + value=0, + drag_value=0, + step=10, + className="w-100", + marks={i: '{}'.format(i) for i in + range(0, settings["SIM_TIME"], config.slider_marks_step)}), + width=10), + ]) + ], className="mt-0 mb-2"), width=8), + dbc.Col(html.Div([ + html.Div([ + html.Div([ + dbc.Button( + html.I(className="bi bi-caret-down-fill"), + id="checkbox_collapse_button", + className="mb-3 control-button", + color="primary", + n_clicks=0, + ), + dbc.Collapse( + dbc.Card(dbc.CardBody( + dbc.Checklist( + options=[ + {"label": "Show node ids", "value": 1}, + ], + value=[1], + id="checkbox_input", + ), + )), + id="checkbox_collapse", + is_open=False, + ), + ]), + html.Div([ + dbc.Label('Number of nodes', html_for="num_nodes", className="form-label"), + dbc.Row([ + dbc.Col(dcc.Input(id="num_nodes", + type="range", + min=config.min_num_of_nodes, + max=config.max_num_of_nodes, + step=config.num_of_nodes_step, + value=settings["NUM_NODES"], + className="form-control"), width=10), + dbc.Col(html.Span(settings["NUM_NODES"], id="num_nodes_display"), width=2) + ]) + ]), # number of nodes input + html.Div([ + dbc.Label('Net range', html_for="net_range", className="form-label"), + dbc.Row([ + dbc.Col(dcc.Input(id="net_range", + type="range", + min=config.min_net_range, + max=config.max_net_range, + step=config.net_range_step, + value=settings["NET_RANGE"], + className="form-control"), width=10), + dbc.Col(html.Span(settings["NET_RANGE"], id="net_range_display"), width=2) + ]) + ], className="mt-2"), # net range input + html.Div([ + dbc.Label('Min. speed', html_for="min_speed", className="form-label"), + dbc.Row([ + dbc.Col(dcc.Input(id="min_speed", + type="range", + min=config.min_speed, + max=settings["MAX_SPEED"], + step=config.speed_step, + value=settings["MIN_SPEED"], + className="form-control"), width=10), + dbc.Col(html.Span(settings["MIN_SPEED"], id="min_speed_display"), width=2) + ]) + ], className="mt-2"), # min speed input + html.Div([ + dbc.Label('Max. speed', html_for="max_speed", className="form-label"), + dbc.Row([ + dbc.Col(dcc.Input(id="max_speed", + type="range", + min=settings["MIN_SPEED"], + max=config.max_speed, + step=config.speed_step, + value=settings["MAX_SPEED"], + className="form-control"), width=10), + dbc.Col(html.Span(settings["MAX_SPEED"], id="max_speed_display"), width=2) + ]) + ], className="mt-2"), # min speed input + html.Div([ + dbc.Label('Simulation time', html_for="sim_time", className="form-label"), + dbc.Row([ + dbc.Col(dcc.Input(id="sim_time", + type="range", + min=config.min_sim_time, + max=config.max_sim_time, + step=config.sim_time_step, + value=settings["SIM_TIME"], + className="form-control"), width=10), + dbc.Col(html.Span(settings["SIM_TIME"], id="sim_time_display"), width=2) + ]) + ], className="mt-2"), # sim time input + html.Div([ + dbc.Label('Router', html_for="router", className="form-label"), + dbc.Select(options=list(ROUTERS.keys()), value="EpidemicRouter", id="router") + ], className="mt-2"), # router input + html.Hr(), + html.H6("Messages"), + html.Div([ + dbc.Label('Interval (s)', html_for="msg_interval", className="form-label"), + dcc.RangeSlider(min=config.messages.min_interval, + max=config.messages.max_interval, + step=config.messages.interval_step, + marks=utils.get_marks_dict(config.messages.min_interval, + config.messages.max_interval, + config.messages.interval_step), + value=[settings["MESSAGES"]["MIN_INTERVAL"], settings["MESSAGES"]["MAX_INTERVAL"]], + id="msg_interval") + ], className="mt-2"), # message interval + html.Div([ + dbc.Label('Size', html_for="msg_size", className="form-label"), + dcc.RangeSlider( + min=config.messages.min_size, + max=config.messages.max_size, + step=config.messages.size_step, + marks=utils.get_marks_dict(config.messages.min_size, + config.messages.max_size, + config.messages.size_step), + value=[settings["MESSAGES"]["MIN_SIZE"], settings["MESSAGES"]["MAX_SIZE"]], + id="msg_size") + ], className="mt-2"), # message size + html.Div([ + dbc.Label('Time to live (s)', html_for="msg_ttl", className="form-label"), + dcc.RangeSlider( + min=config.messages.min_ttl, + max=config.messages.max_ttl, + step=config.messages.ttl_step, + marks=utils.get_marks_dict(config.messages.min_ttl, + config.messages.max_ttl, + config.messages.ttl_step), + value=[settings["MESSAGES"]["MIN_TTL"], settings["MESSAGES"]["MAX_TTL"]], + id="msg_ttl" + ) + ], className="mt-2"), # message ttl + ], className="scrollable h-40"), + html.Div([ + dbc.Button("Reset", + id="resetButton", + color="secondary", + className="w-100", + style={"margin-right": "10px"}), + dbc.Button(dcc.Loading("Save", id="save_button_content", type="dot"), + id="saveButton", + color="primary", + className="w-100", + style={"margin-left": "10px"}), + ], className="d-flex justify-content-between mt-3"), # Buttons + html.Div([ + html.Div([], id="event_div", className="event-display") + ], className="mt-3") # Event Display + ], className="mt-5 h-100", style={"margin-right": "30px"}), width=4) + ], className="h-100"), + dcc.Interval(id="anim_interval", interval=config.refresh_interval), + html.Div("", id="size_helper", hidden=True), + dash_breakpoints.WindowBreakpoints(id="breakpoints"), +], className="h-100 no-scroll") + +app.clientside_callback( + """ + async function getWidth (w, h) { + var delay = ms => new Promise(res => setTimeout(res, ms)); + await delay(200); + var plot = document.getElementsByClassName('bglayer')[0]; + if (plot == null) { return '0;0' }; + var rect = plot.getBoundingClientRect(); + return rect.width.toString() + ';' + rect.height.toString();; + } + """, + Output('size_helper', 'children', allow_duplicate=True), + [Input("breakpoints", "width"), Input("breakpoints", "height")], + prevent_initial_call=True +) + +@app.callback( + Output('size_helper', 'children', allow_duplicate=True), + Input("size_helper", "children"), + prevent_initial_call=True +) +def test(content): + splitted = content.split(";") + width = int(splitted[0]) + height = int(splitted[1]) + if width == 0: + return no_update + factor = (width + height) / 2. + world_size_factor = (settings["WORLD_SIZE"][0] + settings["WORLD_SIZE"][1]) / 2 + settings["SIZEREF"] = factor / world_size_factor + return factor + +@app.callback( + Output("checkbox_collapse", "is_open"), + [Input("checkbox_collapse_button", "n_clicks")], + [State("checkbox_collapse", "is_open")], +) +def toggle_collapse(n, is_open): + if n: + return not is_open + return is_open + + +@app.callback( + [ + Output("plot", "figure", allow_duplicate=True), + Output("anim_slider", "value", allow_duplicate=True), + Output("event_div", "children", allow_duplicate=True), + Output("buffer_div", "children", allow_duplicate=True) + ], + [Input("anim_slider", "value"), Input("anim_slider", "drag_value"), Input("anim_interval", "n_intervals")], + prevent_initial_call=True +) +def animate(slider_value, drag_value, n_intervals): + if "anim_slider.value" in ctx.triggered_prop_ids: + if visualization.is_waiting(): + visualization.play() + visualization.set_frame(int(slider_value)) + return _get_animation_outputs() + if not visualization.is_playing(): + return _get_animation_outputs() + if "anim_slider.drag_value" in ctx.triggered_prop_ids and slider_value != drag_value: + visualization.wait() + return no_update + if "anim_interval.n_intervals" in ctx.triggered_prop_ids: + if n_intervals is None: return no_update - self.settings["NET_RANGE"] = net_range - update_net_range(self.fig, self.settings) - return net_range, self.fig + visualization.update_frame() + return _get_animation_outputs() + + return no_update + + +def _get_animation_outputs() -> Tuple: + frame = visualization.get_frame() + return ( + visualization.get_figure(), + frame, + [html.Div([str(e)]) for e in data_manager.get_events(frame)], + [html.Div([html.Div([key], className="mt-2 mr-2"), + dbc.Progress(value=value, className="mt-3 buffer w-100")], + className="d-flex justify-content-between") for key, value in + data_manager.get_buffer(frame).items()] + ) + + +@app.callback( + Output("playIcon", "className", allow_duplicate=True), + Input("playButton", "n_clicks"), + prevent_initial_call=True +) +def on_play_click(n_clicks): + if visualization.is_playing(): + visualization.pause() + return "bi bi-play" + visualization.play() + return "bi bi-pause" + + +@app.callback( + Output("speed_slider", "value"), + Input("speed_slider", "drag_value"), + prevent_initial_call=True +) +def on_speed_drag(value): + settings["FRAME_STEP"] = value + visualization.set_settings(settings) + return no_update + +@app.callback( + Output("plot", "figure"), + Input("plot", "figure"), + prevent_initial_call=True +) +def onplot(figure): + return no_update + + +@app.callback( + [ + Output("plot", "figure", allow_duplicate=True), + Output("anim_slider", "value", allow_duplicate=True), + Output("event_div", "children", allow_duplicate=True), + Output("buffer_div", "children", allow_duplicate=True) + ], + Input("stepButton", "n_clicks"), + prevent_initial_call=True +) +def on_step_click(n_clicks): + frame = (visualization.get_frame() + settings["FRAME_STEP"]) % settings["SIM_TIME"] + visualization.set_frame(frame) + return ( + visualization.get_figure(), + frame, + [html.Div([str(e)]) for e in data_manager.get_events(frame)], + [html.Div([html.Div([key], className="mt-2 mr-2"), + dbc.Progress(value=value, className="mt-3 buffer w-100")], + className="d-flex justify-content-between") for key, value in + data_manager.get_buffer(frame).items()] + ) + + +@app.callback( + Output("num_nodes_display", "children"), + Input("num_nodes", "value"), + prevent_initial_call=True +) +def on_num_nodes(value): + return value + + +@app.callback( + Output("net_range_display", "children"), + Input("net_range", "value"), + prevent_initial_call=True +) +def on_net_range(value): + return value + + +@app.callback( + [Output("min_speed", "max"), Output("max_speed_display", "children")], + Input("max_speed", "value"), + prevent_initial_call=True +) +def on_max_speed(value): + return value, value + + +@app.callback( + [Output("max_speed", "min"), Output("min_speed_display", "children")], + Input("min_speed", "value"), + prevent_initial_call=True +) +def on_min_speed(value): + return value, value + + +@app.callback( + Output("sim_time_display", "children"), + Input("sim_time", "value"), + prevent_initial_call=True +) +def on_sim_time(value): + return value + + +@app.callback( + [ + Output("plot", "figure", allow_duplicate=True), + Output("playIcon", "className", allow_duplicate=True), + Output("anim_slider", "value", allow_duplicate=True), + Output("anim_slider", "max", allow_duplicate=True), + Output("anim_slider", "marks"), + Output("save_button_content", "children"), + Output("anim_interval", "interval"), + ], + Input("saveButton", "n_clicks"), + [ + State("num_nodes", "value"), + State("net_range", "value"), + State("min_speed", "value"), + State("max_speed", "value"), + State("sim_time", "value"), + State("router", "value"), + State("msg_interval", "value"), + State("msg_size", "value"), + State("msg_ttl", "value"), + ], + prevent_initial_call=True +) +def on_save(n_clicks, num_nodes, net_range, min_speed, max_speed, sim_time, router, msg_interval, msg_size, msg_ttl): + visualization.pause() + sim_time = int(sim_time) + settings["NUM_NODES"] = int(num_nodes) + settings["NET_RANGE"] = int(net_range) + settings["MIN_SPEED"] = float(min_speed) + settings["MAX_SPEED"] = float(max_speed) + settings["SIM_TIME"] = sim_time + settings["ROUTER_CLASS"] = ROUTERS[router] + settings["MESSAGES"]["MIN_INTERVAL"] = msg_interval[0] + settings["MESSAGES"]["MAX_INTERVAL"] = msg_interval[1] + settings["MESSAGES"]["MIN_SIZE"] = msg_size[0] + settings["MESSAGES"]["MAX_SIZE"] = msg_size[1] + settings["MESSAGES"]["MIN_TTL"] = msg_ttl[0] + settings["MESSAGES"]["MAX_TTL"] = msg_ttl[1] + data_manager.update_settings(settings) + visualization.set_settings(settings) + visualization.on_data_update() + visualization.set_frame(0) + return ( + visualization.get_figure(), + "bi bi-play-fill", + 0, + sim_time, {i: '{}'.format(i) for i in range(0, sim_time, int(sim_time / 20))}, + "Save", + config.refresh_interval + ) + + +@app.callback( + [ + Output("num_nodes", "value", allow_duplicate=True), + Output("net_range", "value", allow_duplicate=True), + Output("min_speed", "value", allow_duplicate=True), + Output("max_speed", "value", allow_duplicate=True) + ], + Input("resetButton", "n_clicks"), + prevent_initial_call=True +) +def on_reset(n_clicks): + return settings["NUM_NODES"], settings["NET_RANGE"], settings["MIN_SPEED"], settings["MAX_SPEED"], + + +@app.callback( + Output("plot", "figure", allow_duplicate=True), + Input("checkbox_input", "value"), + prevent_initial_call=True +) +def on_checkbox_input(value): + settings["SHOW_NODE_IDS"] = 1 in value + return visualization.get_figure() -app = App() +if __name__ == "__main__": + app.run(debug=True, host="0.0.0.0") diff --git a/ui/assets/styles.css b/ui/assets/styles.css new file mode 100644 index 0000000..f05b465 --- /dev/null +++ b/ui/assets/styles.css @@ -0,0 +1,109 @@ +.ml-0 { + margin-left: 0 !important; +} + +.ml-1 { + margin-left: 0.25rem !important; +} + +.ml-2 { + margin-left: 0.5rem !important; +} + +.ml-3 { + margin-left: 1rem !important; +} + +.ml-4 { + margin-left: 1.5rem !important; +} + +.ml-5 { + margin-left: 3rem !important; +} + +.mr-0 { + margin-right: 0 !important; +} + +.mr-1 { + margin-right: 0.25rem !important; +} + +.mr-2 { + margin-right: 0.5rem !important; +} + +.mr-3 { + margin-right: 1rem !important; +} + +.mr-4 { + margin-right: 1.5rem !important; +} + +.mr-5 { + margin-right: 3rem !important; +} + +.mx-auto { + margin-left: auto !important;; + margin-right: auto !important;; +} + +.scrollable { + overflow-y: scroll; +} + +.h-100 { + height: 95vh +} + +.h-40 { + height: 40%; +} + +html { + box-sizing: border-box; +} +*, *:before, *:after { + box-sizing: inherit; +} + +html, body {margin: 0; height: 100%; overflow: hidden} + + +.event-display { + height: 200px; + overflow: auto; + display: flex; + flex-direction: column-reverse; + border: solid #ced4da 1px; + padding: 10px; +} + +.control-button { + --bs-btn-padding-y: 0.175rem; + --bs-btn-padding-x: 0.5rem; +} + +.buffer { + height: 13px !important; +} + +.square { + position: relative; + width: 90%; +} + +.square:after { + content: ""; + display: block; + padding-bottom: 100%; +} + +.square-content { + position: absolute; + width: 100%; + height: 100%; +} \ No newline at end of file diff --git a/ui/assets/test.js b/ui/assets/test.js new file mode 100644 index 0000000..579fa2e --- /dev/null +++ b/ui/assets/test.js @@ -0,0 +1,3 @@ +console.log("hallo"); + +console.log(document.getElementById("plot")); \ No newline at end of file diff --git a/ui/components/__init__.py b/ui/components/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ui/config.json b/ui/config.json new file mode 100644 index 0000000..36fdcfc --- /dev/null +++ b/ui/config.json @@ -0,0 +1,29 @@ +{ + "minSimTime": 0, + "maxSimTime": 3600, + "simTimeStep": 50, + "sliderMarksStep": 200, + "minNumOfNodes": 1, + "maxNumOfNodes": 10, + "numOfNodesStep": 1, + "minNetRange": 20, + "maxNetRange": 1000, + "netRangeStep": 5, + "minSpeed": 0, + "maxSpeed": 20, + "speedStep": 0.2, + "frameStepOptions": [1, 2, 5, 10, 100], + "refreshInterval": 80, + "capacity": 10000, + "messages": { + "minInterval": 0, + "maxInterval": 1000, + "intervalStep": 100, + "minSize": 0, + "maxSize": 8224, + "sizeStep": 1028, + "minTtl": 0, + "maxTtl": 3600, + "ttlStep": 400 + } +} \ No newline at end of file diff --git a/ui/config.py b/ui/config.py new file mode 100644 index 0000000..65ded2f --- /dev/null +++ b/ui/config.py @@ -0,0 +1,43 @@ +from dataclasses import dataclass +from dataclasses_json import dataclass_json, LetterCase +from typing import List + + +@dataclass_json(letter_case=LetterCase.CAMEL) +@dataclass +class MessageConfig: + min_interval: int + max_interval: int + interval_step: int + min_size: int + max_size: int + size_step: int + min_ttl: int + max_ttl: int + ttl_step: int + + +@dataclass_json(letter_case=LetterCase.CAMEL) +@dataclass +class Config: + min_sim_time: int + max_sim_time: int + sim_time_step: int + slider_marks_step: int + min_num_of_nodes: int + max_num_of_nodes: int + num_of_nodes_step: int + min_net_range: int + max_net_range: int + net_range_step: int + min_speed: float + max_speed: float + speed_step: float + frame_step_options: List[int] + refresh_interval: int + capacity: int + messages: MessageConfig + + +with open("config.json", "r") as file: + config = Config.from_json(file.read()) \ No newline at end of file diff --git a/ui/data.py b/ui/data.py index 24deb34..324e1f9 100644 --- a/ui/data.py +++ b/ui/data.py @@ -1,78 +1,219 @@ import random -from typing import Dict +from typing import Dict, Any, Tuple, List import pandas as pd import pons RANDOM_SEED = 42 -# SIM_TIME = 3600*24*7 -# SIM_TIME = 3600*24 -SIM_TIME = 3600 -WORLD_SIZE = (1000, 1000) CAPACITY = 10000 - random.seed(RANDOM_SEED) -def generate_movement(settings: Dict): - return pons.generate_randomwaypoint_movement(SIM_TIME, - settings["NUM_NODES"], - WORLD_SIZE[0], - WORLD_SIZE[1], - max_pause=60.0) - - -def simulate(moves, settings: Dict): - num_nodes = settings["NUM_NODES"] - net = pons.NetworkSettings("WIFI_50m", range=settings["NET_RANGE"]) - epidemic = pons.routing.EpidemicRouter(capacity=CAPACITY) - - nodes = pons.generate_nodes( - num_nodes, net=[net], router=epidemic) - config = {"movement_logger": False, "peers_logger": False} - - msggenconfig = {"type": "single", "interval": 30, "src": ( - 0, num_nodes), "dst": (0, num_nodes), "size": 100, "id": "M", "ttl": 3600} - - netsim = pons.NetSim(SIM_TIME, WORLD_SIZE, nodes, moves, - config=config, msggens=[msggenconfig]) - - -def get_dataframe(settings: Dict): - data = generate_movement(settings) - missing = [(float(t), n) for t in range(SIM_TIME) for n in range(settings["NUM_NODES"])] - d = {} - index = 0 - result = {"time": {}, "node": {}, "x": {}, "y": {}, "range": {}} - for entry in data: - time = entry[0] - node = entry[1] - x = entry[2] - y = entry[3] - if time in d: - d[time][node] = (x, y) - else: - d[time] = {node: (x, y)} - result["time"][index] = time - result["node"][index] = node - result["x"][index] = x - result["y"][index] = y - index += 1 - missing.remove((time, node)) - - missing = sorted(missing, key=lambda m: m[0]) - - for (time, node) in missing: - value = d[time - 1][node] - if time not in d: - d[time] = {} - d[time][node] = value - result["time"][index] = time - result["node"][index] = node - result["x"][index] = value[0] - result["y"][index] = value[1] - index += 1 - - return pd.DataFrame.from_dict(result) \ No newline at end of file +class DataManager: + def __init__(self, settings: Dict[str, Any]): + self._settings: Dict[str, Any] = settings + self._moves: List[Tuple[float, int, int, int]] = [] + self._helper: Dict[float, Dict[int, Tuple[int, int]]] = {} + self._data: Dict[float, Dict[str, List]] = {} + self._events: Dict[float, Dict[str, List]] = {} + self._events_list: List["pons.Event"] = [] + self._stores: Dict[float, Dict[int, List[pons.Message]]] = {} + self._connections: Dict[float, List[pd.DataFrame]] = {} + self._generate() + + def _generate_movement(self) -> List[Tuple[float, int, int, int]]: + world_size = self._settings["WORLD_SIZE"] + return pons.generate_randomwaypoint_movement(self._settings["SIM_TIME"], + self._settings["NUM_NODES"], + world_size[0], + world_size[1], + max_pause=60.0, + min_speed=self._settings["MIN_SPEED"], + max_speed=self._settings["MAX_SPEED"]) + + def _simulate(self): + self._moves = self._generate_movement() + + num_nodes = self._settings["NUM_NODES"] + net = pons.NetworkSettings("WIFI_50m", range=self._settings["NET_RANGE"]) + + nodes = pons.generate_nodes( + num_nodes, net=[net], router=self._settings["ROUTER"]) + config = {"movement_logger": False, "peers_logger": False} + + msggenconfig = { + "type": "single", + "interval": (self._settings["MESSAGES"]["MIN_INTERVAL"], self._settings["MESSAGES"]["MAX_INTERVAL"]), + "src": (0, num_nodes), + "dst": (0, num_nodes), + "size": (self._settings["MESSAGES"]["MIN_SIZE"], self._settings["MESSAGES"]["MAX_SIZE"]), + "id": "M", + "ttl": (self._settings["MESSAGES"]["MIN_TTL"], self._settings["MESSAGES"]["MAX_TTL"]) + } + + netsim = pons.NetSim(self._settings["SIM_TIME"], self._settings["WORLD_SIZE"], nodes, self._moves, + config=config, msggens=[msggenconfig]) + + netsim.setup() + netsim.run() + self._events_list = sorted(netsim.event_manager.events, key=lambda e: e.time) + + def _add(self, + data: Dict[float, Dict[str, List]], + helper: Dict[float, Dict[int, Tuple[int, int]]], + time: float, + node: int, + x: int, + y: int): + data[time]["node"].append(str(node)) + data[time]["x"].append(x) + data[time]["y"].append(y) + data[time]["stores"].append(f"{node}
{'
'.join([msg.id for msg in self._stores[time][node]])}") + helper[time][node] = (x, y) + + def _add_missing_data(self, data: Dict[float, Dict[str, List]], helper: Dict[float, Dict[int, Tuple[int, int]]]): + for time in range(self._settings["SIM_TIME"]): + time = float(time) + if time not in data: + data[time] = {"node": [], "x": [], "y": [], "stores": []} + helper[time] = {} + for node in range(self._settings["NUM_NODES"]): + if node in helper[time]: + continue + x, y = helper[time - 1][node] + self._add(data, helper, time, node, x, y) + + def _generate_data(self): + data = {} + helper = {} + for move in self._moves: + time = move[0] + node = move[1] + x = move[2] + y = move[3] + if time not in data: + data[time] = {"node": [], "x": [], "y": [], "stores": []} + helper[time] = {} + self._add(data, helper, time, node, x, y) + self._add_missing_data(data, helper) + self._data = data + self._helper = helper + + def _add_event(self, target: Dict[float, Dict[str, List]], event: pons.Event): + time = event.time + if time not in self._helper or event.from_node not in self._helper[time]: + return + target[time]["type"].append(event.type) + from_x, from_y = self._helper[time][event.from_node] + target[time]["from_x"].append(from_x) + target[time]["from_y"].append(from_y) + to_x, to_y = self._helper[time][event.node] + target[time]["to_x"].append(to_x) + target[time]["to_y"].append(to_y) + + def _generate_event_data(self): + data = {} + for event in self._events_list: + if event.time not in data: + data[event.time] = {"type": [], "from_x": [], "from_y": [], "to_x": [], "to_y": []} + self._add_event(data, event) + self._events = data + + def _generate_connections(self): + data = {} + current_conns = set() + for time in range(0, self._settings["SIM_TIME"]): + data[time] = [] + for event in [event for event in self._events_list if event.time == time]: + if event.type == pons.EventType.CONNECTION_UP: + current_conns.add(frozenset((event.node, event.from_node))) + elif event.type == pons.EventType.CONNECTION_DOWN: + conn = frozenset((event.node, event.from_node)) + if conn in current_conns: + current_conns.remove(conn) + for conn in current_conns: + node1, node2 = tuple(conn) + from_x, from_y = self._helper[time][node1] + to_x, to_y = self._helper[time][node2] + data[time].append(pd.DataFrame.from_dict({ + "x": [from_x, to_x], + "y": [from_y, to_y] + })) + self._connections = data + + def _generate_stores(self): + stores = {} + stores[0] = {i: set() for i in range(0, self._settings["NUM_NODES"])} + for time in range(1, self._settings["SIM_TIME"]): + stores[time] = {i: stores[time - 1][i].copy() for i in range(0, self._settings["NUM_NODES"])} + for event in self._events_list: + if time != event.time: + continue + if event.type in [pons.EventType.CREATED, pons.EventType.RECEIVED, pons.EventType.DELIVERED]: + stores[time][event.node].add(event.message) + elif event.type == pons.EventType.DROPPED: + if event.message in stores[time][event.node]: + stores[time][event.node].remove(event.message) + self._stores = stores + + def _generate(self): + self._simulate() + self._generate_stores() + self._generate_data() + self._generate_event_data() + self._generate_connections() + + @staticmethod + def _to_dataframes(data: Dict[float, Dict[str, List]]) -> Dict[float, pd.DataFrame]: + dfs = {} + for time in data: + dfs[time] = pd.DataFrame.from_dict(data[time]) + return dfs + + def get_data(self) -> Dict[float, pd.DataFrame]: + """gets the movement data""" + return self._to_dataframes(self._data) + + def get_event_data(self, types=None) -> Dict[float, List[pd.DataFrame]]: + """gets the event data""" + if types is None: + types = [] + + data: Dict[float, Any] = {} + for time in self._events: + data[time] = [] + event = self._events[time] + for i in range(0, len(event["from_x"])): + if event["type"][i] in types: + data[time].append(pd.DataFrame.from_dict({ + "x": [event["from_x"][i], event["to_x"][i]], + "y": [event["from_y"][i], event["to_y"][i]] + })) + return data + + def get_buffer(self, time) -> Dict[int, float]: + capacity = self._settings["ROUTER"].capacity + data = {} + for i in range(0, self._settings["NUM_NODES"]): + messages = self._stores[time][i] + size = sum([msg.size for msg in messages]) + data[i] = (float(size) / float(capacity)) * 100. + return data + + def get_connection_data(self): + return self._connections + + def get_events(self, until: float, exclude_types=None): + if exclude_types is None: + exclude_types = [] + return reversed([str(e) for e in self._events_list if e.time <= until if e.type not in exclude_types]) + + def get_events_as_str(self, until: float, exclude_types=None) -> str: + return "\n".join(self.get_events(until, exclude_types)) + + def update_settings(self, settings: Dict[str, Any]): + """updates the settings""" + self._settings = settings + self._generate() diff --git a/ui/requirements.txt b/ui/requirements.txt index dc147dd..bc250e8 100644 --- a/ui/requirements.txt +++ b/ui/requirements.txt @@ -1,3 +1,4 @@ dash-bootstrap-components dash -plotly \ No newline at end of file +plotly +dash_breakpoints \ No newline at end of file diff --git a/ui/visualization.py b/ui/visualization.py new file mode 100644 index 0000000..41536cf --- /dev/null +++ b/ui/visualization.py @@ -0,0 +1,153 @@ +import math +from enum import Enum +from typing import Dict, Any +from typing import List + +import pandas as pd +import plotly.graph_objects as go + +import pons +import ui + +FIGURE_SIZE = (700, 700) +SIZE_FACTOR = 0.8 + + +class VisualizationStatus(Enum): + PLAY = 0 + PAUSE = 1 + WAIT = 2 + + +class Visualization: + """class handling the visualization of the simulation""" + + def __init__(self, data_manager: "ui.DataManager", settings: Dict[str, Any]): + self._data_manager: ui.DataManager = data_manager + self.on_data_update() + self._settings: Dict[str, Any] = settings + self._frame: int = 0 + self._status: VisualizationStatus = VisualizationStatus.PAUSE + self.fig = None + + def get_figure(self) -> go.Figure: + """ + returns the figure for the current frame + """ + dataframe = self._data[float(self._frame)] + world_size = self._settings["WORLD_SIZE"] + + fig = go.Figure() + + + fig.update_layout(autosize=True, margin={ "l": 0, "r": 0, "t": 0, "b": 0}) + fig.update_yaxes(scaleanchor="x", scaleratio=1) + fig.update_xaxes(title="", range=[0, world_size[0]]) + fig.update_yaxes(title="", range=[0 + 61, world_size[1]]) + + mode = "markers" + ("+text" if self._settings["SHOW_NODE_IDS"] else "") + # core + fig.add_trace(go.Scattergl( + x=dataframe["x"], + y=dataframe["y"], + text=dataframe["node"], + textposition="top center", + marker={ + # seems to max out at like 30-40 for scattergl + "size": 7, + "color": "rgba(204, 102, 255, 255)", + }, + line={"width": 0}, + showlegend=False, + hoverinfo="text", + hovertext=dataframe["stores"], + mode=mode + )) + + # radius + fig.add_trace(go.Scatter( + x=dataframe["x"], + y=dataframe["y"], + marker={ + "size": self._settings["NET_RANGE"] * self._settings["SIZEREF"], + "color": "rgba(0, 0, 0, 0)", + "line": { + "color": "rgba(102, 153, 255, 255)", + "width": 2 + } + }, + hoverinfo="skip", + showlegend=False + )) + + # connections + if self._frame in self._connected_events: + for df in self._connected_events[float(self._frame)]: + fig.add_trace(go.Scattergl( + x=df["x"], + y=df["y"], + showlegend=False, + line={"color": "rgba(0, 102, 204, 255)"}, + mode="lines" + )) + + # events + if self._frame in self._received_events: + for df in self._received_events[float(self._frame)]: + fig.add_trace(go.Scattergl( + x=df["x"], + y=df["y"], + showlegend=False, + line={"color": "rgba(78, 252, 3, 255)"}, + mode="lines" + )) + self.fig = fig + return fig + + def set_settings(self, settings: Dict): + """updates the settings""" + self._settings = settings + + def on_data_update(self): + """handles data update""" + self._data = self._data_manager.get_data() + self._connected_events = self._data_manager.get_connection_data() + self._received_events = self._data_manager.get_event_data([pons.EventType.RECEIVED]) + + def set_frame(self, frame: int): + """sets the frame""" + if frame > self._settings["SIM_TIME"]: + raise KeyError(f'{frame} is not in the simulation time {self._settings["SIM_TIME"]})') + self._frame = frame + + def update_frame(self): + """updates the frame using the frame step from settings""" + self._frame = (self._frame + self._settings["FRAME_STEP"]) % (self._settings["SIM_TIME"]) + + def get_frame(self) -> int: + """gets the current frame""" + return self._frame + + def is_playing(self) -> bool: + """returns true if the visualization is playing""" + return self._status == VisualizationStatus.PLAY + + def is_pausing(self) -> bool: + """returns true if the visualization is pausing""" + return self._status == VisualizationStatus.PAUSE + + def is_waiting(self) -> bool: + """returns true if the visualization is waiting""" + return self._status == VisualizationStatus.WAIT + + def play(self): + """sets the visualization status to PLAY""" + self._status = VisualizationStatus.PLAY + + def pause(self): + """sets the visualization status to PAUSE""" + self._status = VisualizationStatus.PAUSE + + def wait(self): + """sets the visualization status to WAIT""" + self._status = VisualizationStatus.WAIT diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e0e63cb --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1,2 @@ +from utils.list_utils import to_lookup, contains +from utils.misc import get_marks_dict \ No newline at end of file diff --git a/utils/list_utils.py b/utils/list_utils.py new file mode 100644 index 0000000..44a1cd0 --- /dev/null +++ b/utils/list_utils.py @@ -0,0 +1,23 @@ +from typing import List, Callable, Dict, TypeVar + +K = TypeVar("K") +V = TypeVar("V") + + +def to_lookup(values: List[V], key_selector: Callable[[V], K]) -> Dict[K, List[V]]: + """converts a list to a lookup table based on a key selector function""" + lookup = {} + for value in values: + key = key_selector(value) + if key not in lookup: + lookup[key] = [] + lookup[key].append(value) + return lookup + + +def contains(values: List[V], predicate: Callable[[V], bool]) -> bool: + """checks if a list contains a value conforming to a predicate""" + for value in values: + if predicate(value): + return True + return False diff --git a/utils/misc.py b/utils/misc.py new file mode 100644 index 0000000..7700753 --- /dev/null +++ b/utils/misc.py @@ -0,0 +1,2 @@ +def get_marks_dict(min: int, max: int, step: int): + return {step: str(step) for step in range(min, max + 1, step)} \ No newline at end of file From 905af952731374e266631b38b2c2df624db2019d Mon Sep 17 00:00:00 2001 From: grimmfl Date: Mon, 18 Sep 2023 16:32:00 +0200 Subject: [PATCH 5/6] refactor and documentation --- ui/__init__.py | 7 +- ui/app.py | 413 ++++++++++------------------- ui/assets/test.js | 3 - ui/components/__init__.py | 0 ui/components/animation.py | 18 ++ ui/components/animation_control.py | 45 ++++ ui/components/animation_graph.py | 27 ++ ui/components/animation_speed.py | 23 ++ ui/components/layout.py | 30 +++ ui/components/settings.py | 29 ++ ui/components/settings_checkbox.py | 30 +++ ui/components/settings_inputs.py | 19 ++ ui/components/settings_messages.py | 52 ++++ ui/components/settings_sim.py | 82 ++++++ ui/config.py | 18 +- ui/data.py | 68 ++++- ui/visualization.py | 16 +- 17 files changed, 581 insertions(+), 299 deletions(-) delete mode 100644 ui/assets/test.js delete mode 100644 ui/components/__init__.py create mode 100644 ui/components/animation.py create mode 100644 ui/components/animation_control.py create mode 100644 ui/components/animation_graph.py create mode 100644 ui/components/animation_speed.py create mode 100644 ui/components/layout.py create mode 100644 ui/components/settings.py create mode 100644 ui/components/settings_checkbox.py create mode 100644 ui/components/settings_inputs.py create mode 100644 ui/components/settings_messages.py create mode 100644 ui/components/settings_sim.py diff --git a/ui/__init__.py b/ui/__init__.py index cf853c5..158591e 100644 --- a/ui/__init__.py +++ b/ui/__init__.py @@ -1,3 +1,4 @@ -from visualization import Visualization -from data import DataManager -from config import config \ No newline at end of file +from .components.layout import layout_component +from .config import config, ROUTERS +from .data import DataManager +from .visualization import Visualization diff --git a/ui/app.py b/ui/app.py index 45b5bb1..dd4505c 100644 --- a/ui/app.py +++ b/ui/app.py @@ -1,20 +1,9 @@ -import inspect -import math from typing import Tuple import dash_bootstrap_components as dbc -from dash import Dash, html, dcc, no_update, Input, Output, ctx, State -import dash_breakpoints +from dash import Dash, html, no_update, Input, Output, ctx, State -import pons -from ui import Visualization, DataManager, config -import utils - -IGNORE_ROUTERS = ["Router"] - -ROUTERS = {member: (cls(capacity=config.capacity) if "capacity" in inspect.getfullargspec(cls.__init__).args else cls()) - for (member, cls) in inspect.getmembers(pons.routing) if - inspect.isclass(cls) and member not in IGNORE_ROUTERS} +from ui import Visualization, DataManager, layout_component, config, ROUTERS settings = { "SIM_TIME": 3600, @@ -23,9 +12,10 @@ "MIN_SPEED": 1., "MAX_SPEED": 5., "NET_RANGE": 50, - "SIZEREF": 605. / 1000., # fixed value + "SIZEREF": 605. / 1000., "FRAME_STEP": 1, "ROUTER": ROUTERS["EpidemicRouter"], + "ROUTER_NAME": "EpidemicRouter", "SHOW_NODE_IDS": True, "MESSAGES": { "MIN_INTERVAL": 30, @@ -42,226 +32,11 @@ fig = visualization.get_figure() app = Dash(__name__, external_stylesheets=[dbc.themes.COSMO, dbc.icons.BOOTSTRAP]) -# graph = dcc.Graph(figure=fig, id="plot", config={"displayModeBar": False}, className="mt-0 mb-0") - -app.layout = html.Div([ - dbc.Row([ - dbc.Col( - html.Div([ - dbc.Row([ - dbc.Col( - html.Div(["Animation speed"], className="ml-2"), - width=2 - ), - dbc.Col( - dcc.Slider(id="speed_slider", - min=1, - max=50, - step=None, - marks={1: "1", 2: "2", 5: "5", 10: "10", 20: "20", 30: "30", 40: "40", 50: "50"}, - className="w-75"), - width=10 - ), - ], className="mt-2"), - dbc.Row([ - dbc.Col( - html.Div([dcc.Graph(figure=fig, id="plot", config={"displayModeBar": False}, className="mt-0 mb-0 square-content", responsive=True)], className="square"), - width=9, - ), - dbc.Col( - html.Div([html.Div([html.Div([node], className="mt-2 mr-2"), - dbc.Progress(value=0, className="mt-3 buffer w-100")], - className="d-flex justify-content-between") for node in - range(settings["NUM_NODES"])], id="buffer_div"), - width=3 - ) - ]), - dbc.Row([ - dbc.Col( - html.Div([ - dbc.Button( - html.I(id="playIcon", className="bi bi-play"), - id="playButton", - color="primary", - outline=True, - className="ml-2 control-button" - ), - dbc.Button( - html.I(id="stepIcon", className="bi bi-skip-end"), - id="stepButton", - color="primary", - outline=True, - className="ml-2 control-button" - ) - ]), - width=2 - ), - dbc.Col(dcc.Slider(id="anim_slider", - min=0, - max=settings["SIM_TIME"], - value=0, - drag_value=0, - step=10, - className="w-100", - marks={i: '{}'.format(i) for i in - range(0, settings["SIM_TIME"], config.slider_marks_step)}), - width=10), - ]) - ], className="mt-0 mb-2"), width=8), - dbc.Col(html.Div([ - html.Div([ - html.Div([ - dbc.Button( - html.I(className="bi bi-caret-down-fill"), - id="checkbox_collapse_button", - className="mb-3 control-button", - color="primary", - n_clicks=0, - ), - dbc.Collapse( - dbc.Card(dbc.CardBody( - dbc.Checklist( - options=[ - {"label": "Show node ids", "value": 1}, - ], - value=[1], - id="checkbox_input", - ), - )), - id="checkbox_collapse", - is_open=False, - ), - ]), - html.Div([ - dbc.Label('Number of nodes', html_for="num_nodes", className="form-label"), - dbc.Row([ - dbc.Col(dcc.Input(id="num_nodes", - type="range", - min=config.min_num_of_nodes, - max=config.max_num_of_nodes, - step=config.num_of_nodes_step, - value=settings["NUM_NODES"], - className="form-control"), width=10), - dbc.Col(html.Span(settings["NUM_NODES"], id="num_nodes_display"), width=2) - ]) - ]), # number of nodes input - html.Div([ - dbc.Label('Net range', html_for="net_range", className="form-label"), - dbc.Row([ - dbc.Col(dcc.Input(id="net_range", - type="range", - min=config.min_net_range, - max=config.max_net_range, - step=config.net_range_step, - value=settings["NET_RANGE"], - className="form-control"), width=10), - dbc.Col(html.Span(settings["NET_RANGE"], id="net_range_display"), width=2) - ]) - ], className="mt-2"), # net range input - html.Div([ - dbc.Label('Min. speed', html_for="min_speed", className="form-label"), - dbc.Row([ - dbc.Col(dcc.Input(id="min_speed", - type="range", - min=config.min_speed, - max=settings["MAX_SPEED"], - step=config.speed_step, - value=settings["MIN_SPEED"], - className="form-control"), width=10), - dbc.Col(html.Span(settings["MIN_SPEED"], id="min_speed_display"), width=2) - ]) - ], className="mt-2"), # min speed input - html.Div([ - dbc.Label('Max. speed', html_for="max_speed", className="form-label"), - dbc.Row([ - dbc.Col(dcc.Input(id="max_speed", - type="range", - min=settings["MIN_SPEED"], - max=config.max_speed, - step=config.speed_step, - value=settings["MAX_SPEED"], - className="form-control"), width=10), - dbc.Col(html.Span(settings["MAX_SPEED"], id="max_speed_display"), width=2) - ]) - ], className="mt-2"), # min speed input - html.Div([ - dbc.Label('Simulation time', html_for="sim_time", className="form-label"), - dbc.Row([ - dbc.Col(dcc.Input(id="sim_time", - type="range", - min=config.min_sim_time, - max=config.max_sim_time, - step=config.sim_time_step, - value=settings["SIM_TIME"], - className="form-control"), width=10), - dbc.Col(html.Span(settings["SIM_TIME"], id="sim_time_display"), width=2) - ]) - ], className="mt-2"), # sim time input - html.Div([ - dbc.Label('Router', html_for="router", className="form-label"), - dbc.Select(options=list(ROUTERS.keys()), value="EpidemicRouter", id="router") - ], className="mt-2"), # router input - html.Hr(), - html.H6("Messages"), - html.Div([ - dbc.Label('Interval (s)', html_for="msg_interval", className="form-label"), - dcc.RangeSlider(min=config.messages.min_interval, - max=config.messages.max_interval, - step=config.messages.interval_step, - marks=utils.get_marks_dict(config.messages.min_interval, - config.messages.max_interval, - config.messages.interval_step), - value=[settings["MESSAGES"]["MIN_INTERVAL"], settings["MESSAGES"]["MAX_INTERVAL"]], - id="msg_interval") - ], className="mt-2"), # message interval - html.Div([ - dbc.Label('Size', html_for="msg_size", className="form-label"), - dcc.RangeSlider( - min=config.messages.min_size, - max=config.messages.max_size, - step=config.messages.size_step, - marks=utils.get_marks_dict(config.messages.min_size, - config.messages.max_size, - config.messages.size_step), - value=[settings["MESSAGES"]["MIN_SIZE"], settings["MESSAGES"]["MAX_SIZE"]], - id="msg_size") - ], className="mt-2"), # message size - html.Div([ - dbc.Label('Time to live (s)', html_for="msg_ttl", className="form-label"), - dcc.RangeSlider( - min=config.messages.min_ttl, - max=config.messages.max_ttl, - step=config.messages.ttl_step, - marks=utils.get_marks_dict(config.messages.min_ttl, - config.messages.max_ttl, - config.messages.ttl_step), - value=[settings["MESSAGES"]["MIN_TTL"], settings["MESSAGES"]["MAX_TTL"]], - id="msg_ttl" - ) - ], className="mt-2"), # message ttl - ], className="scrollable h-40"), - html.Div([ - dbc.Button("Reset", - id="resetButton", - color="secondary", - className="w-100", - style={"margin-right": "10px"}), - dbc.Button(dcc.Loading("Save", id="save_button_content", type="dot"), - id="saveButton", - color="primary", - className="w-100", - style={"margin-left": "10px"}), - ], className="d-flex justify-content-between mt-3"), # Buttons - html.Div([ - html.Div([], id="event_div", className="event-display") - ], className="mt-3") # Event Display - ], className="mt-5 h-100", style={"margin-right": "30px"}), width=4) - ], className="h-100"), - dcc.Interval(id="anim_interval", interval=config.refresh_interval), - html.Div("", id="size_helper", hidden=True), - dash_breakpoints.WindowBreakpoints(id="breakpoints"), -], className="h-100 no-scroll") +app.layout = layout_component(fig, settings) +# region Callbacks + +# js code getting width and height of plot app.clientside_callback( """ async function getWidth (w, h) { @@ -278,21 +53,6 @@ prevent_initial_call=True ) -@app.callback( - Output('size_helper', 'children', allow_duplicate=True), - Input("size_helper", "children"), - prevent_initial_call=True -) -def test(content): - splitted = content.split(";") - width = int(splitted[0]) - height = int(splitted[1]) - if width == 0: - return no_update - factor = (width + height) / 2. - world_size_factor = (settings["WORLD_SIZE"][0] + settings["WORLD_SIZE"][1]) / 2 - settings["SIZEREF"] = factor / world_size_factor - return factor @app.callback( Output("checkbox_collapse", "is_open"), @@ -300,6 +60,12 @@ def test(content): [State("checkbox_collapse", "is_open")], ) def toggle_collapse(n, is_open): + """ + toggling the checkbox collapse + @param n: n_clicks of checkbox_collapse_button + @param is_open: state of checkbox_collapse is_open property + @return: toggled value for is_open + """ if n: return not is_open return is_open @@ -312,23 +78,44 @@ def toggle_collapse(n, is_open): Output("event_div", "children", allow_duplicate=True), Output("buffer_div", "children", allow_duplicate=True) ], - [Input("anim_slider", "value"), Input("anim_slider", "drag_value"), Input("anim_interval", "n_intervals")], + [ + Input("anim_slider", "value"), + Input("anim_slider", "drag_value"), + Input("anim_interval", "n_intervals") + ], prevent_initial_call=True ) def animate(slider_value, drag_value, n_intervals): + """ + handles all animations + @param slider_value: value of the animation slider + @param drag_value: drag value of the animation slider + @param n_intervals: current animation interval value + @return: tuple consisting of the plot figure, + the new slider value, the event_div content and the buffer_div content + """ + # if slider value has been triggered if "anim_slider.value" in ctx.triggered_prop_ids: + # if visualization is currently waiting because of drag_value if visualization.is_waiting(): + # start to play visualization.play() + # set the visualization frame to the slider value visualization.set_frame(int(slider_value)) return _get_animation_outputs() + # else if visualization is currently not playing, do not change anything if not visualization.is_playing(): return _get_animation_outputs() + # else if drag value is triggered and it differs from the slider value if "anim_slider.drag_value" in ctx.triggered_prop_ids and slider_value != drag_value: + # wait and do not change anything visualization.wait() return no_update + # if the interval value has changed if "anim_interval.n_intervals" in ctx.triggered_prop_ids: if n_intervals is None: return no_update + # update the frame visualization.update_frame() return _get_animation_outputs() @@ -336,11 +123,18 @@ def animate(slider_value, drag_value, n_intervals): def _get_animation_outputs() -> Tuple: + """ + returns the animation outputs as a tuple + """ frame = visualization.get_frame() return ( + # plot figure visualization.get_figure(), + # current frame frame, + # event_div content [html.Div([str(e)]) for e in data_manager.get_events(frame)], + # buffer_div content [html.Div([html.Div([key], className="mt-2 mr-2"), dbc.Progress(value=value, className="mt-3 buffer w-100")], className="d-flex justify-content-between") for key, value in @@ -354,6 +148,11 @@ def _get_animation_outputs() -> Tuple: prevent_initial_call=True ) def on_play_click(n_clicks): + """ + handling the play click + @param n_clicks: n_clicks of playButton + @return: new playIcon class + """ if visualization.is_playing(): visualization.pause() return "bi bi-play" @@ -367,18 +166,15 @@ def on_play_click(n_clicks): prevent_initial_call=True ) def on_speed_drag(value): + """ + handling the speed slider + @param value: the drag_value of the speed slider + @return: the slider value of the speed slider + """ settings["FRAME_STEP"] = value visualization.set_settings(settings) return no_update -@app.callback( - Output("plot", "figure"), - Input("plot", "figure"), - prevent_initial_call=True -) -def onplot(figure): - return no_update - @app.callback( [ @@ -391,17 +187,15 @@ def onplot(figure): prevent_initial_call=True ) def on_step_click(n_clicks): + """ + handles a click on the step button + @param n_clicks: n_clicks of the stepButton + @return: tuple consisting of the plot figure, + the new slider value, the event_div content and the buffer_div content + """ frame = (visualization.get_frame() + settings["FRAME_STEP"]) % settings["SIM_TIME"] visualization.set_frame(frame) - return ( - visualization.get_figure(), - frame, - [html.Div([str(e)]) for e in data_manager.get_events(frame)], - [html.Div([html.Div([key], className="mt-2 mr-2"), - dbc.Progress(value=value, className="mt-3 buffer w-100")], - className="d-flex justify-content-between") for key, value in - data_manager.get_buffer(frame).items()] - ) + return _get_animation_outputs() @app.callback( @@ -410,6 +204,11 @@ def on_step_click(n_clicks): prevent_initial_call=True ) def on_num_nodes(value): + """ + forwards input change of num_nodes slider to the display + @param value: the slider value of num_nodes + @return: the value for the display + """ return value @@ -419,6 +218,11 @@ def on_num_nodes(value): prevent_initial_call=True ) def on_net_range(value): + """ + forwards input change of net_range slider to the display + @param value: the slider value of net_range + @return: the value for the display + """ return value @@ -428,6 +232,11 @@ def on_net_range(value): prevent_initial_call=True ) def on_max_speed(value): + """ + forwards input change of max_speed slider to the display and the max of min_speed + @param value: the slider value of max_speed + @return: a tuple consisting of the value for the display and the max value for min_speed + """ return value, value @@ -437,6 +246,11 @@ def on_max_speed(value): prevent_initial_call=True ) def on_min_speed(value): + """ + forwards input change of min_speed slider to the display and the min of max_speed + @param value: the slider value of min_speed + @return: a tuple consisting of the value for the display and the min value for max_speed + """ return value, value @@ -446,6 +260,11 @@ def on_min_speed(value): prevent_initial_call=True ) def on_sim_time(value): + """ + forwards input change of sim_time slider to the display + @param value: the slider value of sim_time + @return: the value for the display + """ return value @@ -474,30 +293,58 @@ def on_sim_time(value): prevent_initial_call=True ) def on_save(n_clicks, num_nodes, net_range, min_speed, max_speed, sim_time, router, msg_interval, msg_size, msg_ttl): + """ + saves changes to settings + @param n_clicks: n_clicks of the saveButton + @param num_nodes: state of num_nodes slider + @param net_range: state of net_range slider + @param min_speed: state of min_speed slider + @param max_speed: state of max_speed slider + @param sim_time: state of sim_time slider + @param router: state of router dropdown + @param msg_interval: state of msg_interval + @param msg_size: state of msg_size + @param msg_ttl: state of msg_ttl + @return: a tuple consisting of the plot figure, the play icon class, + the value of the animation slider, the max of the animation slider, + the content of the save button, the animation interval + """ + # pause the visualization visualization.pause() sim_time = int(sim_time) + # update the settings settings["NUM_NODES"] = int(num_nodes) settings["NET_RANGE"] = int(net_range) settings["MIN_SPEED"] = float(min_speed) settings["MAX_SPEED"] = float(max_speed) settings["SIM_TIME"] = sim_time - settings["ROUTER_CLASS"] = ROUTERS[router] + settings["ROUTER"] = ROUTERS[router] + settings["ROUTER_NAME"] = router settings["MESSAGES"]["MIN_INTERVAL"] = msg_interval[0] settings["MESSAGES"]["MAX_INTERVAL"] = msg_interval[1] settings["MESSAGES"]["MIN_SIZE"] = msg_size[0] settings["MESSAGES"]["MAX_SIZE"] = msg_size[1] settings["MESSAGES"]["MIN_TTL"] = msg_ttl[0] settings["MESSAGES"]["MAX_TTL"] = msg_ttl[1] + # forward the settings to all channels data_manager.update_settings(settings) visualization.set_settings(settings) + # let the visualization know, that data has changed visualization.on_data_update() + # start animation from start visualization.set_frame(0) return ( + # figure visualization.get_figure(), + # play icon "bi bi-play-fill", + # animation slider value 0, + # animation slider max sim_time, {i: '{}'.format(i) for i in range(0, sim_time, int(sim_time / 20))}, + # save button content "Save", + # animation interval config.refresh_interval ) @@ -507,13 +354,34 @@ def on_save(n_clicks, num_nodes, net_range, min_speed, max_speed, sim_time, rout Output("num_nodes", "value", allow_duplicate=True), Output("net_range", "value", allow_duplicate=True), Output("min_speed", "value", allow_duplicate=True), - Output("max_speed", "value", allow_duplicate=True) + Output("max_speed", "value", allow_duplicate=True), + Output("sim_time", "value", allow_duplicate=True), + Output("router", "value", allow_duplicate=True), + Output("msg_interval", "value", allow_duplicate=True), + Output("msg_size", "value", allow_duplicate=True), + Output("msg_ttl", "value", allow_duplicate=True), ], Input("resetButton", "n_clicks"), prevent_initial_call=True ) def on_reset(n_clicks): - return settings["NUM_NODES"], settings["NET_RANGE"], settings["MIN_SPEED"], settings["MAX_SPEED"], + """ + resets the inputs + @param n_clicks: n_clicks of resetButton + @return: tuple consisting of num_nodes, net_range, min_speed, max_speed, + sim_time, router, msg_interval, msg_size, msg_ttl + """ + return ( + settings["NUM_NODES"], + settings["NET_RANGE"], + settings["MIN_SPEED"], + settings["MAX_SPEED"], + settings["SIM_TIME"], + settings["ROUTER_NAME"], + [settings["MESSAGES"]["MIN_INTERVAL"], settings["MESSAGES"]["MAX_INTERVAL"]], + [settings["MESSAGES"]["MIN_SIZE"], settings["MESSAGES"]["MAX_SIZE"]], + [settings["MESSAGES"]["MIN_TTL"], settings["MESSAGES"]["MAX_TTL"]], + ) @app.callback( @@ -522,9 +390,16 @@ def on_reset(n_clicks): prevent_initial_call=True ) def on_checkbox_input(value): + """ + shows node ids based on checkbox value + @param value: the checkbox value + @return: the plot figure + """ settings["SHOW_NODE_IDS"] = 1 in value return visualization.get_figure() +# endregion + if __name__ == "__main__": app.run(debug=True, host="0.0.0.0") diff --git a/ui/assets/test.js b/ui/assets/test.js deleted file mode 100644 index 579fa2e..0000000 --- a/ui/assets/test.js +++ /dev/null @@ -1,3 +0,0 @@ -console.log("hallo"); - -console.log(document.getElementById("plot")); \ No newline at end of file diff --git a/ui/components/__init__.py b/ui/components/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/ui/components/animation.py b/ui/components/animation.py new file mode 100644 index 0000000..25adc10 --- /dev/null +++ b/ui/components/animation.py @@ -0,0 +1,18 @@ +from dash import html + +from .animation_control import animation_control_component +from .animation_graph import animation_graph_component +from .animation_speed import animation_speed_component + + +def animation_component(fig, settings): + """ + builds all parts regarding animation (graphs, slider, speed, etc.) + @param fig: the plotly figure + @param settings: the settings defined in app.py + """ + return html.Div([ + animation_speed_component(), + animation_graph_component(fig, settings), + animation_control_component(settings) + ], className="mt-0 mb-2") diff --git a/ui/components/animation_control.py b/ui/components/animation_control.py new file mode 100644 index 0000000..a7afa5f --- /dev/null +++ b/ui/components/animation_control.py @@ -0,0 +1,45 @@ +import dash_bootstrap_components as dbc +from dash import html, dcc + +from ..config import config + + +def animation_control_component(settings): + """ + builds the animation slider and step forward buttons + @param settings: the settings defined in app.py + """ + return dbc.Row([ + dbc.Col( + html.Div([ + dbc.Button( + html.I(id="playIcon", className="bi bi-play"), + id="playButton", + color="primary", + outline=True, + className="ml-2 control-button" + ), + dbc.Button( + html.I(id="stepIcon", className="bi bi-skip-end"), + id="stepButton", + color="primary", + outline=True, + className="ml-2 control-button" + ) + ]), + width=2 + ), + dbc.Col( + dcc.Slider( + id="anim_slider", + min=0, + max=settings["SIM_TIME"], + value=0, + drag_value=0, + step=10, + className="w-100", + marks={i: '{}'.format(i) for i in + range(0, settings["SIM_TIME"], config.slider_marks_step)} + ), + width=10), + ]) diff --git a/ui/components/animation_graph.py b/ui/components/animation_graph.py new file mode 100644 index 0000000..d8cb80d --- /dev/null +++ b/ui/components/animation_graph.py @@ -0,0 +1,27 @@ +import dash_bootstrap_components as dbc +from dash import html, dcc + + +def animation_graph_component(fig, settings): + """ + builds the graphs for the animation. + @param fig: the plotly figure + @param settings: the settings defined in app.py + """ + return dbc.Row([ + dbc.Col( + html.Div([dcc.Graph(figure=fig, + id="plot", + config={"displayModeBar": False}, + className="mt-0 mb-0 square-content", + responsive=True)], className="square"), + width=9, + ), + dbc.Col( + html.Div([html.Div([html.Div([node], className="mt-2 mr-2"), + dbc.Progress(value=0, className="mt-3 buffer w-100")], + className="d-flex justify-content-between") for node in + range(settings["NUM_NODES"])], id="buffer_div"), + width=3 + ) + ]) diff --git a/ui/components/animation_speed.py b/ui/components/animation_speed.py new file mode 100644 index 0000000..2ac3987 --- /dev/null +++ b/ui/components/animation_speed.py @@ -0,0 +1,23 @@ +import dash_bootstrap_components as dbc +from dash import html, dcc + + +def animation_speed_component(): + """ + builds the speed control for the animation + """ + return dbc.Row([ + dbc.Col( + html.Div(["Animation speed"], className="ml-2"), + width=2 + ), + dbc.Col( + dcc.Slider(id="speed_slider", + min=1, + max=50, + step=None, + marks={1: "1", 2: "2", 5: "5", 10: "10", 20: "20", 30: "30", 40: "40", 50: "50"}, + className="w-75"), + width=10 + ) + ], className="mt-2") diff --git a/ui/components/layout.py b/ui/components/layout.py new file mode 100644 index 0000000..e53f020 --- /dev/null +++ b/ui/components/layout.py @@ -0,0 +1,30 @@ +import dash_bootstrap_components as dbc +import dash_breakpoints +from dash import html, dcc + +from .animation import animation_component +from .settings import settings_component +from ..config import config + + +def layout_component(fig, settings): + """ + builds the apps layout + @param fig: the plotly figure + @param settings: the settings defined in app.py + """ + return html.Div([ + dbc.Row([ + dbc.Col( + animation_component(fig, settings), + width=8 + ), + dbc.Col( + settings_component(settings), + width=4 + ) + ], className="h-100"), + dcc.Interval(id="anim_interval", interval=config.refresh_interval), + html.Div("", id="size_helper", hidden=True), + dash_breakpoints.WindowBreakpoints(id="breakpoints"), + ], className="h-100 no-scroll") diff --git a/ui/components/settings.py b/ui/components/settings.py new file mode 100644 index 0000000..50f7672 --- /dev/null +++ b/ui/components/settings.py @@ -0,0 +1,29 @@ +import dash_bootstrap_components as dbc +from dash import html, dcc + +from .settings_inputs import settings_inputs_component + + +def settings_component(settings): + """ + builds the settings part of the app + @param settings: the settings defined in app.py + """ + return html.Div([ + settings_inputs_component(settings), + html.Div([ + dbc.Button("Reset", + id="resetButton", + color="secondary", + className="w-100", + style={"margin-right": "10px"}), + dbc.Button(dcc.Loading("Save", id="save_button_content", type="dot"), + id="saveButton", + color="primary", + className="w-100", + style={"margin-left": "10px"}), + ], className="d-flex justify-content-between mt-3"), # Buttons + html.Div([ + html.Div([], id="event_div", className="event-display") + ], className="mt-3") # Event Display + ], className="mt-5 h-100", style={"margin-right": "30px"}) diff --git a/ui/components/settings_checkbox.py b/ui/components/settings_checkbox.py new file mode 100644 index 0000000..e12b595 --- /dev/null +++ b/ui/components/settings_checkbox.py @@ -0,0 +1,30 @@ +import dash_bootstrap_components as dbc +from dash import html + + +def settings_checkbox_component(): + """ + builds the checkbox settings + """ + return html.Div([ + dbc.Button( + html.I(className="bi bi-caret-down-fill"), + id="checkbox_collapse_button", + className="mb-3 control-button", + color="primary", + n_clicks=0, + ), + dbc.Collapse( + dbc.Card(dbc.CardBody( + dbc.Checklist( + options=[ + {"label": "Show node ids", "value": 1}, + ], + value=[1], + id="checkbox_input", + ), + )), + id="checkbox_collapse", + is_open=False, + ), + ]) diff --git a/ui/components/settings_inputs.py b/ui/components/settings_inputs.py new file mode 100644 index 0000000..8a517ad --- /dev/null +++ b/ui/components/settings_inputs.py @@ -0,0 +1,19 @@ +from dash import html + +from .settings_checkbox import settings_checkbox_component +from .settings_messages import settings_messages_component +from .settings_sim import settings_sim_component + + +def settings_inputs_component(settings): + """ + builds the settings inputs + @param settings: the settings defined in app.py + """ + return html.Div( + [settings_checkbox_component()] + + settings_sim_component(settings) + + [html.Hr()] + + settings_messages_component(settings), + className="scrollable h-40" + ) diff --git a/ui/components/settings_messages.py b/ui/components/settings_messages.py new file mode 100644 index 0000000..e0bd683 --- /dev/null +++ b/ui/components/settings_messages.py @@ -0,0 +1,52 @@ +import dash_bootstrap_components as dbc +from dash import html, dcc + +import utils +from ..config import config + + +def settings_messages_component(settings): + """ + builds the message settings + @param settings: the settings defined in app.py + """ + return [ + html.H6("Messages"), + html.Div([ + dbc.Label('Interval (s)', html_for="msg_interval", className="form-label"), + dcc.RangeSlider(min=config.messages.min_interval, + max=config.messages.max_interval, + step=config.messages.interval_step, + marks=utils.get_marks_dict(config.messages.min_interval, + config.messages.max_interval, + config.messages.interval_step), + value=[settings["MESSAGES"]["MIN_INTERVAL"], + settings["MESSAGES"]["MAX_INTERVAL"]], + id="msg_interval") + ], className="mt-2"), # message interval + html.Div([ + dbc.Label('Size', html_for="msg_size", className="form-label"), + dcc.RangeSlider( + min=config.messages.min_size, + max=config.messages.max_size, + step=config.messages.size_step, + marks=utils.get_marks_dict(config.messages.min_size, + config.messages.max_size, + config.messages.size_step), + value=[settings["MESSAGES"]["MIN_SIZE"], settings["MESSAGES"]["MAX_SIZE"]], + id="msg_size") + ], className="mt-2"), # message size + html.Div([ + dbc.Label('Time to live (s)', html_for="msg_ttl", className="form-label"), + dcc.RangeSlider( + min=config.messages.min_ttl, + max=config.messages.max_ttl, + step=config.messages.ttl_step, + marks=utils.get_marks_dict(config.messages.min_ttl, + config.messages.max_ttl, + config.messages.ttl_step), + value=[settings["MESSAGES"]["MIN_TTL"], settings["MESSAGES"]["MAX_TTL"]], + id="msg_ttl" + ) + ], className="mt-2") # message ttl + ] diff --git a/ui/components/settings_sim.py b/ui/components/settings_sim.py new file mode 100644 index 0000000..c9c43e2 --- /dev/null +++ b/ui/components/settings_sim.py @@ -0,0 +1,82 @@ +import dash_bootstrap_components as dbc +from dash import html, dcc + +from ..config import config, ROUTERS + + +def settings_sim_component(settings): + """ + builds the simulation settings + @param settings: the settings defined in app.py + """ + return [ + html.Div([ + dbc.Label('Number of nodes', html_for="num_nodes", className="form-label"), + dbc.Row([ + dbc.Col(dcc.Input(id="num_nodes", + type="range", + min=config.min_num_of_nodes, + max=config.max_num_of_nodes, + step=config.num_of_nodes_step, + value=settings["NUM_NODES"], + className="form-control"), width=10), + dbc.Col(html.Span(settings["NUM_NODES"], id="num_nodes_display"), width=2) + ]) + ]), # number of nodes input + html.Div([ + dbc.Label('Net range', html_for="net_range", className="form-label"), + dbc.Row([ + dbc.Col(dcc.Input(id="net_range", + type="range", + min=config.min_net_range, + max=config.max_net_range, + step=config.net_range_step, + value=settings["NET_RANGE"], + className="form-control"), width=10), + dbc.Col(html.Span(settings["NET_RANGE"], id="net_range_display"), width=2) + ]) + ], className="mt-2"), # net range input + html.Div([ + dbc.Label('Min. speed', html_for="min_speed", className="form-label"), + dbc.Row([ + dbc.Col(dcc.Input(id="min_speed", + type="range", + min=config.min_speed, + max=settings["MAX_SPEED"], + step=config.speed_step, + value=settings["MIN_SPEED"], + className="form-control"), width=10), + dbc.Col(html.Span(settings["MIN_SPEED"], id="min_speed_display"), width=2) + ]) + ], className="mt-2"), # min speed input + html.Div([ + dbc.Label('Max. speed', html_for="max_speed", className="form-label"), + dbc.Row([ + dbc.Col(dcc.Input(id="max_speed", + type="range", + min=settings["MIN_SPEED"], + max=config.max_speed, + step=config.speed_step, + value=settings["MAX_SPEED"], + className="form-control"), width=10), + dbc.Col(html.Span(settings["MAX_SPEED"], id="max_speed_display"), width=2) + ]) + ], className="mt-2"), # min speed input + html.Div([ + dbc.Label('Simulation time', html_for="sim_time", className="form-label"), + dbc.Row([ + dbc.Col(dcc.Input(id="sim_time", + type="range", + min=config.min_sim_time, + max=config.max_sim_time, + step=config.sim_time_step, + value=settings["SIM_TIME"], + className="form-control"), width=10), + dbc.Col(html.Span(settings["SIM_TIME"], id="sim_time_display"), width=2) + ]) + ], className="mt-2"), # sim time input + html.Div([ + dbc.Label('Router', html_for="router", className="form-label"), + dbc.Select(options=list(ROUTERS.keys()), value="EpidemicRouter", id="router") + ], className="mt-2") # router input + ] diff --git a/ui/config.py b/ui/config.py index 65ded2f..c944469 100644 --- a/ui/config.py +++ b/ui/config.py @@ -1,11 +1,16 @@ +import inspect from dataclasses import dataclass -from dataclasses_json import dataclass_json, LetterCase from typing import List +from dataclasses_json import dataclass_json, LetterCase + +import pons + @dataclass_json(letter_case=LetterCase.CAMEL) @dataclass class MessageConfig: + """config class for messages""" min_interval: int max_interval: int interval_step: int @@ -20,6 +25,7 @@ class MessageConfig: @dataclass_json(letter_case=LetterCase.CAMEL) @dataclass class Config: + """config class""" min_sim_time: int max_sim_time: int sim_time_step: int @@ -40,4 +46,12 @@ class Config: with open("config.json", "r") as file: - config = Config.from_json(file.read()) \ No newline at end of file + config = Config.from_json(file.read()) + +IGNORE_ROUTERS = ["Router"] + +ROUTERS = { + member: (cls(capacity=config.capacity) if "capacity" in inspect.getfullargspec(cls.__init__).args else cls()) + for (member, cls) in inspect.getmembers(pons.routing) if + inspect.isclass(cls) and member not in IGNORE_ROUTERS +} diff --git a/ui/data.py b/ui/data.py index 324e1f9..3d24d72 100644 --- a/ui/data.py +++ b/ui/data.py @@ -6,12 +6,13 @@ import pons RANDOM_SEED = 42 -CAPACITY = 10000 random.seed(RANDOM_SEED) class DataManager: + """handling everything regarding the data preparation for the visualization""" + def __init__(self, settings: Dict[str, Any]): self._settings: Dict[str, Any] = settings self._moves: List[Tuple[float, int, int, int]] = [] @@ -34,6 +35,7 @@ def _generate_movement(self) -> List[Tuple[float, int, int, int]]: max_speed=self._settings["MAX_SPEED"]) def _simulate(self): + """simulations as shown in sim.py""" self._moves = self._generate_movement() num_nodes = self._settings["NUM_NODES"] @@ -86,6 +88,7 @@ def _add_missing_data(self, data: Dict[float, Dict[str, List]], helper: Dict[flo self._add(data, helper, time, node, x, y) def _generate_data(self): + """generates movement data""" data = {} helper = {} for move in self._moves: @@ -102,6 +105,7 @@ def _generate_data(self): self._helper = helper def _add_event(self, target: Dict[float, Dict[str, List]], event: pons.Event): + """adds event to event data""" time = event.time if time not in self._helper or event.from_node not in self._helper[time]: return @@ -114,6 +118,7 @@ def _add_event(self, target: Dict[float, Dict[str, List]], event: pons.Event): target[time]["to_y"].append(to_y) def _generate_event_data(self): + """generates event data""" data = {} for event in self._events_list: if event.time not in data: @@ -122,19 +127,28 @@ def _generate_event_data(self): self._events = data def _generate_connections(self): + """generates connection data""" data = {} current_conns = set() + # for each time for time in range(0, self._settings["SIM_TIME"]): data[time] = [] + # for every event that takes place at that time for event in [event for event in self._events_list if event.time == time]: + # if event is CONNECTION_UP if event.type == pons.EventType.CONNECTION_UP: + # add connection to current_conns current_conns.add(frozenset((event.node, event.from_node))) + # else if event is CONNECTION_DOWN elif event.type == pons.EventType.CONNECTION_DOWN: + # remove event from current_conns conn = frozenset((event.node, event.from_node)) if conn in current_conns: current_conns.remove(conn) + # for each connection for conn in current_conns: node1, node2 = tuple(conn) + # extract coordinates and append to data from_x, from_y = self._helper[time][node1] to_x, to_y = self._helper[time][node2] data[time].append(pd.DataFrame.from_dict({ @@ -144,21 +158,31 @@ def _generate_connections(self): self._connections = data def _generate_stores(self): - stores = {} - stores[0] = {i: set() for i in range(0, self._settings["NUM_NODES"])} + """generates the store data""" + # set stores for time=0 to empty sets + stores = {0: {i: set() for i in range(0, self._settings["NUM_NODES"])}} + # for each time starting with 1 for time in range(1, self._settings["SIM_TIME"]): + # copy the stores of time - 1 stores[time] = {i: stores[time - 1][i].copy() for i in range(0, self._settings["NUM_NODES"])} + # for each event for event in self._events_list: + # that takes place at the time if time != event.time: continue + # if event is of types CREATED, RECEIVED or DELIVERED if event.type in [pons.EventType.CREATED, pons.EventType.RECEIVED, pons.EventType.DELIVERED]: + # add the message to the store stores[time][event.node].add(event.message) + # else if event is of type DROPPED elif event.type == pons.EventType.DROPPED: if event.message in stores[time][event.node]: + # remove the message from the store stores[time][event.node].remove(event.message) self._stores = stores def _generate(self): + """generates all data""" self._simulate() self._generate_stores() self._generate_data() @@ -167,6 +191,7 @@ def _generate(self): @staticmethod def _to_dataframes(data: Dict[float, Dict[str, List]]) -> Dict[float, pd.DataFrame]: + """converts dicts to dataframes""" dfs = {} for time in data: dfs[time] = pd.DataFrame.from_dict(data[time]) @@ -176,11 +201,12 @@ def get_data(self) -> Dict[float, pd.DataFrame]: """gets the movement data""" return self._to_dataframes(self._data) - def get_event_data(self, types=None) -> Dict[float, List[pd.DataFrame]]: - """gets the event data""" - if types is None: - types = [] - + def get_event_data(self, types) -> Dict[float, List[pd.DataFrame]]: + """ + loads event data for given types (used for message relay visualization) + @param types: list of event types + @return: dict mapping time to event dataframe + """ data: Dict[float, Any] = {} for time in self._events: data[time] = [] @@ -193,27 +219,43 @@ def get_event_data(self, types=None) -> Dict[float, List[pd.DataFrame]]: })) return data - def get_buffer(self, time) -> Dict[int, float]: + def get_buffer(self, time: float) -> Dict[int, float]: + """ + gets the data for the buffer animation + @param time: current time + @return: dict mapping node to buffer fullness + """ capacity = self._settings["ROUTER"].capacity data = {} + # for every node for i in range(0, self._settings["NUM_NODES"]): + # get current stored messages messages = self._stores[time][i] + # calculate total size size = sum([msg.size for msg in messages]) + # calculate fullness data[i] = (float(size) / float(capacity)) * 100. return data def get_connection_data(self): + """gets the connection data between nodes""" return self._connections def get_events(self, until: float, exclude_types=None): + """ + get events until given time with the possibility to exclude event types + @param until: time until the events should be returned + @param exclude_types: (optional) list of types to exclude + @return: events as strings in order + """ if exclude_types is None: exclude_types = [] return reversed([str(e) for e in self._events_list if e.time <= until if e.type not in exclude_types]) - def get_events_as_str(self, until: float, exclude_types=None) -> str: - return "\n".join(self.get_events(until, exclude_types)) - def update_settings(self, settings: Dict[str, Any]): - """updates the settings""" + """ + updates settings + @param settings: the settings dict from app.py + """ self._settings = settings self._generate() diff --git a/ui/visualization.py b/ui/visualization.py index 41536cf..616d8db 100644 --- a/ui/visualization.py +++ b/ui/visualization.py @@ -1,19 +1,14 @@ -import math from enum import Enum from typing import Dict, Any -from typing import List -import pandas as pd import plotly.graph_objects as go import pons import ui -FIGURE_SIZE = (700, 700) -SIZE_FACTOR = 0.8 - class VisualizationStatus(Enum): + """visualization status enum""" PLAY = 0 PAUSE = 1 WAIT = 2 @@ -39,14 +34,17 @@ def get_figure(self) -> go.Figure: fig = go.Figure() - + # remove all margins fig.update_layout(autosize=True, margin={ "l": 0, "r": 0, "t": 0, "b": 0}) fig.update_yaxes(scaleanchor="x", scaleratio=1) fig.update_xaxes(title="", range=[0, world_size[0]]) + # removes double 0 fig.update_yaxes(title="", range=[0 + 61, world_size[1]]) + # show node ids of SHOW_NODE_IDS is true mode = "markers" + ("+text" if self._settings["SHOW_NODE_IDS"] else "") - # core + + # core of nodes fig.add_trace(go.Scattergl( x=dataframe["x"], y=dataframe["y"], @@ -64,7 +62,7 @@ def get_figure(self) -> go.Figure: mode=mode )) - # radius + # radius of ndoes fig.add_trace(go.Scatter( x=dataframe["x"], y=dataframe["y"], From a9230c642adccd4958ad4b12caa21ffc3cbe3b28 Mon Sep 17 00:00:00 2001 From: fgrimm Date: Wed, 20 Sep 2023 16:10:24 +0200 Subject: [PATCH 6/6] refactor and documentation --- ui/app.py | 19 ++++++++++---- ui/components/animation_control.py | 2 +- ui/components/animation_speed.py | 4 ++- ui/config.py | 4 ++- ui/data.py | 40 ++++++++++++++++++++++-------- utils/__init__.py | 2 +- 6 files changed, 52 insertions(+), 19 deletions(-) diff --git a/ui/app.py b/ui/app.py index dd4505c..1906590 100644 --- a/ui/app.py +++ b/ui/app.py @@ -59,14 +59,14 @@ [Input("checkbox_collapse_button", "n_clicks")], [State("checkbox_collapse", "is_open")], ) -def toggle_collapse(n, is_open): +def toggle_collapse(n_clicks, is_open): """ toggling the checkbox collapse - @param n: n_clicks of checkbox_collapse_button + @param n_clicks: n_clicks of checkbox_collapse_button @param is_open: state of checkbox_collapse is_open property @return: toggled value for is_open """ - if n: + if n_clicks: return not is_open return is_open @@ -292,7 +292,16 @@ def on_sim_time(value): ], prevent_initial_call=True ) -def on_save(n_clicks, num_nodes, net_range, min_speed, max_speed, sim_time, router, msg_interval, msg_size, msg_ttl): +def on_save(n_clicks, + num_nodes, + net_range, + min_speed, + max_speed, + sim_time, + router, + msg_interval, + msg_size, + msg_ttl): """ saves changes to settings @param n_clicks: n_clicks of the saveButton @@ -341,7 +350,7 @@ def on_save(n_clicks, num_nodes, net_range, min_speed, max_speed, sim_time, rout # animation slider value 0, # animation slider max - sim_time, {i: '{}'.format(i) for i in range(0, sim_time, int(sim_time / 20))}, + sim_time, {i: f"{i}" for i in range(0, sim_time, int(sim_time / 20))}, # save button content "Save", # animation interval diff --git a/ui/components/animation_control.py b/ui/components/animation_control.py index a7afa5f..b355f81 100644 --- a/ui/components/animation_control.py +++ b/ui/components/animation_control.py @@ -38,7 +38,7 @@ def animation_control_component(settings): drag_value=0, step=10, className="w-100", - marks={i: '{}'.format(i) for i in + marks={i: f'{i}' for i in range(0, settings["SIM_TIME"], config.slider_marks_step)} ), width=10), diff --git a/ui/components/animation_speed.py b/ui/components/animation_speed.py index 2ac3987..ff72614 100644 --- a/ui/components/animation_speed.py +++ b/ui/components/animation_speed.py @@ -16,7 +16,9 @@ def animation_speed_component(): min=1, max=50, step=None, - marks={1: "1", 2: "2", 5: "5", 10: "10", 20: "20", 30: "30", 40: "40", 50: "50"}, + marks={ + 1: "1", 2: "2", 5: "5", 10: "10", 20: "20", 30: "30", 40: "40", 50: "50" + }, className="w-75"), width=10 ) diff --git a/ui/config.py b/ui/config.py index c944469..3e69b12 100644 --- a/ui/config.py +++ b/ui/config.py @@ -51,7 +51,9 @@ class Config: IGNORE_ROUTERS = ["Router"] ROUTERS = { - member: (cls(capacity=config.capacity) if "capacity" in inspect.getfullargspec(cls.__init__).args else cls()) + member: (cls(capacity=config.capacity) + if "capacity" in inspect.getfullargspec(cls.__init__).args + else cls()) for (member, cls) in inspect.getmembers(pons.routing) if inspect.isclass(cls) and member not in IGNORE_ROUTERS } diff --git a/ui/data.py b/ui/data.py index 3d24d72..b05099c 100644 --- a/ui/data.py +++ b/ui/data.py @@ -47,16 +47,25 @@ def _simulate(self): msggenconfig = { "type": "single", - "interval": (self._settings["MESSAGES"]["MIN_INTERVAL"], self._settings["MESSAGES"]["MAX_INTERVAL"]), + "interval": ( + self._settings["MESSAGES"]["MIN_INTERVAL"], + self._settings["MESSAGES"]["MAX_INTERVAL"] + ), "src": (0, num_nodes), "dst": (0, num_nodes), - "size": (self._settings["MESSAGES"]["MIN_SIZE"], self._settings["MESSAGES"]["MAX_SIZE"]), + "size": ( + self._settings["MESSAGES"]["MIN_SIZE"], + self._settings["MESSAGES"]["MAX_SIZE"] + ), "id": "M", - "ttl": (self._settings["MESSAGES"]["MIN_TTL"], self._settings["MESSAGES"]["MAX_TTL"]) + "ttl": ( + self._settings["MESSAGES"]["MIN_TTL"], + self._settings["MESSAGES"]["MAX_TTL"] + ) } - netsim = pons.NetSim(self._settings["SIM_TIME"], self._settings["WORLD_SIZE"], nodes, self._moves, - config=config, msggens=[msggenconfig]) + netsim = pons.NetSim(self._settings["SIM_TIME"], self._settings["WORLD_SIZE"], nodes, + self._moves, config=config, msggens=[msggenconfig]) netsim.setup() netsim.run() @@ -72,10 +81,15 @@ def _add(self, data[time]["node"].append(str(node)) data[time]["x"].append(x) data[time]["y"].append(y) - data[time]["stores"].append(f"{node}
{'
'.join([msg.id for msg in self._stores[time][node]])}") + data[time]["stores"].append( + f"{node}
{'
'.join([msg.id for msg in self._stores[time][node]])}" + ) helper[time][node] = (x, y) - def _add_missing_data(self, data: Dict[float, Dict[str, List]], helper: Dict[float, Dict[int, Tuple[int, int]]]): + def _add_missing_data(self, + data: Dict[float, Dict[str, List]], + helper: Dict[float, Dict[int, Tuple[int, int]]] + ): for time in range(self._settings["SIM_TIME"]): time = float(time) if time not in data: @@ -164,14 +178,18 @@ def _generate_stores(self): # for each time starting with 1 for time in range(1, self._settings["SIM_TIME"]): # copy the stores of time - 1 - stores[time] = {i: stores[time - 1][i].copy() for i in range(0, self._settings["NUM_NODES"])} + stores[time] = { + i: stores[time - 1][i].copy() for i in range(0, self._settings["NUM_NODES"]) + } # for each event for event in self._events_list: # that takes place at the time if time != event.time: continue # if event is of types CREATED, RECEIVED or DELIVERED - if event.type in [pons.EventType.CREATED, pons.EventType.RECEIVED, pons.EventType.DELIVERED]: + if event.type in [ + pons.EventType.CREATED, pons.EventType.RECEIVED, pons.EventType.DELIVERED + ]: # add the message to the store stores[time][event.node].add(event.message) # else if event is of type DROPPED @@ -250,7 +268,9 @@ def get_events(self, until: float, exclude_types=None): """ if exclude_types is None: exclude_types = [] - return reversed([str(e) for e in self._events_list if e.time <= until if e.type not in exclude_types]) + return reversed( + [str(e) for e in self._events_list if e.time <= until if e.type not in exclude_types] + ) def update_settings(self, settings: Dict[str, Any]): """ diff --git a/utils/__init__.py b/utils/__init__.py index e0e63cb..febe995 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -1,2 +1,2 @@ from utils.list_utils import to_lookup, contains -from utils.misc import get_marks_dict \ No newline at end of file +from utils.misc import get_marks_dict