Source code for struphy.propagators.base

"Propagator base class."

import logging
from abc import ABCMeta, abstractmethod
from dataclasses import dataclass
from typing import Literal

import cunumpy as xp
from feectools.linalg.block import BlockVector
from feectools.linalg.stencil import StencilVector

from struphy.feec.basis_projection_ops import BasisProjectionOperators
from struphy.feec.mass import WeightedMassOperators
from struphy.feec.psydac_derham import Derham
from struphy.fields_background.projected_equils import ProjectedFluidEquilibriumWithB
from struphy.geometry.base import Domain
from struphy.io.options import OptionsBase
from struphy.models.variables import FEECVariable, PICVariable, SPHVariable, Variable
from struphy.utils.utils import check_option

logger = logging.getLogger("struphy")


[docs] class Propagator(metaclass=ABCMeta): """Base class for propagators used in StruphyModels. Note ---- All Struphy propagators are subclasses of ``Propagator`` and must be added under ``struphy/propagators/``. """
[docs] @abstractmethod class Variables: """Define variable names and types to be updated by the propagator.""" def __init__(self): self._var1 = None @property def var1(self): return self._var1 @var1.setter def var1(self, new): assert isinstance(new, PICVariable) assert new.space == "Particles6D" self._var1 = new
@abstractmethod def __init__(self): self.variables = self.Variables()
[docs] @abstractmethod @dataclass(repr=False) class Options(OptionsBase): """Template for configuration options of a propagator. Subclasses should override this to define specific propagator options. """ # specific literals OptsTemplate = Literal["implicit", "explicit"] # propagator options opt1: str = ("implicit",) def __post_init__(self): # checks check_option(self.opt1, self.OptsTemplate)
@property @abstractmethod def options(self) -> Options: if not hasattr(self, "_options"): self._options = self.Options() return self._options @options.setter @abstractmethod 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}")
[docs] @abstractmethod def allocate(self): """Allocate all data/objects of the instance."""
@abstractmethod def __call__(self, dt: float): """Update variables from t -> t + dt. Use ``Propagators.feec_vars_update`` to write to FEEC variables to ``Propagator.feec_vars``. Parameters ---------- dt : float Time step size. """
[docs] def show_options(self): """Print the options of the propagator.""" logger.info(f"\nOptions for propagator '{self.__class__.__name__}':") for k, v in self.options.__dict__.items(): logger.info(f" {k + ':':<20}{v}")
[docs] def update_feec_variables(self, **new_coeffs): r"""Return max_diff = max(abs(new - old)) for each new_coeffs, update feec coefficients and update ghost regions. Returns ------- diffs : dict max_diff for all feec variables. """ diffs = {} for var, new in new_coeffs.items(): assert "_" + var in self.variables.__dict__, f"{var} not in {self.variables.__dict__}." assert isinstance(new, (StencilVector, BlockVector)) old_var = getattr(self.variables, var) assert isinstance(old_var, FEECVariable) old = old_var.spline.vector assert new.space == old.space # calculate maximum of difference abs(new - old) diffs[var] = xp.max(xp.abs(new.toarray() - old.toarray())) # copy new coeffs into old new.copy(out=old) # important: sync processes! old.update_ghost_regions() return diffs
@property def init_kernels(self): r"""List of initialization kernels for evaluation at :math:`\boldsymbol \eta^n` in an iterative :class:`~struphy.pic.pushing.pusher.Pusher`. """ return self._init_kernels @property def eval_kernels(self): r"""List of evaluation kernels for evaluation at :math:`\alpha_i \eta_{i}^{n+1,k} + (1 - \alpha_i) \eta_{i}^n` for :math:`i=1, 2, 3` and different :math:`\alpha_i \in [0,1]`, in an iterative :class:`~struphy.pic.pushing.pusher.Pusher`. """ return self._eval_kernels @property def rank(self): """MPI rank, is 0 if no communicator.""" return self._rank @property def derham(self) -> Derham: """Derham spaces and projectors.""" assert hasattr( self, "_derham", ), "Derham not set. Please do obj.derham = ..." assert isinstance(self._derham, Derham) return self._derham @derham.setter def derham(self, derham: Derham): assert isinstance(derham, Derham) self._derham = derham @property def domain(self) -> Domain: """Domain object that characterizes the mapping from the logical to the physical domain.""" assert hasattr(self, "_domain"), "Domain for analytical MHD equilibrium not set. Please do obj.domain = ..." assert isinstance(self._domain, Domain) return self._domain @domain.setter def domain(self, domain: Domain): assert isinstance(domain, Domain) self._domain = domain @property def mass_ops(self) -> WeightedMassOperators: """Weighted mass operators.""" assert hasattr(self, "_mass_ops"), "Weighted mass operators not set. Please do obj.mass_ops = ..." assert isinstance(self._mass_ops, WeightedMassOperators) return self._mass_ops @mass_ops.setter def mass_ops(self, mass_ops: WeightedMassOperators): assert isinstance(mass_ops, WeightedMassOperators) self._mass_ops = mass_ops @property def basis_ops(self) -> BasisProjectionOperators: """Basis projection operators.""" assert hasattr(self, "_basis_ops"), "Basis projection operators not set. Please do obj.basis_ops = ..." assert isinstance(self._basis_ops, BasisProjectionOperators) return self._basis_ops @basis_ops.setter def basis_ops(self, basis_ops: BasisProjectionOperators): assert isinstance(basis_ops, BasisProjectionOperators) self._basis_ops = basis_ops @property def projected_equil(self) -> ProjectedFluidEquilibriumWithB: """Fluid equilibrium projected on 3d Derham sequence with commuting projectors.""" assert hasattr( self, "_projected_equil", ), "Projected MHD equilibrium not set." assert isinstance(self._projected_equil, ProjectedFluidEquilibriumWithB) return self._projected_equil @projected_equil.setter def projected_equil(self, new: ProjectedFluidEquilibriumWithB): assert isinstance(new, ProjectedFluidEquilibriumWithB) self._projected_equil = new @property def time_state(self): """A pointer to the time variable of the dynamics ('t').""" return self._time_state
[docs] def add_time_state(self, time_state): """Add a pointer to the time variable of the dynamics ('t'). Parameters ---------- time_state : ndarray Of size 1, holds the current physical time 't'. """ assert time_state.size == 1 self._time_state = time_state
[docs] def add_init_kernel( self, kernel, column_nr: int, comps: tuple | int, args_init: tuple, ): """Add an initialization kernel to self.init_kernels. Parameters ---------- kernel : pyccel func The kernel function. column_nr : int The column index at which the result is stored in marker array. comps : tuple | int None or (0) for scalar-valued function evaluation. In vector valued case, allows to specify which components to save at column_nr:column_nr + len(comps). args_init : tuple The arguments for the kernel function. """ if comps is None: comps = xp.array([0]) # case for scalar evaluation else: comps = xp.array(comps, dtype=int) if not hasattr(self, "_init_kernels"): self._init_kernels = [] self._init_kernels += [ ( kernel, column_nr, comps, args_init, ), ]
[docs] def add_eval_kernel( self, kernel, column_nr: int, comps: tuple | int, args_eval: tuple, alpha: float | int | tuple | list = 1.0, ): """Add an evaluation kernel to self.eval_kernels. Parameters ---------- kernel : pyccel func The kernel function. column_nr : int The column index at which the result is stored in marker array. comps : tuple | int None for scalar-valued function evaluation. In vecotr valued case, allows to specify which components to save at column_nr:column_nr + len(comps). args_init : tuple The arguments for the kernel function. alpha : float | int | tuple | list Evaluations in kernel are at the weighted average alpha[i]*markers[:, i] + (1 - alpha[i])*markers[:, buffer_idx + i], for i=0,1,2. If float or int or then alpha = [alpha]*dim, where dim is the dimension of the phase space (<=6). alpha[i] must be between 0 and 1. """ if isinstance(alpha, int) or isinstance(alpha, float): alpha = [alpha] * 6 alpha = xp.array(alpha) if comps is None: comps = xp.array([0]) # case for scalar evaluation else: comps = xp.array(comps, dtype=int) if not hasattr(self, "_eval_kernels"): self._eval_kernels = [] self._eval_kernels += [ ( kernel, alpha, column_nr, comps, args_eval, ), ]