From 1bbfe4bd77006734f1c9cb9959c09723a65df5cb Mon Sep 17 00:00:00 2001 From: BrianMarre Date: Fri, 2 Aug 2024 10:55:03 +0200 Subject: [PATCH] refactor simulation init manager init --- lib/python/picongpu/picmi/__init__.py | 18 +++- .../picmi/distribution/FoilDistribution.py | 4 +- .../distribution/GaussianDistribution.py | 4 +- .../picmi/distribution/UniformDistribution.py | 4 +- .../picongpu/picmi/interaction/__init__.py | 3 +- .../picongpu/picmi/interaction/interaction.py | 29 +++--- .../picmi/interaction/interactioninterface.py | 31 ------ .../thomasfermi.py | 3 + .../ionization/fieldionization/ADK.py | 2 + .../ionization/fieldionization/BSI.py | 13 ++- .../ionization/fieldionization/keldysh.py | 5 +- .../ionization/groundstateionizationmodel.py | 2 + .../interaction/ionization/ionizationmodel.py | 15 ++- lib/python/picongpu/picmi/simulation.py | 94 ++++++++++++------ lib/python/picongpu/picmi/species.py | 86 +++++----------- .../pypicongpu/rendering/renderedobject.py | 3 +- .../pypicongpu/species/initmanager.py | 4 +- .../pypicongpu/species/operation/__init__.py | 4 +- ...setboundelectrons.py => setchargestate.py} | 17 ++-- .../picongpu/pypicongpu/species/species.py | 16 +++ ...stateionization.GroundStateIonization.json | 2 +- .../species/initmanager.InitManager.json | 4 +- ...son => setchargestate.SetChargeState.json} | 10 +- .../picongpu/param/particle.param.mustache | 6 +- .../param/speciesDefinition.param.mustache | 2 +- .../speciesInitialization.param.mustache | 5 +- .../python/picongpu/quick/picmi/simulation.py | 6 +- test/python/picongpu/quick/picmi/species.py | 18 ++-- .../quick/pypicongpu/species/initmanager.py | 8 +- .../pypicongpu/species/operation/__init__.py | 2 +- ...setboundelectrons.py => setchargestate.py} | 98 +++++++++---------- 31 files changed, 259 insertions(+), 259 deletions(-) delete mode 100644 lib/python/picongpu/picmi/interaction/interactioninterface.py rename lib/python/picongpu/pypicongpu/species/operation/{setboundelectrons.py => setchargestate.py} (67%) rename share/picongpu/pypicongpu/schema/species/operation/{setboundelectrons.SetBoundElectrons.json => setchargestate.SetChargeState.json} (66%) rename test/python/picongpu/quick/pypicongpu/species/operation/{setboundelectrons.py => setchargestate.py} (59%) diff --git a/lib/python/picongpu/picmi/__init__.py b/lib/python/picongpu/picmi/__init__.py index 62f5e813c39..3873f2d73ff 100644 --- a/lib/python/picongpu/picmi/__init__.py +++ b/lib/python/picongpu/picmi/__init__.py @@ -1,7 +1,6 @@ """ PICMI for PIConGPU """ - from .simulation import Simulation from .grid import Cartesian3DGrid from .solver import ElectromagneticSolver @@ -10,9 +9,10 @@ from .layout import PseudoRandomLayout from . import constants -from .distribution import FoilDistribution -from .distribution import UniformDistribution -from .distribution import GaussianDistribution +from .distribution import FoilDistribution, UniformDistribution, GaussianDistribution +from .interaction import Interaction +from .interaction.ionization.fieldionization import ADK, ADKVariant, BSI, BSIExtension, Keldysh +from .interaction.ionization.electroniccollisionalequilibrium import ThomasFermi import picmistandard @@ -27,12 +27,20 @@ "GaussianLaser", "Species", "PseudoRandomLayout", + "constants", "FoilDistribution", "UniformDistribution", "GaussianDistribution", - "constants", + "ADK", + "ADKVariant", + "BSI", + "BSIExtension", + "Keldysh", + "ThomasFermi", + "Interaction", ] + codename = "picongpu" """ name of this PICMI implementation diff --git a/lib/python/picongpu/picmi/distribution/FoilDistribution.py b/lib/python/picongpu/picmi/distribution/FoilDistribution.py index 1a8ece02602..4951142a0a3 100644 --- a/lib/python/picongpu/picmi/distribution/FoilDistribution.py +++ b/lib/python/picongpu/picmi/distribution/FoilDistribution.py @@ -42,8 +42,8 @@ def picongpu_get_rms_velocity_si(self) -> typing.Tuple[float, float, float]: def get_as_pypicongpu(self) -> species.operation.densityprofile.DensityProfile: util.unsupported("fill in", self.fill_in) - util.unsupported("lower bound", self.lower_bound, [None, None, None]) - util.unsupported("upper bound", self.upper_bound, [None, None, None]) + util.unsupported("lower bound", self.lower_bound, (None, None, None)) + util.unsupported("upper bound", self.upper_bound, (None, None, None)) foilProfile = species.operation.densityprofile.Foil() foilProfile.density_si = self.density diff --git a/lib/python/picongpu/picmi/distribution/GaussianDistribution.py b/lib/python/picongpu/picmi/distribution/GaussianDistribution.py index 053f81d6708..0a34f541a61 100644 --- a/lib/python/picongpu/picmi/distribution/GaussianDistribution.py +++ b/lib/python/picongpu/picmi/distribution/GaussianDistribution.py @@ -61,8 +61,8 @@ def get_as_pypicongpu(self) -> species.operation.densityprofile.DensityProfile: util.unsupported("fill in not active", self.fill_in, True) # @todo support bounds, Brian Marre, 2024 - util.unsupported("lower bound", self.lower_bound, [None, None, None]) - util.unsupported("upper bound", self.upper_bound, [None, None, None]) + util.unsupported("lower bound", self.lower_bound, (None, None, None)) + util.unsupported("upper bound", self.upper_bound, (None, None, None)) gaussian_profile = species.operation.densityprofile.Gaussian() diff --git a/lib/python/picongpu/picmi/distribution/UniformDistribution.py b/lib/python/picongpu/picmi/distribution/UniformDistribution.py index e7034b1edee..a84c0f431bf 100644 --- a/lib/python/picongpu/picmi/distribution/UniformDistribution.py +++ b/lib/python/picongpu/picmi/distribution/UniformDistribution.py @@ -44,8 +44,8 @@ def picongpu_get_rms_velocity_si(self) -> typing.Tuple[float, float, float]: def get_as_pypicongpu(self) -> species.operation.densityprofile.DensityProfile: util.unsupported("fill in", self.fill_in) - util.unsupported("lower bound", self.lower_bound, [None, None, None]) - util.unsupported("upper bound", self.upper_bound, [None, None, None]) + util.unsupported("lower bound", self.lower_bound, (None, None, None)) + util.unsupported("upper bound", self.upper_bound, (None, None, None)) profile = species.operation.densityprofile.Uniform() profile.density_si = self.density diff --git a/lib/python/picongpu/picmi/interaction/__init__.py b/lib/python/picongpu/picmi/interaction/__init__.py index e1088d4ca31..69896a4ddfe 100644 --- a/lib/python/picongpu/picmi/interaction/__init__.py +++ b/lib/python/picongpu/picmi/interaction/__init__.py @@ -1,5 +1,4 @@ -from .interactioninterface import InteractionInterface from .interaction import Interaction from . import ionization -__all__ = ["InteractionInterface", "Interaction", "ionization"] +__all__ = ["Interaction", "ionization"] diff --git a/lib/python/picongpu/picmi/interaction/interaction.py b/lib/python/picongpu/picmi/interaction/interaction.py index 0e31f10c631..7c914be8dcd 100644 --- a/lib/python/picongpu/picmi/interaction/interaction.py +++ b/lib/python/picongpu/picmi/interaction/interaction.py @@ -8,15 +8,15 @@ from ... import pypicongpu from .ionization.groundstateionizationmodel import GroundStateIonizationModel, IonizationModel -from .interactioninterface import InteractionInterface -from ..species import Species import picmistandard + import typeguard +import pydantic @typeguard.typechecked -class Interaction(InteractionInterface): +class Interaction(pydantic.BaseModel): """ Common interface of Particle-In-Cell particle interaction extensions @@ -71,20 +71,23 @@ def get_interaction_constants( ]: """get list of all constants required by interactions for the given species""" + has_ionization = False constant_list = [] ionization_model_conversion = {} for model in self.ground_state_ionization_model_list: if model.ion_species == picmi_species: + has_ionization = True model_constants = model.get_constants() Interaction.update_constant_list(constant_list, model_constants) ionization_model_conversion[model] = model.get_as_pypicongpu() - # add GroundStateIonization constant for entire species - constant_list.append( - pypicongpu.species.constant.GroundStateIonization( - ionization_model_list=ionization_model_conversion.values() + if has_ionization: + # add GroundStateIonization constant for entire species + constant_list.append( + pypicongpu.species.constant.GroundStateIonization( + ionization_model_list=ionization_model_conversion.values() + ) ) - ) # add additional interaction sub groups needing constants here return constant_list, ionization_model_conversion @@ -125,16 +128,20 @@ def fill_in_ionization_electron_species( ] pypicongpu_ionization_model.ionization_electron_species = pypicongpu_ionization_electron_species - def has_ground_state_ionization(self, species: Species) -> bool: + def __has_ground_state_ionization(self, species) -> bool: """does at least one ground state ionization model list species as ion species?""" + for ionization_model in self.ground_state_ionization_model_list: if species == ionization_model.ion_species: return True return False - def has_ionization(self, species: Species) -> bool: + def has_ionization(self, species) -> bool: """does at least one ionization model list species as ion species?""" + from ..species import Species + + assert isinstance(species, Species) # add additional groups of ionization models here - ionization_configured = self.has_ground_state_ionization(species) + ionization_configured = self.__has_ground_state_ionization(species) return ionization_configured diff --git a/lib/python/picongpu/picmi/interaction/interactioninterface.py b/lib/python/picongpu/picmi/interaction/interactioninterface.py deleted file mode 100644 index ae5a3f425d6..00000000000 --- a/lib/python/picongpu/picmi/interaction/interactioninterface.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2024 PIConGPU contributors -Authors: Brian Edward Marre -License: GPLv3+ -""" - -from ... import pypicongpu - -import picmistandard -import pydantic -import typeguard - - -@typeguard.typechecked -class InteractionInterface(pydantic.BaseModel): - """ - interface for forward declaration - """ - - def get_interaction_constants( - self, species: picmistandard.PICMI_Species - ) -> list[pypicongpu.species.constant.Constant]: - """get list of all constants required by interactions for the given species""" - raise NotImplementedError("abstract interface for forward declaration only!") - - def fill_in_ionization_electron_species( - self, pypicongpu_by_picmi_species: dict[picmistandard.PICMI_Species, pypicongpu.species.Species] - ): - """add ionization electron species to pypicongpu species' ionization model""" - raise NotImplementedError("abstract interface for forward declaration only!") diff --git a/lib/python/picongpu/picmi/interaction/ionization/electroniccollisionalequilibrium/thomasfermi.py b/lib/python/picongpu/picmi/interaction/ionization/electroniccollisionalequilibrium/thomasfermi.py index bdc7638fdd2..ca11d40a7e2 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/electroniccollisionalequilibrium/thomasfermi.py +++ b/lib/python/picongpu/picmi/interaction/ionization/electroniccollisionalequilibrium/thomasfermi.py @@ -7,6 +7,7 @@ from ..groundstateionizationmodel import GroundStateIonizationModel from ..... import pypicongpu + import typeguard @@ -17,4 +18,6 @@ class ThomasFermi(GroundStateIonizationModel): MODEL_NAME: str = "ThomasFermi" def get_as_pypicongpu(self) -> pypicongpu.species.constant.ionizationmodel.IonizationModel: + self.check() + return pypicongpu.species.constant.ionizationmodel.ThomasFermi() diff --git a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ADK.py b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ADK.py index da988288f93..20a65a970ea 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ADK.py +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ADK.py @@ -36,6 +36,8 @@ class ADK(FieldIonization): """extension to the BSI model""" def get_as_pypicongpu(self) -> IonizationModel: + self.check() + if self.ADK_variant is ADKVariant.LinearPolarization: return ADKLinearPolarization(ionization_current=None_()) if self.ADK_variant is ADKVariant.CircularPolarization: diff --git a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/BSI.py b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/BSI.py index 153a6477e75..3eb8387433d 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/BSI.py +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/BSI.py @@ -7,11 +7,10 @@ from .fieldionization import FieldIonization +from ..... import pypicongpu from .....pypicongpu.species.constant.ionizationcurrent import None_ from .....pypicongpu.species.constant import ionizationmodel -from ..... import pypicongpu - import enum import typeguard @@ -33,17 +32,17 @@ class BSI(FieldIonization): BSI_extensions: tuple[BSIExtension] """extension to the BSI model""" - def get_as_pypicongpu(self) -> pypicongpu.species.constant.ionizationmodel.IonizationModel: + def get_as_pypicongpu(self) -> ionizationmodel.IonizationModel: + self.check() + if self.BSI_extensions == []: return ionizationmodel.BSI(ionization_current=None_()) if len(self.BSI_extensions) > 1: - pypicongpu.util.unsupported("more than one BSI_extension") - else: - pypicongpu.util.unsupported(f"unknown BSI_extension {self.BSI_extensions[0]}") + 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_()) if self.BSI_extensions[0] is BSIExtension.EffectiveZ: return ionizationmodel.BSIEffectiveZ(ionization_current=None_()) - raise ValueError("unknown BSI extension") + 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 fa470d57693..7cc79f30f41 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/keldysh.py +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/keldysh.py @@ -10,7 +10,6 @@ from .....pypicongpu.species.constant.ionizationcurrent import None_ from .....pypicongpu.species.constant import ionizationmodel -from ..... import pypicongpu import typeguard @@ -20,5 +19,7 @@ class Keldysh(FieldIonization): MODEL_NAME: str = "Keldysh" - def get_as_pypicongpu(self) -> pypicongpu.species.constant.ionizationmodel.IonizationModel: + def get_as_pypicongpu(self) -> ionizationmodel.IonizationModel: + self.check() + 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 a3a4c36face..68a596be3ea 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/groundstateionizationmodel.py +++ b/lib/python/picongpu/picmi/interaction/ionization/groundstateionizationmodel.py @@ -16,6 +16,8 @@ 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""" + self.check() + Z = self.ion_species.picongpu_element.get_atomic_number() assert self.ion_species.charge_state <= Z, f"charge_state must be <= atomic number ({Z})" diff --git a/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py b/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py index b97405c97f4..a3d772ffadd 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py +++ b/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py @@ -5,11 +5,11 @@ License: GPLv3+ """ -from ...species import Species from .... import pypicongpu import pydantic import typeguard +import typing @typeguard.typechecked @@ -23,10 +23,10 @@ class IonizationModel(pydantic.BaseModel): MODEL_NAME: str """ionization model""" - ion_species: Species + ion_species: typing.Any """PICMI ion species to apply ionization model for""" - ionization_electron_species: Species + ionization_electron_species: typing.Any """PICMI electron species of which to create macro particle upon ionization""" def __hash__(self): @@ -43,6 +43,15 @@ def __hash__(self): raise TypeError return hash_value + def check(self): + # import here to avoid circular import + 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 species object" + def get_constants(self) -> list[pypicongpu.species.constant.Constant]: raise NotImplementedError("abstract base class only!") diff --git a/lib/python/picongpu/picmi/simulation.py b/lib/python/picongpu/picmi/simulation.py index 5f6c559463a..c456c5a5419 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 from .. import pypicongpu +from .species import Species +from .interaction.ionization import IonizationModel from . import constants from .grid import Cartesian3DGrid @@ -94,7 +96,6 @@ def __init__( self.picongpu_custom_user_input = None self.__runner = None - # second call PICMI __init__ to do PICMI initialization and setting class attribute values outside of pydantic model picmistandard.PICMI_Simulation.__init__(self, **keyword_arguments) # additional PICMI stuff checks, @todo move to picmistandard, Brian Marre, 2024 @@ -180,7 +181,7 @@ def __yee_compute_cfl_or_delta_t(self) -> None: def __get_operations_simple_density( self, - pypicongpu_by_picmi_species: typing.Dict[picmistandard.PICMI_Species, pypicongpu.species.Species], + pypicongpu_by_picmi_species: typing.Dict[Species, pypicongpu.species.Species], ) -> typing.List[pypicongpu.species.operation.SimpleDensity]: """ retrieve operations for simple density placements @@ -235,7 +236,7 @@ def __get_operations_simple_density( def __get_operations_not_placed( self, - pypicongpu_by_picmi_species: typing.Dict[picmistandard.PICMI_Species, pypicongpu.species.Species], + pypicongpu_by_picmi_species: typing.Dict[Species, pypicongpu.species.Species], ) -> typing.List[pypicongpu.species.operation.NotPlaced]: """ retrieve operations for not placed species @@ -265,7 +266,7 @@ def __get_operations_not_placed( def __get_operations_from_individual_species( self, - pypicongpu_by_picmi_species: typing.Dict[picmistandard.PICMI_Species, pypicongpu.species.Species], + pypicongpu_by_picmi_species: typing.Dict[Species, pypicongpu.species.Species], ) -> typing.List[pypicongpu.species.operation.Operation]: """ call get_independent_operations() of all species @@ -281,65 +282,94 @@ def __get_operations_from_individual_species( return all_operations - def __get_init_manager(self) -> pypicongpu.species.InitManager: - """ - create & fill an initmanager - - performs the following steps: - 1. check preconditions - 2. translate species to pypicongpu representation - Note: Cache translations to avoid creating new translations by - continuosly translating again and again - 3. generate operations which have inter-species dependencies - 4. generate operations without inter-species dependencies - """ - initmgr = pypicongpu.species.InitManager() - - # check preconditions, @todo move to picmistandard, Brian Marre 2024 + def __check_preconditions_init_manager(self) -> None: + """check preconditions, @todo move to picmistandard, Brian Marre 2024""" assert len(self.species) == len(self.layouts) - # check either no layout AND no profile, or both and ratio only set if leyout and profile also set for layout, picmi_species in zip(self.layouts, self.species): profile = picmi_species.initial_distribution ratio = picmi_species.density_scale - # either both None or both not None: assert 1 != [layout, profile].count( None ), "species need BOTH layout AND initial distribution set (or neither)" - # ratio only set if, layout and profile are also set 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" - # get species list + 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 + """ - ## @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 - initmgr.all_species.append(pypicongpu_species) - # fill inter-species dependencies + return pypicongpu_by_picmi_species, ionization_model_conversion_by_species - # 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 + 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 ) - # operations with inter-species dependencies - ## + def __get_init_manager(self) -> pypicongpu.species.InitManager: + """ + 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 = pypicongpu.species.InitManager() + + 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 without inter-species dependencies - ## + # 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) diff --git a/lib/python/picongpu/picmi/species.py b/lib/python/picongpu/picmi/species.py index 90a7d1fe9da..c5ade6ac2a5 100644 --- a/lib/python/picongpu/picmi/species.py +++ b/lib/python/picongpu/picmi/species.py @@ -5,17 +5,16 @@ License: GPLv3+ """ +from .predefinedparticletypeproperties import non_element_particle_type_properties +from .interaction import Interaction + from .. import pypicongpu from ..pypicongpu.species.util.element import Element -from .interaction import InteractionInterface -from .predefinedparticletypeproperties import non_element_particle_type_properties import picmistandard import typing import typeguard -import pydantic -import pydantic_core import logging import re @@ -33,13 +32,13 @@ class Species(picmistandard.PICMI_Species): @attention ONLY set non-element particles here, all other are handled by element """ - __non_element_particle_types: list[str] = __non_element_particle_type_properties.keys() - """list of particle types""" - picongpu_element = pypicongpu.util.build_typesafe_property(typing.Optional[Element]) """element information of object""" - picongpu_fixed_charge = pypicongpu.util.build_typesafe_property(typing.Optional[bool]) + __non_element_particle_types: list[str] = __non_element_particle_type_properties.keys() + """list of 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""" @@ -47,48 +46,7 @@ class Species(picmistandard.PICMI_Species): __warned_already: bool = False __previous_check: bool = False - @classmethod - def __get_pydantic_core_schema__( - cls, source: typing.Type[typing.Any], handler: pydantic.GetCoreSchemaHandler - ) -> pydantic_core.core_schema.CoreSchema: - """return schema for species instances for pydantic validation""" - - element_schema = handler.generate_schema(typing.Optional[Element]) - - def val_element(v: Species, handler: pydantic.ValidatorFunctionWrapHandler) -> Species: - v.picongpu_element = handler(v.picongpu_element) - return v - - python_schema = pydantic_core.core_schema.chain_schema( - # `chain_schema` means do the following steps in order: - [ - # Ensure the value is an instance of Owner - pydantic_core.core_schema.is_instance_schema(cls), - # Use the element_schema to validate `picongpu_element` - pydantic_core.core_schema.no_info_wrap_validator_function(val_element, element_schema), - ] - ) - - return pydantic_core.core_schema.json_or_python_schema( - # for JSON accept an object with name and item keys - json_schema=pydantic_core.core_schema.chain_schema( - [ - pydantic_core.core_schema.typed_dict_schema( - { - "picongpu_element": pydantic_core.core_schema.typed_dict_field(element_schema), - } - ), - # after validating the json data convert it to python - pydantic_core.core_schema.no_info_before_validator_function( - lambda data: Species(picongpu_element=None, keyword_arguments=data), - python_schema, - ), - ] - ), - python_schema=python_schema, - ) - - def __init__(self, picongpu_fixed_charge=None, **keyword_arguments): + def __init__(self, picongpu_fixed_charge: bool = False, **keyword_arguments): self.picongpu_fixed_charge = picongpu_fixed_charge self.picongpu_element = None @@ -162,7 +120,7 @@ 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 | None) -> bool: + def has_ionization(self, interaction: Interaction | None) -> bool: """does species have ionization configured?""" if interaction is None: return False @@ -183,7 +141,7 @@ def is_ion(self) -> bool: return False return True - def __check_ionization_configuration(self, interaction: InteractionInterface | None) -> None: + def __check_ionization_configuration(self, interaction: Interaction | None) -> None: """ check species ioniaztion- and species- configuration are compatible @@ -199,7 +157,7 @@ def __check_ionization_configuration(self, interaction: InteractionInterface | N "type, must either set particle_type explicitly or only use charge instead" ) assert ( - self.picongpu_fixed_charge is None + self.picongpu_fixed_charge is False ), f"Species {self.name} specified fixed charge without also specifying particle_type" else: # particle type is @@ -210,7 +168,7 @@ def __check_ionization_configuration(self, interaction: InteractionInterface | N interaction ), f"Species {self.name} configured with active ionization but particle type indicates non ion." assert ( - self.picongpu_fixed_charge is None + 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 @@ -222,7 +180,7 @@ def __check_ionization_configuration(self, interaction: InteractionInterface | N ), f"Species {self.name} intial charge state is unphysical" if self.has_ionization(interaction): - assert not self.picongpu_fixed_charge, ( + 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." ) @@ -232,7 +190,7 @@ def __check_ionization_configuration(self, interaction: InteractionInterface | N ) else: # ion with fixed charge - if not self.picongpu_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" ) @@ -250,11 +208,11 @@ def __check_ionization_configuration(self, interaction: InteractionInterface | N # unknown particle type raise ValueError(f"unknown particle type {self.particle_type} in species {self.name}") - def __check_interaction_configuration(self, interaction: InteractionInterface | None) -> None: + 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: InteractionInterface | None) -> None: + 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 @@ -273,7 +231,7 @@ def check(self, interaction: InteractionInterface | None) -> None: self.__previous_check = True def get_as_pypicongpu( - self, interaction: InteractionInterface | None + self, interaction: Interaction | None ) -> tuple[ pypicongpu.species.Species, None | dict[typing.Any, pypicongpu.species.constant.ionizationmodel.IonizationModel] ]: @@ -334,8 +292,10 @@ def get_as_pypicongpu( return s, pypicongpu_model_by_picmi_model def get_independent_operations( - self, pypicongpu_species: pypicongpu.species.Species, interaction: InteractionInterface | None + 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() @@ -368,11 +328,11 @@ def get_independent_operations( all_operations.append(momentum_op) - # assign bound electrons + # assign boundElectrons attribute if self.is_ion() and self.has_ionization(interaction): - bound_electrons_op = pypicongpu.species.operation.SetBoundElectrons() + bound_electrons_op = pypicongpu.species.operation.SetChargeState() bound_electrons_op.species = pypicongpu_species - bound_electrons_op.bound_electrons = self.picongpu_element.get_atomic_number() - self.charge_state + bound_electrons_op.charge_state = self.charge_state all_operations.append(bound_electrons_op) else: # fixed charge state -> therefore no bound electron attribute necessary diff --git a/lib/python/picongpu/pypicongpu/rendering/renderedobject.py b/lib/python/picongpu/pypicongpu/rendering/renderedobject.py index 46c064fe85d..1f249ca3326 100644 --- a/lib/python/picongpu/pypicongpu/rendering/renderedobject.py +++ b/lib/python/picongpu/pypicongpu/rendering/renderedobject.py @@ -174,7 +174,8 @@ def _get_schema_from_class(class_type: type) -> typing.Any: if type(schema) is dict: if "unevaluatedProperties" not in schema: logging.warning("schema does not explicitly forbid " "unevaluated properties: {}".format(fqn)) - elif schema["unevaluatedProperties"]: + # special exemption for custom user input which is never evaluated + elif schema["unevaluatedProperties"] and fqn != "picongpu.pypicongpu.customuserinput.CustomUserInput": logging.warning("schema supports unevaluated properties: {}".format(fqn)) else: logging.warning("schema is not dict: {}".format(fqn)) diff --git a/lib/python/picongpu/pypicongpu/species/initmanager.py b/lib/python/picongpu/pypicongpu/species/initmanager.py index 6de5f677d2c..01a6873b863 100644 --- a/lib/python/picongpu/pypicongpu/species/initmanager.py +++ b/lib/python/picongpu/pypicongpu/species/initmanager.py @@ -14,7 +14,7 @@ DensityOperation, SimpleDensity, SimpleMomentum, - SetBoundElectrons, + SetChargeState, ) from .attribute import Attribute from .constant import Constant @@ -500,7 +500,7 @@ def _get_serialized(self) -> dict: operation_types_by_name = { "simple_density": SimpleDensity, "simple_momentum": SimpleMomentum, - "set_bound_electrons": SetBoundElectrons, + "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 diff --git a/lib/python/picongpu/pypicongpu/species/operation/__init__.py b/lib/python/picongpu/pypicongpu/species/operation/__init__.py index 6fd4327caf6..adc75ade306 100644 --- a/lib/python/picongpu/pypicongpu/species/operation/__init__.py +++ b/lib/python/picongpu/pypicongpu/species/operation/__init__.py @@ -4,7 +4,7 @@ from .notplaced import NotPlaced from .simplemomentum import SimpleMomentum from .noboundelectrons import NoBoundElectrons -from .setboundelectrons import SetBoundElectrons +from .setchargestate import SetChargeState from . import densityprofile from . import momentum @@ -16,7 +16,7 @@ "NotPlaced", "SimpleMomentum", "NoBoundElectrons", - "SetBoundElectrons", + "SetChargeState", "densityprofile", "momentum", ] diff --git a/lib/python/picongpu/pypicongpu/species/operation/setboundelectrons.py b/lib/python/picongpu/pypicongpu/species/operation/setchargestate.py similarity index 67% rename from lib/python/picongpu/pypicongpu/species/operation/setboundelectrons.py rename to lib/python/picongpu/pypicongpu/species/operation/setchargestate.py index b743dfbd3d3..53a475d5fb0 100644 --- a/lib/python/picongpu/pypicongpu/species/operation/setboundelectrons.py +++ b/lib/python/picongpu/pypicongpu/species/operation/setchargestate.py @@ -15,17 +15,17 @@ @typeguard.typechecked -class SetBoundElectrons(Operation): +class SetChargeState(Operation): """ - assigns and set the boundElectrons attribute + assigns boundElectrons attribute and sets it to the initial charge state - Standard attribute for pre-ionization. + used for ionization of ions """ species = util.build_typesafe_property(Species) """species which will have boundElectrons set""" - bound_electrons = util.build_typesafe_property(int) + charge_state = util.build_typesafe_property(int) """number of bound electrons to set""" def __init__(self): @@ -34,11 +34,10 @@ def __init__(self): def check_preconditions(self) -> None: assert self.species.has_constant_of_type(GroundStateIonization), "BoundElectrons requires GroundStateIonization" - if self.bound_electrons < 0: - raise ValueError("bound electrons must be >0") + if self.charge_state < 0: + raise ValueError("charge state must be > 0") - if 0 == self.bound_electrons: - raise ValueError("bound electrons must be >0, use NoBoundElectrons to assign " "0 bound electrons") + # may not check for charge_state > Z since Z not known in this context def prebook_species_attributes(self) -> None: self.attributes_by_species = { @@ -48,5 +47,5 @@ def prebook_species_attributes(self) -> None: def _get_serialized(self) -> dict: return { "species": self.species.get_rendering_context(), - "bound_electrons": self.bound_electrons, + "charge_state": self.charge_state, } diff --git a/lib/python/picongpu/pypicongpu/species/species.py b/lib/python/picongpu/pypicongpu/species/species.py index 1b21e51aab0..8e66890c64d 100644 --- a/lib/python/picongpu/pypicongpu/species/species.py +++ b/lib/python/picongpu/pypicongpu/species/species.py @@ -41,6 +41,22 @@ class Species(RenderedObject): name = util.build_typesafe_property(str) """name of the species""" + def __str__(self) -> str: + try: + return ( + self.name + + " : \n\t constants: " + + str(self.constants) + + "\n\t attributes: " + + str(self.attributes) + + "\n" + ) + except Exception: + try: + return self.name + " : \n\t constants: " + str(self.constants) + "\n" + except Exception: + return self.name + def get_cxx_typename(self) -> str: """ get (standalone) C++ name for this species diff --git a/share/picongpu/pypicongpu/schema/species/constant/groundstateionization.GroundStateIonization.json b/share/picongpu/pypicongpu/schema/species/constant/groundstateionization.GroundStateIonization.json index 1a541597a43..7f69ab32ccd 100644 --- a/share/picongpu/pypicongpu/schema/species/constant/groundstateionization.GroundStateIonization.json +++ b/share/picongpu/pypicongpu/schema/species/constant/groundstateionization.GroundStateIonization.json @@ -1,7 +1,7 @@ { "$id":"https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.groundstateionization.GroundStateIonization", "required":["ionization_model_list"], - "unevaluated":false, + "unevaluatedProperties":false, "properties": { "ionization_model_list": { "type": "array", diff --git a/share/picongpu/pypicongpu/schema/species/initmanager.InitManager.json b/share/picongpu/pypicongpu/schema/species/initmanager.InitManager.json index fe5e2b67579..e7fd9014039 100644 --- a/share/picongpu/pypicongpu/schema/species/initmanager.InitManager.json +++ b/share/picongpu/pypicongpu/schema/species/initmanager.InitManager.json @@ -35,10 +35,10 @@ "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.operation.simplemomentum.SimpleMomentum" } }, - "set_bound_electrons": { + "set_charge_state": { "type": "array", "items": { - "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.operation.setboundelectrons.SetBoundElectrons" + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.operation.setchargestate.SetChargeState" } } } diff --git a/share/picongpu/pypicongpu/schema/species/operation/setboundelectrons.SetBoundElectrons.json b/share/picongpu/pypicongpu/schema/species/operation/setchargestate.SetChargeState.json similarity index 66% rename from share/picongpu/pypicongpu/schema/species/operation/setboundelectrons.SetBoundElectrons.json rename to share/picongpu/pypicongpu/schema/species/operation/setchargestate.SetChargeState.json index 63d38e45fd5..f3bf3e8144a 100644 --- a/share/picongpu/pypicongpu/schema/species/operation/setboundelectrons.SetBoundElectrons.json +++ b/share/picongpu/pypicongpu/schema/species/operation/setchargestate.SetChargeState.json @@ -1,18 +1,18 @@ { - "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.operation.setboundelectrons.SetBoundElectrons", + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.operation.setchargestate.SetChargeState", "description": "set bound electrons attribute to given value", "type": "object", "unevaluatedProperties": false, - "required": ["species", "bound_electrons"], + "required": ["species", "charge_state"], "properties": { "species": { "description": "species to set bound electrons for", "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species" }, - "bound_electrons": { - "description": "number of bound electrons to set", + "charge_state": { + "description": "charge state to set boundElectrons attribute for", "type": "integer", - "minimum": 1 + "minimum": 0 } } } 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 b1240906817..05bbce05006 100644 --- a/share/picongpu/pypicongpu/template/include/picongpu/param/particle.param.mustache +++ b/share/picongpu/pypicongpu/template/include/picongpu/param/particle.param.mustache @@ -110,10 +110,10 @@ namespace picongpu {{/temperature}} {{/species_initmanager.operations.simple_momentum}} - {{#species_initmanager.operations.set_bound_electrons}} + {{#species_initmanager.operations.set_charge_state}} //! definition of PreIonized manipulator - using PreIonize_{{{species.typename}}} = unary::ChargeState<{{{bound_electrons}}}u>;; - {{/species_initmanager.operations.set_bound_electrons}} + using PreIonize_{{{species.typename}}} = unary::ChargeState<{{{charge_state}}}u>;; + {{/species_initmanager.operations.set_charge_state}} } // 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 3e887044f9c..a2e7e37117c 100644 --- a/share/picongpu/pypicongpu/template/include/picongpu/param/speciesDefinition.param.mustache +++ b/share/picongpu/pypicongpu/template/include/picongpu/param/speciesDefinition.param.mustache @@ -66,7 +66,7 @@ namespace picongpu ionizers, - {{/operations.set_bound_electrons}} - + {{/operations.set_charge_state}} // does nothing -- exists to catch trailing comma left by code generation pypicongpu::nop>; diff --git a/test/python/picongpu/quick/picmi/simulation.py b/test/python/picongpu/quick/picmi/simulation.py index 14f2f0919ca..6a91ef36ea1 100644 --- a/test/python/picongpu/quick/picmi/simulation.py +++ b/test/python/picongpu/quick/picmi/simulation.py @@ -396,12 +396,12 @@ def test_add_ionization_model(self): initmgr = pypic_sim.init_manager operation_types = list(map(lambda op: type(op), initmgr.all_operations)) - self.assertEqual(2, operation_types.count(species.operation.SetBoundElectrons)) + self.assertEqual(2, operation_types.count(species.operation.SetChargeState)) for op in initmgr.all_operations: - if isinstance(op, species.operation.SetBoundElectrons) and op.species.name == "Nitrogen": + if isinstance(op, species.operation.SetChargeState) and op.species.name == "Nitrogen": self.assertEqual(5, op.bound_electrons) - if isinstance(op, species.operation.SetBoundElectrons) and op.species.name == "Hydrogen": + if isinstance(op, species.operation.SetChargeState) and op.species.name == "Hydrogen": self.assertEqual(0, op.bound_electrons) # other ops (position...): ignore diff --git a/test/python/picongpu/quick/picmi/species.py b/test/python/picongpu/quick/picmi/species.py index 624784e84a6..496f1189c2b 100644 --- a/test/python/picongpu/quick/picmi/species.py +++ b/test/python/picongpu/quick/picmi/species.py @@ -125,7 +125,7 @@ def test_get_independent_operations_different_name(self): pypicongpu_s.name = "any" self.assertNotEqual(None, picmi_s.get_independent_operations(pypicongpu_s, None)) - def test_get_independent_operations_ionization_set_bound_electrons(self): + 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") @@ -143,15 +143,15 @@ def test_get_independent_operations_ionization_set_bound_electrons(self): 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(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.SetBoundElectrons): + if not isinstance(op, species.operation.SetChargeState): continue self.assertEqual(pypic_species, op.species) - self.assertEqual(5, op.bound_electrons) + 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""" @@ -161,7 +161,7 @@ def test_get_independent_operations_ionization_not_ionizable(self): 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)) + self.assertEqual(0, ops_types.count(species.operation.SetChargeState)) def test_get_independent_operations_momentum(self): """momentum is correctly translated""" @@ -353,8 +353,8 @@ def test_electron_from_particle_type(self): self.assertAlmostEqual(mass_const.mass_si, picmi.constants.m_e) self.assertAlmostEqual(charge_const.charge_si, -picmi.constants.q_e) - def test_fully_ionized_typesafety(self): - """picongpu_fully_ioinized is type safe""" + 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) @@ -366,8 +366,8 @@ def test_fully_ionized_typesafety(self): with self.assertRaises(typeguard.TypeCheckError): picmi_species.picongpu_fixed_charge = invalid - # None is allowed as value in general (but not in constructor) - picmi_species.picongpu_fixed_charge = None + # False is allowed + picmi_species.picongpu_fixed_charge = False def test_particle_type_invalid(self): """unkown particle type rejects""" diff --git a/test/python/picongpu/quick/pypicongpu/species/initmanager.py b/test/python/picongpu/quick/pypicongpu/species/initmanager.py index 8259819d502..9034c5ac3ff 100644 --- a/test/python/picongpu/quick/pypicongpu/species/initmanager.py +++ b/test/python/picongpu/quick/pypicongpu/species/initmanager.py @@ -865,7 +865,7 @@ def test_constant_constant_dependencies_typechecked(self): with self.assertRaises(typeguard.TypeCheckError): initmgr.bake() - def test_set_bound_electrons_passthrough(self): + def test_set_charge_state_passthrough(self): """bound electrons operation is included in rendering context""" # create full electron species electron = species.Species() @@ -882,9 +882,9 @@ def test_set_bound_electrons_passthrough(self): element_const.element = species.util.Element("N") ion.constants = [ionizers_const, element_const] - ion_op = species.operation.SetBoundElectrons() + ion_op = species.operation.SetChargeState() ion_op.species = ion - ion_op.bound_electrons = 2 + ion_op.charge_state = 2 initmgr = InitManager() initmgr.all_species = [electron, ion] @@ -897,5 +897,5 @@ def test_set_bound_electrons_passthrough(self): self.assertEqual( [ion_op.get_rendering_context()], - context["operations"]["set_bound_electrons"], + 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 index edf495cbc31..1876a936080 100644 --- a/test/python/picongpu/quick/pypicongpu/species/operation/__init__.py +++ b/test/python/picongpu/quick/pypicongpu/species/operation/__init__.py @@ -6,4 +6,4 @@ from .momentum import * # pyflakes.ignore from .simplemomentum import * # pyflakes.ignore from .noboundelectrons import * # pyflakes.ignore -from .setboundelectrons import * # pyflakes.ignore +from .setchargestate import * # pyflakes.ignore diff --git a/test/python/picongpu/quick/pypicongpu/species/operation/setboundelectrons.py b/test/python/picongpu/quick/pypicongpu/species/operation/setchargestate.py similarity index 59% rename from test/python/picongpu/quick/pypicongpu/species/operation/setboundelectrons.py rename to test/python/picongpu/quick/pypicongpu/species/operation/setchargestate.py index ace9dfee20a..4cc78673d7d 100644 --- a/test/python/picongpu/quick/pypicongpu/species/operation/setboundelectrons.py +++ b/test/python/picongpu/quick/pypicongpu/species/operation/setchargestate.py @@ -5,7 +5,7 @@ License: GPLv3+ """ -from picongpu.pypicongpu.species.operation import SetBoundElectrons +from picongpu.pypicongpu.species.operation import SetChargeState import unittest import typeguard @@ -17,7 +17,7 @@ from picongpu.pypicongpu.species.attribute import BoundElectrons, Position, Momentum -class TestSetBoundElectrons(unittest.TestCase): +class TestSetChargeState(unittest.TestCase): def setUp(self): electron = Species() electron.name = "e" @@ -35,94 +35,90 @@ def setUp(self): def test_basic(self): """basic operation""" - sbe = SetBoundElectrons() - sbe.species = self.species1 - sbe.bound_electrons = 2 + scs = SetChargeState() + scs.species = self.species1 + scs.charge_state = 2 # checks pass - sbe.check_preconditions() + scs.check_preconditions() def test_typesafety(self): """typesafety is ensured""" - sbe = SetBoundElectrons() + scs = SetChargeState() for invalid_species in [None, 1, "a", []]: with self.assertRaises(typeguard.TypeCheckError): - sbe.species = invalid_species + scs.species = invalid_species for invalid_number in [None, "a", [], self.species1, 2.3]: with self.assertRaises(typeguard.TypeCheckError): - sbe.bound_electrons = invalid_number + scs.charge_state = invalid_number # works: - sbe.species = self.species1 - sbe.bound_electrons = 1 + scs.species = self.species1 + scs.charge_state = 1 def test_empty(self): """all parameters are mandatory""" for set_species in [True, False]: - for set_bound_electrons in [True, False]: - sbe = SetBoundElectrons() + for set_charge_state in [True, False]: + scs = SetChargeState() if set_species: - sbe.species = self.species1 - if set_bound_electrons: - sbe.bound_electrons = 1 + scs.species = self.species1 + if set_charge_state: + scs.charge_state = 1 - if set_species and set_bound_electrons: + if set_species and set_charge_state: # must pass - sbe.check_preconditions() + scs.check_preconditions() else: # mandatory missing -> must raise with self.assertRaises(Exception): - sbe.check_preconditions() + scs.check_preconditions() def test_attribute_generated(self): """creates bound electrons attribute""" - sbe = SetBoundElectrons() - sbe.species = self.species1 - sbe.bound_electrons = 1 + scs = SetChargeState() + scs.species = self.species1 + scs.charge_state = 1 # emulate initmanager - sbe.check_preconditions() + scs.check_preconditions() self.species1.attributes = [] - sbe.prebook_species_attributes() + scs.prebook_species_attributes() - self.assertEqual(1, len(sbe.attributes_by_species)) - self.assertTrue(self.species1 in sbe.attributes_by_species) - self.assertEqual(1, len(sbe.attributes_by_species[self.species1])) - self.assertTrue(isinstance(sbe.attributes_by_species[self.species1][0], BoundElectrons)) + 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""" - sbe = SetBoundElectrons() - sbe.species = self.species1 - sbe.bound_electrons = 1 + scs = SetChargeState() + scs.species = self.species1 + scs.charge_state = 1 # passes: - self.assertTrue(sbe.species.has_constant_of_type(GroundStateIonization)) - sbe.check_preconditions() + self.assertTrue(scs.species.has_constant_of_type(GroundStateIonization)) + scs.check_preconditions() # without constants does not pass: - sbe.species.constants = [] + scs.species.constants = [] with self.assertRaisesRegex(AssertionError, ".*BoundElectrons requires GroundStateIonization.*"): - sbe.check_preconditions() + scs.check_preconditions() def test_values(self): """bound electrons must be >0""" - sbe = SetBoundElectrons() - sbe.species = self.species1 + scs = SetChargeState() + scs.species = self.species1 - with self.assertRaisesRegex(ValueError, ".*>0.*"): - sbe.bound_electrons = -1 - sbe.check_preconditions() - - with self.assertRaisesRegex(ValueError, ".*NoBoundElectrons.*"): - sbe.bound_electrons = 0 - sbe.check_preconditions() + with self.assertRaisesRegex(ValueError, ".*> 0.*"): + scs.charge_state = -1 + scs.check_preconditions() # silently passes - sbe.bound_electrons = 1 - sbe.check_preconditions() + scs.charge_state = 1 + scs.check_preconditions() def test_rendering(self): """rendering works""" @@ -147,10 +143,10 @@ def test_rendering(self): # can be rendered self.assertNotEqual({}, ion.get_rendering_context()) - sbe = SetBoundElectrons() - sbe.species = ion - sbe.bound_electrons = 1 + scs = SetChargeState() + scs.species = ion + scs.charge_state = 1 - context = sbe.get_rendering_context() - self.assertEqual(1, context["bound_electrons"]) + context = scs.get_rendering_context() + self.assertEqual(1, context["charge_state"]) self.assertEqual(ion.get_rendering_context(), context["species"])