diff --git a/docs/source/usage/picmi/custom_template.rst b/docs/source/usage/picmi/custom_template.rst index 67f524d156..7bd338fe6d 100644 --- a/docs/source/usage/picmi/custom_template.rst +++ b/docs/source/usage/picmi/custom_template.rst @@ -74,7 +74,7 @@ The output of the picongpu simulation in the default templates is configured in $USED_CHARGE_CONSERVATION_FLAGS - {{#species_initmanager.species}} + {{#species}} --{{{name}}}_macroParticlesCount.period {{{period}}} --{{{name}}}_energy.period {{{period}}} @@ -92,7 +92,7 @@ The output of the picongpu simulation in the default templates is configured in --{{{name}}}_png.slicePoint 0.5 --{{{name}}}_png.folder png_{{{name}}}_{{{axis}}} {{/png_axis}} - {{/species_initmanager.species}} + {{/species}} {{/output.auto}} @@ -148,9 +148,9 @@ Instead of hard coding the output we might want to automatically generate one in --Cu_energyHistogram.minEnergy 0 --Cu_energyHistogram.maxEnergy 256000 - {{#species_initmanager.species}} + {{#species}} --{{{name}}}_macroParticlesCount.period 1 - {{/species_initmanager.species}} + {{/species}} {{/output.auto}} Let's go in detail through the above example. @@ -218,9 +218,9 @@ And of course it will be available as such in the rendering of the template whic --Cu_energyHistogram.minEnergy 0 --Cu_energyHistogram.maxEnergy 256000 - {{#species_initmanager.species}} + {{#species}} --{{{name}}}_macroParticlesCount.period 1 - {{/species_initmanager.species}} + {{/species}} {{/output.auto}} .. warning:: diff --git a/lib/python/picongpu/picmi/copy_attributes.py b/lib/python/picongpu/picmi/copy_attributes.py index 190c7d48f0..46764695a4 100644 --- a/lib/python/picongpu/picmi/copy_attributes.py +++ b/lib/python/picongpu/picmi/copy_attributes.py @@ -89,7 +89,11 @@ def copy_attributes( """ assignments = { to_name: _value_generator(from_name) - for from_name, _ in inspect.getmembers(from_instance) + for from_name, _ in ( + type(from_instance).model_fields.items() + if isinstance(from_instance, BaseModel) + else inspect.getmembers(from_instance) + ) if from_name not in ignore and not from_name.startswith("_") and has_attribute(to, to_name := from_name.removeprefix(remove_prefix)) diff --git a/lib/python/picongpu/picmi/diagnostics/__init__.py b/lib/python/picongpu/picmi/diagnostics/__init__.py index 0446d8a320..12c6a0593d 100644 --- a/lib/python/picongpu/picmi/diagnostics/__init__.py +++ b/lib/python/picongpu/picmi/diagnostics/__init__.py @@ -5,7 +5,6 @@ License: GPLv3+ """ -from .auto import Auto from .binning import Binning, BinningAxis, BinSpec from .phase_space import PhaseSpace from .energy_histogram import EnergyHistogram @@ -19,7 +18,6 @@ from .unit_dimension import UnitDimension __all__ = [ - "Auto", "BackendConfig", "OpenPMDConfig", "Binning", diff --git a/lib/python/picongpu/picmi/diagnostics/auto.py b/lib/python/picongpu/picmi/diagnostics/auto.py deleted file mode 100644 index a8783e8d4c..0000000000 --- a/lib/python/picongpu/picmi/diagnostics/auto.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2025 PIConGPU contributors -Authors: Pawel Ordyna -License: GPLv3+ -""" - -import typeguard - -from ...pypicongpu.output.auto import Auto as PyPIConGPUAuto -from ..copy_attributes import default_converts_to -from .timestepspec import TimeStepSpec - - -@default_converts_to(PyPIConGPUAuto) -@typeguard.typechecked -class Auto: - """ - Specifies the parameters for the Auto output. - - Parameters - ---------- - period: int - Number of simulation steps between consecutive outputs. - Unit: steps (simulation time steps). - """ - - period: TimeStepSpec - """Number of simulation steps between consecutive outputs. Unit: steps (simulation time steps).""" - - def __init__(self, period: TimeStepSpec) -> None: - self.period = period diff --git a/lib/python/picongpu/picmi/diagnostics/binning.py b/lib/python/picongpu/picmi/diagnostics/binning.py index 97481aa4bd..e74a4f937f 100644 --- a/lib/python/picongpu/picmi/diagnostics/binning.py +++ b/lib/python/picongpu/picmi/diagnostics/binning.py @@ -8,15 +8,13 @@ from pathlib import Path from ..copy_attributes import default_converts_to -import typeguard from picongpu.picmi.diagnostics.backend_config import OpenPMDConfig from ...pypicongpu.output.binning import Binning as PyPIConGPUBinning from ...pypicongpu.output.binning import BinningAxis as PyPIConGPUBinningAxis from ...pypicongpu.output.binning import BinSpec as PyPIConGPUBinSpec -from ...pypicongpu.species.species import Species as PyPIConGPUSpecies -from ..species import Species as PICMISpecies +from ..species import Species as Species from .particle_functor import ParticleFunctor as BinningFunctor from .timestepspec import TimeStepSpec @@ -30,7 +28,6 @@ def __init__(self, kind, start, stop, nsteps): self.nsteps = nsteps -@typeguard.typechecked class BinningAxis: def __init__( self, @@ -53,14 +50,13 @@ def get_as_pypicongpu(self) -> PyPIConGPUBinningAxis: ) -@typeguard.typechecked class Binning: def __init__( self, name: str, deposition_functor: BinningFunctor, axes: list[BinningAxis], - species: PICMISpecies | list[PICMISpecies], + species: Species | list[Species], period: TimeStepSpec | None = None, openPMD: dict | None = None, openPMDExt: str | None = None, @@ -70,7 +66,7 @@ def __init__( self.name = name self.deposition_functor = deposition_functor self.axes = axes - if isinstance(species, PICMISpecies): + if isinstance(species, Species): species = [species] self.species = species self.period = period or TimeStepSpec[:] @@ -86,19 +82,14 @@ def result_path(self, prefix_path): def get_as_pypicongpu( self, - dict_species_picmi_to_pypicongpu: dict[PICMISpecies, PyPIConGPUSpecies], time_step_size, num_steps, ) -> PyPIConGPUBinning: - if len(not_found := [s for s in self.species if s not in dict_species_picmi_to_pypicongpu.keys()]) > 0: - raise ValueError(f"Species {not_found} are not known to Simulation") - pypic_species = list(map(dict_species_picmi_to_pypicongpu.get, self.species)) - return PyPIConGPUBinning( name=self.name, deposition_functor=self.deposition_functor.get_as_pypicongpu(), axes=list(map(BinningAxis.get_as_pypicongpu, self.axes)), - species=pypic_species, + species=[s.get_as_pypicongpu() for s in self.species], period=self.period.get_as_pypicongpu(time_step_size, num_steps), openPMD=self.openPMD, openPMDExt=self.openPMDExt, diff --git a/lib/python/picongpu/picmi/diagnostics/energy_histogram.py b/lib/python/picongpu/picmi/diagnostics/energy_histogram.py index 36994d5bd8..eec7c7d37b 100644 --- a/lib/python/picongpu/picmi/diagnostics/energy_histogram.py +++ b/lib/python/picongpu/picmi/diagnostics/energy_histogram.py @@ -5,20 +5,20 @@ License: GPLv3+ """ -import typeguard +from pydantic import BaseModel + +from picongpu.picmi.copy_attributes import default_converts_to -from picongpu.picmi.diagnostics.util import diagnostic_converts_to from ...pypicongpu.output.energy_histogram import ( EnergyHistogram as PyPIConGPUEnergyHistogram, ) -from ..species import Species as PICMISpecies +from ..species import Species as Species from .timestepspec import TimeStepSpec -@diagnostic_converts_to(PyPIConGPUEnergyHistogram) -@typeguard.typechecked -class EnergyHistogram: +@default_converts_to(PyPIConGPUEnergyHistogram) +class EnergyHistogram(BaseModel): """ Specifies the parameters for the output of Energy Histogram of species such as electrons. @@ -53,24 +53,17 @@ class EnergyHistogram: Optional name for the energy histogram plugin. """ - def check(self, dict_species_picmi_to_pypicongpu, *args, **kwargs): + def check(self, *args, **kwargs): if self.min_energy >= self.max_energy: raise ValueError("min_energy must be less than max_energy") if self.bin_count <= 0: raise ValueError("bin_count must be > 0") - if self.species not in dict_species_picmi_to_pypicongpu.keys(): - raise ValueError(f"Species {self.species} is not known to Simulation") - - def __init__( - self, - species: PICMISpecies, - period: TimeStepSpec, - bin_count: int, - min_energy: float, - max_energy: float, - ): - self.species = species - self.period = period - self.bin_count = bin_count - self.min_energy = min_energy - self.max_energy = max_energy + + species: Species + period: TimeStepSpec + bin_count: int + min_energy: float + max_energy: float + + class Config: + arbitrary_types_allowed = True diff --git a/lib/python/picongpu/picmi/diagnostics/macro_particle_count.py b/lib/python/picongpu/picmi/diagnostics/macro_particle_count.py index 7194ee8672..33527a01c2 100644 --- a/lib/python/picongpu/picmi/diagnostics/macro_particle_count.py +++ b/lib/python/picongpu/picmi/diagnostics/macro_particle_count.py @@ -5,21 +5,19 @@ License: GPLv3+ """ -import typeguard +from pydantic import BaseModel -from picongpu.picmi.diagnostics.util import diagnostic_converts_to +from picongpu.picmi.copy_attributes import default_converts_to from ...pypicongpu.output.macro_particle_count import ( MacroParticleCount as PyPIConGPUMacroParticleCount, ) -from ...pypicongpu.species.species import Species as PyPIConGPUSpecies -from ..species import Species as PICMISpecies +from ..species import Species as Species from .timestepspec import TimeStepSpec -@diagnostic_converts_to(PyPIConGPUMacroParticleCount) -@typeguard.typechecked -class MacroParticleCount: +@default_converts_to(PyPIConGPUMacroParticleCount) +class MacroParticleCount(BaseModel): """ Specifies the parameters for counting the total number of macro particles of a given species. @@ -39,13 +37,8 @@ class MacroParticleCount: Optional name for the macro particle count plugin. """ - def __init__(self, species: PICMISpecies, period: TimeStepSpec): - self.species = species - self.period = period + species: Species + period: TimeStepSpec - def check(self, dict_species_picmi_to_pypicongpu: dict[PICMISpecies, PyPIConGPUSpecies], *args, **kwargs): - if self.species not in dict_species_picmi_to_pypicongpu.keys(): - raise ValueError(f"Species {self.species} is not known to Simulation") - pypicongpu_species = dict_species_picmi_to_pypicongpu.get(self.species) - if pypicongpu_species is None: - raise ValueError(f"Species {self.species} is not mapped to a PyPIConGPUSpecies.") + class Config: + arbitrary_types_allowed = True diff --git a/lib/python/picongpu/picmi/diagnostics/particle_dump.py b/lib/python/picongpu/picmi/diagnostics/particle_dump.py index 61cdc6e94e..8c262e4551 100644 --- a/lib/python/picongpu/picmi/diagnostics/particle_dump.py +++ b/lib/python/picongpu/picmi/diagnostics/particle_dump.py @@ -10,7 +10,7 @@ from pydantic import BaseModel -from picongpu.picmi.species import Species +from picongpu.picmi.species import Species as Species from .backend_config import BackendConfig, OpenPMDConfig from .timestepspec import TimeStepSpec diff --git a/lib/python/picongpu/picmi/diagnostics/phase_space.py b/lib/python/picongpu/picmi/diagnostics/phase_space.py index b61e249d83..5bf9177652 100644 --- a/lib/python/picongpu/picmi/diagnostics/phase_space.py +++ b/lib/python/picongpu/picmi/diagnostics/phase_space.py @@ -7,18 +7,17 @@ from typing import Literal -import typeguard +from pydantic import BaseModel -from picongpu.picmi.diagnostics.util import diagnostic_converts_to +from picongpu.picmi.copy_attributes import default_converts_to from ...pypicongpu.output.phase_space import PhaseSpace as PyPIConGPUPhaseSpace -from ..species import Species as PICMISpecies +from ..species import Species as Species from .timestepspec import TimeStepSpec -@diagnostic_converts_to(PyPIConGPUPhaseSpace) -@typeguard.typechecked -class PhaseSpace: +@default_converts_to(PyPIConGPUPhaseSpace) +class PhaseSpace(BaseModel): """ Specifies the parameters for the output of Phase Space of species such as electrons. @@ -52,30 +51,16 @@ class PhaseSpace: Optional name for the phase-space plugin. """ - def check(self, dict_species_picmi_to_pypicongpu, *args, **kwargs): + species: Species + period: TimeStepSpec + spatial_coordinate: Literal["x", "y", "z"] + momentum_coordinate: Literal["px", "py", "pz"] + min_momentum: float + max_momentum: float + + def check(self, *args, **kwargs): if self.min_momentum >= self.max_momentum: raise ValueError("min_momentum must be less than max_momentum") - if self.species not in dict_species_picmi_to_pypicongpu.keys(): - raise ValueError(f"Species {self.species} is not known to Simulation") - - # checks if PICMISpecies instance exists in the dictionary. If yes, it returns the corresponding PyPIConGPUSpecies instance. - # self.species refers to the species attribute of the class PhaseSpace(picmistandard.PICMI_PhaseSpace). - if dict_species_picmi_to_pypicongpu.get(self.species) is None: - raise ValueError(f"Species {self.species} is not mapped to a PyPIConGPUSpecies.") - - def __init__( - self, - species: PICMISpecies, - period: TimeStepSpec, - spatial_coordinate: Literal["x", "y", "z"], - momentum_coordinate: Literal["px", "py", "pz"], - min_momentum: float, - max_momentum: float, - ): - self.species = species - self.period = period - self.spatial_coordinate = spatial_coordinate - self.momentum_coordinate = momentum_coordinate - self.min_momentum = min_momentum - self.max_momentum = max_momentum + class Config: + arbitrary_types_allowed = True diff --git a/lib/python/picongpu/picmi/diagnostics/png.py b/lib/python/picongpu/picmi/diagnostics/png.py index a705697dd1..70b25ecaec 100644 --- a/lib/python/picongpu/picmi/diagnostics/png.py +++ b/lib/python/picongpu/picmi/diagnostics/png.py @@ -7,19 +7,18 @@ from typing import List -import typeguard +from pydantic import BaseModel -from picongpu.picmi.diagnostics.util import diagnostic_converts_to +from picongpu.picmi.copy_attributes import default_converts_to from ...pypicongpu.output.png import ColorScaleEnum, EMFieldScaleEnum from ...pypicongpu.output.png import Png as PyPIConGPUPNG -from ..species import Species as PICMISpecies +from ..species import Species as Species from .timestepspec import TimeStepSpec -@diagnostic_converts_to(PyPIConGPUPNG) -@typeguard.typechecked -class Png: +@default_converts_to(PyPIConGPUPNG) +class Png(BaseModel): """ Specifies the parameters for PNG output in PIConGPU. @@ -105,7 +104,7 @@ class Png: Custom expression for channel 3. """ - def check(self, dict_species_picmi_to_pypicongpu, *args, **kwargs): + def check(self, *args, **kwargs): if not (0.0 <= self.slice_point <= 1.0): raise ValueError("Slice point must be between 0.0 and 1.0") @@ -136,60 +135,29 @@ def check(self, dict_species_picmi_to_pypicongpu, *args, **kwargs): if self.pre_channel3_color_scales not in ColorScaleEnum: raise ValueError(f"Invalid color scale for channel 3. Valid options are {list(ColorScaleEnum)}.") - if self.species not in dict_species_picmi_to_pypicongpu.keys(): - raise ValueError(f"Species {self.species} is not known to Simulation") - - pypicongpu_species = dict_species_picmi_to_pypicongpu.get(self.species) - - if pypicongpu_species is None: - raise ValueError(f"Species {self.species} is not mapped to a PyPIConGPUSpecies.") - - def __init__( - self, - species: PICMISpecies, - period: TimeStepSpec, - axis: str, - slice_point: float, - folder_name: str, - scale_image: float, - scale_to_cellsize: bool, - white_box_per_gpu: bool, - em_field_scale_channel1: EMFieldScaleEnum, - em_field_scale_channel2: EMFieldScaleEnum, - em_field_scale_channel3: EMFieldScaleEnum, - pre_particle_density_color_scales: ColorScaleEnum, - pre_channel1_color_scales: ColorScaleEnum, - pre_channel2_color_scales: ColorScaleEnum, - pre_channel3_color_scales: ColorScaleEnum, - custom_normalization_si: List[float], - pre_particle_density_opacity: float, - pre_channel1_opacity: float, - pre_channel2_opacity: float, - pre_channel3_opacity: float, - pre_channel1: str, - pre_channel2: str, - pre_channel3: str, - ): - self.period = period - self.axis = axis - self.slice_point = slice_point - self.species = species - self.folder_name = folder_name - self.scale_image = scale_image - self.scale_to_cellsize = scale_to_cellsize - self.white_box_per_gpu = white_box_per_gpu - self.em_field_scale_channel1 = em_field_scale_channel1 - self.em_field_scale_channel2 = em_field_scale_channel2 - self.em_field_scale_channel3 = em_field_scale_channel3 - self.pre_particle_density_color_scales = pre_particle_density_color_scales - self.pre_channel1_color_scales = pre_channel1_color_scales - self.pre_channel2_color_scales = pre_channel2_color_scales - self.pre_channel3_color_scales = pre_channel3_color_scales - self.custom_normalization_si = custom_normalization_si - self.pre_particle_density_opacity = pre_particle_density_opacity - self.pre_channel1_opacity = pre_channel1_opacity - self.pre_channel2_opacity = pre_channel2_opacity - self.pre_channel3_opacity = pre_channel3_opacity - self.pre_channel1 = pre_channel1 - self.pre_channel2 = pre_channel2 - self.pre_channel3 = pre_channel3 + species: Species + period: TimeStepSpec + axis: str + slice_point: float + folder_name: str + scale_image: float + scale_to_cellsize: bool + white_box_per_gpu: bool + em_field_scale_channel1: EMFieldScaleEnum + em_field_scale_channel2: EMFieldScaleEnum + em_field_scale_channel3: EMFieldScaleEnum + pre_particle_density_color_scales: ColorScaleEnum + pre_channel1_color_scales: ColorScaleEnum + pre_channel2_color_scales: ColorScaleEnum + pre_channel3_color_scales: ColorScaleEnum + custom_normalization_si: List[float] + pre_particle_density_opacity: float + pre_channel1_opacity: float + pre_channel2_opacity: float + pre_channel3_opacity: float + pre_channel1: str + pre_channel2: str + pre_channel3: str + + class Config: + arbitrary_types_allowed = True diff --git a/lib/python/picongpu/picmi/diagnostics/util.py b/lib/python/picongpu/picmi/diagnostics/util.py deleted file mode 100644 index 95a9157d46..0000000000 --- a/lib/python/picongpu/picmi/diagnostics/util.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2025 PIConGPU contributors -Authors: Julian Lenz -License: GPLv3+ -""" - -from picongpu.picmi.copy_attributes import default_converts_to - - -def diagnostic_converts_to(*args, **kwargs): - kwargs["conversions"] = { - "species": lambda self, *args, **kwargs: kwargs["dict_species_picmi_to_pypicongpu"].get(self.species) - } | kwargs.get("conversions", {}) - return default_converts_to(*args, **kwargs) diff --git a/lib/python/picongpu/picmi/distribution/Distribution.py b/lib/python/picongpu/picmi/distribution/Distribution.py index e2bf162214..a008a8f1a7 100644 --- a/lib/python/picongpu/picmi/distribution/Distribution.py +++ b/lib/python/picongpu/picmi/distribution/Distribution.py @@ -44,6 +44,13 @@ class Distribution(pydantic.BaseModel): fill_in: bool = True """whether to fill in the empty space opened up when the simulation window moves""" + @pydantic.field_validator("rms_velocity", mode="after") + @classmethod + def isotropic_temperature(cls, value): + if not (value[0] == value[1] == value[2]): + raise ValueError("all thermal velcoity spread (rms velocity) components must be equal") + return value + def __hash__(self): """custom hash function for indexing in dicts""" hash_value = hash(type(self)) diff --git a/lib/python/picongpu/picmi/distribution/__init__.py b/lib/python/picongpu/picmi/distribution/__init__.py index a9ab964d27..a3c1e3a960 100644 --- a/lib/python/picongpu/picmi/distribution/__init__.py +++ b/lib/python/picongpu/picmi/distribution/__init__.py @@ -9,6 +9,10 @@ from .CylindricalDistribution import CylindricalDistribution from .AnalyticDistribution import AnalyticDistribution +AnyDistribution = ( + UniformDistribution | FoilDistribution | GaussianDistribution | CylindricalDistribution | AnalyticDistribution +) + __all__ = [ "UniformDistribution", "FoilDistribution", diff --git a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ADK.py b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ADK.py index cc313f4fcf..98030026d8 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ADK.py +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ADK.py @@ -38,9 +38,15 @@ def get_as_pypicongpu(self) -> IonizationModel: self.check() if self.ADK_variant is ADKVariant.LinearPolarization: - return ADKLinearPolarization(ionization_current=None_()) + return ADKLinearPolarization( + ionization_current=None_(), + ionization_electron_species=self.ionization_electron_species.get_as_pypicongpu(), + ) if self.ADK_variant is ADKVariant.CircularPolarization: - return ADKCircularPolarization(ionization_current=None_()) + return ADKCircularPolarization( + ionization_current=None_(), + ionization_electron_species=self.ionization_electron_species.get_as_pypicongpu(), + ) # unknown ADK variant raise ValueError(f"ADKVariant {self.ADK_variant} is not supported.") diff --git a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/BSI.py b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/BSI.py index 5da63673cc..c3b5752862 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/BSI.py +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/BSI.py @@ -35,13 +35,22 @@ def get_as_pypicongpu(self) -> ionizationmodel.IonizationModel: self.check() if self.BSI_extensions == []: - return ionizationmodel.BSI(ionization_current=None_()) + return ionizationmodel.BSI( + ionization_current=None_(), + ionization_electron_species=self.ionization_electron_species.get_as_pypicongpu(), + ) if len(self.BSI_extensions) > 1: pypicongpu.util.unsupported("more than one BSI_extension, will use first entry only") if self.BSI_extensions[0] is BSIExtension.StarkShift: - return ionizationmodel.BSIStarkShifted(ionization_current=None_()) + return ionizationmodel.BSIStarkShifted( + ionization_current=None_(), + ionization_electron_species=self.ionization_electron_species.get_as_pypicongpu(), + ) if self.BSI_extensions[0] is BSIExtension.EffectiveZ: - return ionizationmodel.BSIEffectiveZ(ionization_current=None_()) + return ionizationmodel.BSIEffectiveZ( + ionization_current=None_(), + ionization_electron_species=self.ionization_electron_species.get_as_pypicongpu(), + ) raise ValueError(f"unknown BSI_extension {self.BSI_extensions[0]}.") diff --git a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/keldysh.py b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/keldysh.py index 95a3e27f2a..56b4aa4786 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/keldysh.py +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/keldysh.py @@ -22,4 +22,6 @@ class Keldysh(FieldIonization): def get_as_pypicongpu(self) -> ionizationmodel.IonizationModel: self.check() - return ionizationmodel.Keldysh(ionization_current=None_()) + return ionizationmodel.Keldysh( + ionization_current=None_(), ionization_electron_species=self.ionization_electron_species.get_as_pypicongpu() + ) diff --git a/lib/python/picongpu/picmi/interaction/ionization/groundstateionizationmodel.py b/lib/python/picongpu/picmi/interaction/ionization/groundstateionizationmodel.py index 4c53f33abe..0f1e386efc 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/groundstateionizationmodel.py +++ b/lib/python/picongpu/picmi/interaction/ionization/groundstateionizationmodel.py @@ -14,6 +14,10 @@ @typeguard.typechecked class GroundStateIonizationModel(IonizationModel): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ion_species.register_requirements(self.get_constants()) + def get_constants(self) -> list[pypicongpu.species.constant.Constant]: """get all PyPIConGPU constants required by a ground state ionization model in PIConGPU""" self.check() diff --git a/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py b/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py index 5893ded083..de3b50d63e 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py +++ b/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py @@ -5,15 +5,18 @@ License: GPLv3+ """ +from picongpu.pypicongpu.species.util import Element +from picongpu.picmi.species_requirements import GroundStateIonizationConstruction, SetChargeStateOperation +from picongpu.picmi.species import DependsOn, Species +from picongpu.pypicongpu.species.attribute.boundelectrons import BoundElectrons from .... import pypicongpu -import pydantic +from pydantic import BaseModel, model_validator import typeguard -import typing @typeguard.typechecked -class IonizationModel(pydantic.BaseModel): +class IonizationModel(BaseModel): """ common interface for all ionization models @@ -23,12 +26,38 @@ class IonizationModel(pydantic.BaseModel): MODEL_NAME: str """ionization model""" - ion_species: typing.Any + ion_species: Species """PICMI ion species to apply ionization model for""" - ionization_electron_species: typing.Any + ionization_electron_species: Species """PICMI electron species of which to create macro particle upon ionization""" + @model_validator(mode="after") + def check(self): + if not Element.is_element(self.ion_species.particle_type): + raise ValueError(f"{self.ion_species=} must be an ion.") + if self.ion_species.picongpu_fixed_charge: + raise ValueError( + f"I'm trying hard to ionize here but {self.ion_species.picongpu_fixed_charge=} is getting in the way." + ) + if self.ion_species.charge_state is None: + raise ValueError( + f"Species {self.ion_species.name} configured with ionization but no initial charge state specified, " + "must be explicitly specified via charge_state." + ) + return self + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ion_species.register_requirements( + [ + DependsOn(species=self.ionization_electron_species), + GroundStateIonizationConstruction(ionization_model=self), + SetChargeStateOperation(species=self.ion_species), + BoundElectrons(), + ] + ) + def __hash__(self): """custom hash function for indexing in dicts""" hash_value = hash(type(self)) @@ -43,15 +72,6 @@ def __hash__(self): raise TypeError return hash_value - def check(self): - # import here to avoid circular import that stems from projecting different species types from PIConGPU onto the same `Species` type in PICMI - from ... import Species - - assert isinstance(self.ion_species, Species), "ion_species must be an instance of the species object" - assert isinstance(self.ionization_electron_species, Species), ( - "ionization_electron_species must be an instance of the species object" - ) - def get_constants(self) -> list[pypicongpu.species.constant.Constant]: raise NotImplementedError("abstract base class only!") diff --git a/lib/python/picongpu/picmi/layout.py b/lib/python/picongpu/picmi/layout.py index 0ba6cff22f..af38e4c706 100644 --- a/lib/python/picongpu/picmi/layout.py +++ b/lib/python/picongpu/picmi/layout.py @@ -64,3 +64,6 @@ def check(self): def get_as_pypicongpu(self): return OnePosition(ppc=self.n_macroparticles_per_cell) + + +AnyLayout = PseudoRandomLayout | GriddedLayout diff --git a/lib/python/picongpu/picmi/simulation.py b/lib/python/picongpu/picmi/simulation.py index ba3036fa0b..05f82f3e46 100644 --- a/lib/python/picongpu/picmi/simulation.py +++ b/lib/python/picongpu/picmi/simulation.py @@ -7,6 +7,8 @@ # make pypicongpu classes accessible for conversion to pypicongpu import datetime +from functools import reduce +from itertools import chain import logging import math from os import PathLike @@ -14,20 +16,49 @@ from pathlib import Path import picmistandard +from pydantic import BaseModel import typeguard from picongpu.picmi.diagnostics import ParticleDump, FieldDump +from picongpu.picmi.layout import AnyLayout +from picongpu.picmi.species_requirements import ( + SimpleDensityOperation, + SimpleMomentumOperation, + resolving_add, + get_as_pypicongpu, + run_construction, +) from picongpu.pypicongpu.output.openpmd_plugin import OpenPMDPlugin, FieldDump as PyPIConGPUFieldDump -from picongpu.pypicongpu.species.initmanager import InitManager +from picongpu.pypicongpu.species.attribute.weighting import Weighting +from picongpu.pypicongpu.species.attribute.momentum import Momentum from .. import pypicongpu from . import constants from .grid import Cartesian3DGrid from .interaction import Interaction -from .interaction.ionization import IonizationModel from .species import Species +class _DensityImpl(BaseModel): + layout: AnyLayout + grid: Cartesian3DGrid + species: Species + + class Config: + arbitrary_types_allowed = True + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.species.register_requirements( + [ + Weighting(), + SimpleDensityOperation(species=self.species, layout=self.layout, grid=self.grid), + Momentum(), + SimpleMomentumOperation(species=self.species), + ] + ) + + def _unique(iterable): # very naive, just for non-hashables that can still be compared result = [] @@ -139,6 +170,8 @@ class Simulation(picmistandard.PICMI_Simulation): picongpu_binomial_current_interpolation = pypicongpu.util.build_typesafe_property(bool) """switch on a binomial current interpolation""" + picongpu_distributions = pypicongpu.util.build_typesafe_property(list[_DensityImpl]) + __runner = pypicongpu.util.build_typesafe_property(typing.Optional[pypicongpu.runner.Runner]) # @todo remove boiler plate constructor argument list once picmistandard reference implementation switches to @@ -155,8 +188,8 @@ def __init__( picongpu_binomial_current_interpolation: bool = False, **keyword_arguments, ): + self.picongpu_distributions = [] self.picongpu_template_dir = _normalise_template_dir(picongpu_template_dir) - self.picongpu_typical_ppc = picongpu_typical_ppc self.picongpu_moving_window_move_point = picongpu_moving_window_move_point self.picongpu_moving_window_stop_iteration = picongpu_moving_window_stop_iteration self.picongpu_interaction = picongpu_interaction @@ -166,6 +199,10 @@ def __init__( self.picongpu_custom_user_input = None self.__runner = None + if picongpu_typical_ppc is not None and picongpu_typical_ppc <= 0: + raise ValueError(f"Typical ppc should be > 0, not {picongpu_typical_ppc=}.") + self.picongpu_typical_ppc = picongpu_typical_ppc + picmistandard.PICMI_Simulation.__init__(self, **keyword_arguments) # additional PICMI stuff checks, @todo move to picmistandard, Brian Marre, 2024 @@ -244,215 +281,6 @@ def __yee_compute_cfl_or_delta_t(self) -> None: # if neither delta_t nor cfl are given simply silently pass # (might change in the future) - def __get_operations_simple_density( - self, - pypicongpu_by_picmi_species: typing.Dict[Species, pypicongpu.species.Species], - ) -> typing.List[pypicongpu.species.operation.SimpleDensity]: - """ - retrieve operations for simple density placements - - Initialized Position & Weighting based on picmi initial distribution & - layout. - - initializes species using the same layout & profile from the same - operation - """ - # species with the same layout and initial distribution will result in - # the same macro particle placement - # -> throw them into a single operation - picmi_species_by_profile_by_layout = {} - for picmi_species, layout in zip(self.species, self.layouts): - if layout is None or picmi_species.initial_distribution is None: - # not placed -> not handled here - continue - - if layout not in picmi_species_by_profile_by_layout: - picmi_species_by_profile_by_layout[layout] = {} - - profile = picmi_species.initial_distribution - if profile not in picmi_species_by_profile_by_layout[layout]: - picmi_species_by_profile_by_layout[layout][profile] = [] - - picmi_species_by_profile_by_layout[layout][profile].append(picmi_species) - - # re-group as operations - all_operations = [] - for ( - layout, - picmi_species_by_profile, - ) in picmi_species_by_profile_by_layout.items(): - for profile, picmi_species_list in picmi_species_by_profile.items(): - op = pypicongpu.species.operation.SimpleDensity() - op.ppc = layout.n_macroparticles_per_cell - op.profile = profile.get_as_pypicongpu(self.solver.grid) - op.layout = layout.get_as_pypicongpu() - - op.species = set( - map( - lambda picmi_species: pypicongpu_by_picmi_species[picmi_species], - picmi_species_list, - ) - ) - - all_operations.append(op) - - return all_operations - - def __get_operations_not_placed( - self, - pypicongpu_by_picmi_species: typing.Dict[Species, pypicongpu.species.Species], - ) -> typing.List[pypicongpu.species.operation.NotPlaced]: - """ - retrieve operations for not placed species - - Problem: PIConGPU species need a position. But to get a position - generated, a species needs an operation which provides this position. - (E.g. SimpleDensity for regular profiles.) - - Solution: If a species has no initial distribution (profile), the - position attribute is provided by a NotPlaced operator, which does not - create any macroparticles (during initialization, that is). However, - using other methods (electron spawning...) macrosparticles can be - created by PIConGPU itself. - """ - all_operations = [] - - for picmi_species, layout in zip(self.species, self.layouts): - if layout is not None or picmi_species.initial_distribution is not None: - continue - - # is not placed -> add op - not_placed = pypicongpu.species.operation.NotPlaced() - not_placed.species = pypicongpu_by_picmi_species[picmi_species] - all_operations.append(not_placed) - - return all_operations - - def __get_operations_from_individual_species( - self, - pypicongpu_by_picmi_species: typing.Dict[Species, pypicongpu.species.Species], - ) -> typing.List[pypicongpu.species.operation.Operation]: - """ - call get_independent_operations() of all species - - used for momentum: Momentum depends only on temperature & drift, NOT on - other species. Therefore, the generation of the momentum operations is - performed inside of the individual species objects. - """ - all_operations = [] - - for picmi_species, pypicongpu_species in pypicongpu_by_picmi_species.items(): - all_operations += picmi_species.get_independent_operations(pypicongpu_species, self.picongpu_interaction) - - return all_operations - - def __check_preconditions_init_manager(self) -> None: - """check preconditions, @todo move to picmistandard, Brian Marre 2024""" - assert len(self.species) == len(self.layouts) - - for layout, picmi_species in zip(self.layouts, self.species): - profile = picmi_species.initial_distribution - ratio = picmi_species.density_scale - - assert 1 != [layout, profile].count(None), ( - "species need BOTH layout AND initial distribution set (or neither)" - ) - - if ratio is not None: - assert layout is not None and profile is not None, ( - "layout and initial distribution must be set to use density scale" - ) - - def __get_translated_species_and_ionization_models( - self, - ) -> tuple[ - dict[Species, pypicongpu.species.Species], - dict[ - Species, - None - | dict[ - IonizationModel, - pypicongpu.species.constant.ionizationmodel.IonizationModel, - ], - ], - ]: - """ - get mappping of PICMI species to PyPIConGPU species and mapping of of simulation - - @details cache to reuse *exactly the same* object in operations - """ - - pypicongpu_by_picmi_species = {} - ionization_model_conversion_by_species = {} - for picmi_species in self.species: - # @todo split into two different fucntion calls?, Brian Marre, 2024 - pypicongpu_species, ionization_model_conversion = picmi_species.get_as_pypicongpu(self.picongpu_interaction) - - pypicongpu_by_picmi_species[picmi_species] = pypicongpu_species - ionization_model_conversion_by_species[picmi_species] = ionization_model_conversion - - return pypicongpu_by_picmi_species, ionization_model_conversion_by_species - - def __fill_in_ionization_electrons( - self, - pypicongpu_by_picmi_species: dict[Species, pypicongpu.species.Species], - ionization_model_conversion_by_species: dict[ - Species, - None - | dict[ - IonizationModel, - pypicongpu.species.constant.ionizationmodel.IonizationModel, - ], - ], - ) -> None: - """ - set the ionization electron species for each ionization model - - Ionization electron species need to be set after species translation is complete since the PyPIConGPU electron - species is not at the time of translation by the PICMI ion species. - """ - if self.picongpu_interaction is not None: - self.picongpu_interaction.fill_in_ionization_electron_species( - pypicongpu_by_picmi_species, ionization_model_conversion_by_species - ) - - def __get_init_manager( - self, - ) -> tuple[InitManager, typing.Dict[Species, pypicongpu.species.Species]]: - """ - create & fill an Initmanager - - performs the following steps: - 1. check preconditions - 2. translate species and ionization models to PyPIConGPU representations - Note: Cache translations to avoid creating new translations by continuously translating again and again - 3. generate operations which have inter-species dependencies - 4. generate operations without inter-species dependencies - """ - self.__check_preconditions_init_manager() - ( - pypicongpu_by_picmi_species, - ionization_model_conversion_by_species, - ) = self.__get_translated_species_and_ionization_models() - - # fill inter-species dependencies - self.__fill_in_ionization_electrons(pypicongpu_by_picmi_species, ionization_model_conversion_by_species) - - # init PyPIConGPU init manager - initmgr = InitManager() # This works because InitManager is imported - - for pypicongpu_species in pypicongpu_by_picmi_species.values(): - initmgr.all_species.append(pypicongpu_species) - - # operations on multiple species - initmgr.all_operations += self.__get_operations_simple_density(pypicongpu_by_picmi_species) - - # operations on single species - initmgr.all_operations += self.__get_operations_not_placed(pypicongpu_by_picmi_species) - initmgr.all_operations += self.__get_operations_from_individual_species(pypicongpu_by_picmi_species) - - return initmgr, pypicongpu_by_picmi_species - def write_input_file( self, file_name: str, @@ -493,14 +321,14 @@ def step(self, nsteps: int = 1): ) self.picongpu_run() - def _generate_openpmd_plugins(self, diagnostics, pypicongpu_by_picmi_species, num_steps): + def _generate_openpmd_plugins(self, diagnostics, num_steps): diagnostics = list(diagnostics) return [ OpenPMDPlugin( sources=[ ( diagnostic.period.get_as_pypicongpu(time_step_size=self.time_step_size, num_steps=num_steps), - pypicongpu_by_picmi_species[diagnostic.species] + diagnostic.species.get_as_pypicongpu() if isinstance(diagnostic, ParticleDump) else PyPIConGPUFieldDump(name=diagnostic.fieldname), ) @@ -511,97 +339,114 @@ def _generate_openpmd_plugins(self, diagnostics, pypicongpu_by_picmi_species, nu for options in _unique(map(lambda x: x.options, diagnostics)) ] - def _generate_plugins(self, pypicongpu_by_picmi_species, num_steps): + def _generate_plugins(self, num_steps): return [ entry.get_as_pypicongpu( - dict_species_picmi_to_pypicongpu=pypicongpu_by_picmi_species, time_step_size=self.time_step_size, num_steps=num_steps, ) for entry in self.diagnostics if not handled_via_openpmd(entry) - ] + self._generate_openpmd_plugins( - filter(handled_via_openpmd, self.diagnostics), pypicongpu_by_picmi_species, num_steps - ) - - def get_as_pypicongpu(self) -> pypicongpu.simulation.Simulation: - """translate to PyPIConGPU object""" - s = pypicongpu.simulation.Simulation() - - s.delta_t_si = self.time_step_size - s.solver = self.solver.get_as_pypicongpu() - - # already pypicongpu objects, therefore directly passing on - s.custom_user_input = self.picongpu_custom_user_input - - # calculate time step - if self.max_steps is not None: - s.time_steps = self.max_steps - elif self.max_time is not None: - s.time_steps = math.ceil(self.max_time / self.time_step_size) - else: - raise ValueError("runtime not specified (neither as step count nor max time)") + ] + self._generate_openpmd_plugins(filter(handled_via_openpmd, self.diagnostics), num_steps) + def _check_compatibility(self): pypicongpu.util.unsupported("verbose", self.verbose) pypicongpu.util.unsupported("particle shape", self.particle_shape, "linear") pypicongpu.util.unsupported("gamma boost", self.gamma_boost) - - try: - s.grid = self.solver.grid.get_as_pypicongpu() - except AttributeError: - pypicongpu.util.unsupported(f"grid type: {type(self.solver.grid)}") - - # any injection method != None is not supported if len(self.laser_injection_methods) != self.laser_injection_methods.count(None): pypicongpu.util.unsupported("laser injection method", self.laser_injection_methods, []) + if self.max_steps is None and self.max_time is None: + raise ValueError("runtime not specified (neither as step count nor max time)") - if len(self.lasers) > 0: - s.laser = [ll.get_as_pypicongpu() for ll in self.lasers] - else: - # explictly disable laser (as required by pypicongpu) - s.laser = None - - s.init_manager, pypicongpu_by_picmi_species = self.__get_init_manager() - - s.plugins = self._generate_plugins(pypicongpu_by_picmi_species, s.time_steps) - # set typical ppc if not set explicitly by user - if self.picongpu_typical_ppc is None: - s.typical_ppc = (s.init_manager).get_typical_particle_per_cell() - else: - s.typical_ppc = self.picongpu_typical_ppc - - if s.typical_ppc < 1: - raise ValueError("typical_ppc must be >= 1") + def get_as_pypicongpu(self) -> pypicongpu.simulation.Simulation: + """translate to PyPIConGPU object""" + self._check_compatibility() - s.base_density = self.picongpu_base_density or s.init_manager.get_base_density(s.grid) + init_operations = organise_init_operations( + chain(*(s.get_operation_requirements() for s in sorted(self.species))) + ) - # disable moving Window if explicitly activated by the user - if self.picongpu_moving_window_move_point is None: - s.moving_window = None - else: - s.moving_window = pypicongpu.movingwindow.MovingWindow( + typical_ppc = ( + self.picongpu_typical_ppc + if self.picongpu_typical_ppc is not None + else _mid_window(map(lambda op: op.layout.ppc, filter(lambda op: hasattr(op, "layout"), init_operations))) + ) + moving_window = ( + None + if self.picongpu_moving_window_move_point is None + else pypicongpu.movingwindow.MovingWindow( move_point=self.picongpu_moving_window_move_point, stop_iteration=self.picongpu_moving_window_stop_iteration, ) + ) + walltime = ( + None if self.picongpu_walltime is None else pypicongpu.walltime.Walltime(walltime=self.picongpu_walltime) + ) + time_steps = self.max_steps if self.max_steps is not None else math.ceil(self.max_time / self.time_step_size) + + return pypicongpu.simulation.Simulation( + species=map(get_as_pypicongpu, sorted(self.species)), + init_operations=init_operations, + typical_ppc=typical_ppc, + delta_t_si=self.time_step_size, + solver=self.solver.get_as_pypicongpu(), + custom_user_input=self.picongpu_custom_user_input, + grid=self.solver.grid.get_as_pypicongpu(), + binomial_current_interpolation=self.picongpu_binomial_current_interpolation, + moving_window=moving_window, + walltime=walltime, + time_steps=time_steps, + laser=[ll.get_as_pypicongpu() for ll in self.lasers] or None, + plugins=self._generate_plugins(time_steps), + base_density=self._get_base_density(), + ) - if self.picongpu_walltime is None: - s.walltime = None - else: - s.walltime = pypicongpu.walltime.Walltime(walltime=self.picongpu_walltime) - - s.binomial_current_interpolation = self.picongpu_binomial_current_interpolation - - return s + def _get_base_density(self) -> float: + return self.picongpu_base_density or 1.0e25 def picongpu_run(self) -> None: """build and run PIConGPU simulation""" - if self.__runner is None: - self.__runner = pypicongpu.runner.Runner(self.get_as_pypicongpu(), self.picongpu_template_dir) - self.__runner.generate() - self.__runner.build() - self.__runner.run() + runner = self.picongpu_get_runner() + runner.generate() + runner.build() + runner.run() def picongpu_get_runner(self) -> pypicongpu.runner.Runner: if self.__runner is None: self.__runner = pypicongpu.runner.Runner(self.get_as_pypicongpu(), self.picongpu_template_dir) return self.__runner + + def _picongpu_add_species(self, species, layout): + self.species.append(species) + self.layouts.append(layout) + if species.density_scale is not None and (layout is None and species.initial_distribution is None): + raise ValueError("layout and initial distribution must be set to use density scale") + if layout is not None and species.initial_distribution is None: + raise ValueError( + f"An initial distribution needs a layout. You've given {layout=} but {species.initial_distribution=}." + ) + if species.initial_distribution is not None: + self.picongpu_distributions.append(_DensityImpl(species=species, layout=layout, grid=self.solver.grid)) + + def add_species(self, *args, **kwargs): + return self._picongpu_add_species(*args, **kwargs) + + +def organise_init_operations(operations): + cleaned = [] + for op in operations: + cleaned = resolving_add(op, cleaned) + return [run_construction(op) for op in cleaned] + + +def _mid_window(iterable): + """Compute the integer in the middle between min(iterable), max(iterable), return 1 if empty.""" + iterable = iter(iterable) + + try: + start = next(iterable) + except StopIteration: + return 1 + + mi, ma = reduce(lambda lhs, rhs: (min(lhs[0], rhs), max(lhs[1], rhs)), iterable, (start, start)) + return int((ma - mi) // 2 + mi) diff --git a/lib/python/picongpu/picmi/species.py b/lib/python/picongpu/picmi/species.py index 5aa66e2d8f..3f1c551813 100644 --- a/lib/python/picongpu/picmi/species.py +++ b/lib/python/picongpu/picmi/species.py @@ -1,153 +1,93 @@ """ This file is part of PIConGPU. Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre +Authors: Hannes Troepgen, Brian Edward Marre, Julian Lenz License: GPLv3+ """ -from picongpu.pypicongpu.species.species import Shape -from .predefinedparticletypeproperties import PredefinedParticleTypeProperties -from .interaction import Interaction +from enum import Enum +import re +from typing import Any + +from pydantic import BaseModel, PrivateAttr, computed_field, model_validator, field_validator + +from picongpu.picmi.distribution import AnyDistribution +from picongpu.picmi.species_requirements import resolving_add, evaluate_requirements, run_construction +from picongpu.pypicongpu.species.attribute import Momentum, Position +from picongpu.pypicongpu.species.attribute.attribute import Attribute +from picongpu.pypicongpu.species.attribute.weighting import Weighting +from picongpu.pypicongpu.species.constant.charge import Charge +from picongpu.pypicongpu.species.constant.constant import Constant +from picongpu.pypicongpu.species.constant.densityratio import DensityRatio +from picongpu.pypicongpu.species.constant.mass import Mass +from picongpu.pypicongpu.species.operation.operation import Operation +from picongpu.pypicongpu.species.species import Shape, Pusher, Species as PyPIConGPUSpecies from .. import pypicongpu from ..pypicongpu.species.util.element import Element +from .predefinedparticletypeproperties import PredefinedParticleTypeProperties -import picmistandard - -import typing -import typeguard -import logging -import re -import copy - -from scipy import constants as consts - - -@typeguard.typechecked -class Species(picmistandard.PICMI_Species): - """PICMI object for a (single) particle species""" - - picongpu_element = pypicongpu.util.build_typesafe_property(typing.Optional[Element]) - """element information of object""" - - __non_element_particle_types: list[str] = copy.copy(PredefinedParticleTypeProperties().get_known_particle_types()) - """list of all known non element particle types""" - - picongpu_fixed_charge = pypicongpu.util.build_typesafe_property(bool) - - interactions = pypicongpu.util.build_typesafe_property(typing.Optional[list[None]]) - """overwrite base class interactions to disallow setting them""" - - __warned_already: bool = False - __previous_check: bool = False - - def __init__(self, picongpu_fixed_charge: bool = False, **keyword_arguments): - self.picongpu_fixed_charge = picongpu_fixed_charge - self.picongpu_element = None - - # let PICMI class handle remaining init - picmistandard.PICMI_Species.__init__(self, **keyword_arguments) - - @staticmethod - def __get_temperature_kev_by_rms_velocity( - rms_velocity_si: tuple[float, float, float], particle_mass_si: float - ) -> float: - """ - convert temperature from RMS velocity vector to keV - - Uses assertions to reject incorrect format. - Ensures that all three vector components are equal and >0. - - Helper function invoked from inside Distribution classes. - - :param rms_velocity_si: rms velocity (thermal velocity spread) per - direction in m/s - :param particle_mass_si: particle mass in kg - :raises Exception: on impossible conversion - :return: temperature in keV - """ - assert rms_velocity_si[0] == rms_velocity_si[1] and rms_velocity_si[1] == rms_velocity_si[2], ( - "all thermal velcoity spread (rms velocity) components must be equal" - ) - # see - # https://en.wikipedia.org/wiki/Maxwell%E2%80%93Boltzmann_distribution - rms_velocity_si_squared = rms_velocity_si[0] ** 2 - return particle_mass_si * rms_velocity_si_squared * consts.electron_volt**-1 * 10**-3 - - def __get_drift(self) -> typing.Optional[pypicongpu.species.operation.momentum.Drift]: - """ - Retrieve respective pypicongpu drift object (or None) - - Returns none if: rms_velocity is 0 OR distribution is not set - - :return: drift object (might be none) - """ - if self.initial_distribution is None: - return None - return self.initial_distribution.get_picongpu_drift() - - def __maybe_apply_particle_type(self) -> None: - """ - if particle type is set, set self.mass, self.charge and element from particle type - - necessary to ensure consistent state regardless which parameters the user specified in species init - - @raises if both particle_type and charge mass are specified - """ - if (self.particle_type is None) or re.match(r"other:.*", self.particle_type): - # no particle or custom particle type set - pass - else: - # set mass & charge - if self.particle_type in self.__non_element_particle_types: - # not an element, but known particle_type - mass_charge_tuple = PredefinedParticleTypeProperties().get_mass_and_charge_of_non_element( - self.particle_type +class ParticleShape(Enum): + NGP = "NGP" + CIC = "linear" + TSC = "quadratic" + PQS = "cubic" + PCS = "quartic" + counter = "counter" + + +class PusherMethod(Enum): + # supported by PICMI standard and PIConGPU + Boris = "Boris" + Vay = "Vay" + HigueraCary = "Higuera-Cary" + Free = "free" + ReducedLandauLifshitz = "LLRK4" + # only supported by PIConGPU + Acceleration = "Acceleration" + Photon = "Photon" + Probe = "Probe" + Axel = "Axel" + # not supported by PIConGPU + Li = "Li" + + +class Species(BaseModel): + name: str + particle_type: str | None = None + initial_distribution: AnyDistribution | None = None + picongpu_fixed_charge: bool = False + charge_state: int | None = None + density_scale: float | None = None + mass: float | None = None + charge: float | None = None + particle_shape: ParticleShape = ParticleShape("quadratic") + method: PusherMethod = PusherMethod("Boris") + + # Theoretically, Position(), Momentum() and Weighting() are also requirements imposed from the outside, + # e.g., by the current deposition, pusher, ..., but these concepts are not separately modelled in PICMI + # particularly not as being applied to a particular species. + # For now, we add them to all species. Refinements might be necessary in the future. + _requirements: list[Any] = PrivateAttr(default_factory=lambda: [Position(), Weighting(), Momentum()]) + + @field_validator("name", mode="before") + @classmethod + def _validate_name(cls, value, values): + if value is None: + if values["particle_type"] is None: + raise ValueError( + "Can't come up with a proper name for your species because neither name nor particle type are given." ) - self.mass = mass_charge_tuple.mass - self.charge = mass_charge_tuple.charge - elif Element.is_element(self.particle_type): - # element or similar, will raise if element name is unknown - self.picongpu_element = pypicongpu.species.util.Element(self.particle_type) - self.mass = self.picongpu_element.get_mass_si() - self.charge = self.picongpu_element.get_charge_si() - else: - # unknown particle type - raise ValueError(f"Species {self.name} has unknown particle type {self.particle_type}") - - def has_ionization(self, interaction: Interaction | None) -> bool: - """does species have ionization configured?""" - if interaction is None: - return False - if interaction.has_ionization(self): - return True - - # to get typecheck to shut up - return False + value = values["particle_type"] + return value - def is_ion(self) -> bool: - """ - is species an ion? - - @attention requires __maybe_apply_particle_type() to have been called first, - otherwise will return wrong result - """ - if self.picongpu_element is None: - return False - return True - - def __check_ionization_configuration(self, interaction: Interaction | None) -> None: - """ - check species ioniaztion- and species- configuration are compatible - - @raises if incorrect configuration found - """ + class Config: + arbitrary_types_allowed = True + @model_validator(mode="after") + def check(self): if self.particle_type is None: - assert not self.has_ionization(interaction), ( - f"Species {self.name} configured with active ionization but required particle_type not set." - ) assert self.charge_state is None, ( f"Species {self.name} specified initial charge state via charge_state without also specifying particle " "type, must either set particle_type explicitly or only use charge instead" @@ -155,178 +95,88 @@ def __check_ionization_configuration(self, interaction: Interaction | None) -> N assert self.picongpu_fixed_charge is False, ( f"Species {self.name} specified fixed charge without also specifying particle_type" ) - else: - # particle type is - if (self.particle_type in self.__non_element_particle_types) or re.match(r"other:.*", self.particle_type): - # non ion predefined particle, or custom particle type - assert self.charge_state is None, "charge_state may only be set for ions" - assert not self.has_ionization(interaction), ( - f"Species {self.name} configured with active ionization but particle type indicates non ion." + # Returns None if it is not an element, so is False-y in those cases, and True-y otherwise: + elif self.picongpu_element: + if self.charge_state is not None: + assert Element(self.particle_type).get_atomic_number() >= self.charge_state, ( + f"Species {self.name} intial charge state is unphysical" ) - assert self.picongpu_fixed_charge is False, ( - f"Species {self.name} configured with fixed charge state but particle_type indicates non ion" - ) - elif Element.is_element(self.particle_type): - # ion - - # check for unphysical charge state - if self.charge_state is not None: - assert Element(self.particle_type).get_atomic_number() >= self.charge_state, ( - f"Species {self.name} intial charge state is unphysical" - ) - - if self.has_ionization(interaction): - assert self.picongpu_fixed_charge is False, ( - f"Species {self.name} configured both as fixed charge ion and ion with ionization, may be " - " either or but not both." - ) - assert self.charge_state is not None, ( - f"Species {self.name} configured with ionization but no initial charge state specified, " - "must be explicitly specified via charge_state." - ) - else: - # ion with fixed charge - if self.picongpu_fixed_charge is False: - raise ValueError( - f"Species {self.name} configured with fixed charge state without explicitly setting picongpu_fixed_charge=True" - ) - - if not self.__warned_already: - logging.warning( - f"Species {self.name} configured with fixed charge state but particle type" - "indicates element. This is not recommended but supported" - ) - self.__warned_already = True - - # charge_state may be set or None indicating some fixed number of bound electrons or fully ionized - # ion - else: - # unknown particle type - raise ValueError(f"unknown particle type {self.particle_type} in species {self.name}") - - def __check_interaction_configuration(self, interaction: Interaction | None) -> None: - """check all interactions sub groups for compatibility with this species configuration""" - self.__check_ionization_configuration(interaction) - - def check(self, interaction: Interaction | None) -> None: - assert self.name is not None, "picongpu requires each species to have a name set." - - # check charge and mass explicitly set/not set depending on particle_type - if (self.particle_type is None) or re.match(r"other:.*", self.particle_type): - # custom species may not have mass or charge - pass - elif not self.__previous_check: - assert self.charge is None, ( - f"Species' {self.name}, charge is specified implicitly via particle type, do NOT set charge explictly" - ) - assert self.mass is None, ( - f"Species' {self.name}, mass is specified implicitly via particle type, do NOT set mass explictly" - ) - - self.__check_interaction_configuration(interaction) - self.__previous_check = True - - def get_as_pypicongpu( - self, interaction: Interaction | None - ) -> tuple[ - pypicongpu.species.Species, None | dict[typing.Any, pypicongpu.species.constant.ionizationmodel.IonizationModel] - ]: - """ - translate PICMI species object to equivalent PyPIConGPU species object - - @attention only translates ONLY species owned objects, for example species-Constants - everything else requires a call to the corresponding getter of this class - """ - - # error on unsupported options - pypicongpu.util.unsupported("method", self.method) - # @note placement params are respected in associated simulation object - - self.check(interaction) - self.__maybe_apply_particle_type() - - s = pypicongpu.species.Species() - s.name = self.name - s.constants = [] - - if self.mass: - # if 0==mass rather omit mass entirely - assert self.mass > 0 - - mass_constant = pypicongpu.species.constant.Mass(mass_si=self.mass) - s.constants.append(mass_constant) - - if self.density_scale is not None: - assert self.density_scale > 0 - - density_scale_constant = pypicongpu.species.constant.DensityRatio(ratio=self.density_scale) - s.constants.append(density_scale_constant) - - # default case species with no charge and/or no bound electrons or with ionization - charge_constant_value = self.charge - - initial_charge_state_set = self.charge_state is not None - fixed_charge_state = not self.has_ionization(interaction) - if self.is_ion() and initial_charge_state_set and fixed_charge_state: - # fixed not completely ionized ion - charge_constant_value = self.charge_state * consts.elementary_charge - - if charge_constant_value is not None: - charge_constant = pypicongpu.species.constant.Charge(charge_si=charge_constant_value) - s.constants.append(charge_constant) - - if interaction is not None: - interaction_constants, pypicongpu_model_by_picmi_model = interaction.get_interaction_constants(self) - s.constants.extend(interaction_constants) else: - pypicongpu_model_by_picmi_model = None - - s.shape = Shape[(self.particle_shape or "TSC").upper()] - - return s, pypicongpu_model_by_picmi_model - - def get_independent_operations( - self, pypicongpu_species: pypicongpu.species.Species, interaction: Interaction | None - ) -> list[pypicongpu.species.operation.Operation]: - """get a list of all operations only initializing attributes of this species""" - - # assure consistent state of species - self.check(interaction) - self.__maybe_apply_particle_type() - - assert pypicongpu_species.name == self.name, "to generate operations for PyPIConGPU species: names must match" - - all_operations = [] - - # assign momentum - momentum_op = pypicongpu.species.operation.SimpleMomentum() - momentum_op.species = pypicongpu_species - momentum_op.drift = self.__get_drift() - - temperature_kev = 0 - if self.initial_distribution is not None and self.initial_distribution.rms_velocity is not None: - mass_const = pypicongpu_species.get_constant_by_type(pypicongpu.species.constant.Mass) - mass_si = mass_const.mass_si + assert self.charge_state is None, "charge_state may only be set for ions" + assert self.picongpu_fixed_charge is False, ( + f"Species {self.name} configured with fixed charge state but particle_type indicates non ion" + ) + return self - temperature_kev = self.__get_temperature_kev_by_rms_velocity( - tuple(self.initial_distribution.rms_velocity), mass_si + @computed_field + def picongpu_element(self) -> Element | None: + if self.particle_type is None: + return None + try: + return ( + pypicongpu.species.util.Element(self.particle_type) if Element.is_element(self.particle_type) else None ) + except ValueError: + return None - if 0 != temperature_kev: - momentum_op.temperature = pypicongpu.species.operation.momentum.Temperature(temperature_kev=temperature_kev) - else: - momentum_op.temperature = None + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._register_initial_requirements() - all_operations.append(momentum_op) + def _register_initial_requirements(self): + constants = ( + ([DensityRatio(ratio=self.density_scale)] if self.density_scale is not None else []) + + ([Mass(mass_si=self.mass)] if self.mass is not None else []) + + ([Charge(charge_si=self.charge)] if self.charge is not None else []) + ) + self.register_requirements(particle_type_requirements(self.particle_type) + constants) + + def get_as_pypicongpu(self, *args, **kwargs): + return PyPIConGPUSpecies( + name=self.name, + **self._evaluate_species_requirements(), + shape=Shape[self.particle_shape.name], + pusher=Pusher[self.method.name], + ) - # assign boundElectrons attribute - if self.is_ion() and self.has_ionization(interaction): - bound_electrons_op = pypicongpu.species.operation.SetChargeState() - bound_electrons_op.species = pypicongpu_species - bound_electrons_op.charge_state = self.charge_state - all_operations.append(bound_electrons_op) - else: - # fixed charge state -> therefore no bound electron attribute necessary - pass + def get_operation_requirements(self): + return evaluate_requirements(self._requirements, Operation) - return all_operations + def _evaluate_species_requirements(self): + return { + key: [run_construction(value) for value in values] + for key, values in zip( + ("constants", "attributes"), evaluate_requirements(self._requirements, [Constant, Attribute]) + ) + } + + def __gt__(self, other): + # This defines a partial ordering on all species. + # This is necessary to determine the definition order inside of the C++ header. + if not isinstance(other, Species): + raise ValueError(f"Unknown comparison between {self=} and {other=}.") + return any(isinstance(req, DependsOn) and req.species == other for req in self._requirements) + + def register_requirements(self, requirements): + for requirement in requirements: + self._requirements = resolving_add(requirement, self._requirements) + + +def particle_type_requirements(particle_type): + if (particle_type is None) or re.match(r"other:.*", particle_type): + # no particle or custom particle type set + return [] + if particle_type in (props := PredefinedParticleTypeProperties()).get_known_particle_types(): + mass, charge = props.get_mass_and_charge_of_non_element(particle_type) + elif Element.is_element(particle_type): + element = pypicongpu.species.util.Element(particle_type) + mass = element.get_mass_si() + charge = element.get_charge_si() + else: + # unknown particle type + raise ValueError(f"Species has unknown particle type {particle_type}") + return [Mass(mass_si=mass), Charge(charge_si=charge)] + + +class DependsOn(BaseModel): + species: Species diff --git a/lib/python/picongpu/picmi/species_requirements.py b/lib/python/picongpu/picmi/species_requirements.py new file mode 100644 index 0000000000..541dc557a8 --- /dev/null +++ b/lib/python/picongpu/picmi/species_requirements.py @@ -0,0 +1,279 @@ +""" +This file is part of PIConGPU. +Copyright 2025 PIConGPU contributors +Authors: Julian Lenz +License: GPLv3+ +""" + +from typing import Any, Callable +from scipy.constants import electron_volt + +import numpy as np +from pydantic import BaseModel, Field + +from picongpu.pypicongpu.species.attribute.attribute import Attribute +from picongpu.pypicongpu.species.constant.constant import Constant +from picongpu.pypicongpu.species.constant.groundstateionization import GroundStateIonization +from picongpu.pypicongpu.species.constant.mass import Mass +from picongpu.pypicongpu.species.operation.momentum.temperature import Temperature +from picongpu.pypicongpu.species.operation.setchargestate import SetChargeState +from picongpu.pypicongpu.species.operation.simpledensity import SimpleDensity +from picongpu.pypicongpu.species.operation.simplemomentum import SimpleMomentum + + +def resolving_add(new, to): + # safeguard against exhaustible iterators, this is multi-pass + to = list(to) + + for rhs in to: + check_for_conflict(new, rhs) + + if must_be_unique(new) and any(can_be_dropped_due_to_uniqueness(new, rhs) for rhs in to): + return to + + for into_instance in to: + if try_update_with(into_instance, new): + return to + + return to + [new] + + +def get_as_pypicongpu(obj, *args, **kwargs): + if hasattr(obj, "get_as_pypicongpu"): + return obj.get_as_pypicongpu(*args, **kwargs) + return obj + + +def must_be_unique(requirement): + return (hasattr(requirement, "must_be_unique") and requirement.must_be_unique) or ( + isinstance(requirement, Constant) or isinstance(requirement, Attribute) + ) + + +def can_be_dropped_due_to_uniqueness(lhs, rhs): + if hasattr(lhs, "can_be_dropped_due_to_uniqueness") and lhs.can_be_dropped_due_to_uniqueness(rhs): + return True + if hasattr(rhs, "can_be_dropped_due_to_uniqueness") and rhs.can_be_dropped_due_to_uniqueness(lhs): + return True + try: + # These might well be apples and oranges and the comparison might fail. + if lhs == rhs: + return True + except Exception: + pass + return False + + +def try_update_with(into_instance, from_instance): + if hasattr(into_instance, "try_update_with"): + return into_instance.try_update_with(from_instance) + return False + + +class RequirementConflict(Exception): + pass + + +def check_for_conflict(obj1, obj2): + try: + if hasattr(obj1, "check_for_conflict"): + obj1.check_for_conflict(obj2) + if hasattr(obj2, "check_for_conflict"): + obj2.check_for_conflict(obj1) + if isinstance(obj1, Constant) and (isinstance(obj1, type(obj2)) or isinstance(obj2, type(obj1))): + if obj1 != obj2: + raise RequirementConflict(f"Conflicting constants {obj1=} and {obj2=} required.") + except Exception as err: + raise RequirementConflict( + f"A conflict in requirements between {obj1=} and {obj2=} has been detected." + "See above error message for details." + ) from err + + +def run_construction(obj): + return obj.run_construction() if hasattr(obj, "run_construction") else obj + + +def evaluate_requirements(requirements, Types): + if isinstance(Types, type): + return next(evaluate_requirements(requirements, [Types])) + return ( + filter( + lambda req: isinstance(req, Type) + or (isinstance(req, DelayedConstruction) and issubclass(req.metadata.Type, Type)), + requirements, + ) + for Type in Types + ) + + +class _Operators(BaseModel): + # More precisely, this returns self.metadata.Type + # but it is kinda hard to express this in type hints + # and I've got more urgent matters to deal with. + constructor: Callable[[Any], Any] = lambda self: self.metadata.Type( + *map(get_as_pypicongpu, self.metadata.args), + **dict(map(lambda kv: (kv[0], get_as_pypicongpu(kv[1])), self.metadata.kwargs.items())), + ) + try_update_with: Callable[[Any, Any], bool] = lambda self, other: False + is_same_as: Callable[[Any, Any], bool] = lambda self, other: isinstance(other, DelayedConstruction) and ( + self.metadata == other.metadata + ) + # This is supposed to raise in case of conflict: + check_for_conflict: Callable[[Any, Any], None] = lambda self, other: None + + +class _Metadata(BaseModel): + Type: type + args: tuple[Any, ...] = tuple() + kwargs: dict[str, Any] = Field(default_factory=dict) + + # This might be necessary to distinguish + # processes with identical (kw)args but different operatos. + misc: dict[str, Any] = Field(default_factory=dict) + + +class DelayedConstruction(BaseModel): + """ + This class models the delayed construction of an object. + + While the user composes their simulation, + the individual components will register requirements with our PICMI species + in the form of what PyPIConGPU objects it needs the PyPIConGPU species to contain. + But any individual registration cannot assume that + the PICMI species has already obtained all knowledge + necessary to construct its PyPIConGPU counterpart, + so the registering object can only express its intent + but not actually perform the construction in most cases. + + This class models such an intent. + The metadata is supposed to contain + a faithful and complete representation of what is constructed. + It should be sufficient to + - perform the construction and + - compare intents (Is this other DelayedConstruction encoding the same action?) + The operators are customisable actions to take under specific circumstances: + - constructor: How to perform the construction. + - update_with: How to merge another object into this one (returns if successful or not) + - can_be_dropped_due_to_uniqueness: How to compare with another object + The constructor is allowed to assume that at the time of its execution + - all information is available and all objects can be constructed + - the handled objects are stateless, + i.e., the process is repeatable and the order of execution doesn't matter. + A custom constructor should obviously follow these principles as well. + The other operators cannot make such assumptions; they operate on mutable snapshots. + + A word of warning: + You've got arbitrary functions for customisation at your disposal. + Use them wisely! + In particular, the construction should perform the obvious and minimal action + required to fulfill the intent declared in the metadata. + """ + + metadata: _Metadata + operators: _Operators = _Operators() + must_be_unique: bool = False + + def run_construction(self): + # This is a member variable not a member function, + # so we gotta hand it the `self` argument explicitly. + return self.operators.constructor(self) + + def try_update_with(self, other): + return self.operators.try_update_with(self, other) + + def can_be_dropped_due_to_uniqueness(self, other): + return self.operators.is_same_as(self, other) + + def check_for_conflict(self, other): + return self.operators.check_for_conflict(self, other) + + def __eq__(self, other): + # We do not check for operator equality here because we can't possibly inspect that. + return ( + isinstance(other, DelayedConstruction) + and (self.metadata == other.metadata) + and (self.must_be_unique == other.must_be_unique) + ) + + +class GroundStateIonizationConstruction(DelayedConstruction): + def __init__(self, /, ionization_model): + def constructor(self): + return GroundStateIonization( + ionization_model_list=[m.get_as_pypicongpu() for m in self.metadata.kwargs["ionization_model_list"]] + ) + + def try_update_with(self, other): + if not isinstance(other, GroundStateIonizationConstruction): + return False + for model in other.metadata.kwargs["ionization_model_list"]: + if model not in self.metadata.kwargs["ionization_model_list"]: + self.metadata.kwargs["ionization_model_list"].append(model) + return True + + operators = {"constructor": constructor, "try_update_with": try_update_with} + metadata = {"Type": GroundStateIonization, "kwargs": {"ionization_model_list": [ionization_model]}} + + return super().__init__(operators=operators, metadata=metadata, must_be_unique=True) + + +class SetChargeStateOperation(DelayedConstruction): + def __init__(self, /, species): + metadata = {"Type": SetChargeState, "kwargs": {"species": species, "charge_state": species.charge_state}} + return super().__init__(metadata=metadata, must_be_unique=True) + + +class SimpleDensityOperation(DelayedConstruction): + def __init__(self, /, species, grid, layout): + def constructor(self): + kwargs = self.metadata.kwargs + return self.metadata.Type( + species=[s.get_as_pypicongpu() for s in sorted(kwargs["species"])], + profile=kwargs["profile"].get_as_pypicongpu(kwargs["grid"]), + layout=kwargs["layout"].get_as_pypicongpu(), + ) + + def try_update_with(self, other): + return ( + isinstance(other, SimpleDensityOperation) + and other.metadata.kwargs["profile"] == self.metadata.kwargs["profile"] + and other.metadata.kwargs["layout"] == self.metadata.kwargs["layout"] + and (self.metadata.kwargs["species"].extend(other.metadata.kwargs["species"]) or True) + ) + + metadata = { + "Type": SimpleDensity, + "kwargs": { + "species": [species], + "profile": species.initial_distribution, + "layout": layout, + "grid": grid, + }, + } + operators = {"constructor": constructor, "try_update_with": try_update_with} + + return super().__init__(metadata=metadata, operators=operators) + + +class SimpleMomentumOperation(DelayedConstruction): + def __init__(self, /, species): + def constructor(self): + species = self.metadata.kwargs["species"].get_as_pypicongpu() + particle_mass_si = species.get_constant_by_type(Mass).mass_si + rms_velocity_si_squared = np.linalg.norm(self.metadata.kwargs["rms_velocity"]) ** 2 + temperature_kev = particle_mass_si * rms_velocity_si_squared / 3 * electron_volt**-1 * 10**-3 + temperature = Temperature(temperature_kev=temperature_kev) if temperature_kev > 0 else None + return SimpleMomentum(species=species, drift=self.metadata.kwargs["drift"], temperature=temperature) + + metadata = { + "Type": SimpleMomentum, + "kwargs": { + "species": species, + "drift": species.initial_distribution.get_picongpu_drift(), + "rms_velocity": species.initial_distribution.picongpu_get_rms_velocity_si(), + }, + } + operators = {"constructor": constructor} + + return super().__init__(metadata=metadata, operators=operators) diff --git a/lib/python/picongpu/pypicongpu/laser.py b/lib/python/picongpu/pypicongpu/laser.py index eca36a8959..a5cfc68fb5 100644 --- a/lib/python/picongpu/pypicongpu/laser.py +++ b/lib/python/picongpu/pypicongpu/laser.py @@ -264,3 +264,6 @@ class TWTSLaser(_BaseLaser): huygens_surface_positions: Annotated[list[list[int]], PlainSerializer(_get_huygens_surface_serialized)] """Position in cells of the Huygens surface relative to start/ edge(negative numbers) of the total domain""" + + +AnyLaser = DispersivePulseLaser | FromOpenPMDPulseLaser | GaussianLaser | PlaneWaveLaser | TWTSLaser diff --git a/lib/python/picongpu/pypicongpu/output/__init__.py b/lib/python/picongpu/pypicongpu/output/__init__.py index 5a7921e90c..7500dd6a68 100644 --- a/lib/python/picongpu/pypicongpu/output/__init__.py +++ b/lib/python/picongpu/pypicongpu/output/__init__.py @@ -1,4 +1,10 @@ -from .auto import Auto +""" +This file is part of PIConGPU. +Copyright 2025 PIConGPU contributors +Authors: Julian Lenz +License: GPLv3+ +""" + from .phase_space import PhaseSpace from .energy_histogram import EnergyHistogram from .macro_particle_count import MacroParticleCount @@ -9,7 +15,6 @@ from .plugin import Plugin __all__ = [ - "Auto", "OpenPMDPlugin", "Plugin", "PhaseSpace", diff --git a/lib/python/picongpu/pypicongpu/output/auto.py b/lib/python/picongpu/pypicongpu/output/auto.py deleted file mode 100644 index 3d750d2cf5..0000000000 --- a/lib/python/picongpu/pypicongpu/output/auto.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2025 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre, Richard Pausch, Julian Lenz -License: GPLv3+ -""" - -from pydantic import BaseModel, PrivateAttr, computed_field -from .timestepspec import TimeStepSpec -from .plugin import Plugin - - -class Auto(Plugin, BaseModel): - """ - Class to provide output **without further configuration**. - - This class requires a period (in time steps) and will enable as many output - plugins as feasable for all species. - Note: The list of species from the initmanager is used during rendering. - - No further configuration is possible! - If you want to provide additional configuration for plugins, - create a separate class. - """ - - period: TimeStepSpec - """period to print data at""" - _name: str = PrivateAttr("auto") - - @computed_field - def png_axis(self) -> list[dict[str, str]]: - return [ - {"axis": "yx"}, - {"axis": "yz"}, - ] diff --git a/lib/python/picongpu/pypicongpu/runner.py b/lib/python/picongpu/pypicongpu/runner.py index 9e105010c0..af720fa087 100644 --- a/lib/python/picongpu/pypicongpu/runner.py +++ b/lib/python/picongpu/pypicongpu/runner.py @@ -5,6 +5,7 @@ License: GPLv3+ """ +from contextlib import contextmanager import datetime import json import logging @@ -25,6 +26,16 @@ DEFAULT_TEMPLATE_DIRECTORY = (Path(__file__).parents[4] / "share" / "picongpu" / "pypicongpu" / "template").absolute() +@contextmanager +def cd(path): + cwd = Path().absolute() + try: + chdir(path) + yield + finally: + chdir(cwd) + + def runArgs(name, args): assert list(filter(lambda x: x is None, args)) == [], "arguments must not be None!" logging.info("running {}...".format(name)) @@ -306,8 +317,8 @@ def __render_templates(self): def __build(self): """launch build of PIConGPU""" - chdir(self.setup_dir) - runArgs("pic-build", ["pic-build", "-j", "4"]) + with cd(self.setup_dir): + runArgs("pic-build", ["pic-build", "-j", "4"]) def __run(self): """ @@ -317,14 +328,16 @@ def __run(self): therefore will not work with any other configuration TODO multi-device support """ - chdir(self.setup_dir) - runArgs( - "PIConGPU", - ( - ("tbg -s bash -c etc/picongpu/N.cfg -t " + environ["PIC_SYSTEM_TEMPLATE_PATH"] + "/mpiexec.tpl").split() - + [self.run_dir] - ), - ) + with cd(self.setup_dir): + runArgs( + "PIConGPU", + ( + ( + "tbg -s bash -c etc/picongpu/N.cfg -t " + environ["PIC_SYSTEM_TEMPLATE_PATH"] + "/mpiexec.tpl" + ).split() + + [self.run_dir] + ), + ) def generate(self, printDirToConsole=False): """ diff --git a/lib/python/picongpu/pypicongpu/simulation.py b/lib/python/picongpu/pypicongpu/simulation.py index bcdc75fda6..754c789a39 100644 --- a/lib/python/picongpu/pypicongpu/simulation.py +++ b/lib/python/picongpu/pypicongpu/simulation.py @@ -12,21 +12,20 @@ import typeguard -from . import output, species, util +from picongpu.pypicongpu.species.operation.operation import Operation +from picongpu.pypicongpu.species.species import Species + +from . import util from .customuserinput import InterfaceCustomUserInput from .field_solver.DefaultSolver import Solver from .grid import Grid3D -from .laser import DispersivePulseLaser, FromOpenPMDPulseLaser, GaussianLaser, PlaneWaveLaser, TWTSLaser +from .laser import AnyLaser from .movingwindow import MovingWindow from .output import Plugin, OpenPMDPlugin -from .output.timestepspec import TimeStepSpec from .rendering import RenderedObject from .walltime import Walltime -AnyLaser = DispersivePulseLaser | FromOpenPMDPulseLaser | GaussianLaser | PlaneWaveLaser | TWTSLaser - - @typeguard.typechecked class Simulation(RenderedObject): """ @@ -56,9 +55,6 @@ class Simulation(RenderedObject): solver = util.build_typesafe_property(Solver) """Used Solver""" - init_manager = util.build_typesafe_property(species.InitManager) - """init manager holding all species & their information""" - typical_ppc = util.build_typesafe_property(int) """ typical number of macro particles spawned per cell, >=1 @@ -82,21 +78,43 @@ class Simulation(RenderedObject): binomial_current_interpolation = util.build_typesafe_property(bool) """switch on a binomial current interpolation""" - plugins = util.build_typesafe_property(typing.Optional[list[Plugin] | typing.Literal["auto"]]) - - def __get_output_context(self) -> dict | list[dict] | None: - """retrieve all output objects""" - - if self.plugins == "auto": - auto = output.Auto(period=TimeStepSpec([slice(0, None, max(1, int(self.time_steps / 100)))])) - - return [auto.get_rendering_context()] - else: - output_rendering_context = [] - for entry in self.plugins: - output_rendering_context.append(entry.get_rendering_context()) - - return output_rendering_context + plugins = util.build_typesafe_property(typing.Optional[list[Plugin]]) + + species = util.build_typesafe_property(list[Species]) + init_operations = util.build_typesafe_property(list[Operation]) + + def __init__( + self, + /, + typical_ppc, + delta_t_si, + custom_user_input, + solver, + grid, + binomial_current_interpolation, + moving_window, + walltime, + species, + init_operations, + time_steps, + laser, + plugins, + base_density, + ): + self.laser = laser + self.time_steps = time_steps + self.moving_window = moving_window + self.walltime = walltime + self.custom_user_input = custom_user_input + self.binomial_current_interpolation = binomial_current_interpolation + self.grid = grid + self.solver = solver + self.delta_t_si = delta_t_si + self.typical_ppc = typical_ppc + self.species = list(species) + self.init_operations = list(init_operations) + self.plugins = plugins + self.base_density = base_density def __render_custom_user_input_list(self) -> dict: custom_rendering_context = {"tags": []} @@ -135,30 +153,17 @@ def _get_serialized(self) -> dict: "typical_ppc": self.typical_ppc, "solver": self.solver.get_rendering_context(), "grid": self.grid.get_rendering_context(), - "species_initmanager": self.init_manager.get_rendering_context(), - "output": self.__get_output_context(), + "output": [entry.get_rendering_context() for entry in (self.plugins or [])], + "species": [s.get_rendering_context() for s in self.species], + "init_operations": [o.get_rendering_context() for o in self.init_operations], + "laser": None if self.laser is None else [ll.get_rendering_context() for ll in self.laser], + "moving_window": None if self.moving_window is None else self.moving_window.get_rendering_context(), + "walltime": (self.walltime or Walltime(walltime=datetime.timedelta(hours=1))).get_rendering_context(), + "binomial_current_interpolation": self.binomial_current_interpolation, + "customuserinput": None if self.custom_user_input is None else self.__render_custom_user_input_list(), } - if self.plugins is not None: - serialized["output"] = self.__get_output_context() - else: - serialized["output"] = None - - if self.laser is not None: - serialized["laser"] = [ll.get_rendering_context() for ll in self.laser] - else: - serialized["laser"] = None - - serialized["moving_window"] = None if self.moving_window is None else self.moving_window.get_rendering_context() - serialized["walltime"] = ( - self.walltime or Walltime(walltime=datetime.timedelta(hours=1)) - ).get_rendering_context() - - serialized["binomial_current_interpolation"] = self.binomial_current_interpolation if self.custom_user_input is not None: - serialized["customuserinput"] = self.__render_custom_user_input_list() self.__found_custom_input(serialized) - else: - serialized["customuserinput"] = None return serialized diff --git a/lib/python/picongpu/pypicongpu/species/__init__.py b/lib/python/picongpu/pypicongpu/species/__init__.py index 8ef0c53376..ddd1dd5140 100644 --- a/lib/python/picongpu/pypicongpu/species/__init__.py +++ b/lib/python/picongpu/pypicongpu/species/__init__.py @@ -12,11 +12,9 @@ from . import constant from .species import Species -from .initmanager import InitManager __all__ = [ "Species", - "InitManager", "attribute", "constant", "operation", diff --git a/lib/python/picongpu/pypicongpu/species/attribute/attribute.py b/lib/python/picongpu/pypicongpu/species/attribute/attribute.py index fde6e1ba94..34b1c533d9 100644 --- a/lib/python/picongpu/pypicongpu/species/attribute/attribute.py +++ b/lib/python/picongpu/pypicongpu/species/attribute/attribute.py @@ -5,8 +5,10 @@ License: GPLv3+ """ +from pydantic import BaseModel -class Attribute: + +class Attribute(BaseModel): """ attribute of a species @@ -24,8 +26,5 @@ class Attribute: PIConGPU term: "particle attributes" """ - PICONGPU_NAME: str = None + picongpu_name: str """C++ Code implementing this attribute""" - - def __init__(self): - raise NotImplementedError() diff --git a/lib/python/picongpu/pypicongpu/species/attribute/boundelectrons.py b/lib/python/picongpu/pypicongpu/species/attribute/boundelectrons.py index 547285eede..f16543ab33 100644 --- a/lib/python/picongpu/pypicongpu/species/attribute/boundelectrons.py +++ b/lib/python/picongpu/pypicongpu/species/attribute/boundelectrons.py @@ -13,7 +13,4 @@ class BoundElectrons(Attribute): Position of a macroparticle """ - PICONGPU_NAME = "boundElectrons" - - def __init__(self): - pass + picongpu_name: str = "boundElectrons" diff --git a/lib/python/picongpu/pypicongpu/species/attribute/momentum.py b/lib/python/picongpu/pypicongpu/species/attribute/momentum.py index 3f14bc3a92..db98c12f67 100644 --- a/lib/python/picongpu/pypicongpu/species/attribute/momentum.py +++ b/lib/python/picongpu/pypicongpu/species/attribute/momentum.py @@ -13,7 +13,4 @@ class Momentum(Attribute): Position of a macroparticle """ - PICONGPU_NAME = "momentum" - - def __init__(self): - pass + picongpu_name: str = "momentum" diff --git a/lib/python/picongpu/pypicongpu/species/attribute/position.py b/lib/python/picongpu/pypicongpu/species/attribute/position.py index d0a7cbad3b..b01d82b4d0 100644 --- a/lib/python/picongpu/pypicongpu/species/attribute/position.py +++ b/lib/python/picongpu/pypicongpu/species/attribute/position.py @@ -13,7 +13,4 @@ class Position(Attribute): Position of a macroparticle """ - PICONGPU_NAME = "position" - - def __init__(self): - pass + picongpu_name: str = "position" diff --git a/lib/python/picongpu/pypicongpu/species/attribute/weighting.py b/lib/python/picongpu/pypicongpu/species/attribute/weighting.py index 78d121bd80..ea9c532be0 100644 --- a/lib/python/picongpu/pypicongpu/species/attribute/weighting.py +++ b/lib/python/picongpu/pypicongpu/species/attribute/weighting.py @@ -13,7 +13,4 @@ class Weighting(Attribute): Position of a macroparticle """ - PICONGPU_NAME = "weighting" - - def __init__(self): - pass + picongpu_name: str = "weighting" diff --git a/lib/python/picongpu/pypicongpu/species/constant/groundstateionization.py b/lib/python/picongpu/pypicongpu/species/constant/groundstateionization.py index c3b06f3d92..6f9114b8b1 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/groundstateionization.py +++ b/lib/python/picongpu/pypicongpu/species/constant/groundstateionization.py @@ -16,15 +16,6 @@ class GroundStateIonization(Constant, pydantic.BaseModel): ionization_model_list: list[IonizationModel] """list of ground state only ionization models to apply for the species""" - def get(self): - return self.ionization_model_list - - def __hash__(self) -> int: - return_hash_value = hash(type(self)) - for model in self.ionization_model_list: - return_hash_value += hash(model) - return return_hash_value - def check(self) -> None: # check that at least one ionization model in list if len(self.ionization_model_list) == 0: @@ -49,40 +40,6 @@ def check(self) -> None: else: type_already_present[group] = True - def get_species_dependencies(self) -> list[type]: - """get all species one of the ionization models in ionization_model_list depends on""" - - total_species_dependencies = [] - for ionization_model in self.ionization_model_list: - species_dependencies = ionization_model.get_species_dependencies() - for species in species_dependencies: - if species not in total_species_dependencies: - total_species_dependencies.append(species) - - return total_species_dependencies - - def get_attribute_dependencies(self) -> list[type]: - """get all attributes one of the ionization models in ionization_model_list depends on""" - total_attribute_dependencies = [] - for ionization_model in self.ionization_model_list: - attribute_dependencies = ionization_model.get_attribute_dependencies() - for attribute in attribute_dependencies: - if attribute not in total_attribute_dependencies: - total_attribute_dependencies.append(attribute) - - return total_attribute_dependencies - - def get_constant_dependencies(self) -> list[type]: - """get all constants one of the ionization models in ionization_model_list depends on""" - total_constant_dependencies = [] - for ionization_model in self.ionization_model_list: - constant_dependencies = ionization_model.get_constant_dependencies() - for constant in constant_dependencies: - if constant not in total_constant_dependencies: - total_constant_dependencies.append(constant) - - return total_constant_dependencies - def _get_serialized(self) -> dict[str, list[dict[str, typing.Any]]]: self.check() diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/ionizationcurrent.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/ionizationcurrent.py index bd153ecd34..191ab021fe 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/ionizationcurrent.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/ionizationcurrent.py @@ -15,11 +15,11 @@ class IonizationCurrent(Constant, pydantic.BaseModel): """base class for all ionization currents models""" - PICONGPU_NAME: str + picongpu_name: str """C++ Code type name of ionizer""" def _get_serialized(self) -> dict: - return {"picongpu_name": self.PICONGPU_NAME} + return {"picongpu_name": self.picongpu_name} def get_generic_rendering_context(self) -> dict: - return IonizationCurrent(PICONGPU_NAME=self.PICONGPU_NAME).get_rendering_context() + return IonizationCurrent(picongpu_name=self.picongpu_name).get_rendering_context() diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/none_.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/none_.py index 349b9309dc..2218b1eb36 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/none_.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/none_.py @@ -12,4 +12,4 @@ @typeguard.typechecked class None_(IonizationCurrent): - PICONGPU_NAME: str = "None" + picongpu_name: str = "None" diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ADKcircularpolarization.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ADKcircularpolarization.py index 244d392d67..d2fd96e98a 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ADKcircularpolarization.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ADKcircularpolarization.py @@ -21,7 +21,7 @@ class ADKCircularPolarization(IonizationModel): high intensity laser fields. """ - PICONGPU_NAME: str = "ADKCircPol" + picongpu_name: str = "ADKCircPol" """C++ Code type name of ionizer""" ionization_current: IonizationCurrent diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ADKlinearpolarization.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ADKlinearpolarization.py index 0644e5564d..349b44cc14 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ADKlinearpolarization.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ADKlinearpolarization.py @@ -21,7 +21,7 @@ class ADKLinearPolarization(IonizationModel): high intensity laser fields. """ - PICONGPU_NAME: str = "ADKLinPol" + picongpu_name: str = "ADKLinPol" """C++ Code type name of ionizer""" ionization_current: IonizationCurrent diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSI.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSI.py index a207c67160..eb7ddcfca5 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSI.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSI.py @@ -24,7 +24,7 @@ class BSI(IonizationModel): `proton number - number of inner shell electrons`, but neglects the Stark upshift of ionization energies. """ - PICONGPU_NAME: str = "BSI" + picongpu_name: str = "BSI" """C++ Code type name of ionizer""" ionization_current: IonizationCurrent diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIeffectiveZ.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIeffectiveZ.py index 3dc86dedaa..fe67649dd6 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIeffectiveZ.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIeffectiveZ.py @@ -19,7 +19,7 @@ class BSIEffectiveZ(IonizationModel): shielding, but still neglecting the Stark upshift of ionization energies. """ - PICONGPU_NAME: str = "BSIEffectiveZ" + picongpu_name: str = "BSIEffectiveZ" """C++ Code type name of ionizer""" ionization_current: IonizationCurrent diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIstarkshifted.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIstarkshifted.py index e561a789a2..4499ce2c57 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIstarkshifted.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIstarkshifted.py @@ -18,7 +18,7 @@ class BSIStarkShifted(IonizationModel): Variant of the BSI ionization model accounting for the Stark upshift of ionization energies. """ - PICONGPU_NAME: str = "BSIStarkShifted" + picongpu_name: str = "BSIStarkShifted" """C++ Code type name of ionizer""" ionization_current: IonizationCurrent diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py index e429138cff..6b88d96c67 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py @@ -6,9 +6,7 @@ """ from ..constant import Constant -from ...attribute import BoundElectrons from ..ionizationcurrent import IonizationCurrent -from ..elementproperties import ElementProperties import pydantic import typing @@ -27,11 +25,11 @@ class IonizationModel(pydantic.BaseModel, Constant): PIConGPU term: "ionizer" """ - PICONGPU_NAME: str + picongpu_name: str """C++ Code type name of ionizer""" # no typecheck here -- would require circular imports - ionization_electron_species: typing.Any = None + ionization_electron_species: typing.Any """species to be used as electrons""" ionization_current: typing.Optional[IonizationCurrent] = None @@ -69,31 +67,21 @@ def _get_serialized(self) -> dict[str, typing.Any]: if self.ionization_current is None: # case no ionization_current configurable return { - "ionizer_picongpu_name": self.PICONGPU_NAME, + "ionizer_picongpu_name": self.picongpu_name, "ionization_electron_species": self.ionization_electron_species.get_rendering_context(), "ionization_current": None, } # default case return { - "ionizer_picongpu_name": self.PICONGPU_NAME, + "ionizer_picongpu_name": self.picongpu_name, "ionization_electron_species": self.ionization_electron_species.get_rendering_context(), "ionization_current": self.ionization_current.get_generic_rendering_context(), } def get_generic_rendering_context(self) -> dict[str, typing.Any]: return IonizationModel( - PICONGPU_NAME=self.PICONGPU_NAME, + picongpu_name=self.picongpu_name, ionization_electron_species=self.ionization_electron_species, ionization_current=self.ionization_current, ).get_rendering_context() - - def get_species_dependencies(self) -> list[typing.Any]: - self.check() - return [self.ionization_electron_species] - - def get_attribute_dependencies(self) -> list[type]: - return [BoundElectrons] - - def get_constant_dependencies(self) -> list[type]: - return [ElementProperties] diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/keldysh.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/keldysh.py index eefc28997e..2fb78b87e8 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/keldysh.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/keldysh.py @@ -22,7 +22,7 @@ class Keldysh(IonizationModel): high intensity laser fields. """ - PICONGPU_NAME: str = "Keldysh" + picongpu_name: str = "Keldysh" """C++ Code type name of ionizer""" ionization_current: IonizationCurrent diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/thomasfermi.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/thomasfermi.py index af103323fe..6d1f532e5d 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/thomasfermi.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/thomasfermi.py @@ -26,5 +26,5 @@ class ThomasFermi(IonizationModel): This is extenden to arbitrary temperatures and atoms through fitting parameters and temperature cutoffs. """ - PICONGPU_NAME: str = "ThomasFermi" + picongpu_name: str = "ThomasFermi" """C++ Code type name of ionizer""" diff --git a/lib/python/picongpu/pypicongpu/species/initmanager.py b/lib/python/picongpu/pypicongpu/species/initmanager.py deleted file mode 100644 index 532c6f10a5..0000000000 --- a/lib/python/picongpu/pypicongpu/species/initmanager.py +++ /dev/null @@ -1,545 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from ..grid import Grid3D -import typeguard -import typing -from .. import util -from .species import Species -from .operation import ( - Operation, - DensityOperation, - SimpleDensity, - SimpleMomentum, - SetChargeState, -) -from .attribute import Attribute -from .constant import Constant -from functools import reduce -from ..rendering import RenderedObject - - -@typeguard.typechecked -class InitManager(RenderedObject): - """ - Helper to manage species translation to PIConGPU - - Collects all species to be initialized and the operations initializing - them. - - Invokes the methods of the Operation lifecycle (check, prebook, bake). - - Workflow: - - 1. Fill InitManager (from outside) with - - - Species and their constants (no attributes!) - - Operations, fully initialized (all params set) - - 2. invoke InitManager.bake(), which - - - checks the Species for conflicts (name...) - - performs dependency checks, possibly reorders species - - invokes the Operation lifecycle (check, prebook, bake) - - sanity-checks the results - - 3. retrieve rendering context - - - organizes operations into lists - - Note: The InitManager manages a lifecycle, it does not perform deep checks - -> most errors have to be caught by delegated checks. - """ - - all_species = util.build_typesafe_property(typing.List[Species]) - """all species to be initialized""" - - all_operations = util.build_typesafe_property(typing.List[Operation]) - """all species to be initialized""" - - __baked = util.build_typesafe_property(bool) - """if bake() has already been called""" - - def __init__(self) -> None: - self.all_species = [] - self.all_operations = [] - self.__baked = False - - def __hash__(self) -> int: - # every simulation may only ever have one InitManager - return hash(type(self)) - - def __get_all_attributes(self): - """ - accumulate *all* attributes currently assigned to any species - - Note: does not filter duplicates - - :return: list of all attributes - """ - return list( - reduce( - lambda list_a, list_b: list_a + list_b, - map(lambda species: species.attributes, self.all_species), - [], - ) - ) - - def __precheck_species_conflicts(self) -> None: - """ - checks for conflicts between species, if ok passes silently - - intended to verify input (i.e. before any operation is performed) - - conflict types: - - - same object twice in self.all_species - - name not unique in self.all_species - """ - # (1) check object uniqueness - duplicate_species = set([species.name for species in self.all_species if self.all_species.count(species) > 1]) - if 0 != len(duplicate_species): - raise ValueError( - "every species object may only be added once, offending: {}".format(", ".join(duplicate_species)) - ) - - # (2) check name conflicts - species_names = [species.name for species in self.all_species] - duplicate_names = set([name for name in species_names if species_names.count(name) > 1]) - if 0 != len(duplicate_names): - raise ValueError("species names must be unique, offending: {}".format(", ".join(duplicate_names))) - - def __precheck_operation_conflicts(self) -> None: - """ - checks for conflicts between operations, ik ok passes silently - - intended to verify input (i.e. before any operation is performed) - - conflict types: - - - same object twice in self.all_operations - """ - duplicate_operations = set( - [operation for operation in self.all_operations if self.all_operations.count(operation) > 1] - ) - if 0 != len(duplicate_operations): - raise ValueError( - "every operation object may only be added once, offending: {}".format( - ", ".join(map(str, duplicate_operations)) - ) - ) - - def __check_operation_phase_left_attributes_untouched(self, phase_name: str, operation: Operation) -> None: - """ - ensures that no attributes have been added to any species - - if ok passes silently - - intended to be run after an operation phase - - parameters are used for error message generation - :param phase_name: name of operation phase for error (prebook/check) - :param operation: operation that is being checked - """ - # use assertion instead of ValueError() - # rationale: assertions check self (be unfriendly), - # ValueError()s user input (be more friendly) - assert 0 == len(self.__get_all_attributes()), "phase {} of operation {} added attributes: {}".format( - phase_name, - str(operation), - ", ".join(map(lambda attr: attr.PICONGPU_NAME, self.__get_all_attributes())), - ) - - def __check_operation_prebook_only_known_species(self, operation: Operation) -> None: - """ - ensure that only registered species are prebooked - - passes silently if ok - - :param operation: checked operation - """ - # ensure only registered species are added - prebooked_species = set(operation.attributes_by_species.keys()) - unknown_species = prebooked_species - set(self.all_species) - if 0 != len(unknown_species): - unknown_species_names = list(map(lambda species: species.name, unknown_species)) - raise ValueError( - "operation {} initialized species, but they are not registered in InitManager.all_species: {}".format( - str(operation), ", ".join(unknown_species_names) - ) - ) - - def __check_attributes_species_exclusive(self) -> None: - """ - ensure attributes are exclusively owned by exactly one species - - if ok passes silently - """ - all_attributes = list( - reduce( - lambda list_a, list_b: list_a + list_b, - map(lambda species: species.attributes, self.all_species), - [], - ) - ) - if len(all_attributes) != len(set(all_attributes)): - raise ValueError("attributes must be exclusively owned by exactly one species") - - def __check_species_dependencies_registered(self) -> None: - """ - check that all dependencies of species are also in self.all_species - - passes silently if ok, else raises - - Note: only parses constants (operations must NOT have inter-species - dependencies) - """ - for species in self.all_species: - for constant in species.constants: - for dependency in constant.get_species_dependencies(): - if dependency not in self.all_species: - raise ReferenceError( - "species {} is dependency (is required by) {}, but unkown to the init manager".format( - dependency.name, species.name - ) - ) - - def __check_species_dependencies_circular(self) -> None: - """ - ensure that there are no circular dependencies - - passes silently if ok, else raises - - assumes that all dependencies are inside of self.all_species; - see __check_species_dependencies_registered() - - Note: only parses constants - """ - # approach: - # 1) build "closure" (all dependencies including transitive - # dependencies) - # 2) check if self is in closure - - for species in self.all_species: - dependency_closure = set() - - # initialize closure with immediate dependencies - for constant in species.constants: - dependency_closure = dependency_closure.union(constant.get_species_dependencies()) - - # compute transitive dependencies - is_closure_final = False - while not is_closure_final: - closure_size_before = len(dependency_closure) - for dependency_species in dependency_closure: - for constant in dependency_species.constants: - dependency_closure = dependency_closure.union(constant.get_species_dependencies()) - closure_size_after = len(dependency_closure) - is_closure_final = closure_size_after == closure_size_before - - # check: self in dependency closure? - if species in dependency_closure: - raise RecursionError( - "species {} is in circular dependency, all dependencies are: {}".format( - species.name, - ", ".join(map(lambda species: species.name, dependency_closure)), - ) - ) - - def __reorder_species_dependencies(self) -> None: - """ - reorder self.all_species to respect dependencies of constants - - Constants may depend on other species to be present, i.e. ionizers may - require an electron species. - - This method reorders self.all_species to ensure that the dependency is - defined first, and the dependent species follows later. - Transitive dependencies are supported. - - performs additional checks: - - 1. all dependencies registered with init manager - 2. no circular dependencies - """ - # check that all dependencies are registered with initmanager - self.__check_species_dependencies_registered() - - # check for circular dependencies - self.__check_species_dependencies_circular() - - # compute correct inclusion order - # approach: - # 1. assign each species a number - # 2. iterate over species, increasing their - # number to 1 + maximum of their dependencies - # 3. order by this number - # (note: the same ordering number is allowed multiple times) - - # initialize each species with its current index - # -> if possible, the order will be preserved - ordernumber_by_species = dict( - map( - lambda species: (species, self.all_species.index(species)), - self.all_species, - ) - ) - assert 0 == len(ordernumber_by_species) or 0 <= min(ordernumber_by_species.values()) - - is_ordering_final = False - while not is_ordering_final: - # stop working, unless a change is introduced - is_ordering_final = True - - for species in ordernumber_by_species: - # accumulate max order number of dependencies - dependencies_max_ordernumber = -1 - for constant in species.constants: - for dependency in constant.get_species_dependencies(): - dependencies_max_ordernumber = max( - dependencies_max_ordernumber, - ordernumber_by_species[dependency], - ) - - # ensure self comes *after* all dependencies - self_ordernumber = ordernumber_by_species[species] - if dependencies_max_ordernumber >= self_ordernumber: - is_ordering_final = False - ordernumber_by_species[species] = 1 + dependencies_max_ordernumber - - # actually reorder species - self.all_species = sorted(self.all_species, key=lambda species: ordernumber_by_species[species]) - - def __check_constant_attribute_dependencies(self) -> None: - """ - ensure that attributes required by constants are present - - Intended to be run after operations sucessfully executed. - - Silently passes if okay, raises on error. - - Constants may require certain attributes to be present - (e.g. a pusher requires a momentum). - After all attributes have been assigned in a final state, these - dependencies can be checked with this method. - """ - for species in self.all_species: - species_attr_names = set(map(lambda attr: attr.PICONGPU_NAME, species.attributes)) - - for constant in species.constants: - required_attrs = constant.get_attribute_dependencies() - - # perform more rigerous typechecks than typeguard - typeguard.check_type(required_attrs, list[type]) - for required_attr in required_attrs: - if not issubclass(required_attr, Attribute): - raise typeguard.TypeCheckError( - "required attribute must be attribute type, got: {}".format(required_attr) - ) - - # actual check: - assert required_attr.PICONGPU_NAME in species_attr_names, ( - "constant {} of species {} requires attribute {} to be present, but it is not".format( - constant, species.name, required_attr - ) - ) - - def __check_constant_constant_dependencies(self): - """ - ensure that constants required by other constants are present - - Passes silently if okay, raises on error. - - Constants may require the existance of other constants (e.g. Ionizers - may require Atomic Numbers). - Notably the value of these constants can't be checked, only that a - constant of the given type is present. - - A constant may **NOT** depend on itself, circular dependencies are - allowed though. - """ - for species in self.all_species: - for constant in species.constants: - required_constants = constant.get_constant_dependencies() - typeguard.check_type(required_constants, list[type]) - - for required_constant in required_constants: - if not issubclass(required_constant, Constant): - raise typeguard.TypeCheckError( - "required constants must be of Constant type, got: {}".format(required_constant) - ) - - # self-references are not allowed - if type(constant) is required_constant: - raise ReferenceError("constants may not depend on themselves") - - # check if constant exists - assert species.has_constant_of_type(required_constant), ( - "species {}: required constant {} not found, (required by constant {})".format( - species.name, required_constant, constant - ) - ) - - def bake(self) -> None: - """ - apply operations to species - """ - assert not self.__baked, "can only bake once" - - # check that constants required by other constants are present - self.__check_constant_constant_dependencies() - - # check & resolve dependency of constants on other species - # Note: "resolve" here means to reorder - self.__reorder_species_dependencies() - - # check species & operation conflicts - self.__precheck_species_conflicts() - self.__precheck_operation_conflicts() - - # prepare species: set attrs to empty list - for species in self.all_species: - species.attributes = [] - - # apply operation: check - for operation in self.all_operations: - operation.check_preconditions() - self.__check_operation_phase_left_attributes_untouched("check", operation) - - # sync across operations: - # all checks must pass before the first prebook is called - - # apply operation: prebook - for operation in self.all_operations: - operation.prebook_species_attributes() - self.__check_operation_phase_left_attributes_untouched("prebook", operation) - self.__check_operation_prebook_only_known_species(operation) - - # sync across operations: - # We now enter the "species writing" phase (bake() modifies species, - # check & prebook do not write to species). - # This ensures that there are no read-write conflicts between adding - # attributes and running checks/prebooking. - # (this is essentially the naive approach to transaction handling) - - # -> this is non-reversable, set baking flag - self.__baked = True - - # apply operation: bake - for operation in self.all_operations: - # implicitly checks attribute type is unique per species - operation.bake_species_attributes() - - # check attribute objects exclusive to species - self.__check_attributes_species_exclusive() - - # check that attributes required by constants are present - self.__check_constant_attribute_dependencies() - - # check species themselves - for species in self.all_species: - species.check() - - def get_typical_particle_per_cell(self) -> int: - """ - get typical number of macro particles per cell(ppc) of simulation - - @returns middle value of ppc-range of all operations, minimum 1 - """ - ppcs = [] - - for operation in self.all_operations: - if isinstance(operation, DensityOperation): - ppcs.append(operation.ppc) - - if len(ppcs) == 0: - return 1 - - max_ppc = max(ppcs) - min_ppc = min(ppcs) - - if max_ppc < 1: - max_ppc = 1 - if min_ppc < 1: - min_ppc = 1 - - return (max_ppc - min_ppc) // 2 + min_ppc - - def get_base_density(self, grid: Grid3D) -> float: - # There's supposed to be some heuristics here along the lines of - # num_grid = ( - # np.reshape([grid.cell_size_x_si, grid.cell_size_y_si, grid.cell_size_z_si], (-1, 1, 1, 1)) - # * np.mgrid[: grid.cell_cnt_x, : grid.cell_cnt_y, : grid.cell_cnt_z] - # ) - # return float( - # np.max( - # np.fromiter( - # ( - # operation.profile(*num_grid) - # for operation in self.all_operations - # if isinstance(operation, SimpleDensity) - # ), - # dtype=float, - # ) - # ) - # ) - return 1.0e25 - - def _get_serialized(self) -> dict: - """ - retrieve representation for rendering - - The initmanager pass mainly *a set of lists* to the templating engine. - This set of lists is well defined an *always the same*, while only - content of the lists varies. - - To enable direct access to specific types of operations all operations - are split into separate lists containing only operations of this type, - e.g. sth along the lines of: - - .. code:: - - { - species: [species1, species2, ...], - operations: { - simple_density: [density1, density2], - momentum: [], - preionization: [ionization1], - } - } - - (Note: This also make schema description much simpler, as there is no - need for a generic "any operation" schema.) - """ - # note: implicitly runs checks - if not self.__baked: - self.bake() - - operation_types_by_name = { - "simple_density": SimpleDensity, - "simple_momentum": SimpleMomentum, - "set_charge_state": SetChargeState, - # note: NotPlaced is not rendered (as it provides no data & does - # nothing anyways) -> it is not in this list - # same as NoBoundElectrons - } - - # note: this will create lists for every name (which is intented), they - # might be empty - operations_context = {} - for op_name, op_type in operation_types_by_name.items(): - operations_context[op_name] = list( - map( - lambda op: op.get_rendering_context(), - filter(lambda op: type(op) is op_type, self.all_operations), - ) - ) - - return { - "species": list(map(lambda species: species.get_rendering_context(), self.all_species)), - "operations": operations_context, - } diff --git a/lib/python/picongpu/pypicongpu/species/operation/__init__.py b/lib/python/picongpu/pypicongpu/species/operation/__init__.py index adc75ade30..7e0aa8f784 100644 --- a/lib/python/picongpu/pypicongpu/species/operation/__init__.py +++ b/lib/python/picongpu/pypicongpu/species/operation/__init__.py @@ -1,9 +1,7 @@ from .operation import Operation from .densityoperation import DensityOperation from .simpledensity import SimpleDensity -from .notplaced import NotPlaced from .simplemomentum import SimpleMomentum -from .noboundelectrons import NoBoundElectrons from .setchargestate import SetChargeState from . import densityprofile @@ -13,9 +11,7 @@ "Operation", "DensityOperation", "SimpleDensity", - "NotPlaced", "SimpleMomentum", - "NoBoundElectrons", "SetChargeState", "densityprofile", "momentum", diff --git a/lib/python/picongpu/pypicongpu/species/operation/noboundelectrons.py b/lib/python/picongpu/pypicongpu/species/operation/noboundelectrons.py deleted file mode 100644 index 6df696b7f6..0000000000 --- a/lib/python/picongpu/pypicongpu/species/operation/noboundelectrons.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from .operation import Operation -from ..species import Species -from ..attribute import BoundElectrons -from ..constant import GroundStateIonization -from ... import util - -import typeguard - - -@typeguard.typechecked -class NoBoundElectrons(Operation): - """ - assigns a BoundElectrons attribute, but leaves it a 0 - - Intended use for fully ionized ions, which do CAN be ionized. - (Fully ionized ions which can NOT be ionized do not require a - BoundElectrons attribute, and therefore no operation to assign it.) - """ - - species = util.build_typesafe_property(Species) - """species which will have BoundElectrons set to 0""" - - def __init__(self): - pass - - def check_preconditions(self) -> None: - assert self.species.has_constant_of_type(GroundStateIonization), "BoundElectrons requires GroundStateIonization" - - def prebook_species_attributes(self) -> None: - self.attributes_by_species = { - self.species: [BoundElectrons()], - } - - def _get_serialized(self) -> dict: - """ - should not be rendered (does nothing) - - Rationale: This only provides an attribute (via - prebook_species_attributes()), but does not do anything with the - generated attribute -> there is no code generated -> nothing to - serialize - """ - raise RuntimeError("NoBoundElectrons operation has no rendering context representation") diff --git a/lib/python/picongpu/pypicongpu/species/operation/notplaced.py b/lib/python/picongpu/pypicongpu/species/operation/notplaced.py deleted file mode 100644 index 07200ee46c..0000000000 --- a/lib/python/picongpu/pypicongpu/species/operation/notplaced.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from .densityoperation import DensityOperation -from ..species import Species -from ..attribute import Position, Weighting -from ... import util - -import typeguard - - -@typeguard.typechecked -class NotPlaced(DensityOperation): - """ - assigns a position attribute, but does not place a species - - Intended for electrons which do not have a profile, but are used in - pre-ionization. - - Provides attributes Position & Weighting. - """ - - species = util.build_typesafe_property(Species) - """species which will not be placed""" - - ppc = 0 - - def __init__(self): - pass - - def check_preconditions(self) -> None: - # retrieve species one to ensure it is set: - self.species - - def prebook_species_attributes(self) -> None: - self.attributes_by_species = { - self.species: [Position(), Weighting()], - } - - def _get_serialized(self) -> dict: - """ - should not be rendered (does nothing) - - Rationale: This only provides an attribute (via - prebook_species_attributes()), but does not do anything with the - generated attribute -> there is no code generated -> nothing to - serialize - """ - raise RuntimeError("NotPlaced operation has no rendering context representation") diff --git a/lib/python/picongpu/pypicongpu/species/operation/operation.py b/lib/python/picongpu/pypicongpu/species/operation/operation.py index e6563bb336..8cb9e4e76e 100644 --- a/lib/python/picongpu/pypicongpu/species/operation/operation.py +++ b/lib/python/picongpu/pypicongpu/species/operation/operation.py @@ -5,7 +5,7 @@ License: GPLv3+ """ -from ...rendering import RenderedObject +from ...rendering import SelfRegisteringRenderedObject from ... import util from ..attribute import Attribute from ..species import Species @@ -16,7 +16,7 @@ @typeguard.typechecked -class Operation(RenderedObject): +class Operation(SelfRegisteringRenderedObject): """ Defines the initialization of a set of attributes across multiple species @@ -167,7 +167,7 @@ def bake_species_attributes(self) -> None: # (2) every object is exclusive to its species # extract all attribute lists, then join them all_attributes = list(reduce(lambda a, b: a + b, self.attributes_by_species.values())) - duplicate_attribute_names = [attr.PICONGPU_NAME for attr in all_attributes if all_attributes.count(attr) > 1] + duplicate_attribute_names = [attr.picongpu_name for attr in all_attributes if all_attributes.count(attr) > 1] if 0 != len(duplicate_attribute_names): raise ValueError( "attribute objects must be exclusive to species, offending: {}".format( @@ -177,7 +177,7 @@ def bake_species_attributes(self) -> None: # (3) each species only gets one attribute of each type (==name) for species, attributes in self.attributes_by_species.items(): - attr_names = list(map(lambda attr: attr.PICONGPU_NAME, attributes)) + attr_names = list(map(lambda attr: attr.picongpu_name, attributes)) duplicate_names = [name for name in attr_names if attr_names.count(name) > 1] if 0 != len(duplicate_names): raise ValueError( @@ -189,8 +189,8 @@ def bake_species_attributes(self) -> None: # part B: check species to be assigned to # is a pre-booked attribute already defined? for species, attributes in self.attributes_by_species.items(): - present_attr_names = list(map(lambda attr: attr.PICONGPU_NAME, species.attributes)) - prebooked_attr_names = list(map(lambda attr: attr.PICONGPU_NAME, attributes)) + present_attr_names = list(map(lambda attr: attr.picongpu_name, species.attributes)) + prebooked_attr_names = list(map(lambda attr: attr.picongpu_name, attributes)) conflicting_attr_names = set(present_attr_names).intersection(prebooked_attr_names) if 0 != len(conflicting_attr_names): raise ValueError( diff --git a/lib/python/picongpu/pypicongpu/species/operation/setchargestate.py b/lib/python/picongpu/pypicongpu/species/operation/setchargestate.py index b79dc2048c..9a420bba45 100644 --- a/lib/python/picongpu/pypicongpu/species/operation/setchargestate.py +++ b/lib/python/picongpu/pypicongpu/species/operation/setchargestate.py @@ -7,8 +7,6 @@ from .operation import Operation from ..species import Species -from ..attribute import BoundElectrons -from ..constant import GroundStateIonization from ... import util import typeguard @@ -28,23 +26,18 @@ class SetChargeState(Operation): charge_state = util.build_typesafe_property(int) """initial ion charge state""" - def __init__(self): - pass + _name = "setchargestate" - def check_preconditions(self) -> None: - assert self.species.has_constant_of_type(GroundStateIonization), "BoundElectrons requires GroundStateIonization" + def __init__(self, species, charge_state): + self.species = species + self.charge_state = charge_state + def check_preconditions(self) -> None: if self.charge_state < 0: raise ValueError("charge state must be > 0") - # may not check for charge_state > Z since Z not known in this context - - def prebook_species_attributes(self) -> None: - self.attributes_by_species = { - self.species: [BoundElectrons()], - } - def _get_serialized(self) -> dict: + self.check_preconditions() return { "species": self.species.get_rendering_context(), "charge_state": self.charge_state, diff --git a/lib/python/picongpu/pypicongpu/species/operation/simpledensity.py b/lib/python/picongpu/pypicongpu/species/operation/simpledensity.py index a7dbfe6919..75376bc12a 100644 --- a/lib/python/picongpu/pypicongpu/species/operation/simpledensity.py +++ b/lib/python/picongpu/pypicongpu/species/operation/simpledensity.py @@ -9,7 +9,6 @@ from .densityoperation import DensityOperation from .densityprofile import DensityProfile from ..species import Species -from ..attribute import Position, Weighting from ..constant import DensityRatio from ... import util @@ -34,9 +33,6 @@ class SimpleDensity(DensityOperation): note that their density ratios will be respected """ - ppc = util.build_typesafe_property(int) - """particles per cell (random layout), >0""" - profile = util.build_typesafe_property(DensityProfile) """density profile to use, describes the actual density""" @@ -45,14 +41,14 @@ class SimpleDensity(DensityOperation): layout = util.build_typesafe_property(Layout) - def __init__(self): - # nothing to do - pass + _name = "simpledensity" - def check_preconditions(self) -> None: - if self.ppc <= 0: - raise ValueError("must use positive number of particles per cell") + def __init__(self, /, species, profile, layout): + self.profile = profile + self.species = species if isinstance(species, set) else set(species) + self.layout = layout + def check_preconditions(self) -> None: if 0 == len(self.species): raise ValueError("must apply to at least one species") @@ -64,13 +60,6 @@ def check_preconditions(self) -> None: if hasattr(self.profile, "check"): self.profile.check() - def prebook_species_attributes(self) -> None: - self.attributes_by_species = {} - - # assign weighting & position to every species - for species in self.species: - self.attributes_by_species[species] = [Position(), Weighting()] - def _get_serialized(self) -> dict: """ get rendering context for C++ generation @@ -122,7 +111,6 @@ def _get_serialized(self) -> dict: placed_species.append(species.get_rendering_context()) return { - "ppc": self.ppc, "layout": self.layout.get_rendering_context(), "profile": self.profile.get_rendering_context(), "placed_species_initial": placed_species[0], diff --git a/lib/python/picongpu/pypicongpu/species/operation/simplemomentum.py b/lib/python/picongpu/pypicongpu/species/operation/simplemomentum.py index 6ce37e4aea..d40503a812 100644 --- a/lib/python/picongpu/pypicongpu/species/operation/simplemomentum.py +++ b/lib/python/picongpu/pypicongpu/species/operation/simplemomentum.py @@ -8,7 +8,6 @@ from .operation import Operation from .momentum import Temperature, Drift from ..species import Species -from ..attribute import Momentum from ... import util import typeguard @@ -38,20 +37,14 @@ class SimpleMomentum(Operation): drift = util.build_typesafe_property(typing.Optional[Drift]) """drift of particles (if any)""" - def __init__(self): - pass + _name = "simplemomentum" - def check_preconditions(self) -> None: - # acces species to make sure it is set -> no required constants - assert self.species is not None - - def prebook_species_attributes(self) -> None: - # always provides attribute -- might not be set (i.e. left at 0) though - self.attributes_by_species = {self.species: [Momentum()]} + def __init__(self, /, species, temperature=None, drift=None): + self.species = species + self.temperature = temperature + self.drift = drift def _get_serialized(self) -> dict: - self.check_preconditions() - context = { "species": self.species.get_rendering_context(), "temperature": None, diff --git a/lib/python/picongpu/pypicongpu/species/species.py b/lib/python/picongpu/pypicongpu/species/species.py index cd9eae1475..c926a7d691 100644 --- a/lib/python/picongpu/pypicongpu/species/species.py +++ b/lib/python/picongpu/pypicongpu/species/species.py @@ -33,6 +33,20 @@ class Shape(Enum): TSC = "TSC" +class Pusher(Enum): + # supported by standard and PIConGPU + Boris = "Boris" + Vay = "Vay" + Higuera = "Higuera-Cary" + Free = "Free" + # not supported by standard + ReducedLandauLifshitz = "ReducedLandauLifshitz" + Acceleration = "Acceleration" + Photon = "Photon" + Probe = "Probe" + Axel = "Axel" + + @typeguard.typechecked class Species(RenderedObject): """ @@ -56,11 +70,20 @@ class Species(RenderedObject): attributes = util.build_typesafe_property(typing.List[Attribute]) """PIConGPU particle attributes""" + pusher = util.build_typesafe_property(Pusher) + name = util.build_typesafe_property(str) """name of the species""" shape = util.build_typesafe_property(Shape) + def __init__(self, /, name, constants=None, attributes=None, shape=None, pusher=None): + self.name = name + self.constants = constants or [] + self.attributes = attributes or [] + self.shape = shape or Shape["TSC"] + self.pusher = pusher or Pusher["Boris"] + def __str__(self) -> str: try: return ( @@ -134,7 +157,7 @@ def check(self) -> None: ) # each attribute (-name) can only be used once - attr_names = list(map(lambda attr: attr.PICONGPU_NAME, self.attributes)) + attr_names = list(map(lambda attr: attr.picongpu_name, self.attributes)) non_unique_attributes = set([c for c in attr_names if attr_names.count(c) > 1]) if 0 != len(non_unique_attributes): raise ValueError( @@ -220,6 +243,7 @@ def _get_serialized(self) -> dict: "name": self.name, "typename": self.get_cxx_typename(), "shape": shape.value, - "attributes": list(map(lambda attr: {"picongpu_name": attr.PICONGPU_NAME}, self.attributes)), + "pusher": self.pusher.value, + "attributes": list(map(lambda attr: {"picongpu_name": attr.picongpu_name}, self.attributes)), "constants": constants_context, } diff --git a/share/picongpu/pypicongpu/examples/laser_wakefield/main.py b/share/picongpu/pypicongpu/examples/laser_wakefield/main.py index c91b58551b..083240402e 100644 --- a/share/picongpu/pypicongpu/examples/laser_wakefield/main.py +++ b/share/picongpu/pypicongpu/examples/laser_wakefield/main.py @@ -109,10 +109,7 @@ raise ValueError("Ions species required for ionization") hydrogen_with_ionization = picmi.Species( - particle_type="H", - name="hydrogen", - charge_state=0, - initial_distribution=gaussianProfile, + particle_type="H", name="hydrogen", charge_state=0, initial_distribution=gaussianProfile ) species_list.append((hydrogen_with_ionization, random_layout)) diff --git a/share/picongpu/pypicongpu/examples/warm_plasma/main.py b/share/picongpu/pypicongpu/examples/warm_plasma/main.py index 1fb522a181..b13336aa6d 100644 --- a/share/picongpu/pypicongpu/examples/warm_plasma/main.py +++ b/share/picongpu/pypicongpu/examples/warm_plasma/main.py @@ -8,7 +8,6 @@ import logging from picongpu import picmi -from picongpu.picmi.diagnostics.timestepspec import TimeStepSpec # set log level: # options (in ascending order) are: DEBUG, INFO, WARNING, ERROR, CRITICAL @@ -50,9 +49,6 @@ solver=solver, ) -sim.add_diagnostic(picmi.diagnostics.Auto(period=TimeStepSpec[::100])) - - layout = picmi.PseudoRandomLayout(n_macroparticles_per_cell=25) sim.add_species(electron, layout) diff --git a/share/picongpu/pypicongpu/schema/output/auto.Auto.json b/share/picongpu/pypicongpu/schema/output/auto.Auto.json deleted file mode 100644 index a48f6d1906..0000000000 --- a/share/picongpu/pypicongpu/schema/output/auto.Auto.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.auto.Auto", - "description": "enable as many output plugins as feasible with only the period specified", - "type": "object", - "unevaluatedProperties": false, - "required": [ - "period", - "png_axis" - ], - "properties": { - "period": {}, - "png_axis": { - "description": "axis pairs (i.e. planes) for which to generate png output", - "type": "array", - "items": { - "type": "object", - "required": [ - "axis" - ], - "unevaluatedProperties": false, - "properties": { - "axis": { - "description": "actually a plane of two axis, but name follows PIConGPU", - "type": "string", - "pattern": "^(x|y|z)(x|y|z)$" - } - } - } - } - } -} diff --git a/share/picongpu/pypicongpu/schema/output/plugin.Plugin.json b/share/picongpu/pypicongpu/schema/output/plugin.Plugin.json index 4bec3fbd5e..1f8e13ca60 100644 --- a/share/picongpu/pypicongpu/schema/output/plugin.Plugin.json +++ b/share/picongpu/pypicongpu/schema/output/plugin.Plugin.json @@ -12,7 +12,6 @@ "description": "Enum-equivalent of selected type. Note that only one entry should be marked as true, and all others as false, but that is not enforced by the schema.", "type": "object", "required": [ - "auto", "phasespace", "energyhistogram", "macroparticlecount", @@ -23,9 +22,6 @@ ], "unevaluatedProperties": false, "properties": { - "auto": { - "type": "boolean" - }, "phasespace": { "type": "boolean" }, @@ -55,9 +51,6 @@ { "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.phase_space.PhaseSpace" }, - { - "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.auto.Auto" - }, { "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.energy_histogram.EnergyHistogram" }, diff --git a/share/picongpu/pypicongpu/schema/simulation.Simulation.json b/share/picongpu/pypicongpu/schema/simulation.Simulation.json index 82404fb9d5..5570ad3c61 100644 --- a/share/picongpu/pypicongpu/schema/simulation.Simulation.json +++ b/share/picongpu/pypicongpu/schema/simulation.Simulation.json @@ -62,9 +62,6 @@ "type": "boolean", "description": "use binomial current interpolation" }, - "species_initmanager": { - "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.initmanager.InitManager" - }, "output": { "description": "configuration of simulation output", "anyOf": [ @@ -89,7 +86,9 @@ "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.customrenderingcontext.CustomRenderingContext" } ] - } + }, + "species": {}, + "init_operations": {} }, "required": [ "delta_t_si", @@ -100,7 +99,8 @@ "laser", "moving_window", "walltime", - "customuserinput" + "customuserinput", + "species" ], "unevaluatedProperties": false } diff --git a/share/picongpu/pypicongpu/schema/species/operation/operation.Operation.json b/share/picongpu/pypicongpu/schema/species/operation/operation.Operation.json new file mode 100644 index 0000000000..8f2a00abb7 --- /dev/null +++ b/share/picongpu/pypicongpu/schema/species/operation/operation.Operation.json @@ -0,0 +1,3 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.operation.operation.Operation" +} diff --git a/share/picongpu/pypicongpu/schema/species/operation/simpledensity.SimpleDensity.json b/share/picongpu/pypicongpu/schema/species/operation/simpledensity.SimpleDensity.json index 849764280f..83555028f6 100644 --- a/share/picongpu/pypicongpu/schema/species/operation/simpledensity.SimpleDensity.json +++ b/share/picongpu/pypicongpu/schema/species/operation/simpledensity.SimpleDensity.json @@ -3,18 +3,12 @@ "type": "object", "unevaluatedProperties": false, "required": [ - "ppc", "layout", "profile", "placed_species_initial", "placed_species_copied" ], "properties": { - "ppc": { - "description": "Number of macro particles per cell (random position).", - "type": "integer", - "exclusiveMinimum": 0 - }, "layout": { "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.operation.layout.layout.Layout" }, diff --git a/share/picongpu/pypicongpu/schema/species/species.Species.json b/share/picongpu/pypicongpu/schema/species/species.Species.json index 867d0a49b8..5ef5a1332b 100644 --- a/share/picongpu/pypicongpu/schema/species/species.Species.json +++ b/share/picongpu/pypicongpu/schema/species/species.Species.json @@ -8,6 +8,7 @@ "typename", "attributes", "shape", + "pusher", "constants" ], "properties": { @@ -26,6 +27,9 @@ "shape": { "type": "string" }, + "pusher": { + "type": "string" + }, "attributes": { "type": "array", "description": "names of attributes of each macro particle of this species", diff --git a/share/picongpu/pypicongpu/template/etc/picongpu/N.cfg.mustache b/share/picongpu/pypicongpu/template/etc/picongpu/N.cfg.mustache index d4272884f3..ed1ce5e397 100644 --- a/share/picongpu/pypicongpu/template/etc/picongpu/N.cfg.mustache +++ b/share/picongpu/pypicongpu/template/etc/picongpu/N.cfg.mustache @@ -75,18 +75,6 @@ TBG_steps="{{{time_steps}}}" TBG_periodic="--periodic {{{x}}} {{{y}}} {{{z}}}" {{/grid.boundary_condition}} -{{#output}} -{{#typeID.auto}} -# only use charge conservation if solver is yee AND using cuda backend -if [[ "Yee" = "{{{solver.name}}}" ]] && [[ "$PIC_BACKEND" =~ ^cuda(:.+)?$ ]] -then - USED_CHARGE_CONSERVATION_FLAGS="--chargeConservation.period {{#data.period.specs}}{{{start}}}:{{{stop}}}:{{{step}}}{{^_last}},{{/_last}}{{/data.period.specs}}" -else - USED_CHARGE_CONSERVATION_FLAGS="" -fi -{{/typeID.auto}} -{{/output}} - {{#moving_window}} TBG_movingWindow="-m" TBG_windowMovePoint="--windowMovePoint {{{move_point}}}" @@ -106,32 +94,6 @@ pypicongpu_output_with_newlines=" {{#output}} {{#data}} - {{#typeID.auto}} - --fields_energy.period {{#period.specs}}{{{start}}}:{{{stop}}}:{{{step}}}{{^_last}},{{/_last}}{{/period.specs}} - --sumcurr.period {{#period.specs}}{{{start}}}:{{{stop}}}:{{{step}}}{{^_last}},{{/_last}}{{/period.specs}} - $USED_CHARGE_CONSERVATION_FLAGS - - {{#species_initmanager.species}} - --{{{name}}}_macroParticlesCount.period {{#period.specs}}{{{start}}}:{{{stop}}}:{{{step}}}{{^_last}},{{/_last}}{{/period.specs}} - - --{{{name}}}_energy.period {{#period.specs}}{{{start}}}:{{{stop}}}:{{{step}}}{{^_last}},{{/_last}}{{/period.specs}} - --{{{name}}}_energy.filter all - - --{{{name}}}_energyHistogram.period {{#period.specs}}{{{start}}}:{{{stop}}}:{{{step}}}{{^_last}},{{/_last}}{{/period.specs}} - --{{{name}}}_energyHistogram.filter all - --{{{name}}}_energyHistogram.binCount 1024 - --{{{name}}}_energyHistogram.minEnergy 0 - --{{{name}}}_energyHistogram.maxEnergy 256000 - - {{#png_axis}} - --{{{name}}}_png.period {{#period.specs}}{{{start}}}:{{{stop}}}:{{{step}}}{{^_last}},{{/_last}}{{/period.specs}} - --{{{name}}}_png.axis {{{axis}}} - --{{{name}}}_png.slicePoint 0.5 - --{{{name}}}_png.folder png_{{{name}}}_{{{axis}}} - {{/png_axis}} - {{/species_initmanager.species}} - {{/typeID.auto}} - {{#typeID.phasespace}} --{{{species.name}}}_phaseSpace.period {{#period.specs}}{{{start}}}:{{{stop}}}:{{{step}}}{{^_last}},{{/_last}}{{/period.specs}} --{{{species.name}}}_phaseSpace.filter all diff --git a/share/picongpu/pypicongpu/template/include/picongpu/param/density.param.mustache b/share/picongpu/pypicongpu/template/include/picongpu/param/density.param.mustache index ef036a39ef..69f795372c 100644 --- a/share/picongpu/pypicongpu/template/include/picongpu/param/density.param.mustache +++ b/share/picongpu/pypicongpu/template/include/picongpu/param/density.param.mustache @@ -28,7 +28,7 @@ namespace picongpu { namespace densityProfiles::pypicongpu { - {{#species_initmanager.operations.simple_density}} + {{#init_operations}}{{#typeID.simpledensity}}{{#data}} // a species has always only exactly one profile, so only one of the following blocks below will be present {{#profile.typeID.gaussian}} @@ -199,6 +199,6 @@ namespace picongpu using init_{{{placed_species_initial.typename}}} = FreeFormulaImpl; {{/profile.typeID.freeformula}} - {{/species_initmanager.operations.simple_density}} +{{/data}}{{/typeID.simpledensity}}{{/init_operations}} } // namespace densityProfiles::pypicongpu } // namespace picongpu diff --git a/share/picongpu/pypicongpu/template/include/picongpu/param/particle.param.mustache b/share/picongpu/pypicongpu/template/include/picongpu/param/particle.param.mustache index da3e46df09..994d46ecea 100644 --- a/share/picongpu/pypicongpu/template/include/picongpu/param/particle.param.mustache +++ b/share/picongpu/pypicongpu/template/include/picongpu/param/particle.param.mustache @@ -44,7 +44,7 @@ namespace picongpu { namespace pypicongpu { - {{#species_initmanager.operations.simple_density}} + {{#init_operations}}{{#typeID.simpledensity}}{{#data}} {{#layout.typeID.random}} struct random_ppc_{{{placed_species_initial.typename}}} { @@ -56,7 +56,7 @@ namespace picongpu * * unit: none */ - static constexpr uint32_t numParticlesPerCell = {{{ppc}}}; + static constexpr uint32_t numParticlesPerCell = {{{layout.data.ppc}}}; }; using init_{{{placed_species_initial.typename}}} = RandomImpl; {{/layout.typeID.random}} @@ -72,7 +72,7 @@ namespace picongpu * * unit: none */ - static constexpr uint32_t numParticlesPerCell = {{{ppc}}}; + static constexpr uint32_t numParticlesPerCell = {{{layout.data.ppc}}}; /** each x, y, z in-cell position component in range [0.0, 1.0) * @@ -86,15 +86,14 @@ namespace picongpu */ using init_{{{placed_species_initial.typename}}} = OnePositionImpl; {{/layout.typeID.one_position}} - - {{/species_initmanager.operations.simple_density}} +{{/data}}{{/typeID.simpledensity}}{{/init_operations}} } // namespace pypicongpu } // namespace startPosition namespace manipulators { namespace pypicongpu { - {{#species_initmanager.operations.simple_momentum}} + {{#init_operations}}{{#typeID.simplemomentum}}{{#data}} {{! note: this is a loop }} {{! -> there can be multiple "simple momentum"s }} @@ -131,12 +130,12 @@ namespace picongpu }; using AddTemperature_{{{species.typename}}} = unary::Temperature; {{/temperature}} - {{/species_initmanager.operations.simple_momentum}} + {{/data}}{{/typeID.simplemomentum}}{{/init_operations}} - {{#species_initmanager.operations.set_charge_state}} + {{#init_operations}}{{#typeID.setchargestate}}{{#data}} //! definition of PreIonized manipulator using PreIonize_{{{species.typename}}} = unary::ChargeState<{{{charge_state}}}u>;; - {{/species_initmanager.operations.set_charge_state}} + {{/data}}{{/typeID.setchargestate}}{{/init_operations}} } // namespace pypicongpu } // namespace manipulators } // namespace particles diff --git a/share/picongpu/pypicongpu/template/include/picongpu/param/speciesDefinition.param.mustache b/share/picongpu/pypicongpu/template/include/picongpu/param/speciesDefinition.param.mustache index 8525d7e110..5221c2b719 100644 --- a/share/picongpu/pypicongpu/template/include/picongpu/param/speciesDefinition.param.mustache +++ b/share/picongpu/pypicongpu/template/include/picongpu/param/speciesDefinition.param.mustache @@ -32,7 +32,7 @@ namespace picongpu { - {{#species_initmanager.species}} + {{#species}} /**************************************** Definition of Species {{{name}}} ****************************************/ @@ -82,7 +82,7 @@ namespace picongpu {{/element_properties}} {{/constants}} - particlePusher, + particlePusher<{{#pusher}}particles::pusher::{{{pusher}}}{{/pusher}}{{^pusher}}UsedParticlePusher{{/pusher}}>, shape<{{#shape}}particles::shapes::{{{shape}}}{{/shape}}{{^shape}}UsedParticleShape{{/shape}}>, interpolation, current>; @@ -100,15 +100,15 @@ namespace picongpu PMACC_CSTRING("{{{name}}}"), ParticleFlags_{{{typename}}}, ParticleAttributes_{{{typename}}}>; - {{/species_initmanager.species}} + {{/species}} using VectorAllSpecies = MakeSeq_t< - {{#species_initmanager.species}} + {{#species}} {{{typename}}} {{^_last}} , {{/_last}} - {{/species_initmanager.species}} + {{/species}} >; } // namespace picongpu diff --git a/share/picongpu/pypicongpu/template/include/picongpu/param/speciesInitialization.param.mustache b/share/picongpu/pypicongpu/template/include/picongpu/param/speciesInitialization.param.mustache index f3ffd58b16..ce5e1d2355 100644 --- a/share/picongpu/pypicongpu/template/include/picongpu/param/speciesInitialization.param.mustache +++ b/share/picongpu/pypicongpu/template/include/picongpu/param/speciesInitialization.param.mustache @@ -34,7 +34,6 @@ namespace picongpu * the functors are called in order (from first to last functor) */ {{! note: this serves to enter the context -- it is NOT a loop }} - {{#species_initmanager}} using InitPipeline = pmacc::mp_list< /* note to the editors of this file: * @@ -47,8 +46,7 @@ namespace picongpu /**********************************/ /* phase 1: create macroparticles */ /**********************************/ - - {{#operations.simple_density}} +{{#init_operations}}{{#typeID.simpledensity}}{{#data}} /// @details This functor assumes the ratio to be 1. The correct density ratio of the species is applied /// here (automatically by CreateDensity). @@ -63,14 +61,13 @@ namespace picongpu {{{placed_species_initial.typename}}}, {{{typename}}}>, {{/placed_species_copied}} - - {{/operations.simple_density}} +{{/data}}{{/typeID.simpledensity}}{{/init_operations}} /*********************************************/ /* phase 2: adjust other attributes (if any) */ /*********************************************/ - {{#operations.simple_momentum}} +{{#init_operations}}{{#typeID.simplemomentum}}{{#data}} {{! simple momentum adds a temperature & drift without inter-species dependencies }} {{#drift}} // *sets* drift, i.e. overwrites momentum @@ -80,15 +77,14 @@ namespace picongpu // *adds* temperature, does *NOT* overwrite Manipulate, {{/temperature}} - {{/operations.simple_momentum}} +{{/data}}{{/typeID.simplemomentum}}{{/init_operations}} - {{#operations.set_charge_state}} +{{#init_operations}}{{#typeID.setchargestate}}{{#data}} Manipulate, - {{/operations.set_charge_state}} +{{/data}}{{/typeID.setchargestate}}{{/init_operations}} // does nothing -- exists to catch trailing comma left by code generation pypicongpu::nop>; - {{/species_initmanager}} } // namespace particles } // namespace picongpu diff --git a/test/python/picongpu/compiling/simulation.py b/test/python/picongpu/compiling/simulation.py index a20b45602e..a4a440f9f9 100644 --- a/test/python/picongpu/compiling/simulation.py +++ b/test/python/picongpu/compiling/simulation.py @@ -41,32 +41,33 @@ def _set_up_sim(self, **kw): return picmi.Simulation(time_step_size=1.39e-16, max_steps=int(2048), solver=solver, **kw) def _set_up_minimal_sim(self, steps=1): - sim = pypicongpu.Simulation() - sim.delta_t_si = 1.39e-16 - sim.time_steps = steps - sim.typical_ppc = 1 - sim.grid = pypicongpu.grid.Grid3D( - cell_size_si=(1.776e-07, 4.43e-08, 1.776e-07), - cell_cnt=(1, 1, 1), - n_gpus=(1, 1, 1), - boundary_condition=( - BoundaryCondition.PERIODIC, - BoundaryCondition.PERIODIC, - BoundaryCondition.PERIODIC, + return pypicongpu.Simulation( + delta_t_si=1.39e-16, + time_steps=steps, + typical_ppc=1, + grid=pypicongpu.grid.Grid3D( + cell_size_si=(1.776e-07, 4.43e-08, 1.776e-07), + cell_cnt=(1, 1, 1), + n_gpus=(1, 1, 1), + boundary_condition=( + BoundaryCondition.PERIODIC, + BoundaryCondition.PERIODIC, + BoundaryCondition.PERIODIC, + ), + super_cell_size=(8, 8, 4), + grid_dist=None, ), - super_cell_size=(8, 8, 4), - grid_dist=None, + laser=None, + custom_user_input=None, + moving_window=None, + walltime=None, + binomial_current_interpolation=False, + solver=pypicongpu.field_solver.Yee.YeeSolver(), + species=[], + init_operations=[], + plugins=[], + base_density=1.0e25, ) - sim.laser = None - sim.custom_user_input = None - sim.moving_window = None - sim.walltime = None - sim.binomial_current_interpolation = False - sim.solver = pypicongpu.field_solver.Yee.YeeSolver() - sim.plugins = "auto" - sim.init_manager = pypicongpu.species.InitManager() - sim.base_density = 1.0e25 - return sim def test_minimal(self): """smallest possible example""" diff --git a/test/python/picongpu/end-to-end/diagnostics.py b/test/python/picongpu/end-to-end/diagnostics.py index f18e8496c1..c1342d161a 100644 --- a/test/python/picongpu/end-to-end/diagnostics.py +++ b/test/python/picongpu/end-to-end/diagnostics.py @@ -21,7 +21,7 @@ ElectromagneticSolver, GriddedLayout, Simulation, - Species, + Species as Species, ) from picongpu.picmi.diagnostics import ( Checkpoint, diff --git a/test/python/picongpu/quick/__init__.py b/test/python/picongpu/quick/__init__.py index a8871df575..239cf1ba62 100644 --- a/test/python/picongpu/quick/__init__.py +++ b/test/python/picongpu/quick/__init__.py @@ -1,6 +1,6 @@ -from . import picmi, pypicongpu +from . import picmi def load_tests(loader, standard_tests, pattern): - standard_tests.addTests((loader.loadTestsFromModule(module, pattern=pattern) for module in (picmi, pypicongpu))) + standard_tests.addTests((loader.loadTestsFromModule(module, pattern=pattern) for module in (picmi,))) return standard_tests diff --git a/test/python/picongpu/quick/picmi/__init__.py b/test/python/picongpu/quick/picmi/__init__.py index 2354f9f719..cb8b70cf25 100644 --- a/test/python/picongpu/quick/picmi/__init__.py +++ b/test/python/picongpu/quick/picmi/__init__.py @@ -1,6 +1,5 @@ # flake8: noqa from .simulation import * # pyflakes.ignore -from .species import * # pyflakes.ignore from .layout import * # pyflakes.ignore from .distribution import * # pyflakes.ignore from .gaussian_laser import * # pyflakes.ignore @@ -8,3 +7,4 @@ from .diagnostics import * # pyflakes.ignore from .normalise_template_dir import * # pyflakes.ignore from .copy_attributes import * # pyflakes.ignore +from .species import * # pyflakes.ignore diff --git a/test/python/picongpu/quick/picmi/distribution.py b/test/python/picongpu/quick/picmi/distribution.py index dbc56b1d88..4080eedefa 100644 --- a/test/python/picongpu/quick/picmi/distribution.py +++ b/test/python/picongpu/quick/picmi/distribution.py @@ -5,7 +5,6 @@ License: GPLv3+ """ -import math import unittest import typeguard @@ -49,30 +48,6 @@ def _get_distribution(self, lower_bound, upper_bound): """ raise NotImplementedError("must be implemented in child classes") - @unittest.skip("not implemented") - def test_boundary_not_given_at_all(self): - """no boundaries supplied at all""" - picmi_dist = self._get_distribution([None, None, None], [None, None, None]) - pypic = picmi_dist.get_as_pypicongpu(ARBITRARY_GRID) - self.assertEqual((math.inf, math.inf, math.inf), pypic.upper_bound) - self.assertEqual((-math.inf, -math.inf, -math.inf), pypic.lower_bound) - - @unittest.skip("not implemented") - def test_boundary_not_given_partial(self): - """only some boundaries (components) are missing""" - picmi_dist = self._get_distribution(lower_bound=[123, -569, None], upper_bound=[124, None, 17]) - pypic = picmi_dist.get_as_pypicongpu(ARBITRARY_GRID) - self.assertEqual((123, -569, -math.inf), pypic.lower_bound) - self.assertEqual((124, math.inf, 17), pypic.upper_bound) - - @unittest.skip("not implemented") - def test_boundary_passthru(self): - picmi_dist = self._get_distribution(lower_bound=[111, 222, 333], upper_bound=[444, 555, 666]) - pypic = picmi_dist.get_as_pypicongpu(ARBITRARY_GRID) - self.assertTrue(isinstance(pypic, species.attributes.position.profile.Profile)) - self.assertEqual((111, 222, 333), pypic.lower_bound) - self.assertEqual((444, 555, 666), pypic.upper_bound) - class TestPicmiUniformDistribution(unittest.TestCase, HelperTestPicmiBoundaries): def _get_distribution(self, lower_bound, upper_bound): @@ -122,68 +97,6 @@ def test_drift(self): self.assertAlmostEqual(drift.direction_normalized[2], 0.004318114799291135) -@unittest.skip("not implemented") -@typeguard.typechecked -class TestPicmiGaussianBunchDistribution(unittest.TestCase): - def test_full(self): - """check for all possible params""" - gb = picmi.GaussianBunchDistribution(1337, 0.05, centroid_position=[111, 222, 333]) - pypic = gb.get_as_pypicongpu(ARBITRARY_GRID) - self.assertTrue(isinstance(pypic, species.attributes.position.profile.GaussianCloud)) - self.assertEqual((111, 222, 333), pypic.centroid_position_si) - self.assertAlmostEqual(0.05, pypic.rms_bunch_size_si) - self.assertAlmostEqual(679127.9299526414, pypic.max_density_si) - - def test_defaults(self): - """mandatory params are enforced""" - gb = picmi.GaussianBunchDistribution(1, 1) - pypic = gb.get_as_pypicongpu(ARBITRARY_GRID) - self.assertEqual((0, 0, 0), pypic.centroid_position_si) - - def test_conversion(self): - """params are correctly transformed""" - max_density_by_n_particles_and_rms_bunch_size = { - (1337, 0.05): 679127.9299526414, - (1, 1): 0.06349363593424097, - (10, 1): 0.6349363593424097, - (1968.7012432153024, 5): 1, - } - - for param_tuple in max_density_by_n_particles_and_rms_bunch_size: - n_particles, rms_bunch_size = param_tuple - gb = picmi.GaussianBunchDistribution(n_particles, rms_bunch_size) - pypic = gb.get_as_pypicongpu(ARBITRARY_GRID) - self.assertAlmostEqual( - pypic.max_density_si, - max_density_by_n_particles_and_rms_bunch_size[param_tuple], - ) - - def test_drift(self): - """drift is correctly translated""" - # no drift - gb = picmi.GaussianBunchDistribution(1, 1, centroid_velocity=[0, 0, 0]) - drift = gb.get_picongpu_drift() - self.assertEqual(None, drift) - - # some drift - # uses velocity * gamma as input - gb = picmi.GaussianBunchDistribution( - 1, - 1, - centroid_velocity=[ - 17694711.860033844, - 1666140.8815973825, - 63366940.8605043, - ], - ) - drift = gb.get_picongpu_drift() - self.assertNotEqual(None, drift) - self.assertAlmostEqual(drift.gamma, 1.0238123040019211) - self.assertAlmostEqual(drift.direction_normalized[0], 0.2688666691231957) - self.assertAlmostEqual(drift.direction_normalized[1], 0.025316589084272104) - self.assertAlmostEqual(drift.direction_normalized[2], 0.9628446315744488) - - class TestPicmiFoilDistribution(unittest.TestCase, HelperTestPicmiBoundaries): def _get_distribution(self, lower_bound, upper_bound): return picmi.FoilDistribution( diff --git a/test/python/picongpu/quick/picmi/simulation.py b/test/python/picongpu/quick/picmi/simulation.py index 528c015701..c8852219ca 100644 --- a/test/python/picongpu/quick/picmi/simulation.py +++ b/test/python/picongpu/quick/picmi/simulation.py @@ -134,7 +134,7 @@ def test_species_translation(self): layout4 = picmi.PseudoRandomLayout(n_macroparticles_per_cell=4) # species list empty by default - self.assertEqual([], sim.get_as_pypicongpu().init_manager.all_species) + self.assertEqual([], sim.get_as_pypicongpu().species) # not placed sim.add_species(picmi.Species(name="dummy1", mass=5), None) @@ -146,12 +146,12 @@ def test_species_translation(self): sim.add_species(picmi.Species(name="dummy3", mass=3, initial_distribution=profile), layout4) picongpu = sim.get_as_pypicongpu() - self.assertEqual(3, len(picongpu.init_manager.all_species)) - species_names = set(map(lambda species: species.name, picongpu.init_manager.all_species)) + self.assertEqual(3, len(picongpu.species)) + species_names = set(map(lambda species: species.name, picongpu.species)) self.assertEqual({"dummy1", "dummy2", "dummy3"}, species_names) # check typical ppc is derived - self.assertEqual(picongpu.typical_ppc, 2) + self.assertEqual(picongpu.typical_ppc, 3) def test_explicit_typical_ppc(self): grid = get_grid(1, 1, 1, 64) @@ -170,8 +170,8 @@ def test_explicit_typical_ppc(self): sim.add_species(picmi.Species(name="dummy3", mass=3, charge=4, initial_distribution=profile), layout4) picongpu = sim.get_as_pypicongpu() - self.assertEqual(2, len(picongpu.init_manager.all_species)) - species_names = set(map(lambda species: species.name, picongpu.init_manager.all_species)) + self.assertEqual(2, len(picongpu.species)) + species_names = set(map(lambda species: species.name, picongpu.species)) self.assertEqual({"dummy2", "dummy3"}, species_names) # check explicitly set typical ppc is respected @@ -183,16 +183,15 @@ def test_wrong_explicitly_set_typical_ppc(self): wrongValues = [0, -1, -15] for value in wrongValues: - sim = picmi.Simulation(time_step_size=17, max_steps=4, solver=solver, picongpu_typical_ppc=value) - with self.assertRaisesRegex(ValueError, "typical_ppc must be >= 1"): - sim.get_as_pypicongpu() + with self.assertRaisesRegex(ValueError, "Typical ppc should be > 0"): + picmi.Simulation(time_step_size=17, max_steps=4, solver=solver, picongpu_typical_ppc=value) wrongTypes = [0.0, -1.0, -15.0, 1.0, 15.0] for value in wrongTypes: with self.assertRaisesRegex( typeguard.TypeCheckError, '"picongpu_typical_ppc" .* did not match any element in the union' ): - sim = picmi.Simulation(time_step_size=17, max_steps=4, solver=solver, picongpu_typical_ppc=value) + picmi.Simulation(time_step_size=17, max_steps=4, solver=solver, picongpu_typical_ppc=value) def test_invalid_placement(self): profile = picmi.UniformDistribution(density=42) @@ -241,20 +240,21 @@ def test_operations_simple_density_translated(self): ) pypic = self.sim.get_as_pypicongpu() - initmgr = pypic.init_manager + my_species = pypic.species + operations = pypic.init_operations # species - self.assertEqual(4, len(initmgr.all_species)) + self.assertEqual(4, len(my_species)) self.assertEqual( ["colocated1", "colocated2", "separate1", "separate2"], - list(map(lambda species: species.name, initmgr.all_species)), + list(map(lambda species: species.name, my_species)), ) # operations density_operations = list( filter( lambda op: isinstance(op, species.operation.SimpleDensity), - initmgr.all_operations, + operations, ) ) self.assertEqual(3, len(density_operations)) @@ -285,10 +285,10 @@ def test_operations_simple_density_translated(self): # check layout if "separate1" in species_names or "colocated1" in species_names: # used "layout" - self.assertEqual(3, op.ppc) + self.assertEqual(3, op.layout.ppc) else: # used "other_layout" - self.assertEqual(4, op.ppc) + self.assertEqual(4, op.layout.ppc) def test_operation_not_placed_translated(self): """non-placed species are correctly translated""" @@ -296,19 +296,9 @@ def test_operation_not_placed_translated(self): pypicongpu = self.sim.get_as_pypicongpu() - self.assertEqual(1, len(pypicongpu.init_manager.all_species)) + self.assertEqual(1, len(pypicongpu.species)) # not placed, momentum (both initialize to empty) - self.assertEqual(2, len(pypicongpu.init_manager.all_operations)) - - notplaced_ops = list( - filter( - lambda op: isinstance(op, species.operation.NotPlaced), - pypicongpu.init_manager.all_operations, - ) - ) - - self.assertEqual(1, len(notplaced_ops)) - self.assertEqual("notplaced", notplaced_ops[0].species.name) + self.assertEqual(0, len(pypicongpu.init_operations)) def test_operation_momentum(self): """operation for momentum correctly derived from species""" @@ -330,7 +320,7 @@ def test_operation_momentum(self): mom_ops = list( filter( lambda op: isinstance(op, species.operation.SimpleMomentum), - pypicongpu.init_manager.all_operations, + pypicongpu.init_operations, ) ) @@ -393,12 +383,12 @@ def test_add_ionization_model(self): sim.picongpu_interaction = interaction pypic_sim = sim.get_as_pypicongpu() - initmgr = pypic_sim.init_manager + operations = pypic_sim.init_operations - operation_types = list(map(lambda op: type(op), initmgr.all_operations)) + operation_types = list(map(lambda op: type(op), operations)) self.assertEqual(2, operation_types.count(species.operation.SetChargeState)) - for op in initmgr.all_operations: + for op in operations: if isinstance(op, species.operation.SetChargeState) and op.species.name == "Nitrogen": self.assertEqual(5, op.bound_electrons) if isinstance(op, species.operation.SetChargeState) and op.species.name == "Hydrogen": diff --git a/test/python/picongpu/quick/picmi/species.py b/test/python/picongpu/quick/picmi/species.py index 45618e3b9e..3429415d69 100644 --- a/test/python/picongpu/quick/picmi/species.py +++ b/test/python/picongpu/quick/picmi/species.py @@ -1,381 +1,86 @@ """ This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre +Copyright 2025 PIConGPU contributors +Authors: Julian Lenz License: GPLv3+ """ -from picongpu import picmi - -import unittest -import typeguard - -from picongpu.pypicongpu import species -from picongpu.picmi.interaction import Interaction -from picongpu.picmi.interaction.ionization.fieldionization import ADK, ADKVariant -from copy import deepcopy -import re -import logging - - -class TestPicmiSpecies(unittest.TestCase): - def setUp(self): - self.profile_uniform = picmi.UniformDistribution(density=42, rms_velocity=[1, 1, 1]) - - self.species_electron = picmi.Species( - name="e", - density_scale=3, - particle_type="electron", - initial_distribution=self.profile_uniform, - ) - self.species_nitrogen = picmi.Species( - name="nitrogen", - charge_state=+3, - particle_type="N", - picongpu_fixed_charge=True, - initial_distribution=self.profile_uniform, - ) - - def __helper_get_distributions_with_rms_velocity(self): - """ - helper to get a list of (all) profiles (PICMI distributions) that have - an rms_velocity attribute. - - intended to run tests against this temperature - """ - return [ - # TODO add profiles after implementation - # picmi.GaussianBunchDistribution(4e10, 4e-15), - picmi.UniformDistribution(8e24), - # picmi.AnalyticDistribution("x+y+z"), +from unittest import TestCase, main + +from picongpu.picmi.species import Species +from picongpu.picmi.species_requirements import ( + run_construction, + RequirementConflict, + SetChargeStateOperation, +) +from picongpu.pypicongpu.species.operation.setchargestate import SetChargeState +from picongpu.pypicongpu.species.constant.groundstateionization import GroundStateIonization +from picongpu.picmi.interaction.ionization.fieldionization import ADK, BSI +from picongpu.pypicongpu.species.attribute.weighting import Weighting +from picongpu.pypicongpu.species.constant.mass import Mass + + +def unique_in(elements, collection): + collection = list(collection) + return (collection.count(e) == 1 for e in elements) + + +class TestSpeciesRequirementResolution(TestCase): + def test_deduplicate_attributes(self): + species = Species(name="dummy") + requirements = [Weighting()] + species.register_requirements(2 * requirements) + assert all(unique_in(requirements, species.get_as_pypicongpu().attributes)) + + def test_deduplicate_constants(self): + species = Species(name="dummy") + requirements = [Mass(mass_si=1.0)] + species.register_requirements(2 * requirements) + assert all(unique_in(requirements, species.get_as_pypicongpu().constants)) + + def test_deduplicate_delayed_construction(self): + species = Species(name="dummy", particle_type="H", charge_state=1) + requirements = [SetChargeStateOperation(species)] + species.register_requirements(2 * requirements) + assert all(unique_in(requirements, species.get_operation_requirements())) + + def test_conflicting_constants(self): + species = Species(name="dummy") + requirements = [Mass(mass_si=1.0), Mass(mass_si=2.0)] + with self.assertRaises(RequirementConflict): + # Not yet decided which one should raise, but one of them definitely will. + species.register_requirements(requirements) + species.get_as_pypicongpu() + + def test_ionization(self): + ion = Species(name="ion", particle_type="H", charge_state=1) + electron = Species(name="electron", particle_type="electron") + # These all register requirements: + ionizations = [ + # Not great: Production code would use the enums not their integer represenation. + ADK(ion_species=ion, ionization_electron_species=electron, ADK_variant=0, ionization_current=None), + ADK(ion_species=ion, ionization_electron_species=electron, ADK_variant=1, ionization_current=None), + BSI(ion_species=ion, ionization_electron_species=electron, BSI_extensions=[0], ionization_current=None), + BSI(ion_species=ion, ionization_electron_species=electron, BSI_extensions=[1], ionization_current=None), ] - def test_basic(self): - """check that all params are translated""" - # check that translation works - for s in [self.species_electron, self.species_nitrogen]: - pypic, rest = s.get_as_pypicongpu(None) - del rest - self.assertEqual(pypic.name, s.name) - - def test_mandatory(self): - """mandatory params are enforced with a somewhat reasonable message""" - # required: name, particle type - species_no_name = picmi.Species(particle_type="N") - species_empty = picmi.Species() - species_invalid_list = [species_no_name, species_empty] - - for invalid_species in species_invalid_list: - with self.assertRaises(AssertionError): - invalid_species.get_as_pypicongpu(None) - - # (everything else is optional) - - def test_mass_charge(self): - """mass & charge are passed through""" - picmi_s = picmi.Species(name="any", mass=17, charge=-4) - pypicongpu_s, rest = picmi_s.get_as_pypicongpu(None) - - mass_const = pypicongpu_s.get_constant_by_type(species.constant.Mass) - self.assertEqual(17, mass_const.mass_si) - - charge_const = pypicongpu_s.get_constant_by_type(species.constant.Charge) - self.assertEqual(-4, charge_const.charge_si) - - def test_density_scale(self): - """density scale is correctly transformed""" - # simple example - picmi_s = picmi.Species(name="any", density_scale=37.2) - pypicongpu_s, rest = picmi_s.get_as_pypicongpu(None) - - ratio_const = pypicongpu_s.get_constant_by_type(species.constant.DensityRatio) - self.assertAlmostEqual(37.2, ratio_const.ratio) - - # no density scale - picmi_s = picmi.Species(name="any") - pypicongpu_s, rest = picmi_s.get_as_pypicongpu(None) - self.assertTrue(not pypicongpu_s.has_constant_of_type(species.constant.DensityRatio)) - - def test_get_independent_operations(self): - """operations which can be set without external dependencies work""" - picmi_s = picmi.Species(name="any", mass=1, charge=2) - pypicongpu_s, rest = picmi_s.get_as_pypicongpu(None) - - # note: placement is not considered independent (it depends on also - # having no layout) - self.assertNotEqual(None, picmi_s.get_independent_operations(pypicongpu_s, None)) - - def test_get_independent_operations_type(self): - """arg type is checked""" - picmi_s = picmi.Species(name="any", mass=1, charge=2) - for invalid_species in [[], None, picmi_s, "name"]: - with self.assertRaises(typeguard.TypeCheckError): - picmi_s.get_independent_operations(invalid_species, None) - - def test_get_independent_operations_different_name(self): - """only generate operations for pypicongpu species of same name""" - picmi_s = picmi.Species(name="any", mass=1, charge=2) - pypicongpu_s, rest = picmi_s.get_as_pypicongpu(None) - - pypicongpu_s.name = "different" - with self.assertRaisesRegex(AssertionError, ".*name.*"): - picmi_s.get_independent_operations(pypicongpu_s, None) - - # same name is okay: - pypicongpu_s.name = "any" - self.assertNotEqual(None, picmi_s.get_independent_operations(pypicongpu_s, None)) - - def test_get_independent_operations_ionization_set_charge_state(self): - """SetBoundElectrons is properly generated""" - picmi_species = picmi.Species(name="nitrogen", particle_type="N", charge_state=2) - e = picmi.Species(name="e", particle_type="electron") - interaction = Interaction( - ground_state_ionization_model_list=[ - ADK( - ion_species=picmi_species, - ionization_current=None, - ionization_electron_species=e, - ADK_variant=ADKVariant.LinearPolarization, - ) - ] - ) - - pypic_species, rest = picmi_species.get_as_pypicongpu(interaction) - ops = picmi_species.get_independent_operations(pypic_species, interaction) - ops_types = list(map(lambda op: type(op), ops)) - self.assertEqual(1, ops_types.count(species.operation.SetChargeState)) - self.assertEqual(0, ops_types.count(species.operation.NoBoundElectrons)) - - for op in ops: - if not isinstance(op, species.operation.SetChargeState): - continue - - self.assertEqual(pypic_species, op.species) - self.assertEqual(2, op.charge_state) - - def test_get_independent_operations_ionization_not_ionizable(self): - """ionization operation is not returned if there is no ionization""" - picmi_species = picmi.Species(name="hydrogen", particle_type="H", picongpu_fixed_charge=True) - pypic_species, rest = picmi_species.get_as_pypicongpu(None) - - ops = picmi_species.get_independent_operations(pypic_species, None) - ops_types = list(map(lambda op: type(op), ops)) - self.assertEqual(0, ops_types.count(species.operation.NoBoundElectrons)) - self.assertEqual(0, ops_types.count(species.operation.SetChargeState)) - - def test_get_independent_operations_momentum(self): - """momentum is correctly translated""" - for set_drift in [False, True]: - for set_temperature in [False, True]: - for dist in self.__helper_get_distributions_with_rms_velocity(): - if set_temperature: - dist.rms_velocity = 3 * [42] - - if set_drift: - # note: same velocity, different representations - if isinstance(dist, picmi.UniformDistribution) or isinstance(dist, picmi.AnalyticDistribution): - # v (as is) - dist.directed_velocity = [41363723.0, 8212468.0, 68174325.0] - elif isinstance(dist, picmi.GaussianBunchDistribution): - # v * gamma - dist.centroid_velocity = [ - 42926825.65008125, - 8522810.724577945, - 70750579.27176853, - ] - else: - # fail: unkown distribution type - assert False, "unkown distribution type in test: {}".format(type(dist)) - - picmi_s = picmi.Species(name="name", mass=1, initial_distribution=dist) - - pypicongpu_s, rest = picmi_s.get_as_pypicongpu(None) - ops = picmi_s.get_independent_operations(pypicongpu_s, None) + # Ionization makes the ion depend on the electron species. + # This is important for rendering the corresponding C++ header, + # so the electron species gets defined before the ion species. + assert electron < ion - momentum_ops = list( - filter( - lambda op: isinstance(op, species.operation.SimpleMomentum), - ops, - ) - ) + set_charge_state_op = [ + run_construction(op) for op in ion.get_operation_requirements() if op.metadata.Type == SetChargeState + ][0] + assert set_charge_state_op.charge_state == ion.charge_state - self.assertEqual(1, len(momentum_ops)) - # must pass silently - momentum_ops[0].check_preconditions() - self.assertEqual(pypicongpu_s, momentum_ops[0].species) - - if set_drift: - self.assertEqual( - momentum_ops[0].drift.direction_normalized, - ( - 0.5159938229615939, - 0.10244684114313779, - 0.8504440130927325, - ), - ) - self.assertAlmostEqual(momentum_ops[0].drift.gamma, 1.0377892156874091) - else: - self.assertEqual(None, momentum_ops[0].drift) - - if set_temperature: - self.assertAlmostEqual( - momentum_ops[0].temperature.temperature_kev, - 1.10100221e19, - delta=1e13, - ) - else: - self.assertEqual(None, momentum_ops[0].temperature) - - def test_temperature_invalid(self): - """check that invalid rms_velocities are not converted""" - for dist in self.__helper_get_distributions_with_rms_velocity(): - - def get_rms_species(rms_velocity): - dist_copy = deepcopy(dist) - dist_copy.rms_velocity = rms_velocity - new_species = picmi.Species(name="name", mass=1, initial_distribution=dist_copy) - return new_species - - # all components must be equal - invalid_rms_vectors = [[0, 1, 1], [1, 0, 1], [1, 1, 0], [1, 2, 3]] - for invalid_rms_vector in invalid_rms_vectors: - rms_species = get_rms_species(invalid_rms_vector) - with self.assertRaisesRegex(Exception, ".*(equal|same).*"): - pypicongpu_species, rest = rms_species.get_as_pypicongpu(None) - rms_species.get_independent_operations(pypicongpu_species, None) - - def test_from_speciestype(self): - """mass & charge will be derived from species type""" - picmi_species = picmi.Species(name="nitrogen", particle_type="N", charge_state=5) - e = picmi.Species(name="e", particle_type="electron") - - interaction = Interaction( - ground_state_ionization_model_list=[ - ADK( - ion_species=picmi_species, - ionization_current=None, - ionization_electron_species=e, - ADK_variant=ADKVariant.LinearPolarization, - ) - ] - ) - - pypic_species, rest = picmi_species.get_as_pypicongpu(interaction) - - # mass & charge derived - self.assertTrue(pypic_species.has_constant_of_type(species.constant.Mass)) - self.assertTrue(pypic_species.has_constant_of_type(species.constant.Charge)) - - mass_const = pypic_species.get_constant_by_type(species.constant.Mass) - charge_const = pypic_species.get_constant_by_type(species.constant.Charge) - - nitrogen = species.util.Element("N") - self.assertAlmostEqual(mass_const.mass_si, nitrogen.get_mass_si()) - self.assertAlmostEqual(charge_const.charge_si, nitrogen.get_charge_si()) - - # element properties are available - self.assertTrue(pypic_species.has_constant_of_type(species.constant.ElementProperties)) - - def test_charge_state_without_element_forbidden(self): - """charge state is not allowed without element name""" - with self.assertRaisesRegex(Exception, ".*particle_type.*"): - picmi.Species(name="abc", charge=1, mass=1, charge_state=-1, picongpu_fixed_charge=True).get_as_pypicongpu( - None - ) - - # allowed with particle species - # (actual charge state is inserted by ) - picmi.Species(name="abc", particle_type="H", charge_state=+1, picongpu_fixed_charge=True).get_as_pypicongpu( - None - ) - - def test_has_ionizers(self): - """generated species gets ionizers when appropriate""" - # only mass & charge: no ionizers - no_ionizers_picmi = picmi.Species(name="simple", mass=1, charge=2) - no_ionizers_pypic, rest = no_ionizers_picmi.get_as_pypicongpu(None) - self.assertTrue(not no_ionizers_pypic.has_constant_of_type(species.constant.GroundStateIonization)) - - # no charge state, but (theoretically) ionization levels known (as - # particle type is given): - with self.assertLogs(level=logging.WARNING) as implicit_logs: - with_warn_picmi = picmi.Species(name="HELIUM", particle_type="He", picongpu_fixed_charge=True) - - with_warn_pypic, rest = with_warn_picmi.get_as_pypicongpu(None) - self.assertTrue(not with_warn_pypic.has_constant_of_type(species.constant.GroundStateIonization)) - - self.assertEqual(1, len(implicit_logs.output)) - self.assertTrue( - re.match( - ".*HELIUM.*fixed charge state.*", - implicit_logs.output[0], - ) - ) - - with self.assertLogs(level=logging.WARNING) as explicit_logs: - # workaround b/c self.assertNoLogs() is not available yet - logging.warning("TESTWARN") - no_warn_picmi = picmi.Species(name="HELIUM", particle_type="He", picongpu_fixed_charge=True) - no_warn_pypic, rest = no_warn_picmi.get_as_pypicongpu(None) - self.assertTrue(not no_warn_pypic.has_constant_of_type(species.constant.GroundStateIonization)) - - self.assertTrue(1 <= len(explicit_logs.output)) - self.assertTrue("TESTWARN" in explicit_logs.output[0]) - - def test_fully_ionized_warning_electrons(self): - """electrons will not have the fully ionized warning""" - with self.assertLogs(level=logging.WARNING) as explicit_logs: - # workaround b/c self.assertNoLogs() is not available yet - logging.warning("TESTWARN") - no_warn_picmi = picmi.Species(name="ELECTRON", particle_type="electron") - - no_warn_pypic, rest = no_warn_picmi.get_as_pypicongpu(None) - self.assertTrue(not no_warn_pypic.has_constant_of_type(species.constant.GroundStateIonization)) - - self.assertEqual(1, len(explicit_logs.output)) - self.assertTrue("TESTWARN" in explicit_logs.output[0]) - - def test_ionize_non_elements(self): - """non-elements may not have a charge_state""" - with self.assertRaisesRegex(Exception, ".*charge_state may only be set for ions.*"): - picmi.Species(name="e", particle_type="electron", charge_state=-1).get_as_pypicongpu(None) - - def test_electron_from_particle_type(self): - """electron is correctly constructed from particle_type""" - picmi_e = picmi.Species(name="e", particle_type="electron") - pypic_e, rest = picmi_e.get_as_pypicongpu(None) - self.assertTrue(not pypic_e.has_constant_of_type(species.constant.GroundStateIonization)) - self.assertTrue(not pypic_e.has_constant_of_type(species.constant.ElementProperties)) - - mass_const = pypic_e.get_constant_by_type(species.constant.Mass) - charge_const = pypic_e.get_constant_by_type(species.constant.Charge) - - self.assertAlmostEqual(mass_const.mass_si, picmi.constants.m_e) - self.assertAlmostEqual(charge_const.charge_si, -picmi.constants.q_e) - - def test_fixed_charge_typesafety(self): - """picongpu_fixed_charge is type safe""" - for invalid in [1, "yes", [], {}]: - with self.assertRaises(typeguard.TypeCheckError): - picmi.Species(name="x", picongpu_fixed_charge=invalid) - - # works: - picmi_species = picmi.Species(name="x", particle_type="He", picongpu_fixed_charge=True) - - for invalid in [0, "no", [], {}]: - with self.assertRaises(typeguard.TypeCheckError): - picmi_species.picongpu_fixed_charge = invalid - - # False is allowed - picmi_species.picongpu_fixed_charge = False + ground_state_ionizations = [ + x for x in ion.get_as_pypicongpu().constants if isinstance(x, GroundStateIonization) + ] + # They have been merged: + assert len(ground_state_ionizations) == 1 + assert len(ground_state_ionizations[0].ionization_model_list) == len(ionizations) - def test_particle_type_invalid(self): - """unkown particle type rejects""" - for invalid in ["", "elektron", "e", "e-", "Uux"]: - with self.assertRaisesRegex(ValueError, ".*not a valid openPMD particle type.*"): - picmi.Species(name="x", particle_type=invalid).get_as_pypicongpu(None) - def test_ionization_charge_state_too_large(self): - """charge state must be <= number of protons""" - with self.assertRaises(AssertionError): - picmi.Species(name="x", particle_type="N", charge_state=8).get_as_pypicongpu(None) +if __name__ == "__main__": + main() diff --git a/test/python/picongpu/quick/pypicongpu/__init__.py b/test/python/picongpu/quick/pypicongpu/__init__.py deleted file mode 100644 index b5071a5542..0000000000 --- a/test/python/picongpu/quick/pypicongpu/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# flake8: noqa -from .simulation import * # pyflakes.ignore -from .grid import * # pyflakes.ignore -from .solver import * # pyflakes.ignore -from .runner import * # pyflakes.ignore -from .species import * # pyflakes.ignore -from .output import * # pyflakes.ignore -from .rendering import * # pyflakes.ignore -from .laser import * # pyflakes.ignore -from .customuserinput import * # pyflakes.ignore diff --git a/test/python/picongpu/quick/pypicongpu/customuserinput.py b/test/python/picongpu/quick/pypicongpu/customuserinput.py deleted file mode 100644 index d22295850c..0000000000 --- a/test/python/picongpu/quick/pypicongpu/customuserinput.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2024-2024 PIConGPU contributors -Authors: Brian Edward Marre -License: GPLv3+ -""" - -from picongpu.pypicongpu import customuserinput - -import unittest - - -class TestCustomUserInput(unittest.TestCase): - # test standard workflow is possible and data+tag is passed on - def test_standard_case_works(self): - c = customuserinput.CustomUserInput() - data1 = {"test_data_1": 1} - data2 = {"test_data_2": 2} - - tag1 = "tag_1" - tag2 = "tag_2" - - c.addToCustomInput(data1, tag1) - c.addToCustomInput(data2, tag2) - - rendering_context = c.get_rendering_context() - - self.assertEqual(rendering_context["test_data_1"], 1) - self.assertEqual(rendering_context["test_data_2"], 2) - - tags = c.get_tags() - self.assertIn(tag1, tags) - self.assertIn(tag2, tags) - - def test_wrong_tags(self): - c = customuserinput.CustomUserInput() - - data1 = {"test_data_1": 1} - data2 = {"test_data_2": 2} - - tag1_1 = "tag_1" - tag1_2 = "tag_1" - - # first add must succeed - c.addToCustomInput(data1, tag1_1) - with self.assertRaisesRegex(ValueError, "duplicate tag!"): - c.addToCustomInput(data2, tag1_2) - - with self.assertRaisesRegex(ValueError, "tag must not be empty"): - c.addToCustomInput(data2, "") - - def test_wrong_custom_input(self): - c = customuserinput.CustomUserInput() - - data1_1 = {"test_data_1": 1} - data1_2 = {"test_data_1": 2} - empty_data = {} - - tag1 = "tag_1" - tag2 = "tag_2" - - with self.assertRaisesRegex(ValueError, "custom input must contain at least 1 key"): - c.addToCustomInput(empty_data, tag1) - - c.addToCustomInput(data1_1, tag1) - with self.assertRaisesRegex(ValueError, "Key test_data_1 exist already, and specified values differ."): - c.addToCustomInput(data1_2, tag2) - - # test same key with same value is allowed - c.addToCustomInput(data1_1, tag2) diff --git a/test/python/picongpu/quick/pypicongpu/grid.py b/test/python/picongpu/quick/pypicongpu/grid.py deleted file mode 100644 index 83f9c30b35..0000000000 --- a/test/python/picongpu/quick/pypicongpu/grid.py +++ /dev/null @@ -1,114 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre, Richard Pausch -License: GPLv3+ -""" - -from picongpu.pypicongpu.grid import Grid3D, BoundaryCondition - -import unittest - -from pydantic import ValidationError - - -class TestGrid3D(unittest.TestCase): - def setUp(self): - """setup default grid""" - self.kwargs = dict( - cell_size_si=(1.2, 2.3, 4.5), - cell_cnt=(6, 7, 8), - boundary_condition=( - BoundaryCondition.PERIODIC, - BoundaryCondition.ABSORBING, - BoundaryCondition.PERIODIC, - ), - n_gpus=(2, 4, 1), - super_cell_size=(8, 8, 4), - grid_dist=None, - ) - self.g = Grid3D(**self.kwargs) - - def test_basic(self): - """test default setup""" - g = self.g - self.assertSequenceEqual((1.2, 2.3, 4.5), g.cell_size) - self.assertSequenceEqual((6, 7, 8), g.cell_cnt) - self.assertSequenceEqual( - (BoundaryCondition.PERIODIC, BoundaryCondition.ABSORBING, BoundaryCondition.PERIODIC), g.boundary_condition - ) - - def test_types(self): - """test raising errors if types are wrong""" - with self.assertRaises(ValidationError): - Grid3D( - **self.kwargs - | dict(boundary_condition=("open", BoundaryCondition.ABSORBING, BoundaryCondition.PERIODIC)) - ) - with self.assertRaises(ValidationError): - Grid3D( - **self.kwargs | dict(boundary_condition=(BoundaryCondition.PERIODIC, BoundaryCondition.ABSORBING, {})) - ) - with self.assertRaises(ValidationError): - Grid3D(**self.kwargs | dict(cell_cnt=(11.1, 7, 8))) - with self.assertRaises(ValidationError): - Grid3D(**self.kwargs | dict(cell_cnt=(6, 11.412, 8))) - with self.assertRaises(ValidationError): - Grid3D(**self.kwargs | dict(cell_cnt=(6, 7, 16781123173.12637183))) - - def test_gpu_and_cell_cnt_positive(self): - """test if n_gpus and cell number s are >0""" - with self.assertRaisesRegex(ValidationError, ".*contains values <= m=0.*"): - Grid3D(**self.kwargs | dict(cell_cnt=(-1, 7, 8))) - - with self.assertRaisesRegex(ValidationError, ".*contains values <= m=0.*"): - Grid3D(**self.kwargs | dict(cell_cnt=(6, -2, 8))) - - with self.assertRaisesRegex(ValidationError, ".*contains values <= m=0.*"): - Grid3D(**self.kwargs | dict(cell_cnt=(6, 7, 0))) - - for wrong_n_gpus in [tuple([-1, 1, 1]), tuple([1, 1, 0])]: - with self.assertRaisesRegex(ValidationError, ".*contains values <= m=0.*"): - Grid3D(**self.kwargs | dict(n_gpus=wrong_n_gpus)) - - def test_mandatory(self): - """test if None as content fails""" - # check that mandatory arguments can't be none - with self.assertRaises(ValidationError): - Grid3D(**self.kwargs | dict(cell_size_si=None)) - with self.assertRaises(ValidationError): - Grid3D(**self.kwargs | dict(cell_cnt=None)) - with self.assertRaises(ValidationError): - Grid3D(**self.kwargs | dict(boundary_condition=None)) - with self.assertRaises(ValidationError): - Grid3D(**self.kwargs | dict(n_gpus=None)) - - def test_get_rendering_context(self): - """object is correctly serialized""" - # automatically checks against schema - context = self.g.get_rendering_context() - self.assertEqual(1.2, context["cell_size"]["x"]) - self.assertEqual(2.3, context["cell_size"]["y"]) - self.assertEqual(4.5, context["cell_size"]["z"]) - self.assertEqual(6, context["cell_cnt"]["x"]) - self.assertEqual(7, context["cell_cnt"]["y"]) - self.assertEqual(8, context["cell_cnt"]["z"]) - - # boundary condition translated to numbers for cfgfiles - self.assertEqual("1", context["boundary_condition"]["x"]) - self.assertEqual("0", context["boundary_condition"]["y"]) - self.assertEqual("1", context["boundary_condition"]["z"]) - - # n_gpus ouput - self.assertEqual(2, context["gpu_cnt"]["x"]) - self.assertEqual(4, context["gpu_cnt"]["y"]) - self.assertEqual(1, context["gpu_cnt"]["z"]) - - -class TestBoundaryCondition(unittest.TestCase): - def test_cfg_translation(self): - """test boundary condition strings""" - p = BoundaryCondition.PERIODIC - a = BoundaryCondition.ABSORBING - self.assertEqual("0", a.get_cfg_str()) - self.assertEqual("1", p.get_cfg_str()) diff --git a/test/python/picongpu/quick/pypicongpu/laser.py b/test/python/picongpu/quick/pypicongpu/laser.py deleted file mode 100644 index e7def1a3f2..0000000000 --- a/test/python/picongpu/quick/pypicongpu/laser.py +++ /dev/null @@ -1,180 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre, Alexander Debus -License: GPLv3+ -""" - -import copy -import unittest - -from picongpu.pypicongpu.laser import GaussianLaser, PolarizationType -from pydantic import ValidationError - -""" @file we only test for types here, test for values errors is done in the - custom picmi-objects""" - -KWARGS = dict( - wavelength=1.2, - waist=3.4, - duration=5.6, - focal_position=[0, 7.8, 0], - phi0=2.9, - E0=9.0, - pulse_init=1.3, - propagation_direction=[0.0, 1.0, 0.0], - polarization_type=PolarizationType.LINEAR, - polarization_direction=[0.0, 1.0, 0.0], - laguerre_modes=[1.0], - laguerre_phases=[0.0], - huygens_surface_positions=[[1, -1], [1, -1], [1, -1]], -) - - -class TestGaussianLaser(unittest.TestCase): - def test_types(self): - """invalid types are rejected""" - for not_float in [None, [], {}]: - with self.assertRaises(ValidationError): - GaussianLaser(**KWARGS | dict(wavelength=not_float)) - with self.assertRaises(ValidationError): - GaussianLaser(**KWARGS | dict(waist=not_float)) - with self.assertRaises(ValidationError): - GaussianLaser(**KWARGS | dict(duration=not_float)) - with self.assertRaises(ValidationError): - GaussianLaser(**KWARGS | dict(phi0=not_float)) - with self.assertRaises(ValidationError): - GaussianLaser(**KWARGS | dict(E0=not_float)) - with self.assertRaises(ValidationError): - GaussianLaser(**KWARGS | dict(pulse_init=not_float)) - - for not_position_vector in [1, 1.0, None, ["string"]]: - with self.assertRaises(ValidationError): - GaussianLaser(**KWARGS | dict(focal_position=not_position_vector)) - - for not_polarization_type in [1.3, None, "", []]: - with self.assertRaises(ValidationError): - GaussianLaser(**KWARGS | dict(polarization_type=not_polarization_type)) - - for not_direction_vector in [1, 1.3, None, "", ["string"]]: - with self.assertRaises(ValidationError): - GaussianLaser(**KWARGS | dict(polarization_direction=not_direction_vector)) - with self.assertRaises(ValidationError): - GaussianLaser(**KWARGS | dict(propagation_direction=not_direction_vector)) - - for invalid_list in [None, 1.2, "1.2", ["string"]]: - with self.assertRaises(ValidationError): - GaussianLaser(**KWARGS | dict(laguerre_modes=invalid_list)) - with self.assertRaises(ValidationError): - GaussianLaser(**KWARGS | dict(laguerre_phases=invalid_list)) - with self.assertRaises(ValidationError): - GaussianLaser(**KWARGS | dict(polarization_direction=invalid_list)) - with self.assertRaises(ValidationError): - GaussianLaser(**KWARGS | dict(propagation_direction=invalid_list)) - with self.assertRaises(ValidationError): - GaussianLaser(**KWARGS | dict(huygens_surface_positions=invalid_list)) - - def test_polarization_type(self): - """polarization type enum sanity checks""" - lin = PolarizationType.LINEAR - circular = PolarizationType.CIRCULAR - - self.assertNotEqual(lin, circular) - - self.assertNotEqual(lin.get_cpp_str(), circular.get_cpp_str()) - - for polarization_type in [lin, circular]: - self.assertEqual(str, type(polarization_type.get_cpp_str())) - - def test_invalid_huygens_surface_description_types(self): - """Huygens surfaces must be described as - [[x_min:int, x_max:int], [y_min:int,y_max:int], - [z_min:int, z_max:int]]""" - invalid_elements = [None, [], [1.2, 3.4]] - valid_rump = [[5, 6], [7, 8]] - - invalid_descriptions = [] - for invalid_element in invalid_elements: - for pos in range(3): - base = copy.deepcopy(valid_rump) - base.insert(pos, invalid_element) - invalid_descriptions.append(base) - - for invalid_description in invalid_descriptions: - with self.assertRaises(TypeError): - GaussianLaser(**KWARGS).huygens_surface_positions(invalid_description) - - def test_invalid_laguerre_modes_empty(self): - """laguerre modes must be set non-empty""" - with self.assertRaises(ValidationError): - GaussianLaser(**KWARGS | dict(laguerre_modes=[])) - with self.assertRaises(ValidationError): - GaussianLaser(**KWARGS | dict(laguerre_modes=[1.0], laguerre_phases=[])) - - def test_invalid_laguerre_modes_invalid_length(self): - """num of laguerre modes/phases must be equal""" - with self.assertRaises(ValidationError): - GaussianLaser(**KWARGS | dict(laguerre_modes=[1.0], laguerre_phases=[2, 3])) - - def test_positive_definite_laguerre_modes(self): - """test whether laguerre modes are positive definite""" - with self.assertLogs(level="WARNING") as caught_logs: - GaussianLaser(**KWARGS | dict(laguerre_modes=[-1.0])) - self.assertEqual(1, len(caught_logs.output)) - - def test_translation(self): - """is translated to context object""" - # note: implicitly checks against schema - laser = GaussianLaser(**KWARGS) - context = laser.get_rendering_context()["data"] - self.assertEqual(context["wave_length_si"], laser.wave_length_si) - self.assertEqual(context["waist_si"], laser.waist_si) - self.assertEqual(context["pulse_duration_si"], laser.pulse_duration_si) - self.assertEqual( - context["focus_pos_si"], - [ - {"component": laser.focus_pos_si[0]}, - {"component": laser.focus_pos_si[1]}, - {"component": laser.focus_pos_si[2]}, - ], - ) - self.assertEqual(context["phase"], laser.phase) - self.assertEqual(context["E0_si"], laser.E0_si) - self.assertEqual(context["pulse_init"], laser.pulse_init) - self.assertEqual( - context["propagation_direction"], - [ - {"component": laser.propagation_direction[0]}, - {"component": laser.propagation_direction[1]}, - {"component": laser.propagation_direction[2]}, - ], - ) - self.assertEqual(context["polarization_type"], laser.polarization_type.get_cpp_str()) - self.assertEqual( - context["polarization_direction"], - [ - {"component": laser.polarization_direction[0]}, - {"component": laser.polarization_direction[1]}, - {"component": laser.polarization_direction[2]}, - ], - ) - self.assertEqual(context["laguerre_modes"], [{"single_laguerre_mode": 1.0}]) - self.assertEqual(context["laguerre_phases"], [{"single_laguerre_phase": 0.0}]) - self.assertEqual(context["modenumber"], 0) - self.assertEqual( - context["huygens_surface_positions"], - { - "row_x": { - "negative": laser.huygens_surface_positions[0][0], - "positive": laser.huygens_surface_positions[0][1], - }, - "row_y": { - "negative": laser.huygens_surface_positions[1][0], - "positive": laser.huygens_surface_positions[1][1], - }, - "row_z": { - "negative": laser.huygens_surface_positions[2][0], - "positive": laser.huygens_surface_positions[2][1], - }, - }, - ) diff --git a/test/python/picongpu/quick/pypicongpu/output/__init__.py b/test/python/picongpu/quick/pypicongpu/output/__init__.py deleted file mode 100644 index 350673738c..0000000000 --- a/test/python/picongpu/quick/pypicongpu/output/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# flake8: noqa -from .auto import * # pyflakes.ignore -from .phase_space import * # pyflakes.ignore -from .timestepspec import * # pyflakes.ignore diff --git a/test/python/picongpu/quick/pypicongpu/output/auto.py b/test/python/picongpu/quick/pypicongpu/output/auto.py deleted file mode 100644 index e1436dc3f4..0000000000 --- a/test/python/picongpu/quick/pypicongpu/output/auto.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2025 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre, Julian Lenz -License: GPLv3+ -""" - -from picongpu.pypicongpu.output.timestepspec import TimeStepSpec -from picongpu.pypicongpu.output import Auto - -import unittest -from pydantic import ValidationError - - -class TestAuto(unittest.TestCase): - def test_types(self): - """type safety is ensured""" - - invalid_periods = [13.2, [], "2", None, {}, (1)] - for invalid_period in invalid_periods: - with self.assertRaises(ValidationError): - Auto(period=invalid_period) - - def test_rendering(self): - """data transformed to template-consumable version""" - a = Auto(period=TimeStepSpec([slice(0, None, 17)])) - - # normal rendering - context = a.get_rendering_context() - self.assertTrue(context["typeID"]["auto"]) - context = context["data"] - self.assertEqual(17, context["period"]["specs"][0]["step"]) diff --git a/test/python/picongpu/quick/pypicongpu/output/phase_space.py b/test/python/picongpu/quick/pypicongpu/output/phase_space.py deleted file mode 100644 index a4b0461609..0000000000 --- a/test/python/picongpu/quick/pypicongpu/output/phase_space.py +++ /dev/null @@ -1,127 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2025 PIConGPU contributors -Authors: Julian Lenz -License: GPLv3+ -""" - -from picongpu.pypicongpu.output.timestepspec import TimeStepSpec -from picongpu.pypicongpu.output import PhaseSpace -from picongpu.pypicongpu.species import Species -from picongpu.pypicongpu.species.attribute import Position, Momentum - -import unittest -import typeguard - - -def create_species(): - species = Species() - species.name = "electron" - species.attributes = [Position(), Momentum()] - species.constants = [] - return species - - -class TestPhaseSpace(unittest.TestCase): - def test_empty(self): - """empty args handled correctly""" - ps = PhaseSpace() - # unset args - with self.assertRaises(Exception): - ps._get_serialized() - - ps.species = create_species() - ps.period = TimeStepSpec([slice(0, None, 17)]) - ps.spatial_coordinate = "x" - ps.momentum_coordinate = "px" - ps.min_momentum = 0.0 - ps.max_momentum = 1.0 - - # ok: - ps._get_serialized() - - def test_types(self): - """type safety is ensured""" - ps = PhaseSpace() - - invalid_species = ["string", 1, 1.0, None, {}] - for invalid_species_ in invalid_species: - with self.assertRaises(typeguard.TypeCheckError): - ps.species = invalid_species_ - - invalid_periods = [13.2, [], "2", None, {}] - for invalid_period in invalid_periods: - with self.assertRaises(typeguard.TypeCheckError): - ps.period = invalid_period - - invalid_spatial_coordinates = ["a", "b", "c", (1,), None, {}] - for invalid_spatial_coordinate in invalid_spatial_coordinates: - with self.assertRaises(typeguard.TypeCheckError): - ps.spatial_coordinate = invalid_spatial_coordinate - - invalid_momentum_coordinates = ["a", "b", "c", (1,), None, {}] - for invalid_momentum_coordinate in invalid_momentum_coordinates: - with self.assertRaises(typeguard.TypeCheckError): - ps.momentum_coordinate = invalid_momentum_coordinate - - invalid_min_momentum = ["string", (1,), None, {}] - for invalid_min_momentum_ in invalid_min_momentum: - with self.assertRaises(typeguard.TypeCheckError): - ps.min_momentum = invalid_min_momentum_ - - invalid_max_momentum = ["string", (1,), None, {}] - for invalid_max_momentum_ in invalid_max_momentum: - with self.assertRaises(typeguard.TypeCheckError): - ps.max_momentum = invalid_max_momentum_ - - # ok - ps.species = create_species() - ps.period = TimeStepSpec([slice(0, None, 17)]) - ps.spatial_coordinate = "x" - ps.momentum_coordinate = "px" - ps.min_momentum = 0.0 - ps.max_momentum = 1.0 - - def test_rendering(self): - """data transformed to template-consumable version""" - ps = PhaseSpace() - ps.species = create_species() - ps.period = TimeStepSpec([slice(0, None, 42)]) - ps.spatial_coordinate = "x" - ps.momentum_coordinate = "px" - ps.min_momentum = 0.0 - ps.max_momentum = 1.0 - - # normal rendering - context = ps.get_rendering_context() - self.assertTrue(context["typeID"]["phasespace"]) - context = context["data"] - self.assertEqual(42, context["period"]["specs"][0]["step"]) - self.assertEqual("x", context["spatial_coordinate"]) - self.assertEqual("px", context["momentum_coordinate"]) - self.assertEqual(0.0, context["min_momentum"]) - self.assertEqual(1.0, context["max_momentum"]) - - # refuses to render if attributes are not set - ps = PhaseSpace() - with self.assertRaises(Exception): - ps.get_rendering_context() - - def test_momentum_values(self): - """min_momentum and max_momentum values are valid""" - ps = PhaseSpace() - ps.species = create_species() - ps.period = TimeStepSpec([slice(0, None, 1)]) - ps.spatial_coordinate = "x" - ps.momentum_coordinate = "px" - - # Min is larger than max, that's not allowed - ps.min_momentum = 2.0 - ps.max_momentum = 1.0 - - with self.assertRaises(ValueError): - ps.check() - - # get_rendering_context calls check internally, so this should also fail: - with self.assertRaises(ValueError): - ps.get_rendering_context() diff --git a/test/python/picongpu/quick/pypicongpu/output/timestepspec.py b/test/python/picongpu/quick/pypicongpu/output/timestepspec.py deleted file mode 100644 index ab632184d4..0000000000 --- a/test/python/picongpu/quick/pypicongpu/output/timestepspec.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Julian Lenz -License: GPLv3+ -""" - -from picongpu.pypicongpu.output import TimeStepSpec -from picongpu.pypicongpu.output.timestepspec import Spec -import unittest - -from pydantic import ValidationError - - -class TestTimeStepSpec(unittest.TestCase): - def test_init_with_valid_slices(self): - specs = [slice(0, 10, 1), slice(10, 20, 2)] - time_step_spec = TimeStepSpec(specs=specs) - self.assertEqual(time_step_spec.specs, [Spec(start=s.start, stop=s.stop, step=s.step) for s in specs]) - - def test_init_with_invalid_slices(self): - with self.assertRaises(ValidationError): - TimeStepSpec(specs=[slice(0, 10, 1), "invalid"]) - - def test_serialize_with_valid_slices(self): - specs = [slice(0, 10, 1), slice(10, 20, 2)] - time_step_spec = TimeStepSpec(specs=specs) - serialized = time_step_spec.get_rendering_context() - expected = { - "specs": [ - {"start": 0, "stop": 10, "step": 1}, - {"start": 10, "stop": 20, "step": 2}, - ] - } - self.assertEqual(serialized, expected) - - def test_serialize_with_none_values(self): - specs = [slice(None, 10, 1), slice(10, None, 2), slice(10, 20, None)] - time_step_spec = TimeStepSpec(specs=specs) - serialized = time_step_spec.get_rendering_context() - expected = { - "specs": [ - {"start": 0, "stop": 10, "step": 1}, - {"start": 10, "stop": -1, "step": 2}, - {"start": 10, "stop": 20, "step": 1}, - ] - } - self.assertEqual(serialized, expected) diff --git a/test/python/picongpu/quick/pypicongpu/rendering/__init__.py b/test/python/picongpu/quick/pypicongpu/rendering/__init__.py deleted file mode 100644 index c0c4e729a3..0000000000 --- a/test/python/picongpu/quick/pypicongpu/rendering/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# flake8: noqa -from .renderer import * # pyflakes.ignore -from .renderedobject import * # pyflakes.ignore -from .pmaccprinter import * # pyflakes.ignore diff --git a/test/python/picongpu/quick/pypicongpu/rendering/pmaccprinter.py b/test/python/picongpu/quick/pypicongpu/rendering/pmaccprinter.py deleted file mode 100644 index e69045b86c..0000000000 --- a/test/python/picongpu/quick/pypicongpu/rendering/pmaccprinter.py +++ /dev/null @@ -1,106 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2025 PIConGPU contributors -Authors: Julian Lenz -License: GPLv3+ -""" - -import unittest -import sympy -from sympy.abc import x, y -from picongpu.pypicongpu.rendering import PMAccPrinter - -CODE_SNIPPETS = { - "abs": (sympy.Abs(x), "pmacc::math::abs(x)"), - "min": (sympy.Min(x, y), "pmacc::math::min(x, y)"), - "max": (sympy.Max(x, y), "pmacc::math::max(x, y)"), - "erf": (sympy.erf(x), "pmacc::math::erf(x)"), - "exp": (sympy.exp(x), "pmacc::math::exp(x)"), - "log": (sympy.log(x), "pmacc::math::log(x)"), - "pow": (sympy.Pow(x, y), "pmacc::math::pow(x, y)"), - "sqrt": (sympy.sqrt(x), "pmacc::math::sqrt(x)"), - "cbrt": (sympy.cbrt(x), "pmacc::math::cbrt(x)"), - "floor": (sympy.floor(x), "pmacc::math::floor(x)"), - "ceil": (sympy.ceiling(x), "pmacc::math::ceil(x)"), - "sin": (sympy.sin(x), "pmacc::math::sin(x)"), - "cos": (sympy.cos(x), "pmacc::math::cos(x)"), - "tan": (sympy.tan(x), "pmacc::math::tan(x)"), - "asin": (sympy.asin(x), "pmacc::math::asin(x)"), - "acos": (sympy.acos(x), "pmacc::math::acos(x)"), - "atan": (sympy.atan(x), "pmacc::math::atan(x)"), - "atan2": (sympy.atan2(x, y), "pmacc::math::atan2(x, y)"), - "sinh": (sympy.sinh(x), "pmacc::math::sinh(x)"), - "cosh": (sympy.cosh(x), "pmacc::math::cosh(x)"), - "tanh": (sympy.tanh(x), "pmacc::math::tanh(x)"), - "asinh": (sympy.asinh(x), "pmacc::math::asinh(x)"), - "acosh": (sympy.acosh(x), "pmacc::math::acosh(x)"), - "atanh": (sympy.atanh(x), "pmacc::math::atanh(x)"), - "fmod2_native": (x % y, "pmacc::math::fmod(x, y)"), - "fmod_sympy": (sympy.Mod(x, y), "pmacc::math::fmod(x, y)"), - "rsqrt": (1 / sympy.sqrt(x), "pmacc::math::pow(x, -1.0/2.0)"), - "int_division": (x // y, "pmacc::math::floor(x/y)"), - "pi": ( - sympy.S.Pi, - "pmacc::math::Pi::Value", - ), - "2_pi": ( - sympy.S.Pi / 2, - "pmacc::math::Pi::halfValue", - ), - "pi_half": ( - sympy.S.Pi / 4, - "pmacc::math::Pi::quarterValue", - ), - "pi_quarter": ( - 2 / sympy.S.Pi, - "pmacc::math::Pi::doubleReciprocalValue", - ), - "log8": (sympy.log(x, 8), "pmacc::math::log(x)/pmacc::math::log(8)"), - # for these two there's technically specialised functions in PMAcc - # but it's kind of a pain to catch these expressions - # before they got through the printer - # because sympy treats them as log(x)/log(2), ... - # so it's very hard to actually identify such expressions. - "log2": (sympy.log(x, 2), "pmacc::math::log(x)/pmacc::math::log(2)"), - "log10": (sympy.log(x, 10), "pmacc::math::log(x)/pmacc::math::log(10)"), - # PMAcc functionality that's not supported/expressible with sympy: - # "round": (round(x), "pmacc::math::round(x)"), - # subtly different from fmod (in PMAcc at least): - # "remainder": (???, "pmacc::math::remainder(x, y)"), - # "trunc": (???, "pmacc::math::trunc(x)"), - # "lround": (???, "pmacc::math::lround(x)"), - # "llround": (???, "pmacc::math::llround(x)"), - # Sinc can be used but isn't translated into the pmacc equivalent - # but the defining formula. - # Bessel functions are available upon request... -} - - -class TestPMAccPrinterMeta(type): - def __new__(cls, name, bases, dictionary): - # Generate one test for each example in the examples folder - for code_name, (expression, cpp_code) in CODE_SNIPPETS.items(): - code_name = "test_" + code_name - dictionary[code_name] = ( - # This is slightly convoluted: - # Python's semantics around variables implement - # "sharing" semantics (not even quite reference semantics). - # Also, lambdas capture the variable and not the value. - # So after the execution of a loop all lambdas refer to - # the last value of the loop variable - # if they tried to capture it. - # So, we need to eagerly evaluate the `example` variable - # which we achieve via an immediately evaluated lambda expression. - # Please excuse my C++ dialect. - lambda expression, cpp_code: lambda self: self.generic_test(expression, cpp_code) - )(expression, cpp_code) - return type.__new__(cls, name, bases, dictionary) - - -class TestPMAccPrinter(unittest.TestCase, metaclass=TestPMAccPrinterMeta): - def generic_test(self, expression, cpp_code): - self.assertEqual(PMAccPrinter().doprint(expression), cpp_code) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/picongpu/quick/pypicongpu/rendering/renderedobject.py b/test/python/picongpu/quick/pypicongpu/rendering/renderedobject.py deleted file mode 100644 index 694e6217c7..0000000000 --- a/test/python/picongpu/quick/pypicongpu/rendering/renderedobject.py +++ /dev/null @@ -1,407 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from picongpu.pypicongpu.rendering.renderedobject import SelfRegisteringRenderedObject -from picongpu.pypicongpu.rendering import RenderedObject - -from picongpu.pypicongpu.field_solver import YeeSolver -from picongpu.pypicongpu import Simulation - -import unittest -import typing -import typeguard -import jsonschema -import referencing - - -class TestRenderedObject(unittest.TestCase): - def schema_store_init(self) -> None: - RenderedObject._schemas_loaded = False - RenderedObject._maybe_fill_schema_store() - - def add_schema_to_schema_store(self, uri, schema) -> None: - # for testing direct access of internal only methods - RenderedObject._registry = referencing.Registry().with_resource( - uri, referencing.Resource(schema, referencing.jsonschema.DRAFT202012) - ) - RenderedObject._registry = RenderedObject._registry.crawl() - - def get_uri(self, type_: typing.Type) -> str: - # for testing direct access of internal only methods - fqn = RenderedObject._get_fully_qualified_class_name(type_) - uri = RenderedObject._get_schema_uri_by_fully_qualified_class_name(fqn) - return uri - - def schema_store_reset(self) -> None: - # for testing direct access of internal only methods - RenderedObject._schemas_loaded = False - RenderedObject._registry = referencing.Registry() - - def setUp(self) -> None: - self.schema_store_reset() - # if required test case can additionally init the schema store - - def tearDown(self): - self.schema_store_reset() - - def test_basic(self): - """simple example using real-world example""" - yee = YeeSolver() - self.assertTrue(isinstance(yee, RenderedObject)) - self.assertNotEqual({}, RenderedObject._get_schema_from_class(type(yee))) - # no throw -> schema found - self.assertEqual(yee.get_rendering_context(), yee.model_dump(mode="json")) - - # manually check that schema has been loaded - uri = self.get_uri(type(yee)) - - self.assertEqual( - RenderedObject._registry.contents(uri), - { - "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.field_solver.Yee.YeeSolver", - "type": "object", - "properties": {"name": {"type": "string"}}, - "required": ["name"], - "unevaluatedProperties": False, - }, - ) - - def test_not_implemented(self): - """raises if _get_serialized() is not implemented""" - - class EmptyClass(RenderedObject): - pass - - with self.assertRaises(NotImplementedError): - e = EmptyClass() - e.get_rendering_context() - - def test_no_schema(self): - """not finding a schema raises""" - - class HasNoSchema(RenderedObject): - def _get_serialized(self): - return {"any": "thing"} - - with self.assertRaises(referencing.exceptions.NoSuchResource): - h = HasNoSchema() - h.get_rendering_context() - - def test_schema_validation_and_passthru(self): - """schema is properly validated (and passed through)""" - self.schema_store_init() - - class MaybeValid(RenderedObject): - be_valid = False - - def _get_serialized(self): - if self.be_valid: - return {"my_string": "ja", "num": 17} - return {"my_string": ""} - - uri = self.get_uri(MaybeValid) - schema = { - "properties": { - "my_string": {"type": "string"}, - "num": {"type": "number"}, - }, - "required": ["my_string", "num"], - "unevaluatedProperties": False, - } - self.add_schema_to_schema_store(uri, schema) - - # all okay - maybe_valid = MaybeValid() - maybe_valid.be_valid = True - self.assertNotEqual({}, maybe_valid.get_rendering_context()) - - maybe_valid.be_valid = False - with self.assertRaisesRegex(Exception, ".*[Ss]chema.*"): - maybe_valid.get_rendering_context() - - def test_invalid_schema(self): - """schema itself is broken -> creates error""" - self.schema_store_init() - - class HasInvalidSchema(RenderedObject): - def _get_serialized(self): - return {"any": "thing"} - - uri = self.get_uri(HasInvalidSchema) - schema = { - "type": "invalid_type_HJJE$L!BGCDHS", - } - self.add_schema_to_schema_store(uri, schema) - - h = HasInvalidSchema() - with self.assertRaisesRegex(Exception, ".*[Ss]chema.*"): - h.get_rendering_context() - - def test_fully_qualified_classname(self): - """fully qualified classname is correctly generated""" - # concept: define two classes of same name - # FQN (fully qualified name) must contain their names - # but both FQNs must be not equal - - def obj1(): - class MyClass: - pass - - return MyClass - - def obj2(): - class MyClass: - pass - - return MyClass - - t1 = obj1() - t2 = obj2() - # both are not equal - self.assertNotEqual(t1, t2) - # ... but type equality still works (sanity check) - self.assertNotEqual(t1, obj1()) - - fqn1 = RenderedObject._get_fully_qualified_class_name(t1) - fqn2 = RenderedObject._get_fully_qualified_class_name(t2) - - # -> "MyClass" is contained in FQN - self.assertTrue("MyClass" in fqn1) - self.assertTrue("MyClass" in fqn2) - # ... but they are not the same - self.assertNotEqual(fqn1, fqn2) - - def test_schema_optional(self): - """schema may define optional parameters""" - self.schema_store_init() - - class MayReturnNone(RenderedObject): - toreturn = None - - def _get_serialized(self): - return {"value": self.toreturn} - - uri = self.get_uri(MayReturnNone) - schema = { - "type": "object", - "properties": { - "value": { - "anyOf": [ - { - "type": "null", - }, - { - "type": "object", - "properties": { - "mandatory": { - "type": "number", - "exclusiveMinimum": 0, - }, - }, - "required": ["mandatory"], - "unevaluatedProperties": False, - }, - ], - }, - }, - "required": ["value"], - "unevaluatedProperties": False, - } - self.add_schema_to_schema_store(uri, schema) - - # ok: - mrn = MayReturnNone() - mrn.toreturn = None - self.assertEqual({"value": None}, mrn.get_rendering_context()) - mrn.toreturn = {"mandatory": 2} - self.assertEqual({"value": {"mandatory": 2}}, mrn.get_rendering_context()) - - for invalid in [{"mandatory": 0}, {}, "", []]: - with self.assertRaises(Exception): - mrn = MayReturnNone() - mrn.toreturn = invalid - mrn.get_rendering_context() - - def test_check_context(self): - """context check can be used manually""" - yee = YeeSolver() - context_correct = yee.get_rendering_context() - context_incorrect = {} - - # must load schemas if required -> reset schema store - self.schema_store_reset() - self.assertTrue(not RenderedObject._schemas_loaded) - - # (A) context is correctly checked against the given type - # passes: - RenderedObject.check_context_for_type(YeeSolver, context_correct) - - # implicitly filled schema store - self.assertTrue(RenderedObject._schemas_loaded) - - # same context is not valid for simulation object - with self.assertRaises(jsonschema.exceptions.ValidationError): - RenderedObject.check_context_for_type(Simulation, context_correct) - - # incorrect context not accepted for YeeSolver - with self.assertRaises(jsonschema.exceptions.ValidationError): - RenderedObject.check_context_for_type(YeeSolver, context_incorrect) - - # (B) invalid requests are rejected - # wrong argument types - with self.assertRaises(typeguard.TypeCheckError): - RenderedObject.check_context_for_type("YeeSolver", context_correct) - with self.assertRaises(typeguard.TypeCheckError): - RenderedObject.check_context_for_type(YeeSolver, "{}") - - # types without schema - class HasNoValidation: - # note: don't use "Schema" to not accidentally trigger the regex - # for the error message below - # note: does not have to inherit from RenderedObject - pass - - with self.assertRaisesRegex(referencing.exceptions.NoSuchResource, ".*[Ss]chema.*"): - RenderedObject.check_context_for_type(HasNoValidation, {}) - - -class TestSelfRegisteringRenderedObject(unittest.TestCase): - def setUp(self): - class Base(SelfRegisteringRenderedObject): - def _get_serialized(self): - return {} - - self.Base = Base - # We don't want to check against a schema for now: - RenderedObject.check_context_for_type = lambda _, c: c - - def test_names_list_is_empty(self): - self.assertSetEqual(set(self.Base().get_rendering_context()["typeID"].keys()), set()) - - def test_subclass_without_name_is_not_registered(self): - class UnregisteredSubclass(self.Base): - pass - - self.assertSetEqual(set(self.Base().get_rendering_context()["typeID"].keys()), set()) - - def test_subclass_with_name_is_registered(self): - class RegisteredSubclass(self.Base): - _name = "arbitrary_name" - - self.assertSetEqual(set(self.Base().get_rendering_context()["typeID"].keys()), {RegisteredSubclass._name}) - - def test_two_subclasses_with_name_are_registered(self): - class RegisteredSubclass1(self.Base): - _name = "arbitrary_name1" - - class RegisteredSubclass2(self.Base): - _name = "arbitrary_name2" - - self.assertSetEqual( - set(self.Base().get_rendering_context()["typeID"].keys()), - {RegisteredSubclass1._name, RegisteredSubclass2._name}, - ) - - def test_leaves_can_register(self): - class BaseClass(self.Base): - pass - - class LeafClass(BaseClass): - _name = "arbitrary_name2" - - self.assertSetEqual(set(self.Base().get_rendering_context()["typeID"].keys()), {LeafClass._name}) - - def test_z(self): - # This test is last in lexicographical ordering. - # It's to make sure that if the tests are run deterministically in lexicographical order, - # the preconditions are still fulfilled. - self.assertSetEqual(set(self.Base().get_rendering_context()["typeID"].keys()), set()) - - def test_multiple_hierarchies_are_independent(self): - class BaseClass1(SelfRegisteringRenderedObject): - pass - - class LeafClass1(BaseClass1): - _name = "arbitrary_name2" - - def _get_serialized(self): - return {} - - class BaseClass2(SelfRegisteringRenderedObject): - pass - - class LeafClass2(BaseClass2): - _name = "arbitrary_name2" - - def _get_serialized(self): - return {} - - self.assertSetEqual(set(LeafClass1().get_rendering_context()["typeID"].keys()), {LeafClass1._name}) - self.assertSetEqual(set(LeafClass2().get_rendering_context()["typeID"].keys()), {LeafClass2._name}) - - def test_different_leaves_know_who_they_are(self): - class LeafClass1(self.Base): - _name = "arbitrary_name1" - - class LeafClass2(self.Base): - _name = "arbitrary_name2" - - self.assertTrue(LeafClass1().get_rendering_context()["typeID"][LeafClass1._name]) - self.assertFalse(LeafClass1().get_rendering_context()["typeID"][LeafClass2._name]) - self.assertFalse(LeafClass2().get_rendering_context()["typeID"][LeafClass1._name]) - self.assertTrue(LeafClass2().get_rendering_context()["typeID"][LeafClass2._name]) - - def test_get_serialized_into_data(self): - arbitrary_value = 42 - - class LeafClass(self.Base): - _name = "another_arbitrary_name" - - def _get_serialized(self): - return {"arbitrary_name": arbitrary_value} - - self.assertDictEqual(LeafClass().get_rendering_context()["data"], LeafClass()._get_serialized()) - - def test_schema_is_checked_for_base_as_well_as_child(self): - types = [] - RenderedObject.check_context_for_type = lambda t, c: types.append(t) or c - - class LeafClass(self.Base): - _name = "arbitrary_name" - - LeafClass().get_rendering_context() - self.assertSetEqual(set(types), {self.Base, LeafClass}) - - def test_in_deep_hierarchies_only_leaf_and_topmost_schemas_are_checked(self): - types = [] - RenderedObject.check_context_for_type = lambda t, c: types.append(t) or c - - class BaseClass(self.Base): - # This is an additional layer but it is not checked in the schema. - pass - - class LeafClass(BaseClass): - _name = "arbitrary_name2" - - def _get_serialized(self): - return {} - - LeafClass().get_rendering_context() - self.assertSetEqual(set(types), {self.Base, LeafClass}) - - def test_raises_on_identical_names(self): - class LeafClass1(self.Base): - _name = "identical_name" - - def define_class(): - class _(self.Base): - _name = LeafClass1._name - - with self.assertRaisesRegex( - TypeError, "Attempt to register cls=.* with name cls._name=.* failed because that was registered before." - ): - define_class() diff --git a/test/python/picongpu/quick/pypicongpu/rendering/renderer.py b/test/python/picongpu/quick/pypicongpu/rendering/renderer.py deleted file mode 100644 index 000b0532af..0000000000 --- a/test/python/picongpu/quick/pypicongpu/rendering/renderer.py +++ /dev/null @@ -1,309 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from picongpu.pypicongpu.rendering import Renderer - -import unittest -import math -import logging -import os -import tempfile -import pathlib - - -class TestRenderer(unittest.TestCase): - def test_check_rendering_context_basic(self): - """sanity-check valid context""" - # no raise - Renderer.check_rendering_context({}) - Renderer.check_rendering_context( - { - "key": 1, - "ya": None, - "bool": True, - "grades": [ - {"grade": 1}, - {"grade": 2}, - {"grade": 1}, - {"grade": 1}, - ], - "object": { - "nested": { - "num": 3.5, - }, - }, - } - ) - - def test_check_rendering_context_type(self): - """only dicts may be rendering context""" - for invalid in [None, "asd", 123, ["123"]]: - with self.assertRaises(TypeError): - Renderer.check_rendering_context(invalid) - - def test_check_rendering_context_invalid_composition(self): - """rendering context may not be constructed arbitrarily""" - # wrong leaves - for leaf in [set(), ({"key": 1},)]: - with self.assertRaisesRegex(TypeError, ".*[Ll]eaf.*"): - Renderer.check_rendering_context({"key": leaf}) - - # list items not dict - with self.assertRaisesRegex(TypeError, ".*[Ll]ist.*"): - Renderer.check_rendering_context( - { - "list": [1, 2, 3, None], - } - ) - - # tuples not accepted as list replacement - with self.assertRaises(TypeError): - Renderer.check_rendering_context( - { - "not_list": tuple({"k": "v"}), - } - ) - - def test_check_rendering_context_special_values(self): - """special values are correctly allowed/disallowed""" - for valid_leaf in ["", 0, -1.2, [], None, True, False]: - Renderer.check_rendering_context({"key": valid_leaf}) - - for invalid_leaf in [math.nan, math.inf, -math.inf]: - with self.assertRaisesRegex(ValueError, ".*[Ll]eaf.*"): - Renderer.check_rendering_context({"key": invalid_leaf}) - - # empty dict must be *type* error (because invalid type for leaf) - with self.assertRaisesRegex(TypeError, ".*[Ll]eaf.*"): - Renderer.check_rendering_context({"key": dict()}) - - def test_check_rendering_context_invalid_keys(self): - """keys must be well-formed""" - # keys must be string - with self.assertRaisesRegex(TypeError, ".*[Ss]tring.*"): - Renderer.check_rendering_context({123: "jaja"}) - - # keys may not begin with _ - with self.assertRaisesRegex(ValueError, ".*[Uu]nderscore.*"): - Renderer.check_rendering_context({"_reserved": "x"}) - - # keys may not contain . - for invalid_key in [".asd", "hjasd.asd", "asd.", "."]: - with self.assertRaisesRegex(ValueError, ".*[Dd]ot.*"): - Renderer.check_rendering_context({invalid_key: "x"}) - - def test_context_preprocessor_conversion(self): - """leafs are translated to string if applicable""" - leaf_translations = [ - (None, None), - ("any_string", "any_string"), - (True, True), - ([], []), - (0, "0"), - (-1.5, "-1.5"), - ] - - for original, expected_translation in leaf_translations: - orig_context = { - "key": original, - } - translated_context = Renderer.get_context_preprocessed(orig_context) - self.assertEqual(translated_context["key"], expected_translation) - # orig_context not touched - self.assertEqual(orig_context["key"], original) - - # precision kept (double) - # preliminaries: check python - # this particular test number is chosen s.t. all *16* digits are - # preserved, just by pure conincidence (actually only 15 digits are) - double_num_str = "9785135.876452025" - # check that python preserves this precision - self.assertEqual(double_num_str, str(float(double_num_str))) - context_orig = { - "num": float(double_num_str), - } - context_pp = Renderer.get_context_preprocessed(context_orig) - self.assertEqual(context_pp["num"], double_num_str) - - # precision kept (64 bit int, typical value of size_t) - max_uint_str = "18446744073709551616" - context_orig = { - "num": int(max_uint_str), - } - context_pp = Renderer.get_context_preprocessed(context_orig) - self.assertEqual(context_pp["num"], max_uint_str) - - def test_context_preprocessor_add_list_attrs(self): - """preprocessing adds appropriate attributes""" - context_orig = { - "l": [ - {"val": 0}, - {"val": 1}, - {"val": 2}, - {"val": 3}, - {"val": 4}, - {"val": 5}, - ] - } - context_pp = Renderer.get_context_preprocessed(context_orig) - self.assertEqual(context_pp["l"][0]["_last"], False) - self.assertEqual(context_pp["l"][0]["_first"], True) - - for i in range(1, 5): - self.assertEqual(context_pp["l"][i]["_last"], False) - self.assertEqual(context_pp["l"][i]["_first"], False) - - self.assertEqual(context_pp["l"][5]["_last"], True) - self.assertEqual(context_pp["l"][5]["_first"], False) - - def test_context_preprocessor_add_top_level_attrs(self): - """highest level dict gets special attrs""" - context_orig = { - "child": { - "val": "x", - }, - } - context_pp = Renderer.get_context_preprocessed(context_orig) - - self.assertEqual(context_orig["child"], context_pp["child"]) - self.assertTrue("_date" in context_pp) - - def test_get_rendered_template(self): - """sanity-check the rendering engine""" - self.assertEqual("", Renderer.get_rendered_template({}, "")) - self.assertEqual("", Renderer.get_rendered_template({"yes": False}, "{{#yes}}xxx{{/yes}}")) - self.assertEqual( - "xyz", - Renderer.get_rendered_template( - { - "alphabet": [ - {"letter": "x"}, - {"letter": "y"}, - {"letter": "z"}, - ] - }, - "{{#alphabet}}{{{letter}}}{{/alphabet}}", - ), - ) - self.assertEqual( - "subsetvalue", - Renderer.get_rendered_template( - { - "sub": { - "set": { - "value": "subsetvalue", - }, - }, - }, - "{{{sub.set.value}}}", - ), - ) - - def test_get_rendered_template_warn_escape(self): - """if HTML-escaping is used, a warning is issued""" - warn_cnt_by_template = { - "{{#if}}x{{/if}}": 0, - "{{var}}": 1, - "{{var}}{{num}}": 2, - "{{{var}}}": 0, - "{{! comment only }}": 0, - } - - default_context = { - "if": True, - "var": "asd", - "num": "17", - } - - for template, expected_warn_num in warn_cnt_by_template.items(): - with self.assertLogs(level="WARNING") as caught_logs: - # at least one warning is required or this will fails - logging.warning("workaround warning") - Renderer.get_rendered_template(default_context, template) - # -> subtract workaround warning - self.assertEqual(expected_warn_num, len(caught_logs.output) - 1) - - def test_render_directory(self): - """rendering directory traverses correct files""" - tmpdir = None - # get temp dir - with tempfile.TemporaryDirectory() as tmpdir_path: - tmpdir = tmpdir_path - os.chdir(tmpdir) - - os.mkdir("sub") - os.mkdir("sub/dir") - - content_by_path = { - "not_tpl": "{{{var}}}", - "var.mustache": "{{{var}}}", - "empty.mustache": "", - "sub/dir/file": "1", - "sub/dir/num.mustache": "{{{num}}}", - } - - for path, content in content_by_path.items(): - with open(path, "w") as file: - file.write(content) - - default_context = { - "var": "a string", - "num": "0", - } - - expected_content_by_path = { - "not_tpl": "{{{var}}}", - "var": "a string", - "empty": "", - "sub/dir/file": "1", - "sub/dir/num": "0", - } - Renderer.render_directory(default_context, tmpdir) - - # all files exist & content is correct - for path, expected_content in expected_content_by_path.items(): - with open(path, "r") as file: - content = file.read() - self.assertEqual(content, expected_content) - - # no other files have been created - # and all mustache-files are prefixed with a . - expected_files = { - "not_tpl", - ".var.mustache", - "var", - ".empty.mustache", - "empty", - "sub/dir/file", - "sub/dir/.num.mustache", - "sub/dir/num", - } - existing_files = set( - map( - lambda p: str(p.relative_to(tmpdir)), - filter(lambda p: p.is_file(), pathlib.Path(tmpdir).rglob("*")), - ) - ) - self.assertEqual(existing_files, expected_files) - - # temp dir now deleted (b/c left "with" block) - # -> raises on non-existing dir - self.assertTrue(not os.path.exists(tmpdir)) - with self.assertRaises(ValueError): - Renderer.render_directory({}, tmpdir) - - def test_render_directory_overwrite(self): - """reject if files would be overwritten""" - with tempfile.TemporaryDirectory() as tmpdir: - os.chdir(tmpdir) - # create files - for fname in ["file", "file.mustache"]: - with open(fname, "w"): - pass - - with self.assertRaisesRegex(ValueError, ".*over.*"): - Renderer.render_directory({}, tmpdir) diff --git a/test/python/picongpu/quick/pypicongpu/runner.py b/test/python/picongpu/quick/pypicongpu/runner.py deleted file mode 100644 index 9ae6233b91..0000000000 --- a/test/python/picongpu/quick/pypicongpu/runner.py +++ /dev/null @@ -1,451 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -import json -import os -import re -import tempfile -import unittest -from pathlib import Path - -import typeguard -from picongpu import picmi -from picongpu.pypicongpu.runner import Runner - - -class TestRunner(unittest.TestCase): - # note: tests involving actual running/building are in the long tests - - def __get_tmpdir_name(self, relative_to=None): - name = None - with tempfile.TemporaryDirectory() as tmpdir: - name = tmpdir - return name if relative_to is None else str(Path(name).relative_to(Path().absolute())) - - def setUp(self): - # Note that this picmi simulation is a valid specification, - # but -- at the time of writing -- does not successfully build. - # the long tests (which actually build stuff, this here doesn't) - # have a working parameter set - grid = picmi.Cartesian3DGrid( - number_of_cells=[192, 2048, 12], - lower_bound=[0, 0, 0], - upper_bound=[3.40992e-5, 9.07264e-5, 2.1312e-6], - lower_boundary_conditions=["open", "open", "periodic"], - upper_boundary_conditions=["open", "open", "periodic"], - ) - solver = picmi.ElectromagneticSolver(method="Yee", grid=grid) - self.picmi_sim = picmi.Simulation(time_step_size=1.39e-16, max_steps=int(2048), solver=solver) - self.sim = self.picmi_sim.get_as_pypicongpu() - - # reset default scratch dir - if Runner.SCRATCH_ENV_NAME in os.environ: - del os.environ[Runner.SCRATCH_ENV_NAME] - - self.tmpdir = tempfile.gettempdir() - assert os.path.isdir(self.tmpdir) - - # use as working dir - os.chdir(self.tmpdir) - - self.existing_dir1 = self.__get_tmpdir_name() - self.existing_dir2 = self.__get_tmpdir_name() - os.mkdir(self.existing_dir1) - os.mkdir(self.existing_dir2) - self.nonexisting_dir1 = self.__get_tmpdir_name() - self.nonexisting_dir2 = self.__get_tmpdir_name() - self.nonexisting_dir3 = self.__get_tmpdir_name() - - self.nonexisting_relative_dir1 = self.__get_tmpdir_name(relative_to="") - self.nonexisting_relative_dir2 = self.__get_tmpdir_name(relative_to="") - self.existing_relative_dir1 = self.__get_tmpdir_name(relative_to="") - os.mkdir(self.existing_relative_dir1) - - self.__maybe_tmpdir_destroy = [ - self.existing_dir1, - self.existing_dir2, - os.path.abspath(self.existing_relative_dir1), - ] - - assert Runner.SCRATCH_ENV_NAME not in os.environ - assert os.path.isdir(self.tmpdir) - assert os.path.isdir(self.existing_dir1) - assert os.path.isdir(self.existing_dir2) - assert not os.path.exists(self.nonexisting_dir1) - assert not os.path.exists(self.nonexisting_dir2) - assert not os.path.exists(self.nonexisting_dir3) - assert not os.path.exists(self.nonexisting_relative_dir1) - assert not os.path.exists(self.nonexisting_relative_dir2) - assert os.path.isdir(self.existing_relative_dir1) - - def tearDown(self): - for d in self.__maybe_tmpdir_destroy: - if os.path.isdir(d): - os.rmdir(d) - assert not os.path.exists(d) - - assert os.path.isdir(self.tmpdir) - assert not os.path.exists(self.existing_dir1) - assert not os.path.exists(self.existing_dir2) - assert not os.path.exists(self.nonexisting_dir1) - assert not os.path.exists(self.nonexisting_dir2) - assert not os.path.exists(self.nonexisting_dir3) - - def test_test_setup(self): - """check that test setup works""" - # does not throw - Runner(self.picmi_sim.get_as_pypicongpu()) - Runner(self.sim) - - r = Runner( - self.sim, - scratch_dir=self.existing_dir1, - setup_dir=self.nonexisting_dir1, - run_dir=self.nonexisting_dir2, - ) - self.assertEqual(self.existing_dir1, r.scratch_dir) - self.assertEqual(self.nonexisting_dir1, r.setup_dir) - self.assertEqual(self.nonexisting_dir2, r.run_dir) - - def test_param_types(self): - """ - check that arbitrary types are not supported - (also sse test_init_sim_type below) - """ - # also check that correct usage works: - Runner(self.sim) - - with self.assertRaises(typeguard.TypeCheckError): - Runner(self.sim, pypicongpu_template_dir=1) - with self.assertRaises(typeguard.TypeCheckError): - Runner(self.sim, scratch_dir=["/", "tmp"]) - with self.assertRaises(typeguard.TypeCheckError): - Runner(self.sim, setup_dir={}) - with self.assertRaises(typeguard.TypeCheckError): - Runner(self.sim, run_dir=lambda x: x) - - r = Runner(self.sim) - with self.assertRaises(typeguard.TypeCheckError): - r.setup_dir = 1 - with self.assertRaises(typeguard.TypeCheckError): - r.run_dir = [] - with self.assertRaises(typeguard.TypeCheckError): - r.scratch_dir = {} - - def test_dir_collision(self): - """check that dir names are different""" - # others empty: does not throw - Runner(self.sim, run_dir="abc") - Runner(self.sim, setup_dir="abc") - Runner(self.sim, scratch_dir=self.existing_dir1) - - # actual collision tests - # note: do not check the message content here - # -> the error *might* be collision, - # but also might be due to (non-) existing contraints for dirs - with self.assertRaises(Exception): - Runner( - self.sim, - run_dir=self.nonexisting_dir1, - scratch_dir=self.nonexisting_dir1, - ) - with self.assertRaises(Exception): - Runner(self.sim, run_dir=self.nonexisting_dir1, setup_dir=self.nonexisting_dir1) - with self.assertRaises(Exception): - Runner( - self.sim, - scratch_dir=self.nonexisting_dir1, - setup_dir=self.nonexisting_dir1, - ) - with self.assertRaises(Exception): - Runner( - self.sim, - scratch_dir=self.nonexisting_dir1, - setup_dir=self.nonexisting_dir1, - run_dir=self.nonexisting_dir1, - ) - - def test_scratch_from_env(self): - """test that the scratch dir is loaded from environment when None""" - r = Runner(self.sim) - self.assertEqual(None, r.scratch_dir) - - r = Runner(self.sim, scratch_dir=self.existing_dir1) - self.assertEqual(self.existing_dir1, r.scratch_dir) - self.assertTrue(r.run_dir.startswith(r.scratch_dir)) - - # now provide default via environment - os.environ[Runner.SCRATCH_ENV_NAME] = self.existing_dir2 - - r = Runner(self.sim) - self.assertEqual(self.existing_dir2, r.scratch_dir) - self.assertTrue(r.run_dir.startswith(r.scratch_dir)) - - # can still be overwritten though - r = Runner(self.sim, scratch_dir=self.existing_dir1) - self.assertEqual(self.existing_dir1, r.scratch_dir) - self.assertTrue(r.run_dir.startswith(r.scratch_dir)) - - # relative path is accepted, but converted to absolute - os.environ[Runner.SCRATCH_ENV_NAME] = self.existing_relative_dir1 - r = Runner(self.sim) - self.assertEqual( - os.path.realpath(self.existing_relative_dir1), - os.path.realpath(r.scratch_dir), - ) - self.assertTrue(os.path.isabs(r.scratch_dir)) - - def test_missing_dirs_generated(self): - """check that given dirs are kept and not-given dirs generated""" - - def check_postconditions(r): - self.assertTrue(not os.path.exists(r.setup_dir)) - self.assertTrue(not os.path.exists(r.run_dir)) - - # all dirs missing - r = Runner(self.sim) - check_postconditions(r) - # all generated inside tmp dir - self.assertTrue(r.setup_dir.startswith(self.tmpdir)) - self.assertTrue(r.run_dir.startswith(self.tmpdir)) - - # run derived from scratch - r = Runner(self.sim, scratch_dir=self.existing_dir1) - check_postconditions(r) - self.assertTrue(r.run_dir.startswith(r.scratch_dir)) - self.assertTrue(r.setup_dir.startswith(self.tmpdir)) - - # scratch ignore if run is given - r = Runner(self.sim, scratch_dir=self.existing_dir1, run_dir=self.nonexisting_dir1) - check_postconditions(r) - self.assertTrue(not r.setup_dir.startswith(r.scratch_dir)) - self.assertEqual(self.nonexisting_dir1, r.run_dir) - - # setup kept as given - r = Runner(self.sim, setup_dir=self.nonexisting_dir2) - check_postconditions(r) - self.assertEqual(self.nonexisting_dir2, r.setup_dir) - - def test_checks_given_dirs(self): - """sanity checks performed on given dirs""" - # valid call - Runner( - self.sim, - scratch_dir=self.existing_dir1, - setup_dir=self.nonexisting_dir1, - run_dir=self.nonexisting_dir2, - ) - - # make sure that the paths are not used/created during __init__ - assert os.path.isdir(self.existing_dir1) - assert os.path.isdir(self.existing_dir2) - assert not os.path.exists(self.nonexisting_dir1) - assert not os.path.exists(self.nonexisting_dir2) - assert not os.path.exists(self.nonexisting_dir3) - - # invalid calls: - with self.assertRaisesRegex(Exception, ".*scratch.*"): - Runner( - self.sim, - scratch_dir=self.nonexisting_dir3, - setup_dir=self.nonexisting_dir1, - run_dir=self.nonexisting_dir2, - ) - with self.assertRaisesRegex(Exception, ".*setup.*"): - Runner( - self.sim, - scratch_dir=self.existing_dir1, - setup_dir=self.existing_dir2, - run_dir=self.nonexisting_dir2, - ) - with self.assertRaisesRegex(Exception, ".*run.*"): - Runner( - self.sim, - scratch_dir=self.existing_dir1, - setup_dir=self.nonexisting_dir1, - run_dir=self.existing_dir2, - ) - - def test_invalid_dir_names(self): - """forbidden character (not alphanum.-_) produce an error""" - allowed_dir_names = [ - "abas", - "-123", - "123", - "/ahjsd", - "hnaxbcnxyci8HJBASDJASG61723", - "/tmp/hadjs/7123", - ] - forbidden_dir_names = [ - "", - ";asd", - "&/(12)", - "try#123", - "a:colon", - "why is space not allowed", - ] - - for allowed_name in allowed_dir_names: - # no error - Runner(self.sim, setup_dir=allowed_name) - Runner(self.sim, run_dir=allowed_name) - # do not check with scratch dir, as it must not necessarily exist - - for forbidden_name in forbidden_dir_names: - with self.assertRaisesRegex(Exception, ".*valid.*"): - Runner(self.sim, scratch_dir=forbidden_name) - with self.assertRaisesRegex(Exception, ".*valid.*"): - Runner(self.sim, setup_dir=forbidden_name) - with self.assertRaisesRegex(Exception, ".*valid.*"): - Runner(self.sim, run_dir=forbidden_name) - - def test_absolute(self): - r = Runner( - self.sim, - scratch_dir=self.existing_relative_dir1, - setup_dir=self.nonexisting_relative_dir1, - run_dir=self.nonexisting_relative_dir2, - ) - - self.assertNotEqual(self.existing_relative_dir1, r.scratch_dir) - self.assertNotEqual(self.nonexisting_relative_dir1, r.setup_dir) - self.assertNotEqual(self.nonexisting_relative_dir2, r.run_dir) - - # note: realpath for existing dir - self.assertEqual( - os.path.realpath(self.existing_relative_dir1), - os.path.realpath(r.scratch_dir), - ) - self.assertEqual( - os.path.abspath(self.nonexisting_relative_dir1), - os.path.abspath(r.setup_dir), - ) - self.assertEqual(os.path.abspath(self.nonexisting_relative_dir2), os.path.abspath(r.run_dir)) - - self.assertTrue(os.path.isabs(r.scratch_dir)) - self.assertTrue(os.path.isabs(r.setup_dir)) - self.assertTrue(os.path.isabs(r.run_dir)) - - def test_human_readable(self): - """check that autogenerated names are (somewhat) human-readable""" - r = Runner(self.sim) - - setup_dir_base = os.path.basename(r.setup_dir) - run_dir_base = os.path.basename(r.run_dir) - - self.assertTrue(re.match("^pypicongpu-.*$", setup_dir_base)) - self.assertTrue("setup" in setup_dir_base) - - self.assertTrue(re.match("^pypicongpu-.*$", run_dir_base)) - self.assertTrue("run" in run_dir_base) - - # Check that both have a common prefix (besides pypicongpu-), - # typically the date. - # This is useful so dirs generated by the same runner - # are next to each other when sorting. - def get_wo_pypicongpu_prefix(s): - m = re.match("^pypicongpu-(.*)$", s) - # require a match - assert m - return m[1] - - self.assertEqual("123-teststring", get_wo_pypicongpu_prefix("pypicongpu-123-teststring")) - - setup_dir_base_noprefix = get_wo_pypicongpu_prefix(setup_dir_base) - run_dir_base_noprefix = get_wo_pypicongpu_prefix(run_dir_base) - - self.assertEqual("blasabbl", os.path.commonprefix(["blasabbl123", "blasabblajhsdkljh"])) - # common_start would typically be the current date - # (though using the date is not required) - common_start = os.path.commonprefix([setup_dir_base_noprefix, run_dir_base_noprefix]) - # six: shortest useful date representation YYMMDD - self.assertTrue(len(common_start) >= 6) - - def test_init_picmi_or_picongpu(self): - """ - check that the simulation can be initialized using - both picmi or a pypicongpu object - """ - # note: the other tests typically use the picongpu simulation, - # so here emphasis is put on the picmi simulation object - - def is_sims_equal(a, b): - """compare two pypicongpu simulations""" - return a.get_rendering_context() == b.get_rendering_context() - - # check precondition by setup - assert is_sims_equal(self.picmi_sim.get_as_pypicongpu(), self.sim) - - r_from_picmi = Runner(self.picmi_sim) - r_from_picongpu = Runner(self.sim) - - self.assertTrue(is_sims_equal(r_from_picmi.sim, r_from_picongpu.sim)) - # sanity checks if everything worked - self.assertEqual(r_from_picmi.scratch_dir, r_from_picongpu.scratch_dir) - self.assertNotEqual(None, r_from_picmi.setup_dir) - self.assertNotEqual(None, r_from_picmi.run_dir) - - def test_init_sim_type(self): - """ - due to (potentially) circular imports, - the runner __init__ can't use typeguard to check the simulation type. - Instead manual type checks are used. - This tests if these typechecks are implemented correctly. - """ - # must work - Runner(self.sim) - Runner(self.picmi_sim) - - for invalid_sim in [None, {}, 0, ""]: - with self.assertRaises(typeguard.TypeCheckError): - Runner(invalid_sim) - - def test_applies_templates(self): - """runner renders templates from template dir""" - # generate test template - with tempfile.TemporaryDirectory() as template_dir: - template_path = Path(template_dir) - # note: put in subdir, b/c not all directories are cloned by - # pic-create - os.makedirs(template_path / "etc" / "picongpu") - testfile_template = template_path / "etc" / "picongpu" / "date.mustache" - with open(testfile_template, "w") as tpl_file: - tpl_file.write("{{{_date}}}") - # workaround (@todo rm): add location for pypicongpu.param - os.makedirs(template_path / "include" / "picongpu" / "param") - - # create ruunner with previous tempalte dir, rest of directories - # is not predefined - runner = Runner(self.sim, pypicongpu_template_dir=(template_path,)) - - runner.generate() - - setup_path = Path(runner.setup_dir) - - testfile_rendered = setup_path / "etc" / "picongpu" / "date" - with open(testfile_rendered, "r") as rendered_file: - content = rendered_file.read() - # render and template did something - self.assertTrue("_date" not in content) - self.assertNotEqual("{{{_date}}}", content) - - # did not replace with empty (i.e. _date was defined) - self.assertNotEqual("", content) - - def test_dumps_rendering_context(self): - """rendering context is dumped on generation""" - runner = Runner(self.sim) - runner.generate() - setup_path = Path(runner.setup_dir) - dump_file = setup_path / "pypicongpu.json" - - self.assertTrue(dump_file.exists()) - with open(dump_file, "r") as file: - content = file.read() - self.assertNotEqual("", content) - from_file = json.loads(content) - self.assertEqual(self.sim.get_rendering_context(), from_file) diff --git a/test/python/picongpu/quick/pypicongpu/simulation.py b/test/python/picongpu/quick/pypicongpu/simulation.py deleted file mode 100644 index 6cc74fa654..0000000000 --- a/test/python/picongpu/quick/pypicongpu/simulation.py +++ /dev/null @@ -1,292 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2025 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre, Alexander Debus, Richard Pausch, Julian Lenz -License: GPLv3+ -""" - -from picongpu.pypicongpu.simulation import Simulation -from picongpu.pypicongpu.laser import GaussianLaser, PolarizationType -from picongpu.pypicongpu import grid, YeeSolver, species, customuserinput -from picongpu.pypicongpu import rendering -from picongpu.pypicongpu.species.operation.layout import Random - -import unittest -import typeguard - -from copy import deepcopy - - -@typeguard.typechecked -def helper_get_species(name: str) -> species.Species: - spec = species.Species() - spec.name = name - spec.constants = [] - spec.attributes = [species.attribute.Position()] - return spec - - -class TestSimulation(unittest.TestCase): - def setUp(self): - self.s = Simulation() - self.s.delta_t_si = 13.37 - self.s.time_steps = 42 - self.s.typical_ppc = 1 - self.s.grid = grid.Grid3D( - cell_size_si=(1, 2, 3), - cell_cnt=(4, 5, 6), - n_gpus=(1, 1, 1), - boundary_condition=( - grid.BoundaryCondition.PERIODIC, - grid.BoundaryCondition.PERIODIC, - grid.BoundaryCondition.PERIODIC, - ), - super_cell_size=(8, 8, 4), - grid_dist=None, - ) - self.s.solver = YeeSolver() - self.s.laser = None - self.s.custom_user_input = None - self.s.moving_window = None - self.s.walltime = None - self.s.binomial_current_interpolation = False - self.s.plugins = "auto" - self.s.init_manager = species.InitManager() - self.s.base_density = 1.0e25 - - self.laser = [ - GaussianLaser( - wavelength=1.2, - waist=3.4, - duration=5.6, - focal_position=[0, 7.8, 0], - centroid_position=[0, 0, 0], - phi0=2.9, - E0=9.0, - pulse_init=1.3, - propagation_direction=[0, 1, 0], - polarization_type=PolarizationType.LINEAR, - polarization_direction=[0, 0, 1], - laguerre_modes=[1.2, 2.4], - laguerre_phases=[2.4, 3.4], - huygens_surface_positions=[[1, -1], [1, -1], [1, -1]], - ) - ] - - self.customData_1 = [{"test_data_1": 1}, "tag_1"] - self.customData_2 = [{"test_data_2": 2}, "tag_2"] - - def test_basic(self): - s = self.s - self.assertEqual(13.37, s.delta_t_si) - self.assertEqual(42, s.time_steps) - self.assertNotEqual(None, self.s.grid) - - # does not throw: - s.get_rendering_context() - - def test_types(self): - s = self.s - with self.assertRaises(typeguard.TypeCheckError): - s.delta_t_si = "1" - with self.assertRaises(typeguard.TypeCheckError): - s.time_steps = 14.3 - with self.assertRaises(typeguard.TypeCheckError): - s.grid = [42, 13, 37] - with self.assertRaises(typeguard.TypeCheckError): - s.custom_user_input = {"test": 15} - with self.assertRaises(typeguard.TypeCheckError): - s.custom_user_input = customuserinput.CustomUserInput() - - def test_mandatory(self): - # there are two main ways these objects are mandatory: - # 1. they must be set at some point - # 2. the can't be None (==can't be set to none) - # option 1. is not tested, because this is ensured by the property - # builder, # and the test code would be very boilerplate-y - # option 2. is tested below: - - s = self.s - with self.assertRaises(typeguard.TypeCheckError): - s.delta_t_si = None - with self.assertRaises(typeguard.TypeCheckError): - s.time_steps = None - with self.assertRaises(typeguard.TypeCheckError): - s.grid = None - - def test_species_collision(self): - """check that species name collisions are detected""" - particle_1 = helper_get_species("collides") - particle_2 = helper_get_species("collides") - particle_3 = helper_get_species("doesnotcollide") - - expected_error_re = "^.*(collide|twice).*$" - - with self.assertRaisesRegex(Exception, expected_error_re): - self.s.init_manager.all_species = [particle_1, particle_2] - self.s.get_rendering_context() - with self.assertRaisesRegex(Exception, expected_error_re): - self.s.init_manager.all_species = [particle_1, particle_2, particle_3] - self.s.get_rendering_context() - with self.assertRaisesRegex(Exception, expected_error_re): - self.s.init_manager.all_species = [particle_3, particle_3] - self.s.get_rendering_context() - - # no errors: - valid_species_lists = [[particle_2, particle_3], [particle_1, particle_3]] - for valid_species_list in valid_species_lists: - sim = deepcopy(self.s) - # initmanager resets species attributes - # -> no deepcopy necessary - # (but still: species objects are SHARED between loop iterations) - sim.init_manager.all_species = valid_species_list - - self.s.init_manager.all_operations = [] - for single_species in valid_species_list: - not_placed = species.operation.NotPlaced() - not_placed.species = single_species - sim.init_manager.all_operations.append(not_placed) - - op_momentum = species.operation.SimpleMomentum() - op_momentum.species = single_species - op_momentum.drift = None - op_momentum.temperature = None - sim.init_manager.all_operations.append(op_momentum) - - # does not throw: - sim.get_rendering_context() - - def test_get_rendering_context(self): - """rendering context is returned""" - # automatically checks & applies template - self.assertTrue(isinstance(self.s, rendering.RenderedObject)) - - # fill initmanager with some meaningful content - species_dummy = species.Species() - species_dummy.name = "myname" - species_dummy.constants = [] - uniform_dist = species.operation.densityprofile.Uniform(density_si=123) - - op_density = species.operation.SimpleDensity() - op_density.ppc = 1 - op_density.profile = uniform_dist - op_density.species = { - species_dummy, - } - op_density.layout = Random(ppc=1) - - op_momentum = species.operation.SimpleMomentum() - op_momentum.species = species_dummy - op_momentum.drift = None - op_momentum.temperature = None - - self.s.init_manager.all_species = [species_dummy] - self.s.init_manager.all_operations = [op_density, op_momentum] - - context = self.s.get_rendering_context() - - # cross check with set values in setup method - self.assertEqual(13.37, context["delta_t_si"]) - self.assertEqual(42, context["time_steps"]) - self.assertEqual("Yee", context["solver"]["name"]) - self.assertEqual(2, context["grid"]["cell_size"]["y"]) - self.assertEqual(None, context["laser"]) - self.assertEqual(self.s.init_manager.get_rendering_context(), context["species_initmanager"]) - self.assertEqual(1, context["output"][0]["data"]["period"]["specs"][0]["step"]) - - self.assertNotEqual([], context["species_initmanager"]["species"]) - self.assertNotEqual([], context["species_initmanager"]["operations"]) - - def test_laser_passthru(self): - """laser is passed through""" - # no laser - self.assertEqual(None, self.s.laser) - context = self.s.get_rendering_context() - self.assertEqual(None, context["laser"]) - - # a laser - sim = self.s - sim.laser = self.laser - context = sim.get_rendering_context() - laser_context = sim.laser[0].get_rendering_context() - self.assertEqual(context["laser"][0], laser_context) - - def test_output_auto_short_duration(self): - """period is always at least one""" - for time_steps in [1, 17, 99]: - self.s.time_steps = time_steps - self.assertEqual( - 1, - self.s.get_rendering_context()["output"][0]["data"]["period"]["specs"][0]["step"], - ) - - def test_custom_input_pass_thru(self): - i = customuserinput.CustomUserInput() - - i.addToCustomInput(self.customData_1[0], self.customData_1[1]) - i.addToCustomInput(self.customData_2[0], self.customData_2[1]) - - self.s.custom_user_input = [i] - - renderingContextGoodResult = { - "test_data_1": 1, - "test_data_2": 2, - "tags": ["tag_1", "tag_2"], - } - self.assertEqual( - renderingContextGoodResult, - self.s.get_rendering_context()["customuserinput"], - ) - - def test_combination_of_several_custom_inputs(self): - i_1 = customuserinput.CustomUserInput() - i_2 = customuserinput.CustomUserInput() - - i_1.addToCustomInput(self.customData_1[0], self.customData_1[1]) - i_2.addToCustomInput(self.customData_2[0], self.customData_2[1]) - - self.s.custom_user_input = [i_1, i_2] - - renderingContextGoodResult = { - "test_data_1": 1, - "test_data_2": 2, - "tags": ["tag_1", "tag_2"], - } - self.assertEqual( - renderingContextGoodResult, - self.s.get_rendering_context()["customuserinput"], - ) - - def test_duplicated_tag_over_different_custom_inputs(self): - i_1 = customuserinput.CustomUserInput() - i_2 = customuserinput.CustomUserInput() - - i_1.addToCustomInput(self.customData_1[0], self.customData_1[1]) - i_2.addToCustomInput(self.customData_2[0], self.customData_1[1]) - - self.s.custom_user_input = [i_1, i_2] - - with self.assertRaisesRegex(ValueError, "duplicate tag provided!, tags must be unique!"): - self.s.get_rendering_context() - - def test_duplicated_key_over_different_custom_inputs(self): - i = customuserinput.CustomUserInput() - i_sameValue = customuserinput.CustomUserInput() - i_differentValue = customuserinput.CustomUserInput() - - duplicateKeyData_differentValue = {"test_data_1": 3} - duplicateKeyData_sameValue = {"test_data_1": 1} - - i.addToCustomInput(self.customData_1[0], self.customData_1[1]) - i_sameValue.addToCustomInput(duplicateKeyData_sameValue, "tag_2") - i_differentValue.addToCustomInput(duplicateKeyData_differentValue, "tag_3") - - self.s.custom_user_input = [i] - - # should work - self.s.custom_user_input.append(i_sameValue) - self.s.get_rendering_context() - - with self.assertRaisesRegex(ValueError, "Key test_data_1 exist already, and specified values differ."): - self.s.custom_user_input.append(i_differentValue) - self.s.get_rendering_context() diff --git a/test/python/picongpu/quick/pypicongpu/solver.py b/test/python/picongpu/quick/pypicongpu/solver.py deleted file mode 100644 index e262a85b55..0000000000 --- a/test/python/picongpu/quick/pypicongpu/solver.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from picongpu.pypicongpu.field_solver import Solver, YeeSolver, LeheSolver - -import unittest - - -class TestSolver(unittest.TestCase): - def test_basic(self): - # the parent class must raise an error when using - # note: the error is that this class does not exist - with self.assertRaises(Exception): - Solver().get_rendering_context() - - -class TestYeeSolver(unittest.TestCase): - def test_basic(self): - # basically only check the type -- which actually happens automatically - yee = YeeSolver() - self.assertTrue(isinstance(yee, Solver)) - - self.assertEqual("Yee", yee.get_rendering_context()["name"]) - - -class TestLeheSolver(unittest.TestCase): - def test_basic(self): - # basically only check the type -- which actually happens automatically - lehe = LeheSolver() - self.assertTrue(isinstance(lehe, Solver)) - - self.assertEqual("Lehe<>", lehe.get_rendering_context()["name"]) diff --git a/test/python/picongpu/quick/pypicongpu/species/__init__.py b/test/python/picongpu/quick/pypicongpu/species/__init__.py deleted file mode 100644 index 7559ce8def..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# flake8: noqa -from .species import * # pyflakes.ignore -from .initmanager import * # pyflakes.ignore -from .constant import * # pyflakes.ignore -from .attribute import * # pyflakes.ignore -from .operation import * # pyflakes.ignore -from .util import * # pyflakes.ignore diff --git a/test/python/picongpu/quick/pypicongpu/species/attribute/__init__.py b/test/python/picongpu/quick/pypicongpu/species/attribute/__init__.py deleted file mode 100644 index 38c9c513ed..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/attribute/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# flake8: noqa -from .attribute import * # pyflakes.ignore -from .position import * # pyflakes.ignore -from .weighting import * # pyflakes.ignore -from .momentum import * # pyflakes.ignore -from .boundelectrons import * # pyflakes.ignore diff --git a/test/python/picongpu/quick/pypicongpu/species/attribute/attribute.py b/test/python/picongpu/quick/pypicongpu/species/attribute/attribute.py deleted file mode 100644 index 2c81515d5b..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/attribute/attribute.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from picongpu.pypicongpu.species.attribute import Attribute - -import unittest - - -class DummyAttribute(Attribute): - def __init__(self): - pass - - -class TestSpeciesAttribute(unittest.TestCase): - def test_abstract(self): - """methods are not implemented""" - with self.assertRaises(NotImplementedError): - Attribute() - - # must not raise - DummyAttribute() diff --git a/test/python/picongpu/quick/pypicongpu/species/attribute/boundelectrons.py b/test/python/picongpu/quick/pypicongpu/species/attribute/boundelectrons.py deleted file mode 100644 index d71ab99601..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/attribute/boundelectrons.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from picongpu.pypicongpu.species.attribute import BoundElectrons, Attribute - -import unittest - - -class TestBoundElectrons(unittest.TestCase): - def test_is_attr(self): - """is an attribute""" - self.assertTrue(isinstance(BoundElectrons(), Attribute)) - - def test_basic(self): - be = BoundElectrons() - self.assertNotEqual("", be.PICONGPU_NAME) diff --git a/test/python/picongpu/quick/pypicongpu/species/attribute/momentum.py b/test/python/picongpu/quick/pypicongpu/species/attribute/momentum.py deleted file mode 100644 index 93448c0d36..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/attribute/momentum.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from picongpu.pypicongpu.species.attribute import Momentum, Attribute - -import unittest - - -class TestMomentum(unittest.TestCase): - def test_is_attr(self): - """is an attribute""" - self.assertTrue(isinstance(Momentum(), Attribute)) - - def test_basic(self): - m = Momentum() - self.assertNotEqual("", m.PICONGPU_NAME) diff --git a/test/python/picongpu/quick/pypicongpu/species/attribute/position.py b/test/python/picongpu/quick/pypicongpu/species/attribute/position.py deleted file mode 100644 index 2d0168729d..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/attribute/position.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from picongpu.pypicongpu.species.attribute import Position, Attribute - -import unittest - - -class TestPosition(unittest.TestCase): - def test_is_attr(self): - """is an attribute""" - self.assertTrue(isinstance(Position(), Attribute)) - - def test_basic(self): - pos = Position() - self.assertNotEqual("", pos.PICONGPU_NAME) diff --git a/test/python/picongpu/quick/pypicongpu/species/attribute/weighting.py b/test/python/picongpu/quick/pypicongpu/species/attribute/weighting.py deleted file mode 100644 index 601571e272..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/attribute/weighting.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from picongpu.pypicongpu.species.attribute import Weighting, Attribute - -import unittest - - -class TestWeighting(unittest.TestCase): - def test_is_attr(self): - """is an attribute""" - self.assertTrue(isinstance(Weighting(), Attribute)) - - def test_basic(self): - w = Weighting() - self.assertNotEqual("", w.PICONGPU_NAME) diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/__init__.py b/test/python/picongpu/quick/pypicongpu/species/constant/__init__.py deleted file mode 100644 index a8e641905b..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/constant/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# flake8: noqa -from .mass import * -from .charge import * -from .densityratio import * -from .elementproperties import * -from .groundstateionization import * -from .elementproperties import * -from .ionizationmodel import * diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/charge.py b/test/python/picongpu/quick/pypicongpu/species/constant/charge.py deleted file mode 100644 index dc6e17e701..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/constant/charge.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -import unittest - -from picongpu.pypicongpu.species.constant import Charge -from pydantic import ValidationError - - -class TestCharge(unittest.TestCase): - def test_basic(self): - c = Charge(charge_si=0) - self.assertEqual([], c.get_species_dependencies()) - self.assertEqual([], c.get_attribute_dependencies()) - self.assertEqual([], c.get_constant_dependencies()) - - def test_types(self): - """types are checked""" - for invalid in [None, [], {}]: - with self.assertRaises(ValidationError): - Charge(charge_si=invalid) - - def test_rendering(self): - """rendering passes information through""" - c = Charge(charge_si=-3.2) - - context = c.get_rendering_context() - self.assertAlmostEqual(-3.2, context["charge_si"]) diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/densityratio.py b/test/python/picongpu/quick/pypicongpu/species/constant/densityratio.py deleted file mode 100644 index 36d83b4edb..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/constant/densityratio.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -import unittest - -from pydantic import ValidationError -from picongpu.pypicongpu.species.constant import DensityRatio - - -class TestDensityRatio(unittest.TestCase): - def test_basic(self): - """simple example""" - dr = DensityRatio(ratio=1.0) - self.assertEqual([], dr.get_species_dependencies()) - self.assertEqual([], dr.get_attribute_dependencies()) - self.assertEqual([], dr.get_constant_dependencies()) - - def test_types(self): - """type safety ensured""" - for invalid in [None, "asbd", [], {}]: - with self.assertRaises(ValidationError): - DensityRatio(ratio=invalid) - - for valid_type in [1, 171238]: - DensityRatio(ratio=valid_type) - - def test_value_range(self): - """negative values prohibited""" - for invalid in [0, -1, -0.00000001]: - with self.assertRaises(ValidationError): - DensityRatio(ratio=invalid) - - for valid in [0.000001, 2, 3.5]: - DensityRatio(ratio=valid) - - def test_rendering_passthru(self): - """context passes ratio through""" - dr = DensityRatio(ratio=13.37) - context = dr.get_rendering_context() - self.assertAlmostEqual(13.37, context["ratio"]) diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/elementproperties.py b/test/python/picongpu/quick/pypicongpu/species/constant/elementproperties.py deleted file mode 100644 index b2da756682..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/constant/elementproperties.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -import unittest - -from pydantic import ValidationError -from picongpu.pypicongpu.species.constant import ElementProperties -from picongpu.pypicongpu.species.util import Element - - -class TestElementProperties(unittest.TestCase): - def test_basic(self): - """basic operation""" - ep = ElementProperties(element=Element("H")) - self.assertEqual([], ep.get_species_dependencies()) - self.assertEqual([], ep.get_attribute_dependencies()) - self.assertEqual([], ep.get_constant_dependencies()) - - def test_rendering(self): - """members are exposed""" - ep = ElementProperties(element=Element("N")) - context = ep.get_rendering_context() - self.assertEqual(ep.element.get_rendering_context(), context["element"]) - - def test_typesafety(self): - """typesafety is ensured""" - for invalid in [None, 1, "H", []]: - with self.assertRaises(ValidationError): - ElementProperties(element=invalid) - for invalid in [{}]: - with self.assertRaises(TypeError): - ElementProperties(element=invalid) diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/groundstateionization.py b/test/python/picongpu/quick/pypicongpu/species/constant/groundstateionization.py deleted file mode 100644 index 7ab4c466b4..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/constant/groundstateionization.py +++ /dev/null @@ -1,140 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from picongpu.pypicongpu.species import Species -from picongpu.pypicongpu.species.attribute import Position, Momentum, BoundElectrons -from picongpu.pypicongpu.species.constant import Mass, Charge, GroundStateIonization, ElementProperties -from picongpu.pypicongpu.species.constant.ionizationmodel import BSI, BSIStarkShifted, ThomasFermi -from picongpu.pypicongpu.species.constant.ionizationcurrent import None_ -from picongpu.picmi import constants - -import unittest -import pydantic_core - - -class TestGroundStateIonization(unittest.TestCase): - # set maximum length to infinite to get sensible error message on fail - maxDiff = None - - def setUp(self): - electron = Species() - electron.name = "e" - mass_constant = Mass(mass_si=constants.m_e) - charge_constant = Charge(charge_si=constants.q_e) - electron.constants = [ - charge_constant, - mass_constant, - ] - - self.electron = electron - - self.BSI_instance = BSI(ionization_electron_species=self.electron, ionization_current=None_()) - self.BSIstark_instance = BSIStarkShifted(ionization_electron_species=self.electron, ionization_current=None_()) - self.thomas_fermi_instance = ThomasFermi(ionization_electron_species=self.electron) - - def test_basic(self): - """we may create basic Instance""" - # test we may create GroundStateIonization - GroundStateIonization(ionization_model_list=[self.BSI_instance]) - - def test_type_safety(self): - """may only add list of IonizationModel instances""" - - for invalid in ["BSI", ["BSI"], [1], 1.0, self.BSI_instance]: - with self.assertRaises(pydantic_core._pydantic_core.ValidationError): - GroundStateIonization(ionization_model_list=invalid) - - def test_check_empty_ionization_model_list(self): - """empty ionization model list is not allowed""" - - # assignment is possible - instance = GroundStateIonization(ionization_model_list=[]) - - with self.assertRaisesRegex( - ValueError, ".*at least one ionization model must be specified if ground_state_ionization is not none.*" - ): - # but check throws error - instance.check() - - def test_check_doubled_up_model_group(self): - """may not assign more than one ionization model from the same group""" - - # assignment is possible - instance = GroundStateIonization( - ionization_model_list=[self.BSI_instance, self.BSIstark_instance, self.thomas_fermi_instance] - ) - - with self.assertRaisesRegex(ValueError, ".*ionization model group already represented: BSI.*"): - # but check throws - instance.check() - - def test_check_call_on_ionization_model(self): - """check method of ionization models is called""" - - # creation is possible will only raise in check method - invalid_ionization_model = BSI(ionization_electron_species=None, ionization_current=None_()) - - # assignment is allowed - instance = GroundStateIonization(ionization_model_list=[invalid_ionization_model]) - with self.assertRaisesRegex(TypeError, ".*ionization_electron_species must be of type pypicongpu Species.*"): - # but check throws error - instance.check() - - def test_species_dependencies(self): - """correct return""" - self.assertEqual( - GroundStateIonization(ionization_model_list=[self.BSI_instance]).get_species_dependencies(), [self.electron] - ) - - def test_attribute_dependencies(self): - """correct return""" - self.assertEqual( - GroundStateIonization(ionization_model_list=[self.BSI_instance]).get_attribute_dependencies(), - [BoundElectrons], - ) - - def test_constant_dependencies(self): - """correct return""" - self.assertEqual( - GroundStateIonization(ionization_model_list=[self.BSI_instance]).get_constant_dependencies(), - [ElementProperties], - ) - - def test_rendering(self): - """rendering may be called and returns correct context""" - # complete configuration of electron species - electron = self.BSI_instance.ionization_electron_species - electron.attributes = [Position(), Momentum()] - - context = GroundStateIonization(ionization_model_list=[self.BSI_instance]).get_rendering_context() - - expected_context = { - "ionization_model_list": [ - { - "ionizer_picongpu_name": "BSI", - "ionization_electron_species": { - "name": "e", - "shape": "TSC", - "typename": "species_e", - "attributes": [ - {"picongpu_name": "position"}, - {"picongpu_name": "momentum"}, - ], - "constants": { - "mass": {"mass_si": constants.m_e}, - "charge": {"charge_si": constants.q_e}, - "density_ratio": None, - "element_properties": None, - "ground_state_ionization": None, - }, - }, - "ionization_current": {"picongpu_name": "None"}, - } - ] - } - - self.assertEqual(context, expected_context) diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/__init__.py b/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/__init__.py deleted file mode 100644 index 7c484dcb6b..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# flake8: noqa -from .ionizationmodelgroups import * -from .ionizationmodel import * -from .ionizationmodelimplementations import * diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py b/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py deleted file mode 100644 index 7d27152ecd..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py +++ /dev/null @@ -1,166 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2024-2024 PIConGPU contributors -Authors: Brian Edward Marre -License: GPLv3+ -""" - -from picongpu.pypicongpu.species.constant.ionizationmodel import IonizationModel - -from picongpu.pypicongpu.species import Species -from picongpu.pypicongpu.species.constant import Mass, Charge, ElementProperties, GroundStateIonization -from picongpu.pypicongpu.species.attribute import Position, Momentum, BoundElectrons -from picongpu.picmi import constants - -import pydantic_core - -import unittest - - -# raw implementation for testing -class Implementation(IonizationModel): - PICONGPU_NAME: str = "test" - - -class Test_IonizationModel(unittest.TestCase): - def setUp(self): - electron = Species() - electron.name = "e" - mass_constant = Mass(mass_si=constants.m_e) - charge_constant = Charge(charge_si=constants.q_e) - electron.constants = [ - charge_constant, - mass_constant, - ] - # note: attributes not set yet (as would be in init manager) - - self.electron = electron - - def test_not_constructible(self): - with self.assertRaises(Exception): - IonizationModel() - - def test_basic(self): - """simple operation""" - # note: electrons are not checked, because they are not fully - # initialized yet - - instance = Implementation() - instance.ionization_electron_species = self.electron - instance.check() - - self.assertEqual("test", instance.PICONGPU_NAME) - - self.assertEqual([self.electron], instance.get_species_dependencies()) - self.assertEqual([BoundElectrons], instance.get_attribute_dependencies()) - self.assertEqual([ElementProperties], instance.get_constant_dependencies()) - - def test_empty(self): - """electron species is mandatory""" - instance = Implementation() - - # must fail: - with self.assertRaises(Exception): - instance.check() - with self.assertRaises(Exception): - instance.get_species_dependencies() - - # now passes - instance.ionization_electron_species = self.electron - instance.check() - - def test_typesafety(self): - """types are checked""" - instance = Implementation() - for invalid in ["electron", {}, [], 0, None]: - with self.assertRaises(TypeError): - # note: circular imports would be required to use the - # pypicongpu-standard build_typesafe_property, hence the type - # is checked by check() instead of on assignment (as usual) - instance.ionization_electron_species = invalid - instance.check() - - for invalid in ["ionization_current", {}, [], 0]: - with self.assertRaises(pydantic_core._pydantic_core.ValidationError): - # note: circular imports would be required to use the - # pypicongpu-standard build_typesafe_property, hence the type - # is checked by check() instead of on assignment (as usual) - Implementation(ionization_electron_species=self.electron, ionization_current=invalid) - - def test_circular_ionization(self): - """electron species must not be ionizable itself""" - other_electron = Species() - other_electron.name = "e" - mass_constant = Mass(mass_si=constants.m_e) - charge_constant = Charge(charge_si=constants.q_e) - other_electron.constants = [ - charge_constant, - mass_constant, - ] - # note: attributes not set yet, as would be case in init manager - - instance_transitive_const = Implementation() - instance_transitive_const.ionization_electron_species = other_electron - - self.electron.constants.append(GroundStateIonization(ionization_model_list=[instance_transitive_const])) - - # original instance is valid - instance_transitive_const.check() - - # ...but a constant using an ionizable species as electrons must reject - instance = Implementation() - instance.ionization_electron_species = self.electron - with self.assertRaisesRegex(ValueError, ".*ionizable.*"): - instance.check() - - def test_check_passthru(self): - """calls check of electron species & checks during rendering""" - instance = Implementation() - instance.ionization_electron_species = self.electron - - # both pass: - instance.check() - self.assertNotEqual([], instance.get_species_dependencies()) - - # with a broken species... - instance.ionization_electron_species = None - # ...check()... - with self.assertRaises(Exception): - instance.check() - - # ...and get dependencies fail - with self.assertRaises(Exception): - instance.get_species_dependencies() - - def test_rendering(self): - """renders to rendering context""" - # prepare electron species s.t. it can be rendered - self.electron.attributes = [Position(), Momentum()] - # must pass - self.electron.check() - self.assertNotEqual({}, self.electron.get_rendering_context()) - - instance = Implementation() - instance.ionization_electron_species = self.electron - - context = instance.get_rendering_context() - self.assertNotEqual({}, context) - self.assertEqual(self.electron.get_rendering_context(), context["ionization_electron_species"]) - - # do *NOT* render if check() does not pass - instance.ionization_electron_species = None - with self.assertRaises(TypeError): - instance.check() - with self.assertRaises(TypeError): - instance.get_rendering_context() - - # pass again - instance.ionization_electron_species = self.electron - instance.check() - - # do *NOT* render if electron species is broken - instance.ionization_electron_species.attributes = [] - with self.assertRaises(ValueError): - instance.ionization_electron_species.check() - with self.assertRaises(ValueError): - instance.get_rendering_context() diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelgroups.py b/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelgroups.py deleted file mode 100644 index 59a5a4f8f4..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelgroups.py +++ /dev/null @@ -1,104 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2024-2024 PIConGPU contributors -Authors: Brian Edward Marre -License: GPLv3+ -""" - -from picongpu.pypicongpu.species.constant.ionizationmodel import IonizationModelGroups - -from picongpu.pypicongpu.species.constant.ionizationmodel import BSI, BSIEffectiveZ, BSIStarkShifted -from picongpu.pypicongpu.species.constant.ionizationmodel import ADKLinearPolarization, ADKCircularPolarization -from picongpu.pypicongpu.species.constant.ionizationmodel import Keldysh, ThomasFermi - -import unittest -import copy - - -class Test_IonizationModelGroups(unittest.TestCase): - def setUp(self): - self.expected_groups_custom = { - "1": [BSI], - "2": [ADKLinearPolarization, ADKCircularPolarization], - } - - self.expected_groups_standard = { - "BSI_like": [BSI, BSIEffectiveZ, BSIStarkShifted], - "ADK_like": [ADKLinearPolarization, ADKCircularPolarization], - "Keldysh_like": [Keldysh], - "electronic_collisional_equilibrium": [ThomasFermi], - } - - self.expected_by_model_custom = { - BSI: "1", - ADKCircularPolarization: "2", - ADKLinearPolarization: "2", - } - - def test_creation(self): - """may be constructed""" - # default value construction - IonizationModelGroups() - - # custom value construction - IonizationModelGroups(by_group=self.expected_groups_custom) - - def test_get_by_group(self): - """by_group is correctly returned""" - self.assertEqual(IonizationModelGroups().get_by_group(), self.expected_groups_standard) - self.assertEqual( - IonizationModelGroups(by_group=self.expected_groups_custom).get_by_group(), self.expected_groups_custom - ) - - def test_get_by_model(self): - """by_group is correctly converted to by_model""" - self.assertEqual( - IonizationModelGroups(by_group=self.expected_groups_custom).get_by_model(), self.expected_by_model_custom - ) - - def _switch_groups(self, result, one, two): - keys = list(result.keys()) - values = list(result.values()) - - first_group = keys[one] - second_group = keys[two] - - first_models = values[one] - second_models = values[two] - - result[first_group] = second_models - result[second_group] = first_models - - return result - - def test_get_by_group_returns_copy(self): - """get_by_group() return copies only""" - ionization_model_group = IonizationModelGroups(by_group=self.expected_groups_custom) - - # get result - result = ionization_model_group.get_by_group() - - # make copy for reference - result_copy = copy.copy(result) - - # manipulate result - result = self._switch_groups(result, 0, 1) - - # check output is unchanged - self.assertEqual(result_copy, ionization_model_group.get_by_group()) - - def test_get_by_model_returns_copy(self): - """get_by_model returns copies only""" - ionization_model_group = IonizationModelGroups(by_group=self.expected_groups_custom) - - # get result - result = ionization_model_group.get_by_model() - - # make copy for reference - result_copy = copy.copy(result) - - # manipulate result - result = self._switch_groups(result, 0, 1) - - # check output is unchanged - self.assertEqual(result_copy, ionization_model_group.get_by_model()) diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelimplementations.py b/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelimplementations.py deleted file mode 100644 index c2e346fb2d..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelimplementations.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2024-2024 PIConGPU contributors -Authors: Brian Edward Marre -License: GPLv3+ -""" - -from picongpu.pypicongpu.species.constant.ionizationmodel import BSI, BSIEffectiveZ, BSIStarkShifted -from picongpu.pypicongpu.species.constant.ionizationmodel import ADKLinearPolarization, ADKCircularPolarization -from picongpu.pypicongpu.species.constant.ionizationmodel import Keldysh, ThomasFermi -from picongpu.pypicongpu.species.constant.ionizationcurrent import None_ -from picongpu.pypicongpu.species.constant import Charge, Mass -from picongpu.pypicongpu.species import Species -from picongpu.picmi import constants - -import unittest - - -class Test_IonizationModelImplementations(unittest.TestCase): - implementations_withIonizationCurrent = { - BSI: "BSI", - BSIEffectiveZ: "BSIEffectiveZ", - BSIStarkShifted: "BSIStarkShifted", - ADKCircularPolarization: "ADKLinPol", - ADKLinearPolarization: "ADKCircPol", - Keldysh: "Keldysh", - } - - implementations_withoutIonizationCurrent = {ThomasFermi: "ThomasFermi"} - - def setUp(self): - electron = Species() - electron.name = "e" - mass_constant = Mass(mass_si=constants.m_e) - charge_constant = Charge(charge_si=constants.q_e) - electron.constants = [ - charge_constant, - mass_constant, - ] - # note: attributes not set yet (as would be in init manager) - - self.electron = electron - - def test_ionizationCurrentRequired(self): - """ionization current must be explicitly configured""" - for Implementation in self.implementations_withIonizationCurrent.keys(): - with self.assertRaisesRegex(Exception, ".*ionization_current.*"): - implementation = Implementation(ionization_electron_species=self.electron) - # do not call get_rendering_context, since species not completely initialized yet - implementation.check() - - def test_basic(self): - """may create and serialize""" - for Implementation in self.implementations_withIonizationCurrent.keys(): - implementation = Implementation(ionization_electron_species=self.electron, ionization_current=None_()) - implementation.check() - - for Implementation in self.implementations_withoutIonizationCurrent.keys(): - implementation = Implementation(ionization_electron_species=self.electron) - implementation.check() - - def test_picongpu_name(self): - for Implementation, name in self.implementations_withoutIonizationCurrent.items(): - self.assertEqual( - name, - Implementation(ionization_electron_species=self.electron, ionization_current=None_()).PICONGPU_NAME, - ) - for Implementation, name in self.implementations_withoutIonizationCurrent.items(): - self.assertEqual(name, Implementation(ionization_electron_species=self.electron).PICONGPU_NAME) diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/mass.py b/test/python/picongpu/quick/pypicongpu/species/constant/mass.py deleted file mode 100644 index 70aa09eb32..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/constant/mass.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from pydantic import ValidationError -from picongpu.pypicongpu.species.constant import Mass - -import unittest - - -class TestMass(unittest.TestCase): - def test_basic(self): - m = Mass(mass_si=17) - # passes - self.assertEqual([], m.get_species_dependencies()) - self.assertEqual([], m.get_attribute_dependencies()) - self.assertEqual([], m.get_constant_dependencies()) - - def test_type(self): - """types are checked""" - for invalid in [None, [], {}]: - with self.assertRaises(ValidationError): - Mass(mass_si=invalid) - - def test_values(self): - """invalid values are rejected""" - for invalid in [-1, 0, -0.0000001]: - with self.assertRaises(ValidationError): - Mass(mass_si=invalid) - - def test_rendering(self): - """passes value through""" - m = Mass(mass_si=1337) - - context = m.get_rendering_context() - self.assertEqual(1337, context["mass_si"]) diff --git a/test/python/picongpu/quick/pypicongpu/species/initmanager.py b/test/python/picongpu/quick/pypicongpu/species/initmanager.py deleted file mode 100644 index 95350c8bcb..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/initmanager.py +++ /dev/null @@ -1,898 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from picongpu.pypicongpu.species import InitManager - -import unittest - -from .attribute import DummyAttribute - -from picongpu.pypicongpu import species -from picongpu.pypicongpu.species.attribute import Position, Momentum -from picongpu.pypicongpu.species.constant import Mass, Charge -from picongpu.pypicongpu.species.operation import ( - SimpleDensity, - SimpleMomentum, - NotPlaced, - densityprofile, -) -from picongpu.pypicongpu.species.operation.layout import Random - -import typing -import typeguard - - -class TestInitManager(unittest.TestCase): - @typeguard.typechecked - class ConstantWithDependencies(species.constant.Constant): - def __init__(self, dependencies=[]): - if type(dependencies) is not list: - self.dependencies = [dependencies] - else: - self.dependencies = dependencies - - self.attribute_dependencies = [] - self.constant_dependencies = [] - self.constant_dependencies_called = 0 - self.attribute_dependencies_called = 0 - - def check(self): - pass - - def get_species_dependencies(self) -> typing.List[species.Species]: - return self.dependencies - - def get_attribute_dependencies(self) -> typing.List[type]: - self.attribute_dependencies_called += 1 - return self.attribute_dependencies - - def get_constant_dependencies(self) -> typing.List[type]: - self.constant_dependencies_called += 1 - return self.constant_dependencies - - class OperationInvalidBehavior(species.operation.Operation): - def __init__(self, species_list=[]): - self.species_list = species_list - self.check_adds_attribute = False - self.prebook_adds_attribute = False - - def __species_add_attr(self): - for spec in self.species_list: - attr = DummyAttribute() - # -> search for "IDSTRINGDZ" in error message - attr.PICONGPU_NAME = "this_attr_is_added_too_early__IDSTRINGDZ" - spec.attributes = [attr] - - def check_preconditions(self): - if self.check_adds_attribute: - self.__species_add_attr() - - def prebook_species_attributes(self): - if self.prebook_adds_attribute: - self.__species_add_attr() - - class OperationCallTracer(species.operation.Operation): - def __init__(self, unique_id="", species_list=[]): - # record order of function calls - self.calls = ["init"] - self.abort_check = False - self.abort_prebook = False - self.species_list = species_list - self.unique_id = unique_id - - def __hash__(self) -> int: - return hash(self.unique_id) - - def get_attr_name(self): - return "tracer_attr_" + self.unique_id - - def check_preconditions(self): - self.calls.append("check") - assert not self.abort_check, "IDSTRING_FROM_CHECK" - - def prebook_species_attributes(self): - self.calls.append("prebook") - assert not self.abort_prebook, "IDSTRING_FROM_PREBOOK" - - self.attributes_by_species = {} - for spec in self.species_list: - attr = DummyAttribute() - attr.PICONGPU_NAME = self.get_attr_name() - self.attributes_by_species[spec] = [attr] - - class OperationAddMandatoryAttributes(species.operation.Operation): - def __init__(self, species_list=[]): - self.species_list = species_list - - def __hash__(self): - return_hash_value = hash(type(hash)) - for species_ in self.species_list: - return_hash_value += hash(species_) - return return_hash_value - - def check_preconditions(self): - pass - - def prebook_species_attributes(self): - self.attributes_by_species = {} - for spec in self.species_list: - self.attributes_by_species[spec] = [Position(), Momentum()] - - def __get_five_simple_species(self): - """helper to build dependency graphs""" - all_species = [] - - for i in range(5): - new_species = species.Species() - new_species.name = "species{}".format(i) - new_species.constants = [] - # note: attributes left intentionally undefined, as they would be - # when generating from picongpu.pypicongpu - - all_species.append(new_species) - - return tuple(all_species) - - def setUp(self): - self.species1 = species.Species() - self.species1.name = "species1" - self.species1.constants = [] - self.species1_copy = species.Species() - self.species1_copy.name = "species1" - self.species1_copy.constants = [] - self.species2 = species.Species() - self.species2.name = "species2" - self.species2.constants = [] - self.attribute1 = DummyAttribute() - self.attribute1.PICONGPU_NAME = "attribute1" - - self.initmgr = InitManager() - - def test_setup(self): - """setUp() provides a working initmanager""" - # passes silently - self.initmgr.bake() - # implicitly calls checks - self.initmgr.get_rendering_context() - - def test_nameconflicts(self): - """species names must be unique""" - initmgr = self.initmgr - initmgr.all_species = [self.species1, self.species1_copy] - initmgr.all_operations = [self.OperationAddMandatoryAttributes(initmgr.all_species)] - self.assertTrue(initmgr.all_species[0] is not initmgr.all_species[1]) - self.assertEqual(initmgr.all_species[0].name, initmgr.all_species[1].name) - - with self.assertRaisesRegex(ValueError, ".*unique.*species1.*"): - initmgr.bake() - - def test_check_passthru_species(self): - """species.check() is called""" - spec = self.species1 - # trigger check by setting name invalid - spec.name = "-" - with self.assertRaisesRegex(ValueError, ".*c[+][+].*"): - # message must say sth like "c++ incompatible" - spec.check() - - initmgr = self.initmgr - initmgr.all_species = [spec] - - with self.assertRaisesRegex(ValueError, ".*c[+][+].*"): - initmgr.bake() - - def test_shared_consts(self): - """multiple species can share the same const object""" - s1 = self.species1 - s2 = self.species2 - const = self.ConstantWithDependencies() - const.PICONGPU_NAME = "a single const" - - s1.constants = [const] - s2.constants = [const] - - # they use **the same** object - self.assertTrue(s1.constants[0] is s2.constants[0]) - - initmgr = self.initmgr - initmgr.all_species = [s1, s2] - initmgr.all_operations = [self.OperationAddMandatoryAttributes(initmgr.all_species)] - - # simply works - initmgr.bake() - - def test_operation_lifecycle_normal(self): - """operation methods are called in the right order""" - initmgr = self.initmgr - initmgr.all_species = [self.species1] - - # create multiple tracer operations - tracer1 = self.OperationCallTracer("1", initmgr.all_species) - tracer2 = self.OperationCallTracer("2", initmgr.all_species) - tracer3 = self.OperationCallTracer("3", initmgr.all_species) - - initmgr.all_operations = [ - tracer1, - tracer3, - tracer2, - self.OperationAddMandatoryAttributes(initmgr.all_species), - ] - - initmgr.bake() - - expected_callchain = ["init", "check", "prebook"] - self.assertEqual(expected_callchain, tracer1.calls) - self.assertEqual(expected_callchain, tracer2.calls) - self.assertEqual(expected_callchain, tracer3.calls) - - # actually added the attributes - all_attr_names = list(map(lambda attr: attr.PICONGPU_NAME, self.species1.attributes)) - self.assertEqual(5, len(all_attr_names)) - self.assertTrue(tracer1.get_attr_name() in all_attr_names) - self.assertTrue(tracer2.get_attr_name() in all_attr_names) - self.assertTrue(tracer3.get_attr_name() in all_attr_names) - - def test_operation_lifecycle_aborted_check(self): - """no prebook call to any operation made if one check fails""" - initmgr = self.initmgr - initmgr.all_species = [self.species1] - - tracer_passing = self.OperationCallTracer("1", initmgr.all_species) - tracer_throwing = self.OperationCallTracer("2", initmgr.all_species) - tracer_throwing.abort_check = True - initmgr.all_operations = [tracer_passing, tracer_throwing] - - with self.assertRaisesRegex(AssertionError, "IDSTRING_FROM_CHECK"): - initmgr.bake() - - # prebook not called even for passing - self.assertTrue("prebook" not in tracer_passing.calls) - self.assertTrue("prebook" not in tracer_throwing.calls) - - def test_operation_lifecycle_aborted_prebook(self): - """no bake call to operation made if one prebook fails""" - initmgr = self.initmgr - initmgr.all_species = [self.species1] - - tracer_passing = self.OperationCallTracer("1", initmgr.all_species) - tracer_throwing = self.OperationCallTracer("2", initmgr.all_species) - tracer_throwing.abort_prebook = True - initmgr.all_operations = [tracer_passing, tracer_throwing] - - with self.assertRaisesRegex(AssertionError, "IDSTRING_FROM_PREBOOK"): - initmgr.bake() - - # check has been called... - self.assertTrue("check" in tracer_passing.calls) - self.assertTrue("check" in tracer_throwing.calls) - # at least one prebook() has been called, - # tracer_throwing failed - # -> no bake() *at all* has been called - # -> no species have been assigned *at all* - # (i.e. even if one prebook passed) - self.assertEqual([], self.species1.attributes) - - def test_operation_invalid_behavior_str(self): - """check string representation of OperationInvalidBehavior""" - # rationale: sometimes the operation name must be in an error message - # -> ensure regex-able string representation of offending operation - with self.assertRaisesRegex(ValueError, ".*OperationInvalidBehavior.*"): - raise ValueError(str(self.OperationInvalidBehavior([]))) - - def test_operation_invalid_behavior_check(self): - """if a operation check adds attributes it is reported""" - initmgr = self.initmgr - initmgr.all_species = [self.species1] - invalid = self.OperationInvalidBehavior(initmgr.all_species) - invalid.check_adds_attribute = True - initmgr.all_operations = [invalid] - - with self.assertRaisesRegex(AssertionError, ".*check.*OperationInvalidBehavior.*IDSTRINGDZ.*"): - initmgr.bake() - - def test_operation_invalid_behavior_prebook(self): - """if a operation prebook adds attributes it is reported""" - initmgr = self.initmgr - initmgr.all_species = [self.species1] - invalid = self.OperationInvalidBehavior(initmgr.all_species) - invalid.prebook_adds_attribute = True - initmgr.all_operations = [invalid] - - with self.assertRaisesRegex(AssertionError, ".*prebook.*OperationInvalidBehavior.*IDSTRINGDZ.*"): - initmgr.bake() - - def test_multiple_assigned_species(self): - """each species object may only be added once""" - initmgr = InitManager() - initmgr.all_species = [self.species1, self.species1] - initmgr.all_operations = [self.OperationAddMandatoryAttributes([self.species1])] - - with self.assertRaisesRegex(ValueError, ".*[Ss]pecies.*once.*species1.*"): - # duplicate species - initmgr.bake() - - # but works if deduplicated - initmgr.all_species = list(set(initmgr.all_species)) - initmgr.bake() - - def test_multiple_assigned_operation(self): - """each operation object may only be added once""" - initmgr = InitManager() - initmgr.all_species = [self.species1] - tracer = self.OperationCallTracer("", initmgr.all_species) - initmgr.all_operations = [ - self.OperationAddMandatoryAttributes([self.species1]), - tracer, - tracer, - ] - - with self.assertRaisesRegex(ValueError, ".*[Oo]peration.*once.*"): - # duplicate operation - initmgr.bake() - - # but works with deduplicated operations - initmgr.all_operations = list(set(initmgr.all_operations)) - self.assertEqual(2, len(initmgr.all_operations)) - initmgr.bake() - - def test_exclusiveness_checked(self): - """attributes must be exclusively owned by species""" - - class NonExclusiveOp(species.operation.Operation): - attr_for_all = DummyAttribute() - - def __init__(self, species_list=[]): - # record order of function calls - self.species_list = species_list - NonExclusiveOp.attr_for_all.PICONGPU_NAME = "exists_only_once" - - def check_preconditions(self): - pass - - def prebook_species_attributes(self): - self.attributes_by_species = {} - for spec in self.species_list: - # note: uses **global** (static) attribute object - self.attributes_by_species[spec] = [NonExclusiveOp.attr_for_all] - - initmgr = self.initmgr - initmgr.all_species = [self.species1, self.species2] - # idea: assign *the same* attribute object using *two different* - # operation objects to *two different* species - # -> operation-local checks (inside Operation.bake()) will not catch - # this - op1 = NonExclusiveOp([self.species1]) - op2 = NonExclusiveOp([self.species2]) - initmgr.all_operations = [op1, op2] - - with self.assertRaisesRegex(ValueError, ".*exclusive.*"): - initmgr.bake() - - def test_types(self): - """lists have typechecks""" - initmgr = self.initmgr - - invalid_specieslists = [] - for invalid_specieslist in invalid_specieslists: - with self.assertRaises(TypeError): - initmgr.all_species = invalid_specieslist - - invalid_oplists = [] - for invalid_oplist in invalid_oplists: - with self.assertRaises(TypeError): - initmgr.all_operations = invalid_oplist - - def test_empty(self): - """works by default""" - initmgr = InitManager() - # just works: - initmgr.bake() - - # produces empty lists for rendering - context = initmgr.get_rendering_context() - self.assertEqual([], context["species"]) - - # types of operations are listed, but the lists are empty - self.assertNotEqual(0, len(context["operations"])) - self.assertEqual([], context["operations"]["simple_density"]) - - def test_basic(self): - """valid scenario""" - initmgr = self.initmgr - initmgr.all_species = [self.species1, self.species2] - - op1 = self.OperationCallTracer("a", [self.species1]) - op2 = self.OperationCallTracer("xkcd927", [self.species2]) - op3 = self.OperationCallTracer("1337", [self.species1, self.species2]) - initmgr.all_operations = [ - op1, - op2, - op3, - self.OperationAddMandatoryAttributes([self.species1, self.species2]), - ] - - initmgr.bake() - - species1_attr_names = list(map(lambda attr: attr.PICONGPU_NAME, self.species1.attributes)) - species2_attr_names = list(map(lambda attr: attr.PICONGPU_NAME, self.species2.attributes)) - - self.assertEqual(4, len(species1_attr_names)) - self.assertTrue(op1.get_attr_name(), species1_attr_names) - self.assertTrue(op3.get_attr_name(), species1_attr_names) - - self.assertEqual(4, len(species2_attr_names)) - self.assertTrue(op2.get_attr_name(), species2_attr_names) - self.assertTrue(op3.get_attr_name(), species2_attr_names) - - def test_species_check(self): - """species incorrectly created -> species check raises""" - # mandatory "position" attribute missing - initmgr = self.initmgr - initmgr.all_species = [self.species1] - initmgr.all_operations = [] - - with self.assertRaisesRegex(ValueError, ".*[Pp]osition.*"): - initmgr.bake() - - def test_operation_conflict(self): - """conflicting operations reject""" - initmgr = self.initmgr - initmgr.all_species = [self.species1] - - # will create identical attributes - tracer1 = self.OperationCallTracer("1", initmgr.all_species) - tracer1_copy = self.OperationCallTracer("1", initmgr.all_species) - - initmgr.all_operations = [tracer1, tracer1_copy] - - attr_name = tracer1.get_attr_name() - - with self.assertRaisesRegex(ValueError, ".*conflict.*{}.*".format(attr_name)): - initmgr.bake() - - def test_unregistered_species(self): - """all species used by any operator must be known to the InitManager""" - initmgr = self.initmgr - - # species2 is not known to initmgr: - # attr list will not be initialized to [], - # might provoke "unset attribute" error - # -> circumvent this "unset attribute" error by manual initialization - # (DO NOT DO THIS AT HOME) - self.species2.attributes = [] - - initmgr.all_species = [self.species1] - - # assigns more species than known to the init manager - op = self.OperationAddMandatoryAttributes([self.species1, self.species2]) - initmgr.all_operations = [op] - - with self.assertRaisesRegex(ValueError, ".*register.*species2.*"): - initmgr.bake() - - def test_bake_twice(self): - # can only bake once - self.initmgr.bake() - - with self.assertRaisesRegex(AssertionError, ".*once.*"): - self.initmgr.bake() - - def test_rendering_implictly_bakes(self): - """rendering calls bake(), unless already called""" - initmgr = InitManager() - # directly works: - res_no_bake = initmgr.get_rendering_context() - - # also works if bake has been called previously - initmgr = InitManager() - initmgr.bake() - res_with_bake = initmgr.get_rendering_context() - - # must be **equivalent** - self.assertEqual(res_no_bake, res_with_bake) - - def test_rendering_context_passthru(self): - """renderer passes information through""" - initmgr = self.initmgr - initmgr.all_species = [self.species1, self.species2] - - simple_density = SimpleDensity() - simple_density.ppc = 927 - simple_density.profile = densityprofile.Uniform(density_si=1337) - simple_density.species = { - self.species1, - self.species2, - } - simple_density.layout = Random(ppc=1) - momentum_ops = [] - for single_species in initmgr.all_species: - simple_momentum = SimpleMomentum() - simple_momentum.species = single_species - simple_momentum.drift = None - simple_momentum.temperature = None - momentum_ops.append(simple_momentum) - - initmgr.all_operations = [simple_density] + momentum_ops - - # (implicitly bakes) - context = initmgr.get_rendering_context() - - self.assertEqual(2, len(context["species"])) - self.assertEqual(context["species"][0], self.species1.get_rendering_context()) - self.assertEqual(context["species"][1], self.species2.get_rendering_context()) - - self.assertEqual(1, len(context["operations"]["simple_density"])) - self.assertEqual( - context["operations"]["simple_density"][0], - simple_density.get_rendering_context(), - ) - - def test_rendering_context_passthru_ops(self): - """operations are passed through into their respective locations""" - initmgr = self.initmgr - initmgr.all_species = [self.species1, self.species2] - initmgr.all_operations = [] - - simple_density = SimpleDensity() - simple_density.ppc = 927 - simple_density.profile = densityprofile.Uniform(density_si=1337) - simple_density.species = { - self.species1, - } - simple_density.layout = Random(ppc=1) - initmgr.all_operations.append(simple_density) - - # store momentum ops separately for assertion later - momentum_ops = [] - for single_species in initmgr.all_species: - simple_momentum = SimpleMomentum() - simple_momentum.species = single_species - simple_momentum.drift = None - simple_momentum.temperature = None - momentum_ops.append(simple_momentum) - initmgr.all_operations += momentum_ops - - not_placed = NotPlaced() - not_placed.species = self.species2 - initmgr.all_operations.append(not_placed) - - # note: further operation passthrough tests in other methods - - # implicitly bakes - context = initmgr.get_rendering_context() - - # note: NotPlaced only adds attr and no data, hence is not in context - self.assertEqual( - simple_density.get_rendering_context(), - context["operations"]["simple_density"][0], - ) - - for momentum_op in momentum_ops: - self.assertTrue(momentum_op.get_rendering_context() in context["operations"]["simple_momentum"]) - - def test_constants_dependencies_outside(self): - """species dependencies outside of initmanager are detected""" - a, b, c, d, e = self.__get_five_simple_species() - - # a -> b -> c, but c is not registered with initmanager - a.constants = [self.ConstantWithDependencies(b)] - b.constants = [self.ConstantWithDependencies(c)] - - initmgr = InitManager() - initmgr.all_species = [a, b, d, e] - - with self.assertRaisesRegex(ReferenceError, ".*unkown.*"): - initmgr.bake() - - def test_constant_species_dependencies_circle_detection_complicated(self): - """circular dependencies are detected and caught""" - a, b, c, d, e = self.__get_five_simple_species() - - # circle a -> b -> c -> d -> a - # additionally: e -> a, e -> c - a.constants = [self.ConstantWithDependencies(b)] - b.constants = [self.ConstantWithDependencies(c)] - c.constants = [self.ConstantWithDependencies(d)] - d.constants = [self.ConstantWithDependencies(a)] - - e.constants = [self.ConstantWithDependencies([a, c])] - - initmgr = InitManager() - initmgr.all_species = [a, b, c, d, e] - - with self.assertRaisesRegex(RecursionError, ".*circular.*"): - initmgr.bake() - - def test_constant_species_dependencies_circle_detection_simple(self): - """simple circular dependencies are also caugth""" - a, b, c, d, e = self.__get_five_simple_species() - a.constants = [self.ConstantWithDependencies([a, b])] - initmgr = InitManager() - initmgr.all_species = [a, b] - - with self.assertRaisesRegex(RecursionError, ".*circular.*"): - initmgr.bake() - - def test_constant_species_dependencies_order(self): - """order between dependencies is created""" - a, b, c, d, e = self.__get_five_simple_species() - - a.constants = [self.ConstantWithDependencies(d)] - b.constants = [self.ConstantWithDependencies([e, c])] - c.constants = [self.ConstantWithDependencies(a)] - d.constants = [] - e.constants = [self.ConstantWithDependencies(d)] - - initmgr = InitManager() - initmgr.all_species = [a, b, c, d, e] - # associate ops for required attrs (to make checks pass) - initmgr.all_operations = [self.OperationAddMandatoryAttributes(initmgr.all_species)] - - # bake reorders dependencies - initmgr.bake() - baked_names_in_order = list(map(lambda species: species.name, initmgr.all_species)) - - # must be equal to context order - context = initmgr.get_rendering_context() - context_names_in_order = list(map(lambda species: species["name"], context["species"])) - self.assertEqual(baked_names_in_order, context_names_in_order) - - index_by_name = dict( - map( - lambda species_name: ( - species_name, - context_names_in_order.index(species_name), - ), - context_names_in_order, - ) - ) - - # a->0, b->1, c->2, d->3, e->4 - # expected order: d < e = a < c < b - # 3 < 4 = 0 < 2 < 1 - self.assertEqual(0, index_by_name["species3"]) - self.assertEqual({1, 2}, {index_by_name["species0"], index_by_name["species4"]}) - self.assertEqual(3, index_by_name["species2"]) - self.assertEqual(4, index_by_name["species1"]) - - def test_constant_attribute_dependencies_ok(self): - """a constant may require an attribute to be present""" - - class DummyOperation(species.operation.Operation): - def __init__(self): - pass - - def check_preconditions(self): - pass - - def prebook_species_attributes(self): - self.attributes_by_species = {self.species: [self.attr]} - - attr = DummyAttribute() - - op = DummyOperation() - op.species = self.species1 - op.attr = attr - - const = self.ConstantWithDependencies() - const.attribute_dependencies = [type(attr)] - self.species1.constants.append(const) - - initmgr = InitManager() - initmgr.all_species = [self.species1, self.species2] - initmgr.all_operations = [ - self.OperationAddMandatoryAttributes(initmgr.all_species), - op, - ] - - # species1 required "attr" to be present after generation, - # which is provided by op - - # works - initmgr.bake() - self.assertNotEqual(0, const.attribute_dependencies_called) - - def test_constant_attribute_dependencies_missing(self): - """constant requires an attribute, but it is not present""" - const = self.ConstantWithDependencies() - const.attribute_dependencies = [DummyAttribute] - self.species1.constants.append(const) - - initmgr = InitManager() - initmgr.all_species = [self.species1, self.species2] - initmgr.all_operations = [self.OperationAddMandatoryAttributes(initmgr.all_species)] - - # DummyAttribute is required for species1, but not assigned - with self.assertRaisesRegex(AssertionError, ".*species1.*"): - initmgr.bake() - - # ...but works without dependency - const.attribute_dependencies = [] - - initmgr = InitManager() - initmgr.all_species = [self.species1, self.species2] - initmgr.all_operations = [self.OperationAddMandatoryAttributes(initmgr.all_species)] - initmgr.bake() - - def test_constant_attribute_dependencies_typechecked(self): - """constant must return correct dependency list""" - invalid_attr_dependency_lists = [ - set(), - None, - [None], - ["DUMMYATTR"], - "dummyattr", - [int, str], - [Mass], - ] - for invalid_list in invalid_attr_dependency_lists: - const = self.ConstantWithDependencies() - const.attribute_dependencies = invalid_list - self.species1.constants = [const] - - initmgr = InitManager() - initmgr.all_species = [self.species1, self.species2] - initmgr.all_operations = [self.OperationAddMandatoryAttributes(initmgr.all_species)] - - with self.assertRaises(typeguard.TypeCheckError): - initmgr.bake() - - def test_constant_constant_dependencies_ok(self): - """constants requires other constant and it is present""" - mass = Mass(mass_si=1) - charge = Charge(charge_si=1) - - const_dep = self.ConstantWithDependencies() - const_dep.constant_dependencies = [Mass, Charge] - - self.species1.constants = [ - mass, - charge, - const_dep, - ] - - # has no constants (serves as distraction) - self.assertEqual([], self.species2.constants) - - initmgr = InitManager() - initmgr.all_species = [self.species1, self.species2] - initmgr.all_operations = [self.OperationAddMandatoryAttributes(initmgr.all_species)] - - # passes silently, all checks ok - initmgr.bake() - self.assertNotEqual(0, const_dep.constant_dependencies_called) - - def test_constant_constant_dependencies_missing(self): - """constants requires other constant and it is missing""" - charge = Charge(charge_si=1) - - const_dep = self.ConstantWithDependencies() - const_dep.constant_dependencies = [Mass, Charge] - - self.species1.constants = [ - const_dep, - charge, - ] - - initmgr = InitManager() - initmgr.all_species = [self.species1, self.species2] - initmgr.all_operations = [self.OperationAddMandatoryAttributes(initmgr.all_species)] - - with self.assertRaisesRegex(AssertionError, ".*species1.*Mass.*"): - initmgr.bake() - - def test_constant_constant_dependencies_circular(self): - """circular dependencies are allowed, (self references not)""" - - class OtherConstWithDeps(species.constant.Constant): - def __init__(self): - pass - - def check(self): - pass - - def get_species_dependencies(self): - return [] - - def get_attribute_dependencies(self): - return [] - - def get_constant_dependencies(self): - return self.constant_dependencies - - const1_dep = self.ConstantWithDependencies() - const1_dep.constant_dependencies = [OtherConstWithDeps] - - const2_dep = OtherConstWithDeps() - const2_dep.constant_dependencies = [self.ConstantWithDependencies] - - self.species1.constants = [ - const1_dep, - const2_dep, - ] - - initmgr = InitManager() - initmgr.all_species = [self.species1, self.species2] - initmgr.all_operations = [self.OperationAddMandatoryAttributes(initmgr.all_species)] - - # works - initmgr.bake() - self.assertNotEqual(0, const1_dep.constant_dependencies_called) - - def test_constant_constant_dependencies_self(self): - """self-references in dependencies are not allowed (circular are)""" - const_dep = self.ConstantWithDependencies() - const_dep.constant_dependencies = [self.ConstantWithDependencies] - - self.species1.constants = [ - const_dep, - ] - - initmgr = InitManager() - initmgr.all_species = [self.species1] - initmgr.all_operations = [self.OperationAddMandatoryAttributes(initmgr.all_species)] - - # self-reference is detected and caught - with self.assertRaisesRegex(ReferenceError, ".*(self|selve).*"): - initmgr.bake() - - def test_constant_constant_dependencies_typechecked(self): - """the constant-constant dependency interface is typchecked""" - invalid_constant_dependency_lists = [ - set(), - None, - [None], - ["DUMMYCONST"], - "dummyconst", - [int, str], - [Position], - ] - for invalid_list in invalid_constant_dependency_lists: - const = self.ConstantWithDependencies() - const.constant_dependencies = invalid_list - self.species1.constants = [const] - - initmgr = InitManager() - initmgr.all_species = [self.species1, self.species2] - initmgr.all_operations = [self.OperationAddMandatoryAttributes(initmgr.all_species)] - - with self.assertRaises(typeguard.TypeCheckError): - initmgr.bake() - - def test_set_charge_state_passthrough(self): - """bound electrons operation is included in rendering context""" - # create full electron species - electron = species.Species() - electron.name = "e" - electron.constants = [] - - ion = species.Species() - ion.name = "ion" - ionizers_const = species.constant.GroundStateIonization( - ionization_model_list=[species.constant.ionizationmodel.ThomasFermi()] - ) - ionizers_const.ionization_model_list[0].ionization_electron_species = electron - element_const = species.constant.ElementProperties(element=species.util.Element("N")) - ion.constants = [ionizers_const, element_const] - - ion_op = species.operation.SetChargeState() - ion_op.species = ion - ion_op.charge_state = 2 - - initmgr = InitManager() - initmgr.all_species = [electron, ion] - initmgr.all_operations = [ - self.OperationAddMandatoryAttributes(initmgr.all_species), - ion_op, - ] - - context = initmgr.get_rendering_context() - - self.assertEqual( - [ion_op.get_rendering_context()], - context["operations"]["set_charge_state"], - ) diff --git a/test/python/picongpu/quick/pypicongpu/species/operation/__init__.py b/test/python/picongpu/quick/pypicongpu/species/operation/__init__.py deleted file mode 100644 index 1876a93608..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/operation/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# flake8: noqa -from .operation import * # pyflakes.ignore -from .densityprofile import * # pyflakes.ignore -from .simpledensity import * # pyflakes.ignore -from .notplaced import * # pyflakes.ignore -from .momentum import * # pyflakes.ignore -from .simplemomentum import * # pyflakes.ignore -from .noboundelectrons import * # pyflakes.ignore -from .setchargestate import * # pyflakes.ignore diff --git a/test/python/picongpu/quick/pypicongpu/species/operation/densityprofile/__init__.py b/test/python/picongpu/quick/pypicongpu/species/operation/densityprofile/__init__.py deleted file mode 100644 index 166a2b2104..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/operation/densityprofile/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# flake8: noqa -from .densityprofile import * # pyflakes.ignore -from .uniform import * # pyflakes.ignore -from .foil import * # pyflakes.ignore -from .gaussian import * # pyflakes.ignore -from .cylinder import * # pyflakes.ignore diff --git a/test/python/picongpu/quick/pypicongpu/species/operation/densityprofile/cylinder.py b/test/python/picongpu/quick/pypicongpu/species/operation/densityprofile/cylinder.py deleted file mode 100644 index fa58084def..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/operation/densityprofile/cylinder.py +++ /dev/null @@ -1,91 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2024-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -import unittest -from pydantic import ValidationError - -from picongpu.pypicongpu.species.operation.densityprofile.cylinder import Cylinder -from picongpu.pypicongpu.species.operation.densityprofile.plasmaramp import None_ - -KWARGS = dict( - density_si=1.0e20, - center_position_si=(0.0, 0.0, 0.0), - radius_si=1.0e-3, - cylinder_axis=(0.0, 1.0, 0.0), - pre_plasma_ramp=None_(), -) - - -class TestCylinder(unittest.TestCase): - def test_value_pass_through(self): - """values are passed through correctly""" - c = Cylinder( - density_si=2.5e23, - center_position_si=(1.0, 2.0, 3.0), - radius_si=5.0e-4, - cylinder_axis=(1.0, 0.0, 0.0), - pre_plasma_ramp=None_(), - ) - - self.assertAlmostEqual(2.5e23, c.density_si) - self.assertEqual((1.0, 2.0, 3.0), c.center_position_si) - self.assertAlmostEqual(5.0e-4, c.radius_si) - self.assertEqual((1.0, 0.0, 0.0), c.cylinder_axis) - self.assertEqual(None_(), c.pre_plasma_ramp) - - def test_typesafety(self): - """typesafety is ensured""" - for invalid in [None, "str", [], {}]: - with self.assertRaises(ValidationError): - Cylinder(**KWARGS | dict(density_si=invalid)) - - for invalid in [None, "str", [], (1.0), (1.0, 2.0)]: - with self.assertRaises(ValidationError): - Cylinder(**KWARGS | dict(center_position_si=invalid)) - - for invalid in [None, "str", [], {}]: - with self.assertRaises(ValidationError): - Cylinder(**KWARGS | dict(radius_si=invalid)) - - for invalid in [None, "str", [], (1.0, 2.0)]: - with self.assertRaises(ValidationError): - Cylinder(**KWARGS | dict(cylinder_axis=invalid)) - - for invalid in [None, "str", [], 0]: - with self.assertRaises(ValidationError): - Cylinder(**KWARGS | dict(pre_plasma_ramp=invalid)) - - def test_check_density(self): - """invalid density""" - for invalid in [0, -1, -1.0e20]: - with self.assertRaises(ValidationError): - Cylinder(**KWARGS | dict(density_si=invalid)) - - def test_check_radius(self): - """invalid radius""" - for invalid in [-1e-5, -123.0]: - with self.assertRaises(ValidationError): - Cylinder(**KWARGS | dict(radius_si=invalid)) - - def test_rendering(self): - """check rendering context""" - c = Cylinder(**KWARGS) - context = c.get_rendering_context() - # optional: verify some structure - self.assertTrue(context.get("typeID", {}).get("cylinder", False)) - data = context["data"] - self.assertAlmostEqual(c.density_si, data["density_si"]) - self.assertEqual([{"component": pos} for pos in c.center_position_si], data["center_position_si"]) - self.assertAlmostEqual(c.radius_si, data["radius_si"]) - self.assertEqual([{"component": ax} for ax in c.cylinder_axis], data["cylinder_axis"]) - - # expected "no ramp" structure - expectedContextNoRamp = { - "typeID": {"exponential": False, "none": True}, - "data": None, - } - self.assertEqual(expectedContextNoRamp, data["pre_plasma_ramp"]) diff --git a/test/python/picongpu/quick/pypicongpu/species/operation/densityprofile/densityprofile.py b/test/python/picongpu/quick/pypicongpu/species/operation/densityprofile/densityprofile.py deleted file mode 100644 index c1b2a27a4e..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/operation/densityprofile/densityprofile.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2025 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre, Julian Lenz -License: GPLv3+ -""" - -from picongpu.pypicongpu.species.operation.densityprofile import DensityProfile - -import unittest - -from picongpu.pypicongpu.species.operation.densityprofile import Uniform -from picongpu.pypicongpu.rendering import RenderedObject - -import referencing - - -class TestDensityProfile(unittest.TestCase): - def tearDown(self): - # rendering with type test tampers with the schema store: - # reset it after tests - RenderedObject._schemas_loaded = False - RenderedObject._schema_by_uri = {} - - class DummyCheckNotImplemented(DensityProfile): - def __init__(self): - pass - - def test_rendering_not_implemented(self): - """rendering method is defined, but not implemented""" - dummy = self.DummyCheckNotImplemented() - - with self.assertRaises(NotImplementedError): - dummy.get_rendering_context() - - def test_rendering_with_type(self): - """object with added type information is returned & validated""" - # note: use valid objects here b/c the schema enforces non-dummy types - uniform = Uniform(density_si=1) - - # schemas must be loaded by context request - RenderedObject._schemas_loaded = False - RenderedObject._registry = referencing.Registry() - - context = uniform.get_rendering_context() - - # schemas now loaded - self.assertTrue(RenderedObject._schemas_loaded) - - self.assertEqual(context["data"], uniform.model_dump(mode="json")) - - # contains information on all types - self.assertEqual( - context["typeID"], - { - "uniform": True, - "foil": False, - "gaussian": False, - "cylinder": False, - "freeformula": False, - }, - ) - - # is actually validated against "DensityProfile" schema - RenderedObject._schemas_loaded = False - schema = RenderedObject._get_schema_from_class(DensityProfile) - - # check 1: schema actually enforce existance of all keys - self.assertTrue("uniform" in schema["properties"]["typeID"]["required"]) - # TODO: copy line above for more types - - # check 1b: all keys that are available for the "type" dict are - # required - self.assertEqual( - set(schema["properties"]["typeID"]["required"]), - set(schema["properties"]["typeID"]["properties"].keys()), - ) - - # check 2: break the schema, schema rejects everything now - RenderedObject._registry = referencing.Registry() - with self.assertRaises(referencing.exceptions.NoSuchResource): - # schema now rejects everything - # -> must also reject previously correct context - uniform.get_rendering_context() diff --git a/test/python/picongpu/quick/pypicongpu/species/operation/densityprofile/foil.py b/test/python/picongpu/quick/pypicongpu/species/operation/densityprofile/foil.py deleted file mode 100644 index 2bf8386af2..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/operation/densityprofile/foil.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2024-2025 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre, Julian Lenz -License: GPLv3+ -""" - -import unittest - -from picongpu.pypicongpu.species.operation.densityprofile import Foil -from picongpu.pypicongpu.species.operation.densityprofile.plasmaramp import None_ -from pydantic import ValidationError - -KWARGS = dict( - density_si=1e27, - y_value_front_foil_si=0.0, - thickness_foil_si=1.0e-5, - pre_foil_plasmaRamp=None_(), - post_foil_plasmaRamp=None_(), -) - - -class TestFoil(unittest.TestCase): - def test_value_pass_through(self): - """values are passed through""" - f = Foil(**KWARGS) - for key, val in KWARGS.items(): - self.assertAlmostEqual(val, getattr(f, key)) - - def test_typesafety(self): - """typesafety is ensured""" - for invalid in [None, [], {}]: - with self.assertRaises(ValidationError): - Foil(**KWARGS | dict(density_si=invalid)) - - for invalid in [None, [], {}]: - with self.assertRaises(ValidationError): - Foil(**KWARGS | dict(y_value_front_foil_si=invalid)) - - for invalid in [None, [], {}]: - with self.assertRaises(ValidationError): - Foil(**KWARGS | dict(thickness_foil_si=invalid)) - - for invalid in [None, []]: - with self.assertRaises(ValidationError): - Foil(**KWARGS | dict(pre_foil_plasmaRamp=invalid)) - - for invalid in [None, []]: - with self.assertRaises(ValidationError): - Foil(**KWARGS | dict(post_foil_plasmaRamp=invalid)) - - def test_check_density(self): - """validity check on self for invalid density""" - for invalid in [-1, 0, -0.00000003]: - with self.assertRaises(ValidationError): - Foil(**KWARGS | dict(density_si=invalid)) - - def test_check_y_value_front_foil(self): - """validity check on self for invalid y_value_front_foil_si""" - for invalid in [-1, -0.00000003]: - with self.assertRaises(ValidationError): - Foil(**KWARGS | dict(y_value_front_foil_si=invalid)) - - def test_check_thickness(self): - """validity check on self for invalid y_value_front_foil_si""" - for invalid in [-1, -0.00000003]: - with self.assertRaises(ValidationError): - Foil(**KWARGS | dict(thickness_foil_si=invalid)) - - def test_rendering(self): - """value passed through from rendering""" - f = Foil(**KWARGS) - expectedContextNoRamp = { - "typeID": {"exponential": False, "none": True}, - "data": None, - } - - context = f.get_rendering_context() - self.assertTrue(context["typeID"]["foil"]) - context = context["data"] - self.assertAlmostEqual(f.density_si, context["density_si"]) - self.assertAlmostEqual(f.y_value_front_foil_si, context["y_value_front_foil_si"]) - self.assertAlmostEqual(f.thickness_foil_si, context["thickness_foil_si"]) - self.assertEqual(expectedContextNoRamp, context["pre_foil_plasmaRamp"]) - self.assertEqual(expectedContextNoRamp, context["post_foil_plasmaRamp"]) diff --git a/test/python/picongpu/quick/pypicongpu/species/operation/densityprofile/gaussian.py b/test/python/picongpu/quick/pypicongpu/species/operation/densityprofile/gaussian.py deleted file mode 100644 index ed010ba340..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/operation/densityprofile/gaussian.py +++ /dev/null @@ -1,154 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Brian Edward Marre -License: GPLv3+ -""" - -import unittest - -from picongpu.pypicongpu.species.operation.densityprofile import Gaussian -from pydantic import ValidationError - - -class TestGaussian(unittest.TestCase): - values = { - "gas_center_front": 1.0, - "gas_center_rear": 2.0, - "gas_sigma_front": 3.0, - "gas_sigma_rear": 4.0, - "gas_power": 5.0, - "gas_factor": -6.0, - "vacuum_cells_front": 50, - "density": 1.0e25, - } - - def _getGaussian(self, **kwargs): - return Gaussian( - **dict( - center_front=self.values["gas_center_front"], - center_rear=self.values["gas_center_rear"], - sigma_front=self.values["gas_sigma_front"], - sigma_rear=self.values["gas_sigma_rear"], - power=self.values["gas_power"], - factor=self.values["gas_factor"], - vacuum_cells_front=self.values["vacuum_cells_front"], - density=self.values["density"], - ) - | kwargs - ) - - def test_value_pass_through(self): - """values are passed through""" - g = self._getGaussian() - - self.assertAlmostEqual(self.values["gas_center_front"], g.gas_center_front) - self.assertAlmostEqual(self.values["gas_center_rear"], g.gas_center_rear) - self.assertAlmostEqual(self.values["gas_sigma_front"], g.gas_sigma_front) - self.assertAlmostEqual(self.values["gas_sigma_rear"], g.gas_sigma_rear) - self.assertAlmostEqual(self.values["gas_power"], g.gas_power) - self.assertAlmostEqual(self.values["gas_factor"], g.gas_factor) - self.assertEqual(self.values["vacuum_cells_front"], g.vacuum_cells_front) - self.assertAlmostEqual(self.values["density"], g.density) - - def test_typesafety(self): - """typesafety is ensured""" - for invalid in [None, [], {}]: - with self.assertRaises(ValidationError): - self._getGaussian(density=invalid) - - for invalid in [None, [], {}]: - with self.assertRaises(ValidationError): - self._getGaussian(vacuum_cells_front=invalid) - - for invalid in [None, [], {}]: - with self.assertRaises(ValidationError): - self._getGaussian(factor=invalid) - - for invalid in [None, [], {}]: - with self.assertRaises(ValidationError): - self._getGaussian(power=invalid) - - for invalid in [None, [], {}]: - with self.assertRaises(ValidationError): - self._getGaussian(sigma_front=invalid) - - for invalid in [None, [], {}]: - with self.assertRaises(ValidationError): - self._getGaussian(sigma_rear=invalid) - - for invalid in [None, [], {}]: - with self.assertRaises(ValidationError): - self._getGaussian(center_front=invalid) - - for invalid in [None, [], {}]: - with self.assertRaises(ValidationError): - self._getGaussian(center_rear=invalid) - - def test_check_density(self): - """validity check on self for invalid density""" - for invalid in [-1, 0, -0.00000003]: - with self.assertRaises(ValidationError): - self._getGaussian(density=invalid) - - def test_check_vacuum_cells_front(self): - """validity check on self for invalid vacuum_cells_front""" - for invalid in [-1, -15]: - with self.assertRaises(ValidationError): - self._getGaussian(vacuum_cells_front=invalid) - - def test_check_gas_factor(self): - """validity check on self for invalid gas_factor""" - for invalid in [0.0, 1.0]: - with self.assertRaises(ValidationError): - self._getGaussian(factor=invalid) - - def test_check_gas_power(self): - """validity check on self for invalid gas_power""" - for invalid in [0.0]: - with self.assertRaises(ValidationError): - self._getGaussian(power=invalid) - - def test_check_gas_sigma_rear(self): - """validity check on self for invalid gas_sigma_rear""" - for invalid in [0.0]: - with self.assertRaises(ValidationError): - self._getGaussian(sigma_rear=invalid) - - def test_check_gas_sigma_front(self): - """validity check on self for invalid gas_sigma_front""" - for invalid in [0.0]: - with self.assertRaises(ValidationError): - self._getGaussian(sigma_front=invalid) - - def test_check_gas_center_rear(self): - """validity check on self for invalid gas_center_rear""" - for invalid in [-1.0]: - with self.assertRaises(ValidationError): - self._getGaussian(center_rear=invalid) - with self.assertRaises(ValidationError): - self._getGaussian(center_rear=0.9 * self.values["gas_center_front"]) - - def test_check_gas_center_front(self): - """validity check on self for invalid gas_center_front""" - for invalid in [-1.0]: - with self.assertRaises(ValidationError): - self._getGaussian(center_front=invalid) - with self.assertRaises(ValidationError): - self._getGaussian(center_front=1.1 * self.values["gas_center_rear"]) - - def test_rendering(self): - """value passed through from rendering""" - g = self._getGaussian() - - context = g.get_rendering_context() - self.assertTrue(context["typeID"]["gaussian"]) - context = context["data"] - self.assertAlmostEqual(g.gas_center_front, context["gas_center_front"]) - self.assertAlmostEqual(g.gas_center_rear, context["gas_center_rear"]) - self.assertAlmostEqual(g.gas_sigma_front, context["gas_sigma_front"]) - self.assertAlmostEqual(g.gas_sigma_rear, context["gas_sigma_rear"]) - self.assertAlmostEqual(g.gas_power, context["gas_power"]) - self.assertAlmostEqual(g.gas_factor, context["gas_factor"]) - self.assertEqual(g.vacuum_cells_front, context["vacuum_cells_front"]) - self.assertAlmostEqual(g.density, context["density"]) diff --git a/test/python/picongpu/quick/pypicongpu/species/operation/densityprofile/uniform.py b/test/python/picongpu/quick/pypicongpu/species/operation/densityprofile/uniform.py deleted file mode 100644 index ce6d8b6c72..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/operation/densityprofile/uniform.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -import unittest - -from picongpu.pypicongpu.species.operation.densityprofile import Uniform -from pydantic import ValidationError - - -class TestUniform(unittest.TestCase): - def test_typesafety(self): - """typesafety is ensured""" - for invalid in [None, [], {}]: - with self.assertRaises(ValidationError): - Uniform(density_si=invalid) - - def test_check(self): - """validity check on self""" - for invalid in [-1, 0, -0.00000003]: - # assignment passes, but check catches the error - with self.assertRaises(ValidationError): - Uniform(density_si=invalid) - - def test_rendering(self): - """value passed through from rendering""" - u = Uniform(density_si=42.17) - - context = u.get_rendering_context() - self.assertTrue(context["typeID"]["uniform"]) - context = context["data"] - self.assertAlmostEqual(u.density_si, context["density_si"]) diff --git a/test/python/picongpu/quick/pypicongpu/species/operation/momentum/__init__.py b/test/python/picongpu/quick/pypicongpu/species/operation/momentum/__init__.py deleted file mode 100644 index e2b77c924c..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/operation/momentum/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# flake8: noqa -from .drift import * # pyflakes.ignore -from .temperature import * # pyflakes.ignore diff --git a/test/python/picongpu/quick/pypicongpu/species/operation/momentum/drift.py b/test/python/picongpu/quick/pypicongpu/species/operation/momentum/drift.py deleted file mode 100644 index 73a4797ee6..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/operation/momentum/drift.py +++ /dev/null @@ -1,156 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from pydantic import ValidationError -from picongpu.pypicongpu.species.operation.momentum import Drift - -import unittest - -import typeguard -import itertools -import math - - -@typeguard.typechecked -class TestDrift(unittest.TestCase): - def test_passthru(self): - """values are present in output""" - drift = Drift( - gamma=17893, - direction_normalized=( - 0.0413868011324242, - 0.7989163623763952, - 0.6000164819563655, - ), - ) - - context = drift.get_rendering_context() - self.assertEqual( - context, - { - "gamma": drift.gamma, - "direction_normalized": { - "x": drift.direction_normalized[0], - "y": drift.direction_normalized[1], - "z": drift.direction_normalized[2], - }, - }, - ) - - def test_types(self): - """typechecks are applied""" - for invalid in [[], None, 0, tuple([0]), (0, 1)]: - with self.assertRaises(ValidationError): - Drift(gamma=1.0, direction_normalized=invalid) - - for invalid in [[], None, (1, 2, 3), tuple([0]), (0, 1)]: - with self.assertRaises(ValidationError): - Drift(gamma=invalid, direction_normalized=(1, 0, 0)) - - def test_invalid_gamma(self): - """invalid values for gamma are rejected""" - for invalid in [-1, -123.3, 0, 0.9999999999, math.inf, math.nan]: - with self.assertRaises(ValueError): - Drift(direction_normalized=(1, 0, 0), gamma=invalid) - - def test_normalized_checked(self): - """non-normalized direction is rejected""" - non_normalized_directions = [ - (1, 2, 3), - (0, 0, 0), - (1, 1, 0), - (-1, 0, 1), - ] - normalized_directions = [ - (1, 0, 0), - (-1, 0, 0), - (0.7071067811865475, 0.7071067811865475, 0.0), - (0.5773502691896258, 0.5773502691896258, 0.5773502691896258), - (0.0413868011324242, 0.7989163623763952, 0.6000164819563655), - ] - - for components in non_normalized_directions: - for permutation in itertools.permutations(components): - with self.assertRaises(ValidationError): - Drift(gamma=1, direction_normalized=tuple(permutation)) - - for components in normalized_directions: - for permutation in itertools.permutations(components): - Drift(gamma=1, direction_normalized=tuple(permutation)) - - def test_fill_from_invalid(self): - """fill_... methods reject invalid inputs""" - invalid_inputs = [ - (0, 0, 0), - (math.nan, 1, 0), - (math.inf, 1, 0), - (-math.inf, 1, 0), - ] - - for invalid in invalid_inputs: - for permutation in itertools.permutations(invalid): - vector3 = tuple(permutation) - self.assertEqual(3, len(vector3)) - - with self.assertRaises(ValueError): - Drift.from_velocity(vector3) - with self.assertRaises(ValueError): - Drift.from_gamma_velocity(vector3) - - def test_fill_from_velocity(self): - """computation based on velocity vector""" - d = Drift.from_velocity((1742, 1925, 1984)) - self.assertAlmostEqual(d.gamma, 1.00000000006) - self.assertAlmostEqual(d.direction_normalized[0], 0.533132081381511) - self.assertAlmostEqual(d.direction_normalized[1], 0.5891384940639545) - self.assertAlmostEqual(d.direction_normalized[2], 0.607195206349551) - - d = Drift.from_velocity((41782731.0, 61723581.0, 212931235.0)) - self.assertAlmostEqual(d.gamma, 1.5184434266) - self.assertAlmostEqual(d.direction_normalized[0], 0.18520723575308878) - self.assertAlmostEqual(d.direction_normalized[1], 0.2735975735475949) - self.assertAlmostEqual(d.direction_normalized[2], 0.9438446098662472) - - d = Drift.from_velocity((1, 0, 0)) - self.assertAlmostEqual(d.direction_normalized[0], 1) - self.assertAlmostEqual(d.direction_normalized[1], 0) - self.assertAlmostEqual(d.direction_normalized[2], 0) - - def test_faster_than_light(self): - """filling from faster than light velocity fails""" - faster_than_light_list = [ - (3e8, 0, 0), - (3e8, -3e8, 0), - (2e8, 2e8, 2e8), - (299792458, 0, 0), - ] - for components in faster_than_light_list: - for ftl in itertools.permutations(components): - # on that note: the game with the same name as the iteration - # var is awesome - with self.assertRaisesRegex(ValueError, ".*[Ll]ight.*"): - Drift.from_velocity(ftl) - - def test_fill_from_gamma_velocity(self): - """computation based on velocity vector multiplied with gamma""" - d = Drift.from_gamma_velocity((29379221.65264335, 141390308.68517736, 265336.4756518417)) - self.assertAlmostEqual(d.gamma, 1.10997153564) - self.assertAlmostEqual(d.direction_normalized[0], 0.20344224506631242) - self.assertAlmostEqual(d.direction_normalized[1], 0.9790852245720864) - self.assertAlmostEqual(d.direction_normalized[2], 0.001837375031333983) - - d = Drift.from_gamma_velocity((359876252.1771755, 42747107.177341804, 43958755.5088825)) - self.assertAlmostEqual(d.gamma, 1.57570157478) - self.assertAlmostEqual(d.direction_normalized[0], 0.9857936257001714) - self.assertAlmostEqual(d.direction_normalized[1], 0.11709532239932068) - self.assertAlmostEqual(d.direction_normalized[2], 0.1204143388517734) - - d = Drift.from_gamma_velocity((1, 0, 0)) - self.assertAlmostEqual(d.gamma, 1) - self.assertAlmostEqual(d.direction_normalized[0], 1) - self.assertAlmostEqual(d.direction_normalized[1], 0) - self.assertAlmostEqual(d.direction_normalized[2], 0) diff --git a/test/python/picongpu/quick/pypicongpu/species/operation/momentum/temperature.py b/test/python/picongpu/quick/pypicongpu/species/operation/momentum/temperature.py deleted file mode 100644 index 69ed6b5b07..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/operation/momentum/temperature.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from pydantic import ValidationError -from picongpu.pypicongpu.species.operation.momentum import Temperature - -import unittest - - -class TestTemperature(unittest.TestCase): - def test_basic(self): - """expected functions return something (valid)""" - t = Temperature(temperature_kev=17) - - context = t.get_rendering_context() - self.assertEqual(17, context["temperature_kev"]) - - def test_invalid_values(self): - """temperature must be >=0""" - for invalid in [-1, -47.1, -0.0000001]: - with self.assertRaises(ValidationError): - Temperature(temperature_kev=invalid) - - def test_types(self): - """invalid types are rejected""" - for invalid in [None, "asd", {}, []]: - with self.assertRaises(ValidationError): - Temperature(temperature_kev=invalid) diff --git a/test/python/picongpu/quick/pypicongpu/species/operation/noboundelectrons.py b/test/python/picongpu/quick/pypicongpu/species/operation/noboundelectrons.py deleted file mode 100644 index f839c45fbe..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/operation/noboundelectrons.py +++ /dev/null @@ -1,95 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from picongpu.pypicongpu.species.operation import NoBoundElectrons - -import unittest -import typeguard - -from picongpu.pypicongpu.species import Species -from picongpu.pypicongpu.species.constant import GroundStateIonization -from picongpu.pypicongpu.species.constant.ionizationmodel import BSI -from picongpu.pypicongpu.species.constant.ionizationcurrent import None_ -from picongpu.pypicongpu.species.attribute import BoundElectrons - - -class TestNoBoundElectrons(unittest.TestCase): - def setUp(self): - electron = Species() - electron.name = "e" - self.electron = electron - - self.species1 = Species() - self.species1.name = "ion" - self.species1.constants = [ - GroundStateIonization( - ionization_model_list=[BSI(ionization_electron_species=self.electron, ionization_current=None_())] - ) - ] - - def test_no_rendering_context(self): - """results in no rendered code, hence no rendering context available""" - # works: - nbe = NoBoundElectrons() - nbe.species = self.species1 - nbe.check_preconditions() - - with self.assertRaises(RuntimeError): - nbe.get_rendering_context() - - def test_types(self): - """typesafety is ensured""" - nbe = NoBoundElectrons() - for invalid_species in ["x", 0, None, []]: - with self.assertRaises(typeguard.TypeCheckError): - nbe.species = invalid_species - - # works: - nbe.species = self.species1 - - def test_ionizers_required(self): - """species must have ionizers constant""" - nbe = NoBoundElectrons() - nbe.species = self.species1 - - self.assertTrue(self.species1.has_constant_of_type(GroundStateIonization)) - - # passes - nbe.check_preconditions() - - # remove constant: - self.species1.constants = [] - - # now raises b/c ionizers constant is missing - with self.assertRaisesRegex(AssertionError, ".*[Gg]roundStateIonization.*"): - nbe.check_preconditions() - - def test_empty(self): - """species is mandatory""" - nbe = NoBoundElectrons() - with self.assertRaises(Exception): - nbe.check_preconditions() - - nbe.species = self.species1 - # now works: - nbe.check_preconditions() - - def test_bound_electrons_attr_added(self): - """adds attribute BoundElectrons""" - nbe = NoBoundElectrons() - nbe.species = self.species1 - - # emulate initmanager behavior - self.species1.attributes = [] - nbe.check_preconditions() - nbe.prebook_species_attributes() - - self.assertTrue(self.species1 in nbe.attributes_by_species) - self.assertEqual(1, len(nbe.attributes_by_species)) - - self.assertEqual(1, len(nbe.attributes_by_species[self.species1])) - self.assertTrue(isinstance(nbe.attributes_by_species[self.species1][0], BoundElectrons)) diff --git a/test/python/picongpu/quick/pypicongpu/species/operation/notplaced.py b/test/python/picongpu/quick/pypicongpu/species/operation/notplaced.py deleted file mode 100644 index b59b93beb6..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/operation/notplaced.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from picongpu.pypicongpu.species.operation import NotPlaced - -import unittest -import typeguard - -from picongpu.pypicongpu.species import Species -from picongpu.pypicongpu.species.attribute import Position, Weighting - - -class TestNotPlaced(unittest.TestCase): - def setUp(self): - self.species1 = Species() - self.species1.name = "species1" - self.species1.constants = [] - - def test_not_rendering_context(self): - """get_rendering_context raises""" - np = NotPlaced() - np.species = self.species1 - - # check okay: - np.check_preconditions() - - # but can't be represented as context either way: - with self.assertRaises(RuntimeError): - np.get_rendering_context() - - def test_types(self): - """typesafety is ensured""" - np = NotPlaced() - - for invalid_species in ["s", [], {"species1"}, 1, None, {}]: - with self.assertRaises(typeguard.TypeCheckError): - np.species = invalid_species - - def test_empty(self): - """at least one species is required""" - np = NotPlaced() - - # nothing set at all -> raises - with self.assertRaises(Exception): - np.check_postconditions() - - def test_position_added(self): - """provides position attribute""" - np = NotPlaced() - np.species = self.species1 - - np.check_preconditions() - np.prebook_species_attributes() - - self.assertEqual(1, len(np.attributes_by_species)) - - attributes = np.attributes_by_species[np.species] - self.assertEqual(2, len(attributes)) - attr_types = set(map(lambda attr: type(attr), attributes)) - self.assertEqual({Position, Weighting}, attr_types) diff --git a/test/python/picongpu/quick/pypicongpu/species/operation/operation.py b/test/python/picongpu/quick/pypicongpu/species/operation/operation.py deleted file mode 100644 index ecc883721f..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/operation/operation.py +++ /dev/null @@ -1,132 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from picongpu.pypicongpu.species.operation import Operation - -from picongpu.pypicongpu.species import Species - -from ..attribute import DummyAttribute - -import unittest - - -class DummyOperation(Operation): - def __init__(self): - pass - - -class TestOperation(unittest.TestCase): - def setUp(self): - self.op = DummyOperation() - self.species1 = Species() - self.species1.name = "species1" - self.species1.attributes = [] - self.species1.constants = [] - self.species2 = Species() - self.species2.name = "species2" - self.species2.attributes = [] - self.species2.constants = [] - self.attribute1 = DummyAttribute() - self.attribute1.PICONGPU_NAME = "attribute1" - self.attribute1_copy = DummyAttribute() - self.attribute1_copy.PICONGPU_NAME = "attribute1" - self.attribute2 = DummyAttribute() - self.attribute2.PICONGPU_NAME = "attribute2" - self.attribute3 = DummyAttribute() - self.attribute3.PICONGPU_NAME = "attribute3" - - def test_abstract(self): - """constructor is not implemented""" - with self.assertRaises(NotImplementedError): - Operation() - - # other methods are also not implemented - op = DummyOperation() - with self.assertRaises(NotImplementedError): - op.check_preconditions() - with self.assertRaises(NotImplementedError): - op.prebook_species_attributes() - - def test_bake_species_attributes_basic(self): - """valid example works""" - op = self.op - # Note: all these examples access Operation.attributes_by_species - # directly. This is forbidden. *Normally* this would be performed - # inside Operation.prebook_species_attributes(), but it is more concise - # to access Operation.attributes_by_species directly. - op.attributes_by_species = { - self.species1: [ - self.attribute1, - ], - self.species2: [ - self.attribute2, - self.attribute3, - ], - } - op.bake_species_attributes() - - self.assertEqual(self.species1.attributes, [self.attribute1]) - - # note: check if in lists b/c there is no defined order - self.assertEqual(2, len(self.species2.attributes)) - self.assertTrue(self.attribute2 in self.species2.attributes) - self.assertTrue(self.attribute3 in self.species2.attributes) - - def test_bake_species_attributes_empty(self): - """rejects if nothing is prebooked""" - # not defined rejects (no default is set) - op = DummyOperation() - with self.assertRaises(Exception): - op.bake_species_attributes() - - # explicitly empty {} is not allowed - op.attributes_by_species = {} - with self.assertRaisesRegex(ValueError, ".*at least one.*"): - op.bake_species_attributes() - - # species without attributes is not allowed - op.attributes_by_species = { - self.species1: [], - } - with self.assertRaisesRegex(ValueError, ".*at least one.*"): - op.bake_species_attributes() - - def test_bake_species_attributes_attribute_repetition(self): - """every attribute type may only be prebooked once per species""" - # check preconditions from setUp() - self.assertEqual(self.attribute1.PICONGPU_NAME, self.attribute1_copy.PICONGPU_NAME) - self.assertTrue(self.attribute1 is not self.attribute1_copy) - - # provide two objects defining the same attribute "attribute1" - op = self.op - op.attributes_by_species = {self.species1: [self.attribute1, self.attribute1_copy]} - - with self.assertRaisesRegex(ValueError, ".*attribute1.*"): - op.bake_species_attributes() - - def test_bake_species_attributes_attributes_exclusive(self): - """same attribute object can't be prebooked to multiple species""" - # try to assign same attribute **object** to multiple species - op = self.op - op.attributes_by_species = { - self.species1: [self.attribute1], - self.species2: [self.attribute1], - } - self.assertTrue(op.attributes_by_species[self.species1][0] is op.attributes_by_species[self.species2][0]) - with self.assertRaisesRegex(ValueError, ".*exclusive.*"): - op.bake_species_attributes() - - def test_bake_species_attributes_species_checked(self): - """species can not have the same attribute already""" - # note: do use same attribute "attribute1", but not identical object - self.species1.attributes = [self.attribute1_copy] - - op = self.op - op.attributes_by_species = {self.species1: [self.attribute1]} - - with self.assertRaisesRegex(ValueError, ".*attribute1.*"): - op.bake_species_attributes() diff --git a/test/python/picongpu/quick/pypicongpu/species/operation/setchargestate.py b/test/python/picongpu/quick/pypicongpu/species/operation/setchargestate.py deleted file mode 100644 index 53f3bccae7..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/operation/setchargestate.py +++ /dev/null @@ -1,152 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from picongpu.pypicongpu.species.operation import SetChargeState - -import unittest -import typeguard - -from picongpu.pypicongpu.species import Species -from picongpu.pypicongpu.species.constant import GroundStateIonization -from picongpu.pypicongpu.species.constant.ionizationmodel import BSI -from picongpu.pypicongpu.species.constant.ionizationcurrent import None_ -from picongpu.pypicongpu.species.attribute import BoundElectrons, Position, Momentum - - -class TestSetChargeState(unittest.TestCase): - def setUp(self): - electron = Species() - electron.name = "e" - # note: attributes not set yet (as would be in init manager) - - self.electron = electron - - self.species1 = Species() - self.species1.name = "ion" - self.species1.constants = [ - GroundStateIonization( - ionization_model_list=[BSI(ionization_electron_species=self.electron, ionization_current=None_())] - ) - ] - - def test_basic(self): - """basic operation""" - scs = SetChargeState() - scs.species = self.species1 - scs.charge_state = 2 - - # checks pass - scs.check_preconditions() - - def test_typesafety(self): - """typesafety is ensured""" - scs = SetChargeState() - for invalid_species in [None, 1, "a", []]: - with self.assertRaises(typeguard.TypeCheckError): - scs.species = invalid_species - - for invalid_number in [None, "a", [], self.species1, 2.3]: - with self.assertRaises(typeguard.TypeCheckError): - scs.charge_state = invalid_number - - # works: - scs.species = self.species1 - scs.charge_state = 1 - - def test_empty(self): - """all parameters are mandatory""" - for set_species in [True, False]: - for set_charge_state in [True, False]: - scs = SetChargeState() - - if set_species: - scs.species = self.species1 - if set_charge_state: - scs.charge_state = 1 - - if set_species and set_charge_state: - # must pass - scs.check_preconditions() - else: - # mandatory missing -> must raise - with self.assertRaises(Exception): - scs.check_preconditions() - - def test_attribute_generated(self): - """creates bound electrons attribute""" - scs = SetChargeState() - scs.species = self.species1 - scs.charge_state = 1 - - # emulate initmanager - scs.check_preconditions() - self.species1.attributes = [] - scs.prebook_species_attributes() - - self.assertEqual(1, len(scs.attributes_by_species)) - self.assertTrue(self.species1 in scs.attributes_by_species) - self.assertEqual(1, len(scs.attributes_by_species[self.species1])) - self.assertTrue(isinstance(scs.attributes_by_species[self.species1][0], BoundElectrons)) - - def test_ionizers_required(self): - """ionizers constant must be present""" - scs = SetChargeState() - scs.species = self.species1 - scs.charge_state = 1 - - # passes: - self.assertTrue(scs.species.has_constant_of_type(GroundStateIonization)) - scs.check_preconditions() - - # without constants does not pass: - scs.species.constants = [] - with self.assertRaisesRegex(AssertionError, ".*BoundElectrons requires GroundStateIonization.*"): - scs.check_preconditions() - - def test_values(self): - """bound electrons must be >0""" - scs = SetChargeState() - scs.species = self.species1 - - with self.assertRaisesRegex(ValueError, ".*> 0.*"): - scs.charge_state = -1 - scs.check_preconditions() - - # silently passes - scs.charge_state = 1 - scs.check_preconditions() - - def test_rendering(self): - """rendering works""" - # create full electron species - electron = Species() - electron.name = "e" - electron.constants = [] - electron.attributes = [Position(), Momentum()] - - # can be rendered: - self.assertNotEqual({}, electron.get_rendering_context()) - - ion = Species() - ion.name = "ion" - ion.constants = [ - GroundStateIonization( - ionization_model_list=[BSI(ionization_electron_species=electron, ionization_current=None_())] - ), - ] - ion.attributes = [Position(), Momentum(), BoundElectrons()] - - # can be rendered - self.assertNotEqual({}, ion.get_rendering_context()) - - scs = SetChargeState() - scs.species = ion - scs.charge_state = 1 - - context = scs.get_rendering_context() - self.assertEqual(1, context["charge_state"]) - self.assertEqual(ion.get_rendering_context(), context["species"]) diff --git a/test/python/picongpu/quick/pypicongpu/species/operation/simpledensity.py b/test/python/picongpu/quick/pypicongpu/species/operation/simpledensity.py deleted file mode 100644 index 830c491a2e..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/operation/simpledensity.py +++ /dev/null @@ -1,207 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from picongpu.pypicongpu.species.operation import SimpleDensity, Operation - -import unittest -import typeguard - -from picongpu.pypicongpu.species import Species -from picongpu.pypicongpu.species.operation import densityprofile -from picongpu.pypicongpu.species.operation.layout import Random -from picongpu.pypicongpu.species.attribute import Position, Weighting, Momentum -from picongpu.pypicongpu.species.constant import DensityRatio - - -class TestSimpleDensity(unittest.TestCase): - def setUp(self): - self.species1 = Species() - self.species1.name = "species1" - self.species1_density_ratio = DensityRatio(ratio=0.8) - self.species1.constants = [self.species1_density_ratio] - - self.species2 = Species() - self.species2.name = "species2" - self.species2_density_ratio = DensityRatio(ratio=1) - self.species2.constants = [self.species2_density_ratio] - - self.species3 = Species() - self.species3.name = "species3" - self.species3_density_ratio = DensityRatio(ratio=5) - self.species3.constants = [self.species3_density_ratio] - - self.species4 = Species() - self.species4.name = "species4" - # note: no explicit density ratio (should be assumed 1) - self.species4.constants = [] - - self.profile = densityprofile.Uniform(density_si=42) - - self.sd = SimpleDensity() - self.sd.ppc = 2 - self.sd.profile = self.profile - self.sd.species = { - self.species1, - self.species3, - self.species2, - self.species4, - } - - self.sd.layout = Random(ppc=1) - - def test_basic(self): - """simple scenario""" - # passes silently - self.sd.check_preconditions() - self.sd.prebook_species_attributes() - - def test_inheritance(self): - """is an operation""" - self.assertTrue(issubclass(SimpleDensity, Operation)) - - def test_check_passthru(self): - """passes check through to profile & density ratios""" - self.assertTrue(self.species3 in self.sd.species) - self.assertNotEqual([], self.species3.constants) - density_ratio_const = self.species3.constants[0] - - self.assertTrue(isinstance(density_ratio_const, DensityRatio)) - - def test_typesafety(self): - """typesafety enforced""" - for invalid_pcc in [None, [], {}]: - with self.assertRaises(typeguard.TypeCheckError): - self.sd.ppc = invalid_pcc - - for invalid_profile in [None, [], {}, 1, "3"]: - with self.assertRaises(typeguard.TypeCheckError): - self.sd.profile = invalid_profile - - for invalid_set in [None, {self.species1: 2}, 1, {1, 2}, "3"]: - with self.assertRaises(typeguard.TypeCheckError): - self.sd.species = invalid_set - - def test_empty(self): - """empty object raises""" - sd = SimpleDensity() - - # non-assigned attributes - with self.assertRaises(Exception): - sd.check_preconditions() - - sd.species = { - self.species1, - } - with self.assertRaises(Exception): - sd.check_preconditions() - - sd.profile = self.profile - with self.assertRaises(Exception): - sd.check_preconditions() - - sd.ppc = 1 - - # now all attributes present -> ok - sd.check_preconditions() - - # test with empty set of species -> must break - sd.species = set() - - # along the lines of "at least one species" - with self.assertRaisesRegex(ValueError, ".*species.*"): - sd.check_preconditions() - # also does not render - with self.assertRaisesRegex(ValueError, ".*species.*"): - sd.get_rendering_context() - - def test_check(self): - """enforces value ranges""" - # is valid on its own - sd = self.sd - sd.check_preconditions() - - # pcc non-negative - for invalid_ppc in [0, -1, -1000]: - sd.ppc = invalid_ppc - with self.assertRaisesRegex(ValueError, ".*particle.*per.*cell.*"): - sd.check_preconditions() - sd.ppc = 1 - sd.check_preconditions() - - # (profile check passthru tested above in test_check_passthru()) - # (density ratio check passthru tested above in test_check_passthru()) - # (at least one species required is tested above in test_empty()) - - def test_prebooking(self): - """prebooks correct attributes""" - sd = self.sd - self.assertEqual(4, len(sd.species)) - sd.check_preconditions() - sd.prebook_species_attributes() - - self.assertEqual(4, len(sd.attributes_by_species)) - self.assertTrue(self.species1 in sd.attributes_by_species) - self.assertTrue(self.species2 in sd.attributes_by_species) - self.assertTrue(self.species3 in sd.attributes_by_species) - self.assertTrue(self.species4 in sd.attributes_by_species) - - # for each species the same - for species, attributes in sd.attributes_by_species.items(): - # assign position & weighting - attribute_names = list(map(lambda attr: attr.PICONGPU_NAME, attributes)) - self.assertEqual(2, len(attribute_names)) - self.assertTrue(Position.PICONGPU_NAME in attribute_names) - self.assertTrue(Weighting.PICONGPU_NAME in attribute_names) - - def test_rendering_context(self): - """rendering works & passes values through""" - # initialized as would be performed by initmanager (s.t. attributes are - # defined) - for species in self.sd.species: - species.attributes = [Momentum()] - self.sd.check_preconditions() - self.sd.prebook_species_attributes() - self.sd.bake_species_attributes() - - context = self.sd.get_rendering_context() - - self.assertEqual(2, context["ppc"]) - self.assertEqual(context["profile"], self.sd.profile.get_rendering_context()) - - # species with lowest ratio must be placed as first, which is species1 - self.assertEqual(context["placed_species_initial"], self.species1.get_rendering_context()) - - copied_species_names = list(map(lambda d: d["name"], context["placed_species_copied"])) - self.assertEqual({"species2", "species3", "species4"}, set(copied_species_names)) - - def test_rendering_minimal(self): - """minimal example for rendering""" - species = Species() - species.name = "species1" - species.constants = [] - - sd = SimpleDensity() - sd.profile = self.profile - sd.ppc = 1 - sd.species = {species} - - sd.layout = Random(ppc=1) - - # would normally be performed by init manager: - species.attributes = [Momentum()] - sd.check_preconditions() - sd.prebook_species_attributes() - sd.bake_species_attributes() - - # actual checks - context = sd.get_rendering_context() - - self.assertEqual(1, context["ppc"]) - self.assertEqual(context["profile"], sd.profile.get_rendering_context()) - - self.assertEqual(context["placed_species_initial"], species.get_rendering_context()) - self.assertEqual(context["placed_species_copied"], []) diff --git a/test/python/picongpu/quick/pypicongpu/species/operation/simplemomentum.py b/test/python/picongpu/quick/pypicongpu/species/operation/simplemomentum.py deleted file mode 100644 index 93a5931862..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/operation/simplemomentum.py +++ /dev/null @@ -1,116 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from picongpu.pypicongpu.species.operation import SimpleMomentum - -import unittest -import typeguard - -from picongpu.pypicongpu.species.operation.momentum import Temperature, Drift -from picongpu.pypicongpu.species import Species -from picongpu.pypicongpu.species.attribute import Momentum, Position - -from copy import deepcopy - - -class TestSimpleMomentum(unittest.TestCase): - def setUp(self): - self.temperature = Temperature(temperature_kev=42) - - self.drift = Drift(direction_normalized=(1, 0, 0), gamma=1) - - self.species = Species() - self.species.name = "mockname" - self.species.constants = [] - self.species.attributes = [Position()] - - self.sm = SimpleMomentum() - self.sm.species = self.species - self.sm.temperature = self.temperature - self.sm.drift = self.drift - - def test_rendering_context(self): - """renders to context object""" - self.sm.prebook_species_attributes() - self.sm.bake_species_attributes() - context = self.sm.get_rendering_context() - self.assertEqual(context["species"], self.species.get_rendering_context()) - self.assertEqual(context["temperature"], self.temperature.get_rendering_context()) - self.assertEqual(context["drift"], self.drift.get_rendering_context()) - - def test_attribute(self): - """actually provides an attribute""" - for temp in [None, self.temperature]: - for drift in [None, self.drift]: - sm = SimpleMomentum() - sm.species = self.species - sm.temperature = temp - sm.drift = drift - - sm.check_preconditions() - sm.prebook_species_attributes() - - self.assertEqual(1, len(sm.attributes_by_species)) - attrs = sm.attributes_by_species[self.species] - self.assertEqual(1, len(attrs)) - self.assertTrue(isinstance(attrs[0], Momentum)) - - def test_types(self): - """typesafety is ensured""" - for invalid in [1, "", [], {}]: - with self.assertRaises(typeguard.TypeCheckError): - self.sm.temperature = invalid - with self.assertRaises(typeguard.TypeCheckError): - self.sm.drift = invalid - with self.assertRaises(typeguard.TypeCheckError): - self.sm.species = invalid - - with self.assertRaises(typeguard.TypeCheckError): - self.sm.temperature = self.drift - with self.assertRaises(typeguard.TypeCheckError): - self.sm.temperature = self.species - - with self.assertRaises(typeguard.TypeCheckError): - self.sm.drift = self.temperature - with self.assertRaises(typeguard.TypeCheckError): - self.sm.drift = self.species - - with self.assertRaises(typeguard.TypeCheckError): - self.sm.species = self.temperature - with self.assertRaises(typeguard.TypeCheckError): - self.sm.species = self.drift - - def test_optional(self): - """temperature and drift may be left at None""" - for temp in [None, self.temperature]: - for drift in [None, self.drift]: - # would accumulate multiple momentum attributes over multiple - # iterations (which throws) - sm = deepcopy(self.sm) - sm.temperature = temp - sm.drift = drift - - # checks pass - sm.check_preconditions() - - # renders without problems - sm.prebook_species_attributes() - sm.bake_species_attributes() - sm.get_rendering_context() - - def test_species_mandatory(self): - """species must be set""" - sm = SimpleMomentum() - sm.temperature = self.temperature - sm.drift = self.drift - - with self.assertRaises(Exception): - sm.check_preconditions() - - # ok with species: - sm.species = self.species - sm.check_preconditions() diff --git a/test/python/picongpu/quick/pypicongpu/species/species.py b/test/python/picongpu/quick/pypicongpu/species/species.py deleted file mode 100644 index 53238d6ada..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/species.py +++ /dev/null @@ -1,319 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from picongpu.pypicongpu.species import Species - -from picongpu.pypicongpu.species.attribute import Position, Weighting, Momentum -from picongpu.pypicongpu.species.constant import ( - Mass, - Charge, - DensityRatio, - ElementProperties, - Constant, - GroundStateIonization, -) -from picongpu.pypicongpu.species.constant.ionizationcurrent import None_ -from picongpu.pypicongpu.species.constant.ionizationmodel import BSI -from picongpu.pypicongpu.species.util import Element - -from .attribute import DummyAttribute - -import itertools -import unittest -import typeguard - - -class TestSpecies(unittest.TestCase): - def setUp(self): - self.pos = Position() - self.mom = Momentum() - self.species = Species() - self.species.attributes = [self.pos, self.mom] - self.species.constants = [] - self.species.name = "valid" - - self.electron = Species() - self.electron.name = "electron" - self.electron.attributes = [Position(), Momentum()] - self.electron.constants = [] - - self.const = Constant() - self.const_charge = Charge(charge_si=1) - self.const_mass = Mass(mass_si=2) - self.const_density_ratio = DensityRatio(ratio=4.2) - self.const_ground_state_ionization = GroundStateIonization( - ionization_model_list=[ - BSI( - ionization_electron_species=self.electron, - ionization_current=None_(), - ) - ] - ) - - self.const_element_properties = ElementProperties(element=Element("H")) - - def test_basic(self): - """setup provides working species""" - # does not throw - self.species.check() - - def test_empty(self): - """attrs have no defaults""" - s = Species() - - with self.assertRaises(Exception): - # must not pass unless attrs are all explicitly set - s.check() - - s.attributes = [Position(), Momentum()] - s.constants = [] - # note: non-empty not allowed - s.name = "x" - s.check() - - def test_types(self): - """typesafety is ensured""" - species = self.species - - for invalid_name in [None, [], {}, 123]: - with self.assertRaises(typeguard.TypeCheckError): - species.name = invalid_name - - invalid_attr_lists = [None, {}, set(), [Constant()], DummyAttribute()] - for invalid_attr_list in invalid_attr_lists: - with self.assertRaises(typeguard.TypeCheckError): - species.attributes = invalid_attr_list - - invalid_const_lists = [None, {}, set(), [DummyAttribute()], Constant()] - for invalid_const_list in invalid_const_lists: - with self.assertRaises(typeguard.TypeCheckError): - species.constants = invalid_const_list - - def test_mandatory_attribute_position(self): - """test position present""" - self.assertNotEqual([], self.species.attributes) - self.species.check() - - self.species.attributes = [self.mom] - with self.assertRaisesRegex(ValueError, ".*position.*"): - self.species.check() - - def test_mandatory_attribute_momentum(self): - """test momentum present""" - self.assertNotEqual([], self.species.attributes) - self.species.check() - - self.species.attributes = [self.pos] - with self.assertRaisesRegex(ValueError, ".*momentum.*"): - self.species.check() - - def test_attributes_unique(self): - """all defined PIConGPU particle attributes must be uniquely named""" - species = self.species - nattr1 = DummyAttribute() - nattr1.PICONGPU_NAME = "test_attr" - nattr2 = DummyAttribute() - nattr2.PICONGPU_NAME = "test_attr" - other_nattr = DummyAttribute() - other_nattr.PICONGPU_NAME = "other_attr" - - species.attributes = [self.pos, self.mom, nattr1, nattr2, other_nattr] - - # duplicates -> throw (require violiating attr to be mentioned) - with self.assertRaisesRegex(ValueError, ".*test_attr.*"): - species.check() - - for single_attr in [nattr1, nattr2]: - # no duplicates -> pass silently - species.attributes = [self.pos, self.mom, single_attr, other_nattr] - species.check() - - def test_constants_unique(self): - """all defined PIConGPU particle flags must be uniquely named""" - species = self.species - const1 = Charge(charge_si=17) - const2 = Charge(charge_si=18) - other_const = Mass(mass_si=19) - - species.constants = [const1, const2, other_const] - - # duplicates -> throw (require violiating const to be mentioned) - with self.assertRaisesRegex(ValueError, ".*charge.*"): - species.check() - - for single_const in [const1, const2]: - # no duplicates -> pass silently - species.constants = [single_const, other_const] - species.check() - - def test_check_constant_passthhru(self): - """species check also calls constants check""" - - class ConstantFail(Constant): - ERROR_STR: str = "IDSTRING_XKCD_927_BEST" - - def check(self): - raise ValueError(self.ERROR_STR) - - # passes - self.species.check() - - # add raising constant - self.species.constants.append(ConstantFail()) - with self.assertRaisesRegex(ValueError, ConstantFail.ERROR_STR): - self.species.check() - - def test_get_cxx_typename(self): - """c++ typenames make sense""" - # 1. is mandatory - tmp = Species() - tmp.constants = [] - tmp.attributes = [] - with self.assertRaises(Exception): - tmp.check() - with self.assertRaises(Exception): - tmp.get_cxx_typename() - - # 2. is (somewhat) human-readable - def get_typename(name): - self.species.name = name - return self.species.get_cxx_typename() - - for txt in ["H", "h", "electron", "e", "1", "lulal"]: - self.assertTrue(txt in get_typename(txt)) - - # 3. reject invalid strings (not alphanum) - for invalid in ["", "\n", " ", "var\n", "abc sad", ".", "-"]: - with self.assertRaises(ValueError): - get_typename(invalid) - - def test_get_constant_by_type(self): - """constant by type works as specifed""" - species = self.species - self.assertEqual([], species.constants) - - with self.assertRaises(typeguard.TypeCheckError): - species.get_constant_by_type("string") - with self.assertRaises(typeguard.TypeCheckError): - # only children of Constant() are accepted - species.get_constant_by_type(object) - - with self.assertRaises(RuntimeError): - species.get_constant_by_type(type(self.const)) - - species.constants = [self.const, self.const_mass, self.const_charge] - # note: check for *identity* with is (instead of pure equality) - self.assertTrue(self.const is species.get_constant_by_type(Constant)) - self.assertTrue(self.const_charge is species.get_constant_by_type(Charge)) - self.assertTrue(self.const_mass is species.get_constant_by_type(Mass)) - - def test_has_constant_of_type(self): - """check for constant existance is valid""" - species = self.species - self.assertEqual([], species.constants) - - with self.assertRaises(typeguard.TypeCheckError): - species.has_constant_of_type("density") - with self.assertRaises(typeguard.TypeCheckError): - # only children of Constant() are accepted - species.has_constant_of_type(object) - - self.assertTrue(not species.has_constant_of_type(type(self.const))) - self.assertTrue(not species.has_constant_of_type(Mass)) - self.assertTrue(not species.has_constant_of_type(Charge)) - - species.constants = [self.const, self.const_mass] - - self.assertTrue(species.has_constant_of_type(type(self.const))) - self.assertTrue(species.has_constant_of_type(Mass)) - self.assertTrue(not species.has_constant_of_type(Charge)) - - def test_rendering_simple(self): - """passes information from rendering through""" - species = Species() - species.name = "myname" - species.attributes = [Position(), Momentum(), Weighting()] - # note: no charge - species.constants = [ - self.const_mass, - self.const_density_ratio, - ] - - context = species.get_rendering_context() - - self.assertEqual("myname", context["name"]) - self.assertEqual(species.get_cxx_typename(), context["typename"]) - self.assertEqual(3, len(context["attributes"])) - attribute_names = list(map(lambda attr_obj: attr_obj["picongpu_name"], context["attributes"])) - self.assertEqual( - [Position.PICONGPU_NAME, Momentum.PICONGPU_NAME, Weighting.PICONGPU_NAME], - attribute_names, - ) - - self.assertEqual(self.const_mass.get_rendering_context(), context["constants"]["mass"]) - self.assertEqual( - self.const_density_ratio.get_rendering_context(), - context["constants"]["density_ratio"], - ) - - # no charge set -> key still exists - self.assertEqual(None, context["constants"]["charge"]) - - def test_rendering_constants(self): - """constants are rendered as expected""" - # constants are passed as a dictionary which *always* has *all* keys, - # but those undefined are (explicitly) set to "null" - # (rationale: prevent mistyping/false-positive "unkown var" warnings) - # - # This test ensures that for *all* permutations of keys being - # defined/undefined the passthru is as expected - # (note: might be overengineered) - - expected_const_by_name = { - "density_ratio": self.const_density_ratio, - "charge": self.const_charge, - "mass": self.const_mass, - "ground_state_ionization": self.const_ground_state_ionization, - "element_properties": self.const_element_properties, - } - - for enabled_vector in itertools.product((0, 1), repeat=len(expected_const_by_name)): - species = Species() - species.name = "myname" - species.attributes = [Position(), Momentum()] - name_enabled_pairs = zip(expected_const_by_name.keys(), enabled_vector) - enabled_by_name = {} - - species.constants = [] - for const_name, enabled in name_enabled_pairs: - enabled_by_name[const_name] = enabled - if enabled: - species.constants.append(expected_const_by_name[const_name]) - - context = species.get_rendering_context() - self.assertEqual(set(expected_const_by_name.keys()), set(context["constants"].keys())) - - for const_name, enabled in enabled_by_name.items(): - self.assertTrue(const_name in context["constants"]) - if enabled: - self.assertEqual( - expected_const_by_name[const_name].get_rendering_context(), - context["constants"][const_name], - ) - else: - self.assertEqual(None, context["constants"][const_name]) - - def test_rendering_checks(self): - """retrieving rendering context enforces checks""" - species = self.species - species.name = "" - - with self.assertRaisesRegex(ValueError, ".*name.*"): - species.check() - - with self.assertRaisesRegex(ValueError, ".*name.*"): - species.get_rendering_context() diff --git a/test/python/picongpu/quick/pypicongpu/species/util/__init__.py b/test/python/picongpu/quick/pypicongpu/species/util/__init__.py deleted file mode 100644 index 393581f0a8..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/util/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# flake8: noqa -from .element import * # pyflakes.ignore diff --git a/test/python/picongpu/quick/pypicongpu/species/util/element.py b/test/python/picongpu/quick/pypicongpu/species/util/element.py deleted file mode 100644 index b9afd332b4..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/util/element.py +++ /dev/null @@ -1,93 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2024 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from picongpu.pypicongpu.species.util import Element - -import scipy - -import unittest -from picongpu.pypicongpu.rendering import RenderedObject - - -class TestElement(unittest.TestCase): - def setUp(self): - # create test case data - self.test_element = ["H", "#2H", "Cu", "#12C", "C", "Ne", "Ar"] - self.name = ["H", "D", "Cu", "C", "C", "Ne", "Ar"] - self.picongpu_names = ["Hydrogen", "Deuterium", "Copper", "Carbon", "Carbon", "Neon", "Argon"] - self.mass = [ - 1.00794 * scipy.constants.atomic_mass, - 2.014101778 * scipy.constants.atomic_mass, - 63.546 * scipy.constants.atomic_mass, - 12.0 * scipy.constants.atomic_mass, - 12.0107 * scipy.constants.atomic_mass, - 20.1797 * scipy.constants.atomic_mass, - 39.95 * scipy.constants.atomic_mass, - ] - self.atomic_number = [1, 1, 29, 6, 6, 10, 18] - - def test_parse_openpmd(self): - valid_test_strings = ["#3H", "#15He", "#1H", "#3He", "#56Cu"] - mass_number_results = [3, 15, 1, 3, 56] - name_results = ["H", "He", "H", "He", "Cu"] - - for i, string in enumerate(valid_test_strings): - mass_number, name = Element.parse_openpmd_isotopes(string) - self.assertEqual(name, name_results[i]) - self.assertEqual(mass_number, mass_number_results[i]) - - invalid_test_strings = ["#Htest", "#He3", "#Cu-56", "H3", "Fe-56"] - for i, string in enumerate(invalid_test_strings): - with self.assertRaisesRegex(ValueError, string + " is not a valid openPMD particle type"): - name, massNumber = Element.parse_openpmd_isotopes(string) - - def test_basic_use(self): - for name in self.test_element: - Element(name) - - def test_symbol(self): - for openpmd_name, name in zip(self.test_element, self.name): - e = Element(openpmd_name) - self.assertEqual(e.get_symbol(), name) - - def test_is_element(self): - for name in self.test_element: - self.assertTrue(Element.is_element(name)) - self.assertFalse(Element.is_element("n")) - - def test_picongpu_names(self): - """names must be translateable to picongpu""" - for openpmd_name, picongpu_name in zip(self.test_element, self.picongpu_names): - name_test = Element(openpmd_name).get_picongpu_name() - self.assertNotEqual("", picongpu_name) - self.assertEqual(name_test, picongpu_name) - - def test_get_mass(self): - """all elements have mass""" - for openpmd_name, mass in zip(self.test_element, self.mass): - self.assertAlmostEqual(Element(openpmd_name).get_mass_si(), mass) - - def test_charge(self): - """all elements have charge""" - for openpmd_name, atomic_number in zip(self.test_element, self.atomic_number): - self.assertAlmostEqual( - Element(openpmd_name).get_charge_si() / scipy.constants.elementary_charge, atomic_number - ) - - def test_atomic_number(self): - for openpmd_name, atomic_number in zip(self.test_element, self.atomic_number): - e = Element(openpmd_name) - self.assertEqual(e.get_atomic_number(), atomic_number) - - def test_rendering(self): - """all elements can be rendered""" - self.assertTrue(issubclass(Element, RenderedObject)) - for openpmd_name in self.test_element: - e = Element(openpmd_name) - context = e.get_rendering_context() - self.assertEqual(context["symbol"], e.get_symbol()) - self.assertEqual(context["picongpu_name"], e.get_picongpu_name())