"""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