From ce2982d9fa429c045a3c9aadbeca874a22cf5091 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Tue, 10 Feb 2026 13:25:08 +0000 Subject: [PATCH 1/8] Add PlasmaBootstrapCurrent to Stellarator and update tests - Modified the Stellarator class to include plasma_bootstrap as a parameter. - Updated the physics fixture in unit tests to instantiate PlasmaBootstrapCurrent. - Refactored multiple test cases to utilize PlasmaBootstrapCurrent for bootstrap fraction calculations. - Ensured all relevant tests in test_physics.py and test_stellarator.py are updated to reflect the new structure. --- process/main.py | 2 + process/models/physics/physics.py | 878 +++++++++++++++++++++++++++++- tests/unit/test_physics.py | 28 +- tests/unit/test_stellarator.py | 2 + 4 files changed, 883 insertions(+), 27 deletions(-) diff --git a/process/main.py b/process/main.py index 8165ede28..b9d4cc0dc 100644 --- a/process/main.py +++ b/process/main.py @@ -698,11 +698,13 @@ def __init__(self): ) self.plasma_beta = PlasmaBeta() self.plasma_inductance = PlasmaInductance() + self.plasma_bootstrap = PlasmaBootstrapCurrent() self.physics = Physics( plasma_profile=self.plasma_profile, current_drive=self.current_drive, plasma_beta=self.plasma_beta, plasma_inductance=self.plasma_inductance, + plasma_bootstrap=self.plasma_bootstrap, ) self.physics_detailed = DetailedPhysics( plasma_profile=self.plasma_profile, diff --git a/process/models/physics/physics.py b/process/models/physics/physics.py index 66d06eb33..33066bf59 100644 --- a/process/models/physics/physics.py +++ b/process/models/physics/physics.py @@ -1627,13 +1627,14 @@ def _trapped_particle_fraction_sauter( class Physics: - def __init__(self, plasma_profile, current_drive, plasma_beta, plasma_inductance): + def __init__(self, plasma_profile, current_drive, plasma_beta, plasma_inductance, plasma_bootstrap): self.outfile = constants.NOUT self.mfile = constants.MFILE self.plasma_profile = plasma_profile self.current_drive = current_drive self.beta = plasma_beta self.inductance = plasma_inductance + self.bootstrap = plasma_bootstrap def physics(self): """Routine to calculate tokamak plasma physics information @@ -1922,7 +1923,7 @@ def physics(self): # Calculate bootstrap current fraction using various models current_drive_variables.f_c_plasma_bootstrap_iter89 = ( current_drive_variables.cboot - * self.bootstrap_fraction_iter89( + * self.bootstrap.bootstrap_fraction_iter89( physics_variables.aspect, physics_variables.beta_total_vol_avg, physics_variables.b_plasma_total, @@ -1936,7 +1937,7 @@ def physics(self): current_drive_variables.f_c_plasma_bootstrap_nevins = ( current_drive_variables.cboot - * self.bootstrap_fraction_nevins( + * self.bootstrap.bootstrap_fraction_nevins( physics_variables.alphan, physics_variables.alphat, physics_variables.beta_toroidal_vol_avg, @@ -1955,7 +1956,7 @@ def physics(self): # Wilson scaling uses thermal poloidal beta, not total current_drive_variables.f_c_plasma_bootstrap_wilson = ( current_drive_variables.cboot - * self.bootstrap_fraction_wilson( + * self.bootstrap.bootstrap_fraction_wilson( physics_variables.alphaj, physics_variables.alphap, physics_variables.alphat, @@ -1970,14 +1971,14 @@ def physics(self): ( current_drive_variables.f_c_plasma_bootstrap_sauter, physics_variables.j_plasma_bootstrap_sauter_profile, - ) = self.bootstrap_fraction_sauter(self.plasma_profile) + ) = self.bootstrap.bootstrap_fraction_sauter(self.plasma_profile) current_drive_variables.f_c_plasma_bootstrap_sauter *= ( current_drive_variables.cboot ) current_drive_variables.f_c_plasma_bootstrap_sakai = ( current_drive_variables.cboot - * self.bootstrap_fraction_sakai( + * self.bootstrap.bootstrap_fraction_sakai( beta_poloidal=physics_variables.beta_poloidal_vol_avg, q95=physics_variables.q95, q0=physics_variables.q0, @@ -1990,7 +1991,7 @@ def physics(self): current_drive_variables.f_c_plasma_bootstrap_aries = ( current_drive_variables.cboot - * self.bootstrap_fraction_aries( + * self.bootstrap.bootstrap_fraction_aries( beta_poloidal=physics_variables.beta_poloidal_vol_avg, ind_plasma_internal_norm=physics_variables.ind_plasma_internal_norm, core_density=physics_variables.nd_plasma_electron_on_axis, @@ -2001,7 +2002,7 @@ def physics(self): current_drive_variables.f_c_plasma_bootstrap_andrade = ( current_drive_variables.cboot - * self.bootstrap_fraction_andrade( + * self.bootstrap.bootstrap_fraction_andrade( beta_poloidal=physics_variables.beta_poloidal_vol_avg, core_pressure=physics_variables.pres_plasma_thermal_on_axis, average_pressure=physics_variables.pres_plasma_thermal_vol_avg, @@ -2010,7 +2011,7 @@ def physics(self): ) current_drive_variables.f_c_plasma_bootstrap_hoang = ( current_drive_variables.cboot - * self.bootstrap_fraction_hoang( + * self.bootstrap.bootstrap_fraction_hoang( beta_poloidal=physics_variables.beta_poloidal_vol_avg, pressure_index=physics_variables.alphap, current_index=physics_variables.alphaj, @@ -2019,7 +2020,7 @@ def physics(self): ) current_drive_variables.f_c_plasma_bootstrap_wong = ( current_drive_variables.cboot - * self.bootstrap_fraction_wong( + * self.bootstrap.bootstrap_fraction_wong( beta_poloidal=physics_variables.beta_poloidal_vol_avg, density_index=physics_variables.alphan, temperature_index=physics_variables.alphat, @@ -2029,7 +2030,7 @@ def physics(self): ) current_drive_variables.bscf_gi_i = ( current_drive_variables.cboot - * self.bootstrap_fraction_gi_I( + * self.bootstrap.bootstrap_fraction_gi_I( beta_poloidal=physics_variables.beta_poloidal_vol_avg, pressure_index=physics_variables.alphap, temperature_index=physics_variables.alphat, @@ -2042,7 +2043,7 @@ def physics(self): current_drive_variables.bscf_gi_ii = ( current_drive_variables.cboot - * self.bootstrap_fraction_gi_II( + * self.bootstrap.bootstrap_fraction_gi_II( beta_poloidal=physics_variables.beta_poloidal_vol_avg, pressure_index=physics_variables.alphap, temperature_index=physics_variables.alphat, @@ -2052,7 +2053,7 @@ def physics(self): ) current_drive_variables.f_c_plasma_bootstrap_sugiyama_l = ( current_drive_variables.cboot - * self.bootstrap_fraction_sugiyama_l_mode( + * self.bootstrap.bootstrap_fraction_sugiyama_l_mode( eps=physics_variables.eps, beta_poloidal=physics_variables.beta_poloidal_vol_avg, alphan=physics_variables.alphan, @@ -2064,7 +2065,7 @@ def physics(self): ) current_drive_variables.f_c_plasma_bootstrap_sugiyama_h = ( current_drive_variables.cboot - * self.bootstrap_fraction_sugiyama_h_mode( + * self.bootstrap.bootstrap_fraction_sugiyama_h_mode( eps=physics_variables.eps, beta_poloidal=physics_variables.beta_poloidal_vol_avg, alphan=physics_variables.alphan, @@ -9861,6 +9862,855 @@ def output_volt_second_information(self): po.oblnkl(self.outfile) +class PlasmaBootstrapCurrent: + """Class to hold plasma bootstrap current for plasma processing.""" + + def __init__(self): + self.outfile = constants.NOUT + self.mfile = constants.MFILE + + @staticmethod + def bootstrap_fraction_iter89( + aspect: float, + beta: float, + b_plasma_toroidal_on_axis: float, + plasma_current: float, + q95: float, + q0: float, + rmajor: float, + vol_plasma: float, + ) -> float: + """ + Calculate the bootstrap-driven fraction of the plasma current. + + Args: + aspect (float): Plasma aspect ratio. + beta (float): Plasma total beta. + b_plasma_toroidal_on_axis (float): Toroidal field on axis (T). + plasma_current (float): Plasma current (A). + q95 (float): Safety factor at 95% surface. + q0 (float): Central safety factor. + rmajor (float): Plasma major radius (m). + vol_plasma (float): Plasma volume (m3). + + Returns: + float: The bootstrap-driven fraction of the plasma current. + + This function performs the original ITER calculation of the plasma current bootstrap fraction. + + Reference: + - ITER Physics Design Guidelines: 1989 [IPDG89], N. A. Uckan et al, + - ITER Documentation Series No.10, IAEA/ITER/DS/10, IAEA, Vienna, 1990 + """ + + # Calculate the bootstrap current coefficient + c_bs = 1.32 - 0.235 * (q95 / q0) + 0.0185 * (q95 / q0) ** 2 + + # Calculate the average minor radius + average_a = np.sqrt(vol_plasma / (2 * np.pi**2 * rmajor)) + + b_pa = (plasma_current / 1e6) / (5 * average_a) + + # Calculate the poloidal beta for bootstrap current + betapbs = beta * (b_plasma_toroidal_on_axis / b_pa) ** 2 + + # Ensure betapbs is positive + if betapbs <= 0.0: + return 0.0 + + # Calculate and return the bootstrap current fraction + return c_bs * (betapbs / np.sqrt(aspect)) ** 1.3 + + @staticmethod + def bootstrap_fraction_wilson( + alphaj: float, + alphap: float, + alphat: float, + betpth: float, + q0: float, + q95: float, + rmajor: float, + rminor: float, + ) -> float: + """ + Bootstrap current fraction from Wilson et al scaling + + Args: + alphaj (float): Current profile index. + alphap (float): Pressure profile index. + alphat (float): Temperature profile index. + betpth (float): Thermal component of poloidal beta. + q0 (float): Safety factor on axis. + q95 (float): Edge safety factor. + rmajor (float): Major radius (m). + rminor (float): Minor radius (m). + + Returns: + float: The bootstrap current fraction. + + This function calculates the bootstrap current fraction using the numerically fitted algorithm written by Howard Wilson. + + Reference: + - AEA FUS 172: Physics Assessment for the European Reactor Study, 1989 + - H. R. Wilson, Nuclear Fusion 32 (1992) 257 + """ + + term1 = np.log(0.5) + term2 = np.log(q0 / q95) + + # Re-arranging of parabolic profile to be equal to (r/a)^2 where the profile value is half of the core value + + termp = 1.0 - 0.5 ** (1.0 / alphap) + termt = 1.0 - 0.5 ** (1.0 / alphat) + termj = 1.0 - 0.5 ** (1.0 / alphaj) + + # Assuming a parabolic safety factor profile of the form q = q0 + (q95 - q0) * (r/a)^2 + # Substitute (r/a)^2 term from temperature,pressure and current profiles into q profile when values is 50% of core value + # Take natural log of q profile over q95 and q0 to get the profile index + + alfpnw = term1 / np.log(np.log((q0 + (q95 - q0) * termp) / q95) / term2) + alftnw = term1 / np.log(np.log((q0 + (q95 - q0) * termt) / q95) / term2) + aj = term1 / np.log(np.log((q0 + (q95 - q0) * termj) / q95) / term2) + + # Crude check for NaN errors or other illegal values. + if np.isnan(aj) or np.isnan(alfpnw) or np.isnan(alftnw) or aj < 0: + raise ProcessValueError( + "Illegal profile value found", aj=aj, alfpnw=alfpnw, alftnw=alftnw + ) + + # Ratio of ionic charge to electron charge + + z = 1.0 + + # Inverse aspect ratio: r2 = maximum plasma radius, r1 = minimum + # This is the definition used in the paper + r2 = rmajor + rminor + r1 = rmajor - rminor + eps1 = (r2 - r1) / (r2 + r1) + + # Coefficients fitted using least squares techniques + + # Square root of current profile index term + saj = np.sqrt(aj) + + a = np.array([ + 1.41 * (1.0 - 0.28 * saj) * (1.0 + 0.12 / z), + 0.36 * (1.0 - 0.59 * saj) * (1.0 + 0.8 / z), + -0.27 * (1.0 - 0.47 * saj) * (1.0 + 3.0 / z), + 0.0053 * (1.0 + 5.0 / z), + -0.93 * (1.0 - 0.34 * saj) * (1.0 + 0.15 / z), + -0.26 * (1.0 - 0.57 * saj) * (1.0 - 0.27 * z), + 0.064 * (1.0 - 0.6 * aj + 0.15 * aj * aj) * (1.0 + 7.6 / z), + -0.0011 * (1.0 + 9.0 / z), + -0.33 * (1.0 - aj + 0.33 * aj * aj), + -0.26 * (1.0 - 0.87 / saj - 0.16 * aj), + -0.14 * (1.0 - 1.14 / saj - 0.45 * saj), + -0.0069, + ]) + + seps1 = np.sqrt(eps1) + + b = np.array([ + 1.0, + alfpnw, + alftnw, + alfpnw * alftnw, + seps1, + alfpnw * seps1, + alftnw * seps1, + alfpnw * alftnw * seps1, + eps1, + alfpnw * eps1, + alftnw * eps1, + alfpnw * alftnw * eps1, + ]) + + # Empirical bootstrap current fraction + return seps1 * betpth * (a * b).sum() + + @staticmethod + def bootstrap_fraction_nevins( + alphan: float, + alphat: float, + beta_toroidal: float, + b_plasma_toroidal_on_axis: float, + nd_plasma_electrons_vol_avg: float, + plasma_current: float, + q95: float, + q0: float, + rmajor: float, + rminor: float, + te: float, + zeff: float, + ) -> float: + """ + Calculate the bootstrap current fraction from Nevins et al scaling. + + Args: + alphan (float): Density profile index. + alphat (float): Temperature profile index. + beta_toroidal (float): Toroidal plasma beta. + b_plasma_toroidal_on_axis (float): Toroidal field on axis (T). + nd_plasma_electrons_vol_avg (float): Electron density (/m3). + plasma_current (float): Plasma current (A). + q0 (float): Central safety factor. + q95 (float): Safety factor at 95% surface. + rmajor (float): Plasma major radius (m). + rminor (float): Plasma minor radius (m). + te (float): Volume averaged plasma temperature (keV). + zeff (float): Plasma effective charge. + + Returns: + float: The bootstrap current fraction. + + This function calculates the bootstrap current fraction using the Nevins et al method. + + Reference: See appendix of: + Keii Gi, Makoto Nakamura, Kenji Tobita, Yasushi Ono, + Bootstrap current fraction scaling for a tokamak reactor design study, + Fusion Engineering and Design, Volume 89, Issue 11, 2014, Pages 2709-2715, + ISSN 0920-3796, https://doi.org/10.1016/j.fusengdes.2014.07.009. + + Nevins, W. M. "Summary report: ITER specialists' meeting on heating and current drive." + ITER-TN-PH-8-4, June 1988. 1988. + + """ + # Calculate peak electron beta at plasma centre, this is not the form used in the paper + # The paper assumes parabolic profiles for calculating core values with the profile indexes. + # We instead use the directly calculated electron density and temperature values at the core. + # So that it is compatible with all profiles + + betae0 = ( + physics_variables.nd_plasma_electron_on_axis + * physics_variables.temp_plasma_electron_on_axis_kev + * 1.0e3 + * constants.ELECTRON_CHARGE + / (b_plasma_toroidal_on_axis**2 / (2.0 * constants.RMU0)) + ) + + # Call integration routine using definite integral routine from scipy + + ainteg, _ = integrate.quad( + lambda y: _nevins_integral( + y, + nd_plasma_electrons_vol_avg, + te, + b_plasma_toroidal_on_axis, + rminor, + rmajor, + zeff, + alphat, + alphan, + q0, + q95, + beta_toroidal, + ), + 0, # Lower bound + 1.0, # Upper bound + ) + + # Calculate bootstrap current and fraction + + aibs = 2.5 * betae0 * rmajor * b_plasma_toroidal_on_axis * q95 * ainteg + return 1.0e6 * aibs / plasma_current + + @staticmethod + def bootstrap_fraction_sauter(plasma_profile: float) -> float: + """Calculate the bootstrap current fraction from the Sauter et al scaling. + + Args: + plasma_profile (PlasmaProfile): The plasma profile object. + + Returns: + float: The bootstrap current fraction. + + This function calculates the bootstrap current fraction using the Sauter, Angioni, and Lin-Liu scaling. + + Reference: + - O. Sauter, C. Angioni, Y. R. Lin-Liu; + Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime. + Phys. Plasmas 1 July 1999; 6 (7): 2834-2839. https://doi.org/10.1063/1.873240 + - O. Sauter, C. Angioni, Y. R. Lin-Liu; Erratum: + Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime + [Phys. Plasmas 6, 2834 (1999)]. Phys. Plasmas 1 December 2002; 9 (12): 5140. https://doi.org/10.1063/1.1517052 + + Note: + The code was supplied by Emiliano Fable, IPP Garching (private communication). + """ + + # Radial points from 0 to 1 seperated by 1/profile_size + roa = plasma_profile.neprofile.profile_x + + # Local circularised minor radius + rho = np.sqrt(physics_variables.a_plasma_poloidal / np.pi) * roa + + # Square root of local aspect ratio + sqeps = np.sqrt(roa * (physics_variables.rminor / physics_variables.rmajor)) + + # Calculate electron and ion density profiles + ne = plasma_profile.neprofile.profile_y * 1e-19 + ni = ( + physics_variables.nd_plasma_ions_total_vol_avg + / physics_variables.nd_plasma_electrons_vol_avg + ) * ne + + # Calculate electron and ion temperature profiles + tempe = plasma_profile.teprofile.profile_y + tempi = ( + physics_variables.temp_plasma_ion_vol_avg_kev + / physics_variables.temp_plasma_electron_vol_avg_kev + ) * tempe + + # Flat Zeff profile assumed + # Return tempi like array object filled with zeff + zeff = np.full_like(tempi, physics_variables.n_charge_plasma_effective_vol_avg) + + # inverse_q = 1/safety factor + # Parabolic q profile assumed + inverse_q = 1 / ( + physics_variables.q0 + + (physics_variables.q95 - physics_variables.q0) * roa**2 + ) + # Create new array of average mass of fuel portion of ions + amain = np.full_like(inverse_q, physics_variables.m_ions_total_amu) + + # Create new array of average main ion charge + zmain = np.full_like(inverse_q, 1.0 + physics_variables.f_plasma_fuel_helium3) + + # Calculate total bootstrap current (MA) by summing along profiles + # Looping from 2 because _calculate_l31_coefficient() etc should return 0 @ j == 1 + radial_elements = np.arange(2, plasma_profile.profile_size) + + # Change in localised minor radius to be used as delta term in derivative + drho = rho[radial_elements] - rho[radial_elements - 1] + + # Area of annulus, assuming circular plasma cross-section + da = 2 * np.pi * rho[radial_elements - 1] * drho # area of annulus + + # Create the partial derivatives using numpy gradient (central differences) + dlogte_drho = np.gradient(np.log(tempe), rho)[radial_elements - 1] + dlogti_drho = np.gradient(np.log(tempi), rho)[radial_elements - 1] + dlogne_drho = np.gradient(np.log(ne), rho)[radial_elements - 1] + + jboot = ( + 0.5 + * ( + _calculate_l31_coefficient( + radial_elements, + plasma_profile.profile_size, + physics_variables.rmajor, + physics_variables.b_plasma_toroidal_on_axis, + physics_variables.triang, + ne, + ni, + tempe, + tempi, + inverse_q, + rho, + zeff, + sqeps, + ) + * dlogne_drho + + _calculate_l31_32_coefficient( + radial_elements, + plasma_profile.profile_size, + physics_variables.rmajor, + physics_variables.b_plasma_toroidal_on_axis, + physics_variables.triang, + ne, + ni, + tempe, + tempi, + inverse_q, + rho, + zeff, + sqeps, + ) + * dlogte_drho + + _calculate_l34_alpha_31_coefficient( + radial_elements, + plasma_profile.profile_size, + physics_variables.rmajor, + physics_variables.b_plasma_toroidal_on_axis, + physics_variables.triang, + inverse_q, + sqeps, + tempi, + tempe, + amain, + zmain, + ni, + ne, + rho, + zeff, + ) + * dlogti_drho + ) + * 1.0e6 + * ( + -physics_variables.b_plasma_toroidal_on_axis + / (0.2 * np.pi * physics_variables.rmajor) + * rho[radial_elements - 1] + * inverse_q[radial_elements - 1] + ) + ) # A/m2 + + return (np.sum(da * jboot, axis=0) / physics_variables.plasma_current), jboot + + @staticmethod + def bootstrap_fraction_sakai( + beta_poloidal: float, + q95: float, + q0: float, + alphan: float, + alphat: float, + eps: float, + ind_plasma_internal_norm: float, + ) -> float: + """ + Calculate the bootstrap fraction using the Sakai formula. + + Parameters: + beta_poloidal (float): Plasma poloidal beta. + q95 (float): Safety factor at 95% of the plasma radius. + q0 (float): Safety factor at the magnetic axis. + alphan (float): Density profile index + alphat (float): Temperature profile index + eps (float): Inverse aspect ratio. + ind_plasma_internal_norm (float): Plasma normalised internal inductance + + Returns: + float: The calculated bootstrap fraction. + + Notes: + The profile assumed for the alphan and alpat indexes is only a parabolic profile without a pedestal (L-mode). + The Root Mean Squared Error for the fitting database of this formula was 0.025 + Concentrating on the positive shear plasmas using the ACCOME code equilibria with the fully non-inductively driven + conditions with neutral beam (NB) injection only are calculated. + The electron temperature and the ion temperature were assumed to be equal + This can be used for all apsect ratios. + The diamagnetic fraction is included in this formula. + + References: + Ryosuke Sakai, Takaaki Fujita, Atsushi Okamoto, Derivation of bootstrap current fraction scaling formula for 0-D system code analysis, + Fusion Engineering and Design, Volume 149, 2019, 111322, ISSN 0920-3796, + https://doi.org/10.1016/j.fusengdes.2019.111322. + """ + # Sakai states that the ACCOME dataset used has the toridal diamagnetic current included in the bootstrap current + # So the diamganetic current should not be calculated with this. i_diamagnetic_current = 0 + return ( + 10 ** (0.951 * eps - 0.948) + * beta_poloidal ** (1.226 * eps + 1.584) + * ind_plasma_internal_norm ** (-0.184 * eps - 0.282) + * (q95 / q0) ** (-0.042 * eps - 0.02) + * alphan ** (0.13 * eps + 0.05) + * alphat ** (0.502 * eps - 0.273) + ) + + @staticmethod + def bootstrap_fraction_aries( + beta_poloidal: float, + ind_plasma_internal_norm: float, + core_density: float, + average_density: float, + inverse_aspect: float, + ) -> float: + """ + Calculate the bootstrap fraction using the ARIES formula. + + Parameters: + beta_poloidal (float): Plasma poloidal beta. + ind_plasma_internal_norm (float): Plasma normalized internal inductance. + core_density (float): Core plasma density. + average_density (float): Average plasma density. + inverse_aspect (float): Inverse aspect ratio. + + Returns: + float: The calculated bootstrap fraction. + + Notes: + - The source reference does not provide any info about the derivation of the formula. It is only stated + + References: + - Zoran Dragojlovic et al., “An advanced computational algorithm for systems analysis of tokamak power plants,” + Fusion Engineering and Design, vol. 85, no. 2, pp. 243-265, Apr. 2010, + doi: https://doi.org/10.1016/j.fusengdes.2010.02.015. + + """ + # Using the standard variable naming from the ARIES paper + a_1 = ( + 1.10 - 1.165 * ind_plasma_internal_norm + 0.47 * ind_plasma_internal_norm**2 + ) + b_1 = ( + 0.806 + - 0.885 * ind_plasma_internal_norm + + 0.297 * ind_plasma_internal_norm**2 + ) + + c_bs = a_1 + b_1 * (core_density / average_density) + + return c_bs * np.sqrt(inverse_aspect) * beta_poloidal + + @staticmethod + def bootstrap_fraction_andrade( + beta_poloidal: float, + core_pressure: float, + average_pressure: float, + inverse_aspect: float, + ) -> float: + """ + Calculate the bootstrap fraction using the Andrade et al formula. + + Parameters: + beta_poloidal (float): Plasma poloidal beta. + core_pressure (float): Core plasma pressure. + average_pressure (float): Average plasma pressure. + inverse_aspect (float): Inverse aspect ratio. + + Returns: + float: The calculated bootstrap fraction. + + Notes: + - Based off 350 plasma profiles from Experimento Tokamak Esferico (ETE) spherical tokamak + - A = 1.5, R_0 = 0.3m, I_p = 200kA, B_0=0.4T, beta = 4-10%. Profiles taken as Gaussian shaped functions. + - Errors mostly up to the order of 10% are obtained when both expressions are compared with the equilibrium estimates for the + bootstrap current in ETE + + References: + - M. C. R. Andrade and G. O. Ludwig, “Scaling of bootstrap current on equilibrium and plasma profile parameters in tokamak plasmas,” + Plasma Physics and Controlled Fusion, vol. 50, no. 6, pp. 065001-065001, Apr. 2008, + doi: https://doi.org/10.1088/0741-3335/50/6/065001. + + """ + + # Using the standard variable naming from the Andrade et.al. paper + c_p = core_pressure / average_pressure + + # Error +- 0.0007 + c_bs = 0.2340 + + return c_bs * np.sqrt(inverse_aspect) * beta_poloidal * c_p**0.8 + + @staticmethod + def bootstrap_fraction_hoang( + beta_poloidal: float, + pressure_index: float, + current_index: float, + inverse_aspect: float, + ) -> float: + """ + Calculate the bootstrap fraction using the Hoang et al formula. + + Parameters: + beta_poloidal (float): Plasma poloidal beta. + pressure_index (float): Pressure profile index. + current_index (float): Current profile index. + inverse_aspect (float): Inverse aspect ratio. + + Returns: + float: The calculated bootstrap fraction. + + Notes: + - Based off of TFTR data calculated using the TRANSP plasma analysis code + - 170 discharges which was assembled to study the tritium influx and transport in discharges with D-only neutral beam + injection (NBI) + - Contains L-mode, supershots, reversed shear, enhanced reversed shear and increased li discharges + - Discharges with monotonic flux profiles with reversed shear are also included + - Is applied to circular cross-section plasmas + + References: + - G. T. Hoang and R. V. Budny, “The bootstrap fraction in TFTR,” AIP conference proceedings, + Jan. 1997, doi: https://doi.org/10.1063/1.53414. + """ + + # Using the standard variable naming from the Hoang et.al. paper + # Hoang et.al uses a different definition for the profile indexes such that + # alpha_p is defined as the ratio of the central and the volume-averaged values, and the peakedness of the density of the total plasma current + # (defined as ratio of the central value and I_p), alpha_j$ + + # We assume the pressure and current profile is parabolic and use the (profile_index +1) term in lieu + # The term represents the ratio of the the core to volume averaged value + + # This could lead to large changes in the value depending on interpretation of the profile index + + c_bs = np.sqrt((pressure_index + 1) / (current_index + 1)) + + return 0.4 * np.sqrt(inverse_aspect) * beta_poloidal**0.9 * c_bs + + @staticmethod + def bootstrap_fraction_wong( + beta_poloidal: float, + density_index: float, + temperature_index: float, + inverse_aspect: float, + elongation: float, + ) -> float: + """ + Calculate the bootstrap fraction using the Wong et al formula. + + Parameters: + beta_poloidal (float): Plasma poloidal beta. + density_index (float): Density profile index. + temperature_index (float): Temperature profile index. + inverse_aspect (float): Inverse aspect ratio. + elongation (float): Plasma elongation. + + Returns: + float: The calculated bootstrap fraction. + + Notes: + - Data is based off of equilibria from Miller et al. + - A: 1.2 - 3.0 and stable to n ballooning and low n kink modes at a bootstrap fraction of 99% for kappa = 2, 2.5 and 3 + - The results were parameterized as a function of aspect ratio and elongation + - The parametric dependency of beta_p and beta_T are based on fitting of the DIII-D high equivalent DT yield results + - Parabolic profiles should be used for best results as the pressure peaking value is calculated as the product of a parabolic + temperature and density profile + + References: + - C.-P. Wong, J. C. Wesley, R. D. Stambaugh, and E. T. Cheng, “Toroidal reactor designs as a function of aspect ratio and elongation,” + vol. 42, no. 5, pp. 547-556, May 2002, doi: https://doi.org/10.1088/0029-5515/42/5/307. + + - Miller, R L, "Stable bootstrap-current driven equilibria for low aspect ratio tokamaks". + Switzerland: N. p., 1996. Web.https://fusion.gat.com/pubs-ext/MISCONF96/A22433.pdf + """ + # Using the standard variable naming from the Wong et.al. paper + f_peak = 2.0 / scipy.special.beta(0.5, density_index + temperature_index + 1) + + c_bs = 0.773 + 0.019 * elongation + + return c_bs * f_peak**0.25 * beta_poloidal * np.sqrt(inverse_aspect) + + @staticmethod + def bootstrap_fraction_gi_I( # noqa: N802 + beta_poloidal: float, + pressure_index: float, + temperature_index: float, + inverse_aspect: float, + effective_charge: float, + q95: float, + q0: float, + ) -> float: + """ + Calculate the bootstrap fraction using the first scaling from the Gi et al formula. + + Parameters: + beta_poloidal (float): Plasma poloidal beta. + pressure_index (float): Pressure profile index. + temperature_index (float): Temperature profile index. + inverse_aspect (float): Inverse aspect ratio. + effective_charge (float): Plasma effective charge. + q95 (float): Safety factor at 95% of the plasma radius. + q0 (float): Safety factor at the magnetic axis. + + Returns: + float: The calculated bootstrap fraction. + + Notes: + - Scaling found by solving the Hirshman-Sigmar bootstrap modelusing the matrix inversion method + - Method was done to put the scaling into parameters compatible with the TPC systems code + - Uses the ACCOME code to create bootstrap current fractions without using the itrative calculations of the + curent drive and equilibrium models in the scan + - R = 5.0 m, A = 1.3 - 5.0, kappa = 2, traing = 0.3, alpha_n = 0.1 - 0.8, alpha_t = 1.0 - 3.0, Z_eff = 1.2 - 3.0 + - Uses parabolic plasma profiles only. + - Scaling 1 has better accuracy than Scaling 2. However, Scaling 1 overestimated the f_BS value for reversed shear + equilibrium. + + References: + - K. Gi, M. Nakamura, Kenji Tobita, and Y. Ono, “Bootstrap current fraction scaling for a tokamak reactor design study,” + Fusion Engineering and Design, vol. 89, no. 11, pp. 2709-2715, Aug. 2014, + doi: https://doi.org/10.1016/j.fusengdes.2014.07.009. + """ + + # Using the standard variable naming from the Gi et.al. paper + + c_bs = ( + 0.474 + * inverse_aspect**-0.1 + * pressure_index**0.974 + * temperature_index**-0.416 + * effective_charge**0.178 + * (q95 / q0) ** -0.133 + ) + + return c_bs * np.sqrt(inverse_aspect) * beta_poloidal + + @staticmethod + def bootstrap_fraction_gi_II( # noqa: N802 + beta_poloidal: float, + pressure_index: float, + temperature_index: float, + inverse_aspect: float, + effective_charge: float, + ) -> float: + """ + Calculate the bootstrap fraction using the second scaling from the Gi et al formula. + + Parameters: + beta_poloidal (float): Plasma poloidal beta. + pressure_index (float): Pressure profile index. + temperature_index (float): Temperature profile index. + inverse_aspect (float): Inverse aspect ratio. + effective_charge (float): Plasma effective charge. + + Returns: + float: The calculated bootstrap fraction. + + Notes: + - Scaling found by solving the Hirshman-Sigmar bootstrap modelusing the matrix inversion method + - Method was done to put the scaling into parameters compatible with the TPC systems code + - Uses the ACCOME code to create bootstrap current fractions without using the itrative calculations of the + curent drive and equilibrium models in the scan + - R = 5.0 m, A = 1.3 - 5.0, kappa = 2, traing = 0.3, alpha_n = 0.1 - 0.8, alpha_t = 1.0 - 3.0, Z_eff = 1.2 - 3.0 + - Uses parabolic plasma profiles only. + - This scaling has the q profile dependance removed to obtain a scaling formula with much more flexible variables than + that by a single profile factor for internal current profile. + + References: + - K. Gi, M. Nakamura, Kenji Tobita, and Y. Ono, “Bootstrap current fraction scaling for a tokamak reactor design study,” + Fusion Engineering and Design, vol. 89, no. 11, pp. 2709-2715, Aug. 2014, + doi: https://doi.org/10.1016/j.fusengdes.2014.07.009. + """ + + # Using the standard variable naming from the Gi et.al. paper + + c_bs = ( + 0.382 + * inverse_aspect**-0.242 + * pressure_index**0.974 + * temperature_index**-0.416 + * effective_charge**0.178 + ) + + return c_bs * np.sqrt(inverse_aspect) * beta_poloidal + + @staticmethod + def bootstrap_fraction_sugiyama_l_mode( + eps: float, + beta_poloidal: float, + alphan: float, + alphat: float, + zeff: float, + q95: float, + q0: float, + ) -> float: + """ + Calculate the bootstrap fraction using the L-mode scaling from the Sugiyama et al formula. + + :param eps: Inverse aspect ratio. + :type eps: float + :param beta_poloidal: Plasma poloidal beta. + :type beta_poloidal: float + :param alphan: Density profile index. + :type alphan: float + :param alphat: Temperature profile index. + :type alphat: float + :param zeff: Plasma effective charge. + :type zeff: float + :param q95: Safety factor at 95% of the plasma radius. + :type q95: float + :param q0: Safety factor at the magnetic axis. + :type q0: float + + :returns: The calculated bootstrap fraction. + :rtype: float + + :notes: + - This scaling is derived for L-mode plasmas. + - Ion and electron temperature are the same + - Z_eff has a uniform profile, with only fully stripped carbon impurity + + :references: + - S. Sugiyama, T. Goto, H. Utoh, and Y. Sakamoto, “Improvement of core plasma power and + current balance models for tokamak systems code considering H-mode plasma profiles,” + Fusion Engineering and Design, vol. 216, p. 115022, Jul. 2025, doi: + https://doi.org/10.1016/j.fusengdes.2025.115022. + """ + + return ( + 0.740 + * eps**0.418 + * beta_poloidal**0.904 + * alphan**0.06 + * alphat**-0.138 + * zeff**0.230 + * (q95 / q0) ** -0.142 + ) + + @staticmethod + def bootstrap_fraction_sugiyama_h_mode( + eps: float, + beta_poloidal: float, + alphan: float, + alphat: float, + tbeta: float, + zeff: float, + q95: float, + q0: float, + radius_plasma_pedestal_density_norm: float, + nd_plasma_pedestal_electron: float, + n_greenwald: float, + temp_plasma_pedestal_kev: float, + ) -> float: + """ + Calculate the bootstrap fraction using the H-mode scaling from the Sugiyama et al formula. + + :param eps: Inverse aspect ratio. + :type eps: float + :param beta_poloidal: Plasma poloidal beta. + :type beta_poloidal: float + :param alphan: Density profile index. + :type alphan: float + :param alphat: Temperature profile index. + :type alphat: float + :param tbeta: Second temperature profile index. + :type tbeta: float + :param zeff: Plasma effective charge. + :type zeff: float + :param q95: Safety factor at 95% of the plasma radius. + :type q95: float + :param q0: Safety factor at the magnetic axis. + :type q0: float + :param radius_plasma_pedestal_density_norm: Normalised plasma radius of density pedestal. + :type radius_plasma_pedestal_density_norm: float + :param nd_plasma_pedestal_electron: Electron number density at the pedestal [m^-3]. + :type nd_plasma_pedestal_electron: float + :param n_greenwald: Greenwald density limit [m^-3]. + :type n_greenwald: float + :param temp_plasma_pedestal_kev: Electron temperature at the pedestal [keV]. + :type temp_plasma_pedestal_kev: float + + :returns: The calculated bootstrap fraction. + :rtype: float + + :notes: + - This scaling is derived for H-mode plasmas. + - The temperature and density pedestal positions are the same + - Separatrix temperature and density are zero + - Ion and electron temperature are the same + - Z_eff has a uniform profile, with only fully stripped carbon impurity + + :references: + - S. Sugiyama, T. Goto, H. Utoh, and Y. Sakamoto, “Improvement of core plasma power and + current balance models for tokamak systems code considering H-mode plasma profiles,” + Fusion Engineering and Design, vol. 216, p. 115022, Jul. 2025, doi: + https://doi.org/10.1016/j.fusengdes.2025.115022. + """ + + return ( + 0.789 + * eps**0.606 + * beta_poloidal**0.960 + * alphan**0.0319 + * alphat**0.00822 + * tbeta**-0.0783 + * zeff**0.241 + * (q95 / q0) ** -0.103 + * radius_plasma_pedestal_density_norm**0.367 + * (nd_plasma_pedestal_electron / n_greenwald) ** -0.174 + * temp_plasma_pedestal_kev**0.0552 + ) + + class DetailedPhysics: """Class to hold detailed physics models for plasma processing.""" diff --git a/tests/unit/test_physics.py b/tests/unit/test_physics.py index 956912aa7..2137552db 100644 --- a/tests/unit/test_physics.py +++ b/tests/unit/test_physics.py @@ -25,6 +25,7 @@ Physics, PlasmaBeta, PlasmaInductance, + PlasmaBootstrapCurrent, calculate_current_coefficient_hastie, calculate_plasma_current_peng, calculate_poloidal_field, @@ -56,6 +57,7 @@ def physics(): ), PlasmaBeta(), PlasmaInductance(), + PlasmaBootstrapCurrent(), ) @@ -143,7 +145,7 @@ def test_bootstrap_fraction_iter89(bootstrapfractioniter89param, physics): :type monkeypatch: _pytest.monkeypatch.monkeypatch """ - f_c_plasma_bootstrap = physics.bootstrap_fraction_iter89( + f_c_plasma_bootstrap = PlasmaBootstrapCurrent().bootstrap_fraction_iter89( aspect=bootstrapfractioniter89param.aspect, beta=bootstrapfractioniter89param.beta, b_plasma_toroidal_on_axis=bootstrapfractioniter89param.b_plasma_toroidal_on_axis, @@ -238,7 +240,7 @@ def test_bootstrap_fraction_nevins(bootstrapfractionnevinsparam, monkeypatch, ph bootstrapfractionnevinsparam.nd_plasma_electron_on_axis, ) - fibs = physics.bootstrap_fraction_nevins( + fibs = PlasmaBootstrapCurrent().bootstrap_fraction_nevins( alphan=bootstrapfractionnevinsparam.alphan, alphat=bootstrapfractionnevinsparam.alphat, beta_toroidal=bootstrapfractionnevinsparam.beta_toroidal, @@ -316,7 +318,7 @@ def test_bootstrap_fraction_wilson(bootstrapfractionwilsonparam, physics): :type monkeypatch: _pytest.monkeypatch.monkeypatch """ - bfw = physics.bootstrap_fraction_wilson( + bfw = PlasmaBootstrapCurrent().bootstrap_fraction_wilson( alphaj=bootstrapfractionwilsonparam.alphaj, alphap=bootstrapfractionwilsonparam.alphap, alphat=bootstrapfractionwilsonparam.alphat, @@ -555,7 +557,7 @@ def test_bootstrap_fraction_sauter(bootstrapfractionsauterparam, monkeypatch, ph monkeypatch.setattr(physics_variables, "alphat", bootstrapfractionsauterparam.alphat) physics.plasma_profile.run() - bfs, _ = physics.bootstrap_fraction_sauter(physics.plasma_profile) + bfs, _ = PlasmaBootstrapCurrent().bootstrap_fraction_sauter(physics.plasma_profile) assert bfs == pytest.approx(bootstrapfractionsauterparam.expected_bfs) @@ -639,7 +641,7 @@ def test_bootstrap_fraction_sakai(bootstrapfractionsakaiparam, monkeypatch, phys bootstrapfractionsakaiparam.ind_plasma_internal_norm, ) - bfs = physics.bootstrap_fraction_sakai( + bfs = PlasmaBootstrapCurrent().bootstrap_fraction_sakai( beta_poloidal=bootstrapfractionsakaiparam.beta_poloidal, q95=bootstrapfractionsakaiparam.q95, q0=bootstrapfractionsakaiparam.q0, @@ -689,7 +691,7 @@ def test_bootstrap_fraction_aries(bootstrapfractionariesparam, physics): :type bootstrapfractionsauterparam: bootstrapfractionsauterparam """ - bfs = physics.bootstrap_fraction_aries( + bfs = PlasmaBootstrapCurrent().bootstrap_fraction_aries( beta_poloidal=bootstrapfractionariesparam.beta_poloidal, ind_plasma_internal_norm=bootstrapfractionariesparam.ind_plasma_internal_norm, core_density=bootstrapfractionariesparam.core_density, @@ -734,7 +736,7 @@ def test_bootstrap_fraction_andrade(bootstrapfractionandradeparam, physics): :type bootstrapfractionsauterparam: bootstrapfractionsauterparam """ - bfs = physics.bootstrap_fraction_andrade( + bfs = PlasmaBootstrapCurrent().bootstrap_fraction_andrade( beta_poloidal=bootstrapfractionandradeparam.beta_poloidal, core_pressure=bootstrapfractionandradeparam.core_pressure, average_pressure=bootstrapfractionandradeparam.average_pressure, @@ -778,7 +780,7 @@ def test_bootstrap_fraction_hoang(bootstrapfractionhoangparam, physics): :type bootstrapfractionsauterparam: bootstrapfractionsauterparam """ - bfs = physics.bootstrap_fraction_hoang( + bfs = PlasmaBootstrapCurrent().bootstrap_fraction_hoang( beta_poloidal=bootstrapfractionhoangparam.beta_poloidal, pressure_index=bootstrapfractionhoangparam.pressure_index, current_index=bootstrapfractionhoangparam.current_index, @@ -825,7 +827,7 @@ def test_bootstrap_fraction_wong(bootstrapfractionwongparam, physics): :type bootstrapfractionsauterparam: bootstrapfractionsauterparam """ - bfs = physics.bootstrap_fraction_wong( + bfs = PlasmaBootstrapCurrent().bootstrap_fraction_wong( beta_poloidal=bootstrapfractionwongparam.beta_poloidal, density_index=bootstrapfractionwongparam.density_index, temperature_index=bootstrapfractionwongparam.temperature_index, @@ -879,7 +881,7 @@ def test_bootstrap_fraction_gi_I(bootstrapfractiongiiparam, physics): # noqa: N :type bootstrapfractionsauterparam: bootstrapfractionsauterparam """ - bfs = physics.bootstrap_fraction_gi_I( + bfs = PlasmaBootstrapCurrent().bootstrap_fraction_gi_I( beta_poloidal=bootstrapfractiongiiparam.beta_poloidal, pressure_index=bootstrapfractiongiiparam.pressure_index, temperature_index=bootstrapfractiongiiparam.temperature_index, @@ -929,7 +931,7 @@ def test_bootstrap_fraction_gi_II(bootstrapfractiongiiiparam, physics): # noqa: :type bootstrapfractionsauterparam: bootstrapfractionsauterparam """ - bfs = physics.bootstrap_fraction_gi_II( + bfs = PlasmaBootstrapCurrent().bootstrap_fraction_gi_II( beta_poloidal=bootstrapfractiongiiiparam.beta_poloidal, pressure_index=bootstrapfractiongiiiparam.pressure_index, temperature_index=bootstrapfractiongiiiparam.temperature_index, @@ -986,7 +988,7 @@ def test_bootstrap_fraction_sugiyama_l_mode(bootstrapfractionsugiyamalparam, phy :param bootstrapfractionsugiyamalparam: Parameters for the test case. :type bootstrapfractionsugiyamalparam: BootstrapFractionSugiyamaLModeParam """ - bfs = physics.bootstrap_fraction_sugiyama_l_mode( + bfs = PlasmaBootstrapCurrent().bootstrap_fraction_sugiyama_l_mode( eps=bootstrapfractionsugiyamalparam.eps, beta_poloidal=bootstrapfractionsugiyamalparam.beta_poloidal, alphan=bootstrapfractionsugiyamalparam.alphan, @@ -1092,7 +1094,7 @@ def test_bootstrap_fraction_sugiyama_h_mode(bootstrapfractionsugiyamahparam, phy :param bootstrapfractionsugiyamahparam: Parameters for the test case. :type bootstrapfractionsugiyamahparam: BootstrapFractionSugiyamaHModeParam """ - bfs = physics.bootstrap_fraction_sugiyama_h_mode( + bfs = PlasmaBootstrapCurrent().bootstrap_fraction_sugiyama_h_mode( eps=bootstrapfractionsugiyamahparam.eps, beta_poloidal=bootstrapfractionsugiyamahparam.beta_poloidal, alphan=bootstrapfractionsugiyamahparam.alphan, diff --git a/tests/unit/test_stellarator.py b/tests/unit/test_stellarator.py index 268a94b67..972e9f9ce 100644 --- a/tests/unit/test_stellarator.py +++ b/tests/unit/test_stellarator.py @@ -82,9 +82,11 @@ def stellarator(): ), PlasmaBeta(), PlasmaInductance(), + PlasmaBootstrapCurrent(), ), Neoclassics(), plasma_beta=PlasmaBeta(), + plasma_bootstrap=PlasmaBootstrapCurrent(), ) From 531e0484fcd499501d28847a634e7762a383ee1f Mon Sep 17 00:00:00 2001 From: mn3981 Date: Tue, 10 Feb 2026 13:59:25 +0000 Subject: [PATCH 2/8] Refactor bootstrap current calculations to use BootstrapCurrentFractionModel and improve error handling --- process/models/physics/physics.py | 71 ++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/process/models/physics/physics.py b/process/models/physics/physics.py index 33066bf59..013482ff9 100644 --- a/process/models/physics/physics.py +++ b/process/models/physics/physics.py @@ -2081,31 +2081,19 @@ def physics(self): ) ) - bootstrap_map = { - 0: current_drive_variables.f_c_plasma_bootstrap, - 1: current_drive_variables.f_c_plasma_bootstrap_iter89, - 2: current_drive_variables.f_c_plasma_bootstrap_nevins, - 3: current_drive_variables.f_c_plasma_bootstrap_wilson, - 4: current_drive_variables.f_c_plasma_bootstrap_sauter, - 5: current_drive_variables.f_c_plasma_bootstrap_sakai, - 6: current_drive_variables.f_c_plasma_bootstrap_aries, - 7: current_drive_variables.f_c_plasma_bootstrap_andrade, - 8: current_drive_variables.f_c_plasma_bootstrap_hoang, - 9: current_drive_variables.f_c_plasma_bootstrap_wong, - 10: current_drive_variables.bscf_gi_i, - 11: current_drive_variables.bscf_gi_ii, - 12: current_drive_variables.f_c_plasma_bootstrap_sugiyama_l, - 13: current_drive_variables.f_c_plasma_bootstrap_sugiyama_h, - } - if int(physics_variables.i_bootstrap_current) in bootstrap_map: - current_drive_variables.f_c_plasma_bootstrap = bootstrap_map[ + # Calculate beta_norm_max based on i_beta_norm_max + try: + model = BootstrapCurrentFractionModel( int(physics_variables.i_bootstrap_current) - ] - else: + ) + current_drive_variables.f_c_plasma_bootstrap = ( + self.bootstrap.get_bootstrap_current_fraction_value(model) + ) + except ValueError: raise ProcessValueError( "Illegal value of i_bootstrap_current", i_bootstrap_current=physics_variables.i_bootstrap_current, - ) + ) from None physics_variables.err242 = 0 if ( @@ -9862,6 +9850,25 @@ def output_volt_second_information(self): po.oblnkl(self.outfile) +class BootstrapCurrentFractionModel(IntEnum): + """Bootstrap plasma current fraction (f_BS) model types""" + + USER_INPUT = 0 + ITER_89 = 1 + NEVINS = 2 + WILSON = 3 + SAUTER = 4 + SAKAI = 5 + ARIES = 6 + ANDRADE = 7 + HOANG = 8 + WONG = 9 + GI_1 = 10 + GI_2 = 11 + SUGIYAMA_L_MODE = 12 + SUGIYAMA_H_MODE = 13 + + class PlasmaBootstrapCurrent: """Class to hold plasma bootstrap current for plasma processing.""" @@ -9869,6 +9876,28 @@ def __init__(self): self.outfile = constants.NOUT self.mfile = constants.MFILE + def get_bootstrap_current_fraction_value( + self, model: BootstrapCurrentFractionModel + ) -> float: + """Get the plasma current bootstrap fraction (f_BS) for the specified model.""" + model_map = { + BootstrapCurrentFractionModel.USER_INPUT: current_drive_variables.f_c_plasma_bootstrap, + BootstrapCurrentFractionModel.ITER_89: current_drive_variables.f_c_plasma_bootstrap_iter89, + BootstrapCurrentFractionModel.NEVINS: current_drive_variables.f_c_plasma_bootstrap_nevins, + BootstrapCurrentFractionModel.WILSON: current_drive_variables.f_c_plasma_bootstrap_wilson, + BootstrapCurrentFractionModel.SAUTER: current_drive_variables.f_c_plasma_bootstrap_sauter, + BootstrapCurrentFractionModel.SAKAI: current_drive_variables.f_c_plasma_bootstrap_sakai, + BootstrapCurrentFractionModel.ARIES: current_drive_variables.f_c_plasma_bootstrap_aries, + BootstrapCurrentFractionModel.ANDRADE: current_drive_variables.f_c_plasma_bootstrap_andrade, + BootstrapCurrentFractionModel.HOANG: current_drive_variables.f_c_plasma_bootstrap_hoang, + BootstrapCurrentFractionModel.WONG: current_drive_variables.f_c_plasma_bootstrap_wong, + BootstrapCurrentFractionModel.GI_1: current_drive_variables.bscf_gi_i, + BootstrapCurrentFractionModel.GI_2: current_drive_variables.bscf_gi_ii, + BootstrapCurrentFractionModel.SUGIYAMA_L_MODE: current_drive_variables.f_c_plasma_bootstrap_sugiyama_l, + BootstrapCurrentFractionModel.SUGIYAMA_H_MODE: current_drive_variables.f_c_plasma_bootstrap_sugiyama_h, + } + return model_map[model] + @staticmethod def bootstrap_fraction_iter89( aspect: float, From 70cc6533d33118091ff909a40557e2176b9609c6 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Tue, 10 Feb 2026 14:10:29 +0000 Subject: [PATCH 3/8] Create dedicated bootstrap info output --- process/models/physics/physics.py | 493 +++++++++++++++--------------- 1 file changed, 245 insertions(+), 248 deletions(-) diff --git a/process/models/physics/physics.py b/process/models/physics/physics.py index 013482ff9..753b17444 100644 --- a/process/models/physics/physics.py +++ b/process/models/physics/physics.py @@ -5800,254 +5800,7 @@ def outplas(self): self.inductance.output_volt_second_information() - po.ovarrf( - self.outfile, - "Bootstrap current fraction multiplier", - "(cboot)", - current_drive_variables.cboot, - ) - po.ovarrf( - self.outfile, - "Bootstrap fraction (ITER 1989)", - "(f_c_plasma_bootstrap_iter89)", - current_drive_variables.f_c_plasma_bootstrap_iter89, - "OP ", - ) - - po.ovarrf( - self.outfile, - "Bootstrap fraction (Sauter et al)", - "(f_c_plasma_bootstrap_sauter)", - current_drive_variables.f_c_plasma_bootstrap_sauter, - "OP ", - ) - for point in range(len(physics_variables.j_plasma_bootstrap_sauter_profile)): - po.ovarrf( - self.mfile, - f"Sauter et al bootstrap current density profile at point {point}", - f"(j_plasma_bootstrap_sauter_profile{point})", - physics_variables.j_plasma_bootstrap_sauter_profile[point], - "OP ", - ) - - po.ovarrf( - self.outfile, - "Bootstrap fraction (Nevins et al)", - "(f_c_plasma_bootstrap_nevins)", - current_drive_variables.f_c_plasma_bootstrap_nevins, - "OP ", - ) - po.ovarrf( - self.outfile, - "Bootstrap fraction (Wilson)", - "(f_c_plasma_bootstrap_wilson)", - current_drive_variables.f_c_plasma_bootstrap_wilson, - "OP ", - ) - po.ovarrf( - self.outfile, - "Bootstrap fraction (Sakai)", - "(f_c_plasma_bootstrap_sakai)", - current_drive_variables.f_c_plasma_bootstrap_sakai, - "OP ", - ) - po.ovarrf( - self.outfile, - "Bootstrap fraction (ARIES)", - "(f_c_plasma_bootstrap_aries)", - current_drive_variables.f_c_plasma_bootstrap_aries, - "OP ", - ) - po.ovarrf( - self.outfile, - "Bootstrap fraction (Andrade)", - "(f_c_plasma_bootstrap_andrade)", - current_drive_variables.f_c_plasma_bootstrap_andrade, - "OP ", - ) - po.ovarrf( - self.outfile, - "Bootstrap fraction (Hoang)", - "(f_c_plasma_bootstrap_hoang)", - current_drive_variables.f_c_plasma_bootstrap_hoang, - "OP ", - ) - po.ovarrf( - self.outfile, - "Bootstrap fraction (Wong)", - "(f_c_plasma_bootstrap_wong)", - current_drive_variables.f_c_plasma_bootstrap_wong, - "OP ", - ) - po.ovarrf( - self.outfile, - "Bootstrap fraction (Gi I)", - "(bscf_gi_i)", - current_drive_variables.bscf_gi_i, - "OP ", - ) - po.ovarrf( - self.outfile, - "Bootstrap fraction (Gi II)", - "(bscf_gi_ii)", - current_drive_variables.bscf_gi_ii, - "OP ", - ) - po.ovarrf( - self.outfile, - "Bootstrap fraction (Sugiyama L-mode)", - "(f_c_plasma_bootstrap_sugiyama_l)", - current_drive_variables.f_c_plasma_bootstrap_sugiyama_l, - "OP ", - ) - po.ovarrf( - self.outfile, - "Bootstrap fraction (Sugiyama H-mode)", - "(f_c_plasma_bootstrap_sugiyama_h)", - current_drive_variables.f_c_plasma_bootstrap_sugiyama_h, - "OP ", - ) - - po.ovarrf( - self.outfile, - "Diamagnetic fraction (Hender)", - "(f_c_plasma_diamagnetic_hender)", - current_drive_variables.f_c_plasma_diamagnetic_hender, - "OP ", - ) - po.ovarrf( - self.outfile, - "Diamagnetic fraction (SCENE)", - "(f_c_plasma_diamagnetic_scene)", - current_drive_variables.f_c_plasma_diamagnetic_scene, - "OP ", - ) - po.ovarrf( - self.outfile, - "Pfirsch-Schlueter fraction (SCENE)", - "(f_c_plasma_pfirsch_schluter_scene)", - current_drive_variables.f_c_plasma_pfirsch_schluter_scene, - "OP ", - ) - # Error to catch if bootstap fraction limit has been enforced - if physics_variables.err242 == 1: - logger.error("Bootstrap fraction upper limit enforced") - - # Error to catch if self-driven current fraction limit has been enforced - if physics_variables.err243 == 1: - logger.error( - "Predicted plasma driven current is more than upper limit on non-inductive fraction" - ) - - if physics_variables.i_bootstrap_current == 0: - po.ocmmnt( - self.outfile, " (User-specified bootstrap current fraction used)" - ) - elif physics_variables.i_bootstrap_current == 1: - po.ocmmnt( - self.outfile, " (ITER 1989 bootstrap current fraction model used)" - ) - elif physics_variables.i_bootstrap_current == 2: - po.ocmmnt( - self.outfile, - " (Nevins et al bootstrap current fraction model used)", - ) - elif physics_variables.i_bootstrap_current == 3: - po.ocmmnt( - self.outfile, " (Wilson bootstrap current fraction model used)" - ) - elif physics_variables.i_bootstrap_current == 4: - po.ocmmnt( - self.outfile, - " (Sauter et al bootstrap current fraction model used)", - ) - elif physics_variables.i_bootstrap_current == 5: - po.ocmmnt( - self.outfile, - " (Sakai et al bootstrap current fraction model used)", - ) - elif physics_variables.i_bootstrap_current == 6: - po.ocmmnt( - self.outfile, - " (ARIES bootstrap current fraction model used)", - ) - elif physics_variables.i_bootstrap_current == 7: - po.ocmmnt( - self.outfile, - " (Andrade et al bootstrap current fraction model used)", - ) - elif physics_variables.i_bootstrap_current == 8: - po.ocmmnt( - self.outfile, - " (Hoang et al bootstrap current fraction model used)", - ) - elif physics_variables.i_bootstrap_current == 9: - po.ocmmnt( - self.outfile, - " (Wong et al bootstrap current fraction model used)", - ) - elif physics_variables.i_bootstrap_current == 10: - po.ocmmnt( - self.outfile, - " (Gi-I et al bootstrap current fraction model used)", - ) - elif physics_variables.i_bootstrap_current == 11: - po.ocmmnt( - self.outfile, - " (Gi-II et al bootstrap current fraction model used)", - ) - - if physics_variables.i_diamagnetic_current == 0: - po.ocmmnt( - self.outfile, " (Diamagnetic current fraction not calculated)" - ) - # Error to show if diamagnetic current is above 1% but not used - if current_drive_variables.f_c_plasma_diamagnetic_scene > 0.01e0: - logger.error( - "Diamagnetic fraction is more than 1%, but not calculated. " - "Consider using i_diamagnetic_current=2 and i_pfirsch_schluter_current=1" - ) - - elif physics_variables.i_diamagnetic_current == 1: - po.ocmmnt( - self.outfile, " (Hender diamagnetic current fraction scaling used)" - ) - elif physics_variables.i_diamagnetic_current == 2: - po.ocmmnt( - self.outfile, " (SCENE diamagnetic current fraction scaling used)" - ) - - if physics_variables.i_pfirsch_schluter_current == 0: - po.ocmmnt( - self.outfile, " Pfirsch-Schluter current fraction not calculated" - ) - elif physics_variables.i_pfirsch_schluter_current == 1: - po.ocmmnt( - self.outfile, - " (SCENE Pfirsch-Schluter current fraction scaling used)", - ) - - po.ovarrf( - self.outfile, - "Bootstrap fraction (enforced)", - "(f_c_plasma_bootstrap.)", - current_drive_variables.f_c_plasma_bootstrap, - "OP ", - ) - po.ovarrf( - self.outfile, - "Diamagnetic fraction (enforced)", - "(f_c_plasma_diamagnetic.)", - current_drive_variables.f_c_plasma_diamagnetic, - "OP ", - ) - po.ovarrf( - self.outfile, - "Pfirsch-Schlueter fraction (enforced)", - "(f_c_plasma_pfirsch_schluter.)", - current_drive_variables.f_c_plasma_pfirsch_schluter, - "OP ", - ) + self.bootstrap.output_bootstrap_current_information() po.osubhd(self.outfile, "Fuelling :") po.ovarre( @@ -10739,6 +10492,250 @@ def bootstrap_fraction_sugiyama_h_mode( * temp_plasma_pedestal_kev**0.0552 ) + def output_bootstrap_current_information(self): + """Output the calculated bootstrap current information to the output file.""" + + po.ovarrf( + self.outfile, + "Bootstrap current fraction multiplier", + "(cboot)", + current_drive_variables.cboot, + ) + po.ovarrf( + self.outfile, + "Bootstrap fraction (ITER 1989)", + "(f_c_plasma_bootstrap_iter89)", + current_drive_variables.f_c_plasma_bootstrap_iter89, + "OP ", + ) + + po.ovarrf( + self.outfile, + "Bootstrap fraction (Sauter et al)", + "(f_c_plasma_bootstrap_sauter)", + current_drive_variables.f_c_plasma_bootstrap_sauter, + "OP ", + ) + for point in range(len(physics_variables.j_plasma_bootstrap_sauter_profile)): + po.ovarrf( + self.mfile, + f"Sauter et al bootstrap current density profile at point {point}", + f"(j_plasma_bootstrap_sauter_profile{point})", + physics_variables.j_plasma_bootstrap_sauter_profile[point], + "OP ", + ) + + po.ovarrf( + self.outfile, + "Bootstrap fraction (Nevins et al)", + "(f_c_plasma_bootstrap_nevins)", + current_drive_variables.f_c_plasma_bootstrap_nevins, + "OP ", + ) + po.ovarrf( + self.outfile, + "Bootstrap fraction (Wilson)", + "(f_c_plasma_bootstrap_wilson)", + current_drive_variables.f_c_plasma_bootstrap_wilson, + "OP ", + ) + po.ovarrf( + self.outfile, + "Bootstrap fraction (Sakai)", + "(f_c_plasma_bootstrap_sakai)", + current_drive_variables.f_c_plasma_bootstrap_sakai, + "OP ", + ) + po.ovarrf( + self.outfile, + "Bootstrap fraction (ARIES)", + "(f_c_plasma_bootstrap_aries)", + current_drive_variables.f_c_plasma_bootstrap_aries, + "OP ", + ) + po.ovarrf( + self.outfile, + "Bootstrap fraction (Andrade)", + "(f_c_plasma_bootstrap_andrade)", + current_drive_variables.f_c_plasma_bootstrap_andrade, + "OP ", + ) + po.ovarrf( + self.outfile, + "Bootstrap fraction (Hoang)", + "(f_c_plasma_bootstrap_hoang)", + current_drive_variables.f_c_plasma_bootstrap_hoang, + "OP ", + ) + po.ovarrf( + self.outfile, + "Bootstrap fraction (Wong)", + "(f_c_plasma_bootstrap_wong)", + current_drive_variables.f_c_plasma_bootstrap_wong, + "OP ", + ) + po.ovarrf( + self.outfile, + "Bootstrap fraction (Gi I)", + "(bscf_gi_i)", + current_drive_variables.bscf_gi_i, + "OP ", + ) + po.ovarrf( + self.outfile, + "Bootstrap fraction (Gi II)", + "(bscf_gi_ii)", + current_drive_variables.bscf_gi_ii, + "OP ", + ) + po.ovarrf( + self.outfile, + "Bootstrap fraction (Sugiyama L-mode)", + "(f_c_plasma_bootstrap_sugiyama_l)", + current_drive_variables.f_c_plasma_bootstrap_sugiyama_l, + "OP ", + ) + po.ovarrf( + self.outfile, + "Bootstrap fraction (Sugiyama H-mode)", + "(f_c_plasma_bootstrap_sugiyama_h)", + current_drive_variables.f_c_plasma_bootstrap_sugiyama_h, + "OP ", + ) + + po.ovarrf( + self.outfile, + "Diamagnetic fraction (Hender)", + "(f_c_plasma_diamagnetic_hender)", + current_drive_variables.f_c_plasma_diamagnetic_hender, + "OP ", + ) + po.ovarrf( + self.outfile, + "Diamagnetic fraction (SCENE)", + "(f_c_plasma_diamagnetic_scene)", + current_drive_variables.f_c_plasma_diamagnetic_scene, + "OP ", + ) + po.ovarrf( + self.outfile, + "Pfirsch-Schlueter fraction (SCENE)", + "(f_c_plasma_pfirsch_schluter_scene)", + current_drive_variables.f_c_plasma_pfirsch_schluter_scene, + "OP ", + ) + # Error to catch if bootstap fraction limit has been enforced + if physics_variables.err242 == 1: + logger.error("Bootstrap fraction upper limit enforced") + + # Error to catch if self-driven current fraction limit has been enforced + if physics_variables.err243 == 1: + logger.error( + "Predicted plasma driven current is more than upper limit on non-inductive fraction" + ) + + if physics_variables.i_bootstrap_current == 0: + po.ocmmnt(self.outfile, " (User-specified bootstrap current fraction used)") + elif physics_variables.i_bootstrap_current == 1: + po.ocmmnt( + self.outfile, " (ITER 1989 bootstrap current fraction model used)" + ) + elif physics_variables.i_bootstrap_current == 2: + po.ocmmnt( + self.outfile, + " (Nevins et al bootstrap current fraction model used)", + ) + elif physics_variables.i_bootstrap_current == 3: + po.ocmmnt(self.outfile, " (Wilson bootstrap current fraction model used)") + elif physics_variables.i_bootstrap_current == 4: + po.ocmmnt( + self.outfile, + " (Sauter et al bootstrap current fraction model used)", + ) + elif physics_variables.i_bootstrap_current == 5: + po.ocmmnt( + self.outfile, + " (Sakai et al bootstrap current fraction model used)", + ) + elif physics_variables.i_bootstrap_current == 6: + po.ocmmnt( + self.outfile, + " (ARIES bootstrap current fraction model used)", + ) + elif physics_variables.i_bootstrap_current == 7: + po.ocmmnt( + self.outfile, + " (Andrade et al bootstrap current fraction model used)", + ) + elif physics_variables.i_bootstrap_current == 8: + po.ocmmnt( + self.outfile, + " (Hoang et al bootstrap current fraction model used)", + ) + elif physics_variables.i_bootstrap_current == 9: + po.ocmmnt( + self.outfile, + " (Wong et al bootstrap current fraction model used)", + ) + elif physics_variables.i_bootstrap_current == 10: + po.ocmmnt( + self.outfile, + " (Gi-I et al bootstrap current fraction model used)", + ) + elif physics_variables.i_bootstrap_current == 11: + po.ocmmnt( + self.outfile, + " (Gi-II et al bootstrap current fraction model used)", + ) + + if physics_variables.i_diamagnetic_current == 0: + po.ocmmnt(self.outfile, " (Diamagnetic current fraction not calculated)") + # Error to show if diamagnetic current is above 1% but not used + if current_drive_variables.f_c_plasma_diamagnetic_scene > 0.01e0: + logger.error( + "Diamagnetic fraction is more than 1%, but not calculated. " + "Consider using i_diamagnetic_current=2 and i_pfirsch_schluter_current=1" + ) + + elif physics_variables.i_diamagnetic_current == 1: + po.ocmmnt( + self.outfile, " (Hender diamagnetic current fraction scaling used)" + ) + elif physics_variables.i_diamagnetic_current == 2: + po.ocmmnt( + self.outfile, " (SCENE diamagnetic current fraction scaling used)" + ) + + if physics_variables.i_pfirsch_schluter_current == 0: + po.ocmmnt(self.outfile, " Pfirsch-Schluter current fraction not calculated") + elif physics_variables.i_pfirsch_schluter_current == 1: + po.ocmmnt( + self.outfile, + " (SCENE Pfirsch-Schluter current fraction scaling used)", + ) + + po.ovarrf( + self.outfile, + "Bootstrap fraction (enforced)", + "(f_c_plasma_bootstrap.)", + current_drive_variables.f_c_plasma_bootstrap, + "OP ", + ) + po.ovarrf( + self.outfile, + "Diamagnetic fraction (enforced)", + "(f_c_plasma_diamagnetic.)", + current_drive_variables.f_c_plasma_diamagnetic, + "OP ", + ) + po.ovarrf( + self.outfile, + "Pfirsch-Schlueter fraction (enforced)", + "(f_c_plasma_pfirsch_schluter.)", + current_drive_variables.f_c_plasma_pfirsch_schluter, + "OP ", + ) + class DetailedPhysics: """Class to hold detailed physics models for plasma processing.""" From fdc4948423231b69b7eb0d472b5a579f4d0705a4 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Thu, 26 Feb 2026 10:28:47 +0000 Subject: [PATCH 4/8] Post rebase structure updates --- process/main.py | 2 ++ process/models/physics/physics.py | 9 ++++++++- process/models/stellarator/stellarator.py | 2 ++ tests/unit/test_physics.py | 2 +- tests/unit/test_stellarator.py | 7 ++++++- 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/process/main.py b/process/main.py index b9d4cc0dc..1313c0c5e 100644 --- a/process/main.py +++ b/process/main.py @@ -97,6 +97,7 @@ DetailedPhysics, Physics, PlasmaBeta, + PlasmaBootstrapCurrent, PlasmaInductance, ) from process.models.physics.plasma_geometry import PlasmaGeom @@ -723,6 +724,7 @@ def __init__(self): physics=self.physics, neoclassics=self.neoclassics, plasma_beta=self.plasma_beta, + plasma_bootstrap=self.plasma_bootstrap, ) self.dcll = DCLL(fw=self.fw) diff --git a/process/models/physics/physics.py b/process/models/physics/physics.py index 753b17444..a26cd0cb9 100644 --- a/process/models/physics/physics.py +++ b/process/models/physics/physics.py @@ -1627,7 +1627,14 @@ def _trapped_particle_fraction_sauter( class Physics: - def __init__(self, plasma_profile, current_drive, plasma_beta, plasma_inductance, plasma_bootstrap): + def __init__( + self, + plasma_profile, + current_drive, + plasma_beta, + plasma_inductance, + plasma_bootstrap, + ): self.outfile = constants.NOUT self.mfile = constants.MFILE self.plasma_profile = plasma_profile diff --git a/process/models/stellarator/stellarator.py b/process/models/stellarator/stellarator.py index dee445a26..4d04f6181 100644 --- a/process/models/stellarator/stellarator.py +++ b/process/models/stellarator/stellarator.py @@ -100,6 +100,7 @@ def __init__( physics: Physics, neoclassics: Neoclassics, plasma_beta, + plasma_bootstrap, ) -> None: self.outfile: int = constants.NOUT self.first_call_stfwbs = True @@ -115,6 +116,7 @@ def __init__( self.physics = physics self.neoclassics = neoclassics self.beta = plasma_beta + self.bootstrap = plasma_bootstrap def run(self, output: bool): """Routine to call the physics and engineering modules diff --git a/tests/unit/test_physics.py b/tests/unit/test_physics.py index 2137552db..302f0c496 100644 --- a/tests/unit/test_physics.py +++ b/tests/unit/test_physics.py @@ -24,8 +24,8 @@ DetailedPhysics, Physics, PlasmaBeta, - PlasmaInductance, PlasmaBootstrapCurrent, + PlasmaInductance, calculate_current_coefficient_hastie, calculate_plasma_current_peng, calculate_poloidal_field, diff --git a/tests/unit/test_stellarator.py b/tests/unit/test_stellarator.py index 972e9f9ce..7a9634a4f 100644 --- a/tests/unit/test_stellarator.py +++ b/tests/unit/test_stellarator.py @@ -29,7 +29,12 @@ LowerHybrid, NeutralBeam, ) -from process.models.physics.physics import Physics, PlasmaBeta, PlasmaInductance +from process.models.physics.physics import ( + Physics, + PlasmaBeta, + PlasmaBootstrapCurrent, + PlasmaInductance, +) from process.models.physics.plasma_profiles import PlasmaProfile from process.models.power import Power from process.models.stellarator.build import st_build From 4459a43911f335ff50812730cca10b625efb78cd Mon Sep 17 00:00:00 2001 From: mn3981 Date: Thu, 26 Feb 2026 11:03:52 +0000 Subject: [PATCH 5/8] Create new bootstrap_current file --- process/main.py | 2 +- process/models/physics/bootstrap_current.py | 2150 +++ process/models/physics/physics.py | 14625 +++++++----------- tests/unit/test_physics.py | 2 +- tests/unit/test_stellarator.py | 2 +- 5 files changed, 7922 insertions(+), 8859 deletions(-) create mode 100644 process/models/physics/bootstrap_current.py diff --git a/process/main.py b/process/main.py index 1313c0c5e..d10c12cc5 100644 --- a/process/main.py +++ b/process/main.py @@ -84,6 +84,7 @@ from process.models.fw import FirstWall from process.models.ife import IFE from process.models.pfcoil import PFCoil +from process.models.physics.bootstrap_current import PlasmaBootstrapCurrent from process.models.physics.current_drive import ( CurrentDrive, ElectronBernstein, @@ -97,7 +98,6 @@ DetailedPhysics, Physics, PlasmaBeta, - PlasmaBootstrapCurrent, PlasmaInductance, ) from process.models.physics.plasma_geometry import PlasmaGeom diff --git a/process/models/physics/bootstrap_current.py b/process/models/physics/bootstrap_current.py new file mode 100644 index 000000000..f15824f8f --- /dev/null +++ b/process/models/physics/bootstrap_current.py @@ -0,0 +1,2150 @@ +import logging +from enum import IntEnum + +import numpy as np +import scipy +import scipy.integrate as integrate + +from process import constants +from process import process_output as po +from process.data_structure import ( + current_drive_variables, + physics_variables, +) +from process.exceptions import ProcessValueError +from process.models.physics.plasma_profiles import PlasmaProfile + +logger = logging.getLogger(__name__) + + +class BootstrapCurrentFractionModel(IntEnum): + """Bootstrap plasma current fraction (f_BS) model types""" + + USER_INPUT = 0 + ITER_89 = 1 + NEVINS = 2 + WILSON = 3 + SAUTER = 4 + SAKAI = 5 + ARIES = 6 + ANDRADE = 7 + HOANG = 8 + WONG = 9 + GI_1 = 10 + GI_2 = 11 + SUGIYAMA_L_MODE = 12 + SUGIYAMA_H_MODE = 13 + + +class PlasmaBootstrapCurrent: + """Class to hold plasma bootstrap current for plasma processing.""" + + def __init__(self): + self.outfile = constants.NOUT + self.mfile = constants.MFILE + self.sauter_bootstrap = SauterBootstrapCurrent() + + def get_bootstrap_current_fraction_value( + self, model: BootstrapCurrentFractionModel + ) -> float: + """Get the plasma current bootstrap fraction (f_BS) for the specified model.""" + model_map = { + BootstrapCurrentFractionModel.USER_INPUT: current_drive_variables.f_c_plasma_bootstrap, + BootstrapCurrentFractionModel.ITER_89: current_drive_variables.f_c_plasma_bootstrap_iter89, + BootstrapCurrentFractionModel.NEVINS: current_drive_variables.f_c_plasma_bootstrap_nevins, + BootstrapCurrentFractionModel.WILSON: current_drive_variables.f_c_plasma_bootstrap_wilson, + BootstrapCurrentFractionModel.SAUTER: current_drive_variables.f_c_plasma_bootstrap_sauter, + BootstrapCurrentFractionModel.SAKAI: current_drive_variables.f_c_plasma_bootstrap_sakai, + BootstrapCurrentFractionModel.ARIES: current_drive_variables.f_c_plasma_bootstrap_aries, + BootstrapCurrentFractionModel.ANDRADE: current_drive_variables.f_c_plasma_bootstrap_andrade, + BootstrapCurrentFractionModel.HOANG: current_drive_variables.f_c_plasma_bootstrap_hoang, + BootstrapCurrentFractionModel.WONG: current_drive_variables.f_c_plasma_bootstrap_wong, + BootstrapCurrentFractionModel.GI_1: current_drive_variables.bscf_gi_i, + BootstrapCurrentFractionModel.GI_2: current_drive_variables.bscf_gi_ii, + BootstrapCurrentFractionModel.SUGIYAMA_L_MODE: current_drive_variables.f_c_plasma_bootstrap_sugiyama_l, + BootstrapCurrentFractionModel.SUGIYAMA_H_MODE: current_drive_variables.f_c_plasma_bootstrap_sugiyama_h, + } + return model_map[model] + + @staticmethod + def bootstrap_fraction_iter89( + aspect: float, + beta: float, + b_plasma_toroidal_on_axis: float, + plasma_current: float, + q95: float, + q0: float, + rmajor: float, + vol_plasma: float, + ) -> float: + """ + Calculate the bootstrap-driven fraction of the plasma current. + + Args: + aspect (float): Plasma aspect ratio. + beta (float): Plasma total beta. + b_plasma_toroidal_on_axis (float): Toroidal field on axis (T). + plasma_current (float): Plasma current (A). + q95 (float): Safety factor at 95% surface. + q0 (float): Central safety factor. + rmajor (float): Plasma major radius (m). + vol_plasma (float): Plasma volume (m3). + + Returns: + float: The bootstrap-driven fraction of the plasma current. + + This function performs the original ITER calculation of the plasma current bootstrap fraction. + + Reference: + - ITER Physics Design Guidelines: 1989 [IPDG89], N. A. Uckan et al, + - ITER Documentation Series No.10, IAEA/ITER/DS/10, IAEA, Vienna, 1990 + """ + + # Calculate the bootstrap current coefficient + c_bs = 1.32 - 0.235 * (q95 / q0) + 0.0185 * (q95 / q0) ** 2 + + # Calculate the average minor radius + average_a = np.sqrt(vol_plasma / (2 * np.pi**2 * rmajor)) + + b_pa = (plasma_current / 1e6) / (5 * average_a) + + # Calculate the poloidal beta for bootstrap current + betapbs = beta * (b_plasma_toroidal_on_axis / b_pa) ** 2 + + # Ensure betapbs is positive + if betapbs <= 0.0: + return 0.0 + + # Calculate and return the bootstrap current fraction + return c_bs * (betapbs / np.sqrt(aspect)) ** 1.3 + + @staticmethod + def bootstrap_fraction_wilson( + alphaj: float, + alphap: float, + alphat: float, + betpth: float, + q0: float, + q95: float, + rmajor: float, + rminor: float, + ) -> float: + """ + Bootstrap current fraction from Wilson et al scaling + + Args: + alphaj (float): Current profile index. + alphap (float): Pressure profile index. + alphat (float): Temperature profile index. + betpth (float): Thermal component of poloidal beta. + q0 (float): Safety factor on axis. + q95 (float): Edge safety factor. + rmajor (float): Major radius (m). + rminor (float): Minor radius (m). + + Returns: + float: The bootstrap current fraction. + + This function calculates the bootstrap current fraction using the numerically fitted algorithm written by Howard Wilson. + + Reference: + - AEA FUS 172: Physics Assessment for the European Reactor Study, 1989 + - H. R. Wilson, Nuclear Fusion 32 (1992) 257 + """ + + term1 = np.log(0.5) + term2 = np.log(q0 / q95) + + # Re-arranging of parabolic profile to be equal to (r/a)^2 where the profile value is half of the core value + + termp = 1.0 - 0.5 ** (1.0 / alphap) + termt = 1.0 - 0.5 ** (1.0 / alphat) + termj = 1.0 - 0.5 ** (1.0 / alphaj) + + # Assuming a parabolic safety factor profile of the form q = q0 + (q95 - q0) * (r/a)^2 + # Substitute (r/a)^2 term from temperature,pressure and current profiles into q profile when values is 50% of core value + # Take natural log of q profile over q95 and q0 to get the profile index + + alfpnw = term1 / np.log(np.log((q0 + (q95 - q0) * termp) / q95) / term2) + alftnw = term1 / np.log(np.log((q0 + (q95 - q0) * termt) / q95) / term2) + aj = term1 / np.log(np.log((q0 + (q95 - q0) * termj) / q95) / term2) + + # Crude check for NaN errors or other illegal values. + if np.isnan(aj) or np.isnan(alfpnw) or np.isnan(alftnw) or aj < 0: + raise ProcessValueError( + "Illegal profile value found", aj=aj, alfpnw=alfpnw, alftnw=alftnw + ) + + # Ratio of ionic charge to electron charge + + z = 1.0 + + # Inverse aspect ratio: r2 = maximum plasma radius, r1 = minimum + # This is the definition used in the paper + r2 = rmajor + rminor + r1 = rmajor - rminor + eps1 = (r2 - r1) / (r2 + r1) + + # Coefficients fitted using least squares techniques + + # Square root of current profile index term + saj = np.sqrt(aj) + + a = np.array([ + 1.41 * (1.0 - 0.28 * saj) * (1.0 + 0.12 / z), + 0.36 * (1.0 - 0.59 * saj) * (1.0 + 0.8 / z), + -0.27 * (1.0 - 0.47 * saj) * (1.0 + 3.0 / z), + 0.0053 * (1.0 + 5.0 / z), + -0.93 * (1.0 - 0.34 * saj) * (1.0 + 0.15 / z), + -0.26 * (1.0 - 0.57 * saj) * (1.0 - 0.27 * z), + 0.064 * (1.0 - 0.6 * aj + 0.15 * aj * aj) * (1.0 + 7.6 / z), + -0.0011 * (1.0 + 9.0 / z), + -0.33 * (1.0 - aj + 0.33 * aj * aj), + -0.26 * (1.0 - 0.87 / saj - 0.16 * aj), + -0.14 * (1.0 - 1.14 / saj - 0.45 * saj), + -0.0069, + ]) + + seps1 = np.sqrt(eps1) + + b = np.array([ + 1.0, + alfpnw, + alftnw, + alfpnw * alftnw, + seps1, + alfpnw * seps1, + alftnw * seps1, + alfpnw * alftnw * seps1, + eps1, + alfpnw * eps1, + alftnw * eps1, + alfpnw * alftnw * eps1, + ]) + + # Empirical bootstrap current fraction + return seps1 * betpth * (a * b).sum() + + @staticmethod + def _nevins_integral( + y: float, + nd_plasma_electrons_vol_avg: float, + te: float, + b_plasma_toroidal_on_axis: float, + rminor: float, + rmajor: float, + zeff: float, + alphat: float, + alphan: float, + q0: float, + q95: float, + beta_toroidal: float, + ) -> float: + """Integrand function for Nevins et al bootstrap current scaling. + + This function calculates the integrand function for the Nevins et al bootstrap current scaling. + + Parameters + ---------- + y : + abscissa of integration, normalized minor radius + nd_plasma_electrons_vol_avg : + volume averaged electron density (/m^3) + te : + volume averaged electron temperature (keV) + b_plasma_toroidal_on_axis : + toroidal field on axis (T) + rminor : + plasma minor radius (m) + rmajor : + plasma major radius (m) + zeff : + plasma effective charge + alphat : + temperature profile index + alphan : + density profile index + q0 : + normalized safety factor at the magnetic axis + q95 : + normalized safety factor at 95% of the plasma radius + beta_toroidal : + Toroidal plasma beta + + + Returns + ------- + type + - float, the integrand value + + + Reference: See appendix of: + Keii Gi, Makoto Nakamura, Kenji Tobita, Yasushi Ono, + Bootstrap current fraction scaling for a tokamak reactor design study, + Fusion Engineering and Design, Volume 89, Issue 11, 2014, Pages 2709-2715, + ISSN 0920-3796, https://doi.org/10.1016/j.fusengdes.2014.07.009. + + Nevins, W. M. "Summary report: ITER specialists' meeting on heating and current drive." + ITER-TN-PH-8-4, June 1988. 1988. + + """ + + # Compute average electron beta + betae = ( + nd_plasma_electrons_vol_avg + * te + * 1.0e3 + * constants.ELECTRON_CHARGE + / (b_plasma_toroidal_on_axis**2 / (2.0 * constants.RMU0)) + ) + + nabla = rminor * np.sqrt(y) / rmajor + x = (1.46 * np.sqrt(nabla) + 2.4 * nabla) / (1.0 - nabla) ** 1.5 + z = zeff + d = ( + 1.414 * z + + z**2 + + x * (0.754 + 2.657 * z + (2.0 * z**2)) + + (x**2 * (0.348 + 1.243 * z + z**2)) + ) + a1 = (alphan + alphat) * (1.0 - y) ** (alphan + alphat - 1.0) + a2 = alphat * (1.0 - y) ** (alphan + alphat - 1.0) + al1 = (x / d) * (0.754 + 2.21 * z + z**2 + x * (0.348 + 1.243 * z + z**2)) + al2 = -x * ((0.884 + 2.074 * z) / d) + alphai = -1.172 / (1.0 + 0.462 * x) + + # q-profile + q = q0 + (q95 - q0) * ((y + y**2 + y**3) / (3.0)) + + pratio = (beta_toroidal - betae) / betae + + return (q / q95) * (al1 * (a1 + (pratio * (a1 + alphai * a2))) + al2 * a2) + + def bootstrap_fraction_sauter( + self, plasma_profile: PlasmaProfile + ) -> tuple[float, np.ndarray]: + """Get the bootstrap current fraction using Sauter et al scaling. + + Args: + plasma_profile: The plasma profile object. + + Returns: + tuple: (bootstrap fraction, bootstrap current density profile) + """ + return self.sauter_bootstrap.bootstrap_fraction_sauter(plasma_profile) + + def bootstrap_fraction_nevins( + self, + alphan: float, + alphat: float, + beta_toroidal: float, + b_plasma_toroidal_on_axis: float, + nd_plasma_electrons_vol_avg: float, + plasma_current: float, + q95: float, + q0: float, + rmajor: float, + rminor: float, + te: float, + zeff: float, + ) -> float: + """ + Calculate the bootstrap current fraction from Nevins et al scaling. + + Args: + alphan (float): Density profile index. + alphat (float): Temperature profile index. + beta_toroidal (float): Toroidal plasma beta. + b_plasma_toroidal_on_axis (float): Toroidal field on axis (T). + nd_plasma_electrons_vol_avg (float): Electron density (/m3). + plasma_current (float): Plasma current (A). + q0 (float): Central safety factor. + q95 (float): Safety factor at 95% surface. + rmajor (float): Plasma major radius (m). + rminor (float): Plasma minor radius (m). + te (float): Volume averaged plasma temperature (keV). + zeff (float): Plasma effective charge. + + Returns: + float: The bootstrap current fraction. + + This function calculates the bootstrap current fraction using the Nevins et al method. + + Reference: See appendix of: + Keii Gi, Makoto Nakamura, Kenji Tobita, Yasushi Ono, + Bootstrap current fraction scaling for a tokamak reactor design study, + Fusion Engineering and Design, Volume 89, Issue 11, 2014, Pages 2709-2715, + ISSN 0920-3796, https://doi.org/10.1016/j.fusengdes.2014.07.009. + + Nevins, W. M. "Summary report: ITER specialists' meeting on heating and current drive." + ITER-TN-PH-8-4, June 1988. 1988. + + """ + # Calculate peak electron beta at plasma centre, this is not the form used in the paper + # The paper assumes parabolic profiles for calculating core values with the profile indexes. + # We instead use the directly calculated electron density and temperature values at the core. + # So that it is compatible with all profiles + + betae0 = ( + physics_variables.nd_plasma_electron_on_axis + * physics_variables.temp_plasma_electron_on_axis_kev + * 1.0e3 + * constants.ELECTRON_CHARGE + / (b_plasma_toroidal_on_axis**2 / (2.0 * constants.RMU0)) + ) + + # Call integration routine using definite integral routine from scipy + + ainteg, _ = integrate.quad( + lambda y: self._nevins_integral( + y, + nd_plasma_electrons_vol_avg, + te, + b_plasma_toroidal_on_axis, + rminor, + rmajor, + zeff, + alphat, + alphan, + q0, + q95, + beta_toroidal, + ), + 0, # Lower bound + 1.0, # Upper bound + ) + + # Calculate bootstrap current and fraction + + aibs = 2.5 * betae0 * rmajor * b_plasma_toroidal_on_axis * q95 * ainteg + return 1.0e6 * aibs / plasma_current + + @staticmethod + def bootstrap_fraction_sakai( + beta_poloidal: float, + q95: float, + q0: float, + alphan: float, + alphat: float, + eps: float, + ind_plasma_internal_norm: float, + ) -> float: + """ + Calculate the bootstrap fraction using the Sakai formula. + + Parameters: + beta_poloidal (float): Plasma poloidal beta. + q95 (float): Safety factor at 95% of the plasma radius. + q0 (float): Safety factor at the magnetic axis. + alphan (float): Density profile index + alphat (float): Temperature profile index + eps (float): Inverse aspect ratio. + ind_plasma_internal_norm (float): Plasma normalised internal inductance + + Returns: + float: The calculated bootstrap fraction. + + Notes: + The profile assumed for the alphan and alpat indexes is only a parabolic profile without a pedestal (L-mode). + The Root Mean Squared Error for the fitting database of this formula was 0.025 + Concentrating on the positive shear plasmas using the ACCOME code equilibria with the fully non-inductively driven + conditions with neutral beam (NB) injection only are calculated. + The electron temperature and the ion temperature were assumed to be equal + This can be used for all apsect ratios. + The diamagnetic fraction is included in this formula. + + References: + Ryosuke Sakai, Takaaki Fujita, Atsushi Okamoto, Derivation of bootstrap current fraction scaling formula for 0-D system code analysis, + Fusion Engineering and Design, Volume 149, 2019, 111322, ISSN 0920-3796, + https://doi.org/10.1016/j.fusengdes.2019.111322. + """ + # Sakai states that the ACCOME dataset used has the toridal diamagnetic current included in the bootstrap current + # So the diamganetic current should not be calculated with this. i_diamagnetic_current = 0 + return ( + 10 ** (0.951 * eps - 0.948) + * beta_poloidal ** (1.226 * eps + 1.584) + * ind_plasma_internal_norm ** (-0.184 * eps - 0.282) + * (q95 / q0) ** (-0.042 * eps - 0.02) + * alphan ** (0.13 * eps + 0.05) + * alphat ** (0.502 * eps - 0.273) + ) + + @staticmethod + def bootstrap_fraction_aries( + beta_poloidal: float, + ind_plasma_internal_norm: float, + core_density: float, + average_density: float, + inverse_aspect: float, + ) -> float: + """ + Calculate the bootstrap fraction using the ARIES formula. + + Parameters: + beta_poloidal (float): Plasma poloidal beta. + ind_plasma_internal_norm (float): Plasma normalized internal inductance. + core_density (float): Core plasma density. + average_density (float): Average plasma density. + inverse_aspect (float): Inverse aspect ratio. + + Returns: + float: The calculated bootstrap fraction. + + Notes: + - The source reference does not provide any info about the derivation of the formula. It is only stated + + References: + - Zoran Dragojlovic et al., “An advanced computational algorithm for systems analysis of tokamak power plants,” + Fusion Engineering and Design, vol. 85, no. 2, pp. 243-265, Apr. 2010, + doi: https://doi.org/10.1016/j.fusengdes.2010.02.015. + + """ + # Using the standard variable naming from the ARIES paper + a_1 = ( + 1.10 - 1.165 * ind_plasma_internal_norm + 0.47 * ind_plasma_internal_norm**2 + ) + b_1 = ( + 0.806 + - 0.885 * ind_plasma_internal_norm + + 0.297 * ind_plasma_internal_norm**2 + ) + + c_bs = a_1 + b_1 * (core_density / average_density) + + return c_bs * np.sqrt(inverse_aspect) * beta_poloidal + + @staticmethod + def bootstrap_fraction_andrade( + beta_poloidal: float, + core_pressure: float, + average_pressure: float, + inverse_aspect: float, + ) -> float: + """ + Calculate the bootstrap fraction using the Andrade et al formula. + + Parameters: + beta_poloidal (float): Plasma poloidal beta. + core_pressure (float): Core plasma pressure. + average_pressure (float): Average plasma pressure. + inverse_aspect (float): Inverse aspect ratio. + + Returns: + float: The calculated bootstrap fraction. + + Notes: + - Based off 350 plasma profiles from Experimento Tokamak Esferico (ETE) spherical tokamak + - A = 1.5, R_0 = 0.3m, I_p = 200kA, B_0=0.4T, beta = 4-10%. Profiles taken as Gaussian shaped functions. + - Errors mostly up to the order of 10% are obtained when both expressions are compared with the equilibrium estimates for the + bootstrap current in ETE + + References: + - M. C. R. Andrade and G. O. Ludwig, “Scaling of bootstrap current on equilibrium and plasma profile parameters in tokamak plasmas,” + Plasma Physics and Controlled Fusion, vol. 50, no. 6, pp. 065001-065001, Apr. 2008, + doi: https://doi.org/10.1088/0741-3335/50/6/065001. + + """ + + # Using the standard variable naming from the Andrade et.al. paper + c_p = core_pressure / average_pressure + + # Error +- 0.0007 + c_bs = 0.2340 + + return c_bs * np.sqrt(inverse_aspect) * beta_poloidal * c_p**0.8 + + @staticmethod + def bootstrap_fraction_hoang( + beta_poloidal: float, + pressure_index: float, + current_index: float, + inverse_aspect: float, + ) -> float: + """ + Calculate the bootstrap fraction using the Hoang et al formula. + + Parameters: + beta_poloidal (float): Plasma poloidal beta. + pressure_index (float): Pressure profile index. + current_index (float): Current profile index. + inverse_aspect (float): Inverse aspect ratio. + + Returns: + float: The calculated bootstrap fraction. + + Notes: + - Based off of TFTR data calculated using the TRANSP plasma analysis code + - 170 discharges which was assembled to study the tritium influx and transport in discharges with D-only neutral beam + injection (NBI) + - Contains L-mode, supershots, reversed shear, enhanced reversed shear and increased li discharges + - Discharges with monotonic flux profiles with reversed shear are also included + - Is applied to circular cross-section plasmas + + References: + - G. T. Hoang and R. V. Budny, “The bootstrap fraction in TFTR,” AIP conference proceedings, + Jan. 1997, doi: https://doi.org/10.1063/1.53414. + """ + + # Using the standard variable naming from the Hoang et.al. paper + # Hoang et.al uses a different definition for the profile indexes such that + # alpha_p is defined as the ratio of the central and the volume-averaged values, and the peakedness of the density of the total plasma current + # (defined as ratio of the central value and I_p), alpha_j$ + + # We assume the pressure and current profile is parabolic and use the (profile_index +1) term in lieu + # The term represents the ratio of the the core to volume averaged value + + # This could lead to large changes in the value depending on interpretation of the profile index + + c_bs = np.sqrt((pressure_index + 1) / (current_index + 1)) + + return 0.4 * np.sqrt(inverse_aspect) * beta_poloidal**0.9 * c_bs + + @staticmethod + def bootstrap_fraction_wong( + beta_poloidal: float, + density_index: float, + temperature_index: float, + inverse_aspect: float, + elongation: float, + ) -> float: + """ + Calculate the bootstrap fraction using the Wong et al formula. + + Parameters: + beta_poloidal (float): Plasma poloidal beta. + density_index (float): Density profile index. + temperature_index (float): Temperature profile index. + inverse_aspect (float): Inverse aspect ratio. + elongation (float): Plasma elongation. + + Returns: + float: The calculated bootstrap fraction. + + Notes: + - Data is based off of equilibria from Miller et al. + - A: 1.2 - 3.0 and stable to n ballooning and low n kink modes at a bootstrap fraction of 99% for kappa = 2, 2.5 and 3 + - The results were parameterized as a function of aspect ratio and elongation + - The parametric dependency of beta_p and beta_T are based on fitting of the DIII-D high equivalent DT yield results + - Parabolic profiles should be used for best results as the pressure peaking value is calculated as the product of a parabolic + temperature and density profile + + References: + - C.-P. Wong, J. C. Wesley, R. D. Stambaugh, and E. T. Cheng, “Toroidal reactor designs as a function of aspect ratio and elongation,” + vol. 42, no. 5, pp. 547-556, May 2002, doi: https://doi.org/10.1088/0029-5515/42/5/307. + + - Miller, R L, "Stable bootstrap-current driven equilibria for low aspect ratio tokamaks". + Switzerland: N. p., 1996. Web.https://fusion.gat.com/pubs-ext/MISCONF96/A22433.pdf + """ + # Using the standard variable naming from the Wong et.al. paper + f_peak = 2.0 / scipy.special.beta(0.5, density_index + temperature_index + 1) + + c_bs = 0.773 + 0.019 * elongation + + return c_bs * f_peak**0.25 * beta_poloidal * np.sqrt(inverse_aspect) + + @staticmethod + def bootstrap_fraction_gi_I( # noqa: N802 + beta_poloidal: float, + pressure_index: float, + temperature_index: float, + inverse_aspect: float, + effective_charge: float, + q95: float, + q0: float, + ) -> float: + """ + Calculate the bootstrap fraction using the first scaling from the Gi et al formula. + + Parameters: + beta_poloidal (float): Plasma poloidal beta. + pressure_index (float): Pressure profile index. + temperature_index (float): Temperature profile index. + inverse_aspect (float): Inverse aspect ratio. + effective_charge (float): Plasma effective charge. + q95 (float): Safety factor at 95% of the plasma radius. + q0 (float): Safety factor at the magnetic axis. + + Returns: + float: The calculated bootstrap fraction. + + Notes: + - Scaling found by solving the Hirshman-Sigmar bootstrap modelusing the matrix inversion method + - Method was done to put the scaling into parameters compatible with the TPC systems code + - Uses the ACCOME code to create bootstrap current fractions without using the itrative calculations of the + curent drive and equilibrium models in the scan + - R = 5.0 m, A = 1.3 - 5.0, kappa = 2, traing = 0.3, alpha_n = 0.1 - 0.8, alpha_t = 1.0 - 3.0, Z_eff = 1.2 - 3.0 + - Uses parabolic plasma profiles only. + - Scaling 1 has better accuracy than Scaling 2. However, Scaling 1 overestimated the f_BS value for reversed shear + equilibrium. + + References: + - K. Gi, M. Nakamura, Kenji Tobita, and Y. Ono, “Bootstrap current fraction scaling for a tokamak reactor design study,” + Fusion Engineering and Design, vol. 89, no. 11, pp. 2709-2715, Aug. 2014, + doi: https://doi.org/10.1016/j.fusengdes.2014.07.009. + """ + + # Using the standard variable naming from the Gi et.al. paper + + c_bs = ( + 0.474 + * inverse_aspect**-0.1 + * pressure_index**0.974 + * temperature_index**-0.416 + * effective_charge**0.178 + * (q95 / q0) ** -0.133 + ) + + return c_bs * np.sqrt(inverse_aspect) * beta_poloidal + + @staticmethod + def bootstrap_fraction_gi_II( # noqa: N802 + beta_poloidal: float, + pressure_index: float, + temperature_index: float, + inverse_aspect: float, + effective_charge: float, + ) -> float: + """ + Calculate the bootstrap fraction using the second scaling from the Gi et al formula. + + Parameters: + beta_poloidal (float): Plasma poloidal beta. + pressure_index (float): Pressure profile index. + temperature_index (float): Temperature profile index. + inverse_aspect (float): Inverse aspect ratio. + effective_charge (float): Plasma effective charge. + + Returns: + float: The calculated bootstrap fraction. + + Notes: + - Scaling found by solving the Hirshman-Sigmar bootstrap modelusing the matrix inversion method + - Method was done to put the scaling into parameters compatible with the TPC systems code + - Uses the ACCOME code to create bootstrap current fractions without using the itrative calculations of the + curent drive and equilibrium models in the scan + - R = 5.0 m, A = 1.3 - 5.0, kappa = 2, traing = 0.3, alpha_n = 0.1 - 0.8, alpha_t = 1.0 - 3.0, Z_eff = 1.2 - 3.0 + - Uses parabolic plasma profiles only. + - This scaling has the q profile dependance removed to obtain a scaling formula with much more flexible variables than + that by a single profile factor for internal current profile. + + References: + - K. Gi, M. Nakamura, Kenji Tobita, and Y. Ono, “Bootstrap current fraction scaling for a tokamak reactor design study,” + Fusion Engineering and Design, vol. 89, no. 11, pp. 2709-2715, Aug. 2014, + doi: https://doi.org/10.1016/j.fusengdes.2014.07.009. + """ + + # Using the standard variable naming from the Gi et.al. paper + + c_bs = ( + 0.382 + * inverse_aspect**-0.242 + * pressure_index**0.974 + * temperature_index**-0.416 + * effective_charge**0.178 + ) + + return c_bs * np.sqrt(inverse_aspect) * beta_poloidal + + @staticmethod + def bootstrap_fraction_sugiyama_l_mode( + eps: float, + beta_poloidal: float, + alphan: float, + alphat: float, + zeff: float, + q95: float, + q0: float, + ) -> float: + """ + Calculate the bootstrap fraction using the L-mode scaling from the Sugiyama et al formula. + + :param eps: Inverse aspect ratio. + :type eps: float + :param beta_poloidal: Plasma poloidal beta. + :type beta_poloidal: float + :param alphan: Density profile index. + :type alphan: float + :param alphat: Temperature profile index. + :type alphat: float + :param zeff: Plasma effective charge. + :type zeff: float + :param q95: Safety factor at 95% of the plasma radius. + :type q95: float + :param q0: Safety factor at the magnetic axis. + :type q0: float + + :returns: The calculated bootstrap fraction. + :rtype: float + + :notes: + - This scaling is derived for L-mode plasmas. + - Ion and electron temperature are the same + - Z_eff has a uniform profile, with only fully stripped carbon impurity + + :references: + - S. Sugiyama, T. Goto, H. Utoh, and Y. Sakamoto, “Improvement of core plasma power and + current balance models for tokamak systems code considering H-mode plasma profiles,” + Fusion Engineering and Design, vol. 216, p. 115022, Jul. 2025, doi: + https://doi.org/10.1016/j.fusengdes.2025.115022. + """ + + return ( + 0.740 + * eps**0.418 + * beta_poloidal**0.904 + * alphan**0.06 + * alphat**-0.138 + * zeff**0.230 + * (q95 / q0) ** -0.142 + ) + + @staticmethod + def bootstrap_fraction_sugiyama_h_mode( + eps: float, + beta_poloidal: float, + alphan: float, + alphat: float, + tbeta: float, + zeff: float, + q95: float, + q0: float, + radius_plasma_pedestal_density_norm: float, + nd_plasma_pedestal_electron: float, + n_greenwald: float, + temp_plasma_pedestal_kev: float, + ) -> float: + """ + Calculate the bootstrap fraction using the H-mode scaling from the Sugiyama et al formula. + + :param eps: Inverse aspect ratio. + :type eps: float + :param beta_poloidal: Plasma poloidal beta. + :type beta_poloidal: float + :param alphan: Density profile index. + :type alphan: float + :param alphat: Temperature profile index. + :type alphat: float + :param tbeta: Second temperature profile index. + :type tbeta: float + :param zeff: Plasma effective charge. + :type zeff: float + :param q95: Safety factor at 95% of the plasma radius. + :type q95: float + :param q0: Safety factor at the magnetic axis. + :type q0: float + :param radius_plasma_pedestal_density_norm: Normalised plasma radius of density pedestal. + :type radius_plasma_pedestal_density_norm: float + :param nd_plasma_pedestal_electron: Electron number density at the pedestal [m^-3]. + :type nd_plasma_pedestal_electron: float + :param n_greenwald: Greenwald density limit [m^-3]. + :type n_greenwald: float + :param temp_plasma_pedestal_kev: Electron temperature at the pedestal [keV]. + :type temp_plasma_pedestal_kev: float + + :returns: The calculated bootstrap fraction. + :rtype: float + + :notes: + - This scaling is derived for H-mode plasmas. + - The temperature and density pedestal positions are the same + - Separatrix temperature and density are zero + - Ion and electron temperature are the same + - Z_eff has a uniform profile, with only fully stripped carbon impurity + + :references: + - S. Sugiyama, T. Goto, H. Utoh, and Y. Sakamoto, “Improvement of core plasma power and + current balance models for tokamak systems code considering H-mode plasma profiles,” + Fusion Engineering and Design, vol. 216, p. 115022, Jul. 2025, doi: + https://doi.org/10.1016/j.fusengdes.2025.115022. + """ + + return ( + 0.789 + * eps**0.606 + * beta_poloidal**0.960 + * alphan**0.0319 + * alphat**0.00822 + * tbeta**-0.0783 + * zeff**0.241 + * (q95 / q0) ** -0.103 + * radius_plasma_pedestal_density_norm**0.367 + * (nd_plasma_pedestal_electron / n_greenwald) ** -0.174 + * temp_plasma_pedestal_kev**0.0552 + ) + + def output_bootstrap_current_information(self): + """Output the calculated bootstrap current information to the output file.""" + + po.ovarrf( + self.outfile, + "Bootstrap current fraction multiplier", + "(cboot)", + current_drive_variables.cboot, + ) + po.ovarrf( + self.outfile, + "Bootstrap fraction (ITER 1989)", + "(f_c_plasma_bootstrap_iter89)", + current_drive_variables.f_c_plasma_bootstrap_iter89, + "OP ", + ) + + po.ovarrf( + self.outfile, + "Bootstrap fraction (Sauter et al)", + "(f_c_plasma_bootstrap_sauter)", + current_drive_variables.f_c_plasma_bootstrap_sauter, + "OP ", + ) + for point in range(len(physics_variables.j_plasma_bootstrap_sauter_profile)): + po.ovarrf( + self.mfile, + f"Sauter et al bootstrap current density profile at point {point}", + f"(j_plasma_bootstrap_sauter_profile{point})", + physics_variables.j_plasma_bootstrap_sauter_profile[point], + "OP ", + ) + + po.ovarrf( + self.outfile, + "Bootstrap fraction (Nevins et al)", + "(f_c_plasma_bootstrap_nevins)", + current_drive_variables.f_c_plasma_bootstrap_nevins, + "OP ", + ) + po.ovarrf( + self.outfile, + "Bootstrap fraction (Wilson)", + "(f_c_plasma_bootstrap_wilson)", + current_drive_variables.f_c_plasma_bootstrap_wilson, + "OP ", + ) + po.ovarrf( + self.outfile, + "Bootstrap fraction (Sakai)", + "(f_c_plasma_bootstrap_sakai)", + current_drive_variables.f_c_plasma_bootstrap_sakai, + "OP ", + ) + po.ovarrf( + self.outfile, + "Bootstrap fraction (ARIES)", + "(f_c_plasma_bootstrap_aries)", + current_drive_variables.f_c_plasma_bootstrap_aries, + "OP ", + ) + po.ovarrf( + self.outfile, + "Bootstrap fraction (Andrade)", + "(f_c_plasma_bootstrap_andrade)", + current_drive_variables.f_c_plasma_bootstrap_andrade, + "OP ", + ) + po.ovarrf( + self.outfile, + "Bootstrap fraction (Hoang)", + "(f_c_plasma_bootstrap_hoang)", + current_drive_variables.f_c_plasma_bootstrap_hoang, + "OP ", + ) + po.ovarrf( + self.outfile, + "Bootstrap fraction (Wong)", + "(f_c_plasma_bootstrap_wong)", + current_drive_variables.f_c_plasma_bootstrap_wong, + "OP ", + ) + po.ovarrf( + self.outfile, + "Bootstrap fraction (Gi I)", + "(bscf_gi_i)", + current_drive_variables.bscf_gi_i, + "OP ", + ) + po.ovarrf( + self.outfile, + "Bootstrap fraction (Gi II)", + "(bscf_gi_ii)", + current_drive_variables.bscf_gi_ii, + "OP ", + ) + po.ovarrf( + self.outfile, + "Bootstrap fraction (Sugiyama L-mode)", + "(f_c_plasma_bootstrap_sugiyama_l)", + current_drive_variables.f_c_plasma_bootstrap_sugiyama_l, + "OP ", + ) + po.ovarrf( + self.outfile, + "Bootstrap fraction (Sugiyama H-mode)", + "(f_c_plasma_bootstrap_sugiyama_h)", + current_drive_variables.f_c_plasma_bootstrap_sugiyama_h, + "OP ", + ) + + po.ovarrf( + self.outfile, + "Diamagnetic fraction (Hender)", + "(f_c_plasma_diamagnetic_hender)", + current_drive_variables.f_c_plasma_diamagnetic_hender, + "OP ", + ) + po.ovarrf( + self.outfile, + "Diamagnetic fraction (SCENE)", + "(f_c_plasma_diamagnetic_scene)", + current_drive_variables.f_c_plasma_diamagnetic_scene, + "OP ", + ) + po.ovarrf( + self.outfile, + "Pfirsch-Schlueter fraction (SCENE)", + "(f_c_plasma_pfirsch_schluter_scene)", + current_drive_variables.f_c_plasma_pfirsch_schluter_scene, + "OP ", + ) + # Error to catch if bootstap fraction limit has been enforced + if physics_variables.err242 == 1: + logger.error("Bootstrap fraction upper limit enforced") + + # Error to catch if self-driven current fraction limit has been enforced + if physics_variables.err243 == 1: + logger.error( + "Predicted plasma driven current is more than upper limit on non-inductive fraction" + ) + + if physics_variables.i_bootstrap_current == 0: + po.ocmmnt(self.outfile, " (User-specified bootstrap current fraction used)") + elif physics_variables.i_bootstrap_current == 1: + po.ocmmnt( + self.outfile, " (ITER 1989 bootstrap current fraction model used)" + ) + elif physics_variables.i_bootstrap_current == 2: + po.ocmmnt( + self.outfile, + " (Nevins et al bootstrap current fraction model used)", + ) + elif physics_variables.i_bootstrap_current == 3: + po.ocmmnt(self.outfile, " (Wilson bootstrap current fraction model used)") + elif physics_variables.i_bootstrap_current == 4: + po.ocmmnt( + self.outfile, + " (Sauter et al bootstrap current fraction model used)", + ) + elif physics_variables.i_bootstrap_current == 5: + po.ocmmnt( + self.outfile, + " (Sakai et al bootstrap current fraction model used)", + ) + elif physics_variables.i_bootstrap_current == 6: + po.ocmmnt( + self.outfile, + " (ARIES bootstrap current fraction model used)", + ) + elif physics_variables.i_bootstrap_current == 7: + po.ocmmnt( + self.outfile, + " (Andrade et al bootstrap current fraction model used)", + ) + elif physics_variables.i_bootstrap_current == 8: + po.ocmmnt( + self.outfile, + " (Hoang et al bootstrap current fraction model used)", + ) + elif physics_variables.i_bootstrap_current == 9: + po.ocmmnt( + self.outfile, + " (Wong et al bootstrap current fraction model used)", + ) + elif physics_variables.i_bootstrap_current == 10: + po.ocmmnt( + self.outfile, + " (Gi-I et al bootstrap current fraction model used)", + ) + elif physics_variables.i_bootstrap_current == 11: + po.ocmmnt( + self.outfile, + " (Gi-II et al bootstrap current fraction model used)", + ) + + if physics_variables.i_diamagnetic_current == 0: + po.ocmmnt(self.outfile, " (Diamagnetic current fraction not calculated)") + # Error to show if diamagnetic current is above 1% but not used + if current_drive_variables.f_c_plasma_diamagnetic_scene > 0.01e0: + logger.error( + "Diamagnetic fraction is more than 1%, but not calculated. " + "Consider using i_diamagnetic_current=2 and i_pfirsch_schluter_current=1" + ) + + elif physics_variables.i_diamagnetic_current == 1: + po.ocmmnt( + self.outfile, " (Hender diamagnetic current fraction scaling used)" + ) + elif physics_variables.i_diamagnetic_current == 2: + po.ocmmnt( + self.outfile, " (SCENE diamagnetic current fraction scaling used)" + ) + + if physics_variables.i_pfirsch_schluter_current == 0: + po.ocmmnt(self.outfile, " Pfirsch-Schluter current fraction not calculated") + elif physics_variables.i_pfirsch_schluter_current == 1: + po.ocmmnt( + self.outfile, + " (SCENE Pfirsch-Schluter current fraction scaling used)", + ) + + po.ovarrf( + self.outfile, + "Bootstrap fraction (enforced)", + "(f_c_plasma_bootstrap.)", + current_drive_variables.f_c_plasma_bootstrap, + "OP ", + ) + po.ovarrf( + self.outfile, + "Diamagnetic fraction (enforced)", + "(f_c_plasma_diamagnetic.)", + current_drive_variables.f_c_plasma_diamagnetic, + "OP ", + ) + po.ovarrf( + self.outfile, + "Pfirsch-Schlueter fraction (enforced)", + "(f_c_plasma_pfirsch_schluter.)", + current_drive_variables.f_c_plasma_pfirsch_schluter, + "OP ", + ) + + +class SauterBootstrapCurrent: + """Class to calculate the bootstrap current using the Sauter et al formula.""" + + def bootstrap_fraction_sauter(self, plasma_profile: float) -> float: + """Calculate the bootstrap current fraction from the Sauter et al scaling. + + Args: + plasma_profile (PlasmaProfile): The plasma profile object. + + Returns: + float: The bootstrap current fraction. + + This function calculates the bootstrap current fraction using the Sauter, Angioni, and Lin-Liu scaling. + + Reference: + - O. Sauter, C. Angioni, Y. R. Lin-Liu; + Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime. + Phys. Plasmas 1 July 1999; 6 (7): 2834-2839. https://doi.org/10.1063/1.873240 + - O. Sauter, C. Angioni, Y. R. Lin-Liu; Erratum: + Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime + [Phys. Plasmas 6, 2834 (1999)]. Phys. Plasmas 1 December 2002; 9 (12): 5140. https://doi.org/10.1063/1.1517052 + + Note: + The code was supplied by Emiliano Fable, IPP Garching (private communication). + """ + + # Radial points from 0 to 1 seperated by 1/profile_size + roa = plasma_profile.neprofile.profile_x + + # Local circularised minor radius + rho = np.sqrt(physics_variables.a_plasma_poloidal / np.pi) * roa + + # Square root of local aspect ratio + sqeps = np.sqrt(roa * (physics_variables.rminor / physics_variables.rmajor)) + + # Calculate electron and ion density profiles + ne = plasma_profile.neprofile.profile_y * 1e-19 + ni = ( + physics_variables.nd_plasma_ions_total_vol_avg + / physics_variables.nd_plasma_electrons_vol_avg + ) * ne + + # Calculate electron and ion temperature profiles + tempe = plasma_profile.teprofile.profile_y + tempi = ( + physics_variables.temp_plasma_ion_vol_avg_kev + / physics_variables.temp_plasma_electron_vol_avg_kev + ) * tempe + + # Flat Zeff profile assumed + # Return tempi like array object filled with zeff + zeff = np.full_like(tempi, physics_variables.n_charge_plasma_effective_vol_avg) + + # inverse_q = 1/safety factor + # Parabolic q profile assumed + inverse_q = 1 / ( + physics_variables.q0 + + (physics_variables.q95 - physics_variables.q0) * roa**2 + ) + # Create new array of average mass of fuel portion of ions + amain = np.full_like(inverse_q, physics_variables.m_ions_total_amu) + + # Create new array of average main ion charge + zmain = np.full_like(inverse_q, 1.0 + physics_variables.f_plasma_fuel_helium3) + + # Calculate total bootstrap current (MA) by summing along profiles + # Looping from 2 because _calculate_l31_coefficient() etc should return 0 @ j == 1 + radial_elements = np.arange(2, plasma_profile.profile_size) + + # Change in localised minor radius to be used as delta term in derivative + drho = rho[radial_elements] - rho[radial_elements - 1] + + # Area of annulus, assuming circular plasma cross-section + da = 2 * np.pi * rho[radial_elements - 1] * drho # area of annulus + + # Create the partial derivatives using numpy gradient (central differences) + dlogte_drho = np.gradient(np.log(tempe), rho)[radial_elements - 1] + dlogti_drho = np.gradient(np.log(tempi), rho)[radial_elements - 1] + dlogne_drho = np.gradient(np.log(ne), rho)[radial_elements - 1] + + jboot = ( + 0.5 + * ( + self._calculate_l31_coefficient( + radial_elements, + plasma_profile.profile_size, + physics_variables.rmajor, + physics_variables.b_plasma_toroidal_on_axis, + physics_variables.triang, + ne, + ni, + tempe, + tempi, + inverse_q, + rho, + zeff, + sqeps, + ) + * dlogne_drho + + self._calculate_l31_32_coefficient( + radial_elements, + plasma_profile.profile_size, + physics_variables.rmajor, + physics_variables.b_plasma_toroidal_on_axis, + physics_variables.triang, + ne, + ni, + tempe, + tempi, + inverse_q, + rho, + zeff, + sqeps, + ) + * dlogte_drho + + self._calculate_l34_alpha_31_coefficient( + radial_elements, + plasma_profile.profile_size, + physics_variables.rmajor, + physics_variables.b_plasma_toroidal_on_axis, + physics_variables.triang, + inverse_q, + sqeps, + tempi, + tempe, + amain, + zmain, + ni, + ne, + rho, + zeff, + ) + * dlogti_drho + ) + * 1.0e6 + * ( + -physics_variables.b_plasma_toroidal_on_axis + / (0.2 * np.pi * physics_variables.rmajor) + * rho[radial_elements - 1] + * inverse_q[radial_elements - 1] + ) + ) # A/m2 + + return (np.sum(da * jboot, axis=0) / physics_variables.plasma_current), jboot + + @staticmethod + def _coulomb_logarithm_sauter( + radial_elements: int, tempe: np.ndarray, ne: np.ndarray + ) -> np.ndarray: + """Calculate the Coulomb logarithm used in the arrays for the Sauter bootstrap current scaling. + + This function calculates the Coulomb logarithm, which is valid for e-e collisions (T_e > 0.01 keV) + and for e-i collisions (T_e > 0.01*Zeff^2) (Alexander, 9/5/1994). + + Parameters + ---------- + radial_elements : + the radial element indexes in the range 1 to nr + tempe : + the electron temperature array + ne : + the electron density array + + Returns + ------- + type + - np.ndarray, the Coulomb logarithm at each array point + + Reference: + - C. A. Ordonez, M. I. Molina; + Evaluation of the Coulomb logarithm using cutoff and screened Coulomb interaction potentials. + Phys. Plasmas 1 August 1994; 1 (8): 2515-2518. https://doi.org/10.1063/1.870578 + - Y. R. Shen, “Recent advances in nonlinear optics,” Reviews of Modern Physics, vol. 48, no. 1, + pp. 1-32, Jan. 1976, doi: https://doi.org/10.1103/revmodphys.48.1. + + """ + return ( + 15.9 + - 0.5 * np.log(ne[radial_elements - 1]) + + np.log(tempe[radial_elements - 1]) + ) + + def _electron_collisions_sauter( + self, radial_elements: np.ndarray, tempe: np.ndarray, ne: np.ndarray + ) -> np.ndarray: + """Calculate the frequency of electron-electron collisions used in the arrays for the Sauter bootstrap current scaling. + + This function calculates the frequency of electron-electron collisions + + Parameters + ---------- + radial_elements : + the radial element indexes in the range 1 to nr + tempe : + the electron temperature array + ne : + the electron density array + + Returns + ------- + : + the frequency of electron-electron collisions (Hz) + + + Reference: + - Yushmanov, 25th April 1987 (?), updated by Pereverzev, 9th November 1994 (?) + + """ + return ( + 670.0 + * self._coulomb_logarithm_sauter(radial_elements, tempe, ne) + * ne[radial_elements - 1] + / (tempe[radial_elements - 1] * np.sqrt(tempe[radial_elements - 1])) + ) + + def _electron_collisionality_sauter( + self, + radial_elements: np.ndarray, + rmajor: float, + zeff: np.ndarray, + inverse_q: np.ndarray, + sqeps: np.ndarray, + tempe: np.ndarray, + ne: np.ndarray, + ) -> np.ndarray: + """Calculate the electron collisionality used in the arrays for the Sauter bootstrap current scaling. + + Parameters + ---------- + radial_elements : + the radial element index in the range 1 to nr + rmajor : + the plasma major radius (m) + zeff : + the effective charge array + inverse_q : + inverse safety factor profile + sqeps : + the square root of the inverse aspect ratio array + tempe : + the electron temperature array + ne : + the electron density array + + Returns + ------- + : + the relative frequency of electron collisions + + Reference: + - Yushmanov, 30th April 1987 (?) + + """ + return ( + self._electron_collisions_sauter(radial_elements, tempe, ne) + * 1.4 + * zeff[radial_elements - 1] + * rmajor + / np.abs( + inverse_q[radial_elements - 1] + * (sqeps[radial_elements - 1] ** 3) + * np.sqrt(tempe[radial_elements - 1]) + * 1.875e7 + ) + ) + + @staticmethod + def _ion_collisions_sauter( + radial_elements: np.ndarray, + zeff: np.ndarray, + ni: np.ndarray, + tempi: np.ndarray, + amain: np.ndarray, + ) -> np.ndarray: + """Calculate the full frequency of ion collisions used in the arrays for the Sauter bootstrap current scaling. + + This function calculates the full frequency of ion collisions using the Coulomb logarithm of 15. + + Parameters + ---------- + radial_elements : + the radial element indexes in the range 1 to nr + zeff : + the effective charge array + ni : + the ion density array + tempi : + the ion temperature array + amain : + the atomic mass of the main ion species array + + Returns + ------- + : + the full frequency of ion collisions (Hz) + """ + return ( + zeff[radial_elements - 1] ** 4 + * ni[radial_elements - 1] + * 322.0 + / ( + tempi[radial_elements - 1] + * np.sqrt(tempi[radial_elements - 1] * amain[radial_elements - 1]) + ) + ) + + def _ion_collisionality_sauter( + self, + radial_elements: np.ndarray, + rmajor: float, + inverse_q: np.ndarray, + sqeps: np.ndarray, + tempi: np.ndarray, + amain: np.ndarray, + zeff: np.ndarray, + ni: np.ndarray, + ) -> float: + """Calculate the ion collisionality to be used in the Sauter bootstrap current scaling. + + Parameters + ---------- + radial_elements : + the radial element indexes in the range 1 to nr + rmajor : + the plasma major radius (m) + inverse_q : + inverse safety factor profile + sqeps : + the square root of the inverse aspect ratio profile + tempi : + the ion temperature profile (keV) + amain : + the atomic mass of the main ion species profile + zeff : + the effective charge of the main ion species + ni : + the ion density profile (/m^3) + + Returns + ------- + : + the ion collisionality + """ + return ( + 3.2e-6 + * self._ion_collisions_sauter(radial_elements, zeff, ni, tempi, amain) + * rmajor + / ( + np.abs(inverse_q[radial_elements - 1] + 1.0e-4) + * sqeps[radial_elements - 1] ** 3 + * np.sqrt(tempi[radial_elements - 1] / amain[radial_elements - 1]) + ) + ) + + def _calculate_l31_coefficient( + self, + radial_elements: np.ndarray, + number_of_elements: int, + rmajor: float, + b_plasma_toroidal_on_axis: float, + triang: float, + ne: np.ndarray, + ni: np.ndarray, + tempe: np.ndarray, + tempi: np.ndarray, + inverse_q: np.ndarray, + rho: np.ndarray, + zeff: np.ndarray, + sqeps: np.ndarray, + ) -> float: + """L31 coefficient before Grad(ln(ne)) in the Sauter bootstrap scaling. + + + Parameters + ---------- + radial_elements : + radial element indexes in range 2 to nr + number_of_elements : + maximum value of radial_elements + rmajor : + plasma major radius (m) + b_plasma_toroidal_on_axis : + toroidal field on axis (T) + triang : + plasma triangularity + ne : + electron density profile (/m^3) + ni : + ion density profile (/m^3) + tempe : + electron temperature profile (keV) + tempi : + ion temperature profile (keV) + inverse_q : + inverse safety factor profile + rho : + normalized minor radius profile + zeff : + effective charge profile + sqeps : + square root of inverse aspect ratio profile + + Returns + ------- + type + - float, the coefficient scaling grad(ln(ne)) in the Sauter bootstrap current scaling. + + This function calculates the coefficient scaling grad(ln(ne)) in the Sauter bootstrap current scaling. + + Reference: + - O. Sauter, C. Angioni, Y. R. Lin-Liu; + Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime. + Phys. Plasmas 1 July 1999; 6 (7): 2834-2839. https://doi.org/10.1063/1.873240 + - O. Sauter, C. Angioni, Y. R. Lin-Liu; Erratum: + Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime + [Phys. Plasmas 6, 2834 (1999)]. Phys. Plasmas 1 December 2002; 9 (12): 5140. https://doi.org/10.1063/1.1517052 + + """ + # Prevents first element being 0 + charge_profile = zeff[radial_elements - 1] + + # Calculate trapped particle fraction + f_trapped = self._trapped_particle_fraction_sauter( + radial_elements, triang, sqeps + ) + + # Calculated electron collisionality; nu_e* + electron_collisionality = self._electron_collisionality_sauter( + radial_elements, rmajor, zeff, inverse_q, sqeps, tempe, ne + ) + + # $f^{31}_{teff}(\nu_{e*})$, Eq.14b + f31_teff = f_trapped / ( + (1.0 + (1.0 - 0.1 * f_trapped) * np.sqrt(electron_collisionality)) + + (0.5 * (1.0 - f_trapped) * electron_collisionality) / charge_profile + ) + + l31_coefficient = ( + ((1.0 + 1.4 / (charge_profile + 1.0)) * f31_teff) + - (1.9 / (charge_profile + 1.0) * f31_teff**2) + + ((0.3 * f31_teff**3 + 0.2 * f31_teff**4) / (charge_profile + 1.0)) + ) + + # Corrections suggested by Fable, 15/05/2015 + return l31_coefficient * self._beta_poloidal_total_sauter( + radial_elements, + number_of_elements, + rmajor, + b_plasma_toroidal_on_axis, + ne, + ni, + tempe, + tempi, + inverse_q, + rho, + ) + + def _calculate_l31_32_coefficient( + self, + radial_elements: np.ndarray, + number_of_elements: int, + rmajor: float, + b_plasma_toroidal_on_axis: float, + triang: float, + ne: np.ndarray, + ni: np.ndarray, + tempe: np.ndarray, + tempi: np.ndarray, + inverse_q: np.ndarray, + rho: np.ndarray, + zeff: np.ndarray, + sqeps: np.ndarray, + ) -> float: + """L31 & L32 coefficient before Grad(ln(Te)) in the Sauter bootstrap scaling. + + This function calculates the coefficient scaling grad(ln(Te)) in the Sauter bootstrap current scaling. + + + Parameters + ---------- + radial_elements : + radial element indexes in range 2 to nr + number_of_elements : + maximum value of radial_elements + rmajor : + plasma major radius (m) + b_plasma_toroidal_on_axis : + toroidal field on axis (T) + triang : + plasma triangularity + ne : + electron density profile (/m^3) + ni : + ion density profile (/m^3) + tempe : + electron temperature profile (keV) + tempi : + ion temperature profile (keV) + inverse_q : + inverse safety factor profile + rho : + normalized minor radius profile + zeff : + effective charge profile + sqeps : + square root of inverse aspect ratio profile + + + Returns + ------- + : + the L31 & L32 coefficient scaling grad(ln(Te)) in the Sauter bootstrap current scaling. + + + Reference: + - O. Sauter, C. Angioni, Y. R. Lin-Liu; + Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime. + Phys. Plasmas 1 July 1999; 6 (7): 2834-2839. https://doi.org/10.1063/1.873240 + - O. Sauter, C. Angioni, Y. R. Lin-Liu; Erratum: + Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime + [Phys. Plasmas 6, 2834 (1999)]. Phys. Plasmas 1 December 2002; 9 (12): 5140. https://doi.org/10.1063/1.1517052 + + """ + + # Prevents first element being 0 + charge_profile = zeff[radial_elements - 1] + + # Calculate trapped particle fraction + f_trapped = self._trapped_particle_fraction_sauter( + radial_elements, triang, sqeps + ) + + # Calculated electron collisionality; nu_e* + electron_collisionality = self._electron_collisionality_sauter( + radial_elements, rmajor, zeff, inverse_q, sqeps, tempe, ne + ) + + # $f^{32\_ee}_{teff}(\nu_{e*})$, Eq.15d + f32ee_teff = f_trapped / ( + 1.0 + + 0.26 * (1.0 - f_trapped) * np.sqrt(electron_collisionality) + + ( + 0.18 + * (1.0 - 0.37 * f_trapped) + * electron_collisionality + / np.sqrt(charge_profile) + ) + ) + + # $f^{32\_ei}_{teff}(\nu_{e*})$, Eq.15e + f32ei_teff = f_trapped / ( + (1.0 + (1.0 + 0.6 * f_trapped) * np.sqrt(electron_collisionality)) + + ( + 0.85 + * (1.0 - 0.37 * f_trapped) + * electron_collisionality + * (1.0 + charge_profile) + ) + ) + + # $F_{32\_ee}(X)$, Eq.15b + big_f32ee_teff = ( + ( + (0.05 + 0.62 * charge_profile) + / charge_profile + / (1.0 + 0.44 * charge_profile) + * (f32ee_teff - f32ee_teff**4) + ) + + ( + (f32ee_teff**2 - f32ee_teff**4 - 1.2 * (f32ee_teff**3 - f32ee_teff**4)) + / (1.0 + 0.22 * charge_profile) + ) + + (1.2 / (1.0 + 0.5 * charge_profile) * f32ee_teff**4) + ) + + # $F_{32\_ei}(Y)$, Eq.15c + big_f32ei_teff = ( + ( + -(0.56 + 1.93 * charge_profile) + / charge_profile + / (1.0 + 0.44 * charge_profile) + * (f32ei_teff - f32ei_teff**4) + ) + + ( + 4.95 + / (1.0 + 2.48 * charge_profile) + * ( + f32ei_teff**2 + - f32ei_teff**4 + - 0.55 * (f32ei_teff**3 - f32ei_teff**4) + ) + ) + - (1.2 / (1.0 + 0.5 * charge_profile) * f32ei_teff**4) + ) + + # big_f32ee_teff + big_f32ei_teff = L32 coefficient + + # Corrections suggested by Fable, 15/05/2015 + return self._beta_poloidal_sauter( + radial_elements, + number_of_elements, + rmajor, + b_plasma_toroidal_on_axis, + ne, + tempe, + inverse_q, + rho, + ) * (big_f32ee_teff + big_f32ei_teff) + self._calculate_l31_coefficient( + radial_elements, + number_of_elements, + rmajor, + b_plasma_toroidal_on_axis, + triang, + ne, + ni, + tempe, + tempi, + inverse_q, + rho, + zeff, + sqeps, + ) * self._beta_poloidal_sauter( + radial_elements, + number_of_elements, + rmajor, + b_plasma_toroidal_on_axis, + ne, + tempe, + inverse_q, + rho, + ) / self._beta_poloidal_total_sauter( + radial_elements, + number_of_elements, + rmajor, + b_plasma_toroidal_on_axis, + ne, + ni, + tempe, + tempi, + inverse_q, + rho, + ) + + def _calculate_l34_alpha_31_coefficient( + self, + radial_elements: np.ndarray, + number_of_elements: int, + rmajor: float, + b_plasma_toroidal_on_axis: float, + triang: float, + inverse_q: np.ndarray, + sqeps: np.ndarray, + tempi: np.ndarray, + tempe: np.ndarray, + amain: float, + zmain: float, + ni: np.ndarray, + ne: np.ndarray, + rho: np.ndarray, + zeff: np.ndarray, + ) -> float: + """L34, alpha and L31 coefficient before Grad(ln(Ti)) in the Sauter bootstrap scaling. + + This function calculates the coefficient scaling grad(ln(Ti)) in the Sauter bootstrap current scaling. + + + Parameters + ---------- + radial_elements : + radial element indexes in range 2 to nr + number_of_elements : + maximum value of radial_elements + rmajor : + plasma major radius (m) + b_plasma_toroidal_on_axis : + toroidal field on axis (T) + triang : + plasma triangularity + inverse_q : + inverse safety factor profile + sqeps : + square root of inverse aspect ratio profile + tempi : + ion temperature profile (keV) + tempe : + electron temperature profile (keV) + amain : + atomic mass of the main ion + zmain : + charge of the main ion + ni : + ion density profile (/m^3) + ne : + electron density profile (/m^3) + rho : + normalized minor radius profile + zeff : + effective charge profile + + + Returns + ------- + : + the L34, alpha and L31 coefficient scaling grad(ln(Ti)) in the Sauter bootstrap current scaling. + + + Reference: + - O. Sauter, C. Angioni, Y. R. Lin-Liu; + Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime. + Phys. Plasmas 1 July 1999; 6 (7): 2834-2839. https://doi.org/10.1063/1.873240 + - O. Sauter, C. Angioni, Y. R. Lin-Liu; Erratum: + Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime + [Phys. Plasmas 6, 2834 (1999)]. Phys. Plasmas 1 December 2002; 9 (12): 5140. https://doi.org/10.1063/1.1517052 + + """ + # Prevents first element being 0 + charge_profile = zeff[radial_elements - 1] + + # Calculate trapped particle fraction + f_trapped = self._trapped_particle_fraction_sauter( + radial_elements, triang, sqeps + ) + + # Calculated electron collisionality; nu_e* + electron_collisionality = self._electron_collisionality_sauter( + radial_elements, rmajor, zeff, inverse_q, sqeps, tempe, ne + ) + + # $f^{34}_{teff}(\nu_{e*})$, Eq.16b + f34_teff = f_trapped / ( + (1.0 + (1.0 - 0.1 * f_trapped) * np.sqrt(electron_collisionality)) + + 0.5 * (1.0 - 0.5 * f_trapped) * electron_collisionality / charge_profile + ) + + # Eq.16a + l34_coefficient = ( + ((1.0 + (1.4 / (charge_profile + 1.0))) * f34_teff) + - ((1.9 / (charge_profile + 1.0)) * f34_teff**2) + + ((0.3 / (charge_profile + 1.0)) * f34_teff**3) + + ((0.2 / (charge_profile + 1.0)) * f34_teff**4) + ) + + # $\alpha_0$, Eq.17a + alpha_0 = (-1.17 * (1.0 - f_trapped)) / ( + 1.0 - (0.22 * f_trapped) - 0.19 * f_trapped**2 + ) + + # Calculate the ion collisionality + ion_collisionality = self._ion_collisionality_sauter( + radial_elements, rmajor, inverse_q, sqeps, tempi, amain, zmain, ni + ) + + # $\alpha(\nu_{i*})$, Eq.17b + alpha = ( + (alpha_0 + (0.25 * (1.0 - f_trapped**2)) * np.sqrt(ion_collisionality)) + / (1.0 + (0.5 * np.sqrt(ion_collisionality))) + + (0.315 * ion_collisionality**2 * f_trapped**6) + ) / (1.0 + (0.15 * ion_collisionality**2 * f_trapped**6)) + + # Corrections suggested by Fable, 15/05/2015 + # Below calculates the L34 * alpha + L31 coefficient + return ( + self._beta_poloidal_total_sauter( + radial_elements, + number_of_elements, + rmajor, + b_plasma_toroidal_on_axis, + ne, + ni, + tempe, + tempi, + inverse_q, + rho, + ) + - self._beta_poloidal_sauter( + radial_elements, + number_of_elements, + rmajor, + b_plasma_toroidal_on_axis, + ne, + tempe, + inverse_q, + rho, + ) + ) * (l34_coefficient * alpha) + self._calculate_l31_coefficient( + radial_elements, + number_of_elements, + rmajor, + b_plasma_toroidal_on_axis, + triang, + ne, + ni, + tempe, + tempi, + inverse_q, + rho, + zeff, + sqeps, + ) * ( + 1.0 + - self._beta_poloidal_sauter( + radial_elements, + number_of_elements, + rmajor, + b_plasma_toroidal_on_axis, + ne, + tempe, + inverse_q, + rho, + ) + / self._beta_poloidal_total_sauter( + radial_elements, + number_of_elements, + rmajor, + b_plasma_toroidal_on_axis, + ne, + ni, + tempe, + tempi, + inverse_q, + rho, + ) + ) + + @staticmethod + def _beta_poloidal_sauter( + radial_elements: np.ndarray, + nr: int, + rmajor: float, + b_plasma_toroidal_on_axis: float, + ne: np.ndarray, + tempe: np.ndarray, + inverse_q: np.ndarray, + rho: np.ndarray, + ) -> np.ndarray: + """Calculate the local beta poloidal using only electron profiles for the Sauter bootstrap current scaling. + + Parameters + ---------- + radial_elements : + radial element indexes in range 1 to nr + nr : + maximum value of radial_elements + rmajor : + plasma major radius (m) + b_plasma_toroidal_on_axis : + toroidal field on axis (T) + ne : + electron density profile (/m^3) + tempe : + electron temperature profile (keV) + inverse_q : + inverse safety factor profile + rho : + normalized minor radius profile + + + Returns + ------- + : + the local beta poloidal + """ + return ( + np.where( + radial_elements != nr, + 1.6e-4 + * np.pi + * (ne[radial_elements] + ne[radial_elements - 1]) + * (tempe[radial_elements] + tempe[radial_elements - 1]), + 6.4e-4 * np.pi * ne[radial_elements - 1] * tempe[radial_elements - 1], + ) + * ( + rmajor + / ( + b_plasma_toroidal_on_axis + * rho[radial_elements - 1] + * np.abs(inverse_q[radial_elements - 1] + 1.0e-4) + ) + ) + ** 2 + ) + + @staticmethod + def _beta_poloidal_total_sauter( + radial_elements: np.ndarray, + nr: int, + rmajor: float, + b_plasma_toroidal_on_axis: float, + ne: np.ndarray, + ni: np.ndarray, + tempe: np.ndarray, + tempi: np.ndarray, + inverse_q: np.ndarray, + rho: np.ndarray, + ) -> np.ndarray: + """Calculate the local beta poloidal including ion pressure for the Sauter bootstrap current scaling. + + Parameters + ---------- + radial_elements : + radial element indexes in range 1 to nr + nr : + maximum value of radial_elements + rmajor : + plasma major radius (m) + b_plasma_toroidal_on_axis : + toroidal field on axis (T) + ne : + electron density profile (/m^3) + ni : + ion density profile (/m^3) + tempe : + electron temperature profile (keV) + tempi : + ion temperature profile (keV) + inverse_q : + inverse safety factor profile + rho : + normalized minor radius profile + + Returns + ------- + : + the local total beta poloidal + """ + return ( + np.where( + radial_elements != nr, + 1.6e-4 + * np.pi + * ( + ( + (ne[radial_elements] + ne[radial_elements - 1]) + * (tempe[radial_elements] + tempe[radial_elements - 1]) + ) + + ( + (ni[radial_elements] + ni[radial_elements - 1]) + * (tempi[radial_elements] + tempi[radial_elements - 1]) + ) + ), + 6.4e-4 + * np.pi + * ( + ne[radial_elements - 1] * tempe[radial_elements - 1] + + ni[radial_elements - 1] * tempi[radial_elements - 1] + ), + ) + * ( + rmajor + / ( + b_plasma_toroidal_on_axis + * rho[radial_elements - 1] + * np.abs(inverse_q[radial_elements - 1] + 1.0e-4) + ) + ) + ** 2 + ) + + @staticmethod + def _trapped_particle_fraction_sauter( + radial_elements: np.ndarray, triang: float, sqeps: np.ndarray, fit: int = 0 + ) -> np.ndarray: + """Calculates the trapped particle fraction to be used in the Sauter bootstrap current scaling. + Parameters + ---------- + radial_elements : + radial element index in range 1 to nr + triang : + plasma triangularity + sqeps : + square root of local aspect ratio + fit : + fit method (1 = ASTRA method, 2 = Equation from Sauter 2002, 3 = Equation from Sauter 2016) + + Returns + ------- + type + - list, trapped particle fraction + + This function calculates the trapped particle fraction at a given radius. + + References + ---------- + Used in this paper: + - O. Sauter, C. Angioni, Y. R. Lin-Liu; + Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime. + Phys. Plasmas 1 July 1999; 6 (7): 2834-2839. https://doi.org/10.1063/1.873240 + + - O. Sauter, R. J. Buttery, R. Felton, T. C. Hender, D. F. Howell, and contributors to the E.-J. Workprogramme, + “Marginal-limit for neoclassical tearing modes in JET H-mode discharges,” + Plasma Physics and Controlled Fusion, vol. 44, no. 9, pp. 1999-2019, Aug. 2002, + doi: https://doi.org/10.1088/0741-3335/44/9/315. + + - O. Sauter, Geometric formulas for system codes including the effect of negative triangularity, + Fusion Engineering and Design, Volume 112, 2016, Pages 633-645, ISSN 0920-3796, + https://doi.org/10.1016/j.fusengdes.2016.04.033. + + """ + # Prevent first element from being zero + sqeps_reduced = sqeps[radial_elements - 1] + eps = sqeps_reduced**2 + + if fit == 0: + # ASTRA method, from Emiliano Fable, private communication + # (Excluding h term which dominates for inverse aspect ratios < 0.5, + # and tends to take the trapped particle fraction to 1) + + zz = 1.0 - eps + return 1.0 - zz * np.sqrt(zz) / (1.0 + 1.46 * sqeps_reduced) + + if fit == 1: + # Equation 4 of Sauter 2002; https://doi.org/10.1088/0741-3335/44/9/315. + # Similar to, but not quite identical to above + + return 1.0 - ( + ((1.0 - eps) ** 2) + / ((1.0 + 1.46 * sqeps_reduced) * np.sqrt(1.0 - eps**2)) + ) + + if fit == 2: + # Sauter 2016; https://doi.org/10.1016/j.fusengdes.2016.04.033. + # Includes correction for triangularity + + epseff = 0.67 * (1.0 - 1.4 * triang * np.abs(triang)) * eps + + return 1.0 - ( + ((1.0 - epseff) / (1.0 + 2.0 * np.sqrt(epseff))) + * (np.sqrt((1.0 - eps) / (1.0 + eps))) + ) + + raise ProcessValueError(f"fit={fit} is not valid. Must be 1, 2, or 3") diff --git a/process/models/physics/physics.py b/process/models/physics/physics.py index a26cd0cb9..1c3391c59 100644 --- a/process/models/physics/physics.py +++ b/process/models/physics/physics.py @@ -4,8 +4,6 @@ import numba as nb import numpy as np -import scipy -import scipy.integrate as integrate from scipy.optimize import root_scalar import process.models.physics.confinement_time as confinement @@ -30,6 +28,9 @@ stellarator_variables, times_variables, ) +from process.models.physics.bootstrap_current import ( + BootstrapCurrentFractionModel, +) logger = logging.getLogger(__name__) @@ -571,107 +572,6 @@ def calculate_current_coefficient_fiesta( return 0.538 * (1.0 + 2.440 * eps**2.736) * kappa**2.154 * triang**0.060 -# -------------------------------- -# Bootstrap Current Calculations -# -------------------------------- - - -@nb.jit(nopython=True, cache=True) -def _nevins_integral( - y: float, - nd_plasma_electrons_vol_avg: float, - te: float, - b_plasma_toroidal_on_axis: float, - rminor: float, - rmajor: float, - zeff: float, - alphat: float, - alphan: float, - q0: float, - q95: float, - beta_toroidal: float, -) -> float: - """Integrand function for Nevins et al bootstrap current scaling. - - This function calculates the integrand function for the Nevins et al bootstrap current scaling. - - Parameters - ---------- - y : - abscissa of integration, normalized minor radius - nd_plasma_electrons_vol_avg : - volume averaged electron density (/m^3) - te : - volume averaged electron temperature (keV) - b_plasma_toroidal_on_axis : - toroidal field on axis (T) - rminor : - plasma minor radius (m) - rmajor : - plasma major radius (m) - zeff : - plasma effective charge - alphat : - temperature profile index - alphan : - density profile index - q0 : - normalized safety factor at the magnetic axis - q95 : - normalized safety factor at 95% of the plasma radius - beta_toroidal : - Toroidal plasma beta - - - Returns - ------- - type - - float, the integrand value - - - Reference: See appendix of: - Keii Gi, Makoto Nakamura, Kenji Tobita, Yasushi Ono, - Bootstrap current fraction scaling for a tokamak reactor design study, - Fusion Engineering and Design, Volume 89, Issue 11, 2014, Pages 2709-2715, - ISSN 0920-3796, https://doi.org/10.1016/j.fusengdes.2014.07.009. - - Nevins, W. M. "Summary report: ITER specialists' meeting on heating and current drive." - ITER-TN-PH-8-4, June 1988. 1988. - - """ - - # Compute average electron beta - betae = ( - nd_plasma_electrons_vol_avg - * te - * 1.0e3 - * constants.ELECTRON_CHARGE - / (b_plasma_toroidal_on_axis**2 / (2.0 * constants.RMU0)) - ) - - nabla = rminor * np.sqrt(y) / rmajor - x = (1.46 * np.sqrt(nabla) + 2.4 * nabla) / (1.0 - nabla) ** 1.5 - z = zeff - d = ( - 1.414 * z - + z**2 - + x * (0.754 + 2.657 * z + (2.0 * z**2)) - + (x**2 * (0.348 + 1.243 * z + z**2)) - ) - a1 = (alphan + alphat) * (1.0 - y) ** (alphan + alphat - 1.0) - a2 = alphat * (1.0 - y) ** (alphan + alphat - 1.0) - al1 = (x / d) * (0.754 + 2.21 * z + z**2 + x * (0.348 + 1.243 * z + z**2)) - al2 = -x * ((0.884 + 2.074 * z) / d) - alphai = -1.172 / (1.0 + 0.462 * x) - - # q-profile - q = q0 + (q95 - q0) * ((y + y**2 + y**3) / (3.0)) - - pratio = (beta_toroidal - betae) / betae - - return (q / q95) * (al1 * (a1 + (pratio * (a1 + alphai * a2))) + al2 * a2) - - # ----------------------------------------------------- # Diamagnetic and Pfirsch-Schlüter Current Calculations # ----------------------------------------------------- @@ -735,4704 +635,3813 @@ def ps_fraction_scene(beta: float) -> float: return -9e-2 * beta -# -------------------------------------------------- -# Sauter Bootstrap Current Scaling Functions -# -------------------------------------------------- - - -@nb.jit(nopython=True, cache=True) -def _coulomb_logarithm_sauter( - radial_elements: int, tempe: np.ndarray, ne: np.ndarray -) -> np.ndarray: - """Calculate the Coulomb logarithm used in the arrays for the Sauter bootstrap current scaling. - - This function calculates the Coulomb logarithm, which is valid for e-e collisions (T_e > 0.01 keV) - and for e-i collisions (T_e > 0.01*Zeff^2) (Alexander, 9/5/1994). +class Physics: + def __init__( + self, + plasma_profile, + current_drive, + plasma_beta, + plasma_inductance, + plasma_bootstrap, + ): + self.outfile = constants.NOUT + self.mfile = constants.MFILE + self.plasma_profile = plasma_profile + self.current_drive = current_drive + self.beta = plasma_beta + self.inductance = plasma_inductance + self.bootstrap = plasma_bootstrap - Parameters - ---------- - radial_elements : - the radial element indexes in the range 1 to nr - tempe : - the electron temperature array - ne : - the electron density array + def physics(self): + """Routine to calculate tokamak plasma physics information + This routine calculates all the primary plasma physics parameters for a tokamak fusion reactor. - Returns - ------- - type - - np.ndarray, the Coulomb logarithm at each array point + References + ---------- + - M. Kovari et al, 2014, "PROCESS": A systems code for fusion power plants - Part 1: Physics + https://www.sciencedirect.com/science/article/pii/S0920379614005961 + - H. Zohm et al, 2013, On the Physics Guidelines for a Tokamak DEMO + https://iopscience.iop.org/article/10.1088/0029-5515/53/7/073019 + - T. Hartmann, 2013, Development of a modular systems code to analyse the implications of physics assumptions on the design of a demonstration fusion power plant + https://inis.iaea.org/search/search.aspx?orig_q=RN:45031642 + """ - Reference: - - C. A. Ordonez, M. I. Molina; - Evaluation of the Coulomb logarithm using cutoff and screened Coulomb interaction potentials. - Phys. Plasmas 1 August 1994; 1 (8): 2515-2518. https://doi.org/10.1063/1.870578 - - Y. R. Shen, “Recent advances in nonlinear optics,” Reviews of Modern Physics, vol. 48, no. 1, - pp. 1-32, Jan. 1976, doi: https://doi.org/10.1103/revmodphys.48.1. + # Calculate plasma composition + # Issue #261 Remove old radiation model (imprad_model=0) + self.plasma_composition() - """ - return ( - 15.9 - 0.5 * np.log(ne[radial_elements - 1]) + np.log(tempe[radial_elements - 1]) - ) + ( + physics_variables.m_plasma_fuel_ions, + physics_variables.m_plasma_ions_total, + physics_variables.m_plasma_alpha, + physics_variables.m_plasma_electron, + physics_variables.m_plasma, + ) = self.calculate_plasma_masses( + physics_variables.m_fuel_amu, + physics_variables.m_ions_total_amu, + physics_variables.nd_plasma_ions_total_vol_avg, + physics_variables.nd_plasma_fuel_ions_vol_avg, + physics_variables.nd_plasma_alphas_vol_avg, + physics_variables.vol_plasma, + physics_variables.nd_plasma_electrons_vol_avg, + ) + # Define coulomb logarithm + # (collisions: ion-electron, electron-electron) + physics_variables.dlamee = ( + 31.0 + - (np.log(physics_variables.nd_plasma_electrons_vol_avg) / 2.0) + + np.log(physics_variables.temp_plasma_electron_vol_avg_kev * 1000.0) + ) + physics_variables.dlamie = ( + 31.3 + - (np.log(physics_variables.nd_plasma_electrons_vol_avg) / 2.0) + + np.log(physics_variables.temp_plasma_electron_vol_avg_kev * 1000.0) + ) -@nb.jit(nopython=True, cache=True) -def _electron_collisions_sauter( - radial_elements: np.ndarray, tempe: np.ndarray, ne: np.ndarray -) -> np.ndarray: - """Calculate the frequency of electron-electron collisions used in the arrays for the Sauter bootstrap current scaling. + # Calculate plasma current + ( + physics_variables.b_plasma_poloidal_average, + physics_variables.qstar, + physics_variables.plasma_current, + ) = self.calculate_plasma_current( + physics_variables.alphaj, + physics_variables.alphap, + physics_variables.b_plasma_toroidal_on_axis, + physics_variables.eps, + physics_variables.i_plasma_current, + physics_variables.kappa, + physics_variables.kappa95, + physics_variables.pres_plasma_thermal_on_axis, + physics_variables.len_plasma_poloidal, + physics_variables.q95, + physics_variables.rmajor, + physics_variables.rminor, + physics_variables.triang, + physics_variables.triang95, + ) - This function calculates the frequency of electron-electron collisions + # ----------------------------------------------------- + # Plasma Current Profile + # ----------------------------------------------------- - Parameters - ---------- - radial_elements : - the radial element indexes in the range 1 to nr - tempe : - the electron temperature array - ne : - the electron density array + physics_variables.alphaj_wesson = self.calculate_current_profile_index_wesson( + qstar=physics_variables.qstar, q0=physics_variables.q0 + ) - Returns - ------- - : - the frequency of electron-electron collisions (Hz) + # Map calculation methods to a dictionary + alphaj_calculations = { + 0: physics_variables.alphaj, + 1: physics_variables.alphaj_wesson, + } + # Calculate alphaj based on i_alphaj + if int(physics_variables.i_alphaj) in alphaj_calculations: + physics_variables.alphaj = alphaj_calculations[ + int(physics_variables.i_alphaj) + ] + else: + raise ProcessValueError( + "Illegal value of i_alphaj", + i_alphaj=physics_variables.i_alphaj, + ) - Reference: - - Yushmanov, 25th April 1987 (?), updated by Pereverzev, 9th November 1994 (?) + # ================================================== - """ - return ( - 670.0 - * _coulomb_logarithm_sauter(radial_elements, tempe, ne) - * ne[radial_elements - 1] - / (tempe[radial_elements - 1] * np.sqrt(tempe[radial_elements - 1])) - ) + # ----------------------------------------------------- + # Plasma Normalised Internal Inductance + # ----------------------------------------------------- + self.inductance.run() -@nb.jit(nopython=True, cache=True) -def _electron_collisionality_sauter( - radial_elements: np.ndarray, - rmajor: float, - zeff: np.ndarray, - inverse_q: np.ndarray, - sqeps: np.ndarray, - tempe: np.ndarray, - ne: np.ndarray, -) -> np.ndarray: - """Calculate the electron collisionality used in the arrays for the Sauter bootstrap current scaling. + # =================================================== - Parameters - ---------- - radial_elements : - the radial element index in the range 1 to nr - rmajor : - the plasma major radius (m) - zeff : - the effective charge array - inverse_q : - inverse safety factor profile - sqeps : - the square root of the inverse aspect ratio array - tempe : - the electron temperature array - ne : - the electron density array + # Calculate density and temperature profile quantities + # If physics_variables.i_plasma_pedestal = 1 then set pedestal density to + # physics_variables.f_nd_plasma_pedestal_greenwald * Greenwald density limit + # Note: this used to be done before plasma current + if (physics_variables.i_plasma_pedestal == 1) and ( + physics_variables.f_nd_plasma_pedestal_greenwald >= 0e0 + ): + physics_variables.nd_plasma_pedestal_electron = ( + physics_variables.f_nd_plasma_pedestal_greenwald + * 1.0e14 + * physics_variables.plasma_current + / (np.pi * physics_variables.rminor * physics_variables.rminor) + ) - Returns - ------- - : - the relative frequency of electron collisions + if (physics_variables.i_plasma_pedestal == 1) and ( + physics_variables.f_nd_plasma_separatrix_greenwald >= 0e0 + ): + physics_variables.nd_plasma_separatrix_electron = ( + physics_variables.f_nd_plasma_separatrix_greenwald + * 1.0e14 + * physics_variables.plasma_current + / (np.pi * physics_variables.rminor * physics_variables.rminor) + ) - Reference: - - Yushmanov, 30th April 1987 (?) + self.plasma_profile.run() - """ - return ( - _electron_collisions_sauter(radial_elements, tempe, ne) - * 1.4 - * zeff[radial_elements - 1] - * rmajor - / np.abs( - inverse_q[radial_elements - 1] - * (sqeps[radial_elements - 1] ** 3) - * np.sqrt(tempe[radial_elements - 1]) - * 1.875e7 + # Calculate total magnetic field [T] + physics_variables.b_plasma_total = np.sqrt( + physics_variables.b_plasma_toroidal_on_axis**2 + + physics_variables.b_plasma_poloidal_average**2 ) - ) + # Calculate the toroidal field across the plasma + # Calculate the toroidal field profile across the plasma (1/R dependence) + # Double element size to include both sides of the plasma + rho = np.linspace( + physics_variables.rmajor - physics_variables.rminor, + physics_variables.rmajor + physics_variables.rminor, + 2 * physics_variables.n_plasma_profile_elements, + ) -@nb.jit(nopython=True, cache=True) -def _ion_collisions_sauter( - radial_elements: np.ndarray, - zeff: np.ndarray, - ni: np.ndarray, - tempi: np.ndarray, - amain: np.ndarray, -) -> np.ndarray: - """Calculate the full frequency of ion collisions used in the arrays for the Sauter bootstrap current scaling. - - This function calculates the full frequency of ion collisions using the Coulomb logarithm of 15. + # Calculate the inboard and outboard toroidal field + physics_variables.b_plasma_inboard_toroidal = ( + physics_variables.rmajor + * physics_variables.b_plasma_toroidal_on_axis + / (physics_variables.rmajor - physics_variables.rminor) + ) - Parameters - ---------- - radial_elements : - the radial element indexes in the range 1 to nr - zeff : - the effective charge array - ni : - the ion density array - tempi : - the ion temperature array - amain : - the atomic mass of the main ion species array + physics_variables.b_plasma_outboard_toroidal = ( + physics_variables.rmajor + * physics_variables.b_plasma_toroidal_on_axis + / (physics_variables.rmajor + physics_variables.rminor) + ) - Returns - ------- - : - the full frequency of ion collisions (Hz) - """ - return ( - zeff[radial_elements - 1] ** 4 - * ni[radial_elements - 1] - * 322.0 - / ( - tempi[radial_elements - 1] - * np.sqrt(tempi[radial_elements - 1] * amain[radial_elements - 1]) + # Avoid division by zero at the magnetic axis + rho = np.where(rho == 0, 1e-10, rho) + physics_variables.b_plasma_toroidal_profile = ( + physics_variables.rmajor * physics_variables.b_plasma_toroidal_on_axis / rho ) - ) + # ============================================ -@nb.jit(nopython=True, cache=True) -def _ion_collisionality_sauter( - radial_elements: np.ndarray, - rmajor: float, - inverse_q: np.ndarray, - sqeps: np.ndarray, - tempi: np.ndarray, - amain: np.ndarray, - zeff: np.ndarray, - ni: np.ndarray, -) -> float: - """Calculate the ion collisionality to be used in the Sauter bootstrap current scaling. + # ----------------------------------------------------- + # Beta Components + # ----------------------------------------------------- - Parameters - ---------- - radial_elements : - the radial element indexes in the range 1 to nr - rmajor : - the plasma major radius (m) - inverse_q : - inverse safety factor profile - sqeps : - the square root of the inverse aspect ratio profile - tempi : - the ion temperature profile (keV) - amain : - the atomic mass of the main ion species profile - zeff : - the effective charge of the main ion species - ni : - the ion density profile (/m^3) + self.beta.run() - Returns - ------- - : - the ion collisionality - """ - return ( - 3.2e-6 - * _ion_collisions_sauter(radial_elements, zeff, ni, tempi, amain) - * rmajor - / ( - np.abs(inverse_q[radial_elements - 1] + 1.0e-4) - * sqeps[radial_elements - 1] ** 3 - * np.sqrt(tempi[radial_elements - 1] / amain[radial_elements - 1]) - ) - ) - - -@nb.jit(nopython=True, cache=True) -def _calculate_l31_coefficient( - radial_elements: np.ndarray, - number_of_elements: int, - rmajor: float, - b_plasma_toroidal_on_axis: float, - triang: float, - ne: np.ndarray, - ni: np.ndarray, - tempe: np.ndarray, - tempi: np.ndarray, - inverse_q: np.ndarray, - rho: np.ndarray, - zeff: np.ndarray, - sqeps: np.ndarray, -) -> float: - """L31 coefficient before Grad(ln(ne)) in the Sauter bootstrap scaling. - - - Parameters - ---------- - radial_elements : - radial element indexes in range 2 to nr - number_of_elements : - maximum value of radial_elements - rmajor : - plasma major radius (m) - b_plasma_toroidal_on_axis : - toroidal field on axis (T) - triang : - plasma triangularity - ne : - electron density profile (/m^3) - ni : - ion density profile (/m^3) - tempe : - electron temperature profile (keV) - tempi : - ion temperature profile (keV) - inverse_q : - inverse safety factor profile - rho : - normalized minor radius profile - zeff : - effective charge profile - sqeps : - square root of inverse aspect ratio profile - - Returns - ------- - type - - float, the coefficient scaling grad(ln(ne)) in the Sauter bootstrap current scaling. - - This function calculates the coefficient scaling grad(ln(ne)) in the Sauter bootstrap current scaling. - - Reference: - - O. Sauter, C. Angioni, Y. R. Lin-Liu; - Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime. - Phys. Plasmas 1 July 1999; 6 (7): 2834-2839. https://doi.org/10.1063/1.873240 - - O. Sauter, C. Angioni, Y. R. Lin-Liu; Erratum: - Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime - [Phys. Plasmas 6, 2834 (1999)]. Phys. Plasmas 1 December 2002; 9 (12): 5140. https://doi.org/10.1063/1.1517052 - - """ - # Prevents first element being 0 - charge_profile = zeff[radial_elements - 1] - - # Calculate trapped particle fraction - f_trapped = _trapped_particle_fraction_sauter(radial_elements, triang, sqeps) - - # Calculated electron collisionality; nu_e* - electron_collisionality = _electron_collisionality_sauter( - radial_elements, rmajor, zeff, inverse_q, sqeps, tempe, ne - ) - - # $f^{31}_{teff}(\nu_{e*})$, Eq.14b - f31_teff = f_trapped / ( - (1.0 + (1.0 - 0.1 * f_trapped) * np.sqrt(electron_collisionality)) - + (0.5 * (1.0 - f_trapped) * electron_collisionality) / charge_profile - ) - - l31_coefficient = ( - ((1.0 + 1.4 / (charge_profile + 1.0)) * f31_teff) - - (1.9 / (charge_profile + 1.0) * f31_teff**2) - + ((0.3 * f31_teff**3 + 0.2 * f31_teff**4) / (charge_profile + 1.0)) - ) + # ======================================================= - # Corrections suggested by Fable, 15/05/2015 - return l31_coefficient * _beta_poloidal_total_sauter( - radial_elements, - number_of_elements, - rmajor, - b_plasma_toroidal_on_axis, - ne, - ni, - tempe, - tempi, - inverse_q, - rho, - ) + # Set PF coil ramp times + if pulse_variables.i_pulsed_plant != 1: + if times_variables.i_t_current_ramp_up == 0: + times_variables.t_plant_pulse_plasma_current_ramp_up = ( + physics_variables.plasma_current / 5.0e5 + ) + times_variables.t_plant_pulse_coil_precharge = ( + times_variables.t_plant_pulse_plasma_current_ramp_up + ) + times_variables.t_plant_pulse_plasma_current_ramp_down = ( + times_variables.t_plant_pulse_plasma_current_ramp_up + ) + else: + if times_variables.pulsetimings == 0.0e0: + # times_variables.t_plant_pulse_coil_precharge is input + times_variables.t_plant_pulse_plasma_current_ramp_up = ( + physics_variables.plasma_current / 1.0e5 + ) + times_variables.t_plant_pulse_plasma_current_ramp_down = ( + times_variables.t_plant_pulse_plasma_current_ramp_up + ) -@nb.jit(nopython=True, cache=True) -def _calculate_l31_32_coefficient( - radial_elements: np.ndarray, - number_of_elements: int, - rmajor: float, - b_plasma_toroidal_on_axis: float, - triang: float, - ne: np.ndarray, - ni: np.ndarray, - tempe: np.ndarray, - tempi: np.ndarray, - inverse_q: np.ndarray, - rho: np.ndarray, - zeff: np.ndarray, - sqeps: np.ndarray, -) -> float: - """L31 & L32 coefficient before Grad(ln(Te)) in the Sauter bootstrap scaling. + else: + # times_variables.t_plant_pulse_plasma_current_ramp_up is set either in INITIAL or INPUT, or by being + # iterated using limit equation 41. + times_variables.t_plant_pulse_coil_precharge = max( + times_variables.t_plant_pulse_coil_precharge, + times_variables.t_plant_pulse_plasma_current_ramp_up, + ) + # t_plant_pulse_plasma_current_ramp_down = max(t_plant_pulse_plasma_current_ramp_down,t_plant_pulse_plasma_current_ramp_up) + times_variables.t_plant_pulse_plasma_current_ramp_down = ( + times_variables.t_plant_pulse_plasma_current_ramp_up + ) - This function calculates the coefficient scaling grad(ln(Te)) in the Sauter bootstrap current scaling. + # Reset second times_variables.t_plant_pulse_burn value (times_variables.t_burn_0). + # This is used to ensure that the burn time is used consistently; + # see convergence loop in fcnvmc1, evaluators.f90 + times_variables.t_burn_0 = times_variables.t_plant_pulse_burn + # Time during the pulse in which a plasma is present + times_variables.t_plant_pulse_plasma_present = ( + times_variables.t_plant_pulse_plasma_current_ramp_up + + times_variables.t_plant_pulse_fusion_ramp + + times_variables.t_plant_pulse_burn + + times_variables.t_plant_pulse_plasma_current_ramp_down + ) + times_variables.t_plant_pulse_no_burn = ( + times_variables.t_plant_pulse_coil_precharge + + times_variables.t_plant_pulse_plasma_current_ramp_up + + times_variables.t_plant_pulse_plasma_current_ramp_down + + times_variables.t_plant_pulse_dwell + + times_variables.t_plant_pulse_fusion_ramp + ) - Parameters - ---------- - radial_elements : - radial element indexes in range 2 to nr - number_of_elements : - maximum value of radial_elements - rmajor : - plasma major radius (m) - b_plasma_toroidal_on_axis : - toroidal field on axis (T) - triang : - plasma triangularity - ne : - electron density profile (/m^3) - ni : - ion density profile (/m^3) - tempe : - electron temperature profile (keV) - tempi : - ion temperature profile (keV) - inverse_q : - inverse safety factor profile - rho : - normalized minor radius profile - zeff : - effective charge profile - sqeps : - square root of inverse aspect ratio profile + # Total cycle time + times_variables.t_plant_pulse_total = ( + times_variables.t_plant_pulse_coil_precharge + + times_variables.t_plant_pulse_plasma_current_ramp_up + + times_variables.t_plant_pulse_fusion_ramp + + times_variables.t_plant_pulse_burn + + times_variables.t_plant_pulse_plasma_current_ramp_down + + times_variables.t_plant_pulse_dwell + ) + # ***************************** # + # DIAMAGNETIC CURRENT # + # ***************************** # - Returns - ------- - : - the L31 & L32 coefficient scaling grad(ln(Te)) in the Sauter bootstrap current scaling. + # Hender scaling for diamagnetic current at tight physics_variables.aspect ratio + current_drive_variables.f_c_plasma_diamagnetic_hender = ( + diamagnetic_fraction_hender(physics_variables.beta_total_vol_avg) + ) + # SCENE scaling for diamagnetic current + current_drive_variables.f_c_plasma_diamagnetic_scene = ( + diamagnetic_fraction_scene( + physics_variables.beta_total_vol_avg, + physics_variables.q95, + physics_variables.q0, + ) + ) - Reference: - - O. Sauter, C. Angioni, Y. R. Lin-Liu; - Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime. - Phys. Plasmas 1 July 1999; 6 (7): 2834-2839. https://doi.org/10.1063/1.873240 - - O. Sauter, C. Angioni, Y. R. Lin-Liu; Erratum: - Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime - [Phys. Plasmas 6, 2834 (1999)]. Phys. Plasmas 1 December 2002; 9 (12): 5140. https://doi.org/10.1063/1.1517052 + if physics_variables.i_diamagnetic_current == 1: + current_drive_variables.f_c_plasma_diamagnetic = ( + current_drive_variables.f_c_plasma_diamagnetic_hender + ) + elif physics_variables.i_diamagnetic_current == 2: + current_drive_variables.f_c_plasma_diamagnetic = ( + current_drive_variables.f_c_plasma_diamagnetic_scene + ) - """ + # ***************************** # + # PFIRSCH-SCHLÜTER CURRENT # + # ***************************** # - # Prevents first element being 0 - charge_profile = zeff[radial_elements - 1] + # Pfirsch-Schlüter scaling for diamagnetic current + current_drive_variables.f_c_plasma_pfirsch_schluter_scene = ps_fraction_scene( + physics_variables.beta_total_vol_avg + ) - # Calculate trapped particle fraction - f_trapped = _trapped_particle_fraction_sauter(radial_elements, triang, sqeps) + if physics_variables.i_pfirsch_schluter_current == 1: + current_drive_variables.f_c_plasma_pfirsch_schluter = ( + current_drive_variables.f_c_plasma_pfirsch_schluter_scene + ) - # Calculated electron collisionality; nu_e* - electron_collisionality = _electron_collisionality_sauter( - radial_elements, rmajor, zeff, inverse_q, sqeps, tempe, ne - ) + # ***************************** # + # BOOTSTRAP CURRENT # + # ***************************** # - # $f^{32\_ee}_{teff}(\nu_{e*})$, Eq.15d - f32ee_teff = f_trapped / ( - 1.0 - + 0.26 * (1.0 - f_trapped) * np.sqrt(electron_collisionality) - + ( - 0.18 - * (1.0 - 0.37 * f_trapped) - * electron_collisionality - / np.sqrt(charge_profile) + # Calculate bootstrap current fraction using various models + current_drive_variables.f_c_plasma_bootstrap_iter89 = ( + current_drive_variables.cboot + * self.bootstrap.bootstrap_fraction_iter89( + physics_variables.aspect, + physics_variables.beta_total_vol_avg, + physics_variables.b_plasma_total, + physics_variables.plasma_current, + physics_variables.q95, + physics_variables.q0, + physics_variables.rmajor, + physics_variables.vol_plasma, + ) ) - ) - # $f^{32\_ei}_{teff}(\nu_{e*})$, Eq.15e - f32ei_teff = f_trapped / ( - (1.0 + (1.0 + 0.6 * f_trapped) * np.sqrt(electron_collisionality)) - + ( - 0.85 - * (1.0 - 0.37 * f_trapped) - * electron_collisionality - * (1.0 + charge_profile) + current_drive_variables.f_c_plasma_bootstrap_nevins = ( + current_drive_variables.cboot + * self.bootstrap.bootstrap_fraction_nevins( + physics_variables.alphan, + physics_variables.alphat, + physics_variables.beta_toroidal_vol_avg, + physics_variables.b_plasma_toroidal_on_axis, + physics_variables.nd_plasma_electrons_vol_avg, + physics_variables.plasma_current, + physics_variables.q95, + physics_variables.q0, + physics_variables.rmajor, + physics_variables.rminor, + physics_variables.temp_plasma_electron_vol_avg_kev, + physics_variables.n_charge_plasma_effective_vol_avg, + ) ) - ) - # $F_{32\_ee}(X)$, Eq.15b - big_f32ee_teff = ( - ( - (0.05 + 0.62 * charge_profile) - / charge_profile - / (1.0 + 0.44 * charge_profile) - * (f32ee_teff - f32ee_teff**4) - ) - + ( - (f32ee_teff**2 - f32ee_teff**4 - 1.2 * (f32ee_teff**3 - f32ee_teff**4)) - / (1.0 + 0.22 * charge_profile) + # Wilson scaling uses thermal poloidal beta, not total + current_drive_variables.f_c_plasma_bootstrap_wilson = ( + current_drive_variables.cboot + * self.bootstrap.bootstrap_fraction_wilson( + physics_variables.alphaj, + physics_variables.alphap, + physics_variables.alphat, + physics_variables.beta_thermal_poloidal_vol_avg, + physics_variables.q0, + physics_variables.q95, + physics_variables.rmajor, + physics_variables.rminor, + ) ) - + (1.2 / (1.0 + 0.5 * charge_profile) * f32ee_teff**4) - ) - # $F_{32\_ei}(Y)$, Eq.15c - big_f32ei_teff = ( ( - -(0.56 + 1.93 * charge_profile) - / charge_profile - / (1.0 + 0.44 * charge_profile) - * (f32ei_teff - f32ei_teff**4) - ) - + ( - 4.95 - / (1.0 + 2.48 * charge_profile) - * (f32ei_teff**2 - f32ei_teff**4 - 0.55 * (f32ei_teff**3 - f32ei_teff**4)) + current_drive_variables.f_c_plasma_bootstrap_sauter, + physics_variables.j_plasma_bootstrap_sauter_profile, + ) = self.bootstrap.bootstrap_fraction_sauter(self.plasma_profile) + current_drive_variables.f_c_plasma_bootstrap_sauter *= ( + current_drive_variables.cboot ) - - (1.2 / (1.0 + 0.5 * charge_profile) * f32ei_teff**4) - ) - - # big_f32ee_teff + big_f32ei_teff = L32 coefficient - - # Corrections suggested by Fable, 15/05/2015 - return _beta_poloidal_sauter( - radial_elements, - number_of_elements, - rmajor, - b_plasma_toroidal_on_axis, - ne, - tempe, - inverse_q, - rho, - ) * (big_f32ee_teff + big_f32ei_teff) + _calculate_l31_coefficient( - radial_elements, - number_of_elements, - rmajor, - b_plasma_toroidal_on_axis, - triang, - ne, - ni, - tempe, - tempi, - inverse_q, - rho, - zeff, - sqeps, - ) * _beta_poloidal_sauter( - radial_elements, - number_of_elements, - rmajor, - b_plasma_toroidal_on_axis, - ne, - tempe, - inverse_q, - rho, - ) / _beta_poloidal_total_sauter( - radial_elements, - number_of_elements, - rmajor, - b_plasma_toroidal_on_axis, - ne, - ni, - tempe, - tempi, - inverse_q, - rho, - ) - - -@nb.jit(nopython=True, cache=True) -def _calculate_l34_alpha_31_coefficient( - radial_elements: np.ndarray, - number_of_elements: int, - rmajor: float, - b_plasma_toroidal_on_axis: float, - triang: float, - inverse_q: np.ndarray, - sqeps: np.ndarray, - tempi: np.ndarray, - tempe: np.ndarray, - amain: float, - zmain: float, - ni: np.ndarray, - ne: np.ndarray, - rho: np.ndarray, - zeff: np.ndarray, -) -> float: - """L34, alpha and L31 coefficient before Grad(ln(Ti)) in the Sauter bootstrap scaling. - - This function calculates the coefficient scaling grad(ln(Ti)) in the Sauter bootstrap current scaling. - - - Parameters - ---------- - radial_elements : - radial element indexes in range 2 to nr - number_of_elements : - maximum value of radial_elements - rmajor : - plasma major radius (m) - b_plasma_toroidal_on_axis : - toroidal field on axis (T) - triang : - plasma triangularity - inverse_q : - inverse safety factor profile - sqeps : - square root of inverse aspect ratio profile - tempi : - ion temperature profile (keV) - tempe : - electron temperature profile (keV) - amain : - atomic mass of the main ion - zmain : - charge of the main ion - ni : - ion density profile (/m^3) - ne : - electron density profile (/m^3) - rho : - normalized minor radius profile - zeff : - effective charge profile + current_drive_variables.f_c_plasma_bootstrap_sakai = ( + current_drive_variables.cboot + * self.bootstrap.bootstrap_fraction_sakai( + beta_poloidal=physics_variables.beta_poloidal_vol_avg, + q95=physics_variables.q95, + q0=physics_variables.q0, + alphan=physics_variables.alphan, + alphat=physics_variables.alphat, + eps=physics_variables.eps, + ind_plasma_internal_norm=physics_variables.ind_plasma_internal_norm, + ) + ) - Returns - ------- - : - the L34, alpha and L31 coefficient scaling grad(ln(Ti)) in the Sauter bootstrap current scaling. + current_drive_variables.f_c_plasma_bootstrap_aries = ( + current_drive_variables.cboot + * self.bootstrap.bootstrap_fraction_aries( + beta_poloidal=physics_variables.beta_poloidal_vol_avg, + ind_plasma_internal_norm=physics_variables.ind_plasma_internal_norm, + core_density=physics_variables.nd_plasma_electron_on_axis, + average_density=physics_variables.nd_plasma_electrons_vol_avg, + inverse_aspect=physics_variables.eps, + ) + ) - - Reference: - - O. Sauter, C. Angioni, Y. R. Lin-Liu; - Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime. - Phys. Plasmas 1 July 1999; 6 (7): 2834-2839. https://doi.org/10.1063/1.873240 - - O. Sauter, C. Angioni, Y. R. Lin-Liu; Erratum: - Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime - [Phys. Plasmas 6, 2834 (1999)]. Phys. Plasmas 1 December 2002; 9 (12): 5140. https://doi.org/10.1063/1.1517052 - - """ - # Prevents first element being 0 - charge_profile = zeff[radial_elements - 1] - - # Calculate trapped particle fraction - f_trapped = _trapped_particle_fraction_sauter(radial_elements, triang, sqeps) - - # Calculated electron collisionality; nu_e* - electron_collisionality = _electron_collisionality_sauter( - radial_elements, rmajor, zeff, inverse_q, sqeps, tempe, ne - ) - - # $f^{34}_{teff}(\nu_{e*})$, Eq.16b - f34_teff = f_trapped / ( - (1.0 + (1.0 - 0.1 * f_trapped) * np.sqrt(electron_collisionality)) - + 0.5 * (1.0 - 0.5 * f_trapped) * electron_collisionality / charge_profile - ) - - # Eq.16a - l34_coefficient = ( - ((1.0 + (1.4 / (charge_profile + 1.0))) * f34_teff) - - ((1.9 / (charge_profile + 1.0)) * f34_teff**2) - + ((0.3 / (charge_profile + 1.0)) * f34_teff**3) - + ((0.2 / (charge_profile + 1.0)) * f34_teff**4) - ) - - # $\alpha_0$, Eq.17a - alpha_0 = (-1.17 * (1.0 - f_trapped)) / ( - 1.0 - (0.22 * f_trapped) - 0.19 * f_trapped**2 - ) - - # Calculate the ion collisionality - ion_collisionality = _ion_collisionality_sauter( - radial_elements, rmajor, inverse_q, sqeps, tempi, amain, zmain, ni - ) - - # $\alpha(\nu_{i*})$, Eq.17b - alpha = ( - (alpha_0 + (0.25 * (1.0 - f_trapped**2)) * np.sqrt(ion_collisionality)) - / (1.0 + (0.5 * np.sqrt(ion_collisionality))) - + (0.315 * ion_collisionality**2 * f_trapped**6) - ) / (1.0 + (0.15 * ion_collisionality**2 * f_trapped**6)) - - # Corrections suggested by Fable, 15/05/2015 - # Below calculates the L34 * alpha + L31 coefficient - return ( - _beta_poloidal_total_sauter( - radial_elements, - number_of_elements, - rmajor, - b_plasma_toroidal_on_axis, - ne, - ni, - tempe, - tempi, - inverse_q, - rho, - ) - - _beta_poloidal_sauter( - radial_elements, - number_of_elements, - rmajor, - b_plasma_toroidal_on_axis, - ne, - tempe, - inverse_q, - rho, - ) - ) * (l34_coefficient * alpha) + _calculate_l31_coefficient( - radial_elements, - number_of_elements, - rmajor, - b_plasma_toroidal_on_axis, - triang, - ne, - ni, - tempe, - tempi, - inverse_q, - rho, - zeff, - sqeps, - ) * ( - 1.0 - - _beta_poloidal_sauter( - radial_elements, - number_of_elements, - rmajor, - b_plasma_toroidal_on_axis, - ne, - tempe, - inverse_q, - rho, - ) - / _beta_poloidal_total_sauter( - radial_elements, - number_of_elements, - rmajor, - b_plasma_toroidal_on_axis, - ne, - ni, - tempe, - tempi, - inverse_q, - rho, + current_drive_variables.f_c_plasma_bootstrap_andrade = ( + current_drive_variables.cboot + * self.bootstrap.bootstrap_fraction_andrade( + beta_poloidal=physics_variables.beta_poloidal_vol_avg, + core_pressure=physics_variables.pres_plasma_thermal_on_axis, + average_pressure=physics_variables.pres_plasma_thermal_vol_avg, + inverse_aspect=physics_variables.eps, + ) ) - ) - - -@nb.jit(nopython=True, cache=True) -def _beta_poloidal_sauter( - radial_elements: np.ndarray, - nr: int, - rmajor: float, - b_plasma_toroidal_on_axis: float, - ne: np.ndarray, - tempe: np.ndarray, - inverse_q: np.ndarray, - rho: np.ndarray, -) -> np.ndarray: - """Calculate the local beta poloidal using only electron profiles for the Sauter bootstrap current scaling. - - Parameters - ---------- - radial_elements : - radial element indexes in range 1 to nr - nr : - maximum value of radial_elements - rmajor : - plasma major radius (m) - b_plasma_toroidal_on_axis : - toroidal field on axis (T) - ne : - electron density profile (/m^3) - tempe : - electron temperature profile (keV) - inverse_q : - inverse safety factor profile - rho : - normalized minor radius profile - - - Returns - ------- - : - the local beta poloidal - """ - return ( - np.where( - radial_elements != nr, - 1.6e-4 - * np.pi - * (ne[radial_elements] + ne[radial_elements - 1]) - * (tempe[radial_elements] + tempe[radial_elements - 1]), - 6.4e-4 * np.pi * ne[radial_elements - 1] * tempe[radial_elements - 1], - ) - * ( - rmajor - / ( - b_plasma_toroidal_on_axis - * rho[radial_elements - 1] - * np.abs(inverse_q[radial_elements - 1] + 1.0e-4) + current_drive_variables.f_c_plasma_bootstrap_hoang = ( + current_drive_variables.cboot + * self.bootstrap.bootstrap_fraction_hoang( + beta_poloidal=physics_variables.beta_poloidal_vol_avg, + pressure_index=physics_variables.alphap, + current_index=physics_variables.alphaj, + inverse_aspect=physics_variables.eps, ) ) - ** 2 - ) - - -@nb.jit(nopython=True, cache=True) -def _beta_poloidal_total_sauter( - radial_elements: np.ndarray, - nr: int, - rmajor: float, - b_plasma_toroidal_on_axis: float, - ne: np.ndarray, - ni: np.ndarray, - tempe: np.ndarray, - tempi: np.ndarray, - inverse_q: np.ndarray, - rho: np.ndarray, -) -> np.ndarray: - """Calculate the local beta poloidal including ion pressure for the Sauter bootstrap current scaling. - - Parameters - ---------- - radial_elements : - radial element indexes in range 1 to nr - nr : - maximum value of radial_elements - rmajor : - plasma major radius (m) - b_plasma_toroidal_on_axis : - toroidal field on axis (T) - ne : - electron density profile (/m^3) - ni : - ion density profile (/m^3) - tempe : - electron temperature profile (keV) - tempi : - ion temperature profile (keV) - inverse_q : - inverse safety factor profile - rho : - normalized minor radius profile - - Returns - ------- - : - the local total beta poloidal - """ - return ( - np.where( - radial_elements != nr, - 1.6e-4 - * np.pi - * ( - ( - (ne[radial_elements] + ne[radial_elements - 1]) - * (tempe[radial_elements] + tempe[radial_elements - 1]) - ) - + ( - (ni[radial_elements] + ni[radial_elements - 1]) - * (tempi[radial_elements] + tempi[radial_elements - 1]) - ) - ), - 6.4e-4 - * np.pi - * ( - ne[radial_elements - 1] * tempe[radial_elements - 1] - + ni[radial_elements - 1] * tempi[radial_elements - 1] - ), + current_drive_variables.f_c_plasma_bootstrap_wong = ( + current_drive_variables.cboot + * self.bootstrap.bootstrap_fraction_wong( + beta_poloidal=physics_variables.beta_poloidal_vol_avg, + density_index=physics_variables.alphan, + temperature_index=physics_variables.alphat, + inverse_aspect=physics_variables.eps, + elongation=physics_variables.kappa, + ) ) - * ( - rmajor - / ( - b_plasma_toroidal_on_axis - * rho[radial_elements - 1] - * np.abs(inverse_q[radial_elements - 1] + 1.0e-4) + current_drive_variables.bscf_gi_i = ( + current_drive_variables.cboot + * self.bootstrap.bootstrap_fraction_gi_I( + beta_poloidal=physics_variables.beta_poloidal_vol_avg, + pressure_index=physics_variables.alphap, + temperature_index=physics_variables.alphat, + inverse_aspect=physics_variables.eps, + effective_charge=physics_variables.n_charge_plasma_effective_vol_avg, + q95=physics_variables.q95, + q0=physics_variables.q0, ) ) - ** 2 - ) + current_drive_variables.bscf_gi_ii = ( + current_drive_variables.cboot + * self.bootstrap.bootstrap_fraction_gi_II( + beta_poloidal=physics_variables.beta_poloidal_vol_avg, + pressure_index=physics_variables.alphap, + temperature_index=physics_variables.alphat, + inverse_aspect=physics_variables.eps, + effective_charge=physics_variables.n_charge_plasma_effective_vol_avg, + ) + ) + current_drive_variables.f_c_plasma_bootstrap_sugiyama_l = ( + current_drive_variables.cboot + * self.bootstrap.bootstrap_fraction_sugiyama_l_mode( + eps=physics_variables.eps, + beta_poloidal=physics_variables.beta_poloidal_vol_avg, + alphan=physics_variables.alphan, + alphat=physics_variables.alphat, + zeff=physics_variables.n_charge_plasma_effective_vol_avg, + q95=physics_variables.q95, + q0=physics_variables.q0, + ) + ) + current_drive_variables.f_c_plasma_bootstrap_sugiyama_h = ( + current_drive_variables.cboot + * self.bootstrap.bootstrap_fraction_sugiyama_h_mode( + eps=physics_variables.eps, + beta_poloidal=physics_variables.beta_poloidal_vol_avg, + alphan=physics_variables.alphan, + alphat=physics_variables.alphat, + tbeta=physics_variables.tbeta, + zeff=physics_variables.n_charge_plasma_effective_vol_avg, + q95=physics_variables.q95, + q0=physics_variables.q0, + radius_plasma_pedestal_density_norm=physics_variables.radius_plasma_pedestal_density_norm, + nd_plasma_pedestal_electron=physics_variables.nd_plasma_pedestal_electron, + n_greenwald=physics_variables.nd_plasma_electron_max_array[6], + temp_plasma_pedestal_kev=physics_variables.temp_plasma_pedestal_kev, + ) + ) -@nb.jit(nopython=True, cache=True) -def _trapped_particle_fraction_sauter( - radial_elements: np.ndarray, triang: float, sqeps: np.ndarray, fit: int = 0 -) -> np.ndarray: - """Calculates the trapped particle fraction to be used in the Sauter bootstrap current scaling. - Parameters - ---------- - radial_elements : - radial element index in range 1 to nr - triang : - plasma triangularity - sqeps : - square root of local aspect ratio - fit : - fit method (1 = ASTRA method, 2 = Equation from Sauter 2002, 3 = Equation from Sauter 2016) - - Returns - ------- - type - - list, trapped particle fraction + # Calculate beta_norm_max based on i_beta_norm_max + try: + model = BootstrapCurrentFractionModel( + int(physics_variables.i_bootstrap_current) + ) + current_drive_variables.f_c_plasma_bootstrap = ( + self.bootstrap.get_bootstrap_current_fraction_value(model) + ) + except ValueError: + raise ProcessValueError( + "Illegal value of i_bootstrap_current", + i_bootstrap_current=physics_variables.i_bootstrap_current, + ) from None - This function calculates the trapped particle fraction at a given radius. + physics_variables.err242 = 0 + if ( + current_drive_variables.f_c_plasma_bootstrap + > current_drive_variables.f_c_plasma_bootstrap_max + ) and physics_variables.i_bootstrap_current != 0: + current_drive_variables.f_c_plasma_bootstrap = min( + current_drive_variables.f_c_plasma_bootstrap, + current_drive_variables.f_c_plasma_bootstrap_max, + ) + physics_variables.err242 = 1 - References - ---------- - Used in this paper: - - O. Sauter, C. Angioni, Y. R. Lin-Liu; - Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime. - Phys. Plasmas 1 July 1999; 6 (7): 2834-2839. https://doi.org/10.1063/1.873240 + current_drive_variables.f_c_plasma_internal = ( + current_drive_variables.f_c_plasma_bootstrap + + current_drive_variables.f_c_plasma_diamagnetic + + current_drive_variables.f_c_plasma_pfirsch_schluter + ) - - O. Sauter, R. J. Buttery, R. Felton, T. C. Hender, D. F. Howell, and contributors to the E.-J. Workprogramme, - “Marginal-limit for neoclassical tearing modes in JET H-mode discharges,” - Plasma Physics and Controlled Fusion, vol. 44, no. 9, pp. 1999-2019, Aug. 2002, - doi: https://doi.org/10.1088/0741-3335/44/9/315. + # Plasma driven current fraction (Bootstrap + Diamagnetic + # + Pfirsch-Schlüter) constrained to be less than + # or equal to the total fraction of the plasma current + # produced by non-inductive means (which also includes + # the current drive proportion) + physics_variables.err243 = 0 + if ( + current_drive_variables.f_c_plasma_internal + > physics_variables.f_c_plasma_non_inductive + ): + current_drive_variables.f_c_plasma_internal = min( + current_drive_variables.f_c_plasma_internal, + physics_variables.f_c_plasma_non_inductive, + ) + physics_variables.err243 = 1 - - O. Sauter, Geometric formulas for system codes including the effect of negative triangularity, - Fusion Engineering and Design, Volume 112, 2016, Pages 633-645, ISSN 0920-3796, - https://doi.org/10.1016/j.fusengdes.2016.04.033. + # Fraction of plasma current produced by inductive means + physics_variables.f_c_plasma_inductive = max( + 1.0e-10, (1.0e0 - physics_variables.f_c_plasma_non_inductive) + ) + # Fraction of plasma current produced by auxiliary current drive + physics_variables.f_c_plasma_auxiliary = ( + physics_variables.f_c_plasma_non_inductive + - current_drive_variables.f_c_plasma_internal + ) - """ - # Prevent first element from being zero - sqeps_reduced = sqeps[radial_elements - 1] - eps = sqeps_reduced**2 + # Auxiliary current drive power calculations - if fit == 0: - # ASTRA method, from Emiliano Fable, private communication - # (Excluding h term which dominates for inverse aspect ratios < 0.5, - # and tends to take the trapped particle fraction to 1) + if current_drive_variables.i_hcd_calculations != 0: + self.current_drive.cudriv() - zz = 1.0 - eps - return 1.0 - zz * np.sqrt(zz) / (1.0 + 1.46 * sqeps_reduced) + # ***************************** # + # FUSION REACTIONS # + # ***************************** # - if fit == 1: - # Equation 4 of Sauter 2002; https://doi.org/10.1088/0741-3335/44/9/315. - # Similar to, but not quite identical to above + # Calculate fusion power + components - return 1.0 - ( - ((1.0 - eps) ** 2) / ((1.0 + 1.46 * sqeps_reduced) * np.sqrt(1.0 - eps**2)) + fusion_reactions = reactions.FusionReactionRate(self.plasma_profile) + fusion_reactions.deuterium_branching( + physics_variables.temp_plasma_ion_vol_avg_kev ) + fusion_reactions.calculate_fusion_rates() + fusion_reactions.set_physics_variables() - if fit == 2: - # Sauter 2016; https://doi.org/10.1016/j.fusengdes.2016.04.033. - # Includes correction for triangularity - - epseff = 0.67 * (1.0 - 1.4 * triang * np.abs(triang)) * eps - - return 1.0 - ( - ((1.0 - epseff) / (1.0 + 2.0 * np.sqrt(epseff))) - * (np.sqrt((1.0 - eps) / (1.0 + eps))) + # This neglects the power from the beam + physics_variables.p_plasma_dt_mw = ( + physics_variables.dt_power_density_plasma * physics_variables.vol_plasma + ) + physics_variables.p_dhe3_total_mw = ( + physics_variables.dhe3_power_density * physics_variables.vol_plasma + ) + physics_variables.p_dd_total_mw = ( + physics_variables.dd_power_density * physics_variables.vol_plasma ) - raise ProcessValueError(f"fit={fit} is not valid. Must be 1, 2, or 3") - - -class Physics: - def __init__( - self, - plasma_profile, - current_drive, - plasma_beta, - plasma_inductance, - plasma_bootstrap, - ): - self.outfile = constants.NOUT - self.mfile = constants.MFILE - self.plasma_profile = plasma_profile - self.current_drive = current_drive - self.beta = plasma_beta - self.inductance = plasma_inductance - self.bootstrap = plasma_bootstrap - - def physics(self): - """Routine to calculate tokamak plasma physics information - This routine calculates all the primary plasma physics parameters for a tokamak fusion reactor. + # Calculate neutral beam slowing down effects + # If ignited, then ignore beam fusion effects - References - ---------- - - M. Kovari et al, 2014, "PROCESS": A systems code for fusion power plants - Part 1: Physics - https://www.sciencedirect.com/science/article/pii/S0920379614005961 - - H. Zohm et al, 2013, On the Physics Guidelines for a Tokamak DEMO - https://iopscience.iop.org/article/10.1088/0029-5515/53/7/073019 - - T. Hartmann, 2013, Development of a modular systems code to analyse the implications of physics assumptions on the design of a demonstration fusion power plant - https://inis.iaea.org/search/search.aspx?orig_q=RN:45031642 - """ + if (current_drive_variables.c_beam_total != 0.0e0) and ( + physics_variables.i_plasma_ignited == 0 + ): + ( + physics_variables.beta_beam, + physics_variables.nd_beam_ions_out, + physics_variables.p_beam_alpha_mw, + ) = reactions.beam_fusion( + physics_variables.beamfus0, + physics_variables.betbm0, + physics_variables.b_plasma_poloidal_average, + physics_variables.b_plasma_toroidal_on_axis, + current_drive_variables.c_beam_total, + physics_variables.nd_plasma_electrons_vol_avg, + physics_variables.nd_plasma_fuel_ions_vol_avg, + physics_variables.dlamie, + current_drive_variables.e_beam_kev, + physics_variables.f_plasma_fuel_deuterium, + physics_variables.f_plasma_fuel_tritium, + current_drive_variables.f_beam_tritium, + physics_variables.sigmav_dt_average, + physics_variables.temp_plasma_electron_density_weighted_kev, + physics_variables.temp_plasma_ion_density_weighted_kev, + physics_variables.vol_plasma, + physics_variables.n_charge_plasma_effective_mass_weighted_vol_avg, + ) + physics_variables.fusden_total = ( + physics_variables.fusden_plasma + + 1.0e6 + * physics_variables.p_beam_alpha_mw + / (constants.DT_ALPHA_ENERGY) + / physics_variables.vol_plasma + ) + physics_variables.fusden_alpha_total = ( + physics_variables.fusden_plasma_alpha + + 1.0e6 + * physics_variables.p_beam_alpha_mw + / (constants.DT_ALPHA_ENERGY) + / physics_variables.vol_plasma + ) + physics_variables.p_dt_total_mw = ( + physics_variables.p_plasma_dt_mw + + (1.0 / (1.0 - constants.DT_NEUTRON_ENERGY_FRACTION)) + * physics_variables.p_beam_alpha_mw + ) + physics_variables.p_beam_neutron_mw = physics_variables.p_beam_alpha_mw * ( + constants.DT_NEUTRON_ENERGY_FRACTION + / (1 - constants.DT_NEUTRON_ENERGY_FRACTION) + ) + physics_variables.p_beam_dt_mw = physics_variables.p_beam_alpha_mw * ( + 1 / (1 - constants.DT_NEUTRON_ENERGY_FRACTION) + ) + else: + # If no beams present then the total alpha rates and power are the same as the plasma values + physics_variables.fusden_total = physics_variables.fusden_plasma + physics_variables.fusden_alpha_total = physics_variables.fusden_plasma_alpha + physics_variables.p_dt_total_mw = physics_variables.p_plasma_dt_mw - # Calculate plasma composition - # Issue #261 Remove old radiation model (imprad_model=0) - self.plasma_composition() + physics_variables.fusrat_total = ( + physics_variables.fusden_total * physics_variables.vol_plasma + ) + # Create some derived values and add beam contribution to fusion power ( - physics_variables.m_plasma_fuel_ions, - physics_variables.m_plasma_ions_total, - physics_variables.m_plasma_alpha, - physics_variables.m_plasma_electron, - physics_variables.m_plasma, - ) = self.calculate_plasma_masses( - physics_variables.m_fuel_amu, - physics_variables.m_ions_total_amu, - physics_variables.nd_plasma_ions_total_vol_avg, - physics_variables.nd_plasma_fuel_ions_vol_avg, - physics_variables.nd_plasma_alphas_vol_avg, + physics_variables.pden_neutron_total_mw, + physics_variables.p_plasma_alpha_mw, + physics_variables.p_alpha_total_mw, + physics_variables.p_plasma_neutron_mw, + physics_variables.p_neutron_total_mw, + physics_variables.p_non_alpha_charged_mw, + physics_variables.pden_alpha_total_mw, + physics_variables.f_pden_alpha_electron_mw, + physics_variables.f_pden_alpha_ions_mw, + physics_variables.p_charged_particle_mw, + physics_variables.p_fusion_total_mw, + ) = reactions.set_fusion_powers( + physics_variables.f_alpha_electron, + physics_variables.f_alpha_ion, + physics_variables.p_beam_alpha_mw, + physics_variables.pden_non_alpha_charged_mw, + physics_variables.pden_plasma_neutron_mw, physics_variables.vol_plasma, + physics_variables.pden_plasma_alpha_mw, + ) + + physics_variables.beta_fast_alpha = self.beta.fast_alpha_beta( + b_plasma_poloidal_average=physics_variables.b_plasma_poloidal_average, + b_plasma_toroidal_on_axis=physics_variables.b_plasma_toroidal_on_axis, + nd_plasma_electrons_vol_avg=physics_variables.nd_plasma_electrons_vol_avg, + nd_plasma_fuel_ions_vol_avg=physics_variables.nd_plasma_fuel_ions_vol_avg, + nd_plasma_ions_total_vol_avg=physics_variables.nd_plasma_ions_total_vol_avg, + temp_plasma_electron_density_weighted_kev=physics_variables.temp_plasma_electron_density_weighted_kev, + temp_plasma_ion_density_weighted_kev=physics_variables.temp_plasma_ion_density_weighted_kev, + pden_alpha_total_mw=physics_variables.pden_alpha_total_mw, + pden_plasma_alpha_mw=physics_variables.pden_plasma_alpha_mw, + i_beta_fast_alpha=physics_variables.i_beta_fast_alpha, + ) + + # Nominal mean neutron wall load on entire first wall area including divertor and beam holes + # Note that 'a_fw_total' excludes these, so they have been added back in. + if physics_variables.i_pflux_fw_neutron == 1: + physics_variables.pflux_fw_neutron_mw = ( + physics_variables.ffwal + * physics_variables.p_neutron_total_mw + / physics_variables.a_plasma_surface + ) + else: + if divertor_variables.n_divertors == 2: + # Double null configuration + physics_variables.pflux_fw_neutron_mw = ( + ( + 1.0e0 + - fwbs_variables.f_a_fw_outboard_hcd + - 2.0e0 * fwbs_variables.f_ster_div_single + ) + * physics_variables.p_neutron_total_mw + / build_variables.a_fw_total + ) + else: + # Single null Configuration + physics_variables.pflux_fw_neutron_mw = ( + ( + 1.0e0 + - fwbs_variables.f_a_fw_outboard_hcd + - fwbs_variables.f_ster_div_single + ) + * physics_variables.p_neutron_total_mw + / build_variables.a_fw_total + ) + + # Calculate ion/electron equilibration power + + physics_variables.pden_ion_electron_equilibration_mw = rether( + physics_variables.alphan, + physics_variables.alphat, physics_variables.nd_plasma_electrons_vol_avg, + physics_variables.dlamie, + physics_variables.temp_plasma_electron_vol_avg_kev, + physics_variables.temp_plasma_ion_vol_avg_kev, + physics_variables.n_charge_plasma_effective_mass_weighted_vol_avg, ) - # Define coulomb logarithm - # (collisions: ion-electron, electron-electron) - physics_variables.dlamee = ( - 31.0 - - (np.log(physics_variables.nd_plasma_electrons_vol_avg) / 2.0) - + np.log(physics_variables.temp_plasma_electron_vol_avg_kev * 1000.0) + # Calculate radiation power + + radpwrdata = physics_funcs.calculate_radiation_powers( + self.plasma_profile, + physics_variables.nd_plasma_electron_on_axis, + physics_variables.rminor, + physics_variables.b_plasma_toroidal_on_axis, + physics_variables.aspect, + physics_variables.alphan, + physics_variables.alphat, + physics_variables.tbeta, + physics_variables.temp_plasma_electron_on_axis_kev, + physics_variables.f_sync_reflect, + physics_variables.rmajor, + physics_variables.kappa, + physics_variables.vol_plasma, ) - physics_variables.dlamie = ( - 31.3 - - (np.log(physics_variables.nd_plasma_electrons_vol_avg) / 2.0) - + np.log(physics_variables.temp_plasma_electron_vol_avg_kev * 1000.0) + physics_variables.pden_plasma_sync_mw = radpwrdata.pden_plasma_sync_mw + physics_variables.pden_plasma_core_rad_mw = radpwrdata.pden_plasma_core_rad_mw + physics_variables.pden_plasma_outer_rad_mw = radpwrdata.pden_plasma_outer_rad_mw + physics_variables.pden_plasma_rad_mw = radpwrdata.pden_plasma_rad_mw + + physics_variables.p_plasma_sync_mw = ( + physics_variables.pden_plasma_sync_mw * physics_variables.vol_plasma + ) + physics_variables.p_plasma_inner_rad_mw = ( + physics_variables.pden_plasma_core_rad_mw * physics_variables.vol_plasma + ) + physics_variables.p_plasma_outer_rad_mw = ( + physics_variables.pden_plasma_outer_rad_mw * physics_variables.vol_plasma + ) + physics_variables.p_plasma_rad_mw = ( + physics_variables.pden_plasma_rad_mw * physics_variables.vol_plasma ) - # Calculate plasma current + # Calculate ohmic power ( - physics_variables.b_plasma_poloidal_average, - physics_variables.qstar, + physics_variables.pden_plasma_ohmic_mw, + physics_variables.p_plasma_ohmic_mw, + physics_variables.f_res_plasma_neo, + physics_variables.res_plasma, + ) = self.plasma_ohmic_heating( + physics_variables.f_c_plasma_inductive, + physics_variables.kappa95, physics_variables.plasma_current, - ) = self.calculate_plasma_current( - physics_variables.alphaj, - physics_variables.alphap, + physics_variables.rmajor, + physics_variables.rminor, + physics_variables.temp_plasma_electron_density_weighted_kev, + physics_variables.vol_plasma, + physics_variables.n_charge_plasma_effective_vol_avg, + ) + + # Calculate L- to H-mode power threshold for different scalings + physics_variables.l_h_threshold_powers = l_h_threshold_power( + physics_variables.nd_plasma_electron_line, physics_variables.b_plasma_toroidal_on_axis, - physics_variables.eps, - physics_variables.i_plasma_current, - physics_variables.kappa, - physics_variables.kappa95, - physics_variables.pres_plasma_thermal_on_axis, - physics_variables.len_plasma_poloidal, - physics_variables.q95, physics_variables.rmajor, physics_variables.rminor, - physics_variables.triang, - physics_variables.triang95, + physics_variables.kappa, + physics_variables.a_plasma_surface, + physics_variables.m_ions_total_amu, + physics_variables.aspect, + physics_variables.plasma_current, ) - # ----------------------------------------------------- - # Plasma Current Profile - # ----------------------------------------------------- + # Enforced L-H power threshold value (if constraint 15 is turned on) + physics_variables.p_l_h_threshold_mw = physics_variables.l_h_threshold_powers[ + physics_variables.i_l_h_threshold - 1 + ] - physics_variables.alphaj_wesson = self.calculate_current_profile_index_wesson( - qstar=physics_variables.qstar, q0=physics_variables.q0 + # Power transported to the divertor by charged particles, + # i.e. excludes neutrons and radiation, and also NBI orbit loss power, + # which is assumed to be absorbed by the first wall + pinj = ( + current_drive_variables.p_hcd_injected_total_mw + if physics_variables.i_plasma_ignited == 0 + else 0.0 ) - # Map calculation methods to a dictionary - alphaj_calculations = { - 0: physics_variables.alphaj, - 1: physics_variables.alphaj_wesson, - } - - # Calculate alphaj based on i_alphaj - if int(physics_variables.i_alphaj) in alphaj_calculations: - physics_variables.alphaj = alphaj_calculations[ - int(physics_variables.i_alphaj) - ] - else: - raise ProcessValueError( - "Illegal value of i_alphaj", - i_alphaj=physics_variables.i_alphaj, - ) - - # ================================================== - - # ----------------------------------------------------- - # Plasma Normalised Internal Inductance - # ----------------------------------------------------- + physics_variables.p_plasma_separatrix_mw = ( + physics_variables.f_p_alpha_plasma_deposited + * physics_variables.p_alpha_total_mw + + physics_variables.p_non_alpha_charged_mw + + pinj + + physics_variables.p_plasma_ohmic_mw + - physics_variables.p_plasma_rad_mw + ) - self.inductance.run() + physics_variables.plfux_plasma_surface_neutron_avg_mw = ( + physics_variables.p_neutron_total_mw / physics_variables.a_plasma_surface + ) - # =================================================== + # KLUDGE: Ensure p_plasma_separatrix_mw is continuously positive (physical, rather than + # negative potential power), as required by other models (e.g. + # Physics.calculate_density_limit()) + physics_variables.p_plasma_separatrix_mw = ( + physics_variables.p_plasma_separatrix_mw + / (1 - np.exp(-physics_variables.p_plasma_separatrix_mw)) + ) - # Calculate density and temperature profile quantities - # If physics_variables.i_plasma_pedestal = 1 then set pedestal density to - # physics_variables.f_nd_plasma_pedestal_greenwald * Greenwald density limit - # Note: this used to be done before plasma current - if (physics_variables.i_plasma_pedestal == 1) and ( - physics_variables.f_nd_plasma_pedestal_greenwald >= 0e0 - ): - physics_variables.nd_plasma_pedestal_electron = ( - physics_variables.f_nd_plasma_pedestal_greenwald - * 1.0e14 - * physics_variables.plasma_current - / (np.pi * physics_variables.rminor * physics_variables.rminor) + # if double null configuration share the power + # over the upper and lower divertor, where physics_variables.f_p_div_lower gives + # the factor of power conducted to the lower divertor + if divertor_variables.n_divertors == 2: + physics_variables.p_div_lower_separatrix_mw = ( + physics_variables.f_p_div_lower + * physics_variables.p_plasma_separatrix_mw ) - - if (physics_variables.i_plasma_pedestal == 1) and ( - physics_variables.f_nd_plasma_separatrix_greenwald >= 0e0 - ): - physics_variables.nd_plasma_separatrix_electron = ( - physics_variables.f_nd_plasma_separatrix_greenwald - * 1.0e14 - * physics_variables.plasma_current - / (np.pi * physics_variables.rminor * physics_variables.rminor) + physics_variables.p_div_upper_separatrix_mw = ( + 1.0e0 - physics_variables.f_p_div_lower + ) * physics_variables.p_plasma_separatrix_mw + physics_variables.p_div_separatrix_max_mw = max( + physics_variables.p_div_lower_separatrix_mw, + physics_variables.p_div_upper_separatrix_mw, ) - self.plasma_profile.run() - - # Calculate total magnetic field [T] - physics_variables.b_plasma_total = np.sqrt( - physics_variables.b_plasma_toroidal_on_axis**2 - + physics_variables.b_plasma_poloidal_average**2 + # Resistive diffusion time = current penetration time ~ mu0.a^2/resistivity + physics_variables.t_plasma_res_diffusion = res_diff_time( + physics_variables.rmajor, + physics_variables.res_plasma, + physics_variables.kappa95, ) - # Calculate the toroidal field across the plasma - # Calculate the toroidal field profile across the plasma (1/R dependence) - # Double element size to include both sides of the plasma - rho = np.linspace( - physics_variables.rmajor - physics_variables.rminor, - physics_variables.rmajor + physics_variables.rminor, - 2 * physics_variables.n_plasma_profile_elements, + # Power transported to the first wall by escaped alpha particles + physics_variables.p_fw_alpha_mw = physics_variables.p_alpha_total_mw * ( + 1.0e0 - physics_variables.f_p_alpha_plasma_deposited ) - # Calculate the inboard and outboard toroidal field - physics_variables.b_plasma_inboard_toroidal = ( - physics_variables.rmajor - * physics_variables.b_plasma_toroidal_on_axis - / (physics_variables.rmajor - physics_variables.rminor) + # Density limit + ( + physics_variables.nd_plasma_electron_max_array, + physics_variables.nd_plasma_electrons_max, + ) = self.calculate_density_limit( + physics_variables.b_plasma_toroidal_on_axis, + physics_variables.i_density_limit, + physics_variables.p_plasma_separatrix_mw, + current_drive_variables.p_hcd_injected_total_mw, + physics_variables.plasma_current, + divertor_variables.prn1, + physics_variables.qstar, + physics_variables.q95, + physics_variables.rmajor, + physics_variables.rminor, + physics_variables.a_plasma_surface, + physics_variables.n_charge_plasma_effective_vol_avg, ) - physics_variables.b_plasma_outboard_toroidal = ( - physics_variables.rmajor - * physics_variables.b_plasma_toroidal_on_axis - / (physics_variables.rmajor + physics_variables.rminor) + # Calculate transport losses and energy confinement time using the + # chosen scaling law + ( + physics_variables.pden_electron_transport_loss_mw, + physics_variables.pden_ion_transport_loss_mw, + physics_variables.t_electron_energy_confinement, + physics_variables.t_energy_confinement, + physics_variables.t_ion_energy_confinement, + physics_variables.p_plasma_loss_mw, + ) = self.calculate_confinement_time( + physics_variables.m_fuel_amu, + physics_variables.p_alpha_total_mw, + physics_variables.aspect, + physics_variables.b_plasma_toroidal_on_axis, + physics_variables.nd_plasma_ions_total_vol_avg, + physics_variables.nd_plasma_electrons_vol_avg, + physics_variables.nd_plasma_electron_line, + physics_variables.eps, + physics_variables.hfact, + physics_variables.i_confinement_time, + physics_variables.i_plasma_ignited, + physics_variables.kappa, + physics_variables.kappa95, + physics_variables.p_non_alpha_charged_mw, + current_drive_variables.p_hcd_injected_total_mw, + physics_variables.plasma_current, + physics_variables.pden_plasma_core_rad_mw, + physics_variables.rmajor, + physics_variables.rminor, + physics_variables.temp_plasma_electron_density_weighted_kev, + physics_variables.temp_plasma_ion_density_weighted_kev, + physics_variables.q95, + physics_variables.qstar, + physics_variables.vol_plasma, + physics_variables.n_charge_plasma_effective_vol_avg, ) - # Avoid division by zero at the magnetic axis - rho = np.where(rho == 0, 1e-10, rho) - physics_variables.b_plasma_toroidal_profile = ( - physics_variables.rmajor * physics_variables.b_plasma_toroidal_on_axis / rho + # Total transport power from scaling law (MW) + physics_variables.p_electron_transport_loss_mw = ( + physics_variables.pden_electron_transport_loss_mw + * physics_variables.vol_plasma + ) + physics_variables.p_ion_transport_loss_mw = ( + physics_variables.pden_ion_transport_loss_mw * physics_variables.vol_plasma ) - # ============================================ + # Calculate Volt-second requirements + ( + physics_variables.vs_plasma_internal, + physics_variables.ind_plasma, + physics_variables.vs_plasma_burn_required, + physics_variables.vs_plasma_ramp_required, + physics_variables.vs_plasma_ind_ramp, + physics_variables.vs_plasma_res_ramp, + physics_variables.vs_plasma_total_required, + physics_variables.v_plasma_loop_burn, + ) = self.inductance.calculate_volt_second_requirements( + physics_variables.csawth, + physics_variables.eps, + physics_variables.f_c_plasma_inductive, + physics_variables.ejima_coeff, + physics_variables.kappa, + physics_variables.rmajor, + physics_variables.res_plasma, + physics_variables.plasma_current, + times_variables.t_plant_pulse_fusion_ramp, + times_variables.t_plant_pulse_burn, + physics_variables.ind_plasma_internal_norm, + ) - # ----------------------------------------------------- - # Beta Components - # ----------------------------------------------------- + physics_variables.e_plasma_magnetic_stored = ( + 0.5e0 * physics_variables.ind_plasma * physics_variables.plasma_current**2 + ) - self.beta.run() + # Calculate auxiliary physics related information + sbar = 1.0e0 + ( + physics_variables.burnup, + physics_variables.ntau, + physics_variables.nTtau, + physics_variables.figmer, + physics_variables.fusrat, + physics_variables.molflow_plasma_fuelling_required, + physics_variables.rndfuel, + physics_variables.t_alpha_confinement, + physics_variables.f_alpha_energy_confinement, + ) = self.phyaux( + physics_variables.aspect, + physics_variables.nd_plasma_electrons_vol_avg, + physics_variables.temp_plasma_electron_vol_avg_kev, + physics_variables.nd_plasma_fuel_ions_vol_avg, + physics_variables.fusden_total, + physics_variables.fusden_alpha_total, + physics_variables.plasma_current, + sbar, + physics_variables.nd_plasma_alphas_vol_avg, + physics_variables.t_energy_confinement, + physics_variables.vol_plasma, + ) - # ======================================================= + # Total transport power from scaling law (MW) + physics_variables.pscalingmw = ( + physics_variables.p_electron_transport_loss_mw + + physics_variables.p_ion_transport_loss_mw + ) - # Set PF coil ramp times - if pulse_variables.i_pulsed_plant != 1: - if times_variables.i_t_current_ramp_up == 0: - times_variables.t_plant_pulse_plasma_current_ramp_up = ( - physics_variables.plasma_current / 5.0e5 - ) - times_variables.t_plant_pulse_coil_precharge = ( - times_variables.t_plant_pulse_plasma_current_ramp_up - ) - times_variables.t_plant_pulse_plasma_current_ramp_down = ( - times_variables.t_plant_pulse_plasma_current_ramp_up - ) + # ============================================================ + # MDK + # Nominal mean photon wall load on entire first wall area including divertor and beam holes + # Note that 'a_fw_total' excludes these, so they have been added back in. + if physics_variables.i_pflux_fw_neutron == 1: + physics_variables.pflux_fw_rad_mw = ( + physics_variables.ffwal + * physics_variables.p_plasma_rad_mw + / physics_variables.a_plasma_surface + ) else: - if times_variables.pulsetimings == 0.0e0: - # times_variables.t_plant_pulse_coil_precharge is input - times_variables.t_plant_pulse_plasma_current_ramp_up = ( - physics_variables.plasma_current / 1.0e5 - ) - times_variables.t_plant_pulse_plasma_current_ramp_down = ( - times_variables.t_plant_pulse_plasma_current_ramp_up + if divertor_variables.n_divertors == 2: + # Double Null configuration in - including SoL radiation + physics_variables.pflux_fw_rad_mw = ( + ( + 1.0e0 + - fwbs_variables.f_a_fw_outboard_hcd + - 2.0e0 * fwbs_variables.f_ster_div_single + ) + * physics_variables.p_plasma_rad_mw + / build_variables.a_fw_total + + ( + 1.0e0 + - fwbs_variables.f_a_fw_outboard_hcd + - 2.0e0 * fwbs_variables.f_ster_div_single + ) + * physics_variables.rad_fraction_sol + * physics_variables.p_plasma_separatrix_mw + / (build_variables.a_fw_total) ) - else: - # times_variables.t_plant_pulse_plasma_current_ramp_up is set either in INITIAL or INPUT, or by being - # iterated using limit equation 41. - times_variables.t_plant_pulse_coil_precharge = max( - times_variables.t_plant_pulse_coil_precharge, - times_variables.t_plant_pulse_plasma_current_ramp_up, - ) - # t_plant_pulse_plasma_current_ramp_down = max(t_plant_pulse_plasma_current_ramp_down,t_plant_pulse_plasma_current_ramp_up) - times_variables.t_plant_pulse_plasma_current_ramp_down = ( - times_variables.t_plant_pulse_plasma_current_ramp_up + # Single null configuration - including SoL radaition + physics_variables.pflux_fw_rad_mw = ( + ( + 1.0e0 + - fwbs_variables.f_a_fw_outboard_hcd + - fwbs_variables.f_ster_div_single + ) + * physics_variables.p_plasma_rad_mw + / build_variables.a_fw_total + + ( + 1.0e0 + - fwbs_variables.f_a_fw_outboard_hcd + - fwbs_variables.f_ster_div_single + ) + * physics_variables.rad_fraction_sol + * physics_variables.p_plasma_separatrix_mw + / build_variables.a_fw_total ) - # Reset second times_variables.t_plant_pulse_burn value (times_variables.t_burn_0). - # This is used to ensure that the burn time is used consistently; - # see convergence loop in fcnvmc1, evaluators.f90 - times_variables.t_burn_0 = times_variables.t_plant_pulse_burn - - # Time during the pulse in which a plasma is present - times_variables.t_plant_pulse_plasma_present = ( - times_variables.t_plant_pulse_plasma_current_ramp_up - + times_variables.t_plant_pulse_fusion_ramp - + times_variables.t_plant_pulse_burn - + times_variables.t_plant_pulse_plasma_current_ramp_down - ) - times_variables.t_plant_pulse_no_burn = ( - times_variables.t_plant_pulse_coil_precharge - + times_variables.t_plant_pulse_plasma_current_ramp_up - + times_variables.t_plant_pulse_plasma_current_ramp_down - + times_variables.t_plant_pulse_dwell - + times_variables.t_plant_pulse_fusion_ramp + constraint_variables.pflux_fw_rad_max_mw = ( + physics_variables.pflux_fw_rad_mw * constraint_variables.f_fw_rad_max ) - # Total cycle time - times_variables.t_plant_pulse_total = ( - times_variables.t_plant_pulse_coil_precharge - + times_variables.t_plant_pulse_plasma_current_ramp_up - + times_variables.t_plant_pulse_fusion_ramp - + times_variables.t_plant_pulse_burn - + times_variables.t_plant_pulse_plasma_current_ramp_down - + times_variables.t_plant_pulse_dwell + # Calculate the target imbalances + # find the total power into the targets + physics_variables.ptarmw = physics_variables.p_plasma_separatrix_mw * ( + 1.0e0 - physics_variables.rad_fraction_sol ) + # use physics_variables.f_p_div_lower to find deltarsep + # Parameters taken from double null machine + # D. Brunner et al + physics_variables.lambdaio = 1.57e-3 - # ***************************** # - # DIAMAGNETIC CURRENT # - # ***************************** # - - # Hender scaling for diamagnetic current at tight physics_variables.aspect ratio - current_drive_variables.f_c_plasma_diamagnetic_hender = ( - diamagnetic_fraction_hender(physics_variables.beta_total_vol_avg) - ) + # Issue #1559 Infinities in physics_module.drsep when running single null in a double null machine + # C W Ashe + if physics_variables.f_p_div_lower < 4.5e-5: + physics_variables.drsep = 1.5e-2 + elif physics_variables.f_p_div_lower > (1.0e0 - 4.5e-5): + physics_variables.drsep = -1.5e-2 + else: + physics_variables.drsep = ( + -2.0e0 + * 1.5e-3 + * math.atanh(2.0e0 * (physics_variables.f_p_div_lower - 0.5e0)) + ) + # Model Taken from D3-D paper for conventional divertor + # Journal of Nuclear Materials + # Volumes 290-293, March 2001, Pages 935-939 + # Find the innner and outer lower target imbalance - # SCENE scaling for diamagnetic current - current_drive_variables.f_c_plasma_diamagnetic_scene = ( - diamagnetic_fraction_scene( - physics_variables.beta_total_vol_avg, - physics_variables.q95, - physics_variables.q0, + physics_variables.fio = 0.16e0 + (0.16e0 - 0.41e0) * ( + 1.0e0 + - ( + 2.0e0 + / ( + 1.0e0 + + np.exp( + -((physics_variables.drsep / physics_variables.lambdaio) ** 2) + ) + ) ) ) - - if physics_variables.i_diamagnetic_current == 1: - current_drive_variables.f_c_plasma_diamagnetic = ( - current_drive_variables.f_c_plasma_diamagnetic_hender + if divertor_variables.n_divertors == 2: + # Double Null configuration + # Find all the power fractions accross the targets + # Taken from D3-D conventional divertor design + physics_variables.fli = ( + physics_variables.f_p_div_lower * physics_variables.fio ) - elif physics_variables.i_diamagnetic_current == 2: - current_drive_variables.f_c_plasma_diamagnetic = ( - current_drive_variables.f_c_plasma_diamagnetic_scene + physics_variables.flo = physics_variables.f_p_div_lower * ( + 1.0e0 - physics_variables.fio ) + physics_variables.fui = ( + 1.0e0 - physics_variables.f_p_div_lower + ) * physics_variables.fio + physics_variables.fuo = (1.0e0 - physics_variables.f_p_div_lower) * ( + 1.0e0 - physics_variables.fio + ) + # power into each target + physics_variables.plimw = physics_variables.fli * physics_variables.ptarmw + physics_variables.plomw = physics_variables.flo * physics_variables.ptarmw + physics_variables.puimw = physics_variables.fui * physics_variables.ptarmw + physics_variables.puomw = physics_variables.fuo * physics_variables.ptarmw + else: + # Single null configuration + physics_variables.fli = physics_variables.fio + physics_variables.flo = 1.0e0 - physics_variables.fio + # power into each target + physics_variables.plimw = physics_variables.fli * physics_variables.ptarmw + physics_variables.plomw = physics_variables.flo * physics_variables.ptarmw - # ***************************** # - # PFIRSCH-SCHLÜTER CURRENT # - # ***************************** # - - # Pfirsch-Schlüter scaling for diamagnetic current - current_drive_variables.f_c_plasma_pfirsch_schluter_scene = ps_fraction_scene( - physics_variables.beta_total_vol_avg + # Calculate some derived quantities that may not have been defined earlier + physics_variables.p_plasma_heating_total_mw = 1e6 * ( + physics_variables.f_p_alpha_plasma_deposited + * physics_variables.p_alpha_total_mw + + physics_variables.p_non_alpha_charged_mw + + physics_variables.p_plasma_ohmic_mw + + current_drive_variables.p_hcd_injected_total_mw + ) + physics_variables.f_p_plasma_separatrix_rad = ( + 1.0e6 + * physics_variables.p_plasma_rad_mw + / physics_variables.p_plasma_heating_total_mw + ) + physics_variables.rad_fraction_total = ( + physics_variables.f_p_plasma_separatrix_rad + + (1.0e0 - physics_variables.f_p_plasma_separatrix_rad) + * physics_variables.rad_fraction_sol + ) + physics_variables.pradsolmw = ( + physics_variables.rad_fraction_sol * physics_variables.p_plasma_separatrix_mw ) - if physics_variables.i_pfirsch_schluter_current == 1: - current_drive_variables.f_c_plasma_pfirsch_schluter = ( - current_drive_variables.f_c_plasma_pfirsch_schluter_scene + if 78 in numerics.icc: + po.write( + self.outfile, + ( + f"reinke t and fz, physics = {physics_variables.temp_plasma_separatrix_kev} , {reinke_variables.fzmin}" + ), ) - - # ***************************** # - # BOOTSTRAP CURRENT # - # ***************************** # - - # Calculate bootstrap current fraction using various models - current_drive_variables.f_c_plasma_bootstrap_iter89 = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_iter89( - physics_variables.aspect, - physics_variables.beta_total_vol_avg, - physics_variables.b_plasma_total, - physics_variables.plasma_current, - physics_variables.q95, - physics_variables.q0, - physics_variables.rmajor, - physics_variables.vol_plasma, + fgw = ( + physics_variables.nd_plasma_electron_max_array[6] + / physics_variables.nd_plasma_electrons_vol_avg ) - ) - - current_drive_variables.f_c_plasma_bootstrap_nevins = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_nevins( - physics_variables.alphan, - physics_variables.alphat, - physics_variables.beta_toroidal_vol_avg, + # calculate separatrix temperature, if Reinke criterion is used + physics_variables.temp_plasma_separatrix_kev = reinke_tsep( physics_variables.b_plasma_toroidal_on_axis, - physics_variables.nd_plasma_electrons_vol_avg, - physics_variables.plasma_current, + physics_variables.p_plasma_separatrix_mw + / physics_variables.p_l_h_threshold_mw, physics_variables.q95, - physics_variables.q0, physics_variables.rmajor, - physics_variables.rminor, - physics_variables.temp_plasma_electron_vol_avg_kev, - physics_variables.n_charge_plasma_effective_vol_avg, + physics_variables.eps, + fgw, + physics_variables.kappa, + reinke_variables.lhat, ) - ) - # Wilson scaling uses thermal poloidal beta, not total - current_drive_variables.f_c_plasma_bootstrap_wilson = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_wilson( - physics_variables.alphaj, - physics_variables.alphap, - physics_variables.alphat, - physics_variables.beta_thermal_poloidal_vol_avg, - physics_variables.q0, - physics_variables.q95, - physics_variables.rmajor, - physics_variables.rminor, - ) - ) + if reinke_variables.fzmin >= 1.0e0: + logger.error( + "REINKE IMPURITY MODEL: fzmin is greater than or equal to 1.0, this is at least notable" + ) - ( - current_drive_variables.f_c_plasma_bootstrap_sauter, - physics_variables.j_plasma_bootstrap_sauter_profile, - ) = self.bootstrap.bootstrap_fraction_sauter(self.plasma_profile) - current_drive_variables.f_c_plasma_bootstrap_sauter *= ( - current_drive_variables.cboot - ) - - current_drive_variables.f_c_plasma_bootstrap_sakai = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_sakai( - beta_poloidal=physics_variables.beta_poloidal_vol_avg, - q95=physics_variables.q95, - q0=physics_variables.q0, - alphan=physics_variables.alphan, - alphat=physics_variables.alphat, - eps=physics_variables.eps, - ind_plasma_internal_norm=physics_variables.ind_plasma_internal_norm, + po.write( + self.outfile, + ( + f" 'fzactual, frac, reinke_variables.impvardiv = {reinke_variables.fzactual}," + f" {impurity_radiation_module.f_nd_impurity_electron_array(reinke_variables.impvardiv)}," + f" {reinke_variables.impvardiv}" + ), ) - ) - current_drive_variables.f_c_plasma_bootstrap_aries = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_aries( - beta_poloidal=physics_variables.beta_poloidal_vol_avg, - ind_plasma_internal_norm=physics_variables.ind_plasma_internal_norm, - core_density=physics_variables.nd_plasma_electron_on_axis, - average_density=physics_variables.nd_plasma_electrons_vol_avg, - inverse_aspect=physics_variables.eps, - ) - ) + @staticmethod + def calculate_current_profile_index_wesson(qstar: float, q0: float) -> float: + """Calculate the Wesson current profile index. - current_drive_variables.f_c_plasma_bootstrap_andrade = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_andrade( - beta_poloidal=physics_variables.beta_poloidal_vol_avg, - core_pressure=physics_variables.pres_plasma_thermal_on_axis, - average_pressure=physics_variables.pres_plasma_thermal_vol_avg, - inverse_aspect=physics_variables.eps, - ) - ) - current_drive_variables.f_c_plasma_bootstrap_hoang = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_hoang( - beta_poloidal=physics_variables.beta_poloidal_vol_avg, - pressure_index=physics_variables.alphap, - current_index=physics_variables.alphaj, - inverse_aspect=physics_variables.eps, - ) - ) - current_drive_variables.f_c_plasma_bootstrap_wong = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_wong( - beta_poloidal=physics_variables.beta_poloidal_vol_avg, - density_index=physics_variables.alphan, - temperature_index=physics_variables.alphat, - inverse_aspect=physics_variables.eps, - elongation=physics_variables.kappa, - ) - ) - current_drive_variables.bscf_gi_i = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_gi_I( - beta_poloidal=physics_variables.beta_poloidal_vol_avg, - pressure_index=physics_variables.alphap, - temperature_index=physics_variables.alphat, - inverse_aspect=physics_variables.eps, - effective_charge=physics_variables.n_charge_plasma_effective_vol_avg, - q95=physics_variables.q95, - q0=physics_variables.q0, - ) - ) + Parameters + ---------- + qstar : float + Cylindrical safety factor. + q0 : float + Safety factor on axis. - current_drive_variables.bscf_gi_ii = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_gi_II( - beta_poloidal=physics_variables.beta_poloidal_vol_avg, - pressure_index=physics_variables.alphap, - temperature_index=physics_variables.alphat, - inverse_aspect=physics_variables.eps, - effective_charge=physics_variables.n_charge_plasma_effective_vol_avg, - ) - ) - current_drive_variables.f_c_plasma_bootstrap_sugiyama_l = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_sugiyama_l_mode( - eps=physics_variables.eps, - beta_poloidal=physics_variables.beta_poloidal_vol_avg, - alphan=physics_variables.alphan, - alphat=physics_variables.alphat, - zeff=physics_variables.n_charge_plasma_effective_vol_avg, - q95=physics_variables.q95, - q0=physics_variables.q0, - ) - ) - current_drive_variables.f_c_plasma_bootstrap_sugiyama_h = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_sugiyama_h_mode( - eps=physics_variables.eps, - beta_poloidal=physics_variables.beta_poloidal_vol_avg, - alphan=physics_variables.alphan, - alphat=physics_variables.alphat, - tbeta=physics_variables.tbeta, - zeff=physics_variables.n_charge_plasma_effective_vol_avg, - q95=physics_variables.q95, - q0=physics_variables.q0, - radius_plasma_pedestal_density_norm=physics_variables.radius_plasma_pedestal_density_norm, - nd_plasma_pedestal_electron=physics_variables.nd_plasma_pedestal_electron, - n_greenwald=physics_variables.nd_plasma_electron_max_array[6], - temp_plasma_pedestal_kev=physics_variables.temp_plasma_pedestal_kev, - ) - ) + Returns + ------- + float + The Wesson current profile index. - # Calculate beta_norm_max based on i_beta_norm_max - try: - model = BootstrapCurrentFractionModel( - int(physics_variables.i_bootstrap_current) - ) - current_drive_variables.f_c_plasma_bootstrap = ( - self.bootstrap.get_bootstrap_current_fraction_value(model) - ) - except ValueError: - raise ProcessValueError( - "Illegal value of i_bootstrap_current", - i_bootstrap_current=physics_variables.i_bootstrap_current, - ) from None + Notes + ----- + - It is recommended to use this method with the other Wesson relations for normalised beta and + normalised internal inductance. + - This relation is only true for the cyclindrical plasma approximation. - physics_variables.err242 = 0 - if ( - current_drive_variables.f_c_plasma_bootstrap - > current_drive_variables.f_c_plasma_bootstrap_max - ) and physics_variables.i_bootstrap_current != 0: - current_drive_variables.f_c_plasma_bootstrap = min( - current_drive_variables.f_c_plasma_bootstrap, - current_drive_variables.f_c_plasma_bootstrap_max, - ) - physics_variables.err242 = 1 + References + ---------- + - Wesson, J. (2011) Tokamaks. 4th Edition, 2011 Oxford Science Publications, + International Series of Monographs on Physics, Volume 149. - current_drive_variables.f_c_plasma_internal = ( - current_drive_variables.f_c_plasma_bootstrap - + current_drive_variables.f_c_plasma_diamagnetic - + current_drive_variables.f_c_plasma_pfirsch_schluter - ) + """ + return qstar / q0 - 1.0 - # Plasma driven current fraction (Bootstrap + Diamagnetic - # + Pfirsch-Schlüter) constrained to be less than - # or equal to the total fraction of the plasma current - # produced by non-inductive means (which also includes - # the current drive proportion) - physics_variables.err243 = 0 - if ( - current_drive_variables.f_c_plasma_internal - > physics_variables.f_c_plasma_non_inductive - ): - current_drive_variables.f_c_plasma_internal = min( - current_drive_variables.f_c_plasma_internal, - physics_variables.f_c_plasma_non_inductive, - ) - physics_variables.err243 = 1 + @staticmethod + def calculate_density_limit( + b_plasma_toroidal_on_axis: float, + i_density_limit: int, + p_plasma_separatrix_mw: float, + p_hcd_injected_total_mw: float, + plasma_current: float, + prn1: float, + qcyl: float, + q95: float, + rmajor: float, + rminor: float, + a_plasma_surface: float, + zeff: float, + ) -> tuple[np.ndarray, float]: + """Calculate the density limit using various models. - # Fraction of plasma current produced by inductive means - physics_variables.f_c_plasma_inductive = max( - 1.0e-10, (1.0e0 - physics_variables.f_c_plasma_non_inductive) - ) - # Fraction of plasma current produced by auxiliary current drive - physics_variables.f_c_plasma_auxiliary = ( - physics_variables.f_c_plasma_non_inductive - - current_drive_variables.f_c_plasma_internal - ) + Parameters + ---------- + b_plasma_toroidal_on_axis : float + Toroidal field on axis (T). + i_density_limit : int + Switch denoting which formula to enforce. + p_plasma_separatrix_mw : float + Power flowing to the edge plasma via charged particles (MW). + p_hcd_injected_total_mw : float + Power injected into the plasma (MW). + plasma_current : float + Plasma current (A). + prn1 : float + Edge density / average plasma density. + qcyl : float + Equivalent cylindrical safety factor (qstar). + q95 : float + Safety factor at 95% surface. + rmajor : float + Plasma major radius (m). + rminor : float + Plasma minor radius (m). + a_plasma_surface : float + Plasma surface area (m^2). + zeff : float + Plasma effective charge. - # Auxiliary current drive power calculations + Returns + ------- + Tuple[np.ndarray, float] + A tuple containing: + - nd_plasma_electron_max_array (np.ndarray): Average plasma density limit using seven different models (m^-3). + - nd_plasma_electrons_max (float): Enforced average plasma density limit (m^-3). - if current_drive_variables.i_hcd_calculations != 0: - self.current_drive.cudriv() + Raises + ------ + ValueError + If i_density_limit is not between 1 and 7. + Notes + ----- + ValueError + If i_density_limit is not between 1 and 7. + Notes + ----- + This routine calculates several different formulae for the density limit and enforces the one chosen by the user. + ValueError + If i_density_limit is not between 1 and 7. + Notes + ----- + This routine calculates several different formulae for the density limit and enforces the one chosen by the user. + For i_density_limit = 1-5, 8, we scale the sepatrix density limit output by the ratio of the separatrix to volume averaged density - # ***************************** # - # FUSION REACTIONS # - # ***************************** # + References + ---------- + - AEA FUS 172 + Physics Assessment for the European Reactor Study + - N.A. Uckan and ITER Physics Group, 'ITER Physics Design Guidelines + 1989 + - N.A. Uckan and ITER Physics Group, 'ITER Physics Design Guidelines + 1989 + - M. Bernert et al., “The H-mode density limit in the full tungsten ASDEX Upgrade tokamak,” + vol. 57, no. 1, pp. 014038-014038, Nov. 2014, doi: https://doi.org/10.1088/0741-3335/57/1/014038. ‌ - # Calculate fusion power + components + """ - fusion_reactions = reactions.FusionReactionRate(self.plasma_profile) - fusion_reactions.deuterium_branching( - physics_variables.temp_plasma_ion_vol_avg_kev - ) - fusion_reactions.calculate_fusion_rates() - fusion_reactions.set_physics_variables() + if i_density_limit < 1 or i_density_limit > 7: + raise ProcessValueError( + "Illegal value for i_density_limit", i_density_limit=i_density_limit + ) - # This neglects the power from the beam - physics_variables.p_plasma_dt_mw = ( - physics_variables.dt_power_density_plasma * physics_variables.vol_plasma - ) - physics_variables.p_dhe3_total_mw = ( - physics_variables.dhe3_power_density * physics_variables.vol_plasma - ) - physics_variables.p_dd_total_mw = ( - physics_variables.dd_power_density * physics_variables.vol_plasma - ) + nd_plasma_electron_max_array = np.empty((8,)) - # Calculate neutral beam slowing down effects - # If ignited, then ignore beam fusion effects + # Power per unit area crossing the plasma edge + # (excludes radiation and neutrons) - if (current_drive_variables.c_beam_total != 0.0e0) and ( - physics_variables.i_plasma_ignited == 0 - ): - ( - physics_variables.beta_beam, - physics_variables.nd_beam_ions_out, - physics_variables.p_beam_alpha_mw, - ) = reactions.beam_fusion( - physics_variables.beamfus0, - physics_variables.betbm0, - physics_variables.b_plasma_poloidal_average, - physics_variables.b_plasma_toroidal_on_axis, - current_drive_variables.c_beam_total, - physics_variables.nd_plasma_electrons_vol_avg, - physics_variables.nd_plasma_fuel_ions_vol_avg, - physics_variables.dlamie, - current_drive_variables.e_beam_kev, - physics_variables.f_plasma_fuel_deuterium, - physics_variables.f_plasma_fuel_tritium, - current_drive_variables.f_beam_tritium, - physics_variables.sigmav_dt_average, - physics_variables.temp_plasma_electron_density_weighted_kev, - physics_variables.temp_plasma_ion_density_weighted_kev, - physics_variables.vol_plasma, - physics_variables.n_charge_plasma_effective_mass_weighted_vol_avg, - ) - physics_variables.fusden_total = ( - physics_variables.fusden_plasma - + 1.0e6 - * physics_variables.p_beam_alpha_mw - / (constants.DT_ALPHA_ENERGY) - / physics_variables.vol_plasma - ) - physics_variables.fusden_alpha_total = ( - physics_variables.fusden_plasma_alpha - + 1.0e6 - * physics_variables.p_beam_alpha_mw - / (constants.DT_ALPHA_ENERGY) - / physics_variables.vol_plasma - ) - physics_variables.p_dt_total_mw = ( - physics_variables.p_plasma_dt_mw - + (1.0 / (1.0 - constants.DT_NEUTRON_ENERGY_FRACTION)) - * physics_variables.p_beam_alpha_mw - ) - physics_variables.p_beam_neutron_mw = physics_variables.p_beam_alpha_mw * ( - constants.DT_NEUTRON_ENERGY_FRACTION - / (1 - constants.DT_NEUTRON_ENERGY_FRACTION) - ) - physics_variables.p_beam_dt_mw = physics_variables.p_beam_alpha_mw * ( - 1 / (1 - constants.DT_NEUTRON_ENERGY_FRACTION) - ) - else: - # If no beams present then the total alpha rates and power are the same as the plasma values - physics_variables.fusden_total = physics_variables.fusden_plasma - physics_variables.fusden_alpha_total = physics_variables.fusden_plasma_alpha - physics_variables.p_dt_total_mw = physics_variables.p_plasma_dt_mw + p_perp = p_plasma_separatrix_mw / a_plasma_surface - physics_variables.fusrat_total = ( - physics_variables.fusden_total * physics_variables.vol_plasma - ) + # Old ASDEX density limit formula + # This applies to the density at the plasma edge, so must be scaled + # to give the density limit applying to the average plasma density. - # Create some derived values and add beam contribution to fusion power - ( - physics_variables.pden_neutron_total_mw, - physics_variables.p_plasma_alpha_mw, - physics_variables.p_alpha_total_mw, - physics_variables.p_plasma_neutron_mw, - physics_variables.p_neutron_total_mw, - physics_variables.p_non_alpha_charged_mw, - physics_variables.pden_alpha_total_mw, - physics_variables.f_pden_alpha_electron_mw, - physics_variables.f_pden_alpha_ions_mw, - physics_variables.p_charged_particle_mw, - physics_variables.p_fusion_total_mw, - ) = reactions.set_fusion_powers( - physics_variables.f_alpha_electron, - physics_variables.f_alpha_ion, - physics_variables.p_beam_alpha_mw, - physics_variables.pden_non_alpha_charged_mw, - physics_variables.pden_plasma_neutron_mw, - physics_variables.vol_plasma, - physics_variables.pden_plasma_alpha_mw, - ) + nd_plasma_electron_max_array[0] = ( + 1.54e20 + * p_perp**0.43 + * b_plasma_toroidal_on_axis**0.31 + / (q95 * rmajor) ** 0.45 + ) / prn1 - physics_variables.beta_fast_alpha = self.beta.fast_alpha_beta( - b_plasma_poloidal_average=physics_variables.b_plasma_poloidal_average, - b_plasma_toroidal_on_axis=physics_variables.b_plasma_toroidal_on_axis, - nd_plasma_electrons_vol_avg=physics_variables.nd_plasma_electrons_vol_avg, - nd_plasma_fuel_ions_vol_avg=physics_variables.nd_plasma_fuel_ions_vol_avg, - nd_plasma_ions_total_vol_avg=physics_variables.nd_plasma_ions_total_vol_avg, - temp_plasma_electron_density_weighted_kev=physics_variables.temp_plasma_electron_density_weighted_kev, - temp_plasma_ion_density_weighted_kev=physics_variables.temp_plasma_ion_density_weighted_kev, - pden_alpha_total_mw=physics_variables.pden_alpha_total_mw, - pden_plasma_alpha_mw=physics_variables.pden_plasma_alpha_mw, - i_beta_fast_alpha=physics_variables.i_beta_fast_alpha, - ) + # Borrass density limit model for ITER (I) + # This applies to the density at the plasma edge, so must be scaled + # to give the density limit applying to the average plasma density. + # Borrass et al, ITER-TN-PH-9-6 (1989) - # Nominal mean neutron wall load on entire first wall area including divertor and beam holes - # Note that 'a_fw_total' excludes these, so they have been added back in. - if physics_variables.i_pflux_fw_neutron == 1: - physics_variables.pflux_fw_neutron_mw = ( - physics_variables.ffwal - * physics_variables.p_neutron_total_mw - / physics_variables.a_plasma_surface - ) - else: - if divertor_variables.n_divertors == 2: - # Double null configuration - physics_variables.pflux_fw_neutron_mw = ( - ( - 1.0e0 - - fwbs_variables.f_a_fw_outboard_hcd - - 2.0e0 * fwbs_variables.f_ster_div_single - ) - * physics_variables.p_neutron_total_mw - / build_variables.a_fw_total - ) - else: - # Single null Configuration - physics_variables.pflux_fw_neutron_mw = ( - ( - 1.0e0 - - fwbs_variables.f_a_fw_outboard_hcd - - fwbs_variables.f_ster_div_single - ) - * physics_variables.p_neutron_total_mw - / build_variables.a_fw_total + nd_plasma_electron_max_array[1] = ( + 1.8e20 + * p_perp**0.53 + * b_plasma_toroidal_on_axis**0.31 + / (q95 * rmajor) ** 0.22 + ) / prn1 + + # Borrass density limit model for ITER (II) + # This applies to the density at the plasma edge, so must be scaled + # to give the density limit applying to the average plasma density. + # This formula is (almost) identical to that in the original routine + # denlim (now deleted). + + nd_plasma_electron_max_array[2] = ( + 0.5e20 + * p_perp**0.57 + * b_plasma_toroidal_on_axis**0.31 + / (q95 * rmajor) ** 0.09 + ) / prn1 + + # JET edge radiation density limit model + # This applies to the density at the plasma edge, so must be scaled + # to give the density limit applying to the average plasma density. + # qcyl=qstar here, but literature is not clear. + + denom = (zeff - 1.0) * (1.0 - 4.0 / (3.0 * qcyl)) + if denom <= 0.0: + if i_density_limit == 4: + logger.error( + f"qcyl < 4/3; nd_plasma_electron_max_array(4) set to zero; model 5 will be enforced instead. {denom=} {qcyl=}" ) + i_density_limit = 5 - # Calculate ion/electron equilibration power + nd_plasma_electron_max_array[3] = 0.0 + else: + nd_plasma_electron_max_array[3] = ( + 1.0e20 * np.sqrt(p_hcd_injected_total_mw / denom) + ) / prn1 - physics_variables.pden_ion_electron_equilibration_mw = rether( - physics_variables.alphan, - physics_variables.alphat, - physics_variables.nd_plasma_electrons_vol_avg, - physics_variables.dlamie, - physics_variables.temp_plasma_electron_vol_avg_kev, - physics_variables.temp_plasma_ion_vol_avg_kev, - physics_variables.n_charge_plasma_effective_mass_weighted_vol_avg, - ) + # JET simplified density limit model + # This applies to the density at the plasma edge, so must be scaled + # to give the density limit applying to the average plasma density. - # Calculate radiation power + nd_plasma_electron_max_array[4] = ( + 0.237e20 + * b_plasma_toroidal_on_axis + * np.sqrt(p_plasma_separatrix_mw) + / rmajor + ) / prn1 - radpwrdata = physics_funcs.calculate_radiation_powers( - self.plasma_profile, - physics_variables.nd_plasma_electron_on_axis, - physics_variables.rminor, - physics_variables.b_plasma_toroidal_on_axis, - physics_variables.aspect, - physics_variables.alphan, - physics_variables.alphat, - physics_variables.tbeta, - physics_variables.temp_plasma_electron_on_axis_kev, - physics_variables.f_sync_reflect, - physics_variables.rmajor, - physics_variables.kappa, - physics_variables.vol_plasma, + # Hugill-Murakami M.q limit + # qcyl=qstar here, which is okay according to the literature + + nd_plasma_electron_max_array[5] = ( + 3.0e20 * b_plasma_toroidal_on_axis / (rmajor * qcyl) ) - physics_variables.pden_plasma_sync_mw = radpwrdata.pden_plasma_sync_mw - physics_variables.pden_plasma_core_rad_mw = radpwrdata.pden_plasma_core_rad_mw - physics_variables.pden_plasma_outer_rad_mw = radpwrdata.pden_plasma_outer_rad_mw - physics_variables.pden_plasma_rad_mw = radpwrdata.pden_plasma_rad_mw - physics_variables.p_plasma_sync_mw = ( - physics_variables.pden_plasma_sync_mw * physics_variables.vol_plasma - ) - physics_variables.p_plasma_inner_rad_mw = ( - physics_variables.pden_plasma_core_rad_mw * physics_variables.vol_plasma - ) - physics_variables.p_plasma_outer_rad_mw = ( - physics_variables.pden_plasma_outer_rad_mw * physics_variables.vol_plasma - ) - physics_variables.p_plasma_rad_mw = ( - physics_variables.pden_plasma_rad_mw * physics_variables.vol_plasma - ) + # Greenwald limit - # Calculate ohmic power - ( - physics_variables.pden_plasma_ohmic_mw, - physics_variables.p_plasma_ohmic_mw, - physics_variables.f_res_plasma_neo, - physics_variables.res_plasma, - ) = self.plasma_ohmic_heating( - physics_variables.f_c_plasma_inductive, - physics_variables.kappa95, - physics_variables.plasma_current, - physics_variables.rmajor, - physics_variables.rminor, - physics_variables.temp_plasma_electron_density_weighted_kev, - physics_variables.vol_plasma, - physics_variables.n_charge_plasma_effective_vol_avg, + nd_plasma_electron_max_array[6] = ( + 1.0e14 * plasma_current / (np.pi * rminor * rminor) ) - # Calculate L- to H-mode power threshold for different scalings - physics_variables.l_h_threshold_powers = l_h_threshold_power( - physics_variables.nd_plasma_electron_line, - physics_variables.b_plasma_toroidal_on_axis, - physics_variables.rmajor, - physics_variables.rminor, - physics_variables.kappa, - physics_variables.a_plasma_surface, - physics_variables.m_ions_total_amu, - physics_variables.aspect, - physics_variables.plasma_current, - ) + nd_plasma_electron_max_array[7] = ( + 1.0e20 + * 0.506 + * (p_hcd_injected_total_mw**0.396 * (plasma_current / 1.0e6) ** 0.265) + / (q95**0.323) + ) / prn1 - # Enforced L-H power threshold value (if constraint 15 is turned on) - physics_variables.p_l_h_threshold_mw = physics_variables.l_h_threshold_powers[ - physics_variables.i_l_h_threshold - 1 + # Enforce the chosen density limit + + return nd_plasma_electron_max_array, nd_plasma_electron_max_array[ + i_density_limit - 1 ] - # Power transported to the divertor by charged particles, - # i.e. excludes neutrons and radiation, and also NBI orbit loss power, - # which is assumed to be absorbed by the first wall - pinj = ( - current_drive_variables.p_hcd_injected_total_mw - if physics_variables.i_plasma_ignited == 0 - else 0.0 - ) + @staticmethod + def plasma_composition(): + """Calculates various plasma component fractional makeups. - physics_variables.p_plasma_separatrix_mw = ( - physics_variables.f_p_alpha_plasma_deposited - * physics_variables.p_alpha_total_mw - + physics_variables.p_non_alpha_charged_mw - + pinj - + physics_variables.p_plasma_ohmic_mw - - physics_variables.p_plasma_rad_mw - ) + This subroutine determines the various plasma component fractional makeups. + It is the replacement for the original routine betcom(), and is used in conjunction + with the new impurity radiation model. - physics_variables.plfux_plasma_surface_neutron_avg_mw = ( - physics_variables.p_neutron_total_mw / physics_variables.a_plasma_surface - ) + This function performs the following calculations: + - Determines the alpha ash portion. + - Calculates the proton density. + - Calculates the beam hot ion component. + - Sums the ion densities for all impurity ions with charge greater than helium. + - Ensures charge neutrality by adjusting the fuel portion. + - Calculates the total ion density. + - Sets the relative impurity densities for radiation calculations. + - Calculates the effective charge. + - Defines the Coulomb logarithm for ion-electron and electron-electron collisions. + - Calculates the fraction of alpha energy to ions and electrons. + - Calculates the average atomic masses of injected fuel species and neutral beams. + - Calculates the density weighted mass and mass weighted plasma effective charge. + """ - # KLUDGE: Ensure p_plasma_separatrix_mw is continuously positive (physical, rather than - # negative potential power), as required by other models (e.g. - # Physics.calculate_density_limit()) - physics_variables.p_plasma_separatrix_mw = ( - physics_variables.p_plasma_separatrix_mw - / (1 - np.exp(-physics_variables.p_plasma_separatrix_mw)) + # Alpha ash portion + physics_variables.nd_plasma_alphas_vol_avg = ( + physics_variables.nd_plasma_electrons_vol_avg + * physics_variables.f_nd_alpha_electron ) - # if double null configuration share the power - # over the upper and lower divertor, where physics_variables.f_p_div_lower gives - # the factor of power conducted to the lower divertor - if divertor_variables.n_divertors == 2: - physics_variables.p_div_lower_separatrix_mw = ( - physics_variables.f_p_div_lower - * physics_variables.p_plasma_separatrix_mw + # ====================================================================== + + # Protons + # This calculation will be wrong on the first call as the particle + # production rates are evaluated later in the calling sequence + # Issue #557 Allow f_nd_protium_electrons impurity to be specified: 'f_nd_protium_electrons' + # This will override the calculated value which is a minimum. + if physics_variables.fusden_alpha_total < 1.0e-6: # not calculated yet... + physics_variables.nd_plasma_protons_vol_avg = max( + physics_variables.f_nd_protium_electrons + * physics_variables.nd_plasma_electrons_vol_avg, + physics_variables.nd_plasma_alphas_vol_avg + * (physics_variables.f_plasma_fuel_helium3 + 1.0e-3), + ) # rough estimate + else: + physics_variables.nd_plasma_protons_vol_avg = max( + physics_variables.f_nd_protium_electrons + * physics_variables.nd_plasma_electrons_vol_avg, + physics_variables.nd_plasma_alphas_vol_avg + * physics_variables.proton_rate_density + / physics_variables.fusden_alpha_total, ) - physics_variables.p_div_upper_separatrix_mw = ( - 1.0e0 - physics_variables.f_p_div_lower - ) * physics_variables.p_plasma_separatrix_mw - physics_variables.p_div_separatrix_max_mw = max( - physics_variables.p_div_lower_separatrix_mw, - physics_variables.p_div_upper_separatrix_mw, + + # ====================================================================== + + # Beam hot ion component + # If ignited, prevent beam fusion effects + if physics_variables.i_plasma_ignited == 0: + physics_variables.nd_beam_ions = ( + physics_variables.nd_plasma_electrons_vol_avg + * physics_variables.f_nd_beam_electron ) + else: + physics_variables.nd_beam_ions = 0.0 - # Resistive diffusion time = current penetration time ~ mu0.a^2/resistivity - physics_variables.t_plasma_res_diffusion = res_diff_time( - physics_variables.rmajor, - physics_variables.res_plasma, - physics_variables.kappa95, - ) + # ====================================================================== - # Power transported to the first wall by escaped alpha particles - physics_variables.p_fw_alpha_mw = physics_variables.p_alpha_total_mw * ( - 1.0e0 - physics_variables.f_p_alpha_plasma_deposited - ) + # Sum of Zi.ni for all impurity ions (those with charge > helium) + znimp = 0.0 + for imp in range(impurity_radiation_module.N_IMPURITIES): + if impurity_radiation_module.impurity_arr_z[imp] > 2: + znimp += impurity_radiation.zav_of_te( + imp, np.array([physics_variables.temp_plasma_electron_vol_avg_kev]) + ).squeeze() * ( + impurity_radiation_module.f_nd_impurity_electron_array[imp] + * physics_variables.nd_plasma_electrons_vol_avg + ) - # Density limit - ( - physics_variables.nd_plasma_electron_max_array, - physics_variables.nd_plasma_electrons_max, - ) = self.calculate_density_limit( - physics_variables.b_plasma_toroidal_on_axis, - physics_variables.i_density_limit, - physics_variables.p_plasma_separatrix_mw, - current_drive_variables.p_hcd_injected_total_mw, - physics_variables.plasma_current, - divertor_variables.prn1, - physics_variables.qstar, - physics_variables.q95, - physics_variables.rmajor, - physics_variables.rminor, - physics_variables.a_plasma_surface, - physics_variables.n_charge_plasma_effective_vol_avg, - ) + # ====================================================================== - # Calculate transport losses and energy confinement time using the - # chosen scaling law - ( - physics_variables.pden_electron_transport_loss_mw, - physics_variables.pden_ion_transport_loss_mw, - physics_variables.t_electron_energy_confinement, - physics_variables.t_energy_confinement, - physics_variables.t_ion_energy_confinement, - physics_variables.p_plasma_loss_mw, - ) = self.calculate_confinement_time( - physics_variables.m_fuel_amu, - physics_variables.p_alpha_total_mw, - physics_variables.aspect, - physics_variables.b_plasma_toroidal_on_axis, - physics_variables.nd_plasma_ions_total_vol_avg, - physics_variables.nd_plasma_electrons_vol_avg, - physics_variables.nd_plasma_electron_line, - physics_variables.eps, - physics_variables.hfact, - physics_variables.i_confinement_time, - physics_variables.i_plasma_ignited, - physics_variables.kappa, - physics_variables.kappa95, - physics_variables.p_non_alpha_charged_mw, - current_drive_variables.p_hcd_injected_total_mw, - physics_variables.plasma_current, - physics_variables.pden_plasma_core_rad_mw, - physics_variables.rmajor, - physics_variables.rminor, - physics_variables.temp_plasma_electron_density_weighted_kev, - physics_variables.temp_plasma_ion_density_weighted_kev, - physics_variables.q95, - physics_variables.qstar, - physics_variables.vol_plasma, - physics_variables.n_charge_plasma_effective_vol_avg, + # Fuel portion - conserve charge neutrality + # znfuel is the sum of Zi.ni for the three fuel ions + znfuel = ( + physics_variables.nd_plasma_electrons_vol_avg + - 2.0 * physics_variables.nd_plasma_alphas_vol_avg + - physics_variables.nd_plasma_protons_vol_avg + - physics_variables.nd_beam_ions + - znimp ) - # Total transport power from scaling law (MW) - physics_variables.p_electron_transport_loss_mw = ( - physics_variables.pden_electron_transport_loss_mw - * physics_variables.vol_plasma - ) - physics_variables.p_ion_transport_loss_mw = ( - physics_variables.pden_ion_transport_loss_mw * physics_variables.vol_plasma + # ====================================================================== + + # Fuel ion density, nd_plasma_fuel_ions_vol_avg + # For D-T-He3 mix, nd_plasma_fuel_ions_vol_avg = nD + nT + nHe3, while znfuel = nD + nT + 2*nHe3 + # So nd_plasma_fuel_ions_vol_avg = znfuel - nHe3 = znfuel - f_plasma_fuel_helium3*nd_plasma_fuel_ions_vol_avg + physics_variables.nd_plasma_fuel_ions_vol_avg = znfuel / ( + 1.0 + physics_variables.f_plasma_fuel_helium3 ) - # Calculate Volt-second requirements - ( - physics_variables.vs_plasma_internal, - physics_variables.ind_plasma, - physics_variables.vs_plasma_burn_required, - physics_variables.vs_plasma_ramp_required, - physics_variables.vs_plasma_ind_ramp, - physics_variables.vs_plasma_res_ramp, - physics_variables.vs_plasma_total_required, - physics_variables.v_plasma_loop_burn, - ) = self.inductance.calculate_volt_second_requirements( - physics_variables.csawth, - physics_variables.eps, - physics_variables.f_c_plasma_inductive, - physics_variables.ejima_coeff, - physics_variables.kappa, - physics_variables.rmajor, - physics_variables.res_plasma, - physics_variables.plasma_current, - times_variables.t_plant_pulse_fusion_ramp, - times_variables.t_plant_pulse_burn, - physics_variables.ind_plasma_internal_norm, - ) + # ====================================================================== - physics_variables.e_plasma_magnetic_stored = ( - 0.5e0 * physics_variables.ind_plasma * physics_variables.plasma_current**2 + # Set hydrogen and helium relative impurity densities for + # radiation calculations + impurity_radiation_module.f_nd_impurity_electron_array[ + impurity_radiation.element2index("H_") + ] = ( + physics_variables.nd_plasma_protons_vol_avg + + ( + physics_variables.f_plasma_fuel_deuterium + + physics_variables.f_plasma_fuel_tritium + ) + * physics_variables.nd_plasma_fuel_ions_vol_avg + + physics_variables.nd_beam_ions + ) / physics_variables.nd_plasma_electrons_vol_avg + + impurity_radiation_module.f_nd_impurity_electron_array[ + impurity_radiation.element2index("He") + ] = ( + physics_variables.f_plasma_fuel_helium3 + * physics_variables.nd_plasma_fuel_ions_vol_avg + / physics_variables.nd_plasma_electrons_vol_avg + + physics_variables.f_nd_alpha_electron ) - # Calculate auxiliary physics related information - sbar = 1.0e0 - ( - physics_variables.burnup, - physics_variables.ntau, - physics_variables.nTtau, - physics_variables.figmer, - physics_variables.fusrat, - physics_variables.molflow_plasma_fuelling_required, - physics_variables.rndfuel, - physics_variables.t_alpha_confinement, - physics_variables.f_alpha_energy_confinement, - ) = self.phyaux( - physics_variables.aspect, - physics_variables.nd_plasma_electrons_vol_avg, - physics_variables.temp_plasma_electron_vol_avg_kev, - physics_variables.nd_plasma_fuel_ions_vol_avg, - physics_variables.fusden_total, - physics_variables.fusden_alpha_total, - physics_variables.plasma_current, - sbar, - physics_variables.nd_plasma_alphas_vol_avg, - physics_variables.t_energy_confinement, - physics_variables.vol_plasma, + # ====================================================================== + + # Total impurity density + physics_variables.nd_plasma_impurities_vol_avg = 0.0 + for imp in range(impurity_radiation_module.N_IMPURITIES): + if impurity_radiation_module.impurity_arr_z[imp] > 2: + physics_variables.nd_plasma_impurities_vol_avg += ( + impurity_radiation_module.f_nd_impurity_electron_array[imp] + * physics_variables.nd_plasma_electrons_vol_avg + ) + + # ====================================================================== + + # Total ion density + physics_variables.nd_plasma_ions_total_vol_avg = ( + physics_variables.nd_plasma_fuel_ions_vol_avg + + physics_variables.nd_plasma_alphas_vol_avg + + physics_variables.nd_plasma_protons_vol_avg + + physics_variables.nd_beam_ions + + physics_variables.nd_plasma_impurities_vol_avg ) - # Total transport power from scaling law (MW) - physics_variables.pscalingmw = ( - physics_variables.p_electron_transport_loss_mw - + physics_variables.p_ion_transport_loss_mw + # ====================================================================== + + # Set some relative impurity density variables + # for the benefit of other routines + physics_variables.f_nd_plasma_carbon_electron = ( + impurity_radiation_module.f_nd_impurity_electron_array[ + impurity_radiation.element2index("C_") + ] + ) + physics_variables.f_nd_plasma_oxygen_electron = ( + impurity_radiation_module.f_nd_impurity_electron_array[ + impurity_radiation.element2index("O_") + ] + ) + physics_variables.f_nd_plasma_iron_argon_electron = ( + impurity_radiation_module.f_nd_impurity_electron_array[ + impurity_radiation.element2index("Fe") + ] + + impurity_radiation_module.f_nd_impurity_electron_array[ + impurity_radiation.element2index("Ar") + ] ) - # ============================================================ + # ====================================================================== - # MDK - # Nominal mean photon wall load on entire first wall area including divertor and beam holes - # Note that 'a_fw_total' excludes these, so they have been added back in. - if physics_variables.i_pflux_fw_neutron == 1: - physics_variables.pflux_fw_rad_mw = ( - physics_variables.ffwal - * physics_variables.p_plasma_rad_mw - / physics_variables.a_plasma_surface + # Effective charge + # Calculation should be sum(ni.Zi^2) / sum(ni.Zi), + # but ne = sum(ni.Zi) through quasineutrality + physics_variables.n_charge_plasma_effective_vol_avg = 0.0 + for imp in range(impurity_radiation_module.N_IMPURITIES): + physics_variables.n_charge_plasma_effective_vol_avg += ( + impurity_radiation_module.f_nd_impurity_electron_array[imp] + * impurity_radiation.zav_of_te( + imp, np.array([physics_variables.temp_plasma_electron_vol_avg_kev]) + ).squeeze() + ** 2 + ) + + # ====================================================================== + + # Fraction of alpha energy to ions and electrons + # From Max Fenstermacher + # (used with electron and ion power balance equations only) + # No consideration of pden_non_alpha_charged_mw here... + + # f_temp_plasma_electron_density_vol_avg now calculated in plasma_profiles, after the very first + # call of plasma_composition; use old parabolic profile estimate + # in this case + if physics_variables.first_call == 1: + pc = ( + (1.0 + physics_variables.alphan) + * (1.0 + physics_variables.alphat) + / (1.0 + physics_variables.alphan + physics_variables.alphat) ) + physics_variables.first_call = 0 else: - if divertor_variables.n_divertors == 2: - # Double Null configuration in - including SoL radiation - physics_variables.pflux_fw_rad_mw = ( - ( - 1.0e0 - - fwbs_variables.f_a_fw_outboard_hcd - - 2.0e0 * fwbs_variables.f_ster_div_single - ) - * physics_variables.p_plasma_rad_mw - / build_variables.a_fw_total - + ( - 1.0e0 - - fwbs_variables.f_a_fw_outboard_hcd - - 2.0e0 * fwbs_variables.f_ster_div_single - ) - * physics_variables.rad_fraction_sol - * physics_variables.p_plasma_separatrix_mw - / (build_variables.a_fw_total) - ) - else: - # Single null configuration - including SoL radaition - physics_variables.pflux_fw_rad_mw = ( - ( - 1.0e0 - - fwbs_variables.f_a_fw_outboard_hcd - - fwbs_variables.f_ster_div_single - ) - * physics_variables.p_plasma_rad_mw - / build_variables.a_fw_total - + ( - 1.0e0 - - fwbs_variables.f_a_fw_outboard_hcd - - fwbs_variables.f_ster_div_single - ) - * physics_variables.rad_fraction_sol - * physics_variables.p_plasma_separatrix_mw - / build_variables.a_fw_total - ) + pc = physics_variables.f_temp_plasma_electron_density_vol_avg - constraint_variables.pflux_fw_rad_max_mw = ( - physics_variables.pflux_fw_rad_mw * constraint_variables.f_fw_rad_max + physics_variables.f_alpha_electron = 0.88155 * np.exp( + -physics_variables.temp_plasma_electron_vol_avg_kev * pc / 67.4036 ) + physics_variables.f_alpha_ion = 1.0 - physics_variables.f_alpha_electron - # Calculate the target imbalances - # find the total power into the targets - physics_variables.ptarmw = physics_variables.p_plasma_separatrix_mw * ( - 1.0e0 - physics_variables.rad_fraction_sol + # ====================================================================== + + # Average atomic masses of injected fuel species + physics_variables.m_fuel_amu = ( + (constants.M_DEUTERON_AMU * physics_variables.f_plasma_fuel_deuterium) + + (constants.M_TRITON_AMU * physics_variables.f_plasma_fuel_tritium) + + (constants.M_HELION_AMU * physics_variables.f_plasma_fuel_helium3) ) - # use physics_variables.f_p_div_lower to find deltarsep - # Parameters taken from double null machine - # D. Brunner et al - physics_variables.lambdaio = 1.57e-3 - # Issue #1559 Infinities in physics_module.drsep when running single null in a double null machine - # C W Ashe - if physics_variables.f_p_div_lower < 4.5e-5: - physics_variables.drsep = 1.5e-2 - elif physics_variables.f_p_div_lower > (1.0e0 - 4.5e-5): - physics_variables.drsep = -1.5e-2 - else: - physics_variables.drsep = ( - -2.0e0 - * 1.5e-3 - * math.atanh(2.0e0 * (physics_variables.f_p_div_lower - 0.5e0)) - ) - # Model Taken from D3-D paper for conventional divertor - # Journal of Nuclear Materials - # Volumes 290-293, March 2001, Pages 935-939 - # Find the innner and outer lower target imbalance + # ====================================================================== - physics_variables.fio = 0.16e0 + (0.16e0 - 0.41e0) * ( - 1.0e0 - - ( - 2.0e0 - / ( - 1.0e0 - + np.exp( - -((physics_variables.drsep / physics_variables.lambdaio) ** 2) - ) - ) + # Average atomic masses of injected fuel species in the neutral beams + # Only deuterium and tritium in the beams + physics_variables.m_beam_amu = ( + constants.M_DEUTERON_AMU * (1.0 - current_drive_variables.f_beam_tritium) + ) + (constants.M_TRITON_AMU * current_drive_variables.f_beam_tritium) + + # ====================================================================== + + # Average mass of all ions + physics_variables.m_ions_total_amu = ( + ( + physics_variables.m_fuel_amu + * physics_variables.nd_plasma_fuel_ions_vol_avg ) + + (constants.M_ALPHA_AMU * physics_variables.nd_plasma_alphas_vol_avg) + + (physics_variables.nd_plasma_protons_vol_avg * constants.M_PROTON_AMU) + + (physics_variables.m_beam_amu * physics_variables.nd_beam_ions) ) - if divertor_variables.n_divertors == 2: - # Double Null configuration - # Find all the power fractions accross the targets - # Taken from D3-D conventional divertor design - physics_variables.fli = ( - physics_variables.f_p_div_lower * physics_variables.fio - ) - physics_variables.flo = physics_variables.f_p_div_lower * ( - 1.0e0 - physics_variables.fio - ) - physics_variables.fui = ( - 1.0e0 - physics_variables.f_p_div_lower - ) * physics_variables.fio - physics_variables.fuo = (1.0e0 - physics_variables.f_p_div_lower) * ( - 1.0e0 - physics_variables.fio - ) - # power into each target - physics_variables.plimw = physics_variables.fli * physics_variables.ptarmw - physics_variables.plomw = physics_variables.flo * physics_variables.ptarmw - physics_variables.puimw = physics_variables.fui * physics_variables.ptarmw - physics_variables.puomw = physics_variables.fuo * physics_variables.ptarmw - else: - # Single null configuration - physics_variables.fli = physics_variables.fio - physics_variables.flo = 1.0e0 - physics_variables.fio - # power into each target - physics_variables.plimw = physics_variables.fli * physics_variables.ptarmw - physics_variables.plomw = physics_variables.flo * physics_variables.ptarmw + for imp in range(impurity_radiation_module.N_IMPURITIES): + if impurity_radiation_module.impurity_arr_z[imp] > 2: + physics_variables.m_ions_total_amu += ( + physics_variables.nd_plasma_electrons_vol_avg + * impurity_radiation_module.f_nd_impurity_electron_array[imp] + * impurity_radiation_module.m_impurity_amu_array[imp] + ) - # Calculate some derived quantities that may not have been defined earlier - physics_variables.p_plasma_heating_total_mw = 1e6 * ( - physics_variables.f_p_alpha_plasma_deposited - * physics_variables.p_alpha_total_mw - + physics_variables.p_non_alpha_charged_mw - + physics_variables.p_plasma_ohmic_mw - + current_drive_variables.p_hcd_injected_total_mw - ) - physics_variables.f_p_plasma_separatrix_rad = ( - 1.0e6 - * physics_variables.p_plasma_rad_mw - / physics_variables.p_plasma_heating_total_mw - ) - physics_variables.rad_fraction_total = ( - physics_variables.f_p_plasma_separatrix_rad - + (1.0e0 - physics_variables.f_p_plasma_separatrix_rad) - * physics_variables.rad_fraction_sol - ) - physics_variables.pradsolmw = ( - physics_variables.rad_fraction_sol * physics_variables.p_plasma_separatrix_mw + physics_variables.m_ions_total_amu = ( + physics_variables.m_ions_total_amu + / physics_variables.nd_plasma_ions_total_vol_avg ) - if 78 in numerics.icc: - po.write( - self.outfile, - ( - f"reinke t and fz, physics = {physics_variables.temp_plasma_separatrix_kev} , {reinke_variables.fzmin}" - ), + # ====================================================================== + + # Mass weighted plasma effective charge + # Sum of (Zi^2*n_i) / m_i + physics_variables.n_charge_plasma_effective_mass_weighted_vol_avg = ( + ( + physics_variables.f_plasma_fuel_deuterium + * physics_variables.nd_plasma_fuel_ions_vol_avg + / constants.M_DEUTERON_AMU ) - fgw = ( - physics_variables.nd_plasma_electron_max_array[6] - / physics_variables.nd_plasma_electrons_vol_avg + + ( + physics_variables.f_plasma_fuel_tritium + * physics_variables.nd_plasma_fuel_ions_vol_avg + / constants.M_TRITON_AMU ) - # calculate separatrix temperature, if Reinke criterion is used - physics_variables.temp_plasma_separatrix_kev = reinke_tsep( - physics_variables.b_plasma_toroidal_on_axis, - physics_variables.p_plasma_separatrix_mw - / physics_variables.p_l_h_threshold_mw, - physics_variables.q95, - physics_variables.rmajor, - physics_variables.eps, - fgw, - physics_variables.kappa, - reinke_variables.lhat, + + ( + 4.0 + * physics_variables.f_plasma_fuel_helium3 + * physics_variables.nd_plasma_fuel_ions_vol_avg + / constants.M_HELION_AMU ) - - if reinke_variables.fzmin >= 1.0e0: - logger.error( - "REINKE IMPURITY MODEL: fzmin is greater than or equal to 1.0, this is at least notable" + + (4.0 * physics_variables.nd_plasma_alphas_vol_avg / constants.M_ALPHA_AMU) + + (physics_variables.nd_plasma_protons_vol_avg / constants.M_PROTON_AMU) + + ( + (1.0 - current_drive_variables.f_beam_tritium) + * physics_variables.nd_beam_ions + / constants.M_DEUTERON_AMU + ) + + ( + current_drive_variables.f_beam_tritium + * physics_variables.nd_beam_ions + / constants.M_TRITON_AMU + ) + ) / physics_variables.nd_plasma_electrons_vol_avg + for imp in range(impurity_radiation_module.N_IMPURITIES): + if impurity_radiation_module.impurity_arr_z[imp] > 2: + physics_variables.n_charge_plasma_effective_mass_weighted_vol_avg += ( + impurity_radiation_module.f_nd_impurity_electron_array[imp] + * impurity_radiation.zav_of_te( + imp, + np.array([physics_variables.temp_plasma_electron_vol_avg_kev]), + ).squeeze() + ** 2 + / impurity_radiation_module.m_impurity_amu_array[imp] ) - po.write( - self.outfile, - ( - f" 'fzactual, frac, reinke_variables.impvardiv = {reinke_variables.fzactual}," - f" {impurity_radiation_module.f_nd_impurity_electron_array(reinke_variables.impvardiv)}," - f" {reinke_variables.impvardiv}" - ), - ) + # ====================================================================== @staticmethod - def calculate_current_profile_index_wesson(qstar: float, q0: float) -> float: - """Calculate the Wesson current profile index. + def phyaux( + aspect: float, + nd_plasma_electrons_vol_avg: float, + te: float, + nd_plasma_fuel_ions_vol_avg: float, + fusden_total: float, + fusden_alpha_total: float, + plasma_current: float, + sbar: float, + nd_plasma_alphas_vol_avg: float, + t_energy_confinement: float, + vol_plasma: float, + ) -> tuple[float, float, float, float, float, float, float, float]: + """Auxiliary physics quantities Parameters ---------- - qstar : float - Cylindrical safety factor. - q0 : float - Safety factor on axis. + aspect : float + Plasma aspect ratio. + nd_plasma_electrons_vol_avg : float + Electron density (/m3). + te : float + Volume avergaed electron temperature (keV). + nd_plasma_fuel_ions_vol_avg : float + Fuel ion density (/m3). + fusden_total : float + Fusion reaction rate from plasma and beams (/m3/s). + fusden_alpha_total : float + Alpha particle production rate (/m3/s). + plasma_current : float + Plasma current (A). + sbar : float + Exponent for aspect ratio (normally 1). + nd_plasma_alphas_vol_avg : float + Alpha ash density (/m3). + t_energy_confinement : float + Global energy confinement time (s). + vol_plasma : float + Plasma volume (m3). Returns ------- - float - The Wesson current profile index. + tuple + A tuple containing: + - burnup (float): Fractional plasma burnup. + - ntau (float): Plasma average n-tau (s/m3). + - nTtau (float): Plasma triple product nT-tau (s/m3). + - figmer (float): Physics figure of merit. + - fusrat (float): Number of fusion reactions per second. + - molflow_plasma_fuelling_required (float): Fuelling rate for D-T (nucleus-pairs/sec). + - rndfuel (float): Fuel burnup rate (reactions/s). + - t_alpha_confinement (float): Alpha particle confinement time (s). + - f_alpha_energy_confinement (float): Fraction of alpha energy confinement. + This subroutine calculates extra physics related items needed by other parts of the code. - Notes - ----- - - It is recommended to use this method with the other Wesson relations for normalised beta and - normalised internal inductance. - - This relation is only true for the cyclindrical plasma approximation. + """ + figmer = 1e-6 * plasma_current * aspect**sbar - References - ---------- - - Wesson, J. (2011) Tokamaks. 4th Edition, 2011 Oxford Science Publications, - International Series of Monographs on Physics, Volume 149. + ntau = t_energy_confinement * nd_plasma_electrons_vol_avg + nTtau = ntau * te - """ - return qstar / q0 - 1.0 + # Fusion reactions per second + fusrat = fusden_total * vol_plasma + + # Alpha particle confinement time (s) + # Number of alphas / alpha production rate + if fusden_alpha_total != 0.0: + t_alpha_confinement = nd_plasma_alphas_vol_avg / fusden_alpha_total + else: # only likely if DD is only active fusion reaction + t_alpha_confinement = 0.0 + + # Fractional burnup + # (Consider detailed model in: G. L. Jackson, V. S. Chan, R. D. Stambaugh, + # Fusion Science and Technology, vol.64, no.1, July 2013, pp.8-12) + # The ratio of ash to fuel particle confinement times is given by + # tauratio + # Possible logic... + # burnup = fuel ion-pairs burned/m3 / initial fuel ion-pairs/m3; + # fuel ion-pairs burned/m3 = alpha particles/m3 (for both D-T and D-He3 reactions) + # initial fuel ion-pairs/m3 = burnt fuel ion-pairs/m3 + unburnt fuel-ion pairs/m3 + # Remember that unburnt fuel-ion pairs/m3 = 0.5 * unburnt fuel-ions/m3 + if physics_variables.burnup_in <= 1.0e-9: + burnup = ( + nd_plasma_alphas_vol_avg + / (nd_plasma_alphas_vol_avg + 0.5 * nd_plasma_fuel_ions_vol_avg) + / physics_variables.tauratio + ) + else: + burnup = physics_variables.burnup_in + + # Fuel burnup rate (reactions/second) (previously Amps) + rndfuel = fusrat + + # Required fuelling rate (fuel ion pairs/second) (previously Amps) + molflow_plasma_fuelling_required = rndfuel / burnup + + f_alpha_energy_confinement = t_alpha_confinement / t_energy_confinement + + return ( + burnup, + ntau, + nTtau, + figmer, + fusrat, + molflow_plasma_fuelling_required, + rndfuel, + t_alpha_confinement, + f_alpha_energy_confinement, + ) @staticmethod - def calculate_density_limit( - b_plasma_toroidal_on_axis: float, - i_density_limit: int, - p_plasma_separatrix_mw: float, - p_hcd_injected_total_mw: float, + def plasma_ohmic_heating( + f_c_plasma_inductive: float, + kappa95: float, plasma_current: float, - prn1: float, - qcyl: float, - q95: float, rmajor: float, rminor: float, - a_plasma_surface: float, + temp_plasma_electron_density_weighted_kev: float, + vol_plasma: float, zeff: float, - ) -> tuple[np.ndarray, float]: - """Calculate the density limit using various models. + ) -> tuple[float, float, float, float]: + """Calculate the ohmic heating power and related parameters. Parameters ---------- - b_plasma_toroidal_on_axis : float - Toroidal field on axis (T). - i_density_limit : int - Switch denoting which formula to enforce. - p_plasma_separatrix_mw : float - Power flowing to the edge plasma via charged particles (MW). - p_hcd_injected_total_mw : float - Power injected into the plasma (MW). + f_c_plasma_inductive : float + Fraction of plasma current driven inductively. + kappa95 : float + Plasma elongation at 95% surface. plasma_current : float Plasma current (A). - prn1 : float - Edge density / average plasma density. - qcyl : float - Equivalent cylindrical safety factor (qstar). - q95 : float - Safety factor at 95% surface. rmajor : float - Plasma major radius (m). + Major radius (m). rminor : float - Plasma minor radius (m). - a_plasma_surface : float - Plasma surface area (m^2). + Minor radius (m). + temp_plasma_electron_density_weighted_kev : float + Density weighted average electron temperature (keV). + vol_plasma : float + Plasma volume (m^3). zeff : float Plasma effective charge. Returns ------- - Tuple[np.ndarray, float] - A tuple containing: - - nd_plasma_electron_max_array (np.ndarray): Average plasma density limit using seven different models (m^-3). - - nd_plasma_electrons_max (float): Enforced average plasma density limit (m^-3). + Tuple[float, float, float, float] + Tuple containing: + - pden_plasma_ohmic_mw (float): Ohmic heating power per unit volume (MW/m^3). + - p_plasma_ohmic_mw (float): Total ohmic heating power (MW). + - f_res_plasma_neo (float): Neo-classical resistivity enhancement factor. + - res_plasma (float): Plasma resistance (ohm). + + References + ---------- + - ITER Physics Design Guidelines + 1989 [IPDG89], N. A. Uckan et al, + + """ + # Density weighted electron temperature in 10 keV units + t10 = temp_plasma_electron_density_weighted_kev / 10.0 + + # Plasma resistance, from loop voltage calculation in ITER Physics Design Guidelines: 1989 + res_plasma = ( + physics_variables.plasma_res_factor + * 2.15e-9 + * zeff + * rmajor + / (kappa95 * rminor**2 * t10**1.5) + ) + + # Neo-classical resistivity enhancement factor + # Taken from ITER Physics Design Guidelines: 1989 + # The expression is valid for aspect ratios in the range 2.5 to 4.0 + + f_res_plasma_neo = ( + 1.0 if 2.5 >= rmajor / rminor <= 4.0 else 4.3 - 0.6 * rmajor / rminor + ) + + res_plasma = res_plasma * f_res_plasma_neo + + # Check to see if plasma resistance is negative + # (possible if aspect ratio is too high) + if res_plasma <= 0.0: + logger.error( + f"Negative plasma resistance res_plasma. {res_plasma=} {physics_variables.aspect=}" + ) + + # Ohmic heating power per unit volume + # Corrected from: pden_plasma_ohmic_mw = (f_c_plasma_inductive*plasma_current)**2 * ... + + pden_plasma_ohmic_mw = ( + f_c_plasma_inductive * plasma_current**2 * res_plasma * 1.0e-6 / vol_plasma + ) + + # Total ohmic heating power + p_plasma_ohmic_mw = pden_plasma_ohmic_mw * vol_plasma + + return pden_plasma_ohmic_mw, p_plasma_ohmic_mw, f_res_plasma_neo, res_plasma + + @staticmethod + def calculate_plasma_current( + alphaj: float, + alphap: float, + b_plasma_toroidal_on_axis: float, + eps: float, + i_plasma_current: int, + kappa: float, + kappa95: float, + pres_plasma_on_axis: float, + len_plasma_poloidal: float, + q95: float, + rmajor: float, + rminor: float, + triang: float, + triang95: float, + ) -> tuple[float, float, float, float, float]: + """Calculate the plasma current. + + This routine calculates the plasma current based on the edge safety factor q95. + It will also make the current profile parameters consistent with the q-profile if required. + + Parameters + ---------- + alphaj : float + Current profile index. + alphap : float + Pressure profile index. + b_plasma_toroidal_on_axis : float + Toroidal field on axis (T). + eps : float + Inverse aspect ratio. + i_plasma_current : int + Current scaling model to use. + 1 = Peng analytic fit + 2 = Peng divertor scaling (TART,STAR) + 3 = Simple ITER scaling + 4 = IPDG89 scaling + 5 = Todd empirical scaling I + 6 = Todd empirical scaling II + 7 = Connor-Hastie model + 8 = Sauter scaling (allowing negative triangularity) + 9 = FIESTA ST scaling + kappa : float + Plasma elongation. + kappa95 : float + Plasma elongation at 95% surface. + pres_plasma_on_axis : float + Central plasma pressure (Pa). + len_plasma_poloidal : float + Plasma perimeter length (m). + q95 : float + Plasma safety factor at 95% flux (= q-bar for i_plasma_current=2). + ind_plasma_internal_norm : float + Plasma normalised internal inductance. + rmajor : float + Major radius (m). + rminor : float + Minor radius (m). + triang : float + Plasma triangularity. + triang95 : float + Plasma triangularity at 95% surface. + + Returns + ------- + Tuple[float, float, float,] + Tuple containing b_plasma_poloidal_average, qstar, plasma_current, Raises ------ ValueError - If i_density_limit is not between 1 and 7. - Notes - ----- + If invalid value for i_plasma_current is provided. ValueError - If i_density_limit is not between 1 and 7. - Notes - ----- - This routine calculates several different formulae for the density limit and enforces the one chosen by the user. + If invalid value for i_plasma_current is provided. ValueError - If i_density_limit is not between 1 and 7. - Notes - ----- - This routine calculates several different formulae for the density limit and enforces the one chosen by the user. - For i_density_limit = 1-5, 8, we scale the sepatrix density limit output by the ratio of the separatrix to volume averaged density + If invalid value for i_plasma_current is provided. References ---------- - - AEA FUS 172 - Physics Assessment for the European Reactor Study - - N.A. Uckan and ITER Physics Group, 'ITER Physics Design Guidelines - 1989 - - N.A. Uckan and ITER Physics Group, 'ITER Physics Design Guidelines - 1989 - - M. Bernert et al., “The H-mode density limit in the full tungsten ASDEX Upgrade tokamak,” - vol. 57, no. 1, pp. 014038-014038, Nov. 2014, doi: https://doi.org/10.1088/0741-3335/57/1/014038. ‌ + - J D Galambos, STAR Code + Spherical Tokamak Analysis and Reactor Code, unpublished internal Oak Ridge document + - J D Galambos, STAR Code + Spherical Tokamak Analysis and Reactor Code, unpublished internal Oak Ridge document + - Peng, Y. K. M., Galambos, J. D., & Shipe, P. C. (1992). + 'Small Tokamaks for Fusion Technology Testing'. Fusion Technology, 21(3P2A), + 1729-1738. https://doi.org/10.13182/FST92-A29971 + - ITER Physics Design Guidelines + 1989 [IPDG89], N. A. Uckan et al, ITER Documentation Series No.10, IAEA/ITER/DS/10, IAEA, Vienna, 1990 + - M. Kovari et al, 2014, "PROCESS" + A systems code for fusion power plants - Part 1: Physics + - M. Kovari et al, 2014, "PROCESS" + A systems code for fusion power plants - Part 1: Physics + - H. Zohm et al, 2013, On the Physics Guidelines for a Tokamak DEMO + - M. Kovari et al, 2014, "PROCESS" + A systems code for fusion power plants - Part 1: Physics + - H. Zohm et al, 2013, On the Physics Guidelines for a Tokamak DEMO + - T. Hartmann, 2013, Development of a modular systems code to analyse the implications of physics assumptions on the design of a demonstration fusion power plant + - M. Kovari et al, 2014, "PROCESS" + A systems code for fusion power plants - Part 1: Physics + - H. Zohm et al, 2013, On the Physics Guidelines for a Tokamak DEMO + - T. Hartmann, 2013, Development of a modular systems code to analyse the implications of physics assumptions on the design of a demonstration fusion power plant + - Sauter, Geometric formulas for systems codes..., FED 2016 """ + # Aspect ratio + aspect_ratio = 1.0 / eps - if i_density_limit < 1 or i_density_limit > 7: + # Only the Sauter scaling (i_plasma_current=8) is suitable for negative triangularity: + if i_plasma_current != 8 and triang < 0.0: raise ProcessValueError( - "Illegal value for i_density_limit", i_density_limit=i_density_limit + f"Triangularity is negative without i_plasma_current = 8 selected: {triang=}, {i_plasma_current=}" ) - nd_plasma_electron_max_array = np.empty((8,)) + # Calculate the function Fq that scales the edge q from the + # circular cross-section cylindrical case - # Power per unit area crossing the plasma edge - # (excludes radiation and neutrons) + # Peng analytical fit + if i_plasma_current == 1: + fq = calculate_current_coefficient_peng(eps, len_plasma_poloidal, rminor) - p_perp = p_plasma_separatrix_mw / a_plasma_surface + # Peng scaling for double null divertor; TARTs [STAR Code] + elif i_plasma_current == 2: + plasma_current = 1.0e6 * calculate_plasma_current_peng( + q95, aspect_ratio, eps, rminor, b_plasma_toroidal_on_axis, kappa, triang + ) - # Old ASDEX density limit formula - # This applies to the density at the plasma edge, so must be scaled - # to give the density limit applying to the average plasma density. + # Simple ITER scaling (simply the cylindrical case) + elif i_plasma_current == 3: + fq = 1.0 - nd_plasma_electron_max_array[0] = ( - 1.54e20 - * p_perp**0.43 - * b_plasma_toroidal_on_axis**0.31 - / (q95 * rmajor) ** 0.45 - ) / prn1 + # ITER formula (IPDG89) + elif i_plasma_current == 4: + fq = calculate_current_coefficient_ipdg89(eps, kappa95, triang95) - # Borrass density limit model for ITER (I) - # This applies to the density at the plasma edge, so must be scaled - # to give the density limit applying to the average plasma density. - # Borrass et al, ITER-TN-PH-9-6 (1989) + # Todd empirical scalings + # D.C.Robinson and T.N.Todd, Plasma and Contr Fusion 28 (1986) 1181 + elif i_plasma_current in [5, 6]: + fq = calculate_current_coefficient_todd(eps, kappa95, triang95, model=1) - nd_plasma_electron_max_array[1] = ( - 1.8e20 - * p_perp**0.53 - * b_plasma_toroidal_on_axis**0.31 - / (q95 * rmajor) ** 0.22 - ) / prn1 + if i_plasma_current == 6: + fq = calculate_current_coefficient_todd(eps, kappa95, triang95, model=2) - # Borrass density limit model for ITER (II) - # This applies to the density at the plasma edge, so must be scaled - # to give the density limit applying to the average plasma density. - # This formula is (almost) identical to that in the original routine - # denlim (now deleted). - - nd_plasma_electron_max_array[2] = ( - 0.5e20 - * p_perp**0.57 - * b_plasma_toroidal_on_axis**0.31 - / (q95 * rmajor) ** 0.09 - ) / prn1 - - # JET edge radiation density limit model - # This applies to the density at the plasma edge, so must be scaled - # to give the density limit applying to the average plasma density. - # qcyl=qstar here, but literature is not clear. + # Connor-Hastie asymptotically-correct expression + elif i_plasma_current == 7: + fq = calculate_current_coefficient_hastie( + alphaj, + alphap, + b_plasma_toroidal_on_axis, + triang95, + eps, + kappa95, + pres_plasma_on_axis, + constants.RMU0, + ) - denom = (zeff - 1.0) * (1.0 - 4.0 / (3.0 * qcyl)) - if denom <= 0.0: - if i_density_limit == 4: - logger.error( - f"qcyl < 4/3; nd_plasma_electron_max_array(4) set to zero; model 5 will be enforced instead. {denom=} {qcyl=}" - ) - i_density_limit = 5 + # Sauter scaling allowing negative triangularity [FED May 2016] + # https://doi.org/10.1016/j.fusengdes.2016.04.033. + elif i_plasma_current == 8: + # Assumes zero squareness, note takes kappa, delta at separatrix not _95 + fq = calculate_current_coefficient_sauter(eps, kappa, triang) - nd_plasma_electron_max_array[3] = 0.0 + # FIESTA ST scaling + # https://doi.org/10.1016/j.fusengdes.2020.111530. + elif i_plasma_current == 9: + fq = calculate_current_coefficient_fiesta(eps, kappa, triang) else: - nd_plasma_electron_max_array[3] = ( - 1.0e20 * np.sqrt(p_hcd_injected_total_mw / denom) - ) / prn1 - - # JET simplified density limit model - # This applies to the density at the plasma edge, so must be scaled - # to give the density limit applying to the average plasma density. - - nd_plasma_electron_max_array[4] = ( - 0.237e20 - * b_plasma_toroidal_on_axis - * np.sqrt(p_plasma_separatrix_mw) - / rmajor - ) / prn1 + raise ProcessValueError(f"Invalid value {i_plasma_current=}") - # Hugill-Murakami M.q limit - # qcyl=qstar here, which is okay according to the literature + # Main plasma current calculation using the fq value from the different settings + if i_plasma_current != 2: + plasma_current = ( + (2.0 * np.pi / constants.RMU0) + * rminor**2 + / (rmajor * q95) + * fq + * b_plasma_toroidal_on_axis + ) + # i_plasma_current == 2 case covered above - nd_plasma_electron_max_array[5] = ( - 3.0e20 * b_plasma_toroidal_on_axis / (rmajor * qcyl) + # Calculate cyclindrical safety factor from IPDG89 + qstar = ( + 5.0e6 + * rminor**2 + / (rmajor * plasma_current / b_plasma_toroidal_on_axis) + * 0.5 + * (1.0 + kappa95**2 * (1.0 + 2.0 * triang95**2 - 1.2 * triang95**3)) ) - # Greenwald limit - - nd_plasma_electron_max_array[6] = ( - 1.0e14 * plasma_current / (np.pi * rminor * rminor) + # Calculate the poloidal field generated by the plasma current + b_plasma_poloidal_average = calculate_poloidal_field( + i_plasma_current, + plasma_current, + q95, + aspect_ratio, + eps, + b_plasma_toroidal_on_axis, + kappa, + triang, + len_plasma_poloidal, + constants.RMU0, ) - nd_plasma_electron_max_array[7] = ( - 1.0e20 - * 0.506 - * (p_hcd_injected_total_mw**0.396 * (plasma_current / 1.0e6) ** 0.265) - / (q95**0.323) - ) / prn1 + return b_plasma_poloidal_average, qstar, plasma_current - # Enforce the chosen density limit + def outtim(self): + po.oheadr(self.outfile, "Times") - return nd_plasma_electron_max_array, nd_plasma_electron_max_array[ - i_density_limit - 1 - ] + po.ovarrf( + self.outfile, + "Initial charge time for CS from zero current (s)", + "(t_plant_pulse_coil_precharge)", + times_variables.t_plant_pulse_coil_precharge, + ) + po.ovarrf( + self.outfile, + "Plasma current ramp-up time (s)", + "(t_plant_pulse_plasma_current_ramp_up)", + times_variables.t_plant_pulse_plasma_current_ramp_up, + ) + po.ovarrf( + self.outfile, + "Heating time (s)", + "(t_plant_pulse_fusion_ramp)", + times_variables.t_plant_pulse_fusion_ramp, + ) + po.ovarre( + self.outfile, + "Burn time (s)", + "(t_plant_pulse_burn)", + times_variables.t_plant_pulse_burn, + "OP ", + ) + po.ovarrf( + self.outfile, + "Reset time to zero current for CS (s)", + "(t_plant_pulse_plasma_current_ramp_down)", + times_variables.t_plant_pulse_plasma_current_ramp_down, + ) + po.ovarrf( + self.outfile, + "Time between pulses (s)", + "(t_plant_pulse_dwell)", + times_variables.t_plant_pulse_dwell, + ) + po.oblnkl(self.outfile) + po.ovarre( + self.outfile, + "Total plant cycle time (s)", + "(t_plant_pulse_total)", + times_variables.t_plant_pulse_total, + "OP ", + ) - @staticmethod - def plasma_composition(): - """Calculates various plasma component fractional makeups. + def calculate_effective_charge_ionisation_profiles(self): + """Calculate the effective charge profiles for ionisation calculations.""" - This subroutine determines the various plasma component fractional makeups. - It is the replacement for the original routine betcom(), and is used in conjunction - with the new impurity radiation model. + # Calculate the effective charge (zeff) profile across the plasma + # Returns an array of zeff at each radial point + zeff_profile = np.zeros_like(self.plasma_profile.teprofile.profile_y) + for i in range(len(zeff_profile)): + zeff_profile[i] = 0.0 + for imp in range(impurity_radiation_module.N_IMPURITIES): + zeff_profile[i] += ( + impurity_radiation_module.f_nd_impurity_electron_array[imp] + * impurity_radiation.zav_of_te( + imp, np.array([self.plasma_profile.teprofile.profile_y[i]]) + ).squeeze() + ** 2 + ) + physics_variables.n_charge_plasma_effective_profile = zeff_profile - This function performs the following calculations: - - Determines the alpha ash portion. - - Calculates the proton density. - - Calculates the beam hot ion component. - - Sums the ion densities for all impurity ions with charge greater than helium. - - Ensures charge neutrality by adjusting the fuel portion. - - Calculates the total ion density. - - Sets the relative impurity densities for radiation calculations. - - Calculates the effective charge. - - Defines the Coulomb logarithm for ion-electron and electron-electron collisions. - - Calculates the fraction of alpha energy to ions and electrons. - - Calculates the average atomic masses of injected fuel species and neutral beams. - - Calculates the density weighted mass and mass weighted plasma effective charge. + # Assign the charge profiles of each species + n_impurities = impurity_radiation_module.N_IMPURITIES + te_profile = self.plasma_profile.teprofile.profile_y + n_points = len(te_profile) + # Create a 2D array: (n_impurities, n_points) + charge_profiles = np.zeros((n_impurities, n_points)) + for imp in range(n_impurities): + for i in range(n_points): + charge_profiles[imp, i] = impurity_radiation.zav_of_te( + imp, np.array([te_profile[i]]) + ).squeeze() + impurity_radiation_module.n_charge_impurity_profile = charge_profiles + + def outplas(self): + """Subroutine to output the plasma physics information + self.outfile : input integer : Fortran output unit identifier + This routine writes the plasma physics information + to a file, in a tidy format. """ + # Dimensionless plasma parameters. See reference below. + physics_variables.nu_star = ( + 1 + / constants.RMU0 + * (15.0e0 * constants.ELECTRON_CHARGE**4 * physics_variables.dlamie) + / (4.0e0 * np.pi**1.5e0 * constants.EPSILON0**2) + * physics_variables.vol_plasma**2 + * physics_variables.rmajor**2 + * physics_variables.b_plasma_toroidal_on_axis + * np.sqrt(physics_variables.eps) + * physics_variables.nd_plasma_electron_line**3 + * physics_variables.kappa + / (physics_variables.e_plasma_beta**2 * physics_variables.plasma_current) + ) - # Alpha ash portion - physics_variables.nd_plasma_alphas_vol_avg = ( - physics_variables.nd_plasma_electrons_vol_avg - * physics_variables.f_nd_alpha_electron + physics_variables.rho_star = np.sqrt( + 2.0e0 + * constants.PROTON_MASS + * physics_variables.m_ions_total_amu + * physics_variables.e_plasma_beta + / ( + 3.0e0 + * physics_variables.vol_plasma + * physics_variables.nd_plasma_electron_line + ) + ) / ( + constants.ELECTRON_CHARGE + * physics_variables.b_plasma_toroidal_on_axis + * physics_variables.eps + * physics_variables.rmajor ) - # ====================================================================== + physics_variables.beta_mcdonald = ( + 4.0e0 + / 3.0e0 + * constants.RMU0 + * physics_variables.e_plasma_beta + / ( + physics_variables.vol_plasma + * physics_variables.b_plasma_toroidal_on_axis**2 + ) + ) - # Protons - # This calculation will be wrong on the first call as the particle - # production rates are evaluated later in the calling sequence - # Issue #557 Allow f_nd_protium_electrons impurity to be specified: 'f_nd_protium_electrons' - # This will override the calculated value which is a minimum. - if physics_variables.fusden_alpha_total < 1.0e-6: # not calculated yet... - physics_variables.nd_plasma_protons_vol_avg = max( - physics_variables.f_nd_protium_electrons - * physics_variables.nd_plasma_electrons_vol_avg, - physics_variables.nd_plasma_alphas_vol_avg - * (physics_variables.f_plasma_fuel_helium3 + 1.0e-3), - ) # rough estimate - else: - physics_variables.nd_plasma_protons_vol_avg = max( - physics_variables.f_nd_protium_electrons - * physics_variables.nd_plasma_electrons_vol_avg, - physics_variables.nd_plasma_alphas_vol_avg - * physics_variables.proton_rate_density - / physics_variables.fusden_alpha_total, - ) - - # ====================================================================== + po.oheadr(self.outfile, "Plasma") - # Beam hot ion component - # If ignited, prevent beam fusion effects - if physics_variables.i_plasma_ignited == 0: - physics_variables.nd_beam_ions = ( - physics_variables.nd_plasma_electrons_vol_avg - * physics_variables.f_nd_beam_electron - ) + if stellarator_variables.istell == 0: + if divertor_variables.n_divertors == 0: + po.ocmmnt(self.outfile, "Plasma configuration = limiter") + elif divertor_variables.n_divertors == 1: + po.ocmmnt(self.outfile, "Plasma configuration = single null divertor") + elif divertor_variables.n_divertors == 2: + po.ocmmnt(self.outfile, "Plasma configuration = double null divertor") + else: + raise ProcessValueError( + "Illegal value of n_divertors", + n_divertors=divertor_variables.n_divertors, + ) else: - physics_variables.nd_beam_ions = 0.0 - - # ====================================================================== + po.ocmmnt(self.outfile, "Plasma configuration = stellarator") - # Sum of Zi.ni for all impurity ions (those with charge > helium) - znimp = 0.0 - for imp in range(impurity_radiation_module.N_IMPURITIES): - if impurity_radiation_module.impurity_arr_z[imp] > 2: - znimp += impurity_radiation.zav_of_te( - imp, np.array([physics_variables.temp_plasma_electron_vol_avg_kev]) - ).squeeze() * ( - impurity_radiation_module.f_nd_impurity_electron_array[imp] - * physics_variables.nd_plasma_electrons_vol_avg + if stellarator_variables.istell == 0: + if physics_variables.itart == 0: + physics_variables.itart_r = physics_variables.itart + po.ovarin( + self.outfile, + "Tokamak aspect ratio = Conventional, itart = 0", + "(itart)", + physics_variables.itart_r, + ) + elif physics_variables.itart == 1: + physics_variables.itart_r = physics_variables.itart + po.ovarin( + self.outfile, + "Tokamak aspect ratio = Spherical, itart = 1", + "(itart)", + physics_variables.itart_r, ) - # ====================================================================== - - # Fuel portion - conserve charge neutrality - # znfuel is the sum of Zi.ni for the three fuel ions - znfuel = ( - physics_variables.nd_plasma_electrons_vol_avg - - 2.0 * physics_variables.nd_plasma_alphas_vol_avg - - physics_variables.nd_plasma_protons_vol_avg - - physics_variables.nd_beam_ions - - znimp + po.osubhd(self.outfile, "Plasma Geometry :") + po.ovarin( + self.outfile, + "Plasma shaping model", + "(i_plasma_shape)", + physics_variables.i_plasma_shape, ) - - # ====================================================================== - - # Fuel ion density, nd_plasma_fuel_ions_vol_avg - # For D-T-He3 mix, nd_plasma_fuel_ions_vol_avg = nD + nT + nHe3, while znfuel = nD + nT + 2*nHe3 - # So nd_plasma_fuel_ions_vol_avg = znfuel - nHe3 = znfuel - f_plasma_fuel_helium3*nd_plasma_fuel_ions_vol_avg - physics_variables.nd_plasma_fuel_ions_vol_avg = znfuel / ( - 1.0 + physics_variables.f_plasma_fuel_helium3 + if physics_variables.i_plasma_shape == 0: + po.osubhd(self.outfile, "Classic PROCESS plasma shape model is used :") + elif physics_variables.i_plasma_shape == 1: + po.osubhd(self.outfile, "Sauter plasma shape model is used :") + po.ovarrf(self.outfile, "Major radius (m)", "(rmajor)", physics_variables.rmajor) + po.ovarrf( + self.outfile, + "Minor radius (m)", + "(rminor)", + physics_variables.rminor, + "OP ", ) - - # ====================================================================== - - # Set hydrogen and helium relative impurity densities for - # radiation calculations - impurity_radiation_module.f_nd_impurity_electron_array[ - impurity_radiation.element2index("H_") - ] = ( - physics_variables.nd_plasma_protons_vol_avg - + ( - physics_variables.f_plasma_fuel_deuterium - + physics_variables.f_plasma_fuel_tritium - ) - * physics_variables.nd_plasma_fuel_ions_vol_avg - + physics_variables.nd_beam_ions - ) / physics_variables.nd_plasma_electrons_vol_avg - - impurity_radiation_module.f_nd_impurity_electron_array[ - impurity_radiation.element2index("He") - ] = ( - physics_variables.f_plasma_fuel_helium3 - * physics_variables.nd_plasma_fuel_ions_vol_avg - / physics_variables.nd_plasma_electrons_vol_avg - + physics_variables.f_nd_alpha_electron + po.ovarrf(self.outfile, "Aspect ratio", "(aspect)", physics_variables.aspect) + po.ovarrf( + self.outfile, + "Plasma squareness", + "(plasma_square)", + physics_variables.plasma_square, + "IP", ) + if stellarator_variables.istell == 0: + if physics_variables.i_plasma_geometry in [0, 6, 8]: + po.ovarrf( + self.outfile, + "Elongation, X-point (input value used)", + "(kappa)", + physics_variables.kappa, + "IP ", + ) + elif physics_variables.i_plasma_geometry == 1: + po.ovarrf( + self.outfile, + "Elongation, X-point (TART scaling)", + "(kappa)", + physics_variables.kappa, + "OP ", + ) + elif physics_variables.i_plasma_geometry in [2, 3]: + po.ovarrf( + self.outfile, + "Elongation, X-point (Zohm scaling)", + "(kappa)", + physics_variables.kappa, + "OP ", + ) + po.ovarrf( + self.outfile, + "Zohm scaling adjustment factor", + "(fkzohm)", + physics_variables.fkzohm, + ) + elif physics_variables.i_plasma_geometry in [4, 5, 7]: + po.ovarrf( + self.outfile, + "Elongation, X-point (calculated from kappa95)", + "(kappa)", + physics_variables.kappa, + "OP ", + ) + elif physics_variables.i_plasma_geometry == 9: + po.ovarrf( + self.outfile, + "Elongation, X-point (calculated from aspect ratio and li(3))", + "(kappa)", + physics_variables.kappa, + "OP ", + ) + elif physics_variables.i_plasma_geometry == 10: + po.ovarrf( + self.outfile, + "Elongation, X-point (calculated from aspect ratio and stability margin)", + "(kappa)", + physics_variables.kappa, + "OP ", + ) + elif physics_variables.i_plasma_geometry == 11: + po.ovarrf( + self.outfile, + "Elongation, X-point (calculated from aspect ratio via Menard 2016)", + "(kappa)", + physics_variables.kappa, + "OP ", + ) + else: + raise ProcessValueError( + "Illegal value of i_plasma_geometry", + i_plasma_geometry=physics_variables.i_plasma_geometry, + ) - # ====================================================================== - - # Total impurity density - physics_variables.nd_plasma_impurities_vol_avg = 0.0 - for imp in range(impurity_radiation_module.N_IMPURITIES): - if impurity_radiation_module.impurity_arr_z[imp] > 2: - physics_variables.nd_plasma_impurities_vol_avg += ( - impurity_radiation_module.f_nd_impurity_electron_array[imp] - * physics_variables.nd_plasma_electrons_vol_avg + if physics_variables.i_plasma_geometry in [4, 5, 7]: + po.ovarrf( + self.outfile, + "Elongation, 95% surface (input value used)", + "(kappa95)", + physics_variables.kappa95, + "IP ", + ) + else: + po.ovarrf( + self.outfile, + "Elongation, 95% surface (calculated from kappa)", + "(kappa95)", + physics_variables.kappa95, + "OP ", ) - # ====================================================================== + if physics_variables.i_plasma_geometry in [0, 2, 6, 8, 9, 10, 11]: + po.ovarrf( + self.outfile, + "Triangularity, X-point (input value used)", + "(triang)", + physics_variables.triang, + "IP ", + ) + elif physics_variables.i_plasma_geometry == 1: + po.ovarrf( + self.outfile, + "Triangularity, X-point (TART scaling)", + "(triang)", + physics_variables.triang, + "OP ", + ) + else: + po.ovarrf( + self.outfile, + "Triangularity, X-point (calculated from triang95)", + "(triang)", + physics_variables.triang, + "OP ", + ) - # Total ion density - physics_variables.nd_plasma_ions_total_vol_avg = ( - physics_variables.nd_plasma_fuel_ions_vol_avg - + physics_variables.nd_plasma_alphas_vol_avg - + physics_variables.nd_plasma_protons_vol_avg - + physics_variables.nd_beam_ions - + physics_variables.nd_plasma_impurities_vol_avg - ) - - # ====================================================================== - - # Set some relative impurity density variables - # for the benefit of other routines - physics_variables.f_nd_plasma_carbon_electron = ( - impurity_radiation_module.f_nd_impurity_electron_array[ - impurity_radiation.element2index("C_") - ] - ) - physics_variables.f_nd_plasma_oxygen_electron = ( - impurity_radiation_module.f_nd_impurity_electron_array[ - impurity_radiation.element2index("O_") - ] - ) - physics_variables.f_nd_plasma_iron_argon_electron = ( - impurity_radiation_module.f_nd_impurity_electron_array[ - impurity_radiation.element2index("Fe") - ] - + impurity_radiation_module.f_nd_impurity_electron_array[ - impurity_radiation.element2index("Ar") - ] - ) - - # ====================================================================== + if physics_variables.i_plasma_geometry in [3, 4, 5, 7]: + po.ovarrf( + self.outfile, + "Triangularity, 95% surface (input value used)", + "(triang95)", + physics_variables.triang95, + "IP ", + ) + else: + po.ovarrf( + self.outfile, + "Triangularity, 95% surface (calculated from triang)", + "(triang95)", + physics_variables.triang95, + "OP ", + ) - # Effective charge - # Calculation should be sum(ni.Zi^2) / sum(ni.Zi), - # but ne = sum(ni.Zi) through quasineutrality - physics_variables.n_charge_plasma_effective_vol_avg = 0.0 - for imp in range(impurity_radiation_module.N_IMPURITIES): - physics_variables.n_charge_plasma_effective_vol_avg += ( - impurity_radiation_module.f_nd_impurity_electron_array[imp] - * impurity_radiation.zav_of_te( - imp, np.array([physics_variables.temp_plasma_electron_vol_avg_kev]) - ).squeeze() - ** 2 + po.ovarrf( + self.outfile, + "Plasma poloidal perimeter (m)", + "(len_plasma_poloidal)", + physics_variables.len_plasma_poloidal, + "OP ", ) - # ====================================================================== + po.ovarre( + self.outfile, + "Plasma cross-sectional area (m2)", + "(a_plasma_poloidal)", + physics_variables.a_plasma_poloidal, + "OP ", + ) + po.ovarre( + self.outfile, + "Plasma surface area (m2)", + "(a_plasma_surface)", + physics_variables.a_plasma_surface, + "OP ", + ) + po.ovarre( + self.outfile, + "Plasma volume (m3)", + "(vol_plasma)", + physics_variables.vol_plasma, + "OP ", + ) - # Fraction of alpha energy to ions and electrons - # From Max Fenstermacher - # (used with electron and ion power balance equations only) - # No consideration of pden_non_alpha_charged_mw here... + po.osubhd(self.outfile, "Current and Field :") - # f_temp_plasma_electron_density_vol_avg now calculated in plasma_profiles, after the very first - # call of plasma_composition; use old parabolic profile estimate - # in this case - if physics_variables.first_call == 1: - pc = ( - (1.0 + physics_variables.alphan) - * (1.0 + physics_variables.alphat) - / (1.0 + physics_variables.alphan + physics_variables.alphat) - ) - physics_variables.first_call = 0 - else: - pc = physics_variables.f_temp_plasma_electron_density_vol_avg + if stellarator_variables.istell == 0: + po.oblnkl(self.outfile) + po.ovarin( + self.outfile, + "Plasma current scaling law used", + "(i_plasma_current)", + physics_variables.i_plasma_current, + ) - physics_variables.f_alpha_electron = 0.88155 * np.exp( - -physics_variables.temp_plasma_electron_vol_avg_kev * pc / 67.4036 - ) - physics_variables.f_alpha_ion = 1.0 - physics_variables.f_alpha_electron + po.ovarrf( + self.outfile, + "Plasma current (MA)", + "(plasma_current_MA)", + physics_variables.plasma_current / 1.0e6, + "OP ", + ) - # ====================================================================== + if physics_variables.i_alphaj == 1: + po.ovarrf( + self.outfile, + "Current density profile factor", + "(alphaj)", + physics_variables.alphaj, + "OP ", + ) + else: + po.ovarrf( + self.outfile, + "Current density profile factor", + "(alphaj)", + physics_variables.alphaj, + ) + po.ocmmnt(self.outfile, "Current profile index scalings:") + po.oblnkl(self.outfile) - # Average atomic masses of injected fuel species - physics_variables.m_fuel_amu = ( - (constants.M_DEUTERON_AMU * physics_variables.f_plasma_fuel_deuterium) - + (constants.M_TRITON_AMU * physics_variables.f_plasma_fuel_tritium) - + (constants.M_HELION_AMU * physics_variables.f_plasma_fuel_helium3) - ) + po.ovarrf( + self.outfile, + "J. Wesson plasma current profile index", + "(alphaj_wesson)", + physics_variables.alphaj_wesson, + "OP ", + ) + po.oblnkl(self.outfile) + po.ovarrf( + self.outfile, + "On-axis plasma current density (A/m2)", + "(j_plasma_on_axis)", + physics_variables.j_plasma_on_axis, + "OP ", + ) + po.ovarrf( + self.outfile, + "Vertical field at plasma (T)", + "(b_plasma_vertical_required)", + physics_variables.b_plasma_vertical_required, + "OP ", + ) - # ====================================================================== + po.ovarrf( + self.outfile, + "Vacuum toroidal field at R (T)", + "(b_plasma_toroidal_on_axis)", + physics_variables.b_plasma_toroidal_on_axis, + ) + po.ovarrf( + self.outfile, + "Toroidal field at plasma inboard (T)", + "(b_plasma_inboard_toroidal)", + physics_variables.b_plasma_inboard_toroidal, + ) + po.ovarrf( + self.outfile, + "Toroidal field at plasma outboard (T)", + "(b_plasma_outboard_toroidal)", + physics_variables.b_plasma_outboard_toroidal, + ) - # Average atomic masses of injected fuel species in the neutral beams - # Only deuterium and tritium in the beams - physics_variables.m_beam_amu = ( - constants.M_DEUTERON_AMU * (1.0 - current_drive_variables.f_beam_tritium) - ) + (constants.M_TRITON_AMU * current_drive_variables.f_beam_tritium) + for i in range(len(physics_variables.b_plasma_toroidal_profile)): + po.ovarre( + self.mfile, + f"Toroidal field in plasma at point {i}", + f"b_plasma_toroidal_profile{i}", + physics_variables.b_plasma_toroidal_profile[i], + ) + po.ovarrf( + self.outfile, + "Average poloidal field (T)", + "(b_plasma_poloidal_average)", + physics_variables.b_plasma_poloidal_average, + "OP ", + ) - # ====================================================================== + po.ovarrf( + self.outfile, + "Total field (sqrt(b_plasma_poloidal_average^2 + b_plasma_toroidal_on_axis^2)) (T)", + "(b_plasma_total)", + physics_variables.b_plasma_total, + "OP ", + ) - # Average mass of all ions - physics_variables.m_ions_total_amu = ( - ( - physics_variables.m_fuel_amu - * physics_variables.nd_plasma_fuel_ions_vol_avg + if stellarator_variables.istell == 0: + po.ovarrf( + self.outfile, "Safety factor on axis", "(q0)", physics_variables.q0 ) - + (constants.M_ALPHA_AMU * physics_variables.nd_plasma_alphas_vol_avg) - + (physics_variables.nd_plasma_protons_vol_avg * constants.M_PROTON_AMU) - + (physics_variables.m_beam_amu * physics_variables.nd_beam_ions) - ) - for imp in range(impurity_radiation_module.N_IMPURITIES): - if impurity_radiation_module.impurity_arr_z[imp] > 2: - physics_variables.m_ions_total_amu += ( - physics_variables.nd_plasma_electrons_vol_avg - * impurity_radiation_module.f_nd_impurity_electron_array[imp] - * impurity_radiation_module.m_impurity_amu_array[imp] + + if physics_variables.i_plasma_current == 2: + po.ovarrf( + self.outfile, + "Mean edge safety factor", + "(q95)", + physics_variables.q95, ) - physics_variables.m_ions_total_amu = ( - physics_variables.m_ions_total_amu - / physics_variables.nd_plasma_ions_total_vol_avg - ) + po.ovarrf( + self.outfile, + "Safety factor at 95% flux surface", + "(q95)", + physics_variables.q95, + ) - # ====================================================================== + po.ovarrf( + self.outfile, + "Cylindrical safety factor (qcyl)", + "(qstar)", + physics_variables.qstar, + "OP ", + ) - # Mass weighted plasma effective charge - # Sum of (Zi^2*n_i) / m_i - physics_variables.n_charge_plasma_effective_mass_weighted_vol_avg = ( - ( - physics_variables.f_plasma_fuel_deuterium - * physics_variables.nd_plasma_fuel_ions_vol_avg - / constants.M_DEUTERON_AMU - ) - + ( - physics_variables.f_plasma_fuel_tritium - * physics_variables.nd_plasma_fuel_ions_vol_avg - / constants.M_TRITON_AMU + if physics_variables.i_plasma_geometry == 1: + po.ovarrf( + self.outfile, + "Lower limit for edge safety factor q95", + "(q95_min)", + physics_variables.q95_min, + "OP ", + ) + po.ovarrf( + self.outfile, + "Plasma normalised internal inductance", + "(ind_plasma_internal_norm)", + physics_variables.ind_plasma_internal_norm, + "OP ", ) - + ( - 4.0 - * physics_variables.f_plasma_fuel_helium3 - * physics_variables.nd_plasma_fuel_ions_vol_avg - / constants.M_HELION_AMU + po.oblnkl(self.outfile) + po.ocmmnt(self.outfile, "Plasma normalised internal inductance scalings:") + po.oblnkl(self.outfile) + po.ovarrf( + self.outfile, + "J. Wesson plasma normalised internal inductance", + "(ind_plasma_internal_norm_wesson)", + physics_variables.ind_plasma_internal_norm_wesson, + "OP ", ) - + (4.0 * physics_variables.nd_plasma_alphas_vol_avg / constants.M_ALPHA_AMU) - + (physics_variables.nd_plasma_protons_vol_avg / constants.M_PROTON_AMU) - + ( - (1.0 - current_drive_variables.f_beam_tritium) - * physics_variables.nd_beam_ions - / constants.M_DEUTERON_AMU + po.ovarrf( + self.outfile, + "J. Menard plasma normalised internal inductance", + "(ind_plasma_internal_norm_menard)", + physics_variables.ind_plasma_internal_norm_menard, + "OP ", ) - + ( - current_drive_variables.f_beam_tritium - * physics_variables.nd_beam_ions - / constants.M_TRITON_AMU + po.ovarrf( + self.outfile, + "ITER li(3) plasma normalised internal inductance", + "(ind_plasma_internal_norm_iter_3)", + physics_variables.ind_plasma_internal_norm_iter_3, + "OP ", ) - ) / physics_variables.nd_plasma_electrons_vol_avg - for imp in range(impurity_radiation_module.N_IMPURITIES): - if impurity_radiation_module.impurity_arr_z[imp] > 2: - physics_variables.n_charge_plasma_effective_mass_weighted_vol_avg += ( - impurity_radiation_module.f_nd_impurity_electron_array[imp] - * impurity_radiation.zav_of_te( - imp, - np.array([physics_variables.temp_plasma_electron_vol_avg_kev]), - ).squeeze() - ** 2 - / impurity_radiation_module.m_impurity_amu_array[imp] - ) - - # ====================================================================== - - @staticmethod - def phyaux( - aspect: float, - nd_plasma_electrons_vol_avg: float, - te: float, - nd_plasma_fuel_ions_vol_avg: float, - fusden_total: float, - fusden_alpha_total: float, - plasma_current: float, - sbar: float, - nd_plasma_alphas_vol_avg: float, - t_energy_confinement: float, - vol_plasma: float, - ) -> tuple[float, float, float, float, float, float, float, float]: - """Auxiliary physics quantities - - Parameters - ---------- - aspect : float - Plasma aspect ratio. - nd_plasma_electrons_vol_avg : float - Electron density (/m3). - te : float - Volume avergaed electron temperature (keV). - nd_plasma_fuel_ions_vol_avg : float - Fuel ion density (/m3). - fusden_total : float - Fusion reaction rate from plasma and beams (/m3/s). - fusden_alpha_total : float - Alpha particle production rate (/m3/s). - plasma_current : float - Plasma current (A). - sbar : float - Exponent for aspect ratio (normally 1). - nd_plasma_alphas_vol_avg : float - Alpha ash density (/m3). - t_energy_confinement : float - Global energy confinement time (s). - vol_plasma : float - Plasma volume (m3). - - Returns - ------- - tuple - A tuple containing: - - burnup (float): Fractional plasma burnup. - - ntau (float): Plasma average n-tau (s/m3). - - nTtau (float): Plasma triple product nT-tau (s/m3). - - figmer (float): Physics figure of merit. - - fusrat (float): Number of fusion reactions per second. - - molflow_plasma_fuelling_required (float): Fuelling rate for D-T (nucleus-pairs/sec). - - rndfuel (float): Fuel burnup rate (reactions/s). - - t_alpha_confinement (float): Alpha particle confinement time (s). - - f_alpha_energy_confinement (float): Fraction of alpha energy confinement. - This subroutine calculates extra physics related items needed by other parts of the code. - - """ - figmer = 1e-6 * plasma_current * aspect**sbar - - ntau = t_energy_confinement * nd_plasma_electrons_vol_avg - nTtau = ntau * te - - # Fusion reactions per second - fusrat = fusden_total * vol_plasma - - # Alpha particle confinement time (s) - # Number of alphas / alpha production rate - if fusden_alpha_total != 0.0: - t_alpha_confinement = nd_plasma_alphas_vol_avg / fusden_alpha_total - else: # only likely if DD is only active fusion reaction - t_alpha_confinement = 0.0 - # Fractional burnup - # (Consider detailed model in: G. L. Jackson, V. S. Chan, R. D. Stambaugh, - # Fusion Science and Technology, vol.64, no.1, July 2013, pp.8-12) - # The ratio of ash to fuel particle confinement times is given by - # tauratio - # Possible logic... - # burnup = fuel ion-pairs burned/m3 / initial fuel ion-pairs/m3; - # fuel ion-pairs burned/m3 = alpha particles/m3 (for both D-T and D-He3 reactions) - # initial fuel ion-pairs/m3 = burnt fuel ion-pairs/m3 + unburnt fuel-ion pairs/m3 - # Remember that unburnt fuel-ion pairs/m3 = 0.5 * unburnt fuel-ions/m3 - if physics_variables.burnup_in <= 1.0e-9: - burnup = ( - nd_plasma_alphas_vol_avg - / (nd_plasma_alphas_vol_avg + 0.5 * nd_plasma_fuel_ions_vol_avg) - / physics_variables.tauratio - ) else: - burnup = physics_variables.burnup_in - - # Fuel burnup rate (reactions/second) (previously Amps) - rndfuel = fusrat - - # Required fuelling rate (fuel ion pairs/second) (previously Amps) - molflow_plasma_fuelling_required = rndfuel / burnup - - f_alpha_energy_confinement = t_alpha_confinement / t_energy_confinement - - return ( - burnup, - ntau, - nTtau, - figmer, - fusrat, - molflow_plasma_fuelling_required, - rndfuel, - t_alpha_confinement, - f_alpha_energy_confinement, - ) - - @staticmethod - def plasma_ohmic_heating( - f_c_plasma_inductive: float, - kappa95: float, - plasma_current: float, - rmajor: float, - rminor: float, - temp_plasma_electron_density_weighted_kev: float, - vol_plasma: float, - zeff: float, - ) -> tuple[float, float, float, float]: - """Calculate the ohmic heating power and related parameters. - - Parameters - ---------- - f_c_plasma_inductive : float - Fraction of plasma current driven inductively. - kappa95 : float - Plasma elongation at 95% surface. - plasma_current : float - Plasma current (A). - rmajor : float - Major radius (m). - rminor : float - Minor radius (m). - temp_plasma_electron_density_weighted_kev : float - Density weighted average electron temperature (keV). - vol_plasma : float - Plasma volume (m^3). - zeff : float - Plasma effective charge. - - Returns - ------- - Tuple[float, float, float, float] - Tuple containing: - - pden_plasma_ohmic_mw (float): Ohmic heating power per unit volume (MW/m^3). - - p_plasma_ohmic_mw (float): Total ohmic heating power (MW). - - f_res_plasma_neo (float): Neo-classical resistivity enhancement factor. - - res_plasma (float): Plasma resistance (ohm). - - References - ---------- - - ITER Physics Design Guidelines - 1989 [IPDG89], N. A. Uckan et al, + po.ovarrf( + self.outfile, + "Rotational transform", + "(iotabar)", + stellarator_variables.iotabar, + ) + po.oblnkl(self.outfile) + po.ostars(self.outfile, 110) + po.oblnkl(self.outfile) - """ - # Density weighted electron temperature in 10 keV units - t10 = temp_plasma_electron_density_weighted_kev / 10.0 + # Output beta information + self.beta.output_beta_information() - # Plasma resistance, from loop voltage calculation in ITER Physics Design Guidelines: 1989 - res_plasma = ( - physics_variables.plasma_res_factor - * 2.15e-9 - * zeff - * rmajor - / (kappa95 * rminor**2 * t10**1.5) + po.osubhd(self.outfile, "Temperature and Density (volume averaged) :") + po.ovarrf( + self.outfile, + "Number of radial points in plasma profiles", + "(n_plasma_profile_elements)", + physics_variables.n_plasma_profile_elements, ) - - # Neo-classical resistivity enhancement factor - # Taken from ITER Physics Design Guidelines: 1989 - # The expression is valid for aspect ratios in the range 2.5 to 4.0 - - f_res_plasma_neo = ( - 1.0 if 2.5 >= rmajor / rminor <= 4.0 else 4.3 - 0.6 * rmajor / rminor + po.ovarrf( + self.outfile, + "Volume averaged electron temperature (keV)", + "(temp_plasma_electron_vol_avg_kev)", + physics_variables.temp_plasma_electron_vol_avg_kev, ) - - res_plasma = res_plasma * f_res_plasma_neo - - # Check to see if plasma resistance is negative - # (possible if aspect ratio is too high) - if res_plasma <= 0.0: - logger.error( - f"Negative plasma resistance res_plasma. {res_plasma=} {physics_variables.aspect=}" + po.ovarrf( + self.outfile, + "Ratio of ion to electron volume-averaged temperature", + "(f_temp_plasma_ion_electron)", + physics_variables.f_temp_plasma_ion_electron, + "IP ", + ) + po.ovarrf( + self.outfile, + "Electron temperature on axis (keV)", + "(temp_plasma_electron_on_axis_kev)", + physics_variables.temp_plasma_electron_on_axis_kev, + "OP ", + ) + po.ovarrf( + self.outfile, + "Ion temperature (keV)", + "(temp_plasma_ion_vol_avg_kev)", + physics_variables.temp_plasma_ion_vol_avg_kev, + ) + po.ovarrf( + self.outfile, + "Ion temperature on axis (keV)", + "(temp_plasma_ion_on_axis_kev)", + physics_variables.temp_plasma_ion_on_axis_kev, + "OP ", + ) + po.ovarrf( + self.outfile, + "Electron temp., density weighted (keV)", + "(temp_plasma_electron_density_weighted_kev)", + physics_variables.temp_plasma_electron_density_weighted_kev, + "OP ", + ) + po.ovarrf( + self.outfile, + "Ratio of electron density weighted temp. to volume averaged temp.", + "(f_temp_plasma_electron_density_vol_avg)", + physics_variables.f_temp_plasma_electron_density_vol_avg, + "OP ", + ) + po.ovarre( + self.outfile, + "Volume averaged electron number density (/m3)", + "(nd_plasma_electrons_vol_avg)", + physics_variables.nd_plasma_electrons_vol_avg, + ) + po.ovarre( + self.outfile, + "Electron number density on axis (/m3)", + "(nd_plasma_electron_on_axis)", + physics_variables.nd_plasma_electron_on_axis, + "OP ", + ) + po.ovarre( + self.outfile, + "Line-averaged electron number density (/m3)", + "(nd_plasma_electron_line)", + physics_variables.nd_plasma_electron_line, + "OP ", + ) + po.ovarre( + self.outfile, + "Plasma pressure on axis (Pa)", + "(pres_plasma_thermal_on_axis)", + physics_variables.pres_plasma_thermal_on_axis, + "OP ", + ) + po.ovarre( + self.outfile, + "Volume averaged plasma pressure (Pa)", + "(pres_plasma_thermal_vol_avg)", + physics_variables.pres_plasma_thermal_vol_avg, + "OP ", + ) + # As array output is not currently supported, each element is output as a float instance + # Output plasma pressure profiles to mfile + for i in range(len(physics_variables.pres_plasma_thermal_total_profile)): + po.ovarre( + self.mfile, + f"Total plasma pressure at point {i}", + f"(pres_plasma_thermal_total_profile{i})", + physics_variables.pres_plasma_thermal_total_profile[i], + ) + for i in range(len(physics_variables.pres_plasma_electron_profile)): + po.ovarre( + self.mfile, + f"Total plasma electron pressure at point {i}", + f"(pres_plasma_electron_profile{i})", + physics_variables.pres_plasma_electron_profile[i], + ) + for i in range(len(physics_variables.pres_plasma_ion_total_profile)): + po.ovarre( + self.mfile, + f"Total plasma ion pressure at point {i}", + f"(pres_plasma_ion_total_profile{i})", + physics_variables.pres_plasma_ion_total_profile[i], + ) + for i in range(len(physics_variables.pres_plasma_fuel_profile)): + po.ovarre( + self.mfile, + f"Total plasma fuel pressure at point {i}", + f"(pres_plasma_fuel_profile{i})", + physics_variables.pres_plasma_fuel_profile[i], ) - # Ohmic heating power per unit volume - # Corrected from: pden_plasma_ohmic_mw = (f_c_plasma_inductive*plasma_current)**2 * ... + if stellarator_variables.istell == 0: + po.ovarre( + self.outfile, + "Line-averaged electron density / Greenwald density", + "(dnla_gw)", + physics_variables.nd_plasma_electron_line + / physics_variables.nd_plasma_electron_max_array[6], + "OP ", + ) - pden_plasma_ohmic_mw = ( - f_c_plasma_inductive * plasma_current**2 * res_plasma * 1.0e-6 / vol_plasma + po.ovarre( + self.outfile, + "Total Ion number density (/m3)", + "(nd_plasma_ions_total_vol_avg)", + physics_variables.nd_plasma_ions_total_vol_avg, + "OP ", + ) + po.ovarre( + self.outfile, + "Fuel ion number density (/m3)", + "(nd_plasma_fuel_ions_vol_avg)", + physics_variables.nd_plasma_fuel_ions_vol_avg, + "OP ", + ) + po.ovarre( + self.outfile, + "Total impurity number density with Z > 2 (no He) (/m3)", + "(nd_plasma_impurities_vol_avg)", + physics_variables.nd_plasma_impurities_vol_avg, + "OP ", + ) + po.ovarre( + self.outfile, + "Helium ion number density (thermalised ions only) (/m3)", + "(nd_plasma_alphas_vol_avg)", + physics_variables.nd_plasma_alphas_vol_avg, + "OP ", + ) + po.ovarre( + self.outfile, + "Helium ion density (thermalised ions only) / electron number density", + "(f_nd_alpha_electron)", + physics_variables.f_nd_alpha_electron, + ) + po.ovarre( + self.outfile, + "Plasma volume averaged proton number density (/m3)", + "(nd_plasma_protons_vol_avg)", + physics_variables.nd_plasma_protons_vol_avg, + "OP ", + ) + po.ovarre( + self.outfile, + "Proton number density / electron number density", + "(f_nd_protium_electrons)", + physics_variables.f_nd_protium_electrons, + "OP ", ) - # Total ohmic heating power - p_plasma_ohmic_mw = pden_plasma_ohmic_mw * vol_plasma + po.ovarre( + self.outfile, + "Hot beam ion number density (/m3)", + "(nd_beam_ions)", + physics_variables.nd_beam_ions, + "OP ", + ) + po.ovarre( + self.outfile, + "Hot beam ion number density / electron density", + "(f_nd_beam_electron)", + physics_variables.f_nd_beam_electron, + "OP ", + ) - return pden_plasma_ohmic_mw, p_plasma_ohmic_mw, f_res_plasma_neo, res_plasma + po.oblnkl(self.outfile) + po.oblnkl(self.outfile) + po.ostars(self.outfile, 110) + po.oblnkl(self.outfile) - @staticmethod - def calculate_plasma_current( - alphaj: float, - alphap: float, - b_plasma_toroidal_on_axis: float, - eps: float, - i_plasma_current: int, - kappa: float, - kappa95: float, - pres_plasma_on_axis: float, - len_plasma_poloidal: float, - q95: float, - rmajor: float, - rminor: float, - triang: float, - triang95: float, - ) -> tuple[float, float, float, float, float]: - """Calculate the plasma current. + po.ocmmnt(self.outfile, "Impurities:") + po.oblnkl(self.outfile) + po.ocmmnt(self.outfile, "Plasma ion densities / electron density:") - This routine calculates the plasma current based on the edge safety factor q95. - It will also make the current profile parameters consistent with the q-profile if required. + for imp in range(impurity_radiation_module.N_IMPURITIES): + # MDK Update f_nd_impurity_electrons, as this will make the ITV output work correctly. + impurity_radiation_module.f_nd_impurity_electrons[imp] = ( + impurity_radiation_module.f_nd_impurity_electron_array[imp] + ) + str1 = ( + impurity_radiation_module.impurity_arr_label[imp].item() + + " concentration" + ) + str2 = f"(f_nd_impurity_electrons({imp + 1:02}))" + # MDK Add output flag for H which is calculated. + if imp == 0: + po.ovarre( + self.outfile, + str1, + str2, + impurity_radiation_module.f_nd_impurity_electrons[imp], + "OP ", + ) + else: + po.ovarre( + self.outfile, + str1, + str2, + impurity_radiation_module.f_nd_impurity_electrons[imp], + ) - Parameters - ---------- - alphaj : float - Current profile index. - alphap : float - Pressure profile index. - b_plasma_toroidal_on_axis : float - Toroidal field on axis (T). - eps : float - Inverse aspect ratio. - i_plasma_current : int - Current scaling model to use. - 1 = Peng analytic fit - 2 = Peng divertor scaling (TART,STAR) - 3 = Simple ITER scaling - 4 = IPDG89 scaling - 5 = Todd empirical scaling I - 6 = Todd empirical scaling II - 7 = Connor-Hastie model - 8 = Sauter scaling (allowing negative triangularity) - 9 = FIESTA ST scaling - kappa : float - Plasma elongation. - kappa95 : float - Plasma elongation at 95% surface. - pres_plasma_on_axis : float - Central plasma pressure (Pa). - len_plasma_poloidal : float - Plasma perimeter length (m). - q95 : float - Plasma safety factor at 95% flux (= q-bar for i_plasma_current=2). - ind_plasma_internal_norm : float - Plasma normalised internal inductance. - rmajor : float - Major radius (m). - rminor : float - Minor radius (m). - triang : float - Plasma triangularity. - triang95 : float - Plasma triangularity at 95% surface. - - Returns - ------- - Tuple[float, float, float,] - Tuple containing b_plasma_poloidal_average, qstar, plasma_current, - - Raises - ------ - ValueError - If invalid value for i_plasma_current is provided. - ValueError - If invalid value for i_plasma_current is provided. - ValueError - If invalid value for i_plasma_current is provided. - - References - ---------- - - J D Galambos, STAR Code - Spherical Tokamak Analysis and Reactor Code, unpublished internal Oak Ridge document - - J D Galambos, STAR Code - Spherical Tokamak Analysis and Reactor Code, unpublished internal Oak Ridge document - - Peng, Y. K. M., Galambos, J. D., & Shipe, P. C. (1992). - 'Small Tokamaks for Fusion Technology Testing'. Fusion Technology, 21(3P2A), - 1729-1738. https://doi.org/10.13182/FST92-A29971 - - ITER Physics Design Guidelines - 1989 [IPDG89], N. A. Uckan et al, ITER Documentation Series No.10, IAEA/ITER/DS/10, IAEA, Vienna, 1990 - - M. Kovari et al, 2014, "PROCESS" - A systems code for fusion power plants - Part 1: Physics - - M. Kovari et al, 2014, "PROCESS" - A systems code for fusion power plants - Part 1: Physics - - H. Zohm et al, 2013, On the Physics Guidelines for a Tokamak DEMO - - M. Kovari et al, 2014, "PROCESS" - A systems code for fusion power plants - Part 1: Physics - - H. Zohm et al, 2013, On the Physics Guidelines for a Tokamak DEMO - - T. Hartmann, 2013, Development of a modular systems code to analyse the implications of physics assumptions on the design of a demonstration fusion power plant - - M. Kovari et al, 2014, "PROCESS" - A systems code for fusion power plants - Part 1: Physics - - H. Zohm et al, 2013, On the Physics Guidelines for a Tokamak DEMO - - T. Hartmann, 2013, Development of a modular systems code to analyse the implications of physics assumptions on the design of a demonstration fusion power plant - - Sauter, Geometric formulas for systems codes..., FED 2016 - - """ - # Aspect ratio - aspect_ratio = 1.0 / eps - - # Only the Sauter scaling (i_plasma_current=8) is suitable for negative triangularity: - if i_plasma_current != 8 and triang < 0.0: - raise ProcessValueError( - f"Triangularity is negative without i_plasma_current = 8 selected: {triang=}, {i_plasma_current=}" - ) - - # Calculate the function Fq that scales the edge q from the - # circular cross-section cylindrical case - - # Peng analytical fit - if i_plasma_current == 1: - fq = calculate_current_coefficient_peng(eps, len_plasma_poloidal, rminor) - - # Peng scaling for double null divertor; TARTs [STAR Code] - elif i_plasma_current == 2: - plasma_current = 1.0e6 * calculate_plasma_current_peng( - q95, aspect_ratio, eps, rminor, b_plasma_toroidal_on_axis, kappa, triang - ) - - # Simple ITER scaling (simply the cylindrical case) - elif i_plasma_current == 3: - fq = 1.0 - - # ITER formula (IPDG89) - elif i_plasma_current == 4: - fq = calculate_current_coefficient_ipdg89(eps, kappa95, triang95) - - # Todd empirical scalings - # D.C.Robinson and T.N.Todd, Plasma and Contr Fusion 28 (1986) 1181 - elif i_plasma_current in [5, 6]: - fq = calculate_current_coefficient_todd(eps, kappa95, triang95, model=1) - - if i_plasma_current == 6: - fq = calculate_current_coefficient_todd(eps, kappa95, triang95, model=2) - - # Connor-Hastie asymptotically-correct expression - elif i_plasma_current == 7: - fq = calculate_current_coefficient_hastie( - alphaj, - alphap, - b_plasma_toroidal_on_axis, - triang95, - eps, - kappa95, - pres_plasma_on_axis, - constants.RMU0, - ) - - # Sauter scaling allowing negative triangularity [FED May 2016] - # https://doi.org/10.1016/j.fusengdes.2016.04.033. - elif i_plasma_current == 8: - # Assumes zero squareness, note takes kappa, delta at separatrix not _95 - fq = calculate_current_coefficient_sauter(eps, kappa, triang) - - # FIESTA ST scaling - # https://doi.org/10.1016/j.fusengdes.2020.111530. - elif i_plasma_current == 9: - fq = calculate_current_coefficient_fiesta(eps, kappa, triang) - else: - raise ProcessValueError(f"Invalid value {i_plasma_current=}") - - # Main plasma current calculation using the fq value from the different settings - if i_plasma_current != 2: - plasma_current = ( - (2.0 * np.pi / constants.RMU0) - * rminor**2 - / (rmajor * q95) - * fq - * b_plasma_toroidal_on_axis - ) - # i_plasma_current == 2 case covered above - - # Calculate cyclindrical safety factor from IPDG89 - qstar = ( - 5.0e6 - * rminor**2 - / (rmajor * plasma_current / b_plasma_toroidal_on_axis) - * 0.5 - * (1.0 + kappa95**2 * (1.0 + 2.0 * triang95**2 - 1.2 * triang95**3)) - ) - - # Calculate the poloidal field generated by the plasma current - b_plasma_poloidal_average = calculate_poloidal_field( - i_plasma_current, - plasma_current, - q95, - aspect_ratio, - eps, - b_plasma_toroidal_on_axis, - kappa, - triang, - len_plasma_poloidal, - constants.RMU0, + po.ovarre( + self.outfile, + "Average mass of all ions (amu)", + "(m_ions_total_amu)", + physics_variables.m_ions_total_amu, + "OP ", ) - - return b_plasma_poloidal_average, qstar, plasma_current - - def outtim(self): - po.oheadr(self.outfile, "Times") - - po.ovarrf( + po.ovarre( self.outfile, - "Initial charge time for CS from zero current (s)", - "(t_plant_pulse_coil_precharge)", - times_variables.t_plant_pulse_coil_precharge, + "Total mass of all ions in plasma (kg)", + "(m_plasma_ions_total)", + physics_variables.m_plasma_ions_total, + "OP ", ) - po.ovarrf( + po.ovarre( self.outfile, - "Plasma current ramp-up time (s)", - "(t_plant_pulse_plasma_current_ramp_up)", - times_variables.t_plant_pulse_plasma_current_ramp_up, + "Average mass of all fuel ions (amu)", + "(m_fuel_amu)", + physics_variables.m_fuel_amu, + "OP ", ) - po.ovarrf( + po.ovarre( self.outfile, - "Heating time (s)", - "(t_plant_pulse_fusion_ramp)", - times_variables.t_plant_pulse_fusion_ramp, + "Total mass of all fuel ions in plasma (kg)", + "(m_plasma_fuel_ions)", + physics_variables.m_plasma_fuel_ions, + "OP ", ) po.ovarre( self.outfile, - "Burn time (s)", - "(t_plant_pulse_burn)", - times_variables.t_plant_pulse_burn, + "Average mass of all beam ions (amu)", + "(m_beam_amu)", + physics_variables.m_beam_amu, "OP ", ) - po.ovarrf( + po.ovarre( self.outfile, - "Reset time to zero current for CS (s)", - "(t_plant_pulse_plasma_current_ramp_down)", - times_variables.t_plant_pulse_plasma_current_ramp_down, + "Total mass of all alpha particles in plasma (kg)", + "(m_plasma_alpha)", + physics_variables.m_plasma_alpha, + "OP ", ) - po.ovarrf( + po.ovarre( self.outfile, - "Time between pulses (s)", - "(t_plant_pulse_dwell)", - times_variables.t_plant_pulse_dwell, + "Total mass of all electrons in plasma (kg)", + "(m_plasma_electron)", + physics_variables.m_plasma_electron, + "OP ", ) - po.oblnkl(self.outfile) po.ovarre( self.outfile, - "Total plant cycle time (s)", - "(t_plant_pulse_total)", - times_variables.t_plant_pulse_total, + "Total mass of the plasma (kg)", + "(m_plasma)", + physics_variables.m_plasma, "OP ", ) - def calculate_effective_charge_ionisation_profiles(self): - """Calculate the effective charge profiles for ionisation calculations.""" - - # Calculate the effective charge (zeff) profile across the plasma - # Returns an array of zeff at each radial point - zeff_profile = np.zeros_like(self.plasma_profile.teprofile.profile_y) - for i in range(len(zeff_profile)): - zeff_profile[i] = 0.0 - for imp in range(impurity_radiation_module.N_IMPURITIES): - zeff_profile[i] += ( - impurity_radiation_module.f_nd_impurity_electron_array[imp] - * impurity_radiation.zav_of_te( - imp, np.array([self.plasma_profile.teprofile.profile_y[i]]) - ).squeeze() - ** 2 - ) - physics_variables.n_charge_plasma_effective_profile = zeff_profile - - # Assign the charge profiles of each species - n_impurities = impurity_radiation_module.N_IMPURITIES - te_profile = self.plasma_profile.teprofile.profile_y - n_points = len(te_profile) - # Create a 2D array: (n_impurities, n_points) - charge_profiles = np.zeros((n_impurities, n_points)) - for imp in range(n_impurities): - for i in range(n_points): - charge_profiles[imp, i] = impurity_radiation.zav_of_te( - imp, np.array([te_profile[i]]) - ).squeeze() - impurity_radiation_module.n_charge_impurity_profile = charge_profiles - - def outplas(self): - """Subroutine to output the plasma physics information - self.outfile : input integer : Fortran output unit identifier - This routine writes the plasma physics information - to a file, in a tidy format. - """ - # Dimensionless plasma parameters. See reference below. - physics_variables.nu_star = ( - 1 - / constants.RMU0 - * (15.0e0 * constants.ELECTRON_CHARGE**4 * physics_variables.dlamie) - / (4.0e0 * np.pi**1.5e0 * constants.EPSILON0**2) - * physics_variables.vol_plasma**2 - * physics_variables.rmajor**2 - * physics_variables.b_plasma_toroidal_on_axis - * np.sqrt(physics_variables.eps) - * physics_variables.nd_plasma_electron_line**3 - * physics_variables.kappa - / (physics_variables.e_plasma_beta**2 * physics_variables.plasma_current) - ) - - physics_variables.rho_star = np.sqrt( - 2.0e0 - * constants.PROTON_MASS - * physics_variables.m_ions_total_amu - * physics_variables.e_plasma_beta - / ( - 3.0e0 - * physics_variables.vol_plasma - * physics_variables.nd_plasma_electron_line - ) - ) / ( - constants.ELECTRON_CHARGE - * physics_variables.b_plasma_toroidal_on_axis - * physics_variables.eps - * physics_variables.rmajor + po.oblnkl(self.outfile) + po.ovarrf( + self.outfile, + "Volume averaged plasma effective charge", + "(n_charge_plasma_effective_vol_avg)", + physics_variables.n_charge_plasma_effective_vol_avg, + "OP ", ) - - physics_variables.beta_mcdonald = ( - 4.0e0 - / 3.0e0 - * constants.RMU0 - * physics_variables.e_plasma_beta - / ( - physics_variables.vol_plasma - * physics_variables.b_plasma_toroidal_on_axis**2 + for i in range(len(physics_variables.n_charge_plasma_effective_profile)): + po.ovarre( + self.mfile, + "Effective charge at point", + f"(n_charge_plasma_effective_profile{i})", + physics_variables.n_charge_plasma_effective_profile[i], + "OP ", ) - ) - - po.oheadr(self.outfile, "Plasma") - - if stellarator_variables.istell == 0: - if divertor_variables.n_divertors == 0: - po.ocmmnt(self.outfile, "Plasma configuration = limiter") - elif divertor_variables.n_divertors == 1: - po.ocmmnt(self.outfile, "Plasma configuration = single null divertor") - elif divertor_variables.n_divertors == 2: - po.ocmmnt(self.outfile, "Plasma configuration = double null divertor") - else: - raise ProcessValueError( - "Illegal value of n_divertors", - n_divertors=divertor_variables.n_divertors, - ) - else: - po.ocmmnt(self.outfile, "Plasma configuration = stellarator") - if stellarator_variables.istell == 0: - if physics_variables.itart == 0: - physics_variables.itart_r = physics_variables.itart - po.ovarin( - self.outfile, - "Tokamak aspect ratio = Conventional, itart = 0", - "(itart)", - physics_variables.itart_r, - ) - elif physics_variables.itart == 1: - physics_variables.itart_r = physics_variables.itart - po.ovarin( - self.outfile, - "Tokamak aspect ratio = Spherical, itart = 1", - "(itart)", - physics_variables.itart_r, + for imp in range(impurity_radiation_module.N_IMPURITIES): + for i in range(physics_variables.n_plasma_profile_elements): + po.ovarre( + self.mfile, + "Impurity charge at point", + f"(n_charge_plasma_profile{imp}_{i})", + impurity_radiation_module.n_charge_impurity_profile[imp][i], + "OP ", ) - po.osubhd(self.outfile, "Plasma Geometry :") - po.ovarin( - self.outfile, - "Plasma shaping model", - "(i_plasma_shape)", - physics_variables.i_plasma_shape, - ) - if physics_variables.i_plasma_shape == 0: - po.osubhd(self.outfile, "Classic PROCESS plasma shape model is used :") - elif physics_variables.i_plasma_shape == 1: - po.osubhd(self.outfile, "Sauter plasma shape model is used :") - po.ovarrf(self.outfile, "Major radius (m)", "(rmajor)", physics_variables.rmajor) po.ovarrf( self.outfile, - "Minor radius (m)", - "(rminor)", - physics_variables.rminor, + "Mass-weighted Effective charge", + "(n_charge_plasma_effective_mass_weighted_vol_avg)", + physics_variables.n_charge_plasma_effective_mass_weighted_vol_avg, "OP ", ) - po.ovarrf(self.outfile, "Aspect ratio", "(aspect)", physics_variables.aspect) + po.ovarrf( + self.outfile, "Density profile factor", "(alphan)", physics_variables.alphan + ) + po.ovarin( self.outfile, - "Plasma squareness", - "(plasma_square)", - physics_variables.plasma_square, - "IP", + "Plasma profile model", + "(i_plasma_pedestal)", + physics_variables.i_plasma_pedestal, ) - if stellarator_variables.istell == 0: - if physics_variables.i_plasma_geometry in [0, 6, 8]: - po.ovarrf( - self.outfile, - "Elongation, X-point (input value used)", - "(kappa)", - physics_variables.kappa, - "IP ", - ) - elif physics_variables.i_plasma_geometry == 1: - po.ovarrf( + + if physics_variables.i_plasma_pedestal >= 1: + if ( + physics_variables.nd_plasma_electron_on_axis + < physics_variables.nd_plasma_pedestal_electron + ): + logger.error("Central density is less than pedestal density") + + po.ocmmnt(self.outfile, "Pedestal profiles are used.") + po.ovarrf( + self.outfile, + "Density pedestal r/a location", + "(radius_plasma_pedestal_density_norm)", + physics_variables.radius_plasma_pedestal_density_norm, + ) + if physics_variables.f_nd_plasma_pedestal_greenwald >= 0e0: + po.ovarre( self.outfile, - "Elongation, X-point (TART scaling)", - "(kappa)", - physics_variables.kappa, + "Electron density pedestal height (/m3)", + "(nd_plasma_pedestal_electron)", + physics_variables.nd_plasma_pedestal_electron, "OP ", ) - elif physics_variables.i_plasma_geometry in [2, 3]: - po.ovarrf( + else: + po.ovarre( self.outfile, - "Elongation, X-point (Zohm scaling)", - "(kappa)", - physics_variables.kappa, - "OP ", + "Electron density pedestal height (/m3)", + "(nd_plasma_pedestal_electron)", + physics_variables.nd_plasma_pedestal_electron, ) - po.ovarrf( - self.outfile, - "Zohm scaling adjustment factor", - "(fkzohm)", - physics_variables.fkzohm, + + # must be assigned to their exisiting values# + fgwped_out = ( + physics_variables.nd_plasma_pedestal_electron + / physics_variables.nd_plasma_electron_max_array[6] + ) + fgwsep_out = ( + physics_variables.nd_plasma_separatrix_electron + / physics_variables.nd_plasma_electron_max_array[6] + ) + if physics_variables.f_nd_plasma_pedestal_greenwald >= 0e0: + physics_variables.f_nd_plasma_pedestal_greenwald = ( + physics_variables.nd_plasma_pedestal_electron + / physics_variables.nd_plasma_electron_max_array[6] ) - elif physics_variables.i_plasma_geometry in [4, 5, 7]: + if physics_variables.f_nd_plasma_separatrix_greenwald >= 0e0: + physics_variables.f_nd_plasma_separatrix_greenwald = ( + physics_variables.nd_plasma_separatrix_electron + / physics_variables.nd_plasma_electron_max_array[6] + ) + + po.ovarre( + self.outfile, + "Electron density at pedestal / nGW", + "(fgwped_out)", + fgwped_out, + ) + po.ovarrf( + self.outfile, + "Temperature pedestal r/a location", + "(radius_plasma_pedestal_temp_norm)", + physics_variables.radius_plasma_pedestal_temp_norm, + ) + + po.ovarrf( + self.outfile, + "Electron temp. pedestal height (keV)", + "(temp_plasma_pedestal_kev)", + physics_variables.temp_plasma_pedestal_kev, + ) + if 78 in numerics.icc: po.ovarrf( self.outfile, - "Elongation, X-point (calculated from kappa95)", - "(kappa)", - physics_variables.kappa, + "Electron temp. at separatrix (keV)", + "(temp_plasma_separatrix_kev)", + physics_variables.temp_plasma_separatrix_kev, "OP ", ) - elif physics_variables.i_plasma_geometry == 9: + else: po.ovarrf( self.outfile, - "Elongation, X-point (calculated from aspect ratio and li(3))", - "(kappa)", - physics_variables.kappa, - "OP ", - ) - elif physics_variables.i_plasma_geometry == 10: - po.ovarrf( - self.outfile, - "Elongation, X-point (calculated from aspect ratio and stability margin)", - "(kappa)", - physics_variables.kappa, - "OP ", - ) - elif physics_variables.i_plasma_geometry == 11: - po.ovarrf( - self.outfile, - "Elongation, X-point (calculated from aspect ratio via Menard 2016)", - "(kappa)", - physics_variables.kappa, - "OP ", - ) - else: - raise ProcessValueError( - "Illegal value of i_plasma_geometry", - i_plasma_geometry=physics_variables.i_plasma_geometry, - ) - - if physics_variables.i_plasma_geometry in [4, 5, 7]: - po.ovarrf( - self.outfile, - "Elongation, 95% surface (input value used)", - "(kappa95)", - physics_variables.kappa95, - "IP ", - ) - else: - po.ovarrf( - self.outfile, - "Elongation, 95% surface (calculated from kappa)", - "(kappa95)", - physics_variables.kappa95, - "OP ", - ) - - if physics_variables.i_plasma_geometry in [0, 2, 6, 8, 9, 10, 11]: - po.ovarrf( - self.outfile, - "Triangularity, X-point (input value used)", - "(triang)", - physics_variables.triang, - "IP ", - ) - elif physics_variables.i_plasma_geometry == 1: - po.ovarrf( - self.outfile, - "Triangularity, X-point (TART scaling)", - "(triang)", - physics_variables.triang, - "OP ", - ) - else: - po.ovarrf( - self.outfile, - "Triangularity, X-point (calculated from triang95)", - "(triang)", - physics_variables.triang, - "OP ", - ) - - if physics_variables.i_plasma_geometry in [3, 4, 5, 7]: - po.ovarrf( - self.outfile, - "Triangularity, 95% surface (input value used)", - "(triang95)", - physics_variables.triang95, - "IP ", - ) - else: - po.ovarrf( - self.outfile, - "Triangularity, 95% surface (calculated from triang)", - "(triang95)", - physics_variables.triang95, - "OP ", + "Electron temp. at separatrix (keV)", + "(temp_plasma_separatrix_kev)", + physics_variables.temp_plasma_separatrix_kev, ) - po.ovarrf( + po.ovarre( self.outfile, - "Plasma poloidal perimeter (m)", - "(len_plasma_poloidal)", - physics_variables.len_plasma_poloidal, - "OP ", + "Electron density at separatrix (/m3)", + "(nd_plasma_separatrix_electron)", + physics_variables.nd_plasma_separatrix_electron, ) - po.ovarre( self.outfile, - "Plasma cross-sectional area (m2)", - "(a_plasma_poloidal)", - physics_variables.a_plasma_poloidal, - "OP ", + "Electron density at separatrix / nGW", + "(fgwsep_out)", + fgwsep_out, ) + + # Issue 558 - addition of constraint 76 to limit the value of nd_plasma_separatrix_electron, in proportion with the ballooning parameter and Greenwald density + if 76 in numerics.icc: po.ovarre( self.outfile, - "Plasma surface area (m2)", - "(a_plasma_surface)", - physics_variables.a_plasma_surface, - "OP ", + "Critical ballooning parameter value", + "(alpha_crit)", + physics_variables.alpha_crit, ) po.ovarre( self.outfile, - "Plasma volume (m3)", - "(vol_plasma)", - physics_variables.vol_plasma, - "OP ", + "Critical electron density at separatrix (/m3)", + "(nd_plasma_separatrix_electron_eich_max)", + physics_variables.nd_plasma_separatrix_electron_eich_max, ) - po.osubhd(self.outfile, "Current and Field :") - - if stellarator_variables.istell == 0: - po.oblnkl(self.outfile) - po.ovarin( - self.outfile, - "Plasma current scaling law used", - "(i_plasma_current)", - physics_variables.i_plasma_current, - ) - - po.ovarrf( - self.outfile, - "Plasma current (MA)", - "(plasma_current_MA)", - physics_variables.plasma_current / 1.0e6, - "OP ", - ) - - if physics_variables.i_alphaj == 1: - po.ovarrf( - self.outfile, - "Current density profile factor", - "(alphaj)", - physics_variables.alphaj, - "OP ", - ) - else: - po.ovarrf( - self.outfile, - "Current density profile factor", - "(alphaj)", - physics_variables.alphaj, - ) - po.ocmmnt(self.outfile, "Current profile index scalings:") - po.oblnkl(self.outfile) + po.ovarrf( + self.outfile, + "Temperature profile index", + "(alphat)", + physics_variables.alphat, + ) + po.ovarrf( + self.outfile, + "Temperature profile index beta", + "(tbeta)", + physics_variables.tbeta, + ) + po.ovarrf( + self.outfile, + "Pressure profile index", + "(alphap)", + physics_variables.alphap, + ) - po.ovarrf( - self.outfile, - "J. Wesson plasma current profile index", - "(alphaj_wesson)", - physics_variables.alphaj_wesson, - "OP ", - ) - po.oblnkl(self.outfile) - po.ovarrf( - self.outfile, - "On-axis plasma current density (A/m2)", - "(j_plasma_on_axis)", - physics_variables.j_plasma_on_axis, - "OP ", - ) - po.ovarrf( - self.outfile, - "Vertical field at plasma (T)", - "(b_plasma_vertical_required)", - physics_variables.b_plasma_vertical_required, - "OP ", - ) + po.oblnkl(self.outfile) + po.ostars(self.outfile, 110) + po.oblnkl(self.outfile) - po.ovarrf( + if stellarator_variables.istell == 0: + po.osubhd(self.outfile, "Density Limit using different models :") + po.ovarre( self.outfile, - "Vacuum toroidal field at R (T)", - "(b_plasma_toroidal_on_axis)", - physics_variables.b_plasma_toroidal_on_axis, + "Old ASDEX model", + "(nd_plasma_electron_max_array(1))", + physics_variables.nd_plasma_electron_max_array[0], + "OP ", ) - po.ovarrf( + po.ovarre( self.outfile, - "Toroidal field at plasma inboard (T)", - "(b_plasma_inboard_toroidal)", - physics_variables.b_plasma_inboard_toroidal, + "Borrass ITER model I", + "(nd_plasma_electron_max_array(2))", + physics_variables.nd_plasma_electron_max_array[1], + "OP ", ) - po.ovarrf( + po.ovarre( self.outfile, - "Toroidal field at plasma outboard (T)", - "(b_plasma_outboard_toroidal)", - physics_variables.b_plasma_outboard_toroidal, + "Borrass ITER model II", + "(nd_plasma_electron_max_array(3))", + physics_variables.nd_plasma_electron_max_array[2], + "OP ", ) - - for i in range(len(physics_variables.b_plasma_toroidal_profile)): - po.ovarre( - self.mfile, - f"Toroidal field in plasma at point {i}", - f"b_plasma_toroidal_profile{i}", - physics_variables.b_plasma_toroidal_profile[i], - ) - po.ovarrf( + po.ovarre( self.outfile, - "Average poloidal field (T)", - "(b_plasma_poloidal_average)", - physics_variables.b_plasma_poloidal_average, + "JET edge radiation model", + "(nd_plasma_electron_max_array(4))", + physics_variables.nd_plasma_electron_max_array[3], "OP ", ) - - po.ovarrf( + po.ovarre( self.outfile, - "Total field (sqrt(b_plasma_poloidal_average^2 + b_plasma_toroidal_on_axis^2)) (T)", - "(b_plasma_total)", - physics_variables.b_plasma_total, + "JET simplified model", + "(nd_plasma_electron_max_array(5))", + physics_variables.nd_plasma_electron_max_array[4], "OP ", ) - - if stellarator_variables.istell == 0: - po.ovarrf( - self.outfile, "Safety factor on axis", "(q0)", physics_variables.q0 - ) - - if physics_variables.i_plasma_current == 2: - po.ovarrf( - self.outfile, - "Mean edge safety factor", - "(q95)", - physics_variables.q95, - ) - - po.ovarrf( - self.outfile, - "Safety factor at 95% flux surface", - "(q95)", - physics_variables.q95, - ) - - po.ovarrf( - self.outfile, - "Cylindrical safety factor (qcyl)", - "(qstar)", - physics_variables.qstar, - "OP ", - ) - - if physics_variables.i_plasma_geometry == 1: - po.ovarrf( - self.outfile, - "Lower limit for edge safety factor q95", - "(q95_min)", - physics_variables.q95_min, - "OP ", - ) - po.ovarrf( + po.ovarre( self.outfile, - "Plasma normalised internal inductance", - "(ind_plasma_internal_norm)", - physics_variables.ind_plasma_internal_norm, + "Hugill-Murakami Mq model", + "(nd_plasma_electron_max_array(6))", + physics_variables.nd_plasma_electron_max_array[5], "OP ", ) - po.oblnkl(self.outfile) - po.ocmmnt(self.outfile, "Plasma normalised internal inductance scalings:") - po.oblnkl(self.outfile) - po.ovarrf( + po.ovarre( self.outfile, - "J. Wesson plasma normalised internal inductance", - "(ind_plasma_internal_norm_wesson)", - physics_variables.ind_plasma_internal_norm_wesson, + "Greenwald model", + "(nd_plasma_electron_max_array(7))", + physics_variables.nd_plasma_electron_max_array[6], "OP ", ) - po.ovarrf( + po.ovarre( self.outfile, - "J. Menard plasma normalised internal inductance", - "(ind_plasma_internal_norm_menard)", - physics_variables.ind_plasma_internal_norm_menard, + "ASDEX New", + "(nd_plasma_electron_max_array(8))", + physics_variables.nd_plasma_electron_max_array[7], "OP ", ) - po.ovarrf( + po.ovarre( self.outfile, - "ITER li(3) plasma normalised internal inductance", - "(ind_plasma_internal_norm_iter_3)", - physics_variables.ind_plasma_internal_norm_iter_3, + "Density limit from scaling (/m3)", + "(nd_plasma_electrons_max)", + physics_variables.nd_plasma_electrons_max, "OP ", ) - else: - po.ovarrf( - self.outfile, - "Rotational transform", - "(iotabar)", - stellarator_variables.iotabar, - ) po.oblnkl(self.outfile) po.ostars(self.outfile, 110) po.oblnkl(self.outfile) - # Output beta information - self.beta.output_beta_information() + po.oheadr(self.outfile, "Plasma Reactions :") - po.osubhd(self.outfile, "Temperature and Density (volume averaged) :") - po.ovarrf( - self.outfile, - "Number of radial points in plasma profiles", - "(n_plasma_profile_elements)", - physics_variables.n_plasma_profile_elements, - ) - po.ovarrf( - self.outfile, - "Volume averaged electron temperature (keV)", - "(temp_plasma_electron_vol_avg_kev)", - physics_variables.temp_plasma_electron_vol_avg_kev, - ) - po.ovarrf( - self.outfile, - "Ratio of ion to electron volume-averaged temperature", - "(f_temp_plasma_ion_electron)", - physics_variables.f_temp_plasma_ion_electron, - "IP ", - ) - po.ovarrf( - self.outfile, - "Electron temperature on axis (keV)", - "(temp_plasma_electron_on_axis_kev)", - physics_variables.temp_plasma_electron_on_axis_kev, - "OP ", - ) + po.osubhd(self.outfile, "Fuel Constituents :") po.ovarrf( self.outfile, - "Ion temperature (keV)", - "(temp_plasma_ion_vol_avg_kev)", - physics_variables.temp_plasma_ion_vol_avg_kev, + "Deuterium fuel fraction", + "(f_plasma_fuel_deuterium)", + physics_variables.f_plasma_fuel_deuterium, ) po.ovarrf( self.outfile, - "Ion temperature on axis (keV)", - "(temp_plasma_ion_on_axis_kev)", - physics_variables.temp_plasma_ion_on_axis_kev, - "OP ", + "Tritium fuel fraction", + "(f_plasma_fuel_tritium)", + physics_variables.f_plasma_fuel_tritium, ) po.ovarrf( self.outfile, - "Electron temp., density weighted (keV)", - "(temp_plasma_electron_density_weighted_kev)", - physics_variables.temp_plasma_electron_density_weighted_kev, - "OP ", + "3-Helium fuel fraction", + "(f_plasma_fuel_helium3)", + physics_variables.f_plasma_fuel_helium3, ) - po.ovarrf( + po.oblnkl(self.outfile) + po.ocmmnt(self.outfile, "----------------------------") + po.osubhd(self.outfile, "Fusion rates :") + po.ovarre( self.outfile, - "Ratio of electron density weighted temp. to volume averaged temp.", - "(f_temp_plasma_electron_density_vol_avg)", - physics_variables.f_temp_plasma_electron_density_vol_avg, + "Fusion rate: total (reactions/sec)", + "(fusrat_total)", + physics_variables.fusrat_total, "OP ", ) po.ovarre( self.outfile, - "Volume averaged electron number density (/m3)", - "(nd_plasma_electrons_vol_avg)", - physics_variables.nd_plasma_electrons_vol_avg, + "Fusion rate density: total (reactions/m3/sec)", + "(fusden_total)", + physics_variables.fusden_total, + "OP ", ) po.ovarre( self.outfile, - "Electron number density on axis (/m3)", - "(nd_plasma_electron_on_axis)", - physics_variables.nd_plasma_electron_on_axis, + "Fusion rate density: plasma (reactions/m3/sec)", + "(fusden_plasma)", + physics_variables.fusden_plasma, "OP ", ) - po.ovarre( + + po.oblnkl(self.outfile) + po.ocmmnt(self.outfile, "----------------------------") + po.osubhd(self.outfile, "Fusion Powers :") + po.ocmmnt( self.outfile, - "Line-averaged electron number density (/m3)", - "(nd_plasma_electron_line)", - physics_variables.nd_plasma_electron_line, - "OP ", + "Fusion power totals from the main plasma and beam-plasma interactions (if present)\n", ) + po.ovarre( self.outfile, - "Plasma pressure on axis (Pa)", - "(pres_plasma_thermal_on_axis)", - physics_variables.pres_plasma_thermal_on_axis, + "Total fusion power (MW)", + "(p_fusion_total_mw)", + physics_variables.p_fusion_total_mw, "OP ", ) po.ovarre( self.outfile, - "Volume averaged plasma pressure (Pa)", - "(pres_plasma_thermal_vol_avg)", - physics_variables.pres_plasma_thermal_vol_avg, + "D-T fusion power: total (MW)", + "(p_dt_total_mw)", + physics_variables.p_dt_total_mw, "OP ", ) - # As array output is not currently supported, each element is output as a float instance - # Output plasma pressure profiles to mfile - for i in range(len(physics_variables.pres_plasma_thermal_total_profile)): + for i in range(len(physics_variables.fusrat_plasma_dt_profile)): po.ovarre( self.mfile, - f"Total plasma pressure at point {i}", - f"(pres_plasma_thermal_total_profile{i})", - physics_variables.pres_plasma_thermal_total_profile[i], + f"DT fusion rate at point {i}", + f"fusrat_plasma_dt_profile{i}", + physics_variables.fusrat_plasma_dt_profile[i], ) - for i in range(len(physics_variables.pres_plasma_electron_profile)): + + for i in range(len(physics_variables.fusrat_plasma_dd_triton_profile)): po.ovarre( self.mfile, - f"Total plasma electron pressure at point {i}", - f"(pres_plasma_electron_profile{i})", - physics_variables.pres_plasma_electron_profile[i], + f"D-D -> T fusion rate at point {i}", + f"fusrat_plasma_dd_triton_profile{i}", + physics_variables.fusrat_plasma_dd_triton_profile[i], ) - for i in range(len(physics_variables.pres_plasma_ion_total_profile)): + for i in range(len(physics_variables.fusrat_plasma_dd_helion_profile)): po.ovarre( self.mfile, - f"Total plasma ion pressure at point {i}", - f"(pres_plasma_ion_total_profile{i})", - physics_variables.pres_plasma_ion_total_profile[i], + f"D-D -> 3He fusion rate at point {i}", + f"fusrat_plasma_dd_helion_profile{i}", + physics_variables.fusrat_plasma_dd_helion_profile[i], ) - for i in range(len(physics_variables.pres_plasma_fuel_profile)): + for i in range(len(physics_variables.fusrat_plasma_dhe3_profile)): po.ovarre( self.mfile, - f"Total plasma fuel pressure at point {i}", - f"(pres_plasma_fuel_profile{i})", - physics_variables.pres_plasma_fuel_profile[i], - ) - - if stellarator_variables.istell == 0: - po.ovarre( - self.outfile, - "Line-averaged electron density / Greenwald density", - "(dnla_gw)", - physics_variables.nd_plasma_electron_line - / physics_variables.nd_plasma_electron_max_array[6], - "OP ", + f"D-3He fusion rate at point {i}", + f"fusrat_plasma_dhe3_profile{i}", + physics_variables.fusrat_plasma_dhe3_profile[i], ) - po.ovarre( self.outfile, - "Total Ion number density (/m3)", - "(nd_plasma_ions_total_vol_avg)", - physics_variables.nd_plasma_ions_total_vol_avg, + "D-T fusion power: plasma (MW)", + "(p_plasma_dt_mw)", + physics_variables.p_plasma_dt_mw, "OP ", ) po.ovarre( self.outfile, - "Fuel ion number density (/m3)", - "(nd_plasma_fuel_ions_vol_avg)", - physics_variables.nd_plasma_fuel_ions_vol_avg, + "D-T fusion power: beam (MW)", + "(p_beam_dt_mw)", + physics_variables.p_beam_dt_mw, "OP ", ) po.ovarre( self.outfile, - "Total impurity number density with Z > 2 (no He) (/m3)", - "(nd_plasma_impurities_vol_avg)", - physics_variables.nd_plasma_impurities_vol_avg, + "D-D fusion power (MW)", + "(p_dd_total_mw)", + physics_variables.p_dd_total_mw, "OP ", ) po.ovarre( self.outfile, - "Helium ion number density (thermalised ions only) (/m3)", - "(nd_plasma_alphas_vol_avg)", - physics_variables.nd_plasma_alphas_vol_avg, + "D-D branching ratio for tritium producing reactions", + "(f_dd_branching_trit)", + physics_variables.f_dd_branching_trit, "OP ", ) po.ovarre( self.outfile, - "Helium ion density (thermalised ions only) / electron number density", - "(f_nd_alpha_electron)", - physics_variables.f_nd_alpha_electron, + "D-He3 fusion power (MW)", + "(p_dhe3_total_mw)", + physics_variables.p_dhe3_total_mw, + "OP ", ) + + po.oblnkl(self.outfile) + po.ocmmnt(self.outfile, "----------------------------") + po.osubhd(self.outfile, "Alpha Powers :") po.ovarre( self.outfile, - "Plasma volume averaged proton number density (/m3)", - "(nd_plasma_protons_vol_avg)", - physics_variables.nd_plasma_protons_vol_avg, - "OP ", + "Alpha rate density: total (particles/m3/sec)", + "(fusden_alpha_total)", + physics_variables.fusden_alpha_total, + "OP ", ) po.ovarre( self.outfile, - "Proton number density / electron number density", - "(f_nd_protium_electrons)", - physics_variables.f_nd_protium_electrons, + "Alpha rate density: plasma (particles/m3/sec)", + "(fusden_plasma_alpha)", + physics_variables.fusden_plasma_alpha, "OP ", ) - po.ovarre( self.outfile, - "Hot beam ion number density (/m3)", - "(nd_beam_ions)", - physics_variables.nd_beam_ions, + "Alpha power: total (MW)", + "(p_alpha_total_mw)", + physics_variables.p_alpha_total_mw, "OP ", ) po.ovarre( self.outfile, - "Hot beam ion number density / electron density", - "(f_nd_beam_electron)", - physics_variables.f_nd_beam_electron, + "Alpha power density: total (MW/m^3)", + "(pden_alpha_total_mw)", + physics_variables.pden_alpha_total_mw, + "OP ", + ) + po.ovarre( + self.outfile, + "Alpha power: plasma only (MW)", + "(p_plasma_alpha_mw)", + physics_variables.p_plasma_alpha_mw, + "OP ", + ) + po.ovarre( + self.outfile, + "Alpha power density: plasma (MW/m^3)", + "(pden_plasma_alpha_mw)", + physics_variables.pden_plasma_alpha_mw, + "OP ", + ) + po.ovarre( + self.outfile, + "Alpha power: beam-plasma (MW)", + "(p_beam_alpha_mw)", + physics_variables.p_beam_alpha_mw, + "OP ", + ) + po.ovarre( + self.outfile, + "Alpha power per unit volume transferred to electrons (MW/m3)", + "(f_pden_alpha_electron_mw)", + physics_variables.f_pden_alpha_electron_mw, + "OP ", + ) + po.ovarre( + self.outfile, + "Alpha power per unit volume transferred to ions (MW/m3)", + "(f_pden_alpha_ions_mw)", + physics_variables.f_pden_alpha_ions_mw, "OP ", ) po.oblnkl(self.outfile) - po.oblnkl(self.outfile) - po.ostars(self.outfile, 110) - po.oblnkl(self.outfile) - - po.ocmmnt(self.outfile, "Impurities:") - po.oblnkl(self.outfile) - po.ocmmnt(self.outfile, "Plasma ion densities / electron density:") - - for imp in range(impurity_radiation_module.N_IMPURITIES): - # MDK Update f_nd_impurity_electrons, as this will make the ITV output work correctly. - impurity_radiation_module.f_nd_impurity_electrons[imp] = ( - impurity_radiation_module.f_nd_impurity_electron_array[imp] - ) - str1 = ( - impurity_radiation_module.impurity_arr_label[imp].item() - + " concentration" - ) - str2 = f"(f_nd_impurity_electrons({imp + 1:02}))" - # MDK Add output flag for H which is calculated. - if imp == 0: - po.ovarre( - self.outfile, - str1, - str2, - impurity_radiation_module.f_nd_impurity_electrons[imp], - "OP ", - ) - else: - po.ovarre( - self.outfile, - str1, - str2, - impurity_radiation_module.f_nd_impurity_electrons[imp], - ) - + po.ocmmnt(self.outfile, "----------------------------") + po.osubhd(self.outfile, "Neutron Powers :") po.ovarre( self.outfile, - "Average mass of all ions (amu)", - "(m_ions_total_amu)", - physics_variables.m_ions_total_amu, + "Neutron power: total (MW)", + "(p_neutron_total_mw)", + physics_variables.p_neutron_total_mw, "OP ", ) po.ovarre( self.outfile, - "Total mass of all ions in plasma (kg)", - "(m_plasma_ions_total)", - physics_variables.m_plasma_ions_total, + "Neutron power density: total (MW/m^3)", + "(pden_neutron_total_mw)", + physics_variables.pden_neutron_total_mw, "OP ", ) po.ovarre( self.outfile, - "Average mass of all fuel ions (amu)", - "(m_fuel_amu)", - physics_variables.m_fuel_amu, + "Neutron power: plasma only (MW)", + "(p_plasma_neutron_mw)", + physics_variables.p_plasma_neutron_mw, "OP ", ) po.ovarre( self.outfile, - "Total mass of all fuel ions in plasma (kg)", - "(m_plasma_fuel_ions)", - physics_variables.m_plasma_fuel_ions, + "Neutron power density: plasma (MW/m^3)", + "(pden_plasma_neutron_mw)", + physics_variables.pden_plasma_neutron_mw, "OP ", ) po.ovarre( self.outfile, - "Average mass of all beam ions (amu)", - "(m_beam_amu)", - physics_variables.m_beam_amu, + "Neutron power: beam-plasma (MW)", + "(p_beam_neutron_mw)", + physics_variables.p_beam_neutron_mw, "OP ", ) + po.oblnkl(self.outfile) + po.ocmmnt(self.outfile, "----------------------------") + + po.osubhd(self.outfile, "Charged Particle Powers :") + po.ovarre( self.outfile, - "Total mass of all alpha particles in plasma (kg)", - "(m_plasma_alpha)", - physics_variables.m_plasma_alpha, + "Charged particle power (p, 3He, T) (excluding alphas) (MW)", + "(p_non_alpha_charged_mw)", + physics_variables.p_non_alpha_charged_mw, "OP ", ) po.ovarre( self.outfile, - "Total mass of all electrons in plasma (kg)", - "(m_plasma_electron)", - physics_variables.m_plasma_electron, + "Charged particle power density (p, 3He, T) (excluding alphas) (MW)", + "(pden_non_alpha_charged_mw)", + physics_variables.pden_non_alpha_charged_mw, "OP ", ) po.ovarre( self.outfile, - "Total mass of the plasma (kg)", - "(m_plasma)", - physics_variables.m_plasma, + "Total charged particle power (including alphas) (MW)", + "(p_charged_particle_mw)", + physics_variables.p_charged_particle_mw, "OP ", ) po.oblnkl(self.outfile) + po.ostars(self.outfile, 110) + po.oblnkl(self.outfile) + + po.osubhd(self.outfile, "Plasma radiation powers (excluding SOL):") + po.ovarre( + self.outfile, + "Plasma total synchrotron radiation power (MW)", + "(p_plasma_sync_mw)", + physics_variables.p_plasma_sync_mw, + "OP ", + ) + po.ovarre( + self.outfile, + "Plasma total synchrotron radiation power density (MW/m^3)", + "(pden_plasma_sync_mw)", + physics_variables.pden_plasma_sync_mw, + "OP ", + ) po.ovarrf( self.outfile, - "Volume averaged plasma effective charge", - "(n_charge_plasma_effective_vol_avg)", - physics_variables.n_charge_plasma_effective_vol_avg, + "Synchrotron wall reflectivity factor", + "(f_sync_reflect)", + physics_variables.f_sync_reflect, + ) + po.oblnkl(self.outfile) + po.ovarre( + self.outfile, + "Plasma normalised minor radius defining 'core' region", + "(radius_plasma_core_norm)", + impurity_radiation_module.radius_plasma_core_norm, + ) + po.ovarre( + self.outfile, + "Fractional scaling of radiation power along core profile", + "(f_p_plasma_core_rad_reduction)", + impurity_radiation_module.f_p_plasma_core_rad_reduction, + ) + po.ovarre( + self.outfile, + "Plasma total radiation power from core region (MW)", + "(p_plasma_inner_rad_mw)", + physics_variables.p_plasma_inner_rad_mw, "OP ", ) - for i in range(len(physics_variables.n_charge_plasma_effective_profile)): + po.ovarre( + self.outfile, + "Plasma total radiation power from edge region (MW)", + "(p_plasma_outer_rad_mw)", + physics_variables.p_plasma_outer_rad_mw, + "OP ", + ) + + if stellarator_variables.istell != 0: po.ovarre( - self.mfile, - "Effective charge at point", - f"(n_charge_plasma_effective_profile{i})", - physics_variables.n_charge_plasma_effective_profile[i], + self.outfile, + "SOL radiation power as imposed by f_rad (MW)", + "(psolradmw)", + physics_variables.psolradmw, "OP ", ) - for imp in range(impurity_radiation_module.N_IMPURITIES): - for i in range(physics_variables.n_plasma_profile_elements): - po.ovarre( - self.mfile, - "Impurity charge at point", - f"(n_charge_plasma_profile{imp}_{i})", - impurity_radiation_module.n_charge_impurity_profile[imp][i], - "OP ", - ) - - po.ovarrf( + po.ovarre( self.outfile, - "Mass-weighted Effective charge", - "(n_charge_plasma_effective_mass_weighted_vol_avg)", - physics_variables.n_charge_plasma_effective_mass_weighted_vol_avg, + "Plasma total radiation power from inside last closed flux surface (MW)", + "(p_plasma_rad_mw)", + physics_variables.p_plasma_rad_mw, "OP ", ) - - po.ovarrf( - self.outfile, "Density profile factor", "(alphan)", physics_variables.alphan + po.ovarre( + self.outfile, + "Separatrix radiation fraction (MW)", + "(f_p_plasma_separatrix_rad)", + physics_variables.f_p_plasma_separatrix_rad, + "OP ", ) - po.ovarin( + po.ovarre( self.outfile, - "Plasma profile model", - "(i_plasma_pedestal)", - physics_variables.i_plasma_pedestal, + "Nominal mean radiation load on vessel first-wall (MW/m^2)", + "(pflux_fw_rad_mw)", + physics_variables.pflux_fw_rad_mw, + "OP ", + ) + po.ovarre( + self.outfile, + "Average neutron flux at plasma surface (MW/m^2)", + "(plfux_plasma_surface_neutron_avg_mw)", + physics_variables.plfux_plasma_surface_neutron_avg_mw, + "OP ", ) - if physics_variables.i_plasma_pedestal >= 1: - if ( - physics_variables.nd_plasma_electron_on_axis - < physics_variables.nd_plasma_pedestal_electron - ): - logger.error("Central density is less than pedestal density") - - po.ocmmnt(self.outfile, "Pedestal profiles are used.") - po.ovarrf( - self.outfile, - "Density pedestal r/a location", - "(radius_plasma_pedestal_density_norm)", - physics_variables.radius_plasma_pedestal_density_norm, - ) - if physics_variables.f_nd_plasma_pedestal_greenwald >= 0e0: - po.ovarre( - self.outfile, - "Electron density pedestal height (/m3)", - "(nd_plasma_pedestal_electron)", - physics_variables.nd_plasma_pedestal_electron, - "OP ", - ) - else: - po.ovarre( - self.outfile, - "Electron density pedestal height (/m3)", - "(nd_plasma_pedestal_electron)", - physics_variables.nd_plasma_pedestal_electron, - ) - - # must be assigned to their exisiting values# - fgwped_out = ( - physics_variables.nd_plasma_pedestal_electron - / physics_variables.nd_plasma_electron_max_array[6] - ) - fgwsep_out = ( - physics_variables.nd_plasma_separatrix_electron - / physics_variables.nd_plasma_electron_max_array[6] - ) - if physics_variables.f_nd_plasma_pedestal_greenwald >= 0e0: - physics_variables.f_nd_plasma_pedestal_greenwald = ( - physics_variables.nd_plasma_pedestal_electron - / physics_variables.nd_plasma_electron_max_array[6] - ) - if physics_variables.f_nd_plasma_separatrix_greenwald >= 0e0: - physics_variables.f_nd_plasma_separatrix_greenwald = ( - physics_variables.nd_plasma_separatrix_electron - / physics_variables.nd_plasma_electron_max_array[6] - ) - - po.ovarre( - self.outfile, - "Electron density at pedestal / nGW", - "(fgwped_out)", - fgwped_out, - ) - po.ovarrf( - self.outfile, - "Temperature pedestal r/a location", - "(radius_plasma_pedestal_temp_norm)", - physics_variables.radius_plasma_pedestal_temp_norm, - ) - - po.ovarrf( - self.outfile, - "Electron temp. pedestal height (keV)", - "(temp_plasma_pedestal_kev)", - physics_variables.temp_plasma_pedestal_kev, - ) - if 78 in numerics.icc: - po.ovarrf( - self.outfile, - "Electron temp. at separatrix (keV)", - "(temp_plasma_separatrix_kev)", - physics_variables.temp_plasma_separatrix_kev, - "OP ", - ) - else: - po.ovarrf( - self.outfile, - "Electron temp. at separatrix (keV)", - "(temp_plasma_separatrix_kev)", - physics_variables.temp_plasma_separatrix_kev, - ) - - po.ovarre( - self.outfile, - "Electron density at separatrix (/m3)", - "(nd_plasma_separatrix_electron)", - physics_variables.nd_plasma_separatrix_electron, - ) - po.ovarre( - self.outfile, - "Electron density at separatrix / nGW", - "(fgwsep_out)", - fgwsep_out, - ) - - # Issue 558 - addition of constraint 76 to limit the value of nd_plasma_separatrix_electron, in proportion with the ballooning parameter and Greenwald density - if 76 in numerics.icc: - po.ovarre( - self.outfile, - "Critical ballooning parameter value", - "(alpha_crit)", - physics_variables.alpha_crit, - ) - po.ovarre( - self.outfile, - "Critical electron density at separatrix (/m3)", - "(nd_plasma_separatrix_electron_eich_max)", - physics_variables.nd_plasma_separatrix_electron_eich_max, - ) - - po.ovarrf( + po.ovarre( self.outfile, - "Temperature profile index", - "(alphat)", - physics_variables.alphat, + "Peaking factor for radiation first-wall load", + "(f_fw_rad_max)", + constraint_variables.f_fw_rad_max, + "IP ", ) - po.ovarrf( + po.ovarre( self.outfile, - "Temperature profile index beta", - "(tbeta)", - physics_variables.tbeta, + "Maximum permitted radiation first-wall load (MW/m^2)", + "(pflux_fw_rad_max)", + constraint_variables.pflux_fw_rad_max, + "IP ", ) - po.ovarrf( + po.ovarre( self.outfile, - "Pressure profile index", - "(alphap)", - physics_variables.alphap, + "Peak radiation wall load (MW/m^2)", + "(pflux_fw_rad_max_mw)", + constraint_variables.pflux_fw_rad_max_mw, + "OP ", + ) + po.ovarre( + self.outfile, + "Fast alpha particle power incident on the first-wall (MW)", + "(p_fw_alpha_mw)", + physics_variables.p_fw_alpha_mw, + "OP ", + ) + po.ovarre( + self.outfile, + "Nominal mean neutron load on vessel first-wall (MW/m^2)", + "(pflux_fw_neutron_mw)", + physics_variables.pflux_fw_neutron_mw, + "OP ", ) - - po.oblnkl(self.outfile) - po.ostars(self.outfile, 110) - po.oblnkl(self.outfile) if stellarator_variables.istell == 0: - po.osubhd(self.outfile, "Density Limit using different models :") - po.ovarre( - self.outfile, - "Old ASDEX model", - "(nd_plasma_electron_max_array(1))", - physics_variables.nd_plasma_electron_max_array[0], - "OP ", - ) + po.oblnkl(self.outfile) po.ovarre( self.outfile, - "Borrass ITER model I", - "(nd_plasma_electron_max_array(2))", - physics_variables.nd_plasma_electron_max_array[1], + "Power incident on the divertor targets (MW)", + "(ptarmw)", + physics_variables.ptarmw, "OP ", ) po.ovarre( self.outfile, - "Borrass ITER model II", - "(nd_plasma_electron_max_array(3))", - physics_variables.nd_plasma_electron_max_array[2], - "OP ", + "Fraction of power to the lower divertor", + "(f_p_div_lower)", + physics_variables.f_p_div_lower, + "IP ", ) po.ovarre( self.outfile, - "JET edge radiation model", - "(nd_plasma_electron_max_array(4))", - physics_variables.nd_plasma_electron_max_array[3], + "Outboard side heat flux decay length (m)", + "(lambdaio)", + physics_variables.lambdaio, "OP ", ) + if divertor_variables.n_divertors == 2: + po.ovarre( + self.outfile, + "Midplane seperation of the two magnetic closed flux surfaces (m)", + "(drsep)", + physics_variables.drsep, + "OP ", + ) + po.ovarre( self.outfile, - "JET simplified model", - "(nd_plasma_electron_max_array(5))", - physics_variables.nd_plasma_electron_max_array[4], + "Fraction of power on the inner targets", + "(fio)", + physics_variables.fio, "OP ", ) po.ovarre( self.outfile, - "Hugill-Murakami Mq model", - "(nd_plasma_electron_max_array(6))", - physics_variables.nd_plasma_electron_max_array[5], + "Fraction of power incident on the lower inner target", + "(fLI)", + physics_variables.fli, "OP ", ) po.ovarre( self.outfile, - "Greenwald model", - "(nd_plasma_electron_max_array(7))", - physics_variables.nd_plasma_electron_max_array[6], + "Fraction of power incident on the lower outer target", + "(fLO)", + physics_variables.flo, "OP ", ) + if divertor_variables.n_divertors == 2: + po.ovarre( + self.outfile, + "Fraction of power incident on the upper inner target", + "(fUI)", + physics_variables.fui, + "OP ", + ) + po.ovarre( + self.outfile, + "Fraction of power incident on the upper outer target", + "(fUO)", + physics_variables.fuo, + "OP ", + ) + po.ovarre( self.outfile, - "ASDEX New", - "(nd_plasma_electron_max_array(8))", - physics_variables.nd_plasma_electron_max_array[7], + "Power incident on the lower inner target (MW)", + "(pLImw)", + physics_variables.plimw, "OP ", ) po.ovarre( self.outfile, - "Density limit from scaling (/m3)", - "(nd_plasma_electrons_max)", - physics_variables.nd_plasma_electrons_max, + "Power incident on the lower outer target (MW)", + "(pLOmw)", + physics_variables.plomw, "OP ", ) + if divertor_variables.n_divertors == 2: + po.ovarre( + self.outfile, + "Power incident on the upper innner target (MW)", + "(pUImw)", + physics_variables.puimw, + "OP ", + ) + po.ovarre( + self.outfile, + "Power incident on the upper outer target (MW)", + "(pUOmw)", + physics_variables.puomw, + "OP ", + ) po.oblnkl(self.outfile) - po.ostars(self.outfile, 110) - po.oblnkl(self.outfile) - - po.oheadr(self.outfile, "Plasma Reactions :") - - po.osubhd(self.outfile, "Fuel Constituents :") + po.ovarre( + self.outfile, + "Ohmic heating power (MW)", + "(p_plasma_ohmic_mw)", + physics_variables.p_plasma_ohmic_mw, + "OP ", + ) po.ovarrf( self.outfile, - "Deuterium fuel fraction", - "(f_plasma_fuel_deuterium)", - physics_variables.f_plasma_fuel_deuterium, + "Fraction of alpha power deposited in plasma", + "(f_p_alpha_plasma_deposited)", + physics_variables.f_p_alpha_plasma_deposited, + "IP", ) po.ovarrf( self.outfile, - "Tritium fuel fraction", - "(f_plasma_fuel_tritium)", - physics_variables.f_plasma_fuel_tritium, + "Fraction of alpha power to electrons", + "(f_alpha_electron)", + physics_variables.f_alpha_electron, + "OP ", ) po.ovarrf( self.outfile, - "3-Helium fuel fraction", - "(f_plasma_fuel_helium3)", - physics_variables.f_plasma_fuel_helium3, + "Fraction of alpha power to ions", + "(f_alpha_ion)", + physics_variables.f_alpha_ion, + "OP ", ) - po.oblnkl(self.outfile) - po.ocmmnt(self.outfile, "----------------------------") - po.osubhd(self.outfile, "Fusion rates :") po.ovarre( self.outfile, - "Fusion rate: total (reactions/sec)", - "(fusrat_total)", - physics_variables.fusrat_total, + "Ion transport (MW)", + "(p_ion_transport_loss_mw)", + physics_variables.p_ion_transport_loss_mw, "OP ", ) po.ovarre( self.outfile, - "Fusion rate density: total (reactions/m3/sec)", - "(fusden_total)", - physics_variables.fusden_total, + "Electron transport (MW)", + "(p_electron_transport_loss_mw)", + physics_variables.p_electron_transport_loss_mw, "OP ", ) po.ovarre( self.outfile, - "Fusion rate density: plasma (reactions/m3/sec)", - "(fusden_plasma)", - physics_variables.fusden_plasma, + "Injection power to ions (MW)", + "(p_hcd_injected_ions_mw)", + current_drive_variables.p_hcd_injected_ions_mw, "OP ", ) - - po.oblnkl(self.outfile) - po.ocmmnt(self.outfile, "----------------------------") - po.osubhd(self.outfile, "Fusion Powers :") - po.ocmmnt( + po.ovarre( self.outfile, - "Fusion power totals from the main plasma and beam-plasma interactions (if present)\n", + "Injection power to electrons (MW)", + "(p_hcd_injected_electrons_mw)", + current_drive_variables.p_hcd_injected_electrons_mw, + "OP ", ) + if physics_variables.i_plasma_ignited == 1: + po.ocmmnt(self.outfile, " (Injected power only used for start-up phase)") - po.ovarre( + po.ovarin( self.outfile, - "Total fusion power (MW)", - "(p_fusion_total_mw)", - physics_variables.p_fusion_total_mw, - "OP ", + "Ignited plasma switch (0=not ignited, 1=ignited)", + "(i_plasma_ignited)", + physics_variables.i_plasma_ignited, ) + + po.oblnkl(self.outfile) po.ovarre( self.outfile, - "D-T fusion power: total (MW)", - "(p_dt_total_mw)", - physics_variables.p_dt_total_mw, + "Power into divertor zone via charged particles (MW)", + "(p_plasma_separatrix_mw)", + physics_variables.p_plasma_separatrix_mw, "OP ", ) - for i in range(len(physics_variables.fusrat_plasma_dt_profile)): - po.ovarre( - self.mfile, - f"DT fusion rate at point {i}", - f"fusrat_plasma_dt_profile{i}", - physics_variables.fusrat_plasma_dt_profile[i], + + if physics_variables.p_plasma_separatrix_mw <= 0.001e0: + logger.error( + "Possible problem with high radiation power, forcing p_plasma_separatrix_mw to odd values. " + f"{physics_variables.p_plasma_separatrix_mw=}" + ) + po.oblnkl(self.outfile) + po.ocmmnt( + self.outfile, " BEWARE: possible problem with high radiation power" + ) + po.ocmmnt(self.outfile, " Power into divertor zone is unrealistic;") + po.ocmmnt(self.outfile, " divertor calculations will be nonsense#") + po.ocmmnt( + self.outfile, " Set constraint 17 (Radiation fraction upper limit)." ) + po.oblnkl(self.outfile) - for i in range(len(physics_variables.fusrat_plasma_dd_triton_profile)): + if divertor_variables.n_divertors == 2: + # Double null divertor configuration po.ovarre( - self.mfile, - f"D-D -> T fusion rate at point {i}", - f"fusrat_plasma_dd_triton_profile{i}", - physics_variables.fusrat_plasma_dd_triton_profile[i], + self.outfile, + "Pdivt / R ratio (MW/m) (On peak divertor)", + "(p_div_separatrix_max_mw/physics_variables.rmajor)", + physics_variables.p_div_separatrix_max_mw / physics_variables.rmajor, + "OP ", ) - for i in range(len(physics_variables.fusrat_plasma_dd_helion_profile)): po.ovarre( - self.mfile, - f"D-D -> 3He fusion rate at point {i}", - f"fusrat_plasma_dd_helion_profile{i}", - physics_variables.fusrat_plasma_dd_helion_profile[i], + self.outfile, + "Pdivt Bt / qAR ratio (MWT/m) (On peak divertor)", + "(pdivmaxbt/qar)", + ( + ( + physics_variables.p_div_separatrix_max_mw + * physics_variables.b_plasma_toroidal_on_axis + ) + / ( + physics_variables.q95 + * physics_variables.aspect + * physics_variables.rmajor + ) + ), + "OP ", ) - for i in range(len(physics_variables.fusrat_plasma_dhe3_profile)): + else: + # Single null divertor configuration po.ovarre( - self.mfile, - f"D-3He fusion rate at point {i}", - f"fusrat_plasma_dhe3_profile{i}", - physics_variables.fusrat_plasma_dhe3_profile[i], + self.outfile, + "Psep / R ratio (MW/m)", + "(p_plasma_separatrix_mw/rmajor)", + physics_variables.p_plasma_separatrix_mw / physics_variables.rmajor, + "OP ", + ) + po.ovarre( + self.outfile, + "Psep Bt / qAR ratio (MWT/m)", + "(pdivtbt_over_qar)", + ( + ( + physics_variables.p_plasma_separatrix_mw + * physics_variables.b_plasma_toroidal_on_axis + ) + / ( + physics_variables.q95 + * physics_variables.aspect + * physics_variables.rmajor + ) + ), + "OP ", ) - po.ovarre( - self.outfile, - "D-T fusion power: plasma (MW)", - "(p_plasma_dt_mw)", - physics_variables.p_plasma_dt_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "D-T fusion power: beam (MW)", - "(p_beam_dt_mw)", - physics_variables.p_beam_dt_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "D-D fusion power (MW)", - "(p_dd_total_mw)", - physics_variables.p_dd_total_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "D-D branching ratio for tritium producing reactions", - "(f_dd_branching_trit)", - physics_variables.f_dd_branching_trit, - "OP ", - ) - po.ovarre( - self.outfile, - "D-He3 fusion power (MW)", - "(p_dhe3_total_mw)", - physics_variables.p_dhe3_total_mw, - "OP ", - ) - - po.oblnkl(self.outfile) - po.ocmmnt(self.outfile, "----------------------------") - po.osubhd(self.outfile, "Alpha Powers :") - po.ovarre( - self.outfile, - "Alpha rate density: total (particles/m3/sec)", - "(fusden_alpha_total)", - physics_variables.fusden_alpha_total, - "OP ", - ) - po.ovarre( - self.outfile, - "Alpha rate density: plasma (particles/m3/sec)", - "(fusden_plasma_alpha)", - physics_variables.fusden_plasma_alpha, - "OP ", - ) - po.ovarre( - self.outfile, - "Alpha power: total (MW)", - "(p_alpha_total_mw)", - physics_variables.p_alpha_total_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "Alpha power density: total (MW/m^3)", - "(pden_alpha_total_mw)", - physics_variables.pden_alpha_total_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "Alpha power: plasma only (MW)", - "(p_plasma_alpha_mw)", - physics_variables.p_plasma_alpha_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "Alpha power density: plasma (MW/m^3)", - "(pden_plasma_alpha_mw)", - physics_variables.pden_plasma_alpha_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "Alpha power: beam-plasma (MW)", - "(p_beam_alpha_mw)", - physics_variables.p_beam_alpha_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "Alpha power per unit volume transferred to electrons (MW/m3)", - "(f_pden_alpha_electron_mw)", - physics_variables.f_pden_alpha_electron_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "Alpha power per unit volume transferred to ions (MW/m3)", - "(f_pden_alpha_ions_mw)", - physics_variables.f_pden_alpha_ions_mw, - "OP ", - ) - - po.oblnkl(self.outfile) - po.ocmmnt(self.outfile, "----------------------------") - po.osubhd(self.outfile, "Neutron Powers :") - po.ovarre( - self.outfile, - "Neutron power: total (MW)", - "(p_neutron_total_mw)", - physics_variables.p_neutron_total_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "Neutron power density: total (MW/m^3)", - "(pden_neutron_total_mw)", - physics_variables.pden_neutron_total_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "Neutron power: plasma only (MW)", - "(p_plasma_neutron_mw)", - physics_variables.p_plasma_neutron_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "Neutron power density: plasma (MW/m^3)", - "(pden_plasma_neutron_mw)", - physics_variables.pden_plasma_neutron_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "Neutron power: beam-plasma (MW)", - "(p_beam_neutron_mw)", - physics_variables.p_beam_neutron_mw, - "OP ", - ) - po.oblnkl(self.outfile) - po.ocmmnt(self.outfile, "----------------------------") - - po.osubhd(self.outfile, "Charged Particle Powers :") - - po.ovarre( - self.outfile, - "Charged particle power (p, 3He, T) (excluding alphas) (MW)", - "(p_non_alpha_charged_mw)", - physics_variables.p_non_alpha_charged_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "Charged particle power density (p, 3He, T) (excluding alphas) (MW)", - "(pden_non_alpha_charged_mw)", - physics_variables.pden_non_alpha_charged_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "Total charged particle power (including alphas) (MW)", - "(p_charged_particle_mw)", - physics_variables.p_charged_particle_mw, - "OP ", - ) po.oblnkl(self.outfile) po.ostars(self.outfile, 110) po.oblnkl(self.outfile) - po.osubhd(self.outfile, "Plasma radiation powers (excluding SOL):") - po.ovarre( - self.outfile, - "Plasma total synchrotron radiation power (MW)", - "(p_plasma_sync_mw)", - physics_variables.p_plasma_sync_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "Plasma total synchrotron radiation power density (MW/m^3)", - "(pden_plasma_sync_mw)", - physics_variables.pden_plasma_sync_mw, - "OP ", - ) - po.ovarrf( - self.outfile, - "Synchrotron wall reflectivity factor", - "(f_sync_reflect)", - physics_variables.f_sync_reflect, - ) - po.oblnkl(self.outfile) - po.ovarre( - self.outfile, - "Plasma normalised minor radius defining 'core' region", - "(radius_plasma_core_norm)", - impurity_radiation_module.radius_plasma_core_norm, - ) - po.ovarre( - self.outfile, - "Fractional scaling of radiation power along core profile", - "(f_p_plasma_core_rad_reduction)", - impurity_radiation_module.f_p_plasma_core_rad_reduction, - ) - po.ovarre( - self.outfile, - "Plasma total radiation power from core region (MW)", - "(p_plasma_inner_rad_mw)", - physics_variables.p_plasma_inner_rad_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "Plasma total radiation power from edge region (MW)", - "(p_plasma_outer_rad_mw)", - physics_variables.p_plasma_outer_rad_mw, - "OP ", - ) + if stellarator_variables.istell == 0: + po.osubhd(self.outfile, "H-mode Power Threshold Scalings :") - if stellarator_variables.istell != 0: po.ovarre( self.outfile, - "SOL radiation power as imposed by f_rad (MW)", - "(psolradmw)", - physics_variables.psolradmw, + "ITER 1996 scaling: nominal (MW)", + "(l_h_threshold_powers(1))", + physics_variables.l_h_threshold_powers[0], "OP ", ) - - po.ovarre( - self.outfile, - "Plasma total radiation power from inside last closed flux surface (MW)", - "(p_plasma_rad_mw)", - physics_variables.p_plasma_rad_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "Separatrix radiation fraction (MW)", - "(f_p_plasma_separatrix_rad)", - physics_variables.f_p_plasma_separatrix_rad, - "OP ", - ) - po.ovarre( - self.outfile, - "Nominal mean radiation load on vessel first-wall (MW/m^2)", - "(pflux_fw_rad_mw)", - physics_variables.pflux_fw_rad_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "Average neutron flux at plasma surface (MW/m^2)", - "(plfux_plasma_surface_neutron_avg_mw)", - physics_variables.plfux_plasma_surface_neutron_avg_mw, - "OP ", - ) - - po.ovarre( - self.outfile, - "Peaking factor for radiation first-wall load", - "(f_fw_rad_max)", - constraint_variables.f_fw_rad_max, - "IP ", - ) - po.ovarre( - self.outfile, - "Maximum permitted radiation first-wall load (MW/m^2)", - "(pflux_fw_rad_max)", - constraint_variables.pflux_fw_rad_max, - "IP ", - ) - po.ovarre( - self.outfile, - "Peak radiation wall load (MW/m^2)", - "(pflux_fw_rad_max_mw)", - constraint_variables.pflux_fw_rad_max_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "Fast alpha particle power incident on the first-wall (MW)", - "(p_fw_alpha_mw)", - physics_variables.p_fw_alpha_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "Nominal mean neutron load on vessel first-wall (MW/m^2)", - "(pflux_fw_neutron_mw)", - physics_variables.pflux_fw_neutron_mw, - "OP ", - ) - - if stellarator_variables.istell == 0: - po.oblnkl(self.outfile) po.ovarre( self.outfile, - "Power incident on the divertor targets (MW)", - "(ptarmw)", - physics_variables.ptarmw, + "ITER 1996 scaling: upper bound (MW)", + "(l_h_threshold_powers(2))", + physics_variables.l_h_threshold_powers[1], "OP ", ) po.ovarre( self.outfile, - "Fraction of power to the lower divertor", - "(f_p_div_lower)", - physics_variables.f_p_div_lower, - "IP ", + "ITER 1996 scaling: lower bound (MW)", + "(l_h_threshold_powers(3))", + physics_variables.l_h_threshold_powers[2], + "OP ", ) po.ovarre( self.outfile, - "Outboard side heat flux decay length (m)", - "(lambdaio)", - physics_variables.lambdaio, + "ITER 1997 scaling (1) (MW)", + "(l_h_threshold_powers(4))", + physics_variables.l_h_threshold_powers[3], "OP ", ) - if divertor_variables.n_divertors == 2: - po.ovarre( - self.outfile, - "Midplane seperation of the two magnetic closed flux surfaces (m)", - "(drsep)", - physics_variables.drsep, - "OP ", - ) - po.ovarre( self.outfile, - "Fraction of power on the inner targets", - "(fio)", - physics_variables.fio, + "ITER 1997 scaling (2) (MW)", + "(l_h_threshold_powers(5))", + physics_variables.l_h_threshold_powers[4], "OP ", ) po.ovarre( self.outfile, - "Fraction of power incident on the lower inner target", - "(fLI)", - physics_variables.fli, + "Martin 2008 scaling: nominal (MW)", + "(l_h_threshold_powers(6))", + physics_variables.l_h_threshold_powers[5], "OP ", ) po.ovarre( self.outfile, - "Fraction of power incident on the lower outer target", - "(fLO)", - physics_variables.flo, - "OP ", - ) - if divertor_variables.n_divertors == 2: - po.ovarre( - self.outfile, - "Fraction of power incident on the upper inner target", - "(fUI)", - physics_variables.fui, - "OP ", - ) - po.ovarre( - self.outfile, - "Fraction of power incident on the upper outer target", - "(fUO)", - physics_variables.fuo, - "OP ", - ) - - po.ovarre( - self.outfile, - "Power incident on the lower inner target (MW)", - "(pLImw)", - physics_variables.plimw, - "OP ", - ) - po.ovarre( - self.outfile, - "Power incident on the lower outer target (MW)", - "(pLOmw)", - physics_variables.plomw, - "OP ", - ) - if divertor_variables.n_divertors == 2: - po.ovarre( - self.outfile, - "Power incident on the upper innner target (MW)", - "(pUImw)", - physics_variables.puimw, - "OP ", - ) - po.ovarre( - self.outfile, - "Power incident on the upper outer target (MW)", - "(pUOmw)", - physics_variables.puomw, - "OP ", - ) - - po.oblnkl(self.outfile) - po.ovarre( - self.outfile, - "Ohmic heating power (MW)", - "(p_plasma_ohmic_mw)", - physics_variables.p_plasma_ohmic_mw, - "OP ", - ) - po.ovarrf( - self.outfile, - "Fraction of alpha power deposited in plasma", - "(f_p_alpha_plasma_deposited)", - physics_variables.f_p_alpha_plasma_deposited, - "IP", - ) - po.ovarrf( - self.outfile, - "Fraction of alpha power to electrons", - "(f_alpha_electron)", - physics_variables.f_alpha_electron, - "OP ", - ) - po.ovarrf( - self.outfile, - "Fraction of alpha power to ions", - "(f_alpha_ion)", - physics_variables.f_alpha_ion, - "OP ", - ) - po.ovarre( - self.outfile, - "Ion transport (MW)", - "(p_ion_transport_loss_mw)", - physics_variables.p_ion_transport_loss_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "Electron transport (MW)", - "(p_electron_transport_loss_mw)", - physics_variables.p_electron_transport_loss_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "Injection power to ions (MW)", - "(p_hcd_injected_ions_mw)", - current_drive_variables.p_hcd_injected_ions_mw, - "OP ", - ) - po.ovarre( - self.outfile, - "Injection power to electrons (MW)", - "(p_hcd_injected_electrons_mw)", - current_drive_variables.p_hcd_injected_electrons_mw, - "OP ", - ) - if physics_variables.i_plasma_ignited == 1: - po.ocmmnt(self.outfile, " (Injected power only used for start-up phase)") - - po.ovarin( - self.outfile, - "Ignited plasma switch (0=not ignited, 1=ignited)", - "(i_plasma_ignited)", - physics_variables.i_plasma_ignited, - ) - - po.oblnkl(self.outfile) - po.ovarre( - self.outfile, - "Power into divertor zone via charged particles (MW)", - "(p_plasma_separatrix_mw)", - physics_variables.p_plasma_separatrix_mw, - "OP ", - ) - - if physics_variables.p_plasma_separatrix_mw <= 0.001e0: - logger.error( - "Possible problem with high radiation power, forcing p_plasma_separatrix_mw to odd values. " - f"{physics_variables.p_plasma_separatrix_mw=}" - ) - po.oblnkl(self.outfile) - po.ocmmnt( - self.outfile, " BEWARE: possible problem with high radiation power" - ) - po.ocmmnt(self.outfile, " Power into divertor zone is unrealistic;") - po.ocmmnt(self.outfile, " divertor calculations will be nonsense#") - po.ocmmnt( - self.outfile, " Set constraint 17 (Radiation fraction upper limit)." - ) - po.oblnkl(self.outfile) - - if divertor_variables.n_divertors == 2: - # Double null divertor configuration - po.ovarre( - self.outfile, - "Pdivt / R ratio (MW/m) (On peak divertor)", - "(p_div_separatrix_max_mw/physics_variables.rmajor)", - physics_variables.p_div_separatrix_max_mw / physics_variables.rmajor, - "OP ", - ) - po.ovarre( - self.outfile, - "Pdivt Bt / qAR ratio (MWT/m) (On peak divertor)", - "(pdivmaxbt/qar)", - ( - ( - physics_variables.p_div_separatrix_max_mw - * physics_variables.b_plasma_toroidal_on_axis - ) - / ( - physics_variables.q95 - * physics_variables.aspect - * physics_variables.rmajor - ) - ), - "OP ", - ) - else: - # Single null divertor configuration - po.ovarre( - self.outfile, - "Psep / R ratio (MW/m)", - "(p_plasma_separatrix_mw/rmajor)", - physics_variables.p_plasma_separatrix_mw / physics_variables.rmajor, - "OP ", - ) - po.ovarre( - self.outfile, - "Psep Bt / qAR ratio (MWT/m)", - "(pdivtbt_over_qar)", - ( - ( - physics_variables.p_plasma_separatrix_mw - * physics_variables.b_plasma_toroidal_on_axis - ) - / ( - physics_variables.q95 - * physics_variables.aspect - * physics_variables.rmajor - ) - ), - "OP ", - ) - - po.oblnkl(self.outfile) - po.ostars(self.outfile, 110) - po.oblnkl(self.outfile) - - if stellarator_variables.istell == 0: - po.osubhd(self.outfile, "H-mode Power Threshold Scalings :") - - po.ovarre( - self.outfile, - "ITER 1996 scaling: nominal (MW)", - "(l_h_threshold_powers(1))", - physics_variables.l_h_threshold_powers[0], - "OP ", - ) - po.ovarre( - self.outfile, - "ITER 1996 scaling: upper bound (MW)", - "(l_h_threshold_powers(2))", - physics_variables.l_h_threshold_powers[1], - "OP ", - ) - po.ovarre( - self.outfile, - "ITER 1996 scaling: lower bound (MW)", - "(l_h_threshold_powers(3))", - physics_variables.l_h_threshold_powers[2], - "OP ", - ) - po.ovarre( - self.outfile, - "ITER 1997 scaling (1) (MW)", - "(l_h_threshold_powers(4))", - physics_variables.l_h_threshold_powers[3], - "OP ", - ) - po.ovarre( - self.outfile, - "ITER 1997 scaling (2) (MW)", - "(l_h_threshold_powers(5))", - physics_variables.l_h_threshold_powers[4], - "OP ", - ) - po.ovarre( - self.outfile, - "Martin 2008 scaling: nominal (MW)", - "(l_h_threshold_powers(6))", - physics_variables.l_h_threshold_powers[5], - "OP ", - ) - po.ovarre( - self.outfile, - "Martin 2008 scaling: 95% upper bound (MW)", - "(l_h_threshold_powers(7))", - physics_variables.l_h_threshold_powers[6], + "Martin 2008 scaling: 95% upper bound (MW)", + "(l_h_threshold_powers(7))", + physics_variables.l_h_threshold_powers[6], "OP ", ) po.ovarre( @@ -5451,5298 +4460,3202 @@ def outplas(self): ) po.ovarre( self.outfile, - "Snipes 2000 scaling: upper bound (MW)", - "(l_h_threshold_powers(10))", - physics_variables.l_h_threshold_powers[9], - "OP ", - ) - po.ovarre( - self.outfile, - "Snipes 2000 scaling: lower bound (MW)", - "(l_h_threshold_powers(11))", - physics_variables.l_h_threshold_powers[10], - "OP ", - ) - po.ovarre( - self.outfile, - "Snipes 2000 scaling (closed divertor): nominal (MW)", - "(l_h_threshold_powers(12))", - physics_variables.l_h_threshold_powers[11], - "OP ", - ) - po.ovarre( - self.outfile, - "Snipes 2000 scaling (closed divertor): upper bound (MW)", - "(l_h_threshold_powers(13))", - physics_variables.l_h_threshold_powers[12], - "OP ", - ) - po.ovarre( - self.outfile, - "Snipes 2000 scaling (closed divertor): lower bound (MW)", - "(l_h_threshold_powers(14))", - physics_variables.l_h_threshold_powers[13], - "OP ", - ) - po.ovarre( - self.outfile, - "Hubbard 2012 L-I threshold - nominal (MW)", - "(l_h_threshold_powers(15))", - physics_variables.l_h_threshold_powers[14], - "OP ", - ) - po.ovarre( - self.outfile, - "Hubbard 2012 L-I threshold - lower bound (MW)", - "(l_h_threshold_powers(16))", - physics_variables.l_h_threshold_powers[15], - "OP ", - ) - po.ovarre( - self.outfile, - "Hubbard 2012 L-I threshold - upper bound (MW)", - "(l_h_threshold_powers(17))", - physics_variables.l_h_threshold_powers[16], - "OP ", - ) - po.ovarre( - self.outfile, - "Hubbard 2017 L-I threshold", - "(l_h_threshold_powers(18))", - physics_variables.l_h_threshold_powers[17], - "OP ", - ) - po.ovarre( - self.outfile, - "Martin 2008 aspect ratio corrected scaling: nominal (MW)", - "(l_h_threshold_powers(19))", - physics_variables.l_h_threshold_powers[18], - "OP ", - ) - po.ovarre( - self.outfile, - "Martin 2008 aspect ratio corrected scaling: 95% upper bound (MW)", - "(l_h_threshold_powers(20))", - physics_variables.l_h_threshold_powers[19], - "OP ", - ) - po.ovarre( - self.outfile, - "Martin 2008 aspect ratio corrected scaling: 95% lower bound (MW)", - "(l_h_threshold_powers(21))", - physics_variables.l_h_threshold_powers[20], - "OP ", - ) - po.oblnkl(self.outfile) - if physics_variables.i_l_h_threshold in [9, 10, 11]: - if (physics_variables.b_plasma_toroidal_on_axis < 0.78e0) or ( - physics_variables.b_plasma_toroidal_on_axis > 7.94e0 - ): - po.ocmmnt( - self.outfile, - "(b_plasma_toroidal_on_axis outside Snipes 2000 fitted range)", - ) - logger.warning( - "b_plasma_toroidal_on_axis outside Snipes 2000 fitted range" - ) - - if (physics_variables.rminor < 0.15e0) or ( - physics_variables.rminor > 1.15e0 - ): - po.ocmmnt(self.outfile, "(rminor outside Snipes 2000 fitted range)") - logger.warning("rminor outside Snipes 2000 fitted range") - - if (physics_variables.rmajor < 0.55e0) or ( - physics_variables.rmajor > 3.37e0 - ): - po.ocmmnt( - self.outfile, - "(physics_variables.rmajor outside Snipes 2000 fitted range)", - ) - logger.warning("rmajor outside Snipes 2000 fitted range") - - if (physics_variables.nd_plasma_electron_line < 0.09e20) or ( - physics_variables.nd_plasma_electron_line > 3.16e20 - ): - po.ocmmnt( - self.outfile, - "(physics_variables.nd_plasma_electron_line outside Snipes 2000 fitted range)", - ) - logger.warning( - "nd_plasma_electron_line outside Snipes 2000 fitted range" - ) - - if (physics_variables.kappa < 1.0e0) or ( - physics_variables.kappa > 2.04e0 - ): - po.ocmmnt( - self.outfile, - "(physics_variables.kappa outside Snipes 2000 fitted range)", - ) - logger.warning("kappa outside Snipes 2000 fitted range") - - if (physics_variables.triang < 0.07e0) or ( - physics_variables.triang > 0.74e0 - ): - po.ocmmnt(self.outfile, "(triang outside Snipes 2000 fitted range)") - logger.warning("triang outside Snipes 2000 fitted range") - - po.oblnkl(self.outfile) - - if physics_variables.i_l_h_threshold in [12, 13, 14]: - po.ocmmnt( - self.outfile, - "(L-H threshold for closed divertor only. Limited data used in Snipes fit)", - ) - po.oblnkl(self.outfile) - logger.warning("Closed divertor only. Limited data used in Snipes fit") - - if (numerics.ioptimz > 0) and (numerics.active_constraints[14]): - po.ovarre( - self.outfile, - "L-H threshold power (MW)", - "(p_l_h_threshold_mw)", - physics_variables.p_l_h_threshold_mw, - "OP ", - ) - else: - po.ovarre( - self.outfile, - "L-H threshold power (NOT enforced) (MW)", - "(p_l_h_threshold_mw)", - physics_variables.p_l_h_threshold_mw, - "OP ", - ) - - po.osubhd(self.outfile, "Confinement :") - - if physics_variables.i_plasma_ignited == 1: - po.ocmmnt( - self.outfile, - "Device is assumed to be ignited for the calculation of confinement time", - ) - po.oblnkl(self.outfile) - - tauelaw = physics_variables.LABELS_CONFINEMENT_SCALINGS[ - physics_variables.i_confinement_time - ] - - po.ocmmnt( - self.outfile, - f"Confinement scaling law: {tauelaw}", - ) - - po.ovarst( - self.outfile, - "Confinement scaling law", - "(tauelaw)", - f'"{tauelaw.strip().split(" ")[0]}"', - ) - - po.ovarrf( - self.outfile, "Confinement H factor", "(hfact)", physics_variables.hfact - ) - po.ovarrf( - self.outfile, - "Global thermal energy confinement time, from scaling (s)", - "(t_energy_confinement)", - physics_variables.t_energy_confinement, - "OP ", - ) - po.ovarrf( - self.outfile, - "Directly calculated total energy confinement time (s)", - "(t_energy_confinement_beta)", - physics_variables.t_energy_confinement_beta, - "OP ", - ) - po.ocmmnt( - self.outfile, - "(Total thermal energy derived from total plasma beta / loss power)", - ) - po.ovarrf( - self.outfile, - "Ion energy confinement time, from scaling (s)", - "(t_ion_energy_confinement)", - physics_variables.t_ion_energy_confinement, - "OP ", - ) - po.ovarrf( - self.outfile, - "Electron energy confinement time, from scaling (s)", - "(t_electron_energy_confinement)", - physics_variables.t_electron_energy_confinement, - "OP ", - ) - po.ovarre( - self.outfile, - "Fusion double product (s/m3)", - "(ntau)", - physics_variables.ntau, - "OP ", - ) - po.ovarre( - self.outfile, - "Lawson Triple product (keV s/m3)", - "(nTtau)", - physics_variables.nTtau, - "OP ", - ) - po.ovarre( - self.outfile, - "Transport loss power assumed in scaling law (MW)", - "(p_plasma_loss_mw)", - physics_variables.p_plasma_loss_mw, - "OP ", - ) - po.ovarin( - self.outfile, - "Switch for radiation loss term usage in power balance", - "(i_rad_loss)", - physics_variables.i_rad_loss, - ) - if physics_variables.i_rad_loss == 0: - po.ovarre( - self.outfile, - "Radiation power subtracted from plasma power balance (MW)", - "", - physics_variables.p_plasma_rad_mw, - "OP ", - ) - po.ocmmnt(self.outfile, " (Radiation correction is total radiation power)") - elif physics_variables.i_rad_loss == 1: - po.ovarre( - self.outfile, - "Radiation power subtracted from plasma power balance (MW)", - "", - physics_variables.p_plasma_inner_rad_mw, - "OP ", - ) - po.ocmmnt(self.outfile, " (Radiation correction is core radiation power)") - else: - po.ovarre( - self.outfile, - "Radiation power subtracted from plasma power balance (MW)", - "", - 0.0e0, - ) - po.ocmmnt(self.outfile, " (No radiation correction applied)") - po.ovarrf( - self.outfile, - "H* non-radiation corrected", - "(hstar)", - physics_variables.hstar, - "OP", - ) - po.ocmmnt(self.outfile, " (H* assumes IPB98(y,2), ELMy H-mode scaling)") - po.ovarrf( - self.outfile, - "Alpha particle confinement time (s)", - "(t_alpha_confinement)", - physics_variables.t_alpha_confinement, - "OP ", - ) - # Note alpha confinement time is no longer equal to fuel particle confinement time. - po.ovarrf( - self.outfile, - "Alpha particle/energy confinement time ratio", - "(f_alpha_energy_confinement)", - physics_variables.f_alpha_energy_confinement, - "OP ", - ) - po.ovarrf( - self.outfile, - "Lower limit on f_alpha_energy_confinement", - "(f_alpha_energy_confinement_min)", - constraint_variables.f_alpha_energy_confinement_min, - ) - - # Plot table of al the H-factor scalings and coparison values - self.output_confinement_comparison(istell=stellarator_variables.istell) - - if stellarator_variables.istell == 0: - # Issues 363 Output dimensionless plasma parameters MDK - po.osubhd(self.outfile, "Dimensionless plasma parameters") - po.ocmmnt(self.outfile, "For definitions see") - po.ocmmnt( - self.outfile, - "Recent progress on the development and analysis of the ITPA global H-mode confinement database", - ) - po.ocmmnt( - self.outfile, - "D.C. McDonald et al, 2007 Nuclear Fusion v47, 147. (nu_star missing 1/mu0)", - ) - po.ovarre( - self.outfile, - "Normalized plasma pressure beta as defined by McDonald et al", - "(beta_mcdonald)", - physics_variables.beta_mcdonald, - "OP ", - ) - po.ovarre( - self.outfile, - "Normalized ion Larmor radius", - "(rho_star)", - physics_variables.rho_star, - "OP ", - ) - po.ovarre( - self.outfile, - "Normalized collisionality", - "(nu_star)", - physics_variables.nu_star, - "OP ", - ) - po.ovarre( - self.outfile, - "ITER Physics Basis definition of elongation", - "(kappa_ipb)", - physics_variables.kappa_ipb, - "OP ", - ) - - po.oblnkl(self.outfile) - po.ostars(self.outfile, 110) - po.oblnkl(self.outfile) - - self.inductance.output_volt_second_information() - - self.bootstrap.output_bootstrap_current_information() - - po.osubhd(self.outfile, "Fuelling :") - po.ovarre( - self.outfile, - "Ratio of He and pellet particle confinement times", - "(tauratio)", - physics_variables.tauratio, - ) - po.ovarre( - self.outfile, - "Fuelling rate (nucleus-pairs/s)", - "(molflow_plasma_fuelling_required)", - physics_variables.molflow_plasma_fuelling_required, - "OP ", - ) - po.ovarre( - self.outfile, - "Fuel burn-up rate (reactions/s)", - "(rndfuel)", - physics_variables.rndfuel, - "OP ", - ) - po.ovarrf( - self.outfile, - "Burn-up fraction", - "(burnup)", - physics_variables.burnup, - "OP ", - ) - - if 78 in numerics.icc: - po.osubhd(self.outfile, "Reinke Criterion :") - po.ovarin( - self.outfile, - "index of impurity to be iterated for divertor detachment", - "(impvardiv)", - reinke_variables.impvardiv, - ) - po.ovarre( - self.outfile, - "Minimum Impurity fraction from Reinke", - "(fzmin)", - reinke_variables.fzmin, - "OP ", - ) - po.ovarre( - self.outfile, - "Actual Impurity fraction", - "(fzactual)", - reinke_variables.fzactual, - ) - - def output_confinement_comparison(self, istell: int): - """Routine to calculate ignition margin for different confinement scalings and equivalent confinement times for H=1. - - This routine calculates the ignition margin at the final point with different scalings and outputs the results to a file. - - The output includes: - - Energy confinement times - - Required H-factors for power balance - - The routine iterates over a range of confinement times, skipping the first user input and a specific index (25). For each confinement time, it calculates various parameters related to confinement and ignition using the `calculate_confinement_time` method. It then calculates the H-factor for when the plasma is ignited using the `find_other_h_factors` method and writes the results to the output file. - - Output format: - - Header: "Energy confinement times, and required H-factors :" - - Columns: "Scaling law", "Confinement time [s]", "H-factor for power balance" - - Methods used: - - `calculate_confinement_time`: Calculates confinement-related parameters. - - `find_other_h_factors`: Calculates the H-factor for a given confinement time. - - Parameters - ---------- - istell : - Indicator for stellarator (0 for tokamak, >=1 for stellarator). - - """ - - po.oheadr(self.outfile, "Energy confinement times, and required H-factors :") - po.ocmmnt( - self.outfile, - f"{'':>2}{'Scaling law':<27}{'Electron confinement time [s]':<32}Equivalent H-factor for", - ) - po.ocmmnt( - self.outfile, - f"{'':>38}{'for H = 1':<23}same confinement time", - ) - po.oblnkl(self.outfile) - - # List of key values for stellarator scalings - stellarator_scalings = [21, 22, 23, 37, 38] - - # Plot all of the confinement scalings for comparison when H = 1 - # Start from range 1 as the first i_confinement_time is a user input - # If stellarator, use the stellarator scalings - for i_confinement_time in ( - range(1, physics_variables.N_CONFINEMENT_SCALINGS) - if istell == 0 - else stellarator_scalings - ): - if i_confinement_time == 25: - continue - ( - _, - _, - taueez, - _, - _, - _, - ) = self.calculate_confinement_time( - physics_variables.m_fuel_amu, - physics_variables.p_alpha_total_mw, - physics_variables.aspect, - physics_variables.b_plasma_toroidal_on_axis, - physics_variables.nd_plasma_ions_total_vol_avg, - physics_variables.nd_plasma_electrons_vol_avg, - physics_variables.nd_plasma_electron_line, - physics_variables.eps, - 1.0, - i_confinement_time, - physics_variables.i_plasma_ignited, - physics_variables.kappa, - physics_variables.kappa95, - physics_variables.p_non_alpha_charged_mw, - current_drive_variables.p_hcd_injected_total_mw, - physics_variables.plasma_current, - physics_variables.pden_plasma_core_rad_mw, - physics_variables.rmajor, - physics_variables.rminor, - physics_variables.temp_plasma_electron_density_weighted_kev, - physics_variables.temp_plasma_ion_density_weighted_kev, - physics_variables.q95, - physics_variables.qstar, - physics_variables.vol_plasma, - physics_variables.n_charge_plasma_effective_vol_avg, - ) - - try: - # Calculate the H-factor for the same confinement time in other scalings - physics_variables.hfac[i_confinement_time - 1] = ( - self.find_other_h_factors(i_confinement_time) - ) - except ValueError: - # This is only used for a table in the OUT.DAT so if it fails - # just write a NaN--its not worth crashing PROCESS over. - physics_variables.hfac[i_confinement_time - 1] = np.nan - - po.ocmmnt( - self.outfile, - f"{'':>2}{physics_variables.LABELS_CONFINEMENT_SCALINGS[i_confinement_time]:<38}" - f"{taueez:<28.3f}{physics_variables.hfac[i_confinement_time - 1]:.3f}", - ) - - po.oblnkl(self.outfile) - po.ostars(self.outfile, 110) - - @staticmethod - def bootstrap_fraction_iter89( - aspect: float, - beta: float, - b_plasma_toroidal_on_axis: float, - plasma_current: float, - q95: float, - q0: float, - rmajor: float, - vol_plasma: float, - ) -> float: - """Calculate the bootstrap-driven fraction of the plasma current. - - This function performs the original ITER calculation of the plasma current bootstrap fraction. - - Parameters - ---------- - aspect : float - Plasma aspect ratio. - beta : float - Plasma total beta. - b_plasma_toroidal_on_axis : float - Toroidal field on axis (T). - plasma_current : float - Plasma current (A). - q95 : float - Safety factor at 95% surface. - q0 : float - Central safety factor. - rmajor : float - Plasma major radius (m). - vol_plasma : float - Plasma volume (m3). - - Returns - ------- - float - The bootstrap-driven fraction of the plasma current. - - Reference: - - ITER Physics Design Guidelines: 1989 [IPDG89], N. A. Uckan et al, - - ITER Documentation Series No.10, IAEA/ITER/DS/10, IAEA, Vienna, 1990 - - """ - - # Calculate the bootstrap current coefficient - c_bs = 1.32 - 0.235 * (q95 / q0) + 0.0185 * (q95 / q0) ** 2 - - # Calculate the average minor radius - average_a = np.sqrt(vol_plasma / (2 * np.pi**2 * rmajor)) - - b_pa = (plasma_current / 1e6) / (5 * average_a) - - # Calculate the poloidal beta for bootstrap current - betapbs = beta * (b_plasma_toroidal_on_axis / b_pa) ** 2 - - # Ensure betapbs is positive - if betapbs <= 0.0: - return 0.0 - - # Calculate and return the bootstrap current fraction - return c_bs * (betapbs / np.sqrt(aspect)) ** 1.3 - - @staticmethod - def bootstrap_fraction_wilson( - alphaj: float, - alphap: float, - alphat: float, - betpth: float, - q0: float, - q95: float, - rmajor: float, - rminor: float, - ) -> float: - """Bootstrap current fraction from Wilson et al scaling - - This function calculates the bootstrap current fraction using the numerically fitted algorithm written by Howard Wilson. - - Parameters - ---------- - alphaj : float - Current profile index. - alphap : float - Pressure profile index. - alphat : float - Temperature profile index. - betpth : float - Thermal component of poloidal beta. - q0 : float - Safety factor on axis. - q95 : float - Edge safety factor. - rmajor : float - Major radius (m). - rminor : float - Minor radius (m). - - Returns - ------- - float - The bootstrap current fraction. - - Reference: - - AEA FUS 172 - Physics Assessment for the European Reactor Study, 1989 - - AEA FUS 172 - Physics Assessment for the European Reactor Study, 1989 - - H. R. Wilson, Nuclear Fusion 32 (1992) 257 - - """ - - term1 = np.log(0.5) - term2 = np.log(q0 / q95) - - # Re-arranging of parabolic profile to be equal to (r/a)^2 where the profile value is half of the core value - - termp = 1.0 - 0.5 ** (1.0 / alphap) - termt = 1.0 - 0.5 ** (1.0 / alphat) - termj = 1.0 - 0.5 ** (1.0 / alphaj) - - # Assuming a parabolic safety factor profile of the form q = q0 + (q95 - q0) * (r/a)^2 - # Substitute (r/a)^2 term from temperature,pressure and current profiles into q profile when values is 50% of core value - # Take natural log of q profile over q95 and q0 to get the profile index - - alfpnw = term1 / np.log(np.log((q0 + (q95 - q0) * termp) / q95) / term2) - alftnw = term1 / np.log(np.log((q0 + (q95 - q0) * termt) / q95) / term2) - aj = term1 / np.log(np.log((q0 + (q95 - q0) * termj) / q95) / term2) - - # Crude check for NaN errors or other illegal values. - if np.isnan(aj) or np.isnan(alfpnw) or np.isnan(alftnw) or aj < 0: - raise ProcessValueError( - "Illegal profile value found", aj=aj, alfpnw=alfpnw, alftnw=alftnw - ) - - # Ratio of ionic charge to electron charge - - z = 1.0 - - # Inverse aspect ratio: r2 = maximum plasma radius, r1 = minimum - # This is the definition used in the paper - r2 = rmajor + rminor - r1 = rmajor - rminor - eps1 = (r2 - r1) / (r2 + r1) - - # Coefficients fitted using least squares techniques - - # Square root of current profile index term - saj = np.sqrt(aj) - - a = np.array([ - 1.41 * (1.0 - 0.28 * saj) * (1.0 + 0.12 / z), - 0.36 * (1.0 - 0.59 * saj) * (1.0 + 0.8 / z), - -0.27 * (1.0 - 0.47 * saj) * (1.0 + 3.0 / z), - 0.0053 * (1.0 + 5.0 / z), - -0.93 * (1.0 - 0.34 * saj) * (1.0 + 0.15 / z), - -0.26 * (1.0 - 0.57 * saj) * (1.0 - 0.27 * z), - 0.064 * (1.0 - 0.6 * aj + 0.15 * aj * aj) * (1.0 + 7.6 / z), - -0.0011 * (1.0 + 9.0 / z), - -0.33 * (1.0 - aj + 0.33 * aj * aj), - -0.26 * (1.0 - 0.87 / saj - 0.16 * aj), - -0.14 * (1.0 - 1.14 / saj - 0.45 * saj), - -0.0069, - ]) - - seps1 = np.sqrt(eps1) - - b = np.array([ - 1.0, - alfpnw, - alftnw, - alfpnw * alftnw, - seps1, - alfpnw * seps1, - alftnw * seps1, - alfpnw * alftnw * seps1, - eps1, - alfpnw * eps1, - alftnw * eps1, - alfpnw * alftnw * eps1, - ]) - - # Empirical bootstrap current fraction - return seps1 * betpth * (a * b).sum() - - @staticmethod - def bootstrap_fraction_nevins( - alphan: float, - alphat: float, - beta_toroidal: float, - b_plasma_toroidal_on_axis: float, - nd_plasma_electrons_vol_avg: float, - plasma_current: float, - q95: float, - q0: float, - rmajor: float, - rminor: float, - te: float, - zeff: float, - ) -> float: - """Calculate the bootstrap current fraction from Nevins et al scaling. - - This function calculates the bootstrap current fraction using the Nevins et al method. - - Parameters - ---------- - alphan : float - Density profile index. - alphat : float - Temperature profile index. - beta_toroidal : float - Toroidal plasma beta. - b_plasma_toroidal_on_axis : float - Toroidal field on axis (T). - nd_plasma_electrons_vol_avg : float - Electron density (/m3). - plasma_current : float - Plasma current (A). - q0 : float - Central safety factor. - q95 : float - Safety factor at 95% surface. - rmajor : float - Plasma major radius (m). - rminor : float - Plasma minor radius (m). - te : float - Volume averaged plasma temperature (keV). - zeff : float - Plasma effective charge. - - Returns - ------- - float - The bootstrap current fraction. - - Reference: See appendix of: - Keii Gi, Makoto Nakamura, Kenji Tobita, Yasushi Ono, - Bootstrap current fraction scaling for a tokamak reactor design study, - Fusion Engineering and Design, Volume 89, Issue 11, 2014, Pages 2709-2715, - ISSN 0920-3796, https://doi.org/10.1016/j.fusengdes.2014.07.009. - Nevins, W. M. "Summary report: ITER specialists' meeting on heating and current drive." - ITER-TN-PH-8-4, June 1988. 1988. - - """ - # Calculate peak electron beta at plasma centre, this is not the form used in the paper - # The paper assumes parabolic profiles for calculating core values with the profile indexes. - # We instead use the directly calculated electron density and temperature values at the core. - # So that it is compatible with all profiles - - betae0 = ( - physics_variables.nd_plasma_electron_on_axis - * physics_variables.temp_plasma_electron_on_axis_kev - * 1.0e3 - * constants.ELECTRON_CHARGE - / (b_plasma_toroidal_on_axis**2 / (2.0 * constants.RMU0)) - ) - - # Call integration routine using definite integral routine from scipy - - ainteg, _ = integrate.quad( - lambda y: _nevins_integral( - y, - nd_plasma_electrons_vol_avg, - te, - b_plasma_toroidal_on_axis, - rminor, - rmajor, - zeff, - alphat, - alphan, - q0, - q95, - beta_toroidal, - ), - 0, # Lower bound - 1.0, # Upper bound - ) - - # Calculate bootstrap current and fraction - - aibs = 2.5 * betae0 * rmajor * b_plasma_toroidal_on_axis * q95 * ainteg - return 1.0e6 * aibs / plasma_current - - @staticmethod - def bootstrap_fraction_sauter(plasma_profile: float) -> float: - """Calculate the bootstrap current fraction from the Sauter et al scaling. - - This function calculates the bootstrap current fraction using the Sauter, Angioni, and Lin-Liu scaling. - - Parameters - ---------- - plasma_profile : PlasmaProfile - The plasma profile object. - - Returns - ------- - float - The bootstrap current fraction. - - Reference: - - O. Sauter, C. Angioni, Y. R. Lin-Liu; - Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime. - Phys. Plasmas 1 July 1999; 6 (7): 2834-2839. https://doi.org/10.1063/1.873240 - - O. Sauter, C. Angioni, Y. R. Lin-Liu; Erratum: - Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime - [Phys. Plasmas 6, 2834 (1999)]. Phys. Plasmas 1 December 2002; 9 (12): 5140. https://doi.org/10.1063/1.1517052 - Note: - The code was supplied by Emiliano Fable, IPP Garching (private communication). - - """ - - # Radial points from 0 to 1 seperated by 1/profile_size - roa = plasma_profile.neprofile.profile_x - - # Local circularised minor radius - rho = np.sqrt(physics_variables.a_plasma_poloidal / np.pi) * roa - - # Square root of local aspect ratio - sqeps = np.sqrt(roa * (physics_variables.rminor / physics_variables.rmajor)) - - # Calculate electron and ion density profiles - ne = plasma_profile.neprofile.profile_y * 1e-19 - ni = ( - physics_variables.nd_plasma_ions_total_vol_avg - / physics_variables.nd_plasma_electrons_vol_avg - ) * ne - - # Calculate electron and ion temperature profiles - tempe = plasma_profile.teprofile.profile_y - tempi = ( - physics_variables.temp_plasma_ion_vol_avg_kev - / physics_variables.temp_plasma_electron_vol_avg_kev - ) * tempe - - # Flat Zeff profile assumed - # Return tempi like array object filled with zeff - zeff = np.full_like(tempi, physics_variables.n_charge_plasma_effective_vol_avg) - - # inverse_q = 1/safety factor - # Parabolic q profile assumed - inverse_q = 1 / ( - physics_variables.q0 - + (physics_variables.q95 - physics_variables.q0) * roa**2 - ) - # Create new array of average mass of fuel portion of ions - amain = np.full_like(inverse_q, physics_variables.m_ions_total_amu) - - # Create new array of average main ion charge - zmain = np.full_like(inverse_q, 1.0 + physics_variables.f_plasma_fuel_helium3) - - # Calculate total bootstrap current (MA) by summing along profiles - # Looping from 2 because _calculate_l31_coefficient() etc should return 0 @ j == 1 - radial_elements = np.arange(2, plasma_profile.profile_size) - - # Change in localised minor radius to be used as delta term in derivative - drho = rho[radial_elements] - rho[radial_elements - 1] - - # Area of annulus, assuming circular plasma cross-section - da = 2 * np.pi * rho[radial_elements - 1] * drho # area of annulus - - # Create the partial derivatives using numpy gradient (central differences) - dlogte_drho = np.gradient(np.log(tempe), rho)[radial_elements - 1] - dlogti_drho = np.gradient(np.log(tempi), rho)[radial_elements - 1] - dlogne_drho = np.gradient(np.log(ne), rho)[radial_elements - 1] - - jboot = ( - 0.5 - * ( - _calculate_l31_coefficient( - radial_elements, - plasma_profile.profile_size, - physics_variables.rmajor, - physics_variables.b_plasma_toroidal_on_axis, - physics_variables.triang, - ne, - ni, - tempe, - tempi, - inverse_q, - rho, - zeff, - sqeps, - ) - * dlogne_drho - + _calculate_l31_32_coefficient( - radial_elements, - plasma_profile.profile_size, - physics_variables.rmajor, - physics_variables.b_plasma_toroidal_on_axis, - physics_variables.triang, - ne, - ni, - tempe, - tempi, - inverse_q, - rho, - zeff, - sqeps, - ) - * dlogte_drho - + _calculate_l34_alpha_31_coefficient( - radial_elements, - plasma_profile.profile_size, - physics_variables.rmajor, - physics_variables.b_plasma_toroidal_on_axis, - physics_variables.triang, - inverse_q, - sqeps, - tempi, - tempe, - amain, - zmain, - ni, - ne, - rho, - zeff, - ) - * dlogti_drho - ) - * 1.0e6 - * ( - -physics_variables.b_plasma_toroidal_on_axis - / (0.2 * np.pi * physics_variables.rmajor) - * rho[radial_elements - 1] - * inverse_q[radial_elements - 1] - ) - ) # A/m2 - - return (np.sum(da * jboot, axis=0) / physics_variables.plasma_current), jboot - - @staticmethod - def bootstrap_fraction_sakai( - beta_poloidal: float, - q95: float, - q0: float, - alphan: float, - alphat: float, - eps: float, - ind_plasma_internal_norm: float, - ) -> float: - """Calculate the bootstrap fraction using the Sakai formula. - - Parameters - ---------- - beta_poloidal : - Plasma poloidal beta. - q95 : - Safety factor at 95% of the plasma radius. - q0 : - Safety factor at the magnetic axis. - alphan : - Density profile index - alphat : - Temperature profile index - eps : - Inverse aspect ratio. - ind_plasma_internal_norm : - Plasma normalised internal inductance - - Returns - ------- - : - The calculated bootstrap fraction. - - Notes - ----- - The profile assumed for the alphan and alpat indexes is only a parabolic profile without a pedestal (L-mode). - The Root Mean Squared Error for the fitting database of this formula was 0.025 - Concentrating on the positive shear plasmas using the ACCOME code equilibria with the fully non-inductively driven - conditions with neutral beam (NB) injection only are calculated. - The electron temperature and the ion temperature were assumed to be equal - This can be used for all apsect ratios. - The diamagnetic fraction is included in this formula. - - References - ---------- - Ryosuke Sakai, Takaaki Fujita, Atsushi Okamoto, Derivation of bootstrap current fraction scaling formula for 0-D system code analysis, - Fusion Engineering and Design, Volume 149, 2019, 111322, ISSN 0920-3796, - https://doi.org/10.1016/j.fusengdes.2019.111322. - - """ - # Sakai states that the ACCOME dataset used has the toridal diamagnetic current included in the bootstrap current - # So the diamganetic current should not be calculated with this. i_diamagnetic_current = 0 - return ( - 10 ** (0.951 * eps - 0.948) - * beta_poloidal ** (1.226 * eps + 1.584) - * ind_plasma_internal_norm ** (-0.184 * eps - 0.282) - * (q95 / q0) ** (-0.042 * eps - 0.02) - * alphan ** (0.13 * eps + 0.05) - * alphat ** (0.502 * eps - 0.273) - ) - - @staticmethod - def bootstrap_fraction_aries( - beta_poloidal: float, - ind_plasma_internal_norm: float, - core_density: float, - average_density: float, - inverse_aspect: float, - ) -> float: - """Calculate the bootstrap fraction using the ARIES formula. - Parameters - ---------- - beta_poloidal : - Plasma poloidal beta. - ind_plasma_internal_norm : - Plasma normalized internal inductance. - core_density : - Core plasma density. - average_density : - Average plasma density. - inverse_aspect : - Inverse aspect ratio. - - Returns - ------- - : - The calculated bootstrap fraction. - - Notes - ----- - - The source reference does not provide any info about the derivation of the formula. It is only stated - - References - ---------- - - Zoran Dragojlovic et al., “An advanced computational algorithm for systems analysis of tokamak power plants,” - Fusion Engineering and Design, vol. 85, no. 2, pp. 243-265, Apr. 2010, - doi: https://doi.org/10.1016/j.fusengdes.2010.02.015. - - """ - # Using the standard variable naming from the ARIES paper - a_1 = ( - 1.10 - 1.165 * ind_plasma_internal_norm + 0.47 * ind_plasma_internal_norm**2 - ) - b_1 = ( - 0.806 - - 0.885 * ind_plasma_internal_norm - + 0.297 * ind_plasma_internal_norm**2 - ) - - c_bs = a_1 + b_1 * (core_density / average_density) - - return c_bs * np.sqrt(inverse_aspect) * beta_poloidal - - @staticmethod - def bootstrap_fraction_andrade( - beta_poloidal: float, - core_pressure: float, - average_pressure: float, - inverse_aspect: float, - ) -> float: - """Calculate the bootstrap fraction using the Andrade et al formula. - - Parameters - ---------- - beta_poloidal : - Plasma poloidal beta. - core_pressure : - Core plasma pressure. - average_pressure : - Average plasma pressure. - inverse_aspect : - Inverse aspect ratio. - - Returns - ------- - : - The calculated bootstrap fraction. - - Notes - ----- - - Based off 350 plasma profiles from Experimento Tokamak Esferico (ETE) spherical tokamak - - A = 1.5, R_0 = 0.3m, I_p = 200kA, B_0=0.4T, beta = 4-10%. Profiles taken as Gaussian shaped functions. - - Errors mostly up to the order of 10% are obtained when both expressions are compared with the equilibrium estimates for the - bootstrap current in ETE - - References - ---------- - - M. C. R. Andrade and G. O. Ludwig, “Scaling of bootstrap current on equilibrium and plasma profile parameters in tokamak plasmas,” - Plasma Physics and Controlled Fusion, vol. 50, no. 6, pp. 065001-065001, Apr. 2008, - doi: https://doi.org/10.1088/0741-3335/50/6/065001. - - """ - - # Using the standard variable naming from the Andrade et.al. paper - c_p = core_pressure / average_pressure - - # Error +- 0.0007 - c_bs = 0.2340 - - return c_bs * np.sqrt(inverse_aspect) * beta_poloidal * c_p**0.8 - - @staticmethod - def bootstrap_fraction_hoang( - beta_poloidal: float, - pressure_index: float, - current_index: float, - inverse_aspect: float, - ) -> float: - """Calculate the bootstrap fraction using the Hoang et al formula. - Parameters - ---------- - beta_poloidal : - Plasma poloidal beta. - pressure_index : - Pressure profile index. - current_index : - Current profile index. - inverse_aspect : - Inverse aspect ratio. - - Returns - ------- - : - The calculated bootstrap fraction. - - Notes - ----- - - Based off of TFTR data calculated using the TRANSP plasma analysis code - - 170 discharges which was assembled to study the tritium influx and transport in discharges with D-only neutral beam - injection (NBI) - - Contains L-mode, supershots, reversed shear, enhanced reversed shear and increased li discharges - - Discharges with monotonic flux profiles with reversed shear are also included - - Is applied to circular cross-section plasmas - - References - ---------- - - G. T. Hoang and R. V. Budny, “The bootstrap fraction in TFTR,” AIP conference proceedings, - Jan. 1997, doi: https://doi.org/10.1063/1.53414. - - """ - - # Using the standard variable naming from the Hoang et.al. paper - # Hoang et.al uses a different definition for the profile indexes such that - # alpha_p is defined as the ratio of the central and the volume-averaged values, and the peakedness of the density of the total plasma current - # (defined as ratio of the central value and I_p), alpha_j$ - - # We assume the pressure and current profile is parabolic and use the (profile_index +1) term in lieu - # The term represents the ratio of the the core to volume averaged value - - # This could lead to large changes in the value depending on interpretation of the profile index - - c_bs = np.sqrt((pressure_index + 1) / (current_index + 1)) - - return 0.4 * np.sqrt(inverse_aspect) * beta_poloidal**0.9 * c_bs - - @staticmethod - def bootstrap_fraction_wong( - beta_poloidal: float, - density_index: float, - temperature_index: float, - inverse_aspect: float, - elongation: float, - ) -> float: - """Calculate the bootstrap fraction using the Wong et al formula. - Parameters - ---------- - beta_poloidal : - Plasma poloidal beta. - density_index : - Density profile index. - temperature_index : - Temperature profile index. - inverse_aspect : - Inverse aspect ratio. - elongation : - Plasma elongation. - - Returns - ------- - : - The calculated bootstrap fraction. - - Notes - ----- - - Data is based off of equilibria from Miller et al. - - A: 1.2 - 3.0 and stable to n ballooning and low n kink modes at a bootstrap fraction of 99% for kappa = 2, 2.5 and 3 - - The results were parameterized as a function of aspect ratio and elongation - - The parametric dependency of beta_p and beta_T are based on fitting of the DIII-D high equivalent DT yield results - - Parabolic profiles should be used for best results as the pressure peaking value is calculated as the product of a parabolic - temperature and density profile - - References - ---------- - - C.-P. Wong, J. C. Wesley, R. D. Stambaugh, and E. T. Cheng, “Toroidal reactor designs as a function of aspect ratio and elongation,” - vol. 42, no. 5, pp. 547-556, May 2002, doi: https://doi.org/10.1088/0029-5515/42/5/307. - - - Miller, R L, "Stable bootstrap-current driven equilibria for low aspect ratio tokamaks". - Switzerland: N. p., 1996. Web.https://fusion.gat.com/pubs-ext/MISCONF96/A22433.pdf - - """ - # Using the standard variable naming from the Wong et.al. paper - f_peak = 2.0 / scipy.special.beta(0.5, density_index + temperature_index + 1) - - c_bs = 0.773 + 0.019 * elongation - - return c_bs * f_peak**0.25 * beta_poloidal * np.sqrt(inverse_aspect) - - @staticmethod - def bootstrap_fraction_gi_I( # noqa: N802 - beta_poloidal: float, - pressure_index: float, - temperature_index: float, - inverse_aspect: float, - effective_charge: float, - q95: float, - q0: float, - ) -> float: - """Calculate the bootstrap fraction using the first scaling from the Gi et al formula. - - Parameters - ---------- - beta_poloidal : - Plasma poloidal beta. - pressure_index : - Pressure profile index. - temperature_index : - Temperature profile index. - inverse_aspect : - Inverse aspect ratio. - effective_charge : - Plasma effective charge. - q95 : - Safety factor at 95% of the plasma radius. - q0 : - Safety factor at the magnetic axis. - - Returns - ------- - : - The calculated bootstrap fraction. - - Notes - ----- - - Scaling found by solving the Hirshman-Sigmar bootstrap modelusing the matrix inversion method - - Method was done to put the scaling into parameters compatible with the TPC systems code - - Uses the ACCOME code to create bootstrap current fractions without using the itrative calculations of the - current drive and equilibrium models in the scan - - R = 5.0 m, A = 1.3 - 5.0, kappa = 2, traing = 0.3, alpha_n = 0.1 - 0.8, alpha_t = 1.0 - 3.0, Z_eff = 1.2 - 3.0 - - Uses parabolic plasma profiles only. - - Scaling 1 has better accuracy than Scaling 2. However, Scaling 1 overestimated the f_BS value for reversed shear - equilibrium. - - References - ---------- - - K. Gi, M. Nakamura, Kenji Tobita, and Y. Ono, “Bootstrap current fraction scaling for a tokamak reactor design study,” - Fusion Engineering and Design, vol. 89, no. 11, pp. 2709-2715, Aug. 2014, - doi: https://doi.org/10.1016/j.fusengdes.2014.07.009. - - """ - - # Using the standard variable naming from the Gi et.al. paper - - c_bs = ( - 0.474 - * inverse_aspect**-0.1 - * pressure_index**0.974 - * temperature_index**-0.416 - * effective_charge**0.178 - * (q95 / q0) ** -0.133 - ) - - return c_bs * np.sqrt(inverse_aspect) * beta_poloidal - - @staticmethod - def bootstrap_fraction_gi_II( # noqa: N802 - beta_poloidal: float, - pressure_index: float, - temperature_index: float, - inverse_aspect: float, - effective_charge: float, - ) -> float: - """Calculate the bootstrap fraction using the second scaling from the Gi et al formula. - - Parameters - ---------- - beta_poloidal : - Plasma poloidal beta. - pressure_index : - Pressure profile index. - temperature_index : - Temperature profile index. - inverse_aspect : - Inverse aspect ratio. - effective_charge : - Plasma effective charge. - - Returns - ------- - : - The calculated bootstrap fraction. - - Notes - ----- - - Scaling found by solving the Hirshman-Sigmar bootstrap modelusing the matrix inversion method - - Method was done to put the scaling into parameters compatible with the TPC systems code - - Uses the ACCOME code to create bootstrap current fractions without using the itrative calculations of the - curent drive and equilibrium models in the scan - - R = 5.0 m, A = 1.3 - 5.0, kappa = 2, traing = 0.3, alpha_n = 0.1 - 0.8, alpha_t = 1.0 - 3.0, Z_eff = 1.2 - 3.0 - - Uses parabolic plasma profiles only. - - This scaling has the q profile dependance removed to obtain a scaling formula with much more flexible variables than - that by a single profile factor for internal current profile. - - References - ---------- - - K. Gi, M. Nakamura, Kenji Tobita, and Y. Ono, “Bootstrap current fraction scaling for a tokamak reactor design study,” - Fusion Engineering and Design, vol. 89, no. 11, pp. 2709-2715, Aug. 2014, - doi: https://doi.org/10.1016/j.fusengdes.2014.07.009. - - """ - - # Using the standard variable naming from the Gi et.al. paper - - c_bs = ( - 0.382 - * inverse_aspect**-0.242 - * pressure_index**0.974 - * temperature_index**-0.416 - * effective_charge**0.178 - ) - - return c_bs * np.sqrt(inverse_aspect) * beta_poloidal - - @staticmethod - def bootstrap_fraction_sugiyama_l_mode( - eps: float, - beta_poloidal: float, - alphan: float, - alphat: float, - zeff: float, - q95: float, - q0: float, - ) -> float: - """Calculate the bootstrap fraction using the L-mode scaling from the Sugiyama et al formula. - - Parameters - ---------- - eps : float - Inverse aspect ratio. - beta_poloidal : float - Plasma poloidal beta. - alphan : float - Density profile index. - alphat : float - Temperature profile index. - zeff : float - Plasma effective charge. - q95 : float - Safety factor at 95% of the plasma radius. - q0 : float - Safety factor at the magnetic axis. - - Returns - ------- - float - The calculated bootstrap fraction. - - Notes - ----- - - This scaling is derived for L-mode plasmas. - - Ion and electron temperature are the same - - Z_eff has a uniform profile, with only fully stripped carbon impurity - - References - ---------- - - S. Sugiyama, T. Goto, H. Utoh, and Y. Sakamoto, “Improvement of core plasma power and - current balance models for tokamak systems code considering H-mode plasma profiles,” - Fusion Engineering and Design, vol. 216, p. 115022, Jul. 2025, doi: - https://doi.org/10.1016/j.fusengdes.2025.115022. - - """ - - return ( - 0.740 - * eps**0.418 - * beta_poloidal**0.904 - * alphan**0.06 - * alphat**-0.138 - * zeff**0.230 - * (q95 / q0) ** -0.142 - ) - - @staticmethod - def bootstrap_fraction_sugiyama_h_mode( - eps: float, - beta_poloidal: float, - alphan: float, - alphat: float, - tbeta: float, - zeff: float, - q95: float, - q0: float, - radius_plasma_pedestal_density_norm: float, - nd_plasma_pedestal_electron: float, - n_greenwald: float, - temp_plasma_pedestal_kev: float, - ) -> float: - """Calculate the bootstrap fraction using the H-mode scaling from the Sugiyama et al formula. - - Parameters - ---------- - eps : float - Inverse aspect ratio. - beta_poloidal : float - Plasma poloidal beta. - alphan : float - Density profile index. - alphat : float - Temperature profile index. - tbeta : float - Second temperature profile index. - zeff : float - Plasma effective charge. - q95 : float - Safety factor at 95% of the plasma radius. - q0 : float - Safety factor at the magnetic axis. - radius_plasma_pedestal_density_norm : float - Normalised plasma radius of density pedestal. - nd_plasma_pedestal_electron : float - Electron number density at the pedestal [m^-3]. - n_greenwald : float - Greenwald density limit [m^-3]. - temp_plasma_pedestal_kev : float - Electron temperature at the pedestal [keV]. - - Returns - ------- - float - The calculated bootstrap fraction. - - Notes - ----- - - This scaling is derived for H-mode plasmas. - - The temperature and density pedestal positions are the same - - Separatrix temperature and density are zero - - Ion and electron temperature are the same - - Z_eff has a uniform profile, with only fully stripped carbon impurity - - References - ---------- - - S. Sugiyama, T. Goto, H. Utoh, and Y. Sakamoto, “Improvement of core plasma power and - current balance models for tokamak systems code considering H-mode plasma profiles,” - Fusion Engineering and Design, vol. 216, p. 115022, Jul. 2025, doi: - https://doi.org/10.1016/j.fusengdes.2025.115022. - - """ - - return ( - 0.789 - * eps**0.606 - * beta_poloidal**0.960 - * alphan**0.0319 - * alphat**0.00822 - * tbeta**-0.0783 - * zeff**0.241 - * (q95 / q0) ** -0.103 - * radius_plasma_pedestal_density_norm**0.367 - * (nd_plasma_pedestal_electron / n_greenwald) ** -0.174 - * temp_plasma_pedestal_kev**0.0552 - ) - - def find_other_h_factors(self, i_confinement_time: int) -> float: - """Function to find H-factor for the equivalent confinement time in other scalings. - - Parameters - ---------- - i_confinement_time : int - Index of the confinement time scaling to use. - - Returns - ------- - float - The calculated H-factor. - - """ - - def fhz(hfact: float) -> float: - """Function used to find power balance. - - Parameters - ---------- - hfact : float - H-factor to be used in the calculation. - hfact: float : - - - Returns - ------- - float - The difference between the calculated power and the required power for balance. - - """ - ( - ptrez, - ptriz, - _, - _, - _, - _, - ) = self.calculate_confinement_time( - physics_variables.m_fuel_amu, - physics_variables.p_alpha_total_mw, - physics_variables.aspect, - physics_variables.b_plasma_toroidal_on_axis, - physics_variables.nd_plasma_ions_total_vol_avg, - physics_variables.nd_plasma_electrons_vol_avg, - physics_variables.nd_plasma_electron_line, - physics_variables.eps, - hfact, - i_confinement_time, - physics_variables.i_plasma_ignited, - physics_variables.kappa, - physics_variables.kappa95, - physics_variables.p_non_alpha_charged_mw, - current_drive_variables.p_hcd_injected_total_mw, - physics_variables.plasma_current, - physics_variables.pden_plasma_core_rad_mw, - physics_variables.rmajor, - physics_variables.rminor, - physics_variables.temp_plasma_electron_density_weighted_kev, - physics_variables.temp_plasma_ion_density_weighted_kev, - physics_variables.q95, - physics_variables.qstar, - physics_variables.vol_plasma, - physics_variables.n_charge_plasma_effective_vol_avg, - ) - - # At power balance, fhz is zero. - fhz_value = ( - ptrez - + ptriz - - physics_variables.f_p_alpha_plasma_deposited - * physics_variables.pden_alpha_total_mw - - physics_variables.pden_non_alpha_charged_mw - - physics_variables.pden_plasma_ohmic_mw - ) - - # Take into account whether injected power is included in tau_e calculation (i.e. whether device is ignited) - if physics_variables.i_plasma_ignited == 0: - fhz_value -= ( - current_drive_variables.p_hcd_injected_total_mw - / physics_variables.vol_plasma - ) - - # Include the radiation power if requested - if physics_variables.i_rad_loss == 0: - fhz_value += physics_variables.pden_plasma_rad_mw - elif physics_variables.i_rad_loss == 1: - fhz_value += physics_variables.pden_plasma_core_rad_mw - - return fhz_value - - return root_scalar(fhz, bracket=(0.01, 150), xtol=0.001).root - - @staticmethod - def calculate_confinement_time( - m_fuel_amu: float, - p_alpha_total_mw: float, - aspect: float, - b_plasma_toroidal_on_axis: float, - nd_plasma_ions_total_vol_avg: float, - nd_plasma_electrons_vol_avg: float, - nd_plasma_electron_line: float, - eps: float, - hfact: float, - i_confinement_time: int, - i_plasma_ignited: int, - kappa: float, - kappa95: float, - p_non_alpha_charged_mw: float, - p_hcd_injected_total_mw: float, - plasma_current: float, - pden_plasma_core_rad_mw: float, - rmajor: float, - rminor: float, - temp_plasma_electron_density_weighted_kev: float, - temp_plasma_ion_density_weighted_kev: float, - q95: float, - qstar: float, - vol_plasma: float, - zeff: float, - ) -> tuple[float, float, float, float, float, float, float]: - """Calculate the confinement times and the transport power loss terms. - - Parameters - ---------- - m_fuel_amu : - Average mass of fuel (amu) - p_alpha_total_mw : - Alpha particle power (MW) - aspect : - Aspect ratio - b_plasma_toroidal_on_axis : - Toroidal field on axis (T) - nd_plasma_ions_total_vol_avg : - Total ion density (/m3) - nd_plasma_electrons_vol_avg : - Volume averaged electron density (/m3) - nd_plasma_electron_line : - Line-averaged electron density (/m3) - eps : - Inverse aspect ratio - hfact : - H factor on energy confinement scalings - i_confinement_time : - Switch for energy confinement scaling to use - i_plasma_ignited : - Switch for ignited calculation - kappa : - Plasma elongation - kappa95 : - Plasma elongation at 95% surface - p_non_alpha_charged_mw : - Non-alpha charged particle fusion power (MW) - p_hcd_injected_total_mw : - Auxiliary power to ions and electrons (MW) - plasma_current : - Plasma current (A) - pden_plasma_core_rad_mw : - Total core radiation power (MW/m3) - q95 : - Edge safety factor (tokamaks), or rotational transform iotabar (stellarators) - qstar : - Equivalent cylindrical edge safety factor - rmajor : - Plasma major radius (m) - rminor : - Plasma minor radius (m) - temp_plasma_electron_density_weighted_kev : - Density weighted average electron temperature (keV) - temp_plasma_ion_density_weighted_kev : - Density weighted average ion temperature (keV) - vol_plasma : - Plasma volume (m3) - a_plasma_poloidal : - Plasma cross-sectional area (m2) - zeff : - Plasma effective charge - - Returns - ------- - type - Tuple containing: - - pden_electron_transport_loss_mw (float): Electron transport power (MW/m3) - - pden_ion_transport_loss_mw (float): Ion transport power (MW/m3) - - t_electron_energy_confinement (float): Electron energy confinement time (s) - - t_ion_energy_confinement (float): Ion energy confinement time (s) - - t_energy_confinement (float): Global energy confinement time (s) - - p_plasma_loss_mw (float): Heating power (MW) assumed in calculation - - """ - - # ======================================================================== - - # Calculate heating power (MW) - p_plasma_loss_mw = ( - physics_variables.f_p_alpha_plasma_deposited * p_alpha_total_mw - + p_non_alpha_charged_mw - + physics_variables.p_plasma_ohmic_mw - ) - - # If the device is not ignited, add the injected auxiliary power - if i_plasma_ignited == 0: - p_plasma_loss_mw = p_plasma_loss_mw + p_hcd_injected_total_mw - - # Include the radiation as a loss term if requested - if physics_variables.i_rad_loss == 0: - p_plasma_loss_mw = ( - p_plasma_loss_mw - physics_variables.pden_plasma_rad_mw * vol_plasma + "Snipes 2000 scaling: upper bound (MW)", + "(l_h_threshold_powers(10))", + physics_variables.l_h_threshold_powers[9], + "OP ", ) - elif physics_variables.i_rad_loss == 1: - p_plasma_loss_mw = ( - p_plasma_loss_mw - pden_plasma_core_rad_mw * vol_plasma - ) # shouldn't this be vol_core instead of vol_plasma? - # else do not adjust p_plasma_loss_mw for radiation - - # Ensure heating power is positive (shouldn't be necessary) - p_plasma_loss_mw = max(p_plasma_loss_mw, 1.0e-3) - - # ======================================================================== - - # Line averaged electron density in scaled units - dnla20 = nd_plasma_electron_line * 1.0e-20 - dnla19 = nd_plasma_electron_line * 1.0e-19 - - # Volume averaged electron density in units of 10**20 m**-3 - n20 = nd_plasma_electrons_vol_avg / 1.0e20 - - # Plasma current in MA - pcur = plasma_current / 1.0e6 - - # Separatrix kappa defined with plasma volume for IPB scalings - # Updated version of kappa used by the IPB98 scalings correction in: - - # None Otto Kardaun, N. K. Thomsen, and None Alexander Chudnovskiy, - # “Corrections to a sequence of papers in Nuclear Fusion,” Nuclear Fusion, - # vol. 48, no. 9, pp. 099801099801, Aug. 2008, - # doi: https://doi.org/10.1088/0029-5515/48/9/099801. - - physics_variables.kappa_ipb = (vol_plasma / (2.0 * np.pi * rmajor)) / ( - np.pi * rminor**2 - ) - - # Electron energy confinement times - - # ======================================================================== - - # User defined confinement time - if i_confinement_time == 0: # t_electron_energy_confinement is an input - t_electron_confinement = physics_variables.tauee_in - - # ======================================================================== - - # Nec-Alcator(NA) OH scaling - if i_confinement_time == 1: - t_electron_confinement = confinement.neo_alcator_confinement_time( - n20, rminor, rmajor, qstar + po.ovarre( + self.outfile, + "Snipes 2000 scaling: lower bound (MW)", + "(l_h_threshold_powers(11))", + physics_variables.l_h_threshold_powers[10], + "OP ", ) - - # ======================================================================== - - # "Mirnov"-like scaling (H-mode) - elif i_confinement_time == 2: # Mirnov scaling (H-mode) - t_electron_confinement = confinement.mirnov_confinement_time( - rminor, kappa95, pcur + po.ovarre( + self.outfile, + "Snipes 2000 scaling (closed divertor): nominal (MW)", + "(l_h_threshold_powers(12))", + physics_variables.l_h_threshold_powers[11], + "OP ", ) - - # ======================================================================== - - # Merezhkin-Mukhovatov (MM) OH/L-mode scaling - elif i_confinement_time == 3: - t_electron_confinement = confinement.merezhkin_muhkovatov_confinement_time( - rmajor, - rminor, - kappa95, - qstar, - dnla20, - m_fuel_amu, - temp_plasma_electron_density_weighted_kev, + po.ovarre( + self.outfile, + "Snipes 2000 scaling (closed divertor): upper bound (MW)", + "(l_h_threshold_powers(13))", + physics_variables.l_h_threshold_powers[12], + "OP ", ) - - # ======================================================================== - - # Shimomura (S) optimized H-mode scaling - elif i_confinement_time == 4: - t_electron_confinement = confinement.shimomura_confinement_time( - rmajor, rminor, b_plasma_toroidal_on_axis, kappa95, m_fuel_amu + po.ovarre( + self.outfile, + "Snipes 2000 scaling (closed divertor): lower bound (MW)", + "(l_h_threshold_powers(14))", + physics_variables.l_h_threshold_powers[13], + "OP ", ) - - # ======================================================================== - - # Kaye-Goldston scaling (L-mode) - elif i_confinement_time == 5: - t_electron_confinement = confinement.kaye_goldston_confinement_time( - pcur, - rmajor, - rminor, - kappa, - dnla20, - b_plasma_toroidal_on_axis, - m_fuel_amu, - p_plasma_loss_mw, + po.ovarre( + self.outfile, + "Hubbard 2012 L-I threshold - nominal (MW)", + "(l_h_threshold_powers(15))", + physics_variables.l_h_threshold_powers[14], + "OP ", ) - - # ======================================================================== - - # ITER Power scaling - ITER 89-P (L-mode) - elif i_confinement_time == 6: - t_electron_confinement = confinement.iter_89p_confinement_time( - pcur, - rmajor, - rminor, - kappa, - dnla20, - b_plasma_toroidal_on_axis, - m_fuel_amu, - p_plasma_loss_mw, + po.ovarre( + self.outfile, + "Hubbard 2012 L-I threshold - lower bound (MW)", + "(l_h_threshold_powers(16))", + physics_variables.l_h_threshold_powers[15], + "OP ", + ) + po.ovarre( + self.outfile, + "Hubbard 2012 L-I threshold - upper bound (MW)", + "(l_h_threshold_powers(17))", + physics_variables.l_h_threshold_powers[16], + "OP ", + ) + po.ovarre( + self.outfile, + "Hubbard 2017 L-I threshold", + "(l_h_threshold_powers(18))", + physics_variables.l_h_threshold_powers[17], + "OP ", + ) + po.ovarre( + self.outfile, + "Martin 2008 aspect ratio corrected scaling: nominal (MW)", + "(l_h_threshold_powers(19))", + physics_variables.l_h_threshold_powers[18], + "OP ", + ) + po.ovarre( + self.outfile, + "Martin 2008 aspect ratio corrected scaling: 95% upper bound (MW)", + "(l_h_threshold_powers(20))", + physics_variables.l_h_threshold_powers[19], + "OP ", + ) + po.ovarre( + self.outfile, + "Martin 2008 aspect ratio corrected scaling: 95% lower bound (MW)", + "(l_h_threshold_powers(21))", + physics_variables.l_h_threshold_powers[20], + "OP ", ) + po.oblnkl(self.outfile) + if physics_variables.i_l_h_threshold in [9, 10, 11]: + if (physics_variables.b_plasma_toroidal_on_axis < 0.78e0) or ( + physics_variables.b_plasma_toroidal_on_axis > 7.94e0 + ): + po.ocmmnt( + self.outfile, + "(b_plasma_toroidal_on_axis outside Snipes 2000 fitted range)", + ) + logger.warning( + "b_plasma_toroidal_on_axis outside Snipes 2000 fitted range" + ) - # ======================================================================== + if (physics_variables.rminor < 0.15e0) or ( + physics_variables.rminor > 1.15e0 + ): + po.ocmmnt(self.outfile, "(rminor outside Snipes 2000 fitted range)") + logger.warning("rminor outside Snipes 2000 fitted range") - # ITER Offset linear scaling - ITER 89-O (L-mode) - elif i_confinement_time == 7: - t_electron_confinement = confinement.iter_89_0_confinement_time( - pcur, - rmajor, - rminor, - kappa, - dnla20, - b_plasma_toroidal_on_axis, - m_fuel_amu, - p_plasma_loss_mw, - ) - # ======================================================================== + if (physics_variables.rmajor < 0.55e0) or ( + physics_variables.rmajor > 3.37e0 + ): + po.ocmmnt( + self.outfile, + "(physics_variables.rmajor outside Snipes 2000 fitted range)", + ) + logger.warning("rmajor outside Snipes 2000 fitted range") - # Rebut-Lallia offset linear scaling (L-mode) - elif i_confinement_time == 8: - t_electron_confinement = confinement.rebut_lallia_confinement_time( - rminor, - rmajor, - kappa, - m_fuel_amu, - pcur, - zeff, - dnla20, - b_plasma_toroidal_on_axis, - p_plasma_loss_mw, - ) + if (physics_variables.nd_plasma_electron_line < 0.09e20) or ( + physics_variables.nd_plasma_electron_line > 3.16e20 + ): + po.ocmmnt( + self.outfile, + "(physics_variables.nd_plasma_electron_line outside Snipes 2000 fitted range)", + ) + logger.warning( + "nd_plasma_electron_line outside Snipes 2000 fitted range" + ) - # ======================================================================== + if (physics_variables.kappa < 1.0e0) or ( + physics_variables.kappa > 2.04e0 + ): + po.ocmmnt( + self.outfile, + "(physics_variables.kappa outside Snipes 2000 fitted range)", + ) + logger.warning("kappa outside Snipes 2000 fitted range") - # Goldston scaling (L-mode) - elif i_confinement_time == 9: # Goldston scaling (L-mode) - t_electron_confinement = confinement.goldston_confinement_time( - pcur, rmajor, rminor, kappa95, m_fuel_amu, p_plasma_loss_mw - ) + if (physics_variables.triang < 0.07e0) or ( + physics_variables.triang > 0.74e0 + ): + po.ocmmnt(self.outfile, "(triang outside Snipes 2000 fitted range)") + logger.warning("triang outside Snipes 2000 fitted range") - # ======================================================================== + po.oblnkl(self.outfile) - # T-10 scaling (L-mode) - elif i_confinement_time == 10: - t_electron_confinement = confinement.t10_confinement_time( - dnla20, - rmajor, - qstar, - b_plasma_toroidal_on_axis, - rminor, - kappa95, - p_plasma_loss_mw, - zeff, - pcur, - ) + if physics_variables.i_l_h_threshold in [12, 13, 14]: + po.ocmmnt( + self.outfile, + "(L-H threshold for closed divertor only. Limited data used in Snipes fit)", + ) + po.oblnkl(self.outfile) + logger.warning("Closed divertor only. Limited data used in Snipes fit") - # ======================================================================== + if (numerics.ioptimz > 0) and (numerics.active_constraints[14]): + po.ovarre( + self.outfile, + "L-H threshold power (MW)", + "(p_l_h_threshold_mw)", + physics_variables.p_l_h_threshold_mw, + "OP ", + ) + else: + po.ovarre( + self.outfile, + "L-H threshold power (NOT enforced) (MW)", + "(p_l_h_threshold_mw)", + physics_variables.p_l_h_threshold_mw, + "OP ", + ) - # JAERI / Odajima-Shimomura L-mode scaling - elif i_confinement_time == 11: # JAERI scaling - t_electron_confinement = confinement.jaeri_confinement_time( - kappa95, - rminor, - m_fuel_amu, - n20, - pcur, - b_plasma_toroidal_on_axis, - rmajor, - qstar, - zeff, - p_plasma_loss_mw, + po.osubhd(self.outfile, "Confinement :") + + if physics_variables.i_plasma_ignited == 1: + po.ocmmnt( + self.outfile, + "Device is assumed to be ignited for the calculation of confinement time", ) + po.oblnkl(self.outfile) - # ======================================================================== + tauelaw = physics_variables.LABELS_CONFINEMENT_SCALINGS[ + physics_variables.i_confinement_time + ] - # Kaye "big" L-mode scaling (based only on big tokamak data) - elif i_confinement_time == 12: - t_electron_confinement = confinement.kaye_big_confinement_time( - rmajor, - rminor, - b_plasma_toroidal_on_axis, - kappa95, - pcur, - n20, - m_fuel_amu, - p_plasma_loss_mw, - ) + po.ocmmnt( + self.outfile, + f"Confinement scaling law: {tauelaw}", + ) - # ======================================================================== + po.ovarst( + self.outfile, + "Confinement scaling law", + "(tauelaw)", + f'"{tauelaw.strip().split(" ")[0]}"', + ) - # ITER H90-P H-mode scaling - elif i_confinement_time == 13: - t_electron_confinement = confinement.iter_h90_p_confinement_time( - pcur, - rmajor, - rminor, - kappa, - dnla20, - b_plasma_toroidal_on_axis, - m_fuel_amu, - p_plasma_loss_mw, + po.ovarrf( + self.outfile, "Confinement H factor", "(hfact)", physics_variables.hfact + ) + po.ovarrf( + self.outfile, + "Global thermal energy confinement time, from scaling (s)", + "(t_energy_confinement)", + physics_variables.t_energy_confinement, + "OP ", + ) + po.ovarrf( + self.outfile, + "Directly calculated total energy confinement time (s)", + "(t_energy_confinement_beta)", + physics_variables.t_energy_confinement_beta, + "OP ", + ) + po.ocmmnt( + self.outfile, + "(Total thermal energy derived from total plasma beta / loss power)", + ) + po.ovarrf( + self.outfile, + "Ion energy confinement time, from scaling (s)", + "(t_ion_energy_confinement)", + physics_variables.t_ion_energy_confinement, + "OP ", + ) + po.ovarrf( + self.outfile, + "Electron energy confinement time, from scaling (s)", + "(t_electron_energy_confinement)", + physics_variables.t_electron_energy_confinement, + "OP ", + ) + po.ovarre( + self.outfile, + "Fusion double product (s/m3)", + "(ntau)", + physics_variables.ntau, + "OP ", + ) + po.ovarre( + self.outfile, + "Lawson Triple product (keV s/m3)", + "(nTtau)", + physics_variables.nTtau, + "OP ", + ) + po.ovarre( + self.outfile, + "Transport loss power assumed in scaling law (MW)", + "(p_plasma_loss_mw)", + physics_variables.p_plasma_loss_mw, + "OP ", + ) + po.ovarin( + self.outfile, + "Switch for radiation loss term usage in power balance", + "(i_rad_loss)", + physics_variables.i_rad_loss, + ) + if physics_variables.i_rad_loss == 0: + po.ovarre( + self.outfile, + "Radiation power subtracted from plasma power balance (MW)", + "", + physics_variables.p_plasma_rad_mw, + "OP ", + ) + po.ocmmnt(self.outfile, " (Radiation correction is total radiation power)") + elif physics_variables.i_rad_loss == 1: + po.ovarre( + self.outfile, + "Radiation power subtracted from plasma power balance (MW)", + "", + physics_variables.p_plasma_inner_rad_mw, + "OP ", + ) + po.ocmmnt(self.outfile, " (Radiation correction is core radiation power)") + else: + po.ovarre( + self.outfile, + "Radiation power subtracted from plasma power balance (MW)", + "", + 0.0e0, ) + po.ocmmnt(self.outfile, " (No radiation correction applied)") + po.ovarrf( + self.outfile, + "H* non-radiation corrected", + "(hstar)", + physics_variables.hstar, + "OP", + ) + po.ocmmnt(self.outfile, " (H* assumes IPB98(y,2), ELMy H-mode scaling)") + po.ovarrf( + self.outfile, + "Alpha particle confinement time (s)", + "(t_alpha_confinement)", + physics_variables.t_alpha_confinement, + "OP ", + ) + # Note alpha confinement time is no longer equal to fuel particle confinement time. + po.ovarrf( + self.outfile, + "Alpha particle/energy confinement time ratio", + "(f_alpha_energy_confinement)", + physics_variables.f_alpha_energy_confinement, + "OP ", + ) + po.ovarrf( + self.outfile, + "Lower limit on f_alpha_energy_confinement", + "(f_alpha_energy_confinement_min)", + constraint_variables.f_alpha_energy_confinement_min, + ) - # ======================================================================== + # Plot table of al the H-factor scalings and coparison values + self.output_confinement_comparison(istell=stellarator_variables.istell) - # Minimum of ITER 89-P and ITER 89-O - elif i_confinement_time == 14: - t_electron_confinement = min( - confinement.iter_89p_confinement_time( - pcur, - rmajor, - rminor, - kappa, - dnla20, - b_plasma_toroidal_on_axis, - m_fuel_amu, - p_plasma_loss_mw, - ), - confinement.iter_89_0_confinement_time( - pcur, - rmajor, - rminor, - kappa, - dnla20, - b_plasma_toroidal_on_axis, - m_fuel_amu, - p_plasma_loss_mw, - ), + if stellarator_variables.istell == 0: + # Issues 363 Output dimensionless plasma parameters MDK + po.osubhd(self.outfile, "Dimensionless plasma parameters") + po.ocmmnt(self.outfile, "For definitions see") + po.ocmmnt( + self.outfile, + "Recent progress on the development and analysis of the ITPA global H-mode confinement database", + ) + po.ocmmnt( + self.outfile, + "D.C. McDonald et al, 2007 Nuclear Fusion v47, 147. (nu_star missing 1/mu0)", + ) + po.ovarre( + self.outfile, + "Normalized plasma pressure beta as defined by McDonald et al", + "(beta_mcdonald)", + physics_variables.beta_mcdonald, + "OP ", ) - - # ======================================================================== - - # Riedel scaling (L-mode) - elif i_confinement_time == 15: - t_electron_confinement = confinement.riedel_l_confinement_time( - pcur, - rmajor, - rminor, - kappa95, - dnla20, - b_plasma_toroidal_on_axis, - p_plasma_loss_mw, + po.ovarre( + self.outfile, + "Normalized ion Larmor radius", + "(rho_star)", + physics_variables.rho_star, + "OP ", ) - - # ======================================================================== - - # Christiansen et al scaling (L-mode) - elif i_confinement_time == 16: - t_electron_confinement = confinement.christiansen_confinement_time( - pcur, - rmajor, - rminor, - kappa95, - dnla20, - b_plasma_toroidal_on_axis, - p_plasma_loss_mw, - m_fuel_amu, + po.ovarre( + self.outfile, + "Normalized collisionality", + "(nu_star)", + physics_variables.nu_star, + "OP ", ) - - # ======================================================================== - - # Lackner-Gottardi scaling (L-mode) - elif i_confinement_time == 17: - t_electron_confinement = confinement.lackner_gottardi_confinement_time( - pcur, - rmajor, - rminor, - kappa95, - dnla20, - b_plasma_toroidal_on_axis, - p_plasma_loss_mw, + po.ovarre( + self.outfile, + "ITER Physics Basis definition of elongation", + "(kappa_ipb)", + physics_variables.kappa_ipb, + "OP ", ) - # ======================================================================== + po.oblnkl(self.outfile) + po.ostars(self.outfile, 110) + po.oblnkl(self.outfile) - # Neo-Kaye scaling (L-mode) - elif i_confinement_time == 18: - t_electron_confinement = confinement.neo_kaye_confinement_time( - pcur, - rmajor, - rminor, - kappa95, - dnla20, - b_plasma_toroidal_on_axis, - p_plasma_loss_mw, - ) + self.inductance.output_volt_second_information() - # ======== ================================================================ + self.bootstrap.output_bootstrap_current_information() - # Riedel scaling (H-mode) - elif i_confinement_time == 19: - t_electron_confinement = confinement.riedel_h_confinement_time( - pcur, - rmajor, - rminor, - kappa95, - dnla20, - b_plasma_toroidal_on_axis, - m_fuel_amu, - p_plasma_loss_mw, + po.osubhd(self.outfile, "Fuelling :") + po.ovarre( + self.outfile, + "Ratio of He and pellet particle confinement times", + "(tauratio)", + physics_variables.tauratio, + ) + po.ovarre( + self.outfile, + "Fuelling rate (nucleus-pairs/s)", + "(molflow_plasma_fuelling_required)", + physics_variables.molflow_plasma_fuelling_required, + "OP ", + ) + po.ovarre( + self.outfile, + "Fuel burn-up rate (reactions/s)", + "(rndfuel)", + physics_variables.rndfuel, + "OP ", + ) + po.ovarrf( + self.outfile, + "Burn-up fraction", + "(burnup)", + physics_variables.burnup, + "OP ", + ) + + if 78 in numerics.icc: + po.osubhd(self.outfile, "Reinke Criterion :") + po.ovarin( + self.outfile, + "index of impurity to be iterated for divertor detachment", + "(impvardiv)", + reinke_variables.impvardiv, + ) + po.ovarre( + self.outfile, + "Minimum Impurity fraction from Reinke", + "(fzmin)", + reinke_variables.fzmin, + "OP ", + ) + po.ovarre( + self.outfile, + "Actual Impurity fraction", + "(fzactual)", + reinke_variables.fzactual, ) - # ======================================================================== + def output_confinement_comparison(self, istell: int): + """Routine to calculate ignition margin for different confinement scalings and equivalent confinement times for H=1. - # Amended version of ITER H90-P law - elif i_confinement_time == 20: - t_electron_confinement = confinement.iter_h90_p_amended_confinement_time( - pcur, - b_plasma_toroidal_on_axis, - m_fuel_amu, - rmajor, - p_plasma_loss_mw, - kappa, - ) + This routine calculates the ignition margin at the final point with different scalings and outputs the results to a file. - # ========================================================================== + The output includes: + - Energy confinement times + - Required H-factors for power balance - # Sudo et al. scaling (stellarators/heliotron) - elif i_confinement_time == 21: - t_electron_confinement = confinement.sudo_et_al_confinement_time( - rmajor, - rminor, - dnla20, - b_plasma_toroidal_on_axis, - p_plasma_loss_mw, - ) + The routine iterates over a range of confinement times, skipping the first user input and a specific index (25). For each confinement time, it calculates various parameters related to confinement and ignition using the `calculate_confinement_time` method. It then calculates the H-factor for when the plasma is ignited using the `find_other_h_factors` method and writes the results to the output file. - # ========================================================================== + Output format: + - Header: "Energy confinement times, and required H-factors :" + - Columns: "Scaling law", "Confinement time [s]", "H-factor for power balance" - # Gyro-reduced Bohm scaling - elif i_confinement_time == 22: - t_electron_confinement = confinement.gyro_reduced_bohm_confinement_time( - b_plasma_toroidal_on_axis, - dnla20, - p_plasma_loss_mw, - rminor, - rmajor, - ) + Methods used: + - `calculate_confinement_time`: Calculates confinement-related parameters. + - `find_other_h_factors`: Calculates the H-factor for a given confinement time. - # ========================================================================== + Parameters + ---------- + istell : + Indicator for stellarator (0 for tokamak, >=1 for stellarator). - # Lackner-Gottardi stellarator scaling - elif i_confinement_time == 23: - t_electron_confinement = ( - confinement.lackner_gottardi_stellarator_confinement_time( - rmajor, - rminor, - dnla20, - b_plasma_toroidal_on_axis, - p_plasma_loss_mw, - q95, - ) - ) + """ - # ========================================================================== + po.oheadr(self.outfile, "Energy confinement times, and required H-factors :") + po.ocmmnt( + self.outfile, + f"{'':>2}{'Scaling law':<27}{'Electron confinement time [s]':<32}Equivalent H-factor for", + ) + po.ocmmnt( + self.outfile, + f"{'':>38}{'for H = 1':<23}same confinement time", + ) + po.oblnkl(self.outfile) - # ITER_93 ELM-free H-mode scaling - elif i_confinement_time == 24: - t_electron_confinement = confinement.iter_93h_confinement_time( - pcur, - b_plasma_toroidal_on_axis, - p_plasma_loss_mw, - m_fuel_amu, - rmajor, - dnla20, - aspect, - kappa, + # List of key values for stellarator scalings + stellarator_scalings = [21, 22, 23, 37, 38] + + # Plot all of the confinement scalings for comparison when H = 1 + # Start from range 1 as the first i_confinement_time is a user input + # If stellarator, use the stellarator scalings + for i_confinement_time in ( + range(1, physics_variables.N_CONFINEMENT_SCALINGS) + if istell == 0 + else stellarator_scalings + ): + if i_confinement_time == 25: + continue + ( + _, + _, + taueez, + _, + _, + _, + ) = self.calculate_confinement_time( + physics_variables.m_fuel_amu, + physics_variables.p_alpha_total_mw, + physics_variables.aspect, + physics_variables.b_plasma_toroidal_on_axis, + physics_variables.nd_plasma_ions_total_vol_avg, + physics_variables.nd_plasma_electrons_vol_avg, + physics_variables.nd_plasma_electron_line, + physics_variables.eps, + 1.0, + i_confinement_time, + physics_variables.i_plasma_ignited, + physics_variables.kappa, + physics_variables.kappa95, + physics_variables.p_non_alpha_charged_mw, + current_drive_variables.p_hcd_injected_total_mw, + physics_variables.plasma_current, + physics_variables.pden_plasma_core_rad_mw, + physics_variables.rmajor, + physics_variables.rminor, + physics_variables.temp_plasma_electron_density_weighted_kev, + physics_variables.temp_plasma_ion_density_weighted_kev, + physics_variables.q95, + physics_variables.qstar, + physics_variables.vol_plasma, + physics_variables.n_charge_plasma_effective_vol_avg, ) - # ========================================================================== - # Scaling removed - elif i_confinement_time == 25: - raise ProcessValueError("Scaling removed") - # ========================================================================== + try: + # Calculate the H-factor for the same confinement time in other scalings + physics_variables.hfac[i_confinement_time - 1] = ( + self.find_other_h_factors(i_confinement_time) + ) + except ValueError: + # This is only used for a table in the OUT.DAT so if it fails + # just write a NaN--its not worth crashing PROCESS over. + physics_variables.hfac[i_confinement_time - 1] = np.nan - # ELM-free: ITERH-97P - elif i_confinement_time == 26: - t_electron_confinement = confinement.iter_h97p_confinement_time( - pcur, - b_plasma_toroidal_on_axis, - p_plasma_loss_mw, - dnla19, - rmajor, - aspect, - kappa, - m_fuel_amu, + po.ocmmnt( + self.outfile, + f"{'':>2}{physics_variables.LABELS_CONFINEMENT_SCALINGS[i_confinement_time]:<38}" + f"{taueez:<28.3f}{physics_variables.hfac[i_confinement_time - 1]:.3f}", ) - # ========================================================================== + po.oblnkl(self.outfile) + po.ostars(self.outfile, 110) - # ELMy: ITERH-97P(y) - elif i_confinement_time == 27: - t_electron_confinement = confinement.iter_h97p_elmy_confinement_time( - pcur, - b_plasma_toroidal_on_axis, - p_plasma_loss_mw, - dnla19, - rmajor, - aspect, - kappa, - m_fuel_amu, - ) + def find_other_h_factors(self, i_confinement_time: int) -> float: + """Function to find H-factor for the equivalent confinement time in other scalings. - # ========================================================================== + Parameters + ---------- + i_confinement_time : int + Index of the confinement time scaling to use. - # ITER-96P (= ITER-97L) L-mode scaling - elif i_confinement_time == 28: - t_electron_confinement = confinement.iter_96p_confinement_time( - pcur, - b_plasma_toroidal_on_axis, - kappa95, - rmajor, - aspect, - dnla19, - m_fuel_amu, - p_plasma_loss_mw, - ) + Returns + ------- + float + The calculated H-factor. - # ========================================================================== + """ - # Valovic modified ELMy-H mode scaling - # WARNING: No reference found for this scaling. This may not be its real name - elif i_confinement_time == 29: - t_electron_confinement = confinement.valovic_elmy_confinement_time( - pcur, - b_plasma_toroidal_on_axis, - dnla19, - m_fuel_amu, - rmajor, - rminor, - kappa, - p_plasma_loss_mw, - ) + def fhz(hfact: float) -> float: + """Function used to find power balance. - # ========================================================================== + Parameters + ---------- + hfact : float + H-factor to be used in the calculation. + hfact: float : - # Kaye PPPL Workshop April 1998 L-mode scaling - # WARNING: No reference found for this scaling. This may not be its real name - elif i_confinement_time == 30: - t_electron_confinement = confinement.kaye_confinement_time( - pcur, - b_plasma_toroidal_on_axis, - kappa, - rmajor, - aspect, - dnla19, - m_fuel_amu, - p_plasma_loss_mw, - ) - # ========================================================================== + Returns + ------- + float + The difference between the calculated power and the required power for balance. - # ITERH-PB98P(y), ELMy H-mode scaling - # WARNING: No reference found for this scaling. This may not be its real name - elif i_confinement_time == 31: - t_electron_confinement = confinement.iter_pb98py_confinement_time( - pcur, - b_plasma_toroidal_on_axis, - dnla19, - p_plasma_loss_mw, - rmajor, - physics_variables.kappa_ipb, - aspect, - m_fuel_amu, + """ + ( + ptrez, + ptriz, + _, + _, + _, + _, + ) = self.calculate_confinement_time( + physics_variables.m_fuel_amu, + physics_variables.p_alpha_total_mw, + physics_variables.aspect, + physics_variables.b_plasma_toroidal_on_axis, + physics_variables.nd_plasma_ions_total_vol_avg, + physics_variables.nd_plasma_electrons_vol_avg, + physics_variables.nd_plasma_electron_line, + physics_variables.eps, + hfact, + i_confinement_time, + physics_variables.i_plasma_ignited, + physics_variables.kappa, + physics_variables.kappa95, + physics_variables.p_non_alpha_charged_mw, + current_drive_variables.p_hcd_injected_total_mw, + physics_variables.plasma_current, + physics_variables.pden_plasma_core_rad_mw, + physics_variables.rmajor, + physics_variables.rminor, + physics_variables.temp_plasma_electron_density_weighted_kev, + physics_variables.temp_plasma_ion_density_weighted_kev, + physics_variables.q95, + physics_variables.qstar, + physics_variables.vol_plasma, + physics_variables.n_charge_plasma_effective_vol_avg, ) - # ========================================================================== - - # IPB98(y), ELMy H-mode scaling - elif i_confinement_time == 32: - t_electron_confinement = confinement.iter_ipb98y_confinement_time( - pcur, - b_plasma_toroidal_on_axis, - dnla19, - p_plasma_loss_mw, - rmajor, - kappa, - aspect, - m_fuel_amu, + # At power balance, fhz is zero. + fhz_value = ( + ptrez + + ptriz + - physics_variables.f_p_alpha_plasma_deposited + * physics_variables.pden_alpha_total_mw + - physics_variables.pden_non_alpha_charged_mw + - physics_variables.pden_plasma_ohmic_mw ) - # ========================================================================== + # Take into account whether injected power is included in tau_e calculation (i.e. whether device is ignited) + if physics_variables.i_plasma_ignited == 0: + fhz_value -= ( + current_drive_variables.p_hcd_injected_total_mw + / physics_variables.vol_plasma + ) - # IPB98(y,1), ELMy H-mode scaling - elif i_confinement_time == 33: - t_electron_confinement = confinement.iter_ipb98y1_confinement_time( - pcur, - b_plasma_toroidal_on_axis, - dnla19, - p_plasma_loss_mw, - rmajor, - physics_variables.kappa_ipb, - aspect, - m_fuel_amu, - ) + # Include the radiation power if requested + if physics_variables.i_rad_loss == 0: + fhz_value += physics_variables.pden_plasma_rad_mw + elif physics_variables.i_rad_loss == 1: + fhz_value += physics_variables.pden_plasma_core_rad_mw - # ========================================================================== + return fhz_value - # IPB98(y,2), ELMy H-mode scaling - elif i_confinement_time == 34: - t_electron_confinement = confinement.iter_ipb98y2_confinement_time( - pcur, - b_plasma_toroidal_on_axis, - dnla19, - p_plasma_loss_mw, - rmajor, - physics_variables.kappa_ipb, - aspect, - m_fuel_amu, - ) + return root_scalar(fhz, bracket=(0.01, 150), xtol=0.001).root - # ========================================================================== + @staticmethod + def calculate_confinement_time( + m_fuel_amu: float, + p_alpha_total_mw: float, + aspect: float, + b_plasma_toroidal_on_axis: float, + nd_plasma_ions_total_vol_avg: float, + nd_plasma_electrons_vol_avg: float, + nd_plasma_electron_line: float, + eps: float, + hfact: float, + i_confinement_time: int, + i_plasma_ignited: int, + kappa: float, + kappa95: float, + p_non_alpha_charged_mw: float, + p_hcd_injected_total_mw: float, + plasma_current: float, + pden_plasma_core_rad_mw: float, + rmajor: float, + rminor: float, + temp_plasma_electron_density_weighted_kev: float, + temp_plasma_ion_density_weighted_kev: float, + q95: float, + qstar: float, + vol_plasma: float, + zeff: float, + ) -> tuple[float, float, float, float, float, float, float]: + """Calculate the confinement times and the transport power loss terms. - # IPB98(y,3), ELMy H-mode scaling - elif i_confinement_time == 35: - t_electron_confinement = confinement.iter_ipb98y3_confinement_time( - pcur, - b_plasma_toroidal_on_axis, - dnla19, - p_plasma_loss_mw, - rmajor, - physics_variables.kappa_ipb, - aspect, - m_fuel_amu, - ) + Parameters + ---------- + m_fuel_amu : + Average mass of fuel (amu) + p_alpha_total_mw : + Alpha particle power (MW) + aspect : + Aspect ratio + b_plasma_toroidal_on_axis : + Toroidal field on axis (T) + nd_plasma_ions_total_vol_avg : + Total ion density (/m3) + nd_plasma_electrons_vol_avg : + Volume averaged electron density (/m3) + nd_plasma_electron_line : + Line-averaged electron density (/m3) + eps : + Inverse aspect ratio + hfact : + H factor on energy confinement scalings + i_confinement_time : + Switch for energy confinement scaling to use + i_plasma_ignited : + Switch for ignited calculation + kappa : + Plasma elongation + kappa95 : + Plasma elongation at 95% surface + p_non_alpha_charged_mw : + Non-alpha charged particle fusion power (MW) + p_hcd_injected_total_mw : + Auxiliary power to ions and electrons (MW) + plasma_current : + Plasma current (A) + pden_plasma_core_rad_mw : + Total core radiation power (MW/m3) + q95 : + Edge safety factor (tokamaks), or rotational transform iotabar (stellarators) + qstar : + Equivalent cylindrical edge safety factor + rmajor : + Plasma major radius (m) + rminor : + Plasma minor radius (m) + temp_plasma_electron_density_weighted_kev : + Density weighted average electron temperature (keV) + temp_plasma_ion_density_weighted_kev : + Density weighted average ion temperature (keV) + vol_plasma : + Plasma volume (m3) + a_plasma_poloidal : + Plasma cross-sectional area (m2) + zeff : + Plasma effective charge - # ========================================================================== + Returns + ------- + type + Tuple containing: + - pden_electron_transport_loss_mw (float): Electron transport power (MW/m3) + - pden_ion_transport_loss_mw (float): Ion transport power (MW/m3) + - t_electron_energy_confinement (float): Electron energy confinement time (s) + - t_ion_energy_confinement (float): Ion energy confinement time (s) + - t_energy_confinement (float): Global energy confinement time (s) + - p_plasma_loss_mw (float): Heating power (MW) assumed in calculation - # IPB98(y,4), ELMy H-mode scaling - elif i_confinement_time == 36: - t_electron_confinement = confinement.iter_ipb98y4_confinement_time( - pcur, - b_plasma_toroidal_on_axis, - dnla19, - p_plasma_loss_mw, - rmajor, - physics_variables.kappa_ipb, - aspect, - m_fuel_amu, - ) + """ - # ========================================================================== + # ======================================================================== - # ISS95 stellarator scaling - elif i_confinement_time == 37: - # dummy argument q95 is actual argument iotabar for stellarators - iotabar = q95 - t_electron_confinement = confinement.iss95_stellarator_confinement_time( - rminor, - rmajor, - dnla19, - b_plasma_toroidal_on_axis, - p_plasma_loss_mw, - iotabar, - ) + # Calculate heating power (MW) + p_plasma_loss_mw = ( + physics_variables.f_p_alpha_plasma_deposited * p_alpha_total_mw + + p_non_alpha_charged_mw + + physics_variables.p_plasma_ohmic_mw + ) - # ========================================================================== + # If the device is not ignited, add the injected auxiliary power + if i_plasma_ignited == 0: + p_plasma_loss_mw = p_plasma_loss_mw + p_hcd_injected_total_mw - # ISS04 stellarator scaling - elif i_confinement_time == 38: - # dummy argument q95 is actual argument iotabar for stellarators - iotabar = q95 - t_electron_confinement = confinement.iss04_stellarator_confinement_time( - rminor, - rmajor, - dnla19, - b_plasma_toroidal_on_axis, - p_plasma_loss_mw, - iotabar, + # Include the radiation as a loss term if requested + if physics_variables.i_rad_loss == 0: + p_plasma_loss_mw = ( + p_plasma_loss_mw - physics_variables.pden_plasma_rad_mw * vol_plasma ) + elif physics_variables.i_rad_loss == 1: + p_plasma_loss_mw = ( + p_plasma_loss_mw - pden_plasma_core_rad_mw * vol_plasma + ) # shouldn't this be vol_core instead of vol_plasma? + # else do not adjust p_plasma_loss_mw for radiation - # ========================================================================== - - # DS03 beta-independent H-mode scaling - elif i_confinement_time == 39: - t_electron_confinement = confinement.ds03_confinement_time( - pcur, - b_plasma_toroidal_on_axis, - dnla19, - p_plasma_loss_mw, - rmajor, - kappa95, - aspect, - m_fuel_amu, - ) + # Ensure heating power is positive (shouldn't be necessary) + p_plasma_loss_mw = max(p_plasma_loss_mw, 1.0e-3) - # ========================================================================== + # ======================================================================== - # Murari "Non-power law" scaling - elif i_confinement_time == 40: - t_electron_confinement = confinement.murari_confinement_time( - pcur, - rmajor, - physics_variables.kappa_ipb, - dnla19, - b_plasma_toroidal_on_axis, - p_plasma_loss_mw, - ) + # Line averaged electron density in scaled units + dnla20 = nd_plasma_electron_line * 1.0e-20 + dnla19 = nd_plasma_electron_line * 1.0e-19 - # ========================================================================== + # Volume averaged electron density in units of 10**20 m**-3 + n20 = nd_plasma_electrons_vol_avg / 1.0e20 - # Petty08, beta independent dimensionless scaling - elif i_confinement_time == 41: - t_electron_confinement = confinement.petty08_confinement_time( - pcur, - b_plasma_toroidal_on_axis, - dnla19, - p_plasma_loss_mw, - rmajor, - physics_variables.kappa_ipb, - aspect, - ) + # Plasma current in MA + pcur = plasma_current / 1.0e6 - # ========================================================================== + # Separatrix kappa defined with plasma volume for IPB scalings + # Updated version of kappa used by the IPB98 scalings correction in: - # Lang high density relevant confinement scaling - elif i_confinement_time == 42: - t_electron_confinement = confinement.lang_high_density_confinement_time( - plasma_current, - b_plasma_toroidal_on_axis, - nd_plasma_electron_line, - p_plasma_loss_mw, - rmajor, - rminor, - q95, - qstar, - aspect, - m_fuel_amu, - physics_variables.kappa_ipb, - ) + # None Otto Kardaun, N. K. Thomsen, and None Alexander Chudnovskiy, + # “Corrections to a sequence of papers in Nuclear Fusion,” Nuclear Fusion, + # vol. 48, no. 9, pp. 099801099801, Aug. 2008, + # doi: https://doi.org/10.1088/0029-5515/48/9/099801. - # ========================================================================== + physics_variables.kappa_ipb = (vol_plasma / (2.0 * np.pi * rmajor)) / ( + np.pi * rminor**2 + ) - # Hubbard 2017 I-mode confinement time scaling - nominal - elif i_confinement_time == 43: - t_electron_confinement = confinement.hubbard_nominal_confinement_time( - pcur, - b_plasma_toroidal_on_axis, - dnla20, - p_plasma_loss_mw, - ) + # Electron energy confinement times - # ========================================================================== + # ======================================================================== - # Hubbard 2017 I-mode confinement time scaling - lower - elif i_confinement_time == 44: - t_electron_confinement = confinement.hubbard_lower_confinement_time( - pcur, - b_plasma_toroidal_on_axis, - dnla20, - p_plasma_loss_mw, - ) + # User defined confinement time + if i_confinement_time == 0: # t_electron_energy_confinement is an input + t_electron_confinement = physics_variables.tauee_in - # ========================================================================== + # ======================================================================== - # Hubbard 2017 I-mode confinement time scaling - upper - elif i_confinement_time == 45: - t_electron_confinement = confinement.hubbard_upper_confinement_time( - pcur, - b_plasma_toroidal_on_axis, - dnla20, - p_plasma_loss_mw, + # Nec-Alcator(NA) OH scaling + if i_confinement_time == 1: + t_electron_confinement = confinement.neo_alcator_confinement_time( + n20, rminor, rmajor, qstar ) - # ========================================================================== + # ======================================================================== - # Menard NSTX, ELMy H-mode scaling - elif i_confinement_time == 46: - t_electron_confinement = confinement.menard_nstx_confinement_time( - pcur, - b_plasma_toroidal_on_axis, - dnla19, - p_plasma_loss_mw, + # "Mirnov"-like scaling (H-mode) + elif i_confinement_time == 2: # Mirnov scaling (H-mode) + t_electron_confinement = confinement.mirnov_confinement_time( + rminor, kappa95, pcur + ) + + # ======================================================================== + + # Merezhkin-Mukhovatov (MM) OH/L-mode scaling + elif i_confinement_time == 3: + t_electron_confinement = confinement.merezhkin_muhkovatov_confinement_time( rmajor, - physics_variables.kappa_ipb, - aspect, + rminor, + kappa95, + qstar, + dnla20, m_fuel_amu, + temp_plasma_electron_density_weighted_kev, ) - # ========================================================================== + # ======================================================================== - # Menard NSTX-Petty08 Hybrid - elif i_confinement_time == 47: - t_electron_confinement = ( - confinement.menard_nstx_petty08_hybrid_confinement_time( - pcur, - b_plasma_toroidal_on_axis, - dnla19, - p_plasma_loss_mw, - rmajor, - physics_variables.kappa_ipb, - aspect, - m_fuel_amu, - ) + # Shimomura (S) optimized H-mode scaling + elif i_confinement_time == 4: + t_electron_confinement = confinement.shimomura_confinement_time( + rmajor, rminor, b_plasma_toroidal_on_axis, kappa95, m_fuel_amu ) - # ========================================================================== + # ======================================================================== - # NSTX gyro-Bohm (Buxton) - elif i_confinement_time == 48: - t_electron_confinement = confinement.nstx_gyro_bohm_confinement_time( + # Kaye-Goldston scaling (L-mode) + elif i_confinement_time == 5: + t_electron_confinement = confinement.kaye_goldston_confinement_time( pcur, - b_plasma_toroidal_on_axis, - p_plasma_loss_mw, rmajor, + rminor, + kappa, dnla20, + b_plasma_toroidal_on_axis, + m_fuel_amu, + p_plasma_loss_mw, ) - # ========================================================================== + # ======================================================================== - # ITPA20 H-mode scaling - elif i_confinement_time == 49: - t_electron_confinement = confinement.itpa20_confinement_time( + # ITER Power scaling - ITER 89-P (L-mode) + elif i_confinement_time == 6: + t_electron_confinement = confinement.iter_89p_confinement_time( pcur, + rmajor, + rminor, + kappa, + dnla20, b_plasma_toroidal_on_axis, - dnla19, + m_fuel_amu, p_plasma_loss_mw, - rmajor, - physics_variables.triang, - physics_variables.kappa_ipb, - eps, - physics_variables.m_ions_total_amu, ) - # ========================================================================== + # ======================================================================== - # ITPA20-IL confinement time scaling - elif i_confinement_time == 50: - t_electron_confinement = confinement.itpa20_il_confinement_time( + # ITER Offset linear scaling - ITER 89-O (L-mode) + elif i_confinement_time == 7: + t_electron_confinement = confinement.iter_89_0_confinement_time( pcur, + rmajor, + rminor, + kappa, + dnla20, b_plasma_toroidal_on_axis, + m_fuel_amu, p_plasma_loss_mw, - dnla19, - physics_variables.m_ions_total_amu, - rmajor, - physics_variables.triang, - physics_variables.kappa_ipb, ) + # ======================================================================== - # ========================================================================== - - else: - raise ProcessValueError( - "Illegal value for i_confinement_time", - i_confinement_time=i_confinement_time, + # Rebut-Lallia offset linear scaling (L-mode) + elif i_confinement_time == 8: + t_electron_confinement = confinement.rebut_lallia_confinement_time( + rminor, + rmajor, + kappa, + m_fuel_amu, + pcur, + zeff, + dnla20, + b_plasma_toroidal_on_axis, + p_plasma_loss_mw, ) - # Apply H-factor correction to chosen scaling - t_electron_energy_confinement = hfact * t_electron_confinement - - # Ion energy confinement time - t_ion_energy_confinement = t_electron_energy_confinement + # ======================================================================== - # Calculate H* non-radiation corrected H factor - # Note: we will assume the IPB-98y2 scaling. - if physics_variables.i_rad_loss == 1: - physics_variables.hstar = ( - hfact - * ( - p_plasma_loss_mw - / ( - p_plasma_loss_mw - + physics_variables.pden_plasma_sync_mw * vol_plasma - + physics_variables.p_plasma_inner_rad_mw - ) - ) - ** 0.31 - ) - elif physics_variables.i_rad_loss == 0: - physics_variables.hstar = ( - hfact - * ( - p_plasma_loss_mw - / ( - p_plasma_loss_mw - + physics_variables.pden_plasma_rad_mw * vol_plasma - ) - ) - ** 0.31 + # Goldston scaling (L-mode) + elif i_confinement_time == 9: # Goldston scaling (L-mode) + t_electron_confinement = confinement.goldston_confinement_time( + pcur, rmajor, rminor, kappa95, m_fuel_amu, p_plasma_loss_mw ) - elif physics_variables.i_rad_loss == 2: - physics_variables.hstar = hfact - - # Calculation of the transport power loss terms - # Transport losses in Watts/m3 are 3/2 * n.e.T / tau , with T in eV - # (here, temp_plasma_ion_density_weighted_kev and temp_plasma_electron_density_weighted_kev are in keV, and pden_electron_transport_loss_mw and pden_ion_transport_loss_mw are in MW/m3) - - # The transport losses is just the electron and ion thermal energies divided by the confinement time. - pden_ion_transport_loss_mw = ( - (3 / 2) - * (constants.ELECTRON_CHARGE / 1e3) - * nd_plasma_ions_total_vol_avg - * temp_plasma_ion_density_weighted_kev - / t_ion_energy_confinement - ) - pden_electron_transport_loss_mw = ( - (3 / 2) - * (constants.ELECTRON_CHARGE / 1e3) - * nd_plasma_electrons_vol_avg - * temp_plasma_electron_density_weighted_kev - / t_electron_energy_confinement - ) - - ratio = (nd_plasma_ions_total_vol_avg / nd_plasma_electrons_vol_avg) * ( - temp_plasma_ion_density_weighted_kev - / temp_plasma_electron_density_weighted_kev - ) - - # Global energy confinement time - - t_energy_confinement = (ratio + 1.0e0) / ( - ratio / t_ion_energy_confinement + 1.0e0 / t_electron_energy_confinement - ) - - # For comparison directly calculate the confinement time from the stored energy calculated - # from the total plasma beta and the loss power used above. - physics_variables.t_energy_confinement_beta = ( - physics_variables.e_plasma_beta / 1e6 - ) / p_plasma_loss_mw - - return ( - pden_electron_transport_loss_mw, - pden_ion_transport_loss_mw, - t_electron_energy_confinement, - t_ion_energy_confinement, - t_energy_confinement, - p_plasma_loss_mw, - ) - - @staticmethod - def calculate_plasma_masses( - m_fuel_amu: float, - m_ions_total_amu: float, - nd_plasma_ions_total_vol_avg: float, - nd_plasma_fuel_ions_vol_avg: float, - nd_plasma_alphas_vol_avg: float, - vol_plasma: float, - nd_plasma_electrons_vol_avg: float, - ) -> tuple[float, float, float, float, float]: - """Calculate the plasma masses. - - Parameters - ---------- - m_fuel_amu : float - Average mass of fuel (amu). - m_ions_total_amu : float - Average mass of all ions (amu). - nd_plasma_ions_total_vol_avg : float - Total ion density (/m3). - nd_plasma_fuel_ions_vol_avg : float - Fuel ion density (/m3). - nd_plasma_alphas_vol_avg : float - Alpha ash density (/m3). - vol_plasma : float - Plasma volume (m3). - nd_plasma_electrons_vol_avg : float - Volume averaged electron density (/m3). - - Returns - ------- - tuple[float, float, float, float, float] - A tuple containing: - - """ - # Calculate mass of fuel ions - m_plasma_fuel_ions = (m_fuel_amu * constants.ATOMIC_MASS_UNIT) * ( - nd_plasma_fuel_ions_vol_avg * vol_plasma - ) - - m_plasma_ions_total = (m_ions_total_amu * constants.ATOMIC_MASS_UNIT) * ( - nd_plasma_ions_total_vol_avg * vol_plasma - ) - - m_plasma_alpha = (nd_plasma_alphas_vol_avg * vol_plasma) * constants.ALPHA_MASS - - m_plasma_electron = constants.ELECTRON_MASS * ( - nd_plasma_electrons_vol_avg * vol_plasma - ) - - m_plasma = m_plasma_electron + m_plasma_ions_total - - return ( - m_plasma_fuel_ions, - m_plasma_ions_total, - m_plasma_alpha, - m_plasma_electron, - m_plasma, - ) + # ======================================================================== + # T-10 scaling (L-mode) + elif i_confinement_time == 10: + t_electron_confinement = confinement.t10_confinement_time( + dnla20, + rmajor, + qstar, + b_plasma_toroidal_on_axis, + rminor, + kappa95, + p_plasma_loss_mw, + zeff, + pcur, + ) -def res_diff_time(rmajor, res_plasma, kappa95): - """Calculates resistive diffusion time + # ======================================================================== - Parameters - ---------- - rmajor : - plasma major radius (m) - res_plasma : - plasma resistivity (Ohms) - kappa95 : - plasma elongation at 95% flux surface + # JAERI / Odajima-Shimomura L-mode scaling + elif i_confinement_time == 11: # JAERI scaling + t_electron_confinement = confinement.jaeri_confinement_time( + kappa95, + rminor, + m_fuel_amu, + n20, + pcur, + b_plasma_toroidal_on_axis, + rmajor, + qstar, + zeff, + p_plasma_loss_mw, + ) - """ + # ======================================================================== - return 2 * constants.RMU0 * rmajor / (res_plasma * kappa95) + # Kaye "big" L-mode scaling (based only on big tokamak data) + elif i_confinement_time == 12: + t_electron_confinement = confinement.kaye_big_confinement_time( + rmajor, + rminor, + b_plasma_toroidal_on_axis, + kappa95, + pcur, + n20, + m_fuel_amu, + p_plasma_loss_mw, + ) + # ======================================================================== -def l_h_threshold_power( - nd_plasma_electron_line: float, - b_plasma_toroidal_on_axis: float, - rmajor: float, - rminor: float, - kappa: float, - a_plasma_surface: float, - m_ions_total_amu: float, - aspect: float, - plasma_current: float, -) -> list[float]: - """L-mode to H-mode power threshold calculation. + # ITER H90-P H-mode scaling + elif i_confinement_time == 13: + t_electron_confinement = confinement.iter_h90_p_confinement_time( + pcur, + rmajor, + rminor, + kappa, + dnla20, + b_plasma_toroidal_on_axis, + m_fuel_amu, + p_plasma_loss_mw, + ) - Parameters - ---------- - nd_plasma_electron_line : float - Line-averaged electron density (/m3) - b_plasma_toroidal_on_axis : float - Toroidal field on axis (T) - rmajor : float - Plasma major radius (m) - rminor : float - Plasma minor radius (m) - kappa : float - Plasma elongation - a_plasma_surface : float - Plasma surface area (m2) - m_ions_total_amu : float - Average mass of all ions (amu) - aspect : float - Aspect ratio - plasma_current : float - Plasma current (A) + # ======================================================================== - Returns - ------- - list[float] - Array of power thresholds + # Minimum of ITER 89-P and ITER 89-O + elif i_confinement_time == 14: + t_electron_confinement = min( + confinement.iter_89p_confinement_time( + pcur, + rmajor, + rminor, + kappa, + dnla20, + b_plasma_toroidal_on_axis, + m_fuel_amu, + p_plasma_loss_mw, + ), + confinement.iter_89_0_confinement_time( + pcur, + rmajor, + rminor, + kappa, + dnla20, + b_plasma_toroidal_on_axis, + m_fuel_amu, + p_plasma_loss_mw, + ), + ) - """ + # ======================================================================== - dnla20 = 1e-20 * nd_plasma_electron_line + # Riedel scaling (L-mode) + elif i_confinement_time == 15: + t_electron_confinement = confinement.riedel_l_confinement_time( + pcur, + rmajor, + rminor, + kappa95, + dnla20, + b_plasma_toroidal_on_axis, + p_plasma_loss_mw, + ) - # ======================================================================== + # ======================================================================== - # ITER-1996 H-mode power threshold database - # Fit to 1996 H-mode power threshold database: nominal + # Christiansen et al scaling (L-mode) + elif i_confinement_time == 16: + t_electron_confinement = confinement.christiansen_confinement_time( + pcur, + rmajor, + rminor, + kappa95, + dnla20, + b_plasma_toroidal_on_axis, + p_plasma_loss_mw, + m_fuel_amu, + ) - # i_l_h_threshold = 1 - iterdd = transition.calculate_iter1996_nominal( - dnla20, b_plasma_toroidal_on_axis, rmajor - ) + # ======================================================================== - # Fit to 1996 H-mode power threshold database: upper bound - # i_l_h_threshold = 2 - iterdd_ub = transition.calculate_iter1996_upper( - dnla20, b_plasma_toroidal_on_axis, rmajor - ) + # Lackner-Gottardi scaling (L-mode) + elif i_confinement_time == 17: + t_electron_confinement = confinement.lackner_gottardi_confinement_time( + pcur, + rmajor, + rminor, + kappa95, + dnla20, + b_plasma_toroidal_on_axis, + p_plasma_loss_mw, + ) - # Fit to 1996 H-mode power threshold database: lower bound - # i_l_h_threshold = 3 - iterdd_lb = transition.calculate_iter1996_lower( - dnla20, b_plasma_toroidal_on_axis, rmajor - ) + # ======================================================================== - # ======================================================================== + # Neo-Kaye scaling (L-mode) + elif i_confinement_time == 18: + t_electron_confinement = confinement.neo_kaye_confinement_time( + pcur, + rmajor, + rminor, + kappa95, + dnla20, + b_plasma_toroidal_on_axis, + p_plasma_loss_mw, + ) - # Snipes 1997 ITER H-mode power threshold + # ======== ================================================================ - # i_l_h_threshold = 4 - snipes_1997 = transition.calculate_snipes1997_iter( - dnla20, b_plasma_toroidal_on_axis, rmajor - ) + # Riedel scaling (H-mode) + elif i_confinement_time == 19: + t_electron_confinement = confinement.riedel_h_confinement_time( + pcur, + rmajor, + rminor, + kappa95, + dnla20, + b_plasma_toroidal_on_axis, + m_fuel_amu, + p_plasma_loss_mw, + ) - # i_l_h_threshold = 5 - snipes_1997_kappa = transition.calculate_snipes1997_kappa( - dnla20, b_plasma_toroidal_on_axis, rmajor, kappa - ) + # ======================================================================== - # ======================================================================== + # Amended version of ITER H90-P law + elif i_confinement_time == 20: + t_electron_confinement = confinement.iter_h90_p_amended_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + m_fuel_amu, + rmajor, + p_plasma_loss_mw, + kappa, + ) - # Martin et al (2008) for recent ITER scaling, with mass correction - # and 95% confidence limits + # ========================================================================== - # i_l_h_threshold = 6 - martin_nominal = transition.calculate_martin08_nominal( - dnla20, b_plasma_toroidal_on_axis, a_plasma_surface, m_ions_total_amu - ) + # Sudo et al. scaling (stellarators/heliotron) + elif i_confinement_time == 21: + t_electron_confinement = confinement.sudo_et_al_confinement_time( + rmajor, + rminor, + dnla20, + b_plasma_toroidal_on_axis, + p_plasma_loss_mw, + ) - # i_l_h_threshold = 7 - martin_ub = transition.calculate_martin08_upper( - dnla20, b_plasma_toroidal_on_axis, a_plasma_surface, m_ions_total_amu - ) + # ========================================================================== - # i_l_h_threshold = 8 - martin_lb = transition.calculate_martin08_lower( - dnla20, b_plasma_toroidal_on_axis, a_plasma_surface, m_ions_total_amu - ) + # Gyro-reduced Bohm scaling + elif i_confinement_time == 22: + t_electron_confinement = confinement.gyro_reduced_bohm_confinement_time( + b_plasma_toroidal_on_axis, + dnla20, + p_plasma_loss_mw, + rminor, + rmajor, + ) - # ======================================================================== + # ========================================================================== - # Snipes et al (2000) scaling with mass correction - # Nominal, upper and lower + # Lackner-Gottardi stellarator scaling + elif i_confinement_time == 23: + t_electron_confinement = ( + confinement.lackner_gottardi_stellarator_confinement_time( + rmajor, + rminor, + dnla20, + b_plasma_toroidal_on_axis, + p_plasma_loss_mw, + q95, + ) + ) - # i_l_h_threshold = 9 - snipes_2000 = transition.calculate_snipes2000_nominal( - dnla20, b_plasma_toroidal_on_axis, rmajor, rminor, m_ions_total_amu - ) + # ========================================================================== - # i_l_h_threshold = 10 - snipes_2000_ub = transition.calculate_snipes2000_upper( - dnla20, b_plasma_toroidal_on_axis, rmajor, rminor, m_ions_total_amu - ) + # ITER_93 ELM-free H-mode scaling + elif i_confinement_time == 24: + t_electron_confinement = confinement.iter_93h_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + p_plasma_loss_mw, + m_fuel_amu, + rmajor, + dnla20, + aspect, + kappa, + ) - # i_l_h_threshold = 11 - snipes_2000_lb = transition.calculate_snipes2000_lower( - dnla20, b_plasma_toroidal_on_axis, rmajor, rminor, m_ions_total_amu - ) + # ========================================================================== + # Scaling removed + elif i_confinement_time == 25: + raise ProcessValueError("Scaling removed") + # ========================================================================== - # ======================================================================== + # ELM-free: ITERH-97P + elif i_confinement_time == 26: + t_electron_confinement = confinement.iter_h97p_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + p_plasma_loss_mw, + dnla19, + rmajor, + aspect, + kappa, + m_fuel_amu, + ) - # Snipes et al (2000) scaling (closed divertor) with mass correction - # Nominal, upper and lower + # ========================================================================== - # i_l_h_threshold = 12 - snipes_2000_cd = transition.calculate_snipes2000_closed_divertor_nominal( - dnla20, b_plasma_toroidal_on_axis, rmajor, m_ions_total_amu - ) + # ELMy: ITERH-97P(y) + elif i_confinement_time == 27: + t_electron_confinement = confinement.iter_h97p_elmy_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + p_plasma_loss_mw, + dnla19, + rmajor, + aspect, + kappa, + m_fuel_amu, + ) - # i_l_h_threshold = 13 - snipes_2000_cd_ub = transition.calculate_snipes2000_closed_divertor_upper( - dnla20, b_plasma_toroidal_on_axis, rmajor, m_ions_total_amu - ) + # ========================================================================== - # i_l_h_threshold = 14 - snipes_2000_cd_lb = transition.calculate_snipes2000_closed_divertor_lower( - dnla20, b_plasma_toroidal_on_axis, rmajor, m_ions_total_amu - ) + # ITER-96P (= ITER-97L) L-mode scaling + elif i_confinement_time == 28: + t_electron_confinement = confinement.iter_96p_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + kappa95, + rmajor, + aspect, + dnla19, + m_fuel_amu, + p_plasma_loss_mw, + ) - # ======================================================================== + # ========================================================================== - # Hubbard et al. 2012 L-I threshold scaling + # Valovic modified ELMy-H mode scaling + # WARNING: No reference found for this scaling. This may not be its real name + elif i_confinement_time == 29: + t_electron_confinement = confinement.valovic_elmy_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + dnla19, + m_fuel_amu, + rmajor, + rminor, + kappa, + p_plasma_loss_mw, + ) - # i_l_h_threshold = 15 - hubbard_2012 = transition.calculate_hubbard2012_nominal(plasma_current, dnla20) + # ========================================================================== - # i_l_h_threshold = 16 - hubbard_2012_lb = transition.calculate_hubbard2012_lower(plasma_current, dnla20) + # Kaye PPPL Workshop April 1998 L-mode scaling + # WARNING: No reference found for this scaling. This may not be its real name + elif i_confinement_time == 30: + t_electron_confinement = confinement.kaye_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + kappa, + rmajor, + aspect, + dnla19, + m_fuel_amu, + p_plasma_loss_mw, + ) - # i_l_h_threshold = 17 - hubbard_2012_ub = transition.calculate_hubbard2012_upper(plasma_current, dnla20) + # ========================================================================== - # ======================================================================== + # ITERH-PB98P(y), ELMy H-mode scaling + # WARNING: No reference found for this scaling. This may not be its real name + elif i_confinement_time == 31: + t_electron_confinement = confinement.iter_pb98py_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + dnla19, + p_plasma_loss_mw, + rmajor, + physics_variables.kappa_ipb, + aspect, + m_fuel_amu, + ) - # Hubbard et al. 2017 L-I threshold scaling + # ========================================================================== - # i_l_h_threshold = 18 - hubbard_2017 = transition.calculate_hubbard2017( - dnla20, a_plasma_surface, b_plasma_toroidal_on_axis - ) + # IPB98(y), ELMy H-mode scaling + elif i_confinement_time == 32: + t_electron_confinement = confinement.iter_ipb98y_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + dnla19, + p_plasma_loss_mw, + rmajor, + kappa, + aspect, + m_fuel_amu, + ) - # ======================================================================== + # ========================================================================== - # Aspect ratio corrected Martin et al (2008) + # IPB98(y,1), ELMy H-mode scaling + elif i_confinement_time == 33: + t_electron_confinement = confinement.iter_ipb98y1_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + dnla19, + p_plasma_loss_mw, + rmajor, + physics_variables.kappa_ipb, + aspect, + m_fuel_amu, + ) - # i_l_h_threshold = 19 - martin_nominal_aspect = transition.calculate_martin08_aspect_nominal( - dnla20, b_plasma_toroidal_on_axis, a_plasma_surface, m_ions_total_amu, aspect - ) + # ========================================================================== - # i_l_h_threshold = 20 - martin_ub_aspect = transition.calculate_martin08_aspect_upper( - dnla20, b_plasma_toroidal_on_axis, a_plasma_surface, m_ions_total_amu, aspect - ) + # IPB98(y,2), ELMy H-mode scaling + elif i_confinement_time == 34: + t_electron_confinement = confinement.iter_ipb98y2_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + dnla19, + p_plasma_loss_mw, + rmajor, + physics_variables.kappa_ipb, + aspect, + m_fuel_amu, + ) - # i_l_h_threshold = 21 - martin_lb_aspect = transition.calculate_martin08_aspect_lower( - dnla20, b_plasma_toroidal_on_axis, a_plasma_surface, m_ions_total_amu, aspect - ) + # ========================================================================== - # ======================================================================== + # IPB98(y,3), ELMy H-mode scaling + elif i_confinement_time == 35: + t_electron_confinement = confinement.iter_ipb98y3_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + dnla19, + p_plasma_loss_mw, + rmajor, + physics_variables.kappa_ipb, + aspect, + m_fuel_amu, + ) - return [ - iterdd, - iterdd_ub, - iterdd_lb, - snipes_1997, - snipes_1997_kappa, - martin_nominal, - martin_ub, - martin_lb, - snipes_2000, - snipes_2000_ub, - snipes_2000_lb, - snipes_2000_cd, - snipes_2000_cd_ub, - snipes_2000_cd_lb, - hubbard_2012, - hubbard_2012_lb, - hubbard_2012_ub, - hubbard_2017, - martin_nominal_aspect, - martin_ub_aspect, - martin_lb_aspect, - ] + # ========================================================================== + # IPB98(y,4), ELMy H-mode scaling + elif i_confinement_time == 36: + t_electron_confinement = confinement.iter_ipb98y4_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + dnla19, + p_plasma_loss_mw, + rmajor, + physics_variables.kappa_ipb, + aspect, + m_fuel_amu, + ) -def reinke_tsep(b_plasma_toroidal_on_axis, flh, qstar, rmajor, eps, fgw, kappa, lhat): - """Function for calculating upstream temperature(keV) in Reinke model - This function calculates the upstream temperature in the - divertor/SoL model used for the Reinke citerion. - Issue #707 - M.L. Reinke 2017 Nucl. Fusion 57 034004 + # ========================================================================== - Parameters - ---------_ - b_plasma_toroidal_on_axis : - toroidal field on axis (T) - flh : - fraction of Psep/P_LH - qstar : - safety factor similar to q95 (see #707) - rmajor : - major radius (m) - eps : - inverse aspect ratio - fgw : - ratio of volume averaged density to n_GW - kappa : - elongation - lhat : - connection length factor - """ - kappa_0 = 2.0e3 # Stangeby W/m/eV^(7/2) + # ISS95 stellarator scaling + elif i_confinement_time == 37: + # dummy argument q95 is actual argument iotabar for stellarators + iotabar = q95 + t_electron_confinement = confinement.iss95_stellarator_confinement_time( + rminor, + rmajor, + dnla19, + b_plasma_toroidal_on_axis, + p_plasma_loss_mw, + iotabar, + ) - return ( - ( - b_plasma_toroidal_on_axis**0.72 - * flh**0.29 - * fgw**0.21 - * qstar**0.08 - * rmajor**0.33 - ) - * (eps**0.15 * (1.0 + kappa**2.0) ** 0.34) - * (lhat**0.29 * kappa_0 ** (-0.29) * 0.285) - ) + # ========================================================================== + # ISS04 stellarator scaling + elif i_confinement_time == 38: + # dummy argument q95 is actual argument iotabar for stellarators + iotabar = q95 + t_electron_confinement = confinement.iss04_stellarator_confinement_time( + rminor, + rmajor, + dnla19, + b_plasma_toroidal_on_axis, + p_plasma_loss_mw, + iotabar, + ) -class BetaNormMaxModel(IntEnum): - """Beta norm max (β_N_max) model types""" + # ========================================================================== - USER_INPUT = 0 - WESSON = 1 - ORIGINAL_SCALING = 2 - MENARD = 3 - THLOREUS = 4 - STAMBAUGH = 5 + # DS03 beta-independent H-mode scaling + elif i_confinement_time == 39: + t_electron_confinement = confinement.ds03_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + dnla19, + p_plasma_loss_mw, + rmajor, + kappa95, + aspect, + m_fuel_amu, + ) + # ========================================================================== -class PlasmaBeta: - """Class to hold plasma beta calculations for plasma processing.""" + # Murari "Non-power law" scaling + elif i_confinement_time == 40: + t_electron_confinement = confinement.murari_confinement_time( + pcur, + rmajor, + physics_variables.kappa_ipb, + dnla19, + b_plasma_toroidal_on_axis, + p_plasma_loss_mw, + ) - def __init__(self): - self.outfile = constants.NOUT - self.mfile = constants.MFILE + # ========================================================================== - def get_beta_norm_max_value(self, model: BetaNormMaxModel) -> float: - """Get the beta norm max value (β_N_max) for the specified model. + # Petty08, beta independent dimensionless scaling + elif i_confinement_time == 41: + t_electron_confinement = confinement.petty08_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + dnla19, + p_plasma_loss_mw, + rmajor, + physics_variables.kappa_ipb, + aspect, + ) - Parameters - ---------- - model: BetaNormMaxModel : - """ - model_map = { - BetaNormMaxModel.USER_INPUT: physics_variables.beta_norm_max, - BetaNormMaxModel.WESSON: physics_variables.beta_norm_max_wesson, - BetaNormMaxModel.ORIGINAL_SCALING: physics_variables.beta_norm_max_original_scaling, - BetaNormMaxModel.MENARD: physics_variables.beta_norm_max_menard, - BetaNormMaxModel.THLOREUS: physics_variables.beta_norm_max_thloreus, - BetaNormMaxModel.STAMBAUGH: physics_variables.beta_norm_max_stambaugh, - } - return model_map[model] + # ========================================================================== - def run(self): - """Calculate plasma beta values.""" + # Lang high density relevant confinement scaling + elif i_confinement_time == 42: + t_electron_confinement = confinement.lang_high_density_confinement_time( + plasma_current, + b_plasma_toroidal_on_axis, + nd_plasma_electron_line, + p_plasma_loss_mw, + rmajor, + rminor, + q95, + qstar, + aspect, + m_fuel_amu, + physics_variables.kappa_ipb, + ) - # ----------------------------------------------------- - # Normalised Beta Limit - # ----------------------------------------------------- + # ========================================================================== - # Normalised beta from Troyon beta limit - physics_variables.beta_norm_total = self.calculate_normalised_beta( - beta=physics_variables.beta_total_vol_avg, - rminor=physics_variables.rminor, - c_plasma=physics_variables.plasma_current, - b_field=physics_variables.b_plasma_toroidal_on_axis, - ) + # Hubbard 2017 I-mode confinement time scaling - nominal + elif i_confinement_time == 43: + t_electron_confinement = confinement.hubbard_nominal_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + dnla20, + p_plasma_loss_mw, + ) - # Define beta_norm_max calculations + # ========================================================================== - physics_variables.beta_norm_max_wesson = self.calculate_beta_norm_max_wesson( - ind_plasma_internal_norm=physics_variables.ind_plasma_internal_norm - ) + # Hubbard 2017 I-mode confinement time scaling - lower + elif i_confinement_time == 44: + t_electron_confinement = confinement.hubbard_lower_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + dnla20, + p_plasma_loss_mw, + ) - # Original scaling law - physics_variables.beta_norm_max_original_scaling = ( - self.calculate_beta_norm_max_original(eps=physics_variables.eps) - ) + # ========================================================================== - # J. Menard scaling law - physics_variables.beta_norm_max_menard = self.calculate_beta_norm_max_menard( - eps=physics_variables.eps - ) + # Hubbard 2017 I-mode confinement time scaling - upper + elif i_confinement_time == 45: + t_electron_confinement = confinement.hubbard_upper_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + dnla20, + p_plasma_loss_mw, + ) - # E. Tholerus scaling law - physics_variables.beta_norm_max_thloreus = self.calculate_beta_norm_max_thloreus( - c_beta=physics_variables.c_beta, - pres_plasma_on_axis=physics_variables.pres_plasma_thermal_on_axis, - pres_plasma_vol_avg=physics_variables.pres_plasma_thermal_vol_avg, - ) + # ========================================================================== - # R. D. Stambaugh scaling law - physics_variables.beta_norm_max_stambaugh = ( - self.calculate_beta_norm_max_stambaugh( - f_c_plasma_bootstrap=current_drive_variables.f_c_plasma_bootstrap, - kappa=physics_variables.kappa, - aspect=physics_variables.aspect, + # Menard NSTX, ELMy H-mode scaling + elif i_confinement_time == 46: + t_electron_confinement = confinement.menard_nstx_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + dnla19, + p_plasma_loss_mw, + rmajor, + physics_variables.kappa_ipb, + aspect, + m_fuel_amu, ) - ) - - # Calculate beta_norm_max based on i_beta_norm_max - try: - model = BetaNormMaxModel(int(physics_variables.i_beta_norm_max)) - physics_variables.beta_norm_max = self.get_beta_norm_max_value(model) - except ValueError: - raise ProcessValueError( - "Illegal value of i_beta_norm_max", - i_beta_norm_max=physics_variables.i_beta_norm_max, - ) from None - # calculate_beta_limit() returns the beta_vol_avg_max for beta - physics_variables.beta_vol_avg_max = self.calculate_beta_limit_from_norm( - b_plasma_toroidal_on_axis=physics_variables.b_plasma_toroidal_on_axis, - beta_norm_max=physics_variables.beta_norm_max, - plasma_current=physics_variables.plasma_current, - rminor=physics_variables.rminor, - ) + # ========================================================================== - physics_variables.beta_toroidal_vol_avg = ( - physics_variables.beta_total_vol_avg - * physics_variables.b_plasma_total**2 - / physics_variables.b_plasma_toroidal_on_axis**2 - ) + # Menard NSTX-Petty08 Hybrid + elif i_confinement_time == 47: + t_electron_confinement = ( + confinement.menard_nstx_petty08_hybrid_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + dnla19, + p_plasma_loss_mw, + rmajor, + physics_variables.kappa_ipb, + aspect, + m_fuel_amu, + ) + ) - # Calculate physics_variables.beta poloidal [-] - physics_variables.beta_poloidal_vol_avg = self.calculate_poloidal_beta( - b_plasma_total=physics_variables.b_plasma_total, - b_plasma_poloidal_average=physics_variables.b_plasma_poloidal_average, - beta=physics_variables.beta_total_vol_avg, - ) + # ========================================================================== - physics_variables.beta_thermal_vol_avg = ( - physics_variables.beta_total_vol_avg - - physics_variables.beta_fast_alpha - - physics_variables.beta_beam - ) + # NSTX gyro-Bohm (Buxton) + elif i_confinement_time == 48: + t_electron_confinement = confinement.nstx_gyro_bohm_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + p_plasma_loss_mw, + rmajor, + dnla20, + ) - physics_variables.beta_poloidal_eps = ( - physics_variables.beta_poloidal_vol_avg * physics_variables.eps - ) + # ========================================================================== - physics_variables.beta_thermal_poloidal_vol_avg = ( - physics_variables.beta_thermal_vol_avg - * ( - physics_variables.b_plasma_total - / physics_variables.b_plasma_poloidal_average - ) - ** 2 - ) - physics_variables.beta_thermal_toroidal_vol_avg = ( - physics_variables.beta_thermal_vol_avg - * ( - physics_variables.b_plasma_total - / physics_variables.b_plasma_toroidal_on_axis + # ITPA20 H-mode scaling + elif i_confinement_time == 49: + t_electron_confinement = confinement.itpa20_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + dnla19, + p_plasma_loss_mw, + rmajor, + physics_variables.triang, + physics_variables.kappa_ipb, + eps, + physics_variables.m_ions_total_amu, ) - ** 2 - ) - # ======================================================= + # ========================================================================== - # Mirror the pressure profiles to match the doubled toroidal field profile - pres_profile_total = np.concatenate([ - physics_variables.pres_plasma_thermal_total_profile[::-1], - physics_variables.pres_plasma_thermal_total_profile, - ]) + # ITPA20-IL confinement time scaling + elif i_confinement_time == 50: + t_electron_confinement = confinement.itpa20_il_confinement_time( + pcur, + b_plasma_toroidal_on_axis, + p_plasma_loss_mw, + dnla19, + physics_variables.m_ions_total_amu, + rmajor, + physics_variables.triang, + physics_variables.kappa_ipb, + ) - physics_variables.beta_thermal_toroidal_profile = np.array([ - self.calculate_plasma_beta( - pres_plasma=pres_profile_total[i], - b_field=physics_variables.b_plasma_toroidal_profile[i], + # ========================================================================== + + else: + raise ProcessValueError( + "Illegal value for i_confinement_time", + i_confinement_time=i_confinement_time, ) - for i in range(len(physics_variables.b_plasma_toroidal_profile)) - ]) - # ======================================================= + # Apply H-factor correction to chosen scaling + t_electron_energy_confinement = hfact * t_electron_confinement - physics_variables.beta_norm_thermal = self.calculate_normalised_beta( - beta=physics_variables.beta_thermal_vol_avg, - rminor=physics_variables.rminor, - c_plasma=physics_variables.plasma_current, - b_field=physics_variables.b_plasma_toroidal_on_axis, - ) + # Ion energy confinement time + t_ion_energy_confinement = t_electron_energy_confinement - physics_variables.beta_norm_toroidal = ( - physics_variables.beta_norm_total - * ( - physics_variables.b_plasma_total - / physics_variables.b_plasma_toroidal_on_axis + # Calculate H* non-radiation corrected H factor + # Note: we will assume the IPB-98y2 scaling. + if physics_variables.i_rad_loss == 1: + physics_variables.hstar = ( + hfact + * ( + p_plasma_loss_mw + / ( + p_plasma_loss_mw + + physics_variables.pden_plasma_sync_mw * vol_plasma + + physics_variables.p_plasma_inner_rad_mw + ) + ) + ** 0.31 ) - ** 2 - ) - physics_variables.beta_norm_poloidal = ( - physics_variables.beta_norm_total - * ( - physics_variables.b_plasma_total - / physics_variables.b_plasma_poloidal_average + elif physics_variables.i_rad_loss == 0: + physics_variables.hstar = ( + hfact + * ( + p_plasma_loss_mw + / ( + p_plasma_loss_mw + + physics_variables.pden_plasma_rad_mw * vol_plasma + ) + ) + ** 0.31 ) - ** 2 - ) + elif physics_variables.i_rad_loss == 2: + physics_variables.hstar = hfact - physics_variables.f_beta_alpha_beam_thermal = ( - physics_variables.beta_fast_alpha + physics_variables.beta_beam - ) / physics_variables.beta_thermal_vol_avg + # Calculation of the transport power loss terms + # Transport losses in Watts/m3 are 3/2 * n.e.T / tau , with T in eV + # (here, temp_plasma_ion_density_weighted_kev and temp_plasma_electron_density_weighted_kev are in keV, and pden_electron_transport_loss_mw and pden_ion_transport_loss_mw are in MW/m3) - # Plasma thermal energy derived from the thermal beta - physics_variables.e_plasma_beta_thermal = self.calculate_plasma_energy_from_beta( - beta=physics_variables.beta_thermal_vol_avg, - b_field=physics_variables.b_plasma_total, - vol_plasma=physics_variables.vol_plasma, + # The transport losses is just the electron and ion thermal energies divided by the confinement time. + pden_ion_transport_loss_mw = ( + (3 / 2) + * (constants.ELECTRON_CHARGE / 1e3) + * nd_plasma_ions_total_vol_avg + * temp_plasma_ion_density_weighted_kev + / t_ion_energy_confinement ) - - # Plasma thermal energy derived from the total beta - physics_variables.e_plasma_beta = self.calculate_plasma_energy_from_beta( - beta=physics_variables.beta_total_vol_avg, - b_field=physics_variables.b_plasma_total, - vol_plasma=physics_variables.vol_plasma, + pden_electron_transport_loss_mw = ( + (3 / 2) + * (constants.ELECTRON_CHARGE / 1e3) + * nd_plasma_electrons_vol_avg + * temp_plasma_electron_density_weighted_kev + / t_electron_energy_confinement ) - @staticmethod - def calculate_plasma_beta( - pres_plasma: float | np.ndarray, b_field: float | np.ndarray - ) -> float | np.ndarray: - """Calculate the plasma beta (β) for a given pressure and field. + ratio = (nd_plasma_ions_total_vol_avg / nd_plasma_electrons_vol_avg) * ( + temp_plasma_ion_density_weighted_kev + / temp_plasma_electron_density_weighted_kev + ) - Plasma beta is the ratio of plasma pressure to magnetic pressure. + # Global energy confinement time - Parameters - ---------- - pres_plasma : float | np.ndarray - Plasma pressure (in Pascals). - b_field : float | np.ndarray - Magnetic field strength (in Tesla). + t_energy_confinement = (ratio + 1.0e0) / ( + ratio / t_ion_energy_confinement + 1.0e0 / t_electron_energy_confinement + ) - Returns - ------- - float | np.ndarray - The plasma beta (dimensionless). - """ + # For comparison directly calculate the confinement time from the stored energy calculated + # from the total plasma beta and the loss power used above. + physics_variables.t_energy_confinement_beta = ( + physics_variables.e_plasma_beta / 1e6 + ) / p_plasma_loss_mw - return 2 * constants.RMU0 * pres_plasma / (b_field**2) + return ( + pden_electron_transport_loss_mw, + pden_ion_transport_loss_mw, + t_electron_energy_confinement, + t_ion_energy_confinement, + t_energy_confinement, + p_plasma_loss_mw, + ) @staticmethod - def calculate_beta_norm_max_wesson(ind_plasma_internal_norm: float) -> float: - """Calculate the Wesson normalsied beta upper limit. + def calculate_plasma_masses( + m_fuel_amu: float, + m_ions_total_amu: float, + nd_plasma_ions_total_vol_avg: float, + nd_plasma_fuel_ions_vol_avg: float, + nd_plasma_alphas_vol_avg: float, + vol_plasma: float, + nd_plasma_electrons_vol_avg: float, + ) -> tuple[float, float, float, float, float]: + """Calculate the plasma masses. Parameters ---------- - ind_plasma_internal_norm : float - Plasma normalised internal inductance - + m_fuel_amu : float + Average mass of fuel (amu). + m_ions_total_amu : float + Average mass of all ions (amu). + nd_plasma_ions_total_vol_avg : float + Total ion density (/m3). + nd_plasma_fuel_ions_vol_avg : float + Fuel ion density (/m3). + nd_plasma_alphas_vol_avg : float + Alpha ash density (/m3). + vol_plasma : float + Plasma volume (m3). + nd_plasma_electrons_vol_avg : float + Volume averaged electron density (/m3). - Returns + Returns ------- - float - The Wesson normalised beta upper limit. - - Notes - ----- - - It is recommended to use this method with the other Wesson relations for normalsied internal - inductance and current profile index. - - This fit is derived from the DIII-D database for β_N >= 2.5 - - References - ---------- - - Wesson, J. (2011) Tokamaks. 4th Edition, 2011 Oxford Science Publications, - International Series of Monographs on Physics, Volume 149. - - - T. T. S et al., “Profile Optimization and High Beta Discharges and Stability of High Elongation Plasmas in the DIII-D Tokamak,” - Osti.gov, Oct. 1990. https://www.osti.gov/biblio/6194284 (accessed Dec. 19, 2024). + tuple[float, float, float, float, float] + A tuple containing: """ - return 4 * ind_plasma_internal_norm - @staticmethod - def calculate_beta_norm_max_original(eps: float) -> float: - """Calculate the original scaling law normalsied beta upper limit. + # Calculate mass of fuel ions + m_plasma_fuel_ions = (m_fuel_amu * constants.ATOMIC_MASS_UNIT) * ( + nd_plasma_fuel_ions_vol_avg * vol_plasma + ) - Parameters - ---------- - eps : float - Plasma normalised internal inductance + m_plasma_ions_total = (m_ions_total_amu * constants.ATOMIC_MASS_UNIT) * ( + nd_plasma_ions_total_vol_avg * vol_plasma + ) - Returns - ------- - float + m_plasma_alpha = (nd_plasma_alphas_vol_avg * vol_plasma) * constants.ALPHA_MASS - References - ---------- - The original scaling law normalised beta upper limit. + m_plasma_electron = constants.ELECTRON_MASS * ( + nd_plasma_electrons_vol_avg * vol_plasma + ) - """ - return 2.7 * (1.0 + 5.0 * eps**3.5) + m_plasma = m_plasma_electron + m_plasma_ions_total - @staticmethod - def calculate_beta_norm_max_menard(eps: float) -> float: - """Calculate the Menard normalsied beta upper limit. + return ( + m_plasma_fuel_ions, + m_plasma_ions_total, + m_plasma_alpha, + m_plasma_electron, + m_plasma, + ) - Parameters - ---------- - eps : float - Plasma normalised internal inductance - Returns - ------- - float - The Menard normalised beta upper limit. +def res_diff_time(rmajor, res_plasma, kappa95): + """Calculates resistive diffusion time - Notes - ----- - - Found as a reasonable fit to the computed no wall limit at f_BS ≈ 50% - - Uses maximum κ data from NSTX at A = 1.45, A = 1.75. Along with record - β_T data from DIII-D at A = 2.9 and high κ. + Parameters + ---------- + rmajor : + plasma major radius (m) + res_plasma : + plasma resistivity (Ohms) + kappa95 : + plasma elongation at 95% flux surface - References - ---------- - - # J. E. Menard et al., “Fusion nuclear science facilities and pilot plants based on the spherical tokamak,” - Nuclear Fusion, vol. 56, no. 10, p. 106023, Aug. 2016, - doi: https://doi.org/10.1088/0029-5515/56/10/106023. + """ - """ - return 3.12 + 3.5 * eps**1.7 + return 2 * constants.RMU0 * rmajor / (res_plasma * kappa95) - @staticmethod - def calculate_beta_norm_max_thloreus( - c_beta: float, pres_plasma_on_axis: float, pres_plasma_vol_avg: float - ) -> float: - """Calculate the E. Tholerus normalized beta upper limit. - Parameters - ---------- - c_beta : float - Pressure peaking factor coefficient. - pres_plasma_on_axis : float - Central plasma pressure (Pa). - pres_plasma_vol_avg : float - Volume-averaged plasma pressure (Pa). - c_beta: float : +def l_h_threshold_power( + nd_plasma_electron_line: float, + b_plasma_toroidal_on_axis: float, + rmajor: float, + rminor: float, + kappa: float, + a_plasma_surface: float, + m_ions_total_amu: float, + aspect: float, + plasma_current: float, +) -> list[float]: + """L-mode to H-mode power threshold calculation. - pres_plasma_on_axis: float : + Parameters + ---------- + nd_plasma_electron_line : float + Line-averaged electron density (/m3) + b_plasma_toroidal_on_axis : float + Toroidal field on axis (T) + rmajor : float + Plasma major radius (m) + rminor : float + Plasma minor radius (m) + kappa : float + Plasma elongation + a_plasma_surface : float + Plasma surface area (m2) + m_ions_total_amu : float + Average mass of all ions (amu) + aspect : float + Aspect ratio + plasma_current : float + Plasma current (A) - pres_plasma_vol_avg: float : + Returns + ------- + list[float] + Array of power thresholds + """ - Returns - ------- - float - The E. Tholerus normalized beta upper limit. + dnla20 = 1e-20 * nd_plasma_electron_line - Notes - ----- - - This method calculates the normalized beta upper limit based on the pressure peaking factor (Fp), - which is defined as the ratio of the peak pressure to the average pressure. - - The formula is derived from operational space studies of flat-top plasma in the STEP power plant. + # ======================================================================== - References - ---------- - - E. Tholerus et al., “Flat-top plasma operational space of the STEP power plant,” - Nuclear Fusion, Aug. 2024, doi: https://doi.org/10.1088/1741-4326/ad6ea2. + # ITER-1996 H-mode power threshold database + # Fit to 1996 H-mode power threshold database: nominal - """ - return 3.7 + ( - (c_beta / (pres_plasma_on_axis / pres_plasma_vol_avg)) - * (12.5 - 3.5 * (pres_plasma_on_axis / pres_plasma_vol_avg)) - ) + # i_l_h_threshold = 1 + iterdd = transition.calculate_iter1996_nominal( + dnla20, b_plasma_toroidal_on_axis, rmajor + ) - @staticmethod - def calculate_beta_norm_max_stambaugh( - f_c_plasma_bootstrap: float, - kappa: float, - aspect: float, - ) -> float: - """Calculate the Stambaugh normalized beta upper limit. + # Fit to 1996 H-mode power threshold database: upper bound + # i_l_h_threshold = 2 + iterdd_ub = transition.calculate_iter1996_upper( + dnla20, b_plasma_toroidal_on_axis, rmajor + ) - Parameters - ---------- - f_c_plasma_bootstrap : float - Bootstrap current fraction. - kappa : float - Plasma separatrix elongation. - aspect : float - Plasma aspect ratio. + # Fit to 1996 H-mode power threshold database: lower bound + # i_l_h_threshold = 3 + iterdd_lb = transition.calculate_iter1996_lower( + dnla20, b_plasma_toroidal_on_axis, rmajor + ) + # ======================================================================== - Returns - ------- - float - The Stambaugh normalized beta upper limit. + # Snipes 1997 ITER H-mode power threshold - Notes - ----- - - This method calculates the normalized beta upper limit based on the Stambaugh scaling. - - The formula is derived from empirical fits to high-performance, steady-state tokamak equilibria. + # i_l_h_threshold = 4 + snipes_1997 = transition.calculate_snipes1997_iter( + dnla20, b_plasma_toroidal_on_axis, rmajor + ) - References - ---------- - - R. D. Stambaugh et al., “Fusion Nuclear Science Facility Candidates,” - Fusion Science and Technology, vol. 59, no. 2, pp. 279-307, Feb. 2011, - doi: https://doi.org/10.13182/fst59-279. + # i_l_h_threshold = 5 + snipes_1997_kappa = transition.calculate_snipes1997_kappa( + dnla20, b_plasma_toroidal_on_axis, rmajor, kappa + ) - - Y. R. Lin-Liu and R. D. Stambaugh, “Optimum equilibria for high performance, steady state tokamaks,” - Nuclear Fusion, vol. 44, no. 4, pp. 548-554, Mar. 2004, - doi: https://doi.org/10.1088/0029-5515/44/4/009. + # ======================================================================== - """ - return ( - f_c_plasma_bootstrap - * 10 - * (-0.7748 + (1.2869 * kappa) - (0.2921 * kappa**2) + (0.0197 * kappa**3)) - / (aspect**0.5523 * np.tanh((1.8524 + (0.2319 * kappa)) / aspect**0.6163)) - ) + # Martin et al (2008) for recent ITER scaling, with mass correction + # and 95% confidence limits - @staticmethod - def calculate_normalised_beta( - beta: float, rminor: float, c_plasma: float, b_field: float - ) -> float: - """Calculate normalised beta (β_N). + # i_l_h_threshold = 6 + martin_nominal = transition.calculate_martin08_nominal( + dnla20, b_plasma_toroidal_on_axis, a_plasma_surface, m_ions_total_amu + ) - Parameters - ---------- - beta : float - Plasma beta (fraction). - rminor : float - Plasma minor radius (m). - c_plasma : float - Plasma current (A). - b_field : float - Magnetic field (T). + # i_l_h_threshold = 7 + martin_ub = transition.calculate_martin08_upper( + dnla20, b_plasma_toroidal_on_axis, a_plasma_surface, m_ions_total_amu + ) - Returns - ------- - float - Normalised beta. + # i_l_h_threshold = 8 + martin_lb = transition.calculate_martin08_lower( + dnla20, b_plasma_toroidal_on_axis, a_plasma_surface, m_ions_total_amu + ) - Notes - ----- - - 1.0e8 is a conversion factor to get beta_N in standard units, as plasma current is normally in MA and - beta is in percentage instead of fraction. + # ======================================================================== - """ + # Snipes et al (2000) scaling with mass correction + # Nominal, upper and lower + + # i_l_h_threshold = 9 + snipes_2000 = transition.calculate_snipes2000_nominal( + dnla20, b_plasma_toroidal_on_axis, rmajor, rminor, m_ions_total_amu + ) + + # i_l_h_threshold = 10 + snipes_2000_ub = transition.calculate_snipes2000_upper( + dnla20, b_plasma_toroidal_on_axis, rmajor, rminor, m_ions_total_amu + ) - return 1.0e8 * (beta * rminor * b_field) / c_plasma + # i_l_h_threshold = 11 + snipes_2000_lb = transition.calculate_snipes2000_lower( + dnla20, b_plasma_toroidal_on_axis, rmajor, rminor, m_ions_total_amu + ) - @staticmethod - def calculate_plasma_energy_from_beta( - beta: float, b_field: float, vol_plasma: float - ) -> float: - """Calculate plasma thermal energy from beta. + # ======================================================================== - E_plasma = 1.5 * β * B² / (2 * μ_0) * V + # Snipes et al (2000) scaling (closed divertor) with mass correction + # Nominal, upper and lower - Parameters - ---------- - beta : float - Plasma beta (fraction). - b_field : float - Magnetic field (T). - vol_plasma : float - Plasma volume (m³). + # i_l_h_threshold = 12 + snipes_2000_cd = transition.calculate_snipes2000_closed_divertor_nominal( + dnla20, b_plasma_toroidal_on_axis, rmajor, m_ions_total_amu + ) - Returns - ------- - float - Plasma energy (J). - """ + # i_l_h_threshold = 13 + snipes_2000_cd_ub = transition.calculate_snipes2000_closed_divertor_upper( + dnla20, b_plasma_toroidal_on_axis, rmajor, m_ions_total_amu + ) - return (1.5e0 * beta * b_field**2) / (2.0e0 * constants.RMU0) * vol_plasma + # i_l_h_threshold = 14 + snipes_2000_cd_lb = transition.calculate_snipes2000_closed_divertor_lower( + dnla20, b_plasma_toroidal_on_axis, rmajor, m_ions_total_amu + ) - @staticmethod - def calculate_beta_limit_from_norm( - b_plasma_toroidal_on_axis: float, - beta_norm_max: float, - plasma_current: float, - rminor: float, - ) -> float: - """Calculate the maximum allowed beta (β) from a given normalised (β_N). + # ======================================================================== - This subroutine calculates the beta limit using the algorithm documented in AEA FUS 172. - The limit applies to beta defined with respect to the total B-field. - Switch i_beta_component determines which components of beta to include. + # Hubbard et al. 2012 L-I threshold scaling - Parameters - ---------- - b_plasma_toroidal_on_axis : float - Toroidal B-field on plasma axis [T]. - beta_norm_max : float - Troyon-like g coefficient. - plasma_current : float - Plasma current [A]. - rminor : float - Plasma minor axis [m]. + # i_l_h_threshold = 15 + hubbard_2012 = transition.calculate_hubbard2012_nominal(plasma_current, dnla20) - Returns - ------- - float - Beta limit as defined below. + # i_l_h_threshold = 16 + hubbard_2012_lb = transition.calculate_hubbard2012_lower(plasma_current, dnla20) - Notes - ----- - - If i_beta_component = 0, then the limit is applied to the total beta. - - If i_beta_component = 1, then the limit is applied to the thermal beta only. - - If i_beta_component = 2, then the limit is applied to the thermal + neutral beam beta components. - - If i_beta_component = 3, then the limit is applied to the toroidal beta. + # i_l_h_threshold = 17 + hubbard_2012_ub = transition.calculate_hubbard2012_upper(plasma_current, dnla20) - - The default value for the g coefficient is beta_norm_max = 3.5. + # ======================================================================== - References - ---------- - - F. Troyon et.al, “Beta limit in tokamaks. Experimental and computational status,” - Plasma Physics and Controlled Fusion, vol. 30, no. 11, pp. 1597-1609, Oct. 1988, - doi: https://doi.org/10.1088/0741-3335/30/11/019. + # Hubbard et al. 2017 L-I threshold scaling - - T.C.Hender et.al., 'Physics Assesment of the European Reactor Study', AEA FUS 172, 1992 - """ + # i_l_h_threshold = 18 + hubbard_2017 = transition.calculate_hubbard2017( + dnla20, a_plasma_surface, b_plasma_toroidal_on_axis + ) - # Multiplied by 0.01 to convert from % to fraction - return ( - 0.01 - * beta_norm_max - * (plasma_current / 1.0e6) - / (rminor * b_plasma_toroidal_on_axis) - ) + # ======================================================================== - @staticmethod - def calculate_poloidal_beta( - b_plasma_total: float, b_plasma_poloidal_average: float, beta: float - ) -> float: - """Calculates total poloidal beta (β_p) + # Aspect ratio corrected Martin et al (2008) - Parameters - ---------- - b_plasma_poloidal_average : float - The average poloidal magnetic field of the plasma (in Tesla). - beta : float - The plasma beta, a dimensionless parameter representing the ratio of plasma pressure to magnetic pressure. + # i_l_h_threshold = 19 + martin_nominal_aspect = transition.calculate_martin08_aspect_nominal( + dnla20, b_plasma_toroidal_on_axis, a_plasma_surface, m_ions_total_amu, aspect + ) - Returns - ------- - float - The calculated total poloidal beta. + # i_l_h_threshold = 20 + martin_ub_aspect = transition.calculate_martin08_aspect_upper( + dnla20, b_plasma_toroidal_on_axis, a_plasma_surface, m_ions_total_amu, aspect + ) - References - ---------- - - J.P. Freidberg, "Plasma physics and fusion energy", Cambridge University Press (2007) - Page 270 ISBN 0521851076 + # i_l_h_threshold = 21 + martin_lb_aspect = transition.calculate_martin08_aspect_lower( + dnla20, b_plasma_toroidal_on_axis, a_plasma_surface, m_ions_total_amu, aspect + ) - """ - return beta * (b_plasma_total / b_plasma_poloidal_average) ** 2 + # ======================================================================== - @staticmethod - def fast_alpha_beta( - b_plasma_poloidal_average: float, - b_plasma_toroidal_on_axis: float, - nd_plasma_electrons_vol_avg: float, - nd_plasma_fuel_ions_vol_avg: float, - nd_plasma_ions_total_vol_avg: float, - temp_plasma_electron_density_weighted_kev: float, - temp_plasma_ion_density_weighted_kev: float, - pden_alpha_total_mw: float, - pden_plasma_alpha_mw: float, - i_beta_fast_alpha: int, - ) -> float: - """Calculate the fast alpha beta (β_fast_alpha) component. + return [ + iterdd, + iterdd_ub, + iterdd_lb, + snipes_1997, + snipes_1997_kappa, + martin_nominal, + martin_ub, + martin_lb, + snipes_2000, + snipes_2000_ub, + snipes_2000_lb, + snipes_2000_cd, + snipes_2000_cd_ub, + snipes_2000_cd_lb, + hubbard_2012, + hubbard_2012_lb, + hubbard_2012_ub, + hubbard_2017, + martin_nominal_aspect, + martin_ub_aspect, + martin_lb_aspect, + ] - This function computes the fast alpha beta contribution based on the provided plasma parameters. - Parameters - ---------- - b_plasma_poloidal_average : float - Poloidal field (T). - b_plasma_toroidal_on_axis : float - Toroidal field on axis (T). - nd_plasma_electrons_vol_avg : float - Electron density (m⁻³). - nd_plasma_fuel_ions_vol_avg : float - Fuel ion density (m⁻³). - nd_plasma_ions_total_vol_avg : float - Total ion density (m⁻³). - temp_plasma_electron_density_weighted_kev : float - Density-weighted electron temperature (keV). - temp_plasma_ion_density_weighted_kev : float - Density-weighted ion temperature (keV). - pden_alpha_total_mw : float - Alpha power per unit volume, from beams and plasma (MW/m³). - pden_plasma_alpha_mw : float - Alpha power per unit volume just from plasma (MW/m³). - i_beta_fast_alpha : int - Switch for fast alpha pressure method. +def reinke_tsep(b_plasma_toroidal_on_axis, flh, qstar, rmajor, eps, fgw, kappa, lhat): + """Function for calculating upstream temperature(keV) in Reinke model + This function calculates the upstream temperature in the + divertor/SoL model used for the Reinke citerion. + Issue #707 + M.L. Reinke 2017 Nucl. Fusion 57 034004 - Returns - ------- - float - Fast alpha beta component. + Parameters + ---------_ + b_plasma_toroidal_on_axis : + toroidal field on axis (T) + flh : + fraction of Psep/P_LH + qstar : + safety factor similar to q95 (see #707) + rmajor : + major radius (m) + eps : + inverse aspect ratio + fgw : + ratio of volume averaged density to n_GW + kappa : + elongation + lhat : + connection length factor + """ + kappa_0 = 2.0e3 # Stangeby W/m/eV^(7/2) - Notes - ----- - - For IPDG89 scaling applicability is Z_eff = 1.5, T_i/T_e = 1, 〈T〉 = 5-20 keV + return ( + ( + b_plasma_toroidal_on_axis**0.72 + * flh**0.29 + * fgw**0.21 + * qstar**0.08 + * rmajor**0.33 + ) + * (eps**0.15 * (1.0 + kappa**2.0) ** 0.34) + * (lhat**0.29 * kappa_0 ** (-0.29) * 0.285) + ) - References - ---------- - - N.A. Uckan and ITER Physics Group, 'ITER Physics Design Guidelines: 1989', - https://inis.iaea.org/collection/NCLCollectionStore/_Public/21/068/21068960.pdf +class BetaNormMaxModel(IntEnum): + """Beta norm max (β_N_max) model types""" - - Uckan, N. A., Tolliver, J. S., Houlberg, W. A., and Attenberger, S. E. - Influence of fast alpha diffusion and thermal alpha buildup on tokamak reactor performance. - United States: N. p., 1987. Web.https://www.osti.gov/servlets/purl/5611706 + USER_INPUT = 0 + WESSON = 1 + ORIGINAL_SCALING = 2 + MENARD = 3 + THLOREUS = 4 + STAMBAUGH = 5 - """ - # Determine average fast alpha density - if physics_variables.f_plasma_fuel_deuterium < 1.0: - beta_thermal = ( - 2.0 - * constants.RMU0 - * constants.KILOELECTRON_VOLT - * ( - nd_plasma_electrons_vol_avg - * temp_plasma_electron_density_weighted_kev - + nd_plasma_ions_total_vol_avg * temp_plasma_ion_density_weighted_kev - ) - / (b_plasma_toroidal_on_axis**2 + b_plasma_poloidal_average**2) - ) +class PlasmaBeta: + """Class to hold plasma beta calculations for plasma processing.""" - # jlion: This "fact" model is heavily flawed for smaller temperatures! It is unphysical for a stellarator (high n low T) - # IPDG89 fast alpha scaling - if i_beta_fast_alpha == 0: - fact = min( - 0.3, - 0.29 - * (nd_plasma_fuel_ions_vol_avg / nd_plasma_electrons_vol_avg) ** 2 - * ( - ( - temp_plasma_electron_density_weighted_kev - + temp_plasma_ion_density_weighted_kev - ) - / 20.0 - - 0.37 - ), - ) + def __init__(self): + self.outfile = constants.NOUT + self.mfile = constants.MFILE - # Modified scaling, D J Ward - else: - fact = min( - 0.30, - 0.26 - * (nd_plasma_fuel_ions_vol_avg / nd_plasma_electrons_vol_avg) ** 2 - * np.sqrt( - max( - 0.0, - ( - ( - temp_plasma_electron_density_weighted_kev - + temp_plasma_ion_density_weighted_kev - ) - / 20.0 - - 0.65 - ), - ) - ), - ) + def get_beta_norm_max_value(self, model: BetaNormMaxModel) -> float: + """Get the beta norm max value (β_N_max) for the specified model. - fact = max(fact, 0.0) - fact2 = pden_alpha_total_mw / pden_plasma_alpha_mw - beta_fast_alpha = beta_thermal * fact * fact2 + Parameters + ---------- + model: BetaNormMaxModel : + """ + model_map = { + BetaNormMaxModel.USER_INPUT: physics_variables.beta_norm_max, + BetaNormMaxModel.WESSON: physics_variables.beta_norm_max_wesson, + BetaNormMaxModel.ORIGINAL_SCALING: physics_variables.beta_norm_max_original_scaling, + BetaNormMaxModel.MENARD: physics_variables.beta_norm_max_menard, + BetaNormMaxModel.THLOREUS: physics_variables.beta_norm_max_thloreus, + BetaNormMaxModel.STAMBAUGH: physics_variables.beta_norm_max_stambaugh, + } + return model_map[model] - else: # negligible alpha production, alpha_power_density = p_beam_alpha_mw = 0 - beta_fast_alpha = 0.0 + def run(self): + """Calculate plasma beta values.""" - return beta_fast_alpha + # ----------------------------------------------------- + # Normalised Beta Limit + # ----------------------------------------------------- - def output_beta_information(self): - """Output beta information to file.""" + # Normalised beta from Troyon beta limit + physics_variables.beta_norm_total = self.calculate_normalised_beta( + beta=physics_variables.beta_total_vol_avg, + rminor=physics_variables.rminor, + c_plasma=physics_variables.plasma_current, + b_field=physics_variables.b_plasma_toroidal_on_axis, + ) - po.osubhd(self.outfile, "Beta Information :") - if physics_variables.i_beta_component == 0: - po.ovarrf( - self.outfile, - "Upper limit on total beta", - "(beta_vol_avg_max)", - physics_variables.beta_vol_avg_max, - "OP ", - ) - elif physics_variables.i_beta_component == 1: - po.ovarrf( - self.outfile, - "Upper limit on thermal beta", - "(beta_vol_avg_max)", - physics_variables.beta_vol_avg_max, - "OP ", - ) - else: - po.ovarrf( - self.outfile, - "Upper limit on thermal + NB beta", - "(beta_vol_avg_max)", - physics_variables.beta_vol_avg_max, - "OP ", - ) + # Define beta_norm_max calculations - po.ovarre( - self.outfile, - "Total plasma beta", - "(beta_total_vol_avg)", - physics_variables.beta_total_vol_avg, - ) - if physics_variables.i_beta_component == 0: - po.ovarrf( - self.outfile, - "Lower limit on total beta", - "(beta_vol_avg_min)", - physics_variables.beta_vol_avg_min, - "IP", - ) - elif physics_variables.i_beta_component == 1: - po.ovarrf( - self.outfile, - "Lower limit on thermal beta", - "(beta_vol_avg_min)", - physics_variables.beta_vol_avg_min, - "IP", - ) - else: - po.ovarrf( - self.outfile, - "Lower limit on thermal + NB beta", - "(beta_vol_avg_min)", - physics_variables.beta_vol_avg_min, - "IP", - ) - po.ovarre( - self.outfile, - "Upper limit on poloidal beta", - "(beta_poloidal_max)", - constraint_variables.beta_poloidal_max, - "IP", - ) - po.ovarre( - self.outfile, - "Total poloidal beta", - "(beta_poloidal_vol_avg)", - physics_variables.beta_poloidal_vol_avg, - "OP ", + physics_variables.beta_norm_max_wesson = self.calculate_beta_norm_max_wesson( + ind_plasma_internal_norm=physics_variables.ind_plasma_internal_norm ) - po.ovarre( - self.outfile, - "Volume averaged toroidal beta", - "(beta_toroidal_vol_avg)", - physics_variables.beta_toroidal_vol_avg, - "OP ", + + # Original scaling law + physics_variables.beta_norm_max_original_scaling = ( + self.calculate_beta_norm_max_original(eps=physics_variables.eps) ) - for i in range(len(physics_variables.beta_thermal_toroidal_profile)): - po.ovarre( - self.mfile, - f"Beta toroidal profile at point {i}", - f"beta_thermal_toroidal_profile{i}", - physics_variables.beta_thermal_toroidal_profile[i], - ) - po.ovarre( - self.outfile, - "Fast alpha beta", - "(beta_fast_alpha)", - physics_variables.beta_fast_alpha, - "OP ", + # J. Menard scaling law + physics_variables.beta_norm_max_menard = self.calculate_beta_norm_max_menard( + eps=physics_variables.eps ) - po.ovarre( - self.outfile, - "Neutral Beam ion beta", - "(beta_beam)", - physics_variables.beta_beam, - "OP ", + + # E. Tholerus scaling law + physics_variables.beta_norm_max_thloreus = self.calculate_beta_norm_max_thloreus( + c_beta=physics_variables.c_beta, + pres_plasma_on_axis=physics_variables.pres_plasma_thermal_on_axis, + pres_plasma_vol_avg=physics_variables.pres_plasma_thermal_vol_avg, ) - po.ovarre( - self.outfile, - "Ratio of fast alpha and beam beta to thermal beta", - "(f_beta_alpha_beam_thermal)", - physics_variables.f_beta_alpha_beam_thermal, - "OP ", + + # R. D. Stambaugh scaling law + physics_variables.beta_norm_max_stambaugh = ( + self.calculate_beta_norm_max_stambaugh( + f_c_plasma_bootstrap=current_drive_variables.f_c_plasma_bootstrap, + kappa=physics_variables.kappa, + aspect=physics_variables.aspect, + ) ) - po.ovarre( - self.outfile, - "Volume averaged thermal beta", - "(beta_thermal_vol_avg)", - physics_variables.beta_thermal_vol_avg, - "OP ", + # Calculate beta_norm_max based on i_beta_norm_max + try: + model = BetaNormMaxModel(int(physics_variables.i_beta_norm_max)) + physics_variables.beta_norm_max = self.get_beta_norm_max_value(model) + except ValueError: + raise ProcessValueError( + "Illegal value of i_beta_norm_max", + i_beta_norm_max=physics_variables.i_beta_norm_max, + ) from None + + # calculate_beta_limit() returns the beta_vol_avg_max for beta + physics_variables.beta_vol_avg_max = self.calculate_beta_limit_from_norm( + b_plasma_toroidal_on_axis=physics_variables.b_plasma_toroidal_on_axis, + beta_norm_max=physics_variables.beta_norm_max, + plasma_current=physics_variables.plasma_current, + rminor=physics_variables.rminor, ) - po.ovarre( - self.outfile, - "Thermal poloidal beta", - "(beta_thermal_poloidal_vol_avg)", - physics_variables.beta_thermal_poloidal_vol_avg, - "OP ", + + physics_variables.beta_toroidal_vol_avg = ( + physics_variables.beta_total_vol_avg + * physics_variables.b_plasma_total**2 + / physics_variables.b_plasma_toroidal_on_axis**2 ) - po.ovarre( - self.outfile, - "Thermal toroidal beta", - "(beta_thermal_toroidal_vol_avg)", - physics_variables.beta_thermal_toroidal_vol_avg, - "OP ", + + # Calculate physics_variables.beta poloidal [-] + physics_variables.beta_poloidal_vol_avg = self.calculate_poloidal_beta( + b_plasma_total=physics_variables.b_plasma_total, + b_plasma_poloidal_average=physics_variables.b_plasma_poloidal_average, + beta=physics_variables.beta_total_vol_avg, ) - po.ovarrf( - self.outfile, - "Poloidal beta and inverse aspect ratio", - "(beta_poloidal_eps)", - physics_variables.beta_poloidal_eps, - "OP ", + physics_variables.beta_thermal_vol_avg = ( + physics_variables.beta_total_vol_avg + - physics_variables.beta_fast_alpha + - physics_variables.beta_beam ) - po.ovarrf( - self.outfile, - "Poloidal beta and inverse aspect ratio upper limit", - "(beta_poloidal_eps_max)", - physics_variables.beta_poloidal_eps_max, + + physics_variables.beta_poloidal_eps = ( + physics_variables.beta_poloidal_vol_avg * physics_variables.eps ) - po.osubhd(self.outfile, "Normalised Beta Information :") - if stellarator_variables.istell == 0: - if physics_variables.i_beta_norm_max != 0: - po.ovarrf( - self.outfile, - "Beta g coefficient", - "(beta_norm_max)", - physics_variables.beta_norm_max, - "OP ", - ) - else: - po.ovarrf( - self.outfile, - "Beta g coefficient", - "(beta_norm_max)", - physics_variables.beta_norm_max, - ) - po.ovarrf( - self.outfile, - "Normalised total beta", - "(beta_norm_total)", - physics_variables.beta_norm_total, - "OP ", + + physics_variables.beta_thermal_poloidal_vol_avg = ( + physics_variables.beta_thermal_vol_avg + * ( + physics_variables.b_plasma_total + / physics_variables.b_plasma_poloidal_average ) - po.ovarrf( - self.outfile, - "Normalised thermal beta", - "(beta_norm_thermal) ", - physics_variables.beta_norm_thermal, - "OP ", + ** 2 + ) + physics_variables.beta_thermal_toroidal_vol_avg = ( + physics_variables.beta_thermal_vol_avg + * ( + physics_variables.b_plasma_total + / physics_variables.b_plasma_toroidal_on_axis ) + ** 2 + ) - po.ovarrf( - self.outfile, - "Normalised toroidal beta", - "(beta_norm_toroidal) ", - physics_variables.beta_norm_toroidal, - "OP ", - ) + # ======================================================= - po.ovarrf( - self.outfile, - "Normalised poloidal beta", - "(beta_norm_poloidal) ", - physics_variables.beta_norm_poloidal, - "OP ", - ) + # Mirror the pressure profiles to match the doubled toroidal field profile + pres_profile_total = np.concatenate([ + physics_variables.pres_plasma_thermal_total_profile[::-1], + physics_variables.pres_plasma_thermal_total_profile, + ]) - po.osubhd(self.outfile, "Maximum normalised beta scalings :") - po.ovarrf( - self.outfile, - "J. Wesson normalised beta upper limit", - "(beta_norm_max_wesson) ", - physics_variables.beta_norm_max_wesson, - "OP ", - ) - po.ovarrf( - self.outfile, - "Original normalsied beta upper limit", - "(beta_norm_max_original_scaling) ", - physics_variables.beta_norm_max_original_scaling, - "OP ", - ) - po.ovarrf( - self.outfile, - "J. Menard normalised beta upper limit", - "(beta_norm_max_menard) ", - physics_variables.beta_norm_max_menard, - "OP ", + physics_variables.beta_thermal_toroidal_profile = np.array([ + self.calculate_plasma_beta( + pres_plasma=pres_profile_total[i], + b_field=physics_variables.b_plasma_toroidal_profile[i], ) - po.ovarrf( - self.outfile, - "E. Thloreus normalised beta upper limit", - "(beta_norm_max_thloreus) ", - physics_variables.beta_norm_max_thloreus, - "OP ", + for i in range(len(physics_variables.b_plasma_toroidal_profile)) + ]) + + # ======================================================= + + physics_variables.beta_norm_thermal = self.calculate_normalised_beta( + beta=physics_variables.beta_thermal_vol_avg, + rminor=physics_variables.rminor, + c_plasma=physics_variables.plasma_current, + b_field=physics_variables.b_plasma_toroidal_on_axis, + ) + + physics_variables.beta_norm_toroidal = ( + physics_variables.beta_norm_total + * ( + physics_variables.b_plasma_total + / physics_variables.b_plasma_toroidal_on_axis ) - po.ovarrf( - self.outfile, - "R. Stambaugh normalised beta upper limit", - "(beta_norm_max_stambaugh) ", - physics_variables.beta_norm_max_stambaugh, - "OP ", + ** 2 + ) + physics_variables.beta_norm_poloidal = ( + physics_variables.beta_norm_total + * ( + physics_variables.b_plasma_total + / physics_variables.b_plasma_poloidal_average ) + ** 2 + ) - po.osubhd(self.outfile, "Plasma energies derived from beta :") - po.ovarre( - self.outfile, - "Plasma thermal energy derived from thermal beta (J)", - "(e_plasma_beta_thermal) ", - physics_variables.e_plasma_beta_thermal, - "OP", + physics_variables.f_beta_alpha_beam_thermal = ( + physics_variables.beta_fast_alpha + physics_variables.beta_beam + ) / physics_variables.beta_thermal_vol_avg + + # Plasma thermal energy derived from the thermal beta + physics_variables.e_plasma_beta_thermal = self.calculate_plasma_energy_from_beta( + beta=physics_variables.beta_thermal_vol_avg, + b_field=physics_variables.b_plasma_total, + vol_plasma=physics_variables.vol_plasma, ) - po.ovarre( - self.outfile, - "Plasma thermal energy derived from the total beta (J)", - "(e_plasma_beta)", - physics_variables.e_plasma_beta, - "OP", + # Plasma thermal energy derived from the total beta + physics_variables.e_plasma_beta = self.calculate_plasma_energy_from_beta( + beta=physics_variables.beta_total_vol_avg, + b_field=physics_variables.b_plasma_total, + vol_plasma=physics_variables.vol_plasma, ) - po.oblnkl(self.outfile) - po.ostars(self.outfile, 110) - po.oblnkl(self.outfile) + @staticmethod + def calculate_plasma_beta( + pres_plasma: float | np.ndarray, b_field: float | np.ndarray + ) -> float | np.ndarray: + """Calculate the plasma beta (β) for a given pressure and field. + + Plasma beta is the ratio of plasma pressure to magnetic pressure. + + Parameters + ---------- + pres_plasma : float | np.ndarray + Plasma pressure (in Pascals). + b_field : float | np.ndarray + Magnetic field strength (in Tesla). + + Returns + ------- + float | np.ndarray + The plasma beta (dimensionless). + """ + + return 2 * constants.RMU0 * pres_plasma / (b_field**2) + + @staticmethod + def calculate_beta_norm_max_wesson(ind_plasma_internal_norm: float) -> float: + """Calculate the Wesson normalsied beta upper limit. + + Parameters + ---------- + ind_plasma_internal_norm : float + Plasma normalised internal inductance + + + Returns + ------- + float + The Wesson normalised beta upper limit. + + Notes + ----- + - It is recommended to use this method with the other Wesson relations for normalsied internal + inductance and current profile index. + - This fit is derived from the DIII-D database for β_N >= 2.5 + + References + ---------- + - Wesson, J. (2011) Tokamaks. 4th Edition, 2011 Oxford Science Publications, + International Series of Monographs on Physics, Volume 149. + + - T. T. S et al., “Profile Optimization and High Beta Discharges and Stability of High Elongation Plasmas in the DIII-D Tokamak,” + Osti.gov, Oct. 1990. https://www.osti.gov/biblio/6194284 (accessed Dec. 19, 2024). + + """ + return 4 * ind_plasma_internal_norm + + @staticmethod + def calculate_beta_norm_max_original(eps: float) -> float: + """Calculate the original scaling law normalsied beta upper limit. + + Parameters + ---------- + eps : float + Plasma normalised internal inductance + + Returns + ------- + float + + References + ---------- + The original scaling law normalised beta upper limit. + + """ + return 2.7 * (1.0 + 5.0 * eps**3.5) + + @staticmethod + def calculate_beta_norm_max_menard(eps: float) -> float: + """Calculate the Menard normalsied beta upper limit. + Parameters + ---------- + eps : float + Plasma normalised internal inductance -class IndInternalNormModel(IntEnum): - """Normalised internal inductance (l_i) model types""" + Returns + ------- + float + The Menard normalised beta upper limit. - USER_INPUT = 0 - WESSON = 1 - MENARD = 2 + Notes + ----- + - Found as a reasonable fit to the computed no wall limit at f_BS ≈ 50% + - Uses maximum κ data from NSTX at A = 1.45, A = 1.75. Along with record + β_T data from DIII-D at A = 2.9 and high κ. + References + ---------- + - # J. E. Menard et al., “Fusion nuclear science facilities and pilot plants based on the spherical tokamak,” + Nuclear Fusion, vol. 56, no. 10, p. 106023, Aug. 2016, + doi: https://doi.org/10.1088/0029-5515/56/10/106023. -class PlasmaInductance: - """Class to hold plasma inductance calculations for plasma processing.""" + """ + return 3.12 + 3.5 * eps**1.7 - def __init__(self): - self.outfile = constants.NOUT - self.mfile = constants.MFILE + @staticmethod + def calculate_beta_norm_max_thloreus( + c_beta: float, pres_plasma_on_axis: float, pres_plasma_vol_avg: float + ) -> float: + """Calculate the E. Tholerus normalized beta upper limit. - def run(self): - physics_variables.ind_plasma_internal_norm_wesson = ( - self.calculate_internal_inductance_wesson(alphaj=physics_variables.alphaj) - ) + Parameters + ---------- + c_beta : float + Pressure peaking factor coefficient. + pres_plasma_on_axis : float + Central plasma pressure (Pa). + pres_plasma_vol_avg : float + Volume-averaged plasma pressure (Pa). + c_beta: float : - # Spherical Tokamak relation for internal inductance - # Menard et al. (2016), Nuclear Fusion, 56, 106023 - physics_variables.ind_plasma_internal_norm_menard = ( - self.calculate_internal_inductance_menard(kappa=physics_variables.kappa) - ) + pres_plasma_on_axis: float : - physics_variables.ind_plasma_internal_norm_iter_3 = ( - self.calculate_normalised_internal_inductance_iter_3( - b_plasma_poloidal_vol_avg=physics_variables.b_plasma_poloidal_average, - c_plasma=physics_variables.plasma_current, - vol_plasma=physics_variables.vol_plasma, - rmajor=physics_variables.rmajor, - ) - ) + pres_plasma_vol_avg: float : - # Calculate ind_plasma_internal_norm based on i_ind_plasma_internal_norm - try: - model = IndInternalNormModel( - int(physics_variables.i_ind_plasma_internal_norm) - ) - physics_variables.ind_plasma_internal_norm = ( - self.get_ind_internal_norm_value(model) - ) - except ValueError: - raise ProcessValueError( - "Illegal value of i_ind_plasma_internal_norm", - i_ind_plasma_internal_norm=physics_variables.i_ind_plasma_internal_norm, - ) from None - def get_ind_internal_norm_value(self, model: IndInternalNormModel) -> float: - """Get the normalised internal inductance (l_i) for the specified model. + Returns + ------- + float + The E. Tholerus normalized beta upper limit. - Parameters + Notes + ----- + - This method calculates the normalized beta upper limit based on the pressure peaking factor (Fp), + which is defined as the ratio of the peak pressure to the average pressure. + - The formula is derived from operational space studies of flat-top plasma in the STEP power plant. + + References ---------- - model: IndInternalNormModel : + - E. Tholerus et al., “Flat-top plasma operational space of the STEP power plant,” + Nuclear Fusion, Aug. 2024, doi: https://doi.org/10.1088/1741-4326/ad6ea2. + """ - model_map = { - IndInternalNormModel.USER_INPUT: physics_variables.ind_plasma_internal_norm, - IndInternalNormModel.WESSON: physics_variables.ind_plasma_internal_norm_wesson, - IndInternalNormModel.MENARD: physics_variables.ind_plasma_internal_norm_menard, - } - return model_map[model] + return 3.7 + ( + (c_beta / (pres_plasma_on_axis / pres_plasma_vol_avg)) + * (12.5 - 3.5 * (pres_plasma_on_axis / pres_plasma_vol_avg)) + ) @staticmethod - def calculate_volt_second_requirements( - csawth: float, - eps: float, - f_c_plasma_inductive: float, - ejima_coeff: float, + def calculate_beta_norm_max_stambaugh( + f_c_plasma_bootstrap: float, kappa: float, - rmajor: float, - res_plasma: float, - plasma_current: float, - t_plant_pulse_fusion_ramp: float, - t_plant_pulse_burn: float, - ind_plasma_internal_norm: float, - ) -> tuple[float, float, float, float, float, float]: - """Calculate the volt-second requirements and related parameters for plasma physics. + aspect: float, + ) -> float: + """Calculate the Stambaugh normalized beta upper limit. Parameters ---------- - csawth : float - Coefficient for sawteeth effects - eps : float - Inverse aspect ratio - f_c_plasma_inductive : float - Fraction of plasma current produced inductively - ejima_coeff : float - Ejima coefficient for resistive start-up V-s component + f_c_plasma_bootstrap : float + Bootstrap current fraction. kappa : float - Plasma elongation - rmajor : float - Plasma major radius (m) - res_plasma : float - Plasma resistance (ohm) - plasma_current : float - Plasma current (A) - t_plant_pulse_fusion_ramp : float - Heating time (s) - t_plant_pulse_burn : float - Burn time (s) - ind_plasma_internal_norm : float - Plasma normalized internal inductance + Plasma separatrix elongation. + aspect : float + Plasma aspect ratio. Returns ------- - tuple[float, float, float, float, float, float] - A tuple containing: - - vs_plasma_internal: Internal plasma volt-seconds (Wb) - - ind_plasma_internal: Plasma inductance (H) - - vs_plasma_burn_required: Volt-seconds needed during flat-top (heat+burn) (Wb) - - vs_plasma_ramp_required: Volt-seconds needed during ramp-up (Wb) - - ind_plasma_total,: Internal and external plasma inductance V-s (Wb) - - vs_res_ramp: Resistive losses in start-up volt-seconds (Wb) - - vs_plasma_total_required: Total volt-seconds needed (Wb) + float + The Stambaugh normalized beta upper limit. + + Notes + ----- + - This method calculates the normalized beta upper limit based on the Stambaugh scaling. + - The formula is derived from empirical fits to high-performance, steady-state tokamak equilibria. References ---------- - - S. Ejima, R. W. Callis, J. L. Luxon, R. D. Stambaugh, T. S. Taylor, and J. C. Wesley, - “Volt-second analysis and consumption in Doublet III plasmas,” - Nuclear Fusion, vol. 22, no. 10, pp. 1313-1319, Oct. 1982, doi: - https://doi.org/10.1088/0029-5515/22/10/006. - - - S. C. Jardin, C. E. Kessel, and N Pomphrey, - “Poloidal flux linkage requirements for the International Thermonuclear Experimental Reactor,” - Nuclear Fusion, vol. 34, no. 8, pp. 1145-1160, Aug. 1994, - doi: https://doi.org/10.1088/0029-5515/34/8/i07. + - R. D. Stambaugh et al., “Fusion Nuclear Science Facility Candidates,” + Fusion Science and Technology, vol. 59, no. 2, pp. 279-307, Feb. 2011, + doi: https://doi.org/10.13182/fst59-279. - - S. P. Hirshman and G. H. Neilson, “External inductance of an axisymmetric plasma,” - The Physics of Fluids, vol. 29, no. 3, pp. 790-793, Mar. 1986, - doi: https://doi.org/10.1063/1.865934. + - Y. R. Lin-Liu and R. D. Stambaugh, “Optimum equilibria for high performance, steady state tokamaks,” + Nuclear Fusion, vol. 44, no. 4, pp. 548-554, Mar. 2004, + doi: https://doi.org/10.1088/0029-5515/44/4/009. """ - # Plasma internal inductance - - ind_plasma_internal = constants.RMU0 * rmajor * ind_plasma_internal_norm / 2.0 - - # Internal plasma flux (V-s) component - vs_plasma_internal = ind_plasma_internal * plasma_current - - # Start-up resistive component - # Uses ITER formula without the 10 V-s add-on - - vs_res_ramp = ejima_coeff * constants.RMU0 * plasma_current * rmajor - - # ====================================================================== - - # Hirshman and Neilson fit for external inductance - - aeps = (1.0 + 1.81 * np.sqrt(eps) + 2.05 * eps) * np.log(8.0 / eps) - ( - 2.0 + 9.25 * np.sqrt(eps) - 1.21 * eps + return ( + f_c_plasma_bootstrap + * 10 + * (-0.7748 + (1.2869 * kappa) - (0.2921 * kappa**2) + (0.0197 * kappa**3)) + / (aspect**0.5523 * np.tanh((1.8524 + (0.2319 * kappa)) / aspect**0.6163)) ) - beps = 0.73 * np.sqrt(eps) * (1.0 + 2.0 * eps**4 - 6.0 * eps**5 + 3.7 * eps**6) - ind_plasma_external = ( - rmajor * constants.RMU0 * aeps * (1.0 - eps) / (1.0 - eps + beps * kappa) - ) + @staticmethod + def calculate_normalised_beta( + beta: float, rminor: float, c_plasma: float, b_field: float + ) -> float: + """Calculate normalised beta (β_N). - # ====================================================================== + Parameters + ---------- + beta : float + Plasma beta (fraction). + rminor : float + Plasma minor radius (m). + c_plasma : float + Plasma current (A). + b_field : float + Magnetic field (T). - ind_plasma_total = ind_plasma_external + ind_plasma_internal + Returns + ------- + float + Normalised beta. - # Inductive V-s component + Notes + ----- + - 1.0e8 is a conversion factor to get beta_N in standard units, as plasma current is normally in MA and + beta is in percentage instead of fraction. - vs_self_ind_ramp = ind_plasma_total * plasma_current - vs_plasma_ramp_required = vs_res_ramp + vs_self_ind_ramp + """ - # Plasma loop voltage during flat-top - # Include enhancement factor in flattop V-s requirement - # to account for MHD sawtooth effects. + return 1.0e8 * (beta * rminor * b_field) / c_plasma - v_plasma_loop_burn = plasma_current * res_plasma * f_c_plasma_inductive + @staticmethod + def calculate_plasma_energy_from_beta( + beta: float, b_field: float, vol_plasma: float + ) -> float: + """Calculate plasma thermal energy from beta. - v_burn_resistive = v_plasma_loop_burn * csawth + E_plasma = 1.5 * β * B² / (2 * μ_0) * V - # N.B. t_plant_pulse_burn on first iteration will not be correct - # if the pulsed reactor option is used, but the value - # will be correct on subsequent calls. + Parameters + ---------- + beta : float + Plasma beta (fraction). + b_field : float + Magnetic field (T). + vol_plasma : float + Plasma volume (m³). - vs_plasma_burn_required = v_burn_resistive * ( - t_plant_pulse_fusion_ramp + t_plant_pulse_burn - ) - vs_plasma_total_required = vs_plasma_ramp_required + vs_plasma_burn_required + Returns + ------- + float + Plasma energy (J). + """ - return ( - vs_plasma_internal, - ind_plasma_total, - vs_plasma_burn_required, - vs_plasma_ramp_required, - vs_self_ind_ramp, - vs_res_ramp, - vs_plasma_total_required, - v_plasma_loop_burn, - ) + return (1.5e0 * beta * b_field**2) / (2.0e0 * constants.RMU0) * vol_plasma @staticmethod - def calculate_normalised_internal_inductance_iter_3( - b_plasma_poloidal_vol_avg: float, - c_plasma: float, - vol_plasma: float, - rmajor: float, + def calculate_beta_limit_from_norm( + b_plasma_toroidal_on_axis: float, + beta_norm_max: float, + plasma_current: float, + rminor: float, ) -> float: - """Calculate the normalised internal inductance using ITER-3 scaling li(3). + """Calculate the maximum allowed beta (β) from a given normalised (β_N). + + This subroutine calculates the beta limit using the algorithm documented in AEA FUS 172. + The limit applies to beta defined with respect to the total B-field. + Switch i_beta_component determines which components of beta to include. Parameters - ------------ - b_plasma_poloidal_vol_avg : float - Volume-averaged poloidal magnetic field (T). - c_plasma : float - Plasma current (A). - vol_plasma : float - Plasma volume (m^3). - rmajor : float - Plasma major radius (m). + ---------- + b_plasma_toroidal_on_axis : float + Toroidal B-field on plasma axis [T]. + beta_norm_max : float + Troyon-like g coefficient. + plasma_current : float + Plasma current [A]. + rminor : float + Plasma minor axis [m]. Returns - -------- + ------- float - The li(3) normalised internal inductance. + Beta limit as defined below. - References - ----------- - - T. C. Luce, D. A. Humphreys, G. L. Jackson, and W. M. Solomon, - “Inductive flux usage and its optimization in tokamak operation,” - Nuclear Fusion, vol. 54, no. 9, p. 093005, Jul. 2014, - doi: https://doi.org/10.1088/0029-5515/54/9/093005. + Notes + ----- + - If i_beta_component = 0, then the limit is applied to the total beta. + - If i_beta_component = 1, then the limit is applied to the thermal beta only. + - If i_beta_component = 2, then the limit is applied to the thermal + neutral beam beta components. + - If i_beta_component = 3, then the limit is applied to the toroidal beta. - - G. L. Jackson et al., “ITER startup studies in the DIII-D tokamak,” - Nuclear Fusion, vol. 48, no. 12, p. 125002, Nov. 2008, - doi: https://doi.org/10.1088/0029-5515/48/12/125002. + - The default value for the g coefficient is beta_norm_max = 3.5. + References + ---------- + - F. Troyon et.al, “Beta limit in tokamaks. Experimental and computational status,” + Plasma Physics and Controlled Fusion, vol. 30, no. 11, pp. 1597-1609, Oct. 1988, + doi: https://doi.org/10.1088/0741-3335/30/11/019. + + - T.C.Hender et.al., 'Physics Assesment of the European Reactor Study', AEA FUS 172, 1992 """ + # Multiplied by 0.01 to convert from % to fraction return ( - 2 - * vol_plasma - * b_plasma_poloidal_vol_avg**2 - / (constants.RMU0**2 * c_plasma**2 * rmajor) + 0.01 + * beta_norm_max + * (plasma_current / 1.0e6) + / (rminor * b_plasma_toroidal_on_axis) ) @staticmethod - def calculate_internal_inductance_menard(kappa: float) -> float: - """Calculate the Menard plasma normalized internal inductance. + def calculate_poloidal_beta( + b_plasma_total: float, b_plasma_poloidal_average: float, beta: float + ) -> float: + """Calculates total poloidal beta (β_p) Parameters ---------- - kappa : float - Plasma separatrix elongation. + b_plasma_poloidal_average : float + The average poloidal magnetic field of the plasma (in Tesla). + beta : float + The plasma beta, a dimensionless parameter representing the ratio of plasma pressure to magnetic pressure. Returns ------- float - The Menard plasma normalised internal inductance. - - Notes - ----- - - This relation is based off of data from NSTX for l_i in the range of 0.4-0.85 - - This model is only recommended to be used for ST's with kappa > 2.5 + The calculated total poloidal beta. References ---------- - - J. E. Menard et al., “Fusion nuclear science facilities and pilot plants based on the spherical tokamak,” - Nuclear Fusion, vol. 56, no. 10, p. 106023, Aug. 2016, - doi: https://doi.org/10.1088/0029-5515/56/10/106023. + - J.P. Freidberg, "Plasma physics and fusion energy", Cambridge University Press (2007) + Page 270 ISBN 0521851076 """ - return 3.4 - kappa + return beta * (b_plasma_total / b_plasma_poloidal_average) ** 2 @staticmethod - def calculate_internal_inductance_wesson(alphaj: float) -> float: - """Calculate the Wesson plasma normalized internal inductance. + def fast_alpha_beta( + b_plasma_poloidal_average: float, + b_plasma_toroidal_on_axis: float, + nd_plasma_electrons_vol_avg: float, + nd_plasma_fuel_ions_vol_avg: float, + nd_plasma_ions_total_vol_avg: float, + temp_plasma_electron_density_weighted_kev: float, + temp_plasma_ion_density_weighted_kev: float, + pden_alpha_total_mw: float, + pden_plasma_alpha_mw: float, + i_beta_fast_alpha: int, + ) -> float: + """Calculate the fast alpha beta (β_fast_alpha) component. + + This function computes the fast alpha beta contribution based on the provided plasma parameters. Parameters ---------- - alphaj : float - Current profile index. + b_plasma_poloidal_average : float + Poloidal field (T). + b_plasma_toroidal_on_axis : float + Toroidal field on axis (T). + nd_plasma_electrons_vol_avg : float + Electron density (m⁻³). + nd_plasma_fuel_ions_vol_avg : float + Fuel ion density (m⁻³). + nd_plasma_ions_total_vol_avg : float + Total ion density (m⁻³). + temp_plasma_electron_density_weighted_kev : float + Density-weighted electron temperature (keV). + temp_plasma_ion_density_weighted_kev : float + Density-weighted ion temperature (keV). + pden_alpha_total_mw : float + Alpha power per unit volume, from beams and plasma (MW/m³). + pden_plasma_alpha_mw : float + Alpha power per unit volume just from plasma (MW/m³). + i_beta_fast_alpha : int + Switch for fast alpha pressure method. Returns ------- float - The Wesson plasma normalised internal inductance. + Fast alpha beta component. Notes ----- - - It is recommended to use this method with the other Wesson relations for normalised beta and - current profile index. - - This relation is only true for the cyclindrical plasma approximation with parabolic profiles. + - For IPDG89 scaling applicability is Z_eff = 1.5, T_i/T_e = 1, 〈T〉 = 5-20 keV + References ---------- - - Wesson, J. (2011) Tokamaks. 4th Edition, 2011 Oxford Science Publications, - International Series of Monographs on Physics, Volume 149. + - N.A. Uckan and ITER Physics Group, 'ITER Physics Design Guidelines: 1989', + https://inis.iaea.org/collection/NCLCollectionStore/_Public/21/068/21068960.pdf + + - Uckan, N. A., Tolliver, J. S., Houlberg, W. A., and Attenberger, S. E. + Influence of fast alpha diffusion and thermal alpha buildup on tokamak reactor performance. + United States: N. p., 1987. Web.https://www.osti.gov/servlets/purl/5611706 """ - return np.log(1.65 + 0.89 * alphaj) - def output_volt_second_information(self): - """Output volt-second information to file.""" + # Determine average fast alpha density + if physics_variables.f_plasma_fuel_deuterium < 1.0: + beta_thermal = ( + 2.0 + * constants.RMU0 + * constants.KILOELECTRON_VOLT + * ( + nd_plasma_electrons_vol_avg + * temp_plasma_electron_density_weighted_kev + + nd_plasma_ions_total_vol_avg * temp_plasma_ion_density_weighted_kev + ) + / (b_plasma_toroidal_on_axis**2 + b_plasma_poloidal_average**2) + ) + + # jlion: This "fact" model is heavily flawed for smaller temperatures! It is unphysical for a stellarator (high n low T) + # IPDG89 fast alpha scaling + if i_beta_fast_alpha == 0: + fact = min( + 0.3, + 0.29 + * (nd_plasma_fuel_ions_vol_avg / nd_plasma_electrons_vol_avg) ** 2 + * ( + ( + temp_plasma_electron_density_weighted_kev + + temp_plasma_ion_density_weighted_kev + ) + / 20.0 + - 0.37 + ), + ) + + # Modified scaling, D J Ward + else: + fact = min( + 0.30, + 0.26 + * (nd_plasma_fuel_ions_vol_avg / nd_plasma_electrons_vol_avg) ** 2 + * np.sqrt( + max( + 0.0, + ( + ( + temp_plasma_electron_density_weighted_kev + + temp_plasma_ion_density_weighted_kev + ) + / 20.0 + - 0.65 + ), + ) + ), + ) + + fact = max(fact, 0.0) + fact2 = pden_alpha_total_mw / pden_plasma_alpha_mw + beta_fast_alpha = beta_thermal * fact * fact2 + + else: # negligible alpha production, alpha_power_density = p_beam_alpha_mw = 0 + beta_fast_alpha = 0.0 + + return beta_fast_alpha + + def output_beta_information(self): + """Output beta information to file.""" + + po.osubhd(self.outfile, "Beta Information :") + if physics_variables.i_beta_component == 0: + po.ovarrf( + self.outfile, + "Upper limit on total beta", + "(beta_vol_avg_max)", + physics_variables.beta_vol_avg_max, + "OP ", + ) + elif physics_variables.i_beta_component == 1: + po.ovarrf( + self.outfile, + "Upper limit on thermal beta", + "(beta_vol_avg_max)", + physics_variables.beta_vol_avg_max, + "OP ", + ) + else: + po.ovarrf( + self.outfile, + "Upper limit on thermal + NB beta", + "(beta_vol_avg_max)", + physics_variables.beta_vol_avg_max, + "OP ", + ) - po.osubhd(self.outfile, "Plasma Volt-second Requirements :") po.ovarre( self.outfile, - "Total plasma volt-seconds required for pulse (Wb)", - "(vs_plasma_total_required)", - physics_variables.vs_plasma_total_required, - "OP ", + "Total plasma beta", + "(beta_total_vol_avg)", + physics_variables.beta_total_vol_avg, ) - po.oblnkl(self.outfile) + if physics_variables.i_beta_component == 0: + po.ovarrf( + self.outfile, + "Lower limit on total beta", + "(beta_vol_avg_min)", + physics_variables.beta_vol_avg_min, + "IP", + ) + elif physics_variables.i_beta_component == 1: + po.ovarrf( + self.outfile, + "Lower limit on thermal beta", + "(beta_vol_avg_min)", + physics_variables.beta_vol_avg_min, + "IP", + ) + else: + po.ovarrf( + self.outfile, + "Lower limit on thermal + NB beta", + "(beta_vol_avg_min)", + physics_variables.beta_vol_avg_min, + "IP", + ) po.ovarre( self.outfile, - "Total plasma inductive flux consumption for plasma current ramp-up (Wb)", - "(vs_plasma_ind_ramp)", - physics_variables.vs_plasma_ind_ramp, - "OP ", + "Upper limit on poloidal beta", + "(beta_poloidal_max)", + constraint_variables.beta_poloidal_max, + "IP", ) po.ovarre( self.outfile, - "Plasma resistive flux consumption for plasma current ramp-up (Wb)", - "(vs_plasma_res_ramp)", - physics_variables.vs_plasma_res_ramp, + "Total poloidal beta", + "(beta_poloidal_vol_avg)", + physics_variables.beta_poloidal_vol_avg, "OP ", ) po.ovarre( self.outfile, - "Total flux consumption for plasma current ramp-up (Wb)", - "(vs_plasma_ramp_required)", - physics_variables.vs_plasma_ramp_required, + "Volume averaged toroidal beta", + "(beta_toroidal_vol_avg)", + physics_variables.beta_toroidal_vol_avg, "OP ", ) - po.ovarrf( - self.outfile, - "Ejima coefficient", - "(ejima_coeff)", - physics_variables.ejima_coeff, - ) - po.oblnkl(self.outfile) - po.ovarre( - self.outfile, - "Internal plasma V-s", - "(vs_plasma_internal)", - physics_variables.vs_plasma_internal, - ) + for i in range(len(physics_variables.beta_thermal_toroidal_profile)): + po.ovarre( + self.mfile, + f"Beta toroidal profile at point {i}", + f"beta_thermal_toroidal_profile{i}", + physics_variables.beta_thermal_toroidal_profile[i], + ) po.ovarre( self.outfile, - "Plasma volt-seconds needed for flat-top (heat + burn times) (Wb)", - "(vs_plasma_burn_required)", - physics_variables.vs_plasma_burn_required, + "Fast alpha beta", + "(beta_fast_alpha)", + physics_variables.beta_fast_alpha, "OP ", ) - po.ovarre( self.outfile, - "Plasma loop voltage during burn (V)", - "(v_plasma_loop_burn)", - physics_variables.v_plasma_loop_burn, + "Neutral Beam ion beta", + "(beta_beam)", + physics_variables.beta_beam, "OP ", ) - po.ovarrf( - self.outfile, - "Coefficient for sawtooth effects on burn V-s requirement", - "(csawth)", - physics_variables.csawth, - ) - po.oblnkl(self.outfile) po.ovarre( self.outfile, - "Plasma resistance (ohm)", - "(res_plasma)", - physics_variables.res_plasma, + "Ratio of fast alpha and beam beta to thermal beta", + "(f_beta_alpha_beam_thermal)", + physics_variables.f_beta_alpha_beam_thermal, "OP ", ) po.ovarre( self.outfile, - "Plasma resistive diffusion time (s)", - "(t_plasma_res_diffusion)", - physics_variables.t_plasma_res_diffusion, + "Volume averaged thermal beta", + "(beta_thermal_vol_avg)", + physics_variables.beta_thermal_vol_avg, "OP ", ) po.ovarre( self.outfile, - "Plasma inductance (H)", - "(ind_plasma)", - physics_variables.ind_plasma, + "Thermal poloidal beta", + "(beta_thermal_poloidal_vol_avg)", + physics_variables.beta_thermal_poloidal_vol_avg, "OP ", ) po.ovarre( self.outfile, - "Plasma magnetic energy stored (J)", - "(e_plasma_magnetic_stored)", - physics_variables.e_plasma_magnetic_stored, + "Thermal toroidal beta", + "(beta_thermal_toroidal_vol_avg)", + physics_variables.beta_thermal_toroidal_vol_avg, "OP ", ) + po.ovarrf( self.outfile, - "Plasma normalised internal inductance", - "(ind_plasma_internal_norm)", - physics_variables.ind_plasma_internal_norm, + "Poloidal beta and inverse aspect ratio", + "(beta_poloidal_eps)", + physics_variables.beta_poloidal_eps, "OP ", ) + po.ovarrf( + self.outfile, + "Poloidal beta and inverse aspect ratio upper limit", + "(beta_poloidal_eps_max)", + physics_variables.beta_poloidal_eps_max, + ) + po.osubhd(self.outfile, "Normalised Beta Information :") + if stellarator_variables.istell == 0: + if physics_variables.i_beta_norm_max != 0: + po.ovarrf( + self.outfile, + "Beta g coefficient", + "(beta_norm_max)", + physics_variables.beta_norm_max, + "OP ", + ) + else: + po.ovarrf( + self.outfile, + "Beta g coefficient", + "(beta_norm_max)", + physics_variables.beta_norm_max, + ) + po.ovarrf( + self.outfile, + "Normalised total beta", + "(beta_norm_total)", + physics_variables.beta_norm_total, + "OP ", + ) + po.ovarrf( + self.outfile, + "Normalised thermal beta", + "(beta_norm_thermal) ", + physics_variables.beta_norm_thermal, + "OP ", + ) - po.oblnkl(self.outfile) - po.ostars(self.outfile, 110) - po.oblnkl(self.outfile) - - -class BootstrapCurrentFractionModel(IntEnum): - """Bootstrap plasma current fraction (f_BS) model types""" - - USER_INPUT = 0 - ITER_89 = 1 - NEVINS = 2 - WILSON = 3 - SAUTER = 4 - SAKAI = 5 - ARIES = 6 - ANDRADE = 7 - HOANG = 8 - WONG = 9 - GI_1 = 10 - GI_2 = 11 - SUGIYAMA_L_MODE = 12 - SUGIYAMA_H_MODE = 13 - - -class PlasmaBootstrapCurrent: - """Class to hold plasma bootstrap current for plasma processing.""" - - def __init__(self): - self.outfile = constants.NOUT - self.mfile = constants.MFILE - - def get_bootstrap_current_fraction_value( - self, model: BootstrapCurrentFractionModel - ) -> float: - """Get the plasma current bootstrap fraction (f_BS) for the specified model.""" - model_map = { - BootstrapCurrentFractionModel.USER_INPUT: current_drive_variables.f_c_plasma_bootstrap, - BootstrapCurrentFractionModel.ITER_89: current_drive_variables.f_c_plasma_bootstrap_iter89, - BootstrapCurrentFractionModel.NEVINS: current_drive_variables.f_c_plasma_bootstrap_nevins, - BootstrapCurrentFractionModel.WILSON: current_drive_variables.f_c_plasma_bootstrap_wilson, - BootstrapCurrentFractionModel.SAUTER: current_drive_variables.f_c_plasma_bootstrap_sauter, - BootstrapCurrentFractionModel.SAKAI: current_drive_variables.f_c_plasma_bootstrap_sakai, - BootstrapCurrentFractionModel.ARIES: current_drive_variables.f_c_plasma_bootstrap_aries, - BootstrapCurrentFractionModel.ANDRADE: current_drive_variables.f_c_plasma_bootstrap_andrade, - BootstrapCurrentFractionModel.HOANG: current_drive_variables.f_c_plasma_bootstrap_hoang, - BootstrapCurrentFractionModel.WONG: current_drive_variables.f_c_plasma_bootstrap_wong, - BootstrapCurrentFractionModel.GI_1: current_drive_variables.bscf_gi_i, - BootstrapCurrentFractionModel.GI_2: current_drive_variables.bscf_gi_ii, - BootstrapCurrentFractionModel.SUGIYAMA_L_MODE: current_drive_variables.f_c_plasma_bootstrap_sugiyama_l, - BootstrapCurrentFractionModel.SUGIYAMA_H_MODE: current_drive_variables.f_c_plasma_bootstrap_sugiyama_h, - } - return model_map[model] - - @staticmethod - def bootstrap_fraction_iter89( - aspect: float, - beta: float, - b_plasma_toroidal_on_axis: float, - plasma_current: float, - q95: float, - q0: float, - rmajor: float, - vol_plasma: float, - ) -> float: - """ - Calculate the bootstrap-driven fraction of the plasma current. - - Args: - aspect (float): Plasma aspect ratio. - beta (float): Plasma total beta. - b_plasma_toroidal_on_axis (float): Toroidal field on axis (T). - plasma_current (float): Plasma current (A). - q95 (float): Safety factor at 95% surface. - q0 (float): Central safety factor. - rmajor (float): Plasma major radius (m). - vol_plasma (float): Plasma volume (m3). - - Returns: - float: The bootstrap-driven fraction of the plasma current. - - This function performs the original ITER calculation of the plasma current bootstrap fraction. - - Reference: - - ITER Physics Design Guidelines: 1989 [IPDG89], N. A. Uckan et al, - - ITER Documentation Series No.10, IAEA/ITER/DS/10, IAEA, Vienna, 1990 - """ - - # Calculate the bootstrap current coefficient - c_bs = 1.32 - 0.235 * (q95 / q0) + 0.0185 * (q95 / q0) ** 2 - - # Calculate the average minor radius - average_a = np.sqrt(vol_plasma / (2 * np.pi**2 * rmajor)) - - b_pa = (plasma_current / 1e6) / (5 * average_a) - - # Calculate the poloidal beta for bootstrap current - betapbs = beta * (b_plasma_toroidal_on_axis / b_pa) ** 2 - - # Ensure betapbs is positive - if betapbs <= 0.0: - return 0.0 - - # Calculate and return the bootstrap current fraction - return c_bs * (betapbs / np.sqrt(aspect)) ** 1.3 - - @staticmethod - def bootstrap_fraction_wilson( - alphaj: float, - alphap: float, - alphat: float, - betpth: float, - q0: float, - q95: float, - rmajor: float, - rminor: float, - ) -> float: - """ - Bootstrap current fraction from Wilson et al scaling - - Args: - alphaj (float): Current profile index. - alphap (float): Pressure profile index. - alphat (float): Temperature profile index. - betpth (float): Thermal component of poloidal beta. - q0 (float): Safety factor on axis. - q95 (float): Edge safety factor. - rmajor (float): Major radius (m). - rminor (float): Minor radius (m). - - Returns: - float: The bootstrap current fraction. - - This function calculates the bootstrap current fraction using the numerically fitted algorithm written by Howard Wilson. - - Reference: - - AEA FUS 172: Physics Assessment for the European Reactor Study, 1989 - - H. R. Wilson, Nuclear Fusion 32 (1992) 257 - """ - - term1 = np.log(0.5) - term2 = np.log(q0 / q95) - - # Re-arranging of parabolic profile to be equal to (r/a)^2 where the profile value is half of the core value - - termp = 1.0 - 0.5 ** (1.0 / alphap) - termt = 1.0 - 0.5 ** (1.0 / alphat) - termj = 1.0 - 0.5 ** (1.0 / alphaj) - - # Assuming a parabolic safety factor profile of the form q = q0 + (q95 - q0) * (r/a)^2 - # Substitute (r/a)^2 term from temperature,pressure and current profiles into q profile when values is 50% of core value - # Take natural log of q profile over q95 and q0 to get the profile index + po.ovarrf( + self.outfile, + "Normalised toroidal beta", + "(beta_norm_toroidal) ", + physics_variables.beta_norm_toroidal, + "OP ", + ) - alfpnw = term1 / np.log(np.log((q0 + (q95 - q0) * termp) / q95) / term2) - alftnw = term1 / np.log(np.log((q0 + (q95 - q0) * termt) / q95) / term2) - aj = term1 / np.log(np.log((q0 + (q95 - q0) * termj) / q95) / term2) + po.ovarrf( + self.outfile, + "Normalised poloidal beta", + "(beta_norm_poloidal) ", + physics_variables.beta_norm_poloidal, + "OP ", + ) - # Crude check for NaN errors or other illegal values. - if np.isnan(aj) or np.isnan(alfpnw) or np.isnan(alftnw) or aj < 0: - raise ProcessValueError( - "Illegal profile value found", aj=aj, alfpnw=alfpnw, alftnw=alftnw + po.osubhd(self.outfile, "Maximum normalised beta scalings :") + po.ovarrf( + self.outfile, + "J. Wesson normalised beta upper limit", + "(beta_norm_max_wesson) ", + physics_variables.beta_norm_max_wesson, + "OP ", + ) + po.ovarrf( + self.outfile, + "Original normalsied beta upper limit", + "(beta_norm_max_original_scaling) ", + physics_variables.beta_norm_max_original_scaling, + "OP ", + ) + po.ovarrf( + self.outfile, + "J. Menard normalised beta upper limit", + "(beta_norm_max_menard) ", + physics_variables.beta_norm_max_menard, + "OP ", + ) + po.ovarrf( + self.outfile, + "E. Thloreus normalised beta upper limit", + "(beta_norm_max_thloreus) ", + physics_variables.beta_norm_max_thloreus, + "OP ", + ) + po.ovarrf( + self.outfile, + "R. Stambaugh normalised beta upper limit", + "(beta_norm_max_stambaugh) ", + physics_variables.beta_norm_max_stambaugh, + "OP ", ) - # Ratio of ionic charge to electron charge + po.osubhd(self.outfile, "Plasma energies derived from beta :") + po.ovarre( + self.outfile, + "Plasma thermal energy derived from thermal beta (J)", + "(e_plasma_beta_thermal) ", + physics_variables.e_plasma_beta_thermal, + "OP", + ) - z = 1.0 + po.ovarre( + self.outfile, + "Plasma thermal energy derived from the total beta (J)", + "(e_plasma_beta)", + physics_variables.e_plasma_beta, + "OP", + ) - # Inverse aspect ratio: r2 = maximum plasma radius, r1 = minimum - # This is the definition used in the paper - r2 = rmajor + rminor - r1 = rmajor - rminor - eps1 = (r2 - r1) / (r2 + r1) + po.oblnkl(self.outfile) + po.ostars(self.outfile, 110) + po.oblnkl(self.outfile) - # Coefficients fitted using least squares techniques - # Square root of current profile index term - saj = np.sqrt(aj) +class IndInternalNormModel(IntEnum): + """Normalised internal inductance (l_i) model types""" - a = np.array([ - 1.41 * (1.0 - 0.28 * saj) * (1.0 + 0.12 / z), - 0.36 * (1.0 - 0.59 * saj) * (1.0 + 0.8 / z), - -0.27 * (1.0 - 0.47 * saj) * (1.0 + 3.0 / z), - 0.0053 * (1.0 + 5.0 / z), - -0.93 * (1.0 - 0.34 * saj) * (1.0 + 0.15 / z), - -0.26 * (1.0 - 0.57 * saj) * (1.0 - 0.27 * z), - 0.064 * (1.0 - 0.6 * aj + 0.15 * aj * aj) * (1.0 + 7.6 / z), - -0.0011 * (1.0 + 9.0 / z), - -0.33 * (1.0 - aj + 0.33 * aj * aj), - -0.26 * (1.0 - 0.87 / saj - 0.16 * aj), - -0.14 * (1.0 - 1.14 / saj - 0.45 * saj), - -0.0069, - ]) + USER_INPUT = 0 + WESSON = 1 + MENARD = 2 - seps1 = np.sqrt(eps1) - - b = np.array([ - 1.0, - alfpnw, - alftnw, - alfpnw * alftnw, - seps1, - alfpnw * seps1, - alftnw * seps1, - alfpnw * alftnw * seps1, - eps1, - alfpnw * eps1, - alftnw * eps1, - alfpnw * alftnw * eps1, - ]) - # Empirical bootstrap current fraction - return seps1 * betpth * (a * b).sum() +class PlasmaInductance: + """Class to hold plasma inductance calculations for plasma processing.""" - @staticmethod - def bootstrap_fraction_nevins( - alphan: float, - alphat: float, - beta_toroidal: float, - b_plasma_toroidal_on_axis: float, - nd_plasma_electrons_vol_avg: float, - plasma_current: float, - q95: float, - q0: float, - rmajor: float, - rminor: float, - te: float, - zeff: float, - ) -> float: - """ - Calculate the bootstrap current fraction from Nevins et al scaling. - - Args: - alphan (float): Density profile index. - alphat (float): Temperature profile index. - beta_toroidal (float): Toroidal plasma beta. - b_plasma_toroidal_on_axis (float): Toroidal field on axis (T). - nd_plasma_electrons_vol_avg (float): Electron density (/m3). - plasma_current (float): Plasma current (A). - q0 (float): Central safety factor. - q95 (float): Safety factor at 95% surface. - rmajor (float): Plasma major radius (m). - rminor (float): Plasma minor radius (m). - te (float): Volume averaged plasma temperature (keV). - zeff (float): Plasma effective charge. - - Returns: - float: The bootstrap current fraction. - - This function calculates the bootstrap current fraction using the Nevins et al method. - - Reference: See appendix of: - Keii Gi, Makoto Nakamura, Kenji Tobita, Yasushi Ono, - Bootstrap current fraction scaling for a tokamak reactor design study, - Fusion Engineering and Design, Volume 89, Issue 11, 2014, Pages 2709-2715, - ISSN 0920-3796, https://doi.org/10.1016/j.fusengdes.2014.07.009. - - Nevins, W. M. "Summary report: ITER specialists' meeting on heating and current drive." - ITER-TN-PH-8-4, June 1988. 1988. + def __init__(self): + self.outfile = constants.NOUT + self.mfile = constants.MFILE - """ - # Calculate peak electron beta at plasma centre, this is not the form used in the paper - # The paper assumes parabolic profiles for calculating core values with the profile indexes. - # We instead use the directly calculated electron density and temperature values at the core. - # So that it is compatible with all profiles - - betae0 = ( - physics_variables.nd_plasma_electron_on_axis - * physics_variables.temp_plasma_electron_on_axis_kev - * 1.0e3 - * constants.ELECTRON_CHARGE - / (b_plasma_toroidal_on_axis**2 / (2.0 * constants.RMU0)) - ) - - # Call integration routine using definite integral routine from scipy - - ainteg, _ = integrate.quad( - lambda y: _nevins_integral( - y, - nd_plasma_electrons_vol_avg, - te, - b_plasma_toroidal_on_axis, - rminor, - rmajor, - zeff, - alphat, - alphan, - q0, - q95, - beta_toroidal, - ), - 0, # Lower bound - 1.0, # Upper bound + def run(self): + physics_variables.ind_plasma_internal_norm_wesson = ( + self.calculate_internal_inductance_wesson(alphaj=physics_variables.alphaj) ) - # Calculate bootstrap current and fraction - - aibs = 2.5 * betae0 * rmajor * b_plasma_toroidal_on_axis * q95 * ainteg - return 1.0e6 * aibs / plasma_current - - @staticmethod - def bootstrap_fraction_sauter(plasma_profile: float) -> float: - """Calculate the bootstrap current fraction from the Sauter et al scaling. - - Args: - plasma_profile (PlasmaProfile): The plasma profile object. + # Spherical Tokamak relation for internal inductance + # Menard et al. (2016), Nuclear Fusion, 56, 106023 + physics_variables.ind_plasma_internal_norm_menard = ( + self.calculate_internal_inductance_menard(kappa=physics_variables.kappa) + ) - Returns: - float: The bootstrap current fraction. + physics_variables.ind_plasma_internal_norm_iter_3 = ( + self.calculate_normalised_internal_inductance_iter_3( + b_plasma_poloidal_vol_avg=physics_variables.b_plasma_poloidal_average, + c_plasma=physics_variables.plasma_current, + vol_plasma=physics_variables.vol_plasma, + rmajor=physics_variables.rmajor, + ) + ) - This function calculates the bootstrap current fraction using the Sauter, Angioni, and Lin-Liu scaling. + # Calculate ind_plasma_internal_norm based on i_ind_plasma_internal_norm + try: + model = IndInternalNormModel( + int(physics_variables.i_ind_plasma_internal_norm) + ) + physics_variables.ind_plasma_internal_norm = ( + self.get_ind_internal_norm_value(model) + ) + except ValueError: + raise ProcessValueError( + "Illegal value of i_ind_plasma_internal_norm", + i_ind_plasma_internal_norm=physics_variables.i_ind_plasma_internal_norm, + ) from None - Reference: - - O. Sauter, C. Angioni, Y. R. Lin-Liu; - Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime. - Phys. Plasmas 1 July 1999; 6 (7): 2834-2839. https://doi.org/10.1063/1.873240 - - O. Sauter, C. Angioni, Y. R. Lin-Liu; Erratum: - Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime - [Phys. Plasmas 6, 2834 (1999)]. Phys. Plasmas 1 December 2002; 9 (12): 5140. https://doi.org/10.1063/1.1517052 + def get_ind_internal_norm_value(self, model: IndInternalNormModel) -> float: + """Get the normalised internal inductance (l_i) for the specified model. - Note: - The code was supplied by Emiliano Fable, IPP Garching (private communication). + Parameters + ---------- + model: IndInternalNormModel : """ + model_map = { + IndInternalNormModel.USER_INPUT: physics_variables.ind_plasma_internal_norm, + IndInternalNormModel.WESSON: physics_variables.ind_plasma_internal_norm_wesson, + IndInternalNormModel.MENARD: physics_variables.ind_plasma_internal_norm_menard, + } + return model_map[model] - # Radial points from 0 to 1 seperated by 1/profile_size - roa = plasma_profile.neprofile.profile_x + @staticmethod + def calculate_volt_second_requirements( + csawth: float, + eps: float, + f_c_plasma_inductive: float, + ejima_coeff: float, + kappa: float, + rmajor: float, + res_plasma: float, + plasma_current: float, + t_plant_pulse_fusion_ramp: float, + t_plant_pulse_burn: float, + ind_plasma_internal_norm: float, + ) -> tuple[float, float, float, float, float, float]: + """Calculate the volt-second requirements and related parameters for plasma physics. - # Local circularised minor radius - rho = np.sqrt(physics_variables.a_plasma_poloidal / np.pi) * roa + Parameters + ---------- + csawth : float + Coefficient for sawteeth effects + eps : float + Inverse aspect ratio + f_c_plasma_inductive : float + Fraction of plasma current produced inductively + ejima_coeff : float + Ejima coefficient for resistive start-up V-s component + kappa : float + Plasma elongation + rmajor : float + Plasma major radius (m) + res_plasma : float + Plasma resistance (ohm) + plasma_current : float + Plasma current (A) + t_plant_pulse_fusion_ramp : float + Heating time (s) + t_plant_pulse_burn : float + Burn time (s) + ind_plasma_internal_norm : float + Plasma normalized internal inductance - # Square root of local aspect ratio - sqeps = np.sqrt(roa * (physics_variables.rminor / physics_variables.rmajor)) - # Calculate electron and ion density profiles - ne = plasma_profile.neprofile.profile_y * 1e-19 - ni = ( - physics_variables.nd_plasma_ions_total_vol_avg - / physics_variables.nd_plasma_electrons_vol_avg - ) * ne + Returns + ------- + tuple[float, float, float, float, float, float] + A tuple containing: + - vs_plasma_internal: Internal plasma volt-seconds (Wb) + - ind_plasma_internal: Plasma inductance (H) + - vs_plasma_burn_required: Volt-seconds needed during flat-top (heat+burn) (Wb) + - vs_plasma_ramp_required: Volt-seconds needed during ramp-up (Wb) + - ind_plasma_total,: Internal and external plasma inductance V-s (Wb) + - vs_res_ramp: Resistive losses in start-up volt-seconds (Wb) + - vs_plasma_total_required: Total volt-seconds needed (Wb) - # Calculate electron and ion temperature profiles - tempe = plasma_profile.teprofile.profile_y - tempi = ( - physics_variables.temp_plasma_ion_vol_avg_kev - / physics_variables.temp_plasma_electron_vol_avg_kev - ) * tempe + References + ---------- + - S. Ejima, R. W. Callis, J. L. Luxon, R. D. Stambaugh, T. S. Taylor, and J. C. Wesley, + “Volt-second analysis and consumption in Doublet III plasmas,” + Nuclear Fusion, vol. 22, no. 10, pp. 1313-1319, Oct. 1982, doi: + https://doi.org/10.1088/0029-5515/22/10/006. - # Flat Zeff profile assumed - # Return tempi like array object filled with zeff - zeff = np.full_like(tempi, physics_variables.n_charge_plasma_effective_vol_avg) + - S. C. Jardin, C. E. Kessel, and N Pomphrey, + “Poloidal flux linkage requirements for the International Thermonuclear Experimental Reactor,” + Nuclear Fusion, vol. 34, no. 8, pp. 1145-1160, Aug. 1994, + doi: https://doi.org/10.1088/0029-5515/34/8/i07. - # inverse_q = 1/safety factor - # Parabolic q profile assumed - inverse_q = 1 / ( - physics_variables.q0 - + (physics_variables.q95 - physics_variables.q0) * roa**2 - ) - # Create new array of average mass of fuel portion of ions - amain = np.full_like(inverse_q, physics_variables.m_ions_total_amu) + - S. P. Hirshman and G. H. Neilson, “External inductance of an axisymmetric plasma,” + The Physics of Fluids, vol. 29, no. 3, pp. 790-793, Mar. 1986, + doi: https://doi.org/10.1063/1.865934. - # Create new array of average main ion charge - zmain = np.full_like(inverse_q, 1.0 + physics_variables.f_plasma_fuel_helium3) + """ + # Plasma internal inductance - # Calculate total bootstrap current (MA) by summing along profiles - # Looping from 2 because _calculate_l31_coefficient() etc should return 0 @ j == 1 - radial_elements = np.arange(2, plasma_profile.profile_size) + ind_plasma_internal = constants.RMU0 * rmajor * ind_plasma_internal_norm / 2.0 - # Change in localised minor radius to be used as delta term in derivative - drho = rho[radial_elements] - rho[radial_elements - 1] + # Internal plasma flux (V-s) component + vs_plasma_internal = ind_plasma_internal * plasma_current - # Area of annulus, assuming circular plasma cross-section - da = 2 * np.pi * rho[radial_elements - 1] * drho # area of annulus + # Start-up resistive component + # Uses ITER formula without the 10 V-s add-on - # Create the partial derivatives using numpy gradient (central differences) - dlogte_drho = np.gradient(np.log(tempe), rho)[radial_elements - 1] - dlogti_drho = np.gradient(np.log(tempi), rho)[radial_elements - 1] - dlogne_drho = np.gradient(np.log(ne), rho)[radial_elements - 1] + vs_res_ramp = ejima_coeff * constants.RMU0 * plasma_current * rmajor - jboot = ( - 0.5 - * ( - _calculate_l31_coefficient( - radial_elements, - plasma_profile.profile_size, - physics_variables.rmajor, - physics_variables.b_plasma_toroidal_on_axis, - physics_variables.triang, - ne, - ni, - tempe, - tempi, - inverse_q, - rho, - zeff, - sqeps, - ) - * dlogne_drho - + _calculate_l31_32_coefficient( - radial_elements, - plasma_profile.profile_size, - physics_variables.rmajor, - physics_variables.b_plasma_toroidal_on_axis, - physics_variables.triang, - ne, - ni, - tempe, - tempi, - inverse_q, - rho, - zeff, - sqeps, - ) - * dlogte_drho - + _calculate_l34_alpha_31_coefficient( - radial_elements, - plasma_profile.profile_size, - physics_variables.rmajor, - physics_variables.b_plasma_toroidal_on_axis, - physics_variables.triang, - inverse_q, - sqeps, - tempi, - tempe, - amain, - zmain, - ni, - ne, - rho, - zeff, - ) - * dlogti_drho - ) - * 1.0e6 - * ( - -physics_variables.b_plasma_toroidal_on_axis - / (0.2 * np.pi * physics_variables.rmajor) - * rho[radial_elements - 1] - * inverse_q[radial_elements - 1] - ) - ) # A/m2 + # ====================================================================== - return (np.sum(da * jboot, axis=0) / physics_variables.plasma_current), jboot + # Hirshman and Neilson fit for external inductance - @staticmethod - def bootstrap_fraction_sakai( - beta_poloidal: float, - q95: float, - q0: float, - alphan: float, - alphat: float, - eps: float, - ind_plasma_internal_norm: float, - ) -> float: - """ - Calculate the bootstrap fraction using the Sakai formula. - - Parameters: - beta_poloidal (float): Plasma poloidal beta. - q95 (float): Safety factor at 95% of the plasma radius. - q0 (float): Safety factor at the magnetic axis. - alphan (float): Density profile index - alphat (float): Temperature profile index - eps (float): Inverse aspect ratio. - ind_plasma_internal_norm (float): Plasma normalised internal inductance - - Returns: - float: The calculated bootstrap fraction. - - Notes: - The profile assumed for the alphan and alpat indexes is only a parabolic profile without a pedestal (L-mode). - The Root Mean Squared Error for the fitting database of this formula was 0.025 - Concentrating on the positive shear plasmas using the ACCOME code equilibria with the fully non-inductively driven - conditions with neutral beam (NB) injection only are calculated. - The electron temperature and the ion temperature were assumed to be equal - This can be used for all apsect ratios. - The diamagnetic fraction is included in this formula. - - References: - Ryosuke Sakai, Takaaki Fujita, Atsushi Okamoto, Derivation of bootstrap current fraction scaling formula for 0-D system code analysis, - Fusion Engineering and Design, Volume 149, 2019, 111322, ISSN 0920-3796, - https://doi.org/10.1016/j.fusengdes.2019.111322. - """ - # Sakai states that the ACCOME dataset used has the toridal diamagnetic current included in the bootstrap current - # So the diamganetic current should not be calculated with this. i_diamagnetic_current = 0 - return ( - 10 ** (0.951 * eps - 0.948) - * beta_poloidal ** (1.226 * eps + 1.584) - * ind_plasma_internal_norm ** (-0.184 * eps - 0.282) - * (q95 / q0) ** (-0.042 * eps - 0.02) - * alphan ** (0.13 * eps + 0.05) - * alphat ** (0.502 * eps - 0.273) + aeps = (1.0 + 1.81 * np.sqrt(eps) + 2.05 * eps) * np.log(8.0 / eps) - ( + 2.0 + 9.25 * np.sqrt(eps) - 1.21 * eps ) + beps = 0.73 * np.sqrt(eps) * (1.0 + 2.0 * eps**4 - 6.0 * eps**5 + 3.7 * eps**6) - @staticmethod - def bootstrap_fraction_aries( - beta_poloidal: float, - ind_plasma_internal_norm: float, - core_density: float, - average_density: float, - inverse_aspect: float, - ) -> float: - """ - Calculate the bootstrap fraction using the ARIES formula. - - Parameters: - beta_poloidal (float): Plasma poloidal beta. - ind_plasma_internal_norm (float): Plasma normalized internal inductance. - core_density (float): Core plasma density. - average_density (float): Average plasma density. - inverse_aspect (float): Inverse aspect ratio. - - Returns: - float: The calculated bootstrap fraction. - - Notes: - - The source reference does not provide any info about the derivation of the formula. It is only stated - - References: - - Zoran Dragojlovic et al., “An advanced computational algorithm for systems analysis of tokamak power plants,” - Fusion Engineering and Design, vol. 85, no. 2, pp. 243-265, Apr. 2010, - doi: https://doi.org/10.1016/j.fusengdes.2010.02.015. - - """ - # Using the standard variable naming from the ARIES paper - a_1 = ( - 1.10 - 1.165 * ind_plasma_internal_norm + 0.47 * ind_plasma_internal_norm**2 - ) - b_1 = ( - 0.806 - - 0.885 * ind_plasma_internal_norm - + 0.297 * ind_plasma_internal_norm**2 + ind_plasma_external = ( + rmajor * constants.RMU0 * aeps * (1.0 - eps) / (1.0 - eps + beps * kappa) ) - c_bs = a_1 + b_1 * (core_density / average_density) - - return c_bs * np.sqrt(inverse_aspect) * beta_poloidal + # ====================================================================== - @staticmethod - def bootstrap_fraction_andrade( - beta_poloidal: float, - core_pressure: float, - average_pressure: float, - inverse_aspect: float, - ) -> float: - """ - Calculate the bootstrap fraction using the Andrade et al formula. + ind_plasma_total = ind_plasma_external + ind_plasma_internal - Parameters: - beta_poloidal (float): Plasma poloidal beta. - core_pressure (float): Core plasma pressure. - average_pressure (float): Average plasma pressure. - inverse_aspect (float): Inverse aspect ratio. + # Inductive V-s component - Returns: - float: The calculated bootstrap fraction. + vs_self_ind_ramp = ind_plasma_total * plasma_current + vs_plasma_ramp_required = vs_res_ramp + vs_self_ind_ramp - Notes: - - Based off 350 plasma profiles from Experimento Tokamak Esferico (ETE) spherical tokamak - - A = 1.5, R_0 = 0.3m, I_p = 200kA, B_0=0.4T, beta = 4-10%. Profiles taken as Gaussian shaped functions. - - Errors mostly up to the order of 10% are obtained when both expressions are compared with the equilibrium estimates for the - bootstrap current in ETE + # Plasma loop voltage during flat-top + # Include enhancement factor in flattop V-s requirement + # to account for MHD sawtooth effects. - References: - - M. C. R. Andrade and G. O. Ludwig, “Scaling of bootstrap current on equilibrium and plasma profile parameters in tokamak plasmas,” - Plasma Physics and Controlled Fusion, vol. 50, no. 6, pp. 065001-065001, Apr. 2008, - doi: https://doi.org/10.1088/0741-3335/50/6/065001. + v_plasma_loop_burn = plasma_current * res_plasma * f_c_plasma_inductive - """ + v_burn_resistive = v_plasma_loop_burn * csawth - # Using the standard variable naming from the Andrade et.al. paper - c_p = core_pressure / average_pressure + # N.B. t_plant_pulse_burn on first iteration will not be correct + # if the pulsed reactor option is used, but the value + # will be correct on subsequent calls. - # Error +- 0.0007 - c_bs = 0.2340 + vs_plasma_burn_required = v_burn_resistive * ( + t_plant_pulse_fusion_ramp + t_plant_pulse_burn + ) + vs_plasma_total_required = vs_plasma_ramp_required + vs_plasma_burn_required - return c_bs * np.sqrt(inverse_aspect) * beta_poloidal * c_p**0.8 + return ( + vs_plasma_internal, + ind_plasma_total, + vs_plasma_burn_required, + vs_plasma_ramp_required, + vs_self_ind_ramp, + vs_res_ramp, + vs_plasma_total_required, + v_plasma_loop_burn, + ) @staticmethod - def bootstrap_fraction_hoang( - beta_poloidal: float, - pressure_index: float, - current_index: float, - inverse_aspect: float, + def calculate_normalised_internal_inductance_iter_3( + b_plasma_poloidal_vol_avg: float, + c_plasma: float, + vol_plasma: float, + rmajor: float, ) -> float: - """ - Calculate the bootstrap fraction using the Hoang et al formula. - - Parameters: - beta_poloidal (float): Plasma poloidal beta. - pressure_index (float): Pressure profile index. - current_index (float): Current profile index. - inverse_aspect (float): Inverse aspect ratio. - - Returns: - float: The calculated bootstrap fraction. - - Notes: - - Based off of TFTR data calculated using the TRANSP plasma analysis code - - 170 discharges which was assembled to study the tritium influx and transport in discharges with D-only neutral beam - injection (NBI) - - Contains L-mode, supershots, reversed shear, enhanced reversed shear and increased li discharges - - Discharges with monotonic flux profiles with reversed shear are also included - - Is applied to circular cross-section plasmas - - References: - - G. T. Hoang and R. V. Budny, “The bootstrap fraction in TFTR,” AIP conference proceedings, - Jan. 1997, doi: https://doi.org/10.1063/1.53414. - """ - - # Using the standard variable naming from the Hoang et.al. paper - # Hoang et.al uses a different definition for the profile indexes such that - # alpha_p is defined as the ratio of the central and the volume-averaged values, and the peakedness of the density of the total plasma current - # (defined as ratio of the central value and I_p), alpha_j$ + """Calculate the normalised internal inductance using ITER-3 scaling li(3). - # We assume the pressure and current profile is parabolic and use the (profile_index +1) term in lieu - # The term represents the ratio of the the core to volume averaged value + Parameters + ------------ + b_plasma_poloidal_vol_avg : float + Volume-averaged poloidal magnetic field (T). + c_plasma : float + Plasma current (A). + vol_plasma : float + Plasma volume (m^3). + rmajor : float + Plasma major radius (m). - # This could lead to large changes in the value depending on interpretation of the profile index + Returns + -------- + float + The li(3) normalised internal inductance. - c_bs = np.sqrt((pressure_index + 1) / (current_index + 1)) + References + ----------- + - T. C. Luce, D. A. Humphreys, G. L. Jackson, and W. M. Solomon, + “Inductive flux usage and its optimization in tokamak operation,” + Nuclear Fusion, vol. 54, no. 9, p. 093005, Jul. 2014, + doi: https://doi.org/10.1088/0029-5515/54/9/093005. - return 0.4 * np.sqrt(inverse_aspect) * beta_poloidal**0.9 * c_bs + - G. L. Jackson et al., “ITER startup studies in the DIII-D tokamak,” + Nuclear Fusion, vol. 48, no. 12, p. 125002, Nov. 2008, + doi: https://doi.org/10.1088/0029-5515/48/12/125002. - @staticmethod - def bootstrap_fraction_wong( - beta_poloidal: float, - density_index: float, - temperature_index: float, - inverse_aspect: float, - elongation: float, - ) -> float: - """ - Calculate the bootstrap fraction using the Wong et al formula. - - Parameters: - beta_poloidal (float): Plasma poloidal beta. - density_index (float): Density profile index. - temperature_index (float): Temperature profile index. - inverse_aspect (float): Inverse aspect ratio. - elongation (float): Plasma elongation. - - Returns: - float: The calculated bootstrap fraction. - - Notes: - - Data is based off of equilibria from Miller et al. - - A: 1.2 - 3.0 and stable to n ballooning and low n kink modes at a bootstrap fraction of 99% for kappa = 2, 2.5 and 3 - - The results were parameterized as a function of aspect ratio and elongation - - The parametric dependency of beta_p and beta_T are based on fitting of the DIII-D high equivalent DT yield results - - Parabolic profiles should be used for best results as the pressure peaking value is calculated as the product of a parabolic - temperature and density profile - - References: - - C.-P. Wong, J. C. Wesley, R. D. Stambaugh, and E. T. Cheng, “Toroidal reactor designs as a function of aspect ratio and elongation,” - vol. 42, no. 5, pp. 547-556, May 2002, doi: https://doi.org/10.1088/0029-5515/42/5/307. - - - Miller, R L, "Stable bootstrap-current driven equilibria for low aspect ratio tokamaks". - Switzerland: N. p., 1996. Web.https://fusion.gat.com/pubs-ext/MISCONF96/A22433.pdf """ - # Using the standard variable naming from the Wong et.al. paper - f_peak = 2.0 / scipy.special.beta(0.5, density_index + temperature_index + 1) - c_bs = 0.773 + 0.019 * elongation - - return c_bs * f_peak**0.25 * beta_poloidal * np.sqrt(inverse_aspect) + return ( + 2 + * vol_plasma + * b_plasma_poloidal_vol_avg**2 + / (constants.RMU0**2 * c_plasma**2 * rmajor) + ) @staticmethod - def bootstrap_fraction_gi_I( # noqa: N802 - beta_poloidal: float, - pressure_index: float, - temperature_index: float, - inverse_aspect: float, - effective_charge: float, - q95: float, - q0: float, - ) -> float: - """ - Calculate the bootstrap fraction using the first scaling from the Gi et al formula. - - Parameters: - beta_poloidal (float): Plasma poloidal beta. - pressure_index (float): Pressure profile index. - temperature_index (float): Temperature profile index. - inverse_aspect (float): Inverse aspect ratio. - effective_charge (float): Plasma effective charge. - q95 (float): Safety factor at 95% of the plasma radius. - q0 (float): Safety factor at the magnetic axis. - - Returns: - float: The calculated bootstrap fraction. - - Notes: - - Scaling found by solving the Hirshman-Sigmar bootstrap modelusing the matrix inversion method - - Method was done to put the scaling into parameters compatible with the TPC systems code - - Uses the ACCOME code to create bootstrap current fractions without using the itrative calculations of the - curent drive and equilibrium models in the scan - - R = 5.0 m, A = 1.3 - 5.0, kappa = 2, traing = 0.3, alpha_n = 0.1 - 0.8, alpha_t = 1.0 - 3.0, Z_eff = 1.2 - 3.0 - - Uses parabolic plasma profiles only. - - Scaling 1 has better accuracy than Scaling 2. However, Scaling 1 overestimated the f_BS value for reversed shear - equilibrium. - - References: - - K. Gi, M. Nakamura, Kenji Tobita, and Y. Ono, “Bootstrap current fraction scaling for a tokamak reactor design study,” - Fusion Engineering and Design, vol. 89, no. 11, pp. 2709-2715, Aug. 2014, - doi: https://doi.org/10.1016/j.fusengdes.2014.07.009. - """ + def calculate_internal_inductance_menard(kappa: float) -> float: + """Calculate the Menard plasma normalized internal inductance. - # Using the standard variable naming from the Gi et.al. paper + Parameters + ---------- + kappa : float + Plasma separatrix elongation. - c_bs = ( - 0.474 - * inverse_aspect**-0.1 - * pressure_index**0.974 - * temperature_index**-0.416 - * effective_charge**0.178 - * (q95 / q0) ** -0.133 - ) + Returns + ------- + float + The Menard plasma normalised internal inductance. - return c_bs * np.sqrt(inverse_aspect) * beta_poloidal + Notes + ----- + - This relation is based off of data from NSTX for l_i in the range of 0.4-0.85 + - This model is only recommended to be used for ST's with kappa > 2.5 + + References + ---------- + - J. E. Menard et al., “Fusion nuclear science facilities and pilot plants based on the spherical tokamak,” + Nuclear Fusion, vol. 56, no. 10, p. 106023, Aug. 2016, + doi: https://doi.org/10.1088/0029-5515/56/10/106023. - @staticmethod - def bootstrap_fraction_gi_II( # noqa: N802 - beta_poloidal: float, - pressure_index: float, - temperature_index: float, - inverse_aspect: float, - effective_charge: float, - ) -> float: - """ - Calculate the bootstrap fraction using the second scaling from the Gi et al formula. - - Parameters: - beta_poloidal (float): Plasma poloidal beta. - pressure_index (float): Pressure profile index. - temperature_index (float): Temperature profile index. - inverse_aspect (float): Inverse aspect ratio. - effective_charge (float): Plasma effective charge. - - Returns: - float: The calculated bootstrap fraction. - - Notes: - - Scaling found by solving the Hirshman-Sigmar bootstrap modelusing the matrix inversion method - - Method was done to put the scaling into parameters compatible with the TPC systems code - - Uses the ACCOME code to create bootstrap current fractions without using the itrative calculations of the - curent drive and equilibrium models in the scan - - R = 5.0 m, A = 1.3 - 5.0, kappa = 2, traing = 0.3, alpha_n = 0.1 - 0.8, alpha_t = 1.0 - 3.0, Z_eff = 1.2 - 3.0 - - Uses parabolic plasma profiles only. - - This scaling has the q profile dependance removed to obtain a scaling formula with much more flexible variables than - that by a single profile factor for internal current profile. - - References: - - K. Gi, M. Nakamura, Kenji Tobita, and Y. Ono, “Bootstrap current fraction scaling for a tokamak reactor design study,” - Fusion Engineering and Design, vol. 89, no. 11, pp. 2709-2715, Aug. 2014, - doi: https://doi.org/10.1016/j.fusengdes.2014.07.009. """ + return 3.4 - kappa - # Using the standard variable naming from the Gi et.al. paper + @staticmethod + def calculate_internal_inductance_wesson(alphaj: float) -> float: + """Calculate the Wesson plasma normalized internal inductance. - c_bs = ( - 0.382 - * inverse_aspect**-0.242 - * pressure_index**0.974 - * temperature_index**-0.416 - * effective_charge**0.178 - ) + Parameters + ---------- + alphaj : float + Current profile index. - return c_bs * np.sqrt(inverse_aspect) * beta_poloidal + Returns + ------- + float + The Wesson plasma normalised internal inductance. - @staticmethod - def bootstrap_fraction_sugiyama_l_mode( - eps: float, - beta_poloidal: float, - alphan: float, - alphat: float, - zeff: float, - q95: float, - q0: float, - ) -> float: - """ - Calculate the bootstrap fraction using the L-mode scaling from the Sugiyama et al formula. - - :param eps: Inverse aspect ratio. - :type eps: float - :param beta_poloidal: Plasma poloidal beta. - :type beta_poloidal: float - :param alphan: Density profile index. - :type alphan: float - :param alphat: Temperature profile index. - :type alphat: float - :param zeff: Plasma effective charge. - :type zeff: float - :param q95: Safety factor at 95% of the plasma radius. - :type q95: float - :param q0: Safety factor at the magnetic axis. - :type q0: float - - :returns: The calculated bootstrap fraction. - :rtype: float - - :notes: - - This scaling is derived for L-mode plasmas. - - Ion and electron temperature are the same - - Z_eff has a uniform profile, with only fully stripped carbon impurity - - :references: - - S. Sugiyama, T. Goto, H. Utoh, and Y. Sakamoto, “Improvement of core plasma power and - current balance models for tokamak systems code considering H-mode plasma profiles,” - Fusion Engineering and Design, vol. 216, p. 115022, Jul. 2025, doi: - https://doi.org/10.1016/j.fusengdes.2025.115022. - """ + Notes + ----- + - It is recommended to use this method with the other Wesson relations for normalised beta and + current profile index. + - This relation is only true for the cyclindrical plasma approximation with parabolic profiles. - return ( - 0.740 - * eps**0.418 - * beta_poloidal**0.904 - * alphan**0.06 - * alphat**-0.138 - * zeff**0.230 - * (q95 / q0) ** -0.142 - ) + References + ---------- + - Wesson, J. (2011) Tokamaks. 4th Edition, 2011 Oxford Science Publications, + International Series of Monographs on Physics, Volume 149. - @staticmethod - def bootstrap_fraction_sugiyama_h_mode( - eps: float, - beta_poloidal: float, - alphan: float, - alphat: float, - tbeta: float, - zeff: float, - q95: float, - q0: float, - radius_plasma_pedestal_density_norm: float, - nd_plasma_pedestal_electron: float, - n_greenwald: float, - temp_plasma_pedestal_kev: float, - ) -> float: - """ - Calculate the bootstrap fraction using the H-mode scaling from the Sugiyama et al formula. - - :param eps: Inverse aspect ratio. - :type eps: float - :param beta_poloidal: Plasma poloidal beta. - :type beta_poloidal: float - :param alphan: Density profile index. - :type alphan: float - :param alphat: Temperature profile index. - :type alphat: float - :param tbeta: Second temperature profile index. - :type tbeta: float - :param zeff: Plasma effective charge. - :type zeff: float - :param q95: Safety factor at 95% of the plasma radius. - :type q95: float - :param q0: Safety factor at the magnetic axis. - :type q0: float - :param radius_plasma_pedestal_density_norm: Normalised plasma radius of density pedestal. - :type radius_plasma_pedestal_density_norm: float - :param nd_plasma_pedestal_electron: Electron number density at the pedestal [m^-3]. - :type nd_plasma_pedestal_electron: float - :param n_greenwald: Greenwald density limit [m^-3]. - :type n_greenwald: float - :param temp_plasma_pedestal_kev: Electron temperature at the pedestal [keV]. - :type temp_plasma_pedestal_kev: float - - :returns: The calculated bootstrap fraction. - :rtype: float - - :notes: - - This scaling is derived for H-mode plasmas. - - The temperature and density pedestal positions are the same - - Separatrix temperature and density are zero - - Ion and electron temperature are the same - - Z_eff has a uniform profile, with only fully stripped carbon impurity - - :references: - - S. Sugiyama, T. Goto, H. Utoh, and Y. Sakamoto, “Improvement of core plasma power and - current balance models for tokamak systems code considering H-mode plasma profiles,” - Fusion Engineering and Design, vol. 216, p. 115022, Jul. 2025, doi: - https://doi.org/10.1016/j.fusengdes.2025.115022. """ + return np.log(1.65 + 0.89 * alphaj) - return ( - 0.789 - * eps**0.606 - * beta_poloidal**0.960 - * alphan**0.0319 - * alphat**0.00822 - * tbeta**-0.0783 - * zeff**0.241 - * (q95 / q0) ** -0.103 - * radius_plasma_pedestal_density_norm**0.367 - * (nd_plasma_pedestal_electron / n_greenwald) ** -0.174 - * temp_plasma_pedestal_kev**0.0552 - ) - - def output_bootstrap_current_information(self): - """Output the calculated bootstrap current information to the output file.""" - - po.ovarrf( - self.outfile, - "Bootstrap current fraction multiplier", - "(cboot)", - current_drive_variables.cboot, - ) - po.ovarrf( - self.outfile, - "Bootstrap fraction (ITER 1989)", - "(f_c_plasma_bootstrap_iter89)", - current_drive_variables.f_c_plasma_bootstrap_iter89, - "OP ", - ) - - po.ovarrf( - self.outfile, - "Bootstrap fraction (Sauter et al)", - "(f_c_plasma_bootstrap_sauter)", - current_drive_variables.f_c_plasma_bootstrap_sauter, - "OP ", - ) - for point in range(len(physics_variables.j_plasma_bootstrap_sauter_profile)): - po.ovarrf( - self.mfile, - f"Sauter et al bootstrap current density profile at point {point}", - f"(j_plasma_bootstrap_sauter_profile{point})", - physics_variables.j_plasma_bootstrap_sauter_profile[point], - "OP ", - ) + def output_volt_second_information(self): + """Output volt-second information to file.""" - po.ovarrf( - self.outfile, - "Bootstrap fraction (Nevins et al)", - "(f_c_plasma_bootstrap_nevins)", - current_drive_variables.f_c_plasma_bootstrap_nevins, - "OP ", - ) - po.ovarrf( + po.osubhd(self.outfile, "Plasma Volt-second Requirements :") + po.ovarre( self.outfile, - "Bootstrap fraction (Wilson)", - "(f_c_plasma_bootstrap_wilson)", - current_drive_variables.f_c_plasma_bootstrap_wilson, + "Total plasma volt-seconds required for pulse (Wb)", + "(vs_plasma_total_required)", + physics_variables.vs_plasma_total_required, "OP ", ) - po.ovarrf( + po.oblnkl(self.outfile) + po.ovarre( self.outfile, - "Bootstrap fraction (Sakai)", - "(f_c_plasma_bootstrap_sakai)", - current_drive_variables.f_c_plasma_bootstrap_sakai, + "Total plasma inductive flux consumption for plasma current ramp-up (Wb)", + "(vs_plasma_ind_ramp)", + physics_variables.vs_plasma_ind_ramp, "OP ", ) - po.ovarrf( + po.ovarre( self.outfile, - "Bootstrap fraction (ARIES)", - "(f_c_plasma_bootstrap_aries)", - current_drive_variables.f_c_plasma_bootstrap_aries, + "Plasma resistive flux consumption for plasma current ramp-up (Wb)", + "(vs_plasma_res_ramp)", + physics_variables.vs_plasma_res_ramp, "OP ", ) - po.ovarrf( + po.ovarre( self.outfile, - "Bootstrap fraction (Andrade)", - "(f_c_plasma_bootstrap_andrade)", - current_drive_variables.f_c_plasma_bootstrap_andrade, + "Total flux consumption for plasma current ramp-up (Wb)", + "(vs_plasma_ramp_required)", + physics_variables.vs_plasma_ramp_required, "OP ", ) po.ovarrf( self.outfile, - "Bootstrap fraction (Hoang)", - "(f_c_plasma_bootstrap_hoang)", - current_drive_variables.f_c_plasma_bootstrap_hoang, - "OP ", + "Ejima coefficient", + "(ejima_coeff)", + physics_variables.ejima_coeff, ) - po.ovarrf( + po.oblnkl(self.outfile) + po.ovarre( self.outfile, - "Bootstrap fraction (Wong)", - "(f_c_plasma_bootstrap_wong)", - current_drive_variables.f_c_plasma_bootstrap_wong, - "OP ", + "Internal plasma V-s", + "(vs_plasma_internal)", + physics_variables.vs_plasma_internal, ) - po.ovarrf( + + po.ovarre( self.outfile, - "Bootstrap fraction (Gi I)", - "(bscf_gi_i)", - current_drive_variables.bscf_gi_i, + "Plasma volt-seconds needed for flat-top (heat + burn times) (Wb)", + "(vs_plasma_burn_required)", + physics_variables.vs_plasma_burn_required, "OP ", ) - po.ovarrf( + + po.ovarre( self.outfile, - "Bootstrap fraction (Gi II)", - "(bscf_gi_ii)", - current_drive_variables.bscf_gi_ii, + "Plasma loop voltage during burn (V)", + "(v_plasma_loop_burn)", + physics_variables.v_plasma_loop_burn, "OP ", ) po.ovarrf( self.outfile, - "Bootstrap fraction (Sugiyama L-mode)", - "(f_c_plasma_bootstrap_sugiyama_l)", - current_drive_variables.f_c_plasma_bootstrap_sugiyama_l, - "OP ", + "Coefficient for sawtooth effects on burn V-s requirement", + "(csawth)", + physics_variables.csawth, ) - po.ovarrf( + po.oblnkl(self.outfile) + po.ovarre( self.outfile, - "Bootstrap fraction (Sugiyama H-mode)", - "(f_c_plasma_bootstrap_sugiyama_h)", - current_drive_variables.f_c_plasma_bootstrap_sugiyama_h, + "Plasma resistance (ohm)", + "(res_plasma)", + physics_variables.res_plasma, "OP ", ) - po.ovarrf( - self.outfile, - "Diamagnetic fraction (Hender)", - "(f_c_plasma_diamagnetic_hender)", - current_drive_variables.f_c_plasma_diamagnetic_hender, - "OP ", - ) - po.ovarrf( - self.outfile, - "Diamagnetic fraction (SCENE)", - "(f_c_plasma_diamagnetic_scene)", - current_drive_variables.f_c_plasma_diamagnetic_scene, - "OP ", - ) - po.ovarrf( + po.ovarre( self.outfile, - "Pfirsch-Schlueter fraction (SCENE)", - "(f_c_plasma_pfirsch_schluter_scene)", - current_drive_variables.f_c_plasma_pfirsch_schluter_scene, + "Plasma resistive diffusion time (s)", + "(t_plasma_res_diffusion)", + physics_variables.t_plasma_res_diffusion, "OP ", ) - # Error to catch if bootstap fraction limit has been enforced - if physics_variables.err242 == 1: - logger.error("Bootstrap fraction upper limit enforced") - - # Error to catch if self-driven current fraction limit has been enforced - if physics_variables.err243 == 1: - logger.error( - "Predicted plasma driven current is more than upper limit on non-inductive fraction" - ) - - if physics_variables.i_bootstrap_current == 0: - po.ocmmnt(self.outfile, " (User-specified bootstrap current fraction used)") - elif physics_variables.i_bootstrap_current == 1: - po.ocmmnt( - self.outfile, " (ITER 1989 bootstrap current fraction model used)" - ) - elif physics_variables.i_bootstrap_current == 2: - po.ocmmnt( - self.outfile, - " (Nevins et al bootstrap current fraction model used)", - ) - elif physics_variables.i_bootstrap_current == 3: - po.ocmmnt(self.outfile, " (Wilson bootstrap current fraction model used)") - elif physics_variables.i_bootstrap_current == 4: - po.ocmmnt( - self.outfile, - " (Sauter et al bootstrap current fraction model used)", - ) - elif physics_variables.i_bootstrap_current == 5: - po.ocmmnt( - self.outfile, - " (Sakai et al bootstrap current fraction model used)", - ) - elif physics_variables.i_bootstrap_current == 6: - po.ocmmnt( - self.outfile, - " (ARIES bootstrap current fraction model used)", - ) - elif physics_variables.i_bootstrap_current == 7: - po.ocmmnt( - self.outfile, - " (Andrade et al bootstrap current fraction model used)", - ) - elif physics_variables.i_bootstrap_current == 8: - po.ocmmnt( - self.outfile, - " (Hoang et al bootstrap current fraction model used)", - ) - elif physics_variables.i_bootstrap_current == 9: - po.ocmmnt( - self.outfile, - " (Wong et al bootstrap current fraction model used)", - ) - elif physics_variables.i_bootstrap_current == 10: - po.ocmmnt( - self.outfile, - " (Gi-I et al bootstrap current fraction model used)", - ) - elif physics_variables.i_bootstrap_current == 11: - po.ocmmnt( - self.outfile, - " (Gi-II et al bootstrap current fraction model used)", - ) - - if physics_variables.i_diamagnetic_current == 0: - po.ocmmnt(self.outfile, " (Diamagnetic current fraction not calculated)") - # Error to show if diamagnetic current is above 1% but not used - if current_drive_variables.f_c_plasma_diamagnetic_scene > 0.01e0: - logger.error( - "Diamagnetic fraction is more than 1%, but not calculated. " - "Consider using i_diamagnetic_current=2 and i_pfirsch_schluter_current=1" - ) - - elif physics_variables.i_diamagnetic_current == 1: - po.ocmmnt( - self.outfile, " (Hender diamagnetic current fraction scaling used)" - ) - elif physics_variables.i_diamagnetic_current == 2: - po.ocmmnt( - self.outfile, " (SCENE diamagnetic current fraction scaling used)" - ) - - if physics_variables.i_pfirsch_schluter_current == 0: - po.ocmmnt(self.outfile, " Pfirsch-Schluter current fraction not calculated") - elif physics_variables.i_pfirsch_schluter_current == 1: - po.ocmmnt( - self.outfile, - " (SCENE Pfirsch-Schluter current fraction scaling used)", - ) - - po.ovarrf( + po.ovarre( self.outfile, - "Bootstrap fraction (enforced)", - "(f_c_plasma_bootstrap.)", - current_drive_variables.f_c_plasma_bootstrap, + "Plasma inductance (H)", + "(ind_plasma)", + physics_variables.ind_plasma, "OP ", ) - po.ovarrf( + po.ovarre( self.outfile, - "Diamagnetic fraction (enforced)", - "(f_c_plasma_diamagnetic.)", - current_drive_variables.f_c_plasma_diamagnetic, + "Plasma magnetic energy stored (J)", + "(e_plasma_magnetic_stored)", + physics_variables.e_plasma_magnetic_stored, "OP ", ) po.ovarrf( self.outfile, - "Pfirsch-Schlueter fraction (enforced)", - "(f_c_plasma_pfirsch_schluter.)", - current_drive_variables.f_c_plasma_pfirsch_schluter, + "Plasma normalised internal inductance", + "(ind_plasma_internal_norm)", + physics_variables.ind_plasma_internal_norm, "OP ", ) + po.oblnkl(self.outfile) + po.ostars(self.outfile, 110) + po.oblnkl(self.outfile) + class DetailedPhysics: """Class to hold detailed physics models for plasma processing.""" diff --git a/tests/unit/test_physics.py b/tests/unit/test_physics.py index 302f0c496..b2d40c290 100644 --- a/tests/unit/test_physics.py +++ b/tests/unit/test_physics.py @@ -11,6 +11,7 @@ impurity_radiation_module, physics_variables, ) +from process.models.physics.bootstrap_current import PlasmaBootstrapCurrent from process.models.physics.current_drive import ( CurrentDrive, ElectronBernstein, @@ -24,7 +25,6 @@ DetailedPhysics, Physics, PlasmaBeta, - PlasmaBootstrapCurrent, PlasmaInductance, calculate_current_coefficient_hastie, calculate_plasma_current_peng, diff --git a/tests/unit/test_stellarator.py b/tests/unit/test_stellarator.py index 7a9634a4f..df05369cc 100644 --- a/tests/unit/test_stellarator.py +++ b/tests/unit/test_stellarator.py @@ -21,6 +21,7 @@ from process.models.buildings import Buildings from process.models.costs.costs import Costs from process.models.fw import FirstWall +from process.models.physics.bootstrap_current import PlasmaBootstrapCurrent from process.models.physics.current_drive import ( CurrentDrive, ElectronBernstein, @@ -32,7 +33,6 @@ from process.models.physics.physics import ( Physics, PlasmaBeta, - PlasmaBootstrapCurrent, PlasmaInductance, ) from process.models.physics.plasma_profiles import PlasmaProfile From f17755a7d26fb65c302f4412802194d86904b3a2 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Thu, 26 Feb 2026 11:15:57 +0000 Subject: [PATCH 6/8] Create a running function for bootstrap models --- process/main.py | 4 +- process/models/physics/bootstrap_current.py | 179 ++++++++++++++++++- process/models/physics/physics.py | 182 +------------------- tests/unit/test_physics.py | 50 ++++-- tests/unit/test_stellarator.py | 4 +- 5 files changed, 220 insertions(+), 199 deletions(-) diff --git a/process/main.py b/process/main.py index d10c12cc5..23c835c26 100644 --- a/process/main.py +++ b/process/main.py @@ -699,7 +699,9 @@ def __init__(self): ) self.plasma_beta = PlasmaBeta() self.plasma_inductance = PlasmaInductance() - self.plasma_bootstrap = PlasmaBootstrapCurrent() + self.plasma_bootstrap = PlasmaBootstrapCurrent( + plasma_profile=self.plasma_profile + ) self.physics = Physics( plasma_profile=self.plasma_profile, current_drive=self.current_drive, diff --git a/process/models/physics/bootstrap_current.py b/process/models/physics/bootstrap_current.py index f15824f8f..967b59f8c 100644 --- a/process/models/physics/bootstrap_current.py +++ b/process/models/physics/bootstrap_current.py @@ -39,11 +39,188 @@ class BootstrapCurrentFractionModel(IntEnum): class PlasmaBootstrapCurrent: """Class to hold plasma bootstrap current for plasma processing.""" - def __init__(self): + def __init__(self, plasma_profile: PlasmaProfile) -> None: self.outfile = constants.NOUT self.mfile = constants.MFILE + self.plasma_profile = plasma_profile self.sauter_bootstrap = SauterBootstrapCurrent() + def run(self) -> None: + # Calculate bootstrap current fraction using various models + current_drive_variables.f_c_plasma_bootstrap_iter89 = ( + current_drive_variables.cboot + * self.bootstrap_fraction_iter89( + physics_variables.aspect, + physics_variables.beta_total_vol_avg, + physics_variables.b_plasma_total, + physics_variables.plasma_current, + physics_variables.q95, + physics_variables.q0, + physics_variables.rmajor, + physics_variables.vol_plasma, + ) + ) + + current_drive_variables.f_c_plasma_bootstrap_nevins = ( + current_drive_variables.cboot + * self.bootstrap_fraction_nevins( + physics_variables.alphan, + physics_variables.alphat, + physics_variables.beta_toroidal_vol_avg, + physics_variables.b_plasma_toroidal_on_axis, + physics_variables.nd_plasma_electrons_vol_avg, + physics_variables.plasma_current, + physics_variables.q95, + physics_variables.q0, + physics_variables.rmajor, + physics_variables.rminor, + physics_variables.temp_plasma_electron_vol_avg_kev, + physics_variables.n_charge_plasma_effective_vol_avg, + ) + ) + + # Wilson scaling uses thermal poloidal beta, not total + current_drive_variables.f_c_plasma_bootstrap_wilson = ( + current_drive_variables.cboot + * self.bootstrap_fraction_wilson( + physics_variables.alphaj, + physics_variables.alphap, + physics_variables.alphat, + physics_variables.beta_thermal_poloidal_vol_avg, + physics_variables.q0, + physics_variables.q95, + physics_variables.rmajor, + physics_variables.rminor, + ) + ) + + ( + current_drive_variables.f_c_plasma_bootstrap_sauter, + physics_variables.j_plasma_bootstrap_sauter_profile, + ) = self.bootstrap_fraction_sauter(self.plasma_profile) + current_drive_variables.f_c_plasma_bootstrap_sauter *= ( + current_drive_variables.cboot + ) + + current_drive_variables.f_c_plasma_bootstrap_sakai = ( + current_drive_variables.cboot + * self.bootstrap_fraction_sakai( + beta_poloidal=physics_variables.beta_poloidal_vol_avg, + q95=physics_variables.q95, + q0=physics_variables.q0, + alphan=physics_variables.alphan, + alphat=physics_variables.alphat, + eps=physics_variables.eps, + ind_plasma_internal_norm=physics_variables.ind_plasma_internal_norm, + ) + ) + + current_drive_variables.f_c_plasma_bootstrap_aries = ( + current_drive_variables.cboot + * self.bootstrap_fraction_aries( + beta_poloidal=physics_variables.beta_poloidal_vol_avg, + ind_plasma_internal_norm=physics_variables.ind_plasma_internal_norm, + core_density=physics_variables.nd_plasma_electron_on_axis, + average_density=physics_variables.nd_plasma_electrons_vol_avg, + inverse_aspect=physics_variables.eps, + ) + ) + + current_drive_variables.f_c_plasma_bootstrap_andrade = ( + current_drive_variables.cboot + * self.bootstrap_fraction_andrade( + beta_poloidal=physics_variables.beta_poloidal_vol_avg, + core_pressure=physics_variables.pres_plasma_thermal_on_axis, + average_pressure=physics_variables.pres_plasma_thermal_vol_avg, + inverse_aspect=physics_variables.eps, + ) + ) + current_drive_variables.f_c_plasma_bootstrap_hoang = ( + current_drive_variables.cboot + * self.bootstrap_fraction_hoang( + beta_poloidal=physics_variables.beta_poloidal_vol_avg, + pressure_index=physics_variables.alphap, + current_index=physics_variables.alphaj, + inverse_aspect=physics_variables.eps, + ) + ) + current_drive_variables.f_c_plasma_bootstrap_wong = ( + current_drive_variables.cboot + * self.bootstrap_fraction_wong( + beta_poloidal=physics_variables.beta_poloidal_vol_avg, + density_index=physics_variables.alphan, + temperature_index=physics_variables.alphat, + inverse_aspect=physics_variables.eps, + elongation=physics_variables.kappa, + ) + ) + current_drive_variables.bscf_gi_i = ( + current_drive_variables.cboot + * self.bootstrap_fraction_gi_I( + beta_poloidal=physics_variables.beta_poloidal_vol_avg, + pressure_index=physics_variables.alphap, + temperature_index=physics_variables.alphat, + inverse_aspect=physics_variables.eps, + effective_charge=physics_variables.n_charge_plasma_effective_vol_avg, + q95=physics_variables.q95, + q0=physics_variables.q0, + ) + ) + + current_drive_variables.bscf_gi_ii = ( + current_drive_variables.cboot + * self.bootstrap_fraction_gi_II( + beta_poloidal=physics_variables.beta_poloidal_vol_avg, + pressure_index=physics_variables.alphap, + temperature_index=physics_variables.alphat, + inverse_aspect=physics_variables.eps, + effective_charge=physics_variables.n_charge_plasma_effective_vol_avg, + ) + ) + current_drive_variables.f_c_plasma_bootstrap_sugiyama_l = ( + current_drive_variables.cboot + * self.bootstrap_fraction_sugiyama_l_mode( + eps=physics_variables.eps, + beta_poloidal=physics_variables.beta_poloidal_vol_avg, + alphan=physics_variables.alphan, + alphat=physics_variables.alphat, + zeff=physics_variables.n_charge_plasma_effective_vol_avg, + q95=physics_variables.q95, + q0=physics_variables.q0, + ) + ) + current_drive_variables.f_c_plasma_bootstrap_sugiyama_h = ( + current_drive_variables.cboot + * self.bootstrap_fraction_sugiyama_h_mode( + eps=physics_variables.eps, + beta_poloidal=physics_variables.beta_poloidal_vol_avg, + alphan=physics_variables.alphan, + alphat=physics_variables.alphat, + tbeta=physics_variables.tbeta, + zeff=physics_variables.n_charge_plasma_effective_vol_avg, + q95=physics_variables.q95, + q0=physics_variables.q0, + radius_plasma_pedestal_density_norm=physics_variables.radius_plasma_pedestal_density_norm, + nd_plasma_pedestal_electron=physics_variables.nd_plasma_pedestal_electron, + n_greenwald=physics_variables.nd_plasma_electron_max_array[6], + temp_plasma_pedestal_kev=physics_variables.temp_plasma_pedestal_kev, + ) + ) + + # Calculate beta_norm_max based on i_beta_norm_max + try: + model = BootstrapCurrentFractionModel( + int(physics_variables.i_bootstrap_current) + ) + current_drive_variables.f_c_plasma_bootstrap = ( + self.get_bootstrap_current_fraction_value(model) + ) + except ValueError: + raise ProcessValueError( + "Illegal value of i_bootstrap_current", + i_bootstrap_current=physics_variables.i_bootstrap_current, + ) from None + def get_bootstrap_current_fraction_value( self, model: BootstrapCurrentFractionModel ) -> float: diff --git a/process/models/physics/physics.py b/process/models/physics/physics.py index 1c3391c59..922874450 100644 --- a/process/models/physics/physics.py +++ b/process/models/physics/physics.py @@ -28,9 +28,6 @@ stellarator_variables, times_variables, ) -from process.models.physics.bootstrap_current import ( - BootstrapCurrentFractionModel, -) logger = logging.getLogger(__name__) @@ -932,184 +929,7 @@ def physics(self): current_drive_variables.f_c_plasma_pfirsch_schluter_scene ) - # ***************************** # - # BOOTSTRAP CURRENT # - # ***************************** # - - # Calculate bootstrap current fraction using various models - current_drive_variables.f_c_plasma_bootstrap_iter89 = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_iter89( - physics_variables.aspect, - physics_variables.beta_total_vol_avg, - physics_variables.b_plasma_total, - physics_variables.plasma_current, - physics_variables.q95, - physics_variables.q0, - physics_variables.rmajor, - physics_variables.vol_plasma, - ) - ) - - current_drive_variables.f_c_plasma_bootstrap_nevins = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_nevins( - physics_variables.alphan, - physics_variables.alphat, - physics_variables.beta_toroidal_vol_avg, - physics_variables.b_plasma_toroidal_on_axis, - physics_variables.nd_plasma_electrons_vol_avg, - physics_variables.plasma_current, - physics_variables.q95, - physics_variables.q0, - physics_variables.rmajor, - physics_variables.rminor, - physics_variables.temp_plasma_electron_vol_avg_kev, - physics_variables.n_charge_plasma_effective_vol_avg, - ) - ) - - # Wilson scaling uses thermal poloidal beta, not total - current_drive_variables.f_c_plasma_bootstrap_wilson = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_wilson( - physics_variables.alphaj, - physics_variables.alphap, - physics_variables.alphat, - physics_variables.beta_thermal_poloidal_vol_avg, - physics_variables.q0, - physics_variables.q95, - physics_variables.rmajor, - physics_variables.rminor, - ) - ) - - ( - current_drive_variables.f_c_plasma_bootstrap_sauter, - physics_variables.j_plasma_bootstrap_sauter_profile, - ) = self.bootstrap.bootstrap_fraction_sauter(self.plasma_profile) - current_drive_variables.f_c_plasma_bootstrap_sauter *= ( - current_drive_variables.cboot - ) - - current_drive_variables.f_c_plasma_bootstrap_sakai = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_sakai( - beta_poloidal=physics_variables.beta_poloidal_vol_avg, - q95=physics_variables.q95, - q0=physics_variables.q0, - alphan=physics_variables.alphan, - alphat=physics_variables.alphat, - eps=physics_variables.eps, - ind_plasma_internal_norm=physics_variables.ind_plasma_internal_norm, - ) - ) - - current_drive_variables.f_c_plasma_bootstrap_aries = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_aries( - beta_poloidal=physics_variables.beta_poloidal_vol_avg, - ind_plasma_internal_norm=physics_variables.ind_plasma_internal_norm, - core_density=physics_variables.nd_plasma_electron_on_axis, - average_density=physics_variables.nd_plasma_electrons_vol_avg, - inverse_aspect=physics_variables.eps, - ) - ) - - current_drive_variables.f_c_plasma_bootstrap_andrade = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_andrade( - beta_poloidal=physics_variables.beta_poloidal_vol_avg, - core_pressure=physics_variables.pres_plasma_thermal_on_axis, - average_pressure=physics_variables.pres_plasma_thermal_vol_avg, - inverse_aspect=physics_variables.eps, - ) - ) - current_drive_variables.f_c_plasma_bootstrap_hoang = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_hoang( - beta_poloidal=physics_variables.beta_poloidal_vol_avg, - pressure_index=physics_variables.alphap, - current_index=physics_variables.alphaj, - inverse_aspect=physics_variables.eps, - ) - ) - current_drive_variables.f_c_plasma_bootstrap_wong = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_wong( - beta_poloidal=physics_variables.beta_poloidal_vol_avg, - density_index=physics_variables.alphan, - temperature_index=physics_variables.alphat, - inverse_aspect=physics_variables.eps, - elongation=physics_variables.kappa, - ) - ) - current_drive_variables.bscf_gi_i = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_gi_I( - beta_poloidal=physics_variables.beta_poloidal_vol_avg, - pressure_index=physics_variables.alphap, - temperature_index=physics_variables.alphat, - inverse_aspect=physics_variables.eps, - effective_charge=physics_variables.n_charge_plasma_effective_vol_avg, - q95=physics_variables.q95, - q0=physics_variables.q0, - ) - ) - - current_drive_variables.bscf_gi_ii = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_gi_II( - beta_poloidal=physics_variables.beta_poloidal_vol_avg, - pressure_index=physics_variables.alphap, - temperature_index=physics_variables.alphat, - inverse_aspect=physics_variables.eps, - effective_charge=physics_variables.n_charge_plasma_effective_vol_avg, - ) - ) - current_drive_variables.f_c_plasma_bootstrap_sugiyama_l = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_sugiyama_l_mode( - eps=physics_variables.eps, - beta_poloidal=physics_variables.beta_poloidal_vol_avg, - alphan=physics_variables.alphan, - alphat=physics_variables.alphat, - zeff=physics_variables.n_charge_plasma_effective_vol_avg, - q95=physics_variables.q95, - q0=physics_variables.q0, - ) - ) - current_drive_variables.f_c_plasma_bootstrap_sugiyama_h = ( - current_drive_variables.cboot - * self.bootstrap.bootstrap_fraction_sugiyama_h_mode( - eps=physics_variables.eps, - beta_poloidal=physics_variables.beta_poloidal_vol_avg, - alphan=physics_variables.alphan, - alphat=physics_variables.alphat, - tbeta=physics_variables.tbeta, - zeff=physics_variables.n_charge_plasma_effective_vol_avg, - q95=physics_variables.q95, - q0=physics_variables.q0, - radius_plasma_pedestal_density_norm=physics_variables.radius_plasma_pedestal_density_norm, - nd_plasma_pedestal_electron=physics_variables.nd_plasma_pedestal_electron, - n_greenwald=physics_variables.nd_plasma_electron_max_array[6], - temp_plasma_pedestal_kev=physics_variables.temp_plasma_pedestal_kev, - ) - ) - - # Calculate beta_norm_max based on i_beta_norm_max - try: - model = BootstrapCurrentFractionModel( - int(physics_variables.i_bootstrap_current) - ) - current_drive_variables.f_c_plasma_bootstrap = ( - self.bootstrap.get_bootstrap_current_fraction_value(model) - ) - except ValueError: - raise ProcessValueError( - "Illegal value of i_bootstrap_current", - i_bootstrap_current=physics_variables.i_bootstrap_current, - ) from None + self.bootstrap.run() physics_variables.err242 = 0 if ( diff --git a/tests/unit/test_physics.py b/tests/unit/test_physics.py index b2d40c290..83929b90a 100644 --- a/tests/unit/test_physics.py +++ b/tests/unit/test_physics.py @@ -57,7 +57,7 @@ def physics(): ), PlasmaBeta(), PlasmaInductance(), - PlasmaBootstrapCurrent(), + PlasmaBootstrapCurrent(plasma_profile=PlasmaProfile()), ) @@ -145,7 +145,9 @@ def test_bootstrap_fraction_iter89(bootstrapfractioniter89param, physics): :type monkeypatch: _pytest.monkeypatch.monkeypatch """ - f_c_plasma_bootstrap = PlasmaBootstrapCurrent().bootstrap_fraction_iter89( + f_c_plasma_bootstrap = PlasmaBootstrapCurrent( + plasma_profile=physics.plasma_profile + ).bootstrap_fraction_iter89( aspect=bootstrapfractioniter89param.aspect, beta=bootstrapfractioniter89param.beta, b_plasma_toroidal_on_axis=bootstrapfractioniter89param.b_plasma_toroidal_on_axis, @@ -240,7 +242,9 @@ def test_bootstrap_fraction_nevins(bootstrapfractionnevinsparam, monkeypatch, ph bootstrapfractionnevinsparam.nd_plasma_electron_on_axis, ) - fibs = PlasmaBootstrapCurrent().bootstrap_fraction_nevins( + fibs = PlasmaBootstrapCurrent( + plasma_profile=physics.plasma_profile + ).bootstrap_fraction_nevins( alphan=bootstrapfractionnevinsparam.alphan, alphat=bootstrapfractionnevinsparam.alphat, beta_toroidal=bootstrapfractionnevinsparam.beta_toroidal, @@ -318,7 +322,9 @@ def test_bootstrap_fraction_wilson(bootstrapfractionwilsonparam, physics): :type monkeypatch: _pytest.monkeypatch.monkeypatch """ - bfw = PlasmaBootstrapCurrent().bootstrap_fraction_wilson( + bfw = PlasmaBootstrapCurrent( + plasma_profile=physics.plasma_profile + ).bootstrap_fraction_wilson( alphaj=bootstrapfractionwilsonparam.alphaj, alphap=bootstrapfractionwilsonparam.alphap, alphat=bootstrapfractionwilsonparam.alphat, @@ -557,7 +563,9 @@ def test_bootstrap_fraction_sauter(bootstrapfractionsauterparam, monkeypatch, ph monkeypatch.setattr(physics_variables, "alphat", bootstrapfractionsauterparam.alphat) physics.plasma_profile.run() - bfs, _ = PlasmaBootstrapCurrent().bootstrap_fraction_sauter(physics.plasma_profile) + bfs, _ = PlasmaBootstrapCurrent( + plasma_profile=physics.plasma_profile + ).bootstrap_fraction_sauter(physics.plasma_profile) assert bfs == pytest.approx(bootstrapfractionsauterparam.expected_bfs) @@ -641,7 +649,9 @@ def test_bootstrap_fraction_sakai(bootstrapfractionsakaiparam, monkeypatch, phys bootstrapfractionsakaiparam.ind_plasma_internal_norm, ) - bfs = PlasmaBootstrapCurrent().bootstrap_fraction_sakai( + bfs = PlasmaBootstrapCurrent( + plasma_profile=PlasmaProfile() + ).bootstrap_fraction_sakai( beta_poloidal=bootstrapfractionsakaiparam.beta_poloidal, q95=bootstrapfractionsakaiparam.q95, q0=bootstrapfractionsakaiparam.q0, @@ -691,7 +701,9 @@ def test_bootstrap_fraction_aries(bootstrapfractionariesparam, physics): :type bootstrapfractionsauterparam: bootstrapfractionsauterparam """ - bfs = PlasmaBootstrapCurrent().bootstrap_fraction_aries( + bfs = PlasmaBootstrapCurrent( + plasma_profile=PlasmaProfile() + ).bootstrap_fraction_aries( beta_poloidal=bootstrapfractionariesparam.beta_poloidal, ind_plasma_internal_norm=bootstrapfractionariesparam.ind_plasma_internal_norm, core_density=bootstrapfractionariesparam.core_density, @@ -736,7 +748,9 @@ def test_bootstrap_fraction_andrade(bootstrapfractionandradeparam, physics): :type bootstrapfractionsauterparam: bootstrapfractionsauterparam """ - bfs = PlasmaBootstrapCurrent().bootstrap_fraction_andrade( + bfs = PlasmaBootstrapCurrent( + plasma_profile=PlasmaProfile() + ).bootstrap_fraction_andrade( beta_poloidal=bootstrapfractionandradeparam.beta_poloidal, core_pressure=bootstrapfractionandradeparam.core_pressure, average_pressure=bootstrapfractionandradeparam.average_pressure, @@ -780,7 +794,9 @@ def test_bootstrap_fraction_hoang(bootstrapfractionhoangparam, physics): :type bootstrapfractionsauterparam: bootstrapfractionsauterparam """ - bfs = PlasmaBootstrapCurrent().bootstrap_fraction_hoang( + bfs = PlasmaBootstrapCurrent( + plasma_profile=PlasmaProfile() + ).bootstrap_fraction_hoang( beta_poloidal=bootstrapfractionhoangparam.beta_poloidal, pressure_index=bootstrapfractionhoangparam.pressure_index, current_index=bootstrapfractionhoangparam.current_index, @@ -827,7 +843,7 @@ def test_bootstrap_fraction_wong(bootstrapfractionwongparam, physics): :type bootstrapfractionsauterparam: bootstrapfractionsauterparam """ - bfs = PlasmaBootstrapCurrent().bootstrap_fraction_wong( + bfs = PlasmaBootstrapCurrent(plasma_profile=PlasmaProfile()).bootstrap_fraction_wong( beta_poloidal=bootstrapfractionwongparam.beta_poloidal, density_index=bootstrapfractionwongparam.density_index, temperature_index=bootstrapfractionwongparam.temperature_index, @@ -881,7 +897,7 @@ def test_bootstrap_fraction_gi_I(bootstrapfractiongiiparam, physics): # noqa: N :type bootstrapfractionsauterparam: bootstrapfractionsauterparam """ - bfs = PlasmaBootstrapCurrent().bootstrap_fraction_gi_I( + bfs = PlasmaBootstrapCurrent(plasma_profile=PlasmaProfile()).bootstrap_fraction_gi_I( beta_poloidal=bootstrapfractiongiiparam.beta_poloidal, pressure_index=bootstrapfractiongiiparam.pressure_index, temperature_index=bootstrapfractiongiiparam.temperature_index, @@ -931,7 +947,9 @@ def test_bootstrap_fraction_gi_II(bootstrapfractiongiiiparam, physics): # noqa: :type bootstrapfractionsauterparam: bootstrapfractionsauterparam """ - bfs = PlasmaBootstrapCurrent().bootstrap_fraction_gi_II( + bfs = PlasmaBootstrapCurrent( + plasma_profile=PlasmaProfile() + ).bootstrap_fraction_gi_II( beta_poloidal=bootstrapfractiongiiiparam.beta_poloidal, pressure_index=bootstrapfractiongiiiparam.pressure_index, temperature_index=bootstrapfractiongiiiparam.temperature_index, @@ -988,7 +1006,9 @@ def test_bootstrap_fraction_sugiyama_l_mode(bootstrapfractionsugiyamalparam, phy :param bootstrapfractionsugiyamalparam: Parameters for the test case. :type bootstrapfractionsugiyamalparam: BootstrapFractionSugiyamaLModeParam """ - bfs = PlasmaBootstrapCurrent().bootstrap_fraction_sugiyama_l_mode( + bfs = PlasmaBootstrapCurrent( + plasma_profile=PlasmaProfile() + ).bootstrap_fraction_sugiyama_l_mode( eps=bootstrapfractionsugiyamalparam.eps, beta_poloidal=bootstrapfractionsugiyamalparam.beta_poloidal, alphan=bootstrapfractionsugiyamalparam.alphan, @@ -1094,7 +1114,9 @@ def test_bootstrap_fraction_sugiyama_h_mode(bootstrapfractionsugiyamahparam, phy :param bootstrapfractionsugiyamahparam: Parameters for the test case. :type bootstrapfractionsugiyamahparam: BootstrapFractionSugiyamaHModeParam """ - bfs = PlasmaBootstrapCurrent().bootstrap_fraction_sugiyama_h_mode( + bfs = PlasmaBootstrapCurrent( + plasma_profile=PlasmaProfile() + ).bootstrap_fraction_sugiyama_h_mode( eps=bootstrapfractionsugiyamahparam.eps, beta_poloidal=bootstrapfractionsugiyamahparam.beta_poloidal, alphan=bootstrapfractionsugiyamahparam.alphan, diff --git a/tests/unit/test_stellarator.py b/tests/unit/test_stellarator.py index df05369cc..dec4b1165 100644 --- a/tests/unit/test_stellarator.py +++ b/tests/unit/test_stellarator.py @@ -87,11 +87,11 @@ def stellarator(): ), PlasmaBeta(), PlasmaInductance(), - PlasmaBootstrapCurrent(), + PlasmaBootstrapCurrent(plasma_profile=PlasmaProfile()), ), Neoclassics(), plasma_beta=PlasmaBeta(), - plasma_bootstrap=PlasmaBootstrapCurrent(), + plasma_bootstrap=PlasmaBootstrapCurrent(plasma_profile=PlasmaProfile()), ) From 06925e9b51c6031cefefa5373f28bb3b675e7273 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Thu, 26 Feb 2026 11:46:04 +0000 Subject: [PATCH 7/8] Refactor PlasmaBootstrapCurrent methods to improve parameter naming and update docstrings for clarity --- process/models/physics/bootstrap_current.py | 1460 ++++++++++--------- 1 file changed, 803 insertions(+), 657 deletions(-) diff --git a/process/models/physics/bootstrap_current.py b/process/models/physics/bootstrap_current.py index 967b59f8c..8717a2266 100644 --- a/process/models/physics/bootstrap_current.py +++ b/process/models/physics/bootstrap_current.py @@ -50,32 +50,32 @@ def run(self) -> None: current_drive_variables.f_c_plasma_bootstrap_iter89 = ( current_drive_variables.cboot * self.bootstrap_fraction_iter89( - physics_variables.aspect, - physics_variables.beta_total_vol_avg, - physics_variables.b_plasma_total, - physics_variables.plasma_current, - physics_variables.q95, - physics_variables.q0, - physics_variables.rmajor, - physics_variables.vol_plasma, + aspect=physics_variables.aspect, + beta=physics_variables.beta_total_vol_avg, + b_plasma_toroidal_on_axis=physics_variables.b_plasma_total, + plasma_current=physics_variables.plasma_current, + q95=physics_variables.q95, + q0=physics_variables.q0, + rmajor=physics_variables.rmajor, + vol_plasma=physics_variables.vol_plasma, ) ) current_drive_variables.f_c_plasma_bootstrap_nevins = ( current_drive_variables.cboot * self.bootstrap_fraction_nevins( - physics_variables.alphan, - physics_variables.alphat, - physics_variables.beta_toroidal_vol_avg, - physics_variables.b_plasma_toroidal_on_axis, - physics_variables.nd_plasma_electrons_vol_avg, - physics_variables.plasma_current, - physics_variables.q95, - physics_variables.q0, - physics_variables.rmajor, - physics_variables.rminor, - physics_variables.temp_plasma_electron_vol_avg_kev, - physics_variables.n_charge_plasma_effective_vol_avg, + alphan=physics_variables.alphan, + alphat=physics_variables.alphat, + beta_toroidal=physics_variables.beta_toroidal_vol_avg, + b_plasma_toroidal_on_axis=physics_variables.b_plasma_toroidal_on_axis, + nd_plasma_electrons_vol_avg=physics_variables.nd_plasma_electrons_vol_avg, + plasma_current=physics_variables.plasma_current, + q95=physics_variables.q95, + q0=physics_variables.q0, + rmajor=physics_variables.rmajor, + rminor=physics_variables.rminor, + te=physics_variables.temp_plasma_electron_vol_avg_kev, + zeff=physics_variables.n_charge_plasma_effective_vol_avg, ) ) @@ -83,14 +83,14 @@ def run(self) -> None: current_drive_variables.f_c_plasma_bootstrap_wilson = ( current_drive_variables.cboot * self.bootstrap_fraction_wilson( - physics_variables.alphaj, - physics_variables.alphap, - physics_variables.alphat, - physics_variables.beta_thermal_poloidal_vol_avg, - physics_variables.q0, - physics_variables.q95, - physics_variables.rmajor, - physics_variables.rminor, + alphaj=physics_variables.alphaj, + alphap=physics_variables.alphap, + alphat=physics_variables.alphat, + betpth=physics_variables.beta_thermal_poloidal_vol_avg, + q0=physics_variables.q0, + q95=physics_variables.q95, + rmajor=physics_variables.rmajor, + rminor=physics_variables.rminor, ) ) @@ -224,7 +224,18 @@ def run(self) -> None: def get_bootstrap_current_fraction_value( self, model: BootstrapCurrentFractionModel ) -> float: - """Get the plasma current bootstrap fraction (f_BS) for the specified model.""" + """Get the plasma current bootstrap fraction (f_BS) for the specified model. + + Parameters + ---------- + model : BootstrapCurrentFractionModel + The bootstrap current model type. + + Returns + ------- + float + The bootstrap current fraction value. + """ model_map = { BootstrapCurrentFractionModel.USER_INPUT: current_drive_variables.f_c_plasma_bootstrap, BootstrapCurrentFractionModel.ITER_89: current_drive_variables.f_c_plasma_bootstrap_iter89, @@ -254,27 +265,40 @@ def bootstrap_fraction_iter89( rmajor: float, vol_plasma: float, ) -> float: - """ - Calculate the bootstrap-driven fraction of the plasma current. + """Calculate the bootstrap-driven fraction of the plasma current. - Args: - aspect (float): Plasma aspect ratio. - beta (float): Plasma total beta. - b_plasma_toroidal_on_axis (float): Toroidal field on axis (T). - plasma_current (float): Plasma current (A). - q95 (float): Safety factor at 95% surface. - q0 (float): Central safety factor. - rmajor (float): Plasma major radius (m). - vol_plasma (float): Plasma volume (m3). + Parameters + ---------- + aspect : float + Plasma aspect ratio. + beta : float + Plasma total beta. + b_plasma_toroidal_on_axis : float + Toroidal field on axis (T). + plasma_current : float + Plasma current (A). + q95 : float + Safety factor at 95% surface. + q0 : float + Central safety factor. + rmajor : float + Plasma major radius (m). + vol_plasma : float + Plasma volume (m³). - Returns: - float: The bootstrap-driven fraction of the plasma current. + Returns + ------- + float + The bootstrap-driven fraction of the plasma current. + Notes + ----- This function performs the original ITER calculation of the plasma current bootstrap fraction. - Reference: - - ITER Physics Design Guidelines: 1989 [IPDG89], N. A. Uckan et al, - - ITER Documentation Series No.10, IAEA/ITER/DS/10, IAEA, Vienna, 1990 + References + ---------- + ITER Physics Design Guidelines: 1989 [IPDG89], N. A. Uckan et al, + ITER Documentation Series No.10, IAEA/ITER/DS/10, IAEA, Vienna, 1990 """ # Calculate the bootstrap current coefficient @@ -306,27 +330,42 @@ def bootstrap_fraction_wilson( rmajor: float, rminor: float, ) -> float: - """ - Bootstrap current fraction from Wilson et al scaling - - Args: - alphaj (float): Current profile index. - alphap (float): Pressure profile index. - alphat (float): Temperature profile index. - betpth (float): Thermal component of poloidal beta. - q0 (float): Safety factor on axis. - q95 (float): Edge safety factor. - rmajor (float): Major radius (m). - rminor (float): Minor radius (m). - - Returns: - float: The bootstrap current fraction. - - This function calculates the bootstrap current fraction using the numerically fitted algorithm written by Howard Wilson. - - Reference: - - AEA FUS 172: Physics Assessment for the European Reactor Study, 1989 - - H. R. Wilson, Nuclear Fusion 32 (1992) 257 + """Bootstrap current fraction from Wilson et al scaling. + + Parameters + ---------- + alphaj : float + Current profile index. + alphap : float + Pressure profile index. + alphat : float + Temperature profile index. + betpth : float + Thermal component of poloidal beta. + q0 : float + Safety factor on axis. + q95 : float + Edge safety factor. + rmajor : float + Major radius (m). + rminor : float + Minor radius (m). + + Returns + ------- + float + The bootstrap current fraction. + + Notes + ----- + This function calculates the bootstrap current fraction using the numerically fitted algorithm + written by Howard Wilson. + + References + ---------- + AEA FUS 172: Physics Assessment for the European Reactor Study, 1989 + + H. R. Wilson, Nuclear Fusion 32 (1992) 257 """ term1 = np.log(0.5) @@ -419,51 +458,47 @@ def _nevins_integral( ) -> float: """Integrand function for Nevins et al bootstrap current scaling. - This function calculates the integrand function for the Nevins et al bootstrap current scaling. - Parameters ---------- - y : - abscissa of integration, normalized minor radius - nd_plasma_electrons_vol_avg : - volume averaged electron density (/m^3) - te : - volume averaged electron temperature (keV) - b_plasma_toroidal_on_axis : - toroidal field on axis (T) - rminor : - plasma minor radius (m) - rmajor : - plasma major radius (m) - zeff : - plasma effective charge - alphat : - temperature profile index - alphan : - density profile index - q0 : - normalized safety factor at the magnetic axis - q95 : - normalized safety factor at 95% of the plasma radius - beta_toroidal : - Toroidal plasma beta - + y : float + Abscissa of integration, normalized minor radius. + nd_plasma_electrons_vol_avg : float + Volume averaged electron density (/m³). + te : float + Volume averaged electron temperature (keV). + b_plasma_toroidal_on_axis : float + Toroidal field on axis (T). + rminor : float + Plasma minor radius (m). + rmajor : float + Plasma major radius (m). + zeff : float + Plasma effective charge. + alphat : float + Temperature profile index. + alphan : float + Density profile index. + q0 : float + Normalized safety factor at the magnetic axis. + q95 : float + Normalized safety factor at 95% of the plasma radius. + beta_toroidal : float + Toroidal plasma beta. Returns ------- - type - - float, the integrand value - + float + The integrand value. - Reference: See appendix of: - Keii Gi, Makoto Nakamura, Kenji Tobita, Yasushi Ono, - Bootstrap current fraction scaling for a tokamak reactor design study, - Fusion Engineering and Design, Volume 89, Issue 11, 2014, Pages 2709-2715, - ISSN 0920-3796, https://doi.org/10.1016/j.fusengdes.2014.07.009. - - Nevins, W. M. "Summary report: ITER specialists' meeting on heating and current drive." - ITER-TN-PH-8-4, June 1988. 1988. + References + ---------- + Keii Gi, Makoto Nakamura, Kenji Tobita, Yasushi Ono, + "Bootstrap current fraction scaling for a tokamak reactor design study," + Fusion Engineering and Design, Volume 89, Issue 11, 2014, Pages 2709-2715, + ISSN 0920-3796, https://doi.org/10.1016/j.fusengdes.2014.07.009. + Nevins, W. M. "Summary report: ITER specialists' meeting on heating and current drive." + ITER-TN-PH-8-4, June 1988. """ # Compute average electron beta @@ -502,11 +537,16 @@ def bootstrap_fraction_sauter( ) -> tuple[float, np.ndarray]: """Get the bootstrap current fraction using Sauter et al scaling. - Args: - plasma_profile: The plasma profile object. + Parameters + ---------- + plasma_profile : PlasmaProfile + The plasma profile object containing the necessary plasma parameters for the Sauter + bootstrap calculation. - Returns: - tuple: (bootstrap fraction, bootstrap current density profile) + Returns + ------- + tuple[float, np.ndarray] + A tuple containing the bootstrap current fraction and the bootstrap current density profile. """ return self.sauter_bootstrap.bootstrap_fraction_sauter(plasma_profile) @@ -525,37 +565,52 @@ def bootstrap_fraction_nevins( te: float, zeff: float, ) -> float: - """ - Calculate the bootstrap current fraction from Nevins et al scaling. - - Args: - alphan (float): Density profile index. - alphat (float): Temperature profile index. - beta_toroidal (float): Toroidal plasma beta. - b_plasma_toroidal_on_axis (float): Toroidal field on axis (T). - nd_plasma_electrons_vol_avg (float): Electron density (/m3). - plasma_current (float): Plasma current (A). - q0 (float): Central safety factor. - q95 (float): Safety factor at 95% surface. - rmajor (float): Plasma major radius (m). - rminor (float): Plasma minor radius (m). - te (float): Volume averaged plasma temperature (keV). - zeff (float): Plasma effective charge. - - Returns: - float: The bootstrap current fraction. + """Calculate the bootstrap current fraction from Nevins et al scaling. + + Parameters + ---------- + alphan : float + Density profile index. + alphat : float + Temperature profile index. + beta_toroidal : float + Toroidal plasma beta. + b_plasma_toroidal_on_axis : float + Toroidal field on axis (T). + nd_plasma_electrons_vol_avg : float + Electron density (/m³). + plasma_current : float + Plasma current (A). + q95 : float + Safety factor at 95% surface. + q0 : float + Central safety factor. + rmajor : float + Plasma major radius (m). + rminor : float + Plasma minor radius (m). + te : float + Volume averaged plasma temperature (keV). + zeff : float + Plasma effective charge. + Returns + ------- + float + The bootstrap current fraction. + + Notes + ----- This function calculates the bootstrap current fraction using the Nevins et al method. - Reference: See appendix of: - Keii Gi, Makoto Nakamura, Kenji Tobita, Yasushi Ono, - Bootstrap current fraction scaling for a tokamak reactor design study, - Fusion Engineering and Design, Volume 89, Issue 11, 2014, Pages 2709-2715, - ISSN 0920-3796, https://doi.org/10.1016/j.fusengdes.2014.07.009. + References + ---------- + K. Gi, M. Nakamura, Kenji Tobita, and Y. Ono, "Bootstrap current fraction scaling for a tokamak reactor design study," + Fusion Engineering and Design, vol. 89, no. 11, pp. 2709-2715, Aug. 2014, + doi: https://doi.org/10.1016/j.fusengdes.2014.07.009. Nevins, W. M. "Summary report: ITER specialists' meeting on heating and current drive." - ITER-TN-PH-8-4, June 1988. 1988. - + ITER-TN-PH-8-4, June 1988. """ # Calculate peak electron beta at plasma centre, this is not the form used in the paper # The paper assumes parabolic profiles for calculating core values with the profile indexes. @@ -606,34 +661,46 @@ def bootstrap_fraction_sakai( eps: float, ind_plasma_internal_norm: float, ) -> float: - """ - Calculate the bootstrap fraction using the Sakai formula. - - Parameters: - beta_poloidal (float): Plasma poloidal beta. - q95 (float): Safety factor at 95% of the plasma radius. - q0 (float): Safety factor at the magnetic axis. - alphan (float): Density profile index - alphat (float): Temperature profile index - eps (float): Inverse aspect ratio. - ind_plasma_internal_norm (float): Plasma normalised internal inductance - - Returns: - float: The calculated bootstrap fraction. - - Notes: - The profile assumed for the alphan and alpat indexes is only a parabolic profile without a pedestal (L-mode). - The Root Mean Squared Error for the fitting database of this formula was 0.025 - Concentrating on the positive shear plasmas using the ACCOME code equilibria with the fully non-inductively driven - conditions with neutral beam (NB) injection only are calculated. - The electron temperature and the ion temperature were assumed to be equal - This can be used for all apsect ratios. - The diamagnetic fraction is included in this formula. - - References: - Ryosuke Sakai, Takaaki Fujita, Atsushi Okamoto, Derivation of bootstrap current fraction scaling formula for 0-D system code analysis, - Fusion Engineering and Design, Volume 149, 2019, 111322, ISSN 0920-3796, - https://doi.org/10.1016/j.fusengdes.2019.111322. + """Calculate the bootstrap fraction using the Sakai formula. + + Parameters + ---------- + beta_poloidal : float + Plasma poloidal beta. + q95 : float + Safety factor at 95% of the plasma radius. + q0 : float + Safety factor at the magnetic axis. + alphan : float + Density profile index. + alphat : float + Temperature profile index. + eps : float + Inverse aspect ratio. + ind_plasma_internal_norm : float + Plasma normalized internal inductance. + + Returns + ------- + float + The calculated bootstrap fraction. + + Notes + ----- + - The profile assumed for the alphan and alphat indexes is only a parabolic profile without a + pedestal (L-mode). + - The Root Mean Squared Error for the fitting database of this formula was 0.025. + - Concentrating on the positive shear plasmas using the ACCOME code equilibria with the fully + non-inductively driven conditions with neutral beam (NB) injection only are calculated. + - The electron temperature and the ion temperature were assumed to be equal. + - This can be used for all aspect ratios. + - The diamagnetic fraction is included in this formula. + + References + ---------- + R. Sakai, T. Fujita, and A. Okamoto, "Derivation of bootstrap current fraction scaling formula for + 0-D system code analysis," Fusion Engineering and Design, vol. 149, p. 111322, Dec. 2019, + doi: https://doi.org/10.1016/j.fusengdes.2019.111322. """ # Sakai states that the ACCOME dataset used has the toridal diamagnetic current included in the bootstrap current # So the diamganetic current should not be calculated with this. i_diamagnetic_current = 0 @@ -654,27 +721,36 @@ def bootstrap_fraction_aries( average_density: float, inverse_aspect: float, ) -> float: - """ - Calculate the bootstrap fraction using the ARIES formula. - - Parameters: - beta_poloidal (float): Plasma poloidal beta. - ind_plasma_internal_norm (float): Plasma normalized internal inductance. - core_density (float): Core plasma density. - average_density (float): Average plasma density. - inverse_aspect (float): Inverse aspect ratio. + """Calculate the bootstrap fraction using the ARIES formula. - Returns: - float: The calculated bootstrap fraction. + Parameters + ---------- + beta_poloidal : float + Plasma poloidal beta. + ind_plasma_internal_norm : float + Plasma normalized internal inductance. + core_density : float + Core plasma density. + average_density : float + Average plasma density. + inverse_aspect : float + Inverse aspect ratio. - Notes: - - The source reference does not provide any info about the derivation of the formula. It is only stated + Returns + ------- + float + The calculated bootstrap fraction. - References: - - Zoran Dragojlovic et al., “An advanced computational algorithm for systems analysis of tokamak power plants,” - Fusion Engineering and Design, vol. 85, no. 2, pp. 243-265, Apr. 2010, - doi: https://doi.org/10.1016/j.fusengdes.2010.02.015. + Notes + ----- + The source reference does not provide any info about the derivation of the formula. + It is only stated. + References + ---------- + Zoran Dragojlovic et al., "An advanced computational algorithm for systems analysis of tokamak power plants," + Fusion Engineering and Design, vol. 85, no. 2, pp. 243-265, Apr. 2010, + doi: https://doi.org/10.1016/j.fusengdes.2010.02.015. """ # Using the standard variable naming from the ARIES paper a_1 = ( @@ -697,29 +773,37 @@ def bootstrap_fraction_andrade( average_pressure: float, inverse_aspect: float, ) -> float: - """ - Calculate the bootstrap fraction using the Andrade et al formula. - - Parameters: - beta_poloidal (float): Plasma poloidal beta. - core_pressure (float): Core plasma pressure. - average_pressure (float): Average plasma pressure. - inverse_aspect (float): Inverse aspect ratio. + """Calculate the bootstrap fraction using the Andrade et al formula. - Returns: - float: The calculated bootstrap fraction. + Parameters + ---------- + beta_poloidal : float + Plasma poloidal beta. + core_pressure : float + Core plasma pressure. + average_pressure : float + Average plasma pressure. + inverse_aspect : float + Inverse aspect ratio. - Notes: - - Based off 350 plasma profiles from Experimento Tokamak Esferico (ETE) spherical tokamak - - A = 1.5, R_0 = 0.3m, I_p = 200kA, B_0=0.4T, beta = 4-10%. Profiles taken as Gaussian shaped functions. - - Errors mostly up to the order of 10% are obtained when both expressions are compared with the equilibrium estimates for the - bootstrap current in ETE + Returns + ------- + float + The calculated bootstrap fraction. - References: - - M. C. R. Andrade and G. O. Ludwig, “Scaling of bootstrap current on equilibrium and plasma profile parameters in tokamak plasmas,” - Plasma Physics and Controlled Fusion, vol. 50, no. 6, pp. 065001-065001, Apr. 2008, - doi: https://doi.org/10.1088/0741-3335/50/6/065001. + Notes + ----- + - Based off 350 plasma profiles from Experimento Tokamak Esferico (ETE) spherical tokamak + with A = 1.5, R_0 = 0.3m, I_p = 200kA, B_0=0.4T, beta = 4-10%. + - Profiles taken as Gaussian shaped functions. + - Errors mostly up to the order of 10% are obtained when both expressions are compared with + the equilibrium estimates for the bootstrap current in ETE. + References + ---------- + M. C. R. Andrade and G. O. Ludwig, "Scaling of bootstrap current on equilibrium and plasma profile parameters in tokamak plasmas," + Plasma Physics and Controlled Fusion, vol. 50, no. 6, pp. 065001-065001, Apr. 2008, + doi: https://doi.org/10.1088/0741-3335/50/6/065001. """ # Using the standard variable naming from the Andrade et.al. paper @@ -737,29 +821,37 @@ def bootstrap_fraction_hoang( current_index: float, inverse_aspect: float, ) -> float: - """ - Calculate the bootstrap fraction using the Hoang et al formula. - - Parameters: - beta_poloidal (float): Plasma poloidal beta. - pressure_index (float): Pressure profile index. - current_index (float): Current profile index. - inverse_aspect (float): Inverse aspect ratio. - - Returns: - float: The calculated bootstrap fraction. - - Notes: - - Based off of TFTR data calculated using the TRANSP plasma analysis code - - 170 discharges which was assembled to study the tritium influx and transport in discharges with D-only neutral beam - injection (NBI) - - Contains L-mode, supershots, reversed shear, enhanced reversed shear and increased li discharges - - Discharges with monotonic flux profiles with reversed shear are also included - - Is applied to circular cross-section plasmas - - References: - - G. T. Hoang and R. V. Budny, “The bootstrap fraction in TFTR,” AIP conference proceedings, - Jan. 1997, doi: https://doi.org/10.1063/1.53414. + """Calculate the bootstrap fraction using the Hoang et al formula. + + Parameters + ---------- + beta_poloidal : float + Plasma poloidal beta. + pressure_index : float + Pressure profile index. + current_index : float + Current profile index. + inverse_aspect : float + Inverse aspect ratio. + + Returns + ------- + float + The calculated bootstrap fraction. + + Notes + ----- + - Based off of TFTR data calculated using the TRANSP plasma analysis code. + - 170 discharges which was assembled to study the tritium influx and transport in discharges + with D-only neutral beam injection (NBI). + - Contains L-mode, supershots, reversed shear, enhanced reversed shear and increased li discharges. + - Discharges with monotonic flux profiles with reversed shear are also included. + - Is applied to circular cross-section plasmas. + + References + ---------- + G. T. Hoang and R. V. Budny, "The bootstrap fraction in TFTR," AIP conference proceedings, + Jan. 1997, doi: https://doi.org/10.1063/1.53414. """ # Using the standard variable naming from the Hoang et.al. paper @@ -784,33 +876,44 @@ def bootstrap_fraction_wong( inverse_aspect: float, elongation: float, ) -> float: - """ - Calculate the bootstrap fraction using the Wong et al formula. - - Parameters: - beta_poloidal (float): Plasma poloidal beta. - density_index (float): Density profile index. - temperature_index (float): Temperature profile index. - inverse_aspect (float): Inverse aspect ratio. - elongation (float): Plasma elongation. - - Returns: - float: The calculated bootstrap fraction. - - Notes: - - Data is based off of equilibria from Miller et al. - - A: 1.2 - 3.0 and stable to n ballooning and low n kink modes at a bootstrap fraction of 99% for kappa = 2, 2.5 and 3 - - The results were parameterized as a function of aspect ratio and elongation - - The parametric dependency of beta_p and beta_T are based on fitting of the DIII-D high equivalent DT yield results - - Parabolic profiles should be used for best results as the pressure peaking value is calculated as the product of a parabolic - temperature and density profile - - References: - - C.-P. Wong, J. C. Wesley, R. D. Stambaugh, and E. T. Cheng, “Toroidal reactor designs as a function of aspect ratio and elongation,” - vol. 42, no. 5, pp. 547-556, May 2002, doi: https://doi.org/10.1088/0029-5515/42/5/307. - - - Miller, R L, "Stable bootstrap-current driven equilibria for low aspect ratio tokamaks". - Switzerland: N. p., 1996. Web.https://fusion.gat.com/pubs-ext/MISCONF96/A22433.pdf + """Calculate the bootstrap fraction using the Wong et al formula. + + Parameters + ---------- + beta_poloidal : float + Plasma poloidal beta. + density_index : float + Density profile index. + temperature_index : float + Temperature profile index. + inverse_aspect : float + Inverse aspect ratio. + elongation : float + Plasma elongation. + + Returns + ------- + float + The calculated bootstrap fraction. + + Notes + ----- + - Data is based off of equilibria from Miller et al. + - A: 1.2 - 3.0 and stable to n ballooning and low n kink modes at a bootstrap fraction of 99% + for kappa = 2, 2.5 and 3. + - The results were parameterized as a function of aspect ratio and elongation. + - The parametric dependency of beta_p and beta_T are based on fitting of the DIII-D high + equivalent DT yield results. + - Parabolic profiles should be used for best results as the pressure peaking value is calculated + as the product of a parabolic temperature and density profile. + + References + ---------- + C.-P. Wong, J. C. Wesley, R. D. Stambaugh, and E. T. Cheng, "Toroidal reactor designs as a function of aspect ratio and elongation," + vol. 42, no. 5, pp. 547-556, May 2002, doi: https://doi.org/10.1088/0029-5515/42/5/307. + + Miller, R L, "Stable bootstrap-current driven equilibria for low aspect ratio tokamaks". + Switzerland: N. p., 1996. Web.https://fusion.gat.com/pubs-ext/MISCONF96/A22433.pdf """ # Using the standard variable naming from the Wong et.al. paper f_peak = 2.0 / scipy.special.beta(0.5, density_index + temperature_index + 1) @@ -829,35 +932,47 @@ def bootstrap_fraction_gi_I( # noqa: N802 q95: float, q0: float, ) -> float: - """ - Calculate the bootstrap fraction using the first scaling from the Gi et al formula. - - Parameters: - beta_poloidal (float): Plasma poloidal beta. - pressure_index (float): Pressure profile index. - temperature_index (float): Temperature profile index. - inverse_aspect (float): Inverse aspect ratio. - effective_charge (float): Plasma effective charge. - q95 (float): Safety factor at 95% of the plasma radius. - q0 (float): Safety factor at the magnetic axis. - - Returns: - float: The calculated bootstrap fraction. - - Notes: - - Scaling found by solving the Hirshman-Sigmar bootstrap modelusing the matrix inversion method - - Method was done to put the scaling into parameters compatible with the TPC systems code - - Uses the ACCOME code to create bootstrap current fractions without using the itrative calculations of the - curent drive and equilibrium models in the scan - - R = 5.0 m, A = 1.3 - 5.0, kappa = 2, traing = 0.3, alpha_n = 0.1 - 0.8, alpha_t = 1.0 - 3.0, Z_eff = 1.2 - 3.0 - - Uses parabolic plasma profiles only. - - Scaling 1 has better accuracy than Scaling 2. However, Scaling 1 overestimated the f_BS value for reversed shear - equilibrium. - - References: - - K. Gi, M. Nakamura, Kenji Tobita, and Y. Ono, “Bootstrap current fraction scaling for a tokamak reactor design study,” - Fusion Engineering and Design, vol. 89, no. 11, pp. 2709-2715, Aug. 2014, - doi: https://doi.org/10.1016/j.fusengdes.2014.07.009. + """Calculate the bootstrap fraction using the first scaling from the Gi et al formula. + + Parameters + ---------- + beta_poloidal : float + Plasma poloidal beta. + pressure_index : float + Pressure profile index. + temperature_index : float + Temperature profile index. + inverse_aspect : float + Inverse aspect ratio. + effective_charge : float + Plasma effective charge. + q95 : float + Safety factor at 95% of the plasma radius. + q0 : float + Safety factor at the magnetic axis. + + Returns + ------- + float + The calculated bootstrap fraction. + + Notes + ----- + - Scaling found by solving the Hirshman-Sigmar bootstrap model using the matrix inversion method. + - Method was done to put the scaling into parameters compatible with the TPC systems code. + - Uses the ACCOME code to create bootstrap current fractions without using the iterative + calculations of the current drive and equilibrium models in the scan. + - R = 5.0 m, A = 1.3 - 5.0, kappa = 2, triang = 0.3, alpha_n = 0.1 - 0.8, alpha_t = 1.0 - 3.0, + Z_eff = 1.2 - 3.0. + - Uses parabolic plasma profiles only. + - Scaling 1 has better accuracy than Scaling 2. However, Scaling 1 overestimated the f_BS value + for reversed shear equilibrium. + + References + ---------- + K. Gi, M. Nakamura, Kenji Tobita, and Y. Ono, "Bootstrap current fraction scaling for a tokamak reactor design study," + Fusion Engineering and Design, vol. 89, no. 11, pp. 2709-2715, Aug. 2014, + doi: https://doi.org/10.1016/j.fusengdes.2014.07.009. """ # Using the standard variable naming from the Gi et.al. paper @@ -881,33 +996,43 @@ def bootstrap_fraction_gi_II( # noqa: N802 inverse_aspect: float, effective_charge: float, ) -> float: - """ - Calculate the bootstrap fraction using the second scaling from the Gi et al formula. - - Parameters: - beta_poloidal (float): Plasma poloidal beta. - pressure_index (float): Pressure profile index. - temperature_index (float): Temperature profile index. - inverse_aspect (float): Inverse aspect ratio. - effective_charge (float): Plasma effective charge. - - Returns: - float: The calculated bootstrap fraction. - - Notes: - - Scaling found by solving the Hirshman-Sigmar bootstrap modelusing the matrix inversion method - - Method was done to put the scaling into parameters compatible with the TPC systems code - - Uses the ACCOME code to create bootstrap current fractions without using the itrative calculations of the - curent drive and equilibrium models in the scan - - R = 5.0 m, A = 1.3 - 5.0, kappa = 2, traing = 0.3, alpha_n = 0.1 - 0.8, alpha_t = 1.0 - 3.0, Z_eff = 1.2 - 3.0 - - Uses parabolic plasma profiles only. - - This scaling has the q profile dependance removed to obtain a scaling formula with much more flexible variables than - that by a single profile factor for internal current profile. - - References: - - K. Gi, M. Nakamura, Kenji Tobita, and Y. Ono, “Bootstrap current fraction scaling for a tokamak reactor design study,” - Fusion Engineering and Design, vol. 89, no. 11, pp. 2709-2715, Aug. 2014, - doi: https://doi.org/10.1016/j.fusengdes.2014.07.009. + """Calculate the bootstrap fraction using the second scaling from the Gi et al formula. + + Parameters + ---------- + beta_poloidal : float + Plasma poloidal beta. + pressure_index : float + Pressure profile index. + temperature_index : float + Temperature profile index. + inverse_aspect : float + Inverse aspect ratio. + effective_charge : float + Plasma effective charge. + + Returns + ------- + float + The calculated bootstrap fraction. + + Notes + ----- + - Scaling found by solving the Hirshman-Sigmar bootstrap model using the matrix inversion method. + - Method was done to put the scaling into parameters compatible with the TPC systems code. + - Uses the ACCOME code to create bootstrap current fractions without using the iterative + calculations of the current drive and equilibrium models in the scan. + - R = 5.0 m, A = 1.3 - 5.0, kappa = 2, triang = 0.3, alpha_n = 0.1 - 0.8, alpha_t = 1.0 - 3.0, + Z_eff = 1.2 - 3.0. + - Uses parabolic plasma profiles only. + - This scaling has the q profile dependence removed to obtain a scaling formula with much more + flexible variables than that by a single profile factor for internal current profile. + + References + ---------- + K. Gi, M. Nakamura, Kenji Tobita, and Y. Ono, "Bootstrap current fraction scaling for a tokamak reactor design study," + Fusion Engineering and Design, vol. 89, no. 11, pp. 2709-2715, Aug. 2014, + doi: https://doi.org/10.1016/j.fusengdes.2014.07.009. """ # Using the standard variable naming from the Gi et.al. paper @@ -932,37 +1057,42 @@ def bootstrap_fraction_sugiyama_l_mode( q95: float, q0: float, ) -> float: - """ - Calculate the bootstrap fraction using the L-mode scaling from the Sugiyama et al formula. - - :param eps: Inverse aspect ratio. - :type eps: float - :param beta_poloidal: Plasma poloidal beta. - :type beta_poloidal: float - :param alphan: Density profile index. - :type alphan: float - :param alphat: Temperature profile index. - :type alphat: float - :param zeff: Plasma effective charge. - :type zeff: float - :param q95: Safety factor at 95% of the plasma radius. - :type q95: float - :param q0: Safety factor at the magnetic axis. - :type q0: float - - :returns: The calculated bootstrap fraction. - :rtype: float - - :notes: - - This scaling is derived for L-mode plasmas. - - Ion and electron temperature are the same - - Z_eff has a uniform profile, with only fully stripped carbon impurity - - :references: - - S. Sugiyama, T. Goto, H. Utoh, and Y. Sakamoto, “Improvement of core plasma power and - current balance models for tokamak systems code considering H-mode plasma profiles,” - Fusion Engineering and Design, vol. 216, p. 115022, Jul. 2025, doi: - https://doi.org/10.1016/j.fusengdes.2025.115022. + """Calculate the bootstrap fraction using the L-mode scaling from the Sugiyama et al formula. + + Parameters + ---------- + eps : float + Inverse aspect ratio. + beta_poloidal : float + Plasma poloidal beta. + alphan : float + Density profile index. + alphat : float + Temperature profile index. + zeff : float + Plasma effective charge. + q95 : float + Safety factor at 95% of the plasma radius. + q0 : float + Safety factor at the magnetic axis. + + Returns + ------- + float + The calculated bootstrap fraction. + + Notes + ----- + - This scaling is derived for L-mode plasmas. + - Ion and electron temperature are the same. + - Z_eff has a uniform profile, with only fully stripped carbon impurity. + + References + ---------- + S. Sugiyama, T. Goto, H. Utoh, and Y. Sakamoto, "Improvement of core plasma power and + current balance models for tokamak systems code considering H-mode plasma profiles," + Fusion Engineering and Design, vol. 216, p. 115022, Jul. 2025, doi: + https://doi.org/10.1016/j.fusengdes.2025.115022. """ return ( @@ -990,49 +1120,54 @@ def bootstrap_fraction_sugiyama_h_mode( n_greenwald: float, temp_plasma_pedestal_kev: float, ) -> float: - """ - Calculate the bootstrap fraction using the H-mode scaling from the Sugiyama et al formula. - - :param eps: Inverse aspect ratio. - :type eps: float - :param beta_poloidal: Plasma poloidal beta. - :type beta_poloidal: float - :param alphan: Density profile index. - :type alphan: float - :param alphat: Temperature profile index. - :type alphat: float - :param tbeta: Second temperature profile index. - :type tbeta: float - :param zeff: Plasma effective charge. - :type zeff: float - :param q95: Safety factor at 95% of the plasma radius. - :type q95: float - :param q0: Safety factor at the magnetic axis. - :type q0: float - :param radius_plasma_pedestal_density_norm: Normalised plasma radius of density pedestal. - :type radius_plasma_pedestal_density_norm: float - :param nd_plasma_pedestal_electron: Electron number density at the pedestal [m^-3]. - :type nd_plasma_pedestal_electron: float - :param n_greenwald: Greenwald density limit [m^-3]. - :type n_greenwald: float - :param temp_plasma_pedestal_kev: Electron temperature at the pedestal [keV]. - :type temp_plasma_pedestal_kev: float - - :returns: The calculated bootstrap fraction. - :rtype: float - - :notes: - - This scaling is derived for H-mode plasmas. - - The temperature and density pedestal positions are the same - - Separatrix temperature and density are zero - - Ion and electron temperature are the same - - Z_eff has a uniform profile, with only fully stripped carbon impurity - - :references: - - S. Sugiyama, T. Goto, H. Utoh, and Y. Sakamoto, “Improvement of core plasma power and - current balance models for tokamak systems code considering H-mode plasma profiles,” - Fusion Engineering and Design, vol. 216, p. 115022, Jul. 2025, doi: - https://doi.org/10.1016/j.fusengdes.2025.115022. + """Calculate the bootstrap fraction using the H-mode scaling from the Sugiyama et al formula. + + Parameters + ---------- + eps : float + Inverse aspect ratio. + beta_poloidal : float + Plasma poloidal beta. + alphan : float + Density profile index. + alphat : float + Temperature profile index. + tbeta : float + Second temperature profile index. + zeff : float + Plasma effective charge. + q95 : float + Safety factor at 95% of the plasma radius. + q0 : float + Safety factor at the magnetic axis. + radius_plasma_pedestal_density_norm : float + Normalised plasma radius of density pedestal. + nd_plasma_pedestal_electron : float + Electron number density at the pedestal [m⁻³]. + n_greenwald : float + Greenwald density limit [m⁻³]. + temp_plasma_pedestal_kev : float + Electron temperature at the pedestal [keV]. + + Returns + ------- + float + The calculated bootstrap fraction. + + Notes + ----- + - This scaling is derived for H-mode plasmas. + - The temperature and density pedestal positions are the same. + - Separatrix temperature and density are zero. + - Ion and electron temperature are the same. + - Z_eff has a uniform profile, with only fully stripped carbon impurity. + + References + ---------- + S. Sugiyama, T. Goto, H. Utoh, and Y. Sakamoto, "Improvement of core plasma power and + current balance models for tokamak systems code considering H-mode plasma profiles," + Fusion Engineering and Design, vol. 216, p. 115022, Jul. 2025, doi: + https://doi.org/10.1016/j.fusengdes.2025.115022. """ return ( @@ -1300,24 +1435,34 @@ class SauterBootstrapCurrent: def bootstrap_fraction_sauter(self, plasma_profile: float) -> float: """Calculate the bootstrap current fraction from the Sauter et al scaling. - Args: - plasma_profile (PlasmaProfile): The plasma profile object. + Parameters + ---------- + plasma_profile : PlasmaProfile + The plasma profile object. + + Returns + ------- + tuple[float, np.ndarray] + The bootstrap current fraction and current density profile. - Returns: - float: The bootstrap current fraction. + Notes + ----- + This function calculates the bootstrap current fraction using the Sauter, Angioni, and + Lin-Liu scaling. - This function calculates the bootstrap current fraction using the Sauter, Angioni, and Lin-Liu scaling. + References + ---------- + O. Sauter, C. Angioni, Y. R. Lin-Liu; + Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria + and arbitrary collisionality regime. + Phys. Plasmas 1 July 1999; 6 (7): 2834-2839. https://doi.org/10.1063/1.873240 - Reference: - - O. Sauter, C. Angioni, Y. R. Lin-Liu; - Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime. - Phys. Plasmas 1 July 1999; 6 (7): 2834-2839. https://doi.org/10.1063/1.873240 - - O. Sauter, C. Angioni, Y. R. Lin-Liu; Erratum: - Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime - [Phys. Plasmas 6, 2834 (1999)]. Phys. Plasmas 1 December 2002; 9 (12): 5140. https://doi.org/10.1063/1.1517052 + O. Sauter, C. Angioni, Y. R. Lin-Liu; Erratum: + Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria + and arbitrary collisionality regime [Phys. Plasmas 6, 2834 (1999)]. + Phys. Plasmas 1 December 2002; 9 (12): 5140. https://doi.org/10.1063/1.1517052 - Note: - The code was supplied by Emiliano Fable, IPP Garching (private communication). + Note: The code was supplied by Emiliano Fable, IPP Garching (private communication). """ # Radial points from 0 to 1 seperated by 1/profile_size @@ -1450,25 +1595,26 @@ def _coulomb_logarithm_sauter( Parameters ---------- - radial_elements : - the radial element indexes in the range 1 to nr - tempe : - the electron temperature array - ne : - the electron density array + radial_elements : int + The radial element indexes in the range 1 to nr. + tempe : np.ndarray + The electron temperature array. + ne : np.ndarray + The electron density array. Returns ------- - type - - np.ndarray, the Coulomb logarithm at each array point + np.ndarray + The Coulomb logarithm at each array point. - Reference: - - C. A. Ordonez, M. I. Molina; - Evaluation of the Coulomb logarithm using cutoff and screened Coulomb interaction potentials. - Phys. Plasmas 1 August 1994; 1 (8): 2515-2518. https://doi.org/10.1063/1.870578 - - Y. R. Shen, “Recent advances in nonlinear optics,” Reviews of Modern Physics, vol. 48, no. 1, - pp. 1-32, Jan. 1976, doi: https://doi.org/10.1103/revmodphys.48.1. + References + ---------- + C. A. Ordonez, M. I. Molina; + Evaluation of the Coulomb logarithm using cutoff and screened Coulomb interaction potentials. + Phys. Plasmas 1 August 1994; 1 (8): 2515-2518. https://doi.org/10.1063/1.870578 + Y. R. Shen, "Recent advances in nonlinear optics," Reviews of Modern Physics, vol. 48, no. 1, + pp. 1-32, Jan. 1976, doi: https://doi.org/10.1103/revmodphys.48.1. """ return ( 15.9 @@ -1481,26 +1627,23 @@ def _electron_collisions_sauter( ) -> np.ndarray: """Calculate the frequency of electron-electron collisions used in the arrays for the Sauter bootstrap current scaling. - This function calculates the frequency of electron-electron collisions - Parameters ---------- - radial_elements : - the radial element indexes in the range 1 to nr - tempe : - the electron temperature array - ne : - the electron density array + radial_elements : np.ndarray + The radial element indexes in the range 1 to nr. + tempe : np.ndarray + The electron temperature array. + ne : np.ndarray + The electron density array. Returns ------- - : - the frequency of electron-electron collisions (Hz) - - - Reference: - - Yushmanov, 25th April 1987 (?), updated by Pereverzev, 9th November 1994 (?) + np.ndarray + The frequency of electron-electron collisions (Hz). + References + ---------- + Yushmanov, 25th April 1987 (?), updated by Pereverzev, 9th November 1994 (?) """ return ( 670.0 @@ -1523,29 +1666,29 @@ def _electron_collisionality_sauter( Parameters ---------- - radial_elements : - the radial element index in the range 1 to nr - rmajor : - the plasma major radius (m) - zeff : - the effective charge array - inverse_q : - inverse safety factor profile - sqeps : - the square root of the inverse aspect ratio array - tempe : - the electron temperature array - ne : - the electron density array + radial_elements : np.ndarray + The radial element index in the range 1 to nr. + rmajor : float + The plasma major radius (m). + zeff : np.ndarray + The effective charge array. + inverse_q : np.ndarray + Inverse safety factor profile. + sqeps : np.ndarray + The square root of the inverse aspect ratio array. + tempe : np.ndarray + The electron temperature array. + ne : np.ndarray + The electron density array. Returns ------- - : - the relative frequency of electron collisions - - Reference: - - Yushmanov, 30th April 1987 (?) + np.ndarray + The relative frequency of electron collisions. + References + ---------- + Yushmanov, 30th April 1987 (?) """ return ( self._electron_collisions_sauter(radial_elements, tempe, ne) @@ -1574,21 +1717,21 @@ def _ion_collisions_sauter( Parameters ---------- - radial_elements : - the radial element indexes in the range 1 to nr - zeff : - the effective charge array - ni : - the ion density array - tempi : - the ion temperature array - amain : - the atomic mass of the main ion species array + radial_elements : np.ndarray + The radial element indexes in the range 1 to nr. + zeff : np.ndarray + The effective charge array. + ni : np.ndarray + The ion density array. + tempi : np.ndarray + The ion temperature array. + amain : np.ndarray + The atomic mass of the main ion species array. Returns ------- - : - the full frequency of ion collisions (Hz) + np.ndarray + The full frequency of ion collisions (Hz). """ return ( zeff[radial_elements - 1] ** 4 @@ -1615,27 +1758,27 @@ def _ion_collisionality_sauter( Parameters ---------- - radial_elements : - the radial element indexes in the range 1 to nr - rmajor : - the plasma major radius (m) - inverse_q : - inverse safety factor profile - sqeps : - the square root of the inverse aspect ratio profile - tempi : - the ion temperature profile (keV) - amain : - the atomic mass of the main ion species profile - zeff : - the effective charge of the main ion species - ni : - the ion density profile (/m^3) + radial_elements : np.ndarray + The radial element indexes in the range 1 to nr. + rmajor : float + The plasma major radius (m). + inverse_q : np.ndarray + Inverse safety factor profile. + sqeps : np.ndarray + The square root of the inverse aspect ratio profile. + tempi : np.ndarray + The ion temperature profile (keV). + amain : np.ndarray + The atomic mass of the main ion species profile. + zeff : np.ndarray + The effective charge of the main ion species. + ni : np.ndarray + The ion density profile (/m³). Returns ------- - : - the ion collisionality + float + The ion collisionality. """ return ( 3.2e-6 @@ -1666,51 +1809,51 @@ def _calculate_l31_coefficient( ) -> float: """L31 coefficient before Grad(ln(ne)) in the Sauter bootstrap scaling. - Parameters ---------- - radial_elements : - radial element indexes in range 2 to nr - number_of_elements : - maximum value of radial_elements - rmajor : - plasma major radius (m) - b_plasma_toroidal_on_axis : - toroidal field on axis (T) - triang : - plasma triangularity - ne : - electron density profile (/m^3) - ni : - ion density profile (/m^3) - tempe : - electron temperature profile (keV) - tempi : - ion temperature profile (keV) - inverse_q : - inverse safety factor profile - rho : - normalized minor radius profile - zeff : - effective charge profile - sqeps : - square root of inverse aspect ratio profile + radial_elements : np.ndarray + Radial element indexes in range 2 to nr. + number_of_elements : int + Maximum value of radial_elements. + rmajor : float + Plasma major radius (m). + b_plasma_toroidal_on_axis : float + Toroidal field on axis (T). + triang : float + Plasma triangularity. + ne : np.ndarray + Electron density profile (/m³). + ni : np.ndarray + Ion density profile (/m³). + tempe : np.ndarray + Electron temperature profile (keV). + tempi : np.ndarray + Ion temperature profile (keV). + inverse_q : np.ndarray + Inverse safety factor profile. + rho : np.ndarray + Normalized minor radius profile. + zeff : np.ndarray + Effective charge profile. + sqeps : np.ndarray + Square root of inverse aspect ratio profile. Returns ------- - type - - float, the coefficient scaling grad(ln(ne)) in the Sauter bootstrap current scaling. - - This function calculates the coefficient scaling grad(ln(ne)) in the Sauter bootstrap current scaling. - - Reference: - - O. Sauter, C. Angioni, Y. R. Lin-Liu; - Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime. - Phys. Plasmas 1 July 1999; 6 (7): 2834-2839. https://doi.org/10.1063/1.873240 - - O. Sauter, C. Angioni, Y. R. Lin-Liu; Erratum: - Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime - [Phys. Plasmas 6, 2834 (1999)]. Phys. Plasmas 1 December 2002; 9 (12): 5140. https://doi.org/10.1063/1.1517052 + float + The coefficient scaling grad(ln(ne)) in the Sauter bootstrap current scaling. + References + ---------- + O. Sauter, C. Angioni, Y. R. Lin-Liu; + Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria + and arbitrary collisionality regime. + Phys. Plasmas 1 July 1999; 6 (7): 2834-2839. https://doi.org/10.1063/1.873240 + + O. Sauter, C. Angioni, Y. R. Lin-Liu; Erratum: + Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria + and arbitrary collisionality regime [Phys. Plasmas 6, 2834 (1999)]. + Phys. Plasmas 1 December 2002; 9 (12): 5140. https://doi.org/10.1063/1.1517052 """ # Prevents first element being 0 charge_profile = zeff[radial_elements - 1] @@ -1771,51 +1914,51 @@ def _calculate_l31_32_coefficient( This function calculates the coefficient scaling grad(ln(Te)) in the Sauter bootstrap current scaling. - Parameters ---------- - radial_elements : - radial element indexes in range 2 to nr - number_of_elements : - maximum value of radial_elements - rmajor : - plasma major radius (m) - b_plasma_toroidal_on_axis : - toroidal field on axis (T) - triang : - plasma triangularity - ne : - electron density profile (/m^3) - ni : - ion density profile (/m^3) - tempe : - electron temperature profile (keV) - tempi : - ion temperature profile (keV) - inverse_q : - inverse safety factor profile - rho : - normalized minor radius profile - zeff : - effective charge profile - sqeps : - square root of inverse aspect ratio profile - + radial_elements : np.ndarray + Radial element indexes in range 2 to nr. + number_of_elements : int + Maximum value of radial_elements. + rmajor : float + Plasma major radius (m). + b_plasma_toroidal_on_axis : float + Toroidal field on axis (T). + triang : float + Plasma triangularity. + ne : np.ndarray + Electron density profile (/m³). + ni : np.ndarray + Ion density profile (/m³). + tempe : np.ndarray + Electron temperature profile (keV). + tempi : np.ndarray + Ion temperature profile (keV). + inverse_q : np.ndarray + Inverse safety factor profile. + rho : np.ndarray + Normalized minor radius profile. + zeff : np.ndarray + Effective charge profile. + sqeps : np.ndarray + Square root of inverse aspect ratio profile. Returns ------- - : - the L31 & L32 coefficient scaling grad(ln(Te)) in the Sauter bootstrap current scaling. - - - Reference: - - O. Sauter, C. Angioni, Y. R. Lin-Liu; - Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime. - Phys. Plasmas 1 July 1999; 6 (7): 2834-2839. https://doi.org/10.1063/1.873240 - - O. Sauter, C. Angioni, Y. R. Lin-Liu; Erratum: - Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime - [Phys. Plasmas 6, 2834 (1999)]. Phys. Plasmas 1 December 2002; 9 (12): 5140. https://doi.org/10.1063/1.1517052 + float + The L31 & L32 coefficient scaling grad(ln(Te)) in the Sauter bootstrap current scaling. + References + ---------- + O. Sauter, C. Angioni, Y. R. Lin-Liu; + Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria + and arbitrary collisionality regime. + Phys. Plasmas 1 July 1999; 6 (7): 2834-2839. https://doi.org/10.1063/1.873240 + + O. Sauter, C. Angioni, Y. R. Lin-Liu; Erratum: + Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria + and arbitrary collisionality regime [Phys. Plasmas 6, 2834 (1999)]. + Phys. Plasmas 1 December 2002; 9 (12): 5140. https://doi.org/10.1063/1.1517052 """ # Prevents first element being 0 @@ -1959,55 +2102,55 @@ def _calculate_l34_alpha_31_coefficient( This function calculates the coefficient scaling grad(ln(Ti)) in the Sauter bootstrap current scaling. - Parameters ---------- - radial_elements : - radial element indexes in range 2 to nr - number_of_elements : - maximum value of radial_elements - rmajor : - plasma major radius (m) - b_plasma_toroidal_on_axis : - toroidal field on axis (T) - triang : - plasma triangularity - inverse_q : - inverse safety factor profile - sqeps : - square root of inverse aspect ratio profile - tempi : - ion temperature profile (keV) - tempe : - electron temperature profile (keV) - amain : - atomic mass of the main ion - zmain : - charge of the main ion - ni : - ion density profile (/m^3) - ne : - electron density profile (/m^3) - rho : - normalized minor radius profile - zeff : - effective charge profile - + radial_elements : np.ndarray + Radial element indexes in range 2 to nr. + number_of_elements : int + Maximum value of radial_elements. + rmajor : float + Plasma major radius (m). + b_plasma_toroidal_on_axis : float + Toroidal field on axis (T). + triang : float + Plasma triangularity. + inverse_q : np.ndarray + Inverse safety factor profile. + sqeps : np.ndarray + Square root of inverse aspect ratio profile. + tempi : np.ndarray + Ion temperature profile (keV). + tempe : np.ndarray + Electron temperature profile (keV). + amain : float + Atomic mass of the main ion. + zmain : float + Charge of the main ion. + ni : np.ndarray + Ion density profile (/m³). + ne : np.ndarray + Electron density profile (/m³). + rho : np.ndarray + Normalized minor radius profile. + zeff : np.ndarray + Effective charge profile. Returns ------- - : - the L34, alpha and L31 coefficient scaling grad(ln(Ti)) in the Sauter bootstrap current scaling. - - - Reference: - - O. Sauter, C. Angioni, Y. R. Lin-Liu; - Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime. - Phys. Plasmas 1 July 1999; 6 (7): 2834-2839. https://doi.org/10.1063/1.873240 - - O. Sauter, C. Angioni, Y. R. Lin-Liu; Erratum: - Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime - [Phys. Plasmas 6, 2834 (1999)]. Phys. Plasmas 1 December 2002; 9 (12): 5140. https://doi.org/10.1063/1.1517052 + float + The L34, alpha and L31 coefficient scaling grad(ln(Ti)) in the Sauter bootstrap current scaling. + References + ---------- + O. Sauter, C. Angioni, Y. R. Lin-Liu; + Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria + and arbitrary collisionality regime. + Phys. Plasmas 1 July 1999; 6 (7): 2834-2839. https://doi.org/10.1063/1.873240 + + O. Sauter, C. Angioni, Y. R. Lin-Liu; Erratum: + Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria + and arbitrary collisionality regime [Phys. Plasmas 6, 2834 (1999)]. + Phys. Plasmas 1 December 2002; 9 (12): 5140. https://doi.org/10.1063/1.1517052 """ # Prevents first element being 0 charge_profile = zeff[radial_elements - 1] @@ -2133,28 +2276,27 @@ def _beta_poloidal_sauter( Parameters ---------- - radial_elements : - radial element indexes in range 1 to nr - nr : - maximum value of radial_elements - rmajor : - plasma major radius (m) - b_plasma_toroidal_on_axis : - toroidal field on axis (T) - ne : - electron density profile (/m^3) - tempe : - electron temperature profile (keV) - inverse_q : - inverse safety factor profile - rho : - normalized minor radius profile - + radial_elements : np.ndarray + Radial element indexes in range 1 to nr. + nr : int + Maximum value of radial_elements. + rmajor : float + Plasma major radius (m). + b_plasma_toroidal_on_axis : float + Toroidal field on axis (T). + ne : np.ndarray + Electron density profile (/m³). + tempe : np.ndarray + Electron temperature profile (keV). + inverse_q : np.ndarray + Inverse safety factor profile. + rho : np.ndarray + Normalized minor radius profile. Returns ------- - : - the local beta poloidal + np.ndarray + The local beta poloidal. """ return ( np.where( @@ -2193,31 +2335,31 @@ def _beta_poloidal_total_sauter( Parameters ---------- - radial_elements : - radial element indexes in range 1 to nr - nr : - maximum value of radial_elements - rmajor : - plasma major radius (m) - b_plasma_toroidal_on_axis : - toroidal field on axis (T) - ne : - electron density profile (/m^3) - ni : - ion density profile (/m^3) - tempe : - electron temperature profile (keV) - tempi : - ion temperature profile (keV) - inverse_q : - inverse safety factor profile - rho : - normalized minor radius profile + radial_elements : np.ndarray + Radial element indexes in range 1 to nr. + nr : int + Maximum value of radial_elements. + rmajor : float + Plasma major radius (m). + b_plasma_toroidal_on_axis : float + Toroidal field on axis (T). + ne : np.ndarray + Electron density profile (/m³). + ni : np.ndarray + Ion density profile (/m³). + tempe : np.ndarray + Electron temperature profile (keV). + tempi : np.ndarray + Ion temperature profile (keV). + inverse_q : np.ndarray + Inverse safety factor profile. + rho : np.ndarray + Normalized minor radius profile. Returns ------- - : - the local total beta poloidal + np.ndarray + The local total beta poloidal. """ return ( np.where( @@ -2257,40 +2399,44 @@ def _trapped_particle_fraction_sauter( radial_elements: np.ndarray, triang: float, sqeps: np.ndarray, fit: int = 0 ) -> np.ndarray: """Calculates the trapped particle fraction to be used in the Sauter bootstrap current scaling. + Parameters ---------- - radial_elements : - radial element index in range 1 to nr - triang : - plasma triangularity - sqeps : - square root of local aspect ratio - fit : - fit method (1 = ASTRA method, 2 = Equation from Sauter 2002, 3 = Equation from Sauter 2016) + radial_elements : np.ndarray + Radial element index in range 1 to nr. + triang : float + Plasma triangularity. + sqeps : np.ndarray + Square root of local aspect ratio. + fit : int, optional + Fit method (0 = ASTRA method, 1 = Sauter 2002, 2 = Sauter 2016). + Default is 0. Returns ------- - type - - list, trapped particle fraction + np.ndarray + Trapped particle fraction. - This function calculates the trapped particle fraction at a given radius. + Notes + ----- + This function calculates the trapped particle fraction at a given radius. References ---------- - Used in this paper: - - O. Sauter, C. Angioni, Y. R. Lin-Liu; - Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria and arbitrary collisionality regime. - Phys. Plasmas 1 July 1999; 6 (7): 2834-2839. https://doi.org/10.1063/1.873240 - - - O. Sauter, R. J. Buttery, R. Felton, T. C. Hender, D. F. Howell, and contributors to the E.-J. Workprogramme, - “Marginal-limit for neoclassical tearing modes in JET H-mode discharges,” - Plasma Physics and Controlled Fusion, vol. 44, no. 9, pp. 1999-2019, Aug. 2002, - doi: https://doi.org/10.1088/0741-3335/44/9/315. - - - O. Sauter, Geometric formulas for system codes including the effect of negative triangularity, - Fusion Engineering and Design, Volume 112, 2016, Pages 633-645, ISSN 0920-3796, - https://doi.org/10.1016/j.fusengdes.2016.04.033. - + Used in this paper: + O. Sauter, C. Angioni, Y. R. Lin-Liu; + Neoclassical conductivity and bootstrap current formulas for general axisymmetric equilibria + and arbitrary collisionality regime. + Phys. Plasmas 1 July 1999; 6 (7): 2834-2839. https://doi.org/10.1063/1.873240 + + O. Sauter, R. J. Buttery, R. Felton, T. C. Hender, D. F. Howell, and contributors to the E.-J. Workprogramme, + "Marginal-limit for neoclassical tearing modes in JET H-mode discharges," + Plasma Physics and Controlled Fusion, vol. 44, no. 9, pp. 1999-2019, Aug. 2002, + doi: https://doi.org/10.1088/0741-3335/44/9/315. + + O. Sauter, Geometric formulas for system codes including the effect of negative triangularity, + Fusion Engineering and Design, Volume 112, 2016, Pages 633-645, ISSN 0920-3796, + https://doi.org/10.1016/j.fusengdes.2016.04.033. """ # Prevent first element from being zero sqeps_reduced = sqeps[radial_elements - 1] @@ -2324,4 +2470,4 @@ def _trapped_particle_fraction_sauter( * (np.sqrt((1.0 - eps) / (1.0 + eps))) ) - raise ProcessValueError(f"fit={fit} is not valid. Must be 1, 2, or 3") + raise ProcessValueError(f"fit={fit} is not valid. Must be 0, 1, or 2") From 0f440de3acd282a8e14a3f95c80e98df87cf09a5 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Mon, 2 Mar 2026 14:51:14 +0000 Subject: [PATCH 8/8] Refactor import statements in bootstrap_current.py for consistency and clarity --- process/models/physics/bootstrap_current.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/process/models/physics/bootstrap_current.py b/process/models/physics/bootstrap_current.py index 8717a2266..46010b83a 100644 --- a/process/models/physics/bootstrap_current.py +++ b/process/models/physics/bootstrap_current.py @@ -5,13 +5,13 @@ import scipy import scipy.integrate as integrate -from process import constants -from process import process_output as po +from process.core import constants +from process.core import process_output as po +from process.core.exceptions import ProcessValueError from process.data_structure import ( current_drive_variables, physics_variables, ) -from process.exceptions import ProcessValueError from process.models.physics.plasma_profiles import PlasmaProfile logger = logging.getLogger(__name__)