Source code for struphy.propagators.pressure_coupling_6d

"Particle and FEEC variables are updated."

import logging
from dataclasses import dataclass

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

from struphy.feec import preconditioner
from struphy.feec.linear_operators import LinOpWithTransp
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.pic.accumulation import accum_kernels
from struphy.pic.accumulation.filter import FilterParameters
from struphy.pic.accumulation.particles_to_grid import Accumulator
from struphy.pic.pushing import pusher_kernels
from struphy.pic.pushing.pusher import Pusher
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 PressureCoupling6D(Propagator): r""" :ref:`FEEC <gempic>` discretization of the following equations: find :math:`\tilde{\mathbf{U}} \in \{H(\textnormal{curl}), H(\textnormal{div}), (H^1)^3\}` and :math:`f` such that .. math:: \int_\Omega \rho_0 &\frac{\partial \tilde{\mathbf{U}}}{\partial t} \cdot \mathbf V \,\textrm d \mathbf x = - \frac{A_\textnormal{h}}{A_\textnormal{b}} \nabla \cdot \tilde{\mathbb{P}}_{\textnormal{h},\perp} \cdot \mathbf V \,\textrm d \mathbf x \qquad \forall \, \mathbf V \in \{H(\textnormal{curl}), H(\textnormal{div}), (H^1)^3\}\,, \\[2mm] &\frac{\partial f_\textnormal{h}}{\partial t} - \left(\nabla \tilde{\mathbf U}_\perp \cdot \mathbf{v} \right) \cdot \frac{\partial f_\textnormal{h}}{\partial \mathbf{v}} =0\,, \\[2mm] &\tilde{\mathbb{P}}_{\textnormal{h},\perp} = \int_{\mathbb{R}^3}f_\textnormal{h}\mathbf{v}_\perp \mathbf{v}_\perp^\top \,\textnormal{d}^3 \mathbf v\,. :ref:`time_discret`: Crank-Nicolson (implicit mid-point). System size reduction via :class:`~struphy.linear_algebra.schur_solver.SchurSolver`: .. math:: \begin{bmatrix} u^{n+1} - u^n \\ V^{n+1} - V^n \end{bmatrix} = \frac{\Delta t}{2} \begin{bmatrix} 0 & (\mathbb M^n)^{-1} V^\top (\bar {\mathcal X})^\top \mathbb G^\top (\bar {\mathbf \Lambda}^1)^\top \bar {DF}^{-1} \\ - {DF}^{-\top} \bar {\mathbf \Lambda}^1 \mathbb G \bar {\mathcal X} V (\mathbb M^n)^{-1} & 0 \end{bmatrix} \begin{bmatrix} {\mathbb M^n}(u^{n+1} + u^n) \\ \bar W (V^{n+1} + V^{n} \end{bmatrix} \,. """
[docs] class Variables: """Container for variables advanced by :class:`PressureCoupling6D`. Attributes ---------- u : FEECVariable Fluid-like FEEC variable in one of ``"Hcurl"``, ``"Hdiv"``, or ``"H1vec"``. energetic_ions : PICVariable Energetic-ion particle variable in ``"Particles6D"`` space. """ def __init__(self): self._u: FEECVariable = None self._energetic_ions: PICVariable = 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 energetic_ions(self) -> PICVariable: return self._energetic_ions @energetic_ions.setter def energetic_ions(self, new): assert isinstance(new, PICVariable) assert new.space == "Particles6D" self._energetic_ions = new
def __init__(self): self.variables = self.Variables()
[docs] @dataclass(repr=False) class Options(OptionsBase): """Configuration options for :class:`PressureCoupling6D`. Parameters ---------- ep_scale : float, default=1.0 Scaling factor applied to energetic-particle pressure coupling terms. u_space : LiteralOptions.OptsVecSpace, default="Hdiv" FEEC space used for the unknown ``u`` variable. solver : LiteralOptions.OptsSymmSolver, default="pcg" Symmetric iterative solver used by :class:`SchurSolver`. precond : LiteralOptions.OptsMassPrecond, default="MassMatrixPreconditioner" Preconditioner applied to the mass matrix block for ``u``. solver_params : SolverParameters, default=None Iterative-solver controls. If ``None``, defaults to ``SolverParameters()``. filter_params : FilterParameters, default=None Particle-to-grid filtering parameters used by the accumulator. If ``None``, defaults to ``FilterParameters()``. use_perp_model : bool, default=True If ``True``, use the perpendicular pressure-coupling model; otherwise use the full-model kernels. """ # propagator options ep_scale: float = 1.0 u_space: LiteralOptions.OptsVecSpace = "Hdiv" solver: LiteralOptions.OptsSymmSolver = "pcg" precond: LiteralOptions.OptsMassPrecond = "MassMatrixPreconditioner" solver_params: SolverParameters = None filter_params: FilterParameters = None use_perp_model: bool = True def __post_init__(self): # checks check_option(self.u_space, LiteralOptions.OptsVecSpace) check_option(self.solver, LiteralOptions.OptsSymmSolver) check_option(self.precond, LiteralOptions.OptsMassPrecond) assert isinstance(self.ep_scale, float) assert isinstance(self.use_perp_model, bool) # defaults if self.solver_params is None: self.solver_params = SolverParameters() if self.filter_params is None: self.filter_params = FilterParameters()
@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): if self.options.u_space == "H1vec": self._u_form_int = 0 else: self._u_form_int = int(self.derham.space_to_form[self.options.u_space]) if self.options.u_space == "Hcurl": id_Mn = "M1n" id_X = "X1" elif self.options.u_space == "Hdiv": id_Mn = "M2n" id_X = "X2" elif self.options.u_space == "H1vec": id_Mn = "Mvn" id_X = "Xv" # call operatros id_M = "M" + self.derham.space_to_form[self.options.u_space] + "n" _A = getattr(self.mass_ops, id_M) self._X = getattr(self.basis_ops, id_X) self._XT = self._X.transpose() grad = self.derham.grad gradT = grad.transpose() # 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)) # Call the accumulation and Pusher class if self.options.use_perp_model: accum_ker = Pyccelkernel(accum_kernels.pc_lin_mhd_6d) pusher_ker = Pyccelkernel(pusher_kernels.push_pc_GXu) else: accum_ker = Pyccelkernel(accum_kernels.pc_lin_mhd_6d_full) pusher_ker = Pyccelkernel(pusher_kernels.push_pc_GXu_full) # define Accumulator and arguments self._ACC = Accumulator( self.variables.energetic_ions.particles, "Hcurl", # TODO:check accum_ker, self.mass_ops, self.domain.args_domain, add_vector=True, symmetry="pressure", filter_params=self.options.filter_params, ) self._tmp_g1 = grad.codomain.zeros() self._tmp_g2 = grad.codomain.zeros() self._tmp_g3 = grad.codomain.zeros() # instantiate Pusher args_pusher_kernel = ( self.derham.args_derham, self._tmp_g1[0]._data, self._tmp_g1[1]._data, self._tmp_g1[2]._data, self._tmp_g2[0]._data, self._tmp_g2[1]._data, self._tmp_g2[2]._data, self._tmp_g3[0]._data, self._tmp_g3[1]._data, self._tmp_g3[2]._data, ) self._pusher = Pusher( self.variables.energetic_ions.particles, pusher_ker, args_pusher_kernel, self.domain.args_domain, alpha_in_kernel=1.0, ) self.u_temp = self.variables.u.spline.vector.space.zeros() self.u_temp2 = self.variables.u.spline.vector.space.zeros() self._tmp = self._X.codomain.zeros() self._BV = self.variables.u.spline.vector.space.zeros() self._MAT = [ [self._ACC.operators[0], self._ACC.operators[1], self._ACC.operators[2]], [self._ACC.operators[1], self._ACC.operators[3], self._ACC.operators[4]], [self._ACC.operators[2], self._ACC.operators[4], self._ACC.operators[5]], ] self._GT_VEC = BlockVector(self.derham.Vv) _BC = -1 / 4 * self._XT @ self.GT_MAT_G(self.derham, self._MAT) @ self._X self._schur_solver = SchurSolver( _A, _BC, self.options.solver, precond=pc, solver_params=self.options.solver_params, ) def __call__(self, dt): # current FE coeffs un = self.variables.u.spline.vector # operators grad = self.derham.grad gradT = grad.transpose() # acuumulate MAT and VEC self._ACC( self.options.ep_scale, ) # update GT_VEC for i in range(3): self._GT_VEC[i] = gradT.dot(self._ACC.vectors[i]) self._BV = self._XT.dot(self._GT_VEC) * (-1 / 2) # update u (no tmps created here) un1, info = self._schur_solver(un, self._BV, dt, out=self.u_temp) _u = un.copy(out=self.u_temp2) _u += un1 # calculate GXu Xu = self._X.dot(_u, out=self._tmp) GXu_1 = grad.dot(Xu[0], out=self._tmp_g1) GXu_2 = grad.dot(Xu[1], out=self._tmp_g2) GXu_3 = grad.dot(Xu[2], out=self._tmp_g3) GXu_1.update_ghost_regions() GXu_2.update_ghost_regions() GXu_3.update_ghost_regions() # push particles self._pusher(dt) # write new coeffs into Propagator.variables diffs = self.update_feec_variables(u=un1) # update weights in case of control variate if self.variables.energetic_ions.species.weights_params.control_variate: self.variables.energetic_ions.particles.update_weights() if self.options.solver_params.info and MPI.COMM_WORLD.Get_rank() == 0: logger.info(f"Status for StepPressurecoupling: {info['success']}") logger.info(f"Iterations for StepPressurecoupling: {info['niter']}") logger.info(f"Maxdiff u1 for StepPressurecoupling: {diffs['u']}") logger.info("")
[docs] class GT_MAT_G(LinOpWithTransp): r""" Class for defining LinearOperator corresponding to :math:`G^\top (\text{MAT}) G \in \mathbb{R}^{3N^0 \times 3N^0}` where :math:`\text{MAT} = V^\top (\bar {\mathbf \Lambda}^1)^\top \bar{DF}^{-1} \bar{W} \bar{DF}^{-\top} \bar{\mathbf \Lambda}^1 V \in \mathbb{R}^{3N^1 \times 3N^1}`. Parameters ---------- derham : struphy.feec.psydac_derham.Derham Discrete de Rham sequence on the logical unit cube. MAT : List of StencilMatrices List with six of accumulated pressure terms """ def __init__(self, derham, MAT, transposed=False): self._derham = derham self._grad = derham.grad self._gradT = derham.grad.transpose() self._domain = derham.Vv self._codomain = derham.Vv self._MAT = MAT self._vector = BlockVector(derham.Vv) self._temp = BlockVector(derham.V1) @property def domain(self): return self._domain @property def codomain(self): return self._codomain @property def dtype(self): return self._derham.Vv.dtype @property def tosparse(self): raise NotImplementedError() @property def toarray(self): raise NotImplementedError() @property def transposed(self): return self._transposed
[docs] def transpose(self): return self.GT_MAT_G(self._derham, self._MAT, True)
[docs] def dot(self, v, out=None): """dot product between GT_MAT_G and v. Parameters ---------- v : StencilVector or BlockVector Input FE coefficients from V.coeff_space. Returns ------- A StencilVector or BlockVector from W.coeff_space.""" assert v.space == self.domain v.update_ghost_regions() for i in range(3): for j in range(3): self._temp += self._MAT[i][j].dot(self._grad.dot(v[j])) self._vector[i] = self._gradT.dot(self._temp) self._temp *= 0.0 self._vector.update_ghost_regions() if out is not None: self._vector.copy(out=out) assert self._vector.space == self.codomain return self._vector