AIMM simulator documentation

Last modified: 2021-06-10T09:53

Purpose

The AIMM simulator emulates a cellular radio system roughly following 5G concepts and channel models. The intention is to have an easy-to-use and fast system written in pure Python with minimal dependencies. It is especially designed to be suitable for interfacing to AI engines such as tensorflow or pytorch, and it is not a principal aim for it to be extremely accurate at the level of the radio channel.

Software prerequisites

  1. Python 3.8 or higher.

  2. NumPy. This normally comes with python distributions.

  3. Simpy. This will need to be installed, e.g. with pip install simpy.

  4. If real-time plotting is needed, matplotlib.

Installation

Place the files AIMM_simulator_core.py, NR_5G_standard_functions_00.py, and UMa_pathloss_model_00.py somewhere in your python path. Place realtime_plotter_03.py somewhere in your shell path.

Quick start

The following example will test the installation and introduce the basic concepts. This creates a simulator instance sim, creates one cell, creates one UE and immediately attaches it to the cell, and runs for 100 seconds of simulated time (typically about 0.03 seconds of run time). There is no logger defined, which means there will be no output apart from a few set-up messages (which are sent to stderr). The code is in AIMM_simulator_example_n0.py.

from AIMM_simulator_core import Sim
sim=Sim()
sim.make_cell()
sim.make_UE().attach_to_nearest_cell()
sim.run(until=100)

The basic steps to build and run a simulation are:

  1. Create a Sim instance.

  2. Create one or more cells with make_cell(). Cells are given a unique index, starting from 0.

  3. Create one or more UEs with make_UE(). UEs are given a unique index, starting from 0.

  4. Attach UEs with the method attach_to_nearest_cell().

  5. Create a Scenario, which typically moves the UEs according to some mobility model, but in general can include any events which affect the network.

  6. Create one or more instances of Logger.

  7. Optionally create a RIC, possibly linking to an AI engine.

  8. Start the simulation with sim.run().

  9. Plot or analyse the results in the logfiles.

The AIMM simulator uses a discrete event simulation framework. Internally, a queue of pending events is maintained, but this is invisible to the programmer. All functions and classes have default arguments appropriate to the simulation of a 5G macrocell deployment at 3.5GHz. This means that setting up a simple simulation is almost trivial, but also means that care is needed to set parameters correctly for other scenarios. Subbanding is implemented on all Cell objects, but the number of subbands may be set to 1, effectively switching off this feature.

The AIMM simulator normally operates without a graphical user interface, and just writes logfiles for subsequent analysis. The default logfile format is tab-separated columns, with purely numerical data. These files can then be easily processed with shell utilities such as cut, head, tail, etc., or read into python or R scripts, or, if all else fails, even imported into spreadsheet programs. However, a custom logger can create a logfile in any desired format.

AIMM simulator blocks

AIMM simulator block diagram

AIMM simulator block structure.

Tutorial examples

Example 1

This example (the code is in AIMM_simulator_example_n1.py) creates a simulator instance, creates one cell, creates four UEs and immediately attaches them to the cell, adds a default logger, and runs for 100 seconds of simulated time. UEs by default are placed randomly in a 1km square.

1
2
3
4
5
6
from AIMM_simulator_core import Sim,Logger
sim=Sim()
cell=sim.make_cell()
ues=[sim.make_UE().attach(cell) for i in range(4)]
sim.add_logger(Logger(sim,logging_interval=10))
sim.run(until=100)

Typical output follows. The locations are 3-dimensional, with the z component being the antenna height. The default logger prints 3 columns to stdout, with a row for each UE report: cell index, UE index, CQI value. We will see later how to create a custom logger.

Cell[0] is at [434.44 591.64  20.  ]
UE[0]   is at [602.14 403.7    2.  ]
UE[1]   is at [263.87 301.28   2.  ]
UE[2]   is at [319.12 506.63   2.  ]
UE[3]   is at [370.7  394.92   2.  ]
KDTree built.
Sim: starting main loop for simulation time 100 seconds...
0     0       15
0     1       15
0     2       15
0     3       15
...
Sim: finished main loop in 0.04 seconds.

Example 2 - adding a scenario

A scenario is created by subclassing the Scenario class. The code is in AIMM_simulator_example_n2.py. The subclass must implement the loop method as in the example: it must have an infinite loop, yielding an s.sim.wait() object, which determines the time to the next event; in this case the next change to the UE positions. In this example, an MME is also added. This handles UE handovers, and ensures that UEs are always attached to the nearest cell.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from AIMM_simulator_core import Sim,Logger,MME,Scenario
from numpy.random import standard_normal

class MyScenario(Scenario):
  def loop(self,interval=0.1):
    while True:
      for ue in self.sim.UEs: ue.xyz[:2]+=standard_normal(2)
      yield self.sim.wait(interval)

def example_n2():
  sim=Sim()
  cell=sim.make_cell()
  for i in range(4): sim.make_UE().attach(cell)
  sim.add_logger(Logger(sim,logging_interval=10))
  sim.add_scenario(MyScenario(sim))
  sim.add_MME(MME(sim,interval=10.0))
  sim.run(until=500)

example_n2()

Example 3 - adding a custom logger

There are two ways to create a custom logger. The simplest way is to specify a function when creating the Logger instance. This function must accept two arguments, the Sim instance and the file object to write to. Example code for this method is in AIMM_simulator_example_n3a.py. The custom logger must format a line of output, and write it to the file object f. Access to all simulation variables is possible; see the API documentation below. A convenience function np_array_to_str is available, which removes square brackets and commas from normal numpy array formatting.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from AIMM_simulator_core import Sim,Logger,MME,Scenario,np_array_to_str
from numpy.random import standard_normal,seed

def scenario_func(sim):
  for ue in sim.UEs: ue.xyz[:2]+=standard_normal(2)

def example_n3a():
  def logger_func(f):
    for cell in sim.cells:
      for ue_i in cell.reports['cqi']:
        xy=sim.get_UE_position(ue_i)[:2]
        tp=np_array_to_str(cell.get_UE_throughput(ue_i))
        f.write(f'{sim.env.now:.1f}\t{cell.i}\t{ue_i}\t{xy[0]:.0f}\t{xy[1]:.0f}\t{tp}\n')
  sim=Sim()
  for i in range(4): sim.make_cell()
  for i in range(8): sim.make_UE().attach_to_nearest_cell()
  sim.add_logger(Logger(sim,func=logger_func,header='#time\tcell\tUE\tx\ty\tthroughput Mb/s\n',logging_interval=1))
  sim.add_scenario(Scenario(sim,func=scenario_func))
  sim.add_MME(MME(sim,interval=10.0))
  sim.run(until=10)

seed(1)
example_n3a()

More generally, a custom logger can be created by subclassing the Logger class. The subclass must implement the loop method as in the example: it must have an infinite loop, yielding an s.sim.wait() object, which determines the time to the next write to the logfile (which defaults to stdout). The custom logger must format a line of output, and write it to the file object self.f. Example code for this method is in AIMM_simulator_example_n3.py.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from AIMM_simulator_core import Sim,Scenario,Logger,MME,np_array_to_str
from numpy.random import standard_normal,seed

class MyScenario(Scenario):
  def loop(self,interval=0.1):
    while True:
      for ue in self.sim.UEs: ue.xyz[:2]+=standard_normal(2)
      yield self.sim.wait(interval)

class MyLogger(Logger):
  def loop(self):
    self.f.write('#time\tcell\tUE\tx\ty\tthroughput\n')
    while True:
      for cell in self.sim.cells:
        for ue_i in cell.reports['cqi']:
          xy=self.sim.get_UE_position(ue_i)[:2]
          tp=np_array_to_str(cell.get_UE_throughput(ue_i))
          self.f.write(f't={self.sim.env.now:.1f}\tcell={cell.i}\tUE={ue_i}\tx={xy[0]:.0f}\ty={xy[1]:.0f}\ttp={tp}Mb/s\n')
      yield self.sim.wait(self.logging_interval)

def example_n3():
  sim=Sim()
  for i in range(4): sim.make_cell()
  for i in range(8): sim.make_UE().attach_to_nearest_cell()
  sim.add_logger(MyLogger(sim,logging_interval=1))
  sim.add_scenario(MyScenario(sim))
  sim.add_MME(MME(sim,interval=10.0))
  sim.run(until=10)

seed(1)
example_n3()

Typical output is:

#time cell    UE      x       y       throughput Mb/s
0.0   0       5       618     694     6.63
0.0   0       7       435     549     1.17
0.0   1       1       709     593     13.26
0.0   2       0       395     405     0.98
0.0   2       2       567     266     2.65
0.0   2       3       718     496     0.61
0.0   2       4       484     346     2.65
0.0   2       6       310     377     0.61
1.0   0       5       616     694     6.63
1.0   0       7       437     548     1.17
1.0   1       1       710     592     13.26
1.0   2       0       395     406     0.98
1.0   2       2       566     264     2.05
1.0   2       3       719     497     0.61
1.0   2       4       484     347     2.65
1.0   2       6       312     377     0.61

Example 4 - adding a RIC

A RIC (radio intelligent controller) is an agent able to control any aspect of a cell configuration in real time. This toy example illustrates the detection of the UE with the lowest throughout (throughputs[0][1], after the sort), and allocates a new subband to the cell serving that UE.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from AIMM_simulator_core import Sim,Logger,MME,Scenario,RIC,np_array_to_str
from numpy.random import standard_normal,seed

class MyScenario(Scenario):
  def loop(self,interval=0.1):
    while True:
      for ue in self.sim.UEs: ue.xyz[:2]+=standard_normal(2)
      yield self.sim.wait(interval)

class MyLogger(Logger):
  def loop(self):
    self.f.write('#time\tcell\tUE\tx\ty\tthroughput\n')
    while True:
      for cell in self.sim.cells:
        for ue_i in cell.reports['cqi']:
          xy=self.sim.get_UE_position(ue_i)[:2]
          tp=np_array_to_str(cell.get_UE_throughput(ue_i))
          self.f.write(f't={self.sim.env.now:.1f}\tcell={cell.i}\tUE={ue_i}\tx={xy[0]:.0f}\ty={xy[1]:.0f}\ttp={tp}Mb/s\n')
      yield self.sim.wait(self.logging_interval)

class MyRIC(RIC):
  def loop(self,interval=10):
    while True:
      throughputs=[(self.sim.cells[ue.serving_cell.i].get_UE_throughput(ue.i)[0],ue.i,) for ue in self.sim.UEs]
      throughputs.sort()
      mask=self.sim.UEs[throughputs[0][1]].serving_cell.subband_mask
      for i,bit in enumerate(mask):
        if bit==0:
          mask[i]=1
          break
      yield self.sim.wait(interval)

def example_n4():
  sim=Sim()
  for i in range(4): sim.make_cell()
  for i in range(8): sim.make_UE().attach_to_nearest_cell()
  sim.add_logger(MyLogger(sim,logging_interval=1))
  sim.add_scenario(MyScenario(sim))
  sim.add_MME(MME(sim,interval=10.0))
  sim.add_ric(MyRIC(sim,interval=10.0))
  sim.run(until=10)

seed(1)
example_n4()

Example 5 - adding an antenna radiation pattern

In this example there are two cells, and one UE which is driven around a circle centred on Cell[0] by the MyScenario class. There is no MME, so no handovers occur. With omnidirectional antennas, the UE would be exactly at the cell edge at times which are multiples of 100 seconds. But with the pattern implemented for Cell[0] in line 29, the antenna has a beam pointing east, towards the interfering cell (Cell[1], which remains omnidirectional). This considerable improves the average throughput.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from math import cos,sin,pi
from numpy.random import seed
from AIMM_simulator_core import Sim,Scenario,Logger,to_dB

class MyScenario(Scenario):
  # move the UE around a circle of specified radius, period T seconds
  def loop(self,interval=1.0,radius=100.0,T=100.0):
    while True:
      t=self.sim.env.now
      for ue in self.sim.UEs:
        ue.xyz[:2]=radius*cos(2*pi*t/T),radius*sin(2*pi*t/T)
      yield self.sim.wait(interval)

class MyLogger(Logger):
  def loop(self,ue_i=0,subband=0):
    ' log for UE[ue_i] only, from reports sent to Cell[0]. '
    cell=self.sim.cells[0]
    UE=self.sim.UEs[ue_i]
    reports=cell.reports['throughput_Mbps']
    while True:
      tm=self.sim.env.now              # current time
      xy=UE.get_xyz()[:2]              # current UE position
      tp=reports[ue_i].value[subband]  # current UE throughput
      self.f.write(f'{tm:.1f}\t{xy[0]:.0f}\t{xy[1]:.0f}\t{tp}\n')
      yield self.sim.wait(self.logging_interval)

def example_n5():
  sim=Sim()
  pattern=lambda angle: 10.0+to_dB(abs(cos(0.5*angle*pi/180.0)))
  cell0=sim.make_cell(xyz=[  0.0,0.0,20.0],pattern=pattern)
  cell1=sim.make_cell(xyz=[200.0,0.0,20.0])
  ue=sim.make_UE(xyz=[100.0,0.0,2.0])
  ue.attach(cell0)
  sim.add_logger(MyLogger(sim,logging_interval=5))
  sim.add_scenario(MyScenario(sim))
  sim.run(until=500)

seed(1)
example_n5()

A typical command to run this using the real-time plotter would be:

python3 AIMM_simulator_example_n5.py | ./realtime_plotter_03.py -np=3 -tm=500 -ylims='{0: (-100,100), 1: (-100,100), 2: (0,45)}' -ylabels='{0: "UE[0] $x$", 1: "UE[0] $y$", 2: "UE[0] throughput"}' -fnb='img/AIMM_simulator_example_n5'.

This generates a plot like this:

AIMM_simulator_example_n5.png

Example 6 - a hetnet (heterogeneous network) with macro and small cells

In this example we start with 9 macro-cells in a 3x3 grid arrangement (line 30). We then drop 50 UEs at random into the system (line 32), and start the simulation (there is no UE mobility). The logger just computes the average throughput over all UEs. The scenario has these discrete events:

  1. At time 20, 20 small cells at random locations are added to the system (line 8). There is a drop in average throughput because the new cells just generate interference and are not yet used as serving cells.

  2. At time 40, the UEs are reattached to the best cell (line 11). Now throughput improves to above the initial value, because some UEs are now served by a nearby small cell.

  3. At time 60, subband masks are applied to make the macro and small cells non-interfering. Macro cells are given 1/4 of the channel bandwidth (line 15), and the small cells have 3/4 of the channel bandwidth (line 17). Now throughput improves further. As this subband allocation will not be optimal, further improvement will still be possible.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from sys import stderr
from numpy.random import seed
from AIMM_simulator_core import Sim,Logger,Scenario

class MyScenario(Scenario):
  def loop(self):
    yield self.sim.wait(20) # wait 20 seconds
    for i in range(20): self.sim.make_cell(power_dBm=10.0,n_subbands=4)
    print(f'small cells added at t={self.sim.env.now:.2f}',file=stderr)
    yield self.sim.wait(20) # wait 20 seconds
    for UE in self.sim.UEs: UE.attach_to_best_cell()
    print(f'UEs reattached at t={self.sim.env.now:.2f}',file=stderr)
    yield self.sim.wait(20) # wait 20 seconds
    for i in range(9):      # set subband mask for macro cells
      self.sim.cells[i].set_subband_mask([1,0,0,0])
    for i in range(9,29):   # set subband mask for small cells
      self.sim.cells[i].set_subband_mask([0,1,1,1])
    print(f'cells masked at t={self.sim.env.now:.2f}',file=stderr)

class MyLogger(Logger):
  def loop(self):
    while True: # log average throughput over all UEs
      ave=self.sim.get_average_throughput()
      self.f.write(f'{self.sim.env.now:.2f}\t{ave:.4}\n')
      yield self.sim.wait(self.logging_interval)

def hetnet():
  sim=Sim()
  for i in range(9):
    sim.make_cell(xyz=(1000.0*(i//3),1000.0*(i%3),20.0),power_dBm=30.0,n_subbands=4)
  for i in range(50):
    sim.make_UE().attach_to_best_cell()
  sim.add_logger(MyLogger(sim,logging_interval=1.0e-1))
  sim.add_scenario(MyScenario(sim))
  sim.run(until=100)

seed(3)
hetnet()

The command

python3 AIMM_simulator_example_n6.py | ./realtime_plotter_03.py -np=1 -tm=100 -ylims='(0,0.25)' -ylabels='{0: "average downlink throughput over all UEs"}' -fnb='img/AIMM_simulator_example_n6'

then generates a plot like this:

AIMM_simulator_example_n6.png

Example 7 - a hetnet with mobility

This is similar to example 6, but now an MME is added to perform handovers for the mobile UEs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from numpy.random import seed,standard_normal
from AIMM_simulator_core import Sim,Logger,Scenario,MME,np_array_to_str

class MyScenario(Scenario):
  def loop(self,interval=10):
    while True:
      for ue in self.sim.UEs: ue.xyz[:2]+=20*standard_normal(2)
      yield self.sim.wait(interval)

class MyLogger(Logger):
  # throughput of UE[0], UE[0] position, serving cell index
  def loop(self):
    while True:
      sc=self.sim.UEs[0].serving_cell.i
      tp=self.sim.cells[sc].get_UE_throughput(0)[0]
      xy0=np_array_to_str(self.sim.UEs[0].xyz[:2])
      self.f.write(f'{self.sim.env.now:.2f}\t{tp:.4f}\t{xy0}\t{sc}\n')
      yield self.sim.wait(self.logging_interval)

def hetnet(n_subbands=1):
  sim=Sim()
  for i in range(9): # macros
    sim.make_cell(xyz=(500.0*(i//3),500.0*(i%3),20.0),power_dBm=30.0,n_subbands=n_subbands)
  for i in range(10): # small cells
    sim.make_cell(power_dBm=10.0,n_subbands=n_subbands)
  for i in range(20):
    sim.make_UE().attach_to_nearest_cell()
  sim.UEs[0].set_xyz([500.0,500.0,2.0])
  for UE in sim.UEs: UE.attach_to_best_cell()
  sim.add_logger(MyLogger(sim,logging_interval=1.0))
  sim.add_scenario(MyScenario(sim))
  sim.add_MME(MME(sim,verbosity=0,interval=50.0))
  sim.run(until=2000)

seed(3)
hetnet()

The command

python3 AIMM_simulator_example_n7.py | ./realtime_plotter_03.py -np=4 -tm=2000 -ylims='{0: (0,10), 1: (0,1000), 2: (0,1000), 3: (0,30)}' -ylabels='{0: "UE[0] throughput", 1: "UE[0] $x$", 2: "UE[0] $y$", 3: "UE[0] serving cell"}' -fnb='img/AIMM_simulator_example_n7'

then generates a plot like this:

AIMM_simulator_example_n7.png

Example 8 - estimating CQI distribution

This is similar to example 7, but now we create a histogram at the end of the simulation, rather than using real-time plotting. A MIMO gain boost is added half-way through the simulation, in order to observe the effect on the CQI values at UE[0]. This example also illustrates the use of the finalize function, to create the histograms after the simulation has finished.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
from math import cos,sin,pi
import numpy as np
from numpy.random import seed,standard_normal
from AIMM_simulator_core import Sim,Logger,Scenario,MME

class MyScenario(Scenario):
  def loop(self,interval=1,radius=100.0,T=100.0,circle=False):
    while True: 
      for i,ue in enumerate(self.sim.UEs):
        if circle and i==0: # walk UE[0] around a circle
          t=self.sim.env.now
          ue.xyz[:2]=500+radius*cos(2*pi*t/T),500+radius*sin(2*pi*t/T)
        else: # random walk, mean speed=1
          ue.xyz[:2]+=standard_normal(2)/1.414
      yield self.sim.wait(interval)

class Histogram_Logger(Logger):
  # CQI histogram for UE[0]
  h_cqi0=np.zeros(16)
  h_cqi1=np.zeros(16)
  def loop(self):
    ue0=self.sim.UEs[0]
    while self.sim.env.now<0.5*self.sim.until:
      sc=ue0.get_serving_cell()
      cqi=ue0.get_CQI()
      if cqi is not None: self.h_cqi0[cqi[0]]+=1
      yield self.sim.wait(self.logging_interval)
    # half-time break - boost MIMO gain of all cells
    for cell in self.sim.cells:
      cell.set_MIMO_gain(6.0)
    while True:
      sc=ue0.get_serving_cell()
      cqi=ue0.get_CQI()
      if cqi is not None: self.h_cqi1[cqi[0]]+=1
      yield self.sim.wait(self.logging_interval)
  def finalize(s):
    hs=(s.h_cqi0/np.sum(s.h_cqi0),s.h_cqi1/np.sum(s.h_cqi1))
    plot_histograms(hs)

def plot_histograms(hs,fn='img/AIMM_simulator_example_n8'):
  import matplotlib.pyplot as plt
  from matplotlib.patches import Rectangle
  from matplotlib.collections import PatchCollection
  from fig_timestamp_00 import fig_timestamp
  ymax=max(max(h) for h in hs)
  fig=plt.figure(figsize=(8,8))
  ax=fig.add_subplot(1,1,1)
  ax.grid(linewidth=1,color='gray',alpha=0.25)
  ax.set_xlabel('CQI for UE[0]'); ax.set_ylabel('relative frequency')
  ax.set_xlim(0,15); ax.set_ylim(0,1.1*ymax)
  for h,fc,x in zip(hs,('b','r'),(-0.1,0.1),):
    ax.add_collection(PatchCollection([Rectangle((i+x,0),0.2,hi) for i,hi in enumerate(h)],facecolor=fc,alpha=0.8))
  ax.annotate('blue: normal\nred: after 6dB MIMO gain boost',(7,0.97*ymax),color='k',fontsize=14,bbox=dict(facecolor='w',edgecolor='k',boxstyle='round,pad=1'))
  fig_timestamp(fig,author='Keith Briggs',fontsize=8)
  for h,fc in zip(hs,('b','r'),):
    mean=sum(i*hi for i,hi in enumerate(h))/np.sum(h)
    ax.text(mean,-0.04,'mean',ha='center',va='center',rotation=90,size=8,bbox=dict(boxstyle='rarrow,pad=0.1',fc=fc,ec=fc,lw=1))
  fig.savefig(fn+'.pdf')
  fig.savefig(fn+'.png')

def example_n8():
  sim=Sim()
  for i in range(9):  # cells
    sim.make_cell(xyz=(300+200.0*(i//3),300+200.0*(i%3),10.0),power_dBm=10.0)
  for i in range(9): # UEs
    sim.make_UE(verbosity=1).attach_to_best_cell()
  sim.UEs[0].set_xyz([503.0,507.0,2.0])
  sim.UEs[0].attach_to_best_cell()
  logger=Histogram_Logger(sim,logging_interval=1.0)
  sim.add_logger(logger)
  sim.add_scenario(MyScenario(sim))
  sim.add_MME(MME(sim,verbosity=0,interval=20.0))
  sim.run(until=2*5000)

if __name__=='__main__':
  seed(1)
  example_n8()

The command

python3 AIMM_simulator_example_n8.py

then generates a plot like this:

AIMM_simulator_example_n8.png

Using the real-time plotting utility

As an aid to development and debugging, a stand-alone python script realtime_plotter_03.py for real-time plotting is included. This reads stdin and plots in a window as the data is generated. By default, png and pdf images are saved when all data has been read. It is configured with these command-line arguments:

-np      number of plots (default 1)
-tm      t_max (maximum time on x-axis, default 10)
-xl      x-axis label (default 'time')
-fst     final sleep time (before closing the window and saving the images)
-fnb     filename base
-ylims   y-axis limits (a python dictionary)
-ylabels y-axis labels (a python dictionary
-title   figure title
-lw      line width (default 2)

If -fnb is specifed, the final plot is saved as png and pdf figures. Typical usage would be in a bash script like this:

1
2
3
4
5
6
7
8
9
python3 AIMM_simulator_RIC_example_01.py | \
   ./realtime_plotter_03.py \
   -np=7 \
   -tm=10000 \
   -fst=30 \
   -fnb='img/AIMM_simulator_RIC_example' \
   -ylims='{0: (-100,100), 1: (-100,100), 2: (-1,16), 3: (-1,16), 4: (-1,16), 5: (0,50), 6: (0,9)}' \
   -ylabels='{0: "$x$", 1: "$y$", 2: "CQI$_0$", 3: "CQI$_1$", 4: "CQI$_2$", 5: "throughput", 6: "serving cell"}' \
   -title 'UE$_0$'

This generates a plot like this:

AIMM_simulator_RIC_example.png

Simulator module API reference

AIMM simulator core

class AIMM_simulator_core.Cell(sim, bw_MHz=10.0, n_subbands=1, xyz=None, h_BS=20.0, power_dBm=30.0, MIMO_gain_dB=0.0, pattern=None, verbosity=0)

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.

  • 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).

  • verbosity (int) – Level of debugging output (0=none).

get_RSRP_reports()

Return the current RSRP reports to this cell, as a list of tuples (ue.i, rsrp).

get_RSRP_reports_dict()

Return the current RSRP reports to this cell, as a dictionary ue.i: rsrp.

get_UE_CQI(ue_i)

Return the current CQI of UE[i] in the simulation, as an array across all subbands.

get_UE_throughput(ue_i)

Return the current throughput in Mb/s of UE[i] in the simulation, as an array across all subbands.

get_average_throughput()

Return the average throughput over all UEs attached to this cell.

get_power_dBm()

Return the transmit power in dBm currently used by this cell.

get_subband_mask()

Get the current subband mask.

get_xyz()

Return the current position of this Cell.

loop()

Main loop of cell class. Default: do nothing (slowly).

set_MIMO_gain(MIMO_gain_dB)

Set the MIMO gain in dB to be used by this cell.

set_pattern(pattern)

Set the antenna radiation pattern.

set_power_dBm(p)

Set the transmit power in dBm to be used by this cell.

set_subband_mask(mask)

Set the subband mask to mask.

set_xyz(xyz)

Set a new position for this Cell.

class AIMM_simulator_core.Logger(sim, func=None, header='', f=<_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'>, logging_interval=10, np_array_to_str=<function np_array_to_str>)

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.

finalize()

Function called at end of simulation, to implement any required finalization actions.

loop()

Main loop of Logger class. Can be overridden to provide custom functionality.

class AIMM_simulator_core.MME(sim, interval=10.0, verbosity=0)

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

finalize()

Function called at end of simulation, to implement any required finalization actions.

loop()

Main loop of MME.

class AIMM_simulator_core.RIC(sim, interval=10, verbosity=0)

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

finalize()

Function called at end of simulation, to implement any required finalization actions.

loop()

Main loop of RIC class. Must be overridden to provide functionality.

class AIMM_simulator_core.Report(time: float, sender: int, report_type: str = '', message: str = '', value: numpy.array = array([], dtype=float64))

Encapsulates a UE report. Internal use only.

class AIMM_simulator_core.Scenario(sim, func=None, interval=1.0, verbosity=0)

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

loop()

Main loop of Scenario class. Can be overridden to provide different functionalities.

class AIMM_simulator_core.Sim(params={})
Class representing the complete simulation.
paramsdict

A dictionary of additional global parameters which need to be accessible to downstream functions.

add_MME(mme)

Add an MME instance to the simulation.

add_logger(logger)

Add a logger to the simulation.

add_loggers(loggers)

Add a sequence of loggers to the simulation.

add_ric(ric)

Add a RIC instance to the simulation.

add_scenario(scenario)

Add a Scenario instance to the simulation.

get_UE_position(ue_i)

Return the xyz position of UE[i] in the simulation.

get_average_throughput()

Return the average throughput over all UEs attached to all cells.

get_best_cell(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.

get_nearest_cell(xy)

Return the index of the geographical nearest cell (in 2 dimensions) to the point xy.

get_nues()

Return the current number of UEs in the simulation.

make_UE(**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.

make_cell(**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 value stays the same throughout the simulation).

wait(interval=1.0)

Convenience function to avoid low-level reference to env.timeout(). loop functions in each class must yield this.

class AIMM_simulator_core.UE(sim, xyz=None, reporting_interval=1.0, pathloss_model=<class 'UMa_pathloss_model_00.UMa_pathloss'>, h_UT=2.0, verbosity=0)

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 termial in metres; only used if xyz is not provided.

  • reporting_interval (float) – Time interval between UE reports being sent to the serving cell.

  • pathloss_model – An instance of a pathloss model; see NR_5G_standard_functions_00.py.

attach(cell, quiet=True)

Attach this UE to a specific Cell instance.

attach_to_best_cell()

Attach to the cell delivering the strongest signal at the current UE position.

attach_to_nearest_cell()

Attach this UE to the geographically nearest Cell instance.

detach(quiet=True)

Detach this UE from its serving cell.

get_CQI()

Return the current CQI of this UE, as an array across all subbands.

get_serving_cell()

Return the current serving Cell object (not index) for this UE instance.

get_serving_cell_i()

Return the current serving Cell index for this UE instance.

get_xyz()

Return the current position of this UE.

send_rsrp_reports(threshold=- 120.0)

Send RSRP reports in dBm to all cells for which it is over the threshold. TODO: antenna pattern

send_subband_cqi_report()

Send an array of RSRP, CQI, and throughput reports, one for each subband, to the serving cell. Also save the CQI value in s.cqi.

set_xyz(xyz, verbose=False)

Set a new position for this UE.

AIMM_simulator_core.np_array_to_str(x)

Formats a 1-axis np.array as a tab-separated string

NR 5G standard functions

NR_5G_standard_functions_00.RSRP_report(rsrp_dBm)

Convert RSRP report from dBm to standard range.

Parameters

rsrp_dBm (float) – RSRP report in dBm

Returns

RSRP report in standard range.

Return type

int

class NR_5G_standard_functions_00.Radio_state(NofSlotsPerRadioFrame: int = 20, NofRadioFramePerSec: int = 100, NRB_sc: int = 12, Nsh_symb: int = 13, NPRB_oh: int = 0, nPRB: int = 273, Qm: int = 8, v: int = 4, R: float = 0.948, MCS: int = 20)

UMa pathloss model

class UMa_pathloss_model_00.UMa_pathloss(fc_GHz=3.5, h_UT=2.0, h_BS=25.0)

Urban macrocell dual-slope pathloss model, from 3GPP standard 36.873, Table 7.2-1, line-of-sight model.

See https://portal.3gpp.org/desktopmodules/Specifications/SpecificationDetails.aspx?specificationId=2574.

TODO: could we usefully vectorize this?

__call__(d3D_m)

Return the pathloss at 3-dimensional distance d3D_m (in metres).

__init__(fc_GHz=3.5, h_UT=2.0, h_BS=25.0)

Initialize a pathloss model instance.

Parameters
  • fc_GHz (float) – Centre frequency in GigaHertz.

  • h_UT (float) – Height of User Terminal (=UE) in metres.

  • h_BS (float) – Height of Base Station in metres.

Last modified: 2021-06-10T09:53