Commit b8065120 authored by Kirill Smelkov's avatar Kirill Smelkov

nrarfcn: New package to do computations with NR bands, frequencies and NR-ARFCN numbers.

Do a package for converting DL/UL NR-ARFCN in between each other and to
convert DL NR-ARFCN to SSB NR-ARFCN. The API mimics xlte.earfcn added in 6cb9d37f.

xlte.nrarfcn complements pypi.org/project/nrarfcn, which we use here under the hood.

See package documentation for API details.
parent 6cb9d37f
......@@ -5,6 +5,7 @@
XLTE repository provides assorted tools and packages with functionality related to LTE:
- `earfcn` - do computations with LTE bands, frequencies and EARFCN numbers.
- `nrarfcn` - do computations with NR bands, frequencies and NR-ARFCN numbers.
- `kpi` - process measurements and compute KPIs from them.
- `amari.drb` - infrastructure to process flows on data radio bearers.
- `amari.kpi` - driver for Amarisoft LTE stack to retrieve KPI-related measurements from logs.
......
......@@ -16,13 +16,13 @@
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
"""Package earfcn helps to do computations with LTE bands, frequencies and EARFCN numbers.
"""Package xlte.earfcn helps to do computations with LTE bands, frequencies and EARFCN numbers.
- frequency converts EARFCN to frequency.
- dl2ul and ul2dl convert between DL EARFCN and UL EARFCN corresponding to each other.
- band returns information about band to which EARFCN belongs.
See also pypi.org/project/nrarfcn which provides similar functionality for 5G.
See also package xlte.nrafcn and pypi.org/project/nrarfcn which provide similar functionality for 5G.
"""
import collections
......
# Copyright (C) 2023 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
"""Package xlte.nrarfcn helps to do computations with NR bands, frequencies and NR-ARFCN numbers.
It complements pypi.org/project/nrarfcn and provides the following additional utilities:
- frequency converts NR-ARFCN to frequency.
- dl2ul and ul2dl convert between DL NR-ARFCN and UL NR-ARFCN corresponding to
each other in particular band.
- dl2ssb returns SSB NR-ARFCN that is located nearby DL NR-ARFCN on Global Synchronization Raster.
See also package xlte.earfcn which provides similar functionality for 4G.
"""
# import pypi.org/project/nrarfcn with avoiding name collision with xlte.nrarfcn even if xlte is installed in editable mode.
def _():
modname = 'nrarfcn'
import sys, importlib.util
import xlte
# if already imported - we are done.
# but if previously ran `import nrarfcn` resolved to xlte/nrarfcn.py due to
# the way how easy_install handles xlte editable install with adding xlte
# onto sys.path, undo that.
mod = sys.modules.get(modname)
if mod is not None:
if mod.__spec__.origin == __spec__.origin:
del sys.modules[modname]
mod = None
if mod is not None:
return mod
# import nrarfcn with ignoring xlte.nrarfcn spec
# based on https://docs.python.org/3/library/importlib.html#approximating-importlib-import-module
# we also ignore cwd/xlte, if automatically injected to sys.path[0] by python and pytest, so that running things in xlte/ also work
pathsave = {} # idx -> sys.path[idx]
for p in [''] + xlte.__path__:
try:
i = sys.path.index(p)
except ValueError:
pass
else:
pathsave[i] = p
for i in sorted(pathsave, reverse=True):
sys.path.pop(i)
try:
for finder in sys.meta_path:
spec = finder.find_spec(modname, None)
if spec is not None and spec.origin != __spec__.origin:
break
else:
raise ModuleNotFoundError('Module %r not found' % modname)
finally:
for i in sorted(pathsave):
sys.path.insert(i, pathsave[i])
mod = importlib.util.module_from_spec(spec)
assert modname not in sys.modules
sys.modules[modname] = mod
spec.loader.exec_module(mod)
return mod
nr = _()
# dl2ul returns UL NR-ARFCN that corresponds to DL NR-ARFCN and band.
def dl2ul(dl_nr_arfcn, band): # -> ul_nr_arfcn
dl_lo, dl_hi = nr.get_nrarfcn_range(band, 'dl')
if dl_lo == 'N/A':
raise AssertionError('band%r does not have downlink spectrum' % band)
assert dl_lo <= dl_nr_arfcn <= dl_hi
ul_lo, ul_hi = nr.get_nrarfcn_range(band, 'ul')
if ul_lo == 'N/A':
raise KeyError('band%r, to which DL NR-ARFCN=%r belongs, does not have uplink spectrum' % (band, dl_nr_arfcn))
if dl_nr_arfcn - dl_lo > ul_hi - ul_lo:
raise KeyError('band%r does not have enough uplink spectrum to provide pair for NR-ARFCN=%r' % (band, dl_nr_arfcn))
ul_nr_arfcn = ul_lo + (dl_nr_arfcn - dl_lo)
assert ul_lo <= ul_nr_arfcn <= ul_hi
return ul_nr_arfcn
# ul2dl returns DL NR-ARFCN that corresponds to UL NR-ARFCN and band.
def ul2dl(ul_nr_arfcn, band): # -> dl_nr_arfcn
ul_lo, ul_hi = nr.get_nrarfcn_range(band, 'ul')
if ul_lo == 'N/A':
raise AssertionError('band%r does not have uplink spectrum' % band)
assert ul_lo <= ul_nr_arfcn <= ul_hi
dl_lo, dl_hi = nr.get_nrarfcn_range(band, 'dl')
if dl_lo == 'N/A':
raise KeyError('band%r, to which UL NR-ARFCN=%r belongs, does not have downlink spectrum' % (band, ul_nr_arfcn))
if ul_nr_arfcn - ul_lo > dl_hi - dl_lo:
raise KeyError('band%r does not have enough downlink spectrum to provide pair for NR-ARFCN=%r' % (band, ul_nr_arfcn))
dl_nr_arfcn = dl_lo + (ul_nr_arfcn - ul_lo)
assert dl_lo <= dl_nr_arfcn <= dl_hi
return dl_nr_arfcn
# dl2ssb returns SSB NR-ARFCN that is located nearby DL NR-ARFCN on Global Synchronization Raster.
#
# input Fdl should be aligned with ΔFraster.
# for return (Fdl - Fssb) is aligned with some SSB SubCarrier Spacing of given band.
# max_ssb_scs_khz indicates max SSB SubCarrier Spacing for which it was possible to find Fssb constrained with above alignment requirement.
#
# ValueError is raised if Fssb is not possible to find for given Fdl and band.
def dl2ssb(dl_nr_arfcn, band): # -> ssb_nr_arfcn, max_ssb_scs_khz
_trace('\ndl2ssb %r %r' % (dl_nr_arfcn, band))
dl_lo, dl_hi = nr.get_nrarfcn_range(band, 'dl')
if dl_lo == 'N/A':
raise AssertionError('band%r does not have downlink spectrum' % band)
assert dl_lo <= dl_nr_arfcn <= dl_hi
f = frequency(nrarfcn=dl_nr_arfcn)
_trace('f %.16g' % f)
# query all SSB SCS available in this band
if isinstance(band, int):
band = 'n%d' % band
tab_fr1 = nr.tables.applicable_ss_raster_fr1.table_applicable_ss_raster_fr1()
tab_fr2 = nr.tables.applicable_ss_raster_fr2.table_applicable_ss_raster_fr2()
scs_v = []
for tab in (tab_fr1, tab_fr2):
for row in tab.data:
if tab.get_cell(row, 'band') == band:
scs_v.append( tab.get_cell(row, 'scs') )
# for each scs↓ try to find suitable sync point
for scs_khz in sorted(scs_v, reverse=True):
_trace('trying scs %r' % scs_khz)
scs = scs_khz / 1000 # khz -> mhz
# locate nearby point on global sync raster and further search around it
# until sync point aligns to be multiple of scs
gscn = nr.get_gscn_by_frequency(f)
while 1:
f_sync = nr.get_frequency_by_gscn(gscn)
f_sync_arfcn = nr.get_nrarfcn(f_sync)
if not (dl_lo <= f_sync_arfcn <= dl_hi):
break
# check `(f_sync - f) % scs == 0` with tolerating fp rounding
δf = f_sync - f
q, r = divmod(δf, scs)
r_scs = r / scs
_trace('gscn %d\tf_sync %.16g (%d) δf %+.3f //scs %d %%scs %.16g·scs' % (gscn, f_sync, nr.get_nrarfcn(f_sync), δf, q, r_scs))
if abs(r_scs - round(r_scs)) < 1e-5:
_trace('-> %d %d' % (f_sync_arfcn, scs_khz))
return f_sync_arfcn, scs_khz
gscn += (+1 if δf > 0 else -1)
raise ValueError('dl2ssb %r %s: cannot find SSB frequency that is both on GSR and aligns from dl modulo SSB SCS of the given band' % (dl_nr_arfcn, band))
# frequency returns frequency corresponding to DL or UL NR-ARFCN.
def frequency(nrarfcn): # -> freq (MHz)
return nr.get_frequency(nrarfcn)
_debug = False
def _trace(*argv):
if _debug:
print(*argv)
# Copyright (C) 2023 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
from xlte.nrarfcn import frequency, dl2ul, ul2dl, dl2ssb
from xlte.nrarfcn import nr
from pytest import raises
# verify nrarfcn-related calculations wrt several points.
def test_nrarfcn():
def _(band, dl_nr_arfcn, ul_nr_arfcn, fdl, ful, rf_mode, ssb_nr_arfcn, max_ssb_scs_khz):
assert rf_mode == nr.get_duplex_mode(band).upper()
if dl_nr_arfcn is not None:
assert frequency(dl_nr_arfcn) == fdl
if ul_nr_arfcn is not None:
assert dl2ul(dl_nr_arfcn, band) == ul_nr_arfcn
else:
assert rf_mode in ('FDD', 'SDL')
if rf_mode == 'FDD':
estr = 'does not have enough uplink spectrum'
if rf_mode == 'SDL':
estr = 'does not have uplink spectrum'
with raises(KeyError, match=estr):
dl2ul(dl_nr_arfcn, band)
if ul_nr_arfcn is not None:
assert frequency(ul_nr_arfcn) == ful
if dl_nr_arfcn is not None:
assert ul2dl(ul_nr_arfcn, band) == dl_nr_arfcn
else:
assert rf_mode in ('FDD', 'SUL')
if rf_mode == 'FDD':
estr = 'does not have enough downlink spectrum'
if rf_mode == 'SUL':
estr = 'does not have downlink spectrum'
with raises(KeyError, match=estr):
ul2dl(ul_nr_arfcn, band)
if dl_nr_arfcn is not None:
if not isinstance(ssb_nr_arfcn, type):
assert dl2ssb(dl_nr_arfcn, band) == (ssb_nr_arfcn, max_ssb_scs_khz)
else:
with raises(ssb_nr_arfcn):
dl2ssb(dl_nr_arfcn, band)
# band dl ul fdl ful rf_mode ssb max_ssb_scs_khz
_( 1, 428000, 390000, 2140, 1950, 'FDD', 427970, 15)
_( 2, 396000, 380000, 1980, 1900, 'FDD', 396030, 15)
_( 5, 176300, 167300, 881.5, 836.5, 'FDD', 176210, 30)
_( 5, 176320, 167320, 881.6, 836.6, 'FDD', 176410, 30)
_( 7, 526000, 502000, 2630, 2510, 'FDD', 526090, 15)
_( 29, 144500, None, 722.5, None, 'SDL', 144530, 15)
_( 39, 378000, 378000, 1890, 1890, 'TDD', 378030, 30) # % 30khz = 0
_( 39, 378003, 378003, 1890.015, 1890.015, 'TDD', 378030, 15) # % 15khz = 0 % 30khz ≠ 0
_( 38, 520000, 520000, 2600, 2600, 'TDD', 520090, 30)
_( 41, 523020, 523020, 2615.1, 2615.1, 'TDD', 522990, 30) # % 30khz = 0
_( 41, 523023, 523023, 2615.115, 2615.115, 'TDD', 522990, 15) # % 15khz = 0 % 30khz ≠ 0
_( 66, 431000, 351000, 2155, 1755, 'FDD', 431090, 30)
_( 66, 437000, None, 2185, None, 'FDD', 437090, 30) # NOTE in n66 range(dl) > range(ul)
_( 78, 632628, 632628, 3489.42, 3489.42, 'TDD', 632640, 30)
_( 91, 285900, 166900, 1429.5, 834.5, 'FDD', 285870, 15)
_( 91, None, 172400, None, 862, 'FDD', None, None) # NOTE in n91 range(dl) < range(ul)
_( 80, None, 342000, None, 1710, 'SUL', None, None)
_(257, 2079167, 2079167, 28000.08, 28000.08, 'TDD', 2079163, 240) # FR2-1
_(257, 2079169, 2079169, 28000.20, 28000.20, 'TDD', 2079163, 120) # FR2-1 % 240khz ≠ 0
_(263, 2680027, 2680027, 64051.68, 64051.68, 'TDD', 2679931, 960) # FR2-2
_(263, 2680003, 2680003, 64050.24, 64050.24, 'TDD', 2679931, 480) # FR2-2 % 960khz ≠ 0
_(263, 2679991, 2679991, 64049.52, 64049.52, 'TDD', 2679931, 120) # FR2-2 % 480khz ≠ 0
# some dl points not on ΔFraster -> ssb cannot be found
_( 78, 632629, 632629, 3489.435, 3489.435, 'TDD', ValueError, None)
_(257, 2079168, 2079168, 28000.14, 28000.14, 'TDD', ValueError, None)
......@@ -90,6 +90,7 @@ setup(
'websocket-client',
'pygolang',
'numpy',
'nrarfcn',
],
extras_require = {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment