Skip to content
Merged
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
12 changes: 6 additions & 6 deletions sharc/parameters/imt/parameters_antenna_imt.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ class ParametersAntennaImt(ParametersBase):
# PS: it isn't implemented for UEs
# and current implementation doesn't make sense for UEs
horizontal_beamsteering_range: tuple[float |
int, float | int] = (-180., 180.)
vertical_beamsteering_range: tuple[float | int, float | int] = (0., 180.)
int, float | int] = (-180., 179.9999)
vertical_beamsteering_range: tuple[float | int, float | int] = (0., 179.9999)

# Mechanical downtilt [degrees].
# PS: downtilt doesn't make sense on UE's
Expand Down Expand Up @@ -174,11 +174,11 @@ def validate(self, ctx: str):
f"Invalid {ctx}.horizontal_beamsteering_range={self.horizontal_beamsteering_range}\n"
"The second value must be bigger than the first"
)
if not all(map(lambda x: x >= -180. and x <= 180.,
if not all(map(lambda x: x >= -180. and x < 180.,
self.horizontal_beamsteering_range)):
raise ValueError(
f"Invalid {ctx}.horizontal_beamsteering_range={self.horizontal_beamsteering_range}\n"
"Horizontal beamsteering limit angles must be in the range [-180, 180]"
"Horizontal beamsteering limit angles must be in the range [-180, 180)"
)

if isinstance(self.vertical_beamsteering_range, list):
Expand All @@ -199,11 +199,11 @@ def validate(self, ctx: str):
f"Invalid {ctx}.vertical_beamsteering_range={self.vertical_beamsteering_range}\n"
"The second value must be bigger than the first"
)
if not all(map(lambda x: x >= 0. and x <= 180.,
if not all(map(lambda x: x >= 0. and x < 180.,
self.vertical_beamsteering_range)):
raise ValueError(
f"Invalid {ctx}.vertical_beamsteering_range={self.vertical_beamsteering_range}\n"
"vertical beamsteering limit angles must be in the range [0, 180]"
"vertical beamsteering limit angles must be in the range [0, 180)"
)

def get_normalization_data_if_needed(self):
Expand Down
6 changes: 3 additions & 3 deletions sharc/parameters/imt/parameters_imt.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,11 @@ class ParametersUE(ParametersBase):

def validate(self, ctx: str):
"""Validate the UE antenna beamsteering range parameters."""
if self.antenna.array.horizontal_beamsteering_range != (-180., 180.)\
or self.antenna.array.vertical_beamsteering_range != (0., 180.):
if self.antenna.array.horizontal_beamsteering_range != (-180., 179.9999)\
or self.antenna.array.vertical_beamsteering_range != (0., 179.9999):
raise NotImplementedError(
"UE antenna beamsteering limit has not been implemented. Default values of\n"
"horizontal = (-180., 180.), vertical = (0., 180.) should not be changed")
"horizontal = (-180., 179.9999), vertical = (0., 179.9999) should not be changed")

ue: ParametersUE = field(default_factory=ParametersUE)

Expand Down
12 changes: 10 additions & 2 deletions sharc/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from sharc.station_manager import StationManager
from sharc.results import Results
from sharc.propagation.propagation_factory import PropagationFactory
from sharc.support.sharc_utils import wrap2_180, clip_angle


class Simulation(ABC, Observable):
Expand Down Expand Up @@ -482,6 +483,8 @@ def select_ue(self, random_number_gen: np.random.RandomState):
self.ue, )

bs_active = np.where(self.bs.active)[0]

assert np.all((-180 <= self.bs.azimuth) & (self.bs.azimuth <= 180)), "BS azimuth angles should be in [-180, 180] range"
for bs in bs_active:
# select K UE's among the ones that are connected to BS
random_number_gen.shuffle(self.link[bs])
Expand All @@ -494,9 +497,14 @@ def select_ue(self, random_number_gen: np.random.RandomState):
# add beam to BS antennas

# limit beamforming angle
bs_beam_phi = np.clip(
beam_h_min, beam_h_max = wrap2_180(
self.parameters.imt.bs.antenna.array.horizontal_beamsteering_range + self.bs.azimuth[bs]
)

bs_beam_phi = clip_angle(
self.bs_to_ue_phi[bs, ue],
*(self.parameters.imt.bs.antenna.array.horizontal_beamsteering_range + self.bs.azimuth[bs])
beam_h_min,
beam_h_max,
)

bs_beam_theta = np.clip(
Expand Down
3 changes: 2 additions & 1 deletion sharc/station_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
from sharc.mask.spectral_mask_3gpp import SpectralMask3Gpp
from sharc.mask.spectral_mask_mss import SpectralMaskMSS
from sharc.support.sharc_geom import GeometryConverter
from sharc.support.sharc_utils import wrap2_180


class StationFactory(object):
Expand Down Expand Up @@ -119,7 +120,7 @@ def generate_imt_base_stations(
else:
imt_base_stations.height = param.bs.height * np.ones(num_bs)

imt_base_stations.azimuth = topology.azimuth
imt_base_stations.azimuth = wrap2_180(topology.azimuth)
imt_base_stations.active = random_number_gen.rand(
num_bs,
) < param.bs.load_probability
Expand Down
43 changes: 43 additions & 0 deletions sharc/support/sharc_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,49 @@ def is_float(s: str) -> bool:
return False


def wrap2_180(a):
"""
Wraps angles to the [-180, 180) range
"""
return (a + 180) % 360 - 180


def angular_dist(a1, a2):
"""
Returns smallest angular distance between 2 angles
in the [0, 180] range
"""
return np.abs(wrap2_180(a1 - a2))


def clip_angle(a, a_min, a_max):
"""
Returns `a` if it is inside the angle range defined by [`a_min`, `a_max`]
otherwise returns the closest bound, be it `a_min` or `a_max`
It is assumed all angles passes are already in [-180, 180] range
"""
outside_rng = False
# NOTE: angle limits such as -180 + [-60, 60]
# should be passed as wrapped around [120, -120]
# So it is important to check for this case
if a_min > a_max:
if a > a_max and a < a_min:
outside_rng = True
else:
# Normal interval
if a < a_min or a > a_max:
outside_rng = True

# always clip to the closest bound
if outside_rng:
if angular_dist(a, a_min) < angular_dist(a, a_max):
return a_min
else:
return a_max

return a


def to_scalar(x):
"""Convert a numpy scalar or array to a Python scalar if possible."""
if isinstance(x, np.ndarray):
Expand Down
4 changes: 2 additions & 2 deletions tests/parameters/test_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ def test_parameters_imt(self):
self.assertEqual(
self.parameters.imt.ue.antenna.array.horizontal_beamsteering_range,
(-180.,
180.))
179.9999))
self.assertEqual(
self.parameters.imt.ue.antenna.array.vertical_beamsteering_range, (0., 180.))
self.parameters.imt.ue.antenna.array.vertical_beamsteering_range, (0., 179.9999))
self.assertEqual(self.parameters.imt.ue.k, 3)
self.assertEqual(self.parameters.imt.ue.k_m, 1)
self.assertEqual(self.parameters.imt.ue.indoor_percent, 5.0)
Expand Down
65 changes: 65 additions & 0 deletions tests/test_sharc_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import unittest

from sharc.support.sharc_utils import clip_angle


class StationTest(unittest.TestCase):
"""Unit tests for some utilities."""

def setUp(self):
"""
setup
"""
pass

def test_clip_angle(self):
"""
Testing in range for non wrapping around angles
"""
for a in [90, 100, 180, -180, -135.01]:
self.assertEqual(clip_angle(a, 0, 90), 90)
for a in [-134.99, -90, -45, 0]:
self.assertEqual(clip_angle(a, 0, 90), 0)
for a in [0, 45, 90]:
self.assertEqual(clip_angle(a, 0, 90), a)

for a in [-90, -80, 0, 44.99]:
self.assertEqual(clip_angle(a, -180, -90), -90)
for a in [45.01, 90, 135, 180, -180]:
self.assertEqual(clip_angle(a, -180, -90), -180)
for a in [-180, -135, -90]:
self.assertEqual(clip_angle(a, -180, -90), a)

for a in [180, -180, -135, -90.01]:
self.assertEqual(clip_angle(a, 0, 180), 180)
for a in [-89.99, -45, 0]:
self.assertEqual(clip_angle(a, 0, 180), 0)
for a in [0, 45, 90, 135, 180]:
self.assertEqual(clip_angle(a, 0, 180), a)

"""
Testing in range for wrapping around angles
"""
for a in [-90, -80, 0, 44.99]:
self.assertEqual(clip_angle(a, 180, -90), -90)
for a in [45.01, 90, 135, 180, 180]:
self.assertEqual(clip_angle(a, 180, -90), 180)
for a in [-180, -135, -90]:
self.assertEqual(clip_angle(a, 180, -90), a)

for a in [-180, -135, -90.01]:
self.assertEqual(clip_angle(a, 0, -180), -180)
for a in [-89.99, -45, 0]:
self.assertEqual(clip_angle(a, 0, -180), 0)
for a in [0, 45, 90, 135, -180]:
self.assertEqual(clip_angle(a, 0, -180), a)

self.assertEqual(clip_angle(91, 180, 0), 180)
self.assertEqual(clip_angle(89, 180, 0), 0)

self.assertEqual(clip_angle(91, -180, 0), -180)
self.assertEqual(clip_angle(89, -180, 0), 0)


if __name__ == '__main__':
unittest.main()
2 changes: 1 addition & 1 deletion tests/test_simulation_uplink.py
Original file line number Diff line number Diff line change
Expand Up @@ -868,7 +868,7 @@ def test_beamforming_gains(self):
# Physical pointing angles
self.assertEqual(self.simulation.bs.antenna[0].azimuth, 0)
self.assertEqual(self.simulation.bs.antenna[0].elevation, -10)
self.assertEqual(self.simulation.bs.antenna[1].azimuth, 180)
self.assertEqual(self.simulation.bs.antenna[1].azimuth, -180)
self.assertEqual(self.simulation.bs.antenna[0].elevation, -10)

# Change UE pointing
Expand Down