From fb79c692bd5523ede65dd3db8119bbc13381af90 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Thu, 3 Sep 2026 12:02:23 +0100 Subject: [PATCH 01/18] rework neoclassics --- process/models/stellarator/neoclassics.py | 597 +++++++--------------- process/models/stellarator/stellarator.py | 14 - 2 files changed, 192 insertions(+), 419 deletions(-) diff --git a/process/models/stellarator/neoclassics.py b/process/models/stellarator/neoclassics.py index b1e00bd16d..e16e872933 100644 --- a/process/models/stellarator/neoclassics.py +++ b/process/models/stellarator/neoclassics.py @@ -4,6 +4,7 @@ from dataclasses import dataclass import numpy as np +from numpy.polynomial.polynomial import polyval from process.core import constants from process.core.model import Model @@ -34,6 +35,16 @@ def no_roots(self): """Obtain number of Gauss Laguerre roots""" return self.data.neoclassics.roots.shape[0] + @property + def mass(self): + """Component mass array for electron, deuterium, tritium and Helium""" + return np.array([ + constants.ELECTRON_MASS, + constants.PROTON_MASS * 2.0, + constants.PROTON_MASS * 3.0, + constants.PROTON_MASS * 4.0, + ]) + def output(self): """Neoclassics model doesn't have any output""" @@ -156,144 +167,69 @@ def init_neoclassics(self, r_effin, eps_effin, iotain): ) self.data.neoclassics.q_flux = self.neoclassics_calc_q_flux() - def init_profile_values_from_PROCESS(self, rho): + def init_profile_values_from_PROCESS(self, rho) -> tuple[np.ndarray, ...]: """Initialises the profile_values object from PROCESS' parabolic profiles Parameters ---------- rho : + Returns + ------- + dens: + density of electron, deuterium, tritum and alpha + temp: + temperature of electron, deuterium, tritum and alpha + dr_dens: + derivative density of electron, deuterium, tritum and alpha + dr_temp: + derivative temperature of electron, deuterium, tritum and alpha """ - tempe = ( - self.data.physics.temp_plasma_electron_on_axis_kev - * (1 - rho**2) ** self.data.physics.alphat - * KEV - ) - tempT = ( - self.data.physics.temp_plasma_ion_on_axis_kev - * (1 - rho**2) ** self.data.physics.alphat - * KEV - ) - tempD = ( - self.data.physics.temp_plasma_ion_on_axis_kev - * (1 - rho**2) ** self.data.physics.alphat - * KEV - ) - tempa = ( - self.data.physics.temp_plasma_ion_on_axis_kev - * (1 - rho**2) ** self.data.physics.alphat - * KEV - ) + t_suffix = (1 - rho**2) ** self.data.physics.alphat * KEV - dense = ( - self.data.physics.nd_plasma_electron_on_axis - * (1 - rho**2) ** self.data.physics.alphan - ) - densT = ( - (1 - self.data.physics.f_plasma_fuel_deuterium) - * self.data.physics.nd_plasma_ions_on_axis - * (1 - rho**2) ** self.data.physics.alphan - ) - densD = ( + dens_suffix = (1 - rho**2) ** self.data.physics.alphan + + deriv_prefix = -2.0 * 1.0 / self.data.physics.rminor * rho + + # array are set up as electron, deuterium, tritum, alpha + density_array = np.array([ + self.data.physics.nd_plasma_electron_on_axis, self.data.physics.f_plasma_fuel_deuterium - * self.data.physics.nd_plasma_ions_on_axis - * (1 - rho**2) ** self.data.physics.alphan - ) - densa = ( + * self.data.physics.nd_plasma_ions_on_axis, + (1 - self.data.physics.f_plasma_fuel_deuterium) + * self.data.physics.nd_plasma_ions_on_axis, self.data.physics.nd_plasma_alphas_thermal_vol_avg - * (1 + self.data.physics.alphan) - * (1 - rho**2) ** self.data.physics.alphan - ) + * (1 + self.data.physics.alphan), + ]) + + temperature_array = np.array([ + self.data.physics.temp_plasma_electron_on_axis_kev, + self.data.physics.temp_plasma_ion_on_axis_kev, + self.data.physics.temp_plasma_ion_on_axis_kev, + self.data.physics.temp_plasma_ion_on_axis_kev, + ]) # Derivatives in real space - dr_tempe = ( - -2.0 - * 1.0 - / self.data.physics.rminor - * self.data.physics.temp_plasma_electron_on_axis_kev - * rho - * (1.0 - rho**2) ** (self.data.physics.alphat - 1.0) - * self.data.physics.alphat - * KEV - ) - dr_tempT = ( - -2.0 - * 1.0 - / self.data.physics.rminor - * self.data.physics.temp_plasma_ion_on_axis_kev - * rho - * (1.0 - rho**2) ** (self.data.physics.alphat - 1.0) - * self.data.physics.alphat - * KEV - ) - dr_tempD = ( - -2.0 - * 1.0 - / self.data.physics.rminor - * self.data.physics.temp_plasma_ion_on_axis_kev - * rho - * (1.0 - rho**2) ** (self.data.physics.alphat - 1.0) - * self.data.physics.alphat - * KEV + dr_dens = ( + deriv_prefix + * density_array + * (1.0 - rho**2) ** (self.data.physics.alphan - 1.0) + * self.data.physics.alphan ) - dr_tempa = ( - -2.0 - * 1.0 - / self.data.physics.rminor - * self.data.physics.temp_plasma_ion_on_axis_kev - * rho + dr_temp = ( + deriv_prefix + * temperature_array * (1.0 - rho**2) ** (self.data.physics.alphat - 1.0) * self.data.physics.alphat * KEV ) - - dr_dense = ( - -2.0 - * 1.0 - / self.data.physics.rminor - * rho - * self.data.physics.nd_plasma_electron_on_axis - * (1.0 - rho**2) ** (self.data.physics.alphan - 1.0) - * self.data.physics.alphan - ) - dr_densT = ( - -2.0 - * 1.0 - / self.data.physics.rminor - * rho - * (1 - self.data.physics.f_plasma_fuel_deuterium) - * self.data.physics.nd_plasma_ions_on_axis - * (1.0 - rho**2) ** (self.data.physics.alphan - 1.0) - * self.data.physics.alphan - ) - dr_densD = ( - -2.0 - * 1.0 - / self.data.physics.rminor - * rho - * self.data.physics.f_plasma_fuel_deuterium - * self.data.physics.nd_plasma_ions_on_axis - * (1.0 - rho**2) ** (self.data.physics.alphan - 1.0) - * self.data.physics.alphan - ) - dr_densa = ( - -2.0 - * 1.0 - / self.data.physics.rminor - * rho - * self.data.physics.nd_plasma_alphas_thermal_vol_avg - * (1 + self.data.physics.alphan) - * (1.0 - rho**2) ** (self.data.physics.alphan - 1.0) - * self.data.physics.alphan + return ( + (density_array * dens_suffix), + (temperature_array * t_suffix), + dr_dens, + dr_temp, ) - dens = np.array([dense, densD, densT, densa]) - temp = np.array([tempe, tempD, tempT, tempa]) - dr_dens = np.array([dr_dense, dr_densD, dr_densT, dr_densa]) - dr_temp = np.array([dr_tempe, dr_tempD, dr_tempT, dr_tempa]) - - return dens, temp, dr_dens, dr_temp - def calc_neoclassics(self): """Calculate neoclassics parameters""" if self.data.stellarator_config.stella_config_epseff < 0: @@ -307,16 +243,6 @@ def calc_neoclassics(self): self.data.stellarator.iotabar, ) - q_PROCESS = ( - ( - self.data.physics.f_p_alpha_plasma_deposited - * self.data.physics.pden_alpha_total_vol_avg_mw - - self.data.physics.pden_plasma_core_rad_mw - ) - * self.data.physics.vol_plasma - / self.data.physics.a_plasma_surface - * self.data.impurity_radiation.radius_plasma_core_norm - ) q_PROCESS_r1 = ( ( self.data.physics.f_p_alpha_plasma_deposited @@ -327,81 +253,25 @@ def calc_neoclassics(self): / self.data.physics.a_plasma_surface ) - q_neo = sum(self.data.neoclassics.q_flux * 1e-6) - gamma_neo = sum( - self.data.neoclassics.gamma_flux * self.data.neoclassics.temperatures * 1e-6 - ) - - total_q_neo = sum( - self.data.neoclassics.q_flux * 1e-6 - + self.data.neoclassics.gamma_flux - * self.data.neoclassics.temperatures - * 1e-6 - ) - - total_q_neo_e = ( - 2 - * 2 - * ( - self.data.neoclassics.q_flux[0] * 1e-6 - + self.data.neoclassics.gamma_flux[0] - * self.data.neoclassics.temperatures[0] - * 1e-6 - ) - ) - - q_neo_e = self.data.neoclassics.q_flux[0] * 1e-6 - q_neo_D = self.data.neoclassics.q_flux[1] * 1e-6 - q_neo_a = self.data.neoclassics.q_flux[3] * 1e-6 - q_neo_T = self.data.neoclassics.q_flux[2] * 1e-6 - - g_neo_e = ( - self.data.neoclassics.gamma_flux[0] - * 1e-6 - * self.data.neoclassics.temperatures[0] - ) - g_neo_D = ( - self.data.neoclassics.gamma_flux[1] - * 1e-6 - * self.data.neoclassics.temperatures[1] - ) - g_neo_a = ( - self.data.neoclassics.gamma_flux[3] - * 1e-6 - * self.data.neoclassics.temperatures[3] - ) - g_neo_T = ( - self.data.neoclassics.gamma_flux[2] - * 1e-6 - * self.data.neoclassics.temperatures[2] - ) + q_PROCESS = q_PROCESS_r1 * self.data.impurity_radiation.radius_plasma_core_norm dndt_neo_e = self.data.neoclassics.gamma_flux[0] - dndt_neo_D = self.data.neoclassics.gamma_flux[1] - dndt_neo_a = self.data.neoclassics.gamma_flux[3] - dndt_neo_T = self.data.neoclassics.gamma_flux[2] + q_neo_e = self.data.neoclassics.q_flux[0] * 1e-6 + g_neo_e = 1e-6 * dndt_neo_e * self.data.neoclassics.temperatures[0] + total_q_neo_e = 4 * (q_neo_e + g_neo_e) - dndt_neo_fuel = ( - (dndt_neo_D + dndt_neo_T) - * self.data.physics.a_plasma_surface - * self.data.impurity_radiation.radius_plasma_core_norm - ) - dmdt_neo_fuel = ( - dndt_neo_fuel * self.data.physics.m_fuel_amu * constants.PROTON_MASS * 1.0e6 - ) # mg dmdt_neo_fuel_from_e = ( - 4 + 4e6 * dndt_neo_e * self.data.physics.a_plasma_surface * self.data.impurity_radiation.radius_plasma_core_norm * self.data.physics.m_fuel_amu * constants.PROTON_MASS - * 1.0e6 ) # kg chi_neo_e = -( self.data.neoclassics.q_flux[0] - + self.data.neoclassics.gamma_flux[0] * self.data.neoclassics.temperatures[0] + + dndt_neo_e * self.data.neoclassics.temperatures[0] ) / ( self.data.neoclassics.densities[0] * self.data.neoclassics.dr_temperatures[0] + self.data.neoclassics.temperatures[0] @@ -410,56 +280,78 @@ def calc_neoclassics(self): chi_PROCESS_e = self.st_calc_eff_chi() - nu_star = NormalisedCollisionality( - e=self.data.neoclassics.nu_star_averaged[0], - D=self.data.neoclassics.nu_star_averaged[1], - T=self.data.neoclassics.nu_star_averaged[2], - He=self.data.neoclassics.nu_star_averaged[3], - ) - + # Unused calculations + # q_neo_sum = 1e-6 * sum(self.data.neoclassics.q_flux) + # gamma_neo = 1e-6 * sum( + # self.data.neoclassics.gamma_flux * self.data.neoclassics.temperatures + # ) + + # total_q_neo = 1e-6 * sum( + # self.data.neoclassics.q_flux + # + self.data.neoclassics.gamma_flux * self.data.neoclassics.temperatures + # ) + + # dndt_neo_D = self.data.neoclassics.gamma_flux[1] + # dndt_neo_a = self.data.neoclassics.gamma_flux[3] + # dndt_neo_T = self.data.neoclassics.gamma_flux[2] + + # dndt_neo_fuel = ( + # (dndt_neo_D + dndt_neo_T) + # * self.data.physics.a_plasma_surface + # * self.data.impurity_radiation.radius_plasma_core_norm + # ) + # dmdt_neo_fuel = ( + # dndt_neo_fuel + # * self.data.physics.m_fuel_amu + # * constants.PROTON_MASS + # * 1.0e6 + # ) # mg return ( q_PROCESS, q_PROCESS_r1, - q_neo, - gamma_neo, - total_q_neo, total_q_neo_e, q_neo_e, - q_neo_D, - q_neo_a, - q_neo_T, g_neo_e, - g_neo_D, - g_neo_a, - g_neo_T, dndt_neo_e, - dndt_neo_D, - dndt_neo_a, - dndt_neo_T, - dndt_neo_fuel, - dmdt_neo_fuel, dmdt_neo_fuel_from_e, chi_neo_e, chi_PROCESS_e, - nu_star, + NormalisedCollisionality( + e=self.data.neoclassics.nu_star_averaged[0], + D=self.data.neoclassics.nu_star_averaged[1], + T=self.data.neoclassics.nu_star_averaged[2], + He=self.data.neoclassics.nu_star_averaged[3], + ), ) def neoclassics_calc_KT(self): """Calculates the energy on the given grid which is given by the gauss laguerre roots. """ - k = np.repeat((self.data.neoclassics.roots / KEV)[:, np.newaxis], 4, axis=1) - - return (k * self.data.neoclassics.temperatures).T + return ( + self.data.neoclassics.roots[None] / KEV + ) * self.data.neoclassics.temperatures[:, None] + + @staticmethod + def _nu_erfn(xk, expxk): + """Error function""" + # Rational approximation for erf + # t term + t = 1.0 / (1.0 + 0.3275911 * np.sqrt(xk)) + # Expand: t * (c0 + c1*t + c2*t^2 + ...) + coeffs = np.array([ + 0, + 0.254829592, + -0.284496736, + 1.421413741, + -1.453152027, + 1.061405429, + ]) + return 1.0 - polyval(t, coeffs) * expxk def neoclassics_calc_nu(self): """Calculates the collision frequency""" - mass = np.array([ - constants.ELECTRON_MASS, - constants.PROTON_MASS * 2.0, - constants.PROTON_MASS * 3.0, - constants.PROTON_MASS * 4.0, - ]) + mass = self.mass z = np.array([-1.0, 1.0, 1.0, 2.0]) * constants.ELECTRON_CHARGE # transform the temperature back in eV @@ -472,76 +364,41 @@ def neoclassics_calc_nu(self): * np.log10(self.data.neoclassics.temperatures[0] / constants.ELECTRON_CHARGE) ) - neoclassics_calc_nu = np.zeros((4, self.no_roots), order="F") - - for j in range(4): - for i in range(self.no_roots): - x = self.data.neoclassics.roots[i] - for k in range(4): - xk = ( - (mass[k] / mass[j]) - * ( - self.data.neoclassics.temperatures[j] - / self.data.neoclassics.temperatures[k] - ) - * x - ) - expxk = np.exp(-xk) - t = 1.0 / (1.0 + 0.3275911 * np.sqrt(xk)) - erfn = ( - 1.0 - - t - * ( - 0.254829592 - + t - * ( - -0.284496736 - + t - * (1.421413741 + t * (-1.453152027 + t * 1.061405429)) - ) - ) - * expxk - ) - phixmgx = (1.0 - 0.5 / xk) * erfn + expxk / np.sqrt(np.pi * xk) - v = np.sqrt( - 2.0 * x * self.data.neoclassics.temperatures[j] / mass[j] - ) - neoclassics_calc_nu[j, i] += ( - self.data.neoclassics.densities[k] - * (z[j] * z[k]) ** 2 - * lnlambda - * phixmgx - / (4.0 * np.pi * constants.EPSILON0**2 * mass[j] ** 2 * v**3) - ) - - return neoclassics_calc_nu + roots = self.data.neoclassics.roots + temp = self.data.neoclassics.temperatures - def neoclassics_calc_nu_star(self): - """Calculates the normalized collision frequency""" - k = np.repeat(self.data.neoclassics.roots[:, np.newaxis], 4, axis=1) - kk = (k * self.data.neoclassics.temperatures).T + inv_mass = 1 / mass + xk = np.einsum("k,j,j,k,r->jkr", mass, inv_mass, temp, 1 / temp, roots) - mass = np.array([ - constants.ELECTRON_MASS, - constants.PROTON_MASS * 2.0, - constants.PROTON_MASS * 3.0, - constants.PROTON_MASS * 4.0, - ]) + expxk = np.exp(-xk) - v = np.empty((4, self.no_roots)) - v[0, :] = constants.SPEED_LIGHT * np.sqrt( - 1.0 - (kk[0, :] / (mass[0] * constants.SPEED_LIGHT**2) + 1) ** (-1) - ) - v[1, :] = constants.SPEED_LIGHT * np.sqrt( - 1.0 - (kk[1, :] / (mass[1] * constants.SPEED_LIGHT**2) + 1) ** (-1) - ) - v[2, :] = constants.SPEED_LIGHT * np.sqrt( - 1.0 - (kk[2, :] / (mass[2] * constants.SPEED_LIGHT**2) + 1) ** (-1) + erfn = self._nu_erfn(xk, expxk) + + phixmgx = (1.0 - 0.5 * 1 / xk) * erfn + expxk / np.sqrt(np.pi * xk) + + v = np.sqrt(2.0 * np.einsum("r,j,j->jr", roots, temp, inv_mass)) + denom = 1 / (4 * np.pi * constants.EPSILON0**2 * mass[:, None] ** 2 * v**3) + + return lnlambda * np.einsum( + "k,jk,jkr,jr->jr", + self.data.neoclassics.densities, + np.einsum("i,j->ij", z, z) ** 2, + phixmgx, + denom, ) - v[3, :] = constants.SPEED_LIGHT * np.sqrt( - 1.0 - (kk[3, :] / (mass[3] * constants.SPEED_LIGHT**2) + 1) ** (-1) + + def neoclassics_calc_nu_star(self): + """Calculates the normalized collision frequency""" + kk = ( + self.data.neoclassics.roots[None] + * self.data.neoclassics.temperatures[:, None] ) + mass = self.mass + + v = constants.SPEED_LIGHT * np.sqrt( + 1.0 - (kk / (mass[:, None] * constants.SPEED_LIGHT**2) + 1) ** -1 + ) return ( self.data.physics.rmajor * self.data.neoclassics.nu @@ -557,15 +414,13 @@ def neoclassics_calc_nu_star_fromT(self, iota): """ - temp = ( - np.array([ - self.data.physics.temp_plasma_electron_vol_avg_kev, - self.data.physics.temp_plasma_ion_vol_avg_kev, - self.data.physics.temp_plasma_ion_vol_avg_kev, - self.data.physics.temp_plasma_ion_vol_avg_kev, - ]) - * KEV - ) + temp = KEV * np.array([ + self.data.physics.temp_plasma_electron_vol_avg_kev, + self.data.physics.temp_plasma_ion_vol_avg_kev, + self.data.physics.temp_plasma_ion_vol_avg_kev, + self.data.physics.temp_plasma_ion_vol_avg_kev, + ]) + density = np.array([ self.data.physics.nd_plasma_electrons_vol_avg, self.data.physics.nd_plasma_fuel_ions_vol_avg @@ -575,12 +430,7 @@ def neoclassics_calc_nu_star_fromT(self, iota): self.data.physics.nd_plasma_alphas_thermal_vol_avg, ]) - mass = np.array([ - constants.ELECTRON_MASS, - constants.PROTON_MASS * 2.0, - constants.PROTON_MASS * 3.0, - constants.PROTON_MASS * 4.0, - ]) + mass = self.mass z = np.array([-1.0, 1.0, 1.0, 2.0]) * constants.ELECTRON_CHARGE # transform the temperature back in eV @@ -592,120 +442,57 @@ def neoclassics_calc_nu_star_fromT(self, iota): + 2.3 * np.log10(temp[0] / constants.ELECTRON_CHARGE) ) - neoclassics_calc_nu_star_fromT = np.zeros((4,)) - - for j in range(4): - v = np.sqrt(2.0 * temp[j] / mass[j]) - for k in range(4): - xk = (mass[k] / mass[j]) * (temp[j] / temp[k]) - - expxk = 0.0 - if xk < 200.0: - expxk = np.exp(-xk) - - t = 1.0 / (1.0 + 0.3275911 * np.sqrt(xk)) - erfn = ( - 1.0 - - t - * ( - 0.254829592 - + t - * ( - -0.284496736 - + t * (1.421413741 + t * (-1.453152027 + t * 1.061405429)) - ) - ) - * expxk - ) - phixmgx = (1.0 - 0.5 / xk) * erfn + expxk / np.sqrt(np.pi * xk) - neoclassics_calc_nu_star_fromT[j] += ( - density[k] - * (z[j] * z[k]) ** 2 - * lnlambda - * phixmgx - / (4.0 * np.pi * constants.EPSILON0**2 * mass[j] ** 2 * v**4) - * self.data.physics.rmajor - / iota - ) - return neoclassics_calc_nu_star_fromT + inv_mass = 1 / mass + v = np.sqrt(2.0 * temp * inv_mass) + xk = np.einsum("k,j,j,k->jk", mass, inv_mass, temp, 1 / temp) - def neoclassics_calc_vd(self): - """Calculates the drift velocity on GL roots""" - vde = ( - self.data.neoclassics.roots - * self.data.neoclassics.temperatures[0] - / ( - constants.ELECTRON_CHARGE - * self.data.physics.rmajor - * self.data.physics.b_plasma_toroidal_on_axis - ) - ) - vdD = ( - self.data.neoclassics.roots - * self.data.neoclassics.temperatures[1] - / ( - constants.ELECTRON_CHARGE - * self.data.physics.rmajor - * self.data.physics.b_plasma_toroidal_on_axis + # exp(-xk), clipped at xk >= 200 + mask_xk_lt_200 = xk < 200.0 + expxk = np.zeros_like(xk) + expxk[mask_xk_lt_200] = np.exp(-xk[mask_xk_lt_200]) + + erfn = self._nu_erfn(xk, expxk) + + phixmgx = (1.0 - 0.5 * 1 / xk) * erfn + expxk / np.sqrt(np.pi * xk) + + denom = 1 / (4 * np.pi * constants.EPSILON0**2 * mass**2 * v**4) + + # sum over k dimension + return ( + lnlambda + * self.data.physics.rmajor + / iota + * np.einsum( + "k,jk,jk,j->j", density, np.einsum("i,j->ij", z, z) ** 2, phixmgx, denom ) ) - vdT = ( - self.data.neoclassics.roots - * self.data.neoclassics.temperatures[2] - / ( - constants.ELECTRON_CHARGE - * self.data.physics.rmajor - * self.data.physics.b_plasma_toroidal_on_axis - ) + + def neoclassics_calc_vd(self): + """Calculates the drift velocity on GL roots""" + # alpha denominator is 2*vd_suffix + vd_suffix = ( + constants.ELECTRON_CHARGE + * self.data.physics.rmajor + * self.data.physics.b_plasma_toroidal_on_axis + * np.array([1, 1, 1, 2]) ) - vda = ( - self.data.neoclassics.roots - * self.data.neoclassics.temperatures[3] - / ( - 2.0 - * constants.ELECTRON_CHARGE - * self.data.physics.rmajor - * self.data.physics.b_plasma_toroidal_on_axis - ) + return ( + self.data.neoclassics.roots[None] + * self.data.neoclassics.temperatures[:, None] + / vd_suffix[:, None] ) - vd = np.empty((4, self.no_roots)) - - vd[0, :] = vde - vd[1, :] = vdD - vd[2, :] = vdT - vd[3, :] = vda - - return vd - def neoclassics_calc_D11_plateau(self): """Calculates the plateau transport coefficients (D11_star sometimes)""" - mass = np.array([ - constants.ELECTRON_MASS, - constants.PROTON_MASS * 2.0, - constants.PROTON_MASS * 3.0, - constants.PROTON_MASS * 4.0, - ]) + mass = self.mass - v = np.empty((4, self.no_roots)) - v[0, :] = constants.SPEED_LIGHT * np.sqrt( + v = constants.SPEED_LIGHT * np.sqrt( 1.0 - - (self.data.neoclassics.kt[0, :] / (mass[0] * constants.SPEED_LIGHT**2) + 1) - ** (-1) - ) - v[1, :] = constants.SPEED_LIGHT * np.sqrt( - 1.0 - - (self.data.neoclassics.kt[1, :] / (mass[1] * constants.SPEED_LIGHT**2) + 1) - ** (-1) - ) - v[2, :] = constants.SPEED_LIGHT * np.sqrt( - 1.0 - - (self.data.neoclassics.kt[2, :] / (mass[2] * constants.SPEED_LIGHT**2) + 1) - ** (-1) - ) - v[3, :] = constants.SPEED_LIGHT * np.sqrt( - 1.0 - - (self.data.neoclassics.kt[3, :] / (mass[3] * constants.SPEED_LIGHT**2) + 1) + - ( + self.data.neoclassics.kt[None] + / (mass[:, None] * constants.SPEED_LIGHT**2) + + 1 + ) ** (-1) ) diff --git a/process/models/stellarator/stellarator.py b/process/models/stellarator/stellarator.py index d9b58d781c..93d0076094 100644 --- a/process/models/stellarator/stellarator.py +++ b/process/models/stellarator/stellarator.py @@ -2406,24 +2406,10 @@ def st_phys(self, output): ( q_PROCESS, q_PROCESS_r1, - _q_neo, - _gamma_neo, - _total_q_neo, total_q_neo_e, q_neo_e, - _q_neo_D, - _q_neo_a, - _q_neo_T, g_neo_e, - _g_neo_D, - _g_neo_a, - _g_neo_T, dndt_neo_e, - _dndt_neo_D, - _dndt_neo_a, - _dndt_neo_T, - _dndt_neo_fuel, - _dmdt_neo_fuel, dmdt_neo_fuel_from_e, chi_neo_e, chi_PROCESS_e, From f9dcdb286b6c92d9232559823de4738da7641125 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Tue, 29 Sep 2026 13:45:55 +0100 Subject: [PATCH 02/18] typing --- process/core/io/plot/summary.py | 156 ++++++++++++++------------------ 1 file changed, 68 insertions(+), 88 deletions(-) diff --git a/process/core/io/plot/summary.py b/process/core/io/plot/summary.py index 6d1379d3d5..863d7626f7 100644 --- a/process/core/io/plot/summary.py +++ b/process/core/io/plot/summary.py @@ -418,17 +418,12 @@ def poloidal_cross_section( plot_tf_coils(axis, mfile, scan, colour_scheme) plot_pf_coils(axis, mfile, scan, colour_scheme) - # Ranges - # --- - # DEMO : Fixed ranges for comparison if demo_ranges: - axis.set_ylim([-15, 15]) - axis.set_xlim([0, 20]) + axis.set_ylim(-15, 15) + axis.set_xlim(0, 20) - # Adaptive ranges else: - axis.set_xlim([0, axis.get_xlim()[1]]) - # --- + axis.set_xlim(0, axis.get_xlim()[1]) def plot_full_machine_poloidal_cross_section( @@ -521,7 +516,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur # Display the plasma image over the figure, not the axes new_ax = axis.inset_axes( - [-0.15, 0.6, 0.45, 0.45], transform=axis.transAxes, zorder=1 + (-0.15, 0.6, 0.45, 0.45), transform=axis.transAxes, zorder=1 ) new_ax.imshow(plasma) new_ax.axis("off") @@ -542,7 +537,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur neutron = mpimg.imread(img_path.open("rb")) new_ax = axis.inset_axes( - [0.2, 0.85, 0.03, 0.03], transform=axis.transAxes, zorder=10 + (0.2, 0.85, 0.03, 0.03), transform=axis.transAxes, zorder=10 ) new_ax.imshow(neutron) new_ax.axis("off") @@ -689,7 +684,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur # Display the alpha particle image over the figure, not the axes new_ax = axis.inset_axes( - [0.16, 0.95, 0.025, 0.025], transform=axis.transAxes, zorder=10 + (0.16, 0.95, 0.025, 0.025), transform=axis.transAxes, zorder=10 ) new_ax.imshow(alpha) new_ax.axis("off") @@ -775,11 +770,11 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur # Display the injector image over the figure, not the axes new_ax = axis.inset_axes( - [-0.2, 0.8, 0.15, 0.15], transform=axis.transAxes, zorder=10 + (-0.2, 0.8, 0.15, 0.15), transform=axis.transAxes, zorder=10 ) new_ax.imshow(hcd_injector_1) new_ax.axis("off") - new_ax = axis.inset_axes([-0.2, 0.5, 0.15, 0.5], transform=axis.transAxes, zorder=10) + new_ax = axis.inset_axes((-0.2, 0.5, 0.15, 0.5), transform=axis.transAxes, zorder=10) new_ax.imshow(hcd_injector_2) new_ax.axis("off") @@ -1042,7 +1037,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur turbine = mpimg.imread(img_path.open("rb")) # Display the turbine image over the figure, not the axes - new_ax = axis.inset_axes([1.1, 0.0, 0.15, 0.15], transform=axis.transAxes, zorder=10) + new_ax = axis.inset_axes((1.1, 0.0, 0.15, 0.15), transform=axis.transAxes, zorder=10) new_ax.imshow(turbine) new_ax.axis("off") @@ -1099,7 +1094,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur # Display the generator image over the figure, not the axes new_ax = axis.inset_axes( - [0.96, 0.0, 0.15, 0.15], transform=axis.transAxes, zorder=10 + (0.96, 0.0, 0.15, 0.15), transform=axis.transAxes, zorder=10 ) new_ax.imshow(generator) new_ax.axis("off") @@ -1168,7 +1163,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur # Display the pylon image over the figure, not the axes new_ax = axis.inset_axes( - [0.925, -0.1, 0.1, 0.1], transform=axis.transAxes, zorder=10 + (0.925, -0.1, 0.1, 0.1), transform=axis.transAxes, zorder=10 ) new_ax.imshow(pylon) new_ax.axis("off") @@ -1376,7 +1371,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur fw = mpimg.imread(img_path.open("rb")) # Display the first wall image over the figure, not the axes - new_ax = axis.inset_axes([0.4, 0.625, 0.4, 0.4], transform=axis.transAxes, zorder=10) + new_ax = axis.inset_axes((0.4, 0.625, 0.4, 0.4), transform=axis.transAxes, zorder=10) new_ax.imshow(fw) new_ax.axis("off") @@ -1635,7 +1630,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur # Display the blanket image over the figure, not the axes new_ax = axis.inset_axes( - [0.75, 0.625, 0.4, 0.4], transform=axis.transAxes, zorder=10 + (0.75, 0.625, 0.4, 0.4), transform=axis.transAxes, zorder=10 ) new_ax.imshow(blanket) new_ax.axis("off") @@ -1678,7 +1673,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur # Display the vacuum vessel image over the figure, not the axes new_ax = axis.inset_axes( - [0.975, 0.625, 0.4, 0.4], transform=axis.transAxes, zorder=10 + (0.975, 0.625, 0.4, 0.4), transform=axis.transAxes, zorder=10 ) new_ax.imshow(vv) new_ax.axis("off") @@ -1781,7 +1776,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur divertor = mpimg.imread(img_path.open("rb")) # Display the divertor image over the figure, not the axes - new_ax = axis.inset_axes([0.1, 0.4, 0.3, 0.25], transform=axis.transAxes, zorder=10) + new_ax = axis.inset_axes((0.1, 0.4, 0.3, 0.25), transform=axis.transAxes, zorder=10) new_ax.imshow(divertor) new_ax.axis("off") @@ -2888,7 +2883,7 @@ def plot_main_plasma_information( # Display the neutron image over the figure, not the axes new_ax = axis.inset_axes( - [0.975, 0.275, 0.075, 0.075], transform=axis.transAxes, zorder=10 + (0.975, 0.275, 0.075, 0.075), transform=axis.transAxes, zorder=10 ) new_ax.imshow(alpha_particle) new_ax.axis("off") @@ -2921,7 +2916,7 @@ def plot_main_plasma_information( ) as neutron_image_path: neutron = mpimg.imread(neutron_image_path.open("rb")) new_ax = axis.inset_axes( - [0.975, 0.75, 0.075, 0.075], transform=axis.transAxes, zorder=10 + (0.975, 0.75, 0.075, 0.075), transform=axis.transAxes, zorder=10 ) new_ax.imshow(neutron) new_ax.axis("off") @@ -3146,26 +3141,14 @@ def plot_main_plasma_information( ) # Add radiation label - axis.text( - 0.725, - 0.78, - "$\\gamma$", - fontsize=23, - verticalalignment="top", - transform=fig.transFigure, - ) + axis.text(0.725, 0.78, "$\\gamma$", **text_args) # Add L-H threshold information - textstr_lh = ( - f"$\\mathbf{{L-H \\ threshold:}}$\n" - f"({PlasmaConfinementTransitionModel(int(mfile.get('i_l_h_threshold', scan=scan))).full_name})\n\n" - f"$P_{{\\text{{L-H}}}}:$ {mfile.get('p_l_h_threshold_mw', scan=scan):.4f} MW\n" - ) - - # Wrap long model names to new line model_name = PlasmaConfinementTransitionModel( int(mfile.get("i_l_h_threshold", scan=scan)) ).full_name + + # Wrap long model names to new line if len(model_name) > 20: model_name = "\n".join(textwrap.wrap(model_name, width=20)) @@ -3507,8 +3490,8 @@ def plot_cryostat( def color_key(axis: plt.Axes, mfile: MFile, scan: int, colour_scheme: Literal[1, 2]): """Function to plot the colour key""" - axis.set_ylim([0, 10]) - axis.set_xlim([0, 10]) + axis.set_ylim(0, 10) + axis.set_xlim(0, 10) axis.set_axis_off() axis.set_autoscaley_on(False) axis.set_autoscalex_on(False) @@ -3552,7 +3535,7 @@ def color_key(axis: plt.Axes, mfile: MFile, scan: int, colour_scheme: Literal[1, axis.text(x_pos, y_pos, text, ha="left", va="top", size="small") axis.add_patch( patches.Rectangle( - [x_pos + 1.5, y_pos - 0.35], + (x_pos + 1.5, y_pos - 0.35), 0.5, 0.4, lw=0 if color != "none" else 1, @@ -3795,13 +3778,13 @@ def calc_xy(rt, e=e): # --- # DEMO : Fixed ranges for comparison if demo_ranges: - axis.set_ylim([0, 20]) - axis.set_xlim([0, 20]) + axis.set_ylim(0, 20) + axis.set_xlim(0, 20) # Adaptive ranges else: - axis.set_ylim([0.0, axis.get_ylim()[1]]) - axis.set_xlim([0.0, axis.get_xlim()[1]]) + axis.set_ylim(0.0, axis.get_ylim()[1]) + axis.set_xlim(0.0, axis.get_xlim()[1]) # --- @@ -4050,14 +4033,14 @@ def plot_n_profiles(prof, demo_ranges: bool, mfile: MFile, scan: int): # Ranges # --- # DEMO : Fixed ranges for comparison - ax_main.set_xlim([0, 1]) - ax_impurity.set_xlim([0, 1]) + ax_main.set_xlim(0, 1) + ax_impurity.set_xlim(0, 1) if demo_ranges: - ax_main.set_ylim([0, 20]) + ax_main.set_ylim(0, 20) # Adaptive ranges else: - ax_main.set_ylim([0, ax_main.get_ylim()[1]]) + ax_main.set_ylim(0, ax_main.get_ylim()[1]) # Use logarithmic scale for impurity axis if any impurity values are very small impurity_data = [ imp_frac[i] * ne / 1e16 @@ -4067,7 +4050,7 @@ def plot_n_profiles(prof, demo_ranges: bool, mfile: MFile, scan: int): if impurity_data and np.min(impurity_data) / np.max(impurity_data) < 0.01: # If range spans more than 100x, use log scale ax_impurity.set_yscale("log") - ax_impurity.set_ylim([1e-3, ax_impurity.get_ylim()[1]]) + ax_impurity.set_ylim(1e-3, ax_impurity.get_ylim()[1]) if i_plasma_pedestal != 0: # Print pedestal lines @@ -4312,14 +4295,14 @@ def plot_t_profiles(prof, demo_ranges: bool, mfile: MFile, scan: int): # Ranges # --- - prof.set_xlim([0, 1]) + prof.set_xlim(0, 1) # DEMO : Fixed ranges for comparison if demo_ranges: - prof.set_ylim([0, 50]) + prof.set_ylim(0, 50) # Adaptive ranges else: - prof.set_ylim([0, prof.get_ylim()[1]]) + prof.set_ylim(0, prof.get_ylim()[1]) if i_plasma_pedestal != 0: # Plot pedestal lines @@ -4421,14 +4404,14 @@ def plot_qprofile(prof, demo_ranges: bool, mfile: MFile, scan: int): # Ranges # --- - prof.set_xlim([0, 1]) + prof.set_xlim(0, 1) # DEMO : Fixed ranges for comparison if demo_ranges: - prof.set_ylim([0, 10]) + prof.set_ylim(0, 10) # Adaptive ranges else: - prof.set_ylim([0, q95 * 1.2]) + prof.set_ylim(0, q95 * 1.2) prof.text( 0.6, @@ -4684,16 +4667,16 @@ def plot_line_brem_power_density_profile( # Ranges # --- axis.legend(loc="upper left", bbox_to_anchor=(-0.1, -0.1), ncol=4) - axis.set_xlim([0, 1.0]) + axis.set_xlim(0, 1.0) axis.set_yscale("log") axis.yaxis.grid(True, which="both", alpha=0.2) # DEMO : Fixed ranges for comparison if demo_ranges: - axis.set_ylim([1e-4, 0.5]) + axis.set_ylim(1e-4, 0.5) # Adaptive ranges else: - axis.set_ylim([1e-4, axis.get_ylim()[1]]) + axis.set_ylim(1e-4, axis.get_ylim()[1]) # --- @@ -5628,8 +5611,8 @@ def plot_first_wall_top_down_cross_section(axis: plt.Axes, mfile: MFile, scan: i axis.set_xlabel("X [cm]") axis.set_ylabel("R [cm]") axis.set_title("First Wall Top-Down Cross Section") - axis.set_xlim([-1, 2 * dx_fw_module + 1]) - axis.set_ylim([-1, 2 * (dr_fw_wall + radius_fw_channel) + 1]) + axis.set_xlim(-1, 2 * dx_fw_module + 1) + axis.set_ylim(-1, 2 * (dr_fw_wall + radius_fw_channel) + 1) def plot_first_wall_poloidal_cross_section(axis: plt.Axes, mfile: MFile, scan: int): @@ -5750,8 +5733,8 @@ def plot_first_wall_poloidal_cross_section(axis: plt.Axes, mfile: MFile, scan: i axis.set_xlabel("R [m]") axis.set_ylabel("Z [m]") axis.set_title("First Wall Poloidal Cross Section") - axis.set_xlim([-0.01, (dx_fw_module + radius_fw_channel * 2) + 0.01]) - axis.set_ylim([-0.2, len_fw_channel + 0.2]) + axis.set_xlim(-0.01, (dx_fw_module + radius_fw_channel * 2) + 0.01) + axis.set_ylim(-0.2, len_fw_channel + 0.2) def plot_firstwall( @@ -8393,8 +8376,8 @@ def plot_header(axis: plt.Axes, mfile: MFile, scan: int): ymin = -16 ymax = 1 - axis.set_ylim([ymin, ymax]) - axis.set_xlim([xmin, xmax]) + axis.set_ylim(ymin, ymax) + axis.set_xlim(xmin, xmax) axis.set_axis_off() axis.set_autoscaley_on(False) axis.set_autoscalex_on(False) @@ -8516,8 +8499,8 @@ def plot_geometry_info(axis: plt.Axes, mfile: MFile, scan: int): ymax = 1 axis.text(-0.05, 1, "Geometry:", ha="left", va="center") - axis.set_ylim([ymin, ymax]) - axis.set_xlim([xmin, xmax]) + axis.set_ylim(ymin, ymax) + axis.set_xlim(xmin, xmax) axis.set_axis_off() axis.set_autoscaley_on(False) axis.set_autoscalex_on(False) @@ -8565,8 +8548,8 @@ def plot_physics_info(axis: plt.Axes, mfile: MFile, scan: int): ymax = 1 axis.text(-0.05, 1, "Physics:", ha="left", va="center") - axis.set_ylim([ymin, ymax]) - axis.set_xlim([xmin, xmax]) + axis.set_ylim(ymin, ymax) + axis.set_xlim(xmin, xmax) axis.set_axis_off() axis.set_autoscaley_on(False) axis.set_autoscalex_on(False) @@ -8645,8 +8628,8 @@ def plot_magnetics_info(axis: plt.Axes, mfile: MFile, scan: int): ymax = 1 axis.text(-0.05, 1, "Coil currents etc:", ha="left", va="center") - axis.set_ylim([ymin, ymax]) - axis.set_xlim([xmin, xmax]) + axis.set_ylim(ymin, ymax) + axis.set_xlim(xmin, xmax) axis.set_axis_off() axis.set_autoscaley_on(False) axis.set_autoscalex_on(False) @@ -8771,8 +8754,8 @@ def plot_power_info(axis: plt.Axes, mfile: MFile, scan: int): ymax = 1 axis.text(-0.05, 1, "Power flows:", ha="left", va="center") - axis.set_ylim([ymin, ymax]) - axis.set_xlim([xmin, xmax]) + axis.set_ylim(ymin, ymax) + axis.set_xlim(xmin, xmax) axis.set_axis_off() axis.set_autoscaley_on(False) axis.set_autoscalex_on(False) @@ -8904,8 +8887,8 @@ def plot_current_drive_info(axis: plt.Axes, mfile: MFile, scan: int): if i_hcd_secondary == 2: secondary_heating = "ICCD" - axis.set_ylim([ymin, ymax]) - axis.set_xlim([xmin, xmax]) + axis.set_ylim(ymin, ymax) + axis.set_xlim(xmin, xmax) axis.set_axis_off() axis.set_autoscaley_on(False) axis.set_autoscalex_on(False) @@ -9322,11 +9305,11 @@ def plot_brunner_divertor_power_split_comparison_stackplot( alpha=0.5, label="$\u0394 r_{\\mathrm{sep}}$", ) - axis.set_ylim([0.0, 1.0]) - axis.set_xlim([ + axis.set_ylim(0.0, 1.0) + axis.set_xlim( -5 * len_plasma_sol_outboard_pd, 5 * len_plasma_sol_outboard_pd, - ]) + ) axis.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.35) axis.set_title("Brunner Divertor Power Split Fractions") axis.set_xlabel("$\\Delta r_{\\mathrm{sep}}$ [m]") @@ -12235,9 +12218,9 @@ def plot_fusion_rate_profiles(axis: plt.Axes, fig, mfile: MFile, scan: int): ) axis.set_yscale("log") axis.grid(True, which="both", linestyle="--", alpha=0.5) - axis.set_xlim([0, 1.025]) + axis.set_xlim(0, 1.025) axis.minorticks_on() - axis.set_ylim([1e10, 1e23]) + axis.set_ylim(1e10, 1e23) axis.yaxis.set_major_locator(plt.LogLocator(base=10.0, numticks=10)) axis.yaxis.set_minor_locator( plt.LogLocator(base=10.0, subs=np.arange(1, 10) * 0.1, numticks=100) @@ -12689,7 +12672,7 @@ def plot_plasma_pressure_profiles(axis: plt.Axes, mfile: MFile, scan: int): axis.grid(which="minor", linestyle=":", linewidth=0.5, alpha=0.5) axis.set_title("Plasma Thermal Pressure Profiles") axis.grid(True, linestyle="--", alpha=0.5) - axis.set_xlim([0, 1.025]) + axis.set_xlim(0, 1.025) axis.set_ylim(bottom=0) axis.legend() @@ -13349,10 +13332,7 @@ def reaction_plot_grid( ax.set_xlabel("R [m]") ax.set_xlim(rmajor - 1.2 * rminor, rmajor + 1.2 * rminor) - ax.set_ylim( - -1.2 * rminor * kappa, - 1.2 * kappa * rminor, - ) + ax.set_ylim(-1.2 * rminor * kappa, 1.2 * kappa * rminor) ax.set_ylabel("Z [m]") ax.plot( rmajor, @@ -17170,14 +17150,14 @@ def _add_page(name: str | None = None): demo_ranges, colour_scheme, ) - ax_full_toroidal.set_ylim([ + ax_full_toroidal.set_ylim( -ax_full_toroidal.get_ylim()[1], ax_full_toroidal.get_ylim()[1], - ]) - ax_full_toroidal.set_xlim([ + ) + ax_full_toroidal.set_xlim( -ax_full_toroidal.get_xlim()[1], ax_full_toroidal.get_xlim()[1], - ]) + ) ax18 = _add_page().add_subplot(211) ax18.set_position([0.1, 0.33, 0.8, 0.6]) From 5434c6246a613b5d67bf64c332134df5fe2d76be Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Tue, 29 Sep 2026 13:46:23 +0100 Subject: [PATCH 03/18] dedupe --- process/core/io/plot/summary.py | 1033 +++++++------------------------ 1 file changed, 234 insertions(+), 799 deletions(-) diff --git a/process/core/io/plot/summary.py b/process/core/io/plot/summary.py index 863d7626f7..bc4dd7a738 100644 --- a/process/core/io/plot/summary.py +++ b/process/core/io/plot/summary.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from importlib import resources from pathlib import Path -from typing import Any, Literal +from typing import Any, Literal, TypedDict import matplotlib as mpl import matplotlib.backends.backend_pdf as bpdf @@ -15,6 +15,7 @@ from matplotlib import patches from matplotlib.patches import Circle, Rectangle from matplotlib.path import Path as mplPath +from matplotlib.transforms import Transform from scipy.interpolate import interp1d from process.core import constants @@ -184,6 +185,22 @@ class RadialBuild: rtangle2 = 2 * rtangle +def _box_style(colour: str): + return {"boxstyle": "round", "facecolor": colour, "alpha": 1.0, "linewidth": 2} + + +white_box = {"boxstyle": "round", "facecolor": "white", "alpha": 1.0} + + +def text_layout(fig): + return { + "fontsize": 9, + "verticalalignment": "bottom", + "horizontalalignment": "left", + "transform": fig.transFigure, + } + + def plot_plasma( axis: plt.Axes, mfile: MFile, @@ -712,10 +729,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.37, 0.775, f"$P_{{\\text{{neutron}}}}$:\n{mfile.get('p_neutron_total_mw', scan=scan):,.2f} MW", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "grey", @@ -735,16 +749,8 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.0725, 0.83, f"$P_{{\\text{{HCD,primary}}}}$: {mfile.get('p_hcd_primary_injected_mw', scan=scan) + mfile.get('p_hcd_primary_extra_heat_mw', scan=scan):.2f} MW", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lightyellow", - "alpha": 1.0, - "linewidth": 2, - }, + **text_layout(fig), + bbox=_box_style("lightyellow"), ) # Add HCD secondary injected power @@ -752,16 +758,8 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.0725, 0.725, f"$P_{{\\text{{HCD,secondary}}}}$: {mfile.get('p_hcd_secondary_injected_mw', scan=scan) + mfile.get('p_hcd_secondary_extra_heat_mw', scan=scan):.2f} MW", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lightyellow", - "alpha": 1.0, - "linewidth": 2, - }, + **text_layout(fig), + bbox=_box_style("lightyellow"), ) # Load the HCD injector image @@ -810,16 +808,8 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.04, 0.45, "\n\nH&CD Power Supply\n\n", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lightyellow", - "alpha": 1.0, - "linewidth": 2, - }, + **text_layout(fig), + bbox=_box_style("lightyellow"), zorder=4, ) @@ -844,17 +834,8 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.2, 0.435, f"$P_{{\\text{{secondary,loss}}}}$:\n{mfile.get('p_hcd_secondary_electric_mw', scan=scan) * (1.0 - mfile.get('eta_hcd_secondary_injector_wall_plug', scan=scan)):.2f} MWe", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lightblue", - "alpha": 1.0, - "linewidth": 2, - "linestyle": "dashed", - }, + **text_layout(fig), + bbox=_box_style("lightblue") | {"linestyle": "dashed"}, ) # Draw an arrow from HCD secondary losses to the total secondary heat power @@ -926,17 +907,8 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.2, 0.485, f"$P_{{\\text{{primary,loss}}}}$:\n{mfile.get('p_hcd_primary_electric_mw', scan=scan) * (1.0 - mfile.get('eta_hcd_primary_injector_wall_plug', scan=scan)):.2f} MWe", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lightblue", - "alpha": 1.0, - "linewidth": 2, - "linestyle": "dashed", - }, + **text_layout(fig), + bbox=_box_style("lightblue") | {"linestyle": "dashed"}, ) # Draw arrow from HCD primary electric box to HCD power supply box @@ -972,16 +944,8 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.12, 0.35, f"$P_{{\\text{{secondary}}}}$:\n{mfile.get('p_hcd_secondary_electric_mw', scan=scan):.2f} MWe \n$\\eta$: {mfile.get('eta_hcd_secondary_injector_wall_plug', scan=scan):.2f}", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lightyellow", - "alpha": 1.0, - "linewidth": 2, - }, + **text_layout(fig), + bbox=_box_style("lightyellow"), ) # Plot HCD primary electric box @@ -989,16 +953,8 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.025, 0.35, f"$P_{{\\text{{primary}}}}$:\n{mfile.get('p_hcd_primary_electric_mw', scan=scan):.2f} MWe\n$\\eta$: {mfile.get('eta_hcd_primary_injector_wall_plug', scan=scan):.2f}", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lightyellow", - "alpha": 1.0, - "linewidth": 2, - }, + **text_layout(fig), + bbox=_box_style("lightyellow"), ) # ============================================= @@ -1016,13 +972,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur verticalalignment="bottom", horizontalalignment="center", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lightblue", - "alpha": 1.0, - "linewidth": 2, - "linestyle": "dashed", - }, + bbox=_box_style("lightblue") | {"linestyle": "dashed"}, zorder=4, ) @@ -1046,16 +996,8 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.9, 0.25, f"$P_{{\\text{{primary,thermal}}}}$:\n{mfile.get('p_plant_primary_heat_mw', scan=scan):,.2f} MW \n$\\eta_{{\\text{{turbine}}}}$: {mfile.get('eta_turbine', scan=scan):.3f}", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "orange", - "alpha": 1.0, - "linewidth": 2, - }, + **text_layout(fig), + bbox=_box_style("orange"), ) # Draw arrow from bend to turbine inlet @@ -1119,10 +1061,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.79, 0.16, "Generator", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), zorder=20, ) @@ -1173,16 +1112,8 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.68, 0.15, f"$P_{{\\text{{gross}}}}$:\n{mfile.get('p_plant_electric_gross_mw', scan=scan):,.2f} MWe", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lime", - "alpha": 1.0, - "linewidth": 2, - }, + **text_layout(fig), + bbox=_box_style("lime"), ) # Gross to net electric power @@ -1205,17 +1136,8 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.875, 0.05, f"$P_{{\\text{{loss}}}}$:\n{mfile.get('p_turbine_loss_mw', scan=scan):,.2f} MWth", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "orange", - "alpha": 1.0, - "linewidth": 2, - "linestyle": "dashed", - }, + **text_layout(fig), + bbox=_box_style("orange") | {"linestyle": "dashed"}, ) # Shield primary thermal to plant total primary thermal arrow @@ -1238,16 +1160,8 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.68, 0.05, f"$P_{{\\text{{net,electric}}}}$:\n{mfile.get('p_plant_electric_net_mw', scan=scan):,.2f} MWe", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lime", - "alpha": 1.0, - "linewidth": 2, - }, + **text_layout(fig), + bbox=_box_style("lime"), ) # Plot the recirculated electric power box @@ -1258,16 +1172,8 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur f"$P_{{\\text{{recirc,electric}}}}$:\n{mfile.get('p_plant_electric_recirc_mw', scan=scan):,.2f} MWe\n" f"$f_{{\\text{{recirc}}}}$:\n{mfile.get('f_p_plant_electric_recirc', scan=scan):,.2f}" ), - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lime", - "alpha": 1.0, - "linewidth": 2, - }, + **text_layout(fig), + bbox=_box_style("lime"), ) # Gross to recirculated power arrow @@ -1391,16 +1297,8 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.46, 0.85, f"$P_{{\\text{{FW, }}\\alpha}}$:\n{mfile.get('p_fw_alpha_mw', scan=scan):.2f} MW", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "red", - "alpha": 1.0, - "linewidth": 2, - }, + **text_layout(fig), + bbox=_box_style("red"), ) # Neutron power incident on first wall box @@ -1408,10 +1306,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.46, 0.775, f"$P_{{\\text{{FW,nuclear}}}}$:\n{mfile.get('p_fw_nuclear_heat_total_mw', scan=scan):,.2f} MW", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "grey", @@ -1425,10 +1320,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.46, 0.71, f"$P_{{\\text{{FW,rad}}}}$:\n{mfile.get('p_fw_rad_total_mw', scan=scan):,.2f} MW", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "dodgerblue", @@ -1487,10 +1379,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.5, 0.555, f"Primary thermal\n(inc pump): {mfile.get('p_fw_heat_deposited_mw', scan=scan):,.2f} MWth", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "orange", @@ -1503,10 +1392,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.7, 0.555, f"Primary thermal\n(inc pump): {mfile.get('p_blkt_heat_deposited_mw', scan=scan):,.2f} MWth", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "orange", @@ -1519,10 +1405,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.875, 0.555, f"Primary thermal:\n{mfile.get('p_shld_heat_deposited_mw', scan=scan):.2f} MWth", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "orange", @@ -1610,16 +1493,8 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.6, 0.49, f"Primary thermal (inc pump): {mfile.get('p_fw_blkt_heat_deposited_mw', scan=scan):,.2f} MWth\n", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "orange", - "alpha": 1.0, - "linewidth": 2, - }, + **text_layout(fig), + bbox=_box_style("orange"), ) # Load the blanket image @@ -1655,10 +1530,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur f"$P_{{\\text{{Blkt,multiplication}}}}$:\n{mfile.get('p_blkt_multiplication_mw', scan=scan):,.2f} MW\n" f"$f_{{\\text{{multiplication}}}}$:\n{mfile.get('f_p_blkt_multiplication', scan=scan):,.2f}" ), - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "grey", @@ -1694,17 +1566,8 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.38, 0.375, f"$P_{{\\text{{shld,secondary}}}}$:\n{mfile.get('p_shld_secondary_heat_mw', scan=scan):,.2f} MWth", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lightblue", - "alpha": 1.0, - "linewidth": 2, - "linestyle": "dashed", - }, + **text_layout(fig), + bbox=_box_style("lightblue") | {"linestyle": "dashed"}, ) # Shield secondary power box to secondary heat total @@ -1785,10 +1648,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.29, 0.57, f"$P_{{\\text{{div,rad}}}}$:\n{mfile.get('p_div_rad_total_mw', scan=scan):,.2f} MW", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "dodgerblue", @@ -1802,10 +1662,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.4, 0.58, f"$P_{{\\text{{div,nuclear}}}}$:\n{mfile.get('p_div_nuclear_heat_total_mw', scan=scan):,.2f} MW", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "grey", @@ -1823,10 +1680,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur f"Solid angle fraction: {mfile.get('f_ster_div_single', scan=scan):.3f}\n" f"Primary heat fraction: {mfile.get('f_p_div_primary_heat', scan=scan):.3f}" ), - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "orange", @@ -1840,17 +1694,8 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.3, 0.375, f"$P_{{\\text{{div,secondary}}}}$:\n{mfile.get('p_div_secondary_heat_mw', scan=scan):.2f} MWth", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lightblue", - "alpha": 1.0, - "linewidth": 2, - "linestyle": "dashed", - }, + **text_layout(fig), + bbox=_box_style("lightblue") | {"linestyle": "dashed"}, ) # Divertor to divertor secondary heat arrow @@ -1911,10 +1756,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.55, 0.33, f"$P_{{\\text{{div,pump}}}}$: {mfile.get('p_div_coolant_pump_mw', scan=scan):.2f} MW", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "wheat", @@ -1973,10 +1815,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.875, 0.325, f"$P_{{\\text{{shld,pump}}}}$:\n{mfile.get('p_shld_coolant_pump_mw', scan=scan):.2f} MW", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "wheat", @@ -1990,10 +1829,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.725, 0.4, f"$P_{{\\text{{FW + Blkt}}}}$:\n{mfile.get('p_fw_blkt_coolant_pump_mw', scan=scan):.2f} MW", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "wheat", @@ -2055,10 +1891,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur f"Coolant pumps electric:\n{mfile.get('p_coolant_pump_elec_total_mw', scan=scan):.3f} MWe\n" f"$\\eta$: {mfile.get('eta_coolant_pump_electric', scan=scan):.3f}" ), - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "lime", @@ -2072,10 +1905,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.7, 0.325, f"Coolant pumps total:\n{mfile.get('p_coolant_pump_total_mw', scan=scan):.3f} MW", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "wheat", @@ -2104,10 +1934,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.5, 0.235, f"Coolant pumps losses total:\n{mfile.get('p_coolant_pump_loss_total_mw', scan=scan):.3f} MWth", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "lightblue", @@ -2160,10 +1987,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.49, 0.05, f"Cryo Plant:\n{mfile.get('p_cryo_plant_electric_mw', scan=scan):.3f} MWe", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "burlywood", @@ -2192,10 +2016,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.4, 0.05, f"Tritium Plant:\n{mfile.get('p_tritium_plant_electric_mw', scan=scan):.3f} MWe", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "burlywood", @@ -2224,10 +2045,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.575, 0.05, f"Vacuum pumps:\n{mfile.get('vachtmw', scan=scan):.3f} MWe", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "burlywood", @@ -2260,10 +2078,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur f"Minimum base load:\n{mfile.get('p_plant_electric_base', scan=scan) * 1.0e-6:.3f} MWe\n" f"Plant floor power density:\n{mfile.get('pflux_plant_floor_electric', scan=scan) * 1.0e-3:.3f} kW$\\text{{m}}^{{-2}}$" ), - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "burlywood", @@ -2277,10 +2092,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.325, 0.075, f"TF coils:\n{mfile.get('p_tf_electric_supplies_mw', scan=scan):.3f} MWe", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "burlywood", @@ -2294,10 +2106,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.25, 0.05, f"PF coils:\n{mfile.get('p_pf_electric_supplies_mw', scan=scan):.3f} MWe", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "burlywood", @@ -2356,10 +2165,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.46, 0.285, f"$P_{{\\text{{HCD,loss}}}}$:\n{mfile.get('p_hcd_secondary_heat_mw', scan=scan):.2f} MWth", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "lightblue", @@ -2406,17 +2212,8 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.155, 0.25, f"$P_{{\\text{{TF,nuclear}}}}$:\n{mfile.get('p_tf_nuclear_heat_mw', scan=scan):.2f} MWth", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lightblue", - "alpha": 1.0, - "linewidth": 2, - "linestyle": "dashed", - }, + **text_layout(fig), + bbox=_box_style("lightblue") | {"linestyle": "dashed"}, ) # TF nuclear heat to secondary heat total box arrow @@ -2498,7 +2295,7 @@ def plot_main_plasma_information( fontsize=15, verticalalignment="center", horizontalalignment="center", - bbox={"boxstyle": "round", "facecolor": "white", "alpha": 1.0}, + bbox=white_box, transform=fig.transFigure, ) @@ -2520,7 +2317,7 @@ def plot_main_plasma_information( fontsize=9, color="black", ha="center", - bbox={"boxstyle": "round", "facecolor": "white", "alpha": 1.0}, + bbox=white_box, ) # ============================================ @@ -2541,7 +2338,7 @@ def plot_main_plasma_information( fontsize=9, color="black", ha="center", - bbox={"boxstyle": "round", "facecolor": "white", "alpha": 1.0}, + bbox=white_box, ) # ============================================ @@ -2564,7 +2361,7 @@ def plot_main_plasma_information( rotation=270, verticalalignment="center", transform=axis.transAxes, - bbox={"boxstyle": "round", "facecolor": "white", "alpha": 1.0}, + bbox=white_box, ) # ============================================= @@ -2586,7 +2383,7 @@ def plot_main_plasma_information( color="black", rotation=0, verticalalignment="center", - bbox={"boxstyle": "round", "facecolor": "white", "alpha": 1.0}, + bbox=white_box, ) # ============================================= @@ -2609,7 +2406,7 @@ def plot_main_plasma_information( color="black", rotation=0, verticalalignment="center", - bbox={"boxstyle": "round", "facecolor": "white", "alpha": 1.0}, + bbox=white_box, ) # ================================================ @@ -2634,37 +2431,21 @@ def plot_main_plasma_information( verticalalignment="top", horizontalalignment="left", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lightyellow", - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("lightyellow"), ) # ============================================ # Draw a red arrow coming from the right and pointing at the plasma - axis.annotate( - "", - xy=(rmajor + (rminor * 0.8), kappa * rminor * 0.2), # Pointing at the plasma - xytext=( - rmajor + (rminor * 1.4), - kappa * rminor * 0.2, - ), # Starting point of the arrow - arrowprops={"facecolor": "red", "edgecolor": "red", "lw": 2}, - ) - - # Draw a red arrow coming from the right and pointing at the plasma - axis.annotate( - "", - xy=(rmajor + (rminor * 0.8), -kappa * rminor * 0.2), # Pointing at the plasma - xytext=( - rmajor + (rminor * 1.4), - -kappa * rminor * 0.2, - ), # Starting point of the arrow - arrowprops={"facecolor": "red", "edgecolor": "red", "lw": 2}, - ) + for kap in (-kappa, kappa): + axis.annotate( + "", + # Pointing at plasma + xy=(rmajor + (rminor * 0.8), kap * rminor * 0.2), + # Starting point of arrow + xytext=(rmajor + (rminor * 1.4), kap * rminor * 0.2), + arrowprops={"facecolor": "red", "edgecolor": "red", "lw": 2}, + ) i_hcd_primary = mfile.get("i_hcd_primary", scan=scan) i_hcd_secondary = mfile.get("i_hcd_secondary", scan=scan) @@ -2695,24 +2476,22 @@ def plot_main_plasma_information( fontsize=9, verticalalignment="top", transform=plt.gcf().transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "paleturquoise", - "alpha": 1.0, - "linewidth": 2, - "edgecolor": "black", - }, + bbox=_box_style("paleturquoise") | {"edgecolor": "black"}, ) + class TextArgs(TypedDict): + fontsize: int + verticalalignment: str + transform: Transform + + text_args = TextArgs({ + "fontsize": 23, + "verticalalignment": "top", + "transform": fig.transFigure, + }) + # Add injected power label - axis.text( - 0.92, - 0.625, - "$P_{\\text{inj}}$", - fontsize=23, - verticalalignment="top", - transform=fig.transFigure, - ) + axis.text(0.92, 0.625, "$P_{\\text{inj}}$", **text_args) # ================================================ @@ -2737,23 +2516,11 @@ def plot_main_plasma_information( fontsize=9, verticalalignment="top", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lightblue", - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("lightblue"), ) # Add beta label - axis.text( - 0.27, - 0.94, - "$\\beta$", - fontsize=23, - verticalalignment="top", - transform=fig.transFigure, - ) + axis.text(0.27, 0.94, "$\\beta$", **text_args) # ================================================ @@ -2779,23 +2546,11 @@ def plot_main_plasma_information( fontsize=9, verticalalignment="top", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lightgreen", - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("lightgreen"), ) # Add volt second label - axis.text( - 0.30, - 0.77, - "Vs", - fontsize=23, - verticalalignment="top", - transform=fig.transFigure, - ) + axis.text(0.30, 0.77, "Vs", **text_args) # ========================================= @@ -2813,23 +2568,11 @@ def plot_main_plasma_information( fontsize=9, verticalalignment="top", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "orange", - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("orange"), ) # Add divertor label - axis.text( - 0.45, - 0.1, - "$P_{\\text{div}}$", - fontsize=23, - verticalalignment="top", - transform=fig.transFigure, - ) + axis.text(0.45, 0.1, "$P_{\\text{div}}$", **text_args) # ================================================ @@ -2853,24 +2596,12 @@ def plot_main_plasma_information( fontsize=9, verticalalignment="top", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "gainsboro", # Changed to a not normal color (Aquamarine) - "alpha": 1.0, - "linewidth": 2, - "edgecolor": "black", - }, + # Changed to a not normal color (Aquamarine) + bbox=_box_style("gainsboro") | {"edgecolor": "black"}, ) # Add tau label - axis.text( - 0.3, - 0.55, - "$\\tau_{\\text{e}} $", - fontsize=23, - verticalalignment="top", - transform=fig.transFigure, - ) + axis.text(0.3, 0.55, "$\\tau_{\\text{e}} $", **text_args) # ========================================= @@ -2989,13 +2720,7 @@ def plot_main_plasma_information( fontsize=9, verticalalignment="top", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "khaki", - "alpha": 1.0, - "linewidth": 2, - "edgecolor": "black", - }, + bbox=_box_style("khaki") | {"edgecolor": "black"}, ) # ================================================ @@ -3030,14 +2755,7 @@ def plot_main_plasma_information( ) # Add ion charge label - axis.text( - 0.815, - 0.29, - "$Z$", - fontsize=23, - verticalalignment="top", - transform=fig.transFigure, - ) + axis.text(0.815, 0.29, "$Z$", **text_args) # ================================================ @@ -3059,23 +2777,11 @@ def plot_main_plasma_information( fontsize=9, verticalalignment="top", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "#C8A2C8", # Hex code for lilac color - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("#C8A2C8"), # Hex code for lilac color ) # Add plasma current label - axis.text( - 0.93, - 0.9, - "$I_{\\text{p}} $", - fontsize=23, - verticalalignment="top", - transform=fig.transFigure, - ) + axis.text(0.93, 0.9, "$I_{\\text{p}} $", **text_args) # Add magnetic field information textstr_fields = ( @@ -3094,23 +2800,11 @@ def plot_main_plasma_information( fontsize=9, verticalalignment="top", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "royalblue", - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("royalblue"), ) # Add magnetic field label - axis.text( - 0.75, - 0.12, - "$B$", - fontsize=23, - verticalalignment="top", - transform=fig.transFigure, - ) + axis.text(0.75, 0.12, "$B$", **text_args) # Add radiation information textstr_radiation = ( @@ -3131,13 +2825,7 @@ def plot_main_plasma_information( fontsize=9, verticalalignment="top", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lavender", - "alpha": 1.0, - "linewidth": 2, - "edgecolor": "black", - }, + bbox=_box_style("lavender") | {"edgecolor": "black"}, ) # Add radiation label @@ -3165,12 +2853,7 @@ def plot_main_plasma_information( fontsize=9, verticalalignment="top", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "peachpuff", - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("peachpuff"), ) # Add density limit information @@ -3188,12 +2871,7 @@ def plot_main_plasma_information( fontsize=9, verticalalignment="top", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "pink", - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("pink"), ) @@ -3414,12 +3092,7 @@ def plot_system_power_profiles_over_time(axis: plt.Axes, mfile: MFile, scan: int fontsize=9, verticalalignment="top", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "grey", - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("grey"), ) # Add energy produced info @@ -3442,7 +3115,7 @@ def plot_system_power_profiles_over_time(axis: plt.Axes, mfile: MFile, scan: int fontsize=9, verticalalignment="top", transform=fig.transFigure, - bbox={"boxstyle": "round", "facecolor": "grey", "alpha": 1.0, "linewidth": 2}, + bbox=_box_style("grey"), ) @@ -7105,7 +6778,7 @@ def plot_resistive_tf_info(axis: plt.Axes, mfile: MFile, scan: int, fig): verticalalignment="top", horizontalalignment="left", transform=fig.transFigure, - bbox={"boxstyle": "round", "facecolor": "grey", "alpha": 1.0, "linewidth": 2}, + bbox=_box_style("grey"), ) # Add info about the steel casing surrounding the WP @@ -7191,12 +6864,7 @@ def plot_resistive_tf_info(axis: plt.Axes, mfile: MFile, scan: int, fig): verticalalignment="top", horizontalalignment="left", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "wheat", - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("wheat"), ) @@ -7638,12 +7306,7 @@ def _pack_strands_rectangular_with_obstacles( verticalalignment="top", horizontalalignment="left", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "red", - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("red"), ) # Add info about the steel casing surrounding the WP @@ -7660,12 +7323,7 @@ def _pack_strands_rectangular_with_obstacles( verticalalignment="top", horizontalalignment="left", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "grey", - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("grey"), ) if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.NON_INTEGER: @@ -7697,12 +7355,7 @@ def _pack_strands_rectangular_with_obstacles( verticalalignment="top", horizontalalignment="left", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "royalblue", - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("royalblue"), ) if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.NON_INTEGER: @@ -7724,15 +7377,10 @@ def _pack_strands_rectangular_with_obstacles( 0.9, textstr_turn, fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "wheat", - "alpha": 1.0, - "linewidth": 2, - }, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=_box_style("wheat"), ) # Add info about the steel casing surrounding the WP @@ -7750,12 +7398,7 @@ def _pack_strands_rectangular_with_obstacles( verticalalignment="top", horizontalalignment="left", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "white", - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("white"), ) textstr_superconductor = ( @@ -7785,12 +7428,7 @@ def _pack_strands_rectangular_with_obstacles( verticalalignment="top", horizontalalignment="left", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "#6dd3f7", # light blue for superconductors - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("#6dd3f7"), # light blue for superconductors ) @@ -7919,12 +7557,7 @@ def plot_tf_croco_turn(axis: plt.Axes, fig, mfile: MFile, scan: int): verticalalignment="top", horizontalalignment="left", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "red", - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("red"), ) # Add info about the steel casing surrounding the WP @@ -7941,12 +7574,7 @@ def plot_tf_croco_turn(axis: plt.Axes, fig, mfile: MFile, scan: int): verticalalignment="top", horizontalalignment="left", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "grey", - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("grey"), ) if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.NON_INTEGER: @@ -7978,12 +7606,7 @@ def plot_tf_croco_turn(axis: plt.Axes, fig, mfile: MFile, scan: int): verticalalignment="top", horizontalalignment="left", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "royalblue", - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("royalblue"), ) if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.NON_INTEGER: @@ -8008,12 +7631,7 @@ def plot_tf_croco_turn(axis: plt.Axes, fig, mfile: MFile, scan: int): verticalalignment="top", horizontalalignment="left", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "wheat", - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("wheat"), ) # Add info about the steel casing surrounding the WP @@ -8031,12 +7649,7 @@ def plot_tf_croco_turn(axis: plt.Axes, fig, mfile: MFile, scan: int): verticalalignment="top", horizontalalignment="left", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "white", - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("white"), ) textstr_superconductor = ( @@ -8065,12 +7678,7 @@ def plot_tf_croco_turn(axis: plt.Axes, fig, mfile: MFile, scan: int): verticalalignment="top", horizontalalignment="left", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "#6dd3f7", # light blue for superconductors - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("#6dd3f7"), ) @@ -8148,12 +7756,7 @@ def plot_cable_in_conduit_cable(axis: plt.Axes, fig, mfile: MFile, scan: int): verticalalignment="top", horizontalalignment="left", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "#cccccc", # grayish color - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("#cccccc"), ) axis.set_aspect("equal") @@ -10690,16 +10293,8 @@ def plot_cs_coil_structure( 0.5, 0.6, textstr_cs, - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lightyellow", - "alpha": 1.0, - "linewidth": 2, - }, + **text_layout(fig), + bbox=_box_style("lightyellow"), ) # Plot the current filament points as blue dots and label them @@ -10844,16 +10439,8 @@ def plot_cs_turn_structure(axis: plt.Axes, fig, mfile: MFile, scan: int): 0.7, 0.375, textstr_turn, - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lightyellow", - "alpha": 1.0, - "linewidth": 2, - }, + **text_layout(fig), + bbox=_box_style("lightyellow"), ) axis.set_xlim(-dr_cs_turn * 0.2, dr_cs_turn * 1.2) @@ -12250,16 +11837,8 @@ def plot_fusion_rate_profiles(axis: plt.Axes, fig, mfile: MFile, scan: int): 0.05, 0.85, textstr_general, - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lightyellow", - "alpha": 1.0, - "linewidth": 2, - }, + **text_layout(fig), + bbox=_box_style("lightyellow"), ) # ============================================================================ @@ -12275,16 +11854,8 @@ def plot_fusion_rate_profiles(axis: plt.Axes, fig, mfile: MFile, scan: int): 0.05, 0.75, textstr_dt, - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lightyellow", - "alpha": 1.0, - "linewidth": 2, - }, + **text_layout(fig), + bbox=_box_style("lightyellow"), ) axis.text( @@ -12308,16 +11879,8 @@ def plot_fusion_rate_profiles(axis: plt.Axes, fig, mfile: MFile, scan: int): 0.05, 0.65, textstr_dd, - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lightyellow", - "alpha": 1.0, - "linewidth": 2, - }, + **text_layout(fig), + bbox=_box_style("lightyellow"), ) axis.text( @@ -12340,16 +11903,8 @@ def plot_fusion_rate_profiles(axis: plt.Axes, fig, mfile: MFile, scan: int): 0.05, 0.55, textstr_dhe3, - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "lightyellow", - "alpha": 1.0, - "linewidth": 2, - }, + **text_layout(fig), + bbox=_box_style("lightyellow"), ) axis.text( @@ -12379,16 +11934,8 @@ def plot_fusion_rate_profiles(axis: plt.Axes, fig, mfile: MFile, scan: int): 0.05, 0.25, textstr_alpha, - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "red", - "alpha": 1.0, - "linewidth": 2, - }, + **text_layout(fig), + bbox=_box_style("red"), ) axis.text( @@ -12414,16 +11961,8 @@ def plot_fusion_rate_profiles(axis: plt.Axes, fig, mfile: MFile, scan: int): 0.05, 0.1, textstr_neutron, - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "grey", - "alpha": 1.0, - "linewidth": 2, - }, + **text_layout(fig), + bbox=_box_style("grey"), ) axis.text( @@ -12518,12 +12057,7 @@ def plot_cover_page( ha="left", va="top", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "#e0f7fa", - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("#e0f7fa"), ) # Box 2: File/Branch Info @@ -12541,12 +12075,7 @@ def plot_cover_page( ha="left", va="top", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "#fffde7", - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("#fffde7"), ) # Box 3: Run Settings @@ -12570,12 +12099,7 @@ def plot_cover_page( ha="left", va="top", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "#f3e5f5", - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("#f3e5f5"), ) axis.text( @@ -12752,12 +12276,11 @@ def plot_plasma_current_comparison(axis: plt.Axes, mfile: MFile, scan: int): } # Create the violin plot - axis.violinplot(data.values(), showextrema=False) + data_values = list(data.values()) + axis.violinplot(data_values, showextrema=False) # Create the box plot - axis.boxplot( - data.values(), showfliers=True, showmeans=True, meanline=True, widths=0.3 - ) + axis.boxplot(data_values, showfliers=True, showmeans=True, meanline=True, widths=0.3) # Scatter plot for each data point colors = plt.cm.plasma(np.linspace(0, 1, len(data.values()))) @@ -12797,7 +12320,7 @@ def plot_plasma_current_comparison(axis: plt.Axes, mfile: MFile, scan: int): axis.set_title("Plasma Current ($I_p$) Comparison") axis.set_ylabel(r"Plasma Current [MA]") axis.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x * 1e-6:.1f}")) - axis.set_xlim([0.5, 1.5]) + axis.set_xlim(0.5, 1.5) axis.set_xticks([]) axis.set_xticklabels([]) axis.set_facecolor("#f0f0f0") @@ -12831,14 +12354,12 @@ def plot_max_normalised_beta_comparison(axis: plt.Axes, mfile: MFile, scan: int) f"{BetaNormMaxModel.THOLERUS.full_name}": beta_norm_max_tholerus, f"{BetaNormMaxModel.STAMBAUGH.full_name}": beta_norm_max_stambaugh, } - + data_values = list(data.values()) # Create the violin plot - axis.violinplot(data.values(), showextrema=False) + axis.violinplot(data_values, showextrema=False) # Create the box plot - axis.boxplot( - data.values(), showfliers=True, showmeans=True, meanline=True, widths=0.3 - ) + axis.boxplot(data_values, showfliers=True, showmeans=True, meanline=True, widths=0.3) # Scatter plot for each data point colors = plt.cm.plasma(np.linspace(0, 1, len(data.values()))) @@ -12877,7 +12398,7 @@ def plot_max_normalised_beta_comparison(axis: plt.Axes, mfile: MFile, scan: int) axis.set_title("Max Normalised Beta ($\\beta_N$) Comparison") axis.set_ylabel("Max Normalised Beta $\\beta_N$ [unitless]") - axis.set_xlim([0.5, 1.5]) + axis.set_xlim(0.5, 1.5) axis.set_xticks([]) axis.set_xticklabels([]) axis.set_facecolor("#f0f0f0") @@ -12928,7 +12449,7 @@ def plot_plasma_pressure_gradient_profiles(axis: plt.Axes, mfile: MFile, scan: i axis.grid(which="minor", linestyle=":", linewidth=0.5, alpha=0.5) axis.set_title("Plasma Thermal Pressure Gradient Profiles") axis.grid(True, linestyle="--", alpha=0.5) - axis.set_xlim([0, 1.025]) + axis.set_xlim(0, 1.025) axis.legend() @@ -12974,7 +12495,6 @@ def plot_plasma_poloidal_pressure_contours(axis: plt.Axes, mfile: MFile, scan: i # Mask points outside the plasma boundary (optional, but grid is inside by construction) # Plot filled contour - c = axis.contourf(r_grid, z_grid, pressure_grid, levels=50, cmap="plasma") c = axis.contourf(r_grid, -z_grid, pressure_grid, levels=50, cmap="plasma") # Add colorbar for pressure (now in kPa) @@ -13022,45 +12542,35 @@ def interp1d_profile(profile, mfile: MFile, scan: int): ) # Create a grid of (R, Z) points inside the plasma boundary - n_rho = 500 - n_theta = 720 - rho = np.linspace(0, 1, n_rho) - theta = np.linspace(0, 2 * np.pi, n_theta) + rho = np.linspace(0, 1, 500) + theta = np.linspace(0, 2 * np.pi, 720) rho_grid, theta_grid = np.meshgrid(rho, theta) # Map (rho, theta) to (R, Z) using plasma boundary shape # For each theta, get boundary (R, Z), then scale by rho - boundary_r = pg.rs - boundary_z = pg.zs + bdry_r = pg.rs + bdry_z = pg.zs # Interpolate boundary for all theta - boundary_theta = np.arctan2(boundary_z - pg.zs.mean(), boundary_r - pg.rs.mean()) - # Ensure boundary_theta is monotonic and covers [0, 2pi] - boundary_theta = np.unwrap(boundary_theta) - # Sort boundary_theta and corresponding r/z for monotonic interpolation - sort_idx = np.argsort(boundary_theta) - boundary_theta = boundary_theta[sort_idx] - boundary_r = boundary_r[sort_idx] - boundary_z = boundary_z[sort_idx] + bdry_theta = np.arctan2(bdry_z - pg.zs.mean(), bdry_r - pg.rs.mean()) + # Ensure bdry_theta is monotonic and covers [0, 2pi] + bdry_theta = np.unwrap(bdry_theta) + # Sort bdry_theta and corresponding r/z for monotonic interpolation + sort_idx = np.argsort(bdry_theta) + bdry_theta = bdry_theta[sort_idx] + bdry_r = bdry_r[sort_idx] + bdry_z = bdry_z[sort_idx] # Extend boundary to cover full [0, 2pi] if needed - if boundary_theta[0] > 0 or boundary_theta[-1] < 2 * np.pi: - boundary_theta = np.concatenate(([0], boundary_theta, [2 * np.pi])) - boundary_r = np.concatenate(([boundary_r[0]], boundary_r, [boundary_r[-1]])) - boundary_z = np.concatenate(([boundary_z[0]], boundary_z, [boundary_z[-1]])) + if bdry_theta[0] > 0 or bdry_theta[-1] < 2 * np.pi: + bdry_theta = np.concatenate(([0], bdry_theta, [2 * np.pi])) + bdry_r = np.concatenate(([bdry_r[0]], bdry_r, [bdry_r[-1]])) + bdry_z = np.concatenate(([bdry_z[0]], bdry_z, [bdry_z[-1]])) # Map theta to boundary r/z f_r = interp1d( - boundary_theta, - boundary_r, - kind="linear", - fill_value="extrapolate", - assume_sorted=True, + bdry_theta, bdry_r, kind="linear", fill_value="extrapolate", assume_sorted=True ) # Map theta to boundary z f_z = interp1d( - boundary_theta, - boundary_z, - kind="linear", - fill_value="extrapolate", - assume_sorted=True, + bdry_theta, bdry_z, kind="linear", fill_value="extrapolate", assume_sorted=True ) # For each (theta, rho), get boundary (R, Z), then scale by rho # Use the boundary center for scaling, not mean, to avoid vertical offset @@ -13295,12 +12805,7 @@ def plot_tf_corc_cable_summary_box(axis, fig, mfile: MFile, scan: int): verticalalignment="top", horizontalalignment="left", transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "#cccccc", # grayish color - "alpha": 1.0, - "linewidth": 2, - }, + bbox=_box_style("#cccccc"), # grayish color ) @@ -13370,51 +12875,24 @@ def reaction_plot_grid( ax.legend(legend_handles, legend_labels, loc="upper right", fontsize=8) -def plot_fusion_rate_contours( - fig1, - fig2, - mfile: MFile, - scan: int, -): +def plot_fusion_rate_contours(fig1, fig2, mfile: MFile, scan: int): """Plot fusion rate density contours""" - fusden_plasma_dt_profile = [] - fusden_plasma_dd_triton_profile = [] - fusden_plasma_dd_helion_profile = [] - fusden_plasma_dhe3_profile = [] - rmajor = mfile.get("rmajor", scan=scan) rminor = mfile.get("rminor", scan=scan) kappa = mfile.get("kappa", scan=scan) n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) - fusden_plasma_dt_profile = [ - mfile.get(f"fusden_plasma_dt_profile{i}", scan=scan) - for i in range(n_plasma_profile_elements) - ] - - fusden_plasma_dd_triton_profile = [ - mfile.get(f"fusden_plasma_dd_triton_profile{i}", scan=scan) - for i in range(n_plasma_profile_elements) - ] - - fusden_plasma_dd_helion_profile = [ - mfile.get(f"fusden_plasma_dd_helion_profile{i}", scan=scan) - for i in range(n_plasma_profile_elements) - ] - fusden_plasma_dhe3_profile = [ - mfile.get(f"fusden_plasma_dhe3_profile{i}", scan=scan) - for i in range(n_plasma_profile_elements) - ] - - dt_grid, _r_grid, _z_grid = interp1d_profile(fusden_plasma_dt_profile, mfile, scan) + def fusrat(name): + fusrat_dat = [ + mfile.get(f"fusrat_plasma_{name}_profile{i}", scan=scan) + for i in range(n_plasma_profile_elements) + ] + return interp1d_profile(fusrat_dat, mfile, scan) - dd_triton_grid, _r_grid, _z_grid = interp1d_profile( - fusden_plasma_dd_triton_profile, mfile, scan - ) - dd_helion_grid, _r_grid, _z_grid = interp1d_profile( - fusden_plasma_dd_helion_profile, mfile, scan - ) - dhe3_grid, r_grid, z_grid = interp1d_profile(fusden_plasma_dhe3_profile, mfile, scan) + dt_grid, _r_grid, _z_grid = fusrat("dt") + dd_triton_grid, _r_grid, _z_grid = fusrat(" dd_triton ") + dd_helion_grid, _r_grid, _z_grid = fusrat(" dd_helion ") + dhe3_grid, r_grid, z_grid = fusrat(" dhe3") dt_axes = fig1.add_subplot(121, aspect="equal") dd_triton_axes = fig1.add_subplot(122, aspect="equal") @@ -13491,7 +12969,7 @@ def plot_magnetic_fields_in_plasma(axis: plt.Axes, mfile: MFile, scan: int): verticalalignment="center", horizontalalignment="center", transform=axis.transAxes, - bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 1.0, "linewidth": 2}, + bbox=_box_style("wheat"), ) # Text box for outboard toroidal field @@ -13503,7 +12981,7 @@ def plot_magnetic_fields_in_plasma(axis: plt.Axes, mfile: MFile, scan: int): verticalalignment="center", horizontalalignment="center", transform=axis.transAxes, - bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 1.0, "linewidth": 2}, + bbox=_box_style("wheat"), ) axis.set_xlabel("Radial Position [m]") @@ -14114,7 +13592,7 @@ def plot_debye_length_profile(axis: plt.Axes, mfile_data: MFile, scan: int): axis.set_xlabel("$\\rho \\ [r/a]$") axis.grid(True, which="both", linestyle="--", alpha=0.5) - axis.set_xlim([0, 1.025]) + axis.set_xlim(0, 1.025) axis.minorticks_on() axis.legend() @@ -14180,7 +13658,7 @@ def plot_velocity_profile(axis: plt.Axes, mfile_data: MFile, scan: int) -> None: axis.set_ylabel("Velocity [m/s]") axis.set_xlabel("$\\rho \\ [r/a]$") axis.grid(True, which="both", linestyle="--", alpha=0.5) - axis.set_xlim([0, 1.025]) + axis.set_xlim(0, 1.025) axis.minorticks_on() axis.legend() @@ -14979,20 +14457,14 @@ def plot_blkt_structure( r_fw_inboard_out = r_blkt_inboard_in + dr_blkt_inboard + dr_fw_inboard # Plot a horizontal line at dz_blkt_half (blanket half height) - ax.axhline( - dz_blkt_half, - color="purple", - linestyle="--", - linewidth=1.5, - label="Blanket Half Height", - ) - ax.axhline( - -dz_blkt_half, - color="purple", - linestyle="--", - linewidth=1.5, - label="Blanket Half Height", - ) + for dz_blkt in (dz_blkt_half, -dz_blkt_half): + ax.axhline( + dz_blkt, + color="purple", + linestyle="--", + linewidth=1.5, + label="Blanket Half Height", + ) if DivertorNumberModels(i_single_null) == DivertorNumberModels.DOUBLE_NULL: # Plot arrows for the outboard blanket angles @@ -15056,21 +14528,14 @@ def plot_blkt_structure( ) # Plot arrows for the inboard blanket angles - ax.annotate( - "", - xy=(rmajor, 0), - xytext=(r_fw_inboard_out, dz_blkt_half), - arrowprops={"arrowstyle": "<-", "color": "green"}, - zorder=5, - ) - - ax.annotate( - "", - xy=(rmajor, 0), - xytext=(r_fw_inboard_out, -dz_blkt_half), - arrowprops={"arrowstyle": "<-", "color": "green"}, - zorder=5, - ) + for dz_blkt in (dz_blkt_half, -dz_blkt_half): + ax.annotate( + "", + xy=(rmajor, 0), + xytext=(r_fw_inboard_out, dz_blkt), + arrowprops={"arrowstyle": "<-", "color": "green"}, + zorder=5, + ) # Plot arc showing the angle between the two inboard blanket arrows arc_radius = 1.0 @@ -15153,7 +14618,7 @@ def plot_blkt_structure( arc_radius = 1.5 # 3 to 6 o'clock is -90 degrees angle_start = -90.0 - angle_end = -90.0 - deg_div_poloidal_plasma + angle_end = angle_start - deg_div_poloidal_plasma theta = np.linspace(np.deg2rad(angle_start), np.deg2rad(angle_end), 50) arc_x = rmajor + arc_radius * np.cos(theta) @@ -15188,32 +14653,18 @@ def plot_blkt_structure( ) # Plot vertical lines at the inner and outer radial boundaries of the blanket - ax.axvline( - r_blkt_inboard_in, color="black", linestyle="--", linewidth=1.5, zorder=10 - ) - ax.axvline( - r_blkt_outboard_out, color="black", linestyle="--", linewidth=1.5, zorder=10 - ) - ax.axvline(r_fw_inboard_out, color="black", linestyle="--", linewidth=1.5, zorder=10) - - ax.axvline(r_fw_outboard_in, color="black", linestyle="--", linewidth=1.5, zorder=10) + linestyle = {"color": "black", "linestyle": "--", "linewidth": 1.5, "zorder": 10} + ax.axvline(r_blkt_inboard_in, **linestyle) + ax.axvline(r_blkt_outboard_out, **linestyle) + ax.axvline(r_fw_inboard_out, **linestyle) + ax.axvline(r_fw_outboard_in, **linestyle) ax.axvline( - rmajor, - color="black", - linestyle="--", - linewidth=1.5, - label="Major Radius $R_0$", + rmajor, color="black", linestyle="--", linewidth=1.5, label="Major Radius $R_0$" ) # Plot midplane line (horizontal dashed line at Z=0) - ax.axhline( - 0.0, - color="black", - linestyle="--", - linewidth=1.5, - label="Midplane", - ) + ax.axhline(0.0, color="black", linestyle="--", linewidth=1.5, label="Midplane") textstr_blkt_areas = ( f"$\\mathbf{{Blanket \\ Areas:}}$\n\n" @@ -15229,16 +14680,8 @@ def plot_blkt_structure( 0.05, 0.3, textstr_blkt_areas, - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "wheat", - "alpha": 1.0, - "linewidth": 2, - }, + **text_layout(fig), + bbox=_box_style("wheat"), ) textstr_blkt_volumes = ( @@ -15255,16 +14698,8 @@ def plot_blkt_structure( 0.05, 0.05, textstr_blkt_volumes, - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "wheat", - "alpha": 1.0, - "linewidth": 2, - }, + **text_layout(fig), + bbox=_box_style("wheat"), ) From 6cc3b9b5867d3badbe9ccbccface283243fdf1e9 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:46:56 +0100 Subject: [PATCH 04/18] type dedupe and cleanup --- process/core/io/plot/summary.py | 446 +++++++------------- process/data_structure/physics_variables.py | 2 + 2 files changed, 162 insertions(+), 286 deletions(-) diff --git a/process/core/io/plot/summary.py b/process/core/io/plot/summary.py index bc4dd7a738..44f361aeaa 100644 --- a/process/core/io/plot/summary.py +++ b/process/core/io/plot/summary.py @@ -13,6 +13,7 @@ import matplotlib.pyplot as plt import numpy as np from matplotlib import patches +from matplotlib.axes import Axes from matplotlib.patches import Circle, Rectangle from matplotlib.path import Path as mplPath from matplotlib.transforms import Transform @@ -201,6 +202,14 @@ def text_layout(fig): } +def setup_axis(axis, xmin, xmax, ymin, ymax): + axis.set_ylim(ymin, ymax) + axis.set_xlim(xmin, xmax) + axis.set_axis_off() + axis.set_autoscaley_on(False) + axis.set_autoscalex_on(False) + + def plot_plasma( axis: plt.Axes, mfile: MFile, @@ -3855,7 +3864,7 @@ def plot_jprofile(prof, mfile: MFile, scan: int): prof.set_ylabel(r"Current density $[kA/m^2]$") prof.set_title("$J$ profile") prof.minorticks_on() - prof.set_xlim([0, 1.0]) + prof.set_xlim(0, 1.0) rho = np.linspace(0, 1) y2 = (j_plasma_0 * (1 - rho**2) ** alphaj) / 1e3 @@ -4145,7 +4154,7 @@ def read_imprad_data(_skiprows, data_path): if "infinite confinement" in header.content: zav = np.asarray(header.data, dtype=float) - lzdata[i] = np.column_stack((Te, lz, zav)) + lzdata[i] = np.column_stack([Te, lz, zav]) # then switch string to floats return np.array(lzdata, dtype=float) @@ -4581,12 +4590,12 @@ def plot_line_brem_loss_function_profile( axis.set_xlabel(r"$\rho \quad [r/a]$") axis.set_ylabel(r"$L_z$ $[\mathrm{W}\mathrm{m}^3]$") axis.set_title("Line & Bremsstrahlung Loss Function ($L_z$) Profiles") - axis.set_xlim([0, 1.0]) + axis.set_xlim(0, 1.0) axis.set_yscale("log") axis.yaxis.grid(True, which="both", alpha=0.2) -def plot_rad_density_contour(axis: "mpl.axes.Axes", mfile: "Any", scan: int, impp: str): +def plot_rad_density_contour(axis: Axes, mfile: MFile, scan: int, impp: str): """Plots the contour of line and bremsstrahlung radiation density [MW/m³] for a plasma cross-section. @@ -5712,7 +5721,7 @@ def plot_superconducting_tf_wp(axis: plt.Axes, mfile: MFile, scan: int, fig): if i_tf_sup == 1: axis.add_patch( Circle( - [0, 0], + (0, 0), r_tf_inboard_in, facecolor="none", edgecolor="black", @@ -5723,7 +5732,7 @@ def plot_superconducting_tf_wp(axis: plt.Axes, mfile: MFile, scan: int, fig): if i_tf_case_geom == TFPlasmaCaseType.CIRCULAR: axis.add_patch( Circle( - [0, 0], + (0, 0), r_tf_inboard_out, facecolor="none", edgecolor="black", @@ -6391,7 +6400,7 @@ def plot_resistive_tf_wp(axis: plt.Axes, mfile: MFile, scan: int, fig): axis.add_patch( Circle( - [0, 0], + (0, 0), r_tf_inboard_in, facecolor="none", edgecolor="black", @@ -6402,7 +6411,7 @@ def plot_resistive_tf_wp(axis: plt.Axes, mfile: MFile, scan: int, fig): if i_tf_case_geom == TFPlasmaCaseType.CIRCULAR: axis.add_patch( Circle( - [0, 0], + (0, 0), r_tf_inboard_out, facecolor="none", edgecolor="black", @@ -7089,7 +7098,7 @@ def _pack_strands_rectangular_with_obstacles( if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.NON_INTEGER: axis.add_patch( Rectangle( - [0, 0], + (0, 0), turn_width, turn_width, facecolor="red", @@ -7099,7 +7108,7 @@ def _pack_strands_rectangular_with_obstacles( # Plot the steel conduit axis.add_patch( Rectangle( - [insulation_thickness, insulation_thickness], + (insulation_thickness, insulation_thickness), (turn_width - 2 * insulation_thickness), (turn_width - 2 * insulation_thickness), facecolor="grey", @@ -7110,10 +7119,10 @@ def _pack_strands_rectangular_with_obstacles( # Plot the cable space with rounded corners axis.add_patch( patches.FancyBboxPatch( - [ + ( insulation_thickness + steel_thickness, insulation_thickness + steel_thickness, - ], + ), (turn_width - 2 * (insulation_thickness + steel_thickness)), (turn_width - 2 * (insulation_thickness + steel_thickness)), boxstyle=patches.BoxStyle( @@ -7127,10 +7136,10 @@ def _pack_strands_rectangular_with_obstacles( # Plot dashed line around the cable space axis.add_patch( Rectangle( - [ + ( insulation_thickness + steel_thickness, insulation_thickness + steel_thickness, - ], + ), (turn_width - 2 * (insulation_thickness + steel_thickness)), (turn_width - 2 * (insulation_thickness + steel_thickness)), facecolor="none", @@ -7143,7 +7152,7 @@ def _pack_strands_rectangular_with_obstacles( # Plot the coolant channel axis.add_patch( Circle( - [(turn_width / 2), (turn_width / 2)], + ((turn_width / 2), (turn_width / 2)), he_pipe_diameter / 2, facecolor="white", edgecolor="black", @@ -7194,7 +7203,7 @@ def _pack_strands_rectangular_with_obstacles( elif TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.INTEGER: axis.add_patch( Rectangle( - [0, 0], + (0, 0), turn_width, turn_height, facecolor="red", @@ -7205,7 +7214,7 @@ def _pack_strands_rectangular_with_obstacles( # Plot the steel conduit axis.add_patch( Rectangle( - [insulation_thickness, insulation_thickness], + (insulation_thickness, insulation_thickness), (turn_width - 2 * insulation_thickness), (turn_height - 2 * insulation_thickness), facecolor="grey", @@ -7216,10 +7225,10 @@ def _pack_strands_rectangular_with_obstacles( # Plot the cable space with rounded corners axis.add_patch( patches.FancyBboxPatch( - [ + ( insulation_thickness + steel_thickness, insulation_thickness + steel_thickness, - ], + ), (turn_width - 2 * (insulation_thickness + steel_thickness)), (turn_height - 2 * (insulation_thickness + steel_thickness)), boxstyle=patches.BoxStyle( @@ -7232,10 +7241,10 @@ def _pack_strands_rectangular_with_obstacles( # Plot dashed line around the cable space axis.add_patch( Rectangle( - [ + ( insulation_thickness + steel_thickness, insulation_thickness + steel_thickness, - ], + ), (turn_width - 2 * (insulation_thickness + steel_thickness)), (turn_height - 2 * (insulation_thickness + steel_thickness)), facecolor="none", @@ -7247,7 +7256,7 @@ def _pack_strands_rectangular_with_obstacles( ) axis.add_patch( Circle( - [(turn_width / 2), (turn_height / 2)], + ((turn_width / 2), (turn_height / 2)), he_pipe_diameter / 2, facecolor="white", edgecolor="black", @@ -7472,7 +7481,7 @@ def plot_tf_croco_turn(axis: plt.Axes, fig, mfile: MFile, scan: int): if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.NON_INTEGER: axis.add_patch( Rectangle( - [0, 0], + (0, 0), turn_width, turn_width, facecolor="red", @@ -7490,27 +7499,20 @@ def plot_tf_croco_turn(axis: plt.Axes, fig, mfile: MFile, scan: int): ), ) - # Plot the central cable space - axis.add_patch( - Circle( - [(turn_width / 2), (turn_width / 2)], - 1.5 * dia_tf_turn_croco_cable, - facecolor="white", - edgecolor="black", - linewidth=1.2, - ), - ) - - # PLot the central copper cyclinder - axis.add_patch( - Circle( - [(turn_width / 2), (turn_width / 2)], - dia_tf_turn_croco_cable / 2, - facecolor="#B87333", - edgecolor="black", - linewidth=1.2, - ), - ) + # Plot the central cable space and copper cylinder + for rad, col in [ + (1.5 * dia_tf_turn_croco_cable, "white"), + (dia_tf_turn_croco_cable / 2, "#B87333"), + ]: + axis.add_patch( + Circle( + ((turn_width / 2), (turn_width / 2)), + rad, + facecolor=col, + edgecolor="black", + linewidth=1.2, + ), + ) # Plot six surrounding Croco cables in a hexagonal layout. center_x = turn_width / 2 @@ -7979,11 +7981,7 @@ def plot_header(axis: plt.Axes, mfile: MFile, scan: int): ymin = -16 ymax = 1 - axis.set_ylim(ymin, ymax) - axis.set_xlim(xmin, xmax) - axis.set_axis_off() - axis.set_autoscaley_on(False) - axis.set_autoscalex_on(False) + setup_axis(axis, xmin, xmax, ymin, ymax) data2 = [ (f"!{mfile.get('runtitle', scan=-1)}", "Run title", ""), @@ -8102,11 +8100,7 @@ def plot_geometry_info(axis: plt.Axes, mfile: MFile, scan: int): ymax = 1 axis.text(-0.05, 1, "Geometry:", ha="left", va="center") - axis.set_ylim(ymin, ymax) - axis.set_xlim(xmin, xmax) - axis.set_axis_off() - axis.set_autoscaley_on(False) - axis.set_autoscalex_on(False) + setup_axis(axis, xmin, xmax, ymin, ymax) in_blanket_thk = mfile.get("dr_shld_inboard", scan=scan) + mfile.get( "dr_blkt_inboard", scan=scan @@ -8151,11 +8145,7 @@ def plot_physics_info(axis: plt.Axes, mfile: MFile, scan: int): ymax = 1 axis.text(-0.05, 1, "Physics:", ha="left", va="center") - axis.set_ylim(ymin, ymax) - axis.set_xlim(xmin, xmax) - axis.set_axis_off() - axis.set_autoscaley_on(False) - axis.set_autoscalex_on(False) + setup_axis(axis, xmin, xmax, ymin, ymax) nong = mfile.get("nd_plasma_electron_line", scan=scan) / mfile.get( "nd_plasma_electron_max_array(7)", scan=scan @@ -8225,17 +8215,8 @@ def plot_magnetics_info(axis: plt.Axes, mfile: MFile, scan: int): # Check for Copper magnets i_tf_sup = int(mfile.get("i_tf_sup", scan=scan)) if "i_tf_sup" in mfile.data else 1 - xmin = 0 - xmax = 1 - ymin = -16 - ymax = 1 - axis.text(-0.05, 1, "Coil currents etc:", ha="left", va="center") - axis.set_ylim(ymin, ymax) - axis.set_xlim(xmin, xmax) - axis.set_axis_off() - axis.set_autoscaley_on(False) - axis.set_autoscalex_on(False) + setup_axis(axis, xmin=0, xmax=1, ymin=-16, ymax=1) # Number of coils (1 is OH coil) number_of_coils = 0 @@ -8261,22 +8242,21 @@ def plot_magnetics_info(axis: plt.Axes, mfile: MFile, scan: int): t_plant_pulse_burn = mfile.get("t_plant_pulse_burn", scan=scan) / 3600.0 - if "i_tf_bucking" in mfile.data: - i_tf_bucking = int(mfile.get("i_tf_bucking", scan=scan)) - else: - i_tf_bucking = 1 + i_tf_bucking = ( + int(mfile.get("i_tf_bucking", scan=scan)) if "i_tf_bucking" in mfile.data else 1 + ) # Get superconductor material (i_tf_sc_mat) # If i_tf_sc_mat not present, assume resistive - if "i_tf_sc_mat" in mfile.data: - i_tf_sc_mat = int(mfile.get("i_tf_sc_mat", scan=scan)) - else: - i_tf_sc_mat = 0 + i_tf_sc_mat = ( + int(mfile.get("i_tf_sc_mat", scan=scan)) if "i_tf_sc_mat" in mfile.data else 0 + ) - if i_tf_sc_mat > 0: - tftype = SuperconductorModel(int(mfile.get("i_tf_sc_mat", scan=scan))).full_name - else: - tftype = "Resistive Copper" + tftype = ( + SuperconductorModel(int(mfile.get("i_tf_sc_mat", scan=scan))).full_name + if i_tf_sc_mat > 0 + else "Resistive Copper" + ) vssoft = mfile.get("vs_plasma_res_ramp", scan=scan) + mfile.get( "vs_plasma_ind_ramp", scan=scan @@ -8351,17 +8331,8 @@ def plot_power_info(axis: plt.Axes, mfile: MFile, scan: int): scan : scan number to use """ - xmin = 0 - xmax = 1 - ymin = -16 - ymax = 1 - axis.text(-0.05, 1, "Power flows:", ha="left", va="center") - axis.set_ylim(ymin, ymax) - axis.set_xlim(xmin, xmax) - axis.set_axis_off() - axis.set_autoscaley_on(False) - axis.set_autoscalex_on(False) + setup_axis(axis, xmin=0, xmax=1, ymin=-16, ymax=1) gross_eff = 100.0 * ( mfile.get("p_plant_electric_gross_mw", scan=scan) @@ -8442,28 +8413,17 @@ def plot_current_drive_info(axis: plt.Axes, mfile: MFile, scan: int): scan : scan number to use """ - xmin = 0 - xmax = 1 - ymin = -16 - ymax = 1 + setup_axis(axis, xmin=0, xmax=1, ymin=-16, ymax=1) + i_hcd_primary = mfile.get("i_hcd_primary", scan=scan) - nbi = False - ecrh = False - ebw = False - lhcd = False - iccd = False - - if i_hcd_primary in {5, 8}: - nbi = True + + if nbi := (i_hcd_primary in {5, 8}): axis.text(-0.05, 1, "Neutral Beam Current Drive:", ha="left", va="center") - if i_hcd_primary in {3, 7, 10, 11, 13}: - ecrh = True + if ecrh := (i_hcd_primary in {3, 7, 10, 11, 13}): axis.text(-0.05, 1, "Electron Cyclotron Current Drive:", ha="left", va="center") - if i_hcd_primary == 12: - ebw = True + if ebw := (i_hcd_primary == 12): axis.text(-0.05, 1, "Electron Bernstein Wave Drive:", ha="left", va="center") - if i_hcd_primary in {1, 4, 6}: - lhcd = True + if lhcd := (i_hcd_primary in {1, 4, 6}): axis.text( -0.05, 1, @@ -8471,30 +8431,22 @@ def plot_current_drive_info(axis: plt.Axes, mfile: MFile, scan: int): ha="left", va="center", ) - if i_hcd_primary == 2: - iccd = True + if iccd := (i_hcd_primary == 2): axis.text(-0.05, 1, "Ion Cyclotron Current Drive:", ha="left", va="center") - if "i_hcd_secondary" in mfile.data: + i_hcd_secondary = mfile.get("i_hcd_secondary", scan=scan) or 0 + if i_hcd_secondary in {5, 8}: + secondary_heating = "NBI" + elif i_hcd_secondary in {3, 7, 10, 11, 13}: + secondary_heating = "ECH" + elif i_hcd_secondary == 12: + secondary_heating = "EBW" + elif i_hcd_secondary in {1, 4, 6}: + secondary_heating = "LHCD" + elif i_hcd_secondary == 2: + secondary_heating = "ICCD" + else: secondary_heating = "" - i_hcd_secondary = mfile.get("i_hcd_secondary", scan=scan) - - if i_hcd_secondary in {5, 8}: - secondary_heating = "NBI" - if i_hcd_secondary in {3, 7, 10, 11, 13}: - secondary_heating = "ECH" - if i_hcd_secondary == 12: - secondary_heating = "EBW" - if i_hcd_secondary in {1, 4, 6}: - secondary_heating = "LHCD" - if i_hcd_secondary == 2: - secondary_heating = "ICCD" - - axis.set_ylim(ymin, ymax) - axis.set_xlim(xmin, xmax) - axis.set_axis_off() - axis.set_autoscaley_on(False) - axis.set_autoscalex_on(False) pinjie = mfile.get("p_hcd_injected_total_mw", scan=scan) p_plasma_separatrix_mw = mfile.get("p_plasma_separatrix_mw", scan=scan) @@ -8514,166 +8466,88 @@ def plot_current_drive_info(axis: plt.Axes, mfile: MFile, scan: int): # Assume Martin scaling if pthresh is not printed # Accounts for pthresh not being written prior to issue #679 and #680 - if "p_l_h_threshold_mw" in mfile.data: - pthresh = mfile.get("p_l_h_threshold_mw", scan=scan) - else: - pthresh = mfile.get("l_h_threshold_powers(6)", scan=scan) + pthresh_name = ( + "p_l_h_threshold_mw" + if "p_l_h_threshold_mw" in mfile.data + else "l_h_threshold_powers(6)" + ) + pthresh = mfile.get(pthresh_name, scan=scan) flh = p_plasma_separatrix_mw / pthresh hstar = mfile.get("hstar", scan=scan) - if ecrh: - data = [ - (pinjie, "Steady state auxiliary power", "MW"), - ("p_hcd_primary_extra_heat_mw", "Power for heating only", "MW"), - ("f_c_plasma_bootstrap", "Bootstrap fraction", ""), - ("f_c_plasma_auxiliary", "Auxiliary fraction", ""), - ("f_c_plasma_inductive", "Inductive fraction", ""), - ("p_plasma_loss_mw", "Plasma heating used for H factor", "MW"), - ( - "eta_cd_hcd_primary", - "Current drive efficiency", - "A W$^{-1}$", - ), - (pdivr, r"$\frac{P_{\mathrm{div}}}{R_{0}}$", "MW m$^{-1}$"), - ( - pdivnr, - r"$\frac{P_{\mathrm{div}}}{\langle n \rangle R_{0}}$", - r"$\times 10^{-20}$ MW m$^{2}$", - ), - (flh, r"$\frac{P_{\mathrm{div}}}{P_{\mathrm{LH}}}$", ""), - (hstar, "H* (non-rad. corr.)", ""), - ] - # i_hcd_secondary is now always in the MFILE with = 0 meaning no fixed heating - if mfile.get("i_hcd_secondary", scan=scan) != 0: - data.insert( - 1, ("pinjmwfix", f"{secondary_heating} secondary auxiliary power", "MW") - ) - data[0] = ((pinjie - pinjmwfix), "Primary auxiliary power", "MW") - data.insert(2, (pinjie, "Total auxillary power", "MW")) - - if nbi: - data = [ - (pinjie, "Steady state auxiliary power", "MW"), - ("p_hcd_primary_extra_heat_mw", "Power for heating only", "MW"), - ("f_c_plasma_bootstrap", "Bootstrap fraction", ""), - ("f_c_plasma_auxiliary", "Auxiliary fraction", ""), - ("f_c_plasma_inductive", "Inductive fraction", ""), + data = [ + (pinjie, "Steady state auxiliary power", "MW"), + ("p_hcd_primary_extra_heat_mw", "Power for heating only", "MW"), + ("f_c_plasma_bootstrap", "Bootstrap fraction", ""), + ("f_c_plasma_auxiliary", "Auxiliary fraction", ""), + ("f_c_plasma_inductive", "Inductive fraction", ""), + ("p_plasma_loss_mw", "Plasma heating used for H factor", "MW"), + (pdivr, r"$\frac{P_{\mathrm{div}}}{R_{0}}$", "MW m$^{-1}$"), + ( + pdivnr, + r"$\frac{P_{\mathrm{div}}}{\langle n \rangle R_{0}}$", + r"$\times 10^{-20}$ MW m$^{2}$", + ), + (flh, r"$\frac{P_{\mathrm{div}}}{P_{\mathrm{LH}}}$", ""), + (hstar, "H* (non-rad. corr.)", ""), + ] + # Optional override based on condition + field_overrides = { + "ecrh": ( + "eta_cd_hcd_primary", + r"$\frac{P_{\mathrm{div}}}{R_{0}}$", + "A W$^{-1}$", + ), + "nbi": ( ("gamnb", "NB gamma", "$10^{20}$ A W$^{-1}$ m$^{-2}$"), ("e_beam_kev", "NB energy", "keV"), - ("p_plasma_loss_mw", "Plasma heating used for H factor", "MW"), - (pdivr, r"$\frac{P_{\mathrm{div}}}{R_{0}}$", "MW m$^{-1}$"), - ( - pdivnr, - r"$\frac{P_{\mathrm{div}}}{\langle n \rangle R_{0}}$", - r"$\times 10^{-20}$ MW m$^{2}$", - ), - (flh, r"$\frac{P_{\mathrm{div}}}{P_{\mathrm{LH}}}$", ""), - (hstar, "H* (non-rad. corr.)", ""), - ] - if mfile.get("i_hcd_secondary", scan=scan) != 0: - data.insert( - 1, ("pinjmwfix", f"{secondary_heating} secondary auxiliary power", "MW") - ) - data[0] = ((pinjie - pinjmwfix), "Primary auxiliary power", "MW") - data.insert(2, (pinjie, "Total auxillary power", "MW")) - - if ebw: - data = [ - (pinjie, "Steady state auxiliary power", "MW"), - ("p_hcd_primary_extra_heat_mw", "Power for heating only", "MW"), - ("f_c_plasma_bootstrap", "Bootstrap fraction", ""), - ("f_c_plasma_auxiliary", "Auxiliary fraction", ""), - ("f_c_plasma_inductive", "Inductive fraction", ""), - ("p_plasma_loss_mw", "Plasma heating used for H factor", "MW"), - ( - "eta_cd_norm_hcd_primary", - "Normalised current drive efficiency of primary HCD system", - "(10$^{20}$ A/(Wm$^{2}$))", - ), - (pdivr, r"$\frac{P_{\mathrm{div}}}{R_{0}}$", "MW m$^{-1}$"), - ( - pdivnr, - r"$\frac{P_{\mathrm{div}}}{\langle n \rangle R_{0}}$", - r"$\times 10^{-20}$ MW m$^{2}$", - ), - (flh, r"$\frac{P_{\mathrm{div}}}{P_{\mathrm{LH}}}$", ""), - (hstar, "H* (non-rad. corr.)", ""), - ] - if "i_hcd_secondary" in mfile.data: - data.insert( - 1, ("pinjmwfix", f"{secondary_heating} secondary auxiliary power", "MW") - ) - data[0] = ((pinjie - pinjmwfix), "Primary auxiliary power", "MW") - data.insert(2, (pinjie, "Total auxillary power", "MW")) - - if lhcd: - data = [ - (pinjie, "Steady state auxiliary power", "MW"), - ("p_hcd_primary_extra_heat_mw", "Power for heating only", "MW"), - ("f_c_plasma_bootstrap", "Bootstrap fraction", ""), - ("f_c_plasma_auxiliary", "Auxiliary fraction", ""), - ("f_c_plasma_inductive", "Inductive fraction", ""), - ("p_plasma_loss_mw", "Plasma heating used for H factor", "MW"), - ( - "eta_cd_norm_hcd_primary", - "Normalised current drive efficiency", - "(10$^{20}$ A/(Wm$^{2}$))", - ), - (pdivr, r"$\frac{P_{\mathrm{div}}}{R_{0}}$", "MW m$^{-1}$"), - ( - pdivnr, - r"$\frac{P_{\mathrm{div}}}{\langle n \rangle R_{0}}$", - r"$\times 10^{-20}$ MW m$^{2}$", - ), - (flh, r"$\frac{P_{\mathrm{div}}}{P_{\mathrm{LH}}}$", ""), - (hstar, "H* (non-rad. corr.)", ""), - ] - if "i_hcd_secondary" in mfile.data: - data.insert( - 1, ("pinjmwfix", f"{secondary_heating} secondary auxiliary power", "MW") - ) - data[0] = ((pinjie - pinjmwfix), "Primary auxiliary power", "MW") - data.insert(2, (pinjie, "Total auxillary power", "MW")) + ), + "ebw": ( + "eta_cd_norm_hcd_primary", + "Normalised current drive efficiency of primary HCD system", + "(10$^{20}$ A/(Wm$^{2}$))", + ), + "lhcd": ( + "eta_cd_norm_hcd_primary", + "Normalised current drive efficiency", + "(10$^{20}$ A/(Wm$^{2}$))", + ), + "iccd": ( + "eta_cd_norm_hcd_primary", + "Normalised current drive efficiency", + "(10$^{20}$ A/(Wm$^{2}$))", + ), + } - if iccd: - data = [ - (pinjie, "Steady state auxiliary power", "MW"), - ("p_hcd_primary_extra_heat_mw", "Power for heating only", "MW"), - ("f_c_plasma_bootstrap", "Bootstrap fraction", ""), - ("f_c_plasma_auxiliary", "Auxiliary fraction", ""), - ("f_c_plasma_inductive", "Inductive fraction", ""), - ("p_plasma_loss_mw", "Plasma heating used for H factor", "MW"), - ( - "eta_cd_norm_hcd_primary", - "Normalised current drive efficiency", - "(10$^{20}$ A/(Wm$^{2}$))", - ), - (pdivr, r"$\frac{P_{\mathrm{div}}}{R_{0}}$", "MW m$^{-1}$"), - ( - pdivnr, - r"$\frac{P_{\mathrm{div}}}{\langle n \rangle R_{0}}$", - r"$\times 10^{-20}$ MW m$^{2}$", - ), - (flh, r"$\frac{P_{\mathrm{div}}}{P_{\mathrm{LH}}}$", ""), - (hstar, "H* (non-rad. corr.)", ""), - ] - if "i_hcd_secondary" in mfile.data: - data.insert( - 1, ("pinjmwfix", f"{secondary_heating} secondary auxiliary power", "MW") - ) - data[0] = ((pinjie - pinjmwfix), "Primary auxiliary power", "MW") - data.insert(2, (pinjie, "Total auxillary power", "MW")) + if ecrh: + data.insert(6, field_overrides["ecrh"]) + elif nbi: + data.insert(6, field_overrides["nbi"][0]) + data.insert(7, field_overrides["nbi"][1]) + elif ebw: + data.insert(6, field_overrides["ebw"]) + elif lhcd: + data.insert(6, field_overrides["lhcd"]) + elif iccd: + data.insert(6, field_overrides["iccd"]) + + # Secondary heating logic — common across all cases + if mfile.get("i_hcd_secondary", scan=scan) != 0: + data.insert( + 1, ("pinjmwfix", f"{secondary_heating} secondary auxiliary power", "MW") + ) + data[0] = ((pinjie - pinjmwfix), "Primary auxiliary power", "MW") + data.insert(2, (pinjie, "Total auxillary power", "MW")) coe = mfile.get("coe", scan=scan) - if coe == 0.0: # noqa: RUF069 - data.append(("", "", "")) - data.append(("#Costs", "", "")) - data.append(("", "Cost output not selected", "")) - else: - data.append(("", "", "")) - data.append(("#Costs", "", "")) - data.append((coe, "Cost of electricity", r"\$/MWh")) + data.extend(( + ("", "", ""), + ("#Costs", "", ""), + ("", "Cost output not selected", "") + if coe == 0.0 # noqa: RUF069 + else (coe, "Cost of electricity", r"\$/MWh"), + )) plot_info(axis, data, mfile, scan) @@ -8761,7 +8635,7 @@ def plot_bootstrap_comparison(axis: plt.Axes, mfile: MFile, scan: int): axis.set_title("Bootstrap Current Fraction ($f_\\text{BS}$) Comparison") axis.set_ylabel("Bootstrap Current Fraction") - axis.set_xlim([0.5, 1.5]) + axis.set_xlim(0.5, 1.5) axis.set_xticks([]) axis.set_xticklabels([]) axis.set_facecolor("#f0f0f0") @@ -8847,7 +8721,7 @@ def plot_sol_power_decay_length_comparison(axis: plt.Axes, mfile: MFile, scan: i axis.set_title("SOL Power Decay Length ($\\lambda_q$) Comparison") axis.set_ylabel("Power Decay Length [mm]") - axis.set_xlim([0.5, 1.5]) + axis.set_xlim(0.5, 1.5) axis.set_xticks([]) axis.set_xticklabels([]) axis.set_facecolor("#f0f0f0") @@ -9236,7 +9110,7 @@ def plot_h_threshold_comparison(axis: plt.Axes, mfile: MFile, scan: int, u_seed= axis.set_title("L-H Threshold ($P_\\text{LH}$) Comparison") axis.set_ylabel("L-H threshold power [MW]") - axis.set_xlim([0.5, 1.5]) + axis.set_xlim(0.5, 1.5) axis.set_xticks([]) axis.set_xticklabels([]) @@ -9578,7 +9452,7 @@ def plot_confinement_time_comparison( axis.set_title("Confinement time ($\\tau_{\\text{E}}$) Comparison") axis.set_ylabel("Confinement time, $\\tau_{\\text{E}}$ [s]") - axis.set_xlim([0.5, 1.5]) + axis.set_xlim(0.5, 1.5) axis.set_xticks([]) axis.set_xticklabels([]) @@ -10114,7 +9988,7 @@ def plot_density_limit_comparison(axis: plt.Axes, mfile: MFile, scan: int): axis.set_title("Density Limit Comparison") axis.set_ylabel(r"Density Limit [$10^{20}$ m$^{-3}$]") axis.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x * 1e-20:.1f}")) - axis.set_xlim([0.5, 1.5]) + axis.set_xlim(0.5, 1.5) axis.set_xticks([]) axis.set_xticklabels([]) axis.set_facecolor("#f0f0f0") diff --git a/process/data_structure/physics_variables.py b/process/data_structure/physics_variables.py index dad94478af..752f18a10e 100644 --- a/process/data_structure/physics_variables.py +++ b/process/data_structure/physics_variables.py @@ -11,6 +11,8 @@ class PlasmaConfinementTransitionModel(IntEnum): """Enum for plasma L -> H and L -> I transition power threshold models.""" + full_name: str + ITER1996_NOMINAL = (1, "ITER-1996 Nominal") ITER1996_UPPER = (2, "ITER-1996 Upper") ITER1996_LOWER = (3, "ITER-1996 Lower") From 747a5c1ba8c1c25006538d6feebe6cc33062e226 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:51:57 +0100 Subject: [PATCH 05/18] PLR0914 --- process/core/io/plot/summary.py | 85 +++++++++++++-------------------- 1 file changed, 34 insertions(+), 51 deletions(-) diff --git a/process/core/io/plot/summary.py b/process/core/io/plot/summary.py index 44f361aeaa..06195ee866 100644 --- a/process/core/io/plot/summary.py +++ b/process/core/io/plot/summary.py @@ -8998,65 +8998,49 @@ def plot_h_threshold_comparison(axis: plt.Axes, mfile: MFile, scan: int, u_seed= u_seed : (Default value = None) """ - iter_nominal = mfile.get("l_h_threshold_powers(1)", scan=scan) - iter_upper = mfile.get("l_h_threshold_powers(2)", scan=scan) - iter_lower = mfile.get("l_h_threshold_powers(3)", scan=scan) - iter_1997_1 = mfile.get("l_h_threshold_powers(4)", scan=scan) - iter_1997_2 = mfile.get("l_h_threshold_powers(5)", scan=scan) - martin_nominal = mfile.get("l_h_threshold_powers(6)", scan=scan) - martin_upper = mfile.get("l_h_threshold_powers(7)", scan=scan) - martin_lower = mfile.get("l_h_threshold_powers(8)", scan=scan) - snipes_nominal = mfile.get("l_h_threshold_powers(9)", scan=scan) - snipes_upper = mfile.get("l_h_threshold_powers(10)", scan=scan) - snipes_lower = mfile.get("l_h_threshold_powers(11)", scan=scan) - snipes_closed_nominal = mfile.get("l_h_threshold_powers(12)", scan=scan) - snipes_closed_upper = mfile.get("l_h_threshold_powers(13)", scan=scan) - snipes_closed_lower = mfile.get("l_h_threshold_powers(14)", scan=scan) - hubbard_nominal = mfile.get("l_h_threshold_powers(15)", scan=scan) - hubbard_lower = mfile.get("l_h_threshold_powers(16)", scan=scan) - hubbard_upper = mfile.get("l_h_threshold_powers(17)", scan=scan) - hubbard_2017 = mfile.get("l_h_threshold_powers(18)", scan=scan) - martin_aspect_nominal = mfile.get("l_h_threshold_powers(19)", scan=scan) - martin_aspect_upper = mfile.get("l_h_threshold_powers(20)", scan=scan) - martin_aspect_lower = mfile.get("l_h_threshold_powers(21)", scan=scan) - # Data for the box plot data = { - "ITER 1996 Nominal": iter_nominal, - "ITER 1996 Upper": iter_upper, - "ITER 1996 Lower": iter_lower, - "ITER 1997 (1)": iter_1997_1, - "ITER 1997 (2)": iter_1997_2, - "Martin Nominal": martin_nominal, - "Martin Upper": martin_upper, - "Martin Lower": martin_lower, - "Snipes Nominal": snipes_nominal, - "Snipes Upper": snipes_upper, - "Snipes Lower": snipes_lower, - "Snipes Closed Divertor Nominal": snipes_closed_nominal, - "Snipes Closed Divertor Upper": snipes_closed_upper, - "Snipes Closed Divertor Lower": snipes_closed_lower, - "Hubbard Nominal (I-mode)": hubbard_nominal, - "Hubbard Lower (I-mode)": hubbard_lower, - "Hubbard Upper (I-mode)": hubbard_upper, - "Hubbard 2017 (I-mode)": hubbard_2017, - "Martin Aspect Corrected Nominal": martin_aspect_nominal, - "Martin Aspect Corrected Upper": martin_aspect_upper, - "Martin Aspect Corrected Lower": martin_aspect_lower, + "ITER 1996 Nominal": mfile.get("l_h_threshold_powers(1)", scan=scan), + "ITER 1996 Upper": mfile.get("l_h_threshold_powers(2)", scan=scan), + "ITER 1996 Lower": mfile.get("l_h_threshold_powers(3)", scan=scan), + "ITER 1997 (1)": mfile.get("l_h_threshold_powers(4)", scan=scan), + "ITER 1997 (2)": mfile.get("l_h_threshold_powers(5)", scan=scan), + "Martin Nominal": mfile.get("l_h_threshold_powers(6)", scan=scan), + "Martin Upper": mfile.get("l_h_threshold_powers(7)", scan=scan), + "Martin Lower": mfile.get("l_h_threshold_powers(8)", scan=scan), + "Snipes Nominal": mfile.get("l_h_threshold_powers(9)", scan=scan), + "Snipes Upper": mfile.get("l_h_threshold_powers(10)", scan=scan), + "Snipes Lower": mfile.get("l_h_threshold_powers(11)", scan=scan), + "Snipes Closed Divertor Nominal": mfile.get( + "l_h_threshold_powers(12)", scan=scan + ), + "Snipes Closed Divertor Upper": mfile.get("l_h_threshold_powers(13)", scan=scan), + "Snipes Closed Divertor Lower": mfile.get("l_h_threshold_powers(14)", scan=scan), + "Hubbard Nominal (I-mode)": mfile.get("l_h_threshold_powers(15)", scan=scan), + "Hubbard Lower (I-mode)": mfile.get("l_h_threshold_powers(16)", scan=scan), + "Hubbard Upper (I-mode)": mfile.get("l_h_threshold_powers(17)", scan=scan), + "Hubbard 2017 (I-mode)": mfile.get("l_h_threshold_powers(18)", scan=scan), + "Martin Aspect Corrected Nominal": mfile.get( + "l_h_threshold_powers(19)", scan=scan + ), + "Martin Aspect Corrected Upper": mfile.get( + "l_h_threshold_powers(20)", scan=scan + ), + "Martin Aspect Corrected Lower": mfile.get( + "l_h_threshold_powers(21)", scan=scan + ), } - + data_values = list(data.values()) # Create the violin plot - axis.violinplot(data.values(), showextrema=False) + axis.violinplot(data_values, showextrema=False) # Create the box plot - axis.boxplot( - data.values(), showfliers=True, showmeans=True, meanline=True, widths=0.3 - ) + axis.boxplot(data_values, showfliers=True, showmeans=True, meanline=True, widths=0.3) # Scatter plot for each data point - colors = plt.cm.plasma(np.linspace(0, 1, len(data.values()))) + colors = plt.cm.plasma(np.linspace(0, 1, len(data_values))) generator = np.random.default_rng(seed=u_seed) - x_values = generator.normal(loc=1, scale=0.01, size=len(data.values())) + x_values = generator.normal(loc=1, scale=0.01, size=len(data_values)) for index, (key, value) in enumerate(data.items()): if "ITER 1996" in key: color = "blue" @@ -9080,7 +9064,6 @@ def plot_h_threshold_comparison(axis: plt.Axes, mfile: MFile, scan: int, u_seed= axis.legend(loc="upper left", bbox_to_anchor=(-1.1, 1), ncol=2) # Calculate average, standard deviation, and median - data_values = list(data.values()) avg_threshold = np.mean(data_values) std_threshold = np.std(data_values) median_threshold = np.median(data_values) From 45ae05c314e21ddb962a485c79fb46dde771d415 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:06:52 +0100 Subject: [PATCH 06/18] type dedupe and cleanup --- process/core/io/plot/summary.py | 65 ++++++++++++--------------------- 1 file changed, 24 insertions(+), 41 deletions(-) diff --git a/process/core/io/plot/summary.py b/process/core/io/plot/summary.py index 06195ee866..ef575d435f 100644 --- a/process/core/io/plot/summary.py +++ b/process/core/io/plot/summary.py @@ -7491,7 +7491,7 @@ def plot_tf_croco_turn(axis: plt.Axes, fig, mfile: MFile, scan: int): # Plot the steel conduit axis.add_patch( Rectangle( - [insulation_thickness, insulation_thickness], + (insulation_thickness, insulation_thickness), (turn_width - 2 * insulation_thickness), (turn_width - 2 * insulation_thickness), facecolor="grey", @@ -7851,8 +7851,12 @@ def plot_pf_coils( ) # Get axis height for fontsize scaling - bbox = axis.get_window_extent().transformed(axis.figure.dpi_scale_trans.inverted()) - axis_height = bbox.height + axis_height = ( + axis + .get_window_extent() + .transformed(axis.figure.dpi_scale_trans.inverted()) + .height + ) for i in range(len(coils_r)): mirrored_r_points = [x_scale * r for r in r_points[i]] @@ -7976,12 +7980,7 @@ def plot_header(axis: plt.Axes, mfile: MFile, scan: int): scan : scan number to use """ - xmin = 0 - xmax = 1 - ymin = -16 - ymax = 1 - - setup_axis(axis, xmin, xmax, ymin, ymax) + setup_axis(axis, xmin=0, xmax=1, ymin=-16, ymax=1) data2 = [ (f"!{mfile.get('runtitle', scan=-1)}", "Run title", ""), @@ -8564,43 +8563,28 @@ def plot_bootstrap_comparison(axis: plt.Axes, mfile: MFile, scan: int): scan : scan number to use """ - boot_ipdg = mfile.get("f_c_plasma_bootstrap_iter89", scan=scan) - boot_sauter = mfile.get("f_c_plasma_bootstrap_sauter", scan=scan) - boot_nenins = mfile.get("f_c_plasma_bootstrap_nevins", scan=scan) - boot_wilson = mfile.get("f_c_plasma_bootstrap_wilson", scan=scan) - boot_sakai = mfile.get("f_c_plasma_bootstrap_sakai", scan=scan) - boot_aries = mfile.get("f_c_plasma_bootstrap_aries", scan=scan) - boot_andrade = mfile.get("f_c_plasma_bootstrap_andrade", scan=scan) - boot_hoang = mfile.get("f_c_plasma_bootstrap_hoang", scan=scan) - boot_wong = mfile.get("f_c_plasma_bootstrap_wong", scan=scan) - boot_gi_I = mfile.get("bscf_gi_i", scan=scan) # noqa: N806 - boot_gi_II = mfile.get("bscf_gi_ii", scan=scan) # noqa: N806 - boot_sugiyama_l = mfile.get("f_c_plasma_bootstrap_sugiyama_l", scan=scan) - boot_sugiyama_h = mfile.get("f_c_plasma_bootstrap_sugiyama_h", scan=scan) - # Data for the box plot data = { - "IPDG": boot_ipdg, - "Sauter": boot_sauter, - "Nevins": boot_nenins, - "Wilson": boot_wilson, - "Sakai": boot_sakai, - "ARIES": boot_aries, - "Andrade": boot_andrade, - "Hoang": boot_hoang, - "Wong": boot_wong, - "Gi-I": boot_gi_I, - "Gi-II": boot_gi_II, - "Sugiyama (L-mode)": boot_sugiyama_l, - "Sugiyama (H-mode)": boot_sugiyama_h, + "IPDG": mfile.get("f_c_plasma_bootstrap_iter89", scan=scan), + "Sauter": mfile.get("f_c_plasma_bootstrap_sauter", scan=scan), + "Nevins": mfile.get("f_c_plasma_bootstrap_nevins", scan=scan), + "Wilson": mfile.get("f_c_plasma_bootstrap_wilson", scan=scan), + "Sakai": mfile.get("f_c_plasma_bootstrap_sakai", scan=scan), + "ARIES": mfile.get("f_c_plasma_bootstrap_aries", scan=scan), + "Andrade": mfile.get("f_c_plasma_bootstrap_andrade", scan=scan), + "Hoang": mfile.get("f_c_plasma_bootstrap_hoang", scan=scan), + "Wong": mfile.get("f_c_plasma_bootstrap_wong", scan=scan), + "Gi-I": mfile.get("bscf_gi_i", scan=scan), + "Gi-II": mfile.get("bscf_gi_ii", scan=scan), + "Sugiyama (L-mode)": mfile.get("f_c_plasma_bootstrap_sugiyama_l", scan=scan), + "Sugiyama (H-mode)": mfile.get("f_c_plasma_bootstrap_sugiyama_h", scan=scan), } # Create the violin plot - axis.violinplot(data.values(), showextrema=False) + data_values = list(data.values()) + axis.violinplot(data_values, showextrema=False) # Create the box plot - axis.boxplot( - data.values(), showfliers=True, showmeans=True, meanline=True, widths=0.3 - ) + axis.boxplot(data_values, showfliers=True, showmeans=True, meanline=True, widths=0.3) # Scatter plot for each data point colors = plt.cm.plasma(np.linspace(0, 1, len(data.values()))) @@ -8609,7 +8593,6 @@ def plot_bootstrap_comparison(axis: plt.Axes, mfile: MFile, scan: int): axis.legend(loc="upper left", bbox_to_anchor=(1, 1)) # Calculate average, standard deviation, and median - data_values = list(data.values()) avg_bootstrap = np.mean(data_values) std_bootstrap = np.std(data_values) median_bootstrap = np.median(data_values) From bc259917180d055912e1e0eef4501f49b84f4c0a Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:15:11 +0100 Subject: [PATCH 07/18] PLR0914 --- process/core/io/plot/summary.py | 35 ++++++++++++--------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/process/core/io/plot/summary.py b/process/core/io/plot/summary.py index ef575d435f..22580805ae 100644 --- a/process/core/io/plot/summary.py +++ b/process/core/io/plot/summary.py @@ -210,6 +210,12 @@ def setup_axis(axis, xmin, xmax, ymin, ymax): axis.set_autoscalex_on(False) +def _colourbar(contour_fill, axis, colourbar_axis): + if colourbar_axis is None: + return axis.figure.colorbar(contour_fill, ax=axis, pad=0.02) + return axis.figure.colorbar(contour_fill, cax=colourbar_axis) + + def plot_plasma( axis: plt.Axes, mfile: MFile, @@ -15273,10 +15279,7 @@ def plot_cs_radial_stress_contour_profile( ) # Use a dedicated colorbar axes when provided so the main axes width is unchanged. - if colorbar_axis is None: - cbar = axis.figure.colorbar(contour_fill, ax=axis, pad=0.02) - else: - cbar = axis.figure.colorbar(contour_fill, cax=colorbar_axis) + cbar = _colourbar(contour_fill, axis, colorbar_axis) cbar.set_label("Radial Stress (MPa)") axis.set_xlabel("R [m]") @@ -15367,10 +15370,7 @@ def plot_cs_hoop_stress_contour_profile( ) # Use a dedicated colorbar axes when provided so the main axes width is unchanged. - if colorbar_axis is None: - cbar = axis.figure.colorbar(contour_fill, ax=axis, pad=0.02) - else: - cbar = axis.figure.colorbar(contour_fill, cax=colorbar_axis) + cbar = _colourbar(contour_fill, axis, colorbar_axis) cbar.set_label("Hoop Stress (MPa)") axis.set_xlabel("R [m]") @@ -15481,10 +15481,7 @@ def plot_vertical_stress_contour_profile( ) # Use a dedicated colorbar axes when provided so the main axes width is unchanged. - if colorbar_axis is None: - cbar = axis.figure.colorbar(contour_fill, ax=axis, pad=0.02) - else: - cbar = axis.figure.colorbar(contour_fill, cax=colorbar_axis) + cbar = _colourbar(contour_fill, axis, colorbar_axis) cbar.set_label("Vertical Stress (MPa)") axis.set_xlabel("R [m]") @@ -15553,7 +15550,6 @@ def plot_cs_tresca_2d_contour( ) # Plot filled contour of Tresca stress distribution - contour_fill = axis.contourf(r, z, tresca_data, levels=15, cmap="RdYlBu_r") contour_lines = axis.contour( r, z, @@ -15591,10 +15587,8 @@ def plot_cs_tresca_2d_contour( ) # Use a dedicated colorbar axes when provided so the main axes width is unchanged. - if colorbar_axis is None: - cbar = axis.figure.colorbar(contour_fill, ax=axis, pad=0.02) - else: - cbar = axis.figure.colorbar(contour_fill, cax=colorbar_axis) + contour_fill = axis.contourf(r, z, tresca_data, levels=15, cmap="RdYlBu_r") + cbar = _colourbar(contour_fill, axis, colorbar_axis) cbar.set_label("Tresca Stress (MPa)") axis.set_xlabel("R [m]") @@ -15667,7 +15661,6 @@ def plot_cs_von_mises_2d_contour( ) # Plot filled contour of Von Mises stress distribution - contour_fill = axis.contourf(r, z, von_mises_data, levels=15, cmap="RdYlBu_r") contour_lines = axis.contour( r, z, @@ -15705,10 +15698,8 @@ def plot_cs_von_mises_2d_contour( ) # Use a dedicated colorbar axes when provided so the main axes width is unchanged. - if colorbar_axis is None: - cbar = axis.figure.colorbar(contour_fill, ax=axis, pad=0.02) - else: - cbar = axis.figure.colorbar(contour_fill, cax=colorbar_axis) + contour_fill = axis.contourf(r, z, von_mises_data, levels=15, cmap="RdYlBu_r") + cbar = _colourbar(contour_fill, axis, colorbar_axis) cbar.set_label("Von Mises Stress (MPa)") axis.set_xlabel("R [m]") From 83678dce6d69344406ea531f6c1a49e294264db3 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:17:39 +0100 Subject: [PATCH 08/18] repetition and unused vars --- process/core/io/plot/summary.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/process/core/io/plot/summary.py b/process/core/io/plot/summary.py index 22580805ae..d7a60b450d 100644 --- a/process/core/io/plot/summary.py +++ b/process/core/io/plot/summary.py @@ -211,6 +211,7 @@ def setup_axis(axis, xmin, xmax, ymin, ymax): def _colourbar(contour_fill, axis, colourbar_axis): + # Use a dedicated colorbar axes when provided so the main axes width is unchanged. if colourbar_axis is None: return axis.figure.colorbar(contour_fill, ax=axis, pad=0.02) return axis.figure.colorbar(contour_fill, cax=colourbar_axis) @@ -4174,9 +4175,7 @@ def profiles_with_pedestal(mfile, scan: int): temp_plasma_electron_on_axis_kev = mfile.get( "temp_plasma_electron_on_axis_kev", scan=scan ) - radius_plasma_pedestal_density_norm = mfile.get( - "radius_plasma_pedestal_density_norm", scan=scan - ) + radius_plasma_pedestal_temp_norm = mfile.get( "radius_plasma_pedestal_temp_norm", scan=scan ) @@ -4184,7 +4183,6 @@ def profiles_with_pedestal(mfile, scan: int): n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) i_plasma_pedestal = mfile.get("i_plasma_pedestal", scan=scan) nd_plasma_pedestal_electron = mfile.get("nd_plasma_pedestal_electron", scan=scan) - ne0 = mfile.get("nd_plasma_electron_on_axis", scan=scan) radius_plasma_pedestal_density_norm = mfile.get( "radius_plasma_pedestal_density_norm", scan=scan ) @@ -8665,22 +8663,21 @@ def plot_sol_power_decay_length_comparison(axis: plt.Axes, mfile: MFile, scan: i f"{OutbordSOLPowerDecayLengthModel.EICH_2011_JET.description}": len_plasma_sol_eich11_jet_power_decay_mm, f"{OutbordSOLPowerDecayLengthModel.EICH_2011_JET_ASDEX.description}": len_plasma_sol_eich11_jet_asdex_power_decay_mm, } + data_values = list(data.values()) + # Create the violin plot - axis.violinplot(data.values(), showextrema=False) + axis.violinplot(data_values, showextrema=False) # Create the box plot - axis.boxplot( - data.values(), showfliers=True, showmeans=True, meanline=True, widths=0.3 - ) + axis.boxplot(data_values, showfliers=True, showmeans=True, meanline=True, widths=0.3) # Scatter plot for each data point - colors = plt.cm.plasma(np.linspace(0, 1, len(data.values()))) + colors = plt.cm.plasma(np.linspace(0, 1, len(data_values))) for index, (key, value) in enumerate(data.items()): axis.scatter(1, value, color=colors[index], label=key, alpha=1.0) axis.legend(loc="upper left", bbox_to_anchor=(1, 1)) # Calculate average, standard deviation, and median - data_values = list(data.values()) avg_decay_length = np.mean(data_values) std_decay_length = np.std(data_values) median_decay_length = np.median(data_values) @@ -15278,7 +15275,6 @@ def plot_cs_radial_stress_contour_profile( linewidth=2, ) - # Use a dedicated colorbar axes when provided so the main axes width is unchanged. cbar = _colourbar(contour_fill, axis, colorbar_axis) cbar.set_label("Radial Stress (MPa)") @@ -15369,7 +15365,6 @@ def plot_cs_hoop_stress_contour_profile( linewidth=2, ) - # Use a dedicated colorbar axes when provided so the main axes width is unchanged. cbar = _colourbar(contour_fill, axis, colorbar_axis) cbar.set_label("Hoop Stress (MPa)") @@ -15480,7 +15475,6 @@ def plot_vertical_stress_contour_profile( linewidth=2, ) - # Use a dedicated colorbar axes when provided so the main axes width is unchanged. cbar = _colourbar(contour_fill, axis, colorbar_axis) cbar.set_label("Vertical Stress (MPa)") @@ -15586,7 +15580,6 @@ def plot_cs_tresca_2d_contour( linewidth=2, ) - # Use a dedicated colorbar axes when provided so the main axes width is unchanged. contour_fill = axis.contourf(r, z, tresca_data, levels=15, cmap="RdYlBu_r") cbar = _colourbar(contour_fill, axis, colorbar_axis) cbar.set_label("Tresca Stress (MPa)") @@ -15697,7 +15690,6 @@ def plot_cs_von_mises_2d_contour( linewidth=2, ) - # Use a dedicated colorbar axes when provided so the main axes width is unchanged. contour_fill = axis.contourf(r, z, von_mises_data, levels=15, cmap="RdYlBu_r") cbar = _colourbar(contour_fill, axis, colorbar_axis) cbar.set_label("Von Mises Stress (MPa)") @@ -15851,6 +15843,7 @@ def plot_pf_dimensions( axis.set_aspect("equal", adjustable="box") +<<<<<<< HEAD def plot_plasma_thermal_energy_profiles(axis, m_file: MFile, scan: int): """Function to plot plasma thermal energy profiles on the given axis. @@ -16059,6 +16052,8 @@ def plot_cumulative_plasma_thermal_energy_profiles(axis, m_file: MFile, scan: in ) +======= +>>>>>>> 36d00592d (repetition and unused vars) def main_plot( m_file: MFile, scan: int, From 328c145d7ced030d7eeafa7b5dd82de7603dbc86 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:21:04 +0100 Subject: [PATCH 09/18] cleanup --- process/core/io/plot/summary.py | 140 +++++++++++++++----------------- 1 file changed, 65 insertions(+), 75 deletions(-) diff --git a/process/core/io/plot/summary.py b/process/core/io/plot/summary.py index d7a60b450d..b51498f906 100644 --- a/process/core/io/plot/summary.py +++ b/process/core/io/plot/summary.py @@ -193,7 +193,7 @@ def _box_style(colour: str): white_box = {"boxstyle": "round", "facecolor": "white", "alpha": 1.0} -def text_layout(fig): +def _text_layout(fig): return { "fontsize": 9, "verticalalignment": "bottom", @@ -202,7 +202,7 @@ def text_layout(fig): } -def setup_axis(axis, xmin, xmax, ymin, ymax): +def _setup_axis(axis, xmin, xmax, ymin, ymax): axis.set_ylim(ymin, ymax) axis.set_xlim(xmin, xmax) axis.set_axis_off() @@ -745,7 +745,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.37, 0.775, f"$P_{{\\text{{neutron}}}}$:\n{mfile.get('p_neutron_total_mw', scan=scan):,.2f} MW", - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "grey", @@ -765,7 +765,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.0725, 0.83, f"$P_{{\\text{{HCD,primary}}}}$: {mfile.get('p_hcd_primary_injected_mw', scan=scan) + mfile.get('p_hcd_primary_extra_heat_mw', scan=scan):.2f} MW", - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("lightyellow"), ) @@ -774,7 +774,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.0725, 0.725, f"$P_{{\\text{{HCD,secondary}}}}$: {mfile.get('p_hcd_secondary_injected_mw', scan=scan) + mfile.get('p_hcd_secondary_extra_heat_mw', scan=scan):.2f} MW", - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("lightyellow"), ) @@ -824,7 +824,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.04, 0.45, "\n\nH&CD Power Supply\n\n", - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("lightyellow"), zorder=4, ) @@ -850,7 +850,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.2, 0.435, f"$P_{{\\text{{secondary,loss}}}}$:\n{mfile.get('p_hcd_secondary_electric_mw', scan=scan) * (1.0 - mfile.get('eta_hcd_secondary_injector_wall_plug', scan=scan)):.2f} MWe", - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("lightblue") | {"linestyle": "dashed"}, ) @@ -923,7 +923,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.2, 0.485, f"$P_{{\\text{{primary,loss}}}}$:\n{mfile.get('p_hcd_primary_electric_mw', scan=scan) * (1.0 - mfile.get('eta_hcd_primary_injector_wall_plug', scan=scan)):.2f} MWe", - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("lightblue") | {"linestyle": "dashed"}, ) @@ -960,7 +960,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.12, 0.35, f"$P_{{\\text{{secondary}}}}$:\n{mfile.get('p_hcd_secondary_electric_mw', scan=scan):.2f} MWe \n$\\eta$: {mfile.get('eta_hcd_secondary_injector_wall_plug', scan=scan):.2f}", - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("lightyellow"), ) @@ -969,7 +969,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.025, 0.35, f"$P_{{\\text{{primary}}}}$:\n{mfile.get('p_hcd_primary_electric_mw', scan=scan):.2f} MWe\n$\\eta$: {mfile.get('eta_hcd_primary_injector_wall_plug', scan=scan):.2f}", - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("lightyellow"), ) @@ -1012,7 +1012,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.9, 0.25, f"$P_{{\\text{{primary,thermal}}}}$:\n{mfile.get('p_plant_primary_heat_mw', scan=scan):,.2f} MW \n$\\eta_{{\\text{{turbine}}}}$: {mfile.get('eta_turbine', scan=scan):.3f}", - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("orange"), ) @@ -1077,7 +1077,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.79, 0.16, "Generator", - **text_layout(fig), + **_text_layout(fig), zorder=20, ) @@ -1128,7 +1128,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.68, 0.15, f"$P_{{\\text{{gross}}}}$:\n{mfile.get('p_plant_electric_gross_mw', scan=scan):,.2f} MWe", - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("lime"), ) @@ -1152,7 +1152,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.875, 0.05, f"$P_{{\\text{{loss}}}}$:\n{mfile.get('p_turbine_loss_mw', scan=scan):,.2f} MWth", - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("orange") | {"linestyle": "dashed"}, ) @@ -1176,7 +1176,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.68, 0.05, f"$P_{{\\text{{net,electric}}}}$:\n{mfile.get('p_plant_electric_net_mw', scan=scan):,.2f} MWe", - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("lime"), ) @@ -1188,7 +1188,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur f"$P_{{\\text{{recirc,electric}}}}$:\n{mfile.get('p_plant_electric_recirc_mw', scan=scan):,.2f} MWe\n" f"$f_{{\\text{{recirc}}}}$:\n{mfile.get('f_p_plant_electric_recirc', scan=scan):,.2f}" ), - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("lime"), ) @@ -1313,7 +1313,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.46, 0.85, f"$P_{{\\text{{FW, }}\\alpha}}$:\n{mfile.get('p_fw_alpha_mw', scan=scan):.2f} MW", - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("red"), ) @@ -1322,7 +1322,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.46, 0.775, f"$P_{{\\text{{FW,nuclear}}}}$:\n{mfile.get('p_fw_nuclear_heat_total_mw', scan=scan):,.2f} MW", - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "grey", @@ -1336,7 +1336,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.46, 0.71, f"$P_{{\\text{{FW,rad}}}}$:\n{mfile.get('p_fw_rad_total_mw', scan=scan):,.2f} MW", - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "dodgerblue", @@ -1395,7 +1395,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.5, 0.555, f"Primary thermal\n(inc pump): {mfile.get('p_fw_heat_deposited_mw', scan=scan):,.2f} MWth", - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "orange", @@ -1408,7 +1408,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.7, 0.555, f"Primary thermal\n(inc pump): {mfile.get('p_blkt_heat_deposited_mw', scan=scan):,.2f} MWth", - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "orange", @@ -1421,7 +1421,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.875, 0.555, f"Primary thermal:\n{mfile.get('p_shld_heat_deposited_mw', scan=scan):.2f} MWth", - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "orange", @@ -1509,7 +1509,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.6, 0.49, f"Primary thermal (inc pump): {mfile.get('p_fw_blkt_heat_deposited_mw', scan=scan):,.2f} MWth\n", - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("orange"), ) @@ -1546,7 +1546,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur f"$P_{{\\text{{Blkt,multiplication}}}}$:\n{mfile.get('p_blkt_multiplication_mw', scan=scan):,.2f} MW\n" f"$f_{{\\text{{multiplication}}}}$:\n{mfile.get('f_p_blkt_multiplication', scan=scan):,.2f}" ), - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "grey", @@ -1582,7 +1582,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.38, 0.375, f"$P_{{\\text{{shld,secondary}}}}$:\n{mfile.get('p_shld_secondary_heat_mw', scan=scan):,.2f} MWth", - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("lightblue") | {"linestyle": "dashed"}, ) @@ -1664,7 +1664,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.29, 0.57, f"$P_{{\\text{{div,rad}}}}$:\n{mfile.get('p_div_rad_total_mw', scan=scan):,.2f} MW", - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "dodgerblue", @@ -1678,7 +1678,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.4, 0.58, f"$P_{{\\text{{div,nuclear}}}}$:\n{mfile.get('p_div_nuclear_heat_total_mw', scan=scan):,.2f} MW", - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "grey", @@ -1696,7 +1696,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur f"Solid angle fraction: {mfile.get('f_ster_div_single', scan=scan):.3f}\n" f"Primary heat fraction: {mfile.get('f_p_div_primary_heat', scan=scan):.3f}" ), - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "orange", @@ -1710,7 +1710,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.3, 0.375, f"$P_{{\\text{{div,secondary}}}}$:\n{mfile.get('p_div_secondary_heat_mw', scan=scan):.2f} MWth", - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("lightblue") | {"linestyle": "dashed"}, ) @@ -1772,7 +1772,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.55, 0.33, f"$P_{{\\text{{div,pump}}}}$: {mfile.get('p_div_coolant_pump_mw', scan=scan):.2f} MW", - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "wheat", @@ -1831,7 +1831,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.875, 0.325, f"$P_{{\\text{{shld,pump}}}}$:\n{mfile.get('p_shld_coolant_pump_mw', scan=scan):.2f} MW", - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "wheat", @@ -1845,7 +1845,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.725, 0.4, f"$P_{{\\text{{FW + Blkt}}}}$:\n{mfile.get('p_fw_blkt_coolant_pump_mw', scan=scan):.2f} MW", - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "wheat", @@ -1907,7 +1907,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur f"Coolant pumps electric:\n{mfile.get('p_coolant_pump_elec_total_mw', scan=scan):.3f} MWe\n" f"$\\eta$: {mfile.get('eta_coolant_pump_electric', scan=scan):.3f}" ), - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "lime", @@ -1921,7 +1921,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.7, 0.325, f"Coolant pumps total:\n{mfile.get('p_coolant_pump_total_mw', scan=scan):.3f} MW", - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "wheat", @@ -1950,7 +1950,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.5, 0.235, f"Coolant pumps losses total:\n{mfile.get('p_coolant_pump_loss_total_mw', scan=scan):.3f} MWth", - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "lightblue", @@ -2003,7 +2003,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.49, 0.05, f"Cryo Plant:\n{mfile.get('p_cryo_plant_electric_mw', scan=scan):.3f} MWe", - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "burlywood", @@ -2032,7 +2032,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.4, 0.05, f"Tritium Plant:\n{mfile.get('p_tritium_plant_electric_mw', scan=scan):.3f} MWe", - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "burlywood", @@ -2061,7 +2061,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.575, 0.05, f"Vacuum pumps:\n{mfile.get('vachtmw', scan=scan):.3f} MWe", - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "burlywood", @@ -2094,7 +2094,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur f"Minimum base load:\n{mfile.get('p_plant_electric_base', scan=scan) * 1.0e-6:.3f} MWe\n" f"Plant floor power density:\n{mfile.get('pflux_plant_floor_electric', scan=scan) * 1.0e-3:.3f} kW$\\text{{m}}^{{-2}}$" ), - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "burlywood", @@ -2108,7 +2108,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.325, 0.075, f"TF coils:\n{mfile.get('p_tf_electric_supplies_mw', scan=scan):.3f} MWe", - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "burlywood", @@ -2122,7 +2122,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.25, 0.05, f"PF coils:\n{mfile.get('p_pf_electric_supplies_mw', scan=scan):.3f} MWe", - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "burlywood", @@ -2181,7 +2181,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.46, 0.285, f"$P_{{\\text{{HCD,loss}}}}$:\n{mfile.get('p_hcd_secondary_heat_mw', scan=scan):.2f} MWth", - **text_layout(fig), + **_text_layout(fig), bbox={ "boxstyle": "round", "facecolor": "lightblue", @@ -2228,7 +2228,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.155, 0.25, f"$P_{{\\text{{TF,nuclear}}}}$:\n{mfile.get('p_tf_nuclear_heat_mw', scan=scan):.2f} MWth", - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("lightblue") | {"linestyle": "dashed"}, ) @@ -7984,7 +7984,7 @@ def plot_header(axis: plt.Axes, mfile: MFile, scan: int): scan : scan number to use """ - setup_axis(axis, xmin=0, xmax=1, ymin=-16, ymax=1) + _setup_axis(axis, xmin=0, xmax=1, ymin=-16, ymax=1) data2 = [ (f"!{mfile.get('runtitle', scan=-1)}", "Run title", ""), @@ -8103,7 +8103,7 @@ def plot_geometry_info(axis: plt.Axes, mfile: MFile, scan: int): ymax = 1 axis.text(-0.05, 1, "Geometry:", ha="left", va="center") - setup_axis(axis, xmin, xmax, ymin, ymax) + _setup_axis(axis, xmin, xmax, ymin, ymax) in_blanket_thk = mfile.get("dr_shld_inboard", scan=scan) + mfile.get( "dr_blkt_inboard", scan=scan @@ -8148,7 +8148,7 @@ def plot_physics_info(axis: plt.Axes, mfile: MFile, scan: int): ymax = 1 axis.text(-0.05, 1, "Physics:", ha="left", va="center") - setup_axis(axis, xmin, xmax, ymin, ymax) + _setup_axis(axis, xmin, xmax, ymin, ymax) nong = mfile.get("nd_plasma_electron_line", scan=scan) / mfile.get( "nd_plasma_electron_max_array(7)", scan=scan @@ -8219,7 +8219,7 @@ def plot_magnetics_info(axis: plt.Axes, mfile: MFile, scan: int): i_tf_sup = int(mfile.get("i_tf_sup", scan=scan)) if "i_tf_sup" in mfile.data else 1 axis.text(-0.05, 1, "Coil currents etc:", ha="left", va="center") - setup_axis(axis, xmin=0, xmax=1, ymin=-16, ymax=1) + _setup_axis(axis, xmin=0, xmax=1, ymin=-16, ymax=1) # Number of coils (1 is OH coil) number_of_coils = 0 @@ -8335,7 +8335,7 @@ def plot_power_info(axis: plt.Axes, mfile: MFile, scan: int): scan number to use """ axis.text(-0.05, 1, "Power flows:", ha="left", va="center") - setup_axis(axis, xmin=0, xmax=1, ymin=-16, ymax=1) + _setup_axis(axis, xmin=0, xmax=1, ymin=-16, ymax=1) gross_eff = 100.0 * ( mfile.get("p_plant_electric_gross_mw", scan=scan) @@ -8416,7 +8416,7 @@ def plot_current_drive_info(axis: plt.Axes, mfile: MFile, scan: int): scan : scan number to use """ - setup_axis(axis, xmin=0, xmax=1, ymin=-16, ymax=1) + _setup_axis(axis, xmin=0, xmax=1, ymin=-16, ymax=1) i_hcd_primary = mfile.get("i_hcd_primary", scan=scan) @@ -9332,14 +9332,13 @@ def plot_confinement_time_comparison( rf"{ConfinementTimeModel.ITPA20.full_name}": itpa20, rf"{ConfinementTimeModel.ITPA20_IL.full_name}": itpa20_ilc, } + data_values = list(data.values()) # Create the violin plot - axis.violinplot(data.values(), showextrema=False) + axis.violinplot(data_values, showextrema=False) # Create the box plot - axis.boxplot( - data.values(), showfliers=True, showmeans=True, meanline=True, widths=0.3 - ) + axis.boxplot(data_values, showfliers=True, showmeans=True, meanline=True, widths=0.3) # Scatter plot for each data point # Use a set of distinct colors for better differentiation @@ -9384,7 +9383,6 @@ def plot_confinement_time_comparison( axis.legend(loc="upper left", bbox_to_anchor=(-1.3, 0.75), ncol=2) # Calculate average, standard deviation, and median - data_values = list(data.values()) avg_threshold = np.mean(data_values) std_threshold = np.std(data_values) median_threshold = np.median(data_values) @@ -9909,14 +9907,13 @@ def plot_density_limit_comparison(axis: plt.Axes, mfile: MFile, scan: int): "Greenwald": greenwald, "ASDEX New": asdex_new, } + data_values = list(data.values()) # Create the violin plot - axis.violinplot(data.values(), showextrema=False) + axis.violinplot(data_values, showextrema=False) # Create the box plot - axis.boxplot( - data.values(), showfliers=True, showmeans=True, meanline=True, widths=0.3 - ) + axis.boxplot(data_values, showfliers=True, showmeans=True, meanline=True, widths=0.3) # Scatter plot for each data point colors = plt.cm.plasma(np.linspace(0, 1, len(data.values()))) @@ -9925,7 +9922,6 @@ def plot_density_limit_comparison(axis: plt.Axes, mfile: MFile, scan: int): axis.legend(loc="upper left", bbox_to_anchor=(1, 1)) # Calculate average, standard deviation, and median - data_values = list(data.values()) avg_density_limit = np.mean(data_values) std_density_limit = np.std(data_values) median_density_limit = np.median(data_values) @@ -10132,13 +10128,7 @@ def plot_cs_coil_structure( f"$\\tau_{{\\text{{shear,peak}}}}:$ {mfile.get('stress_shear_cs_peak', scan=scan) / 1e6:.3f} MPa " ) - axis.text( - 0.5, - 0.6, - textstr_cs, - **text_layout(fig), - bbox=_box_style("lightyellow"), - ) + axis.text(0.5, 0.6, textstr_cs, **_text_layout(fig), bbox=_box_style("lightyellow")) # Plot the current filament points as blue dots and label them @@ -10282,7 +10272,7 @@ def plot_cs_turn_structure(axis: plt.Axes, fig, mfile: MFile, scan: int): 0.7, 0.375, textstr_turn, - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("lightyellow"), ) @@ -11680,7 +11670,7 @@ def plot_fusion_rate_profiles(axis: plt.Axes, fig, mfile: MFile, scan: int): 0.05, 0.85, textstr_general, - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("lightyellow"), ) @@ -11697,7 +11687,7 @@ def plot_fusion_rate_profiles(axis: plt.Axes, fig, mfile: MFile, scan: int): 0.05, 0.75, textstr_dt, - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("lightyellow"), ) @@ -11722,7 +11712,7 @@ def plot_fusion_rate_profiles(axis: plt.Axes, fig, mfile: MFile, scan: int): 0.05, 0.65, textstr_dd, - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("lightyellow"), ) @@ -11746,7 +11736,7 @@ def plot_fusion_rate_profiles(axis: plt.Axes, fig, mfile: MFile, scan: int): 0.05, 0.55, textstr_dhe3, - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("lightyellow"), ) @@ -11777,7 +11767,7 @@ def plot_fusion_rate_profiles(axis: plt.Axes, fig, mfile: MFile, scan: int): 0.05, 0.25, textstr_alpha, - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("red"), ) @@ -11804,7 +11794,7 @@ def plot_fusion_rate_profiles(axis: plt.Axes, fig, mfile: MFile, scan: int): 0.05, 0.1, textstr_neutron, - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("grey"), ) @@ -14523,7 +14513,7 @@ def plot_blkt_structure( 0.05, 0.3, textstr_blkt_areas, - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("wheat"), ) @@ -14541,7 +14531,7 @@ def plot_blkt_structure( 0.05, 0.05, textstr_blkt_volumes, - **text_layout(fig), + **_text_layout(fig), bbox=_box_style("wheat"), ) From 0f2c45282c61549173c1d76025384274fe9494ec Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:26:47 +0100 Subject: [PATCH 10/18] cleanup --- process/core/io/plot/summary.py | 168 +++++++++++++++----------------- 1 file changed, 80 insertions(+), 88 deletions(-) diff --git a/process/core/io/plot/summary.py b/process/core/io/plot/summary.py index b51498f906..4fb2685082 100644 --- a/process/core/io/plot/summary.py +++ b/process/core/io/plot/summary.py @@ -959,7 +959,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur axis.text( 0.12, 0.35, - f"$P_{{\\text{{secondary}}}}$:\n{mfile.get('p_hcd_secondary_electric_mw', scan=scan):.2f} MWe \n$\\eta$: {mfile.get('eta_hcd_secondary_injector_wall_plug', scan=scan):.2f}", + f"$P_{{\\text{{secondary}}}}$:\n{mfile.get('p_hcd_secondary_electric_mw', scan=scan):.2f} MWe\n$\\eta$: {mfile.get('eta_hcd_secondary_injector_wall_plug', scan=scan):.2f}", **_text_layout(fig), bbox=_box_style("lightyellow"), ) @@ -1011,7 +1011,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur axis.text( 0.9, 0.25, - f"$P_{{\\text{{primary,thermal}}}}$:\n{mfile.get('p_plant_primary_heat_mw', scan=scan):,.2f} MW \n$\\eta_{{\\text{{turbine}}}}$: {mfile.get('eta_turbine', scan=scan):.3f}", + f"$P_{{\\text{{primary,thermal}}}}$:\n{mfile.get('p_plant_primary_heat_mw', scan=scan):,.2f} MW\n$\\eta_{{\\text{{turbine}}}}$: {mfile.get('eta_turbine', scan=scan):.3f}", **_text_layout(fig), bbox=_box_style("orange"), ) @@ -1542,7 +1542,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur 0.625, 0.775, ( - f"$P_{{\\text{{Blkt,nuclear}}}}$:\n{mfile.get('p_blkt_nuclear_heat_total_mw', scan=scan):,.2f} MW \n" + f"$P_{{\\text{{Blkt,nuclear}}}}$:\n{mfile.get('p_blkt_nuclear_heat_total_mw', scan=scan):,.2f} MW\n" f"$P_{{\\text{{Blkt,multiplication}}}}$:\n{mfile.get('p_blkt_multiplication_mw', scan=scan):,.2f} MW\n" f"$f_{{\\text{{multiplication}}}}$:\n{mfile.get('f_p_blkt_multiplication', scan=scan):,.2f}" ), @@ -2432,7 +2432,7 @@ def plot_main_plasma_information( geom_type = PlasmaGeometryModelType(mfile.get("i_plasma_geometry", scan=scan)) textstr_plasma = ( - f"$\\mathbf{{Shaping:}}$\n \n" + f"$\\mathbf{{Shaping:}}$\n\n" f"$\\kappa_{{95}}$: {mfile.get('kappa95', scan=scan):.2f} ({geom_type.kappa95_model.description}) | $\\delta_{{95}}$: {mfile.get('triang95', scan=scan):.2f} ({geom_type.triang95_model.description}) | $\\zeta$: {mfile.get('plasma_square', scan=scan):.2f}\n" f"$\\kappa$: {mfile.get('kappa', scan=scan):.2f} ({geom_type.kappa_model.description}) | $\\delta$: {mfile.get('triang', scan=scan):.2f} ({geom_type.triang_model.description}) | A: {mfile.get('aspect', scan=scan):.2f}\n" f"$ V_{{\\text{{p}}}}:$ {mfile.get('vol_plasma', scan=scan):,.2f}$ \\ \\text{{m}}^3$ | $ A_{{\\text{{p,surface}}}}:$ {mfile.get('a_plasma_surface', scan=scan):,.2f}$ \\ \\text{{m}}^2$ | $ A_{{\\text{{p,poloidal}}}}:$ {mfile.get('a_plasma_poloidal', scan=scan):,.3f}$ \\ \\text{{m}}^2$\n" @@ -2468,19 +2468,19 @@ def plot_main_plasma_information( # Add heating and current drive information textstr_hcd = ( - f"$\\mathbf{{Heating \\ & \\ current \\ drive:}}$\n \n" - f"Total injected heat: {mfile.get('p_hcd_injected_total_mw', scan=scan):.3f} MW \n" - f"Ohmic heating power: {mfile.get('p_plasma_ohmic_mw', scan=scan):.3f} MW \n\n" - f"$\\mathbf{{Primary \\ system: {CurrentDriveModel(i_hcd_primary).abbreviation}}}$ \n" + f"$\\mathbf{{Heating \\ & \\ current \\ drive:}}$\n\n" + f"Total injected heat: {mfile.get('p_hcd_injected_total_mw', scan=scan):.3f} MW\n" + f"Ohmic heating power: {mfile.get('p_plasma_ohmic_mw', scan=scan):.3f} MW\n\n" + f"$\\mathbf{{Primary \\ system: {CurrentDriveModel(i_hcd_primary).abbreviation}}}$\n" f"Current driving power {mfile.get('p_hcd_primary_injected_mw', scan=scan):.4f} MW\n" f"Extra heat power: {mfile.get('p_hcd_primary_extra_heat_mw', scan=scan):.4f} MW\n" - f"$\\eta_{{\\text{{CD,prim}}}}$: {mfile.get('eta_cd_hcd_primary', scan=scan):.4f} A/W | $\\langle\\zeta_{{\\text{{CD,prim}}}}\\rangle$: {mfile.get('eta_cd_dimensionless_hcd_primary', scan=scan):.4f} \n" + f"$\\eta_{{\\text{{CD,prim}}}}$: {mfile.get('eta_cd_hcd_primary', scan=scan):.4f} A/W | $\\langle\\zeta_{{\\text{{CD,prim}}}}\\rangle$: {mfile.get('eta_cd_dimensionless_hcd_primary', scan=scan):.4f}\n" f"$\\gamma_{{\\text{{CD,prim}}}}$: {mfile.get('eta_cd_norm_hcd_primary', scan=scan):.4f} $\\times 10^{{20}} \\mathrm{{A}} / \\mathrm{{Wm}}^2$\n" f"Current driven by primary: {mfile.get('c_hcd_primary_driven', scan=scan) / 1e6:.3f} MA\n\n" - f"$\\mathbf{{Secondary \\ system: {CurrentDriveModel(i_hcd_secondary).abbreviation}}}$ \n" + f"$\\mathbf{{Secondary \\ system: {CurrentDriveModel(i_hcd_secondary).abbreviation}}}$\n" f"Current driving power {mfile.get('p_hcd_secondary_injected_mw', scan=scan):.4f} MW\n" f"Extra heat power: {mfile.get('p_hcd_secondary_extra_heat_mw', scan=scan):.4f} MW\n" - f"$\\eta_{{\\text{{CD,sec}}}}$: {mfile.get('eta_cd_hcd_secondary', scan=scan):.4f} A/W | $\\langle\\zeta_{{\\text{{CD,sec}}}}\\rangle$: {mfile.get('eta_cd_dimensionless_hcd_secondary', scan=scan):.4f} \n" + f"$\\eta_{{\\text{{CD,sec}}}}$: {mfile.get('eta_cd_hcd_secondary', scan=scan):.4f} A/W | $\\langle\\zeta_{{\\text{{CD,sec}}}}\\rangle$: {mfile.get('eta_cd_dimensionless_hcd_secondary', scan=scan):.4f}\n" f"$\\gamma_{{\\text{{CD,sec}}}}$: {mfile.get('eta_cd_norm_hcd_secondary', scan=scan):.4f} $\\times 10^{{20}} \\mathrm{{A}} / \\mathrm{{Wm}}^2$\n" f"Current driven by secondary: {mfile.get('c_hcd_secondary_driven', scan=scan) / 1e6:.3f} MA" ) @@ -2513,7 +2513,7 @@ class TextArgs(TypedDict): # Add beta information textstr_beta = ( - f"$\\mathbf{{Beta \\ Information:}}$\n \n" + f"$\\mathbf{{Beta \\ Information:}}$\n\n" f"Total beta,$ \\ \\langle \\beta \\rangle$: {mfile.get('beta_total_vol_avg', scan=scan):.4f}\n" f"Thermal beta,$ \\ \\langle \\beta_{{\\text{{thermal}}}} \\rangle$: {mfile.get('beta_thermal_vol_avg', scan=scan):.4f}\n" f"Toroidal beta,$ \\ \\langle \\beta_{{\\text{{t}}}} \\rangle$: {mfile.get('beta_toroidal_vol_avg', scan=scan):.4f}\n" @@ -2542,8 +2542,8 @@ class TextArgs(TypedDict): # Add volt-second information textstr_volt_second = ( - f"$\\mathbf{{Volt-second \\ requirements:}}$\n \n" - f"Total volt-second consumption: {mfile.get('vs_plasma_total_required', scan=scan):.4f} Vs \n" + f"$\\mathbf{{Volt-second \\ requirements:}}$\n\n" + f"Total volt-second consumption: {mfile.get('vs_plasma_total_required', scan=scan):.4f} Vs\n" f" - Internal volt-seconds: {mfile.get('vs_plasma_internal', scan=scan):.4f} Vs\n" f" - Volt-seconds needed for burn: {mfile.get('vs_plasma_burn_required', scan=scan):.4f} Vs\n" f" - Volt-seconds needed for ramp: {mfile.get('vs_plasma_ramp_required', scan=scan):.4f} Vs | $C_{{\\text{{ejima}}}}$: {mfile.get('ejima_coeff', scan=scan):.4f}\n" @@ -2572,8 +2572,8 @@ class TextArgs(TypedDict): # Add divertor information textstr_div = ( - f"\n$P_{{\\text{{sep}}}}$: {mfile.get('p_plasma_separatrix_mw', scan=scan):.2f} MW \n" - f"$\\frac{{P_{{\\text{{sep}}}}}}{{R}}$: {mfile.get('p_plasma_separatrix_rmajor_mw', scan=scan):.2f} MW/m \n" + f"\n$P_{{\\text{{sep}}}}$: {mfile.get('p_plasma_separatrix_mw', scan=scan):.2f} MW\n" + f"$\\frac{{P_{{\\text{{sep}}}}}}{{R}}$: {mfile.get('p_plasma_separatrix_rmajor_mw', scan=scan):.2f} MW/m\n" f"$\\frac{{P_{{\\text{{sep}}}}B_T}}{{q_{{95}} A R}}$: {mfile.get('p_div_bt_q_aspect_rmajor_mw', scan=scan):.2f} MW T/m " ) @@ -2594,7 +2594,7 @@ class TextArgs(TypedDict): # Add confinement information textstr_confinement = ( - f"$\\mathbf{{Confinement:}}$\n \n" + f"$\\mathbf{{Confinement:}}$\n\n" f"Confinement scaling law: {mfile.get('tauelaw', scan=scan)}\n" f"Confinement $H$ factor: {mfile.get('hfact', scan=scan):.4f}\n" f"Energy confinement time from scaling: {mfile.get('t_energy_confinement', scan=scan):.4f} s\n" @@ -2643,7 +2643,7 @@ class TextArgs(TypedDict): ) textstr_alpha = ( - f"$P_{{\\alpha,\\text{{loss}}}}$ {mfile.get('p_fw_alpha_mw', scan=scan):.2f} MW \n" + f"$P_{{\\alpha,\\text{{loss}}}}$ {mfile.get('p_fw_alpha_mw', scan=scan):.2f} MW\n" f"$f_{{\\alpha,\\text{{coupled}}}}$ {mfile.get('f_p_alpha_plasma_deposited', scan=scan):.2f}" ) @@ -2677,7 +2677,7 @@ class TextArgs(TypedDict): ) textstr_neutron = ( - f"$P_{{\\text{{n,total}}}}$ {mfile.get('p_neutron_total_mw', scan=scan):.2f} MW \n" + f"$P_{{\\text{{n,total}}}}$ {mfile.get('p_neutron_total_mw', scan=scan):.2f} MW\n" f"$\\phi_{{\\text{{n,avg}}}}$ {mfile.get('pflux_plasma_surface_neutron_avg_mw', scan=scan):.3f} MW/m²" ) @@ -2695,8 +2695,8 @@ class TextArgs(TypedDict): # Add fusion reaction information textstr_reactions = ( - f"$\\mathbf{{Fusion \\ Reactions:}}$\n \n" - f"Fuel mixture: \n" + f"$\\mathbf{{Fusion \\ Reactions:}}$\n\n" + f"Fuel mixture:\n" f"| D: {mfile.get('f_plasma_fuel_deuterium', scan=scan):.2f} | T: {mfile.get('f_plasma_fuel_tritium', scan=scan):.2f} | 3He: {mfile.get('f_plasma_fuel_helium3', scan=scan):.2f} |\n\n" f"Fusion Power, $P_{{\\text{{fus}}}}:$ {mfile.get('p_fusion_total_mw', scan=scan):,.2f} MW\n" f"D-T Power, $P_{{\\text{{fus,DT}}}}:$ {mfile.get('p_dt_total_mw', scan=scan):,.2f} MW\n" @@ -2719,14 +2719,14 @@ class TextArgs(TypedDict): # Add fuelling information textstr_fuelling = ( - f"$\\mathbf{{Fuelling:}}$\n \n" + f"$\\mathbf{{Fuelling:}}$\n\n" f"Plasma mass: {mfile.get('m_plasma', scan=scan) * 1000:.4f} g\n" f" - Average mass of all plasma ions: {mfile.get('m_ions_total_amu', scan=scan):.3f} amu\n" f"Fuel mass: {mfile.get('m_plasma_fuel_ions', scan=scan) * 1000:.4f} g\n" f" - Average mass of all fuel ions: {mfile.get('m_fuel_amu', scan=scan):.3f} amu\n\n" f"Fueling rate: {mfile.get('molflow_plasma_fuelling_required', scan=scan):.3e} nucleus-pairs/s\n" - f"Fuel burn-up rate: {mfile.get('rndfuel', scan=scan):.3e} reactions/s \n" - f"Burn-up fraction: {mfile.get('burnup', scan=scan):.4f} \n" + f"Fuel burn-up rate: {mfile.get('rndfuel', scan=scan):.3e} reactions/s\n" + f"Burn-up fraction: {mfile.get('burnup', scan=scan):.4f}\n" ) axis.text( @@ -2746,7 +2746,7 @@ class TextArgs(TypedDict): textstr_ions = ( f" $\\mathbf{{Ion \\ to \\ electron}}$\n" f" $\\mathbf{{relative \\ number}}$\n" - f" $\\mathbf{{densities:}}$\n \n" + f" $\\mathbf{{densities:}}$\n\n" f" Effective charge: {mfile.get('n_charge_plasma_effective_vol_avg', scan=scan):.3f}\n\n" + "\n".join( f" {label.replace('_', '') + ':':<6}" @@ -2778,7 +2778,7 @@ class TextArgs(TypedDict): # Add plasma current information textstr_currents = ( f"$\\mathbf{{Plasma\\ currents:}}$\n\n" - f"Plasma current ({PlasmaCurrentModel(int(mfile.get('i_plasma_current', scan=scan))).full_name}): {mfile.get('plasma_current_ma', scan=scan):.4f} MA \n" + f"Plasma current ({PlasmaCurrentModel(int(mfile.get('i_plasma_current', scan=scan))).full_name}): {mfile.get('plasma_current_ma', scan=scan):.4f} MA\n" f" - Bootstrap fraction ({BootstrapCurrentFractionModel(int(mfile.get('i_bootstrap_current', scan=scan))).full_name}): {mfile.get('f_c_plasma_bootstrap', scan=scan):.4f}\n" f" - Diamagnetic fraction ({PlasmaDiamagneticCurrentModel(int(mfile.get('i_diamagnetic_current', scan=scan))).full_name}): {mfile.get('f_c_plasma_diamagnetic', scan=scan):.4f}\n" f" - Pfirsch-Schlüter fraction {mfile.get('f_c_plasma_pfirsch_schluter', scan=scan):.4f}\n" @@ -2802,10 +2802,10 @@ class TextArgs(TypedDict): # Add magnetic field information textstr_fields = ( f"$\\mathbf{{Magnetic\\ fields:}}$\n\n" - f"Toroidal field at $R_0$, $B_{{T}}$: {mfile.get('b_plasma_toroidal_on_axis', scan=scan):.4f} T \n" - f" Ripple at outboard , $\\delta$: {mfile.get('ripple_b_tf_plasma_edge', scan=scan):.2f}% \n" + f"Toroidal field at $R_0$, $B_{{T}}$: {mfile.get('b_plasma_toroidal_on_axis', scan=scan):.4f} T\n" + f" Ripple at outboard , $\\delta$: {mfile.get('ripple_b_tf_plasma_edge', scan=scan):.2f}%\n" f"Surface average poloidal field, $\\langle B_{{p}}(a) \\rangle$: {mfile.get('b_plasma_surface_poloidal_average', scan=scan):.4f} T\n" - f"Total field, $B_{{tot}}$: {mfile.get('b_plasma_total', scan=scan):.4f} T \n" + f"Total field, $B_{{tot}}$: {mfile.get('b_plasma_total', scan=scan):.4f} T\n" f"Vertical field, $B_{{vert}}$: {mfile.get('b_plasma_vertical_required', scan=scan):.4f} T" ) @@ -3097,8 +3097,8 @@ def plot_system_power_profiles_over_time(axis: plt.Axes, mfile: MFile, scan: int # Add energy produced info textstr_energy = ( f"$\\mathbf{{Energy \\ Production:}}$\n\n" - f"Energy produced over whole pulse: {mfile.get('e_plant_net_electric_pulse_mj', scan=scan):,.4f} MJ \n" - f"Energy produced over whole pulse: {mfile.get('e_plant_net_electric_pulse_kwh', scan=scan):,.4f} kWh \n" + f"Energy produced over whole pulse: {mfile.get('e_plant_net_electric_pulse_mj', scan=scan):,.4f} MJ\n" + f"Energy produced over whole pulse: {mfile.get('e_plant_net_electric_pulse_kwh', scan=scan):,.4f} kWh\n" ) axis.text( @@ -6244,7 +6244,7 @@ def plot_superconducting_tf_wp(axis: plt.Axes, mfile: MFile, scan: int, fig): # Add info about the steel casing surrounding the WP textstr_casing = ( - f"$\\mathbf{{Casing:}}$\n \n" + f"$\\mathbf{{Casing:}}$\n\n" f"Coil half angle: {mfile.get('rad_tf_coil_inboard_toroidal_half', scan=scan):.3f} radians\n\n" f"$\\text{{Full Coil Case:}}$\n" f"$r_{{start}} \\rightarrow r_{{end}}$: {mfile.get('r_tf_inboard_in', scan=scan):.3f} $\\rightarrow$ {mfile.get('r_tf_inboard_out', scan=scan):.3f} m\n" @@ -6281,7 +6281,7 @@ def plot_superconducting_tf_wp(axis: plt.Axes, mfile: MFile, scan: int, fig): # Add info about the steel casing surrounding the WP textstr_wp_insulation = ( - f"$\\mathbf{{Ground \\ Insulation:}}$\n \n" + f"$\\mathbf{{Ground \\ Insulation:}}$\n\n" f"Area of insulation around WP: {mfile.get('a_tf_wp_ground_insulation', scan=scan):.3f} $\\mathrm{{m}}^2$\n" f"$\\Delta r$: {mfile.get('dx_tf_wp_insulation', scan=scan):.4f} m\n\n" f"WP Insertion Gap:\n" @@ -6305,7 +6305,7 @@ def plot_superconducting_tf_wp(axis: plt.Axes, mfile: MFile, scan: int, fig): # Add info about the Winding Pack textstr_wp = ( - f"$\\mathbf{{Winding \\ Pack:}}$\n \n" + f"$\\mathbf{{Winding \\ Pack:}}$\n\n" f"$N_{{\\text{{turns}}}}$: " f"{int(mfile.get('n_tf_coil_turns', scan=scan))} turns\n" f"$r_{{start}} \\rightarrow r_{{end}}$: {mfile.get('r_tf_wp_inboard_inner', scan=scan):.3f} $\\rightarrow$ {mfile.get('r_tf_wp_inboard_outer', scan=scan):.3f} m\n" @@ -6342,7 +6342,7 @@ def plot_superconducting_tf_wp(axis: plt.Axes, mfile: MFile, scan: int, fig): # Add info about the Winding Pack textstr_general_info = ( - f"$\\mathbf{{General \\ info:}}$\n \n" + f"$\\mathbf{{General \\ info:}}$\n\n" f"$N_{{\\text{{TF,coil}}}}$: {mfile.get('n_tf_coils', scan=scan)}\n" f"Self inductance of single coil: {mfile.get('ind_tf_coil', scan=scan) * 1e6:.4f} $\\mu$H\n" f"Stored energy of all coils: {mfile.get('e_tf_magnetic_stored_total_gj', scan=scan):.4f} GJ\n" @@ -6768,7 +6768,7 @@ def plot_resistive_tf_info(axis: plt.Axes, mfile: MFile, scan: int, fig): """Plot info about the resistive TF coils""" # Add info about the steel casing surrounding the WP textstr_casing = ( - f"$\\mathbf{{Casing:}}$\n \n" + f"$\\mathbf{{Casing:}}$\n\n" f"Coil half angle: {mfile.get('rad_tf_coil_inboard_toroidal_half', scan=scan):.3f} radians\n\n" f"$\\text{{Full Coil Case:}}$\n" f"$r_{{start}} \\rightarrow r_{{end}}$: {mfile.get('r_tf_inboard_in', scan=scan):.3f} $\\rightarrow$ {mfile.get('r_tf_inboard_out', scan=scan):.3f} m\n" @@ -6796,7 +6796,7 @@ def plot_resistive_tf_info(axis: plt.Axes, mfile: MFile, scan: int, fig): # Add info about the steel casing surrounding the WP textstr_wp_insulation = ( - f"$\\mathbf{{Insulation:}}$\n \n" + f"$\\mathbf{{Insulation:}}$\n\n" f"Area of insulation around WP: {mfile.get('a_tf_wp_ground_insulation', scan=scan):.3f} $\\mathrm{{m}}^2$\n" f"$\\Delta r$: {mfile.get('dx_tf_wp_insulation', scan=scan):.4f} m\n\n" f"$\\text{{Turn Insulation:}}$\n" @@ -6815,7 +6815,7 @@ def plot_resistive_tf_info(axis: plt.Axes, mfile: MFile, scan: int, fig): # Add info about the Winding Pack textstr_wp = ( - f"$\\mathbf{{Winding Pack:}}$\n \n" + f"$\\mathbf{{Winding Pack:}}$\n\n" f"$N_{{\\text{{turns}}}}$: " f"{int(mfile.get('n_tf_coil_turns', scan=scan))} turns\n" f"$r_{{start}} \\rightarrow r_{{end}}$: {mfile.get('r_tf_wp_inboard_inner', scan=scan):.3f} $\\rightarrow$ {mfile.get('r_tf_wp_inboard_outer', scan=scan):.3f} m\n" @@ -6840,7 +6840,7 @@ def plot_resistive_tf_info(axis: plt.Axes, mfile: MFile, scan: int, fig): # Add info about the Winding Pack textstr_general_info = ( - f"$\\mathbf{{General \\ info:}}$\n \n" + f"$\\mathbf{{General \\ info:}}$\n\n" f"Self inductance: {mfile.get('ind_tf_coil', scan=scan) * 1e6:.4f} $\\mu$H\n" f"Stored energy of all coils: {mfile.get('e_tf_magnetic_stored_total_gj', scan=scan):.4f} GJ\n" ) @@ -6857,7 +6857,7 @@ def plot_resistive_tf_info(axis: plt.Axes, mfile: MFile, scan: int, fig): # Add info about the Winding Pack textstr_cooling = ( - f"$\\mathbf{{Cooling \\ info:}}$\n \n" + f"$\\mathbf{{Cooling \\ info:}}$\n\n" f"Coolant inlet temperature: {mfile.get('temp_cp_coolant_inlet', scan=scan):.2f} K\n" f"Coolant temperature rise: {mfile.get('dtemp_cp_coolant', scan=scan):.2f} K\n" f"Coolant velocity: {mfile.get('vel_cp_coolant_midplane', scan=scan):.2f} $\\mathrm{{ms^{{-1}}}}$\n\n" @@ -7345,15 +7345,15 @@ def _pack_strands_rectangular_with_obstacles( f"$\\mathbf{{Cable \\ Space:}}$\n\n" f"$\\Delta r:$ {cable_space_width:.3e} m\n" f"Corner radius, $r$: {radius_tf_turn_cable_space_corners:.3e} m\n" - f"Cable area with no cooling \nchannel or gaps: {a_tf_turn_cable_space_no_void:.3e} m$^2$\n" + f"Cable area with no cooling\nchannel or gaps: {a_tf_turn_cable_space_no_void:.3e} m$^2$\n" f"Extra cable space area void fraction: {f_a_tf_turn_cable_space_extra_void}\n" f"True cable space area: {a_tf_turn_cable_space_effective:.3e} m$^2$" ) elif TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.INTEGER: textstr_turn_cable_space = ( f"$\\mathbf{{Cable \\ Space:}}$\n\n" - f"Cable space: \n$\\Delta r$: {cable_space_width_radial:.3e} m \n" - f"$\\Delta x$: {cable_space_width_toroidal:.3e} m \n" + f"Cable space:\n$\\Delta r$: {cable_space_width_radial:.3e} m\n" + f"$\\Delta x$: {cable_space_width_toroidal:.3e} m\n" f"Corner radius, $r$: {radius_tf_turn_cable_space_corners:.3e} m\n" f"Cable area with no cooling channel or gaps: {a_tf_turn_cable_space_no_void:.3e} m$^2$\n" f"Extra cable space area void fraction: {f_a_tf_turn_cable_space_extra_void}\n" @@ -7415,23 +7415,23 @@ def _pack_strands_rectangular_with_obstacles( ) textstr_superconductor = ( - f"$\\mathbf{{Superconductor:}}$\n \n" - f"Superconductor used: \n" + f"$\\mathbf{{Superconductor:}}$\n\n" + f"Superconductor used:\n" f"{SuperconductorModel(mfile.get('i_tf_sc_mat', scan=scan)).full_name}\n" - f"Critical field at zero \ntemperature and strain: {mfile.get('b_tf_superconductor_critical_zero_temp_strain', scan=scan):.4f} T\n" - f"Critical temperature at \nzero field and strain: {mfile.get('temp_tf_superconductor_critical_zero_field_strain', scan=scan):.4f} K\n" + f"Critical field at zero\ntemperature and strain: {mfile.get('b_tf_superconductor_critical_zero_temp_strain', scan=scan):.4f} T\n" + f"Critical temperature at\nzero field and strain: {mfile.get('temp_tf_superconductor_critical_zero_field_strain', scan=scan):.4f} K\n" f"Temperature at conductor: {mfile.get('tftmp', scan=scan):.4f} K\n" f"Field at conductor: {mfile.get('b_tf_inboard_peak_with_ripple', scan=scan):.4f} T\n" - f"Superconductor critical current density at \noperating conditions: {mfile.get('j_tf_superconductor_critical', scan=scan):.2e} A/m$^2$\n" + f"Superconductor critical current density at\noperating conditions: {mfile.get('j_tf_superconductor_critical', scan=scan):.2e} A/m$^2$\n" f"$I_{{\\text{{TF,turn critical}}}}$: {mfile.get('c_turn_cables_critical', scan=scan):,.2f} A\n" f"$I_{{\\text{{TF,turn}}}}$: {mfile.get('c_tf_turn', scan=scan):,.2f} A\n" f"Critcal current ratio: {mfile.get('f_c_tf_turn_operating_critical', scan=scan):,.4f}\n" - f"Superconductor temperature \nmargin: {mfile.get('temp_tf_superconductor_margin', scan=scan):,.4f} K\n" - f"\n$\\mathbf{{Quench:}}$\n \n" + f"Superconductor temperature\nmargin: {mfile.get('temp_tf_superconductor_margin', scan=scan):,.4f} K\n" + f"\n$\\mathbf{{Quench:}}$\n\n" f"Quench dump time: {mfile.get('t_tf_superconductor_quench', scan=scan):.4f} s\n" f"Quench detection time: {mfile.get('t_tf_quench_detection', scan=scan):.4f} s\n" - f"User input max temperature \nduring quench: {mfile.get('temp_tf_conductor_quench_max', scan=scan):.2f} K\n" - f"Required maxium WP current \ndensity for heat protection:\n{mfile.get('j_tf_wp_quench_heat_max', scan=scan):.2e} A/m$^2$\n" + f"User input max temperature\nduring quench: {mfile.get('temp_tf_conductor_quench_max', scan=scan):.2f} K\n" + f"Required maxium WP current\ndensity for heat protection:\n{mfile.get('j_tf_wp_quench_heat_max', scan=scan):.2e} A/m$^2$\n" ) axis.text( 0.75, @@ -7589,15 +7589,15 @@ def plot_tf_croco_turn(axis: plt.Axes, fig, mfile: MFile, scan: int): f"$\\mathbf{{Cable \\ Space:}}$\n\n" f"$\\Delta r:$ {cable_space_width:.3e} m\n" f"Corner radius, $r$: {radius_tf_turn_cable_space_corners:.3e} m\n" - f"Cable area with no cooling \nchannel or gaps: {a_tf_turn_cable_space_no_void:.3e} m$^2$\n" + f"Cable area with no cooling\nchannel or gaps: {a_tf_turn_cable_space_no_void:.3e} m$^2$\n" f"Extra cable space area void fraction: {f_a_tf_turn_cable_space_extra_void}\n" f"True cable space area: {a_tf_turn_cable_space_effective:.3e} m$^2$" ) elif TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.INTEGER: textstr_turn_cable_space = ( f"$\\mathbf{{Cable \\ Space:}}$\n\n" - f"Cable space: \n$\\Delta r$: {cable_space_width_radial:.3e} m \n" - f"$\\Delta x$: {cable_space_width_toroidal:.3e} m \n" + f"Cable space:\n$\\Delta r$: {cable_space_width_radial:.3e} m\n" + f"$\\Delta x$: {cable_space_width_toroidal:.3e} m\n" f"Corner radius, $r$: {radius_tf_turn_cable_space_corners:.3e} m\n" f"Cable area with no cooling channel or gaps: {a_tf_turn_cable_space_no_void:.3e} m$^2$\n" f"Extra cable space area void fraction: {f_a_tf_turn_cable_space_extra_void}\n" @@ -7659,22 +7659,22 @@ def plot_tf_croco_turn(axis: plt.Axes, fig, mfile: MFile, scan: int): ) textstr_superconductor = ( - f"$\\mathbf{{Superconductor:}}$\n \n" + f"$\\mathbf{{Superconductor:}}$\n\n" f"Superconductor used: {SuperconductorModel(mfile.get('i_tf_sc_mat', scan=scan)).full_name}\n" - f"Critical field at zero \ntemperature and strain: {mfile.get('b_tf_superconductor_critical_zero_temp_strain', scan=scan):.4f} T\n" - f"Critical temperature at \nzero field and strain: {mfile.get('temp_tf_superconductor_critical_zero_field_strain', scan=scan):.4f} K\n" + f"Critical field at zero\ntemperature and strain: {mfile.get('b_tf_superconductor_critical_zero_temp_strain', scan=scan):.4f} T\n" + f"Critical temperature at\nzero field and strain: {mfile.get('temp_tf_superconductor_critical_zero_field_strain', scan=scan):.4f} K\n" f"Temperature at conductor: {mfile.get('tftmp', scan=scan):.4f} K\n" f"Field at conductor: {mfile.get('b_tf_inboard_peak_with_ripple', scan=scan):.4f} T\n" - f"Superconductor critical current density at \noperating conditions: {mfile.get('j_tf_superconductor_critical', scan=scan):.2e} A/m$^2$\n" + f"Superconductor critical current density at\noperating conditions: {mfile.get('j_tf_superconductor_critical', scan=scan):.2e} A/m$^2$\n" f"$I_{{\\text{{TF,turn critical}}}}$: {mfile.get('c_turn_cables_critical', scan=scan):,.2f} A\n" f"$I_{{\\text{{TF,turn}}}}$: {mfile.get('c_tf_turn', scan=scan):,.2f} A\n" f"Critcal current ratio: {mfile.get('f_c_tf_turn_operating_critical', scan=scan):,.4f}\n" - f"Superconductor temperature \nmargin: {mfile.get('temp_tf_superconductor_margin', scan=scan):,.4f} K\n" - f"\n$\\mathbf{{Quench:}}$\n \n" + f"Superconductor temperature\nmargin: {mfile.get('temp_tf_superconductor_margin', scan=scan):,.4f} K\n" + f"\n$\\mathbf{{Quench:}}$\n\n" f"Quench dump time: {mfile.get('t_tf_superconductor_quench', scan=scan):.4e} s\n" f"Quench detection time: {mfile.get('t_tf_quench_detection', scan=scan):.4e} s\n" - f"User input max temperature \nduring quench: {mfile.get('temp_tf_conductor_quench_max', scan=scan):.2f} K\n" - f"Required maxium WP current \ndensity for heat protection:\n{mfile.get('j_tf_wp_quench_heat_max', scan=scan):.2e} A/m$^2$\n" + f"User input max temperature\nduring quench: {mfile.get('temp_tf_conductor_quench_max', scan=scan):.2f} K\n" + f"Required maxium WP current\ndensity for heat protection:\n{mfile.get('j_tf_wp_quench_heat_max', scan=scan):.2e} A/m$^2$\n" ) axis.text( 0.75, @@ -7747,7 +7747,7 @@ def plot_cable_in_conduit_cable(axis: plt.Axes, fig, mfile: MFile, scan: int): ) textstr_cable = ( - f"$\\mathbf{{Cable:}}$\n \n" + f"$\\mathbf{{Cable:}}$\n\n" f"Cable diameter: {cable_diameter_mm:,.4f} mm\n" f"Copper area fraction: {mfile.get('f_a_tf_turn_cable_copper', scan=scan):.4f}\n" f"Number of strands per turn: {int(mfile.get('n_tf_turn_superconducting_cables', scan=scan)):,}\n" @@ -10111,7 +10111,7 @@ def plot_cs_coil_structure( ) textstr_cs = ( - f"$\\mathbf{{Coil \\ parameters:}}$\n \n" + f"$\\mathbf{{Coil \\ parameters:}}$\n\n" f"CS height vs TF internal height: {mfile.get('f_z_cs_tf_internal', scan=scan):.2f}\n" f"CS thickness: {mfile.get('dr_cs', scan=scan):.4f} m\n" f"CS radial middle: {mfile.get('r_cs_middle', scan=scan):.4f} m\n" @@ -11479,11 +11479,6 @@ def plot_fw_90_deg_pipe_bend(ax, m_file, scan: int): def plot_fusion_rate_profiles(axis: plt.Axes, fig, mfile: MFile, scan: int): """Plot the fusion rate density profiles on the given axis""" - fusden_plasma_dt_profile = [] - fusden_plasma_dd_triton_profile = [] - fusden_plasma_dd_helion_profile = [] - fusden_plasma_dhe3_profile = [] - n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) fusden_plasma_dt_profile = [ @@ -11678,7 +11673,7 @@ def plot_fusion_rate_profiles(axis: plt.Axes, fig, mfile: MFile, scan: int): textstr_dt = ( f"Total fusion power: {mfile.get('p_dt_total_mw', scan=scan):,.2f} MW\n" - f"Plasma fusion power: {mfile.get('p_plasma_dt_mw', scan=scan):,.2f} MW \n" + f"Plasma fusion power: {mfile.get('p_plasma_dt_mw', scan=scan):,.2f} MW\n" f"Volume-averaged fusion power density: plasma: {mfile.get('pden_plasma_dt_vol_avg_mw', scan=scan):,.3f} MW/m³\n" f"Beam fusion power: {mfile.get('p_beam_dt_mw', scan=scan):,.2f} MW\n" ) @@ -11705,7 +11700,7 @@ def plot_fusion_rate_profiles(axis: plt.Axes, fig, mfile: MFile, scan: int): textstr_dd = ( f"Total fusion power: {mfile.get('p_dd_total_mw', scan=scan):,.2f} MW\n" f"Volume-averaged total power density: {mfile.get('pden_dd_total_vol_avg_mw', scan=scan):,.3e} MW/m³\n" - f"Tritium branching ratio: {mfile.get('f_dd_branching_trit', scan=scan):.4f} \n" + f"Tritium branching ratio: {mfile.get('f_dd_branching_trit', scan=scan):.4f}\n" ) axis.text( @@ -11728,7 +11723,7 @@ def plot_fusion_rate_profiles(axis: plt.Axes, fig, mfile: MFile, scan: int): # ================================================= textstr_dhe3 = ( - f"Total fusion power: {mfile.get('p_dhe3_total_mw', scan=scan):,.2f} MW \n" + f"Total fusion power: {mfile.get('p_dhe3_total_mw', scan=scan):,.2f} MW\n\n" f"Volume-averaged total power density: {mfile.get('pden_dhe3_total_vol_avg_mw', scan=scan):,.3e} MW/m³\n\n" ) @@ -12616,13 +12611,13 @@ def plot_hts_tape_geometry( def plot_tf_corc_cable_summary_box(axis, fig, mfile: MFile, scan: int): """Plot TF CORC cable summary box""" textstr_cable = ( - f"$\\mathbf{{CroCo \\ Cable:}}$\n \n" + f"$\\mathbf{{CroCo \\ Cable:}}$\n\n" f"Cable diameter: {mfile.get('dia_tf_turn_croco_cable', scan=scan) * 1e3:,.4f} mm\n" f"Copper width: {mfile.get('dx_tf_croco_strand_copper', scan=scan) * 1e3:,.4f} mm\n" f"Diameter of solder tape region: {mfile.get('dia_tf_croco_strand_tape_region', scan=scan) * 1e3:,.4f} mm\n" f"Height of tape stack: {mfile.get('dx_tf_croco_strand_tape_stack', scan=scan) * 1e3:,.4f} mm\n" f"Width of HTS tape / tape stack: {mfile.get('dr_tf_hts_tape', scan=scan) * 1e3:,.4f} mm\n" - f"Number of HTS tape layers: {int(mfile.get('n_tf_croco_strand_hts_tapes', scan=scan))}\n \n" + f"Number of HTS tape layers: {int(mfile.get('n_tf_croco_strand_hts_tapes', scan=scan))}\n\n" f"Total copper area: {mfile.get('a_tf_croco_strand_copper_total', scan=scan) * 1e6:,.4f} mm²\n" f"Total hastelloy area: {mfile.get('a_tf_croco_strand_hastelloy', scan=scan) * 1e6:,.4f} mm²\n" f"Total solder area: {mfile.get('a_tf_croco_strand_solder', scan=scan) * 1e6:,.4f} mm²\n" @@ -14501,11 +14496,11 @@ def plot_blkt_structure( textstr_blkt_areas = ( f"$\\mathbf{{Blanket \\ Areas:}}$\n\n" - f"Inboard blanket, with holes and gaps: {m_file.get('a_blkt_inboard_surface', scan=scan):,.3f} $\\text{{m}}^2$ \n" - f"Outboard blanket, with holes and gaps: {m_file.get('a_blkt_outboard_surface', scan=scan):,.3f} $\\text{{m}}^2$ \n" - f"Total blanket, with holes and gaps: {m_file.get('a_blkt_total_surface', scan=scan):,.3f} $\\text{{m}}^2$ \n\n" - f"Inboard blanket, full coverage: {m_file.get('a_blkt_inboard_surface_full_coverage', scan=scan):,.3f} $\\text{{m}}^2$ \n" - f"Outboard blanket, full coverage: {m_file.get('a_blkt_outboard_surface_full_coverage', scan=scan):,.3f} $\\text{{m}}^2$ \n" + f"Inboard blanket, with holes and gaps: {m_file.get('a_blkt_inboard_surface', scan=scan):,.3f} $\\text{{m}}^2$\n" + f"Outboard blanket, with holes and gaps: {m_file.get('a_blkt_outboard_surface', scan=scan):,.3f} $\\text{{m}}^2$\n" + f"Total blanket, with holes and gaps: {m_file.get('a_blkt_total_surface', scan=scan):,.3f} $\\text{{m}}^2$\n\n" + f"Inboard blanket, full coverage: {m_file.get('a_blkt_inboard_surface_full_coverage', scan=scan):,.3f} $\\text{{m}}^2$\n" + f"Outboard blanket, full coverage: {m_file.get('a_blkt_outboard_surface_full_coverage', scan=scan):,.3f} $\\text{{m}}^2$\n" f"Total blanket, full coverage: {m_file.get('a_blkt_total_surface_full_coverage', scan=scan):,.3f} $\\text{{m}}^2$ " ) @@ -14519,11 +14514,11 @@ def plot_blkt_structure( textstr_blkt_volumes = ( f"$\\mathbf{{Blanket \\ Volumes:}}$\n\n" - f"Inboard blanket, with holes and gaps: {m_file.get('vol_blkt_inboard', scan=scan):,.3f} $\\text{{m}}^3$ \n" - f"Outboard blanket, with holes and gaps: {m_file.get('vol_blkt_outboard', scan=scan):,.3f} $\\text{{m}}^3$ \n" - f"Total blanket, with holes and gaps: {m_file.get('vol_blkt_total', scan=scan):,.3f} $\\text{{m}}^3$ \n\n" - f"Inboard blanket, full coverage: {m_file.get('vol_blkt_inboard_full_coverage', scan=scan):,.3f} $\\text{{m}}^3$ \n" - f"Outboard blanket, full coverage: {m_file.get('vol_blkt_outboard_full_coverage', scan=scan):,.3f} $\\text{{m}}^3$ \n" + f"Inboard blanket, with holes and gaps: {m_file.get('vol_blkt_inboard', scan=scan):,.3f} $\\text{{m}}^3$\n" + f"Outboard blanket, with holes and gaps: {m_file.get('vol_blkt_outboard', scan=scan):,.3f} $\\text{{m}}^3$\n" + f"Total blanket, with holes and gaps: {m_file.get('vol_blkt_total', scan=scan):,.3f} $\\text{{m}}^3$\n\n" + f"Inboard blanket, full coverage: {m_file.get('vol_blkt_inboard_full_coverage', scan=scan):,.3f} $\\text{{m}}^3$\n" + f"Outboard blanket, full coverage: {m_file.get('vol_blkt_outboard_full_coverage', scan=scan):,.3f} $\\text{{m}}^3$\n" f"Total blanket, full coverage: {m_file.get('vol_blkt_total_full_coverage', scan=scan):,.3f} $\\text{{m}}^3$ " ) @@ -15833,7 +15828,6 @@ def plot_pf_dimensions( axis.set_aspect("equal", adjustable="box") -<<<<<<< HEAD def plot_plasma_thermal_energy_profiles(axis, m_file: MFile, scan: int): """Function to plot plasma thermal energy profiles on the given axis. @@ -16042,8 +16036,6 @@ def plot_cumulative_plasma_thermal_energy_profiles(axis, m_file: MFile, scan: in ) -======= ->>>>>>> 36d00592d (repetition and unused vars) def main_plot( m_file: MFile, scan: int, From e50965eff9b936681a54796b5d9d639fcdeb3e85 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:33:44 +0100 Subject: [PATCH 11/18] refactor plot_blkt_pipe_bends --- process/core/io/plot/summary.py | 169 ++++++++++++++------------------ 1 file changed, 71 insertions(+), 98 deletions(-) diff --git a/process/core/io/plot/summary.py b/process/core/io/plot/summary.py index 4fb2685082..0bf24d2715 100644 --- a/process/core/io/plot/summary.py +++ b/process/core/io/plot/summary.py @@ -11338,6 +11338,68 @@ def plot_tf_stress(axis: plt.Axes, mfile: MFile): plt.tight_layout() +def draw_bend( + ax: Axes, + elbow_radius: float, + theta_span: float, + radius_pipe: float, + title: str = "Bend", + alpha: float = 0.8, +): + """ + Draws a circular pipe bend with centerline and inner/outer boundaries. + + Parameters + ---------- + ax: + Target axes for plotting. + elbow_radius: + Radius of the elbow in meters. + theta_span: + Array of angles [0, θ] where θ is pi/2 or pi. + radius_pipe: + Pipe radius in meters (fallback to 0.1m if not provided). + title: + Plot title string. + alpha: + fill opacity + """ + # Convert all inputs to mm + elbow_radius_mm = elbow_radius * 1000 + pipe_radius_mm = radius_pipe * 1000 + + theta = np.linspace(0, theta_span, 100) + x_center = elbow_radius_mm * np.cos(theta) + y_center = elbow_radius_mm * np.sin(theta) + + # Outer and inner walls (offset by ± pipe radius in mm) + x_outer = (elbow_radius_mm + pipe_radius_mm) * np.cos(theta) + y_outer = (elbow_radius_mm + pipe_radius_mm) * np.sin(theta) + x_inner = (elbow_radius_mm - pipe_radius_mm) * np.cos(theta) + y_inner = (elbow_radius_mm - pipe_radius_mm) * np.sin(theta) + + # Plot + ax.plot(x_center, y_center, color="black", linestyle="--", label="Centerline") + ax.plot(x_outer, y_outer, color="black") + ax.plot(x_inner, y_inner, color="black") + ax.fill( + np.concatenate([x_outer, x_inner[::-1]]), + np.concatenate([y_outer, y_inner[::-1]]), + color="lightgrey", + alpha=alpha, + ) + + ax.set_aspect("equal") + ax.set_xlabel("X [mm]") + ax.set_ylabel("Y [mm]") + ax.set_title(title) + ax.grid(True, linestyle="--", alpha=0.3) + + # Legend: Centerline + pipe radius info + legend_text = f"Centerline\nPipe radius: {pipe_radius_mm:.2f} mm\nElbow radius: {elbow_radius_mm:.2f} mm" + ax.legend([legend_text], loc="upper right") + + def plot_blkt_pipe_bends(fig, m_file, scan: int): """Plot the blanket pipe bends on the given axis, with axes in mm. @@ -11353,80 +11415,18 @@ def plot_blkt_pipe_bends(fig, m_file, scan: int): ax_90 = fig.add_subplot(341) ax_180 = fig.add_subplot(342) - # Get pipe radius from m_file, fallback to 0.1 m r = m_file.get("radius_blkt_channel", scan=scan) - elbow_radius_90 = m_file.get("radius_blkt_channel_90_bend", scan=scan) - - # --- 90 degree bend --- - theta_90 = np.linspace(0, np.pi / 2, 100) - # Convert coordinates from meters to millimeters - x_center_90 = elbow_radius_90 * np.cos(theta_90) * 1000 - y_center_90 = elbow_radius_90 * np.sin(theta_90) * 1000 - x_outer_90 = (elbow_radius_90 + r) * np.cos(theta_90) * 1000 - y_outer_90 = (elbow_radius_90 + r) * np.sin(theta_90) * 1000 - x_inner_90 = (elbow_radius_90 - r) * np.cos(theta_90) * 1000 - y_inner_90 = (elbow_radius_90 - r) * np.sin(theta_90) * 1000 - - ax_90.plot( - x_center_90, y_center_90, color="black", linestyle="--", label="Centerline" - ) - ax_90.plot(x_outer_90, y_outer_90, color="black") - ax_90.plot(x_inner_90, y_inner_90, color="black") - ax_90.fill( - np.concatenate([x_outer_90, x_inner_90[::-1]]), - np.concatenate([y_outer_90, y_inner_90[::-1]]), - color="lightgrey", - alpha=1.0, - ) - ax_90.set_aspect("equal") - ax_90.set_xlabel("X [mm]") - ax_90.set_ylabel("Y [mm]") - ax_90.set_title("Blanket Pipe 90° Bend") - ax_90.grid(True, linestyle="--", alpha=0.3) - # Add legend with radius values - ax_90.legend( - [ - f"Centerline\nPipe radius: {r * 1000:.2f} mm\nElbow radius: {elbow_radius_90 * 1000:.2f} mm" - ], - loc="upper right", - ) - - # --- 180 degree bend --- - - elbow_radius_180 = m_file.get("radius_blkt_channel_180_bend", scan=scan) - - theta_180 = np.linspace(0, np.pi, 100) - x_center_180 = elbow_radius_180 * np.cos(theta_180) * 1000 - y_center_180 = elbow_radius_180 * np.sin(theta_180) * 1000 - x_outer_180 = (elbow_radius_180 + r) * np.cos(theta_180) * 1000 - y_outer_180 = (elbow_radius_180 + r) * np.sin(theta_180) * 1000 - x_inner_180 = (elbow_radius_180 - r) * np.cos(theta_180) * 1000 - y_inner_180 = (elbow_radius_180 - r) * np.sin(theta_180) * 1000 + fallback_radius = 0.1 # meters - ax_180.plot( - x_center_180, y_center_180, color="black", linestyle="--", label="Centerline" + elbow_radius_90 = ( + m_file.get("radius_blkt_channel_90_bend", scan=scan) or fallback_radius ) - ax_180.plot(x_outer_180, y_outer_180, color="black") - ax_180.plot(x_inner_180, y_inner_180, color="black") - ax_180.fill( - np.concatenate([x_outer_180, x_inner_180[::-1]]), - np.concatenate([y_outer_180, y_inner_180[::-1]]), - color="lightgrey", - alpha=1.0, + elbow_radius_180 = ( + m_file.get("radius_blkt_channel_180_bend", scan=scan) or fallback_radius ) - ax_180.set_aspect("equal") - ax_180.set_xlabel("X [mm]") - ax_180.set_ylabel("Y [mm]") - ax_180.set_title("Blanket Pipe 180° Bend") - ax_180.grid(True, linestyle="--", alpha=0.3) - # Add legend with radius values - ax_180.legend( - [ - f"Centerline\nPipe radius: {r * 1000:.2f} mm\nElbow radius: {elbow_radius_180 * 1000:.2f} mm" - ], - loc="upper right", - ) + draw_bend(ax_90, elbow_radius_90, np.pi / 2, r, title="Blanket Pipe 90° Bend") + draw_bend(ax_180, elbow_radius_180, np.pi, r, title="Blanket Pipe 180° Bend") def plot_fw_90_deg_pipe_bend(ax, m_file, scan: int): @@ -11445,35 +11445,8 @@ def plot_fw_90_deg_pipe_bend(ax, m_file, scan: int): r = m_file.get("radius_fw_channel", scan=scan) elbow_radius = m_file.get("radius_fw_channel_90_bend", scan=scan) - # --- 90 degree bend --- - theta_90 = np.linspace(0, np.pi / 2, 100) - # Convert coordinates from meters to millimeters - x_center_90 = elbow_radius * np.cos(theta_90) * 1000 - y_center_90 = elbow_radius * np.sin(theta_90) * 1000 - x_outer_90 = (elbow_radius + r) * np.cos(theta_90) * 1000 - y_outer_90 = (elbow_radius + r) * np.sin(theta_90) * 1000 - x_inner_90 = (elbow_radius - r) * np.cos(theta_90) * 1000 - y_inner_90 = (elbow_radius - r) * np.sin(theta_90) * 1000 - - ax.plot(x_center_90, y_center_90, color="black", linestyle="--", label="Centerline") - ax.plot(x_outer_90, y_outer_90, color="black") - ax.plot(x_inner_90, y_inner_90, color="black") - ax.fill( - np.concatenate([x_outer_90, x_inner_90[::-1]]), - np.concatenate([y_outer_90, y_inner_90[::-1]]), - color="lightgrey", - alpha=1.0, - ) - ax.set_aspect("equal") - ax.set_xlabel("X [mm]") - ax.set_ylabel("Y [mm]") - ax.set_title("First Wall Pipe 90° Bend") - ax.grid(True, linestyle="--", alpha=0.3) - ax.legend( - [ - f"Centerline\nPipe radius: {r * 1000:.2f} mm\nElbow radius: {elbow_radius * 1000:.2f} mm" - ], - loc="upper right", + draw_bend( + ax, elbow_radius, np.pi / 2, r, title="First Wall Pipe 90° Bend", alpha=1.0 ) From dbba067beb2ae6061a6c581c961586dc3fa054f6 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:46:34 +0100 Subject: [PATCH 12/18] undo typing fix --- process/data_structure/physics_variables.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/process/data_structure/physics_variables.py b/process/data_structure/physics_variables.py index 752f18a10e..dad94478af 100644 --- a/process/data_structure/physics_variables.py +++ b/process/data_structure/physics_variables.py @@ -11,8 +11,6 @@ class PlasmaConfinementTransitionModel(IntEnum): """Enum for plasma L -> H and L -> I transition power threshold models.""" - full_name: str - ITER1996_NOMINAL = (1, "ITER-1996 Nominal") ITER1996_UPPER = (2, "ITER-1996 Upper") ITER1996_LOWER = (3, "ITER-1996 Lower") From 1d6a966527321c08649ff57240b8df47e1780f73 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:15:05 +0100 Subject: [PATCH 13/18] summary split up --- process/core/io/plot/summary.py | 16805 ---------------- process/core/io/plot/summary/__init__.py | 37 + process/core/io/plot/summary/api.py | 942 + process/core/io/plot/summary/common.py | 151 + process/core/io/plot/summary/constants.py | 87 + .../core/io/plot/summary/geometry/__init__.py | 59 + .../core/io/plot/summary/geometry/build.py | 356 + process/core/io/plot/summary/geometry/misc.py | 385 + .../core/io/plot/summary/geometry/poloidal.py | 1062 + .../core/io/plot/summary/geometry/toroidal.py | 330 + .../core/io/plot/summary/magnets/__init__.py | 61 + .../core/io/plot/summary/magnets/cables.py | 209 + process/core/io/plot/summary/magnets/cs.py | 640 + process/core/io/plot/summary/magnets/pf.py | 294 + process/core/io/plot/summary/magnets/tf.py | 3737 ++++ .../core/io/plot/summary/plasma/__init__.py | 51 + .../io/plot/summary/plasma/confinement.py | 541 + .../io/plot/summary/plasma/current_drive.py | 291 + .../core/io/plot/summary/plasma/overview.py | 1101 + .../core/io/plot/summary/plasma/physics.py | 873 + process/core/io/plot/summary/power_flow.py | 1980 ++ .../core/io/plot/summary/profiles/__init__.py | 104 + .../core/io/plot/summary/profiles/atomic.py | 589 + process/core/io/plot/summary/profiles/misc.py | 377 + .../core/io/plot/summary/profiles/plasma.py | 1552 ++ .../io/plot/summary/profiles/radiation.py | 462 + .../core/io/plot/summary/profiles/stress.py | 532 + process/core/io/plot/summary/rendering.py | 76 + .../io/plot/summary/reporting/__init__.py | 49 + .../io/plot/summary/reporting/constraints.py | 295 + .../core/io/plot/summary/reporting/layouts.py | 252 + .../core/io/plot/summary/reporting/misc.py | 678 + .../core/io/plot/summary/reporting/panels.py | 647 + process/core/io/plot/summary/time_profiles.py | 267 + 34 files changed, 19067 insertions(+), 16805 deletions(-) delete mode 100644 process/core/io/plot/summary.py create mode 100644 process/core/io/plot/summary/__init__.py create mode 100644 process/core/io/plot/summary/api.py create mode 100644 process/core/io/plot/summary/common.py create mode 100644 process/core/io/plot/summary/constants.py create mode 100644 process/core/io/plot/summary/geometry/__init__.py create mode 100644 process/core/io/plot/summary/geometry/build.py create mode 100644 process/core/io/plot/summary/geometry/misc.py create mode 100644 process/core/io/plot/summary/geometry/poloidal.py create mode 100644 process/core/io/plot/summary/geometry/toroidal.py create mode 100644 process/core/io/plot/summary/magnets/__init__.py create mode 100644 process/core/io/plot/summary/magnets/cables.py create mode 100644 process/core/io/plot/summary/magnets/cs.py create mode 100644 process/core/io/plot/summary/magnets/pf.py create mode 100644 process/core/io/plot/summary/magnets/tf.py create mode 100644 process/core/io/plot/summary/plasma/__init__.py create mode 100644 process/core/io/plot/summary/plasma/confinement.py create mode 100644 process/core/io/plot/summary/plasma/current_drive.py create mode 100644 process/core/io/plot/summary/plasma/overview.py create mode 100644 process/core/io/plot/summary/plasma/physics.py create mode 100644 process/core/io/plot/summary/power_flow.py create mode 100644 process/core/io/plot/summary/profiles/__init__.py create mode 100644 process/core/io/plot/summary/profiles/atomic.py create mode 100644 process/core/io/plot/summary/profiles/misc.py create mode 100644 process/core/io/plot/summary/profiles/plasma.py create mode 100644 process/core/io/plot/summary/profiles/radiation.py create mode 100644 process/core/io/plot/summary/profiles/stress.py create mode 100644 process/core/io/plot/summary/rendering.py create mode 100644 process/core/io/plot/summary/reporting/__init__.py create mode 100644 process/core/io/plot/summary/reporting/constraints.py create mode 100644 process/core/io/plot/summary/reporting/layouts.py create mode 100644 process/core/io/plot/summary/reporting/misc.py create mode 100644 process/core/io/plot/summary/reporting/panels.py create mode 100644 process/core/io/plot/summary/time_profiles.py diff --git a/process/core/io/plot/summary.py b/process/core/io/plot/summary.py deleted file mode 100644 index 0bf24d2715..0000000000 --- a/process/core/io/plot/summary.py +++ /dev/null @@ -1,16805 +0,0 @@ -"""PROCESS plot_summary""" - -import json -import textwrap -from dataclasses import dataclass -from importlib import resources -from pathlib import Path -from typing import Any, Literal, TypedDict - -import matplotlib as mpl -import matplotlib.backends.backend_pdf as bpdf -import matplotlib.image as mpimg -import matplotlib.pyplot as plt -import numpy as np -from matplotlib import patches -from matplotlib.axes import Axes -from matplotlib.patches import Circle, Rectangle -from matplotlib.path import Path as mplPath -from matplotlib.transforms import Transform -from scipy.interpolate import interp1d - -from process.core import constants -from process.core.io.mfile import MFile, MFileErrorClass -from process.data_structure.build_variables import TFCSRadialConfiguration -from process.data_structure.impurity_radiation_variables import ( - N_IMPURITIES, - ImpurityRadiationData, -) -from process.data_structure.numerics import FiguresOfMerit, PROCESSRunMode -from process.data_structure.pfcoil_variables import NFIXMX -from process.data_structure.physics_variables import ( - ConfinementTimeModel, - DivertorNumberModels, - OutbordSOLPowerDecayLengthModel, -) -from process.data_structure.superconducting_tf_coil_variables import TFWPIntegerTurnType -from process.models.build import Build -from process.models.engineering.materials import ( - calculate_tresca_stress, - calculate_von_mises_stress, - poisson_steel, -) -from process.models.geometry.blanket import ( - blanket_geometry_double_null, - blanket_geometry_single_null, -) -from process.models.geometry.cryostat import cryostat_geometry -from process.models.geometry.firstwall import ( - first_wall_geometry_double_null, - first_wall_geometry_single_null, -) -from process.models.geometry.pfcoil import pfcoil_geometry -from process.models.geometry.plasma import plasma_geometry -from process.models.geometry.shield import ( - shield_geometry_double_null, - shield_geometry_single_null, -) -from process.models.geometry.tfcoil import ( - tfcoil_geometry_d_shape, - tfcoil_geometry_rectangular_shape, -) -from process.models.geometry.vacuum_vessel import ( - vacuum_vessel_geometry_double_null, - vacuum_vessel_geometry_single_null, -) -from process.models.pfcoil import N_CS_STRESS_PROFILE_POINTS, CSCoil -from process.models.physics.bootstrap_current import BootstrapCurrentFractionModel -from process.models.physics.confinement_time import PlasmaConfinementTime -from process.models.physics.current_drive import ( - CurrentDriveMethodType, - CurrentDriveModel, - ElectronBernstein, - ElectronCyclotron, -) -from process.models.physics.density_limit import DensityLimitModel -from process.models.physics.exhaust import calculate_brunner_divertor_power_splits -from process.models.physics.impurity_radiation import read_impurity_file -from process.models.physics.l_h_transition import PlasmaConfinementTransitionModel -from process.models.physics.physics import ( - BetaComponentLimits, - BetaNormMaxModel, - IndInternalNormModel, -) -from process.models.physics.plasma_current import ( - PlasmaCurrentModel, - PlasmaDiamagneticCurrentModel, -) -from process.models.physics.plasma_geometry import ( - PlasmaGeometryModelType, - PlasmaShapeModelType, -) -from process.models.physics.profiles import ( - PlasmaProfileShapeType, - calculate_profile_shell_contributions, -) -from process.models.pulse import PulseTimings -from process.models.superconductors import SuperconductorModel -from process.models.tfcoil.base import ( - TFCoilShapeModel, - TFConductorModel, - TFPlasmaCaseType, -) -from process.models.tfcoil.quench import ( - _build_cumulative_quench_integral, - calculate_quench_protection_current_density, -) -from process.models.tfcoil.superconducting import SuperconductingTFTurnType - - -@dataclass -class RadialBuild: - """Dataclass containing radial build dictionaries""" - - upper: dict[str, float] - lower: dict[str, float] - radial: dict[str, float] - - cumulative_upper: dict[str, float] - cumulative_lower: dict[str, float] - cumulative_radial: dict[str, float] - - -# Colours are PROCESS default, BLUEMIRA -SOLENOID_COLOUR = ["pink", "#1764ab"] -CSCOMPRESSION_COLOUR = ["maroon", "#33CCCC"] -TFC_COLOUR = ["cyan", "#084a91"] -THERMAL_SHIELD_COLOUR = ["gray", "#e3eef9"] -VESSEL_COLOUR = ["green", "#b7d4ea"] -SHIELD_COLOUR = ["green", "#94c4df"] -BLANKET_COLOUR = ["magenta", "#4a98c9"] -PLASMA_COLOUR = ["khaki", "#cc8acc"] -CRYOSTAT_COLOUR = ["red", "#2e7ebc"] -FIRSTWALL_COLOUR = ["darkblue", "darkblue"] -NBSHIELD_COLOUR = ["black", "black"] - -thin = 0.0 - -RADIAL_BUILD = [ - "dr_bore", - "dr_cs", - "dr_cs_precomp", - "dr_cs_tf_gap", - "dr_tf_inboard", - "dr_tf_shld_gap", - "dr_shld_thermal_inboard", - "dr_shld_vv_gap_inboard", - "dr_vv_inboard", - "dr_shld_inboard", - "vvblgapi", - "dr_blkt_inboard", - "dr_fw_inboard", - "dr_fw_plasma_gap_inboard", - "rminori", - "rminoro", - "dr_fw_plasma_gap_outboard", - "dr_fw_outboard", - "dr_blkt_outboard", - "vvblgapo", - "dr_shld_outboard", - "dr_vv_outboard", - "dr_shld_vv_gap_outboard", - "dr_shld_thermal_outboard", - "dr_tf_shld_gap", - "dr_tf_outboard", -] - -vertical_lower = [ - "z_plasma_xpoint_lower", - "dz_xpoint_divertor", - "dz_divertor", - "dz_shld_lower", - "dz_vv_lower", - "dz_shld_vv_gap", - "dz_shld_thermal", - "dr_tf_shld_gap", - "dr_tf_inboard", -] - -ANIMATION_INFO = [ - ("rmajor", "Major radius", "m"), - ("rminor", "Minor radius", "m"), - ("aspect", "Aspect ratio", ""), -] - -rtangle = np.pi / 2 -rtangle2 = 2 * rtangle - - -def _box_style(colour: str): - return {"boxstyle": "round", "facecolor": colour, "alpha": 1.0, "linewidth": 2} - - -white_box = {"boxstyle": "round", "facecolor": "white", "alpha": 1.0} - - -def _text_layout(fig): - return { - "fontsize": 9, - "verticalalignment": "bottom", - "horizontalalignment": "left", - "transform": fig.transFigure, - } - - -def _setup_axis(axis, xmin, xmax, ymin, ymax): - axis.set_ylim(ymin, ymax) - axis.set_xlim(xmin, xmax) - axis.set_axis_off() - axis.set_autoscaley_on(False) - axis.set_autoscalex_on(False) - - -def _colourbar(contour_fill, axis, colourbar_axis): - # Use a dedicated colorbar axes when provided so the main axes width is unchanged. - if colourbar_axis is None: - return axis.figure.colorbar(contour_fill, ax=axis, pad=0.02) - return axis.figure.colorbar(contour_fill, cax=colourbar_axis) - - -def plot_plasma( - axis: plt.Axes, - mfile: MFile, - scan: int, - colour_scheme: Literal[1, 2], - mirror_negative_x: bool = False, -): - """Plots the plasma boundary arcs. - - Parameters - ---------- - axis : - axis object to plot to - mfile : - MFILE data object - scan : - scan number to use - colour_scheme : - colour scheme to use for plots - mirror_negative_x : - if True, mirror the plot to the negative x-axis (Default value = False) - - - Raises - ------ - ValueError - If an unsupported plasma shape model type is encountered. - """ - r_0, a, triang, kappa, i_single_null, i_plasma_shape, plasma_square = ( - mfile.get_variables( - "rmajor", - "rminor", - "triang", - "kappa", - "i_single_null", - "i_plasma_shape", - "plasma_square", - scan=scan, - ) - ) - - pg = plasma_geometry( - rmajor=r_0, - rminor=a, - triang=triang, - kappa=kappa, - i_single_null=i_single_null, - i_plasma_shape=i_plasma_shape, - square=plasma_square, - ) - - # Apply mirror transformation if requested - x_scale = -1 if mirror_negative_x else 1 - - match PlasmaShapeModelType(i_plasma_shape): - case PlasmaShapeModelType.PROCESS_ORIGINAL: - # Plot the 2 plasma outline arcs. - axis.plot(x_scale * np.array(pg.rs[0]), pg.zs[0], color="black") - axis.plot(x_scale * np.array(pg.rs[1]), pg.zs[1], color="black") - - # Set triang_95 to stop plotting plasma past boundary - # Assume IPDG scaling - triang_95 = triang / 1.5 - - # Colour in right side of plasma - axis.fill_between( - x=x_scale * np.array(pg.rs[0]), - y1=pg.zs[0], - where=(pg.rs[0] > r_0 - (triang_95 * a * 1.5)), - color=PLASMA_COLOUR[colour_scheme - 1], - ) - # Colour in left side of plasma - axis.fill_between( - x=x_scale * np.array(pg.rs[1]), - y1=pg.zs[1], - where=(pg.rs[1] < r_0 - (triang_95 * a * 1.5)), - color=PLASMA_COLOUR[colour_scheme - 1], - ) - - case PlasmaShapeModelType.SAUTER: - axis.plot(x_scale * np.array(pg.rs), pg.zs, color="black") - axis.fill( - x_scale * np.array(pg.rs), pg.zs, color=PLASMA_COLOUR[colour_scheme - 1] - ) - case _: - raise ValueError(f"Unsupported plasma shape model type: {i_plasma_shape}") - - -def plot_centre_cross( - axis: plt.Axes, mfile: MFile, scan: int, mirror_negative_x: bool = False -): - """Function to plot centre cross on plot - - Parameters - ---------- - axis : - axis object to plot to - mfile : - MFILE data object - scan : - scan number to use - mirror_negative_x : - if True, mirror the plot to the negative x-axis (Default value = False) - """ - rmajor = mfile.get("rmajor", scan=scan) - x_scale = -1 if mirror_negative_x else 1 - axis.plot( - x_scale * np.array([rmajor - 0.25, rmajor + 0.25, rmajor, rmajor, rmajor]), - [0, 0, 0, 0.25, -0.25], - color="black", - ) - - -def cumulative_radial_build(section, mfile: MFile, scan: int): - """Function for calculating the cumulative radial build up to and - including the given section. - - Parameters - ---------- - section : - section of the radial build to go up to - mfile : - MFILE data object - scan : - scan number to use - - Returns - ------- - : - cumulative_build:cumulative radial build up to section given - """ - complete = False - cumulative_build = 0 - for item in RADIAL_BUILD: - if item in {"rminori", "rminoro"}: - cumulative_build += mfile.get("rminor", scan=scan) - elif item in {"vvblgapi", "vvblgapo"}: - cumulative_build += mfile.get("dr_shld_blkt_gap", scan=scan) - elif "dr_vv_inboard" in item: - cumulative_build += mfile.get("dr_vv_inboard", scan=scan) - elif "dr_vv_outboard" in item: - cumulative_build += mfile.get("dr_vv_outboard", scan=scan) - else: - cumulative_build += mfile.get(item, scan=scan) - if item == section: - complete = True - break - - if complete is False: - print("radial build parameter ", section, " not found") - return cumulative_build - - -def cumulative_radial_build2(section, mfile: MFile, scan: int): - """Function for calculating the cumulative radial build up to and - including the given section. - - Parameters - ---------- - section : - section of the radial build to go up to - mfile : - MFILE data object - scan : - scan number to use - - Returns - ------- - : - cumulative_build --> cumulative radial build up to and including - section given - previous --> cumulative radial build up to section given - """ - cumulative_build = 0 - build = 0 - for item in RADIAL_BUILD: - if item in {"rminori", "rminoro"}: - build = mfile.get("rminor", scan=scan) - elif item in {"vvblgapi", "vvblgapo"}: - build = mfile.get("dr_shld_blkt_gap", scan=scan) - elif "dr_vv_inboard" in item: - build = mfile.get("dr_vv_inboard", scan=scan) - elif "dr_vv_outboard" in item: - build = mfile.get("dr_vv_outboard", scan=scan) - else: - build = mfile.get(item, scan=scan) - cumulative_build += build - if item == section: - break - previous = cumulative_build - build - return (cumulative_build, previous) - - -def poloidal_cross_section( - axis: plt.Axes, - mfile: MFile, - scan: int, - demo_ranges: bool, - radial_build: RadialBuild, - colour_scheme: Literal[1, 2], -): - """Function to plot poloidal cross-section - - Parameters - ---------- - axis : - axis object to add plot to - mfile : - MFILE data object - scan : - scan number to use - demo_ranges: - - colour_scheme : - colour scheme to use for plots - """ - axis.set_xlabel("R [m]") - axis.set_ylabel("Z [m]") - axis.set_title("Poloidal Cross-Section") - axis.minorticks_on() - axis.grid(which="both", linestyle="--", linewidth=0.5, alpha=0.2) - - plot_vacuum_vessel_and_divertor(axis, mfile, scan, radial_build, colour_scheme) - plot_shield(axis, mfile, scan, radial_build, colour_scheme) - plot_blanket(axis, mfile, scan, radial_build, colour_scheme) - plot_firstwall(axis, mfile, scan, radial_build, colour_scheme) - - plot_plasma(axis, mfile, scan, colour_scheme) - plot_centre_cross(axis, mfile, scan) - plot_cryostat(axis, mfile, scan, colour_scheme) - - plot_tf_coils(axis, mfile, scan, colour_scheme) - plot_pf_coils(axis, mfile, scan, colour_scheme) - - if demo_ranges: - axis.set_ylim(-15, 15) - axis.set_xlim(0, 20) - - else: - axis.set_xlim(0, axis.get_xlim()[1]) - - -def plot_full_machine_poloidal_cross_section( - axis: plt.Axes, - mfile: MFile, - scan: int, - radial_build: RadialBuild, - colour_scheme: Literal[1, 2], -): - """Function to plot full machine poloidal cross-section, including mirrored negative x-axis - - Parameters - ---------- - axis : - axis object to add plot to - mfile : - MFILE data object - scan : - scan number to use - radial_build : - radial build data - colour_scheme : - colour scheme to use for plots - """ - plot_vacuum_vessel_and_divertor(axis, mfile, scan, radial_build, colour_scheme) - plot_vacuum_vessel_and_divertor( - axis, mfile, scan, radial_build, colour_scheme, mirror_negative_x=True - ) - plot_shield(axis, mfile, scan, radial_build, colour_scheme) - plot_shield(axis, mfile, scan, radial_build, colour_scheme, mirror_negative_x=True) - - plot_blanket(axis, mfile, scan, radial_build, colour_scheme) - plot_blanket(axis, mfile, scan, radial_build, colour_scheme, mirror_negative_x=True) - plot_firstwall(axis, mfile, scan, radial_build, colour_scheme) - plot_firstwall( - axis, mfile, scan, radial_build, colour_scheme, mirror_negative_x=True - ) - plot_plasma(axis, mfile, scan, colour_scheme) - plot_plasma(axis, mfile, scan, colour_scheme, mirror_negative_x=True) - plot_centre_cross(axis, mfile, scan) - plot_centre_cross(axis, mfile, scan, mirror_negative_x=True) - plot_cryostat(axis, mfile, scan, colour_scheme) - plot_cryostat(axis, mfile, scan, colour_scheme, mirror_negative_x=True) - plot_tf_coils(axis, mfile, scan, colour_scheme) - plot_tf_coils(axis, mfile, scan, colour_scheme, mirror_negative_x=True) - plot_pf_coils(axis, mfile, scan, colour_scheme) - plot_pf_coils(axis, mfile, scan, colour_scheme, mirror_negative_x=True) - - axis.set_xlabel("R [m]") - axis.set_ylabel("Z [m]") - axis.set_aspect("equal") - axis.minorticks_on() - axis.grid(which="minor", linestyle=":", linewidth=0.5, alpha=0.5) - - -def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figure): - """Plots the main power flow diagram for the fusion reactor, including plasma, heating and current drive, - first wall, blanket, vacuum vessel, divertor, coolant pumps, turbine, generator, and auxiliary systems. - Annotates the diagram with power values and draws arrows to indicate power flows. - - Parameters - ---------- - axis: - The matplotlib axis object to plot on. - mfile: - The MFILE data object containing power flow parameters. - scan: - The scan number to use for extracting data. - fig: - The matplotlib figure object for additional annotations. - """ - axis.text( - 0.05, - 0.95, - "* Components do not represent the design", - transform=fig.transFigure, - horizontalalignment="left", - verticalalignment="bottom", - zorder=2, - fontsize=11, - ) - - # ========================================== - # Plasma - # =========================================== - - # Load the plasma image - with resources.path("process.core.io.plot.images", "plasma.png") as img_path: - plasma = mpimg.imread(img_path.open("rb")) - - # Display the plasma image over the figure, not the axes - new_ax = axis.inset_axes( - (-0.15, 0.6, 0.45, 0.45), transform=axis.transAxes, zorder=1 - ) - new_ax.imshow(plasma) - new_ax.axis("off") - - # Add fusion power to plasma - axis.text( - 0.22, - 0.75, - f"$P_{{{{fus}}}}$\n{mfile.get('p_fusion_total_mw', scan=scan):.2f} MW", - transform=fig.transFigure, - horizontalalignment="left", - verticalalignment="bottom", - zorder=2, - fontsize=11, - ) - # Load the neutron image - with resources.path("process.core.io.plot.images", "neutron.png") as img_path: - neutron = mpimg.imread(img_path.open("rb")) - - new_ax = axis.inset_axes( - (0.2, 0.85, 0.03, 0.03), transform=axis.transAxes, zorder=10 - ) - new_ax.imshow(neutron) - new_ax.axis("off") - - # Add lost alpha power - axis.text( - 0.22, - 0.81, - f"$P_{{\\alpha,{{loss}}}}$\n{mfile.get('p_fw_alpha_mw', scan=scan):,.2f} MW", - transform=fig.transFigure, - horizontalalignment="left", - verticalalignment="bottom", - zorder=2, - fontsize=11, - ) - - # Add radiation power to plasma - axis.text( - 0.22, - 0.69, - f"$P_{{{{rad}}}}$\n{mfile.get('p_plasma_rad_mw', scan=scan):,.2f} MW", - transform=fig.transFigure, - horizontalalignment="left", - verticalalignment="bottom", - zorder=2, - fontsize=11, - ) - - # Add photon image to plasma - axis.text( - 0.34, - 0.71, - "$\\gamma$", - transform=fig.transFigure, - horizontalalignment="left", - verticalalignment="bottom", - zorder=2, - fontsize=12, - ) - - # Draw from gamma arrow bend towards divertor - axis.annotate( - "", - xy=(0.35, 0.55), - xytext=(0.35, 0.695), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "blue", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Add separatrix power to plasma - axis.text( - 0.22, - 0.63, - f"$P_{{{{sep}}}}$\n{mfile.get('p_plasma_separatrix_mw', scan=scan):,.2f} MW", - transform=fig.transFigure, - horizontalalignment="left", - verticalalignment="bottom", - zorder=2, - fontsize=11, - ) - - # Draw from separatrix power to arrow bend - axis.annotate( - "", - xy=(0.3725, 0.65), - xytext=(0.3, 0.65), - xycoords=fig.transFigure, - arrowprops={ - "color": "pink", - "arrowstyle": "-", # No arrow head - "linewidth": 2.0, - }, - ) - - # Draw from separatrix arrow bend to the divertor - axis.annotate( - "", - xy=(0.37, 0.55), - xytext=(0.37, 0.65), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", # solid filled head - "color": "pink", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Draw neutron arrow from plasma - axis.annotate( - "", - xy=(0.95, 0.76), - xytext=(0.31, 0.76), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "grey", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Draw arrow from main neutron arrow down to divertor - axis.annotate( - "", - xy=(0.39, 0.55), - xytext=(0.39, 0.76), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "grey", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Draw radiation arrow from plasma - axis.annotate( - "", - xy=(0.56, 0.695), - xytext=(0.3, 0.695), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "blue", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Load the alpha particle image - with resources.path("process.core.io.plot.images", "alpha_particle.png") as img_path: - alpha = mpimg.imread(img_path.open("rb")) - - # Display the alpha particle image over the figure, not the axes - new_ax = axis.inset_axes( - (0.16, 0.95, 0.025, 0.025), transform=axis.transAxes, zorder=10 - ) - new_ax.imshow(alpha) - new_ax.axis("off") - - # Hide the axes for a cleaner look - axis.axis("off") - - # Draw alpha particle arrow from plasma - axis.annotate( - "", - xy=(0.56, 0.83), - xytext=(0.3, 0.83), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "red", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Plot neutron power from plasma to box - axis.text( - 0.37, - 0.775, - f"$P_{{\\text{{neutron}}}}$:\n{mfile.get('p_neutron_total_mw', scan=scan):,.2f} MW", - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "grey", - "alpha": 0.8, - "linewidth": 2, - }, - ) - - # =========================================== - - # ========================================= - # Heating and current drive systems - # ========================================= - - # Add HCD primary injected power - axis.text( - 0.0725, - 0.83, - f"$P_{{\\text{{HCD,primary}}}}$: {mfile.get('p_hcd_primary_injected_mw', scan=scan) + mfile.get('p_hcd_primary_extra_heat_mw', scan=scan):.2f} MW", - **_text_layout(fig), - bbox=_box_style("lightyellow"), - ) - - # Add HCD secondary injected power - axis.text( - 0.0725, - 0.725, - f"$P_{{\\text{{HCD,secondary}}}}$: {mfile.get('p_hcd_secondary_injected_mw', scan=scan) + mfile.get('p_hcd_secondary_extra_heat_mw', scan=scan):.2f} MW", - **_text_layout(fig), - bbox=_box_style("lightyellow"), - ) - - # Load the HCD injector image - with resources.path("process.core.io.plot.images", "hcd_injector.png") as img_path: - hcd_injector_1 = hcd_injector_2 = mpimg.imread(img_path.open("rb")) - - # Display the injector image over the figure, not the axes - new_ax = axis.inset_axes( - (-0.2, 0.8, 0.15, 0.15), transform=axis.transAxes, zorder=10 - ) - new_ax.imshow(hcd_injector_1) - new_ax.axis("off") - new_ax = axis.inset_axes((-0.2, 0.5, 0.15, 0.5), transform=axis.transAxes, zorder=10) - new_ax.imshow(hcd_injector_2) - new_ax.axis("off") - - # Draw a dashed line with an arrow tip coming from the left of each injector - for y in [0.875, 0.75]: - axis.annotate( - "", - xy=(-0.2, y), - xytext=(-0.28, y), - xycoords=axis.transAxes, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 1.5, - "zorder": 11, - }, - annotation_clip=False, - ) - - # Plot line from HCD power supply to bend for injected - axis.plot( - [-0.28, -0.28], - [0.875, 0.5], - transform=axis.transAxes, - color="black", - linewidth=1.5, - zorder=3, - clip_on=False, - ) - - # Plot the HCD power supply box - axis.text( - 0.04, - 0.45, - "\n\nH&CD Power Supply\n\n", - **_text_layout(fig), - bbox=_box_style("lightyellow"), - zorder=4, - ) - - # Draw arrow from HCD box going to primary HCD losses - axis.annotate( - "", - xy=(0.2, 0.5), - xytext=(0.1, 0.5), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.2", - "color": "black", - "linestyle": "--", - "linewidth": 1.5, - "zorder": 5, - "fill": True, - }, - ) - - # Plot electric power losses for secondary HCD - axis.text( - 0.2, - 0.435, - f"$P_{{\\text{{secondary,loss}}}}$:\n{mfile.get('p_hcd_secondary_electric_mw', scan=scan) * (1.0 - mfile.get('eta_hcd_secondary_injector_wall_plug', scan=scan)):.2f} MWe", - **_text_layout(fig), - bbox=_box_style("lightblue") | {"linestyle": "dashed"}, - ) - - # Draw an arrow from HCD secondary losses to the total secondary heat power - axis.annotate( - "", - xy=(0.25, 0.3), - xytext=(0.25, 0.43), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 1.5, - "zorder": 5, - "fill": True, - "linestyle": "--", - }, - ) - - # Draw an arrow from HCD primary losses bend to the total secondary heat power - axis.annotate( - "", - xy=(0.28, 0.3), - xytext=(0.28, 0.5), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 1.5, - "zorder": 5, - "fill": True, - "linestyle": "--", - }, - ) - - # Draw line from HCD primary losses to the arrow bend - axis.annotate( - "", - xy=(0.26, 0.5), - xytext=(0.28, 0.5), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-", - "color": "black", - "linestyle": "--", - "linewidth": 1.5, - "zorder": 5, - "fill": True, - }, - ) - - # Draw arrow frim HCD power supply to secondary HCD losses - axis.annotate( - "", - xy=(0.2, 0.46), - xytext=(0.1, 0.46), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.2", - "color": "black", - "linestyle": "--", - "linewidth": 1.5, - "zorder": 5, - "fill": True, - }, - ) - - # Plot electric power losses for primary HCD - axis.text( - 0.2, - 0.485, - f"$P_{{\\text{{primary,loss}}}}$:\n{mfile.get('p_hcd_primary_electric_mw', scan=scan) * (1.0 - mfile.get('eta_hcd_primary_injector_wall_plug', scan=scan)):.2f} MWe", - **_text_layout(fig), - bbox=_box_style("lightblue") | {"linestyle": "dashed"}, - ) - - # Draw arrow from HCD primary electric box to HCD power supply box - axis.annotate( - "", - xy=(0.06, 0.45), - xytext=(0.06, 0.38), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "->", - "color": "black", - "linewidth": 1.5, - "zorder": 5, - }, - ) - - # Draw arrow from HCD secondary electric box to HCD power supply box - axis.annotate( - "", - xy=(0.12, 0.45), - xytext=(0.12, 0.38), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "->", - "color": "black", - "linewidth": 1.5, - "zorder": 5, - }, - ) - - # Plot HCD secondary losses box - axis.text( - 0.12, - 0.35, - f"$P_{{\\text{{secondary}}}}$:\n{mfile.get('p_hcd_secondary_electric_mw', scan=scan):.2f} MWe\n$\\eta$: {mfile.get('eta_hcd_secondary_injector_wall_plug', scan=scan):.2f}", - **_text_layout(fig), - bbox=_box_style("lightyellow"), - ) - - # Plot HCD primary electric box - axis.text( - 0.025, - 0.35, - f"$P_{{\\text{{primary}}}}$:\n{mfile.get('p_hcd_primary_electric_mw', scan=scan):.2f} MWe\n$\\eta$: {mfile.get('eta_hcd_primary_injector_wall_plug', scan=scan):.2f}", - **_text_layout(fig), - bbox=_box_style("lightyellow"), - ) - - # ============================================= - - # ============================================= - # Low grade heat total - # ============================================= - - # Plot box of total low grade secondary heat - axis.text( - 0.325, - 0.225, - f"\n\nTotal Low Grade Secondary Heat\n\n {mfile.get('p_plant_secondary_heat_mw', scan=scan):,.2f} MWth", - fontsize=9, - verticalalignment="bottom", - horizontalalignment="center", - transform=fig.transFigure, - bbox=_box_style("lightblue") | {"linestyle": "dashed"}, - zorder=4, - ) - - # ============================================= - - # ========================================== - # Power conversion systems - # =========================================== - - # Load the turbine image - with resources.path("process.core.io.plot.images", "turbine.png") as img_path: - turbine = mpimg.imread(img_path.open("rb")) - - # Display the turbine image over the figure, not the axes - new_ax = axis.inset_axes((1.1, 0.0, 0.15, 0.15), transform=axis.transAxes, zorder=10) - new_ax.imshow(turbine) - new_ax.axis("off") - - # Plot the total primary thermal power box - axis.text( - 0.9, - 0.25, - f"$P_{{\\text{{primary,thermal}}}}$:\n{mfile.get('p_plant_primary_heat_mw', scan=scan):,.2f} MW\n$\\eta_{{\\text{{turbine}}}}$: {mfile.get('eta_turbine', scan=scan):.3f}", - **_text_layout(fig), - bbox=_box_style("orange"), - ) - - # Draw arrow from bend to turbine inlet - axis.annotate( - "", - xy=(0.925, 0.165), - xytext=(0.96, 0.165), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "orange", - "linewidth": 3.0, - "zorder": 5, - "fill": True, - }, - ) - - # Total primary thermal to turbine inlet line bend - axis.annotate( - "", - xy=(0.96, 0.245), - xytext=(0.96, 0.1625), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-", - "color": "orange", - "linewidth": 3.0, - "zorder": 5, - "fill": True, - }, - ) - - # Load the generator image - with resources.path("process.core.io.plot.images", "generator.png") as img_path: - generator = mpimg.imread(img_path.open("rb")) - - # Display the generator image over the figure, not the axes - new_ax = axis.inset_axes( - (0.96, 0.0, 0.15, 0.15), transform=axis.transAxes, zorder=10 - ) - new_ax.imshow(generator) - new_ax.axis("off") - - # Generator to gross electric power - axis.annotate( - "", - xy=(0.745, 0.17), - xytext=(0.79, 0.17), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Generator labels - axis.text( - 0.79, - 0.16, - "Generator", - **_text_layout(fig), - zorder=20, - ) - - # Connector from turbine to generator - axis.annotate( - "", - xy=(0.85, 0.17), - xytext=(0.925, 0.17), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-", - "color": "black", - "linewidth": 7.0, - "zorder": 5, - "fill": True, - }, - ) - - # Turbine to loss power - axis.annotate( - "", - xy=(0.91, 0.08), - xytext=(0.91, 0.13), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - "linestyle": "dashed", - }, - ) - - # Load the pylon image - with resources.path("process.core.io.plot.images", "pylon.png") as img_path: - pylon = mpimg.imread(img_path.open("rb")) - - # Display the pylon image over the figure, not the axes - new_ax = axis.inset_axes( - (0.925, -0.1, 0.1, 0.1), transform=axis.transAxes, zorder=10 - ) - new_ax.imshow(pylon) - new_ax.axis("off") - - # Plot the gross electric power box - axis.text( - 0.68, - 0.15, - f"$P_{{\\text{{gross}}}}$:\n{mfile.get('p_plant_electric_gross_mw', scan=scan):,.2f} MWe", - **_text_layout(fig), - bbox=_box_style("lime"), - ) - - # Gross to net electric power - axis.annotate( - "", - xy=(0.72, 0.08), - xytext=(0.72, 0.15), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Plot the turbine loss box - axis.text( - 0.875, - 0.05, - f"$P_{{\\text{{loss}}}}$:\n{mfile.get('p_turbine_loss_mw', scan=scan):,.2f} MWth", - **_text_layout(fig), - bbox=_box_style("orange") | {"linestyle": "dashed"}, - ) - - # Shield primary thermal to plant total primary thermal arrow - axis.annotate( - "", - xy=(0.95, 0.3), - xytext=(0.95, 0.55), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "orange", - "linewidth": 3.0, - "zorder": 5, - "fill": True, - }, - ) - - # Plot the net electric power box - axis.text( - 0.68, - 0.05, - f"$P_{{\\text{{net,electric}}}}$:\n{mfile.get('p_plant_electric_net_mw', scan=scan):,.2f} MWe", - **_text_layout(fig), - bbox=_box_style("lime"), - ) - - # Plot the recirculated electric power box - axis.text( - 0.575, - 0.14, - ( - f"$P_{{\\text{{recirc,electric}}}}$:\n{mfile.get('p_plant_electric_recirc_mw', scan=scan):,.2f} MWe\n" - f"$f_{{\\text{{recirc}}}}$:\n{mfile.get('f_p_plant_electric_recirc', scan=scan):,.2f}" - ), - **_text_layout(fig), - bbox=_box_style("lime"), - ) - - # Gross to recirculated power arrow - axis.annotate( - "", - xy=(0.64, 0.17), - xytext=(0.675, 0.17), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Recirculated to pumps electric - axis.annotate( - "", - xy=(0.7, 0.225), - xytext=(0.645, 0.185), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Recirculated power to HCD secondary electric arrow bend - axis.annotate( - "", - xy=(0.14, 0.2), - xytext=(0.57, 0.2), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-", - "color": "black", - "linewidth": 1.5, - "zorder": 5, - "fill": True, - }, - ) - - # Recirculated power to HCD primary electric arrow bend - axis.annotate( - "", - xy=(0.08, 0.18), - xytext=(0.57, 0.18), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-", - "color": "black", - "linewidth": 1.5, - "zorder": 5, - "fill": True, - }, - ) - - # Arrow to primary HCD electric from bend - axis.annotate( - "", - xy=(0.08, 0.35), - xytext=(0.08, 0.1775), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 1.5, - "zorder": 5, - "fill": True, - }, - ) - - # Arrow to secondary HCD electric from bend - axis.annotate( - "", - xy=(0.14, 0.35), - xytext=(0.14, 0.2), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 1.5, - "zorder": 5, - "fill": True, - }, - ) - - # ========================================== - - # ================================ - # First wall, blanket and shield - # ================================ - - # Load the first wall image - with resources.path("process.core.io.plot.images", "fw.png") as img_path: - fw = mpimg.imread(img_path.open("rb")) - - # Display the first wall image over the figure, not the axes - new_ax = axis.inset_axes((0.4, 0.625, 0.4, 0.4), transform=axis.transAxes, zorder=10) - new_ax.imshow(fw) - new_ax.axis("off") - - # Add first wall label above image - axis.text( - 0.5, - 0.9, - "First Wall", - fontsize=11, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - ) - - # Alpha power incident on first wall box - axis.text( - 0.46, - 0.85, - f"$P_{{\\text{{FW, }}\\alpha}}$:\n{mfile.get('p_fw_alpha_mw', scan=scan):.2f} MW", - **_text_layout(fig), - bbox=_box_style("red"), - ) - - # Neutron power incident on first wall box - axis.text( - 0.46, - 0.775, - f"$P_{{\\text{{FW,nuclear}}}}$:\n{mfile.get('p_fw_nuclear_heat_total_mw', scan=scan):,.2f} MW", - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "grey", - "alpha": 0.8, - "linewidth": 2, - }, - ) - - # Plot radiation power incident on first wall box - axis.text( - 0.46, - 0.71, - f"$P_{{\\text{{FW,rad}}}}$:\n{mfile.get('p_fw_rad_total_mw', scan=scan):,.2f} MW", - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "dodgerblue", - "alpha": 0.8, - "linewidth": 2, - }, - ) - - # Draw arrow from FW to heat depsoited box - axis.annotate( - "", - xy=(0.61, 0.585), - xytext=(0.61, 0.65), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "orange", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Draw arrow from Blanket to heat deposited box - axis.annotate( - "", - xy=(0.81, 0.585), - xytext=(0.81, 0.63), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "orange", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Draw arrow from shield to heat deposited box - axis.annotate( - "", - xy=(0.92, 0.59), - xytext=(0.92, 0.62), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "orange", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # First wall heat deposited box - axis.text( - 0.5, - 0.555, - f"Primary thermal\n(inc pump): {mfile.get('p_fw_heat_deposited_mw', scan=scan):,.2f} MWth", - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "orange", - "linewidth": 2, - }, - ) - - # Blanket heat deposited box - axis.text( - 0.7, - 0.555, - f"Primary thermal\n(inc pump): {mfile.get('p_blkt_heat_deposited_mw', scan=scan):,.2f} MWth", - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "orange", - "linewidth": 2, - }, - ) - - # Shield heat deposited box - axis.text( - 0.875, - 0.555, - f"Primary thermal:\n{mfile.get('p_shld_heat_deposited_mw', scan=scan):.2f} MWth", - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "orange", - "linewidth": 2, - }, - ) - - # Draw arrow from FW primary heat box to blanket and FW primary heat deposited box - axis.annotate( - "", - xy=(0.65, 0.52), - xytext=(0.62, 0.55), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "orange", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Draw arrow from blanket primary heat box to blanket and FW primary heat deposited box - axis.annotate( - "", - xy=(0.68, 0.52), - xytext=(0.7, 0.55), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "orange", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Draw a downward arrow from the primary thermal box to the right side of the generator - axis.annotate( - "", - xy=(0.825, 0.57), - xytext=(0.87, 0.57), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-", - "color": "orange", - "linewidth": 3.0, - "zorder": 5, - "fill": True, - }, - ) - - # Connect blanket thermal heat deposited to the shield heat deposited - axis.annotate( - "", - xy=(0.625, 0.57), - xytext=(0.695, 0.57), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-", - "color": "orange", - "linewidth": 3.0, - "zorder": 5, - "fill": True, - }, - ) - - # Connect first wall thermal heat deposited to the blanket heat deposited - axis.annotate( - "", - xy=(0.56, 0.52), - xytext=(0.56, 0.55), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-", - "color": "orange", - "linewidth": 3.0, - "zorder": 5, - "fill": True, - }, - ) - - # FW and blanket heat deposited box - axis.text( - 0.6, - 0.49, - f"Primary thermal (inc pump): {mfile.get('p_fw_blkt_heat_deposited_mw', scan=scan):,.2f} MWth\n", - **_text_layout(fig), - bbox=_box_style("orange"), - ) - - # Load the blanket image - with resources.path( - "process.core.io.plot.images", "blanket_with_coolant.png" - ) as img_path: - blanket = mpimg.imread(img_path.open("rb")) - - # Display the blanket image over the figure, not the axes - new_ax = axis.inset_axes( - (0.75, 0.625, 0.4, 0.4), transform=axis.transAxes, zorder=10 - ) - new_ax.imshow(blanket) - new_ax.axis("off") - - # Add blanket label above image - axis.text( - 0.7, - 0.9, - "Blanket", - fontsize=11, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - ) - - # Plot the nuclear heat total from blanket - axis.text( - 0.625, - 0.775, - ( - f"$P_{{\\text{{Blkt,nuclear}}}}$:\n{mfile.get('p_blkt_nuclear_heat_total_mw', scan=scan):,.2f} MW\n" - f"$P_{{\\text{{Blkt,multiplication}}}}$:\n{mfile.get('p_blkt_multiplication_mw', scan=scan):,.2f} MW\n" - f"$f_{{\\text{{multiplication}}}}$:\n{mfile.get('f_p_blkt_multiplication', scan=scan):,.2f}" - ), - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "grey", - "alpha": 0.8, - "linewidth": 2, - }, - ) - - # Load the vacuum vessel image - with resources.path("process.core.io.plot.images", "vv.png") as img_path: - vv = mpimg.imread(img_path.open("rb")) - - # Display the vacuum vessel image over the figure, not the axes - new_ax = axis.inset_axes( - (0.975, 0.625, 0.4, 0.4), transform=axis.transAxes, zorder=10 - ) - new_ax.imshow(vv) - new_ax.axis("off") - - # Add vacuum vessel label above image - axis.text( - 0.85, - 0.9, - "Vacuum Vessel", - fontsize=11, - verticalalignment="bottom", - horizontalalignment="left", - transform=fig.transFigure, - ) - - # Plot the secondary heat from the shield - axis.text( - 0.38, - 0.375, - f"$P_{{\\text{{shld,secondary}}}}$:\n{mfile.get('p_shld_secondary_heat_mw', scan=scan):,.2f} MWth", - **_text_layout(fig), - bbox=_box_style("lightblue") | {"linestyle": "dashed"}, - ) - - # Shield secondary power box to secondary heat total - axis.annotate( - "", - xy=(0.4, 0.3), - xytext=(0.4, 0.37), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - "linestyle": "--", - }, - ) - - # Arrow from shield bend to sheidl secondary heat - axis.annotate( - "", - xy=(0.445, 0.39), - xytext=(0.85, 0.39), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - "linestyle": "--", - }, - ) - - # Line from shield to arrow bend for secondary heat - axis.annotate( - "", - xy=(0.85, 0.385), - xytext=(0.85, 0.625), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - "linestyle": "--", - }, - ) - - # ============================================ - # Divertor - # ============================================ - - axis.text( - 0.325, - 0.48, - "Divertor", - transform=fig.transFigure, - horizontalalignment="left", - verticalalignment="bottom", - zorder=1000, # bring to front - fontsize=11, - color="white", # make text white - ) - - # Load the divertor image - with resources.path("process.core.io.plot.images", "divertor.png") as img_path: - divertor = mpimg.imread(img_path.open("rb")) - - # Display the divertor image over the figure, not the axes - new_ax = axis.inset_axes((0.1, 0.4, 0.3, 0.25), transform=axis.transAxes, zorder=10) - new_ax.imshow(divertor) - new_ax.axis("off") - - # Total divertor radiation power box - axis.text( - 0.29, - 0.57, - f"$P_{{\\text{{div,rad}}}}$:\n{mfile.get('p_div_rad_total_mw', scan=scan):,.2f} MW", - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "dodgerblue", - "alpha": 0.8, - "linewidth": 2, - }, - ) - - # Divertor nuclear heat total box - axis.text( - 0.4, - 0.58, - f"$P_{{\\text{{div,nuclear}}}}$:\n{mfile.get('p_div_nuclear_heat_total_mw', scan=scan):,.2f} MW", - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "grey", - "alpha": 0.8, - "linewidth": 2, - }, - ) - - # Divertor primary thermal heat deposited box - axis.text( - 0.44, - 0.46, - ( - f"Primary thermal (inc pump):\n{mfile.get('p_div_heat_deposited_mw', scan=scan):.2f} MWth\n" - f"Solid angle fraction: {mfile.get('f_ster_div_single', scan=scan):.3f}\n" - f"Primary heat fraction: {mfile.get('f_p_div_primary_heat', scan=scan):.3f}" - ), - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "orange", - "linewidth": 2, - }, - zorder=100, - ) - - # Divertor secondary heat box - axis.text( - 0.3, - 0.375, - f"$P_{{\\text{{div,secondary}}}}$:\n{mfile.get('p_div_secondary_heat_mw', scan=scan):.2f} MWth", - **_text_layout(fig), - bbox=_box_style("lightblue") | {"linestyle": "dashed"}, - ) - - # Divertor to divertor secondary heat arrow - axis.annotate( - "", - xy=(0.33, 0.405), - xytext=(0.33, 0.5), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - "linestyle": "--", - }, - ) - - # Divertor to divertor primary thermal heat arrow - axis.annotate( - "", - xy=(0.445, 0.5), - xytext=(0.4, 0.5), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "orange", - "linewidth": 2.0, - "zorder": 50, - "fill": True, - }, - ) - - # Divertor secondary heat to total secondary heat arrow - axis.annotate( - "", - xy=(0.33, 0.3), - xytext=(0.33, 0.375), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - "linestyle": "--", - }, - ) - - # =========================================== - - # =========================================== - # Coolant pumps - # =========================================== - - # Divertor coolant pump box - axis.text( - 0.55, - 0.33, - f"$P_{{\\text{{div,pump}}}}$: {mfile.get('p_div_coolant_pump_mw', scan=scan):.2f} MW", - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "wheat", - "alpha": 0.8, - "linewidth": 2, - }, - ) - - # Divertor pump box to divertor primary heat deposited box - axis.annotate( - "", - xy=(0.57, 0.46), - xytext=(0.57, 0.35), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 3.0, - "zorder": 5, - "fill": True, - }, - ) - - # Coolant pumps total to divertor pump box - axis.annotate( - "", - xy=(0.64, 0.34), - xytext=(0.7, 0.34), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Pumps total to shield bump box arrow - axis.annotate( - "", - xy=(0.875, 0.34), - xytext=(0.81, 0.34), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Shield coolant pump box - axis.text( - 0.875, - 0.325, - f"$P_{{\\text{{shld,pump}}}}$:\n{mfile.get('p_shld_coolant_pump_mw', scan=scan):.2f} MW", - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "wheat", - "alpha": 0.8, - "linewidth": 2, - }, - ) - - # FW and Blanket coolant pumps total - axis.text( - 0.725, - 0.4, - f"$P_{{\\text{{FW + Blkt}}}}$:\n{mfile.get('p_fw_blkt_coolant_pump_mw', scan=scan):.2f} MW", - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "wheat", - "alpha": 0.8, - "linewidth": 2, - }, - ) - - # FW and Blanket coolant pumps total to FW and Blanket heat deposited box - axis.annotate( - "", - xy=(0.75, 0.49), - xytext=(0.75, 0.44), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 3.0, - "zorder": 5, - "fill": True, - }, - ) - - # Coolant pumps total to blanket and FW pump - axis.annotate( - "", - xy=(0.75, 0.4), - xytext=(0.75, 0.36), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Shield pump to sheild primary thermal - axis.annotate( - "", - xy=(0.9, 0.54), - xytext=(0.9, 0.36), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Coolant pumps total electric box - axis.text( - 0.7, - 0.225, - ( - f"Coolant pumps electric:\n{mfile.get('p_coolant_pump_elec_total_mw', scan=scan):.3f} MWe\n" - f"$\\eta$: {mfile.get('eta_coolant_pump_electric', scan=scan):.3f}" - ), - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "lime", - "alpha": 0.8, - "linewidth": 2, - }, - ) - - # Coolant pumps total - axis.text( - 0.7, - 0.325, - f"Coolant pumps total:\n{mfile.get('p_coolant_pump_total_mw', scan=scan):.3f} MW", - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "wheat", - "alpha": 0.8, - "linewidth": 2, - }, - ) - - # Electric recirculated to pumps total arrow - axis.annotate( - "", - xy=(0.75, 0.325), - xytext=(0.75, 0.275), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Coolant pumps losses total box - axis.text( - 0.5, - 0.235, - f"Coolant pumps losses total:\n{mfile.get('p_coolant_pump_loss_total_mw', scan=scan):.3f} MWth", - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "lightblue", - "alpha": 0.8, - "linewidth": 2, - "linestyle": "dashed", - }, - ) - - # Coolant electric to pump losses arrow - axis.annotate( - "", - xy=(0.645, 0.25), - xytext=(0.695, 0.25), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - "linestyle": "--", - }, - ) - - # Coolant losses to secondary heat total arrow - axis.annotate( - "", - xy=(0.405, 0.25), - xytext=(0.4975, 0.25), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - "linestyle": "--", - }, - ) - - # ============================================ - - # =========================================== - # Plant core systems - # =========================================== - - # Cryo Plant box - axis.text( - 0.49, - 0.05, - f"Cryo Plant:\n{mfile.get('p_cryo_plant_electric_mw', scan=scan):.3f} MWe", - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "burlywood", - "alpha": 0.8, - "linewidth": 2, - }, - ) - - # Recirculated power to cryo plant arrow - axis.annotate( - "", - xy=(0.525, 0.075), - xytext=(0.525, 0.1625), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Tritium Plant box - axis.text( - 0.4, - 0.05, - f"Tritium Plant:\n{mfile.get('p_tritium_plant_electric_mw', scan=scan):.3f} MWe", - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "burlywood", - "alpha": 0.8, - "linewidth": 2, - }, - ) - - # # Recirculated power to tritium plant arrow - axis.annotate( - "", - xy=(0.44, 0.075), - xytext=(0.44, 0.1625), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Vacuum Pumps box - axis.text( - 0.575, - 0.05, - f"Vacuum pumps:\n{mfile.get('vachtmw', scan=scan):.3f} MWe", - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "burlywood", - "alpha": 0.8, - "linewidth": 2, - }, - ) - - # Recirculated power to vacuum pumps arrow - axis.annotate( - "", - xy=(0.62, 0.08), - xytext=(0.62, 0.1375), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Plant base load box - axis.text( - 0.085, - 0.075, - ( - f"Plant base load:\n{mfile.get('p_plant_electric_base_total_mw', scan=scan):.3f} MWe\n" - f"Minimum base load:\n{mfile.get('p_plant_electric_base', scan=scan) * 1.0e-6:.3f} MWe\n" - f"Plant floor power density:\n{mfile.get('pflux_plant_floor_electric', scan=scan) * 1.0e-3:.3f} kW$\\text{{m}}^{{-2}}$" - ), - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "burlywood", - "alpha": 0.8, - "linewidth": 2, - }, - ) - - # TF coil power box - axis.text( - 0.325, - 0.075, - f"TF coils:\n{mfile.get('p_tf_electric_supplies_mw', scan=scan):.3f} MWe", - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "burlywood", - "alpha": 0.8, - "linewidth": 2, - }, - ) - - # PF coil power box - axis.text( - 0.25, - 0.05, - f"PF coils:\n{mfile.get('p_pf_electric_supplies_mw', scan=scan):.3f} MWe", - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "burlywood", - "alpha": 0.8, - "linewidth": 2, - }, - ) - - # Recirculated power to TF,PF and plant base arrow bend - axis.annotate( - "", - xy=(0.22, 0.16), - xytext=(0.574, 0.16), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 1.5, - "zorder": 5, - "fill": True, - }, - ) - - # Recirculated power to PF - axis.annotate( - "", - xy=(0.28, 0.075), - xytext=(0.28, 0.1625), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # Recirculated power to TF - axis.annotate( - "", - xy=(0.35, 0.1), - xytext=(0.35, 0.1625), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - }, - ) - - # HCD secondary heat box - axis.text( - 0.46, - 0.285, - f"$P_{{\\text{{HCD,loss}}}}$:\n{mfile.get('p_hcd_secondary_heat_mw', scan=scan):.2f} MWth", - **_text_layout(fig), - bbox={ - "boxstyle": "round", - "facecolor": "lightblue", - "alpha": 0.8, - "linewidth": 2, - "linestyle": "dashed", - }, - ) - - # FW to HCD secondary heat arrow - axis.annotate( - "", - xy=(0.47, 0.32), - xytext=(0.47, 0.65), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - "linestyle": "--", - }, - ) - - # HCD loss to total secondary heat - axis.annotate( - "", - xy=(0.41, 0.295), - xytext=(0.455, 0.295), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - "linestyle": "--", - }, - ) - - # TF nuclear heat box - axis.text( - 0.155, - 0.25, - f"$P_{{\\text{{TF,nuclear}}}}$:\n{mfile.get('p_tf_nuclear_heat_mw', scan=scan):.2f} MWth", - **_text_layout(fig), - bbox=_box_style("lightblue") | {"linestyle": "dashed"}, - ) - - # TF nuclear heat to secondary heat total box arrow - axis.annotate( - "", - xy=(0.245, 0.265), - xytext=(0.215, 0.265), - xycoords=fig.transFigure, - arrowprops={ - "arrowstyle": "-|>,head_length=1,head_width=0.3", - "color": "black", - "linewidth": 2.0, - "zorder": 5, - "fill": True, - "linestyle": "--", - }, - ) - - # =========================================== - - -def plot_main_plasma_information( - axis: plt.Axes, - mfile: MFile, - scan: int, - colour_scheme: Literal[1, 2], - fig: plt.Figure, -): - """Plots the main plasma information including plasma shape, geometry, currents, heating, - confinement, and other relevant plasma parameters. - - Parameters - ---------- - axis : plt.Axes - The matplotlib axis object to plot on. - mfile : MFile - The MFILE data object containing plasma parameters. - scan : int - The scan number to use for extracting data. - colour_scheme : int - The colour scheme to use for plots. - fig : plt.Figure - The matplotlib figure object for additional annotations. - """ - # Import key variables - triang = mfile.get("triang", scan=scan) - kappa = mfile.get("kappa", scan=scan) - - # Remove the axes - axis.axis("off") - - # Plot the main plasma shape - plot_plasma(axis, mfile, scan, colour_scheme) - - rmajor = mfile.get("rmajor", scan=scan) - rminor = mfile.get("rminor", scan=scan) - # Get the plasma permieter points for the core plasma region - pg = plasma_geometry( - rmajor=rmajor, - rminor=mfile.get("rminor", scan=scan) - * mfile.get("radius_plasma_core_norm", scan=scan), - triang=mfile.get("triang", scan=scan), - kappa=mfile.get("kappa", scan=scan), - i_single_null=mfile.get("i_single_null", scan=scan), - i_plasma_shape=1, - square=mfile.get("plasma_square", scan=scan), - ) - # Plot the core plasma boundary line - axis.plot(pg.rs, pg.zs, color="black", linestyle="--") - - # Plot the centre of the plasma - axis.plot(rmajor, 0, "r+", markersize=20, markeredgewidth=2) - - # Add Q plasma information box - axis.text( - 0.725, - 0.175, - f"$Q_{{\\text{{plasma}}}}$: {mfile.get('big_q_plasma', scan=scan):.2f}", - fontsize=15, - verticalalignment="center", - horizontalalignment="center", - bbox=white_box, - transform=fig.transFigure, - ) - - # ========================================= - - # Draw a double-ended arrow from the inner plasma edge to the center - axis.annotate( - "", - xy=(rmajor - rminor, 0), # Inner plasma edge - xytext=(rmajor, 0), # Center - arrowprops={"arrowstyle": "<->", "color": "black"}, - ) - - # Add a label for the minor radius - axis.text( - rmajor - rminor / 2, - -rminor * kappa * 0.08, - f"$a$: {rminor:.2f} m", - fontsize=9, - color="black", - ha="center", - bbox=white_box, - ) - - # ============================================ - - # Draw a single-ended arrow from the machien centre to the plasma center - axis.annotate( - "", - xy=(axis.get_xlim()[0], -rminor * 0.3 * kappa), # Inner plasma edge - xytext=(rmajor, -rminor * 0.3 * kappa), # Center - arrowprops={"arrowstyle": "<-", "color": "black"}, - ) - - # Add a label for the major radius - axis.text( - rmajor - rminor / 2, - -rminor * kappa * 0.25, - f"$R_0$: {rmajor:.2f} m", - fontsize=9, - color="black", - ha="center", - bbox=white_box, - ) - - # ============================================ - - # Draw a double-ended arrow from the xpoint to the center to show elongation - axis.annotate( - "", - xy=(rmajor - rminor * triang, kappa * rminor), # Inner plasma edge - xytext=(rmajor - rminor * triang, 0), # Center - arrowprops={"arrowstyle": "<->", "color": "black"}, - ) - - # Write the elongation beside the vertical line, position relative to figure axes - axis.text( - 0.3, - 0.75, - f"$\\kappa$: {mfile.get('kappa', scan=scan):.2f}", - fontsize=9, - color="black", - rotation=270, - verticalalignment="center", - transform=axis.transAxes, - bbox=white_box, - ) - - # ============================================= - - # Draw a double-ended arrow from the inner plasma edge to the center - axis.annotate( - "", - xy=(rmajor - rminor * triang, kappa * rminor * 0.25), # Inner plasma edge - xytext=(rmajor, kappa * rminor * 0.25), # Center - arrowprops={"arrowstyle": "<->", "color": "black"}, - ) - - # Write the triangularity to the left of the cross, position relative to figure axes - axis.text( - rmajor - (rminor * triang * 0.75), - kappa * rminor * 0.3, - f"$\\delta$: {mfile.get('triang', scan=scan):.2f}", - fontsize=9, - color="black", - rotation=0, - verticalalignment="center", - bbox=white_box, - ) - - # ============================================= - - radius_plasma_core_norm = mfile.get("radius_plasma_core_norm", scan=scan) - - # Draw a double-ended arrow for the plasma core region - axis.annotate( - "", - xy=(rmajor, -rminor * 0.1 * kappa), # Inner plasma edge - xytext=(rmajor + (rminor * radius_plasma_core_norm), -rminor * 0.1 * kappa), - arrowprops={"arrowstyle": "<->", "color": "black"}, - ) - # Add a label for core region - axis.text( - rmajor + (rminor * radius_plasma_core_norm / 4), - -rminor * kappa * 0.15, - f"$\\rho_{{\\text{{core}}}}$: {radius_plasma_core_norm:.2f}", - fontsize=9, - color="black", - rotation=0, - verticalalignment="center", - bbox=white_box, - ) - - # ================================================ - - # Add plasma volume, areas and shaping information - - geom_type = PlasmaGeometryModelType(mfile.get("i_plasma_geometry", scan=scan)) - - textstr_plasma = ( - f"$\\mathbf{{Shaping:}}$\n\n" - f"$\\kappa_{{95}}$: {mfile.get('kappa95', scan=scan):.2f} ({geom_type.kappa95_model.description}) | $\\delta_{{95}}$: {mfile.get('triang95', scan=scan):.2f} ({geom_type.triang95_model.description}) | $\\zeta$: {mfile.get('plasma_square', scan=scan):.2f}\n" - f"$\\kappa$: {mfile.get('kappa', scan=scan):.2f} ({geom_type.kappa_model.description}) | $\\delta$: {mfile.get('triang', scan=scan):.2f} ({geom_type.triang_model.description}) | A: {mfile.get('aspect', scan=scan):.2f}\n" - f"$ V_{{\\text{{p}}}}:$ {mfile.get('vol_plasma', scan=scan):,.2f}$ \\ \\text{{m}}^3$ | $ A_{{\\text{{p,surface}}}}:$ {mfile.get('a_plasma_surface', scan=scan):,.2f}$ \\ \\text{{m}}^2$ | $ A_{{\\text{{p,poloidal}}}}:$ {mfile.get('a_plasma_poloidal', scan=scan):,.3f}$ \\ \\text{{m}}^2$\n" - f"$ L_{{\\text{{p,poloidal}}}}:$ {mfile.get('len_plasma_poloidal', scan=scan):,.3f}$ \\ \\text{{m}}$" - ) - - axis.text( - 0.365, - 0.975, - textstr_plasma, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=_box_style("lightyellow"), - ) - - # ============================================ - - # Draw a red arrow coming from the right and pointing at the plasma - for kap in (-kappa, kappa): - axis.annotate( - "", - # Pointing at plasma - xy=(rmajor + (rminor * 0.8), kap * rminor * 0.2), - # Starting point of arrow - xytext=(rmajor + (rminor * 1.4), kap * rminor * 0.2), - arrowprops={"facecolor": "red", "edgecolor": "red", "lw": 2}, - ) - - i_hcd_primary = mfile.get("i_hcd_primary", scan=scan) - i_hcd_secondary = mfile.get("i_hcd_secondary", scan=scan) - - # Add heating and current drive information - textstr_hcd = ( - f"$\\mathbf{{Heating \\ & \\ current \\ drive:}}$\n\n" - f"Total injected heat: {mfile.get('p_hcd_injected_total_mw', scan=scan):.3f} MW\n" - f"Ohmic heating power: {mfile.get('p_plasma_ohmic_mw', scan=scan):.3f} MW\n\n" - f"$\\mathbf{{Primary \\ system: {CurrentDriveModel(i_hcd_primary).abbreviation}}}$\n" - f"Current driving power {mfile.get('p_hcd_primary_injected_mw', scan=scan):.4f} MW\n" - f"Extra heat power: {mfile.get('p_hcd_primary_extra_heat_mw', scan=scan):.4f} MW\n" - f"$\\eta_{{\\text{{CD,prim}}}}$: {mfile.get('eta_cd_hcd_primary', scan=scan):.4f} A/W | $\\langle\\zeta_{{\\text{{CD,prim}}}}\\rangle$: {mfile.get('eta_cd_dimensionless_hcd_primary', scan=scan):.4f}\n" - f"$\\gamma_{{\\text{{CD,prim}}}}$: {mfile.get('eta_cd_norm_hcd_primary', scan=scan):.4f} $\\times 10^{{20}} \\mathrm{{A}} / \\mathrm{{Wm}}^2$\n" - f"Current driven by primary: {mfile.get('c_hcd_primary_driven', scan=scan) / 1e6:.3f} MA\n\n" - f"$\\mathbf{{Secondary \\ system: {CurrentDriveModel(i_hcd_secondary).abbreviation}}}$\n" - f"Current driving power {mfile.get('p_hcd_secondary_injected_mw', scan=scan):.4f} MW\n" - f"Extra heat power: {mfile.get('p_hcd_secondary_extra_heat_mw', scan=scan):.4f} MW\n" - f"$\\eta_{{\\text{{CD,sec}}}}$: {mfile.get('eta_cd_hcd_secondary', scan=scan):.4f} A/W | $\\langle\\zeta_{{\\text{{CD,sec}}}}\\rangle$: {mfile.get('eta_cd_dimensionless_hcd_secondary', scan=scan):.4f}\n" - f"$\\gamma_{{\\text{{CD,sec}}}}$: {mfile.get('eta_cd_norm_hcd_secondary', scan=scan):.4f} $\\times 10^{{20}} \\mathrm{{A}} / \\mathrm{{Wm}}^2$\n" - f"Current driven by secondary: {mfile.get('c_hcd_secondary_driven', scan=scan) / 1e6:.3f} MA" - ) - - axis.text( - 0.73, - 0.675, - textstr_hcd, - fontsize=9, - verticalalignment="top", - transform=plt.gcf().transFigure, - bbox=_box_style("paleturquoise") | {"edgecolor": "black"}, - ) - - class TextArgs(TypedDict): - fontsize: int - verticalalignment: str - transform: Transform - - text_args = TextArgs({ - "fontsize": 23, - "verticalalignment": "top", - "transform": fig.transFigure, - }) - - # Add injected power label - axis.text(0.92, 0.625, "$P_{\\text{inj}}$", **text_args) - - # ================================================ - - # Add beta information - textstr_beta = ( - f"$\\mathbf{{Beta \\ Information:}}$\n\n" - f"Total beta,$ \\ \\langle \\beta \\rangle$: {mfile.get('beta_total_vol_avg', scan=scan):.4f}\n" - f"Thermal beta,$ \\ \\langle \\beta_{{\\text{{thermal}}}} \\rangle$: {mfile.get('beta_thermal_vol_avg', scan=scan):.4f}\n" - f"Toroidal beta,$ \\ \\langle \\beta_{{\\text{{t}}}} \\rangle$: {mfile.get('beta_toroidal_vol_avg', scan=scan):.4f}\n" - f"Poloidal beta,$ \\ \\langle \\beta_{{\\text{{p}}}} \\rangle$: {mfile.get('beta_poloidal_vol_avg', scan=scan):.4f}\n" - f"Fast-alpha beta,$ \\ \\langle \\beta_{{\\alpha}} \\rangle$: {mfile.get('beta_fast_alpha', scan=scan):.4f}\n" - f"Upper limit on {BetaComponentLimits(int(mfile.get('i_beta_component', scan=scan))).full_name}: $ \\langle \\beta \\rangle$: {mfile.get('beta_vol_avg_max', scan=scan):.4f}\n" - f"Normalised total beta,$ \\ \\beta_{{\\text{{N}}}}$: {mfile.get('beta_norm_total', scan=scan):.4f}\n" - f"Normalised thermal beta,$ \\ \\beta_{{\\text{{N,thermal}}}}$: {mfile.get('beta_norm_thermal', scan=scan):.4f}\n" - f"Maximum normalised beta ({BetaNormMaxModel(int(mfile.get('i_beta_norm_max', scan=scan))).full_name}),$ \\ \\beta_{{\\text{{N,max}}}}$: {mfile.get('beta_norm_max', scan=scan):.4f}" - ) - - axis.text( - 0.025, - 0.975, - textstr_beta, - fontsize=9, - verticalalignment="top", - transform=fig.transFigure, - bbox=_box_style("lightblue"), - ) - - # Add beta label - axis.text(0.27, 0.94, "$\\beta$", **text_args) - - # ================================================ - - # Add volt-second information - textstr_volt_second = ( - f"$\\mathbf{{Volt-second \\ requirements:}}$\n\n" - f"Total volt-second consumption: {mfile.get('vs_plasma_total_required', scan=scan):.4f} Vs\n" - f" - Internal volt-seconds: {mfile.get('vs_plasma_internal', scan=scan):.4f} Vs\n" - f" - Volt-seconds needed for burn: {mfile.get('vs_plasma_burn_required', scan=scan):.4f} Vs\n" - f" - Volt-seconds needed for ramp: {mfile.get('vs_plasma_ramp_required', scan=scan):.4f} Vs | $C_{{\\text{{ejima}}}}$: {mfile.get('ejima_coeff', scan=scan):.4f}\n" - f"$V_{{\\text{{loop}}}}$: {mfile.get('v_plasma_loop_burn', scan=scan):.4f} V\n" - f"$\\Omega_{{\\text{{p}}}}$: {mfile.get('res_plasma', scan=scan):.4e} $\\Omega$\n" - f"Plasma resistive diffusion time: {mfile.get('t_plasma_res_diffusion', scan=scan):,.4f} s\n" - f"Plasma inductance: {mfile.get('ind_plasma', scan=scan):.4e} H | ITER $l_i(3)$: {mfile.get('ind_plasma_internal_norm_iter_3', scan=scan):.4f}\n" - f"Plasma stored magnetic energy: {mfile.get('e_plasma_magnetic_stored', scan=scan) / 1e9:.4f} GJ\n" - f"Plasma normalised internal inductance, $l_i$ ({IndInternalNormModel(int(mfile.get('i_ind_plasma_internal_norm', scan=scan))).full_name}) :{mfile.get('ind_plasma_internal_norm', scan=scan):.3f}" - ) - - axis.text( - 0.025, - 0.78, - textstr_volt_second, - fontsize=9, - verticalalignment="top", - transform=fig.transFigure, - bbox=_box_style("lightgreen"), - ) - - # Add volt second label - axis.text(0.30, 0.77, "Vs", **text_args) - - # ========================================= - - # Add divertor information - textstr_div = ( - f"\n$P_{{\\text{{sep}}}}$: {mfile.get('p_plasma_separatrix_mw', scan=scan):.2f} MW\n" - f"$\\frac{{P_{{\\text{{sep}}}}}}{{R}}$: {mfile.get('p_plasma_separatrix_rmajor_mw', scan=scan):.2f} MW/m\n" - f"$\\frac{{P_{{\\text{{sep}}}}B_T}}{{q_{{95}} A R}}$: {mfile.get('p_div_bt_q_aspect_rmajor_mw', scan=scan):.2f} MW T/m " - ) - - axis.text( - 0.35, - 0.12, - textstr_div, - fontsize=9, - verticalalignment="top", - transform=fig.transFigure, - bbox=_box_style("orange"), - ) - - # Add divertor label - axis.text(0.45, 0.1, "$P_{\\text{div}}$", **text_args) - - # ================================================ - - # Add confinement information - textstr_confinement = ( - f"$\\mathbf{{Confinement:}}$\n\n" - f"Confinement scaling law: {mfile.get('tauelaw', scan=scan)}\n" - f"Confinement $H$ factor: {mfile.get('hfact', scan=scan):.4f}\n" - f"Energy confinement time from scaling: {mfile.get('t_energy_confinement', scan=scan):.4f} s\n" - f"Fusion double product: {mfile.get('ntau', scan=scan):.4e} s/m³\n" - f"Lawson Triple product: {mfile.get('nttau', scan=scan):.4e} keV·s/m³\n" - f"Transport loss power assumed in scaling law: {mfile.get('p_plasma_loss_mw', scan=scan):.4f} MW\n" - f"Plasma thermal energy (inc. $\\alpha$), $W$: {mfile.get('e_plasma_beta', scan=scan) / 1e9:.4f} GJ\n" - f"Alpha particle confinement time: {mfile.get('t_alpha_confinement', scan=scan):.4f} s | $\\tau_{{\\alpha}}/\\tau_{{e}}$: {mfile.get('f_t_alpha_energy_confinement', scan=scan):.4f}" - ) - - axis.text( - 0.025, - 0.57, - textstr_confinement, - fontsize=9, - verticalalignment="top", - transform=fig.transFigure, - # Changed to a not normal color (Aquamarine) - bbox=_box_style("gainsboro") | {"edgecolor": "black"}, - ) - - # Add tau label - axis.text(0.3, 0.55, "$\\tau_{\\text{e}} $", **text_args) - - # ========================================= - - # Load the neutron image - with resources.path( - "process.core.io.plot.images", "alpha_particle.png" - ) as alpha_particle_image_path: - # Use importlib.resources to locate the image - alpha_particle = mpimg.imread(alpha_particle_image_path.open("rb")) - - # Display the neutron image over the figure, not the axes - new_ax = axis.inset_axes( - (0.975, 0.275, 0.075, 0.075), transform=axis.transAxes, zorder=10 - ) - new_ax.imshow(alpha_particle) - new_ax.axis("off") - - axis.annotate( - "", - xy=(rmajor + rminor, -rminor * kappa * 0.55), # Pointing at the plasma - xytext=(rmajor + 0.2 * rminor, -rminor * kappa * 0.25), - arrowprops={"facecolor": "red", "edgecolor": "grey", "lw": 1}, - ) - - textstr_alpha = ( - f"$P_{{\\alpha,\\text{{loss}}}}$ {mfile.get('p_fw_alpha_mw', scan=scan):.2f} MW\n" - f"$f_{{\\alpha,\\text{{coupled}}}}$ {mfile.get('f_p_alpha_plasma_deposited', scan=scan):.2f}" - ) - - axis.text( - 1.0, - 0.275, - textstr_alpha, - fontsize=9, - verticalalignment="top", - transform=axis.transAxes, - bbox={"boxstyle": "round", "facecolor": "red", "alpha": 1.0, "linewidth": 2}, - ) - - # ========================================= - with resources.path( - "process.core.io.plot.images", "neutron.png" - ) as neutron_image_path: - neutron = mpimg.imread(neutron_image_path.open("rb")) - new_ax = axis.inset_axes( - (0.975, 0.75, 0.075, 0.075), transform=axis.transAxes, zorder=10 - ) - new_ax.imshow(neutron) - new_ax.axis("off") - - # Draw a red arrow coming from the right and pointing at the plasma - axis.annotate( - "", - xy=(rmajor + rminor, rminor * kappa * 0.65), # Pointing at the plasma - xytext=(rmajor, rminor * kappa * 0.5), - arrowprops={"facecolor": "grey", "edgecolor": "grey", "lw": 1}, - ) - - textstr_neutron = ( - f"$P_{{\\text{{n,total}}}}$ {mfile.get('p_neutron_total_mw', scan=scan):.2f} MW\n" - f"$\\phi_{{\\text{{n,avg}}}}$ {mfile.get('pflux_plasma_surface_neutron_avg_mw', scan=scan):.3f} MW/m²" - ) - - axis.text( - 0.775, - 0.875, - textstr_neutron, - fontsize=9, - verticalalignment="top", - transform=axis.transAxes, - bbox={"boxstyle": "round", "facecolor": "grey", "alpha": 0.8, "linewidth": 2}, - ) - - # =============================================== - - # Add fusion reaction information - textstr_reactions = ( - f"$\\mathbf{{Fusion \\ Reactions:}}$\n\n" - f"Fuel mixture:\n" - f"| D: {mfile.get('f_plasma_fuel_deuterium', scan=scan):.2f} | T: {mfile.get('f_plasma_fuel_tritium', scan=scan):.2f} | 3He: {mfile.get('f_plasma_fuel_helium3', scan=scan):.2f} |\n\n" - f"Fusion Power, $P_{{\\text{{fus}}}}:$ {mfile.get('p_fusion_total_mw', scan=scan):,.2f} MW\n" - f"D-T Power, $P_{{\\text{{fus,DT}}}}:$ {mfile.get('p_dt_total_mw', scan=scan):,.2f} MW\n" - f"D-D Power, $P_{{\\text{{fus,DD}}}}:$ {mfile.get('p_dd_total_mw', scan=scan):,.2f} MW\n" - f"D-3He Power, $P_{{\\text{{fus,D3He}}}}:$ {mfile.get('p_dhe3_total_mw', scan=scan):,.2f} MW\n" - f"Alpha Power, $P_{{\\alpha}}:$ {mfile.get('p_alpha_total_mw', scan=scan):,.2f} MW" - ) - - axis.text( - 0.025, - 0.4, - textstr_reactions, - fontsize=9, - verticalalignment="top", - transform=fig.transFigure, - bbox={"boxstyle": "round", "facecolor": "red", "alpha": 0.6, "linewidth": 2}, - ) - - # ================================================ - - # Add fuelling information - textstr_fuelling = ( - f"$\\mathbf{{Fuelling:}}$\n\n" - f"Plasma mass: {mfile.get('m_plasma', scan=scan) * 1000:.4f} g\n" - f" - Average mass of all plasma ions: {mfile.get('m_ions_total_amu', scan=scan):.3f} amu\n" - f"Fuel mass: {mfile.get('m_plasma_fuel_ions', scan=scan) * 1000:.4f} g\n" - f" - Average mass of all fuel ions: {mfile.get('m_fuel_amu', scan=scan):.3f} amu\n\n" - f"Fueling rate: {mfile.get('molflow_plasma_fuelling_required', scan=scan):.3e} nucleus-pairs/s\n" - f"Fuel burn-up rate: {mfile.get('rndfuel', scan=scan):.3e} reactions/s\n" - f"Burn-up fraction: {mfile.get('burnup', scan=scan):.4f}\n" - ) - - axis.text( - 0.025, - 0.22, - textstr_fuelling, - fontsize=9, - verticalalignment="top", - transform=fig.transFigure, - bbox=_box_style("khaki") | {"edgecolor": "black"}, - ) - - # ================================================ - - # Add ion density information - impurity_data = ImpurityRadiationData() - textstr_ions = ( - f" $\\mathbf{{Ion \\ to \\ electron}}$\n" - f" $\\mathbf{{relative \\ number}}$\n" - f" $\\mathbf{{densities:}}$\n\n" - f" Effective charge: {mfile.get('n_charge_plasma_effective_vol_avg', scan=scan):.3f}\n\n" - + "\n".join( - f" {label.replace('_', '') + ':':<6}" - f"{mfile.get(f'f_nd_impurity_electrons({index:02d})', scan=scan):.4e}" - for index, label in enumerate(impurity_data.imp_label[:14], start=1) - ) - ) - - axis.text( - 0.805, - 0.335, - textstr_ions, - fontsize=9, - verticalalignment="top", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "olivedrab", - "alpha": 0.7, - "linewidth": 2, - }, - ) - - # Add ion charge label - axis.text(0.815, 0.29, "$Z$", **text_args) - - # ================================================ - - # Add plasma current information - textstr_currents = ( - f"$\\mathbf{{Plasma\\ currents:}}$\n\n" - f"Plasma current ({PlasmaCurrentModel(int(mfile.get('i_plasma_current', scan=scan))).full_name}): {mfile.get('plasma_current_ma', scan=scan):.4f} MA\n" - f" - Bootstrap fraction ({BootstrapCurrentFractionModel(int(mfile.get('i_bootstrap_current', scan=scan))).full_name}): {mfile.get('f_c_plasma_bootstrap', scan=scan):.4f}\n" - f" - Diamagnetic fraction ({PlasmaDiamagneticCurrentModel(int(mfile.get('i_diamagnetic_current', scan=scan))).full_name}): {mfile.get('f_c_plasma_diamagnetic', scan=scan):.4f}\n" - f" - Pfirsch-Schlüter fraction {mfile.get('f_c_plasma_pfirsch_schluter', scan=scan):.4f}\n" - f" - Auxiliary fraction {mfile.get('f_c_plasma_auxiliary', scan=scan):.4f}\n" - f" - Inductive fraction {mfile.get('f_c_plasma_inductive', scan=scan):.4f}" - ) - - axis.text( - 0.72, - 0.975, - textstr_currents, - fontsize=9, - verticalalignment="top", - transform=fig.transFigure, - bbox=_box_style("#C8A2C8"), # Hex code for lilac color - ) - - # Add plasma current label - axis.text(0.93, 0.9, "$I_{\\text{p}} $", **text_args) - - # Add magnetic field information - textstr_fields = ( - f"$\\mathbf{{Magnetic\\ fields:}}$\n\n" - f"Toroidal field at $R_0$, $B_{{T}}$: {mfile.get('b_plasma_toroidal_on_axis', scan=scan):.4f} T\n" - f" Ripple at outboard , $\\delta$: {mfile.get('ripple_b_tf_plasma_edge', scan=scan):.2f}%\n" - f"Surface average poloidal field, $\\langle B_{{p}}(a) \\rangle$: {mfile.get('b_plasma_surface_poloidal_average', scan=scan):.4f} T\n" - f"Total field, $B_{{tot}}$: {mfile.get('b_plasma_total', scan=scan):.4f} T\n" - f"Vertical field, $B_{{vert}}$: {mfile.get('b_plasma_vertical_required', scan=scan):.4f} T" - ) - - axis.text( - 0.5325, - 0.14, - textstr_fields, - fontsize=9, - verticalalignment="top", - transform=fig.transFigure, - bbox=_box_style("royalblue"), - ) - - # Add magnetic field label - axis.text(0.75, 0.12, "$B$", **text_args) - - # Add radiation information - textstr_radiation = ( - f" $\\mathbf{{Radiation:}}$\n\n" - f" Total radiation power {mfile.get('p_plasma_rad_mw', scan=scan):.4f} MW\n" - f" Separatrix radiation fraction {mfile.get('f_p_plasma_separatrix_rad', scan=scan):.4f}\n" - f" Core radiation power {mfile.get('p_plasma_inner_rad_mw', scan=scan):.4f} MW\n" - f" - $f_{{\\text{{core,reduce}}}}$ {mfile.get('f_p_plasma_core_rad_reduction', scan=scan):.4f}\n" - f" Edge radiation power {mfile.get('p_plasma_outer_rad_mw', scan=scan):.4f} MW\n" - f" Synchrotron radiation power {mfile.get('p_plasma_sync_mw', scan=scan):.4f} MW\n" - f" Synchrotron wall reflectivity {mfile.get('f_sync_reflect', scan=scan):.4f}" - ) - - axis.text( - 0.72, - 0.83, - textstr_radiation, - fontsize=9, - verticalalignment="top", - transform=fig.transFigure, - bbox=_box_style("lavender") | {"edgecolor": "black"}, - ) - - # Add radiation label - axis.text(0.725, 0.78, "$\\gamma$", **text_args) - - # Add L-H threshold information - model_name = PlasmaConfinementTransitionModel( - int(mfile.get("i_l_h_threshold", scan=scan)) - ).full_name - - # Wrap long model names to new line - if len(model_name) > 20: - model_name = "\n".join(textwrap.wrap(model_name, width=20)) - - textstr_lh = ( - f"$\\mathbf{{L-H \\ threshold:}}$\n" - f"{model_name}\n\n" - f"$P_{{\\text{{L-H}}}}:$ {mfile.get('p_l_h_threshold_mw', scan=scan):.4f} MW" - ) - - axis.text( - 0.22, - 0.4, - textstr_lh, - fontsize=9, - verticalalignment="top", - transform=fig.transFigure, - bbox=_box_style("peachpuff"), - ) - - # Add density limit information - textstr_density_limit = ( - f"$\\mathbf{{Density \\ limit:}}$\n" - f"({DensityLimitModel(int(mfile.get('i_density_limit', scan=scan))).full_name})\n" - f"$n_{{\\text{{e,limit}}}}: {mfile.get('nd_plasma_electrons_max', scan=scan):.3e} \\ m^{{-3}}$\n" - f"$f_{{\\text{{GW}}}}$: {mfile.get('f_nd_plasma_greenwald', scan=scan):.4f}" - ) - - axis.text( - 0.22, - 0.31, - textstr_density_limit, - fontsize=9, - verticalalignment="top", - transform=fig.transFigure, - bbox=_box_style("pink"), - ) - - -def plot_current_profiles_over_time(axis: plt.Axes, mfile: MFile, scan: int): - """Plots the current profiles over time for PF circuits, CS coil, and plasma.""" - pulse_timings = PulseTimings( - t_plant_pulse_coil_precharge=mfile.get( - "t_plant_pulse_coil_precharge", scan=scan - ), - t_plant_pulse_plasma_current_ramp_up=mfile.get( - "t_plant_pulse_plasma_current_ramp_up", scan=scan - ), - t_plant_pulse_fusion_ramp=mfile.get("t_plant_pulse_fusion_ramp", scan=scan), - t_plant_pulse_burn=mfile.get("t_plant_pulse_burn", scan=scan), - t_plant_pulse_plasma_current_ramp_down=mfile.get( - "t_plant_pulse_plasma_current_ramp_down", scan=scan - ), - t_plant_pulse_dwell=mfile.get("t_plant_pulse_dwell", scan=scan), - ) - - # Find the number of PF circuits, n_pf_cs_plasma_circuits includes the CS and plasma circuits - n_pf_cs_plasma_circuits = mfile.get("n_pf_cs_plasma_circuits", scan=scan) - - # Extract PF circuit times - # n_pf_cs_plasma_circuits contains the CS and plasma at the end so we subtract 2 - pf_circuits = {} - for i in range(int(n_pf_cs_plasma_circuits - 2)): - pf_circuits[f"PF Circuit {i}"] = [ - mfile.get(f"pfc{i}t{j}", scan=scan) - for j in range(pulse_timings.n_pf_active_points_total) - ] - # Change from 0 to 1 index to align with poloidal cross-section plot numbering - axis.plot( - pulse_timings.pf_active_cumulative, - pf_circuits[f"PF Circuit {i}"], - label=f"PF Coil {i + 1}", - linestyle="--", - ) - - # Since CS may not always be present try to retrieve values - try: - cs_circuit = [ - mfile.get(f"cs_t{i}", scan=scan) - for i in range(pulse_timings.n_pf_active_points_total) - ] - axis.plot( - pulse_timings.pf_active_cumulative, - cs_circuit, - label="CS Coil", - linestyle="--", - ) - except KeyError: - pass - - # Plasma current values - plasmat1 = mfile.get("plasmat1", scan=scan) - plasmat2 = mfile.get("plasmat2", scan=scan) - plasmat3 = mfile.get("plasmat3", scan=scan) - plasmat4 = mfile.get("plasmat4", scan=scan) - plasmat5 = mfile.get("plasmat5", scan=scan) - - # x-coordinates for the plasma current - x_plasma = pulse_timings.pf_active_cumulative[1:] - # x-coordinates for the plasma current - y_plasma = [plasmat1, plasmat2, plasmat3, plasmat4, plasmat5] - - # Plot the plasma current - axis.plot(x_plasma, y_plasma, "black", linewidth=2, label="Plasma") - - # Move the x-axis to 0 on the y-axis - axis.spines["bottom"].set_position("zero") - - # Annotate key points - # Create a secondary x-axis for annotations - secax = axis.secondary_xaxis("bottom") - # Exclude the dwell point so tick positions and labels remain aligned. - secax.set_xticks(pulse_timings.pf_active_cumulative[:-1]) - secax.set_xticklabels( - pulse_timings.POINT_LABELS[ - :-1 - ], # Exclude the last label as it corresponds to the dwell period - rotation=60, - ) - secax.tick_params(axis="x", which="major") - - # Add axis labels - axis.set_xlabel("Time [s]", fontsize=12) - axis.xaxis.set_label_coords(1.05, 0.5) - axis.set_ylabel("Current [A]", fontsize=12) - - # Add a title - axis.set_title("Current Profiles Over Time", fontsize=14) - - # Add a legend - axis.legend() - - axis.set_yscale("symlog") - - # Add a grid for better readability - axis.grid(True, linestyle="--", alpha=0.6) - - -def plot_system_power_profiles_over_time(axis: plt.Axes, mfile: MFile, scan: int, fig): - """Plots the power profiles over time for various systems.""" - pulse_timings = PulseTimings( - t_plant_pulse_coil_precharge=mfile.get( - "t_plant_pulse_coil_precharge", scan=scan - ), - t_plant_pulse_plasma_current_ramp_up=mfile.get( - "t_plant_pulse_plasma_current_ramp_up", scan=scan - ), - t_plant_pulse_fusion_ramp=mfile.get("t_plant_pulse_fusion_ramp", scan=scan), - t_plant_pulse_burn=mfile.get("t_plant_pulse_burn", scan=scan), - t_plant_pulse_plasma_current_ramp_down=mfile.get( - "t_plant_pulse_plasma_current_ramp_down", scan=scan - ), - t_plant_pulse_dwell=mfile.get("t_plant_pulse_dwell", scan=scan), - ) - - # Create empty arrays for the power at each time step for each system - power_profiles = { - "Fusion Power": np.zeros(pulse_timings.n_pulse_points_total), - "Plant Base Load": np.zeros(pulse_timings.n_pulse_points_total), - "Cryo Plant": np.zeros(pulse_timings.n_pulse_points_total), - "Tritium Plant": np.zeros(pulse_timings.n_pulse_points_total), - "Vacuum Pumps": np.zeros(pulse_timings.n_pulse_points_total), - "TF Coil Supplies": np.zeros(pulse_timings.n_pulse_points_total), - "PF Coil Supplies": np.zeros(pulse_timings.n_pulse_points_total), - "Coolant Pump Elec Total": np.zeros(pulse_timings.n_pulse_points_total), - "HCD Electric Total": np.zeros(pulse_timings.n_pulse_points_total), - "Gross Electric Power": np.zeros(pulse_timings.n_pulse_points_total), - "Net Electric Power": np.zeros(pulse_timings.n_pulse_points_total), - } - - # Fill power_profiles arrays using vectorized assignment - for label, key in [ - ("Fusion Power", "p_fusion_total_profile_mw"), - ("Gross Electric Power", "p_plant_electric_gross_profile_mw"), - ("Net Electric Power", "p_plant_electric_net_profile_mw"), - ("Plant Base Load", "p_plant_electric_base_total_profile_mw"), - ("Cryo Plant", "p_cryo_plant_electric_profile_mw"), - ("Tritium Plant", "p_tritium_plant_electric_profile_mw"), - ("Vacuum Pumps", "vachtmw_profile_mw"), - ("TF Coil Supplies", "p_tf_electric_supplies_profile_mw"), - ("PF Coil Supplies", "p_pf_electric_supplies_profile_mw"), - ("Coolant Pump Elec Total", "p_coolant_pump_elec_total_profile_mw"), - ("HCD Electric Total", "p_hcd_electric_total_profile_mw"), - ]: - for time in range(pulse_timings.n_pulse_points_total): - power_profiles[label][time] = mfile.get(f"{key}{time}", scan=scan) - - # Define line styles for each system - # All net drains (negative power flows) use the same line style: dashed - line_styles = { - "Fusion Power": ":", - "Plant Base Load": "--", - "Cryo Plant": "--", - "Tritium Plant": "--", - "Vacuum Pumps": "--", - "TF Coil Supplies": "--", - "PF Coil Supplies": "--", - "Coolant Pump Elec Total": "--", - "HCD Electric Total": "--", - "Gross Electric Power": "-", - "Net Electric Power": "-", - } - - # Plot each system's power profile over time with different line styles - for label, powers in power_profiles.items(): - style = line_styles.get(label, "-") - axis.plot( - pulse_timings.total_pulse_cumulative, powers, label=label, linestyle=style - ) - - # Move the x-axis to 0 on the y-axis - axis.spines["bottom"].set_position("zero") - - # Annotate key points - # Create a secondary x-axis for annotations - secax = axis.secondary_xaxis("bottom") - # Label phase starts only (exclude final end-of-dwell point). - secax.set_xticks(pulse_timings.total_pulse_cumulative[:-1]) - secax.set_xticklabels( - pulse_timings.POINT_LABELS, - rotation=60, - ) - secax.tick_params(axis="x", which="major") - - # Add axis labels - axis.set_xlabel("Time [s]", fontsize=12) - axis.xaxis.set_label_coords(1.05, 0.5) - axis.set_ylabel("Power [MW]", fontsize=12) - - # Add a title - axis.set_title("System Power Over Time", fontsize=14) - - # Add a legend - axis.legend() - - axis.set_yscale("symlog") - axis.minorticks_on() - axis.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.2) - - # Add a grid for better readability - axis.grid(True, linestyle="--", alpha=0.6) - - # Add energy produced info - textstr_energy = ( - f"$\\mathbf{{Energy \\ Production:}}$\n\n" - f"Energy produced over whole pulse: {mfile.get('e_plant_net_electric_pulse_mj', scan=scan):,.4f} MJ\n" - f"Energy produced over whole pulse: {mfile.get('e_plant_net_electric_pulse_kwh', scan=scan):,.4f} kWh\n" - ) - - axis.text( - 0.075, - 0.2, - textstr_energy, - fontsize=9, - verticalalignment="top", - transform=fig.transFigure, - bbox=_box_style("grey"), - ) - - # Add energy produced info - - textstr_times = ( - f"$\\mathbf{{Pulse \\ Timings:}}$\n\n" - f"Coil precharge, $t_{{\\text{{precharge}}}}$: {mfile.get('t_plant_pulse_coil_precharge', scan=scan):,.1f} s ({secs_to_hms(mfile.get('t_plant_pulse_coil_precharge', scan=scan))})\n" - f"Current ramp up, $t_{{\\text{{current ramp}}}}$: {mfile.get('t_plant_pulse_plasma_current_ramp_up', scan=scan):,.1f} s ({secs_to_hms(mfile.get('t_plant_pulse_plasma_current_ramp_up', scan=scan))})\n" - f"Fusion ramp, $t_{{\\text{{fusion ramp}}}}$: {mfile.get('t_plant_pulse_fusion_ramp', scan=scan):,.1f} s ({secs_to_hms(mfile.get('t_plant_pulse_fusion_ramp', scan=scan))})\n" - f"Burn, $t_{{\\text{{burn}}}}$: {mfile.get('t_plant_pulse_burn', scan=scan):,.1f} s ({secs_to_hms(mfile.get('t_plant_pulse_burn', scan=scan))})\n" - f"Ramp down, $t_{{\\text{{ramp down}}}}$: {mfile.get('t_plant_pulse_plasma_current_ramp_down', scan=scan):,.1f} s ({secs_to_hms(mfile.get('t_plant_pulse_plasma_current_ramp_down', scan=scan))})\n" - f"Between pulse, $t_{{\\text{{between pulse}}}}$: {mfile.get('t_plant_pulse_dwell', scan=scan):,.1f} s ({secs_to_hms(mfile.get('t_plant_pulse_dwell', scan=scan))})\n\n" - f"Total pulse length, $t_{{\\text{{cycle}}}}$: {mfile.get('t_plant_pulse_total', scan=scan):,.1f} s ({secs_to_hms(mfile.get('t_plant_pulse_total', scan=scan))})\n" - ) - - axis.text( - 0.6, - 0.225, - textstr_times, - fontsize=9, - verticalalignment="top", - transform=fig.transFigure, - bbox=_box_style("grey"), - ) - - -def plot_cryostat( - axis: plt.Axes, - mfile: MFile, - scan: int, - colour_scheme: Literal[1, 2], - mirror_negative_x: bool = False, -): - """Function to plot cryostat in poloidal cross-section - - Parameters - ---------- - axis : plt.Axes - axis object to plot to - mfile : MFile - MFILE data object - scan : int - scan number to use - colour_scheme : Literal[1, 2] - colour scheme to use for plots - mirror_negative_x : bool - if True, mirror the plot to the negative x-axis (Default value = False) - """ - rects = cryostat_geometry( - r_cryostat_inboard=mfile.get("r_cryostat_inboard", scan=scan), - dr_cryostat=mfile.get("dr_cryostat", scan=scan), - z_cryostat_half_inside=mfile.get("z_cryostat_half_inside", scan=scan), - ) - - # Apply mirror transformation if requested - x_scale = -1 if mirror_negative_x else 1 - - for rec in rects: - axis.add_patch( - patches.Rectangle( - xy=(x_scale * rec.anchor_x, rec.anchor_z), - width=x_scale * rec.width, - height=rec.height, - facecolor=CRYOSTAT_COLOUR[colour_scheme - 1], - ) - ) - - -def color_key(axis: plt.Axes, mfile: MFile, scan: int, colour_scheme: Literal[1, 2]): - """Function to plot the colour key""" - axis.set_ylim(0, 10) - axis.set_xlim(0, 10) - axis.set_axis_off() - axis.set_autoscaley_on(False) - axis.set_autoscalex_on(False) - - labels = [ - ("CS coil", SOLENOID_COLOUR[colour_scheme - 1]), - ("CS comp", CSCOMPRESSION_COLOUR[colour_scheme - 1]), - ( - "TF coil", - ( - TFC_COLOUR[colour_scheme - 1] - if mfile.get("i_tf_sup", scan=scan) != 0 - else "#b87333" - ), - ), - ("Thermal shield", THERMAL_SHIELD_COLOUR[colour_scheme - 1]), - ("VV & shield", VESSEL_COLOUR[colour_scheme - 1]), - ("Blanket", BLANKET_COLOUR[colour_scheme - 1]), - ("First wall", FIRSTWALL_COLOUR[colour_scheme - 1]), - ("Plasma", PLASMA_COLOUR[colour_scheme - 1]), - ("PF coils", "none"), - ("Divertor", "black"), - ] - - if (mfile.get("i_hcd_primary", scan=scan) in {5, 8}) or ( - mfile.get("i_hcd_secondary", scan=scan) in {5, 8} - ): - labels.extend(( - ("NB duct shield", NBSHIELD_COLOUR[colour_scheme - 1]), - ("Cryostat", CRYOSTAT_COLOUR[colour_scheme - 1]), - )) - else: - labels.append(("Cryostat", CRYOSTAT_COLOUR[colour_scheme - 1])) - - for i, (text, color) in enumerate(labels): - row = i // 4 - col = i % 4 - y_pos = 9 - row * 1.5 - x_pos = col * 2.5 - - axis.text(x_pos, y_pos, text, ha="left", va="top", size="small") - axis.add_patch( - patches.Rectangle( - (x_pos + 1.5, y_pos - 0.35), - 0.5, - 0.4, - lw=0 if color != "none" else 1, - facecolor=color if color != "none" else "none", - edgecolor="black" if color == "none" else "none", - ) - ) - - -# helper to convert seconds to "Hh Mm Ss" -def secs_to_hms(s): - """Convert seconds to 'Hh Mm Ss' string.""" - s = float(s) - return f"{int(s // 3600)}h {int((s % 3600) // 60)}m {int(s % 60)}s" - - -def toroidal_cross_section( - axis: plt.Axes, - mfile: MFile, - scan: int, - demo_ranges: bool, - colour_scheme: Literal[1, 2], -): - """Function to plot toroidal cross-section""" - axis.set_xlabel("R [m]") - axis.set_ylabel("X [m]") - axis.set_title("Toroidal Cross-Section") - axis.minorticks_on() - axis.grid(which="both", linestyle="--", linewidth=0.5, alpha=0.2) - - rmajor = mfile.get("rmajor", scan=scan) - rminor = mfile.get("rminor", scan=scan) - r_cryostat_inboard = mfile.get("r_cryostat_inboard", scan=scan) - dr_cryostat = mfile.get("dr_cryostat", scan=scan) - n_tf_coils = mfile.get("n_tf_coils", scan=scan) - if ( - CurrentDriveModel(mfile.get("i_hcd_primary", scan=scan)).method - == CurrentDriveMethodType.NEUTRAL_BEAM - or CurrentDriveModel(mfile.get("i_hcd_secondary", scan=scan)).method - == CurrentDriveMethodType.NEUTRAL_BEAM - ): - dx_beam_shield = mfile.get("dx_beam_shield", scan=scan) - dx_beam_duct = mfile.get("dx_beam_duct", scan=scan) - radius_beam_tangency = mfile.get("radius_beam_tangency", scan=scan) - else: - dx_beam_shield = 0 - dx_beam_duct = 0 - radius_beam_tangency = 0 - - dr_tf_outboard = mfile.get("dr_tf_outboard", scan=scan) - full_angle = 2 * np.pi - arc(axis, rmajor, theta2=full_angle, style="dashed") - - # Colour in the main components - for v, colours in [ - ("dr_cs", SOLENOID_COLOUR[colour_scheme - 1]), - ("dr_cs_precomp", CSCOMPRESSION_COLOUR[colour_scheme - 1]), - ( - "dr_tf_inboard", - ( - TFC_COLOUR[colour_scheme - 1] - if TFConductorModel(mfile.get("i_tf_sup", scan=scan)) - != TFConductorModel.WATER_COOLED_COPPER - else "#b87333" - ), - ), - ("dr_shld_thermal_inboard", THERMAL_SHIELD_COLOUR[colour_scheme - 1]), - ("dr_vv_inboard", VESSEL_COLOUR[colour_scheme - 1]), - ("dr_shld_inboard", VESSEL_COLOUR[colour_scheme - 1]), - ("dr_blkt_inboard", BLANKET_COLOUR[colour_scheme - 1]), - ("dr_fw_inboard", FIRSTWALL_COLOUR[colour_scheme - 1]), - ("dr_fw_outboard", FIRSTWALL_COLOUR[colour_scheme - 1]), - ("dr_blkt_outboard", BLANKET_COLOUR[colour_scheme - 1]), - ("dr_shld_outboard", SHIELD_COLOUR[colour_scheme - 1]), - ("dr_vv_outboard", VESSEL_COLOUR[colour_scheme - 1]), - ("dr_shld_thermal_outboard", THERMAL_SHIELD_COLOUR[colour_scheme - 1]), - ]: - r2, r1 = cumulative_radial_build2(v, mfile, scan) - arc_fill(axis, r1, r2, color=colours, theta2=full_angle + 1) - - arc_fill( - axis, - rmajor - rminor, - rmajor + rminor, - color=PLASMA_COLOUR[colour_scheme - 1], - theta2=full_angle + 1, - ) - - arc_fill( - axis, - r_cryostat_inboard, - r_cryostat_inboard + dr_cryostat, - color=CRYOSTAT_COLOUR[colour_scheme - 1], - theta2=full_angle + 1, - ) - - # Segment the TF coil inboard - # Calculate centrelines - spacing = 2 * np.pi / n_tf_coils - coil_indices = np.arange(int(n_tf_coils)) - - r1, _ = cumulative_radial_build2("dr_cs_tf_gap", mfile, scan) - r2, _ = cumulative_radial_build2("dr_tf_inboard", mfile, scan) - r4, r3 = cumulative_radial_build2("dr_tf_outboard", mfile, scan) - - # Coil width - w = r2 * np.tan(spacing / 2) - for ang in (coil_indices * spacing) - spacing / 2: - axis.plot( - [r1 * np.cos(ang), r2 * np.cos(ang)], - [r1 * np.sin(ang), r2 * np.sin(ang)], - color="black", - ) - - for item in coil_indices: - # Neutral beam shielding - TF_outboard( - axis, - item, - n_tf_coils=n_tf_coils, - r3=r3, - r4=r4, - w=w + dx_beam_shield, - facecolor=NBSHIELD_COLOUR[colour_scheme - 1], - ) - # Overlay TF coil segments - TF_outboard( - axis, - item, - n_tf_coils=n_tf_coils, - r3=r3, - r4=r4, - w=w, - facecolor=( - TFC_COLOUR[colour_scheme - 1] - if TFConductorModel(mfile.get("i_tf_sup", scan=scan)) - != TFConductorModel.WATER_COOLED_COPPER - else "#b87333" - ), - ) - - i_hcd_primary = mfile.get("i_hcd_primary", scan=scan) - if CurrentDriveModel(i_hcd_primary).method == CurrentDriveMethodType.NEUTRAL_BEAM: - # Neutral beam geometry. See docs for diagram. - a = w + dx_beam_shield - b = dr_tf_outboard - d = r3 - e = np.sqrt(a**2 + (d + b) ** 2) - - # Beam edges from centreline - half_duct = 0.5 * dx_beam_duct - r_beam_inner = radius_beam_tangency - half_duct - r_beam_outer = radius_beam_tangency + half_duct - - def calc_xy(rt, e=e): - arg = np.clip(rt / e, -1.0, 1.0) - beta = np.arccos(arg) - x = rt * np.cos(beta) - y = rt * np.sin(beta) - return x, y - - # Tangency points - x_beam_inner, y_beam_inner = calc_xy(r_beam_inner) - x_beam_outer, y_beam_outer = calc_xy(r_beam_outer) - - # TF-side positions (beam sits inside shield) - x0 = r4 - y0_beam_inner = w + dx_beam_shield - y0_beam_outer = y0_beam_inner + dx_beam_duct - - # Centreline tangency point - x_beam_centre, y_beam_centre = calc_xy(radius_beam_tangency) - y0_beam_centre = y0_beam_inner + 0.5 * dx_beam_duct - - # Draw beam duct boundaries - axis.plot( - [x_beam_inner, x0], - [y_beam_inner, y0_beam_inner], - linestyle="dotted", - color="black", - ) - axis.plot( - [x_beam_outer, x0], - [y_beam_outer, y0_beam_outer], - linestyle="dotted", - color="black", - ) - # Draw beam centreline - axis.plot( - [x_beam_centre, x0], - [y_beam_centre, y0_beam_centre], - linestyle="--", - color="black", - linewidth=1.5, - ) - - # Draw dividing lines in the blanket (inboard modules, toroidal direction) - n_blkt_inboard_modules_toroidal = mfile.get( - "n_blkt_inboard_modules_toroidal", scan=scan - ) - if n_blkt_inboard_modules_toroidal > 1: - # Calculate the angular spacing for each module - spacing = full_angle / (n_blkt_inboard_modules_toroidal) - r1, _ = cumulative_radial_build2("dr_shld_inboard", mfile, scan) - r2, _ = cumulative_radial_build2("dr_blkt_inboard", mfile, scan) - for i in range(int(n_blkt_inboard_modules_toroidal)): - ang = i * spacing - # Draw a line from r1 to r2 at angle ang - axis.plot( - [r1 * np.cos(ang), r2 * np.cos(ang)], - [r1 * np.sin(ang), r2 * np.sin(ang)], - color="black", - linestyle="-", - linewidth=1.5, - zorder=100, - ) - - # Draw dividing lines in the blanket (outboard modules, toroidal direction) - n_blkt_outboard_modules_toroidal = mfile.get( - "n_blkt_outboard_modules_toroidal", scan=scan - ) - if n_blkt_outboard_modules_toroidal > 1: - # Calculate the angular spacing for each module - spacing = full_angle / (n_blkt_outboard_modules_toroidal) - r1, _ = cumulative_radial_build2("dr_fw_outboard", mfile, scan) - r2, _ = cumulative_radial_build2("dr_blkt_outboard", mfile, scan) - for i in range(int(n_blkt_outboard_modules_toroidal)): - ang = i * spacing - # Draw a line from r1 to r2 at angle ang - axis.plot( - [r1 * np.cos(ang), r2 * np.cos(ang)], - [r1 * np.sin(ang), r2 * np.sin(ang)], - color="black", - linestyle="-", - linewidth=1.5, - zorder=100, - ) - - # Ranges - # --- - # DEMO : Fixed ranges for comparison - if demo_ranges: - axis.set_ylim(0, 20) - axis.set_xlim(0, 20) - - # Adaptive ranges - else: - axis.set_ylim(0.0, axis.get_ylim()[1]) - axis.set_xlim(0.0, axis.get_xlim()[1]) - # --- - - -def TF_outboard(axis: plt.Axes, item, n_tf_coils, r3, r4, w, facecolor): - """Plot outboard TF coils""" - spacing = 2 * np.pi / n_tf_coils - ang = item * spacing - dx = w * np.sin(ang) - dy = w * np.cos(ang) - x1 = r3 * np.cos(ang) + dx - y1 = r3 * np.sin(ang) - dy - x2 = r4 * np.cos(ang) + dx - y2 = r4 * np.sin(ang) - dy - x3 = r4 * np.cos(ang) - dx - y3 = r4 * np.sin(ang) + dy - x4 = r3 * np.cos(ang) - dx - y4 = r3 * np.sin(ang) + dy - verts = [(x1, y1), (x2, y2), (x3, y3), (x4, y4), (x1, y1)] - path = mplPath(verts, closed=True) - patch = patches.PathPatch(path, facecolor=facecolor, lw=0) - axis.add_patch(patch) - - -def arc(axis: plt.Axes, r, theta1=0, theta2=rtangle, style="solid"): - """Plots an arc. - - Parameters - ---------- - axis : - plot object - r : - radius - theta1 : - starting polar angle (Default value = 0) - theta2 : - finishing polar angle (Default value = rtangle) - axis: plt.Axes : - - style : - (Default value = "solid") - """ - angs = np.linspace(theta1, theta2) - xs = r * np.cos(angs) - ys = r * np.sin(angs) - axis.plot(xs, ys, linestyle=style, color="black", lw=0.2) - - -def arc_fill(axis: plt.Axes, r1, r2, color="pink", theta1=0, theta2=rtangle): - """Fills the space between two quarter circles. - - Parameters - ---------- - axis : - plot object - r1 : - r2 radii to be filled - axis: plt.Axes : - - r2 : - - color : - (Default value = "pink") - """ - angs = np.linspace(theta1, theta2, endpoint=True) - xs1 = r1 * np.cos(angs) - ys1 = r1 * np.sin(angs) - angs = np.linspace(theta2, theta1, endpoint=True) - xs2 = r2 * np.cos(angs) - ys2 = r2 * np.sin(angs) - verts = list(zip(xs1, ys1, strict=False)) - verts.extend(list(zip(xs2, ys2, strict=False))) - path = mplPath(verts, closed=True) - patch = patches.PathPatch(path, facecolor=color, lw=0) - axis.add_patch(patch) - - -def plot_n_profiles(prof, demo_ranges: bool, mfile: MFile, scan: int): - """Function to plot density profile - - Parameters - ---------- - prof : - axis object to add plot to - demo_ranges: bool : - - mfile: MFile : - - scan: int : - - """ - nd_alphas = mfile.get("nd_plasma_alphas_thermal_vol_avg", scan=scan) - nd_protons = mfile.get("nd_plasma_protons_vol_avg", scan=scan) - nd_impurities = mfile.get("nd_plasma_impurities_vol_avg", scan=scan) - nd_ions_total = mfile.get("nd_plasma_ions_total_vol_avg", scan=scan) - nd_fuel_ions = mfile.get("nd_plasma_fuel_ions_vol_avg", scan=scan) - alphan = mfile.get("alphan", scan=scan) - f_nd_plasma_pedestal_greenwald = mfile.get( - "f_nd_plasma_pedestal_greenwald", scan=scan - ) - f_nd_plasma_separatrix_greenwald = mfile.get( - "f_nd_plasma_separatrix_greenwald", scan=scan - ) - nd_plasma_electrons_vol_avg = mfile.get("nd_plasma_electrons_vol_avg", scan=scan) - # find impurity densities - imp_frac = np.array([ - mfile.get(f"f_nd_impurity_electrons({i:02d})", scan=scan) for i in range(1, 15) - ]) - - nd_plasma_separatrix_electron = mfile.get("nd_plasma_separatrix_electron", scan=scan) - - ax_main = prof.add_subplot(631) - ax_main.set_position([0.075, 0.625, 0.25, 0.325]) - ax_impurity = prof.add_subplot(634, sharex=ax_main) - ax_impurity.set_position([0.075, 0.275, 0.25, 0.325]) - ax_main.tick_params(labelbottom=False) - - ax_impurity.set_xlabel(r"$\rho \quad [r/a]$") - ax_main.set_ylabel(r"$n \ [10^{19}\ \mathrm{m}^{-3}]$") - ax_impurity.set_ylabel(r"$n \ [10^{16}\ \mathrm{m}^{-3}]$") - ax_main.set_title("Density profile") - - i_plasma_pedestal = mfile.get("i_plasma_pedestal", scan=scan) - nd_plasma_pedestal_electron = mfile.get("nd_plasma_pedestal_electron", scan=scan) - ne0 = mfile.get("nd_plasma_electron_on_axis", scan=scan) - nd_plasma_electrons_vol_avg = mfile.get("nd_plasma_electrons_vol_avg", scan=scan) - radius_plasma_pedestal_density_norm = mfile.get( - "radius_plasma_pedestal_density_norm", scan=scan - ) - ne0 = mfile.get("nd_plasma_electron_on_axis", scan=scan) - n_plasma_profile_elements = mfile.get("n_plasma_profile_elements", scan=scan) - - # build electron profile and species profiles (scale with electron profile shape) - if i_plasma_pedestal == 1: - rho = np.linspace(0, 1.0, int(n_plasma_profile_elements)) - ne = np.zeros_like(rho) - - for i in range(len(rho)): - if rho[i] <= radius_plasma_pedestal_density_norm: - ne[i] = ( - nd_plasma_pedestal_electron - + (ne0 - nd_plasma_pedestal_electron) - * (1 - rho[i] ** 2 / radius_plasma_pedestal_density_norm**2) - ** alphan - ) - else: - ne[i] = nd_plasma_separatrix_electron + ( - nd_plasma_pedestal_electron - nd_plasma_separatrix_electron - ) * (1 - rho[i]) / (1 - min(0.9999, radius_plasma_pedestal_density_norm)) - else: - rho = np.linspace(0, 1.0, n_plasma_profile_elements) - ne = ne0 * (1 - rho**2) ** alphan - - # species profiles scaled by their average fraction relative to electrons - - if nd_plasma_electrons_vol_avg != 0: - fracs = ( - np.array([ - nd_fuel_ions, - nd_alphas, - nd_protons, - nd_impurities, - nd_ions_total, - nd_plasma_electrons_vol_avg, - ]) - / nd_plasma_electrons_vol_avg - ) - else: - fracs = np.zeros(5) - - # build species density profiles from electron profile and fractions - # fracs = [fuel, alpha, protons, impurities, ions_total] - # Create a density profile for each species by multiplying ne by each fraction in fracs - density_profiles = np.array([ne * frac for frac in fracs]) - - # convert to 1e19 m^-3 units for plotting (vectorised) - density_profiles_plotting = density_profiles / 1e19 - - ax_main.plot( - rho, - density_profiles_plotting[0], - label=r"$n_{\text{fuel}}$", - color="#2ca02c", - linewidth=1.5, - ) - ax_main.plot( - rho, - density_profiles_plotting[1], - label=r"$n_{\alpha,\text{thermal}}$", - color="#d62728", - linewidth=1.5, - ) - ax_impurity.plot( - rho, - density_profiles_plotting[2] * 1e3, - label=r"$n_{p}$", - color="#17becf", - linewidth=1.5, - ) - ax_impurity.plot( - rho, - density_profiles_plotting[3] * 1e3, - label=r"$n_{imp,total}$", - color="#9467bd", - linewidth=2.5, - linestyle="dotted", - ) - ax_main.plot( - rho, - density_profiles_plotting[4], - label=r"$n_{i,total}$", - color="#ff7f0e", - linewidth=1.5, - ) - ax_main.plot( - rho, density_profiles_plotting[5], label=r"$n_{e}$", color="blue", linewidth=1.5 - ) - - if imp_frac[2] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[2] * ne / 1e16, label=r"$n_{\text{Be}}$") - if imp_frac[3] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[3] * ne / 1e16, label=r"$n_{\text{C}}$") - if imp_frac[4] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[4] * ne / 1e16, label=r"$n_{\text{N}}$") - if imp_frac[5] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[5] * ne / 1e16, label=r"$n_{\text{O}}$") - if imp_frac[6] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[6] * ne / 1e16, label=r"$n_{\text{Ne}}$") - if imp_frac[7] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[7] * ne / 1e16, label=r"$n_{\text{Si}}$") - if imp_frac[8] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[8] * ne / 1e16, label=r"$n_{\text{Ar}}$") - if imp_frac[9] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[9] * ne / 1e16, label=r"$n_{\text{Fe}}$") - if imp_frac[10] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[10] * ne / 1e16, label=r"$n_{\text{Ni}}$") - if imp_frac[11] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[11] * ne / 1e16, label=r"$n_{\text{Kr}}$") - if imp_frac[12] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[12] * ne / 1e16, label=r"$n_{\text{Xe}}$") - if imp_frac[13] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[13] * ne / 1e16, label=r"$n_{\text{W}}$") - - ax_main.legend(loc="best") - ax_impurity.legend(loc="best") - - # Ranges - # --- - # DEMO : Fixed ranges for comparison - ax_main.set_xlim(0, 1) - ax_impurity.set_xlim(0, 1) - if demo_ranges: - ax_main.set_ylim(0, 20) - - # Adaptive ranges - else: - ax_main.set_ylim(0, ax_main.get_ylim()[1]) - # Use logarithmic scale for impurity axis if any impurity values are very small - impurity_data = [ - imp_frac[i] * ne / 1e16 - for i in range(len(imp_frac)) - if imp_frac[i] > 1.0e-30 - ] - if impurity_data and np.min(impurity_data) / np.max(impurity_data) < 0.01: - # If range spans more than 100x, use log scale - ax_impurity.set_yscale("log") - ax_impurity.set_ylim(1e-3, ax_impurity.get_ylim()[1]) - - if i_plasma_pedestal != 0: - # Print pedestal lines - ax_main.axhline( - y=nd_plasma_pedestal_electron / 1e19, - xmax=radius_plasma_pedestal_density_norm, - color="r", - linestyle="-", - linewidth=0.4, - alpha=0.4, - ) - ax_main.vlines( - x=radius_plasma_pedestal_density_norm, - ymin=0.0, - ymax=nd_plasma_pedestal_electron / 1e19, - color="r", - linestyle="-", - linewidth=0.4, - alpha=0.4, - ) - ax_main.minorticks_on() - ax_impurity.minorticks_on() - - # Add text box with density profile parameters - textstr_density = "\n".join(( - ( - rf"$\langle n_{{\text{{e}}}} \rangle$: {nd_plasma_electrons_vol_avg:.3e} m$^{{-3}}$" - rf"$\hspace{{4}} \overline{{n_{{e}}}}$: {mfile.get('nd_plasma_electron_line', scan=scan):.3e} m$^{{-3}}$" - ), - ( - rf"$n_{{\text{{e,0}}}}$: {ne0:.3e} m$^{{-3}}$" - rf"$\hspace{{4}} \alpha_{{\text{{n}}}}$: {alphan:.3f}" - ), - ( - rf"$n_{{\text{{e,ped}}}}$: {nd_plasma_pedestal_electron:.3e} m$^{{-3}}$" - r"$ \hspace{3} \frac{\langle n_i \rangle}{\langle n_e \rangle}$: " - f"{nd_fuel_ions / nd_plasma_electrons_vol_avg:.3f}" - ), - ( - rf"$f_{{\text{{GW e,ped}}}}$: {f_nd_plasma_pedestal_greenwald:.3f}" - r"$ \hspace{7} \frac{n_{e,0}}{\langle n_e \rangle}$: " - f"{ne0 / nd_plasma_electrons_vol_avg:.3f}" - ), - ( - rf"$\rho_{{\text{{ped,n}}}}$: {radius_plasma_pedestal_density_norm:.3f}" - r"$ \hspace{8} \frac{\overline{n_{e}}}{n_{\text{GW}}}$: " - f"{mfile.get('nd_plasma_electron_line', scan=scan) / mfile.get('nd_plasma_electron_max_array(7)', scan=scan):.3f}" - ), - rf"$n_{{\text{{e,sep}}}}$: {nd_plasma_separatrix_electron:.3e} m$^{{-3}}$", - rf"$f_{{\text{{GW e,sep}}}}$: {f_nd_plasma_separatrix_greenwald:.3f}", - )) - - props_density = {"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5} - ax_main.text( - -0.05, - -0.175, - textstr_density, - transform=ax_impurity.transAxes, - fontsize=9, - verticalalignment="top", - bbox=props_density, - ) - - textstr_ions = "\n".join(( - ( - r"$\langle n_{\text{ions-total}} \rangle $: " - f"{mfile.get('nd_plasma_ions_total_vol_avg', scan=scan):.3e} m$^{{-3}}$" - ), - ( - r"$\langle n_{\text{fuel}} \rangle $: " - f"{mfile.get('nd_plasma_fuel_ions_vol_avg', scan=scan):.3e} m$^{{-3}}$" - ), - ( - r"$\langle n_{\alpha,\text{thermal}} \rangle $: " - f"{mfile.get('nd_plasma_alphas_thermal_vol_avg', scan=scan):.3e} m$^{{-3}}$" - ), - ( - r"$\langle n_{\text{impurities}} \rangle $: " - f"{mfile.get('nd_plasma_impurities_vol_avg', scan=scan):.3e} m$^{{-3}}$" - ), - ( - r"$\langle n_{\text{protons}} \rangle $:" - f"{mfile.get('nd_plasma_protons_vol_avg', scan=scan):.3e} m$^{{-3}}$" - ), - )) - - ax_impurity.text( - 1.2, - 0.05, - textstr_ions, - fontsize=9, - verticalalignment="bottom", - horizontalalignment="left", - transform=ax_impurity.transAxes, - bbox={ - "boxstyle": "round", - "facecolor": "wheat", - "alpha": 0.5, - }, - ) - - ax_main.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.2) - ax_impurity.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.2) - # --- - - -def plot_jprofile(prof, mfile: MFile, scan: int): - """Function to plot density profile - - Parameters - ---------- - prof : - axis object to add plot to - mfile: MFile : - - scan: int : - - """ - alphaj = mfile.get("alphaj", scan=scan) - j_plasma_0 = mfile.get("j_plasma_on_axis", scan=scan) - n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) - - j_plasma_bootstrap_sauter_profile = [ - mfile.get(f"j_plasma_bootstrap_sauter_profile{i}", scan=scan) / 1000.0 - for i in range(n_plasma_profile_elements - 3) - ] - - prof.set_xlabel(r"$\rho \quad [r/a]$") - prof.set_ylabel(r"Current density $[kA/m^2]$") - prof.set_title("$J$ profile") - prof.minorticks_on() - prof.set_xlim(0, 1.0) - - rho = np.linspace(0, 1) - y2 = (j_plasma_0 * (1 - rho**2) ** alphaj) / 1e3 - - prof.plot(rho, y2, color="red") - - prof.plot( - np.linspace(0, 1, n_plasma_profile_elements - 3), - j_plasma_bootstrap_sauter_profile, - label="Sauter Bootstrap", - color="green", - linestyle="--", - ) - prof.legend() - - textstr_j = "\n".join(( - r"$j_0$: " + f"{y2[0]:.3f} kA m$^{{-2}}$\n", - r"$\alpha_J$: " + f"{alphaj:.3f}", - )) - - props_j = {"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5} - prof.text( - 0.65, - 1.6, - textstr_j, - transform=prof.transAxes, - fontsize=9, - verticalalignment="top", - bbox=props_j, - ) - - prof.text( - 0.35, - 0.04, - "*Current profile is assumed to be parabolic", - fontsize=10, - ha="left", - transform=plt.gcf().transFigure, - ) - prof.text( - 0.35, - 0.02, - "*Bootstrap profile is for representation only", - fontsize=10, - ha="left", - transform=plt.gcf().transFigure, - ) - prof.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.2) - - -def plot_t_profiles(prof, demo_ranges: bool, mfile: MFile, scan: int): - """Function to plot temperature profile - - Parameters - ---------- - prof : - axis object to add plot to - demo_ranges: bool : - - mfile: MFile : - - scan: int : - - """ - prof.set_xlabel(r"$\rho \quad [r/a]$") - prof.set_ylabel("$T$ [keV]") - prof.set_title("Temperature profile") - - alphat = mfile.get("alphat", scan=scan) - radius_plasma_pedestal_temp_norm = mfile.get( - "radius_plasma_pedestal_temp_norm", scan=scan - ) - - n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) - i_plasma_pedestal = mfile.get("i_plasma_pedestal", scan=scan) - rho = np.linspace(0, 1.0, n_plasma_profile_elements) - temp_plasma_pedestal_electron_kev = mfile.get( - "temp_plasma_pedestal_electron_kev", scan=scan - ) - temp_plasma_separatrix_electron_kev = mfile.get( - "temp_plasma_separatrix_electron_kev", scan=scan - ) - f_temp_plasma_ion_electron = mfile.get("f_temp_plasma_ion_electron", scan=scan) - tbeta = mfile.get("tbeta", scan=scan) - te0 = mfile.get("temp_plasma_electron_on_axis_kev", scan=scan) - - if i_plasma_pedestal == 1: - rhocore = np.linspace(0.0, radius_plasma_pedestal_temp_norm) - tcore = ( - temp_plasma_pedestal_electron_kev - + (te0 - temp_plasma_pedestal_electron_kev) - * (1 - (rhocore / radius_plasma_pedestal_temp_norm) ** tbeta) ** alphat - ) - - rhosep = np.linspace(radius_plasma_pedestal_temp_norm, 1) - tsep = temp_plasma_separatrix_electron_kev + ( - temp_plasma_pedestal_electron_kev - temp_plasma_separatrix_electron_kev - ) * (1 - rhosep) / (1 - min(0.9999, radius_plasma_pedestal_temp_norm)) - - rho = np.append(rhocore, rhosep) - te = np.append(tcore, tsep) - else: - rho1 = np.linspace(0, 0.95) - rho2 = np.linspace(0.95, 1) - rho = np.append(rho1, rho2) - te = te0 * (1 - rho**2) ** alphat - prof.plot(rho, te, color="blue", label="$T_{e}$") - prof.plot(rho, te[:] * f_temp_plasma_ion_electron, color="red", label="$T_{i}$") - prof.legend() - - # Ranges - # --- - prof.set_xlim(0, 1) - # DEMO : Fixed ranges for comparison - if demo_ranges: - prof.set_ylim(0, 50) - - # Adaptive ranges - else: - prof.set_ylim(0, prof.get_ylim()[1]) - - if i_plasma_pedestal != 0: - # Plot pedestal lines - prof.axhline( - y=temp_plasma_pedestal_electron_kev, - xmax=radius_plasma_pedestal_temp_norm, - color="r", - linestyle="-", - linewidth=0.4, - alpha=0.4, - ) - prof.vlines( - x=radius_plasma_pedestal_temp_norm, - ymin=0.0, - ymax=temp_plasma_pedestal_electron_kev, - color="r", - linestyle="-", - linewidth=0.4, - alpha=0.4, - ) - prof.minorticks_on() - - te = mfile.get("temp_plasma_electron_vol_avg_kev", scan=scan) - # Add text box with temperature profile parameters - textstr_temperature = "\n".join(( - ( - rf"$\langle T_{{\text{{e}}}} \rangle_\text{{V}}$: {mfile.get('temp_plasma_electron_vol_avg_kev', scan=scan):.3f} keV" - rf"$\hspace{{2}} \langle T_{{\text{{e}}}} \rangle_\text{{n}}$: {mfile.get('temp_plasma_electron_density_weighted_kev', scan=scan):.3f} keV" - rf"$\hspace{{2}} \overline{{T_{{e}}}}$: {mfile.get('temp_plasma_electron_line_avg_kev', scan=scan):.3f} keV" - ), - ( - rf"$T_{{\text{{e,0}}}}$: {te0:.3f} keV" - rf"$\hspace{{3}} \alpha_{{\text{{T}}}}$: {alphat:.3f} " - rf"$\hspace{{3}} \langle T_{{\text{{i}}}} \rangle_\text{{V}}$: {mfile.get('temp_plasma_ion_vol_avg_kev', scan=scan):.3f} keV" - ), - ( - rf"$T_{{\text{{e,ped}}}}$: {temp_plasma_pedestal_electron_kev:.3f} keV" - r"$ \hspace{3} \frac{\langle T_i \rangle}{\langle T_e \rangle}$: " - f"{f_temp_plasma_ion_electron:.3f} " - f"$\\hspace{{4}} T_{{\\text{{i,0}}}}$: {mfile.get('temp_plasma_ion_on_axis_kev', scan=scan):.3f} keV" - ), - ( - rf"$\rho_{{\text{{ped,T}}}}$: {radius_plasma_pedestal_temp_norm:.3f}" - r"$ \hspace{5} \frac{T_{e,0}}{\langle T_e \rangle}$: " - f"{mfile.get('f_temp_plasma_electron_on_axis_vol_avg', scan=scan):.3f} " - f"$\\hspace{{4}} T_{{\\text{{i,ped}}}}$: {mfile.get('temp_plasma_pedestal_ion_kev', scan=scan):.3f} keV" - ), - ( - rf"$T_{{\text{{e,sep}}}}$: {temp_plasma_separatrix_electron_kev:.3f} keV" - r"$\hspace{3} \frac{{{\langle T_e \rangle_n}}}{{{\langle T_e \rangle_V}}}$: " - f"{mfile.get('f_temp_plasma_electron_density_vol_avg', scan=scan):.3f}" - f"$\\hspace{{4}} T_{{\\text{{i,sep}}}}$: {mfile.get('temp_plasma_separatrix_ion_kev', scan=scan):.3f} keV" - ), - )) - - props_temperature = {"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5} - prof.text( - -0.1, - -0.125, - textstr_temperature, - transform=prof.transAxes, - fontsize=9, - verticalalignment="top", - bbox=props_temperature, - ) - prof.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.2) - # --- - - -def plot_qprofile(prof, demo_ranges: bool, mfile: MFile, scan: int): - """Function to plot q profile, formula taken from Nevins bootstrap model. - - Parameters - ---------- - prof : - axis object to add plot to - demo_ranges: bool : - - mfile: MFile : - - scan: int : - - """ - prof.set_xlabel(r"$\rho \quad [r/a]$") - prof.set_ylabel("$q$") - prof.set_title("$q$ profile") - prof.minorticks_on() - - rho = np.linspace(0, 1) - q0 = mfile.get("q0", scan=scan) - q95 = mfile.get("q95", scan=scan) - - q_r_nevin = q0 + (q95 - q0) * (rho + rho * rho + rho**3) / (3.0) - q_r_sauter = q0 + (q95 - q0) * (rho * rho) - - prof.plot(rho, q_r_nevin, label="Nevins") - prof.plot(rho, q_r_sauter, label="Sauter") - prof.legend() - - # Ranges - # --- - prof.set_xlim(0, 1) - # DEMO : Fixed ranges for comparison - if demo_ranges: - prof.set_ylim(0, 10) - - # Adaptive ranges - else: - prof.set_ylim(0, q95 * 1.2) - - prof.text( - 0.6, - 0.04, - "*Profile is not calculated, only $q_0$ and $q_{95}$ are known.", - fontsize=10, - ha="left", - transform=plt.gcf().transFigure, - ) - prof.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.2) - # --- - - textstr_q = " | ".join(( - r"$q_0$: " + f"{q0:.3f}", - r"$q_{95}$: " + f"{q95:.3f}", - r"$q_{\text{cyl}}$: " + f"{mfile.get('qstar', scan=scan):.3f}", - )) - - props_q = {"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5} - prof.text( - 0.0, - 1.4, - textstr_q, - transform=prof.transAxes, - fontsize=9, - verticalalignment="top", - bbox=props_q, - ) - - -def read_imprad_data(_skiprows, data_path): - """Function to read all data needed for creation of radiation profile - - Parameters - ---------- - _skiprows : - number of rows to skip when reading impurity data files - data_path : - path to impurity data - - """ - lzdata = [0.0 for x in range(len(ImpurityRadiationData().imp_label))] - - for i in range(len(ImpurityRadiationData().imp_label)): - file_iden = data_path + ImpurityRadiationData().imp_label[i].ljust(3, "_") - - Te = None - lz = None - zav = None - - for header in read_impurity_file(file_iden + "lz_tau.dat"): - if "Te[eV]" in header.content: - Te = np.asarray(header.data, dtype=float) - - if "infinite confinement" in header.content: - lz = np.asarray(header.data, dtype=float) - for header in read_impurity_file(file_iden + "z_tau.dat"): - if "infinite confinement" in header.content: - zav = np.asarray(header.data, dtype=float) - - lzdata[i] = np.column_stack([Te, lz, zav]) - - # then switch string to floats - return np.array(lzdata, dtype=float) - - -def profiles_with_pedestal(mfile, scan: int): - """Calculate profiles with pedestal""" - alphan = mfile.get("alphan", scan=scan) - alphat = mfile.get("alphat", scan=scan) - nd_plasma_electron_on_axis = mfile.get("nd_plasma_electron_on_axis", scan=scan) - temp_plasma_electron_on_axis_kev = mfile.get( - "temp_plasma_electron_on_axis_kev", scan=scan - ) - - radius_plasma_pedestal_temp_norm = mfile.get( - "radius_plasma_pedestal_temp_norm", scan=scan - ) - - n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) - i_plasma_pedestal = mfile.get("i_plasma_pedestal", scan=scan) - nd_plasma_pedestal_electron = mfile.get("nd_plasma_pedestal_electron", scan=scan) - radius_plasma_pedestal_density_norm = mfile.get( - "radius_plasma_pedestal_density_norm", scan=scan - ) - ne0 = mfile.get("nd_plasma_electron_on_axis", scan=scan) - rho = np.linspace(0, 1.0, n_plasma_profile_elements) - nd_plasma_separatrix_electron = mfile.get("nd_plasma_separatrix_electron", scan=scan) - temp_plasma_pedestal_electron_kev = mfile.get( - "temp_plasma_pedestal_electron_kev", scan=scan - ) - temp_plasma_separatrix_electron_kev = mfile.get( - "temp_plasma_separatrix_electron_kev", scan=scan - ) - tbeta = mfile.get("tbeta", scan=scan) - te0 = mfile.get("temp_plasma_electron_on_axis_kev", scan=scan) - - if i_plasma_pedestal == 0: - # Initialise the radius - - # The density profile - ne = nd_plasma_electron_on_axis * (1 - rho**2) ** alphan - - # The temperature profile - te = temp_plasma_electron_on_axis_kev * (1 - rho**2) ** alphat - - # Profiles with pedestal - elif i_plasma_pedestal == 1: - # The density and temperature profile - # Initiliase empty normalised array with zeros - ne = np.zeros_like(rho) - te = np.zeros_like(rho) - # Reconstruct the temperature and density profiles with pedestal - for q in range(rho.shape[0]): - # Core density region - if rho[q] <= radius_plasma_pedestal_density_norm: - ne[q] = ( - nd_plasma_pedestal_electron - + (ne0 - nd_plasma_pedestal_electron) - * (1 - rho[q] ** 2 / radius_plasma_pedestal_density_norm**2) - ** alphan - ) - else: - # Pedestal density region - ne[q] = nd_plasma_separatrix_electron + ( - nd_plasma_pedestal_electron - nd_plasma_separatrix_electron - ) * (1 - rho[q]) / (1 - radius_plasma_pedestal_density_norm) - - # Core temperature region - if rho[q] <= radius_plasma_pedestal_temp_norm: - te[q] = ( - temp_plasma_pedestal_electron_kev - + (te0 - temp_plasma_pedestal_electron_kev) - * (1 - (rho[q] / radius_plasma_pedestal_temp_norm) ** tbeta) - ** alphat - ) - else: - # Pedestal temperature region - te[q] = temp_plasma_separatrix_electron_kev + ( - temp_plasma_pedestal_electron_kev - - temp_plasma_separatrix_electron_kev - ) * (1 - rho[q]) / (1 - radius_plasma_pedestal_temp_norm) - - return rho, ne, te - - -def plot_line_brem_power_density_profile( - axis: plt.Axes, mfile: MFile, scan: int, impp: str, demo_ranges: bool -): - """Function to plot Line and Bremsstrahlung radiation power density [MW/m³] profile. - - Parameters - ---------- - axis : plt.Axes - axis object to add plot to - mfile : MFile - MFile object containing plasma and impurity profile information. - scan : int - scan number to use - impp : str - impurity path - demo_ranges : bool - whether to use fixed demo ranges for the plot - - """ - axis.set_xlabel(r"$\rho \quad [r/a]$") - axis.set_ylabel(r"$P_{\mathrm{rad}}$ $[\mathrm{MW.m}^{-3}]$") - axis.set_title("Raw Data: Line & Bremsstrahlung Radiation Density Profile") - - # read in the impurity data - imp_data = read_imprad_data(_skiprows=2, data_path=impp) - - # find impurity densities - imp_frac = np.array([ - mfile.get(f"f_nd_impurity_electrons({i:02d})", scan=scan) for i in range(1, 15) - ]) - - rho, nd_electron, temp_electron_kev = profiles_with_pedestal(mfile=mfile, scan=scan) - # imp_data Te values are in eV, but te from profiles_with_pedestal is in keV - temp_electron_ev = temp_electron_kev * 1.0e3 - - # Intailise the radiation profile arrays - pden_rad_array = np.zeros([imp_data.shape[0], temp_electron_kev.shape[0]]) - lz = np.zeros([imp_data.shape[0], temp_electron_kev.shape[0]]) - pden_total_profile = np.zeros(temp_electron_kev.shape[0]) - - # Intailise the impurity radiation profile - for temp_point in range(temp_electron_kev.shape[0]): - for impurity in range(imp_data.shape[0]): - if temp_electron_ev[temp_point] <= imp_data[impurity][0][0]: - lz[impurity][temp_point] = imp_data[impurity][0][1] - elif ( - temp_electron_ev[temp_point] - >= imp_data[impurity][imp_data.shape[1] - 1][0] - ): - lz[impurity][temp_point] = imp_data[impurity][imp_data.shape[1] - 1][1] - else: - # Use np.interp for log-log interpolation - log_te_data = np.log([row[0] for row in imp_data[impurity]]) - log_lz_data = np.log([row[1] for row in imp_data[impurity]]) - lz[impurity][temp_point] = np.exp( - np.interp( - np.log(temp_electron_ev[temp_point]), log_te_data, log_lz_data - ) - ) - pden_rad_array[impurity][temp_point] = ( - imp_frac[impurity] - * nd_electron[temp_point] - * nd_electron[temp_point] - * lz[impurity][temp_point] - ) - - for l_ in range(imp_data.shape[0]): - pden_total_profile[temp_point] += pden_rad_array[l_][temp_point] * 1.0e-6 - - # Plot the total radiation profile and individual impurity contributions - axis.plot(rho, pden_total_profile, label="Total", linestyle="dotted") - axis.plot(rho, pden_rad_array[0] * 1.0e-6, label="H") - axis.plot(rho, pden_rad_array[1] * 1.0e-6, label="He") - - # Plot the remaining impurity contributions if their fraction is significant - for ind in range(2, imp_data.shape[0]): - if imp_frac[ind] > 1.0e-30: - axis.plot( - rho, - pden_rad_array[ind] * 1.0e-6, - label=ImpurityRadiationData().imp_label[ind].replace("_", ""), - ) - - axis.minorticks_on() - # Plot a vertical line at the core region radius - core_radius = mfile.get("radius_plasma_core_norm", scan=scan) - - # Plot a vertical line at the core region radius - axis.axvline(x=core_radius, color="black", linestyle="--", linewidth=1.0, alpha=0.7) - # Plot a box in the bottom left with f_{core,reduce} and \rho_{core} - props_core_reduce = {"boxstyle": "round", "facecolor": "khaki", "alpha": 0.8} - axis.text( - 0.02, - 0.02, - rf"$f_{{\text{{core,reduce}}}}$ = {1.0}" - "\n" - rf"$\rho_{{\text{{core}}}}$ = {core_radius:.3f}", - transform=axis.transAxes, - fontsize=8, - verticalalignment="bottom", - bbox=props_core_reduce, - ) - - # Ranges - # --- - axis.legend(loc="upper left", bbox_to_anchor=(-0.1, -0.1), ncol=4) - axis.set_xlim(0, 1.0) - axis.set_yscale("log") - axis.yaxis.grid(True, which="both", alpha=0.2) - # DEMO : Fixed ranges for comparison - if demo_ranges: - axis.set_ylim(1e-4, 0.5) - - # Adaptive ranges - else: - axis.set_ylim(1e-4, axis.get_ylim()[1]) - # --- - - -def plot_line_brem_power_profile( - axis: plt.Axes, - mfile: MFile, - scan: int, - impp: str, -): - """Function to plot Line and Bremsstrahlung radiation power [MW] profile. - - Parameters - ---------- - axis : plt.Axes - axis object to add plot to - mfile : MFile - MFile object containing plasma and impurity profile information. - scan : int - scan number to use - impp : str - impurity path - - """ - axis.set_xlabel(r"$\rho \quad [r/a]$") - axis.set_ylabel(r"$P_{\mathrm{rad}}$ $[\mathrm{MW}]$") - axis.set_title("Line & Bremsstrahlung Radiation Power Profile") - - # read in the impurity data - imp_data = read_imprad_data(_skiprows=2, data_path=impp) - - # find impurity densities - imp_frac = np.array([ - mfile.get(f"f_nd_impurity_electrons({i:02d})", scan=scan) for i in range(1, 15) - ]) - vol_plasma = mfile.get("vol_plasma", scan=scan) - p_plasma_rad_imps_mw = mfile.get("p_plasma_rad_mw", scan=scan) - mfile.get( - "p_plasma_sync_mw", scan=scan - ) - - rho, nd_electron, temp_electron_kev = profiles_with_pedestal(mfile=mfile, scan=scan) - # imp_data Te values are in eV, but te from profiles_with_pedestal is in keV - temp_electron_ev = temp_electron_kev * 1.0e3 - - # Intailise the radiation profile arrays - p_rad_array = np.zeros([imp_data.shape[0], temp_electron_kev.shape[0]]) - lz = np.zeros([imp_data.shape[0], temp_electron_kev.shape[0]]) - p_total_profile = np.zeros(temp_electron_kev.shape[0]) - - # Intailise the impurity radiation profile - for temp_point in range(temp_electron_kev.shape[0]): - for impurity in range(imp_data.shape[0]): - if temp_electron_ev[temp_point] <= imp_data[impurity][0][0]: - lz[impurity][temp_point] = imp_data[impurity][0][1] - elif ( - temp_electron_ev[temp_point] - >= imp_data[impurity][imp_data.shape[1] - 1][0] - ): - lz[impurity][temp_point] = imp_data[impurity][imp_data.shape[1] - 1][1] - else: - # Use np.interp for log-log interpolation - log_te_data = np.log([row[0] for row in imp_data[impurity]]) - log_lz_data = np.log([row[1] for row in imp_data[impurity]]) - lz[impurity][temp_point] = np.exp( - np.interp( - np.log(temp_electron_ev[temp_point]), log_te_data, log_lz_data - ) - ) - - # Calculate the absolue radiation power for each volumetric shell for each - # impurity - p_rad_array[impurity] = calculate_profile_shell_contributions( - profile_x=rho, - profile_y=( - imp_frac[impurity] * nd_electron * nd_electron * lz[impurity] - ), - vol_plasma=vol_plasma, - profile_dx=rho[1] - rho[0], - ) - - for l_ in range(imp_data.shape[0]): - p_total_profile[temp_point] += p_rad_array[l_][temp_point] * 1.0e-6 - - axis.plot( - rho, p_total_profile, label="$P_{\\Sigma\\text{Impurities}}$", linestyle="dotted" - ) - - # Plot individual impurity radiation profiles - axis.plot(rho, p_rad_array[0] * 1.0e-6, label="H") - axis.plot(rho, p_rad_array[1] * 1.0e-6, label="He") - # Only plot impurities with a significant fraction - for ind in range(2, imp_data.shape[0]): - if imp_frac[ind] > 1.0e-30: - axis.plot( - rho, - p_rad_array[ind] * 1.0e-6, - label=ImpurityRadiationData().imp_label[ind].replace("_", ""), - ) - - axis.plot( - rho, - np.cumsum(p_total_profile), - label="$\\Sigma P_{\\Sigma\\text{Impurities}}$", - color="black", - ) - axis.axhline( - y=p_plasma_rad_imps_mw, - color="black", - linestyle="--", - label="$P_{\\text{total}}$", - ) - - axis.minorticks_on() - # Plot a vertical line at the core region radius - core_radius = mfile.get("radius_plasma_core_norm", scan=scan) - - # Plot a vertical line at the core region radius - axis.axvline(x=core_radius, color="black", linestyle="--", linewidth=1.0, alpha=0.7) - # Plot a box in the bottom left with f_{core,reduce} and \rho_{core} - props_core_reduce = {"boxstyle": "round", "facecolor": "khaki", "alpha": 0.8} - axis.text( - 0.05, - 0.02, - rf"$f_{{\text{{core,reduce}}}}$ = {1.0}" - "\n" - rf"$\rho_{{\text{{core}}}}$ = {core_radius:.3f}", - transform=axis.transAxes, - fontsize=8, - verticalalignment="bottom", - bbox=props_core_reduce, - ) - - cumulative_thermal_energy_mj = np.cumsum(p_total_profile) - half_thermal_energy_mj = 0.5 * p_plasma_rad_imps_mw - half_thermal_energy_position = np.interp( - half_thermal_energy_mj, - cumulative_thermal_energy_mj, - np.linspace(0, 1, temp_electron_kev.shape[0]), - ) - axis.axhline( - y=half_thermal_energy_mj, - label="$50\\%\\ P_{\\text{total}}$", - color="tab:green", - linestyle=":", - ) - axis.axvline( - x=half_thermal_energy_position, - color="tab:green", - linestyle=":", - ) - - # Ranges - # --- - axis.set_xlim([0, 1.0]) - axis.legend(loc="upper left", bbox_to_anchor=(1.0, 1.0), ncol=1) - axis.set_yscale("log") - axis.yaxis.grid(True, which="both", alpha=0.2) - - -def plot_line_brem_loss_function_profile( - axis: plt.Axes, - mfile: MFile, - scan: int, - impp: str, -): - """Function to plot Line and Bremsstrahlung loss function (L_z) profile. - - Parameters - ---------- - axis : plt.Axes - axis object to add plot to - mfile : MFile - MFile object containing plasma and impurity profile information. - scan : int - scan number to use - impp : str - impurity path - - """ - # read in the impurity data - imp_data = read_imprad_data(_skiprows=2, data_path=impp) - - # find impurity densities - imp_frac = np.array([ - mfile.get(f"f_nd_impurity_electrons({i:02d})", scan=scan) for i in range(1, 15) - ]) - - rho, _, temp_electron_kev = profiles_with_pedestal(mfile, scan) - # imp_data Te values are in eV, but te from profiles_with_pedestal is in keV - temp_electron_ev = temp_electron_kev * 1.0e3 - - # Intailise the radiation profile arrays - lz = np.zeros([imp_data.shape[0], temp_electron_kev.shape[0]]) - - # Intailise the impurity radiation profile - for temp_point in range(temp_electron_kev.shape[0]): - for impurity in range(imp_data.shape[0]): - if temp_electron_ev[temp_point] <= imp_data[impurity][0][0]: - lz[impurity][temp_point] = imp_data[impurity][0][1] - elif ( - temp_electron_ev[temp_point] - >= imp_data[impurity][imp_data.shape[1] - 1][0] - ): - lz[impurity][temp_point] = imp_data[impurity][imp_data.shape[1] - 1][1] - else: - # Use np.interp for log-log interpolation - log_te_data = np.log([row[0] for row in imp_data[impurity]]) - log_lz_data = np.log([row[1] for row in imp_data[impurity]]) - lz[impurity][temp_point] = np.exp( - np.interp( - np.log(temp_electron_ev[temp_point]), log_te_data, log_lz_data - ) - ) - - # Plot the radiation loss function profiles - axis.plot(rho, lz[0], label="H") - axis.plot(rho, lz[1], label="He") - # Plot the remaining impurities if their fraction is significant - impurity_data = ImpurityRadiationData() - for ind in range(2, imp_data.shape[0]): - if imp_frac[ind] > 1.0e-30: - axis.plot( - rho, - lz[ind], - label=impurity_data.imp_label[ind].replace("_", ""), - ) - - axis.legend(loc="best", ncol=4) - axis.minorticks_on() - axis.set_xlabel(r"$\rho \quad [r/a]$") - axis.set_ylabel(r"$L_z$ $[\mathrm{W}\mathrm{m}^3]$") - axis.set_title("Line & Bremsstrahlung Loss Function ($L_z$) Profiles") - axis.set_xlim(0, 1.0) - axis.set_yscale("log") - axis.yaxis.grid(True, which="both", alpha=0.2) - - -def plot_rad_density_contour(axis: Axes, mfile: MFile, scan: int, impp: str): - """Plots the contour of line and bremsstrahlung radiation density [MW/m³] for a - plasma cross-section. - - This function reads impurity and plasma profile data, computes the radiation - density profile, interpolates it onto a 2D grid, and plots the upper and lower - half contours on the provided axis. - - Parameters - ---------- - axis : matplotlib.axes.Axes - The matplotlib axis object to plot the contours on. - mfile : Any - Data object containing plasma and impurity profile information. - scan : int - The scan index to extract profile data for plotting. - impp : str - The impurity data path - - Notes - ----- - - The function assumes the existence of several global or previously defined - variables and functions, such as `read_imprad_data`, `interp1d_profile`, and plasma - pedestal parameters. - - The plotted contours represent the radiation density in units of [MW/m³] - - The function adds colorbar, axis labels, title, and core reduction annotation to - the plot. - """ - rminor = mfile.get("rminor", scan=scan) - rmajor = mfile.get("rmajor", scan=scan) - # Read in the impurity data - imp_data = read_imprad_data(2, impp) - # imp data is a 3D array with shape (num_impurities, num_temp_points, (temp, lz, zav)) - - # Find the relative number density of each impurity - imp_frac = np.array([ - mfile.get(f"f_nd_impurity_electrons({i:02d})", scan=scan) for i in range(1, 15) - ]) - - # Initialize the radius - rho, ne, te = profiles_with_pedestal(mfile, scan) - # imp_data Te values are in eV, but te from profiles_with_pedestal is in keV - te_ev = te * 1.0e3 - - # Intailise the radiation profile arrays - pimpden = np.zeros([imp_data.shape[0], te.shape[0]]) - lz = np.zeros([imp_data.shape[0], te.shape[0]]) - prad = np.zeros(te.shape[0]) - - # Intailise the impurity radiation profile - for rho in range(te.shape[0]): - # imp data is a 3D array with shape (num_impurities, num_temp_points, (temp, lz, zav)) - for impurity in range(imp_data.shape[0]): - # Check if profile temperature is lower than dataset minimum. - # If so, use the minimum loss function value - if te_ev[rho] <= imp_data[impurity][0][0]: - lz[impurity][rho] = imp_data[impurity][0][1] - - # Check if profile temperature is higher than dataset maximum. - # If so, use the maximum loss function value - elif te_ev[rho] >= imp_data[impurity][imp_data.shape[1] - 1][0]: - lz[impurity][rho] = imp_data[impurity][imp_data.shape[1] - 1][1] - else: - # If profile valie is within dataset range, use log-log interpolation to find value for loss function - log_te_data = np.log([row[0] for row in imp_data[impurity]]) - log_lz_data = np.log([row[1] for row in imp_data[impurity]]) - lz[impurity][rho] = np.exp( - np.interp(np.log(te_ev[rho]), log_te_data, log_lz_data) - ) - # Find the power density for each impurity at each rho - pimpden[impurity][rho] = ( - imp_frac[impurity] * ne[rho] * ne[rho] * lz[impurity][rho] - ) - - for impurity in range(imp_data.shape[0]): - prad[rho] += pimpden[impurity][rho] * 1.0e-6 - - p_rad_grid, r_grid, z_grid = interp1d_profile(prad, mfile, scan) - - # Plot the upper half contour - p_rad_upper = axis.contourf( - r_grid, z_grid, p_rad_grid, levels=25, cmap="turbo", zorder=2 - ) - # Plot the lower half contour (mirror) - axis.contourf(r_grid, -z_grid, p_rad_grid, levels=25, cmap="turbo", zorder=2) - - axis.figure.colorbar( - p_rad_upper, - ax=axis, - label=r"$P_{\mathrm{rad}}$ $[\mathrm{MW.m}^{-3}]$", - location="left", - anchor=(-0.25, 0.5), - ) - - axis.set_xlabel("R [m]") - axis.set_xlim(rmajor - 1.2 * rminor, rmajor + 1.2 * rminor) - axis.set_ylim( - -1.2 * rminor * mfile.get("kappa", scan=scan), - 1.2 * mfile.get("kappa", scan=scan) * rminor, - ) - axis.set_ylabel("Z [m]") - axis.set_title("Line & Bremsstrahlung Radiation Density Contours") - axis.plot( - rmajor, - 0, - marker="o", - color="red", - markersize=6, - markeredgecolor="black", - zorder=100, - ) - # enable minor ticks and grid for clearer reading - axis.minorticks_on() - axis.grid(True, which="major", linestyle="--", linewidth=0.8, alpha=0.7, zorder=1) - - axis.grid(True, which="minor", linestyle=":", linewidth=0.4, alpha=0.5, zorder=1) - props_core_reduce = {"boxstyle": "round", "facecolor": "khaki", "alpha": 0.8} - axis.text( - 0.02, - 0.02, - rf"$f_{{\text{{core,reduce}}}}$ = {1.0}", - transform=axis.transAxes, - fontsize=8, - verticalalignment="bottom", - bbox=props_core_reduce, - ) - # make minor ticks visible on all sides and draw ticks inward for compact look - axis.tick_params(which="both", direction="in", top=True, right=True) - - -def plot_vacuum_vessel_and_divertor( - axis, - mfile: MFile, - scan, - radial_build, - colour_scheme, - mirror_negative_x: bool = False, -): - """Function to plot vacuum vessel and divertor boxes - - Parameters - ---------- - axis : - axis object to plot to - mfile : - MFILE data object - scan : - scan number to use - radial_build : - - colour_scheme : - colour scheme to use for plots - mirror_negative_x : - if True, mirror the plot to the negative x-axis (Default value = False) - """ - cumulative_upper = radial_build.cumulative_upper - cumulative_lower = radial_build.cumulative_lower - upper = radial_build.upper - lower = radial_build.lower - - i_single_null = int(mfile.get("i_single_null", scan=scan)) - triang_95 = mfile.get("triang95", scan=scan) - dz_divertor = mfile.get("dz_divertor", scan=scan) - dz_xpoint_divertor = mfile.get("dz_xpoint_divertor", scan=scan) - kappa = mfile.get("kappa", scan=scan) - rminor = mfile.get("rminor", scan=scan) - dr_vv_inboard = mfile.get("dr_vv_inboard", scan=scan) - dr_vv_outboard = mfile.get("dr_vv_outboard", scan=scan) - dr_shld_inboard = mfile.get("dr_shld_inboard", scan=scan) - dr_shld_outboard = mfile.get("dr_shld_outboard", scan=scan) - dr_blkt_inboard = mfile.get("dr_blkt_inboard", scan=scan) - dr_blkt_outboard = mfile.get("dr_blkt_outboard", scan=scan) - - # Outer side (furthest from plasma) - radx_outer = ( - cumulative_radial_build("dr_vv_outboard", mfile, scan) - + cumulative_radial_build("dr_shld_vv_gap_inboard", mfile, scan) - ) / 2.0 - rminx_outer = ( - cumulative_radial_build("dr_vv_outboard", mfile, scan) - - cumulative_radial_build("dr_shld_vv_gap_inboard", mfile, scan) - ) / 2.0 - - # Inner side (nearest to the plasma) - radx_inner = ( - cumulative_radial_build("dr_shld_outboard", mfile, scan) - + cumulative_radial_build("dr_vv_inboard", mfile, scan) - ) / 2.0 - rminx_inner = ( - cumulative_radial_build("dr_shld_outboard", mfile, scan) - - cumulative_radial_build("dr_vv_inboard", mfile, scan) - ) / 2.0 - - z_divertor_lower_top = (-kappa * rminor) - dz_xpoint_divertor - z_divertor_lower_bottom = z_divertor_lower_top - dz_divertor - - # Apply mirror transformation if requested - x_scale = -1 if mirror_negative_x else 1 - - match DivertorNumberModels(i_single_null): - case DivertorNumberModels.SINGLE_NULL: - z_divertor_upper_bottom = None - z_divertor_upper_top = None - vvg_single_null = vacuum_vessel_geometry_single_null( - cumulative_upper=cumulative_upper, - upper=upper, - triang=triang_95, - radx_outer=radx_outer, - rminx_outer=rminx_outer, - radx_inner=radx_inner, - rminx_inner=rminx_inner, - cumulative_lower=cumulative_lower, - lower=lower, - ) - - axis.plot( - x_scale * np.array(vvg_single_null.rs), - vvg_single_null.zs, - color="black", - lw=thin, - zorder=5, - ) - - axis.fill( - x_scale * np.array(vvg_single_null.rs), - vvg_single_null.zs, - color=VESSEL_COLOUR[colour_scheme - 1], - lw=0.01, - zorder=5, - ) - - # Find indices where vessel boundary is between z_divertor_bottom and z_divertor_top - # Find the min and max R values of the vessel boundary between the divertor lines - mask = (vvg_single_null.zs >= z_divertor_lower_bottom) & ( - vvg_single_null.zs <= z_divertor_lower_top - ) - # Get the min/max R for the region between the divertor lines - r_min = ( - np.min(vvg_single_null.rs[mask]) - + dr_vv_inboard - + dr_shld_inboard - + (dr_blkt_inboard * 0.5) - ) - r_max = ( - np.max(vvg_single_null.rs[mask]) - - dr_vv_outboard - - dr_shld_outboard - - (dr_blkt_outboard * 0.5) - ) - # Draw a rectangle (box) between the two lines and inside the vessel - axis.add_patch( - patches.Rectangle( - ( - x_scale * r_min, - z_divertor_lower_bottom, - ), - x_scale * (r_max - r_min), - z_divertor_lower_top - z_divertor_lower_bottom, - facecolor="black", - alpha=0.8, - zorder=1, - ) - ) - - case DivertorNumberModels.DOUBLE_NULL: - z_divertor_upper_bottom = (kappa * rminor) + dz_xpoint_divertor - z_divertor_upper_top = z_divertor_upper_bottom + dz_divertor - vvg_double_null = vacuum_vessel_geometry_double_null( - cumulative_lower=cumulative_lower, - lower=lower, - radx_inner=radx_inner, - radx_outer=radx_outer, - rminx_inner=rminx_inner, - rminx_outer=rminx_outer, - triang=triang_95, - ) - axis.plot( - x_scale * np.array(vvg_double_null.rs), - vvg_double_null.zs, - color="black", - lw=thin, - zorder=5, - ) - - axis.fill( - x_scale * np.array(vvg_double_null.rs), - vvg_double_null.zs, - color=VESSEL_COLOUR[colour_scheme - 1], - lw=0.01, - zorder=5, - ) - - # Plot lower divertor - # Find indices where vessel boundary is between z_divertor_bottom and z_divertor_top - # Find the min and max R values of the vessel boundary between the divertor lines - mask = (vvg_double_null.zs >= z_divertor_lower_bottom) & ( - vvg_double_null.zs <= z_divertor_lower_top - ) - # Get the min/max R for the region between the divertor lines - r_min = ( - np.min(vvg_double_null.rs[mask]) - + dr_vv_inboard - + dr_shld_inboard - + (dr_blkt_inboard * 0.5) - ) - r_max = ( - np.max(vvg_double_null.rs[mask]) - - dr_vv_outboard - - dr_shld_outboard - - (dr_blkt_outboard * 0.5) - ) - # Draw a rectangle (box) between the two lines and inside the vessel - axis.add_patch( - patches.Rectangle( - ( - x_scale * r_min, - z_divertor_lower_bottom, - ), - x_scale * (r_max - r_min), - z_divertor_lower_top - z_divertor_lower_bottom, - facecolor="black", - alpha=0.8, - zorder=1, - ) - ) - # Plot upper divertor - # Find indices where vessel boundary is between z_divertor_bottom and z_divertor_top - # Find the min and max R values of the vessel boundary between the divertor lines - mask = (vvg_double_null.zs >= z_divertor_upper_bottom) & ( - vvg_double_null.zs <= z_divertor_upper_top - ) - # Get the min/max R for the region between the divertor lines - r_min = ( - np.min(vvg_double_null.rs[mask]) - + dr_vv_inboard - + dr_shld_inboard - + (dr_blkt_inboard * 0.5) - ) - r_max = ( - np.max(vvg_double_null.rs[mask]) - - dr_vv_outboard - - dr_shld_outboard - - (dr_blkt_outboard * 0.5) - ) - # Draw a rectangle (box) between the two lines and inside the vessel - axis.add_patch( - patches.Rectangle( - ( - x_scale * r_min, - z_divertor_upper_bottom, - ), - x_scale * (r_max - r_min), - z_divertor_upper_top - z_divertor_upper_bottom, - facecolor="black", - alpha=0.8, - zorder=1, - ) - ) - - -def plot_shield( - axis: plt.Axes, - mfile: MFile, - scan: int, - radial_build, - colour_scheme, - mirror_negative_x: bool = False, -): - """Function to plot shield - - Parameters - ---------- - axis : - axis object to plot to - mfile : - MFILE data object - scan : - scan number to use - radial_build : - - colour_scheme : - colour scheme to use for plots - mirror_negative_x : - if True, mirror the plot to the negative x-axis (Default value = False) - - """ - cumulative_upper = radial_build.cumulative_upper - cumulative_lower = radial_build.cumulative_lower - - i_single_null = mfile.get("i_single_null", scan=scan) - triang_95 = mfile.get("triang95", scan=scan) - - # Side furthest from plasma - radx_far = ( - cumulative_radial_build("dr_shld_outboard", mfile, scan) - + cumulative_radial_build("dr_vv_inboard", mfile, scan) - ) / 2.0 - rminx_far = ( - cumulative_radial_build("dr_shld_outboard", mfile, scan) - - cumulative_radial_build("dr_vv_inboard", mfile, scan) - ) / 2.0 - - # Side nearest to the plasma - radx_near = ( - cumulative_radial_build("vvblgapo", mfile, scan) - + cumulative_radial_build("dr_shld_inboard", mfile, scan) - ) / 2.0 - rminx_near = ( - cumulative_radial_build("vvblgapo", mfile, scan) - - cumulative_radial_build("dr_shld_inboard", mfile, scan) - ) / 2.0 - - # Apply mirror transformation if requested - x_scale = -1 if mirror_negative_x else 1 - - match DivertorNumberModels(i_single_null): - case DivertorNumberModels.SINGLE_NULL: - shield_geometry = shield_geometry_single_null( - cumulative_upper=cumulative_upper, - radx_far=radx_far, - rminx_far=rminx_far, - radx_near=radx_near, - rminx_near=rminx_near, - triang=triang_95, - cumulative_lower=cumulative_lower, - ) - case DivertorNumberModels.DOUBLE_NULL: - shield_geometry = shield_geometry_double_null( - cumulative_lower=cumulative_lower, - radx_far=radx_far, - radx_near=radx_near, - rminx_far=rminx_far, - rminx_near=rminx_near, - triang=triang_95, - ) - - axis.plot( - x_scale * np.array(shield_geometry.rs), - shield_geometry.zs, - color="black", - lw=thin, - ) - axis.fill( - x_scale * np.array(shield_geometry.rs), - shield_geometry.zs, - color=SHIELD_COLOUR[colour_scheme - 1], - lw=0.01, - ) - - -def plot_blanket( - axis: plt.Axes, - mfile: MFile, - scan, - radial_build, - colour_scheme, - mirror_negative_x: bool = False, -): - """Function to plot blanket - - Parameters - ---------- - axis : - axis object to plot to - mfile : - MFILE - scan : - scan number to use - radial_build : - - colour_scheme : - colour scheme to use for plots - mirror_negative_x : - if True, mirror the plot to the negative x-axis (Default value = False) - """ - cumulative_upper = radial_build.cumulative_upper - cumulative_lower = radial_build.cumulative_lower - - dr_blkt_inboard = mfile.get("dr_blkt_inboard", scan=scan) - dr_blkt_outboard = mfile.get("dr_blkt_outboard", scan=scan) - # Single null: Draw top half from output - # Double null: Reflect bottom half to top - i_single_null = mfile.get("i_single_null", scan=scan) - triang_95 = mfile.get("triang95", scan=scan) - if int(i_single_null) == 1: - dz_blkt_upper = mfile.get("dz_blkt_upper", scan=scan) - else: - dz_blkt_upper = 0.0 - - c_shldith = cumulative_radial_build("dr_shld_inboard", mfile, scan) - c_blnkoth = cumulative_radial_build("dr_blkt_outboard", mfile, scan) - - # Apply mirror transformation if requested - x_scale = -1 if mirror_negative_x else 1 - - match DivertorNumberModels(i_single_null): - case DivertorNumberModels.SINGLE_NULL: - # Upper blanket: outer surface - radx_outer = ( - cumulative_radial_build("dr_blkt_outboard", mfile, scan) - + cumulative_radial_build("vvblgapi", mfile, scan) - ) / 2.0 - rminx_outer = ( - cumulative_radial_build("dr_blkt_outboard", mfile, scan) - - cumulative_radial_build("vvblgapi", mfile, scan) - ) / 2.0 - - # Upper blanket: inner surface - radx_inner = ( - cumulative_radial_build("dr_fw_outboard", mfile, scan) - + cumulative_radial_build("dr_blkt_inboard", mfile, scan) - ) / 2.0 - rminx_inner = ( - cumulative_radial_build("dr_fw_outboard", mfile, scan) - - cumulative_radial_build("dr_blkt_inboard", mfile, scan) - ) / 2.0 - bg_single_null = blanket_geometry_single_null( - radx_outer=radx_outer, - rminx_outer=rminx_outer, - radx_inner=radx_inner, - rminx_inner=rminx_inner, - cumulative_upper=cumulative_upper, - triang=triang_95, - cumulative_lower=cumulative_lower, - dz_blkt_upper=dz_blkt_upper, - c_shldith=c_shldith, - c_blnkoth=c_blnkoth, - dr_blkt_inboard=dr_blkt_inboard, - dr_blkt_outboard=dr_blkt_outboard, - ) - - # Plot blanket - axis.plot( - x_scale * np.array(bg_single_null.rs), - bg_single_null.zs, - color="black", - lw=thin, - zorder=5, - ) - - axis.fill( - x_scale * np.array(bg_single_null.rs), - bg_single_null.zs, - color=BLANKET_COLOUR[colour_scheme - 1], - lw=0.01, - zorder=5, - ) - - case DivertorNumberModels.DOUBLE_NULL: - bg_double_null = blanket_geometry_double_null( - cumulative_lower=cumulative_lower, - triang=triang_95, - dz_blkt_upper=dz_blkt_upper, - c_shldith=c_shldith, - c_blnkoth=c_blnkoth, - dr_blkt_inboard=dr_blkt_inboard, - dr_blkt_outboard=dr_blkt_outboard, - ) - # Plot blanket - axis.plot( - x_scale * np.array(bg_double_null.rs[0]), - bg_double_null.zs[0], - color="black", - lw=thin, - ) - axis.fill( - x_scale * np.array(bg_double_null.rs[0]), - bg_double_null.zs[0], - color=BLANKET_COLOUR[colour_scheme - 1], - lw=0.01, - zorder=5, - ) - if dr_blkt_inboard > 0.0: - # only plot inboard blanket if inboard blanket thickness > 0 - axis.plot( - x_scale * np.array(bg_double_null.rs[1]), - bg_double_null.zs[1], - color="black", - lw=thin, - zorder=5, - ) - axis.fill( - x_scale * np.array(bg_double_null.rs[1]), - bg_double_null.zs[1], - color=BLANKET_COLOUR[colour_scheme - 1], - lw=0.01, - zorder=5, - ) - - -def plot_first_wall_top_down_cross_section(axis: plt.Axes, mfile: MFile, scan: int): - """Plot first wall top down cross-section""" - # Import required variables - radius_fw_channel = mfile.get("radius_fw_channel", scan=scan) * 100 - dr_fw_wall = mfile.get("dr_fw_wall", scan=scan) * 100 - dx_fw_module = mfile.get("dx_fw_module", scan=scan) * 100 - - # Flot first module - axis.add_patch( - patches.Rectangle( - xy=(0, 0), - width=dx_fw_module, - height=2 * (dr_fw_wall + radius_fw_channel), - edgecolor="black", - facecolor="gray", - ) - ) - - # Plot cooling channel in first module - axis.add_patch( - patches.Circle( - xy=(dx_fw_module / 2, dr_fw_wall + radius_fw_channel), - radius=radius_fw_channel, - edgecolor="black", - facecolor="#b87333", - ) - ) - - # Plot second module - axis.add_patch( - patches.Rectangle( - xy=(dx_fw_module, 0), - width=dx_fw_module, - height=2 * (dr_fw_wall + radius_fw_channel), - edgecolor="black", - facecolor="gray", - ) - ) - - # Plot cooling channel in second module - axis.add_patch( - patches.Circle( - xy=(dx_fw_module + dx_fw_module / 2, dr_fw_wall + radius_fw_channel), - radius=radius_fw_channel, - edgecolor="black", - facecolor="#b87333", - ) - ) - - # Draw radius line in the second circle - axis.plot( - [ - dx_fw_module + dx_fw_module / 2, - dx_fw_module + dx_fw_module / 2 + radius_fw_channel * np.cos(np.pi / 4), - ], - [ - dr_fw_wall + radius_fw_channel, - dr_fw_wall + radius_fw_channel + radius_fw_channel * np.sin(np.pi / 4), - ], - color="black", - linestyle="--", - label=f"$r_{{channel}}$ = {radius_fw_channel:.3f} cm", - ) - - # Draw width line below the second module - axis.plot( - [0, 0], - [0, 0], - color="black", - label=f"$w_{{module}}$ = {dx_fw_module:.3f} cm", - ) - axis.annotate( - "", - xy=(dx_fw_module, -0.2), - xytext=(2 * dx_fw_module, -0.2), - arrowprops={"arrowstyle": "<->", "color": "black"}, - ) - - # Draw dotted line above the channel - axis.plot( - [dx_fw_module * 1.5, dx_fw_module * 1.5], - [2 * radius_fw_channel + dr_fw_wall, 2 * (radius_fw_channel + dr_fw_wall)], - color="black", - linestyle="dotted", - label=rf"$\Delta r_{{wall}}$ = {dr_fw_wall:.3f} cm", - ) - - # Draw dotted line below the channel - axis.plot( - [dx_fw_module * 1.5, dx_fw_module * 1.5], - [0, dr_fw_wall], - color="black", - linestyle="dotted", - ) - # Plot a dot in the center of the second channel - axis.plot( - dx_fw_module + dx_fw_module / 2, - dr_fw_wall + radius_fw_channel, - marker="o", - color="black", - ) - - # Add the legend to the plot - axis.legend() - axis.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.2) - axis.set_xlabel("X [cm]") - axis.set_ylabel("R [cm]") - axis.set_title("First Wall Top-Down Cross Section") - axis.set_xlim(-1, 2 * dx_fw_module + 1) - axis.set_ylim(-1, 2 * (dr_fw_wall + radius_fw_channel) + 1) - - -def plot_first_wall_poloidal_cross_section(axis: plt.Axes, mfile: MFile, scan: int): - """Plot first wall poloidal cross-section""" - # Import required variables - radius_fw_channel = mfile.get("radius_fw_channel", scan=scan) - dr_fw_wall = mfile.get("dr_fw_wall", scan=scan) - dx_fw_module = mfile.get("dx_fw_module", scan=scan) - len_fw_channel = mfile.get("len_fw_channel", scan=scan) - temp_fw_coolant_in = mfile.get("temp_fw_coolant_in", scan=scan) - temp_fw_coolant_out = mfile.get("temp_fw_coolant_out", scan=scan) - i_fw_coolant_type = mfile.get("i_fw_coolant_type", scan=scan).strip("'\"") - temp_fw_peak = mfile.get("temp_fw_peak", scan=scan) - pres_fw_coolant = mfile.get("pres_fw_coolant", scan=scan) - n_fw_outboard_channels = mfile.get("n_fw_outboard_channels", scan=scan) - n_fw_inboard_channels = mfile.get("n_fw_inboard_channels", scan=scan) - - # Plot first wall structure facing plasma - axis.add_patch( - patches.Rectangle( - xy=(0, 0), - width=dr_fw_wall, - height=len_fw_channel, - edgecolor="black", - facecolor="gray", - ) - ) - - # Plot the cooling channel - axis.add_patch( - patches.Rectangle( - xy=(dr_fw_wall, 0), - width=2 * radius_fw_channel, - height=len_fw_channel, - edgecolor="black", - facecolor="#b87333", # Copper color - ) - ) - - # Plot the back wall of the first wall - axis.add_patch( - patches.Rectangle( - xy=(dr_fw_wall + 2 * radius_fw_channel, 0), - width=dr_fw_wall, - height=len_fw_channel, - edgecolor="black", - facecolor="grey", - ) - ) - - # Draw an upward pointing arrow - axis.arrow( - dx_fw_module + 0.5 * dr_fw_wall, - dr_fw_wall + radius_fw_channel, - 0, - len_fw_channel / 6, - head_width=dr_fw_wall, - head_length=len_fw_channel / 20, - fc="black", - ec="black", - ) - - # Add the inlet temperature beside the arrow - axis.text( - dx_fw_module + 2 * dr_fw_wall, - dr_fw_wall + radius_fw_channel + len_fw_channel / 6, - f"$T_{{inlet}} = ${temp_fw_coolant_in:.2f} K", - ha="left", - va="bottom", - fontsize=10, - color="black", - ) - - # Draw a right pointing arrow - axis.arrow( - dx_fw_module + 0.5 * dr_fw_wall, - len_fw_channel, - 2 * dr_fw_wall, - 0, - head_width=len_fw_channel / 30, - head_length=dr_fw_wall, - fc="black", - ec="black", - linewidth=5, # Thicker stem - ) - - # Add the outlet temperature beside the arrow - axis.text( - dx_fw_module + 0.5 * dr_fw_wall, - len_fw_channel * 0.9, - f"$T_{{outlet}} = ${temp_fw_coolant_out:.2f} K", - ha="left", - va="bottom", - fontsize=10, - color="black", - ) - - textstr_fw = "\n".join(( - rf"Coolant type: {i_fw_coolant_type}", - rf"$T_{{FW,peak}}$: {temp_fw_peak:,.3f} K", - rf"$P_{{FW}}$: {pres_fw_coolant / 1e3:,.3f} kPa", - rf"$P_{{FW}}$: {pres_fw_coolant / 1e5:,.3f} bar", - rf"$N_{{outboard}}$: {n_fw_outboard_channels}", - rf"$N_{{inboard}}$: {n_fw_inboard_channels}", - )) - - props_fw = {"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5} - axis.text( - -0.5, - 0.05, - textstr_fw, - transform=axis.transAxes, - fontsize=11, - verticalalignment="bottom", - bbox=props_fw, - ) - - axis.set_xlabel("R [m]") - axis.set_ylabel("Z [m]") - axis.set_title("First Wall Poloidal Cross Section") - axis.set_xlim(-0.01, (dx_fw_module + radius_fw_channel * 2) + 0.01) - axis.set_ylim(-0.2, len_fw_channel + 0.2) - - -def plot_firstwall( - axis: plt.Axes, - mfile: MFile, - scan: int, - radial_build, - colour_scheme, - mirror_negative_x: bool = False, -): - """Function to plot first wall - - Parameters - ---------- - axis : - axis object to plot to - mfile : - MFILE - scan : - scan number to use - radial_build : - - colour_scheme : - colour scheme to use for plots - mirror_negative_x : - if True, mirror the plot to the negative x-axis (Default value = False) - """ - cumulative_upper = radial_build.cumulative_upper - cumulative_lower = radial_build.cumulative_lower - - i_single_null = mfile.get("i_single_null", scan=scan) - triang_95 = mfile.get("triang95", scan=scan) - if int(i_single_null) == 1: - dz_blkt_upper = mfile.get("dz_blkt_upper", scan=scan) - tfwvt = mfile.get("dz_fw_upper", scan=scan) - else: - dz_blkt_upper = tfwvt = 0.0 - - c_blnkith = cumulative_radial_build("dr_blkt_inboard", mfile, scan) - c_fwoth = cumulative_radial_build("dr_fw_outboard", mfile, scan) - - dr_fw_inboard = mfile.get("dr_fw_inboard", scan=scan) - dr_fw_outboard = mfile.get("dr_fw_outboard", scan=scan) - - # Apply mirror transformation if requested - x_scale = -1 if mirror_negative_x else 1 - - match DivertorNumberModels(i_single_null): - case DivertorNumberModels.SINGLE_NULL: - # Upper first wall: outer surface - radx_outer = ( - cumulative_radial_build("dr_fw_outboard", mfile, scan) - + cumulative_radial_build("dr_blkt_inboard", mfile, scan) - ) / 2.0 - rminx_outer = ( - cumulative_radial_build("dr_fw_outboard", mfile, scan) - - cumulative_radial_build("dr_blkt_inboard", mfile, scan) - ) / 2.0 - - # Upper first wall: inner surface - radx_inner = ( - cumulative_radial_build("dr_fw_plasma_gap_outboard", mfile, scan) - + cumulative_radial_build("dr_fw_inboard", mfile, scan) - ) / 2.0 - rminx_inner = ( - cumulative_radial_build("dr_fw_plasma_gap_outboard", mfile, scan) - - cumulative_radial_build("dr_fw_inboard", mfile, scan) - ) / 2.0 - - fwg_single_null = first_wall_geometry_single_null( - radx_outer=radx_outer, - rminx_outer=rminx_outer, - radx_inner=radx_inner, - rminx_inner=rminx_inner, - cumulative_upper=cumulative_upper, - triang=triang_95, - cumulative_lower=cumulative_lower, - dz_blkt_upper=dz_blkt_upper, - c_blnkith=c_blnkith, - c_fwoth=c_fwoth, - dr_fw_inboard=dr_fw_inboard, - dr_fw_outboard=dr_fw_outboard, - tfwvt=tfwvt, - ) - - # Plot first wall - axis.plot( - x_scale * np.array(fwg_single_null.rs), - fwg_single_null.zs, - color="black", - lw=thin, - ) - axis.fill( - x_scale * np.array(fwg_single_null.rs), - fwg_single_null.zs, - color=FIRSTWALL_COLOUR[colour_scheme - 1], - lw=0.01, - ) - - case DivertorNumberModels.DOUBLE_NULL: - fwg_double_null = first_wall_geometry_double_null( - cumulative_lower=cumulative_lower, - triang=triang_95, - dz_blkt_upper=dz_blkt_upper, - c_blnkith=c_blnkith, - c_fwoth=c_fwoth, - dr_fw_inboard=dr_fw_inboard, - dr_fw_outboard=dr_fw_outboard, - tfwvt=tfwvt, - ) - # Plot first wall - axis.plot( - x_scale * np.array(fwg_double_null.rs[0]), - fwg_double_null.zs[0], - color="black", - lw=thin, - ) - axis.plot( - x_scale * np.array(fwg_double_null.rs[1]), - fwg_double_null.zs[1], - color="black", - lw=thin, - ) - axis.fill( - x_scale * np.array(fwg_double_null.rs[0]), - fwg_double_null.zs[0], - color=FIRSTWALL_COLOUR[colour_scheme - 1], - lw=0.01, - ) - axis.fill( - x_scale * np.array(fwg_double_null.rs[1]), - fwg_double_null.zs[1], - color=FIRSTWALL_COLOUR[colour_scheme - 1], - lw=0.01, - ) - - -def plot_tf_coils( - axis: plt.Axes, - mfile: MFile, - scan: int, - colour_scheme: Literal[1, 2], - mirror_negative_x: bool = False, -): - """Function to plot TF coils - - Parameters - ---------- - axis : - axis object to plot to - mfile : - MFILE - scan : - scan number to use - colour_scheme : - colour scheme to use for plots - mirror_negative_x : - if True, mirror the plot to the negative x-axis (Default value = False) - """ - # Apply mirror transformation if requested - x_scale = -1 if mirror_negative_x else 1 - - # Arc points - # MDK Only 4 points now required for elliptical arcs - x1 = mfile.get("r_tf_arc(1)", scan=scan) - y1 = mfile.get("z_tf_arc(1)", scan=scan) - x2 = mfile.get("r_tf_arc(2)", scan=scan) - y2 = mfile.get("z_tf_arc(2)", scan=scan) - x3 = mfile.get("r_tf_arc(3)", scan=scan) - y3 = mfile.get("z_tf_arc(3)", scan=scan) - x4 = mfile.get("r_tf_arc(4)", scan=scan) - y4 = mfile.get("z_tf_arc(4)", scan=scan) - x5 = mfile.get("r_tf_arc(5)", scan=scan) - y5 = mfile.get("z_tf_arc(5)", scan=scan) - - dr_tf_inboard = mfile.get("dr_tf_inboard", scan=scan) - dr_tf_outboard = mfile.get("dr_tf_outboard", scan=scan) - dr_shld_thermal_inboard = mfile.get("dr_shld_thermal_inboard", scan=scan) - dr_shld_thermal_outboard = mfile.get("dr_shld_thermal_outboard", scan=scan) - dr_tf_shld_gap = mfile.get("dr_tf_shld_gap", scan=scan) - if y3 != 0: - print("TF coil geometry: The value of z_tf_arc(3) is not zero, but should be.") - - if dr_shld_thermal_inboard != dr_shld_thermal_outboard: - print( - "dr_shld_thermal_inboard and dr_shld_thermal_outboard are different. Using dr_shld_thermal_inboard" - "for the poloidal plot of the thermal shield." - ) - - for offset, colour in ( - ( - dr_shld_thermal_inboard + dr_tf_shld_gap, - THERMAL_SHIELD_COLOUR[colour_scheme - 1], - ), - (dr_tf_shld_gap, "white"), - ( - 0.0, - ( - TFC_COLOUR[colour_scheme - 1] - if mfile.get("i_tf_sup", scan=scan) != 0 - else "#b87333" - ), - ), - ): - # Check for TF coil shape - if "i_tf_shape" in mfile.data: - i_tf_shape = int(mfile.get("i_tf_shape", scan=scan)) - else: - i_tf_shape = 1 - - if i_tf_shape == TFCoilShapeModel.PICTURE_FRAME: - rects = tfcoil_geometry_rectangular_shape( - x1=x1, - x2=x2, - x4=x4, - x5=x5, - y1=y1, - y2=y2, - y4=y4, - y5=y5, - dr_tf_inboard=dr_tf_inboard, - dr_tf_outboard=dr_tf_outboard, - offset_in=offset, - ) - - else: - rects, verts = tfcoil_geometry_d_shape( - x1=x1, - x2=x2, - x3=x3, - x4=x4, - x5=x5, - y1=y1, - y2=y2, - y4=y4, - y5=y5, - dr_tf_inboard=dr_tf_inboard, - rtangle=rtangle, - rtangle2=rtangle2, - offset_in=offset, - ) - - for vert in verts: - # Mirror vertices if needed - mirrored_vert = [[x_scale * point[0], point[1]] for point in vert] - path = mplPath(mirrored_vert, closed=True) - patch = patches.PathPatch(path, facecolor=colour, lw=0) - axis.add_patch(patch) - - for rec in rects: - axis.add_patch( - patches.Rectangle( - xy=(x_scale * rec.anchor_x, rec.anchor_z), - width=x_scale * rec.width, - height=rec.height, - facecolor=colour, - ) - ) - - -def plot_superconducting_tf_wp(axis: plt.Axes, mfile: MFile, scan: int, fig): - """Plots inboard TF coil and winding pack. - - Parameters - ---------- - axis : matplotlib.axes object - Axis object to plot to. - mfile : MFILE data object - Object containing data for the plot. - scan : int - Scan number to use. - """ - # Import the TF variables - r_tf_inboard_in = mfile.get("r_tf_inboard_in", scan=scan) - r_tf_inboard_out = mfile.get("r_tf_inboard_out", scan=scan) - dx_tf_wp_primary_toroidal = mfile.get("dx_tf_wp_primary_toroidal", scan=scan) - dx_tf_side_case_peak = mfile.get("dx_tf_side_case_peak", scan=scan) - dx_tf_wp_secondary_toroidal = mfile.get("dx_tf_wp_secondary_toroidal", scan=scan) - dr_tf_wp_with_insulation = mfile.get("dr_tf_wp_with_insulation", scan=scan) - r_tf_wp_inboard_inner = mfile.get("r_tf_wp_inboard_inner", scan=scan) - dx_tf_wp_insulation = mfile.get("dx_tf_wp_insulation", scan=scan) - n_tf_coil_turns = round(mfile.get("n_tf_coil_turns", scan=scan)) - i_tf_wp_geom = round(mfile.get("i_tf_wp_geom", scan=scan)) - i_tf_sup = round(mfile.get("i_tf_sup", scan=scan)) - i_tf_case_geom = mfile.get("i_tf_case_geom", scan=scan) - i_tf_turns_integer = mfile.get("i_tf_turns_integer", scan=scan) - b_tf_inboard_peak_symmetric = mfile.get("b_tf_inboard_peak_symmetric", scan=scan) - b_tf_inboard_peak_with_ripple = mfile.get("b_tf_inboard_peak_with_ripple", scan=scan) - f_b_tf_inboard_peak_ripple_symmetric = mfile.get( - "f_b_tf_inboard_peak_ripple_symmetric", scan=scan - ) - r_b_tf_inboard_peak = mfile.get("r_b_tf_inboard_peak", scan=scan) - dx_tf_wp_insertion_gap = mfile.get("dx_tf_wp_insertion_gap", scan=scan) - r_tf_wp_inboard_outer = mfile.get("r_tf_wp_inboard_outer", scan=scan) - r_tf_wp_inboard_centre = mfile.get("r_tf_wp_inboard_centre", scan=scan) - - if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.INTEGER: - turn_layers = mfile.get("n_tf_wp_layers", scan=scan) - turn_pancakes = mfile.get("n_tf_wp_pancakes", scan=scan) - - # Superconducting coil check - if i_tf_sup == 1: - axis.add_patch( - Circle( - (0, 0), - r_tf_inboard_in, - facecolor="none", - edgecolor="black", - linestyle="--", - ), - ) - - if i_tf_case_geom == TFPlasmaCaseType.CIRCULAR: - axis.add_patch( - Circle( - (0, 0), - r_tf_inboard_out, - facecolor="none", - edgecolor="black", - linestyle="--", - ), - ) - - # Equations for plotting the TF case - rad_tf_coil_inboard_toroidal_half = mfile.get( - "rad_tf_coil_inboard_toroidal_half", scan=scan - ) - - # X points for inboard case curve - x11 = r_tf_inboard_in * np.cos( - np.linspace( - rad_tf_coil_inboard_toroidal_half, - -rad_tf_coil_inboard_toroidal_half, - 256, - endpoint=True, - ) - ) - # Y points for inboard case curve - y11 = r_tf_inboard_in * np.sin( - np.linspace( - rad_tf_coil_inboard_toroidal_half, - -rad_tf_coil_inboard_toroidal_half, - 256, - endpoint=True, - ) - ) - # Check for plasma side case type - if i_tf_case_geom == TFPlasmaCaseType.CIRCULAR: - # Rounded case - - # X points for outboard case curve - x12 = r_tf_inboard_out * np.cos( - np.linspace( - rad_tf_coil_inboard_toroidal_half, - -rad_tf_coil_inboard_toroidal_half, - 256, - endpoint=True, - ) - ) - - elif i_tf_case_geom == TFPlasmaCaseType.STRAIGHT: - # Flat case - - # X points for outboard case - x12 = np.full(256, r_tf_inboard_out) - else: - raise NotImplementedError("i_tf_case_geom must be 0 or 1") - - # Y points for outboard case - y12 = r_tf_inboard_out * np.sin( - np.linspace( - rad_tf_coil_inboard_toroidal_half, - -rad_tf_coil_inboard_toroidal_half, - 256, - endpoint=True, - ) - ) - - # Cordinates of the top and bottom of case curves, - # used to plot the lines connecting the inside and outside of the case - y13 = [y11[0], y12[0]] - x13 = [x11[0], x12[0]] - y14 = [y11[-1], y12[-1]] - x14 = [x11[-1], x12[-1]] - - # Plot the case outline - axis.plot(x11, y11, color="black") - axis.plot(x12, y12, color="black") - axis.plot(x13, y13, color="black") - axis.plot(x14, y14, color="black") - - # Fill in the case segemnts - - # Upper main - if i_tf_case_geom == TFPlasmaCaseType.CIRCULAR: - axis.fill_between( - [ - (r_tf_inboard_in * np.cos(rad_tf_coil_inboard_toroidal_half)), - (r_tf_inboard_out * np.cos(rad_tf_coil_inboard_toroidal_half)), - ], - y13, - color="grey", - alpha=0.25, - ) - # Lower main - axis.fill_between( - [ - (r_tf_inboard_in * np.cos(rad_tf_coil_inboard_toroidal_half)), - (r_tf_inboard_out * np.cos(rad_tf_coil_inboard_toroidal_half)), - ], - y14, - color="grey", - alpha=0.25, - ) - axis.fill_between( - x12, - y12, - color="grey", - alpha=0.25, - ) - elif i_tf_case_geom == TFPlasmaCaseType.STRAIGHT: - axis.fill_between( - [ - (r_tf_inboard_in * np.cos(rad_tf_coil_inboard_toroidal_half)), - (r_tf_inboard_out), - ], - y13, - color="grey", - alpha=0.25, - ) - # Lower main - axis.fill_between( - [ - (r_tf_inboard_in * np.cos(rad_tf_coil_inboard_toroidal_half)), - (r_tf_inboard_out), - ], - y14, - color="grey", - alpha=0.25, - ) - - # Removes ovelapping colours on inner nose case - axis.fill_between( - x11, - y11, - color="white", - alpha=1.0, - ) - - # Centre line for relative reference - axis.axhline(y=0.0, color="r", linestyle="--", linewidth=0.25) - - # ================================================================ - - # Plot the rectangular WP - if i_tf_wp_geom == 0: - if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.INTEGER: - long_turns = round(turn_layers) - short_turns = round(turn_pancakes) - else: - wp_side_ratio = ( - dr_tf_wp_with_insulation - - (2 * (dx_tf_wp_insulation + dx_tf_wp_insertion_gap)) - ) / ( - dx_tf_wp_primary_toroidal - - (2 * (dx_tf_wp_insulation + dx_tf_wp_insertion_gap)) - ) # row to height - side_unit = n_tf_coil_turns / wp_side_ratio - root_turns = round(np.sqrt(side_unit), 1) - long_turns = round(root_turns * wp_side_ratio) - short_turns = round(root_turns) - - # Plots the surrounding insualtion - axis.add_patch( - Rectangle( - (r_tf_wp_inboard_inner, -(0.5 * dx_tf_wp_primary_toroidal)), - dr_tf_wp_with_insulation, - dx_tf_wp_primary_toroidal, - color="darkgreen", - ), - ) - # Plots the WP inside the insulation - axis.add_patch( - Rectangle( - ( - r_tf_wp_inboard_inner - + dx_tf_wp_insulation - + dx_tf_wp_insertion_gap, - -(0.5 * dx_tf_wp_primary_toroidal) - + dx_tf_wp_insulation - + dx_tf_wp_insertion_gap, - ), - ( - dr_tf_wp_with_insulation - - (2 * (dx_tf_wp_insulation + dx_tf_wp_insertion_gap)) - ), - ( - dx_tf_wp_primary_toroidal - - (2 * (dx_tf_wp_insulation + dx_tf_wp_insertion_gap)) - ), - color="blue", - ) - ) - # Dvides the WP up into the turn segments - for i in range(1, long_turns): - axis.plot( - [ - ( - r_tf_wp_inboard_inner - + dx_tf_wp_insulation - + dx_tf_wp_insertion_gap - ) - + i - * ( - ( - dr_tf_wp_with_insulation - - dx_tf_wp_insulation - - dx_tf_wp_insertion_gap - ) - / long_turns - ), - ( - r_tf_wp_inboard_inner - + dx_tf_wp_insulation - + dx_tf_wp_insertion_gap - ) - + i - * ( - ( - dr_tf_wp_with_insulation - - dx_tf_wp_insulation - - dx_tf_wp_insertion_gap - ) - / long_turns - ), - ], - [ - -0.5 * dx_tf_wp_primary_toroidal - + (dx_tf_wp_insulation + dx_tf_wp_insertion_gap), - 0.5 * dx_tf_wp_primary_toroidal - - (dx_tf_wp_insulation + dx_tf_wp_insertion_gap), - ], - color="white", - linewidth="0.25", - linestyle="dashed", - ) - - for i in range(1, short_turns): - axis.plot( - [ - ( - r_tf_wp_inboard_inner - + dx_tf_wp_insulation - + dx_tf_wp_insertion_gap - ), - ( - r_tf_wp_inboard_outer - - dx_tf_wp_insulation - - dx_tf_wp_insertion_gap - ), - ], - [ - ( - -0.5 * dx_tf_wp_primary_toroidal - + dx_tf_wp_insulation - + dx_tf_wp_insertion_gap - ) - + ( - i - * ( - dx_tf_wp_primary_toroidal - - dx_tf_wp_insulation - - dx_tf_wp_insertion_gap - ) - / short_turns - ), - ( - -0.5 * dx_tf_wp_primary_toroidal - + dx_tf_wp_insulation - + dx_tf_wp_insertion_gap - ) - + ( - i - * ( - dx_tf_wp_primary_toroidal - - dx_tf_wp_insulation - - dx_tf_wp_insertion_gap - ) - / short_turns - ), - ], - color="white", - linewidth="0.25", - linestyle="dashed", - ) - - # ================================================================ - - # Plot the double rectangle winding pack - if i_tf_wp_geom == 1: - # Inner WP insulation - axis.add_patch( - Rectangle( - ( - r_tf_wp_inboard_inner, - -(0.5 * dx_tf_wp_secondary_toroidal), - ), - (dr_tf_wp_with_insulation / 2) + (dx_tf_wp_insulation), - dx_tf_wp_secondary_toroidal, - color="darkgreen", - ), - ) - - # Outer WP insulation - axis.add_patch( - Rectangle( - ( - r_tf_wp_inboard_centre, - -(0.5 * dx_tf_wp_primary_toroidal), - ), - (dr_tf_wp_with_insulation / 2), - dx_tf_wp_primary_toroidal, - color="darkgreen", - ), - ) - - # Outer WP - axis.add_patch( - Rectangle( - ( - r_tf_wp_inboard_centre - + dx_tf_wp_insulation - + dx_tf_wp_insertion_gap, - -(0.5 * dx_tf_wp_primary_toroidal) - + dx_tf_wp_insulation - + dx_tf_wp_insertion_gap, - ), - (dr_tf_wp_with_insulation / 2) - - (2 * (dx_tf_wp_insulation + dx_tf_wp_insertion_gap)), - dx_tf_wp_primary_toroidal - - (2 * (dx_tf_wp_insulation + dx_tf_wp_insertion_gap)), - color="blue", - ), - ) - # Inner WP - axis.add_patch( - Rectangle( - ( - r_tf_wp_inboard_inner - + dx_tf_wp_insulation - + dx_tf_wp_insertion_gap, - -(0.5 * dx_tf_wp_secondary_toroidal) - + dx_tf_wp_insulation - + dx_tf_wp_insertion_gap, - ), - (dr_tf_wp_with_insulation / 2), - dx_tf_wp_secondary_toroidal - - (2 * (dx_tf_wp_insulation + dx_tf_wp_insertion_gap)), - color="blue", - ), - ) - - # ================================================================ - - # Trapezium WP - if i_tf_wp_geom == 2: - # WP insulation - x = [ - r_tf_wp_inboard_inner, - r_tf_wp_inboard_inner, - r_tf_wp_inboard_outer, - r_tf_wp_inboard_outer, - ] - y = [ - (-0.5 * dx_tf_wp_secondary_toroidal), - (0.5 * dx_tf_wp_secondary_toroidal), - (0.5 * dx_tf_wp_primary_toroidal), - (-0.5 * dx_tf_wp_primary_toroidal), - ] - axis.add_patch( - patches.Polygon( - xy=list(zip(x, y, strict=False)), - color="darkgreen", - ) - ) - - # WP - x = [ - r_tf_wp_inboard_inner + dx_tf_wp_insulation + dx_tf_wp_insertion_gap, - r_tf_wp_inboard_inner + dx_tf_wp_insulation + dx_tf_wp_insertion_gap, - (r_tf_wp_inboard_outer - dx_tf_wp_insulation - dx_tf_wp_insertion_gap), - (r_tf_wp_inboard_outer - dx_tf_wp_insulation - dx_tf_wp_insertion_gap), - ] - y = [ - ( - -0.5 * dx_tf_wp_secondary_toroidal - + dx_tf_wp_insulation - + dx_tf_wp_insertion_gap - ), - ( - 0.5 * dx_tf_wp_secondary_toroidal - - dx_tf_wp_insulation - - dx_tf_wp_insertion_gap - ), - ( - 0.5 * dx_tf_wp_primary_toroidal - - dx_tf_wp_insulation - - dx_tf_wp_insertion_gap - ), - ( - -0.5 * dx_tf_wp_primary_toroidal - + dx_tf_wp_insulation - + dx_tf_wp_insertion_gap - ), - ] - axis.add_patch( - patches.Polygon( - xy=list(zip(x, y, strict=False)), - color="blue", - ) - ) - - # Plot a dot for the location of the peak field - axis.plot( - r_b_tf_inboard_peak, - 0, - marker="o", - color="red", - label=( - f"Peak axisymmetric field: {b_tf_inboard_peak_symmetric:.3f} T\n" - f"Peak non-axisymmetric field with ripple: " - f"{b_tf_inboard_peak_with_ripple:.3f} T\n" - f"$\\frac{{B_{{\\text{{axisymmetric}}}}}}{{B_{{\\text{{non-axisymmetric}}}}}}$: " - f"{f_b_tf_inboard_peak_ripple_symmetric:.3f}\n" - f"$r_{{\\text{{peak}}}}$={r_b_tf_inboard_peak:.3f} m" - ), - ) - - # Plot a horizontal line at y = dx_tf_wp_inner_toroidal - axis.axhline( - y=dx_tf_wp_secondary_toroidal / 2, - color="black", - linestyle="--", - linewidth=0.6, - alpha=0.5, - ) - # Plot a horizontal line at y = dx_tf_wp_inner_toroidal - axis.axhline( - y=-dx_tf_wp_secondary_toroidal / 2, - color="black", - linestyle="--", - linewidth=0.6, - alpha=0.5, - ) - axis.axhline( - y=dx_tf_wp_primary_toroidal / 2, - color="black", - linestyle="--", - linewidth=0.6, - alpha=0.5, - ) - axis.axhline( - y=-dx_tf_wp_primary_toroidal / 2, - color="black", - linestyle="--", - linewidth=0.6, - alpha=0.5, - ) - # Max toroidal width including side case - axis.axhline( - y=(dx_tf_wp_primary_toroidal / 2) + dx_tf_side_case_peak, - color="black", - linestyle="--", - linewidth=0.6, - alpha=0.5, - ) - - axis.axhline( - y=-(dx_tf_wp_primary_toroidal / 2) - dx_tf_side_case_peak, - color="black", - linestyle="--", - linewidth=0.6, - alpha=0.5, - ) - - axis.axvline( - x=r_tf_inboard_in, - color="black", - linestyle="--", - linewidth=0.6, - alpha=0.5, - ) - axis.axvline( - x=r_tf_wp_inboard_inner, - color="black", - linestyle="--", - linewidth=0.6, - alpha=0.5, - ) - axis.axvline( - x=r_tf_wp_inboard_outer, - color="black", - linestyle="--", - linewidth=0.6, - alpha=0.5, - ) - axis.axvline( - x=r_tf_wp_inboard_centre, - color="black", - linestyle="--", - linewidth=0.6, - alpha=0.5, - ) - axis.axvline( - x=r_tf_inboard_out, - color="black", - linestyle="--", - linewidth=0.6, - alpha=0.5, - ) - - # Add info about the steel casing surrounding the WP - textstr_casing = ( - f"$\\mathbf{{Casing:}}$\n\n" - f"Coil half angle: {mfile.get('rad_tf_coil_inboard_toroidal_half', scan=scan):.3f} radians\n\n" - f"$\\text{{Full Coil Case:}}$\n" - f"$r_{{start}} \\rightarrow r_{{end}}$: {mfile.get('r_tf_inboard_in', scan=scan):.3f} $\\rightarrow$ {mfile.get('r_tf_inboard_out', scan=scan):.3f} m\n" - f"$\\Delta r$: {mfile.get('dr_tf_inboard', scan=scan):.3f} m\n" - f"Area of casing around WP: {mfile.get('a_tf_coil_inboard_case', scan=scan):.3f} $\\mathrm{{m}}^2$\n\n" - f"$\\text{{Nose Case:}}$\n" - f"$r_{{start}} \\rightarrow r_{{end}}$: {mfile.get('r_tf_inboard_in', scan=scan):.3f} $\\rightarrow$ {mfile.get('r_tf_wp_inboard_inner', scan=scan):.3f} m\n" - f"$\\Delta r$: {mfile.get('dr_tf_nose_case', scan=scan):.3f} m\n" - f"$A$: {mfile.get('a_tf_coil_nose_case', scan=scan):.3f} $\\mathrm{{m}}^2$\n\n" - f"$\\text{{Plasma Case:}}$\n" - f"$r_{{start}} \\rightarrow r_{{end}}$: {mfile.get('r_tf_wp_inboard_outer', scan=scan):.3f} $\\rightarrow$ {mfile.get('r_tf_inboard_out', scan=scan):.3f} m\n" - f"$\\Delta r$: {mfile.get('dr_tf_plasma_case', scan=scan):.3f} m\n" - f"$A$: {mfile.get('a_tf_plasma_case', scan=scan):.3f} $\\mathrm{{m}}^2$\n\n" - f"$\\text{{Side Case:}}$\n" - f"Minimum $\\Delta r$: {mfile.get('dx_tf_side_case_min', scan=scan):.3f} m\n" - f"Average $\\Delta r$: {mfile.get('dx_tf_side_case_average', scan=scan):.3f} m\n" - f"Max $\\Delta r$: {mfile.get('dx_tf_side_case_peak', scan=scan):.3f} m" - ) - axis.text( - 0.55, - 0.975, - textstr_casing, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "grey", - "alpha": 1.0, - "linewidth": 2, - }, - ) - - # Add info about the steel casing surrounding the WP - textstr_wp_insulation = ( - f"$\\mathbf{{Ground \\ Insulation:}}$\n\n" - f"Area of insulation around WP: {mfile.get('a_tf_wp_ground_insulation', scan=scan):.3f} $\\mathrm{{m}}^2$\n" - f"$\\Delta r$: {mfile.get('dx_tf_wp_insulation', scan=scan):.4f} m\n\n" - f"WP Insertion Gap:\n" - f"$\\Delta r$: {mfile.get('dx_tf_wp_insertion_gap', scan=scan):.4f} m" - ) - axis.text( - 0.55, - 0.575, - textstr_wp_insulation, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "green", - "alpha": 1.0, - "linewidth": 2, - }, - ) - - # Add info about the Winding Pack - textstr_wp = ( - f"$\\mathbf{{Winding \\ Pack:}}$\n\n" - f"$N_{{\\text{{turns}}}}$: " - f"{int(mfile.get('n_tf_coil_turns', scan=scan))} turns\n" - f"$r_{{start}} \\rightarrow r_{{end}}$: {mfile.get('r_tf_wp_inboard_inner', scan=scan):.3f} $\\rightarrow$ {mfile.get('r_tf_wp_inboard_outer', scan=scan):.3f} m\n" - f"$\\Delta r$: {mfile.get('dr_tf_wp_with_insulation', scan=scan):.3f} m\n\n" - f"$A$, with insulation: {mfile.get('a_tf_wp_with_insulation', scan=scan):.4f} $\\mathrm{{m}}^2$\n" - f"$A$, no insulation: {mfile.get('a_tf_wp_no_insulation', scan=scan):.4f} $\\mathrm{{m}}^2$\n" - f"$A$, total turn insulation: {mfile.get('a_tf_coil_wp_turn_insulation', scan=scan):.4f} $\\mathrm{{m}}^2$\n" - f"$A$, total turn steel: {mfile.get('a_tf_wp_steel', scan=scan):.4f} $\\mathrm{{m}}^2$\n" - f"$A$, total conductor: {mfile.get('a_tf_wp_conductor', scan=scan):.4f} $\\mathrm{{m}}^2$\n" - f"$A$, total non-cooling void: {mfile.get('a_tf_wp_extra_void', scan=scan):.4f} $\\mathrm{{m}}^2$\n\n" - f"Primary WP:\n" - f"$\\Delta x$: {mfile.get('dx_tf_wp_primary_toroidal', scan=scan):.4f} m\n\n" - f"Secondary WP:\n" - f"$\\Delta x$: {mfile.get('dx_tf_wp_secondary_toroidal', scan=scan):.4f} m\n\n" - f"$J$ no insulation: {mfile.get('j_tf_wp', scan=scan) / 1e6:.4f} MA/m$^2$" - ) - - axis.text( - 0.775, - 0.95, - textstr_wp, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - color="white", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "blue", - "alpha": 1.0, - "linewidth": 2, - }, - ) - - # Add info about the Winding Pack - textstr_general_info = ( - f"$\\mathbf{{General \\ info:}}$\n\n" - f"$N_{{\\text{{TF,coil}}}}$: {mfile.get('n_tf_coils', scan=scan)}\n" - f"Self inductance of single coil: {mfile.get('ind_tf_coil', scan=scan) * 1e6:.4f} $\\mu$H\n" - f"Stored energy of all coils: {mfile.get('e_tf_magnetic_stored_total_gj', scan=scan):.4f} GJ\n" - f"Stored energy of a single coil: {mfile.get('e_tf_coil_magnetic_stored', scan=scan) / 1e9:.2f} GJ\n" - f"Total area of steel in coil: {mfile.get('a_tf_coil_inboard_steel', scan=scan):.4f} $\\mathrm{{m}}^2$\n" - f"Total area fraction of steel: {mfile.get('f_a_tf_coil_inboard_steel', scan=scan):.4f}\n" - f"Total area fraction of insulation: {mfile.get('f_a_tf_coil_inboard_insulation', scan=scan):.4f}\n" - f"$A$, all insulation in coil: {mfile.get('a_tf_coil_inboard_insulation', scan=scan):.4f} $\\mathrm{{m}}^2$\n" - ) - axis.text( - 0.775, - 0.58, - textstr_general_info, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox={ - "boxstyle": "round", - "facecolor": "wheat", - "alpha": 1.0, - "linewidth": 2, - }, - ) - - axis.minorticks_on() - axis.set_xlim(r_tf_inboard_in * 0.8, r_tf_inboard_out * 1.1) - axis.set_ylim((y14[-1] * 1.25), (-y14[-1] * 1.25)) - - axis.set_title("Top-down view of inboard TF coil at midplane") - axis.set_xlabel("Radial distance [m]") - axis.set_ylabel("Toroidal distance [m]") - axis.legend(loc="upper left") - - -def plot_resistive_tf_wp(axis: plt.Axes, mfile: MFile, scan: int, fig): - """Plots inboard TF coil and winding pack. - - Parameters - ---------- - axis : matplotlib.axes object - Axis object to plot to. - mfile : MFILE data object - Object containing data for the plot. - scan : int - Scan number to use. - """ - # Import the TF variables - r_tf_inboard_in = mfile.get("r_tf_inboard_in", scan=scan) - r_tf_inboard_out = mfile.get("r_tf_inboard_out", scan=scan) - - r_tf_wp_inboard_inner = mfile.get("r_tf_wp_inboard_inner", scan=scan) - i_tf_case_geom = mfile.get("i_tf_case_geom", scan=scan) - b_tf_inboard_peak_symmetric = mfile.get("b_tf_inboard_peak_symmetric", scan=scan) - r_b_tf_inboard_peak = mfile.get("r_b_tf_inboard_peak", scan=scan) - r_tf_wp_inboard_outer = mfile.get("r_tf_wp_inboard_outer", scan=scan) - r_tf_wp_inboard_centre = mfile.get("r_tf_wp_inboard_centre", scan=scan) - dx_tf_wp_insulation = mfile.get("dx_tf_wp_insulation", scan=scan) - - axis.add_patch( - Circle( - (0, 0), - r_tf_inboard_in, - facecolor="none", - edgecolor="black", - linestyle="--", - ), - ) - - if i_tf_case_geom == TFPlasmaCaseType.CIRCULAR: - axis.add_patch( - Circle( - (0, 0), - r_tf_inboard_out, - facecolor="none", - edgecolor="black", - linestyle="--", - ), - ) - - # Equations for plotting the TF case - rad_tf_coil_inboard_toroidal_half = mfile.get( - "rad_tf_coil_inboard_toroidal_half", scan=scan - ) - - # X points for inboard case curve - x11 = r_tf_inboard_in * np.cos( - np.linspace( - rad_tf_coil_inboard_toroidal_half, - -rad_tf_coil_inboard_toroidal_half, - 256, - endpoint=True, - ) - ) - # Y points for inboard case curve - y11 = r_tf_inboard_in * np.sin( - np.linspace( - rad_tf_coil_inboard_toroidal_half, - -rad_tf_coil_inboard_toroidal_half, - 256, - endpoint=True, - ) - ) - # Check for plasma side case type - if i_tf_case_geom == TFPlasmaCaseType.CIRCULAR: - # Rounded case - - # X points for outboard case curve - x12 = r_tf_inboard_out * np.cos( - np.linspace( - rad_tf_coil_inboard_toroidal_half, - -rad_tf_coil_inboard_toroidal_half, - 256, - endpoint=True, - ) - ) - - elif i_tf_case_geom == TFPlasmaCaseType.STRAIGHT: - # Flat case - - # X points for outboard case - x12 = np.full(256, r_tf_inboard_out) - else: - raise NotImplementedError("i_tf_case_geom must be 0 or 1") - - # Y points for outboard case - y12 = r_tf_inboard_out * np.sin( - np.linspace( - rad_tf_coil_inboard_toroidal_half, - -rad_tf_coil_inboard_toroidal_half, - 256, - endpoint=True, - ) - ) - - # Cordinates of the top and bottom of case curves, - # used to plot the lines connecting the inside and outside of the case - y13 = [y11[0], y12[0]] - x13 = [x11[0], x12[0]] - y14 = [y11[-1], y12[-1]] - x14 = [x11[-1], x12[-1]] - - # Plot the case outline - axis.plot(x11, y11, color="black") - axis.plot(x12, y12, color="black") - axis.plot(x13, y13, color="black") - axis.plot(x14, y14, color="black") - - # Fill in the case segemnts - - # Upper main - if i_tf_case_geom == TFPlasmaCaseType.CIRCULAR: - axis.fill_between( - [ - (r_tf_inboard_in * np.cos(rad_tf_coil_inboard_toroidal_half)), - (r_tf_inboard_out * np.cos(rad_tf_coil_inboard_toroidal_half)), - ], - y13, - color="grey", - alpha=0.25, - ) - # Lower main - axis.fill_between( - [ - (r_tf_inboard_in * np.cos(rad_tf_coil_inboard_toroidal_half)), - (r_tf_inboard_out * np.cos(rad_tf_coil_inboard_toroidal_half)), - ], - y14, - color="grey", - alpha=0.25, - ) - axis.fill_between( - x12, - y12, - color="grey", - alpha=0.25, - ) - elif i_tf_case_geom == TFPlasmaCaseType.STRAIGHT: - axis.fill_between( - [ - (r_tf_inboard_in * np.cos(rad_tf_coil_inboard_toroidal_half)), - (r_tf_inboard_out), - ], - y13, - color="grey", - alpha=0.25, - ) - # Lower main - axis.fill_between( - [ - (r_tf_inboard_in * np.cos(rad_tf_coil_inboard_toroidal_half)), - (r_tf_inboard_out), - ], - y14, - color="grey", - alpha=0.25, - ) - - # Removes ovelapping colours on inner nose case - axis.fill_between( - x11, - y11, - color="white", - alpha=1.0, - ) - - # Centre line for relative reference - axis.axhline(y=0.0, color="r", linestyle="--", linewidth=0.25) - - # ================================================================ - - # Plot the WP insulation - - # X points for inboard insulation curve - x11 = r_tf_wp_inboard_inner * np.cos( - np.linspace( - rad_tf_coil_inboard_toroidal_half, - -rad_tf_coil_inboard_toroidal_half, - 500, - endpoint=True, - ) - ) - # Y points for inboard insulation curve - y11 = r_tf_wp_inboard_inner * np.sin( - np.linspace( - rad_tf_coil_inboard_toroidal_half, - -rad_tf_coil_inboard_toroidal_half, - 500, - endpoint=True, - ) - ) - - # X points for outboard insulation curve - x12 = r_tf_wp_inboard_outer * np.cos( - np.linspace( - rad_tf_coil_inboard_toroidal_half, - -rad_tf_coil_inboard_toroidal_half, - 500, - endpoint=True, - ) - ) - - # Y points for outboard insulation curve - y12 = r_tf_wp_inboard_outer * np.sin( - np.linspace( - rad_tf_coil_inboard_toroidal_half, - -rad_tf_coil_inboard_toroidal_half, - 500, - endpoint=True, - ) - ) - - # Cordinates of the top and bottom of WP insulation curves, - y13 = [y11[0], y12[0]] - x13 = [x11[0], x12[0]] - y14 = [y11[-1], y12[-1]] - x14 = [x11[-1], x12[-1]] - - # Plot the insualtion outline - axis.plot(x11, y11, color="black") - axis.plot(x12, y12, color="black") - axis.plot(x13, y13, color="black") - axis.plot(x14, y14, color="black") - - # Upper main - if i_tf_case_geom == TFPlasmaCaseType.CIRCULAR: - axis.fill_between( - [ - (r_tf_wp_inboard_inner * np.cos(rad_tf_coil_inboard_toroidal_half)), - (r_tf_wp_inboard_outer * np.cos(rad_tf_coil_inboard_toroidal_half)), - ], - y13, - color="green", - ) - # Lower main - axis.fill_between( - [ - (r_tf_wp_inboard_inner * np.cos(rad_tf_coil_inboard_toroidal_half)), - (r_tf_wp_inboard_outer * np.cos(rad_tf_coil_inboard_toroidal_half)), - ], - y14, - color="green", - ) - axis.fill_between( - x12, - y12, - color="green", - ) - - # ================================================================ - - # Plot the WP - - # The winding pack should be inside the insulation, so subtract dx_tf_wp_insulation from both the inner and outer radii. - # The angular extent should also be reduced by the insulation thickness, i.e., the winding pack does not extend all the way to the top/bottom. - - # Calculate the reduced angle for the winding pack (subtract insulation thickness in arc length, convert to angle) - # arc_length = r * angle => angle = arc_length / r - # So, for both inner and outer radii, compute the angle offset due to insulation thickness - angle_offset_inner = ( - dx_tf_wp_insulation / r_tf_wp_inboard_inner if r_tf_wp_inboard_inner > 0 else 0 - ) - angle_offset_outer = ( - dx_tf_wp_insulation / r_tf_wp_inboard_outer if r_tf_wp_inboard_outer > 0 else 0 - ) - - # Use the maximum angle offset to ensure the winding pack stays within the insulation - angle_offset = max(angle_offset_inner, angle_offset_outer) - - # Define the angular range for the winding pack - theta_start = rad_tf_coil_inboard_toroidal_half - angle_offset - theta_end = -rad_tf_coil_inboard_toroidal_half + angle_offset - theta_vals = np.linspace(theta_start, theta_end, 256, endpoint=True) - - # X and Y points for inboard and outboard winding pack curves - x11 = (r_tf_wp_inboard_inner + dx_tf_wp_insulation) * np.cos(theta_vals) - y11 = (r_tf_wp_inboard_inner + dx_tf_wp_insulation) * np.sin(theta_vals) - x12 = (r_tf_wp_inboard_outer - dx_tf_wp_insulation) * np.cos(theta_vals) - y12 = (r_tf_wp_inboard_outer - dx_tf_wp_insulation) * np.sin(theta_vals) - - # Cordinates of the top and bottom of WP curves, - y13 = [y11[0], y12[0]] - x13 = [x11[0], x12[0]] - y14 = [y11[-1], y12[-1]] - x14 = [x11[-1], x12[-1]] - - # Plot the winding pack outline - axis.plot(x11, y11, color="black") - axis.plot(x12, y12, color="black") - axis.plot(x13, y13, color="black") - axis.plot(x14, y14, color="black") - - # Choose color based on i_tf_sup: copper for resistive, aluminium for cryo - # light steel blue (cryo aluminium) or copper color - wp_color = "#b0c4de" if mfile.get("i_tf_sup", scan=scan) == 2 else "#b87333" - - axis.fill_between( - [ - (r_tf_wp_inboard_inner + dx_tf_wp_insulation) * np.cos(theta_vals[0]), - (r_tf_wp_inboard_outer - dx_tf_wp_insulation) * np.cos(theta_vals[0]), - ], - y13, - color=wp_color, - ) - # Lower main - axis.fill_between( - [ - (r_tf_wp_inboard_inner + dx_tf_wp_insulation) * np.cos(theta_vals[-1]), - (r_tf_wp_inboard_outer - dx_tf_wp_insulation) * np.cos(theta_vals[-1]), - ], - y14, - color=wp_color, - ) - axis.fill_between(x12, y12, color=wp_color) - - # ================================================================ - - # Divide the winding pack into toroidal segments based on n_tf_coil_turns - n_turns = int(mfile.get("n_tf_coil_turns", scan=scan)) - if n_turns > 0: - # Calculate the angular extent for each turn - theta_start = rad_tf_coil_inboard_toroidal_half - angle_offset - theta_end = -rad_tf_coil_inboard_toroidal_half + angle_offset - theta_vals = np.linspace(theta_start, theta_end, 256, endpoint=True) - - # For each turn, plot a radial line at the corresponding angle - turn_angles = np.linspace(theta_start, theta_end, n_turns + 1) - for t in range(1, n_turns): - angle = turn_angles[t] - # Inner and outer points for this turn - x_in = (r_tf_wp_inboard_inner + dx_tf_wp_insulation) * np.cos(angle) - y_in = (r_tf_wp_inboard_inner + dx_tf_wp_insulation) * np.sin(angle) - x_out = (r_tf_wp_inboard_outer - dx_tf_wp_insulation) * np.cos(angle) - y_out = (r_tf_wp_inboard_outer - dx_tf_wp_insulation) * np.sin(angle) - axis.plot( - [x_in, x_out], - [y_in, y_out], - color="white", - linewidth=0.5, - linestyle="--", - ) - - # ================================================================ - - # Plot a dot for the location of the peak field - axis.plot( - r_b_tf_inboard_peak, - 0, - marker="o", - color="red", - label=f"Peak Field: {b_tf_inboard_peak_symmetric:.2f} T\nr={r_b_tf_inboard_peak:.3f} m", - ) - - x_kwargs = {"color": "black", "linestyle": "--", "linewidth": 0.6, "alpha": 0.5} - axis.axvline(x=r_tf_inboard_in, **x_kwargs) - axis.axvline(x=r_tf_wp_inboard_inner, **x_kwargs) - axis.axvline(x=r_tf_wp_inboard_outer, **x_kwargs) - axis.axvline(x=r_tf_wp_inboard_centre, **x_kwargs) - axis.axvline(x=r_tf_inboard_out, **x_kwargs) - - axis.minorticks_on() - axis.set_xlim(0.0, r_tf_inboard_out * 1.1) - axis.set_ylim((y14[-1] * 1.65), (-y14[-1] * 1.65)) - - axis.set_title("Top-down view of inboard TF coil at midplane") - axis.set_xlabel("Radial distance [m]") - axis.set_ylabel("Toroidal distance [m]") - axis.legend(loc="upper left") - - axis.text( - 0.05, - 0.975, - "*Turn insulation and cooling pipes not shown", - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - color="black", - transform=fig.transFigure, - ) - - -def plot_resistive_tf_info(axis: plt.Axes, mfile: MFile, scan: int, fig): - """Plot info about the resistive TF coils""" - # Add info about the steel casing surrounding the WP - textstr_casing = ( - f"$\\mathbf{{Casing:}}$\n\n" - f"Coil half angle: {mfile.get('rad_tf_coil_inboard_toroidal_half', scan=scan):.3f} radians\n\n" - f"$\\text{{Full Coil Case:}}$\n" - f"$r_{{start}} \\rightarrow r_{{end}}$: {mfile.get('r_tf_inboard_in', scan=scan):.3f} $\\rightarrow$ {mfile.get('r_tf_inboard_out', scan=scan):.3f} m\n" - f"$\\Delta r$: {mfile.get('dr_tf_inboard', scan=scan):.3f} m\n" - f"Area of casing around WP: {mfile.get('a_tf_coil_inboard_case', scan=scan):.3f} $\\mathrm{{m}}^2$\n\n" - f"$\\text{{Nose Case:}}$\n" - f"$r_{{start}} \\rightarrow r_{{end}}$: {mfile.get('r_tf_inboard_in', scan=scan):.3f} $\\rightarrow$ {mfile.get('r_tf_wp_inboard_inner', scan=scan):.3f} m\n" - f"$\\Delta r$: {mfile.get('dr_tf_nose_case', scan=scan):.3f} m\n" - f"$A$: {mfile.get('a_tf_coil_nose_case', scan=scan):.4f} $\\mathrm{{m}}^2$\n\n" - f"$\\text{{Plasma Case:}}$\n" - f"$r_{{start}} \\rightarrow r_{{end}}$: {mfile.get('r_tf_wp_inboard_outer', scan=scan):.3f} $\\rightarrow$ {mfile.get('r_tf_inboard_out', scan=scan):.3f} m\n" - f"$\\Delta r$: {mfile.get('dr_tf_plasma_case', scan=scan):.3f} m\n" - f"$A$: {mfile.get('a_tf_plasma_case', scan=scan):.3f} $\\mathrm{{m}}^2$" - ) - axis.text( - 0.775, - 0.925, - textstr_casing, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=_box_style("grey"), - ) - - # Add info about the steel casing surrounding the WP - textstr_wp_insulation = ( - f"$\\mathbf{{Insulation:}}$\n\n" - f"Area of insulation around WP: {mfile.get('a_tf_wp_ground_insulation', scan=scan):.3f} $\\mathrm{{m}}^2$\n" - f"$\\Delta r$: {mfile.get('dx_tf_wp_insulation', scan=scan):.4f} m\n\n" - f"$\\text{{Turn Insulation:}}$\n" - f"$\\Delta r$: {mfile.get('dx_tf_turn_insulation', scan=scan):.4f} m" - ) - axis.text( - 0.775, - 0.62, - textstr_wp_insulation, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox={"boxstyle": "round", "facecolor": "green", "alpha": 1.0, "linewidth": 2}, - ) - - # Add info about the Winding Pack - textstr_wp = ( - f"$\\mathbf{{Winding Pack:}}$\n\n" - f"$N_{{\\text{{turns}}}}$: " - f"{int(mfile.get('n_tf_coil_turns', scan=scan))} turns\n" - f"$r_{{start}} \\rightarrow r_{{end}}$: {mfile.get('r_tf_wp_inboard_inner', scan=scan):.3f} $\\rightarrow$ {mfile.get('r_tf_wp_inboard_outer', scan=scan):.3f} m\n" - f"$\\Delta r$: {mfile.get('dr_tf_wp_with_insulation', scan=scan):.3f} m\n" - f"$A$, with insulation: {mfile.get('a_tf_wp_with_insulation', scan=scan):.3f} $\\mathrm{{m}}^2$\n" - f"$A$, no insulation: {mfile.get('a_tf_wp_no_insulation', scan=scan):.3f} $\\mathrm{{m}}^2$\n\n" - f"Current per turn: {mfile.get('c_tf_turn', scan=scan) / 1e3:.3f} $\\mathrm{{kA}}$\n" - f"Resistive conductor per coil: {mfile.get('a_res_tf_coil_conductor', scan=scan):.3f} $\\mathrm{{m}}^2$\n" - f"Coolant area void fraction per turn: {mfile.get('fcoolcp', scan=scan):.3f}" - ) - axis.text( - 0.77, - 0.475, - textstr_wp, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - color="white", - transform=fig.transFigure, - bbox={"boxstyle": "round", "facecolor": "blue", "alpha": 1.0, "linewidth": 2}, - ) - - # Add info about the Winding Pack - textstr_general_info = ( - f"$\\mathbf{{General \\ info:}}$\n\n" - f"Self inductance: {mfile.get('ind_tf_coil', scan=scan) * 1e6:.4f} $\\mu$H\n" - f"Stored energy of all coils: {mfile.get('e_tf_magnetic_stored_total_gj', scan=scan):.4f} GJ\n" - ) - axis.text( - 0.55, - 0.475, - textstr_general_info, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 1.0, "linewidth": 2}, - ) - - # Add info about the Winding Pack - textstr_cooling = ( - f"$\\mathbf{{Cooling \\ info:}}$\n\n" - f"Coolant inlet temperature: {mfile.get('temp_cp_coolant_inlet', scan=scan):.2f} K\n" - f"Coolant temperature rise: {mfile.get('dtemp_cp_coolant', scan=scan):.2f} K\n" - f"Coolant velocity: {mfile.get('vel_cp_coolant_midplane', scan=scan):.2f} $\\mathrm{{ms^{{-1}}}}$\n\n" - f"Average CP temperature: {mfile.get('temp_cp_average', scan=scan):.2f} K\n" - f"CP resistivity: {mfile.get('rho_cp', scan=scan):.2e} $\\Omega \\mathrm{{m}}$\n" - f"Leg resistivity: {mfile.get('rho_tf_leg', scan=scan):.2e} $\\Omega \\mathrm{{m}}$\n" - f"Leg resistance: {mfile.get('res_tf_leg', scan=scan):.2e} $\\Omega$\n" - f"CP resistive losses: {mfile.get('p_cp_resistive', scan=scan):,.2f} $\\mathrm{{W}}$\n" - f"Leg resistive losses: {mfile.get('p_tf_leg_resistive', scan=scan):,.2f} $\\mathrm{{W}}$\n" - f"Joints resistive losses: {mfile.get('p_tf_joints_resistive', scan=scan):,.2f} $\\mathrm{{W}}$\n" - ) - axis.text( - 0.55, - 0.35, - textstr_cooling, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=_box_style("wheat"), - ) - - -def plot_tf_cable_in_conduit_turn(axis: plt.Axes, fig, mfile: MFile, scan: int): - """Plots inboard TF coil CICC individual turn structure. - - Parameters - ---------- - axis : matplotlib.axes object - Axis object to plot to. - mfile : MFILE data object - Object containing data for the plot. - scan : int - Scan number to use. - """ - - def _pack_strands_rectangular_with_obstacles( - cable_space_bounds, - pipe_center, - pipe_radius, - strand_diameter, - void_fraction, - n_strands, - axis, - corner_radius, - f_a_tf_turn_cable_copper, - ): - """Pack circular strands in rectangular space with cooling pipe obstacle - - Parameters - ---------- - cable_space_bounds : - - pipe_center : - - pipe_radius : - - strand_diameter : - - void_fraction : - - n_strands : - - axis : - - corner_radius : - - f_a_tf_turn_cable_copper : - - """ - x, y, width, height = cable_space_bounds - - radius = strand_diameter / 2 - placed_strands = [] - attempts = 0 - - pipe_x, pipe_y = pipe_center - - # Hexagonal packing parameters - # Calculate the spacing between strand centers for the desired void fraction - # For hexagonal packing, packing fraction = pi/(2*sqrt(3)) ~ 0.9069 - # To achieve a lower packing fraction (higher void fraction), increase spacing - ideal_packing_fraction = np.pi / (2 * np.sqrt(3)) - target_packing_fraction = 1 - void_fraction - spacing_factor = np.sqrt(ideal_packing_fraction / target_packing_fraction) - strand_spacing = strand_diameter * spacing_factor - - # Number of rows and columns that fit in the cable space - n_rows = int((height - 2 * radius) // (strand_spacing * np.sqrt(3) / 2)) - n_cols = int((width - 2 * radius) // strand_spacing) - - # Calculate the radius of the inner superconductor circle based on the copper area fraction - # Area_superconductor = (1 - f_a_tf_turn_cable_copper) * Area_strand - # Area_strand = pi * radius^2 - # So, radius_superconductor = sqrt(1 - f_a_tf_turn_cable_copper) * radius - radius_superconductor = np.sqrt(1 - f_a_tf_turn_cable_copper) * radius - - # Generate hexagonal grid positions - for row in range(n_rows): - y_pos = (y + radius + row * strand_spacing * np.sqrt(3) / 2) * 1.07 - x_offset = strand_spacing / 2 if row % 2 else 0 - for col in range(n_cols): - candidate_x = (x + radius + col * strand_spacing + x_offset) * 1.05 - candidate_y = y_pos - - # Check if within bounds - if candidate_x > x + width - radius or candidate_y > y + height - radius: - continue - - # Check collision with cooling pipe - pipe_distance = np.sqrt( - (candidate_x - pipe_x) ** 2 + (candidate_y - pipe_y) ** 2 - ) - if pipe_distance < (pipe_radius + radius): - continue - - # Check collision with corners if rounded - if corner_radius > 0: - corners = [ - (x + corner_radius, y + corner_radius), # bottom-left - (x + width - corner_radius, y + corner_radius), # bottom-right - ( - x + width - corner_radius, - y + height - corner_radius, - ), # top-right - (x + corner_radius, y + height - corner_radius), # top-left - ] - if ( - ( - candidate_x < corners[0][0] - and candidate_y < corners[0][1] - and np.sqrt( - (candidate_x - corners[0][0]) ** 2 - + (candidate_y - corners[0][1]) ** 2 - ) - > corner_radius - radius - ) - or ( - candidate_x > corners[1][0] - and candidate_y < corners[1][1] - and np.sqrt( - (candidate_x - corners[1][0]) ** 2 - + (candidate_y - corners[1][1]) ** 2 - ) - > corner_radius - radius - ) - or ( - candidate_x > corners[2][0] - and candidate_y > corners[2][1] - and np.sqrt( - (candidate_x - corners[2][0]) ** 2 - + (candidate_y - corners[2][1]) ** 2 - ) - > corner_radius - radius - ) - or ( - candidate_x < corners[3][0] - and candidate_y > corners[3][1] - and np.sqrt( - (candidate_x - corners[3][0]) ** 2 - + (candidate_y - corners[3][1]) ** 2 - ) - > corner_radius - radius - ) - ): - continue - - # Check collision with existing strands - collision = False - for existing_x, existing_y in placed_strands: - distance = np.sqrt( - (candidate_x - existing_x) ** 2 + (candidate_y - existing_y) ** 2 - ) - if distance < strand_diameter: - collision = True - break - - if not collision: - placed_strands.append((candidate_x, candidate_y)) - # Plot the strand - circle_copper_surrounding = Circle( - (candidate_x, candidate_y), - radius, - facecolor="#b87333", # copper color - edgecolor="#8B4000", # darker copper edge - linewidth=0.1, - alpha=0.8, - ) - axis.add_patch(circle_copper_surrounding) - - circle_central_conductor = Circle( - (candidate_x, candidate_y), - radius_superconductor, - facecolor="black", - linewidth=0.3, - alpha=0.5, - ) - axis.add_patch(circle_central_conductor) - - if len(placed_strands) >= n_strands: - break - if len(placed_strands) >= n_strands: - break - - attempts = n_rows * n_cols - - return len(placed_strands), attempts - - # Import the TF turn variables then multiply into mm - i_tf_turns_integer = mfile.get("i_tf_turns_integer", scan=scan) - # If integer turns switch is on then the turns can have non square dimensions - if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.INTEGER: - turn_width = mfile.get("dr_tf_turn", scan=scan) - turn_height = mfile.get("dx_tf_turn", scan=scan) - cable_space_width_radial = mfile.get("dr_tf_turn_cable_space", scan=scan) - cable_space_width_toroidal = mfile.get("dx_tf_turn_cable_space", scan=scan) - - elif TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.NON_INTEGER: - turn_width = mfile.get("dx_tf_turn_general", scan=scan) - cable_space_width = mfile.get("dx_tf_turn_cable_space_average", scan=scan) - - he_pipe_diameter = mfile.get("dia_tf_turn_coolant_channel", scan=scan) - steel_thickness = mfile.get("dx_tf_turn_steel", scan=scan) - insulation_thickness = mfile.get("dx_tf_turn_insulation", scan=scan) - - a_tf_turn_cable_space_no_void = mfile.get("a_tf_turn_cable_space_no_void", scan=scan) - radius_tf_turn_cable_space_corners = mfile.get( - "radius_tf_turn_cable_space_corners", scan=scan - ) - - a_tf_wp_coolant_channels = mfile.get("a_tf_wp_coolant_channels", scan=scan) - - f_a_tf_turn_cable_space_extra_void = mfile.get( - "f_a_tf_turn_cable_space_extra_void", scan=scan - ) - a_tf_turn_steel = mfile.get("a_tf_turn_steel", scan=scan) - a_tf_turn_cable_space_effective = mfile.get( - "a_tf_turn_cable_space_effective", scan=scan - ) - - # Plot the total turn shape - if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.NON_INTEGER: - axis.add_patch( - Rectangle( - (0, 0), - turn_width, - turn_width, - facecolor="red", - edgecolor="black", - ), - ) - # Plot the steel conduit - axis.add_patch( - Rectangle( - (insulation_thickness, insulation_thickness), - (turn_width - 2 * insulation_thickness), - (turn_width - 2 * insulation_thickness), - facecolor="grey", - edgecolor="black", - ), - ) - - # Plot the cable space with rounded corners - axis.add_patch( - patches.FancyBboxPatch( - ( - insulation_thickness + steel_thickness, - insulation_thickness + steel_thickness, - ), - (turn_width - 2 * (insulation_thickness + steel_thickness)), - (turn_width - 2 * (insulation_thickness + steel_thickness)), - boxstyle=patches.BoxStyle( - "Round", pad=0, rounding_size=radius_tf_turn_cable_space_corners - ), - facecolor="royalblue", - edgecolor="black", - ), - ) - - # Plot dashed line around the cable space - axis.add_patch( - Rectangle( - ( - insulation_thickness + steel_thickness, - insulation_thickness + steel_thickness, - ), - (turn_width - 2 * (insulation_thickness + steel_thickness)), - (turn_width - 2 * (insulation_thickness + steel_thickness)), - facecolor="none", - edgecolor="black", - linestyle="--", - linewidth=1.2, - alpha=0.5, - ), - ) - # Plot the coolant channel - axis.add_patch( - Circle( - ((turn_width / 2), (turn_width / 2)), - he_pipe_diameter / 2, - facecolor="white", - edgecolor="black", - ), - ) - - # Cable strand packing parameters - strand_diameter = mfile.get("dia_tf_turn_superconducting_cable", scan=scan) - void_fraction = mfile.get("f_a_tf_turn_cable_space_extra_void", scan=scan) - - # Cable space bounds - cable_bounds = [ - insulation_thickness + steel_thickness, - insulation_thickness + steel_thickness, - turn_width - 2 * (insulation_thickness + steel_thickness), - turn_width - 2 * (insulation_thickness + steel_thickness), - ] - - # Pack strands if significant void fraction - if void_fraction > 0.001: - _n_strands, _attempts = _pack_strands_rectangular_with_obstacles( - cable_space_bounds=cable_bounds, - pipe_center=( - turn_width / 2, - ( - turn_width - if TFWPIntegerTurnType(i_tf_turns_integer) - == TFWPIntegerTurnType.NON_INTEGER - else turn_height - ) - / 2, - ), - pipe_radius=he_pipe_diameter / 2, - strand_diameter=strand_diameter, - void_fraction=void_fraction, - axis=axis, - corner_radius=radius_tf_turn_cable_space_corners, - n_strands=mfile.get("n_tf_turn_superconducting_cables", scan=scan), - f_a_tf_turn_cable_copper=mfile.get( - "f_a_tf_turn_cable_copper", scan=scan - ), - ) - - axis.set_xlim(-turn_width * 0.05, turn_width * 1.05) - axis.set_ylim(-turn_width * 0.05, turn_width * 1.05) - - # Non square turns - elif TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.INTEGER: - axis.add_patch( - Rectangle( - (0, 0), - turn_width, - turn_height, - facecolor="red", - edgecolor="black", - ), - ) - - # Plot the steel conduit - axis.add_patch( - Rectangle( - (insulation_thickness, insulation_thickness), - (turn_width - 2 * insulation_thickness), - (turn_height - 2 * insulation_thickness), - facecolor="grey", - edgecolor="black", - ), - ) - - # Plot the cable space with rounded corners - axis.add_patch( - patches.FancyBboxPatch( - ( - insulation_thickness + steel_thickness, - insulation_thickness + steel_thickness, - ), - (turn_width - 2 * (insulation_thickness + steel_thickness)), - (turn_height - 2 * (insulation_thickness + steel_thickness)), - boxstyle=patches.BoxStyle( - "Round", pad=0, rounding_size=radius_tf_turn_cable_space_corners - ), - facecolor="royalblue", - edgecolor="black", - ), - ) - # Plot dashed line around the cable space - axis.add_patch( - Rectangle( - ( - insulation_thickness + steel_thickness, - insulation_thickness + steel_thickness, - ), - (turn_width - 2 * (insulation_thickness + steel_thickness)), - (turn_height - 2 * (insulation_thickness + steel_thickness)), - facecolor="none", - edgecolor="black", - linestyle="--", - linewidth=1.0, - alpha=0.5, - ), - ) - axis.add_patch( - Circle( - ((turn_width / 2), (turn_height / 2)), - he_pipe_diameter / 2, - facecolor="white", - edgecolor="black", - ), - ) - - # Cable space bounds - cable_bounds = [ - insulation_thickness + steel_thickness, - insulation_thickness + steel_thickness, - turn_width - 2 * (insulation_thickness + steel_thickness), - turn_height - 2 * (insulation_thickness + steel_thickness), - ] - - # Cable strand packing parameters - strand_diameter = mfile.get("dia_tf_turn_superconducting_cable", scan=scan) - void_fraction = mfile.get("f_a_tf_turn_cable_space_extra_void", scan=scan) - - # Pack strands if significant void fraction - if void_fraction > 0.001: - _, _ = _pack_strands_rectangular_with_obstacles( - cable_space_bounds=cable_bounds, - pipe_center=( - turn_width / 2, - turn_height / 2, - ), - pipe_radius=he_pipe_diameter / 2, - strand_diameter=strand_diameter, - void_fraction=void_fraction, - axis=axis, - corner_radius=radius_tf_turn_cable_space_corners, - n_strands=mfile.get("n_tf_turn_superconducting_cables", scan=scan), - f_a_tf_turn_cable_copper=mfile.get( - "f_a_tf_turn_cable_copper", scan=scan - ), - ) - - axis.set_xlim(-turn_width * 0.05, turn_width * 1.05) - axis.set_ylim(-turn_height * 0.05, turn_height * 1.05) - - axis.minorticks_on() - axis.set_title("WP Turn Structure") - axis.set_xlabel("r [m]") - axis.set_ylabel("x [m]") - - # Add info about the steel casing surrounding the WP - textstr_turn_insulation = ( - f"$\\mathbf{{Turn \\ Insulation:}}$\n\n$\\Delta r:${insulation_thickness:.3e} m" - ) - - axis.text( - 0.4, - 0.9, - textstr_turn_insulation, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=_box_style("red"), - ) - - # Add info about the steel casing surrounding the WP - textstr_turn_steel = ( - f"$\\mathbf{{Steel \\ Conduit:}}$\n\n$\\Delta r:${steel_thickness:.3e} m\n" - f"$A$: {a_tf_turn_steel:.3e} m$^2$" - ) - - axis.text( - 0.65, - 0.9, - textstr_turn_steel, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=_box_style("grey"), - ) - - if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.NON_INTEGER: - # Add info about the steel casing surrounding the WP - textstr_turn_cable_space = ( - f"$\\mathbf{{Cable \\ Space:}}$\n\n" - f"$\\Delta r:$ {cable_space_width:.3e} m\n" - f"Corner radius, $r$: {radius_tf_turn_cable_space_corners:.3e} m\n" - f"Cable area with no cooling\nchannel or gaps: {a_tf_turn_cable_space_no_void:.3e} m$^2$\n" - f"Extra cable space area void fraction: {f_a_tf_turn_cable_space_extra_void}\n" - f"True cable space area: {a_tf_turn_cable_space_effective:.3e} m$^2$" - ) - elif TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.INTEGER: - textstr_turn_cable_space = ( - f"$\\mathbf{{Cable \\ Space:}}$\n\n" - f"Cable space:\n$\\Delta r$: {cable_space_width_radial:.3e} m\n" - f"$\\Delta x$: {cable_space_width_toroidal:.3e} m\n" - f"Corner radius, $r$: {radius_tf_turn_cable_space_corners:.3e} m\n" - f"Cable area with no cooling channel or gaps: {a_tf_turn_cable_space_no_void:.3e} m$^2$\n" - f"Extra cable space area void fraction: {f_a_tf_turn_cable_space_extra_void}\n" - f"True cable space area: {a_tf_turn_cable_space_effective:.3e} m$^2$" - ) - - axis.text( - 0.40, - 0.7, - textstr_turn_cable_space, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=_box_style("royalblue"), - ) - - if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.NON_INTEGER: - textstr_turn = ( - f"$\\mathbf{{Turn:}}$\n\n" - f"$\\Delta r$: {turn_width:.3e} m\n" - f"$\\Delta x$: {turn_width:.3e} m" - ) - - if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.INTEGER: - textstr_turn = ( - f"$\\mathbf{{Turn:}}$\n\n" - f"$\\Delta r$: {turn_width:.3e} m\n" - f"$\\Delta x$: {turn_height:.3e} m" - ) - - axis.text( - 0.525, - 0.9, - textstr_turn, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=_box_style("wheat"), - ) - - # Add info about the steel casing surrounding the WP - textstr_turn_cooling = ( - f"$\\mathbf{{Cooling:}}$\n\n" - f"$\\varnothing$: {he_pipe_diameter:.3e} m\n" - f"Total area of all coolant channels: {a_tf_wp_coolant_channels:.4f} m$^2$" - ) - - axis.text( - 0.45, - 0.8, - textstr_turn_cooling, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=_box_style("white"), - ) - - textstr_superconductor = ( - f"$\\mathbf{{Superconductor:}}$\n\n" - f"Superconductor used:\n" - f"{SuperconductorModel(mfile.get('i_tf_sc_mat', scan=scan)).full_name}\n" - f"Critical field at zero\ntemperature and strain: {mfile.get('b_tf_superconductor_critical_zero_temp_strain', scan=scan):.4f} T\n" - f"Critical temperature at\nzero field and strain: {mfile.get('temp_tf_superconductor_critical_zero_field_strain', scan=scan):.4f} K\n" - f"Temperature at conductor: {mfile.get('tftmp', scan=scan):.4f} K\n" - f"Field at conductor: {mfile.get('b_tf_inboard_peak_with_ripple', scan=scan):.4f} T\n" - f"Superconductor critical current density at\noperating conditions: {mfile.get('j_tf_superconductor_critical', scan=scan):.2e} A/m$^2$\n" - f"$I_{{\\text{{TF,turn critical}}}}$: {mfile.get('c_turn_cables_critical', scan=scan):,.2f} A\n" - f"$I_{{\\text{{TF,turn}}}}$: {mfile.get('c_tf_turn', scan=scan):,.2f} A\n" - f"Critcal current ratio: {mfile.get('f_c_tf_turn_operating_critical', scan=scan):,.4f}\n" - f"Superconductor temperature\nmargin: {mfile.get('temp_tf_superconductor_margin', scan=scan):,.4f} K\n" - f"\n$\\mathbf{{Quench:}}$\n\n" - f"Quench dump time: {mfile.get('t_tf_superconductor_quench', scan=scan):.4f} s\n" - f"Quench detection time: {mfile.get('t_tf_quench_detection', scan=scan):.4f} s\n" - f"User input max temperature\nduring quench: {mfile.get('temp_tf_conductor_quench_max', scan=scan):.2f} K\n" - f"Required maxium WP current\ndensity for heat protection:\n{mfile.get('j_tf_wp_quench_heat_max', scan=scan):.2e} A/m$^2$\n" - ) - axis.text( - 0.75, - 0.9, - textstr_superconductor, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=_box_style("#6dd3f7"), # light blue for superconductors - ) - - -def plot_tf_croco_turn(axis: plt.Axes, fig, mfile: MFile, scan: int): - """Plots inboard TF coil CICC individual turn structure with croco cable layout.""" - # Import the TF turn variables then multiply into mm - i_tf_turns_integer = mfile.get("i_tf_turns_integer", scan=scan) - # If integer turns switch is on then the turns can have non square dimensions - if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.INTEGER: - turn_width = mfile.get("dr_tf_turn", scan=scan) - turn_height = mfile.get("dx_tf_turn", scan=scan) - cable_space_width_radial = mfile.get("dr_tf_turn_cable_space", scan=scan) - cable_space_width_toroidal = mfile.get("dx_tf_turn_cable_space", scan=scan) - - elif TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.NON_INTEGER: - turn_width = mfile.get("dx_tf_turn_general", scan=scan) - cable_space_width = mfile.get("dx_tf_turn_cable_space_average", scan=scan) - - steel_thickness = mfile.get("dx_tf_turn_steel", scan=scan) - insulation_thickness = mfile.get("dx_tf_turn_insulation", scan=scan) - - a_tf_turn_cable_space_no_void = mfile.get("a_tf_turn_cable_space_no_void", scan=scan) - radius_tf_turn_cable_space_corners = mfile.get( - "radius_tf_turn_cable_space_corners", scan=scan - ) - - a_tf_wp_coolant_channels = mfile.get("a_tf_wp_coolant_channels", scan=scan) - - f_a_tf_turn_cable_space_extra_void = mfile.get( - "f_a_tf_turn_cable_space_extra_void", scan=scan - ) - a_tf_turn_steel = mfile.get("a_tf_turn_steel", scan=scan) - a_tf_turn_cable_space_effective = mfile.get( - "a_tf_turn_cable_space_effective", scan=scan - ) - - he_pipe_diameter = mfile.get("dia_tf_turn_coolant_channel", scan=scan) - dia_tf_turn_croco_cable = mfile.get("dia_tf_turn_croco_cable", scan=scan) - - # Plot the total turn shape - if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.NON_INTEGER: - axis.add_patch( - Rectangle( - (0, 0), - turn_width, - turn_width, - facecolor="red", - edgecolor="black", - ), - ) - # Plot the steel conduit - axis.add_patch( - Rectangle( - (insulation_thickness, insulation_thickness), - (turn_width - 2 * insulation_thickness), - (turn_width - 2 * insulation_thickness), - facecolor="grey", - edgecolor="black", - ), - ) - - # Plot the central cable space and copper cylinder - for rad, col in [ - (1.5 * dia_tf_turn_croco_cable, "white"), - (dia_tf_turn_croco_cable / 2, "#B87333"), - ]: - axis.add_patch( - Circle( - ((turn_width / 2), (turn_width / 2)), - rad, - facecolor=col, - edgecolor="black", - linewidth=1.2, - ), - ) - - # Plot six surrounding Croco cables in a hexagonal layout. - center_x = turn_width / 2 - center_y = turn_width / 2 - ring_radius = dia_tf_turn_croco_cable - for angle in np.linspace(0, 2 * np.pi, 6, endpoint=False): - plot_corc_cable_geometry( - axis=axis, - r_centre=center_x + ring_radius * np.cos(angle), - z_centre=center_y + ring_radius * np.sin(angle), - dia_croco_strand=mfile.get("dia_tf_turn_croco_cable", scan=scan), - dx_croco_strand_copper=mfile.get("dx_tf_croco_strand_copper", scan=scan), - dr_hts_tape=mfile.get("dr_tf_hts_tape", scan=scan), - dx_croco_strand_tape_stack=mfile.get( - "dx_tf_croco_strand_tape_stack", scan=scan - ), - n_croco_strand_hts_tapes=mfile.get( - "n_tf_croco_strand_hts_tapes", scan=scan - ), - dx_hts_tape_rebco=mfile.get("dx_tf_hts_tape_rebco", scan=scan), - dx_hts_tape_copper=mfile.get("dx_tf_hts_tape_copper", scan=scan), - dx_hts_tape_hastelloy=mfile.get("dx_tf_hts_tape_hastelloy", scan=scan), - show_legend=False, - ) - - axis.minorticks_on() - axis.set_title("WP Turn Structure") - axis.set_xlim(-turn_width * 0.025, turn_width * 1.025) - axis.set_ylim(-turn_width * 0.025, turn_width * 1.025) - axis.set_aspect("equal", adjustable="box") - axis.set_xlabel("r [m]") - axis.set_ylabel("x [m]") - - # Add info about the steel casing surrounding the WP - textstr_turn_insulation = ( - f"$\\mathbf{{Turn \\ Insulation:}}$\n\n$\\Delta r:${insulation_thickness:.3e} m" - ) - - axis.text( - 0.4, - 0.9, - textstr_turn_insulation, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=_box_style("red"), - ) - - # Add info about the steel casing surrounding the WP - textstr_turn_steel = ( - f"$\\mathbf{{Steel \\ Conduit:}}$\n\n$\\Delta r:${steel_thickness:.3e} m\n" - f"$A$: {a_tf_turn_steel:.3e} m$^2$" - ) - - axis.text( - 0.65, - 0.9, - textstr_turn_steel, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=_box_style("grey"), - ) - - if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.NON_INTEGER: - # Add info about the steel casing surrounding the WP - textstr_turn_cable_space = ( - f"$\\mathbf{{Cable \\ Space:}}$\n\n" - f"$\\Delta r:$ {cable_space_width:.3e} m\n" - f"Corner radius, $r$: {radius_tf_turn_cable_space_corners:.3e} m\n" - f"Cable area with no cooling\nchannel or gaps: {a_tf_turn_cable_space_no_void:.3e} m$^2$\n" - f"Extra cable space area void fraction: {f_a_tf_turn_cable_space_extra_void}\n" - f"True cable space area: {a_tf_turn_cable_space_effective:.3e} m$^2$" - ) - elif TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.INTEGER: - textstr_turn_cable_space = ( - f"$\\mathbf{{Cable \\ Space:}}$\n\n" - f"Cable space:\n$\\Delta r$: {cable_space_width_radial:.3e} m\n" - f"$\\Delta x$: {cable_space_width_toroidal:.3e} m\n" - f"Corner radius, $r$: {radius_tf_turn_cable_space_corners:.3e} m\n" - f"Cable area with no cooling channel or gaps: {a_tf_turn_cable_space_no_void:.3e} m$^2$\n" - f"Extra cable space area void fraction: {f_a_tf_turn_cable_space_extra_void}\n" - f"True cable space area: {a_tf_turn_cable_space_effective:.3e} m$^2$" - ) - - axis.text( - 0.40, - 0.7, - textstr_turn_cable_space, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=_box_style("royalblue"), - ) - - if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.NON_INTEGER: - textstr_turn = ( - f"$\\mathbf{{Turn:}}$\n\n" - f"$\\Delta r$: {turn_width:.3e} m\n" - f"$\\Delta x$: {turn_width:.3e} m" - ) - - if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.INTEGER: - textstr_turn = ( - f"$\\mathbf{{Turn:}}$\n\n" - f"$\\Delta r$: {turn_width:.3e} m\n" - f"$\\Delta x$: {turn_height:.3e} m" - ) - - axis.text( - 0.525, - 0.9, - textstr_turn, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=_box_style("wheat"), - ) - - # Add info about the steel casing surrounding the WP - textstr_turn_cooling = ( - f"$\\mathbf{{Cooling:}}$\n\n" - f"$\\varnothing$: {he_pipe_diameter:.3e} m\n" - f"Total area of all coolant channels: {a_tf_wp_coolant_channels:.4f} m$^2$" - ) - - axis.text( - 0.45, - 0.8, - textstr_turn_cooling, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=_box_style("white"), - ) - - textstr_superconductor = ( - f"$\\mathbf{{Superconductor:}}$\n\n" - f"Superconductor used: {SuperconductorModel(mfile.get('i_tf_sc_mat', scan=scan)).full_name}\n" - f"Critical field at zero\ntemperature and strain: {mfile.get('b_tf_superconductor_critical_zero_temp_strain', scan=scan):.4f} T\n" - f"Critical temperature at\nzero field and strain: {mfile.get('temp_tf_superconductor_critical_zero_field_strain', scan=scan):.4f} K\n" - f"Temperature at conductor: {mfile.get('tftmp', scan=scan):.4f} K\n" - f"Field at conductor: {mfile.get('b_tf_inboard_peak_with_ripple', scan=scan):.4f} T\n" - f"Superconductor critical current density at\noperating conditions: {mfile.get('j_tf_superconductor_critical', scan=scan):.2e} A/m$^2$\n" - f"$I_{{\\text{{TF,turn critical}}}}$: {mfile.get('c_turn_cables_critical', scan=scan):,.2f} A\n" - f"$I_{{\\text{{TF,turn}}}}$: {mfile.get('c_tf_turn', scan=scan):,.2f} A\n" - f"Critcal current ratio: {mfile.get('f_c_tf_turn_operating_critical', scan=scan):,.4f}\n" - f"Superconductor temperature\nmargin: {mfile.get('temp_tf_superconductor_margin', scan=scan):,.4f} K\n" - f"\n$\\mathbf{{Quench:}}$\n\n" - f"Quench dump time: {mfile.get('t_tf_superconductor_quench', scan=scan):.4e} s\n" - f"Quench detection time: {mfile.get('t_tf_quench_detection', scan=scan):.4e} s\n" - f"User input max temperature\nduring quench: {mfile.get('temp_tf_conductor_quench_max', scan=scan):.2f} K\n" - f"Required maxium WP current\ndensity for heat protection:\n{mfile.get('j_tf_wp_quench_heat_max', scan=scan):.2e} A/m$^2$\n" - ) - axis.text( - 0.75, - 0.9, - textstr_superconductor, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=_box_style("#6dd3f7"), - ) - - -def plot_cable_in_conduit_cable(axis: plt.Axes, fig, mfile: MFile, scan: int): - """Plots TF coil CICC cable cross-section. - - Parameters - ---------- - axis: plt.Axes : - - fig : - - mfile: MFile : - - scan: int : - - """ - dia_tf_turn_superconducting_cable = mfile.get( - "dia_tf_turn_superconducting_cable", scan=scan - ) - f_a_tf_turn_cable_copper = mfile.get("f_a_tf_turn_cable_copper", scan=scan) - - # Convert to mm - dia_mm = dia_tf_turn_superconducting_cable * 1000 - radius_superconductor_mm = np.sqrt(1 - f_a_tf_turn_cable_copper) * (dia_mm / 2) - - # Draw the outer copper circle - circle_copper_surrounding = patches.Circle( - (0, 0), - dia_mm / 2, - facecolor="#b87333", # copper color - edgecolor="#8B4000", # darker copper edge - linewidth=0.1, - alpha=0.8, - label="Copper", - zorder=1, - ) - axis.add_patch(circle_copper_surrounding) - - # Draw the inner superconductor circle - circle_central_conductor = patches.Circle( - (0, 0), - radius_superconductor_mm, - facecolor="black", - linewidth=0.3, - alpha=0.7, - label="Superconductor", - zorder=2, - ) - axis.add_patch(circle_central_conductor) - - # Convert cable diameter to mm - cable_diameter_mm = mfile.get("dia_tf_turn_superconducting_cable", scan=scan) * 1000 - # Convert lengths from meters to kilometers for display - len_tf_coil_superconductor_km = ( - mfile.get("len_tf_coil_superconductor", scan=scan) / 1000.0 - ) - len_tf_superconductor_total_km = ( - mfile.get("len_tf_superconductor_total", scan=scan) / 1000.0 - ) - - textstr_cable = ( - f"$\\mathbf{{Cable:}}$\n\n" - f"Cable diameter: {cable_diameter_mm:,.4f} mm\n" - f"Copper area fraction: {mfile.get('f_a_tf_turn_cable_copper', scan=scan):.4f}\n" - f"Number of strands per turn: {int(mfile.get('n_tf_turn_superconducting_cables', scan=scan)):,}\n" - f"Length of superconductor per coil: {len_tf_coil_superconductor_km:,.2f} km\n" - f"Total length of superconductor in all coils: {len_tf_superconductor_total_km:,.2f} km\n" - ) - axis.text( - 0.4, - 0.3, - textstr_cable, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=_box_style("#cccccc"), - ) - - axis.set_aspect("equal") - axis.set_xlim(-dia_mm / 1.5, dia_mm / 1.5) - axis.set_ylim(-dia_mm / 1.5, dia_mm / 1.5) - axis.set_title("TF CICC Cable Cross-Section") - axis.minorticks_on() - axis.legend(loc="upper right") - axis.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.2) - axis.set_xlabel("X [mm]") - axis.set_ylabel("Y [mm]") - - -def plot_pf_coils( - axis: plt.Axes, - mfile: MFile, - scan: int, - colour_scheme: Literal[1, 2], - mirror_negative_x: bool = False, -): - """Function to plot PF coils - - Parameters - ---------- - axis : - axis object to plot to - mfile : - MFILE - scan : - scan number to use - colour_scheme : - colour scheme to use for plots - mirror_negative_x : - if True, mirror the plot to the negative x-axis (Default value = False) - """ - # Apply mirror transformation if requested - x_scale = -1 if mirror_negative_x else 1 - - coils_r = [] - coils_z = [] - coils_dr = [] - coils_dz = [] - coil_text = [] - - dr_cs_bore = mfile.get("dr_cs_bore", scan=scan) - dr_cs = mfile.get("dr_cs", scan=scan) - dz_cs_full = mfile.get("dz_cs_full", scan=scan) - - # Number of coils, both PF and CS - number_of_coils = 0 - for item in mfile.data: - if "r_pf_coil_middle[" in item: - number_of_coils += 1 - - # Check for Central Solenoid - iohcl = mfile.get("iohcl", scan=scan) if "iohcl" in mfile.data else 1 - - # If Central Solenoid present, ignore last entry in for loop - # The last entry will be the OH coil in this case - noc = number_of_coils - 1 if iohcl == 1 else number_of_coils - - for coil in range(noc): - coils_r.append(mfile.get(f"r_pf_coil_middle[{coil + 1:01}]", scan=scan)) - coils_z.append(mfile.get(f"z_pf_coil_middle[{coil + 1:01}]", scan=scan)) - coils_dr.append(mfile.get(f"pfdr({coil + 1:01})", scan=scan)) - coils_dz.append(mfile.get(f"pfdz({coil + 1:01})", scan=scan)) - coil_text.append(str(coil + 1)) - - r_points, z_points, central_coil = pfcoil_geometry( - coils_r=coils_r, - coils_z=coils_z, - coils_dr=coils_dr, - coils_dz=coils_dz, - dr_cs_bore=dr_cs_bore, - dr_cs=dr_cs, - ohdz=dz_cs_full, - ) - - # Plot CS compression structure - r_precomp_outer, r_precomp_inner = cumulative_radial_build2( - "dr_cs_precomp", mfile, scan - ) - axis.add_patch( - patches.Rectangle( - xy=(x_scale * r_precomp_inner, central_coil.anchor_z), - width=(x_scale * (r_precomp_outer - r_precomp_inner)), - height=central_coil.height, - facecolor=CSCOMPRESSION_COLOUR[colour_scheme - 1], - ) - ) - - # Get axis height for fontsize scaling - axis_height = ( - axis - .get_window_extent() - .transformed(axis.figure.dpi_scale_trans.inverted()) - .height - ) - - for i in range(len(coils_r)): - mirrored_r_points = [x_scale * r for r in r_points[i]] - axis.plot(mirrored_r_points, z_points[i], color="black") - # Scale fontsize relative to axis height and coil size - fontsize = max(6, axis_height * abs(coils_dr[i] * coils_dz[i]) * 1.5) - axis.text( - x_scale * coils_r[i], - coils_z[i] - 0.05, - coil_text[i], - ha="center", - va="center", - fontsize=fontsize, - ) - axis.add_patch( - patches.Rectangle( - xy=(x_scale * central_coil.anchor_x, central_coil.anchor_z), - width=x_scale * central_coil.width, - height=central_coil.height, - facecolor=SOLENOID_COLOUR[colour_scheme - 1], - edgecolor="black", - linewidth=1, - ) - ) - axis.add_patch( - patches.Rectangle( - xy=(0.0, central_coil.anchor_z), - width=x_scale * central_coil.anchor_x, - height=central_coil.height, - facecolor="grey", - alpha=0.5, - ) - ) - - -def plot_info(axis: plt.Axes, data, mfile: MFile, scan: int): - """Function to plot data in written form on a matplotlib plot. - - Parameters - ---------- - axis : - axis object to plot to - data : - plot information - mfile : - MFILE - scan : - scan number to use - """ - eqpos = 0.75 - for i in range(len(data)): - colorflag = "black" - if mfile.data[data[i][0]].exists: - if mfile.data[data[i][0]].var_flag == "ITV": - colorflag = "red" - elif mfile.data[data[i][0]].var_flag == "OP": - colorflag = "blue" - axis.text(0, -i, data[i][1], color=colorflag, ha="left", va="center") - if isinstance(data[i][0], str): - if not data[i][0]: - axis.text(eqpos, -i, "\n", ha="left", va="center") - elif data[i][0][0] == "#": - axis.text(-0.05, -i, f"{data[i][0][1:]}\n", ha="left", va="center") - elif data[i][0][0] == "!": - value = data[i][0][1:].replace('"', "") - axis.text( - 0.4, - -i, - f"--> {value} {data[i][2]}", - ha="left", - va="center", - ) - elif mfile.data[data[i][0]].exists: - dat = mfile.get(data[i][0], scan=scan) - if isinstance(dat, str): - value = dat - else: - value = f"{mfile.get(data[i][0], scan=scan):.4g}" - if "alpha" in data[i][0]: - value = str(float(value) + 1.0) - axis.text( - eqpos, - -i, - f"= {value} {data[i][2]}", - color=colorflag, - ha="left", - va="center", - ) - else: - mfile.get(data[i][0], scan=-1) - axis.text( - eqpos, - -i, - "= ERROR! Var missing", - color=colorflag, - ha="left", - va="center", - ) - else: - dat = data[i][0] - value = dat if isinstance(dat, str) else f"{data[i][0]:.4g}" - axis.text( - eqpos, - -i, - f"= {value} {data[i][2]}", - color=colorflag, - ha="left", - va="center", - ) - - -def plot_header(axis: plt.Axes, mfile: MFile, scan: int): - """Function to plot header info: date, rutitle etc - - Parameters - ---------- - axis : - axis object to plot to - mfile : - MFILE - scan : - scan number to use - """ - _setup_axis(axis, xmin=0, xmax=1, ymin=-16, ymax=1) - - data2 = [ - (f"!{mfile.get('runtitle', scan=-1)}", "Run title", ""), - (f"!{mfile.get('procver', scan=-1)}", "PROCESS Version", ""), - (f"!{mfile.get('date', scan=-1)}", "Date:", ""), - (f"!{mfile.get('time', scan=-1)}", "Time:", ""), - (f"!{mfile.get('username', scan=-1)}", "User:", ""), - ( - ("!Evaluation", "Run type", "") - if isinstance(mfile.data["i_figure_merit"], MFileErrorClass) - else ( - f"!{FiguresOfMerit(abs(int(mfile.get('i_figure_merit', scan=-1)))).description}", - "Optimising:", - "", - ) - ), - ] - - axis.text(-0.05, 4.0, "Colour Legend:", ha="left", va="center") - axis.text( - 0.0, 3.0, "ITR --> Iteration variable", color="red", ha="left", va="center" - ) - axis.text(0.0, 2.0, "OP --> Output variable", color="blue", ha="left", va="center") - - H = mfile.get("f_nd_impurity_electrons(01)", scan=scan) - He = mfile.get("f_nd_impurity_electrons(02)", scan=scan) - Be = mfile.get("f_nd_impurity_electrons(03)", scan=scan) - C = mfile.get("f_nd_impurity_electrons(04)", scan=scan) - N = mfile.get("f_nd_impurity_electrons(05)", scan=scan) - O = mfile.get("f_nd_impurity_electrons(06)", scan=scan) # noqa: E741 - Ne = mfile.get("f_nd_impurity_electrons(07)", scan=scan) - Si = mfile.get("f_nd_impurity_electrons(08)", scan=scan) - Ar = mfile.get("f_nd_impurity_electrons(09)", scan=scan) - Fe = mfile.get("f_nd_impurity_electrons(10)", scan=scan) - Ni = mfile.get("f_nd_impurity_electrons(11)", scan=scan) - Kr = mfile.get("f_nd_impurity_electrons(12)", scan=scan) - Xe = mfile.get("f_nd_impurity_electrons(13)", scan=scan) - W = mfile.get("f_nd_impurity_electrons(14)", scan=scan) - - data = [("", "", ""), ("", "", "")] - count = 0 - - data = [*data, (H, "D + T", "")] - count += 1 - - data = [*data, (He, "He", "")] - count += 1 - if Be > 1e-10: - data = [*data, (Be, "Be", "")] - count += +1 - if C > 1e-10: - data = [*data, (C, "C", "")] - count += 1 - if N > 1e-10: - data = [*data, (N, "N", "")] - count += 1 - if O > 1e-10: - data = [*data, (O, "O", "")] - count += 1 - if Ne > 1e-10: - data = [*data, (Ne, "Ne", "")] - count += 1 - if Si > 1e-10: - data = [*data, (Si, "Si", "")] - count += 1 - if Ar > 1e-10: - data = [*data, (Ar, "Ar", "")] - count += 1 - if Fe > 1e-10: - data = [*data, (Fe, "Fe", "")] - count += 1 - if Ni > 1e-10: - data = [*data, (Ni, "Ni", "")] - count += 1 - if Kr > 1e-10: - data = [*data, (Kr, "Kr", "")] - count += 1 - if Xe > 1e-10: - data = [*data, (Xe, "Xe", "")] - count += 1 - if W > 1e-10: - data = [*data, (W, "W", "")] - count += 1 - - if count > 11: - data = [("", "", ""), ("", "", ""), ("", "More than 11 impurities", "")] - else: - axis.text(-0.05, -6.4, "Plasma composition:", ha="left", va="center") - axis.text( - -0.05, - -7.2, - "Number densities relative to electron density:", - ha="left", - va="center", - ) - data2 += data - - plot_info(axis, data2, mfile, scan) - - -def plot_geometry_info(axis: plt.Axes, mfile: MFile, scan: int): - """Function to plot geometry info - - Parameters - ---------- - axis : - axis object to plot to - mfile : - MFILE - scan : - scan number to use - """ - xmin = 0 - xmax = 1 - ymin = -16 - ymax = 1 - - axis.text(-0.05, 1, "Geometry:", ha="left", va="center") - _setup_axis(axis, xmin, xmax, ymin, ymax) - - in_blanket_thk = mfile.get("dr_shld_inboard", scan=scan) + mfile.get( - "dr_blkt_inboard", scan=scan - ) - out_blanket_thk = mfile.get("dr_shld_outboard", scan=scan) + mfile.get( - "dr_blkt_outboard", scan=scan - ) - - data = [ - ("rmajor", "$R_0$", "m"), - ("rminor", "a", "m"), - ("aspect", "A", ""), - ("kappa95", r"$\kappa_{95}$", ""), - ("triang95", r"$\delta_{95}$", ""), - ("a_plasma_surface", "Plasma surface area", "m$^2$"), - ("a_plasma_poloidal", "Plasma cross-sectional area", "m$^2$"), - ("vol_plasma", "Plasma volume", "m$^3$"), - ("n_tf_coils", "No. of TF coils", ""), - (in_blanket_thk, "Inboard blanket+shield", "m"), - ("dr_inboard_build", "Inboard build thickness", "m"), - (out_blanket_thk, "Outboard blanket+shield", "m"), - ] - - plot_info(axis, data, mfile, scan) - - -def plot_physics_info(axis: plt.Axes, mfile: MFile, scan: int): - """Function to plot geometry info - - Parameters - ---------- - axis : - axis object to plot to - mfile : - MFILE - scan : - scan number to use - """ - xmin = 0 - xmax = 1 - ymin = -16 - ymax = 1 - - axis.text(-0.05, 1, "Physics:", ha="left", va="center") - _setup_axis(axis, xmin, xmax, ymin, ymax) - - nong = mfile.get("nd_plasma_electron_line", scan=scan) / mfile.get( - "nd_plasma_electron_max_array(7)", scan=scan - ) - - nd_plasma_impurities_vol_avg = mfile.get( - "nd_plasma_impurities_vol_avg", scan=scan - ) / mfile.get("nd_plasma_electrons_vol_avg", scan=scan) - - tepeak = mfile.get("temp_plasma_electron_on_axis_kev", scan=scan) / mfile.get( - "temp_plasma_electron_vol_avg_kev", scan=scan - ) - - nepeak = mfile.get("nd_plasma_electron_on_axis", scan=scan) / mfile.get( - "nd_plasma_electrons_vol_avg", scan=scan - ) - - # Assume Martin scaling if pthresh is not printed - # Accounts for pthresh not being written prior to issue #679 and #680 - if "p_l_h_threshold_mw" in mfile.data: - pthresh = mfile.get("p_l_h_threshold_mw", scan=scan) - else: - pthresh = mfile.get("l_h_threshold_powers(6)", scan=scan) - - data = [ - ("p_fusion_total_mw", "Fusion power", "MW"), - ("big_q_plasma", "$Q_{p}$", ""), - ("plasma_current_ma", "$I_p$", "MA"), - ("b_plasma_toroidal_on_axis", "Vacuum $B_T$ at $R_0$", "T"), - ("q95", r"$q_{\mathrm{95}}$", ""), - ("beta_norm_thermal", r"$\beta_N$, thermal", "% m T MA$^{-1}$"), - ("beta_norm_toroidal", r"$\beta_N$, toroidal", "% m T MA$^{-1}$"), - ("beta_thermal_poloidal_vol_avg", r"$\beta_P$, thermal", ""), - ("beta_poloidal_vol_avg", r"$\beta_P$, total", ""), - ("temp_plasma_electron_vol_avg_kev", r"$\langle T_e \rangle$", "keV"), - ("nd_plasma_electrons_vol_avg", r"$\langle n_e \rangle$", "m$^{-3}$"), - (nong, r"$\langle n_{\mathrm{e,line}} \rangle \ / \ n_G$", ""), - (tepeak, r"$T_{e0} \ / \ \langle T_e \rangle$", ""), - (nepeak, r"$n_{e0} \ / \ \langle n_{\mathrm{e, vol}} \rangle$", ""), - ("n_charge_plasma_effective_vol_avg", r"$Z_{\mathrm{eff}}$", ""), - ( - nd_plasma_impurities_vol_avg, - r"$n_Z \ / \ \langle n_{\mathrm{e, vol}} \rangle$", - "", - ), - ("t_energy_confinement", r"$\tau_e$", "s"), - ("hfact", "H-factor", ""), - (pthresh, "H-mode threshold", "MW"), - ("tauelaw", "Scaling law", ""), - ] - - plot_info(axis, data, mfile, scan) - - -def plot_magnetics_info(axis: plt.Axes, mfile: MFile, scan: int): - """Function to plot magnet info - - Parameters - ---------- - axis : - axis object to plot to - mfile : - MFILE - scan : - scan number to use - """ - # Check for Copper magnets - i_tf_sup = int(mfile.get("i_tf_sup", scan=scan)) if "i_tf_sup" in mfile.data else 1 - - axis.text(-0.05, 1, "Coil currents etc:", ha="left", va="center") - _setup_axis(axis, xmin=0, xmax=1, ymin=-16, ymax=1) - - # Number of coils (1 is OH coil) - number_of_coils = 0 - for item in mfile.data: - if "r_pf_coil_middle[" in item: - number_of_coils += 1 - - pf_info = [ - ( - mfile.get(f"c_pf_cs_coils_peak_ma[{i:01}]", scan=scan), - f"PF {i}", - ) - for i in range(1, number_of_coils) - if i % 2 != 0 - ] - - if len(pf_info) > 2: - pf_info_3_a = pf_info[2][0] - pf_info_3_b = pf_info[2][1] - else: - pf_info_3_a = "" - pf_info_3_b = "" - - t_plant_pulse_burn = mfile.get("t_plant_pulse_burn", scan=scan) / 3600.0 - - i_tf_bucking = ( - int(mfile.get("i_tf_bucking", scan=scan)) if "i_tf_bucking" in mfile.data else 1 - ) - - # Get superconductor material (i_tf_sc_mat) - # If i_tf_sc_mat not present, assume resistive - i_tf_sc_mat = ( - int(mfile.get("i_tf_sc_mat", scan=scan)) if "i_tf_sc_mat" in mfile.data else 0 - ) - - tftype = ( - SuperconductorModel(int(mfile.get("i_tf_sc_mat", scan=scan))).full_name - if i_tf_sc_mat > 0 - else "Resistive Copper" - ) - - vssoft = mfile.get("vs_plasma_res_ramp", scan=scan) + mfile.get( - "vs_plasma_ind_ramp", scan=scan - ) - - sig_case = 1.0e-6 * mfile.get(f"s_shear_tf_peak({i_tf_bucking})", scan=scan) - sig_cond = 1.0e-6 * mfile.get(f"s_shear_tf_peak({i_tf_bucking + 1})", scan=scan) - - if i_tf_sup == 1: - data = [ - (pf_info[0][0], pf_info[0][1], "MA"), - (pf_info[1][0], pf_info[1][1], "MA"), - (pf_info_3_a, pf_info_3_b, "MA"), - (vssoft, "Startup flux swing", "Wb"), - ("vs_cs_pf_total_pulse", "Available flux swing", "Wb"), - (t_plant_pulse_burn, "Burn time", "hrs"), - ("", "", ""), - (f"#TF coil type is {tftype}", "", ""), - ("b_tf_inboard_peak_with_ripple", "Peak field at conductor (w. rip.)", "T"), - ("f_c_tf_turn_operating_critical", r"I/I$_{\mathrm{crit}}$", ""), - ("temp_tf_superconductor_margin", "TF Temperature margin", "K"), - ("temp_cs_superconductor_margin", "CS Temperature margin", "K"), - (sig_cond, "TF Cond max TRESCA stress", "MPa"), - (sig_case, "TF Case max TRESCA stress", "MPa"), - ("m_tf_coils_total/n_tf_coils", "Mass per TF coil", "kg"), - ] - - else: - p_cp_resistive = 1.0e-6 * mfile.get("p_cp_resistive", scan=scan) - p_tf_leg_resistive = 1.0e-6 * mfile.get("p_tf_leg_resistive", scan=scan) - p_tf_joints_resistive = 1.0e-6 * mfile.get("p_tf_joints_resistive", scan=scan) - fcoolcp = 100.0 * mfile.get("fcoolcp", scan=scan) - - data = [ - (pf_info[0][0], pf_info[0][1], "MA"), - (pf_info[1][0], pf_info[1][1], "MA"), - (pf_info_3_a, pf_info_3_b, "MA"), - (vssoft, "Startup flux swing", "Wb"), - ("vs_cs_pf_total_pulse", "Available flux swing", "Wb"), - (t_plant_pulse_burn, "Burn time", "hrs"), - ("", "", ""), - (f"#TF coil type is {tftype}", "", ""), - ("b_tf_inboard_peak_symmetric", "Peak field at conductor (w. rip.)", "T"), - ("c_tf_total", "TF coil currents sum", "A"), - ("", "", ""), - ("#TF coil forces/stresses", "", ""), - (sig_cond, "TF conductor max TRESCA stress", "MPa"), - (sig_case, "TF bucking max TRESCA stress", "MPa"), - (fcoolcp, "CP cooling fraction", "%"), - ("vel_cp_coolant_midplane", "Maximum coolant flow speed", "ms$^{-1}$"), - (p_cp_resistive, "CP resistive heating", "MW"), - ( - p_tf_leg_resistive, - "legs resistive heating (all legs)", - "MW", - ), - (p_tf_joints_resistive, "TF joints resistive heating ", "MW"), - ] - - plot_info(axis, data, mfile, scan) - - -def plot_power_info(axis: plt.Axes, mfile: MFile, scan: int): - """Function to plot power info - - Parameters - ---------- - axis : - axis object to plot to - mfile : - MFILE - scan : - scan number to use - """ - axis.text(-0.05, 1, "Power flows:", ha="left", va="center") - _setup_axis(axis, xmin=0, xmax=1, ymin=-16, ymax=1) - - gross_eff = 100.0 * ( - mfile.get("p_plant_electric_gross_mw", scan=scan) - / mfile.get("p_plant_primary_heat_mw", scan=scan) - ) - - net_eff = 100.0 * ( - ( - mfile.get("p_plant_electric_gross_mw", scan=scan) - - mfile.get("p_coolant_pump_elec_total_mw", scan=scan) - ) - / ( - mfile.get("p_plant_primary_heat_mw", scan=scan) - - mfile.get("p_coolant_pump_elec_total_mw", scan=scan) - ) - ) - - plant_eff = 100.0 * ( - mfile.get("p_plant_electric_net_mw", scan=scan) - / mfile.get("p_fusion_total_mw", scan=scan) - ) - - # Define appropriate pedestal and impurity parameters - coredescription = ( - "radius_plasma_core_norm", - "Normalised radius of 'core' region", - "", - ) - if mfile.get("i_plasma_pedestal", scan=scan) == 1: - ped_height = ( - "nd_plasma_pedestal_electron", - "Electron density at pedestal", - "m$^{-3}$", - ) - ped_pos = ("radius_plasma_pedestal_density_norm", "r/a at density pedestal", "") - else: - ped_height = ("", "No pedestal model used", "") - ped_pos = ("", "", "") - - p_cryo_plant_electric_mw = mfile.get("p_cryo_plant_electric_mw", scan=scan) - - data = [ - ("pflux_fw_neutron_mw", "Nominal neutron wall load", "MW m$^{-2}$"), - coredescription, - ped_height, - ped_pos, - ("p_plasma_inner_rad_mw", "Inner zone radiation", "MW"), - ("p_plasma_rad_mw", "Total radiation in LCFS", "MW"), - ("p_blkt_nuclear_heat_total_mw", "Nuclear heating in blanket", "MW"), - ("p_shld_nuclear_heat_mw", "Nuclear heating in shield", "MW"), - (p_cryo_plant_electric_mw, "TF cryogenic power", "MW"), - ("p_plasma_separatrix_mw", "Power to divertor", "MW"), - ("life_div_fpy", "Divertor life", "years"), - ("p_plant_primary_heat_mw", "Primary (high grade) heat", "MW"), - (gross_eff, "Gross cycle efficiency", "%"), - (net_eff, "Net cycle efficiency", "%"), - ("p_plant_electric_gross_mw", "Gross electric power", "MW"), - ("p_plant_electric_net_mw", "Net electric power", "MW"), - ( - plant_eff, - r"Fusion-to-electric efficiency $\frac{P_{\mathrm{e,net}}}{P_{\mathrm{fus}}}$", - "%", - ), - ] - - plot_info(axis, data, mfile, scan) - - -def plot_current_drive_info(axis: plt.Axes, mfile: MFile, scan: int): - """Function to plot current drive info - - Parameters - ---------- - axis : - axis object to plot to - mfile : - MFILE - scan : - scan number to use - """ - _setup_axis(axis, xmin=0, xmax=1, ymin=-16, ymax=1) - - i_hcd_primary = mfile.get("i_hcd_primary", scan=scan) - - if nbi := (i_hcd_primary in {5, 8}): - axis.text(-0.05, 1, "Neutral Beam Current Drive:", ha="left", va="center") - if ecrh := (i_hcd_primary in {3, 7, 10, 11, 13}): - axis.text(-0.05, 1, "Electron Cyclotron Current Drive:", ha="left", va="center") - if ebw := (i_hcd_primary == 12): - axis.text(-0.05, 1, "Electron Bernstein Wave Drive:", ha="left", va="center") - if lhcd := (i_hcd_primary in {1, 4, 6}): - axis.text( - -0.05, - 1, - "Lower Hybrid Current Drive:", - ha="left", - va="center", - ) - if iccd := (i_hcd_primary == 2): - axis.text(-0.05, 1, "Ion Cyclotron Current Drive:", ha="left", va="center") - - i_hcd_secondary = mfile.get("i_hcd_secondary", scan=scan) or 0 - if i_hcd_secondary in {5, 8}: - secondary_heating = "NBI" - elif i_hcd_secondary in {3, 7, 10, 11, 13}: - secondary_heating = "ECH" - elif i_hcd_secondary == 12: - secondary_heating = "EBW" - elif i_hcd_secondary in {1, 4, 6}: - secondary_heating = "LHCD" - elif i_hcd_secondary == 2: - secondary_heating = "ICCD" - else: - secondary_heating = "" - - pinjie = mfile.get("p_hcd_injected_total_mw", scan=scan) - p_plasma_separatrix_mw = mfile.get("p_plasma_separatrix_mw", scan=scan) - pdivr = p_plasma_separatrix_mw / mfile.get("rmajor", scan=scan) - - if mfile.get("i_hcd_secondary", scan=scan) != 0: - pinjmwfix = mfile.get("pinjmwfix", scan=scan) - - pdivnr = ( - 1.0e20 - * mfile.get("p_plasma_separatrix_mw", scan=scan) - / ( - mfile.get("rmajor", scan=scan) - * mfile.get("nd_plasma_electrons_vol_avg", scan=scan) - ) - ) - - # Assume Martin scaling if pthresh is not printed - # Accounts for pthresh not being written prior to issue #679 and #680 - pthresh_name = ( - "p_l_h_threshold_mw" - if "p_l_h_threshold_mw" in mfile.data - else "l_h_threshold_powers(6)" - ) - pthresh = mfile.get(pthresh_name, scan=scan) - flh = p_plasma_separatrix_mw / pthresh - - hstar = mfile.get("hstar", scan=scan) - - data = [ - (pinjie, "Steady state auxiliary power", "MW"), - ("p_hcd_primary_extra_heat_mw", "Power for heating only", "MW"), - ("f_c_plasma_bootstrap", "Bootstrap fraction", ""), - ("f_c_plasma_auxiliary", "Auxiliary fraction", ""), - ("f_c_plasma_inductive", "Inductive fraction", ""), - ("p_plasma_loss_mw", "Plasma heating used for H factor", "MW"), - (pdivr, r"$\frac{P_{\mathrm{div}}}{R_{0}}$", "MW m$^{-1}$"), - ( - pdivnr, - r"$\frac{P_{\mathrm{div}}}{\langle n \rangle R_{0}}$", - r"$\times 10^{-20}$ MW m$^{2}$", - ), - (flh, r"$\frac{P_{\mathrm{div}}}{P_{\mathrm{LH}}}$", ""), - (hstar, "H* (non-rad. corr.)", ""), - ] - # Optional override based on condition - field_overrides = { - "ecrh": ( - "eta_cd_hcd_primary", - r"$\frac{P_{\mathrm{div}}}{R_{0}}$", - "A W$^{-1}$", - ), - "nbi": ( - ("gamnb", "NB gamma", "$10^{20}$ A W$^{-1}$ m$^{-2}$"), - ("e_beam_kev", "NB energy", "keV"), - ), - "ebw": ( - "eta_cd_norm_hcd_primary", - "Normalised current drive efficiency of primary HCD system", - "(10$^{20}$ A/(Wm$^{2}$))", - ), - "lhcd": ( - "eta_cd_norm_hcd_primary", - "Normalised current drive efficiency", - "(10$^{20}$ A/(Wm$^{2}$))", - ), - "iccd": ( - "eta_cd_norm_hcd_primary", - "Normalised current drive efficiency", - "(10$^{20}$ A/(Wm$^{2}$))", - ), - } - - if ecrh: - data.insert(6, field_overrides["ecrh"]) - elif nbi: - data.insert(6, field_overrides["nbi"][0]) - data.insert(7, field_overrides["nbi"][1]) - elif ebw: - data.insert(6, field_overrides["ebw"]) - elif lhcd: - data.insert(6, field_overrides["lhcd"]) - elif iccd: - data.insert(6, field_overrides["iccd"]) - - # Secondary heating logic — common across all cases - if mfile.get("i_hcd_secondary", scan=scan) != 0: - data.insert( - 1, ("pinjmwfix", f"{secondary_heating} secondary auxiliary power", "MW") - ) - data[0] = ((pinjie - pinjmwfix), "Primary auxiliary power", "MW") - data.insert(2, (pinjie, "Total auxillary power", "MW")) - - coe = mfile.get("coe", scan=scan) - data.extend(( - ("", "", ""), - ("#Costs", "", ""), - ("", "Cost output not selected", "") - if coe == 0.0 # noqa: RUF069 - else (coe, "Cost of electricity", r"\$/MWh"), - )) - - plot_info(axis, data, mfile, scan) - - -def plot_bootstrap_comparison(axis: plt.Axes, mfile: MFile, scan: int): - """Function to plot a scatter box plot of bootstrap current fractions. - - Parameters - ---------- - axis : - axis object to plot to - mfile : - MFILE data object - scan : - scan number to use - """ - # Data for the box plot - data = { - "IPDG": mfile.get("f_c_plasma_bootstrap_iter89", scan=scan), - "Sauter": mfile.get("f_c_plasma_bootstrap_sauter", scan=scan), - "Nevins": mfile.get("f_c_plasma_bootstrap_nevins", scan=scan), - "Wilson": mfile.get("f_c_plasma_bootstrap_wilson", scan=scan), - "Sakai": mfile.get("f_c_plasma_bootstrap_sakai", scan=scan), - "ARIES": mfile.get("f_c_plasma_bootstrap_aries", scan=scan), - "Andrade": mfile.get("f_c_plasma_bootstrap_andrade", scan=scan), - "Hoang": mfile.get("f_c_plasma_bootstrap_hoang", scan=scan), - "Wong": mfile.get("f_c_plasma_bootstrap_wong", scan=scan), - "Gi-I": mfile.get("bscf_gi_i", scan=scan), - "Gi-II": mfile.get("bscf_gi_ii", scan=scan), - "Sugiyama (L-mode)": mfile.get("f_c_plasma_bootstrap_sugiyama_l", scan=scan), - "Sugiyama (H-mode)": mfile.get("f_c_plasma_bootstrap_sugiyama_h", scan=scan), - } - # Create the violin plot - data_values = list(data.values()) - axis.violinplot(data_values, showextrema=False) - - # Create the box plot - axis.boxplot(data_values, showfliers=True, showmeans=True, meanline=True, widths=0.3) - - # Scatter plot for each data point - colors = plt.cm.plasma(np.linspace(0, 1, len(data.values()))) - for index, (key, value) in enumerate(data.items()): - axis.scatter(1, value, color=colors[index], label=key, alpha=1.0) - axis.legend(loc="upper left", bbox_to_anchor=(1, 1)) - - # Calculate average, standard deviation, and median - avg_bootstrap = np.mean(data_values) - std_bootstrap = np.std(data_values) - median_bootstrap = np.median(data_values) - - # Plot average, standard deviation, and median as text - axis.text( - 1.02, 0.2, f"Average: {avg_bootstrap:.4f}", transform=axis.transAxes, fontsize=9 - ) - axis.text( - 1.02, - 0.15, - f"Standard Dev: {std_bootstrap:.4f}", - transform=axis.transAxes, - fontsize=9, - ) - axis.text( - 1.02, - 0.1, - f"Median: {median_bootstrap:.4f}", - transform=axis.transAxes, - fontsize=9, - ) - - axis.set_title("Bootstrap Current Fraction ($f_\\text{BS}$) Comparison") - axis.set_ylabel("Bootstrap Current Fraction") - axis.set_xlim(0.5, 1.5) - axis.set_xticks([]) - axis.set_xticklabels([]) - axis.set_facecolor("#f0f0f0") - - -def plot_sol_power_decay_length_comparison(axis: plt.Axes, mfile: MFile, scan: int): - """Function to plot a scatter box plot of SOL power decay lengths (λ_q). - - Parameters - ---------- - axis : - axis object to plot to - mfile : - MFILE data object - scan : - scan number to use - """ - len_plasma_sol_eich13_power_decay_mm = ( - mfile.get("len_plasma_sol_eich13_power_decay", scan=scan) * 1e3 - ) - len_plasma_sol_mast14_power_decay_1_mm = ( - mfile.get("len_plasma_sol_mast14_power_decay_1", scan=scan) * 1e3 - ) - len_plasma_sol_mast14_power_decay_2_mm = ( - mfile.get("len_plasma_sol_mast14_power_decay_2", scan=scan) * 1e3 - ) - len_plasma_sol_eich11_jet_power_decay_mm = ( - mfile.get("len_plasma_sol_eich11_jet_power_decay", scan=scan) * 1e3 - ) - len_plasma_sol_eich11_jet_asdex_power_decay_mm = ( - mfile.get("len_plasma_sol_eich11_jet_asdex_power_decay", scan=scan) * 1e3 - ) - # Data for the box plot - data = { - f"{OutbordSOLPowerDecayLengthModel.EICH_2013.description}": len_plasma_sol_eich13_power_decay_mm, - f"{OutbordSOLPowerDecayLengthModel.MAST_2014_1.description}": len_plasma_sol_mast14_power_decay_1_mm, - f"{OutbordSOLPowerDecayLengthModel.MAST_2014_2.description}": len_plasma_sol_mast14_power_decay_2_mm, - f"{OutbordSOLPowerDecayLengthModel.EICH_2011_JET.description}": len_plasma_sol_eich11_jet_power_decay_mm, - f"{OutbordSOLPowerDecayLengthModel.EICH_2011_JET_ASDEX.description}": len_plasma_sol_eich11_jet_asdex_power_decay_mm, - } - data_values = list(data.values()) - - # Create the violin plot - axis.violinplot(data_values, showextrema=False) - - # Create the box plot - axis.boxplot(data_values, showfliers=True, showmeans=True, meanline=True, widths=0.3) - - # Scatter plot for each data point - colors = plt.cm.plasma(np.linspace(0, 1, len(data_values))) - for index, (key, value) in enumerate(data.items()): - axis.scatter(1, value, color=colors[index], label=key, alpha=1.0) - axis.legend(loc="upper left", bbox_to_anchor=(1, 1)) - - # Calculate average, standard deviation, and median - avg_decay_length = np.mean(data_values) - std_decay_length = np.std(data_values) - median_decay_length = np.median(data_values) - - # Plot average, standard deviation, and median as text - axis.text( - 1.02, - 0.2, - f"Average: {avg_decay_length:.4f}", - transform=axis.transAxes, - fontsize=9, - ) - axis.text( - 1.02, - 0.15, - f"Standard Dev: {std_decay_length:.4f}", - transform=axis.transAxes, - fontsize=9, - ) - axis.text( - 1.02, - 0.1, - f"Median: {median_decay_length:.4f}", - transform=axis.transAxes, - fontsize=9, - ) - - axis.set_title("SOL Power Decay Length ($\\lambda_q$) Comparison") - axis.set_ylabel("Power Decay Length [mm]") - axis.set_xlim(0.5, 1.5) - axis.set_xticks([]) - axis.set_xticklabels([]) - axis.set_facecolor("#f0f0f0") - - -def plot_brunner_divertor_power_split_comparison_stackplot( - axis: plt.Axes, mfile: MFile, scan: int -): - """Plot Brunner divertor power split fractions as a stack plot over dr_sep.""" - # Use the case decay length when available; fall back to 1 mm if absent. - - len_plasma_sol_outboard_pd = mfile.get("len_sol_outboard_power_decay", scan=scan) - len_plasma_sol_inboard_pd = mfile.get("len_sol_inboard_power_decay", scan=scan) - colors = plt.cm.plasma(np.linspace(0.15, 0.85, 4)) - - dr_sep_values = np.linspace( - -5 * len_plasma_sol_outboard_pd, - 5 * len_plasma_sol_outboard_pd, - 200, - ) - f_p_inboard_lower = np.zeros_like(dr_sep_values) - f_p_inboard_upper = np.zeros_like(dr_sep_values) - f_p_outboard_lower = np.zeros_like(dr_sep_values) - f_p_outboard_upper = np.zeros_like(dr_sep_values) - - for idx, dr_sep in enumerate(dr_sep_values): - div_power_splits = calculate_brunner_divertor_power_splits( - dr_outboard_midplane_sep=dr_sep, - len_plasma_sol_outboard_power_decay=len_plasma_sol_outboard_pd, - len_plasma_sol_inboard_power_decay=len_plasma_sol_inboard_pd, - ) - f_p_inboard_lower[idx] = div_power_splits.f_p_div_inboard_lower_separatrix - f_p_inboard_upper[idx] = div_power_splits.f_p_div_inboard_upper_separatrix - f_p_outboard_lower[idx] = div_power_splits.f_p_div_outboard_lower_separatrix - f_p_outboard_upper[idx] = div_power_splits.f_p_div_outboard_upper_separatrix - - axis.stackplot( - dr_sep_values, - f_p_inboard_lower, - f_p_inboard_upper, - f_p_outboard_lower, - f_p_outboard_upper, - labels=[ - "$f_{P,\\mathrm{in,lower}}$", - "$f_{P,\\mathrm{in,upper}}$", - "$f_{P,\\mathrm{out,lower}}$", - "$f_{P,\\mathrm{out,upper}}$", - ], - colors=colors, - alpha=0.9, - ) - - axis.axvline( - mfile.get("dr_plasma_outboard_midplane_separatrix_separation", scan=scan), - color="k", - linestyle="--", - linewidth=1.0, - alpha=0.5, - label="$\u0394 r_{\\mathrm{sep}}$", - ) - axis.set_ylim(0.0, 1.0) - axis.set_xlim( - -5 * len_plasma_sol_outboard_pd, - 5 * len_plasma_sol_outboard_pd, - ) - axis.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.35) - axis.set_title("Brunner Divertor Power Split Fractions") - axis.set_xlabel("$\\Delta r_{\\mathrm{sep}}$ [m]") - axis.set_ylabel("Power split fraction, $f_P$") - axis.legend(loc="upper left", fontsize=8) - - -def plot_separatrix_power_split(axis: plt.Axes, mfile: MFile, scan: int, colour_scheme): - """Plot separatrix power split fractions as a bar chart.""" - plot_plasma(axis=axis, mfile=mfile, scan=scan, colour_scheme=colour_scheme) - rmajor, rminor, kappa, dr_sep = mfile.get_variables( - "rmajor", - "rminor", - "kappa", - "dr_plasma_outboard_midplane_separatrix_separation", - scan=scan, - ) - - plasma_scale = max(rminor, abs(kappa * rminor), 1e-6) - scale_factor = min(max(plasma_scale / 2.0, 0.7), 1.0) - text_fontsize = 9 * scale_factor - - is_double_null = ( - DivertorNumberModels(mfile.get("i_single_null", scan=scan)) - == DivertorNumberModels.DOUBLE_NULL - ) - p_sep = mfile.get("p_plasma_separatrix_mw", scan=scan) - f_outboard = mfile.get("f_p_div_outboard_separatrix", scan=scan) - f_inboard = mfile.get("f_p_div_inboard_separatrix", scan=scan) - p_outboard = p_sep * f_outboard - p_inboard = p_sep * f_inboard - p_lower_inboard = mfile.get("p_div_lower_inboard_separatrix_mw", scan=scan) - p_lower_outboard = mfile.get("p_div_lower_outboard_separatrix_mw", scan=scan) - - power_values = [ - p_sep, - p_outboard, - p_inboard, - p_lower_inboard, - p_lower_outboard, - ] - - p_upper_inboard = None - p_upper_outboard = None - if is_double_null: - p_upper_inboard = mfile.get("p_div_upper_inboard_separatrix_mw", scan=scan) - p_upper_outboard = mfile.get("p_div_upper_outboard_separatrix_mw", scan=scan) - power_values.extend([p_upper_inboard, p_upper_outboard]) - - power_min = min(power_values) - power_max = max(power_values) - colour_map = mpl.colormaps["coolwarm"] - - def make_bbox_props(power: float) -> dict[str, Any]: - norm_power = ( - 1.0 - if np.isclose(power_max, power_min) - else (power - power_min) / (power_max - power_min) - ) - return { - "boxstyle": f"round,pad={0.3 * scale_factor:.3f}", - "facecolor": colour_map(norm_power), - "alpha": 1.0, - "linewidth": 2 * scale_factor, - "edgecolor": "black", - } - - centre_pos = (rmajor, 0.0) - outboard_pos = (rmajor + rminor, 0.0) - inboard_pos = (rmajor - rminor, 0.0) - lower_inboard_pos = (rmajor - rminor, -kappa * rminor) - lower_outboard_pos = (rmajor + rminor, -kappa * rminor) - upper_inboard_pos = (rmajor - rminor, kappa * rminor) - upper_outboard_pos = (rmajor + rminor, kappa * rminor) - - axis.text( - *centre_pos, - f"$P_{{\\mathrm{{sep}}}} = {p_sep:.3f}$ MW", - fontsize=text_fontsize, - verticalalignment="center", - horizontalalignment="center", - bbox=make_bbox_props(p_sep), - zorder=101, - ) - axis.text( - *outboard_pos, - f"$f_{{\\mathrm{{outboard}}}} = {f_outboard:.3f}$\n" - f"$\\Delta r_{{\\mathrm{{sep}}}} = {dr_sep:.3f}$ m", - fontsize=text_fontsize, - verticalalignment="center", - horizontalalignment="center", - bbox=make_bbox_props(p_outboard), - zorder=101, - ) - axis.text( - *inboard_pos, - f"$f_{{\\mathrm{{inboard}}}} = {f_inboard:.3f}$", - fontsize=text_fontsize, - verticalalignment="center", - horizontalalignment="center", - bbox=make_bbox_props(p_inboard), - zorder=101, - ) - axis.text( - *lower_inboard_pos, - f"$f_{{\\mathrm{{lower\\ inboard}}}} = {mfile.get('f_p_div_lower_inboard_separatrix', scan=scan):.3f}$\n" - f"$P_{{\\mathrm{{lower\\ inboard}}}} = {p_lower_inboard:.3f}$ MW", - fontsize=text_fontsize, - verticalalignment="center", - horizontalalignment="center", - bbox=make_bbox_props(p_lower_inboard), - zorder=101, - ) - axis.text( - *lower_outboard_pos, - f"$f_{{\\mathrm{{lower\\ outboard}}}} = {mfile.get('f_p_div_lower_outboard_separatrix', scan=scan):.3f}$\n" - f"$P_{{\\mathrm{{lower\\ outboard}}}} = {p_lower_outboard:.3f}$ MW", - fontsize=text_fontsize, - verticalalignment="center", - horizontalalignment="center", - bbox=make_bbox_props(p_lower_outboard), - zorder=101, - ) - if is_double_null: - axis.text( - *upper_inboard_pos, - f"$f_{{\\mathrm{{upper\\ inboard}}}} = {mfile.get('f_p_div_upper_inboard_separatrix', scan=scan):.3f}$\n" - f"$P_{{\\mathrm{{upper\\ inboard}}}} = {p_upper_inboard:.3f}$ MW", - fontsize=text_fontsize, - verticalalignment="center", - horizontalalignment="center", - bbox=make_bbox_props(p_upper_inboard), - zorder=101, - ) - axis.text( - *upper_outboard_pos, - f"$f_{{\\mathrm{{upper\\ outboard}}}} = {mfile.get('f_p_div_upper_outboard_separatrix', scan=scan):.3f}$\n" - f"$P_{{\\mathrm{{upper\\ outboard}}}} = {p_upper_outboard:.3f}$ MW", - fontsize=text_fontsize, - verticalalignment="center", - horizontalalignment="center", - bbox=make_bbox_props(p_upper_outboard), - zorder=101, - ) - - arrow_props = { - "arrowstyle": "->", - "color": "red", - "linewidth": 3 * scale_factor, - "shrinkA": 14 * scale_factor, - "shrinkB": 14 * scale_factor, - "mutation_scale": 12 * scale_factor, - } - axis.annotate( - "", xy=outboard_pos, xytext=centre_pos, arrowprops=arrow_props, zorder=1 - ) - axis.annotate( - "", xy=inboard_pos, xytext=centre_pos, arrowprops=arrow_props, zorder=1 - ) - axis.annotate( - "", - xy=lower_outboard_pos, - xytext=outboard_pos, - arrowprops={**arrow_props, "connectionstyle": "angle3,angleA=0,angleB=-90"}, - zorder=102, - ) - axis.annotate( - "", - xy=lower_inboard_pos, - xytext=inboard_pos, - arrowprops={**arrow_props, "connectionstyle": "angle3,angleA=180,angleB=-90"}, - zorder=102, - ) - if is_double_null: - axis.annotate( - "", - xy=upper_outboard_pos, - xytext=outboard_pos, - arrowprops={**arrow_props, "connectionstyle": "angle3,angleA=0,angleB=90"}, - zorder=102, - ) - axis.annotate( - "", - xy=upper_inboard_pos, - xytext=inboard_pos, - arrowprops={**arrow_props, "connectionstyle": "angle3,angleA=180,angleB=90"}, - zorder=102, - ) - - axis.spines["top"].set_visible(False) - axis.spines["right"].set_visible(False) - axis.spines["bottom"].set_visible(False) - axis.spines["left"].set_visible(False) - axis.get_xaxis().set_ticks([]) - axis.get_yaxis().set_ticks([]) - - -def plot_h_threshold_comparison(axis: plt.Axes, mfile: MFile, scan: int, u_seed=None): - """Function to plot a scatter box plot of L-H threshold power comparisons. - - Parameters - ---------- - axis : - Axis object to plot to. - mfile : - MFILE data object. - scan : - Scan number to use. - u_seed : - (Default value = None) - """ - # Data for the box plot - data = { - "ITER 1996 Nominal": mfile.get("l_h_threshold_powers(1)", scan=scan), - "ITER 1996 Upper": mfile.get("l_h_threshold_powers(2)", scan=scan), - "ITER 1996 Lower": mfile.get("l_h_threshold_powers(3)", scan=scan), - "ITER 1997 (1)": mfile.get("l_h_threshold_powers(4)", scan=scan), - "ITER 1997 (2)": mfile.get("l_h_threshold_powers(5)", scan=scan), - "Martin Nominal": mfile.get("l_h_threshold_powers(6)", scan=scan), - "Martin Upper": mfile.get("l_h_threshold_powers(7)", scan=scan), - "Martin Lower": mfile.get("l_h_threshold_powers(8)", scan=scan), - "Snipes Nominal": mfile.get("l_h_threshold_powers(9)", scan=scan), - "Snipes Upper": mfile.get("l_h_threshold_powers(10)", scan=scan), - "Snipes Lower": mfile.get("l_h_threshold_powers(11)", scan=scan), - "Snipes Closed Divertor Nominal": mfile.get( - "l_h_threshold_powers(12)", scan=scan - ), - "Snipes Closed Divertor Upper": mfile.get("l_h_threshold_powers(13)", scan=scan), - "Snipes Closed Divertor Lower": mfile.get("l_h_threshold_powers(14)", scan=scan), - "Hubbard Nominal (I-mode)": mfile.get("l_h_threshold_powers(15)", scan=scan), - "Hubbard Lower (I-mode)": mfile.get("l_h_threshold_powers(16)", scan=scan), - "Hubbard Upper (I-mode)": mfile.get("l_h_threshold_powers(17)", scan=scan), - "Hubbard 2017 (I-mode)": mfile.get("l_h_threshold_powers(18)", scan=scan), - "Martin Aspect Corrected Nominal": mfile.get( - "l_h_threshold_powers(19)", scan=scan - ), - "Martin Aspect Corrected Upper": mfile.get( - "l_h_threshold_powers(20)", scan=scan - ), - "Martin Aspect Corrected Lower": mfile.get( - "l_h_threshold_powers(21)", scan=scan - ), - } - data_values = list(data.values()) - # Create the violin plot - axis.violinplot(data_values, showextrema=False) - - # Create the box plot - axis.boxplot(data_values, showfliers=True, showmeans=True, meanline=True, widths=0.3) - - # Scatter plot for each data point - colors = plt.cm.plasma(np.linspace(0, 1, len(data_values))) - generator = np.random.default_rng(seed=u_seed) - x_values = generator.normal(loc=1, scale=0.01, size=len(data_values)) - for index, (key, value) in enumerate(data.items()): - if "ITER 1996" in key: - color = "blue" - elif "ITER 1997" in key: - color = "cyan" - elif "Martin" in key and "Aspect" not in key: - color = "green" - elif "Snipes" in key and "Closed" not in key: - color = "red" - elif "Snipes Closed" in key: - color = "orange" - elif "Martin Aspect" in key: - color = "yellow" - elif "Hubbard" in key and "2017" not in key: - color = "purple" - elif "Hubbard 2017" in key: - color = "magenta" - else: - color = colors[index] - axis.scatter(x_values[index], value, color=color, label=key, alpha=1.0) - axis.legend(loc="upper left", bbox_to_anchor=(-1.1, 1), ncol=2) - - # Calculate average, standard deviation, and median - avg_threshold = np.mean(data_values) - std_threshold = np.std(data_values) - median_threshold = np.median(data_values) - - # Plot average, standard deviation, and median as text - axis.text( - -0.45, - 0.15, - f"Average: {avg_threshold:.4f}", - transform=axis.transAxes, - fontsize=9, - ) - axis.text( - -0.45, - 0.1, - f"Standard Dev: {std_threshold:.4f}", - transform=axis.transAxes, - fontsize=9, - ) - axis.text( - -0.45, - 0.05, - f"Median: {median_threshold:.4f}", - transform=axis.transAxes, - fontsize=9, - ) - - axis.set_title("L-H Threshold ($P_\\text{LH}$) Comparison") - axis.set_ylabel("L-H threshold power [MW]") - axis.set_xlim(0.5, 1.5) - axis.set_xticks([]) - axis.set_xticklabels([]) - - # Add background color - axis.set_facecolor("#f0f0f0") - - -def plot_confinement_time_comparison( - axis: plt.Axes, mfile: MFile, scan: int, u_seed=None -): - """Function to plot a scatter box plot of confinement time comparisons. - - Parameters - ---------- - axis : - Axis object to plot to. - mfile : - MFILE data object. - scan : - Scan number to use. - u_seed : - (Default value = None) - """ - rminor = mfile.get("rminor", scan=scan) - rmajor = mfile.get("rmajor", scan=scan) - cur_plasma_ma = mfile.get("plasma_current_ma", scan=scan) - kappa95 = mfile.get("kappa95", scan=scan) - nd_plasma_electron_line_20 = mfile.get("nd_plasma_electron_line", scan=scan) / 1e20 - afuel = mfile.get("m_fuel_amu", scan=scan) - b_plasma_toroidal_on_axis = mfile.get("b_plasma_toroidal_on_axis", scan=scan) - p_plasma_separatrix_mw = mfile.get("p_plasma_separatrix_mw", scan=scan) - kappa = mfile.get("kappa", scan=scan) - aspect = mfile.get("aspect", scan=scan) - nd_plasma_electron_line_19 = mfile.get("nd_plasma_electron_line", scan=scan) / 1e19 - kappa_ipb = mfile.get("kappa_ipb", scan=scan) - triang = mfile.get("triang", scan=scan) - m_ions_total_amu = mfile.get("m_ions_total_amu", scan=scan) - - confine = PlasmaConfinementTime() - - # Calculate confinement times using the scan data - iter_89p = confine.iter_89p_confinement_time( - cur_plasma_ma=cur_plasma_ma, - rmajor=rmajor, - rminor=rminor, - kappa=kappa, - nd_plasma_electron_line_20=nd_plasma_electron_line_20, - b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, - afuel=afuel, - p_plasma_loss_mw=p_plasma_separatrix_mw, - ) - iter_89_0 = confine.iter_89_0_confinement_time( - cur_plasma_ma=cur_plasma_ma, - rmajor=rmajor, - rminor=rminor, - kappa=kappa, - nd_plasma_electron_line_20=nd_plasma_electron_line_20, - b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, - afuel=afuel, - p_plasma_loss_mw=p_plasma_separatrix_mw, - ) - iter_h90_p = confine.iter_h90_p_confinement_time( - cur_plasma_ma=cur_plasma_ma, - rmajor=rmajor, - rminor=rminor, - kappa=kappa, - nd_plasma_electron_line_20=nd_plasma_electron_line_20, - b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, - afuel=afuel, - p_plasma_loss_mw=p_plasma_separatrix_mw, - ) - iter_h90_p_amended = confine.iter_h90_p_amended_confinement_time( - cur_plasma_ma=cur_plasma_ma, - b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, - afuel=afuel, - rmajor=rmajor, - p_plasma_loss_mw=p_plasma_separatrix_mw, - kappa=kappa, - ) - iter_93h = confine.iter_93h_confinement_time( - cur_plasma_ma=cur_plasma_ma, - b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, - p_plasma_loss_mw=p_plasma_separatrix_mw, - afuel=afuel, - rmajor=rmajor, - nd_plasma_electron_line_20=nd_plasma_electron_line_20, - aspect=aspect, - kappa=kappa, - ) - iter_h97p = confine.iter_h97p_confinement_time( - cur_plasma_ma=cur_plasma_ma, - b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, - p_plasma_loss_mw=p_plasma_separatrix_mw, - nd_plasma_electron_line_19=nd_plasma_electron_line_19, - rmajor=rmajor, - aspect=aspect, - kappa=kappa, - afuel=afuel, - ) - iter_h97p_elmy = confine.iter_h97p_elmy_confinement_time( - cur_plasma_ma=cur_plasma_ma, - b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, - p_plasma_loss_mw=p_plasma_separatrix_mw, - nd_plasma_electron_line_19=nd_plasma_electron_line_19, - rmajor=rmajor, - aspect=aspect, - kappa=kappa, - afuel=afuel, - ) - iter_96p = confine.iter_96p_confinement_time( - cur_plasma_ma=cur_plasma_ma, - b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, - kappa95=kappa95, - rmajor=rmajor, - aspect=aspect, - nd_plasma_electron_line_19=nd_plasma_electron_line_19, - afuel=afuel, - p_plasma_loss_mw=p_plasma_separatrix_mw, - ) - iter_pb98py = confine.iter_pb98py_confinement_time( - cur_plasma_ma=cur_plasma_ma, - b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, - nd_plasma_electron_line_19=nd_plasma_electron_line_19, - p_plasma_loss_mw=p_plasma_separatrix_mw, - rmajor=rmajor, - kappa=kappa, - aspect=aspect, - afuel=afuel, - ) - iter_ipb98y = confine.iter_ipb98y_confinement_time( - cur_plasma_ma=cur_plasma_ma, - b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, - nd_plasma_electron_line_19=nd_plasma_electron_line_19, - p_plasma_loss_mw=p_plasma_separatrix_mw, - rmajor=rmajor, - kappa=kappa, - aspect=aspect, - afuel=afuel, - ) - iter_ipb98y1 = confine.iter_ipb98y1_confinement_time( - cur_plasma_ma=cur_plasma_ma, - b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, - nd_plasma_electron_line_19=nd_plasma_electron_line_19, - p_plasma_loss_mw=p_plasma_separatrix_mw, - rmajor=rmajor, - kappa_ipb=kappa_ipb, - aspect=aspect, - afuel=afuel, - ) - iter_ipb98y2 = confine.iter_ipb98y2_confinement_time( - cur_plasma_ma=cur_plasma_ma, - b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, - nd_plasma_electron_line_19=nd_plasma_electron_line_19, - p_plasma_loss_mw=p_plasma_separatrix_mw, - rmajor=rmajor, - kappa_ipb=kappa_ipb, - aspect=aspect, - afuel=afuel, - ) - iter_ipb98y3 = confine.iter_ipb98y3_confinement_time( - cur_plasma_ma=cur_plasma_ma, - b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, - nd_plasma_electron_line_19=nd_plasma_electron_line_19, - p_plasma_loss_mw=p_plasma_separatrix_mw, - rmajor=rmajor, - kappa_ipb=kappa_ipb, - aspect=aspect, - afuel=afuel, - ) - iter_ipb98y4 = confine.iter_ipb98y4_confinement_time( - cur_plasma_ma=cur_plasma_ma, - b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, - nd_plasma_electron_line_19=nd_plasma_electron_line_19, - p_plasma_loss_mw=p_plasma_separatrix_mw, - rmajor=rmajor, - kappa_ipb=kappa_ipb, - aspect=aspect, - afuel=afuel, - ) - petty08 = confine.petty08_confinement_time( - cur_plasma_ma=cur_plasma_ma, - b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, - nd_plasma_electron_line_19=nd_plasma_electron_line_19, - p_plasma_loss_mw=p_plasma_separatrix_mw, - rmajor=rmajor, - kappa_ipb=kappa_ipb, - aspect=aspect, - ) - menard_nstx = confine.menard_nstx_confinement_time( - cur_plasma_ma=cur_plasma_ma, - b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, - nd_plasma_electron_line_19=nd_plasma_electron_line_19, - p_plasma_loss_mw=p_plasma_separatrix_mw, - rmajor=rmajor, - kappa_ipb=kappa_ipb, - aspect=aspect, - afuel=afuel, - ) - menard_nstx_petty08 = confine.menard_nstx_petty08_hybrid_confinement_time( - cur_plasma_ma=cur_plasma_ma, - b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, - nd_plasma_electron_line_19=nd_plasma_electron_line_19, - p_plasma_loss_mw=p_plasma_separatrix_mw, - rmajor=rmajor, - kappa_ipb=kappa_ipb, - aspect=aspect, - afuel=afuel, - ) - itpa20 = confine.itpa20_confinement_time( - cur_plasma_ma=cur_plasma_ma, - b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, - nd_plasma_electron_line_19=nd_plasma_electron_line_19, - p_plasma_loss_mw=p_plasma_separatrix_mw, - rmajor=rmajor, - triang=triang, - kappa_ipb=kappa_ipb, - eps=(1 / aspect), - aion=m_ions_total_amu, - ) - itpa20_ilc = confine.itpa20_il_confinement_time( - cur_plasma_ma=cur_plasma_ma, - b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, - p_plasma_loss_mw=p_plasma_separatrix_mw, - nd_plasma_electron_line_19=nd_plasma_electron_line_19, - aion=m_ions_total_amu, - rmajor=rmajor, - triang=triang, - kappa_ipb=kappa_ipb, - ) - - # Data for the box plot - data = { - rf"{ConfinementTimeModel.ITER_89P.full_name}": iter_89p, - rf"{ConfinementTimeModel.ITER_89_0.full_name}": iter_89_0, - rf"{ConfinementTimeModel.ITER_H90_P.full_name}": iter_h90_p, - rf"{ConfinementTimeModel.ITER_H90_P_AMENDED.full_name}": iter_h90_p_amended, - rf"{ConfinementTimeModel.ITER_93H.full_name}": iter_93h, - rf"{ConfinementTimeModel.ITER_H97P.full_name}": iter_h97p, - rf"{ConfinementTimeModel.ITER_H97P_ELMY.full_name}": iter_h97p_elmy, - rf"{ConfinementTimeModel.ITER_96P.full_name}": iter_96p, - rf"{ConfinementTimeModel.ITER_PB98P_Y.full_name}": iter_pb98py, - rf"{ConfinementTimeModel.IPB98_Y.full_name}": iter_ipb98y, - rf"{ConfinementTimeModel.ITER_IPB98Y1.full_name}": iter_ipb98y1, - rf"{ConfinementTimeModel.ITER_IPB98Y2.full_name}": iter_ipb98y2, - rf"{ConfinementTimeModel.ITER_IPB98Y3.full_name}": iter_ipb98y3, - rf"{ConfinementTimeModel.ITER_IPB98Y4.full_name}": iter_ipb98y4, - rf"{ConfinementTimeModel.PETTY08.full_name}": petty08, - rf"{ConfinementTimeModel.MENARD_NSTX.full_name}": menard_nstx, - rf"{ConfinementTimeModel.MENARD_NSTX_PETTY08_HYBRID.full_name}": menard_nstx_petty08, - rf"{ConfinementTimeModel.ITPA20.full_name}": itpa20, - rf"{ConfinementTimeModel.ITPA20_IL.full_name}": itpa20_ilc, - } - data_values = list(data.values()) - - # Create the violin plot - axis.violinplot(data_values, showextrema=False) - - # Create the box plot - axis.boxplot(data_values, showfliers=True, showmeans=True, meanline=True, widths=0.3) - - # Scatter plot for each data point - # Use a set of distinct colors for better differentiation - distinct_colors = [ - "#1f77b4", # blue - "#ff7f0e", # orange - "#2ca02c", # green - "#d62728", # red - "#9467bd", # purple - "#8c564b", # brown - "#e377c2", # pink - "#7f7f7f", # gray - "#bcbd22", # olive - "#17becf", # cyan - "#aec7e8", # light blue - "#ffbb78", # light orange - "#98df8a", # light green - "#ff9896", # light red - "#c5b0d5", # light purple - "#c49c94", # light brown - "#f7b6d2", # light pink - "#c7c7c7", # light gray - "#dbdb8d", # light olive - "#9edae5", # light cyan - ] - generator = np.random.default_rng(seed=u_seed) - x_values = generator.normal(loc=1, scale=0.035, size=len(data.values())) - for index, (key, value) in enumerate(data.items()): - if "Hubbard" in key and "2017" not in key: - color = "#800080" # strong purple - else: - color = distinct_colors[index % len(distinct_colors)] - axis.scatter( - x_values[index], - value, - color=color, - label=key, - alpha=1.0, - edgecolor="black", - linewidth=0.7, - ) - axis.legend(loc="upper left", bbox_to_anchor=(-1.3, 0.75), ncol=2) - - # Calculate average, standard deviation, and median - avg_threshold = np.mean(data_values) - std_threshold = np.std(data_values) - median_threshold = np.median(data_values) - - # Plot average, standard deviation, and median as text - axis.text( - 0.7, - 1.25, - f"Average: {avg_threshold:.4f} s", - transform=axis.transAxes, - fontsize=9, - ) - axis.text( - 0.7, - 1.2, - f"Standard Dev: {std_threshold:.4f} s", - transform=axis.transAxes, - fontsize=9, - ) - axis.text( - 0.7, - 1.15, - f"Median: {median_threshold:.4f} s", - transform=axis.transAxes, - fontsize=9, - ) - axis.text( - 0.75, - -0.05, - r"$H \ factor = 1.0$", - transform=axis.transAxes, - fontsize=9, - ) - - axis.set_title("Confinement time ($\\tau_{\\text{E}}$) Comparison") - axis.set_ylabel("Confinement time, $\\tau_{\\text{E}}$ [s]") - axis.set_xlim(0.5, 1.5) - axis.set_xticks([]) - axis.set_xticklabels([]) - - # Add background color - axis.set_facecolor("#f0f0f0") - - -def plot_radial_build(axis: plt.Axes, mfile: MFile, colour_scheme: Literal[1, 2]): - """Plots the radial build of a fusion device on the given matplotlib axis. - - This function visualizes the different layers/components of the machine's radial build - (such as central solenoid, toroidal field coils, vacuum vessel, shields, blankets, etc.) - as a horizontal stacked bar chart. The thickness of each layer is extracted from the - provided `mfile`, and each segment is color-coded and labeled accordingly. - - If the toroidal field coil is inside the central solenoid (as indicated by the - "i_tf_inside_cs" flag in `mfile`), the order and labels of the components are - adjusted accordingly. - - Parameters - ---------- - axis : matplotlib.axes.Axes - The matplotlib axis on which to plot the radial build. - mfile : MFile - An object containing the machine build data, with required fields for each - radial component and the "i_tf_inside_cs" flag. - colour_scheme: - - Notes - ----- - This function modifies the provided axis in-place and does not return a value. - - Components with zero thickness are omitted from the plot. - - The legend displays the name and thickness (in meters) of each component. - """ - radial_variables = [ - "dr_bore", - "dr_cs", - "dr_cs_precomp", - "dr_cs_tf_gap", - "dr_tf_inboard", - "dr_tf_shld_gap", - "dr_shld_thermal_inboard", - "dr_shld_vv_gap_inboard", - "dr_vv_inboard", - "dr_shld_inboard", - "dr_shld_blkt_gap", - "dr_blkt_inboard", - "dr_fw_inboard", - "dr_fw_plasma_gap_inboard", - "rminor", - "dr_fw_plasma_gap_outboard", - "dr_fw_outboard", - "dr_blkt_outboard", - "dr_shld_blkt_gap", - "dr_vv_outboard", - "dr_shld_outboard", - "dr_shld_vv_gap_outboard", - "dr_shld_thermal_outboard", - "dr_tf_shld_gap", - "dr_tf_outboard", - ] - if int(mfile.get("i_tf_inside_cs", scan=-1)) == TFCSRadialConfiguration.TF_INSIDE_CS: - radial_variables[1] = "dr_tf_inboard" - radial_variables[2] = "dr_cs_tf_gap" - radial_variables[3] = "dr_cs" - radial_variables[4] = "dr_cs_precomp" - radial_variables[5] = "dr_tf_shld_gap" - - radial_build = [[mfile.get(rl, scan=-1) for rl in radial_variables]] - - radial_build = np.array(radial_build) - - for kk in range(radial_build.shape[0]): - radial_build[kk, 14] *= 2.0 - - radial_build = np.transpose(radial_build) - # ==================== - - radial_labels = [ - "Machine Bore", - "Central Solenoid", - "CS precompression", - "CS Coil gap", - "TF Coil Inboard Leg", - "TF Coil gap", - "Inboard Thermal Shield", - "Gap", - "Inboard VV", - "Inboard Shield", - "Gap", - "Inboard Blanket", - "Inboard First Wall", - "Inboard SOL", - "Plasma", - "Outboard SOL", - "Outboard First Wall", - "Outboard Blanket", - "Gap", - "Outboard VV", - "Outboard Shield", - "Gap", - "Outboard Thermal Shield", - "Gap", - "TF Coil Outboard Leg", - ] - if int(mfile.get("i_tf_inside_cs", scan=-1)) == TFCSRadialConfiguration.TF_INSIDE_CS: - radial_labels[1] = "TF Coil Inboard Leg" - radial_labels[2] = "CS Coil gap" - radial_labels[3] = "Central Solenoid" - radial_labels[4] = "CS precompression" - radial_labels[5] = "TF Coil gap" - - radial_color = [ - "white", - SOLENOID_COLOUR[colour_scheme - 1], - CSCOMPRESSION_COLOUR[colour_scheme - 1], - "white", - ( - TFC_COLOUR[colour_scheme - 1] - if mfile.get("i_tf_sup", scan=-1) != 0 - else "#b87333" - ), - "white", - THERMAL_SHIELD_COLOUR[colour_scheme - 1], - "white", - VESSEL_COLOUR[colour_scheme - 1], - SHIELD_COLOUR[colour_scheme - 1], - "white", - BLANKET_COLOUR[colour_scheme - 1], - FIRSTWALL_COLOUR[colour_scheme - 1], - "white", - PLASMA_COLOUR[colour_scheme - 1], - "white", - FIRSTWALL_COLOUR[colour_scheme - 1], - BLANKET_COLOUR[colour_scheme - 1], - "white", - VESSEL_COLOUR[colour_scheme - 1], - SHIELD_COLOUR[colour_scheme - 1], - "white", - THERMAL_SHIELD_COLOUR[colour_scheme - 1], - "white", - ( - TFC_COLOUR[colour_scheme - 1] - if mfile.get("i_tf_sup", scan=-1) != 0 - else "#b87333" - ), - ] - if int(mfile.get("i_tf_inside_cs", scan=-1)) == TFCSRadialConfiguration.TF_INSIDE_CS: - radial_color[1] = ( - TFC_COLOUR[colour_scheme - 1] - if mfile.get("i_tf_sup", scan=-1) != 0 - else "#b87333" - ) - radial_color[2] = "white" - radial_color[3] = SOLENOID_COLOUR[colour_scheme - 1] - radial_color[4] = CSCOMPRESSION_COLOUR[colour_scheme - 1] - radial_color[5] = "white" - - lower = np.zeros(radial_build.shape[1]) - for kk in range(radial_build.shape[0]): - axis.barh( - 0, - radial_build[kk, :], - left=lower, - height=0.8, - label=f"{radial_labels[kk]}\n[{radial_variables[kk]}]\n{radial_build[kk][0]:.3f} m", - color=radial_color[kk], - edgecolor="black", - linewidth=0.05, - ) - lower += radial_build[kk, :] - - axis.set_yticks([]) - - axis.legend( - bbox_to_anchor=(0.5, -0.1), - loc="upper center", - ncol=5, - ) - # Plot a vertical dashed line at rmajor - axis.axvline( - mfile.get("rmajor", scan=-1), - color="black", - linestyle="--", - linewidth=1.2, - label="Major Radius $R_0$", - ) - axis.minorticks_on() - axis.set_xlabel("Radius [m]") - - -def plot_lower_vertical_build( - axis: plt.Axes, mfile: MFile, colour_scheme: Literal[1, 2] -): - """Plots the lower vertical build of a fusion device on the given matplotlib axis. - - This function visualizes the different layers/components of the machine's vertical build - (such as plasma, first wall, divertor, shield, vacuum vessel, thermal shield, TF coil, etc.) - as a vertical stacked bar chart. The thickness of each layer is extracted from the - provided `mfile`, and each segment is color-coded and labeled accordingly. - - Parameters - ---------- - axis : - The matplotlib axis on which to plot the vertical build. - mfile : - An object containing the machine build data, with required fields for each - vertical component. - colour_scheme : - Colour scheme index to use for component colors. - - - Notes - ----- - This function modifies the provided axis in-place and does not return a value. - - Components with zero thickness are omitted from the plot. - - The legend displays the name and thickness (in meters) of each component. - """ - lower_vertical_variables = [ - "z_plasma_xpoint_upper", - "dz_xpoint_divertor", - "dz_divertor", - "dz_shld_upper", - "dz_vv_upper", - "dz_shld_vv_gap", - "dz_shld_thermal", - "dr_tf_shld_gap", - "dr_tf_inboard", - "dz_tf_cryostat", - ] - - lower_vertical_build = [[mfile.get(rl, scan=-1) for rl in lower_vertical_variables]] - - lower_vertical_build = np.array(lower_vertical_build) - - lower_vertical_build = np.transpose(lower_vertical_build) - - lower_vertical_labels = [ - "Plasma Height", - "Plasma - Divertor Gap", - "Divertor", - "Shield", - "Vacuum Vessel", - "Shield - VV Gap", - "Thermal shield", - "TF Coil - Shield Gap", - "TF Coil", - "TF Coil - Cryostat gap", - ] - - lower_vertical_color = [ - PLASMA_COLOUR[colour_scheme - 1], - "white", - "black", - SHIELD_COLOUR[colour_scheme - 1], - VESSEL_COLOUR[colour_scheme - 1], - "white", - THERMAL_SHIELD_COLOUR[colour_scheme - 1], - "white", - ( - TFC_COLOUR[colour_scheme - 1] - if mfile.get("i_tf_sup", scan=-1) != 0 - else "#b87333" - ), - "white", - ] - - # Remove build parts equal to zero - mask = ~(lower_vertical_build[:, 0] == 0.0) # noqa: RUF069 - filtered_vertical_build = lower_vertical_build[mask] - filtered_labels = [lbl for i, lbl in enumerate(lower_vertical_labels) if mask[i]] - filtered_colors = [col for i, col in enumerate(lower_vertical_color) if mask[i]] - - bottom = np.zeros(filtered_vertical_build.shape[1]) - for kk in range(filtered_vertical_build.shape[0]): - axis.bar( - np.arange(filtered_vertical_build.shape[1]), - -filtered_vertical_build[kk, :], - bottom=bottom, - width=0.8, - label=f"{filtered_labels[kk]}\n[{lower_vertical_variables[kk]}]\n{filtered_vertical_build[kk][0]:.3f} m", - color=filtered_colors[kk], - edgecolor="black", - linewidth=0.05, - ) - bottom -= filtered_vertical_build[kk, :] - - axis.set_xticks([]) - axis.legend( - bbox_to_anchor=(0, 0), - loc="upper left", - ncol=5, - ) - axis.minorticks_on() - axis.set_ylabel("Height [m]") - axis.title.set_text("Lower Vertical Build") - - -def plot_upper_vertical_build( - axis: plt.Axes, mfile: MFile, colour_scheme: Literal[1, 2] -): - """Plots the upper vertical build of a fusion device on the given matplotlib axis. - - This function visualizes the different layers/components of the machine's vertical build - (such as plasma, first wall, divertor, shield, vacuum vessel, thermal shield, TF coil, etc.) - as a vertical stacked bar chart. The thickness of each layer is extracted from the - provided `mfile`, and each segment is color-coded and labeled accordingly. - - Parameters - ---------- - axis: - The matplotlib axis on which to plot the vertical build. - mfile: - An object containing the machine build data, with required fields for each - vertical component. - colour_scheme: - Colour scheme index to use for component colors. - - Notes - ----- - This function modifies the provided axis in-place and does not return a value. - - Components with zero thickness are omitted from the plot. - - The legend displays the name and thickness (in meters) of each component. - """ - if mfile.get("i_single_null", scan=-1) == 1: - upper_vertical_variables = [ - "z_plasma_xpoint_upper", - "dz_fw_plasma_gap", - "dz_fw_upper", - "dz_blkt_upper", - "dr_shld_blkt_gap", - "dz_shld_upper", - "dz_vv_upper", - "dz_shld_vv_gap", - "dz_shld_thermal", - "dr_tf_shld_gap", - "dr_tf_inboard", - "dz_tf_cryostat", - ] - upper_vertical_labels = [ - "Plasma Height", - "First Wall - Plasma Gap", - "First Wall Upper", - "Blanket Upper", - "Shield-Blanket Gap", - "Shield Upper", - "Vacuum Vessel Upper", - "Shield-VV Gap", - "Thermal Shield", - "TF Coil - Shield Gap", - "TF Coil", - "TF Coil - Cryostat gap", - ] - upper_vertical_colours = [ - PLASMA_COLOUR[colour_scheme - 1], - "white", - FIRSTWALL_COLOUR[colour_scheme - 1], - BLANKET_COLOUR[colour_scheme - 1], - "white", - SHIELD_COLOUR[colour_scheme - 1], - VESSEL_COLOUR[colour_scheme - 1], - "white", - THERMAL_SHIELD_COLOUR[colour_scheme - 1], - "white", - ( - TFC_COLOUR[colour_scheme - 1] - if mfile.get("i_tf_sup", scan=-1) != 0 - else "#b87333" - ), - "white", - ] - # Double null case - else: - upper_vertical_variables = [ - "z_plasma_xpoint_upper", - "dz_xpoint_divertor", - "dz_divertor", - "dz_shld_upper", - "dz_vv_upper", - "dz_shld_vv_gap", - "dz_shld_thermal", - "dr_tf_shld_gap", - "dr_tf_inboard", - "dz_tf_cryostat", - ] - upper_vertical_labels = [ - "Plasma Height", - "Plasma - Divertor Gap", - "Divertor Upper", - "Shield Upper", - "Vacuum Vessel Upper", - "Shield-VV Gap", - "Thermal Shield", - "TF Coil - Shield Gap", - "TF Coil", - "TF Coil - Cryostat gap", - ] - upper_vertical_colours = [ - PLASMA_COLOUR[colour_scheme - 1], - "white", - "black", - SHIELD_COLOUR[colour_scheme - 1], - VESSEL_COLOUR[colour_scheme - 1], - "white", - THERMAL_SHIELD_COLOUR[colour_scheme - 1], - "white", - ( - TFC_COLOUR[colour_scheme - 1] - if mfile.get("i_tf_sup", scan=-1) != 0 - else "#b87333" - ), - "white", - ] - - # Get thicknesses for each layer - upper_vertical_build = np.array([ - mfile.get(rl, scan=-1) for rl in upper_vertical_variables - ]) - - # Remove build parts equal to zero - mask = ~(upper_vertical_build == 0.0) # noqa: RUF069 - filtered_build = upper_vertical_build[mask] - filtered_labels = [lbl for i, lbl in enumerate(upper_vertical_labels) if mask[i]] - filtered_colors = [col for i, col in enumerate(upper_vertical_colours) if mask[i]] - filtered_vars = [v for i, v in enumerate(upper_vertical_variables) if mask[i]] - - # Compute cumulative positions (bottoms) for stacking - bottoms = np.zeros_like(filtered_build) - for i in range(1, len(filtered_build)): - bottoms[i] = bottoms[i - 1] + filtered_build[i - 1] - - # Plot each layer as a bar, stacking upwards from zero - for kk in range(len(filtered_build)): - axis.bar( - 0, - filtered_build[kk], - bottom=bottoms[kk], - width=0.8, - label=f"{filtered_labels[kk]}\n[{filtered_vars[kk]}]\n{filtered_build[kk]:.3f} m", - color=filtered_colors[kk], - edgecolor="black", - linewidth=0.05, - ) - - axis.set_xticks([]) - axis.legend( - bbox_to_anchor=(0, 0), - loc="upper left", - ncol=6, - ) - axis.minorticks_on() - axis.set_ylabel("Height [m]") - axis.title.set_text("Upper Vertical Build") - - -def plot_density_limit_comparison(axis: plt.Axes, mfile: MFile, scan: int): - """Function to plot a scatter box plot of different density limit comparisons. - - Parameters - ---------- - axis : - Axis object to plot to. - mfile : - MFILE data object. - scan : - Scan number to use. - """ - old_asdex = mfile.get("nd_plasma_electron_max_array(1)", scan=scan) - borrass_iter_i = mfile.get("nd_plasma_electron_max_array(2)", scan=scan) - borrass_iter_ii = mfile.get("nd_plasma_electron_max_array(3)", scan=scan) - jet_edge_radiation = mfile.get("nd_plasma_electron_max_array(4)", scan=scan) - jet_simplified = mfile.get("nd_plasma_electron_max_array(5)", scan=scan) - hugill_murakami = mfile.get("nd_plasma_electron_max_array(6)", scan=scan) - greenwald = mfile.get("nd_plasma_electron_max_array(7)", scan=scan) - asdex_new = mfile.get("nd_plasma_electron_max_array(8)", scan=scan) - - # Data for the box plot - data = { - "Old ASDEX": old_asdex, - "Borrass ITER I": borrass_iter_i, - "Borrass ITER II": borrass_iter_ii, - "JET Edge Radiation": jet_edge_radiation, - "JET Simplified": jet_simplified, - "Hugill-Murakami": hugill_murakami, - "Greenwald": greenwald, - "ASDEX New": asdex_new, - } - data_values = list(data.values()) - - # Create the violin plot - axis.violinplot(data_values, showextrema=False) - - # Create the box plot - axis.boxplot(data_values, showfliers=True, showmeans=True, meanline=True, widths=0.3) - - # Scatter plot for each data point - colors = plt.cm.plasma(np.linspace(0, 1, len(data.values()))) - for index, (key, value) in enumerate(data.items()): - axis.scatter(1, value, color=colors[index], label=key, alpha=1.0) - axis.legend(loc="upper left", bbox_to_anchor=(1, 1)) - - # Calculate average, standard deviation, and median - avg_density_limit = np.mean(data_values) - std_density_limit = np.std(data_values) - median_density_limit = np.median(data_values) - - # Plot average, standard deviation, and median as text - axis.text( - 1.02, - 0.2, - rf"Average: {avg_density_limit * 1e-20:.4f} $\times 10^{{20}}$", - transform=axis.transAxes, - fontsize=9, - ) - axis.text( - 1.02, - 0.15, - rf"Standard Dev: {std_density_limit * 1e-20:.4f} $\times 10^{{20}}$", - transform=axis.transAxes, - fontsize=9, - ) - axis.text( - 1.02, - 0.1, - rf"Median: {median_density_limit * 1e-20:.4f} $\times 10^{{20}}$", - transform=axis.transAxes, - fontsize=9, - ) - - axis.set_yscale("log") - axis.set_title("Density Limit Comparison") - axis.set_ylabel(r"Density Limit [$10^{20}$ m$^{-3}$]") - axis.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x * 1e-20:.1f}")) - axis.set_xlim(0.5, 1.5) - axis.set_xticks([]) - axis.set_xticklabels([]) - axis.set_facecolor("#f0f0f0") - - -def plot_cs_coil_structure( - axis: plt.Axes, fig, mfile: MFile, scan: int, colour_scheme=1 -): - """Function to plot the coil structure of the CS. - - Parameters - ---------- - axis : - axis object to plot to - mfile : - MFILE - scan : - scan number to use - colour_scheme : - colour scheme to use for the plot (Default value = 1) - - """ - # Get CS coil parameters - dr_cs = mfile.get("dr_cs", scan=scan) - dr_cs_full = mfile.get("dr_cs_full", scan=scan) - dz_cs_full = mfile.get("dz_cs_full", scan=scan) - dz_cs = mfile.get("dz_cs_full", scan=scan) - dr_cs_bore = mfile.get("dr_cs_bore", scan=scan) - r_cs_current_filaments_array = [ - mfile.get(f"r_pf_cs_current_filaments{i}", scan=scan) for i in range(NFIXMX) - ] - z_cs_current_filaments_array = [ - mfile.get(f"z_pf_cs_current_filaments{i}", scan=scan) for i in range(NFIXMX) - ] - - # Plot the right side of the CS - right_cs = patches.Rectangle( - (dr_cs_bore, -dz_cs / 2), - dr_cs, - dz_cs, - edgecolor="black", - facecolor=SOLENOID_COLOUR[colour_scheme - 1], - lw=1.5, - ) - axis.add_patch(right_cs) - - # Plot the bore of the machine - bore_rect = patches.Rectangle( - (-dr_cs_bore, -dz_cs / 2), - dr_cs_bore * 2, - dz_cs, - edgecolor="black", - facecolor="lightgrey", - lw=1.0, - ) - axis.add_patch(bore_rect) - - left_cs = patches.Rectangle( - (-dr_cs_bore - dr_cs, -dz_cs / 2), - dr_cs, - dz_cs, - edgecolor="black", - facecolor=SOLENOID_COLOUR[colour_scheme - 1], - lw=1.5, - ) - axis.add_patch(left_cs) - - # Draw vertical lines to represent CS turns - # Get the turn width (radial thickness of each turn) - dr_cs_turn = mfile.get("dr_cs_turn", scan=scan) - dz_cs_turn = mfile.get("dz_cs_turn", scan=scan) - # Number of vertical lines (number of turns) - t_kwargs = {"color": "black", "linestyle": "--", "linewidth": 0.2} - if dr_cs_turn > 0: - n_lines = int(dr_cs / dr_cs_turn) - for i in range(1, n_lines): - x = dr_cs_bore + i * dr_cs_turn - axis.plot([x, x], [-dz_cs / 2, dz_cs / 2], **t_kwargs) - x_left = -dr_cs_bore - dr_cs + i * dr_cs_turn - axis.plot([x_left, x_left], [-dz_cs / 2, dz_cs / 2], **t_kwargs) - # Plot horizontal lines (along Z) for each turn - if dz_cs_turn > 0: - n_hlines = int(dz_cs / dz_cs_turn) - for j in range(1, n_hlines): - y = -dz_cs / 2 + j * dz_cs_turn - # Right CS - axis.plot([dr_cs_bore, dr_cs_bore + dr_cs], [y, y], **t_kwargs) - # Left CS - axis.plot([-dr_cs_bore - dr_cs, -dr_cs_bore], [y, y], **t_kwargs) - - l_kwargs = {"color": "black", "linestyle": "--", "linewidth": 0.6, "alpha": 0.5} - - # Plot a horizontal line at y = 0.0 - axis.axhline(y=0.0, **l_kwargs) - # Plot a vertical line at x = 0.0 - axis.axvline(x=0.0, **l_kwargs) - # Plot a vertical line at x = dr_cs_bore - axis.axvline(x=dr_cs_bore, **l_kwargs) - # Plot a vertical line at x = -dr_cs_bore - axis.axvline(x=-dr_cs_bore, **l_kwargs) - # Plot a vertical line at x = dr_cs_bore + dr_cs - axis.axvline(x=(dr_cs_bore + dr_cs), **l_kwargs) - # Plot a vertical line at x = -dr_cs_bore - dr_cs - axis.axvline(x=-(dr_cs_bore + dr_cs), **l_kwargs) - # Plot a vertical line at y= dz_cs / 2 - axis.axhline(y=(dz_cs / 2), **l_kwargs) - # Plot a vertical line at y= -dz_cs / 2 - axis.axhline(y=-(dz_cs / 2), **l_kwargs) - - # Plot a vertical line at x = r_cs_middle - axis.axvline(x=mfile.get("r_cs_middle", scan=scan), **l_kwargs) - # Plot a vertical line at x= -r_cs_middle - axis.axvline(x=-mfile.get("r_cs_middle", scan=scan), **l_kwargs) - - # Arrow for coil width - axis.annotate( - "", - xy=(0, (dz_cs_full / 2)), - xytext=(0, -(dz_cs_full / 2)), - arrowprops={"arrowstyle": "<->", "color": "black"}, - ) - - # Add a label for full coil width - axis.text( - 0.0, - -(dz_cs_full / 4), - f"{dz_cs_full:.3f} m", - fontsize=7, - color="black", - rotation=270, - verticalalignment="center", - horizontalalignment="center", - bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, - ) - - # Arrow for coil width - axis.annotate( - "", - xy=(-(dr_cs_full / 2), (dz_cs_full / 4)), - xytext=((dr_cs_full / 2), (dz_cs_full / 4)), - arrowprops={"arrowstyle": "<->", "color": "black"}, - ) - - # Add a label for full coil width - axis.text( - 0.0, - (dz_cs_full / 4), - f"{dr_cs_full:.3f} m", - fontsize=7, - color="black", - rotation=0, - verticalalignment="center", - horizontalalignment="center", - bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, - ) - - textstr_cs = ( - f"$\\mathbf{{Coil \\ parameters:}}$\n\n" - f"CS height vs TF internal height: {mfile.get('f_z_cs_tf_internal', scan=scan):.2f}\n" - f"CS thickness: {mfile.get('dr_cs', scan=scan):.4f} m\n" - f"CS radial middle: {mfile.get('r_cs_middle', scan=scan):.4f} m\n" - f"CS full height: {mfile.get('dz_cs_full', scan=scan):.4f} m\n" - f"CS full width: {mfile.get('dr_cs_full', scan=scan):.4f} m\n" - f"CS poloidal area: {mfile.get('a_cs_poloidal', scan=scan):.4f} m$^2$\n" - f"CS top-down toroidal area: {mfile.get('a_cs_toroidal', scan=scan):.4f} m$^2$\n" - f"$N_{{\\text{{turns}}}}:$ {mfile.get('n_pf_coil_turns[n_cs_pf_coils-1]', scan=scan):,.2f}\n" - f"$I_{{\\text{{peak}}}}:$ {mfile.get('c_pf_cs_coils_peak_ma[n_cs_pf_coils-1]', scan=scan):.3f} MA\n" - f"$B_{{\\text{{peak}}}}:$ {mfile.get('b_pf_coil_peak[n_cs_pf_coils-1]', scan=scan):.3f} T\n" - f"$F_{{\\text{{z,self,peak}}}}:$ {mfile.get('forc_z_cs_self_peak_midplane', scan=scan) / 1e6:.3f} MN\n" - f"$\\sigma_{{\\text{{z,self,peak}}}}:$ {mfile.get('stress_z_cs_self_peak_midplane', scan=scan) / 1e6:.3f} MPa\n" - f"$\\sigma_{{\\text{{mises,peak}}}}:$ {mfile.get('stress_mises_cs_peak', scan=scan) / 1e6:.3f} MPa\n" - f"$\\tau_{{\\text{{shear,peak}}}}:$ {mfile.get('stress_shear_cs_peak', scan=scan) / 1e6:.3f} MPa " - ) - - axis.text(0.5, 0.6, textstr_cs, **_text_layout(fig), bbox=_box_style("lightyellow")) - - # Plot the current filament points as blue dots and label them - - axis.plot( - r_cs_current_filaments_array, - z_cs_current_filaments_array, - "bo", - markersize=2, - label="CS, PF and Plasma Current Filaments", - ) - - axis.set_xlabel("R [m]") - axis.set_ylabel("Z [m]") - axis.set_title("Central Solenoid Poloidal Cross-Section") - axis.grid(True, linestyle="--", alpha=0.3) - axis.minorticks_on() - axis.legend() - - -def plot_cs_stress_time_profile(axis: plt.Axes, mfile: MFile, scan: int) -> None: - """Function to plot the time profile of the CS stress during the pulse.""" - pulse_timings = PulseTimings( - t_plant_pulse_coil_precharge=mfile.get( - "t_plant_pulse_coil_precharge", scan=scan - ), - t_plant_pulse_plasma_current_ramp_up=mfile.get( - "t_plant_pulse_plasma_current_ramp_up", scan=scan - ), - t_plant_pulse_fusion_ramp=mfile.get("t_plant_pulse_fusion_ramp", scan=scan), - t_plant_pulse_burn=mfile.get("t_plant_pulse_burn", scan=scan), - t_plant_pulse_plasma_current_ramp_down=mfile.get( - "t_plant_pulse_plasma_current_ramp_down", scan=scan - ), - t_plant_pulse_dwell=mfile.get("t_plant_pulse_dwell", scan=scan), - ) - - stress_z_cs_self_midplane_profile = np.zeros(pulse_timings.n_pf_active_points_total) - for i in range(pulse_timings.n_pf_active_points_total): - stress_z_cs_self_midplane_profile[i] = mfile.get( - f"stress_z_cs_self_midplane_profile[{i}]", scan=scan - ) - - # Plot stress vs time - axis.plot( - pulse_timings.pf_active_cumulative, - stress_z_cs_self_midplane_profile / 1e6, - "o-", - linewidth=2, - markersize=4, - label="$\\sigma_{z}$,Axial Stress", - ) - axis.set_xlabel("Pulse Time (s)") - axis.set_ylabel("Midplane Axial Stress (MPa)") - axis.minorticks_on() - axis.legend(loc="best") - axis.set_title("CS Midplane Axial Stress Time Profile") - axis.grid(True, alpha=0.3) - - -def plot_cs_turn_structure(axis: plt.Axes, fig, mfile: MFile, scan: int): - """Plot the CS turn structure""" - a_cs_turn = mfile.get("a_cs_turn", scan=scan) - dz_cs_turn = mfile.get("dz_cs_turn", scan=scan) - dr_cs_turn = mfile.get("dr_cs_turn", scan=scan) - - f_dr_dz_cs_turn = mfile.get("f_dr_dz_cs_turn", scan=scan) - radius_cs_turn_cable_space = mfile.get("radius_cs_turn_cable_space", scan=scan) - dz_cs_turn_conduit = mfile.get("dz_cs_turn_conduit", scan=scan) - dr_cs_turn_conduit = mfile.get("dr_cs_turn_conduit", scan=scan) - radius_cs_turn_corners = mfile.get("radius_cs_turn_corners", scan=scan) - f_a_cs_turn_steel = mfile.get("f_a_cs_turn_steel", scan=scan) - - # Plot the CS turn as a rectangle representing the conductor cross-section - # Assume dz_cs_turn is the diameter and dr_cs_turn is the length of the conductor cross-section - - # Draw the conductor cross-section as a rectangle - axis.add_patch( - patches.FancyBboxPatch( - (0, 0), - dr_cs_turn, - dz_cs_turn, - boxstyle=patches.BoxStyle( - "Round", pad=0, rounding_size=radius_cs_turn_corners - ), - edgecolor="black", - facecolor="grey", - lw=1.5, - label="CS Turn Steel Conduit", - ) - ) - - # Draw the conductor cross-section as a rectangle - axis.add_patch( - patches.Rectangle( - (dr_cs_turn_conduit + radius_cs_turn_cable_space, dz_cs_turn_conduit), - dr_cs_turn - ((2 * dr_cs_turn_conduit) + (2 * radius_cs_turn_cable_space)), - 2 * radius_cs_turn_cable_space, - facecolor="white", - lw=1.5, - label="CS Turn Cable Space", - zorder=2, - ) - ) - # Plot the right hand circle for the CS turn cable space - axis.add_patch( - patches.Circle( - ( - (dr_cs_turn - dr_cs_turn_conduit - radius_cs_turn_cable_space), - dz_cs_turn / 2, - ), - radius_cs_turn_cable_space, - facecolor="white", - lw=1.5, - zorder=3, - ) - ) - # Plot the left hand circle for the CS turn cable space - axis.add_patch( - patches.Circle( - ((dr_cs_turn_conduit + radius_cs_turn_cable_space), dz_cs_turn / 2), - radius_cs_turn_cable_space, - facecolor="white", - lw=1.5, - zorder=3, - ) - ) - - # Add plasma volume, areas and shaping information - textstr_turn = ( - f"$\\mathbf{{Turn \\ structure:}}$\n\n$A:$ {a_cs_turn:.4e}$ \\ \\text{{m}}^2$\n" - f"Turn width: {dr_cs_turn:.4e}$ \\ \\text{{m}}$\n" - f"Turn height: {dz_cs_turn:.4e}$ \\ \\text{{m}}$\n" - f"Turn width to height ratio: {f_dr_dz_cs_turn:.3f}\n" - f"Steel conduit width: {dr_cs_turn_conduit:.4e}$ \\ \\text{{m}}$\n" - f"Radius of turn cable space: {radius_cs_turn_cable_space:.4e}$ \\ \\text{{m}}$\n" - f"Radius of turn corner: {radius_cs_turn_corners:.4e}$ \\ \\text{{m}}$\n" - f"Fraction of turn area that is steel: {f_a_cs_turn_steel:.4f}\n" - ) - - axis.text( - 0.7, - 0.375, - textstr_turn, - **_text_layout(fig), - bbox=_box_style("lightyellow"), - ) - - axis.set_xlim(-dr_cs_turn * 0.2, dr_cs_turn * 1.2) - axis.set_ylim(-dz_cs_turn * 0.3, dz_cs_turn * 1.3) - axis.set_aspect("equal") - axis.set_xlabel("Length [m]") - axis.set_ylabel("Height [m]") - axis.set_title("CS Turn Conductor Cross-Section") - cs_legend = axis.legend(loc="upper right", bbox_to_anchor=(0.7, -0.25)) - cs_legend.get_frame().set_edgecolor("black") - axis.grid(True, linestyle="--", alpha=0.3) - - -def plot_tf_coil_structure(axis: plt.Axes, mfile: MFile, scan: int, colour_scheme=1): - """Plot the TF coil poloidal cross-section""" - plot_tf_coils(axis, mfile, scan, colour_scheme) - - x1 = mfile.get("r_tf_arc(1)", scan=scan) - y1 = mfile.get("z_tf_arc(1)", scan=scan) - x2 = mfile.get("r_tf_arc(2)", scan=scan) - y2 = mfile.get("z_tf_arc(2)", scan=scan) - x3 = mfile.get("r_tf_arc(3)", scan=scan) - y3 = mfile.get("z_tf_arc(3)", scan=scan) - x4 = mfile.get("r_tf_arc(4)", scan=scan) - y4 = mfile.get("z_tf_arc(4)", scan=scan) - x5 = mfile.get("r_tf_arc(5)", scan=scan) - y5 = mfile.get("z_tf_arc(5)", scan=scan) - - z_tf_inside_half = mfile.get("z_tf_inside_half", scan=scan) - z_tf_top = mfile.get("z_tf_top", scan=scan) - dr_tf_inboard = mfile.get("dr_tf_inboard", scan=scan) - r_tf_inboard_out = mfile.get("r_tf_inboard_out", scan=scan) - r_tf_outboard_in = mfile.get("r_tf_outboard_in", scan=scan) - r_tf_inboard_in = mfile.get("r_tf_inboard_in", scan=scan) - dr_tf_outboard = mfile.get("dr_tf_outboard", scan=scan) - len_tf_coil = mfile.get("len_tf_coil", scan=scan) - dz_tf_upper_lower_midplane = mfile.get("dz_tf_upper_lower_midplane", scan=scan) - - # Plot the points as black dots, number them, and connect them with lines - xs = [x1, x2, x3, x4, x5] - ys = [y1, y2, y3, y4, y5] - labels = [] - for i, (x, y) in enumerate(zip(xs, ys, strict=False), 1): - axis.plot(x, y, "ko", markersize=8) - axis.text( - x, - y, - str(i), - color="red", - fontsize=5, - ha="center", - va="center", - fontweight="bold", - ) - labels.append(f"TF Arc Point {i}: ({x:.2f}, {y:.2f})") - - # ========================================================= - - # If D-shaped coil, plot the full internal height arrow - if mfile.get("i_tf_shape", scan=scan) == 1: - # Arrow for internal coil width - axis.annotate( - "", - xy=(x2, y2), - xytext=(x4, y4), - arrowprops={"arrowstyle": "<->", "color": "black"}, - ) - - # Add a label for the internal coil width - axis.text( - x2, - 0.0, - f"{y2 - y4:.3f} m", - fontsize=7, - color="black", - rotation=270, - verticalalignment="center", - horizontalalignment="center", - bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, - zorder=100, # Ensure label is on top of all plots - ) - - # ========================================================== - - # Arrow for the full TF coil height - if mfile.get("i_tf_shape", scan=scan) == 1: - x = x2 * 0.9 - elif mfile.get("i_tf_shape", scan=scan) == 2: - x = (x2 - x1) / 2 - - axis.annotate( - "", - xy=(x, y2 + dr_tf_inboard), - xytext=(x, y4 - dr_tf_inboard), - arrowprops={"arrowstyle": "<->", "color": "black"}, - ) - - # Add a label for the full TF coil height - axis.text( - x, - 0.0, - f"{((y2 + 2 * dr_tf_inboard) - y4):.3f} m", - fontsize=7, - color="black", - rotation=270, - verticalalignment="center", - horizontalalignment="center", - bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, - zorder=101, # Ensure label is on top of all plots - ) - - # ========================================================== - - # Arrow for top half height of TF coil - axis.annotate( - "", - xy=(-2.0, 0), - xytext=(-2.0, y2 + dr_tf_inboard), - arrowprops={"arrowstyle": "<->", "color": "black"}, - ) - axis.axhline(y=y2 + dr_tf_inboard, color="black", linestyle="--", linewidth=1) - - # Add a label for top of TF coil - axis.text( - -2.0, - (y2 + dr_tf_inboard) / 2, - f"{y2 + dr_tf_inboard:.3f} m", - fontsize=7, - color="black", - rotation=270, - verticalalignment="center", - horizontalalignment="center", - bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, - ) - - # ========================================================== - - # Arrow for bottom half height of TF coil - axis.annotate( - "", - xy=(-2.0, 0), - xytext=(-2.0, y4 - dr_tf_inboard), - arrowprops={"arrowstyle": "<->", "color": "black"}, - ) - axis.axhline(y=y4 - dr_tf_inboard, color="black", linestyle="--", linewidth=1) - - # Add a label for top of TF coil - axis.text( - -2.0, - -z_tf_top / 2, - f"{y4 - dr_tf_inboard:.3f} m", - fontsize=7, - color="black", - rotation=270, - verticalalignment="center", - horizontalalignment="center", - bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, - ) - - # Arrow for top inside internal height - axis.annotate( - "", - xy=(-1.0, 0), - xytext=(-1.0, y2), - arrowprops={"arrowstyle": "<->", "color": "black"}, - ) - axis.axhline(y=y2, color="black", linestyle="--", linewidth=1) - - # Add a label for height of top internal height - axis.text( - -1.0, - y2 / 2, - f"{y2:.3f} m", - fontsize=7, - color="black", - rotation=270, - verticalalignment="center", - horizontalalignment="center", - bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, - ) - - # ========================================================= - - # Arrow for coil internal height - axis.annotate( - "", - xy=(-1.0, 0), # Inner plasma edge - xytext=(-1.0, -z_tf_inside_half), # Center - arrowprops={"arrowstyle": "<->", "color": "black"}, - ) - axis.axhline(y=-z_tf_inside_half, color="black", linestyle="--", linewidth=1) - - # Add a label for coil internal height - axis.text( - -1.0, - -z_tf_inside_half / 2, - f"{z_tf_inside_half:.3f} m", - fontsize=7, - color="black", - rotation=270, - verticalalignment="center", - horizontalalignment="center", - bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, - ) - # ========================================================= - - # Arrow for internal coil width - axis.annotate( - "", - xy=(r_tf_inboard_out, -z_tf_inside_half / 12), - xytext=(r_tf_outboard_in, -z_tf_inside_half / 12), - arrowprops={"arrowstyle": "<->", "color": "black"}, - ) - - # Add a label for the internal coil width - axis.text( - (r_tf_inboard_out + r_tf_outboard_in) / 1.5, - -z_tf_inside_half / 12, - f"{mfile.get('dr_tf_internal_midplane', scan=scan):.3f} m", - fontsize=7, - color="black", - verticalalignment="center", - horizontalalignment="center", - bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, - zorder=100, # Ensure label is on top of all plots - ) - - # ============================================================= - - # Arrow for full coil width - axis.annotate( - "", - xy=(r_tf_inboard_in, 0.0), - xytext=(r_tf_outboard_in + dr_tf_outboard, 0.0), - arrowprops={"arrowstyle": "<|-|>", "color": "black"}, - zorder=100, # Ensure label is on top of all plots - ) - - # Add a label for the full coil width - axis.text( - (r_tf_inboard_out + r_tf_outboard_in) / 1.5, - 0.0, - f"{mfile.get('dr_tf_full_midplane', scan=scan):.3f} m", - fontsize=7, - color="black", - verticalalignment="center", - horizontalalignment="center", - bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, - zorder=100, # Ensure label is on top of all plots - ) - - # ============================================================= - - # Plot vertical lines for the inboard TF coil start and end - axis.axvline( - r_tf_inboard_in, - color="black", - linestyle="--", - linewidth=1, - alpha=0.5, - label="TF Inboard Start", - ) - axis.axvline( - r_tf_inboard_out, - color="black", - linestyle="--", - linewidth=1, - alpha=0.5, - label="TF Inboard End", - ) - # Plot vertical lines for the outboard TF coil start and end - axis.axvline( - r_tf_outboard_in, - color="black", - linestyle="--", - linewidth=1, - alpha=0.5, - label="TF Outboard Start", - ) - axis.axvline( - r_tf_outboard_in + dr_tf_outboard, - color="black", - linestyle="--", - linewidth=1, - alpha=0.5, - label="TF Outboard End", - ) - - # Add a label for the inboard thickness - axis.text( - r_tf_inboard_in, - (y4 - dr_tf_inboard) * 1.1, - rf"$\Delta r = ${dr_tf_inboard:.3f} m", - fontsize=7, - color="black", - verticalalignment="center", - bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, - ) - - # Add a label for the outboard thickness - axis.text( - r_tf_outboard_in, - (y4 - dr_tf_inboard) * 1.1, - rf"$\Delta r = ${dr_tf_outboard:.3f} m", - fontsize=7, - color="black", - verticalalignment="center", - bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, - ) - - # ============================================================== - - # Add a label for the length of the coil - axis.text( - (r_tf_outboard_in + 2 * dr_tf_outboard), - 0.0, - rf"Length of coil = {len_tf_coil:.3f} m", - fontsize=7, - color="black", - verticalalignment="center", - bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, - zorder=100, # Ensure label is on top of all plots - ) - - # ============================================================== - - # Add a label for the length of the coil - axis.text( - (r_tf_outboard_in + 2 * dr_tf_outboard), - -1.0, - f"$\\Delta Z$ upper and lower to midplane = {dz_tf_upper_lower_midplane:.3f} m", - fontsize=7, - color="black", - verticalalignment="center", - bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, - ) - - # ============================================================== - - # Add arow for inboard coil radius - axis.annotate( - "", - xy=(r_tf_inboard_in, 0), - xytext=(0, 0), - arrowprops={"arrowstyle": "->", "color": "black"}, - ) - - # Add label for inboard coil radius - axis.text( - r_tf_inboard_in / 2, - 0.0, - f"{r_tf_inboard_in:.3f} m", - fontsize=7, - color="black", - verticalalignment="center", - horizontalalignment="center", - bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, - zorder=101, # Ensure label is on top of all plots - ) - - # ============================================================= - - # ============================================================== - - if mfile.get("i_tf_shape", scan=scan) == 1: - # Add arow for inboard coil radius - axis.annotate( - "", - xy=(r_tf_outboard_in + dr_tf_outboard, y2 + dr_tf_inboard), - xytext=(0, y2 + dr_tf_inboard), - arrowprops={"arrowstyle": "->", "color": "black"}, - ) - - # Add label for inboard coil radius - axis.text( - r_tf_inboard_in / 2, - y2 + dr_tf_inboard, - f"{r_tf_outboard_in + dr_tf_outboard:.3f} m", - fontsize=7, - color="black", - verticalalignment="center", - horizontalalignment="center", - bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, - ) - - axis.plot( - 0, y2 + dr_tf_inboard, marker="o", color="black", markersize=7, zorder=100 - ) - - # ============================================================== - - y_center = y2 - ((y2 - y4) / 2) - # also draw a red horizontal line at the same vertical centre - axis.axhline(y=y_center, color="red", linestyle="--", linewidth=1.0, zorder=5) - - # Add a label the plasma and TF vertical centre distance offset - axis.text( - (r_tf_outboard_in + 2 * dr_tf_outboard), - -2.0, - f"$\\Delta Z$ coil centre to plasma centre = {mfile.get('dz_tf_plasma_centre_offset', scan=scan):.3f} m", - fontsize=7, - color="black", - verticalalignment="center", - bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, - ) - - # ============================================================= - - # Plot a red dot at (0,0) - axis.plot(0, 0, marker="o", color="red", markersize=7) - - # Plot a red dashed vertical line at R=0 - axis.axvline(0, color="red", linestyle="--", linewidth=1) - - # Add centre line at - axis.axhline(y=0, color="red", linestyle="--", linewidth=1) - axis.set_xlim(-3.0, (r_tf_outboard_in + dr_tf_outboard) * 1.4) - axis.set_ylim((y4 - dr_tf_inboard) * 1.2, (y2 + dr_tf_inboard) * 1.2) - axis.set_xlabel("R [m]") - axis.set_ylabel("Z [m]") - axis.set_title("TF Coil Poloidal Cross-Section") - axis.minorticks_on() - axis.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.2) - # Move the legend to above the plot - axis.legend(labels, loc="upper center", bbox_to_anchor=(1.01, 0.85), ncol=1) - - -def plot_iteration_variables(axis: plt.Axes, m_file: MFile, scan: int): - """Plot the iteration variables and where they lay in their bounds on a given axes - - Parameters - ---------- - axis: plt.Axes : - - m_file: MFile : - - scan: int : - - """ - # Get total number of iteration variables - n_itvars = int(m_file.get("n_iteration_variables", scan=scan)) - - y_labels = [] - y_pos = [] - n_plot = 0 - - # Build a mapping from itvar index to its name (description) - itvar_names = {} - for var in m_file.data: - if var.startswith("itvar"): - idx = int(var[5:]) # e.g. "itvar001" -> 1 - itvar_names[idx] = m_file.data[var].var_description - - for n_plot, n in enumerate(range(1, n_itvars + 1)): - # Get the final value of the iteration variable, its bounds, and relative change - itvar_final = m_file.get(f"itvar{n:03d}", scan=scan) - itvar_upper = m_file.get(f"boundu{n:03d}", scan=scan) - itvar_lower = m_file.get(f"boundl{n:03d}", scan=scan) - itvar_relative_change = m_file.get(f"xcm{n:03d}", scan=scan) - final_value_normalised = m_file.get(f"nitvar{n:03d}", scan=scan) - - # Use the variable name if available, else fallback to "itvarXXX" - var_label = itvar_names.get(n, f"itvar{n:03d}") - - norm_relative_change = ( - ((itvar_final / itvar_relative_change) - itvar_lower) - / (itvar_upper - itvar_lower) - if itvar_final != itvar_lower - else 0 - ) - - # Plot square marker at the final value if at bounds - if np.isclose(final_value_normalised, 1.0, atol=1e-3): - axis.plot( - 1, - n_plot, - "s", - color="black", - markersize=8, - label="Lower Bound" if n_plot == 0 else "", - ) - elif np.isclose(final_value_normalised, 0.0, atol=1e-3): - axis.plot( - 0, - n_plot, - "s", - color="black", - markersize=8, - label="Upper Bound" if n_plot == 0 else "", - ) - # Draw a horizontal bar from 0 to norm_final at y=n_plot - else: - axis.barh( - n_plot, - final_value_normalised, - left=0, - height=1.0, - color="blue", - edgecolor="black", - linewidth=1.5, - alpha=0.7, - label="Final Value" if n_plot == 0 else "", - ) - - # Plot scatter point for normalised relative change - axis.scatter( - norm_relative_change, - n_plot, - color="black", - marker="o", - linewidths=2, - alpha=1.0, - label="Initial Value" if n_plot == 0 else "", - ) - - # Draw an arrow from the initial value to the final value - axis.annotate( - "", - xy=(final_value_normalised, n_plot), - xytext=(norm_relative_change, n_plot), - arrowprops={ - "arrowstyle": "->", - "color": "black", - "linestyle": "--", - "linewidth": 1.0, - "alpha": 0.9, - }, - ) - # Plot the value as a number at x = 0.5 - axis.text( - 0.5, - n_plot, - f"{itvar_final:,.8g}", - va="center", - ha="center", - fontsize=10, - color=( - "orange" - if np.isclose(final_value_normalised, 1.0, atol=1e-3) - or np.isclose(final_value_normalised, 0.0, atol=1e-3) - else "green" - ), - bbox={ - "boxstyle": "round", - "facecolor": "white", - "alpha": 0.8, - "edgecolor": "white", - "linewidth": 1, - }, - ) - - # Plot the value of the upper bound to the right of x=1 - axis.text( - 1.05, - n_plot, - f"{itvar_upper:,.3g}", - va="center", - ha="left", - fontsize=10, - color="gray", - ) - # Plot the value of the lower bound to the left of x=0 - axis.text( - -0.05, - n_plot, - f"{itvar_lower:,.3g}", - va="center", - ha="right", - fontsize=10, - color="gray", - ) - y_labels.append(var_label) - y_pos.append(n_plot) - - # Plot vertical lines at x=0 and x=1 to indicate bounds - axis.axvline(0, color="darkgreen", linewidth=2, zorder=0) - axis.axvline(1, color="red", linewidth=2, zorder=0) - axis.set_yticks(y_pos) - axis.set_yticklabels(y_labels) - axis.set_xticks([]) - axis.set_xticklabels([]) - axis.set_facecolor("#f5f5f5") - axis.set_xlim(-0.2, 1.2) # Normalised bounds - axis.set_title("Iteration Variables Bounds") - axis.set_xticks(np.arange(0, 1.0, 0.1)) - axis.grid(True, axis="x", linestyle="--", alpha=0.3) - axis.legend(loc="upper left", bbox_to_anchor=(-0.15, 1.05), ncol=1) - - -def plot_tf_stress(axis: plt.Axes, mfile: MFile): - """Function to plot the TF coil stress from the SIG_TF.json file. - - Input file: - SIG_TF.json - - Parameters - ---------- - axis: plt.Axes : - - mfile: MFile : - - """ - # Step 1 : Data extraction - # ---------------------------------------------------------------------------------------------- - # Number of physical quantity value per coil layer - n_radial_array_layer = 0 - - # Physical quantities : full vectors - radius = [] - radial_smeared_stress = [] - toroidal_smeared_stress = [] - vertical_smeared_stress = [] - tresca_smeared_stress = [] - radial_stress = [] - toroidal_stress = [] - vertical_stress = [] - vm_stress = [] - tresca_stress = [] - cea_tresca_stress = [] - radial_strain = [] - toroidal_strain = [] - vertical_strain = [] - radial_displacement = [] - - # Physical quantity : WP stress - wp_vertical_stress = [] - - # Physical quantity : values at layer border - bound_radius = [] - bound_radial_smeared_stress = [] - bound_toroidal_smeared_stress = [] - bound_vertical_smeared_stress = [] - bound_tresca_smeared_stress = [] - bound_radial_stress = [] - bound_toroidal_stress = [] - bound_vertical_stress = [] - bound_vm_stress = [] - bound_tresca_stress = [] - bound_cea_tresca_stress = [] - bound_radial_strain = [] - bound_toroidal_strain = [] - bound_vertical_strain = [] - bound_radial_displacement = [] - - with open( - mfile.filename.with_name(mfile.filename.name.replace("MFILE.DAT", "SIG_TF.json")) - ) as f: - sig_data = json.load(f) - - # Getting the data to be plotted - n_radial_array_layer = sig_data["Points per layers"] - n_points = len(sig_data["Radius (m)"]) - n_layers = int(n_points / n_radial_array_layer) - for ii in range(n_layers): - # Full vector - radius.append([]) - radial_stress.append([]) - toroidal_stress.append([]) - vertical_stress.append([]) - radial_smeared_stress.append([]) - toroidal_smeared_stress.append([]) - vertical_smeared_stress.append([]) - vm_stress.append([]) - tresca_stress.append([]) - cea_tresca_stress.append([]) - radial_displacement.append([]) - - for jj in range(n_radial_array_layer): - radius[ii].append(sig_data["Radius (m)"][ii * n_radial_array_layer + jj]) - radial_stress[ii].append( - sig_data["Radial stress (MPa)"][ii * n_radial_array_layer + jj] - ) - toroidal_stress[ii].append( - sig_data["Toroidal stress (MPa)"][ii * n_radial_array_layer + jj] - ) - if len(sig_data["Vertical stress (MPa)"]) == 1: - vertical_stress[ii].append(sig_data["Vertical stress (MPa)"][0]) - else: - vertical_stress[ii].append( - sig_data["Vertical stress (MPa)"][ii * n_radial_array_layer + jj] - ) - radial_smeared_stress[ii].append( - sig_data["Radial smear stress (MPa)"][ii * n_radial_array_layer + jj] - ) - toroidal_smeared_stress[ii].append( - sig_data["Toroidal smear stress (MPa)"][ii * n_radial_array_layer + jj] - ) - vertical_smeared_stress[ii].append( - sig_data["Vertical smear stress (MPa)"][ii * n_radial_array_layer + jj] - ) - vm_stress[ii].append( - sig_data["Von-Mises stress (MPa)"][ii * n_radial_array_layer + jj] - ) - tresca_stress[ii].append( - sig_data["CEA Tresca stress (MPa)"][ii * n_radial_array_layer + jj] - ) - cea_tresca_stress[ii].append( - sig_data["CEA Tresca stress (MPa)"][ii * n_radial_array_layer + jj] - ) - radial_displacement[ii].append( - sig_data["rad. displacement (mm)"][ii * n_radial_array_layer + jj] - ) - - # Layer lower boundaries values - bound_radius.append(sig_data["Radius (m)"][ii * n_radial_array_layer]) - bound_radial_stress.append( - sig_data["Radial stress (MPa)"][ii * n_radial_array_layer] - ) - bound_toroidal_stress.append( - sig_data["Toroidal stress (MPa)"][ii * n_radial_array_layer] - ) - if len(sig_data["Vertical stress (MPa)"]) == 1: - bound_vertical_stress.append(sig_data["Vertical stress (MPa)"][0]) - else: - bound_vertical_stress.append( - sig_data["Vertical stress (MPa)"][ii * n_radial_array_layer] - ) - bound_radial_smeared_stress.append( - sig_data["Radial smear stress (MPa)"][ii * n_radial_array_layer] - ) - bound_toroidal_smeared_stress.append( - sig_data["Toroidal smear stress (MPa)"][ii * n_radial_array_layer] - ) - bound_vertical_smeared_stress.append( - sig_data["Vertical smear stress (MPa)"][ii * n_radial_array_layer] - ) - bound_vm_stress.append( - sig_data["Von-Mises stress (MPa)"][ii * n_radial_array_layer] - ) - bound_tresca_stress.append( - sig_data["CEA Tresca stress (MPa)"][ii * n_radial_array_layer] - ) - bound_cea_tresca_stress.append( - sig_data["CEA Tresca stress (MPa)"][ii * n_radial_array_layer] - ) - bound_radial_displacement.append( - sig_data["rad. displacement (mm)"][ii * n_radial_array_layer] - ) - - # Layer upper boundaries values - bound_radius.append(sig_data["Radius (m)"][(ii + 1) * n_radial_array_layer - 1]) - bound_radial_stress.append( - sig_data["Radial stress (MPa)"][(ii + 1) * n_radial_array_layer - 1] - ) - bound_toroidal_stress.append( - sig_data["Toroidal stress (MPa)"][(ii + 1) * n_radial_array_layer - 1] - ) - if len(sig_data["Vertical stress (MPa)"]) == 1: - bound_vertical_stress.append(sig_data["Vertical stress (MPa)"][0]) - else: - bound_vertical_stress.append( - sig_data["Vertical stress (MPa)"][(ii + 1) * n_radial_array_layer - 1] - ) - bound_radial_smeared_stress.append( - sig_data["Radial smear stress (MPa)"][(ii + 1) * n_radial_array_layer - 1] - ) - bound_toroidal_smeared_stress.append( - sig_data["Toroidal smear stress (MPa)"][(ii + 1) * n_radial_array_layer - 1] - ) - bound_vertical_smeared_stress.append( - sig_data["Vertical smear stress (MPa)"][(ii + 1) * n_radial_array_layer - 1] - ) - bound_vm_stress.append( - sig_data["Von-Mises stress (MPa)"][(ii + 1) * n_radial_array_layer - 1] - ) - bound_tresca_stress.append( - sig_data["CEA Tresca stress (MPa)"][(ii + 1) * n_radial_array_layer - 1] - ) - bound_cea_tresca_stress.append( - sig_data["CEA Tresca stress (MPa)"][(ii + 1) * n_radial_array_layer - 1] - ) - bound_radial_displacement.append( - sig_data["rad. displacement (mm)"][(ii + 1) * n_radial_array_layer - 1] - ) - - # TRESCA smeared stress [MPa] - for ii in range(n_layers): - tresca_smeared_stress.append([]) - - bound_tresca_smeared_stress.extend([ - max( - abs(radial_smeared_stress[ii][0]), - abs(toroidal_smeared_stress[ii][0]), - ) - + vertical_smeared_stress[ii][0], - max( - abs(radial_smeared_stress[ii][n_radial_array_layer - 1]), - abs(toroidal_smeared_stress[ii][n_radial_array_layer - 1]), - ) - + vertical_smeared_stress[ii][n_radial_array_layer - 1], - ]) - for jj in range(n_radial_array_layer): - tresca_smeared_stress[ii].append( - max( - abs(radial_smeared_stress[ii][jj]), - abs(toroidal_smeared_stress[ii][jj]), - ) - + vertical_smeared_stress[ii][jj] - ) - - # Strains - if len(sig_data) > 16: - for ii in range(n_layers): - radial_strain.append([]) - toroidal_strain.append([]) - vertical_strain.append([]) - - bound_radial_strain.extend([ - sig_data["Radial strain"][ii * n_radial_array_layer], - sig_data["Radial strain"][(ii + 1) * n_radial_array_layer - 1], - ]) - bound_toroidal_strain.extend([ - sig_data["Toroidal strain"][ii * n_radial_array_layer], - sig_data["Toroidal strain"][(ii + 1) * n_radial_array_layer - 1], - ]) - bound_vertical_strain.extend([ - sig_data["Vertical strain"][ii * n_radial_array_layer], - sig_data["Vertical strain"][(ii + 1) * n_radial_array_layer - 1], - ]) - for jj in range(n_radial_array_layer): - radial_strain[ii].append( - sig_data["Radial strain"][ii * n_radial_array_layer + jj] - ) - toroidal_strain[ii].append( - sig_data["Toroidal strain"][ii * n_radial_array_layer + jj] - ) - vertical_strain[ii].append( - sig_data["Vertical strain"][ii * n_radial_array_layer + jj] - ) - - if "WP smeared stress (MPa)" in sig_data: - wp_vertical_stress.append(sig_data["WP smeared stress (MPa)"][jj]) - - axis_tick_size = 12 - legend_size = 10 - mark_size = 10 - line_width = 3.5 - - # PLOT 1 : Stress summary - # ------------------------ - - ax = axis[0] - for ii in range(n_layers): - ax.plot( - radius[ii], - radial_stress[ii], - "-", - linewidth=line_width, - color="lightblue", - ) - ax.plot( - radius[ii], - toroidal_stress[ii], - "-", - linewidth=line_width, - color="wheat", - ) - ax.plot( - radius[ii], - vertical_stress[ii], - "-", - linewidth=line_width, - color="lightgrey", - ) - ax.plot(radius[ii], tresca_stress[ii], "-", linewidth=line_width, color="pink") - ax.plot(radius[ii], vm_stress[ii], "-", linewidth=line_width, color="violet") - ax.plot( - radius[0], - radial_stress[0], - "--", - color="dodgerblue", - label=r"$\sigma_{rr}$", - ) - ax.plot( - radius[0], - toroidal_stress[0], - "--", - color="orange", - label=r"$\sigma_{\theta\theta}$", - ) - ax.plot( - radius[0], - vertical_stress[0], - "--", - color="mediumseagreen", - label=r"$\sigma_{zz}$", - ) - ax.plot( - radius[0], - tresca_stress[0], - "-", - color="crimson", - label=r"$\sigma_{TRESCA}$", - ) - ax.plot( - radius[0], - vm_stress[0], - "-", - color="darkviolet", - label=r"$\sigma_{Von\ mises}$", - ) - for ii in range(1, n_layers): - ax.plot(radius[ii], radial_stress[ii], "--", color="dodgerblue") - ax.plot(radius[ii], toroidal_stress[ii], "--", color="orange") - ax.plot(radius[ii], vertical_stress[ii], "--", color="mediumseagreen") - ax.plot(radius[ii], tresca_stress[ii], "-", color="crimson") - ax.plot(radius[ii], vm_stress[ii], "-", color="darkviolet") - ax.plot( - bound_radius, - bound_radial_stress, - "|", - markersize=mark_size, - color="dodgerblue", - ) - ax.plot( - bound_radius, - bound_toroidal_stress, - "|", - markersize=mark_size, - color="orange", - ) - ax.plot( - bound_radius, - bound_vertical_stress, - "|", - markersize=mark_size, - color="mediumseagreen", - ) - ax.plot( - bound_radius, - bound_tresca_stress, - "|", - markersize=mark_size, - color="crimson", - ) - ax.plot(bound_radius, bound_vm_stress, "|", markersize=mark_size, color="darkviolet") - ax.grid(True) - ax.set_ylabel(r"$\sigma$ [$MPa$]", fontsize=axis_tick_size) - ax.set_title("Structure Stress Summary") - ax.legend(loc="center left", bbox_to_anchor=(1, 0.5), fontsize=legend_size) - - # PLOT 2 : Smeared stress summary - # ------------------------ - ax = axis[1] - for ii in range(n_layers): - ax.plot( - radius[ii], - radial_smeared_stress[ii], - "-", - linewidth=line_width, - color="lightblue", - ) - ax.plot( - radius[ii], - toroidal_smeared_stress[ii], - "-", - linewidth=line_width, - color="wheat", - ) - ax.plot( - radius[ii], - vertical_smeared_stress[ii], - "-", - linewidth=line_width, - color="lightgrey", - ) - ax.plot( - radius[ii], - tresca_smeared_stress[ii], - "-", - linewidth=line_width, - color="pink", - ) - ax.plot( - radius[0], - radial_smeared_stress[0], - "--", - color="dodgerblue", - label=r"$\sigma_{rr}^\mathrm{smeared}$", - ) - ax.plot( - radius[0], - toroidal_smeared_stress[0], - "--", - color="orange", - label=r"$\sigma_{\theta\theta}^\mathrm{smeared}$", - ) - ax.plot( - radius[0], - vertical_smeared_stress[0], - "--", - color="mediumseagreen", - label=r"$\sigma_{zz}^\mathrm{smeared}$", - ) - ax.plot( - radius[0], - tresca_smeared_stress[0], - "-", - color="crimson", - label=r"$\sigma_{TRESCA}^\mathrm{smeared}$", - ) - for ii in range(1, n_layers): - ax.plot(radius[ii], radial_smeared_stress[ii], "--", color="dodgerblue") - ax.plot(radius[ii], toroidal_smeared_stress[ii], "--", color="orange") - ax.plot(radius[ii], vertical_smeared_stress[ii], "--", color="mediumseagreen") - ax.plot(radius[ii], tresca_smeared_stress[ii], "-", color="crimson") - ax.plot( - bound_radius, - bound_radial_smeared_stress, - "|", - markersize=mark_size, - color="dodgerblue", - ) - ax.plot( - bound_radius, - bound_toroidal_smeared_stress, - "|", - markersize=mark_size, - color="orange", - ) - ax.plot( - bound_radius, - bound_vertical_smeared_stress, - "|", - markersize=mark_size, - color="mediumseagreen", - ) - ax.plot( - bound_radius, - bound_tresca_smeared_stress, - "|", - markersize=mark_size, - color="crimson", - ) - ax.grid(True) - ax.set_ylabel(r"$\sigma$ [$MPa$]", fontsize=axis_tick_size) - ax.set_title("Smeared Stress Summary") - ax.legend(loc="center left", bbox_to_anchor=(1, 0.5), fontsize=legend_size) - - # PLOT 4 : Displacement - # ---------------------- - ax = axis[2] - ax.plot(radius[0], radial_displacement[0], color="dodgerblue") - for ii in range(1, n_layers): - ax.plot(radius[ii], radial_displacement[ii], color="dodgerblue") - ax.grid(True) - ax.set_ylabel(r"$u_{r}$ [mm]", fontsize=axis_tick_size) - ax.set_xlabel(r"$R$ [$m$]", fontsize=axis_tick_size) - ax.set_title("Radial Displacement") - # Only set legend for the last plot if needed - - # Set x-label only on the last axis - axis[2].set_xlabel(r"$R$ [$m$]", fontsize=axis_tick_size) - - # Set minor ticks on for all axes - for ax in axis: - ax.minorticks_on() - # Set x-ticks and y-ticks font size for all axes - for ax in axis: - ax.tick_params(axis="x", labelsize=axis_tick_size) - ax.tick_params(axis="y", labelsize=axis_tick_size) - plt.tight_layout() - - -def draw_bend( - ax: Axes, - elbow_radius: float, - theta_span: float, - radius_pipe: float, - title: str = "Bend", - alpha: float = 0.8, -): - """ - Draws a circular pipe bend with centerline and inner/outer boundaries. - - Parameters - ---------- - ax: - Target axes for plotting. - elbow_radius: - Radius of the elbow in meters. - theta_span: - Array of angles [0, θ] where θ is pi/2 or pi. - radius_pipe: - Pipe radius in meters (fallback to 0.1m if not provided). - title: - Plot title string. - alpha: - fill opacity - """ - # Convert all inputs to mm - elbow_radius_mm = elbow_radius * 1000 - pipe_radius_mm = radius_pipe * 1000 - - theta = np.linspace(0, theta_span, 100) - x_center = elbow_radius_mm * np.cos(theta) - y_center = elbow_radius_mm * np.sin(theta) - - # Outer and inner walls (offset by ± pipe radius in mm) - x_outer = (elbow_radius_mm + pipe_radius_mm) * np.cos(theta) - y_outer = (elbow_radius_mm + pipe_radius_mm) * np.sin(theta) - x_inner = (elbow_radius_mm - pipe_radius_mm) * np.cos(theta) - y_inner = (elbow_radius_mm - pipe_radius_mm) * np.sin(theta) - - # Plot - ax.plot(x_center, y_center, color="black", linestyle="--", label="Centerline") - ax.plot(x_outer, y_outer, color="black") - ax.plot(x_inner, y_inner, color="black") - ax.fill( - np.concatenate([x_outer, x_inner[::-1]]), - np.concatenate([y_outer, y_inner[::-1]]), - color="lightgrey", - alpha=alpha, - ) - - ax.set_aspect("equal") - ax.set_xlabel("X [mm]") - ax.set_ylabel("Y [mm]") - ax.set_title(title) - ax.grid(True, linestyle="--", alpha=0.3) - - # Legend: Centerline + pipe radius info - legend_text = f"Centerline\nPipe radius: {pipe_radius_mm:.2f} mm\nElbow radius: {elbow_radius_mm:.2f} mm" - ax.legend([legend_text], loc="upper right") - - -def plot_blkt_pipe_bends(fig, m_file, scan: int): - """Plot the blanket pipe bends on the given axis, with axes in mm. - - Parameters - ---------- - fig : - - m_file : - - scan: int : - - """ - ax_90 = fig.add_subplot(341) - ax_180 = fig.add_subplot(342) - - r = m_file.get("radius_blkt_channel", scan=scan) - fallback_radius = 0.1 # meters - - elbow_radius_90 = ( - m_file.get("radius_blkt_channel_90_bend", scan=scan) or fallback_radius - ) - elbow_radius_180 = ( - m_file.get("radius_blkt_channel_180_bend", scan=scan) or fallback_radius - ) - - draw_bend(ax_90, elbow_radius_90, np.pi / 2, r, title="Blanket Pipe 90° Bend") - draw_bend(ax_180, elbow_radius_180, np.pi, r, title="Blanket Pipe 180° Bend") - - -def plot_fw_90_deg_pipe_bend(ax, m_file, scan: int): - """Plot the first wall pipe 90 degree bend on the given axis, with axes in mm. - - Parameters - ---------- - ax : - - m_file : - - scan: int : - - """ - # Get pipe radius from m_file, fallback to 0.1 m - r = m_file.get("radius_fw_channel", scan=scan) - elbow_radius = m_file.get("radius_fw_channel_90_bend", scan=scan) - - draw_bend( - ax, elbow_radius, np.pi / 2, r, title="First Wall Pipe 90° Bend", alpha=1.0 - ) - - -def plot_fusion_rate_profiles(axis: plt.Axes, fig, mfile: MFile, scan: int): - """Plot the fusion rate density profiles on the given axis""" - n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) - - fusden_plasma_dt_profile = [ - mfile.get(f"fusden_plasma_dt_profile{i}", scan=scan) - for i in range(n_plasma_profile_elements) - ] - - fusden_plasma_dd_triton_profile = [ - mfile.get(f"fusden_plasma_dd_triton_profile{i}", scan=scan) - for i in range(n_plasma_profile_elements) - ] - - fusden_plasma_dd_helion_profile = [ - mfile.get(f"fusden_plasma_dd_helion_profile{i}", scan=scan) - for i in range(n_plasma_profile_elements) - ] - fusden_plasma_dhe3_profile = [ - mfile.get(f"fusden_plasma_dhe3_profile{i}", scan=scan) - for i in range(n_plasma_profile_elements) - ] - - fusrat_plasma_total_profile = [ - fusden_plasma_dt_profile[i] - + fusden_plasma_dd_triton_profile[i] - + fusden_plasma_dd_helion_profile[i] - + fusden_plasma_dhe3_profile[i] - for i in range(len(fusden_plasma_dt_profile)) - ] - - axis.spines["left"].set_color("red") - axis.yaxis.label.set_color("black") - axis.tick_params(axis="y", colors="red") - - # Plot fusion rates (dashed lines, left axis) with axis color and different linestyles - axis.plot( - np.linspace(0, 1, len(fusden_plasma_dt_profile)), - fusden_plasma_dt_profile, - color=axis.spines["left"].get_edgecolor(), - linestyle="-", - label=r"$\mathrm{D-T}$", - ) - axis.plot( - np.linspace(0, 1, len(fusden_plasma_dd_triton_profile)), - fusden_plasma_dd_triton_profile, - color=axis.spines["left"].get_edgecolor(), - linestyle=":", - label=r"$\mathrm{D-D \ Triton}$", - ) - axis.plot( - np.linspace(0, 1, len(fusden_plasma_dd_helion_profile)), - fusden_plasma_dd_helion_profile, - color=axis.spines["left"].get_edgecolor(), - linestyle="-.", - label=r"$\mathrm{D-D \ Helion}$", - ) - axis.plot( - np.linspace(0, 1, len(fusden_plasma_dhe3_profile)), - fusden_plasma_dhe3_profile, - color=axis.spines["left"].get_edgecolor(), - linestyle="--", - label=r"$\mathrm{D-3He}$", - ) - axis.plot( - np.linspace(0, 1, len(fusrat_plasma_total_profile)), - fusrat_plasma_total_profile, - color=axis.spines["left"].get_edgecolor(), - linestyle="None", - marker="d", - markersize=1, - label=r"Total", - ) - - # Show the plasma volume-averaged rate density and its position on the - # profile. - profile_positions = np.linspace(0, 1, len(fusrat_plasma_total_profile)) - profile_rates = np.asarray(fusrat_plasma_total_profile) - average_rate = mfile.get("fusden_plasma_vol_avg", scan=scan) - axis.axhline( - average_rate, - color="black", - linestyle="--", - linewidth=0.9, - label="Plasma volume average", - ) - - average_position = profile_positions[ - np.nanargmin(np.abs(profile_rates - average_rate)) - ] - axis.axvline( - average_position, - color="black", - linestyle="--", - linewidth=0.9, - ) - - # Plot fusion power (solid lines, right axis) with axis color and different linestyles - ax2 = axis.twinx() - ax2.spines["right"].set_color("blue") - ax2.yaxis.label.set_color("black") - ax2.tick_params(axis="y", colors="blue") - ax2.plot( - np.linspace(0, 1, len(fusden_plasma_dt_profile)), - np.array(fusden_plasma_dt_profile) * constants.D_T_ENERGY, - color=ax2.spines["right"].get_edgecolor(), - linestyle="-", - ) - - ax2.plot( - np.linspace(0, 1, len(fusden_plasma_dd_triton_profile)), - np.array(fusden_plasma_dd_triton_profile) * constants.DD_TRITON_ENERGY, - color=ax2.spines["right"].get_edgecolor(), - linestyle=":", - ) - ax2.plot( - np.linspace(0, 1, len(fusden_plasma_dd_helion_profile)), - np.array(fusden_plasma_dd_helion_profile) * constants.DD_HELIUM_ENERGY, - color=ax2.spines["right"].get_edgecolor(), - linestyle="-.", - ) - ax2.plot( - np.linspace(0, 1, len(fusden_plasma_dhe3_profile)), - np.array(fusden_plasma_dhe3_profile) * constants.D_HELIUM_ENERGY, - color=ax2.spines["right"].get_edgecolor(), - linestyle="--", - ) - ax2.plot( - np.linspace(0, 1, len(fusrat_plasma_total_profile)), - ( - np.array(fusden_plasma_dhe3_profile) * constants.D_HELIUM_ENERGY - + np.array(fusden_plasma_dd_helion_profile) * constants.DD_HELIUM_ENERGY - + np.array(fusden_plasma_dd_triton_profile) * constants.DD_TRITON_ENERGY - + np.array(fusden_plasma_dt_profile) * constants.D_T_ENERGY - ), - color=ax2.spines["right"].get_edgecolor(), - linestyle="None", - marker="d", - markersize=1, - label=r"Total", - ) - - # ================================================= - - axis.set_xlabel("$\\rho \\ [r/a]$") - axis.set_ylabel("Fusion Rate Density [reactions/m³/sec]") - axis.legend( - loc="lower left", - edgecolor="black", - facecolor="white", - labelcolor="black", - framealpha=1.0, - frameon=True, - ) - axis.set_yscale("log") - axis.grid(True, which="both", linestyle="--", alpha=0.5) - axis.set_xlim(0, 1.025) - axis.minorticks_on() - axis.set_ylim(1e10, 1e23) - axis.yaxis.set_major_locator(plt.LogLocator(base=10.0, numticks=10)) - axis.yaxis.set_minor_locator( - plt.LogLocator(base=10.0, subs=np.arange(1, 10) * 0.1, numticks=100) - ) - axis.tick_params(axis="y", which="minor", colors="red") - - ax2.set_title("Fusion Rate and Fusion Power Density Profiles") - ax2.set_ylabel("Fusion Power Density [W/m³]") - ax2.set_yscale("log") - ax2.minorticks_on() - ax2.yaxis.set_major_locator(plt.LogLocator(base=10.0, numticks=10)) - ax2.yaxis.set_minor_locator( - plt.LogLocator(base=10.0, subs=np.arange(1, 10) * 0.1, numticks=100) - ) - ax2.tick_params(axis="y", which="minor", colors="blue") - - # ================================================= - - # Add plasma volume, areas and shaping information - textstr_general = ( - f"Total fusion rate: {mfile.get('fusrat_total', scan=scan):.4e} reactions/s\n" - f"Total volume averaged fusion rate density: {mfile.get('fusden_total_vol_avg', scan=scan):.4e} reactions/m3/s\n" - f"Plasma volume averaged fusion rate density: {mfile.get('fusden_plasma_vol_avg', scan=scan):.4e} reactions/m3/s\n" - ) - - axis.text( - 0.05, - 0.85, - textstr_general, - **_text_layout(fig), - bbox=_box_style("lightyellow"), - ) - - # ============================================================================ - - textstr_dt = ( - f"Total fusion power: {mfile.get('p_dt_total_mw', scan=scan):,.2f} MW\n" - f"Plasma fusion power: {mfile.get('p_plasma_dt_mw', scan=scan):,.2f} MW\n" - f"Volume-averaged fusion power density: plasma: {mfile.get('pden_plasma_dt_vol_avg_mw', scan=scan):,.3f} MW/m³\n" - f"Beam fusion power: {mfile.get('p_beam_dt_mw', scan=scan):,.2f} MW\n" - ) - - axis.text( - 0.05, - 0.75, - textstr_dt, - **_text_layout(fig), - bbox=_box_style("lightyellow"), - ) - - axis.text( - 0.24, - 0.8, - "$\\text{D - T}$", - fontsize=20, - verticalalignment="top", - transform=fig.transFigure, - ) - - # ================================================= - - textstr_dd = ( - f"Total fusion power: {mfile.get('p_dd_total_mw', scan=scan):,.2f} MW\n" - f"Volume-averaged total power density: {mfile.get('pden_dd_total_vol_avg_mw', scan=scan):,.3e} MW/m³\n" - f"Tritium branching ratio: {mfile.get('f_dd_branching_trit', scan=scan):.4f}\n" - ) - - axis.text( - 0.05, - 0.65, - textstr_dd, - **_text_layout(fig), - bbox=_box_style("lightyellow"), - ) - - axis.text( - 0.22, - 0.685, - "$\\text{D - D}$", - fontsize=20, - verticalalignment="top", - transform=fig.transFigure, - ) - - # ================================================= - - textstr_dhe3 = ( - f"Total fusion power: {mfile.get('p_dhe3_total_mw', scan=scan):,.2f} MW\n\n" - f"Volume-averaged total power density: {mfile.get('pden_dhe3_total_vol_avg_mw', scan=scan):,.3e} MW/m³\n\n" - ) - - axis.text( - 0.05, - 0.55, - textstr_dhe3, - **_text_layout(fig), - bbox=_box_style("lightyellow"), - ) - - axis.text( - 0.21, - 0.59, - "$\\text{D - 3He}$", - fontsize=20, - verticalalignment="top", - transform=fig.transFigure, - ) - - # ================================================= - - textstr_alpha = ( - f"Total power: {mfile.get('p_alpha_total_mw', scan=scan):.2f} MW\n" - f"Plasma power: {mfile.get('p_plasma_alpha_mw', scan=scan):.2f} MW\n" - f"Beam power: {mfile.get('p_beam_alpha_mw', scan=scan):.2f} MW\n\n" - f"Volume-averaged rate density total: {mfile.get('fusden_alpha_total_vol_avg', scan=scan):.4e} particles/m3/sec\n" - f"Volume-averaged rate density, plasma: {mfile.get('fusden_plasma_alpha_vol_avg', scan=scan):.4e} particles/m3/sec\n\n" - f"Volume-averaged total power density: {mfile.get('pden_alpha_total_vol_avg_mw', scan=scan):.4e} MW/m3\n" - f"Volume-averaged plasma power density: {mfile.get('pden_plasma_alpha_vol_avg_mw', scan=scan):.4e} MW/m3\n\n" - f"Power per unit volume transferred to electrons: {mfile.get('f_pden_alpha_electron_mw', scan=scan):.4e} MW/m3\n" - f"Power per unit volume transferred to ions: {mfile.get('f_pden_alpha_ions_mw', scan=scan):.4e} MW/m3\n\n" - ) - - axis.text( - 0.05, - 0.25, - textstr_alpha, - **_text_layout(fig), - bbox=_box_style("red"), - ) - - axis.text( - 0.35, - 0.45, - "$\\alpha$", - fontsize=22, - verticalalignment="top", - transform=fig.transFigure, - ) - - # ================================================= - - textstr_neutron = ( - f"Total power: {mfile.get('p_neutron_total_mw', scan=scan):,.2f} MW\n" - f"Plasma power: {mfile.get('p_plasma_neutron_mw', scan=scan):,.2f} MW\n" - f"Beam power: {mfile.get('p_beam_neutron_mw', scan=scan):,.2f} MW\n\n" - f"Volume-averaged total power density: {mfile.get('pden_neutron_total_vol_avg_mw', scan=scan):,.4e} MW/m3\n" - f"Volume-averaged plasma power density: {mfile.get('pden_plasma_neutron_vol_avg_mw', scan=scan):,.4e} MW/m3\n" - ) - - axis.text( - 0.05, - 0.1, - textstr_neutron, - **_text_layout(fig), - bbox=_box_style("grey"), - ) - - axis.text( - 0.25, - 0.2, - "$n$", - fontsize=20, - verticalalignment="top", - transform=fig.transFigure, - ) - - -def plot_cover_page( - axis: plt.Axes, - mfile: MFile, - scan: int, - fig, - radial_build: RadialBuild, - colour_scheme: Literal[1, 2], -): - """Plots a cover page for the PROCESS run, including run title, date, user, and summary info. - - Parameters - ---------- - axis : plt.Axes - The matplotlib axis object to plot on. - mfile : MFile - The MFILE data object containing run info. - scan : int - The scan number to use for extracting data. - fig : plt.Figure - The matplotlib figure object for additional annotations. - radial_build: - - colour_scheme: - - """ - axis.axis("off") - title = mfile.get("runtitle", scan=-1) - date = mfile.get("date", scan=-1) - time = mfile.get("time", scan=-1) - user = mfile.get("username", scan=-1) - procver = mfile.get("procver", scan=-1) - tagno = mfile.get("tagno", scan=-1) - branch_name = mfile.get("branch_name", scan=-1) - fileprefix = mfile.get("fileprefix", scan=-1) - optmisation_switch = int(mfile.get("i_process_run_mode", scan=-1)) - figure_merit_switch = mfile.get("i_figure_merit", scan=-1) or "N/A" - ifail = mfile.get("ifail", scan=-1) - nvars = mfile.get("n_iteration_variables", scan=-1) - # Objective_function_name - objf_name = mfile.get("objf_name", scan=-1) - # Square_root_of_the_sum_of_squares_of_the_constraint_residuals - sqsumsq = mfile.get("sqsumsq", scan=-1) - # VMCON_convergence_parameter - convergence_parameter = mfile.get("convergence_parameter", scan=-1) or "N/A" - # Number_of_optimising_solver_iterations - n_solver_iterations = int(mfile.get("n_solver_iterations", scan=-1)) or "N/A" - - # Objective name with minimising/maximising - if isinstance(figure_merit_switch, str): - objective_text = "" - elif figure_merit_switch >= 0: - figure_merit_switch = int(figure_merit_switch) - objective_text = f" -> Minimising: {objf_name}" - else: - figure_merit_switch = int(figure_merit_switch) - objective_text = f" -> Maximising: {objf_name}" - - axis.text( - 0.1, - 0.85, - "PROCESS Run Summary", - fontsize=28, - ha="left", - va="center", - transform=fig.transFigure, - ) - - # Box 1: Run Info - run_info = ( - f"• Run Title: {title}\n" - f"• Date: {date} Time: {time}\n" - f"• User: {user}\n" - f"• PROCESS Version: {procver}" - ) - axis.text( - 0.1, - 0.72, - run_info, - fontsize=16, - ha="left", - va="top", - transform=fig.transFigure, - bbox=_box_style("#e0f7fa"), - ) - - # Box 2: File/Branch Info - # Wrap the whole "Branch Name: ..." line if too long - max_line_len = 60 - branch_line = textwrap.fill(f"• Branch Name: {branch_name}", max_line_len) - fileprefix = textwrap.fill(f"File Prefix: {fileprefix}", max_line_len) - - file_info = f"• Tag Number: {tagno}\n{branch_line}\n• {fileprefix}" - axis.text( - 0.1, - 0.57, - file_info, - fontsize=14, - ha="left", - va="top", - transform=fig.transFigure, - bbox=_box_style("#fffde7"), - ) - - # Box 3: Run Settings - settings_info = ( - f"• Optimisation Switch: {int(optmisation_switch)}\n" - f" {PROCESSRunMode(int(optmisation_switch)).description}\n" - f"• Figure of Merit Switch (i_figure_merit): {figure_merit_switch}\n" - f" {objective_text}\n" - f"• Fail Status (ifail): {int(ifail)}\n" - f"• Number of Iteration Variables: {int(nvars)}\n" - f"• Constraint Residuals (sqrt sum sq): {sqsumsq}\n" - f"• Convergence Parameter: {convergence_parameter}\n" - f"• Solver Iterations: {n_solver_iterations}\n" - f"• Runtime: {mfile.get('process_runtime', scan=-1):.6f} seconds" - ) - axis.text( - 0.1, - 0.46, - settings_info, - fontsize=14, - ha="left", - va="top", - transform=fig.transFigure, - bbox=_box_style("#f3e5f5"), - ) - - axis.text( - 0.1, - 0.15, - "For more information, see the following pages.", - fontsize=12, - ha="left", - va="center", - transform=fig.transFigure, - color="gray", - ) - - # Add a small poloidal cross-section inset on the cover page - inset_ax = fig.add_axes([0.55, 0.2, 0.55, 0.55], aspect="equal") - poloidal_cross_section( - inset_ax, - mfile, - scan, - demo_ranges=False, - radial_build=radial_build, - colour_scheme=colour_scheme, - ) - inset_ax.set_title("") # Remove the plot title - inset_ax.axis("off") - - -def plot_plasma_pressure_profiles(axis: plt.Axes, mfile: MFile, scan: int): - """Plot the plasma pressure profiles on the given axis""" - n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) - - pres_plasma_profile = [ - mfile.get(f"pres_plasma_electron_profile{i}", scan=scan) - for i in range(n_plasma_profile_elements) - ] - pres_plasma_profile_ion = [ - mfile.get(f"pres_plasma_ion_total_profile{i}", scan=scan) - for i in range(n_plasma_profile_elements) - ] - pres_plasma_thermal_total_profile = [ - mfile.get(f"pres_plasma_thermal_total_profile{i}", scan=scan) - for i in range(n_plasma_profile_elements) - ] - pres_plasma_profile_fuel = [ - mfile.get(f"pres_plasma_fuel_profile{i}", scan=scan) - for i in range(n_plasma_profile_elements) - ] - pres_plasma_profile_kpa = [p / 1000.0 for p in pres_plasma_profile] - pres_plasma_profile_ion_kpa = [p / 1000.0 for p in pres_plasma_profile_ion] - pres_plasma_profile_fuel_kpa = [p / 1000.0 for p in pres_plasma_profile_fuel] - pres_plasma_profile_total_kpa = [ - p / 1000.0 for p in pres_plasma_thermal_total_profile - ] - - axis.plot( - np.linspace(0, 1, len(pres_plasma_profile_kpa)), - pres_plasma_profile_kpa, - color="blue", - label="Electron", - ) - axis.plot( - np.linspace(0, 1, len(pres_plasma_profile_ion_kpa)), - pres_plasma_profile_ion_kpa, - color="Red", - label="Ion-total", - ) - axis.plot( - np.linspace(0, 1, len(pres_plasma_profile_fuel_kpa)), - pres_plasma_profile_fuel_kpa, - color="orange", - label="Fuel", - ) - axis.plot( - np.linspace(0, 1, len(pres_plasma_profile_total_kpa)), - pres_plasma_profile_total_kpa, - color="green", - label="Total", - ) - - # Plot horizontal line for volume-average thermal pressure (converted to kPa) - p_vol_kpa = mfile.get("pres_plasma_thermal_vol_avg", scan=scan) / 1000.0 - axis.axhline( - p_vol_kpa, - color="black", - linestyle="--", - linewidth=1.2, - label="Volume avg", - zorder=5, - ) - - axis.set_xlabel("$\\rho$ [r/a]") - axis.set_ylabel("Thermal Pressure [kPa]") - axis.minorticks_on() - axis.grid(which="minor", linestyle=":", linewidth=0.5, alpha=0.5) - axis.set_title("Plasma Thermal Pressure Profiles") - axis.grid(True, linestyle="--", alpha=0.5) - axis.set_xlim(0, 1.025) - axis.set_ylim(bottom=0) - axis.legend() - - textstr_pressure = "\n".join(( - ( - rf"$p_0$: {mfile.get('pres_plasma_thermal_on_axis', scan=scan) / 1000:,.3f} kPa" - rf"$\hspace{{2}} \frac{{p_0}}{{\langle p_{{\text{{total}}}} \rangle_\text{{V}}}}$: {mfile.get('f_pres_plasma_thermal_on_axis_vol_avg', scan=scan):,.3f}" - ), - rf"$\langle p_{{\text{{total}}}} \rangle_\text{{V}}$: {mfile.get('pres_plasma_thermal_vol_avg', scan=scan) / 1000:,.3f} kPa", - )) - - axis.text( - 0.5, - 1.2, - textstr_pressure, - transform=axis.transAxes, - fontsize=9, - verticalalignment="top", - horizontalalignment="center", - bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5}, - ) - - if ( - int(mfile.get("i_plasma_pedestal", scan=scan)) - == PlasmaProfileShapeType.PEDESTAL_PROFILE - ): - textstr_pressure_pedestal = "\n".join(( - rf"$p_{{\text{{ped}}}}$: {mfile.get('pres_plasma_pedestal_thermal', scan=scan) / 1000:,.3f} kPa", - rf"$p_{{\text{{sep}}}}$: {mfile.get('pres_plasma_separatrix_thermal', scan=scan) / 1000:,.3f} kPa", - )) - - axis.text( - 0.9, - 1.2, - textstr_pressure_pedestal, - transform=axis.transAxes, - fontsize=9, - verticalalignment="top", - horizontalalignment="center", - bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5}, - ) - - -def plot_plasma_current_comparison(axis: plt.Axes, mfile: MFile, scan: int): - """Function to plot a scatter box plot of different plasma current comparisons. - - Parameters - ---------- - axis : - Axis object to plot to. - mfile : - MFILE data object. - scan : - Scan number to use. - """ - c_plasma_peng_analytic = mfile.get("c_plasma_peng_analytic", scan=scan) - c_plasma_peng_double_null = mfile.get("c_plasma_peng_double_null", scan=scan) - c_plasma_cyclindrical = mfile.get("c_plasma_cyclindrical", scan=scan) - c_plasma_ipdg89 = mfile.get("c_plasma_ipdg89", scan=scan) - c_plasma_todd_empirical_i = mfile.get("c_plasma_todd_empirical_i", scan=scan) - c_plasma_todd_empirical_ii = mfile.get("c_plasma_todd_empirical_ii", scan=scan) - c_plasma_connor_hastie = mfile.get("c_plasma_connor_hastie", scan=scan) - c_plasma_sauter = mfile.get("c_plasma_sauter", scan=scan) - c_plasma_fiesta_st = mfile.get("c_plasma_fiesta_st", scan=scan) - - # Data for the box plot - data = { - f"{PlasmaCurrentModel.PENG_ANALYTIC_FIT.full_name}": c_plasma_peng_analytic, - f"{PlasmaCurrentModel.PENG_DIVERTOR_SCALING.full_name}": c_plasma_peng_double_null, - f"{PlasmaCurrentModel.ITER_SCALING.full_name}": c_plasma_cyclindrical, - f"{PlasmaCurrentModel.IPDG89_SCALING.full_name}": c_plasma_ipdg89, - f"{PlasmaCurrentModel.TODD_EMPIRICAL_SCALING_I.full_name}": c_plasma_todd_empirical_i, - f"{PlasmaCurrentModel.TODD_EMPIRICAL_SCALING_II.full_name}": c_plasma_todd_empirical_ii, - f"{PlasmaCurrentModel.CONNOR_HASTIE_MODEL.full_name}": c_plasma_connor_hastie, - f"{PlasmaCurrentModel.SAUTER_SCALING.full_name}": c_plasma_sauter, - f"{PlasmaCurrentModel.FIESTA_ST_SCALING.full_name}": c_plasma_fiesta_st, - } - - # Create the violin plot - data_values = list(data.values()) - axis.violinplot(data_values, showextrema=False) - - # Create the box plot - axis.boxplot(data_values, showfliers=True, showmeans=True, meanline=True, widths=0.3) - - # Scatter plot for each data point - colors = plt.cm.plasma(np.linspace(0, 1, len(data.values()))) - for index, (key, value) in enumerate(data.items()): - axis.scatter(1, value, color=colors[index], label=key, alpha=1.0) - axis.legend(loc="upper left", bbox_to_anchor=(-0.9, 1)) - - # Calculate average, standard deviation, and median - data_values = list(data.values()) - avg_density_limit = np.mean(data_values) - std_density_limit = np.std(data_values) - median_density_limit = np.median(data_values) - - # Plot average, standard deviation, and median as text - axis.text( - -0.45, - 0.15, - rf"Average: {avg_density_limit * 1e-6:.4f}", - transform=axis.transAxes, - fontsize=9, - ) - axis.text( - -0.45, - 0.1, - rf"Standard Dev: {std_density_limit * 1e-6:.4f}", - transform=axis.transAxes, - fontsize=9, - ) - axis.text( - -0.45, - 0.05, - rf"Median: {median_density_limit * 1e-6:.4f}", - transform=axis.transAxes, - fontsize=9, - ) - - axis.set_title("Plasma Current ($I_p$) Comparison") - axis.set_ylabel(r"Plasma Current [MA]") - axis.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x * 1e-6:.1f}")) - axis.set_xlim(0.5, 1.5) - axis.set_xticks([]) - axis.set_xticklabels([]) - axis.set_facecolor("#f0f0f0") - - -def plot_max_normalised_beta_comparison(axis: plt.Axes, mfile: MFile, scan: int): - """Function to plot a scatter box plot of different max normalised beta comparisons. - - Parameters - ---------- - axis : - Axis object to plot to. - mfile : - MFILE data object. - scan : - Scan number to use. - """ - beta_norm_max_wesson = mfile.get("beta_norm_max_wesson", scan=scan) - beta_norm_max_original_scaling = mfile.get( - "beta_norm_max_original_scaling", scan=scan - ) - beta_norm_max_menard = mfile.get("beta_norm_max_menard", scan=scan) - beta_norm_max_tholerus = mfile.get("beta_norm_max_tholerus", scan=scan) - beta_norm_max_stambaugh = mfile.get("beta_norm_max_stambaugh", scan=scan) - - # Data for the box plot - data = { - f"{BetaNormMaxModel.WESSON.full_name}": beta_norm_max_wesson, - f"{BetaNormMaxModel.ORIGINAL_SCALING.full_name}": beta_norm_max_original_scaling, - f"{BetaNormMaxModel.MENARD.full_name}": beta_norm_max_menard, - f"{BetaNormMaxModel.THOLERUS.full_name}": beta_norm_max_tholerus, - f"{BetaNormMaxModel.STAMBAUGH.full_name}": beta_norm_max_stambaugh, - } - data_values = list(data.values()) - # Create the violin plot - axis.violinplot(data_values, showextrema=False) - - # Create the box plot - axis.boxplot(data_values, showfliers=True, showmeans=True, meanline=True, widths=0.3) - - # Scatter plot for each data point - colors = plt.cm.plasma(np.linspace(0, 1, len(data.values()))) - for index, (key, value) in enumerate(data.items()): - axis.scatter(1, value, color=colors[index], label=key, alpha=1.0) - axis.legend(loc="upper left", bbox_to_anchor=(1.1, 1)) - - # Calculate average, standard deviation, and median - data_values = list(data.values()) - avg_beta_norm_max = np.mean(data_values) - std_beta_norm_max = np.std(data_values) - median_beta_norm_max = np.median(data_values) - - # Plot average, standard deviation, and median as text - axis.text( - 1.1, - 0.15, - rf"Average: {avg_beta_norm_max:.4f}", - transform=axis.transAxes, - fontsize=9, - ) - axis.text( - 1.1, - 0.1, - rf"Standard Dev: {std_beta_norm_max:.4f}", - transform=axis.transAxes, - fontsize=9, - ) - axis.text( - 1.1, - 0.05, - rf"Median: {median_beta_norm_max:.4f}", - transform=axis.transAxes, - fontsize=9, - ) - - axis.set_title("Max Normalised Beta ($\\beta_N$) Comparison") - axis.set_ylabel("Max Normalised Beta $\\beta_N$ [unitless]") - axis.set_xlim(0.5, 1.5) - axis.set_xticks([]) - axis.set_xticklabels([]) - axis.set_facecolor("#f0f0f0") - - -def plot_plasma_pressure_gradient_profiles(axis: plt.Axes, mfile: MFile, scan: int): - """Plot plasma pressure gradient profiles""" - # Get the plasma pressure profiles - n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) - - pres_plasma_profile = [ - mfile.get(f"pres_plasma_electron_profile{i}", scan=scan) - for i in range(n_plasma_profile_elements) - ] - pres_plasma_profile_ion = [ - mfile.get(f"pres_plasma_ion_total_profile{i}", scan=scan) - for i in range(n_plasma_profile_elements) - ] - pres_plasma_profile_total = [ - mfile.get(f"pres_plasma_thermal_total_profile{i}", scan=scan) - for i in range(n_plasma_profile_elements) - ] - pres_plasma_profile_fuel = [ - mfile.get(f"pres_plasma_fuel_profile{i}", scan=scan) - for i in range(n_plasma_profile_elements) - ] - pres_plasma_profile_kpa = np.array(pres_plasma_profile) / 1000.0 - pres_plasma_profile_ion_kpa = np.array(pres_plasma_profile_ion) / 1000.0 - pres_plasma_profile_fuel_kpa = np.array(pres_plasma_profile_fuel) / 1000.0 - pres_plasma_profile_total_kpa = np.array(pres_plasma_profile_total) / 1000.0 - - # Calculate the normalised radius - rho = np.linspace(0, 1, len(pres_plasma_profile_kpa)) - - # Compute gradients using numpy.gradient - grad_electron = np.gradient(pres_plasma_profile_kpa, rho) - grad_ion = np.gradient(pres_plasma_profile_ion_kpa, rho) - grad_total = np.gradient(pres_plasma_profile_total_kpa, rho) - grad_fuel = np.gradient(pres_plasma_profile_fuel_kpa, rho) - - axis.plot(rho, grad_electron, color="blue", label="Electron") - axis.plot(rho, grad_ion, color="red", label="Ion") - axis.plot(rho, grad_total, color="green", label="Total") - axis.plot(rho, grad_fuel, color="orange", label="Fuel") - axis.set_xlabel("$\\rho$ [r/a]") - axis.set_ylabel("$dP/dr$ [kPa / m]") - axis.minorticks_on() - axis.grid(which="minor", linestyle=":", linewidth=0.5, alpha=0.5) - axis.set_title("Plasma Thermal Pressure Gradient Profiles") - axis.grid(True, linestyle="--", alpha=0.5) - axis.set_xlim(0, 1.025) - axis.legend() - - -def plot_plasma_poloidal_pressure_contours(axis: plt.Axes, mfile: MFile, scan: int): - """Plot plasma poloidal pressure contours inside the plasma boundary. - - This function visualizes the poloidal pressure distribution inside the plasma boundary - by interpolating the pressure profile onto a grid defined by the plasma geometry. - The pressure is shown as filled contours, with the plasma boundary overlaid. - - Parameters - ---------- - axis : matplotlib.axes.Axes - Matplotlib axis object to plot on. - mfile : mfile: MFile - MFILE data object containing plasma and geometry data. - scan : int - Scan number to use for extracting data. - """ - n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) - - # Get pressure profile (function of normalised radius rho, 0..1) - pres_plasma_electron_profile = [ - mfile.get(f"pres_plasma_electron_profile{i}", scan=scan) - for i in range(n_plasma_profile_elements) - ] - pres_plasma_profile_ion = [ - mfile.get(f"pres_plasma_ion_total_profile{i}", scan=scan) - for i in range(n_plasma_profile_elements) - ] - - # Convert pressure to kPa - pres_plasma_electron_profile_kpa = [p / 1000.0 for p in pres_plasma_electron_profile] - pres_plasma_profile_ion_kpa = [p / 1000.0 for p in pres_plasma_profile_ion] - pres_plasma_profile = [ - e + i - for e, i in zip( - pres_plasma_electron_profile_kpa, pres_plasma_profile_ion_kpa, strict=False - ) - ] - - pressure_grid, r_grid, z_grid = interp1d_profile(pres_plasma_profile, mfile, scan) - - # Mask points outside the plasma boundary (optional, but grid is inside by construction) - # Plot filled contour - c = axis.contourf(r_grid, -z_grid, pressure_grid, levels=50, cmap="plasma") - - # Add colorbar for pressure (now in kPa) - # You can control the location using the 'location' argument ('left', 'right', 'top', 'bottom') - # For more control, use 'ax' or 'fraction', 'pad', etc. - # Example: location="right", pad=0.05, fraction=0.05 - rmajor = mfile.get("rmajor", scan=scan) - rminor = mfile.get("rminor", scan=scan) - - axis.figure.colorbar( - c, ax=axis, label="Pressure [kPa]", location="left", anchor=(-0.25, 0.5) - ) - - axis.set_aspect("equal") - axis.set_xlabel("R [m]") - axis.set_xlim(rmajor - 1.2 * rminor, rmajor + 1.2 * rminor) - axis.set_ylim( - -1.2 * rminor * mfile.get("kappa", scan=scan), - 1.2 * mfile.get("kappa", scan=scan) * rminor, - ) - axis.set_ylabel("Z [m]") - axis.set_title("Plasma Poloidal Pressure Contours") - axis.plot( - rmajor, - 0, - marker="o", - color="red", - markersize=6, - markeredgecolor="black", - zorder=100, - ) - - -def interp1d_profile(profile, mfile: MFile, scan: int): - """Interpolate profile over a grid""" - # Get plasma geometry and boundary - pg = plasma_geometry( - rmajor=mfile.get("rmajor", scan=scan), - rminor=mfile.get("rminor", scan=scan), - triang=mfile.get("triang", scan=scan), - kappa=mfile.get("kappa", scan=scan), - i_single_null=mfile.get("i_single_null", scan=scan), - i_plasma_shape=mfile.get("i_plasma_shape", scan=scan), - square=mfile.get("plasma_square", scan=scan), - ) - - # Create a grid of (R, Z) points inside the plasma boundary - rho = np.linspace(0, 1, 500) - theta = np.linspace(0, 2 * np.pi, 720) - rho_grid, theta_grid = np.meshgrid(rho, theta) - - # Map (rho, theta) to (R, Z) using plasma boundary shape - # For each theta, get boundary (R, Z), then scale by rho - bdry_r = pg.rs - bdry_z = pg.zs - # Interpolate boundary for all theta - bdry_theta = np.arctan2(bdry_z - pg.zs.mean(), bdry_r - pg.rs.mean()) - # Ensure bdry_theta is monotonic and covers [0, 2pi] - bdry_theta = np.unwrap(bdry_theta) - # Sort bdry_theta and corresponding r/z for monotonic interpolation - sort_idx = np.argsort(bdry_theta) - bdry_theta = bdry_theta[sort_idx] - bdry_r = bdry_r[sort_idx] - bdry_z = bdry_z[sort_idx] - # Extend boundary to cover full [0, 2pi] if needed - if bdry_theta[0] > 0 or bdry_theta[-1] < 2 * np.pi: - bdry_theta = np.concatenate(([0], bdry_theta, [2 * np.pi])) - bdry_r = np.concatenate(([bdry_r[0]], bdry_r, [bdry_r[-1]])) - bdry_z = np.concatenate(([bdry_z[0]], bdry_z, [bdry_z[-1]])) - # Map theta to boundary r/z - f_r = interp1d( - bdry_theta, bdry_r, kind="linear", fill_value="extrapolate", assume_sorted=True - ) - # Map theta to boundary z - f_z = interp1d( - bdry_theta, bdry_z, kind="linear", fill_value="extrapolate", assume_sorted=True - ) - # For each (theta, rho), get boundary (R, Z), then scale by rho - # Use the boundary center for scaling, not mean, to avoid vertical offset - r_center = mfile.get("rmajor", scan=scan) - z_center = pg.zs.mean() - r_grid = r_center + (f_r(theta_grid) - r_center) * rho_grid - z_grid = z_center + (f_z(theta_grid) - z_center) * rho_grid - - # Interpolate profile for each rho - profile_grid = np.interp(rho_grid, np.linspace(0, 1, len(profile)), profile) - - return profile_grid, r_grid, z_grid - - -def plot_corc_cable_geometry( - axis, - r_centre: float, - z_centre: float, - dia_croco_strand: float, - dx_croco_strand_copper: float, - dr_hts_tape: float, - dx_croco_strand_tape_stack: float, - n_croco_strand_hts_tapes: int, - dx_hts_tape_rebco: float, - dx_hts_tape_copper: float, - dx_hts_tape_hastelloy: float, - show_legend: bool = True, -): - """Plot the geometry of a CroCo strand cable. - - Parameters - ---------- - axis : matplotlib.axes._axes.Axes - The matplotlib axis to plot on. - r_centre : float - Radial position of the strand centre (in meters). - z_centre : float - Vertical position of the strand centre (in meters). - dia_croco_strand : float - Diameter of the CroCo strand (in meters). - dx_croco_strand_copper : float - Thickness of the copper layer (in meters). - dr_hts_tape : float - Radius of the HTS tape stack (in meters). - dx_croco_strand_tape_stack : float - Height of the HTS tape stack (in meters). - n_croco_strand_hts_tapes : int - Number of HTS tape layers in the stack. - """ - legend_label = None if show_legend else "_nolegend_" - - # Plot a circle with the given diameter and copper edges - circle = Circle( - (r_centre, z_centre), - radius=(dia_croco_strand / 2), - edgecolor="black", - facecolor="#B87333", - linewidth=0.5, - label="Copper jacket" if show_legend else legend_label, - ) - axis.add_patch(circle) - - # Plot an inner circle with copper edges - circle = Circle( - (r_centre, z_centre), - radius=((dia_croco_strand / 2) - dx_croco_strand_copper), - edgecolor="grey", - facecolor="grey", - linewidth=2, - label="Solder" if show_legend else legend_label, - ) - axis.add_patch(circle) - - # Plot a rectangular tape stack in the middle - rect = Rectangle( - (r_centre - dr_hts_tape / 2, z_centre - dx_croco_strand_tape_stack / 2), - width=dr_hts_tape, - height=dx_croco_strand_tape_stack, - edgecolor="black", - facecolor=None, - linewidth=0.1, - alpha=0.5, - linestyle="--", - label="HTS Tape Stack" if show_legend else legend_label, - ) - axis.add_patch(rect) - - # Slice the tape stack into n_croco_strand_hts_tapes layers - for i in range(int(n_croco_strand_hts_tapes)): - y_start = ( - z_centre - - (dx_croco_strand_tape_stack / 2) - + i * (dx_croco_strand_tape_stack / n_croco_strand_hts_tapes) - ) - plot_hts_tape_geometry( - axis=axis, - r_left=r_centre - (dr_hts_tape / 2), - z_bottom=y_start, - dr_hts_tape=dr_hts_tape, - dx_hts_tape_rebco=dx_hts_tape_rebco, - dx_hts_tape_copper=dx_hts_tape_copper, - dx_hts_tape_hastelloy=dx_hts_tape_hastelloy, - show_legend=False, - ) - - axis.set_xlim(-dia_croco_strand * 0.75, dia_croco_strand * 0.75) - axis.set_ylim(-dia_croco_strand * 0.75, dia_croco_strand * 0.75) - axis.set_aspect("equal", adjustable="datalim") - axis.set_title("CroCo Strand Geometry") - axis.grid(True) - axis.set_xlabel("X-axis (m)") - axis.set_ylabel("Y-axis (m)") - axis.minorticks_on() - if show_legend: - axis.legend(loc="upper right") - - -def plot_hts_tape_geometry( - axis, - r_left: float, - z_bottom: float, - dr_hts_tape: float, - dx_hts_tape_rebco: float, - dx_hts_tape_copper: float, - dx_hts_tape_hastelloy: float, - show_legend: bool = True, -): - """Plot HTS tape geometry""" - legend_label = None if show_legend else "_nolegend_" - # Plot a rectangular tape stack in the middle - rect = Rectangle( - (r_left, z_bottom), - width=dr_hts_tape, - height=dx_hts_tape_copper / 2, - edgecolor=None, - facecolor="#B87333", - linewidth=2, - label="Copper" if show_legend else legend_label, - ) - axis.add_patch(rect) - rect = Rectangle( - (r_left, z_bottom + dx_hts_tape_copper / 2), - width=dr_hts_tape, - height=dx_hts_tape_hastelloy / 2, - edgecolor=None, - facecolor="grey", - linewidth=2, - label="Hastelloy" if show_legend else legend_label, - ) - axis.add_patch(rect) - rect = Rectangle( - (r_left, z_bottom + dx_hts_tape_copper / 2 + dx_hts_tape_hastelloy / 2), - width=dr_hts_tape, - height=dx_hts_tape_rebco, - edgecolor=None, - facecolor="blue", - linewidth=2, - label="REBCO" if show_legend else legend_label, - ) - axis.add_patch(rect) - rect = Rectangle( - ( - r_left, - z_bottom - + dx_hts_tape_copper / 2 - + dx_hts_tape_hastelloy / 2 - + dx_hts_tape_rebco, - ), - width=dr_hts_tape, - height=dx_hts_tape_hastelloy / 2, - edgecolor=None, - facecolor="grey", - linewidth=2, - label="Hastelloy" if show_legend else legend_label, - ) - axis.add_patch(rect) - rect = Rectangle( - ( - r_left, - z_bottom - + dx_hts_tape_copper / 2 - + dx_hts_tape_hastelloy / 2 - + dx_hts_tape_rebco - + dx_hts_tape_hastelloy / 2, - ), - width=dr_hts_tape, - height=dx_hts_tape_copper / 2, - edgecolor=None, - facecolor="#B87333", - linewidth=2, - label="Copper" if show_legend else legend_label, - ) - axis.add_patch(rect) - - axis.set_title("HTS Tape Geometry") - axis.grid(True) - axis.set_xlabel("X-axis (m)") - axis.set_ylabel("Y-axis (m)") - axis.set_xlim(r_left * 0.9, dr_hts_tape * 1.1) - axis.set_ylim( - z_bottom * 0.9, - (dx_hts_tape_copper + dx_hts_tape_hastelloy + dx_hts_tape_rebco) * 1.1, - ) - axis.minorticks_on() - axis.ticklabel_format(style="sci", axis="both", scilimits=(0, 0)) - if show_legend: - axis.legend(loc="upper right") - - -def plot_tf_corc_cable_summary_box(axis, fig, mfile: MFile, scan: int): - """Plot TF CORC cable summary box""" - textstr_cable = ( - f"$\\mathbf{{CroCo \\ Cable:}}$\n\n" - f"Cable diameter: {mfile.get('dia_tf_turn_croco_cable', scan=scan) * 1e3:,.4f} mm\n" - f"Copper width: {mfile.get('dx_tf_croco_strand_copper', scan=scan) * 1e3:,.4f} mm\n" - f"Diameter of solder tape region: {mfile.get('dia_tf_croco_strand_tape_region', scan=scan) * 1e3:,.4f} mm\n" - f"Height of tape stack: {mfile.get('dx_tf_croco_strand_tape_stack', scan=scan) * 1e3:,.4f} mm\n" - f"Width of HTS tape / tape stack: {mfile.get('dr_tf_hts_tape', scan=scan) * 1e3:,.4f} mm\n" - f"Number of HTS tape layers: {int(mfile.get('n_tf_croco_strand_hts_tapes', scan=scan))}\n\n" - f"Total copper area: {mfile.get('a_tf_croco_strand_copper_total', scan=scan) * 1e6:,.4f} mm²\n" - f"Total hastelloy area: {mfile.get('a_tf_croco_strand_hastelloy', scan=scan) * 1e6:,.4f} mm²\n" - f"Total solder area: {mfile.get('a_tf_croco_strand_solder', scan=scan) * 1e6:,.4f} mm²\n" - f"Total superconductor area: {mfile.get('a_tf_croco_strand_rebco', scan=scan) * 1e6:,.4f} mm²\n" - f"Total strand area: {mfile.get('a_tf_croco_strand', scan=scan) * 1e6:,.4f} mm²\n" - ) - - axis.text( - 0.4, - 0.4, - textstr_cable, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=_box_style("#cccccc"), # grayish color - ) - - -def reaction_plot_grid( - rminor, - rmajor, - kappa, - r_grid, - z_grid, - grid, - ax, - fractions=(0.25, 0.5, 0.75), - colours=("blue", "yellow", "red"), -): - """Plot fusion reaction rate density""" - # Mask points outside the plasma boundary (optional, but grid is inside by construction) - # Plot filled contour - - upper = ax.contourf(r_grid, z_grid, grid, levels=50, cmap="plasma", zorder=2) - ax.contourf(r_grid, -z_grid, grid, levels=50, cmap="plasma", zorder=2) - - ax.figure.colorbar( - upper, - ax=ax, - label="Fusion Rate Density [reactions/m³/sec]", - location="left", - anchor=(-0.25, 0.5), - ) - - ax.set_xlabel("R [m]") - ax.set_xlim(rmajor - 1.2 * rminor, rmajor + 1.2 * rminor) - ax.set_ylim(-1.2 * rminor * kappa, 1.2 * kappa * rminor) - ax.set_ylabel("Z [m]") - ax.plot( - rmajor, - 0, - marker="o", - color="red", - markersize=6, - markeredgecolor="black", - zorder=100, - ) - # enable minor ticks and grid for clearer reading - ax.minorticks_on() - ax.grid(True, which="major", linestyle="--", linewidth=0.8, alpha=0.7, zorder=1) - ax.grid(True, which="minor", linestyle=":", linewidth=0.4, alpha=0.5, zorder=1) - # make minor ticks visible on all sides and draw ticks inward for compact look - ax.tick_params(which="both", direction="in", top=True, right=True) - - # draw contours at % of the DT peak value (both top and mirrored bottom) - peak = np.nanmax(grid) - if peak > 0: - c_kwargs = { - "levels": [f * peak for f in fractions], - "colors": colours, - "linewidths": 1.5, - } - # distinct colours for each level - - # top and mirrored bottom contours (no clabel calls — keep only legend) - ax.contour(r_grid, z_grid, grid, **c_kwargs) - ax.contour(r_grid, -z_grid, grid, **c_kwargs) - - # create legend entries (use Line2D proxies so we get one entry per requested level) - legend_handles = [mpl.lines.Line2D([0], [0], color=c, lw=2) for c in colours] - legend_labels = ["25% peak", "50% peak", "75% peak"] - ax.legend(legend_handles, legend_labels, loc="upper right", fontsize=8) - - -def plot_fusion_rate_contours(fig1, fig2, mfile: MFile, scan: int): - """Plot fusion rate density contours""" - rmajor = mfile.get("rmajor", scan=scan) - rminor = mfile.get("rminor", scan=scan) - kappa = mfile.get("kappa", scan=scan) - n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) - - def fusrat(name): - fusrat_dat = [ - mfile.get(f"fusrat_plasma_{name}_profile{i}", scan=scan) - for i in range(n_plasma_profile_elements) - ] - return interp1d_profile(fusrat_dat, mfile, scan) - - dt_grid, _r_grid, _z_grid = fusrat("dt") - dd_triton_grid, _r_grid, _z_grid = fusrat(" dd_triton ") - dd_helion_grid, _r_grid, _z_grid = fusrat(" dd_helion ") - dhe3_grid, r_grid, z_grid = fusrat(" dhe3") - - dt_axes = fig1.add_subplot(121, aspect="equal") - dd_triton_axes = fig1.add_subplot(122, aspect="equal") - dd_helion_axes = fig2.add_subplot(121, aspect="equal") - dhe3_axes = fig2.add_subplot(122, aspect="equal") - - dt_axes.set_title("D+T -> 4He + n Fusion Rate Density Contours") - reaction_plot_grid(rminor, rmajor, kappa, r_grid, z_grid, dt_grid, dt_axes) - - dd_triton_axes.set_title("D+D -> T + p Fusion Rate Density Contours") - reaction_plot_grid( - rminor, rmajor, kappa, r_grid, z_grid, dd_triton_grid, dd_triton_axes - ) - dd_helion_axes.set_title("D+D -> 3He + n Fusion Rate Density Contours") - reaction_plot_grid( - rminor, rmajor, kappa, r_grid, z_grid, dd_helion_grid, dd_helion_axes - ) - dhe3_axes.set_title("D+3He -> 4He + n Fusion Rate Density Contours") - reaction_plot_grid(rminor, rmajor, kappa, r_grid, z_grid, dhe3_grid, dhe3_axes) - - -def plot_magnetic_fields_in_plasma(axis: plt.Axes, mfile: MFile, scan: int): - """Plot magnetic field profiles inside the plasma boundary""" - n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) - - # Get toroidal magnetic field profile (in Tesla) - b_plasma_toroidal_profile = [ - mfile.get(f"b_plasma_toroidal_profile{i}", scan=scan) - for i in range(2 * n_plasma_profile_elements) - ] - - # Get major and minor radius for x-axis in metres - rmajor = mfile.get("rmajor", scan=scan) - rminor = mfile.get("rminor", scan=scan) - - # Plot magnetic field first (background) - axis.plot( - np.linspace(rmajor - rminor, rmajor + rminor, len(b_plasma_toroidal_profile)), - b_plasma_toroidal_profile, - color="blue", - label="Toroidal B-field [T]", - linewidth=2, - ) - - # Plot plasma on top of magnetic field, displaced vertically by bt - plot_plasma(axis, mfile, scan, colour_scheme=1) - - # Plot plasma centre dot - axis.plot(rmajor, 0, marker="o", color="red", markersize=8, label="Plasma Centre") - - v_kwargs = {"color": "green", "linestyle": "--", "linewidth": 1.0} - - # Plot vertical lines at plasma edge - axis.axvline(rmajor - rminor, **v_kwargs) - axis.axvline(rmajor + rminor, **v_kwargs) - - h_kwargs = {"color": "blue", "linestyle": "--", "linewidth": 1.0} - - # Plot horizontal line for toroidal magnetic field at plasma inboard - axis.axhline(mfile.get(f"b_plasma_toroidal_profile{0}", scan=scan), **h_kwargs) - - # Plot horizontal line for toroidal magnetic field at plasma centre - axis.axhline(mfile.get("b_plasma_toroidal_on_axis", scan=scan), **h_kwargs) - - # Plot horizontal line for toroidal magnetic field at plasma outboard - axis.axhline(b_plasma_toroidal_profile[-1], **h_kwargs) - - # Text box for inboard toroidal field - axis.text( - 0.1, - 0.025, - f"$B_{{\\text{{T,inboard}}}}={mfile.get('b_plasma_inboard_toroidal', scan=scan):.2f}$ T\n" - f"$B_{{\\text{{total,inboard}}}}={mfile.get('b_plasma_inboard_total', scan=scan):.2f}$ T", - verticalalignment="center", - horizontalalignment="center", - transform=axis.transAxes, - bbox=_box_style("wheat"), - ) - - # Text box for outboard toroidal field - axis.text( - 0.9, - 0.1, - f"$B_{{\\text{{T,outboard}}}}={mfile.get('b_plasma_outboard_toroidal', scan=scan):.2f}$ T\n" - f"$B_{{\\text{{total,outboard}}}}={mfile.get('b_plasma_outboard_total', scan=scan):.2f}$ T", - verticalalignment="center", - horizontalalignment="center", - transform=axis.transAxes, - bbox=_box_style("wheat"), - ) - - axis.set_xlabel("Radial Position [m]") - axis.set_ylabel("Toroidal Magnetic Field [T]") - axis.set_title("Toroidal Magnetic Field Profile in Plasma") - axis.minorticks_on() - # Enable grid for both major and minor ticks - axis.grid(which="both", linestyle="--", alpha=0.5) - axis.grid(which="minor", linestyle=":", alpha=0.3) - axis.legend(loc="lower right") - axis.set_xlim(rmajor - 1.25 * rminor, rmajor + 1.25 * rminor) - - -def plot_beta_profiles(axis: plt.Axes, mfile: MFile, scan: int): - """Plot the beta profiles on the given axis""" - n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) - - beta_plasma_toroidal_profile = [ - mfile.get(f"beta_thermal_toroidal_profile{i}", scan=scan) - for i in range(2 * n_plasma_profile_elements) - ] - - axis.plot( - np.linspace(-1, 1, 2 * n_plasma_profile_elements), - beta_plasma_toroidal_profile, - color="blue", - label="$\\beta_t$", - ) - - axis.axhline( - mfile.get("beta_thermal_toroidal_vol_avg", scan=scan), - color="blue", - linestyle="--", - linewidth=1.0, - label="$\\langle \\beta_t \\rangle_{\\text{V}}$", - ) - - axis.set_xlabel("$\\rho$ [r/a]") - axis.set_ylabel("$\\beta$") - axis.minorticks_on() - axis.grid(which="minor", linestyle=":", linewidth=0.5, alpha=0.5) - axis.set_title("Thermal Beta Profiles") - axis.legend() - axis.axvline(x=0, color="black", linestyle="--", linewidth=1) - axis.grid(True, linestyle="--", alpha=0.5) - axis.set_ylim(bottom=0.0) - - -def plot_plasma_outboard_toroidal_ripple_map(fig, mfile: MFile, scan: int): - """Plot plasma outboard toroidal ripple map""" - r_tf_outboard_mid = mfile.get("r_tf_outboard_mid", scan=scan) - n_tf_coils = mfile.get("n_tf_coils", scan=scan) - rmajor = mfile.get("rmajor", scan=scan) - rminor = mfile.get("rminor", scan=scan) - r_tf_wp_inboard_inner = mfile.get("r_tf_wp_inboard_inner", scan=scan) - r_tf_wp_inboard_centre = mfile.get("r_tf_wp_inboard_centre", scan=scan) - r_tf_wp_inboard_outer = mfile.get("r_tf_wp_inboard_outer", scan=scan) - dx_tf_wp_primary_toroidal = mfile.get("dx_tf_wp_primary_toroidal", scan=scan) - i_tf_shape = mfile.get("i_tf_shape", scan=scan) - i_tf_sup = mfile.get("i_tf_sup", scan=scan) - dx_tf_wp_insulation = mfile.get("dx_tf_wp_insulation", scan=scan) - dx_tf_wp_insertion_gap = mfile.get("dx_tf_wp_insertion_gap", scan=scan) - ripple_b_tf_plasma_edge_max = mfile.get("ripple_b_tf_plasma_edge_max", scan=scan) - i_tf_wp_geom = round(mfile.get("i_tf_wp_geom", scan=scan)) - - build = Build() - - r_nom = r_tf_outboard_mid - dx_nom = dx_tf_wp_primary_toroidal if dx_tf_wp_primary_toroidal is not None else 0.0 - - # Simple ±20% scan around nominal values for r and dx - r_min = r_nom * 0.9 - r_max = r_nom * 1.1 - - if dx_nom > 0: - dx_min = dx_nom * 0.8 - dx_max = dx_nom * 1.2 - else: - # fallback sensible small range if nominal is zero - dx_min = 1e-3 - dx_max = 1e-2 - - n_r = 50 - n_dx = 50 - r_vals = np.linspace(r_min, r_max, n_r) - dx_vals = np.linspace(dx_min, dx_max, n_dx) - - rg, dxg = np.meshgrid(r_vals, dx_vals) - - # prepare metric array to hold ripple metric for each (r, dx) pair - metric = np.full(rg.shape, np.nan, dtype=float) - - for ii in range(rg.shape[0]): - for jj in range(rg.shape[1]): - r_test = float(rg[ii, jj]) - dx_test = float(dxg[ii, jj]) - - try: - rip, _, _ = build.plasma_outboard_edge_toroidal_ripple( - ripple_b_tf_plasma_edge_max=0.05, - r_tf_outboard_mid=r_test, - n_tf_coils=int(n_tf_coils), - rmajor=rmajor, - rminor=rminor, - r_tf_wp_inboard_inner=r_tf_wp_inboard_inner, - r_tf_wp_inboard_centre=r_tf_wp_inboard_centre, - r_tf_wp_inboard_outer=r_tf_wp_inboard_outer, - dx_tf_wp_primary_toroidal=dx_test, - i_tf_shape=i_tf_shape, - i_tf_sup=i_tf_sup, - dx_tf_wp_insulation=dx_tf_wp_insulation, - dx_tf_wp_insertion_gap=dx_tf_wp_insertion_gap, - i_tf_wp_geom=i_tf_wp_geom, - ) - except (ValueError, ZeroDivisionError, OverflowError, TypeError): - # Only catch expected numeric/validation errors from the ripple calculation; - # let other exceptions propagate so they can be diagnosed. - rip = np.nan - metric[ii, jj] = rip - - # Create two subplots that share the same x axis - ax1 = fig.add_subplot(2, 1, 1) - ax2 = fig.add_subplot(2, 1, 2, sharex=ax1) - - # Make contour plot of the ripple metric (r vs dx) on ax1 - if np.all(np.isnan(metric)): - ax1.text(0.5, 0.5, "No valid ripple data (r vs dx)", ha="center", va="center") - else: - vmin = np.nanmin(metric) - vmax = np.nanmax(metric) - - # Guard against degenerate range - if np.isclose(vmin, vmax, atol=1e-12) or np.isnan(vmin) or np.isnan(vmax): - vmin -= 0.25 - vmax += 0.25 - - # Smooth filled contour levels - levels = np.linspace(vmin, vmax, 50) - cf = ax1.contourf(rg, dxg, metric, levels=levels, cmap="plasma", extend="both") - - # Contour lines only at 0.5 increments - step = 0.5 - start = np.floor(vmin / step) * step - end = np.ceil(vmax / step) * step - contour_levels = np.arange(start, end + 1e-12, step) - - # Fallback if contour_levels is empty for some reason - if contour_levels.size < 2: - contour_levels = np.array([vmin, vmax]) - - contours = ax1.contour( - rg, - dxg, - metric, - levels=contour_levels, - colors="k", - linewidths=0.5, - alpha=0.7, - ) - ax1.clabel(contours, inline=True, fontsize=8, fmt="%.2f%%", colors="white") - # Overlay contour line at the specified target ripple value - - target = float(ripple_b_tf_plasma_edge_max) - - if target is not None and not np.isnan(target): - # Check if target lies within computed metric range - if (target >= vmin) and (target <= vmax): - c_target = ax1.contour( - rg, - dxg, - metric, - levels=[target], - colors="white", - linewidths=2.0, - linestyles="--", - zorder=20, - ) - ax1.clabel( - c_target, - inline=True, - fmt={target: f"Input Max {target:.2f}%"}, - fontsize=8, - colors="white", - ) - else: - # annotate that target is outside plotted range - ax1.text( - 0.02, - 0.98, - f"Target ripple {target:.2f}% outside plot range [{vmin:.2f},{vmax:.2f}]", - transform=ax1.transAxes, - color="white", - fontsize=8, - va="top", - bbox={"facecolor": "black", "alpha": 0.6, "pad": 2}, - ) - - # Colourbar with 0.5 increments (use the same contour_levels as for the contour lines) - ticks = contour_levels - # Fallback to sensible ticks if contour_levels is not appropriate - if ticks.size == 0 or np.isnan(ticks).all(): - ticks = np.linspace(vmin, vmax, 5) - cb = ax1.figure.colorbar( - cf, ax=ax1, label="Plasma Outboard Toroidal Ripple", ticks=ticks - ) - cb.ax.set_yticklabels([f"{t:.2f}%" for t in ticks]) - - # mark nominal point - ax1.scatter( - [r_nom], - [dx_nom], - color="white", - edgecolor="black", - s=200, - linewidths=1.5, - marker="o", - zorder=10, - label="Design Point", - ) - ax1.set_xlabel("Outboard TF leg centre [m]") - ax1.set_ylabel("WP Toroidal Width [m]") - ax1.legend(loc="upper right") - - # --------------------------------------------------------------------- - # Second plot: scan number of TF coils vs r_tf_outboard_mid (keep dx at nominal) - # --------------------------------------------------------------------- - # Determine a sensible integer range of TF coils to scan around nominal - n_nom = int(n_tf_coils) - span = max(2, int(min(12, n_nom // 2))) # choose a span based on nominal - n_min = max(10, n_nom - span) - n_max = n_nom + span - n_vals = np.arange(n_min, n_max + 1, dtype=int) - - n_r2 = 60 - r_vals2 = np.linspace(r_min, r_max, n_r2) - rg2, ng2 = np.meshgrid(r_vals2, n_vals) - - metric2 = np.full(rg2.shape, np.nan, dtype=float) - - for ii in range(rg2.shape[0]): - for jj in range(rg2.shape[1]): - r_test = float(rg2[ii, jj]) - n_test = int(ng2[ii, jj]) - try: - rip, *_ = build.plasma_outboard_edge_toroidal_ripple( - ripple_b_tf_plasma_edge_max=0.05, - r_tf_outboard_mid=r_test, - n_tf_coils=n_test, - rmajor=rmajor, - rminor=rminor, - r_tf_wp_inboard_inner=r_tf_wp_inboard_inner, - r_tf_wp_inboard_centre=r_tf_wp_inboard_centre, - r_tf_wp_inboard_outer=r_tf_wp_inboard_outer, - dx_tf_wp_primary_toroidal=dx_nom, - i_tf_shape=i_tf_shape, - i_tf_sup=i_tf_sup, - dx_tf_wp_insulation=dx_tf_wp_insulation, - dx_tf_wp_insertion_gap=dx_tf_wp_insertion_gap, - i_tf_wp_geom=i_tf_wp_geom, - ) - except (ValueError, ZeroDivisionError, OverflowError, TypeError): - # Only catch expected numeric/validation errors from the ripple calculation; - # let other exceptions propagate so they can be diagnosed. - rip = np.nan - metric2[ii, jj] = rip - - # Plot the second metric on the bottom axes (ax2) so it shares x-axis with ax1 - if np.all(np.isnan(metric2)): - ax2.text( - 0.5, 0.5, "No valid ripple data (r vs n_tf_coils)", ha="center", va="center" - ) - else: - vmin2 = np.nanmin(metric2) - vmax2 = np.nanmax(metric2) - - # filled contour levels (smooth shading) - levels2 = np.linspace(vmin2, vmax2, 40) - cf2 = ax2.contourf( - rg2, ng2, metric2, levels=levels2, cmap="viridis", extend="both" - ) - - # contour lines only at 0.5 steps - step = 0.5 - start = np.floor(vmin2 / step) * step - end = np.ceil(vmax2 / step) * step - contour_levels = np.arange(start, end + 1e-12, step) - - # fallback if arange returned empty (very small range) - if contour_levels.size == 0: - contour_levels = np.array([vmin2, vmax2]) - - contours2 = ax2.contour( - rg2, - ng2, - metric2, - levels=contour_levels, - colors="k", - linewidths=0.5, - alpha=0.7, - ) - ax2.clabel(contours2, inline=True, fontsize=8, fmt="%.2f%%", colors="white") - - target2 = float(ripple_b_tf_plasma_edge_max) - - if target2 is not None and not np.isnan(target2): - if (target2 >= vmin2) and (target2 <= vmax2): - c_target2 = ax2.contour( - rg2, - ng2, - metric2, - levels=[target2], - colors="white", - linewidths=2.0, - linestyles="--", - zorder=20, - ) - ax2.clabel( - c_target2, - inline=True, - fmt={target2: f"Input Max {target2:.2f}%"}, - fontsize=8, - colors="white", - ) - else: - ax2.text( - 0.02, - 0.98, - f"Target ripple {target2:.2f}% outside plot range [{vmin2:.2f},{vmax2:.2f}]", - transform=ax2.transAxes, - color="white", - fontsize=8, - va="top", - bbox={"facecolor": "black", "alpha": 0.6, "pad": 2}, - ) - # colorbar with 0.5 increments - # ensure contour_levels exists and is in 0.5 steps (constructed above) - ticks = contour_levels - cb2 = ax2.figure.colorbar( - cf2, ax=ax2, label="Plasma Outboard Toroidal Ripple", ticks=ticks - ) - cb2.ax.set_yticklabels([f"{t:.2f}%" for t in ticks]) - - # nominal markers - ax2.scatter( - [r_nom], - [n_nom], - color="white", - edgecolor="black", - s=300, - linewidths=1.5, - marker="o", - zorder=10, - label="Design Point", - ) - ax2.set_xlabel("Outboard TF leg centre [m]") - ax2.set_ylabel("Number of TF coils") - ax2.set_yticks(n_vals) - ax2.legend(loc="upper right") - - # Improve layout - fig.tight_layout() - - -def plot_plasma_effective_charge_profile(axis: plt.Axes, mfile: MFile, scan: int): - """Plot plasma effective charge profile""" - n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) - - n_charge_plasma_effective_vol_avg = mfile.get( - "n_charge_plasma_effective_vol_avg", scan=scan - ) - - n_charge_plasma_effective_profile = [ - mfile.get(f"n_charge_plasma_effective_profile{i}", scan=scan) - for i in range(n_plasma_profile_elements) - ] - - axis.plot( - np.linspace(0, 1, n_plasma_profile_elements), - n_charge_plasma_effective_profile, - ) - - axis.hlines( - n_charge_plasma_effective_vol_avg, - xmin=0, - xmax=1, - colors="red", - linestyles="--", - label=f"Volume-Averaged $Z_{{\\text{{eff}}}}$ = {n_charge_plasma_effective_vol_avg:.2f}", - ) - - axis.set_xlabel(r"$\rho \quad [r/a]$") - axis.set_ylabel("Effective Charge ($Z_{\\text{eff}}$)") - axis.set_title("Plasma Effective Charge Profile") - axis.minorticks_on() - axis.set_xlim(0, 1.025) - axis.grid(which="both", linestyle="--", alpha=0.5) - axis.legend() - - -def plot_ion_charge_profile(axis: plt.Axes, mfile: MFile, scan: int): - """Plot ion charge profile""" - n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) - - # find impurity densities - imp_frac = np.array([ - mfile.get("f_nd_impurity_electrons(01)", scan=scan), - mfile.get("f_nd_impurity_electrons(02)", scan=scan), - mfile.get("f_nd_impurity_electrons(03)", scan=scan), - mfile.get("f_nd_impurity_electrons(04)", scan=scan), - mfile.get("f_nd_impurity_electrons(05)", scan=scan), - mfile.get("f_nd_impurity_electrons(06)", scan=scan), - mfile.get("f_nd_impurity_electrons(07)", scan=scan), - mfile.get("f_nd_impurity_electrons(08)", scan=scan), - mfile.get("f_nd_impurity_electrons(09)", scan=scan), - mfile.get("f_nd_impurity_electrons(10)", scan=scan), - mfile.get("f_nd_impurity_electrons(11)", scan=scan), - mfile.get("f_nd_impurity_electrons(12)", scan=scan), - mfile.get("f_nd_impurity_electrons(13)", scan=scan), - mfile.get("f_nd_impurity_electrons(14)", scan=scan), - ]) - - n_charge_plasma_profile = [] - impurity_data = ImpurityRadiationData() - for imp in range(N_IMPURITIES): - if imp_frac[imp] > 1.0e-30: - profile = [ - mfile.get(f"n_charge_plasma_profile{imp}_{i}", scan=scan) - for i in range(n_plasma_profile_elements) - ] - n_charge_plasma_profile.append(profile) - z_max = impurity_data.imp_full_ion_charge[imp] - # Calculate relative ionisation state as percent of full ionisation - rel_ion_state = [ - 100.0 * (val / z_max if z_max > 0 else 0) for val in profile - ] - avg_ionisation = np.mean(rel_ion_state) - axis.plot( - np.linspace(0, 1, n_plasma_profile_elements), - rel_ion_state, - label=f"{impurity_data.imp_label[imp].replace('_', '')} (Z={z_max}): avg {avg_ionisation:.1f}%", - ) - axis.set_ylabel("Relative Ionisation State [% of $Z$]") - axis.legend() - axis.set_xlim(0, 1.025) - axis.set_xlabel(r"$\rho \quad [r/a]$") - axis.set_title("Impurity Ion Charge State Profiles") - axis.minorticks_on() - axis.grid(which="both", linestyle="--", alpha=0.5) - - -def plot_ebw_ecrh_coupling_graph(axis: plt.Axes, mfile: MFile, scan: int): - """Plot EBW and ECRH coupling efficiency graph""" - ebw = ElectronBernstein(plasma_profile=0) - ecrg = ElectronCyclotron(plasma_profile=0) - b_on_axis = mfile.get("b_plasma_toroidal_on_axis", scan=scan) - bs = np.linspace(0.0, b_on_axis + 2.0, 500) - # Use a color map for harmonics - colors = ["red", "green", "blue"] - linestyles = ["-", "--"] # EBW: solid, ECRH: dashed - - for idx, n_harmonic in enumerate(range(1, 4)): - eta_ebw_vals = [] - # For ECRH, store results for both wave modes (0: O-mode, 1: X-mode) - eta_ecrh_vals_omode = [] - eta_ecrh_vals_xmode = [] - for b in bs: - eta_ebw = ebw.electron_bernstein_freethy( - te=mfile.get("temp_plasma_electron_vol_avg_kev", scan=scan), - rmajor=mfile.get("rmajor", scan=scan), - dene20=mfile.get("nd_plasma_electrons_vol_avg", scan=scan) / 1e20, - b_plasma_toroidal_on_axis=b, - n_ecrh_harmonic=n_harmonic, - xi_ebw=mfile.get("xi_ebw", scan=scan), - ) - eta_ecrh_omode = ecrg.electron_cyclotron_freethy( - te=mfile.get("temp_plasma_electron_vol_avg_kev", scan=scan), - zeff=mfile.get("n_charge_plasma_effective_vol_avg", scan=scan), - rmajor=mfile.get("rmajor", scan=scan), - nd_plasma_electrons_vol_avg=mfile.get( - "nd_plasma_electrons_vol_avg", scan=scan - ), - b_plasma_toroidal_on_axis=b, - n_ecrh_harmonic=n_harmonic, - i_ecrh_wave_mode=0, # O-mode - ) - eta_ecrh_xmode = ecrg.electron_cyclotron_freethy( - te=mfile.get("temp_plasma_electron_vol_avg_kev", scan=scan), - zeff=mfile.get("n_charge_plasma_effective_vol_avg", scan=scan), - rmajor=mfile.get("rmajor", scan=scan), - nd_plasma_electrons_vol_avg=mfile.get( - "nd_plasma_electrons_vol_avg", scan=scan - ), - b_plasma_toroidal_on_axis=b, - n_ecrh_harmonic=n_harmonic, - i_ecrh_wave_mode=1, # X-mode - ) - eta_ebw_vals.append(eta_ebw) - eta_ecrh_vals_omode.append(eta_ecrh_omode) - eta_ecrh_vals_xmode.append(eta_ecrh_xmode) - # EBW: solid, ECRH O-mode: dashed, ECRH X-mode: dotted, same color for same harmonic - axis.plot( - bs, - eta_ebw_vals, - label=f"EBW (harmonic {n_harmonic})", - color=colors[idx], - linestyle=linestyles[0], - ) - axis.plot( - bs, - eta_ecrh_vals_omode, - label=f"ECRH O-mode (harmonic {n_harmonic})", - color=colors[idx], - linestyle="--", - ) - axis.plot( - bs, - eta_ecrh_vals_xmode, - label=f"ECRH X-mode (harmonic {n_harmonic})", - color=colors[idx], - linestyle=":", - ) - axis.set_xlabel("On axis toroidal B-field [T]") - axis.set_ylabel("Current drive efficiency [A/W]") - axis.set_title("EBW/ECRH Coupling Efficiency vs Toroidal B-field") - axis.legend() - axis.grid(True) - # Plot a vertical line at the on-axis value of the toroidal B-field - b_on_axis = mfile.get("b_plasma_toroidal_on_axis", scan=scan) - axis.axvline( - b_on_axis, color="black", linestyle="-", linewidth=2.5, label="On-axis $B_T$" - ) - axis.minorticks_on() - - -def plot_larmor_radius_profile(axis: plt.Axes, mfile_data: MFile, scan: int): - """Plot the Larmor radius profile on the given axis.""" - radius_plasma_deuteron_larmor_profile = [ - mfile_data.data[ - f"radius_plasma_deuteron_toroidal_larmor_isotropic_profile{i}" - ].get_scan(scan) - for i in range( - 2 * int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan)) - ) - ] - - radius_plasma_triton_larmor_profile = [ - mfile_data.data[ - f"radius_plasma_triton_toroidal_larmor_isotropic_profile{i}" - ].get_scan(scan) - for i in range( - 2 * int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan)) - ) - ] - - radius_plasma_deuteron_larmor_profile_mm = [ - radius * 1e3 for radius in radius_plasma_deuteron_larmor_profile - ] - - radius_plasma_triton_larmor_profile_mm = [ - radius * 1e3 for radius in radius_plasma_triton_larmor_profile - ] - - axis.plot( - np.linspace(-1, 1, len(radius_plasma_deuteron_larmor_profile_mm)), - radius_plasma_deuteron_larmor_profile_mm, - color="red", - linestyle="-", - label=r"$\rho_{Larmor,toroidal,D}$", - ) - - axis.plot( - np.linspace(-1, 1, len(radius_plasma_triton_larmor_profile_mm)), - radius_plasma_triton_larmor_profile_mm, - color="green", - linestyle="-", - label=r"$\rho_{Larmor,toroidal,T}$", - ) - - axis.set_ylabel(r"Larmor Radii [mm]") - axis.set_title(r" Toroidal Larmor Radii ($v_{\perp}^2 = 2v_{th}^2$)") - axis.set_xlabel("$\\rho \\ [r/a]$") - axis.grid(True, which="both", linestyle="--", alpha=0.5) - axis.minorticks_on() - axis.legend() - - -def plot_debye_length_profile(axis: plt.Axes, mfile_data: MFile, scan: int): - """Plot the Debye length profile on the given axis.""" - len_plasma_debye_electron_profile = [ - mfile_data.data[f"len_plasma_debye_electron_profile{i}"].get_scan(scan) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - - # Convert to micrometres (1e-6 m) - len_plasma_debye_electron_profile_um = [ - length * 1e6 for length in len_plasma_debye_electron_profile - ] - - axis.plot( - np.linspace(0, 1, len(len_plasma_debye_electron_profile_um)), - len_plasma_debye_electron_profile_um, - color="blue", - linestyle="-", - label=r"$\lambda_{Debye,e}$", - ) - - axis.set_ylabel(r"Debye Length [$\mu$m]") - - axis.set_xlabel("$\\rho \\ [r/a]$") - axis.grid(True, which="both", linestyle="--", alpha=0.5) - axis.set_xlim(0, 1.025) - axis.minorticks_on() - axis.legend() - - -def plot_velocity_profile(axis: plt.Axes, mfile_data: MFile, scan: int) -> None: - """Plot the electron thermal velocity profile on the given axis.""" - vel_plasma_electron_profile = [ - mfile_data.data[f"vel_plasma_electron_profile{i}"].get_scan(scan) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - vel_plasma_deuteron_profile = [ - mfile_data.data[f"vel_plasma_deuteron_profile{i}"].get_scan(scan) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - vel_plasma_triton_profile = [ - mfile_data.data[f"vel_plasma_triton_profile{i}"].get_scan(scan) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - vel_plasma_alpha_thermal_profile = [ - mfile_data.data[f"vel_plasma_alpha_thermal_profile{i}"].get_scan(scan) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - - vel_plasma_alpha_birth = mfile_data.data["vel_plasma_alpha_birth"].get_scan(scan) - - axis.plot( - np.linspace(0, 1, len(vel_plasma_electron_profile)), - vel_plasma_electron_profile, - color="blue", - linestyle="-", - label=r"$v_{e}$", - ) - axis.plot( - np.linspace(0, 1, len(vel_plasma_deuteron_profile)), - vel_plasma_deuteron_profile, - color="pink", - linestyle="-", - label=r"$v_{D}$", - ) - axis.plot( - np.linspace(0, 1, len(vel_plasma_triton_profile)), - vel_plasma_triton_profile, - color="green", - linestyle="-", - label=r"$v_{T}$", - ) - axis.plot( - np.linspace(0, 1, len(vel_plasma_alpha_thermal_profile)), - vel_plasma_alpha_thermal_profile, - color="red", - linestyle="-", - label=r"$v_{\alpha,thermal}$", - ) - axis.axhline( - vel_plasma_alpha_birth, - color="red", - linestyle="--", - linewidth=1.5, - label=r"$v_{\alpha,birth}$", - ) - - axis.set_yscale("log") - axis.set_ylabel("Velocity [m/s]") - axis.set_xlabel("$\\rho \\ [r/a]$") - axis.grid(True, which="both", linestyle="--", alpha=0.5) - axis.set_xlim(0, 1.025) - axis.minorticks_on() - axis.legend() - - -def plot_electron_frequency_profile( - axis: plt.Axes, mfile_data: MFile, scan: int -) -> None: - """Plot the electron thermal frequency profile on the given axis.""" - freq_plasma_electron_profile = [ - mfile_data.data[f"freq_plasma_electron_profile{i}"].get_scan(scan) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - freq_plasma_larmor_toroidal_electron_profile = [ - mfile_data.data[f"freq_plasma_larmor_toroidal_electron_profile{i}"].get_scan( - scan - ) - for i in range( - 2 * int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan)) - ) - ] - - freq_plasma_upper_hybrid_electron_profile = [ - mfile_data.data[f"freq_plasma_upper_hybrid_profile{i}"].get_scan(scan) - for i in range( - 2 * int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan)) - ) - ] - - axis.plot( - np.linspace(-1, 1, len(freq_plasma_larmor_toroidal_electron_profile)), - np.array(freq_plasma_larmor_toroidal_electron_profile) / 1e9, - color="red", - linestyle="-", - label=r"$f_{Larmor,toroidal,e}$ | Fundamental", - ) - - axis.plot( - np.linspace(-1, 1, len(freq_plasma_larmor_toroidal_electron_profile)), - 2 * np.array(freq_plasma_larmor_toroidal_electron_profile) / 1e9, - color="red", - linestyle="--", - label=r"$f_{Larmor,toroidal,e}$ | 2nd harmonic", - ) - - axis.plot( - np.linspace(-1, 1, len(freq_plasma_larmor_toroidal_electron_profile)), - 3 * np.array(freq_plasma_larmor_toroidal_electron_profile) / 1e9, - color="red", - linestyle=":", - label=r"$f_{Larmor,toroidal,e}$ | 3rd harmonic", - ) - - x = np.linspace(0, 1, len(freq_plasma_electron_profile)) - y = np.array(freq_plasma_electron_profile) / 1e9 - # original curve - axis.plot( - x, y, color="blue", linestyle="-", label=r"$\omega_{p,e}$ | Plasma Frequency" - ) - # mirrored across the y-axis (drawn at negative rho) - axis.plot(-x, y, color="blue", linestyle="-", label="_nolegend_") - - axis.plot( - np.linspace(-1, 1, len(freq_plasma_upper_hybrid_electron_profile)), - np.array(freq_plasma_upper_hybrid_electron_profile) / 1e9, - color="purple", - linestyle="-", - label=r"$\omega_{UH,e}$ | Upper Hybrid", - ) - - axis.set_xlim(-1.025, 1.025) - axis.set_ylim(None, max(freq_plasma_larmor_toroidal_electron_profile) / 1e9 * 1.6) - - axis.set_xlabel("$\\rho$ [r/a]") - axis.set_ylabel("Frequency [GHz]") - axis.grid(True, which="both", linestyle="--", alpha=0.5) - - # Add secondary x-axis showing radius in metres below the primary axis - ax2 = axis.twiny() - rmajor = mfile_data.get("rmajor", scan=scan) - rminor = mfile_data.get("rminor", scan=scan) - - # Convert normalized radius to actual radius - # rho ranges from -1 to 1, which corresponds to r = rmajor - rminor to rmajor + rminor - rho_ticks = np.array([-1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75, 1]) - r_ticks = rmajor + rho_ticks * rminor - - ax2.set_xticks(rho_ticks) - ax2.set_xticklabels([f"{r:.2f}" for r in r_ticks]) - ax2.set_xlabel("Radius [m]") - ax2.minorticks_on() - ax2.set_xlim(axis.get_xlim()) - - # Move secondary axis to the bottom - ax2.xaxis.set_ticks_position("bottom") - ax2.xaxis.set_label_position("bottom") - ax2.spines["bottom"].set_position(("outward", 30)) - - axis.legend() - - -def plot_ion_frequency_profile(axis: plt.Axes, mfile_data: MFile, scan: int) -> None: - """Plot the ion thermal frequency profile on the given axis.""" - freq_plasma_larmor_toroidal_deuteron_profile = [ - mfile_data.data[f"freq_plasma_larmor_toroidal_deuteron_profile{i}"].get_scan( - scan - ) - for i in range( - 2 * int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan)) - ) - ] - - freq_plasma_larmor_toroidal_triton_profile = [ - mfile_data.data[f"freq_plasma_larmor_toroidal_triton_profile{i}"].get_scan(scan) - for i in range( - 2 * int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan)) - ) - ] - - axis.plot( - np.linspace(-1, 1, len(freq_plasma_larmor_toroidal_deuteron_profile)), - np.array(freq_plasma_larmor_toroidal_deuteron_profile) / 1e6, - color="red", - linestyle="-", - label=r"$f_{Larmor,toroidal,D}$", - ) - axis.plot( - np.linspace(-1, 1, len(freq_plasma_larmor_toroidal_triton_profile)), - np.array(freq_plasma_larmor_toroidal_triton_profile) / 1e6, - color="green", - linestyle="-", - label=r"$f_{Larmor,toroidal,T}$", - ) - - axis.set_ylabel("Frequency [MHz]") - axis.set_xlabel("$\\rho \\ [r/a]$") - axis.grid(True, which="both", linestyle="--", alpha=0.5) - axis.minorticks_on() - axis.legend() - - -def plot_plasma_coloumb_logarithms(axis: plt.Axes, mfile_data: MFile, scan: int) -> None: - """Plot the plasma coloumb logarithms on the given axis.""" - plasma_coulomb_log_electron_electron_profile = [ - mfile_data.data[f"plasma_coulomb_log_electron_electron_profile{i}"].get_scan( - scan - ) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - - plasma_coulomb_log_electron_deuteron_profile = [ - mfile_data.data[f"plasma_coulomb_log_electron_deuteron_profile{i}"].get_scan( - scan - ) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - - plasma_coulomb_log_electron_triton_profile = [ - mfile_data.data[f"plasma_coulomb_log_electron_triton_profile{i}"].get_scan(scan) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - - plasma_coulomb_log_deuteron_triton_profile = [ - mfile_data.data[f"plasma_coulomb_log_deuteron_triton_profile{i}"].get_scan(scan) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - - plasma_coulomb_log_electron_alpha_thermal_profile = [ - mfile_data.data[ - f"plasma_coulomb_log_electron_alpha_thermal_profile{i}" - ].get_scan(scan) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - - axis.plot( - np.linspace(0, 1, len(plasma_coulomb_log_electron_electron_profile)), - plasma_coulomb_log_electron_electron_profile, - color="blue", - linestyle="-", - label=r"$ln \Lambda_{e-e}$", - ) - - axis.plot( - np.linspace(0, 1, len(plasma_coulomb_log_electron_deuteron_profile)), - plasma_coulomb_log_electron_deuteron_profile, - color="pink", - linestyle="-", - label=r"$ln \Lambda_{e-D}$", - ) - - axis.plot( - np.linspace(0, 1, len(plasma_coulomb_log_electron_triton_profile)), - plasma_coulomb_log_electron_triton_profile, - color="green", - linestyle="-", - label=r"$ln \Lambda_{e-T}$", - ) - - axis.plot( - np.linspace(0, 1, len(plasma_coulomb_log_deuteron_triton_profile)), - plasma_coulomb_log_deuteron_triton_profile, - color="orange", - linestyle="-", - label=r"$ln \Lambda_{D-T}$", - ) - - axis.plot( - np.linspace(0, 1, len(plasma_coulomb_log_electron_alpha_thermal_profile)), - plasma_coulomb_log_electron_alpha_thermal_profile, - color="red", - linestyle="-", - label=r"$ln \Lambda_{e-\alpha,thermal}$", - ) - - axis.set_ylabel("Coulomb Logarithm") - axis.set_xlabel("$\\rho \\ [r/a]$") - axis.grid(True, which="both", linestyle="--", alpha=0.5) - axis.minorticks_on() - axis.legend() - - -def plot_collision_time_profile(axis: plt.Axes, mfile_data: MFile, scan: int) -> None: - """Plot the plasma collision times on the given axis.""" - t_plasma_electron_electron_collision_profile = [ - mfile_data.data[f"t_plasma_electron_electron_collision_profile{i}"].get_scan( - scan - ) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - - t_plasma_electron_deuteron_collision_profile = [ - mfile_data.data[f"t_plasma_electron_deuteron_collision_profile{i}"].get_scan( - scan - ) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - - t_plasma_electron_triton_collision_profile = [ - mfile_data.data[f"t_plasma_electron_triton_collision_profile{i}"].get_scan(scan) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - - t_plasma_electron_alpha_thermal_collision_profile = [ - mfile_data.data[ - f"t_plasma_electron_alpha_thermal_collision_profile{i}" - ].get_scan(scan) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - - axis.plot( - np.linspace(0, 1, len(t_plasma_electron_electron_collision_profile)), - t_plasma_electron_electron_collision_profile, - color="blue", - linestyle="-", - label=r"$\tau_{e-e}$", - ) - - axis.plot( - np.linspace(0, 1, len(t_plasma_electron_deuteron_collision_profile)), - t_plasma_electron_deuteron_collision_profile, - color="pink", - linestyle="-", - label=r"$\tau_{e-D}$", - ) - - axis.plot( - np.linspace(0, 1, len(t_plasma_electron_triton_collision_profile)), - t_plasma_electron_triton_collision_profile, - color="green", - linestyle="-", - label=r"$\tau_{e-T}$", - ) - - axis.plot( - np.linspace(0, 1, len(t_plasma_electron_alpha_thermal_collision_profile)), - t_plasma_electron_alpha_thermal_collision_profile, - color="red", - linestyle="-", - label=r"$\tau_{e-\alpha,thermal}$", - ) - - axis.set_yscale("log") - axis.set_ylabel("Collision Time [s]") - axis.set_xlabel("$\\rho \\ [r/a]$") - axis.grid(True, which="both", linestyle="--", alpha=0.5) - axis.minorticks_on() - axis.legend() - - -def plot_collision_frequency_profile( - axis: plt.Axes, mfile_data: MFile, scan: int -) -> None: - """Plot the plasma collision frequencies on the given axis.""" - freq_plasma_electron_electron_collision_profile = [ - mfile_data.data[f"freq_plasma_electron_electron_collision_profile{i}"].get_scan( - scan - ) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - - freq_plasma_electron_deuteron_collision_profile = [ - mfile_data.data[f"freq_plasma_electron_deuteron_collision_profile{i}"].get_scan( - scan - ) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - - freq_plasma_electron_triton_collision_profile = [ - mfile_data.data[f"freq_plasma_electron_triton_collision_profile{i}"].get_scan( - scan - ) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - - freq_plasma_electron_alpha_thermal_collision_profile = [ - mfile_data.data[ - f"freq_plasma_electron_alpha_thermal_collision_profile{i}" - ].get_scan(scan) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - - axis.plot( - np.linspace(0, 1, len(freq_plasma_electron_electron_collision_profile)), - freq_plasma_electron_electron_collision_profile, - color="blue", - linestyle="-", - label=r"$\nu_{e-e}$", - ) - - axis.plot( - np.linspace(0, 1, len(freq_plasma_electron_deuteron_collision_profile)), - freq_plasma_electron_deuteron_collision_profile, - color="pink", - linestyle="-", - label=r"$\nu_{e-D}$", - ) - - axis.plot( - np.linspace(0, 1, len(freq_plasma_electron_triton_collision_profile)), - freq_plasma_electron_triton_collision_profile, - color="green", - linestyle="-", - label=r"$\nu_{e-T}$", - ) - - axis.plot( - np.linspace(0, 1, len(freq_plasma_electron_alpha_thermal_collision_profile)), - freq_plasma_electron_alpha_thermal_collision_profile, - color="red", - linestyle="-", - label=r"$\nu_{e-\alpha,thermal}$", - ) - axis.set_yscale("log") - axis.set_ylabel("Collision Frequency [Hz]") - axis.set_xlabel("$\\rho \\ [r/a]$") - axis.grid(True, which="both", linestyle="--", alpha=0.5) - axis.minorticks_on() - axis.legend() - - -def plot_mean_free_path_profile(axis: plt.Axes, mfile_data: MFile, scan: int) -> None: - """Plot the plasma mean free path on the given axis.""" - len_plasma_electron_electron_mean_free_path_profile = [ - mfile_data.data[ - f"len_plasma_electron_electron_mean_free_path_profile{i}" - ].get_scan(scan) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - - len_plasma_electron_deuteron_mean_free_path_profile = [ - mfile_data.data[ - f"len_plasma_electron_deuteron_mean_free_path_profile{i}" - ].get_scan(scan) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - - len_plasma_electron_triton_mean_free_path_profile = [ - mfile_data.data[ - f"len_plasma_electron_triton_mean_free_path_profile{i}" - ].get_scan(scan) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - - len_plasma_electron_alpha_thermal_mean_free_path_profile = [ - mfile_data.data[ - f"len_plasma_electron_alpha_thermal_mean_free_path_profile{i}" - ].get_scan(scan) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - - axis.plot( - np.linspace(0, 1, len(len_plasma_electron_electron_mean_free_path_profile)), - len_plasma_electron_electron_mean_free_path_profile, - color="blue", - linestyle="-", - label=r"$\lambda_{mfp,e-e}$", - ) - - axis.plot( - np.linspace(0, 1, len(len_plasma_electron_deuteron_mean_free_path_profile)), - len_plasma_electron_deuteron_mean_free_path_profile, - color="pink", - linestyle="-", - label=r"$\lambda_{mfp,e-D}$", - ) - - axis.plot( - np.linspace(0, 1, len(len_plasma_electron_triton_mean_free_path_profile)), - len_plasma_electron_triton_mean_free_path_profile, - color="green", - linestyle="-", - label=r"$\lambda_{mfp,e-T}$", - ) - axis.plot( - np.linspace(0, 1, len(len_plasma_electron_alpha_thermal_mean_free_path_profile)), - len_plasma_electron_alpha_thermal_mean_free_path_profile, - color="red", - linestyle="-", - label=r"$\lambda_{mfp,e-\alpha,thermal}$", - ) - axis.set_yscale("log") - axis.set_ylabel("Mean Free Path [m]") - axis.set_xlabel("$\\rho \\ [r/a]$") - axis.grid(True, which="both", linestyle="--", alpha=0.5) - axis.minorticks_on() - axis.legend() - - -def plot_ion_slowing_down_time_profile( - axis: plt.Axes, mfile_data: MFile, scan: int -) -> None: - """Plot the plasma Spitzer slowing down time on the given axis.""" - t_plasma_electron_alpha_spitzer_slow_profile = [ - mfile_data.data[f"t_plasma_electron_alpha_spitzer_slow_profile{i}"].get_scan( - scan - ) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - - axis.plot( - np.linspace(0, 1, len(t_plasma_electron_alpha_spitzer_slow_profile)), - t_plasma_electron_alpha_spitzer_slow_profile, - color="red", - linestyle="-", - label=r"$\tau_{e-\alpha,Spitzer}$", - ) - - axis.set_yscale("log") - axis.set_ylabel("Spitzer Slowing Down Time [s]") - axis.set_xlabel("$\\rho \\ [r/a]$") - axis.grid(True, which="both", linestyle="--", alpha=0.5) - axis.minorticks_on() - axis.legend() - - -def plot_resistivity_profile(axis: plt.Axes, mfile_data: MFile, scan: int) -> None: - """Plot the plasma resistivity on the given axis.""" - res_plasma_fuel_spitzer_profile = [ - mfile_data.data[f"res_plasma_fuel_spitzer_profile{i}"].get_scan(scan) - for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) - ] - - axis.plot( - np.linspace(0, 1, len(res_plasma_fuel_spitzer_profile)), - res_plasma_fuel_spitzer_profile, - color="red", - linestyle="-", - label=r"$\eta_{Spitzer-fuel}$", - ) - - axis.set_yscale("log") - axis.set_ylabel("Resistivity [Ohm m]") - axis.set_xlabel("$\\rho \\ [r/a]$") - axis.grid(True, which="both", linestyle="--", alpha=0.5) - axis.minorticks_on() - axis.legend() - - -def plot_equality_constraint_equations(axis: plt.Axes, m_file_data: MFile, scan: int): - """Plot the equality constraints for a solution and their normalised residuals - - Parameters - ---------- - axis: plt.Axes : - - m_file_data: MFile : - - scan: int : - - """ - y_labels = [] - y_pos = [] - n_plot = 0 - - # Build a mapping from itvar index to its name (description) - con_names = {} - con_numbers = {} - for var in m_file_data.data: - if var.startswith("eq_con"): - idx = int(var[6:]) # e.g. "itvar001" -> 1 - con_names[idx] = m_file_data.data[var].var_description - con_numbers[idx] = idx - - for n_plot, n in enumerate(con_numbers.values()): - # Constraint value needed - con_value = m_file_data.data[f"val_eq_con{n:03d}"].get_scan(scan) - - # Use the variable name if available, else fallback to "eq_conXXX" - var_label = con_names.get(n, f"eq_con{n:03d}") - - # Normalized residual of the constraint - con_norm_residual = m_file_data.data[f"eq_con{n:03d}"].get_scan(scan) - - # Unit type of the constraint - con_units_raw = m_file_data.data[f"eq_units_con{n:03d}"].get_scan(scan) - con_units = str(con_units_raw).strip("'`") - - # Remove '_normalised_residue' from the label if present - if isinstance(var_label, str) and var_label.endswith("_normalised_residue"): - var_label = var_label.replace("_normalised_residue", "") - - # Remove trailing underscores and replace underscores between words with spaces - var_label = var_label.rstrip("_").replace("_", " ") - - # Plot the normalised residual as a bar - axis.barh( - n_plot, - con_norm_residual, - height=0.6, - color="blue", - label="Normalized Residual" if n_plot == 0 else "", - align="center", - ) - - # Add the value as a number to the right of the bar - axis.text( - con_norm_residual + 0.52, - n_plot, - f"{con_norm_residual:.8g}", - va="center", - ha="left", - fontsize=8, - color="blue", - ) - - # Add the constraint value as text to the left of the y-axis - axis.text( - 0.45, - n_plot, - f"{con_value:.8g} {con_units}", - va="center", - ha="right", - fontsize=8, - color="black", - ) - - y_labels.append(var_label) - y_pos.append(n_plot) - - axis.axvline(0.5, color="red", linewidth=2, zorder=0) - axis.set_yticks(y_pos) - axis.set_yticklabels(y_labels) - axis.set_facecolor("#f5f5f5") - axis.set_xlim(-0.4, 1.2) # Normalised bounds - axis.set_title("Equality Constraint Equations") - axis.set_xticks([]) - axis.legend() - - -def plot_inequality_constraint_equations(axis: plt.Axes, m_file: MFile, scan: int): - """Plot the inequality constraints for a solution and where they lay within their bounds - - Parameters - ---------- - axis: plt.Axes : - - m_file: MFile : - - scan: int : - - """ - y_labels = [] - y_pos = [] - n_plot = 0 - - # Build a mapping from itvar index to its name (description) - con_names = {} - con_numbers = {} - for var in m_file.data: - if var.startswith("ineq_con"): - idx = int(var[8:]) # e.g. "ineq_con001" -> 1 - con_names[idx] = m_file.data[var].var_description - con_numbers[idx] = idx - - for n_plot, n in enumerate(con_numbers.values()): - # Constraint value/bound - con_bound = m_file.data[f"ineq_bound_con{n:03d}"].get_scan(scan) - - # Value of constraint variable - con_value = m_file.data[f"ineq_value_con{n:03d}"].get_scan(scan) - - # Constraint symbol can be `<=` for an upper limit or `>=` for a lower limit - con_symbol = m_file.data[f"ineq_symbol_con{n:03d}"].get_scan(scan) - - # Use the variable name if available, else fallback to "ineq_conXXX" - var_label = con_names.get(n, f"ineq_con{n:03d}") - - # Normalized residual of the constraint - con_residual_norm = m_file.data[f"ineq_con{n:03d}"].get_scan(scan) - - # Unit type of the constraint - con_units = m_file.data[f"ineq_units_con{n:03d}"].get_scan(scan).strip("'`") - - # Add a vertical line at the normalised constraint bounds of 0 and 1 - axis.axvline( - 0.0, - color="red", - linestyle="--", - linewidth=1.5, - zorder=0, - ) - - axis.axvline( - 1.0, - color="red", - linestyle="--", - linewidth=1.5, - zorder=0, - ) - - # Remove '_normalised_residue' from the label if present - if isinstance(var_label, str) and var_label.endswith("_normalised_residue"): - var_label = var_label.replace("_normalised_residue", "") - var_label = var_label.rstrip("_").replace("_", " ") - - # Calculate the normalised constraint threshold depending if the constraint is an upper - # or lower limit - if con_symbol == "'<='": - normalised_value = 1 - con_residual_norm - bar_left = normalised_value - bar_width = 1 - normalised_value - else: - # For a lower limit, the normalised value is the residual itself - normalised_value = con_residual_norm - bar_left = 0 - # Set the bar width to be 1/10 times the normalised value, - # but cap it at 1.0 to avoid overly long bars - bar_width = min(normalised_value * 0.1, 1.0) - - # If the constraint value is very close to the bound then plot a square marker at the bound - if np.isclose(normalised_value, 1.0, atol=1e-3): - axis.plot( - 1, - n_plot, - "s", - color="black", - markersize=8, - zorder=5, - ) - elif np.isclose(normalised_value, 0.0, atol=1e-3): - axis.plot( - 0, - n_plot, - "s", - color="black", - markersize=8, - zorder=5, - ) - - else: - # If constraint value is not very close to bound then plot bar as normal - axis.barh( - n_plot, - bar_width, - left=bar_left, - color="blue", - edgecolor="black", - linewidth=1.5, - height=1.0, - alpha=0.7, - label="Constraint Value" if n_plot == 0 else "", - ) - - # Plot the value as a number at x = 0.5 - axis.text( - 0.5, - n_plot, - f"{con_value:,.8g} {con_units}", - va="center", - ha="center", - fontsize=8, - color=( - "orange" - if np.isclose(normalised_value, 1.0, atol=1e-3) - or np.isclose(normalised_value, 0.0, atol=1e-3) - else "green" - ), - bbox={ - "boxstyle": "round", - "facecolor": "white", - "alpha": 0.8, - "edgecolor": "white", - "linewidth": 1, - }, - ) - # Annoate the bound value depending if it is an upper or lower limit - if con_symbol == "'<='": - # Add the constraint symbol and bound as text - axis.text( - 1.02, # Position text slightly to the right of the normalised bound - n_plot, - f"$\\leq$ {con_bound:,.8g} {con_units}", - va="center", - ha="left", - fontsize=8, - color="black", - ) - else: # con_symbol == ">=" - axis.text( - -0.025, # Position text slightly to the left of the normalised bound - n_plot, - f"$\\geq$ {con_bound:,.8g} {con_units}", - va="center", - ha="right", - fontsize=8, - color="black", - ) - - y_labels.append(var_label) - y_pos.append(n_plot) - - axis.set_yticks(y_pos) - axis.set_yticklabels(y_labels) - axis.set_title("Inequality Constraint Equations") - axis.set_xlim(-0.3, 1.275) - axis.set_xticks([]) - axis.set_facecolor("#f5f5f5") - axis.set_xticks(np.arange(0, 1.0, 0.1)) - axis.grid(True, axis="x", linestyle="--", alpha=0.3) - axis.set_xticklabels([]) - - -def plot_blkt_structure( - ax: plt.Axes, - fig: plt.Figure, - m_file: MFile, - scan: int, - radial_build: dict[str, float], - colour_scheme: Literal[1, 2], -): - """Plot the blkt structure and relevant angles""" - # MFILE variables needed to plot the blkt structure and angles - rmajor = m_file.get("rmajor", scan=scan) - rminor = m_file.get("rminor", scan=scan) - dr_fw_plasma_gap_outboard = m_file.get("dr_fw_plasma_gap_outboard", scan=scan) - dr_fw_plasma_gap_inboard = m_file.get("dr_fw_plasma_gap_inboard", scan=scan) - dr_fw_inboard = m_file.get("dr_fw_inboard", scan=scan) - dr_fw_outboard = m_file.get("dr_fw_outboard", scan=scan) - dr_blkt_outboard = m_file.get("dr_blkt_outboard", scan=scan) - dr_blkt_inboard = m_file.get("dr_blkt_inboard", scan=scan) - dz_blkt_half = m_file.get("dz_blkt_half", scan=scan) - deg_blkt_outboard_poloidal_plasma = m_file.get( - "deg_blkt_outboard_poloidal_plasma", scan=scan - ) - deg_blkt_inboard_poloidal_plasma = m_file.get( - "deg_blkt_inboard_poloidal_plasma", scan=scan - ) - f_deg_blkt_outboard_poloidal_plasma = m_file.get( - "f_deg_blkt_outboard_poloidal_plasma", scan=scan - ) - f_deg_blkt_inboard_poloidal_plasma = m_file.get( - "f_deg_blkt_inboard_poloidal_plasma", scan=scan - ) - deg_div_poloidal_plasma = m_file.get("deg_div_poloidal_plasma", scan=scan) - f_ster_div_single = m_file.get("f_ster_div_single", scan=scan) - i_single_null = m_file.get("i_single_null", scan=scan) - - # ====================== - - plot_blanket(ax, m_file, scan, radial_build, colour_scheme) - plot_plasma(ax, m_file, scan, colour_scheme) - plot_firstwall(ax, m_file, scan, radial_build, colour_scheme) - - ax.set_xlabel("Radial position [m]") - ax.set_ylabel("Vertical position [m]") - ax.set_title("Blanket and First Wall Poloidal Cross-Section") - ax.minorticks_on() - ax.grid(which="minor", linestyle=":", linewidth=0.5, alpha=0.5) - - r_blkt_outboard_out = ( - rmajor + rminor + dr_fw_outboard + dr_fw_plasma_gap_outboard + dr_blkt_outboard - ) - r_blkt_inboard_in = ( - rmajor - rminor - dr_fw_plasma_gap_inboard - dr_fw_inboard - dr_blkt_inboard - ) - r_fw_outboard_in = r_blkt_outboard_out - dr_blkt_outboard - dr_fw_outboard - r_fw_inboard_out = r_blkt_inboard_in + dr_blkt_inboard + dr_fw_inboard - - # Plot a horizontal line at dz_blkt_half (blanket half height) - for dz_blkt in (dz_blkt_half, -dz_blkt_half): - ax.axhline( - dz_blkt, - color="purple", - linestyle="--", - linewidth=1.5, - label="Blanket Half Height", - ) - - if DivertorNumberModels(i_single_null) == DivertorNumberModels.DOUBLE_NULL: - # Plot arrows for the outboard blanket angles - ax.annotate( - "", - xy=(rmajor, 0), - xytext=(rmajor, dz_blkt_half), - arrowprops={"arrowstyle": "<-", "color": "purple"}, - zorder=5, - ) - # If single null then only plot the lower arrow for the outboard blanket angle - ax.annotate( - "", - xy=(rmajor, 0), - xytext=(rmajor, -dz_blkt_half), - arrowprops={"arrowstyle": "<-", "color": "purple"}, - zorder=5, - ) - - # Plot arc showing the angle between the two outboard blanket arrows - arc_radius = 1.0 - - # 3 to 6 o'clock position is -90 degrees, - angle_start = -90.0 - match DivertorNumberModels(i_single_null): - case DivertorNumberModels.SINGLE_NULL: - angle_end = 90.0 + deg_div_poloidal_plasma - case DivertorNumberModels.DOUBLE_NULL: - # 3 to 12 o'clock position is +90 degrees - angle_end = 90.0 - - theta = np.linspace(np.deg2rad(angle_start), np.deg2rad(angle_end), 50) - arc_x = rmajor + arc_radius * np.cos(theta) - arc_y = arc_radius * np.sin(theta) - - ax.plot(arc_x, arc_y, color="purple", linewidth=2) - - # Add angle label at the arc - mid_angle = np.deg2rad((angle_start + angle_end) / 2) - label_radius = arc_radius * 1.8 - label_x = rmajor + label_radius * np.cos(mid_angle) - label_y = label_radius * np.sin(mid_angle) - - # Plot the info box for the outboard blanket - ax.text( - label_x, - label_y, - f"{deg_blkt_outboard_poloidal_plasma:.1f}°\n({f_deg_blkt_outboard_poloidal_plasma * 100:.1f}%)", - fontsize=7, - color="purple", - ha="center", - va="center", - weight="bold", - bbox={ - "boxstyle": "round", - "facecolor": "white", - "alpha": 0.8, - "edgecolor": "purple", - "linewidth": 1.5, - }, - ) - - # Plot arrows for the inboard blanket angles - for dz_blkt in (dz_blkt_half, -dz_blkt_half): - ax.annotate( - "", - xy=(rmajor, 0), - xytext=(r_fw_inboard_out, dz_blkt), - arrowprops={"arrowstyle": "<-", "color": "green"}, - zorder=5, - ) - - # Plot arc showing the angle between the two inboard blanket arrows - arc_radius = 1.0 - angle_start = -deg_blkt_inboard_poloidal_plasma / 2 - angle_end = deg_blkt_inboard_poloidal_plasma / 2 - - theta = np.linspace(np.deg2rad(angle_start), np.deg2rad(angle_end), 50) - arc_x = rmajor - arc_radius * np.cos(theta) - arc_y = arc_radius * np.sin(theta) - - ax.plot(arc_x, arc_y, color="green", linewidth=2) - - # Add angle label at the arc - mid_angle = np.deg2rad((angle_start + angle_end) / 2) - label_radius = arc_radius * 1.8 - label_x = rmajor - label_radius * np.cos(mid_angle) - label_y = label_radius * np.sin(mid_angle) - - # Plot the info box for the inboard blanket - ax.text( - label_x, - label_y, - f"{deg_blkt_inboard_poloidal_plasma:.1f}°\n({f_deg_blkt_inboard_poloidal_plasma * 100:.1f}%)", - fontsize=7, - color="green", - ha="center", - va="center", - weight="bold", - bbox={ - "boxstyle": "round", - "facecolor": "white", - "alpha": 0.8, - "edgecolor": "green", - "linewidth": 1.5, - }, - zorder=5, - ) - - # Plot arrows for the divertor angles - # If double null then plot the upper also - if DivertorNumberModels(i_single_null) == DivertorNumberModels.DOUBLE_NULL: - # Plot arc showing the angle between the two arrows (divertor angle) - arc_radius = 1.5 - # 3 to 12 o'clock position is +90 degrees, - angle_start = 90.0 - angle_end = 90.0 + deg_div_poloidal_plasma - - theta = np.linspace(np.deg2rad(angle_start), np.deg2rad(angle_end), 50) - arc_x = rmajor + arc_radius * np.cos(theta) - arc_y = arc_radius * np.sin(theta) - - ax.plot(arc_x, arc_y, color="black", linewidth=2) - - # Add angle label at the arc - mid_angle = np.deg2rad((angle_start + angle_end) / 2) - label_radius = arc_radius * 1.8 - label_x = rmajor + label_radius * np.cos(mid_angle) - label_y = label_radius * np.sin(mid_angle) - - ax.text( - label_x, - label_y, - f"{deg_div_poloidal_plasma:.1f}°\n({f_ster_div_single * 100:.1f}%)", - fontsize=7, - color="black", - ha="center", - va="center", - weight="bold", - bbox={ - "boxstyle": "round", - "facecolor": "white", - "alpha": 0.8, - "edgecolor": "black", - "linewidth": 1.5, - }, - zorder=5, - ) - - # Plot arc showing the angle between the two arrows for the lower divertor (divertor angle) - arc_radius = 1.5 - # 3 to 6 o'clock is -90 degrees - angle_start = -90.0 - angle_end = angle_start - deg_div_poloidal_plasma - - theta = np.linspace(np.deg2rad(angle_start), np.deg2rad(angle_end), 50) - arc_x = rmajor + arc_radius * np.cos(theta) - arc_y = arc_radius * np.sin(theta) - - ax.plot(arc_x, arc_y, color="black", linewidth=2) - - # Add angle label at the arc - mid_angle = np.deg2rad((angle_start + angle_end) / 2) - label_radius = arc_radius * 1.8 - label_x = rmajor + label_radius * np.cos(mid_angle) - label_y = label_radius * np.sin(mid_angle) - - # Plot the info box for the lower divertor angle - ax.text( - label_x, - label_y, - f"{deg_div_poloidal_plasma:.1f}°\n({f_ster_div_single * 100:.1f}%)", - fontsize=7, - color="black", - ha="center", - va="center", - weight="bold", - bbox={ - "boxstyle": "round", - "facecolor": "white", - "alpha": 0.8, - "edgecolor": "black", - "linewidth": 1.5, - }, - zorder=5, - ) - - # Plot vertical lines at the inner and outer radial boundaries of the blanket - linestyle = {"color": "black", "linestyle": "--", "linewidth": 1.5, "zorder": 10} - ax.axvline(r_blkt_inboard_in, **linestyle) - ax.axvline(r_blkt_outboard_out, **linestyle) - ax.axvline(r_fw_inboard_out, **linestyle) - ax.axvline(r_fw_outboard_in, **linestyle) - - ax.axvline( - rmajor, color="black", linestyle="--", linewidth=1.5, label="Major Radius $R_0$" - ) - - # Plot midplane line (horizontal dashed line at Z=0) - ax.axhline(0.0, color="black", linestyle="--", linewidth=1.5, label="Midplane") - - textstr_blkt_areas = ( - f"$\\mathbf{{Blanket \\ Areas:}}$\n\n" - f"Inboard blanket, with holes and gaps: {m_file.get('a_blkt_inboard_surface', scan=scan):,.3f} $\\text{{m}}^2$\n" - f"Outboard blanket, with holes and gaps: {m_file.get('a_blkt_outboard_surface', scan=scan):,.3f} $\\text{{m}}^2$\n" - f"Total blanket, with holes and gaps: {m_file.get('a_blkt_total_surface', scan=scan):,.3f} $\\text{{m}}^2$\n\n" - f"Inboard blanket, full coverage: {m_file.get('a_blkt_inboard_surface_full_coverage', scan=scan):,.3f} $\\text{{m}}^2$\n" - f"Outboard blanket, full coverage: {m_file.get('a_blkt_outboard_surface_full_coverage', scan=scan):,.3f} $\\text{{m}}^2$\n" - f"Total blanket, full coverage: {m_file.get('a_blkt_total_surface_full_coverage', scan=scan):,.3f} $\\text{{m}}^2$ " - ) - - ax.text( - 0.05, - 0.3, - textstr_blkt_areas, - **_text_layout(fig), - bbox=_box_style("wheat"), - ) - - textstr_blkt_volumes = ( - f"$\\mathbf{{Blanket \\ Volumes:}}$\n\n" - f"Inboard blanket, with holes and gaps: {m_file.get('vol_blkt_inboard', scan=scan):,.3f} $\\text{{m}}^3$\n" - f"Outboard blanket, with holes and gaps: {m_file.get('vol_blkt_outboard', scan=scan):,.3f} $\\text{{m}}^3$\n" - f"Total blanket, with holes and gaps: {m_file.get('vol_blkt_total', scan=scan):,.3f} $\\text{{m}}^3$\n\n" - f"Inboard blanket, full coverage: {m_file.get('vol_blkt_inboard_full_coverage', scan=scan):,.3f} $\\text{{m}}^3$\n" - f"Outboard blanket, full coverage: {m_file.get('vol_blkt_outboard_full_coverage', scan=scan):,.3f} $\\text{{m}}^3$\n" - f"Total blanket, full coverage: {m_file.get('vol_blkt_total_full_coverage', scan=scan):,.3f} $\\text{{m}}^3$ " - ) - - ax.text( - 0.05, - 0.05, - textstr_blkt_volumes, - **_text_layout(fig), - bbox=_box_style("wheat"), - ) - - -def plot_detailed_plasma_parameters(axis: plt.Axes, fig, mfile: MFile, scan: int): - """Function to plot detailed plasma parameters from physics data. - - Parameters - ---------- - axis : plt.Axes - Axis object to plot to - fig : plt.Figure - Figure object for text placement - mfile : MFile - MFILE data object - scan : int - Scan number to use - """ - textstr_debye = ( - f"$\\mathbf{{Debye \\ Lengths:}}$\n\n" - f"$\\langle\\lambda_{{Debye,e}}\\rangle$: {mfile.get('len_plasma_debye_electron_vol_avg', scan=scan):.4e} m" - ) - - textstr_larmor = ( - f"$\\mathbf{{Larmor \\ Radii:}}$\n\n" - f"$\\langle\\rho_{{Larmor,toroidal,D}}\\rangle$: {mfile.get('radius_plasma_deuteron_toroidal_larmor_isotropic_vol_avg', scan=scan):.4e} m\n" - f"$\\langle\\rho_{{Larmor,toroidal,T}}\\rangle$: {mfile.get('radius_plasma_triton_toroidal_larmor_isotropic_vol_avg', scan=scan):.4e} m" - ) - - textstr_velocities = ( - f"$\\mathbf{{Velocities:}}$\n\n" - f"$\\langle v_{{e}}\\rangle$: {mfile.get('vel_plasma_electron_vol_avg', scan=scan):.4e} m/s\n" - f"$\\langle v_{{D}}\\rangle$: {mfile.get('vel_plasma_deuteron_vol_avg', scan=scan):.4e} m/s\n" - f"$\\langle v_{{T}}\\rangle$: {mfile.get('vel_plasma_triton_vol_avg', scan=scan):.4e} m/s\n" - f"$\\langle v_{{\\alpha,thermal}}\\rangle$: {mfile.get('vel_plasma_alpha_thermal_vol_avg', scan=scan):.4e} m/s\n" - f"$v_{{\\alpha,birth}}$: {mfile.get('vel_plasma_alpha_birth', scan=scan):.4e} m/s" - ) - - textstr_frequencies = ( - f"$\\mathbf{{Frequencies:}}$\n\n" - f"$\\langle\\omega_{{p,e}}\\rangle$: {mfile.get('freq_plasma_electron_vol_avg', scan=scan):.4e} Hz\n" - f"$\\langle f_{{Larmor,toroidal,e}}\\rangle$: {mfile.get('freq_plasma_larmor_toroidal_electron_vol_avg', scan=scan):.4e} Hz\n" - f"$\\langle f_{{Larmor,toroidal,D}}\\rangle$: {mfile.get('freq_plasma_larmor_toroidal_deuteron_vol_avg', scan=scan):.4e} Hz\n" - f"$\\langle f_{{Larmor,toroidal,T}}\\rangle$: {mfile.get('freq_plasma_larmor_toroidal_triton_vol_avg', scan=scan):.4e} Hz\n" - f"$\\langle\\omega_{{UH,e}}\\rangle$: {mfile.get('freq_plasma_upper_hybrid_vol_avg', scan=scan):.4e} Hz" - ) - - textstr_coulomb = ( - f"$\\mathbf{{Coulomb \\ Logarithms:}}$\n\n" - f"$\\langle\\ln \\Lambda_{{e-e}}\\rangle$: {mfile.get('plasma_coulomb_log_electron_electron_vol_avg', scan=scan):.4f}\n" - f"$\\langle\\ln \\Lambda_{{e-D}}\\rangle$: {mfile.get('plasma_coulomb_log_electron_deuteron_vol_avg', scan=scan):.4f}\n" - f"$\\langle\\ln \\Lambda_{{e-T}}\\rangle$: {mfile.get('plasma_coulomb_log_electron_triton_vol_avg', scan=scan):.4f}\n" - f"$\\langle\\ln \\Lambda_{{D-T}}\\rangle$: {mfile.get('plasma_coulomb_log_deuteron_triton_vol_avg', scan=scan):.4f}\n" - f"$\\langle\\ln \\Lambda_{{e-\\alpha}}\\rangle$: {mfile.get('plasma_coulomb_log_electron_alpha_thermal_vol_avg', scan=scan):.4f}" - ) - - textstr_collision_times = ( - f"$\\mathbf{{Collision \\ Times:}}$\n\n" - f"$\\langle\\tau_{{e-e}}\\rangle$: {mfile.get('t_plasma_electron_electron_collision_vol_avg', scan=scan):.4e} s\n" - f"$\\langle\\tau_{{e-D}}\\rangle$: {mfile.get('t_plasma_electron_deuteron_collision_vol_avg', scan=scan):.4e} s\n" - f"$\\langle\\tau_{{e-T}}\\rangle$: {mfile.get('t_plasma_electron_triton_collision_vol_avg', scan=scan):.4e} s\n" - f"$\\langle\\tau_{{e-\\alpha}}\\rangle$: {mfile.get('t_plasma_electron_alpha_thermal_collision_vol_avg', scan=scan):.4e} s" - ) - - textstr_collision_freq = ( - f"$\\mathbf{{Collision \\ Frequencies:}}$\n\n" - f"$\\langle\\nu_{{e-e}}\\rangle$: {mfile.get('freq_plasma_electron_electron_collision_vol_avg', scan=scan):.4e} Hz\n" - f"$\\langle\\nu_{{e-D}}\\rangle$: {mfile.get('freq_plasma_electron_deuteron_collision_vol_avg', scan=scan):.4e} Hz\n" - f"$\\langle\\nu_{{e-T}}\\rangle$: {mfile.get('freq_plasma_electron_triton_collision_vol_avg', scan=scan):.4e} Hz\n" - f"$\\langle\\nu_{{e-\\alpha}}\\rangle$: {mfile.get('freq_plasma_electron_alpha_thermal_collision_vol_avg', scan=scan):.4e} Hz" - ) - - textstr_mfp = ( - f"$\\mathbf{{Mean \\ Free \\ Paths:}}$\n\n" - f"$\\langle\\lambda_{{mfp,e-e}}\\rangle$: {mfile.get('len_plasma_electron_electron_mean_free_path_vol_avg', scan=scan):.4e} m\n" - f"$\\langle\\lambda_{{mfp,e-D}}\\rangle$: {mfile.get('len_plasma_electron_deuteron_mean_free_path_vol_avg', scan=scan):.4e} m\n" - f"$\\langle\\lambda_{{mfp,e-T}}\\rangle$: {mfile.get('len_plasma_electron_triton_mean_free_path_vol_avg', scan=scan):.4e} m\n" - f"$\\langle\\lambda_{{mfp,e-\\alpha}}\\rangle$: {mfile.get('len_plasma_electron_alpha_thermal_mean_free_path_vol_avg', scan=scan):.4e} m" - ) - - textstr_spitzer = ( - f"$\\mathbf{{Spitzer \\ Slowing \\ Down:}}$\n\n" - f"$\\langle\\tau_{{e-\\alpha,Spitzer}}\\rangle$: {mfile.get('t_plasma_electron_alpha_spitzer_slow_vol_avg', scan=scan):.4e} s" - ) - - textstr_resistivity = ( - f"$\\mathbf{{Resistivities:}}$\n\n" - f"$\\langle\\eta_{{Spitzer}}\\rangle$: {mfile.get('res_plasma_fuel_spitzer_vol_avg', scan=scan):.4e} $\\Omega\\mathrm{{m}}$" - ) - - light_yellow_box = { - "boxstyle": "round", - "facecolor": "lightyellow", - "alpha": 1.0, - "linewidth": 2, - } - - axis.text( - 0.05, - 0.45, - textstr_debye, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=light_yellow_box, - ) - - axis.text( - 0.25, - 0.45, - textstr_larmor, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=light_yellow_box, - ) - - axis.text( - 0.45, - 0.45, - textstr_velocities, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=light_yellow_box, - ) - - light_cyan_box = { - "boxstyle": "round", - "facecolor": "lightcyan", - "alpha": 1.0, - "linewidth": 2, - } - - axis.text( - 0.05, - 0.31, - textstr_frequencies, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=light_cyan_box, - ) - - axis.text( - 0.25, - 0.31, - textstr_coulomb, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=light_cyan_box, - ) - - axis.text( - 0.45, - 0.31, - textstr_collision_times, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=light_cyan_box, - ) - - light_green_box = { - "boxstyle": "round", - "facecolor": "lightgreen", - "alpha": 1.0, - "linewidth": 2, - } - - axis.text( - 0.05, - 0.17, - textstr_collision_freq, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=light_green_box, - ) - - axis.text( - 0.25, - 0.17, - textstr_mfp, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=light_green_box, - ) - - axis.text( - 0.45, - 0.17, - textstr_spitzer + "\n" + textstr_resistivity, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, - bbox=light_green_box, - ) - - axis.axis("off") - - -def plot_quench_time_evolution( - tau_discharge: float, - b_peak: float, - f_a_cable_copper: float, - f_a_cable_space_helium: float, - temp_he_peak: float, - temp_quench_max: float, - cu_rrr: float, - t_quench_detection: float, - fluence: float, - j_operating: float, - a_tf_turn_cable_space: float, - a_tf_turn: float, - n_points: int = 500, - axes_1: plt.Axes | None = None, - axes_2: plt.Axes | None = None, - show: bool = False, -) -> None: - """Plots the time evolution of the quench model hotspot temperature and current. - - Visualises the adiabatic hotspot temperature rise and exponentially decaying - current during a quench, highlighting the quench detection time. - - Parameters - ---------- - tau_discharge: - Quench discharge time constant [s]. - b_peak: - Magnetic field at the peak point [T]. - f_a_cable_copper: - Fraction of cable cross-section that is copper. - f_a_cable_space_helium: - Fraction of cable space occupied by helium. - temp_he_peak: - Peak helium temperature at quench initiation [K]. - temp_quench_max: - Maximum allowed conductor temperature during quench [K]. - cu_rrr: - Residual resistivity ratio of copper. - t_quench_detection: - Detection time delay [s]. - fluence: - Neutron fluence [n/m²]. - j_operating: - Operating current density [A/m²] to compare against the quench protection limit. - a_tf_turn_cable_space: - Area of the TF turn cable space [m²]. - a_tf_turn: - Area of the TF turn [m²]. - n_points: - Number of time points for the plot. - axes_1: - Optional axis for the current density panel. - axes_2: - Optional axis for the hotspot temperature panel. - show: - Whether to display the plot with Matplotlib. Defaults to False to avoid - GUI backend warnings in non-interactive environments. - - Raises - ------ - ValueError - If only one set of axes is provided, instead of both or neither - """ - figure = None - if axes_1 is None and axes_2 is None: - figure, (axes_1, axes_2) = plt.subplots(2, 1, sharex=True) - elif axes_1 is None or axes_2 is None: - msg = "Both axes_1 and axes_2 must be provided together, or neither." - raise ValueError(msg) - - fluence = np.clip(fluence, 0.0, 1.5e23) - - j_max = ( - a_tf_turn_cable_space / a_tf_turn - ) * calculate_quench_protection_current_density( - tau_discharge=tau_discharge, - b_peak=b_peak, - f_a_cable_copper=f_a_cable_copper, - f_a_cable_space_helium=f_a_cable_space_helium, - temp_he_peak=temp_he_peak, - temp_quench_max=temp_quench_max, - cu_rrr=cu_rrr, - t_quench_detection=t_quench_detection, - fluence=fluence, - ) - - fluence_1e23 = 1e23 - j_max_1e23 = ( - a_tf_turn_cable_space / a_tf_turn - ) * calculate_quench_protection_current_density( - tau_discharge=tau_discharge, - b_peak=b_peak, - f_a_cable_copper=f_a_cable_copper, - f_a_cable_space_helium=f_a_cable_space_helium, - temp_he_peak=temp_he_peak, - temp_quench_max=temp_quench_max, - cu_rrr=cu_rrr, - t_quench_detection=t_quench_detection, - fluence=fluence_1e23, - ) - - # Time axis: from 0 to ~4 time constants after discharge begins at detection. - # This ensures later annotations/interpolations at t_quench_detection + tau_discharge - # and beyond remain within the sampled domain. - t_end = max(4.0 * tau_discharge, t_quench_detection + 4.0 * tau_discharge) - times = np.linspace(0.0, t_end, n_points) - - # Current density decays exponentially after detection - decay = np.exp(-(times - t_quench_detection) / tau_discharge) - - j_profile_required, j_profile_required_1e23, j_profile_real = [ - np.where(times < t_quench_detection, j0, j0 * decay) - for j0 in (j_max, j_max_1e23, j_operating) - ] - - # Adiabatic hotspot temperature: integrate heat balance over time - # T(t) is found by inverting: integral_{T0}^{T(t)} [sum(rho*cp)] / rho_cu dT = integral_0^t J² dt - # We accumulate the (∫J² dt) and map it to temperature via the precomputed integral. - f_cu_cable = (1.0 - f_a_cable_space_helium) * f_a_cable_copper - f_sc_cable = (1.0 - f_a_cable_space_helium) * (1.0 - f_a_cable_copper) - - # Build a temperature lookup: cumulative integral from t_he_peak to T - temp_array, cum_integral = _build_cumulative_quench_integral( - temp_he_peak=temp_he_peak, - temp_quench_max=temp_quench_max, - field=b_peak, - rrr=cu_rrr, - fluence=fluence, - f_a_cable_space_helium=f_a_cable_space_helium, - f_cu_cable=f_cu_cable, - f_sc_cable=f_sc_cable, - ) - temp_array_1e23, cum_integral_1e23 = _build_cumulative_quench_integral( - temp_he_peak=temp_he_peak, - temp_quench_max=temp_quench_max, - field=b_peak, - rrr=cu_rrr, - fluence=fluence_1e23, - f_a_cable_space_helium=f_a_cable_space_helium, - f_cu_cable=f_cu_cable, - f_sc_cable=f_sc_cable, - ) - - # Numerically integrate J² dt over time to get MIIT (Mega-Ampere²-seconds) at - # each time step - dt = times[1] - times[0] - miit_required = np.cumsum(j_profile_required**2) * dt - miit_required_1e23 = np.cumsum(j_profile_required_1e23**2) * dt - miit_real = np.cumsum(j_profile_real**2) * dt - - # Convert the cable-space thermal integral to winding-pack basis to match j_profile_*. - area_ratio = a_tf_turn_cable_space / a_tf_turn - scaled_integral = (area_ratio**2) * f_cu_cable * cum_integral - scaled_integral_1e23 = (area_ratio**2) * f_cu_cable * cum_integral_1e23 - hotspot_temp_required = np.interp(miit_required, scaled_integral, temp_array) - hotspot_temp_required_1e23 = np.interp( - miit_required_1e23, scaled_integral_1e23, temp_array_1e23 - ) - hotspot_temp_real = np.interp(miit_real, scaled_integral, temp_array) - - # --- Current density panel --- - axes_1.plot( - times, - j_profile_required, - color="darkorange", - linewidth=2, - label=f"Max allowed current density for protection (fluence = {fluence:.2e} n/m²)", - ) - axes_1.plot( - times, - j_profile_required_1e23, - color="darkorange", - linewidth=2, - linestyle="--", - label="Max allowed current density for protection (fluence = 1e23 n/m²)", - ) - axes_1.plot( - times, - j_profile_real, - color="blue", - linewidth=2, - label="Operating current density", - ) - axes_1.axvline( - t_quench_detection, - color="crimson", - linestyle="--", - linewidth=1.5, - label=f"Detection time ({t_quench_detection:.1f} s)", - ) - axes_1.axvspan( - 0, t_quench_detection, alpha=0.08, color="crimson", label="Pre-detection phase" - ) - axes_1.set_ylabel("Current density [A/m²]") - axes_1.legend(fontsize=9) - axes_1.grid(True, alpha=0.3) - axes_1.set_title( - "TF Coil Quench Protection: Current Density and Hotspot Temperature Evolution" - ) - - # --- Temperature panel --- - axes_2.plot( - times, - hotspot_temp_required, - color="darkorange", - linewidth=2, - label=f"Hotspot temperature at protection limit (fluence = {fluence:.2e} n/m²)", - ) - axes_2.plot( - times, - hotspot_temp_required_1e23, - color="darkorange", - linewidth=2, - linestyle="--", - label="Hotspot temperature at protection limit (fluence = 1e23 n/m²)", - ) - axes_2.plot( - times, - hotspot_temp_real, - color="blue", - linewidth=2, - label="Operating hotspot temperature", - ) - - axes_2.axvline( - t_quench_detection, - color="crimson", - linestyle="--", - linewidth=1.5, - label=f"$t_{{\\text{{detect}}}}$ ({t_quench_detection:.2f} s)", - ) - axes_2.axvspan(0, t_quench_detection, alpha=0.08, color="crimson") - axes_2.axhline( - temp_quench_max, - color="grey", - linestyle=":", - linewidth=1.5, - label=f"$T_{{\\text{{max}}}}$ = {temp_quench_max} K", - ) - axes_2.set_xlabel("Time [s]") - axes_2.set_ylabel("Temperature [K]") - axes_2.legend(fontsize=9) - axes_2.grid(True, alpha=0.3) - - # Mark tau_discharge after detection time with vertical and horizontal lines - tau_time = t_quench_detection + tau_discharge - tau_j = j_max * np.exp( - -1 - ) # current density at t = t_quench_detection + tau_discharge - tau_temp = float(np.interp(tau_time, times, hotspot_temp_required)) - - for ax, val, label in [ - (axes_1, tau_j, f"$J$ at $\\tau_{{\\text{{discharge}}}}$ ({tau_j:.2e} A/m²)"), - ( - axes_2, - tau_temp, - f"$T$ at $\\tau_{{\\text{{discharge}}}}$ ({tau_temp:.1f} K)", - ), - ]: - ax.axvline( - tau_time, - color="forestgreen", - linestyle="--", - linewidth=1.5, - label=f"$t_{{\\text{{detect}}}} + \\tau_{{\\text{{discharge}}}}$ ({tau_time:.2f} s)", - ) - ax.axhline( - val, - color="forestgreen", - linestyle=":", - linewidth=1.5, - label=label, - ) - axes_1.legend(fontsize=9) - axes_1.minorticks_on() - axes_2.legend(fontsize=9) - axes_2.minorticks_on() - - if figure is not None: - figure.tight_layout() - else: - plt.tight_layout() - - if show: - plt.show() - - -def plot_pf_cs_plasma_mutual_inductance( - axis: plt.Axes, m_file: MFile, scan: int -) -> None: - """Plot the mutual inductance between the plasma and PF/CS coils. - - Parameters - ---------- - axis : plt.Axes - Axis to plot on - m_file : MFile - MFILE data object - scan : int - Scan number to read from MFILE - - """ - n_pf_cs_plasma_circuits = int(m_file.get("n_pf_cs_plasma_circuits", scan=scan)) - mutual_inductance = np.zeros((n_pf_cs_plasma_circuits, n_pf_cs_plasma_circuits)) - iohcl = int(m_file.get("iohcl", scan=scan)) - - for coil in range(n_pf_cs_plasma_circuits): - for circuit in range(n_pf_cs_plasma_circuits): - mutual_inductance[coil, circuit] = m_file.get( - f"ind_pf_cs_plasma_mutual[{coil},_{circuit}]", - scan=scan, - ) - - # Create lower triangular matrix - mutual_inductance = np.tril(mutual_inductance) - im = axis.imshow(mutual_inductance, cmap="RdBu_r", aspect="auto") - axis.set_xlabel("Circuit") - axis.set_ylabel("Circuit") - axis.set_title("PF/CS Plasma Mutual Inductance") - axis.set_xticks(range(n_pf_cs_plasma_circuits)) - axis.set_yticks(range(n_pf_cs_plasma_circuits)) - labels = list(range(1, n_pf_cs_plasma_circuits + 1)) - - if iohcl == 1: - labels[-2] = "CS" - labels[-1] = "Plasma" - axis.set_xticklabels(labels) - axis.set_yticklabels(labels) - - # Add boxes around each cell - for i in range(n_pf_cs_plasma_circuits): - for j in range(n_pf_cs_plasma_circuits): - if mutual_inductance[i, j] != 0: - axis.add_patch( - plt.Rectangle( - (j - 0.5, i - 0.5), - 1, - 1, - fill=False, - edgecolor="black", - linewidth=0.5, - ) - ) - # Add text annotation with values - axis.text( - j, - i, - f"{mutual_inductance[i, j]:.3e}", - ha="center", - va="center", - color="white", - fontsize=8, - ) - - axis.get_figure().colorbar(im, ax=axis, label="Mutual Inductance (H)") - - -def plot_cs_radial_stress_profile( - axis: plt.Axes, - mfile: MFile, - scan: int, - j_cs: float, - b_cs_inner: float, -): - """Plot CS radial stress profile""" - r_cs_inner = mfile.get("r_cs_inner", scan=scan) - r_cs_outer = mfile.get("r_cs_outer", scan=scan) - - radii = np.linspace(r_cs_inner, r_cs_outer, num=25) - stress_values = np.array([ - CSCoil.calculate_cs_radial_stress( - r_stress_point=radius, - r_cs_inner=r_cs_inner, - r_cs_outer=r_cs_outer, - j_cs=j_cs, - b_cs_inner=b_cs_inner, - f_poisson_cs_structure=poisson_steel, - ) - for radius in radii - ]) - - axis.plot( - radii, - stress_values / 1e6, - linewidth=2, - label="$\\sigma_{r}$,Radial Stress", - ) - max_idx = np.argmax(np.abs(stress_values)) - max_radius = radii[max_idx] - max_stress = stress_values[max_idx] / 1e6 - axis.axvline(max_radius, color="black", linestyle="--", linewidth=1.0, alpha=0.7) - axis.axhline(max_stress, color="black", linestyle="--", linewidth=1.0, alpha=0.7) - axis.set_xlabel("Radial Position (m)") - axis.set_ylabel("Radial Stress (MPa)") - axis.minorticks_on() - axis.grid(True, alpha=0.3) - axis.set_title("CS Radial Stress at BOP") - - -def plot_cs_hoop_stress_profile( - axis: plt.Axes, - mfile: MFile, - scan: int, - j_cs: float, - b_cs_inner: float, -): - """Plot CS hoop stress profile""" - r_cs_inner = mfile.get("r_cs_inner", scan=scan) - r_cs_outer = mfile.get("r_cs_outer", scan=scan) - - radii = np.linspace(r_cs_inner, r_cs_outer, num=10) - stress_values = np.array([ - CSCoil.calculate_cs_hoop_stress( - r_stress_point=radius, - r_cs_inner=r_cs_inner, - r_cs_outer=r_cs_outer, - j_cs=j_cs, - b_cs_inner=b_cs_inner, - f_poisson_cs_structure=poisson_steel, - f_a_cs_turn_steel=mfile.get("f_a_cs_turn_steel", scan=scan), - ) - for radius in radii - ]) - - axis.plot( - radii, - stress_values / 1e6, - linewidth=2, - label="$\\sigma_{\\theta}$,Hoop Stress", - ) - max_idx = np.argmax(np.abs(stress_values)) - max_radius = radii[max_idx] - max_stress = stress_values[max_idx] / 1e6 - axis.axvline(max_radius, color="black", linestyle="--", linewidth=1.0, alpha=0.7) - axis.axhline(max_stress, color="black", linestyle="--", linewidth=1.0, alpha=0.7) - axis.set_xlabel("Radial Position (m)") - axis.set_ylabel("Hoop Stress (MPa)") - axis.minorticks_on() - axis.set_title("CS Hoop Stress at BOP") - axis.grid(True, alpha=0.3) - - -def plot_cs_radial_stress_contour_profile( - axis: plt.Axes, - mfile: MFile, - scan: int, - j_cs: float, - b_cs_inner: float, - colorbar_axis: plt.Axes | None = None, -): - """Plot CS radial stress contour profile""" - r_cs_inner = mfile.get("r_cs_inner", scan=scan) - r_cs_outer = mfile.get("r_cs_outer", scan=scan) - dz_cs_full = mfile.get("dz_cs_full", scan=scan) - - # Create 2D grid for contour plot: radial and vertical dimensions - n_radial = 50 - radial_grid = np.linspace(r_cs_inner, r_cs_outer, n_radial) - height_grid = np.linspace( - -dz_cs_full / 2, dz_cs_full / 2, N_CS_STRESS_PROFILE_POINTS - ) - - # Create meshgrid for filled contour - r, z = np.meshgrid(radial_grid, height_grid) - - # Calculate radial stress across the 2D grid - stress_data = np.zeros((len(height_grid), n_radial)) - for i in range(len(height_grid)): - for j in range(n_radial): - stress_data[i, j] = ( - CSCoil.calculate_cs_radial_stress( - r_stress_point=radial_grid[j], - r_cs_inner=r_cs_inner, - r_cs_outer=r_cs_outer, - j_cs=j_cs, - b_cs_inner=b_cs_inner, - f_poisson_cs_structure=poisson_steel, - ) - / 1e6 - ) - - # Plot filled contour of stress distribution - contour_fill = axis.contourf(r, z, stress_data, levels=15, cmap="RdYlBu") - contour_lines = axis.contour( - r, - z, - stress_data, - levels=[stress_data.max()], - colors="black", - linewidths=0.5, - alpha=0.4, - ) - axis.clabel(contour_lines, inline=True, fontsize=8) - - # Plot CS outline - axis.plot( - [r_cs_inner, r_cs_inner], - [-dz_cs_full / 2, dz_cs_full / 2], - "k-", - linewidth=2, - label="CS Inner", - ) - axis.plot( - [r_cs_outer, r_cs_outer], - [-dz_cs_full / 2, dz_cs_full / 2], - "k-", - linewidth=2, - label="CS Outer", - ) - axis.plot( - [r_cs_inner, r_cs_outer], [dz_cs_full / 2, dz_cs_full / 2], "k-", linewidth=2 - ) - axis.plot( - [r_cs_inner, r_cs_outer], - [-dz_cs_full / 2, -dz_cs_full / 2], - "k-", - linewidth=2, - ) - - cbar = _colourbar(contour_fill, axis, colorbar_axis) - cbar.set_label("Radial Stress (MPa)") - - axis.set_xlabel("R [m]") - axis.set_ylabel("Z [m]") - axis.minorticks_on() - axis.set_xlim(r_cs_inner * 0.9, r_cs_outer * 1.1) - axis.set_ylim((-dz_cs_full / 2) * 1.1, (dz_cs_full / 2) * 1.1) - axis.grid(True, alpha=0.3) - - -def plot_cs_hoop_stress_contour_profile( - axis: plt.Axes, - mfile: MFile, - scan: int, - j_cs: float, - b_cs_inner: float, - colorbar_axis: plt.Axes | None = None, -): - """Plot CS hoop stress contour profile""" - r_cs_inner = mfile.get("r_cs_inner", scan=scan) - r_cs_outer = mfile.get("r_cs_outer", scan=scan) - dz_cs_full = mfile.get("dz_cs_full", scan=scan) - f_a_cs_turn_steel = mfile.get("f_a_cs_turn_steel", scan=scan) - - # Create 2D grid for contour plot: radial and vertical dimensions - n_radial = 50 - radial_grid = np.linspace(r_cs_inner, r_cs_outer, n_radial) - height_grid = np.linspace( - -dz_cs_full / 2, dz_cs_full / 2, N_CS_STRESS_PROFILE_POINTS - ) - - # Create meshgrid for filled contour - r, z = np.meshgrid(radial_grid, height_grid) - - # Calculate hoop stress across the 2D grid - stress_data = np.zeros((len(height_grid), n_radial)) - for i in range(len(height_grid)): - for j in range(n_radial): - stress_data[i, j] = ( - CSCoil.calculate_cs_hoop_stress( - r_stress_point=radial_grid[j], - r_cs_inner=r_cs_inner, - r_cs_outer=r_cs_outer, - j_cs=j_cs, - b_cs_inner=b_cs_inner, - f_poisson_cs_structure=poisson_steel, - f_a_cs_turn_steel=f_a_cs_turn_steel, - ) - / 1e6 - ) - - # Plot filled contour of stress distribution - contour_fill = axis.contourf(r, z, stress_data, levels=15, cmap="RdYlBu_r") - contour_lines = axis.contour( - r, - z, - stress_data, - levels=[stress_data.max()], - colors="black", - linewidths=0.5, - alpha=0.4, - ) - axis.clabel(contour_lines, inline=True, fontsize=8) - - # Plot CS outline - axis.plot( - [r_cs_inner, r_cs_inner], - [-dz_cs_full / 2, dz_cs_full / 2], - "k-", - linewidth=2, - label="CS Inner", - ) - axis.plot( - [r_cs_outer, r_cs_outer], - [-dz_cs_full / 2, dz_cs_full / 2], - "k-", - linewidth=2, - label="CS Outer", - ) - axis.plot( - [r_cs_inner, r_cs_outer], [dz_cs_full / 2, dz_cs_full / 2], "k-", linewidth=2 - ) - axis.plot( - [r_cs_inner, r_cs_outer], - [-dz_cs_full / 2, -dz_cs_full / 2], - "k-", - linewidth=2, - ) - - cbar = _colourbar(contour_fill, axis, colorbar_axis) - cbar.set_label("Hoop Stress (MPa)") - - axis.set_xlabel("R [m]") - axis.set_ylabel("Z [m]") - axis.minorticks_on() - axis.set_xlim(r_cs_inner * 0.9, r_cs_outer * 1.1) - axis.set_ylim((-dz_cs_full / 2) * 1.1, (dz_cs_full / 2) * 1.1) - axis.grid(True, alpha=0.3) - - -def plot_cs_vertical_stress_profile( - axis: plt.Axes, - mfile: MFile, - scan: int, -): - """Plot CS vertical stress profile""" - dz_cs_full = mfile.get("dz_cs_full", scan=scan) - - stress_z_profile = np.array([ - float(mfile.data[f"stress_z_cs_self_profile_{i}"].get_scan(scan)) / 1e6 - for i in range(N_CS_STRESS_PROFILE_POINTS) - ]) - z_positions = np.linspace(-dz_cs_full / 2, dz_cs_full / 2, len(stress_z_profile)) - - axis.plot( - stress_z_profile, - z_positions, - linewidth=2, - label="$\\sigma_{z}$,Vertical Stress", - ) - max_idx = np.argmax(np.abs(stress_z_profile)) - max_stress = stress_z_profile[max_idx] - max_z = z_positions[max_idx] - axis.axvline(max_stress, color="black", linestyle="--", linewidth=1.0, alpha=0.7) - axis.axhline(max_z, color="black", linestyle="--", linewidth=1.0, alpha=0.7) - axis.set_xlabel("Vertical Stress (MPa)") - axis.set_ylabel("Z [m]") - axis.minorticks_on() - axis.grid(True, alpha=0.3) - axis.set_title("CS Vertical Stress at BOP") - - -def plot_vertical_stress_contour_profile( - axis: plt.Axes, - mfile: MFile, - scan: int, - colorbar_axis: plt.Axes | None = None, -): - """Vertical stress contour plot""" - dz_cs_full = mfile.get("dz_cs_full", scan=scan) - r_cs_inner = mfile.get("r_cs_inner", scan=scan) - r_cs_outer = mfile.get("r_cs_outer", scan=scan) - - stress_z_profile = [ - float(mfile.data[f"stress_z_cs_self_profile_{i}"].get_scan(scan)) / 1e6 - for i in range(N_CS_STRESS_PROFILE_POINTS) - ] - - # Create 2D grid for contour plot: radial and vertical dimensions - n_radial = 50 - radial_grid = np.linspace(r_cs_inner, r_cs_outer, n_radial) - height_grid = np.linspace(-dz_cs_full / 2, dz_cs_full / 2, len(stress_z_profile)) - - # Create meshgrid for filled contour - r, z = np.meshgrid(radial_grid, height_grid) - - # Interpolate stress values across radial direction (assume linear variation) - stress_data = np.zeros((len(stress_z_profile), n_radial)) - for i, stress_val in enumerate(stress_z_profile): - stress_data[i, :] = stress_val - - # Plot filled contour of stress distribution - contour_fill = axis.contourf(r, z, stress_data, levels=15, cmap="RdYlBu") - contour_lines = axis.contour( - r, - z, - stress_data, - levels=[stress_data.max()], - colors="black", - linewidths=0.5, - alpha=0.4, - ) - axis.clabel(contour_lines, inline=True, fontsize=8) - - # Plot CS outline - axis.plot( - [r_cs_inner, r_cs_inner], - [-dz_cs_full / 2, dz_cs_full / 2], - "k-", - linewidth=2, - label="CS Inner", - ) - axis.plot( - [r_cs_outer, r_cs_outer], - [-dz_cs_full / 2, dz_cs_full / 2], - "k-", - linewidth=2, - label="CS Outer", - ) - axis.plot( - [r_cs_inner, r_cs_outer], [dz_cs_full / 2, dz_cs_full / 2], "k-", linewidth=2 - ) - axis.plot( - [r_cs_inner, r_cs_outer], - [-dz_cs_full / 2, -dz_cs_full / 2], - "k-", - linewidth=2, - ) - - cbar = _colourbar(contour_fill, axis, colorbar_axis) - cbar.set_label("Vertical Stress (MPa)") - - axis.set_xlabel("R [m]") - axis.set_ylabel("Z [m]") - axis.minorticks_on() - axis.set_xlim(r_cs_inner * 0.9, r_cs_outer * 1.1) - axis.set_ylim((-dz_cs_full / 2) * 1.1, (dz_cs_full / 2) * 1.1) - axis.grid(True, alpha=0.3) - - -def plot_cs_tresca_2d_contour( - axis: plt.Axes, - mfile: MFile, - scan: int, - colorbar_axis: plt.Axes | None = None, -): - """CS Tresca stress contour plot""" - dz_cs_full = mfile.get("dz_cs_full", scan=scan) - r_cs_inner = mfile.get("r_cs_inner", scan=scan) - r_cs_outer = mfile.get("r_cs_outer", scan=scan) - j_cs = mfile.get("j_cs_pulse_start", scan=scan) - b_cs_inner = mfile.get("b_cs_peak_pulse_start", scan=scan) - f_a_cs_turn_steel = mfile.get("f_a_cs_turn_steel", scan=scan) - - stress_z_profile = np.array([ - float(mfile.data[f"stress_z_cs_self_profile_{i}"].get_scan(scan)) - for i in range(N_CS_STRESS_PROFILE_POINTS) - ]) - - # Create 2D grid for contour plot: radial and vertical dimensions - n_radial = 50 - radial_grid = np.linspace(r_cs_inner, r_cs_outer, n_radial) - height_grid = np.linspace(-dz_cs_full / 2, dz_cs_full / 2, len(stress_z_profile)) - - # Create meshgrid for filled contour - r, z = np.meshgrid(radial_grid, height_grid) - - # Calculate Tresca stress across the coil cross-section. - tresca_data = np.zeros((len(height_grid), n_radial)) - for i, stress_z in enumerate(stress_z_profile): - for j, radius in enumerate(radial_grid): - stress_hoop = CSCoil.calculate_cs_hoop_stress( - r_stress_point=radius, - r_cs_inner=r_cs_inner, - r_cs_outer=r_cs_outer, - j_cs=j_cs, - b_cs_inner=b_cs_inner, - f_poisson_cs_structure=poisson_steel, - f_a_cs_turn_steel=f_a_cs_turn_steel, - ) - stress_radial = CSCoil.calculate_cs_radial_stress( - r_stress_point=radius, - r_cs_inner=r_cs_inner, - r_cs_outer=r_cs_outer, - j_cs=j_cs, - b_cs_inner=b_cs_inner, - f_poisson_cs_structure=poisson_steel, - ) - tresca_data[i, j] = ( - calculate_tresca_stress( - stress_x=stress_hoop, - stress_y=stress_z, - stress_z=stress_radial, - ) - / 1e6 - ) - - # Plot filled contour of Tresca stress distribution - contour_lines = axis.contour( - r, - z, - tresca_data, - levels=[tresca_data.max()], - colors="black", - linewidths=0.5, - alpha=0.4, - ) - axis.clabel(contour_lines, inline=True, fontsize=8) - - # Plot CS outline - axis.plot( - [r_cs_inner, r_cs_inner], - [-dz_cs_full / 2, dz_cs_full / 2], - "k-", - linewidth=2, - label="CS Inner", - ) - axis.plot( - [r_cs_outer, r_cs_outer], - [-dz_cs_full / 2, dz_cs_full / 2], - "k-", - linewidth=2, - label="CS Outer", - ) - axis.plot( - [r_cs_inner, r_cs_outer], [dz_cs_full / 2, dz_cs_full / 2], "k-", linewidth=2 - ) - axis.plot( - [r_cs_inner, r_cs_outer], - [-dz_cs_full / 2, -dz_cs_full / 2], - "k-", - linewidth=2, - ) - - contour_fill = axis.contourf(r, z, tresca_data, levels=15, cmap="RdYlBu_r") - cbar = _colourbar(contour_fill, axis, colorbar_axis) - cbar.set_label("Tresca Stress (MPa)") - - axis.set_xlabel("R [m]") - axis.set_ylabel("Z [m]") - axis.minorticks_on() - axis.set_xlim(r_cs_inner * 0.9, r_cs_outer * 1.1) - axis.set_ylim((-dz_cs_full / 2) * 1.1, (dz_cs_full / 2) * 1.1) - axis.grid(True, alpha=0.3) - axis.set_title("CS Tresca Stress Contour at BOP") - - -def plot_cs_von_mises_2d_contour( - axis: plt.Axes, - mfile: MFile, - scan: int, - colorbar_axis: plt.Axes | None = None, -): - """CS Von Mises stress contour plot""" - dz_cs_full = mfile.get("dz_cs_full", scan=scan) - r_cs_inner = mfile.get("r_cs_inner", scan=scan) - r_cs_outer = mfile.get("r_cs_outer", scan=scan) - j_cs = mfile.get("j_cs_pulse_start", scan=scan) - b_cs_inner = mfile.get("b_cs_peak_pulse_start", scan=scan) - f_a_cs_turn_steel = mfile.get("f_a_cs_turn_steel", scan=scan) - - stress_z_profile = np.array([ - float(mfile.data[f"stress_z_cs_self_profile_{i}"].get_scan(scan)) - for i in range(N_CS_STRESS_PROFILE_POINTS) - ]) - - # Create 2D grid for contour plot: radial and vertical dimensions - n_radial = 50 - radial_grid = np.linspace(r_cs_inner, r_cs_outer, n_radial) - height_grid = np.linspace(-dz_cs_full / 2, dz_cs_full / 2, len(stress_z_profile)) - - # Create meshgrid for filled contour - r, z = np.meshgrid(radial_grid, height_grid) - - # Calculate Von Mises stress across the coil cross-section. - von_mises_data = np.zeros((len(height_grid), n_radial)) - for i, stress_z in enumerate(stress_z_profile): - for j, radius in enumerate(radial_grid): - stress_hoop = CSCoil.calculate_cs_hoop_stress( - r_stress_point=radius, - r_cs_inner=r_cs_inner, - r_cs_outer=r_cs_outer, - j_cs=j_cs, - b_cs_inner=b_cs_inner, - f_poisson_cs_structure=poisson_steel, - f_a_cs_turn_steel=f_a_cs_turn_steel, - ) - stress_radial = CSCoil.calculate_cs_radial_stress( - r_stress_point=radius, - r_cs_inner=r_cs_inner, - r_cs_outer=r_cs_outer, - j_cs=j_cs, - b_cs_inner=b_cs_inner, - f_poisson_cs_structure=poisson_steel, - ) - von_mises_data[i, j] = ( - calculate_von_mises_stress( - stress_x=stress_hoop, - stress_y=stress_z, - stress_z=stress_radial, - stress_shear_xy=0.0, - stress_shear_yz=0.0, - stress_shear_zx=0.0, - ) - / 1e6 - ) - - # Plot filled contour of Von Mises stress distribution - contour_lines = axis.contour( - r, - z, - von_mises_data, - levels=[von_mises_data.max()], - colors="black", - linewidths=0.5, - alpha=0.4, - ) - axis.clabel(contour_lines, inline=True, fontsize=8) - - # Plot CS outline - axis.plot( - [r_cs_inner, r_cs_inner], - [-dz_cs_full / 2, dz_cs_full / 2], - "k-", - linewidth=2, - label="CS Inner", - ) - axis.plot( - [r_cs_outer, r_cs_outer], - [-dz_cs_full / 2, dz_cs_full / 2], - "k-", - linewidth=2, - label="CS Outer", - ) - axis.plot( - [r_cs_inner, r_cs_outer], [dz_cs_full / 2, dz_cs_full / 2], "k-", linewidth=2 - ) - axis.plot( - [r_cs_inner, r_cs_outer], - [-dz_cs_full / 2, -dz_cs_full / 2], - "k-", - linewidth=2, - ) - - contour_fill = axis.contourf(r, z, von_mises_data, levels=15, cmap="RdYlBu_r") - cbar = _colourbar(contour_fill, axis, colorbar_axis) - cbar.set_label("Von Mises Stress (MPa)") - - axis.set_xlabel("R [m]") - axis.minorticks_on() - axis.set_xlim(r_cs_inner * 0.9, r_cs_outer * 1.1) - axis.set_ylim((-dz_cs_full / 2) * 1.1, (dz_cs_full / 2) * 1.1) - axis.grid(True, alpha=0.3) - axis.set_title("CS Von Mises Stress Contour at BOP") - - -def plot_pf_dimensions( - axis: plt.Axes, mfile: MFile, scan: int, colour_scheme: Literal[1, 2] = 1 -) -> None: - """Plot the PF coil dimensions on the given axis.""" - r_pf_coil_middle = [] - z_pf_coil_middle = [] - radial_thicknesses = [] - vertical_thicknesses = [] - iohcl = mfile.get("iohcl", scan=scan) if "iohcl" in mfile.data else 1 - x = 1 if iohcl == 0 else 2 - for coil in range(int(mfile.get("n_pf_cs_plasma_circuits", scan=scan) - x)): - r_pf_coil_middle.append(mfile.get(f"r_pf_coil_middle[{coil + 1}]", scan=scan)) - z_pf_coil_middle.append(mfile.get(f"z_pf_coil_middle[{coil + 1}]", scan=scan)) - radial_thicknesses.append(mfile.get(f"pfdr({coil + 1})", scan=scan)) - vertical_thicknesses.append(mfile.get(f"pfdz({coil + 1})", scan=scan)) - - plot_pf_coils(axis=axis, mfile=mfile, scan=scan, colour_scheme=colour_scheme) - - if r_pf_coil_middle: - for r_middle, z_middle, dr_coil, dz_coil in zip( - r_pf_coil_middle, - z_pf_coil_middle, - radial_thicknesses, - vertical_thicknesses, - strict=False, - ): - half_radial_thickness = dr_coil / 2 - half_vertical_thickness = dz_coil / 2 - coil_left = r_middle - half_radial_thickness - coil_right = r_middle + half_radial_thickness - coil_bottom = z_middle - half_vertical_thickness - coil_top = z_middle + half_vertical_thickness - - for x_position in (coil_left, r_middle, coil_right): - axis.axvline( - x=x_position, - color="r", - linewidth=0.8, - linestyle="--" if x_position == r_middle else "-", - alpha=0.3, - zorder=4, - ) - - for y_position in (coil_bottom, z_middle, coil_top): - axis.axhline( - y=y_position, - xmax=coil_left, - color="r", - linewidth=0.8, - linestyle="--" if y_position == z_middle else "-", - alpha=0.3, - zorder=4, - ) - - axis.annotate( - f"({r_middle:.3f}, {z_middle:.3f})", - xy=(coil_left * 0.925, z_middle), - ha="right", - va="center", - fontsize=8, - zorder=6, - bbox={ - "boxstyle": "round,pad=0.2", - "fc": "white", - "alpha": 1.0, - "ec": "none", - }, - ) - - radial_arrow_y = coil_bottom if z_middle < 0 else coil_top - radial_label_offset = (0, -24) if z_middle < 0 else (0, 4) - radial_label_va = "top" if z_middle < 0 else "bottom" - axis.annotate( - "", - xy=(coil_left, radial_arrow_y), - xytext=(coil_right, radial_arrow_y), - arrowprops={ - "arrowstyle": "<->", - "linewidth": 0.8, - "color": "red", - "shrinkA": 0, - "shrinkB": 0, - }, - zorder=5, - ) - axis.annotate( - f"ΔR={abs(dr_coil):.3f}", - xy=(r_middle, radial_arrow_y), - xytext=radial_label_offset, - textcoords="offset points", - ha="center", - va=radial_label_va, - fontsize=8, - zorder=6, - bbox={ - "boxstyle": "round,pad=0.2", - "fc": "white", - "alpha": 1.0, - "ec": "none", - }, - ) - - vertical_arrow_x = coil_right - axis.annotate( - "", - xy=(vertical_arrow_x, coil_bottom), - xytext=(vertical_arrow_x, coil_top), - arrowprops={ - "arrowstyle": "<->", - "linewidth": 0.8, - "color": "red", - "shrinkA": 0, - "shrinkB": 0, - }, - ) - axis.annotate( - f"ΔZ={abs(dz_coil):.3f}", - xy=(vertical_arrow_x, z_middle), - xytext=(4, 0), - textcoords="offset points", - ha="left", - va="center", - fontsize=8, - zorder=6, - bbox={ - "boxstyle": "round,pad=0.2", - "fc": "white", - "alpha": 1.0, - "ec": "none", - }, - ) - - axis.set_title("PF Coil Dimensions") - axis.set_xlabel("R [m]") - axis.set_ylabel("Z [m]") - axis.set_xlim(left=0.0) - axis.minorticks_on() - axis.grid(True, alpha=0.3) - axis.set_aspect("equal", adjustable="box") - - -def plot_plasma_thermal_energy_profiles(axis, m_file: MFile, scan: int): - """Function to plot plasma thermal energy profiles on the given axis. - - Parameters - ---------- - axis : - Matplotlib axis to plot on - m_file : - MFILE - scan : - scan to read from MFILE - """ - n_plasma_profile_elements = int(m_file.get("n_plasma_profile_elements", scan=scan)) - - eden_plasma_electrons_thermal_profile_mj = [ - m_file.get(f"eden_plasma_electrons_thermal_profile{i}", scan=scan) / 1e6 - for i in range(n_plasma_profile_elements) - ] - eden_plasma_ions_thermal_profile_mj = [ - m_file.get(f"eden_plasma_ions_thermal_profile{i}", scan=scan) / 1e6 - for i in range(n_plasma_profile_elements) - ] - eden_plasma_thermal_profile_mj = [ - m_file.get(f"eden_plasma_thermal_profile{i}", scan=scan) / 1e6 - for i in range(n_plasma_profile_elements) - ] - e_plasma_electrons_thermal_profile_mj = [ - m_file.get(f"e_plasma_electrons_thermal_profile{i}", scan=scan) / 1e6 - for i in range(n_plasma_profile_elements) - ] - - e_plasma_ions_thermal_profile_mj = [ - m_file.get(f"e_plasma_ions_thermal_profile{i}", scan=scan) / 1e6 - for i in range(n_plasma_profile_elements) - ] - e_plasma_thermal_profile_mj = [ - m_file.get(f"e_plasma_thermal_profile{i}", scan=scan) / 1e6 - for i in range(n_plasma_profile_elements) - ] - - axis.plot( - np.linspace(0, 1, n_plasma_profile_elements), - e_plasma_electrons_thermal_profile_mj, - label="$W_{\\text{e}}$", - color="tab:blue", - linestyle=":", - ) - axis.plot( - np.linspace(0, 1, n_plasma_profile_elements), - e_plasma_ions_thermal_profile_mj, - label="$W_{\\text{i}}$", - color="tab:blue", - linestyle="--", - ) - axis.plot( - np.linspace(0, 1, n_plasma_profile_elements), - e_plasma_thermal_profile_mj, - label="$W_{\\text{total}}$", - color="tab:blue", - linestyle="-", - ) - - total_thermal_energy_max_index = int(np.argmax(e_plasma_thermal_profile_mj)) - total_thermal_energy_max_rho = np.linspace(0, 1, n_plasma_profile_elements)[ - total_thermal_energy_max_index - ] - total_thermal_energy_max = e_plasma_thermal_profile_mj[ - total_thermal_energy_max_index - ] - axis.axvline( - total_thermal_energy_max_rho, - color="tab:red", - alpha=0.7, - label="$W_{\\text{total, peak}}$", - ) - axis.axhline( - total_thermal_energy_max, - color="tab:red", - alpha=0.7, - ) - - density_axis = axis.twinx() - density_axis.plot( - np.linspace(0, 1, n_plasma_profile_elements), - eden_plasma_electrons_thermal_profile_mj, - label="$W_{\\text{density, e}}$", - color="tab:orange", - linestyle=":", - ) - density_axis.plot( - np.linspace(0, 1, n_plasma_profile_elements), - eden_plasma_ions_thermal_profile_mj, - label="$W_{\\text{density, i}}$", - color="tab:orange", - linestyle="--", - ) - density_axis.plot( - np.linspace(0, 1, n_plasma_profile_elements), - eden_plasma_thermal_profile_mj, - label="$W_{\\text{density, total}}$", - color="tab:orange", - linestyle="-", - ) - - axis.grid(True, alpha=0.3) - axis.minorticks_on() - axis.set_xlabel(r"$\rho \quad [r/a]$") - axis.set_xlim(left=0.0, right=1.0) - axis.set_ylabel( - "Thermal Energy [MJ]", - color="tab:blue", - ) - axis.tick_params(axis="y", colors="tab:blue") - density_axis.set_ylabel( - "Thermal Energy Density [MJ/m$^3$]", - color="tab:orange", - ) - density_axis.tick_params(axis="y", colors="tab:orange") - handles, labels = axis.get_legend_handles_labels() - density_handles, density_labels = density_axis.get_legend_handles_labels() - axis.legend(handles + density_handles, labels + density_labels) - - -def plot_cumulative_plasma_thermal_energy_profiles(axis, m_file: MFile, scan: int): - """Function to plot the cumulative plasma thermal energy profiles on the given axis. - - Parameters - ---------- - axis : - Matplotlib axis to plot on - m_file : - MFILE - scan : - scan to read from MFILE - """ - n_plasma_profile_elements = int(m_file.get("n_plasma_profile_elements", scan=scan)) - e_plasma_thermal_total_mj = m_file.get("e_plasma_thermal_total", scan=scan) / 1e6 - e_plasma_electrons_thermal_profile_mj = [ - m_file.get(f"e_plasma_electrons_thermal_profile{i}", scan=scan) / 1e6 - for i in range(n_plasma_profile_elements) - ] - - e_plasma_ions_thermal_profile_mj = [ - m_file.get(f"e_plasma_ions_thermal_profile{i}", scan=scan) / 1e6 - for i in range(n_plasma_profile_elements) - ] - e_plasma_thermal_profile_mj = [ - m_file.get(f"e_plasma_thermal_profile{i}", scan=scan) / 1e6 - for i in range(n_plasma_profile_elements) - ] - - axis.plot( - np.linspace(0, 1, n_plasma_profile_elements), - np.cumsum(e_plasma_electrons_thermal_profile_mj), - label="$\\Sigma W_{\\text{e}}$", - color="tab:blue", - linestyle=":", - ) - axis.plot( - np.linspace(0, 1, n_plasma_profile_elements), - np.cumsum(e_plasma_ions_thermal_profile_mj), - label="$\\Sigma W_{\\text{i}}$", - color="tab:blue", - linestyle="--", - ) - axis.plot( - np.linspace(0, 1, n_plasma_profile_elements), - np.cumsum(e_plasma_thermal_profile_mj), - label="$\\Sigma W_{\\text{total}}$", - color="tab:blue", - linestyle="-", - ) - axis.axhline( - y=e_plasma_thermal_total_mj, - label="$W_{\\text{thermal,total}}$", - color="tab:red", - linestyle="--", - ) - cumulative_thermal_energy_mj = np.cumsum(e_plasma_thermal_profile_mj) - half_thermal_energy_mj = 0.5 * e_plasma_thermal_total_mj - half_thermal_energy_position = np.interp( - half_thermal_energy_mj, - cumulative_thermal_energy_mj, - np.linspace(0, 1, n_plasma_profile_elements), - ) - axis.axhline( - y=half_thermal_energy_mj, - label="$50\\%\\ W_{\\text{thermal,total}}$", - color="tab:green", - linestyle=":", - ) - axis.axvline( - x=half_thermal_energy_position, - color="tab:green", - linestyle=":", - ) - - axis.legend() - axis.set_title("Thermal Energy Profiles and Cumulative Distribution") - axis.grid(True, alpha=0.3) - axis.minorticks_on() - axis.tick_params(axis="x", labelbottom=False) - axis.set_xlim(left=0.0, right=1.0) - axis.set_ylabel( - "Cumulative Thermal Energy [MJ]", - ) - - -def main_plot( - m_file: MFile, - scan: int, - imp: str = "../data/lz_non_corona_14_elements/", - demo_ranges: bool = False, - colour_scheme: Literal[1, 2] = 1, -) -> list[plt.Figure]: - """Function to create radial and vertical build plot on given figure. - - Parameters - ---------- - m_file : - MFILE - scan : - scan to read from MFILE - imp : - path to impurity data - demo_ranges: bool : - (Default value = False) - colour_scheme: - - """ - # Checking the impurity data folder - # Get path to impurity data dir - # TODO use Path objects throughout module, not strings - - with resources.path( - "process.data.lz_non_corona_14_elements", "Ar_lz_tau.dat" - ) as imp_path: - imp = str(imp_path.parent) + "/" - - i_shape = int(m_file.get("i_plasma_shape", scan=scan)) - # Setup params for text plots - plt.rcParams.update({"font.size": 8}) - - pages = {} - - def _add_page(name: str | None = None): - """Add a page to the dictionary of pages. If no name is provided, then assign the lowest unused number. - - Raises - ------ - KeyError - If a page number has already been used - """ - if name is None: - prev_index = max((int(k) for k in pages if k.isnumeric()), default=0) - name = str(prev_index + 1) - if name in pages: - raise KeyError(f"Name collision: {name} already in `pages`!") - pages[name] = plt.figure(figsize=(12, 9), dpi=80) - return pages[name] - - radial_build = create_thickness_builds(m_file, scan) - - plot_cover_page( - _add_page("cover").add_subplot(111), - m_file, - scan, - pages["cover"], - radial_build, - colour_scheme, - ) - - # Plot header info - plot_header(_add_page("first").add_subplot(231), m_file, scan) - - # Geometry - plot_geometry_info(pages["first"].add_subplot(232), m_file, scan) - - # Physics - plot_physics_info(pages["first"].add_subplot(233), m_file, scan) - - # Magnetics - plot_magnetics_info(pages["first"].add_subplot(234), m_file, scan) - - # power/flow economics - plot_power_info(pages["first"].add_subplot(235), m_file, scan) - - # Current drive - # plot_current_drive_info(pages["first"].add_subplot(236), m_file_data, scan) - pages["first"].subplots_adjust(wspace=0.25, hspace=0.25) - - ax7 = _add_page().add_subplot(111) - ax7.set_position([0.25, 0.1, 0.7, 0.8]) # Move plot slightly to the right - plot_iteration_variables(ax7, m_file, scan) - - ax7_5 = _add_page().add_subplot(313) - ax7_5.set_position([0.25, 0.1, 0.7, 0.8]) - plot_equality_constraint_equations(ax7_5, m_file, scan) - ax7_6 = _add_page().add_subplot(111) - ax7_6.set_position([0.3, 0.1, 0.65, 0.8]) - plot_inequality_constraint_equations(ax7_6, m_file, scan) - - # Plot main plasma information - plot_main_plasma_information( - _add_page("plasma_info").add_subplot(111, aspect="equal"), - m_file, - scan, - colour_scheme, - pages["plasma_info"], - ) - - # Plot density profiles - plot_n_profiles(_add_page("profiles"), demo_ranges, m_file, scan) - - # Plot temperature profiles - ax10 = pages["profiles"].add_subplot(232) - ax10.set_position([0.375, 0.575, 0.25, 0.375]) - plot_t_profiles(ax10, demo_ranges, m_file, scan) - - # Plot impurity profiles - ax11 = pages["profiles"].add_subplot(233) - ax11.set_position([0.7, 0.45, 0.25, 0.5]) - - plot_line_brem_power_density_profile( - axis=ax11, mfile=m_file, scan=scan, impp=imp, demo_ranges=demo_ranges - ) - - # Plot current density profile - ax12 = pages["profiles"].add_subplot(4, 3, 10) - ax12.set_position([0.375, 0.105, 0.25, 0.15]) - plot_jprofile(ax12, m_file, scan) - - # Plot q profile - ax13 = pages["profiles"].add_subplot(4, 3, 12) - ax13.set_position([0.7, 0.105, 0.25, 0.15]) - plot_qprofile(ax13, demo_ranges, m_file, scan) - - ax_line_brem = _add_page("rad_contour").add_subplot(325) - plot_line_brem_loss_function_profile( - axis=ax_line_brem, - mfile=m_file, - scan=scan, - impp=imp, - ) - - ax_zeff = pages["rad_contour"].add_subplot(321, sharex=ax_line_brem) - plot_plasma_effective_charge_profile(ax_zeff, m_file, scan) - ax_zeff.set_xlabel("") - ax_zeff.tick_params( - axis="x", which="both", bottom=True, top=False, labelbottom=False - ) - - ax_ion_charge = pages["rad_contour"].add_subplot(323, sharex=ax_line_brem) - plot_ion_charge_profile(ax_ion_charge, m_file, scan) - ax_ion_charge.set_xlabel("") - ax_ion_charge.tick_params( - axis="x", which="both", bottom=True, top=False, labelbottom=False - ) - - if i_shape == 1: - plot_rad_density_contour( - pages["rad_contour"].add_subplot(122, aspect="equal"), m_file, scan, imp - ) - - if i_shape != 1: - msg = ( - "Radiation contour plots require a closed (Sauter) plasma boundary " - "(i_plasma_shape == 1). " - f"Current i_plasma_shape = {i_shape}. Contour plots are skipped; " - "see the 1D radiation plots for available information." - ) - # Add explanatory text to both figures reserved for contour outputs - pages["rad_contour"].text( - 0.75, 0.5, msg, ha="center", va="center", wrap=True, fontsize=12 - ) - - plot_line_brem_power_profile( - _add_page("line_brem_power").add_subplot(121), m_file, scan, imp - ) - - plot_fusion_rate_profiles( - _add_page("fusion_rate").add_subplot(122), pages["fusion_rate"], m_file, scan - ) - - _add_page("rx_1_2"), _add_page("rx_3_4") - if m_file.get("i_plasma_shape", scan=scan) == PlasmaShapeModelType.SAUTER: - plot_fusion_rate_contours(pages["rx_1_2"], pages["rx_3_4"], m_file, scan) - - if i_shape != PlasmaShapeModelType.SAUTER: - msg = ( - "Fusion-rate contour plots require a closed (Sauter) plasma boundary " - "(i_plasma_shape == 1). " - f"Current i_plasma_shape = {i_shape}. Contour plots are skipped; " - "see the 1D fusion rate/profile plots for available information." - ) - # Add explanatory text to both figures reserved for contour outputs - pages["rx_1_2"].text( - 0.5, 0.5, msg, ha="center", va="center", wrap=True, fontsize=12 - ) - pages["rx_3_4"].text( - 0.5, 0.5, msg, ha="center", va="center", wrap=True, fontsize=12 - ) - - plot_plasma_pressure_profiles( - _add_page("pressure_profile").add_subplot(222), m_file, scan - ) - plot_plasma_pressure_gradient_profiles( - pages["pressure_profile"].add_subplot(224), m_file, scan - ) - # Currently only works with Sauter geometry as plasma has a closed surface - - if i_shape == PlasmaShapeModelType.SAUTER: - plot_plasma_poloidal_pressure_contours( - pages["pressure_profile"].add_subplot(121, aspect="equal"), - m_file, - scan, - ) - else: - ax = pages["pressure_profile"].add_subplot(131, aspect="equal") - msg = ( - "Plasma poloidal pressure contours require a closed (Sauter) plasma boundary " - f"(i_plasma_shape == {PlasmaShapeModelType.SAUTER}). " - f"Current i_plasma_shape = {i_shape}. Contour plots are skipped; " - "see the 1D pressure/profile plots for available information." - ) - ax.text( - 0.5, - 0.5, - msg, - ha="center", - va="center", - wrap=True, - fontsize=10, - transform=ax.transAxes, - ) - ax.axis("off") - - plot_magnetic_fields_in_plasma( - _add_page("beta").add_subplot(122, aspect="equal"), m_file, scan - ) - plot_beta_profiles(pages["beta"].add_subplot(321), m_file, scan) - - ax_thermal_energy = pages["beta"].add_subplot(325) - plot_plasma_thermal_energy_profiles(ax_thermal_energy, m_file, scan) - ax_thermal_energy_cumulative = pages["beta"].add_subplot( - 323, sharex=ax_thermal_energy - ) - ax_thermal_energy_cumulative.set_position([0.127, 0.35, 0.35, 0.2]) - plot_cumulative_plasma_thermal_energy_profiles( - ax_thermal_energy_cumulative, m_file, scan - ) - - plot_ebw_ecrh_coupling_graph(_add_page().add_subplot(111), m_file, scan) - - plot_bootstrap_comparison(_add_page("current").add_subplot(221), m_file, scan) - plot_plasma_current_comparison(pages["current"].add_subplot(224), m_file, scan) - plot_h_threshold_comparison( - _add_page("plasma_compare_1").add_subplot(224), m_file, scan - ) - plot_density_limit_comparison( - pages["plasma_compare_1"].add_subplot(221), m_file, scan - ) - - plot_max_normalised_beta_comparison( - _add_page("plasma_compare_2").add_subplot(221), m_file, scan - ) - plot_confinement_time_comparison( - pages["plasma_compare_2"].add_subplot(224), m_file, scan - ) - - plot_sol_power_decay_length_comparison( - _add_page("plasma_compare_3").add_subplot(221), m_file, scan - ) - - plot_brunner_divertor_power_split_comparison_stackplot( - _add_page("plasma_exhaust").add_subplot(121), m_file, scan - ) - - plot_separatrix_power_split( - pages["plasma_exhaust"].add_subplot(122), m_file, scan, colour_scheme - ) - - plot_debye_length_profile( - _add_page("microscopic_quantities").add_subplot(232), m_file, scan - ) - plot_velocity_profile(pages["microscopic_quantities"].add_subplot(233), m_file, scan) - plot_plasma_coloumb_logarithms( - pages["microscopic_quantities"].add_subplot(231), m_file, scan - ) - plot_collision_time_profile( - pages["microscopic_quantities"].add_subplot(234), m_file, scan - ) - plot_collision_frequency_profile( - pages["microscopic_quantities"].add_subplot(235), m_file, scan - ) - plot_mean_free_path_profile( - pages["microscopic_quantities"].add_subplot(236), m_file, scan - ) - - plot_ion_slowing_down_time_profile( - _add_page("detailed_params").add_subplot(231), m_file, scan - ) - - plot_resistivity_profile(pages["detailed_params"].add_subplot(232), m_file, scan) - - plot_detailed_plasma_parameters( - pages["detailed_params"].add_subplot(233), - fig=pages["detailed_params"], - mfile=m_file, - scan=scan, - ) - - ax_electron_freq = _add_page("freq").add_subplot(211) - plot_electron_frequency_profile(ax_electron_freq, m_file, scan) - - ax_ion_freq = pages["freq"].add_subplot(413, sharex=ax_electron_freq) - plot_ion_frequency_profile(ax_ion_freq, m_file, scan) - - ax_larmor = pages["freq"].add_subplot(414, sharex=ax_electron_freq) - plot_larmor_radius_profile(ax_larmor, m_file, scan) - - pages["freq"].subplots_adjust(hspace=0.5) - - # Plot poloidal cross-section - poloidal_cross_section( - _add_page("tokamak_cross_section").add_subplot(121, aspect="equal"), - m_file, - scan, - demo_ranges, - radial_build, - colour_scheme, - ) - - # Plot toroidal cross-section - toroidal_cross_section( - pages["tokamak_cross_section"].add_subplot(122, aspect="equal"), - m_file, - scan, - demo_ranges, - colour_scheme, - ) - - # Plot color key - ax17 = pages["tokamak_cross_section"].add_subplot(222) - ax17.set_position([0.5, 0.5, 0.5, 0.5]) - color_key(ax17, m_file, scan, colour_scheme) - - plot_full_machine_poloidal_cross_section( - _add_page().add_subplot(111, aspect="equal"), - m_file, - scan, - radial_build, - colour_scheme, - ) - - ax_full_toroidal = _add_page("full_machine_toroidal").add_subplot( - 111, aspect="equal" - ) - toroidal_cross_section( - ax_full_toroidal, - m_file, - scan, - demo_ranges, - colour_scheme, - ) - ax_full_toroidal.set_ylim( - -ax_full_toroidal.get_ylim()[1], - ax_full_toroidal.get_ylim()[1], - ) - ax_full_toroidal.set_xlim( - -ax_full_toroidal.get_xlim()[1], - ax_full_toroidal.get_xlim()[1], - ) - - ax18 = _add_page().add_subplot(211) - ax18.set_position([0.1, 0.33, 0.8, 0.6]) - plot_radial_build(ax18, m_file, colour_scheme) - - # Make each axes smaller vertically to leave room for the legend - ax185 = _add_page("vertical_build").add_subplot(211) - ax185.set_position([0.1, 0.61, 0.8, 0.32]) - - ax18b = pages["vertical_build"].add_subplot(212) - ax18b.set_position([0.1, 0.13, 0.8, 0.32]) - plot_upper_vertical_build(ax185, m_file, colour_scheme) - plot_lower_vertical_build(ax18b, m_file, colour_scheme) - - # Can only plot WP and turn structure if superconducting coil at the moment - if m_file.get("i_tf_sup", scan=scan) == TFConductorModel.SUPERCONDUCTING: - # TF coil with WP - ax19 = _add_page("tf_wp").add_subplot(221, aspect="equal") - ax19.set_position([ - 0.025, - 0.45, - 0.5, - 0.5, - ]) # Half height, a bit wider, top left - plot_superconducting_tf_wp(ax19, m_file, scan, pages["tf_wp"]) - - _add_page("cable") - if ( - m_file.get("i_tf_turn_type", scan=scan) - == SuperconductingTFTurnType.CROSS_CONDUCTOR - ): - ax20 = pages["cable"].add_subplot(325, aspect="equal") - ax20.set_position([0.025, 0.5, 0.4, 0.4]) - plot_tf_croco_turn(ax20, pages["cable"], m_file, scan) - elif ( - m_file.get("i_tf_turn_type", scan=scan) - == SuperconductingTFTurnType.CABLE_IN_CONDUIT - ): - # TF coil turn structure - ax20 = pages["cable"].add_subplot(325, aspect="equal") - ax20.set_position([0.025, 0.5, 0.4, 0.4]) - plot_tf_cable_in_conduit_turn(ax20, pages["cable"], m_file, scan) - - if ( - m_file.get("i_tf_turn_type", scan=scan) - == SuperconductingTFTurnType.CROSS_CONDUCTOR - ): - plot_205 = pages["cable"].add_subplot(223, aspect="equal") - plot_205.set_position([0.075, 0.1, 0.3, 0.3]) - plot_corc_cable_geometry( - plot_205, - r_centre=0.0, - z_centre=0.0, - dia_croco_strand=m_file.get("dia_tf_turn_croco_cable", scan=scan), - dx_croco_strand_copper=m_file.get( - "dx_tf_croco_strand_copper", scan=scan - ), - dr_hts_tape=m_file.get("dr_tf_hts_tape", scan=scan), - dx_croco_strand_tape_stack=m_file.get( - "dx_tf_croco_strand_tape_stack", scan=scan - ), - n_croco_strand_hts_tapes=m_file.get( - "n_tf_croco_strand_hts_tapes", scan=scan - ), - dx_hts_tape_rebco=m_file.get("dx_tf_hts_tape_rebco", scan=scan), - dx_hts_tape_copper=m_file.get("dx_tf_hts_tape_copper", scan=scan), - dx_hts_tape_hastelloy=m_file.get("dx_tf_hts_tape_hastelloy", scan=scan), - show_legend=True, - ) - plot_tf_corc_cable_summary_box(plot_205, pages["cable"], m_file, scan) - ax_hts_tape = pages["cable"].add_subplot(339) - ax_hts_tape.set_position([0.75, 0.1, 0.2, 0.2]) - plot_hts_tape_geometry( - axis=ax_hts_tape, - r_left=0.0, - z_bottom=0.0, - dr_hts_tape=m_file.get("dr_tf_hts_tape", scan=scan), - dx_hts_tape_rebco=m_file.get("dx_tf_hts_tape_rebco", scan=scan), - dx_hts_tape_copper=m_file.get("dx_tf_hts_tape_copper", scan=scan), - dx_hts_tape_hastelloy=m_file.get("dx_tf_hts_tape_hastelloy", scan=scan), - show_legend=True, - ) - elif ( - m_file.get("i_tf_turn_type", scan=scan) - == SuperconductingTFTurnType.CABLE_IN_CONDUIT - ): - plot_205 = pages["cable"].add_subplot(223, aspect="equal") - plot_205.set_position([0.075, 0.1, 0.3, 0.3]) - plot_cable_in_conduit_cable(plot_205, pages["cable"], m_file, scan) - plot_quench_time_evolution( - tau_discharge=m_file.get("t_tf_superconductor_quench", scan=scan), - b_peak=m_file.get("b_tf_inboard_peak_with_ripple", scan=scan), - f_a_cable_copper=m_file.get("f_a_tf_turn_cable_copper", scan=scan), - f_a_cable_space_helium=m_file.get( - "f_a_tf_turn_cable_space_cooling", scan=scan - ), - temp_he_peak=m_file.get("tftmp", scan=scan), - temp_quench_max=m_file.get("temp_tf_conductor_quench_max", scan=scan), - cu_rrr=m_file.get("rrr_tf_cu", scan=scan), - t_quench_detection=m_file.get("t_tf_quench_detection", scan=scan), - fluence=m_file.get("flu_tf_neutron_fast_max", scan=scan), - j_operating=m_file.get("j_tf_wp", scan=scan), - a_tf_turn_cable_space=m_file.get( - "a_tf_turn_cable_space_no_void", scan=scan - ), - a_tf_turn=m_file.get("a_tf_turn", scan=scan), - axes_1=_add_page("quench_time_evo").add_subplot(211), - axes_2=pages["quench_time_evo"].add_subplot(212), - ) - else: - ax19 = _add_page("tf_wp").add_subplot(211, aspect="equal") - ax19.set_position([0.06, 0.55, 0.675, 0.4]) - plot_resistive_tf_wp(ax19, m_file, scan, pages["tf_wp"]) - plot_resistive_tf_info(ax19, m_file, scan, pages["tf_wp"]) - plot_tf_coil_structure( - _add_page().add_subplot(111, aspect="equal"), m_file, scan, colour_scheme - ) - - plot_plasma_outboard_toroidal_ripple_map(_add_page(), m_file, scan) - - plot_tf_stress(_add_page().subplots(nrows=3, ncols=1, sharex=True).flatten(), m_file) - - plot_pf_dimensions( - axis=_add_page("pf_dimensions").add_subplot(121, aspect="equal"), - mfile=m_file, - scan=scan, - colour_scheme=colour_scheme, - ) - - plot_current_profiles_over_time(_add_page().add_subplot(111), m_file, scan) - - plot_pf_cs_plasma_mutual_inductance(_add_page().add_subplot(111), m_file, scan) - - plot_cs_coil_structure( - _add_page("cs_structure").add_subplot(121, aspect="equal"), - pages["cs_structure"], - m_file, - scan, - ) - plot_cs_turn_structure( - pages["cs_structure"].add_subplot(326, aspect="equal"), - pages["cs_structure"], - m_file, - scan, - ) - - plot_cs_stress_time_profile( - axis=_add_page("cs_stress").add_subplot(337), mfile=m_file, scan=scan - ) - - ax_332 = pages["cs_stress"].add_subplot(332) - plot_cs_hoop_stress_profile( - axis=ax_332, - mfile=m_file, - scan=scan, - j_cs=m_file.get("j_cs_pulse_start", scan=scan), - b_cs_inner=m_file.get("b_cs_peak_pulse_start", scan=scan), - ) - - ax_333 = pages["cs_stress"].add_subplot(333) - plot_cs_radial_stress_profile( - axis=ax_333, - mfile=m_file, - scan=scan, - j_cs=m_file.get("j_cs_pulse_start", scan=scan), - b_cs_inner=m_file.get("b_cs_peak_pulse_start", scan=scan), - ) - - ax_334 = pages["cs_stress"].add_subplot(334) - ax_334_position = ax_334.get_position() - cbar_ax_334 = pages["cs_stress"].add_axes([ - ax_334_position.x1 - 0.01, - ax_334_position.y0, - 0.012, - ax_334_position.height, - ]) - - ax_336 = pages["cs_stress"].add_subplot(336, sharex=ax_333, sharey=ax_334) - ax_336_position = ax_336.get_position() - cbar_ax_336 = pages["cs_stress"].add_axes([ - ax_336_position.x1 + 0.01, - ax_336_position.y0, - 0.012, - ax_336_position.height, - ]) - - plot_cs_radial_stress_contour_profile( - axis=ax_336, - mfile=m_file, - scan=scan, - j_cs=m_file.get("j_cs_pulse_start", scan=scan), - b_cs_inner=m_file.get("b_cs_peak_pulse_start", scan=scan), - colorbar_axis=cbar_ax_336, - ) - - ax_331 = pages["cs_stress"].add_subplot(331) - plot_cs_vertical_stress_profile( - axis=ax_331, - mfile=m_file, - scan=scan, - ) - plot_vertical_stress_contour_profile( - axis=ax_334, - mfile=m_file, - scan=scan, - colorbar_axis=cbar_ax_334, - ) - - ax_335 = pages["cs_stress"].add_subplot(335, sharex=ax_332, sharey=ax_334) - ax_335_position = ax_335.get_position() - cbar_ax_335 = pages["cs_stress"].add_axes([ - ax_335_position.x1 + 0.01, - ax_335_position.y0, - 0.012, - ax_335_position.height, - ]) - plot_cs_hoop_stress_contour_profile( - axis=ax_335, - mfile=m_file, - scan=scan, - j_cs=m_file.get("j_cs_pulse_start", scan=scan), - b_cs_inner=m_file.get("b_cs_peak_pulse_start", scan=scan), - colorbar_axis=cbar_ax_335, - ) - - pages["cs_stress"].subplots_adjust(wspace=0.45, hspace=0.45) - - # Keep y-axis labeling on the left contour only when sharing y across contour subplots. - for axis in (ax_335, ax_336): - axis.set_ylabel("") - axis.tick_params(axis="y", labelleft=False) - - ax_338 = pages["cs_stress"].add_subplot(338, sharex=ax_332, sharey=ax_335) - ax_338_position = ax_338.get_position() - cbar_ax_338 = pages["cs_stress"].add_axes([ - ax_338_position.x1 + 0.01, - ax_338_position.y0, - 0.012, - ax_338_position.height, - ]) - plot_cs_tresca_2d_contour( - axis=ax_338, - mfile=m_file, - scan=scan, - colorbar_axis=cbar_ax_338, - ) - - ax_339 = pages["cs_stress"].add_subplot(339, sharex=ax_332, sharey=ax_338) - ax_339_position = ax_339.get_position() - cbar_ax_339 = pages["cs_stress"].add_axes([ - ax_339_position.x1 + 0.01, - ax_339_position.y0, - 0.012, - ax_339_position.height, - ]) - - plot_cs_von_mises_2d_contour( - axis=ax_339, - mfile=m_file, - scan=scan, - colorbar_axis=cbar_ax_339, - ) - - plot_first_wall_top_down_cross_section( - _add_page("fw_td_cross_section").add_subplot(221, aspect="equal"), m_file, scan - ) - plot_first_wall_poloidal_cross_section( - pages["fw_td_cross_section"].add_subplot(122), m_file, scan - ) - plot_fw_90_deg_pipe_bend(pages["fw_td_cross_section"].add_subplot(337), m_file, scan) - - plot_blkt_pipe_bends(_add_page("blkt_pipe_bends"), m_file, scan) - ax_blanket = pages["blkt_pipe_bends"].add_subplot(122, aspect="equal") - plot_blkt_structure( - ax_blanket, - pages["blkt_pipe_bends"], - m_file, - scan, - radial_build, - colour_scheme, - ) - - plot_main_power_flow( - _add_page("main_power_flow").add_subplot(111, aspect="equal"), - m_file, - scan, - pages["main_power_flow"], - ) - - ax24 = _add_page("power_profile_over_time").add_subplot(111) - # set_position([left, bottom, width, height]) -> height ~ 0.66 => ~2/3 of page height - ax24.set_position([0.08, 0.35, 0.84, 0.57]) - plot_system_power_profiles_over_time( - ax24, m_file, scan, pages["power_profile_over_time"] - ) - return list(pages.values()) - - -def create_thickness_builds(m_file, scan: int): - """Create the dictionaries of radial and vertical build values and cumulative values""" - if int(m_file.get("i_single_null", scan=scan)) == 0: - vertical_upper = [ - "z_plasma_xpoint_upper", - "dz_fw_plasma_gap", - "dz_divertor", - "dz_shld_upper", - "dz_vv_upper", - "dz_shld_vv_gap", - "dz_shld_thermal", - "dr_tf_shld_gap", - "dr_tf_inboard", - ] - else: - vertical_upper = [ - "z_plasma_xpoint_upper", - "dz_fw_plasma_gap", - "dz_fw_upper", - "dz_blkt_upper", - "dr_shld_blkt_gap", - "dz_shld_upper", - "dz_vv_upper", - "dz_shld_vv_gap", - "dz_shld_thermal", - "dr_tf_shld_gap", - "dr_tf_inboard", - ] - - radial = {} - cumulative_radial = {} - subtotal = 0 - for item in RADIAL_BUILD: - if item in {"rminori", "rminoro"}: - build = m_file.get("rminor", scan=scan) - elif item in {"vvblgapi", "vvblgapo"}: - build = m_file.get("dr_shld_blkt_gap", scan=scan) - elif "dr_vv_inboard" in item: - build = m_file.get("dr_vv_inboard", scan=scan) - elif "dr_vv_outboard" in item: - build = m_file.get("dr_vv_outboard", scan=scan) - else: - build = m_file.get(item, scan=scan) - - radial[item] = build - subtotal += build - cumulative_radial[item] = subtotal - - upper = {} - cumulative_upper = {} - subtotal = 0 - for item in vertical_upper: - upper[item] = m_file.get(item, scan=scan) - subtotal += upper[item] - cumulative_upper[item] = subtotal - - lower = {} - cumulative_lower = {} - subtotal = 0 - for item in vertical_lower: - lower[item] = m_file.get(item, scan=scan) - subtotal -= lower[item] - cumulative_lower[item] = subtotal - - return RadialBuild( - upper, lower, radial, cumulative_upper, cumulative_lower, cumulative_radial - ) - - -def plot_summary( - mfile: Path, - scan: int = -1, - demo_ranges: bool = False, - colour: Literal[1, 2] = 1, - output_format: str = "pdf", - show: bool = False, -): - """Create the summary.pdf""" - - def add_page_footer( - fig: plt.Figure, page_number: int, total_pages: int, run_label: str - ): - footer_text = f"{run_label}" - fig.text( - 0.01, - 0.01, - footer_text, - fontsize=7, - ha="left", - va="bottom", - color="dimgray", - ) - fig.text( - 0.99, - 0.01, - f"Page {page_number}/{total_pages}", - fontsize=7, - ha="right", - va="bottom", - color="dimgray", - ) - - # create main plot - # Increase range when adding new page - # run main_plot - mfile_obj = MFile(mfile) if mfile else MFile("MFILE.DAT") - run_label = f"{mfile_obj.get('fileprefix', scan=-1)} | scan {scan or -1} | {mfile_obj.get('date', scan=-1)} {mfile_obj.get('time', scan=-1)} | {mfile_obj.get('tagno', scan=-1)} | Branch: {mfile_obj.get('branch_name', scan=-1)} " - pages_of_plots = main_plot( - mfile_obj, - scan=scan or -1, - demo_ranges=demo_ranges, - colour_scheme=colour, - ) - - if output_format == "pdf": - with bpdf.PdfPages(mfile.with_name(mfile.name + "SUMMARY.pdf")) as pdf: - for page_number, p in enumerate(pages_of_plots, start=1): - add_page_footer(p, page_number, len(pages_of_plots), run_label) - pdf.savefig(p) - elif output_format == "png": - folder = Path(mfile.with_name(mfile.stem + "_SUMMARY")) - folder.mkdir(parents=True, exist_ok=True) - for no, page in enumerate(pages_of_plots): - add_page_footer(page, no + 1, len(pages_of_plots), run_label) - page.savefig(Path(folder, f"page{no}.png"), format="png") - - # show fig if option used - if show: - plt.show(block=True) - - plt.close("all") diff --git a/process/core/io/plot/summary/__init__.py b/process/core/io/plot/summary/__init__.py new file mode 100644 index 0000000000..69fb2e34d8 --- /dev/null +++ b/process/core/io/plot/summary/__init__.py @@ -0,0 +1,37 @@ +"""PROCESS summary plotting package.""" + +from __future__ import annotations + +import process.core.io.plot.summary.api as _api +import process.core.io.plot.summary.common as _common +import process.core.io.plot.summary.geometry as _geometry +import process.core.io.plot.summary.magnets as _magnets +import process.core.io.plot.summary.plasma as _plasma +import process.core.io.plot.summary.power_flow as _power_flow +import process.core.io.plot.summary.profiles as _profiles +import process.core.io.plot.summary.reporting as _reporting +import process.core.io.plot.summary.time_profiles as _time_profiles + +_MODULES = ( + _api, + _common, + _geometry, + _magnets, + _plasma, + _power_flow, + _profiles, + _reporting, + _time_profiles, +) +_REGISTRY = {} +for _module in _MODULES: + _REGISTRY.update({name: getattr(_module, name) for name in _module.__all__}) +for _module in _MODULES: + _module.__dict__.update(_REGISTRY) + + +plot_summary = _api.plot_summary +main_plot = _api.main_plot +create_thickness_builds = _api.create_thickness_builds + +__all__ = ["create_thickness_builds", "main_plot", "plot_summary"] diff --git a/process/core/io/plot/summary/api.py b/process/core/io/plot/summary/api.py new file mode 100644 index 0000000000..c07541530f --- /dev/null +++ b/process/core/io/plot/summary/api.py @@ -0,0 +1,942 @@ +"""Api functions for PROCESS summary plots.""" + +from __future__ import annotations + +from importlib import resources +from pathlib import Path +from typing import Literal + +import matplotlib.backends.backend_pdf as bpdf +import matplotlib.pyplot as plt + +from process.core.io.mfile import MFile +from process.core.io.plot.summary.common import ( + color_key, +) +from process.core.io.plot.summary.constants import ( + RADIAL_BUILD, + vertical_lower, +) +from process.core.io.plot.summary.geometry import ( + plot_blkt_pipe_bends, + plot_blkt_structure, + plot_first_wall_poloidal_cross_section, + plot_first_wall_top_down_cross_section, + plot_full_machine_poloidal_cross_section, + plot_geometry_info, + plot_radial_build, + poloidal_cross_section, + toroidal_cross_section, +) +from process.core.io.plot.summary.magnets import ( + plot_cable_in_conduit_cable, + plot_corc_cable_geometry, + plot_cs_coil_structure, + plot_cs_turn_structure, + plot_hts_tape_geometry, + plot_magnetics_info, + plot_pf_cs_plasma_mutual_inductance, + plot_pf_dimensions, + plot_physics_info, + plot_quench_time_evolution, + plot_resistive_tf_info, + plot_resistive_tf_wp, + plot_superconducting_tf_wp, + plot_tf_cable_in_conduit_turn, + plot_tf_coil_structure, + plot_tf_corc_cable_summary_box, + plot_tf_croco_turn, + plot_tf_stress, +) +from process.core.io.plot.summary.plasma import ( + plot_bootstrap_comparison, + plot_brunner_divertor_power_split_comparison_stackplot, + plot_confinement_time_comparison, + plot_detailed_plasma_parameters, + plot_magnetic_fields_in_plasma, + plot_main_plasma_information, + plot_max_normalised_beta_comparison, + plot_plasma_coloumb_logarithms, + plot_plasma_current_comparison, + plot_plasma_outboard_toroidal_ripple_map, + plot_sol_power_decay_length_comparison, +) +from process.core.io.plot.summary.power_flow import ( + plot_main_power_flow, + plot_power_info, +) +from process.core.io.plot.summary.profiles import ( + plot_beta_profiles, + plot_collision_frequency_profile, + plot_collision_time_profile, + plot_cs_hoop_stress_contour_profile, + plot_cs_hoop_stress_profile, + plot_cs_radial_stress_contour_profile, + plot_cs_radial_stress_profile, + plot_cs_stress_time_profile, + plot_cs_tresca_2d_contour, + plot_cs_vertical_stress_profile, + plot_cs_von_mises_2d_contour, + plot_cumulative_plasma_thermal_energy_profiles, + plot_debye_length_profile, + plot_electron_frequency_profile, + plot_fusion_rate_contours, + plot_fusion_rate_profiles, + plot_ion_charge_profile, + plot_ion_frequency_profile, + plot_ion_slowing_down_time_profile, + plot_jprofile, + plot_larmor_radius_profile, + plot_line_brem_loss_function_profile, + plot_line_brem_power_density_profile, + plot_mean_free_path_profile, + plot_n_profiles, + plot_plasma_effective_charge_profile, + plot_plasma_poloidal_pressure_contours, + plot_plasma_pressure_gradient_profiles, + plot_plasma_pressure_profiles, + plot_plasma_thermal_energy_profiles, + plot_qprofile, + plot_rad_contour, + plot_resistivity_profile, + plot_t_profiles, + plot_velocity_profile, + plot_vertical_stress_contour_profile, +) +from process.core.io.plot.summary.reporting import ( + RadialBuild, + plot_cover_page, + plot_density_limit_comparison, + plot_ebw_ecrh_coupling_graph, + plot_equality_constraint_equations, + plot_fw_90_deg_pipe_bend, + plot_h_threshold_comparison, + plot_header, + plot_inequality_constraint_equations, + plot_iteration_variables, + plot_lower_vertical_build, + plot_separatrix_power_split, + plot_upper_vertical_build, +) +from process.core.io.plot.summary.time_profiles import ( + plot_current_profiles_over_time, + plot_system_power_profiles_over_time, +) +from process.models.physics.plasma_geometry import PlasmaShapeModelType +from process.models.tfcoil.base import TFConductorModel +from process.models.tfcoil.superconducting import SuperconductingTFTurnType + + +def main_plot( + m_file: MFile, + scan: int, + imp: str = "../data/lz_non_corona_14_elements/", + demo_ranges: bool = False, + colour_scheme: Literal[1, 2] = 1, +) -> list[plt.Figure]: + """Function to create radial and vertical build plot on given figure. + + Parameters + ---------- + m_file : + MFILE + scan : + scan to read from MFILE + imp : + path to impurity data + demo_ranges: bool : + (Default value = False) + colour_scheme: + + """ + # Checking the impurity data folder + # Get path to impurity data dir + # TODO use Path objects throughout module, not strings + + with resources.path( + "process.data.lz_non_corona_14_elements", "Ar_lz_tau.dat" + ) as imp_path: + imp = str(imp_path.parent) + "/" + + i_shape = int(m_file.get("i_plasma_shape", scan=scan)) + # Setup params for text plots + plt.rcParams.update({"font.size": 8}) + + pages = {} + + def _add_page(name: str | None = None): + """Add a page to the dictionary of pages. If no name is provided, then assign the + lowest unused number. + + Raises + ------ + KeyError + If a page number has already been used + """ + if name is None: + prev_index = max((int(k) for k in pages if k.isnumeric()), default=0) + name = str(prev_index + 1) + if name in pages: + raise KeyError(f"Name collision: {name} already in `pages`!") + pages[name] = plt.figure(figsize=(12, 9), dpi=80) + return pages[name] + + radial_build = create_thickness_builds(m_file, scan) + + plot_cover_page( + _add_page("cover").add_subplot(111), + m_file, + scan, + pages["cover"], + radial_build, + colour_scheme, + ) + + # Plot header info + plot_header(_add_page("first").add_subplot(231), m_file, scan) + + # Geometry + plot_geometry_info(pages["first"].add_subplot(232), m_file, scan) + + # Physics + plot_physics_info(pages["first"].add_subplot(233), m_file, scan) + + # Magnetics + plot_magnetics_info(pages["first"].add_subplot(234), m_file, scan) + + # power/flow economics + plot_power_info(pages["first"].add_subplot(235), m_file, scan) + + # Current drive + # plot_current_drive_info(pages["first"].add_subplot(236), m_file_data, scan) + pages["first"].subplots_adjust(wspace=0.25, hspace=0.25) + + ax7 = _add_page().add_subplot(111) + ax7.set_position([0.25, 0.1, 0.7, 0.8]) # Move plot slightly to the right + plot_iteration_variables(ax7, m_file, scan) + + ax7_5 = _add_page().add_subplot(313) + ax7_5.set_position([0.25, 0.1, 0.7, 0.8]) + plot_equality_constraint_equations(ax7_5, m_file, scan) + ax7_6 = _add_page().add_subplot(111) + ax7_6.set_position([0.3, 0.1, 0.65, 0.8]) + plot_inequality_constraint_equations(ax7_6, m_file, scan) + + # Plot main plasma information + plot_main_plasma_information( + _add_page("plasma_info").add_subplot(111, aspect="equal"), + m_file, + scan, + colour_scheme, + pages["plasma_info"], + ) + + # Plot density profiles + plot_n_profiles(_add_page("profiles"), demo_ranges, m_file, scan) + + # Plot temperature profiles + ax10 = pages["profiles"].add_subplot(232) + ax10.set_position([0.375, 0.575, 0.25, 0.375]) + plot_t_profiles(ax10, demo_ranges, m_file, scan) + + # Plot impurity profiles + ax11 = pages["profiles"].add_subplot(233) + ax11.set_position([0.7, 0.45, 0.25, 0.5]) + plot_line_brem_power_density_profile( + axis=ax11, mfile=m_file, scan=scan, impp=imp, demo_ranges=demo_ranges + ) + + # Plot current density profile + ax12 = pages["profiles"].add_subplot(4, 3, 10) + ax12.set_position([0.375, 0.105, 0.25, 0.15]) + plot_jprofile(ax12, m_file, scan) + + # Plot q profile + ax13 = pages["profiles"].add_subplot(4, 3, 12) + ax13.set_position([0.7, 0.105, 0.25, 0.15]) + plot_qprofile(ax13, demo_ranges, m_file, scan) + + ax_line_brem = _add_page("rad_contour").add_subplot(325) + plot_line_brem_loss_function_profile( + axis=ax_line_brem, + mfile=m_file, + scan=scan, + impp=imp, + ) + + ax_zeff = pages["rad_contour"].add_subplot(321, sharex=ax_line_brem) + plot_plasma_effective_charge_profile(ax_zeff, m_file, scan) + ax_zeff.set_xlabel("") + ax_zeff.tick_params( + axis="x", which="both", bottom=True, top=False, labelbottom=False + ) + + ax_ion_charge = pages["rad_contour"].add_subplot(323, sharex=ax_line_brem) + plot_ion_charge_profile(ax_ion_charge, m_file, scan) + ax_ion_charge.set_xlabel("") + ax_ion_charge.tick_params( + axis="x", which="both", bottom=True, top=False, labelbottom=False + ) + + if i_shape == 1: + plot_rad_contour(pages["rad_contour"].add_subplot(122), m_file, scan, imp) + + if i_shape != 1: + msg = ( + "Radiation contour plots require a closed (Sauter) plasma" + " boundary (i_plasma_shape == 1). Current i_plasma_shape =" + f" {i_shape}. Contour plots are skipped; see the 1D radiation" + " plots for available information." + ) + # Add explanatory text to both figures reserved for contour outputs + pages["rad_contour"].text( + 0.75, 0.5, msg, ha="center", va="center", wrap=True, fontsize=12 + ) + + plot_fusion_rate_profiles( + _add_page("fusion_rate").add_subplot(122), + pages["fusion_rate"], + m_file, + scan, + ) + + _add_page("rx_1_2"), _add_page("rx_3_4") + if m_file.get("i_plasma_shape", scan=scan) == PlasmaShapeModelType.SAUTER: + plot_fusion_rate_contours(pages["rx_1_2"], pages["rx_3_4"], m_file, scan) + + if i_shape != PlasmaShapeModelType.SAUTER: + msg = ( + "Fusion-rate contour plots require a closed (Sauter) plasma" + " boundary (i_plasma_shape == 1). Current i_plasma_shape =" + f" {i_shape}. Contour plots are skipped; see the 1D fusion" + " rate/profile plots for available information." + ) + # Add explanatory text to both figures reserved for contour outputs + pages["rx_1_2"].text( + 0.5, 0.5, msg, ha="center", va="center", wrap=True, fontsize=12 + ) + pages["rx_3_4"].text( + 0.5, 0.5, msg, ha="center", va="center", wrap=True, fontsize=12 + ) + + plot_plasma_pressure_profiles( + _add_page("pressure_profile").add_subplot(222), m_file, scan + ) + plot_plasma_pressure_gradient_profiles( + pages["pressure_profile"].add_subplot(224), m_file, scan + ) + # Currently only works with Sauter geometry as plasma has a closed surface + + if i_shape == PlasmaShapeModelType.SAUTER: + plot_plasma_poloidal_pressure_contours( + pages["pressure_profile"].add_subplot(121, aspect="equal"), + m_file, + scan, + ) + else: + ax = pages["pressure_profile"].add_subplot(131, aspect="equal") + msg = ( + "Plasma poloidal pressure contours require a closed (Sauter)" + " plasma boundary (i_plasma_shape ==" + f" {PlasmaShapeModelType.SAUTER}). Current i_plasma_shape =" + f" {i_shape}. Contour plots are skipped; see the 1D" + " pressure/profile plots for available information." + ) + ax.text( + 0.5, + 0.5, + msg, + ha="center", + va="center", + wrap=True, + fontsize=10, + transform=ax.transAxes, + ) + ax.axis("off") + + plot_magnetic_fields_in_plasma( + _add_page("beta").add_subplot(122, aspect="equal"), m_file, scan + ) + plot_beta_profiles(pages["beta"].add_subplot(321), m_file, scan) + + ax_thermal_energy = pages["beta"].add_subplot(325) + plot_plasma_thermal_energy_profiles(ax_thermal_energy, m_file, scan) + ax_thermal_energy_cumulative = pages["beta"].add_subplot( + 323, sharex=ax_thermal_energy + ) + ax_thermal_energy_cumulative.set_position([0.127, 0.35, 0.35, 0.2]) + plot_cumulative_plasma_thermal_energy_profiles( + ax_thermal_energy_cumulative, m_file, scan + ) + + plot_ebw_ecrh_coupling_graph(_add_page().add_subplot(111), m_file, scan) + + plot_bootstrap_comparison(_add_page("current").add_subplot(221), m_file, scan) + plot_plasma_current_comparison(pages["current"].add_subplot(224), m_file, scan) + plot_h_threshold_comparison( + _add_page("plasma_compare_1").add_subplot(224), m_file, scan + ) + plot_density_limit_comparison( + pages["plasma_compare_1"].add_subplot(221), m_file, scan + ) + + plot_max_normalised_beta_comparison( + _add_page("plasma_compare_2").add_subplot(221), m_file, scan + ) + plot_confinement_time_comparison( + pages["plasma_compare_2"].add_subplot(224), m_file, scan + ) + + plot_sol_power_decay_length_comparison( + _add_page("plasma_compare_3").add_subplot(221), m_file, scan + ) + + plot_brunner_divertor_power_split_comparison_stackplot( + _add_page("plasma_exhaust").add_subplot(121), m_file, scan + ) + + plot_separatrix_power_split( + pages["plasma_exhaust"].add_subplot(122), m_file, scan, colour_scheme + ) + + plot_debye_length_profile( + _add_page("microscopic_quantities").add_subplot(232), m_file, scan + ) + plot_velocity_profile(pages["microscopic_quantities"].add_subplot(233), m_file, scan) + plot_plasma_coloumb_logarithms( + pages["microscopic_quantities"].add_subplot(231), m_file, scan + ) + plot_collision_time_profile( + pages["microscopic_quantities"].add_subplot(234), m_file, scan + ) + plot_collision_frequency_profile( + pages["microscopic_quantities"].add_subplot(235), m_file, scan + ) + plot_mean_free_path_profile( + pages["microscopic_quantities"].add_subplot(236), m_file, scan + ) + + plot_ion_slowing_down_time_profile( + _add_page("detailed_params").add_subplot(231), m_file, scan + ) + + plot_resistivity_profile(pages["detailed_params"].add_subplot(232), m_file, scan) + + plot_detailed_plasma_parameters( + pages["detailed_params"].add_subplot(233), + fig=pages["detailed_params"], + mfile=m_file, + scan=scan, + ) + + ax_electron_freq = _add_page("freq").add_subplot(211) + plot_electron_frequency_profile(ax_electron_freq, m_file, scan) + + ax_ion_freq = pages["freq"].add_subplot(413, sharex=ax_electron_freq) + plot_ion_frequency_profile(ax_ion_freq, m_file, scan) + + ax_larmor = pages["freq"].add_subplot(414, sharex=ax_electron_freq) + plot_larmor_radius_profile(ax_larmor, m_file, scan) + + pages["freq"].subplots_adjust(hspace=0.5) + + # Plot poloidal cross-section + poloidal_cross_section( + _add_page("tokamak_cross_section").add_subplot(121, aspect="equal"), + m_file, + scan, + demo_ranges, + radial_build, + colour_scheme, + ) + + # Plot toroidal cross-section + toroidal_cross_section( + pages["tokamak_cross_section"].add_subplot(122, aspect="equal"), + m_file, + scan, + demo_ranges, + colour_scheme, + ) + + # Plot color key + ax17 = pages["tokamak_cross_section"].add_subplot(222) + ax17.set_position([0.5, 0.5, 0.5, 0.5]) + color_key(ax17, m_file, scan, colour_scheme) + + plot_full_machine_poloidal_cross_section( + _add_page().add_subplot(111, aspect="equal"), + m_file, + scan, + radial_build, + colour_scheme, + ) + + ax_full_toroidal = _add_page("full_machine_toroidal").add_subplot( + 111, aspect="equal" + ) + toroidal_cross_section( + ax_full_toroidal, + m_file, + scan, + demo_ranges, + colour_scheme, + ) + ax_full_toroidal.set_ylim( + -ax_full_toroidal.get_ylim()[1], + ax_full_toroidal.get_ylim()[1], + ) + ax_full_toroidal.set_xlim( + -ax_full_toroidal.get_xlim()[1], + ax_full_toroidal.get_xlim()[1], + ) + + ax18 = _add_page().add_subplot(211) + ax18.set_position([0.1, 0.33, 0.8, 0.6]) + plot_radial_build(ax18, m_file, colour_scheme) + + # Make each axes smaller vertically to leave room for the legend + ax185 = _add_page("vertical_build").add_subplot(211) + ax185.set_position([0.1, 0.61, 0.8, 0.32]) + + ax18b = pages["vertical_build"].add_subplot(212) + ax18b.set_position([0.1, 0.13, 0.8, 0.32]) + plot_upper_vertical_build(ax185, m_file, colour_scheme) + plot_lower_vertical_build(ax18b, m_file, colour_scheme) + + # Can only plot WP and turn structure if superconducting coil at the moment + if m_file.get("i_tf_sup", scan=scan) == TFConductorModel.SUPERCONDUCTING: + # TF coil with WP + ax19 = _add_page("tf_wp").add_subplot(221, aspect="equal") + ax19.set_position([ + 0.025, + 0.45, + 0.5, + 0.5, + ]) # Half height, a bit wider, top left + plot_superconducting_tf_wp(ax19, m_file, scan, pages["tf_wp"]) + + _add_page("cable") + if ( + m_file.get("i_tf_turn_type", scan=scan) + == SuperconductingTFTurnType.CROSS_CONDUCTOR + ): + ax20 = pages["cable"].add_subplot(325, aspect="equal") + ax20.set_position([0.025, 0.5, 0.4, 0.4]) + plot_tf_croco_turn(ax20, pages["cable"], m_file, scan) + elif ( + m_file.get("i_tf_turn_type", scan=scan) + == SuperconductingTFTurnType.CABLE_IN_CONDUIT + ): + # TF coil turn structure + ax20 = pages["cable"].add_subplot(325, aspect="equal") + ax20.set_position([0.025, 0.5, 0.4, 0.4]) + plot_tf_cable_in_conduit_turn(ax20, pages["cable"], m_file, scan) + + if ( + m_file.get("i_tf_turn_type", scan=scan) + == SuperconductingTFTurnType.CROSS_CONDUCTOR + ): + plot_205 = pages["cable"].add_subplot(223, aspect="equal") + plot_205.set_position([0.075, 0.1, 0.3, 0.3]) + plot_corc_cable_geometry( + plot_205, + r_centre=0.0, + z_centre=0.0, + dia_croco_strand=m_file.get("dia_tf_turn_croco_cable", scan=scan), + dx_croco_strand_copper=m_file.get( + "dx_tf_croco_strand_copper", scan=scan + ), + dr_hts_tape=m_file.get("dr_tf_hts_tape", scan=scan), + dx_croco_strand_tape_stack=m_file.get( + "dx_tf_croco_strand_tape_stack", scan=scan + ), + n_croco_strand_hts_tapes=m_file.get( + "n_tf_croco_strand_hts_tapes", scan=scan + ), + dx_hts_tape_rebco=m_file.get("dx_tf_hts_tape_rebco", scan=scan), + dx_hts_tape_copper=m_file.get("dx_tf_hts_tape_copper", scan=scan), + dx_hts_tape_hastelloy=m_file.get("dx_tf_hts_tape_hastelloy", scan=scan), + show_legend=True, + ) + plot_tf_corc_cable_summary_box(plot_205, pages["cable"], m_file, scan) + ax_hts_tape = pages["cable"].add_subplot(339) + ax_hts_tape.set_position([0.75, 0.1, 0.2, 0.2]) + plot_hts_tape_geometry( + axis=ax_hts_tape, + r_left=0.0, + z_bottom=0.0, + dr_hts_tape=m_file.get("dr_tf_hts_tape", scan=scan), + dx_hts_tape_rebco=m_file.get("dx_tf_hts_tape_rebco", scan=scan), + dx_hts_tape_copper=m_file.get("dx_tf_hts_tape_copper", scan=scan), + dx_hts_tape_hastelloy=m_file.get("dx_tf_hts_tape_hastelloy", scan=scan), + show_legend=True, + ) + elif ( + m_file.get("i_tf_turn_type", scan=scan) + == SuperconductingTFTurnType.CABLE_IN_CONDUIT + ): + plot_205 = pages["cable"].add_subplot(223, aspect="equal") + plot_205.set_position([0.075, 0.1, 0.3, 0.3]) + plot_cable_in_conduit_cable(plot_205, pages["cable"], m_file, scan) + plot_quench_time_evolution( + tau_discharge=m_file.get("t_tf_superconductor_quench", scan=scan), + b_peak=m_file.get("b_tf_inboard_peak_with_ripple", scan=scan), + f_a_cable_copper=m_file.get("f_a_tf_turn_cable_copper", scan=scan), + f_a_cable_space_helium=m_file.get( + "f_a_tf_turn_cable_space_cooling", scan=scan + ), + temp_he_peak=m_file.get("tftmp", scan=scan), + temp_quench_max=m_file.get("temp_tf_conductor_quench_max", scan=scan), + cu_rrr=m_file.get("rrr_tf_cu", scan=scan), + t_quench_detection=m_file.get("t_tf_quench_detection", scan=scan), + fluence=m_file.get("flu_tf_neutron_fast_max", scan=scan), + j_operating=m_file.get("j_tf_wp", scan=scan), + a_tf_turn_cable_space=m_file.get( + "a_tf_turn_cable_space_no_void", scan=scan + ), + a_tf_turn=m_file.get("a_tf_turn", scan=scan), + axes_1=_add_page("quench_time_evo").add_subplot(211), + axes_2=pages["quench_time_evo"].add_subplot(212), + ) + else: + ax19 = _add_page("tf_wp").add_subplot(211, aspect="equal") + ax19.set_position([0.06, 0.55, 0.675, 0.4]) + plot_resistive_tf_wp(ax19, m_file, scan, pages["tf_wp"]) + plot_resistive_tf_info(ax19, m_file, scan, pages["tf_wp"]) + plot_tf_coil_structure( + _add_page().add_subplot(111, aspect="equal"), + m_file, + scan, + colour_scheme, + ) + + plot_plasma_outboard_toroidal_ripple_map(_add_page(), m_file, scan) + + plot_tf_stress(_add_page().subplots(nrows=3, ncols=1, sharex=True).flatten(), m_file) + + plot_pf_dimensions( + axis=_add_page("pf_dimensions").add_subplot(121, aspect="equal"), + mfile=m_file, + scan=scan, + colour_scheme=colour_scheme, + ) + + plot_current_profiles_over_time(_add_page().add_subplot(111), m_file, scan) + + plot_pf_cs_plasma_mutual_inductance(_add_page().add_subplot(111), m_file, scan) + + plot_cs_coil_structure( + _add_page("cs_structure").add_subplot(121, aspect="equal"), + pages["cs_structure"], + m_file, + scan, + ) + plot_cs_turn_structure( + pages["cs_structure"].add_subplot(326, aspect="equal"), + pages["cs_structure"], + m_file, + scan, + ) + + plot_cs_stress_time_profile( + axis=_add_page("cs_stress").add_subplot(337), mfile=m_file, scan=scan + ) + + ax_332 = pages["cs_stress"].add_subplot(332) + plot_cs_hoop_stress_profile( + axis=ax_332, + mfile=m_file, + scan=scan, + j_cs=m_file.get("j_cs_pulse_start", scan=scan), + b_cs_inner=m_file.get("b_cs_peak_pulse_start", scan=scan), + ) + + ax_333 = pages["cs_stress"].add_subplot(333) + plot_cs_radial_stress_profile( + axis=ax_333, + mfile=m_file, + scan=scan, + j_cs=m_file.get("j_cs_pulse_start", scan=scan), + b_cs_inner=m_file.get("b_cs_peak_pulse_start", scan=scan), + ) + + ax_334 = pages["cs_stress"].add_subplot(334) + ax_334_position = ax_334.get_position() + cbar_ax_334 = pages["cs_stress"].add_axes([ + ax_334_position.x1 - 0.01, + ax_334_position.y0, + 0.012, + ax_334_position.height, + ]) + + ax_336 = pages["cs_stress"].add_subplot(336, sharex=ax_333, sharey=ax_334) + ax_336_position = ax_336.get_position() + cbar_ax_336 = pages["cs_stress"].add_axes([ + ax_336_position.x1 + 0.01, + ax_336_position.y0, + 0.012, + ax_336_position.height, + ]) + + plot_cs_radial_stress_contour_profile( + axis=ax_336, + mfile=m_file, + scan=scan, + j_cs=m_file.get("j_cs_pulse_start", scan=scan), + b_cs_inner=m_file.get("b_cs_peak_pulse_start", scan=scan), + colorbar_axis=cbar_ax_336, + ) + + ax_331 = pages["cs_stress"].add_subplot(331) + plot_cs_vertical_stress_profile( + axis=ax_331, + mfile=m_file, + scan=scan, + ) + plot_vertical_stress_contour_profile( + axis=ax_334, + mfile=m_file, + scan=scan, + colorbar_axis=cbar_ax_334, + ) + + ax_335 = pages["cs_stress"].add_subplot(335, sharex=ax_332, sharey=ax_334) + ax_335_position = ax_335.get_position() + cbar_ax_335 = pages["cs_stress"].add_axes([ + ax_335_position.x1 + 0.01, + ax_335_position.y0, + 0.012, + ax_335_position.height, + ]) + plot_cs_hoop_stress_contour_profile( + axis=ax_335, + mfile=m_file, + scan=scan, + j_cs=m_file.get("j_cs_pulse_start", scan=scan), + b_cs_inner=m_file.get("b_cs_peak_pulse_start", scan=scan), + colorbar_axis=cbar_ax_335, + ) + + pages["cs_stress"].subplots_adjust(wspace=0.45, hspace=0.45) + + # Keep y-axis labeling on the left contour only when sharing y across contour + # subplots. + for axis in (ax_335, ax_336): + axis.set_ylabel("") + axis.tick_params(axis="y", labelleft=False) + + ax_338 = pages["cs_stress"].add_subplot(338, sharex=ax_332, sharey=ax_335) + ax_338_position = ax_338.get_position() + cbar_ax_338 = pages["cs_stress"].add_axes([ + ax_338_position.x1 + 0.01, + ax_338_position.y0, + 0.012, + ax_338_position.height, + ]) + plot_cs_tresca_2d_contour( + axis=ax_338, + mfile=m_file, + scan=scan, + colorbar_axis=cbar_ax_338, + ) + + ax_339 = pages["cs_stress"].add_subplot(339, sharex=ax_332, sharey=ax_338) + ax_339_position = ax_339.get_position() + cbar_ax_339 = pages["cs_stress"].add_axes([ + ax_339_position.x1 + 0.01, + ax_339_position.y0, + 0.012, + ax_339_position.height, + ]) + + plot_cs_von_mises_2d_contour( + axis=ax_339, + mfile=m_file, + scan=scan, + colorbar_axis=cbar_ax_339, + ) + + plot_first_wall_top_down_cross_section( + _add_page("fw_td_cross_section").add_subplot(221, aspect="equal"), + m_file, + scan, + ) + plot_first_wall_poloidal_cross_section( + pages["fw_td_cross_section"].add_subplot(122), m_file, scan + ) + plot_fw_90_deg_pipe_bend(pages["fw_td_cross_section"].add_subplot(337), m_file, scan) + + plot_blkt_pipe_bends(_add_page("blkt_pipe_bends"), m_file, scan) + ax_blanket = pages["blkt_pipe_bends"].add_subplot(122, aspect="equal") + plot_blkt_structure( + ax_blanket, + pages["blkt_pipe_bends"], + m_file, + scan, + radial_build, + colour_scheme, + ) + + plot_main_power_flow( + _add_page("main_power_flow").add_subplot(111, aspect="equal"), + m_file, + scan, + pages["main_power_flow"], + ) + + ax24 = _add_page("power_profile_over_time").add_subplot(111) + # set_position([left, bottom, width, height]) -> height ~ 0.66 => ~2/3 of page height + ax24.set_position([0.08, 0.35, 0.84, 0.57]) + plot_system_power_profiles_over_time( + ax24, m_file, scan, pages["power_profile_over_time"] + ) + return list(pages.values()) + + +def create_thickness_builds(m_file, scan: int): + """Create the dictionaries of radial and vertical build values and cumulative + values + """ + if int(m_file.get("i_single_null", scan=scan)) == 0: + vertical_upper = [ + "z_plasma_xpoint_upper", + "dz_fw_plasma_gap", + "dz_divertor", + "dz_shld_upper", + "dz_vv_upper", + "dz_shld_vv_gap", + "dz_shld_thermal", + "dr_tf_shld_gap", + "dr_tf_inboard", + ] + else: + vertical_upper = [ + "z_plasma_xpoint_upper", + "dz_fw_plasma_gap", + "dz_fw_upper", + "dz_blkt_upper", + "dr_shld_blkt_gap", + "dz_shld_upper", + "dz_vv_upper", + "dz_shld_vv_gap", + "dz_shld_thermal", + "dr_tf_shld_gap", + "dr_tf_inboard", + ] + + radial = {} + cumulative_radial = {} + subtotal = 0 + for item in RADIAL_BUILD: + if item in {"rminori", "rminoro"}: + build = m_file.get("rminor", scan=scan) + elif item in {"vvblgapi", "vvblgapo"}: + build = m_file.get("dr_shld_blkt_gap", scan=scan) + elif "dr_vv_inboard" in item: + build = m_file.get("dr_vv_inboard", scan=scan) + elif "dr_vv_outboard" in item: + build = m_file.get("dr_vv_outboard", scan=scan) + else: + build = m_file.get(item, scan=scan) + + radial[item] = build + subtotal += build + cumulative_radial[item] = subtotal + + upper = {} + cumulative_upper = {} + subtotal = 0 + for item in vertical_upper: + upper[item] = m_file.get(item, scan=scan) + subtotal += upper[item] + cumulative_upper[item] = subtotal + + lower = {} + cumulative_lower = {} + subtotal = 0 + for item in vertical_lower: + lower[item] = m_file.get(item, scan=scan) + subtotal -= lower[item] + cumulative_lower[item] = subtotal + + return RadialBuild( + upper, + lower, + radial, + cumulative_upper, + cumulative_lower, + cumulative_radial, + ) + + +def plot_summary( + mfile: Path, + scan: int = -1, + demo_ranges: bool = False, + colour: Literal[1, 2] = 1, + output_format: str = "pdf", + show: bool = False, +): + """Create the summary.pdf""" + + def add_page_footer( + fig: plt.Figure, page_number: int, total_pages: int, run_label: str + ): + footer_text = f"{run_label}" + fig.text( + 0.01, + 0.01, + footer_text, + fontsize=7, + ha="left", + va="bottom", + color="dimgray", + ) + fig.text( + 0.99, + 0.01, + f"Page {page_number}/{total_pages}", + fontsize=7, + ha="right", + va="bottom", + color="dimgray", + ) + + # create main plot + # Increase range when adding new page + # run main_plot + mfile_obj = MFile(mfile) if mfile else MFile("MFILE.DAT") + run_label = ( + f"{mfile_obj.get('fileprefix', scan=-1)} | scan {scan or -1} |" + f" {mfile_obj.get('date', scan=-1)} {mfile_obj.get('time', scan=-1)} |" + f" {mfile_obj.get('tagno', scan=-1)} | Branch:" + f" {mfile_obj.get('branch_name', scan=-1)} " + ) + pages_of_plots = main_plot( + mfile_obj, + scan=scan or -1, + demo_ranges=demo_ranges, + colour_scheme=colour, + ) + + if output_format == "pdf": + with bpdf.PdfPages(mfile.with_name(mfile.name + "SUMMARY.pdf")) as pdf: + for page_number, p in enumerate(pages_of_plots, start=1): + add_page_footer(p, page_number, len(pages_of_plots), run_label) + pdf.savefig(p) + elif output_format == "png": + folder = Path(mfile.with_name(mfile.stem + "_SUMMARY")) + folder.mkdir(parents=True, exist_ok=True) + for no, page in enumerate(pages_of_plots): + add_page_footer(page, no + 1, len(pages_of_plots), run_label) + page.savefig(Path(folder, f"page{no}.png"), format="png") + + # show fig if option used + if show: + plt.show(block=True) + + plt.close("all") + + +__all__ = ["create_thickness_builds", "main_plot", "plot_summary"] diff --git a/process/core/io/plot/summary/common.py b/process/core/io/plot/summary/common.py new file mode 100644 index 0000000000..ee503e48ca --- /dev/null +++ b/process/core/io/plot/summary/common.py @@ -0,0 +1,151 @@ +"""Common functions for PROCESS summary plots.""" + +from __future__ import annotations + +from importlib import resources +from typing import TYPE_CHECKING, Literal + +import matplotlib.image as mpimg +import matplotlib.pyplot as plt +from matplotlib import patches + +from process.core.io.plot.summary.constants import ( + BLANKET_COLOUR, + CRYOSTAT_COLOUR, + CSCOMPRESSION_COLOUR, + FIRSTWALL_COLOUR, + NBSHIELD_COLOUR, + PLASMA_COLOUR, + SOLENOID_COLOUR, + TFC_COLOUR, + THERMAL_SHIELD_COLOUR, + VESSEL_COLOUR, +) +from process.core.io.plot.summary.rendering import ( + draw_text, +) +from process.models.pulse import PulseTimings + +if TYPE_CHECKING: + from process.core.io.mfile import MFile + + +def load_plot_image(name: str): + """Load an image bundled with the PROCESS plotting resources.""" + image = resources.files("process.core.io.plot.images").joinpath(name) + with image.open("rb") as image_file: + return mpimg.imread(image_file) + + +def place_plot_image(axis, name: str, bounds, *, zorder: int = 10): + """Place a bundled image in an inset axes and hide its frame.""" + image_axis = axis.inset_axes(bounds, transform=axis.transAxes, zorder=zorder) + image_axis.imshow(load_plot_image(name)) + image_axis.axis("off") + return image_axis + + +def get_pulse_timings(mfile, scan: int) -> PulseTimings: + """Construct pulse timings from an MFILE scan.""" + keys = ( + "t_plant_pulse_coil_precharge", + "t_plant_pulse_plasma_current_ramp_up", + "t_plant_pulse_fusion_ramp", + "t_plant_pulse_burn", + "t_plant_pulse_plasma_current_ramp_down", + "t_plant_pulse_dwell", + ) + return PulseTimings(**{key: mfile.get(key, scan=scan) for key in keys}) + + +def box_style(colour: str): + return { + "boxstyle": "round", + "facecolor": colour, + "alpha": 1.0, + "linewidth": 2, + } + + +def text_layout(fig): + return { + "fontsize": 9, + "verticalalignment": "bottom", + "horizontalalignment": "left", + "transform": fig.transFigure, + } + + +def setup_axis(axis, xmin, xmax, ymin, ymax): + axis.set_ylim(ymin, ymax) + axis.set_xlim(xmin, xmax) + axis.set_axis_off() + axis.set_autoscaley_on(False) + axis.set_autoscalex_on(False) + + +def add_colourbar(contour_fill, axis, colourbar_axis): + # Use a dedicated colorbar axes when provided so the main axes width is unchanged. + if colourbar_axis is None: + return axis.figure.colorbar(contour_fill, ax=axis, pad=0.02) + return axis.figure.colorbar(contour_fill, cax=colourbar_axis) + + +def color_key(axis: plt.Axes, mfile: MFile, scan: int, colour_scheme: Literal[1, 2]): + """Function to plot the colour key""" + axis.set_ylim(0, 10) + axis.set_xlim(0, 10) + axis.set_axis_off() + axis.set_autoscaley_on(False) + axis.set_autoscalex_on(False) + + labels = [ + ("CS coil", SOLENOID_COLOUR[colour_scheme - 1]), + ("CS comp", CSCOMPRESSION_COLOUR[colour_scheme - 1]), + ( + "TF coil", + ( + TFC_COLOUR[colour_scheme - 1] + if mfile.get("i_tf_sup", scan=scan) != 0 + else "#b87333" + ), + ), + ("Thermal shield", THERMAL_SHIELD_COLOUR[colour_scheme - 1]), + ("VV & shield", VESSEL_COLOUR[colour_scheme - 1]), + ("Blanket", BLANKET_COLOUR[colour_scheme - 1]), + ("First wall", FIRSTWALL_COLOUR[colour_scheme - 1]), + ("Plasma", PLASMA_COLOUR[colour_scheme - 1]), + ("PF coils", "none"), + ("Divertor", "black"), + ] + + if (mfile.get("i_hcd_primary", scan=scan) in {5, 8}) or ( + mfile.get("i_hcd_secondary", scan=scan) in {5, 8} + ): + labels.extend(( + ("NB duct shield", NBSHIELD_COLOUR[colour_scheme - 1]), + ("Cryostat", CRYOSTAT_COLOUR[colour_scheme - 1]), + )) + else: + labels.append(("Cryostat", CRYOSTAT_COLOUR[colour_scheme - 1])) + + for i, (text, color) in enumerate(labels): + row = i // 4 + col = i % 4 + y_pos = 9 - row * 1.5 + x_pos = col * 2.5 + + draw_text(axis, x_pos, y_pos, text, ha="left", va="top", size="small") + axis.add_patch( + patches.Rectangle( + (x_pos + 1.5, y_pos - 0.35), + 0.5, + 0.4, + lw=0 if color != "none" else 1, + facecolor=color if color != "none" else "none", + edgecolor="black" if color == "none" else "none", + ) + ) + + +__all__ = ["color_key"] diff --git a/process/core/io/plot/summary/constants.py b/process/core/io/plot/summary/constants.py new file mode 100644 index 0000000000..82a9faee7d --- /dev/null +++ b/process/core/io/plot/summary/constants.py @@ -0,0 +1,87 @@ +"""Constants shared by summary plotting modules.""" + +from __future__ import annotations + +import numpy as np + +SOLENOID_COLOUR = ["pink", "#1764ab"] +CSCOMPRESSION_COLOUR = ["maroon", "#33CCCC"] +TFC_COLOUR = ["cyan", "#084a91"] +THERMAL_SHIELD_COLOUR = ["gray", "#e3eef9"] +VESSEL_COLOUR = ["green", "#b7d4ea"] +SHIELD_COLOUR = ["green", "#94c4df"] +BLANKET_COLOUR = ["magenta", "#4a98c9"] +PLASMA_COLOUR = ["khaki", "#cc8acc"] +CRYOSTAT_COLOUR = ["red", "#2e7ebc"] +FIRSTWALL_COLOUR = ["darkblue", "darkblue"] +NBSHIELD_COLOUR = ["black", "black"] +thin = 0.0 +RADIAL_BUILD = [ + "dr_bore", + "dr_cs", + "dr_cs_precomp", + "dr_cs_tf_gap", + "dr_tf_inboard", + "dr_tf_shld_gap", + "dr_shld_thermal_inboard", + "dr_shld_vv_gap_inboard", + "dr_vv_inboard", + "dr_shld_inboard", + "vvblgapi", + "dr_blkt_inboard", + "dr_fw_inboard", + "dr_fw_plasma_gap_inboard", + "rminori", + "rminoro", + "dr_fw_plasma_gap_outboard", + "dr_fw_outboard", + "dr_blkt_outboard", + "vvblgapo", + "dr_shld_outboard", + "dr_vv_outboard", + "dr_shld_vv_gap_outboard", + "dr_shld_thermal_outboard", + "dr_tf_shld_gap", + "dr_tf_outboard", +] +vertical_lower = [ + "z_plasma_xpoint_lower", + "dz_xpoint_divertor", + "dz_divertor", + "dz_shld_lower", + "dz_vv_lower", + "dz_shld_vv_gap", + "dz_shld_thermal", + "dr_tf_shld_gap", + "dr_tf_inboard", +] +ANIMATION_INFO = [ + ("rmajor", "Major radius", "m"), + ("rminor", "Minor radius", "m"), + ("aspect", "Aspect ratio", ""), +] +rtangle = np.pi / 2 +rtangle2 = 2 * rtangle +white_box = {"boxstyle": "round", "facecolor": "white", "alpha": 1.0} + + +__all__ = [ + "ANIMATION_INFO", + "BLANKET_COLOUR", + "CRYOSTAT_COLOUR", + "CSCOMPRESSION_COLOUR", + "FIRSTWALL_COLOUR", + "NBSHIELD_COLOUR", + "PLASMA_COLOUR", + "RADIAL_BUILD", + "SHIELD_COLOUR", + "SOLENOID_COLOUR", + "TFC_COLOUR", + "THERMAL_SHIELD_COLOUR", + "VESSEL_COLOUR", + "rtangle", + "rtangle2", + "thin", + "vertical_lower", + "white_box", +] diff --git a/process/core/io/plot/summary/geometry/__init__.py b/process/core/io/plot/summary/geometry/__init__.py new file mode 100644 index 0000000000..efc0c51635 --- /dev/null +++ b/process/core/io/plot/summary/geometry/__init__.py @@ -0,0 +1,59 @@ +"""Public API for this summary plotting concern.""" + +from __future__ import annotations + +import process.core.io.plot.summary.geometry.build as _build +import process.core.io.plot.summary.geometry.misc as _misc +import process.core.io.plot.summary.geometry.poloidal as _poloidal +import process.core.io.plot.summary.geometry.toroidal as _toroidal + +_MODULES = (_build, _misc, _poloidal, _toroidal) +_REGISTRY = {} +for _module in _MODULES: + _REGISTRY.update({name: getattr(_module, name) for name in _module.__all__}) +for _module in _MODULES: + _module.__dict__.update(_REGISTRY) +arc = _REGISTRY["arc"] +arc_fill = _REGISTRY["arc_fill"] +cumulative_radial_build = _REGISTRY["cumulative_radial_build"] +cumulative_radial_build2 = _REGISTRY["cumulative_radial_build2"] +plot_blanket = _REGISTRY["plot_blanket"] +plot_blkt_pipe_bends = _REGISTRY["plot_blkt_pipe_bends"] +plot_blkt_structure = _REGISTRY["plot_blkt_structure"] +plot_cryostat = _REGISTRY["plot_cryostat"] +plot_first_wall_poloidal_cross_section = _REGISTRY[ + "plot_first_wall_poloidal_cross_section" +] +plot_first_wall_top_down_cross_section = _REGISTRY[ + "plot_first_wall_top_down_cross_section" +] +plot_firstwall = _REGISTRY["plot_firstwall"] +plot_full_machine_poloidal_cross_section = _REGISTRY[ + "plot_full_machine_poloidal_cross_section" +] +plot_geometry_info = _REGISTRY["plot_geometry_info"] +plot_radial_build = _REGISTRY["plot_radial_build"] +plot_shield = _REGISTRY["plot_shield"] +plot_vacuum_vessel_and_divertor = _REGISTRY["plot_vacuum_vessel_and_divertor"] +poloidal_cross_section = _REGISTRY["poloidal_cross_section"] +toroidal_cross_section = _REGISTRY["toroidal_cross_section"] +__all__ = [ + "arc", + "arc_fill", + "cumulative_radial_build", + "cumulative_radial_build2", + "plot_blanket", + "plot_blkt_pipe_bends", + "plot_blkt_structure", + "plot_cryostat", + "plot_first_wall_poloidal_cross_section", + "plot_first_wall_top_down_cross_section", + "plot_firstwall", + "plot_full_machine_poloidal_cross_section", + "plot_geometry_info", + "plot_radial_build", + "plot_shield", + "plot_vacuum_vessel_and_divertor", + "poloidal_cross_section", + "toroidal_cross_section", +] diff --git a/process/core/io/plot/summary/geometry/build.py b/process/core/io/plot/summary/geometry/build.py new file mode 100644 index 0000000000..ac6df537eb --- /dev/null +++ b/process/core/io/plot/summary/geometry/build.py @@ -0,0 +1,356 @@ +"""Geometry functions for PROCESS summary plots.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +import numpy as np + +from process.core.io.plot.summary.common import ( + setup_axis, +) +from process.core.io.plot.summary.constants import ( + BLANKET_COLOUR, + CSCOMPRESSION_COLOUR, + FIRSTWALL_COLOUR, + PLASMA_COLOUR, + RADIAL_BUILD, + SHIELD_COLOUR, + SOLENOID_COLOUR, + TFC_COLOUR, + THERMAL_SHIELD_COLOUR, + VESSEL_COLOUR, +) +from process.core.io.plot.summary.rendering import ( + draw_text, +) +from process.core.io.plot.summary.reporting import ( + plot_info, +) +from process.data_structure.build_variables import TFCSRadialConfiguration + +if TYPE_CHECKING: + import matplotlib.pyplot as plt + + from process.core.io.mfile import MFile + + +def cumulative_radial_build(section, mfile: MFile, scan: int): + """Function for calculating the cumulative radial build up to and + including the given section. + + Parameters + ---------- + section : + section of the radial build to go up to + mfile : + MFILE data object + scan : + scan number to use + + Returns + ------- + : + cumulative_build:cumulative radial build up to section given + """ + complete = False + cumulative_build = 0 + for item in RADIAL_BUILD: + if item in {"rminori", "rminoro"}: + cumulative_build += mfile.get("rminor", scan=scan) + elif item in {"vvblgapi", "vvblgapo"}: + cumulative_build += mfile.get("dr_shld_blkt_gap", scan=scan) + elif "dr_vv_inboard" in item: + cumulative_build += mfile.get("dr_vv_inboard", scan=scan) + elif "dr_vv_outboard" in item: + cumulative_build += mfile.get("dr_vv_outboard", scan=scan) + else: + cumulative_build += mfile.get(item, scan=scan) + if item == section: + complete = True + break + + if complete is False: + print("radial build parameter ", section, " not found") + return cumulative_build + + +def cumulative_radial_build2(section, mfile: MFile, scan: int): + """Function for calculating the cumulative radial build up to and + including the given section. + + Parameters + ---------- + section : + section of the radial build to go up to + mfile : + MFILE data object + scan : + scan number to use + + Returns + ------- + : + cumulative_build --> cumulative radial build up to and including + section given + previous --> cumulative radial build up to section given + """ + cumulative_build = 0 + build = 0 + for item in RADIAL_BUILD: + if item in {"rminori", "rminoro"}: + build = mfile.get("rminor", scan=scan) + elif item in {"vvblgapi", "vvblgapo"}: + build = mfile.get("dr_shld_blkt_gap", scan=scan) + elif "dr_vv_inboard" in item: + build = mfile.get("dr_vv_inboard", scan=scan) + elif "dr_vv_outboard" in item: + build = mfile.get("dr_vv_outboard", scan=scan) + else: + build = mfile.get(item, scan=scan) + cumulative_build += build + if item == section: + break + previous = cumulative_build - build + return (cumulative_build, previous) + + +def plot_geometry_info(axis: plt.Axes, mfile: MFile, scan: int): + """Function to plot geometry info + + Parameters + ---------- + axis : + axis object to plot to + mfile : + MFILE + scan : + scan number to use + """ + xmin = 0 + xmax = 1 + ymin = -16 + ymax = 1 + + draw_text(axis, -0.05, 1, "Geometry:", ha="left", va="center") + setup_axis(axis, xmin, xmax, ymin, ymax) + + in_blanket_thk = mfile.get("dr_shld_inboard", scan=scan) + mfile.get( + "dr_blkt_inboard", scan=scan + ) + out_blanket_thk = mfile.get("dr_shld_outboard", scan=scan) + mfile.get( + "dr_blkt_outboard", scan=scan + ) + + data = [ + ("rmajor", "$R_0$", "m"), + ("rminor", "a", "m"), + ("aspect", "A", ""), + ("kappa95", r"$\kappa_{95}$", ""), + ("triang95", r"$\delta_{95}$", ""), + ("a_plasma_surface", "Plasma surface area", "m$^2$"), + ("a_plasma_poloidal", "Plasma cross-sectional area", "m$^2$"), + ("vol_plasma", "Plasma volume", "m$^3$"), + ("n_tf_coils", "No. of TF coils", ""), + (in_blanket_thk, "Inboard blanket+shield", "m"), + ("dr_inboard_build", "Inboard build thickness", "m"), + (out_blanket_thk, "Outboard blanket+shield", "m"), + ] + + plot_info(axis, data, mfile, scan) + + +def plot_radial_build(axis: plt.Axes, mfile: MFile, colour_scheme: Literal[1, 2]): + """Plots the radial build of a fusion device on the given matplotlib axis. + + This function visualizes the different layers/components of the machine's radial + build + (such as central solenoid, toroidal field coils, vacuum vessel, shields, blankets, + etc.) + as a horizontal stacked bar chart. The thickness of each layer is extracted from the + provided `mfile`, and each segment is color-coded and labeled accordingly. + + If the toroidal field coil is inside the central solenoid (as indicated by the + "i_tf_inside_cs" flag in `mfile`), the order and labels of the components are + adjusted accordingly. + + Parameters + ---------- + axis : matplotlib.axes.Axes + The matplotlib axis on which to plot the radial build. + mfile : MFile + An object containing the machine build data, with required fields for each + radial component and the "i_tf_inside_cs" flag. + colour_scheme: + + Notes + ----- + This function modifies the provided axis in-place and does not return a value. + - Components with zero thickness are omitted from the plot. + - The legend displays the name and thickness (in meters) of each component. + """ + radial_variables = [ + "dr_bore", + "dr_cs", + "dr_cs_precomp", + "dr_cs_tf_gap", + "dr_tf_inboard", + "dr_tf_shld_gap", + "dr_shld_thermal_inboard", + "dr_shld_vv_gap_inboard", + "dr_vv_inboard", + "dr_shld_inboard", + "dr_shld_blkt_gap", + "dr_blkt_inboard", + "dr_fw_inboard", + "dr_fw_plasma_gap_inboard", + "rminor", + "dr_fw_plasma_gap_outboard", + "dr_fw_outboard", + "dr_blkt_outboard", + "dr_shld_blkt_gap", + "dr_vv_outboard", + "dr_shld_outboard", + "dr_shld_vv_gap_outboard", + "dr_shld_thermal_outboard", + "dr_tf_shld_gap", + "dr_tf_outboard", + ] + if int(mfile.get("i_tf_inside_cs", scan=-1)) == TFCSRadialConfiguration.TF_INSIDE_CS: + radial_variables[1] = "dr_tf_inboard" + radial_variables[2] = "dr_cs_tf_gap" + radial_variables[3] = "dr_cs" + radial_variables[4] = "dr_cs_precomp" + radial_variables[5] = "dr_tf_shld_gap" + + radial_build = [[mfile.get(rl, scan=-1) for rl in radial_variables]] + + radial_build = np.array(radial_build) + + for kk in range(radial_build.shape[0]): + radial_build[kk, 14] *= 2.0 + + radial_build = np.transpose(radial_build) + # ==================== + + radial_labels = [ + "Machine Bore", + "Central Solenoid", + "CS precompression", + "CS Coil gap", + "TF Coil Inboard Leg", + "TF Coil gap", + "Inboard Thermal Shield", + "Gap", + "Inboard VV", + "Inboard Shield", + "Gap", + "Inboard Blanket", + "Inboard First Wall", + "Inboard SOL", + "Plasma", + "Outboard SOL", + "Outboard First Wall", + "Outboard Blanket", + "Gap", + "Outboard VV", + "Outboard Shield", + "Gap", + "Outboard Thermal Shield", + "Gap", + "TF Coil Outboard Leg", + ] + if int(mfile.get("i_tf_inside_cs", scan=-1)) == TFCSRadialConfiguration.TF_INSIDE_CS: + radial_labels[1] = "TF Coil Inboard Leg" + radial_labels[2] = "CS Coil gap" + radial_labels[3] = "Central Solenoid" + radial_labels[4] = "CS precompression" + radial_labels[5] = "TF Coil gap" + + radial_color = [ + "white", + SOLENOID_COLOUR[colour_scheme - 1], + CSCOMPRESSION_COLOUR[colour_scheme - 1], + "white", + ( + TFC_COLOUR[colour_scheme - 1] + if mfile.get("i_tf_sup", scan=-1) != 0 + else "#b87333" + ), + "white", + THERMAL_SHIELD_COLOUR[colour_scheme - 1], + "white", + VESSEL_COLOUR[colour_scheme - 1], + SHIELD_COLOUR[colour_scheme - 1], + "white", + BLANKET_COLOUR[colour_scheme - 1], + FIRSTWALL_COLOUR[colour_scheme - 1], + "white", + PLASMA_COLOUR[colour_scheme - 1], + "white", + FIRSTWALL_COLOUR[colour_scheme - 1], + BLANKET_COLOUR[colour_scheme - 1], + "white", + VESSEL_COLOUR[colour_scheme - 1], + SHIELD_COLOUR[colour_scheme - 1], + "white", + THERMAL_SHIELD_COLOUR[colour_scheme - 1], + "white", + ( + TFC_COLOUR[colour_scheme - 1] + if mfile.get("i_tf_sup", scan=-1) != 0 + else "#b87333" + ), + ] + if int(mfile.get("i_tf_inside_cs", scan=-1)) == TFCSRadialConfiguration.TF_INSIDE_CS: + radial_color[1] = ( + TFC_COLOUR[colour_scheme - 1] + if mfile.get("i_tf_sup", scan=-1) != 0 + else "#b87333" + ) + radial_color[2] = "white" + radial_color[3] = SOLENOID_COLOUR[colour_scheme - 1] + radial_color[4] = CSCOMPRESSION_COLOUR[colour_scheme - 1] + radial_color[5] = "white" + + lower = np.zeros(radial_build.shape[1]) + for kk in range(radial_build.shape[0]): + axis.barh( + 0, + radial_build[kk, :], + left=lower, + height=0.8, + label=( + f"{radial_labels[kk]}\n[{radial_variables[kk]}]\n{radial_build[kk][0]:.3f} m" # noqa: E501 + ), + color=radial_color[kk], + edgecolor="black", + linewidth=0.05, + ) + lower += radial_build[kk, :] + + axis.set_yticks([]) + + axis.legend( + bbox_to_anchor=(0.5, -0.1), + loc="upper center", + ncol=5, + ) + # Plot a vertical dashed line at rmajor + axis.axvline( + mfile.get("rmajor", scan=-1), + color="black", + linestyle="--", + linewidth=1.2, + label="Major Radius $R_0$", + ) + axis.minorticks_on() + axis.set_xlabel("Radius [m]") + + +__all__ = [ + "cumulative_radial_build", + "cumulative_radial_build2", + "plot_geometry_info", + "plot_radial_build", +] diff --git a/process/core/io/plot/summary/geometry/misc.py b/process/core/io/plot/summary/geometry/misc.py new file mode 100644 index 0000000000..6121ff64b2 --- /dev/null +++ b/process/core/io/plot/summary/geometry/misc.py @@ -0,0 +1,385 @@ +"""Geometry functions for PROCESS summary plots.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +import numpy as np + +from process.core.io.plot.summary.common import ( + box_style, + text_layout, +) +from process.core.io.plot.summary.geometry.poloidal import ( + plot_blanket, + plot_firstwall, +) +from process.core.io.plot.summary.plasma import ( + plot_plasma, +) +from process.core.io.plot.summary.reporting import ( + draw_bend, +) +from process.data_structure.physics_variables import DivertorNumberModels + +if TYPE_CHECKING: + import matplotlib.pyplot as plt + + from process.core.io.mfile import MFile + + +def plot_blkt_pipe_bends(fig, m_file, scan: int): + """Plot the blanket pipe bends on the given axis, with axes in mm. + + Parameters + ---------- + fig : + + m_file : + + scan: int : + + """ + ax_90 = fig.add_subplot(341) + ax_180 = fig.add_subplot(342) + + r = m_file.get("radius_blkt_channel", scan=scan) + fallback_radius = 0.1 # meters + + elbow_radius_90 = ( + m_file.get("radius_blkt_channel_90_bend", scan=scan) or fallback_radius + ) + elbow_radius_180 = ( + m_file.get("radius_blkt_channel_180_bend", scan=scan) or fallback_radius + ) + + draw_bend(ax_90, elbow_radius_90, np.pi / 2, r, title="Blanket Pipe 90° Bend") + draw_bend(ax_180, elbow_radius_180, np.pi, r, title="Blanket Pipe 180° Bend") + + +def plot_blkt_structure( + ax: plt.Axes, + fig: plt.Figure, + m_file: MFile, + scan: int, + radial_build: dict[str, float], + colour_scheme: Literal[1, 2], +): + """Plot the blkt structure and relevant angles""" + # MFILE variables needed to plot the blkt structure and angles + rmajor = m_file.get("rmajor", scan=scan) + rminor = m_file.get("rminor", scan=scan) + dr_fw_plasma_gap_outboard = m_file.get("dr_fw_plasma_gap_outboard", scan=scan) + dr_fw_plasma_gap_inboard = m_file.get("dr_fw_plasma_gap_inboard", scan=scan) + dr_fw_inboard = m_file.get("dr_fw_inboard", scan=scan) + dr_fw_outboard = m_file.get("dr_fw_outboard", scan=scan) + dr_blkt_outboard = m_file.get("dr_blkt_outboard", scan=scan) + dr_blkt_inboard = m_file.get("dr_blkt_inboard", scan=scan) + dz_blkt_half = m_file.get("dz_blkt_half", scan=scan) + deg_blkt_outboard_poloidal_plasma = m_file.get( + "deg_blkt_outboard_poloidal_plasma", scan=scan + ) + deg_blkt_inboard_poloidal_plasma = m_file.get( + "deg_blkt_inboard_poloidal_plasma", scan=scan + ) + f_deg_blkt_outboard_poloidal_plasma = m_file.get( + "f_deg_blkt_outboard_poloidal_plasma", scan=scan + ) + f_deg_blkt_inboard_poloidal_plasma = m_file.get( + "f_deg_blkt_inboard_poloidal_plasma", scan=scan + ) + deg_div_poloidal_plasma = m_file.get("deg_div_poloidal_plasma", scan=scan) + f_ster_div_single = m_file.get("f_ster_div_single", scan=scan) + i_single_null = m_file.get("i_single_null", scan=scan) + + # ====================== + + plot_blanket(ax, m_file, scan, radial_build, colour_scheme) + plot_plasma(ax, m_file, scan, colour_scheme) + plot_firstwall(ax, m_file, scan, radial_build, colour_scheme) + + ax.set_xlabel("Radial position [m]") + ax.set_ylabel("Vertical position [m]") + ax.set_title("Blanket and First Wall Poloidal Cross-Section") + ax.minorticks_on() + ax.grid(which="minor", linestyle=":", linewidth=0.5, alpha=0.5) + + r_blkt_outboard_out = ( + rmajor + rminor + dr_fw_outboard + dr_fw_plasma_gap_outboard + dr_blkt_outboard + ) + r_blkt_inboard_in = ( + rmajor - rminor - dr_fw_plasma_gap_inboard - dr_fw_inboard - dr_blkt_inboard + ) + r_fw_outboard_in = r_blkt_outboard_out - dr_blkt_outboard - dr_fw_outboard + r_fw_inboard_out = r_blkt_inboard_in + dr_blkt_inboard + dr_fw_inboard + + # Plot a horizontal line at dz_blkt_half (blanket half height) + for dz_blkt in (dz_blkt_half, -dz_blkt_half): + ax.axhline( + dz_blkt, + color="purple", + linestyle="--", + linewidth=1.5, + label="Blanket Half Height", + ) + + if DivertorNumberModels(i_single_null) == DivertorNumberModels.DOUBLE_NULL: + # Plot arrows for the outboard blanket angles + ax.annotate( + "", + xy=(rmajor, 0), + xytext=(rmajor, dz_blkt_half), + arrowprops={"arrowstyle": "<-", "color": "purple"}, + zorder=5, + ) + # If single null then only plot the lower arrow for the outboard blanket angle + ax.annotate( + "", + xy=(rmajor, 0), + xytext=(rmajor, -dz_blkt_half), + arrowprops={"arrowstyle": "<-", "color": "purple"}, + zorder=5, + ) + + # Plot arc showing the angle between the two outboard blanket arrows + arc_radius = 1.0 + + # 3 to 6 o'clock position is -90 degrees, + angle_start = -90.0 + match DivertorNumberModels(i_single_null): + case DivertorNumberModels.SINGLE_NULL: + angle_end = 90.0 + deg_div_poloidal_plasma + case DivertorNumberModels.DOUBLE_NULL: + # 3 to 12 o'clock position is +90 degrees + angle_end = 90.0 + + theta = np.linspace(np.deg2rad(angle_start), np.deg2rad(angle_end), 50) + arc_x = rmajor + arc_radius * np.cos(theta) + arc_y = arc_radius * np.sin(theta) + + ax.plot(arc_x, arc_y, color="purple", linewidth=2) + + # Add angle label at the arc + mid_angle = np.deg2rad((angle_start + angle_end) / 2) + label_radius = arc_radius * 1.8 + label_x = rmajor + label_radius * np.cos(mid_angle) + label_y = label_radius * np.sin(mid_angle) + + # Plot the info box for the outboard blanket + ax.text( + label_x, + label_y, + f"{deg_blkt_outboard_poloidal_plasma:.1f}°\n({f_deg_blkt_outboard_poloidal_plasma * 100:.1f}%)", # noqa: E501 + fontsize=7, + color="purple", + ha="center", + va="center", + weight="bold", + bbox={ + "boxstyle": "round", + "facecolor": "white", + "alpha": 0.8, + "edgecolor": "purple", + "linewidth": 1.5, + }, + ) + + # Plot arrows for the inboard blanket angles + for dz_blkt in (dz_blkt_half, -dz_blkt_half): + ax.annotate( + "", + xy=(rmajor, 0), + xytext=(r_fw_inboard_out, dz_blkt), + arrowprops={"arrowstyle": "<-", "color": "green"}, + zorder=5, + ) + + # Plot arc showing the angle between the two inboard blanket arrows + arc_radius = 1.0 + angle_start = -deg_blkt_inboard_poloidal_plasma / 2 + angle_end = deg_blkt_inboard_poloidal_plasma / 2 + + theta = np.linspace(np.deg2rad(angle_start), np.deg2rad(angle_end), 50) + arc_x = rmajor - arc_radius * np.cos(theta) + arc_y = arc_radius * np.sin(theta) + + ax.plot(arc_x, arc_y, color="green", linewidth=2) + + # Add angle label at the arc + mid_angle = np.deg2rad((angle_start + angle_end) / 2) + label_radius = arc_radius * 1.8 + label_x = rmajor - label_radius * np.cos(mid_angle) + label_y = label_radius * np.sin(mid_angle) + + # Plot the info box for the inboard blanket + ax.text( + label_x, + label_y, + f"{deg_blkt_inboard_poloidal_plasma:.1f}°\n({f_deg_blkt_inboard_poloidal_plasma * 100:.1f}%)", # noqa: E501 + fontsize=7, + color="green", + ha="center", + va="center", + weight="bold", + bbox={ + "boxstyle": "round", + "facecolor": "white", + "alpha": 0.8, + "edgecolor": "green", + "linewidth": 1.5, + }, + zorder=5, + ) + + # Plot arrows for the divertor angles + # If double null then plot the upper also + if DivertorNumberModels(i_single_null) == DivertorNumberModels.DOUBLE_NULL: + # Plot arc showing the angle between the two arrows (divertor angle) + arc_radius = 1.5 + # 3 to 12 o'clock position is +90 degrees, + angle_start = 90.0 + angle_end = 90.0 + deg_div_poloidal_plasma + + theta = np.linspace(np.deg2rad(angle_start), np.deg2rad(angle_end), 50) + arc_x = rmajor + arc_radius * np.cos(theta) + arc_y = arc_radius * np.sin(theta) + + ax.plot(arc_x, arc_y, color="black", linewidth=2) + + # Add angle label at the arc + mid_angle = np.deg2rad((angle_start + angle_end) / 2) + label_radius = arc_radius * 1.8 + label_x = rmajor + label_radius * np.cos(mid_angle) + label_y = label_radius * np.sin(mid_angle) + + ax.text( + label_x, + label_y, + f"{deg_div_poloidal_plasma:.1f}°\n({f_ster_div_single * 100:.1f}%)", + fontsize=7, + color="black", + ha="center", + va="center", + weight="bold", + bbox={ + "boxstyle": "round", + "facecolor": "white", + "alpha": 0.8, + "edgecolor": "black", + "linewidth": 1.5, + }, + zorder=5, + ) + + # Plot arc showing the angle between the two arrows for the lower divertor (divertor + # angle) + arc_radius = 1.5 + # 3 to 6 o'clock is -90 degrees + angle_start = -90.0 + angle_end = angle_start - deg_div_poloidal_plasma + + theta = np.linspace(np.deg2rad(angle_start), np.deg2rad(angle_end), 50) + arc_x = rmajor + arc_radius * np.cos(theta) + arc_y = arc_radius * np.sin(theta) + + ax.plot(arc_x, arc_y, color="black", linewidth=2) + + # Add angle label at the arc + mid_angle = np.deg2rad((angle_start + angle_end) / 2) + label_radius = arc_radius * 1.8 + label_x = rmajor + label_radius * np.cos(mid_angle) + label_y = label_radius * np.sin(mid_angle) + + # Plot the info box for the lower divertor angle + ax.text( + label_x, + label_y, + f"{deg_div_poloidal_plasma:.1f}°\n({f_ster_div_single * 100:.1f}%)", + fontsize=7, + color="black", + ha="center", + va="center", + weight="bold", + bbox={ + "boxstyle": "round", + "facecolor": "white", + "alpha": 0.8, + "edgecolor": "black", + "linewidth": 1.5, + }, + zorder=5, + ) + + # Plot vertical lines at the inner and outer radial boundaries of the blanket + linestyle = { + "color": "black", + "linestyle": "--", + "linewidth": 1.5, + "zorder": 10, + } + ax.axvline(r_blkt_inboard_in, **linestyle) + ax.axvline(r_blkt_outboard_out, **linestyle) + ax.axvline(r_fw_inboard_out, **linestyle) + ax.axvline(r_fw_outboard_in, **linestyle) + + ax.axvline( + rmajor, + color="black", + linestyle="--", + linewidth=1.5, + label="Major Radius $R_0$", + ) + + # Plot midplane line (horizontal dashed line at Z=0) + ax.axhline(0.0, color="black", linestyle="--", linewidth=1.5, label="Midplane") + + textstr_blkt_areas = ( + "$\\mathbf{Blanket \\ Areas:}$\n\nInboard blanket, with holes and" + f" gaps: {m_file.get('a_blkt_inboard_surface', scan=scan):,.3f}" + " $\\text{m}^2$\nOutboard blanket, with holes and gaps:" + f" {m_file.get('a_blkt_outboard_surface', scan=scan):,.3f}" + " $\\text{m}^2$\nTotal blanket, with holes and gaps:" + f" {m_file.get('a_blkt_total_surface', scan=scan):,.3f}" + " $\\text{m}^2$\n\nInboard blanket, full coverage:" + f" {m_file.get('a_blkt_inboard_surface_full_coverage', scan=scan):,.3f}" + " $\\text{m}^2$\nOutboard blanket, full coverage:" + f" {m_file.get('a_blkt_outboard_surface_full_coverage', scan=scan):,.3f}" + " $\\text{m}^2$\nTotal blanket, full coverage:" + f" {m_file.get('a_blkt_total_surface_full_coverage', scan=scan):,.3f}" + " $\\text{m}^2$ " + ) + + ax.text( + 0.05, + 0.3, + textstr_blkt_areas, + **text_layout(fig), + bbox=box_style("wheat"), + ) + + textstr_blkt_volumes = ( + "$\\mathbf{Blanket \\ Volumes:}$\n\nInboard blanket, with holes and" + f" gaps: {m_file.get('vol_blkt_inboard', scan=scan):,.3f}" + " $\\text{m}^3$\nOutboard blanket, with holes and gaps:" + f" {m_file.get('vol_blkt_outboard', scan=scan):,.3f}" + " $\\text{m}^3$\nTotal blanket, with holes and gaps:" + f" {m_file.get('vol_blkt_total', scan=scan):,.3f}" + " $\\text{m}^3$\n\nInboard blanket, full coverage:" + f" {m_file.get('vol_blkt_inboard_full_coverage', scan=scan):,.3f}" + " $\\text{m}^3$\nOutboard blanket, full coverage:" + f" {m_file.get('vol_blkt_outboard_full_coverage', scan=scan):,.3f}" + " $\\text{m}^3$\nTotal blanket, full coverage:" + f" {m_file.get('vol_blkt_total_full_coverage', scan=scan):,.3f}" + " $\\text{m}^3$ " + ) + + ax.text( + 0.05, + 0.05, + textstr_blkt_volumes, + **text_layout(fig), + bbox=box_style("wheat"), + ) + + +__all__ = ["plot_blkt_pipe_bends", "plot_blkt_structure"] diff --git a/process/core/io/plot/summary/geometry/poloidal.py b/process/core/io/plot/summary/geometry/poloidal.py new file mode 100644 index 0000000000..18994beb7c --- /dev/null +++ b/process/core/io/plot/summary/geometry/poloidal.py @@ -0,0 +1,1062 @@ +"""Geometry functions for PROCESS summary plots.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +import matplotlib.pyplot as plt +import numpy as np +from matplotlib import patches + +from process.core.io.plot.summary.constants import ( + BLANKET_COLOUR, + CRYOSTAT_COLOUR, + FIRSTWALL_COLOUR, + SHIELD_COLOUR, + VESSEL_COLOUR, + thin, +) +from process.core.io.plot.summary.geometry.build import ( + cumulative_radial_build, +) +from process.core.io.plot.summary.magnets import ( + plot_pf_coils, + plot_tf_coils, +) +from process.core.io.plot.summary.plasma import ( + plot_plasma, +) +from process.core.io.plot.summary.rendering import ( + draw_annotation, + draw_text, +) +from process.core.io.plot.summary.reporting import ( + plot_centre_cross, +) +from process.data_structure.physics_variables import DivertorNumberModels +from process.models.geometry.blanket import ( + blanket_geometry_double_null, + blanket_geometry_single_null, +) +from process.models.geometry.cryostat import cryostat_geometry +from process.models.geometry.firstwall import ( + first_wall_geometry_double_null, + first_wall_geometry_single_null, +) +from process.models.geometry.shield import ( + shield_geometry_double_null, + shield_geometry_single_null, +) +from process.models.geometry.vacuum_vessel import ( + vacuum_vessel_geometry_double_null, + vacuum_vessel_geometry_single_null, +) + +if TYPE_CHECKING: + from process.core.io.mfile import MFile + from process.core.io.plot.summary.reporting import ( + RadialBuild, + ) + + +def poloidal_cross_section( + axis: plt.Axes, + mfile: MFile, + scan: int, + demo_ranges: bool, + radial_build: RadialBuild, + colour_scheme: Literal[1, 2], +): + """Function to plot poloidal cross-section + + Parameters + ---------- + axis : + axis object to add plot to + mfile : + MFILE data object + scan : + scan number to use + demo_ranges: + + colour_scheme : + colour scheme to use for plots + """ + axis.set_xlabel("R [m]") + axis.set_ylabel("Z [m]") + axis.set_title("Poloidal Cross-Section") + axis.minorticks_on() + axis.grid(which="both", linestyle="--", linewidth=0.5, alpha=0.2) + + plot_vacuum_vessel_and_divertor(axis, mfile, scan, radial_build, colour_scheme) + plot_shield(axis, mfile, scan, radial_build, colour_scheme) + plot_blanket(axis, mfile, scan, radial_build, colour_scheme) + plot_firstwall(axis, mfile, scan, radial_build, colour_scheme) + + plot_plasma(axis, mfile, scan, colour_scheme) + plot_centre_cross(axis, mfile, scan) + plot_cryostat(axis, mfile, scan, colour_scheme) + + plot_tf_coils(axis, mfile, scan, colour_scheme) + plot_pf_coils(axis, mfile, scan, colour_scheme) + + if demo_ranges: + axis.set_ylim(-15, 15) + axis.set_xlim(0, 20) + + else: + axis.set_xlim(0, axis.get_xlim()[1]) + + +def plot_full_machine_poloidal_cross_section( + axis: plt.Axes, + mfile: MFile, + scan: int, + radial_build: RadialBuild, + colour_scheme: Literal[1, 2], +): + """Function to plot full machine poloidal cross-section, including mirrored negative + x-axis + + Parameters + ---------- + axis : + axis object to add plot to + mfile : + MFILE data object + scan : + scan number to use + radial_build : + radial build data + colour_scheme : + colour scheme to use for plots + """ + plot_vacuum_vessel_and_divertor(axis, mfile, scan, radial_build, colour_scheme) + plot_vacuum_vessel_and_divertor( + axis, mfile, scan, radial_build, colour_scheme, mirror_negative_x=True + ) + plot_shield(axis, mfile, scan, radial_build, colour_scheme) + plot_shield(axis, mfile, scan, radial_build, colour_scheme, mirror_negative_x=True) + + plot_blanket(axis, mfile, scan, radial_build, colour_scheme) + plot_blanket(axis, mfile, scan, radial_build, colour_scheme, mirror_negative_x=True) + plot_firstwall(axis, mfile, scan, radial_build, colour_scheme) + plot_firstwall( + axis, mfile, scan, radial_build, colour_scheme, mirror_negative_x=True + ) + plot_plasma(axis, mfile, scan, colour_scheme) + plot_plasma(axis, mfile, scan, colour_scheme, mirror_negative_x=True) + plot_centre_cross(axis, mfile, scan) + plot_centre_cross(axis, mfile, scan, mirror_negative_x=True) + plot_cryostat(axis, mfile, scan, colour_scheme) + plot_cryostat(axis, mfile, scan, colour_scheme, mirror_negative_x=True) + plot_tf_coils(axis, mfile, scan, colour_scheme) + plot_tf_coils(axis, mfile, scan, colour_scheme, mirror_negative_x=True) + plot_pf_coils(axis, mfile, scan, colour_scheme) + plot_pf_coils(axis, mfile, scan, colour_scheme, mirror_negative_x=True) + + axis.set_xlabel("R [m]") + axis.set_ylabel("Z [m]") + axis.set_aspect("equal") + axis.minorticks_on() + axis.grid(which="minor", linestyle=":", linewidth=0.5, alpha=0.5) + + +def plot_cryostat( + axis: plt.Axes, + mfile: MFile, + scan: int, + colour_scheme: Literal[1, 2], + mirror_negative_x: bool = False, +): + """Function to plot cryostat in poloidal cross-section + + Parameters + ---------- + axis : plt.Axes + axis object to plot to + mfile : MFile + MFILE data object + scan : int + scan number to use + colour_scheme : Literal[1, 2] + colour scheme to use for plots + mirror_negative_x : bool + if True, mirror the plot to the negative x-axis (Default value = False) + """ + rects = cryostat_geometry( + r_cryostat_inboard=mfile.get("r_cryostat_inboard", scan=scan), + dr_cryostat=mfile.get("dr_cryostat", scan=scan), + z_cryostat_half_inside=mfile.get("z_cryostat_half_inside", scan=scan), + ) + + # Apply mirror transformation if requested + x_scale = -1 if mirror_negative_x else 1 + + for rec in rects: + axis.add_patch( + patches.Rectangle( + xy=(x_scale * rec.anchor_x, rec.anchor_z), + width=x_scale * rec.width, + height=rec.height, + facecolor=CRYOSTAT_COLOUR[colour_scheme - 1], + ) + ) + + +def plot_vacuum_vessel_and_divertor( + axis, + mfile: MFile, + scan, + radial_build, + colour_scheme, + mirror_negative_x: bool = False, +): + """Function to plot vacuum vessel and divertor boxes + + Parameters + ---------- + axis : + axis object to plot to + mfile : + MFILE data object + scan : + scan number to use + radial_build : + + colour_scheme : + colour scheme to use for plots + mirror_negative_x : + if True, mirror the plot to the negative x-axis (Default value = False) + """ + cumulative_upper = radial_build.cumulative_upper + cumulative_lower = radial_build.cumulative_lower + upper = radial_build.upper + lower = radial_build.lower + + i_single_null = int(mfile.get("i_single_null", scan=scan)) + triang_95 = mfile.get("triang95", scan=scan) + dz_divertor = mfile.get("dz_divertor", scan=scan) + dz_xpoint_divertor = mfile.get("dz_xpoint_divertor", scan=scan) + kappa = mfile.get("kappa", scan=scan) + rminor = mfile.get("rminor", scan=scan) + dr_vv_inboard = mfile.get("dr_vv_inboard", scan=scan) + dr_vv_outboard = mfile.get("dr_vv_outboard", scan=scan) + dr_shld_inboard = mfile.get("dr_shld_inboard", scan=scan) + dr_shld_outboard = mfile.get("dr_shld_outboard", scan=scan) + dr_blkt_inboard = mfile.get("dr_blkt_inboard", scan=scan) + dr_blkt_outboard = mfile.get("dr_blkt_outboard", scan=scan) + + # Outer side (furthest from plasma) + radx_outer = ( + cumulative_radial_build("dr_vv_outboard", mfile, scan) + + cumulative_radial_build("dr_shld_vv_gap_inboard", mfile, scan) + ) / 2.0 + rminx_outer = ( + cumulative_radial_build("dr_vv_outboard", mfile, scan) + - cumulative_radial_build("dr_shld_vv_gap_inboard", mfile, scan) + ) / 2.0 + + # Inner side (nearest to the plasma) + radx_inner = ( + cumulative_radial_build("dr_shld_outboard", mfile, scan) + + cumulative_radial_build("dr_vv_inboard", mfile, scan) + ) / 2.0 + rminx_inner = ( + cumulative_radial_build("dr_shld_outboard", mfile, scan) + - cumulative_radial_build("dr_vv_inboard", mfile, scan) + ) / 2.0 + + z_divertor_lower_top = (-kappa * rminor) - dz_xpoint_divertor + z_divertor_lower_bottom = z_divertor_lower_top - dz_divertor + + # Apply mirror transformation if requested + x_scale = -1 if mirror_negative_x else 1 + + match DivertorNumberModels(i_single_null): + case DivertorNumberModels.SINGLE_NULL: + z_divertor_upper_bottom = None + z_divertor_upper_top = None + vvg_single_null = vacuum_vessel_geometry_single_null( + cumulative_upper=cumulative_upper, + upper=upper, + triang=triang_95, + radx_outer=radx_outer, + rminx_outer=rminx_outer, + radx_inner=radx_inner, + rminx_inner=rminx_inner, + cumulative_lower=cumulative_lower, + lower=lower, + ) + + axis.plot( + x_scale * np.array(vvg_single_null.rs), + vvg_single_null.zs, + color="black", + lw=thin, + zorder=5, + ) + + axis.fill( + x_scale * np.array(vvg_single_null.rs), + vvg_single_null.zs, + color=VESSEL_COLOUR[colour_scheme - 1], + lw=0.01, + zorder=5, + ) + + # Find indices where vessel boundary is between z_divertor_bottom and + # z_divertor_top + # Find the min and max R values of the vessel boundary between the divertor + # lines + mask = (vvg_single_null.zs >= z_divertor_lower_bottom) & ( + vvg_single_null.zs <= z_divertor_lower_top + ) + # Get the min/max R for the region between the divertor lines + r_min = ( + np.min(vvg_single_null.rs[mask]) + + dr_vv_inboard + + dr_shld_inboard + + (dr_blkt_inboard * 0.5) + ) + r_max = ( + np.max(vvg_single_null.rs[mask]) + - dr_vv_outboard + - dr_shld_outboard + - (dr_blkt_outboard * 0.5) + ) + # Draw a rectangle (box) between the two lines and inside the vessel + axis.add_patch( + patches.Rectangle( + ( + x_scale * r_min, + z_divertor_lower_bottom, + ), + x_scale * (r_max - r_min), + z_divertor_lower_top - z_divertor_lower_bottom, + facecolor="black", + alpha=0.8, + zorder=1, + ) + ) + + case DivertorNumberModels.DOUBLE_NULL: + z_divertor_upper_bottom = (kappa * rminor) + dz_xpoint_divertor + z_divertor_upper_top = z_divertor_upper_bottom + dz_divertor + vvg_double_null = vacuum_vessel_geometry_double_null( + cumulative_lower=cumulative_lower, + lower=lower, + radx_inner=radx_inner, + radx_outer=radx_outer, + rminx_inner=rminx_inner, + rminx_outer=rminx_outer, + triang=triang_95, + ) + axis.plot( + x_scale * np.array(vvg_double_null.rs), + vvg_double_null.zs, + color="black", + lw=thin, + zorder=5, + ) + + axis.fill( + x_scale * np.array(vvg_double_null.rs), + vvg_double_null.zs, + color=VESSEL_COLOUR[colour_scheme - 1], + lw=0.01, + zorder=5, + ) + + # Plot lower divertor + # Find indices where vessel boundary is between z_divertor_bottom and + # z_divertor_top + # Find the min and max R values of the vessel boundary between the divertor + # lines + mask = (vvg_double_null.zs >= z_divertor_lower_bottom) & ( + vvg_double_null.zs <= z_divertor_lower_top + ) + # Get the min/max R for the region between the divertor lines + r_min = ( + np.min(vvg_double_null.rs[mask]) + + dr_vv_inboard + + dr_shld_inboard + + (dr_blkt_inboard * 0.5) + ) + r_max = ( + np.max(vvg_double_null.rs[mask]) + - dr_vv_outboard + - dr_shld_outboard + - (dr_blkt_outboard * 0.5) + ) + # Draw a rectangle (box) between the two lines and inside the vessel + axis.add_patch( + patches.Rectangle( + ( + x_scale * r_min, + z_divertor_lower_bottom, + ), + x_scale * (r_max - r_min), + z_divertor_lower_top - z_divertor_lower_bottom, + facecolor="black", + alpha=0.8, + zorder=1, + ) + ) + # Plot upper divertor + # Find indices where vessel boundary is between z_divertor_bottom and + # z_divertor_top + # Find the min and max R values of the vessel boundary between the divertor + # lines + mask = (vvg_double_null.zs >= z_divertor_upper_bottom) & ( + vvg_double_null.zs <= z_divertor_upper_top + ) + # Get the min/max R for the region between the divertor lines + r_min = ( + np.min(vvg_double_null.rs[mask]) + + dr_vv_inboard + + dr_shld_inboard + + (dr_blkt_inboard * 0.5) + ) + r_max = ( + np.max(vvg_double_null.rs[mask]) + - dr_vv_outboard + - dr_shld_outboard + - (dr_blkt_outboard * 0.5) + ) + # Draw a rectangle (box) between the two lines and inside the vessel + axis.add_patch( + patches.Rectangle( + ( + x_scale * r_min, + z_divertor_upper_bottom, + ), + x_scale * (r_max - r_min), + z_divertor_upper_top - z_divertor_upper_bottom, + facecolor="black", + alpha=0.8, + zorder=1, + ) + ) + + +def plot_shield( + axis: plt.Axes, + mfile: MFile, + scan: int, + radial_build, + colour_scheme, + mirror_negative_x: bool = False, +): + """Function to plot shield + + Parameters + ---------- + axis : + axis object to plot to + mfile : + MFILE data object + scan : + scan number to use + radial_build : + + colour_scheme : + colour scheme to use for plots + mirror_negative_x : + if True, mirror the plot to the negative x-axis (Default value = False) + + """ + cumulative_upper = radial_build.cumulative_upper + cumulative_lower = radial_build.cumulative_lower + + i_single_null = mfile.get("i_single_null", scan=scan) + triang_95 = mfile.get("triang95", scan=scan) + + # Side furthest from plasma + radx_far = ( + cumulative_radial_build("dr_shld_outboard", mfile, scan) + + cumulative_radial_build("dr_vv_inboard", mfile, scan) + ) / 2.0 + rminx_far = ( + cumulative_radial_build("dr_shld_outboard", mfile, scan) + - cumulative_radial_build("dr_vv_inboard", mfile, scan) + ) / 2.0 + + # Side nearest to the plasma + radx_near = ( + cumulative_radial_build("vvblgapo", mfile, scan) + + cumulative_radial_build("dr_shld_inboard", mfile, scan) + ) / 2.0 + rminx_near = ( + cumulative_radial_build("vvblgapo", mfile, scan) + - cumulative_radial_build("dr_shld_inboard", mfile, scan) + ) / 2.0 + + # Apply mirror transformation if requested + x_scale = -1 if mirror_negative_x else 1 + + match DivertorNumberModels(i_single_null): + case DivertorNumberModels.SINGLE_NULL: + shield_geometry = shield_geometry_single_null( + cumulative_upper=cumulative_upper, + radx_far=radx_far, + rminx_far=rminx_far, + radx_near=radx_near, + rminx_near=rminx_near, + triang=triang_95, + cumulative_lower=cumulative_lower, + ) + case DivertorNumberModels.DOUBLE_NULL: + shield_geometry = shield_geometry_double_null( + cumulative_lower=cumulative_lower, + radx_far=radx_far, + radx_near=radx_near, + rminx_far=rminx_far, + rminx_near=rminx_near, + triang=triang_95, + ) + + axis.plot( + x_scale * np.array(shield_geometry.rs), + shield_geometry.zs, + color="black", + lw=thin, + ) + axis.fill( + x_scale * np.array(shield_geometry.rs), + shield_geometry.zs, + color=SHIELD_COLOUR[colour_scheme - 1], + lw=0.01, + ) + + +def plot_blanket( + axis: plt.Axes, + mfile: MFile, + scan, + radial_build, + colour_scheme, + mirror_negative_x: bool = False, +): + """Function to plot blanket + + Parameters + ---------- + axis : + axis object to plot to + mfile : + MFILE + scan : + scan number to use + radial_build : + + colour_scheme : + colour scheme to use for plots + mirror_negative_x : + if True, mirror the plot to the negative x-axis (Default value = False) + """ + cumulative_upper = radial_build.cumulative_upper + cumulative_lower = radial_build.cumulative_lower + + dr_blkt_inboard = mfile.get("dr_blkt_inboard", scan=scan) + dr_blkt_outboard = mfile.get("dr_blkt_outboard", scan=scan) + # Single null: Draw top half from output + # Double null: Reflect bottom half to top + i_single_null = mfile.get("i_single_null", scan=scan) + triang_95 = mfile.get("triang95", scan=scan) + if int(i_single_null) == 1: + dz_blkt_upper = mfile.get("dz_blkt_upper", scan=scan) + else: + dz_blkt_upper = 0.0 + + c_shldith = cumulative_radial_build("dr_shld_inboard", mfile, scan) + c_blnkoth = cumulative_radial_build("dr_blkt_outboard", mfile, scan) + + # Apply mirror transformation if requested + x_scale = -1 if mirror_negative_x else 1 + + match DivertorNumberModels(i_single_null): + case DivertorNumberModels.SINGLE_NULL: + # Upper blanket: outer surface + radx_outer = ( + cumulative_radial_build("dr_blkt_outboard", mfile, scan) + + cumulative_radial_build("vvblgapi", mfile, scan) + ) / 2.0 + rminx_outer = ( + cumulative_radial_build("dr_blkt_outboard", mfile, scan) + - cumulative_radial_build("vvblgapi", mfile, scan) + ) / 2.0 + + # Upper blanket: inner surface + radx_inner = ( + cumulative_radial_build("dr_fw_outboard", mfile, scan) + + cumulative_radial_build("dr_blkt_inboard", mfile, scan) + ) / 2.0 + rminx_inner = ( + cumulative_radial_build("dr_fw_outboard", mfile, scan) + - cumulative_radial_build("dr_blkt_inboard", mfile, scan) + ) / 2.0 + bg_single_null = blanket_geometry_single_null( + radx_outer=radx_outer, + rminx_outer=rminx_outer, + radx_inner=radx_inner, + rminx_inner=rminx_inner, + cumulative_upper=cumulative_upper, + triang=triang_95, + cumulative_lower=cumulative_lower, + dz_blkt_upper=dz_blkt_upper, + c_shldith=c_shldith, + c_blnkoth=c_blnkoth, + dr_blkt_inboard=dr_blkt_inboard, + dr_blkt_outboard=dr_blkt_outboard, + ) + + # Plot blanket + axis.plot( + x_scale * np.array(bg_single_null.rs), + bg_single_null.zs, + color="black", + lw=thin, + zorder=5, + ) + + axis.fill( + x_scale * np.array(bg_single_null.rs), + bg_single_null.zs, + color=BLANKET_COLOUR[colour_scheme - 1], + lw=0.01, + zorder=5, + ) + + case DivertorNumberModels.DOUBLE_NULL: + bg_double_null = blanket_geometry_double_null( + cumulative_lower=cumulative_lower, + triang=triang_95, + dz_blkt_upper=dz_blkt_upper, + c_shldith=c_shldith, + c_blnkoth=c_blnkoth, + dr_blkt_inboard=dr_blkt_inboard, + dr_blkt_outboard=dr_blkt_outboard, + ) + # Plot blanket + axis.plot( + x_scale * np.array(bg_double_null.rs[0]), + bg_double_null.zs[0], + color="black", + lw=thin, + ) + axis.fill( + x_scale * np.array(bg_double_null.rs[0]), + bg_double_null.zs[0], + color=BLANKET_COLOUR[colour_scheme - 1], + lw=0.01, + zorder=5, + ) + if dr_blkt_inboard > 0.0: + # only plot inboard blanket if inboard blanket thickness > 0 + axis.plot( + x_scale * np.array(bg_double_null.rs[1]), + bg_double_null.zs[1], + color="black", + lw=thin, + zorder=5, + ) + axis.fill( + x_scale * np.array(bg_double_null.rs[1]), + bg_double_null.zs[1], + color=BLANKET_COLOUR[colour_scheme - 1], + lw=0.01, + zorder=5, + ) + + +def plot_first_wall_top_down_cross_section(axis: plt.Axes, mfile: MFile, scan: int): + """Plot first wall top down cross-section""" + # Import required variables + radius_fw_channel = mfile.get("radius_fw_channel", scan=scan) * 100 + dr_fw_wall = mfile.get("dr_fw_wall", scan=scan) * 100 + dx_fw_module = mfile.get("dx_fw_module", scan=scan) * 100 + + # Flot first module + axis.add_patch( + patches.Rectangle( + xy=(0, 0), + width=dx_fw_module, + height=2 * (dr_fw_wall + radius_fw_channel), + edgecolor="black", + facecolor="gray", + ) + ) + + # Plot cooling channel in first module + axis.add_patch( + patches.Circle( + xy=(dx_fw_module / 2, dr_fw_wall + radius_fw_channel), + radius=radius_fw_channel, + edgecolor="black", + facecolor="#b87333", + ) + ) + + # Plot second module + axis.add_patch( + patches.Rectangle( + xy=(dx_fw_module, 0), + width=dx_fw_module, + height=2 * (dr_fw_wall + radius_fw_channel), + edgecolor="black", + facecolor="gray", + ) + ) + + # Plot cooling channel in second module + axis.add_patch( + patches.Circle( + xy=( + dx_fw_module + dx_fw_module / 2, + dr_fw_wall + radius_fw_channel, + ), + radius=radius_fw_channel, + edgecolor="black", + facecolor="#b87333", + ) + ) + + # Draw radius line in the second circle + axis.plot( + [ + dx_fw_module + dx_fw_module / 2, + dx_fw_module + dx_fw_module / 2 + radius_fw_channel * np.cos(np.pi / 4), + ], + [ + dr_fw_wall + radius_fw_channel, + dr_fw_wall + radius_fw_channel + radius_fw_channel * np.sin(np.pi / 4), + ], + color="black", + linestyle="--", + label=f"$r_{{channel}}$ = {radius_fw_channel:.3f} cm", + ) + + # Draw width line below the second module + axis.plot( + [0, 0], + [0, 0], + color="black", + label=f"$w_{{module}}$ = {dx_fw_module:.3f} cm", + ) + draw_annotation( + axis, + "", + xy=(dx_fw_module, -0.2), + xytext=(2 * dx_fw_module, -0.2), + arrowprops={"arrowstyle": "<->", "color": "black"}, + ) + + # Draw dotted line above the channel + axis.plot( + [dx_fw_module * 1.5, dx_fw_module * 1.5], + [ + 2 * radius_fw_channel + dr_fw_wall, + 2 * (radius_fw_channel + dr_fw_wall), + ], + color="black", + linestyle="dotted", + label=rf"$\Delta r_{{wall}}$ = {dr_fw_wall:.3f} cm", + ) + + # Draw dotted line below the channel + axis.plot( + [dx_fw_module * 1.5, dx_fw_module * 1.5], + [0, dr_fw_wall], + color="black", + linestyle="dotted", + ) + # Plot a dot in the center of the second channel + axis.plot( + dx_fw_module + dx_fw_module / 2, + dr_fw_wall + radius_fw_channel, + marker="o", + color="black", + ) + + # Add the legend to the plot + axis.legend() + axis.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.2) + axis.set_xlabel("X [cm]") + axis.set_ylabel("R [cm]") + axis.set_title("First Wall Top-Down Cross Section") + axis.set_xlim(-1, 2 * dx_fw_module + 1) + axis.set_ylim(-1, 2 * (dr_fw_wall + radius_fw_channel) + 1) + + +def plot_first_wall_poloidal_cross_section(axis: plt.Axes, mfile: MFile, scan: int): + """Plot first wall poloidal cross-section""" + # Import required variables + radius_fw_channel = mfile.get("radius_fw_channel", scan=scan) + dr_fw_wall = mfile.get("dr_fw_wall", scan=scan) + dx_fw_module = mfile.get("dx_fw_module", scan=scan) + len_fw_channel = mfile.get("len_fw_channel", scan=scan) + temp_fw_coolant_in = mfile.get("temp_fw_coolant_in", scan=scan) + temp_fw_coolant_out = mfile.get("temp_fw_coolant_out", scan=scan) + i_fw_coolant_type = mfile.get("i_fw_coolant_type", scan=scan).strip("'\"") + temp_fw_peak = mfile.get("temp_fw_peak", scan=scan) + pres_fw_coolant = mfile.get("pres_fw_coolant", scan=scan) + n_fw_outboard_channels = mfile.get("n_fw_outboard_channels", scan=scan) + n_fw_inboard_channels = mfile.get("n_fw_inboard_channels", scan=scan) + + # Plot first wall structure facing plasma + axis.add_patch( + patches.Rectangle( + xy=(0, 0), + width=dr_fw_wall, + height=len_fw_channel, + edgecolor="black", + facecolor="gray", + ) + ) + + # Plot the cooling channel + axis.add_patch( + patches.Rectangle( + xy=(dr_fw_wall, 0), + width=2 * radius_fw_channel, + height=len_fw_channel, + edgecolor="black", + facecolor="#b87333", # Copper color + ) + ) + + # Plot the back wall of the first wall + axis.add_patch( + patches.Rectangle( + xy=(dr_fw_wall + 2 * radius_fw_channel, 0), + width=dr_fw_wall, + height=len_fw_channel, + edgecolor="black", + facecolor="grey", + ) + ) + + # Draw an upward pointing arrow + axis.arrow( + dx_fw_module + 0.5 * dr_fw_wall, + dr_fw_wall + radius_fw_channel, + 0, + len_fw_channel / 6, + head_width=dr_fw_wall, + head_length=len_fw_channel / 20, + fc="black", + ec="black", + ) + + # Add the inlet temperature beside the arrow + draw_text( + axis, + dx_fw_module + 2 * dr_fw_wall, + dr_fw_wall + radius_fw_channel + len_fw_channel / 6, + f"$T_{{inlet}} = ${temp_fw_coolant_in:.2f} K", + ha="left", + va="bottom", + fontsize=10, + color="black", + ) + + # Draw a right pointing arrow + axis.arrow( + dx_fw_module + 0.5 * dr_fw_wall, + len_fw_channel, + 2 * dr_fw_wall, + 0, + head_width=len_fw_channel / 30, + head_length=dr_fw_wall, + fc="black", + ec="black", + linewidth=5, # Thicker stem + ) + + # Add the outlet temperature beside the arrow + draw_text( + axis, + dx_fw_module + 0.5 * dr_fw_wall, + len_fw_channel * 0.9, + f"$T_{{outlet}} = ${temp_fw_coolant_out:.2f} K", + ha="left", + va="bottom", + fontsize=10, + color="black", + ) + + textstr_fw = "\n".join(( + rf"Coolant type: {i_fw_coolant_type}", + rf"$T_{{FW,peak}}$: {temp_fw_peak:,.3f} K", + rf"$P_{{FW}}$: {pres_fw_coolant / 1e3:,.3f} kPa", + rf"$P_{{FW}}$: {pres_fw_coolant / 1e5:,.3f} bar", + rf"$N_{{outboard}}$: {n_fw_outboard_channels}", + rf"$N_{{inboard}}$: {n_fw_inboard_channels}", + )) + + props_fw = {"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5} + draw_text( + axis, + -0.5, + 0.05, + textstr_fw, + transform=axis.transAxes, + fontsize=11, + verticalalignment="bottom", + bbox=props_fw, + ) + + axis.set_xlabel("R [m]") + axis.set_ylabel("Z [m]") + axis.set_title("First Wall Poloidal Cross Section") + axis.set_xlim(-0.01, (dx_fw_module + radius_fw_channel * 2) + 0.01) + axis.set_ylim(-0.2, len_fw_channel + 0.2) + + +def plot_firstwall( + axis: plt.Axes, + mfile: MFile, + scan: int, + radial_build, + colour_scheme, + mirror_negative_x: bool = False, +): + """Function to plot first wall + + Parameters + ---------- + axis : + axis object to plot to + mfile : + MFILE + scan : + scan number to use + radial_build : + + colour_scheme : + colour scheme to use for plots + mirror_negative_x : + if True, mirror the plot to the negative x-axis (Default value = False) + """ + cumulative_upper = radial_build.cumulative_upper + cumulative_lower = radial_build.cumulative_lower + + i_single_null = mfile.get("i_single_null", scan=scan) + triang_95 = mfile.get("triang95", scan=scan) + if int(i_single_null) == 1: + dz_blkt_upper = mfile.get("dz_blkt_upper", scan=scan) + tfwvt = mfile.get("dz_fw_upper", scan=scan) + else: + dz_blkt_upper = tfwvt = 0.0 + + c_blnkith = cumulative_radial_build("dr_blkt_inboard", mfile, scan) + c_fwoth = cumulative_radial_build("dr_fw_outboard", mfile, scan) + + dr_fw_inboard = mfile.get("dr_fw_inboard", scan=scan) + dr_fw_outboard = mfile.get("dr_fw_outboard", scan=scan) + + # Apply mirror transformation if requested + x_scale = -1 if mirror_negative_x else 1 + + match DivertorNumberModels(i_single_null): + case DivertorNumberModels.SINGLE_NULL: + # Upper first wall: outer surface + radx_outer = ( + cumulative_radial_build("dr_fw_outboard", mfile, scan) + + cumulative_radial_build("dr_blkt_inboard", mfile, scan) + ) / 2.0 + rminx_outer = ( + cumulative_radial_build("dr_fw_outboard", mfile, scan) + - cumulative_radial_build("dr_blkt_inboard", mfile, scan) + ) / 2.0 + + # Upper first wall: inner surface + radx_inner = ( + cumulative_radial_build("dr_fw_plasma_gap_outboard", mfile, scan) + + cumulative_radial_build("dr_fw_inboard", mfile, scan) + ) / 2.0 + rminx_inner = ( + cumulative_radial_build("dr_fw_plasma_gap_outboard", mfile, scan) + - cumulative_radial_build("dr_fw_inboard", mfile, scan) + ) / 2.0 + + fwg_single_null = first_wall_geometry_single_null( + radx_outer=radx_outer, + rminx_outer=rminx_outer, + radx_inner=radx_inner, + rminx_inner=rminx_inner, + cumulative_upper=cumulative_upper, + triang=triang_95, + cumulative_lower=cumulative_lower, + dz_blkt_upper=dz_blkt_upper, + c_blnkith=c_blnkith, + c_fwoth=c_fwoth, + dr_fw_inboard=dr_fw_inboard, + dr_fw_outboard=dr_fw_outboard, + tfwvt=tfwvt, + ) + + # Plot first wall + axis.plot( + x_scale * np.array(fwg_single_null.rs), + fwg_single_null.zs, + color="black", + lw=thin, + ) + axis.fill( + x_scale * np.array(fwg_single_null.rs), + fwg_single_null.zs, + color=FIRSTWALL_COLOUR[colour_scheme - 1], + lw=0.01, + ) + + case DivertorNumberModels.DOUBLE_NULL: + fwg_double_null = first_wall_geometry_double_null( + cumulative_lower=cumulative_lower, + triang=triang_95, + dz_blkt_upper=dz_blkt_upper, + c_blnkith=c_blnkith, + c_fwoth=c_fwoth, + dr_fw_inboard=dr_fw_inboard, + dr_fw_outboard=dr_fw_outboard, + tfwvt=tfwvt, + ) + # Plot first wall + axis.plot( + x_scale * np.array(fwg_double_null.rs[0]), + fwg_double_null.zs[0], + color="black", + lw=thin, + ) + axis.plot( + x_scale * np.array(fwg_double_null.rs[1]), + fwg_double_null.zs[1], + color="black", + lw=thin, + ) + axis.fill( + x_scale * np.array(fwg_double_null.rs[0]), + fwg_double_null.zs[0], + color=FIRSTWALL_COLOUR[colour_scheme - 1], + lw=0.01, + ) + axis.fill( + x_scale * np.array(fwg_double_null.rs[1]), + fwg_double_null.zs[1], + color=FIRSTWALL_COLOUR[colour_scheme - 1], + lw=0.01, + ) + + +__all__ = [ + "plot_blanket", + "plot_cryostat", + "plot_first_wall_poloidal_cross_section", + "plot_first_wall_top_down_cross_section", + "plot_firstwall", + "plot_full_machine_poloidal_cross_section", + "plot_shield", + "plot_vacuum_vessel_and_divertor", + "poloidal_cross_section", +] diff --git a/process/core/io/plot/summary/geometry/toroidal.py b/process/core/io/plot/summary/geometry/toroidal.py new file mode 100644 index 0000000000..c1da82726a --- /dev/null +++ b/process/core/io/plot/summary/geometry/toroidal.py @@ -0,0 +1,330 @@ +"""Geometry functions for PROCESS summary plots.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +import matplotlib.pyplot as plt +import numpy as np +from matplotlib import patches +from matplotlib.path import Path as mplPath + +from process.core.io.plot.summary.constants import ( + BLANKET_COLOUR, + CRYOSTAT_COLOUR, + CSCOMPRESSION_COLOUR, + FIRSTWALL_COLOUR, + NBSHIELD_COLOUR, + PLASMA_COLOUR, + SHIELD_COLOUR, + SOLENOID_COLOUR, + TFC_COLOUR, + THERMAL_SHIELD_COLOUR, + VESSEL_COLOUR, + rtangle, +) +from process.core.io.plot.summary.geometry.build import ( + cumulative_radial_build2, +) +from process.core.io.plot.summary.magnets import ( + TF_outboard, +) +from process.models.physics.current_drive import ( + CurrentDriveMethodType, + CurrentDriveModel, +) +from process.models.tfcoil.base import TFConductorModel + +if TYPE_CHECKING: + from process.core.io.mfile import MFile + + +def toroidal_cross_section( + axis: plt.Axes, + mfile: MFile, + scan: int, + demo_ranges: bool, + colour_scheme: Literal[1, 2], +): + """Function to plot toroidal cross-section""" + axis.set_xlabel("R [m]") + axis.set_ylabel("X [m]") + axis.set_title("Toroidal Cross-Section") + axis.minorticks_on() + axis.grid(which="both", linestyle="--", linewidth=0.5, alpha=0.2) + + rmajor = mfile.get("rmajor", scan=scan) + rminor = mfile.get("rminor", scan=scan) + r_cryostat_inboard = mfile.get("r_cryostat_inboard", scan=scan) + dr_cryostat = mfile.get("dr_cryostat", scan=scan) + n_tf_coils = mfile.get("n_tf_coils", scan=scan) + if ( + CurrentDriveModel(mfile.get("i_hcd_primary", scan=scan)).method + == CurrentDriveMethodType.NEUTRAL_BEAM + or CurrentDriveModel(mfile.get("i_hcd_secondary", scan=scan)).method + == CurrentDriveMethodType.NEUTRAL_BEAM + ): + dx_beam_shield = mfile.get("dx_beam_shield", scan=scan) + dx_beam_duct = mfile.get("dx_beam_duct", scan=scan) + radius_beam_tangency = mfile.get("radius_beam_tangency", scan=scan) + else: + dx_beam_shield = 0 + dx_beam_duct = 0 + radius_beam_tangency = 0 + + dr_tf_outboard = mfile.get("dr_tf_outboard", scan=scan) + full_angle = 2 * np.pi + arc(axis, rmajor, theta2=full_angle, style="dashed") + + # Colour in the main components + for v, colours in [ + ("dr_cs", SOLENOID_COLOUR[colour_scheme - 1]), + ("dr_cs_precomp", CSCOMPRESSION_COLOUR[colour_scheme - 1]), + ( + "dr_tf_inboard", + ( + TFC_COLOUR[colour_scheme - 1] + if TFConductorModel(mfile.get("i_tf_sup", scan=scan)) + != TFConductorModel.WATER_COOLED_COPPER + else "#b87333" + ), + ), + ("dr_shld_thermal_inboard", THERMAL_SHIELD_COLOUR[colour_scheme - 1]), + ("dr_vv_inboard", VESSEL_COLOUR[colour_scheme - 1]), + ("dr_shld_inboard", VESSEL_COLOUR[colour_scheme - 1]), + ("dr_blkt_inboard", BLANKET_COLOUR[colour_scheme - 1]), + ("dr_fw_inboard", FIRSTWALL_COLOUR[colour_scheme - 1]), + ("dr_fw_outboard", FIRSTWALL_COLOUR[colour_scheme - 1]), + ("dr_blkt_outboard", BLANKET_COLOUR[colour_scheme - 1]), + ("dr_shld_outboard", SHIELD_COLOUR[colour_scheme - 1]), + ("dr_vv_outboard", VESSEL_COLOUR[colour_scheme - 1]), + ("dr_shld_thermal_outboard", THERMAL_SHIELD_COLOUR[colour_scheme - 1]), + ]: + r2, r1 = cumulative_radial_build2(v, mfile, scan) + arc_fill(axis, r1, r2, color=colours, theta2=full_angle + 1) + + arc_fill( + axis, + rmajor - rminor, + rmajor + rminor, + color=PLASMA_COLOUR[colour_scheme - 1], + theta2=full_angle + 1, + ) + + arc_fill( + axis, + r_cryostat_inboard, + r_cryostat_inboard + dr_cryostat, + color=CRYOSTAT_COLOUR[colour_scheme - 1], + theta2=full_angle + 1, + ) + + # Segment the TF coil inboard + # Calculate centrelines + spacing = 2 * np.pi / n_tf_coils + coil_indices = np.arange(int(n_tf_coils)) + + r1, _ = cumulative_radial_build2("dr_cs_tf_gap", mfile, scan) + r2, _ = cumulative_radial_build2("dr_tf_inboard", mfile, scan) + r4, r3 = cumulative_radial_build2("dr_tf_outboard", mfile, scan) + + # Coil width + w = r2 * np.tan(spacing / 2) + for ang in (coil_indices * spacing) - spacing / 2: + axis.plot( + [r1 * np.cos(ang), r2 * np.cos(ang)], + [r1 * np.sin(ang), r2 * np.sin(ang)], + color="black", + ) + + for item in coil_indices: + # Neutral beam shielding + TF_outboard( + axis, + item, + n_tf_coils=n_tf_coils, + r3=r3, + r4=r4, + w=w + dx_beam_shield, + facecolor=NBSHIELD_COLOUR[colour_scheme - 1], + ) + # Overlay TF coil segments + TF_outboard( + axis, + item, + n_tf_coils=n_tf_coils, + r3=r3, + r4=r4, + w=w, + facecolor=( + TFC_COLOUR[colour_scheme - 1] + if TFConductorModel(mfile.get("i_tf_sup", scan=scan)) + != TFConductorModel.WATER_COOLED_COPPER + else "#b87333" + ), + ) + + i_hcd_primary = mfile.get("i_hcd_primary", scan=scan) + if CurrentDriveModel(i_hcd_primary).method == CurrentDriveMethodType.NEUTRAL_BEAM: + # Neutral beam geometry. See docs for diagram. + a = w + dx_beam_shield + b = dr_tf_outboard + d = r3 + e = np.sqrt(a**2 + (d + b) ** 2) + + # Beam edges from centreline + half_duct = 0.5 * dx_beam_duct + r_beam_inner = radius_beam_tangency - half_duct + r_beam_outer = radius_beam_tangency + half_duct + + def calc_xy(rt, e=e): + arg = np.clip(rt / e, -1.0, 1.0) + beta = np.arccos(arg) + x = rt * np.cos(beta) + y = rt * np.sin(beta) + return x, y + + # Tangency points + x_beam_inner, y_beam_inner = calc_xy(r_beam_inner) + x_beam_outer, y_beam_outer = calc_xy(r_beam_outer) + + # TF-side positions (beam sits inside shield) + x0 = r4 + y0_beam_inner = w + dx_beam_shield + y0_beam_outer = y0_beam_inner + dx_beam_duct + + # Centreline tangency point + x_beam_centre, y_beam_centre = calc_xy(radius_beam_tangency) + y0_beam_centre = y0_beam_inner + 0.5 * dx_beam_duct + + # Draw beam duct boundaries + axis.plot( + [x_beam_inner, x0], + [y_beam_inner, y0_beam_inner], + linestyle="dotted", + color="black", + ) + axis.plot( + [x_beam_outer, x0], + [y_beam_outer, y0_beam_outer], + linestyle="dotted", + color="black", + ) + # Draw beam centreline + axis.plot( + [x_beam_centre, x0], + [y_beam_centre, y0_beam_centre], + linestyle="--", + color="black", + linewidth=1.5, + ) + + # Draw dividing lines in the blanket (inboard modules, toroidal direction) + n_blkt_inboard_modules_toroidal = mfile.get( + "n_blkt_inboard_modules_toroidal", scan=scan + ) + if n_blkt_inboard_modules_toroidal > 1: + # Calculate the angular spacing for each module + spacing = full_angle / (n_blkt_inboard_modules_toroidal) + r1, _ = cumulative_radial_build2("dr_shld_inboard", mfile, scan) + r2, _ = cumulative_radial_build2("dr_blkt_inboard", mfile, scan) + for i in range(int(n_blkt_inboard_modules_toroidal)): + ang = i * spacing + # Draw a line from r1 to r2 at angle ang + axis.plot( + [r1 * np.cos(ang), r2 * np.cos(ang)], + [r1 * np.sin(ang), r2 * np.sin(ang)], + color="black", + linestyle="-", + linewidth=1.5, + zorder=100, + ) + + # Draw dividing lines in the blanket (outboard modules, toroidal direction) + n_blkt_outboard_modules_toroidal = mfile.get( + "n_blkt_outboard_modules_toroidal", scan=scan + ) + if n_blkt_outboard_modules_toroidal > 1: + # Calculate the angular spacing for each module + spacing = full_angle / (n_blkt_outboard_modules_toroidal) + r1, _ = cumulative_radial_build2("dr_fw_outboard", mfile, scan) + r2, _ = cumulative_radial_build2("dr_blkt_outboard", mfile, scan) + for i in range(int(n_blkt_outboard_modules_toroidal)): + ang = i * spacing + # Draw a line from r1 to r2 at angle ang + axis.plot( + [r1 * np.cos(ang), r2 * np.cos(ang)], + [r1 * np.sin(ang), r2 * np.sin(ang)], + color="black", + linestyle="-", + linewidth=1.5, + zorder=100, + ) + + # Ranges + # --- + # DEMO : Fixed ranges for comparison + if demo_ranges: + axis.set_ylim(0, 20) + axis.set_xlim(0, 20) + + # Adaptive ranges + else: + axis.set_ylim(0.0, axis.get_ylim()[1]) + axis.set_xlim(0.0, axis.get_xlim()[1]) + + +def arc(axis: plt.Axes, r, theta1=0, theta2=rtangle, style="solid"): + """Plots an arc. + + Parameters + ---------- + axis : + plot object + r : + radius + theta1 : + starting polar angle (Default value = 0) + theta2 : + finishing polar angle (Default value = rtangle) + axis: plt.Axes : + + style : + (Default value = "solid") + """ + angs = np.linspace(theta1, theta2) + xs = r * np.cos(angs) + ys = r * np.sin(angs) + axis.plot(xs, ys, linestyle=style, color="black", lw=0.2) + + +def arc_fill(axis: plt.Axes, r1, r2, color="pink", theta1=0, theta2=rtangle): + """Fills the space between two quarter circles. + + Parameters + ---------- + axis : + plot object + r1 : + r2 radii to be filled + axis: plt.Axes : + + r2 : + + color : + (Default value = "pink") + """ + angs = np.linspace(theta1, theta2, endpoint=True) + xs1 = r1 * np.cos(angs) + ys1 = r1 * np.sin(angs) + angs = np.linspace(theta2, theta1, endpoint=True) + xs2 = r2 * np.cos(angs) + ys2 = r2 * np.sin(angs) + verts = list(zip(xs1, ys1, strict=False)) + verts.extend(list(zip(xs2, ys2, strict=False))) + path = mplPath(verts, closed=True) + patch = patches.PathPatch(path, facecolor=color, lw=0) + axis.add_patch(patch) + + +__all__ = ["arc", "arc_fill", "toroidal_cross_section"] diff --git a/process/core/io/plot/summary/magnets/__init__.py b/process/core/io/plot/summary/magnets/__init__.py new file mode 100644 index 0000000000..9a6c65f03d --- /dev/null +++ b/process/core/io/plot/summary/magnets/__init__.py @@ -0,0 +1,61 @@ +"""Public API for this summary plotting concern.""" + +from __future__ import annotations + +import process.core.io.plot.summary.magnets.cables as _cables +import process.core.io.plot.summary.magnets.cs as _cs +import process.core.io.plot.summary.magnets.pf as _pf +import process.core.io.plot.summary.magnets.tf as _tf + +_MODULES = (_cables, _cs, _pf, _tf) +_REGISTRY = {} +for _module in _MODULES: + _REGISTRY.update({name: getattr(_module, name) for name in _module.__all__}) +for _module in _MODULES: + _module.__dict__.update(_REGISTRY) +TF_outboard = _REGISTRY["TF_outboard"] +plot_cable_in_conduit_cable = _REGISTRY["plot_cable_in_conduit_cable"] +plot_corc_cable_geometry = _REGISTRY["plot_corc_cable_geometry"] +plot_cs_coil_structure = _REGISTRY["plot_cs_coil_structure"] +plot_cs_turn_structure = _REGISTRY["plot_cs_turn_structure"] +plot_hts_tape_geometry = _REGISTRY["plot_hts_tape_geometry"] +plot_magnetics_info = _REGISTRY["plot_magnetics_info"] +plot_pf_coils = _REGISTRY["plot_pf_coils"] +plot_pf_cs_plasma_mutual_inductance = _REGISTRY["plot_pf_cs_plasma_mutual_inductance"] +plot_pf_dimensions = _REGISTRY["plot_pf_dimensions"] +plot_physics_info = _REGISTRY["plot_physics_info"] +plot_quench_time_evolution = _REGISTRY["plot_quench_time_evolution"] +plot_resistive_tf_info = _REGISTRY["plot_resistive_tf_info"] +plot_resistive_tf_wp = _REGISTRY["plot_resistive_tf_wp"] +plot_superconducting_tf_wp = _REGISTRY["plot_superconducting_tf_wp"] +plot_tf_cable_in_conduit_turn = _REGISTRY["plot_tf_cable_in_conduit_turn"] +plot_tf_coil_structure = _REGISTRY["plot_tf_coil_structure"] +plot_tf_coils = _REGISTRY["plot_tf_coils"] +plot_tf_corc_cable_summary_box = _REGISTRY["plot_tf_corc_cable_summary_box"] +plot_tf_croco_turn = _REGISTRY["plot_tf_croco_turn"] +plot_tf_stress = _REGISTRY["plot_tf_stress"] +secs_to_hms = _REGISTRY["secs_to_hms"] +__all__ = [ + "TF_outboard", + "plot_cable_in_conduit_cable", + "plot_corc_cable_geometry", + "plot_cs_coil_structure", + "plot_cs_turn_structure", + "plot_hts_tape_geometry", + "plot_magnetics_info", + "plot_pf_coils", + "plot_pf_cs_plasma_mutual_inductance", + "plot_pf_dimensions", + "plot_physics_info", + "plot_quench_time_evolution", + "plot_resistive_tf_info", + "plot_resistive_tf_wp", + "plot_superconducting_tf_wp", + "plot_tf_cable_in_conduit_turn", + "plot_tf_coil_structure", + "plot_tf_coils", + "plot_tf_corc_cable_summary_box", + "plot_tf_croco_turn", + "plot_tf_stress", + "secs_to_hms", +] diff --git a/process/core/io/plot/summary/magnets/cables.py b/process/core/io/plot/summary/magnets/cables.py new file mode 100644 index 0000000000..4b89de6ca2 --- /dev/null +++ b/process/core/io/plot/summary/magnets/cables.py @@ -0,0 +1,209 @@ +"""Magnets functions for PROCESS summary plots.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import matplotlib.pyplot as plt +import numpy as np +from matplotlib import patches +from matplotlib.patches import Rectangle + +from process.core.io.plot.summary.common import ( + box_style, +) +from process.core.io.plot.summary.rendering import ( + draw_text, +) + +if TYPE_CHECKING: + from process.core.io.mfile import MFile + + +def plot_cable_in_conduit_cable(axis: plt.Axes, fig, mfile: MFile, scan: int): + """Plots TF coil CICC cable cross-section. + + Parameters + ---------- + axis: plt.Axes : + + fig : + + mfile: MFile : + + scan: int : + + """ + dia_tf_turn_superconducting_cable = mfile.get( + "dia_tf_turn_superconducting_cable", scan=scan + ) + f_a_tf_turn_cable_copper = mfile.get("f_a_tf_turn_cable_copper", scan=scan) + + # Convert to mm + dia_mm = dia_tf_turn_superconducting_cable * 1000 + radius_superconductor_mm = np.sqrt(1 - f_a_tf_turn_cable_copper) * (dia_mm / 2) + + # Draw the outer copper circle + circle_copper_surrounding = patches.Circle( + (0, 0), + dia_mm / 2, + facecolor="#b87333", # copper color + edgecolor="#8B4000", # darker copper edge + linewidth=0.1, + alpha=0.8, + label="Copper", + zorder=1, + ) + axis.add_patch(circle_copper_surrounding) + + # Draw the inner superconductor circle + circle_central_conductor = patches.Circle( + (0, 0), + radius_superconductor_mm, + facecolor="black", + linewidth=0.3, + alpha=0.7, + label="Superconductor", + zorder=2, + ) + axis.add_patch(circle_central_conductor) + + # Convert cable diameter to mm + cable_diameter_mm = mfile.get("dia_tf_turn_superconducting_cable", scan=scan) * 1000 + # Convert lengths from meters to kilometers for display + len_tf_coil_superconductor_km = ( + mfile.get("len_tf_coil_superconductor", scan=scan) / 1000.0 + ) + len_tf_superconductor_total_km = ( + mfile.get("len_tf_superconductor_total", scan=scan) / 1000.0 + ) + + textstr_cable = ( + f"$\\mathbf{{Cable:}}$\n\nCable diameter: {cable_diameter_mm:,.4f}" + " mm\nCopper area fraction:" + f" {mfile.get('f_a_tf_turn_cable_copper', scan=scan):.4f}\nNumber of" + " strands per turn:" + f" {int(mfile.get('n_tf_turn_superconducting_cables', scan=scan)):,}\nLength" + f" of superconductor per coil: {len_tf_coil_superconductor_km:,.2f}" + " km\nTotal length of superconductor in all coils:" + f" {len_tf_superconductor_total_km:,.2f} km\n" + ) + draw_text( + axis, + 0.4, + 0.3, + textstr_cable, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=box_style("#cccccc"), + ) + + axis.set_aspect("equal") + axis.set_xlim(-dia_mm / 1.5, dia_mm / 1.5) + axis.set_ylim(-dia_mm / 1.5, dia_mm / 1.5) + axis.set_title("TF CICC Cable Cross-Section") + axis.minorticks_on() + axis.legend(loc="upper right") + axis.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.2) + axis.set_xlabel("X [mm]") + axis.set_ylabel("Y [mm]") + + +def plot_hts_tape_geometry( + axis, + r_left: float, + z_bottom: float, + dr_hts_tape: float, + dx_hts_tape_rebco: float, + dx_hts_tape_copper: float, + dx_hts_tape_hastelloy: float, + show_legend: bool = True, +): + """Plot HTS tape geometry""" + legend_label = None if show_legend else "_nolegend_" + # Plot a rectangular tape stack in the middle + rect = Rectangle( + (r_left, z_bottom), + width=dr_hts_tape, + height=dx_hts_tape_copper / 2, + edgecolor=None, + facecolor="#B87333", + linewidth=2, + label="Copper" if show_legend else legend_label, + ) + axis.add_patch(rect) + rect = Rectangle( + (r_left, z_bottom + dx_hts_tape_copper / 2), + width=dr_hts_tape, + height=dx_hts_tape_hastelloy / 2, + edgecolor=None, + facecolor="grey", + linewidth=2, + label="Hastelloy" if show_legend else legend_label, + ) + axis.add_patch(rect) + rect = Rectangle( + ( + r_left, + z_bottom + dx_hts_tape_copper / 2 + dx_hts_tape_hastelloy / 2, + ), + width=dr_hts_tape, + height=dx_hts_tape_rebco, + edgecolor=None, + facecolor="blue", + linewidth=2, + label="REBCO" if show_legend else legend_label, + ) + axis.add_patch(rect) + rect = Rectangle( + ( + r_left, + z_bottom + + dx_hts_tape_copper / 2 + + dx_hts_tape_hastelloy / 2 + + dx_hts_tape_rebco, + ), + width=dr_hts_tape, + height=dx_hts_tape_hastelloy / 2, + edgecolor=None, + facecolor="grey", + linewidth=2, + label="Hastelloy" if show_legend else legend_label, + ) + axis.add_patch(rect) + rect = Rectangle( + ( + r_left, + z_bottom + + dx_hts_tape_copper / 2 + + dx_hts_tape_hastelloy / 2 + + dx_hts_tape_rebco + + dx_hts_tape_hastelloy / 2, + ), + width=dr_hts_tape, + height=dx_hts_tape_copper / 2, + edgecolor=None, + facecolor="#B87333", + linewidth=2, + label="Copper" if show_legend else legend_label, + ) + axis.add_patch(rect) + + axis.set_title("HTS Tape Geometry") + axis.grid(True) + axis.set_xlabel("X-axis (m)") + axis.set_ylabel("Y-axis (m)") + axis.set_xlim(r_left * 0.9, dr_hts_tape * 1.1) + axis.set_ylim( + z_bottom * 0.9, + (dx_hts_tape_copper + dx_hts_tape_hastelloy + dx_hts_tape_rebco) * 1.1, + ) + axis.minorticks_on() + axis.ticklabel_format(style="sci", axis="both", scilimits=(0, 0)) + if show_legend: + axis.legend(loc="upper right") + + +__all__ = ["plot_cable_in_conduit_cable", "plot_hts_tape_geometry"] diff --git a/process/core/io/plot/summary/magnets/cs.py b/process/core/io/plot/summary/magnets/cs.py new file mode 100644 index 0000000000..e7eb6bb302 --- /dev/null +++ b/process/core/io/plot/summary/magnets/cs.py @@ -0,0 +1,640 @@ +"""Magnets functions for PROCESS summary plots.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import matplotlib.pyplot as plt +import numpy as np +from matplotlib import patches + +from process.core.io.plot.summary.common import ( + box_style, + setup_axis, + text_layout, +) +from process.core.io.plot.summary.constants import ( + SOLENOID_COLOUR, +) +from process.core.io.plot.summary.rendering import ( + draw_annotation, + draw_text, +) +from process.core.io.plot.summary.reporting import ( + plot_info, +) +from process.data_structure.pfcoil_variables import NFIXMX +from process.models.superconductors import SuperconductorModel + +if TYPE_CHECKING: + from process.core.io.mfile import MFile + + +def secs_to_hms(s): + """Convert seconds to 'Hh Mm Ss' string.""" + s = float(s) + return f"{int(s // 3600)}h {int((s % 3600) // 60)}m {int(s % 60)}s" + + +def plot_physics_info(axis: plt.Axes, mfile: MFile, scan: int): + """Function to plot geometry info + + Parameters + ---------- + axis : + axis object to plot to + mfile : + MFILE + scan : + scan number to use + """ + xmin = 0 + xmax = 1 + ymin = -16 + ymax = 1 + + draw_text(axis, -0.05, 1, "Physics:", ha="left", va="center") + setup_axis(axis, xmin, xmax, ymin, ymax) + + nong = mfile.get("nd_plasma_electron_line", scan=scan) / mfile.get( + "nd_plasma_electron_max_array(7)", scan=scan + ) + + nd_plasma_impurities_vol_avg = mfile.get( + "nd_plasma_impurities_vol_avg", scan=scan + ) / mfile.get("nd_plasma_electrons_vol_avg", scan=scan) + + tepeak = mfile.get("temp_plasma_electron_on_axis_kev", scan=scan) / mfile.get( + "temp_plasma_electron_vol_avg_kev", scan=scan + ) + + nepeak = mfile.get("nd_plasma_electron_on_axis", scan=scan) / mfile.get( + "nd_plasma_electrons_vol_avg", scan=scan + ) + + # Assume Martin scaling if pthresh is not printed + # Accounts for pthresh not being written prior to issue #679 and #680 + if "p_l_h_threshold_mw" in mfile.data: + pthresh = mfile.get("p_l_h_threshold_mw", scan=scan) + else: + pthresh = mfile.get("l_h_threshold_powers(6)", scan=scan) + + data = [ + ("p_fusion_total_mw", "Fusion power", "MW"), + ("big_q_plasma", "$Q_{p}$", ""), + ("plasma_current_ma", "$I_p$", "MA"), + ("b_plasma_toroidal_on_axis", "Vacuum $B_T$ at $R_0$", "T"), + ("q95", r"$q_{\mathrm{95}}$", ""), + ("beta_norm_thermal", r"$\beta_N$, thermal", "% m T MA$^{-1}$"), + ("beta_norm_toroidal", r"$\beta_N$, toroidal", "% m T MA$^{-1}$"), + ("beta_thermal_poloidal_vol_avg", r"$\beta_P$, thermal", ""), + ("beta_poloidal_vol_avg", r"$\beta_P$, total", ""), + ("temp_plasma_electron_vol_avg_kev", r"$\langle T_e \rangle$", "keV"), + ("nd_plasma_electrons_vol_avg", r"$\langle n_e \rangle$", "m$^{-3}$"), + (nong, r"$\langle n_{\mathrm{e,line}} \rangle \ / \ n_G$", ""), + (tepeak, r"$T_{e0} \ / \ \langle T_e \rangle$", ""), + (nepeak, r"$n_{e0} \ / \ \langle n_{\mathrm{e, vol}} \rangle$", ""), + ("n_charge_plasma_effective_vol_avg", r"$Z_{\mathrm{eff}}$", ""), + ( + nd_plasma_impurities_vol_avg, + r"$n_Z \ / \ \langle n_{\mathrm{e, vol}} \rangle$", + "", + ), + ("t_energy_confinement", r"$\tau_e$", "s"), + ("hfact", "H-factor", ""), + (pthresh, "H-mode threshold", "MW"), + ("tauelaw", "Scaling law", ""), + ] + + plot_info(axis, data, mfile, scan) + + +def plot_magnetics_info(axis: plt.Axes, mfile: MFile, scan: int): + """Function to plot magnet info + + Parameters + ---------- + axis : + axis object to plot to + mfile : + MFILE + scan : + scan number to use + """ + # Check for Copper magnets + i_tf_sup = int(mfile.get("i_tf_sup", scan=scan)) if "i_tf_sup" in mfile.data else 1 + + draw_text(axis, -0.05, 1, "Coil currents etc:", ha="left", va="center") + setup_axis(axis, xmin=0, xmax=1, ymin=-16, ymax=1) + + # Number of coils (1 is OH coil) + number_of_coils = 0 + for item in mfile.data: + if "r_pf_coil_middle[" in item: + number_of_coils += 1 + + pf_info = [ + ( + mfile.get(f"c_pf_cs_coils_peak_ma[{i:01}]", scan=scan), + f"PF {i}", + ) + for i in range(1, number_of_coils) + if i % 2 != 0 + ] + + if len(pf_info) > 2: + pf_info_3_a = pf_info[2][0] + pf_info_3_b = pf_info[2][1] + else: + pf_info_3_a = "" + pf_info_3_b = "" + + t_plant_pulse_burn = mfile.get("t_plant_pulse_burn", scan=scan) / 3600.0 + + i_tf_bucking = ( + int(mfile.get("i_tf_bucking", scan=scan)) if "i_tf_bucking" in mfile.data else 1 + ) + + # Get superconductor material (i_tf_sc_mat) + # If i_tf_sc_mat not present, assume resistive + i_tf_sc_mat = ( + int(mfile.get("i_tf_sc_mat", scan=scan)) if "i_tf_sc_mat" in mfile.data else 0 + ) + + tftype = ( + SuperconductorModel(int(mfile.get("i_tf_sc_mat", scan=scan))).full_name + if i_tf_sc_mat > 0 + else "Resistive Copper" + ) + + vssoft = mfile.get("vs_plasma_res_ramp", scan=scan) + mfile.get( + "vs_plasma_ind_ramp", scan=scan + ) + + sig_case = 1.0e-6 * mfile.get(f"s_shear_tf_peak({i_tf_bucking})", scan=scan) + sig_cond = 1.0e-6 * mfile.get(f"s_shear_tf_peak({i_tf_bucking + 1})", scan=scan) + + if i_tf_sup == 1: + data = [ + (pf_info[0][0], pf_info[0][1], "MA"), + (pf_info[1][0], pf_info[1][1], "MA"), + (pf_info_3_a, pf_info_3_b, "MA"), + (vssoft, "Startup flux swing", "Wb"), + ("vs_cs_pf_total_pulse", "Available flux swing", "Wb"), + (t_plant_pulse_burn, "Burn time", "hrs"), + ("", "", ""), + (f"#TF coil type is {tftype}", "", ""), + ( + "b_tf_inboard_peak_with_ripple", + "Peak field at conductor (w. rip.)", + "T", + ), + ("f_c_tf_turn_operating_critical", r"I/I$_{\mathrm{crit}}$", ""), + ("temp_tf_superconductor_margin", "TF Temperature margin", "K"), + ("temp_cs_superconductor_margin", "CS Temperature margin", "K"), + (sig_cond, "TF Cond max TRESCA stress", "MPa"), + (sig_case, "TF Case max TRESCA stress", "MPa"), + ("m_tf_coils_total/n_tf_coils", "Mass per TF coil", "kg"), + ] + + else: + p_cp_resistive = 1.0e-6 * mfile.get("p_cp_resistive", scan=scan) + p_tf_leg_resistive = 1.0e-6 * mfile.get("p_tf_leg_resistive", scan=scan) + p_tf_joints_resistive = 1.0e-6 * mfile.get("p_tf_joints_resistive", scan=scan) + fcoolcp = 100.0 * mfile.get("fcoolcp", scan=scan) + + data = [ + (pf_info[0][0], pf_info[0][1], "MA"), + (pf_info[1][0], pf_info[1][1], "MA"), + (pf_info_3_a, pf_info_3_b, "MA"), + (vssoft, "Startup flux swing", "Wb"), + ("vs_cs_pf_total_pulse", "Available flux swing", "Wb"), + (t_plant_pulse_burn, "Burn time", "hrs"), + ("", "", ""), + (f"#TF coil type is {tftype}", "", ""), + ( + "b_tf_inboard_peak_symmetric", + "Peak field at conductor (w. rip.)", + "T", + ), + ("c_tf_total", "TF coil currents sum", "A"), + ("", "", ""), + ("#TF coil forces/stresses", "", ""), + (sig_cond, "TF conductor max TRESCA stress", "MPa"), + (sig_case, "TF bucking max TRESCA stress", "MPa"), + (fcoolcp, "CP cooling fraction", "%"), + ( + "vel_cp_coolant_midplane", + "Maximum coolant flow speed", + "ms$^{-1}$", + ), + (p_cp_resistive, "CP resistive heating", "MW"), + ( + p_tf_leg_resistive, + "legs resistive heating (all legs)", + "MW", + ), + (p_tf_joints_resistive, "TF joints resistive heating ", "MW"), + ] + + plot_info(axis, data, mfile, scan) + + +def plot_cs_coil_structure( + axis: plt.Axes, fig, mfile: MFile, scan: int, colour_scheme=1 +): + """Function to plot the coil structure of the CS. + + Parameters + ---------- + axis : + axis object to plot to + mfile : + MFILE + scan : + scan number to use + colour_scheme : + colour scheme to use for the plot (Default value = 1) + + """ + # Get CS coil parameters + dr_cs = mfile.get("dr_cs", scan=scan) + dr_cs_full = mfile.get("dr_cs_full", scan=scan) + dz_cs_full = mfile.get("dz_cs_full", scan=scan) + dz_cs = mfile.get("dz_cs_full", scan=scan) + dr_cs_bore = mfile.get("dr_cs_bore", scan=scan) + r_cs_current_filaments_array = [ + mfile.get(f"r_pf_cs_current_filaments{i}", scan=scan) for i in range(NFIXMX) + ] + z_cs_current_filaments_array = [ + mfile.get(f"z_pf_cs_current_filaments{i}", scan=scan) for i in range(NFIXMX) + ] + + # Plot the right side of the CS + right_cs = patches.Rectangle( + (dr_cs_bore, -dz_cs / 2), + dr_cs, + dz_cs, + edgecolor="black", + facecolor=SOLENOID_COLOUR[colour_scheme - 1], + lw=1.5, + ) + axis.add_patch(right_cs) + + # Plot the bore of the machine + bore_rect = patches.Rectangle( + (-dr_cs_bore, -dz_cs / 2), + dr_cs_bore * 2, + dz_cs, + edgecolor="black", + facecolor="lightgrey", + lw=1.0, + ) + axis.add_patch(bore_rect) + + left_cs = patches.Rectangle( + (-dr_cs_bore - dr_cs, -dz_cs / 2), + dr_cs, + dz_cs, + edgecolor="black", + facecolor=SOLENOID_COLOUR[colour_scheme - 1], + lw=1.5, + ) + axis.add_patch(left_cs) + + # Draw vertical lines to represent CS turns + # Get the turn width (radial thickness of each turn) + dr_cs_turn = mfile.get("dr_cs_turn", scan=scan) + dz_cs_turn = mfile.get("dz_cs_turn", scan=scan) + # Number of vertical lines (number of turns) + t_kwargs = {"color": "black", "linestyle": "--", "linewidth": 0.2} + if dr_cs_turn > 0: + n_lines = int(dr_cs / dr_cs_turn) + for i in range(1, n_lines): + x = dr_cs_bore + i * dr_cs_turn + axis.plot([x, x], [-dz_cs / 2, dz_cs / 2], **t_kwargs) + x_left = -dr_cs_bore - dr_cs + i * dr_cs_turn + axis.plot([x_left, x_left], [-dz_cs / 2, dz_cs / 2], **t_kwargs) + # Plot horizontal lines (along Z) for each turn + if dz_cs_turn > 0: + n_hlines = int(dz_cs / dz_cs_turn) + for j in range(1, n_hlines): + y = -dz_cs / 2 + j * dz_cs_turn + # Right CS + axis.plot([dr_cs_bore, dr_cs_bore + dr_cs], [y, y], **t_kwargs) + # Left CS + axis.plot([-dr_cs_bore - dr_cs, -dr_cs_bore], [y, y], **t_kwargs) + + l_kwargs = { + "color": "black", + "linestyle": "--", + "linewidth": 0.6, + "alpha": 0.5, + } + + # Plot a horizontal line at y = 0.0 + axis.axhline(y=0.0, **l_kwargs) + # Plot a vertical line at x = 0.0 + axis.axvline(x=0.0, **l_kwargs) + # Plot a vertical line at x = dr_cs_bore + axis.axvline(x=dr_cs_bore, **l_kwargs) + # Plot a vertical line at x = -dr_cs_bore + axis.axvline(x=-dr_cs_bore, **l_kwargs) + # Plot a vertical line at x = dr_cs_bore + dr_cs + axis.axvline(x=(dr_cs_bore + dr_cs), **l_kwargs) + # Plot a vertical line at x = -dr_cs_bore - dr_cs + axis.axvline(x=-(dr_cs_bore + dr_cs), **l_kwargs) + # Plot a vertical line at y= dz_cs / 2 + axis.axhline(y=(dz_cs / 2), **l_kwargs) + # Plot a vertical line at y= -dz_cs / 2 + axis.axhline(y=-(dz_cs / 2), **l_kwargs) + + # Plot a vertical line at x = r_cs_middle + axis.axvline(x=mfile.get("r_cs_middle", scan=scan), **l_kwargs) + # Plot a vertical line at x= -r_cs_middle + axis.axvline(x=-mfile.get("r_cs_middle", scan=scan), **l_kwargs) + + # Arrow for coil width + draw_annotation( + axis, + "", + xy=(0, (dz_cs_full / 2)), + xytext=(0, -(dz_cs_full / 2)), + arrowprops={"arrowstyle": "<->", "color": "black"}, + ) + + # Add a label for full coil width + draw_text( + axis, + 0.0, + -(dz_cs_full / 4), + f"{dz_cs_full:.3f} m", + fontsize=7, + color="black", + rotation=270, + verticalalignment="center", + horizontalalignment="center", + bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, + ) + + # Arrow for coil width + draw_annotation( + axis, + "", + xy=(-(dr_cs_full / 2), (dz_cs_full / 4)), + xytext=((dr_cs_full / 2), (dz_cs_full / 4)), + arrowprops={"arrowstyle": "<->", "color": "black"}, + ) + + # Add a label for full coil width + draw_text( + axis, + 0.0, + (dz_cs_full / 4), + f"{dr_cs_full:.3f} m", + fontsize=7, + color="black", + rotation=0, + verticalalignment="center", + horizontalalignment="center", + bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, + ) + + textstr_cs = ( + "$\\mathbf{Coil \\ parameters:}$\n\nCS height vs TF internal" + f" height: {mfile.get('f_z_cs_tf_internal', scan=scan):.2f}\nCS" + f" thickness: {mfile.get('dr_cs', scan=scan):.4f} m\nCS radial middle:" + f" {mfile.get('r_cs_middle', scan=scan):.4f} m\nCS full height:" + f" {mfile.get('dz_cs_full', scan=scan):.4f} m\nCS full width:" + f" {mfile.get('dr_cs_full', scan=scan):.4f} m\nCS poloidal area:" + f" {mfile.get('a_cs_poloidal', scan=scan):.4f} m$^2$\nCS top-down" + f" toroidal area: {mfile.get('a_cs_toroidal', scan=scan):.4f}" + " m$^2$\n$N_{\\text{turns}}:$" + f" {mfile.get('n_pf_coil_turns[n_cs_pf_coils-1]', scan=scan):,.2f}\n$I_{{\\text{{peak}}}}:$" # noqa: E501 + f" {mfile.get('c_pf_cs_coils_peak_ma[n_cs_pf_coils-1]', scan=scan):.3f}" + " MA\n$B_{\\text{peak}}:$" + f" {mfile.get('b_pf_coil_peak[n_cs_pf_coils-1]', scan=scan):.3f}" + " T\n$F_{\\text{z,self,peak}}:$" + f" {mfile.get('forc_z_cs_self_peak_midplane', scan=scan) / 1e6:.3f}" + " MN\n$\\sigma_{\\text{z,self,peak}}:$" + f" {mfile.get('stress_z_cs_self_peak_midplane', scan=scan) / 1e6:.3f}" + " MPa\n$\\sigma_{\\text{mises,peak}}:$" + f" {mfile.get('stress_mises_cs_peak', scan=scan) / 1e6:.3f}" + " MPa\n$\\tau_{\\text{shear,peak}}:$" + f" {mfile.get('stress_shear_cs_peak', scan=scan) / 1e6:.3f} MPa " + ) + + draw_text( + axis, + 0.5, + 0.6, + textstr_cs, + **text_layout(fig), + bbox=box_style("lightyellow"), + ) + + # Plot the current filament points as blue dots and label them + + axis.plot( + r_cs_current_filaments_array, + z_cs_current_filaments_array, + "bo", + markersize=2, + label="CS, PF and Plasma Current Filaments", + ) + + axis.set_xlabel("R [m]") + axis.set_ylabel("Z [m]") + axis.set_title("Central Solenoid Poloidal Cross-Section") + axis.grid(True, linestyle="--", alpha=0.3) + axis.minorticks_on() + axis.legend() + + +def plot_cs_turn_structure(axis: plt.Axes, fig, mfile: MFile, scan: int): + """Plot the CS turn structure""" + a_cs_turn = mfile.get("a_cs_turn", scan=scan) + dz_cs_turn = mfile.get("dz_cs_turn", scan=scan) + dr_cs_turn = mfile.get("dr_cs_turn", scan=scan) + + f_dr_dz_cs_turn = mfile.get("f_dr_dz_cs_turn", scan=scan) + radius_cs_turn_cable_space = mfile.get("radius_cs_turn_cable_space", scan=scan) + dz_cs_turn_conduit = mfile.get("dz_cs_turn_conduit", scan=scan) + dr_cs_turn_conduit = mfile.get("dr_cs_turn_conduit", scan=scan) + radius_cs_turn_corners = mfile.get("radius_cs_turn_corners", scan=scan) + f_a_cs_turn_steel = mfile.get("f_a_cs_turn_steel", scan=scan) + + # Plot the CS turn as a rectangle representing the conductor cross-section + # Assume dz_cs_turn is the diameter and dr_cs_turn is the length of the conductor + # cross-section + + # Draw the conductor cross-section as a rectangle + axis.add_patch( + patches.FancyBboxPatch( + (0, 0), + dr_cs_turn, + dz_cs_turn, + boxstyle=patches.BoxStyle( + "Round", pad=0, rounding_size=radius_cs_turn_corners + ), + edgecolor="black", + facecolor="grey", + lw=1.5, + label="CS Turn Steel Conduit", + ) + ) + + # Draw the conductor cross-section as a rectangle + axis.add_patch( + patches.Rectangle( + ( + dr_cs_turn_conduit + radius_cs_turn_cable_space, + dz_cs_turn_conduit, + ), + dr_cs_turn - ((2 * dr_cs_turn_conduit) + (2 * radius_cs_turn_cable_space)), + 2 * radius_cs_turn_cable_space, + facecolor="white", + lw=1.5, + label="CS Turn Cable Space", + zorder=2, + ) + ) + # Plot the right hand circle for the CS turn cable space + axis.add_patch( + patches.Circle( + ( + (dr_cs_turn - dr_cs_turn_conduit - radius_cs_turn_cable_space), + dz_cs_turn / 2, + ), + radius_cs_turn_cable_space, + facecolor="white", + lw=1.5, + zorder=3, + ) + ) + # Plot the left hand circle for the CS turn cable space + axis.add_patch( + patches.Circle( + ( + (dr_cs_turn_conduit + radius_cs_turn_cable_space), + dz_cs_turn / 2, + ), + radius_cs_turn_cable_space, + facecolor="white", + lw=1.5, + zorder=3, + ) + ) + + # Add plasma volume, areas and shaping information + textstr_turn = ( + f"$\\mathbf{{Turn \\ structure:}}$\n\n$A:$ {a_cs_turn:.4e}$ \\" + f" \\text{{m}}^2$\nTurn width: {dr_cs_turn:.4e}$ \\ \\text{{m}}$\nTurn" + f" height: {dz_cs_turn:.4e}$ \\ \\text{{m}}$\nTurn width to height" + f" ratio: {f_dr_dz_cs_turn:.3f}\nSteel conduit width:" + f" {dr_cs_turn_conduit:.4e}$ \\ \\text{{m}}$\nRadius of turn cable" + f" space: {radius_cs_turn_cable_space:.4e}$ \\ \\text{{m}}$\nRadius of" + f" turn corner: {radius_cs_turn_corners:.4e}$ \\" + " \\text{m}$\nFraction of turn area that is steel:" + f" {f_a_cs_turn_steel:.4f}\n" + ) + + draw_text( + axis, + 0.7, + 0.375, + textstr_turn, + **text_layout(fig), + bbox=box_style("lightyellow"), + ) + + axis.set_xlim(-dr_cs_turn * 0.2, dr_cs_turn * 1.2) + axis.set_ylim(-dz_cs_turn * 0.3, dz_cs_turn * 1.3) + axis.set_aspect("equal") + axis.set_xlabel("Length [m]") + axis.set_ylabel("Height [m]") + axis.set_title("CS Turn Conductor Cross-Section") + cs_legend = axis.legend(loc="upper right", bbox_to_anchor=(0.7, -0.25)) + cs_legend.get_frame().set_edgecolor("black") + axis.grid(True, linestyle="--", alpha=0.3) + + +def plot_pf_cs_plasma_mutual_inductance( + axis: plt.Axes, m_file: MFile, scan: int +) -> None: + """Plot the mutual inductance between the plasma and PF/CS coils. + + Parameters + ---------- + axis : plt.Axes + Axis to plot on + m_file : MFile + MFILE data object + scan : int + Scan number to read from MFILE + + """ + n_pf_cs_plasma_circuits = int(m_file.get("n_pf_cs_plasma_circuits", scan=scan)) + mutual_inductance = np.zeros((n_pf_cs_plasma_circuits, n_pf_cs_plasma_circuits)) + iohcl = int(m_file.get("iohcl", scan=scan)) + + for coil in range(n_pf_cs_plasma_circuits): + for circuit in range(n_pf_cs_plasma_circuits): + mutual_inductance[coil, circuit] = m_file.get( + f"ind_pf_cs_plasma_mutual[{coil},_{circuit}]", + scan=scan, + ) + + # Create lower triangular matrix + mutual_inductance = np.tril(mutual_inductance) + im = axis.imshow(mutual_inductance, cmap="RdBu_r", aspect="auto") + axis.set_xlabel("Circuit") + axis.set_ylabel("Circuit") + axis.set_title("PF/CS Plasma Mutual Inductance") + axis.set_xticks(range(n_pf_cs_plasma_circuits)) + axis.set_yticks(range(n_pf_cs_plasma_circuits)) + labels = list(range(1, n_pf_cs_plasma_circuits + 1)) + + if iohcl == 1: + labels[-2] = "CS" + labels[-1] = "Plasma" + axis.set_xticklabels(labels) + axis.set_yticklabels(labels) + + # Add boxes around each cell + for i in range(n_pf_cs_plasma_circuits): + for j in range(n_pf_cs_plasma_circuits): + if mutual_inductance[i, j] != 0: + axis.add_patch( + plt.Rectangle( + (j - 0.5, i - 0.5), + 1, + 1, + fill=False, + edgecolor="black", + linewidth=0.5, + ) + ) + # Add text annotation with values + draw_text( + axis, + j, + i, + f"{mutual_inductance[i, j]:.3e}", + ha="center", + va="center", + color="white", + fontsize=8, + ) + + axis.get_figure().colorbar(im, ax=axis, label="Mutual Inductance (H)") + + +__all__ = [ + "plot_cs_coil_structure", + "plot_cs_turn_structure", + "plot_magnetics_info", + "plot_pf_cs_plasma_mutual_inductance", + "plot_physics_info", + "secs_to_hms", +] diff --git a/process/core/io/plot/summary/magnets/pf.py b/process/core/io/plot/summary/magnets/pf.py new file mode 100644 index 0000000000..9082ce13de --- /dev/null +++ b/process/core/io/plot/summary/magnets/pf.py @@ -0,0 +1,294 @@ +"""Magnets functions for PROCESS summary plots.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +import matplotlib.pyplot as plt +from matplotlib import patches + +from process.core.io.plot.summary.constants import ( + CSCOMPRESSION_COLOUR, + SOLENOID_COLOUR, +) +from process.core.io.plot.summary.geometry import ( + cumulative_radial_build2, +) +from process.core.io.plot.summary.rendering import ( + draw_annotation, + draw_text, +) +from process.models.geometry.pfcoil import pfcoil_geometry + +if TYPE_CHECKING: + from process.core.io.mfile import MFile + + +def plot_pf_coils( + axis: plt.Axes, + mfile: MFile, + scan: int, + colour_scheme: Literal[1, 2], + mirror_negative_x: bool = False, +): + """Function to plot PF coils + + Parameters + ---------- + axis : + axis object to plot to + mfile : + MFILE + scan : + scan number to use + colour_scheme : + colour scheme to use for plots + mirror_negative_x : + if True, mirror the plot to the negative x-axis (Default value = False) + """ + # Apply mirror transformation if requested + x_scale = -1 if mirror_negative_x else 1 + + coils_r = [] + coils_z = [] + coils_dr = [] + coils_dz = [] + coil_text = [] + + dr_cs_bore = mfile.get("dr_cs_bore", scan=scan) + dr_cs = mfile.get("dr_cs", scan=scan) + dz_cs_full = mfile.get("dz_cs_full", scan=scan) + + # Number of coils, both PF and CS + number_of_coils = 0 + for item in mfile.data: + if "r_pf_coil_middle[" in item: + number_of_coils += 1 + + # Check for Central Solenoid + iohcl = mfile.get("iohcl", scan=scan) if "iohcl" in mfile.data else 1 + + # If Central Solenoid present, ignore last entry in for loop + # The last entry will be the OH coil in this case + noc = number_of_coils - 1 if iohcl == 1 else number_of_coils + + for coil in range(noc): + coils_r.append(mfile.get(f"r_pf_coil_middle[{coil + 1:01}]", scan=scan)) + coils_z.append(mfile.get(f"z_pf_coil_middle[{coil + 1:01}]", scan=scan)) + coils_dr.append(mfile.get(f"pfdr({coil + 1:01})", scan=scan)) + coils_dz.append(mfile.get(f"pfdz({coil + 1:01})", scan=scan)) + coil_text.append(str(coil + 1)) + + r_points, z_points, central_coil = pfcoil_geometry( + coils_r=coils_r, + coils_z=coils_z, + coils_dr=coils_dr, + coils_dz=coils_dz, + dr_cs_bore=dr_cs_bore, + dr_cs=dr_cs, + ohdz=dz_cs_full, + ) + + # Plot CS compression structure + r_precomp_outer, r_precomp_inner = cumulative_radial_build2( + "dr_cs_precomp", mfile, scan + ) + axis.add_patch( + patches.Rectangle( + xy=(x_scale * r_precomp_inner, central_coil.anchor_z), + width=(x_scale * (r_precomp_outer - r_precomp_inner)), + height=central_coil.height, + facecolor=CSCOMPRESSION_COLOUR[colour_scheme - 1], + ) + ) + + # Get axis height for fontsize scaling + axis_height = ( + axis + .get_window_extent() + .transformed(axis.figure.dpi_scale_trans.inverted()) + .height + ) + + for i in range(len(coils_r)): + mirrored_r_points = [x_scale * r for r in r_points[i]] + axis.plot(mirrored_r_points, z_points[i], color="black") + # Scale fontsize relative to axis height and coil size + fontsize = max(6, axis_height * abs(coils_dr[i] * coils_dz[i]) * 1.5) + draw_text( + axis, + x_scale * coils_r[i], + coils_z[i] - 0.05, + coil_text[i], + ha="center", + va="center", + fontsize=fontsize, + ) + axis.add_patch( + patches.Rectangle( + xy=(x_scale * central_coil.anchor_x, central_coil.anchor_z), + width=x_scale * central_coil.width, + height=central_coil.height, + facecolor=SOLENOID_COLOUR[colour_scheme - 1], + edgecolor="black", + linewidth=1, + ) + ) + axis.add_patch( + patches.Rectangle( + xy=(0.0, central_coil.anchor_z), + width=x_scale * central_coil.anchor_x, + height=central_coil.height, + facecolor="grey", + alpha=0.5, + ) + ) + + +def plot_pf_dimensions( + axis: plt.Axes, mfile: MFile, scan: int, colour_scheme: Literal[1, 2] = 1 +) -> None: + """Plot the PF coil dimensions on the given axis.""" + r_pf_coil_middle = [] + z_pf_coil_middle = [] + radial_thicknesses = [] + vertical_thicknesses = [] + iohcl = mfile.get("iohcl", scan=scan) if "iohcl" in mfile.data else 1 + x = 1 if iohcl == 0 else 2 + for coil in range(int(mfile.get("n_pf_cs_plasma_circuits", scan=scan) - x)): + r_pf_coil_middle.append(mfile.get(f"r_pf_coil_middle[{coil + 1}]", scan=scan)) + z_pf_coil_middle.append(mfile.get(f"z_pf_coil_middle[{coil + 1}]", scan=scan)) + radial_thicknesses.append(mfile.get(f"pfdr({coil + 1})", scan=scan)) + vertical_thicknesses.append(mfile.get(f"pfdz({coil + 1})", scan=scan)) + + plot_pf_coils(axis=axis, mfile=mfile, scan=scan, colour_scheme=colour_scheme) + + if r_pf_coil_middle: + for r_middle, z_middle, dr_coil, dz_coil in zip( + r_pf_coil_middle, + z_pf_coil_middle, + radial_thicknesses, + vertical_thicknesses, + strict=False, + ): + half_radial_thickness = dr_coil / 2 + half_vertical_thickness = dz_coil / 2 + coil_left = r_middle - half_radial_thickness + coil_right = r_middle + half_radial_thickness + coil_bottom = z_middle - half_vertical_thickness + coil_top = z_middle + half_vertical_thickness + + for x_position in (coil_left, r_middle, coil_right): + axis.axvline( + x=x_position, + color="r", + linewidth=0.8, + linestyle="--" if x_position == r_middle else "-", + alpha=0.3, + zorder=4, + ) + + for y_position in (coil_bottom, z_middle, coil_top): + axis.axhline( + y=y_position, + xmax=coil_left, + color="r", + linewidth=0.8, + linestyle="--" if y_position == z_middle else "-", + alpha=0.3, + zorder=4, + ) + + draw_annotation( + axis, + f"({r_middle:.3f}, {z_middle:.3f})", + xy=(coil_left * 0.925, z_middle), + ha="right", + va="center", + fontsize=8, + zorder=6, + bbox={ + "boxstyle": "round,pad=0.2", + "fc": "white", + "alpha": 1.0, + "ec": "none", + }, + ) + + radial_arrow_y = coil_bottom if z_middle < 0 else coil_top + radial_label_offset = (0, -24) if z_middle < 0 else (0, 4) + radial_label_va = "top" if z_middle < 0 else "bottom" + draw_annotation( + axis, + "", + xy=(coil_left, radial_arrow_y), + xytext=(coil_right, radial_arrow_y), + arrowprops={ + "arrowstyle": "<->", + "linewidth": 0.8, + "color": "red", + "shrinkA": 0, + "shrinkB": 0, + }, + zorder=5, + ) + draw_annotation( + axis, + f"ΔR={abs(dr_coil):.3f}", + xy=(r_middle, radial_arrow_y), + xytext=radial_label_offset, + textcoords="offset points", + ha="center", + va=radial_label_va, + fontsize=8, + zorder=6, + bbox={ + "boxstyle": "round,pad=0.2", + "fc": "white", + "alpha": 1.0, + "ec": "none", + }, + ) + + vertical_arrow_x = coil_right + draw_annotation( + axis, + "", + xy=(vertical_arrow_x, coil_bottom), + xytext=(vertical_arrow_x, coil_top), + arrowprops={ + "arrowstyle": "<->", + "linewidth": 0.8, + "color": "red", + "shrinkA": 0, + "shrinkB": 0, + }, + ) + draw_annotation( + axis, + f"ΔZ={abs(dz_coil):.3f}", + xy=(vertical_arrow_x, z_middle), + xytext=(4, 0), + textcoords="offset points", + ha="left", + va="center", + fontsize=8, + zorder=6, + bbox={ + "boxstyle": "round,pad=0.2", + "fc": "white", + "alpha": 1.0, + "ec": "none", + }, + ) + + axis.set_title("PF Coil Dimensions") + axis.set_xlabel("R [m]") + axis.set_ylabel("Z [m]") + axis.set_xlim(left=0.0) + axis.minorticks_on() + axis.grid(True, alpha=0.3) + axis.set_aspect("equal", adjustable="box") + + +__all__ = ["plot_pf_coils", "plot_pf_dimensions"] diff --git a/process/core/io/plot/summary/magnets/tf.py b/process/core/io/plot/summary/magnets/tf.py new file mode 100644 index 0000000000..7eb339e4fe --- /dev/null +++ b/process/core/io/plot/summary/magnets/tf.py @@ -0,0 +1,3737 @@ +"""Magnets functions for PROCESS summary plots.""" + +from __future__ import annotations + +import json +from typing import TYPE_CHECKING, Literal + +import matplotlib.pyplot as plt +import numpy as np +from matplotlib import patches +from matplotlib.patches import Circle, Rectangle +from matplotlib.path import Path as mplPath + +from process.core.io.plot.summary.common import ( + box_style, +) +from process.core.io.plot.summary.constants import ( + TFC_COLOUR, + THERMAL_SHIELD_COLOUR, + rtangle, + rtangle2, +) +from process.core.io.plot.summary.magnets.cables import ( + plot_hts_tape_geometry, +) +from process.core.io.plot.summary.rendering import ( + draw_annotation, + draw_text, +) +from process.data_structure.superconducting_tf_coil_variables import ( + TFWPIntegerTurnType, +) +from process.models.geometry.tfcoil import ( + tfcoil_geometry_d_shape, + tfcoil_geometry_rectangular_shape, +) +from process.models.superconductors import SuperconductorModel +from process.models.tfcoil import quench +from process.models.tfcoil.base import TFCoilShapeModel, TFPlasmaCaseType + +if TYPE_CHECKING: + from process.core.io.mfile import MFile + + +def TF_outboard(axis: plt.Axes, item, n_tf_coils, r3, r4, w, facecolor): + """Plot outboard TF coils""" + spacing = 2 * np.pi / n_tf_coils + ang = item * spacing + dx = w * np.sin(ang) + dy = w * np.cos(ang) + x1 = r3 * np.cos(ang) + dx + y1 = r3 * np.sin(ang) - dy + x2 = r4 * np.cos(ang) + dx + y2 = r4 * np.sin(ang) - dy + x3 = r4 * np.cos(ang) - dx + y3 = r4 * np.sin(ang) + dy + x4 = r3 * np.cos(ang) - dx + y4 = r3 * np.sin(ang) + dy + verts = [(x1, y1), (x2, y2), (x3, y3), (x4, y4), (x1, y1)] + path = mplPath(verts, closed=True) + patch = patches.PathPatch(path, facecolor=facecolor, lw=0) + axis.add_patch(patch) + + +def plot_tf_coils( + axis: plt.Axes, + mfile: MFile, + scan: int, + colour_scheme: Literal[1, 2], + mirror_negative_x: bool = False, +): + """Function to plot TF coils + + Parameters + ---------- + axis : + axis object to plot to + mfile : + MFILE + scan : + scan number to use + colour_scheme : + colour scheme to use for plots + mirror_negative_x : + if True, mirror the plot to the negative x-axis (Default value = False) + """ + # Apply mirror transformation if requested + x_scale = -1 if mirror_negative_x else 1 + + # Arc points + # MDK Only 4 points now required for elliptical arcs + x1 = mfile.get("r_tf_arc(1)", scan=scan) + y1 = mfile.get("z_tf_arc(1)", scan=scan) + x2 = mfile.get("r_tf_arc(2)", scan=scan) + y2 = mfile.get("z_tf_arc(2)", scan=scan) + x3 = mfile.get("r_tf_arc(3)", scan=scan) + y3 = mfile.get("z_tf_arc(3)", scan=scan) + x4 = mfile.get("r_tf_arc(4)", scan=scan) + y4 = mfile.get("z_tf_arc(4)", scan=scan) + x5 = mfile.get("r_tf_arc(5)", scan=scan) + y5 = mfile.get("z_tf_arc(5)", scan=scan) + + dr_tf_inboard = mfile.get("dr_tf_inboard", scan=scan) + dr_tf_outboard = mfile.get("dr_tf_outboard", scan=scan) + dr_shld_thermal_inboard = mfile.get("dr_shld_thermal_inboard", scan=scan) + dr_shld_thermal_outboard = mfile.get("dr_shld_thermal_outboard", scan=scan) + dr_tf_shld_gap = mfile.get("dr_tf_shld_gap", scan=scan) + if y3 != 0: + print("TF coil geometry: The value of z_tf_arc(3) is not zero, but should be.") + + if dr_shld_thermal_inboard != dr_shld_thermal_outboard: + print( + "dr_shld_thermal_inboard and dr_shld_thermal_outboard are" + " different. Using dr_shld_thermal_inboardfor the poloidal plot of" + " the thermal shield." + ) + + for offset, colour in ( + ( + dr_shld_thermal_inboard + dr_tf_shld_gap, + THERMAL_SHIELD_COLOUR[colour_scheme - 1], + ), + (dr_tf_shld_gap, "white"), + ( + 0.0, + ( + TFC_COLOUR[colour_scheme - 1] + if mfile.get("i_tf_sup", scan=scan) != 0 + else "#b87333" + ), + ), + ): + # Check for TF coil shape + if "i_tf_shape" in mfile.data: + i_tf_shape = int(mfile.get("i_tf_shape", scan=scan)) + else: + i_tf_shape = 1 + + if i_tf_shape == TFCoilShapeModel.PICTURE_FRAME: + rects = tfcoil_geometry_rectangular_shape( + x1=x1, + x2=x2, + x4=x4, + x5=x5, + y1=y1, + y2=y2, + y4=y4, + y5=y5, + dr_tf_inboard=dr_tf_inboard, + dr_tf_outboard=dr_tf_outboard, + offset_in=offset, + ) + + else: + rects, verts = tfcoil_geometry_d_shape( + x1=x1, + x2=x2, + x3=x3, + x4=x4, + x5=x5, + y1=y1, + y2=y2, + y4=y4, + y5=y5, + dr_tf_inboard=dr_tf_inboard, + rtangle=rtangle, + rtangle2=rtangle2, + offset_in=offset, + ) + + for vert in verts: + # Mirror vertices if needed + mirrored_vert = [[x_scale * point[0], point[1]] for point in vert] + path = mplPath(mirrored_vert, closed=True) + patch = patches.PathPatch(path, facecolor=colour, lw=0) + axis.add_patch(patch) + + for rec in rects: + axis.add_patch( + patches.Rectangle( + xy=(x_scale * rec.anchor_x, rec.anchor_z), + width=x_scale * rec.width, + height=rec.height, + facecolor=colour, + ) + ) + + +def plot_superconducting_tf_wp(axis: plt.Axes, mfile: MFile, scan: int, fig): + """Plots inboard TF coil and winding pack. + + Parameters + ---------- + axis : matplotlib.axes object + Axis object to plot to. + mfile : MFILE data object + Object containing data for the plot. + scan : int + Scan number to use. + """ + # Import the TF variables + r_tf_inboard_in = mfile.get("r_tf_inboard_in", scan=scan) + r_tf_inboard_out = mfile.get("r_tf_inboard_out", scan=scan) + dx_tf_wp_primary_toroidal = mfile.get("dx_tf_wp_primary_toroidal", scan=scan) + dx_tf_side_case_peak = mfile.get("dx_tf_side_case_peak", scan=scan) + dx_tf_wp_secondary_toroidal = mfile.get("dx_tf_wp_secondary_toroidal", scan=scan) + dr_tf_wp_with_insulation = mfile.get("dr_tf_wp_with_insulation", scan=scan) + r_tf_wp_inboard_inner = mfile.get("r_tf_wp_inboard_inner", scan=scan) + dx_tf_wp_insulation = mfile.get("dx_tf_wp_insulation", scan=scan) + n_tf_coil_turns = round(mfile.get("n_tf_coil_turns", scan=scan)) + i_tf_wp_geom = round(mfile.get("i_tf_wp_geom", scan=scan)) + i_tf_sup = round(mfile.get("i_tf_sup", scan=scan)) + i_tf_case_geom = mfile.get("i_tf_case_geom", scan=scan) + i_tf_turns_integer = mfile.get("i_tf_turns_integer", scan=scan) + b_tf_inboard_peak_symmetric = mfile.get("b_tf_inboard_peak_symmetric", scan=scan) + b_tf_inboard_peak_with_ripple = mfile.get("b_tf_inboard_peak_with_ripple", scan=scan) + f_b_tf_inboard_peak_ripple_symmetric = mfile.get( + "f_b_tf_inboard_peak_ripple_symmetric", scan=scan + ) + r_b_tf_inboard_peak = mfile.get("r_b_tf_inboard_peak", scan=scan) + dx_tf_wp_insertion_gap = mfile.get("dx_tf_wp_insertion_gap", scan=scan) + r_tf_wp_inboard_outer = mfile.get("r_tf_wp_inboard_outer", scan=scan) + r_tf_wp_inboard_centre = mfile.get("r_tf_wp_inboard_centre", scan=scan) + + if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.INTEGER: + turn_layers = mfile.get("n_tf_wp_layers", scan=scan) + turn_pancakes = mfile.get("n_tf_wp_pancakes", scan=scan) + + # Superconducting coil check + if i_tf_sup == 1: + axis.add_patch( + Circle( + (0, 0), + r_tf_inboard_in, + facecolor="none", + edgecolor="black", + linestyle="--", + ), + ) + + if i_tf_case_geom == TFPlasmaCaseType.CIRCULAR: + axis.add_patch( + Circle( + (0, 0), + r_tf_inboard_out, + facecolor="none", + edgecolor="black", + linestyle="--", + ), + ) + + # Equations for plotting the TF case + rad_tf_coil_inboard_toroidal_half = mfile.get( + "rad_tf_coil_inboard_toroidal_half", scan=scan + ) + + # X points for inboard case curve + x11 = r_tf_inboard_in * np.cos( + np.linspace( + rad_tf_coil_inboard_toroidal_half, + -rad_tf_coil_inboard_toroidal_half, + 256, + endpoint=True, + ) + ) + # Y points for inboard case curve + y11 = r_tf_inboard_in * np.sin( + np.linspace( + rad_tf_coil_inboard_toroidal_half, + -rad_tf_coil_inboard_toroidal_half, + 256, + endpoint=True, + ) + ) + # Check for plasma side case type + if i_tf_case_geom == TFPlasmaCaseType.CIRCULAR: + # Rounded case + + # X points for outboard case curve + x12 = r_tf_inboard_out * np.cos( + np.linspace( + rad_tf_coil_inboard_toroidal_half, + -rad_tf_coil_inboard_toroidal_half, + 256, + endpoint=True, + ) + ) + + elif i_tf_case_geom == TFPlasmaCaseType.STRAIGHT: + # Flat case + + # X points for outboard case + x12 = np.full(256, r_tf_inboard_out) + else: + raise NotImplementedError("i_tf_case_geom must be 0 or 1") + + # Y points for outboard case + y12 = r_tf_inboard_out * np.sin( + np.linspace( + rad_tf_coil_inboard_toroidal_half, + -rad_tf_coil_inboard_toroidal_half, + 256, + endpoint=True, + ) + ) + + # Cordinates of the top and bottom of case curves, + # used to plot the lines connecting the inside and outside of the case + y13 = [y11[0], y12[0]] + x13 = [x11[0], x12[0]] + y14 = [y11[-1], y12[-1]] + x14 = [x11[-1], x12[-1]] + + # Plot the case outline + axis.plot(x11, y11, color="black") + axis.plot(x12, y12, color="black") + axis.plot(x13, y13, color="black") + axis.plot(x14, y14, color="black") + + # Fill in the case segemnts + + # Upper main + if i_tf_case_geom == TFPlasmaCaseType.CIRCULAR: + axis.fill_between( + [ + (r_tf_inboard_in * np.cos(rad_tf_coil_inboard_toroidal_half)), + (r_tf_inboard_out * np.cos(rad_tf_coil_inboard_toroidal_half)), + ], + y13, + color="grey", + alpha=0.25, + ) + # Lower main + axis.fill_between( + [ + (r_tf_inboard_in * np.cos(rad_tf_coil_inboard_toroidal_half)), + (r_tf_inboard_out * np.cos(rad_tf_coil_inboard_toroidal_half)), + ], + y14, + color="grey", + alpha=0.25, + ) + axis.fill_between( + x12, + y12, + color="grey", + alpha=0.25, + ) + elif i_tf_case_geom == TFPlasmaCaseType.STRAIGHT: + axis.fill_between( + [ + (r_tf_inboard_in * np.cos(rad_tf_coil_inboard_toroidal_half)), + (r_tf_inboard_out), + ], + y13, + color="grey", + alpha=0.25, + ) + # Lower main + axis.fill_between( + [ + (r_tf_inboard_in * np.cos(rad_tf_coil_inboard_toroidal_half)), + (r_tf_inboard_out), + ], + y14, + color="grey", + alpha=0.25, + ) + + # Removes ovelapping colours on inner nose case + axis.fill_between( + x11, + y11, + color="white", + alpha=1.0, + ) + + # Centre line for relative reference + axis.axhline(y=0.0, color="r", linestyle="--", linewidth=0.25) + + # ================================================================ + + # Plot the rectangular WP + if i_tf_wp_geom == 0: + if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.INTEGER: + long_turns = round(turn_layers) + short_turns = round(turn_pancakes) + else: + wp_side_ratio = ( + dr_tf_wp_with_insulation + - (2 * (dx_tf_wp_insulation + dx_tf_wp_insertion_gap)) + ) / ( + dx_tf_wp_primary_toroidal + - (2 * (dx_tf_wp_insulation + dx_tf_wp_insertion_gap)) + ) # row to height + side_unit = n_tf_coil_turns / wp_side_ratio + root_turns = round(np.sqrt(side_unit), 1) + long_turns = round(root_turns * wp_side_ratio) + short_turns = round(root_turns) + + # Plots the surrounding insualtion + axis.add_patch( + Rectangle( + ( + r_tf_wp_inboard_inner, + -(0.5 * dx_tf_wp_primary_toroidal), + ), + dr_tf_wp_with_insulation, + dx_tf_wp_primary_toroidal, + color="darkgreen", + ), + ) + # Plots the WP inside the insulation + axis.add_patch( + Rectangle( + ( + r_tf_wp_inboard_inner + + dx_tf_wp_insulation + + dx_tf_wp_insertion_gap, + -(0.5 * dx_tf_wp_primary_toroidal) + + dx_tf_wp_insulation + + dx_tf_wp_insertion_gap, + ), + ( + dr_tf_wp_with_insulation + - (2 * (dx_tf_wp_insulation + dx_tf_wp_insertion_gap)) + ), + ( + dx_tf_wp_primary_toroidal + - (2 * (dx_tf_wp_insulation + dx_tf_wp_insertion_gap)) + ), + color="blue", + ) + ) + # Dvides the WP up into the turn segments + for i in range(1, long_turns): + axis.plot( + [ + ( + r_tf_wp_inboard_inner + + dx_tf_wp_insulation + + dx_tf_wp_insertion_gap + ) + + i + * ( + ( + dr_tf_wp_with_insulation + - dx_tf_wp_insulation + - dx_tf_wp_insertion_gap + ) + / long_turns + ), + ( + r_tf_wp_inboard_inner + + dx_tf_wp_insulation + + dx_tf_wp_insertion_gap + ) + + i + * ( + ( + dr_tf_wp_with_insulation + - dx_tf_wp_insulation + - dx_tf_wp_insertion_gap + ) + / long_turns + ), + ], + [ + -0.5 * dx_tf_wp_primary_toroidal + + (dx_tf_wp_insulation + dx_tf_wp_insertion_gap), + 0.5 * dx_tf_wp_primary_toroidal + - (dx_tf_wp_insulation + dx_tf_wp_insertion_gap), + ], + color="white", + linewidth="0.25", + linestyle="dashed", + ) + + for i in range(1, short_turns): + axis.plot( + [ + ( + r_tf_wp_inboard_inner + + dx_tf_wp_insulation + + dx_tf_wp_insertion_gap + ), + ( + r_tf_wp_inboard_outer + - dx_tf_wp_insulation + - dx_tf_wp_insertion_gap + ), + ], + [ + ( + -0.5 * dx_tf_wp_primary_toroidal + + dx_tf_wp_insulation + + dx_tf_wp_insertion_gap + ) + + ( + i + * ( + dx_tf_wp_primary_toroidal + - dx_tf_wp_insulation + - dx_tf_wp_insertion_gap + ) + / short_turns + ), + ( + -0.5 * dx_tf_wp_primary_toroidal + + dx_tf_wp_insulation + + dx_tf_wp_insertion_gap + ) + + ( + i + * ( + dx_tf_wp_primary_toroidal + - dx_tf_wp_insulation + - dx_tf_wp_insertion_gap + ) + / short_turns + ), + ], + color="white", + linewidth="0.25", + linestyle="dashed", + ) + + # ================================================================ + + # Plot the double rectangle winding pack + if i_tf_wp_geom == 1: + # Inner WP insulation + axis.add_patch( + Rectangle( + ( + r_tf_wp_inboard_inner, + -(0.5 * dx_tf_wp_secondary_toroidal), + ), + (dr_tf_wp_with_insulation / 2) + (dx_tf_wp_insulation), + dx_tf_wp_secondary_toroidal, + color="darkgreen", + ), + ) + + # Outer WP insulation + axis.add_patch( + Rectangle( + ( + r_tf_wp_inboard_centre, + -(0.5 * dx_tf_wp_primary_toroidal), + ), + (dr_tf_wp_with_insulation / 2), + dx_tf_wp_primary_toroidal, + color="darkgreen", + ), + ) + + # Outer WP + axis.add_patch( + Rectangle( + ( + r_tf_wp_inboard_centre + + dx_tf_wp_insulation + + dx_tf_wp_insertion_gap, + -(0.5 * dx_tf_wp_primary_toroidal) + + dx_tf_wp_insulation + + dx_tf_wp_insertion_gap, + ), + (dr_tf_wp_with_insulation / 2) + - (2 * (dx_tf_wp_insulation + dx_tf_wp_insertion_gap)), + dx_tf_wp_primary_toroidal + - (2 * (dx_tf_wp_insulation + dx_tf_wp_insertion_gap)), + color="blue", + ), + ) + # Inner WP + axis.add_patch( + Rectangle( + ( + r_tf_wp_inboard_inner + + dx_tf_wp_insulation + + dx_tf_wp_insertion_gap, + -(0.5 * dx_tf_wp_secondary_toroidal) + + dx_tf_wp_insulation + + dx_tf_wp_insertion_gap, + ), + (dr_tf_wp_with_insulation / 2), + dx_tf_wp_secondary_toroidal + - (2 * (dx_tf_wp_insulation + dx_tf_wp_insertion_gap)), + color="blue", + ), + ) + + # ================================================================ + + # Trapezium WP + if i_tf_wp_geom == 2: + # WP insulation + x = [ + r_tf_wp_inboard_inner, + r_tf_wp_inboard_inner, + r_tf_wp_inboard_outer, + r_tf_wp_inboard_outer, + ] + y = [ + (-0.5 * dx_tf_wp_secondary_toroidal), + (0.5 * dx_tf_wp_secondary_toroidal), + (0.5 * dx_tf_wp_primary_toroidal), + (-0.5 * dx_tf_wp_primary_toroidal), + ] + axis.add_patch( + patches.Polygon( + xy=list(zip(x, y, strict=False)), + color="darkgreen", + ) + ) + + # WP + x = [ + r_tf_wp_inboard_inner + dx_tf_wp_insulation + dx_tf_wp_insertion_gap, + r_tf_wp_inboard_inner + dx_tf_wp_insulation + dx_tf_wp_insertion_gap, + (r_tf_wp_inboard_outer - dx_tf_wp_insulation - dx_tf_wp_insertion_gap), + (r_tf_wp_inboard_outer - dx_tf_wp_insulation - dx_tf_wp_insertion_gap), + ] + y = [ + ( + -0.5 * dx_tf_wp_secondary_toroidal + + dx_tf_wp_insulation + + dx_tf_wp_insertion_gap + ), + ( + 0.5 * dx_tf_wp_secondary_toroidal + - dx_tf_wp_insulation + - dx_tf_wp_insertion_gap + ), + ( + 0.5 * dx_tf_wp_primary_toroidal + - dx_tf_wp_insulation + - dx_tf_wp_insertion_gap + ), + ( + -0.5 * dx_tf_wp_primary_toroidal + + dx_tf_wp_insulation + + dx_tf_wp_insertion_gap + ), + ] + axis.add_patch( + patches.Polygon( + xy=list(zip(x, y, strict=False)), + color="blue", + ) + ) + + # Plot a dot for the location of the peak field + axis.plot( + r_b_tf_inboard_peak, + 0, + marker="o", + color="red", + label=( + "Peak axisymmetric field:" + f" {b_tf_inboard_peak_symmetric:.3f} T\n" + "Peak non-axisymmetric field with ripple: " + f"{b_tf_inboard_peak_with_ripple:.3f} T\n" + "$\\frac{B_{\\text{axisymmetric}}}{B_{\\text{non-axisymmetric}}}$: " + f"{f_b_tf_inboard_peak_ripple_symmetric:.3f}\n" + f"$r_{{\\text{{peak}}}}$={r_b_tf_inboard_peak:.3f} m" + ), + ) + + # Plot a horizontal line at y = dx_tf_wp_inner_toroidal + axis.axhline( + y=dx_tf_wp_secondary_toroidal / 2, + color="black", + linestyle="--", + linewidth=0.6, + alpha=0.5, + ) + # Plot a horizontal line at y = dx_tf_wp_inner_toroidal + axis.axhline( + y=-dx_tf_wp_secondary_toroidal / 2, + color="black", + linestyle="--", + linewidth=0.6, + alpha=0.5, + ) + axis.axhline( + y=dx_tf_wp_primary_toroidal / 2, + color="black", + linestyle="--", + linewidth=0.6, + alpha=0.5, + ) + axis.axhline( + y=-dx_tf_wp_primary_toroidal / 2, + color="black", + linestyle="--", + linewidth=0.6, + alpha=0.5, + ) + # Max toroidal width including side case + axis.axhline( + y=(dx_tf_wp_primary_toroidal / 2) + dx_tf_side_case_peak, + color="black", + linestyle="--", + linewidth=0.6, + alpha=0.5, + ) + + axis.axhline( + y=-(dx_tf_wp_primary_toroidal / 2) - dx_tf_side_case_peak, + color="black", + linestyle="--", + linewidth=0.6, + alpha=0.5, + ) + + axis.axvline( + x=r_tf_inboard_in, + color="black", + linestyle="--", + linewidth=0.6, + alpha=0.5, + ) + axis.axvline( + x=r_tf_wp_inboard_inner, + color="black", + linestyle="--", + linewidth=0.6, + alpha=0.5, + ) + axis.axvline( + x=r_tf_wp_inboard_outer, + color="black", + linestyle="--", + linewidth=0.6, + alpha=0.5, + ) + axis.axvline( + x=r_tf_wp_inboard_centre, + color="black", + linestyle="--", + linewidth=0.6, + alpha=0.5, + ) + axis.axvline( + x=r_tf_inboard_out, + color="black", + linestyle="--", + linewidth=0.6, + alpha=0.5, + ) + + # Add info about the steel casing surrounding the WP + textstr_casing = ( + "$\\mathbf{Casing:}$\n\nCoil half angle:" + f" {mfile.get('rad_tf_coil_inboard_toroidal_half', scan=scan):.3f}" + " radians\n\n$\\text{Full Coil Case:}$\n$r_{start}" + " \\rightarrow r_{end}$:" + f" {mfile.get('r_tf_inboard_in', scan=scan):.3f} $\\rightarrow$" + f" {mfile.get('r_tf_inboard_out', scan=scan):.3f} m\n$\\Delta r$:" + f" {mfile.get('dr_tf_inboard', scan=scan):.3f} m\nArea of casing" + f" around WP: {mfile.get('a_tf_coil_inboard_case', scan=scan):.3f}" + " $\\mathrm{m}^2$\n\n$\\text{Nose Case:}$\n$r_{start}" + " \\rightarrow r_{end}$:" + f" {mfile.get('r_tf_inboard_in', scan=scan):.3f} $\\rightarrow$" + f" {mfile.get('r_tf_wp_inboard_inner', scan=scan):.3f} m\n$\\Delta" + f" r$: {mfile.get('dr_tf_nose_case', scan=scan):.3f} m\n$A$:" + f" {mfile.get('a_tf_coil_nose_case', scan=scan):.3f}" + " $\\mathrm{m}^2$\n\n$\\text{Plasma Case:}$\n$r_{start}" + " \\rightarrow r_{end}$:" + f" {mfile.get('r_tf_wp_inboard_outer', scan=scan):.3f}" + f" $\\rightarrow$ {mfile.get('r_tf_inboard_out', scan=scan):.3f}" + f" m\n$\\Delta r$: {mfile.get('dr_tf_plasma_case', scan=scan):.3f}" + f" m\n$A$: {mfile.get('a_tf_plasma_case', scan=scan):.3f}" + " $\\mathrm{m}^2$\n\n$\\text{Side Case:}$\nMinimum $\\Delta" + f" r$: {mfile.get('dx_tf_side_case_min', scan=scan):.3f}" + " m\nAverage $\\Delta r$:" + f" {mfile.get('dx_tf_side_case_average', scan=scan):.3f} m\nMax" + " $\\Delta r$:" + f" {mfile.get('dx_tf_side_case_peak', scan=scan):.3f} m" + ) + draw_text( + axis, + 0.55, + 0.975, + textstr_casing, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox={ + "boxstyle": "round", + "facecolor": "grey", + "alpha": 1.0, + "linewidth": 2, + }, + ) + + # Add info about the steel casing surrounding the WP + textstr_wp_insulation = ( + "$\\mathbf{Ground \\ Insulation:}$\n\nArea of insulation around" + f" WP: {mfile.get('a_tf_wp_ground_insulation', scan=scan):.3f}" + " $\\mathrm{m}^2$\n$\\Delta r$:" + f" {mfile.get('dx_tf_wp_insulation', scan=scan):.4f} m\n\nWP" + " Insertion Gap:\n$\\Delta r$:" + f" {mfile.get('dx_tf_wp_insertion_gap', scan=scan):.4f} m" + ) + draw_text( + axis, + 0.55, + 0.575, + textstr_wp_insulation, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox={ + "boxstyle": "round", + "facecolor": "green", + "alpha": 1.0, + "linewidth": 2, + }, + ) + + # Add info about the Winding Pack + textstr_wp = ( + "$\\mathbf{Winding \\ Pack:}$\n\n$N_{\\text{turns}}$:" + f" {int(mfile.get('n_tf_coil_turns', scan=scan))}" + " turns\n$r_{start} \\rightarrow r_{end}$:" + f" {mfile.get('r_tf_wp_inboard_inner', scan=scan):.3f}" + " $\\rightarrow$" + f" {mfile.get('r_tf_wp_inboard_outer', scan=scan):.3f} m\n$\\Delta" + f" r$: {mfile.get('dr_tf_wp_with_insulation', scan=scan):.3f}" + " m\n\n$A$, with insulation:" + f" {mfile.get('a_tf_wp_with_insulation', scan=scan):.4f}" + " $\\mathrm{m}^2$\n$A$, no insulation:" + f" {mfile.get('a_tf_wp_no_insulation', scan=scan):.4f}" + " $\\mathrm{m}^2$\n$A$, total turn insulation:" + f" {mfile.get('a_tf_coil_wp_turn_insulation', scan=scan):.4f}" + " $\\mathrm{m}^2$\n$A$, total turn steel:" + f" {mfile.get('a_tf_wp_steel', scan=scan):.4f}" + " $\\mathrm{m}^2$\n$A$, total conductor:" + f" {mfile.get('a_tf_wp_conductor', scan=scan):.4f}" + " $\\mathrm{m}^2$\n$A$, total non-cooling void:" + f" {mfile.get('a_tf_wp_extra_void', scan=scan):.4f}" + " $\\mathrm{m}^2$\n\nPrimary WP:\n$\\Delta x$:" + f" {mfile.get('dx_tf_wp_primary_toroidal', scan=scan):.4f}" + " m\n\nSecondary WP:\n$\\Delta x$:" + f" {mfile.get('dx_tf_wp_secondary_toroidal', scan=scan):.4f}" + " m\n\n$J$ no insulation:" + f" {mfile.get('j_tf_wp', scan=scan) / 1e6:.4f} MA/m$^2$" + ) + + draw_text( + axis, + 0.775, + 0.95, + textstr_wp, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + color="white", + transform=fig.transFigure, + bbox={ + "boxstyle": "round", + "facecolor": "blue", + "alpha": 1.0, + "linewidth": 2, + }, + ) + + # Add info about the Winding Pack + textstr_general_info = ( + "$\\mathbf{General \\ info:}$\n\n$N_{\\text{TF,coil}}$:" + f" {mfile.get('n_tf_coils', scan=scan)}\nSelf inductance of single" + f" coil: {mfile.get('ind_tf_coil', scan=scan) * 1e6:.4f}" + " $\\mu$H\nStored energy of all coils:" + f" {mfile.get('e_tf_magnetic_stored_total_gj', scan=scan):.4f}" + " GJ\nStored energy of a single coil:" + f" {mfile.get('e_tf_coil_magnetic_stored', scan=scan) / 1e9:.2f}" + " GJ\nTotal area of steel in coil:" + f" {mfile.get('a_tf_coil_inboard_steel', scan=scan):.4f}" + " $\\mathrm{m}^2$\nTotal area fraction of steel:" + f" {mfile.get('f_a_tf_coil_inboard_steel', scan=scan):.4f}\nTotal" + " area fraction of insulation:" + f" {mfile.get('f_a_tf_coil_inboard_insulation', scan=scan):.4f}\n$A$," + " all insulation in coil:" + f" {mfile.get('a_tf_coil_inboard_insulation', scan=scan):.4f}" + " $\\mathrm{m}^2$\n" + ) + draw_text( + axis, + 0.775, + 0.58, + textstr_general_info, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox={ + "boxstyle": "round", + "facecolor": "wheat", + "alpha": 1.0, + "linewidth": 2, + }, + ) + + axis.minorticks_on() + axis.set_xlim(r_tf_inboard_in * 0.8, r_tf_inboard_out * 1.1) + axis.set_ylim((y14[-1] * 1.25), (-y14[-1] * 1.25)) + + axis.set_title("Top-down view of inboard TF coil at midplane") + axis.set_xlabel("Radial distance [m]") + axis.set_ylabel("Toroidal distance [m]") + axis.legend(loc="upper left") + + +def plot_resistive_tf_wp(axis: plt.Axes, mfile: MFile, scan: int, fig): + """Plots inboard TF coil and winding pack. + + Parameters + ---------- + axis : matplotlib.axes object + Axis object to plot to. + mfile : MFILE data object + Object containing data for the plot. + scan : int + Scan number to use. + """ + # Import the TF variables + r_tf_inboard_in = mfile.get("r_tf_inboard_in", scan=scan) + r_tf_inboard_out = mfile.get("r_tf_inboard_out", scan=scan) + + r_tf_wp_inboard_inner = mfile.get("r_tf_wp_inboard_inner", scan=scan) + i_tf_case_geom = mfile.get("i_tf_case_geom", scan=scan) + b_tf_inboard_peak_symmetric = mfile.get("b_tf_inboard_peak_symmetric", scan=scan) + r_b_tf_inboard_peak = mfile.get("r_b_tf_inboard_peak", scan=scan) + r_tf_wp_inboard_outer = mfile.get("r_tf_wp_inboard_outer", scan=scan) + r_tf_wp_inboard_centre = mfile.get("r_tf_wp_inboard_centre", scan=scan) + dx_tf_wp_insulation = mfile.get("dx_tf_wp_insulation", scan=scan) + + axis.add_patch( + Circle( + (0, 0), + r_tf_inboard_in, + facecolor="none", + edgecolor="black", + linestyle="--", + ), + ) + + if i_tf_case_geom == TFPlasmaCaseType.CIRCULAR: + axis.add_patch( + Circle( + (0, 0), + r_tf_inboard_out, + facecolor="none", + edgecolor="black", + linestyle="--", + ), + ) + + # Equations for plotting the TF case + rad_tf_coil_inboard_toroidal_half = mfile.get( + "rad_tf_coil_inboard_toroidal_half", scan=scan + ) + + # X points for inboard case curve + x11 = r_tf_inboard_in * np.cos( + np.linspace( + rad_tf_coil_inboard_toroidal_half, + -rad_tf_coil_inboard_toroidal_half, + 256, + endpoint=True, + ) + ) + # Y points for inboard case curve + y11 = r_tf_inboard_in * np.sin( + np.linspace( + rad_tf_coil_inboard_toroidal_half, + -rad_tf_coil_inboard_toroidal_half, + 256, + endpoint=True, + ) + ) + # Check for plasma side case type + if i_tf_case_geom == TFPlasmaCaseType.CIRCULAR: + # Rounded case + + # X points for outboard case curve + x12 = r_tf_inboard_out * np.cos( + np.linspace( + rad_tf_coil_inboard_toroidal_half, + -rad_tf_coil_inboard_toroidal_half, + 256, + endpoint=True, + ) + ) + + elif i_tf_case_geom == TFPlasmaCaseType.STRAIGHT: + # Flat case + + # X points for outboard case + x12 = np.full(256, r_tf_inboard_out) + else: + raise NotImplementedError("i_tf_case_geom must be 0 or 1") + + # Y points for outboard case + y12 = r_tf_inboard_out * np.sin( + np.linspace( + rad_tf_coil_inboard_toroidal_half, + -rad_tf_coil_inboard_toroidal_half, + 256, + endpoint=True, + ) + ) + + # Cordinates of the top and bottom of case curves, + # used to plot the lines connecting the inside and outside of the case + y13 = [y11[0], y12[0]] + x13 = [x11[0], x12[0]] + y14 = [y11[-1], y12[-1]] + x14 = [x11[-1], x12[-1]] + + # Plot the case outline + axis.plot(x11, y11, color="black") + axis.plot(x12, y12, color="black") + axis.plot(x13, y13, color="black") + axis.plot(x14, y14, color="black") + + # Fill in the case segemnts + + # Upper main + if i_tf_case_geom == TFPlasmaCaseType.CIRCULAR: + axis.fill_between( + [ + (r_tf_inboard_in * np.cos(rad_tf_coil_inboard_toroidal_half)), + (r_tf_inboard_out * np.cos(rad_tf_coil_inboard_toroidal_half)), + ], + y13, + color="grey", + alpha=0.25, + ) + # Lower main + axis.fill_between( + [ + (r_tf_inboard_in * np.cos(rad_tf_coil_inboard_toroidal_half)), + (r_tf_inboard_out * np.cos(rad_tf_coil_inboard_toroidal_half)), + ], + y14, + color="grey", + alpha=0.25, + ) + axis.fill_between( + x12, + y12, + color="grey", + alpha=0.25, + ) + elif i_tf_case_geom == TFPlasmaCaseType.STRAIGHT: + axis.fill_between( + [ + (r_tf_inboard_in * np.cos(rad_tf_coil_inboard_toroidal_half)), + (r_tf_inboard_out), + ], + y13, + color="grey", + alpha=0.25, + ) + # Lower main + axis.fill_between( + [ + (r_tf_inboard_in * np.cos(rad_tf_coil_inboard_toroidal_half)), + (r_tf_inboard_out), + ], + y14, + color="grey", + alpha=0.25, + ) + + # Removes ovelapping colours on inner nose case + axis.fill_between( + x11, + y11, + color="white", + alpha=1.0, + ) + + # Centre line for relative reference + axis.axhline(y=0.0, color="r", linestyle="--", linewidth=0.25) + + # ================================================================ + + # Plot the WP insulation + + # X points for inboard insulation curve + x11 = r_tf_wp_inboard_inner * np.cos( + np.linspace( + rad_tf_coil_inboard_toroidal_half, + -rad_tf_coil_inboard_toroidal_half, + 500, + endpoint=True, + ) + ) + # Y points for inboard insulation curve + y11 = r_tf_wp_inboard_inner * np.sin( + np.linspace( + rad_tf_coil_inboard_toroidal_half, + -rad_tf_coil_inboard_toroidal_half, + 500, + endpoint=True, + ) + ) + + # X points for outboard insulation curve + x12 = r_tf_wp_inboard_outer * np.cos( + np.linspace( + rad_tf_coil_inboard_toroidal_half, + -rad_tf_coil_inboard_toroidal_half, + 500, + endpoint=True, + ) + ) + + # Y points for outboard insulation curve + y12 = r_tf_wp_inboard_outer * np.sin( + np.linspace( + rad_tf_coil_inboard_toroidal_half, + -rad_tf_coil_inboard_toroidal_half, + 500, + endpoint=True, + ) + ) + + # Cordinates of the top and bottom of WP insulation curves, + y13 = [y11[0], y12[0]] + x13 = [x11[0], x12[0]] + y14 = [y11[-1], y12[-1]] + x14 = [x11[-1], x12[-1]] + + # Plot the insualtion outline + axis.plot(x11, y11, color="black") + axis.plot(x12, y12, color="black") + axis.plot(x13, y13, color="black") + axis.plot(x14, y14, color="black") + + # Upper main + if i_tf_case_geom == TFPlasmaCaseType.CIRCULAR: + axis.fill_between( + [ + (r_tf_wp_inboard_inner * np.cos(rad_tf_coil_inboard_toroidal_half)), + (r_tf_wp_inboard_outer * np.cos(rad_tf_coil_inboard_toroidal_half)), + ], + y13, + color="green", + ) + # Lower main + axis.fill_between( + [ + (r_tf_wp_inboard_inner * np.cos(rad_tf_coil_inboard_toroidal_half)), + (r_tf_wp_inboard_outer * np.cos(rad_tf_coil_inboard_toroidal_half)), + ], + y14, + color="green", + ) + axis.fill_between( + x12, + y12, + color="green", + ) + + # ================================================================ + + # Plot the WP + + # The winding pack should be inside the insulation, so subtract dx_tf_wp_insulation + # from both the inner and outer radii. + # The angular extent should also be reduced by the insulation thickness, i.e., the + # winding pack does not extend all the way to the top/bottom. + + # Calculate the reduced angle for the winding pack (subtract insulation thickness in + # arc length, convert to angle) + # arc_length = r * angle => angle = arc_length / r + # So, for both inner and outer radii, compute the angle offset due to insulation + # thickness + angle_offset_inner = ( + dx_tf_wp_insulation / r_tf_wp_inboard_inner if r_tf_wp_inboard_inner > 0 else 0 + ) + angle_offset_outer = ( + dx_tf_wp_insulation / r_tf_wp_inboard_outer if r_tf_wp_inboard_outer > 0 else 0 + ) + + # Use the maximum angle offset to ensure the winding pack stays within the insulation + angle_offset = max(angle_offset_inner, angle_offset_outer) + + # Define the angular range for the winding pack + theta_start = rad_tf_coil_inboard_toroidal_half - angle_offset + theta_end = -rad_tf_coil_inboard_toroidal_half + angle_offset + theta_vals = np.linspace(theta_start, theta_end, 256, endpoint=True) + + # X and Y points for inboard and outboard winding pack curves + x11 = (r_tf_wp_inboard_inner + dx_tf_wp_insulation) * np.cos(theta_vals) + y11 = (r_tf_wp_inboard_inner + dx_tf_wp_insulation) * np.sin(theta_vals) + x12 = (r_tf_wp_inboard_outer - dx_tf_wp_insulation) * np.cos(theta_vals) + y12 = (r_tf_wp_inboard_outer - dx_tf_wp_insulation) * np.sin(theta_vals) + + # Cordinates of the top and bottom of WP curves, + y13 = [y11[0], y12[0]] + x13 = [x11[0], x12[0]] + y14 = [y11[-1], y12[-1]] + x14 = [x11[-1], x12[-1]] + + # Plot the winding pack outline + axis.plot(x11, y11, color="black") + axis.plot(x12, y12, color="black") + axis.plot(x13, y13, color="black") + axis.plot(x14, y14, color="black") + + # Choose color based on i_tf_sup: copper for resistive, aluminium for cryo + # light steel blue (cryo aluminium) or copper color + wp_color = "#b0c4de" if mfile.get("i_tf_sup", scan=scan) == 2 else "#b87333" + + axis.fill_between( + [ + (r_tf_wp_inboard_inner + dx_tf_wp_insulation) * np.cos(theta_vals[0]), + (r_tf_wp_inboard_outer - dx_tf_wp_insulation) * np.cos(theta_vals[0]), + ], + y13, + color=wp_color, + ) + # Lower main + axis.fill_between( + [ + (r_tf_wp_inboard_inner + dx_tf_wp_insulation) * np.cos(theta_vals[-1]), + (r_tf_wp_inboard_outer - dx_tf_wp_insulation) * np.cos(theta_vals[-1]), + ], + y14, + color=wp_color, + ) + axis.fill_between(x12, y12, color=wp_color) + + # ================================================================ + + # Divide the winding pack into toroidal segments based on n_tf_coil_turns + n_turns = int(mfile.get("n_tf_coil_turns", scan=scan)) + if n_turns > 0: + # Calculate the angular extent for each turn + theta_start = rad_tf_coil_inboard_toroidal_half - angle_offset + theta_end = -rad_tf_coil_inboard_toroidal_half + angle_offset + theta_vals = np.linspace(theta_start, theta_end, 256, endpoint=True) + + # For each turn, plot a radial line at the corresponding angle + turn_angles = np.linspace(theta_start, theta_end, n_turns + 1) + for t in range(1, n_turns): + angle = turn_angles[t] + # Inner and outer points for this turn + x_in = (r_tf_wp_inboard_inner + dx_tf_wp_insulation) * np.cos(angle) + y_in = (r_tf_wp_inboard_inner + dx_tf_wp_insulation) * np.sin(angle) + x_out = (r_tf_wp_inboard_outer - dx_tf_wp_insulation) * np.cos(angle) + y_out = (r_tf_wp_inboard_outer - dx_tf_wp_insulation) * np.sin(angle) + axis.plot( + [x_in, x_out], + [y_in, y_out], + color="white", + linewidth=0.5, + linestyle="--", + ) + + # ================================================================ + + # Plot a dot for the location of the peak field + axis.plot( + r_b_tf_inboard_peak, + 0, + marker="o", + color="red", + label=( + f"Peak Field: {b_tf_inboard_peak_symmetric:.2f}" + f" T\nr={r_b_tf_inboard_peak:.3f} m" + ), + ) + + x_kwargs = { + "color": "black", + "linestyle": "--", + "linewidth": 0.6, + "alpha": 0.5, + } + axis.axvline(x=r_tf_inboard_in, **x_kwargs) + axis.axvline(x=r_tf_wp_inboard_inner, **x_kwargs) + axis.axvline(x=r_tf_wp_inboard_outer, **x_kwargs) + axis.axvline(x=r_tf_wp_inboard_centre, **x_kwargs) + axis.axvline(x=r_tf_inboard_out, **x_kwargs) + + axis.minorticks_on() + axis.set_xlim(0.0, r_tf_inboard_out * 1.1) + axis.set_ylim((y14[-1] * 1.65), (-y14[-1] * 1.65)) + + axis.set_title("Top-down view of inboard TF coil at midplane") + axis.set_xlabel("Radial distance [m]") + axis.set_ylabel("Toroidal distance [m]") + axis.legend(loc="upper left") + + draw_text( + axis, + 0.05, + 0.975, + "*Turn insulation and cooling pipes not shown", + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + color="black", + transform=fig.transFigure, + ) + + +def plot_resistive_tf_info(axis: plt.Axes, mfile: MFile, scan: int, fig): + """Plot info about the resistive TF coils""" + # Add info about the steel casing surrounding the WP + textstr_casing = ( + "$\\mathbf{Casing:}$\n\nCoil half angle:" + f" {mfile.get('rad_tf_coil_inboard_toroidal_half', scan=scan):.3f}" + " radians\n\n$\\text{Full Coil Case:}$\n$r_{start} \\rightarrow" + f" r_{{end}}$: {mfile.get('r_tf_inboard_in', scan=scan):.3f}" + f" $\\rightarrow$ {mfile.get('r_tf_inboard_out', scan=scan):.3f}" + f" m\n$\\Delta r$: {mfile.get('dr_tf_inboard', scan=scan):.3f} m\nArea" + " of casing around WP:" + f" {mfile.get('a_tf_coil_inboard_case', scan=scan):.3f}" + " $\\mathrm{m}^2$\n\n$\\text{Nose Case:}$\n$r_{start}" + " \\rightarrow r_{end}$:" + f" {mfile.get('r_tf_inboard_in', scan=scan):.3f} $\\rightarrow$" + f" {mfile.get('r_tf_wp_inboard_inner', scan=scan):.3f} m\n$\\Delta r$:" + f" {mfile.get('dr_tf_nose_case', scan=scan):.3f} m\n$A$:" + f" {mfile.get('a_tf_coil_nose_case', scan=scan):.4f}" + " $\\mathrm{m}^2$\n\n$\\text{Plasma Case:}$\n$r_{start}" + " \\rightarrow r_{end}$:" + f" {mfile.get('r_tf_wp_inboard_outer', scan=scan):.3f} $\\rightarrow$" + f" {mfile.get('r_tf_inboard_out', scan=scan):.3f} m\n$\\Delta r$:" + f" {mfile.get('dr_tf_plasma_case', scan=scan):.3f} m\n$A$:" + f" {mfile.get('a_tf_plasma_case', scan=scan):.3f} $\\mathrm{{m}}^2$" + ) + draw_text( + axis, + 0.775, + 0.925, + textstr_casing, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=box_style("grey"), + ) + + # Add info about the steel casing surrounding the WP + textstr_wp_insulation = ( + "$\\mathbf{Insulation:}$\n\nArea of insulation around WP:" + f" {mfile.get('a_tf_wp_ground_insulation', scan=scan):.3f}" + " $\\mathrm{m}^2$\n$\\Delta r$:" + f" {mfile.get('dx_tf_wp_insulation', scan=scan):.4f}" + " m\n\n$\\text{Turn Insulation:}$\n$\\Delta r$:" + f" {mfile.get('dx_tf_turn_insulation', scan=scan):.4f} m" + ) + draw_text( + axis, + 0.775, + 0.62, + textstr_wp_insulation, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox={ + "boxstyle": "round", + "facecolor": "green", + "alpha": 1.0, + "linewidth": 2, + }, + ) + + # Add info about the Winding Pack + textstr_wp = ( + "$\\mathbf{Winding Pack:}$\n\n$N_{\\text{turns}}$:" + f" {int(mfile.get('n_tf_coil_turns', scan=scan))} turns\n$r_{{start}}" + " \\rightarrow r_{end}$:" + f" {mfile.get('r_tf_wp_inboard_inner', scan=scan):.3f} $\\rightarrow$" + f" {mfile.get('r_tf_wp_inboard_outer', scan=scan):.3f} m\n$\\Delta r$:" + f" {mfile.get('dr_tf_wp_with_insulation', scan=scan):.3f} m\n$A$, with" + f" insulation: {mfile.get('a_tf_wp_with_insulation', scan=scan):.3f}" + " $\\mathrm{m}^2$\n$A$, no insulation:" + f" {mfile.get('a_tf_wp_no_insulation', scan=scan):.3f}" + " $\\mathrm{m}^2$\n\nCurrent per turn:" + f" {mfile.get('c_tf_turn', scan=scan) / 1e3:.3f}" + " $\\mathrm{kA}$\nResistive conductor per coil:" + f" {mfile.get('a_res_tf_coil_conductor', scan=scan):.3f}" + " $\\mathrm{m}^2$\nCoolant area void fraction per turn:" + f" {mfile.get('fcoolcp', scan=scan):.3f}" + ) + draw_text( + axis, + 0.77, + 0.475, + textstr_wp, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + color="white", + transform=fig.transFigure, + bbox={ + "boxstyle": "round", + "facecolor": "blue", + "alpha": 1.0, + "linewidth": 2, + }, + ) + + # Add info about the Winding Pack + textstr_general_info = ( + "$\\mathbf{General \\ info:}$\n\nSelf inductance:" + f" {mfile.get('ind_tf_coil', scan=scan) * 1e6:.4f} $\\mu$H\nStored" + " energy of all coils:" + f" {mfile.get('e_tf_magnetic_stored_total_gj', scan=scan):.4f} GJ\n" + ) + draw_text( + axis, + 0.55, + 0.475, + textstr_general_info, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox={ + "boxstyle": "round", + "facecolor": "wheat", + "alpha": 1.0, + "linewidth": 2, + }, + ) + + # Add info about the Winding Pack + textstr_cooling = ( + "$\\mathbf{Cooling \\ info:}$\n\nCoolant inlet temperature:" + f" {mfile.get('temp_cp_coolant_inlet', scan=scan):.2f} K\nCoolant" + f" temperature rise: {mfile.get('dtemp_cp_coolant', scan=scan):.2f}" + " K\nCoolant velocity:" + f" {mfile.get('vel_cp_coolant_midplane', scan=scan):.2f}" + " $\\mathrm{ms^{-1}}$\n\nAverage CP temperature:" + f" {mfile.get('temp_cp_average', scan=scan):.2f} K\nCP resistivity:" + f" {mfile.get('rho_cp', scan=scan):.2e} $\\Omega \\mathrm{{m}}$\nLeg" + f" resistivity: {mfile.get('rho_tf_leg', scan=scan):.2e} $\\Omega" + " \\mathrm{m}$\nLeg resistance:" + f" {mfile.get('res_tf_leg', scan=scan):.2e} $\\Omega$\nCP resistive" + f" losses: {mfile.get('p_cp_resistive', scan=scan):,.2f}" + " $\\mathrm{W}$\nLeg resistive losses:" + f" {mfile.get('p_tf_leg_resistive', scan=scan):,.2f}" + " $\\mathrm{W}$\nJoints resistive losses:" + f" {mfile.get('p_tf_joints_resistive', scan=scan):,.2f}" + " $\\mathrm{W}$\n" + ) + draw_text( + axis, + 0.55, + 0.35, + textstr_cooling, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=box_style("wheat"), + ) + + +def plot_tf_cable_in_conduit_turn(axis: plt.Axes, fig, mfile: MFile, scan: int): + """Plots inboard TF coil CICC individual turn structure. + + Parameters + ---------- + axis : matplotlib.axes object + Axis object to plot to. + mfile : MFILE data object + Object containing data for the plot. + scan : int + Scan number to use. + """ + + def _pack_strands_rectangular_with_obstacles( + cable_space_bounds, + pipe_center, + pipe_radius, + strand_diameter, + void_fraction, + n_strands, + axis, + corner_radius, + f_a_tf_turn_cable_copper, + ): + """Pack circular strands in rectangular space with cooling pipe obstacle + + Parameters + ---------- + cable_space_bounds : + + pipe_center : + + pipe_radius : + + strand_diameter : + + void_fraction : + + n_strands : + + axis : + + corner_radius : + + f_a_tf_turn_cable_copper : + + """ + x, y, width, height = cable_space_bounds + + radius = strand_diameter / 2 + placed_strands = [] + attempts = 0 + + pipe_x, pipe_y = pipe_center + + # Hexagonal packing parameters + # Calculate the spacing between strand centers for the desired void fraction + # For hexagonal packing, packing fraction = pi/(2*sqrt(3)) ~ 0.9069 + # To achieve a lower packing fraction (higher void fraction), increase spacing + ideal_packing_fraction = np.pi / (2 * np.sqrt(3)) + target_packing_fraction = 1 - void_fraction + spacing_factor = np.sqrt(ideal_packing_fraction / target_packing_fraction) + strand_spacing = strand_diameter * spacing_factor + + # Number of rows and columns that fit in the cable space + n_rows = int((height - 2 * radius) // (strand_spacing * np.sqrt(3) / 2)) + n_cols = int((width - 2 * radius) // strand_spacing) + + # Calculate the radius of the inner superconductor circle based on the copper + # area fraction + # Area_superconductor = (1 - f_a_tf_turn_cable_copper) * Area_strand + # Area_strand = pi * radius^2 + # So, radius_superconductor = sqrt(1 - f_a_tf_turn_cable_copper) * radius + radius_superconductor = np.sqrt(1 - f_a_tf_turn_cable_copper) * radius + + # Generate hexagonal grid positions + for row in range(n_rows): + y_pos = (y + radius + row * strand_spacing * np.sqrt(3) / 2) * 1.07 + x_offset = strand_spacing / 2 if row % 2 else 0 + for col in range(n_cols): + candidate_x = (x + radius + col * strand_spacing + x_offset) * 1.05 + candidate_y = y_pos + + # Check if within bounds + if candidate_x > x + width - radius or candidate_y > y + height - radius: + continue + + # Check collision with cooling pipe + pipe_distance = np.sqrt( + (candidate_x - pipe_x) ** 2 + (candidate_y - pipe_y) ** 2 + ) + if pipe_distance < (pipe_radius + radius): + continue + + # Check collision with corners if rounded + if corner_radius > 0: + corners = [ + (x + corner_radius, y + corner_radius), # bottom-left + ( + x + width - corner_radius, + y + corner_radius, + ), # bottom-right + ( + x + width - corner_radius, + y + height - corner_radius, + ), # top-right + ( + x + corner_radius, + y + height - corner_radius, + ), # top-left + ] + if ( + ( + candidate_x < corners[0][0] + and candidate_y < corners[0][1] + and np.sqrt( + (candidate_x - corners[0][0]) ** 2 + + (candidate_y - corners[0][1]) ** 2 + ) + > corner_radius - radius + ) + or ( + candidate_x > corners[1][0] + and candidate_y < corners[1][1] + and np.sqrt( + (candidate_x - corners[1][0]) ** 2 + + (candidate_y - corners[1][1]) ** 2 + ) + > corner_radius - radius + ) + or ( + candidate_x > corners[2][0] + and candidate_y > corners[2][1] + and np.sqrt( + (candidate_x - corners[2][0]) ** 2 + + (candidate_y - corners[2][1]) ** 2 + ) + > corner_radius - radius + ) + or ( + candidate_x < corners[3][0] + and candidate_y > corners[3][1] + and np.sqrt( + (candidate_x - corners[3][0]) ** 2 + + (candidate_y - corners[3][1]) ** 2 + ) + > corner_radius - radius + ) + ): + continue + + # Check collision with existing strands + collision = False + for existing_x, existing_y in placed_strands: + distance = np.sqrt( + (candidate_x - existing_x) ** 2 + (candidate_y - existing_y) ** 2 + ) + if distance < strand_diameter: + collision = True + break + + if not collision: + placed_strands.append((candidate_x, candidate_y)) + # Plot the strand + circle_copper_surrounding = Circle( + (candidate_x, candidate_y), + radius, + facecolor="#b87333", # copper color + edgecolor="#8B4000", # darker copper edge + linewidth=0.1, + alpha=0.8, + ) + axis.add_patch(circle_copper_surrounding) + + circle_central_conductor = Circle( + (candidate_x, candidate_y), + radius_superconductor, + facecolor="black", + linewidth=0.3, + alpha=0.5, + ) + axis.add_patch(circle_central_conductor) + + if len(placed_strands) >= n_strands: + break + if len(placed_strands) >= n_strands: + break + + attempts = n_rows * n_cols + + return len(placed_strands), attempts + + # Import the TF turn variables then multiply into mm + i_tf_turns_integer = mfile.get("i_tf_turns_integer", scan=scan) + # If integer turns switch is on then the turns can have non square dimensions + if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.INTEGER: + turn_width = mfile.get("dr_tf_turn", scan=scan) + turn_height = mfile.get("dx_tf_turn", scan=scan) + cable_space_width_radial = mfile.get("dr_tf_turn_cable_space", scan=scan) + cable_space_width_toroidal = mfile.get("dx_tf_turn_cable_space", scan=scan) + + elif TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.NON_INTEGER: + turn_width = mfile.get("dx_tf_turn_general", scan=scan) + cable_space_width = mfile.get("dx_tf_turn_cable_space_average", scan=scan) + + he_pipe_diameter = mfile.get("dia_tf_turn_coolant_channel", scan=scan) + steel_thickness = mfile.get("dx_tf_turn_steel", scan=scan) + insulation_thickness = mfile.get("dx_tf_turn_insulation", scan=scan) + + a_tf_turn_cable_space_no_void = mfile.get("a_tf_turn_cable_space_no_void", scan=scan) + radius_tf_turn_cable_space_corners = mfile.get( + "radius_tf_turn_cable_space_corners", scan=scan + ) + + a_tf_wp_coolant_channels = mfile.get("a_tf_wp_coolant_channels", scan=scan) + + f_a_tf_turn_cable_space_extra_void = mfile.get( + "f_a_tf_turn_cable_space_extra_void", scan=scan + ) + a_tf_turn_steel = mfile.get("a_tf_turn_steel", scan=scan) + a_tf_turn_cable_space_effective = mfile.get( + "a_tf_turn_cable_space_effective", scan=scan + ) + + # Plot the total turn shape + if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.NON_INTEGER: + axis.add_patch( + Rectangle( + (0, 0), + turn_width, + turn_width, + facecolor="red", + edgecolor="black", + ), + ) + # Plot the steel conduit + axis.add_patch( + Rectangle( + (insulation_thickness, insulation_thickness), + (turn_width - 2 * insulation_thickness), + (turn_width - 2 * insulation_thickness), + facecolor="grey", + edgecolor="black", + ), + ) + + # Plot the cable space with rounded corners + axis.add_patch( + patches.FancyBboxPatch( + ( + insulation_thickness + steel_thickness, + insulation_thickness + steel_thickness, + ), + (turn_width - 2 * (insulation_thickness + steel_thickness)), + (turn_width - 2 * (insulation_thickness + steel_thickness)), + boxstyle=patches.BoxStyle( + "Round", + pad=0, + rounding_size=radius_tf_turn_cable_space_corners, + ), + facecolor="royalblue", + edgecolor="black", + ), + ) + + # Plot dashed line around the cable space + axis.add_patch( + Rectangle( + ( + insulation_thickness + steel_thickness, + insulation_thickness + steel_thickness, + ), + (turn_width - 2 * (insulation_thickness + steel_thickness)), + (turn_width - 2 * (insulation_thickness + steel_thickness)), + facecolor="none", + edgecolor="black", + linestyle="--", + linewidth=1.2, + alpha=0.5, + ), + ) + # Plot the coolant channel + axis.add_patch( + Circle( + ((turn_width / 2), (turn_width / 2)), + he_pipe_diameter / 2, + facecolor="white", + edgecolor="black", + ), + ) + + # Cable strand packing parameters + strand_diameter = mfile.get("dia_tf_turn_superconducting_cable", scan=scan) + void_fraction = mfile.get("f_a_tf_turn_cable_space_extra_void", scan=scan) + + # Cable space bounds + cable_bounds = [ + insulation_thickness + steel_thickness, + insulation_thickness + steel_thickness, + turn_width - 2 * (insulation_thickness + steel_thickness), + turn_width - 2 * (insulation_thickness + steel_thickness), + ] + + # Pack strands if significant void fraction + if void_fraction > 0.001: + _n_strands, _attempts = _pack_strands_rectangular_with_obstacles( + cable_space_bounds=cable_bounds, + pipe_center=( + turn_width / 2, + ( + turn_width + if TFWPIntegerTurnType(i_tf_turns_integer) + == TFWPIntegerTurnType.NON_INTEGER + else turn_height + ) + / 2, + ), + pipe_radius=he_pipe_diameter / 2, + strand_diameter=strand_diameter, + void_fraction=void_fraction, + axis=axis, + corner_radius=radius_tf_turn_cable_space_corners, + n_strands=mfile.get("n_tf_turn_superconducting_cables", scan=scan), + f_a_tf_turn_cable_copper=mfile.get( + "f_a_tf_turn_cable_copper", scan=scan + ), + ) + + axis.set_xlim(-turn_width * 0.05, turn_width * 1.05) + axis.set_ylim(-turn_width * 0.05, turn_width * 1.05) + + # Non square turns + elif TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.INTEGER: + axis.add_patch( + Rectangle( + (0, 0), + turn_width, + turn_height, + facecolor="red", + edgecolor="black", + ), + ) + + # Plot the steel conduit + axis.add_patch( + Rectangle( + (insulation_thickness, insulation_thickness), + (turn_width - 2 * insulation_thickness), + (turn_height - 2 * insulation_thickness), + facecolor="grey", + edgecolor="black", + ), + ) + + # Plot the cable space with rounded corners + axis.add_patch( + patches.FancyBboxPatch( + ( + insulation_thickness + steel_thickness, + insulation_thickness + steel_thickness, + ), + (turn_width - 2 * (insulation_thickness + steel_thickness)), + (turn_height - 2 * (insulation_thickness + steel_thickness)), + boxstyle=patches.BoxStyle( + "Round", + pad=0, + rounding_size=radius_tf_turn_cable_space_corners, + ), + facecolor="royalblue", + edgecolor="black", + ), + ) + # Plot dashed line around the cable space + axis.add_patch( + Rectangle( + ( + insulation_thickness + steel_thickness, + insulation_thickness + steel_thickness, + ), + (turn_width - 2 * (insulation_thickness + steel_thickness)), + (turn_height - 2 * (insulation_thickness + steel_thickness)), + facecolor="none", + edgecolor="black", + linestyle="--", + linewidth=1.0, + alpha=0.5, + ), + ) + axis.add_patch( + Circle( + ((turn_width / 2), (turn_height / 2)), + he_pipe_diameter / 2, + facecolor="white", + edgecolor="black", + ), + ) + + # Cable space bounds + cable_bounds = [ + insulation_thickness + steel_thickness, + insulation_thickness + steel_thickness, + turn_width - 2 * (insulation_thickness + steel_thickness), + turn_height - 2 * (insulation_thickness + steel_thickness), + ] + + # Cable strand packing parameters + strand_diameter = mfile.get("dia_tf_turn_superconducting_cable", scan=scan) + void_fraction = mfile.get("f_a_tf_turn_cable_space_extra_void", scan=scan) + + # Pack strands if significant void fraction + if void_fraction > 0.001: + _, _ = _pack_strands_rectangular_with_obstacles( + cable_space_bounds=cable_bounds, + pipe_center=( + turn_width / 2, + turn_height / 2, + ), + pipe_radius=he_pipe_diameter / 2, + strand_diameter=strand_diameter, + void_fraction=void_fraction, + axis=axis, + corner_radius=radius_tf_turn_cable_space_corners, + n_strands=mfile.get("n_tf_turn_superconducting_cables", scan=scan), + f_a_tf_turn_cable_copper=mfile.get( + "f_a_tf_turn_cable_copper", scan=scan + ), + ) + + axis.set_xlim(-turn_width * 0.05, turn_width * 1.05) + axis.set_ylim(-turn_height * 0.05, turn_height * 1.05) + + axis.minorticks_on() + axis.set_title("WP Turn Structure") + axis.set_xlabel("r [m]") + axis.set_ylabel("x [m]") + + # Add info about the steel casing surrounding the WP + textstr_turn_insulation = ( + f"$\\mathbf{{Turn \\ Insulation:}}$\n\n$\\Delta r:${insulation_thickness:.3e} m" + ) + + draw_text( + axis, + 0.4, + 0.9, + textstr_turn_insulation, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=box_style("red"), + ) + + # Add info about the steel casing surrounding the WP + textstr_turn_steel = ( + f"$\\mathbf{{Steel \\ Conduit:}}$\n\n$\\Delta r:${steel_thickness:.3e}" + f" m\n$A$: {a_tf_turn_steel:.3e} m$^2$" + ) + + draw_text( + axis, + 0.65, + 0.9, + textstr_turn_steel, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=box_style("grey"), + ) + + if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.NON_INTEGER: + # Add info about the steel casing surrounding the WP + textstr_turn_cable_space = ( + "$\\mathbf{Cable \\ Space:}$\n\n$\\Delta r:$" + f" {cable_space_width:.3e} m\nCorner radius, $r$:" + f" {radius_tf_turn_cable_space_corners:.3e} m\nCable area with no" + f" cooling\nchannel or gaps: {a_tf_turn_cable_space_no_void:.3e}" + " m$^2$\nExtra cable space area void fraction:" + f" {f_a_tf_turn_cable_space_extra_void}\nTrue cable space area:" + f" {a_tf_turn_cable_space_effective:.3e} m$^2$" + ) + elif TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.INTEGER: + textstr_turn_cable_space = ( + "$\\mathbf{Cable \\ Space:}$\n\nCable space:\n$\\Delta r$:" + f" {cable_space_width_radial:.3e} m\n$\\Delta x$:" + f" {cable_space_width_toroidal:.3e} m\nCorner radius, $r$:" + f" {radius_tf_turn_cable_space_corners:.3e} m\nCable area with no" + f" cooling channel or gaps: {a_tf_turn_cable_space_no_void:.3e}" + " m$^2$\nExtra cable space area void fraction:" + f" {f_a_tf_turn_cable_space_extra_void}\nTrue cable space area:" + f" {a_tf_turn_cable_space_effective:.3e} m$^2$" + ) + + draw_text( + axis, + 0.40, + 0.7, + textstr_turn_cable_space, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=box_style("royalblue"), + ) + + if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.NON_INTEGER: + textstr_turn = ( + "$\\mathbf{Turn:}$\n\n" + f"$\\Delta r$: {turn_width:.3e} m\n" + f"$\\Delta x$: {turn_width:.3e} m" + ) + + if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.INTEGER: + textstr_turn = ( + "$\\mathbf{Turn:}$\n\n" + f"$\\Delta r$: {turn_width:.3e} m\n" + f"$\\Delta x$: {turn_height:.3e} m" + ) + + draw_text( + axis, + 0.525, + 0.9, + textstr_turn, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=box_style("wheat"), + ) + + # Add info about the steel casing surrounding the WP + textstr_turn_cooling = ( + f"$\\mathbf{{Cooling:}}$\n\n$\\varnothing$: {he_pipe_diameter:.3e}" + " m\nTotal area of all coolant channels:" + f" {a_tf_wp_coolant_channels:.4f} m$^2$" + ) + + draw_text( + axis, + 0.45, + 0.8, + textstr_turn_cooling, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=box_style("white"), + ) + + textstr_superconductor = ( + "$\\mathbf{Superconductor:}$\n\nSuperconductor" + f" used:\n{SuperconductorModel(mfile.get('i_tf_sc_mat', scan=scan)).full_name}\nCritical" # noqa: E501 + " field at zero\ntemperature and strain:" + f" {mfile.get('b_tf_superconductor_critical_zero_temp_strain', scan=scan):.4f}" + " T\nCritical temperature at\nzero field and strain:" + f" {mfile.get('temp_tf_superconductor_critical_zero_field_strain', scan=scan):.4f}" # noqa: E501 + f" K\nTemperature at conductor: {mfile.get('tftmp', scan=scan):.4f}" + " K\nField at conductor:" + f" {mfile.get('b_tf_inboard_peak_with_ripple', scan=scan):.4f}" + " T\nSuperconductor critical current density at\noperating" + " conditions:" + f" {mfile.get('j_tf_superconductor_critical', scan=scan):.2e}" + " A/m$^2$\n$I_{\\text{TF,turn critical}}$:" + f" {mfile.get('c_turn_cables_critical', scan=scan):,.2f}" + " A\n$I_{\\text{TF,turn}}$:" + f" {mfile.get('c_tf_turn', scan=scan):,.2f} A\nCritcal current ratio:" + f" {mfile.get('f_c_tf_turn_operating_critical', scan=scan):,.4f}\nSuperconductor" + " temperature\nmargin:" + f" {mfile.get('temp_tf_superconductor_margin', scan=scan):,.4f}" + " K\n\n$\\mathbf{Quench:}$\n\nQuench dump time:" + f" {mfile.get('t_tf_superconductor_quench', scan=scan):.4f} s\nQuench" + f" detection time: {mfile.get('t_tf_quench_detection', scan=scan):.4f}" + " s\nUser input max temperature\nduring quench:" + f" {mfile.get('temp_tf_conductor_quench_max', scan=scan):.2f}" + " K\nRequired maxium WP current\ndensity for heat" + f" protection:\n{mfile.get('j_tf_wp_quench_heat_max', scan=scan):.2e}" + " A/m$^2$\n" + ) + draw_text( + axis, + 0.75, + 0.9, + textstr_superconductor, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=box_style("#6dd3f7"), # light blue for superconductors + ) + + +def plot_tf_croco_turn(axis: plt.Axes, fig, mfile: MFile, scan: int): + """Plots inboard TF coil CICC individual turn structure with croco cable layout.""" + # Import the TF turn variables then multiply into mm + i_tf_turns_integer = mfile.get("i_tf_turns_integer", scan=scan) + # If integer turns switch is on then the turns can have non square dimensions + if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.INTEGER: + turn_width = mfile.get("dr_tf_turn", scan=scan) + turn_height = mfile.get("dx_tf_turn", scan=scan) + cable_space_width_radial = mfile.get("dr_tf_turn_cable_space", scan=scan) + cable_space_width_toroidal = mfile.get("dx_tf_turn_cable_space", scan=scan) + + elif TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.NON_INTEGER: + turn_width = mfile.get("dx_tf_turn_general", scan=scan) + cable_space_width = mfile.get("dx_tf_turn_cable_space_average", scan=scan) + + steel_thickness = mfile.get("dx_tf_turn_steel", scan=scan) + insulation_thickness = mfile.get("dx_tf_turn_insulation", scan=scan) + + a_tf_turn_cable_space_no_void = mfile.get("a_tf_turn_cable_space_no_void", scan=scan) + radius_tf_turn_cable_space_corners = mfile.get( + "radius_tf_turn_cable_space_corners", scan=scan + ) + + a_tf_wp_coolant_channels = mfile.get("a_tf_wp_coolant_channels", scan=scan) + + f_a_tf_turn_cable_space_extra_void = mfile.get( + "f_a_tf_turn_cable_space_extra_void", scan=scan + ) + a_tf_turn_steel = mfile.get("a_tf_turn_steel", scan=scan) + a_tf_turn_cable_space_effective = mfile.get( + "a_tf_turn_cable_space_effective", scan=scan + ) + + he_pipe_diameter = mfile.get("dia_tf_turn_coolant_channel", scan=scan) + dia_tf_turn_croco_cable = mfile.get("dia_tf_turn_croco_cable", scan=scan) + + # Plot the total turn shape + if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.NON_INTEGER: + axis.add_patch( + Rectangle( + (0, 0), + turn_width, + turn_width, + facecolor="red", + edgecolor="black", + ), + ) + # Plot the steel conduit + axis.add_patch( + Rectangle( + (insulation_thickness, insulation_thickness), + (turn_width - 2 * insulation_thickness), + (turn_width - 2 * insulation_thickness), + facecolor="grey", + edgecolor="black", + ), + ) + + # Plot the central cable space and copper cylinder + for rad, col in [ + (1.5 * dia_tf_turn_croco_cable, "white"), + (dia_tf_turn_croco_cable / 2, "#B87333"), + ]: + axis.add_patch( + Circle( + ((turn_width / 2), (turn_width / 2)), + rad, + facecolor=col, + edgecolor="black", + linewidth=1.2, + ), + ) + + # Plot six surrounding Croco cables in a hexagonal layout. + center_x = turn_width / 2 + center_y = turn_width / 2 + ring_radius = dia_tf_turn_croco_cable + for angle in np.linspace(0, 2 * np.pi, 6, endpoint=False): + plot_corc_cable_geometry( + axis=axis, + r_centre=center_x + ring_radius * np.cos(angle), + z_centre=center_y + ring_radius * np.sin(angle), + dia_croco_strand=mfile.get("dia_tf_turn_croco_cable", scan=scan), + dx_croco_strand_copper=mfile.get("dx_tf_croco_strand_copper", scan=scan), + dr_hts_tape=mfile.get("dr_tf_hts_tape", scan=scan), + dx_croco_strand_tape_stack=mfile.get( + "dx_tf_croco_strand_tape_stack", scan=scan + ), + n_croco_strand_hts_tapes=mfile.get( + "n_tf_croco_strand_hts_tapes", scan=scan + ), + dx_hts_tape_rebco=mfile.get("dx_tf_hts_tape_rebco", scan=scan), + dx_hts_tape_copper=mfile.get("dx_tf_hts_tape_copper", scan=scan), + dx_hts_tape_hastelloy=mfile.get("dx_tf_hts_tape_hastelloy", scan=scan), + show_legend=False, + ) + + axis.minorticks_on() + axis.set_title("WP Turn Structure") + axis.set_xlim(-turn_width * 0.025, turn_width * 1.025) + axis.set_ylim(-turn_width * 0.025, turn_width * 1.025) + axis.set_aspect("equal", adjustable="box") + axis.set_xlabel("r [m]") + axis.set_ylabel("x [m]") + + # Add info about the steel casing surrounding the WP + textstr_turn_insulation = ( + f"$\\mathbf{{Turn \\ Insulation:}}$\n\n$\\Delta r:${insulation_thickness:.3e} m" + ) + + draw_text( + axis, + 0.4, + 0.9, + textstr_turn_insulation, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=box_style("red"), + ) + + # Add info about the steel casing surrounding the WP + textstr_turn_steel = ( + f"$\\mathbf{{Steel \\ Conduit:}}$\n\n$\\Delta r:${steel_thickness:.3e}" + f" m\n$A$: {a_tf_turn_steel:.3e} m$^2$" + ) + + draw_text( + axis, + 0.65, + 0.9, + textstr_turn_steel, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=box_style("grey"), + ) + + if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.NON_INTEGER: + # Add info about the steel casing surrounding the WP + textstr_turn_cable_space = ( + "$\\mathbf{Cable \\ Space:}$\n\n$\\Delta r:$" + f" {cable_space_width:.3e} m\nCorner radius, $r$:" + f" {radius_tf_turn_cable_space_corners:.3e} m\nCable area with no" + f" cooling\nchannel or gaps: {a_tf_turn_cable_space_no_void:.3e}" + " m$^2$\nExtra cable space area void fraction:" + f" {f_a_tf_turn_cable_space_extra_void}\nTrue cable space area:" + f" {a_tf_turn_cable_space_effective:.3e} m$^2$" + ) + elif TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.INTEGER: + textstr_turn_cable_space = ( + "$\\mathbf{Cable \\ Space:}$\n\nCable space:\n$\\Delta r$:" + f" {cable_space_width_radial:.3e} m\n$\\Delta x$:" + f" {cable_space_width_toroidal:.3e} m\nCorner radius, $r$:" + f" {radius_tf_turn_cable_space_corners:.3e} m\nCable area with no" + f" cooling channel or gaps: {a_tf_turn_cable_space_no_void:.3e}" + " m$^2$\nExtra cable space area void fraction:" + f" {f_a_tf_turn_cable_space_extra_void}\nTrue cable space area:" + f" {a_tf_turn_cable_space_effective:.3e} m$^2$" + ) + + draw_text( + axis, + 0.40, + 0.7, + textstr_turn_cable_space, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=box_style("royalblue"), + ) + + if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.NON_INTEGER: + textstr_turn = ( + "$\\mathbf{Turn:}$\n\n" + f"$\\Delta r$: {turn_width:.3e} m\n" + f"$\\Delta x$: {turn_width:.3e} m" + ) + + if TFWPIntegerTurnType(i_tf_turns_integer) == TFWPIntegerTurnType.INTEGER: + textstr_turn = ( + "$\\mathbf{Turn:}$\n\n" + f"$\\Delta r$: {turn_width:.3e} m\n" + f"$\\Delta x$: {turn_height:.3e} m" + ) + + draw_text( + axis, + 0.525, + 0.9, + textstr_turn, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=box_style("wheat"), + ) + + # Add info about the steel casing surrounding the WP + textstr_turn_cooling = ( + f"$\\mathbf{{Cooling:}}$\n\n$\\varnothing$: {he_pipe_diameter:.3e}" + " m\nTotal area of all coolant channels:" + f" {a_tf_wp_coolant_channels:.4f} m$^2$" + ) + + draw_text( + axis, + 0.45, + 0.8, + textstr_turn_cooling, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=box_style("white"), + ) + + textstr_superconductor = ( + "$\\mathbf{Superconductor:}$\n\nSuperconductor used:" + f" {SuperconductorModel(mfile.get('i_tf_sc_mat', scan=scan)).full_name}\nCritical" # noqa: E501 + " field at zero\ntemperature and strain:" + f" {mfile.get('b_tf_superconductor_critical_zero_temp_strain', scan=scan):.4f}" + " T\nCritical temperature at\nzero field and strain:" + f" {mfile.get('temp_tf_superconductor_critical_zero_field_strain', scan=scan):.4f}" # noqa: E501 + f" K\nTemperature at conductor: {mfile.get('tftmp', scan=scan):.4f}" + " K\nField at conductor:" + f" {mfile.get('b_tf_inboard_peak_with_ripple', scan=scan):.4f}" + " T\nSuperconductor critical current density at\noperating" + " conditions:" + f" {mfile.get('j_tf_superconductor_critical', scan=scan):.2e}" + " A/m$^2$\n$I_{\\text{TF,turn critical}}$:" + f" {mfile.get('c_turn_cables_critical', scan=scan):,.2f}" + " A\n$I_{\\text{TF,turn}}$:" + f" {mfile.get('c_tf_turn', scan=scan):,.2f} A\nCritcal current ratio:" + f" {mfile.get('f_c_tf_turn_operating_critical', scan=scan):,.4f}\nSuperconductor" + " temperature\nmargin:" + f" {mfile.get('temp_tf_superconductor_margin', scan=scan):,.4f}" + " K\n\n$\\mathbf{Quench:}$\n\nQuench dump time:" + f" {mfile.get('t_tf_superconductor_quench', scan=scan):.4e} s\nQuench" + f" detection time: {mfile.get('t_tf_quench_detection', scan=scan):.4e}" + " s\nUser input max temperature\nduring quench:" + f" {mfile.get('temp_tf_conductor_quench_max', scan=scan):.2f}" + " K\nRequired maxium WP current\ndensity for heat" + f" protection:\n{mfile.get('j_tf_wp_quench_heat_max', scan=scan):.2e}" + " A/m$^2$\n" + ) + draw_text( + axis, + 0.75, + 0.9, + textstr_superconductor, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=box_style("#6dd3f7"), + ) + + +def plot_tf_coil_structure(axis: plt.Axes, mfile: MFile, scan: int, colour_scheme=1): + """Plot the TF coil poloidal cross-section""" + plot_tf_coils(axis, mfile, scan, colour_scheme) + + x1 = mfile.get("r_tf_arc(1)", scan=scan) + y1 = mfile.get("z_tf_arc(1)", scan=scan) + x2 = mfile.get("r_tf_arc(2)", scan=scan) + y2 = mfile.get("z_tf_arc(2)", scan=scan) + x3 = mfile.get("r_tf_arc(3)", scan=scan) + y3 = mfile.get("z_tf_arc(3)", scan=scan) + x4 = mfile.get("r_tf_arc(4)", scan=scan) + y4 = mfile.get("z_tf_arc(4)", scan=scan) + x5 = mfile.get("r_tf_arc(5)", scan=scan) + y5 = mfile.get("z_tf_arc(5)", scan=scan) + + z_tf_inside_half = mfile.get("z_tf_inside_half", scan=scan) + z_tf_top = mfile.get("z_tf_top", scan=scan) + dr_tf_inboard = mfile.get("dr_tf_inboard", scan=scan) + r_tf_inboard_out = mfile.get("r_tf_inboard_out", scan=scan) + r_tf_outboard_in = mfile.get("r_tf_outboard_in", scan=scan) + r_tf_inboard_in = mfile.get("r_tf_inboard_in", scan=scan) + dr_tf_outboard = mfile.get("dr_tf_outboard", scan=scan) + len_tf_coil = mfile.get("len_tf_coil", scan=scan) + dz_tf_upper_lower_midplane = mfile.get("dz_tf_upper_lower_midplane", scan=scan) + + # Plot the points as black dots, number them, and connect them with lines + xs = [x1, x2, x3, x4, x5] + ys = [y1, y2, y3, y4, y5] + labels = [] + for i, (x, y) in enumerate(zip(xs, ys, strict=False), 1): + axis.plot(x, y, "ko", markersize=8) + draw_text( + axis, + x, + y, + str(i), + color="red", + fontsize=5, + ha="center", + va="center", + fontweight="bold", + ) + labels.append(f"TF Arc Point {i}: ({x:.2f}, {y:.2f})") + + # ========================================================= + + # If D-shaped coil, plot the full internal height arrow + if mfile.get("i_tf_shape", scan=scan) == 1: + # Arrow for internal coil width + draw_annotation( + axis, + "", + xy=(x2, y2), + xytext=(x4, y4), + arrowprops={"arrowstyle": "<->", "color": "black"}, + ) + + # Add a label for the internal coil width + draw_text( + axis, + x2, + 0.0, + f"{y2 - y4:.3f} m", + fontsize=7, + color="black", + rotation=270, + verticalalignment="center", + horizontalalignment="center", + bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, + zorder=100, # Ensure label is on top of all plots + ) + + # ========================================================== + + # Arrow for the full TF coil height + if mfile.get("i_tf_shape", scan=scan) == 1: + x = x2 * 0.9 + elif mfile.get("i_tf_shape", scan=scan) == 2: + x = (x2 - x1) / 2 + + draw_annotation( + axis, + "", + xy=(x, y2 + dr_tf_inboard), + xytext=(x, y4 - dr_tf_inboard), + arrowprops={"arrowstyle": "<->", "color": "black"}, + ) + + # Add a label for the full TF coil height + draw_text( + axis, + x, + 0.0, + f"{((y2 + 2 * dr_tf_inboard) - y4):.3f} m", + fontsize=7, + color="black", + rotation=270, + verticalalignment="center", + horizontalalignment="center", + bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, + zorder=101, # Ensure label is on top of all plots + ) + + # ========================================================== + + # Arrow for top half height of TF coil + draw_annotation( + axis, + "", + xy=(-2.0, 0), + xytext=(-2.0, y2 + dr_tf_inboard), + arrowprops={"arrowstyle": "<->", "color": "black"}, + ) + axis.axhline(y=y2 + dr_tf_inboard, color="black", linestyle="--", linewidth=1) + + # Add a label for top of TF coil + draw_text( + axis, + -2.0, + (y2 + dr_tf_inboard) / 2, + f"{y2 + dr_tf_inboard:.3f} m", + fontsize=7, + color="black", + rotation=270, + verticalalignment="center", + horizontalalignment="center", + bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, + ) + + # ========================================================== + + # Arrow for bottom half height of TF coil + draw_annotation( + axis, + "", + xy=(-2.0, 0), + xytext=(-2.0, y4 - dr_tf_inboard), + arrowprops={"arrowstyle": "<->", "color": "black"}, + ) + axis.axhline(y=y4 - dr_tf_inboard, color="black", linestyle="--", linewidth=1) + + # Add a label for top of TF coil + draw_text( + axis, + -2.0, + -z_tf_top / 2, + f"{y4 - dr_tf_inboard:.3f} m", + fontsize=7, + color="black", + rotation=270, + verticalalignment="center", + horizontalalignment="center", + bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, + ) + + # Arrow for top inside internal height + draw_annotation( + axis, + "", + xy=(-1.0, 0), + xytext=(-1.0, y2), + arrowprops={"arrowstyle": "<->", "color": "black"}, + ) + axis.axhline(y=y2, color="black", linestyle="--", linewidth=1) + + # Add a label for height of top internal height + draw_text( + axis, + -1.0, + y2 / 2, + f"{y2:.3f} m", + fontsize=7, + color="black", + rotation=270, + verticalalignment="center", + horizontalalignment="center", + bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, + ) + + # ========================================================= + + # Arrow for coil internal height + draw_annotation( + axis, + "", + xy=(-1.0, 0), # Inner plasma edge + xytext=(-1.0, -z_tf_inside_half), # Center + arrowprops={"arrowstyle": "<->", "color": "black"}, + ) + axis.axhline(y=-z_tf_inside_half, color="black", linestyle="--", linewidth=1) + + # Add a label for coil internal height + draw_text( + axis, + -1.0, + -z_tf_inside_half / 2, + f"{z_tf_inside_half:.3f} m", + fontsize=7, + color="black", + rotation=270, + verticalalignment="center", + horizontalalignment="center", + bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, + ) + # ========================================================= + + # Arrow for internal coil width + draw_annotation( + axis, + "", + xy=(r_tf_inboard_out, -z_tf_inside_half / 12), + xytext=(r_tf_outboard_in, -z_tf_inside_half / 12), + arrowprops={"arrowstyle": "<->", "color": "black"}, + ) + + # Add a label for the internal coil width + draw_text( + axis, + (r_tf_inboard_out + r_tf_outboard_in) / 1.5, + -z_tf_inside_half / 12, + f"{mfile.get('dr_tf_internal_midplane', scan=scan):.3f} m", + fontsize=7, + color="black", + verticalalignment="center", + horizontalalignment="center", + bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, + zorder=100, # Ensure label is on top of all plots + ) + + # ============================================================= + + # Arrow for full coil width + draw_annotation( + axis, + "", + xy=(r_tf_inboard_in, 0.0), + xytext=(r_tf_outboard_in + dr_tf_outboard, 0.0), + arrowprops={"arrowstyle": "<|-|>", "color": "black"}, + zorder=100, # Ensure label is on top of all plots + ) + + # Add a label for the full coil width + draw_text( + axis, + (r_tf_inboard_out + r_tf_outboard_in) / 1.5, + 0.0, + f"{mfile.get('dr_tf_full_midplane', scan=scan):.3f} m", + fontsize=7, + color="black", + verticalalignment="center", + horizontalalignment="center", + bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, + zorder=100, # Ensure label is on top of all plots + ) + + # ============================================================= + + # Plot vertical lines for the inboard TF coil start and end + axis.axvline( + r_tf_inboard_in, + color="black", + linestyle="--", + linewidth=1, + alpha=0.5, + label="TF Inboard Start", + ) + axis.axvline( + r_tf_inboard_out, + color="black", + linestyle="--", + linewidth=1, + alpha=0.5, + label="TF Inboard End", + ) + # Plot vertical lines for the outboard TF coil start and end + axis.axvline( + r_tf_outboard_in, + color="black", + linestyle="--", + linewidth=1, + alpha=0.5, + label="TF Outboard Start", + ) + axis.axvline( + r_tf_outboard_in + dr_tf_outboard, + color="black", + linestyle="--", + linewidth=1, + alpha=0.5, + label="TF Outboard End", + ) + + # Add a label for the inboard thickness + draw_text( + axis, + r_tf_inboard_in, + (y4 - dr_tf_inboard) * 1.1, + rf"$\Delta r = ${dr_tf_inboard:.3f} m", + fontsize=7, + color="black", + verticalalignment="center", + bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, + ) + + # Add a label for the outboard thickness + draw_text( + axis, + r_tf_outboard_in, + (y4 - dr_tf_inboard) * 1.1, + rf"$\Delta r = ${dr_tf_outboard:.3f} m", + fontsize=7, + color="black", + verticalalignment="center", + bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, + ) + + # ============================================================== + + # Add a label for the length of the coil + draw_text( + axis, + (r_tf_outboard_in + 2 * dr_tf_outboard), + 0.0, + rf"Length of coil = {len_tf_coil:.3f} m", + fontsize=7, + color="black", + verticalalignment="center", + bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, + zorder=100, # Ensure label is on top of all plots + ) + + # ============================================================== + + # Add a label for the length of the coil + draw_text( + axis, + (r_tf_outboard_in + 2 * dr_tf_outboard), + -1.0, + f"$\\Delta Z$ upper and lower to midplane = {dz_tf_upper_lower_midplane:.3f} m", + fontsize=7, + color="black", + verticalalignment="center", + bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, + ) + + # ============================================================== + + # Add arow for inboard coil radius + draw_annotation( + axis, + "", + xy=(r_tf_inboard_in, 0), + xytext=(0, 0), + arrowprops={"arrowstyle": "->", "color": "black"}, + ) + + # Add label for inboard coil radius + draw_text( + axis, + r_tf_inboard_in / 2, + 0.0, + f"{r_tf_inboard_in:.3f} m", + fontsize=7, + color="black", + verticalalignment="center", + horizontalalignment="center", + bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, + zorder=101, # Ensure label is on top of all plots + ) + + # ============================================================= + + # ============================================================== + + if mfile.get("i_tf_shape", scan=scan) == 1: + # Add arow for inboard coil radius + draw_annotation( + axis, + "", + xy=(r_tf_outboard_in + dr_tf_outboard, y2 + dr_tf_inboard), + xytext=(0, y2 + dr_tf_inboard), + arrowprops={"arrowstyle": "->", "color": "black"}, + ) + + # Add label for inboard coil radius + draw_text( + axis, + r_tf_inboard_in / 2, + y2 + dr_tf_inboard, + f"{r_tf_outboard_in + dr_tf_outboard:.3f} m", + fontsize=7, + color="black", + verticalalignment="center", + horizontalalignment="center", + bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, + ) + + axis.plot( + 0, + y2 + dr_tf_inboard, + marker="o", + color="black", + markersize=7, + zorder=100, + ) + + # ============================================================== + + y_center = y2 - ((y2 - y4) / 2) + # also draw a red horizontal line at the same vertical centre + axis.axhline(y=y_center, color="red", linestyle="--", linewidth=1.0, zorder=5) + + # Add a label the plasma and TF vertical centre distance offset + draw_text( + axis, + (r_tf_outboard_in + 2 * dr_tf_outboard), + -2.0, + "$\\Delta Z$ coil centre to plasma centre =" + f" {mfile.get('dz_tf_plasma_centre_offset', scan=scan):.3f} m", + fontsize=7, + color="black", + verticalalignment="center", + bbox={"boxstyle": "round", "facecolor": "pink", "alpha": 1.0}, + ) + + # ============================================================= + + # Plot a red dot at (0,0) + axis.plot(0, 0, marker="o", color="red", markersize=7) + + # Plot a red dashed vertical line at R=0 + axis.axvline(0, color="red", linestyle="--", linewidth=1) + + # Add centre line at + axis.axhline(y=0, color="red", linestyle="--", linewidth=1) + axis.set_xlim(-3.0, (r_tf_outboard_in + dr_tf_outboard) * 1.4) + axis.set_ylim((y4 - dr_tf_inboard) * 1.2, (y2 + dr_tf_inboard) * 1.2) + axis.set_xlabel("R [m]") + axis.set_ylabel("Z [m]") + axis.set_title("TF Coil Poloidal Cross-Section") + axis.minorticks_on() + axis.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.2) + # Move the legend to above the plot + axis.legend(labels, loc="upper center", bbox_to_anchor=(1.01, 0.85), ncol=1) + + +def plot_tf_stress(axis: plt.Axes, mfile: MFile): + """Function to plot the TF coil stress from the SIG_TF.json file. + + Input file: + SIG_TF.json + + Parameters + ---------- + axis: plt.Axes : + + mfile: MFile : + + """ + # Step 1 : Data extraction + # ---------------------------------------------------------------------------------------------- # noqa: E501 + # Number of physical quantity value per coil layer + n_radial_array_layer = 0 + + # Physical quantities : full vectors + radius = [] + radial_smeared_stress = [] + toroidal_smeared_stress = [] + vertical_smeared_stress = [] + tresca_smeared_stress = [] + radial_stress = [] + toroidal_stress = [] + vertical_stress = [] + vm_stress = [] + tresca_stress = [] + cea_tresca_stress = [] + radial_strain = [] + toroidal_strain = [] + vertical_strain = [] + radial_displacement = [] + + # Physical quantity : WP stress + wp_vertical_stress = [] + + # Physical quantity : values at layer border + bound_radius = [] + bound_radial_smeared_stress = [] + bound_toroidal_smeared_stress = [] + bound_vertical_smeared_stress = [] + bound_tresca_smeared_stress = [] + bound_radial_stress = [] + bound_toroidal_stress = [] + bound_vertical_stress = [] + bound_vm_stress = [] + bound_tresca_stress = [] + bound_cea_tresca_stress = [] + bound_radial_strain = [] + bound_toroidal_strain = [] + bound_vertical_strain = [] + bound_radial_displacement = [] + + with open( + mfile.filename.with_name(mfile.filename.name.replace("MFILE.DAT", "SIG_TF.json")) + ) as f: + sig_data = json.load(f) + + # Getting the data to be plotted + n_radial_array_layer = sig_data["Points per layers"] + n_points = len(sig_data["Radius (m)"]) + n_layers = int(n_points / n_radial_array_layer) + for ii in range(n_layers): + # Full vector + radius.append([]) + radial_stress.append([]) + toroidal_stress.append([]) + vertical_stress.append([]) + radial_smeared_stress.append([]) + toroidal_smeared_stress.append([]) + vertical_smeared_stress.append([]) + vm_stress.append([]) + tresca_stress.append([]) + cea_tresca_stress.append([]) + radial_displacement.append([]) + + for jj in range(n_radial_array_layer): + radius[ii].append(sig_data["Radius (m)"][ii * n_radial_array_layer + jj]) + radial_stress[ii].append( + sig_data["Radial stress (MPa)"][ii * n_radial_array_layer + jj] + ) + toroidal_stress[ii].append( + sig_data["Toroidal stress (MPa)"][ii * n_radial_array_layer + jj] + ) + if len(sig_data["Vertical stress (MPa)"]) == 1: + vertical_stress[ii].append(sig_data["Vertical stress (MPa)"][0]) + else: + vertical_stress[ii].append( + sig_data["Vertical stress (MPa)"][ii * n_radial_array_layer + jj] + ) + radial_smeared_stress[ii].append( + sig_data["Radial smear stress (MPa)"][ii * n_radial_array_layer + jj] + ) + toroidal_smeared_stress[ii].append( + sig_data["Toroidal smear stress (MPa)"][ii * n_radial_array_layer + jj] + ) + vertical_smeared_stress[ii].append( + sig_data["Vertical smear stress (MPa)"][ii * n_radial_array_layer + jj] + ) + vm_stress[ii].append( + sig_data["Von-Mises stress (MPa)"][ii * n_radial_array_layer + jj] + ) + tresca_stress[ii].append( + sig_data["CEA Tresca stress (MPa)"][ii * n_radial_array_layer + jj] + ) + cea_tresca_stress[ii].append( + sig_data["CEA Tresca stress (MPa)"][ii * n_radial_array_layer + jj] + ) + radial_displacement[ii].append( + sig_data["rad. displacement (mm)"][ii * n_radial_array_layer + jj] + ) + + # Layer lower boundaries values + bound_radius.append(sig_data["Radius (m)"][ii * n_radial_array_layer]) + bound_radial_stress.append( + sig_data["Radial stress (MPa)"][ii * n_radial_array_layer] + ) + bound_toroidal_stress.append( + sig_data["Toroidal stress (MPa)"][ii * n_radial_array_layer] + ) + if len(sig_data["Vertical stress (MPa)"]) == 1: + bound_vertical_stress.append(sig_data["Vertical stress (MPa)"][0]) + else: + bound_vertical_stress.append( + sig_data["Vertical stress (MPa)"][ii * n_radial_array_layer] + ) + bound_radial_smeared_stress.append( + sig_data["Radial smear stress (MPa)"][ii * n_radial_array_layer] + ) + bound_toroidal_smeared_stress.append( + sig_data["Toroidal smear stress (MPa)"][ii * n_radial_array_layer] + ) + bound_vertical_smeared_stress.append( + sig_data["Vertical smear stress (MPa)"][ii * n_radial_array_layer] + ) + bound_vm_stress.append( + sig_data["Von-Mises stress (MPa)"][ii * n_radial_array_layer] + ) + bound_tresca_stress.append( + sig_data["CEA Tresca stress (MPa)"][ii * n_radial_array_layer] + ) + bound_cea_tresca_stress.append( + sig_data["CEA Tresca stress (MPa)"][ii * n_radial_array_layer] + ) + bound_radial_displacement.append( + sig_data["rad. displacement (mm)"][ii * n_radial_array_layer] + ) + + # Layer upper boundaries values + bound_radius.append(sig_data["Radius (m)"][(ii + 1) * n_radial_array_layer - 1]) + bound_radial_stress.append( + sig_data["Radial stress (MPa)"][(ii + 1) * n_radial_array_layer - 1] + ) + bound_toroidal_stress.append( + sig_data["Toroidal stress (MPa)"][(ii + 1) * n_radial_array_layer - 1] + ) + if len(sig_data["Vertical stress (MPa)"]) == 1: + bound_vertical_stress.append(sig_data["Vertical stress (MPa)"][0]) + else: + bound_vertical_stress.append( + sig_data["Vertical stress (MPa)"][(ii + 1) * n_radial_array_layer - 1] + ) + bound_radial_smeared_stress.append( + sig_data["Radial smear stress (MPa)"][(ii + 1) * n_radial_array_layer - 1] + ) + bound_toroidal_smeared_stress.append( + sig_data["Toroidal smear stress (MPa)"][(ii + 1) * n_radial_array_layer - 1] + ) + bound_vertical_smeared_stress.append( + sig_data["Vertical smear stress (MPa)"][(ii + 1) * n_radial_array_layer - 1] + ) + bound_vm_stress.append( + sig_data["Von-Mises stress (MPa)"][(ii + 1) * n_radial_array_layer - 1] + ) + bound_tresca_stress.append( + sig_data["CEA Tresca stress (MPa)"][(ii + 1) * n_radial_array_layer - 1] + ) + bound_cea_tresca_stress.append( + sig_data["CEA Tresca stress (MPa)"][(ii + 1) * n_radial_array_layer - 1] + ) + bound_radial_displacement.append( + sig_data["rad. displacement (mm)"][(ii + 1) * n_radial_array_layer - 1] + ) + + # TRESCA smeared stress [MPa] + for ii in range(n_layers): + tresca_smeared_stress.append([]) + + bound_tresca_smeared_stress.extend([ + max( + abs(radial_smeared_stress[ii][0]), + abs(toroidal_smeared_stress[ii][0]), + ) + + vertical_smeared_stress[ii][0], + max( + abs(radial_smeared_stress[ii][n_radial_array_layer - 1]), + abs(toroidal_smeared_stress[ii][n_radial_array_layer - 1]), + ) + + vertical_smeared_stress[ii][n_radial_array_layer - 1], + ]) + for jj in range(n_radial_array_layer): + tresca_smeared_stress[ii].append( + max( + abs(radial_smeared_stress[ii][jj]), + abs(toroidal_smeared_stress[ii][jj]), + ) + + vertical_smeared_stress[ii][jj] + ) + + # Strains + if len(sig_data) > 16: + for ii in range(n_layers): + radial_strain.append([]) + toroidal_strain.append([]) + vertical_strain.append([]) + + bound_radial_strain.extend([ + sig_data["Radial strain"][ii * n_radial_array_layer], + sig_data["Radial strain"][(ii + 1) * n_radial_array_layer - 1], + ]) + bound_toroidal_strain.extend([ + sig_data["Toroidal strain"][ii * n_radial_array_layer], + sig_data["Toroidal strain"][(ii + 1) * n_radial_array_layer - 1], + ]) + bound_vertical_strain.extend([ + sig_data["Vertical strain"][ii * n_radial_array_layer], + sig_data["Vertical strain"][(ii + 1) * n_radial_array_layer - 1], + ]) + for jj in range(n_radial_array_layer): + radial_strain[ii].append( + sig_data["Radial strain"][ii * n_radial_array_layer + jj] + ) + toroidal_strain[ii].append( + sig_data["Toroidal strain"][ii * n_radial_array_layer + jj] + ) + vertical_strain[ii].append( + sig_data["Vertical strain"][ii * n_radial_array_layer + jj] + ) + + if "WP smeared stress (MPa)" in sig_data: + wp_vertical_stress.append(sig_data["WP smeared stress (MPa)"][jj]) + + axis_tick_size = 12 + legend_size = 10 + mark_size = 10 + line_width = 3.5 + + # PLOT 1 : Stress summary + # ------------------------ + + ax = axis[0] + for ii in range(n_layers): + ax.plot( + radius[ii], + radial_stress[ii], + "-", + linewidth=line_width, + color="lightblue", + ) + ax.plot( + radius[ii], + toroidal_stress[ii], + "-", + linewidth=line_width, + color="wheat", + ) + ax.plot( + radius[ii], + vertical_stress[ii], + "-", + linewidth=line_width, + color="lightgrey", + ) + ax.plot( + radius[ii], + tresca_stress[ii], + "-", + linewidth=line_width, + color="pink", + ) + ax.plot( + radius[ii], + vm_stress[ii], + "-", + linewidth=line_width, + color="violet", + ) + ax.plot( + radius[0], + radial_stress[0], + "--", + color="dodgerblue", + label=r"$\sigma_{rr}$", + ) + ax.plot( + radius[0], + toroidal_stress[0], + "--", + color="orange", + label=r"$\sigma_{\theta\theta}$", + ) + ax.plot( + radius[0], + vertical_stress[0], + "--", + color="mediumseagreen", + label=r"$\sigma_{zz}$", + ) + ax.plot( + radius[0], + tresca_stress[0], + "-", + color="crimson", + label=r"$\sigma_{TRESCA}$", + ) + ax.plot( + radius[0], + vm_stress[0], + "-", + color="darkviolet", + label=r"$\sigma_{Von\ mises}$", + ) + for ii in range(1, n_layers): + ax.plot(radius[ii], radial_stress[ii], "--", color="dodgerblue") + ax.plot(radius[ii], toroidal_stress[ii], "--", color="orange") + ax.plot(radius[ii], vertical_stress[ii], "--", color="mediumseagreen") + ax.plot(radius[ii], tresca_stress[ii], "-", color="crimson") + ax.plot(radius[ii], vm_stress[ii], "-", color="darkviolet") + ax.plot( + bound_radius, + bound_radial_stress, + "|", + markersize=mark_size, + color="dodgerblue", + ) + ax.plot( + bound_radius, + bound_toroidal_stress, + "|", + markersize=mark_size, + color="orange", + ) + ax.plot( + bound_radius, + bound_vertical_stress, + "|", + markersize=mark_size, + color="mediumseagreen", + ) + ax.plot( + bound_radius, + bound_tresca_stress, + "|", + markersize=mark_size, + color="crimson", + ) + ax.plot( + bound_radius, + bound_vm_stress, + "|", + markersize=mark_size, + color="darkviolet", + ) + ax.grid(True) + ax.set_ylabel(r"$\sigma$ [$MPa$]", fontsize=axis_tick_size) + ax.set_title("Structure Stress Summary") + ax.legend(loc="center left", bbox_to_anchor=(1, 0.5), fontsize=legend_size) + + # PLOT 2 : Smeared stress summary + # ------------------------ + ax = axis[1] + for ii in range(n_layers): + ax.plot( + radius[ii], + radial_smeared_stress[ii], + "-", + linewidth=line_width, + color="lightblue", + ) + ax.plot( + radius[ii], + toroidal_smeared_stress[ii], + "-", + linewidth=line_width, + color="wheat", + ) + ax.plot( + radius[ii], + vertical_smeared_stress[ii], + "-", + linewidth=line_width, + color="lightgrey", + ) + ax.plot( + radius[ii], + tresca_smeared_stress[ii], + "-", + linewidth=line_width, + color="pink", + ) + ax.plot( + radius[0], + radial_smeared_stress[0], + "--", + color="dodgerblue", + label=r"$\sigma_{rr}^\mathrm{smeared}$", + ) + ax.plot( + radius[0], + toroidal_smeared_stress[0], + "--", + color="orange", + label=r"$\sigma_{\theta\theta}^\mathrm{smeared}$", + ) + ax.plot( + radius[0], + vertical_smeared_stress[0], + "--", + color="mediumseagreen", + label=r"$\sigma_{zz}^\mathrm{smeared}$", + ) + ax.plot( + radius[0], + tresca_smeared_stress[0], + "-", + color="crimson", + label=r"$\sigma_{TRESCA}^\mathrm{smeared}$", + ) + for ii in range(1, n_layers): + ax.plot(radius[ii], radial_smeared_stress[ii], "--", color="dodgerblue") + ax.plot(radius[ii], toroidal_smeared_stress[ii], "--", color="orange") + ax.plot( + radius[ii], + vertical_smeared_stress[ii], + "--", + color="mediumseagreen", + ) + ax.plot(radius[ii], tresca_smeared_stress[ii], "-", color="crimson") + ax.plot( + bound_radius, + bound_radial_smeared_stress, + "|", + markersize=mark_size, + color="dodgerblue", + ) + ax.plot( + bound_radius, + bound_toroidal_smeared_stress, + "|", + markersize=mark_size, + color="orange", + ) + ax.plot( + bound_radius, + bound_vertical_smeared_stress, + "|", + markersize=mark_size, + color="mediumseagreen", + ) + ax.plot( + bound_radius, + bound_tresca_smeared_stress, + "|", + markersize=mark_size, + color="crimson", + ) + ax.grid(True) + ax.set_ylabel(r"$\sigma$ [$MPa$]", fontsize=axis_tick_size) + ax.set_title("Smeared Stress Summary") + ax.legend(loc="center left", bbox_to_anchor=(1, 0.5), fontsize=legend_size) + + # PLOT 4 : Displacement + # ---------------------- + ax = axis[2] + ax.plot(radius[0], radial_displacement[0], color="dodgerblue") + for ii in range(1, n_layers): + ax.plot(radius[ii], radial_displacement[ii], color="dodgerblue") + ax.grid(True) + ax.set_ylabel(r"$u_{r}$ [mm]", fontsize=axis_tick_size) + ax.set_xlabel(r"$R$ [$m$]", fontsize=axis_tick_size) + ax.set_title("Radial Displacement") + # Only set legend for the last plot if needed + + # Set x-label only on the last axis + axis[2].set_xlabel(r"$R$ [$m$]", fontsize=axis_tick_size) + + # Set minor ticks on for all axes + for ax in axis: + ax.minorticks_on() + # Set x-ticks and y-ticks font size for all axes + for ax in axis: + ax.tick_params(axis="x", labelsize=axis_tick_size) + ax.tick_params(axis="y", labelsize=axis_tick_size) + plt.tight_layout() + + +def plot_corc_cable_geometry( + axis, + r_centre: float, + z_centre: float, + dia_croco_strand: float, + dx_croco_strand_copper: float, + dr_hts_tape: float, + dx_croco_strand_tape_stack: float, + n_croco_strand_hts_tapes: int, + dx_hts_tape_rebco: float, + dx_hts_tape_copper: float, + dx_hts_tape_hastelloy: float, + show_legend: bool = True, +): + """Plot the geometry of a CroCo strand cable. + + Parameters + ---------- + axis : matplotlib.axes._axes.Axes + The matplotlib axis to plot on. + r_centre : float + Radial position of the strand centre (in meters). + z_centre : float + Vertical position of the strand centre (in meters). + dia_croco_strand : float + Diameter of the CroCo strand (in meters). + dx_croco_strand_copper : float + Thickness of the copper layer (in meters). + dr_hts_tape : float + Radius of the HTS tape stack (in meters). + dx_croco_strand_tape_stack : float + Height of the HTS tape stack (in meters). + n_croco_strand_hts_tapes : int + Number of HTS tape layers in the stack. + """ + legend_label = None if show_legend else "_nolegend_" + + # Plot a circle with the given diameter and copper edges + circle = Circle( + (r_centre, z_centre), + radius=(dia_croco_strand / 2), + edgecolor="black", + facecolor="#B87333", + linewidth=0.5, + label="Copper jacket" if show_legend else legend_label, + ) + axis.add_patch(circle) + + # Plot an inner circle with copper edges + circle = Circle( + (r_centre, z_centre), + radius=((dia_croco_strand / 2) - dx_croco_strand_copper), + edgecolor="grey", + facecolor="grey", + linewidth=2, + label="Solder" if show_legend else legend_label, + ) + axis.add_patch(circle) + + # Plot a rectangular tape stack in the middle + rect = Rectangle( + ( + r_centre - dr_hts_tape / 2, + z_centre - dx_croco_strand_tape_stack / 2, + ), + width=dr_hts_tape, + height=dx_croco_strand_tape_stack, + edgecolor="black", + facecolor=None, + linewidth=0.1, + alpha=0.5, + linestyle="--", + label="HTS Tape Stack" if show_legend else legend_label, + ) + axis.add_patch(rect) + + # Slice the tape stack into n_croco_strand_hts_tapes layers + for i in range(int(n_croco_strand_hts_tapes)): + y_start = ( + z_centre + - (dx_croco_strand_tape_stack / 2) + + i * (dx_croco_strand_tape_stack / n_croco_strand_hts_tapes) + ) + plot_hts_tape_geometry( + axis=axis, + r_left=r_centre - (dr_hts_tape / 2), + z_bottom=y_start, + dr_hts_tape=dr_hts_tape, + dx_hts_tape_rebco=dx_hts_tape_rebco, + dx_hts_tape_copper=dx_hts_tape_copper, + dx_hts_tape_hastelloy=dx_hts_tape_hastelloy, + show_legend=False, + ) + + axis.set_xlim(-dia_croco_strand * 0.75, dia_croco_strand * 0.75) + axis.set_ylim(-dia_croco_strand * 0.75, dia_croco_strand * 0.75) + axis.set_aspect("equal", adjustable="datalim") + axis.set_title("CroCo Strand Geometry") + axis.grid(True) + axis.set_xlabel("X-axis (m)") + axis.set_ylabel("Y-axis (m)") + axis.minorticks_on() + if show_legend: + axis.legend(loc="upper right") + + +def plot_tf_corc_cable_summary_box(axis, fig, mfile: MFile, scan: int): + """Plot TF CORC cable summary box""" + textstr_cable = ( + "$\\mathbf{CroCo \\ Cable:}$\n\nCable diameter:" + f" {mfile.get('dia_tf_turn_croco_cable', scan=scan) * 1e3:,.4f}" + " mm\nCopper width:" + f" {mfile.get('dx_tf_croco_strand_copper', scan=scan) * 1e3:,.4f}" + " mm\nDiameter of solder tape region:" + f" {mfile.get('dia_tf_croco_strand_tape_region', scan=scan) * 1e3:,.4f}" + " mm\nHeight of tape stack:" + f" {mfile.get('dx_tf_croco_strand_tape_stack', scan=scan) * 1e3:,.4f}" + " mm\nWidth of HTS tape / tape stack:" + f" {mfile.get('dr_tf_hts_tape', scan=scan) * 1e3:,.4f} mm\nNumber of" + " HTS tape layers:" + f" {int(mfile.get('n_tf_croco_strand_hts_tapes', scan=scan))}\n\nTotal" + " copper area:" + f" {mfile.get('a_tf_croco_strand_copper_total', scan=scan) * 1e6:,.4f}" + " mm²\nTotal hastelloy area:" + f" {mfile.get('a_tf_croco_strand_hastelloy', scan=scan) * 1e6:,.4f}" + " mm²\nTotal solder area:" + f" {mfile.get('a_tf_croco_strand_solder', scan=scan) * 1e6:,.4f}" + " mm²\nTotal superconductor area:" + f" {mfile.get('a_tf_croco_strand_rebco', scan=scan) * 1e6:,.4f}" + " mm²\nTotal strand area:" + f" {mfile.get('a_tf_croco_strand', scan=scan) * 1e6:,.4f} mm²\n" + ) + + draw_text( + axis, + 0.4, + 0.4, + textstr_cable, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=box_style("#cccccc"), # grayish color + ) + + +def plot_quench_time_evolution( + tau_discharge: float, + b_peak: float, + f_a_cable_copper: float, + f_a_cable_space_helium: float, + temp_he_peak: float, + temp_quench_max: float, + cu_rrr: float, + t_quench_detection: float, + fluence: float, + j_operating: float, + a_tf_turn_cable_space: float, + a_tf_turn: float, + n_points: int = 500, + axes_1: plt.Axes | None = None, + axes_2: plt.Axes | None = None, + show: bool = False, +) -> None: + """Plots the time evolution of the quench model hotspot temperature and current. + + Visualises the adiabatic hotspot temperature rise and exponentially decaying + current during a quench, highlighting the quench detection time. + + Parameters + ---------- + tau_discharge: + Quench discharge time constant [s]. + b_peak: + Magnetic field at the peak point [T]. + f_a_cable_copper: + Fraction of cable cross-section that is copper. + f_a_cable_space_helium: + Fraction of cable space occupied by helium. + temp_he_peak: + Peak helium temperature at quench initiation [K]. + temp_quench_max: + Maximum allowed conductor temperature during quench [K]. + cu_rrr: + Residual resistivity ratio of copper. + t_quench_detection: + Detection time delay [s]. + fluence: + Neutron fluence [n/m²]. + j_operating: + Operating current density [A/m²] to compare against the quench protection limit. + a_tf_turn_cable_space: + Area of the TF turn cable space [m²]. + a_tf_turn: + Area of the TF turn [m²]. + n_points: + Number of time points for the plot. + axes_1: + Optional axis for the current density panel. + axes_2: + Optional axis for the hotspot temperature panel. + show: + Whether to display the plot with Matplotlib. Defaults to False to avoid + GUI backend warnings in non-interactive environments. + + Raises + ------ + ValueError + If only one set of axes is provided, instead of both or neither + """ + figure = None + if axes_1 is None and axes_2 is None: + figure, (axes_1, axes_2) = plt.subplots(2, 1, sharex=True) + elif axes_1 is None or axes_2 is None: + msg = "Both axes_1 and axes_2 must be provided together, or neither." + raise ValueError(msg) + + fluence = np.clip(fluence, 0.0, 1.5e23) + + j_max = ( + a_tf_turn_cable_space / a_tf_turn + ) * quench.calculate_quench_protection_current_density( + tau_discharge=tau_discharge, + b_peak=b_peak, + f_a_cable_copper=f_a_cable_copper, + f_a_cable_space_helium=f_a_cable_space_helium, + temp_he_peak=temp_he_peak, + temp_quench_max=temp_quench_max, + cu_rrr=cu_rrr, + t_quench_detection=t_quench_detection, + fluence=fluence, + ) + + fluence_1e23 = 1e23 + j_max_1e23 = ( + a_tf_turn_cable_space / a_tf_turn + ) * quench.calculate_quench_protection_current_density( + tau_discharge=tau_discharge, + b_peak=b_peak, + f_a_cable_copper=f_a_cable_copper, + f_a_cable_space_helium=f_a_cable_space_helium, + temp_he_peak=temp_he_peak, + temp_quench_max=temp_quench_max, + cu_rrr=cu_rrr, + t_quench_detection=t_quench_detection, + fluence=fluence_1e23, + ) + + # Time axis: from 0 to ~4 time constants after discharge begins at detection. + # This ensures later annotations/interpolations at t_quench_detection + tau_discharge + # and beyond remain within the sampled domain. + t_end = max(4.0 * tau_discharge, t_quench_detection + 4.0 * tau_discharge) + times = np.linspace(0.0, t_end, n_points) + + # Current density decays exponentially after detection + decay = np.exp(-(times - t_quench_detection) / tau_discharge) + + j_profile_required, j_profile_required_1e23, j_profile_real = [ + np.where(times < t_quench_detection, j0, j0 * decay) + for j0 in (j_max, j_max_1e23, j_operating) + ] + + # Adiabatic hotspot temperature: integrate heat balance over time + # T(t) is found by inverting: integral_{T0}^{T(t)} [sum(rho*cp)] / rho_cu dT = + # integral_0^t J² dt + # We accumulate the (∫J² dt) and map it to temperature via the precomputed integral. + f_cu_cable = (1.0 - f_a_cable_space_helium) * f_a_cable_copper + f_sc_cable = (1.0 - f_a_cable_space_helium) * (1.0 - f_a_cable_copper) + + # Build a temperature lookup: cumulative integral from t_he_peak to T + temp_array, cum_integral = quench._build_cumulative_quench_integral( + temp_he_peak=temp_he_peak, + temp_quench_max=temp_quench_max, + field=b_peak, + rrr=cu_rrr, + fluence=fluence, + f_a_cable_space_helium=f_a_cable_space_helium, + f_cu_cable=f_cu_cable, + f_sc_cable=f_sc_cable, + ) + temp_array_1e23, cum_integral_1e23 = quench._build_cumulative_quench_integral( + temp_he_peak=temp_he_peak, + temp_quench_max=temp_quench_max, + field=b_peak, + rrr=cu_rrr, + fluence=fluence_1e23, + f_a_cable_space_helium=f_a_cable_space_helium, + f_cu_cable=f_cu_cable, + f_sc_cable=f_sc_cable, + ) + + # Numerically integrate J² dt over time to get MIIT (Mega-Ampere²-seconds) at + # each time step + dt = times[1] - times[0] + miit_required = np.cumsum(j_profile_required**2) * dt + miit_required_1e23 = np.cumsum(j_profile_required_1e23**2) * dt + miit_real = np.cumsum(j_profile_real**2) * dt + + # Convert the cable-space thermal integral to winding-pack basis to match + # j_profile_*. + area_ratio = a_tf_turn_cable_space / a_tf_turn + scaled_integral = (area_ratio**2) * f_cu_cable * cum_integral + scaled_integral_1e23 = (area_ratio**2) * f_cu_cable * cum_integral_1e23 + hotspot_temp_required = np.interp(miit_required, scaled_integral, temp_array) + hotspot_temp_required_1e23 = np.interp( + miit_required_1e23, scaled_integral_1e23, temp_array_1e23 + ) + hotspot_temp_real = np.interp(miit_real, scaled_integral, temp_array) + + # --- Current density panel --- + axes_1.plot( + times, + j_profile_required, + color="darkorange", + linewidth=2, + label=( + f"Max allowed current density for protection (fluence = {fluence:.2e} n/m²)" + ), + ) + axes_1.plot( + times, + j_profile_required_1e23, + color="darkorange", + linewidth=2, + linestyle="--", + label=("Max allowed current density for protection (fluence = 1e23 n/m²)"), + ) + axes_1.plot( + times, + j_profile_real, + color="blue", + linewidth=2, + label="Operating current density", + ) + axes_1.axvline( + t_quench_detection, + color="crimson", + linestyle="--", + linewidth=1.5, + label=f"Detection time ({t_quench_detection:.1f} s)", + ) + axes_1.axvspan( + 0, + t_quench_detection, + alpha=0.08, + color="crimson", + label="Pre-detection phase", + ) + axes_1.set_ylabel("Current density [A/m²]") + axes_1.legend(fontsize=9) + axes_1.grid(True, alpha=0.3) + axes_1.set_title( + "TF Coil Quench Protection: Current Density and Hotspot Temperature Evolution" + ) + + # --- Temperature panel --- + axes_2.plot( + times, + hotspot_temp_required, + color="darkorange", + linewidth=2, + label=( + f"Hotspot temperature at protection limit (fluence = {fluence:.2e} n/m²)" + ), + ) + axes_2.plot( + times, + hotspot_temp_required_1e23, + color="darkorange", + linewidth=2, + linestyle="--", + label="Hotspot temperature at protection limit (fluence = 1e23 n/m²)", + ) + axes_2.plot( + times, + hotspot_temp_real, + color="blue", + linewidth=2, + label="Operating hotspot temperature", + ) + + axes_2.axvline( + t_quench_detection, + color="crimson", + linestyle="--", + linewidth=1.5, + label=f"$t_{{\\text{{detect}}}}$ ({t_quench_detection:.2f} s)", + ) + axes_2.axvspan(0, t_quench_detection, alpha=0.08, color="crimson") + axes_2.axhline( + temp_quench_max, + color="grey", + linestyle=":", + linewidth=1.5, + label=f"$T_{{\\text{{max}}}}$ = {temp_quench_max} K", + ) + axes_2.set_xlabel("Time [s]") + axes_2.set_ylabel("Temperature [K]") + axes_2.legend(fontsize=9) + axes_2.grid(True, alpha=0.3) + + # Mark tau_discharge after detection time with vertical and horizontal lines + tau_time = t_quench_detection + tau_discharge + tau_j = j_max * np.exp( + -1 + ) # current density at t = t_quench_detection + tau_discharge + tau_temp = float(np.interp(tau_time, times, hotspot_temp_required)) + + for ax, val, label in [ + ( + axes_1, + tau_j, + f"$J$ at $\\tau_{{\\text{{discharge}}}}$ ({tau_j:.2e} A/m²)", + ), + ( + axes_2, + tau_temp, + f"$T$ at $\\tau_{{\\text{{discharge}}}}$ ({tau_temp:.1f} K)", + ), + ]: + ax.axvline( + tau_time, + color="forestgreen", + linestyle="--", + linewidth=1.5, + label=( + "$t_{\\text{detect}} + \\tau_{\\text{discharge}}$" + f" ({tau_time:.2f} s)" + ), + ) + ax.axhline( + val, + color="forestgreen", + linestyle=":", + linewidth=1.5, + label=label, + ) + axes_1.legend(fontsize=9) + axes_1.minorticks_on() + axes_2.legend(fontsize=9) + axes_2.minorticks_on() + + if figure is not None: + figure.tight_layout() + else: + plt.tight_layout() + + if show: + plt.show() + + +__all__ = [ + "TF_outboard", + "plot_corc_cable_geometry", + "plot_quench_time_evolution", + "plot_resistive_tf_info", + "plot_resistive_tf_wp", + "plot_superconducting_tf_wp", + "plot_tf_cable_in_conduit_turn", + "plot_tf_coil_structure", + "plot_tf_coils", + "plot_tf_corc_cable_summary_box", + "plot_tf_croco_turn", + "plot_tf_stress", +] diff --git a/process/core/io/plot/summary/plasma/__init__.py b/process/core/io/plot/summary/plasma/__init__.py new file mode 100644 index 0000000000..ea33c772e1 --- /dev/null +++ b/process/core/io/plot/summary/plasma/__init__.py @@ -0,0 +1,51 @@ +"""Public API for this summary plotting concern.""" + +from __future__ import annotations + +import process.core.io.plot.summary.plasma.confinement as _confinement +import process.core.io.plot.summary.plasma.current_drive as _current_drive +import process.core.io.plot.summary.plasma.overview as _overview +import process.core.io.plot.summary.plasma.physics as _physics + +_MODULES = (_confinement, _current_drive, _overview, _physics) +_REGISTRY = {} +for _module in _MODULES: + _REGISTRY.update({name: getattr(_module, name) for name in _module.__all__}) +for _module in _MODULES: + _module.__dict__.update(_REGISTRY) +plot_bootstrap_comparison = _REGISTRY["plot_bootstrap_comparison"] +plot_brunner_divertor_power_split_comparison_stackplot = _REGISTRY[ + "plot_brunner_divertor_power_split_comparison_stackplot" +] +plot_confinement_time_comparison = _REGISTRY["plot_confinement_time_comparison"] +plot_current_drive_info = _REGISTRY["plot_current_drive_info"] +plot_detailed_plasma_parameters = _REGISTRY["plot_detailed_plasma_parameters"] +plot_magnetic_fields_in_plasma = _REGISTRY["plot_magnetic_fields_in_plasma"] +plot_main_plasma_information = _REGISTRY["plot_main_plasma_information"] +plot_max_normalised_beta_comparison = _REGISTRY["plot_max_normalised_beta_comparison"] +plot_plasma = _REGISTRY["plot_plasma"] +plot_plasma_coloumb_logarithms = _REGISTRY["plot_plasma_coloumb_logarithms"] +plot_plasma_current_comparison = _REGISTRY["plot_plasma_current_comparison"] +plot_plasma_outboard_toroidal_ripple_map = _REGISTRY[ + "plot_plasma_outboard_toroidal_ripple_map" +] +plot_sol_power_decay_length_comparison = _REGISTRY[ + "plot_sol_power_decay_length_comparison" +] +reaction_plot_grid = _REGISTRY["reaction_plot_grid"] +__all__ = [ + "plot_bootstrap_comparison", + "plot_brunner_divertor_power_split_comparison_stackplot", + "plot_confinement_time_comparison", + "plot_current_drive_info", + "plot_detailed_plasma_parameters", + "plot_magnetic_fields_in_plasma", + "plot_main_plasma_information", + "plot_max_normalised_beta_comparison", + "plot_plasma", + "plot_plasma_coloumb_logarithms", + "plot_plasma_current_comparison", + "plot_plasma_outboard_toroidal_ripple_map", + "plot_sol_power_decay_length_comparison", + "reaction_plot_grid", +] diff --git a/process/core/io/plot/summary/plasma/confinement.py b/process/core/io/plot/summary/plasma/confinement.py new file mode 100644 index 0000000000..d7ded750a3 --- /dev/null +++ b/process/core/io/plot/summary/plasma/confinement.py @@ -0,0 +1,541 @@ +"""Plasma functions for PROCESS summary plots.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import matplotlib.pyplot as plt +import numpy as np + +from process.core.io.plot.summary.rendering import ( + draw_text, +) +from process.data_structure.physics_variables import ( + ConfinementTimeModel, + OutbordSOLPowerDecayLengthModel, +) +from process.models.physics.confinement_time import PlasmaConfinementTime +from process.models.physics.exhaust import ( + calculate_brunner_divertor_power_splits, +) + +if TYPE_CHECKING: + from process.core.io.mfile import MFile + + +def plot_sol_power_decay_length_comparison(axis: plt.Axes, mfile: MFile, scan: int): + """Function to plot a scatter box plot of SOL power decay lengths (λ_q). + + Parameters + ---------- + axis : + axis object to plot to + mfile : + MFILE data object + scan : + scan number to use + """ + len_plasma_sol_eich13_power_decay_mm = ( + mfile.get("len_plasma_sol_eich13_power_decay", scan=scan) * 1e3 + ) + len_plasma_sol_mast14_power_decay_1_mm = ( + mfile.get("len_plasma_sol_mast14_power_decay_1", scan=scan) * 1e3 + ) + len_plasma_sol_mast14_power_decay_2_mm = ( + mfile.get("len_plasma_sol_mast14_power_decay_2", scan=scan) * 1e3 + ) + len_plasma_sol_eich11_jet_power_decay_mm = ( + mfile.get("len_plasma_sol_eich11_jet_power_decay", scan=scan) * 1e3 + ) + len_plasma_sol_eich11_jet_asdex_power_decay_mm = ( + mfile.get("len_plasma_sol_eich11_jet_asdex_power_decay", scan=scan) * 1e3 + ) + # Data for the box plot + data = { + f"{OutbordSOLPowerDecayLengthModel.EICH_2013.description}": ( + len_plasma_sol_eich13_power_decay_mm + ), + f"{OutbordSOLPowerDecayLengthModel.MAST_2014_1.description}": ( + len_plasma_sol_mast14_power_decay_1_mm + ), + f"{OutbordSOLPowerDecayLengthModel.MAST_2014_2.description}": ( + len_plasma_sol_mast14_power_decay_2_mm + ), + f"{OutbordSOLPowerDecayLengthModel.EICH_2011_JET.description}": ( + len_plasma_sol_eich11_jet_power_decay_mm + ), + f"{OutbordSOLPowerDecayLengthModel.EICH_2011_JET_ASDEX.description}": ( + len_plasma_sol_eich11_jet_asdex_power_decay_mm + ), + } + data_values = list(data.values()) + + # Create the violin plot + axis.violinplot(data_values, showextrema=False) + + # Create the box plot + axis.boxplot(data_values, showfliers=True, showmeans=True, meanline=True, widths=0.3) + + # Scatter plot for each data point + colors = plt.cm.plasma(np.linspace(0, 1, len(data_values))) + for index, (key, value) in enumerate(data.items()): + axis.scatter(1, value, color=colors[index], label=key, alpha=1.0) + axis.legend(loc="upper left", bbox_to_anchor=(1, 1)) + + # Calculate average, standard deviation, and median + avg_decay_length = np.mean(data_values) + std_decay_length = np.std(data_values) + median_decay_length = np.median(data_values) + + # Plot average, standard deviation, and median as text + draw_text( + axis, + 1.02, + 0.2, + f"Average: {avg_decay_length:.4f}", + transform=axis.transAxes, + fontsize=9, + ) + draw_text( + axis, + 1.02, + 0.15, + f"Standard Dev: {std_decay_length:.4f}", + transform=axis.transAxes, + fontsize=9, + ) + draw_text( + axis, + 1.02, + 0.1, + f"Median: {median_decay_length:.4f}", + transform=axis.transAxes, + fontsize=9, + ) + + axis.set_title("SOL Power Decay Length ($\\lambda_q$) Comparison") + axis.set_ylabel("Power Decay Length [mm]") + axis.set_xlim(0.5, 1.5) + axis.set_xticks([]) + axis.set_xticklabels([]) + axis.set_facecolor("#f0f0f0") + + +def plot_brunner_divertor_power_split_comparison_stackplot( + axis: plt.Axes, mfile: MFile, scan: int +): + """Plot Brunner divertor power split fractions as a stack plot over dr_sep.""" + # Use the case decay length when available; fall back to 1 mm if absent. + + len_plasma_sol_outboard_pd = mfile.get("len_sol_outboard_power_decay", scan=scan) + len_plasma_sol_inboard_pd = mfile.get("len_sol_inboard_power_decay", scan=scan) + colors = plt.cm.plasma(np.linspace(0.15, 0.85, 4)) + + dr_sep_values = np.linspace( + -5 * len_plasma_sol_outboard_pd, + 5 * len_plasma_sol_outboard_pd, + 200, + ) + f_p_inboard_lower = np.zeros_like(dr_sep_values) + f_p_inboard_upper = np.zeros_like(dr_sep_values) + f_p_outboard_lower = np.zeros_like(dr_sep_values) + f_p_outboard_upper = np.zeros_like(dr_sep_values) + + for idx, dr_sep in enumerate(dr_sep_values): + div_power_splits = calculate_brunner_divertor_power_splits( + dr_outboard_midplane_sep=dr_sep, + len_plasma_sol_outboard_power_decay=len_plasma_sol_outboard_pd, + len_plasma_sol_inboard_power_decay=len_plasma_sol_inboard_pd, + ) + f_p_inboard_lower[idx] = div_power_splits.f_p_div_inboard_lower_separatrix + f_p_inboard_upper[idx] = div_power_splits.f_p_div_inboard_upper_separatrix + f_p_outboard_lower[idx] = div_power_splits.f_p_div_outboard_lower_separatrix + f_p_outboard_upper[idx] = div_power_splits.f_p_div_outboard_upper_separatrix + + axis.stackplot( + dr_sep_values, + f_p_inboard_lower, + f_p_inboard_upper, + f_p_outboard_lower, + f_p_outboard_upper, + labels=[ + "$f_{P,\\mathrm{in,lower}}$", + "$f_{P,\\mathrm{in,upper}}$", + "$f_{P,\\mathrm{out,lower}}$", + "$f_{P,\\mathrm{out,upper}}$", + ], + colors=colors, + alpha=0.9, + ) + + axis.axvline( + mfile.get("dr_plasma_outboard_midplane_separatrix_separation", scan=scan), + color="k", + linestyle="--", + linewidth=1.0, + alpha=0.5, + label="$\u0394 r_{\\mathrm{sep}}$", + ) + axis.set_ylim(0.0, 1.0) + axis.set_xlim( + -5 * len_plasma_sol_outboard_pd, + 5 * len_plasma_sol_outboard_pd, + ) + axis.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.35) + axis.set_title("Brunner Divertor Power Split Fractions") + axis.set_xlabel("$\\Delta r_{\\mathrm{sep}}$ [m]") + axis.set_ylabel("Power split fraction, $f_P$") + axis.legend(loc="upper left", fontsize=8) + + +def plot_confinement_time_comparison( + axis: plt.Axes, mfile: MFile, scan: int, u_seed=None +): + """Function to plot a scatter box plot of confinement time comparisons. + + Parameters + ---------- + axis : + Axis object to plot to. + mfile : + MFILE data object. + scan : + Scan number to use. + u_seed : + (Default value = None) + """ + rminor = mfile.get("rminor", scan=scan) + rmajor = mfile.get("rmajor", scan=scan) + cur_plasma_ma = mfile.get("plasma_current_ma", scan=scan) + kappa95 = mfile.get("kappa95", scan=scan) + nd_plasma_electron_line_20 = mfile.get("nd_plasma_electron_line", scan=scan) / 1e20 + afuel = mfile.get("m_fuel_amu", scan=scan) + b_plasma_toroidal_on_axis = mfile.get("b_plasma_toroidal_on_axis", scan=scan) + p_plasma_separatrix_mw = mfile.get("p_plasma_separatrix_mw", scan=scan) + kappa = mfile.get("kappa", scan=scan) + aspect = mfile.get("aspect", scan=scan) + nd_plasma_electron_line_19 = mfile.get("nd_plasma_electron_line", scan=scan) / 1e19 + kappa_ipb = mfile.get("kappa_ipb", scan=scan) + triang = mfile.get("triang", scan=scan) + m_ions_total_amu = mfile.get("m_ions_total_amu", scan=scan) + + confine = PlasmaConfinementTime() + + # Calculate confinement times using the scan data + iter_89p = confine.iter_89p_confinement_time( + cur_plasma_ma=cur_plasma_ma, + rmajor=rmajor, + rminor=rminor, + kappa=kappa, + nd_plasma_electron_line_20=nd_plasma_electron_line_20, + b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, + afuel=afuel, + p_plasma_loss_mw=p_plasma_separatrix_mw, + ) + iter_89_0 = confine.iter_89_0_confinement_time( + cur_plasma_ma=cur_plasma_ma, + rmajor=rmajor, + rminor=rminor, + kappa=kappa, + nd_plasma_electron_line_20=nd_plasma_electron_line_20, + b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, + afuel=afuel, + p_plasma_loss_mw=p_plasma_separatrix_mw, + ) + iter_h90_p = confine.iter_h90_p_confinement_time( + cur_plasma_ma=cur_plasma_ma, + rmajor=rmajor, + rminor=rminor, + kappa=kappa, + nd_plasma_electron_line_20=nd_plasma_electron_line_20, + b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, + afuel=afuel, + p_plasma_loss_mw=p_plasma_separatrix_mw, + ) + iter_h90_p_amended = confine.iter_h90_p_amended_confinement_time( + cur_plasma_ma=cur_plasma_ma, + b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, + afuel=afuel, + rmajor=rmajor, + p_plasma_loss_mw=p_plasma_separatrix_mw, + kappa=kappa, + ) + iter_93h = confine.iter_93h_confinement_time( + cur_plasma_ma=cur_plasma_ma, + b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, + p_plasma_loss_mw=p_plasma_separatrix_mw, + afuel=afuel, + rmajor=rmajor, + nd_plasma_electron_line_20=nd_plasma_electron_line_20, + aspect=aspect, + kappa=kappa, + ) + iter_h97p = confine.iter_h97p_confinement_time( + cur_plasma_ma=cur_plasma_ma, + b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, + p_plasma_loss_mw=p_plasma_separatrix_mw, + nd_plasma_electron_line_19=nd_plasma_electron_line_19, + rmajor=rmajor, + aspect=aspect, + kappa=kappa, + afuel=afuel, + ) + iter_h97p_elmy = confine.iter_h97p_elmy_confinement_time( + cur_plasma_ma=cur_plasma_ma, + b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, + p_plasma_loss_mw=p_plasma_separatrix_mw, + nd_plasma_electron_line_19=nd_plasma_electron_line_19, + rmajor=rmajor, + aspect=aspect, + kappa=kappa, + afuel=afuel, + ) + iter_96p = confine.iter_96p_confinement_time( + cur_plasma_ma=cur_plasma_ma, + b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, + kappa95=kappa95, + rmajor=rmajor, + aspect=aspect, + nd_plasma_electron_line_19=nd_plasma_electron_line_19, + afuel=afuel, + p_plasma_loss_mw=p_plasma_separatrix_mw, + ) + iter_pb98py = confine.iter_pb98py_confinement_time( + cur_plasma_ma=cur_plasma_ma, + b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, + nd_plasma_electron_line_19=nd_plasma_electron_line_19, + p_plasma_loss_mw=p_plasma_separatrix_mw, + rmajor=rmajor, + kappa=kappa, + aspect=aspect, + afuel=afuel, + ) + iter_ipb98y = confine.iter_ipb98y_confinement_time( + cur_plasma_ma=cur_plasma_ma, + b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, + nd_plasma_electron_line_19=nd_plasma_electron_line_19, + p_plasma_loss_mw=p_plasma_separatrix_mw, + rmajor=rmajor, + kappa=kappa, + aspect=aspect, + afuel=afuel, + ) + iter_ipb98y1 = confine.iter_ipb98y1_confinement_time( + cur_plasma_ma=cur_plasma_ma, + b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, + nd_plasma_electron_line_19=nd_plasma_electron_line_19, + p_plasma_loss_mw=p_plasma_separatrix_mw, + rmajor=rmajor, + kappa_ipb=kappa_ipb, + aspect=aspect, + afuel=afuel, + ) + iter_ipb98y2 = confine.iter_ipb98y2_confinement_time( + cur_plasma_ma=cur_plasma_ma, + b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, + nd_plasma_electron_line_19=nd_plasma_electron_line_19, + p_plasma_loss_mw=p_plasma_separatrix_mw, + rmajor=rmajor, + kappa_ipb=kappa_ipb, + aspect=aspect, + afuel=afuel, + ) + iter_ipb98y3 = confine.iter_ipb98y3_confinement_time( + cur_plasma_ma=cur_plasma_ma, + b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, + nd_plasma_electron_line_19=nd_plasma_electron_line_19, + p_plasma_loss_mw=p_plasma_separatrix_mw, + rmajor=rmajor, + kappa_ipb=kappa_ipb, + aspect=aspect, + afuel=afuel, + ) + iter_ipb98y4 = confine.iter_ipb98y4_confinement_time( + cur_plasma_ma=cur_plasma_ma, + b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, + nd_plasma_electron_line_19=nd_plasma_electron_line_19, + p_plasma_loss_mw=p_plasma_separatrix_mw, + rmajor=rmajor, + kappa_ipb=kappa_ipb, + aspect=aspect, + afuel=afuel, + ) + petty08 = confine.petty08_confinement_time( + cur_plasma_ma=cur_plasma_ma, + b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, + nd_plasma_electron_line_19=nd_plasma_electron_line_19, + p_plasma_loss_mw=p_plasma_separatrix_mw, + rmajor=rmajor, + kappa_ipb=kappa_ipb, + aspect=aspect, + ) + menard_nstx = confine.menard_nstx_confinement_time( + cur_plasma_ma=cur_plasma_ma, + b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, + nd_plasma_electron_line_19=nd_plasma_electron_line_19, + p_plasma_loss_mw=p_plasma_separatrix_mw, + rmajor=rmajor, + kappa_ipb=kappa_ipb, + aspect=aspect, + afuel=afuel, + ) + menard_nstx_petty08 = confine.menard_nstx_petty08_hybrid_confinement_time( + cur_plasma_ma=cur_plasma_ma, + b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, + nd_plasma_electron_line_19=nd_plasma_electron_line_19, + p_plasma_loss_mw=p_plasma_separatrix_mw, + rmajor=rmajor, + kappa_ipb=kappa_ipb, + aspect=aspect, + afuel=afuel, + ) + itpa20 = confine.itpa20_confinement_time( + cur_plasma_ma=cur_plasma_ma, + b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, + nd_plasma_electron_line_19=nd_plasma_electron_line_19, + p_plasma_loss_mw=p_plasma_separatrix_mw, + rmajor=rmajor, + triang=triang, + kappa_ipb=kappa_ipb, + eps=(1 / aspect), + aion=m_ions_total_amu, + ) + itpa20_ilc = confine.itpa20_il_confinement_time( + cur_plasma_ma=cur_plasma_ma, + b_plasma_toroidal_on_axis=b_plasma_toroidal_on_axis, + p_plasma_loss_mw=p_plasma_separatrix_mw, + nd_plasma_electron_line_19=nd_plasma_electron_line_19, + aion=m_ions_total_amu, + rmajor=rmajor, + triang=triang, + kappa_ipb=kappa_ipb, + ) + + # Data for the box plot + data = { + rf"{ConfinementTimeModel.ITER_89P.full_name}": iter_89p, + rf"{ConfinementTimeModel.ITER_89_0.full_name}": iter_89_0, + rf"{ConfinementTimeModel.ITER_H90_P.full_name}": iter_h90_p, + rf"{ConfinementTimeModel.ITER_H90_P_AMENDED.full_name}": (iter_h90_p_amended), + rf"{ConfinementTimeModel.ITER_93H.full_name}": iter_93h, + rf"{ConfinementTimeModel.ITER_H97P.full_name}": iter_h97p, + rf"{ConfinementTimeModel.ITER_H97P_ELMY.full_name}": iter_h97p_elmy, + rf"{ConfinementTimeModel.ITER_96P.full_name}": iter_96p, + rf"{ConfinementTimeModel.ITER_PB98P_Y.full_name}": iter_pb98py, + rf"{ConfinementTimeModel.IPB98_Y.full_name}": iter_ipb98y, + rf"{ConfinementTimeModel.ITER_IPB98Y1.full_name}": iter_ipb98y1, + rf"{ConfinementTimeModel.ITER_IPB98Y2.full_name}": iter_ipb98y2, + rf"{ConfinementTimeModel.ITER_IPB98Y3.full_name}": iter_ipb98y3, + rf"{ConfinementTimeModel.ITER_IPB98Y4.full_name}": iter_ipb98y4, + rf"{ConfinementTimeModel.PETTY08.full_name}": petty08, + rf"{ConfinementTimeModel.MENARD_NSTX.full_name}": menard_nstx, + rf"{ConfinementTimeModel.MENARD_NSTX_PETTY08_HYBRID.full_name}": ( + menard_nstx_petty08 + ), + rf"{ConfinementTimeModel.ITPA20.full_name}": itpa20, + rf"{ConfinementTimeModel.ITPA20_IL.full_name}": itpa20_ilc, + } + data_values = list(data.values()) + + # Create the violin plot + axis.violinplot(data_values, showextrema=False) + + # Create the box plot + axis.boxplot(data_values, showfliers=True, showmeans=True, meanline=True, widths=0.3) + + # Scatter plot for each data point + # Use a set of distinct colors for better differentiation + distinct_colors = [ + "#1f77b4", # blue + "#ff7f0e", # orange + "#2ca02c", # green + "#d62728", # red + "#9467bd", # purple + "#8c564b", # brown + "#e377c2", # pink + "#7f7f7f", # gray + "#bcbd22", # olive + "#17becf", # cyan + "#aec7e8", # light blue + "#ffbb78", # light orange + "#98df8a", # light green + "#ff9896", # light red + "#c5b0d5", # light purple + "#c49c94", # light brown + "#f7b6d2", # light pink + "#c7c7c7", # light gray + "#dbdb8d", # light olive + "#9edae5", # light cyan + ] + generator = np.random.default_rng(seed=u_seed) + x_values = generator.normal(loc=1, scale=0.035, size=len(data.values())) + for index, (key, value) in enumerate(data.items()): + if "Hubbard" in key and "2017" not in key: + color = "#800080" # strong purple + else: + color = distinct_colors[index % len(distinct_colors)] + axis.scatter( + x_values[index], + value, + color=color, + label=key, + alpha=1.0, + edgecolor="black", + linewidth=0.7, + ) + axis.legend(loc="upper left", bbox_to_anchor=(-1.3, 0.75), ncol=2) + + # Calculate average, standard deviation, and median + avg_threshold = np.mean(data_values) + std_threshold = np.std(data_values) + median_threshold = np.median(data_values) + + # Plot average, standard deviation, and median as text + draw_text( + axis, + 0.7, + 1.25, + f"Average: {avg_threshold:.4f} s", + transform=axis.transAxes, + fontsize=9, + ) + draw_text( + axis, + 0.7, + 1.2, + f"Standard Dev: {std_threshold:.4f} s", + transform=axis.transAxes, + fontsize=9, + ) + draw_text( + axis, + 0.7, + 1.15, + f"Median: {median_threshold:.4f} s", + transform=axis.transAxes, + fontsize=9, + ) + draw_text( + axis, + 0.75, + -0.05, + r"$H \ factor = 1.0$", + transform=axis.transAxes, + fontsize=9, + ) + + axis.set_title("Confinement time ($\\tau_{\\text{E}}$) Comparison") + axis.set_ylabel("Confinement time, $\\tau_{\\text{E}}$ [s]") + axis.set_xlim(0.5, 1.5) + axis.set_xticks([]) + axis.set_xticklabels([]) + + # Add background color + axis.set_facecolor("#f0f0f0") + + +__all__ = [ + "plot_brunner_divertor_power_split_comparison_stackplot", + "plot_confinement_time_comparison", + "plot_sol_power_decay_length_comparison", +] diff --git a/process/core/io/plot/summary/plasma/current_drive.py b/process/core/io/plot/summary/plasma/current_drive.py new file mode 100644 index 0000000000..eb2f8581c6 --- /dev/null +++ b/process/core/io/plot/summary/plasma/current_drive.py @@ -0,0 +1,291 @@ +"""Plasma functions for PROCESS summary plots.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import matplotlib.pyplot as plt +import numpy as np + +from process.core.io.plot.summary.common import ( + setup_axis, +) +from process.core.io.plot.summary.rendering import ( + draw_text, +) +from process.core.io.plot.summary.reporting import ( + plot_info, +) + +if TYPE_CHECKING: + from process.core.io.mfile import MFile + + +def plot_current_drive_info(axis: plt.Axes, mfile: MFile, scan: int): + """Function to plot current drive info + + Parameters + ---------- + axis : + axis object to plot to + mfile : + MFILE + scan : + scan number to use + """ + setup_axis(axis, xmin=0, xmax=1, ymin=-16, ymax=1) + + i_hcd_primary = mfile.get("i_hcd_primary", scan=scan) + + if nbi := (i_hcd_primary in {5, 8}): + draw_text( + axis, + -0.05, + 1, + "Neutral Beam Current Drive:", + ha="left", + va="center", + ) + if ecrh := (i_hcd_primary in {3, 7, 10, 11, 13}): + draw_text( + axis, + -0.05, + 1, + "Electron Cyclotron Current Drive:", + ha="left", + va="center", + ) + if ebw := (i_hcd_primary == 12): + draw_text( + axis, + -0.05, + 1, + "Electron Bernstein Wave Drive:", + ha="left", + va="center", + ) + if lhcd := (i_hcd_primary in {1, 4, 6}): + draw_text( + axis, + -0.05, + 1, + "Lower Hybrid Current Drive:", + ha="left", + va="center", + ) + if iccd := (i_hcd_primary == 2): + draw_text( + axis, + -0.05, + 1, + "Ion Cyclotron Current Drive:", + ha="left", + va="center", + ) + + i_hcd_secondary = mfile.get("i_hcd_secondary", scan=scan) or 0 + if i_hcd_secondary in {5, 8}: + secondary_heating = "NBI" + elif i_hcd_secondary in {3, 7, 10, 11, 13}: + secondary_heating = "ECH" + elif i_hcd_secondary == 12: + secondary_heating = "EBW" + elif i_hcd_secondary in {1, 4, 6}: + secondary_heating = "LHCD" + elif i_hcd_secondary == 2: + secondary_heating = "ICCD" + else: + secondary_heating = "" + + pinjie = mfile.get("p_hcd_injected_total_mw", scan=scan) + p_plasma_separatrix_mw = mfile.get("p_plasma_separatrix_mw", scan=scan) + pdivr = p_plasma_separatrix_mw / mfile.get("rmajor", scan=scan) + + if mfile.get("i_hcd_secondary", scan=scan) != 0: + pinjmwfix = mfile.get("pinjmwfix", scan=scan) + + pdivnr = ( + 1.0e20 + * mfile.get("p_plasma_separatrix_mw", scan=scan) + / ( + mfile.get("rmajor", scan=scan) + * mfile.get("nd_plasma_electrons_vol_avg", scan=scan) + ) + ) + + # Assume Martin scaling if pthresh is not printed + # Accounts for pthresh not being written prior to issue #679 and #680 + pthresh_name = ( + "p_l_h_threshold_mw" + if "p_l_h_threshold_mw" in mfile.data + else "l_h_threshold_powers(6)" + ) + pthresh = mfile.get(pthresh_name, scan=scan) + flh = p_plasma_separatrix_mw / pthresh + + hstar = mfile.get("hstar", scan=scan) + + data = [ + (pinjie, "Steady state auxiliary power", "MW"), + ("p_hcd_primary_extra_heat_mw", "Power for heating only", "MW"), + ("f_c_plasma_bootstrap", "Bootstrap fraction", ""), + ("f_c_plasma_auxiliary", "Auxiliary fraction", ""), + ("f_c_plasma_inductive", "Inductive fraction", ""), + ("p_plasma_loss_mw", "Plasma heating used for H factor", "MW"), + (pdivr, r"$\frac{P_{\mathrm{div}}}{R_{0}}$", "MW m$^{-1}$"), + ( + pdivnr, + r"$\frac{P_{\mathrm{div}}}{\langle n \rangle R_{0}}$", + r"$\times 10^{-20}$ MW m$^{2}$", + ), + (flh, r"$\frac{P_{\mathrm{div}}}{P_{\mathrm{LH}}}$", ""), + (hstar, "H* (non-rad. corr.)", ""), + ] + # Optional override based on condition + field_overrides = { + "ecrh": ( + "eta_cd_hcd_primary", + r"$\frac{P_{\mathrm{div}}}{R_{0}}$", + "A W$^{-1}$", + ), + "nbi": ( + ("gamnb", "NB gamma", "$10^{20}$ A W$^{-1}$ m$^{-2}$"), + ("e_beam_kev", "NB energy", "keV"), + ), + "ebw": ( + "eta_cd_norm_hcd_primary", + "Normalised current drive efficiency of primary HCD system", + "(10$^{20}$ A/(Wm$^{2}$))", + ), + "lhcd": ( + "eta_cd_norm_hcd_primary", + "Normalised current drive efficiency", + "(10$^{20}$ A/(Wm$^{2}$))", + ), + "iccd": ( + "eta_cd_norm_hcd_primary", + "Normalised current drive efficiency", + "(10$^{20}$ A/(Wm$^{2}$))", + ), + } + + if ecrh: + data.insert(6, field_overrides["ecrh"]) + elif nbi: + data.insert(6, field_overrides["nbi"][0]) + data.insert(7, field_overrides["nbi"][1]) + elif ebw: + data.insert(6, field_overrides["ebw"]) + elif lhcd: + data.insert(6, field_overrides["lhcd"]) + elif iccd: + data.insert(6, field_overrides["iccd"]) + + # Secondary heating logic — common across all cases + if mfile.get("i_hcd_secondary", scan=scan) != 0: + data.insert( + 1, + ( + "pinjmwfix", + f"{secondary_heating} secondary auxiliary power", + "MW", + ), + ) + data[0] = ((pinjie - pinjmwfix), "Primary auxiliary power", "MW") + data.insert(2, (pinjie, "Total auxillary power", "MW")) + + coe = mfile.get("coe", scan=scan) + data.extend(( + ("", "", ""), + ("#Costs", "", ""), + ( + ("", "Cost output not selected", "") + if coe == 0.0 # noqa: RUF069 + else (coe, "Cost of electricity", r"\$/MWh") + ), + )) + + plot_info(axis, data, mfile, scan) + + +def plot_bootstrap_comparison(axis: plt.Axes, mfile: MFile, scan: int): + """Function to plot a scatter box plot of bootstrap current fractions. + + Parameters + ---------- + axis : + axis object to plot to + mfile : + MFILE data object + scan : + scan number to use + """ + # Data for the box plot + data = { + "IPDG": mfile.get("f_c_plasma_bootstrap_iter89", scan=scan), + "Sauter": mfile.get("f_c_plasma_bootstrap_sauter", scan=scan), + "Nevins": mfile.get("f_c_plasma_bootstrap_nevins", scan=scan), + "Wilson": mfile.get("f_c_plasma_bootstrap_wilson", scan=scan), + "Sakai": mfile.get("f_c_plasma_bootstrap_sakai", scan=scan), + "ARIES": mfile.get("f_c_plasma_bootstrap_aries", scan=scan), + "Andrade": mfile.get("f_c_plasma_bootstrap_andrade", scan=scan), + "Hoang": mfile.get("f_c_plasma_bootstrap_hoang", scan=scan), + "Wong": mfile.get("f_c_plasma_bootstrap_wong", scan=scan), + "Gi-I": mfile.get("bscf_gi_i", scan=scan), + "Gi-II": mfile.get("bscf_gi_ii", scan=scan), + "Sugiyama (L-mode)": mfile.get("f_c_plasma_bootstrap_sugiyama_l", scan=scan), + "Sugiyama (H-mode)": mfile.get("f_c_plasma_bootstrap_sugiyama_h", scan=scan), + } + # Create the violin plot + data_values = list(data.values()) + axis.violinplot(data_values, showextrema=False) + + # Create the box plot + axis.boxplot(data_values, showfliers=True, showmeans=True, meanline=True, widths=0.3) + + # Scatter plot for each data point + colors = plt.cm.plasma(np.linspace(0, 1, len(data.values()))) + for index, (key, value) in enumerate(data.items()): + axis.scatter(1, value, color=colors[index], label=key, alpha=1.0) + axis.legend(loc="upper left", bbox_to_anchor=(1, 1)) + + # Calculate average, standard deviation, and median + avg_bootstrap = np.mean(data_values) + std_bootstrap = np.std(data_values) + median_bootstrap = np.median(data_values) + + # Plot average, standard deviation, and median as text + draw_text( + axis, + 1.02, + 0.2, + f"Average: {avg_bootstrap:.4f}", + transform=axis.transAxes, + fontsize=9, + ) + draw_text( + axis, + 1.02, + 0.15, + f"Standard Dev: {std_bootstrap:.4f}", + transform=axis.transAxes, + fontsize=9, + ) + draw_text( + axis, + 1.02, + 0.1, + f"Median: {median_bootstrap:.4f}", + transform=axis.transAxes, + fontsize=9, + ) + + axis.set_title("Bootstrap Current Fraction ($f_\\text{BS}$) Comparison") + axis.set_ylabel("Bootstrap Current Fraction") + axis.set_xlim(0.5, 1.5) + axis.set_xticks([]) + axis.set_xticklabels([]) + axis.set_facecolor("#f0f0f0") + + +__all__ = ["plot_bootstrap_comparison", "plot_current_drive_info"] diff --git a/process/core/io/plot/summary/plasma/overview.py b/process/core/io/plot/summary/plasma/overview.py new file mode 100644 index 0000000000..948b18d5ba --- /dev/null +++ b/process/core/io/plot/summary/plasma/overview.py @@ -0,0 +1,1101 @@ +"""Plasma functions for PROCESS summary plots.""" + +from __future__ import annotations + +import textwrap +from typing import TYPE_CHECKING, Literal, TypedDict + +import matplotlib.pyplot as plt + +from process.core.io.plot.summary.common import ( + box_style, + load_plot_image, +) +from process.core.io.plot.summary.constants import ( + white_box, +) +from process.core.io.plot.summary.plasma.physics import ( + plot_plasma, +) +from process.core.io.plot.summary.rendering import ( + draw_annotation, + draw_text, +) +from process.models.geometry.plasma import plasma_geometry +from process.models.physics.bootstrap_current import ( + BootstrapCurrentFractionModel, +) +from process.models.physics.current_drive import CurrentDriveModel +from process.models.physics.density_limit import DensityLimitModel +from process.models.physics.l_h_transition import ( + PlasmaConfinementTransitionModel, +) +from process.models.physics.physics import ( + BetaComponentLimits, + BetaNormMaxModel, + IndInternalNormModel, +) +from process.models.physics.plasma_current import ( + PlasmaCurrentModel, + PlasmaDiamagneticCurrentModel, +) +from process.models.physics.plasma_geometry import ( + PlasmaGeometryModelType, +) + +if TYPE_CHECKING: + from matplotlib.transforms import Transform + + from process.core.io.mfile import MFile + + +def plot_main_plasma_information( + axis: plt.Axes, + mfile: MFile, + scan: int, + colour_scheme: Literal[1, 2], + fig: plt.Figure, +): + """Plots the main plasma information including plasma shape, geometry, currents, + heating, + confinement, and other relevant plasma parameters. + + Parameters + ---------- + axis : plt.Axes + The matplotlib axis object to plot on. + mfile : MFile + The MFILE data object containing plasma parameters. + scan : int + The scan number to use for extracting data. + colour_scheme : int + The colour scheme to use for plots. + fig : plt.Figure + The matplotlib figure object for additional annotations. + """ + # Import key variables + triang = mfile.get("triang", scan=scan) + kappa = mfile.get("kappa", scan=scan) + + # Remove the axes + axis.axis("off") + + # Plot the main plasma shape + plot_plasma(axis, mfile, scan, colour_scheme) + + rmajor = mfile.get("rmajor", scan=scan) + rminor = mfile.get("rminor", scan=scan) + # Get the plasma permieter points for the core plasma region + pg = plasma_geometry( + rmajor=rmajor, + rminor=mfile.get("rminor", scan=scan) + * mfile.get("radius_plasma_core_norm", scan=scan), + triang=mfile.get("triang", scan=scan), + kappa=mfile.get("kappa", scan=scan), + i_single_null=mfile.get("i_single_null", scan=scan), + i_plasma_shape=1, + square=mfile.get("plasma_square", scan=scan), + ) + # Plot the core plasma boundary line + axis.plot(pg.rs, pg.zs, color="black", linestyle="--") + + # Plot the centre of the plasma + axis.plot(rmajor, 0, "r+", markersize=20, markeredgewidth=2) + + # Add Q plasma information box + draw_text( + axis, + 0.725, + 0.175, + f"$Q_{{\\text{{plasma}}}}$: {mfile.get('big_q_plasma', scan=scan):.2f}", + fontsize=15, + verticalalignment="center", + horizontalalignment="center", + bbox=white_box, + transform=fig.transFigure, + ) + + # ========================================= + + # Draw a double-ended arrow from the inner plasma edge to the center + draw_annotation( + axis, + "", + xy=(rmajor - rminor, 0), # Inner plasma edge + xytext=(rmajor, 0), # Center + arrowprops={"arrowstyle": "<->", "color": "black"}, + ) + + # Add a label for the minor radius + draw_text( + axis, + rmajor - rminor / 2, + -rminor * kappa * 0.08, + f"$a$: {rminor:.2f} m", + fontsize=9, + color="black", + ha="center", + bbox=white_box, + ) + + # ============================================ + + # Draw a single-ended arrow from the machien centre to the plasma center + draw_annotation( + axis, + "", + xy=(axis.get_xlim()[0], -rminor * 0.3 * kappa), # Inner plasma edge + xytext=(rmajor, -rminor * 0.3 * kappa), # Center + arrowprops={"arrowstyle": "<-", "color": "black"}, + ) + + # Add a label for the major radius + draw_text( + axis, + rmajor - rminor / 2, + -rminor * kappa * 0.25, + f"$R_0$: {rmajor:.2f} m", + fontsize=9, + color="black", + ha="center", + bbox=white_box, + ) + + # ============================================ + + # Draw a double-ended arrow from the xpoint to the center to show elongation + draw_annotation( + axis, + "", + xy=(rmajor - rminor * triang, kappa * rminor), # Inner plasma edge + xytext=(rmajor - rminor * triang, 0), # Center + arrowprops={"arrowstyle": "<->", "color": "black"}, + ) + + # Write the elongation beside the vertical line, position relative to figure axes + draw_text( + axis, + 0.3, + 0.75, + f"$\\kappa$: {mfile.get('kappa', scan=scan):.2f}", + fontsize=9, + color="black", + rotation=270, + verticalalignment="center", + transform=axis.transAxes, + bbox=white_box, + ) + + # ============================================= + + # Draw a double-ended arrow from the inner plasma edge to the center + draw_annotation( + axis, + "", + xy=( + rmajor - rminor * triang, + kappa * rminor * 0.25, + ), # Inner plasma edge + xytext=(rmajor, kappa * rminor * 0.25), # Center + arrowprops={"arrowstyle": "<->", "color": "black"}, + ) + + # Write the triangularity to the left of the cross, position relative to figure axes + draw_text( + axis, + rmajor - (rminor * triang * 0.75), + kappa * rminor * 0.3, + f"$\\delta$: {mfile.get('triang', scan=scan):.2f}", + fontsize=9, + color="black", + rotation=0, + verticalalignment="center", + bbox=white_box, + ) + + # ============================================= + + radius_plasma_core_norm = mfile.get("radius_plasma_core_norm", scan=scan) + + # Draw a double-ended arrow for the plasma core region + draw_annotation( + axis, + "", + xy=(rmajor, -rminor * 0.1 * kappa), # Inner plasma edge + xytext=( + rmajor + (rminor * radius_plasma_core_norm), + -rminor * 0.1 * kappa, + ), + arrowprops={"arrowstyle": "<->", "color": "black"}, + ) + # Add a label for core region + draw_text( + axis, + rmajor + (rminor * radius_plasma_core_norm / 4), + -rminor * kappa * 0.15, + f"$\\rho_{{\\text{{core}}}}$: {radius_plasma_core_norm:.2f}", + fontsize=9, + color="black", + rotation=0, + verticalalignment="center", + bbox=white_box, + ) + + # ================================================ + + # Add plasma volume, areas and shaping information + + geom_type = PlasmaGeometryModelType(mfile.get("i_plasma_geometry", scan=scan)) + + textstr_plasma = ( + "$\\mathbf{Shaping:}$\n\n$\\kappa_{95}$:" + f" {mfile.get('kappa95', scan=scan):.2f}" + f" ({geom_type.kappa95_model.description}) | $\\delta_{{95}}$:" + f" {mfile.get('triang95', scan=scan):.2f}" + f" ({geom_type.triang95_model.description}) | $\\zeta$:" + f" {mfile.get('plasma_square', scan=scan):.2f}\n$\\kappa$:" + f" {mfile.get('kappa', scan=scan):.2f}" + f" ({geom_type.kappa_model.description}) | $\\delta$:" + f" {mfile.get('triang', scan=scan):.2f}" + f" ({geom_type.triang_model.description}) | A:" + f" {mfile.get('aspect', scan=scan):.2f}\n$ V_{{\\text{{p}}}}:$" + f" {mfile.get('vol_plasma', scan=scan):,.2f}$ \\ \\text{{m}}^3$ | $" + " A_{\\text{p,surface}}:$" + f" {mfile.get('a_plasma_surface', scan=scan):,.2f}$ \\ \\text{{m}}^2$" + " | $ A_{\\text{p,poloidal}}:$" + f" {mfile.get('a_plasma_poloidal', scan=scan):,.3f}$ \\" + " \\text{m}^2$\n$ L_{\\text{p,poloidal}}:$" + f" {mfile.get('len_plasma_poloidal', scan=scan):,.3f}$ \\ \\text{{m}}$" + ) + + draw_text( + axis, + 0.365, + 0.975, + textstr_plasma, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=box_style("lightyellow"), + ) + + # ============================================ + + # Draw a red arrow coming from the right and pointing at the plasma + for kap in (-kappa, kappa): + draw_annotation( + axis, + "", + # Pointing at plasma + xy=(rmajor + (rminor * 0.8), kap * rminor * 0.2), + # Starting point of arrow + xytext=(rmajor + (rminor * 1.4), kap * rminor * 0.2), + arrowprops={"facecolor": "red", "edgecolor": "red", "lw": 2}, + ) + + i_hcd_primary = mfile.get("i_hcd_primary", scan=scan) + i_hcd_secondary = mfile.get("i_hcd_secondary", scan=scan) + + # Add heating and current drive information + textstr_hcd = ( + "$\\mathbf{Heating \\ & \\ current \\ drive:}$\n\nTotal injected" + f" heat: {mfile.get('p_hcd_injected_total_mw', scan=scan):.3f}" + " MW\nOhmic heating power:" + f" {mfile.get('p_plasma_ohmic_mw', scan=scan):.3f}" + " MW\n\n$\\mathbf{Primary \\ system:" + f" {CurrentDriveModel(i_hcd_primary).abbreviation}}}$\nCurrent driving" + f" power {mfile.get('p_hcd_primary_injected_mw', scan=scan):.4f}" + " MW\nExtra heat power:" + f" {mfile.get('p_hcd_primary_extra_heat_mw', scan=scan):.4f}" + " MW\n$\\eta_{\\text{CD,prim}}$:" + f" {mfile.get('eta_cd_hcd_primary', scan=scan):.4f} A/W | " + " $\\langle\\zeta_{\\text{CD,prim}}\\rangle$:" + f" {mfile.get('eta_cd_dimensionless_hcd_primary', scan=scan):.4f}\n$\\gamma_{{\\text{{CD,prim}}}}$:" # noqa: E501 + f" {mfile.get('eta_cd_norm_hcd_primary', scan=scan):.4f} $\\times" + " 10^{20} \\mathrm{A} / \\mathrm{Wm}^2$\nCurrent driven by" + f" primary: {mfile.get('c_hcd_primary_driven', scan=scan) / 1e6:.3f}" + " MA\n\n$\\mathbf{Secondary \\ system:" + f" {CurrentDriveModel(i_hcd_secondary).abbreviation}}}$\nCurrent" + " driving power" + f" {mfile.get('p_hcd_secondary_injected_mw', scan=scan):.4f} MW\nExtra" + " heat power:" + f" {mfile.get('p_hcd_secondary_extra_heat_mw', scan=scan):.4f}" + " MW\n$\\eta_{\\text{CD,sec}}$:" + f" {mfile.get('eta_cd_hcd_secondary', scan=scan):.4f} A/W | " + " $\\langle\\zeta_{\\text{CD,sec}}\\rangle$:" + f" {mfile.get('eta_cd_dimensionless_hcd_secondary', scan=scan):.4f}\n$\\gamma_{{\\text{{CD,sec}}}}$:" # noqa: E501 + f" {mfile.get('eta_cd_norm_hcd_secondary', scan=scan):.4f} $\\times" + " 10^{20} \\mathrm{A} / \\mathrm{Wm}^2$\nCurrent driven by" + " secondary:" + f" {mfile.get('c_hcd_secondary_driven', scan=scan) / 1e6:.3f} MA" + ) + + draw_text( + axis, + 0.73, + 0.675, + textstr_hcd, + fontsize=9, + verticalalignment="top", + transform=plt.gcf().transFigure, + bbox=box_style("paleturquoise") | {"edgecolor": "black"}, + ) + + class TextArgs(TypedDict): + fontsize: int + verticalalignment: str + transform: Transform + + text_args = TextArgs({ + "fontsize": 23, + "verticalalignment": "top", + "transform": fig.transFigure, + }) + + # Add injected power label + draw_text(axis, 0.92, 0.625, "$P_{\\text{inj}}$", **text_args) + + # ================================================ + + # Add beta information + textstr_beta = ( + "$\\mathbf{Beta \\ Information:}$\n\nTotal beta,$ \\ \\langle" + " \\beta \\rangle$:" + f" {mfile.get('beta_total_vol_avg', scan=scan):.4f}\nThermal beta,$ \\" + " \\langle \\beta_{\\text{thermal}} \\rangle$:" + f" {mfile.get('beta_thermal_vol_avg', scan=scan):.4f}\nToroidal beta,$" + " \\ \\langle \\beta_{\\text{t}} \\rangle$:" + f" {mfile.get('beta_toroidal_vol_avg', scan=scan):.4f}\nPoloidal" + " beta,$ \\ \\langle \\beta_{\\text{p}} \\rangle$:" + f" {mfile.get('beta_poloidal_vol_avg', scan=scan):.4f}\nFast-alpha" + " beta,$ \\ \\langle \\beta_{\\alpha} \\rangle$:" + f" {mfile.get('beta_fast_alpha', scan=scan):.4f}\nUpper limit on" + f" {BetaComponentLimits(int(mfile.get('i_beta_component', scan=scan))).full_name}:" # noqa: E501 + " $ \\langle \\beta \\rangle$:" + f" {mfile.get('beta_vol_avg_max', scan=scan):.4f}\nNormalised total" + " beta,$ \\ \\beta_{\\text{N}}$:" + f" {mfile.get('beta_norm_total', scan=scan):.4f}\nNormalised thermal" + " beta,$ \\ \\beta_{\\text{N,thermal}}$:" + f" {mfile.get('beta_norm_thermal', scan=scan):.4f}\nMaximum normalised" + " beta" + f" ({BetaNormMaxModel(int(mfile.get('i_beta_norm_max', scan=scan))).full_name}),$" # noqa: E501 + " \\ \\beta_{\\text{N,max}}$:" + f" {mfile.get('beta_norm_max', scan=scan):.4f}" + ) + + draw_text( + axis, + 0.025, + 0.975, + textstr_beta, + fontsize=9, + verticalalignment="top", + transform=fig.transFigure, + bbox=box_style("lightblue"), + ) + + # Add beta label + draw_text(axis, 0.27, 0.94, "$\\beta$", **text_args) + + # ================================================ + + # Add volt-second information + textstr_volt_second = ( + "$\\mathbf{Volt-second \\ requirements:}$\n\nTotal volt-second" + f" consumption: {mfile.get('vs_plasma_total_required', scan=scan):.4f}" + " Vs\n - Internal volt-seconds:" + f" {mfile.get('vs_plasma_internal', scan=scan):.4f} Vs\n -" + " Volt-seconds needed for burn:" + f" {mfile.get('vs_plasma_burn_required', scan=scan):.4f} Vs\n -" + " Volt-seconds needed for ramp:" + f" {mfile.get('vs_plasma_ramp_required', scan=scan):.4f} Vs |" + " $C_{\\text{ejima}}$:" + f" {mfile.get('ejima_coeff', scan=scan):.4f}\n$V_{{\\text{{loop}}}}$:" + f" {mfile.get('v_plasma_loop_burn', scan=scan):.4f}" + " V\n$\\Omega_{\\text{p}}$:" + f" {mfile.get('res_plasma', scan=scan):.4e} $\\Omega$\nPlasma" + " resistive diffusion time:" + f" {mfile.get('t_plasma_res_diffusion', scan=scan):,.4f} s\nPlasma" + f" inductance: {mfile.get('ind_plasma', scan=scan):.4e} H | ITER" + " $l_i(3)$:" + f" {mfile.get('ind_plasma_internal_norm_iter_3', scan=scan):.4f}\nPlasma" + " stored magnetic energy:" + f" {mfile.get('e_plasma_magnetic_stored', scan=scan) / 1e9:.4f}" + " GJ\nPlasma normalised internal inductance, $l_i$" + f" ({IndInternalNormModel(int(mfile.get('i_ind_plasma_internal_norm', scan=scan))).full_name})" # noqa: E501 + f" :{mfile.get('ind_plasma_internal_norm', scan=scan):.3f}" + ) + + draw_text( + axis, + 0.025, + 0.78, + textstr_volt_second, + fontsize=9, + verticalalignment="top", + transform=fig.transFigure, + bbox=box_style("lightgreen"), + ) + + # Add volt second label + draw_text(axis, 0.30, 0.77, "Vs", **text_args) + + # ========================================= + + # Add divertor information + textstr_div = ( + "\n$P_{\\text{sep}}$:" + f" {mfile.get('p_plasma_separatrix_mw', scan=scan):.2f}" + " MW\n$\\frac{P_{\\text{sep}}}{R}$:" + f" {mfile.get('p_plasma_separatrix_rmajor_mw', scan=scan):.2f}" + " MW/m\n$\\frac{P_{\\text{sep}}B_T}{q_{95} A R}$:" + f" {mfile.get('p_div_bt_q_aspect_rmajor_mw', scan=scan):.2f} MW T/m " + " " + ) + + draw_text( + axis, + 0.35, + 0.12, + textstr_div, + fontsize=9, + verticalalignment="top", + transform=fig.transFigure, + bbox=box_style("orange"), + ) + + # Add divertor label + draw_text(axis, 0.45, 0.1, "$P_{\\text{div}}$", **text_args) + + # ================================================ + + # Add confinement information + textstr_confinement = ( + "$\\mathbf{Confinement:}$\n\nConfinement scaling law:" + f" {mfile.get('tauelaw', scan=scan)}\nConfinement $H$ factor:" + f" {mfile.get('hfact', scan=scan):.4f}\nEnergy confinement time from" + f" scaling: {mfile.get('t_energy_confinement', scan=scan):.4f}" + f" s\nFusion double product: {mfile.get('ntau', scan=scan):.4e}" + f" s/m³\nLawson Triple product: {mfile.get('nttau', scan=scan):.4e}" + " keV·s/m³\nTransport loss power assumed in scaling law:" + f" {mfile.get('p_plasma_loss_mw', scan=scan):.4f} MW\nPlasma thermal" + " energy (inc. $\\alpha$), $W$:" + f" {mfile.get('e_plasma_beta', scan=scan) / 1e9:.4f} GJ\nAlpha" + " particle confinement time:" + f" {mfile.get('t_alpha_confinement', scan=scan):.4f} s |" + " $\\tau_{\\alpha}/\\tau_{e}$:" + f" {mfile.get('f_t_alpha_energy_confinement', scan=scan):.4f}" + ) + + draw_text( + axis, + 0.025, + 0.57, + textstr_confinement, + fontsize=9, + verticalalignment="top", + transform=fig.transFigure, + # Changed to a not normal color (Aquamarine) + bbox=box_style("gainsboro") | {"edgecolor": "black"}, + ) + + # Add tau label + draw_text(axis, 0.3, 0.55, "$\\tau_{\\text{e}} $", **text_args) + + # ========================================= + + # Load the neutron image + alpha_particle = load_plot_image("alpha_particle.png") + + # Display the neutron image over the figure, not the axes + new_ax = axis.inset_axes( + (0.975, 0.275, 0.075, 0.075), transform=axis.transAxes, zorder=10 + ) + new_ax.imshow(alpha_particle) + new_ax.axis("off") + + draw_annotation( + axis, + "", + xy=(rmajor + rminor, -rminor * kappa * 0.55), # Pointing at the plasma + xytext=(rmajor + 0.2 * rminor, -rminor * kappa * 0.25), + arrowprops={"facecolor": "red", "edgecolor": "grey", "lw": 1}, + ) + + textstr_alpha = ( + "$P_{\\alpha,\\text{loss}}$" + f" {mfile.get('p_fw_alpha_mw', scan=scan):.2f}" + " MW\n$f_{\\alpha,\\text{coupled}}$" + f" {mfile.get('f_p_alpha_plasma_deposited', scan=scan):.2f}" + ) + + draw_text( + axis, + 1.0, + 0.275, + textstr_alpha, + fontsize=9, + verticalalignment="top", + transform=axis.transAxes, + bbox={ + "boxstyle": "round", + "facecolor": "red", + "alpha": 1.0, + "linewidth": 2, + }, + ) + + # ========================================= + neutron = load_plot_image("neutron.png") + new_ax = axis.inset_axes( + (0.975, 0.75, 0.075, 0.075), transform=axis.transAxes, zorder=10 + ) + new_ax.imshow(neutron) + new_ax.axis("off") + + # Draw a red arrow coming from the right and pointing at the plasma + draw_annotation( + axis, + "", + xy=(rmajor + rminor, rminor * kappa * 0.65), # Pointing at the plasma + xytext=(rmajor, rminor * kappa * 0.5), + arrowprops={"facecolor": "grey", "edgecolor": "grey", "lw": 1}, + ) + + textstr_neutron = ( + "$P_{\\text{n,total}}$" + f" {mfile.get('p_neutron_total_mw', scan=scan):.2f}" + " MW\n$\\phi_{\\text{n,avg}}$" + f" {mfile.get('pflux_plasma_surface_neutron_avg_mw', scan=scan):.3f}" + " MW/m²" + ) + + draw_text( + axis, + 0.775, + 0.875, + textstr_neutron, + fontsize=9, + verticalalignment="top", + transform=axis.transAxes, + bbox={ + "boxstyle": "round", + "facecolor": "grey", + "alpha": 0.8, + "linewidth": 2, + }, + ) + + # =============================================== + + # Add fusion reaction information + textstr_reactions = ( + "$\\mathbf{Fusion \\ Reactions:}$\n\nFuel mixture:\n| D:" + f" {mfile.get('f_plasma_fuel_deuterium', scan=scan):.2f} | T:" + f" {mfile.get('f_plasma_fuel_tritium', scan=scan):.2f} | 3He:" + f" {mfile.get('f_plasma_fuel_helium3', scan=scan):.2f} |\n\nFusion" + " Power, $P_{\\text{fus}}:$" + f" {mfile.get('p_fusion_total_mw', scan=scan):,.2f} MW\nD-T Power," + " $P_{\\text{fus,DT}}:$" + f" {mfile.get('p_dt_total_mw', scan=scan):,.2f} MW\nD-D Power," + " $P_{\\text{fus,DD}}:$" + f" {mfile.get('p_dd_total_mw', scan=scan):,.2f} MW\nD-3He Power," + " $P_{\\text{fus,D3He}}:$" + f" {mfile.get('p_dhe3_total_mw', scan=scan):,.2f} MW\nAlpha Power," + f" $P_{{\\alpha}}:$ {mfile.get('p_alpha_total_mw', scan=scan):,.2f} MW" + ) + + draw_text( + axis, + 0.025, + 0.4, + textstr_reactions, + fontsize=9, + verticalalignment="top", + transform=fig.transFigure, + bbox={ + "boxstyle": "round", + "facecolor": "red", + "alpha": 0.6, + "linewidth": 2, + }, + ) + + # ================================================ + + # Add fuelling information + textstr_fuelling = ( + "$\\mathbf{Fuelling:}$\n\nPlasma mass:" + f" {mfile.get('m_plasma', scan=scan) * 1000:.4f} g\n - Average mass" + f" of all plasma ions: {mfile.get('m_ions_total_amu', scan=scan):.3f}" + " amu\nFuel mass:" + f" {mfile.get('m_plasma_fuel_ions', scan=scan) * 1000:.4f} g\n -" + " Average mass of all fuel ions:" + f" {mfile.get('m_fuel_amu', scan=scan):.3f} amu\n\nFueling rate:" + f" {mfile.get('molflow_plasma_fuelling_required', scan=scan):.3e}" + " nucleus-pairs/s\nFuel burn-up rate:" + f" {mfile.get('rndfuel', scan=scan):.3e} reactions/s\nBurn-up" + f" fraction: {mfile.get('burnup', scan=scan):.4f}\n" + ) + + draw_text( + axis, + 0.025, + 0.22, + textstr_fuelling, + fontsize=9, + verticalalignment="top", + transform=fig.transFigure, + bbox=box_style("khaki") | {"edgecolor": "black"}, + ) + + # ================================================ + + # Add ion density information + textstr_ions = ( + " $\\mathbf{Ion \\ to \\ electron}$\n " + " $\\mathbf{relative \\ number}$\n " + " $\\mathbf{densities:}$\n\n Effective charge:" + f" {mfile.get('n_charge_plasma_effective_vol_avg', scan=scan):.3f}\n\n" + " H: " + f" {mfile.get('f_nd_impurity_electrons(01)', scan=scan):.4e}\n " + " He: " + f" {mfile.get('f_nd_impurity_electrons(02)', scan=scan):.4e}\n " + " Be: " + f" {mfile.get('f_nd_impurity_electrons(03)', scan=scan):.4e}\n " + " C: " + f" {mfile.get('f_nd_impurity_electrons(04)', scan=scan):.4e}\n " + " N: " + f" {mfile.get('f_nd_impurity_electrons(05)', scan=scan):.4e}\n " + " O: " + f" {mfile.get('f_nd_impurity_electrons(06)', scan=scan):.4e}\n " + " Ne: " + f" {mfile.get('f_nd_impurity_electrons(07)', scan=scan):.4e}\n " + " Si: " + f" {mfile.get('f_nd_impurity_electrons(08)', scan=scan):.4e}\n " + " Ar: " + f" {mfile.get('f_nd_impurity_electrons(09)', scan=scan):.4e}\n " + " Fe: " + f" {mfile.get('f_nd_impurity_electrons(10)', scan=scan):.4e}\n " + " Ni: " + f" {mfile.get('f_nd_impurity_electrons(11)', scan=scan):.4e}\n " + " Kr: " + f" {mfile.get('f_nd_impurity_electrons(12)', scan=scan):.4e}\n " + " Xe: " + f" {mfile.get('f_nd_impurity_electrons(13)', scan=scan):.4e}\n " + f" W: {mfile.get('f_nd_impurity_electrons(14)', scan=scan):.4e}" + ) + + draw_text( + axis, + 0.805, + 0.335, + textstr_ions, + fontsize=9, + verticalalignment="top", + transform=fig.transFigure, + bbox={ + "boxstyle": "round", + "facecolor": "olivedrab", + "alpha": 0.7, + "linewidth": 2, + }, + ) + + # Add ion charge label + draw_text(axis, 0.815, 0.29, "$Z$", **text_args) + + # ================================================ + + # Add plasma current information + textstr_currents = ( + "$\\mathbf{Plasma\\ currents:}$\n\nPlasma current" + f" ({PlasmaCurrentModel(int(mfile.get('i_plasma_current', scan=scan))).full_name}):" # noqa: E501 + f" {mfile.get('plasma_current_ma', scan=scan):.4f} MA\n - Bootstrap" + " fraction" + f" ({BootstrapCurrentFractionModel(int(mfile.get('i_bootstrap_current', scan=scan))).full_name}):" # noqa: E501 + f" {mfile.get('f_c_plasma_bootstrap', scan=scan):.4f}\n - Diamagnetic" + " fraction" + f" ({PlasmaDiamagneticCurrentModel(int(mfile.get('i_diamagnetic_current', scan=scan))).full_name}):" # noqa: E501 + f" {mfile.get('f_c_plasma_diamagnetic', scan=scan):.4f}\n -" + " Pfirsch-Schlüter fraction" + f" {mfile.get('f_c_plasma_pfirsch_schluter', scan=scan):.4f}\n -" + " Auxiliary fraction" + f" {mfile.get('f_c_plasma_auxiliary', scan=scan):.4f}\n - Inductive" + f" fraction {mfile.get('f_c_plasma_inductive', scan=scan):.4f}" + ) + + draw_text( + axis, + 0.72, + 0.975, + textstr_currents, + fontsize=9, + verticalalignment="top", + transform=fig.transFigure, + bbox=box_style("#C8A2C8"), # Hex code for lilac color + ) + + # Add plasma current label + draw_text(axis, 0.93, 0.9, "$I_{\\text{p}} $", **text_args) + + # Add magnetic field information + textstr_fields = ( + "$\\mathbf{Magnetic\\ fields:}$\n\nToroidal field at $R_0$," + f" $B_{{T}}$: {mfile.get('b_plasma_toroidal_on_axis', scan=scan):.4f}" + " T\n Ripple at outboard , $\\delta$:" + f" {mfile.get('ripple_b_tf_plasma_edge', scan=scan):.2f}%\nSurface" + " average poloidal field, $\\langle B_{p}(a) \\rangle$:" + f" {mfile.get('b_plasma_surface_poloidal_average', scan=scan):.4f}" + " T\nTotal field, $B_{tot}$:" + f" {mfile.get('b_plasma_total', scan=scan):.4f} T\nVertical field," + " $B_{vert}$:" + f" {mfile.get('b_plasma_vertical_required', scan=scan):.4f} T" + ) + + draw_text( + axis, + 0.5325, + 0.14, + textstr_fields, + fontsize=9, + verticalalignment="top", + transform=fig.transFigure, + bbox=box_style("royalblue"), + ) + + # Add magnetic field label + draw_text(axis, 0.75, 0.12, "$B$", **text_args) + + # Add radiation information + textstr_radiation = ( + " $\\mathbf{Radiation:}$\n\n Total radiation" + f" power {mfile.get('p_plasma_rad_mw', scan=scan):.4f} MW\n " + " Separatrix radiation fraction" + f" {mfile.get('f_p_plasma_separatrix_rad', scan=scan):.4f}\n " + " Core radiation power" + f" {mfile.get('p_plasma_inner_rad_mw', scan=scan):.4f} MW\n " + " - $f_{\\text{core,reduce}}$" + f" {mfile.get('f_p_plasma_core_rad_reduction', scan=scan):.4f}\n " + " Edge radiation power" + f" {mfile.get('p_plasma_outer_rad_mw', scan=scan):.4f} MW\n " + " Synchrotron radiation power" + f" {mfile.get('p_plasma_sync_mw', scan=scan):.4f} MW\n " + " Synchrotron wall reflectivity" + f" {mfile.get('f_sync_reflect', scan=scan):.4f}" + ) + + draw_text( + axis, + 0.72, + 0.83, + textstr_radiation, + fontsize=9, + verticalalignment="top", + transform=fig.transFigure, + bbox=box_style("lavender") | {"edgecolor": "black"}, + ) + + # Add radiation label + draw_text(axis, 0.725, 0.78, "$\\gamma$", **text_args) + + # Add L-H threshold information + model_name = PlasmaConfinementTransitionModel( + int(mfile.get("i_l_h_threshold", scan=scan)) + ).full_name + + # Wrap long model names to new line + if len(model_name) > 20: + model_name = "\n".join(textwrap.wrap(model_name, width=20)) + + textstr_lh = ( + "$\\mathbf{L-H \\" + f" threshold:}}$\n{model_name}\n\n$P_{{\\text{{L-H}}}}:$" + f" {mfile.get('p_l_h_threshold_mw', scan=scan):.4f} MW" + ) + + draw_text( + axis, + 0.22, + 0.4, + textstr_lh, + fontsize=9, + verticalalignment="top", + transform=fig.transFigure, + bbox=box_style("peachpuff"), + ) + + # Add density limit information + textstr_density_limit = ( + "$\\mathbf{Density \\" + f" limit:}}$\n({DensityLimitModel(int(mfile.get('i_density_limit', scan=scan))).full_name})\n$n_{{\\text{{e,limit}}}}:" # noqa: E501 + f" {mfile.get('nd_plasma_electrons_max', scan=scan):.3e} \\" + " m^{-3}$\n$f_{\\text{GW}}$:" + f" {mfile.get('f_nd_plasma_greenwald', scan=scan):.4f}" + ) + + draw_text( + axis, + 0.22, + 0.31, + textstr_density_limit, + fontsize=9, + verticalalignment="top", + transform=fig.transFigure, + bbox=box_style("pink"), + ) + + +def plot_detailed_plasma_parameters(axis: plt.Axes, fig, mfile: MFile, scan: int): + """Function to plot detailed plasma parameters from physics data. + + Parameters + ---------- + axis : plt.Axes + Axis object to plot to + fig : plt.Figure + Figure object for text placement + mfile : MFile + MFILE data object + scan : int + Scan number to use + """ + textstr_debye = ( + "$\\mathbf{Debye \\" + " Lengths:}$\n\n$\\langle\\lambda_{Debye,e}\\rangle$:" + f" {mfile.get('len_plasma_debye_electron_vol_avg', scan=scan):.4e} m" + ) + + textstr_larmor = ( + "$\\mathbf{Larmor \\ Radii:}$\n\n" + "$\\langle\\rho_{Larmor,toroidal,D}\\rangle$:" + f" {mfile.get('radius_plasma_deuteron_toroidal_larmor_isotropic_vol_avg', scan=scan):.4e} m\n" # noqa: E501 + "$\\langle\\rho_{Larmor,toroidal,T}\\rangle$:" + f" {mfile.get('radius_plasma_triton_toroidal_larmor_isotropic_vol_avg', scan=scan):.4e} m" # noqa: E501 + ) + + textstr_velocities = ( + "$\\mathbf{Velocities:}$\n\n$\\langle v_{e}\\rangle$:" + f" {mfile.get('vel_plasma_electron_vol_avg', scan=scan):.4e}" + " m/s\n$\\langle v_{D}\\rangle$:" + f" {mfile.get('vel_plasma_deuteron_vol_avg', scan=scan):.4e}" + " m/s\n$\\langle v_{T}\\rangle$:" + f" {mfile.get('vel_plasma_triton_vol_avg', scan=scan):.4e}" + " m/s\n$\\langle v_{\\alpha,thermal}\\rangle$:" + f" {mfile.get('vel_plasma_alpha_thermal_vol_avg', scan=scan):.4e}" + " m/s\n$v_{\\alpha,birth}$:" + f" {mfile.get('vel_plasma_alpha_birth', scan=scan):.4e} m/s" + ) + + textstr_frequencies = ( + "$\\mathbf{Frequencies:}$\n\n$\\langle\\omega_{p,e}\\rangle$:" + f" {mfile.get('freq_plasma_electron_vol_avg', scan=scan):.4e}" + " Hz\n$\\langle f_{Larmor,toroidal,e}\\rangle$:" + f" {mfile.get('freq_plasma_larmor_toroidal_electron_vol_avg', scan=scan):.4e}" + " Hz\n$\\langle f_{Larmor,toroidal,D}\\rangle$:" + f" {mfile.get('freq_plasma_larmor_toroidal_deuteron_vol_avg', scan=scan):.4e}" + " Hz\n$\\langle f_{Larmor,toroidal,T}\\rangle$:" + f" {mfile.get('freq_plasma_larmor_toroidal_triton_vol_avg', scan=scan):.4e}" + " Hz\n$\\langle\\omega_{UH,e}\\rangle$:" + f" {mfile.get('freq_plasma_upper_hybrid_vol_avg', scan=scan):.4e} Hz" + ) + + textstr_coulomb = ( + "$\\mathbf{Coulomb \\ Logarithms:}$\n\n" + "$\\langle\\ln \\Lambda_{e-e}\\rangle$:" + f" {mfile.get('plasma_coulomb_log_electron_electron_vol_avg', scan=scan):.4f}\n" + "$\\langle\\ln \\Lambda_{e-D}\\rangle$:" + f" {mfile.get('plasma_coulomb_log_electron_deuteron_vol_avg', scan=scan):.4f}\n" + "$\\langle\\ln \\Lambda_{e-T}\\rangle$:" + f" {mfile.get('plasma_coulomb_log_electron_triton_vol_avg', scan=scan):.4f}\n" + "$\\langle\\ln \\Lambda_{D-T}\\rangle$:" + f" {mfile.get('plasma_coulomb_log_deuteron_triton_vol_avg', scan=scan):.4f}\n" + "$\\langle\\ln \\Lambda_{e-\\alpha}\\rangle$:" + f" {mfile.get('plasma_coulomb_log_electron_alpha_thermal_vol_avg', scan=scan):.4f}" # noqa: E501 + ) + + textstr_collision_times = ( + "$\\mathbf{Collision \\ Times:}$\n\n" + "$\\langle\\tau_{e-e}\\rangle$:" + f" {mfile.get('t_plasma_electron_electron_collision_vol_avg', scan=scan):.4e} s\n" # noqa: E501 + "$\\langle\\tau_{e-D}\\rangle$:" + f" {mfile.get('t_plasma_electron_deuteron_collision_vol_avg', scan=scan):.4e} s\n" # noqa: E501 + "$\\langle\\tau_{e-T}\\rangle$:" + f" {mfile.get('t_plasma_electron_triton_collision_vol_avg', scan=scan):.4e} s\n" + "$\\langle\\tau_{e-\\alpha}\\rangle$:" + f" {mfile.get('t_plasma_electron_alpha_thermal_collision_vol_avg', scan=scan):.4e} s" # noqa: E501 + ) + + textstr_collision_freq = ( + "$\\mathbf{Collision \\ Frequencies:}$\n\n" + "$\\langle\\nu_{e-e}\\rangle$:" + f" {mfile.get('freq_plasma_electron_electron_collision_vol_avg', scan=scan):.4e}" + " Hz\n" + "$\\langle\\nu_{e-D}\\rangle$:" + f" {mfile.get('freq_plasma_electron_deuteron_collision_vol_avg', scan=scan):.4e}" + " Hz\n" + "$\\langle\\nu_{e-T}\\rangle$:" + f" {mfile.get('freq_plasma_electron_triton_collision_vol_avg', scan=scan):.4e}" + " Hz\n" + "$\\langle\\nu_{e-\\alpha}\\rangle$:" + f" {mfile.get('freq_plasma_electron_alpha_thermal_collision_vol_avg', scan=scan):.4e} Hz" # noqa: E501 + ) + + textstr_mfp = ( + "$\\mathbf{Mean \\ Free \\ Paths:}$\n\n" + "$\\langle\\lambda_{mfp,e-e}\\rangle$:" + f" {mfile.get('len_plasma_electron_electron_mean_free_path_vol_avg', scan=scan):.4e} m\n" # noqa: E501 + "$\\langle\\lambda_{mfp,e-D}\\rangle$:" + f" {mfile.get('len_plasma_electron_deuteron_mean_free_path_vol_avg', scan=scan):.4e} m\n" # noqa: E501 + "$\\langle\\lambda_{mfp,e-T}\\rangle$:" + f" {mfile.get('len_plasma_electron_triton_mean_free_path_vol_avg', scan=scan):.4e} m\n" # noqa: E501 + "$\\langle\\lambda_{mfp,e-\\alpha}\\rangle$:" + f" {mfile.get('len_plasma_electron_alpha_thermal_mean_free_path_vol_avg', scan=scan):.4e} m" # noqa: E501 + ) + + textstr_spitzer = ( + "$\\mathbf{Spitzer \\ Slowing \\" + " Down:}$\n\n$\\langle\\tau_{e-\\alpha,Spitzer}\\rangle$:" + f" {mfile.get('t_plasma_electron_alpha_spitzer_slow_vol_avg', scan=scan):.4e} s" + ) + + textstr_resistivity = ( + "$\\mathbf{Resistivities:}$\n\n$\\langle\\eta_{Spitzer}\\rangle$:" + f" {mfile.get('res_plasma_fuel_spitzer_vol_avg', scan=scan):.4e}" + " $\\Omega\\mathrm{m}$" + ) + + light_yellow_box = { + "boxstyle": "round", + "facecolor": "lightyellow", + "alpha": 1.0, + "linewidth": 2, + } + + draw_text( + axis, + 0.05, + 0.45, + textstr_debye, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=light_yellow_box, + ) + + draw_text( + axis, + 0.25, + 0.45, + textstr_larmor, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=light_yellow_box, + ) + + draw_text( + axis, + 0.45, + 0.45, + textstr_velocities, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=light_yellow_box, + ) + + light_cyan_box = { + "boxstyle": "round", + "facecolor": "lightcyan", + "alpha": 1.0, + "linewidth": 2, + } + + draw_text( + axis, + 0.05, + 0.31, + textstr_frequencies, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=light_cyan_box, + ) + + draw_text( + axis, + 0.25, + 0.31, + textstr_coulomb, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=light_cyan_box, + ) + + draw_text( + axis, + 0.45, + 0.31, + textstr_collision_times, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=light_cyan_box, + ) + + light_green_box = { + "boxstyle": "round", + "facecolor": "lightgreen", + "alpha": 1.0, + "linewidth": 2, + } + + draw_text( + axis, + 0.05, + 0.17, + textstr_collision_freq, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=light_green_box, + ) + + draw_text( + axis, + 0.25, + 0.17, + textstr_mfp, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=light_green_box, + ) + + draw_text( + axis, + 0.45, + 0.17, + textstr_spitzer + "\n" + textstr_resistivity, + fontsize=9, + verticalalignment="top", + horizontalalignment="left", + transform=fig.transFigure, + bbox=light_green_box, + ) + + axis.axis("off") + + +__all__ = ["plot_detailed_plasma_parameters", "plot_main_plasma_information"] diff --git a/process/core/io/plot/summary/plasma/physics.py b/process/core/io/plot/summary/plasma/physics.py new file mode 100644 index 0000000000..d637c3b980 --- /dev/null +++ b/process/core/io/plot/summary/plasma/physics.py @@ -0,0 +1,873 @@ +"""Plasma functions for PROCESS summary plots.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +import matplotlib as mpl +import matplotlib.pyplot as plt +import numpy as np + +from process.core.io.plot.summary.common import ( + box_style, +) +from process.core.io.plot.summary.constants import ( + PLASMA_COLOUR, +) +from process.core.io.plot.summary.rendering import ( + draw_text, +) +from process.models.build import Build +from process.models.geometry.plasma import plasma_geometry +from process.models.physics.physics import ( + BetaNormMaxModel, +) +from process.models.physics.plasma_current import ( + PlasmaCurrentModel, +) +from process.models.physics.plasma_geometry import ( + PlasmaShapeModelType, +) + +if TYPE_CHECKING: + from process.core.io.mfile import MFile + + +def plot_plasma( + axis: plt.Axes, + mfile: MFile, + scan: int, + colour_scheme: Literal[1, 2], + mirror_negative_x: bool = False, +): + """Plots the plasma boundary arcs. + + Parameters + ---------- + axis : + axis object to plot to + mfile : + MFILE data object + scan : + scan number to use + colour_scheme : + colour scheme to use for plots + mirror_negative_x : + if True, mirror the plot to the negative x-axis (Default value = False) + + + Raises + ------ + ValueError + If an unsupported plasma shape model type is encountered. + """ + r_0, a, triang, kappa, i_single_null, i_plasma_shape, plasma_square = ( + mfile.get_variables( + "rmajor", + "rminor", + "triang", + "kappa", + "i_single_null", + "i_plasma_shape", + "plasma_square", + scan=scan, + ) + ) + + pg = plasma_geometry( + rmajor=r_0, + rminor=a, + triang=triang, + kappa=kappa, + i_single_null=i_single_null, + i_plasma_shape=i_plasma_shape, + square=plasma_square, + ) + + # Apply mirror transformation if requested + x_scale = -1 if mirror_negative_x else 1 + + match PlasmaShapeModelType(i_plasma_shape): + case PlasmaShapeModelType.PROCESS_ORIGINAL: + # Plot the 2 plasma outline arcs. + axis.plot(x_scale * np.array(pg.rs[0]), pg.zs[0], color="black") + axis.plot(x_scale * np.array(pg.rs[1]), pg.zs[1], color="black") + + # Set triang_95 to stop plotting plasma past boundary + # Assume IPDG scaling + triang_95 = triang / 1.5 + + # Colour in right side of plasma + axis.fill_between( + x=x_scale * np.array(pg.rs[0]), + y1=pg.zs[0], + where=(pg.rs[0] > r_0 - (triang_95 * a * 1.5)), + color=PLASMA_COLOUR[colour_scheme - 1], + ) + # Colour in left side of plasma + axis.fill_between( + x=x_scale * np.array(pg.rs[1]), + y1=pg.zs[1], + where=(pg.rs[1] < r_0 - (triang_95 * a * 1.5)), + color=PLASMA_COLOUR[colour_scheme - 1], + ) + + case PlasmaShapeModelType.SAUTER: + axis.plot(x_scale * np.array(pg.rs), pg.zs, color="black") + axis.fill( + x_scale * np.array(pg.rs), + pg.zs, + color=PLASMA_COLOUR[colour_scheme - 1], + ) + case _: + raise ValueError(f"Unsupported plasma shape model type: {i_plasma_shape}") + + +def plot_plasma_current_comparison(axis: plt.Axes, mfile: MFile, scan: int): + """Function to plot a scatter box plot of different plasma current comparisons. + + Parameters + ---------- + axis : + Axis object to plot to. + mfile : + MFILE data object. + scan : + Scan number to use. + """ + c_plasma_peng_analytic = mfile.get("c_plasma_peng_analytic", scan=scan) + c_plasma_peng_double_null = mfile.get("c_plasma_peng_double_null", scan=scan) + c_plasma_cyclindrical = mfile.get("c_plasma_cyclindrical", scan=scan) + c_plasma_ipdg89 = mfile.get("c_plasma_ipdg89", scan=scan) + c_plasma_todd_empirical_i = mfile.get("c_plasma_todd_empirical_i", scan=scan) + c_plasma_todd_empirical_ii = mfile.get("c_plasma_todd_empirical_ii", scan=scan) + c_plasma_connor_hastie = mfile.get("c_plasma_connor_hastie", scan=scan) + c_plasma_sauter = mfile.get("c_plasma_sauter", scan=scan) + c_plasma_fiesta_st = mfile.get("c_plasma_fiesta_st", scan=scan) + + # Data for the box plot + data = { + f"{PlasmaCurrentModel.PENG_ANALYTIC_FIT.full_name}": (c_plasma_peng_analytic), + f"{PlasmaCurrentModel.PENG_DIVERTOR_SCALING.full_name}": ( + c_plasma_peng_double_null + ), + f"{PlasmaCurrentModel.ITER_SCALING.full_name}": c_plasma_cyclindrical, + f"{PlasmaCurrentModel.IPDG89_SCALING.full_name}": c_plasma_ipdg89, + f"{PlasmaCurrentModel.TODD_EMPIRICAL_SCALING_I.full_name}": ( + c_plasma_todd_empirical_i + ), + f"{PlasmaCurrentModel.TODD_EMPIRICAL_SCALING_II.full_name}": ( + c_plasma_todd_empirical_ii + ), + f"{PlasmaCurrentModel.CONNOR_HASTIE_MODEL.full_name}": (c_plasma_connor_hastie), + f"{PlasmaCurrentModel.SAUTER_SCALING.full_name}": c_plasma_sauter, + f"{PlasmaCurrentModel.FIESTA_ST_SCALING.full_name}": (c_plasma_fiesta_st), + } + + # Create the violin plot + data_values = list(data.values()) + axis.violinplot(data_values, showextrema=False) + + # Create the box plot + axis.boxplot(data_values, showfliers=True, showmeans=True, meanline=True, widths=0.3) + + # Scatter plot for each data point + colors = plt.cm.plasma(np.linspace(0, 1, len(data.values()))) + for index, (key, value) in enumerate(data.items()): + axis.scatter(1, value, color=colors[index], label=key, alpha=1.0) + axis.legend(loc="upper left", bbox_to_anchor=(-0.9, 1)) + + # Calculate average, standard deviation, and median + data_values = list(data.values()) + avg_density_limit = np.mean(data_values) + std_density_limit = np.std(data_values) + median_density_limit = np.median(data_values) + + # Plot average, standard deviation, and median as text + draw_text( + axis, + -0.45, + 0.15, + rf"Average: {avg_density_limit * 1e-6:.4f}", + transform=axis.transAxes, + fontsize=9, + ) + draw_text( + axis, + -0.45, + 0.1, + rf"Standard Dev: {std_density_limit * 1e-6:.4f}", + transform=axis.transAxes, + fontsize=9, + ) + draw_text( + axis, + -0.45, + 0.05, + rf"Median: {median_density_limit * 1e-6:.4f}", + transform=axis.transAxes, + fontsize=9, + ) + + axis.set_title("Plasma Current ($I_p$) Comparison") + axis.set_ylabel(r"Plasma Current [MA]") + axis.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x * 1e-6:.1f}")) + axis.set_xlim(0.5, 1.5) + axis.set_xticks([]) + axis.set_xticklabels([]) + axis.set_facecolor("#f0f0f0") + + +def plot_max_normalised_beta_comparison(axis: plt.Axes, mfile: MFile, scan: int): + """Function to plot a scatter box plot of different max normalised beta comparisons. + + Parameters + ---------- + axis : + Axis object to plot to. + mfile : + MFILE data object. + scan : + Scan number to use. + """ + beta_norm_max_wesson = mfile.get("beta_norm_max_wesson", scan=scan) + beta_norm_max_original_scaling = mfile.get( + "beta_norm_max_original_scaling", scan=scan + ) + beta_norm_max_menard = mfile.get("beta_norm_max_menard", scan=scan) + beta_norm_max_tholerus = mfile.get("beta_norm_max_tholerus", scan=scan) + beta_norm_max_stambaugh = mfile.get("beta_norm_max_stambaugh", scan=scan) + + # Data for the box plot + data = { + f"{BetaNormMaxModel.WESSON.full_name}": beta_norm_max_wesson, + f"{BetaNormMaxModel.ORIGINAL_SCALING.full_name}": ( + beta_norm_max_original_scaling + ), + f"{BetaNormMaxModel.MENARD.full_name}": beta_norm_max_menard, + f"{BetaNormMaxModel.THOLERUS.full_name}": beta_norm_max_tholerus, + f"{BetaNormMaxModel.STAMBAUGH.full_name}": beta_norm_max_stambaugh, + } + data_values = list(data.values()) + # Create the violin plot + axis.violinplot(data_values, showextrema=False) + + # Create the box plot + axis.boxplot(data_values, showfliers=True, showmeans=True, meanline=True, widths=0.3) + + # Scatter plot for each data point + colors = plt.cm.plasma(np.linspace(0, 1, len(data.values()))) + for index, (key, value) in enumerate(data.items()): + axis.scatter(1, value, color=colors[index], label=key, alpha=1.0) + axis.legend(loc="upper left", bbox_to_anchor=(1.1, 1)) + + # Calculate average, standard deviation, and median + data_values = list(data.values()) + avg_beta_norm_max = np.mean(data_values) + std_beta_norm_max = np.std(data_values) + median_beta_norm_max = np.median(data_values) + + # Plot average, standard deviation, and median as text + draw_text( + axis, + 1.1, + 0.15, + rf"Average: {avg_beta_norm_max:.4f}", + transform=axis.transAxes, + fontsize=9, + ) + draw_text( + axis, + 1.1, + 0.1, + rf"Standard Dev: {std_beta_norm_max:.4f}", + transform=axis.transAxes, + fontsize=9, + ) + draw_text( + axis, + 1.1, + 0.05, + rf"Median: {median_beta_norm_max:.4f}", + transform=axis.transAxes, + fontsize=9, + ) + + axis.set_title("Max Normalised Beta ($\\beta_N$) Comparison") + axis.set_ylabel("Max Normalised Beta $\\beta_N$ [unitless]") + axis.set_xlim(0.5, 1.5) + axis.set_xticks([]) + axis.set_xticklabels([]) + axis.set_facecolor("#f0f0f0") + + +def reaction_plot_grid( + rminor, + rmajor, + kappa, + r_grid, + z_grid, + grid, + ax, + fractions=(0.25, 0.5, 0.75), + colours=("blue", "yellow", "red"), +): + """Plot fusion reaction rate density""" + # Mask points outside the plasma boundary (optional, but grid is inside by + # construction) + # Plot filled contour + + upper = ax.contourf(r_grid, z_grid, grid, levels=50, cmap="plasma", zorder=2) + ax.contourf(r_grid, -z_grid, grid, levels=50, cmap="plasma", zorder=2) + + ax.figure.colorbar( + upper, + ax=ax, + label="Fusion Rate Density [reactions/m³/sec]", + location="left", + anchor=(-0.25, 0.5), + ) + + ax.set_xlabel("R [m]") + ax.set_xlim(rmajor - 1.2 * rminor, rmajor + 1.2 * rminor) + ax.set_ylim(-1.2 * rminor * kappa, 1.2 * kappa * rminor) + ax.set_ylabel("Z [m]") + ax.plot( + rmajor, + 0, + marker="o", + color="red", + markersize=6, + markeredgecolor="black", + zorder=100, + ) + # enable minor ticks and grid for clearer reading + ax.minorticks_on() + ax.grid(True, which="major", linestyle="--", linewidth=0.8, alpha=0.7, zorder=1) + ax.grid(True, which="minor", linestyle=":", linewidth=0.4, alpha=0.5, zorder=1) + # make minor ticks visible on all sides and draw ticks inward for compact look + ax.tick_params(which="both", direction="in", top=True, right=True) + + # draw contours at % of the DT peak value (both top and mirrored bottom) + peak = np.nanmax(grid) + if peak > 0: + c_kwargs = { + "levels": [f * peak for f in fractions], + "colors": colours, + "linewidths": 1.5, + } + # distinct colours for each level + + # top and mirrored bottom contours (no clabel calls — keep only legend) + ax.contour(r_grid, z_grid, grid, **c_kwargs) + ax.contour(r_grid, -z_grid, grid, **c_kwargs) + + # create legend entries (use Line2D proxies so we get one entry per requested + # level) + legend_handles = [mpl.lines.Line2D([0], [0], color=c, lw=2) for c in colours] + legend_labels = ["25% peak", "50% peak", "75% peak"] + ax.legend(legend_handles, legend_labels, loc="upper right", fontsize=8) + + +def plot_magnetic_fields_in_plasma(axis: plt.Axes, mfile: MFile, scan: int): + """Plot magnetic field profiles inside the plasma boundary""" + n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) + + # Get toroidal magnetic field profile (in Tesla) + b_plasma_toroidal_profile = [ + mfile.get(f"b_plasma_toroidal_profile{i}", scan=scan) + for i in range(2 * n_plasma_profile_elements) + ] + + # Get major and minor radius for x-axis in metres + rmajor = mfile.get("rmajor", scan=scan) + rminor = mfile.get("rminor", scan=scan) + + # Plot magnetic field first (background) + axis.plot( + np.linspace(rmajor - rminor, rmajor + rminor, len(b_plasma_toroidal_profile)), + b_plasma_toroidal_profile, + color="blue", + label="Toroidal B-field [T]", + linewidth=2, + ) + + # Plot plasma on top of magnetic field, displaced vertically by bt + plot_plasma(axis, mfile, scan, colour_scheme=1) + + # Plot plasma centre dot + axis.plot(rmajor, 0, marker="o", color="red", markersize=8, label="Plasma Centre") + + v_kwargs = {"color": "green", "linestyle": "--", "linewidth": 1.0} + + # Plot vertical lines at plasma edge + axis.axvline(rmajor - rminor, **v_kwargs) + axis.axvline(rmajor + rminor, **v_kwargs) + + h_kwargs = {"color": "blue", "linestyle": "--", "linewidth": 1.0} + + # Plot horizontal line for toroidal magnetic field at plasma inboard + axis.axhline(mfile.get(f"b_plasma_toroidal_profile{0}", scan=scan), **h_kwargs) + + # Plot horizontal line for toroidal magnetic field at plasma centre + axis.axhline(mfile.get("b_plasma_toroidal_on_axis", scan=scan), **h_kwargs) + + # Plot horizontal line for toroidal magnetic field at plasma outboard + axis.axhline(b_plasma_toroidal_profile[-1], **h_kwargs) + + # Text box for inboard toroidal field + draw_text( + axis, + 0.1, + 0.025, + f"$B_{{\\text{{T,inboard}}}}={mfile.get('b_plasma_inboard_toroidal', scan=scan):.2f}$ T\n" # noqa: E501 + f"$B_{{\\text{{total,inboard}}}}={mfile.get('b_plasma_inboard_total', scan=scan):.2f}$ T", # noqa: E501 + verticalalignment="center", + horizontalalignment="center", + transform=axis.transAxes, + bbox=box_style("wheat"), + ) + + # Text box for outboard toroidal field + draw_text( + axis, + 0.9, + 0.1, + f"$B_{{\\text{{T,outboard}}}}={mfile.get('b_plasma_outboard_toroidal', scan=scan):.2f}$ T\n" # noqa: E501 + f"$B_{{\\text{{total,outboard}}}}={mfile.get('b_plasma_outboard_total', scan=scan):.2f}$ T", # noqa: E501 + verticalalignment="center", + horizontalalignment="center", + transform=axis.transAxes, + bbox=box_style("wheat"), + ) + + axis.set_xlabel("Radial Position [m]") + axis.set_ylabel("Toroidal Magnetic Field [T]") + axis.set_title("Toroidal Magnetic Field Profile in Plasma") + axis.minorticks_on() + # Enable grid for both major and minor ticks + axis.grid(which="both", linestyle="--", alpha=0.5) + axis.grid(which="minor", linestyle=":", alpha=0.3) + axis.legend(loc="lower right") + axis.set_xlim(rmajor - 1.25 * rminor, rmajor + 1.25 * rminor) + + +def plot_plasma_outboard_toroidal_ripple_map(fig, mfile: MFile, scan: int): + """Plot plasma outboard toroidal ripple map""" + r_tf_outboard_mid = mfile.get("r_tf_outboard_mid", scan=scan) + n_tf_coils = mfile.get("n_tf_coils", scan=scan) + rmajor = mfile.get("rmajor", scan=scan) + rminor = mfile.get("rminor", scan=scan) + r_tf_wp_inboard_inner = mfile.get("r_tf_wp_inboard_inner", scan=scan) + r_tf_wp_inboard_centre = mfile.get("r_tf_wp_inboard_centre", scan=scan) + r_tf_wp_inboard_outer = mfile.get("r_tf_wp_inboard_outer", scan=scan) + dx_tf_wp_primary_toroidal = mfile.get("dx_tf_wp_primary_toroidal", scan=scan) + i_tf_shape = mfile.get("i_tf_shape", scan=scan) + i_tf_sup = mfile.get("i_tf_sup", scan=scan) + dx_tf_wp_insulation = mfile.get("dx_tf_wp_insulation", scan=scan) + dx_tf_wp_insertion_gap = mfile.get("dx_tf_wp_insertion_gap", scan=scan) + ripple_b_tf_plasma_edge_max = mfile.get("ripple_b_tf_plasma_edge_max", scan=scan) + i_tf_wp_geom = round(mfile.get("i_tf_wp_geom", scan=scan)) + + build = Build() + + r_nom = r_tf_outboard_mid + dx_nom = dx_tf_wp_primary_toroidal if dx_tf_wp_primary_toroidal is not None else 0.0 + + # Simple ±20% scan around nominal values for r and dx + r_min = r_nom * 0.9 + r_max = r_nom * 1.1 + + if dx_nom > 0: + dx_min = dx_nom * 0.8 + dx_max = dx_nom * 1.2 + else: + # fallback sensible small range if nominal is zero + dx_min = 1e-3 + dx_max = 1e-2 + + n_r = 50 + n_dx = 50 + r_vals = np.linspace(r_min, r_max, n_r) + dx_vals = np.linspace(dx_min, dx_max, n_dx) + + rg, dxg = np.meshgrid(r_vals, dx_vals) + + # prepare metric array to hold ripple metric for each (r, dx) pair + metric = np.full(rg.shape, np.nan, dtype=float) + + for ii in range(rg.shape[0]): + for jj in range(rg.shape[1]): + r_test = float(rg[ii, jj]) + dx_test = float(dxg[ii, jj]) + + try: + rip, _, _ = build.plasma_outboard_edge_toroidal_ripple( + ripple_b_tf_plasma_edge_max=0.05, + r_tf_outboard_mid=r_test, + n_tf_coils=int(n_tf_coils), + rmajor=rmajor, + rminor=rminor, + r_tf_wp_inboard_inner=r_tf_wp_inboard_inner, + r_tf_wp_inboard_centre=r_tf_wp_inboard_centre, + r_tf_wp_inboard_outer=r_tf_wp_inboard_outer, + dx_tf_wp_primary_toroidal=dx_test, + i_tf_shape=i_tf_shape, + i_tf_sup=i_tf_sup, + dx_tf_wp_insulation=dx_tf_wp_insulation, + dx_tf_wp_insertion_gap=dx_tf_wp_insertion_gap, + i_tf_wp_geom=i_tf_wp_geom, + ) + except (ValueError, ZeroDivisionError, OverflowError, TypeError): + # Only catch expected numeric/validation errors from the ripple + # calculation; + # let other exceptions propagate so they can be diagnosed. + rip = np.nan + metric[ii, jj] = rip + + # Create two subplots that share the same x axis + ax1 = fig.add_subplot(2, 1, 1) + ax2 = fig.add_subplot(2, 1, 2, sharex=ax1) + + # Make contour plot of the ripple metric (r vs dx) on ax1 + if np.all(np.isnan(metric)): + ax1.text( + 0.5, + 0.5, + "No valid ripple data (r vs dx)", + ha="center", + va="center", + ) + else: + vmin = np.nanmin(metric) + vmax = np.nanmax(metric) + + # Guard against degenerate range + if np.isclose(vmin, vmax, atol=1e-12) or np.isnan(vmin) or np.isnan(vmax): + vmin -= 0.25 + vmax += 0.25 + + # Smooth filled contour levels + levels = np.linspace(vmin, vmax, 50) + cf = ax1.contourf(rg, dxg, metric, levels=levels, cmap="plasma", extend="both") + + # Contour lines only at 0.5 increments + step = 0.5 + start = np.floor(vmin / step) * step + end = np.ceil(vmax / step) * step + contour_levels = np.arange(start, end + 1e-12, step) + + # Fallback if contour_levels is empty for some reason + if contour_levels.size < 2: + contour_levels = np.array([vmin, vmax]) + + contours = ax1.contour( + rg, + dxg, + metric, + levels=contour_levels, + colors="k", + linewidths=0.5, + alpha=0.7, + ) + ax1.clabel(contours, inline=True, fontsize=8, fmt="%.2f%%", colors="white") + # Overlay contour line at the specified target ripple value + + target = float(ripple_b_tf_plasma_edge_max) + + if target is not None and not np.isnan(target): + # Check if target lies within computed metric range + if (target >= vmin) and (target <= vmax): + c_target = ax1.contour( + rg, + dxg, + metric, + levels=[target], + colors="white", + linewidths=2.0, + linestyles="--", + zorder=20, + ) + ax1.clabel( + c_target, + inline=True, + fmt={target: f"Input Max {target:.2f}%"}, + fontsize=8, + colors="white", + ) + else: + # annotate that target is outside plotted range + ax1.text( + 0.02, + 0.98, + f"Target ripple {target:.2f}% outside plot range" + f" [{vmin:.2f},{vmax:.2f}]", + transform=ax1.transAxes, + color="white", + fontsize=8, + va="top", + bbox={"facecolor": "black", "alpha": 0.6, "pad": 2}, + ) + + # Colourbar with 0.5 increments (use the same contour_levels as for the contour + # lines) + ticks = contour_levels + # Fallback to sensible ticks if contour_levels is not appropriate + if ticks.size == 0 or np.isnan(ticks).all(): + ticks = np.linspace(vmin, vmax, 5) + cb = ax1.figure.colorbar( + cf, ax=ax1, label="Plasma Outboard Toroidal Ripple", ticks=ticks + ) + cb.ax.set_yticklabels([f"{t:.2f}%" for t in ticks]) + + # mark nominal point + ax1.scatter( + [r_nom], + [dx_nom], + color="white", + edgecolor="black", + s=200, + linewidths=1.5, + marker="o", + zorder=10, + label="Design Point", + ) + ax1.set_xlabel("Outboard TF leg centre [m]") + ax1.set_ylabel("WP Toroidal Width [m]") + ax1.legend(loc="upper right") + + # --------------------------------------------------------------------- + # Second plot: scan number of TF coils vs r_tf_outboard_mid (keep dx at nominal) + # --------------------------------------------------------------------- + # Determine a sensible integer range of TF coils to scan around nominal + n_nom = int(n_tf_coils) + span = max(2, int(min(12, n_nom // 2))) # choose a span based on nominal + n_min = max(10, n_nom - span) + n_max = n_nom + span + n_vals = np.arange(n_min, n_max + 1, dtype=int) + + n_r2 = 60 + r_vals2 = np.linspace(r_min, r_max, n_r2) + rg2, ng2 = np.meshgrid(r_vals2, n_vals) + + metric2 = np.full(rg2.shape, np.nan, dtype=float) + + for ii in range(rg2.shape[0]): + for jj in range(rg2.shape[1]): + r_test = float(rg2[ii, jj]) + n_test = int(ng2[ii, jj]) + try: + rip, *_ = build.plasma_outboard_edge_toroidal_ripple( + ripple_b_tf_plasma_edge_max=0.05, + r_tf_outboard_mid=r_test, + n_tf_coils=n_test, + rmajor=rmajor, + rminor=rminor, + r_tf_wp_inboard_inner=r_tf_wp_inboard_inner, + r_tf_wp_inboard_centre=r_tf_wp_inboard_centre, + r_tf_wp_inboard_outer=r_tf_wp_inboard_outer, + dx_tf_wp_primary_toroidal=dx_nom, + i_tf_shape=i_tf_shape, + i_tf_sup=i_tf_sup, + dx_tf_wp_insulation=dx_tf_wp_insulation, + dx_tf_wp_insertion_gap=dx_tf_wp_insertion_gap, + i_tf_wp_geom=i_tf_wp_geom, + ) + except (ValueError, ZeroDivisionError, OverflowError, TypeError): + # Only catch expected numeric/validation errors from the ripple + # calculation; + # let other exceptions propagate so they can be diagnosed. + rip = np.nan + metric2[ii, jj] = rip + + # Plot the second metric on the bottom axes (ax2) so it shares x-axis with ax1 + if np.all(np.isnan(metric2)): + ax2.text( + 0.5, + 0.5, + "No valid ripple data (r vs n_tf_coils)", + ha="center", + va="center", + ) + else: + vmin2 = np.nanmin(metric2) + vmax2 = np.nanmax(metric2) + + # filled contour levels (smooth shading) + levels2 = np.linspace(vmin2, vmax2, 40) + cf2 = ax2.contourf( + rg2, ng2, metric2, levels=levels2, cmap="viridis", extend="both" + ) + + # contour lines only at 0.5 steps + step = 0.5 + start = np.floor(vmin2 / step) * step + end = np.ceil(vmax2 / step) * step + contour_levels = np.arange(start, end + 1e-12, step) + + # fallback if arange returned empty (very small range) + if contour_levels.size == 0: + contour_levels = np.array([vmin2, vmax2]) + + contours2 = ax2.contour( + rg2, + ng2, + metric2, + levels=contour_levels, + colors="k", + linewidths=0.5, + alpha=0.7, + ) + ax2.clabel(contours2, inline=True, fontsize=8, fmt="%.2f%%", colors="white") + + target2 = float(ripple_b_tf_plasma_edge_max) + + if target2 is not None and not np.isnan(target2): + if (target2 >= vmin2) and (target2 <= vmax2): + c_target2 = ax2.contour( + rg2, + ng2, + metric2, + levels=[target2], + colors="white", + linewidths=2.0, + linestyles="--", + zorder=20, + ) + ax2.clabel( + c_target2, + inline=True, + fmt={target2: f"Input Max {target2:.2f}%"}, + fontsize=8, + colors="white", + ) + else: + ax2.text( + 0.02, + 0.98, + f"Target ripple {target2:.2f}% outside plot range" + f" [{vmin2:.2f},{vmax2:.2f}]", + transform=ax2.transAxes, + color="white", + fontsize=8, + va="top", + bbox={"facecolor": "black", "alpha": 0.6, "pad": 2}, + ) + # colorbar with 0.5 increments + # ensure contour_levels exists and is in 0.5 steps (constructed above) + ticks = contour_levels + cb2 = ax2.figure.colorbar( + cf2, ax=ax2, label="Plasma Outboard Toroidal Ripple", ticks=ticks + ) + cb2.ax.set_yticklabels([f"{t:.2f}%" for t in ticks]) + + # nominal markers + ax2.scatter( + [r_nom], + [n_nom], + color="white", + edgecolor="black", + s=300, + linewidths=1.5, + marker="o", + zorder=10, + label="Design Point", + ) + ax2.set_xlabel("Outboard TF leg centre [m]") + ax2.set_ylabel("Number of TF coils") + ax2.set_yticks(n_vals) + ax2.legend(loc="upper right") + + # Improve layout + fig.tight_layout() + + +def plot_plasma_coloumb_logarithms(axis: plt.Axes, mfile_data: MFile, scan: int) -> None: + """Plot the plasma coloumb logarithms on the given axis.""" + plasma_coulomb_log_electron_electron_profile = [ + mfile_data.data[f"plasma_coulomb_log_electron_electron_profile{i}"].get_scan( + scan + ) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + + plasma_coulomb_log_electron_deuteron_profile = [ + mfile_data.data[f"plasma_coulomb_log_electron_deuteron_profile{i}"].get_scan( + scan + ) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + + plasma_coulomb_log_electron_triton_profile = [ + mfile_data.data[f"plasma_coulomb_log_electron_triton_profile{i}"].get_scan(scan) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + + plasma_coulomb_log_deuteron_triton_profile = [ + mfile_data.data[f"plasma_coulomb_log_deuteron_triton_profile{i}"].get_scan(scan) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + + plasma_coulomb_log_electron_alpha_thermal_profile = [ + mfile_data.data[ + f"plasma_coulomb_log_electron_alpha_thermal_profile{i}" + ].get_scan(scan) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + + axis.plot( + np.linspace(0, 1, len(plasma_coulomb_log_electron_electron_profile)), + plasma_coulomb_log_electron_electron_profile, + color="blue", + linestyle="-", + label=r"$ln \Lambda_{e-e}$", + ) + + axis.plot( + np.linspace(0, 1, len(plasma_coulomb_log_electron_deuteron_profile)), + plasma_coulomb_log_electron_deuteron_profile, + color="pink", + linestyle="-", + label=r"$ln \Lambda_{e-D}$", + ) + + axis.plot( + np.linspace(0, 1, len(plasma_coulomb_log_electron_triton_profile)), + plasma_coulomb_log_electron_triton_profile, + color="green", + linestyle="-", + label=r"$ln \Lambda_{e-T}$", + ) + + axis.plot( + np.linspace(0, 1, len(plasma_coulomb_log_deuteron_triton_profile)), + plasma_coulomb_log_deuteron_triton_profile, + color="orange", + linestyle="-", + label=r"$ln \Lambda_{D-T}$", + ) + + axis.plot( + np.linspace(0, 1, len(plasma_coulomb_log_electron_alpha_thermal_profile)), + plasma_coulomb_log_electron_alpha_thermal_profile, + color="red", + linestyle="-", + label=r"$ln \Lambda_{e-\alpha,thermal}$", + ) + + axis.set_ylabel("Coulomb Logarithm") + axis.set_xlabel("$\\rho \\ [r/a]$") + axis.grid(True, which="both", linestyle="--", alpha=0.5) + axis.minorticks_on() + axis.legend() + + +__all__ = [ + "plot_magnetic_fields_in_plasma", + "plot_max_normalised_beta_comparison", + "plot_plasma", + "plot_plasma_coloumb_logarithms", + "plot_plasma_current_comparison", + "plot_plasma_outboard_toroidal_ripple_map", + "reaction_plot_grid", +] diff --git a/process/core/io/plot/summary/power_flow.py b/process/core/io/plot/summary/power_flow.py new file mode 100644 index 0000000000..add27d689f --- /dev/null +++ b/process/core/io/plot/summary/power_flow.py @@ -0,0 +1,1980 @@ +"""Power Flow functions for PROCESS summary plots.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from process.core.io.plot.summary.common import ( + box_style, + load_plot_image, + setup_axis, + text_layout, +) +from process.core.io.plot.summary.rendering import ( + draw_annotation, + draw_text, +) +from process.core.io.plot.summary.reporting import ( + plot_info, +) + +if TYPE_CHECKING: + import matplotlib.pyplot as plt + + from process.core.io.mfile import MFile + + +def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figure): + """Plots the main power flow diagram for the fusion reactor, including plasma, + heating and current drive, + first wall, blanket, vacuum vessel, divertor, coolant pumps, turbine, generator, and + auxiliary systems. + Annotates the diagram with power values and draws arrows to indicate power flows. + + Parameters + ---------- + axis: + The matplotlib axis object to plot on. + mfile: + The MFILE data object containing power flow parameters. + scan: + The scan number to use for extracting data. + fig: + The matplotlib figure object for additional annotations. + """ + draw_text( + axis, + 0.05, + 0.95, + "* Components do not represent the design", + transform=fig.transFigure, + horizontalalignment="left", + verticalalignment="bottom", + zorder=2, + fontsize=11, + ) + + # ========================================== + # Plasma + # =========================================== + + # Load the plasma image + plasma = load_plot_image("plasma.png") + + # Display the plasma image over the figure, not the axes + new_ax = axis.inset_axes( + (-0.15, 0.6, 0.45, 0.45), transform=axis.transAxes, zorder=1 + ) + new_ax.imshow(plasma) + new_ax.axis("off") + + # Add fusion power to plasma + draw_text( + axis, + 0.22, + 0.75, + f"$P_{{{{fus}}}}$\n{mfile.get('p_fusion_total_mw', scan=scan):.2f} MW", + transform=fig.transFigure, + horizontalalignment="left", + verticalalignment="bottom", + zorder=2, + fontsize=11, + ) + # Load the neutron image + neutron = load_plot_image("neutron.png") + + new_ax = axis.inset_axes( + (0.2, 0.85, 0.03, 0.03), transform=axis.transAxes, zorder=10 + ) + new_ax.imshow(neutron) + new_ax.axis("off") + + # Add lost alpha power + draw_text( + axis, + 0.22, + 0.81, + f"$P_{{\\alpha,{{loss}}}}$\n{mfile.get('p_fw_alpha_mw', scan=scan):,.2f} MW", + transform=fig.transFigure, + horizontalalignment="left", + verticalalignment="bottom", + zorder=2, + fontsize=11, + ) + + # Add radiation power to plasma + draw_text( + axis, + 0.22, + 0.69, + f"$P_{{{{rad}}}}$\n{mfile.get('p_plasma_rad_mw', scan=scan):,.2f} MW", + transform=fig.transFigure, + horizontalalignment="left", + verticalalignment="bottom", + zorder=2, + fontsize=11, + ) + + # Add photon image to plasma + draw_text( + axis, + 0.34, + 0.71, + "$\\gamma$", + transform=fig.transFigure, + horizontalalignment="left", + verticalalignment="bottom", + zorder=2, + fontsize=12, + ) + + # Draw from gamma arrow bend towards divertor + draw_annotation( + axis, + "", + xy=(0.35, 0.55), + xytext=(0.35, 0.695), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "blue", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Add separatrix power to plasma + draw_text( + axis, + 0.22, + 0.63, + f"$P_{{{{sep}}}}$\n{mfile.get('p_plasma_separatrix_mw', scan=scan):,.2f} MW", + transform=fig.transFigure, + horizontalalignment="left", + verticalalignment="bottom", + zorder=2, + fontsize=11, + ) + + # Draw from separatrix power to arrow bend + draw_annotation( + axis, + "", + xy=(0.3725, 0.65), + xytext=(0.3, 0.65), + xycoords=fig.transFigure, + arrowprops={ + "color": "pink", + "arrowstyle": "-", # No arrow head + "linewidth": 2.0, + }, + ) + + # Draw from separatrix arrow bend to the divertor + draw_annotation( + axis, + "", + xy=(0.37, 0.55), + xytext=(0.37, 0.65), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": ("-|>,head_length=1,head_width=0.3"), # solid filled head + "color": "pink", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Draw neutron arrow from plasma + draw_annotation( + axis, + "", + xy=(0.95, 0.76), + xytext=(0.31, 0.76), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "grey", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Draw arrow from main neutron arrow down to divertor + draw_annotation( + axis, + "", + xy=(0.39, 0.55), + xytext=(0.39, 0.76), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "grey", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Draw radiation arrow from plasma + draw_annotation( + axis, + "", + xy=(0.56, 0.695), + xytext=(0.3, 0.695), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "blue", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Load the alpha particle image + alpha = load_plot_image("alpha_particle.png") + + # Display the alpha particle image over the figure, not the axes + new_ax = axis.inset_axes( + (0.16, 0.95, 0.025, 0.025), transform=axis.transAxes, zorder=10 + ) + new_ax.imshow(alpha) + new_ax.axis("off") + + # Hide the axes for a cleaner look + axis.axis("off") + + # Draw alpha particle arrow from plasma + draw_annotation( + axis, + "", + xy=(0.56, 0.83), + xytext=(0.3, 0.83), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "red", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Plot neutron power from plasma to box + draw_text( + axis, + 0.37, + 0.775, + f"$P_{{\\text{{neutron}}}}$:\n{mfile.get('p_neutron_total_mw', scan=scan):,.2f} MW", # noqa: E501 + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "grey", + "alpha": 0.8, + "linewidth": 2, + }, + ) + + # =========================================== + + # ========================================= + # Heating and current drive systems + # ========================================= + + # Add HCD primary injected power + draw_text( + axis, + 0.0725, + 0.83, + "$P_{\\text{HCD,primary}}$:" + f" {mfile.get('p_hcd_primary_injected_mw', scan=scan) + mfile.get('p_hcd_primary_extra_heat_mw', scan=scan):.2f} MW", # noqa: E501 + **text_layout(fig), + bbox=box_style("lightyellow"), + ) + + # Add HCD secondary injected power + draw_text( + axis, + 0.0725, + 0.725, + "$P_{\\text{HCD,secondary}}$:" + f" {mfile.get('p_hcd_secondary_injected_mw', scan=scan) + mfile.get('p_hcd_secondary_extra_heat_mw', scan=scan):.2f} MW", # noqa: E501 + **text_layout(fig), + bbox=box_style("lightyellow"), + ) + + # Load the HCD injector image + hcd_injector_1 = hcd_injector_2 = load_plot_image("hcd_injector.png") + + # Display the injector image over the figure, not the axes + new_ax = axis.inset_axes( + (-0.2, 0.8, 0.15, 0.15), transform=axis.transAxes, zorder=10 + ) + new_ax.imshow(hcd_injector_1) + new_ax.axis("off") + new_ax = axis.inset_axes((-0.2, 0.5, 0.15, 0.5), transform=axis.transAxes, zorder=10) + new_ax.imshow(hcd_injector_2) + new_ax.axis("off") + + # Draw a dashed line with an arrow tip coming from the left of each injector + for y in [0.875, 0.75]: + draw_annotation( + axis, + "", + xy=(-0.2, y), + xytext=(-0.28, y), + xycoords=axis.transAxes, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 1.5, + "zorder": 11, + }, + annotation_clip=False, + ) + + # Plot line from HCD power supply to bend for injected + axis.plot( + [-0.28, -0.28], + [0.875, 0.5], + transform=axis.transAxes, + color="black", + linewidth=1.5, + zorder=3, + clip_on=False, + ) + + # Plot the HCD power supply box + draw_text( + axis, + 0.04, + 0.45, + "\n\nH&CD Power Supply\n\n", + **text_layout(fig), + bbox=box_style("lightyellow"), + zorder=4, + ) + + # Draw arrow from HCD box going to primary HCD losses + draw_annotation( + axis, + "", + xy=(0.2, 0.5), + xytext=(0.1, 0.5), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.2", + "color": "black", + "linestyle": "--", + "linewidth": 1.5, + "zorder": 5, + "fill": True, + }, + ) + + # Plot electric power losses for secondary HCD + draw_text( + axis, + 0.2, + 0.435, + f"$P_{{\\text{{secondary,loss}}}}$:\n{mfile.get('p_hcd_secondary_electric_mw', scan=scan) * (1.0 - mfile.get('eta_hcd_secondary_injector_wall_plug', scan=scan)):.2f} MWe", # noqa: E501 + **text_layout(fig), + bbox=box_style("lightblue") | {"linestyle": "dashed"}, + ) + + # Draw an arrow from HCD secondary losses to the total secondary heat power + draw_annotation( + axis, + "", + xy=(0.25, 0.3), + xytext=(0.25, 0.43), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 1.5, + "zorder": 5, + "fill": True, + "linestyle": "--", + }, + ) + + # Draw an arrow from HCD primary losses bend to the total secondary heat power + draw_annotation( + axis, + "", + xy=(0.28, 0.3), + xytext=(0.28, 0.5), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 1.5, + "zorder": 5, + "fill": True, + "linestyle": "--", + }, + ) + + # Draw line from HCD primary losses to the arrow bend + draw_annotation( + axis, + "", + xy=(0.26, 0.5), + xytext=(0.28, 0.5), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-", + "color": "black", + "linestyle": "--", + "linewidth": 1.5, + "zorder": 5, + "fill": True, + }, + ) + + # Draw arrow frim HCD power supply to secondary HCD losses + draw_annotation( + axis, + "", + xy=(0.2, 0.46), + xytext=(0.1, 0.46), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.2", + "color": "black", + "linestyle": "--", + "linewidth": 1.5, + "zorder": 5, + "fill": True, + }, + ) + + # Plot electric power losses for primary HCD + draw_text( + axis, + 0.2, + 0.485, + f"$P_{{\\text{{primary,loss}}}}$:\n{mfile.get('p_hcd_primary_electric_mw', scan=scan) * (1.0 - mfile.get('eta_hcd_primary_injector_wall_plug', scan=scan)):.2f} MWe", # noqa: E501 + **text_layout(fig), + bbox=box_style("lightblue") | {"linestyle": "dashed"}, + ) + + # Draw arrow from HCD primary electric box to HCD power supply box + draw_annotation( + axis, + "", + xy=(0.06, 0.45), + xytext=(0.06, 0.38), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "->", + "color": "black", + "linewidth": 1.5, + "zorder": 5, + }, + ) + + # Draw arrow from HCD secondary electric box to HCD power supply box + draw_annotation( + axis, + "", + xy=(0.12, 0.45), + xytext=(0.12, 0.38), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "->", + "color": "black", + "linewidth": 1.5, + "zorder": 5, + }, + ) + + # Plot HCD secondary losses box + draw_text( + axis, + 0.12, + 0.35, + f"$P_{{\\text{{secondary}}}}$:\n{mfile.get('p_hcd_secondary_electric_mw', scan=scan):.2f}" # noqa: E501 + " MWe\n$\\eta$:" + f" {mfile.get('eta_hcd_secondary_injector_wall_plug', scan=scan):.2f}", + **text_layout(fig), + bbox=box_style("lightyellow"), + ) + + # Plot HCD primary electric box + draw_text( + axis, + 0.025, + 0.35, + f"$P_{{\\text{{primary}}}}$:\n{mfile.get('p_hcd_primary_electric_mw', scan=scan):.2f}" # noqa: E501 + " MWe\n$\\eta$:" + f" {mfile.get('eta_hcd_primary_injector_wall_plug', scan=scan):.2f}", + **text_layout(fig), + bbox=box_style("lightyellow"), + ) + + # ============================================= + + # ============================================= + # Low grade heat total + # ============================================= + + # Plot box of total low grade secondary heat + draw_text( + axis, + 0.325, + 0.225, + "\n\nTotal Low Grade Secondary Heat\n\n" + f" {mfile.get('p_plant_secondary_heat_mw', scan=scan):,.2f} MWth", + fontsize=9, + verticalalignment="bottom", + horizontalalignment="center", + transform=fig.transFigure, + bbox=box_style("lightblue") | {"linestyle": "dashed"}, + zorder=4, + ) + + # ============================================= + + # ========================================== + # Power conversion systems + # =========================================== + + # Load the turbine image + turbine = load_plot_image("turbine.png") + + # Display the turbine image over the figure, not the axes + new_ax = axis.inset_axes((1.1, 0.0, 0.15, 0.15), transform=axis.transAxes, zorder=10) + new_ax.imshow(turbine) + new_ax.axis("off") + + # Plot the total primary thermal power box + draw_text( + axis, + 0.9, + 0.25, + f"$P_{{\\text{{primary,thermal}}}}$:\n{mfile.get('p_plant_primary_heat_mw', scan=scan):,.2f}" # noqa: E501 + " MW\n$\\eta_{\\text{turbine}}$:" + f" {mfile.get('eta_turbine', scan=scan):.3f}", + **text_layout(fig), + bbox=box_style("orange"), + ) + + # Draw arrow from bend to turbine inlet + draw_annotation( + axis, + "", + xy=(0.925, 0.165), + xytext=(0.96, 0.165), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "orange", + "linewidth": 3.0, + "zorder": 5, + "fill": True, + }, + ) + + # Total primary thermal to turbine inlet line bend + draw_annotation( + axis, + "", + xy=(0.96, 0.245), + xytext=(0.96, 0.1625), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-", + "color": "orange", + "linewidth": 3.0, + "zorder": 5, + "fill": True, + }, + ) + + # Load the generator image + generator = load_plot_image("generator.png") + + # Display the generator image over the figure, not the axes + new_ax = axis.inset_axes( + (0.96, 0.0, 0.15, 0.15), transform=axis.transAxes, zorder=10 + ) + new_ax.imshow(generator) + new_ax.axis("off") + + # Generator to gross electric power + draw_annotation( + axis, + "", + xy=(0.745, 0.17), + xytext=(0.79, 0.17), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Generator labels + draw_text( + axis, + 0.79, + 0.16, + "Generator", + **text_layout(fig), + zorder=20, + ) + + # Connector from turbine to generator + draw_annotation( + axis, + "", + xy=(0.85, 0.17), + xytext=(0.925, 0.17), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-", + "color": "black", + "linewidth": 7.0, + "zorder": 5, + "fill": True, + }, + ) + + # Turbine to loss power + draw_annotation( + axis, + "", + xy=(0.91, 0.08), + xytext=(0.91, 0.13), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + "linestyle": "dashed", + }, + ) + + # Load the pylon image + pylon = load_plot_image("pylon.png") + + # Display the pylon image over the figure, not the axes + new_ax = axis.inset_axes( + (0.925, -0.1, 0.1, 0.1), transform=axis.transAxes, zorder=10 + ) + new_ax.imshow(pylon) + new_ax.axis("off") + + # Plot the gross electric power box + draw_text( + axis, + 0.68, + 0.15, + f"$P_{{\\text{{gross}}}}$:\n{mfile.get('p_plant_electric_gross_mw', scan=scan):,.2f} MWe", # noqa: E501 + **text_layout(fig), + bbox=box_style("lime"), + ) + + # Gross to net electric power + draw_annotation( + axis, + "", + xy=(0.72, 0.08), + xytext=(0.72, 0.15), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Plot the turbine loss box + draw_text( + axis, + 0.875, + 0.05, + f"$P_{{\\text{{loss}}}}$:\n{mfile.get('p_turbine_loss_mw', scan=scan):,.2f}" + " MWth", + **text_layout(fig), + bbox=box_style("orange") | {"linestyle": "dashed"}, + ) + + # Shield primary thermal to plant total primary thermal arrow + draw_annotation( + axis, + "", + xy=(0.95, 0.3), + xytext=(0.95, 0.55), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "orange", + "linewidth": 3.0, + "zorder": 5, + "fill": True, + }, + ) + + # Plot the net electric power box + draw_text( + axis, + 0.68, + 0.05, + f"$P_{{\\text{{net,electric}}}}$:\n{mfile.get('p_plant_electric_net_mw', scan=scan):,.2f} MWe", # noqa: E501 + **text_layout(fig), + bbox=box_style("lime"), + ) + + # Plot the recirculated electric power box + draw_text( + axis, + 0.575, + 0.14, + f"$P_{{\\text{{recirc,electric}}}}$:\n{mfile.get('p_plant_electric_recirc_mw', scan=scan):,.2f}" # noqa: E501 + " MWe\n" + f"$f_{{\\text{{recirc}}}}$:\n{mfile.get('f_p_plant_electric_recirc', scan=scan):,.2f}", # noqa: E501 + **text_layout(fig), + bbox=box_style("lime"), + ) + + # Gross to recirculated power arrow + draw_annotation( + axis, + "", + xy=(0.64, 0.17), + xytext=(0.675, 0.17), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Recirculated to pumps electric + draw_annotation( + axis, + "", + xy=(0.7, 0.225), + xytext=(0.645, 0.185), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Recirculated power to HCD secondary electric arrow bend + draw_annotation( + axis, + "", + xy=(0.14, 0.2), + xytext=(0.57, 0.2), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-", + "color": "black", + "linewidth": 1.5, + "zorder": 5, + "fill": True, + }, + ) + + # Recirculated power to HCD primary electric arrow bend + draw_annotation( + axis, + "", + xy=(0.08, 0.18), + xytext=(0.57, 0.18), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-", + "color": "black", + "linewidth": 1.5, + "zorder": 5, + "fill": True, + }, + ) + + # Arrow to primary HCD electric from bend + draw_annotation( + axis, + "", + xy=(0.08, 0.35), + xytext=(0.08, 0.1775), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 1.5, + "zorder": 5, + "fill": True, + }, + ) + + # Arrow to secondary HCD electric from bend + draw_annotation( + axis, + "", + xy=(0.14, 0.35), + xytext=(0.14, 0.2), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 1.5, + "zorder": 5, + "fill": True, + }, + ) + + # ========================================== + + # ================================ + # First wall, blanket and shield + # ================================ + + # Load the first wall image + fw = load_plot_image("fw.png") + + # Display the first wall image over the figure, not the axes + new_ax = axis.inset_axes((0.4, 0.625, 0.4, 0.4), transform=axis.transAxes, zorder=10) + new_ax.imshow(fw) + new_ax.axis("off") + + # Add first wall label above image + draw_text( + axis, + 0.5, + 0.9, + "First Wall", + fontsize=11, + verticalalignment="bottom", + horizontalalignment="left", + transform=fig.transFigure, + ) + + # Alpha power incident on first wall box + draw_text( + axis, + 0.46, + 0.85, + "$P_{\\text{FW," + f" }}\\alpha}}$:\n{mfile.get('p_fw_alpha_mw', scan=scan):.2f} MW", + **text_layout(fig), + bbox=box_style("red"), + ) + + # Neutron power incident on first wall box + draw_text( + axis, + 0.46, + 0.775, + f"$P_{{\\text{{FW,nuclear}}}}$:\n{mfile.get('p_fw_nuclear_heat_total_mw', scan=scan):,.2f} MW", # noqa: E501 + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "grey", + "alpha": 0.8, + "linewidth": 2, + }, + ) + + # Plot radiation power incident on first wall box + draw_text( + axis, + 0.46, + 0.71, + f"$P_{{\\text{{FW,rad}}}}$:\n{mfile.get('p_fw_rad_total_mw', scan=scan):,.2f} MW", # noqa: E501 + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "dodgerblue", + "alpha": 0.8, + "linewidth": 2, + }, + ) + + # Draw arrow from FW to heat depsoited box + draw_annotation( + axis, + "", + xy=(0.61, 0.585), + xytext=(0.61, 0.65), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "orange", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Draw arrow from Blanket to heat deposited box + draw_annotation( + axis, + "", + xy=(0.81, 0.585), + xytext=(0.81, 0.63), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "orange", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Draw arrow from shield to heat deposited box + draw_annotation( + axis, + "", + xy=(0.92, 0.59), + xytext=(0.92, 0.62), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "orange", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # First wall heat deposited box + draw_text( + axis, + 0.5, + 0.555, + "Primary thermal\n(inc pump):" + f" {mfile.get('p_fw_heat_deposited_mw', scan=scan):,.2f} MWth", + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "orange", + "linewidth": 2, + }, + ) + + # Blanket heat deposited box + draw_text( + axis, + 0.7, + 0.555, + "Primary thermal\n(inc pump):" + f" {mfile.get('p_blkt_heat_deposited_mw', scan=scan):,.2f} MWth", + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "orange", + "linewidth": 2, + }, + ) + + # Shield heat deposited box + draw_text( + axis, + 0.875, + 0.555, + f"Primary thermal:\n{mfile.get('p_shld_heat_deposited_mw', scan=scan):.2f} MWth", + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "orange", + "linewidth": 2, + }, + ) + + # Draw arrow from FW primary heat box to blanket and FW primary heat deposited box + draw_annotation( + axis, + "", + xy=(0.65, 0.52), + xytext=(0.62, 0.55), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "orange", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Draw arrow from blanket primary heat box to blanket and FW primary heat deposited + # box + draw_annotation( + axis, + "", + xy=(0.68, 0.52), + xytext=(0.7, 0.55), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "orange", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Draw a downward arrow from the primary thermal box to the right side of the + # generator + draw_annotation( + axis, + "", + xy=(0.825, 0.57), + xytext=(0.87, 0.57), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-", + "color": "orange", + "linewidth": 3.0, + "zorder": 5, + "fill": True, + }, + ) + + # Connect blanket thermal heat deposited to the shield heat deposited + draw_annotation( + axis, + "", + xy=(0.625, 0.57), + xytext=(0.695, 0.57), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-", + "color": "orange", + "linewidth": 3.0, + "zorder": 5, + "fill": True, + }, + ) + + # Connect first wall thermal heat deposited to the blanket heat deposited + draw_annotation( + axis, + "", + xy=(0.56, 0.52), + xytext=(0.56, 0.55), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-", + "color": "orange", + "linewidth": 3.0, + "zorder": 5, + "fill": True, + }, + ) + + # FW and blanket heat deposited box + draw_text( + axis, + 0.6, + 0.49, + "Primary thermal (inc pump):" + f" {mfile.get('p_fw_blkt_heat_deposited_mw', scan=scan):,.2f} MWth\n", + **text_layout(fig), + bbox=box_style("orange"), + ) + + # Load the blanket image + blanket = load_plot_image("blanket_with_coolant.png") + + # Display the blanket image over the figure, not the axes + new_ax = axis.inset_axes( + (0.75, 0.625, 0.4, 0.4), transform=axis.transAxes, zorder=10 + ) + new_ax.imshow(blanket) + new_ax.axis("off") + + # Add blanket label above image + draw_text( + axis, + 0.7, + 0.9, + "Blanket", + fontsize=11, + verticalalignment="bottom", + horizontalalignment="left", + transform=fig.transFigure, + ) + + # Plot the nuclear heat total from blanket + draw_text( + axis, + 0.625, + 0.775, + f"$P_{{\\text{{Blkt,nuclear}}}}$:\n{mfile.get('p_blkt_nuclear_heat_total_mw', scan=scan):,.2f}" # noqa: E501 + " MW\n" + f"$P_{{\\text{{Blkt,multiplication}}}}$:\n{mfile.get('p_blkt_multiplication_mw', scan=scan):,.2f}" # noqa: E501 + " MW\n" + f"$f_{{\\text{{multiplication}}}}$:\n{mfile.get('f_p_blkt_multiplication', scan=scan):,.2f}", # noqa: E501 + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "grey", + "alpha": 0.8, + "linewidth": 2, + }, + ) + + # Load the vacuum vessel image + vv = load_plot_image("vv.png") + + # Display the vacuum vessel image over the figure, not the axes + new_ax = axis.inset_axes( + (0.975, 0.625, 0.4, 0.4), transform=axis.transAxes, zorder=10 + ) + new_ax.imshow(vv) + new_ax.axis("off") + + # Add vacuum vessel label above image + draw_text( + axis, + 0.85, + 0.9, + "Vacuum Vessel", + fontsize=11, + verticalalignment="bottom", + horizontalalignment="left", + transform=fig.transFigure, + ) + + # Plot the secondary heat from the shield + draw_text( + axis, + 0.38, + 0.375, + f"$P_{{\\text{{shld,secondary}}}}$:\n{mfile.get('p_shld_secondary_heat_mw', scan=scan):,.2f}" # noqa: E501 + " MWth", + **text_layout(fig), + bbox=box_style("lightblue") | {"linestyle": "dashed"}, + ) + + # Shield secondary power box to secondary heat total + draw_annotation( + axis, + "", + xy=(0.4, 0.3), + xytext=(0.4, 0.37), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + "linestyle": "--", + }, + ) + + # Arrow from shield bend to sheidl secondary heat + draw_annotation( + axis, + "", + xy=(0.445, 0.39), + xytext=(0.85, 0.39), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + "linestyle": "--", + }, + ) + + # Line from shield to arrow bend for secondary heat + draw_annotation( + axis, + "", + xy=(0.85, 0.385), + xytext=(0.85, 0.625), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + "linestyle": "--", + }, + ) + + # ============================================ + # Divertor + # ============================================ + + draw_text( + axis, + 0.325, + 0.48, + "Divertor", + transform=fig.transFigure, + horizontalalignment="left", + verticalalignment="bottom", + zorder=1000, # bring to front + fontsize=11, + color="white", # make text white + ) + + # Load the divertor image + divertor = load_plot_image("divertor.png") + + # Display the divertor image over the figure, not the axes + new_ax = axis.inset_axes((0.1, 0.4, 0.3, 0.25), transform=axis.transAxes, zorder=10) + new_ax.imshow(divertor) + new_ax.axis("off") + + # Total divertor radiation power box + draw_text( + axis, + 0.29, + 0.57, + f"$P_{{\\text{{div,rad}}}}$:\n{mfile.get('p_div_rad_total_mw', scan=scan):,.2f} MW", # noqa: E501 + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "dodgerblue", + "alpha": 0.8, + "linewidth": 2, + }, + ) + + # Divertor nuclear heat total box + draw_text( + axis, + 0.4, + 0.58, + f"$P_{{\\text{{div,nuclear}}}}$:\n{mfile.get('p_div_nuclear_heat_total_mw', scan=scan):,.2f} MW", # noqa: E501 + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "grey", + "alpha": 0.8, + "linewidth": 2, + }, + ) + + # Divertor primary thermal heat deposited box + draw_text( + axis, + 0.44, + 0.46, + "Primary thermal (inc" + f" pump):\n{mfile.get('p_div_heat_deposited_mw', scan=scan):.2f}" + " MWth\nSolid angle fraction:" + f" {mfile.get('f_ster_div_single', scan=scan):.3f}\nPrimary heat" + f" fraction: {mfile.get('f_p_div_primary_heat', scan=scan):.3f}", + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "orange", + "linewidth": 2, + }, + zorder=100, + ) + + # Divertor secondary heat box + draw_text( + axis, + 0.3, + 0.375, + f"$P_{{\\text{{div,secondary}}}}$:\n{mfile.get('p_div_secondary_heat_mw', scan=scan):.2f}" # noqa: E501 + " MWth", + **text_layout(fig), + bbox=box_style("lightblue") | {"linestyle": "dashed"}, + ) + + # Divertor to divertor secondary heat arrow + draw_annotation( + axis, + "", + xy=(0.33, 0.405), + xytext=(0.33, 0.5), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + "linestyle": "--", + }, + ) + + # Divertor to divertor primary thermal heat arrow + draw_annotation( + axis, + "", + xy=(0.445, 0.5), + xytext=(0.4, 0.5), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "orange", + "linewidth": 2.0, + "zorder": 50, + "fill": True, + }, + ) + + # Divertor secondary heat to total secondary heat arrow + draw_annotation( + axis, + "", + xy=(0.33, 0.3), + xytext=(0.33, 0.375), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + "linestyle": "--", + }, + ) + + # =========================================== + + # =========================================== + # Coolant pumps + # =========================================== + + # Divertor coolant pump box + draw_text( + axis, + 0.55, + 0.33, + "$P_{\\text{div,pump}}$:" + f" {mfile.get('p_div_coolant_pump_mw', scan=scan):.2f} MW", + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "wheat", + "alpha": 0.8, + "linewidth": 2, + }, + ) + + # Divertor pump box to divertor primary heat deposited box + draw_annotation( + axis, + "", + xy=(0.57, 0.46), + xytext=(0.57, 0.35), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 3.0, + "zorder": 5, + "fill": True, + }, + ) + + # Coolant pumps total to divertor pump box + draw_annotation( + axis, + "", + xy=(0.64, 0.34), + xytext=(0.7, 0.34), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Pumps total to shield bump box arrow + draw_annotation( + axis, + "", + xy=(0.875, 0.34), + xytext=(0.81, 0.34), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Shield coolant pump box + draw_text( + axis, + 0.875, + 0.325, + f"$P_{{\\text{{shld,pump}}}}$:\n{mfile.get('p_shld_coolant_pump_mw', scan=scan):.2f} MW", # noqa: E501 + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "wheat", + "alpha": 0.8, + "linewidth": 2, + }, + ) + + # FW and Blanket coolant pumps total + draw_text( + axis, + 0.725, + 0.4, + "$P_{\\text{FW +" + f" Blkt}}}}$:\n{mfile.get('p_fw_blkt_coolant_pump_mw', scan=scan):.2f} MW", + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "wheat", + "alpha": 0.8, + "linewidth": 2, + }, + ) + + # FW and Blanket coolant pumps total to FW and Blanket heat deposited box + draw_annotation( + axis, + "", + xy=(0.75, 0.49), + xytext=(0.75, 0.44), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 3.0, + "zorder": 5, + "fill": True, + }, + ) + + # Coolant pumps total to blanket and FW pump + draw_annotation( + axis, + "", + xy=(0.75, 0.4), + xytext=(0.75, 0.36), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Shield pump to sheild primary thermal + draw_annotation( + axis, + "", + xy=(0.9, 0.54), + xytext=(0.9, 0.36), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Coolant pumps total electric box + draw_text( + axis, + 0.7, + 0.225, + "Coolant pumps" + f" electric:\n{mfile.get('p_coolant_pump_elec_total_mw', scan=scan):.3f}" + " MWe\n$\\eta$:" + f" {mfile.get('eta_coolant_pump_electric', scan=scan):.3f}", + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "lime", + "alpha": 0.8, + "linewidth": 2, + }, + ) + + # Coolant pumps total + draw_text( + axis, + 0.7, + 0.325, + "Coolant pumps" + f" total:\n{mfile.get('p_coolant_pump_total_mw', scan=scan):.3f} MW", + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "wheat", + "alpha": 0.8, + "linewidth": 2, + }, + ) + + # Electric recirculated to pumps total arrow + draw_annotation( + axis, + "", + xy=(0.75, 0.325), + xytext=(0.75, 0.275), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Coolant pumps losses total box + draw_text( + axis, + 0.5, + 0.235, + "Coolant pumps losses" + f" total:\n{mfile.get('p_coolant_pump_loss_total_mw', scan=scan):.3f}" + " MWth", + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "lightblue", + "alpha": 0.8, + "linewidth": 2, + "linestyle": "dashed", + }, + ) + + # Coolant electric to pump losses arrow + draw_annotation( + axis, + "", + xy=(0.645, 0.25), + xytext=(0.695, 0.25), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + "linestyle": "--", + }, + ) + + # Coolant losses to secondary heat total arrow + draw_annotation( + axis, + "", + xy=(0.405, 0.25), + xytext=(0.4975, 0.25), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + "linestyle": "--", + }, + ) + + # ============================================ + + # =========================================== + # Plant core systems + # =========================================== + + # Cryo Plant box + draw_text( + axis, + 0.49, + 0.05, + f"Cryo Plant:\n{mfile.get('p_cryo_plant_electric_mw', scan=scan):.3f} MWe", + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "burlywood", + "alpha": 0.8, + "linewidth": 2, + }, + ) + + # Recirculated power to cryo plant arrow + draw_annotation( + axis, + "", + xy=(0.525, 0.075), + xytext=(0.525, 0.1625), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Tritium Plant box + draw_text( + axis, + 0.4, + 0.05, + f"Tritium Plant:\n{mfile.get('p_tritium_plant_electric_mw', scan=scan):.3f} MWe", + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "burlywood", + "alpha": 0.8, + "linewidth": 2, + }, + ) + + # # Recirculated power to tritium plant arrow + draw_annotation( + axis, + "", + xy=(0.44, 0.075), + xytext=(0.44, 0.1625), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Vacuum Pumps box + draw_text( + axis, + 0.575, + 0.05, + f"Vacuum pumps:\n{mfile.get('vachtmw', scan=scan):.3f} MWe", + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "burlywood", + "alpha": 0.8, + "linewidth": 2, + }, + ) + + # Recirculated power to vacuum pumps arrow + draw_annotation( + axis, + "", + xy=(0.62, 0.08), + xytext=(0.62, 0.1375), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Plant base load box + draw_text( + axis, + 0.085, + 0.075, + "Plant base" + f" load:\n{mfile.get('p_plant_electric_base_total_mw', scan=scan):.3f}" + " MWe\nMinimum base" + f" load:\n{mfile.get('p_plant_electric_base', scan=scan) * 1.0e-6:.3f}" + " MWe\nPlant floor power" + f" density:\n{mfile.get('pflux_plant_floor_electric', scan=scan) * 1.0e-3:.3f}" + " kW$\\text{m}^{-2}$", + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "burlywood", + "alpha": 0.8, + "linewidth": 2, + }, + ) + + # TF coil power box + draw_text( + axis, + 0.325, + 0.075, + f"TF coils:\n{mfile.get('p_tf_electric_supplies_mw', scan=scan):.3f} MWe", + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "burlywood", + "alpha": 0.8, + "linewidth": 2, + }, + ) + + # PF coil power box + draw_text( + axis, + 0.25, + 0.05, + f"PF coils:\n{mfile.get('p_pf_electric_supplies_mw', scan=scan):.3f} MWe", + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "burlywood", + "alpha": 0.8, + "linewidth": 2, + }, + ) + + # Recirculated power to TF,PF and plant base arrow bend + draw_annotation( + axis, + "", + xy=(0.22, 0.16), + xytext=(0.574, 0.16), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 1.5, + "zorder": 5, + "fill": True, + }, + ) + + # Recirculated power to PF + draw_annotation( + axis, + "", + xy=(0.28, 0.075), + xytext=(0.28, 0.1625), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # Recirculated power to TF + draw_annotation( + axis, + "", + xy=(0.35, 0.1), + xytext=(0.35, 0.1625), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + }, + ) + + # HCD secondary heat box + draw_text( + axis, + 0.46, + 0.285, + f"$P_{{\\text{{HCD,loss}}}}$:\n{mfile.get('p_hcd_secondary_heat_mw', scan=scan):.2f}" # noqa: E501 + " MWth", + **text_layout(fig), + bbox={ + "boxstyle": "round", + "facecolor": "lightblue", + "alpha": 0.8, + "linewidth": 2, + "linestyle": "dashed", + }, + ) + + # FW to HCD secondary heat arrow + draw_annotation( + axis, + "", + xy=(0.47, 0.32), + xytext=(0.47, 0.65), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + "linestyle": "--", + }, + ) + + # HCD loss to total secondary heat + draw_annotation( + axis, + "", + xy=(0.41, 0.295), + xytext=(0.455, 0.295), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + "linestyle": "--", + }, + ) + + # TF nuclear heat box + draw_text( + axis, + 0.155, + 0.25, + f"$P_{{\\text{{TF,nuclear}}}}$:\n{mfile.get('p_tf_nuclear_heat_mw', scan=scan):.2f}" # noqa: E501 + " MWth", + **text_layout(fig), + bbox=box_style("lightblue") | {"linestyle": "dashed"}, + ) + + # TF nuclear heat to secondary heat total box arrow + draw_annotation( + axis, + "", + xy=(0.245, 0.265), + xytext=(0.215, 0.265), + xycoords=fig.transFigure, + arrowprops={ + "arrowstyle": "-|>,head_length=1,head_width=0.3", + "color": "black", + "linewidth": 2.0, + "zorder": 5, + "fill": True, + "linestyle": "--", + }, + ) + + +def plot_power_info(axis: plt.Axes, mfile: MFile, scan: int): + """Function to plot power info + + Parameters + ---------- + axis : + axis object to plot to + mfile : + MFILE + scan : + scan number to use + """ + draw_text(axis, -0.05, 1, "Power flows:", ha="left", va="center") + setup_axis(axis, xmin=0, xmax=1, ymin=-16, ymax=1) + + gross_eff = 100.0 * ( + mfile.get("p_plant_electric_gross_mw", scan=scan) + / mfile.get("p_plant_primary_heat_mw", scan=scan) + ) + + net_eff = 100.0 * ( + ( + mfile.get("p_plant_electric_gross_mw", scan=scan) + - mfile.get("p_coolant_pump_elec_total_mw", scan=scan) + ) + / ( + mfile.get("p_plant_primary_heat_mw", scan=scan) + - mfile.get("p_coolant_pump_elec_total_mw", scan=scan) + ) + ) + + plant_eff = 100.0 * ( + mfile.get("p_plant_electric_net_mw", scan=scan) + / mfile.get("p_fusion_total_mw", scan=scan) + ) + + # Define appropriate pedestal and impurity parameters + coredescription = ( + "radius_plasma_core_norm", + "Normalised radius of 'core' region", + "", + ) + if mfile.get("i_plasma_pedestal", scan=scan) == 1: + ped_height = ( + "nd_plasma_pedestal_electron", + "Electron density at pedestal", + "m$^{-3}$", + ) + ped_pos = ( + "radius_plasma_pedestal_density_norm", + "r/a at density pedestal", + "", + ) + else: + ped_height = ("", "No pedestal model used", "") + ped_pos = ("", "", "") + + p_cryo_plant_electric_mw = mfile.get("p_cryo_plant_electric_mw", scan=scan) + + data = [ + ("pflux_fw_neutron_mw", "Nominal neutron wall load", "MW m$^{-2}$"), + coredescription, + ped_height, + ped_pos, + ("p_plasma_inner_rad_mw", "Inner zone radiation", "MW"), + ("p_plasma_rad_mw", "Total radiation in LCFS", "MW"), + ("p_blkt_nuclear_heat_total_mw", "Nuclear heating in blanket", "MW"), + ("p_shld_nuclear_heat_mw", "Nuclear heating in shield", "MW"), + (p_cryo_plant_electric_mw, "TF cryogenic power", "MW"), + ("p_plasma_separatrix_mw", "Power to divertor", "MW"), + ("life_div_fpy", "Divertor life", "years"), + ("p_plant_primary_heat_mw", "Primary (high grade) heat", "MW"), + (gross_eff, "Gross cycle efficiency", "%"), + (net_eff, "Net cycle efficiency", "%"), + ("p_plant_electric_gross_mw", "Gross electric power", "MW"), + ("p_plant_electric_net_mw", "Net electric power", "MW"), + ( + plant_eff, + ( + r"Fusion-to-electric efficiency" + r" $\frac{P_{\mathrm{e,net}}}{P_{\mathrm{fus}}}$" + ), + "%", + ), + ] + + plot_info(axis, data, mfile, scan) + + +__all__ = ["plot_main_power_flow", "plot_power_info"] diff --git a/process/core/io/plot/summary/profiles/__init__.py b/process/core/io/plot/summary/profiles/__init__.py new file mode 100644 index 0000000000..650be21144 --- /dev/null +++ b/process/core/io/plot/summary/profiles/__init__.py @@ -0,0 +1,104 @@ +"""Public API for this summary plotting concern.""" + +from __future__ import annotations + +import process.core.io.plot.summary.profiles.atomic as _atomic +import process.core.io.plot.summary.profiles.misc as _misc +import process.core.io.plot.summary.profiles.plasma as _plasma +import process.core.io.plot.summary.profiles.radiation as _radiation +import process.core.io.plot.summary.profiles.stress as _stress + +_MODULES = (_atomic, _misc, _plasma, _radiation, _stress) +_REGISTRY = {} +for _module in _MODULES: + _REGISTRY.update({name: getattr(_module, name) for name in _module.__all__}) +for _module in _MODULES: + _module.__dict__.update(_REGISTRY) +interp1d_profile = _REGISTRY["interp1d_profile"] +plot_beta_profiles = _REGISTRY["plot_beta_profiles"] +plot_collision_frequency_profile = _REGISTRY["plot_collision_frequency_profile"] +plot_collision_time_profile = _REGISTRY["plot_collision_time_profile"] +plot_cs_hoop_stress_contour_profile = _REGISTRY["plot_cs_hoop_stress_contour_profile"] +plot_cs_hoop_stress_profile = _REGISTRY["plot_cs_hoop_stress_profile"] +plot_cs_radial_stress_contour_profile = _REGISTRY[ + "plot_cs_radial_stress_contour_profile" +] +plot_cs_radial_stress_profile = _REGISTRY["plot_cs_radial_stress_profile"] +plot_cs_stress_time_profile = _REGISTRY["plot_cs_stress_time_profile"] +plot_cs_tresca_2d_contour = _REGISTRY["plot_cs_tresca_2d_contour"] +plot_cs_vertical_stress_profile = _REGISTRY["plot_cs_vertical_stress_profile"] +plot_cs_von_mises_2d_contour = _REGISTRY["plot_cs_von_mises_2d_contour"] +plot_cumulative_plasma_thermal_energy_profiles = _REGISTRY[ + "plot_cumulative_plasma_thermal_energy_profiles" +] +plot_debye_length_profile = _REGISTRY["plot_debye_length_profile"] +plot_electron_frequency_profile = _REGISTRY["plot_electron_frequency_profile"] +plot_fusion_rate_contours = _REGISTRY["plot_fusion_rate_contours"] +plot_fusion_rate_profiles = _REGISTRY["plot_fusion_rate_profiles"] +plot_ion_charge_profile = _REGISTRY["plot_ion_charge_profile"] +plot_ion_frequency_profile = _REGISTRY["plot_ion_frequency_profile"] +plot_ion_slowing_down_time_profile = _REGISTRY["plot_ion_slowing_down_time_profile"] +plot_jprofile = _REGISTRY["plot_jprofile"] +plot_larmor_radius_profile = _REGISTRY["plot_larmor_radius_profile"] +plot_line_brem_loss_function_profile = _REGISTRY["plot_line_brem_loss_function_profile"] +plot_line_brem_power_density_profile = _REGISTRY["plot_line_brem_power_density_profile"] +plot_mean_free_path_profile = _REGISTRY["plot_mean_free_path_profile"] +plot_n_profiles = _REGISTRY["plot_n_profiles"] +plot_plasma_effective_charge_profile = _REGISTRY["plot_plasma_effective_charge_profile"] +plot_plasma_poloidal_pressure_contours = _REGISTRY[ + "plot_plasma_poloidal_pressure_contours" +] +plot_plasma_pressure_gradient_profiles = _REGISTRY[ + "plot_plasma_pressure_gradient_profiles" +] +plot_plasma_pressure_profiles = _REGISTRY["plot_plasma_pressure_profiles"] +plot_plasma_thermal_energy_profiles = _REGISTRY["plot_plasma_thermal_energy_profiles"] +plot_qprofile = _REGISTRY["plot_qprofile"] +plot_rad_contour = _REGISTRY["plot_rad_contour"] +plot_resistivity_profile = _REGISTRY["plot_resistivity_profile"] +plot_t_profiles = _REGISTRY["plot_t_profiles"] +plot_velocity_profile = _REGISTRY["plot_velocity_profile"] +plot_vertical_stress_contour_profile = _REGISTRY["plot_vertical_stress_contour_profile"] +profiles_with_pedestal = _REGISTRY["profiles_with_pedestal"] +read_imprad_data = _REGISTRY["read_imprad_data"] +__all__ = [ + "interp1d_profile", + "plot_beta_profiles", + "plot_collision_frequency_profile", + "plot_collision_time_profile", + "plot_cs_hoop_stress_contour_profile", + "plot_cs_hoop_stress_profile", + "plot_cs_radial_stress_contour_profile", + "plot_cs_radial_stress_profile", + "plot_cs_stress_time_profile", + "plot_cs_tresca_2d_contour", + "plot_cs_vertical_stress_profile", + "plot_cs_von_mises_2d_contour", + "plot_cumulative_plasma_thermal_energy_profiles", + "plot_debye_length_profile", + "plot_electron_frequency_profile", + "plot_fusion_rate_contours", + "plot_fusion_rate_profiles", + "plot_ion_charge_profile", + "plot_ion_frequency_profile", + "plot_ion_slowing_down_time_profile", + "plot_jprofile", + "plot_larmor_radius_profile", + "plot_line_brem_loss_function_profile", + "plot_line_brem_power_density_profile", + "plot_mean_free_path_profile", + "plot_n_profiles", + "plot_plasma_effective_charge_profile", + "plot_plasma_poloidal_pressure_contours", + "plot_plasma_pressure_gradient_profiles", + "plot_plasma_pressure_profiles", + "plot_plasma_thermal_energy_profiles", + "plot_qprofile", + "plot_rad_contour", + "plot_resistivity_profile", + "plot_t_profiles", + "plot_velocity_profile", + "plot_vertical_stress_contour_profile", + "profiles_with_pedestal", + "read_imprad_data", +] diff --git a/process/core/io/plot/summary/profiles/atomic.py b/process/core/io/plot/summary/profiles/atomic.py new file mode 100644 index 0000000000..c9c8c95ce9 --- /dev/null +++ b/process/core/io/plot/summary/profiles/atomic.py @@ -0,0 +1,589 @@ +"""Profiles functions for PROCESS summary plots.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np + +from process.data_structure.impurity_radiation_variables import N_IMPURITIES + +if TYPE_CHECKING: + import matplotlib.pyplot as plt + + from process.core.io.mfile import MFile + + +def plot_ion_charge_profile(axis: plt.Axes, mfile: MFile, scan: int): + """Plot ion charge profile""" + n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) + + # find impurity densities + imp_frac = np.array([ + mfile.get("f_nd_impurity_electrons(01)", scan=scan), + mfile.get("f_nd_impurity_electrons(02)", scan=scan), + mfile.get("f_nd_impurity_electrons(03)", scan=scan), + mfile.get("f_nd_impurity_electrons(04)", scan=scan), + mfile.get("f_nd_impurity_electrons(05)", scan=scan), + mfile.get("f_nd_impurity_electrons(06)", scan=scan), + mfile.get("f_nd_impurity_electrons(07)", scan=scan), + mfile.get("f_nd_impurity_electrons(08)", scan=scan), + mfile.get("f_nd_impurity_electrons(09)", scan=scan), + mfile.get("f_nd_impurity_electrons(10)", scan=scan), + mfile.get("f_nd_impurity_electrons(11)", scan=scan), + mfile.get("f_nd_impurity_electrons(12)", scan=scan), + mfile.get("f_nd_impurity_electrons(13)", scan=scan), + mfile.get("f_nd_impurity_electrons(14)", scan=scan), + ]) + + imp_label = [ + "H", + "He", + "Be", + "C", + "N", + "O", + "Ne", + "Si", + "Ar", + "Fe", + "Ni", + "Kr", + "Xe", + "W", + ] + full_charge_array = [1, 2, 4, 6, 7, 8, 10, 14, 18, 26, 28, 36, 54, 74] + + n_charge_plasma_profile = [] + for imp in range(N_IMPURITIES): + if imp_frac[imp] > 1.0e-30: + profile = [ + mfile.get(f"n_charge_plasma_profile{imp}_{i}", scan=scan) + for i in range(n_plasma_profile_elements) + ] + n_charge_plasma_profile.append(profile) + z_max = full_charge_array[imp] + # Calculate relative ionisation state as percent of full ionisation + rel_ion_state = [ + 100.0 * (val / z_max if z_max > 0 else 0) for val in profile + ] + avg_ionisation = np.mean(rel_ion_state) + axis.plot( + np.linspace(0, 1, n_plasma_profile_elements), + rel_ion_state, + label=(f"{imp_label[imp]} (Z={z_max}): avg {avg_ionisation:.1f}%"), + ) + axis.set_ylabel("Relative Ionisation State [% of $Z$]") + axis.legend() + axis.set_xlim(0, 1.025) + axis.set_xlabel(r"$\rho \quad [r/a]$") + axis.set_title("Impurity Ion Charge State Profiles") + axis.minorticks_on() + axis.grid(which="both", linestyle="--", alpha=0.5) + + +def plot_debye_length_profile(axis: plt.Axes, mfile_data: MFile, scan: int): + """Plot the Debye length profile on the given axis.""" + len_plasma_debye_electron_profile = [ + mfile_data.data[f"len_plasma_debye_electron_profile{i}"].get_scan(scan) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + + # Convert to micrometres (1e-6 m) + len_plasma_debye_electron_profile_um = [ + length * 1e6 for length in len_plasma_debye_electron_profile + ] + + axis.plot( + np.linspace(0, 1, len(len_plasma_debye_electron_profile_um)), + len_plasma_debye_electron_profile_um, + color="blue", + linestyle="-", + label=r"$\lambda_{Debye,e}$", + ) + + axis.set_ylabel(r"Debye Length [$\mu$m]") + + axis.set_xlabel("$\\rho \\ [r/a]$") + axis.grid(True, which="both", linestyle="--", alpha=0.5) + axis.set_xlim(0, 1.025) + axis.minorticks_on() + axis.legend() + + +def plot_velocity_profile(axis: plt.Axes, mfile_data: MFile, scan: int) -> None: + """Plot the electron thermal velocity profile on the given axis.""" + vel_plasma_electron_profile = [ + mfile_data.data[f"vel_plasma_electron_profile{i}"].get_scan(scan) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + vel_plasma_deuteron_profile = [ + mfile_data.data[f"vel_plasma_deuteron_profile{i}"].get_scan(scan) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + vel_plasma_triton_profile = [ + mfile_data.data[f"vel_plasma_triton_profile{i}"].get_scan(scan) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + vel_plasma_alpha_thermal_profile = [ + mfile_data.data[f"vel_plasma_alpha_thermal_profile{i}"].get_scan(scan) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + + vel_plasma_alpha_birth = mfile_data.data["vel_plasma_alpha_birth"].get_scan(scan) + + axis.plot( + np.linspace(0, 1, len(vel_plasma_electron_profile)), + vel_plasma_electron_profile, + color="blue", + linestyle="-", + label=r"$v_{e}$", + ) + axis.plot( + np.linspace(0, 1, len(vel_plasma_deuteron_profile)), + vel_plasma_deuteron_profile, + color="pink", + linestyle="-", + label=r"$v_{D}$", + ) + axis.plot( + np.linspace(0, 1, len(vel_plasma_triton_profile)), + vel_plasma_triton_profile, + color="green", + linestyle="-", + label=r"$v_{T}$", + ) + axis.plot( + np.linspace(0, 1, len(vel_plasma_alpha_thermal_profile)), + vel_plasma_alpha_thermal_profile, + color="red", + linestyle="-", + label=r"$v_{\alpha,thermal}$", + ) + axis.axhline( + vel_plasma_alpha_birth, + color="red", + linestyle="--", + linewidth=1.5, + label=r"$v_{\alpha,birth}$", + ) + + axis.set_yscale("log") + axis.set_ylabel("Velocity [m/s]") + axis.set_xlabel("$\\rho \\ [r/a]$") + axis.grid(True, which="both", linestyle="--", alpha=0.5) + axis.set_xlim(0, 1.025) + axis.minorticks_on() + axis.legend() + + +def plot_electron_frequency_profile( + axis: plt.Axes, mfile_data: MFile, scan: int +) -> None: + """Plot the electron thermal frequency profile on the given axis.""" + freq_plasma_electron_profile = [ + mfile_data.data[f"freq_plasma_electron_profile{i}"].get_scan(scan) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + freq_plasma_larmor_toroidal_electron_profile = [ + mfile_data.data[f"freq_plasma_larmor_toroidal_electron_profile{i}"].get_scan( + scan + ) + for i in range( + 2 * int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan)) + ) + ] + + freq_plasma_upper_hybrid_electron_profile = [ + mfile_data.data[f"freq_plasma_upper_hybrid_profile{i}"].get_scan(scan) + for i in range( + 2 * int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan)) + ) + ] + + axis.plot( + np.linspace(-1, 1, len(freq_plasma_larmor_toroidal_electron_profile)), + np.array(freq_plasma_larmor_toroidal_electron_profile) / 1e9, + color="red", + linestyle="-", + label=r"$f_{Larmor,toroidal,e}$ | Fundamental", + ) + + axis.plot( + np.linspace(-1, 1, len(freq_plasma_larmor_toroidal_electron_profile)), + 2 * np.array(freq_plasma_larmor_toroidal_electron_profile) / 1e9, + color="red", + linestyle="--", + label=r"$f_{Larmor,toroidal,e}$ | 2nd harmonic", + ) + + axis.plot( + np.linspace(-1, 1, len(freq_plasma_larmor_toroidal_electron_profile)), + 3 * np.array(freq_plasma_larmor_toroidal_electron_profile) / 1e9, + color="red", + linestyle=":", + label=r"$f_{Larmor,toroidal,e}$ | 3rd harmonic", + ) + + x = np.linspace(0, 1, len(freq_plasma_electron_profile)) + y = np.array(freq_plasma_electron_profile) / 1e9 + # original curve + axis.plot( + x, + y, + color="blue", + linestyle="-", + label=r"$\omega_{p,e}$ | Plasma Frequency", + ) + # mirrored across the y-axis (drawn at negative rho) + axis.plot(-x, y, color="blue", linestyle="-", label="_nolegend_") + + axis.plot( + np.linspace(-1, 1, len(freq_plasma_upper_hybrid_electron_profile)), + np.array(freq_plasma_upper_hybrid_electron_profile) / 1e9, + color="purple", + linestyle="-", + label=r"$\omega_{UH,e}$ | Upper Hybrid", + ) + + axis.set_xlim(-1.025, 1.025) + axis.set_ylim(None, max(freq_plasma_larmor_toroidal_electron_profile) / 1e9 * 1.6) + + axis.set_xlabel("$\\rho$ [r/a]") + axis.set_ylabel("Frequency [GHz]") + axis.grid(True, which="both", linestyle="--", alpha=0.5) + + # Add secondary x-axis showing radius in metres below the primary axis + ax2 = axis.twiny() + rmajor = mfile_data.get("rmajor", scan=scan) + rminor = mfile_data.get("rminor", scan=scan) + + # Convert normalized radius to actual radius + # rho ranges from -1 to 1, which corresponds to r = rmajor - rminor to rmajor + + # rminor + rho_ticks = np.array([-1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75, 1]) + r_ticks = rmajor + rho_ticks * rminor + + ax2.set_xticks(rho_ticks) + ax2.set_xticklabels([f"{r:.2f}" for r in r_ticks]) + ax2.set_xlabel("Radius [m]") + ax2.minorticks_on() + ax2.set_xlim(axis.get_xlim()) + + # Move secondary axis to the bottom + ax2.xaxis.set_ticks_position("bottom") + ax2.xaxis.set_label_position("bottom") + ax2.spines["bottom"].set_position(("outward", 30)) + + axis.legend() + + +def plot_ion_frequency_profile(axis: plt.Axes, mfile_data: MFile, scan: int) -> None: + """Plot the ion thermal frequency profile on the given axis.""" + freq_plasma_larmor_toroidal_deuteron_profile = [ + mfile_data.data[f"freq_plasma_larmor_toroidal_deuteron_profile{i}"].get_scan( + scan + ) + for i in range( + 2 * int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan)) + ) + ] + + freq_plasma_larmor_toroidal_triton_profile = [ + mfile_data.data[f"freq_plasma_larmor_toroidal_triton_profile{i}"].get_scan(scan) + for i in range( + 2 * int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan)) + ) + ] + + axis.plot( + np.linspace(-1, 1, len(freq_plasma_larmor_toroidal_deuteron_profile)), + np.array(freq_plasma_larmor_toroidal_deuteron_profile) / 1e6, + color="red", + linestyle="-", + label=r"$f_{Larmor,toroidal,D}$", + ) + axis.plot( + np.linspace(-1, 1, len(freq_plasma_larmor_toroidal_triton_profile)), + np.array(freq_plasma_larmor_toroidal_triton_profile) / 1e6, + color="green", + linestyle="-", + label=r"$f_{Larmor,toroidal,T}$", + ) + + axis.set_ylabel("Frequency [MHz]") + axis.set_xlabel("$\\rho \\ [r/a]$") + axis.grid(True, which="both", linestyle="--", alpha=0.5) + axis.minorticks_on() + axis.legend() + + +def plot_collision_time_profile(axis: plt.Axes, mfile_data: MFile, scan: int) -> None: + """Plot the plasma collision times on the given axis.""" + t_plasma_electron_electron_collision_profile = [ + mfile_data.data[f"t_plasma_electron_electron_collision_profile{i}"].get_scan( + scan + ) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + + t_plasma_electron_deuteron_collision_profile = [ + mfile_data.data[f"t_plasma_electron_deuteron_collision_profile{i}"].get_scan( + scan + ) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + + t_plasma_electron_triton_collision_profile = [ + mfile_data.data[f"t_plasma_electron_triton_collision_profile{i}"].get_scan(scan) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + + t_plasma_electron_alpha_thermal_collision_profile = [ + mfile_data.data[ + f"t_plasma_electron_alpha_thermal_collision_profile{i}" + ].get_scan(scan) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + + axis.plot( + np.linspace(0, 1, len(t_plasma_electron_electron_collision_profile)), + t_plasma_electron_electron_collision_profile, + color="blue", + linestyle="-", + label=r"$\tau_{e-e}$", + ) + + axis.plot( + np.linspace(0, 1, len(t_plasma_electron_deuteron_collision_profile)), + t_plasma_electron_deuteron_collision_profile, + color="pink", + linestyle="-", + label=r"$\tau_{e-D}$", + ) + + axis.plot( + np.linspace(0, 1, len(t_plasma_electron_triton_collision_profile)), + t_plasma_electron_triton_collision_profile, + color="green", + linestyle="-", + label=r"$\tau_{e-T}$", + ) + + axis.plot( + np.linspace(0, 1, len(t_plasma_electron_alpha_thermal_collision_profile)), + t_plasma_electron_alpha_thermal_collision_profile, + color="red", + linestyle="-", + label=r"$\tau_{e-\alpha,thermal}$", + ) + + axis.set_yscale("log") + axis.set_ylabel("Collision Time [s]") + axis.set_xlabel("$\\rho \\ [r/a]$") + axis.grid(True, which="both", linestyle="--", alpha=0.5) + axis.minorticks_on() + axis.legend() + + +def plot_collision_frequency_profile( + axis: plt.Axes, mfile_data: MFile, scan: int +) -> None: + """Plot the plasma collision frequencies on the given axis.""" + freq_plasma_electron_electron_collision_profile = [ + mfile_data.data[f"freq_plasma_electron_electron_collision_profile{i}"].get_scan( + scan + ) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + + freq_plasma_electron_deuteron_collision_profile = [ + mfile_data.data[f"freq_plasma_electron_deuteron_collision_profile{i}"].get_scan( + scan + ) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + + freq_plasma_electron_triton_collision_profile = [ + mfile_data.data[f"freq_plasma_electron_triton_collision_profile{i}"].get_scan( + scan + ) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + + freq_plasma_electron_alpha_thermal_collision_profile = [ + mfile_data.data[ + f"freq_plasma_electron_alpha_thermal_collision_profile{i}" + ].get_scan(scan) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + + axis.plot( + np.linspace(0, 1, len(freq_plasma_electron_electron_collision_profile)), + freq_plasma_electron_electron_collision_profile, + color="blue", + linestyle="-", + label=r"$\nu_{e-e}$", + ) + + axis.plot( + np.linspace(0, 1, len(freq_plasma_electron_deuteron_collision_profile)), + freq_plasma_electron_deuteron_collision_profile, + color="pink", + linestyle="-", + label=r"$\nu_{e-D}$", + ) + + axis.plot( + np.linspace(0, 1, len(freq_plasma_electron_triton_collision_profile)), + freq_plasma_electron_triton_collision_profile, + color="green", + linestyle="-", + label=r"$\nu_{e-T}$", + ) + + axis.plot( + np.linspace(0, 1, len(freq_plasma_electron_alpha_thermal_collision_profile)), + freq_plasma_electron_alpha_thermal_collision_profile, + color="red", + linestyle="-", + label=r"$\nu_{e-\alpha,thermal}$", + ) + axis.set_yscale("log") + axis.set_ylabel("Collision Frequency [Hz]") + axis.set_xlabel("$\\rho \\ [r/a]$") + axis.grid(True, which="both", linestyle="--", alpha=0.5) + axis.minorticks_on() + axis.legend() + + +def plot_mean_free_path_profile(axis: plt.Axes, mfile_data: MFile, scan: int) -> None: + """Plot the plasma mean free path on the given axis.""" + len_plasma_electron_electron_mean_free_path_profile = [ + mfile_data.data[ + f"len_plasma_electron_electron_mean_free_path_profile{i}" + ].get_scan(scan) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + + len_plasma_electron_deuteron_mean_free_path_profile = [ + mfile_data.data[ + f"len_plasma_electron_deuteron_mean_free_path_profile{i}" + ].get_scan(scan) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + + len_plasma_electron_triton_mean_free_path_profile = [ + mfile_data.data[ + f"len_plasma_electron_triton_mean_free_path_profile{i}" + ].get_scan(scan) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + + len_plasma_electron_alpha_thermal_mean_free_path_profile = [ + mfile_data.data[ + f"len_plasma_electron_alpha_thermal_mean_free_path_profile{i}" + ].get_scan(scan) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + + axis.plot( + np.linspace(0, 1, len(len_plasma_electron_electron_mean_free_path_profile)), + len_plasma_electron_electron_mean_free_path_profile, + color="blue", + linestyle="-", + label=r"$\lambda_{mfp,e-e}$", + ) + + axis.plot( + np.linspace(0, 1, len(len_plasma_electron_deuteron_mean_free_path_profile)), + len_plasma_electron_deuteron_mean_free_path_profile, + color="pink", + linestyle="-", + label=r"$\lambda_{mfp,e-D}$", + ) + + axis.plot( + np.linspace(0, 1, len(len_plasma_electron_triton_mean_free_path_profile)), + len_plasma_electron_triton_mean_free_path_profile, + color="green", + linestyle="-", + label=r"$\lambda_{mfp,e-T}$", + ) + axis.plot( + np.linspace(0, 1, len(len_plasma_electron_alpha_thermal_mean_free_path_profile)), + len_plasma_electron_alpha_thermal_mean_free_path_profile, + color="red", + linestyle="-", + label=r"$\lambda_{mfp,e-\alpha,thermal}$", + ) + axis.set_yscale("log") + axis.set_ylabel("Mean Free Path [m]") + axis.set_xlabel("$\\rho \\ [r/a]$") + axis.grid(True, which="both", linestyle="--", alpha=0.5) + axis.minorticks_on() + axis.legend() + + +def plot_ion_slowing_down_time_profile( + axis: plt.Axes, mfile_data: MFile, scan: int +) -> None: + """Plot the plasma Spitzer slowing down time on the given axis.""" + t_plasma_electron_alpha_spitzer_slow_profile = [ + mfile_data.data[f"t_plasma_electron_alpha_spitzer_slow_profile{i}"].get_scan( + scan + ) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + + axis.plot( + np.linspace(0, 1, len(t_plasma_electron_alpha_spitzer_slow_profile)), + t_plasma_electron_alpha_spitzer_slow_profile, + color="red", + linestyle="-", + label=r"$\tau_{e-\alpha,Spitzer}$", + ) + + axis.set_yscale("log") + axis.set_ylabel("Spitzer Slowing Down Time [s]") + axis.set_xlabel("$\\rho \\ [r/a]$") + axis.grid(True, which="both", linestyle="--", alpha=0.5) + axis.minorticks_on() + axis.legend() + + +def plot_resistivity_profile(axis: plt.Axes, mfile_data: MFile, scan: int) -> None: + """Plot the plasma resistivity on the given axis.""" + res_plasma_fuel_spitzer_profile = [ + mfile_data.data[f"res_plasma_fuel_spitzer_profile{i}"].get_scan(scan) + for i in range(int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan))) + ] + + axis.plot( + np.linspace(0, 1, len(res_plasma_fuel_spitzer_profile)), + res_plasma_fuel_spitzer_profile, + color="red", + linestyle="-", + label=r"$\eta_{Spitzer-fuel}$", + ) + + axis.set_yscale("log") + axis.set_ylabel("Resistivity [Ohm m]") + axis.set_xlabel("$\\rho \\ [r/a]$") + axis.grid(True, which="both", linestyle="--", alpha=0.5) + axis.minorticks_on() + axis.legend() + + +__all__ = [ + "plot_collision_frequency_profile", + "plot_collision_time_profile", + "plot_debye_length_profile", + "plot_electron_frequency_profile", + "plot_ion_charge_profile", + "plot_ion_frequency_profile", + "plot_ion_slowing_down_time_profile", + "plot_mean_free_path_profile", + "plot_resistivity_profile", + "plot_velocity_profile", +] diff --git a/process/core/io/plot/summary/profiles/misc.py b/process/core/io/plot/summary/profiles/misc.py new file mode 100644 index 0000000000..d3b1c2a20f --- /dev/null +++ b/process/core/io/plot/summary/profiles/misc.py @@ -0,0 +1,377 @@ +"""Profiles functions for PROCESS summary plots.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +from scipy.interpolate import interp1d + +from process.core.io.plot.summary.profiles.radiation import ( + read_imprad_data, +) +from process.core.io.plot.summary.rendering import ( + draw_text, +) +from process.models.geometry.plasma import plasma_geometry + +if TYPE_CHECKING: + import matplotlib.pyplot as plt + + from process.core.io.mfile import MFile + + +def profiles_with_pedestal(mfile, scan: int): + """Calculate profiles with pedestal""" + alphan = mfile.get("alphan", scan=scan) + alphat = mfile.get("alphat", scan=scan) + nd_plasma_electron_on_axis = mfile.get("nd_plasma_electron_on_axis", scan=scan) + temp_plasma_electron_on_axis_kev = mfile.get( + "temp_plasma_electron_on_axis_kev", scan=scan + ) + + radius_plasma_pedestal_temp_norm = mfile.get( + "radius_plasma_pedestal_temp_norm", scan=scan + ) + + n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) + i_plasma_pedestal = mfile.get("i_plasma_pedestal", scan=scan) + nd_plasma_pedestal_electron = mfile.get("nd_plasma_pedestal_electron", scan=scan) + radius_plasma_pedestal_density_norm = mfile.get( + "radius_plasma_pedestal_density_norm", scan=scan + ) + ne0 = mfile.get("nd_plasma_electron_on_axis", scan=scan) + rho = np.linspace(0, 1.0, n_plasma_profile_elements) + nd_plasma_separatrix_electron = mfile.get("nd_plasma_separatrix_electron", scan=scan) + temp_plasma_pedestal_electron_kev = mfile.get( + "temp_plasma_pedestal_electron_kev", scan=scan + ) + temp_plasma_separatrix_electron_kev = mfile.get( + "temp_plasma_separatrix_electron_kev", scan=scan + ) + tbeta = mfile.get("tbeta", scan=scan) + te0 = mfile.get("temp_plasma_electron_on_axis_kev", scan=scan) + + if i_plasma_pedestal == 0: + # Initialise the radius + + # The density profile + ne = nd_plasma_electron_on_axis * (1 - rho**2) ** alphan + + # The temperature profile + te = temp_plasma_electron_on_axis_kev * (1 - rho**2) ** alphat + + # Profiles with pedestal + elif i_plasma_pedestal == 1: + # The density and temperature profile + # Initiliase empty normalised array with zeros + ne = np.zeros_like(rho) + te = np.zeros_like(rho) + # Reconstruct the temperature and density profiles with pedestal + for q in range(rho.shape[0]): + # Core density region + if rho[q] <= radius_plasma_pedestal_density_norm: + ne[q] = ( + nd_plasma_pedestal_electron + + (ne0 - nd_plasma_pedestal_electron) + * (1 - rho[q] ** 2 / radius_plasma_pedestal_density_norm**2) + ** alphan + ) + else: + # Pedestal density region + ne[q] = nd_plasma_separatrix_electron + ( + nd_plasma_pedestal_electron - nd_plasma_separatrix_electron + ) * (1 - rho[q]) / (1 - radius_plasma_pedestal_density_norm) + + # Core temperature region + if rho[q] <= radius_plasma_pedestal_temp_norm: + te[q] = ( + temp_plasma_pedestal_electron_kev + + (te0 - temp_plasma_pedestal_electron_kev) + * (1 - (rho[q] / radius_plasma_pedestal_temp_norm) ** tbeta) + ** alphat + ) + else: + # Pedestal temperature region + te[q] = temp_plasma_separatrix_electron_kev + ( + temp_plasma_pedestal_electron_kev + - temp_plasma_separatrix_electron_kev + ) * (1 - rho[q]) / (1 - radius_plasma_pedestal_temp_norm) + + return rho, ne, te + + +def plot_line_brem_power_density_profile( + axis: plt.Axes, mfile: MFile, scan: int, impp: str, demo_ranges: bool +): + """Function to plot Line and Bremsstrahlung radiation power density profile. + + Parameters + ---------- + axis : plt.Axes + axis object to add plot to + mfile : MFile + MFile object containing plasma and impurity profile information. + scan : int + scan number to use + impp : str + impurity path + demo_ranges : bool + whether to use fixed demo ranges for the plot + + """ + axis.set_xlabel(r"$\rho \quad [r/a]$") + axis.set_ylabel(r"$P_{\mathrm{rad}}$ $[\mathrm{MW.m}^{-3}]$") + axis.set_title("Raw Data: Line & Bremsstrahlung radiation profile") + + # read in the impurity data + imp_data = read_imprad_data(_skiprows=2, data_path=impp) + + # find impurity densities + imp_frac = np.array([ + mfile.get(f"f_nd_impurity_electrons({i:02d})", scan=scan) for i in range(1, 15) + ]) + + rho, ne, te = profiles_with_pedestal(mfile, scan) + + # Intailise the radiation profile arrays + pimpden = np.zeros([imp_data.shape[0], te.shape[0]]) + lz = np.zeros([imp_data.shape[0], te.shape[0]]) + prad = np.zeros(te.shape[0]) + + # Intailise the impurity radiation profile + for k in range(te.shape[0]): + for i in range(imp_data.shape[0]): + if te[k] <= imp_data[i][0][0]: + lz[i][k] = imp_data[i][0][1] + elif te[k] >= imp_data[i][imp_data.shape[1] - 1][0]: + lz[i][k] = imp_data[i][imp_data.shape[1] - 1][1] + else: + # Use np.interp for log-log interpolation + log_te_data = np.log([row[0] for row in imp_data[i]]) + log_lz_data = np.log([row[1] for row in imp_data[i]]) + lz[i][k] = np.exp(np.interp(np.log(te[k]), log_te_data, log_lz_data)) + pimpden[i][k] = imp_frac[i] * ne[k] * ne[k] * lz[i][k] + + for l_ in range(imp_data.shape[0]): + prad[k] += pimpden[l_][k] * 1.0e-6 + + axis.plot(rho, prad, label="Total", linestyle="dotted") + axis.plot(rho, pimpden[0] * 1.0e-6, label="H") + axis.plot(rho, pimpden[1] * 1.0e-6, label="He") + if imp_frac[2] > 1.0e-30: + axis.plot(rho, pimpden[2] * 1.0e-6, label="Be") + if imp_frac[3] > 1.0e-30: + axis.plot(rho, pimpden[3] * 1.0e-6, label="C") + if imp_frac[4] > 1.0e-30: + axis.plot(rho, pimpden[4] * 1.0e-6, label="N") + if imp_frac[5] > 1.0e-30: + axis.plot(rho, pimpden[5] * 1.0e-6, label="O") + if imp_frac[6] > 1.0e-30: + axis.plot(rho, pimpden[6] * 1.0e-6, label="Ne") + if imp_frac[7] > 1.0e-30: + axis.plot(rho, pimpden[7] * 1.0e-6, label="Si") + if imp_frac[8] > 1.0e-30: + axis.plot(rho, pimpden[8] * 1.0e-6, label="Ar") + if imp_frac[9] > 1.0e-30: + axis.plot(rho, pimpden[9] * 1.0e-6, label="Fe") + if imp_frac[10] > 1.0e-30: + axis.plot(rho, pimpden[10] * 1.0e-6, label="Ni") + if imp_frac[11] > 1.0e-30: + axis.plot(rho, pimpden[11] * 1.0e-6, label="Kr") + if imp_frac[12] > 1.0e-30: + axis.plot(rho, pimpden[12] * 1.0e-6, label="Xe") + if imp_frac[13] > 1.0e-30: + axis.plot(rho, pimpden[13] * 1.0e-6, label="W") + axis.legend(loc="upper left", bbox_to_anchor=(-0.1, -0.1), ncol=4) + axis.minorticks_on() + # Plot a vertical line at the core region radius + core_radius = mfile.get("radius_plasma_core_norm", scan=scan) + + # Plot a vertical line at the core region radius + axis.axvline(x=core_radius, color="black", linestyle="--", linewidth=1.0, alpha=0.7) + # Plot a box in the bottom left with f_{core,reduce} + props_core_reduce = { + "boxstyle": "round", + "facecolor": "khaki", + "alpha": 0.8, + } + draw_text( + axis, + 0.02, + 0.02, + rf"$f_{{\text{{core,reduce}}}}$ = {1.0}", + transform=axis.transAxes, + fontsize=8, + verticalalignment="bottom", + bbox=props_core_reduce, + ) + + # Ranges + # --- + axis.set_xlim(0, 1.0) + axis.set_yscale("log") + axis.yaxis.grid(True, which="both", alpha=0.2) + # DEMO : Fixed ranges for comparison + if demo_ranges: + axis.set_ylim(1e-6, 0.5) + + # Adaptive ranges + else: + axis.set_ylim(1e-6, axis.get_ylim()[1]) + + +def plot_line_brem_loss_function_profile( + axis: plt.Axes, + mfile: MFile, + scan: int, + impp: str, +): + """Function to plot Line and Bremsstrahlung loss function (L_z) profile. + + Parameters + ---------- + axis : plt.Axes + axis object to add plot to + mfile : MFile + MFile object containing plasma and impurity profile information. + scan : int + scan number to use + impp : str + impurity path + + """ + # read in the impurity data + imp_data = read_imprad_data(_skiprows=2, data_path=impp) + + # find impurity densities + imp_frac = np.array([ + mfile.get(f"f_nd_impurity_electrons({i:02d})", scan=scan) for i in range(1, 15) + ]) + + rho, _, te = profiles_with_pedestal(mfile, scan) + + # Intailise the radiation profile arrays + lz = np.zeros([imp_data.shape[0], te.shape[0]]) + + # Intailise the impurity radiation profile + for k in range(te.shape[0]): + for i in range(imp_data.shape[0]): + if te[k] <= imp_data[i][0][0]: + lz[i][k] = imp_data[i][0][1] + elif te[k] >= imp_data[i][imp_data.shape[1] - 1][0]: + lz[i][k] = imp_data[i][imp_data.shape[1] - 1][1] + else: + # Use np.interp for log-log interpolation + log_te_data = np.log([row[0] for row in imp_data[i]]) + log_lz_data = np.log([row[1] for row in imp_data[i]]) + lz[i][k] = np.exp(np.interp(np.log(te[k]), log_te_data, log_lz_data)) + + axis.plot(rho, lz[0], label="H") + axis.plot(rho, lz[1], label="He") + if imp_frac[2] > 1.0e-30: + axis.plot(rho, lz[2], label="Be") + if imp_frac[3] > 1.0e-30: + axis.plot(rho, lz[3], label="C") + if imp_frac[4] > 1.0e-30: + axis.plot(rho, lz[4], label="N") + if imp_frac[5] > 1.0e-30: + axis.plot(rho, lz[5], label="O") + if imp_frac[6] > 1.0e-30: + axis.plot(rho, lz[6], label="Ne") + if imp_frac[7] > 1.0e-30: + axis.plot(rho, lz[7], label="Si") + if imp_frac[8] > 1.0e-30: + axis.plot(rho, lz[8], label="Ar") + if imp_frac[9] > 1.0e-30: + axis.plot(rho, lz[9], label="Fe") + if imp_frac[10] > 1.0e-30: + axis.plot(rho, lz[10], label="Ni") + if imp_frac[11] > 1.0e-30: + axis.plot(rho, lz[11], label="Kr") + if imp_frac[12] > 1.0e-30: + axis.plot(rho, lz[12], label="Xe") + if imp_frac[13] > 1.0e-30: + axis.plot(rho, lz[13], label="W") + axis.legend(loc="best", ncol=4) + axis.minorticks_on() + + axis.set_xlabel(r"$\rho \quad [r/a]$") + axis.set_ylabel(r"$L_z$ $[\mathrm{W}\mathrm{m}^3]$") + axis.set_title("Line & Bremsstrahlung Loss Function ($L_z$) Profiles") + axis.set_xlim(0, 1.0) + axis.set_yscale("log") + axis.yaxis.grid(True, which="both", alpha=0.2) + + +def interp1d_profile(profile, mfile: MFile, scan: int): + """Interpolate profile over a grid""" + # Get plasma geometry and boundary + pg = plasma_geometry( + rmajor=mfile.get("rmajor", scan=scan), + rminor=mfile.get("rminor", scan=scan), + triang=mfile.get("triang", scan=scan), + kappa=mfile.get("kappa", scan=scan), + i_single_null=mfile.get("i_single_null", scan=scan), + i_plasma_shape=mfile.get("i_plasma_shape", scan=scan), + square=mfile.get("plasma_square", scan=scan), + ) + + # Create a grid of (R, Z) points inside the plasma boundary + rho = np.linspace(0, 1, 500) + theta = np.linspace(0, 2 * np.pi, 720) + rho_grid, theta_grid = np.meshgrid(rho, theta) + + # Map (rho, theta) to (R, Z) using plasma boundary shape + # For each theta, get boundary (R, Z), then scale by rho + bdry_r = pg.rs + bdry_z = pg.zs + # Interpolate boundary for all theta + bdry_theta = np.arctan2(bdry_z - pg.zs.mean(), bdry_r - pg.rs.mean()) + # Ensure bdry_theta is monotonic and covers [0, 2pi] + bdry_theta = np.unwrap(bdry_theta) + # Sort bdry_theta and corresponding r/z for monotonic interpolation + sort_idx = np.argsort(bdry_theta) + bdry_theta = bdry_theta[sort_idx] + bdry_r = bdry_r[sort_idx] + bdry_z = bdry_z[sort_idx] + # Extend boundary to cover full [0, 2pi] if needed + if bdry_theta[0] > 0 or bdry_theta[-1] < 2 * np.pi: + bdry_theta = np.concatenate(([0], bdry_theta, [2 * np.pi])) + bdry_r = np.concatenate(([bdry_r[0]], bdry_r, [bdry_r[-1]])) + bdry_z = np.concatenate(([bdry_z[0]], bdry_z, [bdry_z[-1]])) + # Map theta to boundary r/z + f_r = interp1d( + bdry_theta, + bdry_r, + kind="linear", + fill_value="extrapolate", + assume_sorted=True, + ) + # Map theta to boundary z + f_z = interp1d( + bdry_theta, + bdry_z, + kind="linear", + fill_value="extrapolate", + assume_sorted=True, + ) + # For each (theta, rho), get boundary (R, Z), then scale by rho + # Use the boundary center for scaling, not mean, to avoid vertical offset + r_center = mfile.get("rmajor", scan=scan) + z_center = pg.zs.mean() + r_grid = r_center + (f_r(theta_grid) - r_center) * rho_grid + z_grid = z_center + (f_z(theta_grid) - z_center) * rho_grid + + # Interpolate profile for each rho + profile_grid = np.interp(rho_grid, np.linspace(0, 1, len(profile)), profile) + + return profile_grid, r_grid, z_grid + + +__all__ = [ + "interp1d_profile", + "plot_line_brem_loss_function_profile", + "plot_line_brem_power_density_profile", + "profiles_with_pedestal", +] diff --git a/process/core/io/plot/summary/profiles/plasma.py b/process/core/io/plot/summary/profiles/plasma.py new file mode 100644 index 0000000000..b62adec830 --- /dev/null +++ b/process/core/io/plot/summary/profiles/plasma.py @@ -0,0 +1,1552 @@ +"""Profiles functions for PROCESS summary plots.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import matplotlib.pyplot as plt +import numpy as np + +from process.core import constants +from process.core.io.plot.summary.common import ( + box_style, + text_layout, +) +from process.core.io.plot.summary.plasma import ( + reaction_plot_grid, +) +from process.core.io.plot.summary.profiles.misc import ( + interp1d_profile, +) +from process.core.io.plot.summary.rendering import ( + draw_text, +) +from process.models.physics.profiles import PlasmaProfileShapeType + +if TYPE_CHECKING: + from process.core.io.mfile import MFile + + +def plot_n_profiles(prof, demo_ranges: bool, mfile: MFile, scan: int): + """Function to plot density profile + + Parameters + ---------- + prof : + axis object to add plot to + demo_ranges: bool : + + mfile: MFile : + + scan: int : + + """ + nd_alphas = mfile.get("nd_plasma_alphas_thermal_vol_avg", scan=scan) + nd_protons = mfile.get("nd_plasma_protons_vol_avg", scan=scan) + nd_impurities = mfile.get("nd_plasma_impurities_vol_avg", scan=scan) + nd_ions_total = mfile.get("nd_plasma_ions_total_vol_avg", scan=scan) + nd_fuel_ions = mfile.get("nd_plasma_fuel_ions_vol_avg", scan=scan) + alphan = mfile.get("alphan", scan=scan) + f_nd_plasma_pedestal_greenwald = mfile.get( + "f_nd_plasma_pedestal_greenwald", scan=scan + ) + f_nd_plasma_separatrix_greenwald = mfile.get( + "f_nd_plasma_separatrix_greenwald", scan=scan + ) + nd_plasma_electrons_vol_avg = mfile.get("nd_plasma_electrons_vol_avg", scan=scan) + # find impurity densities + imp_frac = np.array([ + mfile.get(f"f_nd_impurity_electrons({i:02d})", scan=scan) for i in range(1, 15) + ]) + + nd_plasma_separatrix_electron = mfile.get("nd_plasma_separatrix_electron", scan=scan) + + ax_main = prof.add_subplot(631) + ax_main.set_position([0.075, 0.625, 0.25, 0.325]) + ax_impurity = prof.add_subplot(634, sharex=ax_main) + ax_impurity.set_position([0.075, 0.275, 0.25, 0.325]) + ax_main.tick_params(labelbottom=False) + + ax_impurity.set_xlabel(r"$\rho \quad [r/a]$") + ax_main.set_ylabel(r"$n \ [10^{19}\ \mathrm{m}^{-3}]$") + ax_impurity.set_ylabel(r"$n \ [10^{16}\ \mathrm{m}^{-3}]$") + ax_main.set_title("Density profile") + + i_plasma_pedestal = mfile.get("i_plasma_pedestal", scan=scan) + nd_plasma_pedestal_electron = mfile.get("nd_plasma_pedestal_electron", scan=scan) + ne0 = mfile.get("nd_plasma_electron_on_axis", scan=scan) + nd_plasma_electrons_vol_avg = mfile.get("nd_plasma_electrons_vol_avg", scan=scan) + radius_plasma_pedestal_density_norm = mfile.get( + "radius_plasma_pedestal_density_norm", scan=scan + ) + ne0 = mfile.get("nd_plasma_electron_on_axis", scan=scan) + n_plasma_profile_elements = mfile.get("n_plasma_profile_elements", scan=scan) + + # build electron profile and species profiles (scale with electron profile shape) + if i_plasma_pedestal == 1: + rho = np.linspace(0, 1.0, int(n_plasma_profile_elements)) + ne = np.zeros_like(rho) + + for i in range(len(rho)): + if rho[i] <= radius_plasma_pedestal_density_norm: + ne[i] = ( + nd_plasma_pedestal_electron + + (ne0 - nd_plasma_pedestal_electron) + * (1 - rho[i] ** 2 / radius_plasma_pedestal_density_norm**2) + ** alphan + ) + else: + ne[i] = nd_plasma_separatrix_electron + ( + nd_plasma_pedestal_electron - nd_plasma_separatrix_electron + ) * (1 - rho[i]) / (1 - min(0.9999, radius_plasma_pedestal_density_norm)) + else: + rho = np.linspace(0, 1.0, n_plasma_profile_elements) + ne = ne0 * (1 - rho**2) ** alphan + + # species profiles scaled by their average fraction relative to electrons + + if nd_plasma_electrons_vol_avg != 0: + fracs = ( + np.array([ + nd_fuel_ions, + nd_alphas, + nd_protons, + nd_impurities, + nd_ions_total, + nd_plasma_electrons_vol_avg, + ]) + / nd_plasma_electrons_vol_avg + ) + else: + fracs = np.zeros(5) + + # build species density profiles from electron profile and fractions + # fracs = [fuel, alpha, protons, impurities, ions_total] + # Create a density profile for each species by multiplying ne by each fraction in + # fracs + density_profiles = np.array([ne * frac for frac in fracs]) + + # convert to 1e19 m^-3 units for plotting (vectorised) + density_profiles_plotting = density_profiles / 1e19 + + ax_main.plot( + rho, + density_profiles_plotting[0], + label=r"$n_{\text{fuel}}$", + color="#2ca02c", + linewidth=1.5, + ) + ax_main.plot( + rho, + density_profiles_plotting[1], + label=r"$n_{\alpha,\text{thermal}}$", + color="#d62728", + linewidth=1.5, + ) + ax_impurity.plot( + rho, + density_profiles_plotting[2] * 1e3, + label=r"$n_{p}$", + color="#17becf", + linewidth=1.5, + ) + ax_impurity.plot( + rho, + density_profiles_plotting[3] * 1e3, + label=r"$n_{imp,total}$", + color="#9467bd", + linewidth=2.5, + linestyle="dotted", + ) + ax_main.plot( + rho, + density_profiles_plotting[4], + label=r"$n_{i,total}$", + color="#ff7f0e", + linewidth=1.5, + ) + ax_main.plot( + rho, + density_profiles_plotting[5], + label=r"$n_{e}$", + color="blue", + linewidth=1.5, + ) + + if imp_frac[2] > 1.0e-30: + ax_impurity.plot(rho, imp_frac[2] * ne / 1e16, label=r"$n_{\text{Be}}$") + if imp_frac[3] > 1.0e-30: + ax_impurity.plot(rho, imp_frac[3] * ne / 1e16, label=r"$n_{\text{C}}$") + if imp_frac[4] > 1.0e-30: + ax_impurity.plot(rho, imp_frac[4] * ne / 1e16, label=r"$n_{\text{N}}$") + if imp_frac[5] > 1.0e-30: + ax_impurity.plot(rho, imp_frac[5] * ne / 1e16, label=r"$n_{\text{O}}$") + if imp_frac[6] > 1.0e-30: + ax_impurity.plot(rho, imp_frac[6] * ne / 1e16, label=r"$n_{\text{Ne}}$") + if imp_frac[7] > 1.0e-30: + ax_impurity.plot(rho, imp_frac[7] * ne / 1e16, label=r"$n_{\text{Si}}$") + if imp_frac[8] > 1.0e-30: + ax_impurity.plot(rho, imp_frac[8] * ne / 1e16, label=r"$n_{\text{Ar}}$") + if imp_frac[9] > 1.0e-30: + ax_impurity.plot(rho, imp_frac[9] * ne / 1e16, label=r"$n_{\text{Fe}}$") + if imp_frac[10] > 1.0e-30: + ax_impurity.plot(rho, imp_frac[10] * ne / 1e16, label=r"$n_{\text{Ni}}$") + if imp_frac[11] > 1.0e-30: + ax_impurity.plot(rho, imp_frac[11] * ne / 1e16, label=r"$n_{\text{Kr}}$") + if imp_frac[12] > 1.0e-30: + ax_impurity.plot(rho, imp_frac[12] * ne / 1e16, label=r"$n_{\text{Xe}}$") + if imp_frac[13] > 1.0e-30: + ax_impurity.plot(rho, imp_frac[13] * ne / 1e16, label=r"$n_{\text{W}}$") + + ax_main.legend(loc="best") + ax_impurity.legend(loc="best") + + # Ranges + # --- + # DEMO : Fixed ranges for comparison + ax_main.set_xlim(0, 1) + ax_impurity.set_xlim(0, 1) + if demo_ranges: + ax_main.set_ylim(0, 20) + + # Adaptive ranges + else: + ax_main.set_ylim(0, ax_main.get_ylim()[1]) + # Use logarithmic scale for impurity axis if any impurity values are very small + impurity_data = [ + imp_frac[i] * ne / 1e16 + for i in range(len(imp_frac)) + if imp_frac[i] > 1.0e-30 + ] + if impurity_data and np.min(impurity_data) / np.max(impurity_data) < 0.01: + # If range spans more than 100x, use log scale + ax_impurity.set_yscale("log") + ax_impurity.set_ylim(1e-3, ax_impurity.get_ylim()[1]) + + if i_plasma_pedestal != 0: + # Print pedestal lines + ax_main.axhline( + y=nd_plasma_pedestal_electron / 1e19, + xmax=radius_plasma_pedestal_density_norm, + color="r", + linestyle="-", + linewidth=0.4, + alpha=0.4, + ) + ax_main.vlines( + x=radius_plasma_pedestal_density_norm, + ymin=0.0, + ymax=nd_plasma_pedestal_electron / 1e19, + color="r", + linestyle="-", + linewidth=0.4, + alpha=0.4, + ) + ax_main.minorticks_on() + ax_impurity.minorticks_on() + + # Add text box with density profile parameters + textstr_density = "\n".join(( + ( + r"$\langle n_{\text{e}} \rangle$:" + rf" {nd_plasma_electrons_vol_avg:.3e}" + r" m$^{-3}$" + r"$\hspace{4} \overline{n_{e}}$:" + rf" {mfile.get('nd_plasma_electron_line', scan=scan):.3e}" + r" m$^{-3}$" + ), + ( + rf"$n_{{\text{{e,0}}}}$: {ne0:.3e} m$^{{-3}}$" + rf"$\hspace{{4}} \alpha_{{\text{{n}}}}$: {alphan:.3f}" + ), + ( + rf"$n_{{\text{{e,ped}}}}$: {nd_plasma_pedestal_electron:.3e}" + r" m$^{-3}$" + r"$ \hspace{3} \frac{\langle n_i \rangle}{\langle n_e" + r" \rangle}$: " + f"{nd_fuel_ions / nd_plasma_electrons_vol_avg:.3f}" + ), + ( + r"$f_{\text{GW e,ped}}$:" + rf" {f_nd_plasma_pedestal_greenwald:.3f}" + r"$ \hspace{7} \frac{n_{e,0}}{\langle n_e \rangle}$: " + f"{ne0 / nd_plasma_electrons_vol_avg:.3f}" + ), + ( + r"$\rho_{\text{ped,n}}$:" + rf" {radius_plasma_pedestal_density_norm:.3f}" + r"$ \hspace{8} \frac{\overline{n_{e}}}{n_{\text{GW}}}$: " + f"{mfile.get('nd_plasma_electron_line', scan=scan) / mfile.get('nd_plasma_electron_max_array(7)', scan=scan):.3f}" # noqa: E501 + ), + ( + rf"$n_{{\text{{e,sep}}}}$: {nd_plasma_separatrix_electron:.3e}" + r" m$^{-3}$" + ), + ( + r"$f_{\text{GW e,sep}}$:" + rf" {f_nd_plasma_separatrix_greenwald:.3f}" + ), + )) + + props_density = {"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5} + ax_main.text( + -0.05, + -0.175, + textstr_density, + transform=ax_impurity.transAxes, + fontsize=9, + verticalalignment="top", + bbox=props_density, + ) + + textstr_ions = "\n".join(( + ( + r"$\langle n_{\text{ions-total}} \rangle $: " + f"{mfile.get('nd_plasma_ions_total_vol_avg', scan=scan):.3e}" + " m$^{-3}$" + ), + ( + r"$\langle n_{\text{fuel}} \rangle $: " + f"{mfile.get('nd_plasma_fuel_ions_vol_avg', scan=scan):.3e}" + " m$^{-3}$" + ), + ( + r"$\langle n_{\alpha,\text{thermal}} \rangle $: " + f"{mfile.get('nd_plasma_alphas_thermal_vol_avg', scan=scan):.3e}" + " m$^{-3}$" + ), + ( + r"$\langle n_{\text{impurities}} \rangle $: " + f"{mfile.get('nd_plasma_impurities_vol_avg', scan=scan):.3e}" + " m$^{-3}$" + ), + ( + r"$\langle n_{\text{protons}} \rangle $:" + f"{mfile.get('nd_plasma_protons_vol_avg', scan=scan):.3e}" + " m$^{-3}$" + ), + )) + + ax_impurity.text( + 1.2, + 0.05, + textstr_ions, + fontsize=9, + verticalalignment="bottom", + horizontalalignment="left", + transform=ax_impurity.transAxes, + bbox={ + "boxstyle": "round", + "facecolor": "wheat", + "alpha": 0.5, + }, + ) + + ax_main.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.2) + ax_impurity.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.2) + + +def plot_jprofile(prof, mfile: MFile, scan: int): + """Function to plot density profile + + Parameters + ---------- + prof : + axis object to add plot to + mfile: MFile : + + scan: int : + + """ + alphaj = mfile.get("alphaj", scan=scan) + j_plasma_0 = mfile.get("j_plasma_on_axis", scan=scan) + n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) + + j_plasma_bootstrap_sauter_profile = [ + mfile.get(f"j_plasma_bootstrap_sauter_profile{i}", scan=scan) / 1000.0 + for i in range(n_plasma_profile_elements - 3) + ] + + prof.set_xlabel(r"$\rho \quad [r/a]$") + prof.set_ylabel(r"Current density $[kA/m^2]$") + prof.set_title("$J$ profile") + prof.minorticks_on() + prof.set_xlim(0, 1.0) + + rho = np.linspace(0, 1) + y2 = (j_plasma_0 * (1 - rho**2) ** alphaj) / 1e3 + + prof.plot(rho, y2, color="red") + + prof.plot( + np.linspace(0, 1, n_plasma_profile_elements - 3), + j_plasma_bootstrap_sauter_profile, + label="Sauter Bootstrap", + color="green", + linestyle="--", + ) + prof.legend() + + textstr_j = "\n".join(( + r"$j_0$: " + f"{y2[0]:.3f} kA m$^{{-2}}$\n", + r"$\alpha_J$: " + f"{alphaj:.3f}", + )) + + props_j = {"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5} + prof.text( + 0.65, + 1.6, + textstr_j, + transform=prof.transAxes, + fontsize=9, + verticalalignment="top", + bbox=props_j, + ) + + prof.text( + 0.35, + 0.04, + "*Current profile is assumed to be parabolic", + fontsize=10, + ha="left", + transform=plt.gcf().transFigure, + ) + prof.text( + 0.35, + 0.02, + "*Bootstrap profile is for representation only", + fontsize=10, + ha="left", + transform=plt.gcf().transFigure, + ) + prof.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.2) + + +def plot_t_profiles(prof, demo_ranges: bool, mfile: MFile, scan: int): + """Function to plot temperature profile + + Parameters + ---------- + prof : + axis object to add plot to + demo_ranges: bool : + + mfile: MFile : + + scan: int : + + """ + prof.set_xlabel(r"$\rho \quad [r/a]$") + prof.set_ylabel("$T$ [keV]") + prof.set_title("Temperature profile") + + alphat = mfile.get("alphat", scan=scan) + radius_plasma_pedestal_temp_norm = mfile.get( + "radius_plasma_pedestal_temp_norm", scan=scan + ) + + n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) + i_plasma_pedestal = mfile.get("i_plasma_pedestal", scan=scan) + rho = np.linspace(0, 1.0, n_plasma_profile_elements) + temp_plasma_pedestal_electron_kev = mfile.get( + "temp_plasma_pedestal_electron_kev", scan=scan + ) + temp_plasma_separatrix_electron_kev = mfile.get( + "temp_plasma_separatrix_electron_kev", scan=scan + ) + f_temp_plasma_ion_electron = mfile.get("f_temp_plasma_ion_electron", scan=scan) + tbeta = mfile.get("tbeta", scan=scan) + te0 = mfile.get("temp_plasma_electron_on_axis_kev", scan=scan) + + if i_plasma_pedestal == 1: + rhocore = np.linspace(0.0, radius_plasma_pedestal_temp_norm) + tcore = ( + temp_plasma_pedestal_electron_kev + + (te0 - temp_plasma_pedestal_electron_kev) + * (1 - (rhocore / radius_plasma_pedestal_temp_norm) ** tbeta) ** alphat + ) + + rhosep = np.linspace(radius_plasma_pedestal_temp_norm, 1) + tsep = temp_plasma_separatrix_electron_kev + ( + temp_plasma_pedestal_electron_kev - temp_plasma_separatrix_electron_kev + ) * (1 - rhosep) / (1 - min(0.9999, radius_plasma_pedestal_temp_norm)) + + rho = np.append(rhocore, rhosep) + te = np.append(tcore, tsep) + else: + rho1 = np.linspace(0, 0.95) + rho2 = np.linspace(0.95, 1) + rho = np.append(rho1, rho2) + te = te0 * (1 - rho**2) ** alphat + prof.plot(rho, te, color="blue", label="$T_{e}$") + prof.plot(rho, te[:] * f_temp_plasma_ion_electron, color="red", label="$T_{i}$") + prof.legend() + + # Ranges + # --- + prof.set_xlim(0, 1) + # DEMO : Fixed ranges for comparison + if demo_ranges: + prof.set_ylim(0, 50) + + # Adaptive ranges + else: + prof.set_ylim(0, prof.get_ylim()[1]) + + if i_plasma_pedestal != 0: + # Plot pedestal lines + prof.axhline( + y=temp_plasma_pedestal_electron_kev, + xmax=radius_plasma_pedestal_temp_norm, + color="r", + linestyle="-", + linewidth=0.4, + alpha=0.4, + ) + prof.vlines( + x=radius_plasma_pedestal_temp_norm, + ymin=0.0, + ymax=temp_plasma_pedestal_electron_kev, + color="r", + linestyle="-", + linewidth=0.4, + alpha=0.4, + ) + prof.minorticks_on() + + te = mfile.get("temp_plasma_electron_vol_avg_kev", scan=scan) + # Add text box with temperature profile parameters + textstr_temperature = "\n".join(( + ( + r"$\langle T_{\text{e}} \rangle_\text{V}$: " + rf" {mfile.get('temp_plasma_electron_vol_avg_kev', scan=scan):.3f} keV" + r"$\hspace{2} \langle T_{\text{e}} \rangle_\text{n}$:" + rf" {mfile.get('temp_plasma_electron_density_weighted_kev', scan=scan):.3f} keV" # noqa: E501 + r"$\hspace{2} \overline{T_{e}}$:" + rf" {mfile.get('temp_plasma_electron_line_avg_kev', scan=scan):.3f} keV" + ), + ( + rf"$T_{{\text{{e,0}}}}$: {te0:.3f} keV" + rf"$\hspace{{3}} \alpha_{{\text{{T}}}}$: {alphat:.3f} " + r"$\hspace{3} \langle T_{\text{i}} \rangle_\text{V}$:" + rf" {mfile.get('temp_plasma_ion_vol_avg_kev', scan=scan):.3f} keV" + ), + ( + r"$T_{\text{e,ped}}$:" + rf" {temp_plasma_pedestal_electron_kev:.3f} keV" + r"$ \hspace{3} \frac{\langle T_i \rangle}{\langle T_e" + r" \rangle}$: " + f"{f_temp_plasma_ion_electron:.3f} " + "$\\hspace{4} T_{\\text{i,0}}$:" + f" {mfile.get('temp_plasma_ion_on_axis_kev', scan=scan):.3f} keV" + ), + ( + r"$\rho_{\text{ped,T}}$:" + rf" {radius_plasma_pedestal_temp_norm:.3f}" + r"$ \hspace{5} \frac{T_{e,0}}{\langle T_e \rangle}$: " + f"{mfile.get('f_temp_plasma_electron_on_axis_vol_avg', scan=scan):.3f} " + "$\\hspace{4} T_{\\text{i,ped}}$:" + f" {mfile.get('temp_plasma_pedestal_ion_kev', scan=scan):.3f} keV" + ), + ( + r"$T_{\text{e,sep}}$:" + rf" {temp_plasma_separatrix_electron_kev:.3f} keV" + r"$\hspace{3} \frac{{{\langle T_e \rangle_n}}}{{{\langle T_e" + r" \rangle_V}}}$: " + f"{mfile.get('f_temp_plasma_electron_density_vol_avg', scan=scan):.3f}" + "$\\hspace{4} T_{\\text{i,sep}}$:" + f" {mfile.get('temp_plasma_separatrix_ion_kev', scan=scan):.3f} keV" + ), + )) + + props_temperature = { + "boxstyle": "round", + "facecolor": "wheat", + "alpha": 0.5, + } + prof.text( + -0.1, + -0.125, + textstr_temperature, + transform=prof.transAxes, + fontsize=9, + verticalalignment="top", + bbox=props_temperature, + ) + prof.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.2) + + +def plot_qprofile(prof, demo_ranges: bool, mfile: MFile, scan: int): + """Function to plot q profile, formula taken from Nevins bootstrap model. + + Parameters + ---------- + prof : + axis object to add plot to + demo_ranges: bool : + + mfile: MFile : + + scan: int : + + """ + prof.set_xlabel(r"$\rho \quad [r/a]$") + prof.set_ylabel("$q$") + prof.set_title("$q$ profile") + prof.minorticks_on() + + rho = np.linspace(0, 1) + q0 = mfile.get("q0", scan=scan) + q95 = mfile.get("q95", scan=scan) + + q_r_nevin = q0 + (q95 - q0) * (rho + rho * rho + rho**3) / (3.0) + q_r_sauter = q0 + (q95 - q0) * (rho * rho) + + prof.plot(rho, q_r_nevin, label="Nevins") + prof.plot(rho, q_r_sauter, label="Sauter") + prof.legend() + + # Ranges + # --- + prof.set_xlim(0, 1) + # DEMO : Fixed ranges for comparison + if demo_ranges: + prof.set_ylim(0, 10) + + # Adaptive ranges + else: + prof.set_ylim(0, q95 * 1.2) + + prof.text( + 0.6, + 0.04, + "*Profile is not calculated, only $q_0$ and $q_{95}$ are known.", + fontsize=10, + ha="left", + transform=plt.gcf().transFigure, + ) + prof.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.2) + # --- + + textstr_q = " | ".join(( + r"$q_0$: " + f"{q0:.3f}", + r"$q_{95}$: " + f"{q95:.3f}", + r"$q_{\text{cyl}}$: " + f"{mfile.get('qstar', scan=scan):.3f}", + )) + + props_q = {"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5} + prof.text( + 0.0, + 1.4, + textstr_q, + transform=prof.transAxes, + fontsize=9, + verticalalignment="top", + bbox=props_q, + ) + + +def plot_fusion_rate_profiles(axis: plt.Axes, fig, mfile: MFile, scan: int): + """Plot the fusion rate density profiles on the given axis""" + n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) + + fusden_plasma_dt_profile = [ + mfile.get(f"fusden_plasma_dt_profile{i}", scan=scan) + for i in range(n_plasma_profile_elements) + ] + + fusden_plasma_dd_triton_profile = [ + mfile.get(f"fusden_plasma_dd_triton_profile{i}", scan=scan) + for i in range(n_plasma_profile_elements) + ] + + fusden_plasma_dd_helion_profile = [ + mfile.get(f"fusden_plasma_dd_helion_profile{i}", scan=scan) + for i in range(n_plasma_profile_elements) + ] + fusden_plasma_dhe3_profile = [ + mfile.get(f"fusden_plasma_dhe3_profile{i}", scan=scan) + for i in range(n_plasma_profile_elements) + ] + + fusrat_plasma_total_profile = [ + fusden_plasma_dt_profile[i] + + fusden_plasma_dd_triton_profile[i] + + fusden_plasma_dd_helion_profile[i] + + fusden_plasma_dhe3_profile[i] + for i in range(len(fusden_plasma_dt_profile)) + ] + + axis.spines["left"].set_color("red") + axis.yaxis.label.set_color("black") + axis.tick_params(axis="y", colors="red") + + # Plot fusion rates (dashed lines, left axis) with axis color and different + # linestyles + axis.plot( + np.linspace(0, 1, len(fusden_plasma_dt_profile)), + fusden_plasma_dt_profile, + color=axis.spines["left"].get_edgecolor(), + linestyle="-", + label=r"$\mathrm{D-T}$", + ) + axis.plot( + np.linspace(0, 1, len(fusden_plasma_dd_triton_profile)), + fusden_plasma_dd_triton_profile, + color=axis.spines["left"].get_edgecolor(), + linestyle=":", + label=r"$\mathrm{D-D \ Triton}$", + ) + axis.plot( + np.linspace(0, 1, len(fusden_plasma_dd_helion_profile)), + fusden_plasma_dd_helion_profile, + color=axis.spines["left"].get_edgecolor(), + linestyle="-.", + label=r"$\mathrm{D-D \ Helion}$", + ) + axis.plot( + np.linspace(0, 1, len(fusden_plasma_dhe3_profile)), + fusden_plasma_dhe3_profile, + color=axis.spines["left"].get_edgecolor(), + linestyle="--", + label=r"$\mathrm{D-3He}$", + ) + axis.plot( + np.linspace(0, 1, len(fusrat_plasma_total_profile)), + fusrat_plasma_total_profile, + color=axis.spines["left"].get_edgecolor(), + linestyle="None", + marker="d", + markersize=1, + label=r"Total", + ) + + # Show the plasma volume-averaged rate density and its position on the + # profile. + profile_positions = np.linspace(0, 1, len(fusrat_plasma_total_profile)) + profile_rates = np.asarray(fusrat_plasma_total_profile) + average_rate = mfile.get("fusden_plasma_vol_avg", scan=scan) + axis.axhline( + average_rate, + color="black", + linestyle="--", + linewidth=0.9, + label="Plasma volume average", + ) + + average_position = profile_positions[ + np.nanargmin(np.abs(profile_rates - average_rate)) + ] + axis.axvline( + average_position, + color="black", + linestyle="--", + linewidth=0.9, + ) + + # Plot fusion power (solid lines, right axis) with axis color and different + # linestyles + ax2 = axis.twinx() + ax2.spines["right"].set_color("blue") + ax2.yaxis.label.set_color("black") + ax2.tick_params(axis="y", colors="blue") + ax2.plot( + np.linspace(0, 1, len(fusden_plasma_dt_profile)), + np.array(fusden_plasma_dt_profile) * constants.D_T_ENERGY, + color=ax2.spines["right"].get_edgecolor(), + linestyle="-", + ) + + ax2.plot( + np.linspace(0, 1, len(fusden_plasma_dd_triton_profile)), + np.array(fusden_plasma_dd_triton_profile) * constants.DD_TRITON_ENERGY, + color=ax2.spines["right"].get_edgecolor(), + linestyle=":", + ) + ax2.plot( + np.linspace(0, 1, len(fusden_plasma_dd_helion_profile)), + np.array(fusden_plasma_dd_helion_profile) * constants.DD_HELIUM_ENERGY, + color=ax2.spines["right"].get_edgecolor(), + linestyle="-.", + ) + ax2.plot( + np.linspace(0, 1, len(fusden_plasma_dhe3_profile)), + np.array(fusden_plasma_dhe3_profile) * constants.D_HELIUM_ENERGY, + color=ax2.spines["right"].get_edgecolor(), + linestyle="--", + ) + ax2.plot( + np.linspace(0, 1, len(fusrat_plasma_total_profile)), + ( + np.array(fusden_plasma_dhe3_profile) * constants.D_HELIUM_ENERGY + + np.array(fusden_plasma_dd_helion_profile) * constants.DD_HELIUM_ENERGY + + np.array(fusden_plasma_dd_triton_profile) * constants.DD_TRITON_ENERGY + + np.array(fusden_plasma_dt_profile) * constants.D_T_ENERGY + ), + color=ax2.spines["right"].get_edgecolor(), + linestyle="None", + marker="d", + markersize=1, + label=r"Total", + ) + + # ================================================= + + axis.set_xlabel("$\\rho \\ [r/a]$") + axis.set_ylabel("Fusion Rate Density [reactions/m³/sec]") + axis.legend( + loc="lower left", + edgecolor="black", + facecolor="white", + labelcolor="black", + framealpha=1.0, + frameon=True, + ) + axis.set_yscale("log") + axis.grid(True, which="both", linestyle="--", alpha=0.5) + axis.set_xlim(0, 1.025) + axis.minorticks_on() + axis.set_ylim(1e10, 1e23) + axis.yaxis.set_major_locator(plt.LogLocator(base=10.0, numticks=10)) + axis.yaxis.set_minor_locator( + plt.LogLocator(base=10.0, subs=np.arange(1, 10) * 0.1, numticks=100) + ) + axis.tick_params(axis="y", which="minor", colors="red") + + ax2.set_title("Fusion Rate and Fusion Power Density Profiles") + ax2.set_ylabel("Fusion Power Density [W/m³]") + ax2.set_yscale("log") + ax2.minorticks_on() + ax2.yaxis.set_major_locator(plt.LogLocator(base=10.0, numticks=10)) + ax2.yaxis.set_minor_locator( + plt.LogLocator(base=10.0, subs=np.arange(1, 10) * 0.1, numticks=100) + ) + ax2.tick_params(axis="y", which="minor", colors="blue") + + # ================================================= + + # Add plasma volume, areas and shaping information + textstr_general = ( + f"Total fusion rate: {mfile.get('fusrat_total', scan=scan):.4e}" + " reactions/s\nTotal volume averaged fusion rate density:" + f" {mfile.get('fusden_total_vol_avg', scan=scan):.4e}" + " reactions/m3/s\nPlasma volume averaged fusion rate density:" + f" {mfile.get('fusden_plasma_vol_avg', scan=scan):.4e}" + " reactions/m3/s\n" + ) + + draw_text( + axis, + 0.05, + 0.85, + textstr_general, + **text_layout(fig), + bbox=box_style("lightyellow"), + ) + + # ============================================================================ + + textstr_dt = ( + f"Total fusion power: {mfile.get('p_dt_total_mw', scan=scan):,.2f}" + " MW\nPlasma fusion power:" + f" {mfile.get('p_plasma_dt_mw', scan=scan):,.2f} MW\nVolume-averaged" + " fusion power density: plasma:" + f" {mfile.get('pden_plasma_dt_vol_avg_mw', scan=scan):,.3f}" + " MW/m³\nBeam fusion power:" + f" {mfile.get('p_beam_dt_mw', scan=scan):,.2f} MW\n" + ) + + draw_text( + axis, + 0.05, + 0.75, + textstr_dt, + **text_layout(fig), + bbox=box_style("lightyellow"), + ) + + draw_text( + axis, + 0.24, + 0.8, + "$\\text{D - T}$", + fontsize=20, + verticalalignment="top", + transform=fig.transFigure, + ) + + # ================================================= + + textstr_dd = ( + f"Total fusion power: {mfile.get('p_dd_total_mw', scan=scan):,.2f}" + " MW\nVolume-averaged total power density:" + f" {mfile.get('pden_dd_total_vol_avg_mw', scan=scan):,.3e}" + " MW/m³\nTritium branching ratio:" + f" {mfile.get('f_dd_branching_trit', scan=scan):.4f}\n" + ) + + draw_text( + axis, + 0.05, + 0.65, + textstr_dd, + **text_layout(fig), + bbox=box_style("lightyellow"), + ) + + draw_text( + axis, + 0.22, + 0.685, + "$\\text{D - D}$", + fontsize=20, + verticalalignment="top", + transform=fig.transFigure, + ) + + # ================================================= + + textstr_dhe3 = ( + f"Total fusion power: {mfile.get('p_dhe3_total_mw', scan=scan):,.2f}" + " MW\n\nVolume-averaged total power density:" + f" {mfile.get('pden_dhe3_total_vol_avg_mw', scan=scan):,.3e} MW/m³\n\n" + ) + + draw_text( + axis, + 0.05, + 0.55, + textstr_dhe3, + **text_layout(fig), + bbox=box_style("lightyellow"), + ) + + draw_text( + axis, + 0.21, + 0.59, + "$\\text{D - 3He}$", + fontsize=20, + verticalalignment="top", + transform=fig.transFigure, + ) + + # ================================================= + + textstr_alpha = ( + f"Total power: {mfile.get('p_alpha_total_mw', scan=scan):.2f}" + f" MW\nPlasma power: {mfile.get('p_plasma_alpha_mw', scan=scan):.2f}" + f" MW\nBeam power: {mfile.get('p_beam_alpha_mw', scan=scan):.2f}" + " MW\n\nVolume-averaged rate density total:" + f" {mfile.get('fusden_alpha_total_vol_avg', scan=scan):.4e}" + " particles/m3/sec\nVolume-averaged rate density, plasma:" + f" {mfile.get('fusden_plasma_alpha_vol_avg', scan=scan):.4e}" + " particles/m3/sec\n\nVolume-averaged total power density:" + f" {mfile.get('pden_alpha_total_vol_avg_mw', scan=scan):.4e}" + " MW/m3\nVolume-averaged plasma power density:" + f" {mfile.get('pden_plasma_alpha_vol_avg_mw', scan=scan):.4e}" + " MW/m3\n\nPower per unit volume transferred to electrons:" + f" {mfile.get('f_pden_alpha_electron_mw', scan=scan):.4e} MW/m3\nPower" + " per unit volume transferred to ions:" + f" {mfile.get('f_pden_alpha_ions_mw', scan=scan):.4e} MW/m3\n\n" + ) + + draw_text( + axis, + 0.05, + 0.25, + textstr_alpha, + **text_layout(fig), + bbox=box_style("red"), + ) + + draw_text( + axis, + 0.35, + 0.45, + "$\\alpha$", + fontsize=22, + verticalalignment="top", + transform=fig.transFigure, + ) + + # ================================================= + + textstr_neutron = ( + f"Total power: {mfile.get('p_neutron_total_mw', scan=scan):,.2f}" + " MW\nPlasma power:" + f" {mfile.get('p_plasma_neutron_mw', scan=scan):,.2f} MW\nBeam power:" + f" {mfile.get('p_beam_neutron_mw', scan=scan):,.2f}" + " MW\n\nVolume-averaged total power density:" + f" {mfile.get('pden_neutron_total_vol_avg_mw', scan=scan):,.4e}" + " MW/m3\nVolume-averaged plasma power density:" + f" {mfile.get('pden_plasma_neutron_vol_avg_mw', scan=scan):,.4e}" + " MW/m3\n" + ) + + draw_text( + axis, + 0.05, + 0.1, + textstr_neutron, + **text_layout(fig), + bbox=box_style("grey"), + ) + + draw_text( + axis, + 0.25, + 0.2, + "$n$", + fontsize=20, + verticalalignment="top", + transform=fig.transFigure, + ) + + +def plot_plasma_pressure_profiles(axis: plt.Axes, mfile: MFile, scan: int): + """Plot the plasma pressure profiles on the given axis""" + n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) + + pres_plasma_profile = [ + mfile.get(f"pres_plasma_electron_profile{i}", scan=scan) + for i in range(n_plasma_profile_elements) + ] + pres_plasma_profile_ion = [ + mfile.get(f"pres_plasma_ion_total_profile{i}", scan=scan) + for i in range(n_plasma_profile_elements) + ] + pres_plasma_thermal_total_profile = [ + mfile.get(f"pres_plasma_thermal_total_profile{i}", scan=scan) + for i in range(n_plasma_profile_elements) + ] + pres_plasma_profile_fuel = [ + mfile.get(f"pres_plasma_fuel_profile{i}", scan=scan) + for i in range(n_plasma_profile_elements) + ] + pres_plasma_profile_kpa = [p / 1000.0 for p in pres_plasma_profile] + pres_plasma_profile_ion_kpa = [p / 1000.0 for p in pres_plasma_profile_ion] + pres_plasma_profile_fuel_kpa = [p / 1000.0 for p in pres_plasma_profile_fuel] + pres_plasma_profile_total_kpa = [ + p / 1000.0 for p in pres_plasma_thermal_total_profile + ] + + axis.plot( + np.linspace(0, 1, len(pres_plasma_profile_kpa)), + pres_plasma_profile_kpa, + color="blue", + label="Electron", + ) + axis.plot( + np.linspace(0, 1, len(pres_plasma_profile_ion_kpa)), + pres_plasma_profile_ion_kpa, + color="Red", + label="Ion-total", + ) + axis.plot( + np.linspace(0, 1, len(pres_plasma_profile_fuel_kpa)), + pres_plasma_profile_fuel_kpa, + color="orange", + label="Fuel", + ) + axis.plot( + np.linspace(0, 1, len(pres_plasma_profile_total_kpa)), + pres_plasma_profile_total_kpa, + color="green", + label="Total", + ) + + # Plot horizontal line for volume-average thermal pressure (converted to kPa) + p_vol_kpa = mfile.get("pres_plasma_thermal_vol_avg", scan=scan) / 1000.0 + axis.axhline( + p_vol_kpa, + color="black", + linestyle="--", + linewidth=1.2, + label="Volume avg", + zorder=5, + ) + + axis.set_xlabel("$\\rho$ [r/a]") + axis.set_ylabel("Thermal Pressure [kPa]") + axis.minorticks_on() + axis.grid(which="minor", linestyle=":", linewidth=0.5, alpha=0.5) + axis.set_title("Plasma Thermal Pressure Profiles") + axis.grid(True, linestyle="--", alpha=0.5) + axis.set_xlim(0, 1.025) + axis.set_ylim(bottom=0) + axis.legend() + + textstr_pressure = "\n".join(( + ( + r"$p_0$:" + rf" {mfile.get('pres_plasma_thermal_on_axis', scan=scan) / 1000:,.3f} kPa" + r"$\hspace{2} \frac{p_0}{\langle p_{\text{total}}" + r" \rangle_\text{V}}$:" + rf" {mfile.get('f_pres_plasma_thermal_on_axis_vol_avg', scan=scan):,.3f}" + ), + ( + r"$\langle p_{\text{total}} \rangle_\text{V}$:" + rf" {mfile.get('pres_plasma_thermal_vol_avg', scan=scan) / 1000:,.3f} kPa" + ), + )) + + draw_text( + axis, + 0.5, + 1.2, + textstr_pressure, + transform=axis.transAxes, + fontsize=9, + verticalalignment="top", + horizontalalignment="center", + bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5}, + ) + + if ( + int(mfile.get("i_plasma_pedestal", scan=scan)) + == PlasmaProfileShapeType.PEDESTAL_PROFILE + ): + textstr_pressure_pedestal = "\n".join(( + ( + r"$p_{\text{ped}}$:" + rf" {mfile.get('pres_plasma_pedestal_thermal', scan=scan) / 1000:,.3f} kPa" # noqa: E501 + ), + ( + r"$p_{\text{sep}}$:" + rf" {mfile.get('pres_plasma_separatrix_thermal', scan=scan) / 1000:,.3f} kPa" # noqa: E501 + ), + )) + + draw_text( + axis, + 0.9, + 1.2, + textstr_pressure_pedestal, + transform=axis.transAxes, + fontsize=9, + verticalalignment="top", + horizontalalignment="center", + bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5}, + ) + + +def plot_plasma_poloidal_pressure_contours(axis: plt.Axes, mfile: MFile, scan: int): + """Plot plasma poloidal pressure contours inside the plasma boundary. + + This function visualizes the poloidal pressure distribution inside the plasma + boundary + by interpolating the pressure profile onto a grid defined by the plasma geometry. + The pressure is shown as filled contours, with the plasma boundary overlaid. + + Parameters + ---------- + axis : matplotlib.axes.Axes + Matplotlib axis object to plot on. + mfile : mfile: MFile + MFILE data object containing plasma and geometry data. + scan : int + Scan number to use for extracting data. + """ + n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) + + # Get pressure profile (function of normalised radius rho, 0..1) + pres_plasma_electron_profile = [ + mfile.get(f"pres_plasma_electron_profile{i}", scan=scan) + for i in range(n_plasma_profile_elements) + ] + pres_plasma_profile_ion = [ + mfile.get(f"pres_plasma_ion_total_profile{i}", scan=scan) + for i in range(n_plasma_profile_elements) + ] + + # Convert pressure to kPa + pres_plasma_electron_profile_kpa = [p / 1000.0 for p in pres_plasma_electron_profile] + pres_plasma_profile_ion_kpa = [p / 1000.0 for p in pres_plasma_profile_ion] + pres_plasma_profile = [ + e + i + for e, i in zip( + pres_plasma_electron_profile_kpa, + pres_plasma_profile_ion_kpa, + strict=False, + ) + ] + + pressure_grid, r_grid, z_grid = interp1d_profile(pres_plasma_profile, mfile, scan) + + # Mask points outside the plasma boundary (optional, but grid is inside by + # construction) + # Plot filled contour + c = axis.contourf(r_grid, -z_grid, pressure_grid, levels=50, cmap="plasma") + + # Add colorbar for pressure (now in kPa) + # You can control the location using the 'location' argument ('left', 'right', 'top', + # 'bottom') + # For more control, use 'ax' or 'fraction', 'pad', etc. + # Example: location="right", pad=0.05, fraction=0.05 + rmajor = mfile.get("rmajor", scan=scan) + rminor = mfile.get("rminor", scan=scan) + + axis.figure.colorbar( + c, + ax=axis, + label="Pressure [kPa]", + location="left", + anchor=(-0.25, 0.5), + ) + + axis.set_aspect("equal") + axis.set_xlabel("R [m]") + axis.set_xlim(rmajor - 1.2 * rminor, rmajor + 1.2 * rminor) + axis.set_ylim( + -1.2 * rminor * mfile.get("kappa", scan=scan), + 1.2 * mfile.get("kappa", scan=scan) * rminor, + ) + axis.set_ylabel("Z [m]") + axis.set_title("Plasma Poloidal Pressure Contours") + axis.plot( + rmajor, + 0, + marker="o", + color="red", + markersize=6, + markeredgecolor="black", + zorder=100, + ) + + +def plot_fusion_rate_contours(fig1, fig2, mfile: MFile, scan: int): + """Plot fusion rate density contours""" + rmajor = mfile.get("rmajor", scan=scan) + rminor = mfile.get("rminor", scan=scan) + kappa = mfile.get("kappa", scan=scan) + n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) + + def fusrat(name): + fusrat_dat = [ + mfile.get(f"fusrat_plasma_{name}_profile{i}", scan=scan) + for i in range(n_plasma_profile_elements) + ] + return interp1d_profile(fusrat_dat, mfile, scan) + + dt_grid, _r_grid, _z_grid = fusrat("dt") + dd_triton_grid, _r_grid, _z_grid = fusrat(" dd_triton ") + dd_helion_grid, _r_grid, _z_grid = fusrat(" dd_helion ") + dhe3_grid, r_grid, z_grid = fusrat(" dhe3") + + dt_axes = fig1.add_subplot(121, aspect="equal") + dd_triton_axes = fig1.add_subplot(122, aspect="equal") + dd_helion_axes = fig2.add_subplot(121, aspect="equal") + dhe3_axes = fig2.add_subplot(122, aspect="equal") + + dt_axes.set_title("D+T -> 4He + n Fusion Rate Density Contours") + reaction_plot_grid(rminor, rmajor, kappa, r_grid, z_grid, dt_grid, dt_axes) + + dd_triton_axes.set_title("D+D -> T + p Fusion Rate Density Contours") + reaction_plot_grid( + rminor, rmajor, kappa, r_grid, z_grid, dd_triton_grid, dd_triton_axes + ) + dd_helion_axes.set_title("D+D -> 3He + n Fusion Rate Density Contours") + reaction_plot_grid( + rminor, rmajor, kappa, r_grid, z_grid, dd_helion_grid, dd_helion_axes + ) + dhe3_axes.set_title("D+3He -> 4He + n Fusion Rate Density Contours") + reaction_plot_grid(rminor, rmajor, kappa, r_grid, z_grid, dhe3_grid, dhe3_axes) + + +def plot_beta_profiles(axis: plt.Axes, mfile: MFile, scan: int): + """Plot the beta profiles on the given axis""" + n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) + + beta_plasma_toroidal_profile = [ + mfile.get(f"beta_thermal_toroidal_profile{i}", scan=scan) + for i in range(2 * n_plasma_profile_elements) + ] + + axis.plot( + np.linspace(-1, 1, 2 * n_plasma_profile_elements), + beta_plasma_toroidal_profile, + color="blue", + label="$\\beta_t$", + ) + + axis.axhline( + mfile.get("beta_thermal_toroidal_vol_avg", scan=scan), + color="blue", + linestyle="--", + linewidth=1.0, + label="$\\langle \\beta_t \\rangle_{\\text{V}}$", + ) + + axis.set_xlabel("$\\rho$ [r/a]") + axis.set_ylabel("$\\beta$") + axis.minorticks_on() + axis.grid(which="minor", linestyle=":", linewidth=0.5, alpha=0.5) + axis.set_title("Thermal Beta Profiles") + axis.legend() + axis.axvline(x=0, color="black", linestyle="--", linewidth=1) + axis.grid(True, linestyle="--", alpha=0.5) + axis.set_ylim(bottom=0.0) + + +def plot_plasma_effective_charge_profile(axis: plt.Axes, mfile: MFile, scan: int): + """Plot plasma effective charge profile""" + n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) + + n_charge_plasma_effective_vol_avg = mfile.get( + "n_charge_plasma_effective_vol_avg", scan=scan + ) + + n_charge_plasma_effective_profile = [ + mfile.get(f"n_charge_plasma_effective_profile{i}", scan=scan) + for i in range(n_plasma_profile_elements) + ] + + axis.plot( + np.linspace(0, 1, n_plasma_profile_elements), + n_charge_plasma_effective_profile, + ) + + axis.hlines( + n_charge_plasma_effective_vol_avg, + xmin=0, + xmax=1, + colors="red", + linestyles="--", + label=( + "Volume-Averaged $Z_{\\text{eff}}$ =" + f" {n_charge_plasma_effective_vol_avg:.2f}" + ), + ) + + axis.set_xlabel(r"$\rho \quad [r/a]$") + axis.set_ylabel("Effective Charge ($Z_{\\text{eff}}$)") + axis.set_title("Plasma Effective Charge Profile") + axis.minorticks_on() + axis.set_xlim(0, 1.025) + axis.grid(which="both", linestyle="--", alpha=0.5) + axis.legend() + + +def plot_plasma_thermal_energy_profiles(axis, m_file: MFile, scan: int): + """Function to plot plasma thermal energy profiles on the given axis. + + Parameters + ---------- + axis : + Matplotlib axis to plot on + m_file : + MFILE + scan : + scan to read from MFILE + """ + n_plasma_profile_elements = int(m_file.get("n_plasma_profile_elements", scan=scan)) + + eden_plasma_electrons_thermal_profile_mj = [ + m_file.get(f"eden_plasma_electrons_thermal_profile{i}", scan=scan) / 1e6 + for i in range(n_plasma_profile_elements) + ] + eden_plasma_ions_thermal_profile_mj = [ + m_file.get(f"eden_plasma_ions_thermal_profile{i}", scan=scan) / 1e6 + for i in range(n_plasma_profile_elements) + ] + eden_plasma_thermal_profile_mj = [ + m_file.get(f"eden_plasma_thermal_profile{i}", scan=scan) / 1e6 + for i in range(n_plasma_profile_elements) + ] + e_plasma_electrons_thermal_profile_mj = [ + m_file.get(f"e_plasma_electrons_thermal_profile{i}", scan=scan) / 1e6 + for i in range(n_plasma_profile_elements) + ] + + e_plasma_ions_thermal_profile_mj = [ + m_file.get(f"e_plasma_ions_thermal_profile{i}", scan=scan) / 1e6 + for i in range(n_plasma_profile_elements) + ] + e_plasma_thermal_profile_mj = [ + m_file.get(f"e_plasma_thermal_profile{i}", scan=scan) / 1e6 + for i in range(n_plasma_profile_elements) + ] + + axis.plot( + np.linspace(0, 1, n_plasma_profile_elements), + e_plasma_electrons_thermal_profile_mj, + label="$W_{\\text{e}}$", + color="tab:blue", + linestyle=":", + ) + axis.plot( + np.linspace(0, 1, n_plasma_profile_elements), + e_plasma_ions_thermal_profile_mj, + label="$W_{\\text{i}}$", + color="tab:blue", + linestyle="--", + ) + axis.plot( + np.linspace(0, 1, n_plasma_profile_elements), + e_plasma_thermal_profile_mj, + label="$W_{\\text{total}}$", + color="tab:blue", + linestyle="-", + ) + + total_thermal_energy_max_index = int(np.argmax(e_plasma_thermal_profile_mj)) + total_thermal_energy_max_rho = np.linspace(0, 1, n_plasma_profile_elements)[ + total_thermal_energy_max_index + ] + total_thermal_energy_max = e_plasma_thermal_profile_mj[ + total_thermal_energy_max_index + ] + axis.axvline( + total_thermal_energy_max_rho, + color="tab:red", + alpha=0.7, + label="$W_{\\text{total, peak}}$", + ) + axis.axhline( + total_thermal_energy_max, + color="tab:red", + alpha=0.7, + ) + + density_axis = axis.twinx() + density_axis.plot( + np.linspace(0, 1, n_plasma_profile_elements), + eden_plasma_electrons_thermal_profile_mj, + label="$W_{\\text{density, e}}$", + color="tab:orange", + linestyle=":", + ) + density_axis.plot( + np.linspace(0, 1, n_plasma_profile_elements), + eden_plasma_ions_thermal_profile_mj, + label="$W_{\\text{density, i}}$", + color="tab:orange", + linestyle="--", + ) + density_axis.plot( + np.linspace(0, 1, n_plasma_profile_elements), + eden_plasma_thermal_profile_mj, + label="$W_{\\text{density, total}}$", + color="tab:orange", + linestyle="-", + ) + + axis.grid(True, alpha=0.3) + axis.minorticks_on() + axis.set_xlabel(r"$\rho \quad [r/a]$") + axis.set_xlim(left=0.0, right=1.0) + axis.set_ylabel( + "Thermal Energy [MJ]", + color="tab:blue", + ) + axis.tick_params(axis="y", colors="tab:blue") + density_axis.set_ylabel( + "Thermal Energy Density [MJ/m$^3$]", + color="tab:orange", + ) + density_axis.tick_params(axis="y", colors="tab:orange") + handles, labels = axis.get_legend_handles_labels() + density_handles, density_labels = density_axis.get_legend_handles_labels() + axis.legend(handles + density_handles, labels + density_labels) + + +def plot_cumulative_plasma_thermal_energy_profiles(axis, m_file: MFile, scan: int): + """Function to plot the cumulative plasma thermal energy profiles on the given axis. + + Parameters + ---------- + axis : + Matplotlib axis to plot on + m_file : + MFILE + scan : + scan to read from MFILE + """ + n_plasma_profile_elements = int(m_file.get("n_plasma_profile_elements", scan=scan)) + e_plasma_thermal_total_mj = m_file.get("e_plasma_thermal_total", scan=scan) / 1e6 + e_plasma_electrons_thermal_profile_mj = [ + m_file.get(f"e_plasma_electrons_thermal_profile{i}", scan=scan) / 1e6 + for i in range(n_plasma_profile_elements) + ] + + e_plasma_ions_thermal_profile_mj = [ + m_file.get(f"e_plasma_ions_thermal_profile{i}", scan=scan) / 1e6 + for i in range(n_plasma_profile_elements) + ] + e_plasma_thermal_profile_mj = [ + m_file.get(f"e_plasma_thermal_profile{i}", scan=scan) / 1e6 + for i in range(n_plasma_profile_elements) + ] + + axis.plot( + np.linspace(0, 1, n_plasma_profile_elements), + np.cumsum(e_plasma_electrons_thermal_profile_mj), + label="$\\Sigma W_{\\text{e}}$", + color="tab:blue", + linestyle=":", + ) + axis.plot( + np.linspace(0, 1, n_plasma_profile_elements), + np.cumsum(e_plasma_ions_thermal_profile_mj), + label="$\\Sigma W_{\\text{i}}$", + color="tab:blue", + linestyle="--", + ) + axis.plot( + np.linspace(0, 1, n_plasma_profile_elements), + np.cumsum(e_plasma_thermal_profile_mj), + label="$\\Sigma W_{\\text{total}}$", + color="tab:blue", + linestyle="-", + ) + axis.axhline( + y=e_plasma_thermal_total_mj, + label="$W_{\\text{thermal,total}}$", + color="tab:red", + linestyle="--", + ) + cumulative_thermal_energy_mj = np.cumsum(e_plasma_thermal_profile_mj) + half_thermal_energy_mj = 0.5 * e_plasma_thermal_total_mj + half_thermal_energy_position = np.interp( + half_thermal_energy_mj, + cumulative_thermal_energy_mj, + np.linspace(0, 1, n_plasma_profile_elements), + ) + axis.axhline( + y=half_thermal_energy_mj, + label="$50\\%\\ W_{\\text{thermal,total}}$", + color="tab:green", + linestyle=":", + ) + axis.axvline( + x=half_thermal_energy_position, + color="tab:green", + linestyle=":", + ) + + axis.legend() + axis.set_title("Thermal Energy Profiles and Cumulative Distribution") + axis.grid(True, alpha=0.3) + axis.minorticks_on() + axis.tick_params(axis="x", labelbottom=False) + axis.set_xlim(left=0.0, right=1.0) + axis.set_ylabel( + "Cumulative Thermal Energy [MJ]", + ) + + +__all__ = [ + "plot_beta_profiles", + "plot_cumulative_plasma_thermal_energy_profiles", + "plot_fusion_rate_contours", + "plot_fusion_rate_profiles", + "plot_jprofile", + "plot_n_profiles", + "plot_plasma_effective_charge_profile", + "plot_plasma_poloidal_pressure_contours", + "plot_plasma_pressure_profiles", + "plot_plasma_thermal_energy_profiles", + "plot_qprofile", + "plot_t_profiles", +] diff --git a/process/core/io/plot/summary/profiles/radiation.py b/process/core/io/plot/summary/profiles/radiation.py new file mode 100644 index 0000000000..e849e5cc2f --- /dev/null +++ b/process/core/io/plot/summary/profiles/radiation.py @@ -0,0 +1,462 @@ +"""Profiles functions for PROCESS summary plots.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np + +from process.core.io.plot.summary.common import ( + add_colourbar, +) +from process.core.io.plot.summary.profiles.misc import ( + interp1d_profile, + profiles_with_pedestal, +) +from process.core.io.plot.summary.rendering import ( + draw_text, +) +from process.models.engineering.materials import ( + poisson_steel, +) +from process.models.pfcoil import N_CS_STRESS_PROFILE_POINTS, CSCoil +from process.models.physics.impurity_radiation import read_impurity_file + +if TYPE_CHECKING: + import matplotlib.pyplot as plt + from matplotlib.axes import Axes + + from process.core.io.mfile import MFile + + +def read_imprad_data(_skiprows, data_path): + """Function to read all data needed for creation of radiation profile + + Parameters + ---------- + _skiprows : + number of rows to skip when reading impurity data files + data_path : + path to impurity data + + """ + label = [ + "H_", + "He", + "Be", + "C_", + "N_", + "O_", + "Ne", + "Si", + "Ar", + "Fe", + "Ni", + "Kr", + "Xe", + "W_", + ] + lzdata = [0.0 for x in range(len(label))] + + for i in range(len(label)): + file_iden = data_path + label[i].ljust(3, "_") + + Te = None + lz = None + zav = None + + for header in read_impurity_file(file_iden + "lz_tau.dat"): + if "Te[eV]" in header.content: + Te = np.asarray(header.data, dtype=float) + + if "infinite confinement" in header.content: + lz = np.asarray(header.data, dtype=float) + for header in read_impurity_file(file_iden + "z_tau.dat"): + if "infinite confinement" in header.content: + zav = np.asarray(header.data, dtype=float) + + lzdata[i] = np.column_stack([Te, lz, zav]) + + # then switch string to floats + return np.array(lzdata, dtype=float) + + +def plot_rad_contour(axis: Axes, mfile: MFile, scan: int, impp: str): + """Plots the contour of line and bremsstrahlung radiation density for a plasma + cross-section. + + This function reads impurity and plasma profile data, computes the radiation density + profile, + interpolates it onto a 2D grid, and plots the upper and lower half contours on the + provided axis. + + Parameters + ---------- + axis : matplotlib.axes.Axes + The matplotlib axis object to plot the contours on. + mfile : Any + Data object containing plasma and impurity profile information. + scan : int + The scan index to extract profile data for plotting. + impp : str + The impurity data path + + Notes + ----- + - The function assumes the existence of several global or previously defined + variables and functions, + such as `read_imprad_data`, `interp1d_profile`, and plasma pedestal parameters. + - The plotted contours represent the radiation density in units of MW.m^-3. + - The function adds colorbar, axis labels, title, and core reduction annotation to + the plot. + """ + rminor = mfile.get("rminor", scan=scan) + rmajor = mfile.get("rmajor", scan=scan) + # Read in the impurity data + imp_data = read_imprad_data(2, impp) + # imp data is a 3D array with shape (num_impurities, num_temp_points, (temp, lz, + # zav)) + + # Find the relative number density of each impurity + imp_frac = np.array([ + mfile.get(f"f_nd_impurity_electrons({i:02d})", scan=scan) for i in range(1, 15) + ]) + + # Initialize the radius + rho, ne, te = profiles_with_pedestal(mfile, scan) + + # Intailise the radiation profile arrays + pimpden = np.zeros([imp_data.shape[0], te.shape[0]]) + lz = np.zeros([imp_data.shape[0], te.shape[0]]) + prad = np.zeros(te.shape[0]) + + # Intailise the impurity radiation profile + for rho in range(te.shape[0]): + # imp data is a 3D array with shape (num_impurities, num_temp_points, (temp, lz, + # zav)) + for impurity in range(imp_data.shape[0]): + # Check if profile temperature is lower than dataset minimum. + # If so, use the minimum loss function value + if te[rho] <= imp_data[impurity][0][0]: + lz[impurity][rho] = imp_data[impurity][0][1] + + # Check if profile temperature is higher than dataset maximum. + # If so, use the maximum loss function value + elif te[rho] >= imp_data[impurity][imp_data.shape[1] - 1][0]: + lz[impurity][rho] = imp_data[impurity][imp_data.shape[1] - 1][1] + else: + # If profile valie is within dataset range, use log-log interpolation to + # find value for loss function + log_te_data = np.log([row[0] for row in imp_data[impurity]]) + log_lz_data = np.log([row[1] for row in imp_data[impurity]]) + lz[impurity][rho] = np.exp( + np.interp(np.log(te[rho]), log_te_data, log_lz_data) + ) + # Find the power density for each impurity at each rho + pimpden[impurity][rho] = ( + imp_frac[impurity] * ne[rho] * ne[rho] * lz[impurity][rho] + ) + + for impurity in range(imp_data.shape[0]): + prad[rho] += pimpden[impurity][rho] * 1.0e-6 + + p_rad_grid, r_grid, z_grid = interp1d_profile(prad, mfile, scan) + + # Plot the upper half contour + p_rad_upper = axis.contourf( + r_grid, z_grid, p_rad_grid, levels=50, cmap="plasma", zorder=2 + ) + # Plot the lower half contour (mirror) + axis.contourf(r_grid, -z_grid, p_rad_grid, levels=50, cmap="plasma", zorder=2) + + axis.figure.colorbar( + p_rad_upper, + ax=axis, + label=r"$P_{\mathrm{rad}}$ $[\mathrm{MW.m}^{-3}]$", + location="left", + anchor=(-0.25, 0.5), + ) + + axis.set_xlabel("R [m]") + axis.set_xlim(rmajor - 1.2 * rminor, rmajor + 1.2 * rminor) + axis.set_ylim( + -1.2 * rminor * mfile.get("kappa", scan=scan), + 1.2 * mfile.get("kappa", scan=scan) * rminor, + ) + axis.set_ylabel("Z [m]") + axis.set_title("Line & Bremsstrahlung Radiation Density Contours") + axis.plot( + rmajor, + 0, + marker="o", + color="red", + markersize=6, + markeredgecolor="black", + zorder=100, + ) + # enable minor ticks and grid for clearer reading + axis.minorticks_on() + axis.grid(True, which="major", linestyle="--", linewidth=0.8, alpha=0.7, zorder=1) + + axis.grid(True, which="minor", linestyle=":", linewidth=0.4, alpha=0.5, zorder=1) + props_core_reduce = { + "boxstyle": "round", + "facecolor": "khaki", + "alpha": 0.8, + } + draw_text( + axis, + 0.02, + 0.02, + rf"$f_{{\text{{core,reduce}}}}$ = {1.0}", + transform=axis.transAxes, + fontsize=8, + verticalalignment="bottom", + bbox=props_core_reduce, + ) + # make minor ticks visible on all sides and draw ticks inward for compact look + axis.tick_params(which="both", direction="in", top=True, right=True) + + +def plot_plasma_pressure_gradient_profiles(axis: plt.Axes, mfile: MFile, scan: int): + """Plot plasma pressure gradient profiles""" + # Get the plasma pressure profiles + n_plasma_profile_elements = int(mfile.get("n_plasma_profile_elements", scan=scan)) + + pres_plasma_profile = [ + mfile.get(f"pres_plasma_electron_profile{i}", scan=scan) + for i in range(n_plasma_profile_elements) + ] + pres_plasma_profile_ion = [ + mfile.get(f"pres_plasma_ion_total_profile{i}", scan=scan) + for i in range(n_plasma_profile_elements) + ] + pres_plasma_profile_total = [ + mfile.get(f"pres_plasma_thermal_total_profile{i}", scan=scan) + for i in range(n_plasma_profile_elements) + ] + pres_plasma_profile_fuel = [ + mfile.get(f"pres_plasma_fuel_profile{i}", scan=scan) + for i in range(n_plasma_profile_elements) + ] + pres_plasma_profile_kpa = np.array(pres_plasma_profile) / 1000.0 + pres_plasma_profile_ion_kpa = np.array(pres_plasma_profile_ion) / 1000.0 + pres_plasma_profile_fuel_kpa = np.array(pres_plasma_profile_fuel) / 1000.0 + pres_plasma_profile_total_kpa = np.array(pres_plasma_profile_total) / 1000.0 + + # Calculate the normalised radius + rho = np.linspace(0, 1, len(pres_plasma_profile_kpa)) + + # Compute gradients using numpy.gradient + grad_electron = np.gradient(pres_plasma_profile_kpa, rho) + grad_ion = np.gradient(pres_plasma_profile_ion_kpa, rho) + grad_total = np.gradient(pres_plasma_profile_total_kpa, rho) + grad_fuel = np.gradient(pres_plasma_profile_fuel_kpa, rho) + + axis.plot(rho, grad_electron, color="blue", label="Electron") + axis.plot(rho, grad_ion, color="red", label="Ion") + axis.plot(rho, grad_total, color="green", label="Total") + axis.plot(rho, grad_fuel, color="orange", label="Fuel") + axis.set_xlabel("$\\rho$ [r/a]") + axis.set_ylabel("$dP/dr$ [kPa / m]") + axis.minorticks_on() + axis.grid(which="minor", linestyle=":", linewidth=0.5, alpha=0.5) + axis.set_title("Plasma Thermal Pressure Gradient Profiles") + axis.grid(True, linestyle="--", alpha=0.5) + axis.set_xlim(0, 1.025) + axis.legend() + + +def plot_larmor_radius_profile(axis: plt.Axes, mfile_data: MFile, scan: int): + """Plot the Larmor radius profile on the given axis.""" + radius_plasma_deuteron_larmor_profile = [ + mfile_data.data[ + f"radius_plasma_deuteron_toroidal_larmor_isotropic_profile{i}" + ].get_scan(scan) + for i in range( + 2 * int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan)) + ) + ] + + radius_plasma_triton_larmor_profile = [ + mfile_data.data[ + f"radius_plasma_triton_toroidal_larmor_isotropic_profile{i}" + ].get_scan(scan) + for i in range( + 2 * int(mfile_data.data["n_plasma_profile_elements"].get_scan(scan)) + ) + ] + + radius_plasma_deuteron_larmor_profile_mm = [ + radius * 1e3 for radius in radius_plasma_deuteron_larmor_profile + ] + + radius_plasma_triton_larmor_profile_mm = [ + radius * 1e3 for radius in radius_plasma_triton_larmor_profile + ] + + axis.plot( + np.linspace(-1, 1, len(radius_plasma_deuteron_larmor_profile_mm)), + radius_plasma_deuteron_larmor_profile_mm, + color="red", + linestyle="-", + label=r"$\rho_{Larmor,toroidal,D}$", + ) + + axis.plot( + np.linspace(-1, 1, len(radius_plasma_triton_larmor_profile_mm)), + radius_plasma_triton_larmor_profile_mm, + color="green", + linestyle="-", + label=r"$\rho_{Larmor,toroidal,T}$", + ) + + axis.set_ylabel(r"Larmor Radii [mm]") + axis.set_title(r" Toroidal Larmor Radii ($v_{\perp}^2 = 2v_{th}^2$)") + axis.set_xlabel("$\\rho \\ [r/a]$") + axis.grid(True, which="both", linestyle="--", alpha=0.5) + axis.minorticks_on() + axis.legend() + + +def plot_cs_radial_stress_profile( + axis: plt.Axes, + mfile: MFile, + scan: int, + j_cs: float, + b_cs_inner: float, +): + """Plot CS radial stress profile""" + r_cs_inner = mfile.get("r_cs_inner", scan=scan) + r_cs_outer = mfile.get("r_cs_outer", scan=scan) + + radii = np.linspace(r_cs_inner, r_cs_outer, num=25) + stress_values = np.array([ + CSCoil.calculate_cs_radial_stress( + r_stress_point=radius, + r_cs_inner=r_cs_inner, + r_cs_outer=r_cs_outer, + j_cs=j_cs, + b_cs_inner=b_cs_inner, + f_poisson_cs_structure=poisson_steel, + ) + for radius in radii + ]) + + axis.plot( + radii, + stress_values / 1e6, + linewidth=2, + label="$\\sigma_{r}$,Radial Stress", + ) + max_idx = np.argmax(np.abs(stress_values)) + max_radius = radii[max_idx] + max_stress = stress_values[max_idx] / 1e6 + axis.axvline(max_radius, color="black", linestyle="--", linewidth=1.0, alpha=0.7) + axis.axhline(max_stress, color="black", linestyle="--", linewidth=1.0, alpha=0.7) + axis.set_xlabel("Radial Position (m)") + axis.set_ylabel("Radial Stress (MPa)") + axis.minorticks_on() + axis.grid(True, alpha=0.3) + axis.set_title("CS Radial Stress at BOP") + + +def plot_cs_radial_stress_contour_profile( + axis: plt.Axes, + mfile: MFile, + scan: int, + j_cs: float, + b_cs_inner: float, + colorbar_axis: plt.Axes | None = None, +): + """Plot CS radial stress contour profile""" + r_cs_inner = mfile.get("r_cs_inner", scan=scan) + r_cs_outer = mfile.get("r_cs_outer", scan=scan) + dz_cs_full = mfile.get("dz_cs_full", scan=scan) + + # Create 2D grid for contour plot: radial and vertical dimensions + n_radial = 50 + radial_grid = np.linspace(r_cs_inner, r_cs_outer, n_radial) + height_grid = np.linspace( + -dz_cs_full / 2, dz_cs_full / 2, N_CS_STRESS_PROFILE_POINTS + ) + + # Create meshgrid for filled contour + r, z = np.meshgrid(radial_grid, height_grid) + + # Calculate radial stress across the 2D grid + stress_data = np.zeros((len(height_grid), n_radial)) + for i in range(len(height_grid)): + for j in range(n_radial): + stress_data[i, j] = ( + CSCoil.calculate_cs_radial_stress( + r_stress_point=radial_grid[j], + r_cs_inner=r_cs_inner, + r_cs_outer=r_cs_outer, + j_cs=j_cs, + b_cs_inner=b_cs_inner, + f_poisson_cs_structure=poisson_steel, + ) + / 1e6 + ) + + # Plot filled contour of stress distribution + contour_fill = axis.contourf(r, z, stress_data, levels=15, cmap="RdYlBu") + contour_lines = axis.contour( + r, + z, + stress_data, + levels=[stress_data.max()], + colors="black", + linewidths=0.5, + alpha=0.4, + ) + axis.clabel(contour_lines, inline=True, fontsize=8) + + # Plot CS outline + axis.plot( + [r_cs_inner, r_cs_inner], + [-dz_cs_full / 2, dz_cs_full / 2], + "k-", + linewidth=2, + label="CS Inner", + ) + axis.plot( + [r_cs_outer, r_cs_outer], + [-dz_cs_full / 2, dz_cs_full / 2], + "k-", + linewidth=2, + label="CS Outer", + ) + axis.plot( + [r_cs_inner, r_cs_outer], + [dz_cs_full / 2, dz_cs_full / 2], + "k-", + linewidth=2, + ) + axis.plot( + [r_cs_inner, r_cs_outer], + [-dz_cs_full / 2, -dz_cs_full / 2], + "k-", + linewidth=2, + ) + + cbar = add_colourbar(contour_fill, axis, colorbar_axis) + cbar.set_label("Radial Stress (MPa)") + + axis.set_xlabel("R [m]") + axis.set_ylabel("Z [m]") + axis.minorticks_on() + axis.set_xlim(r_cs_inner * 0.9, r_cs_outer * 1.1) + axis.set_ylim((-dz_cs_full / 2) * 1.1, (dz_cs_full / 2) * 1.1) + axis.grid(True, alpha=0.3) + + +__all__ = [ + "plot_cs_radial_stress_contour_profile", + "plot_cs_radial_stress_profile", + "plot_larmor_radius_profile", + "plot_plasma_pressure_gradient_profiles", + "plot_rad_contour", + "read_imprad_data", +] diff --git a/process/core/io/plot/summary/profiles/stress.py b/process/core/io/plot/summary/profiles/stress.py new file mode 100644 index 0000000000..7d7cfbfdde --- /dev/null +++ b/process/core/io/plot/summary/profiles/stress.py @@ -0,0 +1,532 @@ +"""Profiles functions for PROCESS summary plots.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np + +from process.core.io.plot.summary.common import ( + add_colourbar, + get_pulse_timings, +) +from process.models.engineering.materials import ( + calculate_tresca_stress, + calculate_von_mises_stress, + poisson_steel, +) +from process.models.pfcoil import N_CS_STRESS_PROFILE_POINTS, CSCoil + +if TYPE_CHECKING: + import matplotlib.pyplot as plt + + from process.core.io.mfile import MFile + + +def plot_cs_stress_time_profile(axis: plt.Axes, mfile: MFile, scan: int) -> None: + """Function to plot the time profile of the CS stress during the pulse.""" + pulse_timings = get_pulse_timings(mfile, scan) + + stress_z_cs_self_midplane_profile = np.zeros(pulse_timings.n_pf_active_points_total) + for i in range(pulse_timings.n_pf_active_points_total): + stress_z_cs_self_midplane_profile[i] = mfile.get( + f"stress_z_cs_self_midplane_profile[{i}]", scan=scan + ) + + # Plot stress vs time + axis.plot( + pulse_timings.pf_active_cumulative, + stress_z_cs_self_midplane_profile / 1e6, + "o-", + linewidth=2, + markersize=4, + label="$\\sigma_{z}$,Axial Stress", + ) + axis.set_xlabel("Pulse Time (s)") + axis.set_ylabel("Midplane Axial Stress (MPa)") + axis.minorticks_on() + axis.legend(loc="best") + axis.set_title("CS Midplane Axial Stress Time Profile") + axis.grid(True, alpha=0.3) + + +def plot_cs_hoop_stress_profile( + axis: plt.Axes, + mfile: MFile, + scan: int, + j_cs: float, + b_cs_inner: float, +): + """Plot CS hoop stress profile""" + r_cs_inner = mfile.get("r_cs_inner", scan=scan) + r_cs_outer = mfile.get("r_cs_outer", scan=scan) + + radii = np.linspace(r_cs_inner, r_cs_outer, num=10) + stress_values = np.array([ + CSCoil.calculate_cs_hoop_stress( + r_stress_point=radius, + r_cs_inner=r_cs_inner, + r_cs_outer=r_cs_outer, + j_cs=j_cs, + b_cs_inner=b_cs_inner, + f_poisson_cs_structure=poisson_steel, + f_a_cs_turn_steel=mfile.get("f_a_cs_turn_steel", scan=scan), + ) + for radius in radii + ]) + + axis.plot( + radii, + stress_values / 1e6, + linewidth=2, + label="$\\sigma_{\\theta}$,Hoop Stress", + ) + max_idx = np.argmax(np.abs(stress_values)) + max_radius = radii[max_idx] + max_stress = stress_values[max_idx] / 1e6 + axis.axvline(max_radius, color="black", linestyle="--", linewidth=1.0, alpha=0.7) + axis.axhline(max_stress, color="black", linestyle="--", linewidth=1.0, alpha=0.7) + axis.set_xlabel("Radial Position (m)") + axis.set_ylabel("Hoop Stress (MPa)") + axis.minorticks_on() + axis.set_title("CS Hoop Stress at BOP") + axis.grid(True, alpha=0.3) + + +def plot_cs_hoop_stress_contour_profile( + axis: plt.Axes, + mfile: MFile, + scan: int, + j_cs: float, + b_cs_inner: float, + colorbar_axis: plt.Axes | None = None, +): + """Plot CS hoop stress contour profile""" + r_cs_inner = mfile.get("r_cs_inner", scan=scan) + r_cs_outer = mfile.get("r_cs_outer", scan=scan) + dz_cs_full = mfile.get("dz_cs_full", scan=scan) + f_a_cs_turn_steel = mfile.get("f_a_cs_turn_steel", scan=scan) + + # Create 2D grid for contour plot: radial and vertical dimensions + n_radial = 50 + radial_grid = np.linspace(r_cs_inner, r_cs_outer, n_radial) + height_grid = np.linspace( + -dz_cs_full / 2, dz_cs_full / 2, N_CS_STRESS_PROFILE_POINTS + ) + + # Create meshgrid for filled contour + r, z = np.meshgrid(radial_grid, height_grid) + + # Calculate hoop stress across the 2D grid + stress_data = np.zeros((len(height_grid), n_radial)) + for i in range(len(height_grid)): + for j in range(n_radial): + stress_data[i, j] = ( + CSCoil.calculate_cs_hoop_stress( + r_stress_point=radial_grid[j], + r_cs_inner=r_cs_inner, + r_cs_outer=r_cs_outer, + j_cs=j_cs, + b_cs_inner=b_cs_inner, + f_poisson_cs_structure=poisson_steel, + f_a_cs_turn_steel=f_a_cs_turn_steel, + ) + / 1e6 + ) + + # Plot filled contour of stress distribution + contour_fill = axis.contourf(r, z, stress_data, levels=15, cmap="RdYlBu_r") + contour_lines = axis.contour( + r, + z, + stress_data, + levels=[stress_data.max()], + colors="black", + linewidths=0.5, + alpha=0.4, + ) + axis.clabel(contour_lines, inline=True, fontsize=8) + + # Plot CS outline + axis.plot( + [r_cs_inner, r_cs_inner], + [-dz_cs_full / 2, dz_cs_full / 2], + "k-", + linewidth=2, + label="CS Inner", + ) + axis.plot( + [r_cs_outer, r_cs_outer], + [-dz_cs_full / 2, dz_cs_full / 2], + "k-", + linewidth=2, + label="CS Outer", + ) + axis.plot( + [r_cs_inner, r_cs_outer], + [dz_cs_full / 2, dz_cs_full / 2], + "k-", + linewidth=2, + ) + axis.plot( + [r_cs_inner, r_cs_outer], + [-dz_cs_full / 2, -dz_cs_full / 2], + "k-", + linewidth=2, + ) + + cbar = add_colourbar(contour_fill, axis, colorbar_axis) + cbar.set_label("Hoop Stress (MPa)") + + axis.set_xlabel("R [m]") + axis.set_ylabel("Z [m]") + axis.minorticks_on() + axis.set_xlim(r_cs_inner * 0.9, r_cs_outer * 1.1) + axis.set_ylim((-dz_cs_full / 2) * 1.1, (dz_cs_full / 2) * 1.1) + axis.grid(True, alpha=0.3) + + +def plot_cs_vertical_stress_profile( + axis: plt.Axes, + mfile: MFile, + scan: int, +): + """Plot CS vertical stress profile""" + dz_cs_full = mfile.get("dz_cs_full", scan=scan) + + stress_z_profile = np.array([ + float(mfile.data[f"stress_z_cs_self_profile_{i}"].get_scan(scan)) / 1e6 + for i in range(N_CS_STRESS_PROFILE_POINTS) + ]) + z_positions = np.linspace(-dz_cs_full / 2, dz_cs_full / 2, len(stress_z_profile)) + + axis.plot( + stress_z_profile, + z_positions, + linewidth=2, + label="$\\sigma_{z}$,Vertical Stress", + ) + max_idx = np.argmax(np.abs(stress_z_profile)) + max_stress = stress_z_profile[max_idx] + max_z = z_positions[max_idx] + axis.axvline(max_stress, color="black", linestyle="--", linewidth=1.0, alpha=0.7) + axis.axhline(max_z, color="black", linestyle="--", linewidth=1.0, alpha=0.7) + axis.set_xlabel("Vertical Stress (MPa)") + axis.set_ylabel("Z [m]") + axis.minorticks_on() + axis.grid(True, alpha=0.3) + axis.set_title("CS Vertical Stress at BOP") + + +def plot_vertical_stress_contour_profile( + axis: plt.Axes, + mfile: MFile, + scan: int, + colorbar_axis: plt.Axes | None = None, +): + """Vertical stress contour plot""" + dz_cs_full = mfile.get("dz_cs_full", scan=scan) + r_cs_inner = mfile.get("r_cs_inner", scan=scan) + r_cs_outer = mfile.get("r_cs_outer", scan=scan) + + stress_z_profile = [ + float(mfile.data[f"stress_z_cs_self_profile_{i}"].get_scan(scan)) / 1e6 + for i in range(N_CS_STRESS_PROFILE_POINTS) + ] + + # Create 2D grid for contour plot: radial and vertical dimensions + n_radial = 50 + radial_grid = np.linspace(r_cs_inner, r_cs_outer, n_radial) + height_grid = np.linspace(-dz_cs_full / 2, dz_cs_full / 2, len(stress_z_profile)) + + # Create meshgrid for filled contour + r, z = np.meshgrid(radial_grid, height_grid) + + # Interpolate stress values across radial direction (assume linear variation) + stress_data = np.zeros((len(stress_z_profile), n_radial)) + for i, stress_val in enumerate(stress_z_profile): + stress_data[i, :] = stress_val + + # Plot filled contour of stress distribution + contour_fill = axis.contourf(r, z, stress_data, levels=15, cmap="RdYlBu") + contour_lines = axis.contour( + r, + z, + stress_data, + levels=[stress_data.max()], + colors="black", + linewidths=0.5, + alpha=0.4, + ) + axis.clabel(contour_lines, inline=True, fontsize=8) + + # Plot CS outline + axis.plot( + [r_cs_inner, r_cs_inner], + [-dz_cs_full / 2, dz_cs_full / 2], + "k-", + linewidth=2, + label="CS Inner", + ) + axis.plot( + [r_cs_outer, r_cs_outer], + [-dz_cs_full / 2, dz_cs_full / 2], + "k-", + linewidth=2, + label="CS Outer", + ) + axis.plot( + [r_cs_inner, r_cs_outer], + [dz_cs_full / 2, dz_cs_full / 2], + "k-", + linewidth=2, + ) + axis.plot( + [r_cs_inner, r_cs_outer], + [-dz_cs_full / 2, -dz_cs_full / 2], + "k-", + linewidth=2, + ) + + cbar = add_colourbar(contour_fill, axis, colorbar_axis) + cbar.set_label("Vertical Stress (MPa)") + + axis.set_xlabel("R [m]") + axis.set_ylabel("Z [m]") + axis.minorticks_on() + axis.set_xlim(r_cs_inner * 0.9, r_cs_outer * 1.1) + axis.set_ylim((-dz_cs_full / 2) * 1.1, (dz_cs_full / 2) * 1.1) + axis.grid(True, alpha=0.3) + + +def plot_cs_tresca_2d_contour( + axis: plt.Axes, + mfile: MFile, + scan: int, + colorbar_axis: plt.Axes | None = None, +): + """CS Tresca stress contour plot""" + dz_cs_full = mfile.get("dz_cs_full", scan=scan) + r_cs_inner = mfile.get("r_cs_inner", scan=scan) + r_cs_outer = mfile.get("r_cs_outer", scan=scan) + j_cs = mfile.get("j_cs_pulse_start", scan=scan) + b_cs_inner = mfile.get("b_cs_peak_pulse_start", scan=scan) + f_a_cs_turn_steel = mfile.get("f_a_cs_turn_steel", scan=scan) + + stress_z_profile = np.array([ + float(mfile.data[f"stress_z_cs_self_profile_{i}"].get_scan(scan)) + for i in range(N_CS_STRESS_PROFILE_POINTS) + ]) + + # Create 2D grid for contour plot: radial and vertical dimensions + n_radial = 50 + radial_grid = np.linspace(r_cs_inner, r_cs_outer, n_radial) + height_grid = np.linspace(-dz_cs_full / 2, dz_cs_full / 2, len(stress_z_profile)) + + # Create meshgrid for filled contour + r, z = np.meshgrid(radial_grid, height_grid) + + # Calculate Tresca stress across the coil cross-section. + tresca_data = np.zeros((len(height_grid), n_radial)) + for i, stress_z in enumerate(stress_z_profile): + for j, radius in enumerate(radial_grid): + stress_hoop = CSCoil.calculate_cs_hoop_stress( + r_stress_point=radius, + r_cs_inner=r_cs_inner, + r_cs_outer=r_cs_outer, + j_cs=j_cs, + b_cs_inner=b_cs_inner, + f_poisson_cs_structure=poisson_steel, + f_a_cs_turn_steel=f_a_cs_turn_steel, + ) + stress_radial = CSCoil.calculate_cs_radial_stress( + r_stress_point=radius, + r_cs_inner=r_cs_inner, + r_cs_outer=r_cs_outer, + j_cs=j_cs, + b_cs_inner=b_cs_inner, + f_poisson_cs_structure=poisson_steel, + ) + tresca_data[i, j] = ( + calculate_tresca_stress( + stress_x=stress_hoop, + stress_y=stress_z, + stress_z=stress_radial, + ) + / 1e6 + ) + + # Plot filled contour of Tresca stress distribution + contour_lines = axis.contour( + r, + z, + tresca_data, + levels=[tresca_data.max()], + colors="black", + linewidths=0.5, + alpha=0.4, + ) + axis.clabel(contour_lines, inline=True, fontsize=8) + + # Plot CS outline + axis.plot( + [r_cs_inner, r_cs_inner], + [-dz_cs_full / 2, dz_cs_full / 2], + "k-", + linewidth=2, + label="CS Inner", + ) + axis.plot( + [r_cs_outer, r_cs_outer], + [-dz_cs_full / 2, dz_cs_full / 2], + "k-", + linewidth=2, + label="CS Outer", + ) + axis.plot( + [r_cs_inner, r_cs_outer], + [dz_cs_full / 2, dz_cs_full / 2], + "k-", + linewidth=2, + ) + axis.plot( + [r_cs_inner, r_cs_outer], + [-dz_cs_full / 2, -dz_cs_full / 2], + "k-", + linewidth=2, + ) + + contour_fill = axis.contourf(r, z, tresca_data, levels=15, cmap="RdYlBu_r") + cbar = add_colourbar(contour_fill, axis, colorbar_axis) + cbar.set_label("Tresca Stress (MPa)") + + axis.set_xlabel("R [m]") + axis.set_ylabel("Z [m]") + axis.minorticks_on() + axis.set_xlim(r_cs_inner * 0.9, r_cs_outer * 1.1) + axis.set_ylim((-dz_cs_full / 2) * 1.1, (dz_cs_full / 2) * 1.1) + axis.grid(True, alpha=0.3) + axis.set_title("CS Tresca Stress Contour at BOP") + + +def plot_cs_von_mises_2d_contour( + axis: plt.Axes, + mfile: MFile, + scan: int, + colorbar_axis: plt.Axes | None = None, +): + """CS Von Mises stress contour plot""" + dz_cs_full = mfile.get("dz_cs_full", scan=scan) + r_cs_inner = mfile.get("r_cs_inner", scan=scan) + r_cs_outer = mfile.get("r_cs_outer", scan=scan) + j_cs = mfile.get("j_cs_pulse_start", scan=scan) + b_cs_inner = mfile.get("b_cs_peak_pulse_start", scan=scan) + f_a_cs_turn_steel = mfile.get("f_a_cs_turn_steel", scan=scan) + + stress_z_profile = np.array([ + float(mfile.data[f"stress_z_cs_self_profile_{i}"].get_scan(scan)) + for i in range(N_CS_STRESS_PROFILE_POINTS) + ]) + + # Create 2D grid for contour plot: radial and vertical dimensions + n_radial = 50 + radial_grid = np.linspace(r_cs_inner, r_cs_outer, n_radial) + height_grid = np.linspace(-dz_cs_full / 2, dz_cs_full / 2, len(stress_z_profile)) + + # Create meshgrid for filled contour + r, z = np.meshgrid(radial_grid, height_grid) + + # Calculate Von Mises stress across the coil cross-section. + von_mises_data = np.zeros((len(height_grid), n_radial)) + for i, stress_z in enumerate(stress_z_profile): + for j, radius in enumerate(radial_grid): + stress_hoop = CSCoil.calculate_cs_hoop_stress( + r_stress_point=radius, + r_cs_inner=r_cs_inner, + r_cs_outer=r_cs_outer, + j_cs=j_cs, + b_cs_inner=b_cs_inner, + f_poisson_cs_structure=poisson_steel, + f_a_cs_turn_steel=f_a_cs_turn_steel, + ) + stress_radial = CSCoil.calculate_cs_radial_stress( + r_stress_point=radius, + r_cs_inner=r_cs_inner, + r_cs_outer=r_cs_outer, + j_cs=j_cs, + b_cs_inner=b_cs_inner, + f_poisson_cs_structure=poisson_steel, + ) + von_mises_data[i, j] = ( + calculate_von_mises_stress( + stress_x=stress_hoop, + stress_y=stress_z, + stress_z=stress_radial, + stress_shear_xy=0.0, + stress_shear_yz=0.0, + stress_shear_zx=0.0, + ) + / 1e6 + ) + + # Plot filled contour of Von Mises stress distribution + contour_lines = axis.contour( + r, + z, + von_mises_data, + levels=[von_mises_data.max()], + colors="black", + linewidths=0.5, + alpha=0.4, + ) + axis.clabel(contour_lines, inline=True, fontsize=8) + + # Plot CS outline + axis.plot( + [r_cs_inner, r_cs_inner], + [-dz_cs_full / 2, dz_cs_full / 2], + "k-", + linewidth=2, + label="CS Inner", + ) + axis.plot( + [r_cs_outer, r_cs_outer], + [-dz_cs_full / 2, dz_cs_full / 2], + "k-", + linewidth=2, + label="CS Outer", + ) + axis.plot( + [r_cs_inner, r_cs_outer], + [dz_cs_full / 2, dz_cs_full / 2], + "k-", + linewidth=2, + ) + axis.plot( + [r_cs_inner, r_cs_outer], + [-dz_cs_full / 2, -dz_cs_full / 2], + "k-", + linewidth=2, + ) + + contour_fill = axis.contourf(r, z, von_mises_data, levels=15, cmap="RdYlBu_r") + cbar = add_colourbar(contour_fill, axis, colorbar_axis) + cbar.set_label("Von Mises Stress (MPa)") + + axis.set_xlabel("R [m]") + axis.minorticks_on() + axis.set_xlim(r_cs_inner * 0.9, r_cs_outer * 1.1) + axis.set_ylim((-dz_cs_full / 2) * 1.1, (dz_cs_full / 2) * 1.1) + axis.grid(True, alpha=0.3) + axis.set_title("CS Von Mises Stress Contour at BOP") + + +__all__ = [ + "plot_cs_hoop_stress_contour_profile", + "plot_cs_hoop_stress_profile", + "plot_cs_stress_time_profile", + "plot_cs_tresca_2d_contour", + "plot_cs_vertical_stress_profile", + "plot_cs_von_mises_2d_contour", + "plot_vertical_stress_contour_profile", +] diff --git a/process/core/io/plot/summary/rendering.py b/process/core/io/plot/summary/rendering.py new file mode 100644 index 0000000000..50733f5892 --- /dev/null +++ b/process/core/io/plot/summary/rendering.py @@ -0,0 +1,76 @@ +"""Reusable rendering primitives for summary plots.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collections.abc import Iterable + + from matplotlib.axes import Axes + from matplotlib.text import Annotation, Text + from matplotlib.transforms import Transform + + +@dataclass(frozen=True) +class TextPanel: + """Declarative text panel rendered through a Matplotlib axes.""" + + x: float + y: float + text: str + options: dict[str, Any] = field(default_factory=dict) + + +@dataclass(frozen=True) +class ArrowSpec: + """Declarative annotation arrow.""" + + start: tuple[float, float] + end: tuple[float, float] + transform: Transform + options: dict[str, Any] = field(default_factory=dict) + + +def draw_text(axis: Axes, *args, **kwargs) -> Text: + """Render text through one shared entry point.""" + return axis.text(*args, **kwargs) + + +def draw_annotation(axis: Axes, *args, **kwargs) -> Annotation: + """Render an annotation through one shared entry point.""" + return axis.annotate(*args, **kwargs) + + +def draw_text_panels(axis: Axes, panels: Iterable[TextPanel]) -> list[Text]: + """Render a sequence of declarative text panels.""" + return [ + draw_text(axis, panel.x, panel.y, panel.text, **panel.options) + for panel in panels + ] + + +def draw_arrows(axis: Axes, arrows: Iterable[ArrowSpec]) -> list[Annotation]: + """Render a sequence of declarative arrows.""" + return [ + draw_annotation( + axis, + "", + xy=arrow.end, + xytext=arrow.start, + xycoords=arrow.transform, + arrowprops=arrow.options, + ) + for arrow in arrows + ] + + +__all__ = [ + "ArrowSpec", + "TextPanel", + "draw_annotation", + "draw_arrows", + "draw_text", + "draw_text_panels", +] diff --git a/process/core/io/plot/summary/reporting/__init__.py b/process/core/io/plot/summary/reporting/__init__.py new file mode 100644 index 0000000000..a37d04c329 --- /dev/null +++ b/process/core/io/plot/summary/reporting/__init__.py @@ -0,0 +1,49 @@ +"""Public API for this summary plotting concern.""" + +from __future__ import annotations + +import process.core.io.plot.summary.reporting.constraints as _constraints +import process.core.io.plot.summary.reporting.layouts as _layouts +import process.core.io.plot.summary.reporting.misc as _misc +import process.core.io.plot.summary.reporting.panels as _panels + +_MODULES = (_constraints, _layouts, _misc, _panels) +_REGISTRY = {} +for _module in _MODULES: + _REGISTRY.update({name: getattr(_module, name) for name in _module.__all__}) +for _module in _MODULES: + _module.__dict__.update(_REGISTRY) +RadialBuild = _REGISTRY["RadialBuild"] +draw_bend = _REGISTRY["draw_bend"] +plot_centre_cross = _REGISTRY["plot_centre_cross"] +plot_cover_page = _REGISTRY["plot_cover_page"] +plot_density_limit_comparison = _REGISTRY["plot_density_limit_comparison"] +plot_ebw_ecrh_coupling_graph = _REGISTRY["plot_ebw_ecrh_coupling_graph"] +plot_equality_constraint_equations = _REGISTRY["plot_equality_constraint_equations"] +plot_fw_90_deg_pipe_bend = _REGISTRY["plot_fw_90_deg_pipe_bend"] +plot_h_threshold_comparison = _REGISTRY["plot_h_threshold_comparison"] +plot_header = _REGISTRY["plot_header"] +plot_inequality_constraint_equations = _REGISTRY["plot_inequality_constraint_equations"] +plot_info = _REGISTRY["plot_info"] +plot_iteration_variables = _REGISTRY["plot_iteration_variables"] +plot_lower_vertical_build = _REGISTRY["plot_lower_vertical_build"] +plot_separatrix_power_split = _REGISTRY["plot_separatrix_power_split"] +plot_upper_vertical_build = _REGISTRY["plot_upper_vertical_build"] +__all__ = [ + "RadialBuild", + "draw_bend", + "plot_centre_cross", + "plot_cover_page", + "plot_density_limit_comparison", + "plot_ebw_ecrh_coupling_graph", + "plot_equality_constraint_equations", + "plot_fw_90_deg_pipe_bend", + "plot_h_threshold_comparison", + "plot_header", + "plot_inequality_constraint_equations", + "plot_info", + "plot_iteration_variables", + "plot_lower_vertical_build", + "plot_separatrix_power_split", + "plot_upper_vertical_build", +] diff --git a/process/core/io/plot/summary/reporting/constraints.py b/process/core/io/plot/summary/reporting/constraints.py new file mode 100644 index 0000000000..ea2500d461 --- /dev/null +++ b/process/core/io/plot/summary/reporting/constraints.py @@ -0,0 +1,295 @@ +"""Reporting functions for PROCESS summary plots.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np + +from process.core.io.plot.summary.rendering import ( + draw_text, +) + +if TYPE_CHECKING: + import matplotlib.pyplot as plt + + from process.core.io.mfile import MFile + + +def plot_equality_constraint_equations(axis: plt.Axes, m_file_data: MFile, scan: int): + """Plot the equality constraints for a solution and their normalised residuals + + Parameters + ---------- + axis: plt.Axes : + + m_file_data: MFile : + + scan: int : + + """ + y_labels = [] + y_pos = [] + n_plot = 0 + + # Build a mapping from itvar index to its name (description) + con_names = {} + con_numbers = {} + for var in m_file_data.data: + if var.startswith("eq_con"): + idx = int(var[6:]) # e.g. "itvar001" -> 1 + con_names[idx] = m_file_data.data[var].var_description + con_numbers[idx] = idx + + for n_plot, n in enumerate(con_numbers.values()): + # Constraint value needed + con_value = m_file_data.data[f"val_eq_con{n:03d}"].get_scan(scan) + + # Use the variable name if available, else fallback to "eq_conXXX" + var_label = con_names.get(n, f"eq_con{n:03d}") + + # Normalized residual of the constraint + con_norm_residual = m_file_data.data[f"eq_con{n:03d}"].get_scan(scan) + + # Unit type of the constraint + con_units_raw = m_file_data.data[f"eq_units_con{n:03d}"].get_scan(scan) + con_units = str(con_units_raw).strip("'`") + + # Remove '_normalised_residue' from the label if present + if isinstance(var_label, str) and var_label.endswith("_normalised_residue"): + var_label = var_label.replace("_normalised_residue", "") + + # Remove trailing underscores and replace underscores between words with + # spaces + var_label = var_label.rstrip("_").replace("_", " ") + + # Plot the normalised residual as a bar + axis.barh( + n_plot, + con_norm_residual, + height=0.6, + color="blue", + label="Normalized Residual" if n_plot == 0 else "", + align="center", + ) + + # Add the value as a number to the right of the bar + draw_text( + axis, + con_norm_residual + 0.52, + n_plot, + f"{con_norm_residual:.8g}", + va="center", + ha="left", + fontsize=8, + color="blue", + ) + + # Add the constraint value as text to the left of the y-axis + draw_text( + axis, + 0.45, + n_plot, + f"{con_value:.8g} {con_units}", + va="center", + ha="right", + fontsize=8, + color="black", + ) + + y_labels.append(var_label) + y_pos.append(n_plot) + + axis.axvline(0.5, color="red", linewidth=2, zorder=0) + axis.set_yticks(y_pos) + axis.set_yticklabels(y_labels) + axis.set_facecolor("#f5f5f5") + axis.set_xlim(-0.4, 1.2) # Normalised bounds + axis.set_title("Equality Constraint Equations") + axis.set_xticks([]) + axis.legend() + + +def plot_inequality_constraint_equations(axis: plt.Axes, m_file: MFile, scan: int): + """Plot the inequality constraints for a solution and where they lay within their + bounds + + Parameters + ---------- + axis: plt.Axes : + + m_file: MFile : + + scan: int : + + """ + y_labels = [] + y_pos = [] + n_plot = 0 + + # Build a mapping from itvar index to its name (description) + con_names = {} + con_numbers = {} + for var in m_file.data: + if var.startswith("ineq_con"): + idx = int(var[8:]) # e.g. "ineq_con001" -> 1 + con_names[idx] = m_file.data[var].var_description + con_numbers[idx] = idx + + for n_plot, n in enumerate(con_numbers.values()): + # Constraint value/bound + con_bound = m_file.data[f"ineq_bound_con{n:03d}"].get_scan(scan) + + # Value of constraint variable + con_value = m_file.data[f"ineq_value_con{n:03d}"].get_scan(scan) + + # Constraint symbol can be `<=` for an upper limit or `>=` for a lower limit + con_symbol = m_file.data[f"ineq_symbol_con{n:03d}"].get_scan(scan) + + # Use the variable name if available, else fallback to "ineq_conXXX" + var_label = con_names.get(n, f"ineq_con{n:03d}") + + # Normalized residual of the constraint + con_residual_norm = m_file.data[f"ineq_con{n:03d}"].get_scan(scan) + + # Unit type of the constraint + con_units = m_file.data[f"ineq_units_con{n:03d}"].get_scan(scan).strip("'`") + + # Add a vertical line at the normalised constraint bounds of 0 and 1 + axis.axvline( + 0.0, + color="red", + linestyle="--", + linewidth=1.5, + zorder=0, + ) + + axis.axvline( + 1.0, + color="red", + linestyle="--", + linewidth=1.5, + zorder=0, + ) + + # Remove '_normalised_residue' from the label if present + if isinstance(var_label, str) and var_label.endswith("_normalised_residue"): + var_label = var_label.replace("_normalised_residue", "") + var_label = var_label.rstrip("_").replace("_", " ") + + # Calculate the normalised constraint threshold depending if the constraint is an + # upper + # or lower limit + if con_symbol == "'<='": + normalised_value = 1 - con_residual_norm + bar_left = normalised_value + bar_width = 1 - normalised_value + else: + # For a lower limit, the normalised value is the residual itself + normalised_value = con_residual_norm + bar_left = 0 + # Set the bar width to be 1/10 times the normalised value, + # but cap it at 1.0 to avoid overly long bars + bar_width = min(normalised_value * 0.1, 1.0) + + # If the constraint value is very close to the bound then plot a square marker at + # the bound + if np.isclose(normalised_value, 1.0, atol=1e-3): + axis.plot( + 1, + n_plot, + "s", + color="black", + markersize=8, + zorder=5, + ) + elif np.isclose(normalised_value, 0.0, atol=1e-3): + axis.plot( + 0, + n_plot, + "s", + color="black", + markersize=8, + zorder=5, + ) + + else: + # If constraint value is not very close to bound then plot bar as normal + axis.barh( + n_plot, + bar_width, + left=bar_left, + color="blue", + edgecolor="black", + linewidth=1.5, + height=1.0, + alpha=0.7, + label="Constraint Value" if n_plot == 0 else "", + ) + + # Plot the value as a number at x = 0.5 + draw_text( + axis, + 0.5, + n_plot, + f"{con_value:,.8g} {con_units}", + va="center", + ha="center", + fontsize=8, + color=( + "orange" + if np.isclose(normalised_value, 1.0, atol=1e-3) + or np.isclose(normalised_value, 0.0, atol=1e-3) + else "green" + ), + bbox={ + "boxstyle": "round", + "facecolor": "white", + "alpha": 0.8, + "edgecolor": "white", + "linewidth": 1, + }, + ) + # Annoate the bound value depending if it is an upper or lower limit + if con_symbol == "'<='": + # Add the constraint symbol and bound as text + draw_text( + axis, + 1.02, # Position text slightly to the right of the normalised bound + n_plot, + f"$\\leq$ {con_bound:,.8g} {con_units}", + va="center", + ha="left", + fontsize=8, + color="black", + ) + else: # con_symbol == ">=" + draw_text( + axis, + -0.025, # Position text slightly to the left of the normalised bound + n_plot, + f"$\\geq$ {con_bound:,.8g} {con_units}", + va="center", + ha="right", + fontsize=8, + color="black", + ) + + y_labels.append(var_label) + y_pos.append(n_plot) + + axis.set_yticks(y_pos) + axis.set_yticklabels(y_labels) + axis.set_title("Inequality Constraint Equations") + axis.set_xlim(-0.3, 1.275) + axis.set_xticks([]) + axis.set_facecolor("#f5f5f5") + axis.set_xticks(np.arange(0, 1.0, 0.1)) + axis.grid(True, axis="x", linestyle="--", alpha=0.3) + axis.set_xticklabels([]) + + +__all__ = [ + "plot_equality_constraint_equations", + "plot_inequality_constraint_equations", +] diff --git a/process/core/io/plot/summary/reporting/layouts.py b/process/core/io/plot/summary/reporting/layouts.py new file mode 100644 index 0000000000..4b32495593 --- /dev/null +++ b/process/core/io/plot/summary/reporting/layouts.py @@ -0,0 +1,252 @@ +"""Reporting functions for PROCESS summary plots.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +import numpy as np + +from process.core.io.plot.summary.constants import ( + BLANKET_COLOUR, + FIRSTWALL_COLOUR, + PLASMA_COLOUR, + SHIELD_COLOUR, + TFC_COLOUR, + THERMAL_SHIELD_COLOUR, + VESSEL_COLOUR, +) + +if TYPE_CHECKING: + import matplotlib.pyplot as plt + from matplotlib.axes import Axes + + from process.core.io.mfile import MFile + + +def plot_upper_vertical_build( + axis: plt.Axes, mfile: MFile, colour_scheme: Literal[1, 2] +): + """Plots the upper vertical build of a fusion device on the given matplotlib axis. + + This function visualizes the different layers/components of the machine's vertical + build + (such as plasma, first wall, divertor, shield, vacuum vessel, thermal shield, TF + coil, etc.) + as a vertical stacked bar chart. The thickness of each layer is extracted from the + provided `mfile`, and each segment is color-coded and labeled accordingly. + + Parameters + ---------- + axis: + The matplotlib axis on which to plot the vertical build. + mfile: + An object containing the machine build data, with required fields for each + vertical component. + colour_scheme: + Colour scheme index to use for component colors. + + Notes + ----- + This function modifies the provided axis in-place and does not return a value. + - Components with zero thickness are omitted from the plot. + - The legend displays the name and thickness (in meters) of each component. + """ + if mfile.get("i_single_null", scan=-1) == 1: + upper_vertical_variables = [ + "z_plasma_xpoint_upper", + "dz_fw_plasma_gap", + "dz_fw_upper", + "dz_blkt_upper", + "dr_shld_blkt_gap", + "dz_shld_upper", + "dz_vv_upper", + "dz_shld_vv_gap", + "dz_shld_thermal", + "dr_tf_shld_gap", + "dr_tf_inboard", + "dz_tf_cryostat", + ] + upper_vertical_labels = [ + "Plasma Height", + "First Wall - Plasma Gap", + "First Wall Upper", + "Blanket Upper", + "Shield-Blanket Gap", + "Shield Upper", + "Vacuum Vessel Upper", + "Shield-VV Gap", + "Thermal Shield", + "TF Coil - Shield Gap", + "TF Coil", + "TF Coil - Cryostat gap", + ] + upper_vertical_colours = [ + PLASMA_COLOUR[colour_scheme - 1], + "white", + FIRSTWALL_COLOUR[colour_scheme - 1], + BLANKET_COLOUR[colour_scheme - 1], + "white", + SHIELD_COLOUR[colour_scheme - 1], + VESSEL_COLOUR[colour_scheme - 1], + "white", + THERMAL_SHIELD_COLOUR[colour_scheme - 1], + "white", + ( + TFC_COLOUR[colour_scheme - 1] + if mfile.get("i_tf_sup", scan=-1) != 0 + else "#b87333" + ), + "white", + ] + # Double null case + else: + upper_vertical_variables = [ + "z_plasma_xpoint_upper", + "dz_xpoint_divertor", + "dz_divertor", + "dz_shld_upper", + "dz_vv_upper", + "dz_shld_vv_gap", + "dz_shld_thermal", + "dr_tf_shld_gap", + "dr_tf_inboard", + "dz_tf_cryostat", + ] + upper_vertical_labels = [ + "Plasma Height", + "Plasma - Divertor Gap", + "Divertor Upper", + "Shield Upper", + "Vacuum Vessel Upper", + "Shield-VV Gap", + "Thermal Shield", + "TF Coil - Shield Gap", + "TF Coil", + "TF Coil - Cryostat gap", + ] + upper_vertical_colours = [ + PLASMA_COLOUR[colour_scheme - 1], + "white", + "black", + SHIELD_COLOUR[colour_scheme - 1], + VESSEL_COLOUR[colour_scheme - 1], + "white", + THERMAL_SHIELD_COLOUR[colour_scheme - 1], + "white", + ( + TFC_COLOUR[colour_scheme - 1] + if mfile.get("i_tf_sup", scan=-1) != 0 + else "#b87333" + ), + "white", + ] + + # Get thicknesses for each layer + upper_vertical_build = np.array([ + mfile.get(rl, scan=-1) for rl in upper_vertical_variables + ]) + + # Remove build parts equal to zero + mask = ~(upper_vertical_build == 0.0) # noqa: RUF069 + filtered_build = upper_vertical_build[mask] + filtered_labels = [lbl for i, lbl in enumerate(upper_vertical_labels) if mask[i]] + filtered_colors = [col for i, col in enumerate(upper_vertical_colours) if mask[i]] + filtered_vars = [v for i, v in enumerate(upper_vertical_variables) if mask[i]] + + # Compute cumulative positions (bottoms) for stacking + bottoms = np.zeros_like(filtered_build) + for i in range(1, len(filtered_build)): + bottoms[i] = bottoms[i - 1] + filtered_build[i - 1] + + # Plot each layer as a bar, stacking upwards from zero + for kk in range(len(filtered_build)): + axis.bar( + 0, + filtered_build[kk], + bottom=bottoms[kk], + width=0.8, + label=( + f"{filtered_labels[kk]}\n[{filtered_vars[kk]}]\n{filtered_build[kk]:.3f} m" # noqa: E501 + ), + color=filtered_colors[kk], + edgecolor="black", + linewidth=0.05, + ) + + axis.set_xticks([]) + axis.legend( + bbox_to_anchor=(0, 0), + loc="upper left", + ncol=6, + ) + axis.minorticks_on() + axis.set_ylabel("Height [m]") + axis.title.set_text("Upper Vertical Build") + + +def draw_bend( + ax: Axes, + elbow_radius: float, + theta_span: float, + radius_pipe: float, + title: str = "Bend", + alpha: float = 0.8, +): + """ + Draws a circular pipe bend with centerline and inner/outer boundaries. + + Parameters + ---------- + ax: + Target axes for plotting. + elbow_radius: + Radius of the elbow in meters. + theta_span: + Array of angles [0, θ] where θ is pi/2 or pi. + radius_pipe: + Pipe radius in meters (fallback to 0.1m if not provided). + title: + Plot title string. + alpha: + fill opacity + """ + # Convert all inputs to mm + elbow_radius_mm = elbow_radius * 1000 + pipe_radius_mm = radius_pipe * 1000 + + theta = np.linspace(0, theta_span, 100) + x_center = elbow_radius_mm * np.cos(theta) + y_center = elbow_radius_mm * np.sin(theta) + + # Outer and inner walls (offset by ± pipe radius in mm) + x_outer = (elbow_radius_mm + pipe_radius_mm) * np.cos(theta) + y_outer = (elbow_radius_mm + pipe_radius_mm) * np.sin(theta) + x_inner = (elbow_radius_mm - pipe_radius_mm) * np.cos(theta) + y_inner = (elbow_radius_mm - pipe_radius_mm) * np.sin(theta) + + # Plot + ax.plot(x_center, y_center, color="black", linestyle="--", label="Centerline") + ax.plot(x_outer, y_outer, color="black") + ax.plot(x_inner, y_inner, color="black") + ax.fill( + np.concatenate([x_outer, x_inner[::-1]]), + np.concatenate([y_outer, y_inner[::-1]]), + color="lightgrey", + alpha=alpha, + ) + + ax.set_aspect("equal") + ax.set_xlabel("X [mm]") + ax.set_ylabel("Y [mm]") + ax.set_title(title) + ax.grid(True, linestyle="--", alpha=0.3) + + # Legend: Centerline + pipe radius info + legend_text = ( + f"Centerline\nPipe radius: {pipe_radius_mm:.2f} mm\nElbow radius:" + f" {elbow_radius_mm:.2f} mm" + ) + ax.legend([legend_text], loc="upper right") + + +__all__ = ["draw_bend", "plot_upper_vertical_build"] diff --git a/process/core/io/plot/summary/reporting/misc.py b/process/core/io/plot/summary/reporting/misc.py new file mode 100644 index 0000000000..5034dc6003 --- /dev/null +++ b/process/core/io/plot/summary/reporting/misc.py @@ -0,0 +1,678 @@ +"""Reporting functions for PROCESS summary plots.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +import matplotlib.pyplot as plt +import numpy as np + +from process.core.io.plot.summary.constants import ( + PLASMA_COLOUR, + SHIELD_COLOUR, + TFC_COLOUR, + THERMAL_SHIELD_COLOUR, + VESSEL_COLOUR, +) +from process.core.io.plot.summary.rendering import ( + draw_annotation, + draw_text, +) +from process.core.io.plot.summary.reporting.layouts import ( + draw_bend, +) +from process.models.physics.current_drive import ( + ElectronBernstein, + ElectronCyclotron, +) + +if TYPE_CHECKING: + from process.core.io.mfile import MFile + + +class RadialBuild: + """Dataclass containing radial build dictionaries""" + + upper: dict[str, float] + lower: dict[str, float] + radial: dict[str, float] + + cumulative_upper: dict[str, float] + cumulative_lower: dict[str, float] + cumulative_radial: dict[str, float] + + +def plot_centre_cross( + axis: plt.Axes, mfile: MFile, scan: int, mirror_negative_x: bool = False +): + """Function to plot centre cross on plot + + Parameters + ---------- + axis : + axis object to plot to + mfile : + MFILE data object + scan : + scan number to use + mirror_negative_x : + if True, mirror the plot to the negative x-axis (Default value = False) + """ + rmajor = mfile.get("rmajor", scan=scan) + x_scale = -1 if mirror_negative_x else 1 + axis.plot( + x_scale * np.array([rmajor - 0.25, rmajor + 0.25, rmajor, rmajor, rmajor]), + [0, 0, 0, 0.25, -0.25], + color="black", + ) + + +def plot_h_threshold_comparison(axis: plt.Axes, mfile: MFile, scan: int, u_seed=None): + """Function to plot a scatter box plot of L-H threshold power comparisons. + + Parameters + ---------- + axis : + Axis object to plot to. + mfile : + MFILE data object. + scan : + Scan number to use. + u_seed : + (Default value = None) + """ + # Data for the box plot + data = { + "ITER 1996 Nominal": mfile.get("l_h_threshold_powers(1)", scan=scan), + "ITER 1996 Upper": mfile.get("l_h_threshold_powers(2)", scan=scan), + "ITER 1996 Lower": mfile.get("l_h_threshold_powers(3)", scan=scan), + "ITER 1997 (1)": mfile.get("l_h_threshold_powers(4)", scan=scan), + "ITER 1997 (2)": mfile.get("l_h_threshold_powers(5)", scan=scan), + "Martin Nominal": mfile.get("l_h_threshold_powers(6)", scan=scan), + "Martin Upper": mfile.get("l_h_threshold_powers(7)", scan=scan), + "Martin Lower": mfile.get("l_h_threshold_powers(8)", scan=scan), + "Snipes Nominal": mfile.get("l_h_threshold_powers(9)", scan=scan), + "Snipes Upper": mfile.get("l_h_threshold_powers(10)", scan=scan), + "Snipes Lower": mfile.get("l_h_threshold_powers(11)", scan=scan), + "Snipes Closed Divertor Nominal": mfile.get( + "l_h_threshold_powers(12)", scan=scan + ), + "Snipes Closed Divertor Upper": mfile.get("l_h_threshold_powers(13)", scan=scan), + "Snipes Closed Divertor Lower": mfile.get("l_h_threshold_powers(14)", scan=scan), + "Hubbard Nominal (I-mode)": mfile.get("l_h_threshold_powers(15)", scan=scan), + "Hubbard Lower (I-mode)": mfile.get("l_h_threshold_powers(16)", scan=scan), + "Hubbard Upper (I-mode)": mfile.get("l_h_threshold_powers(17)", scan=scan), + "Hubbard 2017 (I-mode)": mfile.get("l_h_threshold_powers(18)", scan=scan), + "Martin Aspect Corrected Nominal": mfile.get( + "l_h_threshold_powers(19)", scan=scan + ), + "Martin Aspect Corrected Upper": mfile.get( + "l_h_threshold_powers(20)", scan=scan + ), + "Martin Aspect Corrected Lower": mfile.get( + "l_h_threshold_powers(21)", scan=scan + ), + } + data_values = list(data.values()) + # Create the violin plot + axis.violinplot(data_values, showextrema=False) + + # Create the box plot + axis.boxplot(data_values, showfliers=True, showmeans=True, meanline=True, widths=0.3) + + # Scatter plot for each data point + colors = plt.cm.plasma(np.linspace(0, 1, len(data_values))) + generator = np.random.default_rng(seed=u_seed) + x_values = generator.normal(loc=1, scale=0.01, size=len(data_values)) + for index, (key, value) in enumerate(data.items()): + if "ITER 1996" in key: + color = "blue" + elif "ITER 1997" in key: + color = "cyan" + elif "Martin" in key and "Aspect" not in key: + color = "green" + elif "Snipes" in key and "Closed" not in key: + color = "red" + elif "Snipes Closed" in key: + color = "orange" + elif "Martin Aspect" in key: + color = "yellow" + elif "Hubbard" in key and "2017" not in key: + color = "purple" + elif "Hubbard 2017" in key: + color = "magenta" + else: + color = colors[index] + axis.scatter(x_values[index], value, color=color, label=key, alpha=1.0) + axis.legend(loc="upper left", bbox_to_anchor=(-1.1, 1), ncol=2) + + # Calculate average, standard deviation, and median + avg_threshold = np.mean(data_values) + std_threshold = np.std(data_values) + median_threshold = np.median(data_values) + + # Plot average, standard deviation, and median as text + draw_text( + axis, + -0.45, + 0.15, + f"Average: {avg_threshold:.4f}", + transform=axis.transAxes, + fontsize=9, + ) + draw_text( + axis, + -0.45, + 0.1, + f"Standard Dev: {std_threshold:.4f}", + transform=axis.transAxes, + fontsize=9, + ) + draw_text( + axis, + -0.45, + 0.05, + f"Median: {median_threshold:.4f}", + transform=axis.transAxes, + fontsize=9, + ) + + axis.set_title("L-H Threshold ($P_\\text{LH}$) Comparison") + axis.set_ylabel("L-H threshold power [MW]") + axis.set_xlim(0.5, 1.5) + axis.set_xticks([]) + axis.set_xticklabels([]) + + # Add background color + axis.set_facecolor("#f0f0f0") + + +def plot_lower_vertical_build( + axis: plt.Axes, mfile: MFile, colour_scheme: Literal[1, 2] +): + """Plots the lower vertical build of a fusion device on the given matplotlib axis. + + This function visualizes the different layers/components of the machine's vertical + build + (such as plasma, first wall, divertor, shield, vacuum vessel, thermal shield, TF + coil, etc.) + as a vertical stacked bar chart. The thickness of each layer is extracted from the + provided `mfile`, and each segment is color-coded and labeled accordingly. + + Parameters + ---------- + axis : + The matplotlib axis on which to plot the vertical build. + mfile : + An object containing the machine build data, with required fields for each + vertical component. + colour_scheme : + Colour scheme index to use for component colors. + + + Notes + ----- + This function modifies the provided axis in-place and does not return a value. + - Components with zero thickness are omitted from the plot. + - The legend displays the name and thickness (in meters) of each component. + """ + lower_vertical_variables = [ + "z_plasma_xpoint_upper", + "dz_xpoint_divertor", + "dz_divertor", + "dz_shld_upper", + "dz_vv_upper", + "dz_shld_vv_gap", + "dz_shld_thermal", + "dr_tf_shld_gap", + "dr_tf_inboard", + "dz_tf_cryostat", + ] + + lower_vertical_build = [[mfile.get(rl, scan=-1) for rl in lower_vertical_variables]] + + lower_vertical_build = np.array(lower_vertical_build) + + lower_vertical_build = np.transpose(lower_vertical_build) + + lower_vertical_labels = [ + "Plasma Height", + "Plasma - Divertor Gap", + "Divertor", + "Shield", + "Vacuum Vessel", + "Shield - VV Gap", + "Thermal shield", + "TF Coil - Shield Gap", + "TF Coil", + "TF Coil - Cryostat gap", + ] + + lower_vertical_color = [ + PLASMA_COLOUR[colour_scheme - 1], + "white", + "black", + SHIELD_COLOUR[colour_scheme - 1], + VESSEL_COLOUR[colour_scheme - 1], + "white", + THERMAL_SHIELD_COLOUR[colour_scheme - 1], + "white", + ( + TFC_COLOUR[colour_scheme - 1] + if mfile.get("i_tf_sup", scan=-1) != 0 + else "#b87333" + ), + "white", + ] + + # Remove build parts equal to zero + mask = ~(lower_vertical_build[:, 0] == 0.0) # noqa: RUF069 + filtered_vertical_build = lower_vertical_build[mask] + filtered_labels = [lbl for i, lbl in enumerate(lower_vertical_labels) if mask[i]] + filtered_colors = [col for i, col in enumerate(lower_vertical_color) if mask[i]] + + bottom = np.zeros(filtered_vertical_build.shape[1]) + for kk in range(filtered_vertical_build.shape[0]): + axis.bar( + np.arange(filtered_vertical_build.shape[1]), + -filtered_vertical_build[kk, :], + bottom=bottom, + width=0.8, + label=( + f"{filtered_labels[kk]}\n[{lower_vertical_variables[kk]}]\n{filtered_vertical_build[kk][0]:.3f} m" # noqa: E501 + ), + color=filtered_colors[kk], + edgecolor="black", + linewidth=0.05, + ) + bottom -= filtered_vertical_build[kk, :] + + axis.set_xticks([]) + axis.legend( + bbox_to_anchor=(0, 0), + loc="upper left", + ncol=5, + ) + axis.minorticks_on() + axis.set_ylabel("Height [m]") + axis.title.set_text("Lower Vertical Build") + + +def plot_density_limit_comparison(axis: plt.Axes, mfile: MFile, scan: int): + """Function to plot a scatter box plot of different density limit comparisons. + + Parameters + ---------- + axis : + Axis object to plot to. + mfile : + MFILE data object. + scan : + Scan number to use. + """ + old_asdex = mfile.get("nd_plasma_electron_max_array(1)", scan=scan) + borrass_iter_i = mfile.get("nd_plasma_electron_max_array(2)", scan=scan) + borrass_iter_ii = mfile.get("nd_plasma_electron_max_array(3)", scan=scan) + jet_edge_radiation = mfile.get("nd_plasma_electron_max_array(4)", scan=scan) + jet_simplified = mfile.get("nd_plasma_electron_max_array(5)", scan=scan) + hugill_murakami = mfile.get("nd_plasma_electron_max_array(6)", scan=scan) + greenwald = mfile.get("nd_plasma_electron_max_array(7)", scan=scan) + asdex_new = mfile.get("nd_plasma_electron_max_array(8)", scan=scan) + + # Data for the box plot + data = { + "Old ASDEX": old_asdex, + "Borrass ITER I": borrass_iter_i, + "Borrass ITER II": borrass_iter_ii, + "JET Edge Radiation": jet_edge_radiation, + "JET Simplified": jet_simplified, + "Hugill-Murakami": hugill_murakami, + "Greenwald": greenwald, + "ASDEX New": asdex_new, + } + data_values = list(data.values()) + + # Create the violin plot + axis.violinplot(data_values, showextrema=False) + + # Create the box plot + axis.boxplot(data_values, showfliers=True, showmeans=True, meanline=True, widths=0.3) + + # Scatter plot for each data point + colors = plt.cm.plasma(np.linspace(0, 1, len(data.values()))) + for index, (key, value) in enumerate(data.items()): + axis.scatter(1, value, color=colors[index], label=key, alpha=1.0) + axis.legend(loc="upper left", bbox_to_anchor=(1, 1)) + + # Calculate average, standard deviation, and median + avg_density_limit = np.mean(data_values) + std_density_limit = np.std(data_values) + median_density_limit = np.median(data_values) + + # Plot average, standard deviation, and median as text + draw_text( + axis, + 1.02, + 0.2, + rf"Average: {avg_density_limit * 1e-20:.4f} $\times 10^{{20}}$", + transform=axis.transAxes, + fontsize=9, + ) + draw_text( + axis, + 1.02, + 0.15, + rf"Standard Dev: {std_density_limit * 1e-20:.4f} $\times 10^{{20}}$", + transform=axis.transAxes, + fontsize=9, + ) + draw_text( + axis, + 1.02, + 0.1, + rf"Median: {median_density_limit * 1e-20:.4f} $\times 10^{{20}}$", + transform=axis.transAxes, + fontsize=9, + ) + + axis.set_yscale("log") + axis.set_title("Density Limit Comparison") + axis.set_ylabel(r"Density Limit [$10^{20}$ m$^{-3}$]") + axis.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x * 1e-20:.1f}")) + axis.set_xlim(0.5, 1.5) + axis.set_xticks([]) + axis.set_xticklabels([]) + axis.set_facecolor("#f0f0f0") + + +def plot_iteration_variables(axis: plt.Axes, m_file: MFile, scan: int): + """Plot the iteration variables and where they lay in their bounds on a given axes + + Parameters + ---------- + axis: plt.Axes : + + m_file: MFile : + + scan: int : + + """ + # Get total number of iteration variables + n_itvars = int(m_file.get("n_iteration_variables", scan=scan)) + + y_labels = [] + y_pos = [] + n_plot = 0 + + # Build a mapping from itvar index to its name (description) + itvar_names = {} + for var in m_file.data: + if var.startswith("itvar"): + idx = int(var[5:]) # e.g. "itvar001" -> 1 + itvar_names[idx] = m_file.data[var].var_description + + for n_plot, n in enumerate(range(1, n_itvars + 1)): + # Get the final value of the iteration variable, its bounds, and relative change + itvar_final = m_file.get(f"itvar{n:03d}", scan=scan) + itvar_upper = m_file.get(f"boundu{n:03d}", scan=scan) + itvar_lower = m_file.get(f"boundl{n:03d}", scan=scan) + itvar_relative_change = m_file.get(f"xcm{n:03d}", scan=scan) + final_value_normalised = m_file.get(f"nitvar{n:03d}", scan=scan) + + # Use the variable name if available, else fallback to "itvarXXX" + var_label = itvar_names.get(n, f"itvar{n:03d}") + + norm_relative_change = ( + ((itvar_final / itvar_relative_change) - itvar_lower) + / (itvar_upper - itvar_lower) + if itvar_final != itvar_lower + else 0 + ) + + # Plot square marker at the final value if at bounds + if np.isclose(final_value_normalised, 1.0, atol=1e-3): + axis.plot( + 1, + n_plot, + "s", + color="black", + markersize=8, + label="Lower Bound" if n_plot == 0 else "", + ) + elif np.isclose(final_value_normalised, 0.0, atol=1e-3): + axis.plot( + 0, + n_plot, + "s", + color="black", + markersize=8, + label="Upper Bound" if n_plot == 0 else "", + ) + # Draw a horizontal bar from 0 to norm_final at y=n_plot + else: + axis.barh( + n_plot, + final_value_normalised, + left=0, + height=1.0, + color="blue", + edgecolor="black", + linewidth=1.5, + alpha=0.7, + label="Final Value" if n_plot == 0 else "", + ) + + # Plot scatter point for normalised relative change + axis.scatter( + norm_relative_change, + n_plot, + color="black", + marker="o", + linewidths=2, + alpha=1.0, + label="Initial Value" if n_plot == 0 else "", + ) + + # Draw an arrow from the initial value to the final value + draw_annotation( + axis, + "", + xy=(final_value_normalised, n_plot), + xytext=(norm_relative_change, n_plot), + arrowprops={ + "arrowstyle": "->", + "color": "black", + "linestyle": "--", + "linewidth": 1.0, + "alpha": 0.9, + }, + ) + # Plot the value as a number at x = 0.5 + draw_text( + axis, + 0.5, + n_plot, + f"{itvar_final:,.8g}", + va="center", + ha="center", + fontsize=10, + color=( + "orange" + if np.isclose(final_value_normalised, 1.0, atol=1e-3) + or np.isclose(final_value_normalised, 0.0, atol=1e-3) + else "green" + ), + bbox={ + "boxstyle": "round", + "facecolor": "white", + "alpha": 0.8, + "edgecolor": "white", + "linewidth": 1, + }, + ) + + # Plot the value of the upper bound to the right of x=1 + draw_text( + axis, + 1.05, + n_plot, + f"{itvar_upper:,.3g}", + va="center", + ha="left", + fontsize=10, + color="gray", + ) + # Plot the value of the lower bound to the left of x=0 + draw_text( + axis, + -0.05, + n_plot, + f"{itvar_lower:,.3g}", + va="center", + ha="right", + fontsize=10, + color="gray", + ) + y_labels.append(var_label) + y_pos.append(n_plot) + + # Plot vertical lines at x=0 and x=1 to indicate bounds + axis.axvline(0, color="darkgreen", linewidth=2, zorder=0) + axis.axvline(1, color="red", linewidth=2, zorder=0) + axis.set_yticks(y_pos) + axis.set_yticklabels(y_labels) + axis.set_xticks([]) + axis.set_xticklabels([]) + axis.set_facecolor("#f5f5f5") + axis.set_xlim(-0.2, 1.2) # Normalised bounds + axis.set_title("Iteration Variables Bounds") + axis.set_xticks(np.arange(0, 1.0, 0.1)) + axis.grid(True, axis="x", linestyle="--", alpha=0.3) + axis.legend(loc="upper left", bbox_to_anchor=(-0.15, 1.05), ncol=1) + + +def plot_fw_90_deg_pipe_bend(ax, m_file, scan: int): + """Plot the first wall pipe 90 degree bend on the given axis, with axes in mm. + + Parameters + ---------- + ax : + + m_file : + + scan: int : + + """ + # Get pipe radius from m_file, fallback to 0.1 m + r = m_file.get("radius_fw_channel", scan=scan) + elbow_radius = m_file.get("radius_fw_channel_90_bend", scan=scan) + + draw_bend( + ax, + elbow_radius, + np.pi / 2, + r, + title="First Wall Pipe 90° Bend", + alpha=1.0, + ) + + +def plot_ebw_ecrh_coupling_graph(axis: plt.Axes, mfile: MFile, scan: int): + """Plot EBW and ECRH coupling efficiency graph""" + ebw = ElectronBernstein(plasma_profile=0) + ecrg = ElectronCyclotron(plasma_profile=0) + b_on_axis = mfile.get("b_plasma_toroidal_on_axis", scan=scan) + bs = np.linspace(0.0, b_on_axis + 2.0, 500) + # Use a color map for harmonics + colors = ["red", "green", "blue"] + linestyles = ["-", "--"] # EBW: solid, ECRH: dashed + + for idx, n_harmonic in enumerate(range(1, 4)): + eta_ebw_vals = [] + # For ECRH, store results for both wave modes (0: O-mode, 1: X-mode) + eta_ecrh_vals_omode = [] + eta_ecrh_vals_xmode = [] + for b in bs: + eta_ebw = ebw.electron_bernstein_freethy( + te=mfile.get("temp_plasma_electron_vol_avg_kev", scan=scan), + rmajor=mfile.get("rmajor", scan=scan), + dene20=mfile.get("nd_plasma_electrons_vol_avg", scan=scan) / 1e20, + b_plasma_toroidal_on_axis=b, + n_ecrh_harmonic=n_harmonic, + xi_ebw=mfile.get("xi_ebw", scan=scan), + ) + eta_ecrh_omode = ecrg.electron_cyclotron_freethy( + te=mfile.get("temp_plasma_electron_vol_avg_kev", scan=scan), + zeff=mfile.get("n_charge_plasma_effective_vol_avg", scan=scan), + rmajor=mfile.get("rmajor", scan=scan), + nd_plasma_electrons_vol_avg=mfile.get( + "nd_plasma_electrons_vol_avg", scan=scan + ), + b_plasma_toroidal_on_axis=b, + n_ecrh_harmonic=n_harmonic, + i_ecrh_wave_mode=0, # O-mode + ) + eta_ecrh_xmode = ecrg.electron_cyclotron_freethy( + te=mfile.get("temp_plasma_electron_vol_avg_kev", scan=scan), + zeff=mfile.get("n_charge_plasma_effective_vol_avg", scan=scan), + rmajor=mfile.get("rmajor", scan=scan), + nd_plasma_electrons_vol_avg=mfile.get( + "nd_plasma_electrons_vol_avg", scan=scan + ), + b_plasma_toroidal_on_axis=b, + n_ecrh_harmonic=n_harmonic, + i_ecrh_wave_mode=1, # X-mode + ) + eta_ebw_vals.append(eta_ebw) + eta_ecrh_vals_omode.append(eta_ecrh_omode) + eta_ecrh_vals_xmode.append(eta_ecrh_xmode) + # EBW: solid, ECRH O-mode: dashed, ECRH X-mode: dotted, same color for same + # harmonic + axis.plot( + bs, + eta_ebw_vals, + label=f"EBW (harmonic {n_harmonic})", + color=colors[idx], + linestyle=linestyles[0], + ) + axis.plot( + bs, + eta_ecrh_vals_omode, + label=f"ECRH O-mode (harmonic {n_harmonic})", + color=colors[idx], + linestyle="--", + ) + axis.plot( + bs, + eta_ecrh_vals_xmode, + label=f"ECRH X-mode (harmonic {n_harmonic})", + color=colors[idx], + linestyle=":", + ) + axis.set_xlabel("On axis toroidal B-field [T]") + axis.set_ylabel("Current drive efficiency [A/W]") + axis.set_title("EBW/ECRH Coupling Efficiency vs Toroidal B-field") + axis.legend() + axis.grid(True) + # Plot a vertical line at the on-axis value of the toroidal B-field + b_on_axis = mfile.get("b_plasma_toroidal_on_axis", scan=scan) + axis.axvline( + b_on_axis, + color="black", + linestyle="-", + linewidth=2.5, + label="On-axis $B_T$", + ) + axis.minorticks_on() + + +__all__ = [ + "RadialBuild", + "plot_centre_cross", + "plot_density_limit_comparison", + "plot_ebw_ecrh_coupling_graph", + "plot_fw_90_deg_pipe_bend", + "plot_h_threshold_comparison", + "plot_iteration_variables", + "plot_lower_vertical_build", +] diff --git a/process/core/io/plot/summary/reporting/panels.py b/process/core/io/plot/summary/reporting/panels.py new file mode 100644 index 0000000000..f751a5f5da --- /dev/null +++ b/process/core/io/plot/summary/reporting/panels.py @@ -0,0 +1,647 @@ +"""Reporting functions for PROCESS summary plots.""" + +from __future__ import annotations + +import textwrap +from typing import TYPE_CHECKING, Any, Literal + +import matplotlib as mpl +import matplotlib.pyplot as plt +import numpy as np + +from process.core.io.mfile import MFile, MFileErrorClass +from process.core.io.plot.summary.common import ( + box_style, + setup_axis, +) +from process.core.io.plot.summary.geometry import ( + poloidal_cross_section, +) +from process.core.io.plot.summary.plasma import ( + plot_plasma, +) +from process.core.io.plot.summary.rendering import ( + draw_annotation, + draw_text, +) +from process.data_structure.numerics import FiguresOfMerit, PROCESSRunMode +from process.data_structure.physics_variables import DivertorNumberModels + +if TYPE_CHECKING: + from process.core.io.plot.summary.reporting.misc import ( + RadialBuild, + ) + + +def plot_info(axis: plt.Axes, data, mfile: MFile, scan: int): + """Function to plot data in written form on a matplotlib plot. + + Parameters + ---------- + axis : + axis object to plot to + data : + plot information + mfile : + MFILE + scan : + scan number to use + """ + eqpos = 0.75 + for i in range(len(data)): + colorflag = "black" + if mfile.data[data[i][0]].exists: + if mfile.data[data[i][0]].var_flag == "ITV": + colorflag = "red" + elif mfile.data[data[i][0]].var_flag == "OP": + colorflag = "blue" + draw_text(axis, 0, -i, data[i][1], color=colorflag, ha="left", va="center") + if isinstance(data[i][0], str): + if not data[i][0]: + draw_text(axis, eqpos, -i, "\n", ha="left", va="center") + elif data[i][0][0] == "#": + draw_text( + axis, + -0.05, + -i, + f"{data[i][0][1:]}\n", + ha="left", + va="center", + ) + elif data[i][0][0] == "!": + value = data[i][0][1:].replace('"', "") + draw_text( + axis, + 0.4, + -i, + f"--> {value} {data[i][2]}", + ha="left", + va="center", + ) + elif mfile.data[data[i][0]].exists: + dat = mfile.get(data[i][0], scan=scan) + if isinstance(dat, str): + value = dat + else: + value = f"{mfile.get(data[i][0], scan=scan):.4g}" + if "alpha" in data[i][0]: + value = str(float(value) + 1.0) + draw_text( + axis, + eqpos, + -i, + f"= {value} {data[i][2]}", + color=colorflag, + ha="left", + va="center", + ) + else: + mfile.get(data[i][0], scan=-1) + draw_text( + axis, + eqpos, + -i, + "= ERROR! Var missing", + color=colorflag, + ha="left", + va="center", + ) + else: + dat = data[i][0] + value = dat if isinstance(dat, str) else f"{data[i][0]:.4g}" + draw_text( + axis, + eqpos, + -i, + f"= {value} {data[i][2]}", + color=colorflag, + ha="left", + va="center", + ) + + +def plot_header(axis: plt.Axes, mfile: MFile, scan: int): + """Function to plot header info: date, rutitle etc + + Parameters + ---------- + axis : + axis object to plot to + mfile : + MFILE + scan : + scan number to use + """ + setup_axis(axis, xmin=0, xmax=1, ymin=-16, ymax=1) + + data2 = [ + (f"!{mfile.get('runtitle', scan=-1)}", "Run title", ""), + (f"!{mfile.get('procver', scan=-1)}", "PROCESS Version", ""), + (f"!{mfile.get('date', scan=-1)}", "Date:", ""), + (f"!{mfile.get('time', scan=-1)}", "Time:", ""), + (f"!{mfile.get('username', scan=-1)}", "User:", ""), + ( + ("!Evaluation", "Run type", "") + if isinstance(mfile.data["i_figure_merit"], MFileErrorClass) + else ( + ( + f"!{FiguresOfMerit(abs(int(mfile.get('i_figure_merit', scan=-1)))).description}" # noqa: E501 + ), + "Optimising:", + "", + ) + ), + ] + + draw_text(axis, -0.05, 4.0, "Colour Legend:", ha="left", va="center") + draw_text( + axis, + 0.0, + 3.0, + "ITR --> Iteration variable", + color="red", + ha="left", + va="center", + ) + draw_text( + axis, + 0.0, + 2.0, + "OP --> Output variable", + color="blue", + ha="left", + va="center", + ) + + H = mfile.get("f_nd_impurity_electrons(01)", scan=scan) + He = mfile.get("f_nd_impurity_electrons(02)", scan=scan) + Be = mfile.get("f_nd_impurity_electrons(03)", scan=scan) + C = mfile.get("f_nd_impurity_electrons(04)", scan=scan) + N = mfile.get("f_nd_impurity_electrons(05)", scan=scan) + O = mfile.get("f_nd_impurity_electrons(06)", scan=scan) # noqa: E741 + Ne = mfile.get("f_nd_impurity_electrons(07)", scan=scan) + Si = mfile.get("f_nd_impurity_electrons(08)", scan=scan) + Ar = mfile.get("f_nd_impurity_electrons(09)", scan=scan) + Fe = mfile.get("f_nd_impurity_electrons(10)", scan=scan) + Ni = mfile.get("f_nd_impurity_electrons(11)", scan=scan) + Kr = mfile.get("f_nd_impurity_electrons(12)", scan=scan) + Xe = mfile.get("f_nd_impurity_electrons(13)", scan=scan) + W = mfile.get("f_nd_impurity_electrons(14)", scan=scan) + + data = [("", "", ""), ("", "", "")] + count = 0 + + data = [*data, (H, "D + T", "")] + count += 1 + + data = [*data, (He, "He", "")] + count += 1 + if Be > 1e-10: + data = [*data, (Be, "Be", "")] + count += +1 + if C > 1e-10: + data = [*data, (C, "C", "")] + count += 1 + if N > 1e-10: + data = [*data, (N, "N", "")] + count += 1 + if O > 1e-10: + data = [*data, (O, "O", "")] + count += 1 + if Ne > 1e-10: + data = [*data, (Ne, "Ne", "")] + count += 1 + if Si > 1e-10: + data = [*data, (Si, "Si", "")] + count += 1 + if Ar > 1e-10: + data = [*data, (Ar, "Ar", "")] + count += 1 + if Fe > 1e-10: + data = [*data, (Fe, "Fe", "")] + count += 1 + if Ni > 1e-10: + data = [*data, (Ni, "Ni", "")] + count += 1 + if Kr > 1e-10: + data = [*data, (Kr, "Kr", "")] + count += 1 + if Xe > 1e-10: + data = [*data, (Xe, "Xe", "")] + count += 1 + if W > 1e-10: + data = [*data, (W, "W", "")] + count += 1 + + if count > 11: + data = [ + ("", "", ""), + ("", "", ""), + ("", "More than 11 impurities", ""), + ] + else: + draw_text(axis, -0.05, -6.4, "Plasma composition:", ha="left", va="center") + draw_text( + axis, + -0.05, + -7.2, + "Number densities relative to electron density:", + ha="left", + va="center", + ) + data2 += data + + plot_info(axis, data2, mfile, scan) + + +def plot_separatrix_power_split(axis: plt.Axes, mfile: MFile, scan: int, colour_scheme): + """Plot separatrix power split fractions as a bar chart.""" + plot_plasma(axis=axis, mfile=mfile, scan=scan, colour_scheme=colour_scheme) + rmajor, rminor, kappa, dr_sep = mfile.get_variables( + "rmajor", + "rminor", + "kappa", + "dr_plasma_outboard_midplane_separatrix_separation", + scan=scan, + ) + + plasma_scale = max(rminor, abs(kappa * rminor), 1e-6) + scale_factor = min(max(plasma_scale / 2.0, 0.7), 1.0) + text_fontsize = 9 * scale_factor + + is_double_null = ( + DivertorNumberModels(mfile.get("i_single_null", scan=scan)) + == DivertorNumberModels.DOUBLE_NULL + ) + p_sep = mfile.get("p_plasma_separatrix_mw", scan=scan) + f_outboard = mfile.get("f_p_div_outboard_separatrix", scan=scan) + f_inboard = mfile.get("f_p_div_inboard_separatrix", scan=scan) + p_outboard = p_sep * f_outboard + p_inboard = p_sep * f_inboard + p_lower_inboard = mfile.get("p_div_lower_inboard_separatrix_mw", scan=scan) + p_lower_outboard = mfile.get("p_div_lower_outboard_separatrix_mw", scan=scan) + + power_values = [ + p_sep, + p_outboard, + p_inboard, + p_lower_inboard, + p_lower_outboard, + ] + + p_upper_inboard = None + p_upper_outboard = None + if is_double_null: + p_upper_inboard = mfile.get("p_div_upper_inboard_separatrix_mw", scan=scan) + p_upper_outboard = mfile.get("p_div_upper_outboard_separatrix_mw", scan=scan) + power_values.extend([p_upper_inboard, p_upper_outboard]) + + power_min = min(power_values) + power_max = max(power_values) + colour_map = mpl.colormaps["coolwarm"] + + def make_bbox_props(power: float) -> dict[str, Any]: + norm_power = ( + 1.0 + if np.isclose(power_max, power_min) + else (power - power_min) / (power_max - power_min) + ) + return { + "boxstyle": f"round,pad={0.3 * scale_factor:.3f}", + "facecolor": colour_map(norm_power), + "alpha": 1.0, + "linewidth": 2 * scale_factor, + "edgecolor": "black", + } + + centre_pos = (rmajor, 0.0) + outboard_pos = (rmajor + rminor, 0.0) + inboard_pos = (rmajor - rminor, 0.0) + lower_inboard_pos = (rmajor - rminor, -kappa * rminor) + lower_outboard_pos = (rmajor + rminor, -kappa * rminor) + upper_inboard_pos = (rmajor - rminor, kappa * rminor) + upper_outboard_pos = (rmajor + rminor, kappa * rminor) + + draw_text( + axis, + *centre_pos, + f"$P_{{\\mathrm{{sep}}}} = {p_sep:.3f}$ MW", + fontsize=text_fontsize, + verticalalignment="center", + horizontalalignment="center", + bbox=make_bbox_props(p_sep), + zorder=101, + ) + draw_text( + axis, + *outboard_pos, + f"$f_{{\\mathrm{{outboard}}}} = {f_outboard:.3f}$\n" + f"$\\Delta r_{{\\mathrm{{sep}}}} = {dr_sep:.3f}$ m", + fontsize=text_fontsize, + verticalalignment="center", + horizontalalignment="center", + bbox=make_bbox_props(p_outboard), + zorder=101, + ) + draw_text( + axis, + *inboard_pos, + f"$f_{{\\mathrm{{inboard}}}} = {f_inboard:.3f}$", + fontsize=text_fontsize, + verticalalignment="center", + horizontalalignment="center", + bbox=make_bbox_props(p_inboard), + zorder=101, + ) + draw_text( + axis, + *lower_inboard_pos, + "$f_{\\mathrm{lower\\ inboard}} =" + f" {mfile.get('f_p_div_lower_inboard_separatrix', scan=scan):.3f}$\n$P_{{\\mathrm{{lower\\" # noqa: E501 + f" inboard}}}} = {p_lower_inboard:.3f}$ MW", + fontsize=text_fontsize, + verticalalignment="center", + horizontalalignment="center", + bbox=make_bbox_props(p_lower_inboard), + zorder=101, + ) + draw_text( + axis, + *lower_outboard_pos, + "$f_{\\mathrm{lower\\ outboard}} =" + f" {mfile.get('f_p_div_lower_outboard_separatrix', scan=scan):.3f}$\n$P_{{\\mathrm{{lower\\" # noqa: E501 + f" outboard}}}} = {p_lower_outboard:.3f}$ MW", + fontsize=text_fontsize, + verticalalignment="center", + horizontalalignment="center", + bbox=make_bbox_props(p_lower_outboard), + zorder=101, + ) + if is_double_null: + draw_text( + axis, + *upper_inboard_pos, + "$f_{\\mathrm{upper\\ inboard}} =" + f" {mfile.get('f_p_div_upper_inboard_separatrix', scan=scan):.3f}$\n$P_{{\\mathrm{{upper\\" # noqa: E501 + f" inboard}}}} = {p_upper_inboard:.3f}$ MW", + fontsize=text_fontsize, + verticalalignment="center", + horizontalalignment="center", + bbox=make_bbox_props(p_upper_inboard), + zorder=101, + ) + draw_text( + axis, + *upper_outboard_pos, + "$f_{\\mathrm{upper\\ outboard}} =" + f" {mfile.get('f_p_div_upper_outboard_separatrix', scan=scan):.3f}$\n$P_{{\\mathrm{{upper\\" # noqa: E501 + f" outboard}}}} = {p_upper_outboard:.3f}$ MW", + fontsize=text_fontsize, + verticalalignment="center", + horizontalalignment="center", + bbox=make_bbox_props(p_upper_outboard), + zorder=101, + ) + + arrow_props = { + "arrowstyle": "->", + "color": "red", + "linewidth": 3 * scale_factor, + "shrinkA": 14 * scale_factor, + "shrinkB": 14 * scale_factor, + "mutation_scale": 12 * scale_factor, + } + draw_annotation( + axis, + "", + xy=outboard_pos, + xytext=centre_pos, + arrowprops=arrow_props, + zorder=1, + ) + draw_annotation( + axis, + "", + xy=inboard_pos, + xytext=centre_pos, + arrowprops=arrow_props, + zorder=1, + ) + draw_annotation( + axis, + "", + xy=lower_outboard_pos, + xytext=outboard_pos, + arrowprops={ + **arrow_props, + "connectionstyle": "angle3,angleA=0,angleB=-90", + }, + zorder=102, + ) + draw_annotation( + axis, + "", + xy=lower_inboard_pos, + xytext=inboard_pos, + arrowprops={ + **arrow_props, + "connectionstyle": "angle3,angleA=180,angleB=-90", + }, + zorder=102, + ) + if is_double_null: + draw_annotation( + axis, + "", + xy=upper_outboard_pos, + xytext=outboard_pos, + arrowprops={ + **arrow_props, + "connectionstyle": "angle3,angleA=0,angleB=90", + }, + zorder=102, + ) + draw_annotation( + axis, + "", + xy=upper_inboard_pos, + xytext=inboard_pos, + arrowprops={ + **arrow_props, + "connectionstyle": "angle3,angleA=180,angleB=90", + }, + zorder=102, + ) + + axis.spines["top"].set_visible(False) + axis.spines["right"].set_visible(False) + axis.spines["bottom"].set_visible(False) + axis.spines["left"].set_visible(False) + axis.get_xaxis().set_ticks([]) + axis.get_yaxis().set_ticks([]) + + +def plot_cover_page( + axis: plt.Axes, + mfile: MFile, + scan: int, + fig, + radial_build: RadialBuild, + colour_scheme: Literal[1, 2], +): + """Plots a cover page for the PROCESS run, including run title, date, user, and + summary info. + + Parameters + ---------- + axis : plt.Axes + The matplotlib axis object to plot on. + mfile : MFile + The MFILE data object containing run info. + scan : int + The scan number to use for extracting data. + fig : plt.Figure + The matplotlib figure object for additional annotations. + radial_build: + + colour_scheme: + + """ + axis.axis("off") + title = mfile.get("runtitle", scan=-1) + date = mfile.get("date", scan=-1) + time = mfile.get("time", scan=-1) + user = mfile.get("username", scan=-1) + procver = mfile.get("procver", scan=-1) + tagno = mfile.get("tagno", scan=-1) + branch_name = mfile.get("branch_name", scan=-1) + fileprefix = mfile.get("fileprefix", scan=-1) + optmisation_switch = int(mfile.get("i_process_run_mode", scan=-1)) + figure_merit_switch = mfile.get("i_figure_merit", scan=-1) or "N/A" + ifail = mfile.get("ifail", scan=-1) + nvars = mfile.get("n_iteration_variables", scan=-1) + # Objective_function_name + objf_name = mfile.get("objf_name", scan=-1) + # Square_root_of_the_sum_of_squares_of_the_constraint_residuals + sqsumsq = mfile.get("sqsumsq", scan=-1) + # VMCON_convergence_parameter + convergence_parameter = mfile.get("convergence_parameter", scan=-1) or "N/A" + # Number_of_optimising_solver_iterations + n_solver_iterations = int(mfile.get("n_solver_iterations", scan=-1)) or "N/A" + + # Objective name with minimising/maximising + if isinstance(figure_merit_switch, str): + objective_text = "" + elif figure_merit_switch >= 0: + figure_merit_switch = int(figure_merit_switch) + objective_text = f" -> Minimising: {objf_name}" + else: + figure_merit_switch = int(figure_merit_switch) + objective_text = f" -> Maximising: {objf_name}" + + draw_text( + axis, + 0.1, + 0.85, + "PROCESS Run Summary", + fontsize=28, + ha="left", + va="center", + transform=fig.transFigure, + ) + + # Box 1: Run Info + run_info = ( + f"• Run Title: {title}\n" + f"• Date: {date} Time: {time}\n" + f"• User: {user}\n" + f"• PROCESS Version: {procver}" + ) + draw_text( + axis, + 0.1, + 0.72, + run_info, + fontsize=16, + ha="left", + va="top", + transform=fig.transFigure, + bbox=box_style("#e0f7fa"), + ) + + # Box 2: File/Branch Info + # Wrap the whole "Branch Name: ..." line if too long + max_line_len = 60 + branch_line = textwrap.fill(f"• Branch Name: {branch_name}", max_line_len) + fileprefix = textwrap.fill(f"File Prefix: {fileprefix}", max_line_len) + + file_info = f"• Tag Number: {tagno}\n{branch_line}\n• {fileprefix}" + draw_text( + axis, + 0.1, + 0.57, + file_info, + fontsize=14, + ha="left", + va="top", + transform=fig.transFigure, + bbox=box_style("#fffde7"), + ) + + # Box 3: Run Settings + settings_info = ( + f"• Optimisation Switch: {int(optmisation_switch)}\n" + f" {PROCESSRunMode(int(optmisation_switch)).description}\n" + f"• Figure of Merit Switch (i_figure_merit): {figure_merit_switch}\n" + f" {objective_text}\n" + f"• Fail Status (ifail): {int(ifail)}\n" + f"• Number of Iteration Variables: {int(nvars)}\n" + f"• Constraint Residuals (sqrt sum sq): {sqsumsq}\n" + f"• Convergence Parameter: {convergence_parameter}\n" + f"• Solver Iterations: {n_solver_iterations}\n" + f"• Runtime: {mfile.get('process_runtime', scan=-1):.6f} seconds" + ) + draw_text( + axis, + 0.1, + 0.46, + settings_info, + fontsize=14, + ha="left", + va="top", + transform=fig.transFigure, + bbox=box_style("#f3e5f5"), + ) + + draw_text( + axis, + 0.1, + 0.15, + "For more information, see the following pages.", + fontsize=12, + ha="left", + va="center", + transform=fig.transFigure, + color="gray", + ) + + # Add a small poloidal cross-section inset on the cover page + inset_ax = fig.add_axes([0.55, 0.2, 0.55, 0.55], aspect="equal") + poloidal_cross_section( + inset_ax, + mfile, + scan, + demo_ranges=False, + radial_build=radial_build, + colour_scheme=colour_scheme, + ) + inset_ax.set_title("") # Remove the plot title + inset_ax.axis("off") + + +__all__ = [ + "plot_cover_page", + "plot_header", + "plot_info", + "plot_separatrix_power_split", +] diff --git a/process/core/io/plot/summary/time_profiles.py b/process/core/io/plot/summary/time_profiles.py new file mode 100644 index 0000000000..90fb6aca7f --- /dev/null +++ b/process/core/io/plot/summary/time_profiles.py @@ -0,0 +1,267 @@ +"""Time Profiles functions for PROCESS summary plots.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np + +from process.core.io.plot.summary.common import ( + box_style, + get_pulse_timings, +) +from process.core.io.plot.summary.magnets import ( + secs_to_hms, +) +from process.core.io.plot.summary.rendering import ( + draw_text, +) + +if TYPE_CHECKING: + import matplotlib.pyplot as plt + + from process.core.io.mfile import MFile + + +def plot_current_profiles_over_time(axis: plt.Axes, mfile: MFile, scan: int): + """Plots the current profiles over time for PF circuits, CS coil, and plasma.""" + pulse_timings = get_pulse_timings(mfile, scan) + + # Find the number of PF circuits, n_pf_cs_plasma_circuits includes the CS and plasma + # circuits + n_pf_cs_plasma_circuits = mfile.get("n_pf_cs_plasma_circuits", scan=scan) + + # Extract PF circuit times + # n_pf_cs_plasma_circuits contains the CS and plasma at the end so we subtract 2 + for i in range(int(n_pf_cs_plasma_circuits - 2)): + circuit_current = [ + mfile.get(f"pfc{i}t{j}", scan=scan) + for j in range(pulse_timings.n_pf_active_points_total) + ] + # Change from 0 to 1 index to align with poloidal cross-section plot numbering + axis.plot( + pulse_timings.pf_active_cumulative, + circuit_current, + label=f"PF Coil {i + 1}", + linestyle="--", + ) + + # Since CS may not always be present try to retrieve values + try: + cs_circuit = [ + mfile.get(f"cs_t{i}", scan=scan) + for i in range(pulse_timings.n_pf_active_points_total) + ] + axis.plot( + pulse_timings.pf_active_cumulative, + cs_circuit, + label="CS Coil", + linestyle="--", + ) + except KeyError: + pass + + # Plasma current values + plasmat1 = mfile.get("plasmat1", scan=scan) + plasmat2 = mfile.get("plasmat2", scan=scan) + plasmat3 = mfile.get("plasmat3", scan=scan) + plasmat4 = mfile.get("plasmat4", scan=scan) + plasmat5 = mfile.get("plasmat5", scan=scan) + + # x-coordinates for the plasma current + x_plasma = pulse_timings.pf_active_cumulative[1:] + # x-coordinates for the plasma current + y_plasma = [plasmat1, plasmat2, plasmat3, plasmat4, plasmat5] + + # Plot the plasma current + axis.plot(x_plasma, y_plasma, "black", linewidth=2, label="Plasma") + + # Move the x-axis to 0 on the y-axis + axis.spines["bottom"].set_position("zero") + + # Annotate key points + # Create a secondary x-axis for annotations + secax = axis.secondary_xaxis("bottom") + # Exclude the dwell point so tick positions and labels remain aligned. + secax.set_xticks(pulse_timings.pf_active_cumulative[:-1]) + secax.set_xticklabels( + pulse_timings.POINT_LABELS[ + :-1 + ], # Exclude the last label as it corresponds to the dwell period + rotation=60, + ) + secax.tick_params(axis="x", which="major") + + # Add axis labels + axis.set_xlabel("Time [s]", fontsize=12) + axis.xaxis.set_label_coords(1.05, 0.5) + axis.set_ylabel("Current [A]", fontsize=12) + + # Add a title + axis.set_title("Current Profiles Over Time", fontsize=14) + + # Add a legend + axis.legend() + + axis.set_yscale("symlog") + + # Add a grid for better readability + axis.grid(True, linestyle="--", alpha=0.6) + + +def plot_system_power_profiles_over_time(axis: plt.Axes, mfile: MFile, scan: int, fig): + """Plots the power profiles over time for various systems.""" + pulse_timings = get_pulse_timings(mfile, scan) + + # Create empty arrays for the power at each time step for each system + power_profiles = { + "Fusion Power": np.zeros(pulse_timings.n_pulse_points_total), + "Plant Base Load": np.zeros(pulse_timings.n_pulse_points_total), + "Cryo Plant": np.zeros(pulse_timings.n_pulse_points_total), + "Tritium Plant": np.zeros(pulse_timings.n_pulse_points_total), + "Vacuum Pumps": np.zeros(pulse_timings.n_pulse_points_total), + "TF Coil Supplies": np.zeros(pulse_timings.n_pulse_points_total), + "PF Coil Supplies": np.zeros(pulse_timings.n_pulse_points_total), + "Coolant Pump Elec Total": np.zeros(pulse_timings.n_pulse_points_total), + "HCD Electric Total": np.zeros(pulse_timings.n_pulse_points_total), + "Gross Electric Power": np.zeros(pulse_timings.n_pulse_points_total), + "Net Electric Power": np.zeros(pulse_timings.n_pulse_points_total), + } + + # Fill power_profiles arrays using vectorized assignment + for label, key in [ + ("Fusion Power", "p_fusion_total_profile_mw"), + ("Gross Electric Power", "p_plant_electric_gross_profile_mw"), + ("Net Electric Power", "p_plant_electric_net_profile_mw"), + ("Plant Base Load", "p_plant_electric_base_total_profile_mw"), + ("Cryo Plant", "p_cryo_plant_electric_profile_mw"), + ("Tritium Plant", "p_tritium_plant_electric_profile_mw"), + ("Vacuum Pumps", "vachtmw_profile_mw"), + ("TF Coil Supplies", "p_tf_electric_supplies_profile_mw"), + ("PF Coil Supplies", "p_pf_electric_supplies_profile_mw"), + ("Coolant Pump Elec Total", "p_coolant_pump_elec_total_profile_mw"), + ("HCD Electric Total", "p_hcd_electric_total_profile_mw"), + ]: + for time in range(pulse_timings.n_pulse_points_total): + power_profiles[label][time] = mfile.get(f"{key}{time}", scan=scan) + + # Define line styles for each system + # All net drains (negative power flows) use the same line style: dashed + line_styles = { + "Fusion Power": ":", + "Plant Base Load": "--", + "Cryo Plant": "--", + "Tritium Plant": "--", + "Vacuum Pumps": "--", + "TF Coil Supplies": "--", + "PF Coil Supplies": "--", + "Coolant Pump Elec Total": "--", + "HCD Electric Total": "--", + "Gross Electric Power": "-", + "Net Electric Power": "-", + } + + # Plot each system's power profile over time with different line styles + for label, powers in power_profiles.items(): + style = line_styles.get(label, "-") + axis.plot( + pulse_timings.total_pulse_cumulative, + powers, + label=label, + linestyle=style, + ) + + # Move the x-axis to 0 on the y-axis + axis.spines["bottom"].set_position("zero") + + # Annotate key points + # Create a secondary x-axis for annotations + secax = axis.secondary_xaxis("bottom") + # Label phase starts only (exclude final end-of-dwell point). + secax.set_xticks(pulse_timings.total_pulse_cumulative[:-1]) + secax.set_xticklabels( + pulse_timings.POINT_LABELS, + rotation=60, + ) + secax.tick_params(axis="x", which="major") + + # Add axis labels + axis.set_xlabel("Time [s]", fontsize=12) + axis.xaxis.set_label_coords(1.05, 0.5) + axis.set_ylabel("Power [MW]", fontsize=12) + + # Add a title + axis.set_title("System Power Over Time", fontsize=14) + + # Add a legend + axis.legend() + + axis.set_yscale("symlog") + axis.minorticks_on() + axis.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.2) + + # Add a grid for better readability + axis.grid(True, linestyle="--", alpha=0.6) + + # Add energy produced info + textstr_energy = ( + "$\\mathbf{Energy \\ Production:}$\n\nEnergy produced over whole" + f" pulse: {mfile.get('e_plant_net_electric_pulse_mj', scan=scan):,.4f}" + " MJ\nEnergy produced over whole pulse:" + f" {mfile.get('e_plant_net_electric_pulse_kwh', scan=scan):,.4f} kWh\n" + ) + + draw_text( + axis, + 0.075, + 0.2, + textstr_energy, + fontsize=9, + verticalalignment="top", + transform=fig.transFigure, + bbox=box_style("grey"), + ) + + # Add energy produced info + + textstr_times = ( + "$\\mathbf{Pulse \\ Timings:}$\n\nCoil precharge," + " $t_{\\text{precharge}}$: " + f" {mfile.get('t_plant_pulse_coil_precharge', scan=scan):,.1f} s " + f" ({secs_to_hms(mfile.get('t_plant_pulse_coil_precharge', scan=scan))})\nCurrent" # noqa: E501 + " ramp up, $t_{\\text{current ramp}}$: " + f" {mfile.get('t_plant_pulse_plasma_current_ramp_up', scan=scan):,.1f}" + f" s ({secs_to_hms(mfile.get('t_plant_pulse_plasma_current_ramp_up', scan=scan))})\nFusion" # noqa: E501 + " ramp, $t_{\\text{fusion ramp}}$: " + f" {mfile.get('t_plant_pulse_fusion_ramp', scan=scan):,.1f} s " + f" ({secs_to_hms(mfile.get('t_plant_pulse_fusion_ramp', scan=scan))})\nBurn," + " $t_{\\text{burn}}$: " + f" {mfile.get('t_plant_pulse_burn', scan=scan):,.1f} s " + f" ({secs_to_hms(mfile.get('t_plant_pulse_burn', scan=scan))})\nRamp" + " down, $t_{\\text{ramp down}}$: " + f" {mfile.get('t_plant_pulse_plasma_current_ramp_down', scan=scan):,.1f}" + f" s ({secs_to_hms(mfile.get('t_plant_pulse_plasma_current_ramp_down', scan=scan))})\nBetween" # noqa: E501 + " pulse, $t_{\\text{between pulse}}$: " + f" {mfile.get('t_plant_pulse_dwell', scan=scan):,.1f} s " + f" ({secs_to_hms(mfile.get('t_plant_pulse_dwell', scan=scan))})\n\nTotal" + " pulse length, $t_{\\text{cycle}}$: " + f" {mfile.get('t_plant_pulse_total', scan=scan):,.1f} s " + f" ({secs_to_hms(mfile.get('t_plant_pulse_total', scan=scan))})\n" + ) + + draw_text( + axis, + 0.6, + 0.225, + textstr_times, + fontsize=9, + verticalalignment="top", + transform=fig.transFigure, + bbox=box_style("grey"), + ) + + +__all__ = [ + "plot_current_profiles_over_time", + "plot_system_power_profiles_over_time", +] From daf90d8606bc11f6b023961e8bfe20511aec499d Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:26:55 +0100 Subject: [PATCH 14/18] fix circular imports --- process/core/io/plot/summary/__init__.py | 38 +------ process/core/io/plot/summary/api.py | 106 ++++++++++++------ .../core/io/plot/summary/geometry/__init__.py | 60 +--------- .../core/io/plot/summary/geometry/build.py | 85 +------------- process/core/io/plot/summary/geometry/misc.py | 4 +- .../core/io/plot/summary/geometry/poloidal.py | 16 +-- .../core/io/plot/summary/geometry/toroidal.py | 8 +- .../core/io/plot/summary/magnets/__init__.py | 62 +--------- process/core/io/plot/summary/magnets/cs.py | 2 +- process/core/io/plot/summary/magnets/pf.py | 2 +- .../core/io/plot/summary/plasma/__init__.py | 52 +-------- .../io/plot/summary/plasma/current_drive.py | 2 +- process/core/io/plot/summary/power_flow.py | 2 +- .../core/io/plot/summary/profiles/__init__.py | 105 +---------------- process/core/io/plot/summary/profiles/misc.py | 56 ++++++++- .../core/io/plot/summary/profiles/plasma.py | 2 +- .../io/plot/summary/profiles/radiation.py | 55 +-------- process/core/io/plot/summary/radial_build.py | 93 +++++++++++++++ .../io/plot/summary/reporting/__init__.py | 50 +-------- .../core/io/plot/summary/reporting/misc.py | 2 + .../core/io/plot/summary/reporting/panels.py | 93 +-------------- .../core/io/plot/summary/reporting/text.py | 102 +++++++++++++++++ process/core/io/plot/summary/time_profiles.py | 2 +- 23 files changed, 355 insertions(+), 644 deletions(-) create mode 100644 process/core/io/plot/summary/radial_build.py create mode 100644 process/core/io/plot/summary/reporting/text.py diff --git a/process/core/io/plot/summary/__init__.py b/process/core/io/plot/summary/__init__.py index 69fb2e34d8..c0c0d1d7f1 100644 --- a/process/core/io/plot/summary/__init__.py +++ b/process/core/io/plot/summary/__init__.py @@ -1,37 +1,9 @@ -"""PROCESS summary plotting package.""" +"""PROCESS summary plotting entry points.""" -from __future__ import annotations - -import process.core.io.plot.summary.api as _api -import process.core.io.plot.summary.common as _common -import process.core.io.plot.summary.geometry as _geometry -import process.core.io.plot.summary.magnets as _magnets -import process.core.io.plot.summary.plasma as _plasma -import process.core.io.plot.summary.power_flow as _power_flow -import process.core.io.plot.summary.profiles as _profiles -import process.core.io.plot.summary.reporting as _reporting -import process.core.io.plot.summary.time_profiles as _time_profiles - -_MODULES = ( - _api, - _common, - _geometry, - _magnets, - _plasma, - _power_flow, - _profiles, - _reporting, - _time_profiles, +from process.core.io.plot.summary.api import ( + create_thickness_builds, + main_plot, + plot_summary, ) -_REGISTRY = {} -for _module in _MODULES: - _REGISTRY.update({name: getattr(_module, name) for name in _module.__all__}) -for _module in _MODULES: - _module.__dict__.update(_REGISTRY) - - -plot_summary = _api.plot_summary -main_plot = _api.main_plot -create_thickness_builds = _api.create_thickness_builds __all__ = ["create_thickness_builds", "main_plot", "plot_summary"] diff --git a/process/core/io/plot/summary/api.py b/process/core/io/plot/summary/api.py index c07541530f..9f586b58b5 100644 --- a/process/core/io/plot/summary/api.py +++ b/process/core/io/plot/summary/api.py @@ -17,27 +17,39 @@ RADIAL_BUILD, vertical_lower, ) -from process.core.io.plot.summary.geometry import ( +from process.core.io.plot.summary.geometry.build import ( + plot_geometry_info, + plot_radial_build, +) +from process.core.io.plot.summary.geometry.misc import ( plot_blkt_pipe_bends, plot_blkt_structure, +) +from process.core.io.plot.summary.geometry.poloidal import ( plot_first_wall_poloidal_cross_section, plot_first_wall_top_down_cross_section, plot_full_machine_poloidal_cross_section, - plot_geometry_info, - plot_radial_build, poloidal_cross_section, +) +from process.core.io.plot.summary.geometry.toroidal import ( toroidal_cross_section, ) -from process.core.io.plot.summary.magnets import ( +from process.core.io.plot.summary.magnets.cables import ( plot_cable_in_conduit_cable, - plot_corc_cable_geometry, + plot_hts_tape_geometry, +) +from process.core.io.plot.summary.magnets.cs import ( plot_cs_coil_structure, plot_cs_turn_structure, - plot_hts_tape_geometry, plot_magnetics_info, plot_pf_cs_plasma_mutual_inductance, - plot_pf_dimensions, plot_physics_info, +) +from process.core.io.plot.summary.magnets.pf import ( + plot_pf_dimensions, +) +from process.core.io.plot.summary.magnets.tf import ( + plot_corc_cable_geometry, plot_quench_time_evolution, plot_resistive_tf_info, plot_resistive_tf_wp, @@ -48,75 +60,95 @@ plot_tf_croco_turn, plot_tf_stress, ) -from process.core.io.plot.summary.plasma import ( - plot_bootstrap_comparison, +from process.core.io.plot.summary.plasma.confinement import ( plot_brunner_divertor_power_split_comparison_stackplot, plot_confinement_time_comparison, + plot_sol_power_decay_length_comparison, +) +from process.core.io.plot.summary.plasma.current_drive import ( + plot_bootstrap_comparison, +) +from process.core.io.plot.summary.plasma.overview import ( plot_detailed_plasma_parameters, - plot_magnetic_fields_in_plasma, plot_main_plasma_information, +) +from process.core.io.plot.summary.plasma.physics import ( + plot_magnetic_fields_in_plasma, plot_max_normalised_beta_comparison, plot_plasma_coloumb_logarithms, plot_plasma_current_comparison, plot_plasma_outboard_toroidal_ripple_map, - plot_sol_power_decay_length_comparison, ) from process.core.io.plot.summary.power_flow import ( plot_main_power_flow, plot_power_info, ) -from process.core.io.plot.summary.profiles import ( - plot_beta_profiles, +from process.core.io.plot.summary.profiles.atomic import ( plot_collision_frequency_profile, plot_collision_time_profile, - plot_cs_hoop_stress_contour_profile, - plot_cs_hoop_stress_profile, - plot_cs_radial_stress_contour_profile, - plot_cs_radial_stress_profile, - plot_cs_stress_time_profile, - plot_cs_tresca_2d_contour, - plot_cs_vertical_stress_profile, - plot_cs_von_mises_2d_contour, - plot_cumulative_plasma_thermal_energy_profiles, plot_debye_length_profile, plot_electron_frequency_profile, - plot_fusion_rate_contours, - plot_fusion_rate_profiles, plot_ion_charge_profile, plot_ion_frequency_profile, plot_ion_slowing_down_time_profile, - plot_jprofile, - plot_larmor_radius_profile, + plot_mean_free_path_profile, + plot_resistivity_profile, + plot_velocity_profile, +) +from process.core.io.plot.summary.profiles.misc import ( plot_line_brem_loss_function_profile, plot_line_brem_power_density_profile, - plot_mean_free_path_profile, +) +from process.core.io.plot.summary.profiles.plasma import ( + plot_beta_profiles, + plot_cumulative_plasma_thermal_energy_profiles, + plot_fusion_rate_contours, + plot_fusion_rate_profiles, + plot_jprofile, plot_n_profiles, plot_plasma_effective_charge_profile, plot_plasma_poloidal_pressure_contours, - plot_plasma_pressure_gradient_profiles, plot_plasma_pressure_profiles, plot_plasma_thermal_energy_profiles, plot_qprofile, - plot_rad_contour, - plot_resistivity_profile, plot_t_profiles, - plot_velocity_profile, +) +from process.core.io.plot.summary.profiles.radiation import ( + plot_cs_radial_stress_contour_profile, + plot_cs_radial_stress_profile, + plot_larmor_radius_profile, + plot_plasma_pressure_gradient_profiles, + plot_rad_contour, +) +from process.core.io.plot.summary.profiles.stress import ( + plot_cs_hoop_stress_contour_profile, + plot_cs_hoop_stress_profile, + plot_cs_stress_time_profile, + plot_cs_tresca_2d_contour, + plot_cs_vertical_stress_profile, + plot_cs_von_mises_2d_contour, plot_vertical_stress_contour_profile, ) -from process.core.io.plot.summary.reporting import ( +from process.core.io.plot.summary.reporting.constraints import ( + plot_equality_constraint_equations, + plot_inequality_constraint_equations, +) +from process.core.io.plot.summary.reporting.layouts import ( + plot_upper_vertical_build, +) +from process.core.io.plot.summary.reporting.misc import ( RadialBuild, - plot_cover_page, plot_density_limit_comparison, plot_ebw_ecrh_coupling_graph, - plot_equality_constraint_equations, plot_fw_90_deg_pipe_bend, plot_h_threshold_comparison, - plot_header, - plot_inequality_constraint_equations, plot_iteration_variables, plot_lower_vertical_build, +) +from process.core.io.plot.summary.reporting.panels import ( + plot_cover_page, + plot_header, plot_separatrix_power_split, - plot_upper_vertical_build, ) from process.core.io.plot.summary.time_profiles import ( plot_current_profiles_over_time, diff --git a/process/core/io/plot/summary/geometry/__init__.py b/process/core/io/plot/summary/geometry/__init__.py index efc0c51635..3cae438b0e 100644 --- a/process/core/io/plot/summary/geometry/__init__.py +++ b/process/core/io/plot/summary/geometry/__init__.py @@ -1,59 +1 @@ -"""Public API for this summary plotting concern.""" - -from __future__ import annotations - -import process.core.io.plot.summary.geometry.build as _build -import process.core.io.plot.summary.geometry.misc as _misc -import process.core.io.plot.summary.geometry.poloidal as _poloidal -import process.core.io.plot.summary.geometry.toroidal as _toroidal - -_MODULES = (_build, _misc, _poloidal, _toroidal) -_REGISTRY = {} -for _module in _MODULES: - _REGISTRY.update({name: getattr(_module, name) for name in _module.__all__}) -for _module in _MODULES: - _module.__dict__.update(_REGISTRY) -arc = _REGISTRY["arc"] -arc_fill = _REGISTRY["arc_fill"] -cumulative_radial_build = _REGISTRY["cumulative_radial_build"] -cumulative_radial_build2 = _REGISTRY["cumulative_radial_build2"] -plot_blanket = _REGISTRY["plot_blanket"] -plot_blkt_pipe_bends = _REGISTRY["plot_blkt_pipe_bends"] -plot_blkt_structure = _REGISTRY["plot_blkt_structure"] -plot_cryostat = _REGISTRY["plot_cryostat"] -plot_first_wall_poloidal_cross_section = _REGISTRY[ - "plot_first_wall_poloidal_cross_section" -] -plot_first_wall_top_down_cross_section = _REGISTRY[ - "plot_first_wall_top_down_cross_section" -] -plot_firstwall = _REGISTRY["plot_firstwall"] -plot_full_machine_poloidal_cross_section = _REGISTRY[ - "plot_full_machine_poloidal_cross_section" -] -plot_geometry_info = _REGISTRY["plot_geometry_info"] -plot_radial_build = _REGISTRY["plot_radial_build"] -plot_shield = _REGISTRY["plot_shield"] -plot_vacuum_vessel_and_divertor = _REGISTRY["plot_vacuum_vessel_and_divertor"] -poloidal_cross_section = _REGISTRY["poloidal_cross_section"] -toroidal_cross_section = _REGISTRY["toroidal_cross_section"] -__all__ = [ - "arc", - "arc_fill", - "cumulative_radial_build", - "cumulative_radial_build2", - "plot_blanket", - "plot_blkt_pipe_bends", - "plot_blkt_structure", - "plot_cryostat", - "plot_first_wall_poloidal_cross_section", - "plot_first_wall_top_down_cross_section", - "plot_firstwall", - "plot_full_machine_poloidal_cross_section", - "plot_geometry_info", - "plot_radial_build", - "plot_shield", - "plot_vacuum_vessel_and_divertor", - "poloidal_cross_section", - "toroidal_cross_section", -] +"""Geometry implementation modules for summary plots.""" diff --git a/process/core/io/plot/summary/geometry/build.py b/process/core/io/plot/summary/geometry/build.py index ac6df537eb..a47efe2bed 100644 --- a/process/core/io/plot/summary/geometry/build.py +++ b/process/core/io/plot/summary/geometry/build.py @@ -14,7 +14,6 @@ CSCOMPRESSION_COLOUR, FIRSTWALL_COLOUR, PLASMA_COLOUR, - RADIAL_BUILD, SHIELD_COLOUR, SOLENOID_COLOUR, TFC_COLOUR, @@ -24,7 +23,7 @@ from process.core.io.plot.summary.rendering import ( draw_text, ) -from process.core.io.plot.summary.reporting import ( +from process.core.io.plot.summary.reporting.text import ( plot_info, ) from process.data_structure.build_variables import TFCSRadialConfiguration @@ -35,86 +34,6 @@ from process.core.io.mfile import MFile -def cumulative_radial_build(section, mfile: MFile, scan: int): - """Function for calculating the cumulative radial build up to and - including the given section. - - Parameters - ---------- - section : - section of the radial build to go up to - mfile : - MFILE data object - scan : - scan number to use - - Returns - ------- - : - cumulative_build:cumulative radial build up to section given - """ - complete = False - cumulative_build = 0 - for item in RADIAL_BUILD: - if item in {"rminori", "rminoro"}: - cumulative_build += mfile.get("rminor", scan=scan) - elif item in {"vvblgapi", "vvblgapo"}: - cumulative_build += mfile.get("dr_shld_blkt_gap", scan=scan) - elif "dr_vv_inboard" in item: - cumulative_build += mfile.get("dr_vv_inboard", scan=scan) - elif "dr_vv_outboard" in item: - cumulative_build += mfile.get("dr_vv_outboard", scan=scan) - else: - cumulative_build += mfile.get(item, scan=scan) - if item == section: - complete = True - break - - if complete is False: - print("radial build parameter ", section, " not found") - return cumulative_build - - -def cumulative_radial_build2(section, mfile: MFile, scan: int): - """Function for calculating the cumulative radial build up to and - including the given section. - - Parameters - ---------- - section : - section of the radial build to go up to - mfile : - MFILE data object - scan : - scan number to use - - Returns - ------- - : - cumulative_build --> cumulative radial build up to and including - section given - previous --> cumulative radial build up to section given - """ - cumulative_build = 0 - build = 0 - for item in RADIAL_BUILD: - if item in {"rminori", "rminoro"}: - build = mfile.get("rminor", scan=scan) - elif item in {"vvblgapi", "vvblgapo"}: - build = mfile.get("dr_shld_blkt_gap", scan=scan) - elif "dr_vv_inboard" in item: - build = mfile.get("dr_vv_inboard", scan=scan) - elif "dr_vv_outboard" in item: - build = mfile.get("dr_vv_outboard", scan=scan) - else: - build = mfile.get(item, scan=scan) - cumulative_build += build - if item == section: - break - previous = cumulative_build - build - return (cumulative_build, previous) - - def plot_geometry_info(axis: plt.Axes, mfile: MFile, scan: int): """Function to plot geometry info @@ -349,8 +268,6 @@ def plot_radial_build(axis: plt.Axes, mfile: MFile, colour_scheme: Literal[1, 2] __all__ = [ - "cumulative_radial_build", - "cumulative_radial_build2", "plot_geometry_info", "plot_radial_build", ] diff --git a/process/core/io/plot/summary/geometry/misc.py b/process/core/io/plot/summary/geometry/misc.py index 6121ff64b2..85bc50e333 100644 --- a/process/core/io/plot/summary/geometry/misc.py +++ b/process/core/io/plot/summary/geometry/misc.py @@ -14,10 +14,10 @@ plot_blanket, plot_firstwall, ) -from process.core.io.plot.summary.plasma import ( +from process.core.io.plot.summary.plasma.physics import ( plot_plasma, ) -from process.core.io.plot.summary.reporting import ( +from process.core.io.plot.summary.reporting.layouts import ( draw_bend, ) from process.data_structure.physics_variables import DivertorNumberModels diff --git a/process/core/io/plot/summary/geometry/poloidal.py b/process/core/io/plot/summary/geometry/poloidal.py index 18994beb7c..0b9658f133 100644 --- a/process/core/io/plot/summary/geometry/poloidal.py +++ b/process/core/io/plot/summary/geometry/poloidal.py @@ -16,21 +16,23 @@ VESSEL_COLOUR, thin, ) -from process.core.io.plot.summary.geometry.build import ( - cumulative_radial_build, -) -from process.core.io.plot.summary.magnets import ( +from process.core.io.plot.summary.magnets.pf import ( plot_pf_coils, +) +from process.core.io.plot.summary.magnets.tf import ( plot_tf_coils, ) -from process.core.io.plot.summary.plasma import ( +from process.core.io.plot.summary.plasma.physics import ( plot_plasma, ) +from process.core.io.plot.summary.radial_build import ( + cumulative_radial_build, +) from process.core.io.plot.summary.rendering import ( draw_annotation, draw_text, ) -from process.core.io.plot.summary.reporting import ( +from process.core.io.plot.summary.reporting.misc import ( plot_centre_cross, ) from process.data_structure.physics_variables import DivertorNumberModels @@ -54,7 +56,7 @@ if TYPE_CHECKING: from process.core.io.mfile import MFile - from process.core.io.plot.summary.reporting import ( + from process.core.io.plot.summary.reporting.misc import ( RadialBuild, ) diff --git a/process/core/io/plot/summary/geometry/toroidal.py b/process/core/io/plot/summary/geometry/toroidal.py index c1da82726a..c49d184a38 100644 --- a/process/core/io/plot/summary/geometry/toroidal.py +++ b/process/core/io/plot/summary/geometry/toroidal.py @@ -23,12 +23,12 @@ VESSEL_COLOUR, rtangle, ) -from process.core.io.plot.summary.geometry.build import ( - cumulative_radial_build2, -) -from process.core.io.plot.summary.magnets import ( +from process.core.io.plot.summary.magnets.tf import ( TF_outboard, ) +from process.core.io.plot.summary.radial_build import ( + cumulative_radial_build2, +) from process.models.physics.current_drive import ( CurrentDriveMethodType, CurrentDriveModel, diff --git a/process/core/io/plot/summary/magnets/__init__.py b/process/core/io/plot/summary/magnets/__init__.py index 9a6c65f03d..fee6b5abd1 100644 --- a/process/core/io/plot/summary/magnets/__init__.py +++ b/process/core/io/plot/summary/magnets/__init__.py @@ -1,61 +1 @@ -"""Public API for this summary plotting concern.""" - -from __future__ import annotations - -import process.core.io.plot.summary.magnets.cables as _cables -import process.core.io.plot.summary.magnets.cs as _cs -import process.core.io.plot.summary.magnets.pf as _pf -import process.core.io.plot.summary.magnets.tf as _tf - -_MODULES = (_cables, _cs, _pf, _tf) -_REGISTRY = {} -for _module in _MODULES: - _REGISTRY.update({name: getattr(_module, name) for name in _module.__all__}) -for _module in _MODULES: - _module.__dict__.update(_REGISTRY) -TF_outboard = _REGISTRY["TF_outboard"] -plot_cable_in_conduit_cable = _REGISTRY["plot_cable_in_conduit_cable"] -plot_corc_cable_geometry = _REGISTRY["plot_corc_cable_geometry"] -plot_cs_coil_structure = _REGISTRY["plot_cs_coil_structure"] -plot_cs_turn_structure = _REGISTRY["plot_cs_turn_structure"] -plot_hts_tape_geometry = _REGISTRY["plot_hts_tape_geometry"] -plot_magnetics_info = _REGISTRY["plot_magnetics_info"] -plot_pf_coils = _REGISTRY["plot_pf_coils"] -plot_pf_cs_plasma_mutual_inductance = _REGISTRY["plot_pf_cs_plasma_mutual_inductance"] -plot_pf_dimensions = _REGISTRY["plot_pf_dimensions"] -plot_physics_info = _REGISTRY["plot_physics_info"] -plot_quench_time_evolution = _REGISTRY["plot_quench_time_evolution"] -plot_resistive_tf_info = _REGISTRY["plot_resistive_tf_info"] -plot_resistive_tf_wp = _REGISTRY["plot_resistive_tf_wp"] -plot_superconducting_tf_wp = _REGISTRY["plot_superconducting_tf_wp"] -plot_tf_cable_in_conduit_turn = _REGISTRY["plot_tf_cable_in_conduit_turn"] -plot_tf_coil_structure = _REGISTRY["plot_tf_coil_structure"] -plot_tf_coils = _REGISTRY["plot_tf_coils"] -plot_tf_corc_cable_summary_box = _REGISTRY["plot_tf_corc_cable_summary_box"] -plot_tf_croco_turn = _REGISTRY["plot_tf_croco_turn"] -plot_tf_stress = _REGISTRY["plot_tf_stress"] -secs_to_hms = _REGISTRY["secs_to_hms"] -__all__ = [ - "TF_outboard", - "plot_cable_in_conduit_cable", - "plot_corc_cable_geometry", - "plot_cs_coil_structure", - "plot_cs_turn_structure", - "plot_hts_tape_geometry", - "plot_magnetics_info", - "plot_pf_coils", - "plot_pf_cs_plasma_mutual_inductance", - "plot_pf_dimensions", - "plot_physics_info", - "plot_quench_time_evolution", - "plot_resistive_tf_info", - "plot_resistive_tf_wp", - "plot_superconducting_tf_wp", - "plot_tf_cable_in_conduit_turn", - "plot_tf_coil_structure", - "plot_tf_coils", - "plot_tf_corc_cable_summary_box", - "plot_tf_croco_turn", - "plot_tf_stress", - "secs_to_hms", -] +"""Magnets implementation modules for summary plots.""" diff --git a/process/core/io/plot/summary/magnets/cs.py b/process/core/io/plot/summary/magnets/cs.py index e7eb6bb302..d191dd30f5 100644 --- a/process/core/io/plot/summary/magnets/cs.py +++ b/process/core/io/plot/summary/magnets/cs.py @@ -20,7 +20,7 @@ draw_annotation, draw_text, ) -from process.core.io.plot.summary.reporting import ( +from process.core.io.plot.summary.reporting.text import ( plot_info, ) from process.data_structure.pfcoil_variables import NFIXMX diff --git a/process/core/io/plot/summary/magnets/pf.py b/process/core/io/plot/summary/magnets/pf.py index 9082ce13de..bfe6298bbb 100644 --- a/process/core/io/plot/summary/magnets/pf.py +++ b/process/core/io/plot/summary/magnets/pf.py @@ -11,7 +11,7 @@ CSCOMPRESSION_COLOUR, SOLENOID_COLOUR, ) -from process.core.io.plot.summary.geometry import ( +from process.core.io.plot.summary.radial_build import ( cumulative_radial_build2, ) from process.core.io.plot.summary.rendering import ( diff --git a/process/core/io/plot/summary/plasma/__init__.py b/process/core/io/plot/summary/plasma/__init__.py index ea33c772e1..f73ca409f3 100644 --- a/process/core/io/plot/summary/plasma/__init__.py +++ b/process/core/io/plot/summary/plasma/__init__.py @@ -1,51 +1 @@ -"""Public API for this summary plotting concern.""" - -from __future__ import annotations - -import process.core.io.plot.summary.plasma.confinement as _confinement -import process.core.io.plot.summary.plasma.current_drive as _current_drive -import process.core.io.plot.summary.plasma.overview as _overview -import process.core.io.plot.summary.plasma.physics as _physics - -_MODULES = (_confinement, _current_drive, _overview, _physics) -_REGISTRY = {} -for _module in _MODULES: - _REGISTRY.update({name: getattr(_module, name) for name in _module.__all__}) -for _module in _MODULES: - _module.__dict__.update(_REGISTRY) -plot_bootstrap_comparison = _REGISTRY["plot_bootstrap_comparison"] -plot_brunner_divertor_power_split_comparison_stackplot = _REGISTRY[ - "plot_brunner_divertor_power_split_comparison_stackplot" -] -plot_confinement_time_comparison = _REGISTRY["plot_confinement_time_comparison"] -plot_current_drive_info = _REGISTRY["plot_current_drive_info"] -plot_detailed_plasma_parameters = _REGISTRY["plot_detailed_plasma_parameters"] -plot_magnetic_fields_in_plasma = _REGISTRY["plot_magnetic_fields_in_plasma"] -plot_main_plasma_information = _REGISTRY["plot_main_plasma_information"] -plot_max_normalised_beta_comparison = _REGISTRY["plot_max_normalised_beta_comparison"] -plot_plasma = _REGISTRY["plot_plasma"] -plot_plasma_coloumb_logarithms = _REGISTRY["plot_plasma_coloumb_logarithms"] -plot_plasma_current_comparison = _REGISTRY["plot_plasma_current_comparison"] -plot_plasma_outboard_toroidal_ripple_map = _REGISTRY[ - "plot_plasma_outboard_toroidal_ripple_map" -] -plot_sol_power_decay_length_comparison = _REGISTRY[ - "plot_sol_power_decay_length_comparison" -] -reaction_plot_grid = _REGISTRY["reaction_plot_grid"] -__all__ = [ - "plot_bootstrap_comparison", - "plot_brunner_divertor_power_split_comparison_stackplot", - "plot_confinement_time_comparison", - "plot_current_drive_info", - "plot_detailed_plasma_parameters", - "plot_magnetic_fields_in_plasma", - "plot_main_plasma_information", - "plot_max_normalised_beta_comparison", - "plot_plasma", - "plot_plasma_coloumb_logarithms", - "plot_plasma_current_comparison", - "plot_plasma_outboard_toroidal_ripple_map", - "plot_sol_power_decay_length_comparison", - "reaction_plot_grid", -] +"""Plasma implementation modules for summary plots.""" diff --git a/process/core/io/plot/summary/plasma/current_drive.py b/process/core/io/plot/summary/plasma/current_drive.py index eb2f8581c6..5ce5dbf44e 100644 --- a/process/core/io/plot/summary/plasma/current_drive.py +++ b/process/core/io/plot/summary/plasma/current_drive.py @@ -13,7 +13,7 @@ from process.core.io.plot.summary.rendering import ( draw_text, ) -from process.core.io.plot.summary.reporting import ( +from process.core.io.plot.summary.reporting.text import ( plot_info, ) diff --git a/process/core/io/plot/summary/power_flow.py b/process/core/io/plot/summary/power_flow.py index add27d689f..df0b040580 100644 --- a/process/core/io/plot/summary/power_flow.py +++ b/process/core/io/plot/summary/power_flow.py @@ -14,7 +14,7 @@ draw_annotation, draw_text, ) -from process.core.io.plot.summary.reporting import ( +from process.core.io.plot.summary.reporting.text import ( plot_info, ) diff --git a/process/core/io/plot/summary/profiles/__init__.py b/process/core/io/plot/summary/profiles/__init__.py index 650be21144..7ce6086d9d 100644 --- a/process/core/io/plot/summary/profiles/__init__.py +++ b/process/core/io/plot/summary/profiles/__init__.py @@ -1,104 +1 @@ -"""Public API for this summary plotting concern.""" - -from __future__ import annotations - -import process.core.io.plot.summary.profiles.atomic as _atomic -import process.core.io.plot.summary.profiles.misc as _misc -import process.core.io.plot.summary.profiles.plasma as _plasma -import process.core.io.plot.summary.profiles.radiation as _radiation -import process.core.io.plot.summary.profiles.stress as _stress - -_MODULES = (_atomic, _misc, _plasma, _radiation, _stress) -_REGISTRY = {} -for _module in _MODULES: - _REGISTRY.update({name: getattr(_module, name) for name in _module.__all__}) -for _module in _MODULES: - _module.__dict__.update(_REGISTRY) -interp1d_profile = _REGISTRY["interp1d_profile"] -plot_beta_profiles = _REGISTRY["plot_beta_profiles"] -plot_collision_frequency_profile = _REGISTRY["plot_collision_frequency_profile"] -plot_collision_time_profile = _REGISTRY["plot_collision_time_profile"] -plot_cs_hoop_stress_contour_profile = _REGISTRY["plot_cs_hoop_stress_contour_profile"] -plot_cs_hoop_stress_profile = _REGISTRY["plot_cs_hoop_stress_profile"] -plot_cs_radial_stress_contour_profile = _REGISTRY[ - "plot_cs_radial_stress_contour_profile" -] -plot_cs_radial_stress_profile = _REGISTRY["plot_cs_radial_stress_profile"] -plot_cs_stress_time_profile = _REGISTRY["plot_cs_stress_time_profile"] -plot_cs_tresca_2d_contour = _REGISTRY["plot_cs_tresca_2d_contour"] -plot_cs_vertical_stress_profile = _REGISTRY["plot_cs_vertical_stress_profile"] -plot_cs_von_mises_2d_contour = _REGISTRY["plot_cs_von_mises_2d_contour"] -plot_cumulative_plasma_thermal_energy_profiles = _REGISTRY[ - "plot_cumulative_plasma_thermal_energy_profiles" -] -plot_debye_length_profile = _REGISTRY["plot_debye_length_profile"] -plot_electron_frequency_profile = _REGISTRY["plot_electron_frequency_profile"] -plot_fusion_rate_contours = _REGISTRY["plot_fusion_rate_contours"] -plot_fusion_rate_profiles = _REGISTRY["plot_fusion_rate_profiles"] -plot_ion_charge_profile = _REGISTRY["plot_ion_charge_profile"] -plot_ion_frequency_profile = _REGISTRY["plot_ion_frequency_profile"] -plot_ion_slowing_down_time_profile = _REGISTRY["plot_ion_slowing_down_time_profile"] -plot_jprofile = _REGISTRY["plot_jprofile"] -plot_larmor_radius_profile = _REGISTRY["plot_larmor_radius_profile"] -plot_line_brem_loss_function_profile = _REGISTRY["plot_line_brem_loss_function_profile"] -plot_line_brem_power_density_profile = _REGISTRY["plot_line_brem_power_density_profile"] -plot_mean_free_path_profile = _REGISTRY["plot_mean_free_path_profile"] -plot_n_profiles = _REGISTRY["plot_n_profiles"] -plot_plasma_effective_charge_profile = _REGISTRY["plot_plasma_effective_charge_profile"] -plot_plasma_poloidal_pressure_contours = _REGISTRY[ - "plot_plasma_poloidal_pressure_contours" -] -plot_plasma_pressure_gradient_profiles = _REGISTRY[ - "plot_plasma_pressure_gradient_profiles" -] -plot_plasma_pressure_profiles = _REGISTRY["plot_plasma_pressure_profiles"] -plot_plasma_thermal_energy_profiles = _REGISTRY["plot_plasma_thermal_energy_profiles"] -plot_qprofile = _REGISTRY["plot_qprofile"] -plot_rad_contour = _REGISTRY["plot_rad_contour"] -plot_resistivity_profile = _REGISTRY["plot_resistivity_profile"] -plot_t_profiles = _REGISTRY["plot_t_profiles"] -plot_velocity_profile = _REGISTRY["plot_velocity_profile"] -plot_vertical_stress_contour_profile = _REGISTRY["plot_vertical_stress_contour_profile"] -profiles_with_pedestal = _REGISTRY["profiles_with_pedestal"] -read_imprad_data = _REGISTRY["read_imprad_data"] -__all__ = [ - "interp1d_profile", - "plot_beta_profiles", - "plot_collision_frequency_profile", - "plot_collision_time_profile", - "plot_cs_hoop_stress_contour_profile", - "plot_cs_hoop_stress_profile", - "plot_cs_radial_stress_contour_profile", - "plot_cs_radial_stress_profile", - "plot_cs_stress_time_profile", - "plot_cs_tresca_2d_contour", - "plot_cs_vertical_stress_profile", - "plot_cs_von_mises_2d_contour", - "plot_cumulative_plasma_thermal_energy_profiles", - "plot_debye_length_profile", - "plot_electron_frequency_profile", - "plot_fusion_rate_contours", - "plot_fusion_rate_profiles", - "plot_ion_charge_profile", - "plot_ion_frequency_profile", - "plot_ion_slowing_down_time_profile", - "plot_jprofile", - "plot_larmor_radius_profile", - "plot_line_brem_loss_function_profile", - "plot_line_brem_power_density_profile", - "plot_mean_free_path_profile", - "plot_n_profiles", - "plot_plasma_effective_charge_profile", - "plot_plasma_poloidal_pressure_contours", - "plot_plasma_pressure_gradient_profiles", - "plot_plasma_pressure_profiles", - "plot_plasma_thermal_energy_profiles", - "plot_qprofile", - "plot_rad_contour", - "plot_resistivity_profile", - "plot_t_profiles", - "plot_velocity_profile", - "plot_vertical_stress_contour_profile", - "profiles_with_pedestal", - "read_imprad_data", -] +"""Profiles implementation modules for summary plots.""" diff --git a/process/core/io/plot/summary/profiles/misc.py b/process/core/io/plot/summary/profiles/misc.py index d3b1c2a20f..f726becbde 100644 --- a/process/core/io/plot/summary/profiles/misc.py +++ b/process/core/io/plot/summary/profiles/misc.py @@ -7,13 +7,11 @@ import numpy as np from scipy.interpolate import interp1d -from process.core.io.plot.summary.profiles.radiation import ( - read_imprad_data, -) from process.core.io.plot.summary.rendering import ( draw_text, ) from process.models.geometry.plasma import plasma_geometry +from process.models.physics.impurity_radiation import read_impurity_file if TYPE_CHECKING: import matplotlib.pyplot as plt @@ -21,6 +19,58 @@ from process.core.io.mfile import MFile +def read_imprad_data(_skiprows, data_path): + """Function to read all data needed for creation of radiation profile + + Parameters + ---------- + _skiprows : + number of rows to skip when reading impurity data files + data_path : + path to impurity data + + """ + label = [ + "H_", + "He", + "Be", + "C_", + "N_", + "O_", + "Ne", + "Si", + "Ar", + "Fe", + "Ni", + "Kr", + "Xe", + "W_", + ] + lzdata = [0.0 for x in range(len(label))] + + for i in range(len(label)): + file_iden = data_path + label[i].ljust(3, "_") + + Te = None + lz = None + zav = None + + for header in read_impurity_file(file_iden + "lz_tau.dat"): + if "Te[eV]" in header.content: + Te = np.asarray(header.data, dtype=float) + + if "infinite confinement" in header.content: + lz = np.asarray(header.data, dtype=float) + for header in read_impurity_file(file_iden + "z_tau.dat"): + if "infinite confinement" in header.content: + zav = np.asarray(header.data, dtype=float) + + lzdata[i] = np.column_stack([Te, lz, zav]) + + # then switch string to floats + return np.array(lzdata, dtype=float) + + def profiles_with_pedestal(mfile, scan: int): """Calculate profiles with pedestal""" alphan = mfile.get("alphan", scan=scan) diff --git a/process/core/io/plot/summary/profiles/plasma.py b/process/core/io/plot/summary/profiles/plasma.py index b62adec830..f1252ece73 100644 --- a/process/core/io/plot/summary/profiles/plasma.py +++ b/process/core/io/plot/summary/profiles/plasma.py @@ -12,7 +12,7 @@ box_style, text_layout, ) -from process.core.io.plot.summary.plasma import ( +from process.core.io.plot.summary.plasma.physics import ( reaction_plot_grid, ) from process.core.io.plot.summary.profiles.misc import ( diff --git a/process/core/io/plot/summary/profiles/radiation.py b/process/core/io/plot/summary/profiles/radiation.py index e849e5cc2f..18c99acdf5 100644 --- a/process/core/io/plot/summary/profiles/radiation.py +++ b/process/core/io/plot/summary/profiles/radiation.py @@ -12,6 +12,7 @@ from process.core.io.plot.summary.profiles.misc import ( interp1d_profile, profiles_with_pedestal, + read_imprad_data, ) from process.core.io.plot.summary.rendering import ( draw_text, @@ -20,7 +21,6 @@ poisson_steel, ) from process.models.pfcoil import N_CS_STRESS_PROFILE_POINTS, CSCoil -from process.models.physics.impurity_radiation import read_impurity_file if TYPE_CHECKING: import matplotlib.pyplot as plt @@ -29,58 +29,6 @@ from process.core.io.mfile import MFile -def read_imprad_data(_skiprows, data_path): - """Function to read all data needed for creation of radiation profile - - Parameters - ---------- - _skiprows : - number of rows to skip when reading impurity data files - data_path : - path to impurity data - - """ - label = [ - "H_", - "He", - "Be", - "C_", - "N_", - "O_", - "Ne", - "Si", - "Ar", - "Fe", - "Ni", - "Kr", - "Xe", - "W_", - ] - lzdata = [0.0 for x in range(len(label))] - - for i in range(len(label)): - file_iden = data_path + label[i].ljust(3, "_") - - Te = None - lz = None - zav = None - - for header in read_impurity_file(file_iden + "lz_tau.dat"): - if "Te[eV]" in header.content: - Te = np.asarray(header.data, dtype=float) - - if "infinite confinement" in header.content: - lz = np.asarray(header.data, dtype=float) - for header in read_impurity_file(file_iden + "z_tau.dat"): - if "infinite confinement" in header.content: - zav = np.asarray(header.data, dtype=float) - - lzdata[i] = np.column_stack([Te, lz, zav]) - - # then switch string to floats - return np.array(lzdata, dtype=float) - - def plot_rad_contour(axis: Axes, mfile: MFile, scan: int, impp: str): """Plots the contour of line and bremsstrahlung radiation density for a plasma cross-section. @@ -458,5 +406,4 @@ def plot_cs_radial_stress_contour_profile( "plot_larmor_radius_profile", "plot_plasma_pressure_gradient_profiles", "plot_rad_contour", - "read_imprad_data", ] diff --git a/process/core/io/plot/summary/radial_build.py b/process/core/io/plot/summary/radial_build.py new file mode 100644 index 0000000000..b678372649 --- /dev/null +++ b/process/core/io/plot/summary/radial_build.py @@ -0,0 +1,93 @@ +"""Radial build lookup utilities used by geometry and magnet plots.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from process.core.io.plot.summary.constants import RADIAL_BUILD + +if TYPE_CHECKING: + from process.core.io.mfile import MFile + + +def cumulative_radial_build(section, mfile: MFile, scan: int): + """Function for calculating the cumulative radial build up to and + including the given section. + + Parameters + ---------- + section : + section of the radial build to go up to + mfile : + MFILE data object + scan : + scan number to use + + Returns + ------- + : + cumulative_build:cumulative radial build up to section given + """ + complete = False + cumulative_build = 0 + for item in RADIAL_BUILD: + if item in {"rminori", "rminoro"}: + cumulative_build += mfile.get("rminor", scan=scan) + elif item in {"vvblgapi", "vvblgapo"}: + cumulative_build += mfile.get("dr_shld_blkt_gap", scan=scan) + elif "dr_vv_inboard" in item: + cumulative_build += mfile.get("dr_vv_inboard", scan=scan) + elif "dr_vv_outboard" in item: + cumulative_build += mfile.get("dr_vv_outboard", scan=scan) + else: + cumulative_build += mfile.get(item, scan=scan) + if item == section: + complete = True + break + + if complete is False: + print("radial build parameter ", section, " not found") + return cumulative_build + + +def cumulative_radial_build2(section, mfile: MFile, scan: int): + """Function for calculating the cumulative radial build up to and + including the given section. + + Parameters + ---------- + section : + section of the radial build to go up to + mfile : + MFILE data object + scan : + scan number to use + + Returns + ------- + : + cumulative_build --> cumulative radial build up to and including + section given + previous --> cumulative radial build up to section given + """ + cumulative_build = 0 + build = 0 + for item in RADIAL_BUILD: + if item in {"rminori", "rminoro"}: + build = mfile.get("rminor", scan=scan) + elif item in {"vvblgapi", "vvblgapo"}: + build = mfile.get("dr_shld_blkt_gap", scan=scan) + elif "dr_vv_inboard" in item: + build = mfile.get("dr_vv_inboard", scan=scan) + elif "dr_vv_outboard" in item: + build = mfile.get("dr_vv_outboard", scan=scan) + else: + build = mfile.get(item, scan=scan) + cumulative_build += build + if item == section: + break + previous = cumulative_build - build + return (cumulative_build, previous) + + +__all__ = ["cumulative_radial_build", "cumulative_radial_build2"] diff --git a/process/core/io/plot/summary/reporting/__init__.py b/process/core/io/plot/summary/reporting/__init__.py index a37d04c329..4bee3896c3 100644 --- a/process/core/io/plot/summary/reporting/__init__.py +++ b/process/core/io/plot/summary/reporting/__init__.py @@ -1,49 +1 @@ -"""Public API for this summary plotting concern.""" - -from __future__ import annotations - -import process.core.io.plot.summary.reporting.constraints as _constraints -import process.core.io.plot.summary.reporting.layouts as _layouts -import process.core.io.plot.summary.reporting.misc as _misc -import process.core.io.plot.summary.reporting.panels as _panels - -_MODULES = (_constraints, _layouts, _misc, _panels) -_REGISTRY = {} -for _module in _MODULES: - _REGISTRY.update({name: getattr(_module, name) for name in _module.__all__}) -for _module in _MODULES: - _module.__dict__.update(_REGISTRY) -RadialBuild = _REGISTRY["RadialBuild"] -draw_bend = _REGISTRY["draw_bend"] -plot_centre_cross = _REGISTRY["plot_centre_cross"] -plot_cover_page = _REGISTRY["plot_cover_page"] -plot_density_limit_comparison = _REGISTRY["plot_density_limit_comparison"] -plot_ebw_ecrh_coupling_graph = _REGISTRY["plot_ebw_ecrh_coupling_graph"] -plot_equality_constraint_equations = _REGISTRY["plot_equality_constraint_equations"] -plot_fw_90_deg_pipe_bend = _REGISTRY["plot_fw_90_deg_pipe_bend"] -plot_h_threshold_comparison = _REGISTRY["plot_h_threshold_comparison"] -plot_header = _REGISTRY["plot_header"] -plot_inequality_constraint_equations = _REGISTRY["plot_inequality_constraint_equations"] -plot_info = _REGISTRY["plot_info"] -plot_iteration_variables = _REGISTRY["plot_iteration_variables"] -plot_lower_vertical_build = _REGISTRY["plot_lower_vertical_build"] -plot_separatrix_power_split = _REGISTRY["plot_separatrix_power_split"] -plot_upper_vertical_build = _REGISTRY["plot_upper_vertical_build"] -__all__ = [ - "RadialBuild", - "draw_bend", - "plot_centre_cross", - "plot_cover_page", - "plot_density_limit_comparison", - "plot_ebw_ecrh_coupling_graph", - "plot_equality_constraint_equations", - "plot_fw_90_deg_pipe_bend", - "plot_h_threshold_comparison", - "plot_header", - "plot_inequality_constraint_equations", - "plot_info", - "plot_iteration_variables", - "plot_lower_vertical_build", - "plot_separatrix_power_split", - "plot_upper_vertical_build", -] +"""Reporting implementation modules for summary plots.""" diff --git a/process/core/io/plot/summary/reporting/misc.py b/process/core/io/plot/summary/reporting/misc.py index 5034dc6003..8cd143f3dc 100644 --- a/process/core/io/plot/summary/reporting/misc.py +++ b/process/core/io/plot/summary/reporting/misc.py @@ -2,6 +2,7 @@ from __future__ import annotations +from dataclasses import dataclass from typing import TYPE_CHECKING, Literal import matplotlib.pyplot as plt @@ -30,6 +31,7 @@ from process.core.io.mfile import MFile +@dataclass class RadialBuild: """Dataclass containing radial build dictionaries""" diff --git a/process/core/io/plot/summary/reporting/panels.py b/process/core/io/plot/summary/reporting/panels.py index f751a5f5da..89f320f128 100644 --- a/process/core/io/plot/summary/reporting/panels.py +++ b/process/core/io/plot/summary/reporting/panels.py @@ -14,16 +14,17 @@ box_style, setup_axis, ) -from process.core.io.plot.summary.geometry import ( +from process.core.io.plot.summary.geometry.poloidal import ( poloidal_cross_section, ) -from process.core.io.plot.summary.plasma import ( +from process.core.io.plot.summary.plasma.physics import ( plot_plasma, ) from process.core.io.plot.summary.rendering import ( draw_annotation, draw_text, ) +from process.core.io.plot.summary.reporting.text import plot_info from process.data_structure.numerics import FiguresOfMerit, PROCESSRunMode from process.data_structure.physics_variables import DivertorNumberModels @@ -33,93 +34,6 @@ ) -def plot_info(axis: plt.Axes, data, mfile: MFile, scan: int): - """Function to plot data in written form on a matplotlib plot. - - Parameters - ---------- - axis : - axis object to plot to - data : - plot information - mfile : - MFILE - scan : - scan number to use - """ - eqpos = 0.75 - for i in range(len(data)): - colorflag = "black" - if mfile.data[data[i][0]].exists: - if mfile.data[data[i][0]].var_flag == "ITV": - colorflag = "red" - elif mfile.data[data[i][0]].var_flag == "OP": - colorflag = "blue" - draw_text(axis, 0, -i, data[i][1], color=colorflag, ha="left", va="center") - if isinstance(data[i][0], str): - if not data[i][0]: - draw_text(axis, eqpos, -i, "\n", ha="left", va="center") - elif data[i][0][0] == "#": - draw_text( - axis, - -0.05, - -i, - f"{data[i][0][1:]}\n", - ha="left", - va="center", - ) - elif data[i][0][0] == "!": - value = data[i][0][1:].replace('"', "") - draw_text( - axis, - 0.4, - -i, - f"--> {value} {data[i][2]}", - ha="left", - va="center", - ) - elif mfile.data[data[i][0]].exists: - dat = mfile.get(data[i][0], scan=scan) - if isinstance(dat, str): - value = dat - else: - value = f"{mfile.get(data[i][0], scan=scan):.4g}" - if "alpha" in data[i][0]: - value = str(float(value) + 1.0) - draw_text( - axis, - eqpos, - -i, - f"= {value} {data[i][2]}", - color=colorflag, - ha="left", - va="center", - ) - else: - mfile.get(data[i][0], scan=-1) - draw_text( - axis, - eqpos, - -i, - "= ERROR! Var missing", - color=colorflag, - ha="left", - va="center", - ) - else: - dat = data[i][0] - value = dat if isinstance(dat, str) else f"{data[i][0]:.4g}" - draw_text( - axis, - eqpos, - -i, - f"= {value} {data[i][2]}", - color=colorflag, - ha="left", - va="center", - ) - - def plot_header(axis: plt.Axes, mfile: MFile, scan: int): """Function to plot header info: date, rutitle etc @@ -642,6 +556,5 @@ def plot_cover_page( __all__ = [ "plot_cover_page", "plot_header", - "plot_info", "plot_separatrix_power_split", ] diff --git a/process/core/io/plot/summary/reporting/text.py b/process/core/io/plot/summary/reporting/text.py new file mode 100644 index 0000000000..db046c7d43 --- /dev/null +++ b/process/core/io/plot/summary/reporting/text.py @@ -0,0 +1,102 @@ +"""Text-based report panel helpers.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from process.core.io.plot.summary.rendering import draw_text + +if TYPE_CHECKING: + import matplotlib.pyplot as plt + + from process.core.io.mfile import MFile + + +def plot_info(axis: plt.Axes, data, mfile: MFile, scan: int): + """Function to plot data in written form on a matplotlib plot. + + Parameters + ---------- + axis : + axis object to plot to + data : + plot information + mfile : + MFILE + scan : + scan number to use + """ + eqpos = 0.75 + for i in range(len(data)): + colorflag = "black" + if mfile.data[data[i][0]].exists: + if mfile.data[data[i][0]].var_flag == "ITV": + colorflag = "red" + elif mfile.data[data[i][0]].var_flag == "OP": + colorflag = "blue" + draw_text(axis, 0, -i, data[i][1], color=colorflag, ha="left", va="center") + if isinstance(data[i][0], str): + if not data[i][0]: + draw_text(axis, eqpos, -i, "\n", ha="left", va="center") + elif data[i][0][0] == "#": + draw_text( + axis, + -0.05, + -i, + f"{data[i][0][1:]}\n", + ha="left", + va="center", + ) + elif data[i][0][0] == "!": + value = data[i][0][1:].replace('"', "") + draw_text( + axis, + 0.4, + -i, + f"--> {value} {data[i][2]}", + ha="left", + va="center", + ) + elif mfile.data[data[i][0]].exists: + dat = mfile.get(data[i][0], scan=scan) + if isinstance(dat, str): + value = dat + else: + value = f"{mfile.get(data[i][0], scan=scan):.4g}" + if "alpha" in data[i][0]: + value = str(float(value) + 1.0) + draw_text( + axis, + eqpos, + -i, + f"= {value} {data[i][2]}", + color=colorflag, + ha="left", + va="center", + ) + else: + mfile.get(data[i][0], scan=-1) + draw_text( + axis, + eqpos, + -i, + "= ERROR! Var missing", + color=colorflag, + ha="left", + va="center", + ) + else: + dat = data[i][0] + value = dat if isinstance(dat, str) else f"{data[i][0]:.4g}" + draw_text( + axis, + eqpos, + -i, + f"= {value} {data[i][2]}", + color=colorflag, + ha="left", + va="center", + ) + + +__all__ = ["plot_info"] diff --git a/process/core/io/plot/summary/time_profiles.py b/process/core/io/plot/summary/time_profiles.py index 90fb6aca7f..f85800346b 100644 --- a/process/core/io/plot/summary/time_profiles.py +++ b/process/core/io/plot/summary/time_profiles.py @@ -10,7 +10,7 @@ box_style, get_pulse_timings, ) -from process.core.io.plot.summary.magnets import ( +from process.core.io.plot.summary.magnets.cs import ( secs_to_hms, ) from process.core.io.plot.summary.rendering import ( From deb010103b3402ca0c5b3c4cbaacbbcca4adafad Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:54:31 +0100 Subject: [PATCH 15/18] missed import --- process/core/io/plot/summary/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/process/core/io/plot/summary/__init__.py b/process/core/io/plot/summary/__init__.py index c0c0d1d7f1..a3244cf14d 100644 --- a/process/core/io/plot/summary/__init__.py +++ b/process/core/io/plot/summary/__init__.py @@ -5,5 +5,13 @@ main_plot, plot_summary, ) +from process.core.io.plot.summary.reporting.constraints import ( + plot_inequality_constraint_equations, +) -__all__ = ["create_thickness_builds", "main_plot", "plot_summary"] +__all__ = [ + "create_thickness_builds", + "main_plot", + "plot_inequality_constraint_equations", + "plot_summary", +] From b5799d4f4411bca401399073a935b54eecd00afe Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:57:50 +0100 Subject: [PATCH 16/18] move over changes from lz ev fix --- process/core/io/plot/summary/api.py | 12 +- .../core/io/plot/summary/plasma/overview.py | 43 +-- .../core/io/plot/summary/profiles/atomic.py | 29 +- process/core/io/plot/summary/profiles/misc.py | 352 ++++++++++++------ .../io/plot/summary/profiles/radiation.py | 43 +-- 5 files changed, 297 insertions(+), 182 deletions(-) diff --git a/process/core/io/plot/summary/api.py b/process/core/io/plot/summary/api.py index 9f586b58b5..29d72d5029 100644 --- a/process/core/io/plot/summary/api.py +++ b/process/core/io/plot/summary/api.py @@ -98,6 +98,7 @@ from process.core.io.plot.summary.profiles.misc import ( plot_line_brem_loss_function_profile, plot_line_brem_power_density_profile, + plot_line_brem_power_profile, ) from process.core.io.plot.summary.profiles.plasma import ( plot_beta_profiles, @@ -118,7 +119,7 @@ plot_cs_radial_stress_profile, plot_larmor_radius_profile, plot_plasma_pressure_gradient_profiles, - plot_rad_contour, + plot_rad_density_contour, ) from process.core.io.plot.summary.profiles.stress import ( plot_cs_hoop_stress_contour_profile, @@ -274,6 +275,7 @@ def _add_page(name: str | None = None): # Plot impurity profiles ax11 = pages["profiles"].add_subplot(233) ax11.set_position([0.7, 0.45, 0.25, 0.5]) + plot_line_brem_power_density_profile( axis=ax11, mfile=m_file, scan=scan, impp=imp, demo_ranges=demo_ranges ) @@ -311,7 +313,9 @@ def _add_page(name: str | None = None): ) if i_shape == 1: - plot_rad_contour(pages["rad_contour"].add_subplot(122), m_file, scan, imp) + plot_rad_density_contour( + pages["rad_contour"].add_subplot(122, aspect="equal"), m_file, scan, imp + ) if i_shape != 1: msg = ( @@ -324,7 +328,9 @@ def _add_page(name: str | None = None): pages["rad_contour"].text( 0.75, 0.5, msg, ha="center", va="center", wrap=True, fontsize=12 ) - + plot_line_brem_power_profile( + _add_page("line_brem_power").add_subplot(121), m_file, scan, imp + ) plot_fusion_rate_profiles( _add_page("fusion_rate").add_subplot(122), pages["fusion_rate"], diff --git a/process/core/io/plot/summary/plasma/overview.py b/process/core/io/plot/summary/plasma/overview.py index 948b18d5ba..485d3021ca 100644 --- a/process/core/io/plot/summary/plasma/overview.py +++ b/process/core/io/plot/summary/plasma/overview.py @@ -21,6 +21,7 @@ draw_annotation, draw_text, ) +from process.data_structure.impurity_radiation_variables import ImpurityRadiationData from process.models.geometry.plasma import plasma_geometry from process.models.physics.bootstrap_current import ( BootstrapCurrentFractionModel, @@ -653,38 +654,18 @@ class TextArgs(TypedDict): # ================================================ # Add ion density information + impurity_data = ImpurityRadiationData() textstr_ions = ( - " $\\mathbf{Ion \\ to \\ electron}$\n " - " $\\mathbf{relative \\ number}$\n " - " $\\mathbf{densities:}$\n\n Effective charge:" - f" {mfile.get('n_charge_plasma_effective_vol_avg', scan=scan):.3f}\n\n" - " H: " - f" {mfile.get('f_nd_impurity_electrons(01)', scan=scan):.4e}\n " - " He: " - f" {mfile.get('f_nd_impurity_electrons(02)', scan=scan):.4e}\n " - " Be: " - f" {mfile.get('f_nd_impurity_electrons(03)', scan=scan):.4e}\n " - " C: " - f" {mfile.get('f_nd_impurity_electrons(04)', scan=scan):.4e}\n " - " N: " - f" {mfile.get('f_nd_impurity_electrons(05)', scan=scan):.4e}\n " - " O: " - f" {mfile.get('f_nd_impurity_electrons(06)', scan=scan):.4e}\n " - " Ne: " - f" {mfile.get('f_nd_impurity_electrons(07)', scan=scan):.4e}\n " - " Si: " - f" {mfile.get('f_nd_impurity_electrons(08)', scan=scan):.4e}\n " - " Ar: " - f" {mfile.get('f_nd_impurity_electrons(09)', scan=scan):.4e}\n " - " Fe: " - f" {mfile.get('f_nd_impurity_electrons(10)', scan=scan):.4e}\n " - " Ni: " - f" {mfile.get('f_nd_impurity_electrons(11)', scan=scan):.4e}\n " - " Kr: " - f" {mfile.get('f_nd_impurity_electrons(12)', scan=scan):.4e}\n " - " Xe: " - f" {mfile.get('f_nd_impurity_electrons(13)', scan=scan):.4e}\n " - f" W: {mfile.get('f_nd_impurity_electrons(14)', scan=scan):.4e}" + f" $\\mathbf{{Ion \\ to \\ electron}}$\n" + f" $\\mathbf{{relative \\ number}}$\n" + f" $\\mathbf{{densities:}}$\n\n" + " Effective charge: " + f"{mfile.get('n_charge_plasma_effective_vol_avg', scan=scan):.3f}\n\n" + + "\n".join( + f" {label.replace('_', '') + ':':<6}" + f"{mfile.get(f'f_nd_impurity_electrons({index:02d})', scan=scan):.4e}" + for index, label in enumerate(impurity_data.imp_label[:14], start=1) + ) ) draw_text( diff --git a/process/core/io/plot/summary/profiles/atomic.py b/process/core/io/plot/summary/profiles/atomic.py index c9c8c95ce9..4051a0c5d8 100644 --- a/process/core/io/plot/summary/profiles/atomic.py +++ b/process/core/io/plot/summary/profiles/atomic.py @@ -6,7 +6,10 @@ import numpy as np -from process.data_structure.impurity_radiation_variables import N_IMPURITIES +from process.data_structure.impurity_radiation_variables import ( + N_IMPURITIES, + ImpurityRadiationData, +) if TYPE_CHECKING: import matplotlib.pyplot as plt @@ -36,25 +39,8 @@ def plot_ion_charge_profile(axis: plt.Axes, mfile: MFile, scan: int): mfile.get("f_nd_impurity_electrons(14)", scan=scan), ]) - imp_label = [ - "H", - "He", - "Be", - "C", - "N", - "O", - "Ne", - "Si", - "Ar", - "Fe", - "Ni", - "Kr", - "Xe", - "W", - ] - full_charge_array = [1, 2, 4, 6, 7, 8, 10, 14, 18, 26, 28, 36, 54, 74] - n_charge_plasma_profile = [] + impurity_data = ImpurityRadiationData() for imp in range(N_IMPURITIES): if imp_frac[imp] > 1.0e-30: profile = [ @@ -62,7 +48,7 @@ def plot_ion_charge_profile(axis: plt.Axes, mfile: MFile, scan: int): for i in range(n_plasma_profile_elements) ] n_charge_plasma_profile.append(profile) - z_max = full_charge_array[imp] + z_max = impurity_data.imp_full_ion_charge[imp] # Calculate relative ionisation state as percent of full ionisation rel_ion_state = [ 100.0 * (val / z_max if z_max > 0 else 0) for val in profile @@ -71,7 +57,8 @@ def plot_ion_charge_profile(axis: plt.Axes, mfile: MFile, scan: int): axis.plot( np.linspace(0, 1, n_plasma_profile_elements), rel_ion_state, - label=(f"{imp_label[imp]} (Z={z_max}): avg {avg_ionisation:.1f}%"), + label=f"{impurity_data.imp_label[imp].replace('_', '')} (Z={z_max}): " + f"avg {avg_ionisation:.1f}%", ) axis.set_ylabel("Relative Ionisation State [% of $Z$]") axis.legend() diff --git a/process/core/io/plot/summary/profiles/misc.py b/process/core/io/plot/summary/profiles/misc.py index f726becbde..3549933fca 100644 --- a/process/core/io/plot/summary/profiles/misc.py +++ b/process/core/io/plot/summary/profiles/misc.py @@ -10,8 +10,10 @@ from process.core.io.plot.summary.rendering import ( draw_text, ) +from process.data_structure.impurity_radiation_variables import ImpurityRadiationData from process.models.geometry.plasma import plasma_geometry from process.models.physics.impurity_radiation import read_impurity_file +from process.models.physics.profiles import calculate_profile_shell_contributions if TYPE_CHECKING: import matplotlib.pyplot as plt @@ -30,26 +32,11 @@ def read_imprad_data(_skiprows, data_path): path to impurity data """ - label = [ - "H_", - "He", - "Be", - "C_", - "N_", - "O_", - "Ne", - "Si", - "Ar", - "Fe", - "Ni", - "Kr", - "Xe", - "W_", - ] - lzdata = [0.0 for x in range(len(label))] - - for i in range(len(label)): - file_iden = data_path + label[i].ljust(3, "_") + imp_label = ImpurityRadiationData().imp_label + lzdata = [] + + for label in imp_label: + file_iden = data_path + label.ljust(3, "_") Te = None lz = None @@ -65,7 +52,7 @@ def read_imprad_data(_skiprows, data_path): if "infinite confinement" in header.content: zav = np.asarray(header.data, dtype=float) - lzdata[i] = np.column_stack([Te, lz, zav]) + lzdata.append(np.column_stack([Te, lz, zav])) # then switch string to floats return np.array(lzdata, dtype=float) @@ -154,7 +141,7 @@ def profiles_with_pedestal(mfile, scan: int): def plot_line_brem_power_density_profile( axis: plt.Axes, mfile: MFile, scan: int, impp: str, demo_ranges: bool ): - """Function to plot Line and Bremsstrahlung radiation power density profile. + """Function to plot Line and Bremsstrahlung radiation power density [MW/m³] profile. Parameters ---------- @@ -172,7 +159,7 @@ def plot_line_brem_power_density_profile( """ axis.set_xlabel(r"$\rho \quad [r/a]$") axis.set_ylabel(r"$P_{\mathrm{rad}}$ $[\mathrm{MW.m}^{-3}]$") - axis.set_title("Raw Data: Line & Bremsstrahlung radiation profile") + axis.set_title("Raw Data: Line & Bremsstrahlung Radiation Density Profile") # read in the impurity data imp_data = read_imprad_data(_skiprows=2, data_path=impp) @@ -182,65 +169,66 @@ def plot_line_brem_power_density_profile( mfile.get(f"f_nd_impurity_electrons({i:02d})", scan=scan) for i in range(1, 15) ]) - rho, ne, te = profiles_with_pedestal(mfile, scan) + rho, nd_electron, temp_electron_kev = profiles_with_pedestal(mfile, scan) + # imp_data Te values are in eV, but te from profiles_with_pedestal is in keV + temp_electron_ev = temp_electron_kev * 1.0e3 # Intailise the radiation profile arrays - pimpden = np.zeros([imp_data.shape[0], te.shape[0]]) - lz = np.zeros([imp_data.shape[0], te.shape[0]]) - prad = np.zeros(te.shape[0]) + pden_rad_array = np.zeros([imp_data.shape[0], temp_electron_kev.shape[0]]) + lz = np.zeros([imp_data.shape[0], temp_electron_kev.shape[0]]) + pden_total_profile = np.zeros(temp_electron_kev.shape[0]) # Intailise the impurity radiation profile - for k in range(te.shape[0]): - for i in range(imp_data.shape[0]): - if te[k] <= imp_data[i][0][0]: - lz[i][k] = imp_data[i][0][1] - elif te[k] >= imp_data[i][imp_data.shape[1] - 1][0]: - lz[i][k] = imp_data[i][imp_data.shape[1] - 1][1] + for temp_point in range(temp_electron_kev.shape[0]): + for impurity in range(imp_data.shape[0]): + if temp_electron_ev[temp_point] <= imp_data[impurity][0][0]: + lz[impurity][temp_point] = imp_data[impurity][0][1] + elif ( + temp_electron_ev[temp_point] + >= imp_data[impurity][imp_data.shape[1] - 1][0] + ): + lz[impurity][temp_point] = imp_data[impurity][imp_data.shape[1] - 1][1] else: # Use np.interp for log-log interpolation - log_te_data = np.log([row[0] for row in imp_data[i]]) - log_lz_data = np.log([row[1] for row in imp_data[i]]) - lz[i][k] = np.exp(np.interp(np.log(te[k]), log_te_data, log_lz_data)) - pimpden[i][k] = imp_frac[i] * ne[k] * ne[k] * lz[i][k] + log_te_data = np.log([row[0] for row in imp_data[impurity]]) + log_lz_data = np.log([row[1] for row in imp_data[impurity]]) + lz[impurity][temp_point] = np.exp( + np.interp( + np.log(temp_electron_ev[temp_point]), log_te_data, log_lz_data + ) + ) + pden_rad_array[impurity][temp_point] = ( + imp_frac[impurity] + * nd_electron[temp_point] + * nd_electron[temp_point] + * lz[impurity][temp_point] + ) for l_ in range(imp_data.shape[0]): - prad[k] += pimpden[l_][k] * 1.0e-6 - - axis.plot(rho, prad, label="Total", linestyle="dotted") - axis.plot(rho, pimpden[0] * 1.0e-6, label="H") - axis.plot(rho, pimpden[1] * 1.0e-6, label="He") - if imp_frac[2] > 1.0e-30: - axis.plot(rho, pimpden[2] * 1.0e-6, label="Be") - if imp_frac[3] > 1.0e-30: - axis.plot(rho, pimpden[3] * 1.0e-6, label="C") - if imp_frac[4] > 1.0e-30: - axis.plot(rho, pimpden[4] * 1.0e-6, label="N") - if imp_frac[5] > 1.0e-30: - axis.plot(rho, pimpden[5] * 1.0e-6, label="O") - if imp_frac[6] > 1.0e-30: - axis.plot(rho, pimpden[6] * 1.0e-6, label="Ne") - if imp_frac[7] > 1.0e-30: - axis.plot(rho, pimpden[7] * 1.0e-6, label="Si") - if imp_frac[8] > 1.0e-30: - axis.plot(rho, pimpden[8] * 1.0e-6, label="Ar") - if imp_frac[9] > 1.0e-30: - axis.plot(rho, pimpden[9] * 1.0e-6, label="Fe") - if imp_frac[10] > 1.0e-30: - axis.plot(rho, pimpden[10] * 1.0e-6, label="Ni") - if imp_frac[11] > 1.0e-30: - axis.plot(rho, pimpden[11] * 1.0e-6, label="Kr") - if imp_frac[12] > 1.0e-30: - axis.plot(rho, pimpden[12] * 1.0e-6, label="Xe") - if imp_frac[13] > 1.0e-30: - axis.plot(rho, pimpden[13] * 1.0e-6, label="W") - axis.legend(loc="upper left", bbox_to_anchor=(-0.1, -0.1), ncol=4) + pden_total_profile[temp_point] += pden_rad_array[l_][temp_point] * 1.0e-6 + + # Plot the total radiation profile and individual impurity contributions + axis.plot(rho, pden_total_profile, label="Total", linestyle="dotted") + axis.plot(rho, pden_rad_array[0] * 1.0e-6, label="H") + axis.plot(rho, pden_rad_array[1] * 1.0e-6, label="He") + + # Plot the remaining impurity contributions if their fraction is significant + imp_labels = ImpurityRadiationData().imp_label + for ind in range(2, imp_data.shape[0]): + if imp_frac[ind] > 1.0e-30: + axis.plot( + rho, + pden_rad_array[ind] * 1.0e-6, + label=imp_labels[ind].replace("_", ""), + ) + axis.minorticks_on() # Plot a vertical line at the core region radius core_radius = mfile.get("radius_plasma_core_norm", scan=scan) # Plot a vertical line at the core region radius axis.axvline(x=core_radius, color="black", linestyle="--", linewidth=1.0, alpha=0.7) - # Plot a box in the bottom left with f_{core,reduce} + # Plot a box in the bottom left with f_{core,reduce} and \rho_{core} props_core_reduce = { "boxstyle": "round", "facecolor": "khaki", @@ -250,7 +238,8 @@ def plot_line_brem_power_density_profile( axis, 0.02, 0.02, - rf"$f_{{\text{{core,reduce}}}}$ = {1.0}", + rf"$f_{{\text{{core,reduce}}}}$ = {1.0}\n" + rf"$\rho_{{\text{{core}}}}$ = {core_radius:.3f}", transform=axis.transAxes, fontsize=8, verticalalignment="bottom", @@ -259,16 +248,172 @@ def plot_line_brem_power_density_profile( # Ranges # --- + axis.legend(loc="upper left", bbox_to_anchor=(-0.1, -0.1), ncol=4) axis.set_xlim(0, 1.0) axis.set_yscale("log") axis.yaxis.grid(True, which="both", alpha=0.2) # DEMO : Fixed ranges for comparison if demo_ranges: - axis.set_ylim(1e-6, 0.5) + axis.set_ylim(1e-4, 0.5) # Adaptive ranges else: - axis.set_ylim(1e-6, axis.get_ylim()[1]) + axis.set_ylim(1e-4, axis.get_ylim()[1]) + + +def plot_line_brem_power_profile( + axis: plt.Axes, + mfile: MFile, + scan: int, + impp: str, +): + """Function to plot Line and Bremsstrahlung radiation power [MW] profile. + + Parameters + ---------- + axis : plt.Axes + axis object to add plot to + mfile : MFile + MFile object containing plasma and impurity profile information. + scan : int + scan number to use + impp : str + impurity path + + """ + axis.set_xlabel(r"$\rho \quad [r/a]$") + axis.set_ylabel(r"$P_{\mathrm{rad}}$ $[\mathrm{MW}]$") + axis.set_title("Line & Bremsstrahlung Radiation Power Profile") + + # read in the impurity data + imp_data = read_imprad_data(_skiprows=2, data_path=impp) + + # find impurity densities + imp_frac = np.array([ + mfile.get(f"f_nd_impurity_electrons({i:02d})", scan=scan) for i in range(1, 15) + ]) + vol_plasma = mfile.get("vol_plasma", scan=scan) + p_plasma_rad_imps_mw = mfile.get("p_plasma_rad_mw", scan=scan) - mfile.get( + "p_plasma_sync_mw", scan=scan + ) + + rho, nd_electron, temp_electron_kev = profiles_with_pedestal(mfile=mfile, scan=scan) + # imp_data Te values are in eV, but te from profiles_with_pedestal is in keV + temp_electron_ev = temp_electron_kev * 1.0e3 + + # Intailise the radiation profile arrays + p_rad_array = np.zeros([imp_data.shape[0], temp_electron_kev.shape[0]]) + lz = np.zeros([imp_data.shape[0], temp_electron_kev.shape[0]]) + p_total_profile = np.zeros(temp_electron_kev.shape[0]) + + # Intailise the impurity radiation profile + for temp_point in range(temp_electron_kev.shape[0]): + for impurity in range(imp_data.shape[0]): + if temp_electron_ev[temp_point] <= imp_data[impurity][0][0]: + lz[impurity][temp_point] = imp_data[impurity][0][1] + elif ( + temp_electron_ev[temp_point] + >= imp_data[impurity][imp_data.shape[1] - 1][0] + ): + lz[impurity][temp_point] = imp_data[impurity][imp_data.shape[1] - 1][1] + else: + # Use np.interp for log-log interpolation + log_te_data = np.log([row[0] for row in imp_data[impurity]]) + log_lz_data = np.log([row[1] for row in imp_data[impurity]]) + lz[impurity][temp_point] = np.exp( + np.interp( + np.log(temp_electron_ev[temp_point]), log_te_data, log_lz_data + ) + ) + + # Calculate the absolue radiation power for each volumetric shell for each + # impurity + p_rad_array[impurity] = calculate_profile_shell_contributions( + profile_x=rho, + profile_y=( + imp_frac[impurity] * nd_electron * nd_electron * lz[impurity] + ), + vol_plasma=vol_plasma, + profile_dx=rho[1] - rho[0], + ) + + for l_ in range(imp_data.shape[0]): + p_total_profile[temp_point] += p_rad_array[l_][temp_point] * 1.0e-6 + + axis.plot( + rho, p_total_profile, label="$P_{\\Sigma\\text{Impurities}}$", linestyle="dotted" + ) + + # Plot individual impurity radiation profiles + axis.plot(rho, p_rad_array[0] * 1.0e-6, label="H") + axis.plot(rho, p_rad_array[1] * 1.0e-6, label="He") + # Only plot impurities with a significant fraction + for ind in range(2, imp_data.shape[0]): + if imp_frac[ind] > 1.0e-30: + axis.plot( + rho, + p_rad_array[ind] * 1.0e-6, + label=ImpurityRadiationData().imp_label[ind].replace("_", ""), + ) + + axis.plot( + rho, + np.cumsum(p_total_profile), + label="$\\Sigma P_{\\Sigma\\text{Impurities}}$", + color="black", + ) + axis.axhline( + y=p_plasma_rad_imps_mw, + color="black", + linestyle="--", + label="$P_{\\text{total}}$", + ) + + axis.minorticks_on() + # Plot a vertical line at the core region radius + core_radius = mfile.get("radius_plasma_core_norm", scan=scan) + + # Plot a vertical line at the core region radius + axis.axvline(x=core_radius, color="black", linestyle="--", linewidth=1.0, alpha=0.7) + # Plot a box in the bottom left with f_{core,reduce} and \rho_{core} + props_core_reduce = {"boxstyle": "round", "facecolor": "khaki", "alpha": 0.8} + axis.text( + 0.05, + 0.02, + rf"$f_{{\text{{core,reduce}}}}$ = {1.0}" + "\n" + rf"$\rho_{{\text{{core}}}}$ = {core_radius:.3f}", + transform=axis.transAxes, + fontsize=8, + verticalalignment="bottom", + bbox=props_core_reduce, + ) + + cumulative_thermal_energy_mj = np.cumsum(p_total_profile) + half_thermal_energy_mj = 0.5 * p_plasma_rad_imps_mw + half_thermal_energy_position = np.interp( + half_thermal_energy_mj, + cumulative_thermal_energy_mj, + np.linspace(0, 1, temp_electron_kev.shape[0]), + ) + axis.axhline( + y=half_thermal_energy_mj, + label="$50\\%\\ P_{\\text{total}}$", + color="tab:green", + linestyle=":", + ) + axis.axvline( + x=half_thermal_energy_position, + color="tab:green", + linestyle=":", + ) + + # Ranges + # --- + axis.set_xlim(0, 1.0) + axis.legend(loc="upper left", bbox_to_anchor=(1.0, 1.0), ncol=1) + axis.set_yscale("log") + axis.yaxis.grid(True, which="both", alpha=0.2) def plot_line_brem_loss_function_profile( @@ -299,50 +444,45 @@ def plot_line_brem_loss_function_profile( mfile.get(f"f_nd_impurity_electrons({i:02d})", scan=scan) for i in range(1, 15) ]) - rho, _, te = profiles_with_pedestal(mfile, scan) + rho, _, temp_electron_kev = profiles_with_pedestal(mfile, scan) + # imp_data Te values are in eV, but te from profiles_with_pedestal is in keV + temp_electron_ev = temp_electron_kev * 1.0e3 # Intailise the radiation profile arrays - lz = np.zeros([imp_data.shape[0], te.shape[0]]) + lz = np.zeros([imp_data.shape[0], temp_electron_kev.shape[0]]) # Intailise the impurity radiation profile - for k in range(te.shape[0]): - for i in range(imp_data.shape[0]): - if te[k] <= imp_data[i][0][0]: - lz[i][k] = imp_data[i][0][1] - elif te[k] >= imp_data[i][imp_data.shape[1] - 1][0]: - lz[i][k] = imp_data[i][imp_data.shape[1] - 1][1] + for temp_point in range(temp_electron_kev.shape[0]): + for impurity in range(imp_data.shape[0]): + if temp_electron_ev[temp_point] <= imp_data[impurity][0][0]: + lz[impurity][temp_point] = imp_data[impurity][0][1] + elif ( + temp_electron_ev[temp_point] + >= imp_data[impurity][imp_data.shape[1] - 1][0] + ): + lz[impurity][temp_point] = imp_data[impurity][imp_data.shape[1] - 1][1] else: # Use np.interp for log-log interpolation - log_te_data = np.log([row[0] for row in imp_data[i]]) - log_lz_data = np.log([row[1] for row in imp_data[i]]) - lz[i][k] = np.exp(np.interp(np.log(te[k]), log_te_data, log_lz_data)) + log_te_data = np.log([row[0] for row in imp_data[impurity]]) + log_lz_data = np.log([row[1] for row in imp_data[impurity]]) + lz[impurity][temp_point] = np.exp( + np.interp( + np.log(temp_electron_ev[temp_point]), log_te_data, log_lz_data + ) + ) + # Plot the radiation loss function profiles axis.plot(rho, lz[0], label="H") axis.plot(rho, lz[1], label="He") - if imp_frac[2] > 1.0e-30: - axis.plot(rho, lz[2], label="Be") - if imp_frac[3] > 1.0e-30: - axis.plot(rho, lz[3], label="C") - if imp_frac[4] > 1.0e-30: - axis.plot(rho, lz[4], label="N") - if imp_frac[5] > 1.0e-30: - axis.plot(rho, lz[5], label="O") - if imp_frac[6] > 1.0e-30: - axis.plot(rho, lz[6], label="Ne") - if imp_frac[7] > 1.0e-30: - axis.plot(rho, lz[7], label="Si") - if imp_frac[8] > 1.0e-30: - axis.plot(rho, lz[8], label="Ar") - if imp_frac[9] > 1.0e-30: - axis.plot(rho, lz[9], label="Fe") - if imp_frac[10] > 1.0e-30: - axis.plot(rho, lz[10], label="Ni") - if imp_frac[11] > 1.0e-30: - axis.plot(rho, lz[11], label="Kr") - if imp_frac[12] > 1.0e-30: - axis.plot(rho, lz[12], label="Xe") - if imp_frac[13] > 1.0e-30: - axis.plot(rho, lz[13], label="W") + # Plot the remaining impurities if their fraction is significant + impurity_data = ImpurityRadiationData() + for ind in range(2, imp_data.shape[0]): + if imp_frac[ind] > 1.0e-30: + axis.plot( + rho, + lz[ind], + label=impurity_data.imp_label[ind].replace("_", ""), + ) axis.legend(loc="best", ncol=4) axis.minorticks_on() diff --git a/process/core/io/plot/summary/profiles/radiation.py b/process/core/io/plot/summary/profiles/radiation.py index 18c99acdf5..e59ec558cb 100644 --- a/process/core/io/plot/summary/profiles/radiation.py +++ b/process/core/io/plot/summary/profiles/radiation.py @@ -29,34 +29,33 @@ from process.core.io.mfile import MFile -def plot_rad_contour(axis: Axes, mfile: MFile, scan: int, impp: str): - """Plots the contour of line and bremsstrahlung radiation density for a plasma - cross-section. +def plot_rad_density_contour(axis: Axes, mfile: MFile, scan: int, impp: str): + """Plots the contour of line and bremsstrahlung radiation density [MW/m³] + for a plasma cross-section. - This function reads impurity and plasma profile data, computes the radiation density - profile, - interpolates it onto a 2D grid, and plots the upper and lower half contours on the - provided axis. + This function reads impurity and plasma profile data, computes the radiation + density profile, interpolates it onto a 2D grid, and plots the upper and lower + half contours on the provided axis. Parameters ---------- - axis : matplotlib.axes.Axes + axis: The matplotlib axis object to plot the contours on. - mfile : Any + mfile: Data object containing plasma and impurity profile information. - scan : int + scan: The scan index to extract profile data for plotting. - impp : str + impp: The impurity data path Notes ----- - The function assumes the existence of several global or previously defined - variables and functions, - such as `read_imprad_data`, `interp1d_profile`, and plasma pedestal parameters. - - The plotted contours represent the radiation density in units of MW.m^-3. + variables and functions, such as `read_imprad_data`, `interp1d_profile`, + and plasma pedestal parameters. + - The plotted contours represent the radiation density in units of [MW/m³] - The function adds colorbar, axis labels, title, and core reduction annotation to - the plot. + the plot. """ rminor = mfile.get("rminor", scan=scan) rmajor = mfile.get("rmajor", scan=scan) @@ -72,6 +71,8 @@ def plot_rad_contour(axis: Axes, mfile: MFile, scan: int, impp: str): # Initialize the radius rho, ne, te = profiles_with_pedestal(mfile, scan) + # imp_data Te values are in eV, but te from profiles_with_pedestal is in keV + te_ev = te * 1.0e3 # Intailise the radiation profile arrays pimpden = np.zeros([imp_data.shape[0], te.shape[0]]) @@ -85,12 +86,12 @@ def plot_rad_contour(axis: Axes, mfile: MFile, scan: int, impp: str): for impurity in range(imp_data.shape[0]): # Check if profile temperature is lower than dataset minimum. # If so, use the minimum loss function value - if te[rho] <= imp_data[impurity][0][0]: + if te_ev[rho] <= imp_data[impurity][0][0]: lz[impurity][rho] = imp_data[impurity][0][1] # Check if profile temperature is higher than dataset maximum. # If so, use the maximum loss function value - elif te[rho] >= imp_data[impurity][imp_data.shape[1] - 1][0]: + elif te_ev[rho] >= imp_data[impurity][imp_data.shape[1] - 1][0]: lz[impurity][rho] = imp_data[impurity][imp_data.shape[1] - 1][1] else: # If profile valie is within dataset range, use log-log interpolation to @@ -98,7 +99,7 @@ def plot_rad_contour(axis: Axes, mfile: MFile, scan: int, impp: str): log_te_data = np.log([row[0] for row in imp_data[impurity]]) log_lz_data = np.log([row[1] for row in imp_data[impurity]]) lz[impurity][rho] = np.exp( - np.interp(np.log(te[rho]), log_te_data, log_lz_data) + np.interp(np.log(te_ev[rho]), log_te_data, log_lz_data) ) # Find the power density for each impurity at each rho pimpden[impurity][rho] = ( @@ -112,10 +113,10 @@ def plot_rad_contour(axis: Axes, mfile: MFile, scan: int, impp: str): # Plot the upper half contour p_rad_upper = axis.contourf( - r_grid, z_grid, p_rad_grid, levels=50, cmap="plasma", zorder=2 + r_grid, z_grid, p_rad_grid, levels=25, cmap="turbo", zorder=2 ) # Plot the lower half contour (mirror) - axis.contourf(r_grid, -z_grid, p_rad_grid, levels=50, cmap="plasma", zorder=2) + axis.contourf(r_grid, -z_grid, p_rad_grid, levels=25, cmap="turbo", zorder=2) axis.figure.colorbar( p_rad_upper, @@ -405,5 +406,5 @@ def plot_cs_radial_stress_contour_profile( "plot_cs_radial_stress_profile", "plot_larmor_radius_profile", "plot_plasma_pressure_gradient_profiles", - "plot_rad_contour", + "plot_rad_density_contour", ] From d5e735c625619a618c63fc8e9904194ee2180bb0 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Fri, 2 Oct 2026 08:52:16 +0100 Subject: [PATCH 17/18] tidy --- process/core/io/plot/summary/api.py | 25 ++--- process/core/io/plot/summary/common.py | 10 +- .../core/io/plot/summary/magnets/cables.py | 13 +-- process/core/io/plot/summary/magnets/cs.py | 19 +--- process/core/io/plot/summary/magnets/pf.py | 14 +-- process/core/io/plot/summary/magnets/tf.py | 102 ++++-------------- .../io/plot/summary/plasma/confinement.py | 13 +-- .../io/plot/summary/plasma/current_drive.py | 12 +-- .../core/io/plot/summary/plasma/overview.py | 80 +++----------- .../core/io/plot/summary/plasma/physics.py | 24 ++--- process/core/io/plot/summary/power_flow.py | 9 +- process/core/io/plot/summary/profiles/misc.py | 4 +- .../core/io/plot/summary/profiles/plasma.py | 49 +++------ .../io/plot/summary/profiles/radiation.py | 12 +-- .../core/io/plot/summary/profiles/stress.py | 5 +- .../io/plot/summary/reporting/constraints.py | 6 +- process/core/io/plot/summary/time_profiles.py | 13 +-- 17 files changed, 93 insertions(+), 317 deletions(-) diff --git a/process/core/io/plot/summary/api.py b/process/core/io/plot/summary/api.py index 29d72d5029..ee8382cdee 100644 --- a/process/core/io/plot/summary/api.py +++ b/process/core/io/plot/summary/api.py @@ -10,13 +10,8 @@ import matplotlib.pyplot as plt from process.core.io.mfile import MFile -from process.core.io.plot.summary.common import ( - color_key, -) -from process.core.io.plot.summary.constants import ( - RADIAL_BUILD, - vertical_lower, -) +from process.core.io.plot.summary.common import color_key +from process.core.io.plot.summary.constants import RADIAL_BUILD, vertical_lower from process.core.io.plot.summary.geometry.build import ( plot_geometry_info, plot_radial_build, @@ -31,9 +26,7 @@ plot_full_machine_poloidal_cross_section, poloidal_cross_section, ) -from process.core.io.plot.summary.geometry.toroidal import ( - toroidal_cross_section, -) +from process.core.io.plot.summary.geometry.toroidal import toroidal_cross_section from process.core.io.plot.summary.magnets.cables import ( plot_cable_in_conduit_cable, plot_hts_tape_geometry, @@ -45,9 +38,7 @@ plot_pf_cs_plasma_mutual_inductance, plot_physics_info, ) -from process.core.io.plot.summary.magnets.pf import ( - plot_pf_dimensions, -) +from process.core.io.plot.summary.magnets.pf import plot_pf_dimensions from process.core.io.plot.summary.magnets.tf import ( plot_corc_cable_geometry, plot_quench_time_evolution, @@ -65,9 +56,7 @@ plot_confinement_time_comparison, plot_sol_power_decay_length_comparison, ) -from process.core.io.plot.summary.plasma.current_drive import ( - plot_bootstrap_comparison, -) +from process.core.io.plot.summary.plasma.current_drive import plot_bootstrap_comparison from process.core.io.plot.summary.plasma.overview import ( plot_detailed_plasma_parameters, plot_main_plasma_information, @@ -134,9 +123,7 @@ plot_equality_constraint_equations, plot_inequality_constraint_equations, ) -from process.core.io.plot.summary.reporting.layouts import ( - plot_upper_vertical_build, -) +from process.core.io.plot.summary.reporting.layouts import plot_upper_vertical_build from process.core.io.plot.summary.reporting.misc import ( RadialBuild, plot_density_limit_comparison, diff --git a/process/core/io/plot/summary/common.py b/process/core/io/plot/summary/common.py index ee503e48ca..2836bcde99 100644 --- a/process/core/io/plot/summary/common.py +++ b/process/core/io/plot/summary/common.py @@ -21,9 +21,7 @@ THERMAL_SHIELD_COLOUR, VESSEL_COLOUR, ) -from process.core.io.plot.summary.rendering import ( - draw_text, -) +from process.core.io.plot.summary.rendering import draw_text from process.models.pulse import PulseTimings if TYPE_CHECKING: @@ -67,11 +65,11 @@ def box_style(colour: str): } -def text_layout(fig): +def text_layout(fig, h_align="left", v_align="bottom"): return { "fontsize": 9, - "verticalalignment": "bottom", - "horizontalalignment": "left", + "verticalalignment": v_align, + "horizontalalignment": h_align, "transform": fig.transFigure, } diff --git a/process/core/io/plot/summary/magnets/cables.py b/process/core/io/plot/summary/magnets/cables.py index 4b89de6ca2..dc76dc68e7 100644 --- a/process/core/io/plot/summary/magnets/cables.py +++ b/process/core/io/plot/summary/magnets/cables.py @@ -9,12 +9,8 @@ from matplotlib import patches from matplotlib.patches import Rectangle -from process.core.io.plot.summary.common import ( - box_style, -) -from process.core.io.plot.summary.rendering import ( - draw_text, -) +from process.core.io.plot.summary.common import box_style, text_layout +from process.core.io.plot.summary.rendering import draw_text if TYPE_CHECKING: from process.core.io.mfile import MFile @@ -93,10 +89,7 @@ def plot_cable_in_conduit_cable(axis: plt.Axes, fig, mfile: MFile, scan: int): 0.4, 0.3, textstr_cable, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=box_style("#cccccc"), ) diff --git a/process/core/io/plot/summary/magnets/cs.py b/process/core/io/plot/summary/magnets/cs.py index d191dd30f5..5b6d2bd918 100644 --- a/process/core/io/plot/summary/magnets/cs.py +++ b/process/core/io/plot/summary/magnets/cs.py @@ -8,21 +8,10 @@ import numpy as np from matplotlib import patches -from process.core.io.plot.summary.common import ( - box_style, - setup_axis, - text_layout, -) -from process.core.io.plot.summary.constants import ( - SOLENOID_COLOUR, -) -from process.core.io.plot.summary.rendering import ( - draw_annotation, - draw_text, -) -from process.core.io.plot.summary.reporting.text import ( - plot_info, -) +from process.core.io.plot.summary.common import box_style, setup_axis, text_layout +from process.core.io.plot.summary.constants import SOLENOID_COLOUR +from process.core.io.plot.summary.rendering import draw_annotation, draw_text +from process.core.io.plot.summary.reporting.text import plot_info from process.data_structure.pfcoil_variables import NFIXMX from process.models.superconductors import SuperconductorModel diff --git a/process/core/io/plot/summary/magnets/pf.py b/process/core/io/plot/summary/magnets/pf.py index bfe6298bbb..dc930ff22b 100644 --- a/process/core/io/plot/summary/magnets/pf.py +++ b/process/core/io/plot/summary/magnets/pf.py @@ -7,17 +7,9 @@ import matplotlib.pyplot as plt from matplotlib import patches -from process.core.io.plot.summary.constants import ( - CSCOMPRESSION_COLOUR, - SOLENOID_COLOUR, -) -from process.core.io.plot.summary.radial_build import ( - cumulative_radial_build2, -) -from process.core.io.plot.summary.rendering import ( - draw_annotation, - draw_text, -) +from process.core.io.plot.summary.constants import CSCOMPRESSION_COLOUR, SOLENOID_COLOUR +from process.core.io.plot.summary.radial_build import cumulative_radial_build2 +from process.core.io.plot.summary.rendering import draw_annotation, draw_text from process.models.geometry.pfcoil import pfcoil_geometry if TYPE_CHECKING: diff --git a/process/core/io/plot/summary/magnets/tf.py b/process/core/io/plot/summary/magnets/tf.py index 7eb339e4fe..15ac09d8a6 100644 --- a/process/core/io/plot/summary/magnets/tf.py +++ b/process/core/io/plot/summary/magnets/tf.py @@ -11,25 +11,16 @@ from matplotlib.patches import Circle, Rectangle from matplotlib.path import Path as mplPath -from process.core.io.plot.summary.common import ( - box_style, -) +from process.core.io.plot.summary.common import box_style, text_layout from process.core.io.plot.summary.constants import ( TFC_COLOUR, THERMAL_SHIELD_COLOUR, rtangle, rtangle2, ) -from process.core.io.plot.summary.magnets.cables import ( - plot_hts_tape_geometry, -) -from process.core.io.plot.summary.rendering import ( - draw_annotation, - draw_text, -) -from process.data_structure.superconducting_tf_coil_variables import ( - TFWPIntegerTurnType, -) +from process.core.io.plot.summary.magnets.cables import plot_hts_tape_geometry +from process.core.io.plot.summary.rendering import draw_annotation, draw_text +from process.data_structure.superconducting_tf_coil_variables import TFWPIntegerTurnType from process.models.geometry.tfcoil import ( tfcoil_geometry_d_shape, tfcoil_geometry_rectangular_shape, @@ -1345,10 +1336,7 @@ def plot_resistive_tf_info(axis: plt.Axes, mfile: MFile, scan: int, fig): 0.775, 0.925, textstr_casing, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=box_style("grey"), ) @@ -1366,10 +1354,7 @@ def plot_resistive_tf_info(axis: plt.Axes, mfile: MFile, scan: int, fig): 0.775, 0.62, textstr_wp_insulation, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox={ "boxstyle": "round", "facecolor": "green", @@ -1426,10 +1411,7 @@ def plot_resistive_tf_info(axis: plt.Axes, mfile: MFile, scan: int, fig): 0.55, 0.475, textstr_general_info, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox={ "boxstyle": "round", "facecolor": "wheat", @@ -1463,10 +1445,7 @@ def plot_resistive_tf_info(axis: plt.Axes, mfile: MFile, scan: int, fig): 0.55, 0.35, textstr_cooling, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=box_style("wheat"), ) @@ -1917,10 +1896,7 @@ def _pack_strands_rectangular_with_obstacles( 0.4, 0.9, textstr_turn_insulation, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=box_style("red"), ) @@ -1935,10 +1911,7 @@ def _pack_strands_rectangular_with_obstacles( 0.65, 0.9, textstr_turn_steel, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=box_style("grey"), ) @@ -1970,10 +1943,7 @@ def _pack_strands_rectangular_with_obstacles( 0.40, 0.7, textstr_turn_cable_space, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=box_style("royalblue"), ) @@ -1996,10 +1966,7 @@ def _pack_strands_rectangular_with_obstacles( 0.525, 0.9, textstr_turn, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=box_style("wheat"), ) @@ -2015,10 +1982,7 @@ def _pack_strands_rectangular_with_obstacles( 0.45, 0.8, textstr_turn_cooling, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=box_style("white"), ) @@ -2056,10 +2020,7 @@ def _pack_strands_rectangular_with_obstacles( 0.75, 0.9, textstr_superconductor, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=box_style("#6dd3f7"), # light blue for superconductors ) @@ -2179,10 +2140,7 @@ def plot_tf_croco_turn(axis: plt.Axes, fig, mfile: MFile, scan: int): 0.4, 0.9, textstr_turn_insulation, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=box_style("red"), ) @@ -2197,10 +2155,7 @@ def plot_tf_croco_turn(axis: plt.Axes, fig, mfile: MFile, scan: int): 0.65, 0.9, textstr_turn_steel, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=box_style("grey"), ) @@ -2232,10 +2187,7 @@ def plot_tf_croco_turn(axis: plt.Axes, fig, mfile: MFile, scan: int): 0.40, 0.7, textstr_turn_cable_space, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=box_style("royalblue"), ) @@ -2258,10 +2210,7 @@ def plot_tf_croco_turn(axis: plt.Axes, fig, mfile: MFile, scan: int): 0.525, 0.9, textstr_turn, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=box_style("wheat"), ) @@ -2277,10 +2226,7 @@ def plot_tf_croco_turn(axis: plt.Axes, fig, mfile: MFile, scan: int): 0.45, 0.8, textstr_turn_cooling, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=box_style("white"), ) @@ -2318,10 +2264,7 @@ def plot_tf_croco_turn(axis: plt.Axes, fig, mfile: MFile, scan: int): 0.75, 0.9, textstr_superconductor, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=box_style("#6dd3f7"), ) @@ -3408,10 +3351,7 @@ def plot_tf_corc_cable_summary_box(axis, fig, mfile: MFile, scan: int): 0.4, 0.4, textstr_cable, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=box_style("#cccccc"), # grayish color ) diff --git a/process/core/io/plot/summary/plasma/confinement.py b/process/core/io/plot/summary/plasma/confinement.py index d7ded750a3..8db97a7c89 100644 --- a/process/core/io/plot/summary/plasma/confinement.py +++ b/process/core/io/plot/summary/plasma/confinement.py @@ -7,17 +7,13 @@ import matplotlib.pyplot as plt import numpy as np -from process.core.io.plot.summary.rendering import ( - draw_text, -) +from process.core.io.plot.summary.rendering import draw_text from process.data_structure.physics_variables import ( ConfinementTimeModel, OutbordSOLPowerDecayLengthModel, ) from process.models.physics.confinement_time import PlasmaConfinementTime -from process.models.physics.exhaust import ( - calculate_brunner_divertor_power_splits, -) +from process.models.physics.exhaust import calculate_brunner_divertor_power_splits if TYPE_CHECKING: from process.core.io.mfile import MFile @@ -177,10 +173,7 @@ def plot_brunner_divertor_power_split_comparison_stackplot( label="$\u0394 r_{\\mathrm{sep}}$", ) axis.set_ylim(0.0, 1.0) - axis.set_xlim( - -5 * len_plasma_sol_outboard_pd, - 5 * len_plasma_sol_outboard_pd, - ) + axis.set_xlim(-5 * len_plasma_sol_outboard_pd, 5 * len_plasma_sol_outboard_pd) axis.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.35) axis.set_title("Brunner Divertor Power Split Fractions") axis.set_xlabel("$\\Delta r_{\\mathrm{sep}}$ [m]") diff --git a/process/core/io/plot/summary/plasma/current_drive.py b/process/core/io/plot/summary/plasma/current_drive.py index 5ce5dbf44e..950e84897d 100644 --- a/process/core/io/plot/summary/plasma/current_drive.py +++ b/process/core/io/plot/summary/plasma/current_drive.py @@ -7,15 +7,9 @@ import matplotlib.pyplot as plt import numpy as np -from process.core.io.plot.summary.common import ( - setup_axis, -) -from process.core.io.plot.summary.rendering import ( - draw_text, -) -from process.core.io.plot.summary.reporting.text import ( - plot_info, -) +from process.core.io.plot.summary.common import setup_axis +from process.core.io.plot.summary.rendering import draw_text +from process.core.io.plot.summary.reporting.text import plot_info if TYPE_CHECKING: from process.core.io.mfile import MFile diff --git a/process/core/io/plot/summary/plasma/overview.py b/process/core/io/plot/summary/plasma/overview.py index 485d3021ca..69ef3a9684 100644 --- a/process/core/io/plot/summary/plasma/overview.py +++ b/process/core/io/plot/summary/plasma/overview.py @@ -7,30 +7,16 @@ import matplotlib.pyplot as plt -from process.core.io.plot.summary.common import ( - box_style, - load_plot_image, -) -from process.core.io.plot.summary.constants import ( - white_box, -) -from process.core.io.plot.summary.plasma.physics import ( - plot_plasma, -) -from process.core.io.plot.summary.rendering import ( - draw_annotation, - draw_text, -) +from process.core.io.plot.summary.common import box_style, load_plot_image, text_layout +from process.core.io.plot.summary.constants import white_box +from process.core.io.plot.summary.plasma.physics import plot_plasma +from process.core.io.plot.summary.rendering import draw_annotation, draw_text from process.data_structure.impurity_radiation_variables import ImpurityRadiationData from process.models.geometry.plasma import plasma_geometry -from process.models.physics.bootstrap_current import ( - BootstrapCurrentFractionModel, -) +from process.models.physics.bootstrap_current import BootstrapCurrentFractionModel from process.models.physics.current_drive import CurrentDriveModel from process.models.physics.density_limit import DensityLimitModel -from process.models.physics.l_h_transition import ( - PlasmaConfinementTransitionModel, -) +from process.models.physics.l_h_transition import PlasmaConfinementTransitionModel from process.models.physics.physics import ( BetaComponentLimits, BetaNormMaxModel, @@ -40,9 +26,7 @@ PlasmaCurrentModel, PlasmaDiamagneticCurrentModel, ) -from process.models.physics.plasma_geometry import ( - PlasmaGeometryModelType, -) +from process.models.physics.plasma_geometry import PlasmaGeometryModelType if TYPE_CHECKING: from matplotlib.transforms import Transform @@ -274,10 +258,7 @@ def plot_main_plasma_information( 0.365, 0.975, textstr_plasma, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=box_style("lightyellow"), ) @@ -959,10 +940,7 @@ def plot_detailed_plasma_parameters(axis: plt.Axes, fig, mfile: MFile, scan: int 0.05, 0.45, textstr_debye, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=light_yellow_box, ) @@ -971,10 +949,7 @@ def plot_detailed_plasma_parameters(axis: plt.Axes, fig, mfile: MFile, scan: int 0.25, 0.45, textstr_larmor, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=light_yellow_box, ) @@ -983,10 +958,7 @@ def plot_detailed_plasma_parameters(axis: plt.Axes, fig, mfile: MFile, scan: int 0.45, 0.45, textstr_velocities, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=light_yellow_box, ) @@ -1002,10 +974,7 @@ def plot_detailed_plasma_parameters(axis: plt.Axes, fig, mfile: MFile, scan: int 0.05, 0.31, textstr_frequencies, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=light_cyan_box, ) @@ -1014,10 +983,7 @@ def plot_detailed_plasma_parameters(axis: plt.Axes, fig, mfile: MFile, scan: int 0.25, 0.31, textstr_coulomb, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=light_cyan_box, ) @@ -1026,10 +992,7 @@ def plot_detailed_plasma_parameters(axis: plt.Axes, fig, mfile: MFile, scan: int 0.45, 0.31, textstr_collision_times, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=light_cyan_box, ) @@ -1045,10 +1008,7 @@ def plot_detailed_plasma_parameters(axis: plt.Axes, fig, mfile: MFile, scan: int 0.05, 0.17, textstr_collision_freq, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=light_green_box, ) @@ -1057,10 +1017,7 @@ def plot_detailed_plasma_parameters(axis: plt.Axes, fig, mfile: MFile, scan: int 0.25, 0.17, textstr_mfp, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=light_green_box, ) @@ -1069,10 +1026,7 @@ def plot_detailed_plasma_parameters(axis: plt.Axes, fig, mfile: MFile, scan: int 0.45, 0.17, textstr_spitzer + "\n" + textstr_resistivity, - fontsize=9, - verticalalignment="top", - horizontalalignment="left", - transform=fig.transFigure, + **text_layout(fig, v_align="top"), bbox=light_green_box, ) diff --git a/process/core/io/plot/summary/plasma/physics.py b/process/core/io/plot/summary/plasma/physics.py index d637c3b980..33c23b872e 100644 --- a/process/core/io/plot/summary/plasma/physics.py +++ b/process/core/io/plot/summary/plasma/physics.py @@ -8,26 +8,14 @@ import matplotlib.pyplot as plt import numpy as np -from process.core.io.plot.summary.common import ( - box_style, -) -from process.core.io.plot.summary.constants import ( - PLASMA_COLOUR, -) -from process.core.io.plot.summary.rendering import ( - draw_text, -) +from process.core.io.plot.summary.common import box_style +from process.core.io.plot.summary.constants import PLASMA_COLOUR +from process.core.io.plot.summary.rendering import draw_text from process.models.build import Build from process.models.geometry.plasma import plasma_geometry -from process.models.physics.physics import ( - BetaNormMaxModel, -) -from process.models.physics.plasma_current import ( - PlasmaCurrentModel, -) -from process.models.physics.plasma_geometry import ( - PlasmaShapeModelType, -) +from process.models.physics.physics import BetaNormMaxModel +from process.models.physics.plasma_current import PlasmaCurrentModel +from process.models.physics.plasma_geometry import PlasmaShapeModelType if TYPE_CHECKING: from process.core.io.mfile import MFile diff --git a/process/core/io/plot/summary/power_flow.py b/process/core/io/plot/summary/power_flow.py index df0b040580..c5223b2040 100644 --- a/process/core/io/plot/summary/power_flow.py +++ b/process/core/io/plot/summary/power_flow.py @@ -10,13 +10,8 @@ setup_axis, text_layout, ) -from process.core.io.plot.summary.rendering import ( - draw_annotation, - draw_text, -) -from process.core.io.plot.summary.reporting.text import ( - plot_info, -) +from process.core.io.plot.summary.rendering import draw_annotation, draw_text +from process.core.io.plot.summary.reporting.text import plot_info if TYPE_CHECKING: import matplotlib.pyplot as plt diff --git a/process/core/io/plot/summary/profiles/misc.py b/process/core/io/plot/summary/profiles/misc.py index 3549933fca..7c374ea616 100644 --- a/process/core/io/plot/summary/profiles/misc.py +++ b/process/core/io/plot/summary/profiles/misc.py @@ -7,9 +7,7 @@ import numpy as np from scipy.interpolate import interp1d -from process.core.io.plot.summary.rendering import ( - draw_text, -) +from process.core.io.plot.summary.rendering import draw_text from process.data_structure.impurity_radiation_variables import ImpurityRadiationData from process.models.geometry.plasma import plasma_geometry from process.models.physics.impurity_radiation import read_impurity_file diff --git a/process/core/io/plot/summary/profiles/plasma.py b/process/core/io/plot/summary/profiles/plasma.py index f1252ece73..c5cff2e4ef 100644 --- a/process/core/io/plot/summary/profiles/plasma.py +++ b/process/core/io/plot/summary/profiles/plasma.py @@ -8,19 +8,11 @@ import numpy as np from process.core import constants -from process.core.io.plot.summary.common import ( - box_style, - text_layout, -) -from process.core.io.plot.summary.plasma.physics import ( - reaction_plot_grid, -) -from process.core.io.plot.summary.profiles.misc import ( - interp1d_profile, -) -from process.core.io.plot.summary.rendering import ( - draw_text, -) +from process.core.io.plot.summary.common import box_style, text_layout +from process.core.io.plot.summary.plasma.physics import reaction_plot_grid +from process.core.io.plot.summary.profiles.misc import interp1d_profile +from process.core.io.plot.summary.rendering import draw_text +from process.data_structure.impurity_radiation_variables import ImpurityRadiationData from process.models.physics.profiles import PlasmaProfileShapeType if TYPE_CHECKING: @@ -173,30 +165,13 @@ def plot_n_profiles(prof, demo_ranges: bool, mfile: MFile, scan: int): linewidth=1.5, ) - if imp_frac[2] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[2] * ne / 1e16, label=r"$n_{\text{Be}}$") - if imp_frac[3] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[3] * ne / 1e16, label=r"$n_{\text{C}}$") - if imp_frac[4] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[4] * ne / 1e16, label=r"$n_{\text{N}}$") - if imp_frac[5] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[5] * ne / 1e16, label=r"$n_{\text{O}}$") - if imp_frac[6] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[6] * ne / 1e16, label=r"$n_{\text{Ne}}$") - if imp_frac[7] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[7] * ne / 1e16, label=r"$n_{\text{Si}}$") - if imp_frac[8] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[8] * ne / 1e16, label=r"$n_{\text{Ar}}$") - if imp_frac[9] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[9] * ne / 1e16, label=r"$n_{\text{Fe}}$") - if imp_frac[10] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[10] * ne / 1e16, label=r"$n_{\text{Ni}}$") - if imp_frac[11] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[11] * ne / 1e16, label=r"$n_{\text{Kr}}$") - if imp_frac[12] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[12] * ne / 1e16, label=r"$n_{\text{Xe}}$") - if imp_frac[13] > 1.0e-30: - ax_impurity.plot(rho, imp_frac[13] * ne / 1e16, label=r"$n_{\text{W}}$") + imp_labels = ImpurityRadiationData().imp_label + for ind in range(2, imp_frac.shape[0]): + lbl = imp_labels[ind].replace("_", "") + if imp_frac[ind] > 1.0e-30: + ax_impurity.plot( + rho, imp_frac[ind] * ne / 1e16, label=rf"$n_{{\text{{{lbl}}}}}$" + ) ax_main.legend(loc="best") ax_impurity.legend(loc="best") diff --git a/process/core/io/plot/summary/profiles/radiation.py b/process/core/io/plot/summary/profiles/radiation.py index e59ec558cb..75a9efe725 100644 --- a/process/core/io/plot/summary/profiles/radiation.py +++ b/process/core/io/plot/summary/profiles/radiation.py @@ -6,20 +6,14 @@ import numpy as np -from process.core.io.plot.summary.common import ( - add_colourbar, -) +from process.core.io.plot.summary.common import add_colourbar from process.core.io.plot.summary.profiles.misc import ( interp1d_profile, profiles_with_pedestal, read_imprad_data, ) -from process.core.io.plot.summary.rendering import ( - draw_text, -) -from process.models.engineering.materials import ( - poisson_steel, -) +from process.core.io.plot.summary.rendering import draw_text +from process.models.engineering.materials import poisson_steel from process.models.pfcoil import N_CS_STRESS_PROFILE_POINTS, CSCoil if TYPE_CHECKING: diff --git a/process/core/io/plot/summary/profiles/stress.py b/process/core/io/plot/summary/profiles/stress.py index 7d7cfbfdde..d54299ecfa 100644 --- a/process/core/io/plot/summary/profiles/stress.py +++ b/process/core/io/plot/summary/profiles/stress.py @@ -6,10 +6,7 @@ import numpy as np -from process.core.io.plot.summary.common import ( - add_colourbar, - get_pulse_timings, -) +from process.core.io.plot.summary.common import add_colourbar, get_pulse_timings from process.models.engineering.materials import ( calculate_tresca_stress, calculate_von_mises_stress, diff --git a/process/core/io/plot/summary/reporting/constraints.py b/process/core/io/plot/summary/reporting/constraints.py index ea2500d461..be5240590a 100644 --- a/process/core/io/plot/summary/reporting/constraints.py +++ b/process/core/io/plot/summary/reporting/constraints.py @@ -6,9 +6,7 @@ import numpy as np -from process.core.io.plot.summary.rendering import ( - draw_text, -) +from process.core.io.plot.summary.rendering import draw_text if TYPE_CHECKING: import matplotlib.pyplot as plt @@ -30,7 +28,6 @@ def plot_equality_constraint_equations(axis: plt.Axes, m_file_data: MFile, scan: """ y_labels = [] y_pos = [] - n_plot = 0 # Build a mapping from itvar index to its name (description) con_names = {} @@ -125,7 +122,6 @@ def plot_inequality_constraint_equations(axis: plt.Axes, m_file: MFile, scan: in """ y_labels = [] y_pos = [] - n_plot = 0 # Build a mapping from itvar index to its name (description) con_names = {} diff --git a/process/core/io/plot/summary/time_profiles.py b/process/core/io/plot/summary/time_profiles.py index f85800346b..8115899492 100644 --- a/process/core/io/plot/summary/time_profiles.py +++ b/process/core/io/plot/summary/time_profiles.py @@ -6,16 +6,9 @@ import numpy as np -from process.core.io.plot.summary.common import ( - box_style, - get_pulse_timings, -) -from process.core.io.plot.summary.magnets.cs import ( - secs_to_hms, -) -from process.core.io.plot.summary.rendering import ( - draw_text, -) +from process.core.io.plot.summary.common import box_style, get_pulse_timings +from process.core.io.plot.summary.magnets.cs import secs_to_hms +from process.core.io.plot.summary.rendering import draw_text if TYPE_CHECKING: import matplotlib.pyplot as plt From 04f3ff0857181d9880667c7f50194ee817cbfc7e Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:49:21 +0100 Subject: [PATCH 18/18] blkt pipes output --- process/core/io/plot/summary/api.py | 21 ++++--- process/core/io/plot/summary/power_flow.py | 64 +++++++++++++++++++++- 2 files changed, 73 insertions(+), 12 deletions(-) diff --git a/process/core/io/plot/summary/api.py b/process/core/io/plot/summary/api.py index ee8382cdee..a46e92853c 100644 --- a/process/core/io/plot/summary/api.py +++ b/process/core/io/plot/summary/api.py @@ -69,6 +69,7 @@ plot_plasma_outboard_toroidal_ripple_map, ) from process.core.io.plot.summary.power_flow import ( + plot_blanket_coolant_properties, plot_main_power_flow, plot_power_info, ) @@ -143,6 +144,7 @@ plot_system_power_profiles_over_time, ) from process.models.physics.plasma_geometry import PlasmaShapeModelType +from process.models.power import PumpingPowerModelTypes from process.models.tfcoil.base import TFConductorModel from process.models.tfcoil.superconducting import SuperconductingTFTurnType @@ -792,17 +794,16 @@ def _add_page(name: str | None = None): ) plot_fw_90_deg_pipe_bend(pages["fw_td_cross_section"].add_subplot(337), m_file, scan) - plot_blkt_pipe_bends(_add_page("blkt_pipe_bends"), m_file, scan) - ax_blanket = pages["blkt_pipe_bends"].add_subplot(122, aspect="equal") + ax_blanket = _add_page("blkt_structure").add_subplot(122, aspect="equal") plot_blkt_structure( - ax_blanket, - pages["blkt_pipe_bends"], - m_file, - scan, - radial_build, - colour_scheme, + ax_blanket, pages["blkt_pipe_bends"], m_file, scan, radial_build, colour_scheme ) - + plot_blkt_pipe_bends(_add_page("blkt_cooling"), m_file, scan) + if ( + m_file.get("i_p_coolant_pumping", scan=scan) + == PumpingPowerModelTypes.CALCULATE_PRESSURE_DROP + ): + plot_blanket_coolant_properties(pages["blkt_cooling"], m_file, scan) plot_main_power_flow( _add_page("main_power_flow").add_subplot(111, aspect="equal"), m_file, @@ -928,8 +929,6 @@ def add_page_footer( color="dimgray", ) - # create main plot - # Increase range when adding new page # run main_plot mfile_obj = MFile(mfile) if mfile else MFile("MFILE.DAT") run_label = ( diff --git a/process/core/io/plot/summary/power_flow.py b/process/core/io/plot/summary/power_flow.py index c5223b2040..39e68b19ed 100644 --- a/process/core/io/plot/summary/power_flow.py +++ b/process/core/io/plot/summary/power_flow.py @@ -1972,4 +1972,66 @@ def plot_power_info(axis: plt.Axes, mfile: MFile, scan: int): plot_info(axis, data, mfile, scan) -__all__ = ["plot_main_power_flow", "plot_power_info"] +def plot_blanket_coolant_properties(fig: plt.Figure, m_file: MFile, scan: int): + """Combined plot of blanket coolant channel structure and properties.""" + for side, x_position in (("inboard", 0.1), ("outboard", 0.5)): + + def get(variable: str): + return m_file.get(variable, scan=scan) + + text = ( + f"$\\mathbf{{{side.capitalize()} \\ blanket:}}$\n \n" + "Radius of blanket channel: " + f"{m_file.get('radius_blkt_channel', scan=scan):.4f} m\n" + "Channel roughness ($\\epsilon$): " + f"{m_file.get('roughness_fw_channel', scan=scan):.4e} m\n\n" + "Radial coolant channel length: " + f"{get(f'len_blkt_{side}_coolant_channel_radial'):.4f} m\n" + "Poloidal coolant channel length: " + f"{get(f'len_blkt_{side}_segment_poloidal'):.4f} m\n" + "Number of radial channels: " + f"{get(f'n_blkt_{side}_module_coolant_sections_radial')}\n" + "Number of poloidal channels: " + f"{get(f'n_blkt_{side}_module_coolant_sections_poloidal')}\n" + "Total length of coolant channel straight sections: " + f"{get(f'len_blkt_{side}_channel_total'):.4f} m\n\n" + "Pressure drop for straight sections: " + f"{get(f'dpres_blkt_{side}_coolant_channel_straight_total'):,.2f} Pa\n" + "Pressure drop for 90° bends: " + f"{get(f'dpres_blkt_{side}_coolant_channel_90_bend'):,.2f} Pa\n" + "Total pressure drop for 90° bends: " + f"{get(f'dpres_blkt_{side}_coolant_channel_90_bends_total'):,.2f} Pa\n" + "Pressure drop for 180° bends: " + f"{get(f'dpres_blkt_{side}_coolant_channel_180_bend'):,.2f} Pa\n" + "Total pressure drop for 180° bends: " + f"{get(f'dpres_blkt_{side}_coolant_channel_180_bends_total'):,.2f} Pa\n" + "Total pressure drop for all bends: " + f"{get(f'dpres_blkt_{side}_bends_total'):,.2f} Pa\n\n" + "Reynolds number ($Re$): " + f"{get(f'reynolds_blkt_{side}_coolant'):,.4f}\n" + "Darcy Friction factor ($f$): " + f"{get(f'darcy_frict_blkt_{side}_coolant'):.4f}\n\n" + "Friction drop coefficient for straight sections: " + f"{get(f'f_straight_blkt_{side}_coolant'):.4f}\n" + "Friction drop coefficient for 90° bends: " + f"{get(f'f_elbow_blkt_{side}_90_bend'):.4f}\n" + "Friction drop coefficient for 180° bends: " + f"{get(f'f_elbow_blkt_{side}_180_bend'):.4f}\n\n" + "Total coolant mass flow rate: " + f"{get(f'mflow_blkt_{side}_coolant'):.4f} kg/s\n" + "Coolant mass flow rate in single channel: " + f"{get(f'mflow_blkt_{side}_coolant_channel'):.4f} kg/s\n" + "Coolant velocity in single channel: " + f"{get(f'vel_blkt_{side}_coolant'):.4f} m/s" + ) + + fig.text( + x_position, + 0.5, + text, + **text_layout(fig, v_align="top"), + bbox=box_style("wheat"), + ) + + +__all__ = ["plot_blanket_coolant_properties", "plot_main_power_flow", "plot_power_info"]