diff --git a/sharc/antenna/antenna_f1245_fs.py b/sharc/antenna/antenna_f1245_fs.py new file mode 100644 index 00000000..2643abb8 --- /dev/null +++ b/sharc/antenna/antenna_f1245_fs.py @@ -0,0 +1,186 @@ +import matplotlib.pyplot as plt +from sharc.antenna.antenna import Antenna +from sharc.parameters.parameters_antenna_with_diameter import ParametersAntennaWithDiameter +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: ParametersAntennaWithDiameter): + 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 = ParametersAntennaWithDiameter() + param.frequency = 2155 + 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 051e1f33..aed06714 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 a8fc8a0e..57adccf1 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,10 @@ class ParametersAntenna(ParametersBase): default_factory=ParametersAntennaS672, ) + itu_r_f_1245_fs: ParametersAntennaWithDiameter = field( + default_factory=ParametersAntennaWithDiameter, + ) + def set_external_parameters(self, **kwargs): """ Set external parameters for all sub-parameters of the antenna. @@ -198,6 +204,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 02384d4f..3334488f 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,