diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dee05de --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Ignoring python cache +**/__pycache__/** \ No newline at end of file diff --git a/simu-lora/.gitignore b/simu-lora/.gitignore new file mode 100644 index 0000000..cfb04ce --- /dev/null +++ b/simu-lora/.gitignore @@ -0,0 +1 @@ +lorawan-sim-outputs/** \ No newline at end of file diff --git a/simu-lora/README.md b/simu-lora/README.md new file mode 100644 index 0000000..482b822 --- /dev/null +++ b/simu-lora/README.md @@ -0,0 +1,43 @@ +# LoRaWAN-sim + +Write short project desctiption. + +## Installation + +Please follow this short guide to install the project on your local setup. +Python virtual environments are used to manage dependencies without impacting your +other projects. + +After cloning the project and moving to its folder : + +1. Fetch virtualenv via pip if it is not already installed. + + ``` + pip install virtualenv + ``` + +2. Create a virtualenv `venv` using python 3 in the project folder. + + ``` + virtualenv -p /usr/bin/python3 venv + ``` + +3. Activate `venv`. + + ``` + source venv/bin/activate + ``` + +4. Install all requirements in `venv`. + + ``` + pip install -r requirements.txt + ``` + +## Usage + +A word about launching the scripts. + +## License + +To be determined later diff --git a/simu-lora/config/config.yaml b/simu-lora/config/config.yaml new file mode 100644 index 0000000..eff9345 --- /dev/null +++ b/simu-lora/config/config.yaml @@ -0,0 +1,54 @@ +# The following configuration is LoRa-specific. All durations are expressed in seconds. + +--- +batch-params: + seeds: [11, 12, 13, 14, 15, 16, 17, 18] + +experiment-params: + network-type: LORA # {LORA} + gateways-layout: SINGLE # {HONEYCOMB, SQUARE, POISSON, GENERIC, SINGLE} + devices-layout: POISSON # {POISSON} + duration: 36000 + channels: [1, 2] + spreading-factors: [7] + clock-drift: False + adr: False + class-switching-policy: NONE # {NONE} + + gateways-params: + duty-cycle: 10.0 # 0.0 < duty-cycle <= 100.0 + downlink-interarrival-time: # -1 to disable downlinks + min: -1 + max: -1 + downlink-toa: + min: 0.62694 + max: 0.62694 + beacon-toa: 0.17306 + buffer-sizes: + input: 1 + output: 1 + + devices-params: + duty-cycle: 1.0 # 0.0 < duty-cycle <= 100.0 + uplink-interarrival-time: + min: 3600 + max: 3600 + uplink-toa: + min: 0.62694 + max: 0.62694 + buffer-sizes: + input: 1 + output: 1 + initial-class-repartition: + A: 100 + B: 0 + C: 0 + S: 0 + + devices-densities: + custom-values: [] # Optional custom density list + range: # Mandatory only if custom-values is not set + start: 1 + stop: 400 + step: 50 +... \ No newline at end of file diff --git a/simu-lora/lorawan-sim-campaign-post.py b/simu-lora/lorawan-sim-campaign-post.py new file mode 100755 index 0000000..d09243f --- /dev/null +++ b/simu-lora/lorawan-sim-campaign-post.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 + +from __future__ import division +import matplotlib.pyplot as plt +import sys +import math +import numpy +import os +import io +import scipy.stats + +from simlib.defaults import * +from simlib.models import * + +A = (1,1) +B = (2.5,1) +C = (2,2) + +# modify from here + +# the throughput is the sum of exonential functions +# T = p * mu * greek_PI / AREA * [ COEFFICIENT_1 * exp (-(1-q) * mu * UNION_1) + COEFFICIENT_2 * exp (-(1-q) * mu * UNION_2) + ...] + +UNIONS = (math.pi, ) # (UNION_1, UNION_2, ...) +COEFFICIENTS = (3 * math.pi, ) # (COEFFICIENT_1, COEFFICIENT_2, ...) +AREA = 3 * math.pi # AREA + +# to here + +def postprocessing(): + directory = os.path.abspath(os.path.join('.', DEFAULT.OUTPUT_FOLDER_NAME, 'results-LoRaWAN-sim-3-gw')) + if not os.path.exists(directory): + print('this kind of experiment has never been tested') + return + else: + directory_dumps = os.path.abspath(os.path.join(directory, 'dumps')) + if not os.path.exists(directory_dumps): + print('no simulation present') + return + directory_figures = os.path.abspath(os.path.join(directory, 'figures')) + try: + os.makedirs(directory_figures) + except FileExistsError: + pass + + rate = 1 / DEFAULT.INTERARRIVAL_TIME_MIN + time_on_air = DEFAULT.TIME_TX_PACKET_MAX + channels_values = [1, 3] + duty_cycle_values = [100.0, 1.0] + L = 1 + colors = [['r', 'g', 'b'], [(1.0, 0.5, 0.5), (0.5, 1.0, 0.5), (0.5, 0.5, 1.0)]] + dc2label = {100.0: 'No', 1.0: '1%'} + + data = getdata(directory = directory_dumps) + + figure = plt.figure() + + for enum1, num_channels in enumerate(channels_values): + p, q = pure_Aloha_model(rate, time_on_air, num_channels) + for enum2, duty_cycle in enumerate(duty_cycle_values): + throughput_function = throughput_model(UNIONS, COEFFICIENTS, AREA, p(duty_cycle / 100), q(duty_cycle / 100)) + key = (duty_cycle, num_channels) + xx = numpy.arange(0, 81, 10) + if key in data: + xx = data[key]['xx'] + yy = data[key]['T{}'.format(L)] + num = numpy.array(data[key]['num']) + levels = numpy.array([scipy.stats.t.interval(0.95, n - 1)[1] for n in data[key]['num']]) + yyerr = numpy.array(data[key]['E{}'.format(L)]) * levels + figure.gca().errorbar(xx, yy, yerr=yyerr, + color=colors[enum2][enum1+1], + linestyle=':', + marker = 'o', ms=4, mfc='none', + capsize=2, + label='{} ch, {} DC, sim'.format(num_channels, dc2label[duty_cycle])) + yy_theory = numpy.vectorize(throughput_function)(xx) + figure.gca().plot(xx, yy_theory, + color=colors[enum2][enum1+1], + label='{} ch, {} DC, theory'.format(num_channels, dc2label[duty_cycle])) + + figure.gca().grid() + figure.gca().legend() + figure.gca().set_xlabel(r'$\mathrm{Number\/of\/end\/devices\/per\/R^2\/km^2}$', fontsize='x-large') + figure.gca().set_ylabel(r'$\mathrm{Throughput}$', fontsize='x-large') + figure.savefig(os.path.abspath(os.path.join(directory_figures, 'throughput_REOC.png'))) + figure.savefig(os.path.abspath(os.path.join(directory_figures, 'throughput_REOC.eps'))) + plt.close(figure) + +def getdata(directory): + list_dir = os.listdir(directory) + experiments = [] + successful_rx = 3 + for filename in list_dir: + if 'short' in filename: + f = io.open(os.path.join(directory, filename), encoding='utf-8') + lines = f.readlines() + f.close() + experiment = [] + for line in lines: + line = line.strip() + if line != DEFAULT.SEPARATOR: + experiment += [line] + continue + if len(experiment) != 2: + print(experiment) + assert len(experiment) == 2 + experiments += [experiment[:]] + experiment = [] + data = {} + for experiment in experiments: + header, rawdata = experiment + header = header.split(' ') + rawdata = rawdata.split(' ') + seed = int(header[header.index('SEED') + 1]) + assert seed != None + + duty_cycle = float(header[header[header.index('EDCOM'):].index('duty_cycle') + 1 + header.index('EDCOM')]) + + density = float(header[header[header.index('EDDEP'):].index('density') + 1 + header.index('EDDEP')]) + + index_channels = 1 + channels_chunk = header[header[header.index('COM'):].index('channels') + index_channels + header.index('COM')] + while channels_chunk[-1] != ')': + index_channels += 1 + channels_chunk += header[header[header.index('COM'):].index('channels') + index_channels + header.index('COM')] + channels = len(eval(channels_chunk)) + + duration = float(header[header.index('DURATION') + 1]) + + width = float(header[header.index('width') + 1]) + + height = float(header[header.index('height') + 1]) + + area_simulations = width * height + + area_of_relevance = AREA + + key = (duty_cycle, channels) + + throughput = tuple(float(rawdata[rawdata.index('T{}'.format(i)) + 1]) for i in range(1, successful_rx + 1)) + + if key not in data: + data[key] = {} + if density not in data[key]: + data[key][density] = {'values': [], 'seeds': [], 'durations': []} + if seed not in data[key][density]['seeds']: + data[key][density]['seeds'] += [seed] + data[key][density]['values'] += [throughput] + data[key][density]['durations'] += [duration] + else: + duration_stored_index = data[key][density]['seeds'].index(seed) + duration_stored = data[key][density]['durations'][duration_stored_index] + if duration > duration_stored: + data[key][density]['values'][duration_stored_index] = throughput + data[key][density]['durations'][duration_stored_index] = duration + + for key, item in data.items(): + xx = sorted(item.keys()) + t_avg = dict((i + 1, []) for i in range(successful_rx)) + t_err = dict((i + 1, []) for i in range(successful_rx)) + t_num = [] + for x in xx: + values = numpy.array(item[x]['values']) * area_simulations / area_of_relevance + avg = numpy.mean(values, axis=0) + err = scipy.stats.sem(values, axis=0) + for i in range(successful_rx): + t_avg[i + 1] += [avg[i]] + t_err[i + 1] += [err[i]] + t_num += [len(values)] + data[key] = {'xx': xx, 'num': t_num} + for i in range(1, successful_rx + 1): + data[key]['T{}'.format(i)] = t_avg[i] + data[key]['E{}'.format(i)] = t_err[i] + return data + +if __name__ == '__main__': + postprocessing() diff --git a/simu-lora/lorawan-sim-campaign.py b/simu-lora/lorawan-sim-campaign.py new file mode 100755 index 0000000..8b52070 --- /dev/null +++ b/simu-lora/lorawan-sim-campaign.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 + +import numpy + +from simlib.deployment import * +from simlib.experiment import * +from simlib.defaults import * + + +def lorawan_sim(): + time_id = int(time.time()) + channels_values = [1, 3] + duty_cycle_values = [100.0, 1.0] + densities_values = numpy.arange(0, 81, 5) + + # modify from here + + seeds = [172832539, + 180175907, + 76988325, + 79139770, + 672615, + 167381976, + 267338817, + 267376156, + 987461783, + 283866283 + ] # seeds used to feed the pseudo-random number generator, simulate different scenarios, and achieve statistical significance + # augmente l'intervalle de confiance + + duration = 60*60 # duration of each simulation in seconds + + # to here + + deployment = Deployment.gateway_infrastructure( + width=3.5, # width of the simulation area (in units of distance, i.e., the coverage radius, which is long 1) : it should be large enough to account for all coverage areas + height=3, # height of the simulation area (in units of distance, i.e., the coverage radius, which is long 1): it should be large enough to account for all coverage areas + grid=[ # each line should contain the coordinates (in units of distance, i.e., the coverage radius, which is long 1) of a gateway within the area: the related disk should be into the coverage area + (1, 1), + (2.5, 1), + (2, 2) + ] + ) + + for num_channels in channels_values: + deployment.reset_gateways(channels=tuple(range(num_channels))) + for density in densities_values: + for duty_cycle in duty_cycle_values: + for seed in seeds: + print('-------------------> Channels {}, Density {}, Duty Cycle {}, Seed {}'.format(num_channels, + density, + duty_cycle, + seed)) + e = Experiment(duration=duration, + seed=seed, + execution_id=time_id, + results_dirname='results-LoRaWAN-sim-3-gw', + long_file_enabled=False) + + deployment.poisson_end_device_infrastructure(density=density, + interarrival_time=DEFAULT.INTERARRIVAL_TIME_MIN, + time_tx_packet=DEFAULT.TIME_TX_PACKET_MAX, + duty_cycle=duty_cycle, + output_bufsize=1, + backlog_until_end_of_duty_cycle=True) + # ~ plot_file = 'map-{}-{}'.format(seed, density) + # ~ if not e.isfile(plot_file): + # ~ e.plot(filename = plot_file) + try: + e.run(relaxed=True, verbose=False) + except: + raise + print('exiting simulation... bye bye') + deployment.remove_end_devices() + deployment.reset() + + +if __name__ == '__main__': + lorawan_sim() diff --git a/simu-lora/requirements.txt b/simu-lora/requirements.txt new file mode 100644 index 0000000..be4ce29 --- /dev/null +++ b/simu-lora/requirements.txt @@ -0,0 +1,8 @@ +PyYAML +cycler==0.10.0 +kiwisolver==1.1.0 +matplotlib==3.1.3 +numpy==1.18.1 +pyparsing==2.4.6 +python-dateutil==2.8.1 +six==1.14.0 diff --git a/simu-lora/simlib/__init__.py b/simu-lora/simlib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/simu-lora/simlib/borg.py b/simu-lora/simlib/borg.py new file mode 100644 index 0000000..d5ef5e8 --- /dev/null +++ b/simu-lora/simlib/borg.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 + +from six import with_metaclass + + +class BorgMeta(type): + def __repr__(cls): + if hasattr(cls, '_class_repr'): + return getattr(cls, '_class_repr')() + else: + return super(BorgMeta, cls).__repr__() + + +class Borg(with_metaclass(BorgMeta, object)): + __shared_state = None + __master = None + __master_permitting_update = False + + def __new__(cls, *args, **kwargs): + self = object.__new__(cls) + if cls.__shared_state is None: + cls.__shared_state = {} + self.__dict__ = cls.__shared_state + return self + + def __init__(self, *args, **kwargs): + + def init(*a, **k): + raise AttributeError('this borg class can be reinitialized after having reset the current one') + + master = kwargs.pop('master', False) + master_permitting_update = kwargs.pop('master_permitting_update', False) + if not self.master_exists(): + self.__set_master_internal(master, master_permitting_update) + self.init(*args, **kwargs) + if self.is_master(): + self.init = init + else: + if master or master_permitting_update: + raise ValueError('a master already exists') + if self.master_permitting_update_exists(): + self.init_update(*args, **kwargs) + + def __repr__(self): + class_name = self.__class__.__name__ + dictionary = ', '.join('{}'.format(repr(key)) for key in self.__dict__.keys()) + return '{}({{{}}})'.format(class_name, dictionary) + + @classmethod + def _class_repr(cls): + class_name = cls.__name__ + dictionary = ', '.join('{}'.format(repr(key)) for key in cls.__dict__.keys()) + return '{}({{{}}})'.format(class_name, dictionary) + + def init(self, *args, **kwargs): + pass + + def init_update(self, *args, **kwargs): + pass + + def master_exists(self): + to_return = False + if self.__class__.__master is not None: + to_return = True + return to_return + + def is_master(self): + to_return = False + if self.__class__.__master == id(self): + to_return = True + return to_return + + def master_permitting_update_exists(self): + return self.__class__.__master_permitting_update + + def set_master(self): + if not self.master_exists(): + self.__set_master_internal(True, False) + + def set_master_permitting_update(self): + if not self.master_exists() or self.is_master(): + self.__set_master_internal(True, True) + + def __set_master_internal(self, master, master_permitting_update): + self.__class__.__master_permitting_update = master_permitting_update + if master_permitting_update == True or master == True: + self.__class__.__master = id(self) + + def reset(self): + if self.master_exists() and not self.is_master(): + raise AttributeError('this object has not the right to reset') + self.__class__.__master = None + self.__class__.__master_permitting_update = False + self.__dict__.clear() diff --git a/simu-lora/simlib/buffer.py b/simu-lora/simlib/buffer.py new file mode 100644 index 0000000..c60ac30 --- /dev/null +++ b/simu-lora/simlib/buffer.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 + +from simlib.defaults import * + + +class BufferOverflow(Exception): + pass + + +class Buffer(object): + def __init__(self, bufsize=DEFAULT.BUFFER.SIZE): + self.__bufsize = bufsize + self.__queue = [] + + def enqueue(self, packet): + if self.__bufsize != 0: + if len(self.__queue) == self.__bufsize: + raise BufferOverflow() + self.__queue.append(packet) + + def select(self, condition=DEFAULT.BUFFER.SELECTCONDITION): + if not callable(condition): + raise TypeError('condition must be a function') + if condition.__code__.co_argcount != 1: + raise TypeError('condition function must take 1 argument') + for packet in self.__queue: + if condition(packet): + return packet + + def dequeue(self, packet): + self.__queue.remove(packet) diff --git a/simu-lora/simlib/defaults.py b/simu-lora/simlib/defaults.py new file mode 100644 index 0000000..438792e --- /dev/null +++ b/simu-lora/simlib/defaults.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 + +DEFAULT = lambda: None + +DEFAULT.RADIO = lambda: None +DEFAULT.RADIO.DIRECTION = lambda: None +DEFAULT.RADIO.DIRECTION.TX = 'TX' +DEFAULT.RADIO.DIRECTION.RX = 'RX' +DEFAULT.RADIO.DIRECTION.TXRX = 'TXRX' +DEFAULT.RADIO.STATE = lambda: None +DEFAULT.RADIO.STATE.SLEEP = 'SLEEP' +DEFAULT.RADIO.STATE.TRANSMITTING = 'TRANSMITTING' +DEFAULT.RADIO.STATE.LISTENING = 'LISTENING' +DEFAULT.RADIO.STATE.RECEIVING = 'RECEIVING' +DEFAULT.RADIO.STATE.COLLISION = 'COLLISION' + +DEFAULT.DEVICE = lambda: None +DEFAULT.DEVICE.COMMON = lambda: None +DEFAULT.DEVICE.COMMON.channels = tuple(range(1,2)) +DEFAULT.DEVICE.COMMON.coverage_range = 1 +DEFAULT.DEVICE.OPTIONAL = lambda: None +DEFAULT.DEVICE.OPTIONAL.output_bufsize = 1 +DEFAULT.DEVICE.OPTIONAL.input_bufsize = 0 + +DEFAULT.GATEWAY = lambda: None +DEFAULT.GATEWAY.duty_cycle = 100.0 +DEFAULT.GATEWAY.channels = tuple(range(1,7)) +DEFAULT.GATEWAY.output_bufsize = 4 +DEFAULT.GATEWAY.input_bufsize = 5 + +DEFAULT.ENDDEVICE = lambda: None +DEFAULT.ENDDEVICE.channels = tuple(range(1,4)) +DEFAULT.ENDDEVICE.interarrival_time = 200.0 +DEFAULT.ENDDEVICE.time_tx_packet = 1.0 +DEFAULT.ENDDEVICE.duty_cycle = 100.0 +DEFAULT.ENDDEVICE.backlog_until_end_of_duty_cycle = False +DEFAULT.ENDDEVICE.output_bufsize = 2 +DEFAULT.ENDDEVICE.input_bufsize = 3 + +DEFAULT.EDSTATE = lambda: None +DEFAULT.EDSTATE.IDLE = 'IDLE' +DEFAULT.EDSTATE.TRANSMITTING = 'TRANSMITTING' +DEFAULT.EDSTATE.DUTYCYCLE = 'DUTYCYCLE' + +DEFAULT.GWSTATE = lambda: None +DEFAULT.GWSTATE.IDLE = 'IDLE' +DEFAULT.GWSTATE.RECEIVING = 'RECEIVING' +DEFAULT.GWSTATE.COLLISION = 'COLLISION' + +DEFAULT.LORADEVICE_CLASS = lambda: None +DEFAULT.LORADEVICE_CLASS.A = 'CLASS_A' +DEFAULT.LORADEVICE_CLASS.B = 'CLASS_B' +DEFAULT.LORADEVICE_CLASS.C = 'CLASS_C' +DEFAULT.LORADEVICE_CLASS.S_SLOTTED_ALOHA = 'CLASS_S_SLOTTED_ALOHA' +DEFAULT.LORADEVICE_CLASS.S_SINGLE_GW_SCHEDULING = 'CLASS_S_SINGLE_GW_SCHEDULING' + +DEFAULT.THEORETICAL_CLASS = lambda: None +DEFAULT.THEORETICAL_CLASS.PURE_ALOHA = 'THEORETICAL_PURE_ALOHA' +DEFAULT.THEORETICAL_CLASS.SLOTTED_ALOHA = 'THEORETICAL_SLOTTED_ALOHA' + +DEFAULT.PACKET = lambda: None +DEFAULT.PACKET.GENERATED = 'GENERATED' +DEFAULT.PACKET.STARTTX = 'STARTTX' +DEFAULT.PACKET.STOPTX = 'STOPTX' +DEFAULT.PACKET.STARTRX = 'STARTRX' +DEFAULT.PACKET.STOPRX = 'STOPRX' +DEFAULT.PACKET.TORTX = 'TORTX' +DEFAULT.PACKET.CHANNEL = 'CHANNEL' +DEFAULT.PACKET.OUTCOME = 'OUTCOME' +DEFAULT.PACKET.TX = 'TX' +DEFAULT.PACKET.RX = 'RX' + +DEFAULT.BUFFER = lambda: None +DEFAULT.BUFFER.SIZE = 1 +DEFAULT.BUFFER.SELECTCONDITION = lambda x: True + +DEFAULT.DEPLOYMENT = lambda: None +DEFAULT.DEPLOYMENT.HONEYCOMB = 'HONEYCOMB' +DEFAULT.DEPLOYMENT.SQUARE = 'SQUARE' +DEFAULT.DEPLOYMENT.POISSON = 'POISSON' +DEFAULT.DEPLOYMENT.GENERIC = 'GENERIC' +DEFAULT.DEPLOYMENT.SINGLE = 'SINGLE' +DEFAULT.DEPLOYMENT.INTRAGW_DISTANCE = 1 +DEFAULT.DEPLOYMENT.COVERAGE_RANGE = 1 +DEFAULT.DEPLOYMENT.HONEYCOMB_GW_PER_ROW = 10 +DEFAULT.DEPLOYMENT.HONEYCOMB_ROWS = 12 +DEFAULT.DEPLOYMENT.SQUARE_GW_PER_ROW = 10 +DEFAULT.DEPLOYMENT.SQUARE_ROWS = 10 +DEFAULT.DEPLOYMENT.POISSON_WIDTH = 10 +DEFAULT.DEPLOYMENT.POISSON_HEIGHT = 10 +DEFAULT.DEPLOYMENT.POISSON_GW_DENSITY = 1 +DEFAULT.DEPLOYMENT.POISSON_ED_DENSITY = 10 + +DEFAULT.TIME_TX_PACKET_MAX = 0.368896 +DEFAULT.TIME_TX_PACKET_MIN = 0.046336 +DEFAULT.INTERARRIVAL_TIME_MIN = 60 +DEFAULT.INTERARRIVAL_TIME_MAX = 600 +DEFAULT.DURATION = 60*60*1 +DEFAULT.SIDE = 'SIDE' +DEFAULT.AREA = 'AREA' +DEFAULT.UNIONS = 'UNIONS' +DEFAULT.COEFFICIENTS = 'COEFFICIENTS' +DEFAULT.SEPARATOR = '----------SEPARATOR----------' + +DEFAULT.BEACON_PERIOD = 128 +DEFAULT.BEACON_RESERVED = 2.120 +DEFAULT.BEACON_WINDOW = 122.880 +DEFAULT.BEACON_GUARD = 3 +DEFAULT.PINGSLOT_SIZE = 0.030 + +DEFAULT.OUTPUT_FOLDER_NAME = 'lorawan-sim-outputs' +DEFAULT.ROOT_DIRECTORY = '.' diff --git a/simu-lora/simlib/deployment.py b/simu-lora/simlib/deployment.py new file mode 100644 index 0000000..5b7e3dd --- /dev/null +++ b/simu-lora/simlib/deployment.py @@ -0,0 +1,563 @@ +#!/usr/bin/env python3 + +from __future__ import division +import math +import matplotlib.pyplot as plt +import numpy + +from simlib.borg import * +from simlib.monitor import * +from simlib.eventscheduler import * +from simlib.lora.loraenddevice import LoRaEndDevice +from simlib.lora.loragateway import LoRaGateway +from simlib.defaults import * +from simlib.randomness import * + + +class Deployment(Borg): + ''' Public Borg methods to be defined in subclasses ''' + + def init(self, width=None, height=None, gateways=None, end_devices=None, delete=False, check_coverage=False, + **kwargs): + if self.master_exists() and not self.is_master(): + raise AttributeError('a bug is present, this must never happen') + if None in [width, height]: + if self.master_exists(): + self.reset() + raise ValueError( + 'master cannot be set without proper values for width and/or height and/or distance unit') + if width != height: + raise ValueError( + 'width and height must be configured to have simultaneously their own value or to be both set to None') + if gateways or end_devices: + raise ValueError( + 'gateways and end devices can be added/deleted after that width and height have been specified') + if kwargs: + raise ValueError('further arguments can be specified when also defining width and height') + return + self.set_master_permitting_update() + self.__dimensions = (width, height) + self.__gateways = dict() + self.__end_devices = dict() + self.__init_common() + self.__set_common(**kwargs) + self.init_update(gateways=gateways, end_devices=end_devices, delete=delete, check_coverage=check_coverage) + self.monitor = Monitor() + self.event_scheduler = EventScheduler() + self.device_infrastructure = self.__device_infrastructure + self.gateway_infrastructure = self.__gateway_infrastructure + self.end_device_infrastructure = self.__end_device_infrastructure + self.single_gateway_infrastructure = self.__single_gateway_infrastructure + self.honeycomb_gateway_infrastructure = self.__honeycomb_gateway_infrastructure + self.square_gateway_infrastructure = self.__square_gateway_infrastructure + self.poisson_gateway_infrastructure = self.__poisson_gateway_infrastructure + self.poisson_end_device_infrastructure = self.__poisson_end_device_infrastructure + + def init_update(self, + gateways=None, + end_devices=None, + delete=False, + check_coverage=False + ): + is_gateway = lambda device: isinstance(device, LoRaGateway) + is_end_device = lambda device: isinstance(device, LoRaEndDevice) + is_connected = lambda device: bool(device.connections) + is_disconnected = lambda device: not bool(device.connections) + gateways = set(gateways) if gateways != None else set() + assert all(map(is_gateway, gateways)) + end_devices = set(end_devices) if end_devices != None else set() + assert all(map(is_end_device, end_devices)) + if not delete: + all_end_devices = set(self.__end_devices.values()) + all_end_devices |= end_devices + all_gateways = set(self.__gateways.values()) + all_gateways |= gateways + for device in gateways | end_devices: + if not self.__is_device_admittable(device): + raise ValueError('{} {} is not admittable'.format( + device.__class__.__name__, + device.get_attributes().id_device)) + if is_gateway(device): + neighbors = all_end_devices + else: + neighbors = all_gateways + for neighbor in neighbors: + device.connect(neighbor) + all_end_devices = set(self.__end_devices.values()) + all_end_devices |= end_devices + all_gateways = set(self.__gateways.values()) + all_gateways |= gateways + condition = not check_coverage + condition |= all(map(is_connected, gateways | end_devices)) + condition |= (bool(all_end_devices) != bool(all_gateways)) + if condition: + for gateway in gateways: + id_device = gateway.get_attributes().id_device + self.__gateways[id_device] = gateway + for end_device in end_devices: + id_device = end_device.get_attributes().id_device + self.__end_devices[id_device] = end_device + elif bool(end_devices) != bool(gateways): + for gateway in filter(is_connected, gateways): + id_device = gateway.get_attributes().id_device + self.__gateways[id_device] = gateway + for end_device in filter(is_connected, end_devices): + id_device = end_device.get_attributes().id_device + self.__end_devices[id_device] = end_device + else: + for device in gateways | end_devices: + device.disconnect() + return + for device in gateways | end_devices: + device.disconnect() + old_gateways = set(self.__gateways.values()) + old_end_devices = set(self.__end_devices.values()) + left_gateways = old_gateways - gateways + left_end_devices = old_end_devices - end_devices + condition = check_coverage + condition |= bool(left_end_devices) == bool(left_gateways) + if condition: + gateways.update(filter(is_disconnected, old_gateways)) + end_devices.update(filter(is_disconnected, old_end_devices)) + for device in gateways | end_devices: + id_device = device.get_attributes().id_device + if is_gateway(device): + self.__gateways.pop(id_device) + else: + self.__end_devices.pop(id_device) + self.__init_common() + + ''' Public factory methods ''' + + @classmethod + def device_infrastructure(cls, grid, device_type, **kwargs): + if device_type not in {LoRaEndDevice, LoRaGateway}: + raise AttributeError('unrecognized device type') + common = set(DEFAULT.DEVICE.COMMON.__dict__) + default_device_dict = DEFAULT.GATEWAY.__dict__ if device_type == LoRaGateway else DEFAULT.ENDDEVICE.__dict__ + common_device = (set(default_device_dict) | set(DEFAULT.DEVICE.OPTIONAL.__dict__)) - common + common_gateway, common_end_device = (common_device, None) if device_type == LoRaGateway else ( + None, common_device) + device_kwargs = dict(DEFAULT.DEVICE.OPTIONAL.__dict__) + device_kwargs.update(DEFAULT.DEVICE.COMMON.__dict__) + device_kwargs.update({key: default_device_dict[key] for key in common_device if key in default_device_dict}) + device_kwargs.update({key: kwargs[key] for key in set(kwargs) & (common_device | common)}) + devices = [device_type(i, *item, **device_kwargs) for i, item in enumerate(grid)] + gateways, end_devices = (devices, None) if device_type == LoRaGateway else (None, devices) + return cls(gateways=gateways, end_devices=end_devices, common=common, common_gateway=common_gateway, + common_end_device=common_end_device, **kwargs) + + @classmethod + def gateway_infrastructure(cls, grid, **kwargs): + return cls.device_infrastructure(grid=grid, device_type=LoRaGateway, **kwargs) + + @classmethod + def end_device_infrastructure(cls, grid, **kwargs): + return cls.device_infrastructure(grid=grid, device_type=LoRaEndDevice, **kwargs) + + @classmethod + def single_gateway_infrastructure(cls, width=None, height=None, coverage_range=DEFAULT.DEPLOYMENT.COVERAGE_RANGE, + **kwargs): + if width is not None and height is not None: + grid = [(width / 2, height / 2)] + elif width is None and height is None: + grid = [(coverage_range, coverage_range)] + width = 2 * coverage_range + height = 2 * coverage_range + else: + raise ValueError('width and height must be both None or both set to any values') + gateway_deployment = {'type_of_grid': DEFAULT.DEPLOYMENT.SINGLE} + return cls.gateway_infrastructure(grid=grid, width=width, height=height, coverage_range=coverage_range, + gateway_deployment=gateway_deployment, **kwargs) + + @classmethod + def __regular_gateway_infrastructure(cls, gateway_deployment, width=None, height=None, **kwargs): + if width is not None or height is not None: + raise ValueError('width and height cannot be defined') + grid = Deployment.__create_regular_grid(**gateway_deployment) + width, _ = max(grid, key=lambda x: x[0]) + _, height = max(grid, key=lambda x: x[1]) + return cls.gateway_infrastructure(grid=grid, width=width, height=height, gateway_deployment=gateway_deployment, + **kwargs) + + @classmethod + def honeycomb_gateway_infrastructure(cls, gateways_per_row=DEFAULT.DEPLOYMENT.HONEYCOMB_GW_PER_ROW, + rows=DEFAULT.DEPLOYMENT.HONEYCOMB_ROWS, + intragw_distance=DEFAULT.DEPLOYMENT.INTRAGW_DISTANCE, **kwargs): + gateway_deployment = {'type_of_grid': DEFAULT.DEPLOYMENT.HONEYCOMB, 'gateways_per_row': gateways_per_row, + 'rows': rows, 'intragw_distance': intragw_distance} + return cls.__regular_gateway_infrastructure(gateway_deployment, **kwargs) + + @classmethod + def square_gateway_infrastructure(cls, gateways_per_row=DEFAULT.DEPLOYMENT.SQUARE_GW_PER_ROW, + rows=DEFAULT.DEPLOYMENT.SQUARE_ROWS, + intragw_distance=DEFAULT.DEPLOYMENT.INTRAGW_DISTANCE, **kwargs): + gateway_deployment = {'type_of_grid': DEFAULT.DEPLOYMENT.SQUARE, 'gateways_per_row': gateways_per_row, + 'rows': rows, 'intragw_distance': intragw_distance} + return cls.__regular_gateway_infrastructure(gateway_deployment, **kwargs) + + @classmethod + def poisson_gateway_infrastructure(cls, width=DEFAULT.DEPLOYMENT.POISSON_WIDTH, + height=DEFAULT.DEPLOYMENT.POISSON_HEIGHT, + density=DEFAULT.DEPLOYMENT.POISSON_GW_DENSITY, **kwargs): + grid = Deployment.__create_poisson_grid(width, height, density) + gateway_deployment = {'type_of_grid': DEFAULT.DEPLOYMENT.POISSON, 'density': density} + return cls.gateway_infrastructure(grid=grid, width=width, height=height, gateway_deployment=gateway_deployment, + **kwargs) + + @classmethod + def poisson_end_device_infrastructure(cls, width=DEFAULT.DEPLOYMENT.POISSON_WIDTH, + height=DEFAULT.DEPLOYMENT.POISSON_HEIGHT, + density=DEFAULT.DEPLOYMENT.POISSON_ED_DENSITY, **kwargs): + grid = Deployment.__create_poisson_grid(width, height, density) + end_device_deployment = {'type_of_grid': DEFAULT.DEPLOYMENT.POISSON, 'density': density} + return cls.end_device_infrastructure(grid=grid, width=width, height=height, + end_device_deployment=end_device_deployment, **kwargs) + + ''' Public methods ''' + + def reset_gateways(self, **kwargs): + for gateway in self.__gateways.values(): + gateway.reset(**kwargs) + self.__reset_common_gateway(**kwargs) + for gateway in self.__gateways.values(): + if not self.__is_device_admittable(gateway): + raise ValueError('gateway {} is not admittable anymore'.format(gateway.id_device)) + + def reset_end_devices(self, **kwargs): + for end_device in self.__end_devices.values(): + end_device.reset(**kwargs) + self.__reset_common_end_device(**kwargs) + for end_device in self.__end_devices.values(): + if not self.__is_device_admittable(end_device): + raise ValueError('end device {} is not admittable anymore'.format(end_device.id_device)) + + def remove_end_devices(self): + self.init_update(end_devices=list(self.__end_devices.values()), delete=True) + + def remove_gateways(self): + self.init_update(gateways=list(self.__gateways.values()), delete=True) + + def get_dimensions(self): + if not self.master_exists(): + raise AttributeError('dimensions are not set') + return self.__dimensions + + def plot(self, plot_connections=False, plot_evaluation_area=False, filename='plot', png=True, eps=False): + if not self.master_exists(): + raise AttributeError('dimensions are not set') + xx_bs = [self.__gateways[i].get_attributes().x for i in sorted(self.__gateways.keys())] + yy_bs = [self.__gateways[i].get_attributes().y for i in sorted(self.__gateways.keys())] + xx_ed = [self.__end_devices[i].get_attributes().x for i in sorted(self.__end_devices.keys())] + yy_ed = [self.__end_devices[i].get_attributes().y for i in sorted(self.__end_devices.keys())] + plt.figure(figsize=(6, 6)) + if plot_evaluation_area: + if self.__gateway_deployment.type_of_grid == DEFAULT.DEPLOYMENT.SINGLE: + gateway = list(self.__gateways.values())[0] + circle = plt.Circle((gateway.get_attributes().x, gateway.get_attributes().y), + gateway.get_attributes().coverage_range, color='black', linestyle='--', fill=False) + fig = plt.gcf() + ax = fig.gca() + ax.add_artist(circle) + elif hasattr(self.__common, 'coverage_range') and self.__dimensions[ + 0] > 4 * self.__common.coverage_range and self.__dimensions[1] > 4 * self.__common.coverage_range: + coverage_range = self.__common.coverage_range + plt.plot([2 * coverage_range, self.__dimensions[0] - 2 * coverage_range, + self.__dimensions[0] - 2 * coverage_range, 2 * coverage_range, 2 * coverage_range], + [2 * coverage_range, 2 * coverage_range, self.__dimensions[1] - 2 * coverage_range, + self.__dimensions[1] - 2 * coverage_range, 2 * coverage_range], linewidth=3, color='black', + zorder=0) + if plot_connections: + for ed in self.__end_devices.values(): + for gw in ed.connections: + if ed in gw.connections: + plt.plot([ed.get_attributes().x, gw.get_attributes().x], + [ed.get_attributes().y, gw.get_attributes().y], color='0.2', linestyle='dotted', + zorder=1) + plt.scatter(xx_bs, yy_bs, marker='D', s=30, c='k', zorder=3, label='Gateway') + plt.scatter(xx_ed, yy_ed, c='w', edgecolors='k', zorder=2, label='End-device') + plt.axis('scaled') + plt.legend(loc='center', bbox_to_anchor=(0.9, 0.97), scatterpoints=1) + plt.xlim(0, self.__dimensions[0]) + plt.ylim(0, self.__dimensions[1]) + if png: + plt.savefig('{}.png'.format(filename), dpi=100) + if eps: + plt.savefig('{}.eps'.format(filename), dpi=100) + plt.close() + + def prepare(self): + + def create_string(attr, label=''): + var = '' + for key, value in sorted(attr.__dict__.items()): + var += '{} {} '.format(key, value) + if var: + var = ' '.join([label, var]) + return var + + if not self.master_exists(): + raise AttributeError('the deployment is not correctly initializd') + dimensions = 'AREA width {} height {} '.format(*self.__dimensions) + gateway_deployment = create_string(self.__gateway_deployment, label='GWDEP') + end_device_deployment = create_string(self.__end_device_deployment, label='EDDEP') + common = create_string(self.__common, label='COM') + common_gateway = create_string(self.__common_gateway, label='GWCOM') + common_end_device = create_string(self.__common_end_device, label='EDCOM') + string_deployment = ''.join( + [dimensions, gateway_deployment, end_device_deployment, common, common_gateway, common_end_device]).strip() + string_deployment_specific = '' + for id_device in sorted(self.__end_devices.keys()): + string_deployment_specific += 'ED {} {} {} '.format(id_device, + self.__end_devices[id_device].get_attributes().x, + self.__end_devices[id_device].get_attributes().y) + for id_device in sorted(self.__gateways.keys()): + string_deployment_specific += 'GW {} {} {} '.format(id_device, + self.__gateways[id_device].get_attributes().x, + self.__gateways[id_device].get_attributes().y) + self.monitor.logline(string_deployment, True, True) + self.monitor.logline(string_deployment_specific, True, False) + + def start(self): + for id_device in sorted(self.__end_devices.keys()): + self.__end_devices[id_device].start() + for id_device in sorted(self.__gateways.keys()): + self.__gateways[id_device].start() + + def save_stats(self, relaxed=False): # to be updated in a more scalable manner + if not hasattr(self.__common_end_device, 'time_tx_packet'): + raise AttributeError('this method cannot be called if time_tx_packet is not unique') + if not hasattr(self.__common, 'coverage_range') and not relaxed: + raise AttributeError('this method cannot be called if coverage range is not unique') + + margin = 2 * self.__common.coverage_range if not relaxed and self.__gateway_deployment.type_of_grid != DEFAULT.DEPLOYMENT.SINGLE else 0 + g = 0 + t = 0 + checked_rx = 3 + rx = dict((i, 0) for i in range(1, 1 + checked_rx)) + d = dict((i, []) for i in range(1, 1 + checked_rx)) + for id_device in sorted(self.__end_devices.keys()): + end_device = self.__end_devices[id_device] + stats = end_device.statistics + log_message = '#E{} GEN {} TX {} '.format(id_device, stats['GEN'], stats['TX']) + log_message += ''.join( + ['RX{} {} '.format(i, stats['RX'][i] if i in stats['RX'] else 0.0) for i in range(1, 1 + checked_rx)]) + for i in range(1, 1 + checked_rx): + log_message += 'D{} '.format(i) + d_array = stats['D'][i][:-1] if i in stats['D'] else [] + log_message += '{} '.format(numpy.mean(d_array) if d_array else numpy.nan) + log_message += '{} '.format(numpy.mean(numpy.array(d_array) ** 2) if d_array else numpy.nan) + log_message += '{} '.format(len(stats['D'][i][:-1]) if i in stats['D'] else 0) + log_message = log_message[:-1] + self.monitor.logline(log_message, True, False) + if ( + (self.__dimensions[0] > 2 * margin) and (self.__dimensions[1] > 2 * margin) + and + (end_device.get_attributes().x >= margin) and ( + end_device.get_attributes().x <= self.__dimensions[0] - margin) + and + (end_device.get_attributes().y >= margin) and ( + end_device.get_attributes().y <= self.__dimensions[1] - margin) + ): + g += stats['GEN'] + t += stats['TX'] + for i in range(1, 1 + checked_rx): + rx[i] += stats['RX'][i] if i in stats['RX'] else 0 + d[i] += stats['D'][i][:-1] if i in stats['D'] else [] + + log_message = '#TOT GEN {} TX {} '.format(g, t) + log_message += ''.join(['RX{} {} '.format(i, rx[i] if i in rx else 0.0) for i in range(1, 1 + checked_rx)]) + area = (self.__dimensions[0] - 2 * margin) * (self.__dimensions[ + 1] - 2 * margin) if self.__gateway_deployment.type_of_grid != DEFAULT.DEPLOYMENT.SINGLE else math.pi + estimated_throughput = lambda x: self.__common_end_device.time_tx_packet * math.pi * x / ( + self.event_scheduler.get_duration() * area) + log_message += ''.join( + ['T{} {} '.format(i, estimated_throughput(rx[i]) if i in rx else 0.0) for i in range(1, 1 + checked_rx)]) + for i in range(1, 1 + checked_rx): + log_message += 'D{} '.format(i) + log_message += '{} '.format(numpy.mean(d[i]) if (i in d and d[i]) else numpy.nan) + log_message += '{} '.format(numpy.mean(numpy.array(d[i]) ** 2) if (i in d and d[i]) else numpy.nan) + log_message += '{} '.format(len(d[i]) if i in d else 0) + log_message = log_message[:-1] + self.monitor.logline(log_message, True, True) + + def get_gateways(self): # do not use until it is modified to hinder modification of gateways + return self.__gateways + + def get_lengths(self): + + if not [device for device in self.__gateways.values()] + [device for device in self.__end_devices.values()]: + control = False + elif [device for device in self.__gateways.values() if not device.connections] + [device for device in + self.__end_devices.values() if + not device.connections]: + control = False + else: + control = True + + return len(self.__gateways), len(self.__end_devices), control + + ''' Private factory methods (they modify existent instance)''' + + def __device_infrastructure(self, grid, device_type, check_coverage=True, **kwargs): + if not self.is_master(): + raise AttributeError('modification are authorized only for the master') + if device_type not in {LoRaEndDevice, LoRaGateway}: + raise AttributeError('unrecognized device type') + if device_type == LoRaEndDevice and self.__end_devices: + raise AttributeError('end devices infrastructure already set') + elif device_type == LoRaGateway and self.__gateways: + raise AttributeError('gateways infrastructure already set') + if (self.__gateways or self.__end_devices) and set(self.__common.__dict__.keys()) != set( + DEFAULT.DEVICE.COMMON.__dict__.keys()): + raise AttributeError('this method cannot be used if coverage range and/or channels are not common') + common = set(DEFAULT.DEVICE.COMMON.__dict__) + default_device_dict = DEFAULT.GATEWAY.__dict__ if device_type == LoRaGateway else DEFAULT.ENDDEVICE.__dict__ + common_device = (set(default_device_dict) | set(DEFAULT.DEVICE.OPTIONAL.__dict__)) - common + common_gateway, common_end_device = (common_device, None) if device_type == LoRaGateway else ( + None, common_device) + self.__set_common(common=common, common_gateway=common_gateway, common_end_device=common_end_device, **kwargs) + device_kwargs = dict(DEFAULT.DEVICE.OPTIONAL.__dict__) + device_kwargs.update(DEFAULT.DEVICE.COMMON.__dict__) + device_kwargs.update({key: default_device_dict[key] for key in common_device if key in default_device_dict}) + device_kwargs.update(dict(self.__common.__dict__, **( + self.__common_gateway.__dict__ if device_type == LoRaGateway else self.__common_end_device.__dict__))) + devices = [device_type(i, *item, **device_kwargs) for i, item in enumerate(grid)] + gateways, end_devices = (devices, None) if device_type == LoRaGateway else (None, devices) + self.init_update(gateways=gateways, end_devices=end_devices, check_coverage=check_coverage) + + def __gateway_infrastructure(self, grid, **kwargs): + self.__device_infrastructure(grid, LoRaGateway, **kwargs) + + def __end_device_infrastructure(self, grid, **kwargs): + self.__device_infrastructure(grid, LoRaEndDevice, **kwargs) + + def __single_gateway_infrastructure(self, **kwargs): + grid = [(self.__dimensions[0] / 2, self.__dimensions[1] / 2)] + gateway_deployment = {'type_of_grid': DEFAULT.DEPLOYMENT.SINGLE} + self.__gateway_infrastructure(grid=grid, gateway_deployment=gateway_deployment, **kwargs) + + def __honeycomb_gateway_infrastructure(self, intragw_distance=DEFAULT.DEPLOYMENT.INTRAGW_DISTANCE, **kwargs): + gateways_per_row = int(self.__dimensions[0] / intragw_distance) + rows = int(self.__dimensions[1] / (intragw_distance * math.sqrt(3) / 2)) + gateway_deployment = {'type_of_grid': DEFAULT.DEPLOYMENT.HONEYCOMB, 'gateways_per_row': gateways_per_row, + 'rows': rows, 'intragw_distance': intragw_distance} + grid = Deployment.__create_regular_grid(**gateway_deployment) + self.__gateway_infrastructure(grid=grid, gateway_deployment=gateway_deployment, **kwargs) + + def __square_gateway_infrastructure(self, intragw_distance=DEFAULT.DEPLOYMENT.INTRAGW_DISTANCE, **kwargs): + gateways_per_row = int(self.__dimensions[0] / intragw_distance) + rows = int(self.__dimensions[1] / intragw_distance) + gateway_deployment = {'type_of_grid': DEFAULT.DEPLOYMENT.SQUARE, 'gateways_per_row': gateways_per_row, + 'rows': rows, 'intragw_distance': intragw_distance} + grid = Deployment.__create_regular_grid(**gateway_deployment) + self.__gateway_infrastructure(grid=grid, gateway_deployment=gateway_deployment, **kwargs) + + def __poisson_gateway_infrastructure(self, density=DEFAULT.DEPLOYMENT.POISSON_GW_DENSITY, **kwargs): + grid = Deployment.__create_poisson_grid(self.__dimensions[0], self.__dimensions[1], density) + gateway_deployment = {'type_of_grid': DEFAULT.DEPLOYMENT.POISSON, 'density': density} + self.__gateway_infrastructure(grid=grid, gateway_deployment=gateway_deployment, **kwargs) + + def __poisson_end_device_infrastructure(self, density=DEFAULT.DEPLOYMENT.POISSON_ED_DENSITY, **kwargs): + grid = Deployment.__create_poisson_grid(self.__dimensions[0], self.__dimensions[1], density) + end_device_deployment = {'type_of_grid': DEFAULT.DEPLOYMENT.POISSON, 'density': density} + self.__end_device_infrastructure(grid=grid, end_device_deployment=end_device_deployment, **kwargs) + + ''' Private helper methods''' + + def __set_common(self, common=None, common_gateway=None, common_end_device=None, gateway_deployment=None, + end_device_deployment=None, **kwargs): + common = common if isinstance(common, set) else set() + common_gateway = common_gateway if isinstance(common_gateway, set) else set() + common_end_device = common_end_device if isinstance(common_end_device, set) else set() + if common & common_gateway: + raise ValueError('not admittable common gateway') + if common & common_end_device: + raise ValueError('not admittable common end device') + common -= set(self.__common.__dict__) + self.__common.__dict__.update( + {key: DEFAULT.DEVICE.COMMON.__dict__[key] for key in common & set(DEFAULT.DEVICE.COMMON.__dict__)}) + self.__common.__dict__.update({key: kwargs[key] for key in common & set(kwargs)}) + common_gateway = common_gateway - set(self.__common.__dict__) - set(self.__common_gateway.__dict__) + self.__common_gateway.__dict__.update({key: DEFAULT.DEVICE.OPTIONAL.__dict__[key] for key in + common_gateway & set(DEFAULT.DEVICE.OPTIONAL.__dict__)}) + self.__common_gateway.__dict__.update( + {key: DEFAULT.GATEWAY.__dict__[key] for key in common_gateway & set(DEFAULT.GATEWAY.__dict__)}) + self.__common_gateway.__dict__.update({key: kwargs[key] for key in common_gateway & set(kwargs)}) + common_end_device = common_end_device - set(self.__common.__dict__) - set(self.__common_end_device.__dict__) + self.__common_end_device.__dict__.update({key: DEFAULT.DEVICE.OPTIONAL.__dict__[key] for key in + common_end_device & set(DEFAULT.DEVICE.OPTIONAL.__dict__)}) + self.__common_end_device.__dict__.update( + {key: DEFAULT.ENDDEVICE.__dict__[key] for key in common_end_device & set(DEFAULT.ENDDEVICE.__dict__)}) + self.__common_end_device.__dict__.update({key: kwargs[key] for key in common_end_device & set(kwargs)}) + self.__gateway_deployment.__dict__ = self.__gateway_deployment.__dict__ if gateway_deployment == None else gateway_deployment + self.__end_device_deployment.__dict__ = self.__end_device_deployment.__dict__ if end_device_deployment == None else end_device_deployment + + def __init_common(self): + if not self.__gateways: + self.__common_gateway = lambda: None + self.__gateway_deployment = lambda: None + self.__gateway_deployment.type_of_grid = DEFAULT.DEPLOYMENT.GENERIC + if not self.__end_devices: + self.__common_end_device = lambda: None + self.__end_device_deployment = lambda: None + self.__end_device_deployment.type_of_grid = DEFAULT.DEPLOYMENT.GENERIC + if not self.__gateways and not self.__end_devices: + self.__common = lambda: None + + def __is_device_admittable(self, device): + list_common = list(self.__common.__dict__.items()) + list_common += list(self.__common_gateway.__dict__.items()) if isinstance(device, LoRaGateway) else list( + self.__common_end_device.__dict__.items()) + device_attributes = device.get_attributes() + for key, value in list_common: + if key in device_attributes.__dict__ and getattr(device_attributes, key) != value: + return False + return True + + def __reset_common_gateway(self, **kwargs): + self.__common_gateway.__dict__.update( + {key: kwargs[key] for key in set(self.__common_gateway.__dict__) & set(kwargs)}) + if not set(kwargs.keys()) & set(self.__common.__dict__.keys()): + return + if self.__end_devices: + raise AttributeError('this method cannot be used to change common attributes if end devices are present') + self.__common.__dict__.update({key: kwargs[key] for key in set(self.__common.__dict__) & set(kwargs)}) + + def __reset_common_end_device(self, **kwargs): + self.__common_end_device.__dict__.update( + {key: kwargs[key] for key in set(self.__common_end_device.__dict__) & set(kwargs)}) + if not set(kwargs.keys()) & set(self.__common.__dict__.keys()): + return + if self.__gateways: + raise AttributeError('this method cannot be used to change common attributes if gateways are present') + self.__common.__dict__.update({key: kwargs[key] for key in set(self.__common.__dict__) & set(kwargs)}) + + ''' Private helper static methods''' + + @staticmethod + def __create_regular_grid(gateways_per_row, rows, intragw_distance, type_of_grid): + if not isinstance(gateways_per_row, int): + raise TypeError('the number of gateways per row must be an integer') + if not isinstance(rows, int): + raise TypeError('the number of rows must be an integer') + if type_of_grid == DEFAULT.DEPLOYMENT.HONEYCOMB: + grid = [(intragw_distance * (n + 0.5 * (row % 2)), intragw_distance * row * math.sqrt(3) / 2) for row in + range(rows + 1) for n in range(gateways_per_row + 1 - row % 2)] + elif type_of_grid == DEFAULT.DEPLOYMENT.SQUARE: + grid = [(intragw_distance * n, intragw_distance * row) for n in range(gateways_per_row + 1) for row in + range(rows + 1)] + else: + raise ValueError('unrecognized type of grid') + return grid + + @staticmethod + def __create_poisson_grid(width, height, density): + if not width or not height: + raise ValueError('width and height cannot be set to be null') + predefined_numpy_random = Randomness().get_predefined_numpy_random() + predefined_random = Randomness().get_predefined_random() + number_of_devices = predefined_numpy_random.poisson(width * height * density) + devices_grid = [(predefined_random.random() * width, predefined_random.random() * height) for i in + range(number_of_devices)] + return devices_grid diff --git a/simu-lora/simlib/device.py b/simu-lora/simlib/device.py new file mode 100644 index 0000000..d324f7d --- /dev/null +++ b/simu-lora/simlib/device.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 + +import math + +from simlib.defaults import * +from simlib.radio import * +from simlib.buffer import * + + +class Device(object): + def __init__(self, id_device, x, y, + coverage_range=DEFAULT.DEVICE.COMMON.coverage_range, + channels=DEFAULT.DEVICE.COMMON.channels, + output_bufsize=DEFAULT.DEVICE.OPTIONAL.output_bufsize, + input_bufsize=DEFAULT.DEVICE.OPTIONAL.input_bufsize): + # INHERIT this method in subclasses of Device + self.__id_device = id_device + self.__x = x + self.__y = y + self.connections = set() + self.incoming_connections = set() + if coverage_range is None: + raise TypeError('coverage range cannot be None') + if channels is None: + raise TypeError('channels cannot be None') + if output_bufsize is None: + raise TypeError('output bufsize cannot be None') + if input_bufsize is None: + raise TypeError('input bufsize cannot be None') + self.__reset(coverage_range=coverage_range, channels=channels, output_bufsize=output_bufsize, + input_bufsize=input_bufsize) + + # Overload comparison operator to enable device sorting + def __lt__(self, other): + return self.__id_device < other.get_attributes().id_device + + def start(self): + # INHERIT this method in subclasses of Device + self.__output_buffer = Buffer(self.__output_bufsize) + self.__input_buffer = Buffer(self.__input_bufsize) + self.__radios = set() + self.setup_radios() + + def setup_radios(self): + # OVERRIDE this method in subclasses of Device + radio = Radio(channels=self.__channels, device=self) + self.add_radio(radio) + + def add_radio(self, radio): + self.__radios.add(radio) + + def reset(self, **kwargs): + # INHERIT this method in subclasses of Device + self.__reset(**kwargs) + + def connect(self, neighbor): + distance = math.sqrt( + (self.__x - neighbor.get_attributes().x) ** 2 + (self.__y - neighbor.get_attributes().y) ** 2) + if distance <= self.__coverage_range: + self.connections.add(neighbor) + neighbor.incoming_connections.add(self) + if distance <= neighbor.get_attributes().coverage_range: + neighbor.connections.add(self) + self.incoming_connections.add(neighbor) + + def disconnect(self): + for neighbor in self.connections: + neighbor.incoming_connections.remove(self) + self.connections.clear() + for neighbor in self.incoming_connections: + neighbor.connections.remove(self) + self.incoming_connections.clear() + + def get_neighbors(self): + return self.connections + + def enqueue_transmitting_packet(self, packet): + self.__output_buffer.enqueue(packet) + + def select_transmitting_packet(self, condition=DEFAULT.BUFFER.SELECTCONDITION): + return self.__output_buffer.select(condition) + + def dequeue_transmitted_packet(self, packet): + self.__output_buffer.dequeue(packet) + + def enqueue_receiving_packet(self, packet): + self.__input_buffer.enqueue(packet) + + def select_receiving_packet(self, condition=DEFAULT.BUFFER.SELECTCONDITION): + return self.__input_buffer.select(condition) + + def dequeue_received_packet(self, packet): + self.__input_buffer.dequeue(packet) + + def can_transmit(self, channel=None): + return self.__get_radio(channel).can_transmit() + + def transmit(self, packet=None, condition=DEFAULT.BUFFER.SELECTCONDITION, channel=None, radio=None): + # INHERIT this method in subclasses of Device + if radio == None: + radio = self.__get_radio(channel) + else: + radio_channel = radio.get_fixed_channel() + if radio_channel is None: + assert channel is not None + else: + if channel is None: + channel = radio_channel + assert radio_channel == channel + if not radio.can_transmit(): + return + if packet == None: + packet = self.select_transmitting_packet(condition) + if packet == None: + return + radio.transmit(packet, channel) + for neighbor in self.get_neighbors(): + neighbor.receive(packet, channel) + + def transmit_completed_cb(self, packet, channel): + # INHERIT this method in subclasses of Device + assert packet is not None + assert channel is not None + + def start_availability_cb(self): + # OVERRIDE this method in subclasses of Device + pass + + def start_listening(self, channel): + radio = self.__get_radio(channel) + assert radio is not None + radio.start_listening(channel) + + def stop_listening(self, channel): + radio = self.__get_radio(channel) + assert radio is not None + radio.stop_listening() + + def receive(self, packet, channel): + # INHERIT this method in subclasses of Device + assert packet is not None + assert channel is not None + radio = self.__get_radio(channel) + if radio is None: + return + radio.receive(packet, channel) + + def receive_completed_cb(self, packet, channel): + # OVERRIDE or INHERIT this method in subclasses of Device + if packet is not None: + self.enqueue_receiving_packet(packet) + + def get_attributes(self): + obj = lambda: None + obj.id_device = self.__id_device + obj.x = self.__x + obj.y = self.__y + obj.coverage_range = self.__coverage_range + obj.channels = self.__channels + obj.output_bufsize = self.__output_bufsize + obj.input_bufsize = self.__input_bufsize + return obj + + def __reset(self, coverage_range=None, channels=None, output_bufsize=None, input_bufsize=None): + if coverage_range is not None: + if not coverage_range: + raise ValueError('coverage range cannot be null') + self.__coverage_range = coverage_range + if channels is not None: + if not isinstance(channels, tuple) or channels == (): + raise TypeError('channels must be arranged in a tuple and their number cannot be null') + self.__channels = channels + if output_bufsize is not None: + self.__output_bufsize = output_bufsize + if input_bufsize is not None: + self.__input_bufsize = input_bufsize + + def __get_radio(self, channel=None): + if channel is None: + assert len(self.__radios) == 1 + return list(self.__radios)[0] + for radio in self.__radios: + if radio.is_channel_available(channel): + return radio diff --git a/simu-lora/simlib/eventscheduler.py b/simu-lora/simlib/eventscheduler.py new file mode 100644 index 0000000..1b6f608 --- /dev/null +++ b/simu-lora/simlib/eventscheduler.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 + +from __future__ import division +import bisect +import time + +from simlib.borg import * + + +class EventScheduler(Borg): + ''' Public Borg methods to be defined in subclasses ''' + + def init(self, duration=None): + if self.master_exists() and not self.is_master(): + raise AttributeError('a bug is present, this must never happen') + if duration is None: + if self.master_exists(): + self.reset() + raise ValueError('master cannot be set without specifying a duration') + return + self.set_master() + self.__duration = duration + self.__current_time = 0 + self.__list_event_time = [] + + ''' Public methods ''' + + def run(self, verbose=False): + if not self.is_master(): + raise AttributeError('this method can only be called by the master') + compare_time = time.time() + while (self.__list_event_time != []) and (self.__list_event_time[0][0] <= self.__duration): + event_time, function = self.__list_event_time.pop(0) + assert event_time >= self.__current_time + if verbose: + now = int(event_time * 10 / self.__duration) + earlier = int(self.__current_time * 10 / self.__duration) + if now > earlier: + speed = event_time / (time.time() - compare_time) + print(now * 10, '%') + print('\tspeed\t\t\t{}'.format(speed)) + remaining_time = (self.__duration - event_time) / speed + print('\testimated end in\t{}m{}s'.format(int(remaining_time) // 60, remaining_time % 60)) + print('\tevents in queue\t\t{}'.format(len(self.__list_event_time))) + self.__current_time = event_time + function() + + def schedule_event(self, time_interval, function): + if not self.master_exists(): + raise AttributeError('this method cannot be called if a master does not exist') + absolute_time = self.__current_time + time_interval + if self.__duration is None or absolute_time <= self.__duration: + starting_index = bisect.bisect(self.__list_event_time, (absolute_time,)) + index_to_add = 0 + for index_to_add, item in enumerate(self.__list_event_time[starting_index:]): + if absolute_time != item[0]: + break + self.__list_event_time.insert(starting_index + index_to_add, (absolute_time, function)) + # ~ bisect.insort(self.__list_event_time, (absolute_time, function)) + + def get_current_time(self): + if not self.master_exists(): + raise AttributeError('this method cannot be called if a master does not exist') + return self.__current_time + + def get_duration(self): + if not self.master_exists(): + raise AttributeError('this method cannot be called if a master does not exist') + return self.__duration diff --git a/simu-lora/simlib/experiment.py b/simu-lora/simlib/experiment.py new file mode 100644 index 0000000..edd12cb --- /dev/null +++ b/simu-lora/simlib/experiment.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 + +import os +import time + +from simlib.eventscheduler import * +from simlib.monitor import * +from simlib.deployment import * +from simlib.randomness import * + + +class Experiment(object): + def __init__(self, duration, seed=None, root_directory=DEFAULT.ROOT_DIRECTORY, + results_dirname='results-LoRaWAN-sim', execution_id=None, randomness=None, + long_file_enabled=True): + self.event_scheduler = EventScheduler(duration=duration) + directory = os.path.abspath(os.path.join(root_directory, DEFAULT.OUTPUT_FOLDER_NAME, results_dirname)) + directory_dumps = os.path.abspath(os.path.join(directory, 'dumps')) + if not os.path.exists(directory_dumps): + try: + os.makedirs(directory_dumps) + except FileExistsError: + # Can have meanwhile been created by a concurrent process + pass + self.__directory_figures = os.path.abspath(os.path.join(directory, 'figures')) + if not os.path.exists(self.__directory_figures): + try: + os.makedirs(self.__directory_figures) + except FileExistsError: + # Can have meanwhile been created by a concurrent process + pass + + if execution_id is None: + execution_id = int(time.time()) + self.__execution_id = execution_id + self.monitor = Monitor(os.path.join(directory_dumps, 'dump_{}'.format(str(self.__execution_id))), long_file_enabled) + self.monitor.log('@EXPERIMENT {}DURATION {} '.format('SEED {} '.format(seed) if seed is not None else '', + duration), + True, True) + self.deployment = Deployment() + self.__flag_home_randomness = False + if randomness is None: + self.__flag_home_randomness = True + randomness = Randomness(master=True, seed=seed) + self.randomness = randomness + + def run(self, relaxed=False, verbose=False): + self.deployment.prepare() + self.deployment.start() + flag = False + try: + self.event_scheduler.run(verbose=verbose) + except KeyboardInterrupt: + print('stopped while running experiment') + flag = True + else: + self.deployment.save_stats(relaxed) + finally: + self.event_scheduler.reset() + self.monitor.reset() + if self.__flag_home_randomness: + self.randomness.reset() + if flag: + raise KeyboardInterrupt() + + def plot(self, filename=None, **kwargs): + if filename is None: + filename = 'map_{}'.format(self.__execution_id) + path_to_plot = os.path.abspath(os.path.join(self.__directory_figures, filename)) + self.deployment.plot(filename=path_to_plot, **kwargs) + + def isfile(self, filename): + path_to_plot = os.path.abspath(os.path.join(self.__directory_figures, filename)) + return os.path.isfile('{}.eps'.format(path_to_plot)) or os.path.isfile('{}.png'.format(path_to_plot)) diff --git a/simu-lora/simlib/gridfactory.py b/simu-lora/simlib/gridfactory.py new file mode 100644 index 0000000..d20c8f3 --- /dev/null +++ b/simu-lora/simlib/gridfactory.py @@ -0,0 +1,26 @@ +import math +from simlib.randomness import Randomness + + +class GridFactory: + @staticmethod + def create_poisson_grid(width, height, num_devices, force_in_range_1_gw=False): + if not width or not height: + raise ValueError('width and height cannot be set to be null') + if force_in_range_1_gw: + assert width == 2 and height == 2 + predefined_random = Randomness().get_predefined_random() + devices_grid = [] + for i in range(num_devices): + coords = (predefined_random.random() * width, predefined_random.random() * height) + if force_in_range_1_gw: + while abs(pow(coords[0] - 1, 2) + pow(coords[1] - 1, 2)) > 0.99: + coords = (predefined_random.random() * width, predefined_random.random() * height) + devices_grid.append(coords) + return devices_grid + + +if __name__ == '__main__': + randomness = Randomness(master=True, seed=4) + grid = GridFactory.create_poisson_grid(2, 2, 200, True) + print(grid) diff --git a/simu-lora/simlib/lora/loraenddevice.py b/simu-lora/simlib/lora/loraenddevice.py new file mode 100644 index 0000000..1bf1853 --- /dev/null +++ b/simu-lora/simlib/lora/loraenddevice.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 + +from __future__ import division +from simlib.device import * +from simlib.randomness import * +from simlib.radio import * +from simlib.packet import * + + +class LoRaEndDevice(Device): + def __init__(self, *args, **kwargs): + self.eventscheduler = EventScheduler() + self.__interarrival_time = kwargs.pop('interarrival_time', DEFAULT.ENDDEVICE.interarrival_time) + self.__time_tx_packet = kwargs.pop('time_tx_packet', DEFAULT.ENDDEVICE.time_tx_packet) + self.__duty_cycle = kwargs.pop('duty_cycle', DEFAULT.ENDDEVICE.duty_cycle) + self.__backlog_until_end_of_duty_cycle = kwargs.pop('backlog_until_end_of_duty_cycle', + DEFAULT.ENDDEVICE.backlog_until_end_of_duty_cycle) + self.__packet_count = 0 + self.monitor = Monitor() + self.statistics = {'GEN': 0.0, 'TX': 0.0, 'RX': {}, 'D': {}} + super(LoRaEndDevice, self).__init__(*args, **kwargs) + + def start(self): + super(LoRaEndDevice, self).start() + self.__schedule_generate() + + def setup_radios(self): + channels = tuple(list(self.get_attributes().channels) + [869.75]) + radio = Radio(channels=channels, device=self, duty_cycle=self.__duty_cycle) + self.add_radio(radio) + + def reset(self, interarrival_time=None, time_tx_packet=None, duty_cycle=None, bufsize=None, **kwargs): + super(LoRaEndDevice, self).reset(**kwargs) + self.__interarrival_time = self.__interarrival_time if interarrival_time is None else interarrival_time + self.__time_tx_packet = self.__time_tx_packet if time_tx_packet is None else time_tx_packet + self.__duty_cycle = self.__duty_cycle if duty_cycle is None else duty_cycle + self.statistics = {'GEN': 0.0, 'TX': 0.0, 'RX': {}, 'D': {}} + + def generate(self): + log_message = 'G {}'.format(self.get_attributes().id_device) + self.monitor.logline(log_message, timestamp=True) + self.statistics['GEN'] += 1 + packet = Packet(serial=self.__packet_count, device=self, toa=self.__time_tx_packet) + self.__packet_count += 1 + try: + self.enqueue_transmitting_packet(packet) + except BufferOverflow: + pass + else: + self.transmit() + self.__schedule_generate() + + def transmit(self, packet=None, condition=DEFAULT.BUFFER.SELECTCONDITION, channel=None, radio=None): + packet = self.select_transmitting_packet(condition=lambda packet: packet.is_not_handled()) + if packet is None: + return + if not self.can_transmit(): + log_message = 'P {}'.format(self.get_attributes().id_device) + self.monitor.logline(log_message, timestamp=True) + return + channel, = Randomness().get_runtime_random().sample(self.get_attributes().channels, 1) + super(LoRaEndDevice, self).transmit(packet=packet, channel=channel) + log_message = 'B {} {}'.format(self.get_attributes().id_device, channel) + self.monitor.logline(log_message, timestamp=True) + self.statistics['TX'] += 1 + + def transmit_completed_cb(self, packet, channel): + super(LoRaEndDevice, self).transmit_completed_cb(packet=packet, channel=channel) + if packet.delivery_finished(): + self.log_end_of_transmission(packet) + if not self.__backlog_until_end_of_duty_cycle: + self.dequeue_transmitted_packet(packet) + + def start_availability_cb(self): + if self.__backlog_until_end_of_duty_cycle: + packet = self.select_transmitting_packet(condition=lambda packet: not packet.is_not_handled()) + assert packet is not None + self.dequeue_transmitted_packet(packet) + log_message = 'A {}'.format(self.get_attributes().id_device) + self.monitor.logline(log_message, timestamp=True) + self.transmit() + + def log_end_of_transmission(self, packet): + log_message = 'E {}'.format(self.get_attributes().id_device) + successes = 0 + for id_device, reception in packet.get_receptions().items(): + if reception[DEFAULT.PACKET.OUTCOME]: + log_message += ' {}'.format(id_device) + successes += 1 + self.monitor.logline(log_message, timestamp=True) + current_time = self.eventscheduler.get_current_time() + for success in range(1, successes + 1): + if not success in self.statistics['RX']: + self.statistics['RX'][success] = 0.0 + self.statistics['RX'][success] += 1 + if current_time is None: + continue + if success not in self.statistics['D']: + self.statistics['D'][success] = [] + else: + assert len(self.statistics['D'][success]) >= 1 + previous_time = self.statistics['D'][success].pop() + self.statistics['D'][success] += [current_time - previous_time] + self.statistics['D'][success] += [current_time] + + def get_attributes(self): + obj = super(LoRaEndDevice, self).get_attributes() + obj.interarrival_time = self.__interarrival_time + obj.time_tx_packet = self.__time_tx_packet + obj.duty_cycle = self.__duty_cycle + return obj + + def __schedule_generate(self): + time_generation = Randomness().get_predefined_random().expovariate(1 / self.__interarrival_time) + self.eventscheduler.schedule_event(time_generation, self.generate) diff --git a/simu-lora/simlib/lora/loragateway.py b/simu-lora/simlib/lora/loragateway.py new file mode 100644 index 0000000..cb326c7 --- /dev/null +++ b/simu-lora/simlib/lora/loragateway.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 + +from __future__ import division +from simlib.device import * +from simlib.radio import * +from simlib.packet import * + + +class LoRaGateway(Device): + def __init__(self, *args, **kwargs): + self.__duty_cycle = kwargs.pop('duty_cycle', DEFAULT.GATEWAY.duty_cycle) + super(LoRaGateway, self).__init__(*args, **kwargs) + + def start(self): + super(LoRaGateway, self).start() + for channel in self.get_attributes().channels: + self.start_listening(channel=channel) + + def setup_radios(self): + for channel in self.get_attributes().channels: + radio = Radio(channels=(channel,), device=self, duty_cycle=self.__duty_cycle) + self.add_radio(radio) + radio = Radio(channels=(869.75,), device=self, txonly=True, duty_cycle=self.__duty_cycle) + self.add_radio(radio) + + def receive_completed_cb(self, packet, channel): + if packet is not None and packet.delivery_finished(): + packet.get_sender().log_end_of_transmission(packet) diff --git a/simu-lora/simlib/models.py b/simu-lora/simlib/models.py new file mode 100644 index 0000000..55e1697 --- /dev/null +++ b/simu-lora/simlib/models.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 + +from __future__ import division +import matplotlib.pyplot as plt +import sys +import math +import numpy +import os +import io + +from simlib.defaults import * + +DEPLOYMENTS = { + DEFAULT.DEPLOYMENT.SQUARE: { + 1: { + DEFAULT.SIDE: math.sqrt(2), + DEFAULT.AREA: 2, + DEFAULT.UNIONS: ( + math.pi, + 1.5 * math.pi + 1 + ), + DEFAULT.COEFFICIENTS: { + 1: ( + math.pi, + 2 - math.pi + ) + } + }, + 2: { + DEFAULT.SIDE: 1, + DEFAULT.AREA: 1, + DEFAULT.UNIONS: ( + math.pi, + 4 * math.pi / 3 + 0.5 * math.sqrt(3), + 1.5 * math.pi + 1, + 19 * math.pi / 12 + 0.5 * math.sqrt(3) + 1, + 5 * math.pi / 3 + math.sqrt(3) + 1 + ), + DEFAULT.COEFFICIENTS: { + 1: ( + math.pi, + math.sqrt(3) - 4 * math.pi / 3, + 2 - math.pi, + 5 * math.pi / 3 - 2 * math.sqrt(3), + math.sqrt(3) - math.pi / 3 - 1 + ), + 2: ( + 0, + 4 * math.pi / 3 - math.sqrt(3), + math.pi - 2, + 4 * math.sqrt(3) - 10 * math.pi / 3, + math.pi - 3 * math.sqrt(3) + 3 + ), + } + }, + 3: { + DEFAULT.SIDE: 0.4 * math.sqrt(5), + DEFAULT.AREA: 0.8, + DEFAULT.UNIONS: ( + math.pi, # U1 + 0.8 + math.pi + 2 * math.atan(2), # U21 + 0.4 * math.sqrt(6) + 2 * math.pi - 2 * math.atan(0.5 * math.sqrt(6)), # U22 + 0.8 + 2 * math.pi - 2 * math.atan(2), # U23 + 1.6 + 3 * math.pi - 4 * math.atan(2), # U31 + 1.2 + 0.2 * math.sqrt(6) + 2.5 * math.pi - 2 * math.atan(2) - math.atan(0.5 * math.sqrt(6)), # U32 + 1.2 + 0.4 * math.sqrt(6) + 2 * math.pi + 2 * math.atan(2) - 2 * math.atan(0.5 * math.sqrt(6)), # U33 + 2.4 + 3 * math.pi - 4 * math.atan(2), # U41 + 1.6 + 0.4 * math.sqrt(6) + 3 * math.pi - 2 * math.atan(2) - 2 * math.atan(0.5 * math.sqrt(6)), # U42 + 1.6 + 0.8 * math.sqrt(6) + 3 * math.pi - 4 * math.atan(0.5 * math.sqrt(6)) # U43 #check + ), + DEFAULT.COEFFICIENTS: { + 1: ( + math.pi, + - (-1.6 + 2 * math.pi - 4 * math.atan(2)), + - (-0.8 * math.sqrt(6) + 4 * math.atan(0.5 * math.sqrt(6))), + - (-1.6 + 4 * math.atan(2)), + -1.6 + 2 * math.pi - 4 * math.atan(2), + -1.6 - 0.8 * math.sqrt(6) - 2 * math.pi + 8 * math.atan(2) + 4 * math.atan(0.5 * math.sqrt(6)), + 1.6 - 1.6 * math.sqrt(6) - 4 * math.atan(2) + 8 * math.atan(0.5 * math.sqrt(6)), + - (-0.8 - math.pi + 4 * math.atan(2)), + - (1.6 - 1.6 * math.sqrt(6) - 4 * math.atan(2) + 8 * math.atan(0.5 * math.sqrt(6))), + 0 + ), + 2: ( + 0, + -1.6 + 2 * math.pi - 4 * math.atan(2), + -0.8 * math.sqrt(6) + 4 * math.atan(0.5 * math.sqrt(6)), + -1.6 + 4 * math.atan(2), + -2 * (-1.6 + 2 * math.pi - 4 * math.atan(2)), + -2 * (-1.6 - 0.8 * math.sqrt(6) - 2 * math.pi + 8 * math.atan(2) + 4 * math.atan( + 0.5 * math.sqrt(6))), + -2 * (1.6 - 1.6 * math.sqrt(6) - 4 * math.atan(2) + 8 * math.atan(0.5 * math.sqrt(6))), + 3 * (-0.8 - math.pi + 4 * math.atan(2)), + 3 * (1.6 - 1.6 * math.sqrt(6) - 4 * math.atan(2) + 8 * math.atan(0.5 * math.sqrt(6))), + - (1.6 - 0.8 * math.sqrt(6) - math.pi + 4 * math.atan(0.5 * math.sqrt(6))) + ), + 3: ( + 0, + 0, + 0, + 0, + -1.6 + 2 * math.pi - 4 * math.atan(2), + -1.6 - 0.8 * math.sqrt(6) - 2 * math.pi + 8 * math.atan(2) + 4 * math.atan(0.5 * math.sqrt(6)), + 1.6 - 1.6 * math.sqrt(6) - 4 * math.atan(2) + 8 * math.atan(0.5 * math.sqrt(6)), + -3 * (-0.8 - math.pi + 4 * math.atan(2)), + -3 * (1.6 - 1.6 * math.sqrt(6) - 4 * math.atan(2) + 8 * math.atan(0.5 * math.sqrt(6))), + 3 * (1.6 - 0.8 * math.sqrt(6) - math.pi + 4 * math.atan(0.5 * math.sqrt(6))) + ), + } + }, + }, + DEFAULT.DEPLOYMENT.HONEYCOMB: { + 1: { + DEFAULT.SIDE: math.sqrt(3), + DEFAULT.AREA: 0.75 * math.sqrt(3), + DEFAULT.UNIONS: ( + math.pi, + 5 * math.pi / 3 + 0.5 * math.sqrt(3) + ), + DEFAULT.COEFFICIENTS: { + 1: ( + 0.5 * math.pi, + 0.75 * math.sqrt(3) - 0.5 * math.pi + ) + } + }, + 3: { + DEFAULT.SIDE: 1, + DEFAULT.AREA: 0.25 * math.sqrt(3), + DEFAULT.UNIONS: ( + math.pi, + 4 * math.pi / 3 + 0.5 * math.sqrt(3), + 5 * math.pi / 3 + 0.5 * math.sqrt(3), + 1.5 * math.pi + math.sqrt(3), + 5 * math.pi / 3 + math.sqrt(3), + 5 * math.pi / 3 + 1.5 * math.sqrt(3) + ), + DEFAULT.COEFFICIENTS: { + 1: ( + 0.5 * math.pi, + 0.75 * math.sqrt(3) - math.pi, + 0.75 * math.sqrt(3) - 0.5 * math.pi, + 0.5 * math.pi - 0.5 * math.sqrt(3), + math.pi - 1.5 * math.sqrt(3), + 0.75 * math.sqrt(3) - 0.5 * math.pi + ), + 2: ( + 0, + math.pi - 0.75 * math.sqrt(3), + 0.5 * math.pi - 0.75 * math.sqrt(3), + math.sqrt(3) - math.pi, + 3 * math.sqrt(3) - 2 * math.pi, + 1.5 * math.pi - 2.25 * math.sqrt(3) + ), + 3: ( + 0, + 0, + 0, + 0.5 * math.pi - 0.5 * math.sqrt(3), + math.pi - 1.5 * math.sqrt(3), + 2.25 * math.sqrt(3) - 1.5 * math.pi + ), + } + }, + }, + DEFAULT.DEPLOYMENT.SINGLE: { + 1: { + DEFAULT.AREA: math.pi, + DEFAULT.UNIONS: ( + math.pi, + ), + DEFAULT.COEFFICIENTS: { + 1: ( + math.pi, + ) + } + }, + }, +} + + +def pure_Aloha_model(rate, time_on_air, channels): + x = rate * time_on_air + + def p_function(duty_cycle=1): + eps = 1 / duty_cycle + return x / (1 + eps * x) + + def q_function(duty_cycle=1): + eps = 1 / duty_cycle + min2eps = min(eps, 2) + num = min2eps * x - math.exp(-x) + math.exp(x * (1 - min2eps)) + den = channels * (1 + eps * x) + return 1 - num / den + + return p_function, q_function + + +def throughput_model_single(*args): + unions = (math.pi,) + coefficients = (math.pi,) + area = math.pi + return throughput_model(unions, coefficients, area, *args) + + +def throughput_model_regular(grid_type, minimum_coverage, L, *args): + deployment = DEPLOYMENTS[grid_type][minimum_coverage] + unions = deployment[DEFAULT.UNIONS] + coefficients = deployment[DEFAULT.COEFFICIENTS][L] + area = deployment[DEFAULT.AREA] + return throughput_model(unions, coefficients, area, *args) + + +def throughput_model(unions, coefficients, area, p, q): + assert len(unions) == len(coefficients) + + def throughput_function(density): + throughput_value = 0 + for index in range(len(unions)): + exp_func = math.exp(-(1 - q) * density * unions[index]) + throughput_value += coefficients[index] * exp_func + throughput_value *= p * density * math.pi / area + return throughput_value + + return throughput_function diff --git a/simu-lora/simlib/monitor.py b/simu-lora/simlib/monitor.py new file mode 100644 index 0000000..718274d --- /dev/null +++ b/simu-lora/simlib/monitor.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 + +import threading +import io + +from simlib.borg import * +from simlib.eventscheduler import * +from simlib.defaults import * + + +class Monitor(Borg): + ''' Public Borg methods to be defined in subclasses ''' + + def init(self, file_name=None, long_file_enabled=True): + if self.master_exists() and not self.is_master(): + raise AttributeError('a bug is present, this must never happen') + if file_name is None: + if self.master_exists(): + self.reset() + raise ValueError('master cannot be set without specifying a duration') + return + self.set_master() + self.event_scheduler = EventScheduler() + + self.__file_name = file_name + self.__long_file_enabled = long_file_enabled + self.__stop_condition = threading.Event() + self.__lock = threading.Lock() + self.__queue = [] + self.__thread = threading.Thread(target=self.__run) + self.__thread.start() + + ''' Public methods ''' + + def log(self, log_message, long_file=True, short_file=False, timestamp=False): + if not self.master_exists(): + raise AttributeError('this method cannot be called if a master does not exist') + self.__lock.acquire() + log_message = '{} '.format(self.event_scheduler.get_current_time()) * timestamp + log_message + self.__queue.append((log_message, long_file and self.__long_file_enabled, short_file)) + self.__lock.release() + + def logline(self, log_message, *args, **kwargs): + self.log(log_message + '\n', *args, **kwargs) + + def reset(self): + if self.is_master(): + self.__stop_condition.set() + self.__thread.join() + self.__stop_condition.clear() + super(Monitor, self).reset() + + ''' Private helper methods''' + + def __run(self): + while not self.__stop_condition.is_set(): + self.__stop_condition.wait(100) + dump_long = '' + dump_short = '' + self.__lock.acquire() + for string_to_write, long_file, short_file in self.__queue: + dump_long += long_file * string_to_write + dump_short += short_file * string_to_write + self.__queue.clear() + self.__lock.release() + if self.__stop_condition.is_set(): + if self.__long_file_enabled: + dump_long += DEFAULT.SEPARATOR + '\n' + dump_short += DEFAULT.SEPARATOR + '\n' + if dump_long != '' and self.__long_file_enabled: + with io.open(self.__file_name + '_long.txt', 'a', encoding='utf-8') as f: + f.write(dump_long.encode().decode()) + if dump_short != '': + with io.open(self.__file_name + '_short.txt', 'a', encoding='utf-8') as f: + f.write(dump_short.encode().decode()) diff --git a/simu-lora/simlib/packet.py b/simu-lora/simlib/packet.py new file mode 100644 index 0000000..48adf89 --- /dev/null +++ b/simu-lora/simlib/packet.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 + +from simlib.defaults import * +from simlib.eventscheduler import * +from simlib.monitor import * + + +class Packet(object): + def __init__(self, serial, device, toa): + self.__serial = serial + self.__device = device + self.__toa = toa + self.__retransmissions = {} + self.__reset() + self.__delivery_finished = False + self.eventscheduler = EventScheduler() + self.monitor = Monitor() + + def get_serial(self): + return self.__serial + + def get_toa(self): + return self.__toa + + def get_sender(self): + return self.__device + + def get_receptions(self): + return self.__receptions + + def is_not_handled(self): + return self.__waiting + + def delivery_finished(self): + return self.__delivery_finished + + def start_transmission(self, channel): + # called by radio + assert channel is not None + assert self.__transmission is None + assert self.__receptions is None + self.__delivery_finished = False + self.__transmission = {DEFAULT.PACKET.STARTTX: self.eventscheduler.get_current_time(), + DEFAULT.PACKET.CHANNEL: channel, DEFAULT.PACKET.STOPTX: None} + self.__waiting = False + self.__receptions = {} + + def stop_transmission(self): + # called by radio + assert self.__transmission is not None + assert self.__transmission[DEFAULT.PACKET.STOPTX] is None + self.__transmission[DEFAULT.PACKET.STOPTX] = self.eventscheduler.get_current_time() + if all(value[DEFAULT.PACKET.STOPRX] is not None for value in self.__receptions.values()): + self.__delivery_finished = True + + def start_reception(self, device_id): + # called by radio + assert self.__transmission is not None + self.__receptions[device_id] = {DEFAULT.PACKET.STARTRX: self.eventscheduler.get_current_time(), + DEFAULT.PACKET.OUTCOME: None, DEFAULT.PACKET.STOPRX: None} + + def stop_reception(self, device_id, outcome): + # called by radio + assert type(outcome) == bool + assert self.__receptions is not None + assert device_id in self.__receptions + assert self.__receptions[device_id][DEFAULT.PACKET.OUTCOME] is None + assert self.__receptions[device_id][DEFAULT.PACKET.STOPRX] is None + self.__receptions[device_id][DEFAULT.PACKET.OUTCOME] = outcome + self.__receptions[device_id][DEFAULT.PACKET.STOPRX] = self.eventscheduler.get_current_time() + if (all(value[DEFAULT.PACKET.STOPRX] is not None for value in self.__receptions.values()) + and self.__transmission[DEFAULT.PACKET.STOPTX] is not None): + self.__delivery_finished = True + + def enable_retransmission(self): + # do not use this method by now + if not any(value[DEFAULT.PACKET.OUTCOME] for value in self.__receptions.values()): + nbr_retransmissions = len(self.__retransmissions) + self.__retransmissions[nbr_retransmission] = {} + self.__retransmissions[nbr_retransmission][DEFAULT.PACKET.TX] = dict(self.__transmission) + self.__retransmissions[nbr_retransmission][DEFAULT.PACKET.RX] = dict(self.__receptions) + self.__reset() + + def __reset(self): + self.__waiting = True + self.__transmission = None + self.__receptions = None diff --git a/simu-lora/simlib/radio.py b/simu-lora/simlib/radio.py new file mode 100644 index 0000000..0893ae2 --- /dev/null +++ b/simu-lora/simlib/radio.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 + +from simlib.defaults import * +from simlib.eventscheduler import * +from simlib.receptioncount import * + + +class Radio(object): + def __init__(self, channels, device, duty_cycle=DEFAULT.ENDDEVICE.duty_cycle, txonly=False): + if not isinstance(channels, tuple) or channels == (): + raise TypeError('channels must be arranged in a tuple and their number cannot be null') + self.__state = DEFAULT.RADIO.STATE.SLEEP + self.__outgoing_packet = None + self.__incoming_packet = None + self.__receptions = dict([(channel, ReceptionCount(self, channel)) for channel in channels]) + self.__txonly = txonly + self.__current_channel = None + self.__device = device + self.__duty_cycle = duty_cycle + self.__duty_cycle_active = False + self.eventscheduler = EventScheduler() + + def is_channel_available(self, channel): + return channel in self.__receptions.keys() + + def get_fixed_channel(self): + if len(self.__receptions.keys()) == 1: + return list(self.__receptions.keys())[0] + + def __set_channel(self, channel): + assert self.__current_channel is None + assert channel in self.__receptions.keys() or (len(self.__receptions) == 1 and channel is None) + self.__current_channel = channel if len(self.__receptions) > 1 else list(self.__receptions.keys())[0] + + def __unset_channel(self): + assert self.__current_channel is not None + self.__current_channel = None + + def is_sleep(self): + return self.__state == DEFAULT.RADIO.STATE.SLEEP + + def can_transmit(self): + return self.is_sleep() and not self.__duty_cycle_active + + def transmit(self, packet, channel=None): + assert self.__state == DEFAULT.RADIO.STATE.SLEEP + assert self.__outgoing_packet is None + assert not self.__duty_cycle_active + self.__outgoing_packet = packet + self.__set_channel(channel) + self.eventscheduler.schedule_event(packet.get_toa(), self.transmit_completed_cb) + packet.start_transmission(self.__current_channel) + self.__state = DEFAULT.RADIO.STATE.TRANSMITTING + + def transmit_completed_cb(self): + assert self.__state == DEFAULT.RADIO.STATE.TRANSMITTING + assert self.__outgoing_packet is not None + packet = self.__outgoing_packet + self.__outgoing_packet = None + channel = self.__current_channel + self.__unset_channel() + packet.stop_transmission() + self.__device.transmit_completed_cb(packet, channel) + self.__duty_cycle_active = True + self.eventscheduler.schedule_event(packet.get_toa() * (100 / self.__duty_cycle - 1), self.__start_availability) + self.__state = DEFAULT.RADIO.STATE.SLEEP + + def start_listening(self, channel=None): + if self.__txonly: + raise Exception('starting listening on a txonly channel should never happen') + assert self.__state == DEFAULT.RADIO.STATE.SLEEP + self.__set_channel(channel) + self.__state = (DEFAULT.RADIO.STATE.LISTENING + if self.__receptions[self.__current_channel].get() == 0 + else DEFAULT.RADIO.STATE.COLLISION) + + def stop_listening(self): + if self.__txonly: + raise Exception('stopping listening on a txonly channel should never happen') + assert self.__state not in [DEFAULT.RADIO.STATE.SLEEP, DEFAULT.RADIO.STATE.TRANSMITTING] + self.__incoming_packet = None + self.__unset_channel() + self.__state = DEFAULT.RADIO.STATE.SLEEP + + def receive(self, packet, channel): + if self.__txonly: + raise Exception('starting receiveing on a txonly channel should never happen') + self.__receptions[channel].increment(packet.get_toa()) + if self.__current_channel != channel or self.__state == DEFAULT.RADIO.STATE.TRANSMITTING: + return + device_id = self.__device.get_attributes().id_device + packet.start_reception(device_id=device_id) + if self.__state == DEFAULT.RADIO.STATE.LISTENING: + assert self.__incoming_packet is None + self.__incoming_packet = packet + self.__state = DEFAULT.RADIO.STATE.RECEIVING + else: + assert (self.__incoming_packet is not None) == (self.__state == DEFAULT.RADIO.STATE.RECEIVING) + if self.__incoming_packet is not None: + self.__incoming_packet.stop_reception(device_id=device_id, outcome=False) + self.__incoming_packet = None + packet.stop_reception(device_id=device_id, outcome=False) + self.__state = DEFAULT.RADIO.STATE.COLLISION + + def receive_completed_cb(self, channel): + if self.__txonly: + raise Exception('receiving cannot be completed on a txonly channel') + if self.__current_channel != channel or self.__state == DEFAULT.RADIO.STATE.TRANSMITTING: + return + assert self.__state != DEFAULT.RADIO.STATE.LISTENING + if self.__state == DEFAULT.RADIO.STATE.RECEIVING: + assert self.__receptions[channel].get() == 0 and self.__incoming_packet is not None + self.__incoming_packet.stop_reception(device_id=self.__device.get_attributes().id_device, outcome=True) + self.__device.receive_completed_cb(self.__incoming_packet, channel) + self.__incoming_packet = None + self.__state = DEFAULT.RADIO.STATE.LISTENING if self.__receptions[ + channel].get() == 0 else DEFAULT.RADIO.STATE.COLLISION + + def __start_availability(self): + assert self.__duty_cycle_active + self.__duty_cycle_active = False + self.__device.start_availability_cb() diff --git a/simu-lora/simlib/randomness.py b/simu-lora/simlib/randomness.py new file mode 100644 index 0000000..0e8a9c5 --- /dev/null +++ b/simu-lora/simlib/randomness.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +import numpy +import random + +from simlib.borg import * + + +class Randomness(Borg): + def init(self, seed=None): + if self.master_exists(): + if not self.is_master(): + raise AttributeError('a bug is present, this must never happen') + elif seed is None: + return + else: + self.set_master() + self.__predefined_random = random.Random(seed) + self.__predefined_numpy_random = numpy.random.RandomState(seed) + self.__runtime_random = random.Random(seed) + self.__runtime_numpy_random = numpy.random.RandomState(seed) + + def get_predefined_random(self): + if not self.master_exists(): + raise AttributeError('this method cannot be called if a master does not exist') + return self.__predefined_random + + def get_predefined_numpy_random(self): + if not self.master_exists(): + raise AttributeError('this method cannot be called if a master does not exist') + return self.__predefined_numpy_random + + def get_runtime_random(self): + if not self.master_exists(): + raise AttributeError('this method cannot be called if a master does not exist') + return self.__runtime_random + + def get_runtime_numpy_random(self): + if not self.master_exists(): + raise AttributeError('this method cannot be called if a master does not exist') + return self.__runtime_numpy_random diff --git a/simu-lora/simlib/receptioncount.py b/simu-lora/simlib/receptioncount.py new file mode 100644 index 0000000..42dcbee --- /dev/null +++ b/simu-lora/simlib/receptioncount.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +from simlib.eventscheduler import * + + +class ReceptionCount(object): + def __init__(self, radio, channel): + self.__value = 0 + self.__radio = radio + self.__channel = channel + self.eventscheduler = EventScheduler() + + def increment(self, toa): + self.__value += 1 + self.eventscheduler.schedule_event(toa, self.__decrement) + + def __decrement(self): + assert self.__value > 0 + self.__value -= 1 + self.__radio.receive_completed_cb(self.__channel) + + def get(self): + return self.__value