Source code for torx.equilibrium.eqdsk_m

"""Contains functionality to export equilibria to eqdsk format and files."""
import numpy as np
from pathlib import Path
from scipy.interpolate import interp1d
import scipy.integrate as sint

from torx import Quantity
from torx.autodoc_decorators_m import autodoc_function
from .numerical_m import NumericalEquilibrium
from .eqdsk_io_m import write_eqdsk_file
from torx.geometry.polygon_2d_m import Polygon2D

[docs] @autodoc_function def export_to_eqdsk(equi: NumericalEquilibrium, filepath: Path, dims: tuple=(129, 129), name: str="TorX", shot: str="00000", time: str="0.0", comment: str="TorX", dry: bool=False): """ Export the equilibrium to eqdsk format. Calculates estimations for poloidal current function and pressure based on simple formulas without sophisticated constraints. Maps the poloidal flux on an RZ matrix with bounds given by the limiter polygon and number of points provided by dims. Creates a safety factor profile on a uniform psi grid via field line tracing (may take some time). Returns the dictionary that was written to file. Can specify dry mode True to only get the content dictionary without writing the file. Note ---- For future improvements of the physics output, one might consider using the actual poloidal current for F (Freidberg, Ideal MHD - Eq. (6.13)) and provide a boundary condition for the pressure at the separatrix. """ assert isinstance(equi, NumericalEquilibrium), \ f"Equi must be of type numerical (was {type(equi)})!" # NOTE: This routine uses mixed normalization, i.e. lengths and fields # are normalized while poloidal fluxes are in Wb. At the end # conversion to SI is performed. meter = equi.R0.to("m").m tesla = equi.B0.to("T").m mu0 = Quantity(1, "mu0").to_base_units().m nw = dims[0] nh = dims[1] rmaxis = equi.axis_r_norm.values zmaxis = equi.axis_z_norm.values rcentr = rmaxis bcentr = equi.magfield_absolute(equi.axis_r_norm, equi.axis_z_norm).values simag = equi.psi_limits.psi_axis sibry = equi.psi_limits.psi_separatrix boundary = equi.get_boundary_polygon() limiter = equi.get_limiter_polygon(npoints=boundary.x_points.size) rmax = np.max(limiter.x_points) # Use limiter polygon as "computational box" rdim = np.abs(np.max(limiter.x_points) - np.min(limiter.x_points)) zdim = np.abs(np.max(limiter.y_points) - np.min(limiter.y_points)) rleft = np.min(limiter.x_points) zmid = 0.5 * zdim # Create RZ grid and calculate flux R = np.linspace(rleft, rleft + rdim, nw) Z = np.linspace(zmid - 0.5 * zdim, zmid + 0.5 * zdim, nh) psirz = equi.psi_interpolator(x=R, y=Z, dx=0, dy=0, grid=True) # Create flux grid and calculate quantities R_interp = np.linspace(rcentr, rmax, nw) psi_interp = equi.psi_interpolator(x=R_interp, y=equi.axis_z_norm, \ dx=0, dy=0, grid=False) psi_u = np.linspace(simag, sibry, nw, endpoint=False) R_u = interp1d(psi_interp, R_interp, kind="cubic", fill_value="extrapolate")(psi_u) # NOTE: Avoid bad behavior when tracing close to axis by adding a # small number to the first R value R_u[0] = R_u[0] + 1e-3 r_u = R_u - equi.axis_r_norm.values Z_u = np.ones_like(R_u) * equi.axis_z_norm.values # NOTE: We have to convert to SI here since we add terms of different # normalization. lhs = equi.gradshaf_star(R_u, zmaxis) / meter**2 fpol = equi.magfield_component_toroidal(R_u, Z_u).values * R_u \ * tesla * meter ffprim = fpol * np.gradient(fpol, psi_u) # Freidberg, Ideal MHD - Eq. (6.15) inverted pprime = -(lhs + ffprim) / R_u**2 / meter**2 / mu0 # NOTE: We have no information about the pressure in the equi and the # result from integrating pprime is determined up to a constant. # We shift the profile such that there is no negative pressure at # the outermost radial location for now. pres = sint.cumulative_trapezoid(pprime, psi_u, initial=0) pres = pres + np.abs(np.min(pres)) # Trace field lines for each psi to get the safety factor. Also get # the trace result for further processing qpsi, ode_sol = equi.safety_factor(R_u, Z_u, return_sol=True) # Calculate flux surface area element and total current theta = np.linspace(0.0, 2.0 * np.pi, nw, endpoint=False) fs_area = np.array([Polygon2D(sol(theta)[0,:], sol(theta)[1,:]).signed_area() \ for sol in ode_sol]) # Freidberg, Ideal MHD - Eq. (6.16) current = -sint.trapezoid(lhs / R_u / meter, fs_area * meter**2) / mu0 # Boundary and limiter nbbbs = boundary.x_points.size rbbbs = boundary.x_points * meter zbbbs = boundary.y_points * meter limitr = limiter.x_points.size rlim = limiter.x_points * meter zlim = limiter.y_points * meter # Converted lengths and magfields to SI rmaxis = rmaxis * meter zmaxis = zmaxis * meter rcentr = rcentr * meter rdim = rdim * meter zdim = zdim * meter rleft = rleft * meter zmid = zmid * meter bcentr = bcentr * tesla content = dict(name=name, shot=shot, time=time, comment=comment, nw=nw, nh=nh, rdim=rdim, zdim=zdim, rcentr=rcentr, rleft=rleft, zmid=zmid, rmaxis=rmaxis, zmaxis=zmaxis, simag=simag, sibry=sibry, bcentr=bcentr, current=current, fpol=fpol, pres=pres, ffprim=ffprim, pprime=pprime, psirz=psirz, qpsi=qpsi, nbbbs=nbbbs, limitr=limitr, rbbbs=rbbbs, zbbbs=zbbbs, rlim=rlim, zlim=zlim) if not dry: write_eqdsk_file(filepath, content) return content