AIMM simulator documentation

Last modified: 2022-11-30T09:59

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. The simulator was developed for the AIMM project.

The github sources are at <https://github.com/keithbriggs/AIMM-simulator>.

Software dependencies

  1. Python 3.8 or higher.
  2. NumPy.
  3. Simpy.
  4. If real-time plotting is needed, matplotlib, with an appropriate backend such as PyQt5 (pip install PyQt5).

Installation

Three ways are possible:

  • The simplest way, direct from PyPI: pip install AIMM-simulator. This will not always get the latest version.
  • Download the wheel, typically dist/aimm_simulator-2.0.0-py3-none-any.whl from github, and run pip install <wheel>.
  • Alternatively, the package can be installed by downloading the complete repository (using the green <> Code button) as a zip, unpacking it, and then doing make install_local from inside the unpacked zip.

After installation, run a test with python3 examples/basic_test.py.

Note that a plotting utility src/realtime_plotter.py is included but not installed. If needed, this script should be placed somewhere in your python path. A folder img is used by the examples to save plots in png and pdf format. So in order to run the examples with plotting, this folder must be created.

The documentation can be built from the sources with make doc.

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 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 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.  ]
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
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
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
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
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),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
from math import cos,sin,pi
from numpy.random import seed
from AIMM_simulator 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):
    ' log for UE[ue_i] only, from reports sent to Cell[0]. '
    cell=self.sim.cells[0]
    UE=self.sim.UEs[ue_i]
    while True:
      tm=self.sim.env.now              # current time
      xy=UE.get_xyz()[:2]              # current UE position
      tp=cell.get_UE_throughput(ue_i)  # 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_05.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 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_strongest_cell_simple_pathloss_model()
    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_strongest_cell_simple_pathloss_model()
  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 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)
      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_strongest_cell_simple_pathloss_model()
  sim.UEs[0].set_xyz([500.0,500.0,2.0])
  for UE in sim.UEs: UE.attach_to_strongest_cell_simple_pathloss_model()
  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 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='examples/img/AIMM_simulator_example_n8'):
  import matplotlib.pyplot as plt
  from matplotlib.patches import Rectangle
  from matplotlib.collections import PatchCollection
  from fig_timestamp 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_strongest_cell_simple_pathloss_model()
  sim.UEs[0].set_xyz([503.0,507.0,2.0])
  sim.UEs[0].attach_to_strongest_cell_simple_pathloss_model()
  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 geometry_3d module

This module is provided for running indoor simulations, and takes account of wall adsorptions, but not reflections or diffraction. It is not a full ray-tracing code. The main task it performs is to compute intersections of signal paths with walls, and it is optimized to be fast for this application. An example of usage is below.

More details on usage to be added here.

from geometry_3d import block,Panel,Building,Ray,draw_building
blk0=block(np.array([0, 0,0]),np.array([5,10,3]))
blk1=block(np.array([0,10,0]),np.array([6,12,2]))
blk2=block(np.array([0,12,0]),np.array([6,14,2]))
blk3=block(np.array([0,14,0]),np.array([6,16,2]))
blk4=block(np.array([0,16.5,0]),np.array([6,17,2]))
fence=Panel([Triangle((8,0,0),(8,15,0),(8,15,1)),
             Triangle((8,0,1),(8, 0,0),(8,15,1))])
b=Building(blk0+blk1+blk2+blk3+blk4+(fence,))
ray0=Ray((0.3,0.3,2.0),(0.1,1,-0.01))
line_segments=[(8,8),(18,18),(0,4)] # [xs,ys,zs]
draw_building(b,rays=[ray0],line_segments=line_segments,color='y',limits=[(0,10),(0,20),(0,4)],labels=['$x$','$y$','$z$'],fontsize=6,show=True,pdffn='building0.pdf')

This constructs a building like this:

building0.png

Using the real-time plotting utility

As an aid to development and debugging, a stand-alone python script realtime_plotter_05.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
10
python3 AIMM_simulator_RIC_example_01.py | \
   ./realtime_plotter.py \
   -author='Keith Briggs' \
   -nplots=7 \
   -tmax=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

class AIMM_simulator.Sim(params={'fc_GHz': 3.5, 'h_BS': 20.0, 'h_UT': 2.0}, show_params=True, rng_seed=0)[source]

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.
add_MME(mme)[source]

Add an MME instance to the simulation.

add_logger(logger)[source]

Add a logger to the simulation.

add_loggers(loggers)[source]

Add a sequence of loggers to the simulation.

add_ric(ric)[source]

Add a RIC instance to the simulation.

add_scenario(scenario)[source]

Add a Scenario instance to the simulation.

get_UE_position(ue_i)[source]

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

get_average_throughput()[source]

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

get_best_rsrp_cell(ue_i, dbg=False)[source]

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.

get_ncells()[source]

Return the current number of cells in the simulation.

get_nearest_cell(xy)[source]

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

get_nues()[source]

Return the current number of UEs in the simulation.

get_strongest_cell_simple_pathloss_model(xyz, alpha=3.5)[source]

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.

make_UE(**kwargs)[source]

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)[source]

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

wait(interval=1.0)[source]

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

class AIMM_simulator.Cell(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)[source]

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).
boost_power_dBm(p, mn=None, mx=None)[source]

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.

get_RSRP_reports()[source]

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

get_RSRP_reports_dict()[source]

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

get_UE_CQI(ue_i)[source]

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.

get_UE_throughput(ue_i)[source]

Return the total current throughput in Mb/s of UE[i] in the simulation.

get_average_throughput()[source]

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

get_nattached()[source]

Return the current number of UEs attached to this Cell.

get_power_dBm()[source]

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

get_rsrp(i)[source]

Return last RSRP reported to this cell by UE[i].

get_rsrp_history(i)[source]

Return an array of the last 10 RSRP[1]s reported to this cell by UE[i].

get_subband_mask()[source]

Get the current subband mask.

get_xyz()[source]

Return the current position of this Cell.

loop()[source]

Main loop of Cell class. Default: do nothing.

set_MIMO_gain(MIMO_gain_dB)[source]

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

set_f_callback(f_callback, **kwargs)[source]

Add a callback function to the main loop of this Cell

set_pattern(pattern)[source]

Set the antenna radiation pattern.

set_power_dBm(p)[source]

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

set_subband_mask(mask)[source]

Set the subband mask to mask.

set_xyz(xyz)[source]

Set a new position for this Cell.

class AIMM_simulator.UE(sim, xyz=None, reporting_interval=1.0, pathloss_model=None, h_UT=2.0, f_callback=None, f_callback_kwargs={}, verbosity=0)[source]

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.
attach(cell, quiet=True)[source]

Attach this UE to a specific Cell instance.

attach_to_nearest_cell()[source]

Attach this UE to the geographically nearest Cell instance. Intended for initial attachment only.

attach_to_strongest_cell_simple_pathloss_model()[source]

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.

detach(quiet=True)[source]

Detach this UE from its serving cell.

get_CQI()[source]

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

get_serving_cell()[source]

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

get_serving_cell_i()[source]

Return the current serving Cell index for this UE instance.

get_xyz()[source]

Return the current position of this UE.

loop()[source]

Main loop of UE class

send_rsrp_reports(threshold=-120.0)[source]

Send RSRP reports in dBm to all cells for which it is over the threshold. Subbands not handled.

send_subband_cqi_report()[source]

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.

set_f_callback(f_callback, **kwargs)[source]

Add a callack function to the main loop of this UE

set_xyz(xyz, verbose=False)[source]

Set a new position for this UE.

class AIMM_simulator.Scenario(sim, func=None, interval=1.0, verbosity=0)[source]

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()[source]

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

class AIMM_simulator.MME(sim, interval=10.0, strategy='strongest_cell_simple_pathloss_model', anti_pingpong=30.0, verbosity=0)[source]

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.
do_handovers()[source]

Check whether handovers are required, and do them if so. Normally called from loop(), but can be called manually if required.

finalize()[source]

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

loop()[source]

Main loop of MME.

class AIMM_simulator.RIC(sim, interval=10, verbosity=0)[source]

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()[source]

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

loop()[source]

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

class AIMM_simulator.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>)[source]

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()[source]

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

loop()[source]

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

NR 5G standard functions

NR_5G_standard_functions.RSRP_report(rsrp_dBm)[source]

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.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)[source]

UMa pathloss model

class UMa_pathloss_model.UMa_pathloss(fc_GHz=3.5, h_UT=2.0, h_BS=25.0, LOS=True, h=20.0, W=20.0)[source]

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

The model is defined in 36873-c70.doc from https://portal.3gpp.org/desktopmodules/Specifications/SpecificationDetails.aspx?specificationId=2574.

This code covers the cases 3D-UMa LOS and NLOS. 3D-UMa = three dimensional urban macrocell model. LOS = line-of-sight. NLOS = non-line-of-sight.

__call__(xyz_cell, xyz_UE)[source]

Return the pathloss between 3-dimensional positions xyz_cell and xyz_UE (in metres). Note that the distances, building heights, etc. are not checked to ensure that this pathloss model is actually applicable.

__init__(fc_GHz=3.5, h_UT=2.0, h_BS=25.0, LOS=True, h=20.0, W=20.0)[source]

Initialize a pathloss model instance.

Parameters:
  • fc_GHz (float) – Centre frequency in GigaHertz (default 3.5).
  • h_UT (float) – Height of User Terminal (=UE) in metres (default 2).
  • h_BS (float) – Height of Base Station in metres (default 25).
  • LOS (bool) – Whether line-of-sight model is to be used (default True).
  • h (float) – Average building height (default 20, used in NLOS case only)
  • W (float) – Street width (default 20, used in NLOS case only)
UMa_pathloss_model.plot()[source]

Plot the pathloss model predictions, as a self-test.

UMi pathloss model

class UMi_pathloss_model.UMi_streetcanyon_pathloss(fc_GHz=3.5, h_UT=2.0, h_BS=10.0, LOS=True)[source]

Urban microcell dual-slope pathloss model, from 3GPP standard 36.873, Table 7.2-1. The model is defined in 36873-c70.doc from https://portal.3gpp.org/desktopmodules/Specifications/SpecificationDetails.aspx?specificationId=2574. This code covers the cases 3D-UMi LOS and NLOS. 3D-UMi = three-dimensional urban street canyon model. LOS = line-of-sight. NLOS = non-line-of-sight.

__call__(xyz_cell, xyz_UE)[source]

Return the pathloss between 3-dimensional positions xyz_cell and xyz_UE (in metres). Note that the distances, building heights, etc. are not checked to ensure that this pathloss model is actually applicable.

__init__(fc_GHz=3.5, h_UT=2.0, h_BS=10.0, LOS=True)[source]

Initialize a pathloss model instance.

Parameters:
  • fc_GHz (float) – Centre frequency in GigaHertz (default 3.5).
  • h_UT (float) – Height of User Terminal (=UE) in metres (default 2).
  • h_BS (float) – Height of Base Station in metres (default 10 for UMi).
  • LOS (bool) – Whether line-of-sight model is to be used (default True).
UMi_pathloss_model.plot()[source]

Plot the pathloss model predictions, as a self-test.

InH pathloss model

class InH_pathloss_model.InH_pathloss(fc_GHz=3.5, h_UT=2.0, h_BS=4.0, LOS=True)[source]

3D-InH indoor pathloss model, from 3GPP standard 36.873, Table 7.2-1. Indoor Hotspot cell with high (indoor) UE density.

The model is defined in 36873-c70.doc from https://portal.3gpp.org/desktopmodules/Specifications/SpecificationDetails.aspx?specificationId=2574.

LOS = line-of-sight. NLOS = non-line-of-sight.

__call__(xyz_cell, xyz_UE)[source]

Return the pathloss between 3-dimensional positions xyz_cell and xyz_UE (in metres). Note that the distances, heights, etc. are not checked to ensure that this pathloss model is actually applicable.

__init__(fc_GHz=3.5, h_UT=2.0, h_BS=4.0, LOS=True)[source]

Initialize a pathloss model instance.

Parameters:
  • fc_GHz (float) – Centre frequency in GigaHertz (default 3.5).
  • h_UT (float) – Height of User Terminal (=UE) in metres (default 2).
  • h_BS (float) – Height of Base Station in metres (default 25).
InH_pathloss_model.plot()[source]

Plot the pathloss model predictions, as a self-test.

geometry_3d module

class geometry_3d.Building(panels)[source]

Represents a collection of panels making up a building.

class geometry_3d.Panel(triangles)[source]

Represents a collection of triangles (which must be parallel) making up a single flat wall panel.

class geometry_3d.Plane(point, normal)[source]

Represents an infinite plane, defined by a point on the plane and a normal vector

class geometry_3d.RIS(panel)[source]

TODO a RIS.

class geometry_3d.Ray(tail, dv)[source]

Represents a ray, defined by a tail (starting point) and a direction vector

plot(ax, length=1.0, color='b', alpha=0.5)[source]

Plots the ray in 3d

class geometry_3d.Triangle(p0, p1, p2)[source]

Represents a planar triangle in 3d space, defined by three points. Unoriented.

plot(ax, color='y', alpha=0.5, drawedges=True)[source]

Plots the triangle in 3d. For kwargs, see https://matplotlib.org/stable/api/collections_api.html#matplotlib.collections.Collection

geometry_3d.block(c0, c1)[source]

Represents a rectangular block with opposite corners c0 and c1, with each face a rectangular Panel

geometry_3d.cube(a, b, c=(0.0, 0.0, 0.0))[source]

cube c+[a,b]x[a,b]x[a,b], with each face a square Panel of two Triangles

geometry_3d.draw_building_3d(building, rays=[], line_segments=[], dots=[], color='y', fontsize=6, limits=[(0, 10), (0, 10), (0, 5)], labels=['', '', ''], drawedges=True, show=True, pdffn='', pngfn='', dbg=False)[source]

General function to draw a building, also rays and lines.

Real-time plotting utility

This is independent of the main simulator code. It reads output from the Logger via a pipe from stdin, and plots it using a matplotlib animation. It is driven with command-line options, which can be seen by running realtime_plotter.py --help.

-h, --help            show this help message and exit
--selftest            self-test
-naxes NAXES          number of axes
-nplots NPLOTS        number of plots
-tmax TMAX            t_max
-xlabel XLABEL        x axis label
-fst FST              final sleep time
-fnb FNB              filename base
-ylims YLIMS          y limits (dict)
-ylabels YLABELS      ylabels (dict)
-title TITLE          figure title
-lw LW                plot linewidth
-author AUTHOR        author name for plot bottom margin
-extra EXTRA          extra features to be added to the plot; raw python code
-inputfile INPUTFILE  file to read input from instead of stdin; in this case the plot is not displayed, but written to an mp4 file
-column_to_axis_map COLUMN_TO_AXIS_MAP column_to_axis_map (dict)

Last modified: 2022-11-30T09:59