diff --git a/documentation/eng-models/tf-coil.md b/documentation/eng-models/tf-coil.md index 6dbff33993..4f7245a588 100644 --- a/documentation/eng-models/tf-coil.md +++ b/documentation/eng-models/tf-coil.md @@ -786,6 +786,8 @@ $$ Von-Mises yeild stress is also shown in the output for information.

+------------- + ## TF coil ripple @@ -806,20 +808,40 @@ ripple impacts TF coil design in two ways: conductor is larger than the value obtained using the axisymmetric assumption. This is only true in `PROCESS` for the superconducting coil case. More info can be found [here](../eng-models/tf-coil-superconducting.md#on-coil-ripple-peak_tf_with_ripple). -### Plasma ripple +------------- + +### Plasma ripple | `plasma_outboard_edge_toroidal_ripple()` -The maximum plasma ripple is defined in *PROCESS* with the user input `ripple_b_tf_plasma_edge_max` +The maximum toroidal plasma ripple at the outboard is defined in `PROCESS` with the user input `ripple_b_tf_plasma_edge_max` as: $$ \delta = \frac{B_\mathrm{max}-B_\mathrm{min}}{B_\mathrm{max}+B_\mathrm{min}} $$ -with \( B_\mathrm{min}\) and \( B_\mathrm{max}\) minimum field (between coils) +with $B_\mathrm{min}$ and $B_\mathrm{max}$ minimum field (between coils) and the maximum field (on coil toroidal direction) respectively, measured at -the mid-plane plasma outer limit (separatrix). PROCESS plasma ripple -is estimated using a parametric Bio-Savart calculation fitted using the -UKAEA free boundary MHD code FIESTA. The shape (Princeton-D) +the mid-plane plasma outer limit (separatrix). + +To prevent intolerable fast particles losses and plasma instabilities, +$\delta$ must be limited to a few percent, approximatively \( \delta \in +[0.5-1]\) . If intolerable, the plasma ripple can be reduced with many +different techniques, for example the TF coil shape, stabilisation coils can +be added, more coils can be used or the coil outboard radius can be increased. +All these design modifications affects the coil system design, for example +ripple shape optimisation should be done without generating too much bending +stress due to the un-adapted curvature radius, and adding coils must not prevent +remote maintenance. To keep the design procedure as simple as possible in +`PROCESS`, unacceptable ripple is reduced by simply moving the +TF coil leg to a larger radius. The outboard ripple is directly obtained reverting the ripple fit to provide +$R_\mathrm{outboard\ WP}^\mathrm{mid}$ as a function of the ripple. +The minimal $R_\mathrm{outboard\ WP}^\mathrm{mid}$ is technically obtained +by increasing the gap between the vacuum vessel and the coil (`dr_shld_vv_gap_outboard`). + +#### D-shaped coils + +`PROCESS` plasma ripple is estimated using a parametric Bio-Savart calculation fitted using the +UKAEA free boundary MHD code FIESTA. The shape (Princeton-D) used for these MHD model is shown in the left section of Figure 11. @@ -834,10 +856,10 @@ used for these MHD model is shown in the left section of Figure 11.

- Figure 11 : The left graph shows the filament shape used in the - FIESTA ripple calculations. The current loops are made straight + Figure 11 : The top graph shows the filament shape used in the + FIESTA ripple calculations. The current loops are made straight section (red lines) connecting vertices (blue dots) following the coil - shape. The right plot shows the ripple calculated by FIESTA (lines with + shape. The bottom plot shows the ripple calculated by FIESTA (lines with open circles) compared to the fit values (lines without circles) for different number of coils (16, 18 and 20) and lateral winding pack to TF size ratio (\(x = 0.737\) and \(x = 2.947\)) as a function of the @@ -863,13 +885,12 @@ dimensions defined as: $$ - x = \frac{\Delta R_\mathrm{tWP}^\mathrm{out}}{R_\mathrm{maj}} N_\mathrm{TF} + x = \frac{\Delta x_\mathrm{WP,max}}{R_\mathrm{maj}} N_\mathrm{TF} $$ -with \(\Delta R_\mathrm{tWP}^\mathrm{out}\) the plasma side WP thickness -defined in the Figures 1 and 3, \(N_\mathrm{TF}\) the number of coils and -\(R_\mathrm{maj}\) the plasma centre radius. \(y\) is the plasma outer +with $\Delta x_\mathrm{WP,max}$ the maximum toroidal width of the WP on the plasma side, $N_\mathrm{TF}$ the number of coils and +$R_\mathrm{maj}$ the plasma centre radius. $y$ is the plasma outer mid-plane separatrix to outboard TF leg mid-plane radius ratio: @@ -878,41 +899,33 @@ $$ $$ -with \(a_\mathrm{min}\) the plasma minor radius and \(R_\mathrm{outboard\ TF} -^\mathrm{mid}\) the TF winding pack outboard leg midplane radius at its +with $a_\mathrm{min}$ the plasma minor radius and $R_\mathrm{outboard\ TF} +^\mathrm{mid}$ the TF winding pack outboard leg midplane radius at its centre. The scaling fitting range is provided by: -- **Number of coils:** \( N_\mathrm{TF} \in \{16, 18, 20\} \). +- **Number of coils:** $N_\mathrm{TF} \in \{16, 18, 20\}$ + +- **Winding pack size ratio:** $x \in [0.737-2.95]$ + +- **separatrix to TF ratio:** $y \in [0.7-0.8]$ + +#### Picture frame coils + +For the picture frame coils the presence of a straight outboard leg means the ripple is just given by: + +$$ +\delta = \left(\frac{R_{\text{maj}}+a_{\text{min}}}{R_\mathrm{outboard\ TF}^\mathrm{mid}}\right)^{\frac{1}{N_{\text{TF}}}} +$$ -- **Winding pack size ratio:** \( x \in [0.737-2.95] \) -- **separatrix to TF ratio:** \( y \in [0.7-0.8] \) - -To prevent intolerable fast particles losses and plasma instabilities, -\(\delta\) must be limited to a few percent, approximatively \( \delta \in -[0.5-1]\) . If intolerable, the plasma ripple can be reduced with many -different techniques, for example the TF coil shape, stabilisation coils can -be added, more coils can be used or the coil outboard radius can be increased. -All these design modifications affects the coil system design, for example -ripple shape optimisation should be done without generating too much bending -stress due to the un-adapted curvature radius, and adding coils must not prevent -remote maintenance. To keep the design procedure as simple as possible in -PROCESS, unacceptable ripple is reduced by simply moving the -TF coil leg to a larger radius. The outboard ripple is directly obtained reverting the ripple fit to provide -\(R_\mathrm{outboard\ WP}^\mathrm{mid}\) as a function of the ripple. -The minimal \(R_\mathrm{outboard\ WP}^\mathrm{mid}\) is technically obtained -by increasing the gap between the vacuum vessel and the coil (dr_shld_vv_gap_outboard). -The currently implemented plasma ripple evaluation assumes a Princeton-D shape -and is therefore not valid anymore if very different shapes are considered, -with picture frame coils, for example, different models should be -considered/implemented in PROCESS. +------------ ## TF coil parameter summary table diff --git a/process/build.py b/process/build.py index 8d0d3411f3..0de30cacc3 100644 --- a/process/build.py +++ b/process/build.py @@ -14,6 +14,7 @@ numerics, pfcoil_variables, physics_variables, + superconducting_tf_coil_variables, tfcoil_variables, ) from process.exceptions import ProcessValueError @@ -1517,41 +1518,73 @@ def divgeom(self, output: bool): return divht def plasma_outboard_edge_toroidal_ripple( - self, ripple_b_tf_plasma_edge_max: float, r_tf_outboard_mid: float + self, + ripple_b_tf_plasma_edge_max: float, + r_tf_outboard_mid: float, + n_tf_coils: int, + rmajor: float, + rminor: float, + r_tf_wp_inboard_inner, + r_tf_wp_inboard_centre: float, + r_tf_wp_inboard_outer: float, + dx_tf_wp_primary_toroidal: float, + i_tf_shape: int, + i_tf_sup: int, + dx_tf_wp_insulation: float, + dx_tf_wp_insertion_gap: float, ) -> float: """ - TF ripple calculation - author: P J Knight and C W Ashe, CCFE, Culham Science Centre - ripple_b_tf_plasma_edge_max : input real : maximum allowed ripple at plasma edge (%) - ripple_b_tf_plasma_edge : output real : actual ripple at plasma edge (%) - rtot : input real : radius to the centre of the outboard - TF coil leg (m) - rtotmin : output real : radius to the centre of the outboard - TF coil leg which would produce - a ripple of amplitude ripple_b_tf_plasma_edge_max (m) - flag : output integer : on exit, =1 if the fitted - range of applicability is exceeded - This routine calculates the toroidal field ripple amplitude - at the midplane outboard plasma edge. The fitted coefficients - were produced from MATLAB runs by M. Kovari using the CCFE - MAGINT code to model the coils and fields. -

The minimum radius of the centre of the TF coil legs - to produce the maximum allowed ripple is also calculated. - M. Kovari, Toroidal Field Coils - Maximum Field and Ripple - - Parametric Calculation, July 2014 - ############################################################## - - Picture frame coil model by Ken McClements 2022 gives analytical - solutions within 10% agreement with numerical models. - Activated when i_tf_shape == 2 (picture frame) - + Plasma outboard toroidal field (TF) ripple calculation. + + This routine computes the TF ripple amplitude at the midplane outboard + plasma edge and the minimum radius of the TF coil centre that would + produce a specified maximum allowed ripple. The calculation uses + fitted coefficients derived from numerical modelling (MAGINT) and + includes a simplified analytical picture-frame coil model for + i_tf_shape == 2. + + :param ripple_b_tf_plasma_edge_max: Maximum allowed ripple at plasma edge (percent) + :type ripple_b_tf_plasma_edge_max: float + :param r_tf_outboard_mid: Radius to the centre of the outboard TF coil leg (m) + :type r_tf_outboard_mid: float + :param n_tf_coils: Number of TF coils + :type n_tf_coils: int + :param rmajor: Plasma major radius (m) + :type rmajor: float + :param rminor: Plasma minor radius (m) + :type rminor: float + :param r_tf_wp_inboard_inner: Inner winding-pack inboard radius (m) + :type r_tf_wp_inboard_inner: float + :param r_tf_wp_inboard_centre: Centre winding-pack inboard radius (m) + :type r_tf_wp_inboard_centre: float + :param r_tf_wp_inboard_outer: Outer winding-pack inboard radius (m) + :type r_tf_wp_inboard_outer: float + :param dx_tf_wp_primary_toroidal: Primary toroidal winding-pack thickness (m) + :type dx_tf_wp_primary_toroidal: float + :param i_tf_shape: TF coil shape switch (2 => picture-frame analytical model) + :type i_tf_shape: int + :param i_tf_sup: TF coil support flag (1 => superconducting) + :type i_tf_sup: int + :param dx_tf_wp_insulation: Winding-pack insulation thickness (m) + :type dx_tf_wp_insulation: float + :param dx_tf_wp_insertion_gap: Winding-pack insertion gap (m) + :type dx_tf_wp_insertion_gap: float + + :returns: Tuple containing: + - ripple: Calculated ripple at plasma edge (percent) + - r_tf_outboard_midmin: Minimum r_tf_outboard_mid that yields the specified maximum ripple (m) + - flag: Applicability flag (0 = OK, non-zero = fitted-range concern) + :rtype: tuple[float, float, int] + + :notes: + - Fitted coefficients originate from parametric MAGINT runs (M. Kovari, 2014). + - Picture-frame coil analytical model (Ken McClements, 2022) is used when + `i_tf_shape == 2` and gives approximate results (within ~10% of numerical). + - The routine sets an applicability flag when fitted-range assumptions are exceeded. """ - n = float(tfcoil_variables.n_tf_coils) - if tfcoil_variables.i_tf_sup == 1: + if i_tf_sup == 1: # Minimal inboard WP radius [m] - r_wp_min = ( - build_variables.r_tf_inboard_in + tfcoil_variables.dr_tf_nose_case - ) + r_wp_min = r_tf_wp_inboard_inner # Rectangular WP if tfcoil_variables.i_tf_wp_geom == 0: @@ -1559,55 +1592,36 @@ def plasma_outboard_edge_toroidal_ripple( # Double rectangle WP elif tfcoil_variables.i_tf_wp_geom == 1: - r_wp_max = r_wp_min + 0.5e0 * tfcoil_variables.dr_tf_wp_with_insulation + r_wp_max = r_tf_wp_inboard_centre # Trapezoidal WP elif tfcoil_variables.i_tf_wp_geom == 2: - r_wp_max = r_wp_min + tfcoil_variables.dr_tf_wp_with_insulation + r_wp_max = r_tf_wp_inboard_outer # Calculated maximum toroidal WP toroidal thickness [m] - if tfcoil_variables.tfc_sidewall_is_fraction: - t_wp_max = 2.0e0 * ( - (r_wp_max - tfcoil_variables.casths_fraction * r_wp_min) - * np.tan(np.pi / n) - - tfcoil_variables.dx_tf_wp_insulation - - tfcoil_variables.dx_tf_wp_insertion_gap - ) - else: - t_wp_max = 2.0e0 * ( - r_wp_max * np.tan(np.pi / n) - - tfcoil_variables.dx_tf_side_case_min - - tfcoil_variables.dx_tf_wp_insulation - - tfcoil_variables.dx_tf_wp_insertion_gap - ) + dx_tf_wp_conductor_max = dx_tf_wp_primary_toroidal - 2.0 * ( + dx_tf_wp_insulation + dx_tf_wp_insertion_gap + ) # Resistive magnet case else: - # Radius used to define the t_wp_max [m] - r_wp_max = ( - build_variables.r_tf_inboard_in - + tfcoil_variables.dr_tf_nose_case - + tfcoil_variables.dr_tf_wp_with_insulation - ) - + # Radius used to define the dx_tf_wp_conductor_max [m] + r_wp_max = r_tf_wp_inboard_outer # Calculated maximum toroidal WP toroidal thickness [m] - t_wp_max = 2.0e0 * r_wp_max * np.tan(np.pi / n) + dx_tf_wp_conductor_max = 2.0e0 * r_wp_max * np.tan(np.pi / n_tf_coils) flag = 0 - if tfcoil_variables.i_tf_shape == 2: + if i_tf_shape == 2: # Ken McClements ST picture frame coil analytical ripple calc # Calculated ripple for coil at r_tf_outboard_mid (%) - ripple = 100.0e0 * ( - (physics_variables.rmajor + physics_variables.rminor) - / r_tf_outboard_mid - ) ** (n) + ripple = 100.0e0 * ((rmajor + rminor) / r_tf_outboard_mid) ** (n_tf_coils) # Calculated r_tf_outboard_mid to produce a ripple of amplitude ripple_b_tf_plasma_edge_max - r_tf_outboard_midmin = ( - physics_variables.rmajor + physics_variables.rminor - ) / ((0.01e0 * ripple_b_tf_plasma_edge_max) ** (1.0e0 / n)) + r_tf_outboard_midmin = (rmajor + rminor) / ( + (0.01e0 * ripple_b_tf_plasma_edge_max) ** (1.0e0 / n_tf_coils) + ) else: # Winding pack to iter-coil at plasma centre toroidal lenth ratio - x = t_wp_max * n / physics_variables.rmajor + x = dx_tf_wp_conductor_max * n_tf_coils / rmajor # Fitting parameters c1 = 0.875e0 - 0.0557e0 * x @@ -1617,11 +1631,7 @@ def plasma_outboard_edge_toroidal_ripple( ripple = ( 100.0e0 * c1 - * ( - (physics_variables.rmajor + physics_variables.rminor) - / r_tf_outboard_mid - ) - ** (n - c2) + * ((rmajor + rminor) / r_tf_outboard_mid) ** (n_tf_coils - c2) ) # Calculated r_tf_outboard_mid to produce a ripple of amplitude ripple_b_tf_plasma_edge_max @@ -1634,9 +1644,9 @@ def plasma_outboard_edge_toroidal_ripple( logger.exception("base is <= 1e-6. Kludging to 1e-6.") base = 1e-6 - r_tf_outboard_midmin = ( - physics_variables.rmajor + physics_variables.rminor - ) / (base ** (1.0 / (n - c2))) + r_tf_outboard_midmin = (rmajor + rminor) / ( + base ** (1.0 / (n_tf_coils - c2)) + ) try: assert r_tf_outboard_midmin < np.inf @@ -1644,24 +1654,16 @@ def plasma_outboard_edge_toroidal_ripple( logger.exception( "r_tf_outboard_midmin is inf. Kludging to a large value instead." ) - r_tf_outboard_midmin = ( - physics_variables.rmajor + physics_variables.rminor - ) * 3 + r_tf_outboard_midmin = (rmajor + rminor) * 3 # Notify via flag if a range of applicability is violated flag = 0 if (x < 0.737e0) or (x > 2.95e0): flag = 1 - if (tfcoil_variables.n_tf_coils < 16) or (tfcoil_variables.n_tf_coils > 20): + if (n_tf_coils < 16) or (n_tf_coils > 20): flag = 2 - if ( - (physics_variables.rmajor + physics_variables.rminor) - / r_tf_outboard_mid - < 0.7e0 - ) or ( - (physics_variables.rmajor + physics_variables.rminor) - / r_tf_outboard_mid - > 0.8e0 + if ((rmajor + rminor) / r_tf_outboard_mid < 0.7e0) or ( + (rmajor + rminor) / r_tf_outboard_mid > 0.8e0 ): flag = 3 @@ -1951,8 +1953,19 @@ def calculate_radial_build(self, output: bool) -> None: r_tf_outboard_midl, build_variables.ripflag, ) = self.plasma_outboard_edge_toroidal_ripple( - tfcoil_variables.ripple_b_tf_plasma_edge_max, - build_variables.r_tf_outboard_mid, + ripple_b_tf_plasma_edge_max=tfcoil_variables.ripple_b_tf_plasma_edge_max, + r_tf_outboard_mid=build_variables.r_tf_outboard_mid, + n_tf_coils=tfcoil_variables.n_tf_coils, + rmajor=physics_variables.rmajor, + rminor=physics_variables.rminor, + r_tf_wp_inboard_inner=superconducting_tf_coil_variables.r_tf_wp_inboard_inner, + r_tf_wp_inboard_centre=superconducting_tf_coil_variables.r_tf_wp_inboard_centre, + r_tf_wp_inboard_outer=superconducting_tf_coil_variables.r_tf_wp_inboard_outer, + dx_tf_wp_primary_toroidal=tfcoil_variables.dx_tf_wp_primary_toroidal, + i_tf_shape=tfcoil_variables.i_tf_shape, + i_tf_sup=tfcoil_variables.i_tf_sup, + dx_tf_wp_insulation=tfcoil_variables.dx_tf_wp_insulation, + dx_tf_wp_insertion_gap=tfcoil_variables.dx_tf_wp_insertion_gap, ) # If the tfcoil_variables.ripple is too large then move the outboard TF coil leg @@ -1981,8 +1994,19 @@ def calculate_radial_build(self, output: bool) -> None: r_tf_outboard_midl, build_variables.ripflag, ) = self.plasma_outboard_edge_toroidal_ripple( - tfcoil_variables.ripple_b_tf_plasma_edge_max, - build_variables.r_tf_outboard_mid, + ripple_b_tf_plasma_edge_max=tfcoil_variables.ripple_b_tf_plasma_edge_max, + r_tf_outboard_mid=build_variables.r_tf_outboard_mid, + n_tf_coils=tfcoil_variables.n_tf_coils, + rmajor=physics_variables.rmajor, + rminor=physics_variables.rminor, + r_tf_wp_inboard_inner=superconducting_tf_coil_variables.r_tf_wp_inboard_inner, + r_tf_wp_inboard_centre=superconducting_tf_coil_variables.r_tf_wp_inboard_centre, + r_tf_wp_inboard_outer=superconducting_tf_coil_variables.r_tf_wp_inboard_outer, + dx_tf_wp_primary_toroidal=tfcoil_variables.dx_tf_wp_primary_toroidal, + i_tf_shape=tfcoil_variables.i_tf_shape, + i_tf_sup=tfcoil_variables.i_tf_sup, + dx_tf_wp_insulation=tfcoil_variables.dx_tf_wp_insulation, + dx_tf_wp_insertion_gap=tfcoil_variables.dx_tf_wp_insertion_gap, ) # Half-height of first wall (internal surface) diff --git a/process/data_structure/tfcoil_variables.py b/process/data_structure/tfcoil_variables.py index 9221a19d7c..d1d56ed36f 100644 --- a/process/data_structure/tfcoil_variables.py +++ b/process/data_structure/tfcoil_variables.py @@ -103,7 +103,10 @@ dx_tf_side_case_min: float = None -"""inboard TF coil sidewall case thickness (m) (calculated for stellarators)""" +"""inboard TF coil minimum sidewall case thickness (m) (calculated for stellarators)""" + +dx_tf_side_case_peak: float = None +"""inboard TF coil peak sidewall case thickness (m) (calculated for stellarators)""" casths_fraction: float = None @@ -1106,6 +1109,7 @@ def init_tfcoil_variables(): global f_dr_tf_plasma_case global i_f_dr_tf_plasma_case global dx_tf_side_case_min + global dx_tf_side_case_peak global casths_fraction global tfc_sidewall_is_fraction global t_conductor @@ -1322,6 +1326,7 @@ def init_tfcoil_variables(): f_dr_tf_plasma_case = 0.05 i_f_dr_tf_plasma_case = False dx_tf_side_case_min = 0.0 + dx_tf_side_case_peak = 0.0 casths_fraction = 0.06 t_conductor = 0.0 dx_tf_turn_cable_space_general = 0.0 diff --git a/process/io/plot_proc.py b/process/io/plot_proc.py index 91e55db7d6..286d096c11 100644 --- a/process/io/plot_proc.py +++ b/process/io/plot_proc.py @@ -34,6 +34,7 @@ import process.data_structure.pfcoil_variables as pfcoil_variables import process.io.mfile as mf import process.superconducting_tf_coil as sctf +from process.build import Build from process.data_structure import physics_variables from process.geometry.blanket_geometry import ( blanket_geometry_double_null, @@ -3011,6 +3012,7 @@ def plot_main_plasma_information( textstr_fields = ( f"$\\mathbf{{Magnetic\\ fields:}}$\n\n" f"Toroidal field at $R_0$, $B_{{T}}$: {mfile_data.data['b_plasma_toroidal_on_axis'].get_scan(scan):.4f} T \n" + f" Ripple at outboard , $\\delta$: {mfile_data.data['ripple_b_tf_plasma_edge'].get_scan(scan):.2f}% \n" f"Average poloidal field, $B_{{p}}$: {mfile_data.data['b_plasma_poloidal_average'].get_scan(scan):.4f} T \n" f"Total field, $B_{{tot}}$: {mfile_data.data['b_plasma_total'].get_scan(scan):.4f} T \n" f"Vertical field, $B_{{vert}}$: {mfile_data.data['b_plasma_vertical_required'].get_scan(scan):.4f} T" @@ -4945,6 +4947,7 @@ def plot_superconducting_tf_wp(axis, mfile_data, scan: int, fig) -> None: dx_tf_wp_primary_toroidal = mfile_data.data["dx_tf_wp_primary_toroidal"].get_scan( scan ) + dx_tf_side_case_peak = mfile_data.data["dx_tf_side_case_peak"].get_scan(scan) dx_tf_wp_secondary_toroidal = mfile_data.data[ "dx_tf_wp_secondary_toroidal" ].get_scan(scan) @@ -5444,6 +5447,22 @@ def plot_superconducting_tf_wp(axis, mfile_data, scan: int, fig) -> None: linewidth=0.6, alpha=0.5, ) + # Max toroidal width including side case + axis.axhline( + y=(dx_tf_wp_primary_toroidal / 2) + dx_tf_side_case_peak, + color="black", + linestyle="--", + linewidth=0.6, + alpha=0.5, + ) + + axis.axhline( + y=-(dx_tf_wp_primary_toroidal / 2) - dx_tf_side_case_peak, + color="black", + linestyle="--", + linewidth=0.6, + alpha=0.5, + ) axis.axvline( x=r_tf_inboard_in, @@ -5499,11 +5518,12 @@ def plot_superconducting_tf_wp(axis, mfile_data, scan: int, fig) -> None: f"$A$: {mfile_data.data['a_tf_plasma_case'].get_scan(scan):.3f} $\\mathrm{{m}}^2$\n\n" f"$\\text{{Side Case:}}$\n" f"Minimum $\\Delta r$: {mfile_data.data['dx_tf_side_case_min'].get_scan(scan):.3f} m\n" - f"Average $\\Delta r$: {mfile_data.data['dx_tf_side_case_average'].get_scan(scan):.3f} m" + f"Average $\\Delta r$: {mfile_data.data['dx_tf_side_case_average'].get_scan(scan):.3f} m\n" + f"Max $\\Delta r$: {mfile_data.data['dx_tf_side_case_peak'].get_scan(scan):.3f} m" ) axis.text( 0.55, - 0.95, + 0.975, textstr_casing, fontsize=9, verticalalignment="top", @@ -11729,6 +11749,323 @@ def plot_beta_profiles(axis, mfile_data, scan): axis.set_ylim(bottom=0.0) +def plot_plasma_outboard_toroidal_ripple_map( + fig, mfile_data: mf.MFile, scan: int +) -> None: + r_tf_outboard_mid = mfile_data.data["r_tf_outboard_mid"].get_scan(scan) + n_tf_coils = mfile_data.data["n_tf_coils"].get_scan(scan) + rmajor = mfile_data.data["rmajor"].get_scan(scan) + rminor = mfile_data.data["rminor"].get_scan(scan) + r_tf_wp_inboard_inner = mfile_data.data["r_tf_wp_inboard_inner"].get_scan(scan) + r_tf_wp_inboard_centre = mfile_data.data["r_tf_wp_inboard_centre"].get_scan(scan) + r_tf_wp_inboard_outer = mfile_data.data["r_tf_wp_inboard_outer"].get_scan(scan) + dx_tf_wp_primary_toroidal = mfile_data.data["dx_tf_wp_primary_toroidal"].get_scan( + scan + ) + i_tf_shape = mfile_data.data["i_tf_shape"].get_scan(scan) + i_tf_sup = mfile_data.data["i_tf_sup"].get_scan(scan) + dx_tf_wp_insulation = mfile_data.data["dx_tf_wp_insulation"].get_scan(scan) + dx_tf_wp_insertion_gap = mfile_data.data["dx_tf_wp_insertion_gap"].get_scan(scan) + ripple_b_tf_plasma_edge_max = mfile_data.data[ + "ripple_b_tf_plasma_edge_max" + ].get_scan(scan) + + build = Build() + + r_nom = r_tf_outboard_mid + dx_nom = dx_tf_wp_primary_toroidal if dx_tf_wp_primary_toroidal is not None else 0.0 + + # Simple ±20% scan around nominal values for r and dx + r_min = r_nom * 0.9 + r_max = r_nom * 1.1 + + if dx_nom > 0: + dx_min = dx_nom * 0.8 + dx_max = dx_nom * 1.2 + else: + # fallback sensible small range if nominal is zero + dx_min = 1e-3 + dx_max = 1e-2 + + n_r = 50 + n_dx = 50 + r_vals = np.linspace(r_min, r_max, n_r) + dx_vals = np.linspace(dx_min, dx_max, n_dx) + + rg, dxg = np.meshgrid(r_vals, dx_vals) + + # prepare metric array to hold ripple metric for each (r, dx) pair + metric = np.full(rg.shape, np.nan, dtype=float) + + for ii in range(rg.shape[0]): + for jj in range(rg.shape[1]): + r_test = float(rg[ii, jj]) + dx_test = float(dxg[ii, jj]) + + try: + rip, _, _ = build.plasma_outboard_edge_toroidal_ripple( + ripple_b_tf_plasma_edge_max=0.05, + r_tf_outboard_mid=r_test, + n_tf_coils=int(n_tf_coils), + rmajor=rmajor, + rminor=rminor, + r_tf_wp_inboard_inner=r_tf_wp_inboard_inner, + r_tf_wp_inboard_centre=r_tf_wp_inboard_centre, + r_tf_wp_inboard_outer=r_tf_wp_inboard_outer, + dx_tf_wp_primary_toroidal=dx_test, + i_tf_shape=i_tf_shape, + i_tf_sup=i_tf_sup, + dx_tf_wp_insulation=dx_tf_wp_insulation, + dx_tf_wp_insertion_gap=dx_tf_wp_insertion_gap, + ) + except (ValueError, ZeroDivisionError, OverflowError, TypeError): + # Only catch expected numeric/validation errors from the ripple calculation; + # let other exceptions propagate so they can be diagnosed. + rip = np.nan + metric[ii, jj] = rip + + # Create two subplots that share the same x axis + ax1 = fig.add_subplot(2, 1, 1) + ax2 = fig.add_subplot(2, 1, 2, sharex=ax1) + + # Make contour plot of the ripple metric (r vs dx) on ax1 + if np.all(np.isnan(metric)): + ax1.text(0.5, 0.5, "No valid ripple data (r vs dx)", ha="center", va="center") + else: + vmin = np.nanmin(metric) + vmax = np.nanmax(metric) + + # Guard against degenerate range + if np.isclose(vmin, vmax, atol=1e-12) or np.isnan(vmin) or np.isnan(vmax): + vmin = vmin - 0.25 + vmax = vmax + 0.25 + + # Smooth filled contour levels + levels = np.linspace(vmin, vmax, 50) + cf = ax1.contourf(rg, dxg, metric, levels=levels, cmap="plasma", extend="both") + + # Contour lines only at 0.5 increments + step = 0.5 + start = np.floor(vmin / step) * step + end = np.ceil(vmax / step) * step + contour_levels = np.arange(start, end + 1e-12, step) + + # Fallback if contour_levels is empty for some reason + if contour_levels.size < 2: + contour_levels = np.array([vmin, vmax]) + + contours = ax1.contour( + rg, + dxg, + metric, + levels=contour_levels, + colors="k", + linewidths=0.5, + alpha=0.7, + ) + ax1.clabel(contours, inline=True, fontsize=8, fmt="%.2f%%", colors="white") + # Overlay contour line at the specified target ripple value + + target = float(ripple_b_tf_plasma_edge_max) + + if target is not None and not np.isnan(target): + # Check if target lies within computed metric range + if (target >= vmin) and (target <= vmax): + c_target = ax1.contour( + rg, + dxg, + metric, + levels=[target], + colors="white", + linewidths=2.0, + linestyles="--", + zorder=20, + ) + ax1.clabel( + c_target, + inline=True, + fmt={target: f"Input Max {target:.2f}%"}, + fontsize=8, + colors="white", + ) + else: + # annotate that target is outside plotted range + ax1.text( + 0.02, + 0.98, + f"Target ripple {target:.2f}% outside plot range [{vmin:.2f},{vmax:.2f}]", + transform=ax1.transAxes, + color="white", + fontsize=8, + va="top", + bbox={"facecolor": "black", "alpha": 0.6, "pad": 2}, + ) + + # Colourbar with 0.5 increments (use the same contour_levels as for the contour lines) + ticks = contour_levels + # Fallback to sensible ticks if contour_levels is not appropriate + if ticks.size == 0 or np.isnan(ticks).all(): + ticks = np.linspace(vmin, vmax, 5) + cb = ax1.figure.colorbar( + cf, ax=ax1, label="Plasma Outboard Toroidal Ripple", ticks=ticks + ) + cb.ax.set_yticklabels([f"{t:.2f}%" for t in ticks]) + + # mark nominal point + ax1.scatter( + [r_nom], + [dx_nom], + color="white", + edgecolor="black", + s=200, + linewidths=1.5, + marker="o", + zorder=10, + label="Design Point", + ) + ax1.set_xlabel("Outboard TF leg centre [m]") + ax1.set_ylabel("WP Toroidal Width [m]") + ax1.legend(loc="upper right") + + # --------------------------------------------------------------------- + # Second plot: scan number of TF coils vs r_tf_outboard_mid (keep dx at nominal) + # --------------------------------------------------------------------- + # Determine a sensible integer range of TF coils to scan around nominal + n_nom = int(n_tf_coils) + span = max(2, int(min(12, n_nom // 2))) # choose a span based on nominal + n_min = max(12, n_nom - span) + n_max = n_nom + span + n_vals = np.arange(n_min, n_max + 1, dtype=int) + + n_r2 = 60 + r_vals2 = np.linspace(r_min, r_max, n_r2) + rg2, ng2 = np.meshgrid(r_vals2, n_vals) + + metric2 = np.full(rg2.shape, np.nan, dtype=float) + + for ii in range(rg2.shape[0]): + for jj in range(rg2.shape[1]): + r_test = float(rg2[ii, jj]) + n_test = int(ng2[ii, jj]) + try: + rip, _, _ = build.plasma_outboard_edge_toroidal_ripple( + ripple_b_tf_plasma_edge_max=0.05, + r_tf_outboard_mid=r_test, + n_tf_coils=n_test, + rmajor=rmajor, + rminor=rminor, + r_tf_wp_inboard_inner=r_tf_wp_inboard_inner, + r_tf_wp_inboard_centre=r_tf_wp_inboard_centre, + r_tf_wp_inboard_outer=r_tf_wp_inboard_outer, + dx_tf_wp_primary_toroidal=dx_nom, + i_tf_shape=i_tf_shape, + i_tf_sup=i_tf_sup, + dx_tf_wp_insulation=dx_tf_wp_insulation, + dx_tf_wp_insertion_gap=dx_tf_wp_insertion_gap, + ) + except (ValueError, ZeroDivisionError, OverflowError, TypeError): + # Only catch expected numeric/validation errors from the ripple calculation; + # let other exceptions propagate so they can be diagnosed. + rip = np.nan + metric2[ii, jj] = rip + + # Plot the second metric on the bottom axes (ax2) so it shares x-axis with ax1 + if np.all(np.isnan(metric2)): + ax2.text( + 0.5, 0.5, "No valid ripple data (r vs n_tf_coils)", ha="center", va="center" + ) + else: + vmin2 = np.nanmin(metric2) + vmax2 = np.nanmax(metric2) + + # filled contour levels (smooth shading) + levels2 = np.linspace(vmin2, vmax2, 40) + cf2 = ax2.contourf( + rg2, ng2, metric2, levels=levels2, cmap="viridis", extend="both" + ) + + # contour lines only at 0.5 steps + step = 0.5 + start = np.floor(vmin2 / step) * step + end = np.ceil(vmax2 / step) * step + contour_levels = np.arange(start, end + 1e-12, step) + + # fallback if arange returned empty (very small range) + if contour_levels.size == 0: + contour_levels = np.array([vmin2, vmax2]) + + contours2 = ax2.contour( + rg2, + ng2, + metric2, + levels=contour_levels, + colors="k", + linewidths=0.5, + alpha=0.7, + ) + ax2.clabel(contours2, inline=True, fontsize=8, fmt="%.2f%%", colors="white") + + target2 = float(ripple_b_tf_plasma_edge_max) + + if target2 is not None and not np.isnan(target2): + if (target2 >= vmin2) and (target2 <= vmax2): + c_target2 = ax2.contour( + rg2, + ng2, + metric2, + levels=[target2], + colors="white", + linewidths=2.0, + linestyles="--", + zorder=20, + ) + ax2.clabel( + c_target2, + inline=True, + fmt={target2: f"Input Max {target2:.2f}%"}, + fontsize=8, + colors="white", + ) + else: + ax2.text( + 0.02, + 0.98, + f"Target ripple {target2:.2f}% outside plot range [{vmin2:.2f},{vmax2:.2f}]", + transform=ax2.transAxes, + color="white", + fontsize=8, + va="top", + bbox={"facecolor": "black", "alpha": 0.6, "pad": 2}, + ) + # colorbar with 0.5 increments + # ensure contour_levels exists and is in 0.5 steps (constructed above) + ticks = contour_levels + cb2 = ax2.figure.colorbar( + cf2, ax=ax2, label="Plasma Outboard Toroidal Ripple", ticks=ticks + ) + cb2.ax.set_yticklabels([f"{t:.2f}%" for t in ticks]) + + # nominal markers + ax2.scatter( + [r_nom], + [n_nom], + color="white", + edgecolor="black", + s=300, + linewidths=1.5, + marker="o", + zorder=10, + label="Design Point", + ) + ax2.set_xlabel("Outboard TF leg centre [m]") + ax2.set_ylabel("Number of TF coils") + ax2.set_yticks(n_vals) + ax2.legend(loc="upper right") + + # Improve layout + fig.tight_layout() + + def main_plot( fig0, fig1, @@ -11754,6 +12091,7 @@ def main_plot( fig21, fig22, fig23, + fig24, m_file_data, scan, imp="../data/lz_non_corona_14_elements/", @@ -11957,30 +12295,32 @@ def main_plot( fig15.add_subplot(111, aspect="equal"), m_file_data, scan, colour_scheme ) - axes = fig16.subplots(nrows=3, ncols=1, sharex=True).flatten() + plot_plasma_outboard_toroidal_ripple_map(fig16, m_file_data, scan) + + axes = fig17.subplots(nrows=3, ncols=1, sharex=True).flatten() plot_tf_stress(axes) - plot_bootstrap_comparison(fig17.add_subplot(221), m_file_data, scan) - plot_h_threshold_comparison(fig17.add_subplot(224), m_file_data, scan) - plot_density_limit_comparison(fig18.add_subplot(221), m_file_data, scan) - plot_confinement_time_comparison(fig18.add_subplot(224), m_file_data, scan) - plot_current_profiles_over_time(fig19.add_subplot(111), m_file_data, scan) + plot_bootstrap_comparison(fig18.add_subplot(221), m_file_data, scan) + plot_h_threshold_comparison(fig18.add_subplot(224), m_file_data, scan) + plot_density_limit_comparison(fig19.add_subplot(221), m_file_data, scan) + plot_confinement_time_comparison(fig19.add_subplot(224), m_file_data, scan) + plot_current_profiles_over_time(fig20.add_subplot(111), m_file_data, scan) plot_cs_coil_structure( - fig20.add_subplot(121, aspect="equal"), fig20, m_file_data, scan + fig21.add_subplot(121, aspect="equal"), fig21, m_file_data, scan ) plot_cs_turn_structure( - fig20.add_subplot(224, aspect="equal"), fig20, m_file_data, scan + fig21.add_subplot(224, aspect="equal"), fig21, m_file_data, scan ) plot_first_wall_top_down_cross_section( - fig21.add_subplot(221, aspect="equal"), m_file_data, scan + fig22.add_subplot(221, aspect="equal"), m_file_data, scan ) - plot_first_wall_poloidal_cross_section(fig21.add_subplot(122), m_file_data, scan) - plot_fw_90_deg_pipe_bend(fig21.add_subplot(337), m_file_data, scan) + plot_first_wall_poloidal_cross_section(fig22.add_subplot(122), m_file_data, scan) + plot_fw_90_deg_pipe_bend(fig22.add_subplot(337), m_file_data, scan) - plot_blkt_pipe_bends(fig22, m_file_data, scan) - ax_blanket = fig22.add_subplot(122, aspect="equal") + plot_blkt_pipe_bends(fig23, m_file_data, scan) + ax_blanket = fig23.add_subplot(122, aspect="equal") plot_blanket(ax_blanket, m_file_data, scan, colour_scheme) plot_firstwall(ax_blanket, m_file_data, scan, colour_scheme) ax_blanket.set_xlabel("Radial position [m]") @@ -12023,7 +12363,7 @@ def main_plot( ) plot_main_power_flow( - fig23.add_subplot(111, aspect="equal"), m_file_data, scan, fig23 + fig24.add_subplot(111, aspect="equal"), m_file_data, scan, fig24 ) @@ -12341,6 +12681,7 @@ def main(args=None): page21 = plt.figure(figsize=(12, 9), dpi=80) page22 = plt.figure(figsize=(12, 9), dpi=80) page23 = plt.figure(figsize=(12, 9), dpi=80) + page24 = plt.figure(figsize=(12, 9), dpi=80) # run main_plot main_plot( @@ -12368,6 +12709,7 @@ def main(args=None): page21, page22, page23, + page24, m_file, scan=scan, demo_ranges=demo_ranges, @@ -12400,6 +12742,7 @@ def main(args=None): pdf.savefig(page21) pdf.savefig(page22) pdf.savefig(page23) + pdf.savefig(page24) # show fig if option used if args.show: @@ -12429,6 +12772,7 @@ def main(args=None): plt.close(page21) plt.close(page22) plt.close(page23) + plt.close(page24) if __name__ == "__main__": diff --git a/process/superconducting_tf_coil.py b/process/superconducting_tf_coil.py index 2eb6c4cf0c..0d4fbe3794 100644 --- a/process/superconducting_tf_coil.py +++ b/process/superconducting_tf_coil.py @@ -1950,6 +1950,7 @@ def sc_tf_internal_geom(self, i_tf_wp_geom, i_tf_case_geom, i_tf_turns_integer): superconducting_tf_coil_variables.a_tf_plasma_case, superconducting_tf_coil_variables.a_tf_coil_nose_case, superconducting_tf_coil_variables.dx_tf_side_case_average, + superconducting_tf_coil_variables.dx_tf_side_case_peak, ) = self.superconducting_tf_case_geometry( i_tf_case_geom=i_tf_case_geom, i_tf_wp_geom=i_tf_wp_geom, @@ -2392,7 +2393,7 @@ def superconducting_tf_case_geometry( r_tf_inboard_in: float, dx_tf_side_case_min: float, dr_tf_wp_with_insulation: float, - ) -> tuple[float, float, float, float, float]: + ) -> tuple[float, float, float, float, float, float]: """ Setting the case geometry and area for SC magnets @@ -2433,6 +2434,7 @@ def superconducting_tf_case_geometry( - a_tf_plasma_case (float): Front casing area [m²]. - a_tf_coil_nose_case (float): Nose casing area [m²]. - dx_tf_side_case_average (float): Average lateral casing thickness [m]. + - dx_tf_side_case_peak (float): Peak lateral casing thickness [m]. :rtype: tuple[float, float, float, float, float] :raises: Reports error if calculated casing areas are negative. @@ -2489,12 +2491,32 @@ def superconducting_tf_case_geometry( else: dx_tf_side_case_average = dx_tf_side_case_min + # Peak lateral casing thickness [m] + # -------------- + # Rectangular casing + + if i_tf_wp_geom == 0: + dx_tf_side_case_peak = ( + dx_tf_side_case_min + tan_theta_coil * dr_tf_wp_with_insulation + ) + # Double rectangular WP + elif i_tf_wp_geom == 1: + dx_tf_side_case_peak = ( + dx_tf_side_case_min + 0.5 * tan_theta_coil * dr_tf_wp_with_insulation + ) + + # Trapezoidal WP + # Constant thickness so min = average + else: + dx_tf_side_case_peak = dx_tf_side_case_min + return ( a_tf_coil_inboard_case, a_tf_coil_outboard_case, a_tf_plasma_case, a_tf_coil_nose_case, dx_tf_side_case_average, + dx_tf_side_case_peak, ) def tf_cable_in_conduit_integer_turn_geometry( diff --git a/process/tf_coil.py b/process/tf_coil.py index 5fc89ae424..9927b2e541 100644 --- a/process/tf_coil.py +++ b/process/tf_coil.py @@ -952,6 +952,12 @@ def outtf(self): "(dx_tf_side_case_average)", superconducting_tf_coil_variables.dx_tf_side_case_average, ) + po.ovarre( + self.outfile, + "Inboard leg case sidewall peak thickness (m)", + "(dx_tf_side_case_peak)", + superconducting_tf_coil_variables.dx_tf_side_case_peak, + ) po.ovarre( self.outfile, "External case mass per coil (kg)", diff --git a/tests/unit/test_build.py b/tests/unit/test_build.py index 6abd4fa73c..b301f4f96e 100644 --- a/tests/unit/test_build.py +++ b/tests/unit/test_build.py @@ -7,7 +7,6 @@ build_variables, divertor_variables, physics_variables, - tfcoil_variables, ) @@ -57,46 +56,6 @@ class DivgeomParam(NamedTuple): expected_divht: Any = None -class RippleAmplitudeParam(NamedTuple): - rminor: Any = None - - rmajor: Any = None - - dx_tf_wp_insulation: Any = None - - n_tf_coils: Any = None - - dx_tf_inboard_out_toroidal: Any = None - - dx_tf_side_case_min: Any = None - - dr_tf_wp_with_insulation: Any = None - - dr_tf_nose_case: Any = None - - casths_fraction: Any = None - - i_tf_sup: Any = None - - i_tf_wp_geom: Any = None - - dx_tf_wp_insertion_gap: Any = None - - tfc_sidewall_is_fraction: Any = None - - r_tf_inboard_in: Any = None - - ripple_b_tf_plasma_edge_max: Any = None - - r_tf_outboard_mid: Any = None - - expected_ripple: Any = None - - expected_r_tf_outboard_midmin: Any = None - - expected_flag: Any = None - - @pytest.mark.parametrize( "divgeomparam", ( @@ -193,143 +152,6 @@ def test_divgeom(divgeomparam, monkeypatch, build): assert divht == pytest.approx(divgeomparam.expected_divht) -@pytest.mark.parametrize( - "rippleamplitudeparam", - ( - RippleAmplitudeParam( - rminor=2.8677741935483869, - rmajor=8.8901000000000003, - dx_tf_wp_insulation=0.0080000000000000019, - n_tf_coils=16, - dx_tf_inboard_out_toroidal=1, - dx_tf_side_case_min=0.05000000000000001, - dr_tf_wp_with_insulation=0.54261087836601019, - dr_tf_nose_case=0.52465000000000006, - casths_fraction=0.059999999999999998, - i_tf_sup=1, - i_tf_wp_geom=0, - dx_tf_wp_insertion_gap=0.01, - tfc_sidewall_is_fraction=False, - r_tf_inboard_in=2.9939411851091102, - ripple_b_tf_plasma_edge_max=0.60000000000000009, - r_tf_outboard_mid=14.988874193548387, - expected_ripple=2.3850014198003961, - expected_r_tf_outboard_midmin=16.519405859443332, - expected_flag=0, - ), - RippleAmplitudeParam( - rminor=2.8677741935483869, - rmajor=8.8901000000000003, - dx_tf_wp_insulation=0.0080000000000000019, - n_tf_coils=16, - dx_tf_inboard_out_toroidal=1, - dx_tf_side_case_min=0.05000000000000001, - dr_tf_wp_with_insulation=0.54261087836601019, - dr_tf_nose_case=0.52465000000000006, - casths_fraction=0.059999999999999998, - i_tf_sup=1, - i_tf_wp_geom=0, - dx_tf_wp_insertion_gap=0.01, - tfc_sidewall_is_fraction=False, - r_tf_inboard_in=2.9939411851091102, - ripple_b_tf_plasma_edge_max=0.60000000000000009, - r_tf_outboard_mid=16.519405859443332, - expected_ripple=0.59999999999999987, - expected_r_tf_outboard_midmin=16.519405859443332, - expected_flag=0, - ), - ), -) -def test_ripple_amplitude(rippleamplitudeparam, monkeypatch, build): - """ - Automatically generated Regression Unit Test for ripple_amplitude. - - This test was generated using data from tracking/baseline_2018/baseline_2018_IN.DAT. - - :param rippleamplitudeparam: the data used to mock and assert in this test. - :type rippleamplitudeparam: rippleamplitudeparam - - :param monkeypatch: pytest fixture used to mock module/class variables - :type monkeypatch: _pytest.monkeypatch.monkeypatch - - :param build: fixture containing an initialised `Build` object - :type build: tests.unit.test_build.build (functional fixture) - """ - - monkeypatch.setattr(physics_variables, "rminor", rippleamplitudeparam.rminor) - - monkeypatch.setattr(physics_variables, "rmajor", rippleamplitudeparam.rmajor) - - monkeypatch.setattr( - tfcoil_variables, - "dx_tf_wp_insulation", - rippleamplitudeparam.dx_tf_wp_insulation, - ) - - monkeypatch.setattr(tfcoil_variables, "n_tf_coils", rippleamplitudeparam.n_tf_coils) - - monkeypatch.setattr( - tfcoil_variables, - "dx_tf_inboard_out_toroidal", - rippleamplitudeparam.dx_tf_inboard_out_toroidal, - ) - - monkeypatch.setattr( - tfcoil_variables, - "dx_tf_side_case_min", - rippleamplitudeparam.dx_tf_side_case_min, - ) - - monkeypatch.setattr( - tfcoil_variables, - "dr_tf_wp_with_insulation", - rippleamplitudeparam.dr_tf_wp_with_insulation, - ) - - monkeypatch.setattr( - tfcoil_variables, "dr_tf_nose_case", rippleamplitudeparam.dr_tf_nose_case - ) - - monkeypatch.setattr( - tfcoil_variables, "casths_fraction", rippleamplitudeparam.casths_fraction - ) - - monkeypatch.setattr(tfcoil_variables, "i_tf_sup", rippleamplitudeparam.i_tf_sup) - - monkeypatch.setattr( - tfcoil_variables, "i_tf_wp_geom", rippleamplitudeparam.i_tf_wp_geom - ) - - monkeypatch.setattr( - tfcoil_variables, - "dx_tf_wp_insertion_gap", - rippleamplitudeparam.dx_tf_wp_insertion_gap, - ) - - monkeypatch.setattr( - tfcoil_variables, - "tfc_sidewall_is_fraction", - rippleamplitudeparam.tfc_sidewall_is_fraction, - ) - - monkeypatch.setattr( - build_variables, "r_tf_inboard_in", rippleamplitudeparam.r_tf_inboard_in - ) - - ripple, r_tf_outboard_midmin, flag = build.plasma_outboard_edge_toroidal_ripple( - ripple_b_tf_plasma_edge_max=rippleamplitudeparam.ripple_b_tf_plasma_edge_max, - r_tf_outboard_mid=rippleamplitudeparam.r_tf_outboard_mid, - ) - - assert ripple == pytest.approx(rippleamplitudeparam.expected_ripple) - - assert r_tf_outboard_midmin == pytest.approx( - rippleamplitudeparam.expected_r_tf_outboard_midmin - ) - - assert flag == pytest.approx(rippleamplitudeparam.expected_flag) - - class PortszParam(NamedTuple): r_tf_outboard_mid: Any = None @@ -420,3 +242,168 @@ def test_calculate_beam_port_size(portszparam, build): assert radius_beam_tangency_max == pytest.approx( portszparam.expected_radius_beam_tangency_max ) + + +class PlasmaRippleParam(NamedTuple): + ripple_b_tf_plasma_edge_max: Any = None + r_tf_outboard_mid: Any = None + n_tf_coils: Any = None + rmajor: Any = None + rminor: Any = None + r_tf_wp_inboard_inner: Any = None + r_tf_wp_inboard_centre: Any = None + r_tf_wp_inboard_outer: Any = None + dx_tf_wp_primary_toroidal: Any = None + i_tf_shape: Any = None + i_tf_sup: Any = None + dx_tf_wp_insulation: Any = None + dx_tf_wp_insertion_gap: Any = None + + +@pytest.mark.parametrize( + "param", + ( + # Picture-frame analytical model (i_tf_shape == 2) + PlasmaRippleParam( + ripple_b_tf_plasma_edge_max=0.6, + r_tf_outboard_mid=14.988874193548387, + n_tf_coils=16, + rmajor=8.8901000000000003, + rminor=2.8677741935483869, + r_tf_wp_inboard_inner=2.9939411851091102, + r_tf_wp_inboard_centre=3.0939411851091102, + r_tf_wp_inboard_outer=3.1939411851091102, + dx_tf_wp_primary_toroidal=0.8, + i_tf_shape=2, + i_tf_sup=1, + dx_tf_wp_insulation=0.008, + dx_tf_wp_insertion_gap=0.01, + ), + # Fitted-range diagnostic: small coil width X -> should set flag = 1 + PlasmaRippleParam( + ripple_b_tf_plasma_edge_max=0.6, + r_tf_outboard_mid=10.0, + n_tf_coils=16, + rmajor=8.8901000000000003, + rminor=2.8677741935483869, + r_tf_wp_inboard_inner=0.6, + r_tf_wp_inboard_centre=0.7, + r_tf_wp_inboard_outer=0.8, + dx_tf_wp_primary_toroidal=0.01, # very small -> x << 0.737 + i_tf_shape=0, + i_tf_sup=1, + dx_tf_wp_insulation=0.0, + dx_tf_wp_insertion_gap=0.0, + ), + # Additional picture-frame cases: different coil counts and toroidal thicknesses + PlasmaRippleParam( + ripple_b_tf_plasma_edge_max=0.6, + r_tf_outboard_mid=13.5, + n_tf_coils=8, + rmajor=8.8901000000000003, + rminor=2.8677741935483869, + r_tf_wp_inboard_inner=3.0, + r_tf_wp_inboard_centre=3.1, + r_tf_wp_inboard_outer=3.2, + dx_tf_wp_primary_toroidal=0.5, + i_tf_shape=2, + i_tf_sup=1, + dx_tf_wp_insulation=0.01, + dx_tf_wp_insertion_gap=0.02, + ), + PlasmaRippleParam( + ripple_b_tf_plasma_edge_max=0.6, + r_tf_outboard_mid=17.0, + n_tf_coils=24, + rmajor=8.8901000000000003, + rminor=2.8677741935483869, + r_tf_wp_inboard_inner=3.5, + r_tf_wp_inboard_centre=3.6, + r_tf_wp_inboard_outer=3.7, + dx_tf_wp_primary_toroidal=1.2, + i_tf_shape=2, + i_tf_sup=1, + dx_tf_wp_insulation=0.02, + dx_tf_wp_insertion_gap=0.01, + ), + # Same coil count as baseline but very thin toroidal WP to check sensitivity + PlasmaRippleParam( + ripple_b_tf_plasma_edge_max=0.6, + r_tf_outboard_mid=14.988874193548387, + n_tf_coils=16, + rmajor=8.8901000000000003, + rminor=2.8677741935483869, + r_tf_wp_inboard_inner=2.9, + r_tf_wp_inboard_centre=3.0, + r_tf_wp_inboard_outer=3.1, + dx_tf_wp_primary_toroidal=0.05, + i_tf_shape=2, + i_tf_sup=1, + dx_tf_wp_insulation=0.0, + dx_tf_wp_insertion_gap=0.0, + ), + # Another fitted-range diagnostic case with different coil number and small X + PlasmaRippleParam( + ripple_b_tf_plasma_edge_max=0.6, + r_tf_outboard_mid=9.5, + n_tf_coils=12, + rmajor=8.8901000000000003, + rminor=2.8677741935483869, + r_tf_wp_inboard_inner=0.7, + r_tf_wp_inboard_centre=0.8, + r_tf_wp_inboard_outer=0.9, + dx_tf_wp_primary_toroidal=0.02, + i_tf_shape=0, + i_tf_sup=1, + dx_tf_wp_insulation=0.0, + dx_tf_wp_insertion_gap=0.0, + ), + ), +) +def test_plasma_outboard_edge_toroidal_ripple_additional(param, build): + """ + Additional unit tests for plasma_outboard_edge_toroidal_ripple. + + - First case exercises the picture-frame analytical branch (i_tf_shape == 2) + and checks returned ripple and r_tf_outboard_midmin against the analytical formula. + - Second case forces the fitted-range diagnostic (x out of range) to ensure the + applicability flag is set (flag == 1) and results remain finite/positive. + - Additional cases vary coil counts (n_tf_coils) and toroidal WP thickness + (dx_tf_wp_primary_toroidal) to cover more branches and sensitivities. + """ + + ripple, r_tf_outboard_midmin, flag = build.plasma_outboard_edge_toroidal_ripple( + ripple_b_tf_plasma_edge_max=param.ripple_b_tf_plasma_edge_max, + r_tf_outboard_mid=param.r_tf_outboard_mid, + n_tf_coils=param.n_tf_coils, + rmajor=param.rmajor, + rminor=param.rminor, + r_tf_wp_inboard_inner=param.r_tf_wp_inboard_inner, + r_tf_wp_inboard_centre=param.r_tf_wp_inboard_centre, + r_tf_wp_inboard_outer=param.r_tf_wp_inboard_outer, + dx_tf_wp_primary_toroidal=param.dx_tf_wp_primary_toroidal, + i_tf_shape=param.i_tf_shape, + i_tf_sup=param.i_tf_sup, + dx_tf_wp_insulation=param.dx_tf_wp_insulation, + dx_tf_wp_insertion_gap=param.dx_tf_wp_insertion_gap, + ) + + if param.i_tf_shape == 2: + # Analytical expected values for picture-frame model + expected_ripple = 100.0 * ( + (param.rmajor + param.rminor) / param.r_tf_outboard_mid + ) ** (param.n_tf_coils) + expected_r_min = (param.rmajor + param.rminor) / ( + (0.01 * param.ripple_b_tf_plasma_edge_max) ** (1.0 / param.n_tf_coils) + ) + + assert ripple == pytest.approx(expected_ripple) + assert r_tf_outboard_midmin == pytest.approx(expected_r_min) + assert flag == 0 + else: + # Expect the fitted-range diagnostic to trigger for very small coil-width X + # (existing tests use flag == 3 for that diagnostic; keep the same expectation) + assert flag == 3 + # Results should be finite and positive + assert ripple > 0.0 + assert r_tf_outboard_midmin > 0.0 diff --git a/tests/unit/test_sctfcoil.py b/tests/unit/test_sctfcoil.py index 1721252803..993f476d14 100644 --- a/tests/unit/test_sctfcoil.py +++ b/tests/unit/test_sctfcoil.py @@ -887,6 +887,8 @@ class TfCaseGeomParam(NamedTuple): expected_dx_tf_side_case_average: Any = None + expected_dx_tf_side_case_peak: Any = None + expected_a_tf_plasma_case: Any = None expected_a_tf_coil_nose_case: Any = None @@ -914,6 +916,7 @@ class TfCaseGeomParam(NamedTuple): expected_a_tf_coil_inboard_case=1.0015169239205168, expected_a_tf_coil_outboard_case=1.2752592893394648, expected_dx_tf_side_case_average=0.10396600719086938, + expected_dx_tf_side_case_peak=0.15793201438173876, expected_a_tf_plasma_case=0.18607458590131154, expected_a_tf_coil_nose_case=0.70261616505511615, ), @@ -936,6 +939,7 @@ class TfCaseGeomParam(NamedTuple): expected_a_tf_coil_inboard_case=1.0015169239205168, expected_a_tf_coil_outboard_case=1.2752592893394648, expected_dx_tf_side_case_average=0.10396600719086938, + expected_dx_tf_side_case_peak=0.15793201438173876, expected_a_tf_plasma_case=0.18607458590131154, expected_a_tf_coil_nose_case=0.70261616505511615, ), @@ -957,6 +961,7 @@ def test_superconducting_tf_case_geometry(tfcasegeomparam, sctfcoil): a_tf_plasma_case, a_tf_coil_nose_case, dx_tf_side_case_average, + dx_tf_side_case_peak, ) = sctfcoil.superconducting_tf_case_geometry( i_tf_wp_geom=tfcasegeomparam.i_tf_wp_geom, i_tf_case_geom=tfcasegeomparam.i_tf_case_geom, @@ -987,6 +992,10 @@ def test_superconducting_tf_case_geometry(tfcasegeomparam, sctfcoil): tfcasegeomparam.expected_dx_tf_side_case_average ) + assert dx_tf_side_case_peak == pytest.approx( + tfcasegeomparam.expected_dx_tf_side_case_peak + ) + assert a_tf_plasma_case == pytest.approx(tfcasegeomparam.expected_a_tf_plasma_case) assert a_tf_coil_nose_case == pytest.approx(