Source code for struphy.kinetic_background.maxwellians

"Maxwellian (Gaussian) distributions in velocity space."

import copy
import logging
from inspect import signature
from typing import Callable

import cunumpy as xp

from struphy.fields_background.base import AxisymmMHDequilibrium, FluidEquilibriumWithB
from struphy.geometry.base import Domain
from struphy.initial.base import Perturbation
from struphy.io.options import LiteralOptions
from struphy.kinetic_background.base import Maxwellian

logger = logging.getLogger("struphy")


[docs] class Maxwellian3D(Maxwellian): r"""A :class:`~struphy.kinetic_background.base.Maxwellian` depending :math:`(\eta_1, \eta_2, \eta_3)` and on three (:math:`n=3`) Cartesian velocities. Parameters ---------- n, ui, vthi : tuple Moments of the Maxwellian as tuples. The first entry defines the background (float for constant background or callable), the second entry defines a Perturbation (can be None). uniform_on_disc : bool Whether the density n is uniform on the disc. """ def __init__( self, n: tuple[float | Callable, Perturbation] = (1.0, None), u1: tuple[float | Callable, Perturbation] = (0.0, None), u2: tuple[float | Callable, Perturbation] = (0.0, None), u3: tuple[float | Callable, Perturbation] = (0.0, None), vth1: tuple[float | Callable, Perturbation] = (1.0, None), vth2: tuple[float | Callable, Perturbation] = (1.0, None), vth3: tuple[float | Callable, Perturbation] = (1.0, None), uniform_on_disc: bool = False, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) self.check_maxw_params() # factors multiplied onto the defined moments n, u and vth (can be set via setter) self._moment_factors = { "n": 1.0, "u": [1.0, 1.0, 1.0], "vth": [1.0, 1.0, 1.0], } @property def vdim(self): """Dimension of the velocity space.""" return 3 @property def velocity_coords(self) -> LiteralOptions.VelocityCoordinates: """Velocity coordinates of the background.""" return "cartesian"
[docs] def velocity_jacobian_det(self, eta1, eta2, eta3, vx, vy, vz): """Jacobian determinant is 1 (Cartesian velocity coordinates). Input parameters should be slice of 2d numpy marker array. (i.e. *self.phasespace_coords) Parameters ---------- eta1, eta2, eta3 : array_like Logical evaluation points. vx, vy, vz : array_like Velocity evaluation points. Returns ------- out : array-like The Jacobian determinant evaluated at given logical coordinates. ------- """ assert eta1.ndim == eta2.ndim == eta3.ndim == 1 assert vx.ndim == vy.ndim == vz.ndim == 1 return 1.0 + 0 * eta1
@property def volume_form(self): """Boolean. True if the background is represented as a volume form (thus including the velocity Jacobian).""" return False @property def moment_factors(self): """Collection of factors multiplied onto the defined moments n, u, and vth.""" return self._moment_factors @moment_factors.setter def moment_factors(self, new_factors: dict): for kw, arg in new_factors.items(): assert kw in self._moment_factors, f"Unknown moment factor {kw}." if kw in {"u", "vth"}: assert len(arg) == 3 self._moment_factors[kw] = arg
[docs] def n(self, eta1, eta2, eta3): """Zero-th moment (density).""" out = self._evaluate_moment(eta1, eta2, eta3, name="n") return out * self.moment_factors["n"]
[docs] def u(self, eta1, eta2, eta3): """Mean velocities.""" out = [] out += [self._evaluate_moment(eta1, eta2, eta3, name="u1")] out += [self._evaluate_moment(eta1, eta2, eta3, name="u2")] out += [self._evaluate_moment(eta1, eta2, eta3, name="u3")] return [ou * mom_fac for ou, mom_fac in zip(out, self.moment_factors["u"])]
[docs] def vth(self, eta1, eta2, eta3): """Thermal velocities.""" out = [] out += [self._evaluate_moment(eta1, eta2, eta3, name="vth1")] out += [self._evaluate_moment(eta1, eta2, eta3, name="vth2")] out += [self._evaluate_moment(eta1, eta2, eta3, name="vth3")] return [ou * mom_fac for ou, mom_fac in zip(out, self.moment_factors["vth"])]
[docs] class GyroMaxwellian2D(Maxwellian): r"""A gyrotropic :class:`~struphy.kinetic_background.base.Maxwellian` depending on two velocities :math:`(v_\parallel, \mu)`, :math:`n=2`, where :math:`v_\parallel = \mathbf v \cdot \mathbf b_0` and :math:`\mu = v_\perp^2/(2B_0)` is the magnetic moment, with :math:`B_0` the background magnetic field strength. Parameters ---------- n, u_para, u_perp, vth_para, vth_perp : tuple Moments of the Maxwellian as tuples. The first entry defines the background (float for constant background or callable), the second entry defines a Perturbation (can be None). equil : FluidEquilibriumWithB Fluid background. volume_form : bool Whether to represent the Maxwellian as a volume form; if True it is multiplied by the Jacobian determinant |v_perp| of the polar coordinate transofrmation (default = False). B0: float | Callable Constant or callable background magnetic field strength (default = 2.0). uniform_on_disc : bool Whether the density n is uniform on the disc. """ def __init__( self, n: tuple[float | Callable, Perturbation] = (1.0, None), u_para: tuple[float | Callable, Perturbation] = (0.0, None), u_perp: tuple[float | Callable, Perturbation] = (0.0, None), vth_para: tuple[float | Callable, Perturbation] = (1.0, None), vth_perp: tuple[float | Callable, Perturbation] = (1.0, None), volume_form: bool = True, B0: float | Callable = 2.0, uniform_on_disc: bool = False, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) self.check_maxw_params() # volume form represenation self._volume_form = volume_form # factors multiplied onto the defined moments n, u and vth (can be set via setter) self._moment_factors = { "n": 1.0, "u": [1.0, 1.0], "vth": [1.0, 1.0], } @property def vdim(self): """Dimension of the velocity space.""" return 2 @property def velocity_coords(self) -> LiteralOptions.VelocityCoordinates: """Velocity coordinates of the background.""" return "vpara_mu"
[docs] def velocity_jacobian_det(self, eta1, eta2, eta3, v_para, mu): r"""Jacobian determinant of the velocity coordinate transformation to :math:`(v_\parallel, mu)`, is :math:`B_0`. Input parameters should be slice of 2d numpy marker array. (i.e. *self.phasespace_coords) Parameters ---------- eta1, eta2, eta3 : array_like Logical evaluation points. v_para, mu : array_like Parallel velocity and magnetic moment evaluation points. Returns ------- out : array-like The Jacobian determinant evaluated at given logical coordinates. ------- """ assert eta1.ndim == eta2.ndim == eta3.ndim == 1 assert v_para.ndim == mu.ndim == 1 B0 = self.params["B0"] if callable(B0): etas = xp.stack((eta1, eta2, eta3), axis=0) return B0(etas) return B0 + 0 * eta1
@property def volume_form(self) -> bool: """Boolean. True if the background is represented as a volume form (thus including the velocity Jacobian |v_perp|).""" return self._volume_form @property def moment_factors(self): """Collection of factors multiplied onto the defined moments n, u, and vth.""" return self._moment_factors @moment_factors.setter def moment_factors(self, new_factors: dict): for kw, arg in new_factors.items(): assert kw in self._moment_factors, f"Unknown moment factor {kw}." if kw in {"u", "vth"}: assert len(arg) == 2 self._moment_factors[kw] = arg
[docs] def n(self, eta1, eta2, eta3): """Zero-th moment (density).""" out = self._evaluate_moment(eta1, eta2, eta3, name="n") return out * self.moment_factors["n"]
[docs] def u(self, eta1, eta2, eta3): """Mean velocities.""" out = [] out += [self._evaluate_moment(eta1, eta2, eta3, name="u_para")] out += [self._evaluate_moment(eta1, eta2, eta3, name="u_perp")] return [ou * mom_fac for ou, mom_fac in zip(out, self.moment_factors["u"])]
[docs] def vth(self, eta1, eta2, eta3): """Thermal velocities.""" out = [] out += [self._evaluate_moment(eta1, eta2, eta3, name="vth_para")] out += [self._evaluate_moment(eta1, eta2, eta3, name="vth_perp")] return [ou * mom_fac for ou, mom_fac in zip(out, self.moment_factors["vth"])]
[docs] class GyroMaxwellian2Dvperp(Maxwellian): r"""A gyrotropic :class:`~struphy.kinetic_background.base.Maxwellian` depending on two velocities :math:`(v_\parallel, v_\perp)`, :math:`n=2`, where :math:`v_\parallel = \mathbf v \cdot \mathbf b_0` and :math:`v_\perp` is the radial component of a polar coordinate system perpendicular to the magentic direction :math:`\mathbf b_0`. Parameters ---------- n, u_para, u_perp, vth_para, vth_perp : tuple Moments of the Maxwellian as tuples. The first entry defines the background (float for constant background or callable), the second entry defines a Perturbation (can be None). equil : FluidEquilibriumWithB Fluid background. volume_form : bool Whether to represent the Maxwellian as a volume form; if True it is multiplied by the Jacobian determinant |v_perp| of the polar coordinate transofrmation (default = False). uniform_on_disc : bool Whether the density n is uniform on the disc (default = False). """ def __init__( self, n: tuple[float | Callable, Perturbation] = (1.0, None), u_para: tuple[float | Callable, Perturbation] = (0.0, None), u_perp: tuple[float | Callable, Perturbation] = (0.0, None), vth_para: tuple[float | Callable, Perturbation] = (1.0, None), vth_perp: tuple[float | Callable, Perturbation] = (1.0, None), equil: FluidEquilibriumWithB = None, volume_form: bool = True, uniform_on_disc: bool = False, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) self.check_maxw_params() # volume form represenation self._volume_form = volume_form self._equil = equil # factors multiplied onto the defined moments n, u and vth (can be set via setter) self._moment_factors = { "n": 1.0, "u": [1.0, 1.0], "vth": [1.0, 1.0], } @property def vdim(self): """Dimension of the velocity space.""" return 2 @property def velocity_coords(self) -> LiteralOptions.VelocityCoordinates: """Velocity coordinates of the background.""" return "vpara_vperp"
[docs] def velocity_jacobian_det(self, eta1, eta2, eta3, v_para, v_perp): r"""Jacobian determinant of the velocity coordinate transformation to :math:`(v_\parallel, v_\perp)`, is :math:`v_\perp`. Input parameters should be slice of 2d numpy marker array. (i.e. *self.phasespace_coords) Parameters ---------- eta1, eta2, eta3 : array_like Logical evaluation points. v_para, v_perp : array_like Parallel and perpendicular velocity evaluation points. Returns ------- out : array-like The Jacobian determinant evaluated at given logical coordinates. ------- """ assert eta1.ndim == eta2.ndim == eta3.ndim == 1 assert v_para.ndim == v_perp.ndim == 1 return v_perp
@property def volume_form(self) -> bool: """Boolean. True if the background is represented as a volume form (thus including the velocity Jacobian |v_perp|).""" return self._volume_form @property def equil(self) -> FluidEquilibriumWithB: """Fluid background with B-field.""" return self._equil @property def moment_factors(self): """Collection of factors multiplied onto the defined moments n, u, and vth.""" return self._moment_factors @moment_factors.setter def moment_factors(self, new_factors: dict): for kw, arg in new_factors.items(): assert kw in self._moment_factors, f"Unknown moment factor {kw}." if kw in {"u", "vth"}: assert len(arg) == 2 self._moment_factors[kw] = arg
[docs] def n(self, eta1, eta2, eta3): """Zero-th moment (density).""" out = self._evaluate_moment(eta1, eta2, eta3, name="n") return out * self.moment_factors["n"]
[docs] def u(self, eta1, eta2, eta3): """Mean velocities.""" out = [] out += [self._evaluate_moment(eta1, eta2, eta3, name="u_para")] out += [self._evaluate_moment(eta1, eta2, eta3, name="u_perp")] return [ou * mom_fac for ou, mom_fac in zip(out, self.moment_factors["u"])]
[docs] def vth(self, eta1, eta2, eta3): """Thermal velocities.""" out = [] out += [self._evaluate_moment(eta1, eta2, eta3, name="vth_para")] out += [self._evaluate_moment(eta1, eta2, eta3, name="vth_perp")] return [ou * mom_fac for ou, mom_fac in zip(out, self.moment_factors["vth"])]
[docs] class CanonicalMaxwellian2D(GyroMaxwellian2D): r"""Canonical Maxwellian distribution function in :math:`(\eta_1, \eta_2, \eta_3, v_\parallel, \mu)` coordinates. Uses caching for evaluation of the canonical toroidal momentum in these coordinates. The distribution is parameterized by the density and thermal speed as functions of the canonical toroidal momentum :math:`\psi_c`: .. math:: \psi_c = \psi + \frac{m_s F}{q_s B}v_\parallel - \text{sign}(v_\parallel)\sqrt{2(\epsilon - \mu B)}\frac{m_sF}{q_sB} \mathcal{H}(\epsilon - \mu B), - Energy .. math:: \epsilon = \frac{1}{2}m_sv_\parallel² + \mu B, - Magnetic moment .. math:: \mu = \frac{m_s v_\perp²}{2B}, where :math:`\psi` is the poloidal magnetic flux function, :math:`F=F(\psi)` is the poloidal current function and :math:`\mathcal{H}` is the Heaviside function. With the three constants of motion, a canonical Maxwellian distribution function is defined as .. math:: F(\psi_c, \epsilon, \mu) = \frac{n(\psi_c)}{(2\pi)^{3/2}v_\text{th}³(\psi_c)} \text{exp}\left[ - \frac{\epsilon}{v_\text{th}²(\psi_c)}\right]. Parameters ---------- n, vth : tuple Moments of the canonical Maxwellian as tuples. The first entry defines the background (float for constant background or callable), the second entry defines a Perturbation (can be None). maxw_params : dict Parameters for the kinetic background. vth : tuple[float | Callable, Perturbation] Thermal-speed background and optional perturbation. equil : AxisymmMHDequilibrium, optional Fluid equilibrium used to evaluate background profiles in the magnetic geometry. volume_form : bool, default=True If ``True``, represent the distribution as a volume form and include the appropriate velocity-space Jacobian when evaluating it. cache_size : int, optional Number of rows in the cache buffer for :math:`\psi_c` evaluation. If ``None``, no caching is used. Must be able to accomodate all markers on the current process. shifted : bool, default=True Whether to add the shift :math:`-\text{sign}(v_\parallel)\sqrt{2(\epsilon - \mu B)} \frac{m_s F}{q_s B} \mathcal{H}(\epsilon - \mu B)` (with the on-axis field :math:`B`) to the canonical toroidal momentum, such that the profiles :math:`n(\psi_c)`, :math:`v_\text{th}(\psi_c)` are close to the profiles in the flux label of the position. The shift is not a constant of motion for the trapped particles that bounce where the field is larger than on the axis (the sign of :math:`v_\parallel` changes at the bounce points), so the distribution is then not an exact equilibrium of the guiding-center motion: with steep profiles, use ``shifted=False`` (the exact canonical toroidal momentum :math:`\psi + \frac{m_s F}{q_s B}v_\parallel`, whose profiles are shifted by the drift-orbit width with respect to the position). """ def __init__( self, n: tuple[float | Callable, Perturbation] = (1.0, None), vth: tuple[float | Callable, Perturbation] = (1.0, None), volume_form: bool = True, uniform_on_disc: bool = False, equil: AxisymmMHDequilibrium = None, epsilon: float = 1.0, cache_size: int | None = None, shifted: bool = True, ): assert isinstance(equil, AxisymmMHDequilibrium) super().__init__( n=n, u_para=(0.0, None), u_perp=(0.0, None), vth_para=vth, vth_perp=vth, volume_form=volume_form, B0=equil.absB0, uniform_on_disc=uniform_on_disc, ) # store additional parameters self._equil = equil self._epsilon = epsilon self._shifted = shifted self.params["vth"] = vth self.params["equil"] = equil self.params["epsilon"] = epsilon self.params["cache_size"] = cache_size self.params["shifted"] = shifted # factors multiplied onto the defined moments n and vth (can be set via setter) self._moment_factors = { "n": 1.0, "vth": 1.0, } # create cache for psi_c evaluation if cache_size is not None: self.cbufs = {} self.cbufs["absB0"] = xp.empty(cache_size, dtype=float) self.cbufs["x"] = xp.empty(cache_size, dtype=float) self.cbufs["y"] = xp.empty(cache_size, dtype=float) self.cbufs["z"] = xp.empty(cache_size, dtype=float) self.cbufs["R"] = xp.empty(cache_size, dtype=float) self.cbufs["P"] = xp.empty(cache_size, dtype=float) self.cbufs["Z"] = xp.empty(cache_size, dtype=float) self.cbufs["psi"] = xp.empty(cache_size, dtype=float) self.cbufs["energy"] = xp.empty(cache_size, dtype=float) self.cbufs["psic"] = xp.empty(cache_size, dtype=float) self.cbufs["positive_mask"] = xp.empty(cache_size, dtype=bool) self.cbufs["correction"] = xp.empty(cache_size, dtype=float) logger.debug(f"Created {len(self.cbufs)} cache buffers for psi_c evaluation, each with size {cache_size}.") else: self.cbufs = None @property def equil(self) -> AxisymmMHDequilibrium: """One of :mod:`~struphy.fields_background.equils` in case that moments are to be set in that way, None otherwise. """ return self._equil @property def epsilon(self) -> float: """Epsilon parameter in the canonical toroidal momentum.""" return self._epsilon @GyroMaxwellian2D.moment_factors.setter def moment_factors(self, new_factors: dict): # n and vth are scalar factors here (no u) for kw, arg in new_factors.items(): assert kw in self._moment_factors, f"Unknown moment factor {kw}." self._moment_factors[kw] = arg def _evaluate_moment( self, eta1, eta2, eta3, v_parallel, mu, *, name: str = "n", add_perturbation: bool = None, ): """Scalar moment evaluation as background + perturbation. Incontrast to standard Maxwellians, here the moments are evaluated at the phase space coordinates. Parameters ---------- eta1, eta2, eta3, v_parallel, mu : numpy.arrays Phase space evaluation points. All arrays must be of same shape (can be 1d for flat evaluation). name : str Which moment to evaluate (see varaible "dct" below). add_perturbation : bool | None Whether to add the perturbation defined in params. If None, is taken from self.add_perturbation. Returns ------- A float (background value) or a numpy.array of the evaluated scalar moment. """ # collect arguments assert isinstance(eta1, xp.ndarray) assert isinstance(eta2, xp.ndarray) assert isinstance(eta3, xp.ndarray) assert isinstance(v_parallel, xp.ndarray) assert isinstance(mu, xp.ndarray) assert eta1.shape == eta2.shape == eta3.shape == v_parallel.shape == mu.shape params = self.params[name] assert isinstance(params, tuple) assert len(params) == 2 # flat evaluation for markers (one column per marker) if eta1.ndim == 1: coords = [xp.stack((eta1, eta2, eta3, v_parallel, mu), axis=0)] # assuming that input comes from meshgrid. elif eta1.ndim == 5: coords = (eta1, eta2, eta3, v_parallel, mu) else: raise ValueError(f"Input arrays must be 1d or 5d (from meshgrid), but got {eta1.ndim}d.") # initialize output if eta1.ndim == 1: out = 0.0 * eta1 else: out = 0.0 * coords[0] # evaluate background background = params[0] if isinstance(background, (float, int)): out += background else: assert callable(background) sig = signature(background) assert len(sig.parameters) == 1, ( f"Background function {background} must take one argument (psi_c), but takes {len(sig.parameters)}." ) cached = self._check_psi_c_cached(*coords) logger.debug(f"{'Using cached psi_c' if cached else 'Evaluating psi_c'} for background evaluation.") if not cached: self.psi_c = self.eval_psic(*coords) out += background(self.psi_c) # add perturbation if add_perturbation is None: add_perturbation = self.add_perturbation perturbation = params[1] if perturbation is not None and add_perturbation: assert isinstance(perturbation, Perturbation) if eta1.ndim == 1: out += perturbation(eta1, eta2, eta3) else: raise NotImplementedError("Perturbation evaluation for meshgrid input not implemented yet.") # uniform density on disc (n=2 eta_1) if name == "n" and self.params.get("uniform_on_disc", False): if eta1.ndim == 1: out *= 2.0 * eta1 else: out *= 2.0 * coords[0] return out def _check_psi_c_cached(self, *coords): """Check if psi_c has been cached for the given coordinates.""" cached = False if getattr(self, "psi_c", None) is not None: # markers: coords[0] is (5, n_markers) and psi_c is (n_markers,); meshgrid: all have the same 5d shape shape = coords[0].shape[1:] if len(coords) == 1 else coords[0].shape if self.psi_c.shape == shape and self.psi_c.size > 0: # compare the cached values with a fresh evaluation at a few test points size = self.psi_c.size test_idx = xp.unique(xp.array([0, size // 3, 2 * size // 3, size - 1])) if len(coords) == 1: test_coords = coords[0][:, test_idx] else: test_coords = xp.stack([c.reshape(-1)[test_idx] for c in coords], axis=0) # do not use the cache buffers here, self.psi_c may point to them cached_psi_c = self.eval_psic(test_coords, use_cbufs=False) cached = xp.allclose(self.psi_c.reshape(-1)[test_idx], cached_psi_c) return cached
[docs] def eval_psic(self, *coords, use_cbufs: bool = True): r"""Shifted canonical toroidal momentum evaluated at given particle positions and velocities. If ``use_cbufs`` is ``False``, the cache buffers are not used (and not overwritten) for marker evaluation.""" cbufs = self.cbufs if use_cbufs else None B0 = self.equil.params["B0"] R0 = self.equil.params["R0"] if len(coords) == 1: if use_cbufs and self.cbufs is None: logger.warning( f"Initialize {self.__class__.__name__} with `cache_size` for faster psi_c evaluation for markers!" ) etas = coords[0][:3] # these are views (no mem allocation) vparallel = coords[0][3] mu = coords[0][4] n_markers = etas.shape[1] if cbufs is None: absB0 = self.equil.absB0(etas) x, y, z = self.equil.domain(etas) else: absB0 = cbufs["absB0"][:n_markers] x = cbufs["x"][:n_markers] y = cbufs["y"][:n_markers] z = cbufs["z"][:n_markers] absB0[:] = self.equil.absB0(etas) x[:], y[:], z[:] = self.equil.domain(etas) else: assert len(coords) == 5 assert coords[0].ndim == coords[1].ndim == coords[2].ndim == coords[3].ndim == coords[4].ndim == 5 eta1 = coords[0][:, :, :, 0, 0] eta2 = coords[1][:, :, :, 0, 0] eta3 = coords[2][:, :, :, 0, 0] etas = (eta1, eta2, eta3) absB0 = self.equil.absB0(*etas)[:, :, :, None, None] x, y, z = self.equil.domain(*etas) vparallel = coords[3] mu = coords[4] if cbufs is None or len(coords) != 1: R, P, Z = self.equil.inverse_map(x, y, z) psi = self.equil.psi(R, Z) if len(coords) != 1: psi = psi[:, :, :, None, None] energy = 1 / 2 * vparallel**2 + mu * absB0 psi_c = psi - self._epsilon * B0 * R0 / absB0 * vparallel if self._shifted: positive_mask = (energy - mu * B0) > 0 correction = xp.zeros_like(psi_c) correction[positive_mask] = ( self._epsilon * xp.sign(vparallel[positive_mask]) * xp.sqrt(2 * (energy[positive_mask] - mu[positive_mask] * B0)) * R0 ) psi_c += correction else: R = cbufs["R"][:n_markers] P = cbufs["P"][:n_markers] Z = cbufs["Z"][:n_markers] psi = cbufs["psi"][:n_markers] energy = cbufs["energy"][:n_markers] psi_c = cbufs["psic"][:n_markers] positive_mask = cbufs["positive_mask"][:n_markers] correction = cbufs["correction"][:n_markers] R[:], P[:], Z[:] = self.equil.inverse_map(x, y, z) psi[:] = self.equil.psi(R, Z) energy[:] = 1 / 2 * vparallel**2 + mu * absB0 psi_c[:] = psi - self._epsilon * B0 * R0 / absB0 * vparallel if self._shifted: positive_mask[:] = (energy - mu * B0) > 0 correction[:] = 0.0 correction[positive_mask] = ( self._epsilon * xp.sign(vparallel[positive_mask]) * xp.sqrt(2 * (energy[positive_mask] - mu[positive_mask] * B0)) * R0 ) psi_c[:] += correction return psi_c
[docs] def eval_rc(self, eta1, eta2, eta3, vparallel, mu): r""" Square root of radially normalized canonical toroidal momentum. .. math:: \begin{aligned} r_c^2 &= \frac{\psi_c - \psi_\text{axis}}{\psi_\text{edge} - \psi_\text{axis}} \,, \\[3mm] r_c &= \begin{cases} \sqrt{\frac{\psi_c - \psi_\text{axis}}{\psi_\text{edge} - \psi_\text{axis}}} & \text{if} \quad \frac{\psi_c - \psi_\text{axis}}{\psi_\text{edge} - \psi_\text{axis}} \geq 0 \,, \\ -\sqrt{\frac{\psi_c - \psi_\text{axis}}{\psi_\text{edge} - \psi_\text{axis}}} & \text{if} \quad \frac{\psi_c - \psi_\text{axis}}{\psi_\text{edge} - \psi_\text{axis}} < 0 \,, \end{cases} \end{aligned} where :math:`\psi_\text{axis}` and :math:`\psi_\text{edge}` are poloidal magnetic flux function at the center and edge of poloidal plane respectively. """ # calculate psic psic = self.eval_psic(eta1, eta2, eta3, vparallel, mu) # calculate rc² rc_squared = (psic - self.equil.psi_range[0]) / (self.equil.psi_range[1] - self.equil.psi_range[0]) # sorting out indices of negative rc² neg_index = xp.logical_not(rc_squared >= 0) # make them positive rc_squared[neg_index] *= -1 # calculate rc rc = xp.sqrt(rc_squared) rc[neg_index] *= -1 return rc
[docs] def n(self, eta1, eta2, eta3, vparallel, mu): """Zero-th moment (density).""" out = self._evaluate_moment(eta1, eta2, eta3, vparallel, mu, name="n") return out * self.moment_factors["n"]
[docs] def u(self, eta1, eta2, eta3, vparallel, mu): """Mean velocities (zero for the canonical Maxwellian).""" return [0.0 * eta1, 0.0 * eta1]
[docs] def vth(self, eta1, eta2, eta3, vparallel, mu): """Thermal velocities.""" out = self._evaluate_moment(eta1, eta2, eta3, vparallel, mu, name="vth") out = out * self.moment_factors["vth"] return [out, out]
[docs] class ColdPlasma(Maxwellian): r"""Base class for a distribution as a Dirac-delta in velocity (vth = 0). The __call__ method returns the density evaluation."""
[docs] @classmethod def default_maxw_params(cls): """Default parameters dictionary defining the constant value of the constant background.""" return { "n": 5.0, "u1": 0.0, "u2": 0.0, "u3": 0.0, "vth1": 0.0, "vth2": 0.0, "vth3": 0.0, }
def __init__( self, n: tuple[float | Callable, Perturbation] = (1.0, None), u1: tuple[float | Callable, Perturbation] = (0.0, None), u2: tuple[float | Callable, Perturbation] = (0.0, None), u3: tuple[float | Callable, Perturbation] = (0.0, None), equil: FluidEquilibriumWithB = None, uniform_on_disc: bool = False, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) self._params["vth1"] = (0.0, None) self._params["vth2"] = (0.0, None) self._params["vth3"] = (0.0, None) self.check_maxw_params() self._equil = equil @property def vdim(self): """Dimension of the velocity space (vdim = 0).""" return 0 @property def velocity_coords(self) -> LiteralOptions.VelocityCoordinates: """Velocity coordinates of the background.""" return None @property def volume_form(self): """Boolean. True if the background is represented as a volume form (thus including the velocity Jacobian).""" return False @property def equil(self) -> FluidEquilibriumWithB: """Fluid background with B-field.""" return self._equil
[docs] def velocity_jacobian_det(self, eta1, eta2, eta3, *v): """Jacobian determinant of the velocity coordinate transformation.""" return 1.0 + 0.0 * eta1
[docs] def n(self, eta1, eta2, eta3): """Zero-th moment (density).""" out = self._evaluate_moment(eta1, eta2, eta3, name="n") return out
[docs] def u(self, eta1, eta2, eta3): """Mean velocities.""" out = [] out += [self._evaluate_moment(eta1, eta2, eta3, name="u1")] out += [self._evaluate_moment(eta1, eta2, eta3, name="u2")] out += [self._evaluate_moment(eta1, eta2, eta3, name="u3")] return out
[docs] def vth(self, eta1, eta2, eta3): """Thermal velocities (are zero here, see __init__).""" out = [] out += [self._evaluate_moment(eta1, eta2, eta3, name="vth1")] out += [self._evaluate_moment(eta1, eta2, eta3, name="vth2")] out += [self._evaluate_moment(eta1, eta2, eta3, name="vth3")] return out
def __call__(self, eta1, eta2, eta3): return self.n(eta1, eta2, eta3)