"Base classes for particle deposition (accumulation) on the grid."
from dataclasses import dataclass
import cunumpy as xp
from cunumpy.kernels import Kernel, PyccelKernel
from feectools.linalg.block import BlockVector
from feectools.linalg.stencil import StencilMatrix, StencilVector
from maybempi import MPI
from scope_profiler import ProfileManager
from struphy.feec.mass import WeightedMassOperators
from struphy.feec.psydac_derham import Derham
from struphy.io.options import LiteralOptions
from struphy.kernel_arguments.pusher_args_kernels import DerhamArguments, DomainArguments
from struphy.models.variables import PICVariable, SPHVariable
from struphy.pic.accumulation.filter import AccumFilter, FilterParameters
from struphy.pic.base import Particles
from struphy.utils.cuda_arguments import check_mapping_on_device
from struphy.utils.utils import __dataclass_repr_no_defaults__, check_option
def _accumulation_kernel(kernel) -> Kernel:
"""The kernel as a :class:`cunumpy.kernels.Kernel` (pyccel or CUDA depending on the backend)."""
if not isinstance(kernel, Kernel):
kernel = Kernel(kernel)
return kernel
[docs]
class Accumulator:
r"""
Approximates integrals of the form
.. math::
I_A &= \int_\Omega \int_{\mathbb R^3} \Lambda^\mu_{ijk}(\boldsymbol \eta) \, A^{\mu, \nu}(\boldsymbol \eta, \mathbf v) \, \Lambda^\nu_{mno}(\boldsymbol \eta) \, f^{\textrm{vol}}(\boldsymbol \eta, \mathbf v)\,\mathrm d\mathbf v \textrm d \boldsymbol \eta\,,
\\[2mm]
I_B &= \int_\Omega \int_{\mathbb R^3} \Lambda^\mu_{ijk}(\boldsymbol \eta) \, B^\mu(\boldsymbol \eta, \mathbf v) \, f^{\textrm{vol}}(\boldsymbol \eta, \mathbf v)\,\mathrm d\mathbf v \textrm d \boldsymbol \eta\,,
for given weight functions :math:`A^{\mu,\nu}` and :math:`B^\mu` by Monte-Carlo quadrature through the particle distribution function :math:`f^{\textrm{vol}}`:
.. math::
f^{\textrm{vol}}(\boldsymbol \eta, \mathbf v) \approx \sum_{p=0}^{N-1} w_p \, \delta(\boldsymbol \eta - \boldsymbol \eta_p) \, \delta(\mathbf v - \mathbf v_p)\,.
This results in stencil (block) matrices and vectors
.. math::
M &= (M^{\mu,\nu})_{\mu,\nu}\,,\qquad && M^{\mu,\nu} \in \mathbb R^{\mathbb N^\alpha_\mu \times \mathbb N^\alpha_\nu}\,,
\\[2mm]
V &= (V^\mu)_\mu\,,\qquad &&V^\mu \in \mathbb R^{\mathbb N^\alpha_\mu}\,,
where :math:`N^\alpha_\mu` denotes the dimension of the :math:`\mu`-th component
of the :class:`~struphy.feec.psydac_derham.Derham` space
:math:`V_h^\alpha` (:math:`\mu,\nu = 1,2,3` for vector-valued spaces),
with entries obtained by summing over all particles :math:`p`,
.. math::
M^{\mu,\nu}_{ijk,mno} &= \sum_{p=0}^{N-1} w_p\, \Lambda^\mu_{ijk}(\boldsymbol \eta_p) \, A^{\mu,\nu}_p \, \Lambda^\nu_{mno}(\boldsymbol \eta_p) \,,
\\[2mm]
V^\mu_{ijk} &= \sum_{p=0}^{N-1} w_p\, \Lambda^\mu_{ijk}(\boldsymbol \eta_p) \, B^\mu_p \,.
Here, :math:`\Lambda^\mu_{ijk}(\boldsymbol \eta_p)` denotes the :math:`ijk`-th basis function
of the :math:`\mu`-th component of a Derham space.
Parameters
----------
particles : Particles
Particles object holding the markers to accumulate.
space_id : str
Space identifier for the matrix/vector (H1, Hcurl, Hdiv, L2 or H1vec) to be accumulated into.
kernel : Kernel | PyccelKernel
The accumulation kernel, e.g. ``charge_density_0form`` from ``struphy.pic.accumulation.kernels.charge_density_0form``.
On the CuPy backend, a kernel without CUDA version raises NotImplementedError.
derham : Derham
Discrete FE spaces object.
args_domain : DomainArguments
Mapping infos.
add_vector : bool
True if, additionally to a matrix, a vector in the same space is to be accumulated. Default=False.
symmetry : str
In case of space_id=Hcurl/Hdiv, the symmetry property of the block matrix: diag, asym, symm, pressure or None (=full matrix, default)
filter_params : dict
Params for the accumulation filter: use_filter(string, either `three_point or `fourier), repeat(int), alpha(float) and modes(list with int).
Note
----
Struphy accumulation kernels live in ``struphy/pic/accumulation/kernels/``, one folder per kernel
(see :mod:`struphy.pic.accumulation.kernels`).
"""
[docs]
def __init__(
self,
particles: Particles,
space_id: str,
kernel: Kernel | PyccelKernel,
mass_ops: WeightedMassOperators,
args_domain: DomainArguments,
*,
add_vector: bool = False,
symmetry: str = None,
filter_params: FilterParameters = None,
):
self._particles = particles
self._space_id = space_id
self._kernel = _accumulation_kernel(kernel)
self._derham = mass_ops.derham
self._args_domain = args_domain
# profiling region names (precomputed, they are looked up on every call)
self._region_name = "accum: " + kernel.name
self._comm_region_name = "accum comm: " + kernel.name
self._symmetry = symmetry
self._form = self.derham.space_to_form[space_id]
# initialize matrices (instances of WeightedMassOperator)
self._operators = []
# special treatment in model LinearMHDVlasovPC (symmetry=pressure, six symmetric BlockMatrices are needed)
if symmetry == "pressure":
for _ in range(6):
operator = mass_ops.create_weighted_mass(
space_id,
space_id,
weights="symm",
)
self._operators.append(operator)
# "normal" treatment (just one matrix)
else:
operator = mass_ops.create_weighted_mass(
space_id,
space_id,
weights=symmetry,
)
self._operators.append(operator)
# collect all _data attributes needed in accumulation kernel
self._args_data = ()
for op in self._operators:
if isinstance(op.matrix, StencilMatrix):
self._args_data += (op.matrix._data,)
else:
for a, row in enumerate(op.matrix.blocks):
for b, bl in enumerate(row):
if symmetry in ["pressure", "symm", "asym", "diag"]:
if b >= a and bl is not None:
self._args_data += (bl._data,)
else:
if bl is not None:
self._args_data += (bl._data,)
# initialize vectors
self._vectors = []
self._vectors_temp = []
self._vectors_out = []
if add_vector:
# special treatment in model LinearMHDVlasovPC (symmetry=pressure, three BlockVectors are needed)
if symmetry == "pressure":
for _ in range(3):
self._vectors += [BlockVector(self.derham.coeff_spaces[self.form])]
self._vectors_temp += [
BlockVector(self.derham.coeff_spaces[self.form]),
]
self._vectors_out += [
BlockVector(self.derham.coeff_spaces[self.form]),
]
# normal treatment (just one vector)
else:
for op in self._operators:
if isinstance(op.matrix, StencilMatrix):
self._vectors += [StencilVector(op.matrix.domain)]
self._vectors_temp += [StencilVector(op.matrix.domain)]
self._vectors_out += [StencilVector(op.matrix.domain)]
else:
self._vectors += [BlockVector(op.matrix.domain)]
self._vectors_temp += [BlockVector(op.matrix.domain)]
self._vectors_out += [BlockVector(op.matrix.domain)]
for vec in self._vectors:
if isinstance(vec, StencilVector):
self._args_data += (vec._data,)
else:
for bl in vec.blocks:
self._args_data += (bl._data,)
# initialize filter
self._accfilter = AccumFilter(filter_params, self._derham, self._space_id)
[docs]
def __call__(self, *optional_args):
"""
Performs the accumulation into the matrix/vector by calling the chosen accumulation kernel.
Parameters
----------
particles : Particles
Particles object holding the markers information in format particles.markers.shape == (n_markers, :).
optional_args : any
Additional arguments to be passed to the accumulator kernel, besides the mandatory arguments
which are prepared automatically (spline bases info, mapping info, data arrays).
Examples would be parameters for a background kinetic distribution or spline coefficients of a background magnetic field.
Entries must be pyccel-conform types.
"""
with ProfileManager.profile_region(self._region_name):
self._accumulate(*optional_args)
def _accumulate(self, *optional_args):
"""Body of :meth:`__call__`, see there."""
# flags for break
vec_finished = False
# reset data
for dat in self._args_data:
dat[:] = 0.0
# accumulate into matrix (and vector) with markers
with ProfileManager.profile_region("kernel: " + self.kernel.name):
self.kernel(
self.particles.args_markers,
self.derham.args_derham,
self.args_domain,
*self._args_data,
*optional_args,
)
# apply filter
if self.accfilter.params.use_filter is not None:
for vec in self._vectors:
with ProfileManager.profile_region(self._comm_region_name):
vec.exchange_assembly_data()
vec.update_ghost_regions()
self.accfilter(vec)
vec_finished = True
if self.particles.clone_config is None:
num_clones = 1
else:
num_clones = self.particles.clone_config.num_clones
if num_clones > 1:
with ProfileManager.profile_region(self._comm_region_name):
for data_array in self._args_data:
self.particles.clone_config.inter_comm.Allreduce(
MPI.IN_PLACE,
data_array,
op=MPI.SUM,
)
# finish vector: accumulate ghost regions and update ghost regions
if not vec_finished:
with ProfileManager.profile_region(self._comm_region_name):
for vec in self._vectors:
vec.exchange_assembly_data()
vec.update_ghost_regions()
# finish matrix: accumulate ghost regions, update ghost regions and copy data for symmetric/antisymmetric block matrices
with ProfileManager.profile_region(self._comm_region_name):
for op in self._operators:
op.matrix.exchange_assembly_data()
op.matrix.update_ghost_regions()
if self.symmetry == "symm":
self._operators[0].matrix[0, 1].transpose(
out=self._operators[0].matrix[1, 0],
)
self._operators[0].matrix[0, 2].transpose(
out=self._operators[0].matrix[2, 0],
)
self._operators[0].matrix[1, 2].transpose(
out=self._operators[0].matrix[2, 1],
)
elif self.symmetry == "asym":
self._operators[0].matrix[0, 1].transpose(
out=self._operators[0].matrix[1, 0],
)
self._operators[0].matrix[1, 0] *= -1
self._operators[0].matrix[0, 2].transpose(
out=self._operators[0].matrix[2, 0],
)
self._operators[0].matrix[2, 0] *= -1
self._operators[0].matrix[1, 2].transpose(
out=self._operators[0].matrix[2, 1],
)
self._operators[0].matrix[2, 1] *= -1
elif self.symmetry == "pressure":
for i in range(6):
self._operators[i].matrix[0, 1].transpose(
out=self._operators[i].matrix[1, 0],
)
self._operators[i].matrix[0, 2].transpose(
out=self._operators[i].matrix[2, 0],
)
self._operators[i].matrix[1, 2].transpose(
out=self._operators[i].matrix[2, 1],
)
@property
def particles(self):
"""Particle object."""
return self._particles
@property
def kernel(self) -> Kernel:
"""The accumulation kernel."""
return self._kernel
[docs]
def kernels(self) -> tuple[Kernel, ...]:
"""The kernels this accumulator calls (the accumulation kernel)."""
return (self._kernel,)
@property
def derham(self):
"""Discrete Derham complex on the logical unit cube."""
return self._derham
@property
def args_domain(self):
"""Mapping info for evaluating metric coefficients."""
return self._args_domain
@property
def space_id(self):
"""Space identifier for the matrix/vector (H1, Hcurl, Hdiv, L2 or H1vec) to be accumulated into."""
return self._space_id
@property
def form(self):
"""p-form ("0", "1", "2", "3" or "v") to be accumulated into."""
return self._form
@property
def symmetry(self):
"""Symmetry of the accumulation matrix (diagonal, symmetric, asymmetric, etc.)."""
return self._symmetry
@property
def operators(self):
"""List of WeightedMassOperators of the accumulator. Matrices can be accessed e.g. with operators[0].matrix."""
return self._operators
@property
def vectors(self):
"""List of Stencil-/Block-/PolarVectors of the accumulator."""
out = []
for vec, vec_temp, vec_out in zip(self._vectors, self._vectors_temp, self._vectors_out):
self._derham.extraction_ops[self.form].dot(vec, out=vec_temp)
self._derham.boundary_ops[self.form].dot(vec_temp, out=vec_out)
out += [vec_out]
return out
@property
def accfilter(self):
"""Callable filters"""
return self._accfilter
[docs]
def show_accumulated_spline_field(self, mass_ops: WeightedMassOperators, eta_direction=0, component=0):
r"""1D plot of the spline field corresponding to the accumulated vector.
The latter can be viewed as the rhs of an L2-projection:
.. math::
\mathbb M \mathbf a = \sum_p \boldsymbol \Lambda(\boldsymbol \eta_p) * B_p\,.
The FE coefficients :math:`\mathbf a` determine a FE :class:`~struphy.feec.psydac_derham.SplineFunction`.
"""
from matplotlib import pyplot as plt
from struphy.feec.mass import L2Projector
# L2 projection
proj = L2Projector(self.space_id, mass_ops)
a = proj.solve(self.vectors[0])
# create field and assign coeffs
field = self.derham.create_spline_function("accum_field", self.space_id)
field.vector = a
# plot field
eta = xp.linspace(0, 1, 100)
if eta_direction == 0:
args = (eta, 0.5, 0.5)
elif eta_direction == 1:
args = (0.5, eta, 0.5)
else:
args = (0.5, 0.5, eta)
vals = mass_ops.domain.push(field, *args, kind="1", squeeze_out=True)
plt.plot(eta, vals[component])
plt.title(
f'Spline field accumulated with the kernel "{self.kernel}"',
)
plt.xlabel(rf"$\eta_{eta_direction + 1}$")
plt.ylabel("field amplitude")
plt.show()
[docs]
class AccumulatorVector:
r"""
Approximates integrals of the form
.. math::
I_B = \int_\Omega \int_{\mathbb R^3} \Lambda^\mu_{ijk}(\boldsymbol \eta) \, B^\mu(\boldsymbol \eta, \mathbf v) \, f^{\textrm{vol}}(\boldsymbol \eta, \mathbf v)\,\mathrm d\mathbf v \textrm d \boldsymbol \eta\,,
for a given weight function and :math:`B^\mu` by Monte-Carlo quadrature through the particle distribution function :math:`f^{\textrm{vol}}`:
.. math::
f^{\textrm{vol}}(\boldsymbol \eta, \mathbf v) \approx \sum_{p=0}^{N-1} w_p \, \delta(\boldsymbol \eta - \boldsymbol \eta_p) \, \delta(\mathbf v - \mathbf v_p)\,.
This results in a stencil (block) vector
.. math::
V = (V^\mu)_\mu\,,\qquad V^\mu \in \mathbb R^{\mathbb N^\alpha_\mu}\,,
where :math:`N^\alpha_\mu` denotes the dimension of the :math:`\mu`-th component
of the :class:`~struphy.feec.psydac_derham.Derham` space
:math:`V_h^\alpha` (:math:`\mu,\nu = 1,2,3` for vector-valued spaces),
with entries obtained by summing over all particles :math:`p`,
.. math::
V^\mu_{ijk} = \sum_{p=0}^{N-1} w_p\, \Lambda^\mu_{ijk}(\boldsymbol \eta_p) \, B^\mu_p \,.
Here, :math:`\Lambda^\mu_{ijk}(\boldsymbol \eta_p)` denotes the :math:`ijk`-th basis function
of the :math:`\mu`-th component of a Derham space.
Similar to :class:`~struphy.pic.accumulation.particles_to_grid.Accumulator` but only for vectors :math:`V`.
Parameters
----------
particles : Particles
Particles object holding the markers to accumulate.
space_id : str
Space identifier for the matrix/vector (H1, Hcurl, Hdiv, L2 or H1vec) to be accumulated into.
kernel : Kernel | PyccelKernel
The accumulation kernel, e.g. ``charge_density_0form`` from ``struphy.pic.accumulation.kernels.charge_density_0form``.
On the CuPy backend, a kernel without CUDA version raises NotImplementedError.
derham : Derham
Discrete FE spaces object.
args_domain : DomainArguments
Mapping infos.
"""
[docs]
def __init__(
self,
particles: Particles,
space_id: str,
kernel: Kernel | PyccelKernel,
mass_ops: WeightedMassOperators,
args_domain: DomainArguments,
filter_params: FilterParameters = None,
):
self._particles = particles
self._space_id = space_id
self._kernel = _accumulation_kernel(kernel)
self._derham = mass_ops.derham
self._args_domain = args_domain
# profiling region names (precomputed, they are looked up on every call)
self._region_name = "accum: " + kernel.name
self._comm_region_name = "accum comm: " + kernel.name
self._form = self.derham.space_to_form[space_id]
# initialize vectors
self._vectors = []
self._vectors_temp = []
self._vectors_out = []
# collect all _data attributes needed in accumulation kernel
self._args_data = ()
if space_id in ("H1", "L2"):
self._vectors += [
StencilVector(self.derham.fem_spaces[self.form].coeff_space),
]
self._vectors_temp += [
StencilVector(self.derham.fem_spaces[self.form].coeff_space),
]
self._vectors_out += [
StencilVector(self.derham.fem_spaces[self.form].coeff_space),
]
elif space_id in ("Hcurl", "Hdiv", "H1vec"):
self._vectors += [
BlockVector(
self.derham.fem_spaces[self.form].coeff_space,
),
]
self._vectors_temp += [
BlockVector(
self.derham.fem_spaces[self.form].coeff_space,
),
]
self._vectors_out += [
BlockVector(
self.derham.fem_spaces[self.form].coeff_space,
),
]
for vec in self._vectors:
if isinstance(vec, StencilVector):
self._args_data += (vec._data,)
else:
for bl in vec.blocks:
self._args_data += (bl._data,)
# initialize filter
self._accfilter = AccumFilter(filter_params, self._derham, self._space_id)
[docs]
def __call__(self, *optional_args):
"""
Performs the accumulation into the vector by calling the chosen accumulation kernel.
Parameters
----------
optional_args : any
Additional arguments to be passed to the accumulator kernel, besides the mandatory arguments
which are prepared automatically (spline bases info, mapping info, data arrays).
Examples would be parameters for a background kinetic distribution or spline coefficients of a background magnetic field.
Entries must be pyccel-conform types.
"""
with ProfileManager.profile_region(self._region_name):
self._accumulate(*optional_args)
def _accumulate(self, *optional_args):
"""Body of :meth:`__call__`, see there."""
# flags for break
vec_finished = False
# reset data
for dat in self._args_data:
dat[:] = 0.0
# accumulate into matrix (and vector) with markers
with ProfileManager.profile_region("kernel: " + self.kernel.name):
self.kernel(
self.particles.args_markers,
self.derham.args_derham,
self.args_domain,
*self._args_data,
*optional_args,
)
# apply filter
if self.accfilter.params.use_filter is not None:
for vec in self._vectors:
with ProfileManager.profile_region(self._comm_region_name):
vec.exchange_assembly_data()
vec.update_ghost_regions()
self.accfilter(vec)
vec_finished = True
if self.particles.clone_config is None:
num_clones = 1
else:
num_clones = self.particles.clone_config.num_clones
if num_clones > 1:
with ProfileManager.profile_region(self._comm_region_name):
for data_array in self._args_data:
self.particles.clone_config.inter_comm.Allreduce(
MPI.IN_PLACE,
data_array,
op=MPI.SUM,
)
# finish vector: accumulate ghost regions and update ghost regions
if not vec_finished:
with ProfileManager.profile_region(self._comm_region_name):
for vec in self._vectors:
vec.exchange_assembly_data()
vec.update_ghost_regions()
@property
def particles(self):
"""Particle object."""
return self._particles
@property
def kernel(self) -> Kernel:
"""The accumulation kernel."""
return self._kernel
[docs]
def kernels(self) -> tuple[Kernel, ...]:
"""The kernels this accumulator calls (the accumulation kernel)."""
return (self._kernel,)
@property
def derham(self):
"""Discrete Derham complex on the logical unit cube."""
return self._derham
@property
def args_domain(self):
"""Mapping arguments."""
return self._args_domain
@property
def space_id(self):
"""Space identifier for the matrix/vector (H1, Hcurl, Hdiv, L2 or H1vec) to be accumulated into."""
return self._space_id
@property
def form(self):
"""p-form ("0", "1", "2", "3" or "v") to be accumulated into."""
return self._form
@property
def vectors(self):
"""List of Stencil-/Block-/PolarVectors of the accumulator."""
out = []
for vec, vec_temp, vec_out in zip(self._vectors, self._vectors_temp, self._vectors_out):
self._derham.extraction_ops[self.form].dot(vec, out=vec_temp)
self._derham.boundary_ops[self.form].dot(vec_temp, out=vec_out)
out += [vec_out]
return out
@property
def accfilter(self):
"""Callable filters"""
return self._accfilter
[docs]
def show_accumulated_spline_field(self, mass_ops, eta_direction=(True, False, False), save_L2=False):
r"""1 or 2D plot of the spline field corresponding to the accumulated vector.
The latter can be viewed as the rhs of an L2-projection:
.. math::
\mathbb M \mathbf a = \sum_p \boldsymbol \Lambda(\boldsymbol \eta_p) * B_p\,.
The FE coefficients :math:`\mathbf a` determine a FE :class:`~struphy.feec.psydac_derham.SplineFunction`.
:param eta_direction: axes of eta to show accumulation (eta1, eta2, eta3).
"""
assert sum(eta_direction) < 3, "Current implementation is only possible with 1 and 2D visualization"
from matplotlib import pyplot as plt
from struphy.feec.mass import L2Projector
# L2 projection
proj = L2Projector(self.space_id, mass_ops)
a = proj.solve(self.vectors[0])
if save_L2:
return a
# create field and assign coeffs
field = self.derham.create_spline_function("accum_field", self.space_id)
field.vector = a
# plot field
# initialize axis and slicing
eta = xp.linspace(0, 1, 100)
args = [0.5, 0.5, 0.5]
# fill slices to plot with eta
plt_axis = xp.flatnonzero(eta_direction)
for idx in plt_axis:
args[idx] = eta
args = tuple(args)
# field value at specified axes
field_value = field(*args, squeeze_out=True)
# One-dimensional case
if len(plt_axis) == 1:
plt.plot(eta, field_value)
plt.xlabel(rf"$\eta_{plt_axis[0] + 1}$")
plt.ylabel("field amplitude")
# Two-dimensional case
elif len(plt_axis) == 2:
Eta1, Eta2 = xp.meshgrid(eta, eta, indexing="ij")
pcm = plt.pcolor(Eta1, Eta2, field_value)
plt.colorbar(pcm, label="field amplitude")
plt.xlabel(rf"$\eta_{plt_axis[0] + 1}$")
plt.ylabel(rf"$\eta_{plt_axis[1] + 1}$")
plt.title(
f'Spline field accumulated with the kernel "{self.kernel}"',
)
plt.show()
[docs]
@dataclass
class ParticlesToGrid:
r"""Lightweight, serializable description of a particle-to-grid coupling
(for example charge- or current deposition) into FEEC degrees of freedom.
A ``ParticlesToGrid`` does not perform any accumulation itself: it simply bundles
the pieces needed to build an :class:`~struphy.pic.accumulation.particles_to_grid.AccumulatorVector`.
Parameters
----------
pic_variable : PICVariable | SPHVariable
The kinetic variable whose markers (``pic_variable.particles``) are deposited on the grid.
accum_space : {"H1", "Hcurl", "Hdiv", "L2", "H1vec"}
FEEC space identifier of the vector to accumulate into.
accum_kernel : Kernel | PyccelKernel
Accumulation kernel matching ``accum_space``, for example
``charge_density_0form`` from ``struphy.pic.accumulation.kernels.charge_density_0form``.
Examples
--------
>>> from struphy.pic.accumulation.kernels.charge_density_0form import charge_density_0form
>>> from struphy.pic.accumulation.particles_to_grid import ParticlesToGrid
>>> from struphy.propagators.poisson_solve import PoissonSolve
>>> rho = ParticlesToGrid(
... kinetic_ions.var,
... "H1",
... charge_density_0form,
... )
>>> poisson = PoissonSolve(rho=rho, rho_coeffs=alpha**2 / epsilon)
"""
pic_variable: PICVariable | SPHVariable = None
accum_space: LiteralOptions.OptsFEECSpace = None
accum_kernel: Kernel | PyccelKernel = None
def __post_init__(self):
if self.accum_space is not None:
check_option(self.accum_space, LiteralOptions.OptsFEECSpace)
assert isinstance(self.accum_kernel, (Kernel, PyccelKernel)) or self.accum_kernel is None
def __repr_no_defaults__(self):
return __dataclass_repr_no_defaults__(self)