Source code for struphy.propagators.shear_alfven_current_coupling_5d

import logging
from dataclasses import dataclass
from typing import Literal

from feectools.ddm.mpi import mpi as MPI
from feectools.linalg.block import BlockVector
from feectools.linalg.solvers import inverse
from line_profiler import profile

from struphy.feec import preconditioner
from struphy.feec.basis_projection_ops import (
    BasisProjectionOperator,
    BasisProjectionOperatorLocal,
)
from struphy.io.options import LiteralOptions, OptionsBase
from struphy.linear_algebra.schur_solver import SchurSolver
from struphy.linear_algebra.solver import SolverParameters
from struphy.models.variables import FEECVariable, PICVariable
from struphy.ode.solvers import ODEsolverFEEC
from struphy.ode.utils import ButcherTableau
from struphy.pic.accumulation import accum_kernels_gc
from struphy.pic.accumulation.filter import FilterParameters
from struphy.pic.accumulation.particles_to_grid import AccumulatorVector
from struphy.propagators.base import Propagator
from struphy.utils.pyccel import Pyccelkernel
from struphy.utils.utils import check_option

logger = logging.getLogger("struphy")


[docs] class ShearAlfvenCurrentCoupling5D(Propagator): r""":ref:`FEEC <gempic>` discretization of the following equations: find :math:`\mathbf U \in \{H(\textnormal{curl}), H(\textnormal{div}), (H^1)^3\}` and :math:`\mathbf B \in H(\textnormal{div})` such that .. math:: \left\{ \begin{aligned} \int \rho_0 &\frac{\partial \tilde{\mathbf U}}{\partial t} \cdot \mathbf V \, \textnormal{d} \mathbf{x} = \int \left(\tilde{\mathbf B} - \frac{A_\textnormal{h}}{A_b} \iint f^\text{vol} \mu \mathbf{b}_0\textnormal{d} \mu \textnormal{d} v_\parallel \right) \cdot \nabla \times (\tilde{\mathbf B} \times \mathbf V) \, \textnormal{d} \mathbf{x} \quad \forall \, \mathbf V \in \{H(\textnormal{curl}), H(\textnormal{div}), (H^1)^3\}\,, \,, \\ &\frac{\partial \tilde{\mathbf B}}{\partial t} = - \nabla \times (\tilde{\mathbf B} \times \tilde{\mathbf U}) \,. \end{aligned} \right. :ref:`time_discret`: Crank-Nicolson (implicit mid-point). System size reduction via :class:`~struphy.linear_algebra.schur_solver.SchurSolver`: .. math:: \begin{bmatrix} \mathbf u^{n+1} - \mathbf u^n \\ \mathbf b^{n+1} - \mathbf b^n \end{bmatrix} = \frac{\Delta t}{2} \,. \begin{bmatrix} 0 & (\mathbb M^{2,n})^{-1} \mathcal {T^2}^\top \mathbb C^\top \\ - \mathbb C \mathcal {T^2} (\mathbb M^{2,n})^{-1} & 0 \end{bmatrix} \begin{bmatrix} {\mathbb M^{2,n}}(\mathbf u^{n+1} + \mathbf u^n) \\ \mathbb M_2(\mathbf b^{n+1} + \mathbf b^n) + \sum_k^{N_p} \omega_k \mu_k \hat{\mathbf b}¹_0 (\boldsymbol \eta_k) \cdot \left(\frac{1}{\sqrt{g(\boldsymbol \eta_k)}} \vec \Lambda² (\boldsymbol \eta_k) \right) \end{bmatrix} \,, where :math:`\mathcal{T}^2 = \hat \Pi \left[\frac{\tilde{\mathbf B}^2}{\sqrt{g} \times \vec \Lambda^2\right]` and :math:`\mathbb M^{2,n}` is a :class:`~struphy.feec.mass.WeightedMassOperators` being weighted with :math:`\rho_\text{eq}`, the MHD equilibirum density. """
[docs] class Variables: """Container for variables advanced by :class:`ShearAlfvenCurrentCoupling5D`. Attributes ---------- u : FEECVariable Velocity variable in one of ``"Hcurl"``, ``"Hdiv"``, or ``"H1vec"``. b : FEECVariable Magnetic-field variable in ``"Hdiv"`` space. """ def __init__(self): self._u: FEECVariable = None self._b: FEECVariable = None @property def u(self) -> FEECVariable: return self._u @u.setter def u(self, new): assert isinstance(new, FEECVariable) assert new.space in ("Hcurl", "Hdiv", "H1vec") self._u = new @property def b(self) -> FEECVariable: return self._b @b.setter def b(self, new): assert isinstance(new, FEECVariable) assert new.space == "Hdiv" self._b = new
def __init__(self, energetic_ions: PICVariable = None): """ Parameters ---------- energetic_ions : PICVariable, default=None Energetic-ion particle distribution (``"Particles5D"`` space) providing the current source for the shear Alfvén wave coupling. """ self.variables = self.Variables() self.energetic_ions = energetic_ions
[docs] @dataclass(repr=False) class Options(OptionsBase): """Configuration options for :class:`ShearAlfvenCurrentCoupling5D`. Parameters ---------- ep_scale : float, default=1.0 Scaling factor for particle accumulation terms. u_space : LiteralOptions.OptsVecSpace, default="Hdiv" FEEC space used for velocity variable ``u``. algo : {"implicit", "explicit"}, default="implicit" Time stepping algorithm. solver : LiteralOptions.OptsSymmSolver, default="pcg" Symmetric iterative solver. precond : LiteralOptions.OptsMassPrecond, default="MassMatrixDiagonalPreconditioner" Preconditioner for mass-matrix block. solver_params : SolverParameters, default=None Solver controls. filter_params : FilterParameters, default=None Particle-to-grid filtering parameters. butcher : ButcherTableau, default=None Explicit Runge-Kutta tableau for ``algo="explicit"``. nonlinear : bool, default=True If ``True``, include nonlinear TB operator updates. """ # specific literals OptsAlgo = Literal["implicit", "explicit"] # propagator options ep_scale: float = 1.0 u_space: LiteralOptions.OptsVecSpace = "Hdiv" algo: OptsAlgo = "implicit" solver: LiteralOptions.OptsSymmSolver = "pcg" precond: LiteralOptions.OptsMassPrecond = "MassMatrixDiagonalPreconditioner" solver_params: SolverParameters = None filter_params: FilterParameters = None butcher: ButcherTableau = None nonlinear: bool = True def __post_init__(self): # checks check_option(self.u_space, LiteralOptions.OptsVecSpace) check_option(self.algo, self.OptsAlgo) check_option(self.solver, LiteralOptions.OptsSymmSolver) check_option(self.precond, LiteralOptions.OptsMassPrecond) assert isinstance(self.ep_scale, float) assert isinstance(self.nonlinear, bool) # defaults if self.solver_params is None: self.solver_params = SolverParameters() if self.filter_params is None: self.filter_params = FilterParameters() if self.algo == "explicit" and self.butcher is None: self.butcher = ButcherTableau()
@property def options(self) -> Options: if not hasattr(self, "_options"): self._options = self.Options() return self._options @options.setter def options(self, new): assert isinstance(new, self.Options) self._options = new logger.info(f"\nNew options for propagator '{self.__class__.__name__}':\n{self._options}") @profile def allocate(self): self._u_form = self.derham.space_to_form[self.options.u_space] # call operatros id_M = "M" + self._u_form + "n" id_T = "T" + self._u_form _A = getattr(self.mass_ops, id_M) _T = getattr(self.basis_ops, id_T) M2 = self.mass_ops.M2 curl = self.derham.curl PB = getattr(self.basis_ops, "PB") # define Accumulator and arguments self._ACC = AccumulatorVector( self.energetic_ions.particles, "H1", Pyccelkernel(accum_kernels_gc.gc_mag_density_0form), self.mass_ops, self.domain.args_domain, filter_params=self.options.filter_params, ) # Preconditioner if self.options.precond is None: pc = None else: pc_class = getattr(preconditioner, self.options.precond) pc = pc_class(getattr(self.mass_ops, id_M)) if self.options.nonlinear: # initialize operator TB self._initialize_projection_operator_TB() _T = _T + self._TB _TT = _T.T + self._TBT else: _TT = _T.T if self.options.algo == "implicit": self._info = self.options.solver_params.info # define block matrix [[A B], [C I]] (without time step size dt in the diagonals) self._B = -1 / 2 * _TT @ curl.T @ M2 self._B2 = -1 / 2 * _TT @ curl.T @ PB.T self._C = 1 / 2 * curl @ _T # Instantiate Schur solver (constant in this case) _BC = self._B @ self._C self._schur_solver = SchurSolver( _A, _BC, self.options.solver, precond=pc, solver_params=self.options.solver_params, ) # allocate dummy vectors to avoid temporary array allocations self._u_tmp1 = self.variables.u.spline.vector.space.zeros() self._u_tmp2 = self.variables.u.spline.vector.space.zeros() self._b_tmp1 = self.variables.b.spline.vector.space.zeros() self._byn = self._B.codomain.zeros() self._tmp_acc = self._B2.codomain.zeros() else: self._info = False # define vector field A_inv = inverse( _A, self.options.solver, pc=pc, tol=self.options.solver_params.tol, maxiter=self.options.solver_params.maxiter, verbose=self.options.solver_params.verbose, ) _f1 = A_inv @ _TT @ curl.T @ M2 _f1_acc = A_inv @ _TT @ curl.T @ PB.T _f2 = curl @ _T # allocate output of vector field out_acc = self.variables.u.spline.vector.space.zeros() out1 = self.variables.u.spline.vector.space.zeros() out2 = self.variables.b.spline.vector.space.zeros() def f1(t, y1, y2, out: BlockVector = out1): _f1.dot(y2, out=out) _f1_acc.dot(self._ACC.vectors[0], out=out_acc) out += out_acc out.update_ghost_regions() return out def f2(t, y1, y2, out: BlockVector = out2): _f2.dot(y1, out=out) out *= -1.0 out.update_ghost_regions() return out vector_field = {self.variables.u.spline.vector: f1, self.variables.b.spline.vector: f2} self._ode_solver = ODEsolverFEEC(vector_field, butcher=self.options.butcher) def __call__(self, dt): # update time-dependent operator TB if self.options.nonlinear: self._update_weights_TB() # current FE coeffs un = self.variables.u.spline.vector bn = self.variables.b.spline.vector # accumulate self._ACC(self.options.ep_scale) if self.options.algo == "implicit": # solve for new u coeffs (no tmps created here) byn = self._B.dot(bn, out=self._byn) b2acc = self._B2.dot(self._ACC.vectors[0], out=self._tmp_acc) byn += b2acc un1, info = self._schur_solver(un, byn, dt, out=self._u_tmp1) # new b coeffs (no tmps created here) _u = un.copy(out=self._u_tmp2) _u += un1 bn1 = self._C.dot(_u, out=self._b_tmp1) bn1 *= -dt bn1 += bn diffs = self.update_feec_variables(u=un1, b=bn1) else: self._ode_solver(0.0, dt) if self._info and MPI.COMM_WORLD.Get_rank() == 0: if self.options.algo == "implicit": logger.info(f"Status for ShearAlfvenCurrentCoupling5D: {info['success']}") logger.info(f"Iterations for ShearAlfvenCurrentCoupling5D: {info['niter']}") logger.info(f"Maxdiff up for ShearAlfvenCurrentCoupling5D: {diffs['u']}") logger.info(f"Maxdiff b2 for ShearAlfvenCurrentCoupling5D: {diffs['b']}") logger.info("") def _initialize_projection_operator_TB(self): r"""Initialize BasisProjectionOperator TB with the time-varying weight. .. math:: \mathcal{T}^B_{(\mu,ijk),(\nu,mno)} := \hat \Pi¹_{(\mu,ijk)} \left[ \epsilon_{\mu \alpha \nu} \frac{\tilde{B}²_\alpha}{\sqrt{g}} \Lambda²_{\nu,mno} \right] \,. """ # Call the projector and the space P1 = self.derham.P1 Vh = self.derham.fem_spaces[self._u_form] # Femfield for the field evaluation self._bf = self.derham.create_spline_function("bf", "Hdiv") # Initialize BasisProjectionOperator if self.derham._with_local_projectors: self._TB = BasisProjectionOperatorLocal( P1, Vh, [ [None, None, None], [None, None, None], [None, None, None], ], transposed=False, use_cache=True, polar_shift=True, V_extraction_op=self.derham.extraction_ops[self._u_form], V_boundary_op=self.derham.boundary_ops[self._u_form], P_boundary_op=self.derham.boundary_ops["1"], ) self._TBT = BasisProjectionOperatorLocal( P1, Vh, [ [None, None, None], [None, None, None], [None, None, None], ], transposed=True, use_cache=True, polar_shift=True, V_extraction_op=self.derham.extraction_ops[self._u_form], V_boundary_op=self.derham.boundary_ops[self._u_form], P_boundary_op=self.derham.boundary_ops["1"], ) else: self._TB = BasisProjectionOperator( P1, Vh, [ [None, None, None], [None, None, None], [None, None, None], ], transposed=False, use_cache=True, polar_shift=True, V_extraction_op=self.derham.extraction_ops[self._u_form], V_boundary_op=self.derham.boundary_ops[self._u_form], P_boundary_op=self.derham.boundary_ops["1"], ) self._TBT = BasisProjectionOperator( P1, Vh, [ [None, None, None], [None, None, None], [None, None, None], ], transposed=True, use_cache=True, polar_shift=True, V_extraction_op=self.derham.extraction_ops[self._u_form], V_boundary_op=self.derham.boundary_ops[self._u_form], P_boundary_op=self.derham.boundary_ops["1"], ) def _update_weights_TB(self): """Updats time-dependent weights of the BasisProjectionOperator TB""" # Update Femfield self.variables.b.spline.vector.copy(out=self._bf.vector) # define callable weights def bf1(x, y, z): return self._bf(x, y, z, local=True)[0] def bf2(x, y, z): return self._bf(x, y, z, local=True)[1] def bf3(x, y, z): return self._bf(x, y, z, local=True)[2] from struphy.feec.utilities import LocalRotationMatrix rot_B = LocalRotationMatrix(bf1, bf2, bf3) fun = [] if self._u_form == "v": for m in range(3): fun += [[]] for n in range(3): fun[-1] += [ lambda e1, e2, e3, m=m, n=n: rot_B(e1, e2, e3)[:, :, :, m, n], ] elif self._u_form == "1": for m in range(3): fun += [[]] for n in range(3): fun[-1] += [ lambda e1, e2, e3, m=m, n=n: ( rot_B(e1, e2, e3) @ self.domain.metric_inv( e1, e2, e3, change_out_order=True, squeeze_out=False, ) )[:, :, :, m, n], ] else: for m in range(3): fun += [[]] for n in range(3): fun[-1] += [ lambda e1, e2, e3, m=m, n=n: ( rot_B(e1, e2, e3)[:, :, :, m, n] / abs(self.domain.jacobian_det(e1, e2, e3, squeeze_out=False)) ), ] # update BasisProjectionOperator self._TB.update_weights(fun) self._TBT.update_weights(fun)