Skip to content

Commit

Permalink
Use decimal.Decimal and values in Hz
Browse files Browse the repository at this point in the history
Signed-off-by: gatici <[email protected]>
  • Loading branch information
gatici committed Jan 30, 2025
1 parent 56e101c commit b21652a
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 197 deletions.
59 changes: 39 additions & 20 deletions src/du_parameters/afrcn.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,67 @@
# Copyright 2025 Canonical Ltd.
# See LICENSE file for licensing details.

"""ARFCN calculations for different frequencies."""
"""Calculate ARFCN for given 5G RF center frequency."""

from dataclasses import dataclass
from typing import Optional, Union
from decimal import Decimal, getcontext

getcontext().prec = 28
KHZ = Decimal("1000") # HZ
MHZ = Decimal("1000000") # HZ


@dataclass
class ARFCNRange:
"""ARFCN range class."""

lower: float
upper: float
freq_grid: float
freq_offset: float
lower_frequency: int | Decimal # HZ
upper_frequency: int | Decimal # HZ
freq_grid: int | Decimal # HZ
freq_offset: int | Decimal # HZ
arfcn_offset: int


LOW = ARFCNRange(0, 3000, 0.005, 0, 0)
MID = ARFCNRange(3000, 24250, 0.015, 3000, 600000)
HIGH = ARFCNRange(24250, 100000, 0.06, 24250, 2016667)
LOW = ARFCNRange(Decimal("0"), Decimal("3000") * MHZ, Decimal("5") * KHZ, 0, 0)
MID = ARFCNRange(
Decimal("3000") * MHZ,
Decimal("24250") * MHZ,
Decimal("15") * KHZ,
Decimal("3000") * MHZ,
600000,
)
HIGH = ARFCNRange(
Decimal("24250") * MHZ,
Decimal("100000") * MHZ,
Decimal("60") * KHZ,
Decimal("24250") * MHZ,
2016667,
)


def freq_to_arfcn(frequency: Union[int, float]) -> Optional[int]:
def freq_to_arfcn(frequency: int) -> int:
"""Calculate Absolute Radio Frequency Channel Number (ARFCN).
Args:
frequency (float or int): Center frequency in MHz.
frequency: (int) Center frequency in Hz.
Returns:
arfcn: int if successful, else None
arfcn: int
Raises:
ValueError: If the FREQ_GRID is 0 or frequency is out of range.
"""
if not isinstance(frequency, (int, float)):
raise TypeError(f"Frequency {frequency} is not a numeric value.")

ranges = [LOW, MID, HIGH]

frequency = Decimal(frequency) # type: ignore

for r in ranges:
if r.lower <= frequency < r.upper:
if r.freq_grid == 0:
raise ValueError("FREQ_GRID cannot be zero.")
return int(r.arfcn_offset + ((frequency - r.freq_offset) / r.freq_grid))
if Decimal(r.lower_frequency) <= frequency < Decimal(r.upper_frequency):
freq_offset = Decimal(r.freq_offset)
freq_grid = Decimal(r.freq_grid)
arfcn_offset = Decimal(r.arfcn_offset)
return int(arfcn_offset + ((frequency - freq_offset) / freq_grid))

raise ValueError(f"Frequency {frequency} is out of supported range.")
raise ValueError(
f"Frequency {frequency} is out of supported range. Supported ranges are: {ranges} Hz"
)
107 changes: 58 additions & 49 deletions src/du_parameters/ssb.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,49 @@
# Copyright 2025 Canonical Ltd.
# See LICENSE file for licensing details.

"""Synchronization Signal block calculations for different frequencies."""
"""Calculate Synchronization Signal Block frequency for given 5G RF center frequency."""

import logging
from abc import ABC
from typing import Optional, Union
from decimal import Decimal, getcontext

from src.du_parameters.afrcn import freq_to_arfcn

logger = logging.getLogger(__name__)

getcontext().prec = 28
KHZ = Decimal("1000") # Hz
MHZ = Decimal("1000000") # Hz


class BaseSSB(ABC):
"""Base class for calculations in different frequencies."""

RANGE = (0, 0)
BASE_GSCN = 0
MULTIPLICATION_FACTOR = 0
BASE_FREQ = 0
MAX_N = 0
MIN_N = 0
RANGE = (Decimal("0"), Decimal("0"))
BASE_GSCN = Decimal("0")
MULTIPLICATION_FACTOR = Decimal("0")
BASE_FREQ = Decimal("0")
MAX_N = Decimal("0")
MIN_N = Decimal("0")

def __init__(self, frequency: Union[int, float]):
def __init__(self, frequency: int | Decimal):
"""Initialize frequency with validation."""
if self.__class__ == BaseSSB:
raise NotImplementedError("BaseFrequency cannot be instantiated directly.")

if not isinstance(frequency, (int, float)):
if not isinstance(frequency, int | Decimal):
raise TypeError(f"Frequency {frequency} is not a numeric value.")

if isinstance(frequency, int):
frequency = Decimal(frequency)

self.frequency = frequency

if not (self.RANGE[0] <= frequency < self.RANGE[1]):
raise ValueError(
f"Frequency {frequency} is out of range for {self.__class__.__name__}."
)

self.frequency: Union[int, float] = frequency

def freq_to_gscn(self) -> int:
"""Calculate GSCN according to frequency.
Expand All @@ -56,22 +63,22 @@ def freq_to_gscn(self) -> int:

raise ValueError(f"Value of N: {n} is out of supported range ({self.MIN_N}-{self.MAX_N}).")

def gscn_to_freq(self, gscn: int) -> float:
def gscn_to_freq(self, gscn: int) -> int:
"""Calculate frequency according to GSCN value.
Args:
gscn: int
Returns:
frequency: float(MHz)
frequency: int (Hz)
Raises:
ValueError: If N is out of range.
"""
n = gscn - self.BASE_GSCN
n = Decimal(gscn) - self.BASE_GSCN

if self.MIN_N <= n <= self.MAX_N:
return n * self.MULTIPLICATION_FACTOR + self.BASE_FREQ
return int(n * self.MULTIPLICATION_FACTOR + self.BASE_FREQ)

raise ValueError(f"Value of N: {n} is out of supported range ({self.MIN_N}-{self.MAX_N}).")

Expand All @@ -82,12 +89,12 @@ class HighFrequencySSB(BaseSSB):
The value of N must remain within a specified valid range, depending on the frequency.
"""

RANGE = (24250, 100000)
MULTIPLICATION_FACTOR = 17.28 # MHz
BASE_FREQ = 24250.08 # MHz
MAX_N = 4383
MIN_N = 0
BASE_GSCN = 22256
RANGE = (Decimal("24250") * MHZ, Decimal("100000") * MHZ) # Hz
MULTIPLICATION_FACTOR = Decimal("17.28") * MHZ # Hz
BASE_FREQ = Decimal("24250.08") * MHZ # Hz
MAX_N = Decimal("4383")
MIN_N = Decimal("0")
BASE_GSCN = Decimal("22256")


class MidFrequencySSB(BaseSSB):
Expand All @@ -96,12 +103,12 @@ class MidFrequencySSB(BaseSSB):
The value of N must remain within a specified valid range, depending on the frequency.
"""

RANGE = (3000, 24250)
MULTIPLICATION_FACTOR = 1.44 # MHz
BASE_FREQ = 3000 # MHz
MAX_N = 14756
MIN_N = 0
BASE_GSCN = 7499
RANGE = (Decimal("3000") * MHZ, Decimal("24250") * MHZ) # Hz
MULTIPLICATION_FACTOR = Decimal("1.44") * MHZ # Hz
BASE_FREQ = Decimal("3000") * MHZ # Hz
MAX_N = Decimal("14756")
MIN_N = Decimal("0")
BASE_GSCN = Decimal("7499")


class LowFrequencySSB(BaseSSB):
Expand All @@ -112,12 +119,12 @@ class LowFrequencySSB(BaseSSB):
The value of N must remain within a specified valid range, depending on the frequency.
"""

RANGE = (0, 3000)
M = 3
M_MULTIPLICATION_FACTOR = 0.05 # MHz
MULTIPLICATION_FACTOR = 1.2 # MHz
MAX_N = 2499
MIN_N = 1
RANGE = (Decimal("0"), Decimal("3000") * MHZ) # Hz
M = Decimal("3")
M_MULTIPLICATION_FACTOR = Decimal("50") * KHZ # Hz
MULTIPLICATION_FACTOR = Decimal("1200") * KHZ # Hz
MAX_N = Decimal("2499")
MIN_N = Decimal("1")

def freq_to_gscn(self) -> int:
"""Calculate GSCN according to frequency.
Expand All @@ -133,28 +140,28 @@ def freq_to_gscn(self) -> int:

n = (self.frequency - (self.M * self.M_MULTIPLICATION_FACTOR)) / self.MULTIPLICATION_FACTOR # type: ignore
if self.MIN_N <= n <= self.MAX_N:
return int((3 * n) + (self.M - 3) / 2)
return int((Decimal("3") * n) + (self.M - Decimal("3")) / Decimal("2"))

raise ValueError(f"Value of N: {n} is out of supported range ({self.MIN_N}-{self.MAX_N}).")

def gscn_to_freq(self, gscn: int) -> float:
def gscn_to_freq(self, gscn: int) -> int:
"""Calculate frequency according to GSCN value.
Args:
gscn: int
Returns:
frequency: float(MHz)
frequency: int(Hz)
"""
n = (gscn - (self.M - 3) / 2) / 3
return n * self.MULTIPLICATION_FACTOR + self.M * self.M_MULTIPLICATION_FACTOR
n = (Decimal(gscn) - (self.M - Decimal("3")) / Decimal("2")) / Decimal("3")
return int(n * self.MULTIPLICATION_FACTOR + self.M * self.M_MULTIPLICATION_FACTOR)


def get_frequency_instance(frequency: Union[float, int]) -> BaseSSB:
def get_frequency_instance(frequency: int) -> BaseSSB:
"""Create the instance according to appropriate frequency range class.
Args:
frequency: float or int
frequency: int (Hz)
Returns:
BaseSSB: instance
Expand All @@ -163,13 +170,15 @@ def get_frequency_instance(frequency: Union[float, int]) -> BaseSSB:
ValueError: If frequency is out of supported range
TypeError: If frequency is not a numeric value
"""
if not isinstance(frequency, (int, float)):
if not isinstance(frequency, int | float):
raise TypeError(f"Frequency {frequency} is not a numeric value.")

frequency = Decimal(frequency) # type: ignore

ranges = [
((0, 3000), LowFrequencySSB),
((3000, 24250), MidFrequencySSB),
((24250, 100000), HighFrequencySSB),
((Decimal("0"), Decimal("3000") * MHZ), LowFrequencySSB),
((Decimal("3000") * MHZ, Decimal("24250") * MHZ), MidFrequencySSB),
((Decimal("24250") * MHZ, Decimal("100000") * MHZ), HighFrequencySSB),
]

for (range_min, range_max), frequency_cls in ranges:
Expand All @@ -179,11 +188,11 @@ def get_frequency_instance(frequency: Union[float, int]) -> BaseSSB:
raise ValueError(f"Frequency {frequency} is out of supported range.")


def get_absolute_frequency_ssb(center_freq: Union[int, float]) -> Optional[int]:
def get_absolute_frequency_ssb(center_freq: int) -> int | None:
"""Calculate absolute frequency for ssb using center frequency.
Args:
center_freq (float or int): Center frequency in MHz.
center_freq (int): Center frequency in Hz.
Returns:
arfcn (int): if successful, else None.
Expand Down Expand Up @@ -212,12 +221,12 @@ def get_absolute_frequency_ssb(center_freq: Union[int, float]) -> Optional[int]:

try:
# Convert frequency to ARFCN
arfcn = freq_to_arfcn(frequency_from_gcsn)
absolute_freq_ssb = freq_to_arfcn(frequency_from_gcsn)
except (ValueError, TypeError):
logger.error(f"Failed to calculate ARFCN for center_freq={center_freq}")
return None

return arfcn
return absolute_freq_ssb

except Exception as e:
logger.error(
Expand Down
33 changes: 12 additions & 21 deletions tests/unit/test_arfcn.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,33 @@
# Copyright 2025 Canonical Ltd.
# See LICENSE file for licensing details.

from decimal import Decimal

import pytest

from src.du_parameters.afrcn import HIGH, LOW, MID, freq_to_arfcn

MHZ = 1000000 # HZ


class TestARFCN:
def test_freq_to_arfcn_when_low_frequency_is_given_then_arfcn_is_returned_as_integer(self):
# Frequency in low range
frequency = 1000
expected_arfcn = LOW.arfcn_offset + ((frequency - LOW.freq_offset) / LOW.freq_grid)
frequency = 1000000000 # HZ
expected_arfcn = Decimal(LOW.arfcn_offset) + (
(frequency - Decimal(LOW.freq_offset)) / Decimal(LOW.freq_grid)
) # noqa: E501
assert freq_to_arfcn(frequency) == int(expected_arfcn)

def test_freq_to_arfcn_when_mid_frequency_is_given_then_arfcn_is_returned_as_integer(self):
# Frequency in mid range
frequency = 10000
frequency = 10000 * MHZ
expected_arfcn = MID.arfcn_offset + ((frequency - MID.freq_offset) / MID.freq_grid)
assert freq_to_arfcn(frequency) == int(expected_arfcn)

def test_freq_to_arfcn_when_high_frequency_is_given_then_arfcn_is_returned_as_integer(self):
# Frequency in high range
frequency = 50000
frequency = 50000 * MHZ
expected_arfcn = HIGH.arfcn_offset + ((frequency - HIGH.freq_offset) / HIGH.freq_grid)
assert freq_to_arfcn(frequency) == int(expected_arfcn)

Expand All @@ -33,20 +39,5 @@ def test_freq_to_arfcn_when_too_low_frequency_is_given_then_value_error_is_raise

def test_freq_to_arfcn_when_too_high_frequency_is_given_then_value_error_is_raised(self):
with pytest.raises(ValueError) as exc_info:
freq_to_arfcn(2016668)
assert "Frequency 2016668 is out of supported range." in str(exc_info.value)

def test_freq_to_arfcn_when_non_numeric_input_is_given_then_type_error_is_raised(self):
with pytest.raises(TypeError) as exc_info:
freq_to_arfcn("not_a_number") # type: ignore
assert "Frequency not_a_number is not a numeric value." in str(exc_info.value)

def test_freq_to_arfcn_when_freq_grid_is_zero_then_value_error_is_raised(self):
custom_range = LOW
# Set freq_grid to zero for testing
custom_range.freq_grid = 0
with pytest.raises(ValueError) as exc_info:
freq_to_arfcn(custom_range.lower + 1)
assert "FREQ_GRID cannot be zero." in str(exc_info.value)
# Reset to the initial value not to have side effects
custom_range.freq_grid = 0.005
freq_to_arfcn(2016668 * MHZ)
assert "Frequency 2016668000000 is out of supported range." in str(exc_info.value)
Loading

0 comments on commit b21652a

Please sign in to comment.