Source code for struphy.propagators.current_coupling_5d_gradb

"Particle and FEEC variables are updated."

import logging
from dataclasses import dataclass
from typing import Literal

import cunumpy as xp
from feectools.linalg.solvers import inverse
from line_profiler import profile
from maybempi import MPI

from struphy.feec import preconditioner
from struphy.io.options import LiteralOptions, OptionsBase
from struphy.linear_algebra.solver import DiscreteGradientSolverParameters, SolverParameters
from struphy.models.variables import FEECVariable, PICVariable
from struphy.ode.utils import ButcherTableau
from struphy.pic.accumulation.filter import FilterParameters
from struphy.pic.accumulation.kernels.cc_lin_mhd_5d_gradB import cc_lin_mhd_5d_gradB
from struphy.pic.accumulation.kernels.cc_lin_mhd_5d_gradB_dg import cc_lin_mhd_5d_gradB_dg
from struphy.pic.accumulation.kernels.cc_lin_mhd_5d_gradB_dg_init import cc_lin_mhd_5d_gradB_dg_init
from struphy.pic.accumulation.particles_to_grid import Accumulator, AccumulatorVector
from struphy.pic.diagnostics.kernels.eval_gradB_ediff import eval_gradB_ediff
from struphy.pic.pushing.kernels.push_gc_cc_J2_dg_Hdiv import push_gc_cc_J2_dg_Hdiv
from struphy.pic.pushing.kernels.push_gc_cc_J2_dg_init_Hdiv import push_gc_cc_J2_dg_init_Hdiv
from struphy.pic.pushing.kernels.push_gc_cc_J2_stage_H1vec import push_gc_cc_J2_stage_H1vec
from struphy.pic.pushing.kernels.push_gc_cc_J2_stage_Hdiv import push_gc_cc_J2_stage_Hdiv
from struphy.propagators.base import Propagator
from struphy.utils.utils import check_option

logger = logging.getLogger("struphy")


[docs] class CurrentCoupling5DGradB(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 = - \frac{A_\textnormal{h}}{A_b} \iint \mu \frac{f^\text{vol}}{B^*_\parallel} (\mathbf b_0 \times \nabla B_\parallel) \times \mathbf B \cdot \mathbf V \,\textnormal{d} \mathbf x \textnormal{d} v_\parallel \textnormal{d} \mu \quad \forall \,\mathbf V \in \{H(\textnormal{curl}), H(\textnormal{div}), (H^1)^3\}\,, \\ &\frac{\partial f}{\partial t} = \frac{1}{B^*_\parallel} \left[ \mathbf b_0 \times (\tilde{\mathbf U} \times \mathbf B) \right] \cdot \nabla f\,. \end{aligned} \right. :ref:`time_discret`: Explicit ('rk4', 'forward_euler', 'heun2', 'rk2', 'heun3'). .. math:: \begin{bmatrix} \dot{\mathbf u}\\ \dot{\mathbf H} \end{bmatrix} = \begin{bmatrix} 0 & (\mathbb{M}^{2,n})^{-1} \mathbb{L}² \frac{1}{\bar{\sqrt{g}}} \frac{1}{\bar B^{*0}_\parallel}\bar{B}^\times_f \bar{G}^{-1} \bar{b}^\times_0 \bar{G}^{-1} \\ -\bar{G}^{-1} \bar{b}^\times_0 \bar{G}^{-1} \bar{B}^\times_f \frac{1}{\bar B^{*0}_\parallel} \frac{1}{\bar{\sqrt{g}}} (\mathbb{L}²)^\top (\mathbb{M}^{2,n})^{-1} & 0 \end{bmatrix} \begin{bmatrix} \mathbb M^{2,n} \mathbf u \\ \frac{A_\textnormal{h}}{A_b} \bar M \bar W \overline{\nabla B}_\parallel \end{bmatrix} \,. For the detail explanation of the notations, see `2022_DriftKineticCurrentCoupling <https://gitlab.mpcdf.mpg.de/struphy/struphy-projects/-/blob/main/running-projects/2022_DriftKineticCurrentCoupling.md?ref_type=heads>`_. """
[docs] class Variables: """Container for variables advanced by :class:`CurrentCoupling5DGradB`. Attributes ---------- u : FEECVariable Fluid-like FEEC variable in one of ``"Hcurl"``, ``"Hdiv"``, or ``"H1vec"``. energetic_ions : PICVariable Energetic-ion particle variable in ``"Particles5D"`` 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 "Particles5D" in new.space self._energetic_ions = new
def __init__(self, b_tilde: FEECVariable = None): """ Parameters ---------- b_tilde : FEECVariable, default=None Magnetic perturbation 1-form (``"Hcurl"`` space) entering the grad-B coupling term. If ``None``, only the equilibrium field is used. """ self.variables = self.Variables() self.b_tilde = b_tilde
[docs] @dataclass(repr=False) class Options(OptionsBase): """Configuration options for :class:`CurrentCoupling5DGradB`. Parameters ---------- ep_scale : float, default=1.0 Scaling factor applied in particle accumulation. algo : {"discrete_gradient", "explicit"}, default="explicit" Time integration algorithm. - ``"explicit"``: explicit Runge-Kutta update with ``butcher``. - ``"discrete_gradient"``: nonlinear discrete-gradient update. butcher : ButcherTableau, default=None Butcher tableau used in explicit mode. If ``None`` and ``algo="explicit"``, defaults to ``ButcherTableau()``. u_space : LiteralOptions.OptsVecSpace, default="Hdiv" FEEC space used for the unknown ``u`` variable. ``"Hdiv"`` and ``"H1vec"`` are supported for ``algo="explicit"``, only ``"Hdiv"`` for ``algo="discrete_gradient"``. solver : LiteralOptions.OptsSymmSolver, default="pcg" Symmetric iterative solver used for mass-matrix inversions. precond : LiteralOptions.OptsMassPrecond, default="MassMatrixPreconditioner" Preconditioner used with the mass matrix. 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()``. dg_solver_params : DiscreteGradientSolverParameters, default=None Nonlinear solver controls for discrete-gradient mode. If ``None`` and ``algo="discrete_gradient"``, defaults to ``DiscreteGradientSolverParameters()``. """ # specific literals OptsAlgo = Literal[ "discrete_gradient", "explicit", ] # propagator options ep_scale: float = 1.0 algo: OptsAlgo = "explicit" butcher: ButcherTableau = None u_space: LiteralOptions.OptsVecSpace = "Hdiv" solver: LiteralOptions.OptsSymmSolver = "pcg" precond: LiteralOptions.OptsMassPrecond = "MassMatrixPreconditioner" solver_params: SolverParameters = None filter_params: FilterParameters = None dg_solver_params: DiscreteGradientSolverParameters = None def __post_init__(self): # checks check_option(self.algo, self.OptsAlgo) 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) # supported u_space per algorithm (pusher kernels exist only for these) if self.algo == "explicit": supported_u_spaces = ("Hdiv", "H1vec") else: supported_u_spaces = ("Hdiv",) if self.u_space not in supported_u_spaces: raise ValueError( f"{self.u_space = } is not supported by CurrentCoupling5DGradB with {self.algo = }, " f"choose from {supported_u_spaces}.", ) # defaults if self.algo == "explicit" and self.butcher is None: self.butcher = ButcherTableau() if self.algo == "discrete_gradient" and self.dg_solver_params is None: self.dg_solver_params = DiscreteGradientSolverParameters() 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]) # call operatros id_M = "M" + self.derham.space_to_form[self.options.u_space] + "n" self._A = getattr(self.mass_ops, id_M) self._PB = getattr(self.basis_ops, "PB") # 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)) # linear solver self._A_inv = inverse( self._A, self.options.solver, pc=pc, tol=self.options.solver_params.tol, maxiter=self.options.solver_params.maxiter, verbose=self.options.solver_params.verbose, recycle=self.options.solver_params.recycle, ) # magnetic equilibrium field unit_b1 = self.projected_equil.unit_b1 curl_unit_b2 = self.projected_equil.curl_unit_b2 self._b2 = self.projected_equil.b2 gradB1 = self.projected_equil.gradB1 absB0 = self.projected_equil.absB0 # scaling factor epsilon = self.variables.energetic_ions.species.equation_params.epsilon if self.options.algo == "explicit": # temporary vectors to avoid memory allocation self._b_full = self._b2.space.zeros() self._u_new = self.variables.u.spline.vector.space.zeros() self._u_temp = self.variables.u.spline.vector.space.zeros() self._ku = self.variables.u.spline.vector.space.zeros() self._PB_b = self._PB.codomain.zeros() self._grad_PB_b = self.derham.grad.codomain.zeros() # define Accumulator and arguments self._ACC = Accumulator( self.variables.energetic_ions.particles, self.options.u_space, cc_lin_mhd_5d_gradB, self.mass_ops, self.domain.args_domain, add_vector=True, symmetry="symm", filter_params=self.options.filter_params, ) self._args_accum_kernel = ( epsilon, self.options.ep_scale, self._b_full[0]._data, self._b_full[1]._data, self._b_full[2]._data, unit_b1[0]._data, unit_b1[1]._data, unit_b1[2]._data, curl_unit_b2[0]._data, curl_unit_b2[1]._data, curl_unit_b2[2]._data, self._grad_PB_b[0]._data, self._grad_PB_b[1]._data, self._grad_PB_b[2]._data, gradB1[0]._data, gradB1[1]._data, gradB1[2]._data, self._u_form_int, ) # define Pusher if self.options.u_space == "Hdiv": self._pusher_kernel = push_gc_cc_J2_stage_Hdiv elif self.options.u_space == "H1vec": self._pusher_kernel = push_gc_cc_J2_stage_H1vec else: raise ValueError( f'{self.options.u_space =} not valid, choose from "Hdiv" or "H1vec".', ) # temp fix due to refactoring of ButcherTableau: self._args_pusher_kernel = ( self.domain.args_domain, self.derham.args_derham, epsilon, self._b_full[0]._data, self._b_full[1]._data, self._b_full[2]._data, unit_b1[0]._data, unit_b1[1]._data, unit_b1[2]._data, curl_unit_b2[0]._data, curl_unit_b2[1]._data, curl_unit_b2[2]._data, self._u_temp[0]._data, self._u_temp[1]._data, self._u_temp[2]._data, self.options.butcher.a_stage, self.options.butcher.b, self.options.butcher.c, ) else: # temporary vectors to avoid memory allocation self._b_full = self._b2.space.zeros() self._PB_b = self._PB.codomain.zeros() self._grad_PB_b = self.derham.grad.codomain.zeros() self._u_old = self.variables.u.spline.vector.space.zeros() self._u_new = self.variables.u.spline.vector.space.zeros() self._u_diff = self.variables.u.spline.vector.space.zeros() self._u_mid = self.variables.u.spline.vector.space.zeros() self._M2n_dot_u = self.variables.u.spline.vector.space.zeros() self._ku = self.variables.u.spline.vector.space.zeros() self._u_temp = self.variables.u.spline.vector.space.zeros() # magnetic perturbation (zero if not given) if self.b_tilde is None: b_tilde = self._b2.space.zeros() else: b_tilde = self.b_tilde.spline.vector # Call the accumulation and Pusher class accum_kernel_init = cc_lin_mhd_5d_gradB_dg_init accum_kernel = cc_lin_mhd_5d_gradB_dg self._accum_kernel_en_fB_mid = eval_gradB_ediff self._args_accum_kernel = ( epsilon, self.options.ep_scale, b_tilde[0]._data, b_tilde[1]._data, b_tilde[2]._data, self._b2[0]._data, self._b2[1]._data, self._b2[2]._data, unit_b1[0]._data, unit_b1[1]._data, unit_b1[2]._data, curl_unit_b2[0]._data, curl_unit_b2[1]._data, curl_unit_b2[2]._data, self._grad_PB_b[0]._data, self._grad_PB_b[1]._data, self._grad_PB_b[2]._data, gradB1[0]._data, gradB1[1]._data, gradB1[2]._data, self._u_form_int, ) self._args_accum_kernel_en_fB_mid = ( self.domain.args_domain, self.derham.args_derham, gradB1[0]._data, gradB1[1]._data, gradB1[2]._data, self._grad_PB_b[0]._data, self._grad_PB_b[1]._data, self._grad_PB_b[2]._data, ) self._ACC_init = AccumulatorVector( self.variables.energetic_ions.particles, self.options.u_space, accum_kernel_init, self.mass_ops, self.domain.args_domain, filter_params=self.options.filter_params, ) self._ACC = AccumulatorVector( self.variables.energetic_ions.particles, self.options.u_space, accum_kernel, self.mass_ops, self.domain.args_domain, filter_params=self.options.filter_params, ) self._args_pusher_kernel_init = ( self.domain.args_domain, self.derham.args_derham, epsilon, self._b_full[0]._data, self._b_full[1]._data, self._b_full[2]._data, unit_b1[0]._data, unit_b1[1]._data, unit_b1[2]._data, curl_unit_b2[0]._data, curl_unit_b2[1]._data, curl_unit_b2[2]._data, self.variables.u.spline.vector[0]._data, self.variables.u.spline.vector[1]._data, self.variables.u.spline.vector[2]._data, ) self._args_pusher_kernel = ( self.domain.args_domain, self.derham.args_derham, epsilon, self._b_full[0]._data, self._b_full[1]._data, self._b_full[2]._data, unit_b1[0]._data, unit_b1[1]._data, unit_b1[2]._data, curl_unit_b2[0]._data, curl_unit_b2[1]._data, curl_unit_b2[2]._data, self._u_mid[0]._data, self._u_mid[1]._data, self._u_mid[2]._data, self._u_temp[0]._data, self._u_temp[1]._data, self._u_temp[2]._data, ) self._pusher_kernel_init = push_gc_cc_J2_dg_init_Hdiv self._pusher_kernel = push_gc_cc_J2_dg_Hdiv def __call__(self, dt): # current FE coeffs un = self.variables.u.spline.vector # particle markers and idx particles = self.variables.energetic_ions.particles holes = particles.holes args_markers = particles.args_markers markers = args_markers.markers first_pusher_idx = args_markers.first_pusher_idx first_free_idx = args_markers.first_free_idx # clear buffer markers[:, first_pusher_idx:-2] = 0.0 # save old marker positions markers[:, first_pusher_idx : first_pusher_idx + 3] = markers[:, :3] # sum up total magnetic field b_full1 = b_eq + b_tilde (in-place) b_full = self._b2.copy(out=self._b_full) if self.b_tilde is not None: b_full += self.b_tilde.spline.vector b_full.update_ghost_regions() if self.options.algo == "explicit": if self.b_tilde is not None: PB_b = self._PB.dot(self.b_tilde.spline.vector, out=self._PB_b) grad_PB_b = self.derham.grad.dot(PB_b, out=self._grad_PB_b) grad_PB_b.update_ghost_regions() # save old u u_new = un.copy(out=self._u_new) u_temp = un.copy(out=self._u_temp) for stage in range(self.options.butcher.n_stages): # accumulate self._ACC( *self._args_accum_kernel, ) # push particles self._pusher_kernel( dt, stage, args_markers, *self._args_pusher_kernel, ) # kinetic boundary conditions are applied per marker inside the kernel particles.finish_kernel_bc() if particles.mpi_comm is not None: particles.mpi_sort_markers(apply_bc=False) # solve linear system for updating u coefficients ku = self._A_inv.dot(self._ACC.vectors[0], out=self._ku) info = self._A_inv._info # calculate u^{n+1}_k u_temp = un.copy(out=self._u_temp) u_temp += ku * dt * self.options.butcher.a_stage[stage] u_temp.update_ghost_regions() # calculate u^{n+1} u_new += ku * dt * self.options.butcher.b[stage] u_new.update_ghost_regions() if self.options.solver_params.info and MPI.COMM_WORLD.Get_rank() == 0: logger.info(f"Stage: {stage}") logger.info(f"Status for CurrentCoupling5DGradB: {info['success']}") logger.info(f"Iterations for CurrentCoupling5DGradB: {info['niter']}") logger.info("") # update u coefficients diffs = self.update_feec_variables(u=u_new) # clear the buffer markers[:, first_pusher_idx:-2] = 0.0 # update_weights if self.variables.energetic_ions.species.weights_params.control_variate: particles.update_weights() if self.options.solver_params.info and MPI.COMM_WORLD.Get_rank() == 0: logger.info(f"Maxdiff up for CurrentCoupling5DGradB: {diffs['u']}") logger.info("") else: # total number of markers n_mks_tot = particles.Np # relaxation factor alpha = self.options.dg_solver_params.relaxation_factor # eval parallel tilde b and its gradient PB_b = self._PB_b if self.b_tilde is not None: PB_b = self._PB.dot(self.b_tilde.spline.vector, out=self._PB_b) PB_b.update_ghost_regions() grad_PB_b = self.derham.grad.dot(PB_b, out=self._grad_PB_b) grad_PB_b.update_ghost_regions() # save old u u_old = un.copy(out=self._u_old) u_new = un.copy(out=self._u_new) # save en_U_old self._A.dot(un, out=self._M2n_dot_u) en_U_old = un.inner(self._M2n_dot_u) / 2.0 # save en_fB_old particles.save_magnetic_energy(PB_b) en_fB_old = xp.sum(markers[~holes, 8].dot(markers[~holes, 5])) * self.options.ep_scale en_fB_old /= n_mks_tot buffer_array = xp.array([en_fB_old]) if particles.mpi_comm is not None: particles.mpi_comm.Allreduce( MPI.IN_PLACE, buffer_array, op=MPI.SUM, ) if particles.clone_config is not None: particles.clone_config.inter_comm.Allreduce( MPI.IN_PLACE, buffer_array, op=MPI.SUM, ) en_fB_old = buffer_array[0] en_tot_old = en_U_old + en_fB_old # initial guess self._ACC_init(*self._args_accum_kernel) ku = self._A_inv.dot(self._ACC_init.vectors[0], out=self._ku) u_new += ku * dt u_new.update_ghost_regions() # save en_U_new self._A.dot(u_new, out=self._M2n_dot_u) en_U_new = u_new.inner(self._M2n_dot_u) / 2.0 # push eta self._pusher_kernel_init( dt, args_markers, *self._args_pusher_kernel_init, ) if particles.mpi_comm is not None: particles.mpi_sort_markers(apply_bc=False) # save en_fB_new particles.save_magnetic_energy(PB_b) en_fB_new = xp.sum(markers[~holes, 8].dot(markers[~holes, 5])) * self.options.ep_scale en_fB_new /= n_mks_tot buffer_array = xp.array([en_fB_new]) if particles.mpi_comm is not None: particles.mpi_comm.Allreduce( MPI.IN_PLACE, buffer_array, op=MPI.SUM, ) if particles.clone_config is not None: particles.clone_config.inter_comm.Allreduce( MPI.IN_PLACE, buffer_array, op=MPI.SUM, ) en_fB_new = buffer_array[0] # fixed-point iterations iter_num = 0 while True: iter_num += 1 if self.options.dg_solver_params.verbose and MPI.COMM_WORLD.Get_rank() == 0: logger.info(f"# of iteration: {iter_num}") # calculate discrete gradient # save u^{n+1, k} u_old = u_new.copy(out=self._u_old) u_diff = u_old.copy(out=self._u_diff) u_diff -= un u_diff.update_ghost_regions() u_mid = u_old.copy(out=self._u_mid) u_mid += un u_mid /= 2.0 u_mid.update_ghost_regions() # save H^{n+1, k} markers[~holes, first_free_idx : first_free_idx + 3] = markers[~holes, 0:3] # calculate denominator ||z^{n+1, k} - z^n||^2 sum_u_diff_loc = xp.sum((u_diff.toarray() ** 2)) sum_H_diff_loc = xp.sum( (markers[~holes, :3] - markers[~holes, first_pusher_idx : first_pusher_idx + 3]) ** 2, ) buffer_array = xp.array([sum_u_diff_loc]) if particles.mpi_comm is not None: particles.mpi_comm.Allreduce( MPI.IN_PLACE, buffer_array, op=MPI.SUM, ) denominator = buffer_array[0] buffer_array = xp.array([sum_H_diff_loc]) if particles.mpi_comm is not None: particles.mpi_comm.Allreduce( MPI.IN_PLACE, buffer_array, op=MPI.SUM, ) if particles.clone_config is not None: particles.clone_config.inter_comm.Allreduce( MPI.IN_PLACE, buffer_array, op=MPI.SUM, ) denominator += buffer_array[0] # sorting markers at mid-point if particles.mpi_comm is not None: particles.mpi_sort_markers(apply_bc=False, alpha=0.5) self._accum_kernel_en_fB_mid( args_markers, *self._args_accum_kernel_en_fB_mid, first_free_idx + 3, ) en_fB_mid = xp.sum(markers[~holes, first_free_idx + 3].dot(markers[~holes, 5])) * self.options.ep_scale en_fB_mid /= n_mks_tot buffer_array = xp.array([en_fB_mid]) if particles.mpi_comm is not None: particles.mpi_comm.Allreduce( MPI.IN_PLACE, buffer_array, op=MPI.SUM, ) if particles.clone_config is not None: particles.clone_config.inter_comm.Allreduce( MPI.IN_PLACE, buffer_array, op=MPI.SUM, ) en_fB_mid = buffer_array[0] if denominator == 0.0: const = 0.0 else: const = (en_fB_new - en_fB_old - en_fB_mid) / denominator # update u^{n+1, k} self._ACC(*self._args_accum_kernel, const) ku = self._A_inv.dot(self._ACC.vectors[0], out=self._ku) u_new = un.copy(out=self._u_new) u_new += ku * dt u_new *= alpha u_new += u_old * (1.0 - alpha) u_new.update_ghost_regions() # update en_U_new self._A.dot(u_new, out=self._M2n_dot_u) en_U_new = u_new.inner(self._M2n_dot_u) / 2.0 # update H^{n+1, k} self._pusher_kernel( dt, args_markers, *self._args_pusher_kernel, const, alpha, ) sum_H_diff_loc = xp.sum( xp.abs(markers[~holes, 0:3] - markers[~holes, first_free_idx : first_free_idx + 3]), ) if particles.mpi_comm is not None: particles.mpi_sort_markers(apply_bc=False) # update en_fB_new particles.save_magnetic_energy(PB_b) en_fB_new = xp.sum(markers[~holes, 8].dot(markers[~holes, 5])) * self.options.ep_scale en_fB_new /= n_mks_tot buffer_array = xp.array([en_fB_new]) if particles.mpi_comm is not None: particles.mpi_comm.Allreduce( MPI.IN_PLACE, buffer_array, op=MPI.SUM, ) if particles.clone_config is not None: particles.clone_config.inter_comm.Allreduce( MPI.IN_PLACE, buffer_array, op=MPI.SUM, ) en_fB_new = buffer_array[0] # calculate total energy difference e_diff = xp.abs(en_U_new + en_fB_new - en_tot_old) # calculate ||z^{n+1, k} - z^{n+1, k-1|| sum_u_diff_loc = xp.sum(xp.abs(u_new.toarray() - u_old.toarray())) buffer_array = xp.array([sum_u_diff_loc]) if particles.mpi_comm is not None: particles.mpi_comm.Allreduce( MPI.IN_PLACE, buffer_array, op=MPI.SUM, ) diff = buffer_array[0] buffer_array = xp.array([sum_H_diff_loc]) if particles.mpi_comm is not None: particles.mpi_comm.Allreduce( MPI.IN_PLACE, buffer_array, op=MPI.SUM, ) if particles.clone_config is not None: particles.clone_config.inter_comm.Allreduce( MPI.IN_PLACE, buffer_array, op=MPI.SUM, ) diff += buffer_array[0] # check convergence if diff < self.options.dg_solver_params.tol: if self.options.dg_solver_params.verbose and MPI.COMM_WORLD.Get_rank() == 0: logger.info(f"converged diff: {diff}") logger.info(f"converged e_diff: {e_diff}") if particles.mpi_comm is not None: particles.mpi_comm.Barrier() break else: if self.options.dg_solver_params.verbose and MPI.COMM_WORLD.Get_rank() == 0: logger.info(f"not converged diff: {diff}") logger.info(f"not converged e_diff: {e_diff}") if iter_num == self.options.dg_solver_params.maxiter: if self.options.dg_solver_params.info and MPI.COMM_WORLD.Get_rank() == 0: logger.info( f"{iter_num =}, maxiter={self.options.dg_solver_params.maxiter} reached! diff: {diff}, e_diff: {e_diff}", ) if particles.mpi_comm is not None: particles.mpi_comm.Barrier() break # sorting markers if particles.mpi_comm is not None: particles.mpi_sort_markers() else: particles.apply_kinetic_bc() # update u coefficients diffs = self.update_feec_variables(u=u_new) # clear the buffer markers[:, first_pusher_idx:-2] = 0.0 # update_weights if self.variables.energetic_ions.species.weights_params.control_variate: particles.update_weights() if self.options.dg_solver_params.info and MPI.COMM_WORLD.Get_rank() == 0: logger.info(f"Maxdiff up for CurrentCoupling5DGradB: {diffs['u']}") logger.info("")