__all__ = ["BaseSimulator"]
__author__ = ["Hongyi Yang", "Cheuk Ming Chung"]
from timeit import default_timer as timer
import simpy
import pandas as pd
from tqdm.auto import trange
from tqdm.auto import tqdm
import numpy as np
from scipy.stats import sem as SEM
from NetworkSim.architecture.setup.model import Model
from NetworkSim.simulation.process.ram import RAM
from NetworkSim.simulation.process.receiver.fixed import FR
from NetworkSim.simulation.process.receiver.fixed_upstream import FR_U
from NetworkSim.simulation.process.receiver.fixed_downstream import FR_D
from NetworkSim.simulation.process.receiver.tunable import TR
from NetworkSim.simulation.process.transmitter.fixed import FT
from NetworkSim.simulation.process.transmitter.tunable import TT
from NetworkSim.simulation.process.transmitter.tunable_upstream import TT_U
from NetworkSim.simulation.process.transmitter.tunable_downstream import TT_D
from NetworkSim.simulation.tools.info import Info
from NetworkSim.simulation.tools.summary import Summary
from NetworkSim.simulation.tools.plot import plot_latency, plot_latency_throughput, plot_count
[docs]class BaseSimulator:
"""
Simulation wrapper to create a discrete event simulation of the ring network.
Parameters
----------
until : float
The end time of the simulation. Default is ``None``.
convergence : bool
Automatic convergence mode of simulation. Default is ``True``.
sem_tr : float
The threshold standard error of mean (SEM) for convergence. Default is ``0.05``.
bs : int
The batch size for SEM calculation during convergence. Default is ``10000``.
env: simpy Environment, optional
The environment in which the simulation is carried out. Default is ``simpy.Environment()``.
model : Model, optional
The network model used for the simulation. Default is ``Model()``.
transmitter_type: str
The type of transmitter used for the simulation, chosen from the list:
- `fixed`, `f` or `F`
Fixed transmitter.
- `tunable`, `t` or `T`
Tunable transmitter.
Default is ``tunable``.
receiver_type: str
The type of receiver used for the simulation, chosen from the list:
- `fixed`, `f` or `F`
Fixed receiver.
- `tunable`, `t`, or `T`
Tunable receiver.
Default is ``fixed``.
traffic_generation_method : str
The method used for generate source traffic, chosen from the list:
- `poisson`
Poisson distribution.
- `pareto`
Pareto distribution.
Default is ``poisson``.
bidirectional : bool, optional
The type of network architecture, either bidirectional or unidirectional. \
Default is ``False``, which is unidirectional.
seed : int, optional
The seed used for source traffic generation. Default is ``1``.
----------
latency : list
A list of packet transmission latency recorded during the simulation, containing the keys:
- `Latency Timestamp` : float
The timestamp when the latency is recorded.
- `Source ID` : int
The ID of the source node.
- `Destination ID` : int
The ID of the destination node.
- `Queueing Delay` : float
The recorded queueing delay latency.
- `Transfer Delay` : float
The recorded transfer delay latency.
- `Data Rate` : float
The data rate recorded as the operation is completed.
error : list
A list of transmission errors occurred during the simulation, containing the keys:
- `Error Timestamp` : float
The timestamp when the error occurred.
- `Source ID` : int
The ID of the source node.
- `Destination ID` : int
The ID of the destination node.
- `Error Type` : str
The type of error recorded.
"""
def __init__(
self,
until=None,
convergence=True,
sem_tr=None,
bs=None,
extended=True,
env=None,
model=None,
transmitter_type=None,
receiver_type=None,
traffic_generation_method=None,
bidirectional=False,
id=0,
seed=None,
):
# Check for simulator operation mode
if until is None:
if not convergence:
raise ValueError("Either fixed-time or convergence simulation must be chosen.")
else:
convergence = False
self.until = until
self.convergence = convergence
# Check for standard error of mean range
if convergence:
if sem_tr is None:
sem_tr = 0.05
if sem_tr <= 0 or sem_tr >= 1:
raise ValueError("Standard error of mean (sem) for convergence must lie between 0 and 1.")
self.sem_tr = sem_tr
if bs is None:
bs = 10000
self.bs = bs
self.extended = extended
if env is None:
env = simpy.Environment()
self.env = env
if model is None:
model = Model(bidirectional=bidirectional)
if transmitter_type is None:
transmitter_type = 'T'
if receiver_type is None:
receiver_type = 'F'
if traffic_generation_method is None:
traffic_generation_method = 'poisson'
self.transmitter_type = transmitter_type
self.receiver_type = receiver_type
self.traffic_generation_method = traffic_generation_method
self.bidirectional = bidirectional
# Check for bidirectional ring consistency
if model.bidirectional != self.bidirectional:
raise ValueError("Please ensure Model bidirectional value is consistent with Simulator.")
self.id = id
self.model = model
if seed is None:
seed = 1
self.seed = seed
self.RAM = [None] * self.model.network.num_nodes
if self.bidirectional:
self.upstream_transmitter = [None] * self.model.network.num_nodes
self.downstream_transmitter = [None] * self.model.network.num_nodes
self.upstream_receiver = [None] * self.model.network.num_nodes
self.downstream_receiver = [None] * self.model.network.num_nodes
else:
self.transmitter = [None] * self.model.network.num_nodes
self.receiver = [None] * self.model.network.num_nodes
self._fixed_keywords = {'fixed', 'f', 'F'}
self._tunable_keywords = {'tunable', 't', 'T'}
self.runtime = None
self.latency = []
self.error = []
self.TT_FR_tuning_delay = None
self.ram_queue_delay = []
self.batch_stats = []
def _initialise_ram(self, node_id):
"""
RAM initialisation.
Parameters
----------
node_id : int
Id of the node (RAM).
"""
# Create RAM process
self.RAM[node_id] = RAM(
env=self.env,
until=self.until,
ram_id=node_id,
model=self.model,
distribution=self.traffic_generation_method,
bidirectional=self.bidirectional,
seed=self.seed
)
# Initialise RAM process
self.RAM[node_id].initialise()
def _initialise_transmitter(self, node_id):
"""
Transmitter initialisation.
Parameters
----------
node_id : int
ID of the node (transmitter).
"""
if self.bidirectional:
if self.transmitter_type in self._tunable_keywords:
# Transmitter in upstream direction
self.upstream_transmitter[node_id] = TT_U(
env=self.env,
until=self.until,
ram=self.RAM[node_id],
transmitter_id=node_id,
model=self.model,
simulator=self
)
# Transmitter in downstream direction
self.downstream_transmitter[node_id] = TT_D(
env=self.env,
until=self.until,
ram=self.RAM[node_id],
transmitter_id=node_id,
model=self.model,
simulator=self
)
else:
raise NotImplementedError("Only tunable transmitter is implemented for bi-directional systems.")
self.upstream_transmitter[node_id].initialise()
self.downstream_transmitter[node_id].initialise()
else:
# Create and initialise transmitter process
if self.transmitter_type in self._fixed_keywords:
self.transmitter[node_id] = FT(
env=self.env,
until=self.until,
ram=self.RAM[node_id],
transmitter_id=node_id,
model=self.model,
simulator=self
)
elif self.transmitter_type in self._tunable_keywords:
self.transmitter[node_id] = TT(
env=self.env,
until=self.until,
ram=self.RAM[node_id],
transmitter_id=node_id,
model=self.model,
simulator=self
)
else:
raise NotImplementedError("Transmitter type not implemented.")
self.transmitter[node_id].initialise()
def _initialise_receiver(self, node_id):
"""
Receiver initialisation.
Parameters
----------
node_id : int
ID of the node (receiver).
"""
if self.bidirectional:
if self.receiver_type in self._fixed_keywords:
self.upstream_receiver[node_id] = FR_U(
env=self.env,
until=self.until,
receiver_id=node_id,
model=self.model,
simulator=self
)
self.downstream_receiver[node_id] = FR_D(
env=self.env,
until=self.until,
receiver_id=node_id,
model=self.model,
simulator=self
)
else:
raise NotImplementedError("Only fixed receiver is implemented for bi-directional systems.")
self.upstream_receiver[node_id].initialise()
self.downstream_receiver[node_id].initialise()
else:
# Create and initialise receiver process
if self.receiver_type in self._fixed_keywords:
self.receiver[node_id] = FR(
env=self.env,
until=self.until,
receiver_id=node_id,
model=self.model,
simulator=self
)
elif self.receiver_type in self._tunable_keywords:
self.receiver[node_id] = TR(
env=self.env,
until=self.until,
receiver_id=node_id,
model=self.model,
simulator=self
)
else:
raise NotImplementedError("Receiver type not implemented.")
self.receiver[node_id].initialise()
[docs] def initialise(self):
"""
Initialisation of the simulation, where RAM, transmitter, and receiver processes are added to the environment.
"""
# Check if the combination is implemented
if self.transmitter_type in self._fixed_keywords and self.receiver_type in self._fixed_keywords:
raise NotImplementedError("The FT-FR model is not implemented.")
if self.transmitter_type in self._tunable_keywords and self.receiver_type in self._tunable_keywords:
raise NotImplementedError("The TT-TR model is not implemented.")
# Initialise all three subsystems
for node_id in range(self.model.network.num_nodes):
self._initialise_ram(node_id=node_id)
self._initialise_transmitter(node_id=node_id)
self._initialise_receiver(node_id=node_id)
[docs] def run(self):
"""
Run simulation.
"""
_start_time = timer()
# Fixed-time mode
if not self.convergence:
desc = 'Simulator ' + str(self.id) + ' (convergence disabled)'
for i in trange(1, self.until, desc=desc):
self.env.run(until=i)
# Automatic convergence mode
else:
# initialise parameters
sem = np.inf
mean_data_rate = np.inf
until = 1
batch_number = 1
start = 0
def compute_batch_sem(last_sem, last_mean, start):
"""Function to compute batch SEM of data rate.
Parameters
----------
last_sem : float
Previous SEM value.
last_mean : float
Previous batch mean.
start : int
Index of the starting pointer of the `latency` list.
Returns
-------
sem, end, mean_data_rate : float, int, float
Calculated batch data rate SEM value, the last index of the `latency` list \
and the mean data rate of the batch.
"""
# Check if latency is updated
end = len(self.latency)
if start == end:
return last_sem, start, last_mean
# Calculate batch SEM
batch = self.latency[start:]
batch_data_rate = list([record['Data Rate'] for record in batch])
sem = SEM(batch_data_rate) * 2
mean_data_rate = np.mean(batch_data_rate)
return sem, end, mean_data_rate
def update_until(until):
"""Function to update the `until` values in all processes.
Parameters
----------
until : int
New until value.
"""
for ram in self.RAM:
ram.until = until
if self.bidirectional:
for upstream_transmitter in self.upstream_transmitter:
upstream_transmitter.until = until
for downstream_transmitter in self.downstream_transmitter:
downstream_transmitter.until = until
for upstream_receiver in self.upstream_receiver:
upstream_receiver.until = until
for downstream_receiver in self.downstream_receiver:
downstream_receiver.until = until
else:
for transmitter in self.transmitter:
transmitter.until = until
for receiver in self.receiver:
receiver.until = until
# Run batches until convergence
pbar_while = tqdm(total=batch_number + 1, desc='Simulator ' + str(self.id) + ' (convergence enabled)')
while sem >= self.sem_tr:
# Update until values and run batch
update_until(until=until + self.bs)
desc_for = 'Simulator ' + str(self.id) + ' Batch ' + str(batch_number)
pbar_for = trange(until, until + self.bs, desc=desc_for, leave=False)
for i in pbar_for:
self.env.run(until=i)
# Compute and record batch statistics
stats = {}
stats['timestamp'] = until + self.bs - 1
stats['start_index'] = start
sem, start, mean_data_rate = compute_batch_sem(last_sem=sem, start=start, last_mean=mean_data_rate)
stats['end_index'] = start
stats['sem'] = sem
stats['mean'] = mean_data_rate
self.batch_stats.append(stats)
until += self.bs
batch_number += 1
desc_while = 'Simulator %d (convergence enabled, batch SEM %.5f)' % (self.id, sem)
pbar_while.set_description(desc=desc_while)
pbar_while.update(1)
# Run extended simulations after convergence
if self.extended:
# Run for another 50% of time
extended_begin = int(self.env.now) + 1
extended_until = int(self.env.now * 1.5) + 1
update_until(until=extended_until)
desc = 'Simulator ' + str(self.id) + ' Extended Run'
for i in trange(extended_begin, extended_until, desc=desc):
self.env.run(until=i)
_end_time = timer()
self.runtime = _end_time - _start_time
[docs] def info(
self,
info_type=None,
component_type=None,
component_id=None):
"""
Obtain information of simulation components. Check `Info` class fore more details.
Parameters
----------
info_type : str
The type of information requested, chosen from the following:
- `control` or `c`
Information on control ring. When `device_type == None`, this returns all packet transmission \
information on the control ring, otherwise it refers to control packets transmitted by a transmitter \
or control packets received by a receiver. `component_id` is not required in this case.
- `data` or `d`
Information on data ring. When `device_type == None`, this returns all packet transmission \
information on the data ring, otherwise it refers to data packets transmitted by a transmitter \
or data packets received by a receiver. An `component_id` must be specified in this case.
component_type : str
The type of component in the simulation, chosen from the following:
- `ram` or `RAM`
Transmitter RAM information, where data packets are generated. An `component_id` must be specified \
in this case, but `info_type` is not required.
- `transmitter` or `t`
Transmitter packet information. Both `component_id` and `info_type` must be specified.
- `receiver` or `r`
Receiver packet information. Both `component_id` and `info_type` must be specified.
component_id : int
The ID of the component of choice.
Returns
-------
info : pandas DataFrame
A DataFrame containing the information requested.
"""
_ram_keywords = {'ram', 'RAM'}
_transmitter_keywords = {'transmitter', 't'}
_receiver_keywords = {'receiver', 'r'}
_info = Info(simulator=self)
if component_type is None:
return _info.ring_info(ring_id=component_id, info_type=info_type)
elif component_type in _ram_keywords:
return _info.ram_info(device_id=component_id)
elif component_type in _transmitter_keywords:
return _info.transmitter_info(device_id=component_id, info_type=info_type)
elif component_type in _receiver_keywords:
return _info.receiver_info(device_id=component_id, info_type=info_type)
else:
raise ValueError("Type of information requested is not recognised.")
[docs] def summary(
self,
summary_type=None,
output_format='df',
node_id=None,
latency_type=None,
bar3d=False,
data_range=None,
):
"""
Obtain a summary of the simulation performed. Refer to `Summary` class for more details.
Parameters
----------
summary_type : str, optional
The type of summary, chosen from the following:
- `None`
No `summary_type` input, and a generic summary is returned.
- `latency` or `l`
Latency summary, with latency information for all source-destination combinations.
- `ram` or `RAM`
Transmitter RAM data generation summary.
- `transmitter` or `t`
Transmitter summary.
- `receiver` or `r`
Receiver summary.
- `count` or `c`
Total packet transmission count.
- `queue` or `q`
Queue size in RAMs.
output_format : str, optional
The format of the summary, chosen from the following:
- `df`
Return a pandas DataFrame, which is the default output format.
- `csv`
Export a summary of the simulation performed as a csv file. Refer to `Summary` class for more details.
- `plot`
A plot of the summary selected.
node_id : int, optional
The node ID used for latency summary plot output.
latency_type : string, optional
The latency type of interset, by default is ``None``, which is transfer delay. \
Setting it as `queueing delay` or `qd` switches it to queueing delay.
bar3d : bool, optional
Enable 3d bar plot for queueing delay, default is ``False``.
data_range : str, optional
The range of data selected for latency and delay summary, chosen from the following:
- `all` or `a`
All simulation data.
- `extended` or `e`
Extended simulation data in convergence mode.
- `batch` or `b`
Last batch data in convergence mode.
Default is ``None``, \
which is extended data for convergence mode and all data for non-convergence mode.
"""
_latency_keywords = {'latency', 'l'}
_ram_keywords = {'ram', 'RAM'}
_transmitter_keywords = {'transmitter', 't'}
_receiver_keywords = {'receiver', 'r'}
_count_keywords = {'count', 'c'}
_queueing_delay_keywords = {'queueing delay', 'qd'}
_queue_size_keywords = {'queue', 'q'}
_summary = Summary(simulator=self)
summary_df = None
_summary_name = None
if summary_type is None:
summary_df = _summary.simulation_summary()
summary_type = 'simulation'
elif summary_type in _ram_keywords:
summary_df = _summary.ram_summary()
elif summary_type in _transmitter_keywords:
summary_df = _summary.transmitter_summary()
elif summary_type in _receiver_keywords:
summary_df = _summary.receiver_summary()
elif summary_type in _latency_keywords:
if latency_type in _queueing_delay_keywords:
summary_df = _summary.packet_delay_summary()
else:
summary_df = _summary.latency_summary(data_range=data_range, latency_type=latency_type)
elif summary_type in _count_keywords:
summary_df = _summary.packet_count_summary()
elif summary_type in _queue_size_keywords:
summary_df = _summary.ram_queue_summary()
else:
raise ValueError("Summary type is not recognised.")
# Output summary based on format selected
if output_format == 'df':
return summary_df
elif output_format == 'csv':
_summary_name = summary_type
_file_name = _summary_name + "_summary.csv"
summary_df.to_csv(_file_name, index=False)
elif output_format == 'plot':
if summary_type == 'simulation':
return plot_latency_throughput(self.latency)
elif summary_type in _latency_keywords:
return plot_latency(
simulator=self,
latency=summary_df,
node_id=node_id,
latency_type=latency_type,
bar3d=bar3d
)
elif summary_type in _count_keywords:
return plot_count(summary_df)
else:
raise ValueError("Plot type not recognised.")
else:
raise ValueError("Output format not recognised.")
[docs] def export_data_as_csv(self, file_name=None, data=None, index=None, columns=None):
"""
Export a python list as a csv file.
Parameters
----------
file_name: string
Name of the output csv file
data : ndarray (structured or homogeneous), Iterable, dict, or DataFrame
The data to be exported as csv file
index: list, optional
Index to use for resulting csv file
columns: list, optional
Column labels to use for csv file
"""
if not isinstance(file_name, str):
raise ValueError("file_name must be a string.")
elif file_name.rfind(".csv", -4) == -1:
file_name = file_name + ".csv"
df = pd.DataFrame(data, columns=columns, index=index)
row_names = False
if index:
row_names = True
df.to_csv(file_name, index=row_names)