"""Mapped domains (single patch).
Module providing mapping classes for single-patch geometries used by Struphy.
This module includes classes such as `Tokamak`, `GVECunit`, `DESCunit`,
`IGAPolarCylinder`, `IGAPolarTorus`, and `Cuboid`. Mappings transform
reference coordinates to Cartesian coordinates and integrate with spline-based
grid constructions and field-line tracing.
"""
import copy
import cunumpy as xp
from struphy.fields_background.base import AxisymmMHDequilibrium
from struphy.fields_background.equils import EQDSKequilibrium
from struphy.geometry.base import (
Domain,
PoloidalSplineStraight,
PoloidalSplineTorus,
Spline,
interp_mapping,
)
from struphy.geometry.utilities import field_line_tracing
[docs]
class Tokamak(PoloidalSplineTorus):
r"""Mappings for Tokamak MHD equilibria constructed via :ref:`field-line tracing <field_tracing>` of a poloidal flux function :math:`\psi`.
.. image:: ../../pics/mappings/tokamak.png
Parameters
----------
equilibrium : struphy.fields_background.base.AxisymmMHDequilibrium
The axisymmetric MHD equilibrium for which a flux-aligned grid shall be constructed (default: AdhocTorus).
num_elements : tuple[int]
Number of cells in (radial, angular) direction to be used in spline mapping (default: [8, 32]).
degree : tuple[int]
Spline degrees in (radial, angular) direction to be used in spline mapping (default: [2, 3]).
psi_power : float
Parametrization of radial flux coordinate :math:`\eta_1=\psi_{\mathrm{norm}}^p`, where :math:`\psi_{\mathrm{norm}}` is the normalized poloidal flux (default: 0.75).
psi_shifts : tuple[float]
Start and end shifts of polidal flux in % --> cuts away regions at the axis and edge (default: [2., 2.])
r_min : float
Inner radius of poloidal section (optional, default: 0.0). If >0.0, then r_0 = r_min.
xi_param : str
Parametrization of angular coordinate ("equal_angle", "equal_arc_length" or "sfl" (straight field line), default: "equal_angle").
r0 : float
Initial guess for radial distance from axis used in Newton root-finding method (default: 0.3).
num_elements_pre : tuple[int]
Number of cells in (radial, angular) direction of pre-mapping needed for equal_arc_length and sfl parametrizations (default: [64, 256]).
p_pre : tuple[int]
Spline degrees in (radial, angular) direction of pre-mapping needed for equal_arc_length and sfl parametrizations (default: [3, 3]).
tor_period : int
Toroidal periodicity built into the mapping: :math:`\phi=2\pi\,\eta_3/\mathrm{torperiod}` (default: 1 --> full torus).
Note
----
Regarding r_min and psi_shifts:
If r_min is left at 0.0, psi_shifts defines both the inner and outer boundaries of the computational
domain in terms of the normalized flux coordinate \psi.
When r_min > 0.0, however, psi_shifts[0] is no longer used. Instead, the code computes the flux value
corresponding to the physical radius r_min (measured from the magnetic axis),
which then defines the inner boundary of the domain.
This allows the user to specify the inner boundary using a more intuitive physical radius rather
than a flux coordinate. The outer boundary is still controlled by psi_shifts[1].
In the parameter .yml, use the following in the section `geometry`::
geometry :
type : Tokamak
Tokamak :
num_elements : [8, 32] # number of poloidal grid cells for spline mapping, >degree
degree : [3, 3] # poloidal spline degrees for spline mapping, >1
psi_power : 0.7 # parametrization of radial flux coordinate eta1=psi_norm^psi_power, where psi_norm is normalized flux
psi_shifts : [2., 2.] # start and end shifts of polidal flux in % --> cuts away regions at the axis and edge
r_min : 0.0 # Inner radius of poloidal section. If >0.0, then r_0 = r_min.
xi_param : equal_angle # parametrization of angular coordinate (equal_angle, equal_arc_length or sfl (straight field line))
r0 : 0.3 # initial guess for radial distance from axis used in Newton root-finding method for flux surfaces
num_elements_pre : [64, 256] # number of poloidal grid cells of pre-mapping needed for equal_arc_length and sfl
p_pre : [3, 3] # poloidal spline degrees of pre-mapping needed for equal_arc_length and sfl
tor_period : 1 # toroidal periodicity built into the mapping: phi = 2*pi * eta3 / tor_period
"""
def __init__(
self,
equilibrium: AxisymmMHDequilibrium = None,
num_elements: tuple = (8, 32),
degree: tuple = (2, 3),
psi_power: float = 0.75,
psi_shifts: tuple = (0.01, 2.0),
r_min: float = 0.0,
xi_param: str = "equal_angle",
r0: float = 0.3,
num_elements_pre: tuple = (64, 256),
p_pre: tuple = (3, 3),
tor_period: int = 1,
):
if r_min != 0.0:
r0 = r_min
if equilibrium is None:
equilibrium = EQDSKequilibrium()
else:
assert isinstance(equilibrium, AxisymmMHDequilibrium)
# use the params setter
self.params = copy.deepcopy(locals())
# get control points via field tracing between fluxes [psi_s, psi_e]
psi0, psi1 = equilibrium.psi_range[0], equilibrium.psi_range[1]
assert r_min >= 0.0, f"Inner radius must be non-negative, got {r_min = }."
if r_min == 0.0:
# Default behaviour: keep exactly the historical psi_shifts logic.
psi_s = psi0 + psi_shifts[0] * 0.01 * (psi1 - psi0)
else:
# Annular domain: eta1=0 is the flux surface crossing the outboard
# midplane at distance r_min from the magnetic axis.
psi_s = equilibrium.psi(
equilibrium.psi_axis_RZ[0] + r_min,
equilibrium.psi_axis_RZ[1],
)
psi_e = psi1 - psi_shifts[1] * 0.01 * (psi1 - psi0)
assert (psi_s - psi0) * (psi_s - psi1) <= 0.0, (
f"Inner radius gives a flux outside equilibrium.psi_range: "
f"{r_min = }, {psi_s = }, {equilibrium.psi_range = }."
)
assert (psi_e - psi_s) * (psi1 - psi0) > 0.0, (
f"Invalid radial interval: {psi_s = }, {psi_e = }, {equilibrium.psi_range = }."
)
cx, cy = field_line_tracing(
equilibrium.psi,
equilibrium.psi_axis_RZ[0],
equilibrium.psi_axis_RZ[1],
psi_s,
psi_e,
num_elements,
degree,
psi_power=psi_power,
xi_param=xi_param,
num_elements_pre=num_elements_pre,
p_pre=p_pre,
r0=r0,
)
# init base class
super().__init__(
num_elements=num_elements,
degree=degree,
spl_kind=(False, True),
cx=cx,
cy=cy,
tor_period=tor_period,
)
[docs]
class GVECunit(Spline):
"""The mapping from `pygvec <https://gvec.readthedocs.io/latest/index.html>`_, computed by the GVEC MHD equilibrium code.
.. image:: ../../pics/mappings/gvec.png
Parameters
----------
gvec_equil : struphy.fields_background.equils.GVECequilibrium
GVEC MHD equilibrium object.
Note
----
In the parameter .yml, use the following in the section `geometry`::
geometry :
type : GVECunit
"""
def __init__(self, gvec_equil=None):
import gvec
from struphy.fields_background.equils import GVECequilibrium
if gvec_equil is None:
gvec_equil = GVECequilibrium()
else:
assert isinstance(gvec_equil, GVECequilibrium)
# do not set params here because of a pickling error
num_elements = gvec_equil.params["num_elements"]
degree = gvec_equil.params["degree"]
if gvec_equil.params["use_nfp"]:
spl_kind = (False, True, False)
else:
spl_kind = (False, True, True)
# project mapping to splines
_rmin = gvec_equil.params["rmin"]
def XYZ(e1, e2, e3):
rho = _rmin + e1 * (1.0 - _rmin)
theta = 2 * xp.pi * e2
zeta = 2 * xp.pi * e3 / gvec_equil._nfp
if gvec_equil.params["use_boozer"]:
ev = gvec.EvaluationsBoozer(rho=rho, theta_B=theta, zeta_B=zeta, state=gvec_equil.state)
else:
ev = gvec.Evaluations(rho=rho, theta=theta, zeta=zeta, state=gvec_equil.state)
gvec_equil.state.compute(ev, "pos")
x = ev.pos.data[0]
y = ev.pos.data[1]
z = ev.pos.data[2]
return x, y, z
def X(e1, e2, e3):
return XYZ(e1, e2, e3)[0]
def Y(e1, e2, e3):
return XYZ(e1, e2, e3)[1]
def Z(e1, e2, e3):
return XYZ(e1, e2, e3)[2]
cx, cy, cz = interp_mapping(num_elements, degree, spl_kind, X, Y, Z)
super().__init__(num_elements=num_elements, degree=degree, spl_kind=spl_kind, cx=cx, cy=cy, cz=cz)
[docs]
class DESCunit(Spline):
r"""The mapping :math:`(\rho, \theta,\zeta) \mapsto (X, Y, Z)` to Cartesian coordinates computed by the `DESC MHD equilibrium code
<https://desc-docs.readthedocs.io/en/latest/theory_general.html#flux-coordinates>`_.
.. image:: ../../pics/mappings/desc.png
Parameters
----------
desc_equil : struphy.fields_background.equils.DESCequilibrium
DESC MHD equilibrium object.
Note
----
In the parameter .yml file, use the following::
geometry :
type : DESCunit
"""
def __init__(self, desc_equil=None):
from struphy.fields_background.equils import DESCequilibrium
if desc_equil is None:
desc_equil = DESCequilibrium()
else:
assert isinstance(desc_equil, DESCequilibrium)
num_elements = desc_equil.params["num_elements"]
degree = desc_equil.params["degree"]
if desc_equil.eq.NFP > 1 and desc_equil.use_nfp:
spl_kind = (False, True, False)
else:
spl_kind = (False, True, True)
_rmin = desc_equil.params["rmin"]
nfp = desc_equil.eq.NFP
if not desc_equil.use_nfp:
nfp = 1
# project mapping to splines
def X(e1, e2, e3):
return desc_equil.desc_eval("X", e1, e2, e3, nfp=nfp)
def Y(e1, e2, e3):
return desc_equil.desc_eval("Y", e1, e2, e3, nfp=nfp)
def Z(e1, e2, e3):
return desc_equil.desc_eval("Z", e1, e2, e3, nfp=nfp)
cx, cy, cz = interp_mapping(num_elements, degree, spl_kind, X, Y, Z)
super().__init__(num_elements=num_elements, degree=degree, spl_kind=spl_kind, cx=cx, cy=cy, cz=cz)
[docs]
class IGAPolarCylinder(PoloidalSplineStraight):
r"""A cylinder with the cross section approximated by a spline mapping.
.. math::
F: \begin{bmatrix}\eta_1\\ \eta_2\\ \eta_3\end{bmatrix}\mapsto \begin{bmatrix}
\,\,x= &\sum_{ij} c^x_{ij} N_i(\eta_1) N_j(\eta_2)\approx a\,\eta_1\cos(2\pi\eta_2)\,\,\\
\,\,y= &\sum_{ij} c^y_{ij} N_i(\eta_1) N_j(\eta_2)\approx a\,\eta_1\sin(2\pi\eta_2)\,\,\\
\,\,z= &L_z\eta_3\,\,\end{bmatrix}
.. image:: ../../pics/mappings/iga_cylinder.png
Parameters
----------
num_elements : list[int]
Number of cells in (radial, angular) direction used for spline mapping (default: [8, 24]).
degree : list[int]
Splines degrees in (radial, angular) direction used for spline mapping (default: [2, 3]).
a : float
Radius of cylinder (default: 1.).
Lz : float
Length of cylinder (default: 4.).
Note
----
In the parameter .yml, use the following in the section `geometry`::
geometry :
type : IGAPolarCylinder
IGAPolarCylinder :
num_elements : [8, 24] # number of poloidal grid cells, >degree
degree : [3, 3] # poloidal spline degree, >1
Lz : 6. # Length in third direction
a : 1. # minor radius
"""
def __init__(
self,
num_elements: tuple[int] = (8, 24),
degree: tuple[int] = (2, 3),
a: float = 1.0,
Lz: float = 4.0,
):
# use params setter
self.params = copy.deepcopy(locals())
# get control points
def X(eta1, eta2):
return a * eta1 * xp.cos(2 * xp.pi * eta2)
def Y(eta1, eta2):
return a * eta1 * xp.sin(2 * xp.pi * eta2)
spl_kind = (False, True)
cx, cy = interp_mapping(num_elements, degree, spl_kind, X, Y)
# make sure that control points at pole are all the same (eta1=0 there)
cx[0] = 0.0
cy[0] = 0.0
# init base class
super().__init__(num_elements=num_elements, degree=degree, spl_kind=spl_kind, cx=cx, cy=cy, Lz=Lz)
[docs]
class IGAPolarTorus(PoloidalSplineTorus):
r""" A torus with the poloidal cross-section approximated by a spline mapping.
.. math::
F: \begin{bmatrix}\eta_1\\ \eta_2\\ \eta_3\end{bmatrix}\mapsto \begin{bmatrix}
\,\,x= &\sum_{ij} c^{R}_{ij} N_i(\eta_1) N_j(\eta_2) \cos(\phantom{-}2\pi\eta_3) \approx \left[a\,\eta_1\cos(2\pi\theta(\eta_1, \eta_2)) + R_0\right]\cos(\phantom{-}2\pi\eta_3)\,\,\\
\,\,y= &\sum_{ij} c^{R}_{ij} N_i(\eta_1) N_j(\eta_2) \sin(-2\pi\eta_3)\approx \left[a\,\eta_1\cos(2\pi\theta(\eta_1, \eta_2)) + R_0\right]\sin(-2\pi\eta_3)\,\,\\
\,\,z= &\sum_{ij} c^{Z}_{ij} N_i(\eta_1) N_j(\eta_2)\approx a\,\eta_1\sin(2\pi\theta(\eta_1, \eta_2))\,\,\end{bmatrix}
The angular parametrization :math:`\theta(\eta_1, \eta_2)` can either be equal angle or straight field line (see parameters below).
.. image:: ../../pics/mappings/iga_torus.png
Parameters
----------
num_elements : tuple[int]
Number of cells in (radial, angular) direction used for spline mapping (default: [8, 24]).
degree : tuple[int]
Splines degrees in (radial, angular) direction used for spline mapping (default: [2, 3]).
a : float
Minor radius of torus (default: 1.).
R0 : float
Major radius of torus (default: 3.).
tor_period : int
Toroidal periodicity built into the mapping: :math:`\phi=2\pi\,\eta_3/\mathrm{torperiod}` (default: 3 --> one third of a torus).
sfl : bool
Whether to use straight field line coordinates (default: False).
Note
----
In the parameter .yml, use the following in the section `geometry`::
geometry :
type : IGAPolarTorus
IGAPolarTorus :
num_elements : [8, 24] # number of poloidal grid cells, >degree
degree : [3, 3] # poloidal spline degree, >1
a : 1. # minor radius
R0 : 3. # major radius
tor_period : 2 # toroidal periodicity built into the mapping: phi = 2*pi * eta3 / tor_period
sfl : False # whether to use straight field line coordinates (particular theta parametrization)
"""
def __init__(
self,
num_elements: tuple[int] = (8, 24),
degree: tuple[int] = (2, 3),
a: float = 1.0,
R0: float = 3.0,
sfl: bool = False,
tor_period: int = 3,
):
# use params setter
self.params = copy.deepcopy(locals())
# get control points
if sfl:
def theta(eta1, eta2):
return 2 * xp.arctan(xp.sqrt((1 + a * eta1 / R0) / (1 - a * eta1 / R0)) * xp.tan(xp.pi * eta2))
else:
def theta(eta1, eta2):
return 2 * xp.pi * eta2
def R(eta1, eta2):
return a * eta1 * xp.cos(theta(eta1, eta2)) + R0
def Z(eta1, eta2):
return a * eta1 * xp.sin(theta(eta1, eta2))
spl_kind = (False, True)
cx, cy = interp_mapping(num_elements, degree, spl_kind, R, Z)
# make sure that control points at pole are all the same (eta1=0 there)
cx[0] = R0
cy[0] = 0.0
# init base class
super().__init__(
num_elements=num_elements,
degree=degree,
spl_kind=spl_kind,
cx=cx,
cy=cy,
tor_period=tor_period,
)
[docs]
class Cuboid(Domain):
r""" Slab geometry (Cartesian coordinates).
.. math::
F: \begin{bmatrix}\eta_1\\ \eta_2\\ \eta_3\end{bmatrix}\mapsto \begin{bmatrix}
\,\,x= &l_1 + (r_1 - l_1)\,\eta_1\,\,\\
\,\,y= &l_2 + (r_2 - l_2)\,\eta_2\,\,\\
\,\,z= &l_3 + (r_3 - l_3)\,\eta_3\,\,\end{bmatrix}
.. image:: ../../pics/mappings/cuboid.png
Parameters
----------
l1 : float
Start of x-interval (default: 0.).
r1 : float
End of x-interval, r1>l1 (default: 1.).
l2 : float
Start of y-interval (default: 0.).
r2 : float
End of y-interval, r2>l2 (default: 1.).
l3 : float
Start of z-interval (default: 0.).
r3 : float
End of z-interval, r3>l3 (default: 1.).
"""
def __init__(
self,
l1: float = 0.0,
r1: float = 1.0,
l2: float = 0.0,
r2: float = 1.0,
l3: float = 0.0,
r3: float = 1.0,
):
self.kind_map = 10
# use params setter
self.params = copy.deepcopy(locals())
self.params_numpy = self.get_params_numpy()
# periodicity in eta3-direction and pole at eta1=0
self.periodic_eta3 = False
self.pole = False
super().__init__()
[docs]
class Orthogonal(Domain):
r""" Slab geometry with orthogonal mesh distortion.
.. math::
F: \begin{bmatrix}\eta_1\\ \eta_2\\ \eta_3\end{bmatrix}\mapsto \begin{bmatrix}
\,\,x= &L_x\,\left[\,\eta_1 + \alpha\sin(2\pi\,\eta_1)\right]\,\,\\
\,\,y= &L_y\,\left[\,\eta_2 + \alpha\sin(2\pi\,\eta_2)\right]\,\,\\
\,\,z= &L_z\,\eta_3\,\,\end{bmatrix}
.. image:: ../../pics/mappings/orthogonal.png
Parameters
----------
Lx : float
Length of x-interval (default: 2.).
Ly : float
Length of y-interval (default: 3.).
alpha: float
Distortion factor (default: 0.1).
Lz : float
Length of z-interval (default: 6.).
Note
----
In the parameter .yml, use the following in the section `geometry`::
geometry :
type : Orthogonal
Orthogonal :
Lx : 2. # length in x-direction
Ly : 2. # length in y-direction
alpha : .1 # x-distortion and y-distortion
Lz : 1. # length in z-direction
"""
def __init__(
self,
Lx: float = 2.0,
Ly: float = 3.0,
alpha: float = 0.1,
Lz: float = 6.0,
):
self.kind_map = 11
# use params setter
self.params = copy.deepcopy(locals())
self.params_numpy = self.get_params_numpy()
# periodicity in eta3-direction and pole at eta1=0
self.periodic_eta3 = False
self.pole = False
super().__init__()
[docs]
class Colella(Domain):
r""" Slab geometry with Colella mesh distortion.
.. math::
F: \begin{bmatrix}\eta_1\\ \eta_2\\ \eta_3\end{bmatrix}\mapsto \begin{bmatrix}
\,\,x= &L_x\,\left[\,\eta_1 + \alpha\sin(2\pi\,\eta_1)\sin(2\pi\,\eta_2)\,\right]\,\,\\
\,\,y= &L_y\,\left[\,\eta_2 + \alpha\sin(2\pi\,\eta_2)\sin(2\pi\,\eta_1)\,\right]\,\,\\
\,\,z= &L_z\,\eta_3\,\,\end{bmatrix}
.. image:: ../../pics/mappings/colella.png
Parameters
----------
Lx : float
Length of x-interval (default: 2.).
Ly : float
Length of y-interval (default: 3.).
alpha: float
Distortion factor (default: 0.1).
Lz : float
Length of z-interval (default: 6.).
Note
----
In the parameter .yml, use the following in the section `geometry`::
geometry :
type : Colella
Colella :
Lx : 2. # length in x-direction
Ly : 2. # length in y-direction
alpha : .1 # distortion factor
Lz : 1. # length in third direction
"""
def __init__(
self,
Lx: float = 2.0,
Ly: float = 3.0,
alpha: float = 0.1,
Lz: float = 6.0,
):
self.kind_map = 12
# use params setter
self.params = copy.deepcopy(locals())
self.params_numpy = self.get_params_numpy()
# periodicity in eta3-direction and pole at eta1=0
self.periodic_eta3 = False
self.pole = False
super().__init__()
[docs]
class HollowCylinder(Domain):
r""" Cylinder with possible hole around the axis.
.. math::
F: \begin{bmatrix}\eta_1\\ \eta_2\\ \eta_3\end{bmatrix}\mapsto \begin{bmatrix}
\,\,x= &\left[\,a_1 + (a_2-a_1)\,\eta_1\,\right]\cos(2\pi\,\eta_2 / poc)\,\,\\
\,\,y= &\left[\,a_1 + (a_2-a_1)\,\eta_1\,\right]\sin(2\pi\,\eta_2 / poc)\,\,\\
\,\,z= &L_z\,\eta_3\,\,\end{bmatrix}
.. image:: ../../pics/mappings/hollow_cylinder.png
Parameters
----------
a1 : float
Inner radius of cylinder (default: 0.2).
a2 : float
Outer radius of cylinder (default: 1.0).
Lz: float
Length of cylinder (default: 4.)
poc: int
Which periodicity used in the mapping, i.e. :math: `\theta = 2*\pi*\eta_2 / \mathrm{poc}` (piece of cake) (default: 1).
Note
----
In the parameter .yml, use the following in the section `geometry`::
geometry :
type : HollowCylinder
HollowCylinder :
a1 : .2 # inner radius
a2 : 1. # outer radius
Lz : 4. # length of cylinder
poc: 2. # periodicity of theta used in the mapping
"""
def __init__(
self,
a1: float = 0.2,
a2: float = 1.0,
Lz: float = 4.0,
poc: int = 1,
):
self.kind_map = 20
# use params setter
self.params = copy.deepcopy(locals())
self.params_numpy = self.get_params_numpy()
# periodicity in eta3-direction and pole at eta1=0
self.periodic_eta3 = False
if a1 == 0.0:
self.pole = True
else:
self.pole = False
super().__init__()
[docs]
class PoweredEllipticCylinder(Domain):
r""" Cylinder with elliptic cross section and radial power law.
.. math::
F: \begin{bmatrix}\eta_1\\ \eta_2\\ \eta_3\end{bmatrix}\mapsto \begin{bmatrix}
\,\,x= &r_x\,\eta_1^s\cos(2\pi\,\eta_2)\,\,\\
\,\,y= &r_y\,\eta_1^s\sin(2\pi\,\eta_2)\,\,\\
\,\,z= &L_z\,\eta_3\,\,\end{bmatrix}
.. image:: ../../pics/mappings/pow_elliptic_cyl.png
Parameters
----------
rx : float
Radius in x-direction (default: 1.0).
ry : float
Radius in y-direction (default: 2.0).
Lz: float
Length in z-direction (default: 6.0).
s : float
Power of radial coordinate (default: 0.5).
Note
----
In the parameter .yml, use the following in the section `geometry`::
geometry :
type : PoweredEllipticCylinder
PoweredEllipticCylinder :
rx : 1. # axis length in x-direction
ry : 2. # axis length in y-direction
Lz : 4. # length in z-direction
s : .5 # power of radial coordinate
"""
def __init__(
self,
rx: float = 1.0,
ry: float = 2.0,
Lz: float = 6.0,
s: float = 0.5,
):
self.kind_map = 21
# use params setter
self.params = copy.deepcopy(locals())
self.params_numpy = self.get_params_numpy()
# periodicity in eta3-direction and pole at eta1=0
self.periodic_eta3 = False
self.pole = True
super().__init__()
[docs]
class HollowTorus(Domain):
r""" Torus with possible hole around the magnetic axis (center of the smaller circle).
.. math::
F: \begin{bmatrix}\eta_1\\ \eta_2\\ \eta_3\end{bmatrix}\mapsto \begin{bmatrix}
\,\,x= &\lbrace\left[\,a_1 + (a_2-a_1)\,\eta_1\,\right]\cos\left[\theta(\eta_1,\eta_2)\right]+R_0\rbrace\cos(\phantom{-}2\pi\,\eta_3 / n)\,\,\\
\,\,y= &\lbrace\left[\,a_1 + (a_2-a_1)\,\eta_1\,\right]\cos\left[\theta(\eta_1,\eta_2)\right]+R_0\rbrace\sin(-2\pi\,\eta_3 / n)\,\,\\
\,\,z= &\left[\,a_1 + (a_2-a_1)\,\eta_1\,\right]\sin\left[\theta(\eta_1,\eta_2)\right]\,\,\end{bmatrix}
with the following possible poloidal angle parametrizations:
.. math::
&\theta(\eta_1,\eta_2) = \left\{\begin{aligned}
& 2\pi\,\eta_2\,, \quad &&\textnormal{if}\quad \textnormal{sfl}=\textnormal{False}\,,
&2\arctan\left[\sqrt{\frac{1 + \epsilon(\eta_1)}{1 - \epsilon(\eta_1)}}\,\tan\left(\pi\,\eta_2\right)\right]\quad &&\textnormal{if}\quad \textnormal{sfl}=\textnormal{True}\,,
&\qquad \textrm {with}\qquad \epsilon(\eta_1) = \frac{a_1 + (a_2-a_1)\,\eta_1}{R_0}\,.
\end{aligned}\right.
.. image:: ../../pics/mappings/hollow_torus.png
Parameters
----------
a1 : float
Inner minor radius of hollow torus (default: 0.2).
a2 : float
Outer minor radius of hollow torus (default: 1.0).
R0 : float
Major radius of torus (default: 3.0).
sfl : bool
Whether to use straight field line coordinates (True) or not (False) (default: False).
pol_period: int
Which periodicity used in the mapping, i.e. :math: `\theta = 2*\pi*\eta_2 / \mathrm{pol_period}` (piece of cake) (default: 1, only for sfl=False).
tor_period : int
Toroidal periodicity built into the mapping: :math:`\phi=2\pi\,\eta_3/\mathrm{torperiod}` (default: 3 --> one third of a torus).
Note
----
In the parameter .yml, use the following in the section `geometry`::
geometry :
type : HollowTorus
HollowTorus :
a1 : 0.2 # inner radius
a2 : 1.0 # minor radius
R0 : 3.0 # major radius
sfl : False # straight field line coordinates?
pol_period: 2. # periodicity of theta used in the mapping: theta = 2*pi * eta2 / pol_period (if not sfl)
tor_period : 2 # toroidal periodicity built into the mapping: phi = 2*pi * eta3 / tor_period
"""
def __init__(
self,
a1: float = 0.1,
a2: float = 1.0,
R0: float = 3.0,
sfl: bool = False,
pol_period: int = 1,
tor_period: int = 3,
):
self.kind_map = 22
# use params setter
self.params = copy.deepcopy(locals())
self.params_numpy = self.get_params_numpy()
assert a2 <= R0, f"The minor radius must be smaller or equal than the major radius! {a2 =}, {R0 =}"
if sfl:
assert pol_period == 1, (
"Piece-of-cake is only implemented for torus coordinates, not for straight field line coordinates!"
)
# periodicity in eta3-direction and pole at eta1=0
self.periodic_eta3 = True
if a1 == 0.0:
self.pole = True
else:
self.pole = False
super().__init__()
[docs]
def inverse_map(self, x, y, z, bounded=True, change_out_order=False):
"""Analytical inverse map of HollowTorus"""
mr = xp.sqrt(x**2 + y**2) - self.params["R0"]
eta3 = xp.arctan2(-y, x) % (2 * xp.pi / self.params["tor_period"]) / (2 * xp.pi) * self.params["tor_period"]
eta2 = xp.arctan2(z, mr) % (2 * xp.pi / self.params["pol_period"]) / (2 * xp.pi / self.params["pol_period"])
eta1 = (z / xp.sin(2 * xp.pi * eta2 / self.params["pol_period"]) - self.params["a1"]) / (
self.params["a2"] - self.params["a1"]
)
if bounded:
eta1[eta1 > 1] = 1.0
eta1[eta1 < 0] = 0.0
assert xp.all(xp.logical_and(eta1 >= 0, eta1 <= 1))
assert xp.all(xp.logical_and(eta2 >= 0, eta2 <= 1))
assert xp.all(xp.logical_and(eta3 >= 0, eta3 <= 1))
if change_out_order:
return xp.transpose((eta1, eta2, eta3))
else:
return eta1, eta2, eta3
[docs]
class ShafranovShiftCylinder(Domain):
r""" Cylinder with quadratic Shafranov shift.
.. math::
F: \begin{bmatrix}\eta_1\\ \eta_2\\ \eta_3\end{bmatrix}\mapsto \begin{bmatrix}
\,\,x= &r_x\,\eta_1\cos(2\pi\,\eta_2)+(1-\eta_1^2)\,r_x\Delta\,\,\\
\,\,y= &r_y\,\eta_1\sin(2\pi\,\eta_2)\,\,\\
\,\,z= &L_z\,\eta_3\,\,\end{bmatrix}
.. image:: ../../pics/mappings/shafranov_shift.png
Parameters
----------
rx : float
Radius in x-direction (default: 1.0).
ry : float
Radius in y-direction (default: 1.0).
Lz: float
Length in z-direction (default: 4.0).
delta : float
Shift factor, should be in [0, 0.1] (default: 0.2).
Note
----
In the parameter .yml, use the following in the section `geometry`::
geometry :
type : ShafranovShiftCylinder
ShafranovShiftCylinder :
rx : 1. # axis length
ry : 1. # axis length
Lz : 4. # length in z-direction
delta : .2 # shift factor, should be in [0, 0.1]
"""
def __init__(
self,
rx: float = 1.0,
ry: float = 1.0,
Lz: float = 4.0,
delta: float = 0.2,
):
self.kind_map = 30
# use params setter
self.params = copy.deepcopy(locals())
self.params_numpy = self.get_params_numpy()
# periodicity in eta3-direction and pole at eta1=0
self.periodic_eta3 = False
self.pole = True
super().__init__()
[docs]
class ShafranovSqrtCylinder(Domain):
r""" Cylinder with square-root Shafranov shift.
.. math::
F: \begin{bmatrix}\eta_1\\ \eta_2\\ \eta_3\end{bmatrix}\mapsto \begin{bmatrix}
\,\,x= &r_x\,\eta_1\cos(2\pi\,\eta_2)+(1-\sqrt \eta_1)r_x\Delta\,\,\\
\,\,y= &r_y\,\eta_1\sin(2\pi\,\eta_2)\,\,\\
\,\,z= &L_z\,\eta_3\,\,\end{bmatrix}
.. image:: ../../pics/mappings/shafranov_sqrt.png
Parameters
----------
rx : float
Radius in x-direction (default: 1.0).
ry : float
Radius in y-direction (default: 1.0).
Lz: float
Length in z-direction (default: 4.0).
delta : float
Shift factor, should be in [0, 0.1] (default: 0.2).
Note
----
In the parameter .yml, use the following in the section `geometry`::
geometry :
type : ShafranovSqrtCylinder
ShafranovSqrtCylinder :
rx : 1. # axis length
ry : 1. # axis length
Lz : 4. # length in third direction
delta : .2 # shift factor, should be in [0, 0.1]
"""
def __init__(
self,
rx: float = 1.0,
ry: float = 1.0,
Lz: float = 4.0,
delta: float = 0.2,
):
self.kind_map = 31
# use params setter
self.params = copy.deepcopy(locals())
self.params_numpy = self.get_params_numpy()
# periodicity in eta3-direction and pole at eta1=0
self.periodic_eta3 = False
self.pole = True
super().__init__()
[docs]
class ShafranovDshapedCylinder(Domain):
r""" Cylinder with D-shaped cross section and quadratic Shafranov shift.
.. math::
F: \begin{bmatrix}\eta_1\\ \eta_2\\ \eta_3\end{bmatrix}\mapsto \begin{bmatrix}
\,\,x= &R_0\left[1 + (1 - \eta_1^2)\Delta_x + \eta_1\epsilon\cos(2\pi\,\eta_2 + \arcsin(\delta)\eta_1\sin(2\pi\,\eta_2)) \right]\,\,\\
\,\,y= &R_0\left[ (1 - \eta_1^2)\Delta_y + \eta_1\epsilon\kappa\sin(2\pi\,\eta_2)\right]\,\,\\
\,\,z= &L_z\,\eta_3\,\,\end{bmatrix}
.. image:: ../../pics/mappings/shafranov_dshaped.png
Parameters
----------
R0 : float
Base radius (default: 2.).
Lz : float
Length in z-direction (default: 4.).
delta_x : float
Shafranov shift in x-direction (default: 0.05).
delta_y : float
Shafranov shift in y-direction (default: 0.025).
delta_gs : float
Delta = sin(alpha): triangularity, shift of high point (default: 0.05).
epsilon_gs : float
Epsilon: inverse aspect ratio a/r0 (default: 0.5).
kappa_gs : float
Kappa: ellipticity (elongation) (default: 2.).
Note
----
In the parameter .yml, use the following in the section `geometry`::
geometry :
type : ShafranovDshapedCylinder
ShafranovDshapedCylinder :
R0 : 2. # base radius
Lz : 4. # length in third direction
delta_x : .05 # Shafranov shift in x-direction
delta_y : .025 # Shafranov shift in y-direction
delta_gs : .05 # delta = sin(alpha): triangularity, shift of high point
epsilon_gs : .5 # epsilon: inverse aspect ratio a/r0
kappa_gs : 2. # Kappa: ellipticity (elongation)
"""
def __init__(
self,
R0: float = 2.0,
Lz: float = 3.0,
delta_x: float = 0.1,
delta_y: float = 0.0,
delta_gs: float = 0.33,
epsilon_gs: float = 0.32,
kappa_gs: float = 1.7,
):
self.kind_map = 32
# use params setter
self.params = copy.deepcopy(locals())
self.params_numpy = self.get_params_numpy()
# periodicity in eta3-direction and pole at eta1=0
self.periodic_eta3 = False
self.pole = True
super().__init__()