Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: calculate DL AbsoluteFrequencyPointA #80

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/du_parameters/afrcn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright 2025 Canonical Ltd.
# See LICENSE file for licensing details.

"""ARFCN calculations for different frequencies."""

from dataclasses import dataclass
from typing import Optional, Union


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

lower: float
upper: float
freq_grid: float
freq_offset: float
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)


def freq_to_arfcn(frequency: Union[int, float]) -> Optional[int]:
"""Calculate Absolute Radio Frequency Channel Number (ARFCN).

Args:
frequency (float or int): Center frequency in MHz.

Returns:
arfcn: int if successful, else None

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

raise ValueError(f"Frequency {frequency} is out of supported range.")
53 changes: 53 additions & 0 deletions src/du_parameters/dl_point_a.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Copyright 2025 Canonical Ltd.
# See LICENSE file for licensing details.

"""dl_absoluteFrequencyPointA calculations."""

import logging
from typing import Optional, Union

from src.du_parameters.afrcn import freq_to_arfcn

logger = logging.getLogger(__name__)


def get_dl_absolute_freq_point_a(
center_freq: Union[int, float], bandwidth: Union[int, float]
) -> Optional[int]:
"""Calculate dl_absoluteFrequencyPointA using center frequency and bandwidth.

Args:
center_freq (float or int): Center frequency in MHz.
bandwidth (float or int): (MHz)

Returns:
dl_point_a (int): if successful, else None.
"""
try:
if not isinstance(center_freq, (int, float)):
logger.error(f"Frequency {center_freq} is not a numeric value.")
return None

if not isinstance(bandwidth, (int, float)):
logger.error(f"Bandwidth {bandwidth} is not a numeric value.")
return None

try:
# Get lowest frequency and convert to ARFCN
lowest_freq = center_freq - (bandwidth / 2)
dl_point_a = freq_to_arfcn(lowest_freq)
except (ValueError, TypeError):
logger.error(
f"Failed to calculate dl_absoluteFrequencyPointA using frequency: "
f"{center_freq} and bandwidth: {bandwidth}"
)
return None

return dl_point_a

except Exception as e:
logger.error(
f"Error in getting dl_absoluteFrequencyPointA using frequency:"
f" {center_freq} and bandwidth: {bandwidth}: {e}"
)
return None
226 changes: 226 additions & 0 deletions src/du_parameters/ssb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
# Copyright 2025 Canonical Ltd.
# See LICENSE file for licensing details.

"""Synchronization Signal block calculations for different frequencies."""

import logging
from abc import ABC
from typing import Optional, Union

from src.du_parameters.afrcn import freq_to_arfcn

logger = logging.getLogger(__name__)


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

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

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

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.

Returns:
gscn: int

Raises:
ValueError: If the MULTIPLICATION_FACTOR is 0 or N is out of range.
"""
if self.MULTIPLICATION_FACTOR == 0:
raise ValueError(f"{self.__class__.__name__}.MULTIPLICATION_FACTOR cannot be zero.")

n = (self.frequency - self.BASE_FREQ) / self.MULTIPLICATION_FACTOR # type: ignore

if self.MIN_N <= n <= self.MAX_N:
return int(n + self.BASE_GSCN)

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:
"""Calculate frequency according to GSCN value.

Args:
gscn: int

Returns:
frequency: float(MHz)

Raises:
ValueError: If N is out of range.
"""
n = gscn - self.BASE_GSCN

if self.MIN_N <= n <= self.MAX_N:
return 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}).")


class HighFrequencySSB(BaseSSB):
"""Perform GSCN calculations for high level frequencies.

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


class MidFrequencySSB(BaseSSB):
"""Perform GSCN calculations for mid level frequencies.

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


class LowFrequencySSB(BaseSSB):
"""Perform GSCN calculations for low frequencies.

M is a scaling factor used to adjust how frequencies are divided and mapped in specific ranges
The default value of M is 3.
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

def freq_to_gscn(self) -> int:
"""Calculate GSCN according to frequency.

Returns:
gscn: int

Raises:
ValueError: If the MULTIPLICATION_FACTOR is 0 or N is out of range.
"""
if self.MULTIPLICATION_FACTOR == 0:
raise ValueError(f"{self.__class__.__name__}.MULTIPLICATION_FACTOR cannot be zero.")

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)

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:
"""Calculate frequency according to GSCN value.

Args:
gscn: int

Returns:
frequency: float(MHz)
"""
n = (gscn - (self.M - 3) / 2) / 3
return n * self.MULTIPLICATION_FACTOR + self.M * self.M_MULTIPLICATION_FACTOR


def get_frequency_instance(frequency: Union[float, int]) -> BaseSSB:
"""Create the instance according to appropriate frequency range class.

Args:
frequency: float or int

Returns:
BaseSSB: instance

Raises:
ValueError: If frequency is out of supported range
TypeError: If frequency is not a numeric value
"""
if not isinstance(frequency, (int, float)):
raise TypeError(f"Frequency {frequency} is not a numeric value.")

ranges = [
((0, 3000), LowFrequencySSB),
((3000, 24250), MidFrequencySSB),
((24250, 100000), HighFrequencySSB),
]

for (range_min, range_max), frequency_cls in ranges:
if range_min <= frequency < range_max:
return frequency_cls(frequency)

raise ValueError(f"Frequency {frequency} is out of supported range.")


def get_absolute_frequency_ssb(center_freq: Union[int, float]) -> Optional[int]:
"""Calculate absolute frequency for ssb using center frequency.

Args:
center_freq (float or int): Center frequency in MHz.

Returns:
arfcn (int): if successful, else None.
"""
try:
try:
# Get frequency instance
frequency_instance = get_frequency_instance(center_freq)
except (ValueError, TypeError):
logger.error(f"Failed to create a frequency instance for center_freq={center_freq}")
return None

try:
# Calculate GSCN
gcsn = frequency_instance.freq_to_gscn()
except ValueError:
logger.error(f"Failed to calculate GSCN for center_freq={center_freq}")
return None

try:
# Convert GSCN to frequency
frequency_from_gcsn = frequency_instance.gscn_to_freq(gcsn)
except ValueError:
logger.error(f"Failed to calculate frequency using gcsn={gcsn}")
return None

try:
# Convert frequency to ARFCN
arfcn = 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

except Exception as e:
logger.error(
f"Error in getting absolute frequency for ssb using center_freq={center_freq}: {e}"
)
return None
52 changes: 52 additions & 0 deletions tests/unit/test_arfcn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
# Copyright 2025 Canonical Ltd.
# See LICENSE file for licensing details.

import pytest

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


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)
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
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
expected_arfcn = HIGH.arfcn_offset + ((frequency - HIGH.freq_offset) / HIGH.freq_grid)
assert freq_to_arfcn(frequency) == int(expected_arfcn)

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

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
Loading
Loading