From 87702dc0a3a297c4c68d0cd3d41d27820ebaacf6 Mon Sep 17 00:00:00 2001 From: BrianMarre Date: Fri, 26 Jul 2024 16:57:20 +0200 Subject: [PATCH] fixes --- .../picmi/distribution/Distribution.py | 2 + .../picongpu/picmi/interaction/interaction.py | 50 ++-- .../picmi/interaction/interactioninterface.py | 2 + .../thomasfermi.py | 2 + .../ionization/fieldionization/ADK.py | 15 +- .../ionization/fieldionization/BSI.py | 9 +- .../fieldionization/fieldionization.py | 2 + .../ionizationcurrent/ionizationcurrent.py | 2 + .../ionization/fieldionization/keldysh.py | 4 +- .../ionization/groundstateionizationmodel.py | 5 +- .../interaction/ionization/ionizationmodel.py | 16 ++ .../picmi/predefinedparticletypeproperties.py | 126 ++++++++ lib/python/picongpu/picmi/simulation.py | 26 +- lib/python/picongpu/picmi/species.py | 195 +++---------- .../ionizationcurrent/ionizationcurrent.py | 3 + .../constant/ionizationcurrent/none_.py | 3 + .../ionizationmodel/ionizationmodel.py | 4 +- .../pypicongpu/species/util/element.py | 15 +- .../python/picongpu/quick/picmi/simulation.py | 270 +++--------------- test/python/picongpu/quick/picmi/species.py | 195 ++++++------- .../ionizationmodel/ionizationmodel.py | 8 +- .../ionizationmodelimplementations.py | 2 +- .../species/operation/setboundelectrons.py | 10 +- .../quick/pypicongpu/species/species.py | 2 +- .../quick/pypicongpu/species/util/element.py | 2 +- 25 files changed, 403 insertions(+), 567 deletions(-) create mode 100644 lib/python/picongpu/picmi/predefinedparticletypeproperties.py diff --git a/lib/python/picongpu/picmi/distribution/Distribution.py b/lib/python/picongpu/picmi/distribution/Distribution.py index cc4d096ad64..46459619de3 100644 --- a/lib/python/picongpu/picmi/distribution/Distribution.py +++ b/lib/python/picongpu/picmi/distribution/Distribution.py @@ -9,6 +9,7 @@ import typing import pydantic +import typeguard """ note on rms_velocity: @@ -32,6 +33,7 @@ """ +@typeguard.typechecked class Distribution(pydantic.BaseModel): rms_velocity: typing.Tuple[float, float, float] = (0, 0, 0) """thermal velocity spread [m/s]""" diff --git a/lib/python/picongpu/picmi/interaction/interaction.py b/lib/python/picongpu/picmi/interaction/interaction.py index 645bff63385..0e31f10c631 100644 --- a/lib/python/picongpu/picmi/interaction/interaction.py +++ b/lib/python/picongpu/picmi/interaction/interaction.py @@ -7,13 +7,15 @@ from ... import pypicongpu -from .ionization.groundstateionizationmodel import GroundStateIonizationModel +from .ionization.groundstateionizationmodel import GroundStateIonizationModel, IonizationModel from .interactioninterface import InteractionInterface from ..species import Species import picmistandard +import typeguard +@typeguard.typechecked class Interaction(InteractionInterface): """ Common interface of Particle-In-Cell particle interaction extensions @@ -37,7 +39,7 @@ class Interaction(InteractionInterface): @staticmethod def update_constant_list( existing_list: list[pypicongpu.species.constant.Constant], - new_list: dict[str, pypicongpu.species.constant.Constant], + new_list: list[pypicongpu.species.constant.Constant], ) -> None: """check if dicts may be merged without overwriting previously set values""" @@ -62,38 +64,37 @@ def update_constant_list( existing_list.extend(new_constant_list) def get_interaction_constants( - self, species: picmistandard.PICMI_Species - ) -> list[pypicongpu.species.constant.Constant]: + self, picmi_species: picmistandard.PICMI_Species + ) -> tuple[ + list[pypicongpu.species.constant.Constant], + dict[IonizationModel, pypicongpu.species.constant.ionizationmodel.IonizationModel], + ]: """get list of all constants required by interactions for the given species""" constant_list = [] - ground_state_model_conversion = {} + ionization_model_conversion = {} for model in self.ground_state_ionization_model_list: - if model.ion_species == Species: + if model.ion_species == picmi_species: model_constants = model.get_constants() Interaction.update_constant_list(constant_list, model_constants) - - ground_state_model_conversion[model] = model.get_as_pypicongpu() + ionization_model_conversion[model] = model.get_as_pypicongpu() # add GroundStateIonization constant for entire species constant_list.append( pypicongpu.species.constant.GroundStateIonization( - ground_state_ionization_model_list=ground_state_model_conversion.values() + ionization_model_list=ionization_model_conversion.values() ) ) # add additional interaction sub groups needing constants here - return constant_list, {"ground_state_ionization": ground_state_model_conversion} + return constant_list, ionization_model_conversion def fill_in_ionization_electron_species( self, pypicongpu_by_picmi_species: dict[picmistandard.PICMI_Species, pypicongpu.species.Species], - ionization_model_conversion_by_species: dict[ - str, - dict[ - picmistandard.PICMI_Species, - dict[GroundStateIonizationModel, pypicongpu.species.constant.ionizationmodel.IonizationModel], - ], + ionization_model_conversion_by_type_and_species: dict[ + picmistandard.PICMI_Species, + None | dict[IonizationModel, pypicongpu.species.constant.ionizationmodel.IonizationModel], ], ) -> None: """ @@ -115,15 +116,14 @@ def fill_in_ionization_electron_species( pypicongpu_by_picmi_species) """ - # groundstate ionization model - for species, ionization_model_conversion in ionization_model_conversion_by_species[ - "ground_state_ionization" - ].items(): - for picmi_ionization_model, pypicongpu_ionization_model in ionization_model_conversion.items(): - pypicongpu_ionization_electron_species = pypicongpu_by_picmi_species[ - picmi_ionization_model.ionization_electron_species - ] - pypicongpu_ionization_model.ionization_electron_species = pypicongpu_ionization_electron_species + # ground state ionization model + for species, ionization_model_conversion in ionization_model_conversion_by_type_and_species.items(): + if ionization_model_conversion is not None: + for picmi_ionization_model, pypicongpu_ionization_model in ionization_model_conversion.items(): + pypicongpu_ionization_electron_species = pypicongpu_by_picmi_species[ + picmi_ionization_model.ionization_electron_species + ] + pypicongpu_ionization_model.ionization_electron_species = pypicongpu_ionization_electron_species def has_ground_state_ionization(self, species: Species) -> bool: """does at least one ground state ionization model list species as ion species?""" diff --git a/lib/python/picongpu/picmi/interaction/interactioninterface.py b/lib/python/picongpu/picmi/interaction/interactioninterface.py index 572fae7ee93..ae5a3f425d6 100644 --- a/lib/python/picongpu/picmi/interaction/interactioninterface.py +++ b/lib/python/picongpu/picmi/interaction/interactioninterface.py @@ -9,8 +9,10 @@ import picmistandard import pydantic +import typeguard +@typeguard.typechecked class InteractionInterface(pydantic.BaseModel): """ interface for forward declaration diff --git a/lib/python/picongpu/picmi/interaction/ionization/electroniccollisonalequilibrium/thomasfermi.py b/lib/python/picongpu/picmi/interaction/ionization/electroniccollisonalequilibrium/thomasfermi.py index 5055928e92b..bdc7638fdd2 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/electroniccollisonalequilibrium/thomasfermi.py +++ b/lib/python/picongpu/picmi/interaction/ionization/electroniccollisonalequilibrium/thomasfermi.py @@ -7,8 +7,10 @@ from ..groundstateionizationmodel import GroundStateIonizationModel from ..... import pypicongpu +import typeguard +@typeguard.typechecked class ThomasFermi(GroundStateIonizationModel): """thomas fermi ionization model""" diff --git a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ADK.py b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ADK.py index 0cc4858f6fe..da988288f93 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ADK.py +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ADK.py @@ -8,18 +8,25 @@ from .fieldionization import FieldIonization from .....pypicongpu.species.constant.ionizationcurrent import None_ -from .....pypicongpu.species.constant.ionizationmodel import ADKLinearPolarization, ADKCircularPolarization +from .....pypicongpu.species.constant.ionizationmodel import ( + ADKLinearPolarization, + ADKCircularPolarization, + IonizationModel, +) from ..... import pypicongpu import enum +import typeguard +@typeguard.typechecked class ADKVariant(enum.Enum): LinearPolarization = 0 CircularPolarization = 1 +@typeguard.typechecked class ADK(FieldIonization): """Barrier Suppression Ioniztion model""" @@ -28,11 +35,11 @@ class ADK(FieldIonization): ADK_variant: ADKVariant """extension to the BSI model""" - def get_as_pypicongpu(self) -> pypicongpu.species.constant.ionizationmodel.IonizationModel: + def get_as_pypicongpu(self) -> IonizationModel: if self.ADK_variant is ADKVariant.LinearPolarization: - return ADKLinearPolarization(ionization_current=None_) + return ADKLinearPolarization(ionization_current=None_()) if self.ADK_variant is ADKVariant.CircularPolarization: - return ADKCircularPolarization(ionization_current=None_) + return ADKCircularPolarization(ionization_current=None_()) # unknown/unsupported ADK variant pypicongpu.util.unsupported(f"ADKVariant {self.ADK_variant}") diff --git a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/BSI.py b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/BSI.py index 90d6ba47532..60634fee8d0 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/BSI.py +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/BSI.py @@ -13,8 +13,10 @@ from ..... import pypicongpu import enum +import typeguard +@typeguard.typechecked class BSIExtension(enum.Enum): StarkShift = 0 EffectiveZ = 1 @@ -22,6 +24,7 @@ class BSIExtension(enum.Enum): # add additional features here +@typeguard.typechecked class BSI(FieldIonization): """Barrier Suppression Ioniztion model""" @@ -32,12 +35,12 @@ class BSI(FieldIonization): def get_as_pypicongpu(self) -> pypicongpu.species.constant.ionizationmodel.IonizationModel: if self.BSI_extensions == []: - return ionizationmodel.BSI(ionization_current=None_) + return ionizationmodel.BSI(ionization_current=None_()) if self.BSI_extensions == [BSIExtension.StarkShift]: - return ionizationmodel.BSIStarkShifted(ionization_current=None_) + return ionizationmodel.BSIStarkShifted(ionization_current=None_()) if self.BSI_extensions == [BSIExtension.EffectiveZ]: - return ionizationmodel.BSIEffectiveZ(ionization_current=None_) + return ionizationmodel.BSIEffectiveZ(ionization_current=None_()) if len(self.BSI_extensions) > 1: pypicongpu.util.unsupported("more than one BSI_extension") diff --git a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/fieldionization.py b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/fieldionization.py index 5f34e2f2303..5f484202fef 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/fieldionization.py +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/fieldionization.py @@ -9,8 +9,10 @@ from .ionizationcurrent import IonizationCurrent import typing +import typeguard +@typeguard.typechecked class FieldIonization(GroundStateIonizationModel): """common interface of all field ionization models""" diff --git a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ionizationcurrent/ionizationcurrent.py b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ionizationcurrent/ionizationcurrent.py index 11a1da17467..bb30fe22137 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ionizationcurrent/ionizationcurrent.py +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ionizationcurrent/ionizationcurrent.py @@ -6,8 +6,10 @@ """ import pydantic +import typeguard +@typeguard.typechecked class IonizationCurrent(pydantic.BaseModel): """common interface of all ionization current models""" diff --git a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/keldysh.py b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/keldysh.py index 168925aaf9d..fa470d57693 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/keldysh.py +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/keldysh.py @@ -11,12 +11,14 @@ from .....pypicongpu.species.constant import ionizationmodel from ..... import pypicongpu +import typeguard +@typeguard.typechecked class Keldysh(FieldIonization): """Barrier Suppression Ioniztion model""" MODEL_NAME: str = "Keldysh" def get_as_pypicongpu(self) -> pypicongpu.species.constant.ionizationmodel.IonizationModel: - return ionizationmodel.Keldysh(ionization_current=None_) + return ionizationmodel.Keldysh(ionization_current=None_()) diff --git a/lib/python/picongpu/picmi/interaction/ionization/groundstateionizationmodel.py b/lib/python/picongpu/picmi/interaction/ionization/groundstateionizationmodel.py index e2e31cf36b0..a3a4c36face 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/groundstateionizationmodel.py +++ b/lib/python/picongpu/picmi/interaction/ionization/groundstateionizationmodel.py @@ -9,7 +9,10 @@ from .... import pypicongpu +import typeguard + +@typeguard.typechecked class GroundStateIonizationModel(IonizationModel): def get_constants(self) -> list[pypicongpu.species.constant.Constant]: """get all PyPIConGPU constants required by a ground state ionization model in PIConGPU""" @@ -18,4 +21,4 @@ def get_constants(self) -> list[pypicongpu.species.constant.Constant]: element_properties_const = pypicongpu.species.constant.ElementProperties() element_properties_const.element = self.ion_species.picongpu_element - return element_properties_const + return [element_properties_const] diff --git a/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py b/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py index 552571a9a62..b97405c97f4 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py +++ b/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py @@ -9,8 +9,10 @@ from .... import pypicongpu import pydantic +import typeguard +@typeguard.typechecked class IonizationModel(pydantic.BaseModel): """ common interface for all ionization models @@ -27,6 +29,20 @@ class IonizationModel(pydantic.BaseModel): ionization_electron_species: Species """PICMI electron species of which to create macro particle upon ionization""" + def __hash__(self): + """custom hash function for indexing in dicts""" + hash_value = hash(type(self)) + + for value in self.__dict__.values(): + try: + if value is not None: + hash_value += hash(value) + except TypeError: + print(self) + print(type(self)) + raise TypeError + return hash_value + def get_constants(self) -> list[pypicongpu.species.constant.Constant]: raise NotImplementedError("abstract base class only!") diff --git a/lib/python/picongpu/picmi/predefinedparticletypeproperties.py b/lib/python/picongpu/picmi/predefinedparticletypeproperties.py new file mode 100644 index 00000000000..31691ba390a --- /dev/null +++ b/lib/python/picongpu/picmi/predefinedparticletypeproperties.py @@ -0,0 +1,126 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +import collections +import pdg + +from scipy import constants as consts + +_PropertyTuple: collections.namedtuple = collections.namedtuple("_PropertyTuple", ["mass", "charge"]) + +# based on 2024 Particle data Group values +_quarks = { + "up": _PropertyTuple( + mass=2.16e6 * consts.elementary_charge / consts.speed_of_light**2, charge=2.0 / 3.0 * consts.elementary_charge + ), + "charm": _PropertyTuple( + mass=1.2730e9 * consts.elementary_charge / consts.speed_of_light**2, charge=2.0 / 3.0 * consts.elementary_charge + ), + "top": _PropertyTuple( + mass=172.57e9 * consts.elementary_charge / consts.speed_of_light**2, charge=2.0 / 3.0 * consts.elementary_charge + ), + "down": _PropertyTuple( + mass=4.70e6 * consts.elementary_charge / consts.speed_of_light**2, charge=-1.0 / 3.0 * consts.elementary_charge + ), + "strange": _PropertyTuple( + mass=93.5 * consts.elementary_charge / consts.speed_of_light**2, charge=-1.0 / 3.0 * consts.elementary_charge + ), + "bottom": _PropertyTuple( + mass=4.138 * consts.elementary_charge / consts.speed_of_light**2, charge=-1.0 / 3.0 * consts.elementary_charge + ), + "anti-up": _PropertyTuple( + mass=2.16e6 * consts.elementary_charge / consts.speed_of_light**2, charge=-2.0 / 3.0 * consts.elementary_charge + ), + "anti-charm": _PropertyTuple( + mass=1.2730e9 * consts.elementary_charge / consts.speed_of_light**2, + charge=-2.0 / 3.0 * consts.elementary_charge, + ), + "anti-top": _PropertyTuple( + mass=172.57e9 * consts.elementary_charge / consts.speed_of_light**2, + charge=-2.0 / 3.0 * consts.elementary_charge, + ), + "anti-down": _PropertyTuple( + mass=4.70e6 * consts.elementary_charge / consts.speed_of_light**2, charge=1.0 / 3.0 * consts.elementary_charge + ), + "anti-strange": _PropertyTuple( + mass=93.5 * consts.elementary_charge / consts.speed_of_light**2, charge=1.0 / 3.0 * consts.elementary_charge + ), + "anti-bottom": _PropertyTuple( + mass=4.138 * consts.elementary_charge / consts.speed_of_light**2, charge=1.0 / 3.0 * consts.elementary_charge + ), +} + +_leptons = { + "electron": _PropertyTuple(mass=consts.electron_mass, charge=-consts.elementary_charge), + "muon": _PropertyTuple( + mass=pdg.connect().get_particle_by_name("mu-").mass * 1e9 * consts.elementary_charge / consts.speed_of_light**2, + charge=pdg.connect().get_particle_by_name("mu-").charge * consts.elementary_charge, + ), + "tau": _PropertyTuple( + mass=pdg.connect().get_particle_by_name("tau-").mass + * 1e9 + * consts.elementary_charge + / consts.speed_of_light**2, + charge=pdg.connect().get_particle_by_name("tau-").charge * consts.elementary_charge, + ), + "positron": _PropertyTuple(mass=consts.electron_mass, charge=consts.elementary_charge), + "anti-muon": _PropertyTuple( + mass=pdg.connect().get_particle_by_name("mu+").mass * 1e9 * consts.elementary_charge / consts.speed_of_light**2, + charge=pdg.connect().get_particle_by_name("mu+").charge * consts.elementary_charge, + ), + "anti-tau": _PropertyTuple( + mass=pdg.connect().get_particle_by_name("tau+").mass + * 1e9 + * consts.elementary_charge + / consts.speed_of_light**2, + charge=pdg.connect().get_particle_by_name("tau+").charge * consts.elementary_charge, + ), +} + +_nucleons = { + "proton": _PropertyTuple(mass=consts.proton_mass, charge=consts.elementary_charge), + "anti-proton": _PropertyTuple(mass=consts.proton_mass, charge=-consts.elementary_charge), + "neutron": _PropertyTuple(mass=consts.neutron_mass, charge=None), + "anti-neutron": _PropertyTuple(mass=consts.neutron_mass, charge=None), +} + +_neutrinos = { + "electron-neutrino": _PropertyTuple(mass=0.0, charge=0.0), + "muon-neutrino": _PropertyTuple(mass=0.0, charge=0.0), + "tau-neutrino": _PropertyTuple(mass=0.0, charge=0.0), + "anti-electron-neutrino": _PropertyTuple(mass=0.0, charge=0.0), + "anti-muon-neutrino": _PropertyTuple(mass=0.0, charge=0.0), + "anti-tau-neutrino": _PropertyTuple(mass=0.0, charge=0.0), +} + +_gauge_bosons = { + "photon": _PropertyTuple(mass=None, charge=0.0), + "gluon": _PropertyTuple(mass=None, charge=0.0), + "w-plus-boson": _PropertyTuple( + mass=pdg.connect().get_particle_by_name("W+").mass * 1e9 * consts.elementary_charge / consts.speed_of_light**2, + charge=pdg.connect().get_particle_by_name("W+").charge * consts.elementary_charge, + ), + "w-minus-boson": _PropertyTuple( + mass=pdg.connect().get_particle_by_name("W-").mass * 1e9 * consts.elementary_charge / consts.speed_of_light**2, + charge=pdg.connect().get_particle_by_name("W-").charge * consts.elementary_charge, + ), + "z-boson": _PropertyTuple( + mass=pdg.connect().get_particle_by_name("Z").mass * 1e9 * consts.elementary_charge / consts.speed_of_light**2, + charge=pdg.connect().get_particle_by_name("Z").charge * consts.elementary_charge, + ), + "higgs": _PropertyTuple( + mass=pdg.connect().get_particle_by_name("H").mass * 1e9 * consts.elementary_charge / consts.speed_of_light**2, + charge=pdg.connect().get_particle_by_name("H").charge * consts.elementary_charge, + ), +} + +non_element_particle_type_properties = {} +non_element_particle_type_properties.update(_quarks) +non_element_particle_type_properties.update(_leptons) +non_element_particle_type_properties.update(_neutrinos) +non_element_particle_type_properties.update(_nucleons) +non_element_particle_type_properties.update(_gauge_bosons) diff --git a/lib/python/picongpu/picmi/simulation.py b/lib/python/picongpu/picmi/simulation.py index d91a5be7310..d3d8ea76e22 100644 --- a/lib/python/picongpu/picmi/simulation.py +++ b/lib/python/picongpu/picmi/simulation.py @@ -82,11 +82,11 @@ def __init__( picongpu_interaction: typing.Optional[Interaction] = None, **keyword_arguments, ): - # need to call pydantic.BaseModel constructor first, - # pydantic class instance must have been initialized before we may call the PICMI super class constructor to - # get a properly initialized pydantic model + if picongpu_template_dir is not None: + self.picongpu_template_dir = str(picongpu_template_dir) + else: + self.picongpu_template_dir = picongpu_template_dir - self.picongpu_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 @@ -108,7 +108,7 @@ def __init__( raise ValueError("picongpu_template_dir MUST NOT be empty string") if picongpu_template_dir is not None: template_path = pathlib.Path(picongpu_template_dir) - if template_path.is_dir(): + if not template_path.is_dir(): raise ValueError("picongpu_template_dir must be existing directory") def __yee_compute_cfl_or_delta_t(self) -> None: @@ -318,11 +318,11 @@ def __get_init_manager(self) -> pypicongpu.species.InitManager: ## @details cache to reuse *exactly the same* object in operations pypicongpu_by_picmi_species = {} - ionization_model_conversion_by_picmi_species = {} + ionization_model_conversion_by_species = {} for picmi_species in self.species: - pypicongpu_species, ionization_model_conversion = picmi_species.get_as_pypicongpu() + 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_picmi_species[picmi_species] = ionization_model_conversion + ionization_model_conversion_by_species[picmi_species] = ionization_model_conversion initmgr.all_species.append(pypicongpu_species) # fill inter-species dependencies @@ -330,8 +330,8 @@ def __get_init_manager(self) -> pypicongpu.species.InitManager: # ionization electron species need to be set after species translation is complete since the PyPIConGPU electron # species is not known by the PICMI ion species if self.picongpu_interaction is not None: - self.picongpu_interaction.fill_in_ionization_electrons( - pypicongpu_by_picmi_species, ionization_model_conversion_by_picmi_species + self.picongpu_interaction.fill_in_ionization_electron_species( + pypicongpu_by_picmi_species, ionization_model_conversion_by_species ) # operations with inter-species dependencies @@ -363,7 +363,7 @@ def write_input_file( if pypicongpu_simulation is None: pypicongpu_simulation = self.get_as_pypicongpu() - self.__runner = pypicongpu.runnerRunner(pypicongpu_simulation, self.picongpu_template_dir, setup_dir=file_name) + self.__runner = pypicongpu.runner.Runner(pypicongpu_simulation, self.picongpu_template_dir, setup_dir=file_name) self.__runner.generate() def picongpu_add_custom_user_input(self, custom_user_input: pypicongpu.customuserinput.InterfaceCustomUserInput): @@ -453,12 +453,12 @@ def get_as_pypicongpu(self) -> pypicongpu.simulation.Simulation: def picongpu_run(self) -> None: """build and run PIConGPU simulation""" if self.__runner is None: - self.__runner = pypicongpu.runnerRunner(self.get_as_pypicongpu(), self.picongpu_template_dir) + self.__runner = pypicongpu.runner.Runner(self.get_as_pypicongpu(), self.picongpu_template_dir) self.__runner.generate() self.__runner.build() self.__runner.run() def picongpu_get_runner(self) -> pypicongpu.runner.Runner: if self.__runner is None: - self.__runner = pypicongpu.runnerRunner(self.get_as_pypicongpu(), self.picongpu_template_dir) + self.__runner = pypicongpu.runner.Runner(self.get_as_pypicongpu(), self.picongpu_template_dir) return self.__runner diff --git a/lib/python/picongpu/picmi/species.py b/lib/python/picongpu/picmi/species.py index dc93a7d5b09..6820d16c431 100644 --- a/lib/python/picongpu/picmi/species.py +++ b/lib/python/picongpu/picmi/species.py @@ -8,161 +8,25 @@ from .. import pypicongpu from ..pypicongpu.species.util.element import Element from .interaction import InteractionInterface +from .predefinedparticletypeproperties import non_element_particle_type_properties, _PropertyTuple import picmistandard import typing +import typeguard import pydantic import pydantic_core -import collections import logging import re from scipy import constants as consts -import pdg +@typeguard.typechecked class Species(picmistandard.PICMI_Species): """PICMI object for a (single) particle species""" - _PropertyTuple: collections.namedtuple = collections.namedtuple("_PropertyTuple", ["mass", "charge"]) - - # based on 2024 Particle data Group values - _quarks = { - "up": _PropertyTuple( - mass=2.16e6 * consts.elementary_charge / consts.speed_of_light**2, - charge=2.0 / 3.0 * consts.elementary_charge, - ), - "charm": _PropertyTuple( - mass=1.2730e9 * consts.elementary_charge / consts.speed_of_light**2, - charge=2.0 / 3.0 * consts.elementary_charge, - ), - "top": _PropertyTuple( - mass=172.57e9 * consts.elementary_charge / consts.speed_of_light**2, - charge=2.0 / 3.0 * consts.elementary_charge, - ), - "down": _PropertyTuple( - mass=4.70e6 * consts.elementary_charge / consts.speed_of_light**2, - charge=-1.0 / 3.0 * consts.elementary_charge, - ), - "strange": _PropertyTuple( - mass=93.5 * consts.elementary_charge / consts.speed_of_light**2, - charge=-1.0 / 3.0 * consts.elementary_charge, - ), - "bottom": _PropertyTuple( - mass=4.138 * consts.elementary_charge / consts.speed_of_light**2, - charge=-1.0 / 3.0 * consts.elementary_charge, - ), - "anti-up": _PropertyTuple( - mass=2.16e6 * consts.elementary_charge / consts.speed_of_light**2, - charge=-2.0 / 3.0 * consts.elementary_charge, - ), - "anti-charm": _PropertyTuple( - mass=1.2730e9 * consts.elementary_charge / consts.speed_of_light**2, - charge=-2.0 / 3.0 * consts.elementary_charge, - ), - "anti-top": _PropertyTuple( - mass=172.57e9 * consts.elementary_charge / consts.speed_of_light**2, - charge=-2.0 / 3.0 * consts.elementary_charge, - ), - "anti-down": _PropertyTuple( - mass=4.70e6 * consts.elementary_charge / consts.speed_of_light**2, - charge=1.0 / 3.0 * consts.elementary_charge, - ), - "anti-strange": _PropertyTuple( - mass=93.5 * consts.elementary_charge / consts.speed_of_light**2, charge=1.0 / 3.0 * consts.elementary_charge - ), - "anti-bottom": _PropertyTuple( - mass=4.138 * consts.elementary_charge / consts.speed_of_light**2, - charge=1.0 / 3.0 * consts.elementary_charge, - ), - } - - _leptons = { - "electron": _PropertyTuple(mass=consts.electron_mass, charge=-consts.elementary_charge), - "muon": _PropertyTuple( - mass=pdg.connect().get_particle_by_name("mu-").mass - * 1e9 - * consts.elementary_charge - / consts.speed_of_light**2, - charge=pdg.connect().get_particle_by_name("mu-").charge * consts.elementary_charge, - ), - "tau": _PropertyTuple( - mass=pdg.connect().get_particle_by_name("tau-").mass - * 1e9 - * consts.elementary_charge - / consts.speed_of_light**2, - charge=pdg.connect().get_particle_by_name("tau-").charge * consts.elementary_charge, - ), - "positron": _PropertyTuple(mass=consts.electron_mass, charge=consts.elementary_charge), - "anti-muon": _PropertyTuple( - mass=pdg.connect().get_particle_by_name("mu+").mass - * 1e9 - * consts.elementary_charge - / consts.speed_of_light**2, - charge=pdg.connect().get_particle_by_name("mu+").charge * consts.elementary_charge, - ), - "anti-tau": _PropertyTuple( - mass=pdg.connect().get_particle_by_name("tau+").mass - * 1e9 - * consts.elementary_charge - / consts.speed_of_light**2, - charge=pdg.connect().get_particle_by_name("tau+").charge * consts.elementary_charge, - ), - } - - _nucleons = { - "proton": _PropertyTuple(mass=consts.proton_mass, charge=consts.elementary_charge), - "anti-proton": _PropertyTuple(mass=consts.proton_mass, charge=-consts.elementary_charge), - "neutron": _PropertyTuple(mass=consts.neutron_mass, charge=None), - "anti-neutron": _PropertyTuple(mass=consts.neutron_mass, charge=None), - } - - _neutrinos = { - "electron-neutrino": _PropertyTuple(mass=0.0, charge=0.0), - "muon-neutrino": _PropertyTuple(mass=0.0, charge=0.0), - "tau-neutrino": _PropertyTuple(mass=0.0, charge=0.0), - "anti-electron-neutrino": _PropertyTuple(mass=0.0, charge=0.0), - "anti-muon-neutrino": _PropertyTuple(mass=0.0, charge=0.0), - "anti-tau-neutrino": _PropertyTuple(mass=0.0, charge=0.0), - } - - _gauge_bosons = { - "photon": _PropertyTuple(mass=0.0, charge=0.0), - "gluon": _PropertyTuple(mass=0.0, charge=0.0), - "w-plus-boson": _PropertyTuple( - mass=pdg.connect().get_particle_by_name("W+").mass - * 1e9 - * consts.elementary_charge - / consts.speed_of_light**2, - charge=pdg.connect().get_particle_by_name("W+").charge * consts.elementary_charge, - ), - "w-minus-boson": _PropertyTuple( - mass=pdg.connect().get_particle_by_name("W-").mass - * 1e9 - * consts.elementary_charge - / consts.speed_of_light**2, - charge=pdg.connect().get_particle_by_name("W-").charge * consts.elementary_charge, - ), - "z-boson": _PropertyTuple( - mass=pdg.connect().get_particle_by_name("Z").mass - * 1e9 - * consts.elementary_charge - / consts.speed_of_light**2, - charge=pdg.connect().get_particle_by_name("Z").charge * consts.elementary_charge, - ), - "higgs": _PropertyTuple( - mass=pdg.connect().get_particle_by_name("H").mass - * 1e9 - * consts.elementary_charge - / consts.speed_of_light**2, - charge=pdg.connect().get_particle_by_name("H").charge * consts.elementary_charge, - ), - } - - __non_element_particle_type_properties = ( - ({}).update(_quarks).update(_leptons).update(_nucleons).update(_neutrinos).update(_gauge_bosons) - ) + __non_element_particle_type_properties = non_element_particle_type_properties """ mass/charge to use when passed a non-element particle_type @@ -177,10 +41,11 @@ class Species(picmistandard.PICMI_Species): picongpu_fixed_charge = pypicongpu.util.build_typesafe_property(typing.Optional[bool]) - interactions = pypicongpu.util.build_typesafe_property(type(None)) + 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 @classmethod def __get_pydantic_core_schema__( @@ -228,7 +93,7 @@ def __init__(self, picongpu_fixed_charge=None, **keyword_arguments): self.picongpu_element = None # let PICMI class handle remaining init - picmistandard.PICMI_Species.__init__(**keyword_arguments) + picmistandard.PICMI_Species.__init__(self, **keyword_arguments) @staticmethod def __get_temperature_kev_by_rms_velocity( @@ -297,13 +162,16 @@ def __maybe_apply_particle_type(self) -> None: # unknown particle type raise ValueError(f"Species {self.name} has unknown particle type {self.particle_type}") - def has_ionization(self, interaction: InteractionInterface) -> bool: + def has_ionization(self, interaction: InteractionInterface | 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 + def is_ion(self) -> bool: """ is species an ion? @@ -315,7 +183,7 @@ def is_ion(self) -> bool: return False return True - def __check_ionization_configuration(self, interaction: InteractionInterface) -> None: + def __check_ionization_configuration(self, interaction: InteractionInterface | None) -> None: """ check species ioniaztion- and species- configuration are compatible @@ -346,6 +214,13 @@ def __check_ionization_configuration(self, interaction: InteractionInterface) -> ), 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 not self.picongpu_fixed_charge, ( f"Species {self.name} configured both as fixed charge ion and ion with ionization, may be " @@ -369,33 +244,39 @@ def __check_ionization_configuration(self, interaction: InteractionInterface) -> ) self.__warned_already = True - # charge_state may be set or None indicating some fixed number of bound electrons or fully ion + # 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: InteractionInterface) -> None: + def __check_interaction_configuration(self, interaction: InteractionInterface | None) -> None: """check all interactions sub groups for compatibility with this species configuration""" self.__check_ionization_configuration(interaction) - def check(self, interaction: InteractionInterface) -> None: + def check(self, interaction: InteractionInterface | 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 not None - ), "charge must be set explicitly if no particle type or custom particle type is specified" + self.charge is None + ), f"Species' {self.name}, charge is specified implicitly via particle type, do NOT set charge explictly" assert ( - self.mass is not None - ), "mass must be set explicitly if no particle type or custom particle type is specified" - else: - assert self.charge is None, "charge is specify implicitly via particle type, do NOT set charge explictly" - assert self.mass is None, "mass is specify implicitly via particle type, do NOT set mass explictly" + 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: InteractionInterface) -> pypicongpu.species.Species: + def get_as_pypicongpu( + self, interaction: InteractionInterface | None + ) -> tuple[ + pypicongpu.species.Species, None | dict[typing.Any, pypicongpu.species.constant.ionizationmodel.IonizationModel] + ]: """ translate PICMI species object to equivalent PyPIConGPU species object @@ -447,11 +328,13 @@ def get_as_pypicongpu(self, interaction: InteractionInterface) -> pypicongpu.spe 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 return s, pypicongpu_model_by_picmi_model def get_independent_operations( - self, pypicongpu_species: pypicongpu.species.Species, interaction: InteractionInterface + self, pypicongpu_species: pypicongpu.species.Species, interaction: InteractionInterface | None ) -> list[pypicongpu.species.operation.Operation]: # assure consistent state of species self.check(interaction) diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/ionizationcurrent.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/ionizationcurrent.py index 6cbd45ddd40..a00c469f623 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/ionizationcurrent.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/ionizationcurrent.py @@ -10,7 +10,10 @@ import pydantic import typing +import typeguard + +@typeguard.typechecked class IonizationCurrent(Constant, pydantic.BaseModel): """base class for all ionization currents models""" diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/none_.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/none_.py index c99d0f6a3dc..7019a0db442 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/none_.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/none_.py @@ -7,6 +7,9 @@ from .ionizationcurrent import IonizationCurrent +import typeguard + +@typeguard.typechecked class None_(IonizationCurrent): PICONGPU_NAME: str = "None" diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py index 8ed4644ac2b..bbf1276f9a5 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py @@ -12,8 +12,10 @@ import pydantic import typing +import typeguard +@typeguard.typechecked class IonizationModel(pydantic.BaseModel, Constant): """ base class for an ground state only ionization models of an ion species @@ -86,7 +88,7 @@ def get_generic_rendering_context(self) -> dict[str, typing.Any]: ionization_current=self.ionization_current, ).get_rendering_context() - def get_species_dependencies(self) -> list[type]: + def get_species_dependencies(self) -> list[typing.Any]: self.check() return [self.ionization_electron_species] diff --git a/lib/python/picongpu/pypicongpu/species/util/element.py b/lib/python/picongpu/pypicongpu/species/util/element.py index 3936e73da5a..491b3de9323 100644 --- a/lib/python/picongpu/pypicongpu/species/util/element.py +++ b/lib/python/picongpu/pypicongpu/species/util/element.py @@ -8,12 +8,14 @@ from ...rendering import RenderedObject import pydantic +import typeguard import typing import scipy import periodictable import re +@typeguard.typechecked class Element(RenderedObject, pydantic.BaseModel): """ Denotes an element from the periodic table of elements @@ -32,14 +34,16 @@ class Element(RenderedObject, pydantic.BaseModel): _store: typing.Optional[periodictable.core.Element] = None @staticmethod - def parse_openpmd_isotopes(openpmd_name: str) -> tuple[int, str]: + def parse_openpmd_isotopes(openpmd_name: str) -> tuple[int | None, str]: + if openpmd_name == "": + raise ValueError(f"{openpmd_name} is not a valid openPMD particle type") if openpmd_name[0] != "#" and re.match(r"[A-Z][a-z]?$|n$", openpmd_name): return None, openpmd_name m = re.match(r"#([1-9][0-9]*)([A-Z][a-z]?)$", openpmd_name) if m is None: - raise ValueError(f"{openpmd_name} is not a valid openPMD isotope descriptor") + raise ValueError(f"{openpmd_name} is not a valid openPMD particle type") mass_number = int(m.group(1)) symbol = m.group(2) @@ -69,6 +73,7 @@ def __init__(self, openpmd_name: str) -> None: mass_number, openpmd_name = Element.parse_openpmd_isotopes(openpmd_name) + found = False # search for name in periodic table for element in periodictable.elements: if openpmd_name == element.symbol: @@ -76,10 +81,10 @@ def __init__(self, openpmd_name: str) -> None: self._store = element else: self._store = element[mass_number] - return + found = True - # not found - raise NameError(f"unknown element: {openpmd_name}") + if not found: + raise NameError(f"unknown element: {openpmd_name}") def get_picongpu_name(self) -> str: """ diff --git a/test/python/picongpu/quick/picmi/simulation.py b/test/python/picongpu/quick/picmi/simulation.py index 0619512bf01..14f2f0919ca 100644 --- a/test/python/picongpu/quick/picmi/simulation.py +++ b/test/python/picongpu/quick/picmi/simulation.py @@ -6,19 +6,18 @@ """ from picongpu import picmi +from picongpu.pypicongpu import species, customuserinput +from picongpu.picmi.interaction.ionization.fieldionization import ADK, ADKVariant +from picongpu.picmi.interaction import Interaction import unittest - -import typeguard -import typing - -from picongpu.pypicongpu import species, customuserinput -from copy import deepcopy -import logging import tempfile import shutil import os import pathlib +import typeguard +import typing +import copy @typeguard.typechecked @@ -164,9 +163,11 @@ def test_explicit_typical_ppc(self): layout4 = picmi.PseudoRandomLayout(n_macroparticles_per_cell=4) # placed with entire placement and 3ppc - sim.add_species(picmi.Species(name="dummy2", mass=3, density_scale=4, initial_distribution=profile), layout3) + sim.add_species( + picmi.Species(name="dummy2", mass=3, charge=4, density_scale=4, initial_distribution=profile), layout3 + ) # placed with default ratio of 1 and 4ppc - sim.add_species(picmi.Species(name="dummy3", mass=3, initial_distribution=profile), layout4) + 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)) @@ -200,18 +201,18 @@ def test_invalid_placement(self): # both profile and layout must be given with self.assertRaisesRegex(Exception, ".*initial.*distribution.*"): # no profile - sim = deepcopy(self.sim) + sim = copy.deepcopy(self.sim) sim.add_species(picmi.Species(name="dummy3"), layout) sim.get_as_pypicongpu() with self.assertRaisesRegex(Exception, ".*layout.*"): # no layout - sim = deepcopy(self.sim) + sim = copy.deepcopy(self.sim) sim.add_species(picmi.Species(name="dummy3", initial_distribution=profile), None) sim.get_as_pypicongpu() with self.assertRaisesRegex(Exception, ".*initial.*distribution.*"): # neither profile nor layout, but ratio - sim = deepcopy(self.sim) + sim = copy.deepcopy(self.sim) sim.add_species(picmi.Species(name="dummy3", density_scale=7), None) sim.get_as_pypicongpu() @@ -291,7 +292,7 @@ def test_operations_simple_density_translated(self): def test_operation_not_placed_translated(self): """non-placed species are correctly translated""" - self.sim.add_species(picmi.Species(name="notplaced", initial_distribution=None), None) + self.sim.add_species(picmi.Species(name="notplaced", mass=1, initial_distribution=None), None) pypicongpu = self.sim.get_as_pypicongpu() @@ -363,242 +364,45 @@ def test_moving_window(self): self.assertAlmostEqual(pypic.moving_window.move_point, 0.9) self.assertEqual(pypic.moving_window.stop_iteration, None) - def test_ionization_electron_explicit(self): - """electrons for ionization can be specified explicitly""" - # note: the difficulty here is preserving the PICMI- -> PICMI-object - # relationship and translating it into a PyPIConGPU- -> PyPIConGPU - # relationship - - electrons1 = picmi.Species(name="e1", mass=picmi.constants.m_e, charge=-picmi.constants.q_e) - electrons2 = picmi.Species(name="e2", charge=2, mass=3) - ion = picmi.Species( - name="ion", - particle_type="N", - charge_state=0, - picongpu_ionization_electrons=electrons2, - ) - - sim = self.sim - sim.add_species(ion, None) - sim.add_species(electrons1, None) - sim.add_species(electrons2, None) - - with self.assertLogs(level=logging.INFO) as caught_logs: - # required b/c self.assertNoLogs is not yet available - logging.info("TESTINFO") - pypic_sim = sim.get_as_pypicongpu() - # no logs on electrons at all - electron_logs = list(filter(lambda line: "electron" in line, caught_logs.output)) - self.assertEqual([], electron_logs) - - # ensure species actually exists - pypic_species_by_name = dict( - map( - lambda species: (species.name, species), - pypic_sim.init_manager.all_species, - ) - ) - self.assertEqual({"e1", "e2", "ion"}, set(pypic_species_by_name.keys())) - - pypic_ion = pypic_species_by_name["ion"] - self.assertTrue(pypic_ion.has_constant_of_type(species.constant.Ionizers)) - - ionizers = pypic_ion.get_constant_by_type(species.constant.Ionizers) - - # relationship preserved: - self.assertTrue(ionizers.electron_species is pypic_species_by_name["e2"]) - - def test_ionization_electron_resolution_added(self): - """add electron species if one is required but missing""" - # if electrons to use for ionization are not given explicitly they are - # guessed - - profile = picmi.UniformDistribution(3) - - # no electrons exist -> create one species - ## - hydrogen = picmi.Species( - name="hydrogen", - particle_type="H", - charge_state=+1, - initial_distribution=profile, - ) - sim = self.__get_sim() - sim.add_species(hydrogen, self.layout) - - with self.assertLogs(level=logging.INFO) as caught_logs: - pypic_sim = sim.get_as_pypicongpu() - - # electron species has been added to **PICMI** object - self.assertNotEqual(None, hydrogen.picongpu_ionization_electrons) - - # info that electron species has been added - self.assertNotEqual([], caught_logs.output) - electron_logs = list(filter(lambda line: "electron" in line, caught_logs.output)) - self.assertEqual(1, len(electron_logs)) - - # extra species exists - self.assertEqual(2, len(pypic_sim.init_manager.all_species)) - - # pypic_sim works - pypic_sim.init_manager.bake() - self.assertNotEqual({}, pypic_sim.get_rendering_context()) - - def test_ionization_electron_resolution_guessed(self): - """electron species for ionization is guessed if one exists""" - # two methods for electrons: create electron by setting mass & charge - # like electrons, or by setting the particle type explicitly - for electron_explicit in [True, False]: - profile = picmi.UniformDistribution(2) - hydrogen = picmi.Species( - name="hydrogen", - particle_type="H", - charge_state=+1, - initial_distribution=profile, - ) - - if electron_explicit: - # case A: electrons identified by particle_type - electron = picmi.Species(name="my_e", particle_type="electron") - else: - # case B: electrons identified by mass & charge - electron = picmi.Species(name="my_e", mass=picmi.constants.m_e, charge=-picmi.constants.q_e) - - # note: - # guessing only works if there is **exactly one** electron species - picmi_sim = self.__get_sim() - picmi_sim.add_species(hydrogen, self.layout) - picmi_sim.add_species(electron, None) - - pypic_sim = picmi_sim.get_as_pypicongpu() - - # association happened inside PICMI - self.assertEqual(electron, hydrogen.picongpu_ionization_electrons) - - # only 2 species total - self.assertEqual(2, len(pypic_sim.init_manager.all_species)) - - # association correct inside of pypicongpu - for pypic_species in pypic_sim.init_manager.all_species: - if "my_e" == pypic_species.name: - continue - self.assertEqual("hydrogen", pypic_species.name) - - ionizers_const = pypic_species.get_constant_by_type(species.constant.Ionizers) - self.assertEqual("my_e", ionizers_const.electron_species.name) - - # pypic_sim works - pypic_sim.init_manager.bake() - self.assertNotEqual({}, pypic_sim.get_rendering_context()) - - def test_ionization_electron_resolution_guess_ambiguous(self): - """electron species for ionization is not guessed if multiple exist""" - e1 = picmi.Species(name="the_first_electrons", particle_type="electron") - e2 = picmi.Species( - name="the_other_electrons", - mass=picmi.constants.m_e, - charge=-picmi.constants.q_e, - ) - profile = picmi.UniformDistribution(7) - helium = picmi.Species( - name="helium", - particle_type="He", - charge_state=+1, - initial_distribution=profile, - ) - - sim = self.sim - sim.add_species(e1, None) - sim.add_species(e2, None) - sim.add_species(helium, self.layout) - - # two electron species exist, therefore can't guess which one to use - # for ionization -> raise - with self.assertRaisesRegex(Exception, ".*ambiguous.*"): - sim.get_as_pypicongpu() - - def test_ionization_electron_not_added(self): - """electrons must be used, even if not added via add_species()""" - e1 = picmi.Species(name="my_e", particle_type="electron") - ion = picmi.Species( - name="helium", - particle_type="He", - charge_state=+2, - picongpu_ionization_electrons=e1, - ) - sim = self.sim - - # **ONLY** ion is added to sim - sim.add_species(ion, None) - - with self.assertRaisesRegex(AssertionError, ".*my_e.*helium.*picongpu_ionization_species.*"): - sim.get_as_pypicongpu() - - def test_ionization_added_electron_namecollision(self): - """automatically added electron species avoids name collisions""" - existing_electron_names = ["electron", "e", "e_", "E", "e__"] - - for name in existing_electron_names: - self.sim.add_species(picmi.Species(name=name, mass=1, charge=1), None) - - # add ion species so electrons are actually guessed - self.sim.add_species(picmi.Species(name="ion", particle_type="He", charge_state=2), None) - - # catch logs so they don't show up - with self.assertLogs(level="INFO"): - pypic_sim = self.sim.get_as_pypicongpu() - - # one extra species: the electrons generated - self.assertEqual( - 1 + len(existing_electron_names + ["ion"]), - len(pypic_sim.init_manager.all_species), - ) - - # still works, i.e. there are no name conflicts - pypic_sim.init_manager.bake() - self.assertNotEqual({}, pypic_sim.get_rendering_context()) - - def test_ionization_electrons_guess_not_invoked(self): - """ionization electron guessing is only invoked if required""" - # produce ambiguous guess, but do not add species that would require - # guessing -> must work - - e1 = picmi.Species(name="e1", particle_type="electron") - e2 = picmi.Species(name="e2", particle_type="electron") - - sim = self.sim - sim.add_species(e1, None) - sim.add_species(e2, None) - - # just works: - pypic_sim = sim.get_as_pypicongpu() - self.assertEqual(2, len(pypic_sim.init_manager.all_species)) - self.assertNotEqual({}, pypic_sim.get_rendering_context()) - - def test_ionization_methods_added(self): - """ionization methods are added as applicable""" + def test_add_ionization_model(self): + """ionization model is added correctly""" e = picmi.Species(name="e", particle_type="electron") ion1 = picmi.Species(name="hydrogen", particle_type="H", charge_state=+1) ion2 = picmi.Species(name="nitrogen", particle_type="N", charge_state=+2) + ionization_model_1 = ADK( + ADK_variant=ADKVariant.LinearPolarization, + ionization_current=None, + ion_species=ion1, + ionization_electron_species=e, + ) + ionization_model_2 = ADK( + ADK_variant=ADKVariant.LinearPolarization, + ionization_current=None, + ion_species=ion2, + ionization_electron_species=e, + ) + interaction = Interaction(ground_state_ionization_model_list=[ionization_model_1, ionization_model_2]) + sim = self.sim sim.add_species(e, None) sim.add_species(ion1, None) sim.add_species(ion2, None) + # in use should be set via simulation constructor + sim.picongpu_interaction = interaction + pypic_sim = sim.get_as_pypicongpu() initmgr = pypic_sim.init_manager operation_types = list(map(lambda op: type(op), initmgr.all_operations)) - self.assertEqual(1, operation_types.count(species.operation.NoBoundElectrons)) - self.assertEqual(1, operation_types.count(species.operation.SetBoundElectrons)) + self.assertEqual(2, operation_types.count(species.operation.SetBoundElectrons)) for op in initmgr.all_operations: - if isinstance(op, species.operation.NoBoundElectrons): - self.assertEqual("hydrogen", op.species.name) - elif isinstance(op, species.operation.SetBoundElectrons): - self.assertEqual("nitrogen", op.species.name) + if isinstance(op, species.operation.SetBoundElectrons) and op.species.name == "Nitrogen": self.assertEqual(5, op.bound_electrons) + if isinstance(op, species.operation.SetBoundElectrons) and op.species.name == "Hydrogen": + self.assertEqual(0, op.bound_electrons) # other ops (position...): ignore def test_write_input_file(self): diff --git a/test/python/picongpu/quick/picmi/species.py b/test/python/picongpu/quick/picmi/species.py index 1614fee951f..624784e84a6 100644 --- a/test/python/picongpu/quick/picmi/species.py +++ b/test/python/picongpu/quick/picmi/species.py @@ -11,6 +11,8 @@ 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 @@ -30,6 +32,7 @@ def setUp(self): name="nitrogen", charge_state=+3, particle_type="N", + picongpu_fixed_charge=True, initial_distribution=self.profile_uniform, ) @@ -51,7 +54,8 @@ def test_basic(self): """check that all params are translated""" # check that translation works for s in [self.species_electron, self.species_nitrogen]: - pypic = s.get_as_pypicongpu() + pypic, rest = s.get_as_pypicongpu(None) + del rest self.assertEqual(pypic.name, s.name) def test_mandatory(self): @@ -63,14 +67,14 @@ def test_mandatory(self): for invalid_species in species_invalid_list: with self.assertRaises(AssertionError): - invalid_species.get_as_pypicongpu() + 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 = picmi_s.get_as_pypicongpu() + 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) @@ -82,51 +86,62 @@ def test_density_scale(self): """density scale is correctly transformed""" # simple example picmi_s = picmi.Species(name="any", density_scale=37.2) - pypicongpu_s = picmi_s.get_as_pypicongpu() + 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 = picmi_s.get_as_pypicongpu() + 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 = picmi_s.get_as_pypicongpu() + 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)) + 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) + 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 = picmi_s.get_as_pypicongpu() + 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) + 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)) + self.assertNotEqual(None, picmi_s.get_independent_operations(pypicongpu_s, None)) def test_get_independent_operations_ionization_set_bound_electrons(self): """SetBoundElectrons is properly generated""" picmi_species = picmi.Species(name="nitrogen", particle_type="N", charge_state=2) - pypic_species = picmi_species.get_as_pypicongpu() + 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, + ) + ] + ) - ops = picmi_species.get_independent_operations(pypic_species) + 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.SetBoundElectrons)) self.assertEqual(0, ops_types.count(species.operation.NoBoundElectrons)) @@ -138,28 +153,12 @@ def test_get_independent_operations_ionization_set_bound_electrons(self): self.assertEqual(pypic_species, op.species) self.assertEqual(5, op.bound_electrons) - def test_get_independent_operations_ionization_no_bound_electrons(self): - """fully ionized ions get NoBoundElectrons""" - picmi_species = picmi.Species(name="hydrogen", particle_type="H", charge_state=1) - pypic_species = picmi_species.get_as_pypicongpu() - - ops = picmi_species.get_independent_operations(pypic_species) - ops_types = list(map(lambda op: type(op), ops)) - self.assertEqual(1, ops_types.count(species.operation.NoBoundElectrons)) - self.assertEqual(0, ops_types.count(species.operation.SetBoundElectrons)) - - for op in ops: - if not isinstance(op, species.operation.NoBoundElectrons): - continue - - self.assertEqual(pypic_species, op.species) - 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_fully_ionized=True) - pypic_species = picmi_species.get_as_pypicongpu() + 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) + 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.SetBoundElectrons)) @@ -190,8 +189,8 @@ def test_get_independent_operations_momentum(self): picmi_s = picmi.Species(name="name", mass=1, initial_distribution=dist) - pypicongpu_s = picmi_s.get_as_pypicongpu() - ops = picmi_s.get_independent_operations(pypicongpu_s) + pypicongpu_s, rest = picmi_s.get_as_pypicongpu(None) + ops = picmi_s.get_independent_operations(pypicongpu_s, None) momentum_ops = list( filter( @@ -242,13 +241,26 @@ def get_rms_species(rms_velocity): for invalid_rms_vector in invalid_rms_vectors: rms_species = get_rms_species(invalid_rms_vector) with self.assertRaisesRegex(Exception, ".*(equal|same).*"): - pypicongpu_species = rms_species.get_as_pypicongpu() - rms_species.get_independent_operations(pypicongpu_species) + pypicongpu_species, rest = rms_species.get_as_pypicongpu(None) + rms_species.get_independent_operations(pypicongpu_species, None) def test_from_speciestype(self): - """mass & charge weill be derived from species type""" - picmi_species = picmi.Species(name="nitrogen", particle_type="N") - pypic_species = picmi_species.get_as_pypicongpu() + """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)) @@ -257,7 +269,7 @@ def test_from_speciestype(self): 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 + 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()) @@ -267,41 +279,35 @@ def test_from_speciestype(self): 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).get_as_pypicongpu() + 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).get_as_pypicongpu() + 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) - self.assertTrue(not no_ionizers_picmi.has_ionizers()) - - no_ionizers_pypic = no_ionizers_picmi.get_as_pypicongpu() - self.assertTrue(not no_ionizers_pypic.has_constant_of_type(species.constant.Ionizers)) - - # explicit charge state: has ionizers - explicit_picmi = picmi.Species(name="nitrogen", particle_type="N", charge_state=0) - self.assertTrue(explicit_picmi.has_ionizers()) - - explicit_pypic = explicit_picmi.get_as_pypicongpu() - self.assertTrue(explicit_pypic.has_constant_of_type(species.constant.Ionizers)) + 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") - self.assertTrue(not with_warn_picmi.has_ionizers()) + with_warn_picmi = picmi.Species(name="HELIUM", particle_type="He", picongpu_fixed_charge=True) - with_warn_pypic = with_warn_picmi.get_as_pypicongpu() - self.assertTrue(not with_warn_pypic.has_constant_of_type(species.constant.Ionizers)) + 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.*fully.*ionized.*picongpu_fully_ionized.*", + ".*HELIUM.*fixed charge state.*", implicit_logs.output[0], ) ) @@ -309,12 +315,11 @@ def test_has_ionizers(self): 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_fully_ionized=True) - self.assertTrue(not no_warn_picmi.has_ionizers()) - no_warn_pypic = no_warn_picmi.get_as_pypicongpu() - self.assertTrue(not no_warn_pypic.has_constant_of_type(species.constant.Ionizers)) + 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.assertEqual(1, len(explicit_logs.output)) + self.assertTrue(1 <= len(explicit_logs.output)) self.assertTrue("TESTWARN" in explicit_logs.output[0]) def test_fully_ionized_warning_electrons(self): @@ -324,37 +329,22 @@ def test_fully_ionized_warning_electrons(self): logging.warning("TESTWARN") no_warn_picmi = picmi.Species(name="ELECTRON", particle_type="electron") - self.assertTrue(not no_warn_picmi.has_ionizers()) - no_warn_pypic = no_warn_picmi.get_as_pypicongpu() - self.assertTrue(not no_warn_pypic.has_constant_of_type(species.constant.Ionizers)) + 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_fully_ionized_charge_state_conflict(self): - """picongpu_fully_ionized may only be used if charge_state is None""" - # charge state is not none - with self.assertRaisesRegex(AssertionError, ".*charge_state.*"): - picmi.Species(name="x", particle_type="H", charge_state=1, picongpu_fully_ionized=True).get_as_pypicongpu() - - # particle_type is missing - with self.assertRaisesRegex(AssertionError, ".*particle_type.*"): - picmi.Species(name="x", mass=3, charge=2, picongpu_fully_ionized=True).get_as_pypicongpu() - - # non-elements may generally not be ionized - with self.assertRaisesRegex(AssertionError, ".*[Ee]lement.*"): - picmi.Species(name="x", particle_type="electron", picongpu_fully_ionized=False).get_as_pypicongpu() - def test_ionize_non_elements(self): """non-elements may not have a charge_state""" - with self.assertRaisesRegex(Exception, ".*[Ee]lement.*"): - picmi.Species(name="e", particle_type="electron", charge_state=-1).get_as_pypicongpu() + 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 = picmi_e.get_as_pypicongpu() - self.assertTrue(not pypic_e.has_constant_of_type(species.constant.Ionizers)) + 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) @@ -367,48 +357,25 @@ def test_fully_ionized_typesafety(self): """picongpu_fully_ioinized is type safe""" for invalid in [1, "yes", [], {}]: with self.assertRaises(typeguard.TypeCheckError): - picmi.Species(name="x", picongpu_fully_ionized=invalid) + picmi.Species(name="x", picongpu_fixed_charge=invalid) # works: - picmi_species = picmi.Species(name="x", particle_type="He", picongpu_fully_ionized=True) + 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_fully_ionized = invalid + picmi_species.picongpu_fixed_charge = invalid # None is allowed as value in general (but not in constructor) - picmi_species.picongpu_fully_ionized = None - - def test_ionization_electron_explicit_types(self): - """explicit electron specification requires a PICMI species""" - for invalid in [[], {}, "electron"]: - with self.assertRaises(typeguard.TypeCheckError): - picmi.Species(name="ion", picongpu_ionization_electrons=invalid) - - # with correct type works - electrons = picmi.Species(name="electron", mass=1, charge=2) - picmi.Species(name="ion", picongpu_ionization_electrons=electrons) + picmi_species.picongpu_fixed_charge = None def test_particle_type_invalid(self): """unkown particle type rejects""" for invalid in ["", "elektron", "e", "e-", "Uux"]: - with self.assertRaisesRegex(NameError, ".*unkown.*"): - picmi.Species(name="x", particle_type=invalid).get_as_pypicongpu() - - def test_ionization_electrons_attribute_present(self): - """picongpu_ionization_electrons is always present""" - self.assertEqual(None, picmi.Species(name="x").picongpu_ionization_electrons) - self.assertEqual( - None, - picmi.Species(name="x", particle_type="H").picongpu_ionization_electrons, - ) - - self.assertEqual( - None, - picmi.Species(name="x", particle_type="H", charge_state=-1).picongpu_ionization_electrons, - ) + 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() + picmi.Species(name="x", particle_type="N", charge_state=8).get_as_pypicongpu(None) diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py b/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py index a8244b6ad07..7ceb100129b 100644 --- a/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py +++ b/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py @@ -12,6 +12,8 @@ from picongpu.pypicongpu.species.attribute import Position, Momentum, BoundElectrons from picongpu.picmi import constants +import pydantic_core + import unittest @@ -81,13 +83,11 @@ def test_typesafety(self): instance.check() for invalid in ["ionization_current", {}, [], 0]: - with self.assertRaises(TypeError): + 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) - instance.ionization_electron_species = self.electron - instance.ionization_current = invalid - instance.check() + Implementation(ionization_electron_species=self.electron, ionization_current=invalid) def test_circular_ionization(self): """electron species must not be ionizable itself""" diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelimplementations.py b/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelimplementations.py index ce8d8601153..9f4fd906b01 100644 --- a/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelimplementations.py +++ b/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelimplementations.py @@ -68,4 +68,4 @@ def test_picongpu_name(self): 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)) + self.assertEqual(name, Implementation(ionization_electron_species=self.electron).PICONGPU_NAME) diff --git a/test/python/picongpu/quick/pypicongpu/species/operation/setboundelectrons.py b/test/python/picongpu/quick/pypicongpu/species/operation/setboundelectrons.py index a6d608cde01..ace9dfee20a 100644 --- a/test/python/picongpu/quick/pypicongpu/species/operation/setboundelectrons.py +++ b/test/python/picongpu/quick/pypicongpu/species/operation/setboundelectrons.py @@ -104,7 +104,7 @@ def test_ionizers_required(self): # without constants does not pass: sbe.species.constants = [] - with self.assertRaisesRegex(AssertionError, ".*[Ii]onizers.*"): + with self.assertRaisesRegex(AssertionError, ".*BoundElectrons requires GroundStateIonization.*"): sbe.check_preconditions() def test_values(self): @@ -137,9 +137,11 @@ def test_rendering(self): ion = Species() ion.name = "ion" - ionizers_const = GroundStateIonization() - ionizers_const.electron_species = electron - ion.constants = [ionizers_const] + ion.constants = [ + GroundStateIonization( + ionization_model_list=[BSI(ionization_electron_species=electron, ionization_current=None_())] + ), + ] ion.attributes = [Position(), Momentum(), BoundElectrons()] # can be rendered diff --git a/test/python/picongpu/quick/pypicongpu/species/species.py b/test/python/picongpu/quick/pypicongpu/species/species.py index 37b8bf19bee..5105448d2d8 100644 --- a/test/python/picongpu/quick/pypicongpu/species/species.py +++ b/test/python/picongpu/quick/pypicongpu/species/species.py @@ -53,7 +53,7 @@ def setUp(self): ) self.const_element_properties = ElementProperties() - self.const_element_properties.element = Element.H + self.const_element_properties.element = Element("H") def test_basic(self): """setup provides working species""" diff --git a/test/python/picongpu/quick/pypicongpu/species/util/element.py b/test/python/picongpu/quick/pypicongpu/species/util/element.py index 32ca9eb0710..49a9e65e688 100644 --- a/test/python/picongpu/quick/pypicongpu/species/util/element.py +++ b/test/python/picongpu/quick/pypicongpu/species/util/element.py @@ -47,7 +47,7 @@ def test_parse_openpmd(self): 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 isotope descriptor"): + with self.assertRaisesRegex(ValueError, string + " is not a valid openPMD particle type"): name, massNumber = Element.parse_openpmd_isotopes(string) def test_basic_use(self):