Added simu-lora
practice work.
This commit is contained in:
parent
731a0e2389
commit
57d7a303b1
24 changed files with 2233 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# Ignoring python cache
|
||||||
|
**/__pycache__/**
|
1
simu-lora/.gitignore
vendored
Normal file
1
simu-lora/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
lorawan-sim-outputs/**
|
43
simu-lora/README.md
Normal file
43
simu-lora/README.md
Normal 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
|
54
simu-lora/config/config.yaml
Normal file
54
simu-lora/config/config.yaml
Normal 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
|
||||||
|
...
|
177
simu-lora/lorawan-sim-campaign-post.py
Executable file
177
simu-lora/lorawan-sim-campaign-post.py
Executable 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()
|
79
simu-lora/lorawan-sim-campaign.py
Executable file
79
simu-lora/lorawan-sim-campaign.py
Executable 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()
|
8
simu-lora/requirements.txt
Normal file
8
simu-lora/requirements.txt
Normal 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
|
0
simu-lora/simlib/__init__.py
Normal file
0
simu-lora/simlib/__init__.py
Normal file
94
simu-lora/simlib/borg.py
Normal file
94
simu-lora/simlib/borg.py
Normal 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()
|
31
simu-lora/simlib/buffer.py
Normal file
31
simu-lora/simlib/buffer.py
Normal 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)
|
112
simu-lora/simlib/defaults.py
Normal file
112
simu-lora/simlib/defaults.py
Normal 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 = '.'
|
563
simu-lora/simlib/deployment.py
Normal file
563
simu-lora/simlib/deployment.py
Normal 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
184
simu-lora/simlib/device.py
Normal 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
|
69
simu-lora/simlib/eventscheduler.py
Normal file
69
simu-lora/simlib/eventscheduler.py
Normal 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
|
74
simu-lora/simlib/experiment.py
Normal file
74
simu-lora/simlib/experiment.py
Normal 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))
|
26
simu-lora/simlib/gridfactory.py
Normal file
26
simu-lora/simlib/gridfactory.py
Normal 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)
|
115
simu-lora/simlib/lora/loraenddevice.py
Normal file
115
simu-lora/simlib/lora/loraenddevice.py
Normal 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)
|
28
simu-lora/simlib/lora/loragateway.py
Normal file
28
simu-lora/simlib/lora/loragateway.py
Normal 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
225
simu-lora/simlib/models.py
Normal 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
|
75
simu-lora/simlib/monitor.py
Normal file
75
simu-lora/simlib/monitor.py
Normal 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())
|
87
simu-lora/simlib/packet.py
Normal file
87
simu-lora/simlib/packet.py
Normal 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
122
simu-lora/simlib/radio.py
Normal 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()
|
41
simu-lora/simlib/randomness.py
Normal file
41
simu-lora/simlib/randomness.py
Normal 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
|
23
simu-lora/simlib/receptioncount.py
Normal file
23
simu-lora/simlib/receptioncount.py
Normal 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
|
Loading…
Add table
Reference in a new issue