diff --git a/mixnet/v2/sim/bulk_run.py b/mixnet/v2/sim/bulk_run.py new file mode 100644 index 0000000..a50a5b4 --- /dev/null +++ b/mixnet/v2/sim/bulk_run.py @@ -0,0 +1,124 @@ +import argparse + +import pandas as pd +import seaborn +from matplotlib import pyplot as plt + +from config import P2PConfig, Config +from analysis import Analysis +from simulation import Simulation + +COL_P2P_TYPE = "P2P Type" +COL_NUM_NODES = "Num Nodes" +COL_TRAFFIC_TYPE = "Traffic Type" +COL_STAT = "Stat" +COL_BANDWIDTH = "Bandwidth" + +TRAFFIC_TYPE_INGRESS = "Ingress" +TRAFFIC_TYPE_EGRESS = "Egress" +STAT_MEAN = "mean" +STAT_MAX = "max" + + +def bulk_run(): + parser = argparse.ArgumentParser(description="Run simulation", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument("--config", type=str, required=True, help="Configuration file path") + args = parser.parse_args() + config = Config.load(args.config) + + data = { + COL_P2P_TYPE: [], + COL_NUM_NODES: [], + COL_TRAFFIC_TYPE: [], + COL_STAT: [], + COL_BANDWIDTH: [], + } + + message_size_df = None + + for p2p_type in [P2PConfig.TYPE_ONE_TO_ALL, P2PConfig.TYPE_GOSSIP]: + config.p2p.type = p2p_type + for num_nodes in [10, 100, 1000]: + config.mixnet.num_nodes = num_nodes + sim = Simulation(config) + sim.run() + if message_size_df is None: + message_size_df = Analysis(sim, config).message_size_distribution() + + nonzero_ingresses, nonzero_egresses = [], [] + for ingress_bandwidths, egress_bandwidths in zip(sim.p2p.measurement.ingress_bandwidth_per_time, + sim.p2p.measurement.egress_bandwidth_per_time): + for node in sim.p2p.nodes: + ingress = ingress_bandwidths[node] / 1024.0 + egress = egress_bandwidths[node] / 1024.0 + if ingress > 0: + nonzero_ingresses.append(ingress) + if egress > 0: + nonzero_egresses.append(egress) + + ingresses = pd.Series(nonzero_ingresses) + add_data(data, p2p_type, num_nodes, TRAFFIC_TYPE_INGRESS, STAT_MEAN, ingresses.mean()) + add_data(data, p2p_type, num_nodes, TRAFFIC_TYPE_INGRESS, STAT_MAX, ingresses.max()) + egresses = pd.Series(nonzero_egresses) + add_data(data, p2p_type, num_nodes, TRAFFIC_TYPE_EGRESS, STAT_MEAN, egresses.mean()) + add_data(data, p2p_type, num_nodes, TRAFFIC_TYPE_EGRESS, STAT_MAX, egresses.max()) + + df = pd.DataFrame(data) + draw_bandwidth_plot(df, TRAFFIC_TYPE_INGRESS, config, message_size_df) + draw_bandwidth_plot(df, TRAFFIC_TYPE_EGRESS, config, message_size_df) + + +def add_data(data: dict, p2p_type: str, num_nodes: int, bandwidth_type: str, stat: str, bandwidth: float): + data[COL_P2P_TYPE].append(p2p_type) + data[COL_NUM_NODES].append(num_nodes) + data[COL_TRAFFIC_TYPE].append(bandwidth_type) + data[COL_STAT].append(stat) + data[COL_BANDWIDTH].append(bandwidth) + + +def draw_bandwidth_plot(df: pd.DataFrame, traffic_type: str, config: Config, message_size_df: pd.DataFrame): + ingress_df = df[df[COL_TRAFFIC_TYPE] == traffic_type] + + plt.figure(figsize=(12, 6)) + + mean_df = ingress_df[ingress_df[COL_STAT] == STAT_MEAN] + seaborn.barplot(data=mean_df, x=COL_NUM_NODES, y=COL_BANDWIDTH, hue=COL_P2P_TYPE, ax=plt.gca(), capsize=0.1) + max_df = ingress_df[ingress_df[COL_STAT] == STAT_MAX] + barplot = seaborn.barplot(data=max_df, x=COL_NUM_NODES, y=COL_BANDWIDTH, hue=COL_P2P_TYPE, ax=plt.gca(), + capsize=0.1, alpha=0.5) + + # Adding labels to each bar + for p in barplot.patches: + height = p.get_height() + if height > 0: # Only label bars with positive height + barplot.annotate(format(height, ".2f"), + (p.get_x() + p.get_width() / 2., height), + ha="center", va="center", + xytext=(0, 9), + textcoords="offset points") + + plt.title(f"{traffic_type} Bandwidth") + plt.xlabel(COL_NUM_NODES) + plt.ylabel(f"{COL_BANDWIDTH} (KB/s)") + + # Custom legend to show Mean and Max + handles, labels = barplot.get_legend_handles_labels() + for i in range(len(labels) // 2): + labels[i] = labels[i] + f" ({STAT_MEAN})" + for i in range(len(labels) // 2, len(labels)): + labels[i] = labels[i] + f" ({STAT_MAX})" + plt.legend(handles=handles, labels=labels, loc="upper left") + + desc = ( + f"message: {message_size_df["message_size"].mean():.0f} bytes\n" + f"{config.description()}" + ) + plt.text(1.02, 0.5, desc, transform=plt.gca().transAxes, verticalalignment="center", fontsize=12) + plt.subplots_adjust(right=0.8) # Adjust layout to make room for the text + + plt.show() + + +if __name__ == "__main__": + bulk_run() diff --git a/mixnet/v2/sim/config.py b/mixnet/v2/sim/config.py index c78f739..b3ea439 100644 --- a/mixnet/v2/sim/config.py +++ b/mixnet/v2/sim/config.py @@ -96,18 +96,18 @@ class MixnetConfig: @dataclass class P2PConfig: - # Broadcasting type: naive | gossip + # Broadcasting type: 1-to-all | gossip type: str # A connection density, only if the type is gossip connection_density: int # A maximum network latency between nodes directly connected with each other max_network_latency: float - TYPE_NAIVE = "naive" + TYPE_ONE_TO_ALL = "1-to-all" TYPE_GOSSIP = "gossip" def validate(self): - assert self.type in [self.TYPE_NAIVE, self.TYPE_GOSSIP] + assert self.type in [self.TYPE_ONE_TO_ALL, self.TYPE_GOSSIP] if self.type == self.TYPE_GOSSIP: assert self.connection_density > 0 assert self.max_network_latency >= 0 diff --git a/mixnet/v2/sim/simulation.py b/mixnet/v2/sim/simulation.py index daad06a..38d6cce 100644 --- a/mixnet/v2/sim/simulation.py +++ b/mixnet/v2/sim/simulation.py @@ -22,7 +22,7 @@ class Simulation: @classmethod def init_p2p(cls, env: simpy.Environment, config: Config): match config.p2p.type: - case P2PConfig.TYPE_NAIVE: + case P2PConfig.TYPE_ONE_TO_ALL: return NaiveBroadcastP2P(env, config) case P2PConfig.TYPE_GOSSIP: return GossipP2P(env, config)