diff --git a/antennas/WEST_LH1_half.toml b/antennas/WEST_LH1_half.toml new file mode 100644 index 0000000..3487fd1 --- /dev/null +++ b/antennas/WEST_LH1_half.toml @@ -0,0 +1,109 @@ +# WEST LH1 antenna description +name = "Tore Supra/WEST C3/LH1 antenna (half)" +frequency = 3.7e9 + +# global description of the modules +[global] +# Number of modules per antenna in the poloidal direction. +nb_mod_theta = 1 +# Number of modules per antenna in the toroidal direction. +nb_mod_phi = 8 +# Position index of the modules in the followinf order: +# 1) toroidal direction (from low phi to high phi, +# ie. counter-clockwise when seen from above). +# 2) poloidal direction (from low theta to high theta, +# i.e. from bottom to top if the antenna is on LFS). +idx_mod = [1, 2, 3, 4, 5, 6, 7, 8] +# Spacing between toroidally neighboring modules [m] +spacing_btw_mod_phi = 0 +# Spacing between poloidally neighboring modules [m] +spacing_btw_mod_theta = 0 + +# module description +[module] +# Number of waveguides per module in the toroidal direction. (passive and active) +nb_wg_phi = 6 +# Number of waveguides per module in the poloidal direction. (passive and active) +nb_wg_theta = 3 +# Mask of passive and active waveguides for an internal module +# 1 for active -- 0 for passive. +mask = [1, 1, 1, 1, 1, 1] +# Number of passive waveguide between modules in the toroidal direction. +nb_pwg_btw_mod_phi = 1 +# Number of passive waveguides on each antenna edge in the toroidal direction. +nb_pwg_edge = 1 +# Spacing between poloidally neighboring waveguides [m] +space_btw_wg_theta = 12e-3 +# Height of waveguides in the poloidal direction [m] +wg_size_theta = 70e-3 +# Width of active waveguides [m] +awg_size_phi = 8e-3 +# Width of internal passive waveguides [m] +pwg_size_phi = 6.5e-3 +# Width of edge passive waveguides [m] +pwg_size_edge_phi = 6.5e-3 +# Thickness between waveguides in the toroidal direction [m] +e_phi = 2e-3 # between active waveguides +# Thicness between passive waveguides +e_phi_pwg = 3e-3 +# Short circuit depth for passive waveguides in guided wavelegth +pwg_depth = [0.25] + +[sparameters] +# modules C3 +# La modelisation HFSS de l'antenne debute au voisinage du CM. +# Pour prendre en compte le dephasage entre la mesure de phase et le debut de la modelisation HFSS +# il faut calculer le dephasage necessaire : +# phase incident + phase rallonge + phase s12 HFSS = phase en bout +# soit : +# phase rallonge = dphi - phase s12 HFSS +# ou dphi est la correction (connue, cf. doc Annika) utilisee pour calculer la phase en bout +# a partir de la phase incidente. +# +# mesures relative au module 8B + +# modules BAS +# ----------- +# module correction dphi phase S12 HFSS +# 24b (= 8B) 0 166.4453 +# 23b -5.5 -160.9112 +# 22b 8 -135.2895 +# 21b 14 -122.0280 +# 14b 8.5 -116.1493 +# 13b 20 -131.2212 +# 12b 10 -139.8052 +# 11b (= 1B) 20 -166.9180 +# nom_fichiers = ['S_C3_24b';'S_C3_23b';'S_C3_22b';'S_C3_21b';'S_C3_14b';'S_C3_13b';'S_C3_12b';'S_C3_11b']; +# phase_rallonge = (pi/180)*[0-166.44; -5.5+160.9; 8+135.3; 14+122; 8.5+116.1; 20+131.2; 10+139.8; 20+166.9]; +# +# modules HAUT +# ----------- +# module correction dphi phase S12 HFSS +# 24h (= 8H) 17 168.6118 +# 23h 11 -154.7044 +# 22h 1 -129.9639 +# 21h 15 -118.7455 +# 14h 16 -113.2875 +# 13h 22 -117.8509 +# 12h 17 -132.5663 +# 11h (= 1B) 15 -164.3341 +# +# modules.Sparameters.SFileNames = ['S_C3_24h';'S_C3_23h';'S_C3_22h';'S_C3_21h';'S_C3_14h';'S_C3_13h';'S_C3_12h';'S_C3_11h']; +# phase_rallonge = (pi/180)*[17-168.6; 11+154.7; 1+130; 15+118.7; 16+113.3; 22+117.9; 17+132.6; 15+164.3]; + +filenames = ['S_C3_24b', 'S_C3_23b', 'S_C3_22b', 'S_C3_21b', 'S_C3_14b', 'S_C3_13b', 'S_C3_12b', 'S_C3_11b'] + +## Phase deembedding +# These parameters are the phase correction in order to take into account +# the transmission line length between phase measurement and S-matrix description. +# This is only usefull when using input data from experiments. +# modules.Sparameters.phase_deembedded = zeros(nma_phi,1); +# in degrees +phases_deembedded = [-166.44, 155.4, 143.3, 136, 124.6, 151.2, 149.8, 186.9] + +# Antenna default power and phase excitations +[excitation] +# Default forward power for all modules [watt] +magnitudes = [1, 1, 1, 1, 1, 1, 1, 1] +# Default phase shifts for all modules [deg] +phases = [0, 90, 180, 270, 0, 90, 180, 270] diff --git a/antennas/simple_antenna.toml b/antennas/simple_antenna.toml new file mode 100644 index 0000000..7984920 --- /dev/null +++ b/antennas/simple_antenna.toml @@ -0,0 +1,71 @@ +# 8 active Herewaveguides single row antenna +# Each waveguide is fed independantly, +# ie. each waveguide is an independant module. +name = "8 waveguides single row antenna" +frequency = 3.7e9 + +# global description of the modules +[global] +# Number of modules per antenna in the toroidal direction. (nma_phi) +nb_mod_phi = 8 +# Number of modules per antenna in the poloidal direction. (nma_theta) +nb_mod_theta = 1 +# Position index of the modules in the following order: (ima) +# 1) toroidal direction (from low phi to high phi, +# ie. counter-clockwise when seen from above). +# 2) poloidal direction (from low theta to high theta, +# i.e. from bottom to top if the antenna is on LFS). +idx_mod = [1, 2, 3, 4, 5, 6, 7, 8] +# Spacing between toroidally neighboring modules [m] (sm_phi) +spacing_btw_mod_phi = 0 +# Spacing between poloidally neighboring modules [m] (sm_theta) +spacing_btw_mod_theta = 0 + +# module description +[module] +# Number of waveguides per module in the poloidal direction. (passive and active) (nmw_theta) +nb_wg_theta = 1 +# Number of waveguides per module in the toroidal direction. (passive and active) (nmw_phi) +nb_wg_phi = 1 +# Mask of passive and active waveguides for an internal module +# 1 for active -- 0 for passive. +mask = [1] +# Number of passive waveguide between modules in the toroidal direction. (npwbm_phi) +nb_pwg_btw_mod_phi = 0 +# Number of passive waveguides on each antenna edge in the toroidal direction. (npwe_phi) +nb_pwg_edge = 1 +# Spacing between poloidally neighboring waveguides [m] (sw_theta) +space_btw_wg_theta = 12e-3 +# Height of waveguides in the poloidal direction [m] (hw_theta) +wg_size_theta = 70e-3 +# Width of active waveguides [m] (bwa) +# if scalar, same thickness for all waveguides +awg_size_phi = 10e-3 +# Width of internal passive waveguides [m] (biwp) +# if scalar, same thickness for all waveguides +pwg_size_phi = 8e-3 +# Width of edge passive waveguides [m] (bewp) +# if scalar, same thickness for all waveguides +pwg_size_edge_phi = 8e-3 +# Thickness between waveguides in the toroidal direction [m] +# if scalar, same thickness for all waveguides +e_phi = 2e-3 +# Thickness between passive waveguides +e_phi_pwg = 3e-3 +# Short circuit depth for passive waveguides in guided wavelegth +# if scalar, same value for all passive waveguides (scl) +pwg_depth = 0.25 + +# S-parameters description +[sparameters] +# S-parameters file for each module +filenames = ["S_elem", "S_elem", "S_elem", "S_elem", "S_elem", "S_elem", "S_elem", "S_elem"] +# Phase deembedding for each module inputs +phases_deembedded = [0, 0, 0, 0, 0, 0, 0, 0] + +# Antenna default power and phase excitations +[excitation] +# Default forward power for all modules [watt] +magnitudes = [1, 1, 1, 1, 1, 1, 1, 1] +# Default phase shifts for all modules [deg] +phases = [0, 90, 180, 270, 0, 90, 180, 270] diff --git a/prek.toml b/prek.toml index bb38f74..f7d50cb 100644 --- a/prek.toml +++ b/prek.toml @@ -1,4 +1,20 @@ [[repos]] repo = "https://github.com/pre-commit/pre-commit-hooks" rev = "v6.0.0" -hooks = [{ id = "check-yaml" }, { id = "end-of-file-fixer" }] +hooks = [ + { id = "trailing-whitespace" }, + { id = "check-yaml" }, + { id = "check-toml"}, + { id = "end-of-file-fixer" } +] + +[[repos]] +repo = "https://github.com/astral-sh/ruff-pre-commit" +rev = "v0.15.0" # Ruff version. +hooks = [ + # Run the linter. + { id = "ruff-check", args = ["--fix"], types_or = ["python", "pyi"] }, + + # Run the formatter. + { id = "ruff-format", types_or = ["python", "pyi"] }, +] diff --git a/pyproject.toml b/pyproject.toml index 5c2be83..3bceab7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ build-backend = "uv_build" [dependency-groups] dev = [ + "black>=26.3.1", "prek>=0.3.6", "ruff>=0.15.7", { include-group = "docs" }, @@ -67,4 +68,5 @@ line-ending = "auto" testpaths = [ "src", "doc" ] -addopts = "--cov=aloha --ignore-glob='*.ipynb_checkpoints'" +# warning: coverage makes PyCharm not stop at breakpoints +addopts = "--ignore-glob='*.ipynb_checkpoints'" diff --git a/src/aloha/__init__.py b/src/aloha/__init__.py index ce0cad8..816b9f8 100644 --- a/src/aloha/__init__.py +++ b/src/aloha/__init__.py @@ -1,6 +1,7 @@ -from . import constants, waveguide +from . import antenna, constants, waveguide __all__ = [ "constants", "waveguide", + "antenna" ] diff --git a/src/aloha/antenna.py b/src/aloha/antenna.py new file mode 100644 index 0000000..ef5370f --- /dev/null +++ b/src/aloha/antenna.py @@ -0,0 +1,304 @@ +import os +import tomllib + +import matplotlib.pyplot as plt +import numpy as np + +from .constants import pi +from .waveguide import Waveguide + + +class Antenna: + def __init__(self, source: str | os.PathLike | dict | None = None) -> None: + """ + ALOHA antenna description. + + Parameters + ---------- + source : str, Path, dict, or None + Either a path to a TOML ALOHA antenna file, + a dictionary containing antenna parameters, + or None to create an empty antenna. + """ + self.antenna = {} + if source is None: + return + elif isinstance(source, (str, os.PathLike)): + self.antenna = self.load(source) + elif isinstance(source, dict): + self.antenna = source + else: + raise TypeError("source must be a string, PathLike, dictionary, or None") + + @classmethod + def load(cls, filename: str | os.PathLike) -> dict: + """ + Load antenna parameters from a TOML file. + + Parameters + ---------- + filename : str or Path + Path to a TOML file containing antenna parameters. + + Returns + ------- + dict + Dictionary containing antenna parameters. + """ + with open(filename, "rb") as fp: + antenna_data = cls.validate_description(tomllib.load(fp)) + return antenna_data + + @classmethod + def from_dict(cls, antenna_dict: dict) -> "Antenna": + """ + Create an Antenna instance from a dictionary. + + Parameters + ---------- + antenna_dict : dict + Dictionary containing antenna parameters. + + Returns + ------- + Antenna + An instance of the Antenna class. + """ + _ant = cls() + _ant.antenna = cls.validate_description(antenna_dict) + return _ant + + @classmethod + def from_file(cls, filename: str | os.PathLike) -> "Antenna": + """ + Create an Antenna instance from a TOML file. + + Parameters + ---------- + filename : str or Path + Path to a TOML file containing antenna parameters. + + Returns + ------- + Antenna + An instance of the Antenna class. + """ + antenna_data = cls.load(filename) + return cls.from_dict(antenna_data) + + def __str__(self): + return f"Antenna(name={self.antenna['name']}, frequency={self.antenna['frequency']})" + + @classmethod + def validate_description(cls, antenna: dict) -> dict: + """ + Check that the antenna description is valid. + + Parameters + ---------- + antenna : dict + ALOHA antenna description dictionary. + + Returns + ------- + antenna : dict + Verified antenna description + """ + if not antenna: # empty dict is OK + return antenna + + total_nb_modules = antenna["global"]["nb_mod_phi"] * antenna["global"]["nb_mod_theta"] + # excitations + if len(antenna["excitation"]["magnitudes"]) != total_nb_modules: + raise ValueError( + f"Number of excitation magnitudes ({len(antenna['excitation']['magnitudes'])}) " + f"does not match total number of modules ({total_nb_modules})" + ) + if len(antenna["excitation"]["phases"]) != total_nb_modules: + raise ValueError( + f"Number of excitation phases ({len(antenna['excitation']['phases'])}) " + f"does not match total number of modules ({total_nb_modules})" + ) + + # module indices + if len(antenna["global"]["idx_mod"]) != total_nb_modules: + raise ValueError( + f"Number of module indices ({len(antenna['global']['idx_mod'])}) " + f"does not match total number of modules ({total_nb_modules})" + ) + + # S-parameter + if len(antenna["sparameters"]["filenames"]) != total_nb_modules: + raise ValueError( + f"Number of S-parameter filenames ({len(antenna['sparameters']['filenames'])}) " + f"does not match total number of modules ({total_nb_modules})" + ) + if len(antenna["sparameters"]["phases_deembedded"]) != total_nb_modules: + raise ValueError( + f"Number of deembedded phases ({len(antenna['sparameters']['phases_deembedded'])}) " + f"does not match total number of modules ({total_nb_modules})" + ) + # so far so good + return antenna + + def is_valid(self) -> bool: + """ + Check if the antenna description is valid. + + Returns + ------- + state : bool + True is the antenna description looks valid. + """ + try: + self.validate_description(self.antenna) + return True + except ValueError: + return False + + def plot(self): + """ + Plot the antenna architecture. + """ + if not self.is_valid(): + raise ValueError("Invalid antenna description") + + b, a, z, y, nwr, nwas, act_module_tor = self.antenna_coordinates() + # Extract parameters from antenna dictionary + antenna = self.antenna + plt.figure() + # TODO : plot all poloidal modules if more than one + for idx_pol in range(len(y)): + for idx_tor in range(len(z)): + rect_pos = [z[idx_tor], y[idx_pol], b[idx_tor], a[idx_pol]] + # Create passive/active mask + ar_modules = np.ones(antenna["global"]["nb_mod_phi"]) + ar_pa_mask = np.array(antenna["module"]["mask"]) + + # Add passive waveguides between modules + ar_pa_mask = np.concatenate([ar_pa_mask, np.zeros(antenna["module"]["nb_pwg_btw_mod_phi"])]) + + pa_mask = np.kron(ar_modules, ar_pa_mask) + + # Remove last element if there are passive waveguides between modules + if antenna["module"]["nb_pwg_btw_mod_phi"] > 0: + pa_mask = pa_mask[:-1] + + # Add passive waveguides at edges + pa_mask = np.concatenate( + [np.zeros(antenna["module"]["nb_pwg_edge"]), pa_mask, np.zeros(antenna["module"]["nb_pwg_edge"])] + ) + + if pa_mask[idx_tor] == 0: # passive wg + plt.gca().add_patch( + plt.Rectangle( + (rect_pos[0], rect_pos[1]), + rect_pos[2], + rect_pos[3], + facecolor=[0.8, 0.8, 0.8], + edgecolor="k", + ) + ) + elif pa_mask[idx_tor] == 1: # active wg + plt.gca().add_patch( + plt.Rectangle( + (rect_pos[0], rect_pos[1]), rect_pos[2], rect_pos[3], fill=False, facecolor=[0.8, 0.8, 0.8] + ) + ) + + plt.axis("equal") + plt.xlabel("z [m]") + plt.ylabel("y [m]") + plt.title(f"ALOHA antenna: {antenna['name']}\n (view from the plasma)") + plt.show() + + def antenna_coordinates(self): + """ + Generate waveguide positions and dimensions from the antenna description. + + Returns + ------- + b : list + Waveguide widths (in toroidal direction). + a : list + Waveguide heights (in poloidal direction). + z : list + Waveguide toroidal positions. + y : list + Waveguide poloidal positions. + nb_wg_per_row : int + Number of waveguides per row. + nb_wg_total : int + Total number of waveguides per antenna. + act_module_tor : list + Mask for passive/active waveguides. + + """ + antenna = self.antenna + mod = antenna["global"] + wg = antenna["module"] + + # (total) number of waveguides per row + # = (nb wg in a module) + 2*(nb ext wg) + (nb of wg between modules) + nb_wg_per_row = ( + mod["nb_mod_phi"] * wg["nb_wg_phi"] + + 2 * wg["nb_pwg_edge"] + + (mod["nb_mod_phi"] - 1) * wg["nb_pwg_btw_mod_phi"] + ) + + # (total) number of waveguides per column + nb_wg_per_col = mod["nb_mod_theta"] * wg["nb_wg_theta"] + + # total number of waveguides + nb_wg_total = nb_wg_per_row * nb_wg_per_col + + # waveguide height - supposed constant for all the waveguides of the antenna + a = np.full(nb_wg_total, wg["wg_size_theta"]) + + # b + # Make the array b which contains all the waveguide width of a row of waveguides + b_module = np.where(wg["mask"], wg["awg_size_phi"], wg["pwg_size_phi"]) + b_edge = np.full(wg["nb_pwg_edge"], wg["pwg_size_edge_phi"]) + b_inter = np.full(wg["nb_pwg_btw_mod_phi"], wg["pwg_size_phi"]) + + b = np.concatenate( + [b_edge, np.tile(np.concatenate([b_module, b_inter]), mod["nb_mod_phi"] - 1), b_module, b_edge] + ) + + # e + # Make the array e which contains all the waveguide septum width of a row of waveguides + ne_phi = ( + wg["nb_pwg_btw_mod_phi"] * (mod["nb_mod_phi"] - 1) + + wg["nb_pwg_edge"] * 2 + + mod["nb_mod_phi"] * wg["nb_wg_phi"] + - 1 + ) + e = np.tile(wg["pwg_size_phi"], ne_phi) + + # z + # Make the array z which contains all the waveguide positions in the toroidal direction + z = np.zeros(nb_wg_per_row) + for ind in range(1, nb_wg_per_row): + z[ind] = z[ind - 1] + b[ind - 1] + e[ind - 1] + + # y + # Make the array y which contains all the waveguide positions in the poloidal direction + h = np.concatenate( + [ + np.tile( + np.concatenate( + [np.full(wg["nb_wg_theta"], wg["awg_size_phi"]), np.full(1, mod["spacing_btw_mod_phi"])] + ), + mod["nb_mod_theta"] - 1, + ), + np.full(wg["nb_wg_theta"], wg["awg_size_phi"]), + ] + ) + y = np.zeros(nb_wg_per_col) + for ind in range(1, nb_wg_per_col): + y[ind] = y[ind - 1] + h[ind - 1] + a[ind] + + # index of active waveguides in a module + act_module_tor = wg["mask"] + + return b, a, z, y, nb_wg_per_row, nb_wg_total, act_module_tor diff --git a/src/aloha/constants.py b/src/aloha/constants.py index c70bf4b..eef726b 100644 --- a/src/aloha/constants.py +++ b/src/aloha/constants.py @@ -1,5 +1,6 @@ import numpy as np from scipy.constants import c, e, epsilon_0, m_e, mu_0, pi +# Vacuum impedance and admittance Z0 = np.sqrt(mu_0 / epsilon_0) -Y0 = 1 / (Z0) +Y0 = 1 / Z0 diff --git a/src/aloha/waveguide.py b/src/aloha/waveguide.py index 3832dbe..f68f746 100644 --- a/src/aloha/waveguide.py +++ b/src/aloha/waveguide.py @@ -4,29 +4,88 @@ class Waveguide: - """ - Rectangular Waveguide. - """ + def __init__(self, a: float, b: float, L: float = 0, active: bool = True) -> None: + """ + Rectangular waveguide. + + Parameters + ---------- + a : float + Length of the large side in meter. + b : float + Length of the small side in meter. + L : float, optional + Guide (longitudinal) length in meter. Default is 0. + active : boolean, optional + True if active (directly fed waveguide), False if passive (short-circuited). + Default is True. - def __init__(self, a: float, b: float): + """ self.a = a self.b = b + self.L = L + self.active = active def cutoff_wavenumber(self, m: int = 1, n: int = 0) -> float: """ Cutoff wavenumber for TE_mn or TM_mn mode. + + Parameters + ---------- + f : float + Frequency of the wave in Hz. + m : int + Large side mode index. Default is 1. + n : int + Small side mode index. Default is 0. + + Results + ------- + k_c : float + Cutoff wavenumber in 1/m. + """ return np.sqrt((m * pi / self.a) ** 2 + (n * pi / self.b) ** 2) def cutoff_frequency(self, m: int = 1, n: int = 0) -> float: """ Cutoff frequency for TE_mn or TM_mn mode. + + Parameters + ---------- + f : float + Frequency of the wave in Hz. + m : int + Large side mode index. Default is 1. + n : int + Small side mode index. Default is 0. + + Results + ------- + f_c : float + Cutoff frequency in Hz. + """ return c / (2 * pi) * self.cutoff_wavenumber(m=m, n=n) def cutoff_wavelength(self, m: int = 1, n: int = 0) -> float: """ Cutoff wavelength for TE_mn or TM_mn mode. + + Parameters + ---------- + f : float + Frequency of the wave in Hz. + m : int + Large side mode index. Default is 1. + n : int + Small side mode index. Default is 0. + + Results + ------- + lambda_c : float + Cutoff wavelength in m. + """ return c / self.cutoff_frequency(m=m, n=n) @@ -50,6 +109,20 @@ def guided_wavenumber(self, f: float, m: int = 1, n: int = 0) -> complex: * IMAGINARY for propagating modes * REAL for non-propagating modes + Parameters + ---------- + f : float + Frequency of the wave in Hz. + m : int + Large side mode index. Default is 1. + n : int + Small side mode index. Default is 0. + + Results + ------- + gamma_mn : complex + Complex wavenumber in 1/m. + """ k0 = 2 * pi * f / c kc = self.cutoff_wavenumber(m=m, n=n) @@ -63,6 +136,21 @@ def guided_wavenumber(self, f: float, m: int = 1, n: int = 0) -> complex: def guided_wavelength(self, f: float, m: int = 1, n: int = 0) -> complex: """ Guided wavelength for TE_mn or TM_mn mode. + + Parameters + ---------- + f : float + Frequency of the wave in Hz. + m : int + Large side mode index. Default is 1. + n : int + Small side mode index. Default is 0. + + Results + ------- + lambda_g : complex + Guided wavelength in meter. + """ return 2 * pi / self.guided_wavenumber(f, m, n).imag @@ -73,6 +161,20 @@ def phase_velocity(self, f: float, m: int = 1, n: int = 0) -> complex: .. math:: j \cdot \omega / \gamma + Parameters + ---------- + f : float + Frequency of the wave in Hz. + m : int + Large side mode index. Default is 1. + n : int + Small side mode index. Default is 0. + + Results + ------- + v_phi : complex + Phase velocity in m/s. + Notes ----- The `j` is used so that real phase velocity corresponds to propagation @@ -82,19 +184,26 @@ def phase_velocity(self, f: float, m: int = 1, n: int = 0) -> complex: * :math:`\omega` is angular frequency (rad/s), * :math:`\gamma` is complex propagation constant (rad/m) - Returns - ------- - v_p : :class:`numpy.ndarray` - """ return 1j * 2 * pi * f / self.guided_wavenumber(f, m, n) - def characteristic_impedance(self, f: float, m: int = 1, n: int = 1, mode: str = "te") -> complex: + def characteristic_impedance(self, f: float, m: int = 1, n: int = 0, mode: str = "te") -> complex: r""" The characteristic impedance, :math:`z_{0,mn}`. The characteristic impedance depends of the mode ('TE' or 'TM'). + Parameters + ---------- + f : float + Frequency of the wave in Hz. + m : int + Large side mode index. Default is 1. + n : int + Small side mode index. Default is 0. + mode : str + Electromagnetic mode. 'te' (default) or 'tm' + Returns ------- z0_characteristic : np.ndarray @@ -109,14 +218,60 @@ def characteristic_impedance(self, f: float, m: int = 1, n: int = 1, mode: str = } return impedance_dict[mode] - def characteristic_admittance(self, f: float, m: int = 1, n: int = 1, mode: str = "te") -> complex: + def characteristic_admittance(self, f: float, m: int = 1, n: int = 0, mode: str = "te") -> complex: r"""The characteristic admittance, :math:`y_{0,mn}`. The characteristic admittance depends of the mode ('TE' or 'TM'). + Parameters + ---------- + f : float + Frequency of the wave in Hz. + m : int + Large side mode index. Default is 1. + n : int + Small side mode index. Default is 0. + mode : str + Electromagnetic mode. 'te' (default) or 'tm' + Returns ------- y0_characteristic : np.ndarray Characteristic Impedance in units of ohms """ return 1 / self.characteristic_impedance(f, m, n, mode) + + def electric_length(self, f: float, m: int = 1, n: int = 0, L: float | None = None) -> complex: + r""" + Electric length of the waveguide for the TE or TM mode mn. + + .. math:: + L_e = \gamma_{mn} L + + + Expressing the physical length in terms of the phase shift + that the wave experiences as it propagates down the line. + + The convention has been chosen that forward propagation is + represented by the positive imaginary part of the value + returned by the gamma function. + + Parameters + ---------- + f : float + Frequency of the wave in Hz. + m : int, optional + Large side mode index. Default is 1. + n : int, optional + Small side mode index. Default is 0. + L : float, optional + Longitudinal length of the guide. Default is the object parameter. + + Results + ------- + L_e : complex + Electrical length in meter. + + """ + _L = L if L else self.L + return self.guided_wavenumber(f, m, n) * _L diff --git a/src/tests/test_antenna.py b/src/tests/test_antenna.py new file mode 100644 index 0000000..2529e42 --- /dev/null +++ b/src/tests/test_antenna.py @@ -0,0 +1,35 @@ +# Antenna test +import unittest +from glob import glob +from pathlib import Path + +from aloha.antenna import Antenna + +ANTENNAS_DIR = Path(__file__).resolve().parent.parent.parent / "antennas" + + +class TestAntenna(unittest.TestCase): + def test_antenna_constructor(self): + filename = ANTENNAS_DIR / "simple_antenna.toml" + # from a file + Antenna(filename) + Antenna.from_file(filename) + # from a dict + ant_dict = Antenna.load(filename) + Antenna(ant_dict) + Antenna.from_dict(ant_dict) + + def test_validate_antenna_files(self): + """ + Validation of the TOML schema of all the pre-defined antennas. + """ + # Check if some fields are in the antenna description + # Some logic (number of modules, etc) is tested when creating an Antenna object. + values = ["name", "frequency", "global", "module", "sparameters"] + + antenna_filenames = glob("*.toml", root_dir=ANTENNAS_DIR) + for antenna_filename in antenna_filenames: + ant = Antenna(ANTENNAS_DIR / antenna_filename) + # test if the expected elements are defined + for value in values: + assert value in ant.antenna diff --git a/src/tests/test_waveguide.py b/src/tests/test_waveguide.py index dcc347d..d02d5a8 100644 --- a/src/tests/test_waveguide.py +++ b/src/tests/test_waveguide.py @@ -1,17 +1,19 @@ """ Testing for the Waveguide class """ + import unittest import numpy as np +from scipy.signal import gammatone from skrf import Frequency from skrf.media import RectangularWaveguide +from aloha.constants import Z0, c, pi from aloha.waveguide import Waveguide class WaveguideTests(unittest.TestCase): - def test_waveguide_constructor(self): wg = Waveguide(a=1e-6, b=0.5e-6) assert wg.a == 1e-6 @@ -21,23 +23,51 @@ def test_waveguide_properties(self): "Test various waveguide properties for a few modes" a, b = 76e-3, 14e-3 f0 = 3.7e9 - wg = Waveguide(a, b) + L = 50e-3 + wg = Waveguide(a, b, L=L) # scikit-rf reference - freq = Frequency(f0, f0, npoints=1, unit='Hz') + freq = Frequency(f0, f0, npoints=1, unit="Hz") for m in range(1, 3): for n in range(0, 3): - for mode in ('te', 'tm'): + for mode in ("te", "tm"): # warning is not logged below # to hide the warnings which appears 1/0 (evanescent modes) - with np.errstate(divide='ignore'): + with np.errstate(divide="ignore"): wg_skrf = RectangularWaveguide(freq, a=a, b=b, m=m, n=n, mode_type=mode) # cut-off properties - assert np.isclose(wg.cutoff_wavenumber(m,n), wg_skrf.kc) - assert np.isclose(wg.cutoff_frequency(m,n), wg_skrf.f_cutoff) - assert np.isclose(wg.cutoff_wavelength(m,n), wg_skrf.lambda_cutoff) + assert np.isclose(wg.cutoff_wavenumber(m, n), wg_skrf.kc) + assert np.isclose(wg.cutoff_frequency(m, n), wg_skrf.f_cutoff) + assert np.isclose(wg.cutoff_wavelength(m, n), wg_skrf.lambda_cutoff) # guided properties - assert np.isclose(wg.guided_wavenumber(f0,m,n), wg_skrf.gamma) - assert np.isclose(wg.guided_wavelength(f0,m,n), wg_skrf.lambda_guide) - assert np.isclose(wg.phase_velocity(f0,m,n), wg_skrf.v_p) - assert np.isclose(wg.characteristic_impedance(f0,m,n,mode), wg_skrf.z0_characteristic) - assert np.isclose(wg.characteristic_admittance(f0, m, n, mode), 1/wg_skrf.z0_characteristic) + assert np.isclose(wg.guided_wavenumber(f0, m, n), wg_skrf.gamma) + assert np.isclose(wg.guided_wavelength(f0, m, n), wg_skrf.lambda_guide) + assert np.isclose(wg.phase_velocity(f0, m, n), wg_skrf.v_p) + assert np.isclose(wg.characteristic_impedance(f0, m, n, mode), wg_skrf.z0_characteristic) + assert np.isclose(wg.characteristic_admittance(f0, m, n, mode), 1 / wg_skrf.z0_characteristic) + assert np.isclose(wg.electric_length(f0, m, n), wg_skrf.electrical_length(L)) + # specify directly L + assert np.isclose(wg.electric_length(f0, m, n, L=2 * L), wg_skrf.electrical_length(2 * L)) + + def test_characteristic_admittance(self): + """ + Test characteristic admittances for TE and TM modes. + + Formulas from ALOHA doc: + Y_c_mn = gamma_mn / (j k_0 Z_0) for TE modes + Y_c_mn = j k_0 / (gamma_mn Z_0) fpr TM modes + """ + a, b = 76e-3, 14e-3 + f0 = 3.7e9 + wg = Waveguide(a, b) + k0 = 2 * pi * f0 / c + + for m in range(1, 3): + for n in range(0, 3): + gamma_mn = wg.guided_wavenumber(f0, m, n) + for mode in ("te", "tm"): + if mode == "te": + Y_c_mn = gamma_mn / (1j * k0 * Z0) + else: + Y_c_mn = 1j * k0 / (gamma_mn * Z0) + + assert np.isclose(wg.characteristic_admittance(f0, m, n, mode), Y_c_mn)