From 14411458b25d9fdbf7fc0b764fc7184575bfb703 Mon Sep 17 00:00:00 2001 From: fabricio Date: Fri, 30 Jan 2026 10:27:23 -0300 Subject: [PATCH 1/5] update(antenna): Added antenna F.1245 used by FS system --- sharc/antenna/antenna_f1245_fs.py | 196 +++++++++++++++++++++++++ sharc/antenna/antenna_factory.py | 3 + sharc/parameters/parameters_antenna.py | 22 ++- sharc/station_factory.py | 3 + 4 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 sharc/antenna/antenna_f1245_fs.py diff --git a/sharc/antenna/antenna_f1245_fs.py b/sharc/antenna/antenna_f1245_fs.py new file mode 100644 index 00000000..de554a19 --- /dev/null +++ b/sharc/antenna/antenna_f1245_fs.py @@ -0,0 +1,196 @@ + +# -*- coding: utf-8 -*- +""" +Created on Wed Apr 4 17:08:00 2018 +@author: Calil +""" +import matplotlib.pyplot as plt +from sharc.antenna.antenna import Antenna +from sharc.parameters.imt.parameters_imt import ParametersImt +from sharc.parameters.imt.parameters_imt import ParametersImt +from sharc.parameters.imt.parameters_antenna_imt import ParametersAntennaImt +from sharc.parameters.parameters_antenna import ParametersAntenna +import numpy as np +import math + + +class Antenna_f1245_fs(Antenna): + """Class that implements the ITU-R F.1245 antenna pattern for fixed + satellite service earth stations.""" + + def __init__(self, param: ParametersImt): + super().__init__() + self.peak_gain = param.gain + lmbda = 3e8 / (param.frequency * 1e6) + self.d_lmbda = param.diameter / lmbda + self.g_l = 2 + 15 * math.log10(self.d_lmbda) + self.phi_m = (20 / self.d_lmbda) * math.sqrt(self.peak_gain - self.g_l) + self.phi_r = 12.02 * math.pow(self.d_lmbda, -0.6) + + def calculate_gain(self, *args, **kwargs) -> np.array: + """ + Calculate the antenna gain for the given parameters. + + Parameters + ---------- + *args : tuple + Positional arguments (not used). + **kwargs : dict + Keyword arguments, expects 'phi_vec', 'theta_vec', and 'beams_l'. + + Returns + ------- + np.array + Calculated antenna gain values. + """ + # phi_vec = np.absolute(kwargs["phi_vec"]) + # theta_vec = np.absolute(kwargs["theta_vec"]) + # beams_l = np.absolute(kwargs["beams_l"]) + off_axis = np.absolute(kwargs["off_axis_angle_vec"]) + if self.d_lmbda > 100: + gain = self.calculate_gain_greater(off_axis) + else: + gain = self.calculate_gain_less(off_axis) + # idx_max_gain = np.where(beams_l == -1)[0] + # gain = self.peak_gain + return gain + + def calculate_gain_greater(self, phi: float) -> np.array: + """ + For frequencies in the range 1 GHz to about 70 GHz, in cases where the + ratio between the antenna diameter and the wavelength is GREATER than + 100, this method should be used. + Parameter + --------- + phi : off-axis angle [deg] + Returns + ------- + a numpy array containing the gains in the given angles + """ + gain = np.zeros(phi.shape) + idx_0 = np.where(phi < self.phi_m)[0] + gain[idx_0] = self.peak_gain - 2.5e-3 * \ + np.power(self.d_lmbda * phi[idx_0], 2) + phi_thresh = max(self.phi_m, self.phi_r) + idx_1 = np.where((self.phi_m <= phi) & (phi < phi_thresh))[0] + gain[idx_1] = self.g_l + idx_2 = np.where((phi_thresh <= phi) & (phi < 48))[0] + gain[idx_2] = 29 - 25 * np.log10(phi[idx_2]) + idx_3 = np.where((48 <= phi) & (phi <= 180))[0] + gain[idx_3] = -13 + return gain + + def calculate_gain_less(self, phi: float) -> np.array: + """ + For frequencies in the range 1 GHz to about 70 GHz, in cases where the + ratio between the antenna diameter and the wavelength is LESS than + or equal to 100, this method should be used. + Parameter + --------- + phi : off-axis angle [deg] + Returns + ------- + a numpy array containing the gains in the given angles + """ + gain = np.zeros(phi.shape) + idx_0 = np.where(phi < self.phi_m)[0] + gain[idx_0] = self.peak_gain - 0.0025 * \ + np.power(self.d_lmbda * phi[idx_0], 2) + idx_1 = np.where((self.phi_m <= phi) & (phi < 48))[0] + gain[idx_1] = 39 - 5 * \ + math.log10(self.d_lmbda) - 25 * np.log10(phi[idx_1]) + idx_2 = np.where((48 <= phi) & (phi < 180))[0] + gain[idx_2] = -3 - 5 * math.log10(self.d_lmbda) + return gain + + def add_beam(self, phi: float, theta: float): + """ + Add a new beam to the antenna. + + Parameters + ---------- + phi : float + Azimuth angle in degrees. + theta : float + Elevation angle in degrees. + """ + self.beams_list.append((phi, theta)) + + def calculate_off_axis_angle(self, Az, b): + """ + Calculate the off-axis angle between the main beam and a given direction. + + Parameters + ---------- + Az : float or np.array + Azimuth angle(s) in degrees. + b : float or np.array + Elevation angle(s) in degrees. + + Returns + ------- + float or np.array + Off-axis angle(s) in degrees. + """ + Az0 = self.beams_list[0][0] + a = 90 - self.beams_list[0][1] + C = Az0 - Az + off_axis_rad = np.arccos( + np.cos( + np.radians(a)) * + np.cos( + np.radians(b)) + + np.sin( + np.radians(a)) * + np.sin( + np.radians(b)) * + np.cos( + np.radians(C)), + ) + off_axis_deg = np.degrees(off_axis_rad) + return off_axis_deg + + +if __name__ == '__main__': + off_axis_angle_vec = np.linspace(0.1, 180, num=1001) + # initialize antenna parameters + param = ParametersAntenna() + param.frequency = 2155 + param_gt = ParametersAntennaImt() + param.gain = 33.1 + param.diameter = 2 + antenna_gt = Antenna_f1245_fs(param) + antenna_gt.add_beam(0, 0) + gain_gt = antenna_gt.calculate_gain( + off_axis_angle_vec=off_axis_angle_vec, + ) + param.diameter = 3 + antenna_gt = Antenna_f1245_fs(param) + gain_gt_3 = antenna_gt.calculate_gain( + off_axis_angle_vec=off_axis_angle_vec, + ) + param.diameter = 1.8 + antenna_gt = Antenna_f1245_fs(param) + gain_gt_18 = antenna_gt.calculate_gain( + off_axis_angle_vec=off_axis_angle_vec, + ) + + fig = plt.figure( + figsize=(8, 7), facecolor='w', + edgecolor='k', + ) # create a figure object + plt.semilogx(off_axis_angle_vec, gain_gt, "-b", label="$f = 10.7$ $GHz,$ $D = 2$ $m$") + plt.semilogx(off_axis_angle_vec, gain_gt_3, "-y", label="$f = 10.7$ $GHz,$ $D = 3$ $m$") + plt.semilogx(off_axis_angle_vec, gain_gt_18, "-g", label="$f = 10.7$ $GHz,$ $D = 1.8$ $m$") + + plt.title("ITU-R F.1245 antenna radiation pattern") + plt.xlabel(r"Off-axis angle $\phi$ [deg]") + plt.ylabel("Gain relative to $G_m$ [dB]") + plt.legend(loc="lower left") + # plt.xlim((phi[0], phi[-1])) + plt.ylim((-20, 50)) + # ax = plt.gca() + # ax.set_yticks([-30, -20, -10, 0]) + # ax.set_xticks(np.linspace(1, 9, 9).tolist() + np.linspace(10, 100, 10).tolist()) + plt.grid() + plt.show() diff --git a/sharc/antenna/antenna_factory.py b/sharc/antenna/antenna_factory.py index 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..7de9b4cc 100644 --- a/sharc/parameters/parameters_antenna.py +++ b/sharc/parameters/parameters_antenna.py @@ -29,7 +29,8 @@ class ParametersAntenna(ParametersBase): "ITU-R-S.1528-Taylor", "ITU-R-S.1528-Section1.2", "ITU-R-S.1528-LEO", - "MSS Adjacent"] + "MSS Adjacent", + "ITU-R F.1245_fs"] # chosen antenna radiation pattern pattern: typing.Literal["OMNI", @@ -44,7 +45,8 @@ class ParametersAntenna(ParametersBase): "ITU-R-S.1528-Taylor", "ITU-R-S.1528-Section1.2", "ITU-R-S.1528-LEO", - "MSS Adjacent"] = None + "MSS Adjacent", + "ITU-R F.1245_fs"] = None # antenna gain [dBi] gain: float = None @@ -94,6 +96,20 @@ class ParametersAntenna(ParametersBase): default_factory=ParametersAntennaS672, ) + @dataclass + class ParametersAntennaRF1245(ParametersBase): + gain: float = -25 + diameter: float = None + frequency: float = None + + def validate(self, ctx): + if None in [self.gain, self.diameter, self.frequency]: + raise ValueError(f"{ctx}.antenna_3_dB should be set to a number") + + itu_r_f_1245_fs: ParametersAntennaRF1245 = field( + default_factory=ParametersAntennaRF1245, + ) + def set_external_parameters(self, **kwargs): """ Set external parameters for all sub-parameters of the antenna. @@ -198,6 +214,8 @@ def validate(self, ctx): self.itu_r_s_672.validate(f"{ctx}.itu_r_s_672") case "MSS Adjacent": self.mss_adjacent.validate(f"{ctx}.mss_adjacent") + case "ITU-R F.1245_fs": + self.itu_r_f_1245_fs.validate(f"{ctx}.itu_r_f_1245_fs") case _: raise NotImplementedError( "ParametersAntenna.validate does not implement this antenna validation!", ) diff --git a/sharc/station_factory.py b/sharc/station_factory.py index 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, From 0ff32ce20d89fd5bdcea62ae53374f6401a6fcbe Mon Sep 17 00:00:00 2001 From: fabricio Date: Fri, 30 Jan 2026 10:39:40 -0300 Subject: [PATCH 2/5] fix(build): Fixed syntax errors --- sharc/parameters/parameters_antenna.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sharc/parameters/parameters_antenna.py b/sharc/parameters/parameters_antenna.py index 7de9b4cc..73711078 100644 --- a/sharc/parameters/parameters_antenna.py +++ b/sharc/parameters/parameters_antenna.py @@ -105,7 +105,7 @@ class ParametersAntennaRF1245(ParametersBase): def validate(self, ctx): if None in [self.gain, self.diameter, self.frequency]: raise ValueError(f"{ctx}.antenna_3_dB should be set to a number") - + itu_r_f_1245_fs: ParametersAntennaRF1245 = field( default_factory=ParametersAntennaRF1245, ) From 57f18445d60fa193b985e4ef87503c6bd8bb8cda Mon Sep 17 00:00:00 2001 From: fabricio Date: Fri, 30 Jan 2026 11:01:07 -0300 Subject: [PATCH 3/5] fix(build): Added Docstring --- sharc/parameters/parameters_antenna.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/sharc/parameters/parameters_antenna.py b/sharc/parameters/parameters_antenna.py index 73711078..98ebbc25 100644 --- a/sharc/parameters/parameters_antenna.py +++ b/sharc/parameters/parameters_antenna.py @@ -98,11 +98,36 @@ class ParametersAntenna(ParametersBase): @dataclass class ParametersAntennaRF1245(ParametersBase): + """ + Parameters for ITU-R F.1245 antenna model. It's commonly used + for fixed service antennas. + + Paremeters + ---------- + gain : float, default=-25 + Antenna gain in dB. + diameter : float, optional + Antenna diameter in meters. + frequency : float, optional + Operating frequency. + """ gain: float = -25 diameter: float = None frequency: float = None def validate(self, ctx): + """ + Validate the antenna parameters for correctness. + + Parameters + ---------- + ctx : str + Context string for error messages. + Raises + ------ + ValueError + If any parameter is invalid. + """ if None in [self.gain, self.diameter, self.frequency]: raise ValueError(f"{ctx}.antenna_3_dB should be set to a number") From 42b3ea5680f10603dde7e1225bc9d680486e717b Mon Sep 17 00:00:00 2001 From: Fabricio Campos Date: Wed, 11 Feb 2026 18:57:07 +0000 Subject: [PATCH 4/5] update(antenna): changed ParametersAntennaWithDiameter as the parameter class --- sharc/antenna/antenna_f1245_fs.py | 11 ++------ sharc/parameters/parameters_antenna.py | 39 ++------------------------ 2 files changed, 5 insertions(+), 45 deletions(-) diff --git a/sharc/antenna/antenna_f1245_fs.py b/sharc/antenna/antenna_f1245_fs.py index de554a19..b0999bbf 100644 --- a/sharc/antenna/antenna_f1245_fs.py +++ b/sharc/antenna/antenna_f1245_fs.py @@ -1,4 +1,3 @@ - # -*- coding: utf-8 -*- """ Created on Wed Apr 4 17:08:00 2018 @@ -6,10 +5,7 @@ """ import matplotlib.pyplot as plt from sharc.antenna.antenna import Antenna -from sharc.parameters.imt.parameters_imt import ParametersImt -from sharc.parameters.imt.parameters_imt import ParametersImt -from sharc.parameters.imt.parameters_antenna_imt import ParametersAntennaImt -from sharc.parameters.parameters_antenna import ParametersAntenna +from sharc.parameters.parameters_antenna_with_diameter import ParametersAntennaWithDiameter import numpy as np import math @@ -18,7 +14,7 @@ class Antenna_f1245_fs(Antenna): """Class that implements the ITU-R F.1245 antenna pattern for fixed satellite service earth stations.""" - def __init__(self, param: ParametersImt): + def __init__(self, param: ParametersAntennaWithDiameter): super().__init__() self.peak_gain = param.gain lmbda = 3e8 / (param.frequency * 1e6) @@ -154,9 +150,8 @@ def calculate_off_axis_angle(self, Az, b): if __name__ == '__main__': off_axis_angle_vec = np.linspace(0.1, 180, num=1001) # initialize antenna parameters - param = ParametersAntenna() + param = ParametersAntennaWithDiameter() param.frequency = 2155 - param_gt = ParametersAntennaImt() param.gain = 33.1 param.diameter = 2 antenna_gt = Antenna_f1245_fs(param) diff --git a/sharc/parameters/parameters_antenna.py b/sharc/parameters/parameters_antenna.py index 98ebbc25..57adccf1 100644 --- a/sharc/parameters/parameters_antenna.py +++ b/sharc/parameters/parameters_antenna.py @@ -96,43 +96,8 @@ class ParametersAntenna(ParametersBase): default_factory=ParametersAntennaS672, ) - @dataclass - class ParametersAntennaRF1245(ParametersBase): - """ - Parameters for ITU-R F.1245 antenna model. It's commonly used - for fixed service antennas. - - Paremeters - ---------- - gain : float, default=-25 - Antenna gain in dB. - diameter : float, optional - Antenna diameter in meters. - frequency : float, optional - Operating frequency. - """ - gain: float = -25 - diameter: float = None - frequency: float = None - - def validate(self, ctx): - """ - Validate the antenna parameters for correctness. - - Parameters - ---------- - ctx : str - Context string for error messages. - Raises - ------ - ValueError - If any parameter is invalid. - """ - if None in [self.gain, self.diameter, self.frequency]: - raise ValueError(f"{ctx}.antenna_3_dB should be set to a number") - - itu_r_f_1245_fs: ParametersAntennaRF1245 = field( - default_factory=ParametersAntennaRF1245, + itu_r_f_1245_fs: ParametersAntennaWithDiameter = field( + default_factory=ParametersAntennaWithDiameter, ) def set_external_parameters(self, **kwargs): From a4a17e21adea41280866208d2addbb8ce7bd79b9 Mon Sep 17 00:00:00 2001 From: Fabricio Campos Date: Thu, 12 Feb 2026 14:31:56 +0000 Subject: [PATCH 5/5] Fixed antenna_f1245_fs.py header --- sharc/antenna/antenna_f1245_fs.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/sharc/antenna/antenna_f1245_fs.py b/sharc/antenna/antenna_f1245_fs.py index b0999bbf..2643abb8 100644 --- a/sharc/antenna/antenna_f1245_fs.py +++ b/sharc/antenna/antenna_f1245_fs.py @@ -1,8 +1,3 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed Apr 4 17:08:00 2018 -@author: Calil -""" import matplotlib.pyplot as plt from sharc.antenna.antenna import Antenna from sharc.parameters.parameters_antenna_with_diameter import ParametersAntennaWithDiameter