Source code for struphy.pic.pushing.pusher

"Accelerated particle pushing."

import logging

import cunumpy as xp
from feectools.ddm.mpi import mpi as MPI
from line_profiler import profile
from scope_profiler import ProfileManager

from struphy.kernel_arguments.pusher_args_kernels import DerhamArguments, DomainArguments
from struphy.pic.base import Particles
from struphy.utils.pyccel import Pyccelkernel

logger = logging.getLogger("struphy")


[docs] class Pusher: r""" Class for solving particle ODEs .. math:: \dot{\mathbf Z}_p(t) = \mathbf U(t, \mathbf Z_p(t))\,, for each marker :math:`p` in :class:`~struphy.pic.base.Particles` class, where :math:`\mathbf Z_p` are the marker coordinates and the vector field :math:`\mathbf U` can contain discrete :class:`~struphy.feec.psydac_derham.Derham` splines and metric coefficients from accelerated :mod:`~struphy.geometry.evaluation_kernels`. The solve is MPI distributed and can handle multi-stage Runge-Kutta methods for any :class:`~struphy.ode.utils.ButcherTableau` as well as iterative nonlinear methods. The particle push is performed via accelerated :mod:`~struphy.pic.pushing.pusher_kernels` or :mod:`~struphy.pic.pushing.pusher_kernels_gc` for guiding-center models. Notes ----- For iterative methods with iteration index :math:`k`, spline evaluations at positions :math:`\alpha_i \eta_{p,i}^{n+1,k} + (1 - \alpha_i) \eta_{p,i}^n` for :math:`i=1, 2, 3` and different :math:`\alpha_i \in [0,1]` need particle MPI sorting in between. This requires calling dedicated ``eval_kernels`` during the iteration. Here are some rules to follow for iterative solvers: * Spline/geometry evaluations at :math:`\boldsymbol \eta^n_p` can be be done via ``init_kernels``. * Pusher ``kernel`` and ``eval_kernels`` can perform evaluations at arbitrary weighted averages :math:`\eta_{p,i} = \alpha_i \eta_{p,i}^{n+1,k} + (1 - \alpha_i) \eta_{p,i}^n`, for :math:`i=1,2,3`. * MPI sorting is done automatically before kernel calls according to the specified values :math:`\alpha_i` for each kernel. Parameters ---------- particles : Particles Particles object holding the markers to push. kernel : pyccelized function The pusher kernel. args_kernel : tuple Optional arguments passed to the kernel. args_domain : DomainArguments Mapping infos. alpha_in_kernel: float | int | tuple | list For i=0,1,2, the spline/geometry evaluations in kernel are at alpha[i]*markers[:, i] + (1 - alpha[i])*markers[:, buffer_idx + i]. If float or int or then alpha = (alpha, alpha, alpha). alpha must be between 0 and 1. alpha[i]=0 means that evaluation is at the initial positions (time n), stored at markers[:, buffer_idx + i]. init_kernels : dict Keys: initialization kernels for spline/ SPH evaluations at time n (initial state). Values: optional arguments. eval_kernels : dict Keys: evaluation kernels for splines before the pusher kernel is called. Values: optional arguments and weighting parameters alpha for sorting (before evaluation), according to alpha[i]*markers[:, i] + (1 - alpha[i])*markers[:, buffer_idx + i] for i=0,1,2. alpha must be between 0 and 1, see :meth:`~struphy.pic.base.Particles.mpi_sort_markers`. n_stages : int Number of stages of the pusher (e.g. 4 for RK4) maxiter : int Maximum number of iterations (=1 for explicit pushers). tol : float Iteration terminates when residual<tol. mpi_sort : str When to do MPI sorting: * None : no sorting at all. * each : sort markers after each stage. * last : sort markers after last stage. """ def __init__( self, particles: Particles, kernel: Pyccelkernel, args_kernel: tuple, args_domain: DomainArguments, *, alpha_in_kernel: float | int | tuple | list, init_kernels: list = [], eval_kernels: list = [], n_stages: int = 1, maxiter: int = 1, tol: float = 1.0e-8, mpi_sort: str = None, ): self._particles = particles assert isinstance(kernel, Pyccelkernel), f"{kernel} is not of type Pyccelkernel" self._kernel = kernel self._newton = "newton" in kernel.name self._args_kernel = args_kernel self._args_domain = args_domain # determines the evaluation points for kernel self._alpha_in_kernel = alpha_in_kernel self._n_stages = n_stages self._maxiter = maxiter self._tol = tol self._mpi_sort = mpi_sort # prepare and check init_kernels for ker_args in init_kernels: assert len(ker_args) == 4 column_nr = ker_args[1] comps = ker_args[2] # check marker array column number assert isinstance(comps, xp.ndarray) assert column_nr + comps.size < particles.n_cols, ( f"{column_nr + comps.size} not smaller than {particles.n_cols =}; not enough columns in marker array !!" ) # prepare and check eval_kernels for ker_args in eval_kernels: assert len(ker_args) == 5 column_nr = ker_args[2] comps = ker_args[3] # check marker array column number assert isinstance(comps, xp.ndarray) assert column_nr + comps.size < particles.n_cols, ( f"{column_nr + comps.size} not smaller than {particles.n_cols =}; not enough columns in marker array !!" ) self._init_kernels = init_kernels self._eval_kernels = eval_kernels self._residuals = xp.zeros(self.particles.markers.shape[0]) self._converged_loc = self._residuals == 1.0 self._not_converged_loc = self._residuals == 0.0 if self.particles.sorting_boxes is not None: self._box_comm = self.particles.sorting_boxes.communicate else: self._box_comm = False
[docs] @profile def __call__(self, dt: float): """ Applies the chosen pusher kernel by a time step dt, applies kinetic boundary conditions and performs MPI sorting. """ # some idx and slice markers = self.particles.markers vdim = self.particles.vdim first_pusher_idx = self.particles.first_pusher_idx first_shift_idx = self.particles.first_shift_idx residual_idx = self.particles.residual_idx logger.debug(f"{first_pusher_idx =}") logger.debug(f"{first_shift_idx =}") logger.debug(f"{residual_idx =}") logger.debug(f"{self.particles.n_cols =}") init_slice = slice(first_pusher_idx, first_shift_idx) shift_slice = slice(first_shift_idx, residual_idx) # save initial phase space coordinates markers[:, init_slice] = markers[:, : 3 + vdim] # set boundary shifts to zero markers[:, shift_slice] = 0.0 # clear buffer columns starting from residual index, dont clear ID (last column) and loc_box markers[:, residual_idx:-2] = 0.0 rank = self.particles.mpi_rank logger.debug(f"rank {rank}: starting {self.kernel} ...") # if init_kernels is not empty, do evaluations at initial positions 0:3 for ker_args in self.init_kernels: ker = ker_args[0] column_nr = ker_args[1] comps = ker_args[2] add_args = ker_args[3] ker( xp.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), column_nr, comps, self.particles.args_markers, self._args_domain, *add_args, ) # update boxes if self._box_comm: self.particles.put_particles_in_boxes() # start stages (e.g. n_stages=4 for RK4) for stage in range(self.n_stages): # start iteration (maxiter=1 for explicit schemes) n_not_converged = xp.empty(1, dtype=int) n_not_converged[0] = self.particles.n_mks_loc k = 0 if self.maxiter > 1: max_res = 1.0 logger.debug( f"rank {rank}: {k =}, tol: {self._tol}, {n_not_converged[0] =}, {max_res =}", ) n_not_converged[0] = self.particles.Np while True: k += 1 # if eval_kernels is not empty, do spline evaluations for ker_args in self.eval_kernels: ker = ker_args[0] alpha = ker_args[1] column_nr = ker_args[2] comps = ker_args[3] add_args = ker_args[4] # sort according to alpha-weighted average if self.particles.mpi_comm is not None: self.particles.mpi_sort_markers( apply_bc=False, alpha=alpha[:3], remove_ghost=False, ) # evaluate ker( alpha, column_nr, comps, self.particles.args_markers, self._args_domain, *add_args, ) # update boxes if self._box_comm: self.particles.put_particles_in_boxes() # sort according to alpha-weighted average if self.particles.mpi_comm is not None: self.particles.mpi_sort_markers( apply_bc=False, alpha=self._alpha_in_kernel, remove_ghost=False, ) # push markers with ProfileManager.profile_region("kernel: " + self.kernel.name): self.kernel( dt, stage, self.particles.args_markers, self._args_domain, *self._args_kernel, ) self.particles.apply_kinetic_bc(newton=self._newton) self.particles.update_holes() # update boxes if self._box_comm: self.particles.put_particles_in_boxes() # compute number of non-converged particles (maxiter=1 for explicit schemes) if self.maxiter > 1: self._residuals[:] = markers[:, residual_idx] max_res = xp.max(self._residuals) if max_res < 0.0: max_res = None self._converged_loc[:] = self._residuals < self._tol self._not_converged_loc[:] = ~self._converged_loc n_not_converged[0] = xp.count_nonzero( self._not_converged_loc, ) logger.debug( f"rank {rank}: {k =}, tol: {self._tol}, {n_not_converged[0] =}, {max_res =}", ) if self.particles.mpi_comm is not None: self.particles.mpi_comm.Allreduce( MPI.IN_PLACE, n_not_converged, op=MPI.SUM, ) # take converged markers out of the loop markers[self._converged_loc, first_pusher_idx] = -1.0 # maxiter=1 for explicit schemes if k == self.maxiter: if self.maxiter > 1: rank = self.particles.mpi_rank logger.info( f"rank {rank}: {k =}, maxiter={self.maxiter} reached! tol: {self._tol}, {n_not_converged[0] =}, {max_res =}", ) # sort markers according to domain decomposition if self.mpi_sort == "each": if self.particles.mpi_comm is not None: self.particles.mpi_sort_markers() else: self.particles.apply_kinetic_bc() break # check for convergence if n_not_converged[0] == 0: # sort markers according to domain decomposition if self.mpi_sort == "each": if self.particles.mpi_comm is not None: self.particles.mpi_sort_markers() else: self.particles.apply_kinetic_bc() break # print stage info logger.debug( f"rank {rank}: stage {stage + 1} of {self.n_stages} done.", ) # sort markers according to domain decomposition if self.mpi_sort == "last": if self.particles.mpi_comm is not None: self.particles.mpi_sort_markers(do_test=True) else: self.particles.apply_kinetic_bc()
@property def particles(self): """Particle object.""" return self._particles @property def kernel(self): """The pyccelized pusher kernel.""" return self._kernel @property def init_kernels(self): """A dict of kernels for initial spline evaluation before iteration.""" return self._init_kernels @property def eval_kernels(self): """A dict of kernels for spline evaluation before execution of kernel during iteration.""" return self._eval_kernels @property def args_kernel(self): """Optional arguments for kernel.""" return self._args_kernel @property def args_domain(self): """Mandatory Domain arguments.""" return self._args_domain @property def n_stages(self): """Number of stages of the pusher.""" return self._n_stages @property def maxiter(self): """Maximum number of iterations (=1 for explicit pushers).""" return self._maxiter @property def tol(self): """Iteration terminates when residual<tol.""" return self._tol @property def mpi_sort(self): """When to do MPI sorting: * None : no sorting at all. * each : sort markers after each stage. * last : sort markers after last stage. """ return self._mpi_sort