From d1002e37e7bd7d45a2718027b9c79e3100e137ad Mon Sep 17 00:00:00 2001 From: Julien Hillairet Date: Sat, 21 Mar 2026 19:28:45 +0100 Subject: [PATCH 1/9] update tests --- prek.toml | 7 ++++++- src/tests/test_waveguide.py | 27 ++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/prek.toml b/prek.toml index bb38f74..1a7d8a2 100644 --- a/prek.toml +++ b/prek.toml @@ -1,4 +1,9 @@ [[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" } +] diff --git a/src/tests/test_waveguide.py b/src/tests/test_waveguide.py index dcc347d..b527576 100644 --- a/src/tests/test_waveguide.py +++ b/src/tests/test_waveguide.py @@ -4,11 +4,12 @@ import unittest import numpy as np +from scipy.signal import gammatone from skrf import Frequency from skrf.media import RectangularWaveguide from aloha.waveguide import Waveguide - +from aloha.constants import c, pi, Z0 class WaveguideTests(unittest.TestCase): @@ -41,3 +42,27 @@ def test_waveguide_properties(self): 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) + + 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) From 1f4565b135119ba28d7a8e139ae14f68243e535f Mon Sep 17 00:00:00 2001 From: Julien Hillairet Date: Sat, 21 Mar 2026 20:29:39 +0100 Subject: [PATCH 2/9] Added firsts draft of the antenna class --- antennas/WEST_LH1.toml | 147 +++++++++++++++++++++++++++++++++++ antennas/simple_antenna.toml | 44 +++++++++++ src/aloha/__init__.py | 3 +- src/aloha/antenna.py | 41 ++++++++++ src/tests/test_antenna.py | 25 ++++++ 5 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 antennas/WEST_LH1.toml create mode 100644 antennas/simple_antenna.toml create mode 100644 src/aloha/antenna.py create mode 100644 src/tests/test_antenna.py diff --git a/antennas/WEST_LH1.toml b/antennas/WEST_LH1.toml new file mode 100644 index 0000000..5bd876c --- /dev/null +++ b/antennas/WEST_LH1.toml @@ -0,0 +1,147 @@ +# WEST LH1 antenna description +name = "Tore Supra/WEST C3/LH1 antenna (half)" +frequency = 3.7e9 + +[modules] +# Number of modules per antenna in the poloidal direction. +nma_theta = 1 +# Number of modules per antenna in the toroidal direction. +nma_phi = 8 + +# Position index of the module in the poloidal direction (from low theta to high theta, +# i.e. from bottom to top if the antenna is on LFS). +ima_theta = [1, 1, 1, 1, 1, 1, 1, 1] + +# Position index of the module in the toroidal direction (from low phi to high phi, +# ie. counter-clockwise when seen from above). +# numbering in ALOHA goes from left to right as view from the plasma. +ima_phi = [1, 1, 1, 1, 1, 1, 1, 1, 1] + +# Spacing between poloidally neighboring modules [m] +sm_theta = 0 + +[waveguides] +# Number of waveguides per module in the poloidal direction. (passive and active) +nwm_theta = 3 + +# Number of waveguides per module in the toroidal direction. (passive and active) +nwm_phi = 6 + +# 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. +npwbm_phi = 1 + +# Number of passive waveguides on each antenna edge in the toroidal direction. +npwe_phi = 1 + +# Spacing between poloidally neighboring waveguides [m] +sw_theta = 12e-3 + +# Height of waveguides in the poloidal direction [m] +hw_theta = 70e-3 + +# Width of active waveguides [m] +bwa = 8e-3 + +# Width of internal passive waveguides [m] +biwp = 6.5e-3 + +# Width of edge passive waveguides [m] +bewp = 6.5e-3 + +# Thickness between waveguides in the toroidal direction [m] +# Reminder : length(e_phi) = nma_phi*nwm_phi + (nma_phi - 1)*npwbm_phi + 2*npwe_phi - 1 +e = 2e-3 # between active waveguides +e_pwg = 3e-3 # between passive waveguides + +#e_aw = [e, e, e, e, e] +#e_mod = [e_pwg e_aw e_pwg]; +#waveguides.e_phi = repmat(e_mod, 1, 8); + +# Short circuit length for passive waveguides [m] +# Reminder : length(scl) = nma_phi*npwm_phi + (nma_phi - 1)*npwbm_phi + 2*npwe_phi +#nscl = waveguides.npwbm_phi*(modules.nma_phi-1) + ... +# waveguides.npwe_phi*2 + ... +# sum(not(waveguides.mask))*modules.nma_phi +#waveguides.scl = repmat(1/4, 1, nscl) + +[sparameters] +# Modules Scattering parameters +# matrice S des modules ds des fichiers .m (NB : la matrice est rangee sur une seule colonne) +sparams_pathFrom = "." +sparams_pathTo = "S_HFSS/matrices_HFSS_C3" + +# 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]; + +sparams_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 +#sparams_phase_deembedded = [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] + +## -------------------------------- +## Other antenna_lh CPO parameters +## -------------------------------- +## Not defined here in ALOHA +# Plasma edge characteristics in front of the antenna. +antenna_lh.plasmaedge = [] + +# Amplitude of the TE10 mode injected in the module [W], Matrix (nantenna_lh,max_nmodules). Time-dependent +#modules.amplitude = sqrt(4.0320e6/16)*ones(modules.nma_phi,1) + +# Phase of the TE10 mode injected in the module [rd], Matrix (nantenna_lh, max_nmodules). Time-dependent +# in degrees +#modules.phase = -90*(0:modules.nma_phi-1) + +## Not used at all in ALOHA - +# Reference global antenna position. Vectors (nantenna_lh). Time-dependent +antenna_lh.position = [] +# Beam characteristics +antenna_lh.beam = [] diff --git a/antennas/simple_antenna.toml b/antennas/simple_antenna.toml new file mode 100644 index 0000000..13a3ecd --- /dev/null +++ b/antennas/simple_antenna.toml @@ -0,0 +1,44 @@ +# 8 waveguides single row antenna +name = "8 waveguides single row antenna" +frequency = 3.7e9 + +[modules] +# nbre de lignes de guides poloidales +nb_g_pol = 1 + +# mettre autant de modules que de guides +# nbre de modules sur une ligne poloidale +nb_modules_tor = 8 + +# nbre de guides par module dans le sens poloidal +nb_g_module_pol = 1 + +# nbre de guides par module ds le sens toroidal +nb_g_module_tor = 1 +pass_module_tor = [] + +# nbre de guides passifs entre les modules +nb_g_passifs_inter_modules = 0 +# nbre de guides passifs sur chaque bord +nb_g_passifs_bord = 0 + +[waveguides] +# espacement entre les grills juxtaposes poloidalement +espacement_g_pol = 12e-3 +# hauteur des guides ds le sens poloidal +a = 70e-3 + +#antenne_standard = 1; # = 0 il faut decrire l'antenne sur une ligne : tableaux b et e +# # = 1 parametres scalaires : b_g_actif, b_g_pass et e + +# largeur des guides actifs +b_g_actif = 6.5e-3 +# largeur des guides passifs +b_g_pass = nan +# epaisseur des parois des guides dans le sens toroidal +e = 1e-3 +# longueur du court-circuit (en lambda guidee); +lcc = 0.25 + +[sparameters] +# matrice S des modules ds des fichiers .m (NB : la matrice est rangee sur une seule colonne) 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..d1305e2 --- /dev/null +++ b/src/aloha/antenna.py @@ -0,0 +1,41 @@ +import numpy as np +import tomllib +import os +from .constants import pi +from .waveguide import Waveguide + +class Antenna(object): + """ + ALOHA antenna description. + """ + def __init__(self, filename: str | os.PathLike = None): + """ + ALOHA antenna description. + + Parameters + ---------- + filename : str | os.PathLike + Path to a TOML ALOHA antenna file. + + """ + if filename: + self.antenna = self.load(filename) + + @classmethod + def load(cls, filename: str | os.PathLike) -> dict: + """ + Read an ALOHA antenna description (TOML file format). + + Parameters + ---------- + filename : str + path to a TOML file. + + Returns + ------- + antenna : dict + ALOHA antenna description. + + """ + with open(filename, "rb") as fp: + return tomllib.load(fp) diff --git a/src/tests/test_antenna.py b/src/tests/test_antenna.py new file mode 100644 index 0000000..ec8fceb --- /dev/null +++ b/src/tests/test_antenna.py @@ -0,0 +1,25 @@ +# 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" + ant = Antenna(filename) + + def test_validate_antenna_files(self): + """ + Validation of the TOML schema of all the pre-defined antennas. + """ + values = ["name", "frequency", "modules", "waveguides", "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 From 9b328d14cc01e3333464d1e60ae1b9d6c90e3a6c Mon Sep 17 00:00:00 2001 From: Julien Hillairet Date: Sat, 21 Mar 2026 20:45:05 +0100 Subject: [PATCH 3/9] fix linting issues --- prek.toml | 11 ++++++++++ src/aloha/antenna.py | 10 ++++++--- src/tests/test_antenna.py | 4 +++- src/tests/test_waveguide.py | 43 +++++++++++++++++++------------------ 4 files changed, 43 insertions(+), 25 deletions(-) diff --git a/prek.toml b/prek.toml index 1a7d8a2..f7d50cb 100644 --- a/prek.toml +++ b/prek.toml @@ -7,3 +7,14 @@ hooks = [ { 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/src/aloha/antenna.py b/src/aloha/antenna.py index d1305e2..b3c86e7 100644 --- a/src/aloha/antenna.py +++ b/src/aloha/antenna.py @@ -1,13 +1,17 @@ -import numpy as np -import tomllib import os +import tomllib + +import numpy as np + from .constants import pi from .waveguide import Waveguide -class Antenna(object): + +class Antenna: """ ALOHA antenna description. """ + def __init__(self, filename: str | os.PathLike = None): """ ALOHA antenna description. diff --git a/src/tests/test_antenna.py b/src/tests/test_antenna.py index ec8fceb..ef869dc 100644 --- a/src/tests/test_antenna.py +++ b/src/tests/test_antenna.py @@ -2,14 +2,16 @@ 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" - ant = Antenna(filename) + Antenna(filename) def test_validate_antenna_files(self): """ diff --git a/src/tests/test_waveguide.py b/src/tests/test_waveguide.py index b527576..7b4eeeb 100644 --- a/src/tests/test_waveguide.py +++ b/src/tests/test_waveguide.py @@ -1,6 +1,7 @@ """ Testing for the Waveguide class """ + import unittest import numpy as np @@ -8,11 +9,11 @@ from skrf import Frequency from skrf.media import RectangularWaveguide +from aloha.constants import Z0, c, pi from aloha.waveguide import Waveguide -from aloha.constants import c, pi, Z0 -class WaveguideTests(unittest.TestCase): +class WaveguideTests(unittest.TestCase): def test_waveguide_constructor(self): wg = Waveguide(a=1e-6, b=0.5e-6) assert wg.a == 1e-6 @@ -24,24 +25,24 @@ def test_waveguide_properties(self): f0 = 3.7e9 wg = Waveguide(a, b) # 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) def test_characteristic_admittance(self): """ @@ -54,15 +55,15 @@ def test_characteristic_admittance(self): a, b = 76e-3, 14e-3 f0 = 3.7e9 wg = Waveguide(a, b) - k0 = 2*pi*f0/c + k0 = 2 * pi * f0 / c - for m in range(1,3): - for n in range(0,3): + 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) + 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) + Y_c_mn = 1j * k0 / (gamma_mn * Z0) - assert np.isclose(wg.characteristic_admittance(f0,m,n,mode), Y_c_mn) + assert np.isclose(wg.characteristic_admittance(f0, m, n, mode), Y_c_mn) From 59885cc828c20f13d728f1f7324e211250bf0c06 Mon Sep 17 00:00:00 2001 From: Julien Hillairet Date: Sat, 21 Mar 2026 20:54:05 +0100 Subject: [PATCH 4/9] __init__() return type marked as None as specified in PEP-0484 --- src/aloha/antenna.py | 2 +- src/aloha/waveguide.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/aloha/antenna.py b/src/aloha/antenna.py index b3c86e7..bce057f 100644 --- a/src/aloha/antenna.py +++ b/src/aloha/antenna.py @@ -12,7 +12,7 @@ class Antenna: ALOHA antenna description. """ - def __init__(self, filename: str | os.PathLike = None): + def __init__(self, filename: str | os.PathLike = None) -> None: """ ALOHA antenna description. diff --git a/src/aloha/waveguide.py b/src/aloha/waveguide.py index 3832dbe..3dcc233 100644 --- a/src/aloha/waveguide.py +++ b/src/aloha/waveguide.py @@ -8,7 +8,7 @@ class Waveguide: Rectangular Waveguide. """ - def __init__(self, a: float, b: float): + def __init__(self, a: float, b: float) -> None: self.a = a self.b = b From 45d4ee413cccf65c7259aaac439fc1ae12973830 Mon Sep 17 00:00:00 2001 From: Julien Hillairet Date: Sun, 22 Mar 2026 13:09:20 +0100 Subject: [PATCH 5/9] Improve docstring and add electrical_length to Waveguide --- pyproject.toml | 1 + src/aloha/antenna.py | 15 ++-- src/aloha/constants.py | 3 +- src/aloha/waveguide.py | 175 +++++++++++++++++++++++++++++++++--- src/tests/test_waveguide.py | 6 +- 5 files changed, 179 insertions(+), 21 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5c2be83..8830fb5 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" }, diff --git a/src/aloha/antenna.py b/src/aloha/antenna.py index bce057f..09508cd 100644 --- a/src/aloha/antenna.py +++ b/src/aloha/antenna.py @@ -8,22 +8,19 @@ class Antenna: - """ - ALOHA antenna description. - """ - def __init__(self, filename: str | os.PathLike = None) -> None: """ ALOHA antenna description. Parameters ---------- - filename : str | os.PathLike + filename : str or Path Path to a TOML ALOHA antenna file. """ - if filename: - self.antenna = self.load(filename) + if not filename: + return + self.antenna = self.load(filename) @classmethod def load(cls, filename: str | os.PathLike) -> dict: @@ -32,8 +29,8 @@ def load(cls, filename: str | os.PathLike) -> dict: Parameters ---------- - filename : str - path to a TOML file. + filename : str or Path + Path to a TOML file. Returns ------- 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 3dcc233..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) -> None: + """ 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_waveguide.py b/src/tests/test_waveguide.py index 7b4eeeb..d02d5a8 100644 --- a/src/tests/test_waveguide.py +++ b/src/tests/test_waveguide.py @@ -23,7 +23,8 @@ 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") for m in range(1, 3): @@ -43,6 +44,9 @@ def test_waveguide_properties(self): 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): """ From 8dad99a4554e31216ee518cb5b34e4de8857cf87 Mon Sep 17 00:00:00 2001 From: Julien Hillairet Date: Sun, 22 Mar 2026 15:17:01 +0100 Subject: [PATCH 6/9] add different way to build and Antenna --- pyproject.toml | 3 +- src/aloha/antenna.py | 71 ++++++++++++++++++++++++++++++++------- src/tests/test_antenna.py | 6 ++++ 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8830fb5..3bceab7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,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/antenna.py b/src/aloha/antenna.py index 09508cd..7bb64e8 100644 --- a/src/aloha/antenna.py +++ b/src/aloha/antenna.py @@ -8,35 +8,82 @@ class Antenna: - def __init__(self, filename: str | os.PathLike = None) -> None: + def __init__(self, source: str | os.PathLike | dict | None = None) -> None: """ ALOHA antenna description. Parameters ---------- - filename : str or Path - Path to a TOML ALOHA antenna file. - + 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. """ - if not filename: + self.antenna = {} + if source is None: return - self.antenna = self.load(filename) + 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: """ - Read an ALOHA antenna description (TOML file format). + Load antenna parameters from a TOML file. Parameters ---------- filename : str or Path - Path to a TOML file. + Path to a TOML file containing antenna parameters. Returns ------- - antenna : dict - ALOHA antenna description. - + dict + Dictionary containing antenna parameters. """ with open(filename, "rb") as fp: - return tomllib.load(fp) + antenna_data = 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 = 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']})" diff --git a/src/tests/test_antenna.py b/src/tests/test_antenna.py index ef869dc..3a549e6 100644 --- a/src/tests/test_antenna.py +++ b/src/tests/test_antenna.py @@ -11,7 +11,13 @@ 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): """ From e52b85e13280b4556320f861e1572cbf268c9d28 Mon Sep 17 00:00:00 2001 From: Julien Hillairet Date: Sun, 22 Mar 2026 18:04:46 +0100 Subject: [PATCH 7/9] Add an antenna description validator --- .../{WEST_LH1.toml => WEST_LH1_half.toml} | 90 +++++------------ antennas/simple_antenna.toml | 99 ++++++++++++------- src/aloha/antenna.py | 72 +++++++++++++- src/tests/test_antenna.py | 2 +- 4 files changed, 160 insertions(+), 103 deletions(-) rename antennas/{WEST_LH1.toml => WEST_LH1_half.toml} (61%) diff --git a/antennas/WEST_LH1.toml b/antennas/WEST_LH1_half.toml similarity index 61% rename from antennas/WEST_LH1.toml rename to antennas/WEST_LH1_half.toml index 5bd876c..ca47f1e 100644 --- a/antennas/WEST_LH1.toml +++ b/antennas/WEST_LH1_half.toml @@ -2,78 +2,55 @@ name = "Tore Supra/WEST C3/LH1 antenna (half)" frequency = 3.7e9 -[modules] +# global description of the modules +[global] # Number of modules per antenna in the poloidal direction. nma_theta = 1 # Number of modules per antenna in the toroidal direction. nma_phi = 8 - -# Position index of the module in the poloidal direction (from low theta to high theta, -# i.e. from bottom to top if the antenna is on LFS). -ima_theta = [1, 1, 1, 1, 1, 1, 1, 1] - -# Position index of the module in the toroidal direction (from low phi to high phi, +# 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). -# numbering in ALOHA goes from left to right as view from the plasma. -ima_phi = [1, 1, 1, 1, 1, 1, 1, 1, 1] +# 2) poloidal direction (from low theta to high theta, +# i.e. from bottom to top if the antenna is on LFS). +ima = [1, 2, 3, 4, 5, 6, 7, 8] +# Spacing between toroidally neighboring modules [m] +sm_phi = 0 # Spacing between poloidally neighboring modules [m] sm_theta = 0 -[waveguides] -# Number of waveguides per module in the poloidal direction. (passive and active) -nwm_theta = 3 - +# module description +[module] # Number of waveguides per module in the toroidal direction. (passive and active) nwm_phi = 6 - +# Number of waveguides per module in the poloidal direction. (passive and active) +nwm_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. npwbm_phi = 1 - # Number of passive waveguides on each antenna edge in the toroidal direction. npwe_phi = 1 - # Spacing between poloidally neighboring waveguides [m] sw_theta = 12e-3 - # Height of waveguides in the poloidal direction [m] hw_theta = 70e-3 - # Width of active waveguides [m] bwa = 8e-3 - # Width of internal passive waveguides [m] biwp = 6.5e-3 - # Width of edge passive waveguides [m] bewp = 6.5e-3 - # Thickness between waveguides in the toroidal direction [m] -# Reminder : length(e_phi) = nma_phi*nwm_phi + (nma_phi - 1)*npwbm_phi + 2*npwe_phi - 1 -e = 2e-3 # between active waveguides -e_pwg = 3e-3 # between passive waveguides - -#e_aw = [e, e, e, e, e] -#e_mod = [e_pwg e_aw e_pwg]; -#waveguides.e_phi = repmat(e_mod, 1, 8); - -# Short circuit length for passive waveguides [m] -# Reminder : length(scl) = nma_phi*npwm_phi + (nma_phi - 1)*npwbm_phi + 2*npwe_phi -#nscl = waveguides.npwbm_phi*(modules.nma_phi-1) + ... -# waveguides.npwe_phi*2 + ... -# sum(not(waveguides.mask))*modules.nma_phi -#waveguides.scl = repmat(1/4, 1, nscl) +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 +scl = [0.25] [sparameters] -# Modules Scattering parameters -# matrice S des modules ds des fichiers .m (NB : la matrice est rangee sur une seule colonne) -sparams_pathFrom = "." -sparams_pathTo = "S_HFSS/matrices_HFSS_C3" - # 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 @@ -115,7 +92,7 @@ sparams_pathTo = "S_HFSS/matrices_HFSS_C3" # 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]; -sparams_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'] +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 @@ -124,24 +101,11 @@ sparams_filenames = ['S_C3_24b', 'S_C3_23b', 'S_C3_22b', 'S_C3_21b', 'S_C3_14b', # This is only usefull when using input data from experiments. # modules.Sparameters.phase_deembedded = zeros(nma_phi,1); # in degrees -#sparams_phase_deembedded = [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] - -## -------------------------------- -## Other antenna_lh CPO parameters -## -------------------------------- -## Not defined here in ALOHA -# Plasma edge characteristics in front of the antenna. -antenna_lh.plasmaedge = [] - -# Amplitude of the TE10 mode injected in the module [W], Matrix (nantenna_lh,max_nmodules). Time-dependent -#modules.amplitude = sqrt(4.0320e6/16)*ones(modules.nma_phi,1) - -# Phase of the TE10 mode injected in the module [rd], Matrix (nantenna_lh, max_nmodules). Time-dependent -# in degrees -#modules.phase = -90*(0:modules.nma_phi-1) - -## Not used at all in ALOHA - -# Reference global antenna position. Vectors (nantenna_lh). Time-dependent -antenna_lh.position = [] -# Beam characteristics -antenna_lh.beam = [] +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 index 13a3ecd..6528325 100644 --- a/antennas/simple_antenna.toml +++ b/antennas/simple_antenna.toml @@ -2,43 +2,68 @@ name = "8 waveguides single row antenna" frequency = 3.7e9 -[modules] -# nbre de lignes de guides poloidales -nb_g_pol = 1 +# global description of the modules +[global] +# Number of modules per antenna in the toroidal direction. +nma_phi = 8 +# Number of modules per antenna in the poloidal direction. +nma_theta = 1 +# 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). +ima = [1, 2, 3, 4, 5, 6, 7, 8] +# Spacing between toroidally neighboring modules [m] +sm_phi = 0 +# Spacing between poloidally neighboring modules [m] +sm_theta = 0 -# mettre autant de modules que de guides -# nbre de modules sur une ligne poloidale -nb_modules_tor = 8 - -# nbre de guides par module dans le sens poloidal -nb_g_module_pol = 1 - -# nbre de guides par module ds le sens toroidal -nb_g_module_tor = 1 -pass_module_tor = [] - -# nbre de guides passifs entre les modules -nb_g_passifs_inter_modules = 0 -# nbre de guides passifs sur chaque bord -nb_g_passifs_bord = 0 - -[waveguides] -# espacement entre les grills juxtaposes poloidalement -espacement_g_pol = 12e-3 -# hauteur des guides ds le sens poloidal -a = 70e-3 - -#antenne_standard = 1; # = 0 il faut decrire l'antenne sur une ligne : tableaux b et e -# # = 1 parametres scalaires : b_g_actif, b_g_pass et e - -# largeur des guides actifs -b_g_actif = 6.5e-3 -# largeur des guides passifs -b_g_pass = nan -# epaisseur des parois des guides dans le sens toroidal -e = 1e-3 -# longueur du court-circuit (en lambda guidee); -lcc = 0.25 +# module description +[module] +# Number of waveguides per module in the poloidal direction. (passive and active) +nwm_theta = 1 +# Number of waveguides per module in the toroidal direction. (passive and active) +nwm_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 = 1 +# Number of passive waveguides on each antenna edge in the toroidal direction. +npwe_phi = 1 +# Spacing between poloidally neighboring waveguides [m] +sw_theta = 12e-3 +# Height of waveguides in the poloidal direction [m] +hw_theta = 70e-3 # constant for all waveguides +# Width of active waveguides [m] +# if scalar, same thickness for all waveguides +bwa = 14.65e-3 +# Width of internal passive waveguides [m] +# if scalar, same thickness for all waveguides +biwp = 12e-3 +# Width of edge passive waveguides [m] +# if scalar, same thickness for all waveguides +bewp = 10e-3 +# Thickness between waveguides in the toroidal direction [m] +# if scalar, same thickness for all waveguides +e_phi = 4e-3 +# Thickness between passive waveguides +e_phi_pwg = 4e-3 +# Short circuit depth for passive waveguides in guided wavelegth +# if scalar, same value for all passive waveguides +scl = 0.25 +# S-parameters description [sparameters] -# matrice S des modules ds des fichiers .m (NB : la matrice est rangee sur une seule colonne) +# 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/src/aloha/antenna.py b/src/aloha/antenna.py index 7bb64e8..3218cd8 100644 --- a/src/aloha/antenna.py +++ b/src/aloha/antenna.py @@ -1,5 +1,6 @@ import os import tomllib +from collections import defaultdict import numpy as np @@ -45,7 +46,7 @@ def load(cls, filename: str | os.PathLike) -> dict: Dictionary containing antenna parameters. """ with open(filename, "rb") as fp: - antenna_data = tomllib.load(fp) + antenna_data = cls.validate_description(tomllib.load(fp)) return antenna_data @classmethod @@ -64,7 +65,7 @@ def from_dict(cls, antenna_dict: dict) -> "Antenna": An instance of the Antenna class. """ _ant = cls() - _ant.antenna = antenna_dict + _ant.antenna = cls.validate_description(antenna_dict) return _ant @classmethod @@ -87,3 +88,70 @@ def from_file(cls, filename: str | os.PathLike) -> "Antenna": 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"]["nma_phi"] * antenna["global"]["nma_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"]["ima"]) != total_nb_modules: + raise ValueError( + f"Number of module indices ({len(antenna['global']['ima'])}) " + 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 diff --git a/src/tests/test_antenna.py b/src/tests/test_antenna.py index 3a549e6..4cb2e99 100644 --- a/src/tests/test_antenna.py +++ b/src/tests/test_antenna.py @@ -23,7 +23,7 @@ def test_validate_antenna_files(self): """ Validation of the TOML schema of all the pre-defined antennas. """ - values = ["name", "frequency", "modules", "waveguides", "sparameters"] + values = ["name", "frequency", "global", "module", "sparameters"] antenna_filenames = glob("*.toml", root_dir=ANTENNAS_DIR) for antenna_filename in antenna_filenames: From d02bb3b2f52955f4e13de08d76235f260d9f45e2 Mon Sep 17 00:00:00 2001 From: Julien Hillairet Date: Sun, 22 Mar 2026 19:28:15 +0100 Subject: [PATCH 8/9] Add antenna coordinates and plot --- src/aloha/antenna.py | 122 +++++++++++++++++++++++++++++++++++++- src/tests/test_antenna.py | 2 + 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/src/aloha/antenna.py b/src/aloha/antenna.py index 3218cd8..7657ee3 100644 --- a/src/aloha/antenna.py +++ b/src/aloha/antenna.py @@ -1,7 +1,7 @@ import os import tomllib -from collections import defaultdict +import matplotlib.pyplot as plt import numpy as np from .constants import pi @@ -155,3 +155,123 @@ def is_valid(self) -> bool: 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() + + for idx_pol in range(antenna["global"]["nma_theta"]): + for idx_tor in range(len(z)): + rect_pos = [z[idx_tor], y[idx_pol], b[idx_tor], a] + # Create passive/active mask + ar_modules = np.ones(antenna["global"]["nma_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"]["npwbm_phi"])]) + + pa_mask = np.kron(ar_modules, ar_pa_mask) + + # Remove last element if there are passive waveguides between modules + if antenna["module"]["npwbm_phi"] > 0: + pa_mask = pa_mask[:-1] + + # Add passive waveguides at edges + pa_mask = np.concatenate( + [np.zeros(antenna["module"]["npwe_phi"]), pa_mask, np.zeros(antenna["module"]["npwe_phi"])] + ) + + if pa_mask[idx_tor] == 0: + 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: + 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"Antenna architecture: {antenna['name']}\n(as view from the plasma)") + plt.show() + + def antenna_coordinates(self): + """ + Extract the pertinent information for ALOHA from the antenna description. + + Returns + ------- + b, a, z, y, nwr, nwa, act_module_tor + """ + 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) + nwr = mod["nma_phi"] * wg["nwm_phi"] + 2 * wg["npwe_phi"] + (mod["nma_phi"] - 1) * wg["npwbm_phi"] + + # (total) number of waveguides per column + nwc = mod["nma_theta"] * wg["nwm_theta"] + + # total number of waveguides + nwa = nwr * nwc + + # waveguide height - supposed constant for all the waveguides of the antenna + a = wg["hw_theta"] + + # b + # Make the array b which contains all the waveguide width of a row of waveguides + b_module = np.where(wg["mask"], wg["bwa"], wg["biwp"]) + b_edge = np.full(wg["npwe_phi"], wg["bewp"]) + b_inter = np.full(wg["npwbm_phi"], wg["biwp"]) + + b = np.concatenate([b_edge, np.tile(np.concatenate([b_module, b_inter]), mod["nma_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["npwbm_phi"] * (mod["nma_phi"] - 1) + wg["npwe_phi"] * 2 + mod["nma_phi"] * wg["nwm_phi"] - 1 + e = np.tile(wg["biwp"], ne_phi) + + # z + # Make the array z which contains all the waveguide positions in the toroidal direction + z = np.zeros(nwr) + for ind in range(1, nwr): + 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["nwm_theta"], wg["bwa"]), np.full(1, mod["sm_phi"])]), + mod["nma_theta"] - 1, + ), + np.full(wg["nwm_theta"], wg["bwa"]), + ] + ) + y = np.zeros(nwc) + for ind in range(1, nwc): + y[ind] = y[ind - 1] + h[ind - 1] + a + + # index of active waveguides in a module + act_module_tor = wg["mask"] # np.where(wg['mask'] == 1)[0] + + return b, a, z, y, nwr, nwa, act_module_tor diff --git a/src/tests/test_antenna.py b/src/tests/test_antenna.py index 4cb2e99..2529e42 100644 --- a/src/tests/test_antenna.py +++ b/src/tests/test_antenna.py @@ -23,6 +23,8 @@ 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) From 0fe7f49c10838a8dab2892dae03c6bee0673ca94 Mon Sep 17 00:00:00 2001 From: Julien Hillairet Date: Sun, 22 Mar 2026 20:34:52 +0100 Subject: [PATCH 9/9] use meaningful variable names for antenna description --- antennas/WEST_LH1_half.toml | 32 ++++++------ antennas/simple_antenna.toml | 68 +++++++++++++------------ src/aloha/antenna.py | 97 +++++++++++++++++++++++------------- 3 files changed, 112 insertions(+), 85 deletions(-) diff --git a/antennas/WEST_LH1_half.toml b/antennas/WEST_LH1_half.toml index ca47f1e..3487fd1 100644 --- a/antennas/WEST_LH1_half.toml +++ b/antennas/WEST_LH1_half.toml @@ -5,50 +5,49 @@ frequency = 3.7e9 # global description of the modules [global] # Number of modules per antenna in the poloidal direction. -nma_theta = 1 +nb_mod_theta = 1 # Number of modules per antenna in the toroidal direction. -nma_phi = 8 +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). -ima = [1, 2, 3, 4, 5, 6, 7, 8] - +idx_mod = [1, 2, 3, 4, 5, 6, 7, 8] # Spacing between toroidally neighboring modules [m] -sm_phi = 0 +spacing_btw_mod_phi = 0 # Spacing between poloidally neighboring modules [m] -sm_theta = 0 +spacing_btw_mod_theta = 0 # module description [module] # Number of waveguides per module in the toroidal direction. (passive and active) -nwm_phi = 6 +nb_wg_phi = 6 # Number of waveguides per module in the poloidal direction. (passive and active) -nwm_theta = 3 +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. -npwbm_phi = 1 +nb_pwg_btw_mod_phi = 1 # Number of passive waveguides on each antenna edge in the toroidal direction. -npwe_phi = 1 +nb_pwg_edge = 1 # Spacing between poloidally neighboring waveguides [m] -sw_theta = 12e-3 +space_btw_wg_theta = 12e-3 # Height of waveguides in the poloidal direction [m] -hw_theta = 70e-3 +wg_size_theta = 70e-3 # Width of active waveguides [m] -bwa = 8e-3 +awg_size_phi = 8e-3 # Width of internal passive waveguides [m] -biwp = 6.5e-3 +pwg_size_phi = 6.5e-3 # Width of edge passive waveguides [m] -bewp = 6.5e-3 +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 -scl = [0.25] +pwg_depth = [0.25] [sparameters] # modules C3 @@ -94,7 +93,6 @@ scl = [0.25] 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. diff --git a/antennas/simple_antenna.toml b/antennas/simple_antenna.toml index 6528325..7984920 100644 --- a/antennas/simple_antenna.toml +++ b/antennas/simple_antenna.toml @@ -1,58 +1,60 @@ -# 8 waveguides single row antenna +# 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 = 8 -# Number of modules per antenna in the poloidal direction. -nma_theta = 1 -# Position index of the modules in the followinf order: +# 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). -ima = [1, 2, 3, 4, 5, 6, 7, 8] -# Spacing between toroidally neighboring modules [m] -sm_phi = 0 -# Spacing between poloidally neighboring modules [m] -sm_theta = 0 +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) -nwm_theta = 1 -# Number of waveguides per module in the toroidal direction. (passive and active) -nwm_phi = 1 +# 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 = 1 -# Number of passive waveguides on each antenna edge in the toroidal direction. -npwe_phi = 1 -# Spacing between poloidally neighboring waveguides [m] -sw_theta = 12e-3 -# Height of waveguides in the poloidal direction [m] -hw_theta = 70e-3 # constant for all waveguides -# Width of active waveguides [m] +# 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 -bwa = 14.65e-3 -# Width of internal passive waveguides [m] +awg_size_phi = 10e-3 +# Width of internal passive waveguides [m] (biwp) # if scalar, same thickness for all waveguides -biwp = 12e-3 -# Width of edge passive waveguides [m] +pwg_size_phi = 8e-3 +# Width of edge passive waveguides [m] (bewp) # if scalar, same thickness for all waveguides -bewp = 10e-3 +pwg_size_edge_phi = 8e-3 # Thickness between waveguides in the toroidal direction [m] # if scalar, same thickness for all waveguides -e_phi = 4e-3 +e_phi = 2e-3 # Thickness between passive waveguides -e_phi_pwg = 4e-3 +e_phi_pwg = 3e-3 # Short circuit depth for passive waveguides in guided wavelegth -# if scalar, same value for all passive waveguides -scl = 0.25 +# if scalar, same value for all passive waveguides (scl) +pwg_depth = 0.25 # S-parameters description [sparameters] diff --git a/src/aloha/antenna.py b/src/aloha/antenna.py index 7657ee3..ef5370f 100644 --- a/src/aloha/antenna.py +++ b/src/aloha/antenna.py @@ -107,7 +107,7 @@ def validate_description(cls, antenna: dict) -> dict: if not antenna: # empty dict is OK return antenna - total_nb_modules = antenna["global"]["nma_phi"] * antenna["global"]["nma_theta"] + total_nb_modules = antenna["global"]["nb_mod_phi"] * antenna["global"]["nb_mod_theta"] # excitations if len(antenna["excitation"]["magnitudes"]) != total_nb_modules: raise ValueError( @@ -121,9 +121,9 @@ def validate_description(cls, antenna: dict) -> dict: ) # module indices - if len(antenna["global"]["ima"]) != total_nb_modules: + if len(antenna["global"]["idx_mod"]) != total_nb_modules: raise ValueError( - f"Number of module indices ({len(antenna['global']['ima'])}) " + f"Number of module indices ({len(antenna['global']['idx_mod'])}) " f"does not match total number of modules ({total_nb_modules})" ) @@ -167,29 +167,29 @@ def plot(self): # Extract parameters from antenna dictionary antenna = self.antenna plt.figure() - - for idx_pol in range(antenna["global"]["nma_theta"]): + # 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] + rect_pos = [z[idx_tor], y[idx_pol], b[idx_tor], a[idx_pol]] # Create passive/active mask - ar_modules = np.ones(antenna["global"]["nma_phi"]) + 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"]["npwbm_phi"])]) + 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"]["npwbm_phi"] > 0: + 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"]["npwe_phi"]), pa_mask, np.zeros(antenna["module"]["npwe_phi"])] + [np.zeros(antenna["module"]["nb_pwg_edge"]), pa_mask, np.zeros(antenna["module"]["nb_pwg_edge"])] ) - if pa_mask[idx_tor] == 0: + if pa_mask[idx_tor] == 0: # passive wg plt.gca().add_patch( plt.Rectangle( (rect_pos[0], rect_pos[1]), @@ -199,7 +199,7 @@ def plot(self): edgecolor="k", ) ) - elif pa_mask[idx_tor] == 1: + 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] @@ -209,16 +209,30 @@ def plot(self): plt.axis("equal") plt.xlabel("z [m]") plt.ylabel("y [m]") - # plt.title(f"Antenna architecture: {antenna['name']}\n(as view from the plasma)") + plt.title(f"ALOHA antenna: {antenna['name']}\n (view from the plasma)") plt.show() def antenna_coordinates(self): """ - Extract the pertinent information for ALOHA from the antenna description. + Generate waveguide positions and dimensions from the antenna description. Returns ------- - b, a, z, y, nwr, nwa, act_module_tor + 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"] @@ -226,34 +240,45 @@ def antenna_coordinates(self): # (total) number of waveguides per row # = (nb wg in a module) + 2*(nb ext wg) + (nb of wg between modules) - nwr = mod["nma_phi"] * wg["nwm_phi"] + 2 * wg["npwe_phi"] + (mod["nma_phi"] - 1) * wg["npwbm_phi"] + 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 - nwc = mod["nma_theta"] * wg["nwm_theta"] + nb_wg_per_col = mod["nb_mod_theta"] * wg["nb_wg_theta"] # total number of waveguides - nwa = nwr * nwc + nb_wg_total = nb_wg_per_row * nb_wg_per_col # waveguide height - supposed constant for all the waveguides of the antenna - a = wg["hw_theta"] + 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["bwa"], wg["biwp"]) - b_edge = np.full(wg["npwe_phi"], wg["bewp"]) - b_inter = np.full(wg["npwbm_phi"], wg["biwp"]) + 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["nma_phi"] - 1), b_module, b_edge]) + 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["npwbm_phi"] * (mod["nma_phi"] - 1) + wg["npwe_phi"] * 2 + mod["nma_phi"] * wg["nwm_phi"] - 1 - e = np.tile(wg["biwp"], ne_phi) + 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(nwr) - for ind in range(1, nwr): + 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 @@ -261,17 +286,19 @@ def antenna_coordinates(self): h = np.concatenate( [ np.tile( - np.concatenate([np.full(wg["nwm_theta"], wg["bwa"]), np.full(1, mod["sm_phi"])]), - mod["nma_theta"] - 1, + 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["nwm_theta"], wg["bwa"]), + np.full(wg["nb_wg_theta"], wg["awg_size_phi"]), ] ) - y = np.zeros(nwc) - for ind in range(1, nwc): - y[ind] = y[ind - 1] + h[ind - 1] + a + 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"] # np.where(wg['mask'] == 1)[0] + act_module_tor = wg["mask"] - return b, a, z, y, nwr, nwa, act_module_tor + return b, a, z, y, nb_wg_per_row, nb_wg_total, act_module_tor