From e4cb369c37a654da235bcf3fca1067fa1d7f9ac1 Mon Sep 17 00:00:00 2001 From: artistrea Date: Thu, 11 Sep 2025 17:01:14 -0300 Subject: [PATCH 01/77] feat(station): single_space_station can now point toward arbitrary lat,lon,alt --- sharc/input/parameters.yaml | 11 ++- .../parameters_single_space_station.py | 12 ++- sharc/station_factory.py | 91 +++++++++---------- tests/test_station_factory.py | 74 ++++++++++++++- 4 files changed, 136 insertions(+), 52 deletions(-) diff --git a/sharc/input/parameters.yaml b/sharc/input/parameters.yaml index 8edf9877f..da66eb265 100644 --- a/sharc/input/parameters.yaml +++ b/sharc/input/parameters.yaml @@ -914,11 +914,18 @@ single_space_station: ########################################################################### # earth station lat long [deg] es_long_deg: 1 + # Where to point antenna if azimuth|elevation use "POINTING_AT_LAT_LONG_ALT" + # latitude [deg] + pointing_at_lat: 0. + # longitude [deg] + pointing_at_long: 0. + # altitude [m] + pointing_at_alt: 0. ########################################################################### # Antenna azimuth angle [degrees] azimuth: ########################################################################### - # Type of azimuth. May be "FIXED" or "POINTING_AT_IMT" + # Type of azimuth. May be "FIXED", "POINTING_AT_IMT" or "POINTING_AT_LAT_LONG_ALT" type: POINTING_AT_IMT ########################################################################### # Arbitrary value of azimuth when type == "FIXED" [deg] @@ -927,7 +934,7 @@ single_space_station: # Antenna elevation angle [degrees] elevation: ########################################################################### - # Type of elevation. May be "FIXED" or "POINTING_AT_IMT" + # Type of elevation. May be "FIXED", "POINTING_AT_IMT" or "POINTING_AT_LAT_LONG_ALT" type: POINTING_AT_IMT ########################################################################### # Arbitrary value of elevation when type == "FIXED" [deg] diff --git a/sharc/parameters/parameters_single_space_station.py b/sharc/parameters/parameters_single_space_station.py index 267f87dd3..b1bacfbec 100644 --- a/sharc/parameters/parameters_single_space_station.py +++ b/sharc/parameters/parameters_single_space_station.py @@ -57,13 +57,21 @@ class SpaceStationGeometry(ParametersBase): es_long_deg: float = -47.882778 es_lat_deg: float = -15.793889 + # Used if azimuth or elevation are POINTING_AT_LAT_LONG_ALT + # [deg] + pointing_at_lat: typing.Optional[float] = None + # [deg] + pointing_at_long: typing.Optional[float] = None + # [m] + pointing_at_alt: typing.Optional[float] = None + @dataclass class PointingParam(ParametersBase): """ Defines pointing parameters for the space station geometry. """ - __EXISTING_TYPES = ["FIXED", "POINTING_AT_IMT"] - type: typing.Literal["FIXED", "POINTING_AT_IMT"] = None + __EXISTING_TYPES = ["FIXED", "POINTING_AT_IMT", "POINTING_AT_LAT_LONG_ALT"] + type: typing.Literal["FIXED", "POINTING_AT_IMT", "POINTING_AT_LAT_LONG_ALT"] = None fixed: float = None def validate(self, ctx): diff --git a/sharc/station_factory.py b/sharc/station_factory.py index 22c0bae14..9824e47ee 100644 --- a/sharc/station_factory.py +++ b/sharc/station_factory.py @@ -746,15 +746,13 @@ def generate_system( @staticmethod def generate_single_space_station( param: ParametersSingleSpaceStation, - simplify_dist_to_y=True): + ): """Create a single space station (satellite) based on the provided parameters. Parameters ---------- param : ParametersSingleSpaceStation Parameters for the single space station. - simplify_dist_to_y : bool, optional - If True (default), places the satellite only on the y axis. Returns ------- @@ -765,52 +763,36 @@ def generate_single_space_station( space_station.station_type = StationType.SINGLE_SPACE_STATION space_station.is_space_station = True - # now we set the coordinates according to - # ITU-R P619-1, Attachment A - - # calculate distances to the centre of the Earth - dist_sat_centre_earth_km = ( - EARTH_RADIUS + param.geometry.altitude) / 1000 - dist_imt_centre_earth_km = ( - EARTH_RADIUS + param.geometry.es_altitude - ) / 1000 - - # calculate Cartesian coordinates of satellite, with origin at centre - # of the Earth - sat_lat_rad = param.geometry.location.fixed.lat_deg * np.pi / 180. - imt_long_diff_rad = (param.geometry.location.fixed.long_deg - - param.geometry.es_long_deg) * np.pi / 180. - x1 = dist_sat_centre_earth_km * \ - np.cos(sat_lat_rad) * np.cos(imt_long_diff_rad) - y1 = dist_sat_centre_earth_km * \ - np.cos(sat_lat_rad) * np.sin(imt_long_diff_rad) - z1 = dist_sat_centre_earth_km * np.sin(sat_lat_rad) - - # rotate axis and calculate coordinates with origin at IMT system - imt_lat_rad = param.geometry.es_lat_deg * np.pi / 180. - space_station.x = np.array( - [x1 * np.sin(imt_lat_rad) - z1 * np.cos(imt_lat_rad)], - ) * 1000 - space_station.y = np.array([y1]) * 1000 - space_station.height = np.array([ - ( - z1 * np.sin(imt_lat_rad) + x1 * np.cos(imt_lat_rad) - - dist_imt_centre_earth_km - ) * 1000, - ]) + geoconv = GeometryConverter() + geoconv.set_reference( + param.geometry.es_lat_deg, + param.geometry.es_long_deg, + param.geometry.es_altitude, + ) + x, y, z = geoconv.convert_lla_to_transformed_cartesian( + param.geometry.location.fixed.lat_deg, + param.geometry.location.fixed.long_deg, + param.geometry.altitude, + ) - # putting on y axis - if simplify_dist_to_y: - space_station.y = np.sqrt( - space_station.x * - space_station.x + - space_station.y * - space_station.y) - space_station.x = np.zeros_like(space_station.x) + space_station.x = x + space_station.y = y + space_station.z = z + # TODO: put actual altitude instead of z on station.height + space_station.height = z if param.geometry.azimuth.type == "POINTING_AT_IMT": space_station.azimuth = np.rad2deg( np.arctan2(-space_station.y, -space_station.x)) + elif param.geometry.azimuth.type == "POINTING_AT_LAT_LONG_ALT": + px, py, pz = geoconv.convert_lla_to_transformed_cartesian( + param.geometry.pointing_at_lat, + param.geometry.pointing_at_long, + param.geometry.pointing_at_alt, + ) + + space_station.azimuth = np.rad2deg( + np.arctan2(py - space_station.y, px - space_station.x)) elif param.geometry.azimuth.type == "FIXED": space_station.azimuth = param.geometry.azimuth.fixed else: @@ -818,17 +800,32 @@ def generate_single_space_station( f"Did not recognize azimuth type of { param.geometry.azimuth.type}") - if param.geometry.azimuth.type == "POINTING_AT_IMT": + if param.geometry.elevation.type == "POINTING_AT_IMT": gnd_elev = np.rad2deg( np.arctan2( - space_station.height, + space_station.z, np.sqrt( space_station.y * space_station.y + space_station.x * space_station.x))) space_station.elevation = -gnd_elev - elif param.geometry.azimuth.type == "FIXED": + elif param.geometry.elevation.type == "POINTING_AT_LAT_LONG_ALT": + px, py, pz = geoconv.convert_lla_to_transformed_cartesian( + param.geometry.pointing_at_lat, + param.geometry.pointing_at_long, + param.geometry.pointing_at_alt, + ) + dy = py - space_station.y + dx = px - space_station.x + dz = pz - space_station.z + + gnd_elev = np.rad2deg( + np.arctan2( + dz, + np.sqrt(dy * dy + dx * dx))) + space_station.elevation = gnd_elev + elif param.geometry.elevation.type == "FIXED": space_station.elevation = param.geometry.elevation.fixed else: raise ValueError( diff --git a/tests/test_station_factory.py b/tests/test_station_factory.py index 0453ef9b3..5b212d30b 100644 --- a/tests/test_station_factory.py +++ b/tests/test_station_factory.py @@ -13,6 +13,8 @@ from sharc.station_factory import StationFactory from sharc.topology.topology_ntn import TopologyNTN from sharc.parameters.parameters_single_space_station import ParametersSingleSpaceStation +from sharc.station_manager import StationManager +from sharc.satellite.ngso.constants import EARTH_RADIUS_M class StationFactoryTest(unittest.TestCase): @@ -131,7 +133,7 @@ def test_generate_single_space_station(self): param.validate() # experimental from simulator - max_gso_fov = 81.30784 + max_gso_fov = 81.299501 def get_ground_elevation(ss): return np.rad2deg( @@ -178,6 +180,76 @@ def get_ground_elevation(ss): npt.assert_almost_equal(get_ground_elevation(space_station), 0, 5) npt.assert_almost_equal(space_station.height, 0, 0) + def test_single_space_station_pointing(self): + """Basic test for space station generation.""" + + param = ParametersSingleSpaceStation() + # just passing required parameters: + param.frequency = 8000 + param.bandwidth = 100 + param.channel_model = "P619" + param.tx_power_density = -200 + param.geometry.es_altitude = 0 + param.geometry.azimuth.fixed = 0 + param.antenna.pattern = "OMNI" + param.antenna.gain = 10 + + param.geometry.location.type = "FIXED" + param.geometry.altitude = 35786000.0 + param.geometry.es_lat_deg = 0 + param.geometry.es_long_deg = 0 + param.geometry.es_altitude = 1200 + param.geometry.location.fixed.lat_deg = -5 + param.geometry.location.fixed.long_deg = 5 + + param.propagate_parameters() + # This should not error on this test: + param.validate() + + imt_center = StationManager(1) + imt_center.x = np.array([0.]) + imt_center.y = np.array([0.]) + imt_center.z = np.array([0.]) + + # Test point it toward IMT center (0, 0, 0) + param.geometry.azimuth.type = "POINTING_AT_IMT" + param.geometry.elevation.type = "POINTING_AT_IMT" + + space_station = StationFactory.generate_single_space_station(param) + + npt.assert_almost_equal(space_station.get_off_axis_angle(imt_center), 0, 5) + + # Test pointing it toward IMT center (0, 0, 0) + # but in another way + param.geometry.azimuth.type = "POINTING_AT_LAT_LONG_ALT" + param.geometry.elevation.type = "POINTING_AT_LAT_LONG_ALT" + param.geometry.pointing_at_lat = 0 + param.geometry.pointing_at_long = 0 + param.geometry.pointing_at_alt = 1200 + + space_station = StationFactory.generate_single_space_station(param) + + npt.assert_almost_equal(space_station.get_off_axis_angle(imt_center), 0, 5) + + # Test pointing it toward subsatellite. + # In spherical earth model, + # same as pointing toward center of earth + center_of_earth = StationManager(1) + + center_of_earth.x = np.array([0.]) + center_of_earth.y = np.array([0.]) + center_of_earth.z = -np.array([EARTH_RADIUS_M + 1200]) + + param.geometry.azimuth.type = "POINTING_AT_LAT_LONG_ALT" + param.geometry.elevation.type = "POINTING_AT_LAT_LONG_ALT" + param.geometry.pointing_at_lat = -5 + param.geometry.pointing_at_long = 5 + param.geometry.pointing_at_alt = 1200 + + space_station = StationFactory.generate_single_space_station(param) + + npt.assert_almost_equal(space_station.get_off_axis_angle(center_of_earth), 0, 5) + if __name__ == '__main__': unittest.main() From a72213d9b5c48a63311da8045b3142f8873892c7 Mon Sep 17 00:00:00 2001 From: artistrea Date: Thu, 11 Sep 2025 17:10:21 -0300 Subject: [PATCH 02/77] update: single_space_station parameters testing pointing_at_(lat|lon|alt) --- tests/parameters/parameters_for_testing.yaml | 7 +++++++ tests/parameters/test_parameters.py | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/tests/parameters/parameters_for_testing.yaml b/tests/parameters/parameters_for_testing.yaml index 20297abad..4b6934ca0 100644 --- a/tests/parameters/parameters_for_testing.yaml +++ b/tests/parameters/parameters_for_testing.yaml @@ -884,6 +884,13 @@ single_space_station: ########################################################################### # earth station lat long [deg] es_long_deg: 3.9 + # Where to point antenna if azimuth|elevation use "POINTING_AT_LAT_LONG_ALT" + # latitude [deg] + pointing_at_lat: 12. + # longitude [deg] + pointing_at_long: -1. + # altitude [m] + pointing_at_alt: 123. ########################################################################### # Azimuth angle [degrees] azimuth: diff --git a/tests/parameters/test_parameters.py b/tests/parameters/test_parameters.py index 697693d3f..f740ec5aa 100644 --- a/tests/parameters/test_parameters.py +++ b/tests/parameters/test_parameters.py @@ -745,6 +745,15 @@ def test_parameters_single_space_station(self): self.assertEqual( self.parameters.single_space_station.geometry.es_long_deg, 3.9, ) + self.assertEqual( + self.parameters.single_space_station.geometry.pointing_at_alt, 123, + ) + self.assertEqual( + self.parameters.single_space_station.geometry.pointing_at_lat, 12, + ) + self.assertEqual( + self.parameters.single_space_station.geometry.pointing_at_long, -1, + ) self.assertEqual( self.parameters.single_space_station.geometry.azimuth.type, "FIXED", From ddf9cf710b49c342992d10fddff64fbb3ee401d6 Mon Sep 17 00:00:00 2001 From: Bruno Faria Date: Fri, 12 Sep 2025 16:32:44 -0300 Subject: [PATCH 03/77] fix(antenna): Fixes an error where the AntennaS1528 frequency was always defaulting to the station frequency. --- sharc/antenna/antenna_s672.py | 10 +-- .../antenna/parameters_antenna_s672.py | 81 +++++++++++++++++++ sharc/parameters/parameters_antenna.py | 10 ++- sharc/parameters/parameters_fss_ss.py | 31 ++++--- sharc/parameters/parameters_mss_d2d.py | 24 ++---- sharc/parameters/parameters_mss_ss.py | 23 ++---- sharc/station_factory.py | 16 ++-- tests/parameters/parameters_for_testing.yaml | 44 ++++++---- tests/parameters/test_parameters.py | 32 +++++--- tests/test_antenna_s672.py | 6 +- tests/test_station_factory_ngso.py | 7 +- 11 files changed, 188 insertions(+), 96 deletions(-) create mode 100644 sharc/parameters/antenna/parameters_antenna_s672.py diff --git a/sharc/antenna/antenna_s672.py b/sharc/antenna/antenna_s672.py index 3ca6dad35..e370ab16e 100644 --- a/sharc/antenna/antenna_s672.py +++ b/sharc/antenna/antenna_s672.py @@ -6,7 +6,7 @@ """ from sharc.antenna.antenna import Antenna -from sharc.parameters.parameters_fss_ss import ParametersFssSs +from sharc.parameters.antenna.parameters_antenna_s672 import ParametersAntennaS672 import numpy as np import sys @@ -18,7 +18,7 @@ class AntennaS672(Antenna): according to Recommendation ITU-R S.672-4 Annex 1 """ - def __init__(self, param: ParametersFssSs): + def __init__(self, param: ParametersAntennaS672): super().__init__() self.peak_gain = param.antenna_gain self.l_s = param.antenna_l_s @@ -37,7 +37,7 @@ def __init__(self, param: ParametersFssSs): self.b = 6.32 - self.psi_0 = param.antenna_3_dB / 2 + self.psi_0 = param.antenna_3_dB_bw / 2 self.psi_1 = self.psi_0 * \ np.power(10, (self.peak_gain + self.l_s + 20) / 25) @@ -83,10 +83,10 @@ def calculate_gain(self, *args, **kwargs) -> np.array: import matplotlib.pyplot as plt # initialize antenna parameters - param = ParametersFssSs() + param = ParametersAntennaS672() param.antenna_gain = 50 param.antenna_pattern = "ITU-R S.672-4" - param.antenna_3_dB = 2 + param.antenna_3_dB_bw = 2 psi = np.linspace(1, 30, num=1000) param.antenna_l_s = -20 diff --git a/sharc/parameters/antenna/parameters_antenna_s672.py b/sharc/parameters/antenna/parameters_antenna_s672.py new file mode 100644 index 000000000..a9381b680 --- /dev/null +++ b/sharc/parameters/antenna/parameters_antenna_s672.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +from dataclasses import dataclass + +from sharc.parameters.parameters_base import ParametersBase + + +@dataclass +class ParametersAntennaS672(ParametersBase): + """Dataclass containing the Antenna Pattern S.672 parameters for the simulator. + """ + section_name: str = "ITU-R-S.678" + # Peak antenna gain [dBi] + antenna_gain: float | None = None + # The required near-in-side-lobe level (dB) relative to peak gain + # according to ITU-R S.672-4 + antenna_l_s: float | None = None + # 3 dB beamwidth angle (3 dB below maximum gain) [degrees] + antenna_3_dB_bw: float | None = None + + def load_parameters_from_file(self, config_file: str): + """Load the parameters from file an run a sanity check. + + Parameters + ---------- + file_name : str + the path to the configuration file + + Raises + ------ + ValueError + if a parameter is not valid + """ + super().load_parameters_from_file(config_file) + + self.validate("antenna_s678") + + def load_from_parameters(self, param: ParametersBase): + """Load from another parameter object + + Parameters + ---------- + param : ParametersBase + Parameters object containing ParametersAntennaS1528 + """ + self.antenna_l_s = param.antenna_l_s + self.antenna_3_dB_bw = param.antenna_3_dB_bw + return self + + def set_external_parameters(self, **kwargs): + """ + This method is used to "propagate" parameters from external context + to the values required by antenna S1528. + """ + attr_list = [a for a in dir(self) if not a.startswith('__')] + + for k, v in kwargs.items(): + if k in attr_list: + setattr(self, k, v) + else: + raise ValueError( + f"Parameter {k} is not a valid attribute of { + self.__class__.__name__}") + + self.validate("S.1528") + + def validate(self, ctx: str): + """ + Validate the parameters for the S.1528 antenna configuration. + + Checks that required attributes are set and that the antenna pattern is valid. + + Parameters + ---------- + ctx : str + Context string for error messages. + """ + # Now do the sanity check for some parameters + if None in [self.antenna_l_s, self.antenna_3_dB_bw]: + raise ValueError( + f"{ctx}.[antenna_l_s, antenna_3_dB_bw] = {[self.antenna_l_s, self.antenna_3_dB_bw]}.\ + They need to all be set!") diff --git a/sharc/parameters/parameters_antenna.py b/sharc/parameters/parameters_antenna.py index f004b3874..a8fc8a0e0 100644 --- a/sharc/parameters/parameters_antenna.py +++ b/sharc/parameters/parameters_antenna.py @@ -2,6 +2,7 @@ from sharc.parameters.parameters_antenna_with_diameter import ParametersAntennaWithDiameter from sharc.parameters.parameters_antenna_with_envelope_gain import ParametersAntennaWithEnvelopeGain from sharc.parameters.antenna.parameters_antenna_s1528 import ParametersAntennaS1528 +from sharc.parameters.antenna.parameters_antenna_s672 import ParametersAntennaS672 from sharc.parameters.antenna.parameters_antenna_with_freq import ParametersAntennaWithFreq from sharc.parameters.imt.parameters_antenna_imt import ParametersAntennaImt @@ -89,6 +90,10 @@ class ParametersAntenna(ParametersBase): default_factory=ParametersAntennaS1528, ) + itu_r_s_672: ParametersAntennaS672 = field( + default_factory=ParametersAntennaS672, + ) + def set_external_parameters(self, **kwargs): """ Set external parameters for all sub-parameters of the antenna. @@ -105,7 +110,8 @@ def set_external_parameters(self, **kwargs): param = getattr(self, attr_name) for k, v in kwargs.items(): - if k in dir(param): + # we only set if not already set + if k in dir(param) and getattr(param, k, None) is None: setattr(param, k, v) if "antenna_gain" in dir(param): @@ -188,6 +194,8 @@ def validate(self, ctx): self.itu_r_s_1528.validate(f"{ctx}.itu_r_s_1528") case "ITU-R-S.1528-LEO": self.itu_r_s_1528.validate(f"{ctx}.itu_r_s_1528") + case "ITU-R-S.672": + self.itu_r_s_672.validate(f"{ctx}.itu_r_s_672") case "MSS Adjacent": self.mss_adjacent.validate(f"{ctx}.mss_adjacent") case _: diff --git a/sharc/parameters/parameters_fss_ss.py b/sharc/parameters/parameters_fss_ss.py index 434a5205c..92d84bfce 100644 --- a/sharc/parameters/parameters_fss_ss.py +++ b/sharc/parameters/parameters_fss_ss.py @@ -4,7 +4,8 @@ from sharc.parameters.parameters_base import ParametersBase from sharc.parameters.parameters_p619 import ParametersP619 -from sharc.parameters.antenna.parameters_antenna_s1528 import ParametersAntennaS1528 +from sharc.parameters.parameters_antenna import ParametersAntenna +from sharc.parameters.antenna.parameters_antenna_s672 import ParametersAntennaS672 @dataclass @@ -49,16 +50,15 @@ class ParametersFssSs(ParametersBase): # Antenna parameters # Antenna pattern of the FSS space station # Possible values: "ITU-R S.672", "ITU-R S.1528", "FSS_SS", "OMNI" - antenna_pattern: str = "ITU-R S.672" - ############################ - # Satellite peak receive antenna gain [dBi] - antenna_gain: float = 46.6 - # The required near-in-side-lobe level (dB) relative to peak gain - # according to ITU-R S.672-4 - antenna_l_s: float = -20.0 - # Parameters if antenna_pattern = ITU_R S.1528 - antenna_s1528: ParametersAntennaS1528 = field( - default_factory=ParametersAntennaS1528) + antenna: ParametersAntenna = field( + default_factory=lambda: ParametersAntenna( + pattern="ITU-R S.672", + itu_r_s_672=ParametersAntennaS672( + antenna_l_s=-20, + antenna_3_dB_bw=0.65 + ) + ) + ) ############################ # Parameters for the P.619 propagation model @@ -89,10 +89,10 @@ def load_parameters_from_file(self, config_file: str): super().load_parameters_from_file(config_file) # Now do the sanity check for some parameters - if self.antenna_pattern not in [ + if self.antenna.pattern not in [ "ITU-R S.672", "ITU-R S.1528", "FSS_SS", "OMNI"]: raise ValueError(f"ParametersFssSs: \ - invalid value for parameter antenna_pattern - {self.antenna_pattern}. \ + invalid value for parameter antenna_pattern - {self.antenna.pattern}. \ Possible values \ are \"ITU-R S.672\", \"ITU-R S.1528\", \"FSS_SS\", \"OMNI\"") @@ -111,10 +111,7 @@ def load_parameters_from_file(self, config_file: str): if str(self.param_p619.mean_clutter_height).lower() not in allowed: raise ValueError("Invalid type of mean_clutter_height. mean_clutter_height must be 'Low', 'Mid', or 'High'") - self.antenna_s1528.set_external_parameters( + self.antenna.set_external_parameters( frequency=self.frequency, bandwidth=self.bandwidth, - antenna_gain=self.antenna_gain, - antenna_l_s=self.antenna_l_s, - antenna_3_dB_bw=self.antenna_3_dB, ) diff --git a/sharc/parameters/parameters_mss_d2d.py b/sharc/parameters/parameters_mss_d2d.py index 172964dbb..c566269c0 100644 --- a/sharc/parameters/parameters_mss_d2d.py +++ b/sharc/parameters/parameters_mss_d2d.py @@ -4,6 +4,7 @@ from sharc.parameters.parameters_orbit import ParametersOrbit from sharc.parameters.imt.parameters_imt_mss_dc import ParametersSelectActiveSatellite, ParametersSectorPositioning from sharc.parameters.parameters_p619 import ParametersP619 +from sharc.parameters.parameters_antenna import ParametersAntenna from sharc.parameters.antenna.parameters_antenna_s1528 import ParametersAntennaS1528 @@ -76,18 +77,12 @@ class ParametersMssD2d(ParametersBase): # Possible values: "ITU-R-S.1528-Taylor", "ITU-R-S.1528-LEO" antenna_pattern: str = "ITU-R-S.1528-Taylor" - # Radius of the antenna's circular aperture in meters - antenna_diamter: float = 1.0 - - # The required near-in-side-lobe level (dB) relative to peak gain - antenna_l_s: float = -6.75 - - # 3 dB beamwidth angle (3 dB below maximum gain) [degrees] - antenna_3_dB_bw: float = 4.4127 - - # Paramters for the ITU-R-S.1528 antenna patterns - antenna_s1528: ParametersAntennaS1528 = field( - default_factory=ParametersAntennaS1528) + # Parameters for the antenna patterns + antenna: ParametersAntenna = field( + default_factory=lambda: ParametersAntenna( + pattern="ITU-R-S.1528-Taylor", + gain=30.0, + itu_r_s_1528=ParametersAntennaS1528())) sat_is_active_if: ParametersSelectActiveSatellite = field( default_factory=ParametersSelectActiveSatellite) @@ -182,12 +177,9 @@ def propagate_parameters(self): """ Propagate relevant parameters to nested antenna and beam positioning objects. """ - self.antenna_s1528.set_external_parameters( - antenna_pattern=self.antenna_pattern, + self.antenna.set_external_parameters( frequency=self.frequency, bandwidth=self.bandwidth, - antenna_l_s=self.antenna_l_s, - antenna_3_dB_bw=self.antenna_3_dB_bw, ) if self.beam_positioning.service_grid.beam_radius is None: self.beam_positioning.service_grid.beam_radius = self.cell_radius diff --git a/sharc/parameters/parameters_mss_ss.py b/sharc/parameters/parameters_mss_ss.py index 1a2b7d668..50a3d3acf 100644 --- a/sharc/parameters/parameters_mss_ss.py +++ b/sharc/parameters/parameters_mss_ss.py @@ -5,7 +5,7 @@ from sharc.parameters.parameters_base import ParametersBase from sharc.parameters.parameters_p619 import ParametersP619 -from sharc.parameters.antenna.parameters_antenna_s1528 import ParametersAntennaS1528 +from sharc.parameters.parameters_antenna import ParametersAntenna import numpy as np @@ -66,18 +66,9 @@ class ParametersMssSs(ParametersBase): # Possible values: "ITU-R-S.1528-Section1.2", "ITU-R-S.1528-LEO" antenna_pattern: str = "ITU-R-S.1528-LEO" - # Radius of the antenna's circular aperture in meters - antenna_diamter: float = 1.0 - - # The required near-in-side-lobe level (dB) relative to peak gain - antenna_l_s: float = -6.75 - - # 3 dB beamwidth angle (3 dB below maximum gain) [degrees] - antenna_3_dB_bw: float = 4.4127 - - # Paramters for the ITU-R-S.1528 antenna patterns - antenna_s1528: ParametersAntennaS1528 = field( - default_factory=ParametersAntennaS1528) + # Paramters for the antenna patterns + antenna: ParametersAntenna = field( + default_factory=ParametersAntenna) # paramters for channel model param_p619: ParametersP619 = field(default_factory=ParametersP619) @@ -131,12 +122,10 @@ def load_parameters_from_file(self, config_file: str): f"""ParametersImt: Inavlid Spectral Mask Name { self.spectral_mask}""") - self.antenna_s1528.set_external_parameters( + self.antenna.set_external_parameters( + pattern=self.antenna_pattern, frequency=self.frequency, bandwidth=self.bandwidth, - antenna_gain=self.antenna_gain, - antenna_l_s=self.antenna_l_s, - antenna_3_dB_bw=self.antenna_3_dB_bw, ) if self.channel_model.upper() not in [ diff --git a/sharc/station_factory.py b/sharc/station_factory.py index 9824e47ee..66f93b709 100644 --- a/sharc/station_factory.py +++ b/sharc/station_factory.py @@ -1700,17 +1700,17 @@ def generate_mss_d2d( # repeated state (elevation and azimuth) inside multiple transceiver # implementation mss_d2d.antenna = np.empty(total_satellites, dtype=AntennaS1528Leo) - if params.antenna_pattern == "ITU-R-S.1528-LEO": - antenna_pattern = AntennaS1528Leo(params.antenna_s1528) - elif params.antenna_pattern == "ITU-R-S.1528-Section1.2": - antenna_pattern = AntennaS1528(params.antenna_s1528) - elif params.antenna_pattern == "ITU-R-S.1528-Taylor": - antenna_pattern = AntennaS1528Taylor(params.antenna_s1528) - elif params.antenna_pattern == "MSS Adjacent": + if params.antenna.pattern == "ITU-R-S.1528-LEO": + antenna_pattern = AntennaS1528Leo(params.antenna.itu_r_s_1528) + elif params.antenna.pattern == "ITU-R-S.1528-Section1.2": + antenna_pattern = AntennaS1528(params.antenna.itu_r_s_1528) + elif params.antenna.pattern == "ITU-R-S.1528-Taylor": + antenna_pattern = AntennaS1528Taylor(params.antenna.itu_r_s_1528) + elif params.antenna.pattern == "MSS Adjacent": antenna_pattern = AntennaMSSAdjacent(params.frequency) else: raise ValueError( - "generate_mss_ss: Invalid antenna type: {param_mss.antenna_pattern}") + f"generate_mss_ss: Invalid antenna type: {params.antenna.pattern}") for i in range(mss_d2d.num_stations): mss_d2d.antenna[i] = antenna_pattern diff --git a/tests/parameters/parameters_for_testing.yaml b/tests/parameters/parameters_for_testing.yaml index 4b6934ca0..d8dc7ad28 100644 --- a/tests/parameters/parameters_for_testing.yaml +++ b/tests/parameters/parameters_for_testing.yaml @@ -326,6 +326,8 @@ imt: ohmic_loss : 3.1 # Base Station Antenna parameters: antenna: + pattern: "ARRAY" + # pattern: ITU-R-S.1528-Taylor array: ########################################################################### # If normalization of M2101 should be applied for BS @@ -408,7 +410,19 @@ imt: element_vert_spacing: 0.05 # Sub array eletrical downtilt [deg] eletrical_downtilt: 9.0 - + gain: 34.1 + itu_r_s_1528: + ### The following parameters are used for S.1528-Taylor antenna pattern + # SLR is the side-lobe ratio of the pattern (dB), the difference in gain between the maximum + # gain and the gain at the peak of the first side lobe. + slr: 20.0 + # Number of secondary lobes considered in the diagram (coincide with the roots of the Bessel function) + n_side_lobes: 2 + # Radial (l_r) and transverse (l_t) sizes of the effective radiating area of the satellite transmitt antenna (m) + l_r: 1.6 + l_t: 1.6 + frequency: 2177.0 + bandwidth: 6.0 ########################################################################### # User Equipment parameters: ue: @@ -1060,19 +1074,21 @@ mss_d2d: # Satellite antenna pattern # Antenna pattern from ITU-R S.1528 # Possible values: "ITU-R-S.1528-Section1.2", "ITU-R-S.1528-LEO" - antenna_pattern: ITU-R-S.1528-Taylor - antenna_s1528: - # Satellite antenna gain dBi - antenna_gain: 34.1 - ### The following parameters are used for S.1528-Taylor antenna pattern - # SLR is the side-lobe ratio of the pattern (dB), the difference in gain between the maximum - # gain and the gain at the peak of the first side lobe. - slr: 20.0 - # Number of secondary lobes considered in the diagram (coincide with the roots of the Bessel function) - n_side_lobes: 2 - # Radial (l_r) and transverse (l_t) sizes of the effective radiating area of the satellite transmitt antenna (m) - l_r: 1.6 - l_t: 1.6 + antenna: + pattern: ITU-R-S.1528-Taylor + gain: 34.1 + itu_r_s_1528: + ### The following parameters are used for S.1528-Taylor antenna pattern + # SLR is the side-lobe ratio of the pattern (dB), the difference in gain between the maximum + # gain and the gain at the peak of the first side lobe. + slr: 20.0 + # Number of secondary lobes considered in the diagram (coincide with the roots of the Bessel function) + n_side_lobes: 2 + # Radial (l_r) and transverse (l_t) sizes of the effective radiating area of the satellite transmitt antenna (m) + l_r: 1.6 + l_t: 1.6 + frequency: 2177.0 + bandwidth: 6.0 # channel model, possible values are "FSPL" (free-space path loss), # "SatelliteSimple" (FSPL + 4 + clutter loss) # "P619" diff --git a/tests/parameters/test_parameters.py b/tests/parameters/test_parameters.py index f740ec5aa..292ae1e98 100644 --- a/tests/parameters/test_parameters.py +++ b/tests/parameters/test_parameters.py @@ -171,6 +171,18 @@ def test_parameters_imt(self): self.assertEqual( self.parameters.imt.topology.central_longitude, -12.134) + # Now check S.1528 antenna parameters when used in DC-MSS-IMT + self.parameters.imt.bs.antenna.pattern = "ITU-R-S.1528-Taylor" + self.parameters.imt.bs.antenna.validate("test_imt_parameters") + self.assertEqual(self.parameters.imt.bs.antenna.pattern, "ITU-R-S.1528-Taylor") + self.assertEqual(self.parameters.imt.bs.antenna.gain, 34.1) + self.assertEqual(self.parameters.imt.bs.antenna.itu_r_s_1528.frequency, 2177.0) + self.assertEqual(self.parameters.imt.bs.antenna.itu_r_s_1528.bandwidth, 6.0) + self.assertEqual(self.parameters.imt.bs.antenna.itu_r_s_1528.slr, 20) + self.assertEqual(self.parameters.imt.bs.antenna.itu_r_s_1528.n_side_lobes, 2) + self.assertEqual(self.parameters.imt.bs.antenna.itu_r_s_1528.l_r, 1.6) + self.assertEqual(self.parameters.imt.bs.antenna.itu_r_s_1528.l_t, 1.6) + """Test ParametersSubarrayImt """ # testing default value not enabled @@ -609,21 +621,17 @@ def test_parametes_mss_d2d(self): self.assertEqual(self.parameters.mss_d2d.beam_radius, 19001) self.assertEqual(self.parameters.mss_d2d.tx_power_density, -30) self.assertEqual(self.parameters.mss_d2d.num_sectors, 19) - self.assertEqual(self.parameters.mss_d2d.antenna_diamter, 1.0) - self.assertEqual(self.parameters.mss_d2d.antenna_l_s, -6.75) - self.assertEqual(self.parameters.mss_d2d.antenna_3_dB_bw, 4.4127) - self.assertEqual( - self.parameters.mss_d2d.antenna_pattern, - 'ITU-R-S.1528-Taylor') self.assertEqual( - self.parameters.mss_d2d.antenna_s1528.antenna_pattern, + self.parameters.mss_d2d.antenna.pattern, 'ITU-R-S.1528-Taylor') self.assertEqual( - self.parameters.mss_d2d.antenna_s1528.antenna_gain, 34.1) - self.assertEqual(self.parameters.mss_d2d.antenna_s1528.slr, 20) - self.assertEqual(self.parameters.mss_d2d.antenna_s1528.n_side_lobes, 2) - self.assertEqual(self.parameters.mss_d2d.antenna_s1528.l_r, 1.6) - self.assertEqual(self.parameters.mss_d2d.antenna_s1528.l_t, 1.6) + self.parameters.mss_d2d.antenna.gain, 34.1) + self.assertEqual(self.parameters.mss_d2d.antenna.itu_r_s_1528.frequency, 2177.0) + self.assertEqual(self.parameters.mss_d2d.antenna.itu_r_s_1528.bandwidth, 6.0) + self.assertEqual(self.parameters.mss_d2d.antenna.itu_r_s_1528.slr, 20) + self.assertEqual(self.parameters.mss_d2d.antenna.itu_r_s_1528.n_side_lobes, 2) + self.assertEqual(self.parameters.mss_d2d.antenna.itu_r_s_1528.l_r, 1.6) + self.assertEqual(self.parameters.mss_d2d.antenna.itu_r_s_1528.l_t, 1.6) self.assertEqual(self.parameters.mss_d2d.channel_model, 'P619') self.assertEqual( self.parameters.mss_d2d.param_p619.earth_station_alt_m, 0.0) diff --git a/tests/test_antenna_s672.py b/tests/test_antenna_s672.py index 74efba3a5..a17425fa6 100644 --- a/tests/test_antenna_s672.py +++ b/tests/test_antenna_s672.py @@ -8,7 +8,7 @@ import unittest from sharc.antenna.antenna_s672 import AntennaS672 -from sharc.parameters.parameters_fss_ss import ParametersFssSs +from sharc.parameters.antenna.parameters_antenna_s672 import ParametersAntennaS672 import numpy as np import numpy.testing as npt @@ -19,10 +19,10 @@ class AntennaS672Test(unittest.TestCase): def setUp(self): """Set up test fixtures for AntennaS672 tests.""" - param = ParametersFssSs() + param = ParametersAntennaS672() param.antenna_gain = 50 param.antenna_pattern = "ITU-R S.672-4" - param.antenna_3_dB = 2 + param.antenna_3_dB_bw = 2 param.antenna_l_s = -20 self.antenna20 = AntennaS672(param) diff --git a/tests/test_station_factory_ngso.py b/tests/test_station_factory_ngso.py index 191f0257d..3932c5420 100644 --- a/tests/test_station_factory_ngso.py +++ b/tests/test_station_factory_ngso.py @@ -55,9 +55,10 @@ def setUp(self): orbits=[orbit_1, orbit_2], num_sectors=1, ) - self.param.antenna_s1528.frequency = 43000.0 - self.param.antenna_s1528.bandwidth = 500.0 - self.param.antenna_s1528.antenna_gain = 46.6 + self.param.antenna.pattern = "ITU-R-S.1528-Taylor" + self.param.antenna.itu_r_s_1528.frequency = 43000.0 + self.param.antenna.itu_r_s_1528.bandwidth = 500.0 + self.param.antenna.itu_r_s_1528.antenna_gain = 46.6 # Creating an IMT topology # imt_topology = TopologySingleBaseStation( From 6c643808ed37cbeab82a088721ce8388b1a0cfc8 Mon Sep 17 00:00:00 2001 From: Bruno Faria Date: Sat, 13 Sep 2025 20:17:23 -0300 Subject: [PATCH 04/77] update(parameters): Removed unused (and dangerous) set_external_parameters from Antennas S.672 and S.1528 parameters. --- .../antenna/parameters_antenna_s1528.py | 17 ----------------- .../antenna/parameters_antenna_s672.py | 17 ----------------- 2 files changed, 34 deletions(-) diff --git a/sharc/parameters/antenna/parameters_antenna_s1528.py b/sharc/parameters/antenna/parameters_antenna_s1528.py index e0eb4703c..2fb12f4d5 100644 --- a/sharc/parameters/antenna/parameters_antenna_s1528.py +++ b/sharc/parameters/antenna/parameters_antenna_s1528.py @@ -76,23 +76,6 @@ def load_from_parameters(self, param: ParametersBase): self.n_side_lobes = param.n_side_lobes return self - def set_external_parameters(self, **kwargs): - """ - This method is used to "propagate" parameters from external context - to the values required by antenna S1528. - """ - attr_list = [a for a in dir(self) if not a.startswith('__')] - - for k, v in kwargs.items(): - if k in attr_list: - setattr(self, k, v) - else: - raise ValueError( - f"Parameter {k} is not a valid attribute of { - self.__class__.__name__}") - - self.validate("S.1528") - def validate(self, ctx: str): """ Validate the parameters for the S.1528 antenna configuration. diff --git a/sharc/parameters/antenna/parameters_antenna_s672.py b/sharc/parameters/antenna/parameters_antenna_s672.py index a9381b680..f174c5c62 100644 --- a/sharc/parameters/antenna/parameters_antenna_s672.py +++ b/sharc/parameters/antenna/parameters_antenna_s672.py @@ -46,23 +46,6 @@ def load_from_parameters(self, param: ParametersBase): self.antenna_3_dB_bw = param.antenna_3_dB_bw return self - def set_external_parameters(self, **kwargs): - """ - This method is used to "propagate" parameters from external context - to the values required by antenna S1528. - """ - attr_list = [a for a in dir(self) if not a.startswith('__')] - - for k, v in kwargs.items(): - if k in attr_list: - setattr(self, k, v) - else: - raise ValueError( - f"Parameter {k} is not a valid attribute of { - self.__class__.__name__}") - - self.validate("S.1528") - def validate(self, ctx: str): """ Validate the parameters for the S.1528 antenna configuration. From f897a4a228fde84053f4bf5a60340f661995075e Mon Sep 17 00:00:00 2001 From: Bruno Faria Date: Sat, 13 Sep 2025 20:44:09 -0300 Subject: [PATCH 05/77] update(tests): Check if all ParametersAntenna sub-params are set no None. * Defaulting all ParametersAntennaS1528 to None --- .../antenna/parameters_antenna_s1528.py | 25 +++++-------------- tests/parameters/test_parameters.py | 1 - tests/test_antenna_multiple_transceiver.py | 12 ++++++--- tests/test_station_factory_ngso.py | 4 +++ 4 files changed, 18 insertions(+), 24 deletions(-) diff --git a/sharc/parameters/antenna/parameters_antenna_s1528.py b/sharc/parameters/antenna/parameters_antenna_s1528.py index 2fb12f4d5..4cd5ea35a 100644 --- a/sharc/parameters/antenna/parameters_antenna_s1528.py +++ b/sharc/parameters/antenna/parameters_antenna_s1528.py @@ -15,31 +15,27 @@ class ParametersAntennaS1528(ParametersBase): bandwidth: float = None # Peak antenna gain [dBi] antenna_gain: float = None - # Antenna pattern from ITU-R S.1528 - # Possible values: "ITU-R-S.1528-Section1.2", "ITU-R-S.1528-LEO", - # "ITU-R-S.1528-Taylor" - antenna_pattern: str = "ITU-R-S.1528-LEO" # The required near-in-side-lobe level (dB) relative to peak gain # according to ITU-R S.672-4 - antenna_l_s: float = -20.0 + antenna_l_s: float = None # 3 dB beamwidth angle (3 dB below maximum gain) [degrees] - antenna_3_dB_bw: float = 0.65 + antenna_3_dB_bw: float = None ##################################################################### # The following parameters are used for S.1528-Taylor antenna pattern # SLR is the side-lobe ratio of the pattern (dB), the difference in gain between the maximum # gain and the gain at the peak of the first side lobe. - slr: float = 20.0 + slr: float = None # Number of secondary lobes considered in the diagram (coincide with the # roots of the Bessel function) - n_side_lobes: int = 2 + n_side_lobes: int = None # Radial (l_r) and transverse (l_t) sizes of the effective radiating area # of the satellite transmitt antenna (m) - l_r: float = 1.6 - l_t: float = 1.6 + l_r: float = None + l_t: float = None def load_parameters_from_file(self, config_file: str): """Load the parameters from file an run a sanity check. @@ -92,12 +88,3 @@ def validate(self, ctx: str): raise ValueError( f"{ctx}.[frequency, bandwidth, antenna_gain] = {[self.frequency, self.bandwidth]}.\ They need to all be set!") - - if self.antenna_pattern not in [ - "ITU-R-S.1528-Section1.2", - "ITU-R-S.1528-LEO", - "ITU-R-S.1528-Taylor"]: - raise ValueError(f"{ctx}: \ - invalid value for parameter antenna_pattern - {self.antenna_pattern}. \ - Possible values \ - are \"ITU-R-S.1528-Section1.2\", \"ITU-R-S.1528-LEO\", \"ITU-R-S.1528-Taylor\"") diff --git a/tests/parameters/test_parameters.py b/tests/parameters/test_parameters.py index 292ae1e98..fc2c5ad1a 100644 --- a/tests/parameters/test_parameters.py +++ b/tests/parameters/test_parameters.py @@ -174,7 +174,6 @@ def test_parameters_imt(self): # Now check S.1528 antenna parameters when used in DC-MSS-IMT self.parameters.imt.bs.antenna.pattern = "ITU-R-S.1528-Taylor" self.parameters.imt.bs.antenna.validate("test_imt_parameters") - self.assertEqual(self.parameters.imt.bs.antenna.pattern, "ITU-R-S.1528-Taylor") self.assertEqual(self.parameters.imt.bs.antenna.gain, 34.1) self.assertEqual(self.parameters.imt.bs.antenna.itu_r_s_1528.frequency, 2177.0) self.assertEqual(self.parameters.imt.bs.antenna.itu_r_s_1528.bandwidth, 6.0) diff --git a/tests/test_antenna_multiple_transceiver.py b/tests/test_antenna_multiple_transceiver.py index 7a49cfa02..047b4c67b 100644 --- a/tests/test_antenna_multiple_transceiver.py +++ b/tests/test_antenna_multiple_transceiver.py @@ -15,10 +15,14 @@ class AntennaAntennaMultipleTransceiverTest(unittest.TestCase): def setUp(self): """Set up test fixtures for AntennaMultipleTransceiver tests.""" param = ParametersAntennaS1528() - param.antenna_gain = 30 - param.frequency = 2170.0 - param.bandwidth = 5.0 - param.antenna_3_dB_bw = 4.4127 + param.pattern = "ITU-R-S.1528-Taylor" + param.frequency = 43000.0 + param.bandwidth = 500.0 + param.antenna_gain = 46.6 + param.slr = 20.0 + param.n_side_lobes = 2 + param.l_r = 1.6 + param.l_t = 1.6 self.base_antenna = AntennaS1528Taylor(param) diff --git a/tests/test_station_factory_ngso.py b/tests/test_station_factory_ngso.py index 3932c5420..2b0259e91 100644 --- a/tests/test_station_factory_ngso.py +++ b/tests/test_station_factory_ngso.py @@ -59,6 +59,10 @@ def setUp(self): self.param.antenna.itu_r_s_1528.frequency = 43000.0 self.param.antenna.itu_r_s_1528.bandwidth = 500.0 self.param.antenna.itu_r_s_1528.antenna_gain = 46.6 + self.param.antenna.itu_r_s_1528.slr = 20.0 + self.param.antenna.itu_r_s_1528.n_side_lobes = 2 + self.param.antenna.itu_r_s_1528.l_r = 1.6 + self.param.antenna.itu_r_s_1528.l_t = 1.6 # Creating an IMT topology # imt_topology = TopologySingleBaseStation( From 0775a40bef2e37908b339e585c65225594064c7f Mon Sep 17 00:00:00 2001 From: artistrea Date: Mon, 15 Sep 2025 12:04:27 -0300 Subject: [PATCH 06/77] refactor: sharc_geom shrink_country_polygon_by_km -> shrink_lonlat_polygon_by_km --- sharc/support/sharc_geom.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/sharc/support/sharc_geom.py b/sharc/support/sharc_geom.py index 54317dae4..39b30ce9a 100644 --- a/sharc/support/sharc_geom.py +++ b/sharc/support/sharc_geom.py @@ -517,7 +517,7 @@ def get_lambert_equal_area_crs(polygon: shp.geometry.Polygon): ) -def shrink_country_polygon_by_km( +def shrink_lonlat_polygon_by_km( polygon: shp.geometry.Polygon, km: float ) -> shp.geometry.Polygon: """Project a Polygon to Lambert Azimuthal Equal Area, shrink by km, and reproject back. @@ -540,8 +540,11 @@ def shrink_country_polygon_by_km( Notes ----- Check for polygon validity after transformation: - if poly.is_valid: raise Exception("bad polygon") - if not poly.is_empty and poly.area > 0: continue # ignore + if (not self._polygon.is_valid + or self._polygon.is_empty + or self._polygon.area <= 0 + ): + raise Exception("bad polygon") """ # Lambert is more precise, but could prob. get UTM projection # Didn't see any practical difference for current use cases @@ -586,10 +589,10 @@ def shrink_countries_by_km( for ext_poly in countries: if ext_poly.geom_type == 'Polygon': - polys.append(shrink_country_polygon_by_km(ext_poly, km)) + polys.append(shrink_lonlat_polygon_by_km(ext_poly, km)) elif ext_poly.geom_type == 'MultiPolygon': polys.append(shp.ops.unary_union([ - shrink_country_polygon_by_km(poly, km) for poly in ext_poly.geoms + shrink_lonlat_polygon_by_km(poly, km) for poly in ext_poly.geoms ])) for poly in polys: From 1cd014caf457944dfad88fcb59de3d9655a07f80 Mon Sep 17 00:00:00 2001 From: artistrea Date: Mon, 15 Sep 2025 12:06:09 -0300 Subject: [PATCH 07/77] feat: service grid exclusion zone implementation & plot --- sharc/parameters/imt/parameters_imt_mss_dc.py | 147 +++++++++++++++++- sharc/satellite/scripts/plot_footprints.py | 36 ++++- 2 files changed, 178 insertions(+), 5 deletions(-) diff --git a/sharc/parameters/imt/parameters_imt_mss_dc.py b/sharc/parameters/imt/parameters_imt_mss_dc.py index 3f31ed8da..2aa44817f 100644 --- a/sharc/parameters/imt/parameters_imt_mss_dc.py +++ b/sharc/parameters/imt/parameters_imt_mss_dc.py @@ -6,7 +6,11 @@ import shapely as shp from sharc.support.sharc_utils import load_gdf -from sharc.support.sharc_geom import shrink_countries_by_km, generate_grid_in_multipolygon +from sharc.support.sharc_geom import ( + shrink_countries_by_km, + generate_grid_in_multipolygon, + shrink_lonlat_polygon_by_km, +) from sharc.satellite.utils.sat_utils import lla2ecef from sharc.parameters.parameters_base import ParametersBase from sharc.parameters.parameters_orbit import ParametersOrbit @@ -134,10 +138,141 @@ class ParametersServiceGrid(ParametersBase): country_shapes_filename: Path = SHARC_ROOT_DIR / "sharc" / \ "data" / "countries" / "ne_110m_admin_0_countries.shp" + @dataclass + class ParametersExclusionZone(ParametersBase): + @dataclass + class ParametersCircle(ParametersBase): + center_lat: typing.Optional[float] = None + center_lon: typing.Optional[float] = None + radius_km: typing.Optional[float] = None + + _polygon: shp.Polygon = None + + def validate(self, ctx): + """ + Validates instance parameters. + + Ensures attributes make sense + + Parameters + ---------- + ctx : str + Context string for error messages. + + Raises + ------ + ValueError + If a parameter is not valid. + """ + if None in [ + self.center_lat, + self.center_lon, + self.radius_km, + ]: + raise ValueError( + f"{ctx}.(center_lat|center_lon|radius_km) need to be set" + ) + + if self.radius_km <= 0: + raise ValueError(f"{ctx}.radius_km needs to be positive") + + if not (-180. <= self.center_lon <= 180.): + raise ValueError(f"{ctx}.center_lon needs to be in [-180, 180]") + + if not (-90. <= self.center_lat <= 90.): + raise ValueError(f"{ctx}.center_lat needs to be in [-90, 90]") + + super().validate(ctx) + + self._calculate_polygon() + + def _calculate_polygon(self): + """ + Calculates circle lon,lat polygon according to its attributes + + Raises + ------ + TODO: + ValueError + If resulting polygon is invalid. + """ + self._polygon = shrink_lonlat_polygon_by_km( + shp.geometry.Point(self.center_lon, self.center_lat), + -self.radius_km + ) + + __ALLOWED_TYPES = [None, "CIRCLE"] + type: typing.Literal[None, "CIRCLE"] = None + + circle: ParametersCircle = field(default_factory=ParametersCircle) + + _polygon: shp.geometry.Polygon = None + + def validate(self, ctx): + """ + Validates instance parameters. + + Ensures attributes make sense + + Parameters + ---------- + ctx : str + Context string for error messages. + + Raises + ------ + ValueError + If a parameter is not valid. + """ + if self.type is None: + return + if self.type not in self.__ALLOWED_TYPES: + raise ValueError(f"{ctx}.type should be in {self.__ALLOWED_TYPES}") + + if self.type == "CIRCLE": + self.circle.validate(f"{ctx}.circle") + self._polygon = self.circle._polygon + else: + raise NotImplementedError( + "No validation implemented for\n" + f"\t{ctx}.type == {self.type}" + ) + + if (not self._polygon.is_valid + or self._polygon.is_empty + or self._polygon.area <= 0 + ): + raise Exception(f"Bad {ctx}._polygon was generated") + + def _calculate_polygon(self): + if self.type == "CIRCLE": + self.circle._calculate_polygon() + self._polygon = self.circle._polygon + elif self.type is None: + self._polygon = None + else: + raise NotImplementedError( + f"Polygon calculation for type = {self.type}" + ) + + def apply_exclusion_zone(self, lon, lat): + if self.type is None: + return np.stack((lon, lat)) + + msk = ~shp.vectorized.contains( + self._polygon, + lon, + lat, + ) + + return np.stack((lon[msk], lat[msk])) + country_names: list[str] = field(default_factory=lambda: list([""])) transform_grid_randomly: bool = False + grid_exclusion_zone: ParametersExclusionZone = field(default_factory=ParametersExclusionZone) + # margin from inside of border [km] # if positive, makes border smaller by x km # if negative, makes border bigger by x km @@ -199,10 +334,10 @@ def validate(self, ctx: str): raise ValueError( f"{ctx}.eligible_sats_margin_from_border needs to be a number") - self._load_geom_from_file_if_needed(ctx) - super().validate(ctx) + self._load_geom_from_file_if_needed(ctx) + def load_from_active_sat_conditions( self, sat_is_active_if: "ParametersSelectActiveSatellite", @@ -232,13 +367,17 @@ def reset_grid( """ self._load_geom_from_file_if_needed(ctx, force_update) - self.lon_lat_grid = generate_grid_in_multipolygon( + lon, lat = generate_grid_in_multipolygon( self.grid_borders_polygon, self.beam_radius, self.transform_grid_randomly, rng ) + self.lon_lat_grid = self.grid_exclusion_zone.apply_exclusion_zone( + lon, lat + ) + self.ecef_grid = lla2ecef( self.lon_lat_grid[1], self.lon_lat_grid[0], 0) diff --git a/sharc/satellite/scripts/plot_footprints.py b/sharc/satellite/scripts/plot_footprints.py index 258a2d359..784b7d166 100644 --- a/sharc/satellite/scripts/plot_footprints.py +++ b/sharc/satellite/scripts/plot_footprints.py @@ -6,6 +6,7 @@ import numpy as np import plotly.graph_objects as go from dataclasses import dataclass, field +from functools import reduce from sharc.station_factory import StationFactory from sharc.parameters.parameters_mss_d2d import ParametersOrbit, ParametersMssD2d @@ -311,6 +312,29 @@ def plot_fp( marker=dict(size=1, color='blue', opacity=1.0), name="Service Point" )) + + if params.beam_positioning.service_grid.grid_exclusion_zone._polygon is not None: + polygons_lim = plot_mult_polygon( + params.beam_positioning.service_grid.grid_exclusion_zone._polygon, + geoconv, + True, + 2 + ) + + lim_x, lim_y, lim_z = reduce( + lambda acc, it: (list(it[0]) + [None] + acc[0], list(it[1]) + [None] + acc[1], list(it[2]) + [None] + acc[2]), + polygons_lim, + ([], [], []) + ) + + fig.add_trace(go.Scatter3d( + x=lim_x, + y=lim_y, + z=lim_z, + mode='lines', + line=dict(color='rgb(255, 0, 0)'), + name="Exclusion Zone" + )) # fig.add_trace(go.Scatter3d( # x=center_of_earth.x / 1e3, # y=center_of_earth.y / 1e3, @@ -364,9 +388,19 @@ def plot_fp( "LAT_LONG_INSIDE_COUNTRY", ] params.sat_is_active_if.minimum_elevation_from_es = 5.0 - params.sat_is_active_if.lat_long_inside_country.country_names = ["Brazil"] + params.sat_is_active_if.lat_long_inside_country.country_names = ["Brazil", "Paraguay"] # params.beams_load_factor = 0.1 params.beam_positioning.type = "SERVICE_GRID" + + grid_exclusion_zone = params.beam_positioning.service_grid.grid_exclusion_zone + + # grid_exclusion_zone.type = "CIRCLE" + # # at frienship bridge, so should affect more than 1 grid + # grid_exclusion_zone.circle.center_lat = -25.5094741 + # grid_exclusion_zone.circle.center_lon = -54.6007197 + # grid_exclusion_zone.circle.radius_km = 0.00001 + # grid_exclusion_zone.circle.radius_km = 2 * spotbeam_radius / 1e3 + # params.beam_positioning.type = "ANGLE_AND_DISTANCE_FROM_SUBSATELLITE" # params.beam_positioning.angle_from_subsatellite_phi.type = "~U(MIN,MAX)" # params.beam_positioning.angle_from_subsatellite_phi.distribution.min = -60.0 From 1c26dc868e54379c9f55d834017cf6b151648736 Mon Sep 17 00:00:00 2001 From: artistrea Date: Mon, 15 Sep 2025 12:06:43 -0300 Subject: [PATCH 08/77] feat: tests for service grid exclusion zone implementation --- sharc/parameters/imt/parameters_imt_mss_dc.py | 11 +- tests/parameters/parameters_for_testing.yaml | 14 +++ tests/parameters/test_parameters.py | 116 ++++++++++++++++++ 3 files changed, 133 insertions(+), 8 deletions(-) diff --git a/sharc/parameters/imt/parameters_imt_mss_dc.py b/sharc/parameters/imt/parameters_imt_mss_dc.py index 2aa44817f..db18a5d9b 100644 --- a/sharc/parameters/imt/parameters_imt_mss_dc.py +++ b/sharc/parameters/imt/parameters_imt_mss_dc.py @@ -189,12 +189,6 @@ def validate(self, ctx): def _calculate_polygon(self): """ Calculates circle lon,lat polygon according to its attributes - - Raises - ------ - TODO: - ValueError - If resulting polygon is invalid. """ self._polygon = shrink_lonlat_polygon_by_km( shp.geometry.Point(self.center_lon, self.center_lat), @@ -224,11 +218,12 @@ def validate(self, ctx): ValueError If a parameter is not valid. """ - if self.type is None: - return if self.type not in self.__ALLOWED_TYPES: raise ValueError(f"{ctx}.type should be in {self.__ALLOWED_TYPES}") + if self.type is None: + return + if self.type == "CIRCLE": self.circle.validate(f"{ctx}.circle") self._polygon = self.circle._polygon diff --git a/tests/parameters/parameters_for_testing.yaml b/tests/parameters/parameters_for_testing.yaml index d8dc7ad28..ed61cb379 100644 --- a/tests/parameters/parameters_for_testing.yaml +++ b/tests/parameters/parameters_for_testing.yaml @@ -232,6 +232,13 @@ imt: min: 0.0 max: 66.1 service_grid: + grid_exclusion_zone: + type: CIRCLE + circle: + center_lat: -14.123 + center_lon: -47.1 + radius_km: 123 + transform_grid_randomly: true # add per drop rand rotation + transl. to grid # by default this already gets shapefile from natural earth country_shapes_filename: sharc/data/countries/ne_110m_admin_0_countries.shp @@ -1030,6 +1037,13 @@ mss_d2d: min: 0.0 max: 66.1 service_grid: + grid_exclusion_zone: + type: CIRCLE + circle: + center_lat: -14.123 + center_lon: 120 + radius_km: 321 + transform_grid_randomly: true # add per drop rand rotation + transl. to grid # by default this already gets shapefile from natural earth country_shapes_filename: sharc/data/countries/ne_110m_admin_0_countries.shp diff --git a/tests/parameters/test_parameters.py b/tests/parameters/test_parameters.py index fc2c5ad1a..0169a47dc 100644 --- a/tests/parameters/test_parameters.py +++ b/tests/parameters/test_parameters.py @@ -291,6 +291,20 @@ def test_parameters_imt(self): self.assertEqual( self.parameters.imt.topology.mss_dc.beam_positioning.service_grid.beam_radius, 19000) + + self.assertEqual( + self.parameters.imt.topology.mss_dc.beam_positioning.service_grid.grid_exclusion_zone.type, + "CIRCLE") + self.assertEqual( + self.parameters.imt.topology.mss_dc.beam_positioning.service_grid.grid_exclusion_zone.circle.center_lat, + -14.123) + self.assertEqual( + self.parameters.imt.topology.mss_dc.beam_positioning.service_grid.grid_exclusion_zone.circle.center_lon, + -47.1) + self.assertEqual( + self.parameters.imt.topology.mss_dc.beam_positioning.service_grid.grid_exclusion_zone.circle.radius_km, + 123) + self.assertEqual( self.parameters.imt.topology.mss_dc.beam_positioning.service_grid.transform_grid_randomly, True) @@ -640,6 +654,20 @@ def test_parametes_mss_d2d(self): self.assertEqual( self.parameters.mss_d2d.beam_positioning.service_grid.beam_radius, 19001) + + self.assertEqual( + self.parameters.mss_d2d.beam_positioning.service_grid.grid_exclusion_zone.type, + "CIRCLE") + self.assertEqual( + self.parameters.mss_d2d.beam_positioning.service_grid.grid_exclusion_zone.circle.center_lat, + -14.123) + self.assertEqual( + self.parameters.mss_d2d.beam_positioning.service_grid.grid_exclusion_zone.circle.center_lon, + 120) + self.assertEqual( + self.parameters.mss_d2d.beam_positioning.service_grid.grid_exclusion_zone.circle.radius_km, + 321) + self.assertEqual( self.parameters.mss_d2d.beam_positioning.service_grid.transform_grid_randomly, True) @@ -868,6 +896,94 @@ def test_mss_d2d_loaded_geom(self): (CL_AREA + BR_AREA) / 1e6, delta=53e3) + def test_mss_d2d_loaded_exclusion_zone(self): + """Test loading and geometry checks for MSS D2D exclusion zone parameters.""" + exclusion_zone = self.parameters.mss_d2d.beam_positioning.service_grid.grid_exclusion_zone + exclusion_zone.type = "CIRCLE" + exclusion_zone.circle.center_lat = 0.0 + exclusion_zone.circle.center_lon = 0.0 + exclusion_zone.circle.radius_km = 1.0 + + # test exclusion zone circle area + exclusion_zone._calculate_polygon() + pol = exclusion_zone._polygon + + geod = Geod(a=EARTH_RADIUS_M, b=EARTH_RADIUS_M) + + # geod area should be similar to a circle area for small area + CIRCLE_AREA = np.pi * 1e6 + CIRCLE_PERIMETER = 2 * np.pi * 1e3 + area, perimeter = geod.geometry_area_perimeter(pol) + area = abs(area) + + self.assertAlmostEqual(perimeter, CIRCLE_PERIMETER, delta=3) + # 0.2 % error: + rel_delta = 0.2 / 100 + self.assertAlmostEqual(area, CIRCLE_AREA, delta=rel_delta * CIRCLE_AREA) + + def test_mss_d2d_loaded_service_grid(self): + """ + Testing if service grid is created according to exclusion zone specification + """ + # test service grid on 2 countries + beam_radius_m = 40e3 + seed = 2 + self.parameters.mss_d2d.beam_positioning.service_grid.beam_radius = beam_radius_m + self.parameters.mss_d2d.beam_positioning.service_grid.grid_margin_from_border = beam_radius_m / 1e3 + rng = np.random.RandomState(seed) + + self.parameters.mss_d2d.beam_positioning.service_grid.country_names = [ + "Paraguay", "Brazil"] + + self.parameters.mss_d2d.beam_positioning.service_grid.grid_exclusion_zone.type = None + self.parameters.mss_d2d.beam_positioning.service_grid.grid_exclusion_zone._calculate_polygon() + self.parameters.mss_d2d.beam_positioning.service_grid.reset_grid( + "test", rng, True) + + """Test circle with same radius as margin""" + original_grid = self.parameters.mss_d2d.beam_positioning.service_grid.lon_lat_grid + + grid_exclusion_zone = self.parameters.mss_d2d.beam_positioning.service_grid.grid_exclusion_zone + grid_exclusion_zone.type = "CIRCLE" + # at frienship bridge, so should affect more than 1 grid + grid_exclusion_zone.circle.center_lat = -25.5094741 + grid_exclusion_zone.circle.center_lon = -54.6007197 + grid_exclusion_zone.circle.radius_km = beam_radius_m / 1e3 + + rng = np.random.RandomState(seed) + grid_exclusion_zone._calculate_polygon() + + self.parameters.mss_d2d.beam_positioning.service_grid.reset_grid( + "test", rng, True) + grid_w_exclusion = self.parameters.mss_d2d.beam_positioning.service_grid.lon_lat_grid + + self.assertEqual(original_grid.shape, grid_w_exclusion.shape) + + """Test circle with radius bigger than margin""" + original_grid = self.parameters.mss_d2d.beam_positioning.service_grid.lon_lat_grid + + grid_exclusion_zone = self.parameters.mss_d2d.beam_positioning.service_grid.grid_exclusion_zone + grid_exclusion_zone.type = "CIRCLE" + # at frienship bridge, so should affect more than 1 grid + grid_exclusion_zone.circle.center_lat = -25.5094741 + grid_exclusion_zone.circle.center_lon = -54.6007197 + grid_exclusion_zone.circle.radius_km = 2 * beam_radius_m / 1e3 + + rng = np.random.RandomState(seed) + grid_exclusion_zone._calculate_polygon() + + self.parameters.mss_d2d.beam_positioning.service_grid.reset_grid( + "test", rng, True) + grid_w_exclusion = self.parameters.mss_d2d.beam_positioning.service_grid.lon_lat_grid + + n_original = original_grid.shape[1] + n_after = grid_w_exclusion.shape[1] + + # aft >= orig - 6 + self.assertLessEqual(n_original - 6, n_after) + # aft < orig + self.assertLess(n_after, n_original) + if __name__ == '__main__': unittest.main() From ff2e0fcb3f25cf70c6c21dbf2acaf1c129dae684 Mon Sep 17 00:00:00 2001 From: artistrea Date: Mon, 15 Sep 2025 13:11:57 -0300 Subject: [PATCH 09/77] hotfix: plot_footprints script mss d2d parameters --- sharc/satellite/scripts/plot_footprints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sharc/satellite/scripts/plot_footprints.py b/sharc/satellite/scripts/plot_footprints.py index 784b7d166..e94897151 100644 --- a/sharc/satellite/scripts/plot_footprints.py +++ b/sharc/satellite/scripts/plot_footprints.py @@ -378,11 +378,11 @@ def plot_fp( name="Example-MSS-D2D", antenna_pattern="ITU-R-S.1528-Taylor", num_sectors=19, - antenna_s1528=antenna_params, intersite_distance=np.sqrt(3) * spotbeam_radius, cell_radius=spotbeam_radius, orbits=[orbit_1] ) + params.antenna.itu_r_s_1528 = antenna_params params.sat_is_active_if.conditions = [ # "MINIMUM_ELEVATION_FROM_ES", "LAT_LONG_INSIDE_COUNTRY", From dc004eec02069a15ec6593c3fa07ac17245d2a15 Mon Sep 17 00:00:00 2001 From: artistrea Date: Mon, 15 Sep 2025 13:18:41 -0300 Subject: [PATCH 10/77] fix: exclusion zone missing docstring on public method --- sharc/parameters/imt/parameters_imt_mss_dc.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sharc/parameters/imt/parameters_imt_mss_dc.py b/sharc/parameters/imt/parameters_imt_mss_dc.py index db18a5d9b..afcbc9504 100644 --- a/sharc/parameters/imt/parameters_imt_mss_dc.py +++ b/sharc/parameters/imt/parameters_imt_mss_dc.py @@ -251,6 +251,9 @@ def _calculate_polygon(self): ) def apply_exclusion_zone(self, lon, lat): + """ + Returns coordinates that are not contained in polygon + """ if self.type is None: return np.stack((lon, lat)) From 0e52773bba5327f7fd874f232a83d1f811b688c7 Mon Sep 17 00:00:00 2001 From: artistrea Date: Tue, 16 Sep 2025 12:07:50 -0300 Subject: [PATCH 11/77] refactor: geometry converter is now coordinate system --- .../scripts/plot_3d_param_file.py | 18 +++--- sharc/satellite/scripts/plot_footprints.py | 32 +++++----- sharc/satellite/scripts/plot_globe.py | 40 ++++++------- .../scripts/plot_orbits_single_color_3d.py | 14 ++--- sharc/simulation.py | 10 ++-- sharc/simulation_downlink.py | 2 +- sharc/simulation_uplink.py | 2 +- sharc/station_factory.py | 36 +++++------ sharc/support/sharc_geom.py | 35 ++++++----- sharc/topology/topology_factory.py | 6 +- sharc/topology/topology_imt_mss_dc.py | 54 ++++++++--------- tests/test_geometry_converter.py | 60 +++++++++---------- tests/test_station_factory_ngso.py | 14 ++--- tests/test_topology_imt_mss_dc.py | 14 ++--- 14 files changed, 168 insertions(+), 169 deletions(-) diff --git a/sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/plot_3d_param_file.py b/sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/plot_3d_param_file.py index 25b66cb86..37f7e0a2a 100644 --- a/sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/plot_3d_param_file.py +++ b/sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/plot_3d_param_file.py @@ -7,7 +7,7 @@ import plotly.graph_objects as go from pathlib import Path -from sharc.support.sharc_geom import GeometryConverter +from sharc.support.sharc_geom import CoordinateSystem from sharc.parameters.parameters import Parameters from sharc.topology.topology_factory import TopologyFactory from sharc.station_factory import StationFactory @@ -15,7 +15,7 @@ if __name__ == "__main__": - geoconv = GeometryConverter() + coord_sys = CoordinateSystem() SELECTED_SNAPSHOT_NUMBER = 0 OPAQUE_GLOBE = True print(f"Plotting drop {SELECTED_SNAPSHOT_NUMBER}") @@ -38,14 +38,14 @@ parameters.set_file_name(param_file) parameters.read_params() - geoconv.set_reference( + coord_sys.set_reference( parameters.imt.topology.central_latitude, parameters.imt.topology.central_longitude, parameters.imt.topology.central_altitude, ) print( "imt at (lat, lon, alt) = ", - (geoconv.ref_lat, geoconv.ref_long, geoconv.ref_alt), + (coord_sys.ref_lat, coord_sys.ref_long, coord_sys.ref_alt), ) import random @@ -60,7 +60,7 @@ seed = secondary_seeds[SELECTED_SNAPSHOT_NUMBER] - topology = TopologyFactory.createTopology(parameters, geoconv) + topology = TopologyFactory.createTopology(parameters, coord_sys) random_number_gen = np.random.RandomState(seed) @@ -80,7 +80,7 @@ # Create the other system (FSS, HAPS, etc...) system = StationFactory.generate_system( parameters, topology, random_number_gen, - geoconv + coord_sys ) # Create IMT user equipments @@ -91,11 +91,11 @@ ) # Plot the globe with satellite positions - fig = plot_globe_with_borders(OPAQUE_GLOBE, geoconv, False) + fig = plot_globe_with_borders(OPAQUE_GLOBE, coord_sys, False) polygons_lim = plot_mult_polygon( parameters.mss_d2d.sat_is_active_if.lat_long_inside_country.filter_polygon, - geoconv, + coord_sys, False) from functools import reduce @@ -165,7 +165,7 @@ range=(-range, range) ), camera=dict( - center=dict(x=0, y=0, z=-geoconv.get_translation() / + center=dict(x=0, y=0, z=-coord_sys.get_translation() / (2 * range)), # Look at Earth's center # eye=eye, # Camera position # center=dict(x=0, y=0, z=0), # Look at Earth's center diff --git a/sharc/satellite/scripts/plot_footprints.py b/sharc/satellite/scripts/plot_footprints.py index e94897151..57f5cf98c 100644 --- a/sharc/satellite/scripts/plot_footprints.py +++ b/sharc/satellite/scripts/plot_footprints.py @@ -10,7 +10,7 @@ from sharc.station_factory import StationFactory from sharc.parameters.parameters_mss_d2d import ParametersOrbit, ParametersMssD2d -from sharc.support.sharc_geom import GeometryConverter +from sharc.support.sharc_geom import CoordinateSystem from sharc.satellite.utils.sat_utils import ecef2lla from sharc.station_manager import StationManager from sharc.parameters.antenna.parameters_antenna_s1528 import ParametersAntennaS1528 @@ -51,7 +51,7 @@ class FootPrintOpts: def plot_fp( params, - geoconv, + coord_sys, opts=FootPrintOpts(seed=32) ): """ @@ -59,7 +59,7 @@ def plot_fp( Args: params: Parameters for the MSS D2D system. - geoconv: GeometryConverter instance for coordinate transformations. + coord_sys: CoordinateSystem instance for coordinate transformations. opts: FootPrintOpts instance with plotting options. Returns: @@ -84,13 +84,13 @@ def plot_fp( # rotated and then translated center of earth center_of_earth.x = np.array([0.0]) center_of_earth.y = np.array([0.0]) - center_of_earth.z = np.array([-geoconv.get_translation()]) + center_of_earth.z = np.array([-coord_sys.get_translation()]) - mss_d2d_manager = StationFactory.generate_mss_d2d(params, rng, geoconv) + mss_d2d_manager = StationFactory.generate_mss_d2d(params, rng, coord_sys) # Plot the globe with satellite positions fig = plot_globe_with_borders( - True, geoconv, True + True, coord_sys, True ) # Set the camera position in Plotly @@ -128,7 +128,7 @@ def plot_fp( # Satellite as fp center center_fp_at_sat = 0 # get original sat xyz - orx, ory, orz = geoconv.revert_transformed_cartesian_to_cartesian( + orx, ory, orz = coord_sys.enu2ecef( station_1.x[center_fp_at_sat], station_1.y[center_fp_at_sat], station_1.z[center_fp_at_sat], @@ -139,8 +139,8 @@ def plot_fp( # lon_vals = np.linspace(sat_long - 10.0, sat_long + 10.0, resolution) # # Ground station as fp center - # lat_vals = np.linspace(geoconv.ref_lat, geoconv.ref_lat + 10.0, resolution) - # lon_vals = np.linspace(geoconv.ref_long - 5.0, geoconv.ref_long + 5.0, resolution) + # lat_vals = np.linspace(coord_sys.ref_lat, coord_sys.ref_lat + 10.0, resolution) + # lon_vals = np.linspace(coord_sys.ref_long - 5.0, coord_sys.ref_long + 5.0, resolution) # Arbitrary range for fp calulation lat_vals = np.linspace(-33.69111, 4, resolution) @@ -155,7 +155,7 @@ def plot_fp( # Convert the lat/lon grid to transformed Cartesian coordinates. # Ensure your converter function can handle vectorized (numpy array) inputs. - x_flat, y_flat, z_flat = geoconv.convert_lla_to_transformed_cartesian(lat_flat, lon_flat, 0) + x_flat, y_flat, z_flat = coord_sys.lla2enu(lat_flat, lon_flat, 0) # creates a StationManager to calculate the gains on surf_manager = StationManager(len(x_flat)) @@ -274,7 +274,7 @@ def plot_fp( polygons_lim = plot_mult_polygon( params.sat_is_active_if.lat_long_inside_country.filter_polygon, - geoconv, + coord_sys, True, 2 ) @@ -302,7 +302,7 @@ def plot_fp( lon = np.array(xy_coords[0]) lat = np.array(xy_coords[1]) - x, y, z = geoconv.convert_lla_to_transformed_cartesian(lat, lon, 1e3) + x, y, z = coord_sys.lla2enu(lat, lon, 1e3) fig.add_trace(go.Scatter3d( x=x / 1e3, @@ -316,7 +316,7 @@ def plot_fp( if params.beam_positioning.service_grid.grid_exclusion_zone._polygon is not None: polygons_lim = plot_mult_polygon( params.beam_positioning.service_grid.grid_exclusion_zone._polygon, - geoconv, + coord_sys, True, 2 ) @@ -410,13 +410,13 @@ def plot_fp( params.propagate_parameters() params.validate("opa") - geoconv = GeometryConverter() + coord_sys = CoordinateSystem() sys_lat = -14.5 sys_long = -52 sys_alt = 1200 - geoconv.set_reference( + coord_sys.set_reference( sys_lat, sys_long, sys_alt ) @@ -430,6 +430,6 @@ def plot_fp( ] for opt in opts: - fig = plot_fp(params, geoconv, opt) + fig = plot_fp(params, coord_sys, opt) # fig.write_image(f"fp.png") fig.show() diff --git a/sharc/satellite/scripts/plot_globe.py b/sharc/satellite/scripts/plot_globe.py index 61a034786..81d8fda9e 100644 --- a/sharc/satellite/scripts/plot_globe.py +++ b/sharc/satellite/scripts/plot_globe.py @@ -6,7 +6,7 @@ from sharc.satellite.ngso.constants import EARTH_RADIUS_KM -def plot_back(fig, geoconv, in_km): +def plot_back(fig, coord_sys, in_km): """back half of sphere""" clor = 'rgb(220, 220, 220)' # Create a mesh grid for latitude and longitude. @@ -22,7 +22,7 @@ def plot_back(fig, geoconv, in_km): lat_flat = lat.flatten() lon_flat = lon.flatten() - if geoconv is None: + if coord_sys is None: lon = lon * np.pi / 180 lat = lat * np.pi / 180 @@ -36,7 +36,7 @@ def plot_back(fig, geoconv, in_km): # Convert the lat/lon grid to transformed Cartesian coordinates. # Ensure your converter function can handle vectorized (numpy array) # inputs. - x_flat, y_flat, z_flat = geoconv.convert_lla_to_transformed_cartesian( + x_flat, y_flat, z_flat = coord_sys.lla2enu( lat_flat, lon_flat, 0) if in_km: x_flat, y_flat, z_flat = x_flat / 1e3, y_flat / 1e3, z_flat / 1e3 @@ -58,7 +58,7 @@ def plot_back(fig, geoconv, in_km): ) -def plot_front(fig, geoconv, in_km): +def plot_front(fig, coord_sys, in_km): """front half of sphere""" clor = 'rgb(220, 220, 220)' @@ -75,7 +75,7 @@ def plot_front(fig, geoconv, in_km): lat_flat = lat.flatten() lon_flat = lon.flatten() - if geoconv is None: + if coord_sys is None: lon = lon * np.pi / 180 lat = lat * np.pi / 180 @@ -89,7 +89,7 @@ def plot_front(fig, geoconv, in_km): # Convert the lat/lon grid to transformed Cartesian coordinates. # Ensure your converter function can handle vectorized (numpy array) # inputs. - x_flat, y_flat, z_flat = geoconv.convert_lla_to_transformed_cartesian( + x_flat, y_flat, z_flat = coord_sys.lla2enu( lat_flat, lon_flat, 0) if in_km: x_flat, y_flat, z_flat = x_flat / 1e3, y_flat / 1e3, z_flat / 1e3 @@ -111,13 +111,13 @@ def plot_front(fig, geoconv, in_km): ) -def plot_polygon(poly, geoconv, in_km, alt=0): +def plot_polygon(poly, coord_sys, in_km, alt=0): """ Convert a polygon's coordinates to 3D Cartesian coordinates for plotting on a globe. Args: poly: The polygon object with exterior coordinates. - geoconv: Geodetic converter object or None. + coord_sys: Geodetic converter object or None. in_km (bool): Whether to use kilometers for the output coordinates. alt (float, optional): Altitude to add to the radius. Defaults to 0. @@ -129,7 +129,7 @@ def plot_polygon(poly, geoconv, in_km, alt=0): lon = np.array(xy_coords[0]) lat = np.array(xy_coords[1]) - if geoconv is None: + if coord_sys is None: lon = lon * np.pi / 180 lat = lat * np.pi / 180 @@ -140,7 +140,7 @@ def plot_polygon(poly, geoconv, in_km, alt=0): y = R * np.cos(lat) * np.sin(lon) z = R * np.sin(lat) else: - x, y, z = geoconv.convert_lla_to_transformed_cartesian( + x, y, z = coord_sys.lla2enu( lat, lon, alt * (1e3 if in_km else 1)) if in_km: x, y, z = x / 1e3, y / 1e3, z / 1e3 @@ -148,13 +148,13 @@ def plot_polygon(poly, geoconv, in_km, alt=0): return x, y, z -def plot_mult_polygon(mult_poly, geoconv, in_km: bool, alt=0): +def plot_mult_polygon(mult_poly, coord_sys, in_km: bool, alt=0): """ Convert a MultiPolygon or Polygon to a list of 3D Cartesian coordinate arrays for plotting. Args: mult_poly: The MultiPolygon or Polygon object. - geoconv: Geodetic converter object or None. + coord_sys: Geodetic converter object or None. in_km (bool): Whether to use kilometers for the output coordinates. alt (float, optional): Altitude to add to the radius. Defaults to 0. @@ -162,19 +162,19 @@ def plot_mult_polygon(mult_poly, geoconv, in_km: bool, alt=0): list: A list of tuples containing arrays of x, y, z coordinates for each polygon. """ if mult_poly.geom_type == 'Polygon': - return [plot_polygon(mult_poly, geoconv, in_km, alt)] + return [plot_polygon(mult_poly, coord_sys, in_km, alt)] elif mult_poly.geom_type == 'MultiPolygon': - return [plot_polygon(poly, geoconv, in_km, alt) + return [plot_polygon(poly, coord_sys, in_km, alt) for poly in mult_poly.geoms] -def plot_globe_with_borders(opaque_globe: bool, geoconv, in_km: bool): +def plot_globe_with_borders(opaque_globe: bool, coord_sys, in_km: bool): """ Plot a 3D globe with country borders using Plotly. Args: opaque_globe (bool): Whether to plot the globe as opaque. - geoconv: Geodetic converter object or None. + coord_sys: Geodetic converter object or None. in_km (bool): Whether to use kilometers for the output coordinates. Returns: @@ -196,8 +196,8 @@ def plot_globe_with_borders(opaque_globe: bool, geoconv, in_km: bool): # margin=dict(l=0, r=0, b=0, t=0) # ) if opaque_globe: - plot_front(fig, geoconv, in_km) - plot_back(fig, geoconv, in_km) + plot_front(fig, coord_sys, in_km) + plot_back(fig, coord_sys, in_km) x_all, y_all, z_all = [], [], [] for i in gdf.index: @@ -206,7 +206,7 @@ def plot_globe_with_borders(opaque_globe: bool, geoconv, in_km: bool): polys = gdf.loc[i].geometry # Polygons or MultiPolygons if polys.geom_type == 'Polygon': - x, y, z = plot_polygon(polys, geoconv, in_km, 1 if in_km else 1e3) + x, y, z = plot_polygon(polys, coord_sys, in_km, 1 if in_km else 1e3) x_all.extend(x) x_all.extend([None]) # None separates different polygons y_all.extend(y) @@ -218,7 +218,7 @@ def plot_globe_with_borders(opaque_globe: bool, geoconv, in_km: bool): for poly in polys.geoms: x, y, z = plot_polygon( - poly, geoconv, in_km, 1 if in_km else 1e3) + poly, coord_sys, in_km, 1 if in_km else 1e3) x_all.extend(x) x_all.extend([None]) # None separates different polygons y_all.extend(y) diff --git a/sharc/satellite/scripts/plot_orbits_single_color_3d.py b/sharc/satellite/scripts/plot_orbits_single_color_3d.py index 33bda2100..362e1d7d7 100644 --- a/sharc/satellite/scripts/plot_orbits_single_color_3d.py +++ b/sharc/satellite/scripts/plot_orbits_single_color_3d.py @@ -6,15 +6,15 @@ import numpy as np import plotly.graph_objects as go -from sharc.support.sharc_geom import GeometryConverter +from sharc.support.sharc_geom import CoordinateSystem from sharc.station_manager import StationManager -geoconv = GeometryConverter() +coord_sys = CoordinateSystem() sys_lat = -14.5 sys_long = -45 sys_alt = 1200 -geoconv.set_reference( +coord_sys.set_reference( sys_lat, sys_long, sys_alt ) @@ -72,19 +72,19 @@ # Plot the ground station (blue marker) # ground_sta_pos = lla2ecef(sys_lat, sys_long, sys_alt) - ground_sta_pos = geoconv.convert_lla_to_transformed_cartesian( + ground_sta_pos = coord_sys.lla2enu( sys_lat, sys_long, 1200.0) center_of_earth = StationManager(1) # rotated and then translated center of earth center_of_earth.x = np.array([0.0]) center_of_earth.y = np.array([0.0]) - center_of_earth.z = np.array([-geoconv.get_translation()]) + center_of_earth.z = np.array([-coord_sys.get_translation()]) vis_elevation = [] for _ in range(NUM_DROPS): # Generate satellite positions using the StationFactory - mss_d2d_manager = StationFactory.generate_mss_d2d(params, rng, geoconv) + mss_d2d_manager = StationFactory.generate_mss_d2d(params, rng, coord_sys) # Extract satellite positions x_vec = mss_d2d_manager.x / 1e3 # (Km) @@ -117,7 +117,7 @@ # Plot the globe with satellite positions fig = plot_globe_with_borders( - True, geoconv, True + True, coord_sys, True ) # Plot all satellites (red markers) diff --git a/sharc/simulation.py b/sharc/simulation.py index 120807227..45908a079 100644 --- a/sharc/simulation.py +++ b/sharc/simulation.py @@ -15,7 +15,7 @@ from sharc.support.enumerations import StationType from sharc.topology.topology_factory import TopologyFactory -from sharc.support.sharc_geom import GeometryConverter +from sharc.support.sharc_geom import CoordinateSystem from sharc.parameters.parameters import Parameters from sharc.station_manager import StationManager from sharc.results import Results @@ -81,20 +81,20 @@ def __init__(self, parameters: Parameters, parameter_file: str): self.co_channel = self.parameters.general.enable_cochannel self.adjacent_channel = self.parameters.general.enable_adjacent_channel - geometry_converter = GeometryConverter() + coordinate_system = CoordinateSystem() if self.parameters.imt.topology.central_latitude is not None: - geometry_converter.set_reference( + coordinate_system.set_reference( self.parameters.imt.topology.central_latitude, self.parameters.imt.topology.central_longitude, self.parameters.imt.topology.central_altitude, ) - self.geometry_converter = geometry_converter + self.coordinate_system = coordinate_system self.topology = TopologyFactory.createTopology( self.parameters, - geometry_converter + coordinate_system ) self.bs_power_gain = 0 diff --git a/sharc/simulation_downlink.py b/sharc/simulation_downlink.py index bbe977a9f..04a241096 100644 --- a/sharc/simulation_downlink.py +++ b/sharc/simulation_downlink.py @@ -72,7 +72,7 @@ def snapshot(self, *args, **kwargs): # Create the other system (FSS, HAPS, etc...) self.system = StationFactory.generate_system( self.parameters, self.topology, random_number_gen, - geometry_converter=self.geometry_converter + coordinate_system=self.coordinate_system ) # Create IMT user equipments diff --git a/sharc/simulation_uplink.py b/sharc/simulation_uplink.py index 2a4e48bbb..7d547f114 100644 --- a/sharc/simulation_uplink.py +++ b/sharc/simulation_uplink.py @@ -61,7 +61,7 @@ def snapshot(self, *args, **kwargs): # Create the other system (FSS, HAPS, etc...) self.system = StationFactory.generate_system( self.parameters, self.topology, random_number_gen, - geometry_converter=self.geometry_converter + coordinate_system=self.coordinate_system ) # Create IMT user equipments diff --git a/sharc/station_factory.py b/sharc/station_factory.py index 66f93b709..de9fac813 100644 --- a/sharc/station_factory.py +++ b/sharc/station_factory.py @@ -58,7 +58,7 @@ from sharc.topology.topology_imt_mss_dc import TopologyImtMssDc 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_geom import CoordinateSystem from sharc.support.sharc_utils import wrap2_180 @@ -676,7 +676,7 @@ def generate_system( parameters: Parameters, topology: Topology, random_number_gen: np.random.RandomState, - geometry_converter=GeometryConverter() + coordinate_system=CoordinateSystem() ): """Generate the system based on the provided parameters and topology. @@ -688,8 +688,8 @@ def generate_system( Topology object containing station positions. random_number_gen : np.random.RandomState Random number generator instance. - geometry_converter : GeometryConverter, optional - Converter for coordinate transformations (default is GeometryConverter()). + coordinate_system : CoordinateSystem, optional + Converter for coordinate transformations (default is CoordinateSystem()). Returns ------- @@ -735,7 +735,7 @@ def generate_system( return StationFactory.generate_mss_ss(parameters.mss_ss) elif parameters.general.system == "MSS_D2D": return StationFactory.generate_mss_d2d( - parameters.mss_d2d, random_number_gen, geometry_converter) + parameters.mss_d2d, random_number_gen, coordinate_system) else: sys.stderr.write( "ERROR\nInvalid system: " + @@ -763,13 +763,13 @@ def generate_single_space_station( space_station.station_type = StationType.SINGLE_SPACE_STATION space_station.is_space_station = True - geoconv = GeometryConverter() - geoconv.set_reference( + coord_sys = CoordinateSystem() + coord_sys.set_reference( param.geometry.es_lat_deg, param.geometry.es_long_deg, param.geometry.es_altitude, ) - x, y, z = geoconv.convert_lla_to_transformed_cartesian( + x, y, z = coord_sys.lla2enu( param.geometry.location.fixed.lat_deg, param.geometry.location.fixed.long_deg, param.geometry.altitude, @@ -785,7 +785,7 @@ def generate_single_space_station( space_station.azimuth = np.rad2deg( np.arctan2(-space_station.y, -space_station.x)) elif param.geometry.azimuth.type == "POINTING_AT_LAT_LONG_ALT": - px, py, pz = geoconv.convert_lla_to_transformed_cartesian( + px, py, pz = coord_sys.lla2enu( param.geometry.pointing_at_lat, param.geometry.pointing_at_long, param.geometry.pointing_at_alt, @@ -811,7 +811,7 @@ def generate_single_space_station( space_station.x))) space_station.elevation = -gnd_elev elif param.geometry.elevation.type == "POINTING_AT_LAT_LONG_ALT": - px, py, pz = geoconv.convert_lla_to_transformed_cartesian( + px, py, pz = coord_sys.lla2enu( param.geometry.pointing_at_lat, param.geometry.pointing_at_long, param.geometry.pointing_at_alt, @@ -1615,7 +1615,7 @@ def generate_mss_ss(param_mss: ParametersMssSs): def generate_mss_d2d( params: ParametersMssD2d, random_number_gen: np.random.RandomState, - geometry_converter: GeometryConverter, + coordinate_system: CoordinateSystem, ): """ Generate the MSS D2D constellation with support for multiple orbits and base station visibility. @@ -1626,7 +1626,7 @@ def generate_mss_d2d( Parameters for the MSS D2D system, including orbits and antenna configuration. random_number_gen : np.random.RandomState Random number generator for generating satellite positions. - geometry_converter : GeometryConverter + coordinate_system : CoordinateSystem A converter that has already set a reference for coordinates transformation Returns @@ -1634,11 +1634,11 @@ def generate_mss_d2d( StationManager A StationManager object containing satellite configurations and positions. """ - geometry_converter.validate() + coordinate_system.validate() # Initialize the StationManager for the MSS D2D system mss_d2d_values = TopologyImtMssDc.get_coordinates( - geometry_converter, + coordinate_system, params, random_number_gen, ) @@ -1853,13 +1853,13 @@ def get_random_position(num_stas: int, if __name__ == '__main__': rand_gen = np.random.RandomState(101) - geometry_converter = GeometryConverter() + coordinate_system = CoordinateSystem() # somente vou utilizar a translação que o satĆ©lite teoricamente sofreu: ref_lat = -14.1 ref_long = -45.1 ref_alt = 1200 - geometry_converter.set_reference(ref_lat, ref_long, ref_alt) + coordinate_system.set_reference(ref_lat, ref_long, ref_alt) from sharc.parameters.parameters_orbit import ParametersOrbit orbit = ParametersOrbit( @@ -1880,7 +1880,7 @@ def get_random_position(num_stas: int, params.sat_is_active_if.conditions = ["MINIMUM_ELEVATION_FROM_ES"] params.sat_is_active_if.minimum_elevation_from_es = 5.0 - topology = TopologyImtMssDc(params, geometry_converter) + topology = TopologyImtMssDc(params, coordinate_system) topology.calculate_coordinates(rand_gen) @@ -1907,7 +1907,7 @@ def get_random_position(num_stas: int, ) from sharc.satellite.scripts.plot_globe import plot_globe_with_borders - fig = plot_globe_with_borders(True, geometry_converter, False) + fig = plot_globe_with_borders(True, coordinate_system, False) import plotly.graph_objects as go diff --git a/sharc/support/sharc_geom.py b/sharc/support/sharc_geom.py index 39b30ce9a..77e5b3fbf 100644 --- a/sharc/support/sharc_geom.py +++ b/sharc/support/sharc_geom.py @@ -175,15 +175,14 @@ def rotate_angles_based_on_new_nadir(elev, azim, nadir_elev, nadir_azim): # NOTE: this works for both spherical an ellipsoidal Earth, # just need to change ecef2lla and lla2ecef implementations -# TODO: refactor class and method names -class GeometryConverter(): +class CoordinateSystem(): """Class for transforming coordinates to local ENU using a reference lat, lon, alt. This class receives a reference lat, lon, alt and may transform other coordinate types to local ENU. """ def __init__(self): - """Initialize GeometryConverter with unset reference coordinates.""" + """Initialize CoordinateSystem with unset reference coordinates.""" # geodesical self.ref_lat = None self.ref_long = None @@ -280,7 +279,7 @@ def set_reference(self, ref_lat: float, ref_long: float, ref_alt: float): # can also be confirmed comparing to here: # https://gssc.esa.int/navipedia/index.php/Transformations_between_ECEF_and_ENU_coordinates - def convert_cartesian_to_transformed_cartesian( + def ecef2enu( self, x, y, z, *, translate=None ): """Transform points by the same transformation required to bring reference to (0,0,0). @@ -314,7 +313,7 @@ def convert_cartesian_to_transformed_cartesian( # rotate so axis are same as ENU return self.rotation.apply(xyz).T - def revert_transformed_cartesian_to_cartesian( + def enu2ecef( self, x2, y2, z2, *, translate=None ): """Reverse transformed points by the same transformation required to bring reference to (0,0,0). @@ -350,7 +349,7 @@ def revert_transformed_cartesian_to_cartesian( # translate earth reference back to its original ecef coord return (xyz + translate_val[np.newaxis, :]).T - def convert_lla_to_transformed_cartesian( + def lla2enu( self, lat: np.array, long: np.array, alt: np.array ): """Convert latitude, longitude, altitude to transformed cartesian coordinates. @@ -375,9 +374,9 @@ def convert_lla_to_transformed_cartesian( # get cartesian position by geodesical x, y, z = lla2ecef(lat, long, alt) - return self.convert_cartesian_to_transformed_cartesian(x, y, z) + return self.ecef2enu(x, y, z) - def convert_station_3d_to_2d( + def station_ecef2enu( self, station: StationManager, idx=None ) -> None: """In-place rotate and translate all coordinates so that reference parameters end up in (0,0,0). @@ -394,10 +393,10 @@ def convert_station_3d_to_2d( """ # transform positions if idx is None: - nx, ny, nz = self.convert_cartesian_to_transformed_cartesian( + nx, ny, nz = self.ecef2enu( station.x, station.y, station.z) else: - nx, ny, nz = self.convert_cartesian_to_transformed_cartesian( + nx, ny, nz = self.ecef2enu( station.x[idx], station.y[idx], station.z[idx]) if idx is None: @@ -414,7 +413,7 @@ def convert_station_3d_to_2d( # transform pointing vectors, without considering geodesical earth # coord system - pointing_vec_x, pointing_vec_y, pointing_vec_z = self.convert_cartesian_to_transformed_cartesian( + pointing_vec_x, pointing_vec_y, pointing_vec_z = self.ecef2enu( pointing_vec_x, pointing_vec_y, pointing_vec_z, translate=0) if idx is None: @@ -435,7 +434,7 @@ def convert_station_3d_to_2d( station.azimuth[idx] = azimuth station.elevation[idx] = elevation - def revert_station_2d_to_3d( + def station_enu2ecef( self, station: StationManager, idx=None ) -> None: """In-place rotate and translate all coordinates so that reference parameters end up in (0,0,0). @@ -452,10 +451,10 @@ def revert_station_2d_to_3d( """ # transform positions if idx is None: - nx, ny, nz = self.revert_transformed_cartesian_to_cartesian( + nx, ny, nz = self.enu2ecef( station.x, station.y, station.z) else: - nx, ny, nz = self.revert_transformed_cartesian_to_cartesian( + nx, ny, nz = self.enu2ecef( station.x[idx], station.y[idx], station.z[idx]) if idx is None: @@ -472,7 +471,7 @@ def revert_station_2d_to_3d( # transform pointing vectors, without considering geodesical earth # coord system - pointing_vec_x, pointing_vec_y, pointing_vec_z = self.revert_transformed_cartesian_to_cartesian( + pointing_vec_x, pointing_vec_y, pointing_vec_z = self.enu2ecef( pointing_vec_x, pointing_vec_y, pointing_vec_z, translate=0) if idx is None: @@ -792,13 +791,13 @@ def generate_grid_in_multipolygon( # print(get_rotation_matrix_between_vecs(np.array([0,1,0]), np.array([0,0,1]))) - geoconv = GeometryConverter() + coord_sys = CoordinateSystem() sys_lat = 89 sys_long = 0 sys_alt = 1200 - # geoconv.set_reference( + # coord_sys.set_reference( # sys_lat, sys_long, sys_alt # ) # stat = StationManager(1) @@ -817,7 +816,7 @@ def generate_grid_in_multipolygon( # print("stat.azimuth", stat.azimuth) # print("stat.elevation", stat.elevation) # print("#########") - # geoconv.convert_station_3d_to_2d(stat) + # coord_sys.station_ecef2enu(stat) # print("#########") # print("stat.x", stat.x) diff --git a/sharc/topology/topology_factory.py b/sharc/topology/topology_factory.py index 646df3934..bec0815b8 100644 --- a/sharc/topology/topology_factory.py +++ b/sharc/topology/topology_factory.py @@ -14,7 +14,7 @@ from sharc.topology.topology_ntn import TopologyNTN from sharc.topology.topology_single_base_station import TopologySingleBaseStation from sharc.parameters.parameters import Parameters -from sharc.support.sharc_geom import GeometryConverter +from sharc.support.sharc_geom import CoordinateSystem class TopologyFactory(object): @@ -22,7 +22,7 @@ class TopologyFactory(object): @staticmethod def createTopology(parameters: Parameters, - geometry_converter: GeometryConverter) -> Topology: + coordinate_system: CoordinateSystem) -> Topology: """Create and return a topology object based on the provided parameters.""" if parameters.imt.topology.type == "SINGLE_BS": return TopologySingleBaseStation( @@ -55,7 +55,7 @@ def createTopology(parameters: Parameters, elif parameters.imt.topology.type == "MSS_DC": return TopologyImtMssDc( parameters.imt.topology.mss_dc, - geometry_converter + coordinate_system ) else: sys.stderr.write( diff --git a/sharc/topology/topology_imt_mss_dc.py b/sharc/topology/topology_imt_mss_dc.py index 292f37b5f..b307ec6ec 100644 --- a/sharc/topology/topology_imt_mss_dc.py +++ b/sharc/topology/topology_imt_mss_dc.py @@ -6,7 +6,7 @@ The Space Stations positions are generated from the Keplerian elements of the orbits in the OrbitModel class. Only a subset of Space Stations are used, which are the ones that are visible to the UE. After satellite visibility is calculated, the ECEF coordinates are transformed to a new cartesian coordinate system -centered at the reference point defined in the GeometryConverter object. +centered at the reference point defined in the CoordinateSystem object. The azimuth and elevation angles are also rotated to the new coordinate system. The visible Space Stations are then used to generate the IMT Base Stations. """ @@ -21,7 +21,7 @@ from sharc.parameters.imt.parameters_imt_mss_dc import ParametersImtMssDc from sharc.parameters.parameters_orbit import ParametersOrbit from sharc.satellite.ngso.orbit_model import OrbitModel -from sharc.support.sharc_geom import GeometryConverter, rotate_angles_based_on_new_nadir +from sharc.support.sharc_geom import CoordinateSystem, rotate_angles_based_on_new_nadir from sharc.topology.topology_ntn import TopologyNTN from sharc.satellite.utils.sat_utils import calc_elevation from sharc.support.sharc_geom import lla2ecef, cartesian_to_polar, polar_to_cartesian @@ -36,16 +36,16 @@ class TopologyImtMssDc(Topology): """ def __init__(self, params: ParametersImtMssDc, - geometry_converter: GeometryConverter): + coordinate_system: CoordinateSystem): """Initialize the IMT MSS-DC topology with parameters and geometry converter. Parameters ---------- params : ParametersImtMssDc Input parameters for the IMT MSS-DC topology. - geometry_converter : GeometryConverter - GeometryConverter object that converts the ECEF coordinate system to one - centered at GeometryConverter.reference. + coordinate_system : CoordinateSystem + CoordinateSystem object that converts the ECEF coordinate system to one + centered at CoordinateSystem.reference. """ # That means the we need to pass the groud reference points to the base # stations generator @@ -53,7 +53,7 @@ def __init__(self, params: ParametersImtMssDc, self.num_sectors = params.num_beams # Specific attributes - self.geometry_converter = geometry_converter + self.coordinate_system = coordinate_system self.orbit_params = params self.space_station_x = None self.space_station_y = None @@ -68,7 +68,7 @@ def __init__(self, params: ParametersImtMssDc, @staticmethod def get_coordinates( - geometry_converter: GeometryConverter, + coordinate_system: CoordinateSystem, orbit_params: ParametersImtMssDc, random_number_gen=np.random.RandomState(), ): @@ -168,13 +168,13 @@ def get_coordinates( if "MINIMUM_ELEVATION_FROM_ES" in orbit_params.sat_is_active_if.conditions: # Calculate satellite visibility from base stations elev_from_bs = calc_elevation( - geometry_converter.ref_lat, # Latitude of base station + coordinate_system.ref_lat, # Latitude of base station pos_vec['lat'], # Latitude of satellites - geometry_converter.ref_long, # Longitude of base station + coordinate_system.ref_long, # Longitude of base station pos_vec['lon'], # Longitude of satellites # Perigee altitude in kilometers sat_height=pos_vec['alt'] * 1e3, - es_height=geometry_converter.ref_alt, + es_height=coordinate_system.ref_alt, ) # Determine visible satellites based on minimum elevation @@ -187,13 +187,13 @@ def get_coordinates( if "MINIMUM_ELEVATION_FROM_ES" not in orbit_params.sat_is_active_if.conditions: # Calculate satellite visibility from base stations elev_from_bs = calc_elevation( - geometry_converter.ref_lat, # Latitude of base station + coordinate_system.ref_lat, # Latitude of base station pos_vec['lat'], # Latitude of satellites - geometry_converter.ref_long, # Longitude of base station + coordinate_system.ref_long, # Longitude of base station pos_vec['lon'], # Longitude of satellites # Perigee altitude in kilometers sat_height=pos_vec['alt'] * 1e3, - es_height=geometry_converter.ref_alt, + es_height=coordinate_system.ref_alt, ) # Determine visible satellites based on minimum elevation @@ -259,7 +259,7 @@ def get_coordinates( # Convert the ECEF coordinates to the transformed cartesian coordinates and set the Space Station positions # used to generetate the IMT Base Stations space_station_x, space_station_y, space_station_z = \ - geometry_converter.convert_cartesian_to_transformed_cartesian(space_station_x, space_station_y, space_station_z) + coordinate_system.ecef2enu(space_station_x, space_station_y, space_station_z) # Rotate the azimuth and elevation angles off the center beam the new # transformed cartesian coordinates @@ -269,14 +269,14 @@ def get_coordinates( pointing_vec_x, pointing_vec_y, pointing_vec_z = polar_to_cartesian( r, all_azimuth, all_elevation) pointing_vec_x, pointing_vec_y, pointing_vec_z = \ - geometry_converter.convert_cartesian_to_transformed_cartesian( + coordinate_system.ecef2enu( pointing_vec_x, pointing_vec_y, pointing_vec_z, translate=0) _, all_azimuth, all_elevation = cartesian_to_polar( pointing_vec_x, pointing_vec_y, pointing_vec_z) beams_elev, beams_azim, sx, sy = TopologyImtMssDc.get_satellite_pointing( random_number_gen, - geometry_converter, + coordinate_system, orbit_params, total_active_satellites, all_space_station_x, all_space_station_y, all_space_station_z, @@ -353,7 +353,7 @@ def get_coordinates( @staticmethod def get_satellite_pointing( random_number_gen: np.random.RandomState, - geometry_converter: GeometryConverter, + coordinate_system: CoordinateSystem, orbit_params: ParametersImtMssDc, total_active_satellites: int, all_sat_x: np.ndarray, @@ -442,7 +442,7 @@ def get_satellite_pointing( pointing_vec_x, pointing_vec_y, pointing_vec_z = polar_to_cartesian( r, azim, elev) pointing_vec_x, pointing_vec_y, pointing_vec_z = \ - geometry_converter.convert_cartesian_to_transformed_cartesian( + coordinate_system.ecef2enu( pointing_vec_x, pointing_vec_y, pointing_vec_z, translate=0) _, azim, elev = cartesian_to_polar( pointing_vec_x, pointing_vec_y, pointing_vec_z) @@ -632,10 +632,10 @@ def get_distr( def calculate_coordinates(self, random_number_gen=np.random.RandomState()): """Compute the coordinates of the visible space stations.""" - self.geometry_converter.validate() + self.coordinate_system.validate() sat_values = self.get_coordinates( - self.geometry_converter, + self.coordinate_system, self.orbit_params, random_number_gen) @@ -685,7 +685,7 @@ def transform_ue_xyz(self, bs_i, x, y, z): self.space_station_x[bs_i] ** 2 + self.space_station_y[bs_i] ** 2), self.space_station_z[bs_i] + - self.geometry_converter.get_translation()) + self.coordinate_system.get_translation()) # get around z axis around_z = np.arctan2( @@ -706,7 +706,7 @@ def transform_ue_xyz(self, bs_i, x, y, z): y = ny # translate ue back so other system is in (0,0,0) - z -= self.geometry_converter.get_translation() + z -= self.coordinate_system.get_translation() if convert_to_scalar: return (to_scalar(x), to_scalar(y), to_scalar(z)) @@ -716,7 +716,7 @@ def transform_ue_xyz(self, bs_i, x, y, z): # Example usage if __name__ == '__main__': from sharc.parameters.imt.parameters_imt_mss_dc import ParametersImtMssDc - from sharc.support.sharc_geom import GeometryConverter + from sharc.support.sharc_geom import CoordinateSystem # Define the parameters for the IMT MSS-DC topology # SystemA Orbit parameters @@ -752,11 +752,11 @@ def transform_ue_xyz(self, bs_i, x, y, z): params.validate("validation_at_main") # Define the geometry converter - geometry_converter = GeometryConverter() - geometry_converter.set_reference(-15.0, -42.0, 1200) + coordinate_system = CoordinateSystem() + coordinate_system.set_reference(-15.0, -42.0, 1200) # Instantiate the IMT MSS-DC topology - imt_mss_dc_topology = TopologyImtMssDc(params, geometry_converter) + imt_mss_dc_topology = TopologyImtMssDc(params, coordinate_system) # Calculate the coordinates of the space stations rng = np.random.RandomState(101) diff --git a/tests/test_geometry_converter.py b/tests/test_geometry_converter.py index 674d6e219..7d8b3ddd5 100644 --- a/tests/test_geometry_converter.py +++ b/tests/test_geometry_converter.py @@ -1,30 +1,30 @@ import unittest import numpy as np import numpy.testing as npt -from sharc.support.sharc_geom import GeometryConverter +from sharc.support.sharc_geom import CoordinateSystem from sharc.satellite.utils.sat_utils import ecef2lla, lla2ecef from sharc.station_manager import StationManager class TestGeometryConverter(unittest.TestCase): - """Unit tests for the GeometryConverter class and related coordinate transformations.""" + """Unit tests for the CoordinateSystem class and related coordinate transformations.""" def setUp(self): - """Set up test fixtures for GeometryConverter tests.""" - self.conv0_0km = GeometryConverter() + """Set up test fixtures for CoordinateSystem tests.""" + self.conv0_0km = CoordinateSystem() self.conv0_0km.set_reference( 0, 0, 0 ) - self.conv0_52km = GeometryConverter() + self.conv0_52km = CoordinateSystem() self.conv0_52km.set_reference( 0, 0, 52e3 ) - self.conv1_0km = GeometryConverter() + self.conv1_0km = CoordinateSystem() self.conv1_0km.set_reference( -15, -47, 0 ) - self.conv1_10km = GeometryConverter() + self.conv1_10km = CoordinateSystem() self.conv1_10km.set_reference( -15, -47, 10e3 ) @@ -66,21 +66,21 @@ def test_set_reference(self): def test_reference_ecef(self): """Test ECEF to LLA conversion for reference points.""" - for conv in self.all_converters: - lat, lon, alt = ecef2lla(conv.ref_x, conv.ref_y, conv.ref_z) + for coord_sys in self.all_converters: + lat, lon, alt = ecef2lla(coord_sys.ref_x, coord_sys.ref_y, coord_sys.ref_z) # ecef2lla approximation requires "almost equal" directive - print("conv.ref_lat", conv.ref_lat) - self.assertAlmostEqual(lat[0], conv.ref_lat, places=8) - self.assertAlmostEqual(lon[0], conv.ref_long, places=8) - self.assertAlmostEqual(alt[0], conv.ref_alt, places=8) + print("coord_sys.ref_lat", coord_sys.ref_lat) + self.assertAlmostEqual(lat[0], coord_sys.ref_lat, places=8) + self.assertAlmostEqual(lon[0], coord_sys.ref_long, places=8) + self.assertAlmostEqual(alt[0], coord_sys.ref_alt, places=8) def test_ecef_to_enu(self): """Test ECEF to ENU coordinate transformation.""" # for each converter defined at the setup - for conv in self.all_converters: + for coord_sys in self.all_converters: # check if reference point always goes to (0,0,0) - x, y, z = conv.convert_cartesian_to_transformed_cartesian( - conv.ref_x, conv.ref_y, conv.ref_z) + x, y, z = coord_sys.ecef2enu( + coord_sys.ref_x, coord_sys.ref_y, coord_sys.ref_z) self.assertEqual(x, 0) self.assertEqual(y, 0) self.assertEqual(z, 0) @@ -88,9 +88,9 @@ def test_ecef_to_enu(self): def test_lla_to_enu(self): """Test LLA to ENU coordinate transformation.""" # for each converter defined at the setup - for conv in self.all_converters: - x, y, z = conv.convert_lla_to_transformed_cartesian( - conv.ref_lat, conv.ref_long, conv.ref_alt) + for coord_sys in self.all_converters: + x, y, z = coord_sys.lla2enu( + coord_sys.ref_lat, coord_sys.ref_long, coord_sys.ref_alt) self.assertEqual(x, 0) self.assertEqual(y, 0) self.assertEqual(z, 0) @@ -98,17 +98,17 @@ def test_lla_to_enu(self): def test_enu_to_ecef(self): """Test ENU to ECEF coordinate transformation.""" # for each converter defined at the setup - for conv in self.all_converters: + for coord_sys in self.all_converters: # check if the reverse is true - x, y, z = conv.revert_transformed_cartesian_to_cartesian(0, 0, 0) - self.assertEqual(x, conv.ref_x) - self.assertEqual(y, conv.ref_y) - self.assertEqual(z, conv.ref_z) + x, y, z = coord_sys.enu2ecef(0, 0, 0) + self.assertEqual(x, coord_sys.ref_x) + self.assertEqual(y, coord_sys.ref_y) + self.assertEqual(z, coord_sys.ref_z) def test_station_converter(self): """Test station coordinate and orientation conversions between ECEF and ENU.""" # for each converter defined at the setup - for conv in self.all_converters: + for coord_sys in self.all_converters: rng = np.random.default_rng(0) n_samples = 100 stations = StationManager(n_samples) @@ -119,9 +119,9 @@ def test_station_converter(self): rng.uniform(0, 35e3, n_samples), ) # set first station to be the reference - xyz_bef[0][0] = conv.ref_x - xyz_bef[1][0] = conv.ref_y - xyz_bef[2][0] = conv.ref_z + xyz_bef[0][0] = coord_sys.ref_x + xyz_bef[1][0] = coord_sys.ref_y + xyz_bef[2][0] = coord_sys.ref_z # point them randomly azim_bef = rng.uniform(-180, 180, n_samples) elev_bef = rng.uniform(-90, 90, n_samples) @@ -135,7 +135,7 @@ def test_station_converter(self): off_axis_bef = stations.get_off_axis_angle(stations) # convert stations to enu - conv.convert_station_3d_to_2d(stations) + coord_sys.station_ecef2enu(stations) # check if reference origin self.assertEqual(stations.x[0], 0) @@ -191,7 +191,7 @@ def test_station_converter(self): ) # return stations to starting case: - conv.revert_station_2d_to_3d(stations) + coord_sys.station_enu2ecef(stations) # check if their position is the same as at the start # some precision error occurs, so "almost equal" is needed diff --git a/tests/test_station_factory_ngso.py b/tests/test_station_factory_ngso.py index 2b0259e91..a34ef3497 100644 --- a/tests/test_station_factory_ngso.py +++ b/tests/test_station_factory_ngso.py @@ -3,7 +3,7 @@ from sharc.support.enumerations import StationType from sharc.station_factory import StationFactory from sharc.station_manager import StationManager -from sharc.support.sharc_geom import GeometryConverter, lla2ecef +from sharc.support.sharc_geom import CoordinateSystem, lla2ecef import numpy as np import numpy.testing as npt @@ -42,8 +42,8 @@ def setUp(self): self.long = -47.9292 self.alt = 1200 - self.geoconvert = GeometryConverter() - self.geoconvert.set_reference( + self.coord_sys = CoordinateSystem() + self.coord_sys.set_reference( -15.7801, -47.9292, 1200, @@ -75,7 +75,7 @@ def setUp(self): rng = np.random.RandomState(seed=self.seed) self.ngso_manager = StationFactory.generate_mss_d2d( - self.param, rng, self.geoconvert) + self.param, rng, self.coord_sys) def test_ngso_manager(self): """Test that the NGSO manager creates the correct number and type of stations.""" @@ -130,8 +130,8 @@ def test_satellite_coordinate_reversing(self): rng = np.random.RandomState(seed=self.seed) ngso_original_coord = StationFactory.generate_mss_d2d( - self.param, rng, self.geoconvert) - self.geoconvert.revert_station_2d_to_3d(ngso_original_coord) + self.param, rng, self.coord_sys) + self.coord_sys.station_enu2ecef(ngso_original_coord) # Test: check if azimuth is pointing towards correct direction # y > 0 <=> azimuth < 0 # y < 0 <=> azimuth > 0 @@ -148,7 +148,7 @@ def test_satellite_coordinate_reversing(self): npt.assert_allclose(off_axis_angle, 0.0, atol=1e-05) - self.geoconvert.convert_station_3d_to_2d(ngso_original_coord) + self.coord_sys.station_ecef2enu(ngso_original_coord) npt.assert_allclose( self.ngso_manager.x, diff --git a/tests/test_topology_imt_mss_dc.py b/tests/test_topology_imt_mss_dc.py index 142af61ac..d85c3d6cf 100644 --- a/tests/test_topology_imt_mss_dc.py +++ b/tests/test_topology_imt_mss_dc.py @@ -5,7 +5,7 @@ from sharc.parameters.imt.parameters_imt_mss_dc import ParametersImtMssDc from sharc.station_manager import StationManager from sharc.parameters.parameters_orbit import ParametersOrbit -from sharc.support.sharc_geom import GeometryConverter, lla2ecef +from sharc.support.sharc_geom import CoordinateSystem, lla2ecef class TestTopologyImtMssDc(unittest.TestCase): @@ -45,22 +45,22 @@ def setUp(self): self.params.sat_is_active_if.minimum_elevation_from_es = 5.0 # Define the geometry converter - self.geometry_converter = GeometryConverter() - self.geometry_converter.set_reference(-15.0, -42.0, 1200) + self.coordinate_system = CoordinateSystem() + self.coordinate_system.set_reference(-15.0, -42.0, 1200) # Define the Earth center coordinates self.earth_center_x = np.array([0.]) self.earth_center_y = np.array([0.]) x, y, z = lla2ecef( - self.geometry_converter.ref_lat, - self.geometry_converter.ref_long, - self.geometry_converter.ref_alt, + self.coordinate_system.ref_lat, + self.coordinate_system.ref_long, + self.coordinate_system.ref_alt, ) self.earth_center_z = np.array([-np.sqrt(x * x + y * y + z * z)]) # Instantiate the IMT MSS-DC topology self.imt_mss_dc_topology = TopologyImtMssDc( - self.params, self.geometry_converter) + self.params, self.coordinate_system) def test_initialization(self): """Test initialization of the IMT MSS-DC topology.""" From dde3df07dc5e79e006e1a5d658b308b1536ca116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juliana=20Gar=C3=A7oni=20dos=20Santos?= Date: Wed, 17 Sep 2025 15:26:23 -0300 Subject: [PATCH 12/77] Add n_drops to post_processor.py --- sharc/post_processor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sharc/post_processor.py b/sharc/post_processor.py index 9a0af4f54..f87e0f4d4 100644 --- a/sharc/post_processor.py +++ b/sharc/post_processor.py @@ -612,6 +612,7 @@ def aggregate_results( ul_tdd_factor: float, n_bs_sim: int, n_bs_actual: int, + n_samples: int, random_number_gen=np.random.RandomState(31), ): """ @@ -629,6 +630,8 @@ def aggregate_results( Should probably be 7 * 19 * 3 * 3 or 1 * 19 * 3 * 3 n_bs_actual: int The number of base stations the study wants to have conclusions for. + n_drops: int + The number of random samples to choose. random_number_gen: np.random.RandomState Since this methods uses another montecarlo to aggregate results, it needs a random number generator @@ -647,7 +650,8 @@ def aggregate_results( elif dl_tdd_factor == 0: n_aggregate = len(ul_samples) else: - n_aggregate = min(len(ul_samples), len(dl_samples)) + # n_aggregate = min(len(ul_samples), len(dl_samples)) + n_aggregate = n_drops aggregate_samples = np.empty(n_aggregate) From 4a98e6986b04c1219d3b4724fe4d8617fe6e7ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juliana=20Gar=C3=A7oni=20dos=20Santos?= Date: Wed, 17 Sep 2025 15:31:44 -0300 Subject: [PATCH 13/77] Add "n_drops" to post_processor.py --- sharc/post_processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sharc/post_processor.py b/sharc/post_processor.py index f87e0f4d4..25eb6e6c6 100644 --- a/sharc/post_processor.py +++ b/sharc/post_processor.py @@ -612,7 +612,7 @@ def aggregate_results( ul_tdd_factor: float, n_bs_sim: int, n_bs_actual: int, - n_samples: int, + n_drops: int, random_number_gen=np.random.RandomState(31), ): """ From 825f5c47c76969f62aa0ebc89ab8111db50f739e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juliana=20Gar=C3=A7oni=20dos=20Santos?= Date: Wed, 17 Sep 2025 16:03:49 -0300 Subject: [PATCH 14/77] Allow user-defined n_aggregate in results aggregation --- sharc/post_processor.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/sharc/post_processor.py b/sharc/post_processor.py index 25eb6e6c6..c720940f0 100644 --- a/sharc/post_processor.py +++ b/sharc/post_processor.py @@ -612,7 +612,7 @@ def aggregate_results( ul_tdd_factor: float, n_bs_sim: int, n_bs_actual: int, - n_drops: int, + n_aggregate: int | None = None, random_number_gen=np.random.RandomState(31), ): """ @@ -630,8 +630,9 @@ def aggregate_results( Should probably be 7 * 19 * 3 * 3 or 1 * 19 * 3 * 3 n_bs_actual: int The number of base stations the study wants to have conclusions for. - n_drops: int - The number of random samples to choose. + n_aggregate: int | None + The number of random samples to choose. + If None (default), it will be calculated automatically based on input sample sizes. random_number_gen: np.random.RandomState Since this methods uses another montecarlo to aggregate results, it needs a random number generator @@ -645,13 +646,20 @@ def aggregate_results( dl_tdd_factor = 1 - ul_tdd_factor - if ul_tdd_factor == 0: - n_aggregate = len(dl_samples) - elif dl_tdd_factor == 0: - n_aggregate = len(ul_samples) - else: - # n_aggregate = min(len(ul_samples), len(dl_samples)) - n_aggregate = n_drops + # Set the final n_aggregate value: use the user-provided value if valid, otherwise calculate a default. + if n_aggregate is None: + # If no value was provided, calculate a default based on the input sample sizes. + dl_tdd_factor = 1 - ul_tdd_factor + if ul_tdd_factor == 0: + n_aggregate = len(dl_samples) + elif dl_tdd_factor == 0: + n_aggregate = len(ul_samples) + else: + n_aggregate = min(len(ul_samples), len(dl_samples)) + + elif n_aggregate <= 0: + # Validate that the user-provided value is a positive integer. + raise ValueError(f"n_aggregate must be a positive integer, but got {n_aggregate}.") aggregate_samples = np.empty(n_aggregate) From 0188fcf1489e70bdc6db2caaf7ccecb234afe682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juliana=20Gar=C3=A7oni?= <68389491+jgarconi@users.noreply.github.com> Date: Thu, 18 Sep 2025 11:48:55 -0300 Subject: [PATCH 15/77] Solve trailing whitespace error in post_processor.py --- sharc/post_processor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sharc/post_processor.py b/sharc/post_processor.py index c720940f0..9a8f67431 100644 --- a/sharc/post_processor.py +++ b/sharc/post_processor.py @@ -632,11 +632,12 @@ def aggregate_results( The number of base stations the study wants to have conclusions for. n_aggregate: int | None The number of random samples to choose. - If None (default), it will be calculated automatically based on input sample sizes. + If None (default), it will be calculated automatically based on input sample sizes. random_number_gen: np.random.RandomState Since this methods uses another montecarlo to aggregate results, it needs a random number generator """ + if ul_tdd_factor > 1 or ul_tdd_factor < 0: raise ValueError( "PostProcessor.aggregate_results() was called with invalid ul_tdd_factor parameter." + @@ -649,7 +650,6 @@ def aggregate_results( # Set the final n_aggregate value: use the user-provided value if valid, otherwise calculate a default. if n_aggregate is None: # If no value was provided, calculate a default based on the input sample sizes. - dl_tdd_factor = 1 - ul_tdd_factor if ul_tdd_factor == 0: n_aggregate = len(dl_samples) elif dl_tdd_factor == 0: From 874ee78dce1732b7192007de30d7292ddb228ecd Mon Sep 17 00:00:00 2001 From: artistrea Date: Wed, 24 Sep 2025 15:55:57 -0300 Subject: [PATCH 16/77] fix: p619 apparent elevation angles --- sharc/propagation/propagation_p619.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/sharc/propagation/propagation_p619.py b/sharc/propagation/propagation_p619.py index a5c4907da..809874b86 100644 --- a/sharc/propagation/propagation_p619.py +++ b/sharc/propagation/propagation_p619.py @@ -282,34 +282,33 @@ def _get_beam_spreading_att( def apparent_elevation_angle( cls, elevation_deg: np.array, - space_station_alt_m: float) -> np.array: + earth_station_alt_m: float) -> np.array: """Calculate apparent elevation angle according to ITU-R P619, Attachment B Parameters ---------- elevation_deg : np.array - free-space elevation angle - space_station_alt_m : float - space-station altitude + free-earth elevation angle + earth_station_alt_m : float + earth-station altitude Returns ------- np.array apparent elevation angle """ - elev_angles_rad = np.deg2rad(elevation_deg) - tau_fs1 = 1.728 + 0.5411 * elev_angles_rad + 0.03723 * elev_angles_rad**2 - tau_fs2 = 0.1815 + 0.06272 * elev_angles_rad + 0.01380 * elev_angles_rad**2 - tau_fs3 = 0.01727 + 0.008288 * elev_angles_rad + tau_fs1 = 1.728 + 0.5411 * elevation_deg + 0.03723 * elevation_deg**2 + tau_fs2 = 0.1815 + 0.06272 * elevation_deg + 0.01380 * elevation_deg**2 + tau_fs3 = 0.01727 + 0.008288 * elevation_deg + Ht_km = earth_station_alt_m / 1e3 # change in elevation angle due to refraction tau_fs_deg = 1 / ( - tau_fs1 + space_station_alt_m * tau_fs2 + - space_station_alt_m**2 * tau_fs3 + tau_fs1 + Ht_km * tau_fs2 + + Ht_km**2 * tau_fs3 ) - tau_fs = tau_fs_deg / 180. * np.pi - return np.degrees(elev_angles_rad + tau_fs) + return elevation_deg + tau_fs_deg @dispatch(Parameters, float, StationManager, StationManager, np.ndarray, np.ndarray) @@ -357,7 +356,7 @@ def get_loss( # raise ValueError(f"Invalid shape for station_b_gains = {station_b_gains.shape}") elevation_angles["apparent"] = self.apparent_elevation_angle( elevation_angles["free_space"], - station_a.height, + earth_station_height, ) # Transpose it to fit the expected path loss shape elevation_angles["free_space"] = np.transpose( @@ -370,7 +369,7 @@ def get_loss( earth_station_antenna_gain = station_a_gains elevation_angles["apparent"] = self.apparent_elevation_angle( elevation_angles["free_space"], - station_b.height, + earth_station_height, ) else: raise ValueError( From ec54c15f5306992b497aaa0e685be121b659ab26 Mon Sep 17 00:00:00 2001 From: artistrea Date: Wed, 24 Sep 2025 17:06:48 -0300 Subject: [PATCH 17/77] hotfix: p619 es alt --- sharc/propagation/propagation_p619.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sharc/propagation/propagation_p619.py b/sharc/propagation/propagation_p619.py index 809874b86..79fabaa9d 100644 --- a/sharc/propagation/propagation_p619.py +++ b/sharc/propagation/propagation_p619.py @@ -356,7 +356,7 @@ def get_loss( # raise ValueError(f"Invalid shape for station_b_gains = {station_b_gains.shape}") elevation_angles["apparent"] = self.apparent_elevation_angle( elevation_angles["free_space"], - earth_station_height, + self.earth_station_alt_m, ) # Transpose it to fit the expected path loss shape elevation_angles["free_space"] = np.transpose( @@ -369,7 +369,7 @@ def get_loss( earth_station_antenna_gain = station_a_gains elevation_angles["apparent"] = self.apparent_elevation_angle( elevation_angles["free_space"], - earth_station_height, + self.earth_station_alt_m, ) else: raise ValueError( From 9f79cc863f9b7d4d8e83cedea87bc8ea26461755 Mon Sep 17 00:00:00 2001 From: artistrea Date: Thu, 25 Sep 2025 11:43:10 -0300 Subject: [PATCH 18/77] refactor: deleted height parameter from station manager --- .../scripts/imt_ntn_footprint.py | 2 +- sharc/propagation/propagation_abg.py | 2 +- .../propagation/propagation_clear_air_452.py | 2 +- sharc/propagation/propagation_hdfss.py | 12 +++- sharc/propagation/propagation_indoor.py | 6 +- sharc/propagation/propagation_p619.py | 18 +++-- sharc/propagation/propagation_sat_simple.py | 7 +- sharc/propagation/propagation_tvro.py | 31 ++++---- sharc/propagation/propagation_uma.py | 14 ++-- sharc/propagation/propagation_umi.py | 14 ++-- sharc/satellite/scripts/plot_footprints.py | 3 +- sharc/simulation.py | 16 ++--- sharc/station.py | 17 ++--- sharc/station_factory.py | 34 ++++----- sharc/station_manager.py | 72 +++++++++++++------ tests/e2e/test_integration_imt_victim.py | 8 +-- tests/e2e/test_integration_sys_victim.py | 4 +- tests/test_adjacent_channel.py | 2 - tests/test_simulation_downlink.py | 5 +- tests/test_simulation_indoor.py | 2 +- tests/test_simulation_uplink.py | 5 +- tests/test_station.py | 10 +-- tests/test_station_factory.py | 14 ++-- tests/test_station_factory_ngso.py | 6 +- tests/test_station_manager.py | 69 +++++++++--------- tests/test_topology_imt_mss_dc.py | 2 +- 26 files changed, 207 insertions(+), 170 deletions(-) diff --git a/sharc/campaigns/imt_ntn_footprint/scripts/imt_ntn_footprint.py b/sharc/campaigns/imt_ntn_footprint/scripts/imt_ntn_footprint.py index 6a163a53a..d42beacd6 100644 --- a/sharc/campaigns/imt_ntn_footprint/scripts/imt_ntn_footprint.py +++ b/sharc/campaigns/imt_ntn_footprint/scripts/imt_ntn_footprint.py @@ -68,7 +68,7 @@ ntn_ue.active = np.ones(ntn_ue.num_stations, dtype=bool) ntn_bs = StationFactory.generate_mss_ss(param_mss) - phi, theta = ntn_bs.get_pointing_vector_to(ntn_ue) + phi, theta = ntn_bs.get_global_pointing_vector_to(ntn_ue) station_1_active = np.where(ntn_bs.active)[0] station_2_active = np.where(ntn_ue.active)[0] beams_idx = np.zeros(len(station_2_active), dtype=int) diff --git a/sharc/propagation/propagation_abg.py b/sharc/propagation/propagation_abg.py index 53c6fbaab..12a02cc26 100644 --- a/sharc/propagation/propagation_abg.py +++ b/sharc/propagation/propagation_abg.py @@ -74,7 +74,7 @@ def get_loss( if wrap_around_enabled: _, distances_3d, _, _ = \ - station_a.get_dist_angles_wrap_around(station_b) + station_a.get_global_dist_angles_wrap_around(station_b) else: distances_3d = station_a.get_3d_distance_to(station_b) diff --git a/sharc/propagation/propagation_clear_air_452.py b/sharc/propagation/propagation_clear_air_452.py index 26095ca24..8ba9f1bc3 100644 --- a/sharc/propagation/propagation_clear_air_452.py +++ b/sharc/propagation/propagation_clear_air_452.py @@ -1551,7 +1551,7 @@ def get_loss( indoor_stations = np.tile( station_b.indoor, (station_a.num_stations, 1), ) - elevation = station_b.get_elevation(station_a) + elevation = station_b.get_local_elevation(station_a) if params.imt.interfered_with: tx_gain = station_a_gains rx_gain = station_b_gains diff --git a/sharc/propagation/propagation_hdfss.py b/sharc/propagation/propagation_hdfss.py index a78451775..deb4998c5 100644 --- a/sharc/propagation/propagation_hdfss.py +++ b/sharc/propagation/propagation_hdfss.py @@ -74,7 +74,13 @@ def get_loss( distance = station_a.get_3d_distance_to(station_b) # P.452 expects Kms frequency_array = frequency * \ np.ones(distance.shape) # P.452 expects GHz - elevation = station_b.get_elevation(station_a) + elevation = station_b.get_local_elevation(station_a) + + if station_a.uses_local_coords or station_b.uses_local_coords: + raise NotImplementedError( + "HDFSS currently assumes stations z == height. " + "If stations has local coords != global coords, this probably isn't true" + ) return self.propagation.get_loss( distance_3D=distance, @@ -83,8 +89,8 @@ def get_loss( frequency=frequency_array, imt_x=station_b.x, imt_y=station_b.y, - imt_z=station_b.height, + imt_z=station_b.z, es_x=station_a.x, es_y=station_a.y, - es_z=station_a.height, + es_z=station_a.z, ) diff --git a/sharc/propagation/propagation_indoor.py b/sharc/propagation/propagation_indoor.py index f80be46f6..20dc83f17 100644 --- a/sharc/propagation/propagation_indoor.py +++ b/sharc/propagation/propagation_indoor.py @@ -100,16 +100,16 @@ def get_loss( if wrap_around_enabled: bs_to_ue_dist_2d, bs_to_ue_dist_3d, _, _ = \ - station_b.get_dist_angles_wrap_around(station_a) + station_b.get_global_dist_angles_wrap_around(station_a) else: - bs_to_ue_dist_2d = station_b.get_distance_to(station_a) + bs_to_ue_dist_2d = station_b.get_local_distance_to(station_a) bs_to_ue_dist_3d = station_b.get_3d_distance_to(station_a) frequency_array = frequency * np.ones(bs_to_ue_dist_2d.shape) indoor_stations = np.tile( station_a.indoor, (station_b.num_stations, 1), ) - elevation = np.transpose(station_a.get_elevation(station_b)) + elevation = np.transpose(station_a.get_local_elevation(station_b)) return self.get_loss( bs_to_ue_dist_3d, diff --git a/sharc/propagation/propagation_p619.py b/sharc/propagation/propagation_p619.py index 79fabaa9d..adf5120f1 100644 --- a/sharc/propagation/propagation_p619.py +++ b/sharc/propagation/propagation_p619.py @@ -349,8 +349,13 @@ def get_loss( # Elevation angles seen from the station on Earth. elevation_angles = {} if station_a.is_space_station: - earth_station_height = station_b.height - elevation_angles["free_space"] = station_b.get_elevation(station_a) + if station_b.uses_local_coords: + raise NotImplementedError( + "P619 currently assumes earth station z == height. " + "If ES has local coords != global coords, this probably isn't true" + ) + earth_station_height = station_b.z + elevation_angles["free_space"] = station_b.get_local_elevation(station_a) earth_station_antenna_gain = station_b_gains # if (station_b_gains.shape != distance.shape): # raise ValueError(f"Invalid shape for station_b_gains = {station_b_gains.shape}") @@ -364,8 +369,13 @@ def get_loss( elevation_angles["apparent"] = np.transpose( elevation_angles["apparent"]) elif station_b.is_space_station: - earth_station_height = station_a.height - elevation_angles["free_space"] = station_a.get_elevation(station_b) + if station_a.uses_local_coords: + raise NotImplementedError( + "P619 currently assumes earth station z == height. " + "If ES has local coords != global coords, this probably isn't true" + ) + earth_station_height = station_a.z + elevation_angles["free_space"] = station_a.get_local_elevation(station_b) earth_station_antenna_gain = station_a_gains elevation_angles["apparent"] = self.apparent_elevation_angle( elevation_angles["free_space"], diff --git a/sharc/propagation/propagation_sat_simple.py b/sharc/propagation/propagation_sat_simple.py index e83712914..f90d6ac77 100644 --- a/sharc/propagation/propagation_sat_simple.py +++ b/sharc/propagation/propagation_sat_simple.py @@ -74,8 +74,11 @@ def get_loss( # Elevation angles seen from the station on Earth. elevation_angles = {} + raise NotImplementedError( + "FIXME: apparent_elevation_angle should receive earth station altitude..." + ) if station_a.is_space_station: - elevation_angles["free_space"] = station_b.get_elevation(station_a) + elevation_angles["free_space"] = station_b.get_local_elevation(station_a) # if (station_b_gains.shape != distance.shape): # raise ValueError(f"Invalid shape for station_b_gains = {station_b_gains.shape}") elevation_angles["apparent"] = PropagationP619.apparent_elevation_angle( @@ -86,7 +89,7 @@ def get_loss( elevation_angles["apparent"] = np.transpose( elevation_angles["apparent"]) elif station_b.is_space_station: - elevation_angles["free_space"] = station_a.get_elevation(station_b) + elevation_angles["free_space"] = station_a.get_local_elevation(station_b) elevation_angles["apparent"] = PropagationP619.apparent_elevation_angle( elevation_angles["free_space"], station_b.height, ) else: diff --git a/sharc/propagation/propagation_tvro.py b/sharc/propagation/propagation_tvro.py index 4a501e37d..a92eeea49 100644 --- a/sharc/propagation/propagation_tvro.py +++ b/sharc/propagation/propagation_tvro.py @@ -81,9 +81,9 @@ def get_loss( if wrap_around_enabled and ( station_a.is_imt_station() and station_b.is_imt_station()): distances_2d, distances_3d, _, _ = \ - station_a.get_dist_angles_wrap_around(station_b) + station_a.get_global_dist_angles_wrap_around(station_b) else: - distances_2d = station_a.get_distance_to(station_b) + distances_2d = station_a.get_local_distance_to(station_b) distances_3d = station_a.get_3d_distance_to(station_b) indoor_stations = np.tile( @@ -92,29 +92,35 @@ def get_loss( # Use the right interface whether the link is IMT-IMT or IMT-System # TODO: Refactor __get_loss and get rid of that if-else. if station_a.is_imt_station() and station_b.is_imt_station(): + if station_a.uses_local_coords: + raise NotImplementedError( + "TVRO currently assumes UE z == height. " + "If UE has local coords != global coords, this probably isn't true" + ) + loss = self._get_loss( distance_3D=distances_3d, distance_2D=distances_2d, frequency=frequency * np.ones(distances_2d.shape), - bs_height=station_b.height, - ue_height=station_a.height, + ue_height=station_a.z, indoor_stations=indoor_stations ) else: imt_station, sys_station = (station_a, station_b) \ if station_a.is_imt_station() else (station_b, station_a) + + if sys_station.uses_local_coords: + raise NotImplementedError( + "TVRO currently assumes System z == height. " + "If System has local coords != global coords, this probably isn't true" + ) + loss = self._get_loss( distance_3D=distances_3d, distance_2D=distances_2d, frequency=frequency * np.ones(distances_2d.shape), - bs_height=station_b.height, imt_sta_type=imt_station.station_type, - imt_x=imt_station.x, - imt_y=imt_station.y, - imt_z=imt_station.height, - es_x=sys_station.x, - es_y=sys_station.y, - es_z=sys_station.height, + es_z=sys_station.z, indoor_stations=indoor_stations ) @@ -129,7 +135,6 @@ def _get_loss(self, *args, **kwargs) -> np.array: distance_3D (np.array) : 3D distances between stations distance_2D (np.array) : 2D distances between stations frequency (np.array) : center frequencie [MHz] - bs_height (np.array) : base station antenna heights Returns ------- array with path loss values with dimensions of distance_2D @@ -310,7 +315,7 @@ def get_los_probability( frequency = 3600 * np.ones(distance_2D.shape) h_bs = 25 * np.ones(len(distance_2D[:, 0])) h_ue = 1.5 * np.ones(len(distance_2D[0, :])) - h_tvro = 6 + h_tvro = 6 * np.ones(len(distance_2D[0, :])) distance_3D = np.sqrt(distance_2D**2 + (h_bs[:, np.newaxis] - h_ue)**2) indoor_stations = np.zeros(distance_3D.shape, dtype=bool) shadowing = False diff --git a/sharc/propagation/propagation_uma.py b/sharc/propagation/propagation_uma.py index 1758e53e1..df1eff62c 100644 --- a/sharc/propagation/propagation_uma.py +++ b/sharc/propagation/propagation_uma.py @@ -61,17 +61,23 @@ def get_loss( if wrap_around_enabled and ( station_a.is_imt_station() and station_b.is_imt_station()): distances_2d, distances_3d, _, _ = \ - station_a.get_dist_angles_wrap_around(station_b) + station_a.get_global_dist_angles_wrap_around(station_b) else: - distances_2d = station_a.get_distance_to(station_b) + distances_2d = station_a.get_local_distance_to(station_b) distances_3d = station_a.get_3d_distance_to(station_b) + if station_a.uses_local_coords or station_b.uses_local_coords: + raise NotImplementedError( + "UMa currently assumes stations z == height. " + "If stations has local coords != global coords, this probably isn't true" + ) + loss = self.get_loss( distances_3d, distances_2d, frequency * np.ones(distances_2d.shape), - station_b.height, - station_a.height, + station_b.z, + station_a.z, params.imt.shadowing, ) diff --git a/sharc/propagation/propagation_umi.py b/sharc/propagation/propagation_umi.py index ef90623bb..ae5a20d35 100644 --- a/sharc/propagation/propagation_umi.py +++ b/sharc/propagation/propagation_umi.py @@ -67,17 +67,23 @@ def get_loss( if wrap_around_enabled and ( station_a.is_imt_station() and station_b.is_imt_station()): distance_2d, distance_3d, _, _ = \ - station_a.get_dist_angles_wrap_around(station_b) + station_a.get_global_dist_angles_wrap_around(station_b) else: - distance_2d = station_a.get_distance_to(station_b) + distance_2d = station_a.get_local_distance_to(station_b) distance_3d = station_a.get_3d_distance_to(station_b) + if station_a.uses_local_coords or station_b.uses_local_coords: + raise NotImplementedError( + "UMi currently assumes stations z == height. " + "If stations has local coords != global coords, this probably isn't true" + ) + loss = self.get_loss( distance_3d, distance_2d, frequency * np.ones(distance_2d.shape), - station_b.height, - station_a.height, + station_b.z, + station_a.z, params.imt.shadowing, ) diff --git a/sharc/satellite/scripts/plot_footprints.py b/sharc/satellite/scripts/plot_footprints.py index 57f5cf98c..e7eab2150 100644 --- a/sharc/satellite/scripts/plot_footprints.py +++ b/sharc/satellite/scripts/plot_footprints.py @@ -162,7 +162,6 @@ def plot_fp( surf_manager.x = x_flat surf_manager.y = y_flat surf_manager.z = z_flat - surf_manager.height = z_flat station_1 = mss_d2d_manager mss_active = np.where(station_1.active)[0] @@ -173,7 +172,7 @@ def plot_fp( # Calculate vector and apointment off_axis gains = np.zeros((len(mss_active), len(station_2_active))) off_axis_angle = station_1.get_off_axis_angle(station_2) - phi, theta = station_1.get_pointing_vector_to(station_2) + phi, theta = station_1.get_global_pointing_vector_to(station_2) for k in range(len(mss_active)): gains[k, :] = \ station_1.antenna[k].calculate_gain( diff --git a/sharc/simulation.py b/sharc/simulation.py index 45908a079..aaf430716 100644 --- a/sharc/simulation.py +++ b/sharc/simulation.py @@ -475,11 +475,11 @@ def select_ue(self, random_number_gen: np.random.RandomState): """ if self.wrap_around_enabled: self.bs_to_ue_d_2D, self.bs_to_ue_d_3D, self.bs_to_ue_phi, self.bs_to_ue_theta = \ - self.bs.get_dist_angles_wrap_around(self.ue) + self.bs.get_global_dist_angles_wrap_around(self.ue) else: - self.bs_to_ue_d_2D = self.bs.get_distance_to(self.ue) + self.bs_to_ue_d_2D = self.bs.get_local_distance_to(self.ue) self.bs_to_ue_d_3D = self.bs.get_3d_distance_to(self.ue) - self.bs_to_ue_phi, self.bs_to_ue_theta = self.bs.get_pointing_vector_to( + self.bs_to_ue_phi, self.bs_to_ue_theta = self.bs.get_global_pointing_vector_to( self.ue, ) bs_active = np.where(self.bs.active)[0] @@ -576,7 +576,7 @@ def calculate_gains( theta = self.bs_to_ue_theta beams_idx = self.bs_to_ue_beam_rbs[station_2_active] elif not station_2.is_imt_station(): - phi, theta = station_1.get_pointing_vector_to(station_2) + phi, theta = station_1.get_global_pointing_vector_to(station_2) phi = np.repeat(phi, self.parameters.imt.ue.k, 0) theta = np.repeat(theta, self.parameters.imt.ue.k, 0) beams_idx = np.tile( @@ -584,11 +584,11 @@ def calculate_gains( ) elif (station_1.station_type is StationType.IMT_UE): - phi, theta = station_1.get_pointing_vector_to(station_2) + phi, theta = station_1.get_global_pointing_vector_to(station_2) beams_idx = np.zeros(len(station_2_active), dtype=int) elif not station_1.is_imt_station(): - phi, theta = station_1.get_pointing_vector_to(station_2) + phi, theta = station_1.get_global_pointing_vector_to(station_2) beams_idx = np.zeros(len(station_2_active), dtype=int) # Calculate gains @@ -634,7 +634,7 @@ def calculate_gains( elif not station_1.is_imt_station(): off_axis_angle = station_1.get_off_axis_angle(station_2) - phi, theta = station_1.get_pointing_vector_to(station_2) + phi, theta = station_1.get_global_pointing_vector_to(station_2) for k in station_1_active: gains[k, station_2_active] = \ station_1.antenna[k].calculate_gain( @@ -776,7 +776,7 @@ def plot_scenario(self): # Plot user equipments ax.scatter( - self.ue.x, self.ue.height, color='r', + self.ue.x, self.ue.z, color='r', edgecolor="w", linewidth=0.5, label="UE", ) diff --git a/sharc/station.py b/sharc/station.py index 38c2b3940..b6c0936f7 100644 --- a/sharc/station.py +++ b/sharc/station.py @@ -17,9 +17,9 @@ def __init__(self): self.id = -1 self.x = 0 self.y = 0 + self.z = 0 self.azimuth = 0 self.elevation = 0 - self.height = 0 self.indoor = False self.active = False self.tx_power = 0 @@ -44,23 +44,14 @@ def __eq__(self, other): self.id == other.id and self.x == other.x and self.y == other.y and - self.height == other.height + self.z == other.z ) return equal else: return NotImplemented def __ne__(self, other): - if isinstance(other, self.__class__): - not_equal = ( - self.id != other.id or - self.x != other.x or - self.y != other.y or - self.height != other.height - ) - return not_equal - else: - return NotImplemented + return not self.__eq__(other) def __hash__(self): - return hash(self.id, self.x, self.y, self.height) + return hash(self.id, self.x, self.y, self.z) diff --git a/sharc/station_factory.py b/sharc/station_factory.py index de9fac813..a2db26d1b 100644 --- a/sharc/station_factory.py +++ b/sharc/station_factory.py @@ -100,25 +100,22 @@ def generate_imt_base_stations( imt_base_stations.x = topology.space_station_x * np.ones(num_bs) imt_base_stations.y = topology.space_station_y * np.ones(num_bs) imt_base_stations.z = topology.space_station_z * np.ones(num_bs) - imt_base_stations.height = imt_base_stations.z imt_base_stations.elevation = topology.elevation imt_base_stations.is_space_station = True elif param.topology.type == "MSS_DC": imt_base_stations.x = topology.space_station_x * np.ones(num_bs) imt_base_stations.y = topology.space_station_y * np.ones(num_bs) imt_base_stations.z = topology.space_station_z * np.ones(num_bs) - imt_base_stations.height = topology.height imt_base_stations.elevation = topology.elevation imt_base_stations.is_space_station = True else: imt_base_stations.x = topology.x imt_base_stations.y = topology.y - imt_base_stations.z = topology.z + param.bs.height imt_base_stations.elevation = -param_ant.downtilt * np.ones(num_bs) if param.topology.type == 'INDOOR': - imt_base_stations.height = topology.height + imt_base_stations.z = topology.height else: - imt_base_stations.height = param.bs.height * np.ones(num_bs) + imt_base_stations.z = param.bs.height * np.ones(num_bs) imt_base_stations.azimuth = wrap2_180(topology.azimuth) imt_base_stations.active = random_number_gen.rand( @@ -155,6 +152,8 @@ def generate_imt_base_stations( num_bs, dtype=Antenna, ) + # TODO: transform BS to local coord system before creating antenna + imt_base_stations.antenna = AntennaFactory.create_n_antennas( param.bs.antenna, imt_base_stations.azimuth, @@ -297,7 +296,7 @@ def generate_imt_ue_outdoor( ue_y = list() ue_z = list() - imt_ue.height = param.ue.height * np.ones(num_ue) + imt_ue.z = param.ue.height * np.ones(num_ue) # TODO: Sanitaze the azimuth_range parameter azimuth_range = param.ue.azimuth_range @@ -340,6 +339,7 @@ def generate_imt_ue_outdoor( np.arctan((param.bs.height - param.ue.height) / distance), ) + # FIXME: adapt this to also work with local coords (and space stations) imt_ue.azimuth = (azimuth + theta + np.pi / 2) imt_ue.elevation = elevation + psi @@ -419,6 +419,7 @@ def generate_imt_ue_outdoor( x = radius[idx] * np.cos(np.radians(theta)) y = radius[idx] * np.sin(np.radians(theta)) z = np.zeros_like(x) + # TODO: move this to local coordinate system calc x, y, z = topology.transform_ue_xyz( bs, x, y, z ) @@ -427,6 +428,7 @@ def generate_imt_ue_outdoor( ue_z.extend(z) # calculate UE azimuth wrt serving BS + # FIXME: adapt this to also work with local coords (and space stations) imt_ue.azimuth[idx] = (azimuth[idx] + theta + 180) % 360 # calculate elevation angle @@ -437,6 +439,7 @@ def generate_imt_ue_outdoor( psi = np.degrees( np.arctan((param.bs.height - param.ue.height) / distance), ) + # FIXME: adapt this to also work with space stations imt_ue.elevation[idx] = elevation[idx] + psi else: @@ -631,7 +634,7 @@ def generate_imt_ue_indoor( imt_ue.x = np.array(ue_x) imt_ue.y = np.array(ue_y) - imt_ue.height = np.array(ue_z) + imt_ue.z = np.array(ue_z) imt_ue.active = np.zeros(num_ue, dtype=bool) imt_ue.rx_interference = -500 * np.ones(num_ue) @@ -778,8 +781,6 @@ def generate_single_space_station( space_station.x = x space_station.y = y space_station.z = z - # TODO: put actual altitude instead of z on station.height - space_station.height = z if param.geometry.azimuth.type == "POINTING_AT_IMT": space_station.azimuth = np.rad2deg( @@ -844,7 +845,6 @@ def generate_single_space_station( space_station.elevation[0]) ]) - space_station.z = space_station.height space_station.bandwidth = param.bandwidth space_station.noise_temperature = param.noise_temperature space_station.thermal_noise = -500 @@ -895,13 +895,12 @@ def generate_fss_space_station(param: ParametersFssSs): [x1 * np.sin(imt_lat_rad) - z1 * np.cos(imt_lat_rad)], ) * 1000 fss_space_station.y = np.array([y1]) * 1000 - fss_space_station.height = np.array([ + fss_space_station.z = np.array([ ( z1 * np.sin(imt_lat_rad) + x1 * np.cos(imt_lat_rad) - dist_imt_centre_earth_km ) * 1000, ]) - fss_space_station.z = fss_space_station.height fss_space_station.azimuth = np.array([param.azimuth]) fss_space_station.elevation = np.array([param.elevation]) @@ -1020,7 +1019,6 @@ def generate_fss_earth_station( sys.exit(1) fss_earth_station.z = np.array([param.height]) - fss_earth_station.height = np.array([param.height]) if param.azimuth.upper() == "RANDOM": fss_earth_station.azimuth = np.array( @@ -1152,7 +1150,6 @@ def generate_single_earth_station( sys.exit(1) single_earth_station.z = np.array([param.geometry.height]) - single_earth_station.height = np.array([param.geometry.height]) if param.geometry.azimuth.type == "UNIFORM_DIST": if param.geometry.azimuth.uniform_dist.min < -180: @@ -1256,7 +1253,6 @@ def generate_fs_station(param: ParametersFs): fs_station.x = np.array([param.x]) fs_station.y = np.array([param.y]) fs_station.z = np.array([param.height]) - fs_station.height = np.array([param.height]) fs_station.azimuth = np.array([param.azimuth]) fs_station.elevation = np.array([param.elevation]) @@ -1317,8 +1313,6 @@ def generate_haps( haps.y = np.array([0]) haps.z = param.altitude * np.ones(num_haps) - haps.height = haps.z - elev_max = 68.19 # corresponds to 50 km radius and 20 km altitude haps.azimuth = 360 * random_number_gen.random_sample(num_haps) haps.elevation = ((270 + elev_max) - (270 - elev_max)) * \ @@ -1371,7 +1365,6 @@ def generate_rns( rns.x = np.array([param.x]) rns.y = np.array([param.y]) rns.z = np.array([param.altitude]) - rns.height = np.array([param.altitude]) # minimum and maximum values for azimuth and elevation azimuth = np.array([-30, 30]) @@ -1490,10 +1483,9 @@ def generate_space_station( space_station.y = np.array( [distance * math.cos(math.radians(theta_grd_elev))], ) - space_station.height = np.array( + space_station.z = np.array( [distance * math.sin(math.radians(theta_grd_elev))], ) - space_station.z = space_station.height # Elevation and azimuth at sensor wrt centre of the footprint # It is assumed the sensor is at y-axis, hence azimuth is 270 deg @@ -1560,7 +1552,6 @@ def generate_mss_ss(param_mss: ParametersMssSs): mss_ss.x = ntn_topology.space_station_x * np.ones(num_bs) + param_mss.x mss_ss.y = ntn_topology.space_station_y * np.ones(num_bs) + param_mss.y mss_ss.z = ntn_topology.space_station_z * np.ones(num_bs) - mss_ss.height = ntn_topology.space_station_z * np.ones(num_bs) mss_ss.elevation = ntn_topology.elevation mss_ss.is_space_station = True mss_ss.azimuth = ntn_topology.azimuth @@ -1682,7 +1673,6 @@ def generate_mss_d2d( mss_d2d.z = mss_d2d_values["sat_z"] mss_d2d.elevation = mss_d2d_values["sat_antenna_elev"] mss_d2d.azimuth = mss_d2d_values["sat_antenna_azim"] - mss_d2d.height = mss_d2d_values["sat_alt"] mss_d2d.active = np.zeros(total_satellites, dtype=bool) diff --git a/sharc/station_manager.py b/sharc/station_manager.py index 9ab7a9088..e585829b4 100644 --- a/sharc/station_manager.py +++ b/sharc/station_manager.py @@ -22,12 +22,12 @@ class StationManager(object): def __init__(self, n): self.num_stations = n - self.x = np.empty(n) # x coordinate - self.y = np.empty(n) # y coordinate - self.z = np.empty(n) # z coordinate (includes height above ground) + self.x = np.empty(n) # x in global coordinates + self.y = np.empty(n) # y in global coordinates + self.z = np.empty(n) # z in global coordinates + # antenna azim and elev angles in global coordinates self.azimuth = np.empty(n) self.elevation = np.empty(n) - self.height = np.empty(n) # station height above ground self.idx_orbit = np.empty(n) self.indoor = np.zeros(n, dtype=bool) self.active = np.ones(n, dtype=bool) @@ -54,6 +54,7 @@ def __init__(self, n): self.station_type = StationType.NONE self.is_space_station = False self.intersite_dist = 0.0 + self.uses_local_coords = False def get_station_list(self, id=None) -> list: """Return a list of Station objects for the given indices. @@ -95,7 +96,6 @@ def get_station(self, id) -> Station: station.z = self.z[id] station.azimuth = self.azimuth[id] station.elevation = self.elevation[id] - station.height = self.height[id] station.indoor = self.indoor[id] station.active = self.active[id] station.tx_power = self.tx_power[id] @@ -115,8 +115,28 @@ def get_station(self, id) -> Station: station.station_type = self.station_type return station - def get_distance_to(self, station) -> np.array: - """Calculate the 2D distance between this manager's stations and another's. + def get_local_distance_to(self, station) -> np.array: + """Calculate the 2D distance between this manager's stations and another's + considering this ones coordinate system + + Parameters + ---------- + station : StationManager + StationManager to which the distance is calculated. + + Returns + ------- + np.array + 2D distance matrix between stations. + """ + if not self.uses_local_coords: + return self.get_global_distance_to(station) + # TODO: 2d distance calculation + raise NotImplementedError() + + def get_global_distance_to(self, station) -> np.array: + """Calculate the 2D distance between this manager's stations and another's + considering their global (x,y) Parameters ---------- @@ -161,7 +181,7 @@ def get_3d_distance_to(self, station) -> np.array: ) return dx - def get_dist_angles_wrap_around(self, station) -> np.array: + def get_global_dist_angles_wrap_around(self, station) -> np.array: """Calculate distances and angles using the wrap-around technique. Parameters @@ -222,7 +242,7 @@ def get_dist_angles_wrap_around(self, station) -> np.array: # Calculate 3D distance distance_3D = np.sqrt( np.power(distance_2D, 2) + - np.power(station.height - self.height[:, np.newaxis], 2), + np.power(station.z - self.z[:, np.newaxis], 2), ) # Calcualte pointing vector @@ -230,7 +250,7 @@ def get_dist_angles_wrap_around(self, station) -> np.array: - self.x[:, np.newaxis] point_vec_y = cluster_y[cluster_num, np.arange(station.num_stations)] \ - self.y[:, np.newaxis] - point_vec_z = station.height - self.height[:, np.newaxis] + point_vec_z = station.z - self.z[:, np.newaxis] phi = np.array( np.rad2deg( @@ -243,7 +263,7 @@ def get_dist_angles_wrap_around(self, station) -> np.array: return distance_2D, distance_3D, phi, theta - def get_elevation(self, station) -> np.array: + def get_global_elevation(self, station) -> np.array: """Calculate the elevation angle between this manager's stations and another's. Parameters @@ -255,13 +275,7 @@ def get_elevation(self, station) -> np.array: ------- np.array Elevation angle matrix (degrees). - - Notes - ----- - This implementation is essentially the same as get_elevation_angle (free-space elevation angle), - despite the different matrix dimensions. The methods should be merged to reuse code. """ - elevation = np.empty([self.num_stations, station.num_stations]) for i in range(self.num_stations): @@ -274,7 +288,26 @@ def get_elevation(self, station) -> np.array: return elevation - def get_pointing_vector_to(self, station) -> tuple: + def get_local_elevation(self, station) -> np.array: + """Calculate the elevation angle between this manager's stations and another's + considering this one's loca coordinate system + + Parameters + ---------- + station : StationManager + StationManager to which the elevation angle is calculated. + + Returns + ------- + np.array + Elevation angle matrix (degrees). + """ + if not self.uses_local_coords: + return self.get_global_elevation(station) + + raise NotImplementedError() + + def get_global_pointing_vector_to(self, station) -> tuple: """Calculate the pointing vector (angles) with respect to another station. Parameters @@ -321,7 +354,7 @@ def get_off_axis_angle(self, station) -> np.array: np.array Off-axis angle matrix (degrees). """ - Az, b = self.get_pointing_vector_to(station) + Az, b = self.get_global_pointing_vector_to(station) Az0 = self.azimuth a = 90 - self.elevation[:, np.newaxis] @@ -372,7 +405,6 @@ def copy_active_stations(stations: StationManager) -> StationManager: act_sta.z[idx] = stations.z[active_idx] act_sta.azimuth[idx] = stations.azimuth[active_idx] act_sta.elevation[idx] = stations.elevation[active_idx] - act_sta.height[idx] = stations.height[active_idx] act_sta.indoor[idx] = stations.indoor[active_idx] act_sta.active[idx] = stations.active[active_idx] act_sta.tx_power[idx] = stations.tx_power[active_idx] diff --git a/tests/e2e/test_integration_imt_victim.py b/tests/e2e/test_integration_imt_victim.py index d5bc39945..2f518999b 100644 --- a/tests/e2e/test_integration_imt_victim.py +++ b/tests/e2e/test_integration_imt_victim.py @@ -298,7 +298,7 @@ def test_es_to_ue_aclr_and_acs_partial_overlap(self): g1_co_1k = np.zeros((1, 2)) g1_adj_1k = np.zeros((1, 2)) - phis, thetas = simulation_1k.ue.get_pointing_vector_to(simulation_1k.system) + phis, thetas = simulation_1k.ue.get_global_pointing_vector_to(simulation_1k.system) for i, phi, theta in zip(range(2), phis, thetas): g1_co_1k[0][i] = simulation_1k.ue.antenna[i].calculate_gain( @@ -321,7 +321,7 @@ def test_es_to_ue_aclr_and_acs_partial_overlap(self): g1_co_3k = np.zeros((1, 6)) g1_adj_3k = np.zeros((1, 6)) - phis, thetas = simulation_3k.ue.get_pointing_vector_to(simulation_1k.system) + phis, thetas = simulation_3k.ue.get_global_pointing_vector_to(simulation_1k.system) for i, phi, theta in zip(range(6), phis, thetas): g1_co_3k[0][i] = simulation_3k.ue.antenna[i].calculate_gain( @@ -790,7 +790,7 @@ def test_es_to_ue_mask(self): ) g1_co_1k = np.zeros((1, 2)) - phis, thetas = simulation_1k.ue.get_pointing_vector_to(simulation_1k.system) + phis, thetas = simulation_1k.ue.get_global_pointing_vector_to(simulation_1k.system) for i, phi, theta in zip(range(2), phis, thetas): g1_co_1k[0][i] = simulation_1k.ue.antenna[i].calculate_gain( @@ -806,7 +806,7 @@ def test_es_to_ue_mask(self): ) g1_co_3k = np.zeros((1, 6)) - phis, thetas = simulation_3k.ue.get_pointing_vector_to(simulation_1k.system) + phis, thetas = simulation_3k.ue.get_global_pointing_vector_to(simulation_1k.system) for i, phi, theta in zip(range(6), phis, thetas): g1_co_3k[0][i] = simulation_3k.ue.antenna[i].calculate_gain( diff --git a/tests/e2e/test_integration_sys_victim.py b/tests/e2e/test_integration_sys_victim.py index 717cccb56..55ccfdd75 100644 --- a/tests/e2e/test_integration_sys_victim.py +++ b/tests/e2e/test_integration_sys_victim.py @@ -660,7 +660,7 @@ def test_ue_to_es_aclr_and_acs_partial_overlap(self): g1_co_1k = np.zeros((1, 2)) g1_adj_1k = np.zeros((1, 2)) - phis, thetas = simulation_1k.ue.get_pointing_vector_to(simulation_1k.system) + phis, thetas = simulation_1k.ue.get_global_pointing_vector_to(simulation_1k.system) for i, phi, theta in zip(range(2), phis, thetas): g1_co_1k[0][i] = simulation_1k.ue.antenna[i].calculate_gain( @@ -683,7 +683,7 @@ def test_ue_to_es_aclr_and_acs_partial_overlap(self): g1_co_3k = np.zeros((1, 6)) g1_adj_3k = np.zeros((1, 6)) - phis, thetas = simulation_3k.ue.get_pointing_vector_to(simulation_1k.system) + phis, thetas = simulation_3k.ue.get_global_pointing_vector_to(simulation_1k.system) for i, phi, theta in zip(range(6), phis, thetas): g1_co_3k[0][i] = simulation_3k.ue.antenna[i].calculate_gain( diff --git a/tests/test_adjacent_channel.py b/tests/test_adjacent_channel.py index aca41822e..4dda25180 100644 --- a/tests/test_adjacent_channel.py +++ b/tests/test_adjacent_channel.py @@ -246,7 +246,6 @@ def test_simulation_2bs_4ue_downlink(self): self.simulation.system.x = np.array([0.01]) # avoids zero-division self.simulation.system.y = np.array([0]) self.simulation.system.z = np.array([self.param.fss_ss.altitude]) - self.simulation.system.height = np.array([self.param.fss_ss.altitude]) # test the method that calculates interference from IMT UE to FSS space # station @@ -381,7 +380,6 @@ def test_simulation_2bs_4ue_uplink(self): self.simulation.system.x = np.array([0]) self.simulation.system.y = np.array([0]) self.simulation.system.z = np.array([self.param.fss_ss.altitude]) - self.simulation.system.height = np.array([self.param.fss_ss.altitude]) # test the method that calculates interference from IMT UE to FSS space # station diff --git a/tests/test_simulation_downlink.py b/tests/test_simulation_downlink.py index 16768c0ab..cb3cff593 100644 --- a/tests/test_simulation_downlink.py +++ b/tests/test_simulation_downlink.py @@ -340,7 +340,6 @@ def test_simulation_2bs_4ue_fss_ss(self): self.simulation.system.x = np.array([0.01]) # avoids zero-division self.simulation.system.y = np.array([0]) self.simulation.system.z = np.array([self.param.fss_ss.altitude]) - self.simulation.system.height = np.array([self.param.fss_ss.altitude]) # test the method that calculates interference from IMT UE to FSS space # station @@ -521,7 +520,7 @@ def test_simulation_2bs_4ue_fss_es(self): ) self.simulation.system.x = np.array([-2000]) self.simulation.system.y = np.array([0]) - self.simulation.system.height = np.array([self.param.fss_es.height]) + self.simulation.system.z = np.array([self.param.fss_es.height]) self.simulation.propagation_imt = PropagationFactory.create_propagation( self.param.imt.channel_model, @@ -696,7 +695,7 @@ def test_simulation_2bs_4ue_ras(self): ) self.simulation.system.x = np.array([-2000]) self.simulation.system.y = np.array([0]) - self.simulation.system.height = np.array( + self.simulation.system.z = np.array( [self.param.ras.geometry.height]) self.simulation.system.antenna[0].effective_area = 54.9779 diff --git a/tests/test_simulation_indoor.py b/tests/test_simulation_indoor.py index 1a7ce702a..1b68c97c6 100644 --- a/tests/test_simulation_indoor.py +++ b/tests/test_simulation_indoor.py @@ -239,7 +239,7 @@ def test_simulation_fss_es(self): ) # Test angle to ES in the IMT coord system - phi_es, theta_es = self.simulation.bs.get_pointing_vector_to( + phi_es, theta_es = self.simulation.bs.get_global_pointing_vector_to( self.simulation.system, ) expected_phi_es = np.array([[18.44], [23.96], [33.69], [53.13]]) diff --git a/tests/test_simulation_uplink.py b/tests/test_simulation_uplink.py index def02bb60..57875569e 100644 --- a/tests/test_simulation_uplink.py +++ b/tests/test_simulation_uplink.py @@ -366,7 +366,6 @@ def test_simulation_2bs_4ue_ss(self): self.simulation.system.x = np.array([0]) self.simulation.system.y = np.array([0]) self.simulation.system.z = np.array([self.param.fss_ss.altitude]) - self.simulation.system.height = np.array([self.param.fss_ss.altitude]) # test the method that calculates interference from IMT UE to FSS space # station @@ -781,7 +780,7 @@ def test_simulation_2bs_4ue_ras(self): ) self.simulation.system.x = np.array([-2000]) self.simulation.system.y = np.array([0]) - self.simulation.system.height = np.array( + self.simulation.system.z = np.array( [self.param.ras.geometry.height]) self.simulation.system.antenna[0].effective_area = 54.9779 @@ -895,7 +894,7 @@ def test_beamforming_gains(self): # Test BS gains # Test pointing vector - phi, theta = self.simulation.bs.get_pointing_vector_to( + phi, theta = self.simulation.bs.get_global_pointing_vector_to( self.simulation.ue, ) npt.assert_allclose( diff --git a/tests/test_station.py b/tests/test_station.py index 8b60bced7..087e6ec4e 100644 --- a/tests/test_station.py +++ b/tests/test_station.py @@ -60,7 +60,7 @@ def setUp(self): self.station.station_type = StationType.IMT_BS self.station.x = 10 self.station.y = 15 - self.station.height = 6 + self.station.z = 6 self.station.tx_power = 20 self.station.rx_power = -3 par = self.bs_param.get_antenna_parameters() @@ -71,7 +71,7 @@ def setUp(self): self.station2.station_type = StationType.IMT_UE self.station2.x = 10 self.station2.y = 15 - self.station2.height = 6 + self.station2.z = 6 self.station2.tx_power = 17 self.station2.rx_power = 9 par = self.ue_param.get_antenna_parameters() @@ -82,7 +82,7 @@ def setUp(self): self.station3.station_type = StationType.FSS_SS self.station3.x = 10 self.station3.y = 15 - self.station3.height = 6 + self.station3.z = 6 self.station3.tx_power = 20 self.station3.rx_power = -3 self.station3.antenna = AntennaOmni(50) @@ -105,7 +105,7 @@ def test_y(self): def test_height(self): """Test that the station height is set correctly.""" - self.assertEqual(self.station.height, 6) + self.assertEqual(self.station.z, 6) def test_tx_power(self): """Test that the station transmit power is set correctly.""" @@ -143,7 +143,7 @@ def test_ne(self): """Test the inequality operator for stations.""" self.assertFalse(self.station != self.station2) # changing id, x, y, or height should change the result - self.station.height = 9 + self.station.z = 9 self.assertTrue(self.station != self.station2) # self.assertTrue(self.station != self.station3) diff --git a/tests/test_station_factory.py b/tests/test_station_factory.py index 5b212d30b..d0b126bbf 100644 --- a/tests/test_station_factory.py +++ b/tests/test_station_factory.py @@ -54,7 +54,7 @@ def test_generate_imt_base_stations_ntn(self): ntn_topology.calculate_coordinates() ntn_bs = StationFactory.generate_imt_base_stations( param_imt, param_imt.bs.antenna.array, ntn_topology, rng) - npt.assert_equal(ntn_bs.height, param_imt.topology.ntn.bs_height) + npt.assert_equal(ntn_bs.z, param_imt.topology.ntn.bs_height) # the azimuth seen from BS antenna npt.assert_almost_equal( ntn_bs.azimuth[0], @@ -138,7 +138,7 @@ def test_generate_single_space_station(self): def get_ground_elevation(ss): return np.rad2deg( np.arctan2( - ss.height, + ss.z, np.sqrt( ss.x**2 + ss.y**2))) @@ -147,7 +147,7 @@ def get_ground_elevation(ss): # test if the maximum distance is close to the cell radius within a # 100km range - npt.assert_almost_equal(space_station.height, param.geometry.altitude) + npt.assert_almost_equal(space_station.z, param.geometry.altitude) npt.assert_almost_equal(get_ground_elevation(space_station), 90) param.geometry.es_lat_deg = max_gso_fov @@ -155,7 +155,7 @@ def get_ground_elevation(ss): space_station = StationFactory.generate_single_space_station(param) npt.assert_almost_equal(get_ground_elevation(space_station), 0, 5) - npt.assert_almost_equal(space_station.height, 0, 0) + npt.assert_almost_equal(space_station.z, 0, 0) param.geometry.es_lat_deg = 0 param.geometry.es_long_deg = max_gso_fov @@ -163,7 +163,7 @@ def get_ground_elevation(ss): space_station = StationFactory.generate_single_space_station(param) npt.assert_almost_equal(get_ground_elevation(space_station), 0, 5) - npt.assert_almost_equal(space_station.height, 0, 0) + npt.assert_almost_equal(space_station.z, 0, 0) param.geometry.es_long_deg = 0 param.geometry.location.fixed.lat_deg = max_gso_fov @@ -171,14 +171,14 @@ def get_ground_elevation(ss): space_station = StationFactory.generate_single_space_station(param) npt.assert_almost_equal(get_ground_elevation(space_station), 0, 5) - npt.assert_almost_equal(space_station.height, 0, 0) + npt.assert_almost_equal(space_station.z, 0, 0) param.geometry.location.fixed.lat_deg = 0 param.geometry.location.fixed.long_deg = max_gso_fov space_station = StationFactory.generate_single_space_station(param) npt.assert_almost_equal(get_ground_elevation(space_station), 0, 5) - npt.assert_almost_equal(space_station.height, 0, 0) + npt.assert_almost_equal(space_station.z, 0, 0) def test_single_space_station_pointing(self): """Basic test for space station generation.""" diff --git a/tests/test_station_factory_ngso.py b/tests/test_station_factory_ngso.py index a34ef3497..6b6907b7b 100644 --- a/tests/test_station_factory_ngso.py +++ b/tests/test_station_factory_ngso.py @@ -83,7 +83,7 @@ def test_ngso_manager(self): self.assertEqual(self.ngso_manager.num_stations, 20 * 32 + 12 * 20) self.assertEqual(self.ngso_manager.x.shape, (20 * 32 + 12 * 20,)) self.assertEqual(self.ngso_manager.y.shape, (20 * 32 + 12 * 20,)) - self.assertEqual(self.ngso_manager.height.shape, (20 * 32 + 12 * 20,)) + self.assertEqual(self.ngso_manager.z.shape, (20 * 32 + 12 * 20,)) def test_satellite_antenna_pointing(self): """Test that satellite antennas point to nadir and off-axis angles are correct.""" @@ -162,10 +162,6 @@ def test_satellite_coordinate_reversing(self): self.ngso_manager.z, ngso_original_coord.z, atol=1e-500) - npt.assert_allclose( - self.ngso_manager.height, - ngso_original_coord.height, - atol=1e-500) npt.assert_allclose( self.ngso_manager.azimuth, ngso_original_coord.azimuth, diff --git a/tests/test_station_manager.py b/tests/test_station_manager.py index db86d1e61..c082c0e09 100644 --- a/tests/test_station_manager.py +++ b/tests/test_station_manager.py @@ -64,7 +64,6 @@ def setUp(self): self.station_manager.x = np.array([10, 20, 30]) self.station_manager.y = np.array([15, 25, 35]) self.station_manager.z = np.array([1, 2, 3]) - self.station_manager.height = np.array([1, 2, 3]) self.station_manager.intersite_dist = 100.0 # this is for downlink self.station_manager.tx_power = dict({0: [27, 30], 1: [35], 2: [40]}) @@ -81,7 +80,6 @@ def setUp(self): self.station_manager2.x = np.array([100, 200]) self.station_manager2.y = np.array([105, 250]) self.station_manager2.z = np.array([4, 5]) - self.station_manager2.height = np.array([4, 5]) self.station_manager2.intersite_dist = 100.0 # this is for downlink self.station_manager2.tx_power = dict({0: [25], 1: [28, 35]}) @@ -96,7 +94,6 @@ def setUp(self): self.station_manager3.x = np.array([300]) self.station_manager3.y = np.array([400]) self.station_manager3.z = np.array([2]) - self.station_manager3.height = np.array([2]) self.station_manager3.intersite_dist = 100.0 # this is for uplink self.station_manager3.tx_power = 22 @@ -111,7 +108,7 @@ def setUp(self): self.station.id = 0 self.station.x = 10 self.station.y = 15 - self.station.height = 1 + self.station.z = 1 self.station.tx_power = 30 self.station.rx_power = -50 par = self.ue_param.get_antenna_parameters() @@ -122,7 +119,7 @@ def setUp(self): self.station2.id = 1 self.station2.x = 20 self.station2.y = 25 - self.station2.height = 2 + self.station2.z = 2 self.station2.tx_power = 35 self.station2.rx_power = -35 par = self.bs_param.get_antenna_parameters() @@ -184,21 +181,21 @@ def test_y(self): def test_height(self): """Test getting and setting station heights.""" # get a single value from the original array - self.assertEqual(self.station_manager.height[0], 1) + self.assertEqual(self.station_manager.z[0], 1) # get two specific values - npt.assert_array_equal(self.station_manager.height[[0, 2]], [1, 3]) + npt.assert_array_equal(self.station_manager.z[[0, 2]], [1, 3]) # get values in reverse order npt.assert_array_equal( - self.station_manager.height[[2, 1, 0]], [3, 2, 1], + self.station_manager.z[[2, 1, 0]], [3, 2, 1], ) # get all values (no need to specify the id's) - npt.assert_array_equal(self.station_manager.height, [1, 2, 3]) + npt.assert_array_equal(self.station_manager.z, [1, 2, 3]) # set a single value and get it - self.station_manager.height[1] = 7 - npt.assert_array_equal(self.station_manager.height[[1, 2]], [7, 3]) + self.station_manager.z[1] = 7 + npt.assert_array_equal(self.station_manager.z[[1, 2]], [7, 3]) # set two values and then get all values - self.station_manager.height[[0, 2]] = [5, 4] - npt.assert_array_equal(self.station_manager.height, [5, 7, 4]) + self.station_manager.z[[0, 2]] = [5, 4] + npt.assert_array_equal(self.station_manager.z, [5, 7, 4]) def test_tx_power(self): """Test getting and setting transmit power for stations.""" @@ -315,7 +312,7 @@ def test_station_list(self): def test_distance_to(self): """Test 2D distance calculation between station managers.""" ref_distance = np.array([[356.405, 180.277]]) - distance = self.station_manager3.get_distance_to(self.station_manager2) + distance = self.station_manager3.get_local_distance_to(self.station_manager2) npt.assert_allclose(distance, ref_distance, atol=1e-2) ref_distance = np.asarray([ @@ -323,7 +320,7 @@ def test_distance_to(self): [113.137, 288.140], [98.994, 274.089], ]) - distance = self.station_manager.get_distance_to(self.station_manager2) + distance = self.station_manager.get_local_distance_to(self.station_manager2) npt.assert_allclose(distance, ref_distance, atol=1e-2) def test_3d_distance_to(self): @@ -350,17 +347,17 @@ def test_wrap_around(self): self.station_manager.x = np.array([0, 150]) self.station_manager.y = np.array([0, -32]) self.station_manager.z = np.array([4, 5]) - self.station_manager.height = np.array([4, 5]) + self.station_manager.z = np.array([4, 5]) self.station_manager.intersite_dist = 100.0 self.station_manager2 = StationManager(3) self.station_manager2.x = np.array([10, 200, 30]) self.station_manager2.y = np.array([15, 250, -350]) self.station_manager2.z = np.array([1, 2, 3]) - self.station_manager2.height = np.array([1, 2, 3]) + self.station_manager2.z = np.array([1, 2, 3]) # 2D Distance - d_2D, d_3D, phi, theta = self.station_manager.get_dist_angles_wrap_around( + d_2D, d_3D, phi, theta = self.station_manager.get_global_dist_angles_wrap_around( self.station_manager2, ) ref_d_2D = np.asarray([ [18.03, 150.32, 85.39], @@ -392,7 +389,7 @@ def test_pointing_vector_to(self): """Test calculation of pointing vectors between station managers.""" eps = 1e-1 # Test 1 - phi, theta = self.station_manager.get_pointing_vector_to( + phi, theta = self.station_manager.get_global_pointing_vector_to( self.station_manager2, ) npt.assert_allclose( @@ -411,7 +408,7 @@ def test_pointing_vector_to(self): ) # Test 2 - phi, theta = self.station_manager2.get_pointing_vector_to( + phi, theta = self.station_manager2.get_global_pointing_vector_to( self.station_manager, ) npt.assert_allclose( @@ -428,14 +425,14 @@ def test_pointing_vector_to(self): ) # Test 3 - phi, theta = self.station_manager3.get_pointing_vector_to( + phi, theta = self.station_manager3.get_global_pointing_vector_to( self.station_manager2, ) npt.assert_allclose(phi, np.array([[-124.13, -123.69]]), atol=eps) npt.assert_allclose(theta, np.array([[89.73, 89.05]]), atol=eps) # Test 4 - phi, theta = self.station_manager2.get_pointing_vector_to( + phi, theta = self.station_manager2.get_global_pointing_vector_to( self.station_manager3, ) npt.assert_allclose(phi, np.array([[55.86], [56.31]]), atol=eps) @@ -447,7 +444,7 @@ def test_off_axis_angle(self): sm1.x = np.array([0]) sm1.y = np.array([0]) sm1.z = np.array([0]) - sm1.height = np.array([0]) + sm1.z = np.array([0]) sm1.azimuth = np.array([0]) sm1.elevation = np.array([0]) @@ -455,7 +452,7 @@ def test_off_axis_angle(self): sm2.x = np.array([100, 100, 0, 100, 100, 100]) sm2.y = np.array([0, 0, 100, 100, 100, 100]) sm2.z = np.array([0, 100, 0, 0, 100, 100]) - sm2.height = np.array([0, 100, 0, 0, 100, 100]) + sm2.z = np.array([0, 100, 0, 0, 100, 100]) sm2.azimuth = np.array([180, 180, 180, 180, 180, 225]) sm2.elevation = np.array([0, 0, 0, 0, 0, 0]) @@ -467,7 +464,7 @@ def test_off_axis_angle(self): sm3.x = np.array([0]) sm3.y = np.array([0]) sm3.z = np.array([0]) - sm3.height = np.array([0]) + sm3.z = np.array([0]) sm3.azimuth = np.array([45]) sm3.elevation = np.array([0]) @@ -475,7 +472,7 @@ def test_off_axis_angle(self): sm4.x = np.array([100, 60]) sm4.y = np.array([100, 80]) sm4.z = np.array([100, 100]) - sm4.height = np.array([100, 100]) + sm4.z = np.array([100, 100]) sm4.azimuth = np.array([180, 180]) sm4.elevation = np.array([0, 0]) @@ -487,7 +484,7 @@ def test_off_axis_angle(self): sm5.x = np.array([0]) sm5.y = np.array([0]) sm5.z = np.array([0]) - sm5.height = np.array([0]) + sm5.z = np.array([0]) sm5.azimuth = np.array([0]) sm5.elevation = np.array([45]) @@ -495,7 +492,7 @@ def test_off_axis_angle(self): sm6.x = np.array([100, 100]) sm6.y = np.array([0, 0]) sm6.z = np.array([100, 100]) - sm6.height = np.array([100, 100]) + sm6.z = np.array([100, 100]) sm6.azimuth = np.array([180, 180]) sm6.elevation = np.array([0, 0]) @@ -507,7 +504,7 @@ def test_off_axis_angle(self): sm6.x = np.array([0]) sm6.y = np.array([0]) sm6.z = np.array([100]) - sm6.height = np.array([100]) + sm6.z = np.array([100]) sm6.azimuth = np.array([0]) sm6.elevation = np.array([270]) @@ -515,7 +512,7 @@ def test_off_axis_angle(self): sm7.x = np.array([0, 100]) sm7.y = np.array([0, 0]) sm7.z = np.array([0, 0]) - sm7.height = np.array([0, 0]) + sm7.z = np.array([0, 0]) sm7.azimuth = np.array([180, 180]) sm7.elevation = np.array([0, 0]) @@ -528,32 +525,32 @@ def test_elevation(self): sm1.x = np.array([0]) sm1.y = np.array([0]) sm1.z = np.array([10]) - sm1.height = np.array([10]) + sm1.z = np.array([10]) sm2 = StationManager(6) sm2.x = np.array([10, 10, 0, 0, 30, 20]) sm2.y = np.array([0, 0, 5, 10, 30, 20]) sm2.z = np.array([10, 20, 5, 0, 20, 20]) - sm2.height = np.array([10, 20, 5, 0, 20, 20]) + sm2.z = np.array([10, 20, 5, 0, 20, 20]) elevation_ref = np.array([[0, 45, -45, -45, 13.26, 19.47]]) - npt.assert_allclose(elevation_ref, sm1.get_elevation(sm2), atol=1e-2) + npt.assert_allclose(elevation_ref, sm1.get_local_elevation(sm2), atol=1e-2) ####################################################################### sm3 = StationManager(2) sm3.x = np.array([0, 30]) sm3.y = np.array([0, 0]) sm3.z = np.array([10, 10]) - sm3.height = np.array([10, 10]) + sm3.z = np.array([10, 10]) sm4 = StationManager(2) sm4.x = np.array([10, 10]) sm4.y = np.array([0, 0]) sm4.z = np.array([10, 20]) - sm4.height = np.array([10, 20]) + sm4.z = np.array([10, 20]) elevation_ref = np.array([[0, 45], [0, 26.56]]) - npt.assert_allclose(elevation_ref, sm3.get_elevation(sm4), atol=1e-2) + npt.assert_allclose(elevation_ref, sm3.get_local_elevation(sm4), atol=1e-2) if __name__ == '__main__': diff --git a/tests/test_topology_imt_mss_dc.py b/tests/test_topology_imt_mss_dc.py index d85c3d6cf..50bc0767a 100644 --- a/tests/test_topology_imt_mss_dc.py +++ b/tests/test_topology_imt_mss_dc.py @@ -117,7 +117,7 @@ def test_calculate_coordinates(self): ref_space_stations.y = self.imt_mss_dc_topology.space_station_y ref_space_stations.z = self.imt_mss_dc_topology.space_station_z - phi, theta = ref_space_stations.get_pointing_vector_to( + phi, theta = ref_space_stations.get_global_pointing_vector_to( ref_earth_center) npt.assert_array_almost_equal( np.squeeze( From 7da87e3d59c082db3db3de11cf7b995951a1b924 Mon Sep 17 00:00:00 2001 From: artistrea Date: Thu, 2 Oct 2025 11:58:04 -0300 Subject: [PATCH 19/77] feat: simulator geometry --- .../scripts/imt_ntn_footprint.py | 8 +- sharc/propagation/propagation_abg.py | 4 +- .../propagation/propagation_clear_air_452.py | 6 +- sharc/propagation/propagation_free_space.py | 2 +- sharc/propagation/propagation_hdfss.py | 16 +- sharc/propagation/propagation_indoor.py | 8 +- sharc/propagation/propagation_p619.py | 10 +- sharc/propagation/propagation_sat_simple.py | 6 +- sharc/propagation/propagation_ter_simple.py | 2 +- sharc/propagation/propagation_tvro.py | 10 +- sharc/propagation/propagation_uma.py | 10 +- sharc/propagation/propagation_umi.py | 10 +- sharc/satellite/scripts/plot_footprints.py | 42 +- .../scripts/plot_orbits_single_color_3d.py | 22 +- sharc/simulation.py | 44 +- sharc/simulation_downlink.py | 4 +- sharc/station_factory.py | 278 +++++----- sharc/station_manager.py | 278 +--------- sharc/support/geometry.py | 483 ++++++++++++++++++ sharc/support/sharc_geom.py | 111 +--- tests/e2e/test_integration_imt_victim.py | 20 +- tests/e2e/test_integration_sys_victim.py | 16 +- tests/test_adjacent_channel.py | 20 +- tests/test_geometry_converter.py | 69 ++- tests/test_simulation_downlink.py | 30 +- tests/test_simulation_downlink_haps.py | 4 +- tests/test_simulation_downlink_tvro.py | 16 +- tests/test_simulation_indoor.py | 12 +- tests/test_simulation_uplink.py | 40 +- tests/test_station_factory.py | 44 +- tests/test_station_factory_ngso.py | 85 +-- tests/test_station_manager.py | 250 +++++---- tests/test_topology_imt_mss_dc.py | 16 +- 33 files changed, 1079 insertions(+), 897 deletions(-) create mode 100644 sharc/support/geometry.py diff --git a/sharc/campaigns/imt_ntn_footprint/scripts/imt_ntn_footprint.py b/sharc/campaigns/imt_ntn_footprint/scripts/imt_ntn_footprint.py index d42beacd6..d763091d4 100644 --- a/sharc/campaigns/imt_ntn_footprint/scripts/imt_ntn_footprint.py +++ b/sharc/campaigns/imt_ntn_footprint/scripts/imt_ntn_footprint.py @@ -68,11 +68,11 @@ ntn_ue.active = np.ones(ntn_ue.num_stations, dtype=bool) ntn_bs = StationFactory.generate_mss_ss(param_mss) - phi, theta = ntn_bs.get_global_pointing_vector_to(ntn_ue) + phi, theta = ntn_bs.geom.get_global_pointing_vector_to(ntn_ue) station_1_active = np.where(ntn_bs.active)[0] station_2_active = np.where(ntn_ue.active)[0] beams_idx = np.zeros(len(station_2_active), dtype=int) - off_axis_angle = ntn_bs.get_off_axis_angle(ntn_ue) + off_axis_angle = ntn_bs.geom.get_off_axis_angle(ntn_ue.geom) gains = np.zeros(phi.shape) for k in station_1_active: gains[k, station_2_active] = ntn_bs.antenna[k].calculate_gain( @@ -86,9 +86,9 @@ # ax.set_ylim([-200, 200]) ntn_topology.plot_3d(ax, False) # Plot the 3D topology im = ax.scatter( - xs=ntn_ue.x / + xs=ntn_ue.geom.x_global / 1000, - ys=ntn_ue.y / + ys=ntn_ue.geom.y_global / 1000, c=gains[beam_idx] - np.max( diff --git a/sharc/propagation/propagation_abg.py b/sharc/propagation/propagation_abg.py index 12a02cc26..e2bd9b17e 100644 --- a/sharc/propagation/propagation_abg.py +++ b/sharc/propagation/propagation_abg.py @@ -74,9 +74,9 @@ def get_loss( if wrap_around_enabled: _, distances_3d, _, _ = \ - station_a.get_global_dist_angles_wrap_around(station_b) + station_a.geom.get_global_dist_angles_wrap_around(station_b.geom) else: - distances_3d = station_a.get_3d_distance_to(station_b) + distances_3d = station_a.geom.get_3d_distance_to(station_b.geom) indoor_stations = station_a.indoor diff --git a/sharc/propagation/propagation_clear_air_452.py b/sharc/propagation/propagation_clear_air_452.py index 8ba9f1bc3..4a95d83ec 100644 --- a/sharc/propagation/propagation_clear_air_452.py +++ b/sharc/propagation/propagation_clear_air_452.py @@ -1543,15 +1543,15 @@ def get_loss( Return an array station_a.num_stations x station_b.num_stations with the path loss between each station """ - distance = station_a.get_3d_distance_to( - station_b, + distance = station_a.geom.get_3d_distance_to( + station_b.geom, ) * (1e-3) # P.452 expects Kms frequency_array = frequency * \ np.ones(distance.shape) * (1e-3) # P.452 expects GHz indoor_stations = np.tile( station_b.indoor, (station_a.num_stations, 1), ) - elevation = station_b.get_local_elevation(station_a) + elevation = station_b.geom.get_local_elevation(station_a.geom) if params.imt.interfered_with: tx_gain = station_a_gains rx_gain = station_b_gains diff --git a/sharc/propagation/propagation_free_space.py b/sharc/propagation/propagation_free_space.py index 4ae886a5d..459e8cf9b 100644 --- a/sharc/propagation/propagation_free_space.py +++ b/sharc/propagation/propagation_free_space.py @@ -47,7 +47,7 @@ def get_loss( Return an array station_a.num_stations x station_b.num_stations with the path loss between each station """ - distance_3d = station_a.get_3d_distance_to(station_b) + distance_3d = station_a.geom.get_3d_distance_to(station_b.geom) loss = self.get_free_space_loss( frequency=frequency, distance=distance_3d) diff --git a/sharc/propagation/propagation_hdfss.py b/sharc/propagation/propagation_hdfss.py index deb4998c5..b3c7e60d1 100644 --- a/sharc/propagation/propagation_hdfss.py +++ b/sharc/propagation/propagation_hdfss.py @@ -71,10 +71,10 @@ def get_loss( Return an array station_a.num_stations x station_b.num_stations with the path loss between each station """ - distance = station_a.get_3d_distance_to(station_b) # P.452 expects Kms + distance = station_a.geom.get_3d_distance_to(station_b.geom) # P.452 expects Kms frequency_array = frequency * \ np.ones(distance.shape) # P.452 expects GHz - elevation = station_b.get_local_elevation(station_a) + elevation = station_b.geom.get_local_elevation(station_a.geom) if station_a.uses_local_coords or station_b.uses_local_coords: raise NotImplementedError( @@ -87,10 +87,10 @@ def get_loss( elevation=elevation, imt_sta_type=station_b.station_type, frequency=frequency_array, - imt_x=station_b.x, - imt_y=station_b.y, - imt_z=station_b.z, - es_x=station_a.x, - es_y=station_a.y, - es_z=station_a.z, + imt_x=station_b.geom.x_global, + imt_y=station_b.geom.y_global, + imt_z=station_b.geom.z_global, + es_x=station_a.geom.x_global, + es_y=station_a.geom.y_global, + es_z=station_a.geom.z_global, ) diff --git a/sharc/propagation/propagation_indoor.py b/sharc/propagation/propagation_indoor.py index 20dc83f17..8288734cf 100644 --- a/sharc/propagation/propagation_indoor.py +++ b/sharc/propagation/propagation_indoor.py @@ -100,16 +100,16 @@ def get_loss( if wrap_around_enabled: bs_to_ue_dist_2d, bs_to_ue_dist_3d, _, _ = \ - station_b.get_global_dist_angles_wrap_around(station_a) + station_b.geom.get_global_dist_angles_wrap_around(station_a.geom) else: - bs_to_ue_dist_2d = station_b.get_local_distance_to(station_a) - bs_to_ue_dist_3d = station_b.get_3d_distance_to(station_a) + bs_to_ue_dist_2d = station_b.geom.get_local_distance_to(station_a.geom) + bs_to_ue_dist_3d = station_b.geom.get_3d_distance_to(station_a.geom) frequency_array = frequency * np.ones(bs_to_ue_dist_2d.shape) indoor_stations = np.tile( station_a.indoor, (station_b.num_stations, 1), ) - elevation = np.transpose(station_a.get_local_elevation(station_b)) + elevation = np.transpose(station_a.geom.get_local_elevation(station_b.geom)) return self.get_loss( bs_to_ue_dist_3d, diff --git a/sharc/propagation/propagation_p619.py b/sharc/propagation/propagation_p619.py index adf5120f1..903e09e81 100644 --- a/sharc/propagation/propagation_p619.py +++ b/sharc/propagation/propagation_p619.py @@ -340,7 +340,7 @@ def get_loss( Return an array station_a.num_stations x station_b.num_stations with the path loss between each station """ - distance = station_a.get_3d_distance_to(station_b) + distance = station_a.geom.get_3d_distance_to(station_b.geom) frequency = frequency * np.ones(distance.shape) indoor_stations = np.tile( station_b.indoor, (station_a.num_stations, 1), @@ -354,8 +354,8 @@ def get_loss( "P619 currently assumes earth station z == height. " "If ES has local coords != global coords, this probably isn't true" ) - earth_station_height = station_b.z - elevation_angles["free_space"] = station_b.get_local_elevation(station_a) + earth_station_height = station_b.geom.z_global + elevation_angles["free_space"] = station_b.geom.get_local_elevation(station_a.geom) earth_station_antenna_gain = station_b_gains # if (station_b_gains.shape != distance.shape): # raise ValueError(f"Invalid shape for station_b_gains = {station_b_gains.shape}") @@ -374,8 +374,8 @@ def get_loss( "P619 currently assumes earth station z == height. " "If ES has local coords != global coords, this probably isn't true" ) - earth_station_height = station_a.z - elevation_angles["free_space"] = station_a.get_local_elevation(station_b) + earth_station_height = station_a.geom.z_global + elevation_angles["free_space"] = station_a.geom.get_local_elevation(station_b.geom) earth_station_antenna_gain = station_a_gains elevation_angles["apparent"] = self.apparent_elevation_angle( elevation_angles["free_space"], diff --git a/sharc/propagation/propagation_sat_simple.py b/sharc/propagation/propagation_sat_simple.py index f90d6ac77..8f5bab832 100644 --- a/sharc/propagation/propagation_sat_simple.py +++ b/sharc/propagation/propagation_sat_simple.py @@ -66,7 +66,7 @@ def get_loss( Return an array station_a.num_stations x station_b.num_stations with the path loss between each station """ - distance_3d = station_a.get_3d_distance_to(station_b) + distance_3d = station_a.geom.get_3d_distance_to(station_b.geom) frequency = frequency * np.ones(distance_3d.shape) indoor_stations = np.tile( station_b.indoor, (station_a.num_stations, 1), @@ -78,7 +78,7 @@ def get_loss( "FIXME: apparent_elevation_angle should receive earth station altitude..." ) if station_a.is_space_station: - elevation_angles["free_space"] = station_b.get_local_elevation(station_a) + elevation_angles["free_space"] = station_b.geom.get_local_elevation(station_a.geom) # if (station_b_gains.shape != distance.shape): # raise ValueError(f"Invalid shape for station_b_gains = {station_b_gains.shape}") elevation_angles["apparent"] = PropagationP619.apparent_elevation_angle( @@ -89,7 +89,7 @@ def get_loss( elevation_angles["apparent"] = np.transpose( elevation_angles["apparent"]) elif station_b.is_space_station: - elevation_angles["free_space"] = station_a.get_local_elevation(station_b) + elevation_angles["free_space"] = station_a.geom.get_local_elevation(station_b.geom) elevation_angles["apparent"] = PropagationP619.apparent_elevation_angle( elevation_angles["free_space"], station_b.height, ) else: diff --git a/sharc/propagation/propagation_ter_simple.py b/sharc/propagation/propagation_ter_simple.py index bd20234ad..9008c87de 100644 --- a/sharc/propagation/propagation_ter_simple.py +++ b/sharc/propagation/propagation_ter_simple.py @@ -64,7 +64,7 @@ def get_loss( Return an array station_a.num_stations x station_b.num_stations with the path loss between each station """ - distance = station_a.get_3d_distance_to(station_b) + distance = station_a.geom.get_3d_distance_to(station_b.geom) frequency_array = frequency * np.ones(distance.shape) indoor_stations = np.tile( station_b.indoor, (station_a.num_stations, 1), diff --git a/sharc/propagation/propagation_tvro.py b/sharc/propagation/propagation_tvro.py index a92eeea49..dcb2f81a2 100644 --- a/sharc/propagation/propagation_tvro.py +++ b/sharc/propagation/propagation_tvro.py @@ -81,10 +81,10 @@ def get_loss( if wrap_around_enabled and ( station_a.is_imt_station() and station_b.is_imt_station()): distances_2d, distances_3d, _, _ = \ - station_a.get_global_dist_angles_wrap_around(station_b) + station_a.geom.get_global_dist_angles_wrap_around(station_b.geom) else: - distances_2d = station_a.get_local_distance_to(station_b) - distances_3d = station_a.get_3d_distance_to(station_b) + distances_2d = station_a.geom.get_local_distance_to(station_b.geom) + distances_3d = station_a.geom.get_3d_distance_to(station_b.geom) indoor_stations = np.tile( station_a.indoor, (station_b.num_stations, 1)).transpose() @@ -102,7 +102,7 @@ def get_loss( distance_3D=distances_3d, distance_2D=distances_2d, frequency=frequency * np.ones(distances_2d.shape), - ue_height=station_a.z, + ue_height=station_a.geom.z_global, indoor_stations=indoor_stations ) else: @@ -120,7 +120,7 @@ def get_loss( distance_2D=distances_2d, frequency=frequency * np.ones(distances_2d.shape), imt_sta_type=imt_station.station_type, - es_z=sys_station.z, + es_z=sys_station.geom.z_global, indoor_stations=indoor_stations ) diff --git a/sharc/propagation/propagation_uma.py b/sharc/propagation/propagation_uma.py index df1eff62c..c1ad905dc 100644 --- a/sharc/propagation/propagation_uma.py +++ b/sharc/propagation/propagation_uma.py @@ -61,10 +61,10 @@ def get_loss( if wrap_around_enabled and ( station_a.is_imt_station() and station_b.is_imt_station()): distances_2d, distances_3d, _, _ = \ - station_a.get_global_dist_angles_wrap_around(station_b) + station_a.geom.get_global_dist_angles_wrap_around(station_b.geom) else: - distances_2d = station_a.get_local_distance_to(station_b) - distances_3d = station_a.get_3d_distance_to(station_b) + distances_2d = station_a.geom.get_local_distance_to(station_b.geom) + distances_3d = station_a.geom.get_3d_distance_to(station_b.geom) if station_a.uses_local_coords or station_b.uses_local_coords: raise NotImplementedError( @@ -76,8 +76,8 @@ def get_loss( distances_3d, distances_2d, frequency * np.ones(distances_2d.shape), - station_b.z, - station_a.z, + station_b.geom.z_global, + station_a.geom.z_global, params.imt.shadowing, ) diff --git a/sharc/propagation/propagation_umi.py b/sharc/propagation/propagation_umi.py index ae5a20d35..2bd905af3 100644 --- a/sharc/propagation/propagation_umi.py +++ b/sharc/propagation/propagation_umi.py @@ -67,10 +67,10 @@ def get_loss( if wrap_around_enabled and ( station_a.is_imt_station() and station_b.is_imt_station()): distance_2d, distance_3d, _, _ = \ - station_a.get_global_dist_angles_wrap_around(station_b) + station_a.geom.get_global_dist_angles_wrap_around(station_b.geom) else: - distance_2d = station_a.get_local_distance_to(station_b) - distance_3d = station_a.get_3d_distance_to(station_b) + distance_2d = station_a.geom.get_local_distance_to(station_b.geom) + distance_3d = station_a.geom.get_3d_distance_to(station_b.geom) if station_a.uses_local_coords or station_b.uses_local_coords: raise NotImplementedError( @@ -82,8 +82,8 @@ def get_loss( distance_3d, distance_2d, frequency * np.ones(distance_2d.shape), - station_b.z, - station_a.z, + station_b.geom.z_global, + station_a.geom.z_global, params.imt.shadowing, ) diff --git a/sharc/satellite/scripts/plot_footprints.py b/sharc/satellite/scripts/plot_footprints.py index e7eab2150..2689b79ee 100644 --- a/sharc/satellite/scripts/plot_footprints.py +++ b/sharc/satellite/scripts/plot_footprints.py @@ -82,9 +82,9 @@ def plot_fp( center_of_earth = StationManager(1) # rotated and then translated center of earth - center_of_earth.x = np.array([0.0]) - center_of_earth.y = np.array([0.0]) - center_of_earth.z = np.array([-coord_sys.get_translation()]) + center_of_earth.geom.x_global = np.array([0.0]) + center_of_earth.geom.y_global = np.array([0.0]) + center_of_earth.geom.z_global = np.array([-coord_sys.get_translation()]) mss_d2d_manager = StationFactory.generate_mss_d2d(params, rng, coord_sys) @@ -108,7 +108,7 @@ def plot_fp( range=(-show_range / 2, show_range / 2) ), camera=dict( - center=dict(x=0, y=0, z=center_of_earth.z[0] / show_range / 1e3), + center=dict(x=0, y=0, z=center_of_earth.geom.z_global[0] / show_range / 1e3), eye=dict(x=0, y=0, z=0.7), # Eye position (above the center) up=dict(x=0, y=1, z=0) # "Up" is along +y (default is usually +z) ) @@ -129,9 +129,9 @@ def plot_fp( center_fp_at_sat = 0 # get original sat xyz orx, ory, orz = coord_sys.enu2ecef( - station_1.x[center_fp_at_sat], - station_1.y[center_fp_at_sat], - station_1.z[center_fp_at_sat], + station_1.geom.x_global[center_fp_at_sat], + station_1.geom.y_global[center_fp_at_sat], + station_1.geom.z_global[center_fp_at_sat], ) sat_lat, sat_long, sat_alt = ecef2lla(orx, ory, orz) @@ -159,9 +159,9 @@ def plot_fp( # creates a StationManager to calculate the gains on surf_manager = StationManager(len(x_flat)) - surf_manager.x = x_flat - surf_manager.y = y_flat - surf_manager.z = z_flat + surf_manager.geom.x_global = x_flat + surf_manager.geom.y_global = y_flat + surf_manager.geom.z_global = z_flat station_1 = mss_d2d_manager mss_active = np.where(station_1.active)[0] @@ -171,8 +171,8 @@ def plot_fp( print("Calculating gains (memory intensive)") # Calculate vector and apointment off_axis gains = np.zeros((len(mss_active), len(station_2_active))) - off_axis_angle = station_1.get_off_axis_angle(station_2) - phi, theta = station_1.get_global_pointing_vector_to(station_2) + off_axis_angle = station_1.geom.get_off_axis_angle(station_2.geom) + phi, theta = station_1.geom.get_global_pointing_vector_to(station_2.geom) for k in range(len(mss_active)): gains[k, :] = \ station_1.antenna[k].calculate_gain( @@ -243,9 +243,9 @@ def plot_fp( # Plot all satellites (red markers) print("adding sats") fig.add_trace(go.Scatter3d( - x=mss_d2d_manager.x / 1e3, - y=mss_d2d_manager.y / 1e3, - z=mss_d2d_manager.z / 1e3, + x=mss_d2d_manager.geom.x_global / 1e3, + y=mss_d2d_manager.geom.y_global / 1e3, + z=mss_d2d_manager.geom.z_global / 1e3, mode='markers', marker=dict(size=2, color='red', opacity=0.5), showlegend=False @@ -254,9 +254,9 @@ def plot_fp( # Plot visible satellites (green markers) # print(visible_positions['x'][visible_positions['x'] > 0]) fig.add_trace(go.Scatter3d( - x=mss_d2d_manager.x[mss_d2d_manager.active] / 1e3, - y=mss_d2d_manager.y[mss_d2d_manager.active] / 1e3, - z=mss_d2d_manager.z[mss_d2d_manager.active] / 1e3, + x=mss_d2d_manager.geom.x_global[mss_d2d_manager.active] / 1e3, + y=mss_d2d_manager.geom.y_global[mss_d2d_manager.active] / 1e3, + z=mss_d2d_manager.geom.z_global[mss_d2d_manager.active] / 1e3, mode='markers', marker=dict(size=3, color='green', opacity=0.8), name="MSS D2D sat", @@ -335,9 +335,9 @@ def plot_fp( name="Exclusion Zone" )) # fig.add_trace(go.Scatter3d( - # x=center_of_earth.x / 1e3, - # y=center_of_earth.y / 1e3, - # z=center_of_earth.z / 1e3, + # x=center_of_earth.geom.x_global / 1e3, + # y=center_of_earth.geom.y_global / 1e3, + # z=center_of_earth.geom.z_global / 1e3, # mode='markers', # marker=dict(size=5, color='black', opacity=1.0), # showlegend=False diff --git a/sharc/satellite/scripts/plot_orbits_single_color_3d.py b/sharc/satellite/scripts/plot_orbits_single_color_3d.py index 362e1d7d7..0417d705d 100644 --- a/sharc/satellite/scripts/plot_orbits_single_color_3d.py +++ b/sharc/satellite/scripts/plot_orbits_single_color_3d.py @@ -77,9 +77,9 @@ center_of_earth = StationManager(1) # rotated and then translated center of earth - center_of_earth.x = np.array([0.0]) - center_of_earth.y = np.array([0.0]) - center_of_earth.z = np.array([-coord_sys.get_translation()]) + center_of_earth.geom.x_global = np.array([0.0]) + center_of_earth.geom.y_global = np.array([0.0]) + center_of_earth.geom.z_global = np.array([-coord_sys.get_translation()]) vis_elevation = [] for _ in range(NUM_DROPS): @@ -87,9 +87,9 @@ mss_d2d_manager = StationFactory.generate_mss_d2d(params, rng, coord_sys) # Extract satellite positions - x_vec = mss_d2d_manager.x / 1e3 # (Km) - y_vec = mss_d2d_manager.y / 1e3 # (Km) - z_vec = mss_d2d_manager.z / 1e3 # (Km) + x_vec = mss_d2d_manager.geom.x_global / 1e3 # (Km) + y_vec = mss_d2d_manager.geom.y_global / 1e3 # (Km) + z_vec = mss_d2d_manager.geom.z_global / 1e3 # (Km) # Store all positions all_positions['x'].extend(x_vec) all_positions['y'].extend(y_vec) @@ -99,12 +99,12 @@ vis_sat_idxs = np.where(mss_d2d_manager.active)[0] # should be pointing at nadir - off_axis = mss_d2d_manager.get_off_axis_angle(center_of_earth) + off_axis = mss_d2d_manager.geom.get_off_axis_angle(center_of_earth.geom) visible_positions['x'].extend(x_vec[vis_sat_idxs]) visible_positions['y'].extend(y_vec[vis_sat_idxs]) visible_positions['z'].extend(z_vec[vis_sat_idxs]) - vis_elevation.extend(mss_d2d_manager.elevation[vis_sat_idxs]) + vis_elevation.extend(mss_d2d_manager.geom.pointn_elev_global[vis_sat_idxs]) # Flatten arrays all_positions['x'] = np.concatenate([all_positions['x']]) @@ -153,9 +153,9 @@ )) # fig.add_trace(go.Scatter3d( - # x=center_of_earth.x / 1e3, - # y=center_of_earth.y / 1e3, - # z=center_of_earth.z / 1e3, + # x=center_of_earth.geom.x_global / 1e3, + # y=center_of_earth.geom.y_global / 1e3, + # z=center_of_earth.geom.z_global / 1e3, # mode='markers', # marker=dict(size=5, color='black', opacity=1.0), # showlegend=False diff --git a/sharc/simulation.py b/sharc/simulation.py index aaf430716..23f91e1fd 100644 --- a/sharc/simulation.py +++ b/sharc/simulation.py @@ -475,16 +475,16 @@ def select_ue(self, random_number_gen: np.random.RandomState): """ if self.wrap_around_enabled: self.bs_to_ue_d_2D, self.bs_to_ue_d_3D, self.bs_to_ue_phi, self.bs_to_ue_theta = \ - self.bs.get_global_dist_angles_wrap_around(self.ue) + self.bs.geom.get_global_dist_angles_wrap_around(self.ue.geom) else: - self.bs_to_ue_d_2D = self.bs.get_local_distance_to(self.ue) - self.bs_to_ue_d_3D = self.bs.get_3d_distance_to(self.ue) - self.bs_to_ue_phi, self.bs_to_ue_theta = self.bs.get_global_pointing_vector_to( - self.ue, ) + self.bs_to_ue_d_2D = self.bs.geom.get_local_distance_to(self.ue.geom) + self.bs_to_ue_d_3D = self.bs.geom.get_3d_distance_to(self.ue.geom) + self.bs_to_ue_phi, self.bs_to_ue_theta = self.bs.geom.get_global_pointing_vector_to( + self.ue.geom, ) 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" + assert np.all((-180 <= self.bs.geom.pointn_azim_global) & (self.bs.geom.pointn_azim_global <= 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]) @@ -498,7 +498,7 @@ def select_ue(self, random_number_gen: np.random.RandomState): # limit beamforming angle beam_h_min, beam_h_max = wrap2_180( - self.parameters.imt.bs.antenna.array.horizontal_beamsteering_range + self.bs.azimuth[bs] + self.parameters.imt.bs.antenna.array.horizontal_beamsteering_range + self.bs.geom.pointn_azim_global[bs] ) bs_beam_phi = clip_angle( @@ -576,7 +576,7 @@ def calculate_gains( theta = self.bs_to_ue_theta beams_idx = self.bs_to_ue_beam_rbs[station_2_active] elif not station_2.is_imt_station(): - phi, theta = station_1.get_global_pointing_vector_to(station_2) + phi, theta = station_1.geom.get_global_pointing_vector_to(station_2.geom) phi = np.repeat(phi, self.parameters.imt.ue.k, 0) theta = np.repeat(theta, self.parameters.imt.ue.k, 0) beams_idx = np.tile( @@ -584,17 +584,17 @@ def calculate_gains( ) elif (station_1.station_type is StationType.IMT_UE): - phi, theta = station_1.get_global_pointing_vector_to(station_2) + phi, theta = station_1.geom.get_global_pointing_vector_to(station_2.geom) beams_idx = np.zeros(len(station_2_active), dtype=int) elif not station_1.is_imt_station(): - phi, theta = station_1.get_global_pointing_vector_to(station_2) + phi, theta = station_1.geom.get_global_pointing_vector_to(station_2.geom) beams_idx = np.zeros(len(station_2_active), dtype=int) # Calculate gains gains = np.zeros(phi.shape) if station_1.station_type is StationType.IMT_BS and not station_2.is_imt_station(): - off_axis_angle = station_1.get_off_axis_angle(station_2) + off_axis_angle = station_1.geom.get_off_axis_angle(station_2.geom) for k in station_1_active: for b in range( k * self.parameters.imt.ue.k, @@ -612,7 +612,7 @@ def calculate_gains( station_2_active]) elif station_1.station_type is StationType.IMT_UE and not station_2.is_imt_station(): - off_axis_angle = station_1.get_off_axis_angle(station_2) + off_axis_angle = station_1.geom.get_off_axis_angle(station_2.geom) for k in station_1_active: gains[k, station_2_active] = station_1.antenna[k].calculate_gain( off_axis_angle_vec=off_axis_angle[k, station_2_active], @@ -633,8 +633,8 @@ def calculate_gains( elif not station_1.is_imt_station(): - off_axis_angle = station_1.get_off_axis_angle(station_2) - phi, theta = station_1.get_global_pointing_vector_to(station_2) + off_axis_angle = station_1.geom.get_off_axis_angle(station_2.geom) + phi, theta = station_1.geom.get_global_pointing_vector_to(station_2.geom) for k in station_1_active: gains[k, station_2_active] = \ station_1.antenna[k].calculate_gain( @@ -643,7 +643,7 @@ def calculate_gains( phi_vec=phi[k, station_2_active], ) else: # for IMT <-> IMT - off_axis_angle = station_1.get_off_axis_angle(station_2) + off_axis_angle = station_1.geom.get_off_axis_angle(station_2.geom) for k in station_1_active: gains[k, station_2_active] = station_1.antenna[k].calculate_gain( off_axis_angle_vec=off_axis_angle[k, station_2_active], @@ -739,7 +739,7 @@ def plot_scenario(self): # Plot user equipments ax.scatter( - self.ue.x, self.ue.y, color='r', + self.ue.geom.x_global, self.ue.geom.y_global, color='r', edgecolor="w", linewidth=0.5, label="UE", ) @@ -748,13 +748,13 @@ def plot_scenario(self): # Plot UE's azimuth d = 0.1 * self.topology.cell_radius - for i in range(len(self.ue.x)): + for i in range(len(self.ue.geom.x_global)): plt.plot( - [self.ue.x[i], self.ue.x[i] + d * - math.cos(math.radians(self.ue.azimuth[i]))], + [self.ue.geom.x_global[i], self.ue.geom.x_global[i] + d * + math.cos(math.radians(self.ue.geom.pointn_azim_global[i]))], [ - self.ue.y[i], self.ue.y[i] + d * - math.sin(math.radians(self.ue.azimuth[i])), + self.ue.geom.y_global[i], self.ue.geom.y_global[i] + d * + math.sin(math.radians(self.ue.geom.pointn_azim_global[i])), ], 'r-', ) @@ -776,7 +776,7 @@ def plot_scenario(self): # Plot user equipments ax.scatter( - self.ue.x, self.ue.z, color='r', + self.ue.geom.x_global, self.ue.geom.z_global, color='r', edgecolor="w", linewidth=0.5, label="UE", ) diff --git a/sharc/simulation_downlink.py b/sharc/simulation_downlink.py index 04a241096..ebfcb3cc8 100644 --- a/sharc/simulation_downlink.py +++ b/sharc/simulation_downlink.py @@ -378,8 +378,8 @@ def calculate_sinr_ext(self): # Calculate PFD at the UE # Distance from each system transmitter to each UE receiver (in meters) - dist_sys_to_imt = self.system.get_3d_distance_to( - self.ue) # shape: [n_tx, n_ue] + dist_sys_to_imt = self.system.geom.get_3d_distance_to( + self.ue.geom) # shape: [n_tx, n_ue] # EIRP in dBW/MHz per transmitter eirp_dBW_MHz = self.param_system.tx_power_density + \ diff --git a/sharc/station_factory.py b/sharc/station_factory.py index a2db26d1b..9a3ca6400 100644 --- a/sharc/station_factory.py +++ b/sharc/station_factory.py @@ -97,27 +97,27 @@ def generate_imt_base_stations( imt_base_stations = StationManager(num_bs) imt_base_stations.station_type = StationType.IMT_BS if param.topology.type == "NTN": - imt_base_stations.x = topology.space_station_x * np.ones(num_bs) - imt_base_stations.y = topology.space_station_y * np.ones(num_bs) - imt_base_stations.z = topology.space_station_z * np.ones(num_bs) - imt_base_stations.elevation = topology.elevation + imt_base_stations.geom.x_global = topology.space_station_x * np.ones(num_bs) + imt_base_stations.geom.y_global = topology.space_station_y * np.ones(num_bs) + imt_base_stations.geom.z_global = topology.space_station_z * np.ones(num_bs) + imt_base_stations.geom.pointn_elev_global = topology.elevation imt_base_stations.is_space_station = True elif param.topology.type == "MSS_DC": - imt_base_stations.x = topology.space_station_x * np.ones(num_bs) - imt_base_stations.y = topology.space_station_y * np.ones(num_bs) - imt_base_stations.z = topology.space_station_z * np.ones(num_bs) - imt_base_stations.elevation = topology.elevation + imt_base_stations.geom.x_global = topology.space_station_x * np.ones(num_bs) + imt_base_stations.geom.y_global = topology.space_station_y * np.ones(num_bs) + imt_base_stations.geom.z_global = topology.space_station_z * np.ones(num_bs) + imt_base_stations.geom.pointn_elev_global = topology.elevation imt_base_stations.is_space_station = True else: - imt_base_stations.x = topology.x - imt_base_stations.y = topology.y - imt_base_stations.elevation = -param_ant.downtilt * np.ones(num_bs) + imt_base_stations.geom.x_global = topology.x + imt_base_stations.geom.y_global = topology.y + imt_base_stations.geom.pointn_elev_global = -param_ant.downtilt * np.ones(num_bs) if param.topology.type == 'INDOOR': - imt_base_stations.z = topology.height + imt_base_stations.geom.z_global = topology.height else: - imt_base_stations.z = param.bs.height * np.ones(num_bs) + imt_base_stations.geom.z_global = param.bs.height * np.ones(num_bs) - imt_base_stations.azimuth = wrap2_180(topology.azimuth) + imt_base_stations.geom.pointn_azim_global = wrap2_180(topology.azimuth) imt_base_stations.active = random_number_gen.rand( num_bs, ) < param.bs.load_probability @@ -156,8 +156,8 @@ def generate_imt_base_stations( imt_base_stations.antenna = AntennaFactory.create_n_antennas( param.bs.antenna, - imt_base_stations.azimuth, - imt_base_stations.elevation, + imt_base_stations.geom.pointn_azim_global, + imt_base_stations.geom.pointn_elev_global, num_bs ) @@ -194,9 +194,9 @@ def generate_imt_base_stations( f"Invalid IMT-BS spectral mask {param.spectral_mask}") if param.topology.type == 'MACROCELL': - imt_base_stations.intersite_dist = param.topology.macrocell.intersite_distance + imt_base_stations.geom.intersite_dist = param.topology.macrocell.intersite_distance elif param.topology.type == 'HOTSPOT': - imt_base_stations.intersite_dist = param.topology.hotspot.intersite_distance + imt_base_stations.geom.intersite_dist = param.topology.hotspot.intersite_distance return imt_base_stations @@ -296,7 +296,7 @@ def generate_imt_ue_outdoor( ue_y = list() ue_z = list() - imt_ue.z = param.ue.height * np.ones(num_ue) + imt_ue.geom.z_global = param.ue.height * np.ones(num_ue) # TODO: Sanitaze the azimuth_range parameter azimuth_range = param.ue.azimuth_range @@ -340,8 +340,8 @@ def generate_imt_ue_outdoor( ) # FIXME: adapt this to also work with local coords (and space stations) - imt_ue.azimuth = (azimuth + theta + np.pi / 2) - imt_ue.elevation = elevation + psi + imt_ue.geom.pointn_azim_global = (azimuth + theta + np.pi / 2) + imt_ue.geom.pointn_elev_global = elevation + psi elif param.ue.distribution_type.upper() == "ANGLE_AND_DISTANCE": # The Rayleigh and Normal distribution parameters (mean, scale and cutoff) @@ -429,7 +429,7 @@ def generate_imt_ue_outdoor( # calculate UE azimuth wrt serving BS # FIXME: adapt this to also work with local coords (and space stations) - imt_ue.azimuth[idx] = (azimuth[idx] + theta + 180) % 360 + imt_ue.geom.pointn_azim_global[idx] = (azimuth[idx] + theta + 180) % 360 # calculate elevation angle # psi is the vertical angle of the UE wrt the serving BS @@ -440,7 +440,7 @@ def generate_imt_ue_outdoor( np.arctan((param.bs.height - param.ue.height) / distance), ) # FIXME: adapt this to also work with space stations - imt_ue.elevation[idx] = elevation[idx] + psi + imt_ue.geom.pointn_elev_global[idx] = elevation[idx] + psi else: sys.stderr.write( @@ -449,9 +449,9 @@ def generate_imt_ue_outdoor( ) sys.exit(1) - imt_ue.x = np.array(ue_x) - imt_ue.y = np.array(ue_y) - imt_ue.z = np.array(ue_z) + param.ue.height + imt_ue.geom.x_global = np.array(ue_x) + imt_ue.geom.y_global = np.array(ue_y) + imt_ue.geom.z_global = np.array(ue_z) + param.ue.height imt_ue.active = np.zeros(num_ue, dtype=bool) imt_ue.indoor = random_number_gen.random_sample( @@ -464,8 +464,8 @@ def generate_imt_ue_outdoor( ue_param_ant.get_antenna_parameters() imt_ue.antenna = AntennaFactory.create_n_antennas( param.ue.antenna, - imt_ue.azimuth, - imt_ue.elevation, + imt_ue.geom.pointn_azim_global, + imt_ue.geom.pointn_elev_global, num_ue, ) @@ -498,9 +498,9 @@ def generate_imt_ue_outdoor( imt_ue.spectral_mask.set_mask() if param.topology.type == 'MACROCELL': - imt_ue.intersite_dist = param.topology.macrocell.intersite_distance + imt_ue.geom.intersite_dist = param.topology.macrocell.intersite_distance elif param.topology.type == 'HOTSPOT': - imt_ue.intersite_dist = param.topology.hotspot.intersite_distance + imt_ue.geom.intersite_dist = param.topology.hotspot.intersite_distance return imt_ue @@ -606,7 +606,7 @@ def generate_imt_ue_indoor( ), ) # calculate UE azimuth wrt serving BS - imt_ue.azimuth[idx] = (azimuth[idx] + theta + 180) % 360 + imt_ue.geom.pointn_azim_global[idx] = (azimuth[idx] + theta + 180) % 360 # calculate elevation angle # psi is the vertical angle of the UE wrt the serving BS @@ -616,7 +616,7 @@ def generate_imt_ue_indoor( psi = np.degrees( np.arctan((param.bs.height - param.ue.height) / distance), ) - imt_ue.elevation[idx] = elevation[idx] + psi + imt_ue.geom.pointn_elev_global[idx] = elevation[idx] + psi # check if UE is indoor if bs % topology.num_cells == 0: @@ -632,9 +632,9 @@ def generate_imt_ue_indoor( (y < topology.y[bs] - topology.b_d / 2) imt_ue.indoor[idx] = ~ out - imt_ue.x = np.array(ue_x) - imt_ue.y = np.array(ue_y) - imt_ue.z = np.array(ue_z) + imt_ue.geom.x_global = np.array(ue_x) + imt_ue.geom.y_global = np.array(ue_y) + imt_ue.geom.z_global = np.array(ue_z) imt_ue.active = np.zeros(num_ue, dtype=bool) imt_ue.rx_interference = -500 * np.ones(num_ue) @@ -644,8 +644,8 @@ def generate_imt_ue_indoor( par = ue_param_ant.get_antenna_parameters() for i in range(num_ue): imt_ue.antenna[i] = AntennaBeamformingImt( - par, imt_ue.azimuth[i], - imt_ue.elevation[i], + par, imt_ue.geom.pointn_azim_global[i], + imt_ue.geom.pointn_elev_global[i], ) # imt_ue.antenna = [AntennaOmni(0) for bs in range(num_ue)] @@ -778,13 +778,13 @@ def generate_single_space_station( param.geometry.altitude, ) - space_station.x = x - space_station.y = y - space_station.z = z + space_station.geom.x_global = x + space_station.geom.y_global = y + space_station.geom.z_global = z if param.geometry.azimuth.type == "POINTING_AT_IMT": - space_station.azimuth = np.rad2deg( - np.arctan2(-space_station.y, -space_station.x)) + space_station.geom.pointn_azim_global = np.rad2deg( + np.arctan2(-space_station.geom.y_global, -space_station.geom.x_global)) elif param.geometry.azimuth.type == "POINTING_AT_LAT_LONG_ALT": px, py, pz = coord_sys.lla2enu( param.geometry.pointing_at_lat, @@ -792,10 +792,10 @@ def generate_single_space_station( param.geometry.pointing_at_alt, ) - space_station.azimuth = np.rad2deg( - np.arctan2(py - space_station.y, px - space_station.x)) + space_station.geom.pointn_azim_global = np.rad2deg( + np.arctan2(py - space_station.geom.y_global, px - space_station.geom.x_global)) elif param.geometry.azimuth.type == "FIXED": - space_station.azimuth = param.geometry.azimuth.fixed + space_station.geom.pointn_azim_global = param.geometry.azimuth.fixed else: raise ValueError( f"Did not recognize azimuth type of { @@ -804,30 +804,30 @@ def generate_single_space_station( if param.geometry.elevation.type == "POINTING_AT_IMT": gnd_elev = np.rad2deg( np.arctan2( - space_station.z, + space_station.geom.z_global, np.sqrt( - space_station.y * - space_station.y + - space_station.x * - space_station.x))) - space_station.elevation = -gnd_elev + space_station.geom.y_global * + space_station.geom.y_global + + space_station.geom.x_global * + space_station.geom.x_global))) + space_station.geom.pointn_elev_global = -gnd_elev elif param.geometry.elevation.type == "POINTING_AT_LAT_LONG_ALT": px, py, pz = coord_sys.lla2enu( param.geometry.pointing_at_lat, param.geometry.pointing_at_long, param.geometry.pointing_at_alt, ) - dy = py - space_station.y - dx = px - space_station.x - dz = pz - space_station.z + dy = py - space_station.geom.y_global + dx = px - space_station.geom.x_global + dz = pz - space_station.geom.z_global gnd_elev = np.rad2deg( np.arctan2( dz, np.sqrt(dy * dy + dx * dx))) - space_station.elevation = gnd_elev + space_station.geom.pointn_elev_global = gnd_elev elif param.geometry.elevation.type == "FIXED": - space_station.elevation = param.geometry.elevation.fixed + space_station.geom.pointn_elev_global = param.geometry.elevation.fixed else: raise ValueError( f"Did not recognize elevation type of { @@ -841,8 +841,8 @@ def generate_single_space_station( space_station.rx_interference = -500 space_station.antenna = np.array([ - AntennaFactory.create_antenna(param.antenna, space_station.azimuth[0], - space_station.elevation[0]) + AntennaFactory.create_antenna(param.antenna, space_station.geom.pointn_azim_global[0], + space_station.geom.pointn_elev_global[0]) ]) space_station.bandwidth = param.bandwidth @@ -891,19 +891,19 @@ def generate_fss_space_station(param: ParametersFssSs): # rotate axis and calculate coordinates with origin at IMT system imt_lat_rad = param.earth_station_lat_deg * np.pi / 180. - fss_space_station.x = np.array( + fss_space_station.geom.x_global = np.array( [x1 * np.sin(imt_lat_rad) - z1 * np.cos(imt_lat_rad)], ) * 1000 - fss_space_station.y = np.array([y1]) * 1000 - fss_space_station.z = np.array([ + fss_space_station.geom.y_global = np.array([y1]) * 1000 + fss_space_station.geom.z_global = np.array([ ( z1 * np.sin(imt_lat_rad) + x1 * np.cos(imt_lat_rad) - dist_imt_centre_earth_km ) * 1000, ]) - fss_space_station.azimuth = np.array([param.azimuth]) - fss_space_station.elevation = np.array([param.elevation]) + fss_space_station.geom.pointn_azim_global = np.array([param.azimuth]) + fss_space_station.geom.pointn_elev_global = np.array([param.elevation]) fss_space_station.active = np.array([True]) fss_space_station.tx_power = np.array( @@ -974,22 +974,22 @@ def generate_fss_earth_station( fss_earth_station.station_type = StationType.FSS_ES if param.location.upper() == "FIXED": - fss_earth_station.x = np.array([param.x]) - fss_earth_station.y = np.array([param.y]) + fss_earth_station.geom.x_global = np.array([param.x]) + fss_earth_station.geom.y_global = np.array([param.y]) elif param.location.upper() == "CELL": x, y, _, _ = StationFactory.get_random_position( 1, topology, random_number_gen, param.min_dist_to_bs, True, ) - fss_earth_station.x = np.array(x) - fss_earth_station.y = np.array(y) + fss_earth_station.geom.x_global = np.array(x) + fss_earth_station.geom.y_global = np.array(y) elif param.location.upper() == "NETWORK": x, y, _, _ = StationFactory.get_random_position( 1, topology, random_number_gen, param.min_dist_to_bs, False, ) - fss_earth_station.x = np.array(x) - fss_earth_station.y = np.array(y) + fss_earth_station.geom.x_global = np.array(x) + fss_earth_station.geom.y_global = np.array(y) elif param.location.upper() == "UNIFORM_DIST": # FSS ES is randomly (uniform) created inside a circle of radius # equal to param.max_dist_to_bs @@ -1009,8 +1009,8 @@ def generate_fss_earth_station( if (radius > param.min_dist_to_bs) & ( radius < param.max_dist_to_bs): break - fss_earth_station.x[0] = dist_x - fss_earth_station.y[0] = dist_y + fss_earth_station.geom.x_global[0] = dist_x + fss_earth_station.geom.y_global[0] = dist_y else: sys.stderr.write( "ERROR\nFSS-ES location type {} not supported".format( @@ -1018,18 +1018,18 @@ def generate_fss_earth_station( ) sys.exit(1) - fss_earth_station.z = np.array([param.height]) + fss_earth_station.geom.z_global = np.array([param.height]) if param.azimuth.upper() == "RANDOM": - fss_earth_station.azimuth = np.array( + fss_earth_station.geom.pointn_azim_global = np.array( [random_number_gen.uniform(-180., 180.)]) else: - fss_earth_station.azimuth = np.array([float(param.azimuth)]) + fss_earth_station.geom.pointn_azim_global = np.array([float(param.azimuth)]) elevation = random_number_gen.uniform( param.elevation_min, param.elevation_max, ) - fss_earth_station.elevation = np.array([elevation]) + fss_earth_station.geom.pointn_elev_global = np.array([elevation]) fss_earth_station.active = np.array([True]) fss_earth_station.tx_power = np.array( @@ -1099,10 +1099,10 @@ def generate_single_earth_station( match param.geometry.location.type: case "FIXED": - single_earth_station.x = np.array( + single_earth_station.geom.x_global = np.array( [param.geometry.location.fixed.x], ) - single_earth_station.y = np.array( + single_earth_station.geom.y_global = np.array( [param.geometry.location.fixed.y], ) case "CELL": @@ -1110,15 +1110,15 @@ def generate_single_earth_station( 1, topology, random_number_gen, param.geometry.location.cell.min_dist_to_bs, True, ) - single_earth_station.x = np.array(x) - single_earth_station.y = np.array(y) + single_earth_station.geom.x_global = np.array(x) + single_earth_station.geom.y_global = np.array(y) case "NETWORK": x, y, _, _ = StationFactory.get_random_position( 1, topology, random_number_gen, param.geometry.location.network.min_dist_to_bs, False, ) - single_earth_station.x = np.array(x) - single_earth_station.y = np.array(y) + single_earth_station.geom.x_global = np.array(x) + single_earth_station.geom.y_global = np.array(y) case "UNIFORM_DIST": # ES is randomly (uniform) created inside a circle of radius # equal to param.max_dist_to_bs @@ -1140,8 +1140,8 @@ def generate_single_earth_station( if (radius > param.geometry.location.uniform_dist.min_dist_to_bs) & ( radius < param.geometry.location.uniform_dist.max_dist_to_bs): break - single_earth_station.x[0] = dist_x - single_earth_station.y[0] = dist_y + single_earth_station.geom.x_global[0] = dist_x + single_earth_station.geom.y_global[0] = dist_y case _: sys.stderr.write( "ERROR\nSingle-ES location type {} not supported".format( @@ -1149,7 +1149,7 @@ def generate_single_earth_station( ) sys.exit(1) - single_earth_station.z = np.array([param.geometry.height]) + single_earth_station.geom.z_global = np.array([param.geometry.height]) if param.geometry.azimuth.type == "UNIFORM_DIST": if param.geometry.azimuth.uniform_dist.min < -180: @@ -1165,30 +1165,32 @@ def generate_single_earth_station( ), ) sys.exit(1) - single_earth_station.azimuth = np.array([ + single_earth_station.geom.pointn_azim_global = np.array([ random_number_gen.uniform( param.geometry.azimuth.uniform_dist.min, param.geometry.azimuth.uniform_dist.max, ), ]) else: - single_earth_station.azimuth = np.array( + single_earth_station.geom.pointn_azim_global = np.array( [param.geometry.azimuth.fixed], ) if param.geometry.elevation.type == "UNIFORM_DIST": - single_earth_station.elevation = np.array([ + single_earth_station.geom.pointn_elev_global = np.array([ random_number_gen.uniform( param.geometry.elevation.uniform_dist.min, param.geometry.elevation.uniform_dist.max, ), ]) else: - single_earth_station.elevation = np.array( + single_earth_station.geom.pointn_elev_global = np.array( [param.geometry.elevation.fixed], ) single_earth_station.antenna = np.array([ AntennaFactory.create_antenna( - param.antenna, single_earth_station.azimuth, single_earth_station.elevation + param.antenna, + single_earth_station.geom.pointn_azim_global, + single_earth_station.geom.pointn_elev_global ) ]) @@ -1250,12 +1252,12 @@ def generate_fs_station(param: ParametersFs): fs_station = StationManager(1) fs_station.station_type = StationType.FS - fs_station.x = np.array([param.x]) - fs_station.y = np.array([param.y]) - fs_station.z = np.array([param.height]) + fs_station.geom.x_global = np.array([param.x]) + fs_station.geom.y_global = np.array([param.y]) + fs_station.geom.z_global = np.array([param.height]) - fs_station.azimuth = np.array([param.azimuth]) - fs_station.elevation = np.array([param.elevation]) + fs_station.geom.pointn_azim_global = np.array([param.azimuth]) + fs_station.geom.pointn_elev_global = np.array([param.elevation]) fs_station.active = np.array([True]) fs_station.tx_power = np.array( @@ -1309,13 +1311,13 @@ def generate_haps( # h = (d/3)*math.sqrt(3)/2 # haps.x = np.array([0, 7*d/2, -d/2, -4*d, -7*d/2, d/2, 4*d]) # haps.y = np.array([0, 9*h, 15*h, 6*h, -9*h, -15*h, -6*h]) - haps.x = np.array([0]) - haps.y = np.array([0]) - haps.z = param.altitude * np.ones(num_haps) + haps.geom.x_global = np.array([0]) + haps.geom.y_global = np.array([0]) + haps.geom.z_global = param.altitude * np.ones(num_haps) elev_max = 68.19 # corresponds to 50 km radius and 20 km altitude - haps.azimuth = 360 * random_number_gen.random_sample(num_haps) - haps.elevation = ((270 + elev_max) - (270 - elev_max)) * \ + haps.geom.pointn_azim_global = 360 * random_number_gen.random_sample(num_haps) + haps.geom.pointn_elev_global = ((270 + elev_max) - (270 - elev_max)) * \ random_number_gen.random_sample(num_haps) + (270 - elev_max) haps.active = np.ones(num_haps, dtype=bool) @@ -1362,17 +1364,17 @@ def generate_rns( rns.station_type = StationType.RNS rns.is_space_station = True - rns.x = np.array([param.x]) - rns.y = np.array([param.y]) - rns.z = np.array([param.altitude]) + rns.geom.x_global = np.array([param.x]) + rns.geom.y_global = np.array([param.y]) + rns.geom.z_global = np.array([param.altitude]) # minimum and maximum values for azimuth and elevation azimuth = np.array([-30, 30]) elevation = np.array([-30, 5]) - rns.azimuth = 90 + (azimuth[1] - azimuth[0]) * \ + rns.geom.pointn_azim_global = 90 + (azimuth[1] - azimuth[0]) * \ random_number_gen.random_sample(num_rns) + azimuth[0] - rns.elevation = (elevation[1] - elevation[0]) * \ + rns.geom.pointn_elev_global = (elevation[1] - elevation[0]) * \ random_number_gen.random_sample(num_rns) + elevation[0] rns.active = np.ones(num_rns, dtype=bool) @@ -1381,7 +1383,7 @@ def generate_rns( rns.antenna = np.array([AntennaOmni(param.antenna_gain)]) elif param.antenna_pattern == "ITU-R M.1466": rns.antenna = np.array( - [AntennaM1466(param.antenna_gain, rns.azimuth, rns.elevation)], + [AntennaM1466(param.antenna_gain, rns.geom.pointn_azim_global, rns.geom.pointn_elev_global)], ) else: sys.stderr.write( @@ -1479,18 +1481,18 @@ def generate_space_station( # Elevation at ground (centre of the footprint) theta_grd_elev = 90 - incidence_angle - space_station.x = np.array([0]) - space_station.y = np.array( + space_station.geom.x_global = np.array([0]) + space_station.geom.y_global = np.array( [distance * math.cos(math.radians(theta_grd_elev))], ) - space_station.z = np.array( + space_station.geom.z_global = np.array( [distance * math.sin(math.radians(theta_grd_elev))], ) # Elevation and azimuth at sensor wrt centre of the footprint # It is assumed the sensor is at y-axis, hence azimuth is 270 deg - space_station.azimuth = np.array([270]) - space_station.elevation = np.array([-theta_grd_elev]) + space_station.geom.pointn_azim_global = np.array([270]) + space_station.geom.pointn_elev_global = np.array([-theta_grd_elev]) space_station.active = np.array([True]) space_station.rx_interference = np.array([-500]) @@ -1549,12 +1551,12 @@ def generate_mss_ss(param_mss: ParametersMssSs): num_bs = ntn_topology.num_base_stations mss_ss = StationManager(n=num_bs) mss_ss.station_type = StationType.MSS_SS - mss_ss.x = ntn_topology.space_station_x * np.ones(num_bs) + param_mss.x - mss_ss.y = ntn_topology.space_station_y * np.ones(num_bs) + param_mss.y - mss_ss.z = ntn_topology.space_station_z * np.ones(num_bs) - mss_ss.elevation = ntn_topology.elevation + mss_ss.geom.x_global = ntn_topology.space_station_x * np.ones(num_bs) + param_mss.x + mss_ss.geom.y_global = ntn_topology.space_station_y * np.ones(num_bs) + param_mss.y + mss_ss.geom.z_global = ntn_topology.space_station_z * np.ones(num_bs) + mss_ss.geom.pointn_elev_global = ntn_topology.elevation mss_ss.is_space_station = True - mss_ss.azimuth = ntn_topology.azimuth + mss_ss.geom.pointn_azim_global = ntn_topology.azimuth mss_ss.active = np.ones(num_bs, dtype=int) mss_ss.tx_power = np.ones( num_bs, dtype=int) * param_mss.tx_power_density + 10 * np.log10( @@ -1668,11 +1670,11 @@ def generate_mss_d2d( ) # Configure satellite positions in the StationManager - mss_d2d.x = mss_d2d_values["sat_x"] - mss_d2d.y = mss_d2d_values["sat_y"] - mss_d2d.z = mss_d2d_values["sat_z"] - mss_d2d.elevation = mss_d2d_values["sat_antenna_elev"] - mss_d2d.azimuth = mss_d2d_values["sat_antenna_azim"] + mss_d2d.geom.x_global = mss_d2d_values["sat_x"] + mss_d2d.geom.y_global = mss_d2d_values["sat_y"] + mss_d2d.geom.z_global = mss_d2d_values["sat_z"] + mss_d2d.geom.pointn_elev_global = mss_d2d_values["sat_antenna_elev"] + mss_d2d.geom.pointn_azim_global = mss_d2d_values["sat_antenna_azim"] mss_d2d.active = np.zeros(total_satellites, dtype=bool) @@ -1920,9 +1922,9 @@ def get_random_position(num_stas: int, fig.add_trace( go.Scatter3d( - x=imt_ue.x, - y=imt_ue.y, - z=imt_ue.z, + x=imt_ue.geom.x_global, + y=imt_ue.geom.y_global, + z=imt_ue.geom.z_global, mode='markers', marker=dict(size=1, color='red', opacity=1), showlegend=False @@ -1931,9 +1933,9 @@ def get_random_position(num_stas: int, # TODO: replace this with generate imt mss dc station st = StationManager(topology.num_base_stations) - st.x = topology.space_station_x - st.y = topology.space_station_y - st.z = topology.space_station_z + st.geom.x_global = topology.space_station_x + st.geom.y_global = topology.space_station_y + st.geom.z_global = topology.space_station_z fig.add_trace( go.Scatter3d( @@ -1946,18 +1948,18 @@ def get_random_position(num_stas: int, ) ) - from sharc.support.sharc_geom import polar_to_cartesian + from sharc.support.sharc__globalm import polar_to_cartesian # Plot beam boresight vectors boresight_length = 100 * 1e3 # Length of the boresight vectors for visualization boresight_x, boresight_y, boresight_z = polar_to_cartesian( boresight_length, - imt_ue.azimuth, - imt_ue.elevation + imt_ue.geom.pointn_azim_global, + imt_ue.geom.pointn_elev_global ) # Add arrow heads to the end of the boresight vectors - for x, y, z, bx, by, bz in zip(imt_ue.x, - imt_ue.y, - imt_ue.z, + for x, y, z, bx, by, bz in zip(imt_ue.geom.x_global, + imt_ue.geom.y_global, + imt_ue.geom.z_global, boresight_x, boresight_y, boresight_z): @@ -1973,9 +1975,9 @@ def get_random_position(num_stas: int, sizeref=2 * boresight_length / 5, showscale=False )) - for x, y, z, bx, by, bz in zip(imt_ue.x, - imt_ue.y, - imt_ue.z, + for x, y, z, bx, by, bz in zip(imt_ue.geom.x_global, + imt_ue.geom.y_global, + imt_ue.geom.z_global, boresight_x, boresight_y, boresight_z): @@ -1993,9 +1995,9 @@ def get_random_position(num_stas: int, # Maintain axis proportions fig.update_layout(scene_aspectmode='data') - ref_x = imt_ue.x[11] - ref_y = imt_ue.y[11] - ref_z = imt_ue.z[11] + ref_x = imt_ue.geom.x_global[11] + ref_y = imt_ue.geom.y_global[11] + ref_z = imt_ue.geom.z_global[11] range_scale = 1000 range_scale = 5000 diff --git a/sharc/station_manager.py b/sharc/station_manager.py index e585829b4..191de0a45 100644 --- a/sharc/station_manager.py +++ b/sharc/station_manager.py @@ -11,6 +11,7 @@ from sharc.station import Station from sharc.antenna.antenna import Antenna from sharc.mask.spectral_mask import SpectralMask +from sharc.support.geometry import SimulatorGeometry class StationManager(object): @@ -22,12 +23,6 @@ class StationManager(object): def __init__(self, n): self.num_stations = n - self.x = np.empty(n) # x in global coordinates - self.y = np.empty(n) # y in global coordinates - self.z = np.empty(n) # z in global coordinates - # antenna azim and elev angles in global coordinates - self.azimuth = np.empty(n) - self.elevation = np.empty(n) self.idx_orbit = np.empty(n) self.indoor = np.zeros(n, dtype=bool) self.active = np.ones(n, dtype=bool) @@ -53,8 +48,7 @@ def __init__(self, n): self.center_freq = np.empty(n) self.station_type = StationType.NONE self.is_space_station = False - self.intersite_dist = 0.0 - self.uses_local_coords = False + self.geom = SimulatorGeometry(n) def get_station_list(self, id=None) -> list: """Return a list of Station objects for the given indices. @@ -91,11 +85,11 @@ def get_station(self, id) -> Station: """ station = Station() station.id = id - station.x = self.x[id] - station.y = self.y[id] - station.z = self.z[id] - station.azimuth = self.azimuth[id] - station.elevation = self.elevation[id] + station.x = self.geom.x_global[id] + station.y = self.geom.y_global[id] + station.z = self.geom.z_global[id] + station.azimuth = self.geom.pointn_azim_global[id] + station.elevation = self.geom.pointn_elev_global[id] station.indoor = self.indoor[id] station.active = self.active[id] station.tx_power = self.tx_power[id] @@ -115,262 +109,6 @@ def get_station(self, id) -> Station: station.station_type = self.station_type return station - def get_local_distance_to(self, station) -> np.array: - """Calculate the 2D distance between this manager's stations and another's - considering this ones coordinate system - - Parameters - ---------- - station : StationManager - StationManager to which the distance is calculated. - - Returns - ------- - np.array - 2D distance matrix between stations. - """ - if not self.uses_local_coords: - return self.get_global_distance_to(station) - # TODO: 2d distance calculation - raise NotImplementedError() - - def get_global_distance_to(self, station) -> np.array: - """Calculate the 2D distance between this manager's stations and another's - considering their global (x,y) - - Parameters - ---------- - station : StationManager - StationManager to which the distance is calculated. - - Returns - ------- - np.array - 2D distance matrix between stations. - """ - distance = np.empty([self.num_stations, station.num_stations]) - for i in range(self.num_stations): - distance[i] = np.sqrt( - np.power(self.x[i] - station.x, 2) + - np.power(self.y[i] - station.y, 2), - ) - return distance - - def get_3d_distance_to(self, station) -> np.array: - """Calculate the 3D distance between this manager's stations and another's. - - Parameters - ---------- - station : StationManager - StationManager to which the distance is calculated. - - Returns - ------- - np.array - 3D distance matrix between stations. - """ - dx = np.subtract.outer(self.x, station.x).astype(np.float64) - dy = np.subtract.outer(self.y, station.y).astype(np.float64) - dz = np.subtract.outer(self.z, station.z).astype(np.float64) - np.square(dx, out=dx) - np.square(dy, out=dy) - np.square(dz, out=dz) - np.sqrt( - dx + dy + dz, - out=dx - ) - return dx - - def get_global_dist_angles_wrap_around(self, station) -> np.array: - """Calculate distances and angles using the wrap-around technique. - - Parameters - ---------- - station : StationManager - StationManager to which distances and angles are calculated. - - Returns - ------- - tuple - distance_2D (np.array): 2D distance between stations - distance_3D (np.array): 3D distance between stations - phi (np.array): azimuth of pointing vector to other stations - theta (np.array): elevation of pointing vector to other stations - """ - # Initialize variables - distance_3D = np.empty([self.num_stations, station.num_stations]) - distance_2D = np.inf * np.ones_like(distance_3D) - cluster_num = np.zeros_like(distance_3D, dtype=int) - - # Cluster coordinates - cluster_x = np.array([ - station.x, - station.x + 3.5 * self.intersite_dist, - station.x - 0.5 * self.intersite_dist, - station.x - 4.0 * self.intersite_dist, - station.x - 3.5 * self.intersite_dist, - station.x + 0.5 * self.intersite_dist, - station.x + 4.0 * self.intersite_dist, - ]) - - cluster_y = np.array([ - station.y, - station.y + 1.5 * - np.sqrt(3.0) * self.intersite_dist, - station.y + 2.5 * - np.sqrt(3.0) * self.intersite_dist, - station.y + 1.0 * - np.sqrt(3.0) * self.intersite_dist, - station.y - 1.5 * - np.sqrt(3.0) * self.intersite_dist, - station.y - 2.5 * - np.sqrt(3.0) * self.intersite_dist, - station.y - 1.0 * np.sqrt(3.0) * self.intersite_dist, - ]) - - # Calculate 2D distance - temp_distance = np.zeros_like(distance_2D) - for k, (x, y) in enumerate(zip(cluster_x, cluster_y)): - temp_distance = np.sqrt( - np.power(x - self.x[:, np.newaxis], 2) + - np.power(y - self.y[:, np.newaxis], 2), - ) - is_shorter = temp_distance < distance_2D - distance_2D[is_shorter] = temp_distance[is_shorter] - cluster_num[is_shorter] = k - - # Calculate 3D distance - distance_3D = np.sqrt( - np.power(distance_2D, 2) + - np.power(station.z - self.z[:, np.newaxis], 2), - ) - - # Calcualte pointing vector - point_vec_x = cluster_x[cluster_num, np.arange(station.num_stations)] \ - - self.x[:, np.newaxis] - point_vec_y = cluster_y[cluster_num, np.arange(station.num_stations)] \ - - self.y[:, np.newaxis] - point_vec_z = station.z - self.z[:, np.newaxis] - - phi = np.array( - np.rad2deg( - np.arctan2( - point_vec_y, point_vec_x, - ), - ), ndmin=2, - ) - theta = np.rad2deg(np.arccos(point_vec_z / distance_3D)) - - return distance_2D, distance_3D, phi, theta - - def get_global_elevation(self, station) -> np.array: - """Calculate the elevation angle between this manager's stations and another's. - - Parameters - ---------- - station : StationManager - StationManager to which the elevation angle is calculated. - - Returns - ------- - np.array - Elevation angle matrix (degrees). - """ - elevation = np.empty([self.num_stations, station.num_stations]) - - for i in range(self.num_stations): - distance = np.sqrt( - np.power(self.x[i] - station.x, 2) + - np.power(self.y[i] - station.y, 2), - ) - rel_z = station.z - self.z[i] - elevation[i] = np.degrees(np.arctan2(rel_z, distance)) - - return elevation - - def get_local_elevation(self, station) -> np.array: - """Calculate the elevation angle between this manager's stations and another's - considering this one's loca coordinate system - - Parameters - ---------- - station : StationManager - StationManager to which the elevation angle is calculated. - - Returns - ------- - np.array - Elevation angle matrix (degrees). - """ - if not self.uses_local_coords: - return self.get_global_elevation(station) - - raise NotImplementedError() - - def get_global_pointing_vector_to(self, station) -> tuple: - """Calculate the pointing vector (angles) with respect to another station. - - Parameters - ---------- - station : StationManager - The other StationManager to calculate the pointing vector to. - - Returns - ------- - tuple - phi, theta (phi is calculated with respect to x counter-clockwise and - theta is calculated with respect to z counter-clockwise). - """ - - # malloc - dx = (station.x - self.x[:, np.newaxis]).astype(np.float64) - dy = (station.y - self.y[:, np.newaxis]).astype(np.float64) - dz = (station.z - self.z[:, np.newaxis]).astype(np.float64) - - dist = self.get_3d_distance_to(station) - - # NOTE: doing in place calculations - phi = np.rad2deg(np.arctan2(dy, dx, out=dx), out=dx) - # delete reference dx - del dx - - # in place calculations - theta = np.rad2deg(np.arccos(np.clip(dz / dist, -1.0, 1.0, out=dz), out=dz), out=dz) - # delete reference dz - del dz - - return phi, theta - - def get_off_axis_angle(self, station) -> np.array: - """Calculate the off-axis angle between this manager's stations and another's. - - Parameters - ---------- - station : StationManager - The other StationManager to calculate the off-axis angle to. - - Returns - ------- - np.array - Off-axis angle matrix (degrees). - """ - Az, b = self.get_global_pointing_vector_to(station) - Az0 = self.azimuth - - a = 90 - self.elevation[:, np.newaxis] - C = Az0[:, np.newaxis] - Az - - cos_phi = np.cos(np.radians(a)) * np.cos(np.radians(b)) \ - + np.sin(np.radians(a)) * np.sin(np.radians(b)) * np.cos(np.radians(C)) - phi = np.arccos( - # imprecision may accumulate enough for numbers to be slightly out - # of arccos range - np.clip(cos_phi, -1., 1.) - ) - phi_deg = np.degrees(phi) - - return phi_deg - def is_imt_station(self) -> bool: """Return whether this station manager represents IMT stations. @@ -426,5 +164,5 @@ def copy_active_stations(stations: StationManager) -> StationManager: act_sta.center_freq[idx] = stations.center_freq[active_idx] act_sta.station_type = stations.station_type act_sta.is_space_station = stations.is_space_station - act_sta.intersite_dist = stations.intersite_dist + act_sta.intersite_dist = stations.geom.intersite_dist return act_sta diff --git a/sharc/support/geometry.py b/sharc/support/geometry.py new file mode 100644 index 000000000..b6e74051a --- /dev/null +++ b/sharc/support/geometry.py @@ -0,0 +1,483 @@ +# from sharc.support.sharc_geom import CoordinateSystem +# from sharc.satellite.utils.sat_utils import lla2ecef + +import numpy as np +from abc import ABC + + +def readonly_properties(*fields): + """ + Decorator to update 'field's to be readonly, + and creates the private '_field's for mutations + """ + def decorator(cls): + for field in fields: + private_name = f"_{field}" + + def getter(self, name=private_name): + return getattr(self, name) + setattr(cls, field, property(getter)) + return cls + return decorator + + +# TODO: make these properties readonly +# @readonly_properties( +# "x_global", "y_global", "z_global", +# "pointn_azim_global", "pointn_elev_global", +# "num_geometries" +# ) +class GlobalGeometry(ABC): + """ + Abstract class defining global simulator geometry implementation. + """ + x_global: np.ndarray + y_global: np.ndarray + z_global: np.ndarray + pointn_azim_global: np.ndarray + pointn_elev_global: np.ndarray + + num_geometries: int + + # TODO: remove this from here + # gambiarra_intersite_dist: float + intersite_dist: float + + def setup( + self, + num_geometries, + ): + """ + Initializes variables based on number of geometries + """ + self.x_global = np.empty(num_geometries) + self.y_global = np.empty(num_geometries) + self.z_global = np.empty(num_geometries) + self.pointn_azim_global = np.empty(num_geometries) + self.pointn_elev_global = np.empty(num_geometries) + + self.num_geometries = num_geometries + + def set_global_coords( + self, + x=None, + y=None, + z=None, + azim=None, + elev=None, + ): + """Set passed values to objects global coordinates. + If None is passed, attribute will not be changed. + """ + if x is not None: + self.x_global = x + if y is not None: + self.y_global = y + if z is not None: + self.z_global = z + if elev is not None: + self.pointn_elev_global = elev + if azim is not None: + self.pointn_azim_global = azim + + def get_global_distance_to(self, other: "GlobalGeometry") -> np.array: + """Calculate the 2D distance between this geometry and another + considering their global (x,y) + + Parameters + ---------- + other : GlobalGeometry + GlobalGeometry to which the distance is calculated. + + Returns + ------- + np.array + 2D distance matrix between others. + """ + distance = np.empty([self.num_geometries, other.num_geometries]) + for i in range(self.num_geometries): + distance[i] = np.sqrt( + np.power(self.x_global[i] - other.x_global, 2) + + np.power(self.y_global[i] - other.y_global, 2), + ) + return distance + + def get_3d_distance_to(self, other: "GlobalGeometry") -> np.array: + """Calculate the 3D distance between this manager's stations and another's. + + Parameters + ---------- + other : GlobalGeometry + GlobalGeometry to which the distance is calculated. + + Returns + ------- + np.array + 3D distance matrix between stations. + """ + dx = np.subtract.outer(self.x_global, other.x_global).astype(np.float64) + dy = np.subtract.outer(self.y_global, other.y_global).astype(np.float64) + dz = np.subtract.outer(self.z_global, other.z_global).astype(np.float64) + np.square(dx, out=dx) + np.square(dy, out=dy) + np.square(dz, out=dz) + np.sqrt( + dx + dy + dz, + out=dx + ) + return dx + + def get_global_dist_angles_wrap_around(self, other) -> np.array: + """Calculate distances and angles using the wrap-around technique. + + Parameters + ---------- + other : GlobalGeometry + GlobalGeometry to which distances and angles are calculated. + + Returns + ------- + tuple + distance_2D (np.array): 2D distance between stations + distance_3D (np.array): 3D distance between stations + phi (np.array): azimuth of pointing vector to other stations + theta (np.array): elevation of pointing vector to other stations + """ + if self._num_of_local_refs != 0 or other._num_of_local_refs != 0: + raise ValueError("Wrap around was not implemented for local coord sys") + # Initialize variables + distance_3D = np.empty([self.num_geometries, other.num_geometries]) + distance_2D = np.inf * np.ones_like(distance_3D) + cluster_num = np.zeros_like(distance_3D, dtype=int) + + # Cluster coordinates + cluster_x = np.array([ + other.x_global, + other.x_global + 3.5 * self.intersite_dist, + other.x_global - 0.5 * self.intersite_dist, + other.x_global - 4.0 * self.intersite_dist, + other.x_global - 3.5 * self.intersite_dist, + other.x_global + 0.5 * self.intersite_dist, + other.x_global + 4.0 * self.intersite_dist, + ]) + + cluster_y = np.array([ + other.y_global, + other.y_global + 1.5 * + np.sqrt(3.0) * self.intersite_dist, + other.y_global + 2.5 * + np.sqrt(3.0) * self.intersite_dist, + other.y_global + 1.0 * + np.sqrt(3.0) * self.intersite_dist, + other.y_global - 1.5 * + np.sqrt(3.0) * self.intersite_dist, + other.y_global - 2.5 * + np.sqrt(3.0) * self.intersite_dist, + other.y_global - 1.0 * np.sqrt(3.0) * self.intersite_dist, + ]) + + # Calculate 2D distance + temp_distance = np.zeros_like(distance_2D) + for k, (x, y) in enumerate(zip(cluster_x, cluster_y)): + temp_distance = np.sqrt( + np.power(x - self.x_global[:, np.newaxis], 2) + + np.power(y - self.y_global[:, np.newaxis], 2), + ) + is_shorter = temp_distance < distance_2D + distance_2D[is_shorter] = temp_distance[is_shorter] + cluster_num[is_shorter] = k + + # Calculate 3D distance + distance_3D = np.sqrt( + np.power(distance_2D, 2) + + np.power(other.z_global - self.z_global[:, np.newaxis], 2), + ) + + # Calcualte pointing vector + point_vec_x = cluster_x[cluster_num, np.arange(other.num_geometries)] \ + - self.x_global[:, np.newaxis] + point_vec_y = cluster_y[cluster_num, np.arange(other.num_geometries)] \ + - self.y_global[:, np.newaxis] + point_vec_z = other.z_global - self.z_global[:, np.newaxis] + + phi = np.array( + np.rad2deg( + np.arctan2( + point_vec_y, point_vec_x, + ), + ), ndmin=2, + ) + theta = np.rad2deg(np.arccos(point_vec_z / distance_3D)) + + return distance_2D, distance_3D, phi, theta + + def get_global_elevation(self, other: "GlobalGeometry") -> np.array: + """Calculate the elevation angle between this manager's stations and another's. + + Parameters + ---------- + other : GlobalGeometry + GlobalGeometry to which the elevation angle is calculated. + + Returns + ------- + np.array + Elevation angle matrix (degrees). + """ + elevation = np.empty([self.num_geometries, other.num_geometries]) + + for i in range(self.num_geometries): + distance = np.sqrt( + np.power(self.x_global[i] - other.x_global, 2) + + np.power(self.y_global[i] - other.y_global, 2), + ) + rel_z = other.z_global - self.z_global[i] + elevation[i] = np.degrees(np.arctan2(rel_z, distance)) + + return elevation + + def get_global_pointing_vector_to(self, other: "GlobalGeometry") -> tuple: + """Calculate the pointing vector (angles) with respect to another other. + + Parameters + ---------- + other : GlobalGeometry + The other GlobalGeometry to calculate the pointing vector to. + + Returns + ------- + tuple + phi, theta (phi is calculated with respect to x counter-clockwise and + theta is calculated with respect to z counter-clockwise). + """ + + # malloc + dx = (other.x_global - self.x_global[:, np.newaxis]).astype(np.float64) + dy = (other.y_global - self.y_global[:, np.newaxis]).astype(np.float64) + dz = (other.z_global - self.z_global[:, np.newaxis]).astype(np.float64) + + dist = self.get_3d_distance_to(other) + + # NOTE: doing in place calculations + phi = np.rad2deg(np.arctan2(dy, dx, out=dx), out=dx) + # delete reference dx + del dx + + # in place calculations + theta = np.rad2deg(np.arccos(np.clip(dz / dist, -1.0, 1.0, out=dz), out=dz), out=dz) + # delete reference dz + del dz + + return phi, theta + + def get_off_axis_angle(self, other: "GlobalGeometry") -> np.array: + """Calculate the off-axis angle between this manager's stations and another's. + + Parameters + ---------- + other : GlobalGeometry + The other GlobalGeometry to calculate the off-axis angle to. + + Returns + ------- + np.array + Off-axis angle matrix (degrees). + """ + Az, b = self.get_global_pointing_vector_to(other) + Az0 = self.pointn_azim_global + + a = 90 - self.pointn_elev_global[:, np.newaxis] + C = Az0[:, np.newaxis] - Az + + cos_phi = np.cos(np.radians(a)) * np.cos(np.radians(b)) \ + + np.sin(np.radians(a)) * np.sin(np.radians(b)) * np.cos(np.radians(C)) + phi = np.arccos( + # imprecision may accumulate enough for numbers to be slightly out + # of arccos range + np.clip(cos_phi, -1., 1.) + ) + phi_deg = np.degrees(phi) + + return phi_deg + + +@readonly_properties( + "x_local", "y_local", "z_local", + "pointn_azim_local", "pointn_elev_local", + "global_lla_reference" +) +class SimulatorGeometry(GlobalGeometry): + """ + Class with simplified coordinate system operations. + Just global and local conversion. + """ + # N = num of geometries + # M = num of local references + # M < N + x_local: np.ndarray # (M,) + y_local: np.ndarray # (M,) + z_local: np.ndarray # (M,) + pointn_azim_local: np.ndarray # (M,) + pointn_elev_local: np.ndarray # (M,) + + __local_lla_references: np.ndarray[np.ndarray[float]] # (3, M) + global_lla_reference: tuple[float, float, float] + + _num_of_local_refs: int # M + _geometry_reference_i: np.ndarray[int] # (N,) + + def __init__( + self, + num_geometries, + num_of_local_refs=0, + global_cs: tuple[float, float, float] = None, + ): + """ + Initialize a geometry object with a global coordinate system + and defining how many local coordinate systems should exist + """ + # super().__init__(num_geometries) + self.setup( + num_geometries, + num_of_local_refs, + global_cs, + ) + + def setup( + self, + num_geometries, + num_of_local_refs=0, + global_cs: tuple[float, float, float] = None, + ): + """ + Initializes variables based on number of geometries considered + """ + super().setup(num_geometries) + + self.__global_coord_sys = global_cs + self._num_of_local_refs = num_of_local_refs + + if num_of_local_refs == 0: + self.uses_local_coords = False + self._x_local = None + self._y_local = None + self._z_local = None + self._pointn_azim_local = None + self._pointn_elev_local = None + return + elif global_cs is None: + raise ValueError( + "If there will be a local ref, global coord sys must be passed" + ) + + self._x_local = np.empty(num_geometries) + self._y_local = np.empty(num_geometries) + self._z_local = np.empty(num_geometries) + self._pointn_azim_local = np.empty(num_geometries) + self._pointn_elev_local = np.empty(num_geometries) + + def set_local_coord_sys( + self, + ref_lats, + ref_lons, + ref_alts, + ): + """ + + """ + for r in [ref_lats, ref_lons, ref_alts]: + if len(r) != self._num_of_local_refs: + raise ValueError( + "Incongruent number of coordinate systems. " + f"Passed {len(r)} but should have passed {len(self._num_of_local_refs)}" + ) + self.__local_lla_references = np.stack((ref_lats, ref_lons, ref_alts)) + + def set_local_coords( + self, + x=None, + y=None, + z=None, + azim=None, + elev=None, + ): + """Set values to local coordinate values. + If None is passed, attribute will not be updated. + """ + if x is not None: + self.x_local = x + if y is not None: + self.y_local = y + if z is not None: + self.z_local = z + if elev is not None: + self.pointn_elev_local = elev + if azim is not None: + self.pointn_azim_local = azim + + # def _compute_local_to_global_transf(self): + # return + # local_lat, local_lon, local_alt = self.__local_lla_references + # rotation_around_z = -local_lon - 90 + # rotation_around_x = local_lat - 90 + + # self.rotation = scipy.spatial.transform.Rotation.from_euler( + # 'zx', + # np.stack([rotation_around_z, rotation_around_x], axis=-1), + # degrees=True + # ) + # # (M,3,3) or (3,3) + # rot_mtx = self.rotation.as_matrix() + # if rot_mtx.ndim == 2: + # # guarantee (M, 3, 3) + # rot_mtx = rot_mtx[None, ...] + # # broadcastable (M, 1, 3, 3) + # self.rotation_mtx = rot_mtx[:, None, :, :] + + # inv_rot_mtx = self.rotation.inv().as_matrix() + # if inv_rot_mtx.ndim == 2: + # # guarantee (M, 3, 3) + # inv_rot_mtx = inv_rot_mtx[None, ...] + # # broadcastable (M, 1, 3, 3) + # self.inv_rotation_mtx = inv_rot_mtx[:, None, :, :] + + def get_local_distance_to(self, other: "SimulatorGeometry") -> np.array: + """Calculate the 2D distance between this manager's stations and another's + considering this ones coordinate system + + Parameters + ---------- + station : StationManager + StationManager to which the distance is calculated. + + Returns + ------- + np.array + 2D distance matrix between stations. + """ + if not self.uses_local_coords: + return self.get_global_distance_to(other) + # TODO: 2d distance calculation + raise NotImplementedError() + + def get_local_elevation(self, other: "SimulatorGeometry") -> np.array: + """Calculate the elevation angle between this manager's stations and another's + considering this one's loca coordinate system + + Parameters + ---------- + other : GlobalAndLocalGeometry + GlobalAndLocalGeometry to which the elevation angle is calculated. + + Returns + ------- + np.array + Elevation angle matrix (degrees). + """ + if not self.uses_local_coords: + return self.get_global_elevation(other) + + raise NotImplementedError() + + +# \.(get_global_distance_to|get_3d_distance_to|get_global_dist_angles_wrap_around|get_global_elevation|get_global_pointing_vector_to|get_off_axis_angle) diff --git a/sharc/support/sharc_geom.py b/sharc/support/sharc_geom.py index 77e5b3fbf..1863438d6 100644 --- a/sharc/support/sharc_geom.py +++ b/sharc/support/sharc_geom.py @@ -6,7 +6,6 @@ import typing from sharc.satellite.utils.sat_utils import lla2ecef -from sharc.station_manager import StationManager from sharc.support.sharc_utils import to_scalar from sharc.satellite.ngso.constants import EARTH_RADIUS_M, EARTH_DEFAULT_CRS, EARTH_SPHERICAL_CRS @@ -279,6 +278,10 @@ def set_reference(self, ref_lat: float, ref_long: float, ref_alt: float): # can also be confirmed comparing to here: # https://gssc.esa.int/navipedia/index.php/Transformations_between_ECEF_and_ENU_coordinates + # # pre calculating rotation matrices + # self.rotation_mtx = self.rotation.as_matrix() + # self.inv_rotation_mtx = self.rotation.inv().as_matrix() + def ecef2enu( self, x, y, z, *, translate=None ): @@ -376,36 +379,12 @@ def lla2enu( return self.ecef2enu(x, y, z) - def station_ecef2enu( - self, station: StationManager, idx=None - ) -> None: - """In-place rotate and translate all coordinates so that reference parameters end up in (0,0,0). - - Stations end up in the same relative position according to each other, adapting their angles to the rotation. - If idx is specified, only stations[idx] will be converted. - - Parameters - ---------- - station : StationManager - The station manager whose stations will be transformed. - idx : array-like or None, optional - Indices of stations to convert (default: all). + def angle_ecef2enu( + self, azim: np.ndarray, elev: np.ndarray + ) -> tuple[np.ndarray, np.ndarray]: + """ + Receives pointing angles in ecef coordinates and transforms to ENU """ - # transform positions - if idx is None: - nx, ny, nz = self.ecef2enu( - station.x, station.y, station.z) - else: - nx, ny, nz = self.ecef2enu( - station.x[idx], station.y[idx], station.z[idx]) - - if idx is None: - azim = station.azimuth - elev = station.elevation - else: - azim = station.azimuth[idx] - elev = station.elevation[idx] - r = 1 # then get pointing vec pointing_vec_x, pointing_vec_y, pointing_vec_z = polar_to_cartesian( @@ -416,54 +395,17 @@ def station_ecef2enu( pointing_vec_x, pointing_vec_y, pointing_vec_z = self.ecef2enu( pointing_vec_x, pointing_vec_y, pointing_vec_z, translate=0) - if idx is None: - station.x = nx - station.y = ny - station.z = nz - - _, station.azimuth, station.elevation = cartesian_to_polar( - pointing_vec_x, pointing_vec_y, pointing_vec_z) - else: - station.x[idx] = nx - station.y[idx] = ny - station.z[idx] = nz - - _, azimuth, elevation = cartesian_to_polar( - pointing_vec_x, pointing_vec_y, pointing_vec_z) + _, azimuth, elevation = cartesian_to_polar( + pointing_vec_x, pointing_vec_y, pointing_vec_z) - station.azimuth[idx] = azimuth - station.elevation[idx] = elevation + return azimuth, elevation - def station_enu2ecef( - self, station: StationManager, idx=None - ) -> None: - """In-place rotate and translate all coordinates so that reference parameters end up in (0,0,0). - - Stations end up in the same relative position according to each other, adapting their angles to the rotation. - If idx is specified, only stations[idx] will be converted. - - Parameters - ---------- - station : StationManager - The station manager whose stations will be transformed. - idx : array-like or None, optional - Indices of stations to convert (default: all). + def angle_enu2ecef( + self, azim: np.ndarray, elev: np.ndarray + ) -> tuple[np.ndarray, np.ndarray]: + """ + Receives pointing angles in ENU coordinates and transforms to ECEF """ - # transform positions - if idx is None: - nx, ny, nz = self.enu2ecef( - station.x, station.y, station.z) - else: - nx, ny, nz = self.enu2ecef( - station.x[idx], station.y[idx], station.z[idx]) - - if idx is None: - azim = station.azimuth - elev = station.elevation - else: - azim = station.azimuth[idx] - elev = station.elevation[idx] - r = 1 # then get pointing vec pointing_vec_x, pointing_vec_y, pointing_vec_z = polar_to_cartesian( @@ -474,23 +416,10 @@ def station_enu2ecef( pointing_vec_x, pointing_vec_y, pointing_vec_z = self.enu2ecef( pointing_vec_x, pointing_vec_y, pointing_vec_z, translate=0) - if idx is None: - station.x = nx - station.y = ny - station.z = nz - - _, station.azimuth, station.elevation = cartesian_to_polar( - pointing_vec_x, pointing_vec_y, pointing_vec_z) - else: - station.x[idx] = nx - station.y[idx] = ny - station.z[idx] = nz - - _, azimuth, elevation = cartesian_to_polar( - pointing_vec_x, pointing_vec_y, pointing_vec_z) + _, azimuth, elevation = cartesian_to_polar( + pointing_vec_x, pointing_vec_y, pointing_vec_z) - station.azimuth[idx] = azimuth - station.elevation[idx] = elevation + return azimuth, elevation def get_lambert_equal_area_crs(polygon: shp.geometry.Polygon): diff --git a/tests/e2e/test_integration_imt_victim.py b/tests/e2e/test_integration_imt_victim.py index 2f518999b..a7f70e2b1 100644 --- a/tests/e2e/test_integration_imt_victim.py +++ b/tests/e2e/test_integration_imt_victim.py @@ -278,7 +278,7 @@ def test_es_to_ue_aclr_and_acs_partial_overlap(self): ) p_loss_1k = self.fspl.get_loss( - simulation_1k.system.get_3d_distance_to(simulation_1k.ue), + simulation_1k.system.geom.get_3d_distance_to(simulation_1k.ue.geom), # TODO: maybe should change this? np.array([self.param.single_earth_station.frequency]) ) @@ -287,7 +287,7 @@ def test_es_to_ue_aclr_and_acs_partial_overlap(self): simulation_1k.results.imt_system_path_loss ) p_loss_3k = self.fspl.get_loss( - simulation_3k.system.get_3d_distance_to(simulation_3k.ue), + simulation_3k.system.geom.get_3d_distance_to(simulation_3k.ue.geom), # TODO: maybe should change this? np.array([self.param.single_earth_station.frequency]) ) @@ -298,7 +298,7 @@ def test_es_to_ue_aclr_and_acs_partial_overlap(self): g1_co_1k = np.zeros((1, 2)) g1_adj_1k = np.zeros((1, 2)) - phis, thetas = simulation_1k.ue.get_global_pointing_vector_to(simulation_1k.system) + phis, thetas = simulation_1k.ue.geom.get_global_pointing_vector_to(simulation_1k.system.geom) for i, phi, theta in zip(range(2), phis, thetas): g1_co_1k[0][i] = simulation_1k.ue.antenna[i].calculate_gain( @@ -321,7 +321,7 @@ def test_es_to_ue_aclr_and_acs_partial_overlap(self): g1_co_3k = np.zeros((1, 6)) g1_adj_3k = np.zeros((1, 6)) - phis, thetas = simulation_3k.ue.get_global_pointing_vector_to(simulation_1k.system) + phis, thetas = simulation_3k.ue.geom.get_global_pointing_vector_to(simulation_1k.system.geom) for i, phi, theta in zip(range(6), phis, thetas): g1_co_3k[0][i] = simulation_3k.ue.antenna[i].calculate_gain( @@ -523,7 +523,7 @@ def test_es_to_bs_aclr_and_acs_partial_overlap(self): ) p_loss = self.fspl.get_loss( - simulation_1k.bs.get_3d_distance_to(simulation_1k.system), + simulation_1k.bs.geom.get_3d_distance_to(simulation_1k.system.geom), # TODO: maybe should change this? np.array([self.param.single_earth_station.frequency]) ) @@ -770,7 +770,7 @@ def test_es_to_ue_mask(self): ) p_loss_1k = self.fspl.get_loss( - simulation_1k.system.get_3d_distance_to(simulation_1k.ue), + simulation_1k.system.geom.get_3d_distance_to(simulation_1k.ue.geom), # TODO: maybe should change this? np.array([self.param.single_earth_station.frequency]) ) @@ -780,7 +780,7 @@ def test_es_to_ue_mask(self): ) p_loss_3k = self.fspl.get_loss( - simulation_3k.system.get_3d_distance_to(simulation_3k.ue), + simulation_3k.system.geom.get_3d_distance_to(simulation_3k.ue.geom), # TODO: maybe should change this? np.array([self.param.single_earth_station.frequency]) ) @@ -790,7 +790,7 @@ def test_es_to_ue_mask(self): ) g1_co_1k = np.zeros((1, 2)) - phis, thetas = simulation_1k.ue.get_global_pointing_vector_to(simulation_1k.system) + phis, thetas = simulation_1k.ue.geom.get_global_pointing_vector_to(simulation_1k.system.geom) for i, phi, theta in zip(range(2), phis, thetas): g1_co_1k[0][i] = simulation_1k.ue.antenna[i].calculate_gain( @@ -806,7 +806,7 @@ def test_es_to_ue_mask(self): ) g1_co_3k = np.zeros((1, 6)) - phis, thetas = simulation_3k.ue.get_global_pointing_vector_to(simulation_1k.system) + phis, thetas = simulation_3k.ue.geom.get_global_pointing_vector_to(simulation_1k.system.geom) for i, phi, theta in zip(range(6), phis, thetas): g1_co_3k[0][i] = simulation_3k.ue.antenna[i].calculate_gain( @@ -915,7 +915,7 @@ def test_es_to_bs_mask(self): ) p_loss_1k = self.fspl.get_loss( - simulation_1k.system.get_3d_distance_to(simulation_1k.bs), + simulation_1k.system.geom.get_3d_distance_to(simulation_1k.bs.geom), # TODO: maybe should change this? np.array([self.param.single_earth_station.frequency]) ) diff --git a/tests/e2e/test_integration_sys_victim.py b/tests/e2e/test_integration_sys_victim.py index 55ccfdd75..aff046402 100644 --- a/tests/e2e/test_integration_sys_victim.py +++ b/tests/e2e/test_integration_sys_victim.py @@ -225,7 +225,7 @@ def test_2bs_to_es_mask(self): ) p_loss = self.fspl.get_loss( - simulation_1k.bs.get_3d_distance_to(simulation_1k.system), + simulation_1k.bs.geom.get_3d_distance_to(simulation_1k.system.geom), # TODO: maybe should change this? np.array([self.param.imt.frequency]) ) @@ -329,7 +329,7 @@ def test_2bs_to_es_aclr_and_acs_partial_overlap(self): ) p_loss = self.fspl.get_loss( - simulation_1k.bs.get_3d_distance_to(simulation_1k.system), + simulation_1k.bs.geom.get_3d_distance_to(simulation_1k.system.geom), # TODO: maybe should change this? np.array([self.param.imt.frequency]) ) @@ -523,7 +523,7 @@ def test_ue_to_es_mask(self): ) p_loss_1k = self.fspl.get_loss( - simulation_1k.ue.get_3d_distance_to(simulation_1k.system), + simulation_1k.ue.geom.get_3d_distance_to(simulation_1k.system.geom), # TODO: maybe should change this? np.array([self.param.imt.frequency]) ) @@ -532,7 +532,7 @@ def test_ue_to_es_mask(self): simulation_1k.results.imt_system_path_loss ) p_loss_3k = self.fspl.get_loss( - simulation_3k.ue.get_3d_distance_to(simulation_3k.system), + simulation_3k.ue.geom.get_3d_distance_to(simulation_3k.system.geom), # TODO: maybe should change this? np.array([self.param.imt.frequency]) ) @@ -640,7 +640,7 @@ def test_ue_to_es_aclr_and_acs_partial_overlap(self): ) p_loss_1k = self.fspl.get_loss( - simulation_1k.ue.get_3d_distance_to(simulation_1k.system), + simulation_1k.ue.geom.get_3d_distance_to(simulation_1k.system.geom), # TODO: maybe should change this? np.array([self.param.imt.frequency]) ) @@ -649,7 +649,7 @@ def test_ue_to_es_aclr_and_acs_partial_overlap(self): simulation_1k.results.imt_system_path_loss ) p_loss_3k = self.fspl.get_loss( - simulation_3k.ue.get_3d_distance_to(simulation_3k.system), + simulation_3k.ue.geom.get_3d_distance_to(simulation_3k.system.geom), # TODO: maybe should change this? np.array([self.param.imt.frequency]) ) @@ -660,7 +660,7 @@ def test_ue_to_es_aclr_and_acs_partial_overlap(self): g1_co_1k = np.zeros((1, 2)) g1_adj_1k = np.zeros((1, 2)) - phis, thetas = simulation_1k.ue.get_global_pointing_vector_to(simulation_1k.system) + phis, thetas = simulation_1k.ue.geom.get_global_pointing_vector_to(simulation_1k.system.geom) for i, phi, theta in zip(range(2), phis, thetas): g1_co_1k[0][i] = simulation_1k.ue.antenna[i].calculate_gain( @@ -683,7 +683,7 @@ def test_ue_to_es_aclr_and_acs_partial_overlap(self): g1_co_3k = np.zeros((1, 6)) g1_adj_3k = np.zeros((1, 6)) - phis, thetas = simulation_3k.ue.get_global_pointing_vector_to(simulation_1k.system) + phis, thetas = simulation_3k.ue.geom.get_global_pointing_vector_to(simulation_1k.system.geom) for i, phi, theta in zip(range(6), phis, thetas): g1_co_3k[0][i] = simulation_3k.ue.antenna[i].calculate_gain( diff --git a/tests/test_adjacent_channel.py b/tests/test_adjacent_channel.py index 4dda25180..016214b83 100644 --- a/tests/test_adjacent_channel.py +++ b/tests/test_adjacent_channel.py @@ -175,8 +175,8 @@ def test_simulation_2bs_4ue_downlink(self): self.simulation.topology, random_number_gen, ) - self.simulation.ue.x = np.array([20, 70, 110, 170]) - self.simulation.ue.y = np.array([0, 0, 0, 0]) + self.simulation.ue.geom.x_global = np.array([20, 70, 110, 170]) + self.simulation.ue.geom.y_global = np.array([0, 0, 0, 0]) self.simulation.ue.antenna = np.array( [AntennaOmni(10), AntennaOmni(11), AntennaOmni(22), AntennaOmni(23)], ) @@ -243,9 +243,9 @@ def test_simulation_2bs_4ue_downlink(self): self.simulation.system = StationFactory.generate_fss_space_station( self.param.fss_ss, ) - self.simulation.system.x = np.array([0.01]) # avoids zero-division - self.simulation.system.y = np.array([0]) - self.simulation.system.z = np.array([self.param.fss_ss.altitude]) + self.simulation.system.geom.x_global = np.array([0.01]) # avoids zero-division + self.simulation.system.geom.y_global = np.array([0]) + self.simulation.system.geom.z_global = np.array([self.param.fss_ss.altitude]) # test the method that calculates interference from IMT UE to FSS space # station @@ -312,8 +312,8 @@ def test_simulation_2bs_4ue_uplink(self): self.simulation.topology, random_number_gen, ) - self.simulation.ue.x = np.array([20, 70, 110, 170]) - self.simulation.ue.y = np.array([0, 0, 0, 0]) + self.simulation.ue.geom.x_global = np.array([20, 70, 110, 170]) + self.simulation.ue.geom.y_global = np.array([0, 0, 0, 0]) self.simulation.ue.antenna = np.array( [AntennaOmni(10), AntennaOmni(11), AntennaOmni(22), AntennaOmni(23)], ) @@ -377,9 +377,9 @@ def test_simulation_2bs_4ue_uplink(self): self.simulation.system = StationFactory.generate_fss_space_station( self.param.fss_ss, ) - self.simulation.system.x = np.array([0]) - self.simulation.system.y = np.array([0]) - self.simulation.system.z = np.array([self.param.fss_ss.altitude]) + self.simulation.system.geom.x_global = np.array([0]) + self.simulation.system.geom.y_global = np.array([0]) + self.simulation.system.geom.z_global = np.array([self.param.fss_ss.altitude]) # test the method that calculates interference from IMT UE to FSS space # station diff --git a/tests/test_geometry_converter.py b/tests/test_geometry_converter.py index 7d8b3ddd5..8ed0fa6c5 100644 --- a/tests/test_geometry_converter.py +++ b/tests/test_geometry_converter.py @@ -69,7 +69,6 @@ def test_reference_ecef(self): for coord_sys in self.all_converters: lat, lon, alt = ecef2lla(coord_sys.ref_x, coord_sys.ref_y, coord_sys.ref_z) # ecef2lla approximation requires "almost equal" directive - print("coord_sys.ref_lat", coord_sys.ref_lat) self.assertAlmostEqual(lat[0], coord_sys.ref_lat, places=8) self.assertAlmostEqual(lon[0], coord_sys.ref_long, places=8) self.assertAlmostEqual(alt[0], coord_sys.ref_alt, places=8) @@ -126,25 +125,35 @@ def test_station_converter(self): azim_bef = rng.uniform(-180, 180, n_samples) elev_bef = rng.uniform(-90, 90, n_samples) - stations.x, stations.y, stations.z = xyz_bef - stations.azimuth = azim_bef - stations.elevation = elev_bef + stations.geom.x_global, stations.geom.y_global, stations.geom.z_global = xyz_bef + stations.geom.pointn_azim_global = azim_bef + stations.geom.pointn_elev_global = elev_bef # get relative distances and off axis while in ecef - dists_bef = stations.get_3d_distance_to(stations) - off_axis_bef = stations.get_off_axis_angle(stations) + dists_bef = stations.geom.get_3d_distance_to(stations.geom) + off_axis_bef = stations.geom.get_off_axis_angle(stations.geom) # convert stations to enu - coord_sys.station_ecef2enu(stations) - + nx, ny, nz = coord_sys.ecef2enu( + stations.geom.x_global, + stations.geom.y_global, + stations.geom.z_global, + ) + nazim, nelev = coord_sys.angle_ecef2enu( + stations.geom.pointn_azim_global, + stations.geom.pointn_elev_global, + ) + stations.geom.set_global_coords( + nx, ny, nz, nazim, nelev + ) # check if reference origin - self.assertEqual(stations.x[0], 0) - self.assertEqual(stations.y[0], 0) - self.assertEqual(stations.z[0], 0) + self.assertEqual(stations.geom.x_global[0], 0) + self.assertEqual(stations.geom.y_global[0], 0) + self.assertEqual(stations.geom.z_global[0], 0) # get relative distances and off axis while in enu - dists_aft = stations.get_3d_distance_to(stations) - off_axis_aft = stations.get_off_axis_angle(stations) + dists_aft = stations.geom.get_3d_distance_to(stations.geom) + off_axis_aft = stations.geom.get_off_axis_angle(stations.geom) # all stations should maintain same relative distances and off axis # since their relative positioning and pointing should eq in ECEF @@ -167,39 +176,49 @@ def test_station_converter(self): # so we ignore the first station (used as reference) on these # checks npt.assert_equal( - np.abs(stations.x[1:] - xyz_bef[0][1:]) > 1e3, + np.abs(stations.geom.x_global[1:] - xyz_bef[0][1:]) > 1e3, True ) npt.assert_equal( - np.abs(stations.y[1:] - xyz_bef[1][1:]) > 1e3, + np.abs(stations.geom.y_global[1:] - xyz_bef[1][1:]) > 1e3, True ) npt.assert_equal( - np.abs(stations.z[1:] - xyz_bef[2][1:]) > 1e3, + np.abs(stations.geom.z_global[1:] - xyz_bef[2][1:]) > 1e3, True ) # and the elevation angle may not change much if pointing vector is to the east/west # since the pointing vector is aligned with the x axis, the rotation along it # won't change the value much npt.assert_equal( - np.abs(stations.azimuth - azim_bef) > 0.4, + np.abs(stations.geom.pointn_azim_global - azim_bef) > 0.4, True ) npt.assert_equal( - np.abs(stations.elevation - elev_bef) > 0.4, + np.abs(stations.geom.pointn_elev_global - elev_bef) > 0.4, True ) # return stations to starting case: - coord_sys.station_enu2ecef(stations) - + nx, ny, nz = coord_sys.enu2ecef( + stations.geom.x_global, + stations.geom.y_global, + stations.geom.z_global, + ) + nazim, nelev = coord_sys.angle_enu2ecef( + stations.geom.pointn_azim_global, + stations.geom.pointn_elev_global, + ) + stations.geom.set_global_coords( + nx, ny, nz, nazim, nelev + ) # check if their position is the same as at the start # some precision error occurs, so "almost equal" is needed - npt.assert_almost_equal(stations.x, xyz_bef[0]) - npt.assert_almost_equal(stations.y, xyz_bef[1]) - npt.assert_almost_equal(stations.z, xyz_bef[2]) - npt.assert_almost_equal(stations.azimuth, azim_bef) - npt.assert_almost_equal(stations.elevation, elev_bef) + npt.assert_almost_equal(stations.geom.x_global, xyz_bef[0]) + npt.assert_almost_equal(stations.geom.y_global, xyz_bef[1]) + npt.assert_almost_equal(stations.geom.z_global, xyz_bef[2]) + npt.assert_almost_equal(stations.geom.pointn_azim_global, azim_bef) + npt.assert_almost_equal(stations.geom.pointn_elev_global, elev_bef) if __name__ == '__main__': diff --git a/tests/test_simulation_downlink.py b/tests/test_simulation_downlink.py index cb3cff593..62f3bea5b 100644 --- a/tests/test_simulation_downlink.py +++ b/tests/test_simulation_downlink.py @@ -209,8 +209,8 @@ def test_simulation_2bs_4ue_fss_ss(self): self.simulation.topology, random_number_gen, ) - self.simulation.ue.x = np.array([20, 70, 110, 170]) - self.simulation.ue.y = np.array([0, 0, 0, 0]) + self.simulation.ue.geom.x_global = np.array([20, 70, 110, 170]) + self.simulation.ue.geom.y_global = np.array([0, 0, 0, 0]) self.simulation.ue.antenna = np.array( [AntennaOmni(10), AntennaOmni(11), AntennaOmni(22), AntennaOmni(23)], ) @@ -337,9 +337,9 @@ def test_simulation_2bs_4ue_fss_ss(self): self.simulation.system = StationFactory.generate_fss_space_station( self.param.fss_ss, ) - self.simulation.system.x = np.array([0.01]) # avoids zero-division - self.simulation.system.y = np.array([0]) - self.simulation.system.z = np.array([self.param.fss_ss.altitude]) + self.simulation.system.geom.x_global = np.array([0.01]) # avoids zero-division + self.simulation.system.geom.y_global = np.array([0]) + self.simulation.system.geom.z_global = np.array([self.param.fss_ss.altitude]) # test the method that calculates interference from IMT UE to FSS space # station @@ -419,8 +419,8 @@ def test_simulation_2bs_4ue_fss_es(self): self.simulation.topology, random_number_gen, ) - self.simulation.ue.x = np.array([20, 70, 110, 170]) - self.simulation.ue.y = np.array([0, 0, 0, 0]) + self.simulation.ue.geom.x_global = np.array([20, 70, 110, 170]) + self.simulation.ue.geom.y_global = np.array([0, 0, 0, 0]) self.simulation.ue.antenna = np.array( [AntennaOmni(10), AntennaOmni(11), AntennaOmni(22), AntennaOmni(23)], ) @@ -518,9 +518,9 @@ def test_simulation_2bs_4ue_fss_es(self): self.simulation.system = StationFactory.generate_fss_earth_station( self.param.fss_es, random_number_gen, ) - self.simulation.system.x = np.array([-2000]) - self.simulation.system.y = np.array([0]) - self.simulation.system.z = np.array([self.param.fss_es.height]) + self.simulation.system.geom.x_global = np.array([-2000]) + self.simulation.system.geom.y_global = np.array([0]) + self.simulation.system.geom.z_global = np.array([self.param.fss_es.height]) self.simulation.propagation_imt = PropagationFactory.create_propagation( self.param.imt.channel_model, @@ -640,8 +640,8 @@ def test_simulation_2bs_4ue_ras(self): self.simulation.topology, random_number_gen, ) - self.simulation.ue.x = np.array([20, 70, 110, 170]) - self.simulation.ue.y = np.array([0, 0, 0, 0]) + self.simulation.ue.geom.x_global = np.array([20, 70, 110, 170]) + self.simulation.ue.geom.y_global = np.array([0, 0, 0, 0]) self.simulation.ue.antenna = np.array( [AntennaOmni(10), AntennaOmni(11), AntennaOmni(22), AntennaOmni(23)], ) @@ -693,9 +693,9 @@ def test_simulation_2bs_4ue_ras(self): self.simulation.system = StationFactory.generate_ras_station( self.param.ras, random_number_gen, topology=None, ) - self.simulation.system.x = np.array([-2000]) - self.simulation.system.y = np.array([0]) - self.simulation.system.z = np.array( + self.simulation.system.geom.x_global = np.array([-2000]) + self.simulation.system.geom.y_global = np.array([0]) + self.simulation.system.geom.z_global = np.array( [self.param.ras.geometry.height]) self.simulation.system.antenna[0].effective_area = 54.9779 diff --git a/tests/test_simulation_downlink_haps.py b/tests/test_simulation_downlink_haps.py index 6b85631b0..461d1dbad 100644 --- a/tests/test_simulation_downlink_haps.py +++ b/tests/test_simulation_downlink_haps.py @@ -169,8 +169,8 @@ def test_simulation_2bs_4ue_1haps(self): self.simulation.topology, random_number_gen, ) - self.simulation.ue.x = np.array([20, 70, 110, 170]) - self.simulation.ue.y = np.array([0, 0, 0, 0]) + self.simulation.ue.geom.x_global = np.array([20, 70, 110, 170]) + self.simulation.ue.geom.y_global = np.array([0, 0, 0, 0]) self.simulation.ue.antenna = np.array( [AntennaOmni(10), AntennaOmni(11), AntennaOmni(22), AntennaOmni(23)], ) diff --git a/tests/test_simulation_downlink_tvro.py b/tests/test_simulation_downlink_tvro.py index 216181a94..2e7ddd90d 100644 --- a/tests/test_simulation_downlink_tvro.py +++ b/tests/test_simulation_downlink_tvro.py @@ -153,10 +153,10 @@ def test_simulation_1bs_1ue_tvro(self): self.simulation.topology, random_number_gen, ) - self.simulation.bs.x = np.array([0, -200]) - self.simulation.bs.y = np.array([0, 0]) - self.simulation.bs.azimuth = np.array([0, 180]) - self.simulation.bs.elevation = np.array([-10, -10]) + self.simulation.bs.geom.x_global = np.array([0, -200]) + self.simulation.bs.geom.y_global = np.array([0, 0]) + self.simulation.bs.geom.pointn_azim_global = np.array([0, 180]) + self.simulation.bs.geom.pointn_elev_global = np.array([-10, -10]) self.simulation.ue = StationFactory.generate_imt_ue( self.param.imt, @@ -164,8 +164,8 @@ def test_simulation_1bs_1ue_tvro(self): self.simulation.topology, random_number_gen, ) - self.simulation.ue.x = np.array([30, 60, -220, -300]) - self.simulation.ue.y = np.array([0, 0, 0, 0]) + self.simulation.ue.geom.x_global = np.array([30, 60, -220, -300]) + self.simulation.ue.geom.y_global = np.array([0, 0, 0, 0]) # test connection method self.simulation.connect_ue_to_bs() @@ -285,8 +285,8 @@ def test_simulation_1bs_1ue_tvro(self): self.simulation.system = StationFactory.generate_fss_earth_station( self.param.fss_es, random_number_gen, ) - self.simulation.system.x = np.array([600]) - self.simulation.system.y = np.array([0]) + self.simulation.system.geom.x_global = np.array([600]) + self.simulation.system.geom.y_global = np.array([0]) # test the method that calculates interference from IMT UE to FSS space # station diff --git a/tests/test_simulation_indoor.py b/tests/test_simulation_indoor.py index 1b68c97c6..585ecab11 100644 --- a/tests/test_simulation_indoor.py +++ b/tests/test_simulation_indoor.py @@ -167,10 +167,10 @@ def test_simulation_fss_es(self): # print("Random position:") # self.simulation.plot_scenario() - self.simulation.ue.x = np.array([0.0, 45.0, 75.0, 120.0]) - self.simulation.ue.y = np.array([0.0, 50.0, 0.0, 50.0]) - self.simulation.ue.z = np.ones_like( - self.simulation.ue.x) * self.param.imt.ue.height + self.simulation.ue.geom.x_global = np.array([0.0, 45.0, 75.0, 120.0]) + self.simulation.ue.geom.y_global = np.array([0.0, 50.0, 0.0, 50.0]) + self.simulation.ue.geom.z_global = np.ones_like( + self.simulation.ue.geom.x_global) * self.param.imt.ue.height # print("Forced position:") # self.simulation.plot_scenario() @@ -239,8 +239,8 @@ def test_simulation_fss_es(self): ) # Test angle to ES in the IMT coord system - phi_es, theta_es = self.simulation.bs.get_global_pointing_vector_to( - self.simulation.system, + phi_es, theta_es = self.simulation.bs.geom.get_global_pointing_vector_to( + self.simulation.system.geom, ) expected_phi_es = np.array([[18.44], [23.96], [33.69], [53.13]]) npt.assert_array_almost_equal(phi_es, expected_phi_es, decimal=2) diff --git a/tests/test_simulation_uplink.py b/tests/test_simulation_uplink.py index 57875569e..c5096e3d7 100644 --- a/tests/test_simulation_uplink.py +++ b/tests/test_simulation_uplink.py @@ -201,8 +201,8 @@ def test_simulation_2bs_4ue_ss(self): self.simulation.topology, random_number_gen, ) - self.simulation.ue.x = np.array([20, 70, 110, 170]) - self.simulation.ue.y = np.array([0, 0, 0, 0]) + self.simulation.ue.geom.x_global = np.array([20, 70, 110, 170]) + self.simulation.ue.geom.y_global = np.array([0, 0, 0, 0]) self.simulation.ue.antenna = np.array( [AntennaOmni(10), AntennaOmni(11), AntennaOmni(22), AntennaOmni(23)], ) @@ -363,9 +363,9 @@ def test_simulation_2bs_4ue_ss(self): self.simulation.system = StationFactory.generate_fss_space_station( self.param.fss_ss, ) - self.simulation.system.x = np.array([0]) - self.simulation.system.y = np.array([0]) - self.simulation.system.z = np.array([self.param.fss_ss.altitude]) + self.simulation.system.geom.x_global = np.array([0]) + self.simulation.system.geom.y_global = np.array([0]) + self.simulation.system.geom.z_global = np.array([self.param.fss_ss.altitude]) # test the method that calculates interference from IMT UE to FSS space # station @@ -432,8 +432,8 @@ def test_simulation_2bs_4ue_es(self): self.simulation.topology, random_number_gen, ) - self.simulation.ue.x = np.array([20, 70, 110, 170]) - self.simulation.ue.y = np.array([0, 0, 0, 0]) + self.simulation.ue.geom.x_global = np.array([20, 70, 110, 170]) + self.simulation.ue.geom.y_global = np.array([0, 0, 0, 0]) self.simulation.ue.antenna = np.array( [AntennaOmni(10), AntennaOmni(11), AntennaOmni(22), AntennaOmni(23)], ) @@ -719,8 +719,8 @@ def test_simulation_2bs_4ue_ras(self): self.simulation.topology, random_number_gen, ) - self.simulation.ue.x = np.array([20, 70, 110, 170]) - self.simulation.ue.y = np.array([0, 0, 0, 0]) + self.simulation.ue.geom.x_global = np.array([20, 70, 110, 170]) + self.simulation.ue.geom.y_global = np.array([0, 0, 0, 0]) self.simulation.ue.antenna = np.array( [AntennaOmni(10), AntennaOmni(11), AntennaOmni(22), AntennaOmni(23)], ) @@ -778,9 +778,9 @@ def test_simulation_2bs_4ue_ras(self): self.simulation.system = StationFactory.generate_ras_station( self.param.ras, random_number_gen, None, ) - self.simulation.system.x = np.array([-2000]) - self.simulation.system.y = np.array([0]) - self.simulation.system.z = np.array( + self.simulation.system.geom.x_global = np.array([-2000]) + self.simulation.system.geom.y_global = np.array([0]) + self.simulation.system.geom.z_global = np.array( [self.param.ras.geometry.height]) self.simulation.system.antenna[0].effective_area = 54.9779 @@ -861,8 +861,8 @@ def test_beamforming_gains(self): self.simulation.topology, random_number_gen, ) - self.simulation.ue.x = np.array([50.000, 43.301, 150.000, 175.000]) - self.simulation.ue.y = np.array([0.000, 25.000, 0.000, 43.301]) + self.simulation.ue.geom.x_global = np.array([50.000, 43.301, 150.000, 175.000]) + self.simulation.ue.geom.y_global = np.array([0.000, 25.000, 0.000, 43.301]) # Physical pointing angles self.assertEqual(self.simulation.bs.antenna[0].azimuth, 0) @@ -871,13 +871,13 @@ def test_beamforming_gains(self): self.assertEqual(self.simulation.bs.antenna[0].elevation, -10) # Change UE pointing - self.simulation.ue.azimuth = np.array([180, -90, 90, -90]) - self.simulation.ue.elevation = np.array([-30, -15, 15, 30]) + self.simulation.ue.geom.pointn_azim_global = np.array([180, -90, 90, -90]) + self.simulation.ue.geom.pointn_elev_global = np.array([-30, -15, 15, 30]) par = self.param.imt.ue.antenna.array.get_antenna_parameters() for i in range(self.simulation.ue.num_stations): self.simulation.ue.antenna[i] = AntennaBeamformingImt( - par, self.simulation.ue.azimuth[i], - self.simulation.ue.elevation[i], + par, self.simulation.ue.geom.pointn_azim_global[i], + self.simulation.ue.geom.pointn_elev_global[i], ) self.assertEqual(self.simulation.ue.antenna[0].azimuth, 180) self.assertEqual(self.simulation.ue.antenna[0].elevation, -30) @@ -894,8 +894,8 @@ def test_beamforming_gains(self): # Test BS gains # Test pointing vector - phi, theta = self.simulation.bs.get_global_pointing_vector_to( - self.simulation.ue, + phi, theta = self.simulation.bs.geom.get_global_pointing_vector_to( + self.simulation.ue.geom, ) npt.assert_allclose( phi, np.array([ diff --git a/tests/test_station_factory.py b/tests/test_station_factory.py index d0b126bbf..d0fb19f94 100644 --- a/tests/test_station_factory.py +++ b/tests/test_station_factory.py @@ -54,16 +54,16 @@ def test_generate_imt_base_stations_ntn(self): ntn_topology.calculate_coordinates() ntn_bs = StationFactory.generate_imt_base_stations( param_imt, param_imt.bs.antenna.array, ntn_topology, rng) - npt.assert_equal(ntn_bs.z, param_imt.topology.ntn.bs_height) + npt.assert_equal(ntn_bs.geom.z_global, param_imt.topology.ntn.bs_height) # the azimuth seen from BS antenna npt.assert_almost_equal( - ntn_bs.azimuth[0], + ntn_bs.geom.pointn_azim_global[0], param_imt.topology.ntn.bs_azimuth - 180, 1e-3) # Elevation w.r.t to xy plane - npt.assert_almost_equal(ntn_bs.elevation[0], -45.0, 1e-2) + npt.assert_almost_equal(ntn_bs.geom.pointn_elev_global[0], -45.0, 1e-2) npt.assert_almost_equal( - ntn_bs.x, param_imt.topology.ntn.bs_height * + ntn_bs.geom.x_global, param_imt.topology.ntn.bs_height * np.tan(np.radians(param_imt.topology.ntn.bs_elevation)) * np.cos(np.radians(param_imt.topology.ntn.bs_azimuth)), 1e-2, ) @@ -101,7 +101,7 @@ def test_generate_imt_ue_outdoor_ntn(self): ntn_topology.calculate_coordinates() ntn_ue = StationFactory.generate_imt_ue_outdoor( param_imt, param_imt.ue.antenna.array, rng, ntn_topology) - dist = np.sqrt(ntn_ue.x**2 + ntn_ue.y**2) + dist = np.sqrt(ntn_ue.geom.x_global**2 + ntn_ue.geom.y_global**2) # test if the maximum distance is close to the cell radius within a # 100km range npt.assert_almost_equal( @@ -138,16 +138,16 @@ def test_generate_single_space_station(self): def get_ground_elevation(ss): return np.rad2deg( np.arctan2( - ss.z, + ss.geom.z_global, np.sqrt( - ss.x**2 + - ss.y**2))) + ss.geom.x_global**2 + + ss.geom.y_global**2))) space_station = StationFactory.generate_single_space_station(param) # test if the maximum distance is close to the cell radius within a # 100km range - npt.assert_almost_equal(space_station.z, param.geometry.altitude) + npt.assert_almost_equal(space_station.geom.z_global, param.geometry.altitude) npt.assert_almost_equal(get_ground_elevation(space_station), 90) param.geometry.es_lat_deg = max_gso_fov @@ -155,7 +155,7 @@ def get_ground_elevation(ss): space_station = StationFactory.generate_single_space_station(param) npt.assert_almost_equal(get_ground_elevation(space_station), 0, 5) - npt.assert_almost_equal(space_station.z, 0, 0) + npt.assert_almost_equal(space_station.geom.z_global, 0, 0) param.geometry.es_lat_deg = 0 param.geometry.es_long_deg = max_gso_fov @@ -163,7 +163,7 @@ def get_ground_elevation(ss): space_station = StationFactory.generate_single_space_station(param) npt.assert_almost_equal(get_ground_elevation(space_station), 0, 5) - npt.assert_almost_equal(space_station.z, 0, 0) + npt.assert_almost_equal(space_station.geom.z_global, 0, 0) param.geometry.es_long_deg = 0 param.geometry.location.fixed.lat_deg = max_gso_fov @@ -171,14 +171,14 @@ def get_ground_elevation(ss): space_station = StationFactory.generate_single_space_station(param) npt.assert_almost_equal(get_ground_elevation(space_station), 0, 5) - npt.assert_almost_equal(space_station.z, 0, 0) + npt.assert_almost_equal(space_station.geom.z_global, 0, 0) param.geometry.location.fixed.lat_deg = 0 param.geometry.location.fixed.long_deg = max_gso_fov space_station = StationFactory.generate_single_space_station(param) npt.assert_almost_equal(get_ground_elevation(space_station), 0, 5) - npt.assert_almost_equal(space_station.z, 0, 0) + npt.assert_almost_equal(space_station.geom.z_global, 0, 0) def test_single_space_station_pointing(self): """Basic test for space station generation.""" @@ -207,9 +207,9 @@ def test_single_space_station_pointing(self): param.validate() imt_center = StationManager(1) - imt_center.x = np.array([0.]) - imt_center.y = np.array([0.]) - imt_center.z = np.array([0.]) + imt_center.geom.x_global = np.array([0.]) + imt_center.geom.y_global = np.array([0.]) + imt_center.geom.z_global = np.array([0.]) # Test point it toward IMT center (0, 0, 0) param.geometry.azimuth.type = "POINTING_AT_IMT" @@ -217,7 +217,7 @@ def test_single_space_station_pointing(self): space_station = StationFactory.generate_single_space_station(param) - npt.assert_almost_equal(space_station.get_off_axis_angle(imt_center), 0, 5) + npt.assert_almost_equal(space_station.geom.get_off_axis_angle(imt_center.geom), 0, 5) # Test pointing it toward IMT center (0, 0, 0) # but in another way @@ -229,16 +229,16 @@ def test_single_space_station_pointing(self): space_station = StationFactory.generate_single_space_station(param) - npt.assert_almost_equal(space_station.get_off_axis_angle(imt_center), 0, 5) + npt.assert_almost_equal(space_station.geom.get_off_axis_angle(imt_center.geom), 0, 5) # Test pointing it toward subsatellite. # In spherical earth model, # same as pointing toward center of earth center_of_earth = StationManager(1) - center_of_earth.x = np.array([0.]) - center_of_earth.y = np.array([0.]) - center_of_earth.z = -np.array([EARTH_RADIUS_M + 1200]) + center_of_earth.geom.x_global = np.array([0.]) + center_of_earth.geom.y_global = np.array([0.]) + center_of_earth.geom.z_global = -np.array([EARTH_RADIUS_M + 1200]) param.geometry.azimuth.type = "POINTING_AT_LAT_LONG_ALT" param.geometry.elevation.type = "POINTING_AT_LAT_LONG_ALT" @@ -248,7 +248,7 @@ def test_single_space_station_pointing(self): space_station = StationFactory.generate_single_space_station(param) - npt.assert_almost_equal(space_station.get_off_axis_angle(center_of_earth), 0, 5) + npt.assert_almost_equal(space_station.geom.get_off_axis_angle(center_of_earth.geom), 0, 5) if __name__ == '__main__': diff --git a/tests/test_station_factory_ngso.py b/tests/test_station_factory_ngso.py index 6b6907b7b..76cb10ffb 100644 --- a/tests/test_station_factory_ngso.py +++ b/tests/test_station_factory_ngso.py @@ -81,9 +81,9 @@ def test_ngso_manager(self): """Test that the NGSO manager creates the correct number and type of stations.""" self.assertEqual(self.ngso_manager.station_type, StationType.MSS_D2D) self.assertEqual(self.ngso_manager.num_stations, 20 * 32 + 12 * 20) - self.assertEqual(self.ngso_manager.x.shape, (20 * 32 + 12 * 20,)) - self.assertEqual(self.ngso_manager.y.shape, (20 * 32 + 12 * 20,)) - self.assertEqual(self.ngso_manager.z.shape, (20 * 32 + 12 * 20,)) + self.assertEqual(self.ngso_manager.geom.x_global.shape, (20 * 32 + 12 * 20,)) + self.assertEqual(self.ngso_manager.geom.y_global.shape, (20 * 32 + 12 * 20,)) + self.assertEqual(self.ngso_manager.geom.z_global.shape, (20 * 32 + 12 * 20,)) def test_satellite_antenna_pointing(self): """Test that satellite antennas point to nadir and off-axis angles are correct.""" @@ -93,27 +93,27 @@ def test_satellite_antenna_pointing(self): # y > 0 <=> azimuth < 0 # y < 0 <=> azimuth > 0 npt.assert_array_equal( - np.sign(self.ngso_manager.azimuth), -np.sign(self.ngso_manager.y)) + np.sign(self.ngso_manager.geom.pointn_azim_global), -np.sign(self.ngso_manager.geom.y_global)) # Test: check if center of earth is 0deg off axis, and that its # distance to satellite is correct earth_center = StationManager(1) - earth_center.x = np.array([0.]) - earth_center.y = np.array([0.]) + earth_center.geom.x_global = np.array([0.]) + earth_center.geom.y_global = np.array([0.]) x, y, z = lla2ecef(self.lat, self.long, self.alt) - earth_center.z = -np.sqrt( + earth_center.geom.z_global = -np.sqrt( x * x + y * y + z * z, ) - self.assertNotAlmostEqual(earth_center.z[0], 0.) + self.assertNotAlmostEqual(earth_center.geom.z_global[0], 0.) - off_axis_angle = self.ngso_manager.get_off_axis_angle(earth_center) - distance_to_center_of_earth = self.ngso_manager.get_3d_distance_to( - earth_center) + off_axis_angle = self.ngso_manager.geom.get_off_axis_angle(earth_center.geom) + distance_to_center_of_earth = self.ngso_manager.geom.get_3d_distance_to( + earth_center.geom) distance_to_center_of_earth_should_eq = np.sqrt( - self.ngso_manager.x ** 2 + - self.ngso_manager.y ** 2 + - (np.sqrt(x * x + y * y + z * z) + self.ngso_manager.z) ** 2, + self.ngso_manager.geom.x_global ** 2 + + self.ngso_manager.geom.y_global ** 2 + + (np.sqrt(x * x + y * y + z * z) + self.ngso_manager.geom.z_global) ** 2, ) npt.assert_allclose(off_axis_angle, 0.0, atol=1e-05) @@ -131,44 +131,67 @@ def test_satellite_coordinate_reversing(self): ngso_original_coord = StationFactory.generate_mss_d2d( self.param, rng, self.coord_sys) - self.coord_sys.station_enu2ecef(ngso_original_coord) + nx, ny, nz = self.coord_sys.enu2ecef( + ngso_original_coord.geom.x_global, + ngso_original_coord.geom.y_global, + ngso_original_coord.geom.z_global, + ) + nazim, nelev = self.coord_sys.angle_enu2ecef( + ngso_original_coord.geom.pointn_azim_global, + ngso_original_coord.geom.pointn_elev_global, + ) + ngso_original_coord.geom.set_global_coords( + nx, ny, nz, nazim, nelev + ) # Test: check if azimuth is pointing towards correct direction # y > 0 <=> azimuth < 0 # y < 0 <=> azimuth > 0 npt.assert_array_equal( - np.sign(ngso_original_coord.azimuth), -np.sign(ngso_original_coord.y)) + np.sign(ngso_original_coord.geom.pointn_azim_global), -np.sign(ngso_original_coord.geom.y_global)) # Test: check if center of earth is 0deg off axis earth_center = StationManager(1) - earth_center.x = np.array([0.]) - earth_center.y = np.array([0.]) - earth_center.z = np.array([0.]) + earth_center.geom.x_global = np.array([0.]) + earth_center.geom.y_global = np.array([0.]) + earth_center.geom.z_global = np.array([0.]) - off_axis_angle = ngso_original_coord.get_off_axis_angle(earth_center) + off_axis_angle = ngso_original_coord.geom.get_off_axis_angle(earth_center.geom) npt.assert_allclose(off_axis_angle, 0.0, atol=1e-05) - self.coord_sys.station_ecef2enu(ngso_original_coord) + # convert stations to enu + nx, ny, nz = self.coord_sys.ecef2enu( + ngso_original_coord.geom.x_global, + ngso_original_coord.geom.y_global, + ngso_original_coord.geom.z_global, + ) + nazim, nelev = self.coord_sys.angle_ecef2enu( + ngso_original_coord.geom.pointn_azim_global, + ngso_original_coord.geom.pointn_elev_global, + ) + ngso_original_coord.geom.set_global_coords( + nx, ny, nz, nazim, nelev + ) npt.assert_allclose( - self.ngso_manager.x, - ngso_original_coord.x, + self.ngso_manager.geom.x_global, + ngso_original_coord.geom.x_global, atol=1e-500) npt.assert_allclose( - self.ngso_manager.y, - ngso_original_coord.y, + self.ngso_manager.geom.y_global, + ngso_original_coord.geom.y_global, atol=1e-500) npt.assert_allclose( - self.ngso_manager.z, - ngso_original_coord.z, + self.ngso_manager.geom.z_global, + ngso_original_coord.geom.z_global, atol=1e-500) npt.assert_allclose( - self.ngso_manager.azimuth, - ngso_original_coord.azimuth, + self.ngso_manager.geom.pointn_azim_global, + ngso_original_coord.geom.pointn_azim_global, atol=1e-500) npt.assert_allclose( - self.ngso_manager.elevation, - ngso_original_coord.elevation, + self.ngso_manager.geom.pointn_elev_global, + ngso_original_coord.geom.pointn_elev_global, atol=1e-500) diff --git a/tests/test_station_manager.py b/tests/test_station_manager.py index c082c0e09..e68049db2 100644 --- a/tests/test_station_manager.py +++ b/tests/test_station_manager.py @@ -61,10 +61,10 @@ def setUp(self): self.ue_param.multiplication_factor = 12 self.station_manager = StationManager(3) - self.station_manager.x = np.array([10, 20, 30]) - self.station_manager.y = np.array([15, 25, 35]) - self.station_manager.z = np.array([1, 2, 3]) - self.station_manager.intersite_dist = 100.0 + self.station_manager.geom.x_global = np.array([10, 20, 30]) + self.station_manager.geom.y_global = np.array([15, 25, 35]) + self.station_manager.geom.z_global = np.array([1, 2, 3]) + self.station_manager.geom.intersite_dist = 100.0 # this is for downlink self.station_manager.tx_power = dict({0: [27, 30], 1: [35], 2: [40]}) self.station_manager.rx_power = np.array([-50, -35, -10]) @@ -77,10 +77,10 @@ def setUp(self): self.station_manager.station_type = StationType.IMT_BS self.station_manager2 = StationManager(2) - self.station_manager2.x = np.array([100, 200]) - self.station_manager2.y = np.array([105, 250]) - self.station_manager2.z = np.array([4, 5]) - self.station_manager2.intersite_dist = 100.0 + self.station_manager2.geom.x_global = np.array([100, 200]) + self.station_manager2.geom.y_global = np.array([105, 250]) + self.station_manager2.geom.z_global = np.array([4, 5]) + self.station_manager2.geom.intersite_dist = 100.0 # this is for downlink self.station_manager2.tx_power = dict({0: [25], 1: [28, 35]}) self.station_manager2.rx_power = np.array([-50, -35]) @@ -91,10 +91,10 @@ def setUp(self): self.station_manager2.station_type = StationType.IMT_BS self.station_manager3 = StationManager(1) - self.station_manager3.x = np.array([300]) - self.station_manager3.y = np.array([400]) - self.station_manager3.z = np.array([2]) - self.station_manager3.intersite_dist = 100.0 + self.station_manager3.geom.x_global = np.array([300]) + self.station_manager3.geom.y_global = np.array([400]) + self.station_manager3.geom.z_global = np.array([2]) + self.station_manager3.geom.intersite_dist = 100.0 # this is for uplink self.station_manager3.tx_power = 22 self.station_manager3.rx_power = np.array([-50, -35]) @@ -147,55 +147,55 @@ def test_station_type(self): def test_x(self): """Test getting and setting x-coordinates for stations.""" # get a single value from the original array - self.assertEqual(self.station_manager.x[0], 10) + self.assertEqual(self.station_manager.geom.x_global[0], 10) # get two specific values - npt.assert_array_equal(self.station_manager.x[[1, 2]], [20, 30]) + npt.assert_array_equal(self.station_manager.geom.x_global[[1, 2]], [20, 30]) # get values in reverse order - npt.assert_array_equal(self.station_manager.x[[2, 1, 0]], [30, 20, 10]) + npt.assert_array_equal(self.station_manager.geom.x_global[[2, 1, 0]], [30, 20, 10]) # get all values (no need to specify the id's) - npt.assert_array_equal(self.station_manager.x, [10, 20, 30]) + npt.assert_array_equal(self.station_manager.geom.x_global, [10, 20, 30]) # set a single value and get it - self.station_manager.x[0] = 8 - npt.assert_array_equal(self.station_manager.x[[0, 1]], [8, 20]) + self.station_manager.geom.x_global[0] = 8 + npt.assert_array_equal(self.station_manager.geom.x_global[[0, 1]], [8, 20]) # set two values and then get all values - self.station_manager.x[[1, 2]] = [16, 32] - npt.assert_array_equal(self.station_manager.x, [8, 16, 32]) + self.station_manager.geom.x_global[[1, 2]] = [16, 32] + npt.assert_array_equal(self.station_manager.geom.x_global, [8, 16, 32]) def test_y(self): """Test getting and setting y-coordinates for stations.""" # get a single value from the original array - self.assertEqual(self.station_manager.y[0], 15) + self.assertEqual(self.station_manager.geom.y_global[0], 15) # get two specific values - npt.assert_array_equal(self.station_manager.y[[1, 2]], [25, 35]) + npt.assert_array_equal(self.station_manager.geom.y_global[[1, 2]], [25, 35]) # get values in reverse order - npt.assert_array_equal(self.station_manager.y[[2, 1, 0]], [35, 25, 15]) + npt.assert_array_equal(self.station_manager.geom.y_global[[2, 1, 0]], [35, 25, 15]) # get all values (no need to specify the id's) - npt.assert_array_equal(self.station_manager.y, [15, 25, 35]) + npt.assert_array_equal(self.station_manager.geom.y_global, [15, 25, 35]) # set a single value and get it - self.station_manager.y[1] = 9 - npt.assert_array_equal(self.station_manager.y[[0, 1]], [15, 9]) + self.station_manager.geom.y_global[1] = 9 + npt.assert_array_equal(self.station_manager.geom.y_global[[0, 1]], [15, 9]) # set two values and then get all values - self.station_manager.y[[0, 2]] = [7, 21] - npt.assert_array_equal(self.station_manager.y, [7, 9, 21]) + self.station_manager.geom.y_global[[0, 2]] = [7, 21] + npt.assert_array_equal(self.station_manager.geom.y_global, [7, 9, 21]) def test_height(self): """Test getting and setting station heights.""" # get a single value from the original array - self.assertEqual(self.station_manager.z[0], 1) + self.assertEqual(self.station_manager.geom.z_global[0], 1) # get two specific values - npt.assert_array_equal(self.station_manager.z[[0, 2]], [1, 3]) + npt.assert_array_equal(self.station_manager.geom.z_global[[0, 2]], [1, 3]) # get values in reverse order npt.assert_array_equal( - self.station_manager.z[[2, 1, 0]], [3, 2, 1], + self.station_manager.geom.z_global[[2, 1, 0]], [3, 2, 1], ) # get all values (no need to specify the id's) - npt.assert_array_equal(self.station_manager.z, [1, 2, 3]) + npt.assert_array_equal(self.station_manager.geom.z_global, [1, 2, 3]) # set a single value and get it - self.station_manager.z[1] = 7 - npt.assert_array_equal(self.station_manager.z[[1, 2]], [7, 3]) + self.station_manager.geom.z_global[1] = 7 + npt.assert_array_equal(self.station_manager.geom.z_global[[1, 2]], [7, 3]) # set two values and then get all values - self.station_manager.z[[0, 2]] = [5, 4] - npt.assert_array_equal(self.station_manager.z, [5, 7, 4]) + self.station_manager.geom.z_global[[0, 2]] = [5, 4] + npt.assert_array_equal(self.station_manager.geom.z_global, [5, 7, 4]) def test_tx_power(self): """Test getting and setting transmit power for stations.""" @@ -312,7 +312,7 @@ def test_station_list(self): def test_distance_to(self): """Test 2D distance calculation between station managers.""" ref_distance = np.array([[356.405, 180.277]]) - distance = self.station_manager3.get_local_distance_to(self.station_manager2) + distance = self.station_manager3.geom.get_local_distance_to(self.station_manager2.geom) npt.assert_allclose(distance, ref_distance, atol=1e-2) ref_distance = np.asarray([ @@ -320,14 +320,14 @@ def test_distance_to(self): [113.137, 288.140], [98.994, 274.089], ]) - distance = self.station_manager.get_local_distance_to(self.station_manager2) + distance = self.station_manager.geom.get_local_distance_to(self.station_manager2.geom) npt.assert_allclose(distance, ref_distance, atol=1e-2) def test_3d_distance_to(self): """Test 3D distance calculation between station managers.""" ref_distance = np.asarray([[356.411, 180.302]]) - distance = self.station_manager3.get_3d_distance_to( - self.station_manager2, + distance = self.station_manager3.geom.get_3d_distance_to( + self.station_manager2.geom, ) npt.assert_allclose(distance, ref_distance, atol=1e-2) @@ -336,29 +336,29 @@ def test_3d_distance_to(self): [113.154, 288.156], [99, 274.096], ]) - distance = self.station_manager.get_3d_distance_to( - self.station_manager2, + distance = self.station_manager.geom.get_3d_distance_to( + self.station_manager2.geom, ) npt.assert_allclose(distance, ref_distance, atol=1e-2) def test_wrap_around(self): """Test wrap-around distance and angle calculations between managers.""" self.station_manager = StationManager(2) - self.station_manager.x = np.array([0, 150]) - self.station_manager.y = np.array([0, -32]) - self.station_manager.z = np.array([4, 5]) - self.station_manager.z = np.array([4, 5]) - self.station_manager.intersite_dist = 100.0 + self.station_manager.geom.x_global = np.array([0, 150]) + self.station_manager.geom.y_global = np.array([0, -32]) + self.station_manager.geom.z_global = np.array([4, 5]) + self.station_manager.geom.z_global = np.array([4, 5]) + self.station_manager.geom.intersite_dist = 100.0 self.station_manager2 = StationManager(3) - self.station_manager2.x = np.array([10, 200, 30]) - self.station_manager2.y = np.array([15, 250, -350]) - self.station_manager2.z = np.array([1, 2, 3]) - self.station_manager2.z = np.array([1, 2, 3]) + self.station_manager2.geom.x_global = np.array([10, 200, 30]) + self.station_manager2.geom.y_global = np.array([15, 250, -350]) + self.station_manager2.geom.z_global = np.array([1, 2, 3]) + self.station_manager2.geom.z_global = np.array([1, 2, 3]) # 2D Distance - d_2D, d_3D, phi, theta = self.station_manager.get_global_dist_angles_wrap_around( - self.station_manager2, ) + d_2D, d_3D, phi, theta = self.station_manager.geom.get_global_dist_angles_wrap_around( + self.station_manager2.geom, ) ref_d_2D = np.asarray([ [18.03, 150.32, 85.39], [147.68, 181.12, 205.25], @@ -389,8 +389,8 @@ def test_pointing_vector_to(self): """Test calculation of pointing vectors between station managers.""" eps = 1e-1 # Test 1 - phi, theta = self.station_manager.get_global_pointing_vector_to( - self.station_manager2, + phi, theta = self.station_manager.geom.get_global_pointing_vector_to( + self.station_manager2.geom, ) npt.assert_allclose( phi, np.array([ @@ -408,8 +408,8 @@ def test_pointing_vector_to(self): ) # Test 2 - phi, theta = self.station_manager2.get_global_pointing_vector_to( - self.station_manager, + phi, theta = self.station_manager2.geom.get_global_pointing_vector_to( + self.station_manager.geom, ) npt.assert_allclose( phi, np.array([ @@ -425,15 +425,15 @@ def test_pointing_vector_to(self): ) # Test 3 - phi, theta = self.station_manager3.get_global_pointing_vector_to( - self.station_manager2, + phi, theta = self.station_manager3.geom.get_global_pointing_vector_to( + self.station_manager2.geom, ) npt.assert_allclose(phi, np.array([[-124.13, -123.69]]), atol=eps) npt.assert_allclose(theta, np.array([[89.73, 89.05]]), atol=eps) # Test 4 - phi, theta = self.station_manager2.get_global_pointing_vector_to( - self.station_manager3, + phi, theta = self.station_manager2.geom.get_global_pointing_vector_to( + self.station_manager3.geom, ) npt.assert_allclose(phi, np.array([[55.86], [56.31]]), atol=eps) npt.assert_allclose(theta, np.array([[90.32], [90.95]]), atol=eps) @@ -441,116 +441,104 @@ def test_pointing_vector_to(self): def test_off_axis_angle(self): """Test calculation of off-axis angles between station managers.""" sm1 = StationManager(1) - sm1.x = np.array([0]) - sm1.y = np.array([0]) - sm1.z = np.array([0]) - sm1.z = np.array([0]) - sm1.azimuth = np.array([0]) - sm1.elevation = np.array([0]) + sm1.geom.x_global = np.array([0]) + sm1.geom.y_global = np.array([0]) + sm1.geom.z_global = np.array([0]) + sm1.geom.pointn_azim_global = np.array([0]) + sm1.geom.pointn_elev_global = np.array([0]) sm2 = StationManager(6) - sm2.x = np.array([100, 100, 0, 100, 100, 100]) - sm2.y = np.array([0, 0, 100, 100, 100, 100]) - sm2.z = np.array([0, 100, 0, 0, 100, 100]) - sm2.z = np.array([0, 100, 0, 0, 100, 100]) - sm2.azimuth = np.array([180, 180, 180, 180, 180, 225]) - sm2.elevation = np.array([0, 0, 0, 0, 0, 0]) + sm2.geom.x_global = np.array([100, 100, 0, 100, 100, 100]) + sm2.geom.y_global = np.array([0, 0, 100, 100, 100, 100]) + sm2.geom.z_global = np.array([0, 100, 0, 0, 100, 100]) + sm2.geom.pointn_azim_global = np.array([180, 180, 180, 180, 180, 225]) + sm2.geom.pointn_elev_global = np.array([0, 0, 0, 0, 0, 0]) phi_ref = np.array([[0, 45, 90, 45, 54.73, 54.73]]) - npt.assert_allclose(phi_ref, sm1.get_off_axis_angle(sm2), atol=1e-2) + npt.assert_allclose(phi_ref, sm1.geom.get_off_axis_angle(sm2.geom), atol=1e-2) ####################################################################### sm3 = StationManager(1) - sm3.x = np.array([0]) - sm3.y = np.array([0]) - sm3.z = np.array([0]) - sm3.z = np.array([0]) - sm3.azimuth = np.array([45]) - sm3.elevation = np.array([0]) + sm3.geom.x_global = np.array([0]) + sm3.geom.y_global = np.array([0]) + sm3.geom.z_global = np.array([0]) + sm3.geom.pointn_azim_global = np.array([45]) + sm3.geom.pointn_elev_global = np.array([0]) sm4 = StationManager(2) - sm4.x = np.array([100, 60]) - sm4.y = np.array([100, 80]) - sm4.z = np.array([100, 100]) - sm4.z = np.array([100, 100]) - sm4.azimuth = np.array([180, 180]) - sm4.elevation = np.array([0, 0]) + sm4.geom.x_global = np.array([100, 60]) + sm4.geom.y_global = np.array([100, 80]) + sm4.geom.z_global = np.array([100, 100]) + sm4.geom.pointn_azim_global = np.array([180, 180]) + sm4.geom.pointn_elev_global = np.array([0, 0]) phi_ref = np.array([[35.26, 45.57]]) - npt.assert_allclose(phi_ref, sm3.get_off_axis_angle(sm4), atol=1e-2) + npt.assert_allclose(phi_ref, sm3.geom.get_off_axis_angle(sm4.geom), atol=1e-2) ####################################################################### sm5 = StationManager(1) - sm5.x = np.array([0]) - sm5.y = np.array([0]) - sm5.z = np.array([0]) - sm5.z = np.array([0]) - sm5.azimuth = np.array([0]) - sm5.elevation = np.array([45]) + sm5.geom.x_global = np.array([0]) + sm5.geom.y_global = np.array([0]) + sm5.geom.z_global = np.array([0]) + sm5.geom.pointn_azim_global = np.array([0]) + sm5.geom.pointn_elev_global = np.array([45]) sm6 = StationManager(2) - sm6.x = np.array([100, 100]) - sm6.y = np.array([0, 0]) - sm6.z = np.array([100, 100]) - sm6.z = np.array([100, 100]) - sm6.azimuth = np.array([180, 180]) - sm6.elevation = np.array([0, 0]) + sm6.geom.x_global = np.array([100, 100]) + sm6.geom.y_global = np.array([0, 0]) + sm6.geom.z_global = np.array([100, 100]) + sm6.geom.pointn_azim_global = np.array([180, 180]) + sm6.geom.pointn_elev_global = np.array([0, 0]) phi_ref = np.array([[0, 0]]) - npt.assert_allclose(phi_ref, sm5.get_off_axis_angle(sm6), atol=1e-2) + npt.assert_allclose(phi_ref, sm5.geom.get_off_axis_angle(sm6.geom), atol=1e-2) ####################################################################### sm6 = StationManager(1) - sm6.x = np.array([0]) - sm6.y = np.array([0]) - sm6.z = np.array([100]) - sm6.z = np.array([100]) - sm6.azimuth = np.array([0]) - sm6.elevation = np.array([270]) + sm6.geom.x_global = np.array([0]) + sm6.geom.y_global = np.array([0]) + sm6.geom.z_global = np.array([100]) + sm6.geom.pointn_azim_global = np.array([0]) + sm6.geom.pointn_elev_global = np.array([270]) sm7 = StationManager(2) - sm7.x = np.array([0, 100]) - sm7.y = np.array([0, 0]) - sm7.z = np.array([0, 0]) - sm7.z = np.array([0, 0]) - sm7.azimuth = np.array([180, 180]) - sm7.elevation = np.array([0, 0]) + sm7.geom.x_global = np.array([0, 100]) + sm7.geom.y_global = np.array([0, 0]) + sm7.geom.z_global = np.array([0, 0]) + sm7.geom.pointn_azim_global = np.array([180, 180]) + sm7.geom.pointn_elev_global = np.array([0, 0]) phi_ref = np.array([[0, 45]]) - npt.assert_allclose(phi_ref, sm6.get_off_axis_angle(sm7), atol=1e-2) + npt.assert_allclose(phi_ref, sm6.geom.get_off_axis_angle(sm7.geom), atol=1e-2) def test_elevation(self): """Test calculation of elevation angles between station managers.""" sm1 = StationManager(1) - sm1.x = np.array([0]) - sm1.y = np.array([0]) - sm1.z = np.array([10]) - sm1.z = np.array([10]) + sm1.geom.x_global = np.array([0]) + sm1.geom.y_global = np.array([0]) + sm1.geom.z_global = np.array([10]) sm2 = StationManager(6) - sm2.x = np.array([10, 10, 0, 0, 30, 20]) - sm2.y = np.array([0, 0, 5, 10, 30, 20]) - sm2.z = np.array([10, 20, 5, 0, 20, 20]) - sm2.z = np.array([10, 20, 5, 0, 20, 20]) + sm2.geom.x_global = np.array([10, 10, 0, 0, 30, 20]) + sm2.geom.y_global = np.array([0, 0, 5, 10, 30, 20]) + sm2.geom.z_global = np.array([10, 20, 5, 0, 20, 20]) elevation_ref = np.array([[0, 45, -45, -45, 13.26, 19.47]]) - npt.assert_allclose(elevation_ref, sm1.get_local_elevation(sm2), atol=1e-2) + npt.assert_allclose(elevation_ref, sm1.geom.get_local_elevation(sm2.geom), atol=1e-2) ####################################################################### sm3 = StationManager(2) - sm3.x = np.array([0, 30]) - sm3.y = np.array([0, 0]) - sm3.z = np.array([10, 10]) - sm3.z = np.array([10, 10]) + sm3.geom.x_global = np.array([0, 30]) + sm3.geom.y_global = np.array([0, 0]) + sm3.geom.z_global = np.array([10, 10]) sm4 = StationManager(2) - sm4.x = np.array([10, 10]) - sm4.y = np.array([0, 0]) - sm4.z = np.array([10, 20]) - sm4.z = np.array([10, 20]) + sm4.geom.x_global = np.array([10, 10]) + sm4.geom.y_global = np.array([0, 0]) + sm4.geom.z_global = np.array([10, 20]) elevation_ref = np.array([[0, 45], [0, 26.56]]) - npt.assert_allclose(elevation_ref, sm3.get_local_elevation(sm4), atol=1e-2) + npt.assert_allclose(elevation_ref, sm3.geom.get_local_elevation(sm4.geom), atol=1e-2) if __name__ == '__main__': diff --git a/tests/test_topology_imt_mss_dc.py b/tests/test_topology_imt_mss_dc.py index 50bc0767a..dced50b3c 100644 --- a/tests/test_topology_imt_mss_dc.py +++ b/tests/test_topology_imt_mss_dc.py @@ -107,18 +107,18 @@ def test_calculate_coordinates(self): # by default, satellites should always point to nadir (earth center) ref_earth_center = StationManager(1) - ref_earth_center.x = self.earth_center_x.flatten() - ref_earth_center.y = self.earth_center_y.flatten() - ref_earth_center.z = self.earth_center_z.flatten() + ref_earth_center.geom.x_global = self.earth_center_x.flatten() + ref_earth_center.geom.y_global = self.earth_center_y.flatten() + ref_earth_center.geom.z_global = self.earth_center_z.flatten() ref_space_stations = StationManager( self.imt_mss_dc_topology.num_base_stations) - ref_space_stations.x = self.imt_mss_dc_topology.space_station_x - ref_space_stations.y = self.imt_mss_dc_topology.space_station_y - ref_space_stations.z = self.imt_mss_dc_topology.space_station_z + ref_space_stations.geom.x_global = self.imt_mss_dc_topology.space_station_x + ref_space_stations.geom.y_global = self.imt_mss_dc_topology.space_station_y + ref_space_stations.geom.z_global = self.imt_mss_dc_topology.space_station_z - phi, theta = ref_space_stations.get_global_pointing_vector_to( - ref_earth_center) + phi, theta = ref_space_stations.geom.get_global_pointing_vector_to( + ref_earth_center.geom) npt.assert_array_almost_equal( np.squeeze( phi[center_beam_idxs]), From f06e5c5fc6b9de9f528d01aec162f4f6914440de Mon Sep 17 00:00:00 2001 From: artistrea Date: Tue, 7 Oct 2025 20:48:28 -0300 Subject: [PATCH 20/77] update: geometry readonly global properties & local<>global conversion --- sharc/satellite/scripts/plot_footprints.py | 16 +- .../scripts/plot_orbits_single_color_3d.py | 8 +- sharc/station_factory.py | 292 +++++++++++------- sharc/support/geometry.py | 210 +++++++++---- tests/test_adjacent_channel.py | 28 +- tests/test_geometry_converter.py | 7 +- tests/test_simulation_downlink.py | 102 +++--- tests/test_simulation_downlink_haps.py | 6 +- tests/test_simulation_downlink_tvro.py | 22 +- tests/test_simulation_indoor.py | 9 +- tests/test_simulation_uplink.py | 48 ++- tests/test_station_factory.py | 16 +- tests/test_station_factory_ngso.py | 18 +- tests/test_station_manager.py | 182 ++++++----- tests/test_topology_imt_mss_dc.py | 16 +- 15 files changed, 625 insertions(+), 355 deletions(-) diff --git a/sharc/satellite/scripts/plot_footprints.py b/sharc/satellite/scripts/plot_footprints.py index 2689b79ee..b1ab2397e 100644 --- a/sharc/satellite/scripts/plot_footprints.py +++ b/sharc/satellite/scripts/plot_footprints.py @@ -82,9 +82,11 @@ def plot_fp( center_of_earth = StationManager(1) # rotated and then translated center of earth - center_of_earth.geom.x_global = np.array([0.0]) - center_of_earth.geom.y_global = np.array([0.0]) - center_of_earth.geom.z_global = np.array([-coord_sys.get_translation()]) + center_of_earth.geom.set_global_coords( + np.array([0.0]), + np.array([0.0]), + np.array([-coord_sys.get_translation()]), + ) mss_d2d_manager = StationFactory.generate_mss_d2d(params, rng, coord_sys) @@ -159,9 +161,11 @@ def plot_fp( # creates a StationManager to calculate the gains on surf_manager = StationManager(len(x_flat)) - surf_manager.geom.x_global = x_flat - surf_manager.geom.y_global = y_flat - surf_manager.geom.z_global = z_flat + surf_manager.geom.set_global_coords( + x_flat, + y_flat, + z_flat, + ) station_1 = mss_d2d_manager mss_active = np.where(station_1.active)[0] diff --git a/sharc/satellite/scripts/plot_orbits_single_color_3d.py b/sharc/satellite/scripts/plot_orbits_single_color_3d.py index 0417d705d..f41c56a2a 100644 --- a/sharc/satellite/scripts/plot_orbits_single_color_3d.py +++ b/sharc/satellite/scripts/plot_orbits_single_color_3d.py @@ -77,9 +77,11 @@ center_of_earth = StationManager(1) # rotated and then translated center of earth - center_of_earth.geom.x_global = np.array([0.0]) - center_of_earth.geom.y_global = np.array([0.0]) - center_of_earth.geom.z_global = np.array([-coord_sys.get_translation()]) + center_of_earth.geom.set_global_coords( + np.array([0.0]), + np.array([0.0]), + np.array([-coord_sys.get_translation()]), + ) vis_elevation = [] for _ in range(NUM_DROPS): diff --git a/sharc/station_factory.py b/sharc/station_factory.py index 9a3ca6400..94974f863 100644 --- a/sharc/station_factory.py +++ b/sharc/station_factory.py @@ -97,27 +97,35 @@ def generate_imt_base_stations( imt_base_stations = StationManager(num_bs) imt_base_stations.station_type = StationType.IMT_BS if param.topology.type == "NTN": - imt_base_stations.geom.x_global = topology.space_station_x * np.ones(num_bs) - imt_base_stations.geom.y_global = topology.space_station_y * np.ones(num_bs) - imt_base_stations.geom.z_global = topology.space_station_z * np.ones(num_bs) - imt_base_stations.geom.pointn_elev_global = topology.elevation + imt_base_stations.geom.set_global_coords( + topology.space_station_x * np.ones(num_bs), + topology.space_station_y * np.ones(num_bs), + topology.space_station_z * np.ones(num_bs), + elev=topology.elevation + ) imt_base_stations.is_space_station = True elif param.topology.type == "MSS_DC": - imt_base_stations.geom.x_global = topology.space_station_x * np.ones(num_bs) - imt_base_stations.geom.y_global = topology.space_station_y * np.ones(num_bs) - imt_base_stations.geom.z_global = topology.space_station_z * np.ones(num_bs) - imt_base_stations.geom.pointn_elev_global = topology.elevation + imt_base_stations.geom.set_global_coords( + topology.space_station_x * np.ones(num_bs), + topology.space_station_y * np.ones(num_bs), + topology.space_station_z * np.ones(num_bs), + elev=topology.elevation + ) imt_base_stations.is_space_station = True else: - imt_base_stations.geom.x_global = topology.x - imt_base_stations.geom.y_global = topology.y - imt_base_stations.geom.pointn_elev_global = -param_ant.downtilt * np.ones(num_bs) + imt_base_stations.geom.set_local_coords( + topology.x * np.ones(num_bs), + topology.y * np.ones(num_bs), + param.bs.height * np.ones(num_bs), + elev=-param_ant.downtilt * np.ones(num_bs) + ) if param.topology.type == 'INDOOR': - imt_base_stations.geom.z_global = topology.height - else: - imt_base_stations.geom.z_global = param.bs.height * np.ones(num_bs) - - imt_base_stations.geom.pointn_azim_global = wrap2_180(topology.azimuth) + imt_base_stations.geom.set_local_coords( + z=topology.height + ) + imt_base_stations.geom.set_local_coords( + azim=wrap2_180(topology.azimuth) + ) imt_base_stations.active = random_number_gen.rand( num_bs, ) < param.bs.load_probability @@ -296,7 +304,9 @@ def generate_imt_ue_outdoor( ue_y = list() ue_z = list() - imt_ue.geom.z_global = param.ue.height * np.ones(num_ue) + imt_ue.geom.set_local_coords( + z=param.ue.height * np.ones(num_ue) + ) # TODO: Sanitaze the azimuth_range parameter azimuth_range = param.ue.azimuth_range @@ -339,9 +349,10 @@ def generate_imt_ue_outdoor( np.arctan((param.bs.height - param.ue.height) / distance), ) - # FIXME: adapt this to also work with local coords (and space stations) - imt_ue.geom.pointn_azim_global = (azimuth + theta + np.pi / 2) - imt_ue.geom.pointn_elev_global = elevation + psi + imt_ue.geom.set_local_coords( + azim=(azimuth + theta + np.pi / 2), + elev=elevation + psi, + ) elif param.ue.distribution_type.upper() == "ANGLE_AND_DISTANCE": # The Rayleigh and Normal distribution parameters (mean, scale and cutoff) @@ -407,6 +418,9 @@ def generate_imt_ue_outdoor( ) sys.exit(1) + ue_azims = np.zeros_like(imt_ue.geom.pointn_azim_global) + ue_elevs = np.zeros_like(imt_ue.geom.pointn_elev_global) + for bs in range(num_bs): idx = [ i for i in range( @@ -428,8 +442,7 @@ def generate_imt_ue_outdoor( ue_z.extend(z) # calculate UE azimuth wrt serving BS - # FIXME: adapt this to also work with local coords (and space stations) - imt_ue.geom.pointn_azim_global[idx] = (azimuth[idx] + theta + 180) % 360 + ue_azims[idx] = (azimuth[idx] + theta + 180) % 360 # calculate elevation angle # psi is the vertical angle of the UE wrt the serving BS @@ -439,8 +452,11 @@ def generate_imt_ue_outdoor( psi = np.degrees( np.arctan((param.bs.height - param.ue.height) / distance), ) - # FIXME: adapt this to also work with space stations - imt_ue.geom.pointn_elev_global[idx] = elevation[idx] + psi + ue_elevs[idx] = elevation[idx] + psi + imt_ue.geom.set_local_coords( + azim=ue_azims, + elev=ue_elevs, + ) else: sys.stderr.write( @@ -449,9 +465,11 @@ def generate_imt_ue_outdoor( ) sys.exit(1) - imt_ue.geom.x_global = np.array(ue_x) - imt_ue.geom.y_global = np.array(ue_y) - imt_ue.geom.z_global = np.array(ue_z) + param.ue.height + imt_ue.geom.set_local_coords( + np.array(ue_x), + np.array(ue_y), + np.array(ue_z) + param.ue.height, + ) imt_ue.active = np.zeros(num_ue, dtype=bool) imt_ue.indoor = random_number_gen.random_sample( @@ -559,6 +577,8 @@ def generate_imt_ue_indoor( topology.b_d / math.sqrt(topology.ue_indoor_percent) - topology.b_d ) / 2 + ue_azims = np.zeros_like(imt_ue.geom.pointn_azim_global) + ue_elevs = np.zeros_like(imt_ue.geom.pointn_elev_global) for bs in range(num_bs): idx = [ i for i in range( @@ -606,7 +626,7 @@ def generate_imt_ue_indoor( ), ) # calculate UE azimuth wrt serving BS - imt_ue.geom.pointn_azim_global[idx] = (azimuth[idx] + theta + 180) % 360 + ue_azims[idx] = (azimuth[idx] + theta + 180) % 360 # calculate elevation angle # psi is the vertical angle of the UE wrt the serving BS @@ -616,7 +636,7 @@ def generate_imt_ue_indoor( psi = np.degrees( np.arctan((param.bs.height - param.ue.height) / distance), ) - imt_ue.geom.pointn_elev_global[idx] = elevation[idx] + psi + ue_elevs[idx] = elevation[idx] + psi # check if UE is indoor if bs % topology.num_cells == 0: @@ -632,9 +652,13 @@ def generate_imt_ue_indoor( (y < topology.y[bs] - topology.b_d / 2) imt_ue.indoor[idx] = ~ out - imt_ue.geom.x_global = np.array(ue_x) - imt_ue.geom.y_global = np.array(ue_y) - imt_ue.geom.z_global = np.array(ue_z) + imt_ue.geom.set_local_coords( + np.array(ue_x), + np.array(ue_y), + np.array(ue_z), + np.array(ue_azims), + np.array(ue_elevs), + ) imt_ue.active = np.zeros(num_ue, dtype=bool) imt_ue.rx_interference = -500 * np.ones(num_ue) @@ -777,13 +801,14 @@ def generate_single_space_station( param.geometry.location.fixed.long_deg, param.geometry.altitude, ) - - space_station.geom.x_global = x - space_station.geom.y_global = y - space_station.geom.z_global = z + space_station.geom.set_global_coords( + np.atleast_1d(x), + np.atleast_1d(y), + np.atleast_1d(z), + ) if param.geometry.azimuth.type == "POINTING_AT_IMT": - space_station.geom.pointn_azim_global = np.rad2deg( + azim = np.rad2deg( np.arctan2(-space_station.geom.y_global, -space_station.geom.x_global)) elif param.geometry.azimuth.type == "POINTING_AT_LAT_LONG_ALT": px, py, pz = coord_sys.lla2enu( @@ -792,10 +817,10 @@ def generate_single_space_station( param.geometry.pointing_at_alt, ) - space_station.geom.pointn_azim_global = np.rad2deg( + azim = np.rad2deg( np.arctan2(py - space_station.geom.y_global, px - space_station.geom.x_global)) elif param.geometry.azimuth.type == "FIXED": - space_station.geom.pointn_azim_global = param.geometry.azimuth.fixed + azim = param.geometry.azimuth.fixed else: raise ValueError( f"Did not recognize azimuth type of { @@ -810,7 +835,7 @@ def generate_single_space_station( space_station.geom.y_global + space_station.geom.x_global * space_station.geom.x_global))) - space_station.geom.pointn_elev_global = -gnd_elev + elev = -gnd_elev elif param.geometry.elevation.type == "POINTING_AT_LAT_LONG_ALT": px, py, pz = coord_sys.lla2enu( param.geometry.pointing_at_lat, @@ -825,14 +850,19 @@ def generate_single_space_station( np.arctan2( dz, np.sqrt(dy * dy + dx * dx))) - space_station.geom.pointn_elev_global = gnd_elev + elev = gnd_elev elif param.geometry.elevation.type == "FIXED": - space_station.geom.pointn_elev_global = param.geometry.elevation.fixed + elev = param.geometry.elevation.fixed else: raise ValueError( f"Did not recognize elevation type of { param.geometry.elevation.type}") + space_station.geom.set_global_coords( + azim=np.atleast_1d(azim), + elev=np.atleast_1d(elev), + ) + space_station.active = np.array([True]) space_station.tx_power = np.array( [param.tx_power_density + 10 * @@ -891,19 +921,22 @@ def generate_fss_space_station(param: ParametersFssSs): # rotate axis and calculate coordinates with origin at IMT system imt_lat_rad = param.earth_station_lat_deg * np.pi / 180. - fss_space_station.geom.x_global = np.array( + x = np.array( [x1 * np.sin(imt_lat_rad) - z1 * np.cos(imt_lat_rad)], ) * 1000 - fss_space_station.geom.y_global = np.array([y1]) * 1000 - fss_space_station.geom.z_global = np.array([ + y = np.array([y1]) * 1000 + z = np.array([ ( z1 * np.sin(imt_lat_rad) + x1 * np.cos(imt_lat_rad) - dist_imt_centre_earth_km ) * 1000, ]) - - fss_space_station.geom.pointn_azim_global = np.array([param.azimuth]) - fss_space_station.geom.pointn_elev_global = np.array([param.elevation]) + azim = np.array([param.azimuth]) + elev = np.array([param.elevation]) + fss_space_station.geom.set_global_coords( + x, y, z, + azim, elev + ) fss_space_station.active = np.array([True]) fss_space_station.tx_power = np.array( @@ -974,22 +1007,22 @@ def generate_fss_earth_station( fss_earth_station.station_type = StationType.FSS_ES if param.location.upper() == "FIXED": - fss_earth_station.geom.x_global = np.array([param.x]) - fss_earth_station.geom.y_global = np.array([param.y]) + x = np.array([param.x]) + y = np.array([param.y]) elif param.location.upper() == "CELL": x, y, _, _ = StationFactory.get_random_position( 1, topology, random_number_gen, param.min_dist_to_bs, True, ) - fss_earth_station.geom.x_global = np.array(x) - fss_earth_station.geom.y_global = np.array(y) + x = np.array(x) + y = np.array(y) elif param.location.upper() == "NETWORK": x, y, _, _ = StationFactory.get_random_position( 1, topology, random_number_gen, param.min_dist_to_bs, False, ) - fss_earth_station.geom.x_global = np.array(x) - fss_earth_station.geom.y_global = np.array(y) + x = np.array(x) + y = np.array(y) elif param.location.upper() == "UNIFORM_DIST": # FSS ES is randomly (uniform) created inside a circle of radius # equal to param.max_dist_to_bs @@ -1009,8 +1042,8 @@ def generate_fss_earth_station( if (radius > param.min_dist_to_bs) & ( radius < param.max_dist_to_bs): break - fss_earth_station.geom.x_global[0] = dist_x - fss_earth_station.geom.y_global[0] = dist_y + x = np.array(dist_x) + y = np.array(dist_y) else: sys.stderr.write( "ERROR\nFSS-ES location type {} not supported".format( @@ -1018,18 +1051,27 @@ def generate_fss_earth_station( ) sys.exit(1) - fss_earth_station.geom.z_global = np.array([param.height]) + z = np.array([param.height]) + + fss_earth_station.geom.set_global_coords( + x, y, z + ) if param.azimuth.upper() == "RANDOM": - fss_earth_station.geom.pointn_azim_global = np.array( + azim = np.array( [random_number_gen.uniform(-180., 180.)]) else: - fss_earth_station.geom.pointn_azim_global = np.array([float(param.azimuth)]) + azim = np.array([float(param.azimuth)]) elevation = random_number_gen.uniform( param.elevation_min, param.elevation_max, ) - fss_earth_station.geom.pointn_elev_global = np.array([elevation]) + elev = np.array([elevation]) + + fss_earth_station.geom.set_global_coords( + azim=azim, + elev=elev, + ) fss_earth_station.active = np.array([True]) fss_earth_station.tx_power = np.array( @@ -1099,10 +1141,10 @@ def generate_single_earth_station( match param.geometry.location.type: case "FIXED": - single_earth_station.geom.x_global = np.array( + x = np.array( [param.geometry.location.fixed.x], ) - single_earth_station.geom.y_global = np.array( + y = np.array( [param.geometry.location.fixed.y], ) case "CELL": @@ -1110,15 +1152,15 @@ def generate_single_earth_station( 1, topology, random_number_gen, param.geometry.location.cell.min_dist_to_bs, True, ) - single_earth_station.geom.x_global = np.array(x) - single_earth_station.geom.y_global = np.array(y) + x = np.array(x) + y = np.array(y) case "NETWORK": x, y, _, _ = StationFactory.get_random_position( 1, topology, random_number_gen, param.geometry.location.network.min_dist_to_bs, False, ) - single_earth_station.geom.x_global = np.array(x) - single_earth_station.geom.y_global = np.array(y) + x = np.array(x) + y = np.array(y) case "UNIFORM_DIST": # ES is randomly (uniform) created inside a circle of radius # equal to param.max_dist_to_bs @@ -1140,8 +1182,8 @@ def generate_single_earth_station( if (radius > param.geometry.location.uniform_dist.min_dist_to_bs) & ( radius < param.geometry.location.uniform_dist.max_dist_to_bs): break - single_earth_station.geom.x_global[0] = dist_x - single_earth_station.geom.y_global[0] = dist_y + x = np.array(dist_x) + y = np.array(dist_y) case _: sys.stderr.write( "ERROR\nSingle-ES location type {} not supported".format( @@ -1149,7 +1191,11 @@ def generate_single_earth_station( ) sys.exit(1) - single_earth_station.geom.z_global = np.array([param.geometry.height]) + z = np.array([param.geometry.height]) + + single_earth_station.geom.set_global_coords( + x, y, z + ) if param.geometry.azimuth.type == "UNIFORM_DIST": if param.geometry.azimuth.uniform_dist.min < -180: @@ -1165,27 +1211,32 @@ def generate_single_earth_station( ), ) sys.exit(1) - single_earth_station.geom.pointn_azim_global = np.array([ + azim = np.array([ random_number_gen.uniform( param.geometry.azimuth.uniform_dist.min, param.geometry.azimuth.uniform_dist.max, ), ]) else: - single_earth_station.geom.pointn_azim_global = np.array( + azim = np.array( [param.geometry.azimuth.fixed], ) if param.geometry.elevation.type == "UNIFORM_DIST": - single_earth_station.geom.pointn_elev_global = np.array([ + elev = np.array([ random_number_gen.uniform( param.geometry.elevation.uniform_dist.min, param.geometry.elevation.uniform_dist.max, ), ]) else: - single_earth_station.geom.pointn_elev_global = np.array( + elev = np.array( [param.geometry.elevation.fixed], ) + single_earth_station.geom.set_global_coords( + azim=azim, + elev=elev, + ) + single_earth_station.antenna = np.array([ AntennaFactory.create_antenna( param.antenna, @@ -1252,12 +1303,13 @@ def generate_fs_station(param: ParametersFs): fs_station = StationManager(1) fs_station.station_type = StationType.FS - fs_station.geom.x_global = np.array([param.x]) - fs_station.geom.y_global = np.array([param.y]) - fs_station.geom.z_global = np.array([param.height]) - - fs_station.geom.pointn_azim_global = np.array([param.azimuth]) - fs_station.geom.pointn_elev_global = np.array([param.elevation]) + fs_station.geom.set_global_coords( + np.array([param.x]), + np.array([param.y]), + np.array([param.height]), + np.array([param.azimuth]), + np.array([param.elevation]), + ) fs_station.active = np.array([True]) fs_station.tx_power = np.array( @@ -1311,15 +1363,20 @@ def generate_haps( # h = (d/3)*math.sqrt(3)/2 # haps.x = np.array([0, 7*d/2, -d/2, -4*d, -7*d/2, d/2, 4*d]) # haps.y = np.array([0, 9*h, 15*h, 6*h, -9*h, -15*h, -6*h]) - haps.geom.x_global = np.array([0]) - haps.geom.y_global = np.array([0]) - haps.geom.z_global = param.altitude * np.ones(num_haps) elev_max = 68.19 # corresponds to 50 km radius and 20 km altitude - haps.geom.pointn_azim_global = 360 * random_number_gen.random_sample(num_haps) - haps.geom.pointn_elev_global = ((270 + elev_max) - (270 - elev_max)) * \ + azim = 360 * random_number_gen.random_sample(num_haps) + elev = ((270 + elev_max) - (270 - elev_max)) * \ random_number_gen.random_sample(num_haps) + (270 - elev_max) + haps.geom.set_global_coords( + np.zeros(num_haps), + np.zeros(num_haps), + param.altitude * np.ones(num_haps), + azim, + elev, + ) + haps.active = np.ones(num_haps, dtype=bool) haps.antenna = np.empty(num_haps, dtype=Antenna) @@ -1364,19 +1421,24 @@ def generate_rns( rns.station_type = StationType.RNS rns.is_space_station = True - rns.geom.x_global = np.array([param.x]) - rns.geom.y_global = np.array([param.y]) - rns.geom.z_global = np.array([param.altitude]) + x = np.array([param.x]) + y = np.array([param.y]) + z = np.array([param.altitude]) # minimum and maximum values for azimuth and elevation azimuth = np.array([-30, 30]) elevation = np.array([-30, 5]) - rns.geom.pointn_azim_global = 90 + (azimuth[1] - azimuth[0]) * \ + azim = 90 + (azimuth[1] - azimuth[0]) * \ random_number_gen.random_sample(num_rns) + azimuth[0] - rns.geom.pointn_elev_global = (elevation[1] - elevation[0]) * \ + elev = (elevation[1] - elevation[0]) * \ random_number_gen.random_sample(num_rns) + elevation[0] + rns.geom.set_global_coords( + x, y, z, + azim, elev, + ) + rns.active = np.ones(num_rns, dtype=bool) if param.antenna_pattern == "OMNI": @@ -1481,18 +1543,23 @@ def generate_space_station( # Elevation at ground (centre of the footprint) theta_grd_elev = 90 - incidence_angle - space_station.geom.x_global = np.array([0]) - space_station.geom.y_global = np.array( + x = np.array([0]) + y = np.array( [distance * math.cos(math.radians(theta_grd_elev))], ) - space_station.geom.z_global = np.array( + z = np.array( [distance * math.sin(math.radians(theta_grd_elev))], ) # Elevation and azimuth at sensor wrt centre of the footprint # It is assumed the sensor is at y-axis, hence azimuth is 270 deg - space_station.geom.pointn_azim_global = np.array([270]) - space_station.geom.pointn_elev_global = np.array([-theta_grd_elev]) + azim = np.array([270]) + elev = np.array([-theta_grd_elev]) + + space_station.geom.set_global_coords( + x, y, z, + azim, elev + ) space_station.active = np.array([True]) space_station.rx_interference = np.array([-500]) @@ -1551,12 +1618,17 @@ def generate_mss_ss(param_mss: ParametersMssSs): num_bs = ntn_topology.num_base_stations mss_ss = StationManager(n=num_bs) mss_ss.station_type = StationType.MSS_SS - mss_ss.geom.x_global = ntn_topology.space_station_x * np.ones(num_bs) + param_mss.x - mss_ss.geom.y_global = ntn_topology.space_station_y * np.ones(num_bs) + param_mss.y - mss_ss.geom.z_global = ntn_topology.space_station_z * np.ones(num_bs) - mss_ss.geom.pointn_elev_global = ntn_topology.elevation + x = ntn_topology.space_station_x * np.ones(num_bs) + param_mss.x + y = ntn_topology.space_station_y * np.ones(num_bs) + param_mss.y + z = ntn_topology.space_station_z * np.ones(num_bs) + elev = ntn_topology.elevation + azim = ntn_topology.azimuth + mss_ss.geom.set_global_coords( + x, y, z, + azim, elev + ) + mss_ss.is_space_station = True - mss_ss.geom.pointn_azim_global = ntn_topology.azimuth mss_ss.active = np.ones(num_bs, dtype=int) mss_ss.tx_power = np.ones( num_bs, dtype=int) * param_mss.tx_power_density + 10 * np.log10( @@ -1670,11 +1742,15 @@ def generate_mss_d2d( ) # Configure satellite positions in the StationManager - mss_d2d.geom.x_global = mss_d2d_values["sat_x"] - mss_d2d.geom.y_global = mss_d2d_values["sat_y"] - mss_d2d.geom.z_global = mss_d2d_values["sat_z"] - mss_d2d.geom.pointn_elev_global = mss_d2d_values["sat_antenna_elev"] - mss_d2d.geom.pointn_azim_global = mss_d2d_values["sat_antenna_azim"] + x = mss_d2d_values["sat_x"] + y = mss_d2d_values["sat_y"] + z = mss_d2d_values["sat_z"] + elev = mss_d2d_values["sat_antenna_elev"] + azim = mss_d2d_values["sat_antenna_azim"] + mss_d2d.geom.set_global_coords( + x, y, z, + azim, elev, + ) mss_d2d.active = np.zeros(total_satellites, dtype=bool) @@ -1933,9 +2009,11 @@ def get_random_position(num_stas: int, # TODO: replace this with generate imt mss dc station st = StationManager(topology.num_base_stations) - st.geom.x_global = topology.space_station_x - st.geom.y_global = topology.space_station_y - st.geom.z_global = topology.space_station_z + st.geom.set_global_coords( + topology.space_station_x, + topology.space_station_y, + topology.space_station_z, + ) fig.add_trace( go.Scatter3d( diff --git a/sharc/support/geometry.py b/sharc/support/geometry.py index b6e74051a..dd5dd854a 100644 --- a/sharc/support/geometry.py +++ b/sharc/support/geometry.py @@ -1,6 +1,8 @@ # from sharc.support.sharc_geom import CoordinateSystem # from sharc.satellite.utils.sat_utils import lla2ecef +from sharc.satellite.ngso.constants import EARTH_RADIUS_M +import scipy import numpy as np from abc import ABC @@ -22,11 +24,11 @@ def getter(self, name=private_name): # TODO: make these properties readonly -# @readonly_properties( -# "x_global", "y_global", "z_global", -# "pointn_azim_global", "pointn_elev_global", -# "num_geometries" -# ) +@readonly_properties( + "x_global", "y_global", "z_global", + "pointn_azim_global", "pointn_elev_global", + "num_geometries" +) class GlobalGeometry(ABC): """ Abstract class defining global simulator geometry implementation. @@ -50,13 +52,13 @@ def setup( """ Initializes variables based on number of geometries """ - self.x_global = np.empty(num_geometries) - self.y_global = np.empty(num_geometries) - self.z_global = np.empty(num_geometries) - self.pointn_azim_global = np.empty(num_geometries) - self.pointn_elev_global = np.empty(num_geometries) + self._x_global = np.empty(num_geometries) + self._y_global = np.empty(num_geometries) + self._z_global = np.empty(num_geometries) + self._pointn_azim_global = np.empty(num_geometries) + self._pointn_elev_global = np.empty(num_geometries) - self.num_geometries = num_geometries + self._num_geometries = num_geometries def set_global_coords( self, @@ -70,15 +72,15 @@ def set_global_coords( If None is passed, attribute will not be changed. """ if x is not None: - self.x_global = x + self._x_global = x if y is not None: - self.y_global = y + self._y_global = y if z is not None: - self.z_global = z + self._z_global = z if elev is not None: - self.pointn_elev_global = elev + self._pointn_elev_global = elev if azim is not None: - self.pointn_azim_global = azim + self._pointn_azim_global = azim def get_global_distance_to(self, other: "GlobalGeometry") -> np.array: """Calculate the 2D distance between this geometry and another @@ -336,7 +338,6 @@ def __init__( Initialize a geometry object with a global coordinate system and defining how many local coordinate systems should exist """ - # super().__init__(num_geometries) self.setup( num_geometries, num_of_local_refs, @@ -359,11 +360,11 @@ def setup( if num_of_local_refs == 0: self.uses_local_coords = False - self._x_local = None - self._y_local = None - self._z_local = None - self._pointn_azim_local = None - self._pointn_elev_local = None + self._x_local = self._x_global + self._y_local = self._y_global + self._z_local = self._z_global + self._pointn_azim_local = self._pointn_azim_global + self._pointn_elev_local = self._pointn_elev_global return elif global_cs is None: raise ValueError( @@ -383,7 +384,8 @@ def set_local_coord_sys( ref_alts, ): """ - + Sets local coord system references and prepares + global<->local coordinate transformation """ for r in [ref_lats, ref_lons, ref_alts]: if len(r) != self._num_of_local_refs: @@ -393,6 +395,30 @@ def set_local_coord_sys( ) self.__local_lla_references = np.stack((ref_lats, ref_lons, ref_alts)) + self._compute_global_local_transform() + + def set_global_coords( + self, + x=None, + y=None, + z=None, + azim=None, + elev=None, + ): + """Set passed values to objects global coordinates. If geometry does not have local reference + it will be assumed to be the same as global reference, with local == global + If None is passed, attribute will not be changed. + """ + super().set_global_coords( + x=x, + y=y, + z=z, + azim=azim, + elev=elev, + ) + + self._compute_local_from_global() + def set_local_coords( self, x=None, @@ -401,45 +427,118 @@ def set_local_coords( azim=None, elev=None, ): - """Set values to local coordinate values. + """Set values to local coordinate values. If geometry does not have local reference + it will be assumed to be the same as global reference, with local == global If None is passed, attribute will not be updated. """ if x is not None: - self.x_local = x + self._x_local = x if y is not None: - self.y_local = y + self._y_local = y if z is not None: - self.z_local = z + self._z_local = z if elev is not None: - self.pointn_elev_local = elev + self._pointn_elev_local = elev if azim is not None: - self.pointn_azim_local = azim - - # def _compute_local_to_global_transf(self): - # return - # local_lat, local_lon, local_alt = self.__local_lla_references - # rotation_around_z = -local_lon - 90 - # rotation_around_x = local_lat - 90 - - # self.rotation = scipy.spatial.transform.Rotation.from_euler( - # 'zx', - # np.stack([rotation_around_z, rotation_around_x], axis=-1), - # degrees=True - # ) - # # (M,3,3) or (3,3) - # rot_mtx = self.rotation.as_matrix() - # if rot_mtx.ndim == 2: - # # guarantee (M, 3, 3) - # rot_mtx = rot_mtx[None, ...] - # # broadcastable (M, 1, 3, 3) - # self.rotation_mtx = rot_mtx[:, None, :, :] - - # inv_rot_mtx = self.rotation.inv().as_matrix() - # if inv_rot_mtx.ndim == 2: - # # guarantee (M, 3, 3) - # inv_rot_mtx = inv_rot_mtx[None, ...] - # # broadcastable (M, 1, 3, 3) - # self.inv_rotation_mtx = inv_rot_mtx[:, None, :, :] + self._pointn_azim_local = azim + + self._compute_global_from_local() + + def _compute_global_from_local(self): + if not self.uses_local_coords: + self._x_global = self._x_local + self._y_global = self._y_local + self._z_global = self._z_local + self._pointn_elev_global = self._pointn_elev_local + self._pointn_azim_global = self._pointn_azim_local + return + + p_local = np.stack([self.x_local, self.y_local, self.z_local], axis=-1) + + # Translation happens first (before rotation) + p_local = p_local + self.local2ecef_transl_mtx # (N,3) + + p_global = (self.local2global_rot_mtx @ p_local[..., None]).squeeze(-1) # (N,3) + + # translation happens after rotation + p_global = p_global + self.ecef2global_transl_mtx # (N,3) + + # Store results + self._x_global = p_global[:, 0] + self._y_global = p_global[:, 1] + self._z_global = p_global[:, 2] + + def _compute_local_from_global(self): + if not self.uses_local_coords: + self._x_local = self._x_global + self._y_local = self._y_global + self._z_local = self._z_global + self._pointn_elev_local = self._pointn_elev_global + self._pointn_azim_local = self._pointn_azim_global + return + + p_global = np.stack([self.x_global, self.y_global, self.z_global], axis=-1) + + # Translation happens first (before rotation) + p_global = p_global + self.global2ecef_transl_mtx # (N,3) + + p_local = (self.global2local_rot_mtx @ p_global[..., None]).squeeze(-1) # (N,3) + + # translation happens after rotation + p_local = p_local + self.ecef2local_transl_mtx # (N,3) + + # Store results + self._x_local = p_local[:, 0] + self._y_local = p_local[:, 1] + self._z_local = p_local[:, 2] + + def _compute_global_local_transform(self): + # get ecef to local + local_lat, local_lon, local_alt = self.__local_lla_references + rotation_around_z = -local_lon - 90 + rotation_around_x = local_lat - 90 + + ecef2local_rot = scipy.spatial.transform.Rotation.from_euler( + 'zx', + np.stack([rotation_around_z, rotation_around_x], axis=-1), + degrees=True + ) + + # first translation, after rotation + self.local2ecef_transl_mtx = np.zeros((self._num_of_local_refs, 3)) + # NOTE: works because of spherical Earth + self.local2ecef_transl_mtx[:, 2] = local_alt + EARTH_RADIUS_M + local2ecef_rot_mtx = ecef2local_rot.inv().as_matrix() + + # first rotation, after translation + ecef2local_rot_mtx = ecef2local_rot.as_matrix() + self.ecef2local_transl_mtx = -self.local2ecef_transl_mtx + + # global transforms + global_lat, global_lon, global_alt = self.__global_coord_sys + rotation_around_z = -global_lon - 90 + rotation_around_x = global_lat - 90 + + ecef2global_rot = scipy.spatial.transform.Rotation.from_euler( + 'zx', + np.stack([rotation_around_z, rotation_around_x], axis=-1), + degrees=True + ) + + # first translation, after rotation + self.global2ecef_transl_mtx = np.zeros((1, 3)) + # NOTE: works because of spherical Earth + self.global2ecef_transl_mtx[:, 2] = global_alt + EARTH_RADIUS_M + global2ecef_rot_mtx = ecef2global_rot.inv().as_matrix() + + # first rotation, after translation + ecef2global_rot_mtx = ecef2global_rot.as_matrix() + self.ecef2global_transl_mtx = -self.global2ecef_transl_mtx + + self.local2global_rot_mtx = ecef2global_rot_mtx @ local2ecef_rot_mtx + + self.global2local_rot_mtx = ecef2local_rot_mtx @ global2ecef_rot_mtx + def get_local_distance_to(self, other: "SimulatorGeometry") -> np.array: """Calculate the 2D distance between this manager's stations and another's @@ -480,4 +579,3 @@ def get_local_elevation(self, other: "SimulatorGeometry") -> np.array: raise NotImplementedError() -# \.(get_global_distance_to|get_3d_distance_to|get_global_dist_angles_wrap_around|get_global_elevation|get_global_pointing_vector_to|get_off_axis_angle) diff --git a/tests/test_adjacent_channel.py b/tests/test_adjacent_channel.py index 016214b83..072f791e9 100644 --- a/tests/test_adjacent_channel.py +++ b/tests/test_adjacent_channel.py @@ -175,8 +175,10 @@ def test_simulation_2bs_4ue_downlink(self): self.simulation.topology, random_number_gen, ) - self.simulation.ue.geom.x_global = np.array([20, 70, 110, 170]) - self.simulation.ue.geom.y_global = np.array([0, 0, 0, 0]) + self.simulation.ue.geom.set_global_coords( + np.array([20, 70, 110, 170]), + np.array([0, 0, 0, 0]), + ) self.simulation.ue.antenna = np.array( [AntennaOmni(10), AntennaOmni(11), AntennaOmni(22), AntennaOmni(23)], ) @@ -243,9 +245,11 @@ def test_simulation_2bs_4ue_downlink(self): self.simulation.system = StationFactory.generate_fss_space_station( self.param.fss_ss, ) - self.simulation.system.geom.x_global = np.array([0.01]) # avoids zero-division - self.simulation.system.geom.y_global = np.array([0]) - self.simulation.system.geom.z_global = np.array([self.param.fss_ss.altitude]) + self.simulation.system.geom.set_global_coords( + np.array([0.01]), # avoids zero-division + np.array([0]), + np.array([self.param.fss_ss.altitude]), + ) # test the method that calculates interference from IMT UE to FSS space # station @@ -312,8 +316,10 @@ def test_simulation_2bs_4ue_uplink(self): self.simulation.topology, random_number_gen, ) - self.simulation.ue.geom.x_global = np.array([20, 70, 110, 170]) - self.simulation.ue.geom.y_global = np.array([0, 0, 0, 0]) + self.simulation.ue.geom.set_global_coords( + np.array([20, 70, 110, 170]), + np.array([0, 0, 0, 0]), + ) self.simulation.ue.antenna = np.array( [AntennaOmni(10), AntennaOmni(11), AntennaOmni(22), AntennaOmni(23)], ) @@ -377,9 +383,11 @@ def test_simulation_2bs_4ue_uplink(self): self.simulation.system = StationFactory.generate_fss_space_station( self.param.fss_ss, ) - self.simulation.system.geom.x_global = np.array([0]) - self.simulation.system.geom.y_global = np.array([0]) - self.simulation.system.geom.z_global = np.array([self.param.fss_ss.altitude]) + self.simulation.system.geom.set_global_coords( + np.array([0.01]), # avoids zero-division + np.array([0]), + np.array([self.param.fss_ss.altitude]), + ) # test the method that calculates interference from IMT UE to FSS space # station diff --git a/tests/test_geometry_converter.py b/tests/test_geometry_converter.py index 8ed0fa6c5..bd0cb7de9 100644 --- a/tests/test_geometry_converter.py +++ b/tests/test_geometry_converter.py @@ -125,9 +125,10 @@ def test_station_converter(self): azim_bef = rng.uniform(-180, 180, n_samples) elev_bef = rng.uniform(-90, 90, n_samples) - stations.geom.x_global, stations.geom.y_global, stations.geom.z_global = xyz_bef - stations.geom.pointn_azim_global = azim_bef - stations.geom.pointn_elev_global = elev_bef + stations.geom.set_global_coords( + xyz_bef[0], xyz_bef[1], xyz_bef[2], + azim_bef, elev_bef + ) # get relative distances and off axis while in ecef dists_bef = stations.geom.get_3d_distance_to(stations.geom) diff --git a/tests/test_simulation_downlink.py b/tests/test_simulation_downlink.py index 62f3bea5b..3073daddf 100644 --- a/tests/test_simulation_downlink.py +++ b/tests/test_simulation_downlink.py @@ -119,28 +119,29 @@ def setUp(self): self.param.imt.ue.antenna.array.element_vert_spacing = 0.5 self.param.imt.ue.antenna.array.multiplication_factor = 12 - self.param.fss_ss.frequency = 10000.0 - self.param.fss_ss.bandwidth = 100 - self.param.fss_ss.acs = 0 - self.param.fss_ss.altitude = 35786000 - self.param.fss_ss.lat_deg = 0 - self.param.fss_ss.azimuth = 0 - self.param.fss_ss.elevation = 270 - self.param.fss_ss.tx_power_density = -30 - self.param.fss_ss.noise_temperature = 950 - self.param.fss_ss.antenna_gain = 51 - self.param.fss_ss.antenna_pattern = "OMNI" - self.param.fss_ss.imt_altitude = 1000 - self.param.fss_ss.imt_lat_deg = -23.5629739 - self.param.fss_ss.imt_long_diff_deg = (-46.6555132 - 75) - self.param.fss_ss.channel_model = "FSPL" - self.param.fss_ss.line_of_sight_prob = 0.01 - self.param.fss_ss.surf_water_vapour_density = 7.5 - self.param.fss_ss.specific_gaseous_att = 0.1 - self.param.fss_ss.time_ratio = 0.5 - self.param.fss_ss.antenna_l_s = -20 - self.param.fss_ss.acs = 0 - self.param.fss_ss.polarization_loss = 3.0 + self.param.single_space_station.frequency = 10000.0 + self.param.single_space_station.bandwidth = 100 + self.param.single_space_station.tx_power_density = -30 + self.param.single_space_station.noise_temperature = 950 + self.param.single_space_station.antenna.gain = 51 + self.param.single_space_station.antenna.pattern = "OMNI" + self.param.single_space_station.acs = 0 + self.param.single_space_station.geometry.azimuth.type = "FIXED" + self.param.single_space_station.geometry.azimuth.fixed = 0.0 + self.param.single_space_station.geometry.elevation.type = "FIXED" + self.param.single_space_station.geometry.elevation.fixed = 270 + self.param.single_space_station.geometry.es_altitude = 1000 + self.param.single_space_station.geometry.es_lat_deg = -23.5629739 + self.param.single_space_station.geometry.es_long_deg = -46.6555132 + self.param.single_space_station.geometry.location.type = "FIXED" + self.param.single_space_station.geometry.altitude = 35786000 + self.param.single_space_station.geometry.location.fixed.long_deg = 75 + self.param.single_space_station.geometry.location.fixed.lat_deg = 0 + # self.param.single_space_station.imt_long_diff_deg = (-46.6555132 - 75) + self.param.single_space_station.channel_model = "FSPL" + + self.param.single_space_station.acs = 0 + self.param.single_space_station.polarization_loss = 3.0 self.param.fss_es.x = -5000 self.param.fss_es.y = 0 @@ -180,9 +181,9 @@ def setUp(self): self.param.ras.tx_power_density = -500 self.param.ras.polarization_loss = 0.0 - def test_simulation_2bs_4ue_fss_ss(self): - """Test simulation with 2 base stations and 4 UEs for FSS-SS scenario.""" - self.param.general.system = "FSS_SS" + def test_simulation_2bs_4ue_single_space_station(self): + """Test simulation with 2 base stations and 4 UEs for single space station scenario.""" + self.param.general.system = "SINGLE_SPACE_STATION" self.simulation = SimulationDownlink(self.param, "") self.simulation.initialize() @@ -209,8 +210,10 @@ def test_simulation_2bs_4ue_fss_ss(self): self.simulation.topology, random_number_gen, ) - self.simulation.ue.geom.x_global = np.array([20, 70, 110, 170]) - self.simulation.ue.geom.y_global = np.array([0, 0, 0, 0]) + self.simulation.ue.geom.set_global_coords( + np.array([20, 70, 110, 170]), + np.array([0, 0, 0, 0]), + ) self.simulation.ue.antenna = np.array( [AntennaOmni(10), AntennaOmni(11), AntennaOmni(22), AntennaOmni(23)], ) @@ -232,7 +235,7 @@ def test_simulation_2bs_4ue_fss_ss(self): random_number_gen, ) self.simulation.propagation_system = PropagationFactory.create_propagation( - self.param.fss_ss.channel_model, + self.param.single_space_station.channel_model, self.param, self.simulation.param_system, random_number_gen, @@ -334,12 +337,14 @@ def test_simulation_2bs_4ue_fss_ss(self): rx_power - total_interference, atol=1e-2, ) - self.simulation.system = StationFactory.generate_fss_space_station( - self.param.fss_ss, + self.simulation.system = StationFactory.generate_single_space_station( + self.param.single_space_station, + ) + self.simulation.system.geom.set_global_coords( + np.array([0.01]), + np.array([0]), + np.array([self.param.single_space_station.geometry.altitude]), ) - self.simulation.system.geom.x_global = np.array([0.01]) # avoids zero-division - self.simulation.system.geom.y_global = np.array([0]) - self.simulation.system.geom.z_global = np.array([self.param.fss_ss.altitude]) # test the method that calculates interference from IMT UE to FSS space # station @@ -419,8 +424,10 @@ def test_simulation_2bs_4ue_fss_es(self): self.simulation.topology, random_number_gen, ) - self.simulation.ue.geom.x_global = np.array([20, 70, 110, 170]) - self.simulation.ue.geom.y_global = np.array([0, 0, 0, 0]) + self.simulation.ue.geom.set_global_coords( + np.array([20, 70, 110, 170]), + np.array([0, 0, 0, 0]), + ) self.simulation.ue.antenna = np.array( [AntennaOmni(10), AntennaOmni(11), AntennaOmni(22), AntennaOmni(23)], ) @@ -518,9 +525,11 @@ def test_simulation_2bs_4ue_fss_es(self): self.simulation.system = StationFactory.generate_fss_earth_station( self.param.fss_es, random_number_gen, ) - self.simulation.system.geom.x_global = np.array([-2000]) - self.simulation.system.geom.y_global = np.array([0]) - self.simulation.system.geom.z_global = np.array([self.param.fss_es.height]) + self.simulation.system.geom.set_global_coords( + np.array([-2000]), + np.array([0]), + np.array([self.param.fss_es.height]), + ) self.simulation.propagation_imt = PropagationFactory.create_propagation( self.param.imt.channel_model, @@ -640,8 +649,10 @@ def test_simulation_2bs_4ue_ras(self): self.simulation.topology, random_number_gen, ) - self.simulation.ue.geom.x_global = np.array([20, 70, 110, 170]) - self.simulation.ue.geom.y_global = np.array([0, 0, 0, 0]) + self.simulation.ue.geom.set_global_coords( + np.array([20, 70, 110, 170]), + np.array([0, 0, 0, 0]), + ) self.simulation.ue.antenna = np.array( [AntennaOmni(10), AntennaOmni(11), AntennaOmni(22), AntennaOmni(23)], ) @@ -690,13 +701,14 @@ def test_simulation_2bs_4ue_ras(self): atol=1e-2, ) - self.simulation.system = StationFactory.generate_ras_station( + self.simulation.system = StationFactory.generate_single_earth_station( self.param.ras, random_number_gen, topology=None, ) - self.simulation.system.geom.x_global = np.array([-2000]) - self.simulation.system.geom.y_global = np.array([0]) - self.simulation.system.geom.z_global = np.array( - [self.param.ras.geometry.height]) + self.simulation.system.geom.set_global_coords( + np.array([-2000]), + np.array([0]), + np.array([self.param.ras.geometry.height]), + ) self.simulation.system.antenna[0].effective_area = 54.9779 # Test gain calculation diff --git a/tests/test_simulation_downlink_haps.py b/tests/test_simulation_downlink_haps.py index 461d1dbad..ad5dc2623 100644 --- a/tests/test_simulation_downlink_haps.py +++ b/tests/test_simulation_downlink_haps.py @@ -169,8 +169,10 @@ def test_simulation_2bs_4ue_1haps(self): self.simulation.topology, random_number_gen, ) - self.simulation.ue.geom.x_global = np.array([20, 70, 110, 170]) - self.simulation.ue.geom.y_global = np.array([0, 0, 0, 0]) + self.simulation.ue.geom.set_global_coords( + np.array([20, 70, 110, 170]), + np.array([0, 0, 0, 0]), + ) self.simulation.ue.antenna = np.array( [AntennaOmni(10), AntennaOmni(11), AntennaOmni(22), AntennaOmni(23)], ) diff --git a/tests/test_simulation_downlink_tvro.py b/tests/test_simulation_downlink_tvro.py index 2e7ddd90d..99dc031c2 100644 --- a/tests/test_simulation_downlink_tvro.py +++ b/tests/test_simulation_downlink_tvro.py @@ -153,10 +153,12 @@ def test_simulation_1bs_1ue_tvro(self): self.simulation.topology, random_number_gen, ) - self.simulation.bs.geom.x_global = np.array([0, -200]) - self.simulation.bs.geom.y_global = np.array([0, 0]) - self.simulation.bs.geom.pointn_azim_global = np.array([0, 180]) - self.simulation.bs.geom.pointn_elev_global = np.array([-10, -10]) + self.simulation.bs.geom.set_global_coords( + np.array([0.0, -200.0]), + np.array([0.0, 0.0]), + azim=np.array([0.0, 180.0]), + elev=np.array([-10.0, -10.0]), + ) self.simulation.ue = StationFactory.generate_imt_ue( self.param.imt, @@ -164,8 +166,10 @@ def test_simulation_1bs_1ue_tvro(self): self.simulation.topology, random_number_gen, ) - self.simulation.ue.geom.x_global = np.array([30, 60, -220, -300]) - self.simulation.ue.geom.y_global = np.array([0, 0, 0, 0]) + self.simulation.ue.geom.set_global_coords( + np.array([30.0, 60.0, -220.0, -300.0]), + np.array([0.0, 0.0, 0.0, 0.0]), + ) # test connection method self.simulation.connect_ue_to_bs() @@ -285,8 +289,10 @@ def test_simulation_1bs_1ue_tvro(self): self.simulation.system = StationFactory.generate_fss_earth_station( self.param.fss_es, random_number_gen, ) - self.simulation.system.geom.x_global = np.array([600]) - self.simulation.system.geom.y_global = np.array([0]) + self.simulation.system.geom.set_global_coords( + np.array([600.0]), + np.array([0.0]), + ) # test the method that calculates interference from IMT UE to FSS space # station diff --git a/tests/test_simulation_indoor.py b/tests/test_simulation_indoor.py index 585ecab11..a3b0b4f6d 100644 --- a/tests/test_simulation_indoor.py +++ b/tests/test_simulation_indoor.py @@ -167,10 +167,11 @@ def test_simulation_fss_es(self): # print("Random position:") # self.simulation.plot_scenario() - self.simulation.ue.geom.x_global = np.array([0.0, 45.0, 75.0, 120.0]) - self.simulation.ue.geom.y_global = np.array([0.0, 50.0, 0.0, 50.0]) - self.simulation.ue.geom.z_global = np.ones_like( - self.simulation.ue.geom.x_global) * self.param.imt.ue.height + self.simulation.ue.geom.set_global_coords( + np.array([0.0, 45.0, 75.0, 120.0]), + np.array([0.0, 50.0, 0.0, 50.0]), + np.ones_like(self.simulation.ue.geom.x_global) * self.param.imt.ue.height, + ) # print("Forced position:") # self.simulation.plot_scenario() diff --git a/tests/test_simulation_uplink.py b/tests/test_simulation_uplink.py index c5096e3d7..51fbd1ed4 100644 --- a/tests/test_simulation_uplink.py +++ b/tests/test_simulation_uplink.py @@ -201,8 +201,10 @@ def test_simulation_2bs_4ue_ss(self): self.simulation.topology, random_number_gen, ) - self.simulation.ue.geom.x_global = np.array([20, 70, 110, 170]) - self.simulation.ue.geom.y_global = np.array([0, 0, 0, 0]) + self.simulation.ue.geom.set_global_coords( + np.array([20, 70, 110, 170]), + np.array([0, 0, 0, 0]), + ) self.simulation.ue.antenna = np.array( [AntennaOmni(10), AntennaOmni(11), AntennaOmni(22), AntennaOmni(23)], ) @@ -363,9 +365,11 @@ def test_simulation_2bs_4ue_ss(self): self.simulation.system = StationFactory.generate_fss_space_station( self.param.fss_ss, ) - self.simulation.system.geom.x_global = np.array([0]) - self.simulation.system.geom.y_global = np.array([0]) - self.simulation.system.geom.z_global = np.array([self.param.fss_ss.altitude]) + self.simulation.system.geom.set_global_coords( + np.array([0]), + np.array([0]), + np.array([self.param.fss_ss.altitude]), + ) # test the method that calculates interference from IMT UE to FSS space # station @@ -432,8 +436,10 @@ def test_simulation_2bs_4ue_es(self): self.simulation.topology, random_number_gen, ) - self.simulation.ue.geom.x_global = np.array([20, 70, 110, 170]) - self.simulation.ue.geom.y_global = np.array([0, 0, 0, 0]) + self.simulation.ue.geom.set_global_coords( + np.array([20, 70, 110, 170]), + np.array([0, 0, 0, 0]), + ) self.simulation.ue.antenna = np.array( [AntennaOmni(10), AntennaOmni(11), AntennaOmni(22), AntennaOmni(23)], ) @@ -719,8 +725,10 @@ def test_simulation_2bs_4ue_ras(self): self.simulation.topology, random_number_gen, ) - self.simulation.ue.geom.x_global = np.array([20, 70, 110, 170]) - self.simulation.ue.geom.y_global = np.array([0, 0, 0, 0]) + self.simulation.ue.geom.set_global_coords( + np.array([20, 70, 110, 170]), + np.array([0, 0, 0, 0]), + ) self.simulation.ue.antenna = np.array( [AntennaOmni(10), AntennaOmni(11), AntennaOmni(22), AntennaOmni(23)], ) @@ -778,10 +786,11 @@ def test_simulation_2bs_4ue_ras(self): self.simulation.system = StationFactory.generate_ras_station( self.param.ras, random_number_gen, None, ) - self.simulation.system.geom.x_global = np.array([-2000]) - self.simulation.system.geom.y_global = np.array([0]) - self.simulation.system.geom.z_global = np.array( - [self.param.ras.geometry.height]) + self.simulation.system.geom.set_global_coords( + np.array([-2000]), + np.array([0]), + np.array([self.param.ras.geometry.height]), + ) self.simulation.system.antenna[0].effective_area = 54.9779 # Test gain calculation @@ -861,8 +870,10 @@ def test_beamforming_gains(self): self.simulation.topology, random_number_gen, ) - self.simulation.ue.geom.x_global = np.array([50.000, 43.301, 150.000, 175.000]) - self.simulation.ue.geom.y_global = np.array([0.000, 25.000, 0.000, 43.301]) + self.simulation.ue.geom.set_global_coords( + np.array([50.000, 43.301, 150.000, 175.000]), + np.array([0.000, 25.000, 0.000, 43.301]), + ) # Physical pointing angles self.assertEqual(self.simulation.bs.antenna[0].azimuth, 0) @@ -871,8 +882,11 @@ def test_beamforming_gains(self): self.assertEqual(self.simulation.bs.antenna[0].elevation, -10) # Change UE pointing - self.simulation.ue.geom.pointn_azim_global = np.array([180, -90, 90, -90]) - self.simulation.ue.geom.pointn_elev_global = np.array([-30, -15, 15, 30]) + self.simulation.ue.geom.set_global_coords( + azim=np.array([180, -90, 90, -90]), + elev=np.array([-30, -15, 15, 30]), + ) + par = self.param.imt.ue.antenna.array.get_antenna_parameters() for i in range(self.simulation.ue.num_stations): self.simulation.ue.antenna[i] = AntennaBeamformingImt( diff --git a/tests/test_station_factory.py b/tests/test_station_factory.py index d0fb19f94..b114c2335 100644 --- a/tests/test_station_factory.py +++ b/tests/test_station_factory.py @@ -207,9 +207,11 @@ def test_single_space_station_pointing(self): param.validate() imt_center = StationManager(1) - imt_center.geom.x_global = np.array([0.]) - imt_center.geom.y_global = np.array([0.]) - imt_center.geom.z_global = np.array([0.]) + imt_center.geom.set_global_coords( + np.array([0.]), + np.array([0.]), + np.array([0.]), + ) # Test point it toward IMT center (0, 0, 0) param.geometry.azimuth.type = "POINTING_AT_IMT" @@ -236,9 +238,11 @@ def test_single_space_station_pointing(self): # same as pointing toward center of earth center_of_earth = StationManager(1) - center_of_earth.geom.x_global = np.array([0.]) - center_of_earth.geom.y_global = np.array([0.]) - center_of_earth.geom.z_global = -np.array([EARTH_RADIUS_M + 1200]) + center_of_earth.geom.set_global_coords( + np.array([0.]), + np.array([0.]), + -np.array([EARTH_RADIUS_M + 1200]), + ) param.geometry.azimuth.type = "POINTING_AT_LAT_LONG_ALT" param.geometry.elevation.type = "POINTING_AT_LAT_LONG_ALT" diff --git a/tests/test_station_factory_ngso.py b/tests/test_station_factory_ngso.py index 76cb10ffb..9f2359d5c 100644 --- a/tests/test_station_factory_ngso.py +++ b/tests/test_station_factory_ngso.py @@ -98,11 +98,13 @@ def test_satellite_antenna_pointing(self): # Test: check if center of earth is 0deg off axis, and that its # distance to satellite is correct earth_center = StationManager(1) - earth_center.geom.x_global = np.array([0.]) - earth_center.geom.y_global = np.array([0.]) x, y, z = lla2ecef(self.lat, self.long, self.alt) - earth_center.geom.z_global = -np.sqrt( - x * x + y * y + z * z, + earth_center.geom.set_global_coords( + np.array([0.]), + np.array([0.]), + -np.sqrt( + x * x + y * y + z * z, + ) ) self.assertNotAlmostEqual(earth_center.geom.z_global[0], 0.) @@ -151,9 +153,11 @@ def test_satellite_coordinate_reversing(self): # Test: check if center of earth is 0deg off axis earth_center = StationManager(1) - earth_center.geom.x_global = np.array([0.]) - earth_center.geom.y_global = np.array([0.]) - earth_center.geom.z_global = np.array([0.]) + earth_center.geom.set_global_coords( + np.array([0.]), + np.array([0.]), + np.array([0.]), + ) off_axis_angle = ngso_original_coord.geom.get_off_axis_angle(earth_center.geom) diff --git a/tests/test_station_manager.py b/tests/test_station_manager.py index e68049db2..8259b5eb2 100644 --- a/tests/test_station_manager.py +++ b/tests/test_station_manager.py @@ -61,9 +61,11 @@ def setUp(self): self.ue_param.multiplication_factor = 12 self.station_manager = StationManager(3) - self.station_manager.geom.x_global = np.array([10, 20, 30]) - self.station_manager.geom.y_global = np.array([15, 25, 35]) - self.station_manager.geom.z_global = np.array([1, 2, 3]) + self.station_manager.geom.set_global_coords( + np.array([10, 20, 30]), + np.array([15, 25, 35]), + np.array([1, 2, 3]), + ) self.station_manager.geom.intersite_dist = 100.0 # this is for downlink self.station_manager.tx_power = dict({0: [27, 30], 1: [35], 2: [40]}) @@ -77,9 +79,11 @@ def setUp(self): self.station_manager.station_type = StationType.IMT_BS self.station_manager2 = StationManager(2) - self.station_manager2.geom.x_global = np.array([100, 200]) - self.station_manager2.geom.y_global = np.array([105, 250]) - self.station_manager2.geom.z_global = np.array([4, 5]) + self.station_manager2.geom.set_global_coords( + np.array([100, 200]), + np.array([105, 250]), + np.array([4, 5]), + ) self.station_manager2.geom.intersite_dist = 100.0 # this is for downlink self.station_manager2.tx_power = dict({0: [25], 1: [28, 35]}) @@ -91,9 +95,11 @@ def setUp(self): self.station_manager2.station_type = StationType.IMT_BS self.station_manager3 = StationManager(1) - self.station_manager3.geom.x_global = np.array([300]) - self.station_manager3.geom.y_global = np.array([400]) - self.station_manager3.geom.z_global = np.array([2]) + self.station_manager3.geom.set_global_coords( + np.array([300]), + np.array([400]), + np.array([2]), + ) self.station_manager3.geom.intersite_dist = 100.0 # this is for uplink self.station_manager3.tx_power = 22 @@ -155,10 +161,10 @@ def test_x(self): # get all values (no need to specify the id's) npt.assert_array_equal(self.station_manager.geom.x_global, [10, 20, 30]) # set a single value and get it - self.station_manager.geom.x_global[0] = 8 + self.station_manager.geom._x_global[0] = 8 npt.assert_array_equal(self.station_manager.geom.x_global[[0, 1]], [8, 20]) # set two values and then get all values - self.station_manager.geom.x_global[[1, 2]] = [16, 32] + self.station_manager.geom._x_global[[1, 2]] = [16, 32] npt.assert_array_equal(self.station_manager.geom.x_global, [8, 16, 32]) def test_y(self): @@ -172,10 +178,10 @@ def test_y(self): # get all values (no need to specify the id's) npt.assert_array_equal(self.station_manager.geom.y_global, [15, 25, 35]) # set a single value and get it - self.station_manager.geom.y_global[1] = 9 + self.station_manager.geom._y_global[1] = 9 npt.assert_array_equal(self.station_manager.geom.y_global[[0, 1]], [15, 9]) # set two values and then get all values - self.station_manager.geom.y_global[[0, 2]] = [7, 21] + self.station_manager.geom._y_global[[0, 2]] = [7, 21] npt.assert_array_equal(self.station_manager.geom.y_global, [7, 9, 21]) def test_height(self): @@ -191,10 +197,10 @@ def test_height(self): # get all values (no need to specify the id's) npt.assert_array_equal(self.station_manager.geom.z_global, [1, 2, 3]) # set a single value and get it - self.station_manager.geom.z_global[1] = 7 + self.station_manager.geom._z_global[1] = 7 npt.assert_array_equal(self.station_manager.geom.z_global[[1, 2]], [7, 3]) # set two values and then get all values - self.station_manager.geom.z_global[[0, 2]] = [5, 4] + self.station_manager.geom._z_global[[0, 2]] = [5, 4] npt.assert_array_equal(self.station_manager.geom.z_global, [5, 7, 4]) def test_tx_power(self): @@ -344,17 +350,19 @@ def test_3d_distance_to(self): def test_wrap_around(self): """Test wrap-around distance and angle calculations between managers.""" self.station_manager = StationManager(2) - self.station_manager.geom.x_global = np.array([0, 150]) - self.station_manager.geom.y_global = np.array([0, -32]) - self.station_manager.geom.z_global = np.array([4, 5]) - self.station_manager.geom.z_global = np.array([4, 5]) + self.station_manager.geom.set_global_coords( + np.array([0, 150]), + np.array([0, -32]), + np.array([4, 5]), + ) self.station_manager.geom.intersite_dist = 100.0 self.station_manager2 = StationManager(3) - self.station_manager2.geom.x_global = np.array([10, 200, 30]) - self.station_manager2.geom.y_global = np.array([15, 250, -350]) - self.station_manager2.geom.z_global = np.array([1, 2, 3]) - self.station_manager2.geom.z_global = np.array([1, 2, 3]) + self.station_manager2.geom.set_global_coords( + np.array([10, 200, 30]), + np.array([15, 250, -350]), + np.array([1, 2, 3]), + ) # 2D Distance d_2D, d_3D, phi, theta = self.station_manager.geom.get_global_dist_angles_wrap_around( @@ -441,72 +449,88 @@ def test_pointing_vector_to(self): def test_off_axis_angle(self): """Test calculation of off-axis angles between station managers.""" sm1 = StationManager(1) - sm1.geom.x_global = np.array([0]) - sm1.geom.y_global = np.array([0]) - sm1.geom.z_global = np.array([0]) - sm1.geom.pointn_azim_global = np.array([0]) - sm1.geom.pointn_elev_global = np.array([0]) + sm1.geom.set_global_coords( + np.array([0.]), + np.array([0.]), + np.array([0.]), + np.array([0.]), + np.array([0.]), + ) sm2 = StationManager(6) - sm2.geom.x_global = np.array([100, 100, 0, 100, 100, 100]) - sm2.geom.y_global = np.array([0, 0, 100, 100, 100, 100]) - sm2.geom.z_global = np.array([0, 100, 0, 0, 100, 100]) - sm2.geom.pointn_azim_global = np.array([180, 180, 180, 180, 180, 225]) - sm2.geom.pointn_elev_global = np.array([0, 0, 0, 0, 0, 0]) + sm2.geom.set_global_coords( + np.array([100, 100, 0, 100, 100, 100]), + np.array([0, 0, 100, 100, 100, 100]), + np.array([0, 100, 0, 0, 100, 100]), + np.array([180, 180, 180, 180, 180, 225]), + np.array([0, 0, 0, 0, 0, 0]), + ) phi_ref = np.array([[0, 45, 90, 45, 54.73, 54.73]]) npt.assert_allclose(phi_ref, sm1.geom.get_off_axis_angle(sm2.geom), atol=1e-2) ####################################################################### sm3 = StationManager(1) - sm3.geom.x_global = np.array([0]) - sm3.geom.y_global = np.array([0]) - sm3.geom.z_global = np.array([0]) - sm3.geom.pointn_azim_global = np.array([45]) - sm3.geom.pointn_elev_global = np.array([0]) + sm3.geom.set_global_coords( + np.array([0]), + np.array([0]), + np.array([0]), + np.array([45]), + np.array([0]), + ) sm4 = StationManager(2) - sm4.geom.x_global = np.array([100, 60]) - sm4.geom.y_global = np.array([100, 80]) - sm4.geom.z_global = np.array([100, 100]) - sm4.geom.pointn_azim_global = np.array([180, 180]) - sm4.geom.pointn_elev_global = np.array([0, 0]) + sm4.geom.set_global_coords( + np.array([100, 60]), + np.array([100, 80]), + np.array([100, 100]), + np.array([180, 180]), + np.array([0, 0]), + ) phi_ref = np.array([[35.26, 45.57]]) npt.assert_allclose(phi_ref, sm3.geom.get_off_axis_angle(sm4.geom), atol=1e-2) ####################################################################### sm5 = StationManager(1) - sm5.geom.x_global = np.array([0]) - sm5.geom.y_global = np.array([0]) - sm5.geom.z_global = np.array([0]) - sm5.geom.pointn_azim_global = np.array([0]) - sm5.geom.pointn_elev_global = np.array([45]) + sm5.geom.set_global_coords( + np.array([0]), + np.array([0]), + np.array([0]), + np.array([0]), + np.array([45]), + ) sm6 = StationManager(2) - sm6.geom.x_global = np.array([100, 100]) - sm6.geom.y_global = np.array([0, 0]) - sm6.geom.z_global = np.array([100, 100]) - sm6.geom.pointn_azim_global = np.array([180, 180]) - sm6.geom.pointn_elev_global = np.array([0, 0]) + sm6.geom.set_global_coords( + np.array([100, 100]), + np.array([0, 0]), + np.array([100, 100]), + np.array([180, 180]), + np.array([0, 0]), + ) phi_ref = np.array([[0, 0]]) npt.assert_allclose(phi_ref, sm5.geom.get_off_axis_angle(sm6.geom), atol=1e-2) ####################################################################### sm6 = StationManager(1) - sm6.geom.x_global = np.array([0]) - sm6.geom.y_global = np.array([0]) - sm6.geom.z_global = np.array([100]) - sm6.geom.pointn_azim_global = np.array([0]) - sm6.geom.pointn_elev_global = np.array([270]) + sm6.geom.set_global_coords( + np.array([0]), + np.array([0]), + np.array([100]), + np.array([0]), + np.array([270]), + ) sm7 = StationManager(2) - sm7.geom.x_global = np.array([0, 100]) - sm7.geom.y_global = np.array([0, 0]) - sm7.geom.z_global = np.array([0, 0]) - sm7.geom.pointn_azim_global = np.array([180, 180]) - sm7.geom.pointn_elev_global = np.array([0, 0]) + sm7.geom.set_global_coords( + np.array([0, 100]), + np.array([0, 0]), + np.array([0, 0]), + np.array([180, 180]), + np.array([0, 0]), + ) phi_ref = np.array([[0, 45]]) npt.assert_allclose(phi_ref, sm6.geom.get_off_axis_angle(sm7.geom), atol=1e-2) @@ -514,28 +538,36 @@ def test_off_axis_angle(self): def test_elevation(self): """Test calculation of elevation angles between station managers.""" sm1 = StationManager(1) - sm1.geom.x_global = np.array([0]) - sm1.geom.y_global = np.array([0]) - sm1.geom.z_global = np.array([10]) + sm1.geom.set_global_coords( + np.array([0]), + np.array([0]), + np.array([10]), + ) sm2 = StationManager(6) - sm2.geom.x_global = np.array([10, 10, 0, 0, 30, 20]) - sm2.geom.y_global = np.array([0, 0, 5, 10, 30, 20]) - sm2.geom.z_global = np.array([10, 20, 5, 0, 20, 20]) + sm2.geom.set_global_coords( + np.array([10, 10, 0, 0, 30, 20]), + np.array([0, 0, 5, 10, 30, 20]), + np.array([10, 20, 5, 0, 20, 20]), + ) elevation_ref = np.array([[0, 45, -45, -45, 13.26, 19.47]]) npt.assert_allclose(elevation_ref, sm1.geom.get_local_elevation(sm2.geom), atol=1e-2) ####################################################################### sm3 = StationManager(2) - sm3.geom.x_global = np.array([0, 30]) - sm3.geom.y_global = np.array([0, 0]) - sm3.geom.z_global = np.array([10, 10]) + sm3.geom.set_global_coords( + np.array([0, 30]), + np.array([0, 0]), + np.array([10, 10]), + ) sm4 = StationManager(2) - sm4.geom.x_global = np.array([10, 10]) - sm4.geom.y_global = np.array([0, 0]) - sm4.geom.z_global = np.array([10, 20]) + sm4.geom.set_global_coords( + np.array([10, 10]), + np.array([0, 0]), + np.array([10, 20]), + ) elevation_ref = np.array([[0, 45], [0, 26.56]]) npt.assert_allclose(elevation_ref, sm3.geom.get_local_elevation(sm4.geom), atol=1e-2) diff --git a/tests/test_topology_imt_mss_dc.py b/tests/test_topology_imt_mss_dc.py index dced50b3c..481e7ac85 100644 --- a/tests/test_topology_imt_mss_dc.py +++ b/tests/test_topology_imt_mss_dc.py @@ -107,15 +107,19 @@ def test_calculate_coordinates(self): # by default, satellites should always point to nadir (earth center) ref_earth_center = StationManager(1) - ref_earth_center.geom.x_global = self.earth_center_x.flatten() - ref_earth_center.geom.y_global = self.earth_center_y.flatten() - ref_earth_center.geom.z_global = self.earth_center_z.flatten() + ref_earth_center.geom.set_global_coords( + self.earth_center_x.flatten(), + self.earth_center_y.flatten(), + self.earth_center_z.flatten(), + ) ref_space_stations = StationManager( self.imt_mss_dc_topology.num_base_stations) - ref_space_stations.geom.x_global = self.imt_mss_dc_topology.space_station_x - ref_space_stations.geom.y_global = self.imt_mss_dc_topology.space_station_y - ref_space_stations.geom.z_global = self.imt_mss_dc_topology.space_station_z + ref_space_stations.geom.set_global_coords( + self.imt_mss_dc_topology.space_station_x, + self.imt_mss_dc_topology.space_station_y, + self.imt_mss_dc_topology.space_station_z, + ) phi, theta = ref_space_stations.geom.get_global_pointing_vector_to( ref_earth_center.geom) From 41d527193481b67849716264c13e9e5f3fe37a48 Mon Sep 17 00:00:00 2001 From: artistrea Date: Wed, 8 Oct 2025 02:13:19 -0300 Subject: [PATCH 21/77] refactor: parameters for service grid --- sharc/parameters/imt/parameters_grid.py | 539 ++++++++++++++++++ sharc/parameters/imt/parameters_imt_mss_dc.py | 441 +------------- tests/parameters/parameters_for_testing.yaml | 59 +- tests/parameters/test_parameters.py | 24 +- 4 files changed, 587 insertions(+), 476 deletions(-) create mode 100644 sharc/parameters/imt/parameters_grid.py diff --git a/sharc/parameters/imt/parameters_grid.py b/sharc/parameters/imt/parameters_grid.py new file mode 100644 index 000000000..1ac4fec5c --- /dev/null +++ b/sharc/parameters/imt/parameters_grid.py @@ -0,0 +1,539 @@ +from warnings import warn +from dataclasses import dataclass, field +import numpy as np +import typing +from pathlib import Path +import shapely as shp + +from sharc.support.sharc_utils import load_gdf +from sharc.support.sharc_geom import ( + shrink_countries_by_km, + generate_grid_in_multipolygon, + shrink_lonlat_polygon_by_km, +) +from sharc.satellite.utils.sat_utils import lla2ecef +from sharc.parameters.parameters_base import ParametersBase + +SHARC_ROOT_DIR = (Path(__file__) / ".." / ".." / ".." / "..").resolve() + + +@dataclass +class ParametersZone(ParametersBase): + @dataclass + class ParametersCircle(ParametersBase): + center_lat: typing.Optional[float] = None + center_lon: typing.Optional[float] = None + radius_km: typing.Optional[float] = None + + _polygon: shp.Polygon = None + + def validate(self, ctx): + """ + Validates instance parameters. + + Ensures attributes make sense + + Parameters + ---------- + ctx : str + Context string for error messages. + + Raises + ------ + ValueError + If a parameter is not valid. + """ + if None in [ + self.center_lat, + self.center_lon, + self.radius_km, + ]: + raise ValueError( + f"{ctx}.(center_lat|center_lon|radius_km) need to be set" + ) + + if self.radius_km <= 0: + raise ValueError(f"{ctx}.radius_km needs to be positive") + + if not (-180. <= self.center_lon <= 180.): + raise ValueError(f"{ctx}.center_lon needs to be in [-180, 180]") + + if not (-90. <= self.center_lat <= 90.): + raise ValueError(f"{ctx}.center_lat needs to be in [-90, 90]") + + super().validate(ctx) + + self._calculate_polygon() + + def _calculate_polygon(self): + """ + Calculates circle lon,lat polygon according to its attributes + """ + self._polygon = shrink_lonlat_polygon_by_km( + shp.geometry.Point(self.center_lon, self.center_lat), + -self.radius_km + ) + @dataclass + class ParametersFromCountries(ParametersBase): + country_shapes_filename: Path = SHARC_ROOT_DIR / "sharc" / \ + "data" / "countries" / "ne_110m_admin_0_countries.shp" + + country_names: list[str] = field(default_factory=lambda: list([""])) + # margin from inside of border [km] + # if positive, makes border smaller by x km + # if negative, makes border bigger by x km + margin_from_border: float = None + + _polygon: shp.Polygon = None + _unprocessed_polygon: shp.Polygon = None + + def validate(self, ctx): + """ + Validates instance parameters. + Raises ValueError + If a parameter is not valid. + """ + # conditional is weird due to suboptimal way of working with nested + # array parameters + if len(self.country_names) == 0 or ( + len(self.country_names) == 1 and self.country_names[0] == ""): + raise ValueError( + f"You need to pass at least one country name to {ctx}.country_names") + + if not isinstance( + self.margin_from_border, + float) and not isinstance( + self.margin_from_border, + int): + raise ValueError( + f"{ctx}.margin_from_border needs to be a number") + + self._calculate_polygon() + + def _calculate_polygon(self): + filtered_gdf = load_gdf( + self.country_shapes_filename, + { + "NAME": self.country_names + }, + "from_countries", + ) + + # shrink countries and unite + # them into a single MultiPolygon + self._unprocessed_polygon = filtered_gdf.geometry.values + + shrinked = shrink_countries_by_km( + filtered_gdf.geometry.values, self.margin_from_border + ) + self._polygon = shp.ops.unary_union(shrinked) + + assert self._polygon.is_valid, \ + shp.validation.explain_validity(self._polygon) + + assert not self._polygon.is_empty, \ + "Can't have a empty grid_borders_polygon as filter" + + __ALLOWED_TYPES = [None, "CIRCLE", "FROM_COUNTRIES"] + _ACCEPT_NONE_TYPE: bool = False + + type: typing.Literal[None, "CIRCLE", "FROM_COUNTRIES"] = None + + circle: ParametersCircle = field(default_factory=ParametersCircle) + + from_countries: ParametersFromCountries = field(default_factory=ParametersFromCountries) + + _polygon: shp.geometry.Polygon = None + _unprocessed_polygon: shp.geometry.Polygon = None + + def set_chosen_pol(self): + self.chosen_pol = None + if self.type is None: + return + + if self.type == "CIRCLE": + self.chosen_pol = self.circle + elif self.type == "FROM_COUNTRIES": + self.chosen_pol = self.from_countries + else: + raise NotImplementedError( + f"Cannot set chosen_pol for type == '{self.type}'" + ) + + self._polygon = self.chosen_pol._polygon + if hasattr(self.chosen_pol, "_unprocessed_polygon"): + self._unprocessed_polygon = self.chosen_pol._unprocessed_polygon + else: + self._unprocessed_polygon = self.chosen_pol._polygon + + def validate(self, ctx): + """ + Validates instance parameters. + + Ensures attributes make sense + + Parameters + ---------- + ctx : str + Context string for error messages. + + Raises + ------ + ValueError + If a parameter is not valid. + """ + if self.type not in self.__ALLOWED_TYPES: + raise ValueError(f"{ctx}.type should be in {self.__ALLOWED_TYPES}") + + if self.type is None: + return + + if self.type == "CIRCLE": + self.circle.validate(f"{ctx}.circle") + elif self.type == "FROM_COUNTRIES": + self.from_countries.validate(f"{ctx}.from_countries") + else: + raise NotImplementedError( + "No validation implemented for\n" + f"\t{ctx}.type == {self.type}" + ) + + self.set_chosen_pol() + self._calculate_polygon() + + if (not self._polygon.is_valid + or self._polygon.is_empty + or self._polygon.area <= 0 + ): + raise Exception(f"Bad {ctx}._polygon was generated") + + def _calculate_polygon(self): + self.set_chosen_pol() + + if self.chosen_pol is None: + if self._ACCEPT_NONE_TYPE: + return + raise ValueError("No polygon type has been set for zone") + + self.chosen_pol._calculate_polygon() + self._polygon = self.chosen_pol._polygon + if hasattr(self.chosen_pol, "_unprocessed_polygon"): + self._unprocessed_polygon = self.chosen_pol._unprocessed_polygon + else: + self._unprocessed_polygon = self.chosen_pol._polygon + + def apply_exclusion_zone(self, lon, lat): + """ + Returns coordinates that are not contained in polygon + """ + if self.type is None: + return np.stack((lon, lat)) + + msk = ~shp.vectorized.contains( + self._polygon, + lon, + lat, + ) + + return np.stack((lon[msk], lat[msk])) + + +@dataclass +class ParametersTerrestrialGrid(ParametersBase): + cell_radius: float = None + + transform_grid_randomly: bool = False + + grid_exclusion_zone: ParametersZone = field( + default_factory=lambda: ParametersZone(_ACCEPT_NONE_TYPE=True) + ) + + grid_in_zone: ParametersZone = field( + default_factory=lambda: ParametersZone(type="FROM_COUNTRIES") + ) + + # 2xN, ([lon], [lat]) + lon_lat_grid = None + + def validate(self, ctx: str): + """ + Validate the service grid parameters. + + Ensures that country names and beam radius are set and valid, and sets grid margin if needed. + + Parameters + ---------- + ctx : str + Context string for error messages. + """ + # NOTE: prefer this to be set by a parent/composition + if not isinstance( + self.cell_radius, + float) and not isinstance( + self.cell_radius, + int): + raise ValueError(f"{ctx}.cell_radius needs to be a number") + + if self.grid_in_zone.from_countries.margin_from_border is None: + self.grid_in_zone.from_countries.margin_from_border = self.cell_radius / 1e3 + if self.grid_exclusion_zone.from_countries.margin_from_border is None: + self.grid_exclusion_zone.from_countries.margin_from_border = self.cell_radius / 1e3 + + super().validate(ctx) + + self._recalculate_grid_polygon_if_needed(ctx) + + def reset_grid( + self, + ctx: str, + rng: np.random.RandomState, + force_update=False, + ): + """ + After creating grid, there are some features that can only be implemented + with knowledge of other parts of the simulator. + """ + self._recalculate_grid_polygon_if_needed(ctx, force_update) + + lon, lat = generate_grid_in_multipolygon( + self.grid_in_zone._polygon, + self.cell_radius, + self.transform_grid_randomly, + rng + ) + + self.lon_lat_grid = self.grid_exclusion_zone.apply_exclusion_zone( + lon, lat + ) + + self.ecef_grid = lla2ecef( + self.lon_lat_grid[1], self.lon_lat_grid[0], 0) + + def _recalculate_grid_polygon_if_needed(self, ctx: str, force_update=False): + if self.grid_in_zone._polygon is not None and not force_update: + return + self.grid_in_zone._calculate_polygon() + self.grid_exclusion_zone._calculate_polygon() + + +@dataclass +class ParametersSatelliteWithServiceGrid(ParametersTerrestrialGrid): + """ + Adds parameters for satellite that uses terrestrial service grid for positioning + """ + # margin from inside of border [km] + # if positive, makes border smaller by x km + # if negative, makes border bigger by x km + eligible_sats_margin_from_border: float = None + + eligibility_polygon: typing.Union[shp.MultiPolygon, shp.Polygon] = None + + beam_radius: float = None + + def validate(self, ctx): + if self.cell_radius is not None: + warn( + f"{ctx}.cell_radius should be set through beam_radius parameter" + ) + self.cell_radius = self.beam_radius + + if not isinstance( + self.eligible_sats_margin_from_border, + float) and not isinstance( + self.eligible_sats_margin_from_border, + int): + raise ValueError( + f"{ctx}.eligible_sats_margin_from_border needs to be a number") + + super().validate(ctx) + + def load_from_active_sat_conditions( + self, + sat_is_active_if: "ParametersSelectActiveSatellite", + ): + """ + Load grid parameters from active satellite selection conditions. + + Parameters + ---------- + sat_is_active_if : ParametersSelectActiveSatellite + The object containing satellite selection and country information. + """ + if ( + len(self.grid_in_zone.from_countries.country_names) == 0 + or self.grid_in_zone.from_countries.country_names[0] == "" + ): + self.grid_in_zone.from_countries.country_names = sat_is_active_if.lat_long_inside_country.country_names + if self.eligible_sats_margin_from_border is None: + self.eligible_sats_margin_from_border = sat_is_active_if.lat_long_inside_country.margin_from_border + + def _recalculate_grid_polygon_if_needed(self, ctx: str, force_update=False): + if self.eligibility_polygon is not None and not force_update: + return + self.grid_in_zone._calculate_polygon() + self.grid_exclusion_zone._calculate_polygon() + + # TODO: plot this to verify + self.eligibility_polygon = shp.ops.unary_union(shrink_countries_by_km( + self.grid_in_zone._unprocessed_polygon, + self.eligible_sats_margin_from_border, + )) + + assert self.eligibility_polygon.is_valid, \ + shp.validation.explain_validity(self.eligibility_polygon) + + assert not self.eligibility_polygon.is_empty, \ + "Can't have a empty eligibility_polygon as filter" + + +@dataclass +class ParametersSelectActiveSatellite(ParametersBase): + """ + Parameters for selecting active satellites based on geographic and elevation criteria. + """ + @dataclass + class ParametersLatLongInsideCountry(ParametersBase): + """ + Parameters for checking if a location is inside a given country. + """ + country_shapes_filename: Path = SHARC_ROOT_DIR / "sharc" / \ + "data" / "countries" / "ne_110m_admin_0_countries.shp" + + country_names: list[str] = field(default_factory=lambda: list([""])) + + # margin from inside of border [km] + # if positive, makes border smaller by x km + # if negative, makes border bigger by x km + margin_from_border: float = 0.0 + + # geometry after file processing + filter_polygon: typing.Union[shp.MultiPolygon, shp.Polygon] = None + + def validate(self, ctx: str): + """ + Validate the country names and filter polygon for the location check. + + Parameters + ---------- + ctx : str + Context string for error messages. + """ + # conditional is weird due to suboptimal way of working with nested + # array parameters + if len(self.country_names) == 0 or ( + len(self.country_names) == 1 and self.country_names[0] == ""): + raise ValueError( + f"You need to pass at least one country name to {ctx}.country_names") + + self.reset_filter_polygon(ctx) + + def reset_filter_polygon(self, ctx: str, force_update=False): + """ + Reset the filter polygon for country boundaries, optionally forcing update. + + Parameters + ---------- + ctx : str + Context string for error messages. + force_update : bool, optional + If True, force update even if already set (default is False). + """ + if self.filter_polygon is not None and not force_update: + return + + filtered_gdf = load_gdf( + self.country_shapes_filename, + { + "NAME": self.country_names + }, + ctx, + ) + + # shrink countries and unite + # them into a single MultiPolygon + self.filter_polygon = shp.ops.unary_union(shrink_countries_by_km( + filtered_gdf.geometry.values, self.margin_from_border + )) + + assert self.filter_polygon.is_valid, shp.validation.explain_validity( + self.filter_polygon) + + __ALLOWED_CONDITIONS = [ + "LAT_LONG_INSIDE_COUNTRY", + "MINIMUM_ELEVATION_FROM_ES", + "MAXIMUM_ELEVATION_FROM_ES", + ] + + conditions: list[typing.Literal[ + "LAT_LONG_INSIDE_COUNTRY", + "MINIMUM_ELEVATION_FROM_ES", + "MAXIMUM_ELEVATION_FROM_ES", + ]] = field(default_factory=lambda: list([""])) + + minimum_elevation_from_es: float = None + + maximum_elevation_from_es: float = None + + lat_long_inside_country: ParametersLatLongInsideCountry = field( + default_factory=ParametersLatLongInsideCountry) + + def validate(self, ctx): + """ + Validate the satellite selection conditions and their parameters. + + Parameters + ---------- + ctx : str + Context string for error messages. + """ + if "LAT_LONG_INSIDE_COUNTRY" in self.conditions: + self.lat_long_inside_country.validate( + f"{ctx}.lat_long_inside_country") + + if "MINIMUM_ELEVATION_FROM_ES" in self.conditions: + if not isinstance( + self.minimum_elevation_from_es, + float) and not isinstance( + self.minimum_elevation_from_es, + int): + raise ValueError( + f"{ctx}.minimum_elevation_from_es is not a number!" + ) + if not (self.minimum_elevation_from_es >= + 0 and self.minimum_elevation_from_es < 90): + raise ValueError( + f"{ctx}.minimum_elevation_from_es needs to be a number in interval [0, 90]") + + if "MAXIMUM_ELEVATION_FROM_ES" in self.conditions: + if not isinstance( + self.maximum_elevation_from_es, + float) and not isinstance( + self.maximum_elevation_from_es, + int): + raise ValueError( + f"{ctx}.maximum_elevation_from_es is not a number!" + ) + if not (self.maximum_elevation_from_es >= + 0 and self.maximum_elevation_from_es < 90): + raise ValueError( + f"{ctx}.maximum_elevation_from_es needs to be a number in interval [0, 90]") + if "MINIMUM_ELEVATION_FROM_ES" in self.conditions: + if self.maximum_elevation_from_es < self.minimum_elevation_from_es: + raise ValueError( + f"{ctx}.maximum_elevation_from_es needs to be >= {ctx}.minimum_elevation_from_es") + + if len(self.conditions) == 1 and self.conditions[0] == "": + self.conditions.pop() + + if any(cond not in self.__ALLOWED_CONDITIONS for cond in self.conditions): + raise ValueError( + f"{ctx}.conditions = { + self.conditions}\n" f"However, only the following are allowed: { + self.__ALLOWED_CONDITIONS}") + + if len(set(self.conditions)) != len(self.conditions): + raise ValueError( + f"{ctx}.conditions = {self.conditions}\n" + "And it contains duplicate values!" + ) + diff --git a/sharc/parameters/imt/parameters_imt_mss_dc.py b/sharc/parameters/imt/parameters_imt_mss_dc.py index afcbc9504..c14079f4f 100644 --- a/sharc/parameters/imt/parameters_imt_mss_dc.py +++ b/sharc/parameters/imt/parameters_imt_mss_dc.py @@ -3,17 +3,10 @@ import numpy as np import typing from pathlib import Path -import shapely as shp - -from sharc.support.sharc_utils import load_gdf -from sharc.support.sharc_geom import ( - shrink_countries_by_km, - generate_grid_in_multipolygon, - shrink_lonlat_polygon_by_km, -) -from sharc.satellite.utils.sat_utils import lla2ecef + from sharc.parameters.parameters_base import ParametersBase from sharc.parameters.parameters_orbit import ParametersOrbit +from sharc.parameters.imt.parameters_grid import ParametersSatelliteWithServiceGrid, ParametersSelectActiveSatellite SHARC_ROOT_DIR = (Path(__file__) / ".." / ".." / ".." / "..").resolve() @@ -133,280 +126,6 @@ def validate(self, ctx): f"No validation implemented for {ctx}.type = { self.type}") - @dataclass - class ParametersServiceGrid(ParametersBase): - country_shapes_filename: Path = SHARC_ROOT_DIR / "sharc" / \ - "data" / "countries" / "ne_110m_admin_0_countries.shp" - - @dataclass - class ParametersExclusionZone(ParametersBase): - @dataclass - class ParametersCircle(ParametersBase): - center_lat: typing.Optional[float] = None - center_lon: typing.Optional[float] = None - radius_km: typing.Optional[float] = None - - _polygon: shp.Polygon = None - - def validate(self, ctx): - """ - Validates instance parameters. - - Ensures attributes make sense - - Parameters - ---------- - ctx : str - Context string for error messages. - - Raises - ------ - ValueError - If a parameter is not valid. - """ - if None in [ - self.center_lat, - self.center_lon, - self.radius_km, - ]: - raise ValueError( - f"{ctx}.(center_lat|center_lon|radius_km) need to be set" - ) - - if self.radius_km <= 0: - raise ValueError(f"{ctx}.radius_km needs to be positive") - - if not (-180. <= self.center_lon <= 180.): - raise ValueError(f"{ctx}.center_lon needs to be in [-180, 180]") - - if not (-90. <= self.center_lat <= 90.): - raise ValueError(f"{ctx}.center_lat needs to be in [-90, 90]") - - super().validate(ctx) - - self._calculate_polygon() - - def _calculate_polygon(self): - """ - Calculates circle lon,lat polygon according to its attributes - """ - self._polygon = shrink_lonlat_polygon_by_km( - shp.geometry.Point(self.center_lon, self.center_lat), - -self.radius_km - ) - - __ALLOWED_TYPES = [None, "CIRCLE"] - type: typing.Literal[None, "CIRCLE"] = None - - circle: ParametersCircle = field(default_factory=ParametersCircle) - - _polygon: shp.geometry.Polygon = None - - def validate(self, ctx): - """ - Validates instance parameters. - - Ensures attributes make sense - - Parameters - ---------- - ctx : str - Context string for error messages. - - Raises - ------ - ValueError - If a parameter is not valid. - """ - if self.type not in self.__ALLOWED_TYPES: - raise ValueError(f"{ctx}.type should be in {self.__ALLOWED_TYPES}") - - if self.type is None: - return - - if self.type == "CIRCLE": - self.circle.validate(f"{ctx}.circle") - self._polygon = self.circle._polygon - else: - raise NotImplementedError( - "No validation implemented for\n" - f"\t{ctx}.type == {self.type}" - ) - - if (not self._polygon.is_valid - or self._polygon.is_empty - or self._polygon.area <= 0 - ): - raise Exception(f"Bad {ctx}._polygon was generated") - - def _calculate_polygon(self): - if self.type == "CIRCLE": - self.circle._calculate_polygon() - self._polygon = self.circle._polygon - elif self.type is None: - self._polygon = None - else: - raise NotImplementedError( - f"Polygon calculation for type = {self.type}" - ) - - def apply_exclusion_zone(self, lon, lat): - """ - Returns coordinates that are not contained in polygon - """ - if self.type is None: - return np.stack((lon, lat)) - - msk = ~shp.vectorized.contains( - self._polygon, - lon, - lat, - ) - - return np.stack((lon[msk], lat[msk])) - - country_names: list[str] = field(default_factory=lambda: list([""])) - - transform_grid_randomly: bool = False - - grid_exclusion_zone: ParametersExclusionZone = field(default_factory=ParametersExclusionZone) - - # margin from inside of border [km] - # if positive, makes border smaller by x km - # if negative, makes border bigger by x km - grid_margin_from_border: float = None - - # margin from inside of border [km] - # if positive, makes border smaller by x km - # if negative, makes border bigger by x km - eligible_sats_margin_from_border: float = None - - beam_radius: float = None - - # 2xN, ([lon], [lat]) - lon_lat_grid = None - - eligibility_polygon: typing.Union[shp.MultiPolygon, shp.Polygon] = None - - def validate(self, ctx: str): - """ - Validate the service grid parameters. - - Ensures that country names and beam radius are set and valid, and sets grid margin if needed. - - Parameters - ---------- - ctx : str - Context string for error messages. - """ - # conditional is weird due to suboptimal way of working with nested - # array parameters - if len(self.country_names) == 0 or ( - len(self.country_names) == 1 and self.country_names[0] == ""): - raise ValueError( - f"You need to pass at least one country name to {ctx}.country_names") - - # NOTE: prefer this to be set by a parent/composition - if not isinstance( - self.beam_radius, - float) and not isinstance( - self.beam_radius, - int): - raise ValueError(f"{ctx}.beam_radius needs to be a number") - - if self.grid_margin_from_border is None: - self.grid_margin_from_border = self.beam_radius / 1e3 - if not isinstance( - self.grid_margin_from_border, - float) and not isinstance( - self.grid_margin_from_border, - int): - raise ValueError( - f"{ctx}.grid_margin_from_border needs to be a number") - - if not isinstance( - self.eligible_sats_margin_from_border, - float) and not isinstance( - self.eligible_sats_margin_from_border, - int): - raise ValueError( - f"{ctx}.eligible_sats_margin_from_border needs to be a number") - - super().validate(ctx) - - self._load_geom_from_file_if_needed(ctx) - - def load_from_active_sat_conditions( - self, - sat_is_active_if: "ParametersSelectActiveSatellite", - ): - """ - Load grid parameters from active satellite selection conditions. - - Parameters - ---------- - sat_is_active_if : ParametersSelectActiveSatellite - The object containing satellite selection and country information. - """ - if len(self.country_names) == 0 or self.country_names[0] == "": - self.country_names = sat_is_active_if.lat_long_inside_country.country_names - if self.eligible_sats_margin_from_border is None: - self.eligible_sats_margin_from_border = sat_is_active_if.lat_long_inside_country.margin_from_border - - def reset_grid( - self, - ctx: str, - rng: np.random.RandomState, - force_update=False, - ): - """ - After creating grid, there are some features that can only be implemented - with knowledge of other parts of the simulator. - """ - self._load_geom_from_file_if_needed(ctx, force_update) - - lon, lat = generate_grid_in_multipolygon( - self.grid_borders_polygon, - self.beam_radius, - self.transform_grid_randomly, - rng - ) - - self.lon_lat_grid = self.grid_exclusion_zone.apply_exclusion_zone( - lon, lat - ) - - self.ecef_grid = lla2ecef( - self.lon_lat_grid[1], self.lon_lat_grid[0], 0) - - def _load_geom_from_file_if_needed(self, ctx: str, force_update=False): - if self.eligibility_polygon is not None and not force_update: - return - filtered_gdf = load_gdf( - self.country_shapes_filename, - { - "NAME": self.country_names - }, - ctx, - ) - - # shrink countries and unite - # them into a single MultiPolygon - shrinked = shrink_countries_by_km( - filtered_gdf.geometry.values, self.grid_margin_from_border - ) - self.grid_borders_polygon = shp.ops.unary_union(shrinked) - - assert self.grid_borders_polygon.is_valid, \ - shp.validation.explain_validity(self.grid_borders_polygon) - - assert not self.grid_borders_polygon.is_empty, \ - "Can't have a empty grid_borders_polygon as filter" - - self.eligibility_polygon = shp.ops.unary_union(shrink_countries_by_km( - filtered_gdf.geometry.values, self.eligible_sats_margin_from_border - )) - __ALLOWED_TYPES = [ "ANGLE_FROM_SUBSATELLITE", "ANGLE_AND_DISTANCE_FROM_SUBSATELLITE", @@ -434,8 +153,8 @@ def _load_geom_from_file_if_needed(self, ctx: str, force_update=False): default_factory=lambda: ParametersSectorPositioning.ParametersSectorValue( MIN_VALUE=0.0)) - service_grid: ParametersServiceGrid = field( - default_factory=ParametersServiceGrid) + service_grid: ParametersSatelliteWithServiceGrid = field( + default_factory=ParametersSatelliteWithServiceGrid) def validate(self, ctx): """ @@ -470,158 +189,6 @@ def validate(self, ctx): ) -@dataclass -class ParametersSelectActiveSatellite(ParametersBase): - """ - Parameters for selecting active satellites based on geographic and elevation criteria. - """ - @dataclass - class ParametersLatLongInsideCountry(ParametersBase): - """ - Parameters for checking if a location is inside a given country. - """ - country_shapes_filename: Path = SHARC_ROOT_DIR / "sharc" / \ - "data" / "countries" / "ne_110m_admin_0_countries.shp" - - country_names: list[str] = field(default_factory=lambda: list([""])) - - # margin from inside of border [km] - # if positive, makes border smaller by x km - # if negative, makes border bigger by x km - margin_from_border: float = 0.0 - - # geometry after file processing - filter_polygon: typing.Union[shp.MultiPolygon, shp.Polygon] = None - - def validate(self, ctx: str): - """ - Validate the country names and filter polygon for the location check. - - Parameters - ---------- - ctx : str - Context string for error messages. - """ - # conditional is weird due to suboptimal way of working with nested - # array parameters - if len(self.country_names) == 0 or ( - len(self.country_names) == 1 and self.country_names[0] == ""): - raise ValueError( - f"You need to pass at least one country name to {ctx}.country_names") - - self.reset_filter_polygon(ctx) - - def reset_filter_polygon(self, ctx: str, force_update=False): - """ - Reset the filter polygon for country boundaries, optionally forcing update. - - Parameters - ---------- - ctx : str - Context string for error messages. - force_update : bool, optional - If True, force update even if already set (default is False). - """ - if self.filter_polygon is not None and not force_update: - return - - filtered_gdf = load_gdf( - self.country_shapes_filename, - { - "NAME": self.country_names - }, - ctx, - ) - - # shrink countries and unite - # them into a single MultiPolygon - self.filter_polygon = shp.ops.unary_union(shrink_countries_by_km( - filtered_gdf.geometry.values, self.margin_from_border - )) - - assert self.filter_polygon.is_valid, shp.validation.explain_validity( - self.filter_polygon) - - __ALLOWED_CONDITIONS = [ - "LAT_LONG_INSIDE_COUNTRY", - "MINIMUM_ELEVATION_FROM_ES", - "MAXIMUM_ELEVATION_FROM_ES", - ] - - conditions: list[typing.Literal[ - "LAT_LONG_INSIDE_COUNTRY", - "MINIMUM_ELEVATION_FROM_ES", - "MAXIMUM_ELEVATION_FROM_ES", - ]] = field(default_factory=lambda: list([""])) - - minimum_elevation_from_es: float = None - - maximum_elevation_from_es: float = None - - lat_long_inside_country: ParametersLatLongInsideCountry = field( - default_factory=ParametersLatLongInsideCountry) - - def validate(self, ctx): - """ - Validate the satellite selection conditions and their parameters. - - Parameters - ---------- - ctx : str - Context string for error messages. - """ - if "LAT_LONG_INSIDE_COUNTRY" in self.conditions: - self.lat_long_inside_country.validate( - f"{ctx}.lat_long_inside_country") - - if "MINIMUM_ELEVATION_FROM_ES" in self.conditions: - if not isinstance( - self.minimum_elevation_from_es, - float) and not isinstance( - self.minimum_elevation_from_es, - int): - raise ValueError( - f"{ctx}.minimum_elevation_from_es is not a number!" - ) - if not (self.minimum_elevation_from_es >= - 0 and self.minimum_elevation_from_es < 90): - raise ValueError( - f"{ctx}.minimum_elevation_from_es needs to be a number in interval [0, 90]") - - if "MAXIMUM_ELEVATION_FROM_ES" in self.conditions: - if not isinstance( - self.maximum_elevation_from_es, - float) and not isinstance( - self.maximum_elevation_from_es, - int): - raise ValueError( - f"{ctx}.maximum_elevation_from_es is not a number!" - ) - if not (self.maximum_elevation_from_es >= - 0 and self.maximum_elevation_from_es < 90): - raise ValueError( - f"{ctx}.maximum_elevation_from_es needs to be a number in interval [0, 90]") - if "MINIMUM_ELEVATION_FROM_ES" in self.conditions: - if self.maximum_elevation_from_es < self.minimum_elevation_from_es: - raise ValueError( - f"{ctx}.maximum_elevation_from_es needs to be >= {ctx}.minimum_elevation_from_es") - - if len(self.conditions) == 1 and self.conditions[0] == "": - self.conditions.pop() - - if any(cond not in self.__ALLOWED_CONDITIONS for cond in self.conditions): - raise ValueError( - f"{ctx}.conditions = { - self.conditions}\n" f"However, only the following are allowed: { - self.__ALLOWED_CONDITIONS}") - - if len(set(self.conditions)) != len(self.conditions): - raise ValueError( - f"{ctx}.conditions = {self.conditions}\n" - "And it contains duplicate values!" - ) - - @dataclass class ParametersImtMssDc(ParametersBase): """Dataclass for the IMT MSS-DC topology parameters.""" diff --git a/tests/parameters/parameters_for_testing.yaml b/tests/parameters/parameters_for_testing.yaml index ed61cb379..484cd9388 100644 --- a/tests/parameters/parameters_for_testing.yaml +++ b/tests/parameters/parameters_for_testing.yaml @@ -233,7 +233,7 @@ imt: max: 66.1 service_grid: grid_exclusion_zone: - type: CIRCLE + type: CIRCLE # may be "CIRCLE" or "FROM_COUNTRIES" circle: center_lat: -14.123 center_lon: -47.1 @@ -241,19 +241,21 @@ imt: transform_grid_randomly: true # add per drop rand rotation + transl. to grid # by default this already gets shapefile from natural earth - country_shapes_filename: sharc/data/countries/ne_110m_admin_0_countries.shp - # By default, these are the same as taken from `sat_is_active_if.lat_long_inside_country` - country_names: - - Brazil - - Chile - - # This margin defines where the grid is constructed - # margin from inside of border [km] - # By default, this is made to be the same as beam_radius - # if positive, makes border smaller by x km - # if negative, makes border bigger by x km - grid_margin_from_border: 0.11 + grid_in_zone: + type: FROM_COUNTRIES # may be "CIRCLE" or "FROM_COUNTRIES" + from_countries: + country_shapes_filename: sharc/data/countries/ne_110m_admin_0_countries.shp + # By default, these are the same as taken from `sat_is_active_if.lat_long_inside_country` + country_names: + - Brazil + - Chile + # This margin defines where the grid is constructed + # margin from inside of border [km] + # By default, this is made to be the same as beam_radius + # if positive, makes border smaller by x km + # if negative, makes border bigger by x km + margin_from_border: 0.11 # This margin defines what satellites may serve the grid # it should normally be a smaller value than the grid from border # since a satellite not right above the grid may serve some point better @@ -995,7 +997,7 @@ mss_d2d: # Number of sectors num_sectors: 19 beam_positioning: - # type may be one of + # type may be one of # "ANGLE_FROM_SUBSATELLITE", "ANGLE_AND_DISTANCE_FROM_SUBSATELLITE", # "SERVICE_GRID" # when "ANGLE_FROM_SUBSATELLITE", both phi and theta must be specified @@ -1038,26 +1040,29 @@ mss_d2d: max: 66.1 service_grid: grid_exclusion_zone: - type: CIRCLE + type: CIRCLE # may be "CIRCLE" or "FROM_COUNTRIES" circle: center_lat: -14.123 center_lon: 120 radius_km: 321 transform_grid_randomly: true # add per drop rand rotation + transl. to grid - # by default this already gets shapefile from natural earth - country_shapes_filename: sharc/data/countries/ne_110m_admin_0_countries.shp - # By default, these are the same as taken from `sat_is_active_if.lat_long_inside_country` - country_names: - - Brazil - - Chile - # This margin defines where the grid is constructed - # margin from inside of border [km] - # By default, this is made to be the same as beam_radius - # if positive, makes border smaller by x km - # if negative, makes border bigger by x km - grid_margin_from_border: 0.11 + grid_in_zone: + type: FROM_COUNTRIES # may be "CIRCLE" or "FROM_COUNTRIES" + from_countries: + country_shapes_filename: sharc/data/countries/ne_110m_admin_0_countries.shp + # By default, these are the same as taken from `sat_is_active_if.lat_long_inside_country` + country_names: + - Brazil + - Chile + + # This margin defines where the grid is constructed + # margin from inside of border [km] + # By default, this is made to be the same as beam_radius + # if positive, makes border smaller by x km + # if negative, makes border bigger by x km + margin_from_border: 0.11 # This margin defines what satellites may serve the grid # it should normally be a smaller value than the grid from border diff --git a/tests/parameters/test_parameters.py b/tests/parameters/test_parameters.py index 0169a47dc..8de556032 100644 --- a/tests/parameters/test_parameters.py +++ b/tests/parameters/test_parameters.py @@ -309,17 +309,17 @@ def test_parameters_imt(self): self.parameters.imt.topology.mss_dc.beam_positioning.service_grid.transform_grid_randomly, True) self.assertEqual( - self.parameters.imt.topology.mss_dc.beam_positioning.service_grid.grid_margin_from_border, + self.parameters.imt.topology.mss_dc.beam_positioning.service_grid.grid_in_zone.from_countries.margin_from_border, 0.11) self.assertEqual( self.parameters.imt.topology.mss_dc.beam_positioning.service_grid.eligible_sats_margin_from_border, -2.1) self.assertEqual(len( - self.parameters.imt.topology.mss_dc.beam_positioning.service_grid.country_names), 2) + self.parameters.imt.topology.mss_dc.beam_positioning.service_grid.grid_in_zone.from_countries.country_names), 2) self.assertEqual( - self.parameters.imt.topology.mss_dc.beam_positioning.service_grid.country_names[0], + self.parameters.imt.topology.mss_dc.beam_positioning.service_grid.grid_in_zone.from_countries.country_names[0], "Brazil") self.assertEqual( - self.parameters.imt.topology.mss_dc.beam_positioning.service_grid.country_names[1], + self.parameters.imt.topology.mss_dc.beam_positioning.service_grid.grid_in_zone.from_countries.country_names[1], "Chile") self.assertEqual(len( @@ -672,17 +672,17 @@ def test_parametes_mss_d2d(self): self.parameters.mss_d2d.beam_positioning.service_grid.transform_grid_randomly, True) self.assertEqual( - self.parameters.mss_d2d.beam_positioning.service_grid.grid_margin_from_border, + self.parameters.mss_d2d.beam_positioning.service_grid.grid_in_zone.from_countries.margin_from_border, 0.11) self.assertEqual( self.parameters.mss_d2d.beam_positioning.service_grid.eligible_sats_margin_from_border, -2.1) self.assertEqual( - len(self.parameters.mss_d2d.beam_positioning.service_grid.country_names), 2) + len(self.parameters.mss_d2d.beam_positioning.service_grid.grid_in_zone.from_countries.country_names), 2) self.assertEqual( - self.parameters.mss_d2d.beam_positioning.service_grid.country_names[0], + self.parameters.mss_d2d.beam_positioning.service_grid.grid_in_zone.from_countries.country_names[0], "Brazil") self.assertEqual( - self.parameters.mss_d2d.beam_positioning.service_grid.country_names[1], + self.parameters.mss_d2d.beam_positioning.service_grid.grid_in_zone.from_countries.country_names[1], "Chile") self.assertEqual( @@ -929,7 +929,7 @@ def test_mss_d2d_loaded_service_grid(self): beam_radius_m = 40e3 seed = 2 self.parameters.mss_d2d.beam_positioning.service_grid.beam_radius = beam_radius_m - self.parameters.mss_d2d.beam_positioning.service_grid.grid_margin_from_border = beam_radius_m / 1e3 + self.parameters.mss_d2d.beam_positioning.service_grid.grid_in_zone.from_countries.margin_from_border = beam_radius_m / 1e3 rng = np.random.RandomState(seed) self.parameters.mss_d2d.beam_positioning.service_grid.country_names = [ @@ -967,7 +967,7 @@ def test_mss_d2d_loaded_service_grid(self): # at frienship bridge, so should affect more than 1 grid grid_exclusion_zone.circle.center_lat = -25.5094741 grid_exclusion_zone.circle.center_lon = -54.6007197 - grid_exclusion_zone.circle.radius_km = 2 * beam_radius_m / 1e3 + grid_exclusion_zone.circle.radius_km = 4 * beam_radius_m / 1e3 rng = np.random.RandomState(seed) grid_exclusion_zone._calculate_polygon() @@ -979,8 +979,8 @@ def test_mss_d2d_loaded_service_grid(self): n_original = original_grid.shape[1] n_after = grid_w_exclusion.shape[1] - # aft >= orig - 6 - self.assertLessEqual(n_original - 6, n_after) + # aft >= orig - 12 + self.assertLessEqual(n_original - 12, n_after) # aft < orig self.assertLess(n_after, n_original) From 66f974d6f56d3a311000ae2a9a9ca36482d94949 Mon Sep 17 00:00:00 2001 From: artistrea Date: Wed, 8 Oct 2025 02:30:03 -0300 Subject: [PATCH 22/77] hotfix: simulation geometry unset variable --- sharc/support/geometry.py | 95 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/sharc/support/geometry.py b/sharc/support/geometry.py index dd5dd854a..b7a11bcb2 100644 --- a/sharc/support/geometry.py +++ b/sharc/support/geometry.py @@ -370,6 +370,7 @@ def setup( raise ValueError( "If there will be a local ref, global coord sys must be passed" ) + self.uses_local_coords = True self._x_local = np.empty(num_geometries) self._y_local = np.empty(num_geometries) @@ -578,4 +579,98 @@ def get_local_elevation(self, other: "SimulatorGeometry") -> np.array: raise NotImplementedError() +if __name__ == "__main__": + from sharc.topology.topology_macrocell import TopologyMacrocell + rng = np.random.RandomState(seed=0xcaffe) + topology = TopologyMacrocell( + 3 * 111e3, 1 + ) + + topology.calculate_coordinates(rng) + + global_lla = (-14, -45, 1200) + + num_ue = 3 + + bs_geom = SimulatorGeometry( + topology.num_base_stations, + topology.num_base_stations, + global_lla + ) + ue_geom = SimulatorGeometry( + num_ue * topology.num_base_stations, + num_ue * topology.num_base_stations, + global_lla + ) + + bs_geom.set_local_coord_sys( + np.repeat(11, topology.num_base_stations), + np.repeat(-47, topology.num_base_stations), + np.repeat(1200, topology.num_base_stations), + ) + ue_geom.set_local_coord_sys( + np.repeat(11, num_ue * topology.num_base_stations), + np.repeat(-47, num_ue * topology.num_base_stations), + np.repeat(1200, num_ue * topology.num_base_stations), + ) + + bs_geom.set_local_coords( + topology.x, + topology.y, + topology.z, + ) + + from sharc.station_factory import StationFactory + ue_x, ue_y, ue_z, _, _ = StationFactory.get_random_position( + num_ue * topology.num_base_stations, + topology, + rng, + min_dist_to_bs=35., deterministic_cell=True + ) + ue_geom.set_local_coords( + np.array(ue_x), + np.array(ue_y), + np.array(ue_z), + ) + + from sharc.satellite.scripts.plot_globe import plot_globe_with_borders + from sharc.support.sharc_geom import CoordinateSystem + + # for plotting + global_cs = CoordinateSystem() + global_cs.set_reference( + *global_lla + ) + fig = plot_globe_with_borders( + False, global_cs, False + ) + + import plotly.graph_objects as go + + fig.add_trace(go.Scatter3d( + x=bs_geom.x_global, + y=bs_geom.y_global, + z=bs_geom.z_global, + mode='markers', + marker=dict( + size=3, + color='blue', + opacity=1.0 + ), + name='BS' + )) + fig.add_trace(go.Scatter3d( + x=ue_geom.x_global, + y=ue_geom.y_global, + z=ue_geom.z_global, + mode='markers', + marker=dict( + size=1, + color='red', + opacity=1.0 + ), + name='UE' + )) + + fig.show() From 939807b9347569a6575cd640e5466bb09f864495 Mon Sep 17 00:00:00 2001 From: artistrea Date: Wed, 8 Oct 2025 11:35:50 -0300 Subject: [PATCH 23/77] fix: station factory mss ss spectral mask parameter --- sharc/station_factory.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sharc/station_factory.py b/sharc/station_factory.py index 94974f863..fafb48963 100644 --- a/sharc/station_factory.py +++ b/sharc/station_factory.py @@ -1660,10 +1660,10 @@ def generate_mss_ss(param_mss: ParametersMssSs): param_mss.bandwidth, param_mss.spurious_emissions, scenario="OUTDOOR") - elif params.spectral_mask == "MSS": - mss_ss.spectral_mask = SpectralMaskMSS(params.frequency, - params.bandwidth, - params.spurious_emissions) + elif param_mss.spectral_mask == "MSS": + mss_ss.spectral_mask = SpectralMaskMSS(param_mss.frequency, + param_mss.bandwidth, + param_mss.spurious_emissions) else: raise ValueError( f"Invalid or not implemented spectral mask - {param_mss.spectral_mask}") From b4e5a318f0e818816233125e6a9bf45e897bace8 Mon Sep 17 00:00:00 2001 From: artistrea Date: Wed, 8 Oct 2025 11:59:58 -0300 Subject: [PATCH 24/77] feat: topology_spherical_sampling_from_grid --- sharc/parameters/imt/parameters_grid.py | 50 ++-- .../parameters/imt/parameters_imt_topology.py | 7 +- sharc/satellite/scripts/plot_footprints.py | 7 + sharc/station_factory.py | 197 +++++++-------- sharc/support/geometry.py | 226 ++++++++++++++---- sharc/topology/topology.py | 10 + sharc/topology/topology_factory.py | 8 + sharc/topology/topology_imt_mss_dc.py | 9 +- .../topology_spherical_sampling_from_grid.py | 184 ++++++++++++++ tests/parameters/parameters_for_testing.yaml | 24 ++ tests/parameters/test_parameters.py | 33 +++ 11 files changed, 569 insertions(+), 186 deletions(-) create mode 100644 sharc/topology/topology_spherical_sampling_from_grid.py diff --git a/sharc/parameters/imt/parameters_grid.py b/sharc/parameters/imt/parameters_grid.py index 1ac4fec5c..a7d82f4cc 100644 --- a/sharc/parameters/imt/parameters_grid.py +++ b/sharc/parameters/imt/parameters_grid.py @@ -19,6 +19,8 @@ @dataclass class ParametersZone(ParametersBase): + """Defines parameters for the creation of a 'zone' polygon. + """ @dataclass class ParametersCircle(ParametersBase): center_lat: typing.Optional[float] = None @@ -73,6 +75,7 @@ def _calculate_polygon(self): shp.geometry.Point(self.center_lon, self.center_lat), -self.radius_km ) + @dataclass class ParametersFromCountries(ParametersBase): country_shapes_filename: Path = SHARC_ROOT_DIR / "sharc" / \ @@ -146,7 +149,7 @@ def _calculate_polygon(self): _polygon: shp.geometry.Polygon = None _unprocessed_polygon: shp.geometry.Polygon = None - def set_chosen_pol(self): + def _set_chosen_pol(self): self.chosen_pol = None if self.type is None: return @@ -198,7 +201,7 @@ def validate(self, ctx): f"\t{ctx}.type == {self.type}" ) - self.set_chosen_pol() + self._set_chosen_pol() self._calculate_polygon() if (not self._polygon.is_valid @@ -208,7 +211,7 @@ def validate(self, ctx): raise Exception(f"Bad {ctx}._polygon was generated") def _calculate_polygon(self): - self.set_chosen_pol() + self._set_chosen_pol() if self.chosen_pol is None: if self._ACCEPT_NONE_TYPE: @@ -240,6 +243,9 @@ def apply_exclusion_zone(self, lon, lat): @dataclass class ParametersTerrestrialGrid(ParametersBase): + """Defines parameters for the creation of a (lon, lat) grid considering + spherical Earth. + """ cell_radius: float = None transform_grid_randomly: bool = False @@ -293,27 +299,29 @@ def reset_grid( After creating grid, there are some features that can only be implemented with knowledge of other parts of the simulator. """ - self._recalculate_grid_polygon_if_needed(ctx, force_update) + needed = self._recalculate_grid_polygon_if_needed(ctx, force_update) - lon, lat = generate_grid_in_multipolygon( - self.grid_in_zone._polygon, - self.cell_radius, - self.transform_grid_randomly, - rng - ) + if needed or force_update: + lon, lat = generate_grid_in_multipolygon( + self.grid_in_zone._polygon, + self.cell_radius, + self.transform_grid_randomly, + rng + ) - self.lon_lat_grid = self.grid_exclusion_zone.apply_exclusion_zone( - lon, lat - ) + self.lon_lat_grid = self.grid_exclusion_zone.apply_exclusion_zone( + lon, lat + ) - self.ecef_grid = lla2ecef( - self.lon_lat_grid[1], self.lon_lat_grid[0], 0) + self.ecef_grid = lla2ecef( + self.lon_lat_grid[1], self.lon_lat_grid[0], 0) - def _recalculate_grid_polygon_if_needed(self, ctx: str, force_update=False): + def _recalculate_grid_polygon_if_needed(self, ctx: str, force_update=False) -> bool: if self.grid_in_zone._polygon is not None and not force_update: - return + return False self.grid_in_zone._calculate_polygon() self.grid_exclusion_zone._calculate_polygon() + return True @dataclass @@ -331,6 +339,13 @@ class ParametersSatelliteWithServiceGrid(ParametersTerrestrialGrid): beam_radius: float = None def validate(self, ctx): + """Validates instance parameters. + Parameters + ctx : str + Context string for error messages. + Raises ValueError + If a parameter is not valid. + """ if self.cell_radius is not None: warn( f"{ctx}.cell_radius should be set through beam_radius parameter" @@ -536,4 +551,3 @@ def validate(self, ctx): f"{ctx}.conditions = {self.conditions}\n" "And it contains duplicate values!" ) - diff --git a/sharc/parameters/imt/parameters_imt_topology.py b/sharc/parameters/imt/parameters_imt_topology.py index c4f633cb0..8f4c68025 100644 --- a/sharc/parameters/imt/parameters_imt_topology.py +++ b/sharc/parameters/imt/parameters_imt_topology.py @@ -8,6 +8,7 @@ from sharc.parameters.imt.parameters_ntn import ParametersNTN from sharc.parameters.imt.parameters_imt_mss_dc import ParametersImtMssDc from sharc.parameters.imt.parameters_single_bs import ParametersSingleBS +from sharc.parameters.imt.parameters_spherical_sampling_from_grid import ParametersSamplingFromSphericalGrid @dataclass @@ -18,7 +19,8 @@ class ParametersImtTopology(ParametersBase): nested_parameters_enabled = True type: typing.Literal[ - "MACROCELL", "HOTSPOT", "INDOOR", "SINGLE_BS", "NTN", "MSS_DC" + "MACROCELL", "HOTSPOT", "INDOOR", "SINGLE_BS", "NTN", "MSS_DC", + "SAMPLING_FROM_SPHERICAL_GRID" ] = "MACROCELL" # these parameters are needed in case the other system requires coordinate @@ -33,6 +35,7 @@ class ParametersImtTopology(ParametersBase): single_bs: ParametersSingleBS = field(default_factory=ParametersSingleBS) ntn: ParametersNTN = field(default_factory=ParametersNTN) mss_dc: ParametersImtMssDc = field(default_factory=ParametersImtMssDc) + sampling_from_spherical_grid: ParametersSamplingFromSphericalGrid = field(default_factory=ParametersSamplingFromSphericalGrid) def validate(self, ctx): """ @@ -60,6 +63,8 @@ def validate(self, ctx): self.ntn.validate(f"{ctx}.ntn") case "MSS_DC": self.mss_dc.validate(f"{ctx}.mss_dc") + case "SAMPLING_FROM_SPHERICAL_GRID": + self.sampling_from_spherical_grid.validate(f"{ctx}.sampling_from_spherical_grid") case _: raise NotImplementedError( f"{ctx}.type == '{ diff --git a/sharc/satellite/scripts/plot_footprints.py b/sharc/satellite/scripts/plot_footprints.py index b1ab2397e..52b067be5 100644 --- a/sharc/satellite/scripts/plot_footprints.py +++ b/sharc/satellite/scripts/plot_footprints.py @@ -276,6 +276,7 @@ def plot_fp( )) polygons_lim = plot_mult_polygon( + # params.beam_positioning.service_grid.eligibility_polygon, params.sat_is_active_if.lat_long_inside_country.filter_polygon, coord_sys, True, @@ -392,10 +393,16 @@ def plot_fp( ] params.sat_is_active_if.minimum_elevation_from_es = 5.0 params.sat_is_active_if.lat_long_inside_country.country_names = ["Brazil", "Paraguay"] + params.sat_is_active_if.lat_long_inside_country.margin_from_border = 111 # params.beams_load_factor = 0.1 params.beam_positioning.type = "SERVICE_GRID" grid_exclusion_zone = params.beam_positioning.service_grid.grid_exclusion_zone + grid_exclusion_zone.type = "CIRCLE" + # at frienship bridge, so should affect more than 1 grid + grid_exclusion_zone.circle.center_lat = -25.5094741 + grid_exclusion_zone.circle.center_lon = -54.6007197 + grid_exclusion_zone.circle.radius_km = 5 * spotbeam_radius / 1e3 # grid_exclusion_zone.type = "CIRCLE" # # at frienship bridge, so should affect more than 1 grid diff --git a/sharc/station_factory.py b/sharc/station_factory.py index fafb48963..f3eed4669 100644 --- a/sharc/station_factory.py +++ b/sharc/station_factory.py @@ -113,6 +113,9 @@ def generate_imt_base_stations( ) imt_base_stations.is_space_station = True else: + if topology.determines_local_geometry: + imt_base_stations.geom = topology.get_bs_geometry() + imt_base_stations.geom.set_local_coords( topology.x * np.ones(num_bs), topology.y * np.ones(num_bs), @@ -298,6 +301,8 @@ def generate_imt_ue_outdoor( num_ue = num_bs * num_ue_per_bs imt_ue = StationManager(num_ue) + if topology.determines_local_geometry: + imt_ue.geom = topology.get_ue_geometry(param.ue.k) imt_ue.station_type = StationType.IMT_UE ue_x = list() @@ -1940,143 +1945,105 @@ def get_random_position(num_stas: int, apogee_alt_km=525 ) from sharc.parameters.imt.parameters_imt_mss_dc import ParametersImtMssDc - params = ParametersImtMssDc( + params_mss_dc = ParametersImtMssDc( beam_radius=36516.0, num_beams=7, orbits=[orbit] ) - params.sat_is_active_if.conditions = ["MINIMUM_ELEVATION_FROM_ES"] - params.sat_is_active_if.minimum_elevation_from_es = 5.0 - - topology = TopologyImtMssDc(params, coordinate_system) + params_mss_dc.sat_is_active_if.conditions = ["MINIMUM_ELEVATION_FROM_ES"] + params_mss_dc.sat_is_active_if.minimum_elevation_from_es = 5.0 + + parameters = Parameters() + parameters.imt.topology.mss_dc = params_mss_dc + + parameters.imt.ue.k = 1 + parameters.imt.ue.k_m = 1 + parameters.imt.ue.azimuth_range = (-180, 180) + parameters.imt.ue.distribution_distance = "UNIFORM" + parameters.imt.ue.distribution_type = "ANGLE_AND_DISTANCE" + parameters.imt.ue.distribution_azimuth = "NORMAL" + parameters.imt.ue.height = 1.5 + parameters.imt.ue.indoor_percent = 0 + parameters.imt.bandwidth = 10 + parameters.imt.frequency = 10 + parameters.imt.ue.noise_figure = 0 + + parameters.imt.bs.antenna.array.downtilt = 0 + + parameters.imt.topology.sampling_from_spherical_grid.num_bs = 3 * 3 * 19 + # parameters.imt.topology.sampling_from_spherical_grid.num_bs = 3 * 3 * 19 * 7 + parameters.imt.topology.sampling_from_spherical_grid.max_ue_distance = 800 + parameters.imt.topology.sampling_from_spherical_grid.grid.transform_grid_randomly = True + parameters.imt.topology.sampling_from_spherical_grid.grid.cell_radius = 10e3 + parameters.imt.topology.sampling_from_spherical_grid.grid.grid_in_zone.type = "CIRCLE" + parameters.imt.topology.sampling_from_spherical_grid.grid.grid_in_zone.circle.center_lat = ref_lat + parameters.imt.topology.sampling_from_spherical_grid.grid.grid_in_zone.circle.center_lon = ref_long + parameters.imt.topology.sampling_from_spherical_grid.grid.grid_in_zone.circle.radius_km = 30 * 111 + + parameters.imt.topology.type = "SAMPLING_FROM_SPHERICAL_GRID" + # parameters.imt.topology.type = "MSS_DC" + parameters.imt.validate("station_factory_imt") + # print( + # "parameters.imt.topology.sampling_from_spherical_grid.grid.lon_lat_grid.shape", + # parameters.imt.topology.sampling_from_spherical_grid.grid.lon_lat_grid.shape + # ) + + from sharc.topology.topology_factory import TopologyFactory + topology = TopologyFactory.createTopology( + parameters, + coordinate_system + ) topology.calculate_coordinates(rand_gen) - topology.calculate_coordinates(rand_gen) - parameters = ParametersImt() - parameters.ue.k = 1 - parameters.ue.k_m = 1 - parameters.ue.azimuth_range = (-180, 180) - parameters.ue.distribution_distance = "UNIFORM" - parameters.ue.distribution_type = "ANGLE_AND_DISTANCE" - parameters.ue.distribution_azimuth = "NORMAL" - parameters.ue.height = 1.5 - parameters.ue.indoor_percent = 0 - parameters.bandwidth = 10 - parameters.frequency = 10 - parameters.ue.noise_figure = 0 - imt_ue = StationFactory.generate_imt_ue_outdoor( - parameters, - parameters.ue.antenna.array, + parameters.imt, + parameters.imt.ue.antenna.array, rand_gen, topology ) + imt_bs = StationFactory.generate_imt_base_stations( + parameters.imt, + parameters.imt.bs.antenna.array, + topology, + rand_gen, + ) + # imt_bs.geom.set_local_coords( + # azim=np.zeros_like(imt_bs.geom.pointn_azim_local) + # ) + # imt_bs.geom.set_global_coords( + # elev=np.zeros_like(imt_bs.geom.pointn_azim_local) + 90., + # z=np.zeros_like(imt_bs.geom.pointn_azim_local) + # ) from sharc.satellite.scripts.plot_globe import plot_globe_with_borders fig = plot_globe_with_borders(True, coordinate_system, False) import plotly.graph_objects as go - # fig.add_trace(go.Scatter3d( - # x=topology.x, - # y=topology.y, - # z=topology.z, - # mode='markers', - # marker=dict(size=3, color='green', opacity=0.8), - # showlegend=False - # )) - fig.add_trace(go.Scatter3d( - x=topology.space_station_x, - y=topology.space_station_y, - z=topology.space_station_z, - mode='markers', - marker=dict(size=3, color='green', opacity=0.8), - showlegend=False - )) - - fig.add_trace( - go.Scatter3d( - x=imt_ue.geom.x_global, - y=imt_ue.geom.y_global, - z=imt_ue.geom.z_global, - mode='markers', - marker=dict(size=1, color='red', opacity=1), - showlegend=False - ) - ) - - # TODO: replace this with generate imt mss dc station - st = StationManager(topology.num_base_stations) - st.geom.set_global_coords( - topology.space_station_x, - topology.space_station_y, - topology.space_station_z, - ) - - fig.add_trace( - go.Scatter3d( - x=[0], - y=[0], - z=[0], - mode='markers', - marker=dict(size=3, color='black', opacity=1), - showlegend=False - ) - ) - - from sharc.support.sharc__globalm import polar_to_cartesian - # Plot beam boresight vectors - boresight_length = 100 * 1e3 # Length of the boresight vectors for visualization - boresight_x, boresight_y, boresight_z = polar_to_cartesian( - boresight_length, - imt_ue.geom.pointn_azim_global, - imt_ue.geom.pointn_elev_global - ) - # Add arrow heads to the end of the boresight vectors - for x, y, z, bx, by, bz in zip(imt_ue.geom.x_global, - imt_ue.geom.y_global, - imt_ue.geom.z_global, - boresight_x, - boresight_y, - boresight_z): - fig.add_trace(go.Cone( - x=[x + bx], - y=[y + by], - z=[z + bz], - u=[bx], - v=[by], - w=[bz], - colorscale=[[0, 'orange'], [1, 'orange']], - sizemode='absolute', - sizeref=2 * boresight_length / 5, - showscale=False - )) - for x, y, z, bx, by, bz in zip(imt_ue.geom.x_global, - imt_ue.geom.y_global, - imt_ue.geom.z_global, - boresight_x, - boresight_y, - boresight_z): - fig.add_trace(go.Scatter3d( - x=[x, x + bx], - y=[y, y + by], - z=[z, z + bz], - mode='lines', - line=dict(color='orange', width=2), - name='Boresight' - )) - # Suppress the legend for the boresight plot - fig.update_traces(showlegend=False, selector=dict(name='Boresight')) + # fig.add_trace( + # go.Scatter3d( + # x=[0], + # y=[0], + # z=[0], + # mode='markers', + # marker=dict(size=3, color='black', opacity=1), + # showlegend=False + # ) + # ) + + from sharc.support.geometry import plot_geom + plot_geom(fig, imt_ue.geom) + plot_geom(fig, imt_bs.geom, {"marker": dict(size=2, color='blue', opacity=1)}, True) # Maintain axis proportions fig.update_layout(scene_aspectmode='data') - ref_x = imt_ue.geom.x_global[11] - ref_y = imt_ue.geom.y_global[11] - ref_z = imt_ue.geom.z_global[11] - range_scale = 1000 + # ref_x = imt_ue.geom.x_global[11] + # ref_y = imt_ue.geom.y_global[11] + # ref_z = imt_ue.geom.z_global[11] + # range_scale = 1000 range_scale = 5000 ref_x = 0 diff --git a/sharc/support/geometry.py b/sharc/support/geometry.py index b7a11bcb2..45ff62970 100644 --- a/sharc/support/geometry.py +++ b/sharc/support/geometry.py @@ -2,6 +2,7 @@ # from sharc.satellite.utils.sat_utils import lla2ecef from sharc.satellite.ngso.constants import EARTH_RADIUS_M +from sharc.support.sharc_geom import cartesian_to_polar, polar_to_cartesian import scipy import numpy as np from abc import ABC @@ -306,7 +307,7 @@ def get_off_axis_angle(self, other: "GlobalGeometry") -> np.array: @readonly_properties( "x_local", "y_local", "z_local", "pointn_azim_local", "pointn_elev_local", - "global_lla_reference" + "global_lla_reference", "local_lla_references" ) class SimulatorGeometry(GlobalGeometry): """ @@ -322,7 +323,7 @@ class SimulatorGeometry(GlobalGeometry): pointn_azim_local: np.ndarray # (M,) pointn_elev_local: np.ndarray # (M,) - __local_lla_references: np.ndarray[np.ndarray[float]] # (3, M) + local_lla_references: np.ndarray[np.ndarray[float]] # (3, M) global_lla_reference: tuple[float, float, float] _num_of_local_refs: int # M @@ -355,7 +356,7 @@ def setup( """ super().setup(num_geometries) - self.__global_coord_sys = global_cs + self._global_lla_reference = global_cs self._num_of_local_refs = num_of_local_refs if num_of_local_refs == 0: @@ -365,6 +366,7 @@ def setup( self._z_local = self._z_global self._pointn_azim_local = self._pointn_azim_global self._pointn_elev_local = self._pointn_elev_global + self._local_lla_references = None return elif global_cs is None: raise ValueError( @@ -377,6 +379,7 @@ def setup( self._z_local = np.empty(num_geometries) self._pointn_azim_local = np.empty(num_geometries) self._pointn_elev_local = np.empty(num_geometries) + self._local_lla_references = np.empty((3, num_of_local_refs)) def set_local_coord_sys( self, @@ -394,7 +397,7 @@ def set_local_coord_sys( "Incongruent number of coordinate systems. " f"Passed {len(r)} but should have passed {len(self._num_of_local_refs)}" ) - self.__local_lla_references = np.stack((ref_lats, ref_lons, ref_alts)) + self._local_lla_references = np.stack((ref_lats, ref_lons, ref_alts)) self._compute_global_local_transform() @@ -456,19 +459,43 @@ def _compute_global_from_local(self): p_local = np.stack([self.x_local, self.y_local, self.z_local], axis=-1) - # Translation happens first (before rotation) - p_local = p_local + self.local2ecef_transl_mtx # (N,3) - - p_global = (self.local2global_rot_mtx @ p_local[..., None]).squeeze(-1) # (N,3) - - # translation happens after rotation - p_global = p_global + self.ecef2global_transl_mtx # (N,3) + p_global = self._vec_local2global(p_local) # Store results self._x_global = p_global[:, 0] self._y_global = p_global[:, 1] self._z_global = p_global[:, 2] + r = 1 + # then get pointing vec + point_local = np.stack(polar_to_cartesian( + r, self.pointn_azim_local, self.pointn_elev_local), axis=-1) + + point_global_x, point_global_y, point_global_z = self._vec_local2global( + point_local, False + ).T + + _, global_azimuth, global_elevation = cartesian_to_polar( + point_global_x, point_global_y, point_global_z) + + self._pointn_elev_global = global_elevation + self._pointn_azim_global = global_azimuth + + def _vec_local2global(self, p_local, translate=True): + """Receives a vector shaped as (N, 3) and applies local2global transform + """ + if translate: + # Translation happens first (before rotation) + p_local = p_local + self.local2ecef_transl_mtx # (N,3) + + p_global = (self.local2global_rot_mtx @ p_local[..., None]).squeeze(-1) # (N,3) + + if translate: + # translation happens after rotation + p_global = p_global + self.ecef2global_transl_mtx # (N,3) + + return p_global + def _compute_local_from_global(self): if not self.uses_local_coords: self._x_local = self._x_global @@ -480,22 +507,46 @@ def _compute_local_from_global(self): p_global = np.stack([self.x_global, self.y_global, self.z_global], axis=-1) - # Translation happens first (before rotation) - p_global = p_global + self.global2ecef_transl_mtx # (N,3) - - p_local = (self.global2local_rot_mtx @ p_global[..., None]).squeeze(-1) # (N,3) - - # translation happens after rotation - p_local = p_local + self.ecef2local_transl_mtx # (N,3) + p_local = self._vec_global2local(p_global) # Store results self._x_local = p_local[:, 0] self._y_local = p_local[:, 1] self._z_local = p_local[:, 2] + r = 1 + # then get pointing vec + point_global = np.stack(polar_to_cartesian( + r, self.pointn_azim_global, self.pointn_elev_global), axis=-1) + + point_local_x, point_local_y, point_local_z = self._vec_global2local( + point_global, False + ).T + + _, local_azimuth, local_elevation = cartesian_to_polar( + point_local_x, point_local_y, point_local_z) + + self._pointn_elev_local = local_elevation + self._pointn_azim_local = local_azimuth + + def _vec_global2local(self, p_global, translate=True): + """Receives a vector shaped as (N, 3) and applies global2local transform + """ + if translate: + # Translation happens first (before rotation) + p_global = p_global + self.global2ecef_transl_mtx # (N,3) + + p_local = (self.global2local_rot_mtx @ p_global[..., None]).squeeze(-1) # (N,3) + + if translate: + # translation happens after rotation + p_local = p_local + self.ecef2local_transl_mtx # (N,3) + + return p_local + def _compute_global_local_transform(self): # get ecef to local - local_lat, local_lon, local_alt = self.__local_lla_references + local_lat, local_lon, local_alt = self.local_lla_references rotation_around_z = -local_lon - 90 rotation_around_x = local_lat - 90 @@ -516,7 +567,7 @@ def _compute_global_local_transform(self): self.ecef2local_transl_mtx = -self.local2ecef_transl_mtx # global transforms - global_lat, global_lon, global_alt = self.__global_coord_sys + global_lat, global_lon, global_alt = self._global_lla_reference rotation_around_z = -global_lon - 90 rotation_around_x = global_lat - 90 @@ -540,7 +591,6 @@ def _compute_global_local_transform(self): self.global2local_rot_mtx = ecef2local_rot_mtx @ global2ecef_rot_mtx - def get_local_distance_to(self, other: "SimulatorGeometry") -> np.array: """Calculate the 2D distance between this manager's stations and another's considering this ones coordinate system @@ -579,7 +629,90 @@ def get_local_elevation(self, other: "SimulatorGeometry") -> np.array: raise NotImplementedError() + +def plot_geom( + fig: "go.Figure", + geom: SimulatorGeometry, + scatter_params: dict = {}, + plot_pointing=False, +): + """Adds a given SimulatorGeometry to a plotly figure + considering global coordinates + """ + import plotly.graph_objects as go + scatter_params = { + "mode": 'markers', + "marker": dict(size=1, color='red', opacity=1), + "showlegend": False, + **scatter_params + } + fig.add_trace( + go.Scatter3d( + x=geom.x_global, + y=geom.y_global, + z=geom.z_global, + **scatter_params + ) + ) + + if plot_pointing: + from sharc.support.sharc_geom import polar_to_cartesian + # Plot beam boresight vectors + boresight_length = 100 * 1e3 # Length of the boresight vectors for visualization + boresight_x, boresight_y, boresight_z = polar_to_cartesian( + boresight_length, + geom.pointn_azim_global, + geom.pointn_elev_global + ) + # Add arrow heads to the end of the boresight vectors + for x, y, z, bx, by, bz in zip(geom.x_global, + geom.y_global, + geom.z_global, + boresight_x, + boresight_y, + boresight_z): + fig.add_trace(go.Cone( + x=[x + bx], + y=[y + by], + z=[z + bz], + u=[bx], + v=[by], + w=[bz], + colorscale=[[0, 'orange'], [1, 'orange']], + sizemode='absolute', + sizeref=2 * boresight_length / 5, + showscale=False, + showlegend=False, + )) + for x, y, z, bx, by, bz in zip(geom.x_global, + geom.y_global, + geom.z_global, + boresight_x, + boresight_y, + boresight_z): + fig.add_trace(go.Scatter3d( + x=[x, x + bx], + y=[y, y + by], + z=[z, z + bz], + mode='lines', + line=dict(color='orange', width=2), + showlegend=False, + )) + if __name__ == "__main__": + global_lla = (-14, -45, 1200) + # tg = SimulatorGeometry( + # 1, + # 1, + # global_lla + # ) + # tg.set_local_coord_sys( + # np.array([-14]), + # np.array([-45]), + # np.array([1200]), + # ) + # print(tg.local_lla_references) + from sharc.topology.topology_macrocell import TopologyMacrocell rng = np.random.RandomState(seed=0xcaffe) topology = TopologyMacrocell( @@ -588,8 +721,6 @@ def get_local_elevation(self, other: "SimulatorGeometry") -> np.array: topology.calculate_coordinates(rng) - global_lla = (-14, -45, 1200) - num_ue = 3 bs_geom = SimulatorGeometry( @@ -647,30 +778,29 @@ def get_local_elevation(self, other: "SimulatorGeometry") -> np.array: import plotly.graph_objects as go - fig.add_trace(go.Scatter3d( - x=bs_geom.x_global, - y=bs_geom.y_global, - z=bs_geom.z_global, - mode='markers', - marker=dict( - size=3, - color='blue', - opacity=1.0 - ), - name='BS' - )) - fig.add_trace(go.Scatter3d( - x=ue_geom.x_global, - y=ue_geom.y_global, - z=ue_geom.z_global, - mode='markers', - marker=dict( - size=1, - color='red', - opacity=1.0 - ), - name='UE' - )) + plot_geom( + fig, + bs_geom, + dict( + marker=dict( + size=2, + color='blue', + opacity=1.0 + ), + name='BS' + ) + ) + plot_geom( + fig, + ue_geom, + dict( + marker=dict( + size=1, + color='red', + opacity=1.0 + ), + name='UE' + ) + ) fig.show() - diff --git a/sharc/topology/topology.py b/sharc/topology/topology.py index 2cc41751a..3afda8524 100644 --- a/sharc/topology/topology.py +++ b/sharc/topology/topology.py @@ -8,6 +8,7 @@ from abc import ABCMeta, abstractmethod import numpy as np import matplotlib.axes +from sharc.support.geometry import SimulatorGeometry class Topology(object): @@ -32,6 +33,7 @@ def __init__( self.azimuth = np.empty(0) self.indoor = np.empty(0) self.is_space_station = False + self.determines_local_geometry = False self.num_base_stations = -1 self.static_base_stations = False @@ -39,6 +41,14 @@ def __init__( def calculate_coordinates(self, random_number_gen=np.random.RandomState()): """Calculate the coordinates of the stations according to class attributes.""" + def get_ue_geometry(self, ue_k: int) -> SimulatorGeometry: + if not self.determines_local_geometry: + raise ValueError("cannot get local UE geom if topology doesn't determines_local_geometry") + + def get_bs_geometry(self) -> SimulatorGeometry: + if not self.determines_local_geometry: + raise ValueError("cannot get local BS geom if topology doesn't determines_local_geometry") + # by default, a sharc topology will translate the UE distribution by the # BS position def transform_ue_xyz( diff --git a/sharc/topology/topology_factory.py b/sharc/topology/topology_factory.py index bec0815b8..03da29d23 100644 --- a/sharc/topology/topology_factory.py +++ b/sharc/topology/topology_factory.py @@ -13,6 +13,7 @@ from sharc.topology.topology_indoor import TopologyIndoor from sharc.topology.topology_ntn import TopologyNTN from sharc.topology.topology_single_base_station import TopologySingleBaseStation +from sharc.topology.topology_spherical_sampling_from_grid import TopologySamplingFromSphericalGrid from sharc.parameters.parameters import Parameters from sharc.support.sharc_geom import CoordinateSystem @@ -57,6 +58,13 @@ def createTopology(parameters: Parameters, parameters.imt.topology.mss_dc, coordinate_system ) + elif parameters.imt.topology.type == "SAMPLING_FROM_SPHERICAL_GRID": + return TopologySamplingFromSphericalGrid( + parameters.imt.topology.sampling_from_spherical_grid.max_ue_distance, + parameters.imt.topology.sampling_from_spherical_grid.num_bs, + (coordinate_system.ref_lat, coordinate_system.ref_long, coordinate_system.ref_alt), + parameters.imt.topology.sampling_from_spherical_grid.grid, + ) else: sys.stderr.write( "ERROR\nInvalid topology: " + diff --git a/sharc/topology/topology_imt_mss_dc.py b/sharc/topology/topology_imt_mss_dc.py index b307ec6ec..b7fb3f4a0 100644 --- a/sharc/topology/topology_imt_mss_dc.py +++ b/sharc/topology/topology_imt_mss_dc.py @@ -47,6 +47,11 @@ def __init__(self, params: ParametersImtMssDc, CoordinateSystem object that converts the ECEF coordinate system to one centered at CoordinateSystem.reference. """ + super().__init__( + # FIXME: generated UEs won't follow radius exactly + params.beam_radius * np.sqrt(3), + params.beam_radius + ) # That means the we need to pass the groud reference points to the base # stations generator self.is_space_station = True @@ -59,10 +64,6 @@ def __init__(self, params: ParametersImtMssDc, self.space_station_y = None self.space_station_z = None - self.cell_radius = params.beam_radius - # TODO: check this: - self.intersite_distance = self.cell_radius * np.sqrt(3) - self.lat = None self.lon = None diff --git a/sharc/topology/topology_spherical_sampling_from_grid.py b/sharc/topology/topology_spherical_sampling_from_grid.py new file mode 100644 index 000000000..488136172 --- /dev/null +++ b/sharc/topology/topology_spherical_sampling_from_grid.py @@ -0,0 +1,184 @@ +from sharc.topology.topology import Topology +import numpy as np +from sharc.support.sharc_geom import CoordinateSystem +from sharc.support.geometry import SimulatorGeometry +from sharc.parameters.imt.parameters_grid import ParametersTerrestrialGrid +# from sharc.satellite.utils.sat_utils import lla2ecef +# import math +# import matplotlib.pyplot as plt +# import matplotlib.axes +# import geopandas as gpd +# from shapely.geometry import Polygon, MultiPolygon +# from pathlib import Path + + +class TopologySamplingFromSphericalGrid(Topology): + """ + Class to generate and manage terrestrial networks distributed on a + grid situated on a spherical Earth. + This IMT topology is not meant for IMT TN as victim studies, since distributed + IMT will not have intra-IMT interference from close BS. + """ + + def __init__( + self, + max_ue_distance: float, + num_base_stations: int, + global_sim_lla_reference: tuple[float, float, float], + grid: ParametersTerrestrialGrid, + ): + """ + Initializes a spherical topology with specific network settings. + + Parameters: + max_ue_distance: Radius of the coverage area for each site in meters. + num_base_stations: Number of base stations to sample + global_sim_lla_reference: (3,) tuple for lla of global coordinate system + grid: ParametersTerrestrialGrid for determining grid + """ + # intersite distance is needed for UE distribution, so we calculate it + intersite_dist = max_ue_distance * 3 / 2 + super().__init__(intersite_dist, max_ue_distance) + + self.determines_local_geometry = True + + self.num_base_stations = num_base_stations + self.is_space_station = False + + self.grid = grid + + # sistema de coord global + self.global_cs = global_sim_lla_reference + self.bs_geometry: SimulatorGeometry = None + + self.calculate_coordinates() + + def calculate_coordinates(self, random_number_gen=np.random.RandomState()): + """Compute and set the coordinates and angles for each base station. + + Parameters + ---------- + random_number_gen : np.random.RandomState, optional + Random number generator (not used in this implementation). + """ + self.grid.reset_grid( + "calculate_coords", + random_number_gen, + True + ) + + lla_grid_to_sample = self.grid.lon_lat_grid[::-1] + # print("self.grid.lon_lat_grid.shape", self.grid.lon_lat_grid.shape) + # print("self.grid.lon_lat_grid.shape", self.grid.lon_lat_grid.shape) + # print("self.num_base_stations", self.num_base_stations) + if lla_grid_to_sample.shape[0] == 2: + default_alts = np.zeros((1, lla_grid_to_sample.shape[1])) + lla_grid_to_sample = np.concatenate((lla_grid_to_sample, default_alts)) + + # print("np.arange(lla_grid_to_sample.shape[0]).shape", np.arange(lla_grid_to_sample.shape[0]).shape) + chosen_idxs = random_number_gen.choice( + np.arange(lla_grid_to_sample.shape[1]), + size=self.num_base_stations, + replace=False + ) + + # (3, N) + chosen_llas = lla_grid_to_sample.T[chosen_idxs].T + + geom = SimulatorGeometry( + self.num_base_stations, + self.num_base_stations, + self.global_cs, + ) + lat, lon, alt = chosen_llas + # coords locais para determinar + # transformação global <> local + geom.set_local_coord_sys( + lat, + lon, + alt, + ) + self.x = np.zeros(self.num_base_stations) + self.y = np.zeros(self.num_base_stations) + self.z = np.zeros(self.num_base_stations) + self.azimuth = random_number_gen.uniform(-180., 180., size=self.num_base_stations) + geom.set_local_coords( + self.x, + self.y, + self.z, + self.azimuth, + ) + + # local x,y,z + # self.x = None + # self.y = None + # self.z = None + self.bs_geometry = geom + + def get_bs_geometry(self) -> SimulatorGeometry: + return self.bs_geometry + + def get_ue_geometry(self, ue_k: int) -> SimulatorGeometry: + ue_geom = SimulatorGeometry( + self.num_base_stations * ue_k, + self.num_base_stations * ue_k, + self.global_cs, + ) + ue_geom.set_local_coord_sys( + np.repeat(self.bs_geometry._local_lla_references[0], ue_k), + np.repeat(self.bs_geometry._local_lla_references[1], ue_k), + np.repeat(self.bs_geometry._local_lla_references[2], ue_k), + ) + return ue_geom + + def transform_ue_xyz(self, bs, x, y, z): + return x, y, z + + +# Example usage +if __name__ == '__main__': + global_lla = (-14, -45, 1200) + topology = TopologySamplingFromSphericalGrid( + 0, 3, + global_lla, + np.array([ + # [-1, -47, 400], + # [-3, -47, 400], + # [-5, -47, 400], + # [-7, -47, 400], + # [-9, -47, 400], + [11, -47, 400], + [22, -47, 400], + [-14, -45, 1200] + ]).T, + ) + + topology.calculate_coordinates() + + from sharc.satellite.scripts.plot_globe import plot_globe_with_borders + from sharc.support.sharc_geom import CoordinateSystem + + global_cs = CoordinateSystem() + global_cs.set_reference( + *global_lla + ) + fig = plot_globe_with_borders( + False, global_cs, False + ) + + import plotly.graph_objects as go + + fig.add_trace(go.Scatter3d( + x=topology.bs_geometry.x_global, + y=topology.bs_geometry.y_global, + z=topology.bs_geometry.z_global, + mode='markers', + marker=dict( + size=2, + color='blue', + opacity=1.0 + ), + name='Reference' + )) + + fig.show() diff --git a/tests/parameters/parameters_for_testing.yaml b/tests/parameters/parameters_for_testing.yaml index 484cd9388..1cd0d9fce 100644 --- a/tests/parameters/parameters_for_testing.yaml +++ b/tests/parameters/parameters_for_testing.yaml @@ -313,6 +313,30 @@ imt: # Defines the antenna model to be used in compatibility studies between # IMT and other services in adjacent band # Possible values: SINGLE_ELEMENT, BEAMFORMING + sampling_from_spherical_grid: + num_bs: 324 + max_ue_distance: 132 + grid: + cell_radius: 111 + grid_in_zone: + type: CIRCLE # may be "CIRCLE" or "FROM_COUNTRIES" + circle: + center_lat: 1 + center_lon: 2 + radius_km: 3 + from_countries: + country_shapes_filename: sharc/data/countries/ne_110m_admin_0_countries.shp + # By default, these are the same as taken from `sat_is_active_if.lat_long_inside_country` + country_names: + - Brazil + - Chile + + # This margin defines where the grid is constructed + # margin from inside of border [km] + # By default, this is made to be the same as beam_radius + # if positive, makes border smaller by x km + # if negative, makes border bigger by x km + margin_from_border: 0.11 adjacent_antenna_model : BEAMFORMING # Base station parameters bs: diff --git a/tests/parameters/test_parameters.py b/tests/parameters/test_parameters.py index 8de556032..e174b53db 100644 --- a/tests/parameters/test_parameters.py +++ b/tests/parameters/test_parameters.py @@ -391,6 +391,39 @@ def test_parameters_imt(self): self.assertEqual( getattr(orbit_params, k), expected_orbit_params[i][k]) + """ + Test parameters spherical topology + """ + sampling_from_spherical_grid = self.parameters.imt.topology.sampling_from_spherical_grid + self.assertEqual( + sampling_from_spherical_grid.num_bs, + 324, + ) + self.assertEqual( + sampling_from_spherical_grid.grid.cell_radius, + 111, + ) + self.assertEqual( + sampling_from_spherical_grid.grid.grid_in_zone.type, + "CIRCLE", + ) + self.assertEqual( + sampling_from_spherical_grid.grid.grid_in_zone.circle.center_lat, + 1, + ) + self.assertEqual( + sampling_from_spherical_grid.grid.grid_in_zone.circle.center_lon, + 2, + ) + self.assertEqual( + sampling_from_spherical_grid.grid.grid_in_zone.circle.radius_km, + 3, + ) + + self.assertEqual( + sampling_from_spherical_grid.max_ue_distance, + 132, + ) def test_imt_validation(self): """ From 437e9833738b69d4d70082853cdcc9a774071f4e Mon Sep 17 00:00:00 2001 From: artistrea Date: Wed, 8 Oct 2025 14:43:58 -0300 Subject: [PATCH 25/77] refactor: cleaner SimulatorGeometry & fixed linting errors --- sharc/support/geometry.py | 59 +++++++++---------- sharc/topology/topology.py | 4 ++ .../topology_spherical_sampling_from_grid.py | 5 ++ 3 files changed, 38 insertions(+), 30 deletions(-) diff --git a/sharc/support/geometry.py b/sharc/support/geometry.py index 45ff62970..d55a4ec25 100644 --- a/sharc/support/geometry.py +++ b/sharc/support/geometry.py @@ -24,7 +24,6 @@ def getter(self, name=private_name): return decorator -# TODO: make these properties readonly @readonly_properties( "x_global", "y_global", "z_global", "pointn_azim_global", "pointn_elev_global", @@ -146,8 +145,10 @@ def get_global_dist_angles_wrap_around(self, other) -> np.array: phi (np.array): azimuth of pointing vector to other stations theta (np.array): elevation of pointing vector to other stations """ - if self._num_of_local_refs != 0 or other._num_of_local_refs != 0: - raise ValueError("Wrap around was not implemented for local coord sys") + if self.uses_local_coords or other.uses_local_coords: + raise ValueError( + "Wrap around was not implemented for local coord sys" + ) # Initialize variables distance_3D = np.empty([self.num_geometries, other.num_geometries]) distance_2D = np.inf * np.ones_like(distance_3D) @@ -315,24 +316,21 @@ class SimulatorGeometry(GlobalGeometry): Just global and local conversion. """ # N = num of geometries - # M = num of local references - # M < N - x_local: np.ndarray # (M,) - y_local: np.ndarray # (M,) - z_local: np.ndarray # (M,) - pointn_azim_local: np.ndarray # (M,) - pointn_elev_local: np.ndarray # (M,) - - local_lla_references: np.ndarray[np.ndarray[float]] # (3, M) + x_local: np.ndarray # (N,) + y_local: np.ndarray # (N,) + z_local: np.ndarray # (N,) + pointn_azim_local: np.ndarray # (N,) + pointn_elev_local: np.ndarray # (N,) + + local_lla_references: np.ndarray[np.ndarray[float]] # (3, N) global_lla_reference: tuple[float, float, float] - _num_of_local_refs: int # M - _geometry_reference_i: np.ndarray[int] # (N,) + uses_local_coords: bool def __init__( self, num_geometries, - num_of_local_refs=0, + uses_local_coords=False, global_cs: tuple[float, float, float] = None, ): """ @@ -341,14 +339,14 @@ def __init__( """ self.setup( num_geometries, - num_of_local_refs, + uses_local_coords, global_cs, ) def setup( self, num_geometries, - num_of_local_refs=0, + uses_local_coords=False, global_cs: tuple[float, float, float] = None, ): """ @@ -357,29 +355,27 @@ def setup( super().setup(num_geometries) self._global_lla_reference = global_cs - self._num_of_local_refs = num_of_local_refs + self.uses_local_coords = True - if num_of_local_refs == 0: + if not uses_local_coords: self.uses_local_coords = False self._x_local = self._x_global self._y_local = self._y_global self._z_local = self._z_global self._pointn_azim_local = self._pointn_azim_global self._pointn_elev_local = self._pointn_elev_global - self._local_lla_references = None return elif global_cs is None: raise ValueError( "If there will be a local ref, global coord sys must be passed" ) - self.uses_local_coords = True self._x_local = np.empty(num_geometries) self._y_local = np.empty(num_geometries) self._z_local = np.empty(num_geometries) self._pointn_azim_local = np.empty(num_geometries) self._pointn_elev_local = np.empty(num_geometries) - self._local_lla_references = np.empty((3, num_of_local_refs)) + self._local_lla_references = np.empty((3, num_geometries)) def set_local_coord_sys( self, @@ -392,10 +388,10 @@ def set_local_coord_sys( global<->local coordinate transformation """ for r in [ref_lats, ref_lons, ref_alts]: - if len(r) != self._num_of_local_refs: + if len(r) != self.num_geometries: raise ValueError( "Incongruent number of coordinate systems. " - f"Passed {len(r)} but should have passed {len(self._num_of_local_refs)}" + f"Passed {len(r)} but should have passed {len(self.num_geometries)}" ) self._local_lla_references = np.stack((ref_lats, ref_lons, ref_alts)) @@ -485,13 +481,14 @@ def _vec_local2global(self, p_local, translate=True): """Receives a vector shaped as (N, 3) and applies local2global transform """ if translate: - # Translation happens first (before rotation) + # remove ecef2local translation p_local = p_local + self.local2ecef_transl_mtx # (N,3) + # rotate local2ecef2global p_global = (self.local2global_rot_mtx @ p_local[..., None]).squeeze(-1) # (N,3) if translate: - # translation happens after rotation + # add ecef2global translation p_global = p_global + self.ecef2global_transl_mtx # (N,3) return p_global @@ -533,13 +530,14 @@ def _vec_global2local(self, p_global, translate=True): """Receives a vector shaped as (N, 3) and applies global2local transform """ if translate: - # Translation happens first (before rotation) + # remove ecef2global translation p_global = p_global + self.global2ecef_transl_mtx # (N,3) + # rotate global2ecef2local p_local = (self.global2local_rot_mtx @ p_global[..., None]).squeeze(-1) # (N,3) if translate: - # translation happens after rotation + # add ecef2local translation p_local = p_local + self.ecef2local_transl_mtx # (N,3) return p_local @@ -557,7 +555,7 @@ def _compute_global_local_transform(self): ) # first translation, after rotation - self.local2ecef_transl_mtx = np.zeros((self._num_of_local_refs, 3)) + self.local2ecef_transl_mtx = np.zeros((self.num_geometries, 3)) # NOTE: works because of spherical Earth self.local2ecef_transl_mtx[:, 2] = local_alt + EARTH_RADIUS_M local2ecef_rot_mtx = ecef2local_rot.inv().as_matrix() @@ -699,6 +697,7 @@ def plot_geom( showlegend=False, )) + if __name__ == "__main__": global_lla = (-14, -45, 1200) # tg = SimulatorGeometry( @@ -756,7 +755,7 @@ def plot_geom( num_ue * topology.num_base_stations, topology, rng, - min_dist_to_bs=35., deterministic_cell=True + deterministic_cell=True ) ue_geom.set_local_coords( np.array(ue_x), diff --git a/sharc/topology/topology.py b/sharc/topology/topology.py index 3afda8524..813bb7f9d 100644 --- a/sharc/topology/topology.py +++ b/sharc/topology/topology.py @@ -42,10 +42,14 @@ def calculate_coordinates(self, random_number_gen=np.random.RandomState()): """Calculate the coordinates of the stations according to class attributes.""" def get_ue_geometry(self, ue_k: int) -> SimulatorGeometry: + """Returns UE pre-built SimulatorGeometry if implemented + """ if not self.determines_local_geometry: raise ValueError("cannot get local UE geom if topology doesn't determines_local_geometry") def get_bs_geometry(self) -> SimulatorGeometry: + """Returns BS pre-built SimulatorGeometry if implemented + """ if not self.determines_local_geometry: raise ValueError("cannot get local BS geom if topology doesn't determines_local_geometry") diff --git a/sharc/topology/topology_spherical_sampling_from_grid.py b/sharc/topology/topology_spherical_sampling_from_grid.py index 488136172..c96f4854e 100644 --- a/sharc/topology/topology_spherical_sampling_from_grid.py +++ b/sharc/topology/topology_spherical_sampling_from_grid.py @@ -116,9 +116,13 @@ def calculate_coordinates(self, random_number_gen=np.random.RandomState()): self.bs_geometry = geom def get_bs_geometry(self) -> SimulatorGeometry: + """Returns BS pre-built SimulatorGeometry if implemented + """ return self.bs_geometry def get_ue_geometry(self, ue_k: int) -> SimulatorGeometry: + """Returns UE pre-built SimulatorGeometry if implemented + """ ue_geom = SimulatorGeometry( self.num_base_stations * ue_k, self.num_base_stations * ue_k, @@ -132,6 +136,7 @@ def get_ue_geometry(self, ue_k: int) -> SimulatorGeometry: return ue_geom def transform_ue_xyz(self, bs, x, y, z): + """Do not make any changes to ue position, let SimulatorGeometry take care of it""" return x, y, z From 8802e96e42d1de6ac3e77de9823dff9406006ebc Mon Sep 17 00:00:00 2001 From: artistrea Date: Wed, 8 Oct 2025 14:46:51 -0300 Subject: [PATCH 26/77] feat: missing parameters_spherical_sampling_from_grid --- ...parameters_spherical_sampling_from_grid.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 sharc/parameters/imt/parameters_spherical_sampling_from_grid.py diff --git a/sharc/parameters/imt/parameters_spherical_sampling_from_grid.py b/sharc/parameters/imt/parameters_spherical_sampling_from_grid.py new file mode 100644 index 000000000..d1808b513 --- /dev/null +++ b/sharc/parameters/imt/parameters_spherical_sampling_from_grid.py @@ -0,0 +1,40 @@ +from dataclasses import dataclass, field + +from sharc.parameters.parameters_base import ParametersBase +from sharc.parameters.imt.parameters_grid import ParametersTerrestrialGrid + + +@dataclass +class ParametersSamplingFromSphericalGrid(ParametersBase): + """ + Data class for spherical sampling from grid topology parameters. + """ + num_bs: int = None + + # It is necessary to decouple coverage radius and grid cell radius + # so that grid point calculation and ue positioning are decoupled + # This parameter determines max ue position + max_ue_distance: float = None + + grid: ParametersTerrestrialGrid = field(default_factory=ParametersTerrestrialGrid) + + def validate(self, ctx): + """ + Validate the topology parameters. + + Ensures that all attributes are set to valid values and types. + + Parameters + ---------- + ctx : str + Context string for error messages. + """ + if not isinstance( + self.num_bs, + int) or self.num_bs < 0: + raise ValueError(f"{ctx}.num_bs must be non-negative") + + if self.max_ue_distance <= 0.0: + raise ValueError(f"{ctx}.max_ue_distance must be positive") + + super().validate(ctx) From 56cf68fada4f5e7a1f1556f7c2883b18fb499646 Mon Sep 17 00:00:00 2001 From: Bruno Faria Date: Fri, 10 Oct 2025 09:11:14 -0300 Subject: [PATCH 27/77] update(propagation): Changed P.2108 parameter type to clutter_scenario. The type parameter expects the StationType to check if the clutter scenario is terrestrial or spacial. This is prone to errors if the study is between IMT and another station that is not FSS_ES or FSS_SS. Now the user specifies if it's "terrestrial" path (P.2108 sec 3.2) or "spacial" path (P.2108 sec 3.3). --- .../propagation/propagation_clear_air_452.py | 2 +- sharc/propagation/propagation_clutter_loss.py | 21 ++++++++++++------- sharc/propagation/propagation_p619.py | 2 +- tests/test_propagation_clutter.py | 6 +++--- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/sharc/propagation/propagation_clear_air_452.py b/sharc/propagation/propagation_clear_air_452.py index 26095ca24..1a8dfaffa 100644 --- a/sharc/propagation/propagation_clear_air_452.py +++ b/sharc/propagation/propagation_clear_air_452.py @@ -1824,7 +1824,7 @@ def get_loss( clutter_loss = self.clutter.get_loss( frequency=frequency * 1000, distance=distance * 1000, - station_type=StationType.FSS_ES, + clutter_scenario="terrestrial", # Always terrestrial for P.452 clutter_type=self.model_params.clutter_type ) else: diff --git a/sharc/propagation/propagation_clutter_loss.py b/sharc/propagation/propagation_clutter_loss.py index 43c9b4631..6a839e330 100644 --- a/sharc/propagation/propagation_clutter_loss.py +++ b/sharc/propagation/propagation_clutter_loss.py @@ -61,19 +61,24 @@ def get_loss(self, *args, **kwargs) -> np.array: elevation (np.array) : elevation angles [deg] loc_percentage (np.array) : Percentage locations range [0, 1[ "RANDOM" for random percentage (Default = RANDOM) - station_type (StationType) : if type is IMT or FSS_ES, assume terrestrial - terminal within the clutter (ref § 3.2); otherwise, assume that - one terminal is within the clutter and the other is a satellite, - aeroplane or other platform above the surface of the Earth. + clutter_scenario (str) : clutter scenario type: + "spacial" for Earth-space and aeronautical paths + "terrestrial" for terrestrial paths Returns ------- array with clutter loss values with dimensions of distance """ + # Check for required parameters + required_params = ["frequency", "clutter_scenario", "distance"] + missing_params = [p for p in required_params if p not in kwargs] + if missing_params: + raise ValueError(f"Missing required parameter(s): {', '.join(missing_params)}") + f = kwargs["frequency"] loc_per = kwargs.pop("loc_percentage", "RANDOM") - type = kwargs["station_type"] + clutter_scenario = kwargs["clutter_scenario"] d = kwargs["distance"] if f.size == 1: @@ -86,7 +91,7 @@ def get_loss(self, *args, **kwargs) -> np.array: p1 = loc_per * np.ones(d.shape) p2 = loc_per * np.ones(d.shape) - if type is StationType.IMT_BS or type is StationType.IMT_UE or type is StationType.FSS_ES: + if clutter_scenario == "terrestrial": clutter_type = kwargs["clutter_type"] if clutter_type == 'one_end': loss = self.get_terrestrial_clutter_loss(f, d, p1, True) @@ -94,7 +99,7 @@ def get_loss(self, *args, **kwargs) -> np.array: loss = self.get_terrestrial_clutter_loss(f, d, p1, True) + self.get_terrestrial_clutter_loss(f, d, p2, False) else: raise ValueError("Invalid type of Clutter-type. It can be either 'one_end' or 'both-ends'") - else: + elif clutter_scenario == "spacial": theta = kwargs["elevation"] earth_station_height = kwargs["earth_station_height"] mean_clutter_height = kwargs["mean_clutter_height"] @@ -105,6 +110,8 @@ def get_loss(self, *args, **kwargs) -> np.array: indices = self.random_number_gen.choice(mult_1.size, size=num_ones, replace=False) mult_1.flat[indices] = 1 loss *= mult_1 + else: + raise ValueError("Invalid type of Clutter-scenario. It can be either 'terrestrial' or 'spacial'") return loss def get_spacial_clutter_loss( diff --git a/sharc/propagation/propagation_p619.py b/sharc/propagation/propagation_p619.py index 79fabaa9d..4f3a69527 100644 --- a/sharc/propagation/propagation_p619.py +++ b/sharc/propagation/propagation_p619.py @@ -486,7 +486,7 @@ def get_loss( frequency=frequency, distance=distance, elevation=elevation["free_space"], - station_type=StationType.FSS_SS, + clutter_scenario="spacial", earth_station_height=earth_station_height, mean_clutter_height=self.mean_clutter_height, below_rooftop=self.below_rooftop, diff --git a/tests/test_propagation_clutter.py b/tests/test_propagation_clutter.py index 8ddf7cf71..a0d181c5b 100644 --- a/tests/test_propagation_clutter.py +++ b/tests/test_propagation_clutter.py @@ -25,7 +25,7 @@ def test_spatial_clutter_loss(self): frequency=frequency, elevation=elevation, loc_percentage=loc_percentage, - station_type=StationType.FSS_SS, + clutter_scenario="spacial", earth_station_height=earth_station_height, mean_clutter_height=mean_clutter_height, below_rooftop=below_rooftop, @@ -48,7 +48,7 @@ def test_terrestrial_clutter_loss(self): frequency=frequency, distance=distance, loc_percentage=loc_percentage, - station_type=StationType.IMT_BS, + clutter_scenario="terrestrial", clutter_type=clutter_type ) @@ -65,7 +65,7 @@ def test_random_loc_percentage(self): frequency=frequency, distance=distance, loc_percentage="RANDOM", - station_type=StationType.IMT_UE, + clutter_scenario="terrestrial", clutter_type=clutter_type ) From a5e33414474869fba7d849625a0b190d4753370b Mon Sep 17 00:00:00 2001 From: Bruno Faria Date: Fri, 10 Oct 2025 10:45:34 -0300 Subject: [PATCH 28/77] fix: Fixing mispeling the word spacial to spatial. --- .../Systems/Earth Exploration Satellite Service.md | 2 +- sharc/propagation/propagation_clutter_loss.py | 12 ++++++------ sharc/propagation/propagation_p619.py | 2 +- tests/test_propagation_clutter.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/docusaurus/docs/Systems/Earth Exploration Satellite Service.md b/docs/docusaurus/docs/Systems/Earth Exploration Satellite Service.md index dd8ffdadf..cf799fa05 100644 --- a/docs/docusaurus/docs/Systems/Earth Exploration Satellite Service.md +++ b/docs/docusaurus/docs/Systems/Earth Exploration Satellite Service.md @@ -11,7 +11,7 @@ sidebar_position: 1 The **Earth Exploration Satellite Service** is a radiocommunication service that employs satellites to gather environmetal data, facilitating analysis and monitoring of the Earth's surface and atmosphere. The systme is essential in land-use studies, oceanography, -disaster and environmental management. The implemented spacial system is indispensable for research +disaster and environmental management. The implemented spatial system is indispensable for research and management actities in atmospherical context. ### Overview diff --git a/sharc/propagation/propagation_clutter_loss.py b/sharc/propagation/propagation_clutter_loss.py index 6a839e330..038fd4888 100644 --- a/sharc/propagation/propagation_clutter_loss.py +++ b/sharc/propagation/propagation_clutter_loss.py @@ -62,7 +62,7 @@ def get_loss(self, *args, **kwargs) -> np.array: loc_percentage (np.array) : Percentage locations range [0, 1[ "RANDOM" for random percentage (Default = RANDOM) clutter_scenario (str) : clutter scenario type: - "spacial" for Earth-space and aeronautical paths + "spatial" for Earth-space and aeronautical paths "terrestrial" for terrestrial paths Returns @@ -99,22 +99,22 @@ def get_loss(self, *args, **kwargs) -> np.array: loss = self.get_terrestrial_clutter_loss(f, d, p1, True) + self.get_terrestrial_clutter_loss(f, d, p2, False) else: raise ValueError("Invalid type of Clutter-type. It can be either 'one_end' or 'both-ends'") - elif clutter_scenario == "spacial": + elif clutter_scenario == "spatial": theta = kwargs["elevation"] earth_station_height = kwargs["earth_station_height"] mean_clutter_height = kwargs["mean_clutter_height"] below_rooftop = kwargs["below_rooftop"] - loss = self.get_spacial_clutter_loss(f, theta, p1, earth_station_height, mean_clutter_height) + loss = self.get_spatial_clutter_loss(f, theta, p1, earth_station_height, mean_clutter_height) mult_1 = np.zeros(d.shape) num_ones = int(np.round(mult_1.size * below_rooftop / 100)) indices = self.random_number_gen.choice(mult_1.size, size=num_ones, replace=False) mult_1.flat[indices] = 1 loss *= mult_1 else: - raise ValueError("Invalid type of Clutter-scenario. It can be either 'terrestrial' or 'spacial'") + raise ValueError("Invalid type of Clutter-scenario. It can be either 'terrestrial' or 'spatial'") return loss - def get_spacial_clutter_loss( + def get_spatial_clutter_loss( self, frequency: float, elevation_angle: float, loc_percentage, @@ -330,7 +330,7 @@ def get_terrestrial_clutter_loss( clutter_loss = np.empty([len(elevation_angle), len(loc_percentage)]) for i in range(len(elevation_angle)): - clutter_loss[i, :] = cl.get_spacial_clutter_loss( + clutter_loss[i, :] = cl.get_spatial_clutter_loss( frequency, elevation_angle[i], loc_percentage, diff --git a/sharc/propagation/propagation_p619.py b/sharc/propagation/propagation_p619.py index 4f3a69527..19364d1e1 100644 --- a/sharc/propagation/propagation_p619.py +++ b/sharc/propagation/propagation_p619.py @@ -486,7 +486,7 @@ def get_loss( frequency=frequency, distance=distance, elevation=elevation["free_space"], - clutter_scenario="spacial", + clutter_scenario="spatial", earth_station_height=earth_station_height, mean_clutter_height=self.mean_clutter_height, below_rooftop=self.below_rooftop, diff --git a/tests/test_propagation_clutter.py b/tests/test_propagation_clutter.py index a0d181c5b..5aec00f87 100644 --- a/tests/test_propagation_clutter.py +++ b/tests/test_propagation_clutter.py @@ -25,7 +25,7 @@ def test_spatial_clutter_loss(self): frequency=frequency, elevation=elevation, loc_percentage=loc_percentage, - clutter_scenario="spacial", + clutter_scenario="spatial", earth_station_height=earth_station_height, mean_clutter_height=mean_clutter_height, below_rooftop=below_rooftop, From 04a2d19bc1cb1a223b5cd60850256ba6928ec4a8 Mon Sep 17 00:00:00 2001 From: artistrea Date: Mon, 13 Oct 2025 14:33:37 -0300 Subject: [PATCH 29/77] feat: simulator geometry tests & local coord calculations --- sharc/support/geometry.py | 134 ++++-- ...converter.py => test_coordinate_system.py} | 2 +- tests/test_geometry.py | 395 ++++++++++++++++++ 3 files changed, 497 insertions(+), 34 deletions(-) rename tests/{test_geometry_converter.py => test_coordinate_system.py} (99%) create mode 100644 tests/test_geometry.py diff --git a/sharc/support/geometry.py b/sharc/support/geometry.py index d55a4ec25..a710ecc22 100644 --- a/sharc/support/geometry.py +++ b/sharc/support/geometry.py @@ -146,7 +146,7 @@ def get_global_dist_angles_wrap_around(self, other) -> np.array: theta (np.array): elevation of pointing vector to other stations """ if self.uses_local_coords or other.uses_local_coords: - raise ValueError( + raise NotImplementedError( "Wrap around was not implemented for local coord sys" ) # Initialize variables @@ -391,7 +391,7 @@ def set_local_coord_sys( if len(r) != self.num_geometries: raise ValueError( "Incongruent number of coordinate systems. " - f"Passed {len(r)} but should have passed {len(self.num_geometries)}" + f"Passed {len(r)} but should have passed {self.num_geometries}" ) self._local_lla_references = np.stack((ref_lats, ref_lons, ref_alts)) @@ -468,7 +468,7 @@ def _compute_global_from_local(self): r, self.pointn_azim_local, self.pointn_elev_local), axis=-1) point_global_x, point_global_y, point_global_z = self._vec_local2global( - point_local, False + point_local, translate=False ).T _, global_azimuth, global_elevation = cartesian_to_polar( @@ -477,19 +477,40 @@ def _compute_global_from_local(self): self._pointn_elev_global = global_elevation self._pointn_azim_global = global_azimuth - def _vec_local2global(self, p_local, translate=True): - """Receives a vector shaped as (N, 3) and applies local2global transform + def _vec_local2global( + self, + p_local, + *, + translate=True, + permutate=False, + ): + """Receives a vector shaped as (N, 3) and applies local2global transform, + returning (N, 3). + If permutate == True, then may receive a vector (M, 3), + returning (N, M, 3) """ - if translate: - # remove ecef2local translation - p_local = p_local + self.local2ecef_transl_mtx # (N,3) + N = self.num_geometries - # rotate local2ecef2global - p_global = (self.local2global_rot_mtx @ p_local[..., None]).squeeze(-1) # (N,3) + local2ecef_t = self._local2ecef_transl_mtx if translate else 0 + ecef2global_t = self._ecef2global_transl_mtx if translate else 0 - if translate: + if not permutate: + assert p_local.shape == (N, 3) + # remove ecef2local translation + p_local = p_local + local2ecef_t # (N,3) + # rotate local2ecef2global + p_global = (self._local2global_rot_mtx @ p_local[..., None]).squeeze(-1) # (N,3) # add ecef2global translation - p_global = p_global + self.ecef2global_transl_mtx # (N,3) + p_global = p_global + ecef2global_t # (N,3) + else: + local2ecef_t_cast = local2ecef_t + ecef2global_t_cast = ecef2global_t + if translate: + local2ecef_t_cast = local2ecef_t[:, None, :] + ecef2global_t_cast = ecef2global_t[:, None, :] + p_local_exp = p_local[None, :, :] + local2ecef_t_cast # (N,M,3) + p_global = (self._global2local_rot_mtx[:, None, :, :] @ p_local_exp[..., None]).squeeze(-1) + p_global += ecef2global_t_cast return p_global @@ -517,7 +538,7 @@ def _compute_local_from_global(self): r, self.pointn_azim_global, self.pointn_elev_global), axis=-1) point_local_x, point_local_y, point_local_z = self._vec_global2local( - point_global, False + point_global, translate=False ).T _, local_azimuth, local_elevation = cartesian_to_polar( @@ -526,19 +547,43 @@ def _compute_local_from_global(self): self._pointn_elev_local = local_elevation self._pointn_azim_local = local_azimuth - def _vec_global2local(self, p_global, translate=True): - """Receives a vector shaped as (N, 3) and applies global2local transform + def _vec_global2local( + self, + p_global, + *, + translate=True, + permutate=False + ): + """Receives a vector shaped as (N, 3) and applies global2local transform, + returning (N, 3). + If permutate == True, then may receive a vector (M, 3), + returning (N, M, 3) """ - if translate: + N = self.num_geometries + + global2ecef_t = self._global2ecef_transl_mtx if translate else 0 + ecef2local_t = self._ecef2local_transl_mtx if translate else 0 + + if not permutate: + assert p_global.shape == (N, 3) + # remove ecef2global translation - p_global = p_global + self.global2ecef_transl_mtx # (N,3) + p_global = p_global + global2ecef_t # (N,3) - # rotate global2ecef2local - p_local = (self.global2local_rot_mtx @ p_global[..., None]).squeeze(-1) # (N,3) + # rotate global2ecef2local + p_local = (self._global2local_rot_mtx @ p_global[..., None]).squeeze(-1) # (N,3) - if translate: # add ecef2local translation - p_local = p_local + self.ecef2local_transl_mtx # (N,3) + p_local += ecef2local_t # (N,3) + else: + global2ecef_t_cast = global2ecef_t + ecef2local_t_cast = ecef2local_t + if translate: + global2ecef_t_cast = global2ecef_t[:, None, :] + ecef2local_t_cast = ecef2local_t[:, None, :] + p_global_exp = p_global[None, :, :] + global2ecef_t_cast # (N,M,3) + p_local = (self._global2local_rot_mtx[:, None, :, :] @ p_global_exp[..., None]).squeeze(-1) + p_local += ecef2local_t_cast return p_local @@ -555,14 +600,14 @@ def _compute_global_local_transform(self): ) # first translation, after rotation - self.local2ecef_transl_mtx = np.zeros((self.num_geometries, 3)) + self._local2ecef_transl_mtx = np.zeros((self.num_geometries, 3)) # NOTE: works because of spherical Earth - self.local2ecef_transl_mtx[:, 2] = local_alt + EARTH_RADIUS_M + self._local2ecef_transl_mtx[:, 2] = local_alt + EARTH_RADIUS_M local2ecef_rot_mtx = ecef2local_rot.inv().as_matrix() # first rotation, after translation ecef2local_rot_mtx = ecef2local_rot.as_matrix() - self.ecef2local_transl_mtx = -self.local2ecef_transl_mtx + self._ecef2local_transl_mtx = -self._local2ecef_transl_mtx # global transforms global_lat, global_lon, global_alt = self._global_lla_reference @@ -576,20 +621,25 @@ def _compute_global_local_transform(self): ) # first translation, after rotation - self.global2ecef_transl_mtx = np.zeros((1, 3)) + self._global2ecef_transl_mtx = np.zeros((1, 3)) # NOTE: works because of spherical Earth - self.global2ecef_transl_mtx[:, 2] = global_alt + EARTH_RADIUS_M + self._global2ecef_transl_mtx[:, 2] = global_alt + EARTH_RADIUS_M global2ecef_rot_mtx = ecef2global_rot.inv().as_matrix() # first rotation, after translation ecef2global_rot_mtx = ecef2global_rot.as_matrix() - self.ecef2global_transl_mtx = -self.global2ecef_transl_mtx + self._ecef2global_transl_mtx = -self._global2ecef_transl_mtx - self.local2global_rot_mtx = ecef2global_rot_mtx @ local2ecef_rot_mtx + self._local2global_rot_mtx = ecef2global_rot_mtx @ local2ecef_rot_mtx - self.global2local_rot_mtx = ecef2local_rot_mtx @ global2ecef_rot_mtx + self._global2local_rot_mtx = ecef2local_rot_mtx @ global2ecef_rot_mtx - def get_local_distance_to(self, other: "SimulatorGeometry") -> np.array: + def get_local_distance_to( + self, + other: "SimulatorGeometry", + *, + return_z_dist=False + ) -> np.array: """Calculate the 2D distance between this manager's stations and another's considering this ones coordinate system @@ -605,8 +655,23 @@ def get_local_distance_to(self, other: "SimulatorGeometry") -> np.array: """ if not self.uses_local_coords: return self.get_global_distance_to(other) - # TODO: 2d distance calculation - raise NotImplementedError() + + p_global = np.stack([other.x_global, other.y_global, other.z_global], axis=-1) + other_local = self._vec_global2local( + p_global, + permutate=True + ) + own_local = np.stack([self.x_local, self.y_local, np.zeros_like(self.z_local)], axis=-1) + + if return_z_dist: + z_dist = other_local[..., 2] - self.z_local[:, None] + other_local[..., 2] = 0. + + dist2d = np.sqrt(np.sum(np.power(other_local - own_local[:, None, :], 2), axis=-1)) + + if return_z_dist: + return dist2d, z_dist + return dist2d def get_local_elevation(self, other: "SimulatorGeometry") -> np.array: """Calculate the elevation angle between this manager's stations and another's @@ -625,7 +690,10 @@ def get_local_elevation(self, other: "SimulatorGeometry") -> np.array: if not self.uses_local_coords: return self.get_global_elevation(other) - raise NotImplementedError() + lat, lon, alt = self.local_lla_references + dist2d, z_dist = self.get_local_distance_to(other, return_z_dist=True) + + return np.degrees(np.arctan2(z_dist, dist2d)) def plot_geom( diff --git a/tests/test_geometry_converter.py b/tests/test_coordinate_system.py similarity index 99% rename from tests/test_geometry_converter.py rename to tests/test_coordinate_system.py index bd0cb7de9..cc5081889 100644 --- a/tests/test_geometry_converter.py +++ b/tests/test_coordinate_system.py @@ -6,7 +6,7 @@ from sharc.station_manager import StationManager -class TestGeometryConverter(unittest.TestCase): +class TestCoordinateSystem(unittest.TestCase): """Unit tests for the CoordinateSystem class and related coordinate transformations.""" def setUp(self): diff --git a/tests/test_geometry.py b/tests/test_geometry.py new file mode 100644 index 000000000..0646d4214 --- /dev/null +++ b/tests/test_geometry.py @@ -0,0 +1,395 @@ +import unittest +import numpy as np +import numpy.testing as npt +from sharc.support.geometry import SimulatorGeometry +from sharc.satellite.ngso.constants import EARTH_RADIUS_M +from copy import deepcopy + + +class TestGeometry(unittest.TestCase): + """Unit tests for the CoordinateSystem class and related coordinate transformations.""" + + def _test_expected_geom( + self, + geom, + expect_local, + expect_global, + *, + only_global_azim=None, + only_local_azim=None + ): + npt.assert_allclose(geom.x_local, expect_local["x"], atol=0.001) + npt.assert_allclose(geom.y_local, expect_local["y"], atol=0.001) + npt.assert_allclose(geom.z_local, expect_local["z"], atol=0.001) + npt.assert_allclose(geom.pointn_elev_local, expect_local["elev"], atol=0.001) + if only_local_azim is None: + npt.assert_allclose(geom.pointn_azim_local, expect_local["azim"], atol=0.001) + else: + self.assertEqual(expect_local["azim"].shape, geom.pointn_azim_local.shape) + npt.assert_allclose( + geom.pointn_azim_local[only_local_azim], + expect_local["azim"][only_local_azim], + atol=0.001 + ) + + npt.assert_allclose(geom.x_global, expect_global["x"], atol=0.001) + npt.assert_allclose(geom.y_global, expect_global["y"], atol=0.001) + npt.assert_allclose(geom.z_global, expect_global["z"], atol=0.001) + npt.assert_allclose(geom.pointn_elev_global, expect_global["elev"], atol=0.001) + if only_global_azim is None: + npt.assert_allclose( + geom.pointn_azim_global, + expect_global["azim"], + atol=0.001 + ) + else: + self.assertEqual(expect_local["azim"].shape, geom.pointn_azim_local.shape) + npt.assert_allclose( + geom.pointn_azim_global[only_global_azim], + expect_global["azim"][only_global_azim], + atol=0.001 + ) + + def setUp(self): + """Set up test fixtures for CoordinateSystem tests.""" + pass + + def test_set_coords_when_local_eq_global(self): + """Test setting coordinates when local should eq global + """ + ref = (10, -5, 1200) + + no_local = SimulatorGeometry(3, False, ref) + local_eq_global = SimulatorGeometry(3, True, ref) + local_eq_global.set_local_coord_sys( + np.repeat([ref[0]], 3), + np.repeat([ref[1]], 3), + np.repeat([ref[2]], 3), + ) + + """Test setting global coordinates + """ + expect = { + "x": np.array([10., 11., 21.]), "y": np.array([15., 19., 211]), + "z": np.array([20., 30., 50.]), + "azim": np.array([90., 70., 170.]), "elev": np.array([12., 15., 19.]), + } + no_local.set_global_coords(**expect) + local_eq_global.set_global_coords(**expect) + + """Verify that when local reference is not set or is set to global ref, + local and global are set to the expected, equal, value + """ + self._test_expected_geom(no_local, expect, expect) + self._test_expected_geom(local_eq_global, expect, expect) + + """Test setting local coordinates + """ + expect = { + "x": np.array([1235., 1241., 12341.]), "y": np.array([12413., 89012., 767.]), + "z": np.array([91238., 481., 123980.]), + "azim": np.array([-10., -12., -98.]), "elev": np.array([-70., -1., 0.]), + } + no_local.set_local_coords(**expect) + local_eq_global.set_local_coords(**expect) + + """Verify that when local reference is not set or is the same as global + local and global are set to the expected, equal, value + """ + self._test_expected_geom(no_local, expect, expect) + self._test_expected_geom(local_eq_global, expect, expect) + + def test_setting_different_alts(self): + """Test setting coordinates when local should eq global + """ + ref = (-45.0, 90, 30.) + + alt_vals = np.array([500., 800., 1200., 1800.]) + def init_diff_alt(): + geom = SimulatorGeometry(4, True, ref) + geom.set_local_coord_sys( + np.repeat([ref[0]], 4), + np.repeat([ref[1]], 4), + alt_vals, + ) + return geom + diff_alt = init_diff_alt() + + """Test setting global coordinates + """ + expect_local = { + "x": np.array([10., 11., 12., 985.]), "y": np.array([15., 31., 41341., 10.]), + "z": np.array([-5., 100., 1e4, -1e3]), + "azim": np.array([90., 10., -179., 180.]), "elev": np.array([89., -89., -1., 12.]), + } + expect_global = deepcopy(expect_local) + alt_vals_diff = ref[2] - alt_vals + expect_global["z"] -= alt_vals_diff + + diff_alt.set_global_coords(**expect_global) + + """Verify that when local reference is not set, local and global + are set to the expected, equal, value + """ + self._test_expected_geom(diff_alt, expect_local, expect_global) + + """Test setting local coordinates + """ + diff_alt = init_diff_alt() + diff_alt.set_local_coords(**expect_local) + + """Verify that when local reference is set to be the same as global, + local and global are set to the expected, value + """ + self._test_expected_geom(diff_alt, expect_local, expect_global) + + def test_setting_different_llas(self): + """Test setting coordinates when local should eq global + """ + ref = (0.0, 0.0, 0.0) + + llas = np.array([ + [-90., 0., 0.], + [90., 0., 0.], + [0., 90., 0.], + [0., -90., 0.], + ]).T + def init_diff_llas(): + geom = SimulatorGeometry(4, True, ref) + geom.set_local_coord_sys( + llas[0], + llas[1], + llas[2], + ) + return geom + + """Test setting global coordinates + """ + expect_local = { + "x": np.zeros(4), "y": np.zeros(4), + "z": np.zeros(4), + # point to north + "azim": np.zeros(4) + 90., "elev": np.zeros(4) - 10, + } + expect_global = { + "x": np.array([0., 0., 1., -1.]) * EARTH_RADIUS_M, + "y": np.array([-1., 1., 0., 0.]) * EARTH_RADIUS_M, + "z": np.zeros(4) - EARTH_RADIUS_M, + "azim": np.array([90., -90., 100., 80.]), + "elev": np.array([80., -80., 0., 0.]), + } + + diff_llas = init_diff_llas() + diff_llas.set_local_coords(**expect_local) + + self._test_expected_geom( + diff_llas, expect_local, expect_global, + # only_global_azim=np.where(abs(expect_global["elev"]) != 90.), + # only_local_azim=np.where(abs(expect_local["elev"]) != 90.), + ) + + diff_llas = init_diff_llas() + diff_llas.set_global_coords(**expect_global) + + self._test_expected_geom( + diff_llas, expect_local, expect_global, + # only_global_azim=np.where(abs(expect_global["elev"]) != 90.), + # only_local_azim=np.where(abs(expect_local["elev"]) != 90.), + ) + + def test_get_local_distance_to_diff_ref(self): + """Tests getting local distance from a station to another when + they have different local references + """ + ref = (90., 0., 0.) + local_llas = np.array([ + [0., 0., 0.], + [0., 1., 0.], + [0., -1., 0.], + [1., 0., 0.], + [-1., 0., 0.], + ]).T + def init_geom(): + geom = SimulatorGeometry(5, True, ref) + geom.set_local_coord_sys( + local_llas[0], + local_llas[1], + local_llas[2], + ) + geom.set_local_coords( + np.repeat(0., 5), + np.repeat(0., 5), + np.repeat(0., 5), + np.repeat(0., 5), + np.repeat(0., 5), + ) + return geom + + geom = init_geom() + + dists2d = geom.get_local_distance_to(geom) + + self.assertEqual(dists2d.shape, (5, 5)) + + # distance of any coord to itself + npt.assert_allclose(np.diagonal(dists2d), 0., atol=1e-8) + + # assuming 1deg difference should be ~ 111km at lat,lon = (0, 0) + npt.assert_allclose( + dists2d[0], np.array([0., 111e3, 111e3, 111e3, 111e3]), + atol=1e-8, # tolerance for == 0. + rtol=0.3/100, # tolerance for all others + ) + npt.assert_allclose( + dists2d[1], np.array([111e3, 0., 2 * 111e3, np.sqrt(2) * 111e3, np.sqrt(2) * 111e3]), + atol=1e-8, # tolerance for == 0. + rtol=0.3/100, # tolerance for all others + ) + npt.assert_allclose( + dists2d[2], np.array([111e3, 2 * 111e3, 0., np.sqrt(2) * 111e3, np.sqrt(2) * 111e3]), + atol=1e-8, # tolerance for == 0. + rtol=0.3/100, # tolerance for all others + ) + npt.assert_allclose( + dists2d[3], np.array([111e3, np.sqrt(2) * 111e3, np.sqrt(2) * 111e3, 0., 2 * 111e3]), + atol=1e-8, # tolerance for == 0. + rtol=0.3/100, # tolerance for all others + ) + npt.assert_allclose( + dists2d[4], np.array([111e3, np.sqrt(2) * 111e3, np.sqrt(2) * 111e3, 2 * 111e3, 0.]), + atol=1e-8, # tolerance for == 0. + rtol=0.3/100, # tolerance for all others + ) + + def test_get_local_distance_to_same_ref(self): + """Tests getting local distance from a station to another when + they have same local references + """ + ref = (90., 0., 0.) + local_llas = np.array([ + [0., 0., 0.], + [0., 0., 0.], + [0., -1., 0.], + [0., -1., 0.], + ]).T + + def init_geom(): + geom = SimulatorGeometry(4, True, ref) + geom.set_local_coord_sys( + local_llas[0], + local_llas[1], + local_llas[2], + ) + geom.set_local_coords( + np.tile([0., 20.], 2), + np.tile([0., 20.], 2), + np.tile([0., 10.], 2), + np.repeat(0., 4), + np.repeat(0., 4), + ) + return geom + + geom = init_geom() + + dists2d, z_dist = geom.get_local_distance_to(geom, return_z_dist=True) + + self.assertEqual(dists2d.shape, (4, 4)) + self.assertEqual(z_dist.shape, (4, 4)) + + # distance of any coord to itself + npt.assert_allclose(np.diagonal(dists2d), 0., atol=1e-8) + npt.assert_allclose(np.diagonal(z_dist), 0., atol=1e-8) + + # and that square diagonal is sqrt(2) * side_len + npt.assert_allclose( + dists2d[0, :2], np.array([0., 20 * np.sqrt(2)]), + atol=1e-8, + ) + npt.assert_allclose( + z_dist[0, :2], np.array([0., 10]), + atol=1e-8, + ) + + npt.assert_allclose( + dists2d[1, :2], np.array([20 * np.sqrt(2), 0.]), + atol=1e-8, + ) + npt.assert_allclose( + z_dist[1, :2], np.array([-10, 0.]), + atol=1e-8, + ) + + npt.assert_allclose( + dists2d[2, 2:], np.array([0., 20 * np.sqrt(2)]), + atol=1e-8, + ) + npt.assert_allclose( + z_dist[2, 2:], np.array([0, 10.]), + atol=1e-8, + ) + + npt.assert_allclose( + dists2d[3, 2:], np.array([20 * np.sqrt(2), 0.]), + atol=1e-8, + ) + npt.assert_allclose( + z_dist[3, 2:], np.array([-10., 0.]), + atol=1e-8, + ) + + # assuming 1deg difference should be ~ 111km at lat,lon = (0, 0) + npt.assert_allclose( + dists2d[:2, 2:], 111e3, + rtol=0.4/100, # tolerance for all others + ) + npt.assert_allclose( + dists2d[2:, :2], 111e3, + rtol=0.4/100, # tolerance for all others + ) + + def test_get_local_elevation(self): + """Tests getting local elevation from a station to another. + """ + ref = (90., 0., 0.) + local_llas = np.array([ + [0., 0., 0.], + [0., 0., 0.], + [0., -1., 0.], + [0., -1., 0.], + ]).T + + def init_geom(): + geom = SimulatorGeometry(4, True, ref) + geom.set_local_coord_sys( + local_llas[0], + local_llas[1], + local_llas[2], + ) + geom.set_local_coords( + np.tile([0., 20.], 2), + np.tile([0., 0.], 2), + np.tile([0., 20.], 2), + np.repeat(0., 4), + np.repeat(0., 4), + ) + return geom + + geom = init_geom() + + elev = geom.get_local_elevation(geom) + + self.assertEqual(elev.shape, (4, 4)) + + npt.assert_allclose(elev[0, 1], 45.) + npt.assert_allclose(elev[1, 0], -45.) + + npt.assert_allclose(elev[2, 3], 45.) + npt.assert_allclose(elev[3, 2], -45.) + + # obviously below horizon for each other + npt.assert_array_less(elev[:2, 2:], 0.) + npt.assert_array_less(elev[2:, :2], 0.) + + +if __name__ == '__main__': + unittest.main() From e5beed6d95335a285739e77ecf175486966a3339 Mon Sep 17 00:00:00 2001 From: artistrea Date: Mon, 13 Oct 2025 16:16:13 -0300 Subject: [PATCH 30/77] fix: lint errors --- tests/test_geometry.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 0646d4214..5d8a1f32d 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -105,6 +105,7 @@ def test_setting_different_alts(self): ref = (-45.0, 90, 30.) alt_vals = np.array([500., 800., 1200., 1800.]) + def init_diff_alt(): geom = SimulatorGeometry(4, True, ref) geom.set_local_coord_sys( @@ -154,6 +155,7 @@ def test_setting_different_llas(self): [0., 90., 0.], [0., -90., 0.], ]).T + def init_diff_llas(): geom = SimulatorGeometry(4, True, ref) geom.set_local_coord_sys( @@ -209,6 +211,7 @@ def test_get_local_distance_to_diff_ref(self): [1., 0., 0.], [-1., 0., 0.], ]).T + def init_geom(): geom = SimulatorGeometry(5, True, ref) geom.set_local_coord_sys( @@ -237,28 +240,28 @@ def init_geom(): # assuming 1deg difference should be ~ 111km at lat,lon = (0, 0) npt.assert_allclose( dists2d[0], np.array([0., 111e3, 111e3, 111e3, 111e3]), - atol=1e-8, # tolerance for == 0. - rtol=0.3/100, # tolerance for all others + atol=1e-8, # tolerance for == 0. + rtol=0.3 / 100, # tolerance for all others ) npt.assert_allclose( dists2d[1], np.array([111e3, 0., 2 * 111e3, np.sqrt(2) * 111e3, np.sqrt(2) * 111e3]), - atol=1e-8, # tolerance for == 0. - rtol=0.3/100, # tolerance for all others + atol=1e-8, # tolerance for == 0. + rtol=0.3 / 100, # tolerance for all others ) npt.assert_allclose( - dists2d[2], np.array([111e3, 2 * 111e3, 0., np.sqrt(2) * 111e3, np.sqrt(2) * 111e3]), - atol=1e-8, # tolerance for == 0. - rtol=0.3/100, # tolerance for all others + dists2d[2], np.array([111e3, 2 * 111e3, 0., np.sqrt(2) * 111e3, np.sqrt(2) * 111e3]), + atol=1e-8, # tolerance for == 0. + rtol=0.3 / 100, # tolerance for all others ) npt.assert_allclose( - dists2d[3], np.array([111e3, np.sqrt(2) * 111e3, np.sqrt(2) * 111e3, 0., 2 * 111e3]), - atol=1e-8, # tolerance for == 0. - rtol=0.3/100, # tolerance for all others + dists2d[3], np.array([111e3, np.sqrt(2) * 111e3, np.sqrt(2) * 111e3, 0., 2 * 111e3]), + atol=1e-8, # tolerance for == 0. + rtol=0.3 / 100, # tolerance for all others ) npt.assert_allclose( - dists2d[4], np.array([111e3, np.sqrt(2) * 111e3, np.sqrt(2) * 111e3, 2 * 111e3, 0.]), - atol=1e-8, # tolerance for == 0. - rtol=0.3/100, # tolerance for all others + dists2d[4], np.array([111e3, np.sqrt(2) * 111e3, np.sqrt(2) * 111e3, 2 * 111e3, 0.]), + atol=1e-8, # tolerance for == 0. + rtol=0.3 / 100, # tolerance for all others ) def test_get_local_distance_to_same_ref(self): @@ -340,11 +343,11 @@ def init_geom(): # assuming 1deg difference should be ~ 111km at lat,lon = (0, 0) npt.assert_allclose( dists2d[:2, 2:], 111e3, - rtol=0.4/100, # tolerance for all others + rtol=0.4 / 100, # tolerance for all others ) npt.assert_allclose( dists2d[2:, :2], 111e3, - rtol=0.4/100, # tolerance for all others + rtol=0.4 / 100, # tolerance for all others ) def test_get_local_elevation(self): From cc0203d35ebf2432f0277a9f4766de2e1fcec1b5 Mon Sep 17 00:00:00 2001 From: artistrea Date: Mon, 20 Oct 2025 23:51:56 -0300 Subject: [PATCH 31/77] hotfix: service grid for arbitrary polygon --- sharc/parameters/imt/parameters_grid.py | 10 ++++++++-- sharc/topology/topology_imt_mss_dc.py | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/sharc/parameters/imt/parameters_grid.py b/sharc/parameters/imt/parameters_grid.py index a7d82f4cc..53da12614 100644 --- a/sharc/parameters/imt/parameters_grid.py +++ b/sharc/parameters/imt/parameters_grid.py @@ -384,13 +384,17 @@ def load_from_active_sat_conditions( def _recalculate_grid_polygon_if_needed(self, ctx: str, force_update=False): if self.eligibility_polygon is not None and not force_update: - return + return False self.grid_in_zone._calculate_polygon() self.grid_exclusion_zone._calculate_polygon() # TODO: plot this to verify + pols = self.grid_in_zone._unprocessed_polygon + if not isinstance(pols, list): + pols = [pols] + self.eligibility_polygon = shp.ops.unary_union(shrink_countries_by_km( - self.grid_in_zone._unprocessed_polygon, + pols, self.eligible_sats_margin_from_border, )) @@ -400,6 +404,8 @@ def _recalculate_grid_polygon_if_needed(self, ctx: str, force_update=False): assert not self.eligibility_polygon.is_empty, \ "Can't have a empty eligibility_polygon as filter" + return True + @dataclass class ParametersSelectActiveSatellite(ParametersBase): diff --git a/sharc/topology/topology_imt_mss_dc.py b/sharc/topology/topology_imt_mss_dc.py index b7fb3f4a0..01debf441 100644 --- a/sharc/topology/topology_imt_mss_dc.py +++ b/sharc/topology/topology_imt_mss_dc.py @@ -377,7 +377,7 @@ def get_satellite_pointing( # TODO: remove this reset_grid call from here. # Since it should be called once per drop, it shouldn't # be this deep in the program - orbit_params.beam_positioning.service_grid.reset_grid("service_grid", random_number_gen) + orbit_params.beam_positioning.service_grid.reset_grid("service_grid", random_number_gen, True) # 2xN, (lon, lat) grid = orbit_params.beam_positioning.service_grid.lon_lat_grid From 506087a74fabed0cb836420cc26596a1abc41627 Mon Sep 17 00:00:00 2001 From: artistrea Date: Mon, 20 Oct 2025 23:52:42 -0300 Subject: [PATCH 32/77] hotfix: some mss d2d station needs to be active --- sharc/station_factory.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/sharc/station_factory.py b/sharc/station_factory.py index f3eed4669..1a3c32c7b 100644 --- a/sharc/station_factory.py +++ b/sharc/station_factory.py @@ -1760,13 +1760,15 @@ def generate_mss_d2d( mss_d2d.active = np.zeros(total_satellites, dtype=bool) if mss_d2d_values["num_active_satellites"] != mss_d2d_values["num_satellites"]: - mss_d2d.active[mss_d2d_values["active_satellites_idxs"]] = random_number_gen.uniform( - size=len(mss_d2d_values["active_satellites_idxs"])) < params.beams_load_factor + while np.sum(mss_d2d.active) == 0: + mss_d2d.active[mss_d2d_values["active_satellites_idxs"]] = random_number_gen.uniform( + size=len(mss_d2d_values["active_satellites_idxs"])) < params.beams_load_factor else: - # Set active satellite flags - mss_d2d.active = random_number_gen.uniform( - size=total_satellites - ) < params.beams_load_factor + while np.sum(mss_d2d.active) == 0: + # Set active satellite flags + mss_d2d.active = random_number_gen.uniform( + size=total_satellites + ) < params.beams_load_factor # Initialize satellites antennas # we need to initialize them after coordinates transformation because of From bcc5c00c5c5caf0c7a66c499a1d3b333cce03087 Mon Sep 17 00:00:00 2001 From: Bruno Faria Date: Wed, 22 Oct 2025 11:29:22 +0200 Subject: [PATCH 33/77] update(satutils): Added util functions for elevation to off-axis angles transformations. * Also added the Earth-arc calculation from off-axis. --- sharc/satellite/utils/sat_utils.py | 106 +++++++++++++++++++++++++++++ tests/test_sat_utils.py | 44 ++++++++++++ 2 files changed, 150 insertions(+) diff --git a/sharc/satellite/utils/sat_utils.py b/sharc/satellite/utils/sat_utils.py index 49d86ed23..3d75e17f1 100644 --- a/sharc/satellite/utils/sat_utils.py +++ b/sharc/satellite/utils/sat_utils.py @@ -133,9 +133,115 @@ def haversine( a = np.sin(dlat / 2)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2)**2 return 2 * R * np.arcsin(np.sqrt(a)) +def sat_elevation_to_offaxis(elevation_angle_deg: float | np.ndarray, sat_altitude_m: float | np.ndarray) -> float: + """ + Convert satellite elevation angle to off-axis angle (angle from nadir). + + Parameters + ---------- + elevation_angle_deg : float + Elevation angle in degrees. + sat_altitude_m : float + Satellite altitude above Earth's surface in meters. + + Returns + ------- + float + Off-axis angle in degrees. + """ + if np.isscalar(elevation_angle_deg): + if elevation_angle_deg < 0 or elevation_angle_deg > 90: + raise ValueError("Elevation angle must be between 0 and 90 degrees.") + else: + if (elevation_angle_deg < 0).any() or (elevation_angle_deg > 90).any(): + raise ValueError("Elevation angle must be between 0 and 90 degrees.") + + # Convert elevation angle to radians + elevation_angle_rad = np.deg2rad(elevation_angle_deg) + + # Calculate the distance from the Earth's center to the satellite + r_sat = EARTH_RADIUS_M + sat_altitude_m + + # Calculate the off-axis angle using the spherical triangle relationship + offaxis_angle_rad = np.arcsin( + (EARTH_RADIUS_M / r_sat) * np.sin(elevation_angle_rad + np.pi / 2) + ) + + # Convert off-axis angle back to degrees + offaxis_angle_deg = np.rad2deg(offaxis_angle_rad) + + return offaxis_angle_deg + +def offaxis_to_sat_elevation(offaxis_angle_deg: float | np.ndarray, sat_altitude_m: float | np.ndarray) -> float: + """ + Convert off-axis angle (angle from nadir) to satellite elevation angle. + + Parameters + ---------- + offaxis_angle_deg : float + Off-axis angle in degrees. + sat_altitude_km : float + Satellite altitude above Earth's surface in meters. + + Returns + ------- + float + Elevation angle in degrees. + """ + # We know that 90 deg offaxis is physically impossible, so raise ValueError + if np.isscalar(offaxis_angle_deg): + if offaxis_angle_deg < 0 or offaxis_angle_deg >= 90: + raise ValueError("Elevation angle must be between 0 and 90 degrees.") + else: + if (offaxis_angle_deg < 0).any() or (offaxis_angle_deg >= 90).any(): + raise ValueError("Elevation angle must be between 0 and 90 degrees.") + # Convert off-axis angle to radians + offaxis_angle_rad = np.deg2rad(offaxis_angle_deg) + + # Calculate the distance from the Earth's center to the satellite + r_sat = EARTH_RADIUS_M + sat_altitude_m + + # Calculate the elevation angle using the sine law. + elevation_angle_rad = np.pi / 2 - np.arcsin((r_sat) / EARTH_RADIUS_M * np.sin(offaxis_angle_rad)) + + # Convert elevation angle back to degrees + elevation_angle_deg = np.rad2deg(elevation_angle_rad) + + return elevation_angle_deg + +def earth_arc_length_from_nadir(offaxis_angle_deg: float | np.ndarray, sat_altitude_m: float | np.ndarray) -> float: + """ + Calculate the Earth's surface arc length from nadir to the point + corresponding to the given off-axis angle. + + Parameters + ---------- + offaxis_angle_deg : float + Off-axis angle in degrees. + sat_altitude_km : float + Satellite altitude above Earth's surface in meters. + + Returns + ------- + float + Arc length on Earth's surface in meters. + """ + # Convert off-axis angle to radians + offaxis_angle_rad = np.deg2rad(offaxis_angle_deg) + + phi_rad = np.deg2rad(offaxis_to_sat_elevation(offaxis_angle_deg, sat_altitude_m) + 90.0) + + central_angle_rad = np.pi - phi_rad - offaxis_angle_rad + + # Calculate the arc length on Earth's surface + arc_length_m = EARTH_RADIUS_M * central_angle_rad + + return arc_length_m if __name__ == "__main__": r1 = ecef2lla(7792.1450, 0, 0) print(r1) r2 = lla2ecef(r1[0], r1[1], r1[2]) print(r2) + + print(earth_arc_length_from_nadir(np.array([0, 10, 30, 45, 60]), 520.0)) diff --git a/tests/test_sat_utils.py b/tests/test_sat_utils.py index b1bff794a..c15be42c3 100644 --- a/tests/test_sat_utils.py +++ b/tests/test_sat_utils.py @@ -136,6 +136,50 @@ def test_calculate_elev_angle(self): ) npt.assert_almost_equal(e, expected_elevations[i], 1) + def test_sat_elevation_offaxis_conversion(self): + """Test conversion between satellite elevation angle and off-axis angle.""" + sat_altitude_km = 520.0 + + test_elevation_angles = np.array([0.0, 10.0, 30.0, 45.0, 60.0, 80.0, 90.0]) + for elev_angle in test_elevation_angles: + offaxis_angle = sat_utils.sat_elevation_to_offaxis(elev_angle, sat_altitude_km) + elev_angle_converted = sat_utils.offaxis_to_sat_elevation(offaxis_angle, sat_altitude_km) + npt.assert_almost_equal(elev_angle, elev_angle_converted, 5) + + def test_sat_elevation_offaxis_conversion_raises_value_error(self): + """Test that ValueError is raised for invalid elevation angles.""" + sat_altitude_km = 520.0 + + invalid_elevation_angles = [-10.0, 100.0, 150.0] + for elev_angle in invalid_elevation_angles: + with self.assertRaises(ValueError): + sat_utils.sat_elevation_to_offaxis(elev_angle, sat_altitude_km) + + with self.assertRaises(ValueError): + sat_utils.sat_elevation_to_offaxis(np.array([10.0, -5.0, 30.0]), sat_altitude_km) + + def test_offaxis_sat_elevation_conversion_raises_value_error(self): + """Test that ValueError is raised for invalid elevation angles in offaxis_to_sat_elevation.""" + sat_altitude_km = 520.0 + + invalid_offaxis_angles = [-10.0, 100.0, 150.0] + for elev_angle in invalid_offaxis_angles: + with self.assertRaises(ValueError): + sat_utils.offaxis_to_sat_elevation(elev_angle, sat_altitude_km) + + with self.assertRaises(ValueError): + sat_utils.offaxis_to_sat_elevation(np.array([10.0, -5.0, 30.0]), sat_altitude_km) + + def test_earth_arc_length_from_nadir(self): + """Test calculation of Earth's arc length from nadir based on off-axis angle.""" + sat_altitude_m = 520.0 * 1e3 + + test_offaxis_angles = np.array([0.0, 10.0, 30.0, 45.0, 60.0]) + expected_arc_lengths_m = np.array([0, 91.80971067, 304.53451317, 543.82932797, 1056.78781731]) * 1e3 + + arc_length = sat_utils.earth_arc_length_from_nadir(test_offaxis_angles, sat_altitude_m) + npt.assert_almost_equal(arc_length, expected_arc_lengths_m, decimal=3) + if __name__ == '__main__': unittest.main() From c927f059be06cff1ccc8340819b312781167a58f Mon Sep 17 00:00:00 2001 From: Bruno Faria Date: Wed, 22 Oct 2025 11:30:26 +0200 Subject: [PATCH 34/77] fix(topology): Fixed IMT MSS DC topology subsatellite_distance * The converstion of offaxis to radias was missing. * Now using the curved Earth-arc for better precision. --- sharc/topology/topology_imt_mss_dc.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sharc/topology/topology_imt_mss_dc.py b/sharc/topology/topology_imt_mss_dc.py index b307ec6ec..253559b82 100644 --- a/sharc/topology/topology_imt_mss_dc.py +++ b/sharc/topology/topology_imt_mss_dc.py @@ -23,7 +23,7 @@ from sharc.satellite.ngso.orbit_model import OrbitModel from sharc.support.sharc_geom import CoordinateSystem, rotate_angles_based_on_new_nadir from sharc.topology.topology_ntn import TopologyNTN -from sharc.satellite.utils.sat_utils import calc_elevation +from sharc.satellite.utils.sat_utils import calc_elevation, earth_arc_length_from_nadir from sharc.support.sharc_geom import lla2ecef, cartesian_to_polar, polar_to_cartesian from sharc.satellite.ngso.constants import EARTH_DEFAULT_CRS @@ -529,7 +529,11 @@ def get_satellite_pointing( raise ValueError( f"mss_d2d_params.beam_positioning.angle_from_subsatellite_theta.type = \n" f"'{ orbit_params.beam_positioning.angle_from_subsatellite_theta.type}' is not recognized!") - subsatellite_distance_add = sat_altitude * np.tan(off_nadir_add) + # subsatellite_distance_add = sat_altitude * np.tan(np.deg2rad(off_nadir_add)) + subsatellite_distance_add = earth_arc_length_from_nadir( + off_nadir_add, + sat_altitude + ) azim_add = TopologyImtMssDc.get_distr( random_number_gen, From 392473ef80dabb65603a8f93a1455266505cfdfb Mon Sep 17 00:00:00 2001 From: Bruno Faria Date: Thu, 23 Oct 2025 09:43:00 +0200 Subject: [PATCH 35/77] Fixed lint errors --- sharc/satellite/utils/sat_utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sharc/satellite/utils/sat_utils.py b/sharc/satellite/utils/sat_utils.py index 3d75e17f1..97c37fdd1 100644 --- a/sharc/satellite/utils/sat_utils.py +++ b/sharc/satellite/utils/sat_utils.py @@ -133,6 +133,7 @@ def haversine( a = np.sin(dlat / 2)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2)**2 return 2 * R * np.arcsin(np.sqrt(a)) + def sat_elevation_to_offaxis(elevation_angle_deg: float | np.ndarray, sat_altitude_m: float | np.ndarray) -> float: """ Convert satellite elevation angle to off-axis angle (angle from nadir). @@ -172,6 +173,7 @@ def sat_elevation_to_offaxis(elevation_angle_deg: float | np.ndarray, sat_altitu return offaxis_angle_deg + def offaxis_to_sat_elevation(offaxis_angle_deg: float | np.ndarray, sat_altitude_m: float | np.ndarray) -> float: """ Convert off-axis angle (angle from nadir) to satellite elevation angle. @@ -209,6 +211,7 @@ def offaxis_to_sat_elevation(offaxis_angle_deg: float | np.ndarray, sat_altitude return elevation_angle_deg + def earth_arc_length_from_nadir(offaxis_angle_deg: float | np.ndarray, sat_altitude_m: float | np.ndarray) -> float: """ Calculate the Earth's surface arc length from nadir to the point @@ -238,10 +241,13 @@ def earth_arc_length_from_nadir(offaxis_angle_deg: float | np.ndarray, sat_altit return arc_length_m + if __name__ == "__main__": r1 = ecef2lla(7792.1450, 0, 0) print(r1) r2 = lla2ecef(r1[0], r1[1], r1[2]) print(r2) - print(earth_arc_length_from_nadir(np.array([0, 10, 30, 45, 60]), 520.0)) + print(earth_arc_length_from_nadir(2.19, 520.0 * 1e3)) + + print(sat_elevation_to_offaxis(50.0, 520e3)) From c55eceb9d30ea460d4d82889f5c608821c70e14b Mon Sep 17 00:00:00 2001 From: Bruno Faria Date: Thu, 23 Oct 2025 16:10:36 +0200 Subject: [PATCH 36/77] Revert "fix(topology): Fixed IMT MSS DC topology subsatellite_distance" This reverts commit c927f059be06cff1ccc8340819b312781167a58f. --- sharc/topology/topology_imt_mss_dc.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/sharc/topology/topology_imt_mss_dc.py b/sharc/topology/topology_imt_mss_dc.py index 253559b82..b307ec6ec 100644 --- a/sharc/topology/topology_imt_mss_dc.py +++ b/sharc/topology/topology_imt_mss_dc.py @@ -23,7 +23,7 @@ from sharc.satellite.ngso.orbit_model import OrbitModel from sharc.support.sharc_geom import CoordinateSystem, rotate_angles_based_on_new_nadir from sharc.topology.topology_ntn import TopologyNTN -from sharc.satellite.utils.sat_utils import calc_elevation, earth_arc_length_from_nadir +from sharc.satellite.utils.sat_utils import calc_elevation from sharc.support.sharc_geom import lla2ecef, cartesian_to_polar, polar_to_cartesian from sharc.satellite.ngso.constants import EARTH_DEFAULT_CRS @@ -529,11 +529,7 @@ def get_satellite_pointing( raise ValueError( f"mss_d2d_params.beam_positioning.angle_from_subsatellite_theta.type = \n" f"'{ orbit_params.beam_positioning.angle_from_subsatellite_theta.type}' is not recognized!") - # subsatellite_distance_add = sat_altitude * np.tan(np.deg2rad(off_nadir_add)) - subsatellite_distance_add = earth_arc_length_from_nadir( - off_nadir_add, - sat_altitude - ) + subsatellite_distance_add = sat_altitude * np.tan(off_nadir_add) azim_add = TopologyImtMssDc.get_distr( random_number_gen, From cb18360c3fc7009cfa565a7b4bffe27d9dae80a9 Mon Sep 17 00:00:00 2001 From: Bruno Faria Date: Thu, 23 Oct 2025 16:12:24 +0200 Subject: [PATCH 37/77] fix(topology): Fixes a wrong parameter unit passed to np.tan when calculating subsatellite_distance_add. --- sharc/topology/topology_imt_mss_dc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sharc/topology/topology_imt_mss_dc.py b/sharc/topology/topology_imt_mss_dc.py index b307ec6ec..495fed989 100644 --- a/sharc/topology/topology_imt_mss_dc.py +++ b/sharc/topology/topology_imt_mss_dc.py @@ -529,7 +529,7 @@ def get_satellite_pointing( raise ValueError( f"mss_d2d_params.beam_positioning.angle_from_subsatellite_theta.type = \n" f"'{ orbit_params.beam_positioning.angle_from_subsatellite_theta.type}' is not recognized!") - subsatellite_distance_add = sat_altitude * np.tan(off_nadir_add) + subsatellite_distance_add = sat_altitude * np.tan(np.deg2rad(off_nadir_add)) azim_add = TopologyImtMssDc.get_distr( random_number_gen, From a36c768fe205716d432b07ddfd155baf18894888 Mon Sep 17 00:00:00 2001 From: artistrea Date: Fri, 24 Oct 2025 08:23:52 -0300 Subject: [PATCH 38/77] hotfix: service grid candidate satellite margin area --- sharc/parameters/imt/parameters_grid.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sharc/parameters/imt/parameters_grid.py b/sharc/parameters/imt/parameters_grid.py index 53da12614..bd6e01441 100644 --- a/sharc/parameters/imt/parameters_grid.py +++ b/sharc/parameters/imt/parameters_grid.py @@ -4,6 +4,7 @@ import typing from pathlib import Path import shapely as shp +from collections.abc import Iterable from sharc.support.sharc_utils import load_gdf from sharc.support.sharc_geom import ( @@ -388,9 +389,12 @@ def _recalculate_grid_polygon_if_needed(self, ctx: str, force_update=False): self.grid_in_zone._calculate_polygon() self.grid_exclusion_zone._calculate_polygon() - # TODO: plot this to verify + # For creating selectable satellite zone + # we consider polygon before shrinking country borders or other + # geometry processing pols = self.grid_in_zone._unprocessed_polygon - if not isinstance(pols, list): + + if not isinstance(pols, Iterable): pols = [pols] self.eligibility_polygon = shp.ops.unary_union(shrink_countries_by_km( From bdb0eb9ac8637aacea4d986b01963e5344da744a Mon Sep 17 00:00:00 2001 From: artistrea Date: Fri, 24 Oct 2025 11:40:52 -0300 Subject: [PATCH 39/77] hotfix: geom uses_local_coord reference --- sharc/propagation/propagation_hdfss.py | 2 +- sharc/propagation/propagation_p619.py | 4 ++-- sharc/propagation/propagation_tvro.py | 4 ++-- sharc/propagation/propagation_uma.py | 2 +- sharc/propagation/propagation_umi.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sharc/propagation/propagation_hdfss.py b/sharc/propagation/propagation_hdfss.py index b3c7e60d1..68cdd3bd0 100644 --- a/sharc/propagation/propagation_hdfss.py +++ b/sharc/propagation/propagation_hdfss.py @@ -76,7 +76,7 @@ def get_loss( np.ones(distance.shape) # P.452 expects GHz elevation = station_b.geom.get_local_elevation(station_a.geom) - if station_a.uses_local_coords or station_b.uses_local_coords: + if station_a.geom.uses_local_coords or station_b.geom.uses_local_coords: raise NotImplementedError( "HDFSS currently assumes stations z == height. " "If stations has local coords != global coords, this probably isn't true" diff --git a/sharc/propagation/propagation_p619.py b/sharc/propagation/propagation_p619.py index 903e09e81..fa44c76c4 100644 --- a/sharc/propagation/propagation_p619.py +++ b/sharc/propagation/propagation_p619.py @@ -349,7 +349,7 @@ def get_loss( # Elevation angles seen from the station on Earth. elevation_angles = {} if station_a.is_space_station: - if station_b.uses_local_coords: + if station_b.geom.uses_local_coords: raise NotImplementedError( "P619 currently assumes earth station z == height. " "If ES has local coords != global coords, this probably isn't true" @@ -369,7 +369,7 @@ def get_loss( elevation_angles["apparent"] = np.transpose( elevation_angles["apparent"]) elif station_b.is_space_station: - if station_a.uses_local_coords: + if station_a.geom.uses_local_coords: raise NotImplementedError( "P619 currently assumes earth station z == height. " "If ES has local coords != global coords, this probably isn't true" diff --git a/sharc/propagation/propagation_tvro.py b/sharc/propagation/propagation_tvro.py index dcb2f81a2..c99b52977 100644 --- a/sharc/propagation/propagation_tvro.py +++ b/sharc/propagation/propagation_tvro.py @@ -92,7 +92,7 @@ def get_loss( # Use the right interface whether the link is IMT-IMT or IMT-System # TODO: Refactor __get_loss and get rid of that if-else. if station_a.is_imt_station() and station_b.is_imt_station(): - if station_a.uses_local_coords: + if station_a.geom.uses_local_coords: raise NotImplementedError( "TVRO currently assumes UE z == height. " "If UE has local coords != global coords, this probably isn't true" @@ -109,7 +109,7 @@ def get_loss( imt_station, sys_station = (station_a, station_b) \ if station_a.is_imt_station() else (station_b, station_a) - if sys_station.uses_local_coords: + if sys_station.geom.uses_local_coords: raise NotImplementedError( "TVRO currently assumes System z == height. " "If System has local coords != global coords, this probably isn't true" diff --git a/sharc/propagation/propagation_uma.py b/sharc/propagation/propagation_uma.py index c1ad905dc..74795dd4e 100644 --- a/sharc/propagation/propagation_uma.py +++ b/sharc/propagation/propagation_uma.py @@ -66,7 +66,7 @@ def get_loss( distances_2d = station_a.geom.get_local_distance_to(station_b.geom) distances_3d = station_a.geom.get_3d_distance_to(station_b.geom) - if station_a.uses_local_coords or station_b.uses_local_coords: + if station_a.geom.uses_local_coords or station_b.geom.uses_local_coords: raise NotImplementedError( "UMa currently assumes stations z == height. " "If stations has local coords != global coords, this probably isn't true" diff --git a/sharc/propagation/propagation_umi.py b/sharc/propagation/propagation_umi.py index 2bd905af3..4541397a3 100644 --- a/sharc/propagation/propagation_umi.py +++ b/sharc/propagation/propagation_umi.py @@ -72,7 +72,7 @@ def get_loss( distance_2d = station_a.geom.get_local_distance_to(station_b.geom) distance_3d = station_a.geom.get_3d_distance_to(station_b.geom) - if station_a.uses_local_coords or station_b.uses_local_coords: + if station_a.geom.uses_local_coords or station_b.geom.uses_local_coords: raise NotImplementedError( "UMi currently assumes stations z == height. " "If stations has local coords != global coords, this probably isn't true" From 67ac410ce84854b9395913e4170eea1e531450c3 Mon Sep 17 00:00:00 2001 From: artistrea Date: Mon, 3 Nov 2025 23:42:16 -0300 Subject: [PATCH 40/77] feat: minimum elevation for service grid point to satellite link --- sharc/parameters/imt/parameters_grid.py | 6 +++ sharc/topology/topology_imt_mss_dc.py | 12 +++-- tests/parameters/parameters_for_testing.yaml | 2 + tests/parameters/test_parameters.py | 4 ++ tests/test_topology_imt_mss_dc.py | 46 ++++++++++++++++++++ 5 files changed, 66 insertions(+), 4 deletions(-) diff --git a/sharc/parameters/imt/parameters_grid.py b/sharc/parameters/imt/parameters_grid.py index bd6e01441..551a5d2df 100644 --- a/sharc/parameters/imt/parameters_grid.py +++ b/sharc/parameters/imt/parameters_grid.py @@ -339,6 +339,9 @@ class ParametersSatelliteWithServiceGrid(ParametersTerrestrialGrid): beam_radius: float = None + # [deg] + minimum_service_angle: float = 5.0 + def validate(self, ctx): """Validates instance parameters. Parameters @@ -353,6 +356,9 @@ def validate(self, ctx): ) self.cell_radius = self.beam_radius + if self.minimum_service_angle < 0. or self.minimum_service_angle > 90: + raise ValueError(f"{ctx}.minimum_service_angle should be in [0, 90]") + if not isinstance( self.eligible_sats_margin_from_border, float) and not isinstance( diff --git a/sharc/topology/topology_imt_mss_dc.py b/sharc/topology/topology_imt_mss_dc.py index 01debf441..8c22dc7cf 100644 --- a/sharc/topology/topology_imt_mss_dc.py +++ b/sharc/topology/topology_imt_mss_dc.py @@ -408,7 +408,7 @@ def get_satellite_pointing( eligible_sats_msk &= polygon_mask eligible_sats_idx = np.where(eligible_sats_msk)[0] - elev = calc_elevation( + all_elevations = calc_elevation( grid_lat[:, np.newaxis], all_sat_lat[eligible_sats_msk][np.newaxis, :], grid_lon[:, np.newaxis], @@ -417,7 +417,7 @@ def get_satellite_pointing( es_height=0, ) - best_sats = elev.argmax(axis=-1) + best_sats = all_elevations.argmax(axis=-1) best_sats_true = eligible_sats_idx[best_sats] @@ -450,8 +450,12 @@ def get_satellite_pointing( sat_points_towards = defaultdict(list) - for i, sat in enumerate(best_sats_true): - sat_points_towards[sat].append(i) + min_service_angle = orbit_params.beam_positioning.service_grid.minimum_service_angle + for i, sat in enumerate(best_sats): + if all_elevations[i][sat] < min_service_angle: + continue + actual_best_sat_idx = eligible_sats_idx[sat] + sat_points_towards[actual_best_sat_idx].append(i) # now only return the angles that # the caller asked with the active_sat_idxs parameter diff --git a/tests/parameters/parameters_for_testing.yaml b/tests/parameters/parameters_for_testing.yaml index 1cd0d9fce..208116b63 100644 --- a/tests/parameters/parameters_for_testing.yaml +++ b/tests/parameters/parameters_for_testing.yaml @@ -264,6 +264,7 @@ imt: # if positive, makes border smaller by x km # if negative, makes border bigger by x km eligible_sats_margin_from_border: -2.1 + minimum_service_angle: 10.1 sat_is_active_if: # for a satellite to be active, it needs to respect ALL conditions conditions: @@ -1096,6 +1097,7 @@ mss_d2d: # if positive, makes border smaller by x km # if negative, makes border bigger by x km eligible_sats_margin_from_border: -2.1 + minimum_service_angle: 12.1 sat_is_active_if: # for a satellite to be active, it needs to respect ALL conditions conditions: diff --git a/tests/parameters/test_parameters.py b/tests/parameters/test_parameters.py index e174b53db..f87b5eee4 100644 --- a/tests/parameters/test_parameters.py +++ b/tests/parameters/test_parameters.py @@ -313,6 +313,8 @@ def test_parameters_imt(self): 0.11) self.assertEqual( self.parameters.imt.topology.mss_dc.beam_positioning.service_grid.eligible_sats_margin_from_border, -2.1) + self.assertEqual( + self.parameters.imt.topology.mss_dc.beam_positioning.service_grid.minimum_service_angle, 10.1) self.assertEqual(len( self.parameters.imt.topology.mss_dc.beam_positioning.service_grid.grid_in_zone.from_countries.country_names), 2) self.assertEqual( @@ -709,6 +711,8 @@ def test_parametes_mss_d2d(self): 0.11) self.assertEqual( self.parameters.mss_d2d.beam_positioning.service_grid.eligible_sats_margin_from_border, -2.1) + self.assertEqual( + self.parameters.mss_d2d.beam_positioning.service_grid.minimum_service_angle, 12.1) self.assertEqual( len(self.parameters.mss_d2d.beam_positioning.service_grid.grid_in_zone.from_countries.country_names), 2) self.assertEqual( diff --git a/tests/test_topology_imt_mss_dc.py b/tests/test_topology_imt_mss_dc.py index 481e7ac85..cfa669d96 100644 --- a/tests/test_topology_imt_mss_dc.py +++ b/tests/test_topology_imt_mss_dc.py @@ -6,6 +6,7 @@ from sharc.station_manager import StationManager from sharc.parameters.parameters_orbit import ParametersOrbit from sharc.support.sharc_geom import CoordinateSystem, lla2ecef +from sharc.satellite.utils.sat_utils import calc_elevation class TestTopologyImtMssDc(unittest.TestCase): @@ -156,6 +157,51 @@ def test_visible_satellites(self): # oblateness npt.assert_array_less(min_elevation_angle, xy_plane_elevations) + def test_minimum_service_angle(self): + """Test minimum visilibity angle for service grid service.""" + orbit = ParametersOrbit( + n_planes=1, + sats_per_plane=1, + phasing_deg=3.9, + long_asc_deg=18.0, + inclination_deg=54.5, + perigee_alt_km=525, + apogee_alt_km=525, + ) + self.coordinate_system.set_reference(0.0, 0.0, 0) + + self.params.orbits = [orbit] + self.params.beam_positioning.type = "SERVICE_GRID" + self.params.beam_positioning.service_grid.grid_in_zone.type = "CIRCLE" + self.params.beam_positioning.service_grid.grid_in_zone.circle.center_lat = 0.0 + self.params.beam_positioning.service_grid.grid_in_zone.circle.center_lon = 0.0 + self.params.beam_positioning.service_grid.grid_in_zone.circle.radius_km = 10 * 111.0 + self.params.validate("") + + self.imt_mss_dc_topology = TopologyImtMssDc( + self.params, self.coordinate_system) + + n_previous_selected = np.inf + for a in [5, 50, 80]: + self.imt_mss_dc_topology.orbit_params.beam_positioning.service_grid.minimum_service_angle = a + self.imt_mss_dc_topology.calculate_coordinates( + random_number_gen=np.random.RandomState(8)) + + lon_lat_grid = self.params.beam_positioning.service_grid.lon_lat_grid + elev_from_bs = calc_elevation( + lon_lat_grid[1], + self.imt_mss_dc_topology.lat[0], + lon_lat_grid[0], + self.imt_mss_dc_topology.lon[0], + sat_height=self.imt_mss_dc_topology.height[0], + es_height=0.0, + ) + n_selected = np.sum(elev_from_bs >= a) + self.assertLess(n_selected, n_previous_selected) + self.assertLess(n_selected, len(elev_from_bs)) + self.assertEqual(n_selected, self.imt_mss_dc_topology.num_base_stations) + n_previous_selected = n_selected + if __name__ == '__main__': unittest.main() From 34f455d31ab1b1eaab6b240a4e356f3cbf5aa13e Mon Sep 17 00:00:00 2001 From: artistrea Date: Wed, 5 Nov 2025 15:24:02 -0300 Subject: [PATCH 41/77] fix(topology): imt mss dc topology could create num_stations = 0 fixed --- sharc/topology/topology_imt_mss_dc.py | 447 +++++++++++++------------- 1 file changed, 225 insertions(+), 222 deletions(-) diff --git a/sharc/topology/topology_imt_mss_dc.py b/sharc/topology/topology_imt_mss_dc.py index 8c22dc7cf..e79e1dcbb 100644 --- a/sharc/topology/topology_imt_mss_dc.py +++ b/sharc/topology/topology_imt_mss_dc.py @@ -75,117 +75,101 @@ def get_coordinates( ): """Compute the coordinates of the visible space stations.""" orbit_params.sat_is_active_if.validate("orbit_params.sat_is_active_if") - # Calculate the total number of satellites across all orbits - total_satellites = sum( - orbit.n_planes * - orbit.sats_per_plane for orbit in orbit_params.orbits) - if any([ - not hasattr(orbit_params, attr) - for attr in ["sat_is_active_if", "orbits", "beam_radius", "num_beams", "beam_positioning"] - ]): - raise ValueError( - "Parameter passed to TopologyImtMssDc needs to contain all of the attributes:\n" - '["sat_is_active_if", "orbits", "beam_radius", "num_beams", "beam_positioning"]') - - idx_orbit = np.zeros( - total_satellites, - dtype=int) # Add orbit index array - - # List to store indices of active satellites - active_satellite_idxs = [] + num_base_stations = 0 MAX_ITER = 10000 # Maximum iterations to find at least one visible satellite - i = 0 # Iteration counter for ensuring satellite visibility - while len(active_satellite_idxs) == 0: - # Initialize arrays to store satellite positions, angles and - # distance from center of earth - all_positions = { - "R": [], - "lat": [], - "lon": [], - "sx": [], - "sy": [], - "sz": [], - "alt": []} - all_elevations = [] # Store satellite elevations - all_azimuths = [] # Store satellite azimuths - - current_sat_idx = 0 # Index tracker for satellites across all orbits - - # Iterate through each orbit defined in the parameters - for orbit_idx, param in enumerate(orbit_params.orbits): - orbit = OrbitModel( - Nsp=param.sats_per_plane, # Satellites per plane - Np=param.n_planes, # Number of orbital planes - phasing=param.phasing_deg, # Phasing angle in degrees - long_asc=param.long_asc_deg, # Longitude of ascending node in degrees - omega=param.omega_deg, # Argument of perigee in degrees - delta=param.inclination_deg, # Orbital inclination in degrees - hp=param.perigee_alt_km, # Perigee altitude in kilometers - ha=param.apogee_alt_km, # Apogee altitude in kilometers - Mo=param.initial_mean_anomaly, # Initial mean anomaly in degrees - # whether to use only time as random variable - model_time_as_random_variable=param.model_time_as_random_variable, - t_min=param.t_min, - t_max=param.t_max, - ) - # Generate random positions for satellites in this orbit - pos_vec = orbit.get_orbit_positions_random( - rng=random_number_gen) - - # Determine the number of satellites in this orbit - num_satellites = len(pos_vec["sx"]) - - # Assign orbit index to satellites - idx_orbit[current_sat_idx:current_sat_idx + - num_satellites] = orbit_idx - - # Extract satellite positions and calculate distances - sx, sy, sz = pos_vec['sx'], pos_vec['sy'], pos_vec['sz'] - # Distance from Earth's center - r = np.sqrt(sx**2 + sy**2 + sz**2) - - # When getting azimuth and elevation, we need to consider sx, sy and sz points - # from the center of earth to the satellite, and we need to point the satellite - # towards the center of earth - # Calculate elevation angles - elevations = np.degrees(np.arcsin(-sz / r)) - # Calculate azimuth angles - azimuths = np.degrees(np.arctan2(-sy, -sx)) - - # Append satellite positions and angles to global lists - all_positions['lat'].extend(pos_vec['lat']) # Latitudes - all_positions['lon'].extend(pos_vec['lon']) # Longitudes - all_positions['sx'].extend(sx) # X-coordinates - all_positions['sy'].extend(sy) # Y-coordinates - all_positions['sz'].extend(sz) # Z-coordinates - all_positions["R"].extend(r) - all_positions["alt"].extend(pos_vec['alt']) - all_elevations.extend(elevations) # Elevation angles - all_azimuths.extend(azimuths) # Azimuth angles - - active_sats_mask = np.ones(len(pos_vec['lat']), dtype=bool) - - if "MINIMUM_ELEVATION_FROM_ES" in orbit_params.sat_is_active_if.conditions: - # Calculate satellite visibility from base stations - elev_from_bs = calc_elevation( - coordinate_system.ref_lat, # Latitude of base station - pos_vec['lat'], # Latitude of satellites - coordinate_system.ref_long, # Longitude of base station - pos_vec['lon'], # Longitude of satellites - # Perigee altitude in kilometers - sat_height=pos_vec['alt'] * 1e3, - es_height=coordinate_system.ref_alt, + while num_base_stations == 0: + # Calculate the total number of satellites across all orbits + total_satellites = sum( + orbit.n_planes * + orbit.sats_per_plane for orbit in orbit_params.orbits) + if any([ + not hasattr(orbit_params, attr) + for attr in ["sat_is_active_if", "orbits", "beam_radius", "num_beams", "beam_positioning"] + ]): + raise ValueError( + "Parameter passed to TopologyImtMssDc needs to contain all of the attributes:\n" + '["sat_is_active_if", "orbits", "beam_radius", "num_beams", "beam_positioning"]') + + idx_orbit = np.zeros( + total_satellites, + dtype=int) # Add orbit index array + + # List to store indices of active satellites + active_satellite_idxs = [] + + i = 0 # Iteration counter for ensuring satellite visibility + while len(active_satellite_idxs) == 0: + # Initialize arrays to store satellite positions, angles and + # distance from center of earth + all_positions = { + "R": [], + "lat": [], + "lon": [], + "sx": [], + "sy": [], + "sz": [], + "alt": []} + all_elevations = [] # Store satellite elevations + all_azimuths = [] # Store satellite azimuths + + current_sat_idx = 0 # Index tracker for satellites across all orbits + + # Iterate through each orbit defined in the parameters + for orbit_idx, param in enumerate(orbit_params.orbits): + orbit = OrbitModel( + Nsp=param.sats_per_plane, # Satellites per plane + Np=param.n_planes, # Number of orbital planes + phasing=param.phasing_deg, # Phasing angle in degrees + long_asc=param.long_asc_deg, # Longitude of ascending node in degrees + omega=param.omega_deg, # Argument of perigee in degrees + delta=param.inclination_deg, # Orbital inclination in degrees + hp=param.perigee_alt_km, # Perigee altitude in kilometers + ha=param.apogee_alt_km, # Apogee altitude in kilometers + Mo=param.initial_mean_anomaly, # Initial mean anomaly in degrees + # whether to use only time as random variable + model_time_as_random_variable=param.model_time_as_random_variable, + t_min=param.t_min, + t_max=param.t_max, ) - - # Determine visible satellites based on minimum elevation - # angle - active_sats_mask = active_sats_mask & (elev_from_bs.flatten( - ) >= orbit_params.sat_is_active_if.minimum_elevation_from_es) - - if "MAXIMUM_ELEVATION_FROM_ES" in orbit_params.sat_is_active_if.conditions: - # no need to recalculate if already calculated above - if "MINIMUM_ELEVATION_FROM_ES" not in orbit_params.sat_is_active_if.conditions: + # Generate random positions for satellites in this orbit + pos_vec = orbit.get_orbit_positions_random( + rng=random_number_gen) + + # Determine the number of satellites in this orbit + num_satellites = len(pos_vec["sx"]) + + # Assign orbit index to satellites + idx_orbit[current_sat_idx:current_sat_idx + + num_satellites] = orbit_idx + + # Extract satellite positions and calculate distances + sx, sy, sz = pos_vec['sx'], pos_vec['sy'], pos_vec['sz'] + # Distance from Earth's center + r = np.sqrt(sx**2 + sy**2 + sz**2) + + # When getting azimuth and elevation, we need to consider sx, sy and sz points + # from the center of earth to the satellite, and we need to point the satellite + # towards the center of earth + # Calculate elevation angles + elevations = np.degrees(np.arcsin(-sz / r)) + # Calculate azimuth angles + azimuths = np.degrees(np.arctan2(-sy, -sx)) + + # Append satellite positions and angles to global lists + all_positions['lat'].extend(pos_vec['lat']) # Latitudes + all_positions['lon'].extend(pos_vec['lon']) # Longitudes + all_positions['sx'].extend(sx) # X-coordinates + all_positions['sy'].extend(sy) # Y-coordinates + all_positions['sz'].extend(sz) # Z-coordinates + all_positions["R"].extend(r) + all_positions["alt"].extend(pos_vec['alt']) + all_elevations.extend(elevations) # Elevation angles + all_azimuths.extend(azimuths) # Azimuth angles + + active_sats_mask = np.ones(len(pos_vec['lat']), dtype=bool) + + if "MINIMUM_ELEVATION_FROM_ES" in orbit_params.sat_is_active_if.conditions: # Calculate satellite visibility from base stations elev_from_bs = calc_elevation( coordinate_system.ref_lat, # Latitude of base station @@ -197,122 +181,141 @@ def get_coordinates( es_height=coordinate_system.ref_alt, ) - # Determine visible satellites based on minimum elevation - # angle - active_sats_mask = active_sats_mask & (elev_from_bs.flatten( - ) <= orbit_params.sat_is_active_if.maximum_elevation_from_es) - - if "LAT_LONG_INSIDE_COUNTRY" in orbit_params.sat_is_active_if.conditions: - flat_active_lon = pos_vec["lon"].flatten()[ - active_sats_mask] - flat_active_lat = pos_vec["lat"].flatten()[ - active_sats_mask] - - # create points(lon, lat) to compare to country - sats_points = gpd.points_from_xy( - flat_active_lon, flat_active_lat, crs=EARTH_DEFAULT_CRS) - - # Check if the satellite is inside the country polygon - polygon_mask = np.zeros_like(active_sats_mask) - polygon_mask[active_sats_mask] = sats_points.within( - orbit_params.sat_is_active_if.lat_long_inside_country.filter_polygon) - - active_sats_mask = active_sats_mask & polygon_mask - - visible_sat_idxs = np.arange( - current_sat_idx, current_sat_idx + len(pos_vec['lat']), dtype=int - )[active_sats_mask] - active_satellite_idxs.extend(visible_sat_idxs) - - # Update the index tracker for the next orbit - current_sat_idx += len(sx) - - i += 1 # Increment iteration counter - if i >= MAX_ITER: # Check if maximum iterations reached - raise RuntimeError( - "Maximum iterations reached, and no satellite was selected within the minimum elevation criteria." - ) - # We have the list of visible satellites, now create a Topolgy of this subset and move the coordinate system - # reference. - # Convert X-coordinates to meters - all_space_station_x = np.ravel(np.array(all_positions['sx'])) * 1e3 - # Convert Y-coordinates to meters - all_space_station_y = np.ravel(np.array(all_positions['sy'])) * 1e3 - # Convert Z-coordinates to meters - all_space_station_z = np.ravel(np.array(all_positions['sz'])) * 1e3 - all_elevation = np.ravel(np.array(all_elevations)) # Elevation angles - all_azimuth = np.ravel(np.array(all_azimuths)) # Azimuth angles - all_lat = np.ravel(np.array(all_positions['lat'])) - all_lon = np.ravel(np.array(all_positions['lon'])) - all_sat_altitude = np.ravel(np.array(all_positions['alt'])) * 1e3 - - total_active_satellites = len(active_satellite_idxs) - space_station_x = all_space_station_x[active_satellite_idxs] - space_station_y = all_space_station_y[active_satellite_idxs] - space_station_z = all_space_station_z[active_satellite_idxs] - elevation = all_elevation[active_satellite_idxs] - azimuth = all_azimuth[active_satellite_idxs] - lat = all_lat[active_satellite_idxs] - lon = all_lon[active_satellite_idxs] - sat_altitude = all_sat_altitude[active_satellite_idxs] - sat_altitude = all_sat_altitude[active_satellite_idxs] + # Determine visible satellites based on minimum elevation + # angle + active_sats_mask = active_sats_mask & (elev_from_bs.flatten( + ) >= orbit_params.sat_is_active_if.minimum_elevation_from_es) + + if "MAXIMUM_ELEVATION_FROM_ES" in orbit_params.sat_is_active_if.conditions: + # no need to recalculate if already calculated above + if "MINIMUM_ELEVATION_FROM_ES" not in orbit_params.sat_is_active_if.conditions: + # Calculate satellite visibility from base stations + elev_from_bs = calc_elevation( + coordinate_system.ref_lat, # Latitude of base station + pos_vec['lat'], # Latitude of satellites + coordinate_system.ref_long, # Longitude of base station + pos_vec['lon'], # Longitude of satellites + # Perigee altitude in kilometers + sat_height=pos_vec['alt'] * 1e3, + es_height=coordinate_system.ref_alt, + ) + + # Determine visible satellites based on minimum elevation + # angle + active_sats_mask = active_sats_mask & (elev_from_bs.flatten( + ) <= orbit_params.sat_is_active_if.maximum_elevation_from_es) + + if "LAT_LONG_INSIDE_COUNTRY" in orbit_params.sat_is_active_if.conditions: + flat_active_lon = pos_vec["lon"].flatten()[ + active_sats_mask] + flat_active_lat = pos_vec["lat"].flatten()[ + active_sats_mask] + + # create points(lon, lat) to compare to country + sats_points = gpd.points_from_xy( + flat_active_lon, flat_active_lat, crs=EARTH_DEFAULT_CRS) + + # Check if the satellite is inside the country polygon + polygon_mask = np.zeros_like(active_sats_mask) + polygon_mask[active_sats_mask] = sats_points.within( + orbit_params.sat_is_active_if.lat_long_inside_country.filter_polygon) + + active_sats_mask = active_sats_mask & polygon_mask + + visible_sat_idxs = np.arange( + current_sat_idx, current_sat_idx + len(pos_vec['lat']), dtype=int + )[active_sats_mask] + active_satellite_idxs.extend(visible_sat_idxs) + + # Update the index tracker for the next orbit + current_sat_idx += len(sx) + + i += 1 # Increment iteration counter + if i >= MAX_ITER: # Check if maximum iterations reached + raise RuntimeError( + "Maximum iterations reached, and no satellite was selected within the minimum elevation criteria." + ) + # We have the list of visible satellites, now create a Topolgy of this subset and move the coordinate system + # reference. + # Convert X-coordinates to meters + all_space_station_x = np.ravel(np.array(all_positions['sx'])) * 1e3 + # Convert Y-coordinates to meters + all_space_station_y = np.ravel(np.array(all_positions['sy'])) * 1e3 + # Convert Z-coordinates to meters + all_space_station_z = np.ravel(np.array(all_positions['sz'])) * 1e3 + all_elevation = np.ravel(np.array(all_elevations)) # Elevation angles + all_azimuth = np.ravel(np.array(all_azimuths)) # Azimuth angles + all_lat = np.ravel(np.array(all_positions['lat'])) + all_lon = np.ravel(np.array(all_positions['lon'])) + all_sat_altitude = np.ravel(np.array(all_positions['alt'])) * 1e3 + + total_active_satellites = len(active_satellite_idxs) + space_station_x = all_space_station_x[active_satellite_idxs] + space_station_y = all_space_station_y[active_satellite_idxs] + space_station_z = all_space_station_z[active_satellite_idxs] + elevation = all_elevation[active_satellite_idxs] + azimuth = all_azimuth[active_satellite_idxs] + lat = all_lat[active_satellite_idxs] + lon = all_lon[active_satellite_idxs] + sat_altitude = all_sat_altitude[active_satellite_idxs] + sat_altitude = all_sat_altitude[active_satellite_idxs] + + # Convert the ECEF coordinates to the transformed cartesian coordinates and set the Space Station positions + # used to generetate the IMT Base Stations + space_station_x, space_station_y, space_station_z = \ + coordinate_system.ecef2enu(space_station_x, space_station_y, space_station_z) + + # Rotate the azimuth and elevation angles off the center beam the new + # transformed cartesian coordinates + r = 1 + # transform pointing vectors, without considering geodesical earth + # coord system + pointing_vec_x, pointing_vec_y, pointing_vec_z = polar_to_cartesian( + r, all_azimuth, all_elevation) + pointing_vec_x, pointing_vec_y, pointing_vec_z = \ + coordinate_system.ecef2enu( + pointing_vec_x, pointing_vec_y, pointing_vec_z, translate=0) + _, all_azimuth, all_elevation = cartesian_to_polar( + pointing_vec_x, pointing_vec_y, pointing_vec_z) - # Convert the ECEF coordinates to the transformed cartesian coordinates and set the Space Station positions - # used to generetate the IMT Base Stations - space_station_x, space_station_y, space_station_z = \ - coordinate_system.ecef2enu(space_station_x, space_station_y, space_station_z) - - # Rotate the azimuth and elevation angles off the center beam the new - # transformed cartesian coordinates - r = 1 - # transform pointing vectors, without considering geodesical earth - # coord system - pointing_vec_x, pointing_vec_y, pointing_vec_z = polar_to_cartesian( - r, all_azimuth, all_elevation) - pointing_vec_x, pointing_vec_y, pointing_vec_z = \ - coordinate_system.ecef2enu( - pointing_vec_x, pointing_vec_y, pointing_vec_z, translate=0) - _, all_azimuth, all_elevation = cartesian_to_polar( - pointing_vec_x, pointing_vec_y, pointing_vec_z) - - beams_elev, beams_azim, sx, sy = TopologyImtMssDc.get_satellite_pointing( - random_number_gen, - coordinate_system, - orbit_params, - total_active_satellites, - all_space_station_x, all_space_station_y, all_space_station_z, - all_azimuth, - all_elevation, - all_lat, all_lon, all_sat_altitude, - active_satellite_idxs - ) + beams_elev, beams_azim, sx, sy = TopologyImtMssDc.get_satellite_pointing( + random_number_gen, + coordinate_system, + orbit_params, + total_active_satellites, + all_space_station_x, all_space_station_y, all_space_station_z, + all_azimuth, + all_elevation, + all_lat, all_lon, all_sat_altitude, + active_satellite_idxs + ) - # In SHARC each sector is treated as a separate base station, so we need to repeat the satellite positions - # for each sector. - sat_ocurr = [len(x) for x in beams_elev] - - elevation = np.array( - functools.reduce( - lambda x, - y: list(x) + - list(y), - beams_elev)) - azimuth = np.array( - functools.reduce( - lambda x, - y: list(x) + - list(y), - beams_azim)) - - space_station_x = np.repeat(space_station_x, sat_ocurr) - space_station_y = np.repeat(space_station_y, sat_ocurr) - space_station_z = np.repeat(space_station_z, sat_ocurr) - - num_base_stations = np.sum(sat_ocurr) - lat = np.repeat(lat, sat_ocurr) - lon = np.repeat(lon, sat_ocurr) - - altitudes = np.repeat(sat_altitude, sat_ocurr) + # In SHARC each sector is treated as a separate base station, so we need to repeat the satellite positions + # for each sector. + sat_ocurr = [len(x) for x in beams_elev] + + elevation = np.array( + functools.reduce( + lambda x, + y: list(x) + + list(y), + beams_elev)) + azimuth = np.array( + functools.reduce( + lambda x, + y: list(x) + + list(y), + beams_azim)) + + space_station_x = np.repeat(space_station_x, sat_ocurr) + space_station_y = np.repeat(space_station_y, sat_ocurr) + space_station_z = np.repeat(space_station_z, sat_ocurr) + + num_base_stations = np.sum(sat_ocurr) + lat = np.repeat(lat, sat_ocurr) + lon = np.repeat(lon, sat_ocurr) + + altitudes = np.repeat(sat_altitude, sat_ocurr) assert (space_station_x.shape == (num_base_stations,)) assert (space_station_y.shape == (num_base_stations,)) From 9cd3acd8ef11e97b754fe663a3c61a4817f67ec7 Mon Sep 17 00:00:00 2001 From: Bruno Faria Date: Mon, 10 Nov 2025 17:29:46 -0300 Subject: [PATCH 42/77] update(parameters): Added z and Lf as parameters for ITU-R S.1528 Rec 1.2 antenna pattern. --- sharc/antenna/antenna_s1528.py | 41 +++++++++++-------- .../antenna/parameters_antenna_s1528.py | 8 ++++ tests/parameters/parameters_for_testing.yaml | 3 ++ tests/parameters/test_parameters.py | 19 +++++++++ tests/test_antenna_s1528.py | 2 + 5 files changed, 57 insertions(+), 16 deletions(-) diff --git a/sharc/antenna/antenna_s1528.py b/sharc/antenna/antenna_s1528.py index b8ee90764..de9a966ba 100644 --- a/sharc/antenna/antenna_s1528.py +++ b/sharc/antenna/antenna_s1528.py @@ -175,12 +175,18 @@ def __init__(self, param: ParametersAntennaS1528): # the system design self.l_s = param.antenna_l_s + # far-out side-lobe level [dBi] + if param.far_out_side_lobe is None: + self.l_f = 0 + else: + self.l_f = param.far_out_side_lobe + # for elliptical antennas, this is the ratio major axis/minor axis # we assume circular antennas, so z = 1 - self.z = 1 - - # far-out side-lobe level [dBi] - self.l_f = 0 + if param.major_minor_axis_ratio is None: + self.z = 1 + else: + self.z = param.major_minor_axis_ratio # back-lobe level self.l_b = np.maximum( @@ -190,19 +196,22 @@ def __init__(self, param: ParametersAntennaS1528): # one-half the 3 dB beamwidth in the plane of interest self.psi_b = param.antenna_3_dB_bw / 2 - if self.l_s == -15: - self.a = 2.58 * math.sqrt(1 - 1.4 * math.log10(self.z)) - elif self.l_s == -20: - self.a = 2.58 * math.sqrt(1 - 1.0 * math.log10(self.z)) - elif self.l_s == -25: - self.a = 2.58 * math.sqrt(1 - 0.6 * math.log10(self.z)) - elif self.l_s == -30: - self.a = 2.58 * math.sqrt(1 - 0.4 * math.log10(self.z)) + if math.isclose(self.z, 1.0): + self.a = 2.58 else: - sys.stderr.write( - "ERROR\nInvalid AntennaS1528 L_s parameter: " + str(self.l_s), - ) - sys.exit(1) + if self.l_s == -15: + self.a = 2.58 * math.sqrt(1 - 1.4 * math.log10(self.z)) + elif self.l_s == -20: + self.a = 2.58 * math.sqrt(1 - 1.0 * math.log10(self.z)) + elif self.l_s == -25: + self.a = 2.58 * math.sqrt(1 - 0.6 * math.log10(self.z)) + elif self.l_s == -30: + self.a = 2.58 * math.sqrt(1 - 0.4 * math.log10(self.z)) + else: + sys.stderr.write( + f"ERROR\nInvalid AntennaS1528 L_s parameter {self.l_s} for z={self.z}" + ) + sys.exit(1) self.b = 6.32 self.alpha = 1.5 diff --git a/sharc/parameters/antenna/parameters_antenna_s1528.py b/sharc/parameters/antenna/parameters_antenna_s1528.py index 4cd5ea35a..ed2f204f1 100644 --- a/sharc/parameters/antenna/parameters_antenna_s1528.py +++ b/sharc/parameters/antenna/parameters_antenna_s1528.py @@ -37,6 +37,14 @@ class ParametersAntennaS1528(ParametersBase): l_r: float = None l_t: float = None + # Recommends 1.2 only + # For elliptical antennas, this is the ratio major axis/minor axis + # we assume circular antennas, so z = 1 + major_minor_axis_ratio: float = None + + # Far-out side-lobe level + far_out_side_lobe: float = None + def load_parameters_from_file(self, config_file: str): """Load the parameters from file an run a sanity check. diff --git a/tests/parameters/parameters_for_testing.yaml b/tests/parameters/parameters_for_testing.yaml index 208116b63..86631172b 100644 --- a/tests/parameters/parameters_for_testing.yaml +++ b/tests/parameters/parameters_for_testing.yaml @@ -999,6 +999,9 @@ single_space_station: # Antenna diameter [m] # if no diameter is passed, a diameter will be assumed according to document diameter: 2.12 + itu_r_s_1528: + major_minor_axis_ratio: 1.0 + far_out_side_lobe: -25 ########################################################################### # Selected channel model channel_model: "P619" diff --git a/tests/parameters/test_parameters.py b/tests/parameters/test_parameters.py index f87b5eee4..b959acfb9 100644 --- a/tests/parameters/test_parameters.py +++ b/tests/parameters/test_parameters.py @@ -875,6 +875,25 @@ def test_parameters_single_space_station(self): self.parameters.single_space_station.antenna.gain, ) + self.assertEqual( + self.parameters.single_space_station.antenna.itu_r_s_1528.antenna_gain, + self.parameters.single_space_station.antenna.gain, + ) + self.assertEqual( + self.parameters.single_space_station.antenna.itu_r_s_1528.major_minor_axis_ratio, + 1.0 + ) + + self.assertEqual( + self.parameters.single_space_station.antenna.itu_r_s_1528.major_minor_axis_ratio, + 1.0 + ) + + self.assertEqual( + self.parameters.single_space_station.antenna.itu_r_s_1528.far_out_side_lobe, + -25 + ) + self.assertEqual( self.parameters.single_space_station.param_p619.earth_station_alt_m, self.parameters.single_space_station.geometry.es_altitude, diff --git a/tests/test_antenna_s1528.py b/tests/test_antenna_s1528.py index 0d2aae6cf..b025506b5 100644 --- a/tests/test_antenna_s1528.py +++ b/tests/test_antenna_s1528.py @@ -23,6 +23,8 @@ def setUp(self): param.antenna_gain = 39 param.antenna_pattern = "ITU-R S.1528-0" param.antenna_3_dB_bw = 2 + param.major_minor_axis_ratio = 1 + param.far_out_side_lobe = 0 param.antenna_l_s = -20 self.antenna20 = AntennaS1528(param) From 79472aebbef7dd298b77570b1dbea9c016ec7508 Mon Sep 17 00:00:00 2001 From: artistrea Date: Wed, 26 Nov 2025 09:33:34 -0300 Subject: [PATCH 43/77] feat: propagation model loss calculation only on specific paths --- sharc/propagation/propagation.py | 11 +- sharc/propagation/propagation_abg.py | 28 +- .../propagation_building_entry_loss.py | 9 + .../propagation/propagation_clear_air_452.py | 71 ++-- sharc/propagation/propagation_clutter_loss.py | 10 + sharc/propagation/propagation_free_space.py | 21 +- sharc/propagation/propagation_hdfss.py | 40 +- .../propagation_hdfss_building_side.py | 6 + .../propagation/propagation_hdfss_roof_top.py | 75 ++-- sharc/propagation/propagation_indoor.py | 23 +- sharc/propagation/propagation_inh_office.py | 6 + sharc/propagation/propagation_p1411.py | 6 + sharc/propagation/propagation_p619.py | 89 ++-- sharc/propagation/propagation_path.py | 381 +++++++++++++++++ sharc/propagation/propagation_sat_simple.py | 76 ++-- sharc/propagation/propagation_ter_simple.py | 22 +- sharc/propagation/propagation_tvro.py | 56 +-- sharc/propagation/propagation_uma.py | 55 ++- sharc/propagation/propagation_umi.py | 42 +- sharc/propagation/scintillation.py | 17 +- sharc/simulation.py | 82 ++-- sharc/simulation_downlink.py | 17 + sharc/simulation_uplink.py | 17 + sharc/station_manager.py | 1 + tests/test_adjacent_channel.py | 15 +- tests/test_propagation_abg.py | 41 +- tests/test_propagation_hdfss_roof_top.py | 39 +- tests/test_propagation_path.py | 399 ++++++++++++++++++ tests/test_propagation_uma.py | 54 +-- tests/test_propagation_umi.py | 58 +-- tests/test_simulation_downlink.py | 23 + tests/test_simulation_downlink_haps.py | 7 + tests/test_simulation_downlink_tvro.py | 7 + tests/test_simulation_uplink.py | 23 + 34 files changed, 1389 insertions(+), 438 deletions(-) create mode 100644 sharc/propagation/propagation_path.py create mode 100644 tests/test_propagation_path.py diff --git a/sharc/propagation/propagation.py b/sharc/propagation/propagation.py index 18db95e10..50285c2aa 100644 --- a/sharc/propagation/propagation.py +++ b/sharc/propagation/propagation.py @@ -7,8 +7,11 @@ from abc import ABC, abstractmethod import numpy as np +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from sharc.propagation.propagation_path import PropagationPath -from sharc.station_manager import StationManager from sharc.parameters.parameters import Parameters @@ -22,14 +25,14 @@ def __init__(self, random_number_gen: np.random.RandomState): # Inicates whether this propagation model is for links between earth # and space self.is_earth_space_model = False + self.needs_antenna_gains = False @abstractmethod - def get_loss( + def get_path_loss( self, params: Parameters, frequency: float, - station_a: StationManager, - station_b: StationManager, + path: "PropagationPath", station_a_gains=None, station_b_gains=None, ) -> np.array: diff --git a/sharc/propagation/propagation_abg.py b/sharc/propagation/propagation_abg.py index e2bd9b17e..a2e5ab1fc 100644 --- a/sharc/propagation/propagation_abg.py +++ b/sharc/propagation/propagation_abg.py @@ -8,6 +8,8 @@ import numpy as np from multipledispatch import dispatch +from sharc.propagation.propagation_path import PropagationPath + from sharc.propagation.propagation import Propagation from sharc.station_manager import StationManager from sharc.parameters.parameters import Parameters @@ -35,14 +37,11 @@ def __init__( self.building_loss = 20 self.shadowing_sigma_dB = 6.5 - @dispatch(Parameters, float, StationManager, - StationManager, np.ndarray, np.ndarray) - def get_loss( + def get_path_loss( self, params: Parameters, frequency: float, - station_a: StationManager, - station_b: StationManager, + path: "PropagationPath", station_a_gains=None, station_b_gains=None, ) -> np.array: @@ -74,19 +73,22 @@ def get_loss( if wrap_around_enabled: _, distances_3d, _, _ = \ - station_a.geom.get_global_dist_angles_wrap_around(station_b.geom) + path.sta_a.geom.get_global_dist_angles_wrap_around(path.sta_b.geom) else: - distances_3d = station_a.geom.get_3d_distance_to(station_b.geom) + distances_3d = path.sta_a.geom.get_3d_distance_to(path.sta_b.geom) - indoor_stations = station_a.indoor + indoor_stations = path.sta_a.indoor - loss = \ + mskd_indoor = path.sta_a_to_masked(indoor_stations) + mskd_dist3d = path.mtx_to_masked(distances_3d) + mskd_loss = \ self.get_loss( - distances_3d, - frequency * np.ones(distances_3d.shape), - indoor_stations, + mskd_dist3d, + frequency * np.ones(mskd_dist3d.shape), + mskd_indoor, params.imt.shadowing, ) + loss = path.from_masked_mtx(mskd_loss) return loss @@ -124,7 +126,7 @@ def get_loss( shadowing = 0 building_loss = self.building_loss * \ - np.tile(indoor_stations, (distance.shape[1], 1)).transpose() + indoor_stations loss = 10 * self.alpha * np.log10(distance) + self.beta + 10 * \ self.gamma * np.log10(frequency * 1e-3) + shadowing + building_loss diff --git a/sharc/propagation/propagation_building_entry_loss.py b/sharc/propagation/propagation_building_entry_loss.py index 02a78ff27..4bff257ce 100644 --- a/sharc/propagation/propagation_building_entry_loss.py +++ b/sharc/propagation/propagation_building_entry_loss.py @@ -15,6 +15,15 @@ class PropagationBuildingEntryLoss(Propagation): """ Implements the building entry loss according to ITU-R P.2109-0 (Prediction of Building Entry Loss) """ + def get_path_loss( + self, + params, + frequency: float, + path, + station_a_gains=None, + station_b_gains=None, + ) -> np.array: + raise NotImplementedError() def get_loss( self, frequency_MHz, elevation, prob="RANDOM", diff --git a/sharc/propagation/propagation_clear_air_452.py b/sharc/propagation/propagation_clear_air_452.py index 4a95d83ec..94dd8be8f 100644 --- a/sharc/propagation/propagation_clear_air_452.py +++ b/sharc/propagation/propagation_clear_air_452.py @@ -5,7 +5,7 @@ from multipledispatch import dispatch from sharc.propagation.propagation import Propagation -from sharc.station_manager import StationManager +from sharc.propagation.propagation_path import PropagationPath from sharc.parameters.parameters import Parameters from sharc.parameters.parameters_p452 import ParametersP452 from sharc.propagation.clear_air_452_aux import p676_ga @@ -24,6 +24,7 @@ def __init__( model_params: ParametersP452): """Initialize PropagationClearAir with a random number generator and model parameters.""" super().__init__(random_number_gen) + self.needs_antenna_gains = True self.clutter = PropagationClutterLoss(random_number_gen) self.building_entry = PropagationBuildingEntryLoss( @@ -461,20 +462,6 @@ def pl_los(d, f, p, b0, w, T, press, dlt, dlr): # pylint: disable=function-redefined # pylint: disable=arguments-renamed - def __init__( - self, - random_number_gen: np.random.RandomState, - model_params: ParametersP452): - """Initialize PropagationClearAir with a random number generator and model parameters.""" - super().__init__(random_number_gen) - - self.clutter = PropagationClutterLoss(random_number_gen) - self.building_entry = PropagationBuildingEntryLoss( - self.random_number_gen, - ) - self.building_loss = 20 - self.model_params = model_params - @staticmethod def closs_corr(f, d, h, zone, htg, hrg, ha_t, ha_r, dk_t, dk_r): """Apply clutter loss correction according to ITU-R P.452-16.""" @@ -1508,16 +1495,13 @@ def dl_p(d, h, hts, hrs, hstd, hsrd, f, omega, p, b0, DN): return Ldp, Ld50 - @dispatch(Parameters, float, StationManager, - StationManager, np.ndarray, np.ndarray) - def get_loss( + def get_path_loss( self, params: Parameters, frequency: float, - station_a: StationManager, - station_b: StationManager, - station_a_gains=None, - station_b_gains=None, + path: PropagationPath, + station_a_gains, + station_b_gains, ) -> np.array: """Wrapper function for the get_loss method to fit the Propagation ABC class interface Calculates the loss between station_a and station_b @@ -1543,31 +1527,36 @@ def get_loss( Return an array station_a.num_stations x station_b.num_stations with the path loss between each station """ - distance = station_a.geom.get_3d_distance_to( - station_b.geom, - ) * (1e-3) # P.452 expects Kms + masked_distance = path.mtx_to_masked(path.sta_a.geom.get_3d_distance_to( + path.sta_b.geom, + )) * (1e-3) # P.452 expects Kms frequency_array = frequency * \ - np.ones(distance.shape) * (1e-3) # P.452 expects GHz - indoor_stations = np.tile( - station_b.indoor, (station_a.num_stations, 1), + np.ones(masked_distance.shape) * (1e-3) # P.452 expects GHz + # FIXME: Why always station_b? + masked_indoor_stations = path.sta_b_to_masked(path.sta_b.indoor) + # FIXME: Why station b -> station a? + masked_elevation = path.mtx_to_masked( + path.sta_b.geom.get_local_elevation(path.sta_a.geom).T ) - elevation = station_b.geom.get_local_elevation(station_a.geom) + if params.imt.interfered_with: - tx_gain = station_a_gains - rx_gain = station_b_gains + tx_gain = path.mtx_to_masked(station_a_gains) + rx_gain = path.mtx_to_masked(np.swapaxes(station_b_gains, 0, 1)) else: - tx_gain = station_b_gains - rx_gain = station_a_gains + tx_gain = path.mtx_to_masked(np.swapaxes(station_b_gains, 0, 1)) + rx_gain = path.mtx_to_masked(station_a_gains) - return self.get_loss( - distance, + masked_loss = self.get_loss( + masked_distance, frequency_array, - indoor_stations, - elevation, + masked_indoor_stations, + masked_elevation, tx_gain, rx_gain, ) + return path.from_masked_mtx(masked_loss) + # pylint: disable=arguments-differ @dispatch(np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray) @@ -1629,7 +1618,7 @@ def get_loss( num_dists = distance.size d = np.empty([num_dists, profile_length]) for ii in range(num_dists): - d[ii, :] = np.linspace(0, distance[0][ii], profile_length) + d[ii, :] = np.linspace(0, np.ravel(distance)[ii], profile_length) h = np.zeros(d.shape) @@ -1669,7 +1658,7 @@ def get_loss( # only if not isempty ha_t and ha_r # [dc, hc, zonec, htgc, hrgc, Aht, Ahr] = self.closs_corr(f, d, h, zone, Hte, Hre, ha_t, ha_r, dk_t, dk_r) - Lb = np.empty([1, num_dists]) + Lb = np.empty([num_dists]) # Effective Earth curvature Ce(km ^ -1) Ce = 1 / ae @@ -1813,9 +1802,9 @@ def get_loss( ) + Aht + Ahr if (self.model_params.polarization).lower() == "horizontal": - Lb[0, ii] = Lb_pol[0] + Lb[ii] = Lb_pol[0] elif (self.model_params.polarization).lower() == "vertical": - Lb[0, ii] = Lb_pol[1] + Lb[ii] = Lb_pol[1] else: error_message = "invalid polarization" raise ValueError(error_message) diff --git a/sharc/propagation/propagation_clutter_loss.py b/sharc/propagation/propagation_clutter_loss.py index 43c9b4631..de6ebb2a4 100644 --- a/sharc/propagation/propagation_clutter_loss.py +++ b/sharc/propagation/propagation_clutter_loss.py @@ -50,6 +50,16 @@ class PropagationClutterLoss(Propagation): estimate clutter loss. """ + def get_path_loss( + self, + params, + frequency: float, + path, + station_a_gains=None, + station_b_gains=None, + ) -> np.array: + raise NotImplementedError() + def get_loss(self, *args, **kwargs) -> np.array: """ Calculates clutter loss according to Recommendation P.2108-0 diff --git a/sharc/propagation/propagation_free_space.py b/sharc/propagation/propagation_free_space.py index 459e8cf9b..6ee536eb4 100644 --- a/sharc/propagation/propagation_free_space.py +++ b/sharc/propagation/propagation_free_space.py @@ -6,9 +6,12 @@ """ import numpy as np from multipledispatch import dispatch +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from sharc.propagation.propagation_path import PropagationPath from sharc.propagation.propagation import Propagation -from sharc.station_manager import StationManager from sharc.parameters.parameters import Parameters @@ -19,14 +22,11 @@ class PropagationFreeSpace(Propagation): Frequency in MHz and distance are in meters """ - @dispatch(Parameters, float, StationManager, - StationManager, np.ndarray, np.ndarray) - def get_loss( + def get_path_loss( self, params: Parameters, frequency: float, - station_a: StationManager, - station_b: StationManager, + path: "PropagationPath", station_a_gains=None, station_b_gains=None, ) -> np.array: @@ -47,9 +47,12 @@ def get_loss( Return an array station_a.num_stations x station_b.num_stations with the path loss between each station """ - distance_3d = station_a.geom.get_3d_distance_to(station_b.geom) - loss = self.get_free_space_loss( - frequency=frequency, distance=distance_3d) + distance_3d = path.sta_a.geom.get_3d_distance_to(path.sta_b.geom) + + mskd_dist3d = path.mtx_to_masked(distance_3d) + mskd_loss = self.get_free_space_loss( + frequency=frequency, distance=mskd_dist3d) + loss = path.from_masked_mtx(mskd_loss) return loss diff --git a/sharc/propagation/propagation_hdfss.py b/sharc/propagation/propagation_hdfss.py index 68cdd3bd0..b8d3fed34 100644 --- a/sharc/propagation/propagation_hdfss.py +++ b/sharc/propagation/propagation_hdfss.py @@ -14,6 +14,7 @@ from sharc.parameters.parameters import Parameters from sharc.propagation.propagation_hdfss_roof_top import PropagationHDFSSRoofTop from sharc.propagation.propagation_hdfss_building_side import PropagationHDFSSBuildingSide +from sharc.propagation.propagation_path import PropagationPath class PropagationHDFSS(Propagation): @@ -38,12 +39,11 @@ def __init__( ) sys.exit(1) - def get_loss( + def get_path_loss( self, params: Parameters, frequency: float, - station_a: StationManager, - station_b: StationManager, + path: PropagationPath, station_a_gains=None, station_b_gains=None, ) -> np.array: @@ -71,26 +71,34 @@ def get_loss( Return an array station_a.num_stations x station_b.num_stations with the path loss between each station """ - distance = station_a.geom.get_3d_distance_to(station_b.geom) # P.452 expects Kms - frequency_array = frequency * \ - np.ones(distance.shape) # P.452 expects GHz - elevation = station_b.geom.get_local_elevation(station_a.geom) + distance = path.sta_a.geom.get_3d_distance_to(path.sta_b.geom) # P.452 expects Kms + distance = path.mtx_to_masked(distance) - if station_a.geom.uses_local_coords or station_b.geom.uses_local_coords: + elevation = path.sta_b.geom.get_local_elevation(path.sta_a.geom) + elevation = path.mtx_to_masked(elevation.T) + + frequency_array = frequency * np.ones(distance.shape) # P.452 expects GHz + + if path.sta_a.geom.uses_local_coords or path.sta_b.geom.uses_local_coords: raise NotImplementedError( "HDFSS currently assumes stations z == height. " "If stations has local coords != global coords, this probably isn't true" ) - return self.propagation.get_loss( + loss, build_loss, diff_loss = self.propagation.get_loss( distance_3D=distance, elevation=elevation, - imt_sta_type=station_b.station_type, + imt_sta_type=path.sta_b.station_type, frequency=frequency_array, - imt_x=station_b.geom.x_global, - imt_y=station_b.geom.y_global, - imt_z=station_b.geom.z_global, - es_x=station_a.geom.x_global, - es_y=station_a.geom.y_global, - es_z=station_a.geom.z_global, + imt_x=path.sta_b_to_masked(path.sta_b.geom.x_global), + imt_y=path.sta_b_to_masked(path.sta_b.geom.y_global), + imt_z=path.sta_b_to_masked(path.sta_b.geom.z_global), + es_x=path.sta_a_to_masked(path.sta_a.geom.x_global), + es_y=path.sta_a_to_masked(path.sta_a.geom.y_global), + es_z=path.sta_a_to_masked(path.sta_a.geom.z_global), + ) + return ( + path.from_masked_mtx(loss), + path.from_masked_mtx(build_loss), + path.from_masked_mtx(diff_loss) ) diff --git a/sharc/propagation/propagation_hdfss_building_side.py b/sharc/propagation/propagation_hdfss_building_side.py index 0c9909d09..fe732b541 100644 --- a/sharc/propagation/propagation_hdfss_building_side.py +++ b/sharc/propagation/propagation_hdfss_building_side.py @@ -49,6 +49,12 @@ def __init__( self.random_number_gen, ) + def get_path_loss(self, *args, **kwargs): + """This class isn't supposed to be directly called in the simulation + loop, so this methods doesn't need to be implemented + """ + raise NotImplementedError() + def get_loss(self, *args, **kwargs) -> np.array: """ diff --git a/sharc/propagation/propagation_hdfss_roof_top.py b/sharc/propagation/propagation_hdfss_roof_top.py index 5d62154c1..d9c94d8bc 100644 --- a/sharc/propagation/propagation_hdfss_roof_top.py +++ b/sharc/propagation/propagation_hdfss_roof_top.py @@ -58,6 +58,12 @@ def __init__( self.SPEED_OF_LIGHT = SPEED_OF_LIGHT + def get_path_loss(self, *args, **kwargs): + """This class isn't supposed to be directly called in the simulation + loop, so this methods doesn't need to be implemented + """ + raise NotImplementedError() + def get_loss(self, *args, **kwargs) -> np.array: """ Calculates path loss for given distances and frequencies @@ -123,51 +129,46 @@ def get_loss(self, *args, **kwargs) -> np.array: # Define indexes same_build_idx = np.where(same_build)[0] - fspl_idx = np.where(fspl_bool)[1] - fspl_to_los_idx = np.where(fspl_to_los_bool)[1] - los_idx = np.where(los_bool)[1] - los_to_nlos_idx = np.where(los_to_nlos_bool)[1] - nlos_idx = np.where(nlos_bool)[1] + + fspl_idx = np.where(fspl_bool)[0] + fspl_to_los_idx = np.where(fspl_to_los_bool)[0] + los_idx = np.where(los_bool)[0] + los_to_nlos_idx = np.where(los_to_nlos_bool)[0] + nlos_idx = np.where(nlos_bool)[0] # Path loss loss = np.zeros_like(d) if not self.param.same_building_enabled: - loss[:, same_build_idx] += self.HIGH_LOSS + loss[same_build_idx] += self.HIGH_LOSS else: - loss[:, same_build_idx] += self.get_same_build_loss( + loss[same_build_idx] += self.get_same_build_loss( imt_z[same_build_idx], - es_z, + es_z[same_build_idx], ) - loss[:, fspl_idx] += self.propagation_fspl.get_free_space_loss( - distance=d[:, fspl_idx], frequency=f[:, fspl_idx], + loss[fspl_idx] += self.propagation_fspl.get_free_space_loss( + distance=d[fspl_idx], frequency=f[fspl_idx], ) - loss[:, fspl_to_los_idx] += self.interpolate_fspl_to_los( - d[:, fspl_to_los_idx], - f[:, fspl_to_los_idx], + loss[fspl_to_los_idx] += self.interpolate_fspl_to_los( + d[fspl_to_los_idx], + f[fspl_to_los_idx], self.param.shadow_enabled, ) - loss[:, los_idx] += self.propagation_p1411.get_loss( - distance_3D=d[:, los_idx], - frequency=f[ - :, - los_idx, - ], + loss[los_idx] += self.propagation_p1411.get_loss( + distance_3D=d[los_idx], + frequency=f[los_idx], los=True, shadow=self.param.shadow_enabled, ) - loss[:, los_to_nlos_idx] += self.interpolate_los_to_nlos( - d[:, los_to_nlos_idx], - f[:, los_to_nlos_idx], + loss[los_to_nlos_idx] += self.interpolate_los_to_nlos( + d[los_to_nlos_idx], + f[los_to_nlos_idx], self.param.shadow_enabled, ) - loss[:, nlos_idx] += self.propagation_p1411.get_loss( - distance_3D=d[:, nlos_idx], - frequency=f[ - :, - nlos_idx, - ], + loss[nlos_idx] += self.propagation_p1411.get_loss( + distance_3D=d[nlos_idx], + frequency=f[nlos_idx], los=False, shadow=self.param.shadow_enabled, ) @@ -175,10 +176,10 @@ def get_loss(self, *args, **kwargs) -> np.array: # Building entry loss if self.param.building_loss_enabled: build_loss = np.zeros_like(loss) - build_loss[0, not_same_build] = self.get_building_loss( + build_loss[not_same_build] = self.get_building_loss( imt_sta_type, - f[:, not_same_build], - elevation[:, not_same_build], + f[not_same_build], + elevation[not_same_build], ) else: build_loss = 0.0 @@ -191,11 +192,11 @@ def get_loss(self, *args, **kwargs) -> np.array: es_x, es_y, es_z, ) diff_loss = np.zeros_like(loss) - diff_loss[0, not_same_build] = self.get_diffraction_loss( + diff_loss[not_same_build] = self.get_diffraction_loss( h[not_same_build], d1[not_same_build], d2[not_same_build], - f[:, not_same_build], + f[not_same_build], ) # Compute final loss @@ -339,10 +340,10 @@ def is_same_building(self, imt_x, imt_y, es_x, es_y): Boolean array indicating if each IMT is in the same building as the earth station. """ - building_x_range = es_x + (1 + self.b_tol) * \ - np.array([-self.b_w / 2, +self.b_w / 2]) - building_y_range = es_y + (1 + self.b_tol) * \ - np.array([-self.b_d / 2, +self.b_d / 2]) + building_x_range = es_x[np.newaxis, :] + (1 + self.b_tol) * \ + np.array([-self.b_w / 2, +self.b_w / 2])[:, np.newaxis] + building_y_range = es_y[np.newaxis, :] + (1 + self.b_tol) * \ + np.array([-self.b_d / 2, +self.b_d / 2])[:, np.newaxis] is_in_x = np.logical_and( imt_x >= building_x_range[0], imt_x <= building_x_range[1], diff --git a/sharc/propagation/propagation_indoor.py b/sharc/propagation/propagation_indoor.py index 8288734cf..3067902f6 100644 --- a/sharc/propagation/propagation_indoor.py +++ b/sharc/propagation/propagation_indoor.py @@ -15,6 +15,7 @@ from sharc.propagation.propagation_free_space import PropagationFreeSpace from sharc.propagation.propagation_inh_office import PropagationInhOffice from sharc.propagation.propagation_building_entry_loss import PropagationBuildingEntryLoss +from sharc.propagation.propagation_path import PropagationPath class PropagationIndoor(Propagation): @@ -55,14 +56,11 @@ def __init__( self.bs_per_building = param.num_cells self.ue_per_building = ue_per_cell * param.num_cells - @dispatch(Parameters, float, StationManager, - StationManager, np.ndarray, np.ndarray) - def get_loss( + def get_path_loss( self, params: Parameters, frequency: float, - station_a: StationManager, - station_b: StationManager, + path: "PropagationPath", station_a_gains=None, station_b_gains=None, ) -> np.array: @@ -100,17 +98,18 @@ def get_loss( if wrap_around_enabled: bs_to_ue_dist_2d, bs_to_ue_dist_3d, _, _ = \ - station_b.geom.get_global_dist_angles_wrap_around(station_a.geom) + path.sta_b.geom.get_global_dist_angles_wrap_around(path.sta_a.geom) else: - bs_to_ue_dist_2d = station_b.geom.get_local_distance_to(station_a.geom) - bs_to_ue_dist_3d = station_b.geom.get_3d_distance_to(station_a.geom) + bs_to_ue_dist_2d = path.sta_b.geom.get_local_distance_to(path.sta_a.geom) + bs_to_ue_dist_3d = path.sta_b.geom.get_3d_distance_to(path.sta_a.geom) frequency_array = frequency * np.ones(bs_to_ue_dist_2d.shape) indoor_stations = np.tile( - station_a.indoor, (station_b.num_stations, 1), + path.sta_a.indoor, (path.sta_b.num_stations, 1), ) - elevation = np.transpose(station_a.geom.get_local_elevation(station_b.geom)) - + elevation = np.transpose(path.sta_a.geom.get_local_elevation(path.sta_b.geom)) + # NOTE: we're not masking only selected paths since indoor implementation depends + # on being able to use UE axis separated from BS axis return self.get_loss( bs_to_ue_dist_3d, bs_to_ue_dist_2d, @@ -118,7 +117,7 @@ def get_loss( elevation, indoor_stations, params.imt.shadowing, - ) + ).T # pylint: disable=function-redefined # pylint: disable=arguments-renamed diff --git a/sharc/propagation/propagation_inh_office.py b/sharc/propagation/propagation_inh_office.py index 16c56761b..1f270dab6 100644 --- a/sharc/propagation/propagation_inh_office.py +++ b/sharc/propagation/propagation_inh_office.py @@ -18,6 +18,12 @@ class PropagationInhOffice(Propagation): according to 3GPP TR 38.900 v14.2.0. """ + def get_path_loss(self, *args, **kwargs): + """This class isn't supposed to be directly called in the simulation + loop, so this methods doesn't need to be implemented + """ + raise NotImplementedError() + def get_loss(self, *args, **kwargs) -> np.array: """ Calculates path loss for LOS and NLOS cases with respective shadowing diff --git a/sharc/propagation/propagation_p1411.py b/sharc/propagation/propagation_p1411.py index 30b8e1b53..5ddea5c52 100644 --- a/sharc/propagation/propagation_p1411.py +++ b/sharc/propagation/propagation_p1411.py @@ -44,6 +44,12 @@ def __init__( self.nlos_gamma = 2.36 self.nlos_sigma = 7.60 + def get_path_loss(self, *args, **kwargs): + """This class isn't supposed to be directly called in the simulation + loop, so this methods doesn't need to be implemented + """ + raise NotImplementedError() + def get_loss(self, *args, **kwargs) -> np.array: """Calculate the path loss using the ITU-R P.1411 model. diff --git a/sharc/propagation/propagation_p619.py b/sharc/propagation/propagation_p619.py index fa44c76c4..36f3d8b5e 100644 --- a/sharc/propagation/propagation_p619.py +++ b/sharc/propagation/propagation_p619.py @@ -12,7 +12,7 @@ import numpy as np from multipledispatch import dispatch from warnings import warn -from sharc.station_manager import StationManager +from sharc.propagation.propagation_path import PropagationPath from sharc.parameters.parameters import Parameters from sharc.propagation.propagation import Propagation from sharc.propagation.propagation_free_space import PropagationFreeSpace @@ -59,6 +59,7 @@ def __init__( """ super().__init__(random_number_gen) + self.needs_antenna_gains = True self.is_earth_space_model = True self.clutter = PropagationClutterLoss(self.random_number_gen) @@ -310,16 +311,13 @@ def apparent_elevation_angle( return elevation_deg + tau_fs_deg - @dispatch(Parameters, float, StationManager, - StationManager, np.ndarray, np.ndarray) - def get_loss( + def get_path_loss( self, params: Parameters, frequency: float, - station_a: StationManager, - station_b: StationManager, - station_a_gains=None, - station_b_gains=None, + path: PropagationPath, + station_a_gains, + station_b_gains, ) -> np.array: """Wrapper for the get_loss function that satisfies the ABS class interface @@ -340,45 +338,48 @@ def get_loss( Return an array station_a.num_stations x station_b.num_stations with the path loss between each station """ + station_a = path.sta_a + station_b = path.sta_b + distance = station_a.geom.get_3d_distance_to(station_b.geom) - frequency = frequency * np.ones(distance.shape) - indoor_stations = np.tile( - station_b.indoor, (station_a.num_stations, 1), - ) + masked_distance = path.mtx_to_masked(distance) + masked_frequency = frequency * np.ones_like(masked_distance) # Elevation angles seen from the station on Earth. - elevation_angles = {} + masked_elevation_angles = {} if station_a.is_space_station: if station_b.geom.uses_local_coords: raise NotImplementedError( "P619 currently assumes earth station z == height. " "If ES has local coords != global coords, this probably isn't true" ) - earth_station_height = station_b.geom.z_global - elevation_angles["free_space"] = station_b.geom.get_local_elevation(station_a.geom) - earth_station_antenna_gain = station_b_gains - # if (station_b_gains.shape != distance.shape): - # raise ValueError(f"Invalid shape for station_b_gains = {station_b_gains.shape}") - elevation_angles["apparent"] = self.apparent_elevation_angle( - elevation_angles["free_space"], + masked_indoor_stations = path.sta_b_to_masked(station_b.indoor) + masked_earth_station_height = path.sta_b_to_masked(station_b.geom.z_global) + masked_elevation_angles["free_space"] = station_b.geom.get_local_elevation(station_a.geom) + masked_earth_station_antenna_gain = path.mtx_to_masked(np.swapaxes(station_b_gains, 0, 1)) + masked_elevation_angles["apparent"] = self.apparent_elevation_angle( + masked_elevation_angles["free_space"], self.earth_station_alt_m, ) # Transpose it to fit the expected path loss shape - elevation_angles["free_space"] = np.transpose( - elevation_angles["free_space"]) - elevation_angles["apparent"] = np.transpose( - elevation_angles["apparent"]) + masked_elevation_angles["free_space"] = path.mtx_to_masked(np.transpose( + masked_elevation_angles["free_space"])) + masked_elevation_angles["apparent"] = path.mtx_to_masked(np.transpose( + masked_elevation_angles["apparent"])) elif station_b.is_space_station: if station_a.geom.uses_local_coords: raise NotImplementedError( "P619 currently assumes earth station z == height. " "If ES has local coords != global coords, this probably isn't true" ) - earth_station_height = station_a.geom.z_global - elevation_angles["free_space"] = station_a.geom.get_local_elevation(station_b.geom) - earth_station_antenna_gain = station_a_gains - elevation_angles["apparent"] = self.apparent_elevation_angle( - elevation_angles["free_space"], + masked_indoor_stations = path.sta_a_to_masked(station_a.indoor) + masked_earth_station_height = path.sta_a_to_masked(station_a.geom.z_global) + masked_elevation_angles["free_space"] = path.mtx_to_masked( + station_a.geom.get_local_elevation(station_b.geom) + ) + masked_earth_station_antenna_gain = path.mtx_to_masked(station_a_gains) + masked_elevation_angles["apparent"] = self.apparent_elevation_angle( + masked_elevation_angles["free_space"], self.earth_station_alt_m, ) else: @@ -413,17 +414,19 @@ def get_loss( is_earth_to_space_link = False if imt_station.is_space_station else True is_single_entry_interf = False - loss = self.get_loss( - distance, - frequency, - indoor_stations, - elevation_angles, + masked_loss = self.get_loss( + masked_distance, + masked_frequency, + masked_indoor_stations, + masked_elevation_angles, is_earth_to_space_link, - earth_station_antenna_gain, + masked_earth_station_antenna_gain, is_single_entry_interf, - earth_station_height, + masked_earth_station_height, ) + loss = path.from_masked_mtx(masked_loss) + return loss @dispatch(np.ndarray, np.ndarray, np.ndarray, dict, bool, np.ndarray, bool, np.ndarray) @@ -469,6 +472,7 @@ def get_loss( atmospheric_gasses_loss = self._get_atmospheric_gasses_loss( frequency_MHz=freq_set, + # FIXME: mean of apparent elevation???? apparent_elevation=np.mean(elevation["apparent"]), ) beam_spreading_attenuation = self._get_beam_spreading_att( @@ -479,14 +483,25 @@ def get_loss( diffraction_loss = 0 if single_entry: + # if earth station is IMT BS with ue_k beams + elev = elevation["free_space"] + if earth_station_antenna_gain.ndim == 2: + elev = np.repeat( + elevation["free_space"][..., np.newaxis], + earth_station_antenna_gain.shape[1], + -1 + ) tropo_scintillation_loss = self.scintillation.get_tropospheric_attenuation( - elevation=elevation["free_space"], + elevation=elev, antenna_gain_dB=earth_station_antenna_gain, frequency_MHz=freq_set, earth_station_alt_m=self.earth_station_alt_m, earth_station_lat_deg=self.earth_station_lat_deg, season=self.season, ) + if tropo_scintillation_loss.ndim > 1: + # TODO: better fix for this + tropo_scintillation_loss = np.min(tropo_scintillation_loss, -1) loss = free_space_loss + self.depolarization_loss + atmospheric_gasses_loss + \ beam_spreading_attenuation + diffraction_loss + tropo_scintillation_loss diff --git a/sharc/propagation/propagation_path.py b/sharc/propagation/propagation_path.py new file mode 100644 index 000000000..156abf1bd --- /dev/null +++ b/sharc/propagation/propagation_path.py @@ -0,0 +1,381 @@ +from sharc.station_manager import StationManager +import typing +import numpy as np + +from sharc.parameters.parameters import Parameters +from sharc.propagation.propagation import Propagation + + +class PropagationPath(): + # either + # - (sta_a == IMT UE) and (sta_b == IMT BS) + # OR + # - (sta_a == System) and (sta_b == IMT BS) + # OR + # - (sta_a == System) and (sta_b == IMT UE) + sta_a: StationManager + sta_b: StationManager + + # mask to know what paths are valid paths + _mask: np.ndarray # shape (sys_a.num_station, sys_b.num_station) + + # mask to remove duplicated calculations + _deduplicated_mask: np.ndarray # shape (sys_a.num_station, sys_b.num_station) + + # how to remove useless calculations + _mask_strategies: list[typing.Callable] = [] + + _orig_shape: tuple[int, int] + + _propagation: Propagation + + def __init__( + self, + sta_a: StationManager, + sta_b: StationManager, + mask_strategies: list[typing.Callable] = [] + ): + """Initializes PropagationPath between station_a and station_b + considering masking functions passed. By default stations at the same place + may or may not be masked, since some propagation models depend on more than + just station position. + """ + self.sta_a = sta_a + self.sta_b = sta_b + self._orig_shape = (self.sta_a.num_stations, self.sta_b.num_stations) + self._mask_strategies = mask_strategies + + def get_path_loss( + self, + propagation: Propagation, + parameters: Parameters, + frequency: float, + *, + sta_a_gains=None, + sta_b_gains=None, + ): + """Calculates mask depending on propagation, then calls the propagation + get_path_loss methods that may or may not consider the given paths. + """ + if propagation.needs_antenna_gains: + # NOTE: currently implemented propagation models that require gain + # DIFFERENTIATE between stations at the same position + # so we can't deduplicate solely based on (position, indoor, ...) + self.calc_mask(deduplicate=False) + else: + # NOTE: making sure the propagation has set needs_antenna_gains + # if it needs the gains + sta_a_gains = None + sta_b_gains = None + # NOTE: only propagation models that require gain + # differentiate between stations at the same position + self.calc_mask(deduplicate=True) + + return propagation.get_path_loss( + parameters, + frequency, + self, + sta_a_gains, + sta_b_gains, + ) + + def calc_mask(self, *, deduplicate: bool): + mask = np.ones(self._orig_shape, dtype=bool) + for mask_fn in self._mask_strategies: + mask = mask_fn(self.sta_a, self.sta_b, mask) + + if deduplicate: + self._set_mask_and_deduplicate(mask) + else: + self._set_mask(mask) + self._set_deduplication_info( + mask, + np.arange(len(self._paths_from_to)) + ) + + def mtx_to_masked(self, mtx: np.ndarray): + """Receives matrix occurring from operations of sta_a to sta_b + and returns 1d vector filtering only active paths + """ + assert mtx.shape[:2] == self._orig_shape + return mtx[self._deduplicated_mask] + + def sta_a_to_masked(self, vec: np.ndarray): + """Receives vector occurring from operations of sta_a + and returns elements for all active paths + """ + assert vec.shape[0] == self._orig_shape[0] + return vec[self._deduped_paths_from_to[:, 0]] + + def sta_b_to_masked(self, vec: np.ndarray): + """Receives vector occurring from operations of sta_b + and returns elements for all active paths + """ + assert vec.shape[0] == self._orig_shape[1] + return vec[self._deduped_paths_from_to[:, 1]] + + def from_masked_mtx(self, vec: np.ndarray): + """Receives 1d vector filtering that only contains active paths + and returns original matrix occurring from operations of sta_a to sta_b + """ + assert vec.shape == (self._n_deduped_paths,) + + mtx = np.full(self._orig_shape, np.nan) + expanded_vec = vec[self._deduped_paths_representative_node] + mtx[self._mask] = expanded_vec + + return mtx + + def _set_mask_and_deduplicate(self, mask): + self._mask = mask + self._paths_from_to = np.stack(np.where(mask), axis=-1) + self._n_paths = len(self._paths_from_to) + + dedupe_mask, representative_nodes = self._deduplicate( + self.sta_a, self.sta_b, self._paths_from_to + ) + self._set_deduplication_info(dedupe_mask, representative_nodes) + + def _set_mask(self, mask): + self._mask = mask + self._paths_from_to = np.stack(np.where(mask), axis=-1) + self._n_paths = len(self._paths_from_to) + + def _set_deduplication_info(self, deduplicated_mask, representative_nodes): + self._deduplicated_mask = deduplicated_mask + self._deduped_paths_from_to = np.stack(np.where(deduplicated_mask), axis=-1) + self._deduped_paths_representative_node = representative_nodes + self._n_deduped_paths = np.sum(deduplicated_mask) + + def _deduplicate( + self, sta_a, sta_b, paths_from_to, + ): + """Maps duplicated stations and new mask. + """ + duplicated_sta_a = self._get_representative_nodes(sta_a) + duplicated_sta_b = self._get_representative_nodes(sta_b) + + deduped_paths_from_to = np.copy(paths_from_to) + + deduped_paths_from_to[:, 0] = duplicated_sta_a[ + paths_from_to[:, 0] + ] + deduped_paths_from_to[:, 1] = duplicated_sta_b[ + paths_from_to[:, 1] + ] + + # since deduped_paths_from_to is already ordered + # np.unique's unstable sort doesn't eff it up + uniq_deduped_paths_from_to, adjacency_vec = np.unique( + deduped_paths_from_to, + axis=0, + return_inverse=True + ) + + deduplicated_mask = np.zeros((sta_a.num_stations, sta_b.num_stations), dtype=bool) + deduplicated_mask[ + uniq_deduped_paths_from_to[:, 0], uniq_deduped_paths_from_to[:, 1] + ] = True + + return deduplicated_mask, adjacency_vec + + @staticmethod + def _get_representative_nodes(sta: StationManager): + # tuples for what needs to be considered for differentiating + # stations for deduplication + representative_tuples = np.stack([ + sta.geom.x_global, + sta.geom.y_global, + sta.geom.z_global, + sta.indoor, + ], axis=-1) + + # like an adjacency matrix, but for tree-like struct + # e.g. graph where each node points to itself or its representative + adjacency_vec = np.zeros((len(representative_tuples),)) + repr_nodes = {} + + # NOTE: tried using numpy but np.unique unstable sorts + # and makes the code really ugly + for i in range(len(representative_tuples)): + tup = tuple(representative_tuples[i]) + if tup not in repr_nodes: + repr_nodes[tup] = i + adjacency_vec[i] = repr_nodes[tup] + + return adjacency_vec + + @staticmethod + def create_default( + sta_a, + sta_b, + ) -> "PropagationPath": + """Creates a PropagationPath between stations considering + which ones should exist. + Check default behavior to understand what paths "should exist" + by default + """ + mask_fns = [ + path_mask_inactive_stations, + ] + + if sta_a.is_space_station or sta_b.is_space_station: + # care for through earth interference + mask_fns.append( + create_path_mask_low_elevation_sat_from_es(0.0) + ) + + # if one of the stations considers a maximum earth path distance + # for interference, we remove those paths + max_interf_dist = np.inf + if sta_a.max_earth_sta_interf_distance is not None: + max_interf_dist = min( + max_interf_dist, + sta_a.max_earth_sta_interf_distance, + ) + if sta_b.max_earth_sta_interf_distance is not None: + max_interf_dist = min( + max_interf_dist, + sta_b.max_earth_sta_interf_distance, + ) + + if max_interf_dist < np.inf: + mask_fns.append( + create_path_mask_distant_earth_stations(max_interf_dist), + ) + + return PropagationPath(sta_a, sta_b, mask_fns) + +def create_path_mask_distant_earth_stations(distance_m: float): + """Receives a distance in meters and returns a function + deactivate paths between distant earth stations + """ + def path_mask_distant_earth_stations( + sta_a: StationManager, + sta_b: StationManager, + acc_mask: np.ndarray[np.ndarray[bool]] + ): + if sta_a.is_space_station or sta_b.is_space_station: + return acc_mask + + dists = sta_a.geom.get_3d_distance_to(sta_b.geom) + acc_mask[dists > distance_m] = False + + return acc_mask + + return path_mask_distant_earth_stations + + +def create_path_mask_low_elevation_sat_from_es(min_elev_deg: float = 0.0): + """Receives a distance in meters and returns a function + deactivate paths between distant earth stations + """ + def path_mask_low_elevation_sat_from_es( + sta_a: StationManager, + sta_b: StationManager, + acc_mask: np.ndarray[np.ndarray[bool]] + ): + es: StationManager = None + ss: StationManager = None + if sta_a.is_space_station and not sta_b.is_space_station: + transpose = True + es = sta_b + ss = sta_a + elif not sta_a.is_space_station and sta_b.is_space_station: + transpose = False + es = sta_a + ss = sta_b + else: + return acc_mask + + elevs = es.geom.get_local_elevation(ss.geom) + if transpose: + elevs = elevs.T + acc_mask[elevs < min_elev_deg] = False + + return acc_mask + + return path_mask_low_elevation_sat_from_es + + +def path_mask_inactive_stations( + sta_a: StationManager, + sta_b: StationManager, + acc_mask: np.ndarray[np.ndarray[bool]] +): + acc_mask[~sta_a.active, :] = False + acc_mask[:, ~sta_b.active] = False + + return acc_mask + + +if __name__ == "__main__": + bs = StationManager(2) + ue = StationManager(6) + bs.geom.set_global_coords( + np.repeat(0., 2), + np.repeat(0., 2), + np.repeat(1.5, 2), + ) + + ue.geom.set_global_coords( + np.linspace(10., 60., 6), + np.linspace(10., 60., 6), + np.repeat(1.5, 6), + ) + path = PropagationPath(bs, ue) + path._set_mask_and_deduplicate( + np.array([ + [ + True, True, True, False, False, False + ], + [ + False, False, False, True, True, True + ], + ]) + ) + + # print("path.deduplicated_mask", path.deduplicated_mask) + # print("path.mask", path.mask) + print("path._paths_from_to", path._paths_from_to) + print("path.deduplicated_mask", path._deduplicated_mask) + dist2d = bs.geom.get_local_distance_to(ue.geom) + print("dist2d", dist2d) + masked_dist2d = path.to_masked(dist2d) + print("masked_dist2d", masked_dist2d) + unmasked_dist2d = path.from_masked(masked_dist2d) + print("unmasked_dist2d", unmasked_dist2d) + + print() + print() + print("#####################3") + print() + + # bs.active[0] = False + ue.active[2] = False + path = PropagationPath.create_default(bs, ue) + path.calc_mask(deduplicate=True) + print("path._paths_from_to", path._paths_from_to) + print("path.deduplicated_mask", path._deduplicated_mask) + dist2d = path.sta_a.geom.get_local_distance_to(path.sta_b.geom) + print("dist2d", dist2d) + masked_dist2d = path.to_masked(dist2d) + print("masked_dist2d", masked_dist2d) + unmasked_dist2d = path.from_masked(masked_dist2d) + print("unmasked_dist2d", unmasked_dist2d) + + print() + print() + print("#####################3") + print() + + path = PropagationPath.create_default(bs, ue) + path.calc_mask(deduplicate=False) + print("path._paths_from_to", path._paths_from_to) + print("path.deduplicated_mask", path._deduplicated_mask) + dist2d = path.sta_a.geom.get_local_distance_to(path.sta_b.geom) + print("dist2d", dist2d) + masked_dist2d = path.to_masked(dist2d) + print("masked_dist2d", masked_dist2d) + unmasked_dist2d = path.from_masked(masked_dist2d) + print("unmasked_dist2d", unmasked_dist2d) diff --git a/sharc/propagation/propagation_sat_simple.py b/sharc/propagation/propagation_sat_simple.py index 8f5bab832..d90cb4b82 100644 --- a/sharc/propagation/propagation_sat_simple.py +++ b/sharc/propagation/propagation_sat_simple.py @@ -15,6 +15,7 @@ from sharc.support.enumerations import StationType from sharc.station_manager import StationManager from sharc.parameters.parameters import Parameters +from sharc.propagation.propagation_path import PropagationPath class PropagationSatSimple(Propagation): @@ -37,14 +38,11 @@ def __init__( ) self.atmospheric_loss = 0.75 - @dispatch(Parameters, float, StationManager, - StationManager, np.ndarray, np.ndarray) - def get_loss( + def get_path_loss( self, params: Parameters, frequency: float, - station_a: StationManager, - station_b: StationManager, + path: PropagationPath, station_a_gains=None, station_b_gains=None, ) -> np.array: @@ -66,41 +64,63 @@ def get_loss( Return an array station_a.num_stations x station_b.num_stations with the path loss between each station """ - distance_3d = station_a.geom.get_3d_distance_to(station_b.geom) - frequency = frequency * np.ones(distance_3d.shape) - indoor_stations = np.tile( - station_b.indoor, (station_a.num_stations, 1), - ) + station_a = path.sta_a + station_b = path.sta_b + + distance = station_a.geom.get_3d_distance_to(station_b.geom) + masked_distance = path.mtx_to_masked(distance) + masked_frequency = frequency * np.ones_like(masked_distance) # Elevation angles seen from the station on Earth. - elevation_angles = {} raise NotImplementedError( "FIXME: apparent_elevation_angle should receive earth station altitude..." ) + masked_elevation_angles = {} if station_a.is_space_station: - elevation_angles["free_space"] = station_b.geom.get_local_elevation(station_a.geom) - # if (station_b_gains.shape != distance.shape): - # raise ValueError(f"Invalid shape for station_b_gains = {station_b_gains.shape}") - elevation_angles["apparent"] = PropagationP619.apparent_elevation_angle( - elevation_angles["free_space"], station_a.height, ) + if station_b.geom.uses_local_coords: + raise NotImplementedError( + "P619 currently assumes earth station z == height. " + "If ES has local coords != global coords, this probably isn't true" + ) + masked_indoor_stations = path.sta_b_to_masked(station_b.indoor) + masked_elevation_angles["free_space"] = station_b.geom.get_local_elevation(station_a.geom) + masked_elevation_angles["apparent"] = PropagationP619.apparent_elevation_angle( + masked_elevation_angles["free_space"], + # FIXME + # self.earth_station_alt_m, + ) # Transpose it to fit the expected path loss shape - elevation_angles["free_space"] = np.transpose( - elevation_angles["free_space"]) - elevation_angles["apparent"] = np.transpose( - elevation_angles["apparent"]) + masked_elevation_angles["free_space"] = path.mtx_to_masked(np.transpose( + masked_elevation_angles["free_space"])) + masked_elevation_angles["apparent"] = path.mtx_to_masked(np.transpose( + masked_elevation_angles["apparent"])) elif station_b.is_space_station: - elevation_angles["free_space"] = station_a.geom.get_local_elevation(station_b.geom) - elevation_angles["apparent"] = PropagationP619.apparent_elevation_angle( - elevation_angles["free_space"], station_b.height, ) + if station_a.geom.uses_local_coords: + raise NotImplementedError( + "P619 currently assumes earth station z == height. " + "If ES has local coords != global coords, this probably isn't true" + ) + masked_indoor_stations = path.sta_a_to_masked(station_a.indoor) + masked_elevation_angles["free_space"] = path.mtx_to_masked( + station_a.geom.get_local_elevation(station_b.geom) + ) + masked_elevation_angles["apparent"] = PropagationP619.apparent_elevation_angle( + masked_elevation_angles["free_space"], + # FIXME + # self.earth_station_alt_m, + ) else: raise ValueError( "PropagationP619: At least one station must be an space station", ) - return self.get_loss( - distance_3d, - frequency, - indoor_stations, - elevation_angles) + masked_loss = self.get_loss( + masked_distance, + masked_frequency, + masked_indoor_stations, + masked_elevation_angles, + ) + + return path.from_masked_mtx(masked_loss) @dispatch(np.ndarray, np.ndarray, np.ndarray, dict) def get_loss( diff --git a/sharc/propagation/propagation_ter_simple.py b/sharc/propagation/propagation_ter_simple.py index 9008c87de..873d74f28 100644 --- a/sharc/propagation/propagation_ter_simple.py +++ b/sharc/propagation/propagation_ter_simple.py @@ -13,6 +13,7 @@ from sharc.propagation.propagation_free_space import PropagationFreeSpace from sharc.propagation.propagation_clutter_loss import PropagationClutterLoss from sharc.support.enumerations import StationType +from sharc.propagation.propagation_path import PropagationPath class PropagationTerSimple(Propagation): @@ -28,17 +29,15 @@ def __init__(self, random_number_gen: np.random.RandomState): self.clutter = PropagationClutterLoss(np.random.RandomState(101)) self.free_space = PropagationFreeSpace(np.random.RandomState(101)) self.building_loss = 20 + self.clutter_type = "one_end" - @dispatch(Parameters, float, StationManager, - StationManager, np.ndarray, np.ndarray) - def get_loss( + def get_path_loss( self, params: Parameters, frequency: float, - station_a: StationManager, - station_b: StationManager, - station_a_gains=None, - station_b_gains=None, + path: PropagationPath, + station_a_gains, + station_b_gains, ) -> np.array: """Wrapper function for the get_loss method to fit the Propagation ABC class interface Calculates the loss between station_a and station_b @@ -64,13 +63,11 @@ def get_loss( Return an array station_a.num_stations x station_b.num_stations with the path loss between each station """ - distance = station_a.geom.get_3d_distance_to(station_b.geom) + distance = path.mtx_to_masked(path.sta_a.geom.get_3d_distance_to(path.sta_b.geom)) frequency_array = frequency * np.ones(distance.shape) - indoor_stations = np.tile( - station_b.indoor, (station_a.num_stations, 1), - ) + indoor_stations = path.sta_b_to_masked(path.sta_b.indoor) - return self.get_loss(distance, frequency_array, indoor_stations, -1.0) + return path.from_masked_mtx(self.get_loss(distance, frequency_array, indoor_stations, -1.0)) # pylint: disable=arguments-differ @dispatch(np.ndarray, np.ndarray, np.ndarray, float) @@ -114,6 +111,7 @@ def get_loss( distance=distance, loc_percentage=loc_percentage, station_type=StationType.FSS_ES, + clutter_type=self.clutter_type ) building_loss = self.building_loss * indoor_stations diff --git a/sharc/propagation/propagation_tvro.py b/sharc/propagation/propagation_tvro.py index c99b52977..62089017b 100644 --- a/sharc/propagation/propagation_tvro.py +++ b/sharc/propagation/propagation_tvro.py @@ -14,6 +14,7 @@ from sharc.propagation.propagation import Propagation from sharc.station_manager import StationManager from sharc.parameters.parameters import Parameters +from sharc.propagation.propagation_path import PropagationPath class PropagationTvro(Propagation): @@ -42,12 +43,11 @@ def __init__( self.free_space_path_loss = PropagationFreeSpace(random_number_gen) - def get_loss( + def get_path_loss( self, params: Parameters, frequency: float, - station_a: StationManager, - station_b: StationManager, + path: "PropagationPath", station_a_gains=None, station_b_gains=None, ) -> np.array: @@ -79,35 +79,34 @@ def get_loss( and params.imt.topology.hotspot.num_clusters == 1 if wrap_around_enabled and ( - station_a.is_imt_station() and station_b.is_imt_station()): + path.sta_a.is_imt_station() and path.sta_b.is_imt_station()): distances_2d, distances_3d, _, _ = \ - station_a.geom.get_global_dist_angles_wrap_around(station_b.geom) + path.sta_a.geom.get_global_dist_angles_wrap_around(path.sta_b.geom) else: - distances_2d = station_a.geom.get_local_distance_to(station_b.geom) - distances_3d = station_a.geom.get_3d_distance_to(station_b.geom) + distances_2d = path.sta_a.geom.get_local_distance_to(path.sta_b.geom) + distances_3d = path.sta_a.geom.get_3d_distance_to(path.sta_b.geom) - indoor_stations = np.tile( - station_a.indoor, (station_b.num_stations, 1)).transpose() + indoor_stations = path.sta_a.indoor # Use the right interface whether the link is IMT-IMT or IMT-System # TODO: Refactor __get_loss and get rid of that if-else. - if station_a.is_imt_station() and station_b.is_imt_station(): - if station_a.geom.uses_local_coords: + if path.sta_a.is_imt_station() and path.sta_b.is_imt_station(): + if path.sta_a.geom.uses_local_coords: raise NotImplementedError( "TVRO currently assumes UE z == height. " "If UE has local coords != global coords, this probably isn't true" ) - + mskd_distance_3d = path.mtx_to_masked(distances_3d) loss = self._get_loss( - distance_3D=distances_3d, - distance_2D=distances_2d, - frequency=frequency * np.ones(distances_2d.shape), - ue_height=station_a.geom.z_global, - indoor_stations=indoor_stations + distance_3D=mskd_distance_3d, + distance_2D=path.mtx_to_masked(distances_2d), + frequency=frequency * np.ones(mskd_distance_3d.shape), + ue_height=path.sta_a_to_masked(path.sta_a.geom.z_local), + indoor_stations=path.sta_a_to_masked(indoor_stations) ) else: - imt_station, sys_station = (station_a, station_b) \ - if station_a.is_imt_station() else (station_b, station_a) + imt_station, sys_station = (path.sta_a, path.sta_b) \ + if path.sta_a.is_imt_station() else (path.sta_b, path.sta_a) if sys_station.geom.uses_local_coords: raise NotImplementedError( @@ -115,16 +114,23 @@ def get_loss( "If System has local coords != global coords, this probably isn't true" ) + es_z = sys_station.geom.z_local + if sys_station == path.sta_a: + es_z = path.sta_a_to_masked(es_z) + else: + es_z = path.sta_b_to_masked(es_z) + + mskd_distance_3d = path.mtx_to_masked(distances_3d) loss = self._get_loss( - distance_3D=distances_3d, - distance_2D=distances_2d, - frequency=frequency * np.ones(distances_2d.shape), + distance_3D=mskd_distance_3d, + distance_2D=path.mtx_to_masked(distances_2d), + frequency=frequency * np.ones(mskd_distance_3d.shape), imt_sta_type=imt_station.station_type, - es_z=sys_station.geom.z_global, - indoor_stations=indoor_stations + es_z=es_z, + indoor_stations=path.sta_a_to_masked(indoor_stations) ) - return loss + return path.from_masked_mtx(loss) def _get_loss(self, *args, **kwargs) -> np.array: """ diff --git a/sharc/propagation/propagation_uma.py b/sharc/propagation/propagation_uma.py index 74795dd4e..e6bcdab3f 100644 --- a/sharc/propagation/propagation_uma.py +++ b/sharc/propagation/propagation_uma.py @@ -11,7 +11,7 @@ from multipledispatch import dispatch from sharc.propagation.propagation import Propagation -from sharc.station_manager import StationManager +from sharc.propagation.propagation_path import PropagationPath from sharc.parameters.parameters import Parameters @@ -21,14 +21,11 @@ class PropagationUMa(Propagation): to 3GPP TR 38.900 v14.2.0. TODO: calculate the effective environment height for the generic case """ - @dispatch(Parameters, float, StationManager, - StationManager, np.ndarray, np.ndarray) - def get_loss( + def get_path_loss( self, params: Parameters, frequency: float, - station_a: StationManager, - station_b: StationManager, + path: PropagationPath, station_a_gains=None, station_b_gains=None, ) -> np.array: @@ -57,33 +54,35 @@ def get_loss( if params.imt.topology.type == "HOTSPOT": wrap_around_enabled = params.imt.topology.hotspot.wrap_around \ and params.imt.topology.hotspot.num_clusters == 1 + is_intra_imt = path.sta_a.is_imt_station() and path.sta_b.is_imt_station() - if wrap_around_enabled and ( - station_a.is_imt_station() and station_b.is_imt_station()): + if wrap_around_enabled and is_intra_imt: distances_2d, distances_3d, _, _ = \ - station_a.geom.get_global_dist_angles_wrap_around(station_b.geom) + path.sta_a.geom.get_global_dist_angles_wrap_around(path.sta_b.geom) else: - distances_2d = station_a.geom.get_local_distance_to(station_b.geom) - distances_3d = station_a.geom.get_3d_distance_to(station_b.geom) + distances_2d = path.sta_a.geom.get_local_distance_to(path.sta_b.geom) + distances_3d = path.sta_a.geom.get_3d_distance_to(path.sta_b.geom) - if station_a.geom.uses_local_coords or station_b.geom.uses_local_coords: + if path.sta_a.geom.uses_local_coords or path.sta_b.geom.uses_local_coords: raise NotImplementedError( "UMa currently assumes stations z == height. " "If stations has local coords != global coords, this probably isn't true" ) - loss = self.get_loss( - distances_3d, - distances_2d, - frequency * np.ones(distances_2d.shape), - station_b.geom.z_global, - station_a.geom.z_global, + mskd_distances_3d = path.mtx_to_masked(distances_3d) + mskd_distances_2d = path.mtx_to_masked(distances_2d) + mskd_loss = self.get_loss( + mskd_distances_3d, + mskd_distances_2d, + frequency * np.ones(mskd_distances_2d.shape), + path.sta_b_to_masked(path.sta_b.geom.z_local), + path.sta_a_to_masked(path.sta_a.geom.z_local), params.imt.shadowing, ) # the interface expects station_a.num_stations x station_b.num_stations # array - return loss + return path.from_masked_mtx(mskd_loss) @dispatch(np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, bool) def get_loss( @@ -182,7 +181,7 @@ def get_loss_los( idl = np.where(distance_2D < breakpoint_distance) # get index where distance if greater than breakpoint - idg = np.where(distance_2D >= breakpoint_distance) + idg = np.asarray(distance_2D >= breakpoint_distance).nonzero() loss = np.empty(distance_2D.shape) @@ -194,7 +193,7 @@ def get_loss_los( fitting_term = -10 * \ np.log10( breakpoint_distance**2 + - (h_bs - h_ue[:, np.newaxis])**2, + (h_bs - h_ue)**2, ) loss[idg] = 40 * np.log10(distance_3D[idg]) + 20 * \ np.log10(frequency[idg]) - 27.55 + fitting_term[idg] @@ -227,7 +226,7 @@ def get_loss_nlos( h_bs : array of base stations antenna heights [m] h_ue : array of user equipments antenna heights [m] """ loss_nlos = -46.46 + 39.08 * np.log10(distance_3D) + 20 * np.log10(frequency) \ - - 0.6 * (h_ue[:, np.newaxis] - 1.5) + - 0.6 * (h_ue - 1.5) idl = np.where(distance_2D < 5000) if len(idl[0]): @@ -268,7 +267,7 @@ def get_breakpoint_distance( """ # calculate the effective antenna heights h_bs_eff = h_bs - h_e - h_ue_eff = h_ue[:, np.newaxis] - h_e + h_ue_eff = h_ue - h_e # calculate the breakpoint distance breakpoint_distance = 4 * h_bs_eff * \ @@ -315,7 +314,7 @@ def get_los_probability( idc = np.where(h_ue > 13)[0] if len(idc): - c_prime[:, idc] = np.power((h_ue[idc] - 13) / 10, 1.5) + c_prime[idc] = np.power((h_ue[idc] - 13) / 10, 1.5) p_los = np.ones(distance_2D.shape) idl = np.where(distance_2D > 18) @@ -330,11 +329,9 @@ def get_los_probability( ########################################################################### # Print LOS probability distance_2D = np.column_stack(( - np.linspace(1, 10000, num=10000)[:, np.newaxis], - np.linspace(1, 10000, num=10000)[ - :, np.newaxis, - ], - np.linspace(1, 10000, num=10000)[:, np.newaxis], + np.linspace(1, 10000, num=10000), + np.linspace(1, 10000, num=10000), + np.linspace(1, 10000, num=10000), )) h_ue = np.array([1.5, 17, 23]) uma = PropagationUMa(np.random.RandomState(101)) diff --git a/sharc/propagation/propagation_umi.py b/sharc/propagation/propagation_umi.py index 4541397a3..f1c2654a2 100644 --- a/sharc/propagation/propagation_umi.py +++ b/sharc/propagation/propagation_umi.py @@ -10,6 +10,7 @@ from sharc.propagation.propagation import Propagation from sharc.station_manager import StationManager from sharc.parameters.parameters import Parameters +from sharc.propagation.propagation_path import PropagationPath class PropagationUMi(Propagation): @@ -27,14 +28,11 @@ def __init__( super().__init__(random_number_gen) self.los_adjustment_factor = los_adjustment_factor - @dispatch(Parameters, float, StationManager, - StationManager, np.ndarray, np.ndarray) - def get_loss( + def get_path_loss( self, params: Parameters, frequency: float, - station_a: StationManager, - station_b: StationManager, + path: PropagationPath, station_a_gains=None, station_b_gains=None, ) -> np.array: @@ -65,29 +63,31 @@ def get_loss( and params.imt.topology.hotspot.num_clusters == 1 if wrap_around_enabled and ( - station_a.is_imt_station() and station_b.is_imt_station()): + path.sta_a.is_imt_station() and path.sta_b.is_imt_station()): distance_2d, distance_3d, _, _ = \ - station_a.geom.get_global_dist_angles_wrap_around(station_b.geom) + path.sta_a.geom.get_global_dist_angles_wrap_around(path.sta_b.geom) else: - distance_2d = station_a.geom.get_local_distance_to(station_b.geom) - distance_3d = station_a.geom.get_3d_distance_to(station_b.geom) + distance_2d = path.sta_a.geom.get_local_distance_to(path.sta_b.geom) + distance_3d = path.sta_a.geom.get_3d_distance_to(path.sta_b.geom) - if station_a.geom.uses_local_coords or station_b.geom.uses_local_coords: + if path.sta_a.geom.uses_local_coords or path.sta_b.geom.uses_local_coords: raise NotImplementedError( "UMi currently assumes stations z == height. " "If stations has local coords != global coords, this probably isn't true" ) - loss = self.get_loss( - distance_3d, - distance_2d, - frequency * np.ones(distance_2d.shape), - station_b.geom.z_global, - station_a.geom.z_global, + mskd_distances_3d = path.mtx_to_masked(distance_3d) + mskd_distances_2d = path.mtx_to_masked(distance_2d) + mskd_loss = self.get_loss( + mskd_distances_3d, + mskd_distances_2d, + frequency * np.ones(mskd_distances_2d.shape), + path.sta_b_to_masked(path.sta_b.geom.z_local), + path.sta_a_to_masked(path.sta_a.geom.z_local), params.imt.shadowing, ) - return loss + return path.from_masked_mtx(mskd_loss) # pylint: disable=function-redefined # pylint: disable=arguments-renamed @@ -207,7 +207,7 @@ def get_loss_los( fitting_term = -9.5 * \ np.log10( breakpoint_distance**2 + - (h_bs - h_ue[:, np.newaxis])**2, + (h_bs - h_ue)**2, ) loss[idg] = 40 * np.log10(distance_3D[idg]) + 20 * \ np.log10(frequency[idg]) - 27.55 + fitting_term[idg] @@ -242,7 +242,7 @@ def get_loss_nlos( """ # option 1 for UMi NLOS loss_nlos = -37.55 + 35.3 * np.log10(distance_3D) + 21.3 * np.log10(frequency) \ - - 0.3 * (h_ue[:, np.newaxis] - 1.5) + - 0.3 * (h_ue - 1.5) loss_los = self.get_loss_los( distance_2D, distance_3D, frequency, h_bs, h_ue, h_e, 0, @@ -283,7 +283,7 @@ def get_breakpoint_distance( """ # calculate the effective antenna heights h_bs_eff = h_bs - h_e - h_ue_eff = h_ue[:, np.newaxis] - h_e + h_ue_eff = h_ue - h_e # calculate the breakpoint distance breakpoint_distance = 4 * h_bs_eff * \ @@ -346,7 +346,7 @@ def get_los_probability( ########################################################################### # Print LOS probability # h_ue = np.array([1.5, 17, 23]) - distance_2D = np.linspace(1, 1000, num=1000)[:, np.newaxis] + distance_2D = np.linspace(1, 1000, num=1000) umi = PropagationUMi(np.random.RandomState(101), 18) los_probability = np.empty(distance_2D.shape) diff --git a/sharc/propagation/scintillation.py b/sharc/propagation/scintillation.py index 042e8c884..3cb25425a 100644 --- a/sharc/propagation/scintillation.py +++ b/sharc/propagation/scintillation.py @@ -167,14 +167,14 @@ def get_tropospheric_attenuation(self, *args, **kwargs) -> np.array: plt.figure() for elevation in elevation_vec: attenuation = propagation.get_tropospheric_attenuation( - elevation=elevation, - frequency_MHz=frequency_MHz, - antenna_gain_dB=antenna_gain, + elevation=np.full(percentage_fading_exceeded.shape, elevation), + frequency_MHz=np.full(percentage_fading_exceeded.shape, frequency_MHz), + antenna_gain_dB=np.full(percentage_fading_exceeded.shape, antenna_gain), time_ratio=1 - (percentage_fading_exceeded / 100), wet_refractivity=wet_refractivity, ) - plt.semilogx( + plt.loglog( percentage_fading_exceeded, attenuation, label="{} deg".format(elevation), ) @@ -182,9 +182,9 @@ def get_tropospheric_attenuation(self, *args, **kwargs) -> np.array: percentage_gain_exceeded = 10 ** np.arange(-2, 1.1, .1) for elevation in elevation_vec: attenuation = propagation.get_tropospheric_attenuation( - elevation=elevation, - frequency_MHz=frequency_MHz, - antenna_gain_dB=antenna_gain, + elevation=np.full(percentage_fading_exceeded.shape, elevation), + frequency_MHz=np.full(percentage_fading_exceeded.shape, frequency_MHz), + antenna_gain_dB=np.full(percentage_fading_exceeded.shape, antenna_gain), time_ratio=percentage_gain_exceeded / 100, wet_refractivity=wet_refractivity, ) @@ -195,6 +195,9 @@ def get_tropospheric_attenuation(self, *args, **kwargs) -> np.array: plt.legend(title='elevation') plt.grid(True) + plt.grid(which='minor', color='gray', linestyle=':', linewidth=0.5) + plt.xlim((0.01, 10)) + plt.ylim((0.1, 10)) plt.title("troposcatter scintillation attenuation") plt.xlabel("Percentage time fades and enhancements exceeded") diff --git a/sharc/simulation.py b/sharc/simulation.py index 23f91e1fd..5800dfc6f 100644 --- a/sharc/simulation.py +++ b/sharc/simulation.py @@ -20,6 +20,7 @@ from sharc.station_manager import StationManager from sharc.results import Results from sharc.propagation.propagation_factory import PropagationFactory +from sharc.propagation.propagation_path import PropagationPath from sharc.support.sharc_utils import wrap2_180, clip_angle @@ -124,6 +125,9 @@ def __init__(self, parameters: Parameters, parameter_file: str): self.bs = np.empty(0) self.system = np.empty(0) + self.paths_between_imt_and_sys: PropagationPath = None + self.intra_imt_paths: PropagationPath = None + self.link = dict() self.num_rb_per_bs = 0 @@ -303,9 +307,12 @@ def calculate_coupling_loss_system_imt( # Calculate the antenna gains of the IMT station with respect to the # system's station + use_separated_beams = False if imt_station.station_type is StationType.IMT_UE: # define antenna gains + # shape: (n_sys, n_imt) gain_sys_to_imt = self.calculate_gains(system_station, imt_station) + # shape: (n_sys, n_imt) gain_imt_to_sys = np.transpose( self.calculate_gains( imt_station, @@ -315,17 +322,35 @@ def calculate_coupling_loss_system_imt( + self.parameters.imt.ue.body_loss \ + self.polarization_loss elif imt_station.station_type is StationType.IMT_BS: + use_separated_beams = True # define antenna gains # repeat for each BS beam + # shape: (n_sys, n_imt * ue_k) gain_sys_to_imt = np.repeat( self.calculate_gains(system_station, imt_station), self.parameters.imt.ue.k, 1, ) + # shape: (n_sys, n_imt * ue_k) gain_imt_to_sys = np.transpose( self.calculate_gains( imt_station, system_station, is_co_channel, ), ) + + # shape: (n_sys, n_bs, ue_k) + gain_sys_to_imt_separated_beams = gain_sys_to_imt.reshape( + gain_sys_to_imt.shape[0], + gain_sys_to_imt.shape[1] // self.parameters.imt.ue.k, + self.parameters.imt.ue.k + ) + # shape: (n_bs, n_sys, ue_k) + # reshape makes (n_sys, n_bs, ue_k), transpose changes axis + gain_imt_to_sys_separated_beams_imt_frst = gain_imt_to_sys.reshape( + gain_imt_to_sys.shape[0], + gain_imt_to_sys.shape[1] // self.parameters.imt.ue.k, + self.parameters.imt.ue.k + ).transpose(1, 0, 2) + additional_loss = self.parameters.imt.bs.ohmic_loss \ + self.polarization_loss else: @@ -334,44 +359,36 @@ def calculate_coupling_loss_system_imt( f"Invalid IMT StationType! { imt_station.station_type}") - # TODO: this performance betterment doesn't work when one of the stations is IMT_BS - # so do something that works for it - # # Calculate the path loss based on the propagation model only for active stations - # actv_sys = copy_active_stations(system_station) - # actv_imt = copy_active_stations(imt_station) - # path_loss = np.zeros((system_station.num_stations, imt_station.num_stations)) - # actv_path_loss = self.propagation_system.get_loss( - # self.parameters, - # freq, - # actv_sys, - # actv_imt, - # gain_sys_to_imt, - # gain_imt_to_sys, - # ) - # path_loss[np.ix_(system_station.active, imt_station.active)] = actv_path_loss - - path_loss = self.propagation_system.get_loss( + if use_separated_beams: + sta_a_gains = gain_sys_to_imt_separated_beams + sta_b_gains = gain_imt_to_sys_separated_beams_imt_frst + else: + sta_a_gains = gain_sys_to_imt + sta_b_gains = gain_imt_to_sys.T + + path_loss = self.paths_between_imt_and_sys.get_path_loss( + self.propagation_system, self.parameters, freq, - system_station, - imt_station, - gain_sys_to_imt, - gain_imt_to_sys, + sta_a_gains=sta_a_gains, + sta_b_gains=sta_b_gains, ) + # TODO: remove gambiarra # Store antenna gains and path loss samples if self.param_system.channel_model == "HDFSS": self.imt_system_build_entry_loss = path_loss[1] self.imt_system_diffraction_loss = path_loss[2] path_loss = path_loss[0] - if imt_station.station_type is StationType.IMT_UE: - self.imt_system_path_loss = path_loss - else: - # Repeat for each BS beam - self.imt_system_path_loss = np.repeat( - path_loss, self.parameters.imt.ue.k, 1, - ) + if use_separated_beams: + if path_loss.ndim == 3: + # (n_sys, n_imt * ue_k) + path_loss = path_loss.reshape(path_loss.shape[0], -1) + elif path_loss.shape[1] == imt_station.num_stations: + path_loss = np.repeat(path_loss, self.parameters.imt.ue.k, -1) + + self.imt_system_path_loss = path_loss self.system_imt_antenna_gain = gain_sys_to_imt @@ -429,13 +446,12 @@ def calculate_intra_imt_coupling_loss( # Note on the array dimentions for coupling loss calculations: # The function get_loss returns an array station_a x station_b - path_loss = self.propagation_imt.get_loss( + path_loss = self.intra_imt_paths.get_path_loss( + self.propagation_imt, self.parameters, self.parameters.imt.frequency, - imt_ue_station, - imt_bs_station, - ant_gain_ue_to_bs, - ant_gain_bs_to_ue, + sta_a_gains=ant_gain_ue_to_bs, + sta_b_gains=ant_gain_bs_to_ue, ) # Collect IMT BS and UE antenna gain samples diff --git a/sharc/simulation_downlink.py b/sharc/simulation_downlink.py index ebfcb3cc8..65721f4a6 100644 --- a/sharc/simulation_downlink.py +++ b/sharc/simulation_downlink.py @@ -13,6 +13,7 @@ from sharc.parameters.parameters import Parameters from sharc.station_factory import StationFactory from sharc.parameters.constants import BOLTZMANN_CONSTANT +from sharc.propagation.propagation_path import PropagationPath warn = warnings.warn @@ -83,6 +84,22 @@ def snapshot(self, *args, **kwargs): self.topology, random_number_gen, ) + if self.parameters.imt.interfered_with: + self.paths_between_imt_and_sys = PropagationPath.create_default( + self.system, + self.ue, + ) + else: + self.paths_between_imt_and_sys = PropagationPath.create_default( + self.system, + self.bs, + ) + + self.intra_imt_paths = PropagationPath.create_default( + self.ue, + self.bs, + ) + # self.plot_scenario() self.connect_ue_to_bs() diff --git a/sharc/simulation_uplink.py b/sharc/simulation_uplink.py index 7d547f114..2acad8c24 100644 --- a/sharc/simulation_uplink.py +++ b/sharc/simulation_uplink.py @@ -13,6 +13,7 @@ from sharc.parameters.parameters import Parameters from sharc.station_factory import StationFactory from sharc.parameters.constants import BOLTZMANN_CONSTANT +from sharc.propagation.propagation_path import PropagationPath warn = warnings.warn @@ -71,6 +72,22 @@ def snapshot(self, *args, **kwargs): self.parameters.imt.ue.antenna.array, self.topology, random_number_gen, ) + + if self.parameters.imt.interfered_with: + self.paths_between_imt_and_sys = PropagationPath.create_default( + self.system, + self.bs, + ) + else: + self.paths_between_imt_and_sys = PropagationPath.create_default( + self.system, + self.ue, + ) + + self.intra_imt_paths = PropagationPath.create_default( + self.ue, + self.bs, + ) # self.plot_scenario() self.connect_ue_to_bs() diff --git a/sharc/station_manager.py b/sharc/station_manager.py index 191de0a45..07ecd3ca6 100644 --- a/sharc/station_manager.py +++ b/sharc/station_manager.py @@ -49,6 +49,7 @@ def __init__(self, n): self.station_type = StationType.NONE self.is_space_station = False self.geom = SimulatorGeometry(n) + self.max_earth_sta_interf_distance = np.inf def get_station_list(self, id=None) -> list: """Return a list of Station objects for the given indices. diff --git a/tests/test_adjacent_channel.py b/tests/test_adjacent_channel.py index 072f791e9..358cefa22 100644 --- a/tests/test_adjacent_channel.py +++ b/tests/test_adjacent_channel.py @@ -16,6 +16,7 @@ from sharc.antenna.antenna_omni import AntennaOmni from sharc.station_factory import StationFactory from sharc.propagation.propagation_factory import PropagationFactory +from sharc.propagation.propagation_path import PropagationPath from sharc.parameters.imt.parameters_imt_topology import ParametersImtTopology from sharc.parameters.imt.parameters_single_bs import ParametersSingleBS @@ -206,6 +207,9 @@ def test_simulation_2bs_4ue_downlink(self): random_number_gen, ) + self.simulation.intra_imt_paths = PropagationPath.create_default( + self.simulation.ue, self.simulation.bs + ) # test coupling loss method self.simulation.coupling_loss_imt = self.simulation.calculate_intra_imt_coupling_loss( self.simulation.ue, self.simulation.bs, ) @@ -251,7 +255,10 @@ def test_simulation_2bs_4ue_downlink(self): np.array([self.param.fss_ss.altitude]), ) - # test the method that calculates interference from IMT UE to FSS space + self.simulation.paths_between_imt_and_sys = PropagationPath.create_default( + self.simulation.system, self.simulation.bs + ) + # test the method that calculates interference from IMT BS to FSS space # station self.simulation.calculate_external_interference() @@ -346,6 +353,9 @@ def test_simulation_2bs_4ue_uplink(self): random_number_gen, ) + self.simulation.intra_imt_paths = PropagationPath.create_default( + self.simulation.ue, self.simulation.bs + ) # test coupling loss method self.simulation.coupling_loss_imt = self.simulation.calculate_intra_imt_coupling_loss( self.simulation.ue, self.simulation.bs, ) @@ -389,6 +399,9 @@ def test_simulation_2bs_4ue_uplink(self): np.array([self.param.fss_ss.altitude]), ) + self.simulation.paths_between_imt_and_sys = PropagationPath.create_default( + self.simulation.system, self.simulation.ue + ) # test the method that calculates interference from IMT UE to FSS space # station self.simulation.calculate_external_interference() diff --git a/tests/test_propagation_abg.py b/tests/test_propagation_abg.py index 44849d956..82b648534 100644 --- a/tests/test_propagation_abg.py +++ b/tests/test_propagation_abg.py @@ -16,41 +16,6 @@ class PropagationABGTest(unittest.TestCase): """Unit tests for the PropagationABG class and its loss calculations.""" - - def setUp(self): - """Set up test fixtures for PropagationABG tests.""" - self.abg = PropagationABG( - random_number_gen=np.random.RandomState(), - alpha=3.4, - beta=19.2, - gamma=2.3, - building_loss=20, - shadowing_sigma_dB=6.5, - ) - - def test_loss(self): - """Test the get_loss method for various distance and frequency inputs.""" - d = np.array([[100, 500], [400, 60]]) - f = np.ones(d.shape, dtype=float) * 27000.0 - indoor = np.zeros(d.shape[0], dtype=bool) - shadowing = False - loss = np.array([[120.121, 143.886347], [140.591406, 112.578509]]) - - npt.assert_allclose( - self.abg.get_loss(d, f, indoor, shadowing), - loss, atol=1e-2, - ) - - d = np.array([500, 3000])[:, np.newaxis] - f = np.array([27000, 40000])[:, np.newaxis] - indoor = np.zeros(d.shape[0], dtype=bool) - shadowing = False - loss = np.array([143.886, 174.269])[:, np.newaxis] - npt.assert_allclose( - self.abg.get_loss(d, f, indoor, shadowing), - loss, atol=1e-2, - ) - def setUp(self): """Set up test fixtures for PropagationABG tests.""" self.abg = PropagationABG( @@ -75,11 +40,11 @@ def test_loss(self): loss, atol=1e-2, ) - d = np.array([500, 3000])[:, np.newaxis] - f = np.array([27000, 40000])[:, np.newaxis] + d = np.array([500, 3000]) + f = np.array([27000, 40000]) indoor = np.zeros(d.shape[0], dtype=bool) shadowing = False - loss = np.array([143.886, 174.269])[:, np.newaxis] + loss = np.array([143.886, 174.269]) npt.assert_allclose( self.abg.get_loss(d, f, indoor, shadowing), loss, atol=1e-2, diff --git a/tests/test_propagation_hdfss_roof_top.py b/tests/test_propagation_hdfss_roof_top.py index a2167c80d..e6185e7b7 100644 --- a/tests/test_propagation_hdfss_roof_top.py +++ b/tests/test_propagation_hdfss_roof_top.py @@ -95,7 +95,7 @@ def setUp(self): def test_get_loss(self): """Test the get_loss method for various roof top scenarios.""" # Not on same building - d = np.array([[10.0, 20.0, 30.0, 60.0, 90.0, 300.0, 1000.0]]) + d = np.array([10.0, 20.0, 30.0, 60.0, 90.0, 300.0, 1000.0]) f = 40000 * np.ones_like(d) ele = np.transpose(np.zeros_like(d)) @@ -107,25 +107,25 @@ def test_get_loss(self): imt_x=100.0 * np.ones(7), imt_y=100.0 * np.ones(7), imt_z=100.0 * np.ones(7), - es_x=np.array([0.0]), - es_y=np.array([0.0]), - es_z=np.array([0.0]), + es_x=np.zeros((7,)), + es_y=np.zeros((7,)), + es_z=np.zeros((7,)), ) loss = loss[0] expected_loss = np.array( - [[84.48, 90.50, 94.02, 100.72, 104.75, 139.33, 162.28]], + [84.48, 90.50, 94.02, 100.72, 104.75, 139.33, 162.28], ) npt.assert_allclose(loss, expected_loss, atol=1e-1) # On same building - d = np.array([[10.0, 20.0, 30.0]]) + d = np.array([10.0, 20.0, 30.0]) f = 40000 * np.ones_like(d) ele = np.transpose(np.zeros_like(d)) - es_x = np.array([0.0]) - es_y = np.array([0.0]) - es_z = np.array([10.0]) + es_x = np.repeat([0.0], 3) + es_y = np.repeat([0.0], 3) + es_z = np.repeat([10.0], 3) imt_x = np.array([0.0, 20.0, 30.0]) imt_y = np.array([10.0, 0.0, 0.0]) imt_z = np.array([1.5, 6.0, 7.5]) @@ -144,14 +144,14 @@ def test_get_loss(self): ) loss = loss[0] - expected_loss = np.array([[150 + 84.48, 100 + 90.50, 50 + 94.02]]) + expected_loss = np.array([150 + 84.48, 100 + 90.50, 50 + 94.02]) npt.assert_allclose(loss, expected_loss, atol=1e-1) def test_get_build_loss(self): """Test get_building_loss for various station types and probabilities.""" # Initialize variables - ele = np.array([[0.0, 45.0, 90.0]]) + ele = np.array([0.0, 45.0, 90.0]) f = 40000 * np.ones_like(ele) sta_type = StationType.IMT_BS @@ -165,7 +165,7 @@ def test_get_build_loss(self): self.assertEqual(build_loss, expected_build_loss) # Test 2: fixed probability - expected_build_loss = np.array([[24.4, 33.9, 43.4]]) + expected_build_loss = np.array([24.4, 33.9, 43.4]) build_loss = self.propagation_fixed_prob.get_building_loss( sta_type, f, @@ -174,7 +174,7 @@ def test_get_build_loss(self): npt.assert_allclose(build_loss, expected_build_loss, atol=1e-1) # Test 3: random probability - expected_build_loss = np.array([[21.7, 32.9, 15.9]]) + expected_build_loss = np.array([21.7, 32.9, 15.9]) build_loss = self.propagation_random_prob.get_building_loss( sta_type, f, @@ -184,7 +184,7 @@ def test_get_build_loss(self): # Test 4: UE station sta_type = StationType.IMT_UE - expected_build_loss = np.array([[21.7, 32.9, 15.9]]) + expected_build_loss = np.array([21.7, 32.9, 15.9]) build_loss = self.propagation_fixed_value.get_building_loss( sta_type, f, @@ -197,7 +197,7 @@ def test_get_build_loss(self): ele, ) npt.assert_allclose(build_loss, expected_build_loss, atol=1e-1) - expected_build_loss = np.array([[10.1, 36.8, 52.6]]) + expected_build_loss = np.array([10.1, 36.8, 52.6]) build_loss = self.propagation_random_prob.get_building_loss( sta_type, f, @@ -208,9 +208,9 @@ def test_get_build_loss(self): def test_same_building(self): """Test is_same_building method for correct building identification and loss.""" # Test is_same_building() - es_x = np.array([0.0]) - es_y = np.array([0.0]) - es_z = np.array([19.0]) + es_x = np.repeat([0.0], 5) + es_y = np.repeat([0.0], 5) + es_z = np.repeat([19.0], 5) imt_x = np.array([1.0, 0.0, 80.0, -70.0, 12.0]) imt_y = np.array([1.0, 30.0, 0.0, -29.3, -3.6]) imt_z = 3 * np.ones_like(imt_x) @@ -226,7 +226,6 @@ def test_same_building(self): # Test loss d = np.sqrt(np.power(imt_x, 2) + np.power(imt_y, 2)) - d = np.array([list(d)]) f = 40000 * np.ones_like(d) ele = np.transpose(np.zeros_like(d)) @@ -243,7 +242,7 @@ def test_same_building(self): es_z=es_z, ) loss = loss[0] - expected_loss = np.array([[4067.5, 94.0, 103.6, 103.1, 4086.5]]) + expected_loss = np.array([4067.5, 94.0, 103.6, 103.1, 4086.5]) npt.assert_allclose(loss, expected_loss, atol=1e-1) diff --git a/tests/test_propagation_path.py b/tests/test_propagation_path.py new file mode 100644 index 000000000..45ebc0a8d --- /dev/null +++ b/tests/test_propagation_path.py @@ -0,0 +1,399 @@ +import unittest +import numpy as np +import numpy.testing as npt + +from sharc.parameters.parameters import Parameters +from sharc.propagation.propagation_path import PropagationPath +from sharc.propagation.propagation_free_space import PropagationFreeSpace +from sharc.propagation.propagation_factory import PropagationFactory +from sharc.station_manager import StationManager +from sharc.support.enumerations import StationType + + +def assert_array_not_equal(x, y): + """Asserts that two arrays are not equal.""" + npt.assert_raises(AssertionError, npt.assert_array_equal, x, y) + +def create_mock_function(ret): + def mock_function(*args, **kwargs): + return ret + return mock_function + +class PropagationPathTest(unittest.TestCase): + """This is more of an integration test since it depends on the parts + it joins. + """ + def setUp(self): + pass + + def test_undeduped_masking_operations(self): + """Testing masking operations when not deduplicating + """ + bs = StationManager(4) + bs.geom.set_global_coords( + np.array([5., 15., 25., 35.]), + np.array([0., 0., 0., 0.]), + np.repeat(10., 4), + ) + ue = StationManager(3) + ue.geom.set_global_coords( + np.array([-5., -5., -25.]), + np.array([0., 0., 0.]), + np.repeat(10., 3), + ) + + path = PropagationPath.create_default(ue, bs) + path.calc_mask(deduplicate=False) + + mtx = np.array([ + [1., 2., 3., 4.], + [5., 6., 7., 8.], + [9., 10., 11., 12.], + ]) + mskd_mtx = path.mtx_to_masked(mtx) + + npt.assert_equal(np.ravel(mtx), mskd_mtx) + + unmskd_mtx = path.from_masked_mtx(mskd_mtx) + npt.assert_equal(mtx, unmskd_mtx) + + vec = np.array([10., 20., 30.]) + vec_cast = path.sta_a_to_masked(vec) + # since paths are ordered by sta_a, + # they are iterated for (0, ...), (1, ...) + # as such it repeats the i-th indice n sta_b times + expected = np.array([ + 10., 10., 10., 10., + 20., 20., 20., 20., + 30., 30., 30., 30., + ]) + npt.assert_array_equal(expected, vec_cast) + + # since paths are ordered by sta_a, + # they are iterated for (0, ...j), (1, ...) + # as such it goes through entire j sequence + # sta_a times + vec = np.array([10., 20., 30., 40.]) + vec_cast = path.sta_b_to_masked(vec) + expected = np.array([ + 10., 20., 30., 40., + 10., 20., 30., 40., + 10., 20., 30., 40., + ]) + npt.assert_array_equal(expected, vec_cast) + + ue.active[0] = False + path.calc_mask(deduplicate=False) + + mskd_mtx = path.mtx_to_masked(mtx) + + npt.assert_equal(np.ravel(mtx[1:]), mskd_mtx) + + unmskd_mtx = path.from_masked_mtx(mskd_mtx) + # first row of mtx should be np.nan + expected = np.copy(mtx) + expected[0, :] = np.nan + + npt.assert_equal(expected, unmskd_mtx) + + vec = np.array([10., 20., 30.]) + vec_cast = path.sta_a_to_masked(vec) + expected = np.array([ + 20., 20., 20., 20., + 30., 30., 30., 30., + ]) + npt.assert_array_equal(expected, vec_cast) + + vec = np.array([10., 20., 30., 40.]) + vec_cast = path.sta_b_to_masked(vec) + expected = np.array([ + 10., 20., 30., 40., + 10., 20., 30., 40., + ]) + npt.assert_array_equal(expected, vec_cast) + + def test_deduped_mask_operations(self): + """Testing masking operations when deduplicating + """ + bs = StationManager(4) + bs.geom.set_global_coords( + np.array([5., 15., 25., 35.]), + np.array([0., 0., 0., 0.]), + np.repeat(10., 4), + ) + ue = StationManager(3) + ue.geom.set_global_coords( + np.array([-5., -5., -25.]), + np.array([0., 0., 0.]), + np.repeat(10., 3), + ) + + """Since zeroth row it represents 1, it should be used for both 0 and 1""" + path = PropagationPath.create_default(ue, bs) + path.calc_mask(deduplicate=True) + + mtx = np.array([ + [1., 2., 3., 4.], + [5., 6., 7., 8.], + [9., 10., 11., 12.], + ]) + mskd_mtx = path.mtx_to_masked(mtx) + expected = np.concatenate((mtx[0], mtx[2])) + # expect reduction in values for path loss calc + npt.assert_equal(expected, mskd_mtx) + + unmskd_mtx = path.from_masked_mtx(mskd_mtx) + # and reverse mapping for deduped values (0) and the rows that need + # those values (0 and 1) + expected = np.concatenate(([mtx[0]], [mtx[0]], [mtx[2]])) + npt.assert_equal(expected, unmskd_mtx) + + vec = np.array([10., 20., 30.]) + vec_cast = path.sta_a_to_masked(vec) + # since paths are ordered by sta_a, + # they are iterated for (0, ...), (1, ...) + # as such it repeats the i-th indice n sta_b times + expected = np.array([ + 10., 10., 10., 10., + 30., 30., 30., 30., + ]) + npt.assert_array_equal(expected, vec_cast) + + # since paths are ordered by sta_a, + # they are iterated for (0, ...j), (1, ...) + # as such it goes through entire j sequence + # sta_a times + vec = np.array([10., 20., 30., 40.]) + vec_cast = path.sta_b_to_masked(vec) + expected = np.array([ + 10., 20., 30., 40., + 10., 20., 30., 40., + ]) + npt.assert_array_equal(expected, vec_cast) + + """Even if 0 is not active, since it represents 1, it should be calc'd""" + ue.active[0] = False + path.calc_mask(deduplicate=True) + + mskd_mtx = path.mtx_to_masked(mtx) + # so its value should be masked + expected = np.concatenate((mtx[0], mtx[2])) + npt.assert_equal(expected, mskd_mtx) + + unmskd_mtx = path.from_masked_mtx(mskd_mtx) + # zeroth row of mtx should be np.nan + expected = np.copy(mtx) + expected[0, :] = np.nan + # and second should come from the masked zeroth + expected[1, :] = mtx[0, :] + + npt.assert_equal(expected, unmskd_mtx) + + def test_get_path_loss_fspl(self): + bs = StationManager(3) + bs.geom.set_global_coords( + np.array([5., 15., 25.]), + np.array([0., 0., 0.]), + np.repeat(10., 3), + ) + ue = StationManager(3) + ue.geom.set_global_coords( + np.array([-5., -15., -25.]), + np.array([0., 0., 0.]), + np.repeat(10., 3), + ) + + path = PropagationPath.create_default(ue, bs) + + fspl = path.get_path_loss( + PropagationFreeSpace(None), + None, + 1e3, # [MHz] + ) + + expected_fspl = PropagationFreeSpace(None).get_free_space_loss( + 1e3, + ue.geom.get_3d_distance_to(bs.geom), + ) + + self.assertEqual(fspl.shape, expected_fspl.shape) + npt.assert_array_equal(fspl,expected_fspl) + + def test_get_path_loss_in_propagations(self): + bs = StationManager(12) + bs.geom.set_global_coords( + np.arange(0., 12., 1.0) * 10, + np.repeat(0., 12), + np.repeat(10., 12), + ) + ue = StationManager(36) + bs.station_type = StationType.IMT_UE + ue.geom.set_global_coords( + np.repeat(np.arange(0., 18., 1.0) * 10, 2), + np.repeat(5., 36), + np.repeat(1.5, 36), + ) + bs.is_space_station = False + for i in range(5, 11): + ue.active[i] = False + + for i in range(20, 23): + ue.active[i] = False + + for i in range(0, 3): + bs.active[i] = False + + path = PropagationPath.create_default(ue, bs) + parameters = Parameters() + parameters.imt.topology.type = "MSS_DC" + parameters.imt.interfered_with = True + gains0 = np.zeros(path._orig_shape) + + bs_w_beams_gains = np.zeros([*path._orig_shape, 3]) + ue.is_space_station = True + for ch_model in [ + "FSPL", + "ABG", + "UMa", + "UMi", + # "SatelliteSimple", + "TerrestrialSimple", + "P619", + "P452", + "TVRO-URBAN", + "TVRO-SUBURBAN", + "HDFSS", + "INDOOR", + ]: + rng = np.random.RandomState(1) + propagation = PropagationFactory.create_propagation( + ch_model, parameters, + parameters.single_earth_station, + rng, + ) + if hasattr(propagation, "_get_atmospheric_gasses_loss"): + # since P619 takes too long calculating this + propagation._get_atmospheric_gasses_loss = create_mock_function(0.0) + + ploss = path.get_path_loss( + propagation, + parameters, + 1e3, # [MHz] + # sta_a_gains=gains0, + sta_a_gains=bs_w_beams_gains, + sta_b_gains=gains0.T, + ) + + if ch_model == "HDFSS": + ploss = ploss[0] + + expected_shape = (ue.num_stations, bs.num_stations) + expected_filled = np.stack(np.where(path._mask), axis=0) + expected_nans = np.stack(np.where(~path._mask), axis=0) + + if ch_model == "INDOOR": + # NOTE: current Indoor channel model implementaiton + # depends on the matrix structure so it does not consider + # the only active paths for path loss calculation + # TODO: update indoor implementation to let that happen + expected_filled = np.stack(np.where(np.ones(expected_shape)), axis=0) + expected_nans = np.stack(np.where(np.zeros(expected_shape)), axis=0) + else: + # checking if the _paths_from_to is representative + npt.assert_array_equal(expected_filled, path._paths_from_to.T) + + self.assertEqual(ploss.shape, expected_shape) + npt.assert_array_equal(ploss[tuple(expected_nans)], np.nan) + assert_array_not_equal(ploss[tuple(expected_filled)], np.nan) + + def test_get_path_loss_single_ss_vs_bs_in_propagations(self): + """Test for the case when a single other station is vs base station + important for code coverage in P.619 (single entry interference) + """ + single_sta = StationManager(1) + single_sta.geom.set_global_coords( + np.array([0.]), + np.array([5.]), + np.array([30.]), + ) + single_sta.is_space_station = True + single_sta.active[0] = True + + bs = StationManager(12) + bs.geom.set_global_coords( + np.arange(0., 12., 1.0) * 10, + np.repeat(0., 12), + np.repeat(10., 12), + ) + bs.station_type = StationType.IMT_UE + bs.is_space_station = False + + for i in range(0, 3): + bs.active[i] = False + + path = PropagationPath.create_default(single_sta, bs) + parameters = Parameters() + parameters.imt.topology.type = "MSS_DC" + parameters.imt.interfered_with = True + gains0 = np.zeros(path._orig_shape) + + bs_w_beams_gains = np.zeros([path._orig_shape[1], path._orig_shape[0], 3]) + for ch_model in [ + "FSPL", + "ABG", + "UMa", + "UMi", + # "SatelliteSimple", + "TerrestrialSimple", + "P619", + "P452", + "TVRO-URBAN", + "TVRO-SUBURBAN", + "HDFSS", + "INDOOR", + ]: + rng = np.random.RandomState(1) + propagation = PropagationFactory.create_propagation( + ch_model, parameters, + parameters.single_earth_station, + rng, + ) + if hasattr(propagation, "_get_atmospheric_gasses_loss"): + # since P619 takes too long calculating this + propagation._get_atmospheric_gasses_loss = create_mock_function(0.0) + + ploss = path.get_path_loss( + propagation, + parameters, + 1e3, # [MHz] + sta_a_gains=gains0, + # sta_b_gains=gains0.T, + sta_b_gains=bs_w_beams_gains, + ) + + if ch_model == "HDFSS": + ploss = ploss[0] + + expected_shape = (single_sta.num_stations, bs.num_stations) + expected_filled = np.stack(np.where(path._mask), axis=0) + expected_nans = np.stack(np.where(~path._mask), axis=0) + + if ch_model == "INDOOR": + # NOTE: current Indoor channel model implementaiton + # depends on the matrix structure so it does not consider + # the only active paths for path loss calculation + # TODO: update indoor implementation to let that happen + expected_filled = np.stack(np.where(np.ones(expected_shape)), axis=0) + expected_nans = np.stack(np.where(np.zeros(expected_shape)), axis=0) + else: + # checking if the _paths_from_to is representative + npt.assert_array_equal(expected_filled, path._paths_from_to.T) + + self.assertEqual(ploss.shape, expected_shape) + npt.assert_array_equal(ploss[tuple(expected_nans)], np.nan) + assert_array_not_equal(ploss[tuple(expected_filled)], np.nan) + + +if __name__ == '__main__': + # unittest.main(module="tests.test_propagation_path") + unittest.main() diff --git a/tests/test_propagation_uma.py b/tests/test_propagation_uma.py index efedf64b7..f88f9662a 100644 --- a/tests/test_propagation_uma.py +++ b/tests/test_propagation_uma.py @@ -24,12 +24,12 @@ def test_los_probability(self): distance_2D = np.array([ [10, 15, 40], [17, 60, 80], - ]) - h_ue = np.array([1.5, 8, 15]) + ]).flatten() + h_ue = np.repeat([1.5, 8, 15], 2) los_probability = np.array([ [1, 1, 0.74], [1, 0.57, 0.45], - ]) + ]).flatten() npt.assert_allclose( self.uma.get_los_probability(distance_2D, h_ue), los_probability, @@ -38,14 +38,14 @@ def test_los_probability(self): def test_breakpoint_distance(self): """Test the calculation of breakpoint distance for UMa scenario.""" - h_bs = np.array([15, 20, 25, 30]) - h_ue = np.array([3, 4]) - h_e = np.ones((h_ue.size, h_bs.size)) + h_bs = np.tile([15, 20, 25, 30], 2) + h_ue = np.repeat([3, 4], 4) + h_e = np.ones((h_ue.size,)) frequency = 30000 * np.ones(h_e.shape) breakpoint_distance = np.array([ [11200, 15200, 19200, 23200], [16800, 22800, 28800, 34800], - ]) + ]).flatten() npt.assert_array_equal( self.uma.get_breakpoint_distance(frequency, h_bs, h_ue, h_e), breakpoint_distance, @@ -58,11 +58,11 @@ def test_loss_los(self): [200, 600], [300, 700], [400, 800], - ]) - h_bs = np.array([30, 35]) - h_ue = np.array([2, 3, 4, 5]) + ]).flatten() + h_bs = np.tile([30, 35], 4) + h_ue = np.repeat([2, 3, 4, 5], 2) h_e = np.ones(distance_2D.shape) - distance_3D = np.sqrt(distance_2D**2 + (h_bs - h_ue[:, np.newaxis])**2) + distance_3D = np.sqrt(distance_2D**2 + (h_bs - h_ue)**2) frequency = 30000 * np.ones(distance_2D.shape) shadowing_std = 0 loss = np.array([ @@ -70,7 +70,7 @@ def test_loss_los(self): [108.09, 117.56], [111.56, 118.90], [114.05, 120.06], - ]) + ]).flatten() npt.assert_allclose( self.uma.get_loss_los( distance_2D, distance_3D, frequency, @@ -85,11 +85,11 @@ def test_loss_los(self): [200, 600], [300, 700], [400, 800], - ]) - h_bs = np.array([30, 35]) - h_ue = np.array([2, 3, 4, 5]) + ]).flatten() + h_bs = np.tile([30, 35], 4) + h_ue = np.repeat([2, 3, 4, 5], 2) h_e = np.ones(distance_2D.shape) - distance_3D = np.sqrt(distance_2D**2 + (h_bs - h_ue[:, np.newaxis])**2) + distance_3D = np.sqrt(distance_2D**2 + (h_bs - h_ue)**2) frequency = 300 * np.ones(distance_2D.shape) shadowing_std = 0 loss = np.array([ @@ -97,7 +97,7 @@ def test_loss_los(self): [68.09, 84.39], [71.56, 83.57], [74.05, 83.40], - ]) + ]).flatten() npt.assert_allclose( self.uma.get_loss_los( distance_2D, distance_3D, frequency, @@ -114,11 +114,11 @@ def test_loss_nlos(self): [200, 600], [300, 700], [400, 800], - ]) - h_bs = np.array([30, 35]) - h_ue = np.array([2, 3, 4, 5]) + ]).flatten() + h_bs = np.tile([30, 35], 4) + h_ue = np.repeat([2, 3, 4, 5], 2) h_e = np.ones(distance_2D.shape) - distance_3D = np.sqrt(distance_2D**2 + (h_bs - h_ue[:, np.newaxis])**2) + distance_3D = np.sqrt(distance_2D**2 + (h_bs - h_ue)**2) frequency = 30000 * np.ones(distance_2D.shape) shadowing_std = 0 loss = np.array([ @@ -126,7 +126,7 @@ def test_loss_nlos(self): [132.25, 150.77], [138.45, 152.78], [142.70, 154.44], - ]) + ]).flatten() npt.assert_allclose( self.uma.get_loss_nlos( distance_2D, distance_3D, frequency, @@ -141,11 +141,11 @@ def test_loss_nlos(self): [2000, 6000], [5000, 7000], [4000, 8000], - ]) - h_bs = np.array([30, 35]) - h_ue = np.array([2, 3, 4, 5]) + ]).flatten() + h_bs = np.tile([30, 35], 4) + h_ue = np.repeat([2, 3, 4, 5], 2) h_e = np.ones(distance_2D.shape) - distance_3D = np.sqrt(distance_2D**2 + (h_bs - h_ue[:, np.newaxis])**2) + distance_3D = np.sqrt(distance_2D**2 + (h_bs - h_ue)**2) frequency = 300 * np.ones(distance_2D.shape) shadowing_std = 0 loss = np.array([ @@ -153,7 +153,7 @@ def test_loss_nlos(self): [131.18, 149.83], [146.13, 151.84], [141.75, 153.51], - ]) + ]).flatten() npt.assert_allclose( self.uma.get_loss_nlos( distance_2D, distance_3D, frequency, diff --git a/tests/test_propagation_umi.py b/tests/test_propagation_umi.py index af598453f..ee04a5546 100644 --- a/tests/test_propagation_umi.py +++ b/tests/test_propagation_umi.py @@ -29,11 +29,11 @@ def test_los_probability(self): distance_2D = np.array([ [10, 15, 40], [17, 60, 80], - ]) + ]).flatten() los_probability = np.array([ [1, 1, 0.631], [1, 0.432, 0.308], - ]) + ]).flatten() npt.assert_allclose( self.umi.get_los_probability( distance_2D, @@ -45,14 +45,14 @@ def test_los_probability(self): def test_breakpoint_distance(self): """Test the calculation of breakpoint distance for UMi scenario.""" - h_bs = np.array([15, 20, 25, 30]) - h_ue = np.array([3, 4]) - h_e = np.ones((h_ue.size, h_bs.size)) + h_bs = np.tile([15, 20, 25, 30], 2) + h_ue = np.repeat([3, 4], 4) + h_e = np.ones((h_ue.size)) frequency = 30000 * np.ones(h_e.shape) breakpoint_distance = np.array([ [11200, 15200, 19200, 23200], [16800, 22800, 28800, 34800], - ]) + ]).flatten() npt.assert_array_equal( self.umi.get_breakpoint_distance(frequency, h_bs, h_ue, h_e), breakpoint_distance, @@ -65,11 +65,11 @@ def test_loss_los(self): [200, 600], [300, 700], [400, 800], - ]) - h_bs = np.array([30, 35]) - h_ue = np.array([2, 3, 4, 5]) + ]).flatten() + h_bs = np.tile([30, 35], 4) + h_ue = np.repeat([2, 3, 4, 5], 2) h_e = np.ones(distance_2D.shape) - distance_3D = np.sqrt(distance_2D**2 + (h_bs - h_ue[:, np.newaxis])**2) + distance_3D = np.sqrt(distance_2D**2 + (h_bs - h_ue)**2) frequency = 30000 * np.ones(distance_2D.shape) shadowing_std = 0 loss = np.array([ @@ -77,7 +77,7 @@ def test_loss_los(self): [110.396, 120.346], [114.046, 121.748], [116.653, 122.963], - ]) + ]).flatten() npt.assert_allclose( self.umi.get_loss_los( distance_2D, distance_3D, frequency, @@ -92,11 +92,11 @@ def test_loss_los(self): [200, 600], [300, 700], [400, 800], - ]) - h_bs = np.array([30, 35]) - h_ue = np.array([2, 3, 4, 5]) + ]).flatten() + h_bs = np.tile([30, 35], 4) + h_ue = np.repeat([2, 3, 4, 5], 2) h_e = np.ones(distance_2D.shape) - distance_3D = np.sqrt(distance_2D**2 + (h_bs - h_ue[:, np.newaxis])**2) + distance_3D = np.sqrt(distance_2D**2 + (h_bs - h_ue)**2) frequency = 300 * np.ones(distance_2D.shape) shadowing_std = 0 loss = np.array([ @@ -104,7 +104,7 @@ def test_loss_los(self): [70.396, 86.829], [74.046, 86.187], [76.653, 86.139], - ]) + ]).flatten() npt.assert_allclose( self.umi.get_loss_los( distance_2D, distance_3D, frequency, @@ -121,19 +121,21 @@ def test_loss_nlos(self): [200, 600], [300, 700], [400, 800], - ]) - h_bs = np.array([30, 35]) - h_ue = np.array([2, 3, 4, 5]) - h_e = np.ones(distance_2D.shape) - distance_3D = np.sqrt(distance_2D**2 + (h_bs - h_ue[:, np.newaxis])**2) - frequency = 30000 * np.ones(distance_2D.shape) + ]).flatten() + h_bs = np.tile([30, 35], 4) + h_ue = np.repeat([2, 3, 4, 5], 2) + h_e = np.ones((distance_2D.size,)) + distance_3D = np.sqrt(distance_2D**2 + (h_bs - h_ue)**2) + frequency = 30000 * np.ones(distance_3D.shape) shadowing_std = 0 loss = np.array([ [128.84, 152.96], [138.72, 155.45], [144.56, 157.50], [148.64, 159.25], - ]) + ]).flatten() + distance_2D = distance_2D.flatten() + npt.assert_allclose( self.umi.get_loss_nlos( distance_2D, distance_3D, frequency, @@ -148,11 +150,11 @@ def test_loss_nlos(self): [2000, 6000], [5000, 7000], [4000, 8000], - ]) - h_bs = np.array([30, 35]) - h_ue = np.array([2, 3, 4, 5]) + ]).flatten() + h_bs = np.tile([30, 35], 4) + h_ue = np.repeat([2, 3, 4, 5], 2) h_e = np.ones(distance_2D.shape) - distance_3D = np.sqrt(distance_2D**2 + (h_bs - h_ue[:, np.newaxis])**2) + distance_3D = np.sqrt(distance_2D**2 + (h_bs - h_ue)**2) frequency = 300 * np.ones(distance_2D.shape) shadowing_std = 0 loss = np.array([ @@ -160,7 +162,7 @@ def test_loss_nlos(self): [131.29, 148.13], [145.03, 150.19], [141.31, 151.94], - ]) + ]).flatten() npt.assert_allclose( self.umi.get_loss_nlos( distance_2D, distance_3D, frequency, diff --git a/tests/test_simulation_downlink.py b/tests/test_simulation_downlink.py index 3073daddf..4b351eb12 100644 --- a/tests/test_simulation_downlink.py +++ b/tests/test_simulation_downlink.py @@ -15,6 +15,7 @@ from sharc.antenna.antenna_omni import AntennaOmni from sharc.station_factory import StationFactory from sharc.propagation.propagation_factory import PropagationFactory +from sharc.propagation.propagation_path import PropagationPath from sharc.parameters.imt.parameters_imt_topology import ParametersImtTopology from sharc.parameters.imt.parameters_single_bs import ParametersSingleBS @@ -241,6 +242,10 @@ def test_simulation_2bs_4ue_single_space_station(self): random_number_gen, ) + self.simulation.intra_imt_paths = PropagationPath.create_default( + self.simulation.ue, self.simulation.bs + ) + # test coupling loss method self.simulation.coupling_loss_imt = self.simulation.calculate_intra_imt_coupling_loss( self.simulation.ue, self.simulation.bs, ) @@ -346,6 +351,9 @@ def test_simulation_2bs_4ue_single_space_station(self): np.array([self.param.single_space_station.geometry.altitude]), ) + self.simulation.paths_between_imt_and_sys = PropagationPath.create_default( + self.simulation.system, self.simulation.bs + ) # test the method that calculates interference from IMT UE to FSS space # station self.simulation.calculate_external_interference() @@ -439,6 +447,9 @@ def test_simulation_2bs_4ue_fss_es(self): self.simulation.param_system, random_number_gen, ) + self.simulation.intra_imt_paths = PropagationPath.create_default( + self.simulation.ue, self.simulation.bs + ) self.simulation.connect_ue_to_bs() self.simulation.select_ue(random_number_gen) @@ -544,6 +555,9 @@ def test_simulation_2bs_4ue_fss_es(self): self.simulation.param_system, random_number_gen, ) + self.simulation.paths_between_imt_and_sys = PropagationPath.create_default( + self.simulation.system, self.simulation.ue + ) # what if FSS ES is the interferer? self.simulation.calculate_sinr_ext() @@ -585,6 +599,9 @@ def test_simulation_2bs_4ue_fss_es(self): atol=1e-2, ) + self.simulation.paths_between_imt_and_sys = PropagationPath.create_default( + self.simulation.system, self.simulation.bs + ) # what if IMT is interferer? self.simulation.calculate_external_interference() @@ -670,6 +687,9 @@ def test_simulation_2bs_4ue_ras(self): self.simulation.param_system, random_number_gen, ) + self.simulation.intra_imt_paths = PropagationPath.create_default( + self.simulation.ue, self.simulation.bs + ) self.simulation.connect_ue_to_bs() self.simulation.select_ue(random_number_gen) @@ -717,6 +737,9 @@ def test_simulation_2bs_4ue_ras(self): ) npt.assert_equal(gains, np.array([[50, 50]])) + self.simulation.paths_between_imt_and_sys = PropagationPath.create_default( + self.simulation.system, self.simulation.bs + ) self.simulation.calculate_external_interference() polarization_loss = 3 diff --git a/tests/test_simulation_downlink_haps.py b/tests/test_simulation_downlink_haps.py index ad5dc2623..11891abf3 100644 --- a/tests/test_simulation_downlink_haps.py +++ b/tests/test_simulation_downlink_haps.py @@ -15,6 +15,7 @@ from sharc.antenna.antenna_omni import AntennaOmni from sharc.station_factory import StationFactory from sharc.propagation.propagation_factory import PropagationFactory +from sharc.propagation.propagation_path import PropagationPath from sharc.parameters.imt.parameters_imt_topology import ParametersImtTopology from sharc.parameters.imt.parameters_single_bs import ParametersSingleBS @@ -193,6 +194,9 @@ def test_simulation_2bs_4ue_1haps(self): self.simulation.connect_ue_to_bs() self.simulation.select_ue(random_number_gen) self.simulation.link = {0: [0, 1], 1: [2, 3]} + self.simulation.intra_imt_paths = PropagationPath.create_default( + self.simulation.ue, self.simulation.bs + ) self.simulation.coupling_loss_imt = \ self.simulation.calculate_intra_imt_coupling_loss( self.simulation.ue, @@ -266,6 +270,9 @@ def test_simulation_2bs_4ue_1haps(self): self.param.haps, 0, random_number_gen, ) + self.simulation.paths_between_imt_and_sys = PropagationPath.create_default( + self.simulation.system, self.simulation.ue + ) # now we evaluate interference from HAPS to IMT UE self.simulation.calculate_sinr_ext() diff --git a/tests/test_simulation_downlink_tvro.py b/tests/test_simulation_downlink_tvro.py index 99dc031c2..50dd64614 100644 --- a/tests/test_simulation_downlink_tvro.py +++ b/tests/test_simulation_downlink_tvro.py @@ -14,6 +14,7 @@ from sharc.parameters.parameters import Parameters from sharc.station_factory import StationFactory from sharc.propagation.propagation_factory import PropagationFactory +from sharc.propagation.propagation_path import PropagationPath class SimulationDownlinkTvroTest(unittest.TestCase): @@ -189,6 +190,9 @@ def test_simulation_1bs_1ue_tvro(self): self.param, random_number_gen, ) + self.simulation.intra_imt_paths = PropagationPath.create_default( + self.simulation.ue, self.simulation.bs + ) # test coupling loss method self.simulation.coupling_loss_imt = self.simulation.calculate_intra_imt_coupling_loss( self.simulation.ue, self.simulation.bs, ) @@ -294,6 +298,9 @@ def test_simulation_1bs_1ue_tvro(self): np.array([0.0]), ) + self.simulation.paths_between_imt_and_sys = PropagationPath.create_default( + self.simulation.system, self.simulation.bs + ) # test the method that calculates interference from IMT UE to FSS space # station self.simulation.calculate_external_interference() diff --git a/tests/test_simulation_uplink.py b/tests/test_simulation_uplink.py index 51fbd1ed4..b2f06626e 100644 --- a/tests/test_simulation_uplink.py +++ b/tests/test_simulation_uplink.py @@ -15,6 +15,7 @@ from sharc.antenna.antenna_beamforming_imt import AntennaBeamformingImt from sharc.station_factory import StationFactory from sharc.propagation.propagation_factory import PropagationFactory +from sharc.propagation.propagation_path import PropagationPath class SimulationUplinkTest(unittest.TestCase): @@ -232,6 +233,9 @@ def test_simulation_2bs_4ue_ss(self): random_number_gen, ) + self.simulation.intra_imt_paths = PropagationPath.create_default( + self.simulation.ue, self.simulation.bs + ) # test coupling loss method self.simulation.coupling_loss_imt = self.simulation.calculate_intra_imt_coupling_loss( self.simulation.ue, self.simulation.bs, ) @@ -370,6 +374,9 @@ def test_simulation_2bs_4ue_ss(self): np.array([0]), np.array([self.param.fss_ss.altitude]), ) + self.simulation.paths_between_imt_and_sys = PropagationPath.create_default( + self.simulation.system, self.simulation.ue + ) # test the method that calculates interference from IMT UE to FSS space # station @@ -463,6 +470,9 @@ def test_simulation_2bs_4ue_es(self): random_number_gen, ) + self.simulation.intra_imt_paths = PropagationPath.create_default( + self.simulation.ue, self.simulation.bs + ) # test coupling loss method self.simulation.coupling_loss_imt = self.simulation.calculate_intra_imt_coupling_loss( self.simulation.ue, self.simulation.bs, ) @@ -592,6 +602,10 @@ def test_simulation_2bs_4ue_es(self): self.param.fss_es, random_number_gen, ) + self.simulation.paths_between_imt_and_sys = PropagationPath.create_default( + self.simulation.system, self.simulation.bs + ) + # what if FSS ES is interferer??? self.simulation.calculate_sinr_ext() @@ -660,6 +674,9 @@ def test_simulation_2bs_4ue_es(self): atol=1e-2, ) + self.simulation.paths_between_imt_and_sys = PropagationPath.create_default( + self.simulation.system, self.simulation.ue + ) # what if IMT is interferer? self.simulation.calculate_external_interference() @@ -751,6 +768,9 @@ def test_simulation_2bs_4ue_ras(self): self.simulation.param_system, random_number_gen, ) + self.simulation.intra_imt_paths = PropagationPath.create_default( + self.simulation.ue, self.simulation.bs + ) # test coupling loss method self.simulation.coupling_loss_imt = self.simulation.calculate_intra_imt_coupling_loss( @@ -791,6 +811,9 @@ def test_simulation_2bs_4ue_ras(self): np.array([0]), np.array([self.param.ras.geometry.height]), ) + self.simulation.paths_between_imt_and_sys = PropagationPath.create_default( + self.simulation.system, self.simulation.ue + ) self.simulation.system.antenna[0].effective_area = 54.9779 # Test gain calculation From 6ace23e193af943ef87eb7bf00185bef0ac0cc6c Mon Sep 17 00:00:00 2001 From: artistrea Date: Wed, 26 Nov 2025 17:33:44 -0300 Subject: [PATCH 44/77] update: interference paths consider only actual paths --- sharc/simulation.py | 35 +++ sharc/simulation_downlink.py | 408 ++++++++++++++++------------------- sharc/simulation_uplink.py | 292 ++++++++++++------------- 3 files changed, 358 insertions(+), 377 deletions(-) diff --git a/sharc/simulation.py b/sharc/simulation.py index 5800dfc6f..ca6cc04da 100644 --- a/sharc/simulation.py +++ b/sharc/simulation.py @@ -12,6 +12,7 @@ import math import sys import matplotlib.pyplot as plt +import typing from sharc.support.enumerations import StationType from sharc.topology.topology_factory import TopologyFactory @@ -805,6 +806,40 @@ def plot_scenario(self): # sys.exit(0) + def add_system_imt_interaction_attr_to_results( + self, + link: typing.Literal["DL", "UL"], + attr: str | object, + result_attr: str | None = None, + result_obj: typing.Any = None, + ): + if isinstance(attr, str): + v = np.array(getattr(self, attr)) + else: + v = attr + + if result_obj is None: + result_obj = self.results + if result_attr is None: + result_attr = attr + + sys_to_imt_paths_mask = self.paths_between_imt_and_sys._mask + n_sys = self.system.num_stations + + assert v.shape[0] == n_sys + if ( + not self.parameters.imt.interfered_with and link == "DL" + or self.parameters.imt.interfered_with and link == "UL" + ): + n_bs = self.bs.num_stations + ue_k = self.parameters.imt.ue.k + v = v.reshape( + n_sys, n_bs, ue_k + ) + v = v[sys_to_imt_paths_mask] + + getattr(result_obj, result_attr).extend(v.flatten()) + @abstractmethod def snapshot(self, *args, **kwargs): """ diff --git a/sharc/simulation_downlink.py b/sharc/simulation_downlink.py index 65721f4a6..8673c8b29 100644 --- a/sharc/simulation_downlink.py +++ b/sharc/simulation_downlink.py @@ -172,22 +172,22 @@ def calculate_sinr(self): Calculates the downlink SINR for each UE. """ bs_active = np.where(self.bs.active)[0] + ue_to_bs_path_mask = self.intra_imt_paths._mask + bs_tx_power_array = np.stack([self.bs.tx_power[k] for k in sorted(self.bs.tx_power)]).flatten() + for bs in bs_active: ue = self.link[bs] self.ue.rx_power[ue] = self.bs.tx_power[bs] - \ self.coupling_loss_imt[bs, ue] - # create a list with base stations that generate interference in - # ue_list - bs_interf = [b for b in bs_active if b not in [bs]] + for ui in ue: + bs_interf = np.where(ue_to_bs_path_mask[ui])[0] + bs_interf = [bsi for bsi in bs_interf if bsi != bs] - # calculate intra system interference - for bi in bs_interf: - interference = self.bs.tx_power[bi] - \ - self.coupling_loss_imt[bi, ue] + interference = bs_tx_power_array[bs_interf] - \ + self.coupling_loss_imt[bs_interf, ui] - self.ue.rx_interference[ue] = 10 * np.log10(np.power( - 10, 0.1 * self.ue.rx_interference[ue]) + np.power(10, 0.1 * interference), ) + self.ue.rx_interference[ui] = 10 * np.log10(np.sum(np.power(10, 0.1 * interference))) # Thermal noise in dBm self.ue.thermal_noise = \ @@ -227,170 +227,168 @@ def calculate_sinr_ext(self): # applying a bandwidth scaling factor since UE transmits on a portion # of the satellite's bandwidth - active_sys = np.where(self.system.active)[0] + ue_interferer_paths = self.paths_between_imt_and_sys._mask.T # All UEs are active on an active BS bs_active = np.where(self.bs.active)[0] for bs in bs_active: - ue = self.link[bs] - - # Get the weight factor for the system overlaping bandwidth in each - # UE band. - weights = self.calculate_bw_weights( - self.ue.bandwidth[ue], - self.ue.center_freq[ue], - float(self.param_system.bandwidth), - float(self.param_system.frequency), - ) + for ue in self.link[bs]: + # NOTE: ue is a scalar + system_interfering = np.where(ue_interferer_paths[ue])[0] + + # Get the weight factor for the system overlaping bandwidth in each + # UE band. + weights = self.calculate_bw_weights( + self.ue.bandwidth[ue], + self.ue.center_freq[ue], + float(self.param_system.bandwidth), + float(self.param_system.frequency), + ) - in_band_interf_power = -500. - if self.co_channel: - # Inteferer transmit power in dBm over the overlapping band - # (MHz) with UEs. - if self.overlapping_bandwidth > 0: - # in_band_interf_power = self.param_system.tx_power_density + \ - # 10 * np.log10(self.overlapping_bandwidth * 1e6) + 30 - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", - category=RuntimeWarning, - message="divide by zero encountered in log10", - ) - in_band_interf_power = \ - self.param_system.tx_power_density + 10 * np.log10( - self.ue.bandwidth[ue, np.newaxis] * 1e6 - ) + 10 * np.log10(weights)[:, np.newaxis] - \ - self.coupling_loss_imt_system[ue, :][:, active_sys] - - oob_power = np.resize(-500., (len(ue), 1)) - if self.adjacent_channel: - # emissions outside of tx bandwidth and inside of rx bw - # due to oob emissions on tx side - tx_oob = np.resize(-500., len(ue)) - - # emissions outside of rx bw and inside of tx bw - # due to non ideal filtering on rx side - # will be the same for all UE's, only considering - rx_oob = np.resize(-500., len(ue)) - - # TODO: M.2101 states that: - # "The ACIR value should be calculated based on per UE allocated number of resource blocks" - - # should we actually implement that for ACS since the receiving - # filter is fixed? - - # or maybe ignore ACS altogether (ACS = inf)? If we consider only allocated RB, it makes - # no sense to use ACS. - # At the same time, ignoring ACS doesn't seem correct since the interference - # could DECREASE when it would make sense for it to increase. - # e.g. adjacent systems -> slightly co-channel with ACS = inf - # should interfer ^ less than this ^ - - if self.parameters.imt.adjacent_ch_reception == "ACS": - if self.overlapping_bandwidth: - if getattr(self, "_acs_warned"): - warn( - "You're trying to use ACS on a partially overlapping band " - "with UEs.\n\tVerify the code implements the behavior you expect!!" + in_band_interf_power = -500. + if self.co_channel: + # Inteferer transmit power in dBm over the overlapping band + # (MHz) with UEs. + if self.overlapping_bandwidth > 0: + # in_band_interf_power = self.param_system.tx_power_density + \ + # 10 * np.log10(self.overlapping_bandwidth * 1e6) + 30 + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + category=RuntimeWarning, + message="divide by zero encountered in log10", ) - self._acs_warned = True - non_overlap_sys_bw = self.param_system.bandwidth - self.overlapping_bandwidth - acs_dB = self.parameters.imt.ue.adjacent_ch_selectivity - - # NOTE: only the power not overlapping is attenuated by ACS - # tx_pow_adj_lin = PSD * non_overlap_imt_bw - # rx_oob = tx_pow_adj_lin / acs - rx_oob[::] = self.param_system.tx_power_density + 10 * np.log10(non_overlap_sys_bw * 1e6) - acs_dB - elif self.parameters.imt.adjacent_ch_reception == "OFF": - pass - else: - raise ValueError( - f"No implementation for parameters.imt.adjacent_ch_reception == { - self.parameters.imt.adjacent_ch_reception}") - - # for tx oob we accept ACLR and spectral mask - if self.param_system.adjacent_ch_emissions == "SPECTRAL_MASK": - ue_bws = self.ue.bandwidth[ue] - center_freqs = self.ue.center_freq[ue] - - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", - category=RuntimeWarning, - message="divide by zero encountered in log10") - for i, center_freq, bw in zip( - range(len(center_freqs)), center_freqs, ue_bws): - # calculate tx emissions in UE in use bandwidth only - # [dB] - tx_oob[i] = self.system.spectral_mask.power_calc( - center_freq, - bw + in_band_interf_power = \ + self.param_system.tx_power_density + 10 * np.log10( + self.ue.bandwidth[ue, np.newaxis] * 1e6 + ) + 10 * np.log10(weights) - \ + self.coupling_loss_imt_system[ue, :][system_interfering] + + oob_power = np.resize(-500., (1, 1)) + if self.adjacent_channel: + # emissions outside of tx bandwidth and inside of rx bw + # due to oob emissions on tx side + tx_oob = np.resize(-500., 1) + + # emissions outside of rx bw and inside of tx bw + # due to non ideal filtering on rx side + # will be the same for all UE's, only considering + rx_oob = np.resize(-500., 1) + + # TODO: M.2101 states that: + # "The ACIR value should be calculated based on per UE allocated number of resource blocks" + + # should we actually implement that for ACS since the receiving + # filter is fixed? + + # or maybe ignore ACS altogether (ACS = inf)? If we consider only allocated RB, it makes + # no sense to use ACS. + # At the same time, ignoring ACS doesn't seem correct since the interference + # could DECREASE when it would make sense for it to increase. + # e.g. adjacent systems -> slightly co-channel with ACS = inf + # should interfer ^ less than this ^ + + if self.parameters.imt.adjacent_ch_reception == "ACS": + if self.overlapping_bandwidth: + if getattr(self, "_acs_warned"): + warn( + "You're trying to use ACS on a partially overlapping band " + "with UEs.\n\tVerify the code implements the behavior you expect!!" + ) + self._acs_warned = True + non_overlap_sys_bw = self.param_system.bandwidth - self.overlapping_bandwidth + acs_dB = self.parameters.imt.ue.adjacent_ch_selectivity + + # NOTE: only the power not overlapping is attenuated by ACS + # tx_pow_adj_lin = PSD * non_overlap_imt_bw + # rx_oob = tx_pow_adj_lin / acs + rx_oob[::] = self.param_system.tx_power_density + 10 * np.log10(non_overlap_sys_bw * 1e6) - acs_dB + elif self.parameters.imt.adjacent_ch_reception == "OFF": + pass + else: + raise ValueError( + f"No implementation for parameters.imt.adjacent_ch_reception == { + self.parameters.imt.adjacent_ch_reception}") + + # for tx oob we accept ACLR and spectral mask + if self.param_system.adjacent_ch_emissions == "SPECTRAL_MASK": + ue_bws = self.ue.bandwidth[ue] + center_freqs = self.ue.center_freq[ue] + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", + category=RuntimeWarning, + message="divide by zero encountered in log10") + tx_oob[0] = self.system.spectral_mask.power_calc( + center_freqs, + ue_bws ) - 30 - elif self.param_system.adjacent_ch_emissions == "ACLR": - # consider ACLR only over non co-channel RBs - # This should diminish some of the ACLR interference - # in a way that make sense - non_overlap_imt_bw = self.ue.bandwidth[ue] * (1. - weights) - # NOTE: approximated equal to IMT bw - measurement_bw = self.param_system.bandwidth - aclr_dB = self.param_system.adjacent_ch_leak_ratio - - if self.parameters.imt.bandwidth - self.overlapping_bandwidth > measurement_bw: - # NOTE: ACLR defines total leaked power over a fixed measurement bandwidth. - # If the victim bandwidth is wider, you’re assuming the same leakage - # profile extends beyond the ACLR-defined region, which may overestimate interference - # FIXME: if the victim bw fully contains tx bw, then - # EACH region should be <= measurement_bw - warn( - "Using System ACLR into IMT, but ACLR measurement bw is " - f"{measurement_bw} while the IMT bw is bigger ({self.parameters.imt.bandwidth}).\n" - "Are you sure you intend to apply the same ACLR to the entire IMT bw?" - ) - - # tx_oob_in_measurement = (tx_pow_lin / aclr) - # => approx. PSD = (tx_pow_lin / aclr) / measurement_bw - # approximated received tx_oob = PSD * non_overlap_imt_bw - tx_oob[::] = self.param_system.tx_power_density + \ - 10 * np.log10(1e6) - \ - aclr_dB + 10 * np.log10( - non_overlap_imt_bw) - elif self.param_system.adjacent_ch_emissions == "OFF": - pass - else: - raise ValueError( - f"No implementation for param_system.adjacent_ch_emissions == { - self.param_system.adjacent_ch_emissions}") - - if self.param_system.adjacent_ch_emissions != "OFF": - tx_oob = tx_oob[:, np.newaxis] - self.coupling_loss_imt_system[ue, :][:, active_sys] - - rx_oob = rx_oob[:, np.newaxis] - self.coupling_loss_imt_system_adjacent[ue, :][:, active_sys] + elif self.param_system.adjacent_ch_emissions == "ACLR": + # consider ACLR only over non co-channel RBs + # This should diminish some of the ACLR interference + # in a way that make sense + non_overlap_imt_bw = self.ue.bandwidth[ue] * (1. - weights) + # NOTE: approximated equal to IMT bw + measurement_bw = self.param_system.bandwidth + aclr_dB = self.param_system.adjacent_ch_leak_ratio + + if self.parameters.imt.bandwidth - self.overlapping_bandwidth > measurement_bw: + # NOTE: ACLR defines total leaked power over a fixed measurement bandwidth. + # If the victim bandwidth is wider, you’re assuming the same leakage + # profile extends beyond the ACLR-defined region, which may overestimate interference + # FIXME: if the victim bw fully contains tx bw, then + # EACH region should be <= measurement_bw + warn( + "Using System ACLR into IMT, but ACLR measurement bw is " + f"{measurement_bw} while the IMT bw is bigger ({self.parameters.imt.bandwidth}).\n" + "Are you sure you intend to apply the same ACLR to the entire IMT bw?" + ) - # Out of band power - # sum linearly power leaked into band and power received in the - # adjacent band - oob_power = 10 * np.log10( - 10 ** (0.1 * tx_oob) + 10 ** (0.1 * rx_oob) - ) - # Total external interference into the UE in dBm - ue_ext_int = 10 * np.log10(np.power(10, - 0.1 * in_band_interf_power) + np.power(10, - 0.1 * oob_power)) + # tx_oob_in_measurement = (tx_pow_lin / aclr) + # => approx. PSD = (tx_pow_lin / aclr) / measurement_bw + # approximated received tx_oob = PSD * non_overlap_imt_bw + tx_oob[::] = self.param_system.tx_power_density + \ + 10 * np.log10(1e6) - \ + aclr_dB + 10 * np.log10( + non_overlap_imt_bw) + elif self.param_system.adjacent_ch_emissions == "OFF": + pass + else: + raise ValueError( + f"No implementation for param_system.adjacent_ch_emissions == { + self.param_system.adjacent_ch_emissions}") + + if self.param_system.adjacent_ch_emissions != "OFF": + tx_oob = tx_oob[:] - self.coupling_loss_imt_system[ue, :][system_interfering] + + rx_oob = rx_oob[:] - self.coupling_loss_imt_system_adjacent[ue, :][system_interfering] + + # Out of band power + # sum linearly power leaked into band and power received in the + # adjacent band + oob_power = 10 * np.log10( + 10 ** (0.1 * tx_oob) + 10 ** (0.1 * rx_oob) + ) + # Total external interference into the UE in dBm + ue_ext_int = 10 * np.log10(np.power(10, + 0.1 * in_band_interf_power) + np.power(10, + 0.1 * oob_power)) - # Sum all the interferers for each UE - self.ue.ext_interference[ue] = 10 * \ - np.log10(np.sum(np.power(10, 0.1 * ue_ext_int), axis=1)) + 30 + # Sum all the interferers for each UE + self.ue.ext_interference[ue] = 10 * \ + np.log10(np.sum(np.power(10, 0.1 * ue_ext_int), axis=0)) + 30 - self.ue.sinr_ext[ue] = \ - self.ue.rx_power[ue] - (10 * np.log10(np.power(10, 0.1 * self.ue.total_interference[ue]) + - np.power(10, 0.1 * (self.ue.ext_interference[ue])))) + self.ue.sinr_ext[ue] = \ + self.ue.rx_power[ue] - (10 * np.log10(np.power(10, 0.1 * self.ue.total_interference[ue]) + + np.power(10, 0.1 * (self.ue.ext_interference[ue])))) - # Calculate INR in dB - self.ue.thermal_noise[ue] = \ - 10 * np.log10(BOLTZMANN_CONSTANT * self.parameters.imt.noise_temperature * 1e3) + \ - 10 * np.log10(self.ue.bandwidth[ue] * 1e6) + self.parameters.imt.ue.noise_figure + # Calculate INR in dB + self.ue.thermal_noise[ue] = \ + 10 * np.log10(BOLTZMANN_CONSTANT * self.parameters.imt.noise_temperature * 1e3) + \ + 10 * np.log10(self.ue.bandwidth[ue] * 1e6) + self.parameters.imt.ue.noise_figure - self.ue.inr[ue] = self.ue.ext_interference[ue] - \ - self.ue.thermal_noise[ue] + self.ue.inr[ue] = self.ue.ext_interference[ue] - \ + self.ue.thermal_noise[ue] # Calculate PFD at the UE @@ -413,7 +411,7 @@ def calculate_sinr_ext(self): pfd_linear = 10 ** (self.ue.pfd_external / 10) # Sum PFDs from all transmitters for each UE (axis=0 assumes shape # [n_tx, n_ue]) - pfd_agg_linear = np.sum(pfd_linear[active_sys], axis=0) + pfd_agg_linear = np.sum(pfd_linear[system_interfering], axis=0) # Convert back to dBW self.ue.pfd_external_aggregated = 10 * np.log10(pfd_agg_linear) @@ -529,14 +527,15 @@ def calculate_external_interference(self): f"No implementation for self.param_system.adjacent_ch_reception == { self.param_system.adjacent_ch_reception}") - sys_active = np.where(self.system.active)[0] - if len(sys_active) > 1: + if self.system.num_stations > 1: raise NotImplementedError( "Implementation does not support victim system with more than 1 active station" ) rx_interference = 0 + bs_interferer_paths = self.paths_between_imt_and_sys._mask.T for bs in bs_active: + system_interfering = np.where(bs_interferer_paths[bs])[0] active_beams = [ i for i in range( bs * @@ -546,7 +545,7 @@ def calculate_external_interference(self): ] if self.co_channel: rx_interference += np.sum( - 10 ** (0.1 * (pow_coch - self.coupling_loss_imt_system[active_beams, sys_active])) + 10 ** (0.1 * (pow_coch - self.coupling_loss_imt_system[active_beams, system_interfering])) ) if self.adjacent_channel: @@ -554,7 +553,7 @@ def calculate_external_interference(self): # NOTE: we only consider one beam since all beams should have gain # of a single element for IMT, and as such the coupling loss should be the # same for all beams - adj_loss = self.coupling_loss_imt_system_adjacent[np.ix_(active_beams, sys_active)] + adj_loss = self.coupling_loss_imt_system_adjacent[np.ix_(active_beams, system_interfering)] # FIXME: for more than 1 sys # NOTE: sharc impl already doesn't really work with n_sys > 1 @@ -563,7 +562,7 @@ def calculate_external_interference(self): tx_oob_s = tx_oob - adj_loss[0, :] if self.param_system.adjacent_ch_reception != "OFF": - rx_oob_s = rx_oob - self.coupling_loss_imt_system[active_beams, sys_active] + rx_oob_s = rx_oob - self.coupling_loss_imt_system[active_beams, system_interfering] else: rx_oob_s = -np.inf @@ -625,8 +624,32 @@ def collect_results(self, write_to_file: bool, snapshot_number: int): "effective_area") and self.system.num_stations == 1: self.results.system_pfd.extend([self.system.pfd]) + if self.parameters.imt.interfered_with: + self.add_system_imt_interaction_attr_to_results( + "DL", + self.coupling_loss_imt_system.T, + "sys_to_imt_coupling_loss", + ) + self.add_system_imt_interaction_attr_to_results( + "DL", + self.ue.pfd_external, + "imt_dl_pfd_external", + ) + + self.add_system_imt_interaction_attr_to_results("DL", "system_imt_antenna_gain") + if len(self.imt_system_antenna_gain): + self.add_system_imt_interaction_attr_to_results("DL", "imt_system_antenna_gain") + + if len(self.imt_system_antenna_gain_adjacent): + self.add_system_imt_interaction_attr_to_results("DL", "imt_system_antenna_gain_adjacent") + + self.add_system_imt_interaction_attr_to_results("DL", "imt_system_path_loss") + if self.param_system.channel_model == "HDFSS": + self.add_system_imt_interaction_attr_to_results("DL", "imt_system_build_entry_loss") + self.add_system_imt_interaction_attr_to_results("DL", "imt_system_diffraction_loss") + bs_active = np.where(self.bs.active)[0] - sys_active = np.where(self.system.active)[0] + for bs in bs_active: ue = self.link[bs] @@ -665,58 +688,9 @@ def collect_results(self, write_to_file: bool, snapshot_number: int): ) self.results.imt_dl_inr.extend(self.ue.inr[ue].tolist()) - self.results.imt_dl_pfd_external.extend( - self.ue.pfd_external[sys_active[:, np.newaxis], ue].flatten()) - self.results.imt_dl_pfd_external_aggregated.extend( self.ue.pfd_external_aggregated[ue].tolist()) - self.results.system_imt_antenna_gain.extend( - self.system_imt_antenna_gain[sys_active[:, np.newaxis], ue].flatten(), - ) - if len(self.imt_system_antenna_gain): - self.results.imt_system_antenna_gain.extend( - self.imt_system_antenna_gain[sys_active[:, np.newaxis], ue].flatten(), - ) - if len(self.imt_system_antenna_gain_adjacent): - self.results.imt_system_antenna_gain_adjacent.extend( - self.imt_system_antenna_gain_adjacent[sys_active[:, np.newaxis], ue].flatten(), - ) - self.results.imt_system_path_loss.extend( - self.imt_system_path_loss[sys_active[:, np.newaxis], ue].flatten(), - ) - if self.param_system.channel_model == "HDFSS": - self.results.imt_system_build_entry_loss.extend( - self.imt_system_build_entry_loss[sys_active[:, np.newaxis], ue].flatten(), - ) - self.results.imt_system_diffraction_loss.extend( - self.imt_system_diffraction_loss[sys_active[:, np.newaxis], ue].flatten(), - ) - self.results.sys_to_imt_coupling_loss.extend( - self.coupling_loss_imt_system[np.array(ue)[:, np.newaxis], sys_active].flatten()) - else: # IMT is the interferer - self.results.system_imt_antenna_gain.extend( - self.system_imt_antenna_gain[sys_active[:, np.newaxis], ue].flatten(), - ) - if len(self.imt_system_antenna_gain): - self.results.imt_system_antenna_gain.extend( - self.imt_system_antenna_gain[sys_active[:, np.newaxis], ue].flatten(), - ) - if len(self.imt_system_antenna_gain_adjacent): - self.results.imt_system_antenna_gain_adjacent.extend( - self.imt_system_antenna_gain_adjacent[sys_active[:, np.newaxis], ue].flatten(), - ) - self.results.imt_system_path_loss.extend( - self.imt_system_path_loss[sys_active[:, np.newaxis], ue].flatten(), - ) - if self.param_system.channel_model == "HDFSS": - self.results.imt_system_build_entry_loss.extend( - self.imt_system_build_entry_loss[:, bs], - ) - self.results.imt_system_diffraction_loss.extend( - self.imt_system_diffraction_loss[:, bs], - ) - self.results.imt_dl_tx_power.extend(self.bs.tx_power[bs].tolist()) if not self.parameters.imt.imt_dl_intra_sinr_calculation_disabled: diff --git a/sharc/simulation_uplink.py b/sharc/simulation_uplink.py index 2acad8c24..804849e67 100644 --- a/sharc/simulation_uplink.py +++ b/sharc/simulation_uplink.py @@ -148,23 +148,22 @@ def calculate_sinr(self): """ # calculate uplink received power for each active BS bs_active = np.where(self.bs.active)[0] + bs_to_ue_path_mask = self.intra_imt_paths._mask.T for bs in bs_active: ue = self.link[bs] self.bs.rx_power[bs] = self.ue.tx_power[ue] - \ self.coupling_loss_imt[bs, ue] # create a list of BSs that serve the interfering UEs - bs_interf = [b for b in bs_active if b not in [bs]] + ue_interf = np.where(bs_to_ue_path_mask[bs])[0] + ue_interf = [ui for ui in ue_interf if ui not in ue] # calculate intra system interference - for bi in bs_interf: - ui = self.link[bi] - interference = self.ue.tx_power[ui] - \ - self.coupling_loss_imt[bs, ui] - self.bs.rx_interference[bs] = 10 * np.log10( - np.power(10, 0.1 * self.bs.rx_interference[bs]) + - np.power(10, 0.1 * interference), - ) + interference_per_beam = self.ue.tx_power[ue_interf] - \ + self.coupling_loss_imt[bs, ue_interf] + self.bs.rx_interference[bs] = 10 * np.log10( + np.power(10, 0.1 * interference_per_beam), + ) # calculate N # thermal noise in dBm @@ -209,9 +208,10 @@ def calculate_sinr_ext(self): ) bs_active = np.where(self.bs.active)[0] - sys_active = np.where(self.system.active)[0] + bs_interferer_paths = self.paths_between_imt_and_sys._mask.T for bs in bs_active: + system_interfering = np.where(bs_interferer_paths[bs])[0] active_beams = [ i for i in range( bs * self.parameters.imt.ue.k, @@ -240,7 +240,7 @@ def calculate_sinr_ext(self): in_band_interf = self.param_system.tx_power_density + \ 10 * np.log10(beams_bw[:, np.newaxis] * 1e6) + \ 10 * np.log10(weights)[:, np.newaxis] - \ - self.coupling_loss_imt_system[active_beams, :][:, sys_active] + self.coupling_loss_imt_system[active_beams, :][:, system_interfering] in_band_interf_lin = 10 ** (in_band_interf / 10) oob_interf_lin = 0 @@ -323,10 +323,10 @@ def calculate_sinr_ext(self): if self.param_system.adjacent_ch_emissions != "OFF": # oob for system is inband for IMT - tx_oob = tx_oob[:, np.newaxis] - self.coupling_loss_imt_system[active_beams, :][:, sys_active] + tx_oob = tx_oob[:, np.newaxis] - self.coupling_loss_imt_system[active_beams, :][:, system_interfering] # oob for IMT - rx_oob = rx_oob[:, np.newaxis] - self.coupling_loss_imt_system_adjacent[active_beams, :][:, sys_active] + rx_oob = rx_oob[:, np.newaxis] - self.coupling_loss_imt_system_adjacent[active_beams, :][:, system_interfering] # Out of band power # sum linearly power leaked into band and power received in the @@ -374,111 +374,115 @@ def calculate_external_interference(self): rx_interference = 0 bs_active = np.where(self.bs.active)[0] - sys_active = np.where(self.system.active)[0] + ue_interf_from_sys_paths_mask = self.paths_between_imt_and_sys._mask.T for bs in bs_active: - ue = self.link[bs] - - if self.co_channel: - # TODO: test this in integration testing - weights = self.calculate_bw_weights( - self.ue.bandwidth[ue], - self.ue.center_freq[ue], - self.param_system.bandwidth, - self.param_system.frequency, - ) + ues = self.link[bs] + for ue in ues: + sys_victim = np.where(ue_interf_from_sys_paths_mask[ue])[0] + if len(sys_victim) == 0: + continue + + if self.co_channel: + # TODO: test this in integration testing + weights = self.calculate_bw_weights( + self.ue.bandwidth[ue], + self.ue.center_freq[ue], + self.param_system.bandwidth, + self.param_system.frequency, + ) - interference_ue = self.ue.tx_power[ue] - \ - self.coupling_loss_imt_system[ue, sys_active] - rx_interference += np.sum( - weights * np.power( - 10, - 0.1 * interference_ue, - ), - ) + interference_ue = self.ue.tx_power[ue] - \ + self.coupling_loss_imt_system[ue, sys_victim] + rx_interference += np.sum( + weights * np.power( + 10, + 0.1 * interference_ue, + ), + ) - if self.adjacent_channel: - # These are in dB. Turn to zero linear. - tx_oob = -np.inf - rx_oob = -np.inf - # Calculate how much power is emitted in the adjacent channel: - if self.parameters.imt.adjacent_ch_emissions == "SPECTRAL_MASK": - # The unwanted emission is calculated in terms of TRP (after - # antenna). In SHARC implementation, ohmic losses are already - # included in coupling loss. Then, care has to be taken; - # otherwise ohmic loss will be included twice. - # TODO?: what is ue_power_diff - tx_oob = self.ue.spectral_mask.power_calc(self.param_system.frequency, self.system.bandwidth) \ - - self.ue_power_diff[ue] \ - + self.parameters.imt.ue.ohmic_loss - - elif self.parameters.imt.adjacent_ch_emissions == "ACLR": - non_overlap_sys_bw = self.param_system.bandwidth - self.overlapping_bandwidth - # NOTE: approximated equal to IMT bw - measurement_bw = self.parameters.imt.bandwidth - aclr_dB = self.parameters.imt.ue.adjacent_ch_leak_ratio + if self.adjacent_channel: + # These are in dB. Turn to zero linear. + tx_oob = -np.inf + rx_oob = -np.inf + # Calculate how much power is emitted in the adjacent channel: + if self.parameters.imt.adjacent_ch_emissions == "SPECTRAL_MASK": + # The unwanted emission is calculated in terms of TRP (after + # antenna). In SHARC implementation, ohmic losses are already + # included in coupling loss. Then, care has to be taken; + # otherwise ohmic loss will be included twice. + # TODO?: what is ue_power_diff + tx_oob = self.ue.spectral_mask.power_calc(self.param_system.frequency, self.system.bandwidth) \ + - self.ue_power_diff[ue] \ + + self.parameters.imt.ue.ohmic_loss + + elif self.parameters.imt.adjacent_ch_emissions == "ACLR": + non_overlap_sys_bw = self.param_system.bandwidth - self.overlapping_bandwidth + # NOTE: approximated equal to IMT bw + measurement_bw = self.parameters.imt.bandwidth + aclr_dB = self.parameters.imt.ue.adjacent_ch_leak_ratio + + if non_overlap_sys_bw > measurement_bw: + # NOTE: ACLR defines total leaked power over a fixed measurement bandwidth. + # If the victim bandwidth is wider, you’re assuming the same leakage + # profile extends beyond the ACLR-defined region, which may overestimate interference + # FIXME: if the victim bw fully contains tx bw, then + # EACH region should be <= measurement_bw + warn( + "Using IMT ACLR into system, but ACLR measurement bw is " + f"{measurement_bw} while the system bw is bigger ({non_overlap_sys_bw}).\n" + "Are you sure you intend to apply ACLR to the entire system bw?" + ) - if non_overlap_sys_bw > measurement_bw: - # NOTE: ACLR defines total leaked power over a fixed measurement bandwidth. - # If the victim bandwidth is wider, you’re assuming the same leakage - # profile extends beyond the ACLR-defined region, which may overestimate interference - # FIXME: if the victim bw fully contains tx bw, then - # EACH region should be <= measurement_bw - warn( - "Using IMT ACLR into system, but ACLR measurement bw is " - f"{measurement_bw} while the system bw is bigger ({non_overlap_sys_bw}).\n" - "Are you sure you intend to apply ACLR to the entire system bw?" + # tx_oob_in_measurement = (tx_pow_lin / aclr) + # => approx. PSD = (tx_pow_lin / aclr) / measurement_bw + # approximated received tx_oob = PSD * non_overlap_sys_bw + # NOTE: we don't get total power, but power per beam + # because later broadcast will sum this tx_oob `k` times + tx_oob = self.ue.tx_power[ue] - aclr_dB + 10 * np.log10( + non_overlap_sys_bw / measurement_bw + ) + elif self.parameters.imt.adjacent_ch_emissions == "OFF": + pass + else: + raise ValueError( + f"No implementation for self.parameters.imt.adjacent_ch_emissions == {self.parameters.imt.adjacent_ch_emissions}" ) - # tx_oob_in_measurement = (tx_pow_lin / aclr) - # => approx. PSD = (tx_pow_lin / aclr) / measurement_bw - # approximated received tx_oob = PSD * non_overlap_sys_bw - # NOTE: we don't get total power, but power per beam - # because later broadcast will sum this tx_oob `k` times - tx_oob = self.ue.tx_power[ue] - aclr_dB + 10 * np.log10( - non_overlap_sys_bw / measurement_bw - ) - elif self.parameters.imt.adjacent_ch_emissions == "OFF": - pass - else: - raise ValueError( - f"No implementation for self.parameters.imt.adjacent_ch_emissions == {self.parameters.imt.adjacent_ch_emissions}" - ) - - # Calculate how much power is received in the adjacent channel - if self.param_system.adjacent_ch_reception == "ACS": - non_overlap_imt_bw = self.parameters.imt.bandwidth - self.overlapping_bandwidth - tx_bw = self.parameters.imt.bandwidth - acs_dB = self.param_system.adjacent_ch_selectivity - - # NOTE: only the power not overlapping is attenuated by ACS - # PSD = tx_pow_lin / tx_bw - # tx_pow_adj_lin = PSD * non_overlap_imt_bw - # rx_oob = tx_pow_adj_lin / acs - rx_oob = self.ue.tx_power[ue] + 10 * np.log10( - non_overlap_imt_bw / tx_bw - ) - acs_dB - elif self.param_system.adjacent_ch_reception == "OFF": - if self.parameters.imt.adjacent_ch_emissions == "OFF": - raise ValueError("parameters.imt.adjacent_ch_emissions and parameters.imt.adjacent_ch_reception" - " cannot be both set to \"OFF\"") - pass - else: - raise ValueError( - f"No implementation for self.param_system.adjacent_ch_reception == {self.param_system.adjacent_ch_reception}" - ) + # Calculate how much power is received in the adjacent channel + if self.param_system.adjacent_ch_reception == "ACS": + non_overlap_imt_bw = self.parameters.imt.bandwidth - self.overlapping_bandwidth + tx_bw = self.parameters.imt.bandwidth + acs_dB = self.param_system.adjacent_ch_selectivity + + # NOTE: only the power not overlapping is attenuated by ACS + # PSD = tx_pow_lin / tx_bw + # tx_pow_adj_lin = PSD * non_overlap_imt_bw + # rx_oob = tx_pow_adj_lin / acs + rx_oob = self.ue.tx_power[ue] + 10 * np.log10( + non_overlap_imt_bw / tx_bw + ) - acs_dB + elif self.param_system.adjacent_ch_reception == "OFF": + if self.parameters.imt.adjacent_ch_emissions == "OFF": + raise ValueError("parameters.imt.adjacent_ch_emissions and parameters.imt.adjacent_ch_reception" + " cannot be both set to \"OFF\"") + pass + else: + raise ValueError( + f"No implementation for self.param_system.adjacent_ch_reception == {self.param_system.adjacent_ch_reception}" + ) - # Out of band power - tx_oob -= self.coupling_loss_imt_system_adjacent[ue, sys_active] + # Out of band power + tx_oob -= self.coupling_loss_imt_system_adjacent[ue, sys_victim] - if self.param_system.adjacent_ch_reception != "OFF": - rx_oob -= self.coupling_loss_imt_system[ue, sys_active] - # Out of band power - # sum linearly power leaked into band and power received in the adjacent band - oob_power_lin = 10 ** (0.1 * tx_oob) + 10 ** (0.1 * rx_oob) + if self.param_system.adjacent_ch_reception != "OFF": + rx_oob -= self.coupling_loss_imt_system[ue, sys_victim] + # Out of band power + # sum linearly power leaked into band and power received in the adjacent band + oob_power_lin = 10 ** (0.1 * tx_oob) + 10 ** (0.1 * rx_oob) - rx_interference += np.sum( - oob_power_lin - ) + rx_interference += np.sum( + oob_power_lin + ) self.system.rx_interference = 10 * np.log10(rx_interference) # calculate N @@ -513,21 +517,36 @@ def collect_results(self, write_to_file: bool, snapshot_number: int): write_to_file (bool): Whether to write results to file. snapshot_number (int): The current snapshot number. """ + sys_active = np.where(self.system.active)[0] if not self.parameters.imt.interfered_with and np.any(self.bs.active): - self.results.system_inr.extend(self.system.inr.tolist()) + self.results.system_inr.extend(self.system.inr[sys_active].tolist()) + # FIXME: self.system.rx_interference should not be a scalar self.results.system_ul_interf_power.extend( [self.system.rx_interference], ) self.results.system_ul_interf_power_per_mhz.extend( - [self.system.rx_interference - 10 * math.log10(self.system.bandwidth)], + [self.system.rx_interference + - 10 * math.log10(self.system.bandwidth[sys_active])], ) # TODO: generalize this a bit more if needed if hasattr( self.system.antenna[0], "effective_area") and self.system.num_stations == 1: - self.results.system_pfd.extend([self.system.pfd]) + self.results.system_pfd.extend([self.system.pfd[sys_active]]) + + self.add_system_imt_interaction_attr_to_results("UL", "system_imt_antenna_gain") + if len(self.imt_system_antenna_gain): + self.add_system_imt_interaction_attr_to_results("UL", "imt_system_antenna_gain") + + self.add_system_imt_interaction_attr_to_results("UL", "imt_system_path_loss") + + if len(self.imt_system_antenna_gain_adjacent): + self.add_system_imt_interaction_attr_to_results("UL", "imt_system_antenna_gain_adjacent") + if self.param_system.channel_model == "HDFSS": + self.add_system_imt_interaction_attr_to_results("UL", "imt_system_path_loss") + self.add_system_imt_interaction_attr_to_results("UL", "imt_system_build_entry_loss") + self.add_system_imt_interaction_attr_to_results("UL", "imt_system_diffraction_loss") - sys_active = np.where(self.system.active)[0] bs_active = np.where(self.bs.active)[0] for bs in bs_active: ue = self.link[bs] @@ -564,53 +583,6 @@ def collect_results(self, write_to_file: bool, snapshot_number: int): ) self.results.imt_ul_inr.extend(self.bs.inr[bs].tolist()) - active_beams = np.array([ - i for i in range( - bs * self.parameters.imt.ue.k, (bs + 1) * self.parameters.imt.ue.k, - ) - ]) - self.results.system_imt_antenna_gain.extend( - self.system_imt_antenna_gain[np.ix_(sys_active, active_beams)].flatten(), - ) - self.results.imt_system_antenna_gain.extend( - self.imt_system_antenna_gain[np.ix_(sys_active, active_beams)].flatten(), - ) - if len(self.imt_system_antenna_gain_adjacent): - self.results.imt_system_antenna_gain_adjacent.extend( - self.imt_system_antenna_gain_adjacent[np.ix_(sys_active, active_beams)].flatten(),) - self.results.imt_system_path_loss.extend( - self.imt_system_path_loss[np.ix_(sys_active, active_beams)].flatten(), - ) - if self.param_system.channel_model == "HDFSS": - self.results.imt_system_build_entry_loss.extend( - self.imt_system_build_entry_loss[np.ix_(sys_active, active_beams)], - ) - self.results.imt_system_diffraction_loss.extend( - self.imt_system_diffraction_loss[np.ix_(sys_active, active_beams)], - ) - else: # IMT is the interferer - self.results.system_imt_antenna_gain.extend( - self.system_imt_antenna_gain[np.ix_(sys_active, ue)].flatten(), - ) - if len(self.imt_system_antenna_gain): - self.results.imt_system_antenna_gain.extend( - self.imt_system_antenna_gain[np.ix_(sys_active, ue)].flatten(), - ) - if len(self.imt_system_antenna_gain_adjacent): - self.results.imt_system_antenna_gain_adjacent.extend( - self.imt_system_antenna_gain_adjacent[np.ix_(sys_active, ue)].flatten(), - ) - self.results.imt_system_path_loss.extend( - self.imt_system_path_loss[np.ix_(sys_active, ue)].flatten(), - ) - if self.param_system.channel_model == "HDFSS": - self.results.imt_system_build_entry_loss.extend( - self.imt_system_build_entry_loss[np.ix_(sys_active, ue)], - ) - self.results.imt_system_diffraction_loss.extend( - self.imt_system_diffraction_loss[np.ix_(sys_active, ue)], - ) - self.results.imt_ul_tx_power.extend(self.ue.tx_power[ue].tolist()) imt_ul_tx_power_density = 10 * np.log10( np.power(10, 0.1 * self.ue.tx_power[ue]) / ( From d4087467c23547c564bb7359d21f56b11eaefda4 Mon Sep 17 00:00:00 2001 From: artistrea Date: Wed, 26 Nov 2025 22:27:54 -0300 Subject: [PATCH 45/77] hotfix: path implementation oob power shape & warnings --- sharc/simulation_downlink.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/sharc/simulation_downlink.py b/sharc/simulation_downlink.py index 8673c8b29..b6808d168 100644 --- a/sharc/simulation_downlink.py +++ b/sharc/simulation_downlink.py @@ -187,7 +187,10 @@ def calculate_sinr(self): interference = bs_tx_power_array[bs_interf] - \ self.coupling_loss_imt[bs_interf, ui] - self.ue.rx_interference[ui] = 10 * np.log10(np.sum(np.power(10, 0.1 * interference))) + # sum 1e-50 so that rx_interference >= -500 + self.ue.rx_interference[ui] = 10 * np.log10( + np.sum(np.power(10, 0.1 * interference)) + 1e-50 + ) # Thermal noise in dBm self.ue.thermal_noise = \ @@ -245,7 +248,7 @@ def calculate_sinr_ext(self): float(self.param_system.frequency), ) - in_band_interf_power = -500. + in_band_interf_power = np.array(-500.) if self.co_channel: # Inteferer transmit power in dBm over the overlapping band # (MHz) with UEs. @@ -260,11 +263,11 @@ def calculate_sinr_ext(self): ) in_band_interf_power = \ self.param_system.tx_power_density + 10 * np.log10( - self.ue.bandwidth[ue, np.newaxis] * 1e6 + self.ue.bandwidth[ue] * 1e6 ) + 10 * np.log10(weights) - \ self.coupling_loss_imt_system[ue, :][system_interfering] - oob_power = np.resize(-500., (1, 1)) + oob_power = np.resize(-500., 1) if self.adjacent_channel: # emissions outside of tx bandwidth and inside of rx bw # due to oob emissions on tx side @@ -411,7 +414,9 @@ def calculate_sinr_ext(self): pfd_linear = 10 ** (self.ue.pfd_external / 10) # Sum PFDs from all transmitters for each UE (axis=0 assumes shape # [n_tx, n_ue]) - pfd_agg_linear = np.sum(pfd_linear[system_interfering], axis=0) + sys_active = np.where(self.system.active)[0] + # FIXME: consider only correct paths here + pfd_agg_linear = np.sum(pfd_linear[sys_active], axis=0) # Convert back to dBW self.ue.pfd_external_aggregated = 10 * np.log10(pfd_agg_linear) From b54675b281d3d2ca0b8055305ff39e8acac6977a Mon Sep 17 00:00:00 2001 From: artistrea Date: Thu, 27 Nov 2025 10:36:50 -0300 Subject: [PATCH 46/77] fix(lint): lint errors related to paths impl --- .../propagation_building_entry_loss.py | 3 +++ sharc/propagation/propagation_clutter_loss.py | 3 +++ sharc/propagation/propagation_path.py | 18 ++++++++++++++++++ sharc/simulation.py | 5 +++++ tests/test_propagation_path.py | 17 +++++++++++++---- 5 files changed, 42 insertions(+), 4 deletions(-) diff --git a/sharc/propagation/propagation_building_entry_loss.py b/sharc/propagation/propagation_building_entry_loss.py index 4bff257ce..7e64c4aba 100644 --- a/sharc/propagation/propagation_building_entry_loss.py +++ b/sharc/propagation/propagation_building_entry_loss.py @@ -23,6 +23,9 @@ def get_path_loss( station_a_gains=None, station_b_gains=None, ) -> np.array: + """This class isn't supposed to be directly called in the simulation + loop, so this methods doesn't need to be implemented + """ raise NotImplementedError() def get_loss( diff --git a/sharc/propagation/propagation_clutter_loss.py b/sharc/propagation/propagation_clutter_loss.py index de6ebb2a4..1eb4cb045 100644 --- a/sharc/propagation/propagation_clutter_loss.py +++ b/sharc/propagation/propagation_clutter_loss.py @@ -58,6 +58,9 @@ def get_path_loss( station_a_gains=None, station_b_gains=None, ) -> np.array: + """This class isn't supposed to be directly called in the simulation + loop, so this methods doesn't need to be implemented + """ raise NotImplementedError() def get_loss(self, *args, **kwargs) -> np.array: diff --git a/sharc/propagation/propagation_path.py b/sharc/propagation/propagation_path.py index 156abf1bd..2eeb802a8 100644 --- a/sharc/propagation/propagation_path.py +++ b/sharc/propagation/propagation_path.py @@ -7,6 +7,12 @@ class PropagationPath(): + """This class defines which paths between stations actually + do exist and shall be calculated. The actual paths are calculated + right as get_path_loss is called, so there is no risk of being + out of date. + Paths are marked as disabled by masking functions + """ # either # - (sta_a == IMT UE) and (sta_b == IMT BS) # OR @@ -80,6 +86,9 @@ def get_path_loss( ) def calc_mask(self, *, deduplicate: bool): + """Calculates and updates current mask and paths + based on masking functions in instance. + """ mask = np.ones(self._orig_shape, dtype=bool) for mask_fn in self._mask_strategies: mask = mask_fn(self.sta_a, self.sta_b, mask) @@ -246,6 +255,7 @@ def create_default( return PropagationPath(sta_a, sta_b, mask_fns) + def create_path_mask_distant_earth_stations(distance_m: float): """Receives a distance in meters and returns a function deactivate paths between distant earth stations @@ -255,6 +265,9 @@ def path_mask_distant_earth_stations( sta_b: StationManager, acc_mask: np.ndarray[np.ndarray[bool]] ): + """Marks paths with distance between earth stations + greater than distance_m as disabled + """ if sta_a.is_space_station or sta_b.is_space_station: return acc_mask @@ -275,6 +288,9 @@ def path_mask_low_elevation_sat_from_es( sta_b: StationManager, acc_mask: np.ndarray[np.ndarray[bool]] ): + """Marks paths with elevation between earth station and space station + less than min_elev_deg as disabled + """ es: StationManager = None ss: StationManager = None if sta_a.is_space_station and not sta_b.is_space_station: @@ -303,6 +319,8 @@ def path_mask_inactive_stations( sta_b: StationManager, acc_mask: np.ndarray[np.ndarray[bool]] ): + """Marks paths with inactive stations as disabled + """ acc_mask[~sta_a.active, :] = False acc_mask[:, ~sta_b.active] = False diff --git a/sharc/simulation.py b/sharc/simulation.py index ca6cc04da..6d8910511 100644 --- a/sharc/simulation.py +++ b/sharc/simulation.py @@ -813,6 +813,11 @@ def add_system_imt_interaction_attr_to_results( result_attr: str | None = None, result_obj: typing.Any = None, ): + """Adds attribute (self.attr if attr is str else attr if attr is object) + to the instance results object. + Works with system<>imt interaction, getting results based on existing + paths_between_imt_and_sys + """ if isinstance(attr, str): v = np.array(getattr(self, attr)) else: diff --git a/tests/test_propagation_path.py b/tests/test_propagation_path.py index 45ebc0a8d..86c36cc79 100644 --- a/tests/test_propagation_path.py +++ b/tests/test_propagation_path.py @@ -14,16 +14,21 @@ def assert_array_not_equal(x, y): """Asserts that two arrays are not equal.""" npt.assert_raises(AssertionError, npt.assert_array_equal, x, y) + def create_mock_function(ret): + """Creates function that returns the argument + """ def mock_function(*args, **kwargs): return ret return mock_function + class PropagationPathTest(unittest.TestCase): """This is more of an integration test since it depends on the parts it joins. """ def setUp(self): + """setUp that runs before each test""" pass def test_undeduped_masking_operations(self): @@ -190,6 +195,8 @@ def test_deduped_mask_operations(self): npt.assert_equal(expected, unmskd_mtx) def test_get_path_loss_fspl(self): + """Test get_path_loss with different shapes on fspl + """ bs = StationManager(3) bs.geom.set_global_coords( np.array([5., 15., 25.]), @@ -208,7 +215,7 @@ def test_get_path_loss_fspl(self): fspl = path.get_path_loss( PropagationFreeSpace(None), None, - 1e3, # [MHz] + 1e3, # [MHz] ) expected_fspl = PropagationFreeSpace(None).get_free_space_loss( @@ -217,9 +224,11 @@ def test_get_path_loss_fspl(self): ) self.assertEqual(fspl.shape, expected_fspl.shape) - npt.assert_array_equal(fspl,expected_fspl) + npt.assert_array_equal(fspl, expected_fspl) def test_get_path_loss_in_propagations(self): + """Test get_path_loss method with all propagations + """ bs = StationManager(12) bs.geom.set_global_coords( np.arange(0., 12., 1.0) * 10, @@ -278,7 +287,7 @@ def test_get_path_loss_in_propagations(self): ploss = path.get_path_loss( propagation, parameters, - 1e3, # [MHz] + 1e3, # [MHz] # sta_a_gains=gains0, sta_a_gains=bs_w_beams_gains, sta_b_gains=gains0.T, @@ -365,7 +374,7 @@ def test_get_path_loss_single_ss_vs_bs_in_propagations(self): ploss = path.get_path_loss( propagation, parameters, - 1e3, # [MHz] + 1e3, # [MHz] sta_a_gains=gains0, # sta_b_gains=gains0.T, sta_b_gains=bs_w_beams_gains, From ca393c474e2c32e0d58ccff26a23e3b5d16020d7 Mon Sep 17 00:00:00 2001 From: artistrea Date: Thu, 27 Nov 2025 10:57:48 -0300 Subject: [PATCH 47/77] fix: propagation terrestrial simple clutter loss call --- sharc/propagation/propagation_ter_simple.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sharc/propagation/propagation_ter_simple.py b/sharc/propagation/propagation_ter_simple.py index 873d74f28..6d8a81426 100644 --- a/sharc/propagation/propagation_ter_simple.py +++ b/sharc/propagation/propagation_ter_simple.py @@ -30,6 +30,7 @@ def __init__(self, random_number_gen: np.random.RandomState): self.free_space = PropagationFreeSpace(np.random.RandomState(101)) self.building_loss = 20 self.clutter_type = "one_end" + self.clutter_scenario = "terrestrial" def get_path_loss( self, @@ -111,7 +112,8 @@ def get_loss( distance=distance, loc_percentage=loc_percentage, station_type=StationType.FSS_ES, - clutter_type=self.clutter_type + clutter_type=self.clutter_type, + clutter_scenario=self.clutter_scenario, ) building_loss = self.building_loss * indoor_stations From fcb1530cd9e45ea66e8c028cec0287e80de104dc Mon Sep 17 00:00:00 2001 From: artistrea Date: Thu, 27 Nov 2025 11:16:48 -0300 Subject: [PATCH 48/77] refactor: removed useless variables --- sharc/simulation_uplink.py | 1 - sharc/support/geometry.py | 1 - 2 files changed, 2 deletions(-) diff --git a/sharc/simulation_uplink.py b/sharc/simulation_uplink.py index 804849e67..5ef052157 100644 --- a/sharc/simulation_uplink.py +++ b/sharc/simulation_uplink.py @@ -465,7 +465,6 @@ def calculate_external_interference(self): if self.parameters.imt.adjacent_ch_emissions == "OFF": raise ValueError("parameters.imt.adjacent_ch_emissions and parameters.imt.adjacent_ch_reception" " cannot be both set to \"OFF\"") - pass else: raise ValueError( f"No implementation for self.param_system.adjacent_ch_reception == {self.param_system.adjacent_ch_reception}" diff --git a/sharc/support/geometry.py b/sharc/support/geometry.py index a710ecc22..71b77fdf9 100644 --- a/sharc/support/geometry.py +++ b/sharc/support/geometry.py @@ -690,7 +690,6 @@ def get_local_elevation(self, other: "SimulatorGeometry") -> np.array: if not self.uses_local_coords: return self.get_global_elevation(other) - lat, lon, alt = self.local_lla_references dist2d, z_dist = self.get_local_distance_to(other, return_z_dist=True) return np.degrees(np.arctan2(z_dist, dist2d)) From 741f854ea0bf43a62988639be7ce77b0313c8a34 Mon Sep 17 00:00:00 2001 From: Bruno Faria Date: Tue, 2 Dec 2025 13:26:07 -0300 Subject: [PATCH 49/77] fix(propagation): Fixed the season parameter check in Atmosfere P.835 to --- sharc/propagation/atmosphere.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sharc/propagation/atmosphere.py b/sharc/propagation/atmosphere.py index 846ce1516..d4d382fb7 100644 --- a/sharc/propagation/atmosphere.py +++ b/sharc/propagation/atmosphere.py @@ -385,6 +385,9 @@ def get_reference_atmosphere_p835( """ h_km = altitude / 1000 + season = season.lower() + if season not in ['winter', 'summer']: + raise ValueError(f"Invalid season name f{season}.") if latitude <= 22: # low latitude From 595bddff4de2567190c8ff6f0b751eda147479bc Mon Sep 17 00:00:00 2001 From: Bruno Faria Date: Thu, 4 Dec 2025 10:52:40 -0300 Subject: [PATCH 50/77] fix(simulation): Fixed simulator log facility setup * Also added a CLI option to stream simulation log to a file --- sharc/main_cli.py | 35 +++++++++----------- sharc/support/logging.yaml | 27 ---------------- sharc/support/sharc_logger.py | 61 ++++++++++++++++++++--------------- 3 files changed, 51 insertions(+), 72 deletions(-) delete mode 100644 sharc/support/logging.yaml diff --git a/sharc/main_cli.py b/sharc/main_cli.py index 6d13173e8..a9ffa5363 100644 --- a/sharc/main_cli.py +++ b/sharc/main_cli.py @@ -7,9 +7,9 @@ import os import sys -import getopt +import argparse -from sharc.support.sharc_logger import Logging, SimulationLogger +from sharc.support.sharc_logger import setup_logging, SimulationLogger from sharc.controller import Controller from sharc.gui.view_cli import ViewCli from sharc.model import Model @@ -34,28 +34,25 @@ def main(argv): param_file = "" - try: - opts, _ = getopt.getopt(argv, "hp:") - except getopt.GetoptError: - print("usage: main_cli.py -p ") - sys.exit(2) - - if not opts: - param_file = os.path.join(os.getcwd(), "input", "parameters.yaml") - else: - for opt, arg in opts: - if opt == "-h": - print("usage: main_cli.py -p ") - sys.exit() - elif opt == "-p": - param_file = os.path.join(os.getcwd(), arg) + parser = argparse.ArgumentParser(description="SHARC - Radio Sharing and Compatiblity Monte Carlo Simulator") + parser.add_argument("-p", "--param-file", default=os.path.join(os.getcwd(), "input", "parameters.yaml"), + help="Path to parameter file (default: input/parameters.yaml)") + parser.add_argument("-l", "--log-file", default=None, + help="Path to output log file (optional)") + + args = parser.parse_args(argv) + param_file = os.path.join(os.getcwd(), args.param_file) if not os.path.isabs(args.param_file) else args.param_file + + log_file = None + if args.log_file is not None: + log_file = os.path.join(os.getcwd(), args.log_file) if not os.path.isabs(args.log_file) else args.log_file + + setup_logging(log_file=log_file) # Logger setup start sim_logger = SimulationLogger(param_file) sim_logger.start() - Logging.setup_logging() - model = Model() view_cli = ViewCli() controller = Controller() diff --git a/sharc/support/logging.yaml b/sharc/support/logging.yaml deleted file mode 100644 index 0e04fc413..000000000 --- a/sharc/support/logging.yaml +++ /dev/null @@ -1,27 +0,0 @@ ---- -version: 1 -disable_existing_loggers: False -formatters: - simple: - format: "%(message)s" -# format: "%(asctime)s - %(name)s - %(message)s" - -handlers: - console: - class: logging.StreamHandler - level: INFO - formatter: simple - stream: ext://sys.stdout - - file: - class: logging.handlers.RotatingFileHandler - formatter: simple - filename: output/logfile.log - maxBytes: 10485760 # 10MB - backupCount: 20 - encoding: utf8 - - -root: - level: INFO - handlers: [console, file] diff --git a/sharc/support/sharc_logger.py b/sharc/support/sharc_logger.py index c4771ec54..5d7cc5ac7 100644 --- a/sharc/support/sharc_logger.py +++ b/sharc/support/sharc_logger.py @@ -2,33 +2,42 @@ import os import sys import yaml -import logging +import logging.config import subprocess from pathlib import Path from datetime import datetime -from typing import Optional - - -class Logging: - """Logging utility class for configuring application logging.""" - - @staticmethod - def setup_logging( - default_path="support/logging.yaml", - default_level=logging.INFO, - env_key="LOG_CFG", - ): - """Set up logging configuration for the application.""" - path = default_path - value = os.getenv(env_key, None) - if value: - path = value - if os.path.exists(path): - with open(path, "rt") as f: - config = yaml.safe_load(f.read()) - logging.config.dictConfig(config) - else: - logging.basicConfig(level=default_level) +from typing import Optional, List + +level_mapping = logging.getLevelNamesMapping() + + +def setup_logging(log_file=None, default_level="INFO"): + """Setup logging configuration for the root logger. + + Run this function in the beginning of the simulation to setup the root logger. + """ + + try: + level = level_mapping[default_level] + except KeyError: + raise ValueError("Invalid log level option {}".format(default_level)) + + root_logger = logging.getLogger() + root_logger.setLevel(level) + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + root_logger.handlers = [] + + # Stream to stdout + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(formatter) + root_logger.addHandler(handler) + + # Stream to file if specified + if log_file is not None: + file_handler = logging.FileHandler(log_file) + file_handler.setFormatter(formatter) + root_logger.addHandler(file_handler) + class SimulationLogger: @@ -102,7 +111,7 @@ def _find_root_dir(self, folder_name: str) -> Optional[Path]: return parent return None - def _run_git_cmd(self, args: list[str]) -> Optional[str]: + def _run_git_cmd(self, args: List[str]) -> Optional[str]: try: return ( subprocess.check_output(["git"] + args, stderr=subprocess.DEVNULL) @@ -132,7 +141,7 @@ def _get_invocation_command(self) -> str: def _get_python_version(self) -> str: return sys.version.replace("\n", " ") - def _get_installed_packages(self) -> list[str]: + def _get_installed_packages(self) -> List[str]: try: output = subprocess.check_output( [sys.executable, "-m", "pip", "freeze"], stderr=subprocess.DEVNULL From 10559111c218de039f28def770e5bf352774424e Mon Sep 17 00:00:00 2001 From: Bruno Faria Date: Thu, 4 Dec 2025 10:52:40 -0300 Subject: [PATCH 51/77] fix(simulation): Fixed simulator log facility setup * Also added a CLI option to stream simulation log to a file --- sharc/main_cli.py | 35 ++++++++++---------- sharc/support/logging.yaml | 27 ---------------- sharc/support/sharc_logger.py | 60 ++++++++++++++++++++--------------- 3 files changed, 50 insertions(+), 72 deletions(-) delete mode 100644 sharc/support/logging.yaml diff --git a/sharc/main_cli.py b/sharc/main_cli.py index 6d13173e8..a9ffa5363 100644 --- a/sharc/main_cli.py +++ b/sharc/main_cli.py @@ -7,9 +7,9 @@ import os import sys -import getopt +import argparse -from sharc.support.sharc_logger import Logging, SimulationLogger +from sharc.support.sharc_logger import setup_logging, SimulationLogger from sharc.controller import Controller from sharc.gui.view_cli import ViewCli from sharc.model import Model @@ -34,28 +34,25 @@ def main(argv): param_file = "" - try: - opts, _ = getopt.getopt(argv, "hp:") - except getopt.GetoptError: - print("usage: main_cli.py -p ") - sys.exit(2) - - if not opts: - param_file = os.path.join(os.getcwd(), "input", "parameters.yaml") - else: - for opt, arg in opts: - if opt == "-h": - print("usage: main_cli.py -p ") - sys.exit() - elif opt == "-p": - param_file = os.path.join(os.getcwd(), arg) + parser = argparse.ArgumentParser(description="SHARC - Radio Sharing and Compatiblity Monte Carlo Simulator") + parser.add_argument("-p", "--param-file", default=os.path.join(os.getcwd(), "input", "parameters.yaml"), + help="Path to parameter file (default: input/parameters.yaml)") + parser.add_argument("-l", "--log-file", default=None, + help="Path to output log file (optional)") + + args = parser.parse_args(argv) + param_file = os.path.join(os.getcwd(), args.param_file) if not os.path.isabs(args.param_file) else args.param_file + + log_file = None + if args.log_file is not None: + log_file = os.path.join(os.getcwd(), args.log_file) if not os.path.isabs(args.log_file) else args.log_file + + setup_logging(log_file=log_file) # Logger setup start sim_logger = SimulationLogger(param_file) sim_logger.start() - Logging.setup_logging() - model = Model() view_cli = ViewCli() controller = Controller() diff --git a/sharc/support/logging.yaml b/sharc/support/logging.yaml deleted file mode 100644 index 0e04fc413..000000000 --- a/sharc/support/logging.yaml +++ /dev/null @@ -1,27 +0,0 @@ ---- -version: 1 -disable_existing_loggers: False -formatters: - simple: - format: "%(message)s" -# format: "%(asctime)s - %(name)s - %(message)s" - -handlers: - console: - class: logging.StreamHandler - level: INFO - formatter: simple - stream: ext://sys.stdout - - file: - class: logging.handlers.RotatingFileHandler - formatter: simple - filename: output/logfile.log - maxBytes: 10485760 # 10MB - backupCount: 20 - encoding: utf8 - - -root: - level: INFO - handlers: [console, file] diff --git a/sharc/support/sharc_logger.py b/sharc/support/sharc_logger.py index c4771ec54..d3f73c9c2 100644 --- a/sharc/support/sharc_logger.py +++ b/sharc/support/sharc_logger.py @@ -2,33 +2,41 @@ import os import sys import yaml -import logging +import logging.config import subprocess from pathlib import Path from datetime import datetime -from typing import Optional - - -class Logging: - """Logging utility class for configuring application logging.""" - - @staticmethod - def setup_logging( - default_path="support/logging.yaml", - default_level=logging.INFO, - env_key="LOG_CFG", - ): - """Set up logging configuration for the application.""" - path = default_path - value = os.getenv(env_key, None) - if value: - path = value - if os.path.exists(path): - with open(path, "rt") as f: - config = yaml.safe_load(f.read()) - logging.config.dictConfig(config) - else: - logging.basicConfig(level=default_level) +from typing import Optional, List + +level_mapping = logging.getLevelNamesMapping() + + +def setup_logging(log_file=None, default_level="INFO"): + """Setup logging configuration for the root logger. + + Run this function in the beginning of the simulation to setup the root logger. + """ + + try: + level = level_mapping[default_level] + except KeyError: + raise ValueError("Invalid log level option {}".format(default_level)) + + root_logger = logging.getLogger() + root_logger.setLevel(level) + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + root_logger.handlers = [] + + # Stream to stdout + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(formatter) + root_logger.addHandler(handler) + + # Stream to file if specified + if log_file is not None: + file_handler = logging.FileHandler(log_file) + file_handler.setFormatter(formatter) + root_logger.addHandler(file_handler) class SimulationLogger: @@ -102,7 +110,7 @@ def _find_root_dir(self, folder_name: str) -> Optional[Path]: return parent return None - def _run_git_cmd(self, args: list[str]) -> Optional[str]: + def _run_git_cmd(self, args: List[str]) -> Optional[str]: try: return ( subprocess.check_output(["git"] + args, stderr=subprocess.DEVNULL) @@ -132,7 +140,7 @@ def _get_invocation_command(self) -> str: def _get_python_version(self) -> str: return sys.version.replace("\n", " ") - def _get_installed_packages(self) -> list[str]: + def _get_installed_packages(self) -> List[str]: try: output = subprocess.check_output( [sys.executable, "-m", "pip", "freeze"], stderr=subprocess.DEVNULL From 0f9ee78504ff5935fe1e6a30b0cd57e99bea3244 Mon Sep 17 00:00:00 2001 From: Bruno Faria Date: Thu, 4 Dec 2025 11:41:39 -0300 Subject: [PATCH 52/77] update(campaings): Removed the campaign folder This commit removes the campaign folder. Because: - It's growning larger and larger and getting hard to maintain - Campaings are temporary, Sharc code is not. - Now people should take care of their own campaigns, it's not the code owners reponsibility. --- .gitignore | 2 +- .../Run the simulator using main interface.md | 17 +- .../campaigns/imt_hibs_ras_2600_MHz/README.md | 39 -- .../parameters_hibs_ras_2600_MHz_0km.yaml | 374 ------------ .../parameters_hibs_ras_2600_MHz_45km.yaml | 373 ------------ .../parameters_hibs_ras_2600_MHz_500km.yaml | 373 ------------ .../parameters_hibs_ras_2600_MHz_90km.yaml | 373 ------------ .../output/output-folder.md | 3 - .../scripts/plot_results.py | 100 ---- .../scripts/start_simulations_multi_thread.py | 15 - .../start_simulations_single_thread.py | 15 - .../comparison/contribution_20.csv | 78 --- ...imt_hotspot_eess_active_beam_small_dl.yaml | 376 ------------ ...imt_hotspot_eess_active_beam_small_ul.yaml | 371 ------------ .../imt_hotspot_eess_active/readme.md | 15 - .../scripts/plot_results.py | 90 --- .../scripts/start_simulations_multi_thread.py | 12 - .../start_simulations_single_thread.py | 12 - ... 15 (IMT Uplink) EESS (Passive) Sensor.csv | 37 -- .../Fig. 8 EESS (Passive) Sensor.csv | 27 - ...s_imt_macro_eess_passive_1_cluster_DL.yaml | 270 --------- ...eess_passive_1_cluster_DL_alternative.yaml | 278 --------- ...s_imt_macro_eess_passive_1_cluster_UL.yaml | 271 --------- .../imt_hotspot_eess_passive/readme.md | 16 - .../scripts/plot_results.py | 150 ----- .../scripts/start_simulations_multi_thread.py | 12 - .../start_simulations_single_thread.py | 12 - .../parameters_hibs_ras_2600_MHz_30deg.yaml | 387 ------------ .../parameters_hibs_ras_2600_MHz_45deg.yaml | 391 ------------ .../parameters_hibs_ras_2600_MHz_60deg.yaml | 391 ------------ .../parameters_hibs_ras_2600_MHz_90deg.yaml | 391 ------------ ...rameters_hibs_ras_2600_MHz_fspl_30deg.yaml | 377 ------------ ...rameters_hibs_ras_2600_MHz_fspl_45deg.yaml | 377 ------------ ...rameters_hibs_ras_2600_MHz_fspl_60deg.yaml | 377 ------------ ...rameters_hibs_ras_2600_MHz_fspl_90deg.yaml | 377 ------------ .../scripts/plot_results.py | 105 ---- .../scripts/start_simulations_multi_thread.py | 12 - .../start_simulations_single_thread.py | 12 - .../scripts/imt_ntn_footprint.py | 103 ---- .../imt_ntn_to_imt_tn_co_channel/README.md | 39 -- ...t_ntn_to_imt_tn_sep_distance_template.yaml | 514 ---------------- .../scripts/calculate_cluster_positions.py | 140 ----- .../scripts/parameter_gen.py | 54 -- .../scripts/plot_results.py | 60 -- .../scripts/start_simulations_multi_thread.py | 14 - .../start_simulations_single_thread.py | 14 - .../mss_d2d_to_eess/input/.gitignore | 2 - .../mss_d2d_to_eess/scripts/base_input.yaml | 552 ----------------- .../scripts/generate_inputs.py | 140 ----- .../mss_d2d_to_eess/scripts/plot_results.py | 142 ----- .../scripts/start_simulations_multi_thread.py | 12 - .../start_simulations_single_thread.py | 12 - .../campaigns/mss_d2d_to_imt/input/.gitignore | 6 - ...mss_d2d_to_imt_dl_co_channel_system_A.yaml | 461 -------------- ...mss_d2d_to_imt_lat_variation_template.yaml | 461 -------------- ...mss_d2d_to_imt_ul_co_channel_system_A.yaml | 461 -------------- sharc/campaigns/mss_d2d_to_imt/readme.md | 26 - .../scripts/orbit_model_anaylisis.py | 145 ----- .../scripts/parameter_gen_lat_variation.py | 48 -- .../mss_d2d_to_imt/scripts/plot_results.py | 150 ----- .../scripts/plot_results_lat_variation.py | 59 -- .../scripts/start_simulations.py | 45 -- .../start_simulations_lat_variation.py | 12 - .../scripts/taylor_diagram_for_mss_d2d.py | 62 -- .../input/.gitignore | 2 - .../mss_d2d_to_imt_cross_border/readme.md | 3 - .../scripts/base_input.yaml | 566 ------------------ .../generate_parameters_from_reference.py | 130 ---- .../scripts/plot_3d_param_file.py | 177 ------ .../scripts/plot_results.py | 148 ----- .../scripts/start_simulations_multi_thread.py | 14 - .../start_simulations_single_thread.py | 16 - 72 files changed, 9 insertions(+), 11689 deletions(-) delete mode 100644 sharc/campaigns/imt_hibs_ras_2600_MHz/README.md delete mode 100644 sharc/campaigns/imt_hibs_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_0km.yaml delete mode 100644 sharc/campaigns/imt_hibs_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_45km.yaml delete mode 100644 sharc/campaigns/imt_hibs_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_500km.yaml delete mode 100644 sharc/campaigns/imt_hibs_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_90km.yaml delete mode 100644 sharc/campaigns/imt_hibs_ras_2600_MHz/output/output-folder.md delete mode 100644 sharc/campaigns/imt_hibs_ras_2600_MHz/scripts/plot_results.py delete mode 100644 sharc/campaigns/imt_hibs_ras_2600_MHz/scripts/start_simulations_multi_thread.py delete mode 100644 sharc/campaigns/imt_hibs_ras_2600_MHz/scripts/start_simulations_single_thread.py delete mode 100644 sharc/campaigns/imt_hotspot_eess_active/comparison/contribution_20.csv delete mode 100644 sharc/campaigns/imt_hotspot_eess_active/input/parameters_imt_hotspot_eess_active_beam_small_dl.yaml delete mode 100644 sharc/campaigns/imt_hotspot_eess_active/input/parameters_imt_hotspot_eess_active_beam_small_ul.yaml delete mode 100644 sharc/campaigns/imt_hotspot_eess_active/readme.md delete mode 100644 sharc/campaigns/imt_hotspot_eess_active/scripts/plot_results.py delete mode 100644 sharc/campaigns/imt_hotspot_eess_active/scripts/start_simulations_multi_thread.py delete mode 100644 sharc/campaigns/imt_hotspot_eess_active/scripts/start_simulations_single_thread.py delete mode 100644 sharc/campaigns/imt_hotspot_eess_passive/comparison/Fig. 15 (IMT Uplink) EESS (Passive) Sensor.csv delete mode 100644 sharc/campaigns/imt_hotspot_eess_passive/comparison/Fig. 8 EESS (Passive) Sensor.csv delete mode 100644 sharc/campaigns/imt_hotspot_eess_passive/input/parameters_imt_macro_eess_passive_1_cluster_DL.yaml delete mode 100644 sharc/campaigns/imt_hotspot_eess_passive/input/parameters_imt_macro_eess_passive_1_cluster_DL_alternative.yaml delete mode 100644 sharc/campaigns/imt_hotspot_eess_passive/input/parameters_imt_macro_eess_passive_1_cluster_UL.yaml delete mode 100644 sharc/campaigns/imt_hotspot_eess_passive/readme.md delete mode 100644 sharc/campaigns/imt_hotspot_eess_passive/scripts/plot_results.py delete mode 100644 sharc/campaigns/imt_hotspot_eess_passive/scripts/start_simulations_multi_thread.py delete mode 100644 sharc/campaigns/imt_hotspot_eess_passive/scripts/start_simulations_single_thread.py delete mode 100644 sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_30deg.yaml delete mode 100644 sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_45deg.yaml delete mode 100644 sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_60deg.yaml delete mode 100644 sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_90deg.yaml delete mode 100644 sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_fspl_30deg.yaml delete mode 100644 sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_fspl_45deg.yaml delete mode 100644 sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_fspl_60deg.yaml delete mode 100644 sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_fspl_90deg.yaml delete mode 100644 sharc/campaigns/imt_mss_ras_2600_MHz/scripts/plot_results.py delete mode 100644 sharc/campaigns/imt_mss_ras_2600_MHz/scripts/start_simulations_multi_thread.py delete mode 100644 sharc/campaigns/imt_mss_ras_2600_MHz/scripts/start_simulations_single_thread.py delete mode 100644 sharc/campaigns/imt_ntn_footprint/scripts/imt_ntn_footprint.py delete mode 100644 sharc/campaigns/imt_ntn_to_imt_tn_co_channel/README.md delete mode 100644 sharc/campaigns/imt_ntn_to_imt_tn_co_channel/input/parameters_imt_ntn_to_imt_tn_sep_distance_template.yaml delete mode 100644 sharc/campaigns/imt_ntn_to_imt_tn_co_channel/scripts/calculate_cluster_positions.py delete mode 100644 sharc/campaigns/imt_ntn_to_imt_tn_co_channel/scripts/parameter_gen.py delete mode 100644 sharc/campaigns/imt_ntn_to_imt_tn_co_channel/scripts/plot_results.py delete mode 100644 sharc/campaigns/imt_ntn_to_imt_tn_co_channel/scripts/start_simulations_multi_thread.py delete mode 100644 sharc/campaigns/imt_ntn_to_imt_tn_co_channel/scripts/start_simulations_single_thread.py delete mode 100644 sharc/campaigns/mss_d2d_to_eess/input/.gitignore delete mode 100644 sharc/campaigns/mss_d2d_to_eess/scripts/base_input.yaml delete mode 100644 sharc/campaigns/mss_d2d_to_eess/scripts/generate_inputs.py delete mode 100644 sharc/campaigns/mss_d2d_to_eess/scripts/plot_results.py delete mode 100644 sharc/campaigns/mss_d2d_to_eess/scripts/start_simulations_multi_thread.py delete mode 100644 sharc/campaigns/mss_d2d_to_eess/scripts/start_simulations_single_thread.py delete mode 100644 sharc/campaigns/mss_d2d_to_imt/input/.gitignore delete mode 100644 sharc/campaigns/mss_d2d_to_imt/input/parameters_mss_d2d_to_imt_dl_co_channel_system_A.yaml delete mode 100644 sharc/campaigns/mss_d2d_to_imt/input/parameters_mss_d2d_to_imt_lat_variation_template.yaml delete mode 100644 sharc/campaigns/mss_d2d_to_imt/input/parameters_mss_d2d_to_imt_ul_co_channel_system_A.yaml delete mode 100644 sharc/campaigns/mss_d2d_to_imt/readme.md delete mode 100644 sharc/campaigns/mss_d2d_to_imt/scripts/orbit_model_anaylisis.py delete mode 100644 sharc/campaigns/mss_d2d_to_imt/scripts/parameter_gen_lat_variation.py delete mode 100644 sharc/campaigns/mss_d2d_to_imt/scripts/plot_results.py delete mode 100644 sharc/campaigns/mss_d2d_to_imt/scripts/plot_results_lat_variation.py delete mode 100644 sharc/campaigns/mss_d2d_to_imt/scripts/start_simulations.py delete mode 100644 sharc/campaigns/mss_d2d_to_imt/scripts/start_simulations_lat_variation.py delete mode 100644 sharc/campaigns/mss_d2d_to_imt/scripts/taylor_diagram_for_mss_d2d.py delete mode 100644 sharc/campaigns/mss_d2d_to_imt_cross_border/input/.gitignore delete mode 100644 sharc/campaigns/mss_d2d_to_imt_cross_border/readme.md delete mode 100644 sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/base_input.yaml delete mode 100644 sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/generate_parameters_from_reference.py delete mode 100644 sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/plot_3d_param_file.py delete mode 100644 sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/plot_results.py delete mode 100644 sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/start_simulations_multi_thread.py delete mode 100644 sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/start_simulations_single_thread.py diff --git a/.gitignore b/.gitignore index 4e51303c2..4496b10bf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ /sharc/output/*.csv /sharc/output/*.png /sharc/output_*/* -/sharc/campaigns/*/output/ +/sharc/campaigns/ __pycache__ diff --git a/docs/docusaurus/docs/How to/Run the simulator using main interface.md b/docs/docusaurus/docs/How to/Run the simulator using main interface.md index 47a92f450..ecadec818 100644 --- a/docs/docusaurus/docs/How to/Run the simulator using main interface.md +++ b/docs/docusaurus/docs/How to/Run the simulator using main interface.md @@ -70,17 +70,13 @@ usage: main_cli.py -p python main_cli.py -p /path/to/parameters.yaml ``` - If you don't specify a parameters file, it will default to `input/parameters.yaml`: - ```bash - python main_cli.py - ``` - 3. **View Logs:** - The simulation will start, and logs will be displayed in the terminal. You can monitor these logs for progress and results. Logging is automatically set up via the `Logging.setup_logging()` function. + The simulation will start, and logs will be displayed in the terminal. You might also pass a log file with the + `-l` parameter. ## 5. Parameters File -The configuration file (`parameters.yaml`) should define the simulation parameters. +The configuration file should define the simulation parameters. ### Example `parameters.yaml` @@ -95,13 +91,16 @@ general: imt_link: DOWNLINK ########################################################################### # The chosen system for sharing study - # EESS_PASSIVE, FSS_SS, FSS_ES, FS, RAS, - system: RAS + system: SINGLE_EARTH_STATION ########################################################################### **(example)** ``` These parameters will be used by the simulator during execution, and you can modify them as needed. +The parameter file used for testing in `tests/parameters/parameters_for_testing.yaml` +is a good staring point for checking all possible parameters for every system implemented in the simulator. +For details of the parameter structure and logic refer to the `Parameter` package. You'll find all +the expected parameters and their validation. ## 6. Simulator Components diff --git a/sharc/campaigns/imt_hibs_ras_2600_MHz/README.md b/sharc/campaigns/imt_hibs_ras_2600_MHz/README.md deleted file mode 100644 index b84dfe69e..000000000 --- a/sharc/campaigns/imt_hibs_ras_2600_MHz/README.md +++ /dev/null @@ -1,39 +0,0 @@ - -# Spectrum Sharing Study: IMT HIBS vs. Radio Astronomy (2.6 GHz) -This directory holds the code and data for a simulation study investigating spectrum sharing between: - -IMT HIBS as a (NTN) system and Radio Astronomy Service (RAS) operating in the 2.6 GHz band. - -Each campaing puts the RAS station farther from the IMT BS nadir point over the Earth's surface. - -Main campaign parameters: -- IMT topolgy: NTN with IMT BS at 20km of altitude -- IMT @2680MHz/20MHz BW -- RAS @2695/10MHz BW -- Channel model: P.619 for both IMT and IMT-RAS links. - -# Folder Structure -inputs: This folder contains parameter files used to configure the simulation campaigns. Each file defines specific scenarios for the NTN and RAS systems. -scripts: This folder holds post-processing and plotting scripts used to analyze the simulation data. These scripts generate performance metrics and visualizations based on the simulation outputs. - -# Dependencies -This project may require additional software or libraries to run the simulations and post-processing scripts. Please refer to the individual script files for specific dependencies. - -# Running the Simulations -`python3 main_cli.py -p campaigns/imt_hibs_ras_2600_MHz/input/` -or on root -`python3 sharc/main_cli.py -p sharc/campaigns/imt_hibs_ras_2600_MHz/input/` - -# Running the scripts - -## For plotting - -`python3 sharc/campaigns/imt_hibs_ras_2600_MHz/scripts/plot_results.py` - -## For starting simulation multi thread - -`sharc/campaigns/imt_hibs_ras_2600_MHz/scripts/start_simulations_multi_thread.py` - -## For starting simulation single thread - -`sharc/campaigns/imt_hibs_ras_2600_MHz/scripts/start_simulations_single_thread.py` \ No newline at end of file diff --git a/sharc/campaigns/imt_hibs_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_0km.yaml b/sharc/campaigns/imt_hibs_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_0km.yaml deleted file mode 100644 index d54ba5d98..000000000 --- a/sharc/campaigns/imt_hibs_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_0km.yaml +++ /dev/null @@ -1,374 +0,0 @@ -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots : 1000 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link : DOWNLINK - ########################################################################### - # The chosen system for sharing study - # EESS_PASSIVE, FSS_SS, FSS_ES, FS, RAS, - system: SINGLE_EARTH_STATION - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel : FALSE - enable_adjacent_channel : TRUE - ########################################################################### - # Seed for random number generator - seed : 101 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output : FALSE - ########################################################################### - # output destination folder - this is relative SHARC/sharc directory - output_dir : campaigns/imt_hibs_ras_2600_MHz/output/ - ########################################################################### - # output folder prefix - output_dir_prefix : output_imt_hibs_ras_2600_MHz_0km -imt: - ########################################################################### - # Network topology. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR" - topology: - type: NTN - ntn: - ########################################################################### - # Number of clusters in NTN topology - num_clusters : 1 - ########################################################################### - # NTN cell radius in network topology [m] - cell_radius : 90000 - ########################################################################### - # Minimum 2D separation distance from BS to UE [m] - minimum_separation_distance_bs_ue : 0.5 - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE : IMT suffers interference - # FALSE : IMT generates interference - interfered_with : FALSE - ########################################################################### - # IMT center frequency [MHz] - frequency : 2680 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth : 20 - ########################################################################### - # IMT resource block bandwidth [MHz] - rb_bandwidth : 0.180 - ########################################################################### - # IMT spectrum emission mask. Options are: - # "IMT-2020" : for mmWave as described in ITU-R TG 5/1 Contribution 36 - # "3GPP E-UTRA" : for E-UTRA bands > 1 GHz as described in - # TS 36.104 v11.2.0 (BS) and TS 36.101 v11.2.0 (UE) - spectral_mask : 3GPP E-UTRA - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions : -13 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio : 0.1 - bs: - ########################################################################### - # The load probability (or activity factor) models the statistical - # variation of the network load by defining the number of fully loaded - # base stations that are simultaneously transmitting - load_probability : 1 - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power : 37 - ########################################################################### - # Base station height [m] - height : 20000 - ########################################################################### - # Base station noise figure [dB] - noise_figure : 5 - ########################################################################### - # User equipment noise temperature [K] - noise_temperature : 290 - ########################################################################### - # Base station array ohmic loss [dB] - ohmic_loss : 2 - antenna: - ########################################################################### - # If normalization of M2101 should be applied for BS - normalization : FALSE - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern : M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain : -200 - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor or NTN simulations - downtilt : 90 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g : 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db : 65 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db : 65 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows : 8 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns : 8 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing : 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing : 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am : 30 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v : 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor : 12 - uplink: - ########################################################################### - # Uplink attenuation factor used in link-to-system mapping - attenuation_factor : 0.4 - ########################################################################### - # Uplink minimum SINR of the code set [dB] - sinr_min : -10 - ########################################################################### - # Uplink maximum SINR of the code set [dB] - sinr_max : 22 - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k : 3 - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m : 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent : 0 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type : ANGLE_AND_DISTANCE - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance : RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth : NORMAL - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control : ON - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch : -91 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha : 1 - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax : 23 - ########################################################################### - # UE power dynamic range [dB] - # The minimum transmit power of a UE is (p_cmax - dynamic_range) - power_dynamic_range : 63 - ########################################################################### - # UE height [m] - height : 1.5 - ########################################################################### - # User equipment noise figure [dB] - noise_figure : 9 - ########################################################################### - # User equipment feed loss [dB] - ohmic_loss : 3 - ########################################################################### - # User equipment body loss [dB] - body_loss : 4 - antenna: - # If normalization of M2101 should be applied for UE - normalization : FALSE - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern : M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain : -200 - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g : 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db : 90 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db : 90 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows : 4 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns : 4 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing : 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing : 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am : 25 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v : 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor : 12 - downlink: - ########################################################################### - # Downlink attenuation factor used in link-to-system mapping - attenuation_factor : 0.8 - ########################################################################### - # Downlink minimum SINR of the code set [dB] - sinr_min : -10 - ########################################################################### - # Downlink maximum SINR of the code set [dB] - sinr_max : 30 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "CI" (close-in FS reference distance) - # "UMa" (Urban Macro - 3GPP) - # "UMi" (Urban Micro - 3GPP) - # "TVRO-URBAN" - # "TVRO-SUBURBAN" - # "ABG" (Alpha-Beta-Gamma) - # "P619" - channel_model : P619 - param_p619: - ########################################################################### - # Parameters for the P.619 propagation model - # For IMT NTN the model is used for calculating the coupling loss between - # the BS space station and the UEs on Earth's surface. - # space_station_alt_m - altitude of IMT space station (BS) (in meters) - # earth_station_alt_m - altitude of the system's earth station (in meters) - # earth_station_lat_deg - latitude of the system's earth station (in degrees) - # earth_station_long_diff_deg - difference between longitudes of IMT space station and system's earth station - # (positive if space-station is to the East of earth-station) - # season - season of the year. - # Enter the IMT space station height - space_station_alt_m : 20000.0 - # Enter the UE antenna heigth above sea level - earth_station_alt_m : 1000 - # The RAS station lat - earth_station_lat_deg : -15.7801 - # This parameter is ignored as it will be calculated from x,y positions in run time - earth_station_long_diff_deg : 0.0 - season : SUMMER - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model : SINGLE_ELEMENT -#### System Parameters -single_earth_station: - ########################################################################### - # NOTE: when using P.619 it is suggested that polarization loss = 3dB is normal - # also, - # NOTE: Verification needed: - # polarization mismatch between IMT BS and linear polarization = 3dB in earth P2P case? - # = 0 is safer choice - polarization_loss: 0.0 - ########################################################################### - # frequency [MHz] - frequency: 2695 - ########################################################################### - # bandwidth [MHz] - bandwidth: 10 - ########################################################################### - # Station noise temperature [K] - noise_temperature: 90 - ########################################################################### - # Peak transmit power spectral density (clear sky) [dBW/Hz] - tx_power_density: 0 - # Adjacent channel selectivity [dB] - adjacent_ch_selectivity: 20.0 - geometry: - ########################################################################### - # Antenna height [meters] - height: 15 - ########################################################################### - # Azimuth angle [degrees] - azimuth: - type: FIXED - fixed: -90 - ########################################################################### - # Elevation angle [degrees] - elevation: - type: FIXED - fixed: 45 - ########################################################################### - # Station 2d location [meters]: - location: - ########################################################################### - type: FIXED - fixed: - ########################################################################### - x: 0 - ########################################################################### - y: 0 - antenna: - pattern: OMNI - gain: 0 - channel_model: P619 - - param_p619: - space_station_alt_m: 20000.0 - # Enter the RAS antenna heigth above sea level - earth_station_alt_m: 15 - # The RAS station lat - earth_station_lat_deg: -23.17889 - # This parameter is ignored as it will be calculated from x,y positions in run time - earth_station_long_diff_deg: 0.0 - - season: SUMMER diff --git a/sharc/campaigns/imt_hibs_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_45km.yaml b/sharc/campaigns/imt_hibs_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_45km.yaml deleted file mode 100644 index 7345c4302..000000000 --- a/sharc/campaigns/imt_hibs_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_45km.yaml +++ /dev/null @@ -1,373 +0,0 @@ -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots : 1000 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link : DOWNLINK - ########################################################################### - # The chosen system for sharing study - # EESS_PASSIVE, FSS_SS, FSS_ES, FS, RAS, - system: SINGLE_EARTH_STATION - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel : FALSE - enable_adjacent_channel : TRUE - ########################################################################### - # Seed for random number generator - seed : 101 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output : FALSE - # output destination folder - this is relative SHARC/sharc directory - output_dir : campaigns/imt_hibs_ras_2600_MHz/output/ - ########################################################################### - # output folder prefix - output_dir_prefix : output_imt_hibs_ras_2600_MHz_45km -imt: - ########################################################################### - # Network topology. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR" - topology: - type: NTN - ntn: - ########################################################################### - # Number of clusters in NTN topology - num_clusters : 1 - ########################################################################### - # NTN cell radius in network topology [m] - cell_radius : 90000 - ########################################################################### - # Minimum 2D separation distance from BS to UE [m] - minimum_separation_distance_bs_ue : 0.5 - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE : IMT suffers interference - # FALSE : IMT generates interference - interfered_with : FALSE - ########################################################################### - # IMT center frequency [MHz] - frequency : 2680 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth : 20 - ########################################################################### - # IMT resource block bandwidth [MHz] - rb_bandwidth : 0.180 - ########################################################################### - # IMT spectrum emission mask. Options are: - # "IMT-2020" : for mmWave as described in ITU-R TG 5/1 Contribution 36 - # "3GPP E-UTRA" : for E-UTRA bands > 1 GHz as described in - # TS 36.104 v11.2.0 (BS) and TS 36.101 v11.2.0 (UE) - spectral_mask : 3GPP E-UTRA - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions : -13 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio : 0.1 - bs: - ########################################################################### - # The load probability (or activity factor) models the statistical - # variation of the network load by defining the number of fully loaded - # base stations that are simultaneously transmitting - load_probability : 1 - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power : 37 - ########################################################################### - # Base station height [m] - height : 20000 - ########################################################################### - # Base station noise figure [dB] - noise_figure : 5 - ########################################################################### - # User equipment noise temperature [K] - noise_temperature : 290 - ########################################################################### - # Base station array ohmic loss [dB] - ohmic_loss : 2 - antenna: - ########################################################################### - # If normalization of M2101 should be applied for BS - normalization : FALSE - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern : M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain : -200 - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor or NTN simulations - downtilt : 90 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g : 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db : 65 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db : 65 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows : 8 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns : 8 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing : 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing : 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am : 30 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v : 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor : 12 - uplink: - ########################################################################### - # Uplink attenuation factor used in link-to-system mapping - attenuation_factor : 0.4 - ########################################################################### - # Uplink minimum SINR of the code set [dB] - sinr_min : -10 - ########################################################################### - # Uplink maximum SINR of the code set [dB] - sinr_max : 22 - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k : 3 - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m : 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent : 0 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type : ANGLE_AND_DISTANCE - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance : RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth : NORMAL - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control : ON - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch : -91 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha : 1 - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax : 23 - ########################################################################### - # UE power dynamic range [dB] - # The minimum transmit power of a UE is (p_cmax - dynamic_range) - power_dynamic_range : 63 - ########################################################################### - # UE height [m] - height : 1.5 - ########################################################################### - # User equipment noise figure [dB] - noise_figure : 9 - ########################################################################### - # User equipment feed loss [dB] - ohmic_loss : 3 - ########################################################################### - # User equipment body loss [dB] - body_loss : 4 - antenna: - # If normalization of M2101 should be applied for UE - normalization : FALSE - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern : M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain : -200 - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g : 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db : 90 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db : 90 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows : 4 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns : 4 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing : 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing : 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am : 25 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v : 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor : 12 - downlink: - ########################################################################### - # Downlink attenuation factor used in link-to-system mapping - attenuation_factor : 0.8 - ########################################################################### - # Downlink minimum SINR of the code set [dB] - sinr_min : -10 - ########################################################################### - # Downlink maximum SINR of the code set [dB] - sinr_max : 30 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "CI" (close-in FS reference distance) - # "UMa" (Urban Macro - 3GPP) - # "UMi" (Urban Micro - 3GPP) - # "TVRO-URBAN" - # "TVRO-SUBURBAN" - # "ABG" (Alpha-Beta-Gamma) - # "P619" - channel_model : P619 - param_p619: - ########################################################################### - # Parameters for the P.619 propagation model - # For IMT NTN the model is used for calculating the coupling loss between - # the BS space station and the UEs on Earth's surface. - # space_station_alt_m - altitude of IMT space station (BS) (in meters) - # earth_station_alt_m - altitude of the system's earth station (in meters) - # earth_station_lat_deg - latitude of the system's earth station (in degrees) - # earth_station_long_diff_deg - difference between longitudes of IMT space station and system's earth station - # (positive if space-station is to the East of earth-station) - # season - season of the year. - # Enter the IMT space station height - space_station_alt_m : 20000.0 - # Enter the UE antenna heigth above sea level - earth_station_alt_m : 1000 - # The RAS station lat - earth_station_lat_deg : -15.7801 - # This parameter is ignored as it will be calculated from x,y positions in run time - earth_station_long_diff_deg : 0.0 - season : SUMMER - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model : SINGLE_ELEMENT -#### System Parameters -single_earth_station: - ########################################################################### - # NOTE: when using P.619 it is suggested that polarization loss = 3dB is normal - # also, - # NOTE: Verification needed: - # polarization mismatch between IMT BS and linear polarization = 3dB in earth P2P case? - # = 0 is safer choice - polarization_loss: 0.0 - ########################################################################### - # frequency [MHz] - frequency: 2695 - ########################################################################### - # bandwidth [MHz] - bandwidth: 10 - ########################################################################### - # Station noise temperature [K] - noise_temperature: 90 - ########################################################################### - # Peak transmit power spectral density (clear sky) [dBW/Hz] - tx_power_density: 0 - # Adjacent channel selectivity [dB] - adjacent_ch_selectivity: 20.0 - geometry: - ########################################################################### - # Antenna height [meters] - height: 15 - ########################################################################### - # Azimuth angle [degrees] - azimuth: - type: FIXED - fixed: -90 - ########################################################################### - # Elevation angle [degrees] - elevation: - type: FIXED - fixed: 45 - ########################################################################### - # Station 2d location [meters]: - location: - ########################################################################### - type: FIXED - fixed: - ########################################################################### - x: 45000 - ########################################################################### - y: 0 - antenna: - pattern: OMNI - gain: 0 - channel_model: P619 - - param_p619: - space_station_alt_m: 20000.0 - # Enter the RAS antenna heigth above sea level - earth_station_alt_m: 15 - # The RAS station lat - earth_station_lat_deg: -23.17889 - # This parameter is ignored as it will be calculated from x,y positions in run time - earth_station_long_diff_deg: 0.0 - - season: SUMMER diff --git a/sharc/campaigns/imt_hibs_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_500km.yaml b/sharc/campaigns/imt_hibs_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_500km.yaml deleted file mode 100644 index b1821243f..000000000 --- a/sharc/campaigns/imt_hibs_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_500km.yaml +++ /dev/null @@ -1,373 +0,0 @@ -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots : 1000 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link : DOWNLINK - ########################################################################### - # The chosen system for sharing study - # EESS_PASSIVE, FSS_SS, FSS_ES, FS, RAS, - system: SINGLE_EARTH_STATION - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel : FALSE - enable_adjacent_channel : TRUE - ########################################################################### - # Seed for random number generator - seed : 101 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output : FALSE - # output destination folder - this is relative SHARC/sharc directory - output_dir : campaigns/imt_hibs_ras_2600_MHz/output/ - ########################################################################### - # output folder prefix - output_dir_prefix : output_imt_hibs_ras_2600_MHz_500km -imt: - ########################################################################### - # Network topology. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR" - topology: - type: NTN - ntn: - ########################################################################### - # Number of clusters in NTN topology - num_clusters : 1 - ########################################################################### - # NTN cell radius in network topology [m] - cell_radius : 90000 - ########################################################################### - # Minimum 2D separation distance from BS to UE [m] - minimum_separation_distance_bs_ue : 0.5 - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE : IMT suffers interference - # FALSE : IMT generates interference - interfered_with : FALSE - ########################################################################### - # IMT center frequency [MHz] - frequency : 2680 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth : 20 - ########################################################################### - # IMT resource block bandwidth [MHz] - rb_bandwidth : 0.180 - ########################################################################### - # IMT spectrum emission mask. Options are: - # "IMT-2020" : for mmWave as described in ITU-R TG 5/1 Contribution 36 - # "3GPP E-UTRA" : for E-UTRA bands > 1 GHz as described in - # TS 36.104 v11.2.0 (BS) and TS 36.101 v11.2.0 (UE) - spectral_mask : 3GPP E-UTRA - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions : -13 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio : 0.1 - bs: - ########################################################################### - # The load probability (or activity factor) models the statistical - # variation of the network load by defining the number of fully loaded - # base stations that are simultaneously transmitting - load_probability : 1 - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power : 37 - ########################################################################### - # Base station height [m] - height : 20000 - ########################################################################### - # Base station noise figure [dB] - noise_figure : 5 - ########################################################################### - # User equipment noise temperature [K] - noise_temperature : 290 - ########################################################################### - # Base station array ohmic loss [dB] - ohmic_loss : 2 - antenna: - ########################################################################### - # If normalization of M2101 should be applied for BS - normalization : FALSE - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern : M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain : -200 - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor or NTN simulations - downtilt : 90 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g : 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db : 65 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db : 65 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows : 8 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns : 8 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing : 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing : 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am : 30 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v : 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor : 12 - uplink: - ########################################################################### - # Uplink attenuation factor used in link-to-system mapping - attenuation_factor : 0.4 - ########################################################################### - # Uplink minimum SINR of the code set [dB] - sinr_min : -10 - ########################################################################### - # Uplink maximum SINR of the code set [dB] - sinr_max : 22 - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k : 3 - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m : 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent : 0 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type : ANGLE_AND_DISTANCE - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance : RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth : NORMAL - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control : ON - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch : -91 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha : 1 - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax : 23 - ########################################################################### - # UE power dynamic range [dB] - # The minimum transmit power of a UE is (p_cmax - dynamic_range) - power_dynamic_range : 63 - ########################################################################### - # UE height [m] - height : 1.5 - ########################################################################### - # User equipment noise figure [dB] - noise_figure : 9 - ########################################################################### - # User equipment feed loss [dB] - ohmic_loss : 3 - ########################################################################### - # User equipment body loss [dB] - body_loss : 4 - antenna: - # If normalization of M2101 should be applied for UE - normalization : FALSE - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern : M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain : -200 - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g : 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db : 90 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db : 90 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows : 4 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns : 4 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing : 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing : 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am : 25 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v : 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor : 12 - downlink: - ########################################################################### - # Downlink attenuation factor used in link-to-system mapping - attenuation_factor : 0.8 - ########################################################################### - # Downlink minimum SINR of the code set [dB] - sinr_min : -10 - ########################################################################### - # Downlink maximum SINR of the code set [dB] - sinr_max : 30 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "CI" (close-in FS reference distance) - # "UMa" (Urban Macro - 3GPP) - # "UMi" (Urban Micro - 3GPP) - # "TVRO-URBAN" - # "TVRO-SUBURBAN" - # "ABG" (Alpha-Beta-Gamma) - # "P619" - channel_model : P619 - param_p619: - ########################################################################### - # Parameters for the P.619 propagation model - # For IMT NTN the model is used for calculating the coupling loss between - # the BS space station and the UEs on Earth's surface. - # space_station_alt_m - altitude of IMT space station (BS) (in meters) - # earth_station_alt_m - altitude of the system's earth station (in meters) - # earth_station_lat_deg - latitude of the system's earth station (in degrees) - # earth_station_long_diff_deg - difference between longitudes of IMT space station and system's earth station - # (positive if space-station is to the East of earth-station) - # season - season of the year. - # Enter the IMT space station height - space_station_alt_m : 20000.0 - # Enter the UE antenna heigth above sea level - earth_station_alt_m : 1000 - # The RAS station lat - earth_station_lat_deg : -15.7801 - # This parameter is ignored as it will be calculated from x,y positions in run time - earth_station_long_diff_deg : 0.0 - season : SUMMER - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model : SINGLE_ELEMENT -#### System Parameters -single_earth_station: - ########################################################################### - # NOTE: when using P.619 it is suggested that polarization loss = 3dB is normal - # also, - # NOTE: Verification needed: - # polarization mismatch between IMT BS and linear polarization = 3dB in earth P2P case? - # = 0 is safer choice - polarization_loss: 0.0 - ########################################################################### - # frequency [MHz] - frequency: 2695 - ########################################################################### - # bandwidth [MHz] - bandwidth: 10 - ########################################################################### - # Station noise temperature [K] - noise_temperature: 90 - ########################################################################### - # Peak transmit power spectral density (clear sky) [dBW/Hz] - tx_power_density: 0 - # Adjacent channel selectivity [dB] - adjacent_ch_selectivity: 20.0 - geometry: - ########################################################################### - # Antenna height [meters] - height: 15 - ########################################################################### - # Azimuth angle [degrees] - azimuth: - type: FIXED - fixed: -90 - ########################################################################### - # Elevation angle [degrees] - elevation: - type: FIXED - fixed: 45 - ########################################################################### - # Station 2d location [meters]: - location: - ########################################################################### - type: FIXED - fixed: - ########################################################################### - x: 500000 - ########################################################################### - y: 0 - antenna: - pattern: OMNI - gain: 0 - channel_model: P619 - - param_p619: - space_station_alt_m: 20000.0 - # Enter the RAS antenna heigth above sea level - earth_station_alt_m: 15 - # The RAS station lat - earth_station_lat_deg: -23.17889 - # This parameter is ignored as it will be calculated from x,y positions in run time - earth_station_long_diff_deg: 0.0 - - season: SUMMER diff --git a/sharc/campaigns/imt_hibs_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_90km.yaml b/sharc/campaigns/imt_hibs_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_90km.yaml deleted file mode 100644 index ae126fade..000000000 --- a/sharc/campaigns/imt_hibs_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_90km.yaml +++ /dev/null @@ -1,373 +0,0 @@ -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots : 1000 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link : DOWNLINK - ########################################################################### - # The chosen system for sharing study - # EESS_PASSIVE, FSS_SS, FSS_ES, FS, RAS, - system: SINGLE_EARTH_STATION - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel : FALSE - enable_adjacent_channel : TRUE - ########################################################################### - # Seed for random number generator - seed : 101 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output : FALSE - # output destination folder - this is relative SHARC/sharc directory - output_dir : campaigns/imt_hibs_ras_2600_MHz/output/ - ########################################################################### - # output folder prefix - output_dir_prefix : output_imt_hibs_ras_2600_MHz_90km -imt: - ########################################################################### - # Network topology. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR" - topology: - type: NTN - ntn: - ########################################################################### - # Number of clusters in NTN topology - num_clusters : 1 - ########################################################################### - # NTN cell radius in network topology [m] - cell_radius : 90000 - ########################################################################### - # Minimum 2D separation distance from BS to UE [m] - minimum_separation_distance_bs_ue : 0.5 - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE : IMT suffers interference - # FALSE : IMT generates interference - interfered_with : FALSE - ########################################################################### - # IMT center frequency [MHz] - frequency : 2680 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth : 20 - ########################################################################### - # IMT resource block bandwidth [MHz] - rb_bandwidth : 0.180 - ########################################################################### - # IMT spectrum emission mask. Options are: - # "IMT-2020" : for mmWave as described in ITU-R TG 5/1 Contribution 36 - # "3GPP E-UTRA" : for E-UTRA bands > 1 GHz as described in - # TS 36.104 v11.2.0 (BS) and TS 36.101 v11.2.0 (UE) - spectral_mask : 3GPP E-UTRA - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions : -13 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio : 0.1 - bs: - ########################################################################### - # The load probability (or activity factor) models the statistical - # variation of the network load by defining the number of fully loaded - # base stations that are simultaneously transmitting - load_probability : 1 - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power : 37 - ########################################################################### - # Base station height [m] - height : 20000 - ########################################################################### - # Base station noise figure [dB] - noise_figure : 5 - ########################################################################### - # User equipment noise temperature [K] - noise_temperature : 290 - ########################################################################### - # Base station array ohmic loss [dB] - ohmic_loss : 2 - antenna: - ########################################################################### - # If normalization of M2101 should be applied for BS - normalization : FALSE - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern : M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain : -200 - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor or NTN simulations - downtilt : 90 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g : 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db : 65 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db : 65 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows : 8 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns : 8 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing : 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing : 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am : 30 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v : 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor : 12 - uplink: - ########################################################################### - # Uplink attenuation factor used in link-to-system mapping - attenuation_factor : 0.4 - ########################################################################### - # Uplink minimum SINR of the code set [dB] - sinr_min : -10 - ########################################################################### - # Uplink maximum SINR of the code set [dB] - sinr_max : 22 - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k : 3 - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m : 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent : 0 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type : ANGLE_AND_DISTANCE - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance : RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth : NORMAL - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control : ON - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch : -91 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha : 1 - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax : 23 - ########################################################################### - # UE power dynamic range [dB] - # The minimum transmit power of a UE is (p_cmax - dynamic_range) - power_dynamic_range : 63 - ########################################################################### - # UE height [m] - height : 1.5 - ########################################################################### - # User equipment noise figure [dB] - noise_figure : 9 - ########################################################################### - # User equipment feed loss [dB] - ohmic_loss : 3 - ########################################################################### - # User equipment body loss [dB] - body_loss : 4 - antenna: - # If normalization of M2101 should be applied for UE - normalization : FALSE - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern : M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain : -200 - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g : 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db : 90 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db : 90 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows : 4 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns : 4 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing : 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing : 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am : 25 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v : 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor : 12 - downlink: - ########################################################################### - # Downlink attenuation factor used in link-to-system mapping - attenuation_factor : 0.8 - ########################################################################### - # Downlink minimum SINR of the code set [dB] - sinr_min : -10 - ########################################################################### - # Downlink maximum SINR of the code set [dB] - sinr_max : 30 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "CI" (close-in FS reference distance) - # "UMa" (Urban Macro - 3GPP) - # "UMi" (Urban Micro - 3GPP) - # "TVRO-URBAN" - # "TVRO-SUBURBAN" - # "ABG" (Alpha-Beta-Gamma) - # "P619" - channel_model : P619 - param_p619: - ########################################################################### - # Parameters for the P.619 propagation model - # For IMT NTN the model is used for calculating the coupling loss between - # the BS space station and the UEs on Earth's surface. - # space_station_alt_m - altitude of IMT space station (BS) (in meters) - # earth_station_alt_m - altitude of the system's earth station (in meters) - # earth_station_lat_deg - latitude of the system's earth station (in degrees) - # earth_station_long_diff_deg - difference between longitudes of IMT space station and system's earth station - # (positive if space-station is to the East of earth-station) - # season - season of the year. - # Enter the IMT space station height - space_station_alt_m : 20000.0 - # Enter the UE antenna heigth above sea level - earth_station_alt_m : 1000 - # The RAS station lat - earth_station_lat_deg : -15.7801 - # This parameter is ignored as it will be calculated from x,y positions in run time - earth_station_long_diff_deg : 0.0 - season : SUMMER - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model : SINGLE_ELEMENT -#### System Parameters -single_earth_station: - ########################################################################### - # NOTE: when using P.619 it is suggested that polarization loss = 3dB is normal - # also, - # NOTE: Verification needed: - # polarization mismatch between IMT BS and linear polarization = 3dB in earth P2P case? - # = 0 is safer choice - polarization_loss: 0.0 - ########################################################################### - # frequency [MHz] - frequency: 2695 - ########################################################################### - # bandwidth [MHz] - bandwidth: 10 - ########################################################################### - # Station noise temperature [K] - noise_temperature: 90 - ########################################################################### - # Peak transmit power spectral density (clear sky) [dBW/Hz] - tx_power_density: 0 - # Adjacent channel selectivity [dB] - adjacent_ch_selectivity: 20.0 - geometry: - ########################################################################### - # Antenna height [meters] - height: 15 - ########################################################################### - # Azimuth angle [degrees] - azimuth: - type: FIXED - fixed: -90 - ########################################################################### - # Elevation angle [degrees] - elevation: - type: FIXED - fixed: 45 - ########################################################################### - # Station 2d location [meters]: - location: - ########################################################################### - type: FIXED - fixed: - ########################################################################### - x: 90000 - ########################################################################### - y: 0 - antenna: - pattern: OMNI - gain: 0 - channel_model: P619 - - param_p619: - space_station_alt_m: 20000.0 - # Enter the RAS antenna heigth above sea level - earth_station_alt_m: 15 - # The RAS station lat - earth_station_lat_deg: -23.17889 - # This parameter is ignored as it will be calculated from x,y positions in run time - earth_station_long_diff_deg: 0.0 - - season: SUMMER diff --git a/sharc/campaigns/imt_hibs_ras_2600_MHz/output/output-folder.md b/sharc/campaigns/imt_hibs_ras_2600_MHz/output/output-folder.md deleted file mode 100644 index aef44111d..000000000 --- a/sharc/campaigns/imt_hibs_ras_2600_MHz/output/output-folder.md +++ /dev/null @@ -1,3 +0,0 @@ -# output folder -This folder holds simulation output data. -Don't push the output files to the repository. \ No newline at end of file diff --git a/sharc/campaigns/imt_hibs_ras_2600_MHz/scripts/plot_results.py b/sharc/campaigns/imt_hibs_ras_2600_MHz/scripts/plot_results.py deleted file mode 100644 index 92d8a0725..000000000 --- a/sharc/campaigns/imt_hibs_ras_2600_MHz/scripts/plot_results.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -Script for post-processing and plotting IMT HIBS RAS 2600 MHz simulation results. -Adds legends to result folders and generates plots using SHARC's PostProcessor. -""" -import os -from pathlib import Path -from sharc.results import Results -# import plotly.graph_objects as go -from sharc.post_processor import PostProcessor - -post_processor = PostProcessor() - -# Add a legend to results in folder that match the pattern -# This could easily come from a config file -post_processor\ - .add_plot_legend_pattern( - dir_name_contains="_0km", - legend="0 Km" - ).add_plot_legend_pattern( - dir_name_contains="_45km", - legend="45 Km" - ).add_plot_legend_pattern( - dir_name_contains="_90km", - legend="90 Km" - ).add_plot_legend_pattern( - dir_name_contains="_500km", - legend="500 Km" - ) - -campaign_base_dir = str((Path(__file__) / ".." / "..").resolve()) - -many_results = Results.load_many_from_dir( - os.path.join( - campaign_base_dir, - "output"), - only_latest=True) -# ^: typing.List[Results] - -post_processor.add_results(many_results) - -plots = post_processor.generate_cdf_plots_from_results( - many_results -) - -post_processor.add_plots(plots) - -# # This function aggregates IMT downlink and uplink -# aggregated_results = PostProcessor.aggregate_results( -# downlink_result=post_processor.get_results_by_output_dir("MHz_60deg_dl"), -# uplink_result=post_processor.get_results_by_output_dir("MHz_60deg_ul"), -# ul_tdd_factor=(3, 4), -# n_bs_sim=7 * 19 * 3 * 3, -# n_bs_actual=int -# ) - -# Add a protection criteria line: -# protection_criteria = 160 - -# post_processor\ -# .get_plot_by_results_attribute_name("system_dl_interf_power")\ -# .add_vline(protection_criteria, line_dash="dash") - -# Show a single plot: -# post_processor\ -# .get_plot_by_results_attribute_name("system_dl_interf_power")\ -# .show() - -# Plot every plot: -for plot in plots: - plot.show() - -for result in many_results: - # This generates the mean, median, variance, etc - stats = PostProcessor.generate_statistics( - result=result - ).write_to_results_dir() - # # do whatever you want here: - # if "fspl_45deg" in stats.results_output_dir: - # get some stat and do something - -# # example on how to aggregate results and add it to plot: -# dl_res = post_processor.get_results_by_output_dir("1_cluster") -# aggregated_results = PostProcessor.aggregate_results( -# dl_samples=dl_res.system_dl_interf_power, -# ul_samples=ul_res.system_ul_interf_power, -# ul_tdd_factor=0.75, -# n_bs_sim=1 * 19 * 3 * 3, -# n_bs_actual=7 * 19 * 3 * 3 -# ) - -# relevant = post_processor\ -# .get_plot_by_results_attribute_name("system_ul_interf_power") - -# aggr_x, aggr_y = PostProcessor.cdf_from(aggregated_results) - -# relevant.add_trace( -# go.Scatter(x=aggr_x, y=aggr_y, mode='lines', name='Aggregate interference',), -# ) - -# relevant.show() diff --git a/sharc/campaigns/imt_hibs_ras_2600_MHz/scripts/start_simulations_multi_thread.py b/sharc/campaigns/imt_hibs_ras_2600_MHz/scripts/start_simulations_multi_thread.py deleted file mode 100644 index 69455420e..000000000 --- a/sharc/campaigns/imt_hibs_ras_2600_MHz/scripts/start_simulations_multi_thread.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -Script to start IMT HIBS RAS 2600 MHz simulations in multi-threaded mode using SHARC. -""" -from sharc.run_multiple_campaigns_mut_thread import run_campaign - -# Set the campaign name -# The name of the campaign to run. This should match the name of the -# campaign directory. -name_campaign = "imt_hibs_ras_2600_MHz" - -# Run the campaigns -# This function will execute the campaign with the given name. -# It will look for the campaign directory under the specified name and -# start the necessary processes. -run_campaign(name_campaign) diff --git a/sharc/campaigns/imt_hibs_ras_2600_MHz/scripts/start_simulations_single_thread.py b/sharc/campaigns/imt_hibs_ras_2600_MHz/scripts/start_simulations_single_thread.py deleted file mode 100644 index 7246055e5..000000000 --- a/sharc/campaigns/imt_hibs_ras_2600_MHz/scripts/start_simulations_single_thread.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -Script to start IMT HIBS RAS 2600 MHz simulation in single-threaded mode using SHARC. -""" -from sharc.run_multiple_campaigns import run_campaign - -# Set the campaign name -# The name of the campaign to run. This should match the name of the -# campaign directory. -name_campaign = "imt_hibs_ras_2600_MHz" - -# Run the campaign in single-thread mode -# This function will execute the campaign with the given name in a single-threaded manner. -# It will look for the campaign directory under the specified name and -# start the necessary processes. -run_campaign(name_campaign) diff --git a/sharc/campaigns/imt_hotspot_eess_active/comparison/contribution_20.csv b/sharc/campaigns/imt_hotspot_eess_active/comparison/contribution_20.csv deleted file mode 100644 index 65ce1b706..000000000 --- a/sharc/campaigns/imt_hotspot_eess_active/comparison/contribution_20.csv +++ /dev/null @@ -1,78 +0,0 @@ -# Contrib 20 INR -x,y --11.358820993101236,-0.000281063728990949 --11.138772461094177,-0.00022228782340016906 --10.918723929087118,-0.00004596010662760719 --10.69867539708006,-0.00022228782340016906 --10.478626865073,0.0009532302884172061 --10.258578333065941,0.002892835172915831 --10.038529801058882,0.005361423207732585 --9.818481269051821,0.009416960693502618 --9.598432737044762,0.017586811570633465 --9.378384205037703,0.030399958989443254 --9.188342291031606,0.04291922688029848 --9.048311407027114,0.05725075186020567 --8.928284935023264,0.07169003266702956 --8.808258463019413,0.08647951991133218 --8.720461321461041,0.10189758281264838 --8.646794280442805,0.11634712607917863 --8.568205519011713,0.1316561503461149 --8.498190077009466,0.14625706489331347 --8.42817463500722,0.16285146223846914 --8.358159193004974,0.1800923945451245 --8.29814595700305,0.19604025692878047 --8.238132721001124,0.21209587513935335 --8.178119484999199,0.22944456327292517 --8.118106248997274,0.24700876306033026 --8.049519693566502,0.2664355992844364 --7.988077570993102,0.2838612558658058 --7.948068746991819,0.29765400171113 --7.918062128990856,0.3116191568795207 --7.878053304989573,0.32778253091701004 --7.8180400689876475,0.34506656555443194 --7.748024626985401,0.36279239908221184 --7.718018008984439,0.3791174068600761 --7.674675116316383,0.39366444349381646 --7.634666292315099,0.4093967942236393 --7.594657468313816,0.4252369007803789 --7.5546486443125325,0.4402149607217857 --7.510472234477781,0.4571865034611494 --7.447958446975775,0.475936017344637 --7.405449071474411,0.49508961557906184 --7.354604524306115,0.5120880972751547 --7.31459570030483,0.5267428897358117 --7.274586876303546,0.5419364613310517 --7.234578052302263,0.5579920795416243 --7.186472204395958,0.5751098623318035 --7.147892266966149,0.5917196533665188 --7.107883442964866,0.6066977133079254 --7.067874618963582,0.6215680174224156 --7.027865794962299,0.6358995424023228 --6.997859176961336,0.6492397137745973 --6.946419260388257,0.6647011355681728 --6.909268209529921,0.6822499416660184 --6.857828292956844,0.6987642918254646 --6.817819468955561,0.7152078310129371 --6.7578062329536355,0.7330952982810919 --6.69779299695171,0.7520603238184126 --6.637779760949785,0.7696245236058177 --6.57776652494786,0.7883740374893052 --6.517753288945933,0.8053994581421273 --6.457740052944008,0.8204852739104507 --6.387724610941762,0.8370257933421481 --6.307706962939195,0.8552095891343235 --6.227689314936628,0.8722619487438747 --6.147671666934061,0.8881020553006143 --6.092659533932297,0.9005478533094811 --5.997638576929248,0.9182467478805318 --5.88761431092572,0.9344909387882085 --5.757585632921547,0.9490841564906275 --5.597550336916413,0.9638005237095225 --5.397506216909996,0.9776912293974982 --5.177457684902937,0.9869778224808556 --4.9574091528958775,0.9917386708337161 --4.737360620888818,0.9944423624908962 --4.517312088881759,0.9956766565083044 --4.2972635568747,0.9966170709977584 --4.077215024867641,0.9972636059592579 --3.937184140863149,0.9976358533613334 diff --git a/sharc/campaigns/imt_hotspot_eess_active/input/parameters_imt_hotspot_eess_active_beam_small_dl.yaml b/sharc/campaigns/imt_hotspot_eess_active/input/parameters_imt_hotspot_eess_active_beam_small_dl.yaml deleted file mode 100644 index 06747adbf..000000000 --- a/sharc/campaigns/imt_hotspot_eess_active/input/parameters_imt_hotspot_eess_active_beam_small_dl.yaml +++ /dev/null @@ -1,376 +0,0 @@ -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots: 2400 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link: DOWNLINK - ########################################################################### - # The chosen system for sharing study - # EESS_SS, FSS_SS, FSS_ES, FS, RAS - system: EESS_SS - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel: TRUE - enable_adjacent_channel: FALSE - ########################################################################### - # Seed for random number generator - seed: 31 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output: FALSE - ########################################################################### - # output destination folder - this is relative SHARC/sharc directory - output_dir: campaigns/imt_hotspot_eess_active/output/ - ########################################################################### - # output folder prefix - output_dir_prefix: output_imt_hotspot_eess_active_beam_small_dl -imt: - ########################################################################### - # Network topology. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR" - topology: - type: HOTSPOT - hotspot: - ########################################################################### - # Number of hotspots per macro cell (sector) - num_hotspots_per_cell: 1 - ########################################################################### - # Maximum 2D distance between hotspot and UE [m] - # This is the hotspot radius - max_dist_hotspot_ue: 100 - ########################################################################### - # Minimum 2D distance between macro cell base station and hotspot [m] - min_dist_bs_hotspot: 0 - ########################################################################### - # Enable wrap around. Available only for "MACROCELL" and "HOTSPOT" topologies - wrap_around: FALSE - ########################################################################### - # Number of clusters in macro cell topology - num_clusters: 7 - ########################################################################### - # Inter-site distance in macrocell network topology [m] - #intersite_distance = 1740 - #intersite_distance = 665 - intersite_distance: 660 - ########################################################################### - # Minimum 2D separation distance from BS to UE [m] - minimum_separation_distance_bs_ue: 0 - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE : IMT suffers interference - # FALSE : IMT generates interference - interfered_with: FALSE - ########################################################################### - # IMT center frequency [MHz] - frequency: 10250 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth: 100 - ########################################################################### - # IMT resource block bandwidth [MHz] - rb_bandwidth: 0.180 - ########################################################################### - # IMT spectrum emission mask. Options are: - # "IMT-2020" : for mmWave as described in ITU-R TG 5/1 Contribution 36 - # "3GPP E-UTRA" : for E-UTRA bands > 1 GHz as described in - # TS 36.104 v11.2.0 (BS) and TS 36.101 v11.2.0 (UE) - spectral_mask: IMT-2020 - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions: -13 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio: 0.1 - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model: BEAMFORMING - bs: - ########################################################################### - # The load probability (or activity factor) models the statistical - # variation of the network load by defining the number of fully loaded - # base stations that are simultaneously transmitting - load_probability: .2 - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power: 16 - ########################################################################### - # Base station height [m] - height: 6 - ########################################################################### - # Base station noise figure [dB] - noise_figure: 10 - ########################################################################### - # User equipment noise temperature [K] - noise_temperature: 290 - ########################################################################### - # Base station array ohmic loss [dB] - ohmic_loss: 0 - antenna: - ########################################################################### - # If normalization of M2101 should be applied for BS - normalization: FALSE - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor simulations - downtilt: 10 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g: 5.5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 120 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 120 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 8 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 8 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 30 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - uplink: - ########################################################################### - # Uplink attenuation factor used in link-to-system mapping - attenuation_factor: 1.0 - ########################################################################### - # Uplink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Uplink maximum SINR of the code set [dB] - sinr_max: 22 - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k: 3 - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m: 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent: 5 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type: ANGLE_AND_DISTANCE - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance: RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth: NORMAL - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control: ON - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch: -95 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha: 1 - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax: 23 - ########################################################################### - # UE power dynamic range [dB] - # The minimum transmit power of a UE is (p_cmax - dynamic_range) - power_dynamic_range: 63 - ########################################################################### - # UE height [m] - height: 1.5 - ########################################################################### - # User equipment noise figure [dB] - noise_figure: 10 - ########################################################################### - # User equipment feed loss [dB] - ohmic_loss: 0 - ########################################################################### - # User equipment body loss [dB] - body_loss: 4 - antenna: - ########################################################################### - # If normalization of M2101 should be applied for UE - normalization: FALSE - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: FIXED - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor simulations - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g: -4 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 360 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 180 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 1 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 1 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 25 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - downlink: - ########################################################################### - # Downlink attenuation factor used in link-to-system mapping - attenuation_factor: 1.0 - ########################################################################### - # Downlink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Downlink maximum SINR of the code set [dB] - sinr_max: 30 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "CI" (close-in FS reference distance) - # "UMa" (Urban Macro - 3GPP) - # "UMi" (Urban Micro - 3GPP) - # "TVRO-URBAN" - # "TVRO-SUBURBAN" - # "ABG" (Alpha-Beta-Gamma) - channel_model: UMi - ########################################################################### - # Adjustment factor for LoS probability in UMi path loss model. - # Original value: 18 (3GPP) - los_adjustment_factor: 29 - ########################################################################### - # If shadowing should be applied or not - shadowing: TRUE - ########################################################################### - # System receive noise temperature [K] - noise_temperature: 500 -eess_ss: - ########################################################################### - # sensor center frequency [MHz] - frequency: 9800 - ########################################################################### - # sensor bandwidth [MHz] - bandwidth: 1200 - ###########Creates a statistical distribution of nadir angle############### - ##############following variables nadir_angle_distribution################# - # if distribution_enable = ON, nadir_angle will vary statistically######### - # if distribution_enable = OFF, nadir_angle follow nadir_angle variable ### - # distribution_type = UNIFORM - # UNIFORM = UNIFORM distribution in nadir_angle - # - nadir_angle_distribution = initial nadir angle, final nadir angle - distribution_enable: OFF - distribution_type: UNIFORM - nadir_angle_distribution: 18.6,49.4 - ########################################################################### - # Off-nadir pointing angle [deg] - nadir_angle: 18 - ########################################################################### - # sensor altitude [m] - altitude: 514000.0 - ########################################################################### - # Antenna pattern of the sensor - # Possible values: "ITU-R RS.1813" - # "ITU-R RS.1861 9a" - # "ITU-R RS.1861 9b" - # "ITU-R RS.1861 9c" - # "ITU-R RS.2043" - # "OMNI" - antenna_pattern: ITU-R RS.2043 - # Antenna efficiency for pattern described in ITU-R RS.1813 [0-1] - antenna_efficiency: 1.0 - # Antenna diameter for ITU-R RS.1813 [m] - antenna_diameter: 2.2 - ########################################################################### - # receive antenna gain - applicable for 9a, 9b and OMNI [dBi] - antenna_gain: 47 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "P619" - channel_model: P619 - # Parameters for the P.619 propagation model - # space_station_alt_m - altitude of space station - # earth_station_alt_m - altitude of IMT system (in meters) - # earth_station_lat_deg - latitude of IMT system (in degrees) - # earth_station_long_diff_deg - difference between longitudes of IMT and satellite system - # (positive if space-station is to the East of earth-station) - # season - season of the year. - earth_station_alt_m: 1172 - earth_station_lat_deg: -15.8 - earth_station_long_diff_deg: 0.0 - season: SUMMER diff --git a/sharc/campaigns/imt_hotspot_eess_active/input/parameters_imt_hotspot_eess_active_beam_small_ul.yaml b/sharc/campaigns/imt_hotspot_eess_active/input/parameters_imt_hotspot_eess_active_beam_small_ul.yaml deleted file mode 100644 index ccb1d74f2..000000000 --- a/sharc/campaigns/imt_hotspot_eess_active/input/parameters_imt_hotspot_eess_active_beam_small_ul.yaml +++ /dev/null @@ -1,371 +0,0 @@ -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots: 2400 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link: UPLINK - ########################################################################### - # The chosen system for sharing study - # EESS_SS, FSS_SS, FSS_ES, FS, RAS - system: EESS_SS - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel: TRUE - enable_adjacent_channel: FALSE - ########################################################################### - # Seed for random number generator - seed: 101 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output: FALSE - ########################################################################### - # output destination folder - this is relative SHARC/sharc directory - output_dir: campaigns/imt_hotspot_eess_active/output/ - ########################################################################### - # output folder prefix - output_dir_prefix: output_imt_hotspot_eess_active_beam_small_ul -imt: - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model: SINGLE_ELEMENT - ########################################################################### - # Network topology. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR" - topology: - type: HOTSPOT - hotspot: - ########################################################################### - # Number of hotspots per macro cell (sector) - num_hotspots_per_cell: 1 - ########################################################################### - # Maximum 2D distance between hotspot and UE [m] - # This is the hotspot radius - max_dist_hotspot_ue: 100 - ########################################################################### - # Minimum 2D distance between macro cell base station and hotspot [m] - min_dist_bs_hotspot: 0 - ########################################################################### - # Enable wrap around. Available only for "MACROCELL" and "HOTSPOT" topologies - wrap_around: FALSE - ########################################################################### - # Number of clusters in macro cell topology - num_clusters: 7 - ########################################################################### - # Inter-site distance in macrocell network topology [m] - intersite_distance: 660 - ########################################################################### - # Minimum 2D separation distance from BS to UE [m] - minimum_separation_distance_bs_ue: 0 - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE : IMT suffers interference - # FALSE : IMT generates interference - interfered_with: FALSE - ########################################################################### - # IMT center frequency [MHz] - frequency: 10250 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth: 100 - ########################################################################### - # IMT resource block bandwidth [MHz] - rb_bandwidth: 0.180 - ########################################################################### - # IMT spectrum emission mask. Options are: - # "IMT-2020" : for mmWave as described in ITU-R TG 5/1 Contribution 36 - # "3GPP E-UTRA" : for E-UTRA bands > 1 GHz as described in - # TS 36.104 v11.2.0 (BS) and TS 36.101 v11.2.0 (UE) - spectral_mask: IMT-2020 - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions: -13 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio: 0.1 - bs: - ########################################################################### - # The load probability (or activity factor) models the statistical - # variation of the network load by defining the number of fully loaded - # base stations that are simultaneously transmitting - load_probability: .2 - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power: 16 - ########################################################################### - # Base station height [m] - height: 6 - ########################################################################### - # Base station noise figure [dB] - noise_figure: 10 - ########################################################################### - # User equipment noise temperature [K] - noise_temperature: 290 - ########################################################################### - # Base station array ohmic loss [dB] - ohmic_loss: 3 - antenna: - ########################################################################### - # If normalization of M2101 should be applied for BS - normalization: FALSE - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor simulations - downtilt: 10 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g: 5.5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 90 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 90 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 8 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 8 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 30 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - uplink: - ########################################################################### - # Uplink attenuation factor used in link-to-system mapping - attenuation_factor: 1.0 - ########################################################################### - # Uplink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Uplink maximum SINR of the code set [dB] - sinr_max: 22 - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k: 3 - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m: 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent: 5 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type: ANGLE_AND_DISTANCE - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance: RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth: NORMAL - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control: ON - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch: -95 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha: 1 - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax: 23 - ########################################################################### - # UE power dynamic range [dB] - # The minimum transmit power of a UE is (p_cmax - dynamic_range) - power_dynamic_range: 63 - ########################################################################### - # UE height [m] - height: 1.5 - ########################################################################### - # User equipment noise figure [dB] - noise_figure: 10 - ########################################################################### - # User equipment feed loss [dB] - ohmic_loss: 0 - ########################################################################### - # User equipment body loss [dB] - body_loss: 4 - antenna: - ########################################################################### - # If normalization of M2101 should be applied for UE - normalization: FALSE - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: FIXED - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g: -4 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 360 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 180 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 1 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 1 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 25 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - downlink: - ########################################################################### - # Downlink attenuation factor used in link-to-system mapping - attenuation_factor: 1.0 - ########################################################################### - # Downlink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Downlink maximum SINR of the code set [dB] - sinr_max: 30 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "CI" (close-in FS reference distance) - # "UMa" (Urban Macro - 3GPP) - # "UMi" (Urban Micro - 3GPP) - # "TVRO-URBAN" - # "TVRO-SUBURBAN" - # "ABG" (Alpha-Beta-Gamma) - channel_model: UMi - ########################################################################### - # Adjustment factor for LoS probability in UMi path loss model. - # Original value: 18 (3GPP) - los_adjustment_factor: 29 - ########################################################################### - # If shadowing should be applied or not - shadowing: TRUE - ########################################################################### - # System receive noise temperature [K] - noise_temperature: 500 -eess_ss: - ########################################################################### - # sensor center frequency [MHz] - frequency: 9800 - ########################################################################### - # sensor bandwidth [MHz] - bandwidth: 1200 - ###########Creates a statistical distribution of nadir angle############### - ##############following variables nadir_angle_distribution################# - # if distribution_enable = ON, nadir_angle will vary statistically######### - # if distribution_enable = OFF, nadir_angle follow nadir_angle variable ### - # distribution_type = UNIFORM - # UNIFORM = UNIFORM distribution in nadir_angle - # - nadir_angle_distribution = initial nadir angle, final nadir angle - distribution_enable: OFF - distribution_type: UNIFORM - nadir_angle_distribution: 18.6,49.4 - ########################################################################### - # Off-nadir pointing angle [deg] - nadir_angle: 18 - ########################################################################### - # sensor altitude [m] - altitude: 514000.0 - ########################################################################### - # Antenna pattern of the sensor - # Possible values: "ITU-R RS.1813" - # "ITU-R RS.1861 9a" - # "ITU-R RS.1861 9b" - # "ITU-R RS.1861 9c" - # "ITU-R RS.2043" - # "OMNI" - antenna_pattern: ITU-R RS.2043 - # Antenna efficiency for pattern described in ITU-R RS.1813 [0-1] - antenna_efficiency: 0.7 - # Antenna diameter for ITU-R RS.1813 [m] - antenna_diameter: 2.2 - ########################################################################### - # receive antenna gain - applicable for 9a, 9b and OMNI [dBi] - antenna_gain: 47 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "P619" - channel_model: P619 - # Parameters for the P.619 propagation model - # space_station_alt_m - altitude of space station - # earth_station_alt_m - altitude of IMT system (in meters) - # earth_station_lat_deg - latitude of IMT system (in degrees) - # earth_station_long_diff_deg - difference between longitudes of IMT and satellite system - # (positive if space-station is to the East of earth-station) - # season - season of the year. - earth_station_alt_m: 1172 - earth_station_lat_deg: -15.8 - earth_station_long_diff_deg: 0.0 - season: SUMMER diff --git a/sharc/campaigns/imt_hotspot_eess_active/readme.md b/sharc/campaigns/imt_hotspot_eess_active/readme.md deleted file mode 100644 index 66a2e8738..000000000 --- a/sharc/campaigns/imt_hotspot_eess_active/readme.md +++ /dev/null @@ -1,15 +0,0 @@ -# IMT Hotspot EESS Active Campaign - -This campaign's objective is to try and replicate Luciano's contribution 20. - -Using the script `plot_results.py` automatically aggregates results and compares it to Luciano's INR results in -contrib 20 - -The document that should be referenced is -"HIBS and IMT-2020 Coexistence with other Space/Terrestrial -Communications and Radar Systems", a Doctoral Thesis by Luciano Camilo Alexandre publicated on Dec 2022 - -The document can be downloaded found [here](https://www2.inatel.br/biblioteca/teses-de-doutorado) -and -[downloaded here](https://biblioteca.inatel.br/cict/acervo%20publico/sumarios/Teses%20de%20Doutorado%20do%20Inatel/Luciano%20Camilo%20Alexandre.pdf) - diff --git a/sharc/campaigns/imt_hotspot_eess_active/scripts/plot_results.py b/sharc/campaigns/imt_hotspot_eess_active/scripts/plot_results.py deleted file mode 100644 index c2dd66160..000000000 --- a/sharc/campaigns/imt_hotspot_eess_active/scripts/plot_results.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -Script for post-processing and plotting IMT Hotspot EESS Active simulation results. -Adds legends to result folders and generates plots using SHARC's PostProcessor. -""" -import os -from pathlib import Path -from sharc.results import Results -import plotly.graph_objects as go -from sharc.post_processor import PostProcessor -import pandas - -post_processor = PostProcessor() - -# Add a legend to results in folder that match the pattern -# This could easily come from a config file -post_processor\ - .add_plot_legend_pattern( - dir_name_contains="beam_small_dl", - legend="Small Beam DL" - ).add_plot_legend_pattern( - dir_name_contains="beam_small_ul", - legend="Small Beam UL" - ) - -campaign_base_dir = str((Path(__file__) / ".." / "..").resolve()) - -many_results = Results.load_many_from_dir( - os.path.join( - campaign_base_dir, - "output"), - only_latest=True) - -post_processor.add_results(many_results) - -plots = post_processor.generate_cdf_plots_from_results( - many_results -) - -post_processor.add_plots(plots) - -# This function aggregates IMT downlink and uplink -aggregated_results = PostProcessor.aggregate_results( - dl_samples=post_processor.get_results_by_output_dir("_dl").system_inr, - ul_samples=post_processor.get_results_by_output_dir("_ul").system_inr, - ul_tdd_factor=0.25, - # SF is not exactly 1, but approx - n_bs_sim=1, - n_bs_actual=1 -) - -# Add a protection criteria line: -# protection_criteria = int - -# post_processor\ -# .get_plot_by_results_attribute_name("system_dl_interf_power")\ -# .add_vline(protection_criteria, line_dash="dash") - -# Show a single plot: -relevant = post_processor\ - .get_plot_by_results_attribute_name("system_inr") - -aggr_x, aggr_y = PostProcessor.cdf_from(aggregated_results) - -relevant.add_trace( - go.Scatter( - x=aggr_x, - y=aggr_y, - mode='lines', - name='Aggregate interference', - ), -) - -compare_to = pandas.read_csv( - os.path.join(campaign_base_dir, "comparison", "contribution_20.csv"), - skiprows=1 -) - -comp_x, comp_y = (compare_to.iloc[:, 0], compare_to.iloc[:, 1]) - -relevant.add_trace( - go.Scatter(x=comp_x, y=comp_y, mode='lines', name='Contrib 20 INR',), -) - -relevant.show() - -for result in many_results: - # This generates the mean, median, variance, etc - stats = PostProcessor.generate_statistics( - result=result - ).write_to_results_dir() diff --git a/sharc/campaigns/imt_hotspot_eess_active/scripts/start_simulations_multi_thread.py b/sharc/campaigns/imt_hotspot_eess_active/scripts/start_simulations_multi_thread.py deleted file mode 100644 index e77abad9d..000000000 --- a/sharc/campaigns/imt_hotspot_eess_active/scripts/start_simulations_multi_thread.py +++ /dev/null @@ -1,12 +0,0 @@ -from sharc.run_multiple_campaigns_mut_thread import run_campaign - -# Set the campaign name -# The name of the campaign to run. This should match the name of the -# campaign directory. -name_campaign = "imt_hotspot_eess_active" - -# Run the campaigns -# This function will execute the campaign with the given name. -# It will look for the campaign directory under the specified name and -# start the necessary processes. -run_campaign(name_campaign) diff --git a/sharc/campaigns/imt_hotspot_eess_active/scripts/start_simulations_single_thread.py b/sharc/campaigns/imt_hotspot_eess_active/scripts/start_simulations_single_thread.py deleted file mode 100644 index 1be6efde8..000000000 --- a/sharc/campaigns/imt_hotspot_eess_active/scripts/start_simulations_single_thread.py +++ /dev/null @@ -1,12 +0,0 @@ -from sharc.run_multiple_campaigns import run_campaign - -# Set the campaign name -# The name of the campaign to run. This should match the name of the -# campaign directory. -name_campaign = "imt_hotspot_eess_active" - -# Run the campaign in single-thread mode -# This function will execute the campaign with the given name in a single-threaded manner. -# It will look for the campaign directory under the specified name and -# start the necessary processes. -run_campaign(name_campaign) diff --git a/sharc/campaigns/imt_hotspot_eess_passive/comparison/Fig. 15 (IMT Uplink) EESS (Passive) Sensor.csv b/sharc/campaigns/imt_hotspot_eess_passive/comparison/Fig. 15 (IMT Uplink) EESS (Passive) Sensor.csv deleted file mode 100644 index cd95f43fe..000000000 --- a/sharc/campaigns/imt_hotspot_eess_passive/comparison/Fig. 15 (IMT Uplink) EESS (Passive) Sensor.csv +++ /dev/null @@ -1,37 +0,0 @@ -# Fig 15 -x, y --159.05775075987842, 0.9633492947617223 --158.48024316109422, 0.9543984596408218 --157.75075987841944, 0.9633492947617223 --156.99088145896656, 0.9194190830166598 --156.50455927051672, 0.8453314502151946 --156.20060790273556, 0.7699924848865801 --155.95744680851064, 0.6570033251510908 --155.80547112462006, 0.5658518012594294 --155.62310030395136, 0.49191706844917393 --155.56231003039514, 0.43165336925959 --155.44072948328267, 0.3787724458933014 --155.34954407294833, 0.3201882609885405 --155.22796352583586, 0.2583224566423367 --155.10638297872342, 0.20647375917915298 --154.98480243161094, 0.1531563949210873 --154.89361702127658, 0.13190773718012405 --154.83282674772036, 0.11574800476981631 --154.74164133738603, 0.0850608522508284 --154.68085106382978, 0.06611047389133969 --154.62006079027356, 0.05333680870333699 --154.52887537993922, 0.041842885079015825 --154.43768996960486, 0.032520870510413455 --154.40729483282675, 0.024808162306509743 --154.28571428571428, 0.018061622323899008 --154.25531914893617, 0.014708472321254355 --154.19452887537994, 0.012318009665808635 --154.19452887537994, 0.010609029955405396 --154.16413373860183, 0.009137151183368584 --154.10334346504558, 0.007869478368773596 --154.07294832826747, 0.005267707132875599 --154.01215805471125, 0.004370594758137502 --153.9209726443769, 0.0028984256351332048 --153.82978723404256, 0.001958357122672717 --153.76899696048633, 0.0014526539259467812 --153.7386018237082, 0.0010284002230430917 \ No newline at end of file diff --git a/sharc/campaigns/imt_hotspot_eess_passive/comparison/Fig. 8 EESS (Passive) Sensor.csv b/sharc/campaigns/imt_hotspot_eess_passive/comparison/Fig. 8 EESS (Passive) Sensor.csv deleted file mode 100644 index 729134dc3..000000000 --- a/sharc/campaigns/imt_hotspot_eess_passive/comparison/Fig. 8 EESS (Passive) Sensor.csv +++ /dev/null @@ -1,27 +0,0 @@ -# Results collected from luciano's contribution 21, Figure 8. -x,y --160.4357066950053,0.9613501484827673 --159.798087141339,0.9613501484827673 --158.84165781083954,0.9613501484827673 --157.778958554729,0.9519233878378311 --156.58342189160467,0.9333462695554301 --155.3347502656748,0.8797619862955459 --153.9798087141339,0.7663916856958233 --153.182784272051,0.6231307348505225 --152.4123273113709,0.45910450561838223 --151.96068012752391,0.344987324360697 --151.5356004250797,0.25417657344926176 --151.29649309245482,0.20064350554165133 --150.8182784272051,0.12877841672100215 --150.57917109458023,0.1026626022035896 --150.26036131774708,0.06334492703460955 --149.9415515409139,0.044426921419985395 --149.70244420828905,0.02768378019158192 --149.43676939426143,0.01794415314903341 --149.0913921360255,0.010333925713156772 --148.93198724760893,0.00631371983651216 --148.63974495217855,0.003530115158319504 --148.50690754516472,0.0026008856310466007 --148.34750265674813,0.0017194043905352397 --148.1615302869288,0.0011592951222250565 --148.1615302869288,0.0011592951222250565 diff --git a/sharc/campaigns/imt_hotspot_eess_passive/input/parameters_imt_macro_eess_passive_1_cluster_DL.yaml b/sharc/campaigns/imt_hotspot_eess_passive/input/parameters_imt_macro_eess_passive_1_cluster_DL.yaml deleted file mode 100644 index 9440c604d..000000000 --- a/sharc/campaigns/imt_hotspot_eess_passive/input/parameters_imt_macro_eess_passive_1_cluster_DL.yaml +++ /dev/null @@ -1,270 +0,0 @@ -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots: 10000 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link: DOWNLINK - ########################################################################### - # The chosen system for sharing study - # EESS_SS, FSS_SS, FSS_ES, FS, RAS - system: EESS_SS - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel: FALSE - enable_adjacent_channel: TRUE - ########################################################################### - # Seed for random number generator - seed: 39 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output: FALSE - ########################################################################### - # output destination folder - this is relative SHARC/sharc directory - output_dir: campaigns/imt_hotspot_eess_passive/output/ - ########################################################################### - # output folder prefix - output_dir_prefix: output_imt_hotspot_eess_passive_1_cluster_DL -imt: - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE : IMT suffers interference - # FALSE : IMT generates interference - interfered_with: FALSE - ########################################################################### - # IMT center frequency [MHz] - frequency: 10250 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth: 100 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio: 0.1 - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions: -13 - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model: BEAMFORMING - bs: - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power: 16 - ########################################################################### - # Base station height [m] - height: 6 - ########################################################################### - # Base station array ohmic loss [dB] - ohmic_loss: 0 - antenna: - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor simulations - downtilt: 10 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 8 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 8 - # BS/UE horizontal 3dB beamwidth of single element [degrees]. - element_phi_3db: 120.0 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 90.0 - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g: 5.5 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5195 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5195 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - # element_sla_v = 30 - enable_beamsteering_vertical_limit: "ON" - beamsteering_vertical_limit: 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - topology: - ########################################################################### - # Network topology. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR" - type: HOTSPOT - hotspot: - ########################################################################### - # Enable wrap around. Available only for "MACROCELL" and "HOTSPOT" topologies - wrap_around: FALSE - ########################################################################### - # Number of clusters in macro cell topology - num_clusters: 1 - ########################################################################### - # Inter-site distance in macrocell network topology [m] - # 1,14884 / 1,000,000 m2 - intersite_distance: 4647 - ########################################################################### - # Number of hotspots per macro cell (sector) - num_hotspots_per_cell: 3 - # ########################################################################### - # # Maximum 2D distance between hotspot and UE [m] - # # This is the hotspot radius - # max_dist_hotspot_ue: 100 - # ########################################################################### - # # Minimum 2D distance between macro cell base station and hotspot [m] - # min_dist_bs_hotspot: 0 - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k: 3 - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m: 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent: 5 - ########################################################################### - # UE height [m] - height: 1.5 - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control: ON - ########################################################################### - # User equipment body loss [dB] - body_loss: 4 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type: ANGLE_AND_DISTANCE - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance: RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth: NORMAL - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax: 23 - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch: -95 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha: 1 - antenna: - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 1 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 1 - # BS/UE horizontal 3dB beamwidth of single element [degrees]. - element_phi_3db: 360 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 180 - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: FIXED - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g: -4 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - # element_sla_v = 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 -eess_ss: - # sensor C7 - ########################################################################### - # satellite altitude [m] - altitude: 407000.0 - ########################################################################### - # Off-nadir pointing angle [deg] - nadir_angle: 48.6 - ########################################################################### - # satellite center frequency [MHz] - frequency: 10650 - ########################################################################### - # satellite bandwidth [MHz] - bandwidth: 100 - ###########Creates a statistical distribution of nadir angle############### - ##############following variables nadir_angle_distribution################# - # if distribution_enable = ON, nadir_angle will vary statistically######### - # if distribution_enable = OFF, nadir_angle follow nadir_angle variable ### - # distribution_type = UNIFORM - # UNIFORM = UNIFORM distribution in nadir_angle - # - nadir_angle_distribution = initial nadir angle, final nadir angle - distribution_enable: OFF - # # distribution_type = UNIFORM - # # nadir_angle_distribution = 18.6,49.4 - # ########################################################################### - # Antenna pattern of the sensor - # Possible values: "ITU-R RS.1813" - # "ITU-R RS.1861 9a" - # "ITU-R RS.1861 9b" - # "ITU-R RS.1861 9c" - # "ITU-R RS.2043" - # "OMNI" - antenna_pattern: ITU-R RS.1813 - # Antenna efficiency for pattern described in ITU-R RS.1813 [0-1] - antenna_efficiency: 0.606 - # Antenna diameter for ITU-R RS.1813 [m] - antenna_diameter: 1.1 - ########################################################################### - # receive antenna gain - applicable for 9a, 9b and OMNI [dBi] - antenna_gain: 39.6 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "P619" - channel_model: P619 - # earth station at BrasĆ­lia - Brasil - earth_station_alt_m: 6 - earth_station_lat_deg: -15.8 - earth_station_long_diff_deg: 0.0 - season: SUMMER diff --git a/sharc/campaigns/imt_hotspot_eess_passive/input/parameters_imt_macro_eess_passive_1_cluster_DL_alternative.yaml b/sharc/campaigns/imt_hotspot_eess_passive/input/parameters_imt_macro_eess_passive_1_cluster_DL_alternative.yaml deleted file mode 100644 index 82c6210ce..000000000 --- a/sharc/campaigns/imt_hotspot_eess_passive/input/parameters_imt_macro_eess_passive_1_cluster_DL_alternative.yaml +++ /dev/null @@ -1,278 +0,0 @@ -# ALTERNATIVE: -# set the following attributes to use default: -# bs_element_phi_3db: 90.0 -# bs_element_theta_3db: 90.0 -# element_spacing: 5 -# REASONING: -# Figure 6 presents element pattern found by using the above config - -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots: 10000 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link: DOWNLINK - ########################################################################### - # The chosen system for sharing study - # EESS_SS, FSS_SS, FSS_ES, FS, RAS - system: EESS_SS - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel: FALSE - enable_adjacent_channel: TRUE - ########################################################################### - # Seed for random number generator - seed: 39 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output: FALSE - ########################################################################### - # output destination folder - this is relative SHARC/sharc directory - output_dir: campaigns/imt_hotspot_eess_passive/output/ - ########################################################################### - # output folder prefix - output_dir_prefix: output_imt_hotspot_eess_passive_1_cluster_DL_alternative -imt: - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE : IMT suffers interference - # FALSE : IMT generates interference - interfered_with: FALSE - ########################################################################### - # IMT center frequency [MHz] - frequency: 10250 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth: 100 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio: 0.1 - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions: -13 - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model: BEAMFORMING - bs: - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power: 16 - ########################################################################### - # Base station height [m] - height: 6 - ########################################################################### - # Base station array ohmic loss [dB] - ohmic_loss: 0 - antenna: - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor simulations - downtilt: 10 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 8 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 8 - # BS/UE horizontal 3dB beamwidth of single element [degrees]. - element_phi_3db: 90.0 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 90.0 - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g: 5.5 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - # element_sla_v = 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - topology: - ########################################################################### - # Network topology. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR" - type: HOTSPOT - hotspot: - ########################################################################### - # Enable wrap around. Available only for "MACROCELL" and "HOTSPOT" topologies - wrap_around: FALSE - ########################################################################### - # Number of clusters in macro cell topology - num_clusters: 1 - ########################################################################### - # Inter-site distance in macrocell network topology [m] - # 1,14884 / 1,000,000 m2 - intersite_distance: 4647 - ########################################################################### - # Number of hotspots per macro cell (sector) - num_hotspots_per_cell: 3 - # ########################################################################### - # # Maximum 2D distance between hotspot and UE [m] - # # This is the hotspot radius - # max_dist_hotspot_ue: 100 - # ########################################################################### - # # Minimum 2D distance between macro cell base station and hotspot [m] - # min_dist_bs_hotspot: 0 - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k: 3 - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m: 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent: 5 - ########################################################################### - # UE height [m] - height: 1.5 - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control: ON - ########################################################################### - # User equipment body loss [dB] - body_loss: 4 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type: ANGLE_AND_DISTANCE - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance: RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth: NORMAL - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax: 23 - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch: -95 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha: 1 - antenna: - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 1 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 1 - # BS/UE horizontal 3dB beamwidth of single element [degrees]. - element_phi_3db: 360 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 180 - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: FIXED - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g: -4 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - # element_sla_v = 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - - -eess_ss: - # sensor C7 - ########################################################################### - # satellite altitude [m] - altitude: 407000.0 - ########################################################################### - # Off-nadir pointing angle [deg] - nadir_angle: 48.6 - ########################################################################### - # satellite center frequency [MHz] - frequency: 10650 - ########################################################################### - # satellite bandwidth [MHz] - bandwidth: 100 - ###########Creates a statistical distribution of nadir angle############### - ##############following variables nadir_angle_distribution################# - # if distribution_enable = ON, nadir_angle will vary statistically######### - # if distribution_enable = OFF, nadir_angle follow nadir_angle variable ### - # distribution_type = UNIFORM - # UNIFORM = UNIFORM distribution in nadir_angle - # - nadir_angle_distribution = initial nadir angle, final nadir angle - distribution_enable: OFF - # # distribution_type = UNIFORM - # # nadir_angle_distribution = 18.6,49.4 - # ########################################################################### - # Antenna pattern of the sensor - # Possible values: "ITU-R RS.1813" - # "ITU-R RS.1861 9a" - # "ITU-R RS.1861 9b" - # "ITU-R RS.1861 9c" - # "ITU-R RS.2043" - # "OMNI" - antenna_pattern: ITU-R RS.1813 - # Antenna efficiency for pattern described in ITU-R RS.1813 [0-1] - antenna_efficiency: 0.606 - # Antenna diameter for ITU-R RS.1813 [m] - antenna_diameter: 1.1 - ########################################################################### - # receive antenna gain - applicable for 9a, 9b and OMNI [dBi] - antenna_gain: 39.6 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "P619" - channel_model: P619 - # earth station at BrasĆ­lia - Brasil - earth_station_alt_m: 6 - earth_station_lat_deg: -15.8 - earth_station_long_diff_deg: 0.0 - season: SUMMER diff --git a/sharc/campaigns/imt_hotspot_eess_passive/input/parameters_imt_macro_eess_passive_1_cluster_UL.yaml b/sharc/campaigns/imt_hotspot_eess_passive/input/parameters_imt_macro_eess_passive_1_cluster_UL.yaml deleted file mode 100644 index ac91bca59..000000000 --- a/sharc/campaigns/imt_hotspot_eess_passive/input/parameters_imt_macro_eess_passive_1_cluster_UL.yaml +++ /dev/null @@ -1,271 +0,0 @@ -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots: 10000 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link: UPLINK - ########################################################################### - # The chosen system for sharing study - # EESS_SS, FSS_SS, FSS_ES, FS, RAS - system: EESS_SS - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel: FALSE - enable_adjacent_channel: TRUE - ########################################################################### - # Seed for random number generator - seed: 57 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output: FALSE - ########################################################################### - # output destination folder - this is relative SHARC/sharc directory - output_dir: campaigns/imt_hotspot_eess_passive/output/ - ########################################################################### - # output folder prefix - output_dir_prefix: output_imt_hotspot_eess_passive_1_cluster_UL -imt: - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE : IMT suffers interference - # FALSE : IMT generates interference - interfered_with: FALSE - ########################################################################### - # IMT center frequency [MHz] - frequency: 10250 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth: 100 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio: 0.1 - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions: -13 - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model: SINGLE_ELEMENT - bs: - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power: 16 - ########################################################################### - # Base station height [m] - height: 6 - ########################################################################### - # Base station array ohmic loss [dB] - ohmic_loss: 0 - antenna: - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor simulations - downtilt: 10 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 8 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 8 - # BS/UE horizontal 3dB beamwidth of single element [degrees]. - element_phi_3db: 120.0 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 90.0 - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g: 5.5 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5195 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5195 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - # element_sla_v = 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - ########################################################################### - # Network topology. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR" - topology: - type: HOTSPOT - hotspot: - ########################################################################### - # Enable wrap around. Available only for "MACROCELL" and "HOTSPOT" topologies - wrap_around: FALSE - ########################################################################### - # Number of clusters in macro cell topology - num_clusters: 1 - ########################################################################### - # Inter-site distance in macrocell network topology [m] - intersite_distance: 4647 - ########################################################################### - # Number of hotspots per macro cell (sector) - num_hotspots_per_cell: 3 - # ########################################################################### - # # Maximum 2D distance between hotspot and UE [m] - # # This is the hotspot radius - # max_dist_hotspot_ue: 100 - # ########################################################################### - # # Minimum 2D distance between macro cell base station and hotspot [m] - # min_dist_bs_hotspot: 0 - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k: 3 - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m: 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent: 5 - ########################################################################### - # UE height [m] - height: 1.5 - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control: ON - ########################################################################### - # User equipment body loss [dB] - body_loss: 4 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type: ANGLE_AND_DISTANCE - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance: RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth: NORMAL - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax: 23 - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch: -95 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha: 1 - antenna: - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 1 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 1 - # BS/UE horizontal 3dB beamwidth of single element [degrees]. - element_phi_3db: 360 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 180 - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: FIXED - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g: -4 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - # element_sla_v = 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 -eess_ss: - # sensor C7 - ########################################################################### - # satellite altitude [m] - altitude: 407000.0 - ########################################################################### - # Off-nadir pointing angle [deg] - nadir_angle: 48.6 - ########################################################################### - # satellite center frequency [MHz] - frequency: 10650 - ########################################################################### - # satellite bandwidth [MHz] - bandwidth: 100 - ###########Creates a statistical distribution of nadir angle############### - ##############following variables nadir_angle_distribution################# - # if distribution_enable = ON, nadir_angle will vary statistically######### - # if distribution_enable = OFF, nadir_angle follow nadir_angle variable ### - # distribution_type = UNIFORM - # UNIFORM = UNIFORM distribution in nadir_angle - # - nadir_angle_distribution = initial nadir angle, final nadir angle - distribution_enable: OFF - # # distribution_type = UNIFORM - # # nadir_angle_distribution = 18.6,49.4 - # ########################################################################### - # Antenna pattern of the sensor - # Possible values: "ITU-R RS.1813" - # "ITU-R RS.1861 9a" - # "ITU-R RS.1861 9b" - # "ITU-R RS.1861 9c" - # "ITU-R RS.2043" - # "OMNI" - antenna_pattern: ITU-R RS.1813 - # Antenna efficiency for pattern described in ITU-R RS.1813 [0-1] - antenna_efficiency: 0.606 - # Antenna diameter for ITU-R RS.1813 [m] - antenna_diameter: 1.1 - ########################################################################### - # receive antenna gain - applicable for 9a, 9b and OMNI [dBi] - antenna_gain: 39.6 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "P619" - channel_model: P619 - # earth station at BrasĆ­lia - Brasil - earth_station_alt_m: 1.5 - earth_station_lat_deg: -15.8 - earth_station_long_diff_deg: 0.0 - season: SUMMER diff --git a/sharc/campaigns/imt_hotspot_eess_passive/readme.md b/sharc/campaigns/imt_hotspot_eess_passive/readme.md deleted file mode 100644 index 8867c10f8..000000000 --- a/sharc/campaigns/imt_hotspot_eess_passive/readme.md +++ /dev/null @@ -1,16 +0,0 @@ -# IMT Hotspot EESS Passive Campaign - -This campaign's objective is to try and replicate Luciano's contribution 21. - -You can compare SHARC results against Luciano's results with `plot_results.py`. -It automatically aggregates UL and DL considering TDD and SF (segment factor). - -We did not take the time to process the Uplink analysis to try and reach the same results. - -The document that should be referenced is -"HIBS and IMT-2020 Coexistence with other Space/Terrestrial -Communications and Radar Systems", a Doctoral Thesis by Luciano Camilo Alexandre publicated on Dec 2022 - -The document can be downloaded found [here](https://www2.inatel.br/biblioteca/teses-de-doutorado) -and -[downloaded here](https://biblioteca.inatel.br/cict/acervo%20publico/sumarios/Teses%20de%20Doutorado%20do%20Inatel/Luciano%20Camilo%20Alexandre.pdf) diff --git a/sharc/campaigns/imt_hotspot_eess_passive/scripts/plot_results.py b/sharc/campaigns/imt_hotspot_eess_passive/scripts/plot_results.py deleted file mode 100644 index 530640842..000000000 --- a/sharc/campaigns/imt_hotspot_eess_passive/scripts/plot_results.py +++ /dev/null @@ -1,150 +0,0 @@ -import os -from pathlib import Path -from sharc.results import Results -import plotly.graph_objects as go -from sharc.post_processor import PostProcessor -import pandas - -post_processor = PostProcessor() - -# Add a legend to results in folder that match the pattern -# This could easily come from a config file -post_processor\ - .add_plot_legend_pattern( - dir_name_contains="1_cluster_DL_alternative", - legend="Alternative Downlink" - ).add_plot_legend_pattern( - dir_name_contains="1_cluster_DL", - legend="Downlink" - ).add_plot_legend_pattern( - dir_name_contains="1_cluster_UL", - legend="Uplink" - ) - -campaign_base_dir = str((Path(__file__) / ".." / "..").resolve()) - -many_results = Results.load_many_from_dir( - os.path.join( - campaign_base_dir, - "output"), - only_latest=True) - -post_processor.add_results(many_results) - -plots = post_processor.generate_cdf_plots_from_results( - many_results -) - -post_processor.add_plots(plots) - -uplink_interf_samples = post_processor.get_results_by_output_dir( - "_UL").system_ul_interf_power - -# This function aggregates IMT downlink and uplink -aggregated_results = PostProcessor.aggregate_results( - dl_samples=post_processor.get_results_by_output_dir( - "_DL_alternative").system_dl_interf_power, - ul_samples=uplink_interf_samples, - ul_tdd_factor=0.25, - # SF is not exactly 3, but approx - n_bs_sim=1, - n_bs_actual=3 -) - -# Add a protection criteria line: -# protection_criteria = int - -# post_processor\ -# .get_plot_by_results_attribute_name("system_dl_interf_power")\ -# .add_vline(protection_criteria, line_dash="dash") - -# Show a single plot: -relevant = post_processor\ - .get_plot_by_results_attribute_name("system_dl_interf_power") - -# Title of CDF updated because ul interf power will be included -relevant.update_layout( - title='CDF Plot for Interference', -) - -aggr_x, aggr_y = PostProcessor.cdf_from(uplink_interf_samples) - -relevant.add_trace( - go.Scatter(x=aggr_x, y=aggr_y, mode='lines', name='Uplink',), -) - -aggr_x, aggr_y = PostProcessor.cdf_from(aggregated_results) - -relevant.add_trace( - go.Scatter( - x=aggr_x, - y=aggr_y, - mode='lines', - name='Aggregate interference', - ), -) - -# TODO: put some more stuff into PostProcessor if ends up being really used -compare_to = pandas.read_csv( - os.path.join( - campaign_base_dir, - "comparison", - "Fig. 8 EESS (Passive) Sensor.csv"), - skiprows=1) - -comp_x, comp_y = (compare_to.iloc[:, 0], compare_to.iloc[:, 1]) -# inverting given chart from P of I > x to P of I < x -comp_y = 1 - comp_y -# converting dB to dBm -comp_x = comp_x + 30 - -relevant.add_trace( - go.Scatter( - x=comp_x, - y=comp_y, - mode='lines', - name='Fig. 8 EESS (Passive) Sensor', - ), -) - -compare_to = pandas.read_csv( - os.path.join( - campaign_base_dir, - "comparison", - "Fig. 15 (IMT Uplink) EESS (Passive) Sensor.csv"), - skiprows=1) - -comp_x, comp_y = (compare_to.iloc[:, 0], compare_to.iloc[:, 1]) -# inverting given chart from P of I > x to P of I < x -comp_y = 1 - comp_y -# converting dB to dBm -comp_x = comp_x + 30 - -relevant.add_trace( - go.Scatter( - x=comp_x, - y=comp_y, - mode='lines', - name='Fig. 15 (IMT Uplink) EESS (Passive) Sensor', - ), -) - -relevant.show() - -post_processor\ - .get_plot_by_results_attribute_name("system_ul_interf_power").show() - -# for result in many_results: -# # This generates the mean, median, variance, etc -# stats = PostProcessor.generate_statistics( -# result=result -# ).write_to_results_dir() - -aggregated_res_statistics = PostProcessor.generate_sample_statistics( - "Aggregate Results Statistics", - aggregated_results -) - -print("\n###########################") - -print(aggregated_res_statistics) diff --git a/sharc/campaigns/imt_hotspot_eess_passive/scripts/start_simulations_multi_thread.py b/sharc/campaigns/imt_hotspot_eess_passive/scripts/start_simulations_multi_thread.py deleted file mode 100644 index d6da27253..000000000 --- a/sharc/campaigns/imt_hotspot_eess_passive/scripts/start_simulations_multi_thread.py +++ /dev/null @@ -1,12 +0,0 @@ -from sharc.run_multiple_campaigns_mut_thread import run_campaign - -# Set the campaign name -# The name of the campaign to run. This should match the name of the -# campaign directory. -name_campaign = "imt_hotspot_eess_passive" - -# Run the campaigns -# This function will execute the campaign with the given name. -# It will look for the campaign directory under the specified name and -# start the necessary processes. -run_campaign(name_campaign) diff --git a/sharc/campaigns/imt_hotspot_eess_passive/scripts/start_simulations_single_thread.py b/sharc/campaigns/imt_hotspot_eess_passive/scripts/start_simulations_single_thread.py deleted file mode 100644 index fe3a8c9ac..000000000 --- a/sharc/campaigns/imt_hotspot_eess_passive/scripts/start_simulations_single_thread.py +++ /dev/null @@ -1,12 +0,0 @@ -from sharc.run_multiple_campaigns import run_campaign - -# Set the campaign name -# The name of the campaign to run. This should match the name of the -# campaign directory. -name_campaign = "imt_hotspot_eess_passive" - -# Run the campaign in single-thread mode -# This function will execute the campaign with the given name in a single-threaded manner. -# It will look for the campaign directory under the specified name and -# start the necessary processes. -run_campaign(name_campaign) diff --git a/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_30deg.yaml b/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_30deg.yaml deleted file mode 100644 index 56da3c38f..000000000 --- a/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_30deg.yaml +++ /dev/null @@ -1,387 +0,0 @@ -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots: 1000 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link: DOWNLINK - ########################################################################### - # The chosen system for sharing study - # EESS_PASSIVE, FSS_SS, FSS_ES, FS, RAS, - system: SINGLE_EARTH_STATION - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel: FALSE - enable_adjacent_channel: TRUE - ########################################################################### - # Seed for random number generator - seed: 101 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output: FALSE - ########################################################################### - # output destination folder - this is relative SHARC/sharc directory - output_dir: campaigns/imt_mss_ras_2600_MHz/output/ - ########################################################################### - # output folder prefix - output_dir_prefix: output_imt_mss_ras_2600_MHz_30deg -imt: - topology: - ########################################################################### - # Network topology. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR" - type: NTN - ntn: - ########################################################################### - # NTN cell radius in network topology [m] - cell_radius: 90000 - ########################################################################### - # Number of clusters in macro cell topology - num_clusters: 1 - bs_elevation: 30 - ########################################################################### - # Minimum 2D separation distance from BS to UE [m] - minimum_separation_distance_bs_ue: 0.5 - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE : IMT suffers interference - # FALSE : IMT generates interference - interfered_with: FALSE - ########################################################################### - # IMT center frequency [MHz] - frequency: 2680.0 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth: 20 - ########################################################################### - # IMT resource block bandwidth [MHz] - rb_bandwidth: 0.180 - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model: SINGLE_ELEMENT - ########################################################################### - # IMT spectrum emission mask. Options are: - # "IMT-2020" : for mmWave as described in ITU-R TG 5/1 Contribution 36 - # "3GPP E-UTRA" : for E-UTRA bands > 1 GHz as described in - # TS 36.104 v11.2.0 (BS) and TS 36.101 v11.2.0 (UE) - spectral_mask: 3GPP E-UTRA - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions: -13 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio: 0.1 - bs: - ########################################################################### - # The load probability (or activity factor) models the statistical - # variation of the network load by defining the number of fully loaded - # base stations that are simultaneously transmitting - load_probability: 1 - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power: 37 - ########################################################################### - # Base station height [m] - height: 20000 - ########################################################################### - # Base station noise figure [dB] - noise_figure: 5 - ########################################################################### - # User equipment noise temperature [K] - noise_temperature: 290 - ########################################################################### - # Base station array ohmic loss [dB] - ohmic_loss: 2 - - antenna: - ########################################################################### - # If normalization of M2101 should be applied for BS - normalization: FALSE - ########################################################################### - # File to be used in the BS beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/bs_norm.npz - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor or NTN simulations - downtilt: 90 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g: 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 65 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 65 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 8 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 8 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 30 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - - uplink: - ########################################################################### - # Uplink attenuation factor used in link-to-system mapping - attenuation_factor: 0.4 - ########################################################################### - # Uplink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Uplink maximum SINR of the code set [dB] - sinr_max: 22 - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k: 3 - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m: 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent: 0 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type: ANGLE_AND_DISTANCE - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance: RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth: NORMAL - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control: ON - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch: -91 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha: 1 - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax: 23 - ########################################################################### - # UE power dynamic range [dB] - # The minimum transmit power of a UE is (p_cmax - dynamic_range) - power_dynamic_range: 63 - ########################################################################### - # UE height [m] - height: 1.5 - ########################################################################### - # User equipment noise figure [dB] - noise_figure: 9 - ########################################################################### - # User equipment feed loss [dB] - ohmic_loss: 3 - ########################################################################### - # User equipment body loss [dB] - body_loss: 4 - - antenna: - ########################################################################### - # If normalization of M2101 should be applied for UE - normalization: FALSE - ########################################################################### - # File to be used in the UE beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/ue_norm.npz - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g: 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 90 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 90 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 4 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 4 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 25 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - - downlink: - ########################################################################### - # Downlink attenuation factor used in link-to-system mapping - attenuation_factor: 0.8 - ########################################################################### - # Downlink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Downlink maximum SINR of the code set [dB] - sinr_max: 30 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "CI" (close-in FS reference distance) - # "UMa" (Urban Macro - 3GPP) - # "UMi" (Urban Micro - 3GPP) - # "TVRO-URBAN" - # "TVRO-SUBURBAN" - # "ABG" (Alpha-Beta-Gamma) - # "P619" - channel_model: P619 - - param_p619: - ########################################################################### - # altitude of the space station [m] - space_station_alt_m: 20000.0 - ########################################################################### - # altitude of ES system [m] - earth_station_alt_m: 1000 - ########################################################################### - # latitude of ES system [m] - earth_station_lat_deg: -15.7801 - ########################################################################### - # difference between longitudes of IMT-NTN station and FSS-ES system [deg] - # (positive if space-station is to the East of earth-station) - earth_station_long_diff_deg: 0.0 - - season: SUMMER -#### System Parameters -single_earth_station: - ########################################################################### - # NOTE: when using P.619 it is suggested that polarization loss = 3dB is normal - # also, - # NOTE: Verification needed: - # polarization mismatch between IMT BS and linear polarization = 3dB in earth P2P case? - # = 0 is safer choice - polarization_loss: 0.0 - ########################################################################### - frequency: 2695 - ########################################################################### - bandwidth: 10 - ########################################################################### - noise_temperature: 90 - ########################################################################### - # Peak transmit power spectral density (clear sky) [dBW/Hz] - tx_power_density: 0 - # Adjacent channel selectivity [dB] - adjacent_ch_selectivity: 20.0 - #### - geometry: - ########################################################################### - # Antenna height [meters] - height: 15 - ########################################################################### - # Azimuth angle [degrees] - azimuth: - type: FIXED - fixed: -90 - ########################################################################### - # Elevation angle [degrees] - elevation: - type: FIXED - fixed: 45 - ########################################################################### - # Station 2d location: - location: - ########################################################################### - type: FIXED - fixed: - ########################################################################### - x: 0 - ########################################################################### - y: 0 - ########################################################################### - antenna: - pattern: OMNI - gain: 0 - - channel_model: P619 - param_p619: - space_station_alt_m: 20000.0 - # Enter the RAS antenna heigth above sea level - earth_station_alt_m: 1000 - # The RAS station lat - earth_station_lat_deg: -15.7801 - # This parameter is ignored as it will be calculated from x,y positions in run time - earth_station_long_diff_deg: 0.0 - - season: SUMMER diff --git a/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_45deg.yaml b/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_45deg.yaml deleted file mode 100644 index 2c71d5fed..000000000 --- a/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_45deg.yaml +++ /dev/null @@ -1,391 +0,0 @@ -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots: 1000 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link: DOWNLINK - ########################################################################### - # The chosen system for sharing study - # EESS_PASSIVE, FSS_SS, FSS_ES, FS, RAS, - system: SINGLE_EARTH_STATION - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel: FALSE - enable_adjacent_channel: TRUE - ########################################################################### - # Seed for random number generator - seed: 101 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output: FALSE - ########################################################################### - # output destination folder - this is relative SHARC/sharc directory - output_dir: campaigns/imt_mss_ras_2600_MHz/output/ - ########################################################################### - # output folder prefix - output_dir_prefix: output_imt_mss_ras_2600_MHz_45deg -imt: - topology: - ########################################################################### - # Network topology. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR" - type: NTN - ntn: - ########################################################################### - # NTN cell radius in network topology [m] - cell_radius: 90000 - ########################################################################### - # Number of clusters in macro cell topology - num_clusters: 1 - bs_elevation: 45 - ########################################################################### - # Minimum 2D separation distance from BS to UE [m] - minimum_separation_distance_bs_ue: 0.5 - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE : IMT suffers interference - # FALSE : IMT generates interference - interfered_with: FALSE - ########################################################################### - # IMT center frequency [MHz] - frequency: 2680.0 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth: 20 - ########################################################################### - # IMT resource block bandwidth [MHz] - rb_bandwidth: 0.180 - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model: SINGLE_ELEMENT - ########################################################################### - # IMT spectrum emission mask. Options are: - # "IMT-2020" : for mmWave as described in ITU-R TG 5/1 Contribution 36 - # "3GPP E-UTRA" : for E-UTRA bands > 1 GHz as described in - # TS 36.104 v11.2.0 (BS) and TS 36.101 v11.2.0 (UE) - spectral_mask: 3GPP E-UTRA - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions: -13 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio: 0.1 - bs: - ########################################################################### - # The load probability (or activity factor) models the statistical - # variation of the network load by defining the number of fully loaded - # base stations that are simultaneously transmitting - load_probability: 1 - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power: 37 - ########################################################################### - # Base station height [m] - height: 20000 - ########################################################################### - # Base station noise figure [dB] - noise_figure: 5 - ########################################################################### - # User equipment noise temperature [K] - noise_temperature: 290 - ########################################################################### - # Base station array ohmic loss [dB] - ohmic_loss: 2 - - antenna: - ########################################################################### - # If normalization of M2101 should be applied for BS - normalization: FALSE - ########################################################################### - # File to be used in the BS beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/bs_norm.npz - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor or NTN simulations - downtilt: 90 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g: 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 65 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 65 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 8 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 8 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 30 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - - uplink: - ########################################################################### - # Uplink attenuation factor used in link-to-system mapping - attenuation_factor: 0.4 - ########################################################################### - # Uplink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Uplink maximum SINR of the code set [dB] - sinr_max: 22 - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k: 3 - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m: 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent: 0 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type: ANGLE_AND_DISTANCE - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance: RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth: NORMAL - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control: ON - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch: -91 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha: 1 - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax: 23 - ########################################################################### - # UE power dynamic range [dB] - # The minimum transmit power of a UE is (p_cmax - dynamic_range) - power_dynamic_range: 63 - ########################################################################### - # UE height [m] - height: 1.5 - ########################################################################### - # User equipment noise figure [dB] - noise_figure: 9 - ########################################################################### - # User equipment feed loss [dB] - ohmic_loss: 3 - ########################################################################### - # User equipment body loss [dB] - body_loss: 4 - - antenna: - ########################################################################### - # If normalization of M2101 should be applied for UE - normalization: FALSE - ########################################################################### - # File to be used in the UE beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/ue_norm.npz - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g: 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 90 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 90 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 4 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 4 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 25 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - - downlink: - ########################################################################### - # Downlink attenuation factor used in link-to-system mapping - attenuation_factor: 0.8 - ########################################################################### - # Downlink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Downlink maximum SINR of the code set [dB] - sinr_max: 30 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "CI" (close-in FS reference distance) - # "UMa" (Urban Macro - 3GPP) - # "UMi" (Urban Micro - 3GPP) - # "TVRO-URBAN" - # "TVRO-SUBURBAN" - # "ABG" (Alpha-Beta-Gamma) - # "P619" - channel_model: P619 - ########################################################################### - # Parameters for the P.619 propagation model - # For IMT NTN the model is used for calculating the coupling loss between - # the BS space station and the UEs on Earth's surface. - # space_station_alt_m - altitude of IMT space station (BS) (in meters) - # earth_station_alt_m - altitude of the system's earth station (in meters) - # earth_station_lat_deg - latitude of the system's earth station (in degrees) - # earth_station_long_diff_deg - difference between longitudes of IMT space station and system's earth station - # (positive if space-station is to the East of earth-station) - # season - season of the year. - # Enter the IMT space station height - param_p619: - space_station_alt_m: 20000.0 - # Enter the UE antenna heigth above sea level - earth_station_alt_m: 1000 - # The RAS station lat - earth_station_lat_deg: -15.7801 - # This parameter is ignored as it will be calculated from x,y positions in run time - earth_station_long_diff_deg: 0.0 - - season: SUMMER -#### System Parameters -single_earth_station: - ########################################################################### - # NOTE: when using P.619 it is suggested that polarization loss = 3dB is normal - # also, - # NOTE: Verification needed: - # polarization mismatch between IMT BS and linear polarization = 3dB in earth P2P case? - # = 0 is safer choice - polarization_loss: 0.0 - ########################################################################### - frequency: 2695 - ########################################################################### - bandwidth: 10 - ########################################################################### - noise_temperature: 90 - ########################################################################### - # Peak transmit power spectral density (clear sky) [dBW/Hz] - tx_power_density: 0 - # Adjacent channel selectivity [dB] - adjacent_ch_selectivity: 20.0 - #### - geometry: - ########################################################################### - # Antenna height [meters] - height: 15 - ########################################################################### - # Azimuth angle [degrees] - azimuth: - type: FIXED - fixed: -90 - ########################################################################### - # Elevation angle [degrees] - elevation: - type: FIXED - fixed: 45 - ########################################################################### - # Station 2d location: - location: - ########################################################################### - type: FIXED - fixed: - ########################################################################### - x: 0 - ########################################################################### - y: 0 - ########################################################################### - antenna: - pattern: OMNI - gain: 0 - - channel_model: P619 - param_p619: - space_station_alt_m: 20000.0 - # Enter the RAS antenna heigth above sea level - earth_station_alt_m: 1000 - # The RAS station lat - earth_station_lat_deg: -15.7801 - # This parameter is ignored as it will be calculated from x,y positions in run time - earth_station_long_diff_deg: 0.0 - - season: SUMMER diff --git a/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_60deg.yaml b/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_60deg.yaml deleted file mode 100644 index 587c95b83..000000000 --- a/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_60deg.yaml +++ /dev/null @@ -1,391 +0,0 @@ -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots: 1000 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link: DOWNLINK - ########################################################################### - # The chosen system for sharing study - # EESS_PASSIVE, FSS_SS, FSS_ES, FS, RAS, - system: SINGLE_EARTH_STATION - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel: FALSE - enable_adjacent_channel: TRUE - ########################################################################### - # Seed for random number generator - seed: 101 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output: FALSE - ########################################################################### - # output destination folder - this is relative SHARC/sharc directory - output_dir: campaigns/imt_mss_ras_2600_MHz/output/ - ########################################################################### - # output folder prefix - output_dir_prefix: output_imt_mss_ras_2600_MHz_60deg -imt: - topology: - ########################################################################### - # Network topology. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR" - type: NTN - ntn: - ########################################################################### - # NTN cell radius in network topology [m] - cell_radius: 90000 - ########################################################################### - # Number of clusters in macro cell topology - num_clusters: 1 - bs_elevation: 60 - ########################################################################### - # Minimum 2D separation distance from BS to UE [m] - minimum_separation_distance_bs_ue: 0.5 - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE : IMT suffers interference - # FALSE : IMT generates interference - interfered_with: FALSE - ########################################################################### - # IMT center frequency [MHz] - frequency: 2680.0 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth: 20 - ########################################################################### - # IMT resource block bandwidth [MHz] - rb_bandwidth: 0.180 - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model: SINGLE_ELEMENT - ########################################################################### - # IMT spectrum emission mask. Options are: - # "IMT-2020" : for mmWave as described in ITU-R TG 5/1 Contribution 36 - # "3GPP E-UTRA" : for E-UTRA bands > 1 GHz as described in - # TS 36.104 v11.2.0 (BS) and TS 36.101 v11.2.0 (UE) - spectral_mask: 3GPP E-UTRA - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions: -13 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio: 0.1 - bs: - ########################################################################### - # The load probability (or activity factor) models the statistical - # variation of the network load by defining the number of fully loaded - # base stations that are simultaneously transmitting - load_probability: 1 - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power: 37 - ########################################################################### - # Base station height [m] - height: 20000 - ########################################################################### - # Base station noise figure [dB] - noise_figure: 5 - ########################################################################### - # User equipment noise temperature [K] - noise_temperature: 290 - ########################################################################### - # Base station array ohmic loss [dB] - ohmic_loss: 2 - - antenna: - ########################################################################### - # If normalization of M2101 should be applied for BS - normalization: FALSE - ########################################################################### - # File to be used in the BS beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/bs_norm.npz - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor or NTN simulations - downtilt: 90 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g: 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 65 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 65 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 8 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 8 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 30 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - - uplink: - ########################################################################### - # Uplink attenuation factor used in link-to-system mapping - attenuation_factor: 0.4 - ########################################################################### - # Uplink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Uplink maximum SINR of the code set [dB] - sinr_max: 22 - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k: 3 - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m: 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent: 0 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type: ANGLE_AND_DISTANCE - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance: RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth: NORMAL - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control: ON - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch: -91 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha: 1 - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax: 23 - ########################################################################### - # UE power dynamic range [dB] - # The minimum transmit power of a UE is (p_cmax - dynamic_range) - power_dynamic_range: 63 - ########################################################################### - # UE height [m] - height: 1.5 - ########################################################################### - # User equipment noise figure [dB] - noise_figure: 9 - ########################################################################### - # User equipment feed loss [dB] - ohmic_loss: 3 - ########################################################################### - # User equipment body loss [dB] - body_loss: 4 - - antenna: - ########################################################################### - # If normalization of M2101 should be applied for UE - normalization: FALSE - ########################################################################### - # File to be used in the UE beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/ue_norm.npz - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g: 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 90 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 90 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 4 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 4 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 25 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - - downlink: - ########################################################################### - # Downlink attenuation factor used in link-to-system mapping - attenuation_factor: 0.8 - ########################################################################### - # Downlink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Downlink maximum SINR of the code set [dB] - sinr_max: 30 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "CI" (close-in FS reference distance) - # "UMa" (Urban Macro - 3GPP) - # "UMi" (Urban Micro - 3GPP) - # "TVRO-URBAN" - # "TVRO-SUBURBAN" - # "ABG" (Alpha-Beta-Gamma) - # "P619" - channel_model: P619 - ########################################################################### - # Parameters for the P.619 propagation model - # For IMT NTN the model is used for calculating the coupling loss between - # the BS space station and the UEs on Earth's surface. - # space_station_alt_m - altitude of IMT space station (BS) (in meters) - # earth_station_alt_m - altitude of the system's earth station (in meters) - # earth_station_lat_deg - latitude of the system's earth station (in degrees) - # earth_station_long_diff_deg - difference between longitudes of IMT space station and system's earth station - # (positive if space-station is to the East of earth-station) - # season - season of the year. - # Enter the IMT space station height - param_p619: - space_station_alt_m: 20000.0 - # Enter the UE antenna heigth above sea level - earth_station_alt_m: 1000 - # The RAS station lat - earth_station_lat_deg: -15.7801 - # This parameter is ignored as it will be calculated from x,y positions in run time - earth_station_long_diff_deg: 0.0 - - season: SUMMER -#### System Parameters -single_earth_station: - ########################################################################### - # NOTE: when using P.619 it is suggested that polarization loss = 3dB is normal - # also, - # NOTE: Verification needed: - # polarization mismatch between IMT BS and linear polarization = 3dB in earth P2P case? - # = 0 is safer choice - polarization_loss: 0.0 - ########################################################################### - frequency: 2695 - ########################################################################### - bandwidth: 10 - ########################################################################### - noise_temperature: 90 - ########################################################################### - # Peak transmit power spectral density (clear sky) [dBW/Hz] - tx_power_density: 0 - # Adjacent channel selectivity [dB] - adjacent_ch_selectivity: 20.0 - #### - geometry: - ########################################################################### - # Antenna height [meters] - height: 15 - ########################################################################### - # Azimuth angle [degrees] - azimuth: - type: FIXED - fixed: -90 - ########################################################################### - # Elevation angle [degrees] - elevation: - type: FIXED - fixed: 45 - ########################################################################### - # Station 2d location: - location: - ########################################################################### - type: FIXED - fixed: - ########################################################################### - x: 0 - ########################################################################### - y: 0 - ########################################################################### - antenna: - pattern: OMNI - gain: 0 - - channel_model: P619 - param_p619: - space_station_alt_m: 20000.0 - # Enter the RAS antenna heigth above sea level - earth_station_alt_m: 1000 - # The RAS station lat - earth_station_lat_deg: -15.7801 - # This parameter is ignored as it will be calculated from x,y positions in run time - earth_station_long_diff_deg: 0.0 - - season: SUMMER diff --git a/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_90deg.yaml b/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_90deg.yaml deleted file mode 100644 index 3c7928171..000000000 --- a/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_90deg.yaml +++ /dev/null @@ -1,391 +0,0 @@ -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots: 1000 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link: DOWNLINK - ########################################################################### - # The chosen system for sharing study - # EESS_PASSIVE, FSS_SS, FSS_ES, FS, RAS, - system: SINGLE_EARTH_STATION - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel: FALSE - enable_adjacent_channel: TRUE - ########################################################################### - # Seed for random number generator - seed: 101 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output: FALSE - ########################################################################### - # output destination folder - this is relative SHARC/sharc directory - output_dir: campaigns/imt_mss_ras_2600_MHz/output/ - ########################################################################### - # output folder prefix - output_dir_prefix: output_imt_mss_ras_2600_MHz_90deg -imt: - topology: - ########################################################################### - # Network topology. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR" - type: NTN - ntn: - ########################################################################### - # NTN cell radius in network topology [m] - cell_radius: 90000 - ########################################################################### - # Number of clusters in macro cell topology - num_clusters: 1 - bs_elevation: 90 - ########################################################################### - # Minimum 2D separation distance from BS to UE [m] - minimum_separation_distance_bs_ue: 0.5 - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE : IMT suffers interference - # FALSE : IMT generates interference - interfered_with: FALSE - ########################################################################### - # IMT center frequency [MHz] - frequency: 2680.0 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth: 20 - ########################################################################### - # IMT resource block bandwidth [MHz] - rb_bandwidth: 0.180 - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model: SINGLE_ELEMENT - ########################################################################### - # IMT spectrum emission mask. Options are: - # "IMT-2020" : for mmWave as described in ITU-R TG 5/1 Contribution 36 - # "3GPP E-UTRA" : for E-UTRA bands > 1 GHz as described in - # TS 36.104 v11.2.0 (BS) and TS 36.101 v11.2.0 (UE) - spectral_mask: 3GPP E-UTRA - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions: -13 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio: 0.1 - bs: - ########################################################################### - # The load probability (or activity factor) models the statistical - # variation of the network load by defining the number of fully loaded - # base stations that are simultaneously transmitting - load_probability: 1 - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power: 37 - ########################################################################### - # Base station height [m] - height: 20000 - ########################################################################### - # Base station noise figure [dB] - noise_figure: 5 - ########################################################################### - # User equipment noise temperature [K] - noise_temperature: 290 - ########################################################################### - # Base station array ohmic loss [dB] - ohmic_loss: 2 - - antenna: - ########################################################################### - # If normalization of M2101 should be applied for BS - normalization: FALSE - ########################################################################### - # File to be used in the BS beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/bs_norm.npz - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor or NTN simulations - downtilt: 90 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g: 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 65 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 65 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 8 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 8 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 30 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - - uplink: - ########################################################################### - # Uplink attenuation factor used in link-to-system mapping - attenuation_factor: 0.4 - ########################################################################### - # Uplink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Uplink maximum SINR of the code set [dB] - sinr_max: 22 - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k: 3 - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m: 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent: 0 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type: ANGLE_AND_DISTANCE - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance: RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth: NORMAL - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control: ON - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch: -91 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha: 1 - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax: 23 - ########################################################################### - # UE power dynamic range [dB] - # The minimum transmit power of a UE is (p_cmax - dynamic_range) - power_dynamic_range: 63 - ########################################################################### - # UE height [m] - height: 1.5 - ########################################################################### - # User equipment noise figure [dB] - noise_figure: 9 - ########################################################################### - # User equipment feed loss [dB] - ohmic_loss: 3 - ########################################################################### - # User equipment body loss [dB] - body_loss: 4 - - antenna: - ########################################################################### - # If normalization of M2101 should be applied for UE - normalization: FALSE - ########################################################################### - # File to be used in the UE beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/ue_norm.npz - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g: 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 90 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 90 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 4 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 4 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 25 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - - downlink: - ########################################################################### - # Downlink attenuation factor used in link-to-system mapping - attenuation_factor: 0.8 - ########################################################################### - # Downlink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Downlink maximum SINR of the code set [dB] - sinr_max: 30 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "CI" (close-in FS reference distance) - # "UMa" (Urban Macro - 3GPP) - # "UMi" (Urban Micro - 3GPP) - # "TVRO-URBAN" - # "TVRO-SUBURBAN" - # "ABG" (Alpha-Beta-Gamma) - # "P619" - channel_model: P619 - ########################################################################### - # Parameters for the P.619 propagation model - # For IMT NTN the model is used for calculating the coupling loss between - # the BS space station and the UEs on Earth's surface. - # space_station_alt_m - altitude of IMT space station (BS) (in meters) - # earth_station_alt_m - altitude of the system's earth station (in meters) - # earth_station_lat_deg - latitude of the system's earth station (in degrees) - # earth_station_long_diff_deg - difference between longitudes of IMT space station and system's earth station - # (positive if space-station is to the East of earth-station) - # season - season of the year. - # Enter the IMT space station height - param_p619: - space_station_alt_m: 20000.0 - # Enter the UE antenna heigth above sea level - earth_station_alt_m: 1000 - # The RAS station lat - earth_station_lat_deg: -15.7801 - # This parameter is ignored as it will be calculated from x,y positions in run time - earth_station_long_diff_deg: 0.0 - - season: SUMMER -#### System Parameters -single_earth_station: - ########################################################################### - # NOTE: when using P.619 it is suggested that polarization loss = 3dB is normal - # also, - # NOTE: Verification needed: - # polarization mismatch between IMT BS and linear polarization = 3dB in earth P2P case? - # = 0 is safer choice - polarization_loss: 0.0 - ########################################################################### - frequency: 2695 - ########################################################################### - bandwidth: 10 - ########################################################################### - noise_temperature: 90 - ########################################################################### - # Peak transmit power spectral density (clear sky) [dBW/Hz] - tx_power_density: 0 - # Adjacent channel selectivity [dB] - adjacent_ch_selectivity: 20.0 - #### - geometry: - ########################################################################### - # Antenna height [meters] - height: 15 - ########################################################################### - # Azimuth angle [degrees] - azimuth: - type: FIXED - fixed: -90 - ########################################################################### - # Elevation angle [degrees] - elevation: - type: FIXED - fixed: 45 - ########################################################################### - # Station 2d location: - location: - ########################################################################### - type: FIXED - fixed: - ########################################################################### - x: 0 - ########################################################################### - y: 0 - ########################################################################### - antenna: - pattern: OMNI - gain: 0 - - channel_model: P619 - param_p619: - space_station_alt_m: 20000.0 - # Enter the RAS antenna heigth above sea level - earth_station_alt_m: 1000 - # The RAS station lat - earth_station_lat_deg: -15.7801 - # This parameter is ignored as it will be calculated from x,y positions in run time - earth_station_long_diff_deg: 0.0 - - season: SUMMER diff --git a/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_fspl_30deg.yaml b/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_fspl_30deg.yaml deleted file mode 100644 index 5bf3b0451..000000000 --- a/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_fspl_30deg.yaml +++ /dev/null @@ -1,377 +0,0 @@ -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots: 1000 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link: DOWNLINK - ########################################################################### - # The chosen system for sharing study - # EESS_PASSIVE, FSS_SS, FSS_ES, FS, RAS, - system: SINGLE_EARTH_STATION - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel: FALSE - enable_adjacent_channel: TRUE - ########################################################################### - # Seed for random number generator - seed: 101 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output: FALSE - ########################################################################### - # output destination folder - this is relative SHARC/sharc directory - output_dir: campaigns/imt_mss_ras_2600_MHz/output/ - ########################################################################### - # output folder prefix - output_dir_prefix: output_imt_mss_ras_2600_MHz_fspl_30deg -imt: - topology: - ########################################################################### - # Network topology. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR" - type: NTN - ntn: - ########################################################################### - # NTN cell radius in network topology [m] - cell_radius: 90000 - ########################################################################### - # Number of clusters in macro cell topology - num_clusters: 1 - bs_elevation: 30 - ########################################################################### - # Minimum 2D separation distance from BS to UE [m] - minimum_separation_distance_bs_ue: 0.5 - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE : IMT suffers interference - # FALSE : IMT generates interference - interfered_with: FALSE - ########################################################################### - # IMT center frequency [MHz] - frequency: 2680.0 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth: 20 - ########################################################################### - # IMT resource block bandwidth [MHz] - rb_bandwidth: 0.180 - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model: SINGLE_ELEMENT - ########################################################################### - # IMT spectrum emission mask. Options are: - # "IMT-2020" : for mmWave as described in ITU-R TG 5/1 Contribution 36 - # "3GPP E-UTRA" : for E-UTRA bands > 1 GHz as described in - # TS 36.104 v11.2.0 (BS) and TS 36.101 v11.2.0 (UE) - spectral_mask: 3GPP E-UTRA - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions: -13 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio: 0.1 - bs: - ########################################################################### - # The load probability (or activity factor) models the statistical - # variation of the network load by defining the number of fully loaded - # base stations that are simultaneously transmitting - load_probability: 1 - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power: 37 - ########################################################################### - # Base station height [m] - height: 20000 - ########################################################################### - # Base station noise figure [dB] - noise_figure: 5 - ########################################################################### - # User equipment noise temperature [K] - noise_temperature: 290 - ########################################################################### - # Base station array ohmic loss [dB] - ohmic_loss: 2 - - antenna: - ########################################################################### - # If normalization of M2101 should be applied for BS - normalization: FALSE - ########################################################################### - # File to be used in the BS beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/bs_norm.npz - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor or NTN simulations - downtilt: 90 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g: 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 65 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 65 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 8 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 8 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 30 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - - uplink: - ########################################################################### - # Uplink attenuation factor used in link-to-system mapping - attenuation_factor: 0.4 - ########################################################################### - # Uplink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Uplink maximum SINR of the code set [dB] - sinr_max: 22 - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k: 3 - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m: 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent: 0 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type: ANGLE_AND_DISTANCE - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance: RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth: NORMAL - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control: ON - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch: -91 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha: 1 - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax: 23 - ########################################################################### - # UE power dynamic range [dB] - # The minimum transmit power of a UE is (p_cmax - dynamic_range) - power_dynamic_range: 63 - ########################################################################### - # UE height [m] - height: 1.5 - ########################################################################### - # User equipment noise figure [dB] - noise_figure: 9 - ########################################################################### - # User equipment feed loss [dB] - ohmic_loss: 3 - ########################################################################### - # User equipment body loss [dB] - body_loss: 4 - - antenna: - ########################################################################### - # If normalization of M2101 should be applied for UE - normalization: FALSE - ########################################################################### - # File to be used in the UE beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/ue_norm.npz - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g: 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 90 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 90 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 4 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 4 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 25 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - - downlink: - ########################################################################### - # Downlink attenuation factor used in link-to-system mapping - attenuation_factor: 0.8 - ########################################################################### - # Downlink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Downlink maximum SINR of the code set [dB] - sinr_max: 30 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "CI" (close-in FS reference distance) - # "UMa" (Urban Macro - 3GPP) - # "UMi" (Urban Micro - 3GPP) - # "TVRO-URBAN" - # "TVRO-SUBURBAN" - # "ABG" (Alpha-Beta-Gamma) - # "P619" - channel_model: P619 - - param_p619: - ########################################################################### - # altitude of the space station [m] - space_station_alt_m: 20000.0 - ########################################################################### - # altitude of ES system [m] - earth_station_alt_m: 1000 - ########################################################################### - # latitude of ES system [m] - earth_station_lat_deg: -15.7801 - ########################################################################### - # difference between longitudes of IMT-NTN station and FSS-ES system [deg] - # (positive if space-station is to the East of earth-station) - earth_station_long_diff_deg: 0.0 - - season: SUMMER -#### System Parameters -single_earth_station: - ########################################################################### - # NOTE: when using P.619 it is suggested that polarization loss = 3dB is normal - # also, - # NOTE: Verification needed: - # polarization mismatch between IMT BS and linear polarization = 3dB in earth P2P case? - # = 0 is safer choice - polarization_loss: 0.0 - ########################################################################### - frequency: 2695 - ########################################################################### - bandwidth: 10 - ########################################################################### - noise_temperature: 90 - ########################################################################### - # Peak transmit power spectral density (clear sky) [dBW/Hz] - tx_power_density: 0 - # Adjacent channel selectivity [dB] - adjacent_ch_selectivity: 20.0 - #### - geometry: - ########################################################################### - # Antenna height [meters] - height: 15 - ########################################################################### - # Azimuth angle [degrees] - azimuth: - type: FIXED - fixed: -90 - ########################################################################### - # Elevation angle [degrees] - elevation: - type: FIXED - fixed: 45 - ########################################################################### - # Station 2d location: - location: - ########################################################################### - type: FIXED - fixed: - ########################################################################### - x: 0 - ########################################################################### - y: 0 - ########################################################################### - antenna: - pattern: OMNI - gain: 0 - - channel_model: FSPL diff --git a/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_fspl_45deg.yaml b/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_fspl_45deg.yaml deleted file mode 100644 index afa02e826..000000000 --- a/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_fspl_45deg.yaml +++ /dev/null @@ -1,377 +0,0 @@ -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots: 1000 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link: DOWNLINK - ########################################################################### - # The chosen system for sharing study - # EESS_PASSIVE, FSS_SS, FSS_ES, FS, RAS, - system: SINGLE_EARTH_STATION - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel: FALSE - enable_adjacent_channel: TRUE - ########################################################################### - # Seed for random number generator - seed: 101 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output: FALSE - ########################################################################### - # output destination folder - this is relative SHARC/sharc directory - output_dir: campaigns/imt_mss_ras_2600_MHz/output/ - ########################################################################### - # output folder prefix - output_dir_prefix: output_imt_mss_ras_2600_MHz_fspl_45deg -imt: - topology: - ########################################################################### - # Network topology. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR" - type: NTN - ntn: - ########################################################################### - # NTN cell radius in network topology [m] - cell_radius: 90000 - ########################################################################### - # Number of clusters in macro cell topology - num_clusters: 1 - bs_elevation: 45 - ########################################################################### - # Minimum 2D separation distance from BS to UE [m] - minimum_separation_distance_bs_ue: 0.5 - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE : IMT suffers interference - # FALSE : IMT generates interference - interfered_with: FALSE - ########################################################################### - # IMT center frequency [MHz] - frequency: 2680.0 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth: 20 - ########################################################################### - # IMT resource block bandwidth [MHz] - rb_bandwidth: 0.180 - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model: SINGLE_ELEMENT - ########################################################################### - # IMT spectrum emission mask. Options are: - # "IMT-2020" : for mmWave as described in ITU-R TG 5/1 Contribution 36 - # "3GPP E-UTRA" : for E-UTRA bands > 1 GHz as described in - # TS 36.104 v11.2.0 (BS) and TS 36.101 v11.2.0 (UE) - spectral_mask: 3GPP E-UTRA - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions: -13 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio: 0.1 - bs: - ########################################################################### - # The load probability (or activity factor) models the statistical - # variation of the network load by defining the number of fully loaded - # base stations that are simultaneously transmitting - load_probability: 1 - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power: 37 - ########################################################################### - # Base station height [m] - height: 20000 - ########################################################################### - # Base station noise figure [dB] - noise_figure: 5 - ########################################################################### - # User equipment noise temperature [K] - noise_temperature: 290 - ########################################################################### - # Base station array ohmic loss [dB] - ohmic_loss: 2 - - antenna: - ########################################################################### - # If normalization of M2101 should be applied for BS - normalization: FALSE - ########################################################################### - # File to be used in the BS beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/bs_norm.npz - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor or NTN simulations - downtilt: 90 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g: 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 65 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 65 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 8 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 8 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 30 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - - uplink: - ########################################################################### - # Uplink attenuation factor used in link-to-system mapping - attenuation_factor: 0.4 - ########################################################################### - # Uplink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Uplink maximum SINR of the code set [dB] - sinr_max: 22 - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k: 3 - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m: 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent: 0 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type: ANGLE_AND_DISTANCE - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance: RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth: NORMAL - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control: ON - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch: -91 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha: 1 - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax: 23 - ########################################################################### - # UE power dynamic range [dB] - # The minimum transmit power of a UE is (p_cmax - dynamic_range) - power_dynamic_range: 63 - ########################################################################### - # UE height [m] - height: 1.5 - ########################################################################### - # User equipment noise figure [dB] - noise_figure: 9 - ########################################################################### - # User equipment feed loss [dB] - ohmic_loss: 3 - ########################################################################### - # User equipment body loss [dB] - body_loss: 4 - - antenna: - ########################################################################### - # If normalization of M2101 should be applied for UE - normalization: FALSE - ########################################################################### - # File to be used in the UE beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/ue_norm.npz - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g: 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 90 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 90 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 4 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 4 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 25 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - - downlink: - ########################################################################### - # Downlink attenuation factor used in link-to-system mapping - attenuation_factor: 0.8 - ########################################################################### - # Downlink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Downlink maximum SINR of the code set [dB] - sinr_max: 30 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "CI" (close-in FS reference distance) - # "UMa" (Urban Macro - 3GPP) - # "UMi" (Urban Micro - 3GPP) - # "TVRO-URBAN" - # "TVRO-SUBURBAN" - # "ABG" (Alpha-Beta-Gamma) - # "P619" - channel_model: P619 - - param_p619: - ########################################################################### - # altitude of the space station [m] - space_station_alt_m: 20000.0 - ########################################################################### - # altitude of ES system [m] - earth_station_alt_m: 1000 - ########################################################################### - # latitude of ES system [m] - earth_station_lat_deg: -15.7801 - ########################################################################### - # difference between longitudes of IMT-NTN station and FSS-ES system [deg] - # (positive if space-station is to the East of earth-station) - earth_station_long_diff_deg: 0.0 - - season: SUMMER -#### System Parameters -single_earth_station: - ########################################################################### - # NOTE: when using P.619 it is suggested that polarization loss = 3dB is normal - # also, - # NOTE: Verification needed: - # polarization mismatch between IMT BS and linear polarization = 3dB in earth P2P case? - # = 0 is safer choice - polarization_loss: 0.0 - ########################################################################### - frequency: 2695 - ########################################################################### - bandwidth: 10 - ########################################################################### - noise_temperature: 90 - ########################################################################### - # Peak transmit power spectral density (clear sky) [dBW/Hz] - tx_power_density: 0 - # Adjacent channel selectivity [dB] - adjacent_ch_selectivity: 20.0 - #### - geometry: - ########################################################################### - # Antenna height [meters] - height: 15 - ########################################################################### - # Azimuth angle [degrees] - azimuth: - type: FIXED - fixed: -90 - ########################################################################### - # Elevation angle [degrees] - elevation: - type: FIXED - fixed: 45 - ########################################################################### - # Station 2d location: - location: - ########################################################################### - type: FIXED - fixed: - ########################################################################### - x: 0 - ########################################################################### - y: 0 - ########################################################################### - antenna: - pattern: OMNI - gain: 0 - - channel_model: FSPL diff --git a/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_fspl_60deg.yaml b/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_fspl_60deg.yaml deleted file mode 100644 index 231043535..000000000 --- a/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_fspl_60deg.yaml +++ /dev/null @@ -1,377 +0,0 @@ -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots: 1000 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link: DOWNLINK - ########################################################################### - # The chosen system for sharing study - # EESS_PASSIVE, FSS_SS, FSS_ES, FS, RAS, - system: SINGLE_EARTH_STATION - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel: FALSE - enable_adjacent_channel: TRUE - ########################################################################### - # Seed for random number generator - seed: 101 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output: FALSE - ########################################################################### - # output destination folder - this is relative SHARC/sharc directory - output_dir: campaigns/imt_mss_ras_2600_MHz/output/ - ########################################################################### - # output folder prefix - output_dir_prefix: output_imt_mss_ras_2600_MHz_fspl_60deg -imt: - topology: - ########################################################################### - # Network topology. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR" - type: NTN - ntn: - ########################################################################### - # NTN cell radius in network topology [m] - cell_radius: 90000 - ########################################################################### - # Number of clusters in macro cell topology - num_clusters: 1 - bs_elevation: 60 - ########################################################################### - # Minimum 2D separation distance from BS to UE [m] - minimum_separation_distance_bs_ue: 0.5 - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE : IMT suffers interference - # FALSE : IMT generates interference - interfered_with: FALSE - ########################################################################### - # IMT center frequency [MHz] - frequency: 2680.0 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth: 20 - ########################################################################### - # IMT resource block bandwidth [MHz] - rb_bandwidth: 0.180 - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model: SINGLE_ELEMENT - ########################################################################### - # IMT spectrum emission mask. Options are: - # "IMT-2020" : for mmWave as described in ITU-R TG 5/1 Contribution 36 - # "3GPP E-UTRA" : for E-UTRA bands > 1 GHz as described in - # TS 36.104 v11.2.0 (BS) and TS 36.101 v11.2.0 (UE) - spectral_mask: 3GPP E-UTRA - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions: -13 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio: 0.1 - bs: - ########################################################################### - # The load probability (or activity factor) models the statistical - # variation of the network load by defining the number of fully loaded - # base stations that are simultaneously transmitting - load_probability: 1 - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power: 37 - ########################################################################### - # Base station height [m] - height: &imt_bs_height 20000 - ########################################################################### - # Base station noise figure [dB] - noise_figure: 5 - ########################################################################### - # User equipment noise temperature [K] - noise_temperature: 290 - ########################################################################### - # Base station array ohmic loss [dB] - ohmic_loss: 2 - - antenna: - ########################################################################### - # If normalization of M2101 should be applied for BS - normalization: FALSE - ########################################################################### - # File to be used in the BS beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/bs_norm.npz - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor or NTN simulations - downtilt: 90 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g: 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 65 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 65 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 8 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 8 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 30 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - - uplink: - ########################################################################### - # Uplink attenuation factor used in link-to-system mapping - attenuation_factor: 0.4 - ########################################################################### - # Uplink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Uplink maximum SINR of the code set [dB] - sinr_max: 22 - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k: 3 - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m: 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent: 0 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type: ANGLE_AND_DISTANCE - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance: RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth: NORMAL - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control: ON - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch: -91 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha: 1 - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax: 23 - ########################################################################### - # UE power dynamic range [dB] - # The minimum transmit power of a UE is (p_cmax - dynamic_range) - power_dynamic_range: 63 - ########################################################################### - # UE height [m] - height: 1.5 - ########################################################################### - # User equipment noise figure [dB] - noise_figure: 9 - ########################################################################### - # User equipment feed loss [dB] - ohmic_loss: 3 - ########################################################################### - # User equipment body loss [dB] - body_loss: 4 - - antenna: - ########################################################################### - # If normalization of M2101 should be applied for UE - normalization: FALSE - ########################################################################### - # File to be used in the UE beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/ue_norm.npz - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g: 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 90 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 90 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 4 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 4 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 25 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - - downlink: - ########################################################################### - # Downlink attenuation factor used in link-to-system mapping - attenuation_factor: 0.8 - ########################################################################### - # Downlink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Downlink maximum SINR of the code set [dB] - sinr_max: 30 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "CI" (close-in FS reference distance) - # "UMa" (Urban Macro - 3GPP) - # "UMi" (Urban Micro - 3GPP) - # "TVRO-URBAN" - # "TVRO-SUBURBAN" - # "ABG" (Alpha-Beta-Gamma) - # "P619" - channel_model: P619 - - param_p619: - ########################################################################### - # altitude of the space station [m] - space_station_alt_m: 20000.0 - ########################################################################### - # altitude of ES system [m] - earth_station_alt_m: 1000 - ########################################################################### - # latitude of ES system [m] - earth_station_lat_deg: -15.7801 - ########################################################################### - # difference between longitudes of IMT-NTN station and FSS-ES system [deg] - # (positive if space-station is to the East of earth-station) - earth_station_long_diff_deg: 0.0 - - season: SUMMER -#### System Parameters -single_earth_station: - ########################################################################### - # NOTE: when using P.619 it is suggested that polarization loss = 3dB is normal - # also, - # NOTE: Verification needed: - # polarization mismatch between IMT BS and linear polarization = 3dB in earth P2P case? - # = 0 is safer choice - polarization_loss: 0.0 - ########################################################################### - frequency: 2695 - ########################################################################### - bandwidth: 10 - ########################################################################### - noise_temperature: 90 - ########################################################################### - # Peak transmit power spectral density (clear sky) [dBW/Hz] - tx_power_density: 0 - # Adjacent channel selectivity [dB] - adjacent_ch_selectivity: 20.0 - #### - geometry: - ########################################################################### - # Antenna height [meters] - height: 15 - ########################################################################### - # Azimuth angle [degrees] - azimuth: - type: FIXED - fixed: -90 - ########################################################################### - # Elevation angle [degrees] - elevation: - type: FIXED - fixed: 45 - ########################################################################### - # Station 2d location: - location: - ########################################################################### - type: FIXED - fixed: - ########################################################################### - x: 0 - ########################################################################### - y: 0 - ########################################################################### - antenna: - pattern: OMNI - gain: 0 - - channel_model: FSPL diff --git a/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_fspl_90deg.yaml b/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_fspl_90deg.yaml deleted file mode 100644 index f667ca5dc..000000000 --- a/sharc/campaigns/imt_mss_ras_2600_MHz/input/parameters_hibs_ras_2600_MHz_fspl_90deg.yaml +++ /dev/null @@ -1,377 +0,0 @@ -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots: 1000 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link: DOWNLINK - ########################################################################### - # The chosen system for sharing study - # EESS_PASSIVE, FSS_SS, FSS_ES, FS, RAS, - system: SINGLE_EARTH_STATION - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel: FALSE - enable_adjacent_channel: TRUE - ########################################################################### - # Seed for random number generator - seed: 101 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output: FALSE - ########################################################################### - # output destination folder - this is relative SHARC/sharc directory - output_dir: campaigns/imt_mss_ras_2600_MHz/output/ - ########################################################################### - # output folder prefix - output_dir_prefix: output_imt_mss_ras_2600_MHz_fspl_90deg -imt: - topology: - ########################################################################### - # Network topology. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR" - type: NTN - ntn: - ########################################################################### - # NTN cell radius in network topology [m] - cell_radius: 90000 - ########################################################################### - # Number of clusters in macro cell topology - num_clusters: 1 - bs_elevation: 90 - ########################################################################### - # Minimum 2D separation distance from BS to UE [m] - minimum_separation_distance_bs_ue: 0.5 - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE : IMT suffers interference - # FALSE : IMT generates interference - interfered_with: FALSE - ########################################################################### - # IMT center frequency [MHz] - frequency: 2680.0 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth: 20 - ########################################################################### - # IMT resource block bandwidth [MHz] - rb_bandwidth: 0.180 - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model: SINGLE_ELEMENT - ########################################################################### - # IMT spectrum emission mask. Options are: - # "IMT-2020" : for mmWave as described in ITU-R TG 5/1 Contribution 36 - # "3GPP E-UTRA" : for E-UTRA bands > 1 GHz as described in - # TS 36.104 v11.2.0 (BS) and TS 36.101 v11.2.0 (UE) - spectral_mask: 3GPP E-UTRA - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions: -13 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio: 0.1 - bs: - ########################################################################### - # The load probability (or activity factor) models the statistical - # variation of the network load by defining the number of fully loaded - # base stations that are simultaneously transmitting - load_probability: 1 - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power: 37 - ########################################################################### - # Base station height [m] - height: 20000 - ########################################################################### - # Base station noise figure [dB] - noise_figure: 5 - ########################################################################### - # User equipment noise temperature [K] - noise_temperature: 290 - ########################################################################### - # Base station array ohmic loss [dB] - ohmic_loss: 2 - - antenna: - ########################################################################### - # If normalization of M2101 should be applied for BS - normalization: FALSE - ########################################################################### - # File to be used in the BS beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/bs_norm.npz - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor or NTN simulations - downtilt: 90 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g: 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 65 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 65 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 8 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 8 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 30 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - - uplink: - ########################################################################### - # Uplink attenuation factor used in link-to-system mapping - attenuation_factor: 0.4 - ########################################################################### - # Uplink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Uplink maximum SINR of the code set [dB] - sinr_max: 22 - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k: 3 - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m: 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent: 0 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type: ANGLE_AND_DISTANCE - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance: RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth: NORMAL - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control: ON - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch: -91 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha: 1 - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax: 23 - ########################################################################### - # UE power dynamic range [dB] - # The minimum transmit power of a UE is (p_cmax - dynamic_range) - power_dynamic_range: 63 - ########################################################################### - # UE height [m] - height: 1.5 - ########################################################################### - # User equipment noise figure [dB] - noise_figure: 9 - ########################################################################### - # User equipment feed loss [dB] - ohmic_loss: 3 - ########################################################################### - # User equipment body loss [dB] - body_loss: 4 - - antenna: - ########################################################################### - # If normalization of M2101 should be applied for UE - normalization: FALSE - ########################################################################### - # File to be used in the UE beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/ue_norm.npz - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g: 5 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 90 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 90 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 4 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 4 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 25 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - - downlink: - ########################################################################### - # Downlink attenuation factor used in link-to-system mapping - attenuation_factor: 0.8 - ########################################################################### - # Downlink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Downlink maximum SINR of the code set [dB] - sinr_max: 30 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "CI" (close-in FS reference distance) - # "UMa" (Urban Macro - 3GPP) - # "UMi" (Urban Micro - 3GPP) - # "TVRO-URBAN" - # "TVRO-SUBURBAN" - # "ABG" (Alpha-Beta-Gamma) - # "P619" - channel_model: P619 - - param_p619: - ########################################################################### - # altitude of the space station [m] - space_station_alt_m: 20000.0 - ########################################################################### - # altitude of ES system [m] - earth_station_alt_m: 1000 - ########################################################################### - # latitude of ES system [m] - earth_station_lat_deg: -15.7801 - ########################################################################### - # difference between longitudes of IMT-NTN station and FSS-ES system [deg] - # (positive if space-station is to the East of earth-station) - earth_station_long_diff_deg: 0.0 - - season: SUMMER -#### System Parameters -single_earth_station: - ########################################################################### - # NOTE: when using P.619 it is suggested that polarization loss = 3dB is normal - # also, - # NOTE: Verification needed: - # polarization mismatch between IMT BS and linear polarization = 3dB in earth P2P case? - # = 0 is safer choice - polarization_loss: 0.0 - ########################################################################### - # frequency [MHz] - frequency: 2695 - ########################################################################### - # bandwidth [MHz] - bandwidth: 10 - ########################################################################### - # Station noise temperature [K] - noise_temperature: 90 - ########################################################################### - # Peak transmit power spectral density (clear sky) [dBW/Hz] - tx_power_density: 0 - # Adjacent channel selectivity [dB] - adjacent_ch_selectivity: 20.0 - geometry: - ########################################################################### - # Antenna height [meters] - height: 15 - ########################################################################### - # Azimuth angle [degrees] - azimuth: - type: FIXED - fixed: -90 - ########################################################################### - # Elevation angle [degrees] - elevation: - type: FIXED - fixed: 45 - ########################################################################### - # Station 2d location [meters]: - location: - ########################################################################### - type: FIXED - fixed: - ########################################################################### - x: 0 - ########################################################################### - y: 0 - antenna: - pattern: OMNI - gain: 0 - channel_model: FSPL diff --git a/sharc/campaigns/imt_mss_ras_2600_MHz/scripts/plot_results.py b/sharc/campaigns/imt_mss_ras_2600_MHz/scripts/plot_results.py deleted file mode 100644 index 3f8549b01..000000000 --- a/sharc/campaigns/imt_mss_ras_2600_MHz/scripts/plot_results.py +++ /dev/null @@ -1,105 +0,0 @@ -import os -from pathlib import Path -from sharc.results import Results -# import plotly.graph_objects as go -from sharc.post_processor import PostProcessor - -post_processor = PostProcessor() - -# Add a legend to results in folder that match the pattern -# This could easily come from a config file -post_processor\ - .add_plot_legend_pattern( - dir_name_contains="MHz_30deg", - legend="30 deg (P619)" - ).add_plot_legend_pattern( - dir_name_contains="MHz_45deg", - legend="45 deg (P619)" - ).add_plot_legend_pattern( - dir_name_contains="MHz_60deg", - legend="60 deg (P619)" - ).add_plot_legend_pattern( - dir_name_contains="MHz_90deg", - legend="90 deg (P619)" - ).add_plot_legend_pattern( - dir_name_contains="fspl_30deg", - legend="30 deg (FSPL)" - ).add_plot_legend_pattern( - dir_name_contains="fspl_45deg", - legend="45 deg (FSPL)" - ).add_plot_legend_pattern( - dir_name_contains="fspl_60deg", - legend="60 deg (FSPL)" - ).add_plot_legend_pattern( - dir_name_contains="fspl_90deg", - legend="90 deg (FSPL)" - ) - -campaign_base_dir = str((Path(__file__) / ".." / "..").resolve()) - -many_results = Results.load_many_from_dir( - os.path.join( - campaign_base_dir, - "output"), - only_latest=True) -# ^: typing.List[Results] - -post_processor.add_results(many_results) - -plots = post_processor.generate_cdf_plots_from_results( - many_results -) - -post_processor.add_plots(plots) - -# # This function aggregates IMT downlink and uplink -# aggregated_results = PostProcessor.aggregate_results( -# downlink_result=post_processor.get_results_by_output_dir("MHz_60deg_dl"), -# uplink_result=post_processor.get_results_by_output_dir("MHz_60deg_ul"), -# ul_tdd_factor=(3, 4), -# n_bs_sim=7 * 19 * 3 * 3, -# n_bs_actual=int -# ) - -# Add a protection criteria line: -# protection_criteria = 160 - -# post_processor\ -# .get_plot_by_results_attribute_name("system_dl_interf_power")\ -# .add_vline(protection_criteria, line_dash="dash") - -# Show a single plot: -# post_processor\ -# .get_plot_by_results_attribute_name("system_dl_interf_power")\ -# .show() - -# Plot every plot: -for plot in plots: - plot.show() - -for result in many_results: - # This generates the mean, median, variance, etc - stats = PostProcessor.generate_statistics( - result=result - ).write_to_results_dir() - -# # example on how to aggregate results and add it to plot: -# dl_res = post_processor.get_results_by_output_dir("1_cluster") -# aggregated_results = PostProcessor.aggregate_results( -# dl_samples=dl_res.system_dl_interf_power, -# ul_samples=ul_res.system_ul_interf_power, -# ul_tdd_factor=0.25, -# n_bs_sim=1 * 19 * 3 * 3, -# n_bs_actual=7 * 19 * 3 * 3 -# ) - -# relevant = post_processor\ -# .get_plot_by_results_attribute_name("system_dl_interf_power") - -# aggr_x, aggr_y = PostProcessor.cdf_from(aggregated_results) - -# relevant.add_trace( -# go.Scatter(x=aggr_x, y=aggr_y, mode='lines', name='Aggregate interference',), -# ) - -# relevant.show() diff --git a/sharc/campaigns/imt_mss_ras_2600_MHz/scripts/start_simulations_multi_thread.py b/sharc/campaigns/imt_mss_ras_2600_MHz/scripts/start_simulations_multi_thread.py deleted file mode 100644 index 2e3ac7123..000000000 --- a/sharc/campaigns/imt_mss_ras_2600_MHz/scripts/start_simulations_multi_thread.py +++ /dev/null @@ -1,12 +0,0 @@ -from sharc.run_multiple_campaigns_mut_thread import run_campaign - -# Set the campaign name -# The name of the campaign to run. This should match the name of the -# campaign directory. -name_campaign = "imt_mss_ras_2600_MHz" - -# Run the campaigns -# This function will execute the campaign with the given name. -# It will look for the campaign directory under the specified name and -# start the necessary processes. -run_campaign(name_campaign) diff --git a/sharc/campaigns/imt_mss_ras_2600_MHz/scripts/start_simulations_single_thread.py b/sharc/campaigns/imt_mss_ras_2600_MHz/scripts/start_simulations_single_thread.py deleted file mode 100644 index 799615c6b..000000000 --- a/sharc/campaigns/imt_mss_ras_2600_MHz/scripts/start_simulations_single_thread.py +++ /dev/null @@ -1,12 +0,0 @@ -from sharc.run_multiple_campaigns import run_campaign - -# Set the campaign name -# The name of the campaign to run. This should match the name of the -# campaign directory. -name_campaign = "imt_mss_ras_2600_MHz" - -# Run the campaign in single-thread mode -# This function will execute the campaign with the given name in a single-threaded manner. -# It will look for the campaign directory under the specified name and -# start the necessary processes. -run_campaign(name_campaign) diff --git a/sharc/campaigns/imt_ntn_footprint/scripts/imt_ntn_footprint.py b/sharc/campaigns/imt_ntn_footprint/scripts/imt_ntn_footprint.py deleted file mode 100644 index 6a163a53a..000000000 --- a/sharc/campaigns/imt_ntn_footprint/scripts/imt_ntn_footprint.py +++ /dev/null @@ -1,103 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt - -from sharc.topology.topology_ntn import TopologyNTN -from sharc.station_factory import StationFactory -from sharc.parameters.imt.parameters_imt import ParametersImt -from sharc.parameters.imt.parameters_antenna_imt import ParametersAntennaImt -from sharc.parameters.parameters_mss_ss import ParametersMssSs - - -if __name__ == "__main__": - - # Input parameters for MSS_SS - param_mss = ParametersMssSs() - param_mss.frequency = 2100.0 # MHz - param_mss.bandwidth = 20.0 # MHz - param_mss.altitude = 500e3 # meters - param_mss.azimuth = 0 - param_mss.elevation = 90 # degrees - param_mss.cell_radius = 19e3 # meters - param_mss.intersite_distance = param_mss.cell_radius * np.sqrt(3) - param_mss.num_sectors = 19 - param_mss.antenna_gain = 30 # dBi - param_mss.antenna_3_dB_bw = 4.4127 - param_mss.antenna_l_s = 20 # in dB - # Parameters used for the S.1528 antenna - param_mss.antenna_pattern = "ITU-R-S.1528-Taylor" - # param_mss.antenna_pattern = "ITU-R-S.1528-LEO" - wavelength = 3e8 / (param_mss.frequency * 1e6) # in meters - # assuming 7dB roll-off factor and circular antenna - l_r = 0.74 * wavelength / \ - np.sin(param_mss.antenna_3_dB_bw / 2) # in meters - l_t = l_r - param_mss.antenna_s1528.set_external_parameters( - frequency=param_mss.frequency, - bandwidth=param_mss.bandwidth, - antenna_gain=param_mss.antenna_gain, - antenna_l_s=param_mss.antenna_l_s, - antenna_3_dB_bw=param_mss.antenna_3_dB_bw, - l_r=l_r, - l_t=l_t) - beam_idx = 15 # beam index used for gain analysis - - seed = 100 - rng = np.random.RandomState(seed) - - # Parameters used for IMT-NTN and UE distribution - param_imt = ParametersImt() - param_imt.topology.type = "NTN" - param_imt.ue.azimuth_range = (-180, 180) - param_imt.bandwidth = 10 # MHz - param_imt.frequency = 2100 # MHz - param_imt.spurious_emissions = -13 # dB - param_imt.ue.distribution_azimuth = "UNIFORM" - param_imt.ue.k = 1000 - - ntn_topology = TopologyNTN(param_mss.intersite_distance, - param_mss.cell_radius, - param_mss.altitude, - param_mss.azimuth, - param_mss.elevation, - param_mss.num_sectors) - - ntn_topology.calculate_coordinates() - param_ue_ant = ParametersAntennaImt() - ntn_ue = StationFactory.generate_imt_ue_outdoor( - param_imt, param_ue_ant, rng, ntn_topology) - - ntn_ue.active = np.ones(ntn_ue.num_stations, dtype=bool) - ntn_bs = StationFactory.generate_mss_ss(param_mss) - phi, theta = ntn_bs.get_pointing_vector_to(ntn_ue) - station_1_active = np.where(ntn_bs.active)[0] - station_2_active = np.where(ntn_ue.active)[0] - beams_idx = np.zeros(len(station_2_active), dtype=int) - off_axis_angle = ntn_bs.get_off_axis_angle(ntn_ue) - gains = np.zeros(phi.shape) - for k in station_1_active: - gains[k, station_2_active] = ntn_bs.antenna[k].calculate_gain( - off_axis_angle_vec=off_axis_angle[k, station_2_active], theta_vec=theta[k, station_2_active]) - # phi=off_axis_angle[k, station_2_active], theta=theta[k, - # station_2_active]) - - fig = plt.figure() - ax = fig.add_subplot(111, projection='3d') - # ax.set_xlim([-200, 200]) - # ax.set_ylim([-200, 200]) - ntn_topology.plot_3d(ax, False) # Plot the 3D topology - im = ax.scatter( - xs=ntn_ue.x / - 1000, - ys=ntn_ue.y / - 1000, - c=gains[beam_idx] - - np.max( - param_mss.antenna_gain), - vmin=- - 50, - cmap='jet') - ax.view_init(azim=0, elev=90) - fig.colorbar(im, label='Normalized antenna gain (dBi)') - - plt.show() - exit() diff --git a/sharc/campaigns/imt_ntn_to_imt_tn_co_channel/README.md b/sharc/campaigns/imt_ntn_to_imt_tn_co_channel/README.md deleted file mode 100644 index b81d3b0ee..000000000 --- a/sharc/campaigns/imt_ntn_to_imt_tn_co_channel/README.md +++ /dev/null @@ -1,39 +0,0 @@ -MSS-SS to IMT-DL Simulation – 2300 MHz Band - -šŸ“„ Overview - -This folder contains the simulation setup for evaluating interference from a Mobile Satellite Service - Single Station (MSS-SS) to an IMT downlink (IMT-DL) system operating in the 2300 MHz frequency band. - -The scenario models a single MSS-SS satellite footprint located near the IMT coverage area. The simulation varies the distance between the MSS-SS footprint border and the IMT coverage edge, and computes the resulting Interference-to-Noise Ratio (INR). - -āø» - -šŸš€ Running the Simulation - 1. Generate simulation parameters -Run the parameter generation script: - -./scripts/parameter_gen.py - - - 2. Start the simulation - • For parallel execution (multi-threaded): - -./scripts/start_simulations_multi_thread.py - - - • For serial execution (single-threaded): - -./scripts/start_simulations_single_thread.py - - - -āø» - -šŸ“Š Generating Results - -After the simulations are complete, generate the result plots by running: - -./scripts/plot_resutls.py - -This will produce interactive Plotly HTML graphs summarizing the simulation outcomes. - diff --git a/sharc/campaigns/imt_ntn_to_imt_tn_co_channel/input/parameters_imt_ntn_to_imt_tn_sep_distance_template.yaml b/sharc/campaigns/imt_ntn_to_imt_tn_co_channel/input/parameters_imt_ntn_to_imt_tn_sep_distance_template.yaml deleted file mode 100644 index ff5a730c5..000000000 --- a/sharc/campaigns/imt_ntn_to_imt_tn_co_channel/input/parameters_imt_ntn_to_imt_tn_sep_distance_template.yaml +++ /dev/null @@ -1,514 +0,0 @@ -# ########################################################################### -# Parameters for the MSS_SS to IMT-DL interferece scenario in the 2300MHz band. -# In this scenario the distance between the border of the MSS-SS footprint -# and the IMT cluster is varied. -# ########################################################################### -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots: 10000 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link: DOWNLINK - ########################################################################### - # The chosen system for sharing study - # EESS_PASSIVE, FSS_SS, FSS_ES, FS, RAS, MSS_SS - system: MSS_SS - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel: TRUE - enable_adjacent_channel: false - ########################################################################### - # Seed for random number generator - seed: 101 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output: FALSE - ########################################################################### - # output destination folder - this is relative SHARC/sharc directory - output_dir: campaigns/imt_ntn_to_imt_tn_co_channel/output/ - ########################################################################### - # output folder prefix - output_dir_prefix: output_imt_ntn_to_imt_tn_co_channel_sep_0km -imt: - ########################################################################### - # Minimum 2D separation distance from BS to UE [m] - minimum_separation_distance_bs_ue: 35 - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE: IMT suffers interference - # FALSE : IMT generates interference - interfered_with: TRUE - ########################################################################### - # IMT center frequency [MHz] - frequency: 2300.0 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth: 20.0 - ########################################################################### - # IMT resource block bandwidth [MHz] - rb_bandwidth: 0.180 - ########################################################################### - # IMT spectrum emission mask. Options are: - # "IMT-2020" : for mmWave as described in ITU-R TG 5/1 Contribution 36 - # "3GPP E-UTRA" : for E-UTRA bands > 1 GHz as described in - # TS 36.104 v11.2.0 (BS) and TS 36.101 v11.2.0 (UE) - spectral_mask: 3GPP E-UTRA - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions: -13.0 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio: 0.1 - ########################################################################### - # Network topology parameters. - topology: - ########################################################################### - # Topology Type. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR" - type: MACROCELL - ########################################################################### - # Macrocell Topology parameters. Relevant when imt.topology.type == "MACROCELL" - macrocell: - ########################################################################### - # Inter-site distance in macrocell network topology [m] - intersite_distance: 750.0 - ########################################################################### - # Enable wrap around. - wrap_around: false - ########################################################################### - # Number of clusters in macro cell topology - num_clusters: 1 - ########################################################################### - # Single Base Station Topology parameters. Relevant when imt.topology.type == "SINGLE_BS" - single_bs: - ########################################################################### - # Inter-site distance or Cell Radius in single Base Station network topology [m] - # You can either provide 'cell_radius' or 'intersite_distance' for this topology - # The relationship used is cell_radius = intersite_distance * 2 / 3 - cell_radius: 543 - # intersite_distance: 1 - ########################################################################### - # Number of clusters in single base station topology - # You can simulate 1 or 2 BS's with this topology - num_clusters: 2 - ########################################################################### - # Hotspot Topology parameters. Relevant when imt.topology.type == "HOTSPOT" - hotspot: - ########################################################################### - # Inter-site distance in hotspot network topology [m] - intersite_distance: 321 - ########################################################################### - # Enable wrap around. - wrap_around: true - ########################################################################### - # Number of clusters in macro cell topology - num_clusters: 7 - ########################################################################### - # Number of hotspots per macro cell (sector) - num_hotspots_per_cell: 1 - ########################################################################### - # Maximum 2D distance between hotspot and UE [m] - # This is the hotspot radius - max_dist_hotspot_ue: 99.9 - ########################################################################### - # Minimum 2D distance between macro cell base station and hotspot [m] - min_dist_bs_hotspot: 1.2 - ########################################################################### - # Indoor Topology parameters. Relevant when imt.topology.type == "INOOR" - indoor: - ########################################################################### - # Basic path loss model for indoor topology. Possible values: - # "FSPL" (free-space path loss), - # "INH_OFFICE" (3GPP Indoor Hotspot - Office) - basic_path_loss: FSPL - ########################################################################### - # Number of rows of buildings in the simulation scenario - n_rows: 3 - ########################################################################### - # Number of colums of buildings in the simulation scenario - n_colums: 2 - ########################################################################### - # Number of buildings containing IMT stations. Options: - # 'ALL': all buildings contain IMT stations. - # Number of buildings. - num_imt_buildings: 2 - ########################################################################### - # Street width (building separation) [m] - street_width: 30.1 - ########################################################################### - # Intersite distance [m] - intersite_distance: 40.1 - ########################################################################### - # Number of cells per floor - num_cells: 3 - ########################################################################### - # Number of floors per building - num_floors: 1 - ########################################################################### - # Percentage of indoor UE's [0, 1] - ue_indoor_percent: .95 - ########################################################################### - # Building class: "TRADITIONAL" or "THERMALLY_EFFICIENT" - building_class: THERMALLY_EFFICIENT - ########################################################################### - # NTN Topology Parameters - ntn: - ########################################################################### - # NTN cell radius or intersite distance in network topology [m] - # @important: You can set only one of cell_radius or intersite_distance - # NTN Intersite Distance (m). Intersite distance = Cell Radius * sqrt(3) - # NOTE: note that intersite distance has a different geometric meaning in ntn - cell_radius: 123 - # intersite_distance: 155884 - ########################################################################### - # NTN space station azimuth [degree] - bs_azimuth: 45 - ########################################################################### - # NTN space station elevation [degree] - bs_elevation: 45 - ########################################################################### - # number of sectors [degree] - num_sectors: 19 - ########################################################################### - # Backoff Power [Layer 2] [dB]. Allowed: 7 sector topology - Layer 2 - bs_backoff_power: 3 - ########################################################################### - # NTN Antenna configuration - bs_n_rows_layer1 : 2 - bs_n_columns_layer1: 2 - bs_n_rows_layer2 : 4 - bs_n_columns_layer2: 2 - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model: BEAMFORMING - # Base station parameters - bs: - ########################################################################### - # The load probability (or activity factor) models the statistical - # variation of the network load by defining the number of fully loaded - # base stations that are simultaneously transmitting - load_probability: .5 - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power: 28.0 - ########################################################################### - # Base station height [m] - height: 20.0 - ########################################################################### - # Base station noise figure [dB] - noise_figure: 5.0 - ########################################################################### - # Base station array ohmic loss [dB] - ohmic_loss: 2.0 - # Base Station Antenna parameters: - antenna: - array: - ########################################################################### - # If normalization of M2101 should be applied for BS - normalization: FALSE - ########################################################################### - # If normalization of M2101 should be applied for UE - ########################################################################### - # File to be used in the BS beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/bs_norm.npz - ########################################################################### - # File to be used in the UE beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor simulations - downtilt: 6 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g: 6.4 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 90 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 65 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 4 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 8 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 2.1 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 30 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - - ########################################################################### - # User Equipment parameters: - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k: 1 - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m: 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent: 70 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type: UNIFORM - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance: RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth: UNIFORM - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control: ON - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch: -92.2 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha: 0.8 - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax: 23 - ########################################################################### - # UE power dynamic range [dB] - # The minimum transmit power of a UE is (p_cmax - dynamic_range) - power_dynamic_range: 63 - ########################################################################### - # UE height [m] - height: 1.5 - ########################################################################### - # User equipment noise figure [dB] - noise_figure: 9 - ########################################################################### - # User equipment feed loss [dB] - ohmic_loss: 3 - ########################################################################### - # User equipment body loss [dB] - body_loss: 4 - antenna: - array: - ########################################################################### - # If normalization of M2101 should be applied for UE - normalization: FALSE - ########################################################################### - # File to be used in the UE beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/ue_norm.npz - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: FIXED - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g: -3 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 90 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 90 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 1 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 1 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 25 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - ########################################################################### - # Uplink parameters. Only needed when using uplink on imt - uplink: - ########################################################################### - # Uplink attenuation factor used in link-to-system mapping - attenuation_factor: 0.4 - ########################################################################### - # Uplink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Uplink maximum SINR of the code set [dB] - sinr_max: 22 - ########################################################################### - # Downlink parameters. Only needed when using donwlink on imt - downlink: - ########################################################################### - # Downlink attenuation factor used in link-to-system mapping - attenuation_factor: 0.8 - ########################################################################### - # Downlink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Downlink maximum SINR of the code set [dB] - sinr_max: 30 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "CI" (close-in FS reference distance) - # "UMa" (Urban Macro - 3GPP) - # "UMi" (Urban Micro - 3GPP) - # "TVRO-URBAN" - # "TVRO-SUBURBAN" - # "ABG" (Alpha-Beta-Gamma) - # "P619" - channel_model: UMa - season: SUMMER - ########################################################################### - # Adjustment factor for LoS probability in UMi path loss model. - # Original value: 18 (3GPP) - los_adjustment_factor: 18 - ########################################################################### - # If shadowing should be applied or not. - # Used in propagation UMi, UMa, ABG, when topology == indoor and any - shadowing: FALSE - ########################################################################### - # receiver noise temperature [K] - noise_temperature: 290 - ########################################################################### - # P619 parameters - param_p619: - ########################################################################### - # altitude of the space station [m] - space_station_alt_m: 540000 - ########################################################################### - # altitude of ES system [m] - earth_station_alt_m: 1200 - ########################################################################### - # latitude of ES system [m] - earth_station_lat_deg: 13 - ########################################################################### - # difference between longitudes of IMT-NTN station and FSS-ES system [deg] - # (positive if space-station is to the East of earth-station) - earth_station_long_diff_deg: 0 -#### System Parameters -mss_ss: - # Satellite bore-sight - this is the center of the central beam in meters (m) - x: 0.0 - y: 0.0 - # MSS_SS system center frequency in MHz - frequency: 2300.0 - # MSS_SS system bandwidth in MHz - bandwidth: 5.0 - # MSS_SS altitude w.r.t. sea level in meters (m) - altitude: 525000 - # NTN cell radius in network topology [m] - cell_radius: 39475.0 - # Satellite power density in dBW/Hz - tx_power_density: -52.2 - # Satellite Tx max Gain in dBi - antenna_gain: 34.1 - # Satellite azimuth w.r.t. simulation x-axis - azimuth: 0.0 - # Satellite elevation w.r.t. simulation xy-plane (horizon) - elevation: 90.0 - # Number of sectors - num_sectors: 19 - # Satellite antenna pattern - # Antenna pattern from ITU-R S.1528 - # Possible values: "ITU-R-S.1528-Section1.2", "ITU-R-S.1528-LEO" - antenna_pattern: ITU-R-S.1528-Taylor - antenna_s1528: - ### The following parameters are used for S.1528-Taylor antenna pattern - # Maximum Antenna gain in dBi - antenna_gain: 34.1 - # SLR is the side-lobe ratio of the pattern (dB), the difference in gain between the maximum - # gain and the gain at the peak of the first side lobe. - slr: 20.0 - # Number of secondary lobes considered in the diagram (coincide with the roots of the Bessel function) - n_side_lobes: 2 - # Radial (l_r) and transverse (l_t) sizes of the effective radiating area of the satellite transmitt antenna (m) - l_r: 1.6 - l_t: 1.6 - channel_model: FSPL - ########################################################################### - # P619 parameters - param_p619: - ########################################################################### - # altitude of ES system [m] - earth_station_alt_m: 0 - ########################################################################### - # latitude of ES system [m] - earth_station_lat_deg: 0 - ########################################################################### - # difference between longitudes of IMT-NTN station and FSS-ES system [deg] - # (positive if space-station is to the East of earth-station) - earth_station_long_diff_deg: 0.75 - ########################################################################### - # year season: SUMMER of WINTER - season: SUMMER \ No newline at end of file diff --git a/sharc/campaigns/imt_ntn_to_imt_tn_co_channel/scripts/calculate_cluster_positions.py b/sharc/campaigns/imt_ntn_to_imt_tn_co_channel/scripts/calculate_cluster_positions.py deleted file mode 100644 index 12648dcba..000000000 --- a/sharc/campaigns/imt_ntn_to_imt_tn_co_channel/scripts/calculate_cluster_positions.py +++ /dev/null @@ -1,140 +0,0 @@ -# Helper script that calculates the coordintaes of the NTN footprint based -# on the distance between borders with IMT-TN -from sharc.parameters.parameters_mss_ss import ParametersMssSs -from sharc.topology.topology_ntn import TopologyNTN -from sharc.topology.topology_macrocell import TopologyMacrocell -import matplotlib.pyplot as plt -import numpy as np - -if __name__ == "__main__": - - # distance from topology boarders in meters - border_distances_array = np.array( - [0, 20e3, 50e3, 100e3, 200e3, 300e3, 400e3, 500e3, 600e3, 700e3, 1000e3]) - - # Index in border_distances_array used for plotting - dist_idx = 5 - - # Input parameters for MSS_SS - param_mss = ParametersMssSs() - param_mss.frequency = 2160 # MHz - # param_mss.altitude = 500e3 # meters - param_mss.altitude = 1200e3 # meters - param_mss.azimuth = 0 - param_mss.elevation = 90 # degrees - # param_mss.cell_radius = 19e3 # meters - param_mss.cell_radius = 45e3 # meters - param_mss.intersite_distance = param_mss.cell_radius * np.sqrt(3) - param_mss.num_sectors = 19 - - # Input paramters for IMT-TN macrocell topology - macro_cell_radius = 500 # meters - - macrocell_num_clusters = 1 - - ####################################################### - - # calculate the position of the NTN footprint center - ntn_footprint_left_edge = - 4 * param_mss.cell_radius - ntn_footprint_radius = 5 * param_mss.cell_radius * np.sin(np.pi / 3) - macro_topology_radius = 4 * macro_cell_radius - ntn_footprint_x_offset = macro_topology_radius + \ - ntn_footprint_radius + border_distances_array - param_mss.x = ntn_footprint_x_offset[dist_idx] - - ntn_topology = TopologyNTN(param_mss.intersite_distance, - param_mss.cell_radius, - param_mss.altitude, - param_mss.azimuth, - param_mss.elevation, - param_mss.num_sectors) - ntn_topology.calculate_coordinates() - ntn_topology.x = ntn_topology.x + param_mss.x - - seed = 100 - rng = np.random.RandomState(seed) - - intersite_distance = macro_cell_radius * 3 / 2 - macro_topology = TopologyMacrocell( - intersite_distance, macrocell_num_clusters) - macro_topology.calculate_coordinates() - - ntn_circle = plt.Circle( - (param_mss.x, - 0), - radius=ntn_footprint_radius, - fill=False, - color='green') - macro_cicle = plt.Circle( - (0, 0), radius=macro_topology_radius, fill=False, color='red') - - # Print simulation information - print("\n######## Simulation scenario parameters ########") - for i, d in enumerate(border_distances_array): - print(f"Satellite altitude = {param_mss.altitude / 1000} km") - print(f"Beam footprint radius = {param_mss.cell_radius / 1000} km") - print( - f"Satellite elevation w.r.t to boresight = { - param_mss.elevation} deg") - print( - f"Satellite azimuth w.r.t to boresight = { - param_mss.azimuth} deg") - print(f"Border distance = {d / 1000} km") - print( - f"NTN nadir offset w.r.t. IMT-TN cluster center = {ntn_footprint_x_offset[i] / 1000} km") - print( - f"Satellite elevation w.r.t. IMT-TN cluster center = { - np.round( - np.degrees( - np.arctan( - param_mss.altitude / - ntn_footprint_x_offset[i])))} deg") - slant_path_len = np.sqrt( - param_mss.altitude**2 + - ntn_footprint_x_offset[i]**2) - print( - f"Slant path lenght w.r.t. IMT-TN cluster center = {slant_path_len / 1000} km") - fspl = np.round( - 20 * - np.log10(slant_path_len) + - 20 * - np.log10( - param_mss.frequency * - 1e6) - - 147.55, - 2) - print(f"Free space pathloss w.r.t. IMT-TN cluster center = {fspl}\n") - - # Plot the coverage areas for NTN and TN - fig = plt.figure() - ax = fig.add_subplot(111) - - ntn_topology.plot(ax, scale=1) - macro_topology.plot(ax) - - ax.add_patch(macro_cicle) - ax.add_patch(ntn_circle) - ax.arrow( - macro_topology_radius, - 0, - param_mss.x - - ntn_footprint_radius - - macro_topology_radius, - 0, - width=0.1, - shape='full', - color='red') - ax.annotate( - f'Border distance\n{ - border_distances_array[dist_idx] / 1000} km', - ((macro_topology_radius + border_distances_array[dist_idx] / 2), - 1000)) - - plt.title("IMT-NTN vs IMT-TN Footprints") - plt.xlabel("x-coordinate [m]") - plt.ylabel("y-coordinate [m]") - plt.tight_layout() - plt.grid() - - plt.show() - exit() diff --git a/sharc/campaigns/imt_ntn_to_imt_tn_co_channel/scripts/parameter_gen.py b/sharc/campaigns/imt_ntn_to_imt_tn_co_channel/scripts/parameter_gen.py deleted file mode 100644 index 5cc4a6cc4..000000000 --- a/sharc/campaigns/imt_ntn_to_imt_tn_co_channel/scripts/parameter_gen.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Generates the parameters for the MSS-SS to IMT with varying border distances campaign. -Parameters for the MSS_SS to IMT-DL interferece scenario in the 2300MHz band. -In this scenario the distance between the border of the MSS-SS footprint -and the IMT cluster is varied. -""" -import numpy as np -import yaml -import os - -from sharc.parameters.parameters_base import tuple_constructor - -yaml.SafeLoader.add_constructor( - 'tag:yaml.org,2002:python/tuple', - tuple_constructor) - -local_dir = os.path.dirname(os.path.abspath(__file__)) -parameter_file_name = os.path.join( - local_dir, "../input/parameters_imt_ntn_to_imt_tn_sep_distance_template.yaml") - -# load the base parameters from the yaml file -with open(parameter_file_name, 'r') as file: - parameters_template = yaml.safe_load(file) - -# Distance from topology boarders in meters -border_distances_array = np.array( - [0, 10e3, 20e3, 30e3, 40e3, 50e3, 60e3, 70e3, 80e3, 90e3, 100e3]) - -for dist in border_distances_array: - print(f'Generating parameters for distance {dist / 1e3} km') - # Create a copy of the base parameters - params = parameters_template.copy() - - ntn_footprint_left_edge = - 4 * params['mss_ss']['cell_radius'] - ntn_footprint_radius = 5 * \ - params['mss_ss']['cell_radius'] * np.sin(np.pi / 3) - macro_topology_radius = 4 * \ - params['imt']['topology']['macrocell']['intersite_distance'] / 3 - params['mss_ss']['x'] = float( - macro_topology_radius + - ntn_footprint_radius + - dist) - - # Set the right campaign prefix - params['general']['output_dir_prefix'] = 'output_imt_ntn_to_imt_tn_co_channel_sep_' + \ - str(dist / 1e3) + "_km" - # Save the parameters to a new yaml file - parameter_file_name = "../input/parameters_imt_ntn_to_imt_tn_co_channel_sep_" + \ - str(dist / 1e3) + "_km.yaml" - with open(os.path.join(local_dir, parameter_file_name), 'w') as file: - yaml.dump(params, file, default_flow_style=False) - - print(f'Parameters saved to {parameter_file_name} file.') - - del params diff --git a/sharc/campaigns/imt_ntn_to_imt_tn_co_channel/scripts/plot_results.py b/sharc/campaigns/imt_ntn_to_imt_tn_co_channel/scripts/plot_results.py deleted file mode 100644 index b1d3a1945..000000000 --- a/sharc/campaigns/imt_ntn_to_imt_tn_co_channel/scripts/plot_results.py +++ /dev/null @@ -1,60 +0,0 @@ -import os -import numpy as np -from pathlib import Path -from sharc.results import Results -# import plotly.graph_objects as go -from sharc.post_processor import PostProcessor - -post_processor = PostProcessor() - -# Distance from topology boarders in meters -border_distances_array = np.array( - [0, 10e3, 20e3, 30e3, 40e3, 50e3, 60e3, 70e3, 80e3, 90e3, 100e3]) - -# Add a legend to results in folder that match the pattern -for dist in border_distances_array: - post_processor.add_plot_legend_pattern( - dir_name_contains=f"_imt_ntn_to_imt_tn_co_channel_sep_{dist / 1e3}_km", - legend=f"separation {dist / 1e3} Km" - ) - -campaign_base_dir = str((Path(__file__) / ".." / "..").resolve()) - -many_results = Results.load_many_from_dir( - os.path.join( - campaign_base_dir, - "output"), - only_latest=True) -# ^: typing.List[Results] - -post_processor.add_results(many_results) - -plots = post_processor.generate_cdf_plots_from_results( - many_results -) - -post_processor.add_plots(plots) - -# Add a protection criteria line: -protection_criteria = -6 -post_processor\ - .get_plot_by_results_attribute_name("imt_dl_inr")\ - .add_vline(protection_criteria, line_dash="dash") - -# Plot every plot -plots_dir = Path(campaign_base_dir) / "output" / "plots" -plots_dir.mkdir(parents=True, exist_ok=True) -for plot in plots: - plot.update_layout(legend_traceorder="normal") - plot.write_html( - plots_dir / f"{plot.layout.meta['related_results_attribute']}.html", - include_plotlyjs="cdn", - auto_open=False, - config={"displayModeBar": False} - ) - -for result in many_results: - # This generates the mean, median, variance, etc - stats = PostProcessor.generate_statistics( - result=result - ).write_to_results_dir() diff --git a/sharc/campaigns/imt_ntn_to_imt_tn_co_channel/scripts/start_simulations_multi_thread.py b/sharc/campaigns/imt_ntn_to_imt_tn_co_channel/scripts/start_simulations_multi_thread.py deleted file mode 100644 index 9301a71a3..000000000 --- a/sharc/campaigns/imt_ntn_to_imt_tn_co_channel/scripts/start_simulations_multi_thread.py +++ /dev/null @@ -1,14 +0,0 @@ -from sharc.run_multiple_campaigns_mut_thread import run_campaign_re - -# Set the campaign name -# The name of the campaign to run. This should match the name of the -# campaign directory. -name_campaign = "imt_ntn_to_imt_tn_co_channel" - -# Run the campaigns -# This function will execute the campaign with the given name. -# It will look for the campaign directory under the specified name and -# start the necessary processes. -regex_pattern = r'^parameters_imt_ntn_to_imt_tn_co_channel_sep_.*_km.yaml' -run_campaign_re("imt_ntn_to_imt_tn_co_channel", regex_pattern) -print("Executing campaign with regex pattern:", regex_pattern) diff --git a/sharc/campaigns/imt_ntn_to_imt_tn_co_channel/scripts/start_simulations_single_thread.py b/sharc/campaigns/imt_ntn_to_imt_tn_co_channel/scripts/start_simulations_single_thread.py deleted file mode 100644 index d2cc5583d..000000000 --- a/sharc/campaigns/imt_ntn_to_imt_tn_co_channel/scripts/start_simulations_single_thread.py +++ /dev/null @@ -1,14 +0,0 @@ -from sharc.run_multiple_campaigns import run_campaign_re - -# Set the campaign name -# The name of the campaign to run. This should match the name of the -# campaign directory. -name_campaign = "imt_ntn_to_imt_tn_co_channel" - -# Run the campaign in single-thread mode -# This function will execute the campaign with the given name in a single-threaded manner. -# It will look for the campaign directory under the specified name and -# start the necessary processes. -regex_pattern = r'^parameters_imt_ntn_to_imt_tn_co_channel_sep_.*_km.yaml' -run_campaign_re("imt_ntn_to_imt_tn_co_channel", regex_pattern) -print("Executing campaign with regex pattern:", regex_pattern) diff --git a/sharc/campaigns/mss_d2d_to_eess/input/.gitignore b/sharc/campaigns/mss_d2d_to_eess/input/.gitignore deleted file mode 100644 index d6b7ef32c..000000000 --- a/sharc/campaigns/mss_d2d_to_eess/input/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/sharc/campaigns/mss_d2d_to_eess/scripts/base_input.yaml b/sharc/campaigns/mss_d2d_to_eess/scripts/base_input.yaml deleted file mode 100644 index bdcd18a25..000000000 --- a/sharc/campaigns/mss_d2d_to_eess/scripts/base_input.yaml +++ /dev/null @@ -1,552 +0,0 @@ -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots: 10000 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link: DOWNLINK - ########################################################################### - # The chosen system for sharing study - system: SINGLE_EARTH_STATION - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel: false - enable_adjacent_channel: true - ########################################################################### - # Seed for random number generator - seed: 681603 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output: FALSE - ########################################################################### - # output destination folder - this is relative SHARC/sharc directory - output_dir: campaigns/mss_d2d_to_eess/output/ - ########################################################################### - # output folder prefix - output_dir_prefix: output_mss_d2d_to_eess_base -imt: - ########################################################################### - # Minimum 2D separation distance from BS to UE [m] - minimum_separation_distance_bs_ue: 35 - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE: IMT suffers interference - # FALSE : IMT generates interference - interfered_with: False - ########################################################################### - # IMT center frequency [MHz] - frequency: 2167.5 # B1/B4 - SPACE STATION - 2110-2Ā 170 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth: 5.0 - ########################################################################### - # IMT resource block bandwidth [MHz] - rb_bandwidth: 0.180 - # IMT resource block bandwidth [MHz] - ########################################################################### - adjacent_ch_reception: ACS - ########################################################################### - adjacent_ch_emissions: SPECTRAL_MASK - adjacent_ch_leak_ratio: 50.0 # dB - ########################################################################### - # IMT spectrum emission mask. Options are: - # "IMT-2020" : for mmWave as described in ITU-R TG 5/1 Contribution 36 - # "3GPP E-UTRA" : for E-UTRA bands > 1 GHz as described in - # TS 36.104 v11.2.0 (BS) and TS 36.101 v11.2.0 (UE) - spectral_mask: MSS - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions: -13.0 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio: 0.1 - ########################################################################### - # Network topology parameters. - topology: - ## Refernce latitude and longitude taken from INPE/Cachoeria Paulista - ########################################################################### - # The latitude position - central_latitude: -22.681603 - ########################################################################### - # The longitude position - central_longitude: -45.002609 - ########################## - central_altitude: 563 # meters - ########################################################################### - # Topology Type. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR", "MSS_DC" - type: MSS_DC - ########################################################################### - # Topology parameters for the MSS-DC network - mss_dc: - # MSS_DC cell radius in network topology [m] - beam_radius: 39475.0 - # Number of sectors - num_beams: 1 - beam_positioning: - # type may be one of - # "ANGLE_FROM_SUBSATELLITE", "ANGLE_AND_DISTANCE_FROM_SUBSATELLITE", - # "SERVICE_GRID" - # when "ANGLE_FROM_SUBSATELLITE", both phi and theta must be specified - # when "ANGLE_AND_DISTANCE_FROM_SUBSATELLITE", both phi and distance - # must be specified - type: ANGLE_FROM_SUBSATELLITE - # theta is the off axis angle from satellite nadir - angle_from_subsatellite_theta: - # type may be one of - # "FIXED", "~U(MIN,MAX)", "~SQRT(U(0,1))*MAX" - type: FIXED - # used if TYPE == FIXED is specified - fixed: 0 - # used if some distribution type is specified - # distribution: - # min: 0.0 - # max: 66.1 - # phi completes polar coordinates - # equivalent to "azimuth" from subsatellite in earth plane - angle_from_subsatellite_phi: - # type may be one of - # "FIXED", "~U(MIN,MAX)", "~SQRT(U(0,1))*MAX" - type: FIXED - # used if TYPE == FIXED is specified - fixed: 0 - # used if some distribution type is specified - # distribution: - # min: 0.0 - # max: 66.1 - # distance from subsatellite. Substitutes theta - # distance_from_subsatellite: - # # type may be one of - # # "FIXED", "~U(MIN,MAX)", "~SQRT(U(0,1))*MAX" - # type: FIXED - # # used if TYPE == FIXED is specified - # fixed: 0 - # # used if some distribution type is specified - # distribution: - # min: 0.0 - # max: 66.1 - # service_grid: - # # by default this already gets shapefile from natural earth - # country_shapes_filename: sharc/data/countries/ne_110m_admin_0_countries.shp - # # By default, these are the same as taken from `sat_is_active_if.lat_long_inside_country` - # country_names: - # - Brazil - # - Chile - - # This margin defines where the grid is constructed - # margin from inside of border [km] - # By default, this is made to be the same as beam_radius - # if positive, makes border smaller by x km - # if negative, makes border bigger by x km - # grid_margin_from_border: 0.11 - - # This margin defines what satellites may serve the grid - # it should normally be a smaller value than the grid from border - # since a satellite not right above the grid may serve some point better - # margin from inside of border [km] - # By default, this is made to be the same as beam_radius - # if positive, makes border smaller by x km - # if negative, makes border bigger by x km - # eligible_sats_margin_from_border: -2.1 - sat_is_active_if: - # for a satellite to be active, it needs to respect ALL conditions - conditions: - # - LAT_LONG_INSIDE_COUNTRY - - MINIMUM_ELEVATION_FROM_ES - # - MAXIMUM_ELEVATION_FROM_ES - minimum_elevation_from_es: 5.0 - # maximum_elevation_from_es: 80.0 - # lat_long_inside_country: - # # You may specify another shp file for country borders reference - # # country_shapes_filename: sharc/topology/countries/ne_110m_admin_0_countries.shp - # country_names: - # - Brazil - # - Ecuador - # # margin from inside of border [km] - # # if positive, makes border smaller by x km - # # if negative, makes border bigger by x km - # margin_from_border: 111.0 - ########################################################################### - # Parameters for the orbits - orbits: # System 3 parameters - # Number of planes - - n_planes: 28 - # Inclination in degrees - inclination_deg: 53.0 - # Perigee in km - perigee_alt_km: 525.0 - # Apogee in km - apogee_alt_km: 525.0 - # Number of satellites per plane - sats_per_plane: 120 - # Longitude of the first ascending node in degrees - long_asc_deg: 0.0 - # Phasing in degrees - phasing_deg: 1.5 - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model: SINGLE_ELEMENT - # Base station parameters - bs: - ########################################################################### - # The load probability (or activity factor) models the statistical - # variation of the network load by defining the number of fully loaded - # base stations that are simultaneously transmitting - load_probability: 1.0 - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power: 41.4 # -55.6 [dB/Hz] + 10log(5*1e6)[Hz] + 30.0 - ########################################################################### - # Base station height [m] - height: 20.0 - ########################################################################### - # Base station noise figure [dB] - noise_figure: 5.0 - ########################################################################### - # Base station array ohmic loss [dB] - ohmic_loss: 3.0 - # Base Station Antenna parameters: - antenna: - pattern: ARRAY - # gain: 0 # set to zero when using the adjacent MSS antenna model - # itu_r_s_1528: - # antenna_pattern: ITU-R-S.1528-Taylor - # ### The following parameters are used for S.1528-Taylor antenna pattern - # # Maximum Antenna gain in dBi - # antenna_gain: 34.1 - # # SLR is the side-lobe ratio of the pattern (dB), the difference in gain between the maximum - # # gain and the gain at the peak of the first side lobe. - # slr: 20.0 - # # Number of secondary lobes considered in the diagram (coincide with the roots of the Bessel function) - # n_side_lobes: 2 - # # Radial (l_r) and transverse (l_t) sizes of the effective radiating area of the satellite transmitt antenna (m) - # l_r: 1.6 - # l_t: 1.6 - # mss_adjacent: - # frequency: 2167.5 # MHz - # # Base Station Antenna parameters: - array: - ########################################################################### - # If normalization of M2101 should be applied for BS - normalization: FALSE - ########################################################################### - # Base station horizontal beamsteering range. [deg] - # The range considers the BS azimuth as 0deg - horizontal_beamsteering_range: !!python/tuple [-60., 60.] - ########################################################################### - # Base station horizontal beamsteering range. [deg] - # The range considers the horizon as 90deg, 0 as zenith - vertical_beamsteering_range: !!python/tuple [90., 100.] - ########################################################################### - # File to be used in the BS beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/bs_norm.npz - ########################################################################### - # File to be used in the UE beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor simulations - downtilt: 0.0 # the topology MSS takes care of this - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g: 6.4 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 90 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 65 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 1 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 1 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 2.1 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 30 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - subarray: - ########################################################################### - # to use subarray, set this to true - is_enabled: false - ########################################################################### - # Number of rows in subarray - n_rows: 3 - ########################################################################### - # BS array element vertical spacing (d/lambda). - element_vert_spacing: 0.7 - ########################################################################### - # Sub array eletrical downtilt [deg] - # notice that electrical tilt == -1 * downtilt - eletrical_downtilt: 3.0 - - ########################################################################### - # User Equipment parameters: - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k: 1 - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m: 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent: 0.0 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type: UNIFORM_IN_CELL - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance: RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth: UNIFORM - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control: ON - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch: -92.2 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha: 0.8 - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax: 23 - ########################################################################### - # UE power dynamic range [dB] - # The minimum transmit power of a UE is (p_cmax - dynamic_range) - power_dynamic_range: 63 - ########################################################################### - # UE height [m] - height: 1.5 - ########################################################################### - # User equipment noise figure [dB] - noise_figure: 9 - ########################################################################### - # User equipment feed loss [dB] - ohmic_loss: 0 - ########################################################################### - # User equipment body loss [dB] - body_loss: 4 - antenna: - array: - ########################################################################### - # If normalization of M2101 should be applied for UE - normalization: FALSE - ########################################################################### - # File to be used in the UE beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/ue_norm.npz - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: FIXED - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g: -3 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 90 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 90 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 1 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 1 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 25 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - ########################################################################### - # Uplink parameters. Only needed when using uplink on imt - uplink: - ########################################################################### - # Uplink attenuation factor used in link-to-system mapping - attenuation_factor: 0.4 - ########################################################################### - # Uplink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Uplink maximum SINR of the code set [dB] - sinr_max: 22 - ########################################################################### - # Downlink parameters. Only needed when using donwlink on imt - downlink: - ########################################################################### - # Downlink attenuation factor used in link-to-system mapping - attenuation_factor: 0.8 - ########################################################################### - # Downlink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Downlink maximum SINR of the code set [dB] - sinr_max: 30 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "CI" (close-in FS reference distance) - # "UMa" (Urban Macro - 3GPP) - # "UMi" (Urban Micro - 3GPP) - # "TVRO-URBAN" - # "TVRO-SUBURBAN" - # "ABG" (Alpha-Beta-Gamma) - # "P619" - channel_model: FSPL - season: SUMMER - ########################################################################### - # Adjustment factor for LoS probability in UMi path loss model. - # Original value: 18 (3GPP) - los_adjustment_factor: 18 - ########################################################################### - # If shadowing should be applied or not. - # Used in propagation UMi, UMa, ABG, when topology == indoor and any - shadowing: FALSE - ########################################################################### - # receiver noise temperature [K] - noise_temperature: 290 - ########################################################################### - # P619 parameters - param_p619: - ########################################################################### - # altitude of the space station [m] - space_station_alt_m: 540000 - ########################################################################### - # altitude of ES system [m] - earth_station_alt_m: 1200 - ########################################################################### - # latitude of ES system [m] - earth_station_lat_deg: 13 - ########################################################################### - # difference between longitudes of IMT-NTN station and FSS-ES system [deg] - # (positive if space-station is to the East of earth-station) - earth_station_long_diff_deg: 0 -#### System Parameters -# Single Earth Station representing an EESS Earth Station in the 2.0 GHz band. -# Referce data comes from the REPLY LIAISON STATEMENT TO WORKING PARTY 4C ON -# WRC-27 AGENDA ITEMS 1.12, 1.13, 1.14 AND 1.15 - Document 4C/196-E -single_earth_station: - frequency: 2200 # MHz - bandwidth: 4 # MHz - # tx power density just so simulator runs - tx_power_density: -200 - # also just so simulator runs - noise_temperature: 190 - # This is based in pratical data found in other documents. - # 35-45dBs are generally assumed. We take 35 as a worst case. - adjacent_ch_selectivity: 35 - # Channel model - channel_model: FSPL - # NOTE: when using P.619 it is suggested that polarization loss = 3dB is good - polarization_loss: 0 - # Earth Station parameters - geometry: - height: 7 - azimuth: - # Type of elevation. - type: UNIFORM_DIST - uniform_dist: - max: 180 - min: -180 - # Elevation angle [degrees] - elevation: - # Type of elevation. - type: FIXED - fixed: 90 - # Station 2d location [meters]: - location: - type: FIXED - fixed: - x: 0 - y: 0 - antenna: - ########################################################################### - # Choose the antenna pattern. Can be one of: - # "OMNI", "ITU-R F.699", "ITU-R S.465", "ITU-R S.580", "MODIFIED ITU-R S.465", - # "ITU-R S.1855", "ITU-R S.672" - # since 20deg beamwidth would already cover every area - pattern: ITU-R S.465 - ########################################################################### - # Station peak receive antenna gain [dBi] - gain: 45.8 - itu_r_s_465: - # Diameter [m] - diameter: 12 # derived with efficiency 0.5 - diff --git a/sharc/campaigns/mss_d2d_to_eess/scripts/generate_inputs.py b/sharc/campaigns/mss_d2d_to_eess/scripts/generate_inputs.py deleted file mode 100644 index a4ea945ff..000000000 --- a/sharc/campaigns/mss_d2d_to_eess/scripts/generate_inputs.py +++ /dev/null @@ -1,140 +0,0 @@ -"""Generates scenarios based on main parameters -""" -from dataclasses import dataclass -import typing -import numpy as np -import yaml -import os -from copy import deepcopy - -from sharc.parameters.parameters_base import tuple_constructor - - -@dataclass -class ESParams(): - """ - Data class representing Earth Station parameters for scenario generation. - - Attributes: - name (str): Name of the earth station system. - antenna_gain (float): Antenna gain in dBi. - receive_temperature (float): Receiver noise temperature in K. - frequency (float): Operating frequency in MHz. - bandwidth (float): Bandwidth in MHz. - antenna_diameter (float, optional): Antenna diameter in meters. - antenna_pattern (Literal): Antenna pattern type. - """ - name: str - # [dBi] - antenna_gain: float - # [K] - receive_temperature: float - # [MHz] - frequency: float - # [MHz] - bandwidth: float - # [m] - antenna_diameter: float = None - # TODO: add "ITU RR. Appendix 8, annex III" antenna pattern - antenna_pattern: typing.Literal[ - "ITU-R S.465", "ITU RR. Appendix 8, annex III" - ] = "ITU-R S.465" - - def __post_init__(self): - if self.antenna_diameter is None: - # antenna efficiency: - n = 0.5 - lmbda = 3e8 / (self.frequency * 1e6) - G = 10**(self.antenna_gain / 10) - self.antenna_diameter = float(np.round( - lmbda * np.sqrt(G / n) / np.pi, decimals=2 - )) - print( - f"Earth station { - self.name} antenna diameter of { - self.antenna_diameter} " f"has been assumed for an efficiency of {n}.") - - -yaml.SafeLoader.add_constructor( - 'tag:yaml.org,2002:python/tuple', - tuple_constructor) - -local_dir = os.path.dirname(os.path.abspath(__file__)) -input_dir = os.path.join(local_dir, "../input") - -parameter_file_name = os.path.join(local_dir, "./base_input.yaml") - -# load the base parameters from the yaml file -with open(parameter_file_name, 'r') as file: - gen_parameters = yaml.safe_load(file) - -# doesn't matter from which, both will give same result -output_prefix_pattern = gen_parameters['general']['output_dir_prefix'].replace( - "_base", "_") - -freq = 2200 - -system_B = ESParams( - name="system_b", - antenna_gain=45.8, - receive_temperature=190, - frequency=freq, - bandwidth=4, # MHz -) - -system_D = ESParams( - name="system_d", - antenna_gain=39, - receive_temperature=120, - frequency=freq, - bandwidth=6, # MHz -) - -for sys in [ - system_B, - system_D, -]: - sys_parameters = deepcopy(gen_parameters) - - sys_parameters['single_earth_station']['frequency'] = sys.frequency - sys_parameters['single_earth_station']['bandwidth'] = sys.bandwidth - - sys_parameters['single_earth_station']['antenna']['gain'] = sys.antenna_gain - sys_parameters['single_earth_station']['antenna']['pattern'] = sys.antenna_pattern - sys_parameters['single_earth_station']['antenna']['itu_r_s_465']['diameter'] = sys.antenna_diameter - - sys_parameters['single_earth_station']['noise_temperature'] = sys.receive_temperature - - for eess_elev in [ - 5, 30, 60, 90 - ]: - parameters = deepcopy(sys_parameters) - - parameters['single_earth_station']['geometry']['elevation']['type'] = "FIXED" - parameters['single_earth_station']['geometry']['elevation']['fixed'] = eess_elev - - specific = f"{eess_elev}elev_{sys.name}" - parameters['general']['output_dir_prefix'] = output_prefix_pattern.replace( - "", specific) - - with open( - os.path.join(input_dir, f"./parameters_mss_d2d_to_eess_{specific}.yaml"), - 'w' - ) as file: - yaml.dump(parameters, file, default_flow_style=False) - - # also do uniform dist of elevation angles - parameters = deepcopy(sys_parameters) - parameters['single_earth_station']['geometry']['elevation']['type'] = "UNIFORM_DIST" - parameters['single_earth_station']['geometry']['elevation']['uniform_dist'] = { - 'min': 5, 'max': 90, } - - specific = f"uniform_elev_{sys.name}" - parameters['general']['output_dir_prefix'] = output_prefix_pattern.replace( - "", specific) - - with open( - os.path.join(input_dir, f"./parameters_mss_d2d_to_eess_{specific}.yaml"), - 'w' - ) as file: - yaml.dump(parameters, file, default_flow_style=False) diff --git a/sharc/campaigns/mss_d2d_to_eess/scripts/plot_results.py b/sharc/campaigns/mss_d2d_to_eess/scripts/plot_results.py deleted file mode 100644 index 97cfbdc99..000000000 --- a/sharc/campaigns/mss_d2d_to_eess/scripts/plot_results.py +++ /dev/null @@ -1,142 +0,0 @@ -import os -from pathlib import Path -from sharc.results import Results -from sharc.post_processor import PostProcessor - -auto_open = False - -local_dir = os.path.dirname(os.path.abspath(__file__)) -campaign_base_dir = str((Path(__file__) / ".." / "..").resolve()) - -post_processor = PostProcessor() - -# Samples to plot CCDF from -samples_for_ccdf = [ - "system_dl_interf_power_per_mhz" -] - -samples_for_cdf = [ - "imt_system_antenna_gain", - "imt_system_path_loss", - "system_dl_interf_power", - "system_imt_antenna_gain", - "system_inr", "ccdf" -] - -ccdf_results = Results.load_many_from_dir( - os.path.join( - campaign_base_dir, - "output"), - filter_fn=lambda x: "mss_d2d_to_eess" in x, - only_latest=True, - only_samples=samples_for_ccdf) - -cdf_results = Results.load_many_from_dir( - os.path.join( - campaign_base_dir, - "output"), - filter_fn=lambda x: "mss_d2d_to_eess" in x, - only_latest=True, - only_samples=samples_for_cdf) - -readable_name = { - "system_d": "System D", - "system_b": "System B", -} - - -def linestyle_getter(results): - """ - Determine the line style for plotting based on the results' output directory. - - Parameters - ---------- - results : Results - The results object containing the output directory information. - - Returns - ------- - str - The line style to use for plotting (e.g., 'dash' or 'solid'). - """ - if "system_d" in results.output_directory: - return "dash" - return "solid" - - -post_processor.add_results_linestyle_getter(linestyle_getter) - -for sys_name in ["system_d", "system_b"]: - for elev in [5, 30, 60, 90, "uniform_"]: - if elev == "uniform_": - readable_elev = "Elev = Unif. Dist." - else: - readable_elev = f"Elev = {elev}Āŗ" - # IMT-MSS-D2D-DL to EESS - post_processor\ - .add_plot_legend_pattern( - dir_name_contains=f"{elev}elev_{sys_name}", - legend=f"{readable_name[sys_name]}, {readable_elev}" - ) -# ^: typing.List[Results] - -plots = post_processor.generate_ccdf_plots_from_results( - ccdf_results -) - -post_processor.add_plots(plots) - -plots = post_processor.generate_cdf_plots_from_results( - cdf_results -) - -post_processor.add_plots(plots) - -# plots = post_processor.generate_ccdf_plots_from_results( -# many_results -# ) - -# post_processor.add_plots(plots) - -# Add a protection criteria line: -protection_criteria = -154.0 # dBm/MHz -perc_time = 0.01 -system_dl_interf_power_per_mhz = post_processor.get_plot_by_results_attribute_name( - "system_dl_interf_power_per_mhz", plot_type="ccdf") -system_dl_interf_power_per_mhz.add_vline( - protection_criteria, - line_dash="dash", - annotation=dict( - text="Protection Criteria: " + - str(protection_criteria) + - " dB[W/MHz]", - xref="x", - yref="y", - x=protection_criteria + - 0.5, - y=0.8, - font=dict( - size=12, - color="red"))) -system_dl_interf_power_per_mhz.add_hline(perc_time, line_dash="dash", annotation=dict( - text="Time Percentage: " + str(perc_time * 100) + "%", - xref="x", yref="y", - x=protection_criteria + 0.5, y=perc_time + 0.01, - font=dict(size=12, color="blue") -)) - - -attributes_to_plot = [ - ("imt_system_antenna_gain", "cdf"), - ("imt_system_path_loss", "cdf"), - ("system_dl_interf_power", "cdf"), - ("system_dl_interf_power_per_mhz", "ccdf"), - ("system_imt_antenna_gain", "cdf"), - ("system_inr", "cdf"), -] - -for attr, plot_type in attributes_to_plot: - file = os.path.join(campaign_base_dir, "output", f"{attr}.html") - post_processor\ - .get_plot_by_results_attribute_name(attr, plot_type=plot_type)\ - .write_html(file=file, include_plotlyjs="cdn", auto_open=auto_open) diff --git a/sharc/campaigns/mss_d2d_to_eess/scripts/start_simulations_multi_thread.py b/sharc/campaigns/mss_d2d_to_eess/scripts/start_simulations_multi_thread.py deleted file mode 100644 index 2864e427f..000000000 --- a/sharc/campaigns/mss_d2d_to_eess/scripts/start_simulations_multi_thread.py +++ /dev/null @@ -1,12 +0,0 @@ -from sharc.run_multiple_campaigns_mut_thread import run_campaign - -# Set the campaign name -# The name of the campaign to run. This should match the name of the -# campaign directory. -name_campaign = "mss_d2d_to_eess" - -# Run the campaigns -# This function will execute the campaign with the given name. -# It will look for the campaign directory under the specified name and -# start the necessary processes. -run_campaign(name_campaign) diff --git a/sharc/campaigns/mss_d2d_to_eess/scripts/start_simulations_single_thread.py b/sharc/campaigns/mss_d2d_to_eess/scripts/start_simulations_single_thread.py deleted file mode 100644 index 98e457d90..000000000 --- a/sharc/campaigns/mss_d2d_to_eess/scripts/start_simulations_single_thread.py +++ /dev/null @@ -1,12 +0,0 @@ -from sharc.run_multiple_campaigns import run_campaign_re - -# Set the campaign name -# The name of the campaign to run. This should match the name of the -# campaign directory. -name_campaign = "mss_d2d_to_eess" - -# Run the campaign in single-thread mode -# This function will execute the campaign with the given name in a single-threaded manner. -# It will look for the campaign directory under the specified name and -# start the necessary processes. -run_campaign_re(name_campaign, r'^parameters_mss_d2d_to_eess.yaml') diff --git a/sharc/campaigns/mss_d2d_to_imt/input/.gitignore b/sharc/campaigns/mss_d2d_to_imt/input/.gitignore deleted file mode 100644 index aaefe7107..000000000 --- a/sharc/campaigns/mss_d2d_to_imt/input/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -# This file is used to ignore files in the input directory of the MSS D2D to IMT campaign. -* -!.gitignore -!parameters_mss_d2d_to_imt_dl_co_channel_system_A.yaml -!parameters_mss_d2d_to_imt_ul_co_channel_system_A.yaml -!parameters_mss_d2d_to_imt_lat_variation_template.yaml diff --git a/sharc/campaigns/mss_d2d_to_imt/input/parameters_mss_d2d_to_imt_dl_co_channel_system_A.yaml b/sharc/campaigns/mss_d2d_to_imt/input/parameters_mss_d2d_to_imt_dl_co_channel_system_A.yaml deleted file mode 100644 index bcf2a9156..000000000 --- a/sharc/campaigns/mss_d2d_to_imt/input/parameters_mss_d2d_to_imt_dl_co_channel_system_A.yaml +++ /dev/null @@ -1,461 +0,0 @@ -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots: 10000 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link: DOWNLINK - ########################################################################### - # The chosen system for sharing study - # EESS_PASSIVE, FSS_SS, FSS_ES, FS, RAS, MSS_SS, MSS_D2D - system: MSS_D2D - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel: true - enable_adjacent_channel: false - ########################################################################### - # Seed for random number generator - seed: 67465 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output: FALSE - ########################################################################### - # output destination folder - this is relative SHARC/sharc directory - output_dir: campaigns/mss_d2d_to_imt/output/ - ########################################################################### - # output folder prefix - output_dir_prefix: output_mss_d2d_to_imt_dl_co_channel_system_A -imt: - ########################################################################### - # Minimum 2D separation distance from BS to UE [m] - minimum_separation_distance_bs_ue: 35 - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE: IMT suffers interference - # FALSE : IMT generates interference - interfered_with: TRUE - ########################################################################### - # IMT center frequency [MHz] - frequency: 2160.0 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth: 5.0 - ########################################################################### - # IMT resource block bandwidth [MHz] - rb_bandwidth: 0.180 - # IMT resource block bandwidth [MHz] - adjacent_ch_reception: ACS - ########################################################################### - # IMT spectrum emission mask. Options are: - # "IMT-2020" : for mmWave as described in ITU-R TG 5/1 Contribution 36 - # "3GPP E-UTRA" : for E-UTRA bands > 1 GHz as described in - # TS 36.104 v11.2.0 (BS) and TS 36.101 v11.2.0 (UE) - spectral_mask: 3GPP E-UTRA - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions: -13.0 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio: 0.1 - ########################################################################### - # Network topology parameters. - topology: - ########################################################################### - # The latitude position - central_latitude: -15.7801 # BrasĆ­lia - ########################################################################### - # The longitude position - central_longitude: -47.9292 # BrasĆ­lia - ########################## - central_altitude: 1200 - ########################################################################### - # Topology Type. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR" - type: SINGLE_BS - ########################################################################### - # Single Base Station Topology parameters. Relevant when imt.topology.type == "SINGLE_BS" - single_bs: - ########################################################################### - # Inter-site distance or Cell Radius in single Base Station network topology [m] - # You can either provide 'cell_radius' or 'intersite_distance' for this topology - # The relationship used is cell_radius = intersite_distance * 2 / 3 - cell_radius: 250 - # intersite_distance: 1 - ########################################################################### - # Number of clusters in single base station topology - # You can simulate 1 or 2 BS's with this topology - num_clusters: 1 - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model: SINGLE_ELEMENT - # Base station parameters - From 5D/716 NON-AAS Antenna - Urban - bs: - ########################################################################### - # The load probability (or activity factor) models the statistical - # variation of the network load by defining the number of fully loaded - # base stations that are simultaneously transmitting - load_probability: 1.0 - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power: 46.0 - ########################################################################### - # Base station height [m] - height: 20.0 - ########################################################################### - # Base station noise figure [dB] - noise_figure: 5.0 - ########################################################################### - # Base station array ohmic loss [dB] - ohmic_loss: 3.0 - # Base Station Antenna parameters: - antenna: - array: - ########################################################################### - # If normalization of M2101 should be applied for BS - normalization: FALSE - ########################################################################### - # Base station horizontal beamsteering range. [deg] - # The range considers the BS azimuth as 0deg - horizontal_beamsteering_range: !!python/tuple [-60., 60.] - ########################################################################### - # Base station horizontal beamsteering range. [deg] - # The range considers the horizon as 90deg, 0 as zenith - vertical_beamsteering_range: !!python/tuple [90., 100.] - ########################################################################### - # File to be used in the BS beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/bs_norm.npz - ########################################################################### - # File to be used in the UE beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: F1336 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor simulations - downtilt: 10 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g: 16 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 65 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 0 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 1 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 1 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 2.1 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 30 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - - ########################################################################### - # User Equipment parameters: - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k: 1 # Only one UE to access the worst-case interference from MSS-DC - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m: 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent: 0.0 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type: UNIFORM_IN_CELL - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance: RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth: UNIFORM - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control: ON - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch: -92.2 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha: 0.8 - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax: 23 - ########################################################################### - # UE power dynamic range [dB] - # The minimum transmit power of a UE is (p_cmax - dynamic_range) - power_dynamic_range: 63 - ########################################################################### - # UE height [m] - height: 1.5 - ########################################################################### - # User equipment noise figure [dB] - noise_figure: 9 - ########################################################################### - # User equipment feed loss [dB] - ohmic_loss: 0 - ########################################################################### - # User equipment body loss [dB] - body_loss: 4 - antenna: - array: - ########################################################################### - # If normalization of M2101 should be applied for UE - normalization: FALSE - ########################################################################### - # File to be used in the UE beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/ue_norm.npz - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: FIXED - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g: -3 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 90 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 90 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 1 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 1 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 25 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - ########################################################################### - # Uplink parameters. Only needed when using uplink on imt - uplink: - ########################################################################### - # Uplink attenuation factor used in link-to-system mapping - attenuation_factor: 0.4 - ########################################################################### - # Uplink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Uplink maximum SINR of the code set [dB] - sinr_max: 22 - ########################################################################### - # Downlink parameters. Only needed when using donwlink on imt - downlink: - ########################################################################### - # Downlink attenuation factor used in link-to-system mapping - attenuation_factor: 0.8 - ########################################################################### - # Downlink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Downlink maximum SINR of the code set [dB] - sinr_max: 30 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "CI" (close-in FS reference distance) - # "UMa" (Urban Macro - 3GPP) - # "UMi" (Urban Micro - 3GPP) - # "TVRO-URBAN" - # "TVRO-SUBURBAN" - # "ABG" (Alpha-Beta-Gamma) - # "P619" - channel_model: UMa - season: SUMMER - ########################################################################### - # Adjustment factor for LoS probability in UMi path loss model. - # Original value: 18 (3GPP) - los_adjustment_factor: 18 - ########################################################################### - # If shadowing should be applied or not. - # Used in propagation UMi, UMa, ABG, when topology == indoor and any - shadowing: FALSE - ########################################################################### - # receiver noise temperature [K] - noise_temperature: 290 - ########################################################################### - # P619 parameters - param_p619: - ########################################################################### - # altitude of the space station [m] - space_station_alt_m: 540000 - ########################################################################### - # altitude of ES system [m] - earth_station_alt_m: 1200 - ########################################################################### - # latitude of ES system [m] - earth_station_lat_deg: 13 - ########################################################################### - # difference between longitudes of IMT-NTN station and FSS-ES system [deg] - # (positive if space-station is to the East of earth-station) - earth_station_long_diff_deg: 0 -#### System Parameters -mss_d2d: - # MSS_D2D system name - name: SystemA - # Adjacent channel emissions type - # Possible values are "ACLR", "SPECTRAL_MASK" and "OFF" - adjacent_ch_emissions: SPECTRAL_MASK - # chosen spectral Mask - spectral_mask: 3GPP E-UTRA - # MSS_D2D system center frequency in MHz - frequency: 2160.0 - # MSS_D2d system bandwidth in MHz - bandwidth: 5.0 - # MSS_D2D cell radius in network topology [m] - cell_radius: 39475.0 - # Satellite power density in dBW/Hz - tx_power_density: -54.2 - # Number of sectors - num_sectors: 1 - # conditional to select active satellites - # in simulation we sample uniformly from time - # if no active satellites are active on that time - # we resample - # only active satellites will contribute to interference - sat_is_active_if: - # for a satellite to be active, it needs to respect ALL conditions - conditions: - # - LAT_LONG_INSIDE_COUNTRY - - MINIMUM_ELEVATION_FROM_ES - minimum_elevation_from_es: 5 - # lat_long_inside_country: - # # You may specify another shp file for country borders reference - # # country_shapes_filename: sharc/topology/countries/ne_110m_admin_0_countries.shp - # country_name: BRAZIL - # # margin from inside of border [km] - # # if positive, makes border smaller by x km - # # if negative, makes border bigger by x km - # margin_from_border: 0 - # Satellite antenna pattern - # Antenna pattern from ITU-R S.1528 - # Possible values: "ITU-R-S.1528-Section1.2", "ITU-R-S.1528-LEO" - antenna_pattern: ITU-R-S.1528-Taylor - antenna_s1528: - ### The following parameters are used for S.1528-Taylor antenna pattern - # Maximum Antenna gain in dBi - antenna_gain: 34.1 - # SLR is the side-lobe ratio of the pattern (dB), the difference in gain between the maximum - # gain and the gain at the peak of the first side lobe. - slr: 20.0 - # Number of secondary lobes considered in the diagram (coincide with the roots of the Bessel function) - n_side_lobes: 2 - # Radial (l_r) and transverse (l_t) sizes of the effective radiating area of the satellite transmitt antenna (m) - l_r: 1.6 - l_t: 1.6 - polarization_loss: 0.0 # dB - # channel model, possible values are "FSPL" (free-space path loss), - # "SatelliteSimple" (FSPL + 4 + clutter loss) - # "P619" - channel_model: FSPL - ########################################################################### - # P619 parameters - param_p619: - ########################################################################### - # altitude of ES system [m] - earth_station_alt_m: 1200 - ########################################################################### - # latitude of ES system [m] - earth_station_lat_deg: -15.7801 - ########################################################################### - # difference between longitudes of IMT-NTN station and FSS-ES system [deg] - # (positive if space-station is to the East of earth-station) - earth_station_long_diff_deg: 0.0 - ########################################################################### - # year season: SUMMER of WINTER - season: SUMMER - # Orbit parameters - orbits: - # Number of planes - - n_planes: 28 - # Inclination in degrees - inclination_deg: 53.0 - # Perigee in km - perigee_alt_km: 525.0 - # Apogee in km - apogee_alt_km: 525.0 - # Number of satellites per plane - sats_per_plane: 120 - # Longitude of the first ascending node in degrees - long_asc_deg: 0.0 - # Phasing in degrees - phasing_deg: 1.5 diff --git a/sharc/campaigns/mss_d2d_to_imt/input/parameters_mss_d2d_to_imt_lat_variation_template.yaml b/sharc/campaigns/mss_d2d_to_imt/input/parameters_mss_d2d_to_imt_lat_variation_template.yaml deleted file mode 100644 index 7ff6776a0..000000000 --- a/sharc/campaigns/mss_d2d_to_imt/input/parameters_mss_d2d_to_imt_lat_variation_template.yaml +++ /dev/null @@ -1,461 +0,0 @@ -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots: 10000 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link: DOWNLINK - ########################################################################### - # The chosen system for sharing study - # EESS_PASSIVE, FSS_SS, FSS_ES, FS, RAS, MSS_SS, MSS_D2D - system: MSS_D2D - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel: true - enable_adjacent_channel: false - ########################################################################### - # Seed for random number generator - seed: 67463 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output: FALSE - ########################################################################### - # output destination folder - this is relative SHARC/sharc directory - output_dir: campaigns/mss_d2d_to_imt/output/ - ########################################################################### - # output folder prefix - output_dir_prefix: output_mss_d2d_to_imt_dl_lat_variation -imt: - ########################################################################### - # Minimum 2D separation distance from BS to UE [m] - minimum_separation_distance_bs_ue: 35 - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE: IMT suffers interference - # FALSE : IMT generates interference - interfered_with: TRUE - ########################################################################### - # IMT center frequency [MHz] - frequency: 2160.0 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth: 5.0 - ########################################################################### - # IMT resource block bandwidth [MHz] - rb_bandwidth: 0.180 - # IMT resource block bandwidth [MHz] - adjacent_ch_reception: ACS - ########################################################################### - # IMT spectrum emission mask. Options are: - # "IMT-2020" : for mmWave as described in ITU-R TG 5/1 Contribution 36 - # "3GPP E-UTRA" : for E-UTRA bands > 1 GHz as described in - # TS 36.104 v11.2.0 (BS) and TS 36.101 v11.2.0 (UE) - spectral_mask: 3GPP E-UTRA - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions: -13.0 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio: 0.1 - ########################################################################### - # Network topology parameters. - topology: - ########################################################################### - # The latitude position - central_latitude: -15.7801 # BrasĆ­lia - ########################################################################### - # The longitude position - central_longitude: -47.9292 # BrasĆ­lia - ########################## - central_altitude: 1200 - ########################################################################### - # Topology Type. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR" - type: SINGLE_BS - ########################################################################### - # Single Base Station Topology parameters. Relevant when imt.topology.type == "SINGLE_BS" - single_bs: - ########################################################################### - # Inter-site distance or Cell Radius in single Base Station network topology [m] - # You can either provide 'cell_radius' or 'intersite_distance' for this topology - # The relationship used is cell_radius = intersite_distance * 2 / 3 - cell_radius: 250 - # intersite_distance: 1 - ########################################################################### - # Number of clusters in single base station topology - # You can simulate 1 or 2 BS's with this topology - num_clusters: 1 - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model: SINGLE_ELEMENT - # Base station parameters - From 5D/716 NON-AAS Antenna - Urban - bs: - ########################################################################### - # The load probability (or activity factor) models the statistical - # variation of the network load by defining the number of fully loaded - # base stations that are simultaneously transmitting - load_probability: 1.0 - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power: 46.0 - ########################################################################### - # Base station height [m] - height: 20.0 - ########################################################################### - # Base station noise figure [dB] - noise_figure: 5.0 - ########################################################################### - # Base station array ohmic loss [dB] - ohmic_loss: 3.0 - # Base Station Antenna parameters: - antenna: - array: - ########################################################################### - # If normalization of M2101 should be applied for BS - normalization: FALSE - ########################################################################### - # Base station horizontal beamsteering range. [deg] - # The range considers the BS azimuth as 0deg - horizontal_beamsteering_range: !!python/tuple [-60., 60.] - ########################################################################### - # Base station horizontal beamsteering range. [deg] - # The range considers the horizon as 90deg, 0 as zenith - vertical_beamsteering_range: !!python/tuple [90., 100.] - ########################################################################### - # File to be used in the BS beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/bs_norm.npz - ########################################################################### - # File to be used in the UE beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: F1336 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor simulations - downtilt: 10 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g: 16 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 65 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 0 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 4 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 8 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 2.1 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 30 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - - ########################################################################### - # User Equipment parameters: - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k: 1 # Only one UE to access the worst-case interference from MSS-DC - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m: 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent: 0.0 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type: UNIFORM_IN_CELL - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance: RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth: UNIFORM - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control: ON - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch: -92.2 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha: 0.8 - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax: 23 - ########################################################################### - # UE power dynamic range [dB] - # The minimum transmit power of a UE is (p_cmax - dynamic_range) - power_dynamic_range: 63 - ########################################################################### - # UE height [m] - height: 1.5 - ########################################################################### - # User equipment noise figure [dB] - noise_figure: 9 - ########################################################################### - # User equipment feed loss [dB] - ohmic_loss: 0 - ########################################################################### - # User equipment body loss [dB] - body_loss: 4 - antenna: - array: - ########################################################################### - # If normalization of M2101 should be applied for UE - normalization: FALSE - ########################################################################### - # File to be used in the UE beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/ue_norm.npz - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: FIXED - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g: -3 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 90 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 90 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 1 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 1 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 25 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - ########################################################################### - # Uplink parameters. Only needed when using uplink on imt - uplink: - ########################################################################### - # Uplink attenuation factor used in link-to-system mapping - attenuation_factor: 0.4 - ########################################################################### - # Uplink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Uplink maximum SINR of the code set [dB] - sinr_max: 22 - ########################################################################### - # Downlink parameters. Only needed when using donwlink on imt - downlink: - ########################################################################### - # Downlink attenuation factor used in link-to-system mapping - attenuation_factor: 0.8 - ########################################################################### - # Downlink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Downlink maximum SINR of the code set [dB] - sinr_max: 30 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "CI" (close-in FS reference distance) - # "UMa" (Urban Macro - 3GPP) - # "UMi" (Urban Micro - 3GPP) - # "TVRO-URBAN" - # "TVRO-SUBURBAN" - # "ABG" (Alpha-Beta-Gamma) - # "P619" - channel_model: UMa - season: SUMMER - ########################################################################### - # Adjustment factor for LoS probability in UMi path loss model. - # Original value: 18 (3GPP) - los_adjustment_factor: 18 - ########################################################################### - # If shadowing should be applied or not. - # Used in propagation UMi, UMa, ABG, when topology == indoor and any - shadowing: FALSE - ########################################################################### - # receiver noise temperature [K] - noise_temperature: 290 - ########################################################################### - # P619 parameters - param_p619: - ########################################################################### - # altitude of the space station [m] - space_station_alt_m: 540000 - ########################################################################### - # altitude of ES system [m] - earth_station_alt_m: 1200 - ########################################################################### - # latitude of ES system [m] - earth_station_lat_deg: 13 - ########################################################################### - # difference between longitudes of IMT-NTN station and FSS-ES system [deg] - # (positive if space-station is to the East of earth-station) - earth_station_long_diff_deg: 0 -#### System Parameters -mss_d2d: - # MSS_D2D system name - name: SystemA - # Adjacent channel emissions type - # Possible values are "ACLR", "SPECTRAL_MASK" and "OFF" - adjacent_ch_emissions: SPECTRAL_MASK - # chosen spectral Mask - spectral_mask: 3GPP E-UTRA - # MSS_D2D system center frequency in MHz - frequency: 2160.0 - # MSS_D2d system bandwidth in MHz - bandwidth: 5.0 - # MSS_D2D cell radius in network topology [m] - cell_radius: 39475.0 - # Satellite power density in dBW/Hz - tx_power_density: -54.2 - # Number of sectors - num_sectors: 1 - # conditional to select active satellites - # in simulation we sample uniformly from time - # if no active satellites are active on that time - # we resample - # only active satellites will contribute to interference - sat_is_active_if: - # for a satellite to be active, it needs to respect ALL conditions - conditions: - # - LAT_LONG_INSIDE_COUNTRY - - MINIMUM_ELEVATION_FROM_ES - minimum_elevation_from_es: 5 - # lat_long_inside_country: - # # You may specify another shp file for country borders reference - # # country_shapes_filename: sharc/topology/countries/ne_110m_admin_0_countries.shp - # country_name: BRAZIL - # # margin from inside of border [km] - # # if positive, makes border smaller by x km - # # if negative, makes border bigger by x km - # margin_from_border: 0 - # Satellite antenna pattern - # Antenna pattern from ITU-R S.1528 - # Possible values: "ITU-R-S.1528-Section1.2", "ITU-R-S.1528-LEO" - antenna_pattern: ITU-R-S.1528-Taylor - antenna_s1528: - ### The following parameters are used for S.1528-Taylor antenna pattern - # Maximum Antenna gain in dBi - antenna_gain: 34.1 - # SLR is the side-lobe ratio of the pattern (dB), the difference in gain between the maximum - # gain and the gain at the peak of the first side lobe. - slr: 20.0 - # Number of secondary lobes considered in the diagram (coincide with the roots of the Bessel function) - n_side_lobes: 2 - # Radial (l_r) and transverse (l_t) sizes of the effective radiating area of the satellite transmitt antenna (m) - l_r: 1.6 - l_t: 1.6 - polarization_loss: 0.0 # dB - # channel model, possible values are "FSPL" (free-space path loss), - # "SatelliteSimple" (FSPL + 4 + clutter loss) - # "P619" - channel_model: FSPL - ########################################################################### - # P619 parameters - param_p619: - ########################################################################### - # altitude of ES system [m] - earth_station_alt_m: 1200 - ########################################################################### - # latitude of ES system [m] - earth_station_lat_deg: -15.7801 - ########################################################################### - # difference between longitudes of IMT-NTN station and FSS-ES system [deg] - # (positive if space-station is to the East of earth-station) - earth_station_long_diff_deg: 0.0 - ########################################################################### - # year season: SUMMER of WINTER - season: SUMMER - # Orbit parameters - orbits: - # Number of planes - - n_planes: 28 - # Inclination in degrees - inclination_deg: 53.0 - # Perigee in km - perigee_alt_km: 525.0 - # Apogee in km - apogee_alt_km: 525.0 - # Number of satellites per plane - sats_per_plane: 120 - # Longitude of the first ascending node in degrees - long_asc_deg: 0.0 - # Phasing in degrees - phasing_deg: 1.5 diff --git a/sharc/campaigns/mss_d2d_to_imt/input/parameters_mss_d2d_to_imt_ul_co_channel_system_A.yaml b/sharc/campaigns/mss_d2d_to_imt/input/parameters_mss_d2d_to_imt_ul_co_channel_system_A.yaml deleted file mode 100644 index 8950bc7c0..000000000 --- a/sharc/campaigns/mss_d2d_to_imt/input/parameters_mss_d2d_to_imt_ul_co_channel_system_A.yaml +++ /dev/null @@ -1,461 +0,0 @@ -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots: 10000 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link: UPLINK - ########################################################################### - # The chosen system for sharing study - # EESS_PASSIVE, FSS_SS, FSS_ES, FS, RAS, MSS_SS, MSS_D2D - system: MSS_D2D - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel: true - enable_adjacent_channel: false - ########################################################################### - # Seed for random number generator - seed: 67464 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output: FALSE - ########################################################################### - # output destination folder - this is relative SHARC/sharc directory - output_dir: campaigns/mss_d2d_to_imt/output/ - ########################################################################### - # output folder prefix - output_dir_prefix: output_mss_d2d_to_imt_ul_co_channel_system_A -imt: - ########################################################################### - # Minimum 2D separation distance from BS to UE [m] - minimum_separation_distance_bs_ue: 35 - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE: IMT suffers interference - # FALSE : IMT generates interference - interfered_with: TRUE - ########################################################################### - # IMT center frequency [MHz] - frequency: 2160.0 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth: 20.0 - ########################################################################### - # IMT resource block bandwidth [MHz] - rb_bandwidth: 0.180 - # IMT resource block bandwidth [MHz] - adjacent_ch_reception: ACS - ########################################################################### - # IMT spectrum emission mask. Options are: - # "IMT-2020" : for mmWave as described in ITU-R TG 5/1 Contribution 36 - # "3GPP E-UTRA" : for E-UTRA bands > 1 GHz as described in - # TS 36.104 v11.2.0 (BS) and TS 36.101 v11.2.0 (UE) - spectral_mask: 3GPP E-UTRA - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions: -13.0 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio: 0.1 - ########################################################################### - # Network topology parameters. - topology: - ########################################################################### - # The latitude position - central_latitude: -15.7801 # BrasĆ­lia - ########################################################################### - # The longitude position - central_longitude: -47.9292 # BrasĆ­lia - ########################## - central_altitude: 1200 - ########################################################################### - # Topology Type. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR" - type: SINGLE_BS - ########################################################################### - # Single Base Station Topology parameters. Relevant when imt.topology.type == "SINGLE_BS" - single_bs: - ########################################################################### - # Inter-site distance or Cell Radius in single Base Station network topology [m] - # You can either provide 'cell_radius' or 'intersite_distance' for this topology - # The relationship used is cell_radius = intersite_distance * 2 / 3 - cell_radius: 250 - # intersite_distance: 1 - ########################################################################### - # Number of clusters in single base station topology - # You can simulate 1 or 2 BS's with this topology - num_clusters: 1 - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model: SINGLE_ELEMENT - # Base station parameters - From 5D/716 NON-AAS Antenna - Urban - bs: - ########################################################################### - # The load probability (or activity factor) models the statistical - # variation of the network load by defining the number of fully loaded - # base stations that are simultaneously transmitting - load_probability: 1.0 - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power: 46.0 - ########################################################################### - # Base station height [m] - height: 20.0 - ########################################################################### - # Base station noise figure [dB] - noise_figure: 5.0 - ########################################################################### - # Base station array ohmic loss [dB] - ohmic_loss: 3.0 - # Base Station Antenna parameters: - antenna: - array: - ########################################################################### - # If normalization of M2101 should be applied for BS - normalization: FALSE - ########################################################################### - # Base station horizontal beamsteering range. [deg] - # The range considers the BS azimuth as 0deg - horizontal_beamsteering_range: !!python/tuple [-60., 60.] - ########################################################################### - # Base station horizontal beamsteering range. [deg] - # The range considers the horizon as 90deg, 0 as zenith - vertical_beamsteering_range: !!python/tuple [90., 100.] - ########################################################################### - # File to be used in the BS beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/bs_norm.npz - ########################################################################### - # File to be used in the UE beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: F1336 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor simulations - downtilt: 10 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g: 16 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 65 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 0 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 4 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 8 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 2.1 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 30 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - - ########################################################################### - # User Equipment parameters: - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k: 1 # Only one UE to access the worst-case interference from MSS-DC - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m: 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent: 0.0 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type: UNIFORM_IN_CELL - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance: RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth: UNIFORM - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control: ON - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch: -92.2 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha: 0.8 - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax: 23 - ########################################################################### - # UE power dynamic range [dB] - # The minimum transmit power of a UE is (p_cmax - dynamic_range) - power_dynamic_range: 63 - ########################################################################### - # UE height [m] - height: 1.5 - ########################################################################### - # User equipment noise figure [dB] - noise_figure: 9 - ########################################################################### - # User equipment feed loss [dB] - ohmic_loss: 0 - ########################################################################### - # User equipment body loss [dB] - body_loss: 4 - antenna: - array: - ########################################################################### - # If normalization of M2101 should be applied for UE - normalization: FALSE - ########################################################################### - # File to be used in the UE beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/ue_norm.npz - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: FIXED - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g: -3 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 90 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 90 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 1 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 1 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 25 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - ########################################################################### - # Uplink parameters. Only needed when using uplink on imt - uplink: - ########################################################################### - # Uplink attenuation factor used in link-to-system mapping - attenuation_factor: 0.4 - ########################################################################### - # Uplink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Uplink maximum SINR of the code set [dB] - sinr_max: 22 - ########################################################################### - # Downlink parameters. Only needed when using donwlink on imt - downlink: - ########################################################################### - # Downlink attenuation factor used in link-to-system mapping - attenuation_factor: 0.8 - ########################################################################### - # Downlink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Downlink maximum SINR of the code set [dB] - sinr_max: 30 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "CI" (close-in FS reference distance) - # "UMa" (Urban Macro - 3GPP) - # "UMi" (Urban Micro - 3GPP) - # "TVRO-URBAN" - # "TVRO-SUBURBAN" - # "ABG" (Alpha-Beta-Gamma) - # "P619" - channel_model: UMa - season: SUMMER - ########################################################################### - # Adjustment factor for LoS probability in UMi path loss model. - # Original value: 18 (3GPP) - los_adjustment_factor: 18 - ########################################################################### - # If shadowing should be applied or not. - # Used in propagation UMi, UMa, ABG, when topology == indoor and any - shadowing: FALSE - ########################################################################### - # receiver noise temperature [K] - noise_temperature: 290 - ########################################################################### - # P619 parameters - param_p619: - ########################################################################### - # altitude of the space station [m] - space_station_alt_m: 540000 - ########################################################################### - # altitude of ES system [m] - earth_station_alt_m: 1200 - ########################################################################### - # latitude of ES system [m] - earth_station_lat_deg: 13 - ########################################################################### - # difference between longitudes of IMT-NTN station and FSS-ES system [deg] - # (positive if space-station is to the East of earth-station) - earth_station_long_diff_deg: 0 -#### System Parameters -mss_d2d: - # MSS_D2D system name - name: SystemA - # Adjacent channel emissions type - # Possible values are "ACLR", "SPECTRAL_MASK" and "OFF" - adjacent_ch_emissions: SPECTRAL_MASK - # chosen spectral Mask - spectral_mask: 3GPP E-UTRA - # MSS_D2D system center frequency in MHz - frequency: 2160.0 - # MSS_D2d system bandwidth in MHz - bandwidth: 5.0 - # MSS_D2D cell radius in network topology [m] - cell_radius: 39475.0 - # Satellite power density in dBW/Hz - tx_power_density: -54.2 - # Number of sectors - num_sectors: 1 - # conditional to select active satellites - # in simulation we sample uniformly from time - # if no active satellites are active on that time - # we resample - # only active satellites will contribute to interference - sat_is_active_if: - # for a satellite to be active, it needs to respect ALL conditions - conditions: - # - LAT_LONG_INSIDE_COUNTRY - - MINIMUM_ELEVATION_FROM_ES - minimum_elevation_from_es: 5 - # lat_long_inside_country: - # # You may specify another shp file for country borders reference - # # country_shapes_filename: sharc/topology/countries/ne_110m_admin_0_countries.shp - # country_name: BRAZIL - # # margin from inside of border [km] - # # if positive, makes border smaller by x km - # # if negative, makes border bigger by x km - # margin_from_border: 0 - # Satellite antenna pattern - # Antenna pattern from ITU-R S.1528 - # Possible values: "ITU-R-S.1528-Section1.2", "ITU-R-S.1528-LEO" - antenna_pattern: ITU-R-S.1528-Taylor - antenna_s1528: - ### The following parameters are used for S.1528-Taylor antenna pattern - # Maximum Antenna gain in dBi - antenna_gain: 34.1 - # SLR is the side-lobe ratio of the pattern (dB), the difference in gain between the maximum - # gain and the gain at the peak of the first side lobe. - slr: 20.0 - # Number of secondary lobes considered in the diagram (coincide with the roots of the Bessel function) - n_side_lobes: 2 - # Radial (l_r) and transverse (l_t) sizes of the effective radiating area of the satellite transmitt antenna (m) - l_r: 1.6 - l_t: 1.6 - polarization_loss: 0.0 # dB - # channel model, possible values are "FSPL" (free-space path loss), - # "SatelliteSimple" (FSPL + 4 + clutter loss) - # "P619" - channel_model: FSPL - ########################################################################### - # P619 parameters - param_p619: - ########################################################################### - # altitude of ES system [m] - earth_station_alt_m: 1200 - ########################################################################### - # latitude of ES system [m] - earth_station_lat_deg: -15.7801 - ########################################################################### - # difference between longitudes of IMT-NTN station and FSS-ES system [deg] - # (positive if space-station is to the East of earth-station) - earth_station_long_diff_deg: 0.0 - ########################################################################### - # year season: SUMMER of WINTER - season: SUMMER - # Orbit parameters - orbits: - # Number of planes - - n_planes: 28 - # Inclination in degrees - inclination_deg: 53.0 - # Perigee in km - perigee_alt_km: 525.0 - # Apogee in km - apogee_alt_km: 525.0 - # Number of satellites per plane - sats_per_plane: 120 - # Longitude of the first ascending node in degrees - long_asc_deg: 0.0 - # Phasing in degrees - phasing_deg: 1.5 diff --git a/sharc/campaigns/mss_d2d_to_imt/readme.md b/sharc/campaigns/mss_d2d_to_imt/readme.md deleted file mode 100644 index 124aa6e42..000000000 --- a/sharc/campaigns/mss_d2d_to_imt/readme.md +++ /dev/null @@ -1,26 +0,0 @@ -# Introduction - -This campaign simulates a MSS-DC constellation and computes the interference of it into a IMT station positioned -on Earth surface. -The MSS-DC orbit elements are given as parameteres as well as the IMT coordinates. - -The selection of interferer satellites is done as follows: -- At each drop a random intant of the satellites is generated -- Those satellies that are visible from the IMT terrestrial station are set as "active". -- The visibily constraint is set as the satellites with elevation angles equal or greater that 5 deg (from the IMT -station horizon) - -# Running the simulation - -Inside the `./scripts` folder there are two scritps for executing the simulation: - -### MSS-DC System A campaign -`start_simulation_system_A.py` runs the scenario where the UE station is fixed in BrasĆ­lia. - -### MSS-DC Varying UE latitude -`start_simulations_lat_variation.py` runs a batch of simulations where the UE longitude is varied over the 0 deg meridian. This is to access the impact of longitude values into the total interference. - -Before running the `start_simulations_lat_variation.py` campaing the parameters for the simulation must be generated by runing the `parameter_gen.py` script. - -# Plotting the results -Run `plot_resutls.py`for the "simulation system A" campaign and `plot_resutls_lat_variation.py` for the "Varying UE latitude" campaign. \ No newline at end of file diff --git a/sharc/campaigns/mss_d2d_to_imt/scripts/orbit_model_anaylisis.py b/sharc/campaigns/mss_d2d_to_imt/scripts/orbit_model_anaylisis.py deleted file mode 100644 index 38c6e504c..000000000 --- a/sharc/campaigns/mss_d2d_to_imt/scripts/orbit_model_anaylisis.py +++ /dev/null @@ -1,145 +0,0 @@ -# Description: This script is used to analyze the orbit model of an NGSO constellation. -# The script calculates the number of visible satellites from a ground station and the elevation angles of the satellites. -# The script uses the OrbitModel class from the -# sharc.satellite.ngso.orbit_model module to calculate the satellite -# positions. -import numpy as np -from pathlib import Path -import plotly.graph_objects as go - -from sharc.parameters.parameters_mss_d2d import ParametersMssD2d -from sharc.satellite.ngso.orbit_model import OrbitModel -from sharc.satellite.utils.sat_utils import calc_elevation - - -if __name__ == "__main__": - - # Input parameters - local_dir = Path(__file__).parent.resolve() - param_file = local_dir / ".." / "input" / \ - "parameters_mss_d2d_to_imt_dl_co_channel_system_A.yaml" - params = ParametersMssD2d() - params.load_parameters_from_file(param_file) - orbit_params = params.orbits[0] - print("Orbit parameters:") - print(orbit_params) - - # Ground station location - GROUND_STA_LAT = -15.7801 - GROUND_STA_LON = -42.9292 - MIN_ELEV_ANGLE_DEG = 5.0 # minimum elevation angle for visibility - - # Time duration in days for the linear time simulation - TIME_DURATION_HOURS = 72 - - # Random samples - N_DROPS = 50000 - - # Random seed for reproducibility - SEED = 6 - - # Instantiate the OrbitModel - orbit = OrbitModel( - Nsp=orbit_params.sats_per_plane, - Np=orbit_params.n_planes, - phasing=orbit_params.phasing_deg, - long_asc=orbit_params.long_asc_deg, - omega=orbit_params.omega_deg, - delta=orbit_params.inclination_deg, - hp=orbit_params.perigee_alt_km, - ha=orbit_params.apogee_alt_km, - Mo=orbit_params.initial_mean_anomaly, - model_time_as_random_variable=False, - t_min=0.0, - t_max=None - ) - - # Show visible satellites from ground-station - total_periods = int(TIME_DURATION_HOURS * 3600 / orbit.orbital_period_sec) - print(f"Total periods: {total_periods}") - pos_vec = orbit.get_satellite_positions_time_interval( - initial_time_secs=0, interval_secs=10, n_periods=total_periods) - # altitude of the satellites in kilometers - sat_altitude_km = orbit.apogee_alt_km - num_of_visible_sats_per_drop = [] - elev_angles = calc_elevation( - GROUND_STA_LAT, - pos_vec['lat'], - GROUND_STA_LON, - pos_vec['lon'], - sat_height=sat_altitude_km * 1e3, - es_height=0) - elevation_angles_per_drop = elev_angles[np.where( - np.array(elev_angles) > MIN_ELEV_ANGLE_DEG)] - num_of_visible_sats_per_drop = np.sum( - np.array(elev_angles) > MIN_ELEV_ANGLE_DEG, axis=0) - - # Show visible satellites from ground-station - random - num_of_visible_sats_per_drop_rand = [] - elevation_angles_per_drop_rand = [] - rng = np.random.RandomState(seed=SEED) - pos_vec = orbit.get_orbit_positions_random(rng=rng, n_samples=N_DROPS) - elev_angles = calc_elevation(GROUND_STA_LAT, - pos_vec['lat'], - GROUND_STA_LON, pos_vec['lon'], - sat_height=sat_altitude_km * 1e3, - es_height=0) - elevation_angles_per_drop_rand = elev_angles[np.where( - np.array(elev_angles) > MIN_ELEV_ANGLE_DEG)] - num_of_visible_sats_per_drop_rand = np.sum( - np.array(elev_angles) > MIN_ELEV_ANGLE_DEG, axis=0) - - # Free some memory - del elev_angles - del pos_vec - - # plot histogram of visible satellites - fig = go.Figure( - data=[ - go.Histogram( - x=num_of_visible_sats_per_drop, - histnorm='probability', - xbins=dict( - start=-0.5, - size=1))]) - fig.update_layout( - title_text='Visible satellites per drop', - xaxis_title_text='Num of visible satellites', - yaxis_title_text='Probability', - bargap=0.2, - bargroupgap=0.1, - xaxis=dict( - tickmode='linear', - tick0=0, - dtick=1 - ) - ) - - fig.add_trace(go.Histogram(x=num_of_visible_sats_per_drop_rand, - histnorm='probability', - xbins=dict(start=-0.5, size=1))) - - fig.data[0].name = 'Linear time' - fig.data[1].name = 'Random' - fig.update_layout(legend_title_text='Observation Type') - file_name = Path(__file__).parent / "visible_sats_per_drop.html" - fig.write_html(file=file_name, include_plotlyjs="cdn", auto_open=False) - - # plot histogram of elevation angles - fig = go.Figure( - data=[ - go.Histogram( - x=np.array(elevation_angles_per_drop).flatten(), - histnorm='probability', - xbins=dict( - start=0, - size=5))]) - fig.update_layout( - title_text='Elevation angles', - xaxis_title_text='Elevation angle [deg]', - yaxis_title_text='Probability', - bargap=0.2, - bargroupgap=0.1 - ) - file_name = Path(__file__).parent / "elevation_angles.html" - fig.write_html(file=file_name, include_plotlyjs="cdn", auto_open=False) diff --git a/sharc/campaigns/mss_d2d_to_imt/scripts/parameter_gen_lat_variation.py b/sharc/campaigns/mss_d2d_to_imt/scripts/parameter_gen_lat_variation.py deleted file mode 100644 index 1a6b2f744..000000000 --- a/sharc/campaigns/mss_d2d_to_imt/scripts/parameter_gen_lat_variation.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Generates the parameters for the MSS D2D to IMT with varying latitude campaign. -""" -import numpy as np -import yaml -import os - -from sharc.parameters.parameters_base import tuple_constructor - -yaml.SafeLoader.add_constructor( - 'tag:yaml.org,2002:python/tuple', - tuple_constructor) - -local_dir = os.path.dirname(os.path.abspath(__file__)) -parameter_file_name = os.path.join( - local_dir, "../input/parameters_mss_d2d_to_imt_lat_variation_template.yaml") - -# load the base parameters from the yaml file -with open(parameter_file_name, 'r') as file: - parameters = yaml.safe_load(file) - -# The scenario is to move the Earth station postion throught the Meridian -# from 0 to 60 degrees latitude. -parameters['imt']['topology']['central_longitude'] = 0.0 -lats = np.arange(0, 70, 10) -for direction in [('dl', 'DOWNLINK'), ('ul', 'UPLINK')]: - for lat in lats: - # Create a copy of the base parameters - params = parameters.copy() - - # set the link direction - params['general']['imt_link'] = direction[1] - - # Update the parameters with the new latitude - params['imt']['topology']['central_latitude'] = float(lat) - params['imt']['topology']['central_longitude'] = 0.0 - - # Set the right campaign prefix - params['general']['output_dir_prefix'] = 'output_mss_d2d_to_imt_lat_' + \ - direction[0] + '_' + str(lat) + "_deg" - # Save the parameters to a new yaml file - parameter_file_name = "../input/parameters_mss_d2d_to_imt_lat_" + \ - direction[0] + '_' + str(lat) + "_deg.yaml" - parameter_file_name = os.path.join(local_dir, parameter_file_name) - with open(parameter_file_name, 'w') as file: - yaml.dump(params, file, default_flow_style=False) - - del params - print(f'Generated parameters for latitude {lat} degrees.') diff --git a/sharc/campaigns/mss_d2d_to_imt/scripts/plot_results.py b/sharc/campaigns/mss_d2d_to_imt/scripts/plot_results.py deleted file mode 100644 index e9cf059c1..000000000 --- a/sharc/campaigns/mss_d2d_to_imt/scripts/plot_results.py +++ /dev/null @@ -1,150 +0,0 @@ -import os -from pathlib import Path -from sharc.results import Results -from sharc.post_processor import PostProcessor -import argparse - -# Command line argument parser -parser = argparse.ArgumentParser(description="Generate and plot results.") -parser.add_argument( - "--auto_open", - action="store_true", - default=False, - help="Set this flag to open plots in a browser.") -parser.add_argument( - "--scenario", - type=int, - choices=[ - 0, - 1], - required=True, - help="Scenario parameter: 0 or 1. 0 for MSS-D2D to IMT-UL/DL," - "1 for MSS-D2D to IMT-UL/DL with varying latitude.") -args = parser.parse_args() -scenario = args.scenario -auto_open = args.auto_open - -local_dir = os.path.dirname(os.path.abspath(__file__)) -campaign_base_dir = str((Path(__file__) / ".." / "..").resolve()) - -post_processor = PostProcessor() - -# Add a legend to results in folder that match the pattern -# This could easily come from a config file -if scenario == 0: - many_results = Results.load_many_from_dir( - os.path.join( - campaign_base_dir, - "output"), - filter_fn=lambda x: "_co_channel_system_A" in x, - only_latest=True) - post_processor\ - .add_plot_legend_pattern( - dir_name_contains="_mss_d2d_to_imt_ul_co_channel_system_A", - legend="MSS-D2D to IMT-UL" - ) - - post_processor\ - .add_plot_legend_pattern( - dir_name_contains="_mss_d2d_to_imt_dl_co_channel_system_A", - legend="MSS-D2D to IMT-DL" - ) -elif scenario == 1: - many_results = Results.load_many_from_dir( - os.path.join( - campaign_base_dir, - "output"), - filter_fn=lambda x: "_lat_" in x, - only_latest=True) - for link in ["ul", "dl"]: - for i in range(0, 70, 10): - post_processor.add_plot_legend_pattern( - dir_name_contains="_lat_" + link + "_" + str(i) + "_deg", - legend="IMT-Link=" + link.upper() + " latitude=" + str(i) + "deg") -else: - raise ValueError("Invalid scenario. Choose 0 or 1.") - -# ^: typing.List[Results] - -post_processor.add_results(many_results) - -plots = post_processor.generate_cdf_plots_from_results( - many_results -) - -post_processor.add_plots(plots) - -plots = post_processor.generate_ccdf_plots_from_results( - many_results -) - -post_processor.add_plots(plots) - -# Add a protection criteria line: -protection_criteria = -6 -imt_dl_inr = post_processor.get_plot_by_results_attribute_name( - "imt_dl_inr", plot_type="ccdf") -imt_dl_inr.add_vline(protection_criteria, line_dash="dash", annotation=dict( - text="Protection Criteria: " + str(protection_criteria) + " dB", - xref="x", yref="y", - x=protection_criteria + 0.5, y=0.8, - font=dict(size=12, color="red") -)) -imt_dl_inr.update_layout(template="plotly_white") -imt_ul_inr = post_processor.get_plot_by_results_attribute_name( - "imt_ul_inr", plot_type="ccdf") -imt_ul_inr.add_vline(protection_criteria, line_dash="dash") - -# Combine INR plots into one: - -for trace in imt_ul_inr.data: - imt_dl_inr.add_trace(trace) - -# Update layout if needed -imt_dl_inr.update_layout( - title_text="CCDF Plot for IMT Downlink and Uplink INR", - xaxis_title="IMT INR [dB]", - yaxis_title="Cumulative Probability", - legend_title="Legend") - -file = os.path.join(campaign_base_dir, "output", "imt_dl_ul_inr.html") -imt_dl_inr.write_html(file=file, include_plotlyjs="cdn", auto_open=auto_open) - -file = os.path.join( - campaign_base_dir, - "output", - "imt_system_antenna_gain.html") -imt_system_antenna_gain = post_processor.get_plot_by_results_attribute_name( - "imt_system_antenna_gain") -imt_system_antenna_gain.write_html( - file=file, - include_plotlyjs="cdn", - auto_open=auto_open) - -file = os.path.join( - campaign_base_dir, - "output", - "system_imt_antenna_gain.html") -system_imt_antenna_gain = post_processor.get_plot_by_results_attribute_name( - "system_imt_antenna_gain") -system_imt_antenna_gain.write_html( - file=file, - include_plotlyjs="cdn", - auto_open=auto_open) - -file = os.path.join( - campaign_base_dir, - "output", - "sys_to_imt_coupling_loss.html") -imt_system_path_loss = post_processor.get_plot_by_results_attribute_name( - "imt_system_path_loss") -imt_system_path_loss.write_html( - file=file, - include_plotlyjs="cdn", - auto_open=auto_open) - -for result in many_results: - # This generates the mean, median, variance, etc - stats = PostProcessor.generate_statistics( - result=result - ).write_to_results_dir() diff --git a/sharc/campaigns/mss_d2d_to_imt/scripts/plot_results_lat_variation.py b/sharc/campaigns/mss_d2d_to_imt/scripts/plot_results_lat_variation.py deleted file mode 100644 index 200ae7c5d..000000000 --- a/sharc/campaigns/mss_d2d_to_imt/scripts/plot_results_lat_variation.py +++ /dev/null @@ -1,59 +0,0 @@ -import os -from pathlib import Path -from sharc.results import Results -from sharc.post_processor import PostProcessor - -post_processor = PostProcessor() - -# Add a legend to results in folder that match the pattern -# This could easily come from a config file -for i in range(0, 70, 10): - post_processor.add_plot_legend_pattern( - dir_name_contains="_lat_" + str(i) + "_deg", - legend="latitude=" + str(i) + "deg") - -campaign_base_dir = str((Path(__file__) / ".." / "..").resolve()) - -many_results = Results.load_many_from_dir( - os.path.join( - campaign_base_dir, - "output"), - only_latest=True) -# ^: typing.List[Results] - -post_processor.add_results(many_results) - -plots = post_processor.generate_ccdf_plots_from_results( - many_results -) - -post_processor.add_plots(plots) - -# Add a protection criteria line: -protection_criteria = -6 -post_processor\ - .get_plot_by_results_attribute_name("imt_dl_inr", plot_type='ccdf')\ - .add_vline(protection_criteria, line_dash="dash") - -# Show a single plot: -post_processor .get_plot_by_results_attribute_name( - "imt_system_antenna_gain", plot_type='ccdf') .show() - -post_processor .get_plot_by_results_attribute_name( - "system_imt_antenna_gain", plot_type='ccdf') .show() - -post_processor .get_plot_by_results_attribute_name( - "sys_to_imt_coupling_loss", plot_type='ccdf') .show() - -post_processor .get_plot_by_results_attribute_name( - "imt_system_path_loss", plot_type='ccdf') .show() - -post_processor\ - .get_plot_by_results_attribute_name("imt_dl_inr", plot_type='ccdf')\ - .show() - -for result in many_results: - # This generates the mean, median, variance, etc - stats = PostProcessor.generate_statistics( - result=result - ).write_to_results_dir() diff --git a/sharc/campaigns/mss_d2d_to_imt/scripts/start_simulations.py b/sharc/campaigns/mss_d2d_to_imt/scripts/start_simulations.py deleted file mode 100644 index 6fc589b96..000000000 --- a/sharc/campaigns/mss_d2d_to_imt/scripts/start_simulations.py +++ /dev/null @@ -1,45 +0,0 @@ -from sharc.run_multiple_campaigns_mut_thread import run_campaign_re -import argparse -import subprocess - -# Set the campaign name -# The name of the campaign to run. This should match the name of the -# campaign directory. -name_campaign = "mss_d2d_to_imt" - -# Run the campaign in single-thread mode -# This function will execute the campaign with the given name in a single-threaded manner. -# It will look for the campaign directory under the specified name and start the necessary processes. -# run_campaign_re(name_campaign, r'^parameters_mss_d2d_to_imt_co_channel_system_A.yaml') -# run_campaign_re(name_campaign, r'^parameters_mss_d2d_to_imt_(dl,ul)_co_channel_system_A.yaml') - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Run SHARC MSS-D2D simulations.") - parser.add_argument( - "--scenario", - type=int, - choices=range(0, 2), - required=True, - help="""Specify the campaign scenario as a number (0 or 1). - 0 - MSS-D2D to IMT-UL/DL - 1 - MSS-D2D to IMT-UL/DL with varying latitude.""" - ) - - # Parse the arguments - args = parser.parse_args() - - # Update the campaign regex with the provided scenario - scenario = args.scenario - print(f"Running scenario {scenario}...") - if scenario == 0: - regex_pattern = r'^parameters_mss_d2d_to_imt_(dl|ul)_co_channel_system_A.yaml' - elif scenario == 1: - print("Generating parameters for varying latitude campaign...") - subprocess.run(["python", "parameter_gen_lat_variation.py"], - check=True) - regex_pattern = r'^parameters_mss_d2d_to_imt_lat_.*_deg.yaml' - - # Run the campaign with the updated regex pattern - print("Executing campaign with regex pattern:", regex_pattern) - run_campaign_re(name_campaign, regex_pattern) diff --git a/sharc/campaigns/mss_d2d_to_imt/scripts/start_simulations_lat_variation.py b/sharc/campaigns/mss_d2d_to_imt/scripts/start_simulations_lat_variation.py deleted file mode 100644 index af7756669..000000000 --- a/sharc/campaigns/mss_d2d_to_imt/scripts/start_simulations_lat_variation.py +++ /dev/null @@ -1,12 +0,0 @@ -from sharc.run_multiple_campaigns_mut_thread import run_campaign_re - -# Set the campaign name -# The name of the campaign to run. This should match the name of the -# campaign directory. -name_campaign = "mss_d2d_to_imt" - -# Run the campaign in single-thread mode -# This function will execute the campaign with the given name in a single-threaded manner. -# It will look for the campaign directory under the specified name and -# start the necessary processes. -run_campaign_re(name_campaign, r'^parameters_mss_d2d_to_imt_lat_.*_deg.yaml') diff --git a/sharc/campaigns/mss_d2d_to_imt/scripts/taylor_diagram_for_mss_d2d.py b/sharc/campaigns/mss_d2d_to_imt/scripts/taylor_diagram_for_mss_d2d.py deleted file mode 100644 index ce2e04fdc..000000000 --- a/sharc/campaigns/mss_d2d_to_imt/scripts/taylor_diagram_for_mss_d2d.py +++ /dev/null @@ -1,62 +0,0 @@ - -""" -Script to generate a Taylor diagram for the MSS D2D to IMT sharing scenario. -Parameters are based on Annex 4 to Working Party 4C Chair’s Report, Section 4.1.4. -""" - -import numpy as np -import plotly.graph_objects as go - -from sharc.antenna.antenna_s1528 import AntennaS1528Taylor -from sharc.parameters.antenna.parameters_antenna_s1528 import ParametersAntennaS1528 - -# Antenna parameters -g_max = 34.1 # dBi -l_r = l_t = 1.6 # meters -slr = 20 # dB -n_side_lobes = 2 # number of side lobes -freq = 2e3 # MHz - -antenna_params = ParametersAntennaS1528( - antenna_gain=g_max, - frequency=freq, # in MHz - bandwidth=5, # in MHz - slr=slr, - n_side_lobes=n_side_lobes, - l_r=l_r, - l_t=l_t, -) - -# Create an instance of AntennaS1528Taylor -antenna = AntennaS1528Taylor(antenna_params) - -# Define phi angles from 0 to 60 degrees for plotting -theta_angles = np.linspace(0, 90, 901) - -# Calculate gains for each phi angle at a fixed theta angle (e.g., theta=0) -gain_rolloff_7 = antenna.calculate_gain(off_axis_angle_vec=theta_angles, - theta_vec=np.zeros_like(theta_angles)) - -# Create a plotly figure -fig = go.Figure() - -# Add a trace for the antenna gain -fig.add_trace( - go.Scatter( - x=theta_angles, - y=gain_rolloff_7 - - g_max, - mode='lines', - name='Antenna Gain')) -# Limit the y-axis from 0 to 35 dBi -fig.update_yaxes(range=[-20 - g_max, 2]) -fig.update_xaxes(range=[0, 90]) -# Set the title and labels -fig.update_layout( - title='Normalized SystemA Antenna Pattern', - xaxis_title='Theta (degrees)', - yaxis_title='Gain (dBi)' -) - -# Show the plot -fig.show() diff --git a/sharc/campaigns/mss_d2d_to_imt_cross_border/input/.gitignore b/sharc/campaigns/mss_d2d_to_imt_cross_border/input/.gitignore deleted file mode 100644 index d6b7ef32c..000000000 --- a/sharc/campaigns/mss_d2d_to_imt_cross_border/input/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/sharc/campaigns/mss_d2d_to_imt_cross_border/readme.md b/sharc/campaigns/mss_d2d_to_imt_cross_border/readme.md deleted file mode 100644 index c3b7989fd..000000000 --- a/sharc/campaigns/mss_d2d_to_imt_cross_border/readme.md +++ /dev/null @@ -1,3 +0,0 @@ -A cross border study with IMT TN single BS and UE at Paraguay's side of Friendship Bridge and MSS DC system over Brazil. - -UE is victim diff --git a/sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/base_input.yaml b/sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/base_input.yaml deleted file mode 100644 index 8fc8c2bdc..000000000 --- a/sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/base_input.yaml +++ /dev/null @@ -1,566 +0,0 @@ -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots: 10000 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link: UPLINK - ########################################################################### - # The chosen system for sharing study - # EESS_PASSIVE, FSS_SS, FSS_ES, FS, RAS, MSS_SS, MSS_D2D - system: MSS_D2D - ########################################################################### - # Compatibility scenario (co-channel and/or adjacent channel interference) - enable_cochannel: true - enable_adjacent_channel: false - ########################################################################### - # Seed for random number generator - seed: 101 - ########################################################################### - # if FALSE, then a new output directory is created - overwrite_output: FALSE - ########################################################################### - # output destination folder - this is relative SHARC/sharc directory - output_dir: campaigns/mss_d2d_to_imt_cross_border/output_base_ul/ - ########################################################################### - # output folder prefix - output_dir_prefix: output_mss_d2d_to_imt_cross_border_base_ul -imt: - ########################################################################### - # Minimum 2D separation distance from BS to UE [m] - minimum_separation_distance_bs_ue: 35 - ########################################################################### - # Defines if IMT service is the interferer or interfered-with service - # TRUE: IMT suffers interference - # FALSE : IMT generates interference - interfered_with: TRUE - ########################################################################### - # IMT center frequency [MHz] - frequency: 2160.0 - ########################################################################### - # IMT bandwidth [MHz] - bandwidth: 20.0 - ########################################################################### - # IMT resource block bandwidth [MHz] - rb_bandwidth: 0.180 - # IMT resource block bandwidth [MHz] - adjacent_ch_reception: "OFF" - ########################################################################### - # IMT spectrum emission mask. Options are: - # "IMT-2020" : for mmWave as described in ITU-R TG 5/1 Contribution 36 - # "3GPP E-UTRA" : for E-UTRA bands > 1 GHz as described in - # TS 36.104 v11.2.0 (BS) and TS 36.101 v11.2.0 (UE) - spectral_mask: 3GPP E-UTRA - ########################################################################### - # level of spurious emissions [dBm/MHz] - spurious_emissions: -13.0 - ########################################################################### - # Amount of guard band wrt total bandwidth. Setting this parameter to 0.1 - # means that 10% of the total bandwidth will be used as guard band: 5% in - # the lower - guard_band_ratio: 0.1 - ########################################################################### - # Network topology parameters. - topology: - ########################################################################### - # The latitude position is is_spherical is set to true - central_latitude: -25.5549751 - ########################################################################### - # The longitude position is is_spherical is set to true - central_longitude: -54.5746686 - ########################## - central_altitude: 200 - ########################################################################### - # Topology Type. Possible values are "MACROCELL", "HOTSPOT", "SINGLE_BS" - # "INDOOR" - type: SINGLE_BS - ########################################################################### - # Macrocell Topology parameters. Relevant when imt.topology.type == "MACROCELL" - macrocell: - ########################################################################### - # Inter-site distance in macrocell network topology [m] - intersite_distance: 1500.0 - ########################################################################### - # Enable wrap around. - wrap_around: false - ########################################################################### - # Number of clusters in macro cell topology - num_clusters: 1 - ########################################################################### - # Single Base Station Topology parameters. Relevant when imt.topology.type == "SINGLE_BS" - single_bs: - ########################################################################### - # Inter-site distance or Cell Radius in single Base Station network topology [m] - # You can either provide 'cell_radius' or 'intersite_distance' for this topology - # The relationship used is cell_radius = intersite_distance * 2 / 3 - cell_radius: 500 - # intersite_distance: 1 - ########################################################################### - # Number of clusters in single base station topology - # You can simulate 1 or 2 BS's with this topology - num_clusters: 1 - ########################################################################### - # Hotspot Topology parameters. Relevant when imt.topology.type == "HOTSPOT" - hotspot: - ########################################################################### - # Inter-site distance in hotspot network topology [m] - intersite_distance: 321 - ########################################################################### - # Enable wrap around. - wrap_around: true - ########################################################################### - # Number of clusters in macro cell topology - num_clusters: 7 - ########################################################################### - # Number of hotspots per macro cell (sector) - num_hotspots_per_cell: 1 - ########################################################################### - # Maximum 2D distance between hotspot and UE [m] - # This is the hotspot radius - max_dist_hotspot_ue: 99.9 - ########################################################################### - # Minimum 2D distance between macro cell base station and hotspot [m] - min_dist_bs_hotspot: 1.2 - ########################################################################### - # Indoor Topology parameters. Relevant when imt.topology.type == "INOOR" - indoor: - ########################################################################### - # Basic path loss model for indoor topology. Possible values: - # "FSPL" (free-space path loss), - # "INH_OFFICE" (3GPP Indoor Hotspot - Office) - basic_path_loss: FSPL - ########################################################################### - # Number of rows of buildings in the simulation scenario - n_rows: 3 - ########################################################################### - # Number of colums of buildings in the simulation scenario - n_colums: 2 - ########################################################################### - # Number of buildings containing IMT stations. Options: - # 'ALL': all buildings contain IMT stations. - # Number of buildings. - num_imt_buildings: 2 - ########################################################################### - # Street width (building separation) [m] - street_width: 30.1 - ########################################################################### - # Intersite distance [m] - intersite_distance: 40.1 - ########################################################################### - # Number of cells per floor - num_cells: 3 - ########################################################################### - # Number of floors per building - num_floors: 1 - ########################################################################### - # Percentage of indoor UE's [0, 1] - ue_indoor_percent: .95 - ########################################################################### - # Building class: "TRADITIONAL" or "THERMALLY_EFFICIENT" - building_class: THERMALLY_EFFICIENT - ########################################################################### - # NTN Topology Parameters - ntn: - ########################################################################### - # NTN cell radius or intersite distance in network topology [m] - # @important: You can set only one of cell_radius or intersite_distance - # NTN Intersite Distance (m). Intersite distance = Cell Radius * sqrt(3) - # NOTE: note that intersite distance has a different geometric meaning in ntn - cell_radius: 123 - # intersite_distance: 155884 - ########################################################################### - # NTN space station azimuth [degree] - bs_azimuth: 45 - ########################################################################### - # NTN space station elevation [degree] - bs_elevation: 45 - ########################################################################### - # number of sectors [degree] - num_sectors: 19 - ########################################################################### - # Defines the antenna model to be used in compatibility studies between - # IMT and other services in adjacent band - # Possible values: SINGLE_ELEMENT, BEAMFORMING - adjacent_antenna_model: SINGLE_ELEMENT - # Base station parameters - bs: - ########################################################################### - # The load probability (or activity factor) models the statistical - # variation of the network load by defining the number of fully loaded - # base stations that are simultaneously transmitting - load_probability: 1.0 - ########################################################################### - # Conducted power per antenna element [dBm/bandwidth] - conducted_power: 28.0 - ########################################################################### - # Base station height [m] - height: 20.0 - ########################################################################### - # Base station noise figure [dB] - noise_figure: 5.0 - ########################################################################### - # Base station array ohmic loss [dB] - # NOTE: element gain includes ohmic loss - ohmic_loss: 0.0 - # Base Station Antenna parameters: - antenna: - array: - ########################################################################### - # If normalization of M2101 should be applied for BS - normalization: FALSE - ########################################################################### - # Base station horizontal beamsteering range. [deg] - # The range considers the BS azimuth as 0deg - horizontal_beamsteering_range: !!python/tuple [-60., 60.] - ########################################################################### - # Base station horizontal beamsteering range. [deg] - # The range considers the horizon as 90deg, 0 as zenith - vertical_beamsteering_range: !!python/tuple [90., 100.] - ########################################################################### - # File to be used in the BS beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/bs_norm.npz - ########################################################################### - # File to be used in the UE beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: M2101 - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # mechanical downtilt [degrees] - # NOTE: consider defining it to 90 degrees in case of indoor simulations - downtilt: 6 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # default: element_max_g = 5, for M.2101 - # = 15, for M.2292 - # = -3, for M.2292 - element_max_g: 6.4 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 90 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 65 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 4 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 8 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 2.1 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 30 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 30 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - ########################################################################### - # Subarray for IMT as defined in R23-WP5D-C-0413, Annex 4.2 - # Single column sub array - subarray: - # NOTE: if subarray is enabled, element definition will mostly come from - # the above definitions - is_enabled: true - # Rows per subarray - n_rows: 3 - # Sub array element spacing (d/lambda). - element_vert_spacing: 0.7 - # Sub array eletrical downtilt [deg] - eletrical_downtilt: 3.0 - - ########################################################################### - # User Equipment parameters: - ue: - ########################################################################### - # Number of UEs that are allocated to each cell within handover margin. - # Remember that in macrocell network each base station has 3 cells (sectors) - k: 1 - ########################################################################### - # Multiplication factor that is used to ensure that the sufficient number - # of UE's will distributed throughout ths system area such that the number - # of K users is allocated to each cell. Normally, this values varies - # between 2 and 10 according to the user drop method - k_m: 1 - ########################################################################### - # Percentage of indoor UE's [%] - indoor_percent: 0.0 - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter states how the UEs will be distributed - # Possible values: UNIFORM : UEs will be uniformly distributed within the - # whole simulation area. Not applicable to - # hotspots. - # ANGLE_AND_DISTANCE : UEs will be distributed following - # given distributions for angle and - # distance. In this case, these must be - # defined later. - distribution_type: ANGLE_AND_DISTANCE - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the distance between UE's and BS. - # Possible values: RAYLEIGH, UNIFORM - distribution_distance: RAYLEIGH - ########################################################################### - # Regarding the distribution of active UE's over the cell area, this - # parameter models the azimuth between UE and BS (within ±60° range). - # Possible values: NORMAL, UNIFORM - distribution_azimuth: UNIFORM - ########################################################################### - # Power control algorithm - # tx_power_control = "ON",power control On - # tx_power_control = "OFF",power control Off - tx_power_control: ON - ########################################################################### - # Power per RB used as target value [dBm] - p_o_pusch: -92.2 - ########################################################################### - # Alfa is the balancing factor for UEs with bad channel - # and UEs with good channel - alpha: 0.8 - ########################################################################### - # Maximum UE transmit power [dBm] - p_cmax: 23 - ########################################################################### - # UE power dynamic range [dB] - # The minimum transmit power of a UE is (p_cmax - dynamic_range) - power_dynamic_range: 63 - ########################################################################### - # UE height [m] - height: 1.5 - ########################################################################### - # User equipment noise figure [dB] - noise_figure: 9 - ########################################################################### - # User equipment feed loss [dB] - ohmic_loss: 0 - ########################################################################### - # User equipment body loss [dB] - body_loss: 4 - antenna: - array: - ########################################################################### - # If normalization of M2101 should be applied for UE - normalization: FALSE - ########################################################################### - # File to be used in the UE beamforming normalization - # Normalization files can be generated with the - # antenna/beamforming_normalization/normalize_script.py script - normalization_file: antenna/beamforming_normalization/ue_norm.npz - ########################################################################### - # Radiation pattern of each antenna element - # Possible values: "M2101", "F1336", "FIXED" - element_pattern: FIXED - ########################################################################### - # Minimum array gain for the beamforming antenna [dBi] - minimum_array_gain: -200 - ########################################################################### - # BS/UE maximum transmit/receive element gain [dBi] - # = 15, for M.2292 - # default: element_max_g = 5, for M.2101 - # = -3, for M.2292 - element_max_g: -3 - ########################################################################### - # BS/UE horizontal 3dB beamwidth of single element [degrees] - element_phi_3db: 90 - ########################################################################### - # BS/UE vertical 3dB beamwidth of single element [degrees] - # For F1336: if equal to 0, then beamwidth is calculated automaticaly - element_theta_3db: 90 - ########################################################################### - # BS/UE number of rows in antenna array - n_rows: 1 - ########################################################################### - # BS/UE number of columns in antenna array - n_columns: 1 - ########################################################################### - # BS/UE array horizontal element spacing (d/lambda) - element_horiz_spacing: 0.5 - ########################################################################### - # BS/UE array vertical element spacing (d/lambda) - element_vert_spacing: 0.5 - ########################################################################### - # BS/UE front to back ratio of single element [dB] - element_am: 25 - ########################################################################### - # BS/UE single element vertical sidelobe attenuation [dB] - element_sla_v: 25 - ########################################################################### - # Multiplication factor k that is used to adjust the single-element pattern. - # According to Report ITU-R M.[IMT.AAS], this may give a closer match of the - # side lobes when beamforming is assumed in adjacent channel. - # Original value: 12 (Rec. ITU-R M.2101) - multiplication_factor: 12 - ########################################################################### - # Uplink parameters. Only needed when using uplink on imt - uplink: - ########################################################################### - # Uplink attenuation factor used in link-to-system mapping - attenuation_factor: 0.4 - ########################################################################### - # Uplink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Uplink maximum SINR of the code set [dB] - sinr_max: 22 - ########################################################################### - # Downlink parameters. Only needed when using donwlink on imt - downlink: - ########################################################################### - # Downlink attenuation factor used in link-to-system mapping - attenuation_factor: 0.8 - ########################################################################### - # Downlink minimum SINR of the code set [dB] - sinr_min: -10 - ########################################################################### - # Downlink maximum SINR of the code set [dB] - sinr_max: 30 - ########################################################################### - # Channel parameters - # channel model, possible values are "FSPL" (free-space path loss), - # "CI" (close-in FS reference distance) - # "UMa" (Urban Macro - 3GPP) - # "UMi" (Urban Micro - 3GPP) - # "TVRO-URBAN" - # "TVRO-SUBURBAN" - # "ABG" (Alpha-Beta-Gamma) - # "P619" - channel_model: UMa - season: SUMMER - ########################################################################### - # Adjustment factor for LoS probability in UMi path loss model. - # Original value: 18 (3GPP) - los_adjustment_factor: 18 - ########################################################################### - # If shadowing should be applied or not. - # Used in propagation UMi, UMa, ABG, when topology == indoor and any - shadowing: FALSE - ########################################################################### - # receiver noise temperature [K] - noise_temperature: 290 - ########################################################################### - # P619 parameters - param_p619: - ########################################################################### - # altitude of the space station [m] - space_station_alt_m: 540000 - ########################################################################### - # altitude of ES system [m] - earth_station_alt_m: 1200 - ########################################################################### - # latitude of ES system [m] - earth_station_lat_deg: 13 - ########################################################################### - # difference between longitudes of IMT-NTN station and FSS-ES system [deg] - # (positive if space-station is to the East of earth-station) - earth_station_long_diff_deg: 0 -#### System Parameters -mss_d2d: - # MSS_D2D system name - name: SystemA - # Adjacent channel emissions type - # Possible values are "ACLR", "SPECTRAL_MASK" and "OFF" - adjacent_ch_emissions: SPECTRAL_MASK - # chosen spectral Mask - spectral_mask: 3GPP E-UTRA - # MSS_D2D system center frequency in MHz - frequency: 2167.5 - # MSS_D2d system bandwidth in MHz - bandwidth: 5.0 - # MSS_D2D cell radius in network topology [m] - cell_radius: 39475.0 - # Satellite power density in dBW/Hz - tx_power_density: -54.2 - # Polarization loss [dB] - # P.619 suggests 3dB polarization loss as good constant value for monte carlo - polarization_loss: 0 - # Number of sectors - num_sectors: 19 - # Satellite antenna pattern - # Antenna pattern from ITU-R S.1528 - # Possible values: "ITU-R-S.1528-Section1.2", "ITU-R-S.1528-LEO" - antenna_pattern: ITU-R-S.1528-Taylor - antenna_s1528: - # Maximum Antenna gain in dBi - antenna_gain: 34.1 - ### The following parameters are used for S.1528-Taylor antenna pattern - # SLR is the side-lobe ratio of the pattern (dB), the difference in gain between the maximum - # gain and the gain at the peak of the first side lobe. - slr: 20.0 - # Number of secondarylobes considered in the diagram (coincide with the roots of the Bessel function) - n_side_lobes: 2 - # Radial (l_r) and transverse (l_t) sizes of the effective radiating area of the satellite transmitt antenna (m) - l_r: 1.6 - l_t: 1.6 - sat_is_active_if: - # for a satellite to be active, it needs to respect ALL conditions - conditions: - - LAT_LONG_INSIDE_COUNTRY - - MINIMUM_ELEVATION_FROM_ES - minimum_elevation_from_es: 5 - lat_long_inside_country: - # You may specify another shp file for country borders reference - # country_shapes_filename: sharc/topology/countries/ne_110m_admin_0_countries.shp - country_names: - - Brazil - # - Argentina - # margin from inside of border [km] - # if positive, makes border smaller by x km - # if negative, makes border bigger by x km - margin_from_border: 0 - # margin_from_border: 157.9 - # margin_from_border: 213.4 - # margin_from_border: 268.9 - # margin_from_border: 324.4 - # channel model, possible values are "FSPL" (free-space path loss), - # "SatelliteSimple" (FSPL + 4 + clutter loss) - # "P619" - channel_model: FSPL - # param_p619: - # ########################################################################### - # # altitude of ES system [m] - # earth_station_alt_m: 1200 - # ########################################################################### - # # latitude of ES system [m] - # earth_station_lat_deg: -15.7801 - # ########################################################################### - # # difference between longitudes of IMT-NTN station and FSS-ES system [deg] - # # (positive if space-station is to the East of earth-station) - # earth_station_long_diff_deg: 0.0 - # ########################################################################### - # # year season: SUMMER of WINTER - # season: SUMMER - # Orbit parameters - orbits: - # Number of planes - - n_planes: 28 - # Inclination in degrees - inclination_deg: 53.0 - # Perigee in km - perigee_alt_km: 525.0 - # Apogee in km - apogee_alt_km: 525.0 - # Number of satellites per plane - sats_per_plane: 128 - # Longitude of the first ascending node in degrees - long_asc_deg: 0.0 - # Phasing in degrees - phasing_deg: 1.5 \ No newline at end of file diff --git a/sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/generate_parameters_from_reference.py b/sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/generate_parameters_from_reference.py deleted file mode 100644 index 850e2de78..000000000 --- a/sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/generate_parameters_from_reference.py +++ /dev/null @@ -1,130 +0,0 @@ -"""Generates scenarios based on main parameters -""" -import yaml -import os -from copy import deepcopy - -from sharc.parameters.parameters_base import tuple_constructor - -yaml.SafeLoader.add_constructor( - 'tag:yaml.org,2002:python/tuple', - tuple_constructor) - -local_dir = os.path.dirname(os.path.abspath(__file__)) -input_dir = os.path.join(local_dir, "../input") - -ul_parameter_file_name = os.path.join(local_dir, "./base_input.yaml") - -# load the base parameters from the yaml file -with open(ul_parameter_file_name, 'r') as file: - ul_parameters = yaml.safe_load(file) - -dl_parameters = deepcopy(ul_parameters) -dl_parameters['general']['output_dir'] = ul_parameters['general']['output_dir'].replace( - "_ul", "_dl") -dl_parameters['general']['output_dir_prefix'] = ul_parameters['general']['output_dir_prefix'].replace( - "_ul", "_dl") -dl_parameters['general']['imt_link'] = "DOWNLINK" - -country_border = 4 * ul_parameters["mss_d2d"]["cell_radius"] / 1e3 -print("country_border", country_border) - -# doesn't matter from which, both will give same result -output_dir_pattern = ul_parameters['general']['output_dir'].replace( - "_base_ul", "_") -output_prefix_pattern = ul_parameters['general']['output_dir_prefix'].replace( - "_base_ul", "_") - -for dist in [ - 0, - country_border, - country_border + 111 / 2, - country_border + 111, - country_border + 3 * 111 / 2, - country_border + 2 * 111 -]: - ul_parameters["mss_d2d"]["sat_is_active_if"]["lat_long_inside_country"]["margin_from_border"] = dist - specific = f"{dist}km_base_ul" - ul_parameters['general']['output_dir_prefix'] = output_prefix_pattern.replace( - "", specific) - - dl_parameters["mss_d2d"]["sat_is_active_if"]["lat_long_inside_country"]["margin_from_border"] = dist - specific = f"{dist}km_base_dl" - dl_parameters['general']['output_dir_prefix'] = output_prefix_pattern.replace( - "", specific) - - ul_parameter_file_name = os.path.join( - input_dir, f"./parameters_mss_d2d_to_imt_cross_border_{dist}km_base_ul.yaml") - dl_parameter_file_name = os.path.join( - input_dir, f"./parameters_mss_d2d_to_imt_cross_border_{dist}km_base_dl.yaml") - - with open( - dl_parameter_file_name, - 'w' - ) as file: - yaml.dump(dl_parameters, file, default_flow_style=False) - with open( - ul_parameter_file_name, - 'w' - ) as file: - yaml.dump(ul_parameters, file, default_flow_style=False) - - for link in ["ul", "dl"]: - if link == "ul": - parameters = deepcopy(ul_parameters) - if link == "dl": - parameters = deepcopy(dl_parameters) - - parameters['mss_d2d']['num_sectors'] = 19 - # 1 out of 19 beams are active - parameters['mss_d2d']['beams_load_factor'] = 0.05263157894 - - specific = f"{dist}km_activate_random_beam_5p_{link}" - parameters['general']['output_dir_prefix'] = output_prefix_pattern.replace( - "", specific) - - with open( - os.path.join(input_dir, f"./parameters_mss_d2d_to_imt_cross_border_{specific}.yaml"), - 'w' - ) as file: - yaml.dump(parameters, file, default_flow_style=False) - - parameters['mss_d2d']['num_sectors'] = 19 - parameters['mss_d2d']['beams_load_factor'] = 0.3 - - specific = f"{dist}km_activate_random_beam_30p_{link}" - parameters['general']['output_dir_prefix'] = output_prefix_pattern.replace( - "", specific) - - with open( - os.path.join(input_dir, f"./parameters_mss_d2d_to_imt_cross_border_{specific}.yaml"), - 'w' - ) as file: - yaml.dump(parameters, file, default_flow_style=False) - - parameters['mss_d2d']['num_sectors'] = 1 - parameters['mss_d2d']['beams_load_factor'] = 1 - - parameters['mss_d2d']['beam_positioning'] = {} - - parameters['mss_d2d']['beam_positioning']['type'] = "ANGLE_AND_DISTANCE_FROM_SUBSATELLITE" - - # for uniform area distribution - parameters['mss_d2d']['beam_positioning']['angle_from_subsatellite_phi'] = { - 'type': "~U(MIN,MAX)", 'distribution': {'min': -180., 'max': 180., }} - parameters['mss_d2d']['beam_positioning']['distance_from_subsatellite'] = { - 'type': "~SQRT(U(0,1))*MAX", - 'distribution': { - 'min': 0, - 'max': parameters['mss_d2d']["cell_radius"] * 4, - }} - - specific = f"{dist}km_random_pointing_1beam_{link}" - parameters['general']['output_dir_prefix'] = output_prefix_pattern.replace( - "", specific) - - with open( - os.path.join(input_dir, f"./parameters_mss_d2d_to_imt_cross_border_{specific}.yaml"), - 'w' - ) as file: - yaml.dump(parameters, file, default_flow_style=False) diff --git a/sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/plot_3d_param_file.py b/sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/plot_3d_param_file.py deleted file mode 100644 index 37f7e0a2a..000000000 --- a/sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/plot_3d_param_file.py +++ /dev/null @@ -1,177 +0,0 @@ -# Generates a 3D plot of the Earth with the satellites positions -""" -Script to generate a 3D plot of the Earth with satellite positions for the MSS D2D to IMT cross-border scenario. -""" -# https://geopandas.org/en/stable/docs/user_guide/io.html -import numpy as np -import plotly.graph_objects as go -from pathlib import Path - -from sharc.support.sharc_geom import CoordinateSystem -from sharc.parameters.parameters import Parameters -from sharc.topology.topology_factory import TopologyFactory -from sharc.station_factory import StationFactory -from sharc.satellite.scripts.plot_globe import plot_globe_with_borders, plot_mult_polygon - - -if __name__ == "__main__": - coord_sys = CoordinateSystem() - SELECTED_SNAPSHOT_NUMBER = 0 - OPAQUE_GLOBE = True - print(f"Plotting drop {SELECTED_SNAPSHOT_NUMBER}") - # even when using the same drop number as in a simulation, - # since the random generators are not in the same state, there isn't - # a direct relationship between a drop in this plot and a drop in the - # simulation loop - # NOTE: if you want to plot the actual simulation scenarios for debugging, - # you should do so inside the simulation loop - print(" (not the same drop number as in simulation)") - - script_dir = Path(__file__).parent - param_file = script_dir / "base_input.yaml" - # param_file = script_dir / "../input/parameters_mss_d2d_to_imt_cross_border_0km_random_pointing_1beam_dl.yaml" - param_file = param_file.resolve() - print("File at:") - print(f" '{param_file}'") - - parameters = Parameters() - parameters.set_file_name(param_file) - parameters.read_params() - - coord_sys.set_reference( - parameters.imt.topology.central_latitude, - parameters.imt.topology.central_longitude, - parameters.imt.topology.central_altitude, - ) - print( - "imt at (lat, lon, alt) = ", - (coord_sys.ref_lat, coord_sys.ref_long, coord_sys.ref_alt), - ) - - import random - random.seed(parameters.general.seed) - - secondary_seeds = [None] * parameters.general.num_snapshots - - max_seed = 2**32 - 1 - - for index in range(parameters.general.num_snapshots): - secondary_seeds[index] = random.randint(1, max_seed) - - seed = secondary_seeds[SELECTED_SNAPSHOT_NUMBER] - - topology = TopologyFactory.createTopology(parameters, coord_sys) - - random_number_gen = np.random.RandomState(seed) - - # In case of hotspots, base stations coordinates have to be calculated - # on every snapshot. Anyway, let topology decide whether to calculate - # or not - topology.calculate_coordinates(random_number_gen) - - # Create the base stations (remember that it takes into account the - # network load factor) - bs = StationFactory.generate_imt_base_stations( - parameters.imt, - parameters.imt.bs.antenna.array, - topology, random_number_gen, - ) - - # Create the other system (FSS, HAPS, etc...) - system = StationFactory.generate_system( - parameters, topology, random_number_gen, - coord_sys - ) - - # Create IMT user equipments - ue = StationFactory.generate_imt_ue( - parameters.imt, - parameters.imt.ue.antenna.array, - topology, random_number_gen, - ) - - # Plot the globe with satellite positions - fig = plot_globe_with_borders(OPAQUE_GLOBE, coord_sys, False) - - polygons_lim = plot_mult_polygon( - parameters.mss_d2d.sat_is_active_if.lat_long_inside_country.filter_polygon, - coord_sys, - False) - from functools import reduce - - lim_x, lim_y, lim_z = reduce(lambda acc, it: (list(it[0]) + [None] + acc[0], list( - it[1]) + [None] + acc[1], list(it[2]) + [None] + acc[2]), polygons_lim, ([], [], [])) - - fig.add_trace(go.Scatter3d( - x=lim_x, - y=lim_y, - z=lim_z, - mode='lines', - line=dict(color='rgb(0, 0, 255)'), - showlegend=False - )) - - # Plot all satellites (red markers) - fig.add_trace(go.Scatter3d( - x=system.x, - y=system.y, - z=system.z, - mode='markers', - marker=dict(size=2, color='red', opacity=0.5), - showlegend=False - )) - - # Plot visible satellites (green markers) - # print(visible_positions['x'][visible_positions['x'] > 0]) - # print("vis_elevation", vis_elevation) - fig.add_trace(go.Scatter3d( - x=system.x[system.active], - y=system.y[system.active], - z=system.z[system.active], - mode='markers', - marker=dict(size=3, color='green', opacity=0.8), - showlegend=False - )) - - fig.add_trace(go.Scatter3d( - x=ue.x, - y=ue.y, - z=ue.z, - mode='markers', - marker=dict(size=4, color='blue', opacity=1.0), - showlegend=False - )) - - fig.add_trace(go.Scatter3d( - x=bs.x, - y=bs.y, - z=bs.z, - mode='markers', - marker=dict(size=4, color='black', opacity=1.0), - showlegend=False - )) - - # Display the plot - range = 3e6 - fig.update_layout( - scene=dict( - zaxis=dict( - range=(-range, range) - ), - yaxis=dict( - range=(-range, range) - ), - xaxis=dict( - range=(-range, range) - ), - camera=dict( - center=dict(x=0, y=0, z=-coord_sys.get_translation() / - (2 * range)), # Look at Earth's center - # eye=eye, # Camera position - # center=dict(x=0, y=0, z=0), # Look at Earth's center - # up=dict(x=0, y=0, z=1) # Ensure the up direction is correct - ) - ) - ) - - fig.show() diff --git a/sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/plot_results.py b/sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/plot_results.py deleted file mode 100644 index 4d6b3b99c..000000000 --- a/sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/plot_results.py +++ /dev/null @@ -1,148 +0,0 @@ -"""Script to process and plot results for MSS D2D to IMT cross-border campaign.""" - -import os -from pathlib import Path -from sharc.results import Results -# import plotly.graph_objects as go -from sharc.post_processor import PostProcessor - -post_processor = PostProcessor() - -# If set to True the plots will be opened in the browser automatically -auto_open = False - -# Add a legend to results in folder that match the pattern -# This could easily come from a config file - -prefixes = [ - "0km", - "157.9km", - "213.4km", - "268.9km", - "324.4km", - "379.9km", - "border"] -for link in ["dl", "ul"]: - for prefix in prefixes: - if prefix == "border": - km = "0km" - else: - km = prefix - post_processor\ - .add_plot_legend_pattern( - dir_name_contains=f"{prefix}_base_" + link, - legend=f"19 sectors ({km})" - ).add_plot_legend_pattern( - dir_name_contains=f"{prefix}_activate_random_beam_5p_" + link, - legend=f"19 sectors, load=1/19 ({km})" - ).add_plot_legend_pattern( - dir_name_contains=f"{prefix}_activate_random_beam_30p_" + link, - legend=f"19 sectors, load=30% ({km})" - ).add_plot_legend_pattern( - dir_name_contains=f"{prefix}_random_pointing_1beam_" + link, - legend=f"1 sector random pointing ({km})" - ) - -campaign_base_dir = str((Path(__file__) / ".." / "..").resolve()) - -results_dl = Results.load_many_from_dir( - os.path.join( - campaign_base_dir, - "output_base_dl"), - only_latest=True) -results_ul = Results.load_many_from_dir( - os.path.join( - campaign_base_dir, - "output_base_ul"), - only_latest=True) -# ^: typing.List[Results] -all_results = [*results_ul, *results_dl] - -post_processor.add_results(all_results) - -# Define line styles for different prefixes - the size must match the -# number of unique legends -styles = ["solid", "dot", "dash", "longdash", "dashdot", "longdashdot"] - - -def linestyle_getter(result: Results): - """ - Returns a line style string based on the prefix found in the result's output directory. - """ - for i in range(len(prefixes)): - if "_" + prefixes[i] in result.output_directory: - return styles[i] - return "solid" - - -post_processor.add_results_linestyle_getter(linestyle_getter) - -plots = post_processor.generate_ccdf_plots_from_results( - all_results -) - -post_processor.add_plots(plots) - -# Add a protection criteria line: -protection_criteria = -6 -post_processor\ - .get_plot_by_results_attribute_name("imt_dl_inr", plot_type='ccdf')\ - .add_vline(protection_criteria, line_dash="dash", annotation=dict( - text="Protection criteria", - xref="x", - yref="paper", - x=protection_criteria + 1.0, # Offset for visibility - y=0.95 - )) -perc_of_time = 0.01 -post_processor\ - .get_plot_by_results_attribute_name("imt_dl_inr", plot_type="ccdf")\ - .add_hline(perc_of_time, line_dash="dash") -post_processor\ - .get_plot_by_results_attribute_name("imt_ul_inr", plot_type='ccdf')\ - .add_vline(protection_criteria, line_dash="dash", annotation=dict( - text="Protection criteria", - xref="x", - yref="paper", - x=protection_criteria + 1.0, # Offset for visibility - y=0.95 - )) -post_processor\ - .get_plot_by_results_attribute_name("imt_ul_inr", plot_type="ccdf")\ - .add_hline(perc_of_time, line_dash="dash") - -# Add a protection criteria line: -pfd_protection_criteria = -109 -post_processor\ - .get_plot_by_results_attribute_name("imt_dl_pfd_external_aggregated", plot_type='ccdf')\ - .add_vline(pfd_protection_criteria, line_dash="dash", annotation=dict( - text="PFD protection criteria", - xref="x", - yref="paper", - x=pfd_protection_criteria + 1.0, # Offset for visibility - y=0.95 - )) - - -attributes_to_plot = [ - # "imt_system_antenna_gain", - # "system_imt_antenna_gain", - # "sys_to_imt_coupling_loss", - # "imt_system_path_loss", - "imt_dl_pfd_external", - "imt_dl_pfd_external_aggregated", - "imt_dl_inr", - "imt_ul_inr", -] - -# Ensure the "htmls" directory exists relative to the script directory -htmls_dir = Path(__file__).parent / "htmls" -htmls_dir.mkdir(exist_ok=True) -for attr in attributes_to_plot: - fig = post_processor.get_plot_by_results_attribute_name( - attr, plot_type='ccdf') - fig.update_layout(template="plotly_white") - fig.write_html( - htmls_dir / f"{attr}.html", - include_plotlyjs="cdn", - auto_open=auto_open) diff --git a/sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/start_simulations_multi_thread.py b/sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/start_simulations_multi_thread.py deleted file mode 100644 index 9a6a98363..000000000 --- a/sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/start_simulations_multi_thread.py +++ /dev/null @@ -1,14 +0,0 @@ - -"""Script to start MSS D2D to IMT cross-border campaign simulations in multi-threaded mode.""" -from sharc.run_multiple_campaigns_mut_thread import run_campaign - -# Set the campaign name -# The name of the campaign to run. This should match the name of the -# campaign directory. -name_campaign = "mss_d2d_to_imt_cross_border" - -# Run the campaigns -# This function will execute the campaign with the given name. -# It will look for the campaign directory under the specified name and -# start the necessary processes. -run_campaign(name_campaign) diff --git a/sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/start_simulations_single_thread.py b/sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/start_simulations_single_thread.py deleted file mode 100644 index d43854297..000000000 --- a/sharc/campaigns/mss_d2d_to_imt_cross_border/scripts/start_simulations_single_thread.py +++ /dev/null @@ -1,16 +0,0 @@ - -"""Script to start MSS D2D to IMT cross-border campaign simulations in single-threaded mode.""" -from sharc.run_multiple_campaigns import run_campaign_re - -# Set the campaign name -# The name of the campaign to run. This should match the name of the -# campaign directory. -name_campaign = "mss_d2d_to_imt_cross_border" - -# Run the campaign in single-thread mode -# This function will execute the campaign with the given name in a single-threaded manner. -# It will look for the campaign directory under the specified name and -# start the necessary processes. -run_campaign_re( - name_campaign, - r'^parameters_mss_d2d_to_imt_cross_border_0km_random_pointing_1beam_dl.yaml') From c69f30dc3f7cee5b9a4271a1500dfd86145cc051 Mon Sep 17 00:00:00 2001 From: Bruno Faria Date: Thu, 4 Dec 2025 11:45:17 -0300 Subject: [PATCH 53/77] update(docs): Removed docs related to campaigns --- ...lation campaign in the campaigns folder.md | 123 ------------------ 1 file changed, 123 deletions(-) delete mode 100644 docs/docusaurus/docs/How to/Create a simulation campaign in the campaigns folder.md diff --git a/docs/docusaurus/docs/How to/Create a simulation campaign in the campaigns folder.md b/docs/docusaurus/docs/How to/Create a simulation campaign in the campaigns folder.md deleted file mode 100644 index 1b66e3ae4..000000000 --- a/docs/docusaurus/docs/How to/Create a simulation campaign in the campaigns folder.md +++ /dev/null @@ -1,123 +0,0 @@ - -# Create a Simulation Campaign - ---- - -## 1. Setup the Environment - -### Step 1: Create the Campaign Folder -Choose a descriptive name for your campaign folder based on the simulation parameters. -- **Example:** `imt_hibs_ras_2600_MHz` - ---- - -### Step 2: Structure the Campaign Folder -Create the following subfolders to organize your files: - -- **`input` Folder:** - Stores all input configuration files. - - **Path:** `campaigns/imt_hibs_ras_2600_MHz/input/` - - **Contents:** `.yaml` files with simulation parameters. - -- **`output` Folder:** - SHARC saves all simulation results here. - - **Path:** `campaigns/imt_hibs_ras_2600_MHz/output/` - - **Contents:** Simulation data and visualizations. - -- **`scripts` Folder:** - Contains scripts for running simulations and analyzing results. - - **Path:** `campaigns/imt_hibs_ras_2600_MHz/scripts/` - - **Contents:** Python scripts for running or plotting results from campaigns. - ---- - -## 2. Configuring Your Simulation - -### Step 3: Create a Parameter File -Define your simulation parameters in a `.yaml` file. -- **Example File:** `parameters_hibs_ras_2600_MHz_0km.yaml` -- **Location:** `campaigns/imt_hibs_ras_2600_MHz/input/` - ---- - -### Step 4: Define Simulation Parameters in the `.yaml` File -Customize your simulation settings in the `.yaml` file. Key sections include: - -```yaml -general: - ########################################################################### - # Number of simulation snapshots - num_snapshots: 1000 - ########################################################################### - # IMT link that will be simulated (DOWNLINK or UPLINK) - imt_link: DOWNLINK - ########################################################################### - # The chosen system for sharing study - # EESS_PASSIVE, FSS_SS, FSS_ES, FS, RAS, - system: RAS - ########################################################################### -**(example)** -``` - ---- - -### Step 5: Create Multiple Simulation Configurations -To study different scenarios, create additional `.yaml` files in the `input` folder. -- **Examples:** - - `parameters_hibs_ras_2600_MHz_10km.yaml` - - `parameters_hibs_ras_2600_MHz_20km.yaml` - ---- - -## 3. Running Simulations - -### Step 6: Run the Simulations -In the `scripts` folder, create Python scripts to automate simulation execution. - -#### Multi-threaded Simulation -Run multiple simulations in parallel for efficiency. -- **Script:** `start_simulations_multi_thread.py` - -```python -from sharc.run_multiple_campaigns_mut_thread import run_campaign - -# Set the campaign name -# The name of the campaign to run. This should match the name of the campaign directory. -name_campaign = "imt_mss_ras_2600_MHz" - -# Run the campaigns -# This function will execute the campaign with the given name. -# It will look for the campaign directory under the specified name and start the necessary processes. -run_campaign(name_campaign) -**(example)** -``` - -#### Single-threaded Simulation -Run a single simulation for testing purposes. -- **Script:** `start_simulations_single_thread.py` - -```python -from sharc.run_multiple_campaigns import run_campaign - -# Set the campaign name -# The name of the campaign to run. This should match the name of the campaign directory. -name_campaign = "imt_mss_ras_2600_MHz" - -# Run the campaign in single-thread mode -# This function will execute the campaign with the given name in a single-threaded manner. -# It will look for the campaign directory under the specified name and start the necessary processes. -run_campaign(name_campaign) -**(example)** -``` - ---- - -## 4. Post-processing and Analyzing Results - -### Step 7: Post-process and Analyze Results -Create scripts to read the output data and generate visualizations. - -#### Example Plot Script: `plot_results.py` -This script reads simulation data and generates plots. - ---- From 8e346e4aa69af49ab47a8c65643dadd7f6edb2c0 Mon Sep 17 00:00:00 2001 From: artistrea Date: Thu, 4 Dec 2025 14:26:22 -0300 Subject: [PATCH 54/77] refactor: some comments and getters --- sharc/propagation/propagation_path.py | 6 +++++- sharc/satellite/ngso/orbit_model.py | 26 +++++++++++++++++++++++++- sharc/simulation.py | 5 +++-- sharc/simulation_downlink.py | 6 +++--- sharc/simulation_uplink.py | 6 +++--- sharc/station_factory.py | 11 ----------- sharc/support/geometry.py | 2 -- tests/test_propagation_path.py | 8 ++++---- 8 files changed, 43 insertions(+), 27 deletions(-) diff --git a/sharc/propagation/propagation_path.py b/sharc/propagation/propagation_path.py index 2eeb802a8..60d6637a1 100644 --- a/sharc/propagation/propagation_path.py +++ b/sharc/propagation/propagation_path.py @@ -85,6 +85,10 @@ def get_path_loss( sta_b_gains, ) + @property + def mask(self): + return self._mask + def calc_mask(self, *, deduplicate: bool): """Calculates and updates current mask and paths based on masking functions in instance. @@ -131,7 +135,7 @@ def from_masked_mtx(self, vec: np.ndarray): mtx = np.full(self._orig_shape, np.nan) expanded_vec = vec[self._deduped_paths_representative_node] - mtx[self._mask] = expanded_vec + mtx[self.mask] = expanded_vec return mtx diff --git a/sharc/satellite/ngso/orbit_model.py b/sharc/satellite/ngso/orbit_model.py index 92f598394..342c15ed6 100644 --- a/sharc/satellite/ngso/orbit_model.py +++ b/sharc/satellite/ngso/orbit_model.py @@ -341,9 +341,33 @@ def main(): print("Orbit parameters:") print(orbit_params) + param_file = ( + "/home/artistreak/projects/Radio-Spectrum/SHARC/sharc/campaigns/multiple_mss_dc_to_imt/input/parameter_multiple_mss_dc_to_imt_fr_40exclusion_0.1load_downlink_imt.upto-1GHz.single-bs.urban-macro-bs_system-4.698-960MHz-block2.690km.yaml" + ) + from sharc.parameters.parameters import Parameters + parameters = Parameters() + parameters.set_file_name(param_file) + parameters.read_params() # Instantiate the OrbitModel - orbit_model = OrbitModel(**orbit_params) + # orbit_model = OrbitModel(**orbit_params) + orbit_model_params = parameters.mss_d2d.orbits[0] + orbit_model = OrbitModel( + Nsp=orbit_model_params.sats_per_plane, # Satellites per plane + Np=orbit_model_params.n_planes, # Number of orbital planes + phasing=orbit_model_params.phasing_deg, # Phasing angle in degrees + long_asc=orbit_model_params.long_asc_deg, # Longitude of ascending node in degrees + omega=orbit_model_params.omega_deg, # Argument of perigee in degrees + delta=orbit_model_params.inclination_deg, # Orbital inclination in degrees + hp=orbit_model_params.perigee_alt_km, # Perigee altitude in kilometers + ha=orbit_model_params.apogee_alt_km, # Apogee altitude in kilometers + Mo=orbit_model_params.initial_mean_anomaly, # Initial mean anomaly in degrees + # whether to use only time as random variable + model_time_as_random_variable=orbit_model_params.model_time_as_random_variable, + t_min=orbit_model_params.t_min, + t_max=orbit_model_params.t_max, + ) + # Get satellite positions over time positions = orbit_model.get_satellite_positions_time_interval(n_periods=1) diff --git a/sharc/simulation.py b/sharc/simulation.py index 6d8910511..07e44a800 100644 --- a/sharc/simulation.py +++ b/sharc/simulation.py @@ -375,9 +375,10 @@ def calculate_coupling_loss_system_imt( sta_b_gains=sta_b_gains, ) - # TODO: remove gambiarra # Store antenna gains and path loss samples if self.param_system.channel_model == "HDFSS": + # TODO: standardized way of storing these results + # instead of just for HDFSS self.imt_system_build_entry_loss = path_loss[1] self.imt_system_diffraction_loss = path_loss[2] path_loss = path_loss[0] @@ -828,7 +829,7 @@ def add_system_imt_interaction_attr_to_results( if result_attr is None: result_attr = attr - sys_to_imt_paths_mask = self.paths_between_imt_and_sys._mask + sys_to_imt_paths_mask = self.paths_between_imt_and_sys.mask n_sys = self.system.num_stations assert v.shape[0] == n_sys diff --git a/sharc/simulation_downlink.py b/sharc/simulation_downlink.py index b6808d168..8b7693f19 100644 --- a/sharc/simulation_downlink.py +++ b/sharc/simulation_downlink.py @@ -172,7 +172,7 @@ def calculate_sinr(self): Calculates the downlink SINR for each UE. """ bs_active = np.where(self.bs.active)[0] - ue_to_bs_path_mask = self.intra_imt_paths._mask + ue_to_bs_path_mask = self.intra_imt_paths.mask bs_tx_power_array = np.stack([self.bs.tx_power[k] for k in sorted(self.bs.tx_power)]).flatten() for bs in bs_active: @@ -230,7 +230,7 @@ def calculate_sinr_ext(self): # applying a bandwidth scaling factor since UE transmits on a portion # of the satellite's bandwidth - ue_interferer_paths = self.paths_between_imt_and_sys._mask.T + ue_interferer_paths = self.paths_between_imt_and_sys.mask.T # All UEs are active on an active BS bs_active = np.where(self.bs.active)[0] @@ -538,7 +538,7 @@ def calculate_external_interference(self): ) rx_interference = 0 - bs_interferer_paths = self.paths_between_imt_and_sys._mask.T + bs_interferer_paths = self.paths_between_imt_and_sys.mask.T for bs in bs_active: system_interfering = np.where(bs_interferer_paths[bs])[0] active_beams = [ diff --git a/sharc/simulation_uplink.py b/sharc/simulation_uplink.py index 5ef052157..ebb307189 100644 --- a/sharc/simulation_uplink.py +++ b/sharc/simulation_uplink.py @@ -148,7 +148,7 @@ def calculate_sinr(self): """ # calculate uplink received power for each active BS bs_active = np.where(self.bs.active)[0] - bs_to_ue_path_mask = self.intra_imt_paths._mask.T + bs_to_ue_path_mask = self.intra_imt_paths.mask.T for bs in bs_active: ue = self.link[bs] @@ -208,7 +208,7 @@ def calculate_sinr_ext(self): ) bs_active = np.where(self.bs.active)[0] - bs_interferer_paths = self.paths_between_imt_and_sys._mask.T + bs_interferer_paths = self.paths_between_imt_and_sys.mask.T for bs in bs_active: system_interfering = np.where(bs_interferer_paths[bs])[0] @@ -374,7 +374,7 @@ def calculate_external_interference(self): rx_interference = 0 bs_active = np.where(self.bs.active)[0] - ue_interf_from_sys_paths_mask = self.paths_between_imt_and_sys._mask.T + ue_interf_from_sys_paths_mask = self.paths_between_imt_and_sys.mask.T for bs in bs_active: ues = self.link[bs] for ue in ues: diff --git a/sharc/station_factory.py b/sharc/station_factory.py index 1a3c32c7b..02384d4f2 100644 --- a/sharc/station_factory.py +++ b/sharc/station_factory.py @@ -2024,17 +2024,6 @@ def get_random_position(num_stas: int, import plotly.graph_objects as go - # fig.add_trace( - # go.Scatter3d( - # x=[0], - # y=[0], - # z=[0], - # mode='markers', - # marker=dict(size=3, color='black', opacity=1), - # showlegend=False - # ) - # ) - from sharc.support.geometry import plot_geom plot_geom(fig, imt_ue.geom) plot_geom(fig, imt_bs.geom, {"marker": dict(size=2, color='blue', opacity=1)}, True) diff --git a/sharc/support/geometry.py b/sharc/support/geometry.py index 71b77fdf9..6cf9e51fb 100644 --- a/sharc/support/geometry.py +++ b/sharc/support/geometry.py @@ -41,8 +41,6 @@ class GlobalGeometry(ABC): num_geometries: int - # TODO: remove this from here - # gambiarra_intersite_dist: float intersite_dist: float def setup( diff --git a/tests/test_propagation_path.py b/tests/test_propagation_path.py index 86c36cc79..eb3e42b55 100644 --- a/tests/test_propagation_path.py +++ b/tests/test_propagation_path.py @@ -297,8 +297,8 @@ def test_get_path_loss_in_propagations(self): ploss = ploss[0] expected_shape = (ue.num_stations, bs.num_stations) - expected_filled = np.stack(np.where(path._mask), axis=0) - expected_nans = np.stack(np.where(~path._mask), axis=0) + expected_filled = np.stack(np.where(path.mask), axis=0) + expected_nans = np.stack(np.where(~path.mask), axis=0) if ch_model == "INDOOR": # NOTE: current Indoor channel model implementaiton @@ -384,8 +384,8 @@ def test_get_path_loss_single_ss_vs_bs_in_propagations(self): ploss = ploss[0] expected_shape = (single_sta.num_stations, bs.num_stations) - expected_filled = np.stack(np.where(path._mask), axis=0) - expected_nans = np.stack(np.where(~path._mask), axis=0) + expected_filled = np.stack(np.where(path.mask), axis=0) + expected_nans = np.stack(np.where(~path.mask), axis=0) if ch_model == "INDOOR": # NOTE: current Indoor channel model implementaiton From 3f90cca264325b67098fda66594ec6a1654353ca Mon Sep 17 00:00:00 2001 From: artistrea Date: Thu, 4 Dec 2025 15:00:05 -0300 Subject: [PATCH 55/77] fix: linting --- sharc/satellite/ngso/orbit_model.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sharc/satellite/ngso/orbit_model.py b/sharc/satellite/ngso/orbit_model.py index 342c15ed6..841a30e4b 100644 --- a/sharc/satellite/ngso/orbit_model.py +++ b/sharc/satellite/ngso/orbit_model.py @@ -368,7 +368,6 @@ def main(): t_max=orbit_model_params.t_max, ) - # Get satellite positions over time positions = orbit_model.get_satellite_positions_time_interval(n_periods=1) From d37a16ae882dc1f6c86b122b56aa2e95b2376b6a Mon Sep 17 00:00:00 2001 From: artistrea Date: Thu, 4 Dec 2025 15:02:41 -0300 Subject: [PATCH 56/77] fix: linting again --- sharc/propagation/propagation_path.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sharc/propagation/propagation_path.py b/sharc/propagation/propagation_path.py index 60d6637a1..8504b3a77 100644 --- a/sharc/propagation/propagation_path.py +++ b/sharc/propagation/propagation_path.py @@ -87,6 +87,7 @@ def get_path_loss( @property def mask(self): + """Getter for _mask property""" return self._mask def calc_mask(self, *, deduplicate: bool): From 62bd0cabb43624154480d298fe9ceb38e8286358 Mon Sep 17 00:00:00 2001 From: artistrea Date: Fri, 5 Dec 2025 09:23:09 -0300 Subject: [PATCH 57/77] fix(SimulationDownlink): runtime error in calculate downlink sinr --- sharc/simulation_downlink.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sharc/simulation_downlink.py b/sharc/simulation_downlink.py index 8b7693f19..8d0410a27 100644 --- a/sharc/simulation_downlink.py +++ b/sharc/simulation_downlink.py @@ -173,8 +173,11 @@ def calculate_sinr(self): """ bs_active = np.where(self.bs.active)[0] ue_to_bs_path_mask = self.intra_imt_paths.mask - bs_tx_power_array = np.stack([self.bs.tx_power[k] for k in sorted(self.bs.tx_power)]).flatten() - + bs_tx_power_array = np.stack([ + self.bs.tx_power[k] if k in self.bs.tx_power + else np.full(self.parameters.imt.ue.k, -np.inf) + for k in range(self.bs.num_stations) + ]).flatten() for bs in bs_active: ue = self.link[bs] self.ue.rx_power[ue] = self.bs.tx_power[bs] - \ From 5c4c418e230ae9cadd02bb4c2817f9e4f9e35b4c Mon Sep 17 00:00:00 2001 From: artistrea Date: Fri, 5 Dec 2025 12:19:16 -0300 Subject: [PATCH 58/77] fix: DL intra imt interference --- sharc/simulation_downlink.py | 28 ++++++++++++++++++++-------- tests/test_simulation_downlink.py | 2 -- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/sharc/simulation_downlink.py b/sharc/simulation_downlink.py index 8d0410a27..58e9231be 100644 --- a/sharc/simulation_downlink.py +++ b/sharc/simulation_downlink.py @@ -172,22 +172,34 @@ def calculate_sinr(self): Calculates the downlink SINR for each UE. """ bs_active = np.where(self.bs.active)[0] + ue_k = self.parameters.imt.ue.k + + # paths that actually exist and should make signal or interf ue_to_bs_path_mask = self.intra_imt_paths.mask - bs_tx_power_array = np.stack([ + # since bs tx_power is dict, we get it to array for easier use + bs_tx_power_per_beam_array = np.array([ self.bs.tx_power[k] if k in self.bs.tx_power - else np.full(self.parameters.imt.ue.k, -np.inf) - for k in range(self.bs.num_stations) - ]).flatten() + else np.full(ue_k, -np.inf) + for k in range(self.bs.num_stations) + ]) for bs in bs_active: ue = self.link[bs] self.ue.rx_power[ue] = self.bs.tx_power[bs] - \ self.coupling_loss_imt[bs, ue] - # create a list with base stations that generate interference in for ui in ue: + # base stations that have links/paths to ue bs_interf = np.where(ue_to_bs_path_mask[ui])[0] - bs_interf = [bsi for bsi in bs_interf if bsi != bs] - - interference = bs_tx_power_array[bs_interf] - \ + # remove serving bs to get interferrers + bs_interf = bs_interf[bs_interf != bs] + # get each base station power per beam + interf_tx_pow = bs_tx_power_per_beam_array[bs_interf] + # get UE RB index + ue_rb_beam_idx = int(self.bs_to_ue_beam_rbs[ui]) + # only overlapping RB interf power is considered + interf_tx_pow = interf_tx_pow[:, ue_rb_beam_idx] + # coupling loss already considers only the overlapping beam + # from BS + interference = interf_tx_pow - \ self.coupling_loss_imt[bs_interf, ui] # sum 1e-50 so that rx_interference >= -500 diff --git a/tests/test_simulation_downlink.py b/tests/test_simulation_downlink.py index 4b351eb12..4a8c93f73 100644 --- a/tests/test_simulation_downlink.py +++ b/tests/test_simulation_downlink.py @@ -694,8 +694,6 @@ def test_simulation_2bs_4ue_ras(self): self.simulation.connect_ue_to_bs() self.simulation.select_ue(random_number_gen) self.simulation.link = {0: [0, 1], 1: [2, 3]} - self.simulation.select_ue(random_number_gen) - self.simulation.link = {0: [0, 1], 1: [2, 3]} self.simulation.coupling_loss_imt = self.simulation.calculate_intra_imt_coupling_loss( self.simulation.ue, self.simulation.bs, ) self.simulation.scheduler() From 1f2e3f6042af15474187cb42f91a745e9e1dc60e Mon Sep 17 00:00:00 2001 From: Bruno Faria Date: Thu, 11 Dec 2025 14:35:25 -0300 Subject: [PATCH 59/77] fix(utils): Fixed a potential overflow on ecef2lla --- sharc/satellite/utils/sat_utils.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sharc/satellite/utils/sat_utils.py b/sharc/satellite/utils/sat_utils.py index 97c37fdd1..8cbb0abcf 100644 --- a/sharc/satellite/utils/sat_utils.py +++ b/sharc/satellite/utils/sat_utils.py @@ -19,9 +19,11 @@ def ecef2lla(x: np.ndarray, y: np.ndarray, z: np.ndarray) -> tuple: tuple (lat, long, alt) lat long and altitude in spherical earth model """ - x = np.atleast_1d(x) - y = np.atleast_1d(y) - z = np.atleast_1d(z) + # Transform to kilometers to prevent overflow + x = np.atleast_1d(x).astype(float) + y = np.atleast_1d(y).astype(float) + z = np.atleast_1d(z).astype(float) + xy = np.sqrt(x**2 + y**2) lon = np.arccos(x / xy) From faa33b5f5302d4fc36c58df4cd43b6a03de798e6 Mon Sep 17 00:00:00 2001 From: Bruno Faria <33635129+brunohcfaria@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:30:17 -0300 Subject: [PATCH 60/77] Fixed typo in season error message. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- sharc/propagation/atmosphere.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sharc/propagation/atmosphere.py b/sharc/propagation/atmosphere.py index d4d382fb7..9d5208038 100644 --- a/sharc/propagation/atmosphere.py +++ b/sharc/propagation/atmosphere.py @@ -387,7 +387,7 @@ def get_reference_atmosphere_p835( h_km = altitude / 1000 season = season.lower() if season not in ['winter', 'summer']: - raise ValueError(f"Invalid season name f{season}.") + raise ValueError(f"Invalid season name {season}.") if latitude <= 22: # low latitude From 6d95bc52311aa977b37932945fc4e3440d8a3dce Mon Sep 17 00:00:00 2001 From: Bruno Faria <33635129+brunohcfaria@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:31:40 -0300 Subject: [PATCH 61/77] "visilibity" in the test docstring is misspelled "visilibity" in the test docstring is misspelled; it should be "visibility" to match the parameter name and improve clarity. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/test_topology_imt_mss_dc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_topology_imt_mss_dc.py b/tests/test_topology_imt_mss_dc.py index cfa669d96..69a715ca5 100644 --- a/tests/test_topology_imt_mss_dc.py +++ b/tests/test_topology_imt_mss_dc.py @@ -158,7 +158,7 @@ def test_visible_satellites(self): npt.assert_array_less(min_elevation_angle, xy_plane_elevations) def test_minimum_service_angle(self): - """Test minimum visilibity angle for service grid service.""" + """Test minimum visibility angle for service grid service.""" orbit = ParametersOrbit( n_planes=1, sats_per_plane=1, From e2b9011108b490ddf0630331f55fe0e91e7a4a0d Mon Sep 17 00:00:00 2001 From: Bruno Faria <33635129+brunohcfaria@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:32:50 -0300 Subject: [PATCH 62/77] Update tests/parameters/test_parameters.py These two consecutive assertions both check major_minor_axis_ratio against 1.0, which is redundant and makes the test harder to maintain; one of them can be removed without changing test behavior. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/parameters/test_parameters.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/parameters/test_parameters.py b/tests/parameters/test_parameters.py index b959acfb9..e5fd967d2 100644 --- a/tests/parameters/test_parameters.py +++ b/tests/parameters/test_parameters.py @@ -884,11 +884,6 @@ def test_parameters_single_space_station(self): 1.0 ) - self.assertEqual( - self.parameters.single_space_station.antenna.itu_r_s_1528.major_minor_axis_ratio, - 1.0 - ) - self.assertEqual( self.parameters.single_space_station.antenna.itu_r_s_1528.far_out_side_lobe, -25 From 14411458b25d9fdbf7fc0b764fc7184575bfb703 Mon Sep 17 00:00:00 2001 From: fabricio Date: Fri, 30 Jan 2026 10:27:23 -0300 Subject: [PATCH 63/77] update(antenna): Added antenna F.1245 used by FS system --- sharc/antenna/antenna_f1245_fs.py | 196 +++++++++++++++++++++++++ sharc/antenna/antenna_factory.py | 3 + sharc/parameters/parameters_antenna.py | 22 ++- sharc/station_factory.py | 3 + 4 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 sharc/antenna/antenna_f1245_fs.py diff --git a/sharc/antenna/antenna_f1245_fs.py b/sharc/antenna/antenna_f1245_fs.py new file mode 100644 index 000000000..de554a19d --- /dev/null +++ b/sharc/antenna/antenna_f1245_fs.py @@ -0,0 +1,196 @@ + +# -*- coding: utf-8 -*- +""" +Created on Wed Apr 4 17:08:00 2018 +@author: Calil +""" +import matplotlib.pyplot as plt +from sharc.antenna.antenna import Antenna +from sharc.parameters.imt.parameters_imt import ParametersImt +from sharc.parameters.imt.parameters_imt import ParametersImt +from sharc.parameters.imt.parameters_antenna_imt import ParametersAntennaImt +from sharc.parameters.parameters_antenna import ParametersAntenna +import numpy as np +import math + + +class Antenna_f1245_fs(Antenna): + """Class that implements the ITU-R F.1245 antenna pattern for fixed + satellite service earth stations.""" + + def __init__(self, param: ParametersImt): + super().__init__() + self.peak_gain = param.gain + lmbda = 3e8 / (param.frequency * 1e6) + self.d_lmbda = param.diameter / lmbda + self.g_l = 2 + 15 * math.log10(self.d_lmbda) + self.phi_m = (20 / self.d_lmbda) * math.sqrt(self.peak_gain - self.g_l) + self.phi_r = 12.02 * math.pow(self.d_lmbda, -0.6) + + def calculate_gain(self, *args, **kwargs) -> np.array: + """ + Calculate the antenna gain for the given parameters. + + Parameters + ---------- + *args : tuple + Positional arguments (not used). + **kwargs : dict + Keyword arguments, expects 'phi_vec', 'theta_vec', and 'beams_l'. + + Returns + ------- + np.array + Calculated antenna gain values. + """ + # phi_vec = np.absolute(kwargs["phi_vec"]) + # theta_vec = np.absolute(kwargs["theta_vec"]) + # beams_l = np.absolute(kwargs["beams_l"]) + off_axis = np.absolute(kwargs["off_axis_angle_vec"]) + if self.d_lmbda > 100: + gain = self.calculate_gain_greater(off_axis) + else: + gain = self.calculate_gain_less(off_axis) + # idx_max_gain = np.where(beams_l == -1)[0] + # gain = self.peak_gain + return gain + + def calculate_gain_greater(self, phi: float) -> np.array: + """ + For frequencies in the range 1 GHz to about 70 GHz, in cases where the + ratio between the antenna diameter and the wavelength is GREATER than + 100, this method should be used. + Parameter + --------- + phi : off-axis angle [deg] + Returns + ------- + a numpy array containing the gains in the given angles + """ + gain = np.zeros(phi.shape) + idx_0 = np.where(phi < self.phi_m)[0] + gain[idx_0] = self.peak_gain - 2.5e-3 * \ + np.power(self.d_lmbda * phi[idx_0], 2) + phi_thresh = max(self.phi_m, self.phi_r) + idx_1 = np.where((self.phi_m <= phi) & (phi < phi_thresh))[0] + gain[idx_1] = self.g_l + idx_2 = np.where((phi_thresh <= phi) & (phi < 48))[0] + gain[idx_2] = 29 - 25 * np.log10(phi[idx_2]) + idx_3 = np.where((48 <= phi) & (phi <= 180))[0] + gain[idx_3] = -13 + return gain + + def calculate_gain_less(self, phi: float) -> np.array: + """ + For frequencies in the range 1 GHz to about 70 GHz, in cases where the + ratio between the antenna diameter and the wavelength is LESS than + or equal to 100, this method should be used. + Parameter + --------- + phi : off-axis angle [deg] + Returns + ------- + a numpy array containing the gains in the given angles + """ + gain = np.zeros(phi.shape) + idx_0 = np.where(phi < self.phi_m)[0] + gain[idx_0] = self.peak_gain - 0.0025 * \ + np.power(self.d_lmbda * phi[idx_0], 2) + idx_1 = np.where((self.phi_m <= phi) & (phi < 48))[0] + gain[idx_1] = 39 - 5 * \ + math.log10(self.d_lmbda) - 25 * np.log10(phi[idx_1]) + idx_2 = np.where((48 <= phi) & (phi < 180))[0] + gain[idx_2] = -3 - 5 * math.log10(self.d_lmbda) + return gain + + def add_beam(self, phi: float, theta: float): + """ + Add a new beam to the antenna. + + Parameters + ---------- + phi : float + Azimuth angle in degrees. + theta : float + Elevation angle in degrees. + """ + self.beams_list.append((phi, theta)) + + def calculate_off_axis_angle(self, Az, b): + """ + Calculate the off-axis angle between the main beam and a given direction. + + Parameters + ---------- + Az : float or np.array + Azimuth angle(s) in degrees. + b : float or np.array + Elevation angle(s) in degrees. + + Returns + ------- + float or np.array + Off-axis angle(s) in degrees. + """ + Az0 = self.beams_list[0][0] + a = 90 - self.beams_list[0][1] + C = Az0 - Az + off_axis_rad = np.arccos( + np.cos( + np.radians(a)) * + np.cos( + np.radians(b)) + + np.sin( + np.radians(a)) * + np.sin( + np.radians(b)) * + np.cos( + np.radians(C)), + ) + off_axis_deg = np.degrees(off_axis_rad) + return off_axis_deg + + +if __name__ == '__main__': + off_axis_angle_vec = np.linspace(0.1, 180, num=1001) + # initialize antenna parameters + param = ParametersAntenna() + param.frequency = 2155 + param_gt = ParametersAntennaImt() + param.gain = 33.1 + param.diameter = 2 + antenna_gt = Antenna_f1245_fs(param) + antenna_gt.add_beam(0, 0) + gain_gt = antenna_gt.calculate_gain( + off_axis_angle_vec=off_axis_angle_vec, + ) + param.diameter = 3 + antenna_gt = Antenna_f1245_fs(param) + gain_gt_3 = antenna_gt.calculate_gain( + off_axis_angle_vec=off_axis_angle_vec, + ) + param.diameter = 1.8 + antenna_gt = Antenna_f1245_fs(param) + gain_gt_18 = antenna_gt.calculate_gain( + off_axis_angle_vec=off_axis_angle_vec, + ) + + fig = plt.figure( + figsize=(8, 7), facecolor='w', + edgecolor='k', + ) # create a figure object + plt.semilogx(off_axis_angle_vec, gain_gt, "-b", label="$f = 10.7$ $GHz,$ $D = 2$ $m$") + plt.semilogx(off_axis_angle_vec, gain_gt_3, "-y", label="$f = 10.7$ $GHz,$ $D = 3$ $m$") + plt.semilogx(off_axis_angle_vec, gain_gt_18, "-g", label="$f = 10.7$ $GHz,$ $D = 1.8$ $m$") + + plt.title("ITU-R F.1245 antenna radiation pattern") + plt.xlabel(r"Off-axis angle $\phi$ [deg]") + plt.ylabel("Gain relative to $G_m$ [dB]") + plt.legend(loc="lower left") + # plt.xlim((phi[0], phi[-1])) + plt.ylim((-20, 50)) + # ax = plt.gca() + # ax.set_yticks([-30, -20, -10, 0]) + # ax.set_xticks(np.linspace(1, 9, 9).tolist() + np.linspace(10, 100, 10).tolist()) + plt.grid() + plt.show() diff --git a/sharc/antenna/antenna_factory.py b/sharc/antenna/antenna_factory.py index 051e1f332..aed067148 100644 --- a/sharc/antenna/antenna_factory.py +++ b/sharc/antenna/antenna_factory.py @@ -12,6 +12,7 @@ from sharc.antenna.antenna_s580 import AntennaS580 from sharc.antenna.antenna_s1528 import AntennaS1528 from sharc.antenna.antenna_s1855 import AntennaS1855 +from sharc.antenna.antenna_f1245_fs import Antenna_f1245_fs from sharc.antenna.antenna_s1528 import AntennaS1528, AntennaS1528Leo, AntennaS1528Taylor from sharc.antenna.antenna_beamforming_imt import AntennaBeamformingImt @@ -49,6 +50,8 @@ def create_antenna( return AntennaS465(antenna_params.itu_r_s_465_modified) case "ITU-R S.1855": return AntennaS1855(antenna_params.itu_r_s_1855) + case "ITU-R F.1245_fs": + return Antenna_f1245_fs(antenna_params.itu_r_f_1245_fs) case "ITU-R Reg. RR. Appendice 7 Annex 3": return AntennaReg_RR_A7_3(antenna_params.itu_reg_rr_a7_3) case "MSS Adjacent": diff --git a/sharc/parameters/parameters_antenna.py b/sharc/parameters/parameters_antenna.py index a8fc8a0e0..7de9b4cc9 100644 --- a/sharc/parameters/parameters_antenna.py +++ b/sharc/parameters/parameters_antenna.py @@ -29,7 +29,8 @@ class ParametersAntenna(ParametersBase): "ITU-R-S.1528-Taylor", "ITU-R-S.1528-Section1.2", "ITU-R-S.1528-LEO", - "MSS Adjacent"] + "MSS Adjacent", + "ITU-R F.1245_fs"] # chosen antenna radiation pattern pattern: typing.Literal["OMNI", @@ -44,7 +45,8 @@ class ParametersAntenna(ParametersBase): "ITU-R-S.1528-Taylor", "ITU-R-S.1528-Section1.2", "ITU-R-S.1528-LEO", - "MSS Adjacent"] = None + "MSS Adjacent", + "ITU-R F.1245_fs"] = None # antenna gain [dBi] gain: float = None @@ -94,6 +96,20 @@ class ParametersAntenna(ParametersBase): default_factory=ParametersAntennaS672, ) + @dataclass + class ParametersAntennaRF1245(ParametersBase): + gain: float = -25 + diameter: float = None + frequency: float = None + + def validate(self, ctx): + if None in [self.gain, self.diameter, self.frequency]: + raise ValueError(f"{ctx}.antenna_3_dB should be set to a number") + + itu_r_f_1245_fs: ParametersAntennaRF1245 = field( + default_factory=ParametersAntennaRF1245, + ) + def set_external_parameters(self, **kwargs): """ Set external parameters for all sub-parameters of the antenna. @@ -198,6 +214,8 @@ def validate(self, ctx): self.itu_r_s_672.validate(f"{ctx}.itu_r_s_672") case "MSS Adjacent": self.mss_adjacent.validate(f"{ctx}.mss_adjacent") + case "ITU-R F.1245_fs": + self.itu_r_f_1245_fs.validate(f"{ctx}.itu_r_f_1245_fs") case _: raise NotImplementedError( "ParametersAntenna.validate does not implement this antenna validation!", ) diff --git a/sharc/station_factory.py b/sharc/station_factory.py index 02384d4f2..3334488ff 100644 --- a/sharc/station_factory.py +++ b/sharc/station_factory.py @@ -50,6 +50,7 @@ from sharc.antenna.antenna_s672 import AntennaS672 from sharc.antenna.antenna_s1528 import AntennaS1528 from sharc.antenna.antenna_s1855 import AntennaS1855 +from sharc.antenna.antenna_f1245_fs import Antenna_f1245_fs from sharc.antenna.antenna_s1528 import AntennaS1528, AntennaS1528Leo, AntennaS1528Taylor from sharc.antenna.antenna_beamforming_imt import AntennaBeamformingImt from sharc.topology.topology import Topology @@ -1327,6 +1328,8 @@ def generate_fs_station(param: ParametersFs): fs_station.antenna = np.array([AntennaOmni(param.antenna_gain)]) elif param.antenna_pattern == "ITU-R F.699": fs_station.antenna = np.array([AntennaF699(param)]) + elif param.antenna_pattern == "ITU-R F.1245_fs": + fs_station.antenna == np.array([Antenna_f1245_fs(param)]) else: sys.stderr.write( "ERROR\nInvalid FS antenna pattern: " + param.antenna_pattern, From 0ff32ce20d89fd5bdcea62ae53374f6401a6fcbe Mon Sep 17 00:00:00 2001 From: fabricio Date: Fri, 30 Jan 2026 10:39:40 -0300 Subject: [PATCH 64/77] fix(build): Fixed syntax errors --- sharc/parameters/parameters_antenna.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sharc/parameters/parameters_antenna.py b/sharc/parameters/parameters_antenna.py index 7de9b4cc9..737110781 100644 --- a/sharc/parameters/parameters_antenna.py +++ b/sharc/parameters/parameters_antenna.py @@ -105,7 +105,7 @@ class ParametersAntennaRF1245(ParametersBase): def validate(self, ctx): if None in [self.gain, self.diameter, self.frequency]: raise ValueError(f"{ctx}.antenna_3_dB should be set to a number") - + itu_r_f_1245_fs: ParametersAntennaRF1245 = field( default_factory=ParametersAntennaRF1245, ) From 57f18445d60fa193b985e4ef87503c6bd8bb8cda Mon Sep 17 00:00:00 2001 From: fabricio Date: Fri, 30 Jan 2026 11:01:07 -0300 Subject: [PATCH 65/77] fix(build): Added Docstring --- sharc/parameters/parameters_antenna.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/sharc/parameters/parameters_antenna.py b/sharc/parameters/parameters_antenna.py index 737110781..98ebbc258 100644 --- a/sharc/parameters/parameters_antenna.py +++ b/sharc/parameters/parameters_antenna.py @@ -98,11 +98,36 @@ class ParametersAntenna(ParametersBase): @dataclass class ParametersAntennaRF1245(ParametersBase): + """ + Parameters for ITU-R F.1245 antenna model. It's commonly used + for fixed service antennas. + + Paremeters + ---------- + gain : float, default=-25 + Antenna gain in dB. + diameter : float, optional + Antenna diameter in meters. + frequency : float, optional + Operating frequency. + """ gain: float = -25 diameter: float = None frequency: float = None def validate(self, ctx): + """ + Validate the antenna parameters for correctness. + + Parameters + ---------- + ctx : str + Context string for error messages. + Raises + ------ + ValueError + If any parameter is invalid. + """ if None in [self.gain, self.diameter, self.frequency]: raise ValueError(f"{ctx}.antenna_3_dB should be set to a number") From 8986a18095e70033fff4fa76a6e0ba6c8b94e35b Mon Sep 17 00:00:00 2001 From: artistrea Date: Fri, 30 Jan 2026 15:29:48 -0300 Subject: [PATCH 66/77] wip: optimized M.2101 array antenna implementation --- sharc/antenna/antenna_array.py | 318 +++++++++++++++++++++++++++++ tests/test_antenna_array.py | 361 +++++++++++++++++++++++++++++++++ 2 files changed, 679 insertions(+) create mode 100644 sharc/antenna/antenna_array.py create mode 100644 tests/test_antenna_array.py diff --git a/sharc/antenna/antenna_array.py b/sharc/antenna/antenna_array.py new file mode 100644 index 000000000..582633beb --- /dev/null +++ b/sharc/antenna/antenna_array.py @@ -0,0 +1,318 @@ +""" +This antenna was created after the already existing antenna_beamforming_imt +since that implementation is too slow for use with a lot of stations. + +This is supposed to be a faster implementation, and substitute the previous one +in a future release +""" + +from sharc.antenna.antenna import Antenna +from sharc.parameters.imt.parameters_antenna_imt import ParametersAntennaImt +from sharc.support.geometry import RigidTransform + +import numpy as np + + +class AntennaArray(Antenna): + # par: ParametersAntennaImt + + def __init__( + self, + par: ParametersAntennaImt, + # global2local_transform: RigidTransform + ): + super().__init__() + self.par = par + + def calculate_gain(self, *args, **kwargs) -> np.array: + """ + Calculates the antenan gain. + """ + phi_vec = np.atleast_1d(kwargs["phi_vec"]) + theta_vec = np.atleast_1d(kwargs["theta_vec"]) + co_channel = kwargs.get("co_channel", True) + adj_antenna_model = ( + self.par.adjacent_antenna_model == "SINGLE_ELEMENT" + and not co_channel + ) + if "beams_l" in kwargs.keys(): + beam_idxs = np.asarray(kwargs["beams_l"], dtype=int) + else: + beam_idxs = np.arange(len(phi_vec)) + + assert phi_vec.shape == (len(phi_vec),) + assert theta_vec.shape == (len(theta_vec),) + phi_vec, theta_vec = self._to_local_coord( + phi_vec, + theta_vec, + ) + + el_g = self._element_gain( + phi_vec, theta_vec, + ) + assert el_g.shape == theta_vec.shape + + if adj_antenna_model: + return el_g + + ar_g = self._array_gain( + phi_vec, theta_vec, beam_idxs + ) + assert ar_g.shape == theta_vec.shape + + return ar_g + el_g + + def _element_gain( + self, phi: np.ndarray, theta: np.ndarray + ): + return self._element_gain_dispatch( + self.par, phi, theta, + ) + + def _array_gain( + self, + phi: np.ndarray, theta: np.ndarray, + beam_idxs: np.ndarray + ): + v_vec = self._super_position_vector( + phi, theta, + self.par.n_rows, self.par.n_columns, + self.par.element_vert_spacing, + self.par.element_horiz_spacing, + ) + if len(self.beams_list) == 0: + beam_phi, beam_theta = phi, theta + else: + beam_phi, beam_theta = np.array(self.beams_list).T + + beam_etilt = beam_theta - 90. + beams_w_vec = self._weight_vector( + beam_phi, beam_etilt, + self.par.n_rows, self.par.n_columns, + self.par.element_vert_spacing, + self.par.element_horiz_spacing, + ) + w_vec = beams_w_vec[beam_idxs] + + g = 10 * np.log10( + abs( + np.sum(v_vec * w_vec, axis=(1, 2)) + )**2 + ) + + return g + + @staticmethod + def _weight_vector( + phi_tilt: np.ndarray, + theta_tilt: np.ndarray, + n_rows: int, n_cols: int, + dv: float, dh: float, + ) -> np.array: + """ + Calculates super position vector. + Angles are in the local coordinate system. + + Parameters + ---------- + phi_tilt (float): electrical horizontal steering [degrees] + theta_tilt (float): electrical down-tilt steering [degrees] + + Returns + ------- + w_vec (np.array): weighting vector + """ + # shape (Na, 1, 1) + r_phi = np.atleast_1d( + np.deg2rad(phi_tilt) + )[:, np.newaxis, np.newaxis] + r_theta = np.atleast_1d( + np.deg2rad(theta_tilt) + )[:, np.newaxis, np.newaxis] + + # shape (1, Nr, 1) + n = np.arange(n_rows)[np.newaxis, :, np.newaxis] + 1 + # shape (1, 1, Nc) + m = np.arange(n_cols)[np.newaxis, np.newaxis, :] + 1 + + exp_arg = (n - 1) * dv * np.sin(r_theta) - \ + (m - 1) * dh * np.cos(r_theta) * np.sin(r_phi) + + w_vec = (1 / np.sqrt(n_rows * n_cols)) *\ + np.exp(2 * np.pi * 1.0j * exp_arg) + + # shape (Na, Nr, Nc) + return w_vec + + @staticmethod + def _super_position_vector( + phi: float, theta: float, + n_rows: int, n_cols: int, + dv: float, dh: float, + ) -> np.array: + """ + Calculates super position vector. + Angles are in the local coordinate system. + + Parameters + ---------- + theta (float): elevation angle [degrees] + phi (float): azimuth angle [degrees] + + Returns + ------- + v_vec (np.array): superposition vector + """ + # shape (Na, 1, 1) + r_phi = np.atleast_1d( + np.deg2rad(phi) + )[:, np.newaxis, np.newaxis] + r_theta = np.atleast_1d( + np.deg2rad(theta) + )[:, np.newaxis, np.newaxis] + + # shape (1, Nr, 1) + n = np.arange(n_rows)[np.newaxis, :, np.newaxis] + 1 + # shape (1, 1, Nc) + m = np.arange(n_cols)[np.newaxis, np.newaxis, :] + 1 + + exp_arg = (n - 1) * dv * np.cos(r_theta) + \ + (m - 1) * dh * np.sin(r_theta) * np.sin(r_phi) + + v_vec = np.exp(2 * np.pi * 1.0j * exp_arg) + + # shape (Na, Nr, Nc) + return v_vec + + @staticmethod + def _element_gain_dispatch(par: ParametersAntennaImt, phi, theta): + if par.element_pattern == "M2101": + return AntennaArray._calculate_m2101_element_gain( + phi, theta, + par.element_phi_3db, par.element_theta_3db, + par.element_max_g, par.element_sla_v, par.element_am, + par.multiplication_factor, + ) + else: + raise NotImplementedError( + "No implementation done for element_pattern" + f"='{par.element_pattern}'" + ) + + @staticmethod + def _calculate_m2101_element_gain( + phi: np.ndarray, theta: np.ndarray, + phi_3db: np.ndarray, theta_3db: np.ndarray, + g_max: np.ndarray, sla_v: np.ndarray, am: np.ndarray, + multiplication_factor: np.ndarray = 12 + ): + """Calculates and returns element gain as described in M.2101 + """ + g_horizontal = -1.0 * np.minimum( + multiplication_factor * (phi / phi_3db)**2, am + ) + g_vertical = -1.0 * np.minimum( + multiplication_factor * ((theta-90.) / theta_3db)**2, sla_v + ) + + att = -1.0 * ( + g_horizontal + + g_vertical + ) + + return g_max - np.minimum(att, am) + + def _to_local_coord(self, phi, theta): + return np.array(phi), np.array(theta) + + def add_beam(self, phi_etilt: float, theta_etilt: float): + """ + Add new beam to antenna. + Does not receive angles in local coordinate system. + Theta taken with z axis as reference. + + Parameters + ---------- + phi_etilt (float): azimuth electrical tilt angle [degrees] + theta_etilt (float): elevation electrical tilt angle [degrees] + """ + # phi_etilt, theta_etilt = np.atleast_1d(phi_etilt), np.atleast_1d(theta_etilt) + phi, theta = self._to_local_coord(phi_etilt, theta_etilt) + self.beams_list.append( + (np.ndarray.item(phi), np.ndarray.item(theta)), + ) + + +if __name__ == "__main__": + antenna_params = ParametersAntennaImt() + antenna_params.adjacent_antenna_model = "SINGLE_ELEMENT" + antenna_params.normalization = False + antenna_params.minimum_array_gain = -200 + + antenna_params.element_pattern = "M2101" + antenna_params.element_max_g = 6.5 + antenna_params.element_phi_3db = 65 + antenna_params.element_theta_3db = 90 + antenna_params.element_am = 30 + antenna_params.element_sla_v = 30 + antenna_params.n_rows = 8 + antenna_params.n_columns = 8 + antenna_params.element_horiz_spacing = 0.5 + antenna_params.element_vert_spacing = 0.5 + antenna_params.multiplication_factor = 12 + + par = antenna_params.get_antenna_parameters() + antenna = AntennaArray(par) + + antenna.add_beam(np.array(0.), np.array(90.)) + + phi_scan = np.linspace(-180., 180., num=360) + theta = np.zeros_like(phi_scan) + 90. + + # gain = antenna._element_gain( + # phi_scan, + # theta, + # ) + gain = antenna.calculate_gain( + phi_vec=phi_scan, + theta_vec=theta, + beams_l=np.zeros_like(phi_scan), + ) + + import matplotlib.pyplot as plt + + fig = plt.figure(figsize=(15, 5), facecolor='w', edgecolor='k') + ax1 = fig.add_subplot(121) + + ax1.plot(phi_scan, gain) + top_y_lim = np.ceil(np.max(gain) / 10) * 10 + ax1.set_xlim(-180, 180) + ax1.set_ylim(top_y_lim - 60, top_y_lim) + ax1.grid(True) + ax1.set_xlabel(r"$\varphi$ [deg]") + ax1.set_ylabel("Gain [dBi]") + + theta_scan = np.linspace(0., 180., num=360) + phi = np.zeros_like(theta_scan) + + # gain = antenna._element_gain( + # phi, + # theta_scan, + # ) + gain = antenna.calculate_gain( + phi_vec=phi, + theta_vec=theta_scan, + beams_l=np.zeros_like(theta_scan), + ) + # fig = plt.figure(figsize=(15, 5), facecolor='w', edgecolor='k') + ax2 = fig.add_subplot(122, sharey=ax1) + + ax2.plot(theta_scan, gain) + top_y_lim = np.ceil(np.max(gain) / 10) * 10 + ax2.set_xlim(0, 180.) + ax2.set_ylim(top_y_lim - 60, top_y_lim) + ax2.grid(True) + ax2.set_xlabel(r"$\vartheta$ [deg]") + ax2.set_ylabel("Gain [dBi]") + + plt.show() diff --git a/tests/test_antenna_array.py b/tests/test_antenna_array.py new file mode 100644 index 000000000..adaf1291c --- /dev/null +++ b/tests/test_antenna_array.py @@ -0,0 +1,361 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat Apr 15 15:36:22 2017 + +@author: Calil +""" + +import unittest +import numpy as np +import numpy.testing as npt + +from sharc.antenna.antenna_array import AntennaArray +from sharc.parameters.imt.parameters_antenna_imt import ParametersAntennaImt + + +class AntennaArrayTest(unittest.TestCase): + """Unit tests for the AntennaArray class.""" + + def setUp(self): + """Set up test fixtures for AntennaArray tests.""" + # Array parameters + self.bs_param = ParametersAntennaImt() + self.ue_param = ParametersAntennaImt() + + # NOTE: not implemented: + self.bs_param.adjacent_antenna_model = "SINGLE_ELEMENT" + self.bs_param.normalization = False + self.bs_param.normalization_file = None + self.ue_param.adjacent_antenna_model = "SINGLE_ELEMENT" + self.ue_param.normalization = False + self.ue_param.normalization_file = None + + self.bs_param.element_pattern = "M2101" + self.bs_param.minimum_array_gain = -200 + self.bs_param.downtilt = 0 + self.bs_param.element_max_g = 5 + self.bs_param.element_phi_3db = 80 + self.bs_param.element_theta_3db = 60 + self.bs_param.element_am = 30 + self.bs_param.element_sla_v = 30 + self.bs_param.n_rows = 16 + self.bs_param.n_columns = 16 + self.bs_param.element_horiz_spacing = 1 + self.bs_param.element_vert_spacing = 1 + self.bs_param.multiplication_factor = 12 + + self.ue_param.element_pattern = "M2101" + self.ue_param.minimum_array_gain = -200 + self.ue_param.element_max_g = 10 + self.ue_param.element_phi_3db = 75 + self.ue_param.element_theta_3db = 65 + self.ue_param.element_am = 25 + self.ue_param.element_sla_v = 35 + self.ue_param.n_rows = 2 + self.ue_param.n_columns = 2 + self.ue_param.element_horiz_spacing = 0.5 + self.ue_param.element_vert_spacing = 0.5 + self.ue_param.multiplication_factor = 12 + # Create antenna objects + par = self.bs_param.get_antenna_parameters() + self.antenna1 = AntennaArray(par) + par = self.ue_param.get_antenna_parameters() + self.antenna2 = AntennaArray(par) + + def test_element_gain(self): + """Testing element gain calculations""" + # self.antenna1._calculate_m2101_element_gain() + """Test M.2101 horizontal pattern calculation for various phi values.""" + # phi = 0 results in zero gain + phi = np.array([0., 120., 150.]) + theta = np.zeros_like(phi) + 90. + h_att = self.antenna1._element_gain(phi, theta) + npt.assert_equal( + h_att, + self.antenna1.par.element_max_g - np.array([0.0, 27.0, 30.0]) + ) + + """Test M.2101 vertical pattern calculation for various theta values.""" + theta = np.array([90, 180, 210]) + phi = np.zeros_like(theta) + v_att = self.antenna1._element_gain(phi, theta) + npt.assert_equal( + v_att, + self.antenna1.par.element_max_g - np.array([0.0, 27.0, 30.0]) + ) + + """Test element pattern calculation for various phi/theta values.""" + phi = np.array([0, 80, 150]) + theta = np.array([90, 150, 210]) + e_gain = self.antenna1._element_gain(phi, theta) + npt.assert_equal( + e_gain, + np.array([5.0, -19.0, -25.0]) + ) + + def test_weight_vector(self): + """Test calculation of the weight vector for beamforming.""" + # Error margin + eps = 1e-5 + + acc_phi_scan = [] + acc_theta_tilt = [] + acc_w_vec = [] + # Test 1 + phi_scan = 0 + theta_tilt = 0 + w_vec = self.antenna2._weight_vector( + phi_scan, theta_tilt, + self.antenna2.par.n_rows, self.antenna2.par.n_columns, + self.antenna2.par.element_vert_spacing, + self.antenna2.par.element_horiz_spacing, + ) + acc_phi_scan.append(phi_scan) + acc_theta_tilt.append(theta_tilt) + acc_w_vec.append(w_vec) + expected_w_vec = np.array([[[0.5, 0.5], [0.5, 0.5]]]) + npt.assert_allclose( + w_vec, + expected_w_vec, rtol=eps, + ) + + # Test 2 + phi_scan = 90 + theta_tilt = 90 + w_vec = self.antenna2._weight_vector( + phi_scan, theta_tilt, + self.antenna2.par.n_rows, self.antenna2.par.n_columns, + self.antenna2.par.element_vert_spacing, + self.antenna2.par.element_horiz_spacing, + ) + acc_phi_scan.append(phi_scan) + acc_theta_tilt.append(theta_tilt) + acc_w_vec.append(w_vec) + expected_w_vec = np.array([[[0.5, 0.5], [-0.5, -0.5]]]) + npt.assert_allclose( + w_vec, + expected_w_vec, rtol=eps, + ) + + # Test 3 + phi_scan = 45 + theta_tilt = 45 + w_vec = self.antenna2._weight_vector( + phi_scan, theta_tilt, + self.antenna2.par.n_rows, self.antenna2.par.n_columns, + self.antenna2.par.element_vert_spacing, + self.antenna2.par.element_horiz_spacing, + ) + acc_phi_scan.append(phi_scan) + acc_theta_tilt.append(theta_tilt) + acc_w_vec.append(w_vec) + expected_w_vec = np.array([[ + [0.5 + 0.0j, 0.0 - 0.5j], + [-0.3028499 + 0.3978466j, 0.3978466 + 0.3028499j], + ]]) + npt.assert_allclose( + w_vec, + expected_w_vec, rtol=eps, + ) + + # Test 4 + phi_scan = 0 + theta_tilt = 90 + w_vec = self.antenna2._weight_vector( + phi_scan, theta_tilt, + self.antenna2.par.n_rows, self.antenna2.par.n_columns, + self.antenna2.par.element_vert_spacing, + self.antenna2.par.element_horiz_spacing, + ) + acc_phi_scan.append(phi_scan) + acc_theta_tilt.append(theta_tilt) + acc_w_vec.append(w_vec) + expected_w_vec = np.array([[[0.5, 0.5], [-0.5, -0.5]]]) + npt.assert_allclose( + w_vec, + expected_w_vec, rtol=eps, + ) + + # Test 5 + phi_scan = 45 + theta_tilt = 30 + w_vec = self.antenna2._weight_vector( + phi_scan, theta_tilt, + self.antenna2.par.n_rows, self.antenna2.par.n_columns, + self.antenna2.par.element_vert_spacing, + self.antenna2.par.element_horiz_spacing, + ) + acc_phi_scan.append(phi_scan) + acc_theta_tilt.append(theta_tilt) + acc_w_vec.append(w_vec) + expected_w_vec = np.array([[ + [0.5 + 0.0j, -0.172870 - 0.469169j], + [0.0 + 0.5j, 0.469165 - 0.172870j], + ]]) + npt.assert_allclose( + w_vec, + expected_w_vec, rtol=eps, + ) + + acc_phi_scan = np.array(acc_phi_scan) + acc_theta_tilt = np.array(acc_theta_tilt) + acc_w_vec = np.array(acc_w_vec) + w_vec = self.antenna2._weight_vector( + acc_phi_scan, acc_theta_tilt, + self.antenna2.par.n_rows, self.antenna2.par.n_columns, + self.antenna2.par.element_vert_spacing, + self.antenna2.par.element_horiz_spacing, + ) + expected_w_vec = np.squeeze(acc_w_vec, axis=1) + npt.assert_allclose( + w_vec, + expected_w_vec, rtol=eps, + ) + + def test_super_position_vector(self): + """Test calculation of the superposition vector.""" + # Error margin + eps = 1e-5 + + acc_phi = [] + acc_theta = [] + acc_v_vec = [] + + # Test 1 + phi = 0 + theta = 0 + v_vec = self.antenna2._super_position_vector( + phi, theta, + self.antenna2.par.n_rows, self.antenna2.par.n_columns, + self.antenna2.par.element_vert_spacing, + self.antenna2.par.element_horiz_spacing, + ) + acc_phi.append(phi) + acc_theta.append(theta) + acc_v_vec.append(v_vec) + expected_v_vec = np.array([[[1.0, 1.0], [-1.0, -1.0]]]) + npt.assert_allclose( + v_vec, + expected_v_vec, rtol=eps, + ) + + # Test 2 + phi = 90 + theta = 90 + v_vec = self.antenna2._super_position_vector( + phi, theta, + self.antenna2.par.n_rows, self.antenna2.par.n_columns, + self.antenna2.par.element_vert_spacing, + self.antenna2.par.element_horiz_spacing, + ) + acc_phi.append(phi) + acc_theta.append(theta) + acc_v_vec.append(v_vec) + expected_v_vec = np.array([[[1.0, -1.0], [1.0, -1.0]]]) + npt.assert_allclose( + v_vec, + expected_v_vec, rtol=eps, + ) + + # Test 3 + phi = 45 + theta = 45 + v_vec = self.antenna2._super_position_vector( + phi, theta, + self.antenna2.par.n_rows, self.antenna2.par.n_columns, + self.antenna2.par.element_vert_spacing, + self.antenna2.par.element_horiz_spacing, + ) + acc_phi.append(phi) + acc_theta.append(theta) + acc_v_vec.append(v_vec) + expected_v_vec = np.array([[ + [1.0 + 0.0j, 0.0 + 1.0j], + [-0.6056998 + 0.7956932j, -0.7956932 - 0.6056998j], + ]]) + npt.assert_allclose( + v_vec, + expected_v_vec, rtol=eps, + ) + + # Test 4 + phi = 60 + theta = 90 + v_vec = self.antenna2._super_position_vector( + phi, theta, + self.antenna2.par.n_rows, self.antenna2.par.n_columns, + self.antenna2.par.element_vert_spacing, + self.antenna2.par.element_horiz_spacing, + ) + acc_phi.append(phi) + acc_theta.append(theta) + acc_v_vec.append(v_vec) + expected_v_vec = np.array([[ + [1.0 + 0.0j, -0.912724 + 0.408576j], + [1.0 + 0.0j, -0.912724 + 0.408576j], + ]]) + npt.assert_allclose( + v_vec, + expected_v_vec, rtol=eps, + ) + + acc_phi = np.array(acc_phi) + acc_theta = np.array(acc_theta) + expected_v_vec = np.squeeze(acc_v_vec, axis=1) + v_vec = self.antenna2._super_position_vector( + acc_phi, acc_theta, + self.antenna2.par.n_rows, self.antenna2.par.n_columns, + self.antenna2.par.element_vert_spacing, + self.antenna2.par.element_horiz_spacing, + ) + npt.assert_allclose( + v_vec, + expected_v_vec, rtol=eps, + ) + + def test_calculate_gain(self): + """Test calculation of antenna gain for given phi/theta vectors.""" + # Error margin and antenna + eps = 1e-4 + par = self.bs_param.get_antenna_parameters() + self.antenna1 = AntennaArray(par) + par = self.ue_param.get_antenna_parameters() + self.antenna2 = AntennaArray(par) + + # Test 1 + phi_vec = np.array([45.0, 32.5]) + theta_vec = np.array([45.0, 115.2]) + gains = self.antenna2.calculate_gain( + phi_vec=phi_vec, theta_vec=theta_vec, + ) + npt.assert_allclose(gains, np.array([5.9491, 11.9636]), atol=eps) + + # Test 2 + phi = 0.0 + theta = 60.0 + phi_scan = 45 + theta_tilt = 180 + self.antenna2.add_beam(phi_scan, theta_tilt) + beams_l = np.zeros_like(phi, dtype=int) + gains = self.antenna2.calculate_gain( + phi_vec=phi, theta_vec=theta, + beams_l=beams_l, + ) + npt.assert_allclose(gains, np.array([10.454087]), atol=eps) + + # Test 3 + phi = 40 + theta = 100 + gains = self.antenna1.calculate_gain( + phi_vec=phi, theta_vec=theta, + co_channel=False, + ) + npt.assert_allclose(gains, np.array([1.6667]), atol=eps) + + +if __name__ == '__main__': + unittest.main() +# +# suite = unittest.TestSuite() +# suite.addTest(AntennaArrayTest('test_calculate_gain')) +# unittest.TextTestRunner().run(suite) From 86f1f72d5d6d25faa6be95fb87f59a3f60897376 Mon Sep 17 00:00:00 2001 From: artistrea Date: Fri, 30 Jan 2026 15:42:25 -0300 Subject: [PATCH 67/77] update!: simulator geometry separate concerns refactor and better API This commit adds RigidTransform, ReferenceFrame and ENUReferenceFrame for preparation for usage with other reference frames. SimulatorGeometry was refactored and its API was updated for better maintainability. --- sharc/support/geometry.py | 439 +++++++++++------ .../topology_spherical_sampling_from_grid.py | 45 +- tests/test_geometry.py | 463 ++++++++++++++++-- 3 files changed, 730 insertions(+), 217 deletions(-) diff --git a/sharc/support/geometry.py b/sharc/support/geometry.py index 6cf9e51fb..94a937122 100644 --- a/sharc/support/geometry.py +++ b/sharc/support/geometry.py @@ -5,6 +5,7 @@ from sharc.support.sharc_geom import cartesian_to_polar, polar_to_cartesian import scipy import numpy as np +from dataclasses import dataclass from abc import ABC @@ -303,10 +304,181 @@ def get_off_axis_angle(self, other: "GlobalGeometry") -> np.array: return phi_deg +@dataclass(frozen=True) +class RigidTransform: + """ + Defines a transformation of the type + A(X) = rot @ X + t + where it applies rotation to X and then translates the result. + """ + rot: scipy.spatial.transform.Rotation # N rotations + t: np.ndarray # (N, 3) + + def __post_init__(self): + if not isinstance(self.rot, scipy.spatial.transform.Rotation): + raise ValueError("rot must be a scipy Rotation") + + n_rot = len(self.rot) + n_t = self.t.shape[0] + + if self.t.shape != (n_t, 3): + raise ValueError(f"Invalid transform shape t={n_t}") + + # if some is equal to one, broadcasting still works + if not (n_rot == n_t or n_rot == 1 or n_t == 1): + raise ValueError( + f"Incompatible RigidTransform shapes: rot={n_rot}, t={n_t}" + ) + self.t.flags.writeable = False + + @property + def N(self): + n_rot = len(self.rot) + n_t = self.t.shape[0] + return max(n_rot, n_t) + + def inv(self) -> "RigidTransform": + rot_inv = self.rot.inv() + t_inv = -rot_inv.apply(self.t) + return RigidTransform(rot_inv, t_inv) + + def and_then(self, other: "RigidTransform") -> "RigidTransform": + """Apply self, then other (composition: other @ self).""" + return RigidTransform( + rot=other.rot * self.rot, + t=other.rot.apply(self.t) + other.t + ) + + def take(self, idx: int) -> "RigidTransform": + # NOTE: we slice instead of taking indice to maintain + # array shape structure for functional broadcasting + # and batch computing + return RigidTransform( + rot=self.rot[idx:idx+1], + t=self.t[idx:idx+1], + ) + + def apply_points(self, x: np.ndarray) -> np.ndarray: + """Applies rotations and translations to the points""" + return self.apply_vectors(x) + self.t + + def apply_points_permutation(self, x: np.ndarray) -> np.ndarray: + """Applies rotation and translation to the point""" + t = self.t # (N,3) + + return self.apply_vectors_permutation(x) + t[:, None, :] + + def apply_vectors(self, v: np.ndarray) -> np.ndarray: + """Applies only rotations, considering the vectors as a pointing vec""" + v = np.atleast_2d(v) + assert v.ndim == 2 and v.shape[1] == 3 + M = v.shape[0] + assert self.N == 1 or M == 1 or M == self.N + + return self.rot.apply(v) + np.zeros_like(self.t) + + def apply_vectors_permutation(self, v: np.ndarray) -> np.ndarray: + """Applies rotation and translation to the point""" + # TODO: change impl + v = np.atleast_2d(v) + assert v.ndim == 2 and v.shape[1] == 3 + + # (N,3,3) + R = self.rot.as_matrix() + + # einsum: (N,3,3) . (M,3) -> (N,M,3) + return ( + np.einsum("nij,mj->nmi", R, v) + + np.zeros_like(self.t)[:, None, :] + ) + + +class ReferenceFrame(ABC): + """ + Defines a reference frame, which generates a rigid transform + between ecef and its own local coordinate system. + It should be noted that, while this may define axis position, + the bearing considered (azimuth, elevation) for calculations + CANNOT USE NAVIGATION CONVENTION. + """ + __slots__ = ('_from_ecef', '_to_ecef') + + @property + def from_ecef(self) -> RigidTransform: + return self._from_ecef + + @property + def to_ecef(self) -> RigidTransform: + return self._to_ecef + + +class ENUReferenceFrame(ReferenceFrame): + """ + Defines ENU reference frame. + + NOTE: this does not change bearing convention to azimuth being + angular distance from North. Azimuth is still from x axis towards y axis. + """ + __slots__ = ("_lat", "_lon", "_alt") + + def __init__( + self, + *, + lat: np.ndarray, + lon: np.ndarray, + alt: np.ndarray, + ): + lat = np.atleast_1d(lat) + lon = np.atleast_1d(lon) + alt = np.atleast_1d(alt) + if lat.shape != lon.shape or lat.shape != alt.shape: + raise ValueError("lat, lon, alt must have identical shapes") + + if lat.shape != (len(lat),): + raise ValueError( + "The lla shapes used must be one dimensional: (N,), but is" + f" {lat.shape}" + ) + + self._lat = np.copy(lat) + self._lon = np.copy(lon) + self._alt = np.copy(alt) + + self._from_ecef = self._compute_to_local() + self._to_ecef = self._from_ecef.inv() + + # freeze frame + self._lat.flags.writeable = False + self._lon.flags.writeable = False + self._alt.flags.writeable = False + + def _compute_to_local(self): + lat = self._lat + lon = self._lon + alt = self._alt + N = lat.shape[0] + + rotation_around_z = -lon - 90 + rotation_around_x = lat - 90 + + ecef2local_rot = scipy.spatial.transform.Rotation.from_euler( + 'zx', + np.stack([rotation_around_z, rotation_around_x], axis=-1), + degrees=True + ) + ecef2local_translation = np.zeros((N, 3)) + # NOTE: using constant earth radius works because of spherical Earth + ecef2local_translation[:, 2] = -(alt + EARTH_RADIUS_M) + ecef2local = RigidTransform( + ecef2local_rot, ecef2local_translation + ) + return ecef2local + + @readonly_properties( "x_local", "y_local", "z_local", "pointn_azim_local", "pointn_elev_local", - "global_lla_reference", "local_lla_references" + "global_reference_frame", "local_reference_frame" ) class SimulatorGeometry(GlobalGeometry): """ @@ -320,8 +492,8 @@ class SimulatorGeometry(GlobalGeometry): pointn_azim_local: np.ndarray # (N,) pointn_elev_local: np.ndarray # (N,) - local_lla_references: np.ndarray[np.ndarray[float]] # (3, N) - global_lla_reference: tuple[float, float, float] + local_reference_frame: ReferenceFrame + global_reference_frame: ReferenceFrame uses_local_coords: bool @@ -345,14 +517,14 @@ def setup( self, num_geometries, uses_local_coords=False, - global_cs: tuple[float, float, float] = None, + global_cs: ReferenceFrame = None, ): """ Initializes variables based on number of geometries considered """ super().setup(num_geometries) - self._global_lla_reference = global_cs + self._global_reference_frame = global_cs self.uses_local_coords = True if not uses_local_coords: @@ -373,25 +545,16 @@ def setup( self._z_local = np.empty(num_geometries) self._pointn_azim_local = np.empty(num_geometries) self._pointn_elev_local = np.empty(num_geometries) - self._local_lla_references = np.empty((3, num_geometries)) + self._local_reference_frame = None - def set_local_coord_sys( - self, - ref_lats, - ref_lons, - ref_alts, + def set_local_reference_frame( + self, frame: ReferenceFrame ): """ Sets local coord system references and prepares global<->local coordinate transformation """ - for r in [ref_lats, ref_lons, ref_alts]: - if len(r) != self.num_geometries: - raise ValueError( - "Incongruent number of coordinate systems. " - f"Passed {len(r)} but should have passed {self.num_geometries}" - ) - self._local_lla_references = np.stack((ref_lats, ref_lons, ref_alts)) + self._local_reference_frame = frame self._compute_global_local_transform() @@ -453,7 +616,7 @@ def _compute_global_from_local(self): p_local = np.stack([self.x_local, self.y_local, self.z_local], axis=-1) - p_global = self._vec_local2global(p_local) + p_global = self._local2global_points(p_local) # Store results self._x_global = p_global[:, 0] @@ -465,8 +628,8 @@ def _compute_global_from_local(self): point_local = np.stack(polar_to_cartesian( r, self.pointn_azim_local, self.pointn_elev_local), axis=-1) - point_global_x, point_global_y, point_global_z = self._vec_local2global( - point_local, translate=False + point_global_x, point_global_y, point_global_z = self._local2global_vectors( + point_local ).T _, global_azimuth, global_elevation = cartesian_to_polar( @@ -475,42 +638,73 @@ def _compute_global_from_local(self): self._pointn_elev_global = global_elevation self._pointn_azim_global = global_azimuth - def _vec_local2global( - self, - p_local, - *, - translate=True, - permutate=False, + def _local2global_points( + self, p_local, + ): + """Receives points shaped as (N, 3) and applies local2global transform, + returning (N, 3), where N is the number of local coordinate systems. + """ + return self.local2global.apply_points(p_local) + + def _local2global_vectors( + self, p_local, ): """Receives a vector shaped as (N, 3) and applies local2global transform, - returning (N, 3). - If permutate == True, then may receive a vector (M, 3), - returning (N, M, 3) + returning (N, 3), where N is the number of local coordinate systems. + Does not apply translations, just rotations + """ + return self.local2global.apply_vectors(p_local) + + def _local2global_points_permutation( + self, p_local, + ): + """Receives points as (M, 3), returning (N, M, 3), + where N is the number of local coordinate systems + """ + return self.local2global.apply_points_permutation(p_local) + + def _local2global_vectors_permutation( + self, p_local, + ): + """Receives points as (M, 3), returning (N, M, 3), + where N is the number of local coordinate systems. + Does not apply translations, just rotations + """ + return self.local2global.apply_vectors_permutation(p_local) + + def _global2local_points( + self, p_local, + ): + """Receives points shaped as (N, 3) and applies global2local transform, + returning (N, 3), where N is the number of local coordinate systems. + """ + return self.global2local.apply_points(p_local) + + def _global2local_vectors( + self, p_local, + ): + """Receives a vector shaped as (N, 3) and applies global2local transform, + returning (N, 3), where N is the number of local coordinate systems. + Does not apply translations, just rotations + """ + return self.global2local.apply_vectors(p_local) + + def _global2local_points_permutation( + self, p_local, + ): + """Receives points as (M, 3), returning (N, M, 3), + where N is the number of local coordinate systems + """ + return self.global2local.apply_points_permutation(p_local) + + def _global2local_vectors_permutation( + self, p_local, + ): + """Receives points as (M, 3), returning (N, M, 3), + where N is the number of local coordinate systems. + Does not apply translations, just rotations """ - N = self.num_geometries - - local2ecef_t = self._local2ecef_transl_mtx if translate else 0 - ecef2global_t = self._ecef2global_transl_mtx if translate else 0 - - if not permutate: - assert p_local.shape == (N, 3) - # remove ecef2local translation - p_local = p_local + local2ecef_t # (N,3) - # rotate local2ecef2global - p_global = (self._local2global_rot_mtx @ p_local[..., None]).squeeze(-1) # (N,3) - # add ecef2global translation - p_global = p_global + ecef2global_t # (N,3) - else: - local2ecef_t_cast = local2ecef_t - ecef2global_t_cast = ecef2global_t - if translate: - local2ecef_t_cast = local2ecef_t[:, None, :] - ecef2global_t_cast = ecef2global_t[:, None, :] - p_local_exp = p_local[None, :, :] + local2ecef_t_cast # (N,M,3) - p_global = (self._global2local_rot_mtx[:, None, :, :] @ p_local_exp[..., None]).squeeze(-1) - p_global += ecef2global_t_cast - - return p_global + return self.global2local.apply_vectors_permutation(p_local) def _compute_local_from_global(self): if not self.uses_local_coords: @@ -523,7 +717,7 @@ def _compute_local_from_global(self): p_global = np.stack([self.x_global, self.y_global, self.z_global], axis=-1) - p_local = self._vec_global2local(p_global) + p_local = self._global2local_points(p_global) # Store results self._x_local = p_local[:, 0] @@ -535,8 +729,8 @@ def _compute_local_from_global(self): point_global = np.stack(polar_to_cartesian( r, self.pointn_azim_global, self.pointn_elev_global), axis=-1) - point_local_x, point_local_y, point_local_z = self._vec_global2local( - point_global, translate=False + point_local_x, point_local_y, point_local_z = self._global2local_vectors( + point_global ).T _, local_azimuth, local_elevation = cartesian_to_polar( @@ -545,93 +739,23 @@ def _compute_local_from_global(self): self._pointn_elev_local = local_elevation self._pointn_azim_local = local_azimuth - def _vec_global2local( - self, - p_global, - *, - translate=True, - permutate=False - ): - """Receives a vector shaped as (N, 3) and applies global2local transform, - returning (N, 3). - If permutate == True, then may receive a vector (M, 3), - returning (N, M, 3) - """ - N = self.num_geometries - - global2ecef_t = self._global2ecef_transl_mtx if translate else 0 - ecef2local_t = self._ecef2local_transl_mtx if translate else 0 - - if not permutate: - assert p_global.shape == (N, 3) - - # remove ecef2global translation - p_global = p_global + global2ecef_t # (N,3) - - # rotate global2ecef2local - p_local = (self._global2local_rot_mtx @ p_global[..., None]).squeeze(-1) # (N,3) - - # add ecef2local translation - p_local += ecef2local_t # (N,3) - else: - global2ecef_t_cast = global2ecef_t - ecef2local_t_cast = ecef2local_t - if translate: - global2ecef_t_cast = global2ecef_t[:, None, :] - ecef2local_t_cast = ecef2local_t[:, None, :] - p_global_exp = p_global[None, :, :] + global2ecef_t_cast # (N,M,3) - p_local = (self._global2local_rot_mtx[:, None, :, :] @ p_global_exp[..., None]).squeeze(-1) - p_local += ecef2local_t_cast - - return p_local - def _compute_global_local_transform(self): # get ecef to local - local_lat, local_lon, local_alt = self.local_lla_references - rotation_around_z = -local_lon - 90 - rotation_around_x = local_lat - 90 - - ecef2local_rot = scipy.spatial.transform.Rotation.from_euler( - 'zx', - np.stack([rotation_around_z, rotation_around_x], axis=-1), - degrees=True + self.global2local = ( + self.global_reference_frame + .to_ecef + .and_then( + self.local_reference_frame.from_ecef + ) ) - - # first translation, after rotation - self._local2ecef_transl_mtx = np.zeros((self.num_geometries, 3)) - # NOTE: works because of spherical Earth - self._local2ecef_transl_mtx[:, 2] = local_alt + EARTH_RADIUS_M - local2ecef_rot_mtx = ecef2local_rot.inv().as_matrix() - - # first rotation, after translation - ecef2local_rot_mtx = ecef2local_rot.as_matrix() - self._ecef2local_transl_mtx = -self._local2ecef_transl_mtx - - # global transforms - global_lat, global_lon, global_alt = self._global_lla_reference - rotation_around_z = -global_lon - 90 - rotation_around_x = global_lat - 90 - - ecef2global_rot = scipy.spatial.transform.Rotation.from_euler( - 'zx', - np.stack([rotation_around_z, rotation_around_x], axis=-1), - degrees=True + self.local2global = ( + self.local_reference_frame + .to_ecef + .and_then( + self.global_reference_frame.from_ecef + ) ) - # first translation, after rotation - self._global2ecef_transl_mtx = np.zeros((1, 3)) - # NOTE: works because of spherical Earth - self._global2ecef_transl_mtx[:, 2] = global_alt + EARTH_RADIUS_M - global2ecef_rot_mtx = ecef2global_rot.inv().as_matrix() - - # first rotation, after translation - ecef2global_rot_mtx = ecef2global_rot.as_matrix() - self._ecef2global_transl_mtx = -self._global2ecef_transl_mtx - - self._local2global_rot_mtx = ecef2global_rot_mtx @ local2ecef_rot_mtx - - self._global2local_rot_mtx = ecef2local_rot_mtx @ global2ecef_rot_mtx - def get_local_distance_to( self, other: "SimulatorGeometry", @@ -655,9 +779,8 @@ def get_local_distance_to( return self.get_global_distance_to(other) p_global = np.stack([other.x_global, other.y_global, other.z_global], axis=-1) - other_local = self._vec_global2local( + other_local = self._global2local_points_permutation( p_global, - permutate=True ) own_local = np.stack([self.x_local, self.y_local, np.zeros_like(self.z_local)], axis=-1) @@ -765,17 +888,21 @@ def plot_geom( if __name__ == "__main__": global_lla = (-14, -45, 1200) + global_reference_frame = ENUReferenceFrame( + lat=np.array([global_lla[0]]), + lon=np.array([global_lla[1]]), + alt=np.array([global_lla[2]]), + ) # tg = SimulatorGeometry( # 1, # 1, - # global_lla + # global_reference_frame # ) - # tg.set_local_coord_sys( + # tg.set_local_reference_frame( # np.array([-14]), # np.array([-45]), # np.array([1200]), # ) - # print(tg.local_lla_references) from sharc.topology.topology_macrocell import TopologyMacrocell rng = np.random.RandomState(seed=0xcaffe) @@ -790,23 +917,27 @@ def plot_geom( bs_geom = SimulatorGeometry( topology.num_base_stations, topology.num_base_stations, - global_lla + global_reference_frame ) ue_geom = SimulatorGeometry( num_ue * topology.num_base_stations, num_ue * topology.num_base_stations, - global_lla + global_reference_frame ) - bs_geom.set_local_coord_sys( - np.repeat(11, topology.num_base_stations), - np.repeat(-47, topology.num_base_stations), - np.repeat(1200, topology.num_base_stations), + bs_geom.set_local_reference_frame( + ENUReferenceFrame( + lat=np.repeat(11, topology.num_base_stations), + lon=np.repeat(-47, topology.num_base_stations), + alt=np.repeat(1200, topology.num_base_stations), + ) ) - ue_geom.set_local_coord_sys( - np.repeat(11, num_ue * topology.num_base_stations), - np.repeat(-47, num_ue * topology.num_base_stations), - np.repeat(1200, num_ue * topology.num_base_stations), + ue_geom.set_local_reference_frame( + ENUReferenceFrame( + lat=np.repeat(11, num_ue * topology.num_base_stations), + lon=np.repeat(-47, num_ue * topology.num_base_stations), + alt=np.repeat(1200, num_ue * topology.num_base_stations), + ) ) bs_geom.set_local_coords( diff --git a/sharc/topology/topology_spherical_sampling_from_grid.py b/sharc/topology/topology_spherical_sampling_from_grid.py index c96f4854e..e93162959 100644 --- a/sharc/topology/topology_spherical_sampling_from_grid.py +++ b/sharc/topology/topology_spherical_sampling_from_grid.py @@ -1,7 +1,7 @@ from sharc.topology.topology import Topology import numpy as np from sharc.support.sharc_geom import CoordinateSystem -from sharc.support.geometry import SimulatorGeometry +from sharc.support.geometry import SimulatorGeometry, ENUReferenceFrame from sharc.parameters.imt.parameters_grid import ParametersTerrestrialGrid # from sharc.satellite.utils.sat_utils import lla2ecef # import math @@ -25,7 +25,7 @@ def __init__( max_ue_distance: float, num_base_stations: int, global_sim_lla_reference: tuple[float, float, float], - grid: ParametersTerrestrialGrid, + grid: ParametersTerrestrialGrid | np.ndarray, ): """ Initializes a spherical topology with specific network settings. @@ -61,13 +61,16 @@ def calculate_coordinates(self, random_number_gen=np.random.RandomState()): random_number_gen : np.random.RandomState, optional Random number generator (not used in this implementation). """ - self.grid.reset_grid( - "calculate_coords", - random_number_gen, - True - ) + if not isinstance(self.grid, np.ndarray): + self.grid.reset_grid( + "calculate_coords", + random_number_gen, + True + ) + lla_grid_to_sample = self.grid.lon_lat_grid[::-1] + else: + lla_grid_to_sample = self.grid - lla_grid_to_sample = self.grid.lon_lat_grid[::-1] # print("self.grid.lon_lat_grid.shape", self.grid.lon_lat_grid.shape) # print("self.grid.lon_lat_grid.shape", self.grid.lon_lat_grid.shape) # print("self.num_base_stations", self.num_base_stations) @@ -88,15 +91,21 @@ def calculate_coordinates(self, random_number_gen=np.random.RandomState()): geom = SimulatorGeometry( self.num_base_stations, self.num_base_stations, - self.global_cs, + ENUReferenceFrame( + lat=self.global_cs[0], + lon=self.global_cs[1], + alt=self.global_cs[2], + ), ) lat, lon, alt = chosen_llas # coords locais para determinar # transformação global <> local - geom.set_local_coord_sys( - lat, - lon, - alt, + geom.set_local_reference_frame( + ENUReferenceFrame( + lat=lat, + lon=lon, + alt=alt, + ) ) self.x = np.zeros(self.num_base_stations) self.y = np.zeros(self.num_base_stations) @@ -128,10 +137,12 @@ def get_ue_geometry(self, ue_k: int) -> SimulatorGeometry: self.num_base_stations * ue_k, self.global_cs, ) - ue_geom.set_local_coord_sys( - np.repeat(self.bs_geometry._local_lla_references[0], ue_k), - np.repeat(self.bs_geometry._local_lla_references[1], ue_k), - np.repeat(self.bs_geometry._local_lla_references[2], ue_k), + ue_geom.set_local_reference_frame( + ENUReferenceFrame( + lat=np.repeat(self.bs_geometry._local_lla_references[0], ue_k), + lon=np.repeat(self.bs_geometry._local_lla_references[1], ue_k), + alt=np.repeat(self.bs_geometry._local_lla_references[2], ue_k), + ) ) return ue_geom diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 5d8a1f32d..a9f68c113 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1,9 +1,341 @@ import unittest import numpy as np import numpy.testing as npt -from sharc.support.geometry import SimulatorGeometry +from sharc.support.geometry import ( + SimulatorGeometry, ENUReferenceFrame, RigidTransform +) from sharc.satellite.ngso.constants import EARTH_RADIUS_M from copy import deepcopy +from scipy.spatial.transform import Rotation +from itertools import product + + +def random_rigid_transform(rng, N): + """ + Generate a random RigidTransform with batch size N. + Rotations are orthonormal matrices generated via QR. + """ + A = rng.normal(size=(N, 3, 3)) + Q, _ = np.linalg.qr(A) + + # ensure right-handed (det = +1) + det = np.linalg.det(Q) + Q[det < 0, :, 0] *= -1 + + t = rng.normal(size=(N, 3)) + + return RigidTransform(Rotation.from_matrix(Q), t) + +def rot_identity(n=1): + return Rotation.from_rotvec(np.zeros((n, 3))) + +def rot_z(angle_deg, n=1): + return Rotation.from_rotvec( + np.tile([0.0, 0.0, angle_deg], (n, 1)), + degrees=True + ) + +def rot_x(angle_deg, n=1): + return Rotation.from_rotvec( + np.tile([angle_deg, 0.0, 0.0], (n, 1)), + degrees=True + ) + +def rot_y(angle_deg, n=1): + return Rotation.from_rotvec( + np.tile([0.0, angle_deg, 0.0], (n, 1)), + degrees=True + ) + + +class TestRigidTransform(unittest.TestCase): + def setUp(self): + pass + + def test_init_and_broadcasting(self): + for rot_shp, t_shp in product( + [(1, 3), (4, 3)], + [(1, 3), (4, 3)], + ): + Nrot = rot_shp[0] + Nt = t_shp[0] + N = max(Nrot, Nt) + + rot = rot_identity(rot_shp[0]) + t = np.zeros(t_shp) + + with self.assertRaises(ValueError): + RigidTransform(rot, np.zeros((3,))) + + if Nt > 1: + with self.assertRaises(ValueError): + RigidTransform(rot_identity(N+1), t) + + if Nrot > 1: + with self.assertRaises(ValueError): + RigidTransform(rot, np.zeros((N+1, 3))) + + # should not throw: + tr = RigidTransform(rot, t) + RigidTransform(rot, np.zeros((1, 3))) + RigidTransform(rot_identity(1), t) + + # test broadcasting contracts: + for fn in [tr.apply_points, tr.apply_vectors]: + if N > 1: + with self.assertRaises(AssertionError): + fn(np.zeros((N+1, 3))) + + for in_shp in [(1, 3), (N, 3)]: + res = fn(np.zeros(in_shp)) + npt.assert_equal( + res.shape, (N, 3) + ) + + for fn, in_shp in product( + [tr.apply_points_permutation, tr.apply_vectors_permutation], + [(1, 3), (N, 3), (N+1, 3)] + ): + Nin = in_shp[0] + res = fn(np.zeros(in_shp)) + npt.assert_equal( + res.shape, (N, Nin, 3) + ) + + def test_simple_transformations(self): + eps = 1e-4 + + ux = np.array([1., 0., 0.]) + uy = np.array([0., 1., 0.]) + uz = np.array([0., 0., 1.]) + u = np.array([ux, uy, uz, ux + uy + uz]) + + ####################################################################### + # Identity transform + id_tr = RigidTransform(rot_identity(4), np.zeros((1, 3))) + out = id_tr.apply_points(u) + npt.assert_equal(out, u) + + out = id_tr.apply_vectors(u) + npt.assert_equal(out, u) + + out = id_tr.apply_points_permutation(u) + npt.assert_equal(out, np.stack((u, u, u, u))) + + out = id_tr.apply_vectors_permutation(u) + npt.assert_equal(out, np.stack((u, u, u, u))) + + ####################################################################### + # Translation only transform + t1_tr = RigidTransform(rot_identity(4), np.ones((1, 3))) + out = t1_tr.apply_points(u) + npt.assert_equal(out, u + 1.) + out = t1_tr.inv().apply_points(out) + npt.assert_equal(out, u) + + out = t1_tr.apply_vectors(u) + npt.assert_equal(out, u) + + out = t1_tr.apply_points_permutation(u) + npt.assert_equal(out, np.stack((u, u, u, u)) + 1.) + out = t1_tr.inv().apply_points(out[0]) + npt.assert_equal(out, u) + + out = t1_tr.apply_vectors_permutation(u) + npt.assert_equal(out, np.stack((u, u, u, u))) + + ####################################################################### + # Rotation Z only transform + rot_z90_tr = RigidTransform( + rot_z(-90.), np.zeros((1, 3)) + ) + out_rot_z90 = rot_z90_tr.apply_points(u) + npt.assert_allclose( + out_rot_z90, + np.array([ + -uy, ux, uz, ux - uy + uz + ]), + atol=eps + ) + out = rot_z90_tr.inv().apply_points(out_rot_z90) + npt.assert_allclose( + out, + u, + atol=eps + ) + + ####################################################################### + # Rotation X only transform + rot_x90_tr = RigidTransform( + rot_x(-90., 4), np.zeros((1, 3)) + ) + + out_rot_z90_x90 = rot_x90_tr.apply_points(out_rot_z90) + expected_out_rot_z90_x90 = np.array([ + uz, ux, uy, ux + uy + uz + ]) + npt.assert_allclose( + out_rot_z90_x90, + expected_out_rot_z90_x90, + atol=eps + ) + out = rot_x90_tr.inv().apply_points(expected_out_rot_z90_x90) + npt.assert_allclose( + out, + out_rot_z90, + atol=eps + ) + + ####################################################################### + # Rotation Z and then Rotation X transform + npt.assert_allclose( + rot_z90_tr.and_then(rot_x90_tr).apply_points(u), + expected_out_rot_z90_x90, + atol=eps, + ) + npt.assert_allclose( + (rot_z90_tr.and_then(rot_x90_tr).inv() + .apply_points(expected_out_rot_z90_x90)), + u, + atol=eps, + ) + npt.assert_allclose( + (rot_x90_tr.inv().and_then(rot_z90_tr.inv()) + .apply_points(expected_out_rot_z90_x90)), + u, + atol=eps, + ) + + ####################################################################### + # Rotation Z and then Rotation X and then Translation transform + npt.assert_allclose( + rot_z90_tr.and_then(rot_x90_tr).and_then(t1_tr).apply_points(u), + expected_out_rot_z90_x90 + 1., + atol=eps, + ) + npt.assert_allclose( + (rot_z90_tr.and_then(rot_x90_tr).and_then(t1_tr).inv() + .apply_points(expected_out_rot_z90_x90 + 1.)), + u, + atol=eps, + ) + npt.assert_allclose( + (t1_tr.inv().and_then(rot_x90_tr.inv().and_then(rot_z90_tr.inv())) + .apply_points(expected_out_rot_z90_x90 + 1.)), + u, + atol=eps, + ) + # considering that VECTOR calculation should NOT translate + npt.assert_allclose( + rot_z90_tr.and_then(rot_x90_tr).and_then(t1_tr).apply_vectors(u), + expected_out_rot_z90_x90, + atol=eps, + ) + npt.assert_allclose( + (rot_z90_tr.and_then(rot_x90_tr).and_then(t1_tr).inv() + .apply_vectors(expected_out_rot_z90_x90)), + u, + atol=eps, + ) + npt.assert_allclose( + (t1_tr.inv().and_then(rot_x90_tr.inv().and_then(rot_z90_tr.inv())) + .apply_vectors(expected_out_rot_z90_x90)), + u, + atol=eps, + ) + + def test_permutation_points_equivalence(self): + rng = np.random.default_rng(0) + + for n in range(1, 10): + tr = random_rigid_transform(rng, n) + + x = rng.normal(size=(n, 3)) + + y = tr.apply_points(x) + y_permutation = tr.apply_points_permutation(x) + + # diagonal of permutation must equal non-permutation + npt.assert_allclose( + y, + y_permutation.diagonal().T, + atol=1e-12, + ) + + def test_permutation_vectors_equivalence(self): + rng = np.random.default_rng(0) + + for n in range(1, 10): + tr = random_rigid_transform(rng, n) + + x = rng.normal(size=(n, 3)) + + y = tr.apply_vectors(x) + y_permutation = tr.apply_vectors_permutation(x) + + # diagonal of permutation must equal non-permutation + npt.assert_allclose( + y, + y_permutation.diagonal().T, + atol=1e-12, + ) + + def test_take_commutes_with_apply_points(self): + rng = np.random.default_rng(0) + + for n in range(1, 10): + tr = random_rigid_transform(rng, n) + x = rng.normal(size=(n, 3)) + + y_full = tr.apply_points(x) + + for i in range(n): + y_take = tr.take(i).apply_points(x) + npt.assert_allclose( + y_take[i], + y_full[i], + atol=1e-12, + ) + + def test_take_matches_permutation_points(self): + rng = np.random.default_rng(1) + + for n in [2, 5]: + tr = random_rigid_transform(rng, n) + x = rng.normal(size=(n, 3)) + + Y = tr.apply_points_permutation(x) + + for i in range(n): + tr_i = tr.take(i) + yi = tr_i.apply_points(x) + + npt.assert_allclose( + yi, + Y[i], + rtol=1e-12, + atol=1e-12, + ) + + def test_take_matches_permutation_vectors(self): + rng = np.random.default_rng(1) + + for n in [2, 5]: + tr = random_rigid_transform(rng, n) + x = rng.normal(size=(n, 3)) + + Y = tr.apply_vectors_permutation(x) + + for i in range(n): + tr_i = tr.take(i) + yi = tr_i.apply_vectors(x) + + npt.assert_allclose( + yi, + Y[i], + rtol=1e-12, + atol=1e-12, + ) class TestGeometry(unittest.TestCase): @@ -18,9 +350,14 @@ def _test_expected_geom( only_global_azim=None, only_local_azim=None ): - npt.assert_allclose(geom.x_local, expect_local["x"], atol=0.001) - npt.assert_allclose(geom.y_local, expect_local["y"], atol=0.001) - npt.assert_allclose(geom.z_local, expect_local["z"], atol=0.001) + npt.assert_allclose( + np.stack((geom.x_local, geom.y_local, geom.z_local)), + np.stack((expect_local["x"], expect_local["y"], expect_local["z"])), + atol=0.001 + ) + # npt.assert_allclose(geom.x_local, expect_local["x"], atol=0.001) + # npt.assert_allclose(geom.y_local, expect_local["y"], atol=0.001) + # npt.assert_allclose(geom.z_local, expect_local["z"], atol=0.001) npt.assert_allclose(geom.pointn_elev_local, expect_local["elev"], atol=0.001) if only_local_azim is None: npt.assert_allclose(geom.pointn_azim_local, expect_local["azim"], atol=0.001) @@ -32,9 +369,14 @@ def _test_expected_geom( atol=0.001 ) - npt.assert_allclose(geom.x_global, expect_global["x"], atol=0.001) - npt.assert_allclose(geom.y_global, expect_global["y"], atol=0.001) - npt.assert_allclose(geom.z_global, expect_global["z"], atol=0.001) + npt.assert_allclose( + np.stack((geom.x_global, geom.y_global, geom.z_global)), + np.stack((expect_global["x"], expect_global["y"], expect_global["z"])), + atol=0.001 + ) + # npt.assert_allclose(geom.x_global, expect_global["x"], atol=0.001) + # npt.assert_allclose(geom.y_global, expect_global["y"], atol=0.001) + # npt.assert_allclose(geom.z_global, expect_global["z"], atol=0.001) npt.assert_allclose(geom.pointn_elev_global, expect_global["elev"], atol=0.001) if only_global_azim is None: npt.assert_allclose( @@ -57,14 +399,18 @@ def setUp(self): def test_set_coords_when_local_eq_global(self): """Test setting coordinates when local should eq global """ - ref = (10, -5, 1200) - - no_local = SimulatorGeometry(3, False, ref) - local_eq_global = SimulatorGeometry(3, True, ref) - local_eq_global.set_local_coord_sys( - np.repeat([ref[0]], 3), - np.repeat([ref[1]], 3), - np.repeat([ref[2]], 3), + ref_lla = (10, -5, 1200) + ref_frame = ENUReferenceFrame( + lat=ref_lla[0], lon=ref_lla[1], alt=ref_lla[2], + ) + no_local = SimulatorGeometry(3, False, ref_frame) + local_eq_global = SimulatorGeometry(3, True, ref_frame) + local_eq_global.set_local_reference_frame( + ENUReferenceFrame( + lat=np.repeat([ref_lla[0]], 3), + lon=np.repeat([ref_lla[1]], 3), + alt=np.repeat([ref_lla[2]], 3), + ) ) """Test setting global coordinates @@ -102,16 +448,21 @@ def test_set_coords_when_local_eq_global(self): def test_setting_different_alts(self): """Test setting coordinates when local should eq global """ - ref = (-45.0, 90, 30.) + ref_lla = (-45.0, 90, 30.) + ref_frame = ENUReferenceFrame( + lat=ref_lla[0], lon=ref_lla[1], alt=ref_lla[2], + ) alt_vals = np.array([500., 800., 1200., 1800.]) def init_diff_alt(): - geom = SimulatorGeometry(4, True, ref) - geom.set_local_coord_sys( - np.repeat([ref[0]], 4), - np.repeat([ref[1]], 4), - alt_vals, + geom = SimulatorGeometry(4, True, ref_frame) + geom.set_local_reference_frame( + ENUReferenceFrame( + lat=np.repeat([ref_lla[0]], 4), + lon=np.repeat([ref_lla[1]], 4), + alt=alt_vals, + ) ) return geom diff_alt = init_diff_alt() @@ -124,7 +475,7 @@ def init_diff_alt(): "azim": np.array([90., 10., -179., 180.]), "elev": np.array([89., -89., -1., 12.]), } expect_global = deepcopy(expect_local) - alt_vals_diff = ref[2] - alt_vals + alt_vals_diff = ref_lla[2] - alt_vals expect_global["z"] -= alt_vals_diff diff_alt.set_global_coords(**expect_global) @@ -147,7 +498,10 @@ def init_diff_alt(): def test_setting_different_llas(self): """Test setting coordinates when local should eq global """ - ref = (0.0, 0.0, 0.0) + ref_lla = (0.0, 0.0, 0.0) + ref_frame = ENUReferenceFrame( + lat=ref_lla[0], lon=ref_lla[1], alt=ref_lla[2], + ) llas = np.array([ [-90., 0., 0.], @@ -157,11 +511,13 @@ def test_setting_different_llas(self): ]).T def init_diff_llas(): - geom = SimulatorGeometry(4, True, ref) - geom.set_local_coord_sys( - llas[0], - llas[1], - llas[2], + geom = SimulatorGeometry(4, True, ref_frame) + geom.set_local_reference_frame( + ENUReferenceFrame( + lat=llas[0], + lon=llas[1], + alt=llas[2], + ) ) return geom @@ -203,7 +559,10 @@ def test_get_local_distance_to_diff_ref(self): """Tests getting local distance from a station to another when they have different local references """ - ref = (90., 0., 0.) + ref_lla = (90., 0., 0.) + ref_frame = ENUReferenceFrame( + lat=ref_lla[0], lon=ref_lla[1], alt=ref_lla[2], + ) local_llas = np.array([ [0., 0., 0.], [0., 1., 0.], @@ -213,11 +572,13 @@ def test_get_local_distance_to_diff_ref(self): ]).T def init_geom(): - geom = SimulatorGeometry(5, True, ref) - geom.set_local_coord_sys( - local_llas[0], - local_llas[1], - local_llas[2], + geom = SimulatorGeometry(5, True, ref_frame) + geom.set_local_reference_frame( + ENUReferenceFrame( + lat=local_llas[0], + lon=local_llas[1], + alt=local_llas[2], + ) ) geom.set_local_coords( np.repeat(0., 5), @@ -268,7 +629,10 @@ def test_get_local_distance_to_same_ref(self): """Tests getting local distance from a station to another when they have same local references """ - ref = (90., 0., 0.) + ref_lla = (90., 0., 0.) + ref_frame = ENUReferenceFrame( + lat=ref_lla[0], lon=ref_lla[1], alt=ref_lla[2], + ) local_llas = np.array([ [0., 0., 0.], [0., 0., 0.], @@ -277,11 +641,13 @@ def test_get_local_distance_to_same_ref(self): ]).T def init_geom(): - geom = SimulatorGeometry(4, True, ref) - geom.set_local_coord_sys( - local_llas[0], - local_llas[1], - local_llas[2], + geom = SimulatorGeometry(4, True, ref_frame) + geom.set_local_reference_frame( + ENUReferenceFrame( + lat=local_llas[0], + lon=local_llas[1], + alt=local_llas[2], + ) ) geom.set_local_coords( np.tile([0., 20.], 2), @@ -353,7 +719,10 @@ def init_geom(): def test_get_local_elevation(self): """Tests getting local elevation from a station to another. """ - ref = (90., 0., 0.) + ref_lla = (90., 0., 0.) + ref_frame = ENUReferenceFrame( + lat=ref_lla[0], lon=ref_lla[1], alt=ref_lla[2], + ) local_llas = np.array([ [0., 0., 0.], [0., 0., 0.], @@ -362,11 +731,13 @@ def test_get_local_elevation(self): ]).T def init_geom(): - geom = SimulatorGeometry(4, True, ref) - geom.set_local_coord_sys( - local_llas[0], - local_llas[1], - local_llas[2], + geom = SimulatorGeometry(4, True, ref_frame) + geom.set_local_reference_frame( + ENUReferenceFrame( + lat=local_llas[0], + lon=local_llas[1], + alt=local_llas[2], + ) ) geom.set_local_coords( np.tile([0., 20.], 2), From 88902e29b3a47d15dd2900caf2756324a6feed1d Mon Sep 17 00:00:00 2001 From: artistrea Date: Mon, 2 Feb 2026 16:52:31 -0300 Subject: [PATCH 68/77] hotfix: topology sampling from spherical grid --- sharc/topology/topology_spherical_sampling_from_grid.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sharc/topology/topology_spherical_sampling_from_grid.py b/sharc/topology/topology_spherical_sampling_from_grid.py index e93162959..552b0f437 100644 --- a/sharc/topology/topology_spherical_sampling_from_grid.py +++ b/sharc/topology/topology_spherical_sampling_from_grid.py @@ -98,6 +98,7 @@ def calculate_coordinates(self, random_number_gen=np.random.RandomState()): ), ) lat, lon, alt = chosen_llas + self.chosen_lat, self.chosen_lon, self.chosen_alt = lat, lon, alt # coords locais para determinar # transformação global <> local geom.set_local_reference_frame( @@ -135,13 +136,13 @@ def get_ue_geometry(self, ue_k: int) -> SimulatorGeometry: ue_geom = SimulatorGeometry( self.num_base_stations * ue_k, self.num_base_stations * ue_k, - self.global_cs, + self.bs_geometry.global_reference_frame, ) ue_geom.set_local_reference_frame( ENUReferenceFrame( - lat=np.repeat(self.bs_geometry._local_lla_references[0], ue_k), - lon=np.repeat(self.bs_geometry._local_lla_references[1], ue_k), - alt=np.repeat(self.bs_geometry._local_lla_references[2], ue_k), + lat=np.repeat(self.chosen_lat, ue_k), + lon=np.repeat(self.chosen_lon, ue_k), + alt=np.repeat(self.chosen_alt, ue_k), ) ) return ue_geom From 8df084d601b0c0b91b68be833309c916ee9f8cac Mon Sep 17 00:00:00 2001 From: artistrea Date: Mon, 2 Feb 2026 16:54:07 -0300 Subject: [PATCH 69/77] update: array antenna for local coordinate system AND faster computation --- sharc/antenna/antenna_array.py | 145 ++++++++++++++++++++++----------- 1 file changed, 99 insertions(+), 46 deletions(-) diff --git a/sharc/antenna/antenna_array.py b/sharc/antenna/antenna_array.py index 582633beb..18dea309a 100644 --- a/sharc/antenna/antenna_array.py +++ b/sharc/antenna/antenna_array.py @@ -9,6 +9,7 @@ from sharc.antenna.antenna import Antenna from sharc.parameters.imt.parameters_antenna_imt import ParametersAntennaImt from sharc.support.geometry import RigidTransform +from sharc.support.sharc_geom import polar_to_cartesian, cartesian_to_polar import numpy as np @@ -19,10 +20,26 @@ class AntennaArray(Antenna): def __init__( self, par: ParametersAntennaImt, - # global2local_transform: RigidTransform + global2local_transform: RigidTransform = None, ): super().__init__() self.par = par + self.always_first_beam = False + + self.global2local_transform = global2local_transform + if self.global2local_transform is not None: + if self.global2local_transform.N > 1: + raise ValueError( + "global2local_transform is supposed to have a single" + " transformation for the purposes of antenna calculations" + ) + + def set_always_first_beam(self): + """ + In case this is called, then calculate_gains will sum all beams + contributions for each direction angle. + """ + self.always_first_beam = True def calculate_gain(self, *args, **kwargs) -> np.array: """ @@ -35,7 +52,9 @@ def calculate_gain(self, *args, **kwargs) -> np.array: self.par.adjacent_antenna_model == "SINGLE_ELEMENT" and not co_channel ) - if "beams_l" in kwargs.keys(): + if self.always_first_beam: + beam_idxs = np.zeros(len(phi_vec), dtype=int) + elif "beams_l" in kwargs.keys(): beam_idxs = np.asarray(kwargs["beams_l"], dtype=int) else: beam_idxs = np.arange(len(phi_vec)) @@ -74,29 +93,35 @@ def _array_gain( phi: np.ndarray, theta: np.ndarray, beam_idxs: np.ndarray ): - v_vec = self._super_position_vector( - phi, theta, - self.par.n_rows, self.par.n_columns, - self.par.element_vert_spacing, - self.par.element_horiz_spacing, - ) if len(self.beams_list) == 0: beam_phi, beam_theta = phi, theta else: beam_phi, beam_theta = np.array(self.beams_list).T + beam_phi, beam_theta = self._to_local_coord(beam_phi, beam_theta) beam_etilt = beam_theta - 90. - beams_w_vec = self._weight_vector( + beams_w_vec_row, beams_w_vec_col = self._weight_vector( beam_phi, beam_etilt, self.par.n_rows, self.par.n_columns, self.par.element_vert_spacing, self.par.element_horiz_spacing, ) - w_vec = beams_w_vec[beam_idxs] + w_vec_row, w_vec_col = beams_w_vec_row[beam_idxs], beams_w_vec_col[beam_idxs] + + v_vec_row, v_vec_col = self._super_position_vector( + phi, theta, + self.par.n_rows, self.par.n_columns, + self.par.element_vert_spacing, + self.par.element_horiz_spacing, + ) + # NOTE: this formula has the same result to the one presented on M.2101 + # but it is optimized for computation + # considering W(m, n) = W(m)W(n) and V(m, n) = V(m)V(n) g = 10 * np.log10( abs( - np.sum(v_vec * w_vec, axis=(1, 2)) + np.sum(v_vec_row * w_vec_row, axis=-1) + * np.sum(v_vec_col * w_vec_col, axis=-1) )**2 ) @@ -120,29 +145,33 @@ def _weight_vector( Returns ------- - w_vec (np.array): weighting vector + w_vec (np.array, np.array): + weighting vectors, first for rows, second for columns """ # shape (Na, 1, 1) r_phi = np.atleast_1d( np.deg2rad(phi_tilt) - )[:, np.newaxis, np.newaxis] + )[:, np.newaxis] r_theta = np.atleast_1d( np.deg2rad(theta_tilt) - )[:, np.newaxis, np.newaxis] + )[:, np.newaxis] # shape (1, Nr, 1) - n = np.arange(n_rows)[np.newaxis, :, np.newaxis] + 1 + n = np.arange(n_rows)[np.newaxis, :] + 1 # shape (1, 1, Nc) - m = np.arange(n_cols)[np.newaxis, np.newaxis, :] + 1 + m = np.arange(n_cols)[np.newaxis, :] + 1 + + exp_arg_n = (n - 1) * dv * np.sin(r_theta) + exp_arg_m = (m - 1) * dh * np.cos(r_theta) * np.sin(r_phi) - exp_arg = (n - 1) * dv * np.sin(r_theta) - \ - (m - 1) * dh * np.cos(r_theta) * np.sin(r_phi) + w_vec_n = (1 / np.sqrt(n_rows * n_cols)) *\ + np.exp(2 * np.pi * 1.0j * exp_arg_n) - w_vec = (1 / np.sqrt(n_rows * n_cols)) *\ - np.exp(2 * np.pi * 1.0j * exp_arg) + w_vec_m = (1 / np.sqrt(n_rows * n_cols)) *\ + np.exp(2 * np.pi * 1.0j * exp_arg_m) # shape (Na, Nr, Nc) - return w_vec + return (w_vec_n, w_vec_m) @staticmethod def _super_position_vector( @@ -163,26 +192,27 @@ def _super_position_vector( ------- v_vec (np.array): superposition vector """ - # shape (Na, 1, 1) - r_phi = np.atleast_1d( - np.deg2rad(phi) - )[:, np.newaxis, np.newaxis] - r_theta = np.atleast_1d( - np.deg2rad(theta) - )[:, np.newaxis, np.newaxis] + phi = np.atleast_1d(phi) + theta = np.atleast_1d(theta) - # shape (1, Nr, 1) - n = np.arange(n_rows)[np.newaxis, :, np.newaxis] + 1 - # shape (1, 1, Nc) - m = np.arange(n_cols)[np.newaxis, np.newaxis, :] + 1 + # (Na,) + A = dv * np.cos(np.deg2rad(theta)) + B = dh * np.sin(np.deg2rad(theta)) * np.sin(np.deg2rad(phi)) - exp_arg = (n - 1) * dv * np.cos(r_theta) + \ - (m - 1) * dh * np.sin(r_theta) * np.sin(r_phi) + # indices + n = np.arange(n_rows) # (Nr,) + m = np.arange(n_cols) # (Nc,) - v_vec = np.exp(2 * np.pi * 1.0j * exp_arg) + # (Na, Nr) + row_phase = np.exp( + 2j * np.pi * A[:, None] * n[None, :] + ) + # (Na, Nc) + col_phase = np.exp( + 2j * np.pi * B[:, None] * m[None, :] + ) - # shape (Na, Nr, Nc) - return v_vec + return (row_phase, col_phase) @staticmethod def _element_gain_dispatch(par: ParametersAntennaImt, phi, theta): @@ -212,7 +242,7 @@ def _calculate_m2101_element_gain( multiplication_factor * (phi / phi_3db)**2, am ) g_vertical = -1.0 * np.minimum( - multiplication_factor * ((theta-90.) / theta_3db)**2, sla_v + multiplication_factor * ((theta - 90.) / theta_3db)**2, sla_v ) att = -1.0 * ( @@ -223,7 +253,20 @@ def _calculate_m2101_element_gain( return g_max - np.minimum(att, am) def _to_local_coord(self, phi, theta): - return np.array(phi), np.array(theta) + if self.global2local_transform is None: + return np.array(phi), np.array(theta) + + theta_from_plane = 90 - theta + vecs = np.stack(polar_to_cartesian(1, phi, theta_from_plane), axis=-1) + transformed_vecs = self.global2local_transform.apply_vectors( + vecs + ) + x, y, z = transformed_vecs.T + _r, phi, elev_from_plane = cartesian_to_polar(x, y, z) + + theta = 90 - elev_from_plane + + return phi, theta def add_beam(self, phi_etilt: float, theta_etilt: float): """ @@ -236,10 +279,11 @@ def add_beam(self, phi_etilt: float, theta_etilt: float): phi_etilt (float): azimuth electrical tilt angle [degrees] theta_etilt (float): elevation electrical tilt angle [degrees] """ - # phi_etilt, theta_etilt = np.atleast_1d(phi_etilt), np.atleast_1d(theta_etilt) - phi, theta = self._to_local_coord(phi_etilt, theta_etilt) + phi_etilt, theta_etilt = np.atleast_1d(phi_etilt), np.atleast_1d(theta_etilt) + + # def add_beam_in_local_coords(self): self.beams_list.append( - (np.ndarray.item(phi), np.ndarray.item(theta)), + (np.ndarray.item(phi_etilt), np.ndarray.item(theta_etilt)), ) @@ -262,6 +306,13 @@ def add_beam(self, phi_etilt: float, theta_etilt: float): antenna_params.multiplication_factor = 12 par = antenna_params.get_antenna_parameters() + # from sharc.support.geometry import ENUReferenceFrame + # ref_frame = ENUReferenceFrame( + # lat=np.array([90.]), + # lon=np.array([-90.]), + # alt=np.array([0.]), + # ) + # antenna = AntennaArray(par, ref_frame.from_ecef) antenna = AntennaArray(par) antenna.add_beam(np.array(0.), np.array(90.)) @@ -285,9 +336,9 @@ def add_beam(self, phi_etilt: float, theta_etilt: float): ax1 = fig.add_subplot(121) ax1.plot(phi_scan, gain) - top_y_lim = np.ceil(np.max(gain) / 10) * 10 + top_y_lim1 = np.ceil(np.max(gain) / 10) * 10 ax1.set_xlim(-180, 180) - ax1.set_ylim(top_y_lim - 60, top_y_lim) + ax1.set_ylim(top_y_lim1 - 60, top_y_lim1) ax1.grid(True) ax1.set_xlabel(r"$\varphi$ [deg]") ax1.set_ylabel("Gain [dBi]") @@ -308,9 +359,11 @@ def add_beam(self, phi_etilt: float, theta_etilt: float): ax2 = fig.add_subplot(122, sharey=ax1) ax2.plot(theta_scan, gain) - top_y_lim = np.ceil(np.max(gain) / 10) * 10 + top_y_lim2 = np.ceil(np.max(gain) / 10) * 10 + top_y_lim2 = np.maximum(top_y_lim1, top_y_lim2) + ax2.set_xlim(0, 180.) - ax2.set_ylim(top_y_lim - 60, top_y_lim) + ax2.set_ylim(top_y_lim2 - 60, top_y_lim2) ax2.grid(True) ax2.set_xlabel(r"$\vartheta$ [deg]") ax2.set_ylabel("Gain [dBi]") From 2622b8c2ee2a8efd24ebe668ce84020aefb984ff Mon Sep 17 00:00:00 2001 From: artistrea Date: Mon, 2 Feb 2026 16:58:05 -0300 Subject: [PATCH 70/77] update: antenna array2 in generate_mss_d2d & DWNReferenceFrame for it working --- sharc/parameters/parameters_antenna.py | 6 ++- sharc/station_factory.py | 46 +++++++++++++++++-- sharc/support/geometry.py | 42 ++++++++++++++--- tests/test_geometry.py | 62 +++++++++++++++++++++++--- 4 files changed, 141 insertions(+), 15 deletions(-) diff --git a/sharc/parameters/parameters_antenna.py b/sharc/parameters/parameters_antenna.py index a8fc8a0e0..189514c9a 100644 --- a/sharc/parameters/parameters_antenna.py +++ b/sharc/parameters/parameters_antenna.py @@ -26,6 +26,7 @@ class ParametersAntenna(ParametersBase): "ITU-R S.1855", "ITU-R Reg. RR. Appendice 7 Annex 3", "ARRAY", + "ARRAY2", "ITU-R-S.1528-Taylor", "ITU-R-S.1528-Section1.2", "ITU-R-S.1528-LEO", @@ -41,6 +42,7 @@ class ParametersAntenna(ParametersBase): "ITU-R S.1855", "ITU-R Reg. RR. Appendice 7 Annex 3", "ARRAY", + "ARRAY2", "ITU-R-S.1528-Taylor", "ITU-R-S.1528-Section1.2", "ITU-R-S.1528-LEO", @@ -150,7 +152,7 @@ def validate(self, ctx): f"{ctx}.pattern should be set. Is None instead", ) - if self.pattern != "ARRAY" and self.gain is None: + if self.pattern != "ARRAY" and self.pattern != "ARRAY2" and self.gain is None: raise ValueError( f"{ctx}.gain should be set if not using array antenna.", ) @@ -182,7 +184,7 @@ def validate(self, ctx): # just hijacking validation since diameter is optional self.itu_reg_rr_a7_3.diameter = 0 self.itu_reg_rr_a7_3.validate(f"{ctx}.itu_reg_rr_a7_3") - case "ARRAY": + case "ARRAY" | "ARRAY2": # TODO: validate here and make array non imt specific # self.array.validate( # f"{ctx}.array", diff --git a/sharc/station_factory.py b/sharc/station_factory.py index 02384d4f2..a6666863d 100644 --- a/sharc/station_factory.py +++ b/sharc/station_factory.py @@ -10,6 +10,9 @@ import sys import math +from sharc.support.geometry import ( + DWNReferenceFrame, SimulatorGeometry, ENUReferenceFrame +) from sharc.support.enumerations import StationType from sharc.parameters.parameters import Parameters from sharc.parameters.imt.parameters_imt import ParametersImt @@ -44,6 +47,7 @@ from sharc.antenna.antenna_rs1861_9c import AntennaRS1861_9C from sharc.antenna.antenna_rs2043 import AntennaRS2043 from sharc.antenna.antenna_s465 import AntennaS465 +from sharc.antenna.antenna_array import AntennaArray from sharc.antenna.antenna_rra7_3 import AntennaReg_RR_A7_3 from sharc.antenna.antenna_modified_s465 import AntennaModifiedS465 from sharc.antenna.antenna_s580 import AntennaS580 @@ -1746,12 +1750,29 @@ def generate_mss_d2d( 1e6) + 30 ) - # Configure satellite positions in the StationManager + # Configure satellite positions in the StationManager x = mss_d2d_values["sat_x"] y = mss_d2d_values["sat_y"] z = mss_d2d_values["sat_z"] elev = mss_d2d_values["sat_antenna_elev"] azim = mss_d2d_values["sat_antenna_azim"] + global_ref = ENUReferenceFrame( + lat=coordinate_system.ref_lat, + lon=coordinate_system.ref_long, + alt=coordinate_system.ref_alt, + ) + mss_d2d.geom.setup( + mss_d2d.num_stations, True, + global_ref, + ) + mss_d2d.geom.set_local_reference_frame( + DWNReferenceFrame( + lat=mss_d2d_values["sat_lat"], + lon=mss_d2d_values["sat_lon"], + alt=mss_d2d_values["sat_alt"], + ) + ) + mss_d2d.geom.set_global_coords( x, y, z, azim, elev, @@ -1781,6 +1802,8 @@ def generate_mss_d2d( antenna_pattern = AntennaS1528(params.antenna.itu_r_s_1528) elif params.antenna.pattern == "ITU-R-S.1528-Taylor": antenna_pattern = AntennaS1528Taylor(params.antenna.itu_r_s_1528) + elif params.antenna.pattern == "ARRAY2": + pass elif params.antenna.pattern == "MSS Adjacent": antenna_pattern = AntennaMSSAdjacent(params.frequency) else: @@ -1788,8 +1811,25 @@ def generate_mss_d2d( f"generate_mss_ss: Invalid antenna type: {params.antenna.pattern}") for i in range(mss_d2d.num_stations): + if params.antenna.pattern == "ARRAY2": + antenna_pattern = AntennaArray( + params.antenna.array, + mss_d2d.geom.global2local.take(i) + ) + antenna_pattern.add_beam( + mss_d2d.geom.pointn_azim_global[i], + 90. - mss_d2d.geom.pointn_elev_global[i], + ) + antenna_pattern.set_always_first_beam() + mss_d2d.antenna[i] = antenna_pattern + if params.antenna.pattern == "ARRAY2": + mss_d2d.geom.set_local_coords( + azim=np.zeros_like(mss_d2d.geom.pointn_azim_global), + elev=np.zeros_like(mss_d2d.geom.pointn_elev_global), + ) + return mss_d2d # Return the configured StationManager @staticmethod @@ -1982,8 +2022,8 @@ def get_random_position(num_stas: int, parameters.imt.topology.sampling_from_spherical_grid.grid.grid_in_zone.circle.center_lon = ref_long parameters.imt.topology.sampling_from_spherical_grid.grid.grid_in_zone.circle.radius_km = 30 * 111 - parameters.imt.topology.type = "SAMPLING_FROM_SPHERICAL_GRID" - # parameters.imt.topology.type = "MSS_DC" + # parameters.imt.topology.type = "SAMPLING_FROM_SPHERICAL_GRID" + parameters.imt.topology.type = "MSS_DC" parameters.imt.validate("station_factory_imt") # print( # "parameters.imt.topology.sampling_from_spherical_grid.grid.lon_lat_grid.shape", diff --git a/sharc/support/geometry.py b/sharc/support/geometry.py index 94a937122..1fc9b5480 100644 --- a/sharc/support/geometry.py +++ b/sharc/support/geometry.py @@ -354,8 +354,8 @@ def take(self, idx: int) -> "RigidTransform": # array shape structure for functional broadcasting # and batch computing return RigidTransform( - rot=self.rot[idx:idx+1], - t=self.t[idx:idx+1], + rot=self.rot[idx:idx + 1], + t=self.t[idx:idx + 1], ) def apply_points(self, x: np.ndarray) -> np.ndarray: @@ -453,6 +453,9 @@ def __init__( self._alt.flags.writeable = False def _compute_to_local(self): + return self._compute_to_enu() + + def _compute_to_enu(self): lat = self._lat lon = self._lon alt = self._alt @@ -475,6 +478,31 @@ def _compute_to_local(self): return ecef2local +class DWNReferenceFrame(ENUReferenceFrame): + """ + Defines DWN reference frame. x=Down, y=West, z=North + DWN is a custom reference frame for simplification of satellite's ref frame + useful for antenna usage. + """ + ENU2DWN_ROT = scipy.spatial.transform.Rotation.from_matrix( + np.array([[ + [0, 0, -1], + [-1, 0, 0], + [0, 1, 0], + ]]) + ) + + def _compute_to_local(self): + ecef2enu = self._compute_to_enu() + # basis change + # ENU <> x: east, y: north, z: up + # DWN <> x: down, y: west, z: north + # x2 = -z1, y2 = -x1, z2 = y + enu2dwn = RigidTransform(self.ENU2DWN_ROT, np.zeros((1, 3))) + + return ecef2enu.and_then(enu2dwn) + + @readonly_properties( "x_local", "y_local", "z_local", "pointn_azim_local", "pointn_elev_local", @@ -501,7 +529,7 @@ def __init__( self, num_geometries, uses_local_coords=False, - global_cs: tuple[float, float, float] = None, + global_cs: ReferenceFrame = None, ): """ Initialize a geometry object with a global coordinate system @@ -554,6 +582,10 @@ def set_local_reference_frame( Sets local coord system references and prepares global<->local coordinate transformation """ + if not self.uses_local_coords: + raise ValueError( + "Cannot set local reference frame when not using local coords" + ) self._local_reference_frame = frame self._compute_global_local_transform() @@ -926,14 +958,14 @@ def plot_geom( ) bs_geom.set_local_reference_frame( - ENUReferenceFrame( + DWNReferenceFrame( lat=np.repeat(11, topology.num_base_stations), lon=np.repeat(-47, topology.num_base_stations), alt=np.repeat(1200, topology.num_base_stations), ) ) ue_geom.set_local_reference_frame( - ENUReferenceFrame( + DWNReferenceFrame( lat=np.repeat(11, num_ue * topology.num_base_stations), lon=np.repeat(-47, num_ue * topology.num_base_stations), alt=np.repeat(1200, num_ue * topology.num_base_stations), diff --git a/tests/test_geometry.py b/tests/test_geometry.py index a9f68c113..ee6fa2616 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1,8 +1,9 @@ import unittest import numpy as np import numpy.testing as npt +from sharc.support.sharc_utils import wrap2_180 from sharc.support.geometry import ( - SimulatorGeometry, ENUReferenceFrame, RigidTransform + SimulatorGeometry, DWNReferenceFrame, ENUReferenceFrame, RigidTransform ) from sharc.satellite.ngso.constants import EARTH_RADIUS_M from copy import deepcopy @@ -26,21 +27,25 @@ def random_rigid_transform(rng, N): return RigidTransform(Rotation.from_matrix(Q), t) + def rot_identity(n=1): return Rotation.from_rotvec(np.zeros((n, 3))) + def rot_z(angle_deg, n=1): return Rotation.from_rotvec( np.tile([0.0, 0.0, angle_deg], (n, 1)), degrees=True ) + def rot_x(angle_deg, n=1): return Rotation.from_rotvec( np.tile([angle_deg, 0.0, 0.0], (n, 1)), degrees=True ) + def rot_y(angle_deg, n=1): return Rotation.from_rotvec( np.tile([0.0, angle_deg, 0.0], (n, 1)), @@ -69,11 +74,11 @@ def test_init_and_broadcasting(self): if Nt > 1: with self.assertRaises(ValueError): - RigidTransform(rot_identity(N+1), t) + RigidTransform(rot_identity(N + 1), t) if Nrot > 1: with self.assertRaises(ValueError): - RigidTransform(rot, np.zeros((N+1, 3))) + RigidTransform(rot, np.zeros((N + 1, 3))) # should not throw: tr = RigidTransform(rot, t) @@ -84,7 +89,7 @@ def test_init_and_broadcasting(self): for fn in [tr.apply_points, tr.apply_vectors]: if N > 1: with self.assertRaises(AssertionError): - fn(np.zeros((N+1, 3))) + fn(np.zeros((N + 1, 3))) for in_shp in [(1, 3), (N, 3)]: res = fn(np.zeros(in_shp)) @@ -94,7 +99,7 @@ def test_init_and_broadcasting(self): for fn, in_shp in product( [tr.apply_points_permutation, tr.apply_vectors_permutation], - [(1, 3), (N, 3), (N+1, 3)] + [(1, 3), (N, 3), (N + 1, 3)] ): Nin = in_shp[0] res = fn(np.zeros(in_shp)) @@ -338,6 +343,53 @@ def test_take_matches_permutation_vectors(self): ) +class TestDWNReferenceFrame(unittest.TestCase): + def setUp(self): + self.lat = np.array([0.0]) + self.lon = np.array([0.0]) + self.alt = np.array([0.0]) + + self.enu = ENUReferenceFrame( + lat=self.lat, lon=self.lon, alt=self.alt + ) + self.dwn = DWNReferenceFrame( + lat=self.lat, lon=self.lon, alt=self.alt + ) + + def test_enu_to_dwn_basis(self): + # ENU basis vectors + e = np.array([1.0, 0.0, 0.0]) # East + n = np.array([0.0, 1.0, 0.0]) # North + u = np.array([0.0, 0.0, 1.0]) # Up + + # Transform ENU -> ECEF -> DWN + e_dwn = self.dwn.from_ecef.apply_vectors( + self.enu.to_ecef.apply_vectors(e) + )[0] + n_dwn = self.dwn.from_ecef.apply_vectors( + self.enu.to_ecef.apply_vectors(n) + )[0] + u_dwn = self.dwn.from_ecef.apply_vectors( + self.enu.to_ecef.apply_vectors(u) + )[0] + + npt.assert_allclose(e_dwn, np.array([0.0, -1.0, 0.0]), atol=1e-4) + npt.assert_allclose(n_dwn, np.array([0.0, 0.0, 1.0]), atol=1e-4) + npt.assert_allclose(u_dwn, np.array([-1.0, 0.0, 0.0]), atol=1e-4) + npt.assert_allclose( + np.linalg.norm(e_dwn), + np.linalg.norm(e), + ) + npt.assert_allclose( + np.linalg.norm(n_dwn), + np.linalg.norm(n), + ) + npt.assert_allclose( + np.linalg.norm(u_dwn), + np.linalg.norm(u), + ) + + class TestGeometry(unittest.TestCase): """Unit tests for the CoordinateSystem class and related coordinate transformations.""" From e957c90ca4c9ac2f1542e1ba5e9d1c54f4016d37 Mon Sep 17 00:00:00 2001 From: artistrea Date: Mon, 2 Feb 2026 18:31:02 -0300 Subject: [PATCH 71/77] update: better performance for antenna array implementation --- sharc/antenna/antenna_array.py | 78 +++++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 15 deletions(-) diff --git a/sharc/antenna/antenna_array.py b/sharc/antenna/antenna_array.py index 18dea309a..6c7f0dabf 100644 --- a/sharc/antenna/antenna_array.py +++ b/sharc/antenna/antenna_array.py @@ -100,7 +100,7 @@ def _array_gain( beam_phi, beam_theta = self._to_local_coord(beam_phi, beam_theta) beam_etilt = beam_theta - 90. - beams_w_vec_row, beams_w_vec_col = self._weight_vector( + beams_w_vec_row, beams_w_vec_col = self._weight_vector_components( beam_phi, beam_etilt, self.par.n_rows, self.par.n_columns, self.par.element_vert_spacing, @@ -108,7 +108,7 @@ def _array_gain( ) w_vec_row, w_vec_col = beams_w_vec_row[beam_idxs], beams_w_vec_col[beam_idxs] - v_vec_row, v_vec_col = self._super_position_vector( + v_vec_row, v_vec_col = self._super_position_vector_components( phi, theta, self.par.n_rows, self.par.n_columns, self.par.element_vert_spacing, @@ -127,12 +127,44 @@ def _array_gain( return g + @staticmethod + def _super_position_vector( + phi_tilt: np.ndarray, + theta_tilt: np.ndarray, + n_rows: int, n_cols: int, + dv: float, dh: float, + ) -> np.array: + vn, vm = AntennaArray._super_position_vector_components( + phi_tilt, + theta_tilt, + n_rows, n_cols, + dv, dh, + ) + + return vn[:, :, None] * vm[:, None, :] + @staticmethod def _weight_vector( phi_tilt: np.ndarray, theta_tilt: np.ndarray, n_rows: int, n_cols: int, dv: float, dh: float, + ) -> np.array: + wn, wm = AntennaArray._weight_vector_components( + phi_tilt, + theta_tilt, + n_rows, n_cols, + dv, dh, + ) + + return wn[:, :, None] * wm[:, None, :] + + @staticmethod + def _weight_vector_components( + phi_tilt: np.ndarray, + theta_tilt: np.ndarray, + n_rows: int, n_cols: int, + dv: float, dh: float, ) -> np.array: """ Calculates super position vector. @@ -162,19 +194,18 @@ def _weight_vector( m = np.arange(n_cols)[np.newaxis, :] + 1 exp_arg_n = (n - 1) * dv * np.sin(r_theta) - exp_arg_m = (m - 1) * dh * np.cos(r_theta) * np.sin(r_phi) + exp_arg_m = - (m - 1) * dh * np.cos(r_theta) * np.sin(r_phi) w_vec_n = (1 / np.sqrt(n_rows * n_cols)) *\ np.exp(2 * np.pi * 1.0j * exp_arg_n) - w_vec_m = (1 / np.sqrt(n_rows * n_cols)) *\ - np.exp(2 * np.pi * 1.0j * exp_arg_m) + w_vec_m = np.exp(2 * np.pi * 1.0j * exp_arg_m) # shape (Na, Nr, Nc) return (w_vec_n, w_vec_m) @staticmethod - def _super_position_vector( + def _super_position_vector_components( phi: float, theta: float, n_rows: int, n_cols: int, dv: float, dh: float, @@ -199,18 +230,35 @@ def _super_position_vector( A = dv * np.cos(np.deg2rad(theta)) B = dh * np.sin(np.deg2rad(theta)) * np.sin(np.deg2rad(phi)) - # indices - n = np.arange(n_rows) # (Nr,) - m = np.arange(n_cols) # (Nc,) + # instead of calculating exp for every row, there is a recursive + # relation that speeds this up. Small n_rows means that floating + # point error should not accumulate + # The relationship is: V(n) = V(n-1)V(2) | n > 2 + # V(1) = 1., V(2) = exp(...) + row_phase = np.empty((len(theta), n_rows), dtype=np.complex128) + row_phase[:, 0] = 1. - # (Na, Nr) - row_phase = np.exp( - 2j * np.pi * A[:, None] * n[None, :] + row_phase_term = np.exp( + 2j * np.pi * A ) - # (Na, Nc) - col_phase = np.exp( - 2j * np.pi * B[:, None] * m[None, :] + row_phase[:, 1:] = row_phase_term[:, None] + # recursive relationship by cumulative product + row_phase = np.cumprod(row_phase, axis=-1) + + # instead of calculating exp for every col, there is a recursive + # relation that speeds this up. Small n_cols means that floating + # point error should not accumulate + # The relationship is: V(n) = V(n-1)V(2) | n > 2 + # V(1) = 1., V(2) = exp(...) + col_phase = np.empty((len(theta), n_cols), dtype=np.complex128) + col_phase[:, 0] = 1. + + col_phase_term = np.exp( + 2j * np.pi * B ) + col_phase[:, 1:] = col_phase_term[:, None] + # recursive relationship by cumulative product + col_phase = np.cumprod(col_phase, axis=-1) return (row_phase, col_phase) From 0dd5d9bf45901914955f30bd6c30390cf82dd3fd Mon Sep 17 00:00:00 2001 From: artistrea Date: Mon, 2 Feb 2026 22:57:24 -0300 Subject: [PATCH 72/77] update: antenna array comments, signatures and minor changes --- sharc/antenna/antenna_array.py | 150 ++++++++++++++++++++++++++++----- 1 file changed, 128 insertions(+), 22 deletions(-) diff --git a/sharc/antenna/antenna_array.py b/sharc/antenna/antenna_array.py index 6c7f0dabf..80c4352f3 100644 --- a/sharc/antenna/antenna_array.py +++ b/sharc/antenna/antenna_array.py @@ -1,4 +1,6 @@ """ +Optimized implementation of M.2101 antenna array. + This antenna was created after the already existing antenna_beamforming_imt since that implementation is too slow for use with a lot of stations. @@ -12,16 +14,34 @@ from sharc.support.sharc_geom import polar_to_cartesian, cartesian_to_polar import numpy as np +import typing class AntennaArray(Antenna): - # par: ParametersAntennaImt + """Implements M.2101 antenna array.""" def __init__( self, par: ParametersAntennaImt, global2local_transform: RigidTransform = None, ): + """Constructs antenna array. + + Parameters + ---------- + par: ParametersAntennaImt + Antenna parameters. Partial support only. + global2local_transform: RigidTransform, optional + Transformation from global to local coordinate system. If None, + no transformation is applied. + + Notes + ----- + By partial support, it is meant that not all parameters + from ParametersAntennaImt are used in this implementation. + For example, normalization and subarray support are not + implemented. + """ super().__init__() self.par = par self.always_first_beam = False @@ -35,15 +55,33 @@ def __init__( ) def set_always_first_beam(self): - """ + """Sets the antenna to always use the first beam. + In case this is called, then calculate_gains will sum all beams contributions for each direction angle. """ self.always_first_beam = True def calculate_gain(self, *args, **kwargs) -> np.array: - """ - Calculates the antenan gain. + """Calculates the antenna gain. + + Parameters + ---------- + phi_vec : np.ndarray + Azimuth angles [degrees] in global coordinate system. + theta_vec : np.ndarray + Elevation angles [degrees] in global coordinate system. + co_channel : bool, optional + If True, co-channel interference is considered (default is True). + beams_l : np.ndarray, optional + Indices of beams to consider for each angle. If not provided, + all beams are considered. Also, if always_first_beam is set, + this parameter is ignored. + + Returns + ------- + np.ndarray + Antenna gain [dBi] for each direction. """ phi_vec = np.atleast_1d(kwargs["phi_vec"]) theta_vec = np.atleast_1d(kwargs["theta_vec"]) @@ -84,6 +122,16 @@ def calculate_gain(self, *args, **kwargs) -> np.array: def _element_gain( self, phi: np.ndarray, theta: np.ndarray ): + """ + Calculates the element gain for given angles. + + Parameters + ---------- + phi : np.ndarray + Azimuth angles [degrees] in local coordinate system. + theta : np.ndarray + Elevation angles [degrees] in local coordinate system. + """ return self._element_gain_dispatch( self.par, phi, theta, ) @@ -93,6 +141,15 @@ def _array_gain( phi: np.ndarray, theta: np.ndarray, beam_idxs: np.ndarray ): + """Calculates the array gain for given angles and beam indices. + + Notes + ----- + The mathematical formulation is based on M.2101, but formulation + has been optimized for computational efficiency. It considers + separability of the array factor into row and column components, + allowing for reduced memory bandwidth and faster runtime. + """ if len(self.beams_list) == 0: beam_phi, beam_theta = phi, theta else: @@ -106,7 +163,10 @@ def _array_gain( self.par.element_vert_spacing, self.par.element_horiz_spacing, ) - w_vec_row, w_vec_col = beams_w_vec_row[beam_idxs], beams_w_vec_col[beam_idxs] + w_vec_row, w_vec_col = ( + beams_w_vec_row[beam_idxs], + beams_w_vec_col[beam_idxs] + ) v_vec_row, v_vec_col = self._super_position_vector_components( phi, theta, @@ -125,7 +185,7 @@ def _array_gain( )**2 ) - return g + return np.maximum(g, self.par.minimum_array_gain) @staticmethod def _super_position_vector( @@ -165,19 +225,21 @@ def _weight_vector_components( theta_tilt: np.ndarray, n_rows: int, n_cols: int, dv: float, dh: float, - ) -> np.array: + ) -> typing.Tuple[np.ndarray, np.ndarray]: """ Calculates super position vector. Angles are in the local coordinate system. Parameters ---------- - phi_tilt (float): electrical horizontal steering [degrees] - theta_tilt (float): electrical down-tilt steering [degrees] + phi_tilt: np.ndarray + electrical horizontal steering [degrees] + theta_tilt: np.ndarray + electrical down-tilt steering [degrees] Returns ------- - w_vec (np.array, np.array): + w_vec: (np.ndarray, np.ndarray) weighting vectors, first for rows, second for columns """ # shape (Na, 1, 1) @@ -201,7 +263,6 @@ def _weight_vector_components( w_vec_m = np.exp(2 * np.pi * 1.0j * exp_arg_m) - # shape (Na, Nr, Nc) return (w_vec_n, w_vec_m) @staticmethod @@ -209,19 +270,27 @@ def _super_position_vector_components( phi: float, theta: float, n_rows: int, n_cols: int, dv: float, dh: float, - ) -> np.array: + ) -> typing.Tuple[np.ndarray, np.ndarray]: """ Calculates super position vector. Angles are in the local coordinate system. Parameters ---------- - theta (float): elevation angle [degrees] - phi (float): azimuth angle [degrees] + theta: float + elevation angle [degrees] + phi: float + azimuth angle [degrees] Returns ------- - v_vec (np.array): superposition vector + v_vec: typing.Tuple[np.ndarray, np.ndarray]: + superposition vector components, first for rows, second for columns + + Notes + ----- + This implementation is optimized for computational efficiency, + using recursive relationships to avoid redundant calculations. """ phi = np.atleast_1d(phi) theta = np.atleast_1d(theta) @@ -243,7 +312,7 @@ def _super_position_vector_components( ) row_phase[:, 1:] = row_phase_term[:, None] # recursive relationship by cumulative product - row_phase = np.cumprod(row_phase, axis=-1) + np.cumprod(row_phase, axis=-1, out=row_phase) # instead of calculating exp for every col, there is a recursive # relation that speeds this up. Small n_cols means that floating @@ -258,12 +327,24 @@ def _super_position_vector_components( ) col_phase[:, 1:] = col_phase_term[:, None] # recursive relationship by cumulative product - col_phase = np.cumprod(col_phase, axis=-1) + np.cumprod(col_phase, axis=-1, out=col_phase) return (row_phase, col_phase) @staticmethod def _element_gain_dispatch(par: ParametersAntennaImt, phi, theta): + """ + Dispatch to the correct element gain calculation method. + + Parameters + ---------- + par: ParametersAntennaImt + Antenna parameters. + phi: np.ndarray + Azimuth angles [degrees] in local coordinate system. + theta: np.ndarray + Elevation angles [degrees] in local coordinate system. + """ if par.element_pattern == "M2101": return AntennaArray._calculate_m2101_element_gain( phi, theta, @@ -284,8 +365,7 @@ def _calculate_m2101_element_gain( g_max: np.ndarray, sla_v: np.ndarray, am: np.ndarray, multiplication_factor: np.ndarray = 12 ): - """Calculates and returns element gain as described in M.2101 - """ + """Calculates and returns element gain as described in M.2101.""" g_horizontal = -1.0 * np.minimum( multiplication_factor * (phi / phi_3db)**2, am ) @@ -301,6 +381,31 @@ def _calculate_m2101_element_gain( return g_max - np.minimum(att, am) def _to_local_coord(self, phi, theta): + """ + Transform angles from global to local coordinate system. + + Parameters + ---------- + phi: np.ndarray + Azimuth angles [degrees] in global coordinate system. + theta: np.ndarray + Elevation angles [degrees] in global coordinate system. + + Returns + ------- + phi: np.ndarray + Azimuth angles [degrees] in local coordinate system. + theta: np.ndarray + Elevation angles [degrees] in local coordinate system. + + Notes + ----- + The transformation is done by converting to Cartesian coordinates, + applying the transformation defined on construction, + and converting back to spherical coordinates. It is assumed that + theta is defined with z axis as reference, and phi with x axis as reference + and increasing towards y axis. + """ if self.global2local_transform is None: return np.array(phi), np.array(theta) @@ -324,12 +429,13 @@ def add_beam(self, phi_etilt: float, theta_etilt: float): Parameters ---------- - phi_etilt (float): azimuth electrical tilt angle [degrees] - theta_etilt (float): elevation electrical tilt angle [degrees] + phi_etilt: float + azimuth electrical tilt angle [degrees] + theta_etilt: float + elevation electrical tilt angle [degrees] """ phi_etilt, theta_etilt = np.atleast_1d(phi_etilt), np.atleast_1d(theta_etilt) - # def add_beam_in_local_coords(self): self.beams_list.append( (np.ndarray.item(phi_etilt), np.ndarray.item(theta_etilt)), ) From f714def774a89311f792f2330242340210a62501 Mon Sep 17 00:00:00 2001 From: artistrea Date: Mon, 2 Feb 2026 22:58:11 -0300 Subject: [PATCH 73/77] fix: missing comments --- sharc/support/geometry.py | 11 ++++++++--- tests/test_geometry.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/sharc/support/geometry.py b/sharc/support/geometry.py index 1fc9b5480..c69180195 100644 --- a/sharc/support/geometry.py +++ b/sharc/support/geometry.py @@ -333,11 +333,13 @@ def __post_init__(self): @property def N(self): + """Number of transforms represented""" n_rot = len(self.rot) n_t = self.t.shape[0] return max(n_rot, n_t) def inv(self) -> "RigidTransform": + """Returns the inverse transform""" rot_inv = self.rot.inv() t_inv = -rot_inv.apply(self.t) return RigidTransform(rot_inv, t_inv) @@ -350,6 +352,7 @@ def and_then(self, other: "RigidTransform") -> "RigidTransform": ) def take(self, idx: int) -> "RigidTransform": + """Takes the idx-th transform from the batch""" # NOTE: we slice instead of taking indice to maintain # array shape structure for functional broadcasting # and batch computing @@ -405,10 +408,12 @@ class ReferenceFrame(ABC): @property def from_ecef(self) -> RigidTransform: + """Returns the transform from ECEF to the local coordinate system.""" return self._from_ecef @property def to_ecef(self) -> RigidTransform: + """Returns the transform from the local coordinate system to ECEF.""" return self._to_ecef @@ -486,9 +491,9 @@ class DWNReferenceFrame(ENUReferenceFrame): """ ENU2DWN_ROT = scipy.spatial.transform.Rotation.from_matrix( np.array([[ - [0, 0, -1], - [-1, 0, 0], - [0, 1, 0], + [0, 0, -1], + [-1, 0, 0], + [0, 1, 0], ]]) ) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index ee6fa2616..5eb3c881b 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -29,10 +29,12 @@ def random_rigid_transform(rng, N): def rot_identity(n=1): + """Returns identity rotation of batch size n.""" return Rotation.from_rotvec(np.zeros((n, 3))) def rot_z(angle_deg, n=1): + """Returns rotation about Z axis by angle_deg degrees, batch size n.""" return Rotation.from_rotvec( np.tile([0.0, 0.0, angle_deg], (n, 1)), degrees=True @@ -40,6 +42,7 @@ def rot_z(angle_deg, n=1): def rot_x(angle_deg, n=1): + """Returns rotation about X axis by angle_deg degrees, batch size n.""" return Rotation.from_rotvec( np.tile([angle_deg, 0.0, 0.0], (n, 1)), degrees=True @@ -47,6 +50,7 @@ def rot_x(angle_deg, n=1): def rot_y(angle_deg, n=1): + """Returns rotation about Y axis by angle_deg degrees, batch size n.""" return Rotation.from_rotvec( np.tile([0.0, angle_deg, 0.0], (n, 1)), degrees=True @@ -54,10 +58,14 @@ def rot_y(angle_deg, n=1): class TestRigidTransform(unittest.TestCase): + """Unit tests for RigidTransform class.""" + def setUp(self): + """Set up test fixtures for RigidTransform tests.""" pass def test_init_and_broadcasting(self): + """Test RigidTransform initialization and broadcasting behavior.""" for rot_shp, t_shp in product( [(1, 3), (4, 3)], [(1, 3), (4, 3)], @@ -108,6 +116,7 @@ def test_init_and_broadcasting(self): ) def test_simple_transformations(self): + """Test RigidTransform simple transformations and their combinations.""" eps = 1e-4 ux = np.array([1., 0., 0.]) @@ -250,6 +259,7 @@ def test_simple_transformations(self): ) def test_permutation_points_equivalence(self): + """Test equivalence of permutation and non-permutation point applications.""" rng = np.random.default_rng(0) for n in range(1, 10): @@ -268,6 +278,7 @@ def test_permutation_points_equivalence(self): ) def test_permutation_vectors_equivalence(self): + """Test equivalence of permutation and non-permutation vector applications.""" rng = np.random.default_rng(0) for n in range(1, 10): @@ -286,6 +297,7 @@ def test_permutation_vectors_equivalence(self): ) def test_take_commutes_with_apply_points(self): + """Test that 'take' method commutes with apply_points.""" rng = np.random.default_rng(0) for n in range(1, 10): @@ -303,6 +315,7 @@ def test_take_commutes_with_apply_points(self): ) def test_take_matches_permutation_points(self): + """Test that 'take' method matches permutation point applications.""" rng = np.random.default_rng(1) for n in [2, 5]: @@ -323,6 +336,7 @@ def test_take_matches_permutation_points(self): ) def test_take_matches_permutation_vectors(self): + """Test that 'take' method matches permutation vector applications.""" rng = np.random.default_rng(1) for n in [2, 5]: @@ -344,7 +358,10 @@ def test_take_matches_permutation_vectors(self): class TestDWNReferenceFrame(unittest.TestCase): + """Unit tests for DWNReferenceFrame class.""" + def setUp(self): + """Set up test fixtures for DWNReferenceFrame tests.""" self.lat = np.array([0.0]) self.lon = np.array([0.0]) self.alt = np.array([0.0]) @@ -357,6 +374,7 @@ def setUp(self): ) def test_enu_to_dwn_basis(self): + """Test transformation of basis vectors from ENU to DWN.""" # ENU basis vectors e = np.array([1.0, 0.0, 0.0]) # East n = np.array([0.0, 1.0, 0.0]) # North From e7fd325a92443111cdbea267d3d534bd75711eea Mon Sep 17 00:00:00 2001 From: "Artur P. P." <89547366+artistrea@users.noreply.github.com> Date: Tue, 3 Feb 2026 00:25:07 -0300 Subject: [PATCH 74/77] refactor: comments and docstring for better documentation Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- sharc/antenna/antenna_array.py | 6 +++--- sharc/station_factory.py | 2 +- tests/test_antenna_array.py | 7 ++----- tests/test_geometry.py | 2 +- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/sharc/antenna/antenna_array.py b/sharc/antenna/antenna_array.py index 80c4352f3..b639cfc3f 100644 --- a/sharc/antenna/antenna_array.py +++ b/sharc/antenna/antenna_array.py @@ -57,8 +57,8 @@ def __init__( def set_always_first_beam(self): """Sets the antenna to always use the first beam. - In case this is called, then calculate_gains will sum all beams - contributions for each direction angle. + When this flag is set, :meth:`calculate_gain` ignores any ``beams_l`` + argument and selects the first beam (index 0) for all direction angles. """ self.always_first_beam = True @@ -227,7 +227,7 @@ def _weight_vector_components( dv: float, dh: float, ) -> typing.Tuple[np.ndarray, np.ndarray]: """ - Calculates super position vector. + Calculates the complex weight vectors for beamforming. Angles are in the local coordinate system. Parameters diff --git a/sharc/station_factory.py b/sharc/station_factory.py index a6666863d..322e691f9 100644 --- a/sharc/station_factory.py +++ b/sharc/station_factory.py @@ -11,7 +11,7 @@ import math from sharc.support.geometry import ( - DWNReferenceFrame, SimulatorGeometry, ENUReferenceFrame + DWNReferenceFrame, ENUReferenceFrame ) from sharc.support.enumerations import StationType from sharc.parameters.parameters import Parameters diff --git a/tests/test_antenna_array.py b/tests/test_antenna_array.py index adaf1291c..8a7c4cc0e 100644 --- a/tests/test_antenna_array.py +++ b/tests/test_antenna_array.py @@ -64,7 +64,7 @@ def setUp(self): def test_element_gain(self): """Testing element gain calculations""" - # self.antenna1._calculate_m2101_element_gain() + """Test M.2101 horizontal pattern calculation for various phi values.""" # phi = 0 results in zero gain phi = np.array([0., 120., 150.]) @@ -355,7 +355,4 @@ def test_calculate_gain(self): if __name__ == '__main__': unittest.main() -# -# suite = unittest.TestSuite() -# suite.addTest(AntennaArrayTest('test_calculate_gain')) -# unittest.TextTestRunner().run(suite) + diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 5eb3c881b..a25fa33c5 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1,7 +1,7 @@ import unittest import numpy as np import numpy.testing as npt -from sharc.support.sharc_utils import wrap2_180 + from sharc.support.geometry import ( SimulatorGeometry, DWNReferenceFrame, ENUReferenceFrame, RigidTransform ) From 04685253c649ec14ae90af0263f01f00a7ebea70 Mon Sep 17 00:00:00 2001 From: artistrea Date: Tue, 3 Feb 2026 08:46:31 -0300 Subject: [PATCH 75/77] fix: linting errors --- sharc/support/geometry.py | 76 ++++++++++++++++++++++++++++++++++--- tests/test_antenna_array.py | 1 - tests/test_geometry.py | 6 --- 3 files changed, 71 insertions(+), 12 deletions(-) diff --git a/sharc/support/geometry.py b/sharc/support/geometry.py index c69180195..98895f57f 100644 --- a/sharc/support/geometry.py +++ b/sharc/support/geometry.py @@ -318,6 +318,7 @@ def __post_init__(self): if not isinstance(self.rot, scipy.spatial.transform.Rotation): raise ValueError("rot must be a scipy Rotation") + # NOTE: this throws if the rotation isn't defined as a batch rotation n_rot = len(self.rot) n_t = self.t.shape[0] @@ -362,17 +363,63 @@ def take(self, idx: int) -> "RigidTransform": ) def apply_points(self, x: np.ndarray) -> np.ndarray: - """Applies rotations and translations to the points""" + """Applies transform to the points. + + Parameters + ---------- + x : np.ndarray + Array of input points of shape (N, 3). Each row is a 3D point in + Cartesian coordinates. + + Returns + ------- + np.ndarray + Array of transformed points of shape (N, 3), where ``N`` is the + number of transforms represented by this ``RigidTransform``. + """ return self.apply_vectors(x) + self.t def apply_points_permutation(self, x: np.ndarray) -> np.ndarray: - """Applies rotation and translation to the point""" + """Applies transform to points with permutation broadcasting. + + Parameters + ---------- + x : np.ndarray + Array of input points of shape (M, 3). Each row is a 3D point in + Cartesian coordinates. + + Returns + ------- + np.ndarray + Array of transformed points of shape (N, M, 3), where ``N`` is the + number of transforms represented by this ``RigidTransform``. The + slice ``result[n, :, :]`` contains all ``M`` input points + transformed by the ``n``-th rigid transform. + """ t = self.t # (N,3) return self.apply_vectors_permutation(x) + t[:, None, :] def apply_vectors(self, v: np.ndarray) -> np.ndarray: - """Applies only rotations, considering the vectors as a pointing vec""" + """Applies transform rotation-only to the points. + + Parameters + ---------- + x : np.ndarray + Array of input points of shape (N, 3). Each row is a 3D point in + Cartesian coordinates. + + Returns + ------- + np.ndarray + Array of rotated points of shape (N, 3), where ``N`` is the + number of transforms represented by this ``RigidTransform``. + + Note + ---- + This only applies rotation, it ignores translation. It is useful for + transforming pointing vectors. + """ v = np.atleast_2d(v) assert v.ndim == 2 and v.shape[1] == 3 M = v.shape[0] @@ -381,8 +428,27 @@ def apply_vectors(self, v: np.ndarray) -> np.ndarray: return self.rot.apply(v) + np.zeros_like(self.t) def apply_vectors_permutation(self, v: np.ndarray) -> np.ndarray: - """Applies rotation and translation to the point""" - # TODO: change impl + """Applies rotation-only to points with permutation broadcasting. + + Parameters + ---------- + x : np.ndarray + Array of input points of shape (M, 3). Each row is a 3D point in + Cartesian coordinates. + + Returns + ------- + np.ndarray + Array of transformed points of shape (N, M, 3), where ``N`` is the + number of transforms represented by this ``RigidTransform``. The + slice ``result[n, :, :]`` contains all ``M`` input points + transformed by the ``n``-th rigid transform rotation. + + Note + ---- + This only applies rotation, it ignores translation. It is useful for + transforming pointing vectors. + """ v = np.atleast_2d(v) assert v.ndim == 2 and v.shape[1] == 3 diff --git a/tests/test_antenna_array.py b/tests/test_antenna_array.py index 8a7c4cc0e..686e05575 100644 --- a/tests/test_antenna_array.py +++ b/tests/test_antenna_array.py @@ -355,4 +355,3 @@ def test_calculate_gain(self): if __name__ == '__main__': unittest.main() - diff --git a/tests/test_geometry.py b/tests/test_geometry.py index a25fa33c5..9144eca5e 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -425,9 +425,6 @@ def _test_expected_geom( np.stack((expect_local["x"], expect_local["y"], expect_local["z"])), atol=0.001 ) - # npt.assert_allclose(geom.x_local, expect_local["x"], atol=0.001) - # npt.assert_allclose(geom.y_local, expect_local["y"], atol=0.001) - # npt.assert_allclose(geom.z_local, expect_local["z"], atol=0.001) npt.assert_allclose(geom.pointn_elev_local, expect_local["elev"], atol=0.001) if only_local_azim is None: npt.assert_allclose(geom.pointn_azim_local, expect_local["azim"], atol=0.001) @@ -444,9 +441,6 @@ def _test_expected_geom( np.stack((expect_global["x"], expect_global["y"], expect_global["z"])), atol=0.001 ) - # npt.assert_allclose(geom.x_global, expect_global["x"], atol=0.001) - # npt.assert_allclose(geom.y_global, expect_global["y"], atol=0.001) - # npt.assert_allclose(geom.z_global, expect_global["z"], atol=0.001) npt.assert_allclose(geom.pointn_elev_global, expect_global["elev"], atol=0.001) if only_global_azim is None: npt.assert_allclose( From 42b3ea5680f10603dde7e1225bc9d680486e717b Mon Sep 17 00:00:00 2001 From: Fabricio Campos Date: Wed, 11 Feb 2026 18:57:07 +0000 Subject: [PATCH 76/77] update(antenna): changed ParametersAntennaWithDiameter as the parameter class --- sharc/antenna/antenna_f1245_fs.py | 11 ++------ sharc/parameters/parameters_antenna.py | 39 ++------------------------ 2 files changed, 5 insertions(+), 45 deletions(-) diff --git a/sharc/antenna/antenna_f1245_fs.py b/sharc/antenna/antenna_f1245_fs.py index de554a19d..b0999bbf8 100644 --- a/sharc/antenna/antenna_f1245_fs.py +++ b/sharc/antenna/antenna_f1245_fs.py @@ -1,4 +1,3 @@ - # -*- coding: utf-8 -*- """ Created on Wed Apr 4 17:08:00 2018 @@ -6,10 +5,7 @@ """ import matplotlib.pyplot as plt from sharc.antenna.antenna import Antenna -from sharc.parameters.imt.parameters_imt import ParametersImt -from sharc.parameters.imt.parameters_imt import ParametersImt -from sharc.parameters.imt.parameters_antenna_imt import ParametersAntennaImt -from sharc.parameters.parameters_antenna import ParametersAntenna +from sharc.parameters.parameters_antenna_with_diameter import ParametersAntennaWithDiameter import numpy as np import math @@ -18,7 +14,7 @@ class Antenna_f1245_fs(Antenna): """Class that implements the ITU-R F.1245 antenna pattern for fixed satellite service earth stations.""" - def __init__(self, param: ParametersImt): + def __init__(self, param: ParametersAntennaWithDiameter): super().__init__() self.peak_gain = param.gain lmbda = 3e8 / (param.frequency * 1e6) @@ -154,9 +150,8 @@ def calculate_off_axis_angle(self, Az, b): if __name__ == '__main__': off_axis_angle_vec = np.linspace(0.1, 180, num=1001) # initialize antenna parameters - param = ParametersAntenna() + param = ParametersAntennaWithDiameter() param.frequency = 2155 - param_gt = ParametersAntennaImt() param.gain = 33.1 param.diameter = 2 antenna_gt = Antenna_f1245_fs(param) diff --git a/sharc/parameters/parameters_antenna.py b/sharc/parameters/parameters_antenna.py index 98ebbc258..57adccf11 100644 --- a/sharc/parameters/parameters_antenna.py +++ b/sharc/parameters/parameters_antenna.py @@ -96,43 +96,8 @@ class ParametersAntenna(ParametersBase): default_factory=ParametersAntennaS672, ) - @dataclass - class ParametersAntennaRF1245(ParametersBase): - """ - Parameters for ITU-R F.1245 antenna model. It's commonly used - for fixed service antennas. - - Paremeters - ---------- - gain : float, default=-25 - Antenna gain in dB. - diameter : float, optional - Antenna diameter in meters. - frequency : float, optional - Operating frequency. - """ - gain: float = -25 - diameter: float = None - frequency: float = None - - def validate(self, ctx): - """ - Validate the antenna parameters for correctness. - - Parameters - ---------- - ctx : str - Context string for error messages. - Raises - ------ - ValueError - If any parameter is invalid. - """ - if None in [self.gain, self.diameter, self.frequency]: - raise ValueError(f"{ctx}.antenna_3_dB should be set to a number") - - itu_r_f_1245_fs: ParametersAntennaRF1245 = field( - default_factory=ParametersAntennaRF1245, + itu_r_f_1245_fs: ParametersAntennaWithDiameter = field( + default_factory=ParametersAntennaWithDiameter, ) def set_external_parameters(self, **kwargs): From a4a17e21adea41280866208d2addbb8ce7bd79b9 Mon Sep 17 00:00:00 2001 From: Fabricio Campos Date: Thu, 12 Feb 2026 14:31:56 +0000 Subject: [PATCH 77/77] Fixed antenna_f1245_fs.py header --- sharc/antenna/antenna_f1245_fs.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/sharc/antenna/antenna_f1245_fs.py b/sharc/antenna/antenna_f1245_fs.py index b0999bbf8..2643abb81 100644 --- a/sharc/antenna/antenna_f1245_fs.py +++ b/sharc/antenna/antenna_f1245_fs.py @@ -1,8 +1,3 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed Apr 4 17:08:00 2018 -@author: Calil -""" import matplotlib.pyplot as plt from sharc.antenna.antenna import Antenna from sharc.parameters.parameters_antenna_with_diameter import ParametersAntennaWithDiameter