diff --git a/sharc/antenna/antenna_element_cosine.py b/sharc/antenna/antenna_element_cosine.py new file mode 100644 index 00000000..7c5422f0 --- /dev/null +++ b/sharc/antenna/antenna_element_cosine.py @@ -0,0 +1,58 @@ + +# -*- coding: utf-8 -*- +"""Antenna model for Cosine Antenna channel systems.""" +from sharc.antenna.antenna import Antenna + +import numpy as np + + +class AntennaElementCosine(Antenna): + """ + Implements antenna part of EIRP mask for MSS-DC systems + as defined in the WP4C Working Document 4C/356-E. + """ + + def __init__(self,): + """ + Initialize the AntennaElementCosine class. + + """ + super().__init__() + + def calculate_gain(self, *args, **kwargs) -> np.array: + """ + Calculate the antenna gain for the given off-axis angles. + + Parameters + ---------- + *args : tuple + Positional arguments (not used). + **kwargs : dict + Keyword arguments, expects 'off_axis_angle_vec' as input. + + Returns + ------- + np.array + Calculated antenna gain values. + """ + theta_rad = np.deg2rad(np.absolute(kwargs["off_axis_angle_vec"])) + theta_rad = np.minimum(theta_rad, np.pi / 2 - 1e-5) + return 10 * np.log10(np.cos(theta_rad)) + + +if __name__ == '__main__': + import matplotlib.pyplot as plt + + theta = np.linspace(0.01, 90.01, num=1000) + antenna = AntennaElementCosine() + gain = antenna.calculate_gain(off_axis_angle_vec=theta) + fig = plt.figure(facecolor='w', edgecolor='k') + ax = fig.add_subplot() + ax.plot(theta, gain) + ax.grid(True) + ax.set_title("Antenna Element Cosine Pattern") + ax.set_xlabel(r"Off-axis angle $\theta$ [deg]") + ax.set_ylabel("Antenna Gain [dBi]") + ax.set_xlim((theta[0], theta[-1])) + ax.set_ylim((-40, 10)) + plt.show() diff --git a/sharc/antenna/antenna_factory.py b/sharc/antenna/antenna_factory.py index 051e1f33..56ba5b96 100644 --- a/sharc/antenna/antenna_factory.py +++ b/sharc/antenna/antenna_factory.py @@ -3,7 +3,7 @@ from sharc.parameters.parameters_antenna import ParametersAntenna from sharc.antenna.antenna import Antenna -from sharc.antenna.antenna_mss_adjacent import AntennaMSSAdjacent +from sharc.antenna.antenna_element_cosine import AntennaElementCosine from sharc.antenna.antenna_omni import AntennaOmni from sharc.antenna.antenna_mss_hibleo_x_ue import AntennaMssHibleoXUe from sharc.antenna.antenna_f699 import AntennaF699 @@ -27,7 +27,16 @@ def create_antenna( azimuth: float, elevation: float, ): - """Create and return an antenna instance based on the provided parameters, azimuth, and elevation.""" + """Create and return an antenna instance based on the provided parameters, azimuth, and elevation. + + Args: + antenna_params (ParametersAntenna): The parameters defining the antenna configuration. + azimuth (float): The azimuth angle for the antenna. + elevation (float): The elevation angle for the antenna. + oob_pattern (str, optional): Out-of-band pattern to use instead of the main pattern. + Returns: + Antenna: An instance of the appropriate Antenna subclass. + """ match antenna_params.pattern: case "OMNI": return AntennaOmni(antenna_params.gain) @@ -51,9 +60,8 @@ def create_antenna( return AntennaS1855(antenna_params.itu_r_s_1855) case "ITU-R Reg. RR. Appendice 7 Annex 3": return AntennaReg_RR_A7_3(antenna_params.itu_reg_rr_a7_3) - case "MSS Adjacent": - return AntennaMSSAdjacent( - antenna_params.mss_adjacent.frequency) + case "Cosine Antenna": + return AntennaElementCosine() case "ARRAY": return AntennaBeamformingImt( antenna_params.array.get_antenna_parameters(), @@ -73,11 +81,19 @@ def create_n_antennas( n_stations: int, ): """ - Creates many antennas based on passed parameters. + Create many antennas based on passed parameters. + If antenna does not require each object to have different state, only a single antenna object will be created, and every position in the array will point to it. This is much more performant. + Args: + antenna_params (ParametersAntenna): The parameters defining the antenna configuration. + azimuth (np.ndarray | float): The azimuth angles for the antennas. + elevation (np.ndarray | float): The elevation angles for the antennas. + n_stations (int): Number of antennas to create. + Returns: + np.ndarray: An array of Antenna instances. """ antennas = np.empty((n_stations,), dtype=Antenna) assert n_stations == len(azimuth) diff --git a/sharc/antenna/antenna_mss_adjacent.py b/sharc/antenna/antenna_mss_adjacent.py deleted file mode 100644 index 840a4f92..00000000 --- a/sharc/antenna/antenna_mss_adjacent.py +++ /dev/null @@ -1,122 +0,0 @@ - -# -*- coding: utf-8 -*- -"""Antenna model for MSS adjacent channel systems.""" -from sharc.antenna.antenna import Antenna - -import numpy as np - - -class AntennaMSSAdjacent(Antenna): - """ - Implements part of EIRP mask for MSS-DC systems given in document WPGC - as defined in the WP4C Working Document 4C/356-E - You can choose the adjacent channel by choosing the tx power - You need to also make sure ACLR_db = 0, otherwise SHARC's implementation will - mess the EIRP up. - """ - - def __init__(self, frequency_MHz: float): - """ - Initialize the AntennaMSSAdjacent class. - - Parameters - ---------- - frequency_MHz : float - Frequency in MHz for the antenna model. - """ - super().__init__() - self.frequency = frequency_MHz - - def calculate_gain(self, *args, **kwargs) -> np.array: - """ - Calculate the antenna gain for the given off-axis angles. - - Parameters - ---------- - *args : tuple - Positional arguments (not used). - **kwargs : dict - Keyword arguments, expects 'off_axis_angle_vec' as input. - - Returns - ------- - np.array - Calculated antenna gain values. - """ - theta_rad = np.deg2rad(np.absolute(kwargs["off_axis_angle_vec"])) - theta_rad = np.minimum(theta_rad, np.pi / 2 - 1e-4) - return 20 * np.log10(self.frequency / 2e3) + 10 * \ - np.log10(np.cos(theta_rad)) - - -if __name__ == '__main__': - import matplotlib.pyplot as plt - - frequency = 2170 - theta = np.linspace(0.01, 90, num=100000) - - antenna = AntennaMSSAdjacent(frequency) - gain = antenna.calculate_gain(off_axis_angle_vec=theta) - - def create_plot_adj_channel(frequency, theta, chn, ax=None): - """ - Create a plot for the adjacent channel mask for MSS antennas. - - Parameters - ---------- - frequency : float - Frequency in MHz. - theta : np.array - Array of off-axis angles in degrees. - chn : int - Channel number (-1 or 1). - ax : matplotlib.axes.Axes, optional - Matplotlib axis to plot on. If None, a new figure and axis are created. - - Returns - ------- - matplotlib.axes.Axes - The axis with the plot. - """ - tranls = { - -1: 0, - 1: -55.6, - 2: -73.6, - 3: -83.6, - } - if chn == -1: - ylabel = "Gain [dB]" - else: - ylabel = f"EIRP_{chn}" - - if ax is None: - fig = plt.figure(facecolor='w', edgecolor='k') - ax1 = fig.add_subplot() - else: - ax1 = ax - - ax1.plot(theta, gain + tranls[chn]) - ax1.grid(True) - ax1.set_xlabel(r"Off-axis angle $\theta$ [deg]") - ax1.set_ylabel(ylabel) - - label = f"$f = {frequency / 1e3: .3f}$ GHz" - if chn != -1: - label += f", channel {chn}" - # ax1.semilogx( - ax1.plot( - theta, gain, - label=label, - ) - ax1.legend(loc="lower left") - ax1.set_xlim((theta[0], theta[-1])) - ax1.set_ylim((-80, 10)) - - return ax1 - - create_plot_adj_channel(frequency, theta, -1) - plt.show() - ax = create_plot_adj_channel(frequency, theta, 1) - create_plot_adj_channel(frequency, theta, 2, ax) - create_plot_adj_channel(frequency, theta, 3, ax) - plt.show() diff --git a/sharc/antenna/antenna_mss_hibleo_x_ue.py b/sharc/antenna/antenna_mss_hibleo_x_ue.py index 10efaae3..329ae5cf 100644 --- a/sharc/antenna/antenna_mss_hibleo_x_ue.py +++ b/sharc/antenna/antenna_mss_hibleo_x_ue.py @@ -35,7 +35,7 @@ class AntennaMssHibleoXUe(Antenna): def __init__(self, frequency_MHz: float): """ - Initialize the AntennaMSSAdjacent class. + Initialize the AntennaElementCosine class. Parameters ---------- diff --git a/sharc/mask/spectral_mask_stepped.py b/sharc/mask/spectral_mask_stepped.py new file mode 100644 index 00000000..0c8092d4 --- /dev/null +++ b/sharc/mask/spectral_mask_stepped.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- + +from sharc.mask.spectral_mask import SpectralMask + +import numpy as np +import matplotlib.pyplot as plt + + +class SpectralMaskStepped(SpectralMask): + """ + Implements a stepped spectral mask defined by user-provided steps. + """ + def __init__( + self, + freq_mhz: float, + band_mhz: float, + mask_steps_dBm_mhz: list): + """ + Class constructor. + Parameters: + freq_mhz (float): center frequency of station in MHz + band_mhs (float): transmitting bandwidth of station in MHz + mask_steps_dBm_mhz (list): list of spectral mask step values in dBm/MHz. Each step corresponds to a + multiple of the bandwidth away from the band edge. + The last step is considered to the spurious domain. + """ + + self.mask_steps_dBm_mhz = mask_steps_dBm_mhz + self.freq_mhz = freq_mhz + self.band_mhz = band_mhz + + self.delta_f_lim = np.array( + [self.band_mhz * i for i in range(len(self.mask_steps_dBm_mhz))] + ) + + self.freq_lim = np.concatenate(( + (freq_mhz - band_mhz / 2) - self.delta_f_lim[::-1], + (freq_mhz + band_mhz / 2) + self.delta_f_lim, + )) + + def set_mask(self, p_tx=0): + """ + Set the spectral mask values based on the defined steps. + + Parameters: + p_tx (float): Transmit power in dBm/MHz. + """ + self.p_tx = p_tx - 10 * np.log10(self.band_mhz) + self.mask_dbm = np.concatenate([self.mask_steps_dBm_mhz[::-1], [self.p_tx], self.mask_steps_dBm_mhz]) + + +if __name__ == '__main__': + + freq = 2100 # MHz + band = 5 # MHz + p_tx = 0.0 + 10 * np.log10(band) # dBm + spourious_emissions = -30.0 # dBm/MHz + + mask_steps = [-10, -15, -20] # dBm/MHz + mask_steps = np.concatenate([mask_steps, [-30]]) # dBm/MHz + + # Create mask + msk = SpectralMaskStepped(freq, band, mask_steps) + msk.set_mask(p_tx) + + # Frequencies + freqs = np.linspace(-60, 60, num=1000) + freq + + # Mask values + mask_val = np.ones_like(freqs) * msk.mask_dbm[0] + for k in range(len(msk.freq_lim) - 1, -1, -1): + mask_val[np.where(freqs < msk.freq_lim[k])] = msk.mask_dbm[k] + + # Plot + plt.plot(freqs, mask_val) + plt.title("Stepped Spectral Mask") + plt.xlim([freqs[0], freqs[-1]]) + plt.xlabel(r"$\Delta$f [MHz]") + plt.ylabel("Spectral Mask [dBc]") + plt.grid() + plt.show() + + print(msk.power_calc(center_f=freq + 1 * band, band=band)) + print(msk.power_calc(center_f=freq + 2 * band, band=band)) + print(msk.power_calc(center_f=freq + 3 * band, band=band)) diff --git a/sharc/parameters/imt/parameters_imt.py b/sharc/parameters/imt/parameters_imt.py index 1d007b54..5d502951 100644 --- a/sharc/parameters/imt/parameters_imt.py +++ b/sharc/parameters/imt/parameters_imt.py @@ -61,6 +61,14 @@ class ParametersBS(ParametersBase): default_factory=lambda: ParametersAntenna( pattern="ARRAY", array=ParametersAntennaImt( downtilt=0.0))) + # Flag to indicate if out-of-band antenna pattern should be used + use_oob_antenna: bool = False + # Out-of-band antenna model + oob_antenna: ParametersAntenna = field( + default_factory=lambda: ParametersAntenna( + pattern="ARRAY", array=ParametersAntennaImt( + adjacent_antenna_model="SINGLE_ELEMENT", + downtilt=0.0))) bs: ParametersBS = field(default_factory=ParametersBS) topology: ParametersImtTopology = field( @@ -108,6 +116,15 @@ class ParametersUE(ParametersBase): default_factory=lambda: ParametersAntenna( pattern="ARRAY")) + # Flag to indicate if out-of-band antenna pattern should be used + use_oob_antenna: bool = False + # Out-of-band antenna model + oob_antenna: ParametersAntenna = field( + default_factory=lambda: ParametersAntenna( + pattern="ARRAY", array=ParametersAntennaImt( + adjacent_antenna_model="SINGLE_ELEMENT", + downtilt=0.0))) + def validate(self, ctx: str): """Validate the UE antenna beamsteering range parameters.""" if self.antenna.array.horizontal_beamsteering_range != (-180., 179.9999)\ diff --git a/sharc/parameters/parameters_antenna.py b/sharc/parameters/parameters_antenna.py index a8fc8a0e..f478effb 100644 --- a/sharc/parameters/parameters_antenna.py +++ b/sharc/parameters/parameters_antenna.py @@ -29,7 +29,7 @@ class ParametersAntenna(ParametersBase): "ITU-R-S.1528-Taylor", "ITU-R-S.1528-Section1.2", "ITU-R-S.1528-LEO", - "MSS Adjacent"] + "Cosine Antenna"] # chosen antenna radiation pattern pattern: typing.Literal["OMNI", @@ -44,7 +44,7 @@ class ParametersAntenna(ParametersBase): "ITU-R-S.1528-Taylor", "ITU-R-S.1528-Section1.2", "ITU-R-S.1528-LEO", - "MSS Adjacent"] = None + "Cosine Antenna"] = None # antenna gain [dBi] gain: float = None @@ -147,7 +147,7 @@ def validate(self, ctx): """ if None in [self.pattern]: raise ValueError( - f"{ctx}.pattern should be set. Is None instead", + f"{ctx}.pattern should be set. It is None instead", ) if self.pattern != "ARRAY" and self.gain is None: @@ -196,7 +196,7 @@ def validate(self, ctx): 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": + case "Cosine Antenna": self.mss_adjacent.validate(f"{ctx}.mss_adjacent") case _: raise NotImplementedError( diff --git a/sharc/parameters/parameters_mss_d2d.py b/sharc/parameters/parameters_mss_d2d.py index c566269c..e013142a 100644 --- a/sharc/parameters/parameters_mss_d2d.py +++ b/sharc/parameters/parameters_mss_d2d.py @@ -1,4 +1,5 @@ import numpy as np +import typing from dataclasses import dataclass, field, asdict from sharc.parameters.parameters_base import ParametersBase from sharc.parameters.parameters_orbit import ParametersOrbit @@ -6,6 +7,7 @@ from sharc.parameters.parameters_p619 import ParametersP619 from sharc.parameters.parameters_antenna import ParametersAntenna from sharc.parameters.antenna.parameters_antenna_s1528 import ParametersAntennaS1528 +from sharc.parameters.antenna.parameters_antenna_with_freq import ParametersAntennaWithFreq @dataclass @@ -49,7 +51,14 @@ class ParametersMssD2d(ParametersBase): adjacent_ch_emissions: str = "OFF" # Transmitter spectral mask - spectral_mask: str = "MSS" + spectral_mask: typing.Literal[ + "IMT-2020", + "3GPP E-UTRA", + "MSS", + "STEPPED"] = "MSS" + + # Spectral mask steps in dB for STEPPED mask + spectral_mask_steps: tuple[float | int, float | int] = None # Out-of-band spurious emissions in dB/MHz spurious_emissions: float = -13.0 @@ -84,6 +93,16 @@ class ParametersMssD2d(ParametersBase): gain=30.0, itu_r_s_1528=ParametersAntennaS1528())) + # Flag to indicate if out-of-band antenna pattern should be used + use_oob_antenna: bool = False + + # Parameters for the out-of-band antenna pattern + oob_antenna: ParametersAntenna = field( + default_factory=lambda: ParametersAntenna( + pattern="Cosine Antenna", + gain=0.0, + mss_adjacent=ParametersAntennaWithFreq(frequency=None))) + sat_is_active_if: ParametersSelectActiveSatellite = field( default_factory=ParametersSelectActiveSatellite) @@ -156,7 +175,7 @@ def validate(self, ctx): self.adjacent_ch_emissions}""") if self.spectral_mask.upper() not in [ - "IMT-2020", "3GPP E-UTRA", "MSS"]: + "IMT-2020", "3GPP E-UTRA", "MSS", "STEPPED"]: raise ValueError( f"""ParametersMssD2d: Inavlid Spectral Mask Name { self.spectral_mask}""") @@ -171,6 +190,18 @@ def validate(self, ctx): raise ValueError( f"{ctx}.beams_load_factor must be in interval [0.0, 1.0]") + if self.spectral_mask.upper() == "STEPPED": + if self.spectral_mask_steps is None: + raise ValueError( + f"ParametersMssD2d: spectral_mask_steps must be defined for STEPPED mask.") + if len(self.spectral_mask_steps) < 1: + raise ValueError( + f"ParametersMssD2d: spectral_mask_steps must have at least one step defined.") + for step in self.spectral_mask_steps: + if not isinstance(step, (int, float)): + raise ValueError( + f"ParametersMssD2d: spectral_mask_steps must contain only numeric values.") + super().validate(ctx) def propagate_parameters(self): @@ -181,6 +212,18 @@ def propagate_parameters(self): frequency=self.frequency, bandwidth=self.bandwidth, ) + if self.use_oob_antenna: + if self.oob_antenna.pattern not in ["Cosine Antenna"]: # only supported this pattern for now + raise ValueError( + f"ParametersMssD2d: Invalid out-of-band antenna pattern { + self.oob_antenna.pattern}. Only 'Cosine Antenna' is supported.") + + self.oob_antenna.set_external_parameters( + frequency=self.frequency, + ) + else: + self.oob_antenna = self.antenna # use the same antenna if not specified + 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 50a3d3ac..77d64e92 100644 --- a/sharc/parameters/parameters_mss_ss.py +++ b/sharc/parameters/parameters_mss_ss.py @@ -70,6 +70,12 @@ class ParametersMssSs(ParametersBase): antenna: ParametersAntenna = field( default_factory=ParametersAntenna) + # Use out-of-band antenna for emissions outside the assigned bandwidth + use_oob_antenna: bool = False + + # Out-of-band antenna parameters - only used if use_oob_antenna is True + oob_antenna: ParametersAntenna = field(default_factory=ParametersAntenna) + # paramters for channel model param_p619: ParametersP619 = field(default_factory=ParametersP619) diff --git a/sharc/parameters/parameters_single_earth_station.py b/sharc/parameters/parameters_single_earth_station.py index afc077e8..b372474c 100644 --- a/sharc/parameters/parameters_single_earth_station.py +++ b/sharc/parameters/parameters_single_earth_station.py @@ -69,6 +69,12 @@ class ParametersSingleEarthStation(ParametersBase): # Antenna pattern of the sensor antenna: ParametersAntenna = field(default_factory=ParametersAntenna) + # Use out-of-band antenna for emissions outside the assigned bandwidth + use_oob_antenna: bool = False + + # Out-of-band antenna parameters - only used if use_oob_antenna is True + oob_antenna: ParametersAntenna = field(default_factory=ParametersAntenna) + # Channel model, possible values are "FSPL" (free-space path loss), "P619" channel_model: typing.Literal[ "FSPL", "P619", @@ -283,14 +289,7 @@ def load_parameters_from_file(self, config_file: str): """ super().load_parameters_from_file(config_file) - # this is needed because nested parameters - # don't know/cannot access parents attributes - self.antenna.set_external_parameters( - frequency=self.frequency, - ) - - # this parameter is required in system get description - self.antenna_pattern = self.antenna.pattern + self.propagate_parameters() # this should be done by validating this parameters only if it is the selected system on the general section # TODO: make this better by changing the Parameters class itself @@ -303,6 +302,28 @@ def load_parameters_from_file(self, config_file: str): if should_validate: self.validate(self.section_name) + def propagate_parameters(self): + """ + Propagate relevant parameters to nested objects. + """ + + # this is needed because nested parameters + # don't know/cannot access parents attributes + self.antenna.set_external_parameters( + frequency=self.frequency, + ) + + if self.use_oob_antenna: + self.oob_antenna.set_external_parameters( + frequency=self.frequency + ) + else: + # The oob antenna parameters is replicated here to prevent validation failure. + self.oob_antenna = self.antenna + + # this parameter is required in system get description + self.antenna_pattern = self.antenna.pattern + def validate(self, ctx="single_earth_station"): """ Validate the single earth station parameters for correctness. diff --git a/sharc/parameters/parameters_single_space_station.py b/sharc/parameters/parameters_single_space_station.py index b1bacfbe..2b598964 100644 --- a/sharc/parameters/parameters_single_space_station.py +++ b/sharc/parameters/parameters_single_space_station.py @@ -33,6 +33,12 @@ class ParametersSingleSpaceStation(ParametersBase): # Antenna pattern of the sensor antenna: ParametersAntenna = field(default_factory=ParametersAntenna) + # Use out-of-band antenna for emissions outside the assigned bandwidth + use_oob_antenna: bool = False + + # Out-of-band antenna parameters - only used if use_oob_antenna is True + oob_antenna: ParametersAntenna = field(default_factory=ParametersAntenna) + # Receiver polarization loss # e.g. could come from polarization mismatch or depolarization # check if IMT parameters don't come in values for single polarization @@ -192,6 +198,14 @@ def propagate_parameters(self): frequency=self.frequency, ) + if self.use_oob_antenna: + self.oob_antenna.set_external_parameters( + frequency=self.frequency + ) + else: + # The oob antenna parameters is replicated here to prevent validation failure. + self.oob_antenna = self.antenna + # this parameter is required in system get description self.antenna_pattern = self.antenna.pattern diff --git a/sharc/post_processor.py b/sharc/post_processor.py index 9a8f6743..24c83357 100644 --- a/sharc/post_processor.py +++ b/sharc/post_processor.py @@ -179,18 +179,22 @@ class PostProcessor: "x_label": "Antenna gain [dBi]", "title": "[SYS] system antenna gain towards IMT stations", }, + "system_imt_antenna_gain_adjacent": { + "x_label": "Antenna gain [dBi]", + "title": "[SYS] system adjacent antenna gain towards IMT stations", + }, "imt_system_antenna_gain": { "x_label": "Antenna gain [dBi]", "title": "[IMT] IMT station antenna gain towards system", }, + "imt_system_antenna_gain_adjacnet": { + "x_label": "Antenna gain [dBi]", + "title": "[IMT] IMT station adjacent antenna gain towards system", + }, "imt_system_path_loss": { "x_label": "Path Loss [dB]", "title": "[SYS] IMT to system path loss", }, - "sys_to_imt_coupling_loss": { - "x_label": "Coupling Loss [dB]", - "title": "[SYS] IMT to system coupling loss", - }, "system_dl_interf_power": { "x_label": "Interference Power [dB]", "title": "[SYS] system interference power from IMT DL", diff --git a/sharc/results.py b/sharc/results.py index 92bd005d..f14477e1 100644 --- a/sharc/results.py +++ b/sharc/results.py @@ -56,6 +56,8 @@ def __init__(self): self.imt_system_antenna_gain = SampleList() # Antenna gain [dBi] self.imt_system_antenna_gain_adjacent = SampleList() + # Antenna gain [dBi] - antenna gain of adjacent channel emissions + self.system_imt_antenna_gain_adjacent = SampleList() # Path Loss [dB] self.imt_system_path_loss = SampleList() @@ -63,8 +65,6 @@ def __init__(self): self.imt_system_build_entry_loss = SampleList() # System diffraction loss [dB] self.imt_system_diffraction_loss = SampleList() - # System to IMT coupling loss - self.sys_to_imt_coupling_loss = SampleList() self.imt_dl_tx_power_density = SampleList() # Transmit power [dBm] diff --git a/sharc/simulation.py b/sharc/simulation.py index 45908a07..e5538c95 100644 --- a/sharc/simulation.py +++ b/sharc/simulation.py @@ -113,6 +113,7 @@ def __init__(self, parameters: Parameters, parameter_file: str): self.coupling_loss_imt = np.empty(0) self.coupling_loss_imt_system = np.empty(0) self.coupling_loss_imt_system_adjacent = np.empty(0) + self.coupling_loss_oob_tx_inband_rx = np.empty(0) # Used to store coupling loss for oob emissions self.bs_to_ue_d_2D = np.empty(0) self.bs_to_ue_d_3D = np.empty(0) @@ -271,7 +272,8 @@ def calculate_coupling_loss_system_imt( self, system_station: StationManager, imt_station: StationManager, - is_co_channel=True, + system_inband=True, + imt_inband=True, ) -> np.array: """ Calculates the coupling loss (path loss + antenna gains + other losses) between @@ -286,9 +288,11 @@ def calculate_coupling_loss_system_imt( A StationManager object with system stations imt_station : StationManager A StationManager object with IMT stations - is_co_channel : bool, optional - Whether the interference analysis is co-channel or not, by default True - + system_inband : bool, optional + Whether the interference analysis on system is in-band or adjacent. Default, True. + imt_inband: bool, optional + Whether the interference analysis on IMT is in-band or adjacent. Default, True. +b Returns ------- np.array @@ -305,12 +309,12 @@ def calculate_coupling_loss_system_imt( # system's station if imt_station.station_type is StationType.IMT_UE: # define antenna gains - gain_sys_to_imt = self.calculate_gains(system_station, imt_station) + gain_sys_to_imt = self.calculate_gains(system_station, imt_station, system_inband) gain_imt_to_sys = np.transpose( self.calculate_gains( imt_station, system_station, - is_co_channel)) + imt_inband)) additional_loss = self.parameters.imt.ue.ohmic_loss \ + self.parameters.imt.ue.body_loss \ + self.polarization_loss @@ -318,12 +322,12 @@ def calculate_coupling_loss_system_imt( # define antenna gains # repeat for each BS beam gain_sys_to_imt = np.repeat( - self.calculate_gains(system_station, imt_station), + self.calculate_gains(system_station, imt_station, system_inband), self.parameters.imt.ue.k, 1, ) gain_imt_to_sys = np.transpose( self.calculate_gains( - imt_station, system_station, is_co_channel, + imt_station, system_station, imt_inband, ), ) additional_loss = self.parameters.imt.bs.ohmic_loss \ @@ -373,16 +377,19 @@ def calculate_coupling_loss_system_imt( path_loss, self.parameters.imt.ue.k, 1, ) - self.system_imt_antenna_gain = gain_sys_to_imt + if system_inband: + self.system_imt_antenna_gain = gain_sys_to_imt + else: + self.system_imt_antenna_gain_adjacent = gain_sys_to_imt - if is_co_channel: + if imt_inband: self.imt_system_antenna_gain = gain_imt_to_sys else: self.imt_system_antenna_gain_adjacent = gain_imt_to_sys # calculate coupling loss coupling_loss = \ - self.imt_system_path_loss - self.system_imt_antenna_gain - gain_imt_to_sys + additional_loss + self.imt_system_path_loss - gain_sys_to_imt - gain_imt_to_sys + additional_loss # Simulator expects imt_stations x system_stations shape return np.transpose(coupling_loss) @@ -569,6 +576,14 @@ def calculate_gains( station_1_active = np.where(station_1.active)[0] station_2_active = np.where(station_2.active)[0] + # Select the antenna for in-band or out-of-band emission. + # TODO: refactor to avoid code duplication + # TODO: station_1 and station_2 naming is confusing here. We are assuming that the emitting station is station_1 + if c_channel: + tx_antenna = station_1.antenna + else: + tx_antenna = station_1.oob_antenna + # Initialize variables (phi, theta, beams_idx) if (station_1.station_type is StationType.IMT_BS): if (station_2.station_type is StationType.IMT_UE): @@ -599,61 +614,53 @@ def calculate_gains( for b in range( k * self.parameters.imt.ue.k, (k + 1) * self.parameters.imt.ue.k): - gains[b, - station_2_active] = station_1.antenna[k].calculate_gain(phi_vec=phi[b, - station_2_active], - theta_vec=theta[b, - station_2_active, - ], - beams_l=np.repeat(beams_idx[b], - len(station_2_active)), - co_channel=c_channel, - off_axis_angle_vec=off_axis_angle[k, - station_2_active]) + gains[b, station_2_active] = \ + tx_antenna[k].calculate_gain( + phi_vec=phi[b, station_2_active], + theta_vec=theta[b, station_2_active], + beams_l=np.repeat(beams_idx[b], len(station_2_active)), + co_channel=c_channel, + off_axis_angle_vec=off_axis_angle[k, 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) 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], - phi_vec=phi[k, station_2_active], - theta_vec=theta[ - k, - station_2_active, - ], - beams_l=beams_idx, - co_channel=c_channel, - ) + gains[k, station_2_active] = \ + tx_antenna[k].calculate_gain( + off_axis_angle_vec=off_axis_angle[k, station_2_active], + phi_vec=phi[k, station_2_active], + theta_vec=theta[k, station_2_active,], + beams_l=beams_idx, + co_channel=c_channel,) + # RNS to non-IMT + # TODO: refactor to avoid code duplication with non-IMT to RNS elif station_1.station_type is StationType.RNS: - gains[0, station_2_active] = station_1.antenna[0].calculate_gain( + gains[0, station_2_active] = tx_antenna[0].calculate_gain( phi_vec=phi[0, station_2_active], theta_vec=theta[0, station_2_active], ) + # non-IMT to IMT 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) for k in station_1_active: gains[k, station_2_active] = \ - station_1.antenna[k].calculate_gain( + tx_antenna[k].calculate_gain( off_axis_angle_vec=off_axis_angle[k, station_2_active], theta_vec=theta[k, station_2_active], - phi_vec=phi[k, station_2_active], - ) + phi_vec=phi[k, station_2_active],) else: # for IMT <-> IMT off_axis_angle = station_1.get_off_axis_angle(station_2) 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], - phi_vec=phi[k, station_2_active], - theta_vec=theta[ - k, - station_2_active, - ], - beams_l=beams_idx, - ) + gains[k, station_2_active] = \ + tx_antenna[k].calculate_gain( + off_axis_angle_vec=off_axis_angle[k, station_2_active], + phi_vec=phi[k, station_2_active], + theta_vec=theta[k, station_2_active,], + beams_l=beams_idx,) return gains def calculate_imt_tput( diff --git a/sharc/simulation_downlink.py b/sharc/simulation_downlink.py index 04a24109..70a3e802 100644 --- a/sharc/simulation_downlink.py +++ b/sharc/simulation_downlink.py @@ -192,21 +192,34 @@ def calculate_sinr_ext(self): Calculates the downlink SINR and INR for each UE taking into account the interference that is generated by the other system into IMT system. """ - if self.co_channel or ( - self.adjacent_channel and self.param_system.adjacent_ch_emissions != "OFF" - ): + + if self.co_channel: + # coupling-loss between system's in-band transmission into IMT in-band reception. self.coupling_loss_imt_system = self.calculate_coupling_loss_system_imt( self.system, self.ue, - is_co_channel=True, + system_inband=True, + imt_inband=True, ) + if self.adjacent_channel: - self.coupling_loss_imt_system_adjacent = \ - self.calculate_coupling_loss_system_imt( + # coupling-loss from system's out-of-band emissions into IMT in-band reception + if self.param_system.adjacent_ch_emissions != "OFF": + # TODO: Store this coupling loss for results later. + self.coupling_loss_oob_tx_inband_rx = self.calculate_coupling_loss_system_imt( self.system, self.ue, - is_co_channel=False, + system_inband=False, + imt_inband=True, ) + # coupling-loss from system's in-band transmission into IMT out-of-band reception + # (e.g. due to reception filter imperfections). + self.coupling_loss_imt_system_adjacent = self.calculate_coupling_loss_system_imt( + self.system, + self.ue, + system_inband=True, + imt_inband=False, + ) # applying a bandwidth scaling factor since UE transmits on a portion # of the satellite's bandwidth @@ -344,7 +357,7 @@ def calculate_sinr_ext(self): 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] + tx_oob = tx_oob[:, np.newaxis] - self.coupling_loss_oob_tx_inband_rx[ue, :][:, active_sys] rx_oob = rx_oob[:, np.newaxis] - self.coupling_loss_imt_system_adjacent[ue, :][:, active_sys] @@ -355,9 +368,7 @@ def calculate_sinr_ext(self): 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)) + 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 * \ @@ -404,18 +415,33 @@ def calculate_external_interference(self): """ Calculates interference that IMT system generates on other system """ - if self.co_channel or ( - self.adjacent_channel and self.param_system.adjacent_ch_reception != "OFF" - ): + if self.co_channel: + # coupling-loss between IMT in-band transmission into system in-band reception. self.coupling_loss_imt_system = self.calculate_coupling_loss_system_imt( - self.system, self.bs, is_co_channel=True, ) + self.system, + self.bs, + system_inband=True, + imt_inband=True, + ) + if self.adjacent_channel: - self.coupling_loss_imt_system_adjacent = \ - self.calculate_coupling_loss_system_imt( + # coupling-loss from IMT's out-of-band emissions into System in-band reception + if self.parameters.imt.adjacent_ch_emissions != "OFF": + # TODO: Store this coupling loss for results later. + self.coupling_loss_oob_tx_inband_rx = self.calculate_coupling_loss_system_imt( self.system, self.bs, - is_co_channel=False, + system_inband=True, + imt_inband=False, ) + # coupling-loss from IMT's in-band transmission into Systems out-of-band reception + # (e.g. due to reception filter imperfections). + self.coupling_loss_imt_system_adjacent = self.calculate_coupling_loss_system_imt( + self.system, + self.bs, + system_inband=False, + imt_inband=True, + ) # applying a bandwidth scaling factor since UE transmits on a portion # of the interfered systems bandwidth @@ -533,20 +559,22 @@ def calculate_external_interference(self): ) if self.adjacent_channel: - # oob_power per beam - # 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)] - - # FIXME: for more than 1 sys - # NOTE: sharc impl already doesn't really work with n_sys > 1 - # so more would have to be fixed before this - assert np.all(adj_loss == adj_loss.flat[0]) - - tx_oob_s = tx_oob - adj_loss[0, :] + tx_oob_s = - np.inf + if self.parameters.imt.adjacent_ch_emissions != "OFF": + # oob_power per beam + # 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_oob_tx_inband_rx[np.ix_(active_beams, sys_active)] + + # FIXME: for more than 1 sys + # NOTE: sharc impl already doesn't really work with n_sys > 1 + # so more would have to be fixed before this + assert np.all(adj_loss == adj_loss.flat[0]) + + 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_adjacent[active_beams, sys_active] else: rx_oob_s = -np.inf @@ -665,6 +693,10 @@ def collect_results(self, write_to_file: bool, snapshot_number: int): self.results.imt_system_antenna_gain_adjacent.extend( self.imt_system_antenna_gain_adjacent[sys_active[:, np.newaxis], ue].flatten(), ) + if len(self.system_imt_antenna_gain_adjacent): + self.results.system_imt_antenna_gain_adjacent.extend( + self.system_imt_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(), ) @@ -675,12 +707,15 @@ def collect_results(self, write_to_file: bool, snapshot_number: int): 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.system_imt_antenna_gain): + self.results.system_imt_antenna_gain.extend( + self.system_imt_antenna_gain[sys_active[:, np.newaxis], ue].flatten(), + ) + if len(self.system_imt_antenna_gain_adjacent): + self.results.system_imt_antenna_gain_adjacent.extend( + self.system_imt_antenna_gain_adjacent[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(), diff --git a/sharc/simulation_uplink.py b/sharc/simulation_uplink.py index 7d547f11..74909266 100644 --- a/sharc/simulation_uplink.py +++ b/sharc/simulation_uplink.py @@ -174,22 +174,33 @@ def calculate_sinr_ext(self): interference that is generated by the other system into IMT system. """ - if self.co_channel or ( - self.adjacent_channel and self.param_system.adjacent_ch_emissions != "OFF" - ): + if self.co_channel: + # coupling-loss between system's in-band transmission into IMT in-band reception. self.coupling_loss_imt_system = self.calculate_coupling_loss_system_imt( self.system, self.bs, - is_co_channel=True, + system_inband=True, + imt_inband=True, ) if self.adjacent_channel: - self.coupling_loss_imt_system_adjacent = \ - self.calculate_coupling_loss_system_imt( + # coupling-loss from system's out-of-band emissions into IMT in-band reception + if self.param_system.adjacent_ch_emissions != "OFF": + # TODO: Store this for results later + self.coupling_loss_oob_tx_inband_rx = self.calculate_coupling_loss_system_imt( self.system, self.bs, - is_co_channel=False, + system_inband=False, + imt_inband=True, ) + # coupling-loss from system's in-band transmission into IMT out-of-band reception + # (e.g. due to reception filter imperfections). + self.coupling_loss_imt_system_adjacent = self.calculate_coupling_loss_system_imt( + self.system, + self.bs, + system_inband=True, + imt_inband=False, + ) bs_active = np.where(self.bs.active)[0] sys_active = np.where(self.system.active)[0] @@ -306,7 +317,7 @@ 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_oob_tx_inband_rx[active_beams, :][:, sys_active] # oob for IMT rx_oob = rx_oob[:, np.newaxis] - self.coupling_loss_imt_system_adjacent[active_beams, :][:, sys_active] @@ -337,19 +348,33 @@ def calculate_external_interference(self): Calculates interference that IMT system generates on other system """ - if self.co_channel or ( - # then rx receives emission inside the tx band, so it is co-channel with IMT - self.adjacent_channel and self.param_system.adjacent_ch_reception != "OFF" - ): + if self.co_channel: + # coupling-loss between IMT in-band transmission into system in-band reception. self.coupling_loss_imt_system = self.calculate_coupling_loss_system_imt( - self.system, self.ue, is_co_channel=True, ) + self.system, + self.ue, + system_inband=True, + imt_inband=True, + ) + if self.adjacent_channel: - self.coupling_loss_imt_system_adjacent = \ - self.calculate_coupling_loss_system_imt( + # coupling-loss from IMT's out-of-band emissions into System in-band reception + if self.parameters.imt.adjacent_ch_emissions != "OFF": + # TODO: Store this for results later + self.coupling_loss_oob_tx_inband_rx = self.calculate_coupling_loss_system_imt( self.system, self.ue, - is_co_channel=False, + system_inband=True, + imt_inband=False, ) + # coupling-loss from IMT's in-band transmission into Systems out-of-band reception + # (e.g. due to reception filter imperfections). + self.coupling_loss_imt_system_adjacent = self.calculate_coupling_loss_system_imt( + self.system, + self.ue, + system_inband=False, + imt_inband=True, + ) # applying a bandwidth scaling factor since UE transmits on a portion # of the satellite's bandwidth @@ -451,10 +476,11 @@ def calculate_external_interference(self): ) # Out of band power - tx_oob -= self.coupling_loss_imt_system_adjacent[ue, sys_active] + if self.parameters.imt.adjacent_ch_emissions != "OFF": + tx_oob -= self.coupling_loss_oob_tx_inband_rx[ue, sys_active] if self.param_system.adjacent_ch_reception != "OFF": - rx_oob -= self.coupling_loss_imt_system[ue, sys_active] + rx_oob -= self.coupling_loss_imt_system_adjacent[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) @@ -561,6 +587,9 @@ def collect_results(self, write_to_file: bool, snapshot_number: int): 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(),) + if len(self.system_imt_antenna_gain_adjacent): + self.results.system_imt_antenna_gain_adjacent.extend( + self.system_imt_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(), ) @@ -572,9 +601,13 @@ def collect_results(self, write_to_file: bool, snapshot_number: int): 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.system_imt_antenna_gain): + self.results.system_imt_antenna_gain.extend( + self.system_imt_antenna_gain[np.ix_(sys_active, ue)].flatten(), + ) + if len(self.system_imt_antenna_gain_adjacent): + self.results.system_imt_antenna_gain_adjacent.extend( + self.system_imt_antenna_gain_adjacent[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(), diff --git a/sharc/station_factory.py b/sharc/station_factory.py index de9fac81..6fb5362c 100644 --- a/sharc/station_factory.py +++ b/sharc/station_factory.py @@ -33,7 +33,6 @@ from sharc.antenna.antenna import Antenna from sharc.antenna.antenna_factory import AntennaFactory from sharc.antenna.antenna_fss_ss import AntennaFssSs -from sharc.antenna.antenna_mss_adjacent import AntennaMSSAdjacent from sharc.antenna.antenna_omni import AntennaOmni from sharc.antenna.antenna_f699 import AntennaF699 from sharc.antenna.antenna_f1891 import AntennaF1891 @@ -58,6 +57,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.mask.spectral_mask_stepped import SpectralMaskStepped from sharc.support.sharc_geom import CoordinateSystem from sharc.support.sharc_utils import wrap2_180 @@ -162,6 +162,18 @@ def generate_imt_base_stations( num_bs ) + # Create out-of-band antenna patterns if specified. + # IMT Array antenna pattern has the oob model defined inside + if param.bs.use_oob_antenna and param.bs.antenna.pattern != "ARRAY": + imt_base_stations.oob_antenna = AntennaFactory.create_n_antennas( + param.bs.oob_antenna, + imt_base_stations.azimuth, + imt_base_stations.elevation, + num_bs, + ) + else: + imt_base_stations.oob_antenna = imt_base_stations.antenna + # imt_base_stations.antenna = [AntennaOmni(0) for bs in range(num_bs)] imt_base_stations.bandwidth = param.bandwidth * np.ones(num_bs) imt_base_stations.center_freq = param.frequency * np.ones(num_bs) @@ -465,6 +477,16 @@ def generate_imt_ue_outdoor( imt_ue.elevation, num_ue, ) + # Create out-of-band antenna patterns if specified. + if param.ue.use_oob_antenna and param.ue.antenna.pattern != "ARRAY": + imt_ue.oob_antenna = AntennaFactory.create_n_antennas( + param.ue.oob_antenna, + imt_ue.azimuth, + imt_ue.elevation, + num_ue, + ) + else: + imt_ue.oob_antenna = imt_ue.antenna # imt_ue.antenna = [AntennaOmni(0) for bs in range(num_ue)] imt_ue.bandwidth = param.bandwidth * np.ones(num_ue) @@ -638,12 +660,14 @@ def generate_imt_ue_indoor( imt_ue.ext_interference = -500 * np.ones(num_ue) # TODO: this piece of code works only for uplink + # FIXME: Why inddor UEs use AntennaBeamformingImt always? 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], ) + imt_ue.oob_antenna = imt_ue.antenna # imt_ue.antenna = [AntennaOmni(0) for bs in range(num_ue)] imt_ue.bandwidth = param.bandwidth * np.ones(num_ue) @@ -844,6 +868,15 @@ def generate_single_space_station( space_station.elevation[0]) ]) + # Set the OOB antenna pattern if specified + if param.use_oob_antenna: + space_station.oob_antenna = np.array([ + AntennaFactory.create_antenna(param.oob_antenna, space_station.azimuth[0], + space_station.elevation[0]) + ]) + else: + space_station.oob_antenna = space_station.antenna + space_station.z = space_station.height space_station.bandwidth = param.bandwidth space_station.noise_temperature = param.noise_temperature @@ -931,6 +964,9 @@ def generate_fss_space_station(param: ParametersFssSs): ) sys.exit(1) + # Same OOB antenna pattern as in-band + fss_space_station.oob_antenna = fss_space_station.antenna + fss_space_station.bandwidth = np.array([param.bandwidth]) fss_space_station.noise_temperature = np.array( [param.noise_temperature]) @@ -1059,6 +1095,9 @@ def generate_fss_earth_station( ) sys.exit(1) + # Same OOB antenna pattern as in-band + fss_earth_station.oob_antenna = fss_earth_station.antenna + fss_earth_station.noise_temperature = np.array( [param.noise_temperature]) fss_earth_station.bandwidth = np.array([param.bandwidth]) @@ -1195,6 +1234,15 @@ def generate_single_earth_station( ) ]) + if param.use_oob_antenna: + single_earth_station.oob_antenna = np.array([ + AntennaFactory.create_antenna( + param.oob_antenna, single_earth_station.azimuth, single_earth_station.elevation + ) + ]) + else: + single_earth_station.oob_antenna = single_earth_station.antenna + single_earth_station.active = np.array([True]) single_earth_station.bandwidth = np.array([param.bandwidth]) @@ -1278,6 +1326,9 @@ def generate_fs_station(param: ParametersFs): ) sys.exit(1) + # The OOB antenna pattern should be the same as in-band. + fs_station.oob_antenna = fs_station.antenna + fs_station.noise_temperature = param.noise_temperature fs_station.bandwidth = np.array([param.bandwidth]) @@ -1341,6 +1392,9 @@ def generate_haps( ) sys.exit(1) + # TODO: Set the OOB antenna pattern if needed + haps.oob_antenna = haps.antenna + haps.bandwidth = np.array([param.bandwidth]) return haps @@ -1396,6 +1450,9 @@ def generate_rns( ) sys.exit(1) + # TODO: Set the oob antenna pattern if needed + rns.oob_antenna = rns.antenna + rns.bandwidth = np.array([param.bandwidth]) rns.noise_temperature = param.noise_temperature rns.thermal_noise = -500 @@ -1524,6 +1581,9 @@ def generate_space_station( ) sys.exit(1) + # TODO: Set the oob antenna pattern if needed + space_station.oob_antenna = space_station.antenna + space_station.bandwidth = np.array([param.bandwidth]) # Noise temperature is not an input parameter for yet used systems. # It is included here to calculate the useless I/N values @@ -1570,16 +1630,22 @@ def generate_mss_ss(param_mss: ParametersMssSs): param_mss.bandwidth * 10**6) mss_ss.antenna = np.empty(num_bs, dtype=AntennaS1528Leo) - for i in range(num_bs): - if param_mss.antenna_pattern == "ITU-R-S.1528-LEO": - mss_ss.antenna[i] = AntennaS1528Leo(param_mss.antenna_s1528) - elif param_mss.antenna_pattern == "ITU-R-S.1528-Section1.2": - mss_ss.antenna[i] = AntennaS1528(param_mss.antenna_s1528) - elif param_mss.antenna_pattern == "ITU-R-S.1528-Taylor": - mss_ss.antenna[i] = AntennaS1528Taylor(param_mss.antenna_s1528) - else: - raise ValueError( - "generate_mss_ss: Invalid antenna type: {param_mss.antenna_pattern}") + mss_ss.antenna = AntennaFactory.create_n_antennas( + param_mss.antenna, + mss_ss.azimuth, + mss_ss.elevation, + mss_ss.num_stations + ) + + if param_mss.use_oob_antenna: + mss_ss.oob_antenna = AntennaFactory.create_n_antennas( + param_mss.oob_antenna, + mss_ss.azimuth, + mss_ss.elevation, + mss_ss.num_stations + ) + else: + mss_ss.oob_antenna = mss_ss.antenna if param_mss.spectral_mask == "IMT-2020": mss_ss.spectral_mask = SpectralMaskImt( @@ -1665,6 +1731,10 @@ def generate_mss_d2d( mss_d2d.spectral_mask = SpectralMaskMSS(params.frequency, params.bandwidth, params.spurious_emissions) + elif params.spectral_mask == "STEPPED": + mss_d2d.spectral_mask = SpectralMaskStepped(params.frequency, + params.bandwidth, + mask_steps_dBm_mhz=list(params.spectral_mask_steps)) else: raise ValueError( f"Invalid or not implemented spectral mask - {params.spectral_mask}") @@ -1676,7 +1746,7 @@ def generate_mss_d2d( 1e6) + 30 ) - # Configure satellite positions in the StationManager + # 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"] @@ -1698,22 +1768,24 @@ def generate_mss_d2d( # Initialize satellites antennas # we need to initialize them after coordinates transformation because of # 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.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( - f"generate_mss_ss: Invalid antenna type: {params.antenna.pattern}") + # implementation. + mss_d2d.antenna = AntennaFactory.create_n_antennas( + params.antenna, + mss_d2d.azimuth, + mss_d2d.elevation, + mss_d2d.num_stations + ) - for i in range(mss_d2d.num_stations): - mss_d2d.antenna[i] = antenna_pattern + # Initialize OOB antennas + if params.use_oob_antenna and params.antenna.pattern != "ARRAY": + mss_d2d.oob_antenna = AntennaFactory.create_n_antennas( + params.oob_antenna, + mss_d2d.azimuth, + mss_d2d.elevation, + mss_d2d.num_stations + ) + else: + mss_d2d.oob_antenna = mss_d2d.antenna return mss_d2d # Return the configured StationManager diff --git a/sharc/station_manager.py b/sharc/station_manager.py index 9ab7a908..a78c14b2 100644 --- a/sharc/station_manager.py +++ b/sharc/station_manager.py @@ -36,6 +36,7 @@ def __init__(self, n): self.rx_interference = np.empty(n) # Rx interferece in dBW self.ext_interference = np.empty(n) # External interferece in dBW self.antenna = np.empty(n, dtype=Antenna) + self.oob_antenna = np.empty(n, dtype=Antenna) # Out-of-band antenna pattern self.bandwidth = np.empty(n) # Bandwidth in MHz self.noise_figure = np.empty(n) self.noise_temperature = np.empty(n) diff --git a/tests/e2e/test_integration_imt_victim.py b/tests/e2e/test_integration_imt_victim.py index d5bc3994..bc483a73 100644 --- a/tests/e2e/test_integration_imt_victim.py +++ b/tests/e2e/test_integration_imt_victim.py @@ -152,14 +152,13 @@ def assert_sys_to_imt_dl_results_attr( # testing attributes that should be per ue towards system self.assertEqual(len(res.imt_system_antenna_gain_adjacent), n_ue * n_sys) + self.assertEqual(len(res.system_imt_antenna_gain_adjacent), n_ue * n_sys) self.assertEqual(len(res.imt_system_path_loss), n_ue * n_sys) # NOTE: it may not have co-channel since this test is for adjacent # self.assertEqual(len(res.imt_system_antenna_gain), n_ue * n_sys) # testing attributes that should be per system towards imt self.assertEqual(len(res.system_imt_antenna_gain), n_sys * n_ue) - # FIXME: why is this attr only on system -> IMT DL?? - self.assertEqual(len(res.sys_to_imt_coupling_loss), n_sys * n_ue) def assert_sys_to_imt_ul_results_attr( self, @@ -197,6 +196,7 @@ def assert_sys_to_imt_ul_results_attr( # testing attributes that should be per ue towards system self.assertEqual(len(res.imt_system_antenna_gain_adjacent), n_ue * n_sys) + self.assertEqual(len(res.system_imt_antenna_gain_adjacent), n_ue * n_sys) self.assertEqual(len(res.imt_system_path_loss), n_ue * n_sys) # NOTE: it may not have co-channel since this test is for adjacent, # so if need be, remove this from here and put it elsewhere @@ -730,6 +730,8 @@ def test_es_to_ue_mask(self): """ self.param.general.imt_link = "DOWNLINK" self.param.imt.interfered_with = True + self.param.general.enable_adjacent_channel = True + self.param.general.enable_cochannel = False self.param.single_earth_station.adjacent_ch_emissions = "SPECTRAL_MASK" self.param.single_earth_station.spectral_mask = "MSS" self.param.single_earth_station.spurious_emissions = -13. @@ -830,11 +832,11 @@ def test_es_to_ue_mask(self): npt.assert_allclose( coc_coupling_1k, - simulation_1k.coupling_loss_imt_system + simulation_1k.coupling_loss_oob_tx_inband_rx ) npt.assert_allclose( coc_coupling_3k, - simulation_3k.coupling_loss_imt_system + simulation_3k.coupling_loss_oob_tx_inband_rx ) mask_power_1k = self.param.single_earth_station.spurious_emissions + \ @@ -874,6 +876,8 @@ def test_es_to_bs_mask(self): This simplifies spectral mask calculation by only getting the spurious emissions """ self.param.general.imt_link = "UPLINK" + self.param.general.enable_adjacent_channel = True + self.param.general.enable_cochannel = False self.param.imt.interfered_with = True self.param.single_earth_station.adjacent_ch_emissions = "SPECTRAL_MASK" self.param.single_earth_station.spectral_mask = "MSS" @@ -965,22 +969,22 @@ def test_es_to_bs_mask(self): g2 = self.param.single_earth_station.antenna.gain - coc_coupling_1k = np.transpose(p_loss_1k - g1_co_1k - g2) - coc_coupling_3k = np.transpose(p_loss_3k - g1_co_3k - g2) + coc_oob_coupling_1k = np.transpose(p_loss_1k - g1_co_1k - g2) + coc_oob_coupling_3k = np.transpose(p_loss_3k - g1_co_3k - g2) npt.assert_allclose( - coc_coupling_1k, - simulation_1k.coupling_loss_imt_system + coc_oob_coupling_1k, + simulation_1k.coupling_loss_oob_tx_inband_rx ) npt.assert_allclose( - coc_coupling_3k, - simulation_3k.coupling_loss_imt_system + coc_oob_coupling_3k, + simulation_3k.coupling_loss_oob_tx_inband_rx ) mask_power_1k = self.param.single_earth_station.spurious_emissions + \ self.dB(simulation_1k.bs.bandwidth) mask_power_3k = np.repeat( - self.param.single_earth_station.spurious_emissions + \ + self.param.single_earth_station.spurious_emissions + self.dB(simulation_3k.bs.bandwidth), 3 ) @@ -992,8 +996,8 @@ def test_es_to_bs_mask(self): 1 ) - rx_power_1k = mask_power_1k - coc_coupling_1k.reshape(mask_power_1k.shape) - rx_power_3k = mask_power_3k - coc_coupling_3k.reshape(mask_power_3k.shape) + rx_power_1k = mask_power_1k - coc_oob_coupling_1k.reshape(mask_power_1k.shape) + rx_power_3k = mask_power_3k - coc_oob_coupling_3k.reshape(mask_power_3k.shape) npt.assert_almost_equal( np.ravel(list(simulation_1k.bs.ext_interference.values())), diff --git a/tests/e2e/test_integration_sys_victim.py b/tests/e2e/test_integration_sys_victim.py index 717cccb5..a72b235b 100644 --- a/tests/e2e/test_integration_sys_victim.py +++ b/tests/e2e/test_integration_sys_victim.py @@ -136,6 +136,7 @@ def assert_imt_dl_to_sys_results_attr( # testing attributes that should be per beam towards system self.assertEqual(len(res.imt_system_antenna_gain_adjacent), n_beams * n_sys) + self.assertEqual(len(res.system_imt_antenna_gain_adjacent), n_beams * n_sys) self.assertEqual(len(res.imt_system_path_loss), n_beams * n_sys) # NOTE: it may not have co-channel since this test is for adjacent # self.assertEqual(len(res.imt_system_antenna_gain), n_beams * n_sys) @@ -170,6 +171,7 @@ def assert_imt_ul_to_sys_results_attr( # testing attributes that should be per ue towards system self.assertEqual(len(res.imt_system_antenna_gain_adjacent), n_ue * n_sys) + self.assertEqual(len(res.system_imt_antenna_gain_adjacent), n_ue * n_sys) self.assertEqual(len(res.imt_system_path_loss), n_ue * n_sys) # NOTE: it may not have co-channel since this test is for adjacent # self.assertEqual(len(res.imt_system_antenna_gain), n_ue * n_sys) @@ -236,18 +238,18 @@ def test_2bs_to_es_mask(self): g1 = self.param.imt.bs.antenna.array.element_max_g g2 = self.param.single_earth_station.antenna.gain - coupling = p_loss - g1 - g2 + oob_tx_inband_rx_coupling = p_loss - g1 - g2 npt.assert_allclose( - coupling, - simulation_1k.coupling_loss_imt_system_adjacent + oob_tx_inband_rx_coupling, + simulation_1k.coupling_loss_oob_tx_inband_rx ) npt.assert_allclose( - simulation_1k.coupling_loss_imt_system_adjacent.repeat(3), - np.ravel(simulation_3k.coupling_loss_imt_system_adjacent) + simulation_1k.coupling_loss_oob_tx_inband_rx.repeat(3), + np.ravel(simulation_3k.coupling_loss_oob_tx_inband_rx) ) mask_power = self.param.imt.spurious_emissions + self.dB(self.param.single_earth_station.bandwidth) - rx_power = self.dB(self.lin(mask_power - coupling).sum()) + rx_power = self.dB(self.lin(mask_power - oob_tx_inband_rx_coupling).sum()) self.assertEqual( len(simulation_1k.results.system_dl_interf_power), 1 @@ -258,8 +260,8 @@ def test_2bs_to_es_mask(self): ) npt.assert_allclose( - simulation_3k.coupling_loss_imt_system_adjacent, - simulation_1k.coupling_loss_imt_system_adjacent[0][0], + simulation_3k.coupling_loss_oob_tx_inband_rx, + simulation_1k.coupling_loss_oob_tx_inband_rx[0][0], ) npt.assert_almost_equal( simulation_3k.results.system_dl_interf_power, @@ -280,6 +282,8 @@ def test_2bs_to_es_aclr_and_acs_partial_overlap(self): Testing BS acs and aclr with partial co-channel """ self.param.general.imt_link = "DOWNLINK" + self.param.general.enable_adjacent_channel = True + self.param.general.enable_cochannel = False self.param.imt.interfered_with = False self.param.imt.adjacent_ch_emissions = "ACLR" @@ -298,7 +302,7 @@ def test_2bs_to_es_aclr_and_acs_partial_overlap(self): simulation_1k = SimulationDownlink(self.param, "") simulation_1k.initialize() - self.assertFalse(simulation_1k.co_channel) + # self.assertFalse(simulation_1k.co_channel) simulation_1k.snapshot( write_to_file=False, @@ -377,26 +381,26 @@ def test_2bs_to_es_aclr_and_acs_partial_overlap(self): g2 = self.param.single_earth_station.antenna.gain - adj_coupling = p_loss - np.transpose(g1_adj) - g2 - coc_coupling_1k = p_loss - np.transpose(g1_co_1k) - g2 - coc_coupling_3k = np.reshape(p_loss.repeat(3), (6, 1)) - np.transpose(g1_co_3k) - g2 + oob_tx_indband_tx_coupling = p_loss - np.transpose(g1_adj) - g2 + inband_tx_oob_rx_coupling_1k = p_loss - np.transpose(g1_co_1k) - g2 + indbanx_tx_oob_rx_coupling_3k = np.reshape(p_loss.repeat(3), (6, 1)) - np.transpose(g1_co_3k) - g2 npt.assert_allclose( - simulation_1k.coupling_loss_imt_system_adjacent, - adj_coupling, + simulation_1k.coupling_loss_oob_tx_inband_rx, + oob_tx_indband_tx_coupling, ) npt.assert_allclose( - np.ravel(simulation_3k.coupling_loss_imt_system_adjacent), - np.ravel(adj_coupling.repeat(3)), + np.ravel(simulation_3k.coupling_loss_oob_tx_inband_rx), + np.ravel(oob_tx_indband_tx_coupling.repeat(3)), ) npt.assert_allclose( - coc_coupling_1k, - simulation_1k.coupling_loss_imt_system + inband_tx_oob_rx_coupling_1k, + simulation_1k.coupling_loss_imt_system_adjacent ) npt.assert_allclose( - coc_coupling_3k, - simulation_3k.coupling_loss_imt_system + indbanx_tx_oob_rx_coupling_3k, + simulation_3k.coupling_loss_imt_system_adjacent ) imt_non_overlap = self.param.imt.bandwidth - overlap sys_non_overlap = self.param.single_earth_station.bandwidth - overlap @@ -445,12 +449,12 @@ def test_2bs_to_es_aclr_and_acs_partial_overlap(self): tx_oob_3k = self.dB(psd_3k * sys_non_overlap) rx_power_1k = self.dB( - self.lin(rx_oob_1k - coc_coupling_1k).sum() + \ - self.lin(tx_oob_1k - adj_coupling).sum() + self.lin(rx_oob_1k - inband_tx_oob_rx_coupling_1k).sum() + + self.lin(tx_oob_1k - oob_tx_indband_tx_coupling).sum() ) rx_power_3k = self.dB( - self.lin(rx_oob_3k.reshape(coc_coupling_3k.shape) - coc_coupling_3k).sum() + \ - self.lin(tx_oob_3k - adj_coupling).sum() + self.lin(rx_oob_3k.reshape(indbanx_tx_oob_rx_coupling_3k.shape) - indbanx_tx_oob_rx_coupling_3k).sum() + + self.lin(tx_oob_3k - oob_tx_indband_tx_coupling).sum() ) self.assertEqual( @@ -543,19 +547,19 @@ def test_ue_to_es_mask(self): g1 = self.param.imt.ue.antenna.array.element_max_g g2 = self.param.single_earth_station.antenna.gain - coupling_1k = p_loss_1k - g1 - g2 - coupling_3k = p_loss_3k - g1 - g2 + oob_tx_inband_rx_coupling_1k = p_loss_1k - g1 - g2 + oob_tx_inband_rx_coupling_3k = p_loss_3k - g1 - g2 npt.assert_allclose( - coupling_1k, - simulation_1k.coupling_loss_imt_system_adjacent + oob_tx_inband_rx_coupling_1k, + simulation_1k.coupling_loss_oob_tx_inband_rx ) npt.assert_allclose( - coupling_3k.flatten(), - simulation_3k.coupling_loss_imt_system_adjacent.flatten() + oob_tx_inband_rx_coupling_3k.flatten(), + simulation_3k.coupling_loss_oob_tx_inband_rx.flatten() ) mask_power = self.param.imt.spurious_emissions + self.dB(self.param.single_earth_station.bandwidth) - rx_power = self.dB(self.lin(mask_power - coupling_1k).sum()) + rx_power = self.dB(self.lin(mask_power - oob_tx_inband_rx_coupling_1k).sum()) self.assertEqual( len(simulation_1k.results.system_ul_interf_power), 1 @@ -708,28 +712,27 @@ def test_ue_to_es_aclr_and_acs_partial_overlap(self): g2 = self.param.single_earth_station.antenna.gain - adj_coupling_1k = p_loss_1k - np.transpose(g1_adj_1k) - g2 - - adj_coupling_3k = p_loss_3k - np.transpose(g1_adj_3k) - g2 - coc_coupling_1k = p_loss_1k - np.transpose(g1_co_1k) - g2 - coc_coupling_3k = p_loss_3k - np.transpose(g1_co_3k) - g2 + oob_tx_inband_rx_coupling_1k = p_loss_1k - np.transpose(g1_adj_1k) - g2 + oob_tx_inband_rx_coupling_3k = p_loss_3k - np.transpose(g1_adj_3k) - g2 + inband_tx_oob_rx_coupling_1k = p_loss_1k - np.transpose(g1_co_1k) - g2 + inband_tx_oob_rx_coupling_3k = p_loss_3k - np.transpose(g1_co_3k) - g2 npt.assert_allclose( - simulation_1k.coupling_loss_imt_system_adjacent, - adj_coupling_1k, + simulation_1k.coupling_loss_oob_tx_inband_rx, + oob_tx_inband_rx_coupling_1k, ) npt.assert_allclose( - simulation_3k.coupling_loss_imt_system_adjacent, - adj_coupling_3k, + simulation_3k.coupling_loss_oob_tx_inband_rx, + oob_tx_inband_rx_coupling_3k, ) npt.assert_allclose( - coc_coupling_1k, - simulation_1k.coupling_loss_imt_system + inband_tx_oob_rx_coupling_1k, + simulation_1k.coupling_loss_imt_system_adjacent ) npt.assert_allclose( - coc_coupling_3k, - simulation_3k.coupling_loss_imt_system + inband_tx_oob_rx_coupling_3k, + simulation_3k.coupling_loss_imt_system_adjacent ) imt_non_overlap = self.param.imt.bandwidth - overlap sys_non_overlap = self.param.single_earth_station.bandwidth - overlap @@ -778,13 +781,13 @@ def test_ue_to_es_aclr_and_acs_partial_overlap(self): tx_oob_3k = self.dB(psd_3k * sys_non_overlap) rx_power_1k = self.dB( - self.lin(rx_oob_1k.flatten() - coc_coupling_1k.flatten()).sum() + \ - self.lin(tx_oob_1k.flatten() - adj_coupling_1k.flatten()).sum() + self.lin(rx_oob_1k.flatten() - inband_tx_oob_rx_coupling_1k.flatten()).sum() + \ + self.lin(tx_oob_1k.flatten() - oob_tx_inband_rx_coupling_1k.flatten()).sum() ) rx_power_3k = self.dB( - self.lin(rx_oob_3k.flatten() - coc_coupling_3k.flatten()).sum() + \ - self.lin(tx_oob_3k.flatten() - adj_coupling_3k.flatten()).sum() + self.lin(rx_oob_3k.flatten() - inband_tx_oob_rx_coupling_3k.flatten()).sum() + \ + self.lin(tx_oob_3k.flatten() - oob_tx_inband_rx_coupling_3k.flatten()).sum() ) self.assertEqual( diff --git a/tests/parameters/parameters_for_testing.yaml b/tests/parameters/parameters_for_testing.yaml index ed61cb37..6fc9af71 100644 --- a/tests/parameters/parameters_for_testing.yaml +++ b/tests/parameters/parameters_for_testing.yaml @@ -334,7 +334,6 @@ imt: # Base Station Antenna parameters: antenna: pattern: "ARRAY" - # pattern: ITU-R-S.1528-Taylor array: ########################################################################### # If normalization of M2101 should be applied for BS @@ -430,6 +429,17 @@ imt: l_t: 1.6 frequency: 2177.0 bandwidth: 6.0 + ########################################################################### + # Defines the out-of-band antenna model to be used in compatibility studies + use_oob_antenna: true + # Out-of-band antenna pattern + oob_antenna: + gain: 0.0 + pattern: Cosine Antenna + mss_adjacent: + frequency: 2170.0 + + ########################################################################### # User Equipment parameters: ue: @@ -994,6 +1004,8 @@ mss_d2d: tx_power_density: -30 # Number of sectors num_sectors: 19 + # Spectral mask steps for mask STEPPED + spectral_mask_steps: !!python/tuple [-10., -15., -20.] beam_positioning: # type may be one of # "ANGLE_FROM_SUBSATELLITE", "ANGLE_AND_DISTANCE_FROM_SUBSATELLITE", @@ -1103,6 +1115,14 @@ mss_d2d: l_t: 1.6 frequency: 2177.0 bandwidth: 6.0 + # Flag to indicate if out-of-band antenna pattern should be used + use_oob_antenna: true + # Out-of-band antenna pattern + oob_antenna: + pattern: Cosine Antenna + mss_adjacent: + frequency: 2170.1 + # 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 0169a47d..8fc55c43 100644 --- a/tests/parameters/test_parameters.py +++ b/tests/parameters/test_parameters.py @@ -182,6 +182,10 @@ def test_parameters_imt(self): 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) + # Check the OOB antenna pattern parameters + self.assertEqual(self.parameters.imt.bs.oob_antenna.pattern, "Cosine Antenna") + self.assertEqual(self.parameters.imt.bs.oob_antenna.mss_adjacent.frequency, 2170.0) + """Test ParametersSubarrayImt """ # testing default value not enabled @@ -633,6 +637,7 @@ def test_parametes_mss_d2d(self): self.assertEqual(self.parameters.mss_d2d.cell_radius, 19001) 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.spectral_mask_steps, (-10., -15., -20.)) self.assertEqual(self.parameters.mss_d2d.num_sectors, 19) self.assertEqual( self.parameters.mss_d2d.antenna.pattern, @@ -645,6 +650,10 @@ def test_parametes_mss_d2d(self): 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) + # Test oob antenna pattern + self.assertEqual(self.parameters.mss_d2d.use_oob_antenna, True) + self.assertEqual(self.parameters.mss_d2d.oob_antenna.pattern, 'Cosine Antenna') + self.assertEqual(self.parameters.mss_d2d.oob_antenna.mss_adjacent.frequency, 2170.1) 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_adjacent_channel.py b/tests/test_adjacent_channel.py index aca41822..fa362aad 100644 --- a/tests/test_adjacent_channel.py +++ b/tests/test_adjacent_channel.py @@ -54,7 +54,7 @@ def setUp(self): self.param.imt.bs.conducted_power = 10 self.param.imt.bs.height = 6 - self.param.imt.bs.acs = 30 + self.param.imt.bs.adjacent_ch_selectivity = 30 self.param.imt.bs.noise_figure = 7 self.param.imt.bs.ohmic_loss = 3 self.param.imt.uplink.attenuation_factor = 0.4 @@ -70,10 +70,9 @@ def setUp(self): self.param.imt.ue.p_o_pusch = -95 self.param.imt.ue.alpha = 0.8 self.param.imt.ue.p_cmax = 20 - self.param.imt.ue.conducted_power = 10 self.param.imt.ue.height = 1.5 - self.param.imt.ue.aclr = 20 - self.param.imt.ue.acs = 25 + self.param.imt.ue.adjacent_ch_leak_ratio = 20 + self.param.imt.ue.adjacent_ch_selectivity = 25 self.param.imt.ue.noise_figure = 9 self.param.imt.ue.ohmic_loss = 3 self.param.imt.ue.body_loss = 4 @@ -81,48 +80,10 @@ def setUp(self): self.param.imt.downlink.sinr_min = -10 self.param.imt.downlink.sinr_max = 30 self.param.imt.channel_model = "FSPL" - # probability of line-of-sight (not for FSPL) - self.param.imt.line_of_sight_prob = 0.75 self.param.imt.shadowing = False self.param.imt.noise_temperature = 290 - self.param.imt.bs.antenna.type = "ARRAY" - self.param.imt.ue.antenna.type = "ARRAY" - - self.param.imt.bs.antenna.array.adjacent_antenna_model = "SINGLE_ELEMENT" - self.param.imt.ue.antenna.array.adjacent_antenna_model = "SINGLE_ELEMENT" - self.param.imt.bs.antenna.array.normalization = False - self.param.imt.ue.antenna.array.normalization = False - - self.param.imt.bs.antenna.array.normalization_file = None - self.param.imt.bs.antenna.array.element_pattern = "M2101" - self.param.imt.bs.antenna.array.minimum_array_gain = -200 - self.param.imt.bs.antenna.array.element_max_g = 10 - self.param.imt.bs.antenna.array.element_phi_3db = 80 - self.param.imt.bs.antenna.array.element_theta_3db = 80 - self.param.imt.bs.antenna.array.element_am = 25 - self.param.imt.bs.antenna.array.element_sla_v = 25 - self.param.imt.bs.antenna.array.n_rows = 16 - self.param.imt.bs.antenna.array.n_columns = 16 - self.param.imt.bs.antenna.array.element_horiz_spacing = 1 - self.param.imt.bs.antenna.array.element_vert_spacing = 1 - self.param.imt.bs.antenna.array.multiplication_factor = 12 - self.param.imt.bs.antenna.array.downtilt = 10 - - self.param.imt.ue.antenna.array.element_pattern = "M2101" - self.param.imt.ue.antenna.array.minimum_array_gain = -200 - self.param.imt.ue.antenna.array.normalization_file = None - self.param.imt.ue.antenna.array.element_max_g = 5 - self.param.imt.ue.antenna.array.element_phi_3db = 65 - self.param.imt.ue.antenna.array.element_theta_3db = 65 - self.param.imt.ue.antenna.array.element_am = 30 - self.param.imt.ue.antenna.array.element_sla_v = 30 - self.param.imt.ue.antenna.array.n_rows = 2 - self.param.imt.ue.antenna.array.n_columns = 1 - self.param.imt.ue.antenna.array.element_horiz_spacing = 0.5 - self.param.imt.ue.antenna.array.element_vert_spacing = 0.5 - self.param.imt.ue.antenna.array.multiplication_factor = 12 - + # FSS-SS System Parameters self.param.fss_ss.frequency = 5000 self.param.fss_ss.bandwidth = 100 self.param.fss_ss.altitude = 35786000 @@ -133,15 +94,7 @@ def setUp(self): 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.adjacent_ch_reception = "ACS" self.param.fss_ss.adjacent_ch_selectivity = 46 self.param.fss_ss.polarization_loss = 3.0 @@ -167,6 +120,7 @@ def test_simulation_2bs_4ue_downlink(self): random_number_gen, ) self.simulation.bs.antenna = np.array([AntennaOmni(1), AntennaOmni(2)]) + self.simulation.bs.oob_antenna = self.simulation.bs.antenna self.simulation.bs.active = np.ones(2, dtype=bool) self.simulation.ue = StationFactory.generate_imt_ue( @@ -180,6 +134,7 @@ def test_simulation_2bs_4ue_downlink(self): self.simulation.ue.antenna = np.array( [AntennaOmni(10), AntennaOmni(11), AntennaOmni(22), AntennaOmni(23)], ) + self.simulation.ue.oob_antenna = self.simulation.ue.antenna self.simulation.ue.active = np.ones(4, dtype=bool) # test connection method @@ -304,7 +259,9 @@ def test_simulation_2bs_4ue_uplink(self): self.simulation.topology, random_number_gen, ) + # FIXME: We should not set the oob_antenna manually self.simulation.bs.antenna = np.array([AntennaOmni(1), AntennaOmni(2)]) + self.simulation.bs.oob_antenna = self.simulation.bs.antenna self.simulation.bs.active = np.ones(2, dtype=bool) self.simulation.ue = StationFactory.generate_imt_ue( @@ -318,6 +275,7 @@ def test_simulation_2bs_4ue_uplink(self): self.simulation.ue.antenna = np.array( [AntennaOmni(10), AntennaOmni(11), AntennaOmni(22), AntennaOmni(23)], ) + self.simulation.ue.oob_antenna = self.simulation.ue.antenna self.simulation.ue.active = np.ones(4, dtype=bool) # test connection method diff --git a/tests/test_simulation_downlink_tvro.py b/tests/test_simulation_downlink_tvro.py index 216181a9..9af38bac 100644 --- a/tests/test_simulation_downlink_tvro.py +++ b/tests/test_simulation_downlink_tvro.py @@ -78,7 +78,7 @@ def setUp(self): self.param.imt.shadowing = False self.param.imt.noise_temperature = 290 - self.param.imt.bs.antenna.type = "ARRAY" + self.param.imt.bs.antenna.pattern = "ARRAY" self.param.imt.bs.antenna.array.adjacent_antenna_model = "BEAMFORMING" self.param.imt.ue.antenna.array.adjacent_antenna_model = "BEAMFORMING" self.param.imt.bs.antenna.array.normalization = False @@ -97,7 +97,9 @@ def setUp(self): self.param.imt.bs.antenna.array.multiplication_factor = 12 self.param.imt.bs.antenna.array.downtilt = 10 - self.param.imt.ue.antenna.type = "ARRAY" + self.param.imt.bs.oob_antenna = self.param.imt.bs.antenna + + self.param.imt.ue.antenna.pattern = "ARRAY" self.param.imt.ue.antenna.array.element_pattern = "FIXED" self.param.imt.ue.antenna.array.normalization = False self.param.imt.ue.antenna.array.normalization_file = None @@ -113,6 +115,8 @@ 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.imt.ue.oob_antenna = self.param.imt.ue.antenna + self.param.fss_es.location = "FIXED" self.param.fss_es.x = 100 self.param.fss_es.y = 0 diff --git a/tests/test_spectral_mask_stepped.py b/tests/test_spectral_mask_stepped.py new file mode 100644 index 00000000..6d87141c --- /dev/null +++ b/tests/test_spectral_mask_stepped.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +""" +Created on Tue Dec 5 11:56:10 2017 + +@author: Calil +""" + +import unittest +import numpy as np +import numpy.testing as npt + +from sharc.mask.spectral_mask_stepped import SpectralMaskStepped + + +class SpectalMaskSteppedTest(unittest.TestCase): + """Unit tests for the SpectralMaskStepped class and its power calculation method.""" + + def test_power_calc(self): + """Test power calculation for the Stepped spectral mask at a given frequency and bandwidth.""" + freq = 2100 # MHz + band = 5 # MHz + p_tx_density = 0.0 # dBm / MHz + p_tx = p_tx_density + 10 * np.log10(band) # dBm + spurious_emissions = -30.0 # dBm/MHz + mask_steps = [-10, -15, -20] # dBm/MHz + mask_steps = np.concatenate([mask_steps, [spurious_emissions]]) + + # Create mask + msk = SpectralMaskStepped(freq, band, mask_steps) + msk.set_mask(p_tx) + + N = len(msk.delta_f_lim) + + should_eq = np.zeros(2 * N) + eq = np.zeros(2 * N) + for i in range(N): + f_offset = band + (i) * band + + # center to right edge + should_eq[i + N] = mask_steps[i] + 10 * np.log10(band) + eq[i + N] = msk.power_calc(freq + f_offset, band) + + # center to left edge + should_eq[N - i - 1] = should_eq[i + N] + eq[N - i - 1] = msk.power_calc(freq - f_offset, band) + + npt.assert_almost_equal(should_eq, eq) + + npt.assert_equal( + -np.inf, + msk.power_calc( + freq, band, + ), + ) + + # test between step edges + for i in range(len(msk.mask_steps_dBm_mhz) - 1): + center_f = freq + 3 * band / 2 + i * band + actual_tx_oob = msk.power_calc(center_f=center_f, band=5) + desired_tx_oob = 10 * np.log10(np.power(10, (msk.mask_steps_dBm_mhz[i] + 10 * np.log10(band / 2)) / 10) + + np.power(10, (msk.mask_steps_dBm_mhz[i + 1] + 10 * np.log10(band / 2)) / 10)) + npt.assert_almost_equal(actual_tx_oob, desired_tx_oob) + + # test between step edges plus an offset + for _ in range(1000): # test for flaky behavior + for i in range(len(msk.mask_steps_dBm_mhz) - 1): + center_f = freq + 3 * band / 2 + i * band + offset = np.random.random() * (2.5) + actual_tx_oob = msk.power_calc(center_f=center_f + offset, band=5) + desired_tx_oob = 10 * np.log10( + np.power(10, (msk.mask_steps_dBm_mhz[i] + 10 * np.log10(band / 2 - offset)) / 10) + + np.power(10, (msk.mask_steps_dBm_mhz[i + 1] + 10 * np.log10(band / 2 + offset)) / 10)) + npt.assert_almost_equal(actual_tx_oob, desired_tx_oob) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_station_factory.py b/tests/test_station_factory.py index 5b212d30..9b50f603 100644 --- a/tests/test_station_factory.py +++ b/tests/test_station_factory.py @@ -12,8 +12,10 @@ from sharc.parameters.imt.parameters_imt import ParametersImt from sharc.station_factory import StationFactory from sharc.topology.topology_ntn import TopologyNTN +from sharc.topology.topology_single_base_station import TopologySingleBaseStation from sharc.parameters.parameters_single_space_station import ParametersSingleSpaceStation from sharc.station_manager import StationManager +from sharc.antenna.antenna_element_cosine import AntennaElementCosine from sharc.satellite.ngso.constants import EARTH_RADIUS_M @@ -27,6 +29,69 @@ def setUp(self): def test_generate_imt_base_stations(self): """Test IMT base station generation (placeholder).""" + def test_generate_imt_base_stations_oob_antennas(self): + """Test IMT base station generation with and without out-of-band antennas.""" + rng = np.random.RandomState(42) + param_imt = ParametersImt() + + # First test with ARRAY antenna pattern. The oob antenna should be the same object. + param_imt.bs.antenna.pattern = "ARRAY" + param_imt.bs.use_oob_antenna = True + + param_imt.topology.type = "SINGLE_BS" + param_imt.topology.single_bs.num_clusters = 1 + param_imt.topology.single_bs.intersite_distance = 500 + param_imt.topology.single_bs.cell_radius = 500 + param_imt.topology.single_bs.azimuth = "random" + + param_imt.validate("station factory test") + + single_bs_topology = TopologySingleBaseStation( + param_imt.topology.single_bs.cell_radius, + param_imt.topology.single_bs.num_clusters, + param_imt.topology.single_bs.azimuth, + ) + + single_bs_topology.calculate_coordinates() + + imt_bs = StationFactory.generate_imt_base_stations( + param_imt, param_imt.bs.antenna.array, single_bs_topology, rng) + + # When the in-band antenna is ARRAY, the oob antenna should be the same object + self.assertIs(imt_bs.oob_antenna, imt_bs.antenna) # both should point to the same list + + # What if the user sets a non-ARRAY oob-antenna pattern but the in-band is ARRAY? + param_imt.bs.oob_antenna.pattern = "Cosine Antenna" + param_imt.bs.oob_antenna.gain = 0.0 + param_imt.bs.oob_antenna.mss_adjacent.frequency = 2000.0 + param_imt.validate("station factory test 2") + imt_bs = StationFactory.generate_imt_base_stations( + param_imt, param_imt.bs.antenna.array, single_bs_topology, rng) + # When the in-band antenna is ARRAY, the oob antenna should be the same object no matter what + self.assertIs(imt_bs.oob_antenna, imt_bs.antenna) # both should point to the same list + + # Now test with non-ARRAY antenna pattern. The oob antenna should be a different object. + # Re-create the imt_bs with a non-ARRAY oob-antenna pattern + param_imt.bs.use_oob_antenna = True + param_imt.bs.antenna.gain = 30.0 + param_imt.bs.antenna.pattern = "ITU-R-S.1528-Taylor" + param_imt.bs.antenna.itu_r_s_1528.frequency = 2000.0 + param_imt.bs.antenna.itu_r_s_1528.bandwidth = 5.0 + param_imt.bs.antenna.itu_r_s_1528.slr = 20.0 + param_imt.bs.antenna.itu_r_s_1528.n_side_lobes = 2 + param_imt.bs.oob_antenna.pattern = "Cosine Antenna" + param_imt.bs.oob_antenna.gain = 0.0 + param_imt.bs.oob_antenna.mss_adjacent.frequency = 2000.0 + param_imt.validate("station factory test 2") + + imt_bs = StationFactory.generate_imt_base_stations( + param_imt, param_imt.bs.antenna.array, single_bs_topology, rng) + + # When the in-band antenna is not ARRAY, the oob antenna should be a different object + self.assertIsNot(imt_bs.oob_antenna, imt_bs.antenna) + for oob_antenna in imt_bs.oob_antenna: + self.assertIsInstance(oob_antenna, AntennaElementCosine) + def test_generate_imt_base_stations_ntn(self): """Test for IMT-NTN space station generation.""" # seed = 100 # Unused variable removed diff --git a/tests/test_station_factory_ngso.py b/tests/test_station_factory_ngso.py index a34ef349..ad157738 100644 --- a/tests/test_station_factory_ngso.py +++ b/tests/test_station_factory_ngso.py @@ -3,6 +3,7 @@ from sharc.support.enumerations import StationType from sharc.station_factory import StationFactory from sharc.station_manager import StationManager +from sharc.antenna.antenna_element_cosine import AntennaElementCosine from sharc.support.sharc_geom import CoordinateSystem, lla2ecef import numpy as np @@ -64,6 +65,9 @@ def setUp(self): self.param.antenna.itu_r_s_1528.l_r = 1.6 self.param.antenna.itu_r_s_1528.l_t = 1.6 + self.param.propagate_parameters() + self.param.validate("MSS_D2D_Test") + # Creating an IMT topology # imt_topology = TopologySingleBaseStation( # cell_radius=500, @@ -175,6 +179,49 @@ def test_satellite_coordinate_reversing(self): ngso_original_coord.elevation, atol=1e-500) + def test_ngso_oob_antenna(self): + """Test that out-of-band antenna patterns are created correctly for NGSO stations.""" + rng = np.random.RandomState(seed=self.seed) + + self.param.use_oob_antenna = False + self.param.validate("oob_antenna_test") + + ngso_manager = StationFactory.generate_mss_d2d(self.param, rng, self.coord_sys) + + # If oob_antenna is disabled, both antennas should point to the same object + self.assertIs(ngso_manager.oob_antenna, ngso_manager.antenna) + + self.param.use_oob_antenna = True + self.param.oob_antenna.pattern = "Cosine Antenna" + self.param.oob_antenna.gain = 0.0 + self.param.propagate_parameters() + self.param.validate("oob_antenna_test") + + ngso_manager = StationFactory.generate_mss_d2d(self.param, rng, self.coord_sys) + + # the oob_antenna should be a different object now + self.assertIsNot(ngso_manager.oob_antenna, ngso_manager.antenna) + for a in ngso_manager.oob_antenna: + self.assertIsInstance(a, AntennaElementCosine) + + def test_ngso_spectral_mask_stepped(self): + """Test that NGSO stations use the STEPPED spectral mask when specified.""" + rng = np.random.RandomState(seed=self.seed) + + self.param.spectral_mask = "STEPPED" + self.param.spectral_mask_steps = (-10., -15., -20.) + self.param.propagate_parameters() + self.param.validate("spectral_mask_stepped_test") + + ngso_manager = StationFactory.generate_mss_d2d(self.param, rng, self.coord_sys) + + # Check that all stations have the correct spectral mask type + self.assertEqual(ngso_manager.spectral_mask.__class__.__name__, "SpectralMaskStepped") + self.assertEqual( + ngso_manager.spectral_mask.mask_steps_dBm_mhz, + list(self.param.spectral_mask_steps) + ) + if __name__ == '__main__': unittest.main()