Source code for torx.equilibrium.numerical_m

"""Contains numerical tokamak equilibrium."""
import dask
import warnings
import numpy as np
from pathlib import Path
import xarray as xr
from xarray import Dataset
from scipy.interpolate import RectBivariateSpline, interp1d
from collections import defaultdict
from numba import njit, cfunc
from numbalsoda import lsoda_sig, lsoda

from .equilibrium_m import EquilibriumBaseClass
from torx.analysis import bspline_basis_and_deriv1

from torx.units import Quantity
from torx.units import check_units
from torx.arrays import make_xarray
from torx.units import convert_xarray_to_quantity
from torx.decorators import autogrid_method
from torx.geometry import Polygon2D
from torx.decorators import autodoc_class
from torx.analysis import MagneticFieldTracer

@njit(cache=True)
def _eval_psi_derivs(r, z, tx, ty, c, nx, ny, kx, ky):
    """
    Evaluate dpsi/dr and dpsi/dz at (r, z) from bivariate B-spline tck.
    
    c is 1-D, C-order: c[i * n_cy + j] for basis pair (i, j).
    """
    n_cy = ny - ky - 1

    Br, dBr, ir = bspline_basis_and_deriv1(r, kx, tx)
    Bz, dBz, iz = bspline_basis_and_deriv1(z, ky, ty)

    psi_dr = 0.0
    psi_dz = 0.0
    for a in range(kx + 1):
        ci = ir - kx + a
        row = ci * n_cy
        for b in range(ky + 1):
            cj = iz - ky + b
            coef = c[row + cj]
            psi_dr += coef * dBr[a] * Bz[b]
            psi_dz += coef * Br[a] * dBz[b]

    return psi_dr, psi_dz

[docs] @autodoc_class class NumericalEquilibrium(EquilibriumBaseClass): """Equilibrium with a divertor defined by numerical poloidal flux."""
[docs] @classmethod def initialize_from_params(cls, filepath: Path, params: defaultdict): """Create an equilibrium from a parameter file.""" path_to_netcdf = Path(params["equi_numerical_params"]["path_to_netcdf"]) equilibrium_case = params["equi_numerical_params"]["equilibrium_case"] equi_file = Path(str(filepath / path_to_netcdf / equilibrium_case) + ".nc") assert ( equi_file.exists() ), f"Error: equilibrium file {equi_file.absolute()} not found" return cls.initialize_from_equi_file(equi_file)
[docs] @classmethod def initialize_from_equi_file(cls, equi_file: Path): """Create an equilibrium from an equilibrium file.""" from .io import read_netcdf_equilibrium assert equi_file.exists() and equi_file.suffix == ".nc" return cls(read_netcdf_equilibrium(equi_file), equi_file)
[docs] def write_to_equi_file( self, file_path: Path, description: str, comment: str = "", allow_overwrite: bool = False, ordering: str = "Fortran", ): """ Write the equilibrium to a NetCDF file. Parameters ---------- file_path : Path Output path, must have .nc suffix. description : str Human-readable description written to the file header. comment : str, optional Additional comment written to the file header. allow_overwrite : bool, optional If False, raises FileExistsError if file_path exists. ordering : str, optional Array ordering, "Fortran" or "C". Default is "Fortran". """ from .io import write_netcdf_equilibrium write_netcdf_equilibrium( file_path=file_path, equi=self, description=description, comment=comment, allow_overwrite=allow_overwrite, ordering=ordering, )
[docs] def __init__(self, equi_dict: dict, equi_file: Path, B0=Quantity("1T")): """ Initialize the equilibrium from a standardized dictionary. The dictionary is expected to contain data in normalized units for lengths and Weber for poloidal flux, as returned by read_netcdf_equilibrium(). The normalization length R0 is fixed to the magnetic axis radial position. B0 is set separately after initialization via the B0 setter. Parameters ---------- equi_dict : dict Dictionary as returned by read_netcdf_equilibrium() with keys: - axis_r: float, magnetic axis R in normalized units - axis_z: float, magnetic axis Z in normalized units - axis_Btor: float, toroidal field on axis in Tesla - axis_Btor_units: str, units of axis_Btor - psi_axis: float, poloidal flux on axis in Weber - psi_separatrix: float, poloidal flux on separatrix in Weber - rho_min: float, minimum flux surface label - rho_max: float, maximum flux surface label - spline_basis_r: np.ndarray, R grid in normalized units - spline_basis_z: np.ndarray, Z grid in normalized units - psi_data: np.ndarray, poloidal flux on grid in Weber - norm_length: float, R0 in meters - poloidal_field_factor: float - x_point_r: float or np.ndarray or None - x_point_z: float or np.ndarray or None - divertor_polygon: Polygon2D in normalized units - exclusion_polygon: Polygon2D or None in normalized units - flux_limit_polygons: dict of Polygon2D in normalized units - flux_limit_rho_min: dict of float or None - flux_limit_rho_max: dict of float or None equi_file : Path Path to the source equilibrium file. B0 : Quantity, optional Magnetic field normalization. Default is 1 T. """ self.B0 = B0 self.filepath = equi_file self.poloidal_field_factor = float(equi_dict.get("poloidal_field_factor", 1.0)) self._flipped_Z = False axis_r_units = equi_dict["axis_r_units"] self.axis_r = make_xarray(equi_dict["axis_r"], norm=Quantity(axis_r_units)) self.axis_z = make_xarray(equi_dict["axis_z"], norm=Quantity(axis_r_units)) x_point_units = equi_dict["x_point_units"] self.x_point_r = ( make_xarray(equi_dict["x_point_r"], norm=Quantity(x_point_units)) if equi_dict.get("x_point_r") is not None else None ) self.x_point_z = ( make_xarray(equi_dict["x_point_z"], norm=Quantity(x_point_units)) if equi_dict.get("x_point_z") is not None else None ) self._axis_Btor = float(equi_dict["axis_Btor"]) self._axis_Btor_units = equi_dict["axis_Btor_units"] norm_length = Quantity(equi_dict["axis_r"], axis_r_units) self._norm_length = norm_length self._spline_basis_r = make_xarray( equi_dict["spline_basis_r"], norm=norm_length, dims=["R"] ) self._spline_basis_z = make_xarray( equi_dict["spline_basis_z"], norm=norm_length, dims=["Z"] ) self._psi_data = equi_dict["psi_data"] self.psi_interpolator = RectBivariateSpline( self.spline_basis_r, self.spline_basis_z, self.psi_data.T, ) self._b_pol_interpolator = None self.divertor_polygon = equi_dict["divertor_polygon"] self.exclusion_polygon = equi_dict.get("exclusion_polygon") self.flux_limit_polygons = equi_dict.get("flux_limit_polygons", {}) self.flux_limit_rho_min = equi_dict.get("flux_limit_rho_min", {}) self.flux_limit_rho_max = equi_dict.get("flux_limit_rho_max", {}) self._psi_axis = float(equi_dict["psi_axis"]) self._psi_separatrix = float(equi_dict["psi_separatrix"]) self._rho_min = float(equi_dict["rho_min"]) self._rho_max = float(equi_dict["rho_max"])
[docs] def flip_Z(self, preserve_helicity: bool = True): """ Flip the vertical direction of the equilibrium. Affects toroidal field reversal. """ self._flipped_Z = not self._flipped_Z if preserve_helicity: self.poloidal_field_factor *= -1.0 self.axis_z *= -1.0 if self.x_point_z is not None: self.x_point_z *= -1.0 self._spline_basis_z = -self._spline_basis_z[::-1] self._psi_data = self._psi_data[::-1, :] self.psi_interpolator = RectBivariateSpline( self.spline_basis_r, self.spline_basis_z, self.psi_data.T ) self._b_pol_interpolator = None self.divertor_polygon.flip_Z() if self.exclusion_polygon is not None: self.exclusion_polygon.flip_Z()
def _build_b_pol_interpolator(self) -> RectBivariateSpline: """Build interpolator of the poloidal magnetic field magnitude.""" r = np.asarray(self.spline_basis_r) z = np.asarray(self.spline_basis_z) psi_dr = self.psi_interpolator(r, z, grid=True, dx=1).T psi_dz = self.psi_interpolator(r, z, grid=True, dy=1).T r_grid = r[np.newaxis, :] b_pol = np.sqrt(psi_dr**2 + psi_dz**2) / r_grid return RectBivariateSpline(r, z, b_pol.T) @property def poloidal_magnetic_field_interpolator(self) -> RectBivariateSpline: """Interpolator of the poloidal magnetic field magnitude.""" if not hasattr(self, '_b_pol_interpolator') or \ self._b_pol_interpolator is None: self._b_pol_interpolator = self._build_b_pol_interpolator() return self._b_pol_interpolator @property def spline_basis_r(self): """R grid vector.""" return self._spline_basis_r @property def spline_basis_z(self): """Z grid vector.""" return self._spline_basis_z @property def psi_data(self) -> xr.DataArray: """Raw poloidal flux data on the grid.""" return make_xarray( self._psi_data, norm=Quantity(self.psi_units), ) @property def psi_units(self): """Units of the poloidal flux.""" return "Wb" @property def axis_Btor(self) -> xr.DataArray: """On-axis toroidal magnetic field.""" return make_xarray(self._axis_Btor, norm=Quantity(self._axis_Btor_units)) @property def axis_Btor_norm(self) -> xr.DataArray: """On-axis toroidal field normalized to B0.""" return make_xarray(self._axis_Btor / self.B0.magnitude, norm=self.B0) @property def rho_min(self) -> xr.DataArray: """Minimum flux surface label.""" return make_xarray(self._rho_min, norm=Quantity(1)) @property def rho_max(self) -> xr.DataArray: """Maximum flux surface label.""" return make_xarray(self._rho_max, norm=Quantity(1)) @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(self.axis_z / self.axis_r) @property def x_point_r_norm(self): """Magnetic X-point radial position in normalized units.""" return make_xarray(self.x_point_r / self.axis_r) @property def x_point_z_norm(self): """Magnetic X-point vertical position in normalized units.""" return make_xarray(self.x_point_z / self.axis_r) @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 the length normalization.""" check_units(value, {"[length]": 1}, "R0") self._norm_length = value @property def psi_axis(self) -> float: """Poloidal flux on axis in Weber.""" return self._psi_axis @property def psi_separatrix(self) -> float: """Poloidal flux on separatrix in Weber.""" return self._psi_separatrix
[docs] @autogrid_method def poloidal_flux(self, r_norm, z_norm, **kwargs): """Return the poloidal flux at a point.""" r_norm = self.convert_length_to_normalized(r_norm) z_norm = self.convert_length_to_normalized(z_norm) is_structured = kwargs["is_structured"] interp_result = self.psi_interpolator( x=r_norm, y=z_norm, grid=is_structured ).T if not kwargs["dims"]: interp_result = interp_result.item() psi = make_xarray( interp_result, norm=Quantity(1, self.psi_units), dims=kwargs["dims"], coords=kwargs["coords"] ) return psi
[docs] @autogrid_method def normalized_flux_surface_label(self, r_norm, z_norm, **kwargs) \ -> xr.DataArray: """Return the normalized flux surface label at a point.""" r_norm = self.convert_length_to_normalized(r_norm) z_norm = self.convert_length_to_normalized(z_norm) rho = self._poloidal_flux_to_rho(self.poloidal_flux(r_norm, z_norm)) return xr.DataArray(rho, attrs={"norm": Quantity(1)}, dims=kwargs["dims"], coords=kwargs["coords"])
def _poloidal_flux_to_rho(self, psi_value): """Convert poloidal flux (psi) to normalized flux coordinate (rho).""" psi_ax = self.psi_axis psi_sep = self.psi_separatrix rho_squared = (psi_value - psi_ax) / (psi_sep - psi_ax) rho_squared = np.atleast_1d(rho_squared) # NOTE: Because the evaluation precision of the poloidal flux might be # limited, it is not guaranteed that the flux at the axis (which # is stored at init) is larger than the evaluated flux at axis. # Thus we set a floor of zero for rho squared. rho_squared[np.asarray(rho_squared < 0.0)] = 0.0 if np.size(rho_squared) == 1: rho_squared = rho_squared[0] return np.sqrt(rho_squared)
[docs] @autogrid_method def magfield_component_r(self, r_norm, z_norm, **kwargs): """Return the radial (R) magnetic field at a point.""" is_structured = kwargs["is_structured"] r_norm = self.convert_length_to_normalized(r_norm) z_norm = self.convert_length_to_normalized(z_norm) # Evaluate the poloidal flux, taking the 0th derivative in x (R) # and the 1st derivative in y(Z). Then calculate the magnetic field # according to eq. 2.25 of [1]. # [1] H. Zohm, MHD stability of Tokamaks, p24 interp_dz = self.psi_interpolator( x=r_norm, y=z_norm, dx=0, dy=1, grid=is_structured ).T if not kwargs["dims"]: interp_dz = interp_dz.item() psi_dz = xr.DataArray( interp_dz, dims=kwargs["dims"], coords=kwargs["coords"] ) # NOTE: In the normalization, one factor of R0 comes from the gradient # of psi and one from the normalized R used in the formula. norm = self.axis_r**2 if not np.logical_and( isinstance(r_norm, xr.DataArray), isinstance(z_norm, xr.DataArray) ): norm = norm.values b_r = psi_dz / (2 * np.pi * r_norm * norm) b_r = -self.poloidal_field_factor * b_r return xr.DataArray(b_r / self.B0.magnitude, attrs={"norm": self.B0}, dims=kwargs["dims"], coords=kwargs["coords"])
[docs] @autogrid_method def magfield_component_z(self, r_norm, z_norm, **kwargs): """Return the vertical (Z) magnetic field at a point.""" is_structured = kwargs["is_structured"] r_norm = self.convert_length_to_normalized(r_norm) z_norm = self.convert_length_to_normalized(z_norm) # Evaluate the poloidal flux, taking the 1st derivative in x (R) # and the 0th derivative in y(Z). Then calculate the magnetic field # according to eq. 2.24 of [1]. # [1] H. Zohm, MHD stability of Tokamaks, p24 interp_dr = self.psi_interpolator( x=r_norm, y=z_norm, dx=1, dy=0, grid=is_structured ).T if not kwargs["dims"]: interp_dr = interp_dr.item() psi_dr = xr.DataArray( interp_dr, dims=kwargs["dims"], coords=kwargs["coords"] ) norm = self.axis_r**2 if not np.logical_and( isinstance(r_norm, xr.DataArray), isinstance(z_norm, xr.DataArray) ): norm = norm.values b_z = psi_dr / (2 * np.pi * r_norm * norm) b_z = self.poloidal_field_factor * b_z return xr.DataArray(b_z / self.B0.magnitude, attrs={"norm": self.B0}, dims=kwargs["dims"], coords=kwargs["coords"])
[docs] @autogrid_method def magfield_component_toroidal(self, r_norm, z_norm, **kwargs): """Return the toroidal (Phi) magnetic field at a point.""" if kwargs["is_structured"]: r_mesh, _ = np.meshgrid(r_norm, z_norm) else: r_mesh = r_norm r_mesh = self.convert_length_to_normalized(r_mesh) return xr.DataArray( self.axis_Btor_norm.item() / r_mesh, attrs={"norm": self.B0}, dims=kwargs["dims"], coords=kwargs["coords"] )
def _magfield_scalars(self, r: float, z: float, phi: float) -> tuple: """Return (b_r, b_z, b_t, jacobian) as plain floats for ODE hot path.""" if not hasattr(self, '_denom_factor'): self._denom_factor = ( 2.0 * np.pi * self.axis_r.item() ** 2 * self.B0.magnitude ) self._btor_norm = self.axis_Btor_norm.item() tck = self.psi_interpolator.tck self._tx = tck[0] self._ty = tck[1] self._c = tck[2] self._nx = len(self._tx) self._ny = len(self._ty) self._kx = self.psi_interpolator.degrees[0] self._ky = self.psi_interpolator.degrees[1] psi_dr, psi_dz = _eval_psi_derivs( r, z, self._tx, self._ty, self._c, self._nx, self._ny, self._kx, self._ky ) factor = self.poloidal_field_factor / (r * self._denom_factor) b_r = -factor * psi_dz b_z = factor * psi_dr b_t = self._btor_norm / r return b_r, b_z, b_t, r
[docs] def make_trace_eq(self): """Return cfunc address for numbalsoda field-line tracing.""" tx = self.psi_interpolator.tck[0] ty = self.psi_interpolator.tck[1] c = self.psi_interpolator.tck[2] nx = len(tx) ny = len(ty) kx = self.psi_interpolator.degrees[0] ky = self.psi_interpolator.degrees[1] denom_factor = ( 2.0 * np.pi * self.axis_r.item()**2 * self.B0.magnitude ) btor_norm = self.axis_Btor_norm.item() pol_factor = self.poloidal_field_factor @cfunc(lsoda_sig) def trace_eq(t, y, du, p): # pragma: no cover """Define trace equation for fast field-line tracer.""" r = y[0] z = y[1] psi_dr, psi_dz = _eval_psi_derivs(r, z, tx, ty, c, nx, ny, kx, ky) factor = pol_factor / (r * denom_factor) b_r = -factor * psi_dz b_z = factor * psi_dr r2_bt = r * r / btor_norm du[0] = b_r * r2_bt du[1] = b_z * r2_bt du[2] = np.sqrt(du[0] * du[0] + du[1] * du[1] + r * r) return trace_eq.address
[docs] def make_poloidal_trace_eq(self): r""" Return cfunc address for numbalsoda poloidal field-line integration. The ODE integrates over theta (0 to 2*pi); y[2] accumulates the symmetry angle so that $q = y[2] / (2\pi)$ after one full poloidal turn. The compiled cfunc address is cached on the instance so repeated calls to safety_factor do not trigger recompilation. """ if hasattr(self, '_poloidal_trace_addr'): return self._poloidal_trace_addr tx = self.psi_interpolator.tck[0] ty = self.psi_interpolator.tck[1] c = self.psi_interpolator.tck[2] nx = len(tx) ny = len(ty) kx = self.psi_interpolator.degrees[0] ky = self.psi_interpolator.degrees[1] denom_factor = ( 2.0 * np.pi * self.axis_r.item()**2 * self.B0.magnitude ) btor_norm = self.axis_Btor_norm.item() pol_factor = self.poloidal_field_factor r_ax = self.axis_r_norm.item() z_ax = self.axis_z_norm.item() @cfunc(lsoda_sig) def poloidal_trace_eq(t, y, du, p): # pragma: no cover r = y[0] z = y[1] psi_dr, psi_dz = _eval_psi_derivs(r, z, tx, ty, c, nx, ny, kx, ky) factor = pol_factor / (r * denom_factor) b_r = -factor * psi_dz b_z = factor * psi_dr dz = z - z_ax dr = r - r_ax theta = np.arctan2(dz, dr) radius = np.sqrt(dr * dr + dz * dz) b_along_fs = -b_r * np.sin(theta) + b_z * np.cos(theta) du[0] = radius * b_r / np.abs(b_along_fs) du[1] = radius * b_z / np.abs(b_along_fs) du[2] = btor_norm * np.abs(radius / b_along_fs / (r * r)) self._poloidal_trace_addr = poloidal_trace_eq.address return self._poloidal_trace_addr
[docs] def get_boundary_polygon(self): """Return the divertor polygon.""" return self.divertor_polygon
[docs] def get_limiter_polygon(self, quantile=0.999, npoints=1000, tolerance=1e-4): """ Return the limiter polygon. Defined as the last closed non-separatrix surface. With the quantile argument, one can set how close in psi the limiter should be located. """ # NOTE: We currently ignore flux surface wander integrator = MagneticFieldTracer(self, max_flux_surface_wander=1.0) R_0 = np.array(self.axis_r_norm) Z_0 = np.array(self.axis_z_norm) R_max = np.max(self.get_boundary_polygon().x_points) R_u = np.linspace(R_0, R_max, npoints) psi_u = self.psi_interpolator(x=R_u, y=Z_0, dx=0, dy=0, grid=False) psi_start = self._psi_axis \ + (self._psi_separatrix - self._psi_axis) * quantile R_start = interp1d(psi_u, R_u, kind="linear")(psi_start) poloidal_trace = integrator.poloidal_integration( r_initial=float(R_start), z_initial=float(Z_0), theta_max=2.0*np.pi, tolerance=tolerance) theta = np.linspace(0.0, 2.0*np.pi, npoints, endpoint=False) R, Z, _ = poloidal_trace.sol(theta) limiter = Polygon2D(R, Z) return limiter
[docs] def safety_factor(self, rloc, zloc, tolerance=1e-4, return_sol=False, theta_eval=None): """ Return the safety factor calculated at the given location. Uses a fast poloidal field-line trace via a compiled cfunc. The per-point traces are distributed over threads with dask; the lsoda cfunc releases the GIL, so this gives a real speed-up for a full q-profile. Optionally returns the raw lsoda solution array. Parameters ---------- rloc, zloc : array_like Normalized locations at which to evaluate the safety factor. tolerance : float, optional Relative and absolute tolerance passed to lsoda. return_sol : bool, optional If True, also return the interpolated field-line solutions. theta_eval : array_like, optional Poloidal angles at which to sample the trace when ``return_sol`` is True. Must be a strictly increasing 1D array spanning a full poloidal turn [0, 2*pi] (so q stays correct). Defaults to 200 points over [0, 2*pi]. Returns ------- numpy.ndarray or (numpy.ndarray, list) The safety factors, plus the solution callables if ``return_sol`` is True. """ rloc = np.atleast_1d(rloc) zloc = np.atleast_1d(zloc) assert rloc.size == zloc.size, "R and Z arrays must have same size!" if theta_eval is not None: theta_eval = np.asarray(theta_eval, dtype=float) if theta_eval.ndim != 1 or theta_eval.size < 2: raise ValueError( "theta_eval must be a 1D array-like with >= 2 points.") if np.any(np.diff(theta_eval) <= 0.0): raise ValueError("theta_eval must be strictly increasing.") if not (np.isclose(theta_eval[0], 0.0) and np.isclose(theta_eval[-1], 2.0 * np.pi)): raise ValueError( "theta_eval must span a full poloidal turn [0, 2*pi].") addr = self.make_poloidal_trace_eq() results = dask.compute(*[ dask.delayed(self._single_safety_factor)( addr, r, z, tolerance, return_sol, theta_eval) for r, z in zip(rloc, zloc) ], scheduler='threads') if return_sol: q_factors, sols = zip(*results) return np.array(q_factors), list(sols) return np.array(results)
def _single_safety_factor(self, addr, r, z, tolerance=1e-4, return_sol=False, theta_eval=None): """ Calculate the safety factor for a single location. Near the magnetic axis (rho < axis_rho_tol) the poloidal trace is singular, so q is evaluated on a small offset surface (at the tighter axis_trace_tol) to approximate the on-axis limit q0. Optionally returns a callable sol(theta) -> (3, N) array matching the scipy OdeSolution interface used by eqdsk export. When return_sol is True the trace is sampled at theta_eval (defaulting to 200 points over [0, 2*pi]). """ # The poloidal trace is singular at the magnetic axis (0/0 # right-hand side). Below axis_rho_tol we offset the start radially # by axis_offset in R (landing near rho ~= 4e-3, clear of the # rho <~ 2e-4 breakdown region) to approximate the on-axis limit q0. # The near-axis trace uses the tighter axis_trace_tol so the # offset's O(rho^2) bias (~1e-5), not integration error, floors q0. axis_rho_tol = 1e-3 axis_offset = 1e-3 axis_trace_tol = 1e-8 rho = self.normalized_flux_surface_label(r, z).item() if rho >= 1.0: return (np.nan, None) if return_sol else np.nan if rho < axis_rho_tol: warnings.warn( f"safety_factor evaluated at rho={rho:.2e} < {axis_rho_tol:.0e}" f" (near magnetic axis); tracing from an offset of " f"{axis_offset:.0e} in R to approximate the on-axis limit q0.", stacklevel=2, ) r = r + axis_offset tolerance = min(tolerance, axis_trace_tol) if return_sol: if theta_eval is None: theta_eval = np.linspace(0.0, 2.0 * np.pi, 200) usol, _ = lsoda(addr, np.array([r, z, 0.0]), theta_eval, rtol=tolerance, atol=tolerance) q_factor = usol[-1, 2] / (2.0 * np.pi) sol = interp1d(theta_eval, usol.T, kind='cubic') return (q_factor, sol) usol, _ = lsoda(addr, np.array([r, z, 0.0]), np.array([0.0, 2.0 * np.pi]), rtol=tolerance, atol=tolerance) return usol[-1, 2] / (2.0 * np.pi)
[docs] def gradshaf_star(self, r_norm, z_norm, component=None): """ Evaluate the Laplace star operator of the Grad-Shafranov equation. One can specify to evaluate the radial (component="R"), vertical (component="Z") or the total operator (None). """ assert any([component == y for y in (None, "R", "Z")]) if (component is None) or (component == "R"): # We use the product rule in the formulation to be able to utilize # the derivative from the interpolator dpsi = self.psi_interpolator(x=r_norm, y=z_norm, dx=1, dy=0, grid=False) d2psi = self.psi_interpolator(x=r_norm, y=z_norm, dx=2, dy=0, grid=False) res_R = d2psi - dpsi / r_norm if component == "R": return res_R res_Z = self.psi_interpolator(x=r_norm, y=z_norm, dx=0, dy=2, grid=False) if component == "Z": return res_Z return res_R + res_Z