Source code for torx.equilibrium.flare_m

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