Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions sharc/antenna/antenna_element_cosine.py
Original file line number Diff line number Diff line change
@@ -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()
28 changes: 22 additions & 6 deletions sharc/antenna/antenna_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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(),
Expand All @@ -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)
Expand Down
122 changes: 0 additions & 122 deletions sharc/antenna/antenna_mss_adjacent.py

This file was deleted.

2 changes: 1 addition & 1 deletion sharc/antenna/antenna_mss_hibleo_x_ue.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class AntennaMssHibleoXUe(Antenna):

def __init__(self, frequency_MHz: float):
"""
Initialize the AntennaMSSAdjacent class.
Initialize the AntennaElementCosine class.

Parameters
----------
Expand Down
85 changes: 85 additions & 0 deletions sharc/mask/spectral_mask_stepped.py
Original file line number Diff line number Diff line change
@@ -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))
17 changes: 17 additions & 0 deletions sharc/parameters/imt/parameters_imt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)\
Expand Down
Loading