Added simu-lora practice work.

This commit is contained in:
Yohan Boujon 2025-01-30 14:44:59 +01:00
parent 731a0e2389
commit 57d7a303b1
24 changed files with 2233 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
# Ignoring python cache
**/__pycache__/**

1
simu-lora/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
lorawan-sim-outputs/**

43
simu-lora/README.md Normal file
View file

@ -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

View file

@ -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
...

View file

@ -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()

View file

@ -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()

View file

@ -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

View file

94
simu-lora/simlib/borg.py Normal file
View file

@ -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()

View file

@ -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)

View file

@ -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 = '.'

View file

@ -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

184
simu-lora/simlib/device.py Normal file
View file

@ -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

View file

@ -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

View file

@ -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))

View file

@ -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)

View file

@ -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)

View file

@ -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)

225
simu-lora/simlib/models.py Normal file
View file

@ -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

View file

@ -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())

View file

@ -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

122
simu-lora/simlib/radio.py Normal file
View file

@ -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()

View file

@ -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

View file

@ -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