Source code for NetworkSim.simulation.tools.plot

__author__ = ["Hongyi Yang", "Cheuk Ming Chung"]
__all__ = [
    "init",
    "plot_latency_heatmap",
    "plot_latency_scatter",
    "plot_latency",
    "plot_latency_throughput",
    "plot_latency_3d",
    "plot_count",
    "plot_batch_throughput",
    "plot_analytical_simulation_latency",
    "save_plot",
    "save_all_plots",
    "plot_on_grid",
    "plot_queue_size"
]

import gc
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib
import matplotlib.pyplot as plt
from matplotlib import rc
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.axes_grid1 import make_axes_locatable
import warnings
import os

from NetworkSim.simulation.tools.performance_analysis import get_transfer_delay

latency_heatmap_size = [5, 5]
latency_scatter_size = [6, 5]
count_size = [5, 5]
latency_throughput_size = [6, 5]
ram_queue_size = [6, 5]


[docs]def init(): """ Plot environment initialisation. """ gc.collect() sns.set_theme(style="ticks") sns.set_context("paper") rc('font', **{'family': 'sans-serif', 'sans-serif': ['Helvetica']}) rc('text', usetex=True) matplotlib.rcParams['mathtext.fontset'] = 'stix' matplotlib.rcParams['font.family'] = 'STIXGeneral' SMALL_SIZE = 7 MEDIUM_SIZE = 9 LARGE_SIZE = 11 BIGGER_SIZE = 13 rc('font', size=SMALL_SIZE) # controls default text sizes rc('axes', titlesize=LARGE_SIZE) # fontsize of the axes title rc('axes', labelsize=MEDIUM_SIZE) # fontsize of the x and y labels rc('xtick', labelsize=SMALL_SIZE) # fontsize of the tick labels rc('ytick', labelsize=SMALL_SIZE) # fontsize of the tick labels rc('legend', fontsize=MEDIUM_SIZE) # legend fontsize rc('figure', titlesize=BIGGER_SIZE) # fontsize of the figure title sns.set_palette('RdBu_r') warnings.filterwarnings("ignore")
[docs]def plot_latency_heatmap(latency, type=None, fig=None, ax=None, title=None): """Plot average latency of all nodes in a simulator as a heatmap. Parameters ---------- latency : pandas DataFrame A DataFrame containing the latency information, generated from ``summary``. type : string, optional Type of latency to be plotted, by default ``None``. fig : figure, optional Figure to be plotted on, by default ``None``. ax : axis, optional Axis to be plotted on, by default ``None``. title : string, optional Plot title, by default ``None``. Returns ------- fig : figure Plot figure object. """ init() _queueing_delay_keywords = {'queueing delay', 'qd'} if type in _queueing_delay_keywords: label = 'Average Queueing Delay (ns)' else: label = 'Average Transfer Delay (ns)' start, end = min(latency.columns), max(latency.columns) width, height = latency_heatmap_size[0], latency_heatmap_size[1] if fig is None and ax is None: fig, ax1 = plt.subplots(1, 1, figsize=(width, height), dpi=300) else: fig = fig ax1 = ax divider = make_axes_locatable(ax1) cax1 = divider.append_axes('right', size='5%', pad=0.2) sns.heatmap(data=latency, cmap='RdBu_r', ax=ax1, lw=.25, xticklabels=10, yticklabels=10, square=True, cbar_ax=cax1, cbar_kws={'label': label, 'shrink': .6, 'pad': .1}, ) ax1.tick_params(width=0.5) ax1.set_xlim([start, end]) ax1.set_ylim([end, start]) ax1.xaxis.set_tick_params(width=0.5) ax1.yaxis.set_tick_params(width=0.5) ax1.set_xticklabels(ax1.get_xticklabels(), rotation=0) ax1.set_yticklabels(ax1.get_yticklabels(), rotation=0) ax1.set_xlabel('Destination Node') ax1.set_ylabel('Source Node') frame_linewidth = 1 ax1.axhline(y=start, color='k', linewidth=frame_linewidth) ax1.axhline(y=end, color='k', linewidth=frame_linewidth) ax1.axvline(x=start, color='k', linewidth=frame_linewidth) ax1.axvline(x=end, color='k', linewidth=frame_linewidth) if title is not None: ax1.set_title(title) fig.tight_layout() return fig
[docs]def plot_latency_scatter(latency, node_id): """ Scatter plot of latency information of one node, \ both as a source node and as a destination node. Parameters ---------- latency : list The list of latency from ``BaseSimulator``. node_id : int The ID of the node of interest. """ init() latency_as_source = [] latency_as_destination = [] source_ids = [] destination_ids = [] for latency in latency: if latency['Source ID'] == node_id: destination_ids.append(latency['Destination ID']) latency_as_source.append(latency['Transfer Delay']) if latency['Destination ID'] == node_id: source_ids.append(latency['Source ID']) latency_as_destination.append(latency['Transfer Delay']) x = [destination_ids, source_ids] y = [latency_as_source, latency_as_destination] titles = ['Latency as Source Node', 'Latency as Destination Node'] xlabels = ['Destination Node', 'Source Node'] ylabel = 'Transfer Latency (ns)' width, height = latency_scatter_size[0], latency_scatter_size[1] fig, axes = plt.subplots(2, 1, figsize=(width, height), dpi=300) for i in range(2): sns.scatterplot(x=x[i], y=y[i], ax=axes[i], s=25, alpha=.5, lw=1, edgecolors='white', color=sns.color_palette('RdBu_r')[0]) axes[i].set_ylabel(ylabel) axes[i].set_xlabel(xlabels[i]) axes[i].yaxis.grid(True) axes[i].set_title(titles[i], fontsize=13) fig.tight_layout() return fig
[docs]def plot_count(packet_count, fig=None, ax=None, title=None): """ Plot transmission packet count as a heatmap. Parameters ---------- packet_count : pandas DataFrame A DataFrame containing the packet count summary. fig : figure, optional Figure to be plotted on, by default ``None``. ax : axis, optional Axis to be plotted on, by default ``None``. title : string, optional Plot title, by default ``None``. """ init() width, height = count_size[0], count_size[1] if fig is None and ax is None: fig, ax1 = plt.subplots(1, 1, figsize=(width, height), dpi=300) else: fig = fig ax1 = ax start, end = min(packet_count.columns), max(packet_count.columns) divider = make_axes_locatable(ax1) cax1 = divider.append_axes('right', size='5%', pad=0.2) sns.heatmap(data=packet_count, cmap='RdBu_r', ax=ax1, lw=.25, cbar_ax=cax1, cbar_kws={'label': 'Number of Packets Transmitted', 'shrink': .6, 'pad': 0.1}, xticklabels=10, yticklabels=10, square=True) ax1.tick_params(width=0.5) ax1.set_xlim([start, end]) ax1.set_ylim([end, start]) ax1.xaxis.set_tick_params(width=0.5) ax1.yaxis.set_tick_params(width=0.5) ax1.set_xticklabels(ax1.get_xticklabels(), rotation=0) ax1.set_yticklabels(ax1.get_yticklabels(), rotation=0) ax1.set_xlabel('Destination Node') ax1.set_ylabel('Source Node') frame_linewidth = 1 ax1.axhline(y=start, color='k', linewidth=frame_linewidth) ax1.axhline(y=end, color='k', linewidth=frame_linewidth) ax1.axvline(x=start, color='k', linewidth=frame_linewidth) ax1.axvline(x=end, color='k', linewidth=frame_linewidth) if title is not None: ax1.set_title(title) fig.tight_layout() return fig
[docs]def plot_latency_throughput(latency): """Plot latency and throughput of a simulation across time. Parameters ---------- latency : list The list of `latency` attribute information from ``BaseSimulator``. """ init() time = list(item['Latency Timestamp'] for item in latency) delay = list(item['Transfer Delay'] for item in latency) rate = list(item['Data Rate'] for item in latency) y = [delay, rate] xlabel = 'Time (ns)' ylabels = ['Transfer Delay (ns)', 'Throughput (Gbit/s)'] titles = ['Overall Transfer Delay', 'Overall Throughput'] width, height = latency_throughput_size[0], latency_throughput_size[1] fig, axes = plt.subplots(2, 1, figsize=(width, height), dpi=300) for i in range(2): sns.lineplot(x=time, y=y[i], markers=True, dashes=False, ax=axes[i], lw=1, color=sns.color_palette('RdBu_r')[0]) axes[i].set_ylabel(ylabels[i]) axes[i].set_xlabel(xlabel) axes[i].xaxis.grid(True) axes[i].yaxis.grid(True) axes[i].set_title(titles[i], fontsize=13) fig.tight_layout() return fig
[docs]def plot_latency_3d(latency, type): """ 3d bar plot of parameters as source/destination pair. Parameters ---------- latency : pandas DataFrame A DataFrame containing the latency information, generated from ``summary``. type : string Type of latency to be plotted. """ init() _n = len(latency.columns) x = y = [i for i in range(_n)] T, A = np.meshgrid(x, y) _queueing_delay_keywords = {'queueing delay', 'qd'} if type in _queueing_delay_keywords: zlabel = 'Average Queueing Delay (ns)' else: zlabel = 'Average Transfer Delay (ns)' xlabel = 'Source Node' ylabel = 'Destination Node' std_plot = False if isinstance(latency, tuple): mean_df = latency[0] std_df = latency[1] std_plot = True elif isinstance(latency, pd.DataFrame): mean_df = latency mean = mean_df.values if std_plot is True: std = std_df.values zlabel = ('STD of ' + zlabel) std_transpose = std.transpose() dz = std_transpose.flatten() else: mean_transpose = mean.transpose() dz = mean_transpose.flatten() # Plotting fig = plt.figure(figsize=(15, 15), dpi=300) ax = fig.gca(projection='3d') Xi = T.flatten() Yi = A.flatten() Zi = np.zeros(mean.size) dx = dy = 0.5 ax.bar3d(Xi, Yi, Zi, dx, dy, dz, color=sns.color_palette('RdBu_r')[0], shade=False) ax.view_init(20, 37) ax.set_xlabel(xlabel, fontsize=15) ax.set_ylabel(ylabel, fontsize=15) ax.set_zlabel(zlabel, fontsize=15) return fig
[docs]def plot_batch_throughput(simulator, show_ci=True): """Plot batch throughput of simulations. Confidence intervals demarcated by the SEM values \ are shown by default. Where the maximum SEM value plotted is 1. Parameters ---------- simulator : BaseSimulator The simulator used. show_ci : bool, optional If confidence interval is displayed, by default ``True``. """ # Check for convergence mode if not simulator.convergence: raise NotImplementedError("This plot function is implemented only " "for simulators with convergence mode enabled.") init() stats = simulator.batch_stats time = list(item['timestamp'] for item in stats) mean = list(item['mean'] for item in stats) sem = list(item['sem'] for item in stats) upper_ci = [] lower_ci = [] xlabel = 'Time (ns)' ylabel = 'Throughput (Gbit/s)' title = 'Batch Mean Throughput' if show_ci: for i in range(len(time)): if sem[i] > 1: sem[i] = 1 upper_ci.append(mean[i] * (1 + sem[i])) lower_ci.append(mean[i] * (1 - sem[i])) fig, ax = plt.subplots(1, 1, figsize=(6, 3), dpi=300) sns.lineplot(x=time, y=mean, ax=ax, lw=1, color=sns.color_palette('RdBu_r')[0]) if show_ci: ax.fill_between(time, lower_ci, upper_ci, fc=sns.color_palette('RdBu_r')[2], ec=None, alpha=.75) ax.set_ylabel(ylabel) ax.set_xlabel(xlabel) ax.xaxis.grid(True) ax.yaxis.grid(True) ax.set_title(title, fontsize=13) fig.tight_layout() return fig
[docs]def plot_latency(simulator, latency, node_id=None, latency_type=None, bar3d=False, fig=None, ax=None, title=None): """ Function to plot latency information. Parameters ---------- simulator : BaseSimulator The simulator of choice. latency : pandas DataFrame The latency summary generated. node_id : int, optional The node of interest, by default ``None``. latnecy_type : string, optional The latency type of interset, by default is ``None``, which is transfer delay. bar3d : bool, optional Enable 3d bar plot for queueing delay, default is ``False``. fig : figure, optional Figure to be plotted on, by default ``None``. ax : axis, optional Axis to be plotted on, by default ``None``. title : string, optional Plot title, by default ``None``. """ if node_id is None: if bar3d is False: return plot_latency_heatmap(latency, type=latency_type, fig=fig, ax=ax, title=title) else: return plot_latency_3d(latency, type=latency_type) else: return plot_latency_scatter(simulator.latency, node_id)
def check_grid_plot_input(simulator, grid): """Function to check the inputs of a grid plot. Parameters ---------- simulator : ParallelSimulator The parallel simulator used. grid : list The grid to be plotted on. Returns ------- row, col : int The number of rows and columns in the grid. """ # Check simulator from NetworkSim.simulation.simulator.parallel import ParallelSimulator # Soft dependency if not isinstance(simulator, ParallelSimulator): raise ValueError("This plot function is only implemented for ParallelSimulator.") # Check grid if not isinstance(grid, list) or len(grid) != 2: raise ValueError("Grid must be a list with length 2 indicating number of rows and columns.") # Check row and column dimensions if grid[0] < 2 or grid[1] < 2: raise ValueError("Both number of rows and number of columns must be larger than 1.") return grid[0], grid[1] def plot_on_grid(simulator, grid, titles, summary_type, latency_type=None): """Function to generates plots on a grid. Parameters ---------- simulator : ParallelSimulator The parallel simulator used. grid : list The grid used for plotting. titles : list The list of titles for the subplots. summary_type : string The summary type corresponding to the plots to be generated, same as that used in `BaseSimulator.summary`. latency_type : string, optional The type of latency to be plotted, by default ``None``. Returns ------- fig : figure The figure object produced. """ _row, _col = check_grid_plot_input(simulator=simulator, grid=grid) _latency_keywords = {'latency', 'l'} _count_keywords = {'count', 'c'} # Plot init() _plot_count = 0 if summary_type in _latency_keywords: _width, _height = latency_heatmap_size[0], latency_heatmap_size[1] elif summary_type in _count_keywords: _width, _height = count_size[0], count_size[1] fig, axes = plt.subplots(_row, _col, figsize=(_width * _row, _height * _col), dpi=300) for i in range(_row): for j in range(_col): if _plot_count > len(simulator.simulator): continue _simulator = simulator.simulator[_plot_count] _summary = _simulator.summary(summary_type=summary_type, latency_type=latency_type) if summary_type in _latency_keywords: plot_latency(simulator=_simulator, latency=_summary, latency_type=latency_type, fig=fig, ax=axes[i][j], title=titles[_plot_count]) elif summary_type in _count_keywords: plot_count(packet_count=_summary, fig=fig, ax=axes[i][j], title=titles[_plot_count]) _plot_count += 1 for i in range(max(_row, _col)): fig.tight_layout() return fig
[docs]def plot_analytical_simulation_latency(simulator, data='auto', show_analytical=True): """Plot analytical and simulation transfer delay. Parameters ---------- simulator : ParallelSimulator The simulator used for the plot. data : str, optional Range of data used for the plot, chosen from the list: - `batch` : Use the data from the last batch when convergence mode is enabled. - `extended` : Use the data from the extended run when convergence mode is enabled. - `all` : Use all simulation data. - `auto` : Automatically determine the range of the data used. `batch` is selected when \ convergence mode is enabled and `all` is selected when convergence mode is disabled. Default is ``auto``. show_analytical : bool, optional If analytical results are shown on the plot, by default ``True``. """ # Check for data range if data not in ['auto', 'batch', 'all']: raise ValueError("Plot data range is not recognised.") # Compute plot data load = [] analytical_delay = [] simulation_delay = [] xlabel = 'Network Source Traffic Load (Gbit/s)' ylabel = 'Mean Transfer Delay (ns)' for simulator in simulator.simulator: load.append(simulator.model.network.num_nodes * simulator.model.constants['average_bit_rate']) analytical_delay.append(get_transfer_delay(simulator)) if data == 'auto': if simulator.convergence: data_range = 'all' else: data_range = 'extended' else: data_range = data # TODO: change direct delay calculations to get_delay functions in performance_analysis if data_range == 'all': latency = simulator.latency elif data_range == 'extended': start = simulator.batch_stats[-1]['end_index'] latency = simulator.latency[start:] elif data_range == 'batch': start = simulator.batch_stats[-1]['start_index'] latency = simulator.latency[start:] delay = np.mean(list(item['Transfer Delay'] for item in latency)) simulation_delay.append(delay) # Plot analytical and simulation result if np.isinf(max(analytical_delay)): ylim = 1.2 * max(simulation_delay) else: ylim = 1.2 * max(analytical_delay + simulation_delay) init() fig, ax = plt.subplots(1, 1, figsize=(5, 3), dpi=300) if show_analytical: sns.lineplot(x=load, y=analytical_delay, lw=0.75, ls='--', marker='x', markersize=4, markerfacecolor='None', markeredgecolor='k', ax=ax, label='Analytical', color=sns.color_palette('RdBu_r')[-1]) sns.lineplot(x=load, y=simulation_delay, lw=0.75, ls='-', marker='s', markersize=4, markerfacecolor='None', markeredgecolor='k', ax=ax, label='Simulation', color=sns.color_palette('RdBu_r')[0], ci=95) ax.legend(loc='upper left') ax.set_ylabel(ylabel) ax.set_xlabel(xlabel) ax.xaxis.grid(True) ax.yaxis.grid(True) ax.set_ylim([0, ylim]) fig.tight_layout() return fig
def plot_queue_size(simulator): """Plot queue size information for ParallelSimulator. Parameters ---------- simulator : ParallelSimulator The simulator used for the plot. """ # Check simulator type from NetworkSim.simulation.simulator.parallel import ParallelSimulator if not isinstance(simulator, ParallelSimulator): raise NotImplementedError("This plot is only implemented for ParallelSimulator.") # Get queue size information mean_size = [] max_size = [] load = [] for individual_simulator in simulator.simulator: mean_queue_size, max_queue_size, _ = individual_simulator.summary(summary_type='q') mean_size.append(mean_queue_size) max_size.append(max_queue_size) load.append(individual_simulator.model.constants['average_bit_rate'] * individual_simulator.model.network.num_nodes) init() y = [mean_size, max_size] xlabel = 'Overall Network Load (Gbit/s)' ylabel = 'Queue Size' titles = ['Mean Queue Size in RAM', 'Maximum Queue Size in RAM'] width, height = ram_queue_size[0], ram_queue_size[1] fig, axes = plt.subplots(2, 1, figsize=(width, height), dpi=300) for i in range(2): sns.lineplot(x=load, y=y[i], markers=True, ci=95, dashes=False, ax=axes[i], lw=1, color=sns.color_palette('RdBu_r')[0]) axes[i].set_ylabel(ylabel) axes[i].set_xlabel(xlabel) axes[i].xaxis.grid(True) axes[i].yaxis.grid(True) axes[i].set_title(titles[i], fontsize=13) fig.tight_layout() return fig def save_plot(fig, fname, dir_name=None): """Function to save a figure generated in png format. Parameters ---------- fig : figure The matplotlib figure object to be saved. fname : string The output file name of the figure. """ dir_path = os.path.dirname(os.path.realpath(__file__)) if dir_name is None: dir_name = 'plots' else: dir_name = os.path.join('plots', dir_name) fpath = os.path.join(dir_path, '../../../', dir_name) if not os.path.exists(fpath): os.mkdir(fpath) fig_name = fpath + "/" + fname fig.savefig(fname=fig_name) def _save_all_basesimulator_plots(simulator, dir_name): """Function to save all BaseSimulator plots. Parameters ---------- simulator : BaseSimulator The simulator whose plots are generated. dir_name : string Folder name for the plot output files. """ plt.ioff() # Overall latency and throughput _fig = simulator.summary(format='plot') plt.close(_fig) save_plot(fig=_fig, fname='latency_throughput', dir_name=dir_name) # Transfer and queueing delay _fig = simulator.summary(format='plot', summary_type='l') plt.close(_fig) save_plot(fig=_fig, fname='transfer_delay', dir_name=dir_name) _fig = simulator.summary(format='plot', summary_type='l', latency_type='qd') plt.close(_fig) save_plot(fig=_fig, fname='queueing_delay', dir_name=dir_name) # Packet count _fig = simulator.summary(format='plot', summary_type='c') plt.close(_fig) save_plot(fig=_fig, fname='packet_count', dir_name=dir_name) # Batch throughput _fig = plot_batch_throughput(simulator=simulator) plt.close(_fig) save_plot(fig=_fig, fname='batch_throughput', dir_name=dir_name) del _fig def _save_all_parallelsimulator_plots(simulator, dir_name, grid, titles): plt.ioff() # Transfer delay _fig = plot_on_grid(simulator=simulator, grid=grid, titles=titles, summary_type='l') plt.close(_fig) save_plot(fig=_fig, fname='transfer_delay', dir_name=dir_name) # Queueing delay _fig = plot_on_grid(simulator=simulator, grid=grid, titles=titles, summary_type='l', latency_type='qd') plt.close(_fig) save_plot(fig=_fig, fname='queueing_delay', dir_name=dir_name) # Packet count _fig = plot_on_grid(simulator=simulator, grid=grid, titles=titles, summary_type='c') plt.close(_fig) save_plot(fig=_fig, fname='packet_count', dir_name=dir_name) # Analytical and simulation latency _fig = plot_analytical_simulation_latency(simulator=simulator) plt.close(_fig) save_plot(fig=_fig, fname='analytical_simulation_latency', dir_name=dir_name) del _fig def save_all_plots(simulator, dir_name, grid=None, titles=None): """Function to automatically generate and save plots of a simulator. Parameters ---------- simulator : BaseSimulator or ParallelSimulator The simulator whose plots are generated and saved. dir_name: string The folder name under the `plot` directory to store the simulator's plot images. grid : list, optional The grid used for plotting, by default ``None``. titles : list The list of titles for the subplots, by default ``None``. """ # Soft dependencies from NetworkSim.simulation.simulator.base import BaseSimulator from NetworkSim.simulation.simulator.parallel import ParallelSimulator if isinstance(simulator, BaseSimulator): return _save_all_basesimulator_plots(simulator=simulator, dir_name=dir_name) elif isinstance(simulator, ParallelSimulator): return _save_all_parallelsimulator_plots(simulator=simulator, dir_name=dir_name, grid=grid, titles=titles) else: raise ValueError("A BaseSimulator or ParallelSimulator object is expected.")