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/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 4a779d4..0112bff 100644
--- a/pons/simulation.py
+++ b/pons/simulation.py
@@ -1,5 +1,8 @@
-import simpy
import time
+from typing import List, Dict
+
+import simpy
+
import pons
@@ -21,6 +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.event_manager: pons.EventManager = pons.EventManager(self.env, nodes)
self.mover = pons.OneMovementManager(
self.env, self.nodes, self.movements)
@@ -107,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/ui/__init__.py b/ui/__init__.py
new file mode 100644
index 0000000..158591e
--- /dev/null
+++ b/ui/__init__.py
@@ -0,0 +1,4 @@
+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
new file mode 100644
index 0000000..1906590
--- /dev/null
+++ b/ui/app.py
@@ -0,0 +1,414 @@
+from typing import Tuple
+
+import dash_bootstrap_components as dbc
+from dash import Dash, html, no_update, Input, Output, ctx, State
+
+from ui import Visualization, DataManager, layout_component, config, 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.,
+ "FRAME_STEP": 1,
+ "ROUTER": ROUTERS["EpidemicRouter"],
+ "ROUTER_NAME": "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])
+
+app.layout = layout_component(fig, settings)
+
+# region Callbacks
+
+# js code getting width and height of plot
+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("checkbox_collapse", "is_open"),
+ [Input("checkbox_collapse_button", "n_clicks")],
+ [State("checkbox_collapse", "is_open")],
+)
+def toggle_collapse(n_clicks, is_open):
+ """
+ toggling the checkbox collapse
+ @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_clicks:
+ 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):
+ """
+ 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()
+
+ return no_update
+
+
+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
+ 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):
+ """
+ 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"
+ 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):
+ """
+ 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", 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):
+ """
+ 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 _get_animation_outputs()
+
+
+@app.callback(
+ Output("num_nodes_display", "children"),
+ Input("num_nodes", "value"),
+ 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
+
+
+@app.callback(
+ Output("net_range_display", "children"),
+ Input("net_range", "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
+
+
+@app.callback(
+ [Output("min_speed", "max"), Output("max_speed_display", "children")],
+ Input("max_speed", "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
+
+
+@app.callback(
+ [Output("max_speed", "min"), Output("min_speed_display", "children")],
+ Input("min_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
+
+
+@app.callback(
+ Output("sim_time_display", "children"),
+ Input("sim_time", "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
+
+
+@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):
+ """
+ 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"] = 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: f"{i}" for i in range(0, sim_time, int(sim_time / 20))},
+ # save button content
+ "Save",
+ # animation interval
+ 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),
+ 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):
+ """
+ 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(
+ Output("plot", "figure", allow_duplicate=True),
+ Input("checkbox_input", "value"),
+ 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/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/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..b355f81
--- /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: f'{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..ff72614
--- /dev/null
+++ b/ui/components/animation_speed.py
@@ -0,0 +1,25 @@
+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.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..3e69b12
--- /dev/null
+++ b/ui/config.py
@@ -0,0 +1,59 @@
+import inspect
+from dataclasses import dataclass
+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
+ 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:
+ """config class"""
+ 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())
+
+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
new file mode 100644
index 0000000..b05099c
--- /dev/null
+++ b/ui/data.py
@@ -0,0 +1,281 @@
+import random
+from typing import Dict, Any, Tuple, List
+
+import pandas as pd
+
+import pons
+
+RANDOM_SEED = 42
+
+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]] = []
+ 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):
+ """simulations as shown in sim.py"""
+ 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):
+ """generates movement data"""
+ 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):
+ """adds event to event data"""
+ 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):
+ """generates event data"""
+ 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):
+ """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({
+ "x": [from_x, to_x],
+ "y": [from_y, to_y]
+ }))
+ self._connections = data
+
+ def _generate_stores(self):
+ """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()
+ self._generate_event_data()
+ self._generate_connections()
+
+ @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])
+ 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) -> 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] = []
+ 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: 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 update_settings(self, settings: Dict[str, Any]):
+ """
+ updates settings
+ @param settings: the settings dict from app.py
+ """
+ self._settings = settings
+ self._generate()
diff --git a/ui/requirements.txt b/ui/requirements.txt
new file mode 100644
index 0000000..bc250e8
--- /dev/null
+++ b/ui/requirements.txt
@@ -0,0 +1,4 @@
+dash-bootstrap-components
+dash
+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..616d8db
--- /dev/null
+++ b/ui/visualization.py
@@ -0,0 +1,151 @@
+from enum import Enum
+from typing import Dict, Any
+
+import plotly.graph_objects as go
+
+import pons
+import ui
+
+
+class VisualizationStatus(Enum):
+ """visualization status 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()
+
+ # 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 of nodes
+ 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 of ndoes
+ 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..febe995
--- /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
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