Source code for struphy.initial.perturbations

#!/usr/bin/env python3
"Analytical perturbations."

import copy
import logging
from dataclasses import dataclass

import cunumpy as xp
import scipy
import scipy.special

from struphy.initial.base import Perturbation
from struphy.io.options import LiteralOptions
from struphy.utils.utils import check_option

logger = logging.getLogger("struphy")


[docs] @dataclass class Noise(Perturbation): """White noise for FEEC coefficients. Parameters ---------- direction: str The direction(s) of variation of the noise: 'e1', 'e2', 'e3', 'e1e2', etc. amp: float Noise amplitude. seed: int Seed for the random number generator. """ direction: LiteralOptions.NoiseDirections = "e3" amp: float = 0.0001 seed: int = None comp: int = 0 given_in_basis: LiteralOptions.GivenInBasis = None def __post_init__( self, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) check_option(self.direction, LiteralOptions.NoiseDirections) def __call__(self): pass
[docs] class ModesSin(Perturbation): r"""Sinusoidal function in 3D. .. math:: u(x, y, z) = \sum_{s} \chi_s(z) A_s \sin \left(l_s \frac{2\pi}{L_x} x + m_s \frac{2\pi}{L_y} y + n_s \frac{2\pi}{L_z} z + \theta_s \right) \,. where :math:`\chi_s(z)` is one of .. math:: \chi_s(z) = \left\{ \begin{aligned} 1\,, \\[2mm] \tanh((z - 0.5)/\delta)/\cosh((z - 0.5)/\delta)\,, \end{aligned} \right. Can be used in logical space (use 'given_in_basis'), where :math:`x \to \eta_1,\, y\to \eta_2,\, z \to \eta_3` and :math:`L_x=L_y=L_z=1.0` (default). Parameters ---------- ls : tuple[int] Mode numbers in x-direction (kx = l*2*pi/Lx). ms : tuple[int] Mode numbers in y-direction (ky = m*2*pi/Ly). ns : tuple[int] Mode numbers in z-direction (kz = n*2*pi/Lz). amps : tuple[float] Amplitude of each mode. theta : tuple | list Phase of each mode. pfuns : tuple | list[str] "Id" or "localize" define the profile functions. localize multiply the sinus by :math: `tanh((\eta_3 - 0.5)/\delta)/cosh((\eta_3 - 0.5)/\delta)` to localize it around 0.5. :math: `\delta` is given by the input parameter pfuns_params pfuns_params : tuple | list The parameter needed by the profile function Lx, Ly, Lz : float Domain lengths. given_in_basis : str In which basis the perturbation is represented (see base class). comp : int Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) """ def __init__( self, ls: tuple[int] = None, ms: tuple[int] = None, ns: tuple[int] = None, amps: tuple[float] = (1e-4,), theta: tuple[float] = None, pfuns=("Id",), pfuns_params=(0.0,), Lx=1.0, Ly=1.0, Lz=1.0, given_in_basis: LiteralOptions.GivenInBasis = None, comp: int = 0, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) if ls is not None: n_modes = len(ls) elif ms is not None: n_modes = len(ms) ls = [0] * n_modes elif ns is not None: n_modes = len(ns) ls = [0] * n_modes ms = [0] * n_modes else: n_modes = 1 ls = [0] ms = [0] ns = [0] if ms is None: ms = [0] * n_modes else: assert len(ms) == n_modes if ns is None: ns = [0] * n_modes else: assert len(ns) == n_modes if len(amps) == 1: amps = [amps[0]] * n_modes else: assert len(amps) == n_modes if theta is None: theta = [0] * n_modes if len(theta) == 1: theta = [theta[0]] * n_modes else: assert len(theta) == n_modes if len(pfuns) == 1: pfuns = [pfuns[0]] * n_modes else: assert len(pfuns) == n_modes if len(pfuns_params) == 1: pfuns_params = [pfuns_params[0]] * n_modes else: assert len(pfuns_params) == n_modes self.pfuns = [] for pfun, params in zip(pfuns, pfuns_params): if pfun == "Id": self.pfuns += [lambda eta3: 1.0] elif pfun == "localize": self.pfuns += [ lambda eta3: xp.tanh((eta3 - 0.5) / params) / xp.cosh((eta3 - 0.5) / params), ] else: raise ValueError(f"Profile function {pfun} is not defined..") self.ls = tuple(ls) self.ms = tuple(ms) self.ns = tuple(ns) self.amps = tuple(amps) self.Lx = Lx self.Ly = Ly self.Lz = Lz self.theta = tuple(theta) self.pfuns = tuple(self.pfuns) # use the setters self.given_in_basis = given_in_basis self.comp = comp def __call__(self, x, y, z): val = 0.0 for amp, l, m, n, t, pfun in zip(self.amps, self.ls, self.ms, self.ns, self.theta, self.pfuns): val += ( amp * pfun(z) * xp.sin( l * 2.0 * xp.pi / self.Lx * x + m * 2.0 * xp.pi / self.Ly * y + n * 2.0 * xp.pi / self.Lz * z + t, ) ) return val
[docs] class ModesCos(Perturbation): r"""Cosinusoidal function in 3D. .. math:: u(x, y, z) = \sum_{s} A_s \cos \left(l_s \frac{2\pi}{L_x} x + m_s \frac{2\pi}{L_y} y + n_s \frac{2\pi}{L_z} z \right) \,. Can be used in logical space (use 'given_in_basis'), where :math:`x \to \eta_1,\, y\to \eta_2,\, z \to \eta_3` and :math:`L_x=L_y=L_z=1.0` (default). Parameters ---------- ls : tuple[int] Mode numbers in x-direction (kx = l*2*pi/Lx). ms : tuple[int] Mode numbers in y-direction (ky = m*2*pi/Ly). ns : tuple[int] Mode numbers in z-direction (kz = n*2*pi/Lz). amps : tuple[float] Amplitude of each mode. Lx, Ly, Lz : float Domain lengths. given_in_basis : str In which basis the perturbation is represented (see base class). comp : int Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) perb_domain : tuple[tuple[float]] Subdomain in which the pertrubation is applied to: ((x_min, x_max), (y_min, y_max), (z_min, z_max)). None means apply perturbation to all domain in that direction """ def __init__( self, ls: tuple[int] = None, ms: tuple[int] = None, ns: tuple[int] = None, amps: tuple[float] = (1e-4,), Lx=1.0, Ly=1.0, Lz=1.0, given_in_basis: LiteralOptions.GivenInBasis = None, comp: int = 0, perb_domain: tuple[tuple[float]] = (None, None, None), ): # use setter to store input parameters self.params = copy.deepcopy(locals()) if ls is not None: n_modes = len(ls) elif ms is not None: n_modes = len(ms) ls = [0] * n_modes elif ns is not None: n_modes = len(ns) ls = [0] * n_modes ms = [0] * n_modes else: n_modes = 1 ls = [0] ms = [0] ns = [0] if ms is None: ms = [0] * n_modes else: assert len(ms) == n_modes if ns is None: ns = [0] * n_modes else: assert len(ns) == n_modes if len(amps) == 1: amps = [amps[0]] * n_modes else: assert len(amps) == n_modes self.ls = tuple(ls) self.ms = tuple(ms) self.ns = tuple(ns) self.amps = tuple(amps) self.Lx = Lx self.Ly = Ly self.Lz = Lz # use the setters self.given_in_basis = given_in_basis self.comp = comp self.perb_domain = perb_domain def __call__(self, x, y, z): if self.perb_domain != (None, None, None): val = xp.zeros_like(x) # find mask of particles within the sub domain mask = super()._mask_subdomain(x, y, z, perb_domain=self.perb_domain) # apply perturbation iff perb_domain not specified or (x,y,z) is within perb_domain for amp, l, m, n in zip(self.amps, self.ls, self.ms, self.ns): val[mask] += amp * xp.cos( l * 2.0 * xp.pi / self.Lx * x[mask] + m * 2.0 * xp.pi / self.Ly * y[mask] + n * 2.0 * xp.pi / self.Lz * z[mask], ) else: val = 0.0 for amp, l, m, n in zip(self.amps, self.ls, self.ms, self.ns): val += amp * xp.cos( l * 2.0 * xp.pi / self.Lx * x + m * 2.0 * xp.pi / self.Ly * y + n * 2.0 * xp.pi / self.Lz * z, ) # logger.info( "Cos max value", val.max()) return val
[docs] class CoaxialWaveguideElectric_r(Perturbation): r"""Initializes function for Coaxial Waveguide electric field in radial direction. Solutions taken from TUM master thesis of Alicia Robles Pérez: "Development of a Geometric Particle-in-Cell Method for Cylindrical Coordinate Systems", 2024 Parameters ---------- m : int Number of Modes a1, a2 : float inner and outer radius of Hollow Cylinder a, b : float Parameters of Electric field """ def __init__(self, m=1, a1=1.0, a2=2.0, a=1, b=-0.28): # use setter to store input parameters self.params = copy.deepcopy(locals()) self._m = m self._r1 = a1 self._r2 = a2 self._a = a self._b = b # use the setters self.given_in_basis = "norm" self.comp = 0 def __call__(self, eta1, eta2, eta3): val = 0.0 r = eta1 * (self._r2 - self._r1) + self._r1 theta = eta2 * 2.0 * xp.pi val += ( -self._m / r * xp.cos(self._m * theta) * (self._a * scipy.special.jv(self._m, r) + self._b * scipy.special.yn(self._m, r)) ) return val
[docs] class CoaxialWaveguideElectric_theta(Perturbation): r""" Initializes funtion for Coaxial Waveguide electric field in the azimuthal direction. Solutions taken from TUM master thesis of Alicia Robles Pérez: "Development of a Geometric Particle-in-Cell Method for Cylindrical Coordinate Systems", 2024 Parameters ---------- m : int Number of Modes a1, a2 : float inner and outer radius of Hollow Cylinder a, b : float Parameters of Electric field """ def __init__(self, m=1, a1=1.0, a2=2.0, a=1, b=-0.28): # use setter to store input parameters self.params = copy.deepcopy(locals()) self._m = m self._r1 = a1 self._r2 = a2 self._a = a self._b = b # use the setters self.given_in_basis = "norm" self.comp = 1 def __call__(self, eta1, eta2, eta3): val = 0.0 r = eta1 * (self._r2 - self._r1) + self._r1 theta = eta2 * 2.0 * xp.pi val += ( self._a * ((self._m / r) * scipy.special.jv(self._m, r) - scipy.special.jv(self._m + 1, r)) + (self._b * ((self._m / r) * scipy.special.yn(self._m, r) - scipy.special.yn(self._m + 1, r))) ) * xp.sin(self._m * theta) return val
[docs] class CoaxialWaveguideMagnetic(Perturbation): r"""Initializes funtion for Coaxial Waveguide magnetic field in $z$-direction. Solutions taken from TUM master thesis of Alicia Robles Pérez: "Development of a Geometric Particle-in-Cell Method for Cylindrical Coordinate Systems", 2024 Parameters ---------- m : int Number of Modes a1, a2 : float inner and outer radius of Hollow Cylinder a, b : float Parameters of Electric field """ def __init__(self, m=1, a1=1.0, a2=2.0, a=1, b=-0.28): # use setter to store input parameters self.params = copy.deepcopy(locals()) self._m = m self._r1 = a1 self._r2 = a2 self._a = a self._b = b # use the setters self.given_in_basis = "norm" self.comp = 2 def __call__(self, eta1, eta2, eta3): val = 0.0 r = eta1 * (self._r2 - self._r1) + self._r1 theta = eta2 * 2.0 * xp.pi z = eta3 val += (self._a * scipy.special.jv(self._m, r) + self._b * scipy.special.yn(self._m, r)) * xp.cos( self._m * theta, ) return val
[docs] class ModesCosCos(Perturbation): r""" .. math:: u(x, y, z) = \sum_s A_s \, \chi_s(z) \cos \!\left(l_s \tfrac{2\pi}{L_x} x + \theta_{x,s}\right) \cos \!\left(m_s \tfrac{2\pi}{L_y} y + \theta_{y,s}\right) where :math:`\chi_s(z)` can be either 1 or localized in z. """ def __init__( self, ls=None, ms=None, amps=(1e-4,), theta_x=None, theta_y=None, pfuns=("Id",), pfuns_params=(0.0,), Lx=1.0, Ly=1.0, Lz=1.0, given_in_basis: LiteralOptions.GivenInBasis = None, comp: int = 0, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) if ls is not None: n_modes = len(ls) elif ms is not None: n_modes = len(ms) ls = [0] * n_modes else: n_modes = 1 ls = [0] ms = [0] if ms is None: ms = [0] * n_modes else: assert len(ms) == n_modes if len(amps) == 1: amps = [amps[0]] * n_modes else: assert len(amps) == n_modes if theta_x is None: theta_x = [0] * n_modes elif len(theta_x) == 1: theta_x = [theta_x[0]] * n_modes else: assert len(theta_x) == n_modes if theta_y is None: theta_y = [0] * n_modes elif len(theta_y) == 1: theta_y = [theta_y[0]] * n_modes else: assert len(theta_y) == n_modes if len(pfuns) == 1: pfuns = [pfuns[0]] * n_modes else: assert len(pfuns) == n_modes if len(pfuns_params) == 1: pfuns_params = [pfuns_params[0]] * n_modes else: assert len(pfuns_params) == n_modes self._ls = ls self._ms = ms self._amps = amps self._theta_x = theta_x self._theta_y = theta_y self._Lx = Lx self._Ly = Ly self._Lz = Lz self._pfuns = [] for pfun, params in zip(pfuns, pfuns_params): if pfun == "Id": self._pfuns += [lambda z: 1.0] elif pfun == "localize": self._pfuns += [lambda z, p=params: xp.tanh((z - 0.5) / p) / xp.cosh((z - 0.5) / p)] else: raise ValueError(f"Profile function {pfun} is not defined..") # use the setters self.given_in_basis = given_in_basis self.comp = comp def __call__(self, x, y, z): val = 0.0 for amp, l, m, thx, thy, pfun in zip(self._amps, self._ls, self._ms, self._theta_x, self._theta_y, self._pfuns): val += ( amp * pfun(z) * xp.cos(l * 2.0 * xp.pi / self._Lx * x + thx) * xp.cos(m * 2.0 * xp.pi / self._Ly * y + thy) ) return val
[docs] class ModesSinSin(Perturbation): r""" .. math:: u(x, y, z) = \sum_s A_s \, \chi_s(z) \sin \!\left(l_s \tfrac{2\pi}{L_x} x + \theta_{x,s}\right) \sin \!\left(m_s \tfrac{2\pi}{L_y} y + \theta_{y,s}\right) where :math:`\chi_s(z)` can be either 1 or localized in z. """ def __init__( self, ls=None, ms=None, amps=(1e-4,), theta_x=None, theta_y=None, pfuns=("Id",), pfuns_params=(0.0,), Lx=1.0, Ly=1.0, Lz=1.0, given_in_basis: LiteralOptions.GivenInBasis = None, comp: int = 0, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) if ls is not None: n_modes = len(ls) elif ms is not None: n_modes = len(ms) ls = [0] * n_modes else: n_modes = 1 ls = [0] ms = [0] if ms is None: ms = [0] * n_modes else: assert len(ms) == n_modes if len(amps) == 1: amps = [amps[0]] * n_modes else: assert len(amps) == n_modes if theta_x is None: theta_x = [0] * n_modes elif len(theta_x) == 1: theta_x = [theta_x[0]] * n_modes else: assert len(theta_x) == n_modes if theta_y is None: theta_y = [0] * n_modes elif len(theta_y) == 1: theta_y = [theta_y[0]] * n_modes else: assert len(theta_y) == n_modes if len(pfuns) == 1: pfuns = [pfuns[0]] * n_modes else: assert len(pfuns) == n_modes if len(pfuns_params) == 1: pfuns_params = [pfuns_params[0]] * n_modes else: assert len(pfuns_params) == n_modes self._ls = ls self._ms = ms self._amps = amps self._theta_x = theta_x self._theta_y = theta_y self._Lx = Lx self._Ly = Ly self._Lz = Lz self._pfuns = [] for pfun, params in zip(pfuns, pfuns_params): if pfun == "Id": self._pfuns += [lambda z: 1.0] elif pfun == "localize": self._pfuns += [lambda z, p=params: xp.tanh((z - 0.5) / p) / xp.cosh((z - 0.5) / p)] else: raise ValueError(f"Profile function {pfun} is not defined..") # use the setters self.given_in_basis = given_in_basis self.comp = comp def __call__(self, x, y, z): val = 0.0 for amp, l, m, thx, thy, pfun in zip(self._amps, self._ls, self._ms, self._theta_x, self._theta_y, self._pfuns): val += ( amp * pfun(z) * xp.sin(l * 2.0 * xp.pi / self._Lx * x + thx) * xp.sin(m * 2.0 * xp.pi / self._Ly * y + thy) ) return val
[docs] class ModesSinCos(Perturbation): r""" .. math:: u(x, y, z) = \sum_s A_s \, \chi_s(z) \sin \!\left(l_s \tfrac{2\pi}{L_x} x + \theta_{x,s}\right) \cos \!\left(m_s \tfrac{2\pi}{L_y} y + \theta_{y,s}\right) where :math:`\chi_s(z)` can be either 1 or localized in z. """ def __init__( self, ls=None, ms=None, amps=(1e-4,), theta_x=None, theta_y=None, pfuns=("Id",), pfuns_params=(0.0,), Lx=1.0, Ly=1.0, Lz=1.0, given_in_basis: LiteralOptions.GivenInBasis = None, comp: int = 0, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) # number of modes if ls is not None: n_modes = len(ls) elif ms is not None: n_modes = len(ms) ls = [0] * n_modes else: n_modes = 1 ls = [0] ms = [0] if ms is None: ms = [0] * n_modes else: assert len(ms) == n_modes if len(amps) == 1: amps = [amps[0]] * n_modes else: assert len(amps) == n_modes if theta_x is None: theta_x = [0] * n_modes elif len(theta_x) == 1: theta_x = [theta_x[0]] * n_modes else: assert len(theta_x) == n_modes if theta_y is None: theta_y = [0] * n_modes elif len(theta_y) == 1: theta_y = [theta_y[0]] * n_modes else: assert len(theta_y) == n_modes if len(pfuns) == 1: pfuns = [pfuns[0]] * n_modes else: assert len(pfuns) == n_modes if len(pfuns_params) == 1: pfuns_params = [pfuns_params[0]] * n_modes else: assert len(pfuns_params) == n_modes # store self._ls = ls self._ms = ms self._amps = amps self._theta_x = theta_x self._theta_y = theta_y self._Lx = Lx self._Ly = Ly self._Lz = Lz self._pfuns = [] for pfun, params in zip(pfuns, pfuns_params): if pfun == "Id": self._pfuns += [lambda z: 1.0] elif pfun == "localize": self._pfuns += [lambda z, p=params: xp.tanh((z - 0.5) / p) / xp.cosh((z - 0.5) / p)] else: raise ValueError(f"Profile function {pfun} is not defined..") # use the setters self.given_in_basis = given_in_basis self.comp = comp def __call__(self, x, y, z): val = 0.0 for amp, l, m, thx, thy, pfun in zip(self._amps, self._ls, self._ms, self._theta_x, self._theta_y, self._pfuns): val += ( amp * pfun(z) * xp.sin(l * 2.0 * xp.pi / self._Lx * x + thx) * xp.cos(m * 2.0 * xp.pi / self._Ly * y + thy) ) return val
[docs] class ModesCosSin(Perturbation): r""" .. math:: u(x, y, z) = \sum_s A_s \, \chi_s(z) \cos \!\left(l_s \tfrac{2\pi}{L_x} x + \theta_{x,s}\right) \sin \!\left(m_s \tfrac{2\pi}{L_y} y + \theta_{y,s}\right) where :math:`\chi_s(z)` can be either 1 or localized in z. """ def __init__( self, ls=None, ms=None, amps=(1e-4,), theta_x=None, theta_y=None, pfuns=("Id",), pfuns_params=(0.0,), Lx=1.0, Ly=1.0, Lz=1.0, given_in_basis: LiteralOptions.GivenInBasis = None, comp: int = 0, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) # number of modes if ls is not None: n_modes = len(ls) elif ms is not None: n_modes = len(ms) ls = [0] * n_modes else: n_modes = 1 ls = [0] ms = [0] if ms is None: ms = [0] * n_modes else: assert len(ms) == n_modes if len(amps) == 1: amps = [amps[0]] * n_modes else: assert len(amps) == n_modes if theta_x is None: theta_x = [0] * n_modes elif len(theta_x) == 1: theta_x = [theta_x[0]] * n_modes else: assert len(theta_x) == n_modes if theta_y is None: theta_y = [0] * n_modes elif len(theta_y) == 1: theta_y = [theta_y[0]] * n_modes else: assert len(theta_y) == n_modes if len(pfuns) == 1: pfuns = [pfuns[0]] * n_modes else: assert len(pfuns) == n_modes if len(pfuns_params) == 1: pfuns_params = [pfuns_params[0]] * n_modes else: assert len(pfuns_params) == n_modes # store self._ls = ls self._ms = ms self._amps = amps self._theta_x = theta_x self._theta_y = theta_y self._Lx = Lx self._Ly = Ly self._Lz = Lz self._pfuns = [] for pfun, params in zip(pfuns, pfuns_params): if pfun == "Id": self._pfuns += [lambda z: 1.0] elif pfun == "localize": self._pfuns += [lambda z, p=params: xp.tanh((z - 0.5) / p) / xp.cosh((z - 0.5) / p)] else: raise ValueError(f"Profile function {pfun} is not defined..") # use the setters self.given_in_basis = given_in_basis self.comp = comp def __call__(self, x, y, z): val = 0.0 for amp, l, m, thx, thy, pfun in zip(self._amps, self._ls, self._ms, self._theta_x, self._theta_y, self._pfuns): val += ( amp * pfun(z) * xp.cos(l * 2.0 * xp.pi / self._Lx * x + thx) * xp.sin(m * 2.0 * xp.pi / self._Ly * y + thy) ) return val
[docs] class TorusModesSin(Perturbation): r"""Sinusoidal function in the periodic coordinates of a Torus. .. math:: u(\eta_1, \eta_2, \eta_3) = \sum_{s} \chi_s(\eta_1) A_s \sin(m_s\,2\pi \eta_2 + n_s\,2\pi \eta_3) \,, where :math:`\chi_s(\eta_1)` is one of .. math:: \chi_s(\eta_1) = \left\{ \begin{aligned} &\sin(l_s\pi\eta_1)\,, \\[2mm] &\exp \left(- \frac{(\eta_1 - r_0)^2}{2 \sigma^2} \right) \,, \\[2mm] & - \frac{\eta_1 - r_0}{\sigma} \exp \left(- \frac{(\eta_1 - r_0)^2}{2 \sigma^2} \right) \,. \end{aligned} \right. Can ony be used in logical space (use 'given_in_basis'). Parameters ---------- ms : tuple | list[int] Poloidal mode numbers. ns : tuple | list[int] Toroidal mode numbers. pfuns : tuple | list[str] "sin" or "cos" or "exp" to define the profile functions. amps : tuple | list[float] Amplitudes of each mode (m_i, n_i). pfun_params : tuple | list Provides :math:`[r_0, \sigma]` parameters for each "exp" profile fucntion, and l_s for "sin" and "cos". given_in_basis : str In which basis the perturbation is represented (see base class). comp : int Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) """ def __init__( self, ms=None, ns=None, amps=(1e-4,), pfuns=("sin",), pfun_params=None, given_in_basis: LiteralOptions.GivenInBasis = None, comp: int = 0, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) if given_in_basis is not None: assert "physical" not in given_in_basis if ms is not None: n_modes = len(ms) elif ns is not None: n_modes = len(ns) ms = [0] * n_modes else: n_modes = 1 ms = [1] ns = [0] if ns is None: ns = [0] * n_modes else: assert len(ns) == n_modes if len(amps) == 1: amps = [amps[0]] * n_modes else: assert len(amps) == n_modes if len(pfuns) == 1: pfuns = [pfuns[0]] * n_modes else: assert len(pfuns) == n_modes if pfun_params is None: pfun_params = [None] * n_modes self._ms = ms self._ns = ns self._amps = amps self._pfuns = [] for pfun, params in zip(pfuns, pfun_params): if pfun == "sin": if params is None: ls = 1 else: ls = params self._pfuns += [lambda eta1: xp.sin(ls * xp.pi * eta1)] elif pfun == "exp": self._pfuns += [ lambda eta1: ( xp.exp(-((eta1 - params[0]) ** 2) / (2 * params[1] ** 2)) / xp.sqrt(2 * xp.pi * params[1] ** 2) ), ] elif pfun == "d_exp": self._pfuns += [ lambda eta1: ( -(eta1 - params[0]) / params[1] ** 2 * xp.exp(-((eta1 - params[0]) ** 2) / (2 * params[1] ** 2)) / xp.sqrt(2 * xp.pi * params[1] ** 2) ), ] else: raise ValueError(f"Profile function {pfun} is not defined..") # use the setters self.given_in_basis = given_in_basis self.comp = comp def __call__(self, eta1, eta2, eta3): val = 0.0 for mi, ni, pfun, amp in zip(self._ms, self._ns, self._pfuns, self._amps): val += ( amp * pfun(eta1) * xp.sin( mi * 2.0 * xp.pi * eta2 + ni * 2.0 * xp.pi * eta3, ) ) return val
[docs] class TorusModesCos(Perturbation): r"""Cosinusoidal function in the periodic coordinates of a Torus. .. math:: u(\eta_1, \eta_2, \eta_3) = \sum_{s} \chi_s(\eta_1) A_s \cos(m_s\,2\pi \eta_2 + n_s\,2\pi \eta_3) \,, where :math:`\chi_s(\eta_1)` is one of .. math:: \chi_s(\eta_1) = \left\{ \begin{aligned} &\sin(\pi\eta_1)\,, \\[2mm] &\exp \left(- \frac{(\eta_1 - r_0)^2}{2 \sigma^2} \right) \,, \\[2mm] & - \frac{\eta_1 - r_0}{\sigma} \exp \left(- \frac{(\eta_1 - r_0)^2}{2 \sigma^2} \right) \,. \end{aligned} \right. Can only be used in logical space (use 'given_in_basis'). Parameters ---------- ms : tuple[int] Poloidal mode numbers. ns : tuple[int] Toroidal mode numbers. pfuns : tuple[str] "sin" or "cos" or "exp" to define the profile functions. amps : tuple[float] Amplitudes of each mode (m_i, n_i). pfun_params : tuple | list Provides :math:`[r_0, \sigma]` parameters for each "exp" profile fucntion, and l_s for "sin" and "cos". given_in_basis : str In which basis the perturbation is represented (see base class). comp : int Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) """ def __init__( self, ms: tuple = (2,), ns: tuple = (1,), amps: tuple = (0.1,), pfuns: tuple = ("sin",), pfun_params=None, given_in_basis: LiteralOptions.GivenInBasis = None, comp: int = 0, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) if given_in_basis is not None: assert "physical" not in given_in_basis if ms is not None: n_modes = len(ms) elif ns is not None: n_modes = len(ns) ms = [0] * n_modes else: n_modes = 1 ms = [1] ns = [0] if ns is None: ns = [0] * n_modes else: assert len(ns) == n_modes if len(amps) == 1: amps = [amps[0]] * n_modes else: assert len(amps) == n_modes if len(pfuns) == 1: pfuns = [pfuns[0]] * n_modes else: assert len(pfuns) == n_modes if pfun_params is None: pfun_params = [None] * n_modes self._ms = ms self._ns = ns self._amps = amps self._pfuns = [] for pfun, params in zip(pfuns, pfun_params): if pfun == "sin": if params is None: ls = 1 else: ls = params self._pfuns += [lambda eta1: xp.sin(ls * xp.pi * eta1)] elif pfun == "cos": self._pfuns += [lambda eta1: xp.cos(xp.pi * eta1)] elif pfun == "exp": self._pfuns += [ lambda eta1: ( xp.exp(-((eta1 - params[0]) ** 2) / (2 * params[1] ** 2)) / xp.sqrt(2 * xp.pi * params[1] ** 2) ), ] elif pfun == "d_exp": self._pfuns += [ lambda eta1: ( -(eta1 - params[0]) / params[1] ** 2 * xp.exp(-((eta1 - params[0]) ** 2) / (2 * params[1] ** 2)) / xp.sqrt(2 * xp.pi * params[1] ** 2) ), ] else: raise ValueError( 'Profile function must be "sin" or "cos" or "exp".', ) # use the setters self.given_in_basis = given_in_basis self.comp = comp def __call__(self, eta1, eta2, eta3): val = 0.0 for mi, ni, pfun, amp in zip(self._ms, self._ns, self._pfuns, self._amps): val += ( amp * pfun(eta1) * xp.cos( mi * 2.0 * xp.pi * eta2 + ni * 2.0 * xp.pi * eta3, ) ) return val
[docs] class Shear_x(Perturbation): r"""Double shear layer in eta1 (-1 in outer regions, 1 in inner regions). .. math:: u(\eta_1, \eta_2, \eta_3) = A(-\tanh((\eta_1 - 0.25)/\delta)+\tanh((\eta_1 - 0.75)/\delta) - 1) \,. Can only be used in logical space. Parameters ---------- amps : float Amplitude of the velocity on each side. delta : float Characteristic size of the shear layer given_in_basis : str In which basis the perturbation is represented (see base class). comp : int Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) """ def __init__( self, amp=1e-4, delta=1 / 15, given_in_basis: LiteralOptions.GivenInBasis = None, comp: int = 0, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) if given_in_basis is not None: assert "physical" not in given_in_basis, f"Perturbation {self.__name__} can only be used in logical space." self._amp = amp self._delta = delta # use the setters self.given_in_basis = given_in_basis self.comp = comp def __call__(self, e1, e2, e3): val = self._amp * (-xp.tanh((e1 - 0.75) / self._delta) + xp.tanh((e1 - 0.25) / self._delta) - 1) return val
[docs] class Shear_y(Perturbation): r"""Double shear layer in eta2 (-1 in outer regions, 1 in inner regions). .. math:: u(\eta_1, \eta_2, \eta_3) = A(-\tanh((\eta_2 - 0.25)/\delta) + \tanh((\eta_2 - 0.75)/\delta) - 1) \,. Can only be used in logical space. Parameters ---------- amps : float Amplitude of the velocity on each side. delta : float Characteristic size of the shear layer given_in_basis : str In which basis the perturbation is represented (see base class). comp : int Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) """ def __init__( self, amp=1e-4, delta=1 / 15, given_in_basis: LiteralOptions.GivenInBasis = None, comp: int = 0, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) if given_in_basis is not None: assert "physical" not in given_in_basis, f"Perturbation {self.__name__} can only be used in logical space." self._amp = amp self._delta = delta # use the setters self.given_in_basis = given_in_basis self.comp = comp def __call__(self, e1, e2, e3): val = self._amp * (-xp.tanh((e2 - 0.75) / self._delta) + xp.tanh((e2 - 0.25) / self._delta) - 1) return val
[docs] class Shear_z(Perturbation): r"""Double shear layer in eta3 (-1 in outer regions, 1 in inner regions). .. math:: u(\eta_1, \eta_2, \eta_3) = A(-\tanh((\eta_3 - 0.25)/\delta) + \tanh((\eta_3 - 0.75)/\delta) - 1) \,. Can only be used in logical space. Parameters ---------- amps : float Amplitude of the velocity on each side. delta : float Characteristic size of the shear layer given_in_basis : str In which basis the perturbation is represented (see base class). comp : int Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) """ def __init__( self, amp=1e-4, delta=1 / 15, given_in_basis: LiteralOptions.GivenInBasis = None, comp: int = 0, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) if given_in_basis is not None: assert "physical" not in given_in_basis, f"Perturbation {self.__name__} can only be used in logical space." self._amp = amp self._delta = delta # use the setters self.given_in_basis = given_in_basis self.comp = comp def __call__(self, e1, e2, e3): val = self._amp * (-xp.tanh((e3 - 0.75) / self._delta) + xp.tanh((e3 - 0.25) / self._delta) - 1) return val
[docs] class Erf_z(Perturbation): r"""Shear layer in eta3 (-1 in lower regions, 1 in upper regions). .. math:: u(\eta_1, \eta_2, \eta_3) = A \, erf((\eta_3 - 0.5)/\delta) \,. Can only be used in logical space. Parameters ---------- amp : float Amplitude of the velocity on each side. delta : float Characteristic size of the shear layer given_in_basis : str In which basis the perturbation is represented (see base class). comp : int Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) """ def __init__( self, amp=1e-4, delta=1 / 15, given_in_basis: LiteralOptions.GivenInBasis = None, comp: int = 0, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) if given_in_basis is not None: assert "physical" not in given_in_basis, f"Perturbation {self.__name__} can only be used in logical space." self._amp = amp self._delta = delta # use the setters self.given_in_basis = given_in_basis self.comp = comp def __call__(self, e1, e2, e3): from scipy.special import erf val = self._amp * erf((e3 - 0.5) / self._delta) return val
[docs] class RestelliAnalyticSolutionVelocity(Perturbation): r"""Analytic solution :math:`u=u_e` of the system: .. math:: \partial_t u = - \nabla \phi + u \times B + \nu \Delta u + f \,,\\ 0 = \nabla \phi- u_e \times B + \nu_e \Delta u_e + f_e \,, \\ \nabla \cdot (u-u_e) = 0 \,. where :math:`f` is defined as follows: .. math:: f = \nu \omega \,, \\[2mm] \omega = \left[0, \alpha \frac{R_0 - 4R}{a R_0 R} - \beta \frac{B_p}{B_0}\frac{R_0^2}{a R^3}, 0 \right] \,, \\[2mm] R = \sqrt{x^2 + y^2} \,. Can only be defined in Cartesian coordinates. The solution is given by: .. math:: \alpha \frac{R}{a R_0} \left[\begin{array}{c} -z \\ R-R_0 \\ 0 \end{array} \right] + \beta \frac{B_p}{B_0} \frac{R_0}{aR} \left[\begin{array}{c} z \\ -(R-R_0) \\ \frac{B_0}{B_p} a \end{array} \right] \,, \\[2mm] R = \sqrt{x^2 + y^2} \,. Parameters ---------- a : float Minor radius of torus (default: 1.). R0 : float Major radius of torus (default: 2.). B0 : float On-axis (r=0) toroidal magnetic field (default: 10.). Bp : float Poloidal magnetic field (default: 12.5). alpha : float (default: 0.1) beta : float (default: 1.0) comp : int Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) References ---------- [1] Juan Vicente Gutiérrez-Santacreu, Omar Maj, Marco Restelli: Finite element discretization of a Stokes-like model arising in plasma physics, Journal of Computational Physics 2018. """ def __init__( self, a=1.0, R0=2.0, B0=10.0, Bp=12.5, alpha=0.1, beta=1.0, comp: int = 0, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) self._a = a self._R0 = R0 self._B0 = B0 self._Bp = Bp self._alpha = alpha self._beta = beta # use the setters self.given_in_basis = "physical" self.comp = comp # equilibrium ion velocity def __call__(self, x, y, z): """Velocity of ions and electrons.""" R = xp.sqrt(x**2 + y**2) R = xp.where(R == 0.0, 1e-9, R) phi = xp.arctan2(-y, x) ustarR = ( self._alpha * R / (self._a * self._R0) * (-z) + self._beta * self._Bp * self._R0 / (self._B0 * self._a * R) * z ) ustarZ = self._alpha * R / (self._a * self._R0) * (R - self._R0) + self._beta * self._Bp * self._R0 / ( self._B0 * self._a * R ) * (-(R - self._R0)) ustarphi = self._beta * self._Bp * self._R0 / (self._B0 * self._a * R) * self._B0 * self._a / self._Bp # form normalized to cylindrical coordinates: uR = ustarR uphi = ustarphi / R uZ = ustarZ # from cylindrical to cartesian: if self.comp == 0: ux = xp.cos(phi) * uR - R * xp.sin(phi) * uphi return ux elif self.comp == 1: uy = -xp.sin(phi) * uR - R * xp.cos(phi) * uphi return uy elif self.comp == 2: uz = uZ return uz else: raise ValueError(f"Invalid component '{self._comp}'. Must be 0, 1, or 2.")
[docs] class RestelliAnalyticSolutionVelocity_2(Perturbation): r"""Analytic solution :math:`u=u_e` of the system: .. math:: \partial_t u = - \nabla \phi + u \times B + \nu \Delta u + f \,,\\ 0 = \nabla \phi- u_e \times B + \nu_e \Delta u_e + f_e \,, \\ \nabla \cdot (u-u_e) = 0 \,. where :math:`f` is defined as follows: .. math:: f = \nu \omega \,, \\[2mm] \omega = \left[0, \alpha \frac{R_0 - 4R}{a R_0 R} - \beta \frac{B_p}{B_0}\frac{R_0^2}{a R^3}, 0 \right] \,, \\[2mm] R = \sqrt{x^2 + y^2} \,. Can only be defined in Cartesian coordinates. The solution is given by: .. math:: \alpha \frac{R}{a R_0} \left[\begin{array}{c} -z \\ R-R_0 \\ 0 \end{array} \right] + \beta \frac{B_p}{B_0} \frac{R_0}{aR} \left[\begin{array}{c} z \\ -(R-R_0) \\ \frac{B_0}{B_p} a \end{array} \right] \,, \\[2mm] R = \sqrt{x^2 + y^2} \,. Parameters ---------- a : float Minor radius of torus (default: 1.). R0 : float Major radius of torus (default: 2.). B0 : float On-axis (r=0) toroidal magnetic field (default: 10.). Bp : float Poloidal magnetic field (default: 12.5). alpha : float (default: 0.1) beta : float (default: 1.0) comp : int Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) References ---------- [1] Juan Vicente Gutiérrez-Santacreu, Omar Maj, Marco Restelli: Finite element discretization of a Stokes-like model arising in plasma physics, Journal of Computational Physics 2018. """ def __init__( self, a=1.0, R0=2.0, B0=10.0, Bp=12.5, alpha=0.1, beta=1.0, comp: int = 0, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) self._a = a self._R0 = R0 self._B0 = B0 self._Bp = Bp self._alpha = alpha self._beta = beta # use the setter self.given_in_basis = "physical" self.comp = comp # equilibrium ion velocity def __call__(self, x, y, z): """Velocity of ions and electrons.""" R = xp.sqrt(x**2 + y**2) R = xp.where(R == 0.0, 1e-9, R) phi = xp.arctan2(-y, x) ustarR = ( self._alpha * R / (self._a * self._R0) * (-z) + self._beta * self._Bp * self._R0 / (self._B0 * self._a * R) * z ) ustarZ = self._alpha * R / (self._a * self._R0) * (R - self._R0) + self._beta * self._Bp * self._R0 / ( self._B0 * self._a * R ) * (-(R - self._R0)) ustarphi = self._beta * self._Bp * self._R0 / (self._B0 * self._a * R) * self._B0 * self._a / self._Bp # form normalized to cylindrical coordinates: uR = ustarR uphi = ustarphi / R uZ = ustarZ # from cylindrical to cartesian: if self.comp == 0: ux = xp.cos(phi) * uR - R * xp.sin(phi) * uphi return ux elif self.comp == 1: uy = -xp.sin(phi) * uR - R * xp.cos(phi) * uphi return uy elif self.comp == 2: uz = uZ return uz else: raise ValueError(f"Invalid component '{self._comp}'. Must be '0', '1', or '2'.")
[docs] class RestelliAnalyticSolutionVelocity_3(Perturbation): r"""Analytic solution :math:`u=u_e` of the system: .. math:: \partial_t u = - \nabla \phi + u \times B + \nu \Delta u + f \,,\\ 0 = \nabla \phi- u_e \times B + \nu_e \Delta u_e + f_e \,, \\ \nabla \cdot (u-u_e) = 0 \,. where :math:`f` is defined as follows: .. math:: f = \nu \omega \,, \\[2mm] \omega = \left[0, \alpha \frac{R_0 - 4R}{a R_0 R} - \beta \frac{B_p}{B_0}\frac{R_0^2}{a R^3}, 0 \right] \,, \\[2mm] R = \sqrt{x^2 + y^2} \,. Can only be defined in Cartesian coordinates. The solution is given by: .. math:: \alpha \frac{R}{a R_0} \left[\begin{array}{c} -z \\ R-R_0 \\ 0 \end{array} \right] + \beta \frac{B_p}{B_0} \frac{R_0}{aR} \left[\begin{array}{c} z \\ -(R-R_0) \\ \frac{B_0}{B_p} a \end{array} \right] \,, \\[2mm] R = \sqrt{x^2 + y^2} \,. Parameters ---------- a : float Minor radius of torus (default: 1.). R0 : float Major radius of torus (default: 2.). B0 : float On-axis (r=0) toroidal magnetic field (default: 10.). Bp : float Poloidal magnetic field (default: 12.5). alpha : float (default: 0.1) beta : float (default: 1.0) comp : int Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) References ---------- [1] Juan Vicente Gutiérrez-Santacreu, Omar Maj, Marco Restelli: Finite element discretization of a Stokes-like model arising in plasma physics, Journal of Computational Physics 2018. """ def __init__( self, a=1.0, R0=2.0, B0=10.0, Bp=12.5, alpha=0.1, beta=1.0, comp: int = 0, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) self._a = a self._R0 = R0 self._B0 = B0 self._Bp = Bp self._alpha = alpha self._beta = beta # use the setters self.given_in_basis = "physical" self.comp = comp # equilibrium ion velocity def __call__(self, x, y, z): """Velocity of ions and electrons.""" R = xp.sqrt(x**2 + y**2) R = xp.where(R == 0.0, 1e-9, R) phi = xp.arctan2(-y, x) ustarR = ( self._alpha * R / (self._a * self._R0) * (-z) + self._beta * self._Bp * self._R0 / (self._B0 * self._a * R) * z ) ustarZ = self._alpha * R / (self._a * self._R0) * (R - self._R0) + self._beta * self._Bp * self._R0 / ( self._B0 * self._a * R ) * (-(R - self._R0)) ustarphi = self._beta * self._Bp * self._R0 / (self._B0 * self._a * R) * self._B0 * self._a / self._Bp # form normalized to cylindrical coordinates: uR = ustarR uphi = ustarphi / R uZ = ustarZ # from cylindrical to cartesian: if self.comp == 0: ux = xp.cos(phi) * uR - R * xp.sin(phi) * uphi return ux elif self.comp == 1: uy = -xp.sin(phi) * uR - R * xp.cos(phi) * uphi return uy elif self.comp == 2: uz = uZ return uz else: raise ValueError(f"Invalid component '{self._comp}'. Must be '0', '1', or '2'.")
[docs] class RestelliAnalyticSolutionPotential(Perturbation): r"""Analytic solution :math:`\phi` of the system: .. math:: \partial_t u = - \nabla \phi + u \times B + \nu \Delta u + f \,,\\ 0 = \nabla \phi- u_e \times B + \nu_e \Delta u_e + f_e \,, \\ \nabla \cdot (u-u_e) = 0 \,. where :math:`f` is defined as follows: .. math:: f = \nu \omega \,, \\[2mm] \omega = \left[0, \alpha \frac{R_0 - 4R}{a R_0 R} - \beta \frac{B_p}{B_0}\frac{R_0^2}{a R^3}, 0 \right] \,, \\[2mm] R = \sqrt{x^2 + y^2} \,. Can only be defined in Cartesian coordinates. The solution is given by: .. math:: \phi = \frac{1}{2} a B_0 \alpha \left( \frac{(R-R_0)^2+z^2}{a^2} - \frac{2}{3} \right) \\[2mm] R = \sqrt{x^2 + y^2} \,. Parameters ---------- a : float Minor radius of torus (default: 1.). R0 : float Major radius of torus (default: 2.). B0 : float On-axis (r=0) toroidal magnetic field (default: 10.). Bp : float Poloidal magnetic field (default: 12.5). alpha : float (default: 0.1) beta : float (default: 1.0) References ---------- [1] Juan Vicente Gutiérrez-Santacreu, Omar Maj, Marco Restelli: Finite element discretization of a Stokes-like model arising in plasma physics, Journal of Computational Physics 2018. """ def __init__(self, a=1.0, R0=2.0, B0=10.0, Bp=12.5, alpha=0.1, beta=1.0): # use setter to store input parameters self.params = copy.deepcopy(locals()) self._a = a self._R0 = R0 self._B0 = B0 self._Bp = Bp self._alpha = alpha self._beta = beta # use the setter self.given_in_basis = "physical" # equilibrium potential def __call__(self, x, y, z): """Equilibrium potential.""" R = xp.sqrt(x**2 + y**2) pp = 0.5 * self._a * self._B0 * self._alpha * (((R - self._R0) ** 2 + z**2) / self._a**2 - 2.0 / 3.0) return pp
[docs] class ManufacturedSolutionVelocity(Perturbation): r"""Analytic solutions :math:`u` and :math:`u_e` of the system: .. math:: \partial_t u = - \nabla \phi + u \times B + \nu \Delta u + f \,,\\ 0 = \nabla \phi- u_e \times B + \nu_e \Delta u_e + f_e \,, \\ \nabla \cdot (u-u_e) = 0 \,. Can only be defined in Cartesian coordinates. The solution in 1D is given by: .. math:: u = \left[\begin{array}{c} sin(2 \pi x) + 1.0 \\ 0 \\ 0 \end{array} \right] \,, u_e = \left[\begin{array}{c} sin(2 \pi x) \\ 0 \\ 0 \end{array} \right] \,. The solution in 2D is given by: .. math:: u = \left[\begin{array}{c} -sin(2 \pi x) sin(2 \pi y) \\ -cos(2 \pi y) cos(2 \pi y) \\ 0 \end{array} \right] \,, u_e = \left[\begin{array}{c} -sin(4 \pi x) sin(4 \pi y) \\ -cos(4 \pi y) cos(4 \pi y) \\ 0 \end{array} \right] \,. Parameters ---------- species : string 'Ions' or 'Electrons'. comp : string Which component of the solution ('0', '1' or '2'). dimension: string Defines the manufactured solution to be selected ('1D' or '2D'). b0 : float Magnetic field (default: 1.0). comp : int Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) """ def __init__( self, species="Ions", dimension="1D", b0=1.0, comp: int = 0, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) self._b = b0 self._species = species self._dimension = dimension # use the setters self.given_in_basis = "physical" self.comp = comp # equilibrium ion velocity def __call__(self, x, y, z): if self._species == "Ions": """Velocity of ions.""" """x component""" if self._dimension == "2D": ux = -xp.sin(2 * xp.pi * x) * xp.sin(2 * xp.pi * y) elif self._dimension == "1D": ux = xp.sin(2 * xp.pi * x) + 1.0 """y component""" if self._dimension == "2D": uy = -xp.cos(2 * xp.pi * x) * xp.cos(2 * xp.pi * y) elif self._dimension == "1D": uy = xp.cos(2 * xp.pi * x) """z component""" uz = 0.0 * x if self.comp == 0: return ux elif self.comp == 1: return uy elif self.comp == 2: return uz else: raise ValueError(f"Invalid component '{self._comp}'. Must be '0', '1', or '2'.") elif self._species == "Electrons": """Velocity of electrons.""" """x component""" if self._dimension == "2D": ux = -xp.sin(4 * xp.pi * x) * xp.sin(4 * xp.pi * y) elif self._dimension == "1D": ux = xp.sin(2.0 * xp.pi * x) """y component""" if self._dimension == "2D": uy = -xp.cos(4 * xp.pi * x) * xp.cos(4 * xp.pi * y) elif self._dimension == "1D": uy = xp.cos(2 * xp.pi * x) """z component""" uz = 0.0 * x if self.comp == 0: return ux if self.comp == 1: return uy if self.comp == 2: return uz else: raise ValueError(f"Invalid component '{self._comp}'. Must be '0', '1', or '2'.") else: raise ValueError(f"Invalid species '{self._species}'. Must be 'Ions' or 'Electrons'.")
[docs] class ManufacturedSolutionPotential(Perturbation): r"""Analytic solution :math:`\phi` of the system: .. math:: \partial_t u = - \nabla \phi + u \times B + \nu \Delta u + f \,,\\ 0 = \nabla \phi- u_e \times B + \nu_e \Delta u_e + f_e \,, \\ \nabla \cdot (u-u_e) = 0 \,. where :math:`f` is defined as follows: .. math:: f = \left[1 - b_0 cos(x) - \nu sin(y), 1 - b_0 sin(y) + \nu cos(x) , 0 \right] \,, \\[2mm] f_e = \left[-1 + 0.5 b_0 cos(x) - \nu_e 0.5 sin(y), -1 + 0.5 b_0 sin(y) + \nu_e cos(x) , 0 \right] \,. Can only be defined in Cartesian coordinates. The solution in 1D is given by: .. math:: \phi = sin(2\pi x) \,. The solution in 2D is given by: .. math:: \phi = cos(2\pi x) + sin(2\pi y) \,. Parameters ---------- dimension: string Defines the manufactured solution to be selected ('1D' or '2D'). b0 : float Magnetic field (default: 1.0). """ def __init__(self, dimension="1D", b0=1.0): # use setter to store input parameters self.params = copy.deepcopy(locals()) self._ab = b0 self._dimension = dimension # use the setter self.given_in_basis = "physical" # equilibrium ion velocity def __call__(self, x, y, z): """Potential.""" if self._dimension == "2D": phi = xp.cos(2 * xp.pi * x) + xp.sin(2 * xp.pi * y) elif self._dimension == "1D": phi = xp.sin(2.0 * xp.pi * x) return phi
[docs] class ManufacturedSolutionVelocity_2(Perturbation): r"""Analytic solutions :math:`u` and :math:`u_e` of the system: .. math:: \partial_t u = - \nabla \phi + u \times B + \nu \Delta u + f \,,\\ 0 = \nabla \phi- u_e \times B + \nu_e \Delta u_e + f_e \,, \\ \nabla \cdot (u-u_e) = 0 \,. Can only be defined in Cartesian coordinates. The solution in 1D is given by: .. math:: u = \left[\begin{array}{c} sin(2 \pi x) + 1.0 \\ 0 \\ 0 \end{array} \right] \,, u_e = \left[\begin{array}{c} sin(2 \pi x) \\ 0 \\ 0 \end{array} \right] \,. The solution in 2D is given by: .. math:: u = \left[\begin{array}{c} -sin(2 \pi x) sin(2 \pi y) \\ -cos(2 \pi y) cos(2 \pi y) \\ 0 \end{array} \right] \,, u_e = \left[\begin{array}{c} -sin(4 \pi x) sin(4 \pi y) \\ -cos(4 \pi y) cos(4 \pi y) \\ 0 \end{array} \right] \,. Parameters ---------- species : string 'Ions' or 'Electrons'. dimension: string Defines the manufactured solution to be selected ('1D' or '2D'). b0 : float Magnetic field (default: 1.0). comp : int Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) """ def __init__( self, species="Ions", dimension="1D", b0=1.0, comp: int = 0, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) self._b = b0 self._species = species self._dimension = dimension # use the setters self.given_in_basis = "physical" self.comp = comp # equilibrium ion velocity def __call__(self, x, y, z): if self._species == "Ions": """Velocity of ions.""" """x component""" if self._dimension == "2D": ux = -xp.sin(2 * xp.pi * x) * xp.sin(2 * xp.pi * y) elif self._dimension == "1D": ux = xp.sin(2 * xp.pi * x) + 1.0 """y component""" if self._dimension == "2D": uy = -xp.cos(2 * xp.pi * x) * xp.cos(2 * xp.pi * y) elif self._dimension == "1D": uy = xp.cos(2 * xp.pi * x) """z component""" uz = 0.0 * x if self.comp == 0: return ux elif self.comp == 1: return uy elif self.comp == 2: return uz else: raise ValueError(f"Invalid component '{self._comp}'. Must be '0', '1', or '2'.") elif self._species == "Electrons": """Velocity of electrons.""" """x component""" if self._dimension == "2D": ux = -xp.sin(4 * xp.pi * x) * xp.sin(4 * xp.pi * y) elif self._dimension == "1D": ux = xp.sin(2.0 * xp.pi * x) """y component""" if self._dimension == "2D": uy = -xp.cos(4 * xp.pi * x) * xp.cos(4 * xp.pi * y) elif self._dimension == "1D": uy = xp.cos(2 * xp.pi * x) """z component""" uz = 0.0 * x if self.comp == 0: return ux if self.comp == 1: return uy if self.comp == 2: return uz else: raise ValueError(f"Invalid component '{self._comp}'. Must be '0', '1', or '2'.") else: raise ValueError(f"Invalid species '{self._species}'. Must be 'Ions' or 'Electrons'.")
[docs] class ITPA_density(Perturbation): r"""ITPA radial density profile in `A. Könies et al. 2018 <https://iopscience.iop.org/article/10.1088/1741-4326/aae4e6>`_ .. math:: n(\eta_1) = n_0*c_3\exp\left[-\frac{c_2}{c_1}\tanh\left(\frac{\eta_1 - c_0}{c_2}\right)\right]\,. Parameters ---------- n0 : float ITPA profile density c : tuple | list 4 ITPA profile coefficients """ def __init__( self, n0: float = 0.00720655, c: tuple = (0.491230, 0.298228, 0.198739, 0.521298), comp: int = 0, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) assert len(c) == 4 self._n0 = n0 self._c = c # use the setters self.given_in_basis = "physical" self.comp = comp def __call__(self, eta1, eta2=None, eta3=None): val = 0.0 if self._c[2] == 0.0: val = self._c[3] - 0 * eta1 else: val = ( self._n0 * self._c[3] * xp.exp( -self._c[2] / self._c[1] * xp.tanh((eta1 - self._c[0]) / self._c[2]), ) ) return val
[docs] class TokamakManufacturedSolutionVelocity(Perturbation): r"""Analytic solution :math:`u=u_e` of the system: .. math:: \partial_t u = - \nabla \phi + u \times B + \nu \Delta u + f \,,\\ 0 = \nabla \phi- u_e \times B + \nu_e \Delta u_e + f_e \,, \\ \nabla \cdot (u-u_e) = 0 \,. where :math:`f` is defined as follows: .. math:: f = \left[\begin{array}{c} \alpha \frac{B_0}{a}(R-R_0) - \alpha \frac{1}{a R_0} \frac{R_0 B_0 Z}{R} + \nu \alpha \frac{1}{a R_0} \frac{R_0}{R^2} \\ \alpha \frac{1}{a R_0} (R-R_0) \frac{R_0 B_0}{R} + \alpha \frac{B_0Z}{a} \\ \alpha \frac{1}{a R_0} \frac{R_0 B_p}{a R^2} \left( (R-R_0)^2 + Z^2\right) \end{array} \right] \,, \\[2mm] f = \left[\begin{array}{c} -\alpha \frac{B_0}{a}(R-R_0) + \alpha \frac{1}{a R_0} \frac{R_0 B_0 Z}{R} + \nu_e \alpha \frac{1}{a R_0} \frac{R_0}{R^2} \\ -\alpha \frac{1}{a R_0} (R-R_0) \frac{R_0 B_0}{R} - \alpha \frac{B_0 Z}{a} \\ -\alpha \frac{1}{a R_0} \frac{ R_0 B_p}{a R^2} \left( (R-R_0)^2 + Z^2\right) \end{array} \right] \,, \\[2mm] R = \sqrt{x^2 + y^2} \,. Can only be defined in Cartesian coordinates. The solution is given by: .. math:: \mathbf{u} = \alpha \frac{1}{a R_0} \left[\begin{array}{c} R-R_0 \\ z \\ 0 \end{array} \right] \,, \\[2mm] R = \sqrt{x^2 + y^2} \,. Parameters ---------- comp : string Which component of the solution ('0', '1' or '2'). a : float Minor radius of torus (default: 1.). R0 : float Major radius of torus (default: 2.). B0 : float On-axis (r=0) toroidal magnetic field (default: 10.). Bp : float Poloidal magnetic field (default: 12.5). alpha : float (default: 0.1) beta : float (default: 1.0) References ---------- [1] Juan Vicente Gutiérrez-Santacreu, Omar Maj, Marco Restelli: Finite element discretization of a Stokes-like model arising in plasma physics, Journal of Computational Physics 2018. """ def __init__( self, comp=0, a=1.0, R0=2.0, B0=10.0, Bp=12.5, alpha=0.1, beta=1.0, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) self._comp = comp self._a = a self._R0 = R0 self._B0 = B0 self._Bp = Bp self._alpha = alpha self._beta = beta # use the setters self.given_in_basis = "physical" self.comp = comp # equilibrium ion velocity def __call__(self, x, y, z): """Velocity of ions and electrons.""" R = xp.sqrt(x**2 + y**2) R = xp.where(R == 0.0, 1e-9, R) phi = xp.arctan2(-y, x) A = self._alpha / (self._a * self._R0) C = self._beta * self._Bp * self._R0 / (self._B0 * self._a) uR = A * (R - self._R0) uZ = A * z uphi = 0 # from cylindrical to cartesian: if self.comp == 0: ux = xp.cos(phi) * uR - R * xp.sin(phi) * uphi return ux elif self.comp == 1: uy = -xp.sin(phi) * uR - R * xp.cos(phi) * uphi return uy elif self.comp == 2: uz = uZ return uz else: raise ValueError(f"Invalid component '{self._comp}'. Must be '0', '1', or '2'.")
[docs] class TokamakManufacturedSolutionVelocity_1(Perturbation): r"""Analytic solution :math:`u=u_e` of the system: .. math:: \partial_t u = - \nabla \phi + u \times B + \nu \Delta u + f \,,\\ 0 = \nabla \phi- u_e \times B + \nu_e \Delta u_e + f_e \,, \\ \nabla \cdot (u-u_e) = 0 \,. where :math:`f` is defined as follows: .. math:: f = \left[\begin{array}{c} \alpha \frac{B_0}{a}(R-R_0) - \alpha \frac{1}{a R_0} \frac{R_0 B_0 Z}{R} + \nu \alpha \frac{1}{a R_0} \frac{R_0}{R^2} \\ \alpha \frac{1}{a R_0} (R-R_0) \frac{R_0 B_0}{R} + \alpha \frac{B_0Z}{a} \\ \alpha \frac{1}{a R_0} \frac{R_0 B_p}{a R^2} \left( (R-R_0)^2 + Z^2\right) \end{array} \right] \,, \\[2mm] f = \left[\begin{array}{c} -\alpha \frac{B_0}{a}(R-R_0) + \alpha \frac{1}{a R_0} \frac{R_0 B_0 Z}{R} + \nu_e \alpha \frac{1}{a R_0} \frac{R_0}{R^2} \\ -\alpha \frac{1}{a R_0} (R-R_0) \frac{R_0 B_0}{R} - \alpha \frac{B_0 Z}{a} \\ -\alpha \frac{1}{a R_0} \frac{ R_0 B_p}{a R^2} \left( (R-R_0)^2 + Z^2\right) \end{array} \right] \,, \\[2mm] R = \sqrt{x^2 + y^2} \,. Can only be defined in Cartesian coordinates. The solution is given by: .. math:: \mathbf{u} = \alpha \frac{1}{a R_0} \left[\begin{array}{c} R-R_0 \\ z \\ 0 \end{array} \right] \,, \\[2mm] R = \sqrt{x^2 + y^2} \,. Parameters ---------- comp : string Which component of the solution ('0', '1' or '2'). a : float Minor radius of torus (default: 1.). R0 : float Major radius of torus (default: 2.). B0 : float On-axis (r=0) toroidal magnetic field (default: 10.). Bp : float Poloidal magnetic field (default: 12.5). alpha : float (default: 0.1) beta : float (default: 1.0) References ---------- [1] Juan Vicente Gutiérrez-Santacreu, Omar Maj, Marco Restelli: Finite element discretization of a Stokes-like model arising in plasma physics, Journal of Computational Physics 2018. """ def __init__( self, comp=0, a=1.0, R0=2.0, B0=10.0, Bp=12.5, alpha=0.1, beta=1.0, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) self._comp = comp self._a = a self._R0 = R0 self._B0 = B0 self._Bp = Bp self._alpha = alpha self._beta = beta # use the setters self.given_in_basis = "physical" self.comp = comp # equilibrium ion velocity def __call__(self, x, y, z): """Velocity of ions and electrons.""" R = xp.sqrt(x**2 + y**2) R = xp.where(R == 0.0, 1e-9, R) phi = xp.arctan2(-y, x) A = self._alpha / (self._a * self._R0) C = self._beta * self._Bp * self._R0 / (self._B0 * self._a) uR = A * (R - self._R0) uZ = A * z uphi = 0 # from cylindrical to cartesian: if self.comp == 0: ux = xp.cos(phi) * uR - R * xp.sin(phi) * uphi return ux elif self.comp == 1: uy = -xp.sin(phi) * uR - R * xp.cos(phi) * uphi return uy elif self.comp == 2: uz = uZ return uz else: raise ValueError(f"Invalid component '{self._comp}'. Must be '0', '1', or '2'.")
[docs] class TokamakManufacturedSolutionVelocity_2(Perturbation): r"""Analytic solution :math:`u=u_e` of the system: .. math:: \partial_t u = - \nabla \phi + u \times B + \nu \Delta u + f \,,\\ 0 = \nabla \phi- u_e \times B + \nu_e \Delta u_e + f_e \,, \\ \nabla \cdot (u-u_e) = 0 \,. where :math:`f` is defined as follows: .. math:: f = \left[\begin{array}{c} \alpha \frac{B_0}{a}(R-R_0) - \alpha \frac{1}{a R_0} \frac{R_0 B_0 Z}{R} + \nu \alpha \frac{1}{a R_0} \frac{R_0}{R^2} \\ \alpha \frac{1}{a R_0} (R-R_0) \frac{R_0 B_0}{R} + \alpha \frac{B_0Z}{a} \\ \alpha \frac{1}{a R_0} \frac{R_0 B_p}{a R^2} \left( (R-R_0)^2 + Z^2\right) \end{array} \right] \,, \\[2mm] f = \left[\begin{array}{c} -\alpha \frac{B_0}{a}(R-R_0) + \alpha \frac{1}{a R_0} \frac{R_0 B_0 Z}{R} + \nu_e \alpha \frac{1}{a R_0} \frac{R_0}{R^2} \\ -\alpha \frac{1}{a R_0} (R-R_0) \frac{R_0 B_0}{R} - \alpha \frac{B_0 Z}{a} \\ -\alpha \frac{1}{a R_0} \frac{ R_0 B_p}{a R^2} \left( (R-R_0)^2 + Z^2\right) \end{array} \right] \,, \\[2mm] R = \sqrt{x^2 + y^2} \,. Can only be defined in Cartesian coordinates. The solution is given by: .. math:: \mathbf{u} = \alpha \frac{1}{a R_0} \left[\begin{array}{c} R-R_0 \\ z \\ 0 \end{array} \right] \,, \\[2mm] R = \sqrt{x^2 + y^2} \,. Parameters ---------- comp : string Which component of the solution ('0', '1' or '2'). a : float Minor radius of torus (default: 1.). R0 : float Major radius of torus (default: 2.). B0 : float On-axis (r=0) toroidal magnetic field (default: 10.). Bp : float Poloidal magnetic field (default: 12.5). alpha : float (default: 0.1) beta : float (default: 1.0) References ---------- [1] Juan Vicente Gutiérrez-Santacreu, Omar Maj, Marco Restelli: Finite element discretization of a Stokes-like model arising in plasma physics, Journal of Computational Physics 2018. """ def __init__( self, comp=0, a=1.0, R0=2.0, B0=10.0, Bp=12.5, alpha=0.1, beta=1.0, ): # use setter to store input parameters self.params = copy.deepcopy(locals()) self._comp = comp self._a = a self._R0 = R0 self._B0 = B0 self._Bp = Bp self._alpha = alpha self._beta = beta # use the setters self.given_in_basis = "physical" self.comp = comp # equilibrium ion velocity def __call__(self, x, y, z): """Velocity of ions and electrons.""" R = xp.sqrt(x**2 + y**2) R = xp.where(R == 0.0, 1e-9, R) phi = xp.arctan2(-y, x) A = self._alpha / (self._a * self._R0) C = self._beta * self._Bp * self._R0 / (self._B0 * self._a) uR = A * (R - self._R0) uZ = A * z uphi = 0 # from cylindrical to cartesian: if self.comp == 0: ux = xp.cos(phi) * uR - R * xp.sin(phi) * uphi return ux elif self.comp == 1: uy = -xp.sin(phi) * uR - R * xp.cos(phi) * uphi return uy elif self.comp == 2: uz = uZ return uz else: raise ValueError(f"Invalid component '{self._comp}'. Must be '0', '1', or '2'.")