# Keith Briggs 2022-11-11 2.0 version
# Simulation structure:
# Sim - Scenario - MME
# |
# RIC
# |
# Cell---Cell---Cell--- ....
# | | |
# UE UE UE UE UE UE ...
__version__='2.0.0'
'''The AIMM simulator emulates a cellular radio system roughly following 5G concepts and channel models.'''
from os.path import basename
from sys import stderr,stdout,exit,version as pyversion
from math import hypot,atan2,pi as math_pi
from time import time,sleep
from collections import deque
try:
import numpy as np
except:
print('numpy not found: please do "pip install numpy"',file=stderr)
exit(1)
try:
import simpy
except:
print('simpy not found: please do "pip install simpy"',file=stderr)
exit(1)
from .NR_5G_standard_functions import SINR_to_CQI,CQI_to_64QAM_efficiency
from .UMa_pathloss_model import UMa_pathloss
def np_array_to_str(x):
' Formats a 1-axis np.array as a tab-separated string '
return np.array2string(x,separator='\t').replace('[','').replace(']','')
def _nearest_weighted_point(x,pts,w=1.0):
'''
Internal use only.
Given a point x of shape (dim,), where dim is typically 2 or 3,
an array of points pts of shape (npts,dim),
and a vector of weights w of the same length as pts,
return the index of the point minimizing w[i]*d[i],
where d[i] is the distance from x to point i.
Returns the index of the point minimizing w[i]*d[i].
For the application to cellular radio systems, we let pts be the
cell locations, and then if we set
w[i]=p[i]**(-1/alpha),
where p[i] is the transmit power of cell i, and alpha>=2 is the pathloss
exponent, then this algorithm will give us the index of the cell providing
largest received power at the point x.
'''
weighted_distances=w*np.linalg.norm(pts-x,axis=1)
imin=np.argmin(weighted_distances)
if 0: # dbg
print('x=',x)
print('pts=',pts)
print('weighted_distances=',weighted_distances)
return weighted_distances[imin],imin
def to_dB(x):
return 10.0*np.log10(x)
def from_dB(x):
return np.power(10.0,x/10.0)
[docs]class Cell:
'''
Class representing a single Cell (gNB). As instances are created, the are automatically given indices starting from 0. This index is available as the data member ``cell.i``. The variable ``Cell.i`` is always the current number of cells.
Parameters
----------
sim : Sim
Simulator instance which will manage this Cell.
interval : float
Time interval between Cell updates.
bw_MHz : float
Channel bandwidth in MHz.
n_subbands : int
Number of subbands.
xyz : [float, float, float]
Position of cell in metres, and antenna height.
h_BS : float
Antenna height in metres; only used if xyz is not provided.
power_dBm : float
Transmit power in dBm.
MIMO_gain_dB : float
Effective power gain from MIMO in dB. This is no more than a crude way to
estimate the performance gain from using MIMO. A typical value might be 3dB for 2x2 MIMO.
pattern : array or function
If an array, then a 360-element array giving the antenna gain in dB in 1-degree increments (0=east, then counterclockwise). Otherwise, a function giving the antenna gain in dB in the direction theta=(180/pi)*atan2(y,x).
f_callback :
A function with signature ``f_callback(self,kwargs)``, which will be called
at each iteration of the main loop.
verbosity : int
Level of debugging output (0=none).
'''
i=0
def __init__(s,
sim,
interval=10.0,
bw_MHz=10.0,
n_subbands=1,
xyz=None,
h_BS=20.0,
power_dBm=30.0,
MIMO_gain_dB=0.0,
pattern=None,
f_callback=None,
f_callback_kwargs={},
verbosity=0):
# default scene 1000m x 1000m, but keep cells near the centre
s.i=Cell.i; Cell.i+=1
s.sim=sim
s.interval=interval
s.bw_MHz=bw_MHz
s.n_subbands=n_subbands
s.subband_mask=np.ones(n_subbands) # dtype is float, to allow soft masking
s.rbs=simpy.Resource(s.sim.env,capacity=50)
s.power_dBm=power_dBm
s.pattern=pattern
s.f_callback=f_callback
s.f_callback_kwargs=f_callback_kwargs
s.MIMO_gain_dB=MIMO_gain_dB
s.attached=set()
s.reports={'cqi': {}, 'rsrp': {}, 'throughput_Mbps': {}}
# rsrp_history[i] will be the last 10 reports of rsrp received
# at this cell from UE[i] (no timestamps, just for getting trend)
s.rsrp_history={}
if xyz is not None:
s.xyz=np.array(xyz)
else: # random cell locations
s.xyz=np.empty(3)
s.xyz[:2]=100.0+900.0*s.sim.rng.random(2)
s.xyz[2]=h_BS
if verbosity>1: print(f'Cell[{s.i}] is at',s.xyz,file=stderr)
s.verbosity=verbosity
# every time we make a new Cell, we have to check whether
# we have a hetnet or not...
s.sim._set_hetnet()
#s.sim.env.process(s.loop()) # start Cell main loop
[docs] def set_f_callback(s,f_callback,**kwargs):
' Add a callback function to the main loop of this Cell '
s.f_callback=f_callback
s.f_callback_kwargs=kwargs
[docs] def loop(s):
'''
Main loop of Cell class. Default: do nothing.
'''
while True:
if s.f_callback is not None: s.f_callback(s,**s.f_callback_kwargs)
yield s.sim.env.timeout(s.interval)
def __repr__(s):
return f'Cell(index={s.i},xyz={s.xyz})'
[docs] def get_nattached(s):
'''
Return the current number of UEs attached to this Cell.
'''
return len(s.attached)
[docs] def get_xyz(s):
'''
Return the current position of this Cell.
'''
return s.xyz
[docs] def set_xyz(s,xyz):
'''
Set a new position for this Cell.
'''
s.xyz=np.array(xyz)
s.sim.cell_locations[s.i]=s.xyz
print(f'Cell[{s.i}] is now at {s.xyz}',file=stderr)
[docs] def get_power_dBm(s):
'''
Return the transmit power in dBm currently used by this cell.
'''
return s.power_dBm
[docs] def set_power_dBm(s,p):
'''
Set the transmit power in dBm to be used by this cell.
'''
s.power_dBm=p
s.sim._set_hetnet()
[docs] def boost_power_dBm(s,p,mn=None,mx=None):
'''
Increase or decrease (if p<0) the transmit power in dBm to be used by this cell.
If mn is not ``None``, then the power will not be set if it falls below mn.
If mx is not ``None``, then the power will not be set if it exceeds mx.
Return the new power.
'''
if p<0.0:
if mn is not None and s.power_dBm+p>=mn:
s.power_dBm+=p
return s.power_dBm
if p>0.0:
if mx is not None and s.power_dBm+p<=mx:
s.power_dBm+=p
return s.power_dBm
s.power_dBm+=p
return s.power_dBm
[docs] def get_rsrp(s,i):
'''
Return last RSRP reported to this cell by UE[i].
'''
if i in s.reports['rsrp']:
return s.reports['rsrp'][i][1]
return -np.inf # no reports
[docs] def get_rsrp_history(s,i):
'''
Return an array of the last 10 RSRP[1]s reported to this cell by UE[i].
'''
if i in s.rsrp_history:
return np.array(s.rsrp_history[i])
return -np.inf*np.ones(10) # no recorded history
[docs] def set_MIMO_gain(s,MIMO_gain_dB):
'''
Set the MIMO gain in dB to be used by this cell.
'''
s.MIMO_gain_dB=MIMO_gain_dB
[docs] def get_UE_throughput(s,ue_i): # FIXME do we want an array over subbands?
'''
Return the total current throughput in Mb/s of UE[i] in the simulation.
'''
reports=s.reports['throughput_Mbps']
if ue_i in reports: return reports[ue_i][1]
return -np.inf # [-np.inf]*s.n_subbands # special value to indicate no report
[docs] def get_UE_CQI(s,ue_i):
'''
Return the current CQI of UE[i] in the simulation, as an array across all subbands. An array of NaNs is returned if there is no report.
'''
reports=s.reports['cqi']
return reports[ue_i][1] if ue_i in reports else np.nan*np.ones(s.n_subbands)
[docs] def get_RSRP_reports(s):
'''
Return the current RSRP reports to this cell, as a list of tuples (ue.i, rsrp).
'''
reports=s.reports['rsrp']
return [(ue.i,reports[ue.i][1]) if ue.i in reports else (ue.i,-np.inf) for ue in s.sim.UEs]
[docs] def get_RSRP_reports_dict(s):
'''
Return the current RSRP reports to this cell, as a dictionary ue.i: rsrp.
'''
reports=s.reports['rsrp']
return dict((ue.i,reports[ue.i][1]) if ue.i in reports else (ue.i,-np.inf) for ue in s.sim.UEs)
[docs] def get_average_throughput(s):
'''
Return the average throughput over all UEs attached to this cell.
'''
reports,k=s.reports['throughput_Mbps'],0
ave=np.zeros(s.n_subbands)
for ue_i in reports:
k+=1
#ave+=(reports[ue_i][1][0]-ave)/k
ave+=(np.sum(reports[ue_i][1])-ave)/k
return np.sum(ave)
[docs] def set_pattern(s,pattern):
'''
Set the antenna radiation pattern.
'''
s.pattern=pattern
[docs] def set_subband_mask(s,mask):
'''
Set the subband mask to ``mask``.
'''
#print('set_subband_mask',s.subband_mask.shape,len(mask),file=stderr)
assert s.subband_mask.shape[0]==len(mask)
s.subband_mask=np.array(mask)
[docs] def get_subband_mask(s):
'''
Get the current subband mask.
'''
return s.subband_mask
def monitor_rbs(s):
while True:
if s.rbs.queue:
if s.verbosity>0: print(f'rbs at {s.sim.env.now:.2f} ={s.rbs.count}')
yield s.sim.env.timeout(5.0)
# END class Cell
[docs]class UE:
'''
Represents a single UE. As instances are created, the are automatically given indices starting from 0. This index is available as the data member ``ue.i``. The static (class-level) variable ``UE.i`` is always the current number of UEs.
Parameters
----------
sim : Sim
The Sim instance which will manage this UE.
xyz : [float, float, float]
Position of UE in metres, and antenna height.
h_UT : float
Antenna height of user terminal in metres; only used if xyz is not provided.
reporting_interval : float
Time interval between UE reports being sent to the serving cell.
f_callback :
A function with signature ``f_callback(self,kwargs)``, which will be called at each iteration of the main loop.
f_callback_kwargs :
kwargs for previous function.
pathloss_model
An instance of a pathloss model. This must be a callable object which
takes two arguments, each a 3-vector. The first represent the transmitter
location, and the second the receiver location. It must return the
pathloss in dB along this signal path.
If set to ``None`` (the default), a standard urban macrocell model
is used.
See further ``NR_5G_standard_functions_00.py``.
'''
i=0
def __init__(s,sim,xyz=None,reporting_interval=1.0,pathloss_model=None,h_UT=2.0,f_callback=None,f_callback_kwargs={},verbosity=0):
s.sim=sim
s.i=UE.i; UE.i+=1
s.serving_cell=None
s.f_callback=f_callback
s.f_callback_kwargs=f_callback_kwargs
# next will be a record of last 10 serving cell ids,
# with time of last attachment.
# 0=>current, 1=>previous, etc. -1 => not valid)
# This is for use in handover algorithms
s.serving_cell_ids=deque([(-1,None)]*10,maxlen=10)
s.reporting_interval=reporting_interval
if xyz is not None:
s.xyz=np.array(xyz,dtype=np.float)
else:
s.xyz=250.0+500.0*s.sim.rng.random(3)
s.xyz[2]=h_UT
if verbosity>1: print(f'UE[{s.i}] is at',s.xyz,file=stderr)
# We assume here that the UMa_pathloss model needs to be instantiated,
# but other user-provided models are already instantiated,
# and provide callable objects...
if pathloss_model is None:
s.pathloss=UMa_pathloss(fc_GHz=s.sim.params['fc_GHz'],h_UT=s.sim.params['h_UT'],h_BS=s.sim.params['h_BS'])
if verbosity>1: print(f'Using 5G standard urban macrocell pathloss model.',file=stderr)
else:
s.pathloss=pathloss_model
if s.pathloss.__doc__ is not None:
if verbosity>1: print(f'Using user-specified pathloss model "{s.pathloss.__doc__}".',file=stderr)
else:
print(f'Using user-specified pathloss model.',file=stderr)
s.verbosity=verbosity
s.noise_power_dBm=-140.0
s.cqi=None
# Keith Briggs 2022-10-12 loops now started in Sim.__init__
#s.sim.env.process(s.run_subband_cqi_report())
#s.sim.env.process(s.loop()) # this does reports to all cells
def __repr__(s):
return f'UE(index={s.i},xyz={s.xyz},serving_cell={s.serving_cell})'
[docs] def set_f_callback(s,f_callback,**kwargs):
' Add a callack function to the main loop of this UE '
s.f_callback=f_callback
s.f_callback_kwargs=kwargs
[docs] def loop(s):
' Main loop of UE class '
if s.verbosity>1:
print(f'Main loop of UE[{s.i}] started')
stdout.flush()
while True:
if s.f_callback is not None: s.f_callback(s,**s.f_callback_kwargs)
s.send_rsrp_reports()
s.send_subband_cqi_report() # FIXME merge these two reports
#print(f'dbg: Main loop of UE class started'); exit()
yield s.sim.env.timeout(s.reporting_interval)
[docs] def get_serving_cell(s):
'''
Return the current serving Cell object (not index) for this UE instance.
'''
ss=s.serving_cell
if ss is None: return None
return s.serving_cell
[docs] def get_serving_cell_i(s):
'''
Return the current serving Cell index for this UE instance.
'''
ss=s.serving_cell
if ss is None: return None
return s.serving_cell.i
[docs] def get_xyz(s):
'''
Return the current position of this UE.
'''
return s.xyz
[docs] def set_xyz(s,xyz,verbose=False):
'''
Set a new position for this UE.
'''
s.xyz=np.array(xyz)
if verbose: print(f'UE[{s.i}] is now at {s.xyz}',file=stderr)
[docs] def attach(s,cell,quiet=True):
'''
Attach this UE to a specific Cell instance.
'''
cell.attached.add(s.i)
s.serving_cell=cell
s.serving_cell_ids.appendleft((cell.i,s.sim.env.now,))
if not quiet and s.verbosity>0:
print(f'UE[{s.i:2}] is attached to cell[{cell.i}]',file=stderr)
[docs] def detach(s,quiet=True):
'''
Detach this UE from its serving cell.
'''
if s.serving_cell is None: # Keith Briggs 2022-08-08 added None test
return
s.serving_cell.attached.remove(s.i)
# clear saved reports from this UE...
reports=s.serving_cell.reports
for x in reports:
if s.i in reports[x]: del reports[x][s.i]
if not quiet and s.verbosity>0:
print(f'UE[{s.i}] detached from cell[{s.serving_cell.i}]',file=stderr)
s.serving_cell=None
[docs] def attach_to_strongest_cell_simple_pathloss_model(s):
'''
Attach to the cell delivering the strongest signal
at the current UE position. Intended for initial attachment only.
Uses only a simple power-law pathloss model. For proper handover
behaviour, use the MME module.
'''
celli=s.sim.get_strongest_cell_simple_pathloss_model(s.xyz)
s.serving_cell=s.sim.cells[celli]
s.serving_cell.attached.add(s.i)
if s.verbosity>0:
print(f'UE[{s.i:2}] ⟵⟶ cell[{celli}]',file=stderr)
[docs] def attach_to_nearest_cell(s):
'''
Attach this UE to the geographically nearest Cell instance.
Intended for initial attachment only.
'''
dmin,celli=_nearest_weighted_point(s.xyz[:2],s.sim.cell_locations[:,:2])
if 0: # dbg
print(f'_nearest_weighted_point: celli={celli} dmin={dmin:.2f}')
for cell in s.sim.cells:
d=np.linalg.norm(cell.xyz-s.xyz)
print(f'Cell[{cell.i}] is at distance {d:.2f}')
s.serving_cell=s.sim.cells[celli]
s.serving_cell.attached.add(s.i)
if s.verbosity>0:
print(f'UE[{s.i:2}] ⟵⟶ cell[{celli}]',file=stderr)
[docs] def get_CQI(s):
'''
Return the current CQI of this UE, as an array across all subbands.
'''
return s.cqi
[docs] def send_rsrp_reports(s,threshold=-120.0):
'''
Send RSRP reports in dBm to all cells for which it is over the threshold.
Subbands not handled.
'''
# antenna pattern computation added Keith Briggs 2021-11-24.
for cell in s.sim.cells:
pl_dB=s.pathloss(cell.xyz,s.xyz) # 2021-10-29
antenna_gain_dB=0.0
if cell.pattern is not None:
vector=s.xyz-cell.xyz # vector pointing from cell to UE
angle_degrees=(180.0/math_pi)*atan2(vector[1],vector[0])
antenna_gain_dB=cell.pattern(angle_degrees) if callable(cell.pattern) \
else cell.pattern[int(angle_degrees)%360]
rsrp_dBm=cell.power_dBm+antenna_gain_dB+cell.MIMO_gain_dB-pl_dB
rsrp=from_dB(rsrp_dBm)
if rsrp_dBm>threshold:
cell.reports['rsrp'][s.i]=(s.sim.env.now,rsrp_dBm)
if s.i not in cell.rsrp_history:
cell.rsrp_history[s.i]=deque([-np.inf,]*10,maxlen=10)
cell.rsrp_history[s.i].appendleft(rsrp_dBm)
[docs] def send_subband_cqi_report(s):
'''
For this UE, send an array of CQI reports, one for each subband; and a total throughput report, to the serving cell.
What is sent is a 2-tuple (current time, array of reports).
For RSRP reports, use the function ``send_rsrp_reports``.
Also save the CQI[1]s in s.cqi, and return the throughput value.
'''
if s.serving_cell is None: return 0.0 # 2022-08-08 detached
interference=from_dB(s.noise_power_dBm)*np.ones(s.serving_cell.n_subbands)
for cell in s.sim.cells:
pl_dB=s.pathloss(cell.xyz,s.xyz)
antenna_gain_dB=0.0
if cell.pattern is not None:
vector=s.xyz-cell.xyz # vector pointing from cell to UE
angle_degrees=(180.0/math_pi)*atan2(vector[1],vector[0])
antenna_gain_dB=cell.pattern(angle_degrees) if callable(cell.pattern) \
else cell.pattern[int(angle_degrees)%360]
if cell.i==s.serving_cell.i: # wanted signal
rsrp_dBm=cell.MIMO_gain_dB+antenna_gain_dB+cell.power_dBm-pl_dB
else: # unwanted interference
received_interference_power=antenna_gain_dB+cell.power_dBm-pl_dB
interference+=from_dB(received_interference_power)*cell.subband_mask
rsrp=from_dB(rsrp_dBm)
sinr_dB=to_dB(rsrp/interference) # scalar/array
s.cqi=cqi=SINR_to_CQI(sinr_dB)
spectral_efficiency=np.array([CQI_to_64QAM_efficiency(cqi_i) for cqi_i in cqi])
#print(spectral_efficiency,file=stderr)
now=float(s.sim.env.now)
# per-UE throughput...
throughput_Mbps=s.serving_cell.bw_MHz*(spectral_efficiency@s.serving_cell.subband_mask)/s.serving_cell.n_subbands/len(s.serving_cell.attached)
s.serving_cell.reports['cqi'][s.i]=(now,cqi)
#print(f'UE={s.i} cqi report sent: {cqi}',file=stderr)
s.serving_cell.reports['throughput_Mbps'][s.i]=(now,throughput_Mbps,)
return throughput_Mbps
def run_subband_cqi_report(s): # FIXME merge this with rsrp reporting
while True:
#if s.serving_cell is not None: # UE must be attached 2022-08-08
s.send_subband_cqi_report()
yield s.sim.env.timeout(s.reporting_interval)
# END class UE
[docs]class Sim:
'''
Class representing the complete simulation.
Parameters
----------
params : dict
A dictionary of additional global parameters which need to be accessible to downstream functions. In the instance, these parameters will be available as ``sim.params``. If ``params['profile']`` is set to a non-empty string, then a code profile will be performed and the results saved to the filename given by the string. There will be some execution time overhead when profiling.
'''
def __init__(s,params={'fc_GHz':3.5,'h_UT':2.0,'h_BS':20.0},show_params=True,rng_seed=0):
s.__version__=__version__
s.params=params
# set default values for operating frequenct, user terminal height, and
# base station height...
if 'fc_GHz' not in params: params['fc_GHz']=3.5
if 'h_UT' not in params: params['h_UT']=2.0
if 'h_BS' not in params: params['h_BS']=20.0
s.env=simpy.Environment()
s.rng=np.random.default_rng(rng_seed)
s.loggers=[]
s.scenario=None
s.ric=None
s.mme=None
s.hetnet=None # unknown at this point; will be set to True or False
s.cells=[]
s.UEs=[]
s.events=[]
s.cell_locations=np.empty((0,3))
np.set_printoptions(precision=2,linewidth=200)
pyv=pyversion.replace('\n','') #[:pyversion.index('(default')]
print(f'python version={pyv}',file=stderr)
print(f'numpy version={np.__version__}',file=stderr)
print(f'simpy version={simpy.__version__}',file=stderr)
print(f'AIMM simulator version={s.__version__}',file=stderr)
if show_params:
print(f'Simulation parameters:',file=stderr)
for param in s.params:
print(f" {param}={s.params[param]}",file=stderr)
def _set_hetnet(s):
# internal function only - decide whether we have a hetnet
powers=set(cell.get_power_dBm() for cell in s.cells)
s.hetnet=len(powers)>1 # powers are not all equal
[docs] def wait(s,interval=1.0):
'''
Convenience function to avoid low-level reference to env.timeout().
``loop`` functions in each class must yield this.
'''
return s.env.timeout(interval)
[docs] def make_cell(s,**kwargs):
'''
Convenience function: make a new Cell instance and add it to the simulation; parameters as for the Cell class. Return the new Cell instance. It is assumed that Cells never move after being created (i.e. the initial xyz[1] stays the same throughout the simulation).
'''
s.cells.append(Cell(s,**kwargs))
xyz=s.cells[-1].get_xyz()
s.cell_locations=np.vstack([s.cell_locations,xyz])
return s.cells[-1]
[docs] def make_UE(s,**kwargs):
'''
Convenience function: make a new UE instance and add it to the simulation; parameters as for the UE class. Return the new UE instance.
'''
s.UEs.append(UE(s,**kwargs))
return s.UEs[-1]
[docs] def get_ncells(s):
'''
Return the current number of cells in the simulation.
'''
return len(s.cells)
[docs] def get_nues(s):
'''
Return the current number of UEs in the simulation.
'''
return len(s.UEs)
[docs] def get_UE_position(s,ue_i):
'''
Return the xyz position of UE[i] in the simulation.
'''
return s.UEs[ue_i].xyz
[docs] def get_average_throughput(s):
'''
Return the average throughput over all UEs attached to all cells.
'''
ave,k=0.0,0
for cell in s.cells:
k+=1
ave+=(cell.get_average_throughput()-ave)/k
return ave
[docs] def add_logger(s,logger):
'''
Add a logger to the simulation.
'''
assert isinstance(logger,Logger)
s.loggers.append(logger)
[docs] def add_loggers(s,loggers):
'''
Add a sequence of loggers to the simulation.
'''
for logger in loggers:
assert isinstance(logger,Logger)
s.loggers.append(logger)
[docs] def add_scenario(s,scenario):
'''
Add a Scenario instance to the simulation.
'''
assert isinstance(scenario,Scenario)
s.scenario=scenario
[docs] def add_ric(s,ric):
'''
Add a RIC instance to the simulation.
'''
assert isinstance(ric,RIC)
s.ric=ric
[docs] def add_MME(s,mme):
'''
Add an MME instance to the simulation.
'''
assert isinstance(mme,MME)
s.mme=mme
def add_event(s,event):
s.events.append(event)
def get_serving_cell(s,ue_i):
if ue_i<len(s.UEs): return s.UEs[ue_i].serving_cell
return None
def get_serving_cell_i(s,ue_i):
if ue_i<len(s.UEs): return s.UEs[ue_i].serving_cell.i
return None
[docs] def get_nearest_cell(s,xy):
'''
Return the index of the geographical nearest cell (in 2 dimensions)
to the point xy.
'''
return _nearest_weighted_point(xy[:2],s.cell_locations[:,:2],w=1.0)[1]
[docs] def get_strongest_cell_simple_pathloss_model(s,xyz,alpha=3.5):
'''
Return the index of the cell delivering the strongest signal
at the point xyz (in 3 dimensions), with pathloss exponent alpha.
Note: antenna pattern is not used, so this function is deprecated,
but is adequate for initial UE attachment.
'''
p=np.array([from_dB(cell.get_power_dBm()) for cell in s.cells])
return _nearest_weighted_point(xyz,s.cell_locations,w=p**(-1.0/alpha))[1]
[docs] def get_best_rsrp_cell(s,ue_i,dbg=False):
'''
Return the index of the cell delivering the highest RSRP at UE[i].
Relies on UE reports, and ``None`` is returned if there are not enough
reports (yet) to determine the desired output.
'''
k,best_rsrp=None,-np.inf
cell_rsrp_reports=dict((cell.i,cell.reports['rsrp']) for cell in s.cells)
for cell in s.cells:
if ue_i not in cell_rsrp_reports[cell.i]: continue # no reports for this UE
time,rsrp=cell_rsrp_reports[cell.i][ue_i] # (time, subband reports)
if dbg: print(f"get_best_rsrp_cell at {float(s.env.now):.0f}: cell={cell.i} UE={ue_i} rsrp=",rsrp,file=stderr)
ave_rsrp=np.average(rsrp) # average RSRP over subbands
if ave_rsrp>best_rsrp: k,best_rsrp=cell.i,ave_rsrp
return k
def _start_loops(s):
# internal use only - start all main loops
for logger in s.loggers:
s.env.process(logger.loop())
if s.scenario is not None:
s.env.process(s.scenario.loop())
if s.ric is not None:
s.env.process(s.ric.loop())
if s.mme is not None:
s.env.process(s.mme.loop())
for event in s.events: # TODO ?
s.env.process(event)
for cell in s.cells: # 2022-10-12 start Cells
s.env.process(cell.loop())
for ue in s.UEs: # 2022-10-12 start UEs
#print(f'About to start main loop of UE[{ue.i}]..')
s.env.process(ue.loop())
#s.env.process(UE.run_subband_cqi_report())
#sleep(2); exit()
def run(s,until):
s._set_hetnet()
s.until=until
print(f'Sim: starting run for simulation time {until} seconds...',file=stderr)
s._start_loops()
t0=time()
if 'profile' in s.params and s.params['profile']:
# https://docs.python.org/3.6/library/profile.html
# to keep python 3.6 compatibility, we don't use all the
# features for profiling added in 3.8 or 3.9.
profile_filename=s.params['profile']
print(f'profiling enabled: output file will be {profile_filename}.',file=stderr)
import cProfile,pstats,io
pr=cProfile.Profile()
pr.enable()
s.env.run(until=until) # this is what is profiled
pr.disable()
strm=io.StringIO()
ps=pstats.Stats(pr,stream=strm).sort_stats('tottime')
ps.print_stats()
tbl=strm.getvalue().split('\n')
profile_file=open(profile_filename,'w')
for line in tbl[:50]: print(line,file=profile_file)
profile_file.close()
print(f'profile written to {profile_filename}.',file=stderr)
else:
s.env.run(until=until)
print(f'Sim: finished main loop in {(time()-t0):.2f} seconds.',file=stderr)
#print(f'Sim: hetnet={s.hetnet}.',file=stderr)
if s.mme is not None:
s.mme.finalize()
if s.ric is not None:
s.ric.finalize()
for logger in s.loggers:
logger.finalize()
# END class Sim
[docs]class Scenario:
'''
Base class for a simulation scenario. The default does nothing.
Parameters
----------
sim : Sim
Simulator instance which will manage this Scenario.
func : function
Function called to perform actions.
interval : float
Time interval between actions.
verbosity : int
Level of debugging output (0=none).
'''
def __init__(s,sim,func=None,interval=1.0,verbosity=0):
s.sim=sim
s.func=func
s.verbosity=verbosity
s.interval=interval
[docs] def loop(s):
'''
Main loop of Scenario class. Should be overridden to provide different functionalities.
'''
while True:
if s.func is not None: s.func(s.sim)
yield s.sim.env.timeout(s.interval)
# END class Scenario
[docs]class Logger:
'''
Represents a simulation logger. Multiple loggers (each with their own file) can be used if desired.
Parameters
----------
sim : Sim
The Sim instance which will manage this Logger.
func : function
Function called to perform logginf action.
header : str
Arbitrary text to write to the top of the logfile.
f : file object
An open file object which will be written or appended to.
logging_interval : float
Time interval between logging actions.
'''
def __init__(s,sim,func=None,header='',f=stdout,logging_interval=10,np_array_to_str=np_array_to_str):
s.sim=sim
s.func=s.default_logger if func is None else func
s.f=f
s.np_array_to_str=np_array_to_str
s.logging_interval=float(logging_interval)
if header: s.f.write(header)
def default_logger(s,f=stdout):
for cell in s.sim.cells:
for ue_i in cell.reports['cqi']:
rep=cell.reports['cqi'][ue_i]
if rep is None: continue
cqi=s.np_array_to_str(rep[1])
f.write(f'{cell.i}\t{ue_i}\t{cqi}\n')
[docs] def loop(s):
'''
Main loop of Logger class.
Can be overridden to provide custom functionality.
'''
while True:
s.func(f=s.f)
yield s.sim.env.timeout(s.logging_interval)
[docs] def finalize(s):
'''
Function called at end of simulation, to implement any required finalization actions.
'''
pass
# END class Logger
[docs]class MME:
'''
Represents a MME, for handling UE handovers.
Parameters
----------
sim : Sim
Sim instance which will manage this Scenario.
interval : float
Time interval between checks for handover actions.
verbosity : int
Level of debugging output (0=none).
strategy : str
Handover strategy; possible values are ``strongest_cell_simple_pathloss_model`` (default), or ``best_rsrp_cell``.
anti_pingpong : float
If greater than zero, then a handover pattern x->y->x between cells x and y is not allowed within this number of seconds. Default is 0.0, meaning pingponging is not suppressed.
'''
def __init__(s,sim,interval=10.0,strategy='strongest_cell_simple_pathloss_model',anti_pingpong=30.0,verbosity=0):
s.sim=sim
s.interval=interval
s.strategy=strategy
s.anti_pingpong=anti_pingpong
s.verbosity=verbosity
print(f'MME: using handover strategy {s.strategy}.',file=stderr)
[docs] def do_handovers(s):
'''
Check whether handovers are required, and do them if so.
Normally called from loop(), but can be called manually if required.
'''
for ue in s.sim.UEs:
if ue.serving_cell is None: continue # no handover needed for this UE. 2022-08-08 added None test
oldcelli=ue.serving_cell.i # 2022-08-26
CQI_before=ue.serving_cell.get_UE_CQI(ue.i)
previous,tm=ue.serving_cell_ids[1]
if s.strategy=='strongest_cell_simple_pathloss_model':
celli=s.sim.get_strongest_cell_simple_pathloss_model(ue.xyz)
elif s.strategy=='best_rsrp_cell':
celli=s.sim.get_best_rsrp_cell(ue.i)
if celli is None:
celli=s.sim.get_strongest_cell_simple_pathloss_model(ue.xyz)
else:
print(f'MME.loop: strategy {s.strategy} not implemented, quitting!',file=stderr)
exit()
if celli==ue.serving_cell.i: continue
if s.anti_pingpong>0.0 and previous==celli:
if s.sim.env.now-tm<s.anti_pingpong:
if s.verbosity>2:
print(f't={float(s.sim.env.now):8.2f} handover of UE[{ue.i}] suppressed by anti_pingpong heuristic.',file=stderr)
continue # not enough time since we were last on this cell
ue.detach(quiet=True)
ue.attach(s.sim.cells[celli])
ue.send_rsrp_reports() # make sure we have reports immediately
ue.send_subband_cqi_report()
if s.verbosity>1:
CQI_after=ue.serving_cell.get_UE_CQI(ue.i)
print(f't={float(s.sim.env.now):8.2f} handover of UE[{ue.i:3}] from Cell[{oldcelli:3}] to Cell[{ue.serving_cell.i:3}]',file=stderr,end=' ')
print(f'CQI change {CQI_before} -> {CQI_after}',file=stderr)
[docs] def loop(s):
'''
Main loop of MME.
'''
yield s.sim.env.timeout(0.5*s.interval) # stagger the intervals
print(f'MME started at {float(s.sim.env.now):.2f}, using strategy="{s.strategy}" and anti_pingpong={s.anti_pingpong:.0f}.',file=stderr)
while True:
s.do_handovers()
yield s.sim.env.timeout(s.interval)
[docs] def finalize(s):
'''
Function called at end of simulation, to implement any required finalization actions.
'''
pass
# END class MME
[docs]class RIC:
'''
Base class for a RIC, for hosting xApps. The default does nothing.
Parameters
----------
sim : Sim
Simulator instance which will manage this Scenario.
interval : float
Time interval between RIC actions.
verbosity : int
Level of debugging output (0=none).
'''
def __init__(s,sim,interval=10,verbosity=0):
s.sim=sim
s.interval=interval
s.verbosity=verbosity
[docs] def finalize(s):
'''
Function called at end of simulation, to implement any required finalization actions.
'''
pass
[docs] def loop(s):
'''
Main loop of RIC class. Must be overridden to provide functionality.
'''
print(f'RIC started at {float(s.sim.env.now):.2}.',file=stderr)
while True:
yield s.sim.env.timeout(s.interval)
# END class RIC
if __name__=='__main__': # a simple self-test
np.set_printoptions(precision=4,linewidth=200)
class MyLogger(Logger):
def loop(s):
while True:
for cell in s.sim.cells:
if cell.i!=0: continue # cell[0] only
for ue_i in cell.reports['cqi']:
if ue_i!=0: continue # UE[0] only
rep=cell.reports['cqi'][ue_i]
if not rep: continue
xy= s.np_array_to_str(s.sim.UEs[ue_i].xyz[:2])
cqi=s.np_array_to_str(cell.reports['cqi'][ue_i][1])
tp= s.np_array_to_str(cell.reports['throughput_Mbps'][ue_i][1])
s.f.write(f'{s.sim.env.now:.1f}\t{xy}\t{cqi}\t{tp}\n')
yield s.sim.env.timeout(s.logging_interval)
def test_01(ncells=4,nues=9,n_subbands=2,until=1000.0):
sim=Sim()
for i in range(ncells):
sim.make_cell(n_subbands=n_subbands,MIMO_gain_dB=3.0,verbosity=0)
sim.cells[0].set_xyz((500.0,500.0,20.0)) # fix cell[0]
for i in range(nues):
ue=sim.make_UE(verbosity=1)
if 0==i: # force ue[0] to attach to cell[0]
ue.set_xyz([501.0,502.0,2.0],verbose=True)
ue.attach_to_nearest_cell()
scenario=Scenario(sim,verbosity=0)
logger=MyLogger(sim,logging_interval=1.0)
ric=RIC(sim)
sim.add_logger(logger)
sim.add_scenario(scenario)
sim.add_ric(ric)
sim.run(until=until)
test_01()