"""Contains FLARE equilibrium."""
from pathlib import Path
import numpy as np
import xarray as xr
from torx import Quantity, make_xarray
from torx.units_handling_m import check_units
from torx.equilibrium.equilibrium_m import EquilibriumBaseClass
from torx.autogrid_decorators_m import autogrid_method
from torx.autodoc_decorators_m import autodoc_class
from torx.grid.grid_2d_m import Grid2D
from .check_equilibrium_m import FlareEquiType
from torx.fileio.filepath_resolver_m import filepath_resolver
from ._flare_module_import_m import import_flare
from ._flare_coilset_loader_m import load_coilsets
def _add_phi_doc(func):
"""Append phi documentation to a function's docstring."""
phi_doc = """
Other Parameters
----------------
phi_index : int or float, optional
Plane index. Converted to a toroidal angle for FLARE in radians
based on the grid resolution and whether only the first field period
is used.
phi_angle : float, optional
Physical toroidal angle in radians. If only the first field
period is simulated, the angle is mapped into
[0, 2*pi/num_field_periods).
Exactly one of phi_index or phi_angle must be provided.
"""
if func.__doc__:
func.__doc__ += phi_doc
return func
[docs]
@autodoc_class
class FlareEquilibrium(EquilibriumBaseClass):
"""
Class for equilibria generated by the FLARE tool.
The FLARE code is a magnetic mesh generator that is integrated within a
suite of tools for the analysis of the magnetic geometry in toroidal
fusion devices.
[1] H. Frerichs 2024 Nucl. Fusion 64 106034
[2] https://doi.org/10.1088/1741-4326/ad7303
[3] https://hfrerichs.gitlab.io/flare
"""
[docs]
def __init__(
self,
filepath: Path=Path("./"),
rhomin: float=0.0,
rhomax: float=1.0,
B0: Quantity=Quantity("1T"),
L_ref: float=1.0,
equi_flare_type: str="",
boundary_flare_type: str="",
rho_flare_type: str="",
flare_rho_dirpath: str="",
flare_rho_prefix: str="",
rho_symms: int=1,
flare_equi3d_path_mgrid: Path=Path(""),
flare_equi3d_path_axis: Path=Path(""),
flare_equi3d_path_bfield: Path=Path(""),
flare_equi3d_path_hint: Path=Path(""),
Rmin: float=0.0,
Rmax: float=1.0,
Zmin: float=-1.0,
Zmax: float=1.0,
n_points_phi: int=1,
only_first_field_period: bool=False,
amplitudes: list=[15000, 15000, 15000, 15000, 15000],
bspline_data: str="magnetic_field",
spline_order: int=3,
num_field_periods: int=5,
group: int=-1,
bmax: float=5.0,
**kwargs
):
"""
Initialize the FlareEquilibrium.
Compatible with mgrid, coilset and HINT FLARE equilibrium types.
"""
self._model, self._bfield = import_flare()
# Legacy correction
if equi_flare_type == "equi3d":
equi_flare_type = "equi3d_mgrid"
paths = {
"axis": flare_equi3d_path_axis,
"bfield": flare_equi3d_path_bfield,
"hint": flare_equi3d_path_hint,
"mgrid": flare_equi3d_path_mgrid,
}
self.__equi_name__ = "FlareEquilibrium"
self.filepath = filepath
self.rhomin = rhomin
self.rhomax = rhomax
self.B0 = B0
self.equi_flare_type = equi_flare_type
self.boundary_flare_type = boundary_flare_type
self.rho_flare_type = rho_flare_type
self.flare_rho_dirpath = flare_rho_dirpath
self.flare_rho_prefix = flare_rho_prefix
self.rho_symms = rho_symms
self._norm_length = Quantity(L_ref, "m")
self.flare_equi3d_path_mgrid = flare_equi3d_path_mgrid
self.flare_equi3d_path_axis = flare_equi3d_path_axis
self.flare_equi3d_path_bfield = flare_equi3d_path_bfield
self.flare_equi3d_path_hint = flare_equi3d_path_hint
self.Rmin = Rmin
self.Rmax = Rmax
self.Zmin = Zmin
self.Zmax = Zmax
self.n_points_phi = n_points_phi
self.only_first_field_period = only_first_field_period
self.num_field_periods = num_field_periods
self.amplitudes = amplitudes
self.bspline_data = bspline_data
self.spline_order = spline_order
self.group = group
self.bmax = bmax
if FlareEquiType(equi_flare_type) == FlareEquiType.equi3d_mgrid:
flare_model = self._model.Equi3d_Mgrid(str(paths["mgrid"]),
amplitudes,
bspline_data,
spline_order
)
elif FlareEquiType(equi_flare_type) == FlareEquiType.equi3d_coilset:
flare_model = load_coilsets(paths["bfield"])
elif FlareEquiType(equi_flare_type) == FlareEquiType.equi3d_hint:
flare_model = self._model.Equi3d_Hint(str(paths["hint"]),
group, bmax)
else:
raise NotImplementedError(f"{equi_flare_type} not implemented.")
self._model.init_equi3d_no_boundary(flare_model,
str(flare_equi3d_path_axis))
self._norm_length = Quantity(self.axis_r_flare, "m")
self.Z0 = Quantity(self.axis_z_flare, "m")
self._norm_magfield = Quantity(self.Btor0, "T")
def _read_phi(self, **kwargs):
"""Read, scale and convert phi to radians for FLARE."""
phi_index = kwargs.get("phi_index", None)
phi_angle = kwargs.get("phi_angle", None)
if (phi_index is None) == (phi_angle is None):
raise TypeError(
"Exactly one of 'phi_index' or 'phi_angle' must be provided."
)
if phi_index is not None:
if self.only_first_field_period:
phi = 2 * np.pi / (self.num_field_periods * self.n_points_phi) \
* phi_index
else:
phi = 2 * np.pi / self.n_points_phi * phi_index
elif phi_angle is not None:
if self.only_first_field_period:
phi = phi_angle % (2 * np.pi / self.num_field_periods)
else:
phi = phi_angle % (2 * np.pi)
return phi
[docs]
def axis_r(self, **kwargs):
"""Magnetic axis radial position in non-normalized units."""
phi = self._read_phi(**kwargs)
axis = self._bfield.f2py.analysis.magnetic_axis(phi)
return axis[0]
[docs]
def axis_z(self, **kwargs):
"""Magnetic axis vertical position in non-normalized units."""
phi = self._read_phi(**kwargs)
axis = self._bfield.f2py.analysis.magnetic_axis(phi)
return axis[1]
[docs]
def Btor_axis(self, **kwargs):
"""Return the toroidal magnetic field strength on axis."""
phi = self._read_phi(**kwargs)
return self._bfield.eval(self.axis_r_flare, 0.0, phi)[2]
@property
def axis_r_flare(self):
"""Unnormalized magnetic axis radial position on first plane."""
return self.axis_r(phi_index = 0)
@property
def axis_z_flare(self):
"""Unnormalized magnetic axis vertical position on first plane."""
return self.axis_z(phi_index = 0)
@property
def Btor0(self):
"""Toroidal magnetic field strength on axis for first plane."""
return self.Btor_axis(phi_index = 0)
@property
def axis_r_norm(self):
"""Magnetic axis radial position in normalized units."""
return make_xarray(1.0)
@property
def axis_z_norm(self):
"""Magnetic axis vertical position in normalized units."""
return make_xarray(0.0)
@property
def axis_phi(self):
"""Magnetic axis toroidal position."""
return make_xarray(0.0)
@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 length normalization."""
check_units(value, {"[length]":1}, "R0")
self._norm_length = value
@autogrid_method
def _magfield_all_components_flare(self, r_norm, z_norm, **kwargs) \
-> xr.DataArray:
"""
Evaluate the magnetic field using FLARE on the given points.
Returns all three magnetic field components from FLARE.
Compatible with single values, structured grids and unstructured grids.
"""
phi = self._read_phi(**kwargs)
if isinstance(r_norm, xr.DataArray) and "points" in r_norm.dims:
return self._magfield_unstructured(r_norm, z_norm, phi=phi)
else:
return self._magfield_structured(r_norm, z_norm, phi=phi)
@autogrid_method
def _magfield_unstructured(self, r_norm, z_norm, **kwargs) -> xr.DataArray:
"""B components evaluated for unstructured arrays from FLARE."""
phi = kwargs["phi"]
r_norm = self.convert_length_to_normalized(r_norm)
z_norm = self.convert_length_to_normalized(z_norm)
R_size = r_norm.size
R_flare = r_norm * self.axis_r_flare
Z_flare = z_norm * self.axis_r_flare + self.axis_z_flare
phi_vec = np.full(R_size, phi)
B_flare = self._bfield.eval(R_flare, Z_flare, phi_vec) / self.Btor0
B_flare = np.asarray(B_flare)
B_flare = Quantity(B_flare, "T")
return xr.DataArray(
B_flare,
attrs={"norm": self.B0},
dims=("points", "component"),
coords={
"component": ["Br", "Bz", "Bphi"]
},
)
@autogrid_method
def _magfield_structured(self, r_norm, z_norm, **kwargs) -> xr.DataArray:
"""B components evaluated for structured arrays or values from FLARE."""
phi = kwargs["phi"]
if isinstance(r_norm, xr.DataArray):
r_norm = self.convert_length_to_normalized(r_norm)
z_norm = self.convert_length_to_normalized(z_norm)
r_norm = np.atleast_1d(r_norm)
z_norm = np.atleast_1d(z_norm)
Nr, Nz = r_norm.size, z_norm.size
R_vals = r_norm * self.axis_r_flare
Z_vals = z_norm * self.axis_r_flare + self.axis_z_flare
R_flare = R_vals[:, None]
Z_flare = Z_vals[None, :]
R_flare, Z_flare = np.broadcast_arrays(R_flare, Z_flare)
phi_grid = np.full(R_flare.shape, phi)
B_flare = self._bfield.eval(R_flare.ravel(),
Z_flare.ravel(),
phi_grid.ravel()
) \
/ self.Btor0
B_flare = np.asarray(B_flare)
if B_flare.ndim == 1:
B_flare = B_flare[None, :]
B_flare = B_flare.reshape(Nr, Nz, B_flare.shape[-1]).transpose(1,0,2)
B_flare = Quantity(B_flare, "T")
return xr.DataArray(
B_flare,
attrs={"norm": self.B0},
dims=("Z", "R", "component"),
coords={"component": ["Br", "Bz", "Bphi"]},
)
[docs]
@autogrid_method
@_add_phi_doc
def magfield_component_r(self, r_norm, z_norm, **kwargs) \
-> xr.DataArray:
"""
Return the radial component of the FLARE magnetic field B.
Parameters
----------
r_norm : xr.DataArray or float
Normalized radial coordinates. Can be a float, structured data or
unstructured data.
z_norm : xr.DataArray or float
Normalized vertical coordinates. Can be a float, structured data or
unstructured data.
"""
all_components = self._magfield_all_components_flare(
r_norm, z_norm, **kwargs)
component = all_components.sel(component="Br").drop_vars("component")
return component.squeeze(drop=True)
[docs]
@autogrid_method
@_add_phi_doc
def magfield_component_z(self, r_norm, z_norm, **kwargs) \
-> xr.DataArray:
"""
Return the vertical component of the FLARE magnetic field B.
Parameters
----------
r_norm : xr.DataArray or float
Normalized radial coordinates. Can be a float, structured data or
unstructured data.
z_norm : xr.DataArray or float
Normalized vertical coordinates. Can be a float, structured data or
unstructured data.
"""
all_components = self._magfield_all_components_flare(
r_norm, z_norm, **kwargs)
component = all_components.sel(component="Bz").drop_vars("component")
return component.squeeze(drop=True)
[docs]
@autogrid_method
@_add_phi_doc
def magfield_component_toroidal(self, r_norm, z_norm, **kwargs) \
-> xr.DataArray:
"""
Return the toroidal component of the FLARE magnetic field B.
Parameters
----------
r_norm : xr.DataArray or float
Normalized radial coordinates. Can be a float, structured data or
unstructured data.
z_norm : xr.DataArray or float
Normalized vertical coordinates. Can be a float, structured data or
unstructured data.
"""
all_components = self._magfield_all_components_flare(
r_norm, z_norm, **kwargs)
component = all_components.sel(component="Bphi").drop_vars("component")
return component.squeeze(drop=True)
[docs]
def get_boundary_polygon(self, **kwargs):
"""
Return the boundary polygon.
Todo
----
Not yet implemented.
"""
raise NotImplementedError
[docs]
def normalized_flux_surface_label(self, r_norm, z_norm, **kwargs):
"""
Return the normalized flux surface label.
Todo
----
Not yet implemented.
"""
raise NotImplementedError