{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Introduction\n", "## The WEST ICRH antennas \n", "Three identical ELM-resilient and CW power ICRH antennas have been designed for WEST. The ELM resilience property is obtained through an internal conjugate-T electrical scheme with series capacitors. An antenna has 4 straps (2 toroidal x 2 poloidal) and is fed by 2 generators (left side and right side). Each antenna is equipped with four internal COMET® tuneable vacuum capacitors, with capacitances ranging from 15 pF to 150 pF and specifically upgraded for CW operation. A two-stage quarter-wavelength and water cooled impedance transformer is connected from the T-junction to the vacuum feedthrough.\n", "\n", "\n", "\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## WEST IC antenna Python RF Model" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%load_ext autoreload\n", "%autoreload 2" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import skrf as rf\n", "rf.stylely() # pretty plots\n", "from tqdm.notebook import tqdm\n", "# WEST ICRH Antenna package\n", "import sys; sys.path.append('..')\n", "from west_ic_antenna import WestIcrhAntenna" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The WEST ICRH Antenna RF model can be built by defining one or all of:\n", "- the frequency band of interest, given by a [scikit-rf](https://scikit-rf.readthedocs.io/en/latest/) `Frequency` object\n", "- the front face (Touchstone) S-parameter `filename`, ie. the model of the antenna front-face radiating to a given medium (or a scikit-rf 4-port `Network`)\n", "- the capacitor's capacitances `[C1, C2, C3, C4]`\n", "\n", "All these parameters are optionnal when builing the `WestIcrhAntenna` object. Default parameters is a frequency band 30-70 MHz, with the front-face radiating in vacuum with all capacitances set to 50 pF." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Using default values\n", "antenna = WestIcrhAntenna()\n", "print(antenna)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For example, to reduce the frequency band of interest:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "freq = rf.Frequency(48, 57, npoints=2001, unit='MHz')\n", "antenna = WestIcrhAntenna(frequency=freq)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The antenna circuit can be visualized via the scikit-rf `Circuit` object:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "antenna.circuit().plot_graph(network_labels=True, edge_labels=True, \n", "inter_labels=True, port_labels=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Antenna Matching\n", "Matching the WEST ICRH antenna consists in setting up the 4 capacitances values ($C_1,C_2,C_3,C_4$) to achieve the desired behaviour, typically a low reflected power to the generators. For the given geometry of the WEST antenna, these optimal capacitances depend on:\n", "- the antenna front-face, i.e. the plasma properties facing the antenna; \n", "- the antenna excitation, powers and phasing between left and right sides." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Matching the antenna in one step\n", "The optimum set of capacitances to minimize the reflected power at a given frequency can be obtained using the `match_both_sides()` method:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "f_match = 55e6\n", "C_match = antenna.match_both_sides(f_match=f_match)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# dipole excitation for the set of capacitances\n", "power = [1, 1]\n", "phase = [0, np.pi]\n", "\n", "# Antenna reflection coefficient in dB (active S-parameter, cf next section)\n", "s_act_db = antenna.s_act_db(power, phase, C_match)\n", "\n", "fig, ax = plt.subplots()\n", "ax.plot(antenna.f_scaled, s_act_db)\n", "ax.set_xlabel('Frequency [MHz]')\n", "ax.set_ylabel('$|S_{act}|$ [dB]')\n", "ax.set_title(f'Reflection is min at {f_match/1e6} MHz')\n", "ax.set_xlim(54, 56)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Matching the antenna step by step\n", "When both sides of the antenna are used (which is the desired situation), the figure of merit is not the reflection coefficient from scattering parameters (such as $S_{11}$ or $S_{22}$) but the \"active\" parameters, that is the RF parameters taking into account the antenna feeding and cross-coupling effects between both sides. Because of these cross-coupling effects, the matching point for each side used separately is not the same than for both sides used together.\n", "\n", "Let's see step by step these effects.\n", "\n", "Each side of the antenna can be matched separately, which is what is done in practice since it's simpler to act on two capacitors than four at the same time. \n", "\n", "Let's start with the left side, looking for a solution at 54 MHz, with the solution 1 (corresponding to $C_{top} > C_{bot}$, solution 2 being the opposite). The right side is left unmatched." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "f_match = 54e6\n", "C_match_left = antenna.match_one_side(f_match=f_match, \n", " side='left', solution_number=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Once the solution has been found, we setup the antenna capacitors to these values: " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "antenna.Cs = C_match_left" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's have a look to the S-parameters of the antenna, which is a 2-port network. An easy way to plot them is to retrieve the scikit-rf `Network` object and its convenience methods:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "antenna.circuit().network.plot_s_db(ax=ax)\n", "ax.axvline(f_match, color='gray', ls='--')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's match the right side (the left side being unmatched). This time, it will minimize the S22 at the match frequency." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "C_match_right = antenna.match_one_side(f_match=f_match, \n", " side='right', solution_number=1)\n", "antenna.Cs = C_match_right\n", "fig, ax = plt.subplots()\n", "antenna.circuit().network.plot_s_db(ax=ax)\n", "ax.axvline(f_match, color='gray', ls='--')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If we setup the antenna with the combination of these two solutions, and zoom into the 48-52 MHz band, one sees that antenna shows two optimized frequencies around the match frequencies." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "C_match = [C_match_left[0], C_match_left[1], C_match_right[2], C_match_right[3]]\n", "print(C_match)\n", "antenna.Cs = C_match" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "antenna.circuit(Cs=C_match).network.plot_s_db(ax=ax)\n", "ax.axvline(f_match, color='gray', ls='--')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "These optimum frequencies correspond to the monopole and dipole excitations. Instead of looking to the S-parameters, it is more meaningfull to look to the [_active_ S-parameters](https://scikit-rf.readthedocs.io/en/latest/api/generated/skrf.network.Network.s_active.html), defined by:\n", "\n", "$$\n", "S_{act,m} = \\sum_{n=1}^M S_{mn} \\frac{a_n}{a_m}\n", "$$\n", "\n", "with $m=1..N$ where $N$ is the number of ports (here M=2) and $a_k$ the complex excitation for the k-th port." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# monopole excitation, left side being the reference\n", "power = [1, 1]\n", "phase = [0, 0]\n", "# getting the active s-parameters\n", "s_act_db = antenna.s_act_db(power, phase)\n", "# plotting\n", "fig, ax = plt.subplots()\n", "ax.plot(freq.f_scaled, s_act_db)\n", "ax.axvline(f_match/1e6, ls='--', color='gray')\n", "ax.set_title('monopole excitation')\n", "ax.set_xlabel('f [MHz]')\n", "ax.set_ylabel('$|s_{act}|$ [dB]')\n", "ax.grid(True)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# dipole excitation, left side being the reference\n", "power = [1, 1]\n", "phase = [0, np.pi]\n", "# getting the active s-parameters\n", "s_act_db = antenna.s_act_db(power, phase, Cs=C_match)\n", "# plotting\n", "fig, ax = plt.subplots()\n", "ax.plot(freq.f_scaled, s_act_db)\n", "ax.axvline(f_match/1e6, ls='--', color='gray')\n", "ax.set_title('dipole excitation')\n", "ax.set_xlabel('f [MHz]')\n", "ax.set_ylabel('$|s_{act}|$ [dB]')\n", "ax.grid(True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Voltages and Currents" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# dipole case, 1 MW input on both sides\n", "power = [1e6, 1e6]\n", "phase = [0, np.pi]\n", "\n", "Vs = antenna.voltages(power, phase)\n", "Is = antenna.currents(power, phase)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots(2,1,sharex=True)\n", "ax[0].plot(freq.f_scaled, np.abs(Vs)/1e3)\n", "ax[1].plot(freq.f_scaled, np.abs(Is)/1e3)\n", "ax[1].set_xlabel('f [MHz]')\n", "ax[0].set_ylabel('Voltage [kV]')\n", "ax[1].set_ylabel('Current [kA]')\n", "[a.grid(True) for a in ax]\n", "ax[0].legend(('V1','V2','V3','V4'))\n", "ax[1].legend(('I1','I2','I3','I4'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The voltage and current values are of course not realistic, because the antenna is radiating on vacuum here, not on plasma." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Impedance at the T-junction\n", "The WEST ICRH antennas design is based on the conjugate-T to insure a load-tolerance. In particular, they have been designed to operate with an impedance at the T-junction $Z_T$ close to 3 Ohm. An impedance transformer connects the T-junction to the feeding transmission line (30 Ohm line). Hence, matching the antenna is similar to having a 30 Ohm load connected to the feeding transmission line, such as no power is reflected (VSWR$\\to 1$), which should be equivalent of having an impedance of roughtly 3 Ohm at the T-junction.\n", "\n", "However, due to real-life design and manufacturing constraints, the optimal impedance at the T-junction is not necessarely 3 Ohm, but can be slightly different in both real and imaginary parts. \n", "\n", "So let's evaluate the impact of the realistic geometries (simulated from full-wave tools) on the impedance at the T-junction to the 30 Ohm feeder line (the one which really matter for the generator point-of-view).\n", "\n", "For that, let's take the impedance transformer/vacuum window/service stub network assembly of an antenna:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "freq = rf.Frequency(50, 50, unit='MHz', npoints=1)\n", "antenna = WestIcrhAntenna(frequency=freq)\n", "assembly = antenna.windows_impedance_transformer\n", "# note that port 1 corresponds to the generator side (30 Ohm) \n", "print(assembly)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The port 1 of this network assembly corresponds to the 30 Ohm feeding line while the port 2 correspond to the end of the second section of the impedance transformer. Let's load the port 2 with an ideal impedance $Z_T=R_T + j X_T$ and scanning the effect of $R_T$ and $X_T$ on the VSWR seen by the generator." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# create a grid of R_T and X_T values\n", "R_Ts, X_Ts = np.meshgrid(np.linspace(1, 5, 50), \n", " np.linspace(-3, 3, 50))\n", "media_port2 = rf.DefinedGammaZ0(frequency=freq, z0=assembly.z0[:,1].real)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# calculate the VSWR at port 1 as a function of (R_T, X_T)\n", "vswrs = []\n", "for (R_T,X_T) in tqdm(np.nditer([R_Ts, X_Ts])):\n", " Z_T = R_T + 1j*X_T\n", " # connect the port 2 with a impedance Z_T\n", " ntw = assembly ** media_port2.load(rf.zl_2_Gamma0(assembly.z0[:,1].real, Z_T))\n", " vswrs.append(ntw.s_vswr)\n", "# reshape to 2D\n", "vswrs = np.array(vswrs).reshape(R_Ts.shape)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "cs=ax.contour(R_Ts, X_Ts, vswrs, \n", " np.linspace(1, 2, 21))\n", "ax.clabel(cs, inline=1, fontsize=10)\n", "ax.set_xlabel('R_T [Ohm]')\n", "ax.set_ylabel('X_T [Ohm]')\n", "ax.set_title('SWR at feeding line')\n", "ax.axvline(3, color='gray', alpha=0.8)\n", "ax.axhline(0, color='gray', alpha=0.8)\n", "ax.grid(True, alpha=0.2)\n", "ax.axis([0, 6, -2.2, 2.2])\n", "ax.set_aspect('equal')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Hence the optimal impedance at the T-junction is not 3 Ohm, but slightly close in the complex plane. Let's calculate this optimal value using: " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from scipy.optimize import minimize" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "assembly.s_def = 'power'\n", "def optim_fun(x):\n", " R_T, X_T = x\n", " Z_T = R_T + 1j*X_T\n", " # connect the port 2 with a impedance Z_T\n", " ntw = assembly ** media_port2.load(rf.zl_2_Gamma0(assembly.z0[:,1].real, Z_T))\n", " return ntw.s_vswr.flatten()[0]\n", "\n", "sol = minimize(optim_fun, x0=[3,0])\n", "print('Optimum Z_T=', sol.x[0] + 1j*sol.x[1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The optimum T-impedance is such $Z_T= 2.87 - 0.17j$." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from IPython.core.display import HTML\n", "def _set_css_style(css_file_path):\n", " \"\"\"\n", " Read the custom CSS file and load it into Jupyter\n", " Pass the file path to the CSS file\n", " \"\"\"\n", " styles = open(css_file_path, \"r\").read()\n", " s = '' % styles\n", " return HTML(s)\n", "\n", "_set_css_style('custom.css')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.7" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": true, "sideBar": true, "skip_h1_title": true, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": false, "toc_position": {}, "toc_section_display": true, "toc_window_display": false } }, "nbformat": 4, "nbformat_minor": 4 }