Source code for torx.equilibrium.salpha_m

"""Contains s-alpha equilibrium."""
import numpy as np
import xarray as xr
from numba import njit, cfunc
from numbalsoda import lsoda_sig

from .equilibrium_m import EquilibriumBaseClass
from torx.units import Quantity
from torx.arrays import make_xarray
from torx.units import check_units
from torx.decorators import autogrid_method
from torx.decorators import autodoc_class

[docs] @autodoc_class class SalphaEquilibrium(EquilibriumBaseClass): """ Represents a Salpha equilibrium as defined in PARALLAX. Similar to CircularToroidalEquilibrium but mostly used for legacy access to such simulations. Please consider using CircularToroidalEquilibrium instead. """
[docs] def __init__( self, rhomin: float=0.1, rhomax: float=0.7, minor_r: float=0.365, b_ref: float=1.0, l_ref: float=1.0, q_ref: float=0.854, shear: float=2.184, theta_limiter: float=3.1415, dtheta_limiter: float=0.4, rho_limiter: float=0.3235, B0: Quantity=Quantity("1T"), L_ref: float=1.0, ): """ Initialize the SalphaEquilibrium. Arguments to the initializer match the parameters supplied in PARALLAX. You can either manually specify the arguments, or pass a **kwargs_dictionary generated from a parameter file. The B0 parameter sets the value of the toroidal magnetic field at the normalized axis location (x0, y0). The L_ref parameter sets the length normalization in meters. By definition, the magnetic axis is located at R = L_ref meters. """ self.B0 = B0 self._norm_length = Quantity(L_ref, "m") # Box-fraction boxfac = 1.25 self.x0 = 1.0 self.y0 = 0.0 self.minor_r = minor_r self.rhomin = rhomin self.rhomax = rhomax self.xmin = 1.0 - minor_r * boxfac * rhomax self.xmax = 1.0 + minor_r * boxfac * rhomax self.ymin = -minor_r * boxfac * rhomax self.ymax = minor_r * boxfac * rhomax self.L_ref = l_ref self.B_ref = b_ref self.q_ref = q_ref self.shear = shear self.theta_limiter = theta_limiter self.dtheta_limiter = dtheta_limiter self.rho_limiter = rho_limiter # compute position of limiter plates self.thlim1 = theta_limiter + dtheta_limiter self.thlim1 = np.mod(self.thlim1, 2.0 * np.pi) self.thlim2 = theta_limiter - dtheta_limiter self.thlim2 = np.mod(self.thlim2, 2.0 * np.pi) # if no limiter set, set rho_limiter large if dtheta_limiter <= 0.0: self.rho_limiter = 10.0 * rhomax assert(dtheta_limiter < np.pi), \ "Error: dtheta_limiter cannot be larger than pi"
@property def axis_r_norm(self): """Magnetic axis radial position in normalized units.""" return make_xarray(self.x0) @property def axis_z_norm(self): """Magnetic axis vertical position in normalized units.""" return make_xarray(self.y0) @property def B0(self) -> Quantity: """Magnetic field normalization.""" return self._norm_magfield @B0.setter def B0(self, value: Quantity): """Set the magnetic field normalization.""" check_units(value, {"[mass]":1, "[time]":-2, "[current]":-1}, "B0") self._norm_magfield = value @property def R0(self) -> Quantity: """Length normalization.""" return self._norm_length @R0.setter def R0(self, value: Quantity): """Set the length normalization.""" check_units(value, {"[length]":1}, "R0") self._norm_length = value
[docs] @autogrid_method def normalized_flux_surface_label(self, r_norm, z_norm, **kwargs) \ -> xr.DataArray: """Return the normalized flux surface label (rho_pol).""" r_norm = self.convert_length_to_normalized(r_norm) z_norm = self.convert_length_to_normalized(z_norm) if kwargs["is_structured"]: r_mesh, z_mesh = np.meshgrid(r_norm, z_norm) else: r_mesh = r_norm z_mesh = z_norm rho = type(self)._rho_base( np.asarray(r_mesh), np.asarray(z_mesh), self.minor_r ) return xr.DataArray(rho, attrs={"norm":Quantity(1.0)}, dims=kwargs["dims"], coords=kwargs["coords"])
[docs] @autogrid_method def magfield_component_r(self, r_norm, z_norm, **kwargs) -> xr.DataArray: """Return the radial magnetic field component.""" r_norm = self.convert_length_to_normalized(r_norm) z_norm = self.convert_length_to_normalized(z_norm) if kwargs["is_structured"]: r_mesh, z_mesh = np.meshgrid(r_norm, z_norm) else: r_mesh = r_norm z_mesh = z_norm Br = type(self)._br_base(np.asarray(r_mesh), np.asarray(z_mesh), self.q_ref, self.shear, self.minor_r) return xr.DataArray(Br, attrs={"norm":self.B0}, dims=kwargs["dims"], coords=kwargs["coords"])
[docs] @autogrid_method def magfield_component_z(self, r_norm, z_norm, **kwargs) -> xr.DataArray: """Return the vertical magnetic field component.""" r_norm = self.convert_length_to_normalized(r_norm) z_norm = self.convert_length_to_normalized(z_norm) if kwargs["is_structured"]: r_mesh, z_mesh = np.meshgrid(r_norm, z_norm) else: r_mesh = r_norm z_mesh = z_norm Bz = type(self)._bz_base(np.asarray(r_mesh), np.asarray(z_mesh), self.q_ref, self.shear, self.minor_r) return xr.DataArray(Bz, attrs={"norm":self.B0}, dims=kwargs["dims"], coords=kwargs["coords"])
[docs] @autogrid_method def magfield_component_toroidal(self, r_norm, z_norm, **kwargs) \ -> xr.DataArray: """Return the toroidal magnetic field component.""" r_norm = self.convert_length_to_normalized(r_norm) z_norm = self.convert_length_to_normalized(z_norm) if kwargs["is_structured"]: r_mesh, _ = np.meshgrid(r_norm, z_norm) else: r_mesh = r_norm btor = type(self)._bt_base(np.asarray(r_mesh)) return xr.DataArray(btor, attrs={"norm":self.B0}, dims=kwargs["dims"], coords=kwargs["coords"])
[docs] @autogrid_method def jacobian(self, r_norm, z_norm, **kwargs) -> xr.DataArray: """Return the Jacobian of the s-alpha geometry.""" r_norm = self.convert_length_to_normalized(r_norm) z_norm = self.convert_length_to_normalized(z_norm) if kwargs["is_structured"]: r_mesh, _ = np.meshgrid(r_norm, z_norm) else: r_mesh = r_norm jac = type(self)._jac_base(np.asarray(r_mesh)) return xr.DataArray(jac, attrs={"norm":self.R0}, dims=kwargs["dims"], coords=kwargs["coords"])
[docs] @autogrid_method def theta(self, r_norm, z_norm, **kwargs) -> xr.DataArray: """Return the geometric poloidal angle in the range [0, 2*pi].""" r_norm = self.convert_length_to_normalized(r_norm) z_norm = self.convert_length_to_normalized(z_norm) if kwargs["is_structured"]: r_mesh, z_mesh = np.meshgrid(r_norm, z_norm) else: r_mesh = r_norm z_mesh = z_norm thval = type(self)._theta_base(np.asarray(r_mesh), np.asarray(z_mesh)) return xr.DataArray(thval, attrs={"norm":Quantity(1.0)}, dims=kwargs["dims"], coords=kwargs["coords"])
[docs] @autogrid_method def qval(self, r_norm, z_norm, **kwargs): """Return the safety factor.""" r_norm = self.convert_length_to_normalized(r_norm) z_norm = self.convert_length_to_normalized(z_norm) if kwargs["is_structured"]: r_mesh, z_mesh = np.meshgrid(r_norm, z_norm) else: r_mesh = r_norm z_mesh = z_norm qval = type(self)._qval_base(np.asarray(r_mesh), np.asarray(z_mesh), self.q_ref, self.shear, self.minor_r) return xr.DataArray(qval, attrs={"norm":Quantity(1.0)}, dims=kwargs["dims"], coords=kwargs["coords"])
[docs] def theta_centered(self, theta, limiter_edge): """Return the signed poloidal distance to limiter edge.""" theta_centered = np.mod(theta - limiter_edge, 2.0 * np.pi) if theta_centered.ndim > 0: LL = np.where(theta_centered > np.pi - self.dtheta_limiter) theta_centered[LL] -= 2.0 * np.pi elif theta_centered > np.pi - self.dtheta_limiter: theta_centered -= 2.0 * np.pi return theta_centered
def _magfield_scalars(self, r: float, z: float, phi: float) -> tuple: """Return (b_r, b_z, b_t, jacobian) as plain floats for ODE hot path.""" rho = np.sqrt((r - 1.0)**2 + z**2) / self.minor_r theta = np.arctan2(z, r - 1.0) % (2.0 * np.pi) q = self.q_ref + self.shear * rho**2 b_r = -(rho * self.minor_r / r) * np.sin(theta) / q b_z = (rho * self.minor_r / r) * np.cos(theta) / q return b_r, b_z, 1.0 / r, r
[docs] def get_boundary_polygon(self, angular_resolution=100, rhomax_factor=1.1): """ Return a polygon which approximates the position of the limiter. Can be used for plotting and to control fieldline tracing. """ from torx.geometry import Polygon2D theta_points = np.linspace(0, 2 * np.pi, num=angular_resolution) rho_points = np.ones_like(theta_points) * self.rhomax * rhomax_factor in_limiter = np.logical_and( self.theta_centered(theta_points, self.thlim1) < 0.0, self.theta_centered(theta_points, self.thlim2) > 0.0, ) rho_points[in_limiter] = min(self.rho_limiter, \ self.rhomax * rhomax_factor) x_points = rho_points * np.cos(theta_points) y_points = rho_points * np.sin(theta_points) return Polygon2D(x_points=x_points, y_points=y_points)
@staticmethod @njit def _rho_base(r, z, minor_r): return np.sqrt((r - 1.0)**2 + z**2) / minor_r @staticmethod @njit def _theta_base(r, z): th = np.arctan2(z, r - 1.0) return np.mod(th, 2.0 * np.pi) @staticmethod @njit def _qval_base(r, z, qref, shear, minor_r): rho = np.sqrt((r - 1.0)**2 + z**2) / minor_r return qref + shear * rho**2 @staticmethod @njit def _br_base(r, z, qref, shear, minor_r): rho = np.sqrt((r - 1.0)**2 + z**2) / minor_r theta = np.mod(np.arctan2(z, r - 1.0), 2.0 * np.pi) qval = qref + shear * rho**2 return -(rho * minor_r / r) * np.sin(theta) / qval @staticmethod @njit def _bz_base(r, z, qref, shear, minor_r): rho = np.sqrt((r - 1.0)**2 + z**2) / minor_r theta = np.mod(np.arctan2(z, r - 1.0), 2.0 * np.pi) qval = qref + shear * rho**2 return (rho * minor_r / r) * np.cos(theta) / qval @staticmethod @njit def _bt_base(r): return 1.0 / r @staticmethod @njit def _jac_base(r): return r
[docs] def make_trace_eq(self): """Return cfunc address for numbalsoda field-line tracing.""" minor_r = self.minor_r qref = self.q_ref shear = self.shear @cfunc(lsoda_sig) def trace_eq(t, y, du, p): # pragma: no cover r_norm = y[0] z_norm = y[1] rho = np.sqrt((r_norm - 1.0)**2 + z_norm**2) / minor_r theta = np.mod(np.arctan2(z_norm, r_norm - 1.0), 2.0 * np.pi) qval = qref + shear * rho**2 factor = rho * minor_r * r_norm / qval du[0] = -factor * np.sin(theta) du[1] = factor * np.cos(theta) du[2] = np.sqrt(du[0] * du[0] + du[1] * du[1] + r_norm * r_norm) return trace_eq.address