From e83ffb7ed00f929f7f8a549feefa34fd6c056393 Mon Sep 17 00:00:00 2001 From: mafshari Date: Wed, 3 Jul 2024 16:10:45 +0200 Subject: [PATCH 01/20] add PICMI ionization configuration schema and templates add schemas and templates for the configuration ioniaztion through PICMI --- .../constant/ionizationModels/ADKCircPol.json | 19 ++++++++ .../constant/ionizationModels/ADKLinPol.json | 19 ++++++++ .../constant/ionizationModels/BSI.json | 19 ++++++++ .../ionizationModels/BSIEffectiveZ.json | 19 ++++++++ .../ionizationModels/BSIStarkShifted.json | 19 ++++++++ .../constant/ionizationModels/Keldysh.json | 19 ++++++++ .../ionizationModels/ThomasFermi.json | 19 ++++++++ .../ionizationmodels.IonizationModels.json | 24 +++++++++++ .../species/constant/ionizers.Ionizers.json | 13 ------ .../param/speciesDefinition.param.mustache | 43 +++++++++++++++++-- 10 files changed, 197 insertions(+), 16 deletions(-) create mode 100644 share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ADKCircPol.json create mode 100644 share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ADKLinPol.json create mode 100644 share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSI.json create mode 100644 share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSIEffectiveZ.json create mode 100644 share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSIStarkShifted.json create mode 100644 share/picongpu/pypicongpu/schema/species/constant/ionizationModels/Keldysh.json create mode 100644 share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ThomasFermi.json create mode 100644 share/picongpu/pypicongpu/schema/species/constant/ionizationmodels.IonizationModels.json delete mode 100644 share/picongpu/pypicongpu/schema/species/constant/ionizers.Ionizers.json diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ADKCircPol.json b/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ADKCircPol.json new file mode 100644 index 0000000000..3bdccb4d4e --- /dev/null +++ b/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ADKCircPol.json @@ -0,0 +1,19 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModel.ADKCircPol", + "type": "object", + "description": "Describes the ADK circular polarization ionization model parameters.", + "unevaluatedProperties": false, + "required": [ + "ionization_electron_species", + "active", + "last" + ], + "properties": { + "ionization_electron_species": { + "description": "Electron species spawned by ionization.", + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species" + }, + "active": "boolean", + "last": "boolean" + } +} diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ADKLinPol.json b/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ADKLinPol.json new file mode 100644 index 0000000000..640ce8cd4c --- /dev/null +++ b/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ADKLinPol.json @@ -0,0 +1,19 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModel.ADKLinPol", + "type": "object", + "description": "Describes the ADK linear polarization ionization model parameters.", + "unevaluatedProperties": false, + "required": [ + "ionization_electron_species", + "active", + "last" + ], + "properties": { + "ionization_electron_species": { + "description": "Electron species spawned by ionization.", + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species" + }, + "active": "boolean", + "last": "boolean" + } +} diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSI.json b/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSI.json new file mode 100644 index 0000000000..498950e92d --- /dev/null +++ b/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSI.json @@ -0,0 +1,19 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModel.BSI", + "type": "object", + "description": "Describes the BSI ionization model parameters.", + "unevaluatedProperties": false, + "required": [ + "ionization_electron_species", + "active", + "last" + ], + "properties": { + "ionization_electron_species": { + "description": "Electron species spawned by ionization.", + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species" + }, + "active": "boolean", + "last": "boolean" + } +} diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSIEffectiveZ.json b/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSIEffectiveZ.json new file mode 100644 index 0000000000..4467119e36 --- /dev/null +++ b/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSIEffectiveZ.json @@ -0,0 +1,19 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModel.BSIEffectiveZ", + "type": "object", + "description": "Describes the BSI effective Z ionization model parameters.", + "unevaluatedProperties": false, + "required": [ + "ionization_electron_species", + "active", + "last" + ], + "properties": { + "ionization_electron_species": { + "description": "Electron species spawned by ionization.", + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species" + }, + "active": "boolean", + "last": "boolean" + } +} diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSIStarkShifted.json b/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSIStarkShifted.json new file mode 100644 index 0000000000..f490337ce5 --- /dev/null +++ b/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSIStarkShifted.json @@ -0,0 +1,19 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModel.BSIStarkShifted", + "type": "object", + "description": "Describes the BSI stark shifted ionization model parameters.", + "unevaluatedProperties": false, + "required": [ + "ionization_electron_species", + "active", + "last" + ], + "properties": { + "ionization_electron_species": { + "description": "Electron species spawned by ionization.", + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species" + }, + "active": "boolean", + "last": "boolean" + } +} diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/Keldysh.json b/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/Keldysh.json new file mode 100644 index 0000000000..d2b917ece1 --- /dev/null +++ b/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/Keldysh.json @@ -0,0 +1,19 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModel.Keldysh", + "type": "object", + "description": "Describes the Keldysh ionization model parameters.", + "unevaluatedProperties": false, + "required": [ + "ionization_electron_species", + "active", + "last" + ], + "properties": { + "ionization_electron_species": { + "description": "Electron species spawned by ionization.", + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species" + }, + "active": "boolean", + "last": "boolean" + } +} diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ThomasFermi.json b/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ThomasFermi.json new file mode 100644 index 0000000000..8b669c4b33 --- /dev/null +++ b/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ThomasFermi.json @@ -0,0 +1,19 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModel.ThomasFermi", + "type": "object", + "description": "Describes the Thomas-Fermi ionization model parameters.", + "unevaluatedProperties": false, + "required": [ + "ionization_electron_species" + "active", + "last" + ], + "properties": { + "ionization_electron_species": { + "description": "Electron species spawned by ionization.", + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species" + }, + "active": "boolean", + "last": "boolean" + } +} diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizationmodels.IonizationModels.json b/share/picongpu/pypicongpu/schema/species/constant/ionizationmodels.IonizationModels.json new file mode 100644 index 0000000000..1bab6cc103 --- /dev/null +++ b/share/picongpu/pypicongpu/schema/species/constant/ionizationmodels.IonizationModels.json @@ -0,0 +1,24 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationmodels.IonizationModels", + "description": "Collection of Ionization Modules for a species.", + "type": "object", + "required": [ + "BSI", + "BSIEffectiveZ", + "BSIStarkShifted", + "ADKCircPol", + "ADKLinPol", + "Keldysh", + "ThomasFermi" + ], + "unevaluatedProperties": false, + "properties": { + "BSI": {"$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModels.BSI"}, + "BSIEffectiveZ": {"$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModels.BSIEffectiveZ"}, + "BSIStarkShifted": {"$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModels.BSIStarkShifted"}, + "ADKCircPol": {"$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModels.ADKCircPol"}, + "ADKLinPol": {"$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModels.ADKLinPol"}, + "Keldysh": {"$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModels.Keldysh"}, + "ThomasFermi": {"$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModels.ThomasFermi"} + } +} diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizers.Ionizers.json b/share/picongpu/pypicongpu/schema/species/constant/ionizers.Ionizers.json deleted file mode 100644 index 0ccd74a4fe..0000000000 --- a/share/picongpu/pypicongpu/schema/species/constant/ionizers.Ionizers.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizers.Ionizers", - "description": "used ionization methods and electron species", - "type": "object", - "unevaluatedProperties": false, - "required": ["electron_species"], - "properties": { - "electron_species": { - "description": "electrons used for ionization", - "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species" - } - } -} 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 dbcc974a7c..b79e2acdc4 100644 --- a/share/picongpu/pypicongpu/template/include/picongpu/param/speciesDefinition.param.mustache +++ b/share/picongpu/pypicongpu/template/include/picongpu/param/speciesDefinition.param.mustache @@ -64,9 +64,46 @@ namespace picongpu {{#ionizers}} ionizers, - particles::ionization::ADKCircPol<{{{electron_species.typename}}}, particles::ionization::current::None>, - particles::ionization::ThomasFermi<{{{electron_species.typename}}}>>>, + {{#type.BSIEffectiveZ}} + particles::ionization::BSIEffectiveZ<{{{ionizers.data.ionization_electron_species.typename}}}, particles::ionization::current::None> + {{{^ionizers.data.last}}} + , + {{{/ionizers.data.last}}} + {{/type.BSIEffectiveZ}} + {{#type.ADKCircPol}} + particles::ionization::ADKCircPol<{{{ionizers.data.ionization_electron_species.typename}}}, particles::ionization::current::None>, + {{{^ionizers.data.last}}} + , + {{{/ionizers.data.last}}} + {{/type.ADKCircPol}} + {{#type.ADKLinPol}} + particles::ionization::ADKLinPol<{{{ionizers.data.ionization_electron_species.typename}}}, particles::ionization::current::None>, + {{{^ionizers.data.last}}} + , + {{{/ionizers.data.last}}} + {{/type.ADKLinPol}} + {{#type.BSI}} + particles::ionization::BSI<{{{ionizers.data.ionization_electron_species.typename}}}, particles::ionization::current::None>, + {{{^ionizers.data.last}}} + , + {{{/ionizers.data.last}}} + {{/type.BSI}} + {{#type.BSIStarkShifted}} + particles::ionization::BSIStarkShifted<{{{ionizers.data.ionization_electron_species.typename}}}, particles::ionization::current::None>, + {{{^ionizers.data.last}}} + , + {{{/ionizers.data.last}}} + {{/type.BSIStarkShifted}} + {{#type.Keldysh}} + particles::ionization::Keldysh<{{{ionizers.data.ionization_electron_species.typename}}}, particles::ionization::current::None>, + {{{^ionizers.data.last}}} + , + {{{/ionizers.data.last}}} + {{/type.Keldysh}} + {{#type.ThomasFermi}} + particles::ionization::ThomasFermi<{{{ionizers.data.ionization_electron_species.typename}}}>, + {{/type.ThomasFermi}} + >>, {{/ionizers}} {{#element_properties}} From e3ee972843a87860ff34c9f2b608490d4d6c7d28 Mon Sep 17 00:00:00 2001 From: BrianMarre Date: Wed, 10 Jul 2024 16:40:44 +0200 Subject: [PATCH 02/20] rework PICMI ionization templates and schemas refactor of the PICMI ionization schemas and templates --- .../{ionizers.py => ionizationmodels.py} | 7 +-- .../picongpu/pypicongpu/species/species.py | 7 ++- .../constant/ionizationModels/ADKCircPol.json | 19 ------- .../constant/ionizationModels/ADKLinPol.json | 19 ------- .../constant/ionizationModels/BSI.json | 19 ------- .../ionizationModels/BSIEffectiveZ.json | 19 ------- .../ionizationModels/BSIStarkShifted.json | 19 ------- .../constant/ionizationModels/Keldysh.json | 19 ------- .../ionizationModels/ThomasFermi.json | 19 ------- .../ionizationcurrent.IonizationCurrent.json | 15 ++++++ .../ionizationmodel.IonizationModel.json | 28 ++++++++++ .../ionizationmodels.IonizationModels.json | 24 --------- .../schema/species/species.Species.json | 22 +++++--- .../param/speciesDefinition.param.mustache | 53 ++++--------------- 14 files changed, 75 insertions(+), 214 deletions(-) rename lib/python/picongpu/pypicongpu/species/constant/{ionizers.py => ionizationmodels.py} (88%) delete mode 100644 share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ADKCircPol.json delete mode 100644 share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ADKLinPol.json delete mode 100644 share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSI.json delete mode 100644 share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSIEffectiveZ.json delete mode 100644 share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSIStarkShifted.json delete mode 100644 share/picongpu/pypicongpu/schema/species/constant/ionizationModels/Keldysh.json delete mode 100644 share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ThomasFermi.json create mode 100644 share/picongpu/pypicongpu/schema/species/constant/ionizationcurrent.IonizationCurrent.json create mode 100644 share/picongpu/pypicongpu/schema/species/constant/ionizationmodel.IonizationModel.json delete mode 100644 share/picongpu/pypicongpu/schema/species/constant/ionizationmodels.IonizationModels.json diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizers.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodels.py similarity index 88% rename from lib/python/picongpu/pypicongpu/species/constant/ionizers.py rename to lib/python/picongpu/pypicongpu/species/constant/ionizationmodels.py index e893de73a1..f1faab21ec 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizers.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodels.py @@ -16,10 +16,7 @@ @typeguard.typechecked class Ionizers(Constant): """ - ionizers describing the ionization methods - - Currently the selected ionizers are fixed by the code generation. - When they are selectable by the user, they can be added here. + configuration for ground state only ionization models """ # note: no typecheck here -- which would require circular imports @@ -38,7 +35,7 @@ def check(self) -> None: raise TypeError("electron_species must be of type pypicongpu Species") # electron species must not be ionizable - if self.electron_species.has_constant_of_type(Ionizers): + if self.electron_species.has_constant_of_type(IonizationModels): raise ValueError("used electron species {} must not be ionizable itself".format(self.electron_species.name)) # note: do **NOT** check() electron species here diff --git a/lib/python/picongpu/pypicongpu/species/species.py b/lib/python/picongpu/pypicongpu/species/species.py index 2d62f4793b..17252ab90b 100644 --- a/lib/python/picongpu/pypicongpu/species/species.py +++ b/lib/python/picongpu/pypicongpu/species/species.py @@ -7,14 +7,13 @@ from ..rendering import RenderedObject from .attribute import Attribute, Position, Momentum -from .constant import Constant, Charge, Mass, DensityRatio, Ionizers, ElementProperties +from .constant import Constant, Charge, Mass, DensityRatio, IonizationModels, ElementProperties from .. import util import typeguard import typing import re - @typeguard.typechecked class Species(RenderedObject): """ @@ -148,7 +147,7 @@ def _get_serialized(self) -> dict: # a typo in the variable name in prints a warning (still continues # though -- to be compliant to the rendering standard). # - # To accomodate this behavior, we always define all keys for constant, + # To accommodate this behavior, we always define all keys for constant, # but maybe set them to null. For this below there is a list of *all # known constants*. When adding a constant do not forget to add it in # the JSON schema too. @@ -160,7 +159,7 @@ def _get_serialized(self) -> dict: "mass": Mass, "charge": Charge, "density_ratio": DensityRatio, - "ionizers": Ionizers, + "ionization_models": IonizationModels, "element_properties": ElementProperties, } diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ADKCircPol.json b/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ADKCircPol.json deleted file mode 100644 index 3bdccb4d4e..0000000000 --- a/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ADKCircPol.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModel.ADKCircPol", - "type": "object", - "description": "Describes the ADK circular polarization ionization model parameters.", - "unevaluatedProperties": false, - "required": [ - "ionization_electron_species", - "active", - "last" - ], - "properties": { - "ionization_electron_species": { - "description": "Electron species spawned by ionization.", - "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species" - }, - "active": "boolean", - "last": "boolean" - } -} diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ADKLinPol.json b/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ADKLinPol.json deleted file mode 100644 index 640ce8cd4c..0000000000 --- a/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ADKLinPol.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModel.ADKLinPol", - "type": "object", - "description": "Describes the ADK linear polarization ionization model parameters.", - "unevaluatedProperties": false, - "required": [ - "ionization_electron_species", - "active", - "last" - ], - "properties": { - "ionization_electron_species": { - "description": "Electron species spawned by ionization.", - "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species" - }, - "active": "boolean", - "last": "boolean" - } -} diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSI.json b/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSI.json deleted file mode 100644 index 498950e92d..0000000000 --- a/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSI.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModel.BSI", - "type": "object", - "description": "Describes the BSI ionization model parameters.", - "unevaluatedProperties": false, - "required": [ - "ionization_electron_species", - "active", - "last" - ], - "properties": { - "ionization_electron_species": { - "description": "Electron species spawned by ionization.", - "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species" - }, - "active": "boolean", - "last": "boolean" - } -} diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSIEffectiveZ.json b/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSIEffectiveZ.json deleted file mode 100644 index 4467119e36..0000000000 --- a/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSIEffectiveZ.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModel.BSIEffectiveZ", - "type": "object", - "description": "Describes the BSI effective Z ionization model parameters.", - "unevaluatedProperties": false, - "required": [ - "ionization_electron_species", - "active", - "last" - ], - "properties": { - "ionization_electron_species": { - "description": "Electron species spawned by ionization.", - "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species" - }, - "active": "boolean", - "last": "boolean" - } -} diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSIStarkShifted.json b/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSIStarkShifted.json deleted file mode 100644 index f490337ce5..0000000000 --- a/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/BSIStarkShifted.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModel.BSIStarkShifted", - "type": "object", - "description": "Describes the BSI stark shifted ionization model parameters.", - "unevaluatedProperties": false, - "required": [ - "ionization_electron_species", - "active", - "last" - ], - "properties": { - "ionization_electron_species": { - "description": "Electron species spawned by ionization.", - "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species" - }, - "active": "boolean", - "last": "boolean" - } -} diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/Keldysh.json b/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/Keldysh.json deleted file mode 100644 index d2b917ece1..0000000000 --- a/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/Keldysh.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModel.Keldysh", - "type": "object", - "description": "Describes the Keldysh ionization model parameters.", - "unevaluatedProperties": false, - "required": [ - "ionization_electron_species", - "active", - "last" - ], - "properties": { - "ionization_electron_species": { - "description": "Electron species spawned by ionization.", - "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species" - }, - "active": "boolean", - "last": "boolean" - } -} diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ThomasFermi.json b/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ThomasFermi.json deleted file mode 100644 index 8b669c4b33..0000000000 --- a/share/picongpu/pypicongpu/schema/species/constant/ionizationModels/ThomasFermi.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModel.ThomasFermi", - "type": "object", - "description": "Describes the Thomas-Fermi ionization model parameters.", - "unevaluatedProperties": false, - "required": [ - "ionization_electron_species" - "active", - "last" - ], - "properties": { - "ionization_electron_species": { - "description": "Electron species spawned by ionization.", - "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species" - }, - "active": "boolean", - "last": "boolean" - } -} diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizationcurrent.IonizationCurrent.json b/share/picongpu/pypicongpu/schema/species/constant/ionizationcurrent.IonizationCurrent.json new file mode 100644 index 0000000000..67ee0998a3 --- /dev/null +++ b/share/picongpu/pypicongpu/schema/species/constant/ionizationcurrent.IonizationCurrent.json @@ -0,0 +1,15 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.schema.constant.ionizationcurrent.IonizationCurrent", + "type": "object", + "description": "ionization current configuration", + "required": ["picongpu_name"], + "unevaluatedProperties": false, + "properties": { + "picongpu_name": { + "type": "string", + "description": "c++ code name of ionization current corresponding to ionization current model", + "minLength": 1, + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" + } + } +} diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizationmodel.IonizationModel.json b/share/picongpu/pypicongpu/schema/species/constant/ionizationmodel.IonizationModel.json new file mode 100644 index 0000000000..9eb797f556 --- /dev/null +++ b/share/picongpu/pypicongpu/schema/species/constant/ionizationmodel.IonizationModel.json @@ -0,0 +1,28 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.schema.constant.ionizationmodel.IonizationModel", + "type": "object", + "unevaluatedProperties": false, + "required": [ + "ionizer_picongpu_name", + "ionization_electron_species", + "ionization_current" + ], + "properties": { + "ionizer_picongpu_name": { + "type": "string", + "description": "c++ code name of ionizer corresponding to ionization model", + "minLength": 1, + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" + }, + "ionization_electron_species": { + "description": "Electron species spawned by ionization.", + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species" + }, + "ionization_current": { + "anyOf": [ + {"type": "null"}, + {"$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.schema.constant.ionizationcurrent.IonizationCurrent"} + ] + } + } +} diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizationmodels.IonizationModels.json b/share/picongpu/pypicongpu/schema/species/constant/ionizationmodels.IonizationModels.json deleted file mode 100644 index 1bab6cc103..0000000000 --- a/share/picongpu/pypicongpu/schema/species/constant/ionizationmodels.IonizationModels.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationmodels.IonizationModels", - "description": "Collection of Ionization Modules for a species.", - "type": "object", - "required": [ - "BSI", - "BSIEffectiveZ", - "BSIStarkShifted", - "ADKCircPol", - "ADKLinPol", - "Keldysh", - "ThomasFermi" - ], - "unevaluatedProperties": false, - "properties": { - "BSI": {"$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModels.BSI"}, - "BSIEffectiveZ": {"$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModels.BSIEffectiveZ"}, - "BSIStarkShifted": {"$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModels.BSIStarkShifted"}, - "ADKCircPol": {"$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModels.ADKCircPol"}, - "ADKLinPol": {"$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModels.ADKLinPol"}, - "Keldysh": {"$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModels.Keldysh"}, - "ThomasFermi": {"$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationModels.ThomasFermi"} - } -} diff --git a/share/picongpu/pypicongpu/schema/species/species.Species.json b/share/picongpu/pypicongpu/schema/species/species.Species.json index 068544d310..8000819df8 100644 --- a/share/picongpu/pypicongpu/schema/species/species.Species.json +++ b/share/picongpu/pypicongpu/schema/species/species.Species.json @@ -24,7 +24,7 @@ }, "attributes": { "type": "array", - "description": "names of attributes of each macroparticle", + "description": "names of attributes of each macro particle of this species", "items": { "type": "object", "unevaluatedProperties": false, @@ -33,7 +33,8 @@ "picongpu_name": { "type": "string", "description": "c++ code to define this property", - "minLength": 1 + "minLength": 1, + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" } } } @@ -45,7 +46,8 @@ "required": [ "mass", "charge", - "density_ratio" + "density_ratio", + "ionization_models" ], "properties": { "mass": { @@ -66,10 +68,18 @@ {"$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.densityratio.DensityRatio"} ] }, - "ionizers": { - "anyOf": [ + "ground_state_ionization": { + "anyOf": [ {"type": "null"}, - {"$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizers.Ionizers"} + { + "ionization_model_list": { + "type": "array", + "description": "list of ionization models for species", + "items": { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationmodel.IonizationModels" + } + } + } ] }, "element_properties": { 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 b79e2acdc4..668ffb3223 100644 --- a/share/picongpu/pypicongpu/template/include/picongpu/param/speciesDefinition.param.mustache +++ b/share/picongpu/pypicongpu/template/include/picongpu/param/speciesDefinition.param.mustache @@ -62,49 +62,18 @@ namespace picongpu densityRatio, {{/density_ratio}} - {{#ionizers}} + {{#ground_state_ionization}} ionizers - {{{^ionizers.data.last}}} - , - {{{/ionizers.data.last}}} - {{/type.BSIEffectiveZ}} - {{#type.ADKCircPol}} - particles::ionization::ADKCircPol<{{{ionizers.data.ionization_electron_species.typename}}}, particles::ionization::current::None>, - {{{^ionizers.data.last}}} - , - {{{/ionizers.data.last}}} - {{/type.ADKCircPol}} - {{#type.ADKLinPol}} - particles::ionization::ADKLinPol<{{{ionizers.data.ionization_electron_species.typename}}}, particles::ionization::current::None>, - {{{^ionizers.data.last}}} - , - {{{/ionizers.data.last}}} - {{/type.ADKLinPol}} - {{#type.BSI}} - particles::ionization::BSI<{{{ionizers.data.ionization_electron_species.typename}}}, particles::ionization::current::None>, - {{{^ionizers.data.last}}} - , - {{{/ionizers.data.last}}} - {{/type.BSI}} - {{#type.BSIStarkShifted}} - particles::ionization::BSIStarkShifted<{{{ionizers.data.ionization_electron_species.typename}}}, particles::ionization::current::None>, - {{{^ionizers.data.last}}} - , - {{{/ionizers.data.last}}} - {{/type.BSIStarkShifted}} - {{#type.Keldysh}} - particles::ionization::Keldysh<{{{ionizers.data.ionization_electron_species.typename}}}, particles::ionization::current::None>, - {{{^ionizers.data.last}}} - , - {{{/ionizers.data.last}}} - {{/type.Keldysh}} - {{#type.ThomasFermi}} - particles::ionization::ThomasFermi<{{{ionizers.data.ionization_electron_species.typename}}}>, - {{/type.ThomasFermi}} - >>, - {{/ionizers}} + {{#ionization_model_list}} + particles::ionization::{{{ionizer_picongpu_name}}}< + {{{ionization_electron_species.type_name}}} + {{#ionization_current}} + , particles::ionization::current::{{{picongpu_name}}} + {{/ionization_current}} + >{{^_last}},{{/_last}} + {{/ionization_model_list}} + >>, + {{/ground_state_ionization}} {{#element_properties}} atomicNumbers, From 3b3872a2fc55a55b5b004c0f2c7d365bac244aad Mon Sep 17 00:00:00 2001 From: BrianMarre Date: Mon, 15 Jul 2024 17:27:10 +0200 Subject: [PATCH 03/20] PyPIConGPU ionization configuration interface refactors the Ionizer PyPIConGPU interface to allow almost complete configuration of ionization in PIConGPU through PyPIConGPU --- .../pypicongpu/rendering/renderedobject.py | 8 + .../pypicongpu/species/constant/__init__.py | 4 +- .../pypicongpu/species/constant/constant.py | 5 +- .../species/constant/groundstateionization.py | 82 +++++++++++ .../constant/ionizationcurrent/__init__.py | 4 + .../ionizationcurrent/ionizationcurrent.py | 36 +++++ .../constant/ionizationcurrent/none_.py | 12 ++ .../ADKcircularpolarization.py | 28 ++++ .../ionizationmodel/ADKlinearpolarization.py | 28 ++++ .../species/constant/ionizationmodel/BSI.py | 31 ++++ .../constant/ionizationmodel/BSIeffectiveZ.py | 26 ++++ .../ionizationmodel/BSIstarkshifted.py | 25 ++++ .../constant/ionizationmodel/__init__.py | 21 +++ .../ionizationmodel/ionizationmodel.py | 81 ++++++++++ .../ionizationmodel/ionizationmodelgroups.py | 46 ++++++ .../constant/ionizationmodel/keldysh.py | 29 ++++ .../constant/ionizationmodel/thomasfermi.py | 30 ++++ .../species/constant/ionizationmodels.py | 63 -------- .../pypicongpu/species/initmanager.py | 2 + .../species/operation/noboundelectrons.py | 6 +- .../species/operation/setboundelectrons.py | 4 +- .../picongpu/pypicongpu/species/species.py | 9 +- .../ionizationmodel.Implementation.json | 3 + .../schema/species/species.Species.json | 7 +- .../pypicongpu/species/constant/__init__.py | 14 +- .../species/constant/groundstateionization.py | 6 + .../constant/ionizationmodel/__init__.py | 4 + .../ionizationmodel.py} | 139 ++++++++++-------- .../ionizationmodel/ionizationmodelgroups.py | 104 +++++++++++++ .../ionizationmodelimplementations.py | 62 ++++++++ .../constant/~groundstateionization.py | 28 ++++ .../species/operation/noboundelectrons.py | 8 +- .../species/operation/setboundelectrons.py | 8 +- .../quick/pypicongpu/species/species.py | 3 +- 34 files changed, 806 insertions(+), 160 deletions(-) create mode 100644 lib/python/picongpu/pypicongpu/species/constant/groundstateionization.py create mode 100644 lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/__init__.py create mode 100644 lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/ionizationcurrent.py create mode 100644 lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/none_.py create mode 100644 lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ADKcircularpolarization.py create mode 100644 lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ADKlinearpolarization.py create mode 100644 lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSI.py create mode 100644 lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIeffectiveZ.py create mode 100644 lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIstarkshifted.py create mode 100644 lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/__init__.py create mode 100644 lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py create mode 100644 lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodelgroups.py create mode 100644 lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/keldysh.py create mode 100644 lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/thomasfermi.py delete mode 100644 lib/python/picongpu/pypicongpu/species/constant/ionizationmodels.py create mode 100644 share/picongpu/pypicongpu/schema/species/constant/ionizationmodel.Implementation.json create mode 100644 test/python/picongpu/quick/pypicongpu/species/constant/groundstateionization.py create mode 100644 test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/__init__.py rename test/python/picongpu/quick/pypicongpu/species/constant/{ionizers.py => ionizationmodel/ionizationmodel.py} (52%) create mode 100644 test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelgroups.py create mode 100644 test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelimplementations.py create mode 100644 test/python/picongpu/quick/pypicongpu/species/constant/~groundstateionization.py diff --git a/lib/python/picongpu/pypicongpu/rendering/renderedobject.py b/lib/python/picongpu/pypicongpu/rendering/renderedobject.py index 177e87b29e..261c499562 100644 --- a/lib/python/picongpu/pypicongpu/rendering/renderedobject.py +++ b/lib/python/picongpu/pypicongpu/rendering/renderedobject.py @@ -53,6 +53,14 @@ class RenderedObject: which is used for identification purposes. """ + def __hash__(self): + """custom hash function for indexing in dicts @todo move to rendered_object, Brian Marre, 2024""" + hash_value = hash(type(self)) + for value in self.__dict__.values(): + if value is not None: + hash_value += hash(value) + return hash_value + @staticmethod def _maybe_fill_schema_store() -> None: """ diff --git a/lib/python/picongpu/pypicongpu/species/constant/__init__.py b/lib/python/picongpu/pypicongpu/species/constant/__init__.py index 071c09533a..0c4f2049a1 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/__init__.py +++ b/lib/python/picongpu/pypicongpu/species/constant/__init__.py @@ -2,14 +2,14 @@ from .mass import Mass from .charge import Charge from .densityratio import DensityRatio -from .ionizers import Ionizers from .elementproperties import ElementProperties +from .groundstateionization import GroundStateIonization __all__ = [ "Constant", "Mass", "Charge", "DensityRatio", - "Ionizers", "ElementProperties", + "GroundStateIonization", ] diff --git a/lib/python/picongpu/pypicongpu/species/constant/constant.py b/lib/python/picongpu/pypicongpu/species/constant/constant.py index 1a887f9ff0..edd1ff85b5 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/constant.py +++ b/lib/python/picongpu/pypicongpu/species/constant/constant.py @@ -45,8 +45,9 @@ class Constant(RenderedObject): constant) """ - def __init__(self): - raise NotImplementedError() + def __init__(self, **kw): + if type(self) == Constant: + raise NotImplementedError() def check(self) -> None: """ diff --git a/lib/python/picongpu/pypicongpu/species/constant/groundstateionization.py b/lib/python/picongpu/pypicongpu/species/constant/groundstateionization.py new file mode 100644 index 0000000000..8d897d14ce --- /dev/null +++ b/lib/python/picongpu/pypicongpu/species/constant/groundstateionization.py @@ -0,0 +1,82 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from .constant import Constant +from .ionizationmodel import IonizationModel, IonizationModelGroups + +import pydantic +import typing + + +class GroundStateIonization(Constant, pydantic.BaseModel): + ionization_model_list: list[IonizationModel] + """list of ground state only ionization models to apply for the species""" + + def check(self) -> None: + # check that no ionization model class is doubled up? + groups = IonizationModelGroups().get_by_group().keys() + + # check all ionization models + for ionization_model in self.ionization_model_list: + ionization_model.check() + + type_already_present = {} + for group in groups: + type_already_present[group] = False + + by_model = IonizationModelGroups().get_by_model() + for ionization_model in self.ionization_model_list: + group: str = by_model[type(ionization_model)] + if type_already_present[group]: + raise ValueError(f"ionization model group already represented. {group}") + + # check that at least one ionization model in list + if len(self.ionization_model_list) == 0: + raise ValueError("at least must ionization model must be specfied if ground_state_ionization is not none.") + + def get_species_dependencies(self) -> list[type]: + """get all species one of the ionization models in ionization_model_list depends on""" + + total_species_dependencies = [] + for ionization_model in self.ionization_model_list: + species_dependencies = ionization_model.get_species_dependencies() + for species in species_dependencies: + if species not in total_species_dependencies: + total_species_dependencies.append(species) + + return total_species_dependencies + + def get_attribute_dependencies(self) -> list[type]: + """get all attributes one of the ionization models in ionization_model_list depends on""" + total_attribute_dependencies = [] + for ionization_model in self.ionization_model_list: + attribute_dependencies = ionization_model.get_attribute_dependencies() + for attribute in attribute_dependencies: + if attribute not in total_attribute_dependencies: + total_attribute_dependencies.append(attribute) + + return total_attribute_dependencies + + def get_constant_dependencies(self) -> list[type]: + """get all constants one of the ionization models in ionization_model_list depends on""" + total_constant_dependencies = [] + for ionization_model in self.ionization_model_list: + constant_dependencies = ionization_model.get_constant_dependencies() + for constant in constant_dependencies: + if constant not in total_constant_dependencies: + total_constant_dependencies.append(constant) + + return total_constant_dependencies + + def _get_serialized(self) -> dict[str, list[dict[str, typing.Any]]]: + self.check() + + list_serialized = [] + for ionization_model in self.ionization_model_list: + list_serialized.append(ionization_model.get_rendering_context()) + + return {"ionization_model_list": list_serialized} diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/__init__.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/__init__.py new file mode 100644 index 0000000000..b5f00f4a4a --- /dev/null +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/__init__.py @@ -0,0 +1,4 @@ +from .ionizationcurrent import IonizationCurrent +from .none_ import None_ + +__all__ = ["IonizationCurrent", "None_"] diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/ionizationcurrent.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/ionizationcurrent.py new file mode 100644 index 0000000000..225b9065f4 --- /dev/null +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/ionizationcurrent.py @@ -0,0 +1,36 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from ..constant import Constant + +import pydantic +import typing + + +class IonizationCurrent(Constant, pydantic.BaseModel): + """base class for all ionization currents models""" + + PICONGPU_NAME: str + """C++ Code type name of ionizer""" + + def check(self) -> None: + # nothing to check here + pass + + def _get_serialized(self) -> dict: + # do not remove!, always check + self.check() + return {"picongpu_name": self.PICONGPU_NAME} + + def get_species_dependencies(self): + return [] + + def get_attribute_dependencies(self) -> typing.List[type]: + return [] + + def get_constant_dependencies(self) -> typing.List[type]: + return [] diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/none_.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/none_.py new file mode 100644 index 0000000000..26b9f30a1e --- /dev/null +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/none_.py @@ -0,0 +1,12 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from .ionizationcurrent import IonizationCurrent + + +class None_(IonizationCurrent): + picongpu_name: str = "None" diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ADKcircularpolarization.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ADKcircularpolarization.py new file mode 100644 index 0000000000..ea1c876faa --- /dev/null +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ADKcircularpolarization.py @@ -0,0 +1,28 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from .ionizationmodel import IonizationModel +from ..ionizationcurrent import IonizationCurrent + + +class ADKCircularPolarization(IonizationModel): + """ + Ammosov-Delone-Krainov tunnelling ionization for hydrogenlike atoms model -- circular polarization + + see for example: Delone, N. B.; Krainov, V. P. (1998). + "Tunneling and barrier-suppression ionization of atoms and ions in a laser radiation field" + doi:10.1070/PU1998v041n05ABEH000393 + + @attention this model is derived for near constant fields and may give erroneous predictions for rapidly changing + high intensity laser fields. + """ + + PICONGPU_NAME: str = "ADKCircPol" + """C++ Code type name of ionizer""" + + ionization_current: IonizationCurrent + """ionization current implementation to use""" diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ADKlinearpolarization.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ADKlinearpolarization.py new file mode 100644 index 0000000000..7e1a3e8e8a --- /dev/null +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ADKlinearpolarization.py @@ -0,0 +1,28 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from .ionizationmodel import IonizationModel +from ..ionizationcurrent import IonizationCurrent + + +class ADKLinearPolarization(IonizationModel): + """ + Ammosov-Delone-Krainov tunnelling ionization for hydrogenlike atoms model -- linear polarization + + see for example: Delone, N. B.; Krainov, V. P. (1998). + "Tunneling and barrier-suppression ionization of atoms and ions in a laser radiation field" + doi:10.1070/PU1998v041n05ABEH000393 + + @attention this model is derived for near constant fields and may give erroneous predictions for rapidly changing + high intensity laser fields. + """ + + PICONGPU_NAME: str = "ADKLinearPol" + """C++ Code type name of ionizer""" + + ionization_current: IonizationCurrent + """ionization current implementation to use""" diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSI.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSI.py new file mode 100644 index 0000000000..5ecd59bda1 --- /dev/null +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSI.py @@ -0,0 +1,31 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from .ionizationmodel import IonizationModel +from ..ionizationcurrent import IonizationCurrent + + +class BSI(IonizationModel): + """ + Barrier Suppression Ionization for hydrogen-like ions + + see for example: Delone, N. B.; Krainov, V. P. (1998). + "Tunneling and barrier-suppression ionization of atoms and ions in a laser radiation field" + doi:10.1070/PU1998v041n05ABEH000393 + + Calculates the electric field strength limit necessary to overcome the binding energy of the electron to the + core. If this limit exceed by the local electric field strength of an ion the ion is ionized. + + This model uses for naive inner electron charge shielding, assumes that the charge the electron 'feels' is equal to + `proton number - number of inner shell electrons`, but neglects the Stark upshift of ionization energies. + """ + + PICONGPU_NAME: str = "BSI" + """C++ Code type name of ionizer""" + + ionization_current: IonizationCurrent + """ionization current implementation to use""" diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIeffectiveZ.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIeffectiveZ.py new file mode 100644 index 0000000000..1827cc534a --- /dev/null +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIeffectiveZ.py @@ -0,0 +1,26 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from .ionizationmodel import IonizationModel +from ..ionizationcurrent import IonizationCurrent + + +class BSIEffectiveZ(IonizationModel): + """ + Barrier Suppression Ionization for hydrogen-like ions, using tabulated Z_effective values + + see BSI.py for further information + + Variant of the BSI ionization model using tabulated Z_effective values instead of the naive inner electron charge + shielding, but still neglecting the Stark upshift of ionization energies. + """ + + PICONGPU_NAME: str = "BSI" + """C++ Code type name of ionizer""" + + ionization_current: IonizationCurrent + """ionization current implementation to use""" diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIstarkshifted.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIstarkshifted.py new file mode 100644 index 0000000000..89b11494b9 --- /dev/null +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIstarkshifted.py @@ -0,0 +1,25 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from .ionizationmodel import IonizationModel +from ..ionizationcurrent import IonizationCurrent + + +class BSIStarkShifted(IonizationModel): + """ + Barrier Suppression Ionization for hydrogen-like ions, accounting for stark upshift of ionization energies + + see BSI.py for further information + + Variant of the BSI ionization model accounting for the Stark upshift of ionization energies. + """ + + PICONGPU_NAME: str = "BSI" + """C++ Code type name of ionizer""" + + ionization_current: IonizationCurrent + """ionization current implementation to use""" diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/__init__.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/__init__.py new file mode 100644 index 0000000000..46f81a32b4 --- /dev/null +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/__init__.py @@ -0,0 +1,21 @@ +from .ionizationmodel import IonizationModel +from .ionizationmodelgroups import IonizationModelGroups +from .BSI import BSI +from .BSIeffectiveZ import BSIEffectiveZ +from .BSIstarkshifted import BSIStarkShifted +from .ADKlinearpolarization import ADKLinearPolarization +from .ADKcircularpolarization import ADKCircularPolarization +from .keldysh import Keldysh +from .thomasfermi import ThomasFermi + +__all__ = [ + "IonizationModel", + "IonizationModelGroups", + "BSI", + "BSIEffectiveZ", + "BSIStarkShifted", + "ADKLinearPolarization", + "ADKCircularPolarization", + "Keldysh", + "ThomasFermi", +] diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py new file mode 100644 index 0000000000..a0cff1d359 --- /dev/null +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py @@ -0,0 +1,81 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from ..constant import Constant +from ...attribute import BoundElectrons +from ..ionizationcurrent import IonizationCurrent +from ..elementproperties import ElementProperties + +import pydantic +import typing + + +class IonizationModel(pydantic.BaseModel, Constant): + """ + base class for an ground state only ionization models of an ion species + + Owned by exactly one species. + + Identified by its PIConGPU name. + + PIConGPU term: "ionizer" + """ + + PICONGPU_NAME: str + """C++ Code type name of ionizer""" + + # no typecheck here -- would require circular imports + ionization_electron_species: typing.Any = None + """species to be used as electrons""" + + ionization_current: typing.Optional[IonizationCurrent] = None + """ionization current implementation to use""" + + def check(self) -> None: + """check internal consistency""" + + # import here to avoid circular import + from ...species import Species + from ..groundstateionization import GroundStateIonization + + # check ionization electron species is actually pypicongpu species instance + if not isinstance(self.ionization_electron_species, Species): + raise TypeError("ionization_electron_species must be of type pypicongpu Species") + + # electron species must not be an ionizable + if self.ionization_electron_species.has_constant_of_type(GroundStateIonization): + raise ValueError( + "used electron species {} must not be ionizable itself".format(self.ionization_electron_species.name) + ) + + # test ionization current set if required + test = self.ionization_current # noqa + + # note: do **NOT** check() electron species here + # -> it is not fully initialized at this point in the initialization + # (check requires attributes which are added last, + # but constants are added first) + + def _get_serialized(self) -> dict[str, typing.Any]: + # do not remove!, always do a check call + self.check() + + return { + "ionizer_picongpu_name": self.PICONGPU_NAME, + "ionization_electron_species": self.ionization_electron_species.get_rendering_context(), + "ionization_current": self.ionization_current, + } + + def get_species_dependencies(self) -> list[type]: + self.check() + return [self.ionization_electron_species] + + def get_attribute_dependencies(self) -> list[type]: + return [BoundElectrons] + + def get_constant_dependencies(self) -> list[type]: + return [ElementProperties] diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodelgroups.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodelgroups.py new file mode 100644 index 0000000000..6789d41720 --- /dev/null +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodelgroups.py @@ -0,0 +1,46 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from .BSI import BSI +from .BSIeffectiveZ import BSIEffectiveZ +from .BSIstarkshifted import BSIStarkShifted +from .ADKlinearpolarization import ADKLinearPolarization +from .ADKcircularpolarization import ADKCircularPolarization +from .keldysh import Keldysh +from .thomasfermi import ThomasFermi +from .ionizationmodel import IonizationModel + +import copy +import typing +import pydantic + + +class IonizationModelGroups(pydantic.BaseModel): + """ + grouping of ionization models into sub groups that may not be used at the same time + + every instance of this class is immutable, all method always return copies of the data contained + """ + + by_group: dict[str, list[typing.Type[IonizationModel]]] = { + "BSI_like": [BSI, BSIEffectiveZ, BSIStarkShifted], + "ADK_like": [ADKLinearPolarization, ADKCircularPolarization], + "Keldysh_like": [Keldysh], + "electronic_collisional": [ThomasFermi], + } + + def get_by_group(self) -> dict[str, list[typing.Type[IonizationModel]]]: + return copy.deepcopy(self.by_group) + + def get_by_model(self) -> dict[typing.Type[IonizationModel], str]: + return_dict: dict[typing.Type[IonizationModel], str] = {} + + for ionization_model_type, list_ionization_model in self.by_group.items(): + for ionization_model in list_ionization_model: + return_dict[ionization_model] = copy.deepcopy(ionization_model_type) + + return return_dict diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/keldysh.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/keldysh.py new file mode 100644 index 0000000000..7508c5b405 --- /dev/null +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/keldysh.py @@ -0,0 +1,29 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from .ionizationmodel import IonizationModel +from ..ionizationcurrent import IonizationCurrent + + +class Keldysh(IonizationModel): + """ + Keldysh multi photon ionization + + see for example: D. Bauer and P. Mulser(1999) + "Exact field ionization rates in the barrier-suppression regime from numerical time-dependent + Schroedinger-equation calculations" + Physical Review A, 59(1):569+, January 1999 + + @attention this model is derived for near constant fields and may give erroneous predictions for rapidly changing + high intensity laser fields. + """ + + PICONGPU_NAME: str = "BSI" + """C++ Code type name of ionizer""" + + ionization_current: IonizationCurrent + """ionization current implementation to use""" diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/thomasfermi.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/thomasfermi.py new file mode 100644 index 0000000000..e28d5f1318 --- /dev/null +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/thomasfermi.py @@ -0,0 +1,30 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from .ionizationmodel import IonizationModel + + +class ThomasFermi(IonizationModel): + """ + Thomas-Fermi impact ionization + + See table IV from Pressure Ionization, Resonances, and the Continuity of Bound and Free States + http://www.sciencedirect.com/science/article/pii/S0065219908601451 + doi:10.1016/S0065-2199(08)60145-1 + + This ionization model is based on the assumption of an "ion sphere", constructed based on describing electrons as a + density, a point charge atomic core and a finite atomic potential as a result of matter density. + + In this framework ionization may occur due to due to overlap of adjacent ion spheres lowering the ionization barrier + and causing electrons to become quasi-free in the system, being bound in resonance states. + + This model is used to calculate an average ionization degree with respect to local charge density and temperature. + This is extenden to arbitrary temperatures and atoms through fitting parameters and temperature cutoffs. + """ + + PICONGPU_NAME: str = "BSI" + """C++ Code type name of ionizer""" diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodels.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodels.py deleted file mode 100644 index f1faab21ec..0000000000 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodels.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2023 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - -from .constant import Constant -from ..attribute import BoundElectrons -from .elementproperties import ElementProperties - -import typeguard -import typing - - -@typeguard.typechecked -class Ionizers(Constant): - """ - configuration for ground state only ionization models - """ - - # note: no typecheck here -- which would require circular imports - electron_species = None - """species to be used as electrons""" - - def __init__(self): - # overwrite from parent - pass - - def check(self) -> None: - # import here to avoid circular import - from ..species import Species - - if not isinstance(self.electron_species, Species): - raise TypeError("electron_species must be of type pypicongpu Species") - - # electron species must not be ionizable - if self.electron_species.has_constant_of_type(IonizationModels): - raise ValueError("used electron species {} must not be ionizable itself".format(self.electron_species.name)) - - # note: do **NOT** check() electron species here - # -> it is not fully initialized at this point in the initialization - # (check requires attributes which are added last, - # but constants are added first) - - def _get_serialized(self) -> dict: - # (please resist the temptation of removing the check b/c "its not - # needed here": checks should *always* be run before serialization, - # so make it a habit of expecting it everywhere) - self.check() - return { - "electron_species": self.electron_species.get_rendering_context(), - } - - def get_species_dependencies(self): - self.check() - return [self.electron_species] - - def get_attribute_dependencies(self) -> typing.List[type]: - return [BoundElectrons] - - def get_constant_dependencies(self) -> typing.List[type]: - return [ElementProperties] diff --git a/lib/python/picongpu/pypicongpu/species/initmanager.py b/lib/python/picongpu/pypicongpu/species/initmanager.py index a23562421d..9ae6e38016 100644 --- a/lib/python/picongpu/pypicongpu/species/initmanager.py +++ b/lib/python/picongpu/pypicongpu/species/initmanager.py @@ -235,6 +235,8 @@ def __check_species_dependencies_circular(self) -> None: closure_size_after = len(dependency_closure) is_closure_final = closure_size_after == closure_size_before + print(dependency_closure) + # check: self in dependency closure? if species in dependency_closure: raise RecursionError( diff --git a/lib/python/picongpu/pypicongpu/species/operation/noboundelectrons.py b/lib/python/picongpu/pypicongpu/species/operation/noboundelectrons.py index f4d9857d51..8923233bdc 100644 --- a/lib/python/picongpu/pypicongpu/species/operation/noboundelectrons.py +++ b/lib/python/picongpu/pypicongpu/species/operation/noboundelectrons.py @@ -8,7 +8,7 @@ from .operation import Operation from ..species import Species from ..attribute import BoundElectrons -from ..constant import Ionizers +from ..constant import GroundStateIonization from ... import util import typeguard @@ -25,13 +25,13 @@ class NoBoundElectrons(Operation): """ species = util.build_typesafe_property(Species) - """species which will have boundElectorns set to 0""" + """species which will have BoundElectrons set to 0""" def __init__(self): pass def check_preconditions(self) -> None: - assert self.species.has_constant_of_type(Ionizers), "BoundElectrons requires Ionizers" + assert self.species.has_constant_of_type(GroundStateIonization), "BoundElectrons requires GroundStateIonization" def prebook_species_attributes(self) -> None: self.attributes_by_species = { diff --git a/lib/python/picongpu/pypicongpu/species/operation/setboundelectrons.py b/lib/python/picongpu/pypicongpu/species/operation/setboundelectrons.py index 2f33f844a2..b743dfbd3d 100644 --- a/lib/python/picongpu/pypicongpu/species/operation/setboundelectrons.py +++ b/lib/python/picongpu/pypicongpu/species/operation/setboundelectrons.py @@ -8,7 +8,7 @@ from .operation import Operation from ..species import Species from ..attribute import BoundElectrons -from ..constant import Ionizers +from ..constant import GroundStateIonization from ... import util import typeguard @@ -32,7 +32,7 @@ def __init__(self): pass def check_preconditions(self) -> None: - assert self.species.has_constant_of_type(Ionizers), "BoundElectrons requires Ionizers" + assert self.species.has_constant_of_type(GroundStateIonization), "BoundElectrons requires GroundStateIonization" if self.bound_electrons < 0: raise ValueError("bound electrons must be >0") diff --git a/lib/python/picongpu/pypicongpu/species/species.py b/lib/python/picongpu/pypicongpu/species/species.py index 17252ab90b..616b9d3d56 100644 --- a/lib/python/picongpu/pypicongpu/species/species.py +++ b/lib/python/picongpu/pypicongpu/species/species.py @@ -7,13 +7,14 @@ from ..rendering import RenderedObject from .attribute import Attribute, Position, Momentum -from .constant import Constant, Charge, Mass, DensityRatio, IonizationModels, ElementProperties +from .constant import Constant, Charge, Mass, DensityRatio, GroundStateIonization, ElementProperties from .. import util import typeguard import typing import re + @typeguard.typechecked class Species(RenderedObject): """ @@ -24,7 +25,7 @@ class Species(RenderedObject): - A set of species constants (mass, charge, etc.), - a set of species attributes (position, number of bound electrons), and - a set of operations which collectively initialize these attributes, - where one attribute is initializated by exactly one operation. + where one attribute is initialized by exactly one operation. - (and a name) Note that some of the species attributes or constants are considered @@ -74,7 +75,7 @@ def check(self) -> None: # position if Position not in [type(a) for a in self.attributes]: raise ValueError("Each species must have the position attribute!") - # momentum + # momentum, @todo really necessary?, Brian Marre, 2024 if Momentum not in [type(a) for a in self.attributes]: raise ValueError("Each species must have the momentum attribute!") @@ -159,8 +160,8 @@ def _get_serialized(self) -> dict: "mass": Mass, "charge": Charge, "density_ratio": DensityRatio, - "ionization_models": IonizationModels, "element_properties": ElementProperties, + "ground_state_ionization": GroundStateIonization, } constants_context = {} diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizationmodel.Implementation.json b/share/picongpu/pypicongpu/schema/species/constant/ionizationmodel.Implementation.json new file mode 100644 index 0000000000..a682515dba --- /dev/null +++ b/share/picongpu/pypicongpu/schema/species/constant/ionizationmodel.Implementation.json @@ -0,0 +1,3 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/quick.pypicongpu.species.constant.ionizationmodel.ionizationmodel.Implementation" +} diff --git a/share/picongpu/pypicongpu/schema/species/species.Species.json b/share/picongpu/pypicongpu/schema/species/species.Species.json index 8000819df8..63b548d039 100644 --- a/share/picongpu/pypicongpu/schema/species/species.Species.json +++ b/share/picongpu/pypicongpu/schema/species/species.Species.json @@ -33,8 +33,7 @@ "picongpu_name": { "type": "string", "description": "c++ code to define this property", - "minLength": 1, - "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" + "minLength": 1 } } } @@ -47,7 +46,7 @@ "mass", "charge", "density_ratio", - "ionization_models" + "ground_state_ionization" ], "properties": { "mass": { @@ -76,7 +75,7 @@ "type": "array", "description": "list of ionization models for species", "items": { - "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationmodel.IonizationModels" + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationmodel.IonizationModel" } } } diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/__init__.py b/test/python/picongpu/quick/pypicongpu/species/constant/__init__.py index 1d938abcfd..b5baa09000 100644 --- a/test/python/picongpu/quick/pypicongpu/species/constant/__init__.py +++ b/test/python/picongpu/quick/pypicongpu/species/constant/__init__.py @@ -1,7 +1,9 @@ # flake8: noqa -from .constant import * # pyflakes.ignore -from .mass import * # pyflakes.ignore -from .charge import * # pyflakes.ignore -from .densityratio import * # pyflakes.ignore -from .ionizers import * # pyflakes.ignore -from .elementproperties import * # pyflakes.ignore +from .constant import * +from .mass import * +from .charge import * +from .densityratio import * +from .elementproperties import * +from .groundstateionization import * +from .elementproperties import * +from .ionizationmodel import * diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/groundstateionization.py b/test/python/picongpu/quick/pypicongpu/species/constant/groundstateionization.py new file mode 100644 index 0000000000..f426d27aeb --- /dev/null +++ b/test/python/picongpu/quick/pypicongpu/species/constant/groundstateionization.py @@ -0,0 +1,6 @@ +""" +This file is part of PIConGPU. +Copyright 2021-2023 PIConGPU contributors +Authors: Hannes Troepgen, Brian Edward Marre +License: GPLv3+ +""" diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/__init__.py b/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/__init__.py new file mode 100644 index 0000000000..7c484dcb6b --- /dev/null +++ b/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/__init__.py @@ -0,0 +1,4 @@ +# flake8: noqa +from .ionizationmodelgroups import * +from .ionizationmodel import * +from .ionizationmodelimplementations import * diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/ionizers.py b/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py similarity index 52% rename from test/python/picongpu/quick/pypicongpu/species/constant/ionizers.py rename to test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py index a346e7d8db..a8244b6ad0 100644 --- a/test/python/picongpu/quick/pypicongpu/species/constant/ionizers.py +++ b/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py @@ -1,21 +1,26 @@ """ This file is part of PIConGPU. -Copyright 2021-2023 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre License: GPLv3+ """ -from picongpu.pypicongpu.species.constant import Ionizers, ElementProperties - -import unittest +from picongpu.pypicongpu.species.constant.ionizationmodel import IonizationModel from picongpu.pypicongpu.species import Species -from picongpu.pypicongpu.species.constant import Mass, Charge +from picongpu.pypicongpu.species.constant import Mass, Charge, ElementProperties, GroundStateIonization from picongpu.pypicongpu.species.attribute import Position, Momentum, BoundElectrons from picongpu.picmi import constants +import unittest + + +# raw implementation for testing +class Implementation(IonizationModel): + PICONGPU_NAME: str = "test" -class TestIonizers(unittest.TestCase): + +class Test_IonizationModel(unittest.TestCase): def setUp(self): electron = Species() electron.name = "e" @@ -31,43 +36,58 @@ def setUp(self): self.electron = electron + def test_not_constructible(self): + with self.assertRaises(Exception): + IonizationModel() + def test_basic(self): """simple operation""" # note: electrons are not checked, because they are not fully # initialized yet - ionizers = Ionizers() - ionizers.electron_species = self.electron - ionizers.check() + instance = Implementation() + instance.ionization_electron_species = self.electron + instance.check() - self.assertEqual([self.electron], ionizers.get_species_dependencies()) - self.assertEqual([BoundElectrons], ionizers.get_attribute_dependencies()) - self.assertEqual([ElementProperties], ionizers.get_constant_dependencies()) + self.assertEqual("test", instance.PICONGPU_NAME) - def test_typesafety(self): - """types are checked""" - ionizers = Ionizers() - for invalid in ["electron", {}, [], 0, None]: - with self.assertRaises(TypeError): - # note: circular imports would be required to use the - # pypicongpu-standard build_typesafe_property, hence the type - # is checked by check() instead of on assignment (as usual) - ionizers.electron_species = invalid - ionizers.check() + self.assertEqual([self.electron], instance.get_species_dependencies()) + self.assertEqual([BoundElectrons], instance.get_attribute_dependencies()) + self.assertEqual([ElementProperties], instance.get_constant_dependencies()) def test_empty(self): """electron species is mandatory""" - ionizers = Ionizers() + instance = Implementation() # must fail: with self.assertRaises(Exception): - ionizers.check() + instance.check() with self.assertRaises(Exception): - ionizers.get_species_dependencies() + instance.get_species_dependencies() # now passes - ionizers.electron_species = self.electron - ionizers.check() + instance.ionization_electron_species = self.electron + instance.check() + + def test_typesafety(self): + """types are checked""" + instance = Implementation() + for invalid in ["electron", {}, [], 0, None]: + with self.assertRaises(TypeError): + # note: circular imports would be required to use the + # pypicongpu-standard build_typesafe_property, hence the type + # is checked by check() instead of on assignment (as usual) + instance.ionization_electron_species = invalid + instance.check() + + for invalid in ["ionization_current", {}, [], 0]: + with self.assertRaises(TypeError): + # note: circular imports would be required to use the + # pypicongpu-standard build_typesafe_property, hence the type + # is checked by check() instead of on assignment (as usual) + instance.ionization_electron_species = self.electron + instance.ionization_current = invalid + instance.check() def test_circular_ionization(self): """electron species must not be ionizable itself""" @@ -83,47 +103,38 @@ def test_circular_ionization(self): ] # note: attributes not set yet, as would be case in init manager - ionizers_transitive_const = Ionizers() - ionizers_transitive_const.electron_species = other_electron + instance_transitive_const = Implementation() + instance_transitive_const.ionization_electron_species = other_electron - self.electron.constants.append(ionizers_transitive_const) + self.electron.constants.append(GroundStateIonization(ionization_model_list=[instance_transitive_const])) - # original ionizers is valid - ionizers_transitive_const.check() + # original instance is valid + instance_transitive_const.check() # ...but a constant using an ionizable species as electrons must reject - ionizers = Ionizers() - ionizers.electron_species = self.electron + instance = Implementation() + instance.ionization_electron_species = self.electron with self.assertRaisesRegex(ValueError, ".*ionizable.*"): - ionizers.check() + instance.check() def test_check_passthru(self): """calls check of electron species & checks during rendering""" - ionizers = Ionizers() - - # must raise (b/c no electron species) - with self.assertRaises(Exception): - ionizers.check() - - # subsequently, dependency retrieval mus also raise - with self.assertRaises(Exception): - ionizers.get_species_dependencies() - - ionizers.electron_species = self.electron + instance = Implementation() + instance.ionization_electron_species = self.electron # both pass: - ionizers.check() - self.assertNotEqual([], ionizers.get_species_dependencies()) + instance.check() + self.assertNotEqual([], instance.get_species_dependencies()) # with a broken species... - ionizers.electron_species = None + instance.ionization_electron_species = None # ...check()... with self.assertRaises(Exception): - ionizers.check() + instance.check() # ...and get dependencies fail with self.assertRaises(Exception): - ionizers.get_species_dependencies() + instance.get_species_dependencies() def test_rendering(self): """renders to rendering context""" @@ -133,27 +144,27 @@ def test_rendering(self): self.electron.check() self.assertNotEqual({}, self.electron.get_rendering_context()) - ionizers = Ionizers() - ionizers.electron_species = self.electron + instance = Implementation() + instance.ionization_electron_species = self.electron - context = ionizers.get_rendering_context() + context = instance.get_rendering_context() self.assertNotEqual({}, context) - self.assertEqual(self.electron.get_rendering_context(), context["electron_species"]) + self.assertEqual(self.electron.get_rendering_context(), context["ionization_electron_species"]) # do *NOT* render if check() does not pass - ionizers.electron_species = None + instance.ionization_electron_species = None with self.assertRaises(TypeError): - ionizers.check() + instance.check() with self.assertRaises(TypeError): - ionizers.get_rendering_context() + instance.get_rendering_context() # pass again - ionizers.electron_species = self.electron - ionizers.check() + instance.ionization_electron_species = self.electron + instance.check() # do *NOT* render if electron species is broken - ionizers.electron_species.attributes = [] + instance.ionization_electron_species.attributes = [] with self.assertRaises(ValueError): - ionizers.electron_species.check() + instance.ionization_electron_species.check() with self.assertRaises(ValueError): - ionizers.get_rendering_context() + instance.get_rendering_context() diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelgroups.py b/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelgroups.py new file mode 100644 index 0000000000..1bc14fd3a4 --- /dev/null +++ b/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelgroups.py @@ -0,0 +1,104 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from picongpu.pypicongpu.species.constant.ionizationmodel import IonizationModelGroups + +from picongpu.pypicongpu.species.constant.ionizationmodel import BSI, BSIEffectiveZ, BSIStarkShifted +from picongpu.pypicongpu.species.constant.ionizationmodel import ADKLinearPolarization, ADKCircularPolarization +from picongpu.pypicongpu.species.constant.ionizationmodel import Keldysh, ThomasFermi + +import unittest +import copy + + +class Test_IonizationModelGroups(unittest.TestCase): + def setUp(self): + self.expected_groups_custom = { + "1": [BSI], + "2": [ADKLinearPolarization, ADKCircularPolarization], + } + + self.expected_groups_standard = { + "BSI_like": [BSI, BSIEffectiveZ, BSIStarkShifted], + "ADK_like": [ADKLinearPolarization, ADKCircularPolarization], + "Keldysh_like": [Keldysh], + "electronic_collisional": [ThomasFermi], + } + + self.expected_by_model_custom = { + BSI: "1", + ADKCircularPolarization: "2", + ADKLinearPolarization: "2", + } + + def test_creation(self): + """may be constructed""" + # default value construction + IonizationModelGroups() + + # custom value construction + IonizationModelGroups(by_group=self.expected_groups_custom) + + def test_get_by_group(self): + """by_group is correctly returned""" + self.assertEqual(IonizationModelGroups().get_by_group(), self.expected_groups_standard) + self.assertEqual( + IonizationModelGroups(by_group=self.expected_groups_custom).get_by_group(), self.expected_groups_custom + ) + + def test_get_by_model(self): + """by_group is correctly converted to by_model""" + self.assertEqual( + IonizationModelGroups(by_group=self.expected_groups_custom).get_by_model(), self.expected_by_model_custom + ) + + def _switch_groups(self, result, one, two): + keys = list(result.keys()) + values = list(result.values()) + + first_group = keys[one] + second_group = keys[two] + + first_models = values[one] + second_models = values[two] + + result[first_group] = second_models + result[second_group] = first_models + + return result + + def test_get_by_group_returns_copy(self): + """get_by_group() return copies only""" + ionization_model_group = IonizationModelGroups(by_group=self.expected_groups_custom) + + # get result + result = ionization_model_group.get_by_group() + + # make copy for reference + result_copy = copy.copy(result) + + # manipulate result + result = self._switch_groups(result, 0, 1) + + # check output is unchanged + self.assertEqual(result_copy, ionization_model_group.get_by_group()) + + def test_get_by_model_returns_copy(self): + """get_by_model returns copies only""" + ionization_model_group = IonizationModelGroups(by_group=self.expected_groups_custom) + + # get result + result = ionization_model_group.get_by_model() + + # make copy for reference + result_copy = copy.copy(result) + + # manipulate result + result = self._switch_groups(result, 0, 1) + + # check output is unchanged + self.assertEqual(result_copy, ionization_model_group.get_by_model()) diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelimplementations.py b/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelimplementations.py new file mode 100644 index 0000000000..65a6c74219 --- /dev/null +++ b/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelimplementations.py @@ -0,0 +1,62 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from picongpu.pypicongpu.species.constant.ionizationmodel import BSI, BSIEffectiveZ, BSIStarkShifted +from picongpu.pypicongpu.species.constant.ionizationmodel import ADKLinearPolarization, ADKCircularPolarization +from picongpu.pypicongpu.species.constant.ionizationmodel import Keldysh, ThomasFermi +from picongpu.pypicongpu.species.constant.ionizationcurrent import None_ +from picongpu.pypicongpu.species.constant import Charge, Mass +from picongpu.pypicongpu.species import Species +from picongpu.picmi import constants + +import unittest + + +class Test_IonizationModelImplementations(unittest.TestCase): + implementations_withIonizationCurrent = [ + BSI, + BSIEffectiveZ, + BSIStarkShifted, + ADKCircularPolarization, + ADKLinearPolarization, + Keldysh, + ] + + implementations_withoutIonizationCurrent = [ThomasFermi] + + def setUp(self): + electron = Species() + electron.name = "e" + mass_constant = Mass() + mass_constant.mass_si = constants.m_e + charge_constant = Charge() + charge_constant.charge_si = constants.m_e + electron.constants = [ + charge_constant, + mass_constant, + ] + # note: attributes not set yet (as would be in init manager) + + self.electron = electron + + def test_ionizationCurrentRequired(self): + """ionization current must be explicitly configured""" + for Implementation in self.implementations_withIonizationCurrent: + with self.assertRaisesRegex(Exception, ".*ionization_current.*"): + implementation = Implementation(ionization_electron_species=self.electron) + # do not call get_rendering_context, since species not completely initialized yet + implementation.check() + + def test_basic(self): + """may create and serialize""" + for Implementation in self.implementations_withIonizationCurrent: + implementation = Implementation(ionization_electron_species=self.electron, ionization_current=None_()) + implementation.check() + + for Implementation in self.implementations_withoutIonizationCurrent: + implementation = Implementation(ionization_electron_species=self.electron) + implementation.check() diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/~groundstateionization.py b/test/python/picongpu/quick/pypicongpu/species/constant/~groundstateionization.py new file mode 100644 index 0000000000..ed98e56972 --- /dev/null +++ b/test/python/picongpu/quick/pypicongpu/species/constant/~groundstateionization.py @@ -0,0 +1,28 @@ +""" +This file is part of PIConGPU. +Copyright 2021-2023 PIConGPU contributors +Authors: Hannes Troepgen, Brian Edward Marre +License: GPLv3+ +""" + + +from picongpu.pypicongpu.species import Species +from picongpu.pypicongpu.species.constant import Mass, Charge +from picongpu.picmi import constants + + +class TestGroundStateIonization: + def setUp(self): + electron = Species() + electron.name = "e" + mass_constant = Mass() + mass_constant.mass_si = constants.m_e + charge_constant = Charge() + charge_constant.charge_si = constants.m_e + electron.constants = [ + charge_constant, + mass_constant, + ] + # note: attributes not set yet (as would be in init manager) + + self.electron = electron diff --git a/test/python/picongpu/quick/pypicongpu/species/operation/noboundelectrons.py b/test/python/picongpu/quick/pypicongpu/species/operation/noboundelectrons.py index 50f191744e..45af841494 100644 --- a/test/python/picongpu/quick/pypicongpu/species/operation/noboundelectrons.py +++ b/test/python/picongpu/quick/pypicongpu/species/operation/noboundelectrons.py @@ -11,7 +11,7 @@ import typeguard from picongpu.pypicongpu.species import Species -from picongpu.pypicongpu.species.constant import Ionizers +from picongpu.pypicongpu.species.constant import GroundStateIonization from picongpu.pypicongpu.species.attribute import BoundElectrons @@ -19,7 +19,7 @@ class TestNoBoundElectrons(unittest.TestCase): def setUp(self): self.species1 = Species() self.species1.name = "ion" - self.species1.constants = [Ionizers()] + self.species1.constants = [GroundStateIonization()] def test_no_rendering_context(self): """results in no rendered code, hence no rendering context available""" @@ -46,7 +46,7 @@ def test_ionizers_required(self): nbe = NoBoundElectrons() nbe.species = self.species1 - self.assertTrue(self.species1.has_constant_of_type(Ionizers)) + self.assertTrue(self.species1.has_constant_of_type(GroundStateIonization)) # passes nbe.check_preconditions() @@ -55,7 +55,7 @@ def test_ionizers_required(self): self.species1.constants = [] # now raises b/c ionizers constant is missing - with self.assertRaisesRegex(AssertionError, ".*[Ii]onizers.*"): + with self.assertRaisesRegex(AssertionError, ".*[Gg]roundStateIonization.*"): nbe.check_preconditions() def test_empty(self): diff --git a/test/python/picongpu/quick/pypicongpu/species/operation/setboundelectrons.py b/test/python/picongpu/quick/pypicongpu/species/operation/setboundelectrons.py index 21811e13b0..c560724735 100644 --- a/test/python/picongpu/quick/pypicongpu/species/operation/setboundelectrons.py +++ b/test/python/picongpu/quick/pypicongpu/species/operation/setboundelectrons.py @@ -11,7 +11,7 @@ import typeguard from picongpu.pypicongpu.species import Species -from picongpu.pypicongpu.species.constant import Ionizers +from picongpu.pypicongpu.species.constant import GroundStateIonization from picongpu.pypicongpu.species.attribute import BoundElectrons, Position, Momentum @@ -19,7 +19,7 @@ class TestSetBoundElectrons(unittest.TestCase): def setUp(self): self.species1 = Species() self.species1.name = "ion" - self.species1.constants = [Ionizers()] + self.species1.constants = [GroundStateIonization()] def test_basic(self): """basic operation""" @@ -87,7 +87,7 @@ def test_ionizers_required(self): sbe.bound_electrons = 1 # passes: - self.assertTrue(sbe.species.has_constant_of_type(Ionizers)) + self.assertTrue(sbe.species.has_constant_of_type(GroundStateIonization)) sbe.check_preconditions() # without constants does not pass: @@ -125,7 +125,7 @@ def test_rendering(self): ion = Species() ion.name = "ion" - ionizers_const = Ionizers() + ionizers_const = GroundStateIonization() ionizers_const.electron_species = electron ion.constants = [ionizers_const] ion.attributes = [Position(), Momentum(), BoundElectrons()] diff --git a/test/python/picongpu/quick/pypicongpu/species/species.py b/test/python/picongpu/quick/pypicongpu/species/species.py index cf8e5e24dd..a49770d63c 100644 --- a/test/python/picongpu/quick/pypicongpu/species/species.py +++ b/test/python/picongpu/quick/pypicongpu/species/species.py @@ -12,7 +12,6 @@ Mass, Charge, DensityRatio, - Ionizers, ElementProperties, ) from picongpu.pypicongpu.species.util import Element @@ -46,7 +45,7 @@ def setUp(self): self.const_mass.mass_si = 2 self.const_density_ratio = DensityRatio() self.const_density_ratio.ratio = 4.2 - self.const_ionizers = Ionizers() + self.const_ionizers = None self.const_ionizers.electron_species = self.electron self.const_element_properties = ElementProperties() From 6551dbac4f7dce9e11e1c8e141c902bb1e45b1f9 Mon Sep 17 00:00:00 2001 From: BrianMarre Date: Mon, 15 Jul 2024 19:53:46 +0200 Subject: [PATCH 04/20] fixing PyPIConGPU tests continued refactor of PyPIConGPU tests to tupdate them to the new PyPIConGPU ionization configuration interface --- lib/python/picongpu/picmi/simulation.py | 9 +++-- .../pypicongpu/rendering/renderedobject.py | 12 +++++-- .../pypicongpu/species/constant/constant.py | 4 --- .../species/constant/groundstateionization.py | 11 +++++- .../constant/ionizationcurrent/none_.py | 2 +- .../constant/ionizationmodel/BSIeffectiveZ.py | 2 +- .../ionizationmodel/BSIstarkshifted.py | 2 +- .../ionizationmodel/ionizationmodel.py | 7 ++++ .../constant/ionizationmodel/keldysh.py | 2 +- .../constant/ionizationmodel/thomasfermi.py | 2 +- .../pypicongpu/species/initmanager.py | 6 ++-- .../picongpu/pypicongpu/species/species.py | 4 +++ ...stateionization.GroundStateIonization.json | 14 ++++++++ ...odel.ionizationmodel.IonizationModel.json} | 4 +-- .../schema/species/species.Species.json | 10 +----- .../pypicongpu/species/constant/constant.py | 34 +++---------------- .../ionizationmodelimplementations.py | 33 +++++++++++------- .../quick/pypicongpu/species/initmanager.py | 15 ++++++-- .../species/operation/noboundelectrons.py | 12 ++++++- .../species/operation/setboundelectrons.py | 14 +++++++- .../quick/pypicongpu/species/species.py | 20 ++++------- 21 files changed, 131 insertions(+), 88 deletions(-) create mode 100644 share/picongpu/pypicongpu/schema/species/constant/groundstateionization.GroundStateIonization.json rename share/picongpu/pypicongpu/schema/species/constant/{ionizationmodel.IonizationModel.json => ionizationmodel.ionizationmodel.IonizationModel.json} (85%) diff --git a/lib/python/picongpu/picmi/simulation.py b/lib/python/picongpu/picmi/simulation.py index 884f7f8d88..b964b4bca0 100644 --- a/lib/python/picongpu/picmi/simulation.py +++ b/lib/python/picongpu/picmi/simulation.py @@ -272,7 +272,7 @@ def __fill_ionization_electrons( for picmi_species, pypic_species in pypicongpu_by_picmi_species.items(): # only fill ionization electrons if required (by ionizers) - if not pypic_species.has_constant_of_type(species.constant.Ionizers): + if not pypic_species.has_constant_of_type(species.constant.GroundStateIonization): continue assert picmi_species.picongpu_ionization_electrons in pypicongpu_by_picmi_species, ( @@ -283,9 +283,12 @@ def __fill_ionization_electrons( ) ) - ionizers = pypic_species.get_constant_by_type(species.constant.Ionizers) + ionizer_model_list = pypic_species.get_constant_by_type(species.constant.GroundStateIonization) # is pointer -> sets correct species for actual pypicongpu species - ionizers.electron_species = pypicongpu_by_picmi_species[picmi_species.picongpu_ionization_electrons] + for model in ionizer_model_list: + model.ionization_electron_species = pypicongpu_by_picmi_species[ + picmi_species.picongpu_ionization_electrons + ] def __get_init_manager(self) -> species.InitManager: """ diff --git a/lib/python/picongpu/pypicongpu/rendering/renderedobject.py b/lib/python/picongpu/pypicongpu/rendering/renderedobject.py index 261c499562..575fa259e4 100644 --- a/lib/python/picongpu/pypicongpu/rendering/renderedobject.py +++ b/lib/python/picongpu/pypicongpu/rendering/renderedobject.py @@ -54,11 +54,17 @@ class RenderedObject: """ def __hash__(self): - """custom hash function for indexing in dicts @todo move to rendered_object, Brian Marre, 2024""" + """custom hash function for indexing in dicts""" hash_value = hash(type(self)) + for value in self.__dict__.values(): - if value is not None: - hash_value += hash(value) + try: + if value is not None: + hash_value += hash(value) + except TypeError: + print(self) + print(type(self)) + raise TypeError return hash_value @staticmethod diff --git a/lib/python/picongpu/pypicongpu/species/constant/constant.py b/lib/python/picongpu/pypicongpu/species/constant/constant.py index edd1ff85b5..6aa4b82adc 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/constant.py +++ b/lib/python/picongpu/pypicongpu/species/constant/constant.py @@ -45,10 +45,6 @@ class Constant(RenderedObject): constant) """ - def __init__(self, **kw): - if type(self) == Constant: - raise NotImplementedError() - def check(self) -> None: """ ensure validity of self diff --git a/lib/python/picongpu/pypicongpu/species/constant/groundstateionization.py b/lib/python/picongpu/pypicongpu/species/constant/groundstateionization.py index 8d897d14ce..395dd5016a 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/groundstateionization.py +++ b/lib/python/picongpu/pypicongpu/species/constant/groundstateionization.py @@ -16,6 +16,15 @@ class GroundStateIonization(Constant, pydantic.BaseModel): ionization_model_list: list[IonizationModel] """list of ground state only ionization models to apply for the species""" + def get(self): + return self.ionization_model_list + + def __hash__(self) -> int: + return_hash_value = hash(type(self)) + for model in self.ionization_model_list: + return_hash_value += hash(model) + return return_hash_value + def check(self) -> None: # check that no ionization model class is doubled up? groups = IonizationModelGroups().get_by_group().keys() @@ -77,6 +86,6 @@ def _get_serialized(self) -> dict[str, list[dict[str, typing.Any]]]: list_serialized = [] for ionization_model in self.ionization_model_list: - list_serialized.append(ionization_model.get_rendering_context()) + list_serialized.append(ionization_model.get_generic_rendering_context()) return {"ionization_model_list": list_serialized} diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/none_.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/none_.py index 26b9f30a1e..c99d0f6a3d 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/none_.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/none_.py @@ -9,4 +9,4 @@ class None_(IonizationCurrent): - picongpu_name: str = "None" + PICONGPU_NAME: str = "None" diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIeffectiveZ.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIeffectiveZ.py index 1827cc534a..01ed232ec3 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIeffectiveZ.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIeffectiveZ.py @@ -19,7 +19,7 @@ class BSIEffectiveZ(IonizationModel): shielding, but still neglecting the Stark upshift of ionization energies. """ - PICONGPU_NAME: str = "BSI" + PICONGPU_NAME: str = "BSIEffectiveZ" """C++ Code type name of ionizer""" ionization_current: IonizationCurrent diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIstarkshifted.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIstarkshifted.py index 89b11494b9..c9b77d2111 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIstarkshifted.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/BSIstarkshifted.py @@ -18,7 +18,7 @@ class BSIStarkShifted(IonizationModel): Variant of the BSI ionization model accounting for the Stark upshift of ionization energies. """ - PICONGPU_NAME: str = "BSI" + PICONGPU_NAME: str = "BSIStarkShifted" """C++ Code type name of ionizer""" ionization_current: IonizationCurrent diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py index a0cff1d359..185f56cbb9 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py @@ -70,6 +70,13 @@ def _get_serialized(self) -> dict[str, typing.Any]: "ionization_current": self.ionization_current, } + def get_generic_rendering_context(self) -> dict[str, typing.Any]: + return IonizationModel( + PICONGPU_NAME=self.PICONGPU_NAME, + ionization_electron_species=self.ionization_electron_species, + ionization_current=self.ionization_current, + ).get_rendering_context() + def get_species_dependencies(self) -> list[type]: self.check() return [self.ionization_electron_species] diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/keldysh.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/keldysh.py index 7508c5b405..b61876fc58 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/keldysh.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/keldysh.py @@ -22,7 +22,7 @@ class Keldysh(IonizationModel): high intensity laser fields. """ - PICONGPU_NAME: str = "BSI" + PICONGPU_NAME: str = "Keldysh" """C++ Code type name of ionizer""" ionization_current: IonizationCurrent diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/thomasfermi.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/thomasfermi.py index e28d5f1318..fc88413c31 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/thomasfermi.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/thomasfermi.py @@ -26,5 +26,5 @@ class ThomasFermi(IonizationModel): This is extenden to arbitrary temperatures and atoms through fitting parameters and temperature cutoffs. """ - PICONGPU_NAME: str = "BSI" + PICONGPU_NAME: str = "ThomasFermi" """C++ Code type name of ionizer""" diff --git a/lib/python/picongpu/pypicongpu/species/initmanager.py b/lib/python/picongpu/pypicongpu/species/initmanager.py index 9ae6e38016..6de5f677d2 100644 --- a/lib/python/picongpu/pypicongpu/species/initmanager.py +++ b/lib/python/picongpu/pypicongpu/species/initmanager.py @@ -68,6 +68,10 @@ def __init__(self) -> None: self.all_operations = [] self.__baked = False + def __hash__(self) -> int: + # every simulation may only ever have one InitManager + return hash(type(self)) + def __get_all_attributes(self): """ accumulate *all* attributes currently assigned to any species @@ -235,8 +239,6 @@ def __check_species_dependencies_circular(self) -> None: closure_size_after = len(dependency_closure) is_closure_final = closure_size_after == closure_size_before - print(dependency_closure) - # check: self in dependency closure? if species in dependency_closure: raise RecursionError( diff --git a/lib/python/picongpu/pypicongpu/species/species.py b/lib/python/picongpu/pypicongpu/species/species.py index 616b9d3d56..7749d52f7a 100644 --- a/lib/python/picongpu/pypicongpu/species/species.py +++ b/lib/python/picongpu/pypicongpu/species/species.py @@ -50,6 +50,10 @@ def get_cxx_typename(self) -> str: return "species_" + self.name + def __hash__(self): + # species must be uniquely defined by name + return hash(self.name) + def check(self) -> None: """ sanity-check self, if ok pass silently diff --git a/share/picongpu/pypicongpu/schema/species/constant/groundstateionization.GroundStateIonization.json b/share/picongpu/pypicongpu/schema/species/constant/groundstateionization.GroundStateIonization.json new file mode 100644 index 0000000000..1a541597a4 --- /dev/null +++ b/share/picongpu/pypicongpu/schema/species/constant/groundstateionization.GroundStateIonization.json @@ -0,0 +1,14 @@ +{ + "$id":"https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.groundstateionization.GroundStateIonization", + "required":["ionization_model_list"], + "unevaluated":false, + "properties": { + "ionization_model_list": { + "type": "array", + "description": "list of ionization models for species", + "items": { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationmodel.ionizationmodel.IonizationModel" + } + } + } +} diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizationmodel.IonizationModel.json b/share/picongpu/pypicongpu/schema/species/constant/ionizationmodel.ionizationmodel.IonizationModel.json similarity index 85% rename from share/picongpu/pypicongpu/schema/species/constant/ionizationmodel.IonizationModel.json rename to share/picongpu/pypicongpu/schema/species/constant/ionizationmodel.ionizationmodel.IonizationModel.json index 9eb797f556..a175021e66 100644 --- a/share/picongpu/pypicongpu/schema/species/constant/ionizationmodel.IonizationModel.json +++ b/share/picongpu/pypicongpu/schema/species/constant/ionizationmodel.ionizationmodel.IonizationModel.json @@ -1,5 +1,5 @@ { - "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.schema.constant.ionizationmodel.IonizationModel", + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationmodel.ionizationmodel.IonizationModel", "type": "object", "unevaluatedProperties": false, "required": [ @@ -21,7 +21,7 @@ "ionization_current": { "anyOf": [ {"type": "null"}, - {"$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.schema.constant.ionizationcurrent.IonizationCurrent"} + {"$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationcurrent.IonizationCurrent"} ] } } diff --git a/share/picongpu/pypicongpu/schema/species/species.Species.json b/share/picongpu/pypicongpu/schema/species/species.Species.json index 63b548d039..67d6b22bec 100644 --- a/share/picongpu/pypicongpu/schema/species/species.Species.json +++ b/share/picongpu/pypicongpu/schema/species/species.Species.json @@ -70,15 +70,7 @@ "ground_state_ionization": { "anyOf": [ {"type": "null"}, - { - "ionization_model_list": { - "type": "array", - "description": "list of ionization models for species", - "items": { - "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationmodel.IonizationModel" - } - } - } + {"$ref":"https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.groundstateionization.GroundStateIonization"} ] }, "element_properties": { diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/constant.py b/test/python/picongpu/quick/pypicongpu/species/constant/constant.py index c2d1d9244d..a25b9823b2 100644 --- a/test/python/picongpu/quick/pypicongpu/species/constant/constant.py +++ b/test/python/picongpu/quick/pypicongpu/species/constant/constant.py @@ -10,43 +10,17 @@ import unittest -class DummyConstant(Constant): - def __init__(self): - pass - - def check(self): - pass - - class TestConstant(unittest.TestCase): - def test_abstract(self): - """methods are not implemented""" - with self.assertRaises(NotImplementedError): - Constant() - - # must pass silently - dc = DummyConstant() - dc.check() - - def test_check_abstract(self): - class ConstantCheckAbstract(Constant): - def __init__(self): - pass - - # check() not overwritten - - cca = ConstantCheckAbstract() - with self.assertRaises(NotImplementedError): - cca.check() - def test_rendering_abstract(self): """rendering context not implemented, but available""" - dc = DummyConstant() + dc = Constant() with self.assertRaises(NotImplementedError): dc.get_rendering_context() def test_dependencies_abstract(self): - dc = DummyConstant() + dc = Constant() + with self.assertRaises(NotImplementedError): + Constant().check() with self.assertRaises(NotImplementedError): dc.get_species_dependencies() with self.assertRaises(NotImplementedError): 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 65a6c74219..ce8d860115 100644 --- a/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelimplementations.py +++ b/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelimplementations.py @@ -17,16 +17,16 @@ class Test_IonizationModelImplementations(unittest.TestCase): - implementations_withIonizationCurrent = [ - BSI, - BSIEffectiveZ, - BSIStarkShifted, - ADKCircularPolarization, - ADKLinearPolarization, - Keldysh, - ] + implementations_withIonizationCurrent = { + BSI: "BSI", + BSIEffectiveZ: "BSIEffectiveZ", + BSIStarkShifted: "BSIStarkShifted", + ADKCircularPolarization: "ADKLinPol", + ADKLinearPolarization: "ADKCircPol", + Keldysh: "Keldysh", + } - implementations_withoutIonizationCurrent = [ThomasFermi] + implementations_withoutIonizationCurrent = {ThomasFermi: "ThomasFermi"} def setUp(self): electron = Species() @@ -45,7 +45,7 @@ def setUp(self): def test_ionizationCurrentRequired(self): """ionization current must be explicitly configured""" - for Implementation in self.implementations_withIonizationCurrent: + for Implementation in self.implementations_withIonizationCurrent.keys(): with self.assertRaisesRegex(Exception, ".*ionization_current.*"): implementation = Implementation(ionization_electron_species=self.electron) # do not call get_rendering_context, since species not completely initialized yet @@ -53,10 +53,19 @@ def test_ionizationCurrentRequired(self): def test_basic(self): """may create and serialize""" - for Implementation in self.implementations_withIonizationCurrent: + for Implementation in self.implementations_withIonizationCurrent.keys(): implementation = Implementation(ionization_electron_species=self.electron, ionization_current=None_()) implementation.check() - for Implementation in self.implementations_withoutIonizationCurrent: + for Implementation in self.implementations_withoutIonizationCurrent.keys(): implementation = Implementation(ionization_electron_species=self.electron) implementation.check() + + def test_picongpu_name(self): + for Implementation, name in self.implementations_withoutIonizationCurrent.items(): + self.assertEqual( + name, + Implementation(ionization_electron_species=self.electron, ionization_current=None_()).PICONGPU_NAME, + ) + for Implementation, name in self.implementations_withoutIonizationCurrent.items(): + self.assertEqual(name, Implementation(ionization_electron_species=self.electron)) diff --git a/test/python/picongpu/quick/pypicongpu/species/initmanager.py b/test/python/picongpu/quick/pypicongpu/species/initmanager.py index d81734f572..847e6537dd 100644 --- a/test/python/picongpu/quick/pypicongpu/species/initmanager.py +++ b/test/python/picongpu/quick/pypicongpu/species/initmanager.py @@ -83,6 +83,9 @@ def __init__(self, unique_id="", species_list=[]): self.species_list = species_list self.unique_id = unique_id + def __hash__(self) -> int: + return hash(self.unique_id) + def get_attr_name(self): return "tracer_attr_" + self.unique_id @@ -104,6 +107,12 @@ class OperationAddMandatoryAttributes(species.operation.Operation): def __init__(self, species_list=[]): self.species_list = species_list + def __hash__(self): + return_hash_value = hash(type(hash)) + for species_ in self.species_list: + return_hash_value += hash(species_) + return return_hash_value + def check_preconditions(self): pass @@ -865,8 +874,10 @@ def test_set_bound_electrons_passthrough(self): ion = species.Species() ion.name = "ion" - ionizers_const = species.constant.Ionizers() - ionizers_const.electron_species = electron + ionizers_const = species.constant.GroundStateIonization( + ionization_model_list=[species.constant.ionizationmodel.ThomasFermi()] + ) + ionizers_const.ionization_model_list[0].ionization_electron_species = electron element_const = species.constant.ElementProperties() element_const.element = species.util.Element.N ion.constants = [ionizers_const, element_const] diff --git a/test/python/picongpu/quick/pypicongpu/species/operation/noboundelectrons.py b/test/python/picongpu/quick/pypicongpu/species/operation/noboundelectrons.py index 45af841494..717137aba7 100644 --- a/test/python/picongpu/quick/pypicongpu/species/operation/noboundelectrons.py +++ b/test/python/picongpu/quick/pypicongpu/species/operation/noboundelectrons.py @@ -12,14 +12,24 @@ from picongpu.pypicongpu.species import Species from picongpu.pypicongpu.species.constant import GroundStateIonization +from picongpu.pypicongpu.species.constant.ionizationmodel import BSI +from picongpu.pypicongpu.species.constant.ionizationcurrent import None_ from picongpu.pypicongpu.species.attribute import BoundElectrons class TestNoBoundElectrons(unittest.TestCase): def setUp(self): + electron = Species() + electron.name = "e" + self.electron = electron + self.species1 = Species() self.species1.name = "ion" - self.species1.constants = [GroundStateIonization()] + self.species1.constants = [ + GroundStateIonization( + ionization_model_list=[BSI(ionization_electron_species=self.electron, ionization_current=None_())] + ) + ] def test_no_rendering_context(self): """results in no rendered code, hence no rendering context available""" diff --git a/test/python/picongpu/quick/pypicongpu/species/operation/setboundelectrons.py b/test/python/picongpu/quick/pypicongpu/species/operation/setboundelectrons.py index c560724735..a6d608cde0 100644 --- a/test/python/picongpu/quick/pypicongpu/species/operation/setboundelectrons.py +++ b/test/python/picongpu/quick/pypicongpu/species/operation/setboundelectrons.py @@ -12,14 +12,26 @@ from picongpu.pypicongpu.species import Species from picongpu.pypicongpu.species.constant import GroundStateIonization +from picongpu.pypicongpu.species.constant.ionizationmodel import BSI +from picongpu.pypicongpu.species.constant.ionizationcurrent import None_ from picongpu.pypicongpu.species.attribute import BoundElectrons, Position, Momentum class TestSetBoundElectrons(unittest.TestCase): def setUp(self): + electron = Species() + electron.name = "e" + # note: attributes not set yet (as would be in init manager) + + self.electron = electron + self.species1 = Species() self.species1.name = "ion" - self.species1.constants = [GroundStateIonization()] + self.species1.constants = [ + GroundStateIonization( + ionization_model_list=[BSI(ionization_electron_species=self.electron, ionization_current=None_())] + ) + ] def test_basic(self): """basic operation""" diff --git a/test/python/picongpu/quick/pypicongpu/species/species.py b/test/python/picongpu/quick/pypicongpu/species/species.py index a49770d63c..1951373bc6 100644 --- a/test/python/picongpu/quick/pypicongpu/species/species.py +++ b/test/python/picongpu/quick/pypicongpu/species/species.py @@ -8,16 +8,10 @@ from picongpu.pypicongpu.species import Species from picongpu.pypicongpu.species.attribute import Position, Weighting, Momentum -from picongpu.pypicongpu.species.constant import ( - Mass, - Charge, - DensityRatio, - ElementProperties, -) +from picongpu.pypicongpu.species.constant import Mass, Charge, DensityRatio, ElementProperties, Constant from picongpu.pypicongpu.species.util import Element from .attribute import DummyAttribute -from .constant import DummyConstant import itertools import unittest @@ -38,7 +32,7 @@ def setUp(self): self.electron.attributes = [Position(), Momentum()] self.electron.constants = [] - self.const = DummyConstant() + self.const = Constant() self.const_charge = Charge() self.const_charge.charge_si = 1 self.const_mass = Mass() @@ -78,12 +72,12 @@ def test_types(self): with self.assertRaises(typeguard.TypeCheckError): species.name = invalid_name - invalid_attr_lists = [None, {}, set(), [DummyConstant()], DummyAttribute()] + invalid_attr_lists = [None, {}, set(), [Constant()], DummyAttribute()] for invalid_attr_list in invalid_attr_lists: with self.assertRaises(typeguard.TypeCheckError): species.attributes = invalid_attr_list - invalid_const_lists = [None, {}, set(), [DummyAttribute()], DummyConstant()] + invalid_const_lists = [None, {}, set(), [DummyAttribute()], Constant()] for invalid_const_list in invalid_const_lists: with self.assertRaises(typeguard.TypeCheckError): species.constants = invalid_const_list @@ -134,7 +128,7 @@ def test_constants_unique(self): const1.charge_si = 17 const2 = Charge() const2.charge_si = 18 - other_const = DummyConstant() + other_const = Constant() species.constants = [const1, const2, other_const] @@ -150,7 +144,7 @@ def test_constants_unique(self): def test_check_constant_passthhru(self): """species check also calls constants check""" - class ConstantFail(DummyConstant): + class ConstantFail(Constant): ERROR_STR: str = "IDSTRING_XKCD_927_BEST" def check(self): @@ -204,7 +198,7 @@ def test_get_constant_by_type(self): species.constants = [self.const, self.const_mass, self.const_charge] # note: check for *identity* with is (instead of pure equality) - self.assertTrue(self.const is species.get_constant_by_type(DummyConstant)) + self.assertTrue(self.const is species.get_constant_by_type(Constant)) self.assertTrue(self.const_charge is species.get_constant_by_type(Charge)) self.assertTrue(self.const_mass is species.get_constant_by_type(Mass)) From ddb6ace29788033d9827a4716ef71ede244c3fda Mon Sep 17 00:00:00 2001 From: BrianMarre Date: Tue, 16 Jul 2024 13:31:46 +0200 Subject: [PATCH 05/20] add groundStateIonization as separate group distinguishing between ground-state-only ionization models and ionization models including excited states. This refactor is intended to allow the easy extension of PyPIConGPU. --- .../pypicongpu/rendering/renderedobject.py | 2 +- .../species/constant/groundstateionization.py | 18 +-- .../ionizationcurrent/ionizationcurrent.py | 3 + .../ionizationmodel/ionizationmodel.py | 11 +- ....ionizationcurrent.IonizationCurrent.json} | 2 +- ...model.ionizationmodel.IonizationModel.json | 2 +- .../species/constant/groundstateionization.py | 133 ++++++++++++++++++ .../constant/~groundstateionization.py | 28 ---- 8 files changed, 159 insertions(+), 40 deletions(-) rename share/picongpu/pypicongpu/schema/species/constant/{ionizationcurrent.IonizationCurrent.json => ionizationcurrent.ionizationcurrent.IonizationCurrent.json} (85%) delete mode 100644 test/python/picongpu/quick/pypicongpu/species/constant/~groundstateionization.py diff --git a/lib/python/picongpu/pypicongpu/rendering/renderedobject.py b/lib/python/picongpu/pypicongpu/rendering/renderedobject.py index 575fa259e4..46c064fe85 100644 --- a/lib/python/picongpu/pypicongpu/rendering/renderedobject.py +++ b/lib/python/picongpu/pypicongpu/rendering/renderedobject.py @@ -194,7 +194,7 @@ def get_rendering_context(self) -> dict | None: delegates work to _get_serialized and invokes checks performed by check_context_for_type(). - :raise ValidationError: on schema violiation + :raise ValidationError: on schema violation :raise RuntimeError: on schema not found :return: self as rendering context """ diff --git a/lib/python/picongpu/pypicongpu/species/constant/groundstateionization.py b/lib/python/picongpu/pypicongpu/species/constant/groundstateionization.py index 395dd5016a..65de4341e5 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/groundstateionization.py +++ b/lib/python/picongpu/pypicongpu/species/constant/groundstateionization.py @@ -26,13 +26,17 @@ def __hash__(self) -> int: return return_hash_value def check(self) -> None: - # check that no ionization model class is doubled up? - groups = IonizationModelGroups().get_by_group().keys() + # check that at least one ionization model in list + if len(self.ionization_model_list) == 0: + raise ValueError("at least one ionization model must be specified if ground_state_ionization is not none.") - # check all ionization models + # call check() all ionization models for ionization_model in self.ionization_model_list: ionization_model.check() + # check that no ionization model group is represented more than once + groups = IonizationModelGroups().get_by_group().keys() + type_already_present = {} for group in groups: type_already_present[group] = False @@ -41,11 +45,9 @@ def check(self) -> None: for ionization_model in self.ionization_model_list: group: str = by_model[type(ionization_model)] if type_already_present[group]: - raise ValueError(f"ionization model group already represented. {group}") - - # check that at least one ionization model in list - if len(self.ionization_model_list) == 0: - raise ValueError("at least must ionization model must be specfied if ground_state_ionization is not none.") + raise ValueError(f"ionization model group already represented: {group}") + else: + type_already_present[group] = True def get_species_dependencies(self) -> list[type]: """get all species one of the ionization models in ionization_model_list depends on""" diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/ionizationcurrent.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/ionizationcurrent.py index 225b9065f4..6cbd45ddd4 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/ionizationcurrent.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/ionizationcurrent.py @@ -26,6 +26,9 @@ def _get_serialized(self) -> dict: self.check() return {"picongpu_name": self.PICONGPU_NAME} + def get_generic_rendering_context(self) -> dict: + return IonizationCurrent(PICONGPU_NAME=self.PICONGPU_NAME).get_rendering_context() + def get_species_dependencies(self): return [] diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py index 185f56cbb9..8ed4644ac2 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodel.py @@ -64,10 +64,19 @@ def _get_serialized(self) -> dict[str, typing.Any]: # do not remove!, always do a check call self.check() + if self.ionization_current is None: + # case no ionization_current configurable + return { + "ionizer_picongpu_name": self.PICONGPU_NAME, + "ionization_electron_species": self.ionization_electron_species.get_rendering_context(), + "ionization_current": None, + } + + # default case return { "ionizer_picongpu_name": self.PICONGPU_NAME, "ionization_electron_species": self.ionization_electron_species.get_rendering_context(), - "ionization_current": self.ionization_current, + "ionization_current": self.ionization_current.get_generic_rendering_context(), } def get_generic_rendering_context(self) -> dict[str, typing.Any]: diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizationcurrent.IonizationCurrent.json b/share/picongpu/pypicongpu/schema/species/constant/ionizationcurrent.ionizationcurrent.IonizationCurrent.json similarity index 85% rename from share/picongpu/pypicongpu/schema/species/constant/ionizationcurrent.IonizationCurrent.json rename to share/picongpu/pypicongpu/schema/species/constant/ionizationcurrent.ionizationcurrent.IonizationCurrent.json index 67ee0998a3..144f85238f 100644 --- a/share/picongpu/pypicongpu/schema/species/constant/ionizationcurrent.IonizationCurrent.json +++ b/share/picongpu/pypicongpu/schema/species/constant/ionizationcurrent.ionizationcurrent.IonizationCurrent.json @@ -1,5 +1,5 @@ { - "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.schema.constant.ionizationcurrent.IonizationCurrent", + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationcurrent.ionizationcurrent.IonizationCurrent", "type": "object", "description": "ionization current configuration", "required": ["picongpu_name"], diff --git a/share/picongpu/pypicongpu/schema/species/constant/ionizationmodel.ionizationmodel.IonizationModel.json b/share/picongpu/pypicongpu/schema/species/constant/ionizationmodel.ionizationmodel.IonizationModel.json index a175021e66..386600b35f 100644 --- a/share/picongpu/pypicongpu/schema/species/constant/ionizationmodel.ionizationmodel.IonizationModel.json +++ b/share/picongpu/pypicongpu/schema/species/constant/ionizationmodel.ionizationmodel.IonizationModel.json @@ -21,7 +21,7 @@ "ionization_current": { "anyOf": [ {"type": "null"}, - {"$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationcurrent.IonizationCurrent"} + {"$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.constant.ionizationcurrent.ionizationcurrent.IonizationCurrent"} ] } } diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/groundstateionization.py b/test/python/picongpu/quick/pypicongpu/species/constant/groundstateionization.py index f426d27aeb..204943ee33 100644 --- a/test/python/picongpu/quick/pypicongpu/species/constant/groundstateionization.py +++ b/test/python/picongpu/quick/pypicongpu/species/constant/groundstateionization.py @@ -4,3 +4,136 @@ Authors: Hannes Troepgen, Brian Edward Marre License: GPLv3+ """ + + +from picongpu.pypicongpu.species import Species +from picongpu.pypicongpu.species.attribute import Position, Momentum, BoundElectrons +from picongpu.pypicongpu.species.constant import Mass, Charge, GroundStateIonization, ElementProperties +from picongpu.pypicongpu.species.constant.ionizationmodel import BSI, BSIStarkShifted +from picongpu.pypicongpu.species.constant.ionizationcurrent import None_ +from picongpu.picmi import constants + +import unittest +import pydantic_core + + +class TestGroundStateIonization(unittest.TestCase): + # set maximum length to infinite to get sensible error message on fail + maxDiff = None + + def setUp(self): + electron = Species() + electron.name = "e" + mass_constant = Mass() + mass_constant.mass_si = constants.m_e + charge_constant = Charge() + charge_constant.charge_si = constants.m_e + electron.constants = [ + charge_constant, + mass_constant, + ] + + self.electron = electron + + self.BSI_instance = BSI(ionization_electron_species=self.electron, ionization_current=None_()) + self.BSIStark_instance = BSIStarkShifted(ionization_electron_species=self.electron, ionization_current=None_()) + + def test_basic(self): + """we may create basic Instance""" + # test we may create GroundStateIonization + GroundStateIonization(ionization_model_list=[self.BSI_instance]) + + def test_type_safety(self): + """may only add list of IonizationModel instances""" + + for invalid in ["BSI", ["BSI"], [1], 1.0, self.BSI_instance]: + with self.assertRaises(pydantic_core._pydantic_core.ValidationError): + GroundStateIonization(ionization_model_list=invalid) + + def test_check_empty_ionization_model_list(self): + """empty ionization model list is not allowed""" + + # assignment is possible + instance = GroundStateIonization(ionization_model_list=[]) + + with self.assertRaisesRegex( + ValueError, ".*at least one ionization model must be specified if ground_state_ionization is not none.*" + ): + # but check throws error + instance.check() + + def test_check_doubled_up_model_group(self): + """may not assign more than one ionization model from the same group""" + + # assignment is possible + instance = GroundStateIonization(ionization_model_list=[self.BSI_instance, self.BSIStark_instance]) + + with self.assertRaisesRegex(ValueError, ".*ionization model group already represented: BSI.*"): + # but check throws + instance.check() + + def test_check_call_on_ionization_model(self): + """check method of ionization models is called""" + + # creation is possible will only raise in check method + invalid_ionization_model = BSI(ionization_electron_species=None, ionization_current=None_()) + + # assignment is allowed + instance = GroundStateIonization(ionization_model_list=[invalid_ionization_model]) + with self.assertRaisesRegex(TypeError, ".*ionization_electron_species must be of type pypicongpu Species.*"): + # but check throws error + instance.check() + + def test_species_dependencies(self): + """correct return""" + self.assertEqual( + GroundStateIonization(ionization_model_list=[self.BSI_instance]).get_species_dependencies(), [self.electron] + ) + + def test_attribute_dependencies(self): + """correct return""" + self.assertEqual( + GroundStateIonization(ionization_model_list=[self.BSI_instance]).get_attribute_dependencies(), + [BoundElectrons], + ) + + def test_constant_dependencies(self): + """correct return""" + self.assertEqual( + GroundStateIonization(ionization_model_list=[self.BSI_instance]).get_constant_dependencies(), + [ElementProperties], + ) + + def test_rendering(self): + """rendering may be called and returns correct context""" + # complete configuration of electron species + electron = self.BSI_instance.ionization_electron_species + electron.attributes = [Position(), Momentum()] + + context = GroundStateIonization(ionization_model_list=[self.BSI_instance]).get_rendering_context() + + expected_context = { + "ionization_model_list": [ + { + "ionizer_picongpu_name": "BSI", + "ionization_electron_species": { + "name": "e", + "typename": "species_e", + "attributes": [ + {"picongpu_name": "position"}, + {"picongpu_name": "momentum"}, + ], + "constants": { + "mass": {"mass_si": constants.m_e}, + "charge": {"charge_si": constants.m_e}, + "density_ratio": None, + "element_properties": None, + "ground_state_ionization": None, + }, + }, + "ionization_current": {"picongpu_name": "None"}, + } + ] + } + + self.assertEqual(context, expected_context) diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/~groundstateionization.py b/test/python/picongpu/quick/pypicongpu/species/constant/~groundstateionization.py deleted file mode 100644 index ed98e56972..0000000000 --- a/test/python/picongpu/quick/pypicongpu/species/constant/~groundstateionization.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -This file is part of PIConGPU. -Copyright 2021-2023 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre -License: GPLv3+ -""" - - -from picongpu.pypicongpu.species import Species -from picongpu.pypicongpu.species.constant import Mass, Charge -from picongpu.picmi import constants - - -class TestGroundStateIonization: - def setUp(self): - electron = Species() - electron.name = "e" - mass_constant = Mass() - mass_constant.mass_si = constants.m_e - charge_constant = Charge() - charge_constant.charge_si = constants.m_e - electron.constants = [ - charge_constant, - mass_constant, - ] - # note: attributes not set yet (as would be in init manager) - - self.electron = electron From 8b3e737bba1dfe039cfce8c5df3f3ded854ee54c Mon Sep 17 00:00:00 2001 From: BrianMarre Date: Tue, 16 Jul 2024 13:56:38 +0200 Subject: [PATCH 06/20] refactor PyPIConGPU species tests --- .../species/constant/groundstateionization.py | 9 +++++--- .../quick/pypicongpu/species/species.py | 23 ++++++++++++++----- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/groundstateionization.py b/test/python/picongpu/quick/pypicongpu/species/constant/groundstateionization.py index 204943ee33..01861e0230 100644 --- a/test/python/picongpu/quick/pypicongpu/species/constant/groundstateionization.py +++ b/test/python/picongpu/quick/pypicongpu/species/constant/groundstateionization.py @@ -9,7 +9,7 @@ from picongpu.pypicongpu.species import Species from picongpu.pypicongpu.species.attribute import Position, Momentum, BoundElectrons from picongpu.pypicongpu.species.constant import Mass, Charge, GroundStateIonization, ElementProperties -from picongpu.pypicongpu.species.constant.ionizationmodel import BSI, BSIStarkShifted +from picongpu.pypicongpu.species.constant.ionizationmodel import BSI, BSIStarkShifted, ThomasFermi from picongpu.pypicongpu.species.constant.ionizationcurrent import None_ from picongpu.picmi import constants @@ -36,7 +36,8 @@ def setUp(self): self.electron = electron self.BSI_instance = BSI(ionization_electron_species=self.electron, ionization_current=None_()) - self.BSIStark_instance = BSIStarkShifted(ionization_electron_species=self.electron, ionization_current=None_()) + self.BSIstark_instance = BSIStarkShifted(ionization_electron_species=self.electron, ionization_current=None_()) + self.thomas_fermi_instance = ThomasFermi(ionization_electron_species=self.electron) def test_basic(self): """we may create basic Instance""" @@ -66,7 +67,9 @@ def test_check_doubled_up_model_group(self): """may not assign more than one ionization model from the same group""" # assignment is possible - instance = GroundStateIonization(ionization_model_list=[self.BSI_instance, self.BSIStark_instance]) + instance = GroundStateIonization( + ionization_model_list=[self.BSI_instance, self.BSIstark_instance, self.thomas_fermi_instance] + ) with self.assertRaisesRegex(ValueError, ".*ionization model group already represented: BSI.*"): # but check throws diff --git a/test/python/picongpu/quick/pypicongpu/species/species.py b/test/python/picongpu/quick/pypicongpu/species/species.py index 1951373bc6..37b8bf19be 100644 --- a/test/python/picongpu/quick/pypicongpu/species/species.py +++ b/test/python/picongpu/quick/pypicongpu/species/species.py @@ -1,4 +1,4 @@ -""" +"""sshfs fwk394:/home/marre55 ~/mnt/fwk394 This file is part of PIConGPU. Copyright 2021-2023 PIConGPU contributors Authors: Hannes Troepgen, Brian Edward Marre @@ -8,7 +8,16 @@ from picongpu.pypicongpu.species import Species from picongpu.pypicongpu.species.attribute import Position, Weighting, Momentum -from picongpu.pypicongpu.species.constant import Mass, Charge, DensityRatio, ElementProperties, Constant +from picongpu.pypicongpu.species.constant import ( + Mass, + Charge, + DensityRatio, + ElementProperties, + Constant, + GroundStateIonization, +) +from picongpu.pypicongpu.species.constant.ionizationcurrent import None_ +from picongpu.pypicongpu.species.constant.ionizationmodel import BSI from picongpu.pypicongpu.species.util import Element from .attribute import DummyAttribute @@ -39,8 +48,9 @@ def setUp(self): self.const_mass.mass_si = 2 self.const_density_ratio = DensityRatio() self.const_density_ratio.ratio = 4.2 - self.const_ionizers = None - self.const_ionizers.electron_species = self.electron + self.const_ground_state_ionization = GroundStateIonization( + ionization_model_list=[BSI(ionization_electron_species=self.electron, ionization_current=None_())] + ) self.const_element_properties = ElementProperties() self.const_element_properties.element = Element.H @@ -128,7 +138,8 @@ def test_constants_unique(self): const1.charge_si = 17 const2 = Charge() const2.charge_si = 18 - other_const = Constant() + other_const = Mass() + other_const.mass_si = 19 species.constants = [const1, const2, other_const] @@ -268,7 +279,7 @@ def test_rendering_constants(self): "density_ratio": self.const_density_ratio, "charge": self.const_charge, "mass": self.const_mass, - "ionizers": self.const_ionizers, + "ground_state_ionization": self.const_ground_state_ionization, "element_properties": self.const_element_properties, } From 8fe67d37c06ddb604f34636116cd5f106f6c2fd8 Mon Sep 17 00:00:00 2001 From: BrianMarre Date: Tue, 16 Jul 2024 16:25:19 +0200 Subject: [PATCH 07/20] add PICMI ioniaztion configuration interface --- .../picongpu/picmi/interaction/__init__.py | 4 + .../picongpu/picmi/interaction/interaction.py | 30 +++ .../picmi/interaction/ionization/__init__.py | 6 + .../__init__.py | 3 + .../thomasfermi.py | 14 ++ .../ionization/fieldionization/ADK.py | 24 +++ .../ionization/fieldionization/BSI.py | 26 +++ .../ionization/fieldionization/__init__.py | 7 + .../fieldionization/fieldionization.py | 20 ++ .../ionizationcurrent/__init__.py | 3 + .../ionizationcurrent/ionizationcurrent.py | 14 ++ .../ionization/fieldionization/keldysh.py | 14 ++ .../interaction/ionization/ionizationmodel.py | 27 +++ lib/python/picongpu/picmi/simulation.py | 200 +++++++++++------- .../picongpu/pypicongpu/requirements.txt | 3 +- .../ionizationmodel/ionizationmodelgroups.py | 2 +- .../ionizationmodel/ionizationmodelgroups.py | 2 +- 17 files changed, 317 insertions(+), 82 deletions(-) create mode 100644 lib/python/picongpu/picmi/interaction/__init__.py create mode 100644 lib/python/picongpu/picmi/interaction/interaction.py create mode 100644 lib/python/picongpu/picmi/interaction/ionization/__init__.py create mode 100644 lib/python/picongpu/picmi/interaction/ionization/electroniccollisonalequilibrium/__init__.py create mode 100644 lib/python/picongpu/picmi/interaction/ionization/electroniccollisonalequilibrium/thomasfermi.py create mode 100644 lib/python/picongpu/picmi/interaction/ionization/fieldionization/ADK.py create mode 100644 lib/python/picongpu/picmi/interaction/ionization/fieldionization/BSI.py create mode 100644 lib/python/picongpu/picmi/interaction/ionization/fieldionization/__init__.py create mode 100644 lib/python/picongpu/picmi/interaction/ionization/fieldionization/fieldionization.py create mode 100644 lib/python/picongpu/picmi/interaction/ionization/fieldionization/ionizationcurrent/__init__.py create mode 100644 lib/python/picongpu/picmi/interaction/ionization/fieldionization/ionizationcurrent/ionizationcurrent.py create mode 100644 lib/python/picongpu/picmi/interaction/ionization/fieldionization/keldysh.py create mode 100644 lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py diff --git a/lib/python/picongpu/picmi/interaction/__init__.py b/lib/python/picongpu/picmi/interaction/__init__.py new file mode 100644 index 0000000000..69896a4ddf --- /dev/null +++ b/lib/python/picongpu/picmi/interaction/__init__.py @@ -0,0 +1,4 @@ +from .interaction import Interaction +from . import ionization + +__all__ = ["Interaction", "ionization"] diff --git a/lib/python/picongpu/picmi/interaction/interaction.py b/lib/python/picongpu/picmi/interaction/interaction.py new file mode 100644 index 0000000000..fe52c707eb --- /dev/null +++ b/lib/python/picongpu/picmi/interaction/interaction.py @@ -0,0 +1,30 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +import pydantic +from .ionization import IonizationModel + + +class Interaction(pydantic.BaseModel): + """ + Common interface of Particle-In-Cell particle interaction extensions + + e.g. collisions, ionization, nuclear reactions + + This interface is only a semantic interface for typing interactions for storage in the simulation object. + It does not specify interface requirements for sub classes, since they differ too much. + """ + + Ionization: list[IonizationModel] + """ + list of all interaction models that change the charge state of ions + + e.g. field ionization, collisional ionization, ... + + """ + + # @todo add Collisions as elastic interaction model, Brian Marre, 2024 diff --git a/lib/python/picongpu/picmi/interaction/ionization/__init__.py b/lib/python/picongpu/picmi/interaction/ionization/__init__.py new file mode 100644 index 0000000000..a86996543f --- /dev/null +++ b/lib/python/picongpu/picmi/interaction/ionization/__init__.py @@ -0,0 +1,6 @@ +from .ionizationmodel import IonizationModel +from . import ionizationcurrent +from . import fieldionization +from . import electroniccollisionalequilibrium + +__all__ = ["IonizationModel", "ionizationcurrent", "fieldionization", "electroniccollisionalequilibrium"] diff --git a/lib/python/picongpu/picmi/interaction/ionization/electroniccollisonalequilibrium/__init__.py b/lib/python/picongpu/picmi/interaction/ionization/electroniccollisonalequilibrium/__init__.py new file mode 100644 index 0000000000..a8bb084044 --- /dev/null +++ b/lib/python/picongpu/picmi/interaction/ionization/electroniccollisonalequilibrium/__init__.py @@ -0,0 +1,3 @@ +from .thomasfermi import ThomasFermi + +__all__ = ["ThomasFermi"] diff --git a/lib/python/picongpu/picmi/interaction/ionization/electroniccollisonalequilibrium/thomasfermi.py b/lib/python/picongpu/picmi/interaction/ionization/electroniccollisonalequilibrium/thomasfermi.py new file mode 100644 index 0000000000..288cf256e9 --- /dev/null +++ b/lib/python/picongpu/picmi/interaction/ionization/electroniccollisonalequilibrium/thomasfermi.py @@ -0,0 +1,14 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from ..ionizationmodel import IonizationModel + + +class ThomasFermi(IonizationModel): + """thomas fermi ionization model""" + + MODEL_NAME: str = "ThomasFermi" diff --git a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ADK.py b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ADK.py new file mode 100644 index 0000000000..dd4fcd1434 --- /dev/null +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ADK.py @@ -0,0 +1,24 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from .fieldionization import FieldIonization + +import enum + + +class ADKVariant(enum.Enum): + LinearPolarization = 0 + CircularPolarization = 1 + + +class ADK(FieldIonization): + """Barrier Suppression Ioniztion model""" + + MODEL_NAME: str = "ADK" + + ADK_variant: ADKVariant + """extension to the BSI model""" diff --git a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/BSI.py b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/BSI.py new file mode 100644 index 0000000000..fc886f7c7c --- /dev/null +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/BSI.py @@ -0,0 +1,26 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from .fieldionization import FieldIonization + +import enum + + +class BSIExtension(enum.Enum): + StarkShift = 0 + EffectiveZ = 1 + # consider_excitation = 2 + # add additional features here + + +class BSI(FieldIonization): + """Barrier Suppression Ioniztion model""" + + MODEL_NAME: str = "BSI" + + BIS_extensions: list[BSIExtension] + """extension to the BSI model""" diff --git a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/__init__.py b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/__init__.py new file mode 100644 index 0000000000..9bc5c73be5 --- /dev/null +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/__init__.py @@ -0,0 +1,7 @@ +from .fieldionization import FieldIonization +from .keldysh import Keldysh +from .ADK import ADK, ADKVariant +from .BSI import BSI, BSIExtension +from . import ionizationcurrent + +__all__ = ["FieldIonization", "Keldysh", "ADK", "ADKVariant", "BSI", "BSIExtension", "ionizationcurrent"] diff --git a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/fieldionization.py b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/fieldionization.py new file mode 100644 index 0000000000..4db3a3786a --- /dev/null +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/fieldionization.py @@ -0,0 +1,20 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from .ionizationmodel import IonizationModel +from .ionizationcurrent import IonizationCurrent + +import typing + + +class FieldIonization(IonizationModel): + """common interface of all field ionization models""" + + ionization_current: typing.Optional[IonizationCurrent] + """ionization current for energy conservation of field ionization""" + + # + all IonizationModel interface requirements diff --git a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ionizationcurrent/__init__.py b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ionizationcurrent/__init__.py new file mode 100644 index 0000000000..c48e658e91 --- /dev/null +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ionizationcurrent/__init__.py @@ -0,0 +1,3 @@ +from .ionizationcurrent import IonizationCurrent + +__all__ = ["IonizationCurrent"] diff --git a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ionizationcurrent/ionizationcurrent.py b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ionizationcurrent/ionizationcurrent.py new file mode 100644 index 0000000000..11a1da1746 --- /dev/null +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ionizationcurrent/ionizationcurrent.py @@ -0,0 +1,14 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +import pydantic + + +class IonizationCurrent(pydantic.BaseModel): + """common interface of all ionization current models""" + + MODEL_NAME: str diff --git a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/keldysh.py b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/keldysh.py new file mode 100644 index 0000000000..e477f4b5ee --- /dev/null +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/keldysh.py @@ -0,0 +1,14 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from .fieldionization import FieldIonization + + +class Keldysh(FieldIonization): + """Barrier Suppression Ioniztion model""" + + MODEL_NAME: str = "Keldysh" diff --git a/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py b/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py new file mode 100644 index 0000000000..7983fad46c --- /dev/null +++ b/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py @@ -0,0 +1,27 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from ...species import Species + +import pydantic + + +class IonizationModel(pydantic.BaseModel): + """ + common interface for all ionization models + + @note further configurations may be added by implementations + """ + + MODEL_NAME: str + """ionization model""" + + ion_species: Species + """ion species to apply ionization model for""" + + ionization_electron_species: Species + """electron species of which to create macro particle upon ionization""" diff --git a/lib/python/picongpu/picmi/simulation.py b/lib/python/picongpu/picmi/simulation.py index b964b4bca0..788d0411b2 100644 --- a/lib/python/picongpu/picmi/simulation.py +++ b/lib/python/picongpu/picmi/simulation.py @@ -5,10 +5,15 @@ License: GPLv3+ """ -from ..pypicongpu import simulation, runner, util, species, movingwindow, customuserinput +# make pypicongpu classes accessible for conversion to pypicongpu +from .. import pypicongpu + +from ..pypicongpu import util + from . import constants from .grid import Cartesian3DGrid from .species import Species as PicongpuPicmiSpecies +from .interaction import Interaction import picmistandard @@ -19,6 +24,7 @@ import typing +# no inheritance from pydantic.BaseModel since PICMI does not declare attributes @typeguard.typechecked class Simulation(picmistandard.PICMI_Simulation): """ @@ -28,11 +34,85 @@ class Simulation(picmistandard.PICMI_Simulation): https://picmi-standard.github.io/standard/simulation.html """ - __picongpu_custom_input = util.build_typesafe_property( - typing.Optional[list[customuserinput.InterfaceCustomUserInput]] + picongpu_custom_user_input = util.build_typesafe_property( + typing.Optional[list[pypicongpu.customuserinput.InterfaceCustomUserInput]] ) """list of custom user input objects""" + picongpu_interaction = util.build_typesafe_property(typing.Optional[Interaction]) + """Interaction instance containing all particle interactions of the simulation, set to None to have no interactions""" + + picongpu_typical_ppc = util.build_typesafe_property(typing.Optional[int]) + """ + typical number of particle in a cell in the simulation + + used for normalization of code units + + optional, if set to None, will be set to median ppc of all species ppcs + """ + + picongpu_template_dir = util.build_typesafe_property(str) + """directory containing templates to use for generating picongpu setups""" + + picongpu_moving_window_move_point = util.build_typesafe_property(typing.Optional[float]) + """ + point a light ray reaches in y from the left border until we begin sliding the simulation window with the speed of + light + + in multiples of the simulation window size + + @attention if moving window is active, one gpu in y direction is reserved for initializing new spaces, + thereby reducing the simulation window size accordingrelative spot at which to start moving the simulation window + """ + + picongpu_moving_window_stop_iteration = util.build_typesafe_property(typing.Optional[int]) + """iteration, at which to stop moving the simulation window""" + + __runner = util.build_typesafe_property(typing.Optional[pypicongpu.runner.Runner]) + __electron_species = util.build_typesafe_property(typing.Optional[pypicongpu.species.Species]) + + # @todo remove boiler plate constructor once we switch to pydantic on the PICMI side, Brian Marre, 2024 + def __init__( + self, + picongpu_template_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, + picongpu_typical_ppc: typing.Optional[int] = None, + picongpu_moving_window_move_point: typing.Optional[float] = None, + picongpu_moving_window_stop_iteration: typing.Optional[int] = None, + **kw, + ): + # pass everything not picongpu sepcific to the PICMI interface + super().__init__(**kw) + + # additional checks on inputs, @todo move to picmistandard, Brian Marre, 2024 + ## throw if both cfl & delta_t are set + if self.solver is not None and "Yee" == self.solver.method and isinstance(self.solver.grid, Cartesian3DGrid): + self.__yee_compute_cfl_or_delta_t() + + # checks on picongpu specific stuff + ## template_path is valid + if picongpu_template_dir == "": + raise ValueError("picongpu_template_dir MUST NOT be empty string") + template_path = pathlib.Path(picongpu_template_dir) + if template_path.is_dir(): + raise ValueError("picongpu_template_dir must be existing directory") + + # store picongpu specific stuff + # @todo switch to pydantic for automatic instrumentation of init method, Brian Marre 2024 + 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 + + if picongpu_template_dir is not None: + self.picongpu_template_dir = picongpu_template_dir + else: + self.picongpu_template_dir = None + + # internal stuff for PICMI interface only + # initialized with None, updated by picongpu_add_custom_user_input() calls + self.picongpu_custom_user_input = None + self.__runner = None + self.__electron_species = None + def __yee_compute_cfl_or_delta_t(self) -> None: """ use delta_t or cfl to compute the other @@ -100,56 +180,10 @@ def __yee_compute_cfl_or_delta_t(self) -> None: # if neither delta_t nor cfl are given simply silently pass # (might change in the future) - def __init__( - self, - picongpu_template_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, - picongpu_typical_ppc: typing.Optional[int] = None, - picongpu_moving_window_move_point: typing.Optional[float] = None, - picongpu_moving_window_stop_iteration: typing.Optional[int] = None, - **kw, - ): - # delegate additional work to parent - super().__init__(**kw) - - # additional checks on inputs, @todo move to picmistandard, Brian Marre, 2024 - - ## throw if both cfl & delta_t are set - if self.solver is not None and "Yee" == self.solver.method and isinstance(self.solver.grid, Cartesian3DGrid): - self.__yee_compute_cfl_or_delta_t() - - # store picongpu specific stuff - # @todo switch to pydantic for automatic instrumentation of init method, Brian Marre 2024 - self.picongpu_typical_ppc = picongpu_typical_ppc - - # internal stuff for PICMI interface only - self.__runner = None - self.__electron_species = None - self.__picongpu_custom_input = None - - # set PyPIConGPU template directory - if picongpu_template_dir is None: - self.picongpu_template_dir = None - else: - assert "" != picongpu_template_dir, "picongpu_template_dir MUST NOT be empty" - # note: pathlib.Path(pathlib.Path(...)) is valid - template_path = pathlib.Path(picongpu_template_dir) - assert template_path.is_dir(), "picongpu_template_dir must be existing dir" - self.picongpu_template_dir = str(template_path) - - self.moving_window_move_point = picongpu_moving_window_move_point - self.moving_window_stop_iteration = picongpu_moving_window_stop_iteration - - self.picongpu_typical_ppc = picongpu_typical_ppc - - # store runner state - self.__runner = None - - self.__electron_species = None - def __get_operations_simple_density( self, - pypicongpu_by_picmi_species: typing.Dict[picmistandard.PICMI_Species, species.Species], - ) -> typing.List[species.operation.SimpleDensity]: + pypicongpu_by_picmi_species: typing.Dict[picmistandard.PICMI_Species, pypicongpu.species.Species], + ) -> typing.List[pypicongpu.species.operation.SimpleDensity]: """ retrieve operations for simple density placements @@ -186,7 +220,7 @@ def __get_operations_simple_density( for profile, picmi_species_list in picmi_species_by_profile.items(): assert isinstance(layout, picmistandard.PICMI_PseudoRandomLayout) - op = species.operation.SimpleDensity() + op = pypicongpu.species.operation.SimpleDensity() op.ppc = layout.n_macroparticles_per_cell op.profile = profile.get_as_pypicongpu() @@ -203,8 +237,8 @@ def __get_operations_simple_density( def __get_operations_not_placed( self, - pypicongpu_by_picmi_species: typing.Dict[picmistandard.PICMI_Species, species.Species], - ) -> typing.List[species.operation.NotPlaced]: + pypicongpu_by_picmi_species: typing.Dict[picmistandard.PICMI_Species, pypicongpu.species.Species], + ) -> typing.List[pypicongpu.species.operation.NotPlaced]: """ retrieve operations for not placed species @@ -225,7 +259,7 @@ def __get_operations_not_placed( continue # is not placed -> add op - not_placed = species.operation.NotPlaced() + not_placed = pypicongpu.species.operation.NotPlaced() not_placed.species = pypicongpu_by_picmi_species[picmi_species] all_operations.append(not_placed) @@ -233,8 +267,8 @@ def __get_operations_not_placed( def __get_operations_from_individual_species( self, - pypicongpu_by_picmi_species: typing.Dict[picmistandard.PICMI_Species, species.Species], - ) -> typing.List[species.operation.Operation]: + pypicongpu_by_picmi_species: typing.Dict[picmistandard.PICMI_Species, pypicongpu.species.Species], + ) -> typing.List[pypicongpu.species.operation.Operation]: """ call get_independent_operations() of all species @@ -251,7 +285,7 @@ def __get_operations_from_individual_species( def __fill_ionization_electrons( self, - pypicongpu_by_picmi_species: typing.Dict[picmistandard.PICMI_Species, species.Species], + pypicongpu_by_picmi_species: typing.Dict[picmistandard.PICMI_Species, pypicongpu.species.Species], ) -> None: """ copy used-electron-relationship from PICMI to PIConGPU species @@ -272,7 +306,7 @@ def __fill_ionization_electrons( for picmi_species, pypic_species in pypicongpu_by_picmi_species.items(): # only fill ionization electrons if required (by ionizers) - if not pypic_species.has_constant_of_type(species.constant.GroundStateIonization): + if not pypic_species.has_constant_of_type(pypicongpu.species.constant.GroundStateIonization): continue assert picmi_species.picongpu_ionization_electrons in pypicongpu_by_picmi_species, ( @@ -283,14 +317,14 @@ def __fill_ionization_electrons( ) ) - ionizer_model_list = pypic_species.get_constant_by_type(species.constant.GroundStateIonization) + ionizer_model_list = pypic_species.get_constant_by_type(pypicongpu.species.constant.GroundStateIonization) # is pointer -> sets correct species for actual pypicongpu species for model in ionizer_model_list: model.ionization_electron_species = pypicongpu_by_picmi_species[ picmi_species.picongpu_ionization_electrons ] - def __get_init_manager(self) -> species.InitManager: + def __get_init_manager(self) -> pypicongpu.species.InitManager: """ create & fill an initmanager @@ -302,7 +336,7 @@ def __get_init_manager(self) -> species.InitManager: 3. generate operations which have inter-species dependencies 4. generate operations without inter-species dependencies """ - initmgr = species.InitManager() + initmgr = pypicongpu.species.InitManager() # check preconditions assert len(self.species) == len(self.layouts) @@ -443,7 +477,9 @@ def __resolve_electrons(self) -> None: picmi_species.picongpu_ionization_electrons = self.__get_electron_species() - def write_input_file(self, file_name: str, pypicongpu_simulation: simulation.Simulation | None = None) -> None: + def write_input_file( + self, file_name: str, pypicongpu_simulation: typing.Optional[pypicongpu.simulation.Simulation] = None + ) -> None: """ generate input data set for picongpu @@ -459,9 +495,20 @@ def write_input_file(self, file_name: str, pypicongpu_simulation: simulation.Sim if pypicongpu_simulation is None: pypicongpu_simulation = self.get_as_pypicongpu() - self.__runner = runner.Runner(pypicongpu_simulation, self.picongpu_template_dir, setup_dir=file_name) + self.__runner = pypicongpu.runnerRunner(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): + if self.picongpu_custom_user_input is None: + self.picongpu_custom_user_input = [custom_user_input] + else: + self.picongpu_custom_user_input.append(custom_user_input) + + def add_interaction(self, interaction) -> None: + util.unsupported( + "PICMI standard interactions are not supported by PIConGPU, assign an Interaction object to the picongpu_interaction attribute of the simulation instead." + ) + def step(self, nsteps: int = 1): if nsteps != self.max_steps: raise ValueError( @@ -469,15 +516,15 @@ def step(self, nsteps: int = 1): ) self.picongpu_run() - def get_as_pypicongpu(self) -> simulation.Simulation: + def get_as_pypicongpu(self) -> pypicongpu.simulation.Simulation: """translate to PyPIConGPU object""" - s = simulation.Simulation() + s = pypicongpu.simulation.Simulation() s.delta_t_si = self.time_step_size s.solver = self.solver.get_as_pypicongpu() # already in pypicongpu objects - s.custom_user_input = self.__picongpu_custom_input + s.custom_user_input = self.picongpu_custom_user_input # calculate time step if self.max_steps is not None: @@ -526,30 +573,25 @@ def get_as_pypicongpu(self) -> simulation.Simulation: raise ValueError("typical_ppc must be >= 1") # disable moving Window if explicitly activated by the user - if self.moving_window_move_point is None: + if self.picongpu_moving_window_move_point is None: s.moving_window = None else: - s.moving_window = movingwindow.MovingWindow( - move_point=self.moving_window_move_point, stop_iteration=self.moving_window_stop_iteration + s.moving_window = pypicongpu.movingwindow.MovingWindow( + move_point=self.picongpu_moving_window_move_point, + stop_iteration=self.picongpu_moving_window_stop_iteration, ) return s - def picongpu_add_custom_user_input(self, custom_user_input: customuserinput.InterfaceCustomUserInput): - if self.__picongpu_custom_input is None: - self.__picongpu_custom_input = [custom_user_input] - else: - self.__picongpu_custom_input.append(custom_user_input) - def picongpu_run(self) -> None: """build and run PIConGPU simulation""" if self.__runner is None: - self.__runner = runner.Runner(self.get_as_pypicongpu(), self.picongpu_template_dir) + self.__runner = pypicongpu.runnerRunner(self.get_as_pypicongpu(), self.picongpu_template_dir) self.__runner.generate() self.__runner.build() self.__runner.run() - def picongpu_get_runner(self) -> runner.Runner: + def picongpu_get_runner(self) -> pypicongpu.runnerRunner: if self.__runner is None: - self.__runner = runner.Runner(self.get_as_pypicongpu(), self.picongpu_template_dir) + self.__runner = pypicongpu.runnerRunner(self.get_as_pypicongpu(), self.picongpu_template_dir) return self.__runner diff --git a/lib/python/picongpu/pypicongpu/requirements.txt b/lib/python/picongpu/pypicongpu/requirements.txt index b6799b535e..2fcaef23b8 100644 --- a/lib/python/picongpu/pypicongpu/requirements.txt +++ b/lib/python/picongpu/pypicongpu/requirements.txt @@ -1,4 +1,5 @@ chevron >= 0.13.1 jsonschema >= 4.23.0 typeguard >= 4.2.1 -referencing >=0.35.1 +referencing >= 0.35.1 +pydantic >= 2.6.4 diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodelgroups.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodelgroups.py index 6789d41720..b68f3b678b 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodelgroups.py +++ b/lib/python/picongpu/pypicongpu/species/constant/ionizationmodel/ionizationmodelgroups.py @@ -30,7 +30,7 @@ class IonizationModelGroups(pydantic.BaseModel): "BSI_like": [BSI, BSIEffectiveZ, BSIStarkShifted], "ADK_like": [ADKLinearPolarization, ADKCircularPolarization], "Keldysh_like": [Keldysh], - "electronic_collisional": [ThomasFermi], + "electronic_collisional_equilibrium": [ThomasFermi], } def get_by_group(self) -> dict[str, list[typing.Type[IonizationModel]]]: diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelgroups.py b/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelgroups.py index 1bc14fd3a4..0e99b92e4f 100644 --- a/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelgroups.py +++ b/test/python/picongpu/quick/pypicongpu/species/constant/ionizationmodel/ionizationmodelgroups.py @@ -26,7 +26,7 @@ def setUp(self): "BSI_like": [BSI, BSIEffectiveZ, BSIStarkShifted], "ADK_like": [ADKLinearPolarization, ADKCircularPolarization], "Keldysh_like": [Keldysh], - "electronic_collisional": [ThomasFermi], + "electronic_collisional_equilibrium": [ThomasFermi], } self.expected_by_model_custom = { From e8c70d81a3e6c37249d989e50d1bc74adb85680c Mon Sep 17 00:00:00 2001 From: BrianMarre Date: Tue, 23 Jul 2024 15:30:16 +0200 Subject: [PATCH 08/20] switch to pydantic validation for PICMI simulation class --- lib/python/picongpu/picmi/simulation.py | 82 ++++++++++++++----------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/lib/python/picongpu/picmi/simulation.py b/lib/python/picongpu/picmi/simulation.py index 788d0411b2..87cf8653d5 100644 --- a/lib/python/picongpu/picmi/simulation.py +++ b/lib/python/picongpu/picmi/simulation.py @@ -18,15 +18,13 @@ import picmistandard import math -import typeguard +import pydantic import pathlib import logging import typing -# no inheritance from pydantic.BaseModel since PICMI does not declare attributes -@typeguard.typechecked -class Simulation(picmistandard.PICMI_Simulation): +class Simulation(picmistandard.PICMI_Simulation, pydantic.BaseModel): """ Simulation as defined by PICMI @@ -34,15 +32,26 @@ class Simulation(picmistandard.PICMI_Simulation): https://picmi-standard.github.io/standard/simulation.html """ - picongpu_custom_user_input = util.build_typesafe_property( - typing.Optional[list[pypicongpu.customuserinput.InterfaceCustomUserInput]] - ) - """list of custom user input objects""" + model_config = pydantic.ConfigDict(extra="allow") + """ + set to allow and store additional attributes outside pydantic model validation. + + This ensure that the PICMI defined attributes are also stored in the pydantic instances. + + @attention needs to be the first entry, other wise ignored for some reason + """ + + picongpu_custom_user_input: typing.Optional[list[pypicongpu.customuserinput.InterfaceCustomUserInput]] = None + """ + list of custom user input objects + + update using picongpu_add_custom_user_input() or by direct setting + """ - picongpu_interaction = util.build_typesafe_property(typing.Optional[Interaction]) + picongpu_interaction: typing.Optional[Interaction] """Interaction instance containing all particle interactions of the simulation, set to None to have no interactions""" - picongpu_typical_ppc = util.build_typesafe_property(typing.Optional[int]) + picongpu_typical_ppc: typing.Optional[int] """ typical number of particle in a cell in the simulation @@ -51,10 +60,10 @@ class Simulation(picmistandard.PICMI_Simulation): optional, if set to None, will be set to median ppc of all species ppcs """ - picongpu_template_dir = util.build_typesafe_property(str) + picongpu_template_dir: str """directory containing templates to use for generating picongpu setups""" - picongpu_moving_window_move_point = util.build_typesafe_property(typing.Optional[float]) + picongpu_moving_window_move_point: typing.Optional[float] """ point a light ray reaches in y from the left border until we begin sliding the simulation window with the speed of light @@ -65,25 +74,41 @@ class Simulation(picmistandard.PICMI_Simulation): thereby reducing the simulation window size accordingrelative spot at which to start moving the simulation window """ - picongpu_moving_window_stop_iteration = util.build_typesafe_property(typing.Optional[int]) + picongpu_moving_window_stop_iteration: typing.Optional[int] """iteration, at which to stop moving the simulation window""" - __runner = util.build_typesafe_property(typing.Optional[pypicongpu.runner.Runner]) - __electron_species = util.build_typesafe_property(typing.Optional[pypicongpu.species.Species]) + __runner: typing.Optional[pypicongpu.runner.Runner] = None + __electron_species: typing.Optional[pypicongpu.species.Species] = None - # @todo remove boiler plate constructor once we switch to pydantic on the PICMI side, Brian Marre, 2024 + # @todo remove boiler plate constructor argument list once PICMI switches to pydantic, Brian Marre, 2024 def __init__( self, picongpu_template_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, picongpu_typical_ppc: typing.Optional[int] = None, picongpu_moving_window_move_point: typing.Optional[float] = None, picongpu_moving_window_stop_iteration: typing.Optional[int] = None, - **kw, + picongpu_interaction: typing.Optional[Interaction] = None, + **keyword_arguments, ): - # pass everything not picongpu sepcific to the PICMI interface - super().__init__(**kw) + # 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 - # additional checks on inputs, @todo move to picmistandard, Brian Marre, 2024 + # pass pydantic data + picongpu_data = {} + picongpu_data["picongpu_template_dir"] = picongpu_template_dir + picongpu_data["picongpu_typical_ppc"] = picongpu_typical_ppc + picongpu_data["picongpu_moving_window_move_point"] = picongpu_moving_window_move_point + picongpu_data["picongpu_moving_window_stop_iteration"] = picongpu_moving_window_stop_iteration + picongpu_data["picongpu_interaction"] = picongpu_interaction + + ### pydantic.BaseModel init call + pydantic.BaseModel.__init__(self, **picongpu_data) + + # 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 ## throw if both cfl & delta_t are set if self.solver is not None and "Yee" == self.solver.method and isinstance(self.solver.grid, Cartesian3DGrid): self.__yee_compute_cfl_or_delta_t() @@ -96,23 +121,6 @@ def __init__( if template_path.is_dir(): raise ValueError("picongpu_template_dir must be existing directory") - # store picongpu specific stuff - # @todo switch to pydantic for automatic instrumentation of init method, Brian Marre 2024 - 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 - - if picongpu_template_dir is not None: - self.picongpu_template_dir = picongpu_template_dir - else: - self.picongpu_template_dir = None - - # internal stuff for PICMI interface only - # initialized with None, updated by picongpu_add_custom_user_input() calls - self.picongpu_custom_user_input = None - self.__runner = None - self.__electron_species = None - def __yee_compute_cfl_or_delta_t(self) -> None: """ use delta_t or cfl to compute the other From 874dca4ab23abb4258b0f2ecb9c9b0bc4460132f Mon Sep 17 00:00:00 2001 From: BrianMarre Date: Tue, 23 Jul 2024 17:19:45 +0200 Subject: [PATCH 09/20] switch to periodic table library for getting ion properties in PICMI --- .../picongpu/pypicongpu/requirements.txt | 1 + .../pypicongpu/species/util/element.py | 67 ++++++++----------- 2 files changed, 28 insertions(+), 40 deletions(-) diff --git a/lib/python/picongpu/pypicongpu/requirements.txt b/lib/python/picongpu/pypicongpu/requirements.txt index 2fcaef23b8..d57ed81fa1 100644 --- a/lib/python/picongpu/pypicongpu/requirements.txt +++ b/lib/python/picongpu/pypicongpu/requirements.txt @@ -3,3 +3,4 @@ jsonschema >= 4.23.0 typeguard >= 4.2.1 referencing >= 0.35.1 pydantic >= 2.6.4 +periodictable diff --git a/lib/python/picongpu/pypicongpu/species/util/element.py b/lib/python/picongpu/pypicongpu/species/util/element.py index 881a42246e..d6b9d93728 100644 --- a/lib/python/picongpu/pypicongpu/species/util/element.py +++ b/lib/python/picongpu/pypicongpu/species/util/element.py @@ -6,53 +6,46 @@ """ from ...rendering import RenderedObject +from ... import util -import typeguard -import enum import scipy +import periodictable -@typeguard.typechecked -class Element(RenderedObject, enum.Enum): +class Element(RenderedObject): """ Denotes an element from the periodic table of elements Used to provide fundamental constants for elements, and to map them in a type-safe way to PIConGPU. - The number associated is the number of protons. - Note: Spelling follows periodic table, e.g. "Na", "C", "He" + The number associated is just an id. + Note: Spelling follows periodic table, e.g. "Na", "C", "He" + typical nuclear variations Note that these denote Elements, but when initialized in a species *only* - represent the core, i.e. there are no electrons. To make an atom also - initialize an appropriate ionization. + describe the core, i.e. without electrons. + To describe atoms/ions you also need to initialize the charge_state of the species. """ - H = 1 - """hydrogen""" - He = 2 - """helium""" - N = 7 - """nitrogen""" + store = util.build_typesafe_propety(periodictable.Element) - @staticmethod - def get_by_openpmd_name(openpmd_name: str) -> "Element": + def __init__(self, openpmd_name: str) -> None: """ get the correct substance implementation from a openPMD type name - Names are (case-sensitive) element symbols (e.g. "H", "He", "N"). + @param openpmd_name (case-sensitive) chemical/nuclear element symbols (e.g. "H", "D", "He", "N"). - :param openpmd_name: single species following openPMD species extension :return: object representing the given species """ - element_by_openpmd_name = { - "H": Element.H, - "He": Element.He, - "N": Element.N, - } - if openpmd_name not in element_by_openpmd_name: - raise NameError("unkown element: {}".format(openpmd_name)) - return element_by_openpmd_name[openpmd_name] + + # search for name in periodic table + for element in periodictable.elements: + if openpmd_name == element.symbol: + self.store = element + return + + # not found + raise NameError("unkown element: {}".format(openpmd_name)) def get_picongpu_name(self) -> str: """ @@ -60,12 +53,7 @@ def get_picongpu_name(self) -> str: Used for type name lookups """ - picongpu_name_by_element = { - Element.H: "Hydrogen", - Element.He: "Helium", - Element.N: "Nitrogen", - } - return picongpu_name_by_element[self] + return self.store.name def get_mass_si(self) -> float: """ @@ -76,12 +64,7 @@ def get_mass_si(self) -> float: :return: mass in kg """ - mass_by_particle = { - Element.H: 1.008 * scipy.constants.atomic_mass, - Element.He: 4.0026 * scipy.constants.atomic_mass, - Element.N: 14.007 * scipy.constants.atomic_mass, - } - return mass_by_particle[self] + return self.store.mass * scipy.constants.atomic_mass def get_charge_si(self) -> float: """ @@ -91,10 +74,14 @@ def get_charge_si(self) -> float: :return: charge in C """ - return self.value * scipy.constants.elementary_charge + return self.ions[-1] * scipy.constants.elementary_charge + + def get_symbol(self) -> str: + """get symbol""" + return self.store.symbol def _get_serialized(self) -> dict: return { - "symbol": self.name, + "symbol": self.get_symbol(), "picongpu_name": self.get_picongpu_name(), } From 2e459a7418d0547ec90557c4946f2c8d239eb8b5 Mon Sep 17 00:00:00 2001 From: BrianMarre Date: Tue, 23 Jul 2024 17:20:16 +0200 Subject: [PATCH 10/20] refactor translation ionization to pypicongpu --- .../picongpu/picmi/interaction/__init__.py | 3 +- .../picongpu/picmi/interaction/interaction.py | 118 +++- .../picmi/interaction/interactioninterface.py | 29 + .../picmi/interaction/ionization/__init__.py | 10 +- .../thomasfermi.py | 8 +- .../ionization/fieldionization/ADK.py | 14 + .../ionization/fieldionization/BSI.py | 21 +- .../fieldionization/fieldionization.py | 6 +- .../ionization/fieldionization/keldysh.py | 8 + .../ionization/groundstateionizationmodel.py | 21 + .../interaction/ionization/ionizationmodel.py | 11 +- lib/python/picongpu/picmi/requirements.txt | 1 + lib/python/picongpu/picmi/simulation.py | 249 ++------- lib/python/picongpu/picmi/species.py | 521 +++++++++++++----- .../picongpu/pypicongpu/requirements.txt | 2 +- .../pypicongpu/species/constant/constant.py | 12 + .../picongpu/pypicongpu/species/species.py | 2 +- .../pypicongpu/species/util/element.py | 57 +- .../param/speciesDefinition.param.mustache | 1 + .../species/constant/elementproperties.py | 8 +- .../quick/pypicongpu/species/initmanager.py | 2 +- .../quick/pypicongpu/species/util/element.py | 113 ++-- 22 files changed, 795 insertions(+), 422 deletions(-) create mode 100644 lib/python/picongpu/picmi/interaction/interactioninterface.py create mode 100644 lib/python/picongpu/picmi/interaction/ionization/groundstateionizationmodel.py diff --git a/lib/python/picongpu/picmi/interaction/__init__.py b/lib/python/picongpu/picmi/interaction/__init__.py index 69896a4ddf..e1088d4ca3 100644 --- a/lib/python/picongpu/picmi/interaction/__init__.py +++ b/lib/python/picongpu/picmi/interaction/__init__.py @@ -1,4 +1,5 @@ +from .interactioninterface import InteractionInterface from .interaction import Interaction from . import ionization -__all__ = ["Interaction", "ionization"] +__all__ = ["InteractionInterface", "Interaction", "ionization"] diff --git a/lib/python/picongpu/picmi/interaction/interaction.py b/lib/python/picongpu/picmi/interaction/interaction.py index fe52c707eb..645bff6338 100644 --- a/lib/python/picongpu/picmi/interaction/interaction.py +++ b/lib/python/picongpu/picmi/interaction/interaction.py @@ -5,11 +5,16 @@ License: GPLv3+ """ -import pydantic -from .ionization import IonizationModel +from ... import pypicongpu +from .ionization.groundstateionizationmodel import GroundStateIonizationModel +from .interactioninterface import InteractionInterface +from ..species import Species -class Interaction(pydantic.BaseModel): +import picmistandard + + +class Interaction(InteractionInterface): """ Common interface of Particle-In-Cell particle interaction extensions @@ -19,7 +24,7 @@ class Interaction(pydantic.BaseModel): It does not specify interface requirements for sub classes, since they differ too much. """ - Ionization: list[IonizationModel] + ground_state_ionization_model_list: list[GroundStateIonizationModel] """ list of all interaction models that change the charge state of ions @@ -28,3 +33,108 @@ class Interaction(pydantic.BaseModel): """ # @todo add Collisions as elastic interaction model, Brian Marre, 2024 + + @staticmethod + def update_constant_list( + existing_list: list[pypicongpu.species.constant.Constant], + new_list: dict[str, pypicongpu.species.constant.Constant], + ) -> None: + """check if dicts may be merged without overwriting previously set values""" + + new_constant_list = [] + + for constant_new in new_list: + exists_already = False + for constant in existing_list: + if type(constant) == type(constant_new): + # constant_new already exists in existing constants list + exists_already = True + + if constant != constant_new: + # same type of constant but conflicting values + raise ValueError(f"Constants {constant} and {constant_new} conflict with each other") + + if not exists_already: + new_constant_list.append(constant_new) + # ignore already existing constants + + # update constant_list + existing_list.extend(new_constant_list) + + 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""" + + constant_list = [] + ground_state_model_conversion = {} + for model in self.ground_state_ionization_model_list: + if model.ion_species == Species: + model_constants = model.get_constants() + Interaction.update_constant_list(constant_list, model_constants) + + ground_state_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() + ) + ) + + # add additional interaction sub groups needing constants here + return constant_list, {"ground_state_ionization": ground_state_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], + ], + ], + ) -> None: + """ + add ionization models to pypicongpu species + + in PICMI ioniaztion is defined as a list ionization models owned by the simulation, with each ionization model + storing its PICMI ion and PICMI ionization electron species. + + In contrast in PyPIConGPU each ion PyPIConGPU species owns a list of ionization models, each storing its + PyPIConGPU ionization electron species. + + This creates the problem that upon translation of the PICMI species to an PyPIConGPU species the PyPIConGPU + ionization electron species might not exist yet. + + Therefore we leave the ionization electron unspecified upon species creation and fill it in from the PICMI + simulation ionization model list later. + + (An because python uses pointers, this will be applied to the existing species objects passed in + 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 + + def has_ground_state_ionization(self, species: 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: + """does at least one ionization model list species as ion species?""" + + # add additional groups of ionization models here + 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 new file mode 100644 index 0000000000..572fae7ee9 --- /dev/null +++ b/lib/python/picongpu/picmi/interaction/interactioninterface.py @@ -0,0 +1,29 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from ... import pypicongpu + +import picmistandard +import pydantic + + +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/__init__.py b/lib/python/picongpu/picmi/interaction/ionization/__init__.py index a86996543f..1db647ca4f 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/__init__.py +++ b/lib/python/picongpu/picmi/interaction/ionization/__init__.py @@ -1,6 +1,10 @@ from .ionizationmodel import IonizationModel -from . import ionizationcurrent +from .groundstateionizationmodel import GroundStateIonizationModel from . import fieldionization -from . import electroniccollisionalequilibrium -__all__ = ["IonizationModel", "ionizationcurrent", "fieldionization", "electroniccollisionalequilibrium"] +__all__ = [ + "IonizationModel", + "GroundStateIonizationModel", + "fieldionization", + "electroniccollisionalequilibrium", +] diff --git a/lib/python/picongpu/picmi/interaction/ionization/electroniccollisonalequilibrium/thomasfermi.py b/lib/python/picongpu/picmi/interaction/ionization/electroniccollisonalequilibrium/thomasfermi.py index 288cf256e9..5055928e92 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/electroniccollisonalequilibrium/thomasfermi.py +++ b/lib/python/picongpu/picmi/interaction/ionization/electroniccollisonalequilibrium/thomasfermi.py @@ -5,10 +5,14 @@ License: GPLv3+ """ -from ..ionizationmodel import IonizationModel +from ..groundstateionizationmodel import GroundStateIonizationModel +from ..... import pypicongpu -class ThomasFermi(IonizationModel): +class ThomasFermi(GroundStateIonizationModel): """thomas fermi ionization model""" MODEL_NAME: str = "ThomasFermi" + + def get_as_pypicongpu(self) -> pypicongpu.species.constant.ionizationmodel.IonizationModel: + 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 dd4fcd1434..0cc4858f6f 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ADK.py +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ADK.py @@ -7,6 +7,11 @@ from .fieldionization import FieldIonization +from .....pypicongpu.species.constant.ionizationcurrent import None_ +from .....pypicongpu.species.constant.ionizationmodel import ADKLinearPolarization, ADKCircularPolarization + +from ..... import pypicongpu + import enum @@ -22,3 +27,12 @@ class ADK(FieldIonization): ADK_variant: ADKVariant """extension to the BSI model""" + + def get_as_pypicongpu(self) -> pypicongpu.species.constant.ionizationmodel.IonizationModel: + if self.ADK_variant is ADKVariant.LinearPolarization: + return ADKLinearPolarization(ionization_current=None_) + if self.ADK_variant is ADKVariant.CircularPolarization: + 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 fc886f7c7c..90d6ba4753 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/BSI.py +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/BSI.py @@ -7,6 +7,11 @@ from .fieldionization import FieldIonization +from .....pypicongpu.species.constant.ionizationcurrent import None_ +from .....pypicongpu.species.constant import ionizationmodel + +from ..... import pypicongpu + import enum @@ -22,5 +27,19 @@ class BSI(FieldIonization): MODEL_NAME: str = "BSI" - BIS_extensions: list[BSIExtension] + BSI_extensions: list[BSIExtension] """extension to the BSI model""" + + def get_as_pypicongpu(self) -> pypicongpu.species.constant.ionizationmodel.IonizationModel: + if self.BSI_extensions == []: + return ionizationmodel.BSI(ionization_current=None_) + + if self.BSI_extensions == [BSIExtension.StarkShift]: + return ionizationmodel.BSIStarkShifted(ionization_current=None_) + if self.BSI_extensions == [BSIExtension.EffectiveZ]: + return ionizationmodel.BSIEffectiveZ(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]}") diff --git a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/fieldionization.py b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/fieldionization.py index 4db3a3786a..5f34e2f230 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/fieldionization.py +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/fieldionization.py @@ -5,16 +5,14 @@ License: GPLv3+ """ -from .ionizationmodel import IonizationModel +from ..groundstateionizationmodel import GroundStateIonizationModel from .ionizationcurrent import IonizationCurrent import typing -class FieldIonization(IonizationModel): +class FieldIonization(GroundStateIonizationModel): """common interface of all field ionization models""" ionization_current: typing.Optional[IonizationCurrent] """ionization current for energy conservation of field ionization""" - - # + all IonizationModel interface requirements diff --git a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/keldysh.py b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/keldysh.py index e477f4b5ee..168925aaf9 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/keldysh.py +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/keldysh.py @@ -7,8 +7,16 @@ from .fieldionization import FieldIonization +from .....pypicongpu.species.constant.ionizationcurrent import None_ +from .....pypicongpu.species.constant import ionizationmodel + +from ..... import pypicongpu + 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_) diff --git a/lib/python/picongpu/picmi/interaction/ionization/groundstateionizationmodel.py b/lib/python/picongpu/picmi/interaction/ionization/groundstateionizationmodel.py new file mode 100644 index 0000000000..e2e31cf36b --- /dev/null +++ b/lib/python/picongpu/picmi/interaction/ionization/groundstateionizationmodel.py @@ -0,0 +1,21 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from .ionizationmodel import IonizationModel + +from .... import pypicongpu + + +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""" + Z = self.ion_species.picongpu_element.get_atomic_number() + assert self.ion_species.charge_state <= Z, f"charge_state must be <= atomic number ({Z})" + + element_properties_const = pypicongpu.species.constant.ElementProperties() + element_properties_const.element = self.ion_species.picongpu_element + 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 7983fad46c..552571a9a6 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py +++ b/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py @@ -6,6 +6,7 @@ """ from ...species import Species +from .... import pypicongpu import pydantic @@ -21,7 +22,13 @@ class IonizationModel(pydantic.BaseModel): """ionization model""" ion_species: Species - """ion species to apply ionization model for""" + """PICMI ion species to apply ionization model for""" ionization_electron_species: Species - """electron species of which to create macro particle upon ionization""" + """PICMI electron species of which to create macro particle upon ionization""" + + def get_constants(self) -> list[pypicongpu.species.constant.Constant]: + raise NotImplementedError("abstract base class only!") + + def get_as_pypicongpu(self) -> pypicongpu.species.constant.ionizationmodel.IonizationModel: + raise NotImplementedError("abstract base class only!") diff --git a/lib/python/picongpu/picmi/requirements.txt b/lib/python/picongpu/picmi/requirements.txt index b2a1853e9b..6135cdc3c3 100644 --- a/lib/python/picongpu/picmi/requirements.txt +++ b/lib/python/picongpu/picmi/requirements.txt @@ -4,4 +4,5 @@ picmistandard >= 0.27.0 typeguard >= 4.2.1 sympy >= 1.9 pydantic >= 2.6.4 +pdg >= 0.1.2 -r ../pypicongpu/requirements.txt diff --git a/lib/python/picongpu/picmi/simulation.py b/lib/python/picongpu/picmi/simulation.py index 87cf8653d5..d91a5be731 100644 --- a/lib/python/picongpu/picmi/simulation.py +++ b/lib/python/picongpu/picmi/simulation.py @@ -8,23 +8,22 @@ # make pypicongpu classes accessible for conversion to pypicongpu from .. import pypicongpu -from ..pypicongpu import util - from . import constants from .grid import Cartesian3DGrid -from .species import Species as PicongpuPicmiSpecies from .interaction import Interaction import picmistandard import math -import pydantic +import typeguard import pathlib import logging import typing -class Simulation(picmistandard.PICMI_Simulation, pydantic.BaseModel): +# may not use pydantic since inherits from _DocumentedMetaClass +@typeguard.typechecked +class Simulation(picmistandard.PICMI_Simulation): """ Simulation as defined by PICMI @@ -32,26 +31,19 @@ class Simulation(picmistandard.PICMI_Simulation, pydantic.BaseModel): https://picmi-standard.github.io/standard/simulation.html """ - model_config = pydantic.ConfigDict(extra="allow") - """ - set to allow and store additional attributes outside pydantic model validation. - - This ensure that the PICMI defined attributes are also stored in the pydantic instances. - - @attention needs to be the first entry, other wise ignored for some reason - """ - - picongpu_custom_user_input: typing.Optional[list[pypicongpu.customuserinput.InterfaceCustomUserInput]] = None + picongpu_custom_user_input = pypicongpu.util.build_typesafe_property( + typing.Optional[list[pypicongpu.customuserinput.InterfaceCustomUserInput]] + ) """ list of custom user input objects update using picongpu_add_custom_user_input() or by direct setting """ - picongpu_interaction: typing.Optional[Interaction] + picongpu_interaction = pypicongpu.util.build_typesafe_property(typing.Optional[Interaction]) """Interaction instance containing all particle interactions of the simulation, set to None to have no interactions""" - picongpu_typical_ppc: typing.Optional[int] + picongpu_typical_ppc = pypicongpu.util.build_typesafe_property(typing.Optional[int]) """ typical number of particle in a cell in the simulation @@ -60,10 +52,10 @@ class Simulation(picmistandard.PICMI_Simulation, pydantic.BaseModel): optional, if set to None, will be set to median ppc of all species ppcs """ - picongpu_template_dir: str + picongpu_template_dir = pypicongpu.util.build_typesafe_property(typing.Optional[str]) """directory containing templates to use for generating picongpu setups""" - picongpu_moving_window_move_point: typing.Optional[float] + picongpu_moving_window_move_point = pypicongpu.util.build_typesafe_property(typing.Optional[float]) """ point a light ray reaches in y from the left border until we begin sliding the simulation window with the speed of light @@ -74,13 +66,13 @@ class Simulation(picmistandard.PICMI_Simulation, pydantic.BaseModel): thereby reducing the simulation window size accordingrelative spot at which to start moving the simulation window """ - picongpu_moving_window_stop_iteration: typing.Optional[int] + picongpu_moving_window_stop_iteration = pypicongpu.util.build_typesafe_property(typing.Optional[int]) """iteration, at which to stop moving the simulation window""" - __runner: typing.Optional[pypicongpu.runner.Runner] = None - __electron_species: typing.Optional[pypicongpu.species.Species] = None + __runner = pypicongpu.util.build_typesafe_property(typing.Optional[pypicongpu.runner.Runner]) - # @todo remove boiler plate constructor argument list once PICMI switches to pydantic, Brian Marre, 2024 + # @todo remove boiler plate constructor argument list once picmistandard reference implementation switches to + # pydantic, Brian Marre, 2024 def __init__( self, picongpu_template_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, @@ -90,20 +82,17 @@ def __init__( picongpu_interaction: typing.Optional[Interaction] = None, **keyword_arguments, ): - # call pydantic.BaseModel constructor first, + # 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 - # pass pydantic data - picongpu_data = {} - picongpu_data["picongpu_template_dir"] = picongpu_template_dir - picongpu_data["picongpu_typical_ppc"] = picongpu_typical_ppc - picongpu_data["picongpu_moving_window_move_point"] = picongpu_moving_window_move_point - picongpu_data["picongpu_moving_window_stop_iteration"] = picongpu_moving_window_stop_iteration - picongpu_data["picongpu_interaction"] = picongpu_interaction - - ### pydantic.BaseModel init call - pydantic.BaseModel.__init__(self, **picongpu_data) + 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 + self.picongpu_interaction = picongpu_interaction + 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) @@ -117,9 +106,10 @@ def __init__( ## template_path is valid if picongpu_template_dir == "": raise ValueError("picongpu_template_dir MUST NOT be empty string") - template_path = pathlib.Path(picongpu_template_dir) - if template_path.is_dir(): - raise ValueError("picongpu_template_dir must be existing directory") + if picongpu_template_dir is not None: + template_path = pathlib.Path(picongpu_template_dir) + if template_path.is_dir(): + raise ValueError("picongpu_template_dir must be existing directory") def __yee_compute_cfl_or_delta_t(self) -> None: """ @@ -287,51 +277,10 @@ def __get_operations_from_individual_species( all_operations = [] for picmi_species, pypicongpu_species in pypicongpu_by_picmi_species.items(): - all_operations += picmi_species.get_independent_operations(pypicongpu_species) + all_operations += picmi_species.get_independent_operations(pypicongpu_species, self.picongpu_interaction) return all_operations - def __fill_ionization_electrons( - self, - pypicongpu_by_picmi_species: typing.Dict[picmistandard.PICMI_Species, pypicongpu.species.Species], - ) -> None: - """ - copy used-electron-relationship from PICMI to PIConGPU species - - Translating a PICMI species to a PyPIConGPU species creates a ionizers - constant, but the reference to the used species is missing at this - point (b/c the translated species doesn't know the corresponding - PyPIConGPU species to be associated to.) - - This method fills the pypicongpu ionizers electron_species from the - PICMI picongpu_ionization_electrons attribute. - Note that for this the picongpu_ionization_electrons attribute must be - already set, probably from __resolve_electrons() - - (An b/c python uses pointers, this will be applied to the existing - species objects passed in pypicongpu_by_picmi_species) - """ - - for picmi_species, pypic_species in pypicongpu_by_picmi_species.items(): - # only fill ionization electrons if required (by ionizers) - if not pypic_species.has_constant_of_type(pypicongpu.species.constant.GroundStateIonization): - continue - - assert picmi_species.picongpu_ionization_electrons in pypicongpu_by_picmi_species, ( - "species {} (set as electrons " - "for species {} via picongpu_ionization_species) must be " - "explicitly added with add_species()".format( - picmi_species.picongpu_ionization_electrons.name, pypic_species.name - ) - ) - - ionizer_model_list = pypic_species.get_constant_by_type(pypicongpu.species.constant.GroundStateIonization) - # is pointer -> sets correct species for actual pypicongpu species - for model in ionizer_model_list: - model.ionization_electron_species = pypicongpu_by_picmi_species[ - picmi_species.picongpu_ionization_electrons - ] - def __get_init_manager(self) -> pypicongpu.species.InitManager: """ create & fill an initmanager @@ -346,11 +295,10 @@ def __get_init_manager(self) -> pypicongpu.species.InitManager: """ initmgr = pypicongpu.species.InitManager() - # check preconditions + # check preconditions, @todo move to picmistandard, Brian Marre 2024 assert len(self.species) == len(self.layouts) - # either no layout AND no profile, or both - # (also: no ratio without layout and profile) + # 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 @@ -360,28 +308,31 @@ def __get_init_manager(self) -> pypicongpu.species.InitManager: None ), "species need BOTH layout AND initial distribution set (or neither)" - # ratio only set if + # 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 - ## - # note: 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_picmi_species = {} for picmi_species in self.species: - pypicongpu_species = picmi_species.get_as_pypicongpu() + pypicongpu_species, ionization_model_conversion = picmi_species.get_as_pypicongpu() pypicongpu_by_picmi_species[picmi_species] = pypicongpu_species + ionization_model_conversion_by_picmi_species[picmi_species] = ionization_model_conversion initmgr.all_species.append(pypicongpu_species) # fill inter-species dependencies - ## - # ionization (PICMI species don't know which PyPIConGPU species they - # use as electrons) - self.__fill_ionization_electrons(pypicongpu_by_picmi_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 + if self.picongpu_interaction is not None: + self.picongpu_interaction.fill_in_ionization_electrons( + pypicongpu_by_picmi_species, ionization_model_conversion_by_picmi_species + ) # operations with inter-species dependencies ## @@ -394,97 +345,6 @@ def __get_init_manager(self) -> pypicongpu.species.InitManager: return initmgr - def __get_electron_species(self) -> PicongpuPicmiSpecies: - """ - get electron species from existing species or generate new - - PIConGPU requires an explicit electron species, which PICMI assumes to - implicitly already exist. - This method retrieves an electron species by either reusing an existing - one or generating one if missing. - - Approach: - - 0 electron species: add one (print INFO log) - - 1 electron species: use it - - >1 electron species: raise, b/c is ambiguous - - electrons are identified by either mass & charge, or by particle_type. - """ - # use caching, this is method is expensive - if self.__electron_species is not None: - return self.__electron_species - - all_electrons = [] - for picmi_species in self.species: - if "electron" == picmi_species.particle_type: - all_electrons.append(picmi_species) - elif ( - picmi_species.mass is not None - and math.isclose(picmi_species.mass, constants.m_e) - and picmi_species.charge is not None - and math.isclose(picmi_species.charge, -constants.q_e) - ): - all_electrons.append(picmi_species) - - # exactly one electron species: use it - if 1 == len(all_electrons): - self.__electron_species = all_electrons[0] - return self.__electron_species - - # no electron species: add one - if 0 == len(all_electrons): - # compute unambiguous name - all_species_names = list(map(lambda picmi_species: picmi_species.name, self.species)) - electrons_name = "e" - while electrons_name in all_species_names: - electrons_name += "_" - - logging.info( - "no electron species for ionization available, creating electrons with name: {}".format(electrons_name) - ) - electrons = PicongpuPicmiSpecies(name=electrons_name, particle_type="electron") - self.add_species(electrons, None) - - self.__electron_species = electrons - return self.__electron_species - - # ambiguous choice -> raise - raise ValueError( - "choice of electron species for ionization is ambiguous, please " - "set picongpu_ionization_electrons explicitly for ionizable " - "species; found electron species: {}".format( - ", ".join(map(lambda picmi_species: picmi_species.name, all_electrons)) - ) - ) - - def __resolve_electrons(self) -> None: - """ - fill missing picongpu_ionization_electrons for ionized species - - PIConGPU needs every electron species set explicitly. - For this, PIConGPU PICMI species have a property - picongpu_ionization_electrons, which points to another PICMI species - to be used for ionization. - To be compatible to the native PICMI, this property is not required - from the **user**, but it is stillrequired for **translation**. - - This method guesses the value of picongpu_ionization_electrons if they - are not set. - - The actual electron selection is implemented in - __get_electron_species() - """ - for picmi_species in self.species: - # only handle ionized species anyways - if not picmi_species.has_ionizers(): - continue - - # skip if ionization electrons already set (nothing to guess) - if picmi_species.picongpu_ionization_electrons is not None: - continue - - picmi_species.picongpu_ionization_electrons = self.__get_electron_species() - def write_input_file( self, file_name: str, pypicongpu_simulation: typing.Optional[pypicongpu.simulation.Simulation] = None ) -> None: @@ -507,16 +367,18 @@ def write_input_file( self.__runner.generate() def picongpu_add_custom_user_input(self, custom_user_input: pypicongpu.customuserinput.InterfaceCustomUserInput): + """add custom user input to previously stored input""" if self.picongpu_custom_user_input is None: self.picongpu_custom_user_input = [custom_user_input] else: self.picongpu_custom_user_input.append(custom_user_input) def add_interaction(self, interaction) -> None: - util.unsupported( - "PICMI standard interactions are not supported by PIConGPU, assign an Interaction object to the picongpu_interaction attribute of the simulation instead." + pypicongpu.util.unsupported( + "PICMI standard interactions are not supported by PIConGPU, use the picongpu specific Interaction object instead" ) + # @todo add refactor once restarts are supported by the Runner, Brian Marre, 2024 def step(self, nsteps: int = 1): if nsteps != self.max_steps: raise ValueError( @@ -531,7 +393,7 @@ def get_as_pypicongpu(self) -> pypicongpu.simulation.Simulation: s.delta_t_si = self.time_step_size s.solver = self.solver.get_as_pypicongpu() - # already in pypicongpu objects + # already pypicongpu objects, therefore directly passing on s.custom_user_input = self.picongpu_custom_user_input # calculate time step @@ -542,22 +404,22 @@ def get_as_pypicongpu(self) -> pypicongpu.simulation.Simulation: else: raise ValueError("runtime not specified (neither as step count nor max time)") - util.unsupported("verbose", self.verbose) - util.unsupported("particle shape", self.particle_shape, "linear") - util.unsupported("gamma boost, use picongpu_moving_window_move_point instead", self.gamma_boost) + pypicongpu.util.unsupported("verbose", self.verbose) + pypicongpu.util.unsupported("particle shape", self.particle_shape, "linear") + pypicongpu.util.unsupported("gamma boost, use picongpu_moving_window_move_point instead", self.gamma_boost) try: s.grid = self.solver.grid.get_as_pypicongpu() except AttributeError: - util.unsupported(f"grid type: {type(self.solver.grid)}") + pypicongpu.util.unsupported(f"grid type: {type(self.solver.grid)}") # any injection method != None is not supported if len(self.laser_injection_methods) != self.laser_injection_methods.count(None): - util.unsupported("laser injection method", self.laser_injection_methods, []) + pypicongpu.util.unsupported("laser injection method", self.laser_injection_methods, []) # pypicongpu interface currently only supports one laser, @todo change Brian Marre, 2024 if len(self.lasers) > 1: - util.unsupported("more than one laser") + pypicongpu.util.unsupported("more than one laser") if len(self.lasers) == 1: # check requires grid, so grid is translated (and thereby also checked) above @@ -566,12 +428,9 @@ def get_as_pypicongpu(self) -> pypicongpu.simulation.Simulation: # explictly disable laser (as required by pypicongpu) s.laser = None - # resolve electrons - self.__resolve_electrons() - s.init_manager = self.__get_init_manager() - # set typical ppc if not overwritten by user + # set typical ppc if not set explicitly by user if self.picongpu_typical_ppc is None: s.typical_ppc = (s.init_manager).get_typical_particle_per_cell() else: @@ -599,7 +458,7 @@ def picongpu_run(self) -> None: self.__runner.build() self.__runner.run() - def picongpu_get_runner(self) -> pypicongpu.runnerRunner: + 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) return self.__runner diff --git a/lib/python/picongpu/picmi/species.py b/lib/python/picongpu/picmi/species.py index 1662ebecc3..dc93a7d5b0 100644 --- a/lib/python/picongpu/picmi/species.py +++ b/lib/python/picongpu/picmi/species.py @@ -1,54 +1,238 @@ """ This file is part of PIConGPU. -Copyright 2021-2023 PIConGPU contributors +Copyright 2021-2024 PIConGPU contributors Authors: Hannes Troepgen, Brian Edward Marre License: GPLv3+ """ -from ..pypicongpu import util, species +from .. import pypicongpu +from ..pypicongpu.species.util.element import Element +from .interaction import InteractionInterface import picmistandard -import typeguard import typing +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""" - # ONLY set non-element particles here -- all other are handled by - # element - __mass_charge_by_openpmd_name_non_elements = { - "electron": (consts.electron_mass, -consts.elementary_charge), + _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), } - """mass/charge tuple to use when passed a non-element particle_type""" - picongpu_fully_ionized = util.build_typesafe_property(typing.Optional[bool]) + _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) + ) """ - *usually* ionization is expected to be used on elements -- use this to - explicitly DISABLE ionization + mass/charge to use when passed a non-element particle_type + + @attention ONLY set non-element particles here, all other are handled by element """ - def __init__(self, picongpu_fully_ionized: typing.Optional[bool] = None, picongpu_ionization_electrons=None, **kw): - self.picongpu_fully_ionized = picongpu_fully_ionized + __non_element_particle_types: dict[str, _PropertyTuple] = __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]) + + interactions = pypicongpu.util.build_typesafe_property(type(None)) + """overwrite base class interactions to disallow setting them""" + + __warned_already: 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""" - # note: picongpu_ionization_electrons would *normally* just use a - # forward-declared typecheck "Species" - # However, this requires that "Species" at some point resolves to this - # class. Typically this picmi species object is only available as - # "picmi.Species()", and the resolution fails. - # Hence, the type is checked manually here. - typeguard.check_type(picongpu_ionization_electrons, typing.Optional[Species]) - self.picongpu_ionization_electrons = picongpu_ionization_electrons + element_schema = handler.generate_schema(typing.Optional[Element]) - super().__init__(**kw) + 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): + self.picongpu_fixed_charge = picongpu_fixed_charge + self.picongpu_element = None + + # let PICMI class handle remaining init + picmistandard.PICMI_Species.__init__(**keyword_arguments) @staticmethod def __get_temperature_kev_by_rms_velocity( - rms_velocity_si: typing.Tuple[float, float, float], particle_mass_si: float + rms_velocity_si: tuple[float, float, float], particle_mass_si: float ) -> float: """ convert temperature from RMS velocity vector to keV @@ -72,7 +256,7 @@ def __get_temperature_kev_by_rms_velocity( rms_velocity_si_squared = rms_velocity_si[0] ** 2 return particle_mass_si * rms_velocity_si_squared * consts.electron_volt**-1 * 10**-3 - def __get_drift(self) -> typing.Optional[species.operation.momentum.Drift]: + def __get_drift(self) -> typing.Optional[pypicongpu.species.operation.momentum.Drift]: """ Retrieve respective pypicongpu drift object (or None) @@ -86,85 +270,148 @@ def __get_drift(self) -> typing.Optional[species.operation.momentum.Drift]: def __maybe_apply_particle_type(self) -> None: """ - check if particle type is set, if yes set self.mass and self.charge - """ - if self.particle_type is None: - return + if particle type is set, set self.mass, self.charge and element from particle type - # particle type is set -> retrieve mass & charge - 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" + necessary to ensure consistent state regardless which parameters the user specified in species init - if self.particle_type in self.__mass_charge_by_openpmd_name_non_elements: - # not element, but known - mass_charge_tuple = self.__mass_charge_by_openpmd_name_non_elements[self.particle_type] - self.mass = mass_charge_tuple[0] - self.charge = mass_charge_tuple[1] + @raises if both particle_type and charge mass are specified + """ + + if (self.particle_type is None) or re.match(r"other:.*", self.particle_type): + # no particle or custom particle type set + pass else: - # element (or unkown, which raises when trying to get an - # element for that name) - self.element = species.util.Element.get_by_openpmd_name(self.particle_type) - self.mass = self.element.get_mass_si() - self.charge = self.element.get_charge_si() + # set mass & charge + if self.particle_type in self.__non_element_particle_types: + # not element, but known + mass_charge_tuple = self.__non_element_particle_type_properties[self.particle_type] + + self.mass = mass_charge_tuple.mass + self.charge = mass_charge_tuple.charge + elif Element.is_element(self.particle_type): + # element or similar, will raise if element name is unknown + self.picongpu_element = pypicongpu.species.util.Element(self.particle_type) + self.mass = self.picongpu_element.get_mass_si() + self.charge = self.picongpu_element.get_charge_si() + else: + # unknown particle type + raise ValueError(f"Species {self.name} has unknown particle type {self.particle_type}") + + def has_ionization(self, interaction: InteractionInterface) -> bool: + """does species have ionization configured?""" + if interaction is None: + return False + if interaction.has_ionization(self): + return True - def __check_ionization(self) -> None: + def is_ion(self) -> bool: """ - check if ionization (charge_state) can be applied, potentially warns + is species an ion? + + @attention requires __maybe_apply_particle_type() to have been called first, + otherwise will return wrong result """ - assert not self.picongpu_fully_ionized or self.charge_state is None, ( - "picongpu_fully_ionized may only be used if " "charge_state is none" - ) + if self.picongpu_element is None: + return False + return True - if self.particle_type is None: - # no particle type -> charge state is not allowed - assert self.charge_state is None, "charge_state is ONLY allowed " "when setting particle_type explicitly" + def __check_ionization_configuration(self, interaction: InteractionInterface) -> None: + """ + check species ioniaztion- and species- configuration are compatible + + @raises if incorrect configuration found + """ - # no particle type -> fully ionized flag not permitted - assert self.picongpu_fully_ionized is None, ( - "picongpu_fully_ionized is ONLY allowed " "when setting particle_type explicitly" + if self.particle_type is None: + assert not self.has_ionization( + interaction + ), f"Species {self.name} configured with active ionization but required particle_type not set." + assert self.charge_state is None, ( + f"Species {self.name} specified initial charge state via charge_state without also specifying particle " + "type, must either set particle_type explicitly or only use charge instead" ) + assert ( + self.picongpu_fixed_charge is None + ), f"Species {self.name} specified fixed charge without also specifying particle_type" + else: + # particle type is + if (self.particle_type in self.__non_element_particle_types) or re.match(r"other:.*", self.particle_type): + # non ion predefined particle, or custom particle type + assert self.charge_state is None, "charge_state may only be set for ions" + assert not self.has_ionization( + interaction + ), f"Species {self.name} configured with active ionization but particle type indicates non ion." + assert ( + self.picongpu_fixed_charge is None + ), f"Species {self.name} configured with fixed charge state but particle_type indicates non ion" + elif Element.is_element(self.particle_type): + # ion + 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 " + " either or but not both." + ) + assert self.charge_state is not None, ( + f"Species {self.name} configured with ionization but no initial charge state specified, " + "must be explicitly specified via charge_state." + ) + else: + # ion with fixed charge + if not self.picongpu_fixed_charge: + raise ValueError( + f"Species {self.name} configured with fixed charge state without explicitly setting picongpu_fixed_charge=True" + ) + + if not self.__warned_already: + logging.warning( + f"Species {self.name} configured with fixed charge state but particle type" + "indicates element. This is not recommended but supported" + ) + self.__warned_already = True + + # charge_state may be set or None indicating some fixed number of bound electrons or fully 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: + """check all interactions sub groups for compatibility with this species configuration""" + self.__check_ionization_configuration(interaction) - # no charge_state -> nothing left - return + def check(self, interaction: InteractionInterface) -> None: + assert self.name is not None, "picongpu requires each species to have a name set." - # particle type is set: fully ionized flag *ONLY* allowed if using - # element - if self.picongpu_fully_ionized is not None: + # 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): assert ( - self.particle_type not in self.__mass_charge_by_openpmd_name_non_elements - ), "picongpu_fully_ionized is ONLY allowed for elements" - - # maybe warn - if self.charge_state is None: - # theoretically speaking atoms *always* have a charge state - # for PIConGPU an atom (ion) may exist without a charge state, - # i.e. without ionization, however this may result in - # (physically) incorrect behavior - # Therefore warn if there is no charge state -- unless this - # warning is explicitly disabled with a flag is given - - # (note: omit if not element) - if ( - not self.picongpu_fully_ionized - and self.particle_type not in self.__mass_charge_by_openpmd_name_non_elements - ): - logging.warning( - "species {} will be fully ionized for the entire " - "simulation -- if this is intended, set " - "picongpu_fully_ionized=True".format(self.name) - ) - - def get_as_pypicongpu(self) -> species.Species: - util.unsupported("method", self.method) - util.unsupported("particle shape", self.particle_shape) - # note: placement params are respected in associated simulation object - - assert self.name is not None, "name must be set" + self.charge is not None + ), "charge must be set explicitly if no particle type or custom particle type is specified" + 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.__check_interaction_configuration(interaction) + + def get_as_pypicongpu(self, interaction: InteractionInterface) -> pypicongpu.species.Species: + """ + translate PICMI species object to equivalent PyPIConGPU species object + + @attention only translates ONLY species owned objects, for example species-Constants + everything else requires a call to the corresponding getter of this class + """ + # error on unsupported options + pypicongpu.util.unsupported("method", self.method) + pypicongpu.util.unsupported("particle shape", self.particle_shape) + # @note placement params are respected in associated simulation object + + self.check(interaction) self.__maybe_apply_particle_type() - self.__check_ionization() - s = species.Species() + s = pypicongpu.species.Species() s.name = self.name s.constants = [] @@ -172,57 +419,44 @@ def get_as_pypicongpu(self) -> species.Species: # if 0==mass rather omit mass entirely assert self.mass > 0 - mass_constant = species.constant.Mass() + mass_constant = pypicongpu.species.constant.Mass() mass_constant.mass_si = self.mass s.constants.append(mass_constant) - if self.charge is not None: - charge_constant = species.constant.Charge() - charge_constant.charge_si = self.charge - s.constants.append(charge_constant) - if self.density_scale is not None: assert self.density_scale > 0 - density_scale_constant = species.constant.DensityRatio() + density_scale_constant = pypicongpu.species.constant.DensityRatio() density_scale_constant.ratio = self.density_scale s.constants.append(density_scale_constant) - if self.particle_type and self.particle_type not in self.__mass_charge_by_openpmd_name_non_elements: - # particle type given and is not non-element (==is element) - # -> add element flags - element = species.util.Element.get_by_openpmd_name(self.particle_type) - - elementary_properties_const = species.constant.ElementProperties() - elementary_properties_const.element = element - s.constants.append(elementary_properties_const) + # default case species with no charge and/or no bound electrons or with ionization + charge_constant_value = self.charge - if self.charge_state is not None: - # element must be set from previous code section - assert element is not None + initial_charge_state_set = self.charge_state is not None + fixed_charge_state = not self.has_ionization(interaction) + if self.is_ion() and initial_charge_state_set and fixed_charge_state: + # fixed not completely ionized ion + charge_constant_value = self.charge_state * consts.elementary_charge - atomic_number = element.value - assert self.charge_state <= atomic_number, "charge_state must be <= atomic number ({})".format( - atomic_number - ) + if charge_constant_value is not None: + charge_constant = pypicongpu.species.constant.Charge() + charge_constant.charge_si = charge_constant_value + s.constants.append(charge_constant) - const_ionizers = species.constant.Ionizers() - # const_ionizers.electron_species must be set to a pypicongpu - # species, but this is not available here - # -> inserted externally - s.constants.append(const_ionizers) + if interaction is not None: + interaction_constants, pypicongpu_model_by_picmi_model = interaction.get_interaction_constants(self) + s.constants.extend(interaction_constants) - return s - - def has_ionizers(self) -> bool: - """ - returns true iff a species will have ionizers (algorithms) - """ - return self.charge_state is not None + return s, pypicongpu_model_by_picmi_model def get_independent_operations( - self, pypicongpu_species: species.Species - ) -> typing.List[species.operation.Operation]: + self, pypicongpu_species: pypicongpu.species.Species, interaction: InteractionInterface + ) -> list[pypicongpu.species.operation.Operation]: + # assure consistent state of species + self.check(interaction) + self.__maybe_apply_particle_type() + assert pypicongpu_species.name == self.name, ( "to generate " "operations for PyPIConGPU species: names must match" ) @@ -230,13 +464,13 @@ def get_independent_operations( all_operations = [] # assign momentum - momentum_op = species.operation.SimpleMomentum() + momentum_op = pypicongpu.species.operation.SimpleMomentum() momentum_op.species = pypicongpu_species momentum_op.drift = self.__get_drift() temperature_kev = 0 if self.initial_distribution is not None and self.initial_distribution.rms_velocity is not None: - mass_const = pypicongpu_species.get_constant_by_type(species.constant.Mass) + mass_const = pypicongpu_species.get_constant_by_type(pypicongpu.species.constant.Mass) mass_si = mass_const.mass_si temperature_kev = self.__get_temperature_kev_by_rms_velocity( @@ -244,34 +478,21 @@ def get_independent_operations( ) if 0 != temperature_kev: - momentum_op.temperature = species.operation.momentum.Temperature() + momentum_op.temperature = pypicongpu.species.operation.momentum.Temperature() momentum_op.temperature.temperature_kev = temperature_kev else: momentum_op.temperature = None all_operations.append(momentum_op) - # ionization: - if self.has_ionizers(): - # note: this will raise if called *before* get_as_pypicongpu with - # "self.element" is not defined -- in this case, either fix the - # order or compute the element here on the fly - atomic_number = self.element.value - - # fully ionized? - if self.charge_state == atomic_number: - ion_op_no_electrons = species.operation.NoBoundElectrons() - ion_op_no_electrons.species = pypicongpu_species - all_operations.append(ion_op_no_electrons) - else: - # not fully ionized - bound_electrons = atomic_number - self.charge_state - assert bound_electrons > 0 - - ion_op_electrons = species.operation.SetBoundElectrons() - ion_op_electrons.species = pypicongpu_species - ion_op_electrons.bound_electrons = bound_electrons - - all_operations.append(ion_op_electrons) + # assign bound electrons + if self.is_ion() and self.has_ionization(interaction): + bound_electrons_op = pypicongpu.species.operation.SetBoundElectrons() + bound_electrons_op.species = pypicongpu_species + bound_electrons_op.bound_electrons = self.picongpu_element.get_atomic_number() - self.charge_state + all_operations.append(bound_electrons_op) + else: + # fixed charge state -> therefore no bound electron attribute necessary + pass return all_operations diff --git a/lib/python/picongpu/pypicongpu/requirements.txt b/lib/python/picongpu/pypicongpu/requirements.txt index d57ed81fa1..d30e2f0878 100644 --- a/lib/python/picongpu/pypicongpu/requirements.txt +++ b/lib/python/picongpu/pypicongpu/requirements.txt @@ -3,4 +3,4 @@ jsonschema >= 4.23.0 typeguard >= 4.2.1 referencing >= 0.35.1 pydantic >= 2.6.4 -periodictable +periodictable >= 1.7.1 diff --git a/lib/python/picongpu/pypicongpu/species/constant/constant.py b/lib/python/picongpu/pypicongpu/species/constant/constant.py index 6aa4b82adc..01910842eb 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/constant.py +++ b/lib/python/picongpu/pypicongpu/species/constant/constant.py @@ -45,6 +45,18 @@ class Constant(RenderedObject): constant) """ + def __eq__(self, other) -> bool: + """two constants are equal if the have the same attributes values""" + for key in self.__dict__.keys(): + if key not in other.__dict__: + return False + + for key, value in self.__dict__: + if self.value != other.__dict__[key]: + return False + + return True + def check(self) -> None: """ ensure validity of self diff --git a/lib/python/picongpu/pypicongpu/species/species.py b/lib/python/picongpu/pypicongpu/species/species.py index 7749d52f7a..1b21e51aab 100644 --- a/lib/python/picongpu/pypicongpu/species/species.py +++ b/lib/python/picongpu/pypicongpu/species/species.py @@ -129,7 +129,7 @@ def has_constant_of_type(self, needle_type: typing.Type[Constant]) -> bool: """ lookup if constant of given type is present - Searches through constants of this species and returns true iff a + Searches through constants of this species and returns true if a constant of the given type is present. :param needle_type: constant type to look for diff --git a/lib/python/picongpu/pypicongpu/species/util/element.py b/lib/python/picongpu/pypicongpu/species/util/element.py index d6b9d93728..3936e73da5 100644 --- a/lib/python/picongpu/pypicongpu/species/util/element.py +++ b/lib/python/picongpu/pypicongpu/species/util/element.py @@ -6,13 +6,15 @@ """ from ...rendering import RenderedObject -from ... import util +import pydantic +import typing import scipy import periodictable +import re -class Element(RenderedObject): +class Element(RenderedObject, pydantic.BaseModel): """ Denotes an element from the periodic table of elements @@ -27,7 +29,33 @@ class Element(RenderedObject): To describe atoms/ions you also need to initialize the charge_state of the species. """ - store = util.build_typesafe_propety(periodictable.Element) + _store: typing.Optional[periodictable.core.Element] = None + + @staticmethod + def parse_openpmd_isotopes(openpmd_name: str) -> tuple[int, str]: + 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") + + mass_number = int(m.group(1)) + symbol = m.group(2) + + return mass_number, symbol + + @staticmethod + def is_element(openpmd_name: str) -> bool: + """does openpmd_name describe an element?""" + mass_number, symbol = Element.parse_openpmd_isotopes(openpmd_name) + + for element in periodictable.elements: + if symbol == element.symbol: + if openpmd_name not in ["n"]: + return True + return False def __init__(self, openpmd_name: str) -> None: """ @@ -37,15 +65,21 @@ def __init__(self, openpmd_name: str) -> None: :return: object representing the given species """ + pydantic.BaseModel.__init__(self) + + mass_number, openpmd_name = Element.parse_openpmd_isotopes(openpmd_name) # search for name in periodic table for element in periodictable.elements: if openpmd_name == element.symbol: - self.store = element + if mass_number is None: + self._store = element + else: + self._store = element[mass_number] return # not found - raise NameError("unkown element: {}".format(openpmd_name)) + raise NameError(f"unknown element: {openpmd_name}") def get_picongpu_name(self) -> str: """ @@ -53,7 +87,9 @@ def get_picongpu_name(self) -> str: Used for type name lookups """ - return self.store.name + name = self._store.name + # element names are capitalized in piconpgu + return name[0].upper() + name[1:] def get_mass_si(self) -> float: """ @@ -64,7 +100,7 @@ def get_mass_si(self) -> float: :return: mass in kg """ - return self.store.mass * scipy.constants.atomic_mass + return self._store.mass * scipy.constants.atomic_mass def get_charge_si(self) -> float: """ @@ -74,11 +110,14 @@ def get_charge_si(self) -> float: :return: charge in C """ - return self.ions[-1] * scipy.constants.elementary_charge + return self._store.ions[-1] * scipy.constants.elementary_charge + + def get_atomic_number(self) -> int: + return self._store.number def get_symbol(self) -> str: """get symbol""" - return self.store.symbol + return self._store.symbol def _get_serialized(self) -> dict: return { 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 668ffb3223..e97097e8ef 100644 --- a/share/picongpu/pypicongpu/template/include/picongpu/param/speciesDefinition.param.mustache +++ b/share/picongpu/pypicongpu/template/include/picongpu/param/speciesDefinition.param.mustache @@ -78,6 +78,7 @@ namespace picongpu {{#element_properties}} atomicNumbers, ionizationEnergies, + effectiveNuclearCharge, {{/element_properties}} {{/constants}} diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/elementproperties.py b/test/python/picongpu/quick/pypicongpu/species/constant/elementproperties.py index c3081e422f..b55b5cf7c2 100644 --- a/test/python/picongpu/quick/pypicongpu/species/constant/elementproperties.py +++ b/test/python/picongpu/quick/pypicongpu/species/constant/elementproperties.py @@ -17,7 +17,7 @@ class TestElementProperties(unittest.TestCase): def test_basic(self): """basic operation""" ep = ElementProperties() - ep.element = Element.H + ep.element = Element("H") ep.check() @@ -29,7 +29,7 @@ def test_basic(self): def test_rendering(self): """members are exposed""" ep = ElementProperties() - ep.element = Element.N + ep.element = Element("N") context = ep.get_rendering_context() @@ -42,7 +42,7 @@ def test_mandatory(self): with self.assertRaises(Exception): ep.check() - ep.element = Element.H + ep.element = Element("H") # now passes ep.check() @@ -51,6 +51,6 @@ def test_typesafety(self): """typesafety is ensured""" ep = ElementProperties() - for invalid in [None, "H", 1, [], {}]: + for invalid in [None, 1, "H", [], {}]: with self.assertRaises(typeguard.TypeCheckError): ep.element = invalid diff --git a/test/python/picongpu/quick/pypicongpu/species/initmanager.py b/test/python/picongpu/quick/pypicongpu/species/initmanager.py index 847e6537dd..8259819d50 100644 --- a/test/python/picongpu/quick/pypicongpu/species/initmanager.py +++ b/test/python/picongpu/quick/pypicongpu/species/initmanager.py @@ -879,7 +879,7 @@ def test_set_bound_electrons_passthrough(self): ) ionizers_const.ionization_model_list[0].ionization_electron_species = electron element_const = species.constant.ElementProperties() - element_const.element = species.util.Element.N + element_const.element = species.util.Element("N") ion.constants = [ionizers_const, element_const] ion_op = species.operation.SetBoundElectrons() diff --git a/test/python/picongpu/quick/pypicongpu/species/util/element.py b/test/python/picongpu/quick/pypicongpu/species/util/element.py index 7ef2211304..32ca9eb071 100644 --- a/test/python/picongpu/quick/pypicongpu/species/util/element.py +++ b/test/python/picongpu/quick/pypicongpu/species/util/element.py @@ -7,65 +7,90 @@ from picongpu.pypicongpu.species.util import Element +import scipy + import unittest from picongpu.pypicongpu.rendering import RenderedObject -import re -import typeguard class TestElement(unittest.TestCase): - def test_exists(self): - """there is at least one element""" - self.assertNotEqual([], list(Element)) - - def test_openpmd_names(self): - """elements can be requested by openPMD name""" - expected_element_by_name = { - "H": Element.H, - "He": Element.He, - "N": Element.N, - } - for name, element in expected_element_by_name.items(): - self.assertEqual(element, Element.get_by_openpmd_name(name)) - - for invalid_type in [[], None, 3]: - with self.assertRaises(typeguard.TypeCheckError): - Element.get_by_openpmd_name(invalid_type) - - for unknown_name in ["", " H", "abc"]: - with self.assertRaisesRegex(NameError, ".*unkown.*"): - Element.get_by_openpmd_name(unknown_name) - - def test_periodic_table_names(self): - """names must follow the periodic table""" - element_re = re.compile("^[A-Z][a-z]?$") - for element in list(Element): - self.assertTrue(element_re.match(element.name)) + def setUp(self): + # create test case data + self.test_element = ["H", "#2H", "Cu", "#12C", "C"] + self.name = ["H", "D", "Cu", "C", "C"] + self.picongpu_names = ["Hydrogen", "Deuterium", "Copper", "Carbon", "Carbon"] + self.mass = [ + 1.00794 * scipy.constants.atomic_mass, + 2.014101778 * scipy.constants.atomic_mass, + 63.546 * scipy.constants.atomic_mass, + 12.0 * scipy.constants.atomic_mass, + 12.0107 * scipy.constants.atomic_mass, + ] + self.charge = [ + 1.0 * scipy.constants.elementary_charge, + 1.0 * scipy.constants.elementary_charge, + 27.0 * scipy.constants.elementary_charge, + 6.0 * scipy.constants.elementary_charge, + 6.0 * scipy.constants.elementary_charge, + ] + self.atomic_number = [1, 1, 29, 6, 6] + + def test_parse_openpmd(self): + valid_test_strings = ["#3H", "#15He", "#1H", "#3He", "#56Cu"] + mass_number_results = [3, 15, 1, 3, 56] + name_results = ["H", "He", "H", "He", "Cu"] + + for i, string in enumerate(valid_test_strings): + mass_number, name = Element.parse_openpmd_isotopes(string) + self.assertEqual(name, name_results[i]) + self.assertEqual(mass_number, mass_number_results[i]) + + invalid_test_strings = ["#Htest", "#He3", "#Cu-56", "H3", "Fe-56"] + for i, string in enumerate(invalid_test_strings): + with self.assertRaisesRegex(ValueError, string + " is not a valid openPMD isotope descriptor"): + name, massNumber = Element.parse_openpmd_isotopes(string) + + def test_basic_use(self): + for name in self.test_element: + Element(name) + + def test_symbol(self): + for openpmd_name, name in zip(self.test_element, self.name): + e = Element(openpmd_name) + self.assertEqual(e.get_symbol(), name) + + def test_is_element(self): + for name in self.test_element: + self.assertTrue(Element.is_element(name)) + self.assertFalse(Element.is_element("n")) def test_picongpu_names(self): """names must be translateable to picongpu""" - all_picongpu_names = set() - # all elements are defined - for element in list(Element): - picongpu_name = element.get_picongpu_name() + for openpmd_name, picongpu_name in zip(self.test_element, self.picongpu_names): + name_test = Element(openpmd_name).get_picongpu_name() self.assertNotEqual("", picongpu_name) - self.assertTrue(picongpu_name not in all_picongpu_names) - all_picongpu_names.add(picongpu_name) + self.assertEqual(name_test, picongpu_name) - def test_mass(self): + def test_get_mass(self): """all elements have mass""" - for element in list(Element): - self.assertTrue(0 < element.get_mass_si()) + for openpmd_name, mass in zip(self.test_element, self.mass): + self.assertAlmostEqual(Element(openpmd_name).get_mass_si(), mass) def test_charge(self): """all elements have charge""" - for element in list(Element): - self.assertTrue(0 < element.get_charge_si()) + for openpmd_name, charge in zip(self.test_element, self.charge): + self.assertAlmostEqual(Element(openpmd_name).get_charge_si(), charge) + + def test_atomic_number(self): + for openpmd_name, atomic_number in zip(self.test_element, self.atomic_number): + e = Element(openpmd_name) + self.assertEqual(e.get_atomic_number(), atomic_number) def test_rendering(self): """all elements can be rendered""" self.assertTrue(issubclass(Element, RenderedObject)) - for element in list(Element): - context = element.get_rendering_context() - self.assertEqual(context["symbol"], element.name) - self.assertEqual(context["picongpu_name"], element.get_picongpu_name()) + for openpmd_name in self.test_element: + e = Element(openpmd_name) + context = e.get_rendering_context() + self.assertEqual(context["symbol"], e.get_symbol()) + self.assertEqual(context["picongpu_name"], e.get_picongpu_name()) From dc564f171a6875081ff9e00f97267b8074dcdea2 Mon Sep 17 00:00:00 2001 From: BrianMarre Date: Fri, 26 Jul 2024 16:57:20 +0200 Subject: [PATCH 11/20] 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 +++---------- lib/python/picongpu/pypicongpu/runner.py | 2 +- .../ionizationcurrent/ionizationcurrent.py | 3 + .../constant/ionizationcurrent/none_.py | 3 + .../ionizationmodel/ionizationmodel.py | 4 +- .../pypicongpu/species/util/element.py | 15 +- share/ci/bash.profile | 3 + .../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 +- 27 files changed, 407 insertions(+), 568 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 cc4d096ad6..46459619de 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 645bff6338..0e31f10c63 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 572fae7ee9..ae5a3f425d 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 5055928e92..bdc7638fdd 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 0cc4858f6f..da988288f9 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 90d6ba4753..60634fee8d 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 5f34e2f230..5f484202fe 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 11a1da1746..bb30fe2213 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 168925aaf9..fa470d5769 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 e2e31cf36b..a3a4c36fac 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 552571a9a6..b97405c97f 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 0000000000..31691ba390 --- /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 d91a5be731..d3d8ea76e2 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 dc93a7d5b0..6820d16c43 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/runner.py b/lib/python/picongpu/pypicongpu/runner.py index ecdf1432d4..c51f42e238 100644 --- a/lib/python/picongpu/pypicongpu/runner.py +++ b/lib/python/picongpu/pypicongpu/runner.py @@ -333,7 +333,7 @@ def __run(self): chdir(self.setup_dir) runArgs( "PIConGPU", - "tbg -s bash -c etc/picongpu/N.cfg -t " "etc/picongpu/bash/mpiexec.tpl".split(" ") + [self.run_dir], + r"tbg -s bash -c etc/picongpu/N.cfg -t $PIC_SYSTEM_TEMPLATE_PATH/mpiexec.tpl".split(" ") + [self.run_dir], ) def generate(self, printDirToConsole=False): diff --git a/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/ionizationcurrent.py b/lib/python/picongpu/pypicongpu/species/constant/ionizationcurrent/ionizationcurrent.py index 6cbd45ddd4..a00c469f62 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 c99d0f6a3d..7019a0db44 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 8ed4644ac2..bbf1276f9a 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 3936e73da5..491b3de932 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/share/ci/bash.profile b/share/ci/bash.profile index 72a6f1e439..a52cc992b5 100755 --- a/share/ci/bash.profile +++ b/share/ci/bash.profile @@ -22,6 +22,9 @@ export CMAKE_PREFIX_PATH=$ADIOS2_ROOT:$CMAKE_PREFIX_PATH export PATH=$ADIOS2_ROOT/bin:$PATH export LD_LIBRARY_PATH=$ADIOS2_ROOT/lib:$LD_LIBRARY_PATH +# set environment variable for path to tpls for PyPIConGPU runner +export PIC_SYSTEM_TEMPLATE_PATH=${PIC_SYSTEM_TEMPLATE_PATH:-"etc/picongpu/bash"} + if [ -z "$DISABLE_ISAAC" ] ; then export ICET_ROOT=/opt/icet/2.9.0 export CMAKE_PREFIX_PATH=$ICET_ROOT/lib:$CMAKE_PREFIX_PATH diff --git a/test/python/picongpu/quick/picmi/simulation.py b/test/python/picongpu/quick/picmi/simulation.py index 9b6a503b73..347c685644 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 1614fee951..624784e84a 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 a8244b6ad0..7ceb100129 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 ce8d860115..9f4fd906b0 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 a6d608cde0..ace9dfee20 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 37b8bf19be..5105448d2d 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 32ca9eb071..49a9e65e68 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): From b96f5fa883a00748265d6fee2f72b9cdda71e78e Mon Sep 17 00:00:00 2001 From: BrianMarre Date: Mon, 29 Jul 2024 10:38:37 +0200 Subject: [PATCH 12/20] add are bones documentation --- docs/source/usage/picmi/intro.rst | 68 ++++++++++++++++--- .../python/picongpu/compiling/distribution.py | 1 + test/python/picongpu/compiling/species.py | 1 + 3 files changed, 59 insertions(+), 11 deletions(-) diff --git a/docs/source/usage/picmi/intro.rst b/docs/source/usage/picmi/intro.rst index 204f4a3aea..07a60af1ce 100644 --- a/docs/source/usage/picmi/intro.rst +++ b/docs/source/usage/picmi/intro.rst @@ -113,21 +113,49 @@ Parameters/Methods prefixed with ``picongpu_`` are PIConGPU-exclusive. - **Simulation** - - ``__init__(..., picongpu_template_dir)``: - Specify the template dir to use for code generation, + not supported methods: + + - ``add_interaction(self, interaction)``: + The PIConGPU PICMI interface does not support the PICMI interaction specification, due to PICMI standard ambiguities. + Instead you must use the PIConGPU specific ``Interaction`` interface described below. + + additional constructor/configuration options: + + - ``picongpu_template_dir``: + Specify the template directory to use for code generation, please refer to :ref:`the documentation on the matter for details ` - - ``__init__(..., picongpu_typical_ppc)`` typical ppc to be used for normalization in PIConGPU + - ``picongpu_typical_ppc``: + typical particle per cell(ppc) to be used for normalization in PIConGPU, if not set explicitly, PIConGPU will use the median ppc of all defined species + - ``picongpu_moving_window_move_point``: + portion of the simulation window a light ray reaches from the time of the start of the simulation until the simulation window begins to move. + + .. warning:: + + If the moving window is active, one gpu row in y direction is reserved for initializing new spaces, thereby reducing the simulation window size accordingly + + - ``picongpu_moving_window_stop_iteration``: + iteration at which to stop moving the simulation window + - ``picongpu_interaction``: + ``Interaction`` object specifying all interactions of the simulation, i.e. all ionization models and their configurations and so on. + This replaces the PICMI ``add_interaction`` method. + + additional method arguments: + - ``write_input_file(..., pypicongpu_simulation)``: use a :ref:`PyPIConGPU simulation` object instead of an PICMI- simulation object to generate a PIConGPU input. + + additional methods: + - ``get_as_pypicongpu()``: convert the PICMI simulation object to an equivalent :ref:`PyPIConGPU ` simulation object. - ``picongpu_get_runner()``: - Retrieve a :ref:`PyPIConGPU Runner ` for running a PIConGPU simulation from Python, **not recommended** + Retrieve a :ref:`PyPIConGPU Runner ` for running a PIConGPU simulation from Python, **not recommended, see :ref:`PICMI setup generation `**. - ``picongpu_add_custom_user_input()``: pass custom user input to the code generation. This may be used in conjunction with custom templates to change the code generation. See :ref:`PICMI custom code generation` for the documentation on using custom input. + - **Grid** - ``picongpu_n_gpus``: @@ -150,17 +178,35 @@ Parameters/Methods prefixed with ``picongpu_`` are PIConGPU-exclusive. - **Species** - - ``picongpu_ionization_electrons``: - Electron species to use for ionization. - Optional, will be guessed if possible. - - ``picongpu_fully_ionized``: - When defining an element (using ``particle_type``) it may or may not be ionizable + - ``picongpu_fixed_charge``: + When defining an ion species using ``particle_type`` it may or may not be ionizable + + - to **enable** ionization add an ionization model to the Interaction object of the simulation and set the initial charge state using ``charge_state``. + - to **disable** ionization set ``picongpu_fixed_charge=True``, this will fix the charge of particles of this species for entire simulation. - - to **enable** ionization simulation set ``charge_state`` to an integer - - to **disable** ionization (ions are only core without electrons) set ``picongpu_fully_ionized=True`` + ``picongpu_fixed_charge`` maybe combined with ``charge_state`` to control which charge state is to used for the ion species If neither is set a warning is printed prompting for either of the options above. +- **Interaction** + Configuration of the PIC-algorithm extensions, example of use as follows: + + .. code:: python + + from picongpu import picmi + from picongpu.interaction.ionization.fieldionization import ADK, ADKVariant + from picongpu.interaction import Interaction + + e = picmi.Species(name="e", particle_type="electron") + nitrogen = picmi.Species(name="nitrogen", particle_type="N", charge_state=2) + + ADK_ionization = ADK(ADK_variant = ADKVariant.LinearPolarization, ion_species = nitrogen, ionization_electron_species=e) + interaction = Interaction(ground_state_ionizaion_model_list=[ADK_Ionization]) + + sim = picmi.simulation(picongpu_interaction=interaction) + sim.add_species(e, ...) + sim.add_species(nitrogen, ...) + Output ^^^^^^ Output is currently **not configurable** for picongpu using the PICMI interface. diff --git a/test/python/picongpu/compiling/distribution.py b/test/python/picongpu/compiling/distribution.py index 38d8118643..fc52acccd6 100644 --- a/test/python/picongpu/compiling/distribution.py +++ b/test/python/picongpu/compiling/distribution.py @@ -36,6 +36,7 @@ def _compile_distribution(self, distribution): particle_type="H", charge_state=0, initial_distribution=distribution, + picongpu_fixed_charge=True, ) self.sim.add_species(species_hydrogen, random_layout) runner = Runner(self.sim) diff --git a/test/python/picongpu/compiling/species.py b/test/python/picongpu/compiling/species.py index f59471f2ac..2d902de3ac 100644 --- a/test/python/picongpu/compiling/species.py +++ b/test/python/picongpu/compiling/species.py @@ -52,6 +52,7 @@ def test_hydrogen_atoms(self): charge_state=0, initial_distribution=uniform_dist, density_scale=3, + picongpu_fixed_charge=True, ) random_layout = picmi.PseudoRandomLayout(n_macroparticles_per_cell=2) From 9bc4e7b115cd5f17fd130c816bba8cb52041faa0 Mon Sep 17 00:00:00 2001 From: BrianMarre Date: Wed, 31 Jul 2024 11:23:09 +0200 Subject: [PATCH 13/20] reviewer suggestions --- .../picongpu/picmi/interaction/ionization/__init__.py | 1 + .../picongpu/picmi/predefinedparticletypeproperties.py | 6 +----- lib/python/picongpu/picmi/simulation.py | 5 +---- lib/python/picongpu/picmi/species.py | 4 ++-- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/lib/python/picongpu/picmi/interaction/ionization/__init__.py b/lib/python/picongpu/picmi/interaction/ionization/__init__.py index 1db647ca4f..da88ca3413 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/__init__.py +++ b/lib/python/picongpu/picmi/interaction/ionization/__init__.py @@ -1,6 +1,7 @@ from .ionizationmodel import IonizationModel from .groundstateionizationmodel import GroundStateIonizationModel from . import fieldionization +from . import electroniccollisionalequilibrium __all__ = [ "IonizationModel", diff --git a/lib/python/picongpu/picmi/predefinedparticletypeproperties.py b/lib/python/picongpu/picmi/predefinedparticletypeproperties.py index 31691ba390..bd2cb29f6c 100644 --- a/lib/python/picongpu/picmi/predefinedparticletypeproperties.py +++ b/lib/python/picongpu/picmi/predefinedparticletypeproperties.py @@ -119,8 +119,4 @@ } 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) +non_element_particle_type_properties.update(**_quarks, **_leptons, **_neutrinos, **_nucleons, **_gauge_bosons) diff --git a/lib/python/picongpu/picmi/simulation.py b/lib/python/picongpu/picmi/simulation.py index d3d8ea76e2..5f1840601c 100644 --- a/lib/python/picongpu/picmi/simulation.py +++ b/lib/python/picongpu/picmi/simulation.py @@ -368,10 +368,7 @@ def write_input_file( def picongpu_add_custom_user_input(self, custom_user_input: pypicongpu.customuserinput.InterfaceCustomUserInput): """add custom user input to previously stored input""" - if self.picongpu_custom_user_input is None: - self.picongpu_custom_user_input = [custom_user_input] - else: - self.picongpu_custom_user_input.append(custom_user_input) + self.picongpu_custom_user_input = (self.picongpu_custom_user_input + []) + [custom_user_input] def add_interaction(self, interaction) -> None: pypicongpu.util.unsupported( diff --git a/lib/python/picongpu/picmi/species.py b/lib/python/picongpu/picmi/species.py index 6820d16c43..90a7d1fe9d 100644 --- a/lib/python/picongpu/picmi/species.py +++ b/lib/python/picongpu/picmi/species.py @@ -8,7 +8,7 @@ from .. import pypicongpu from ..pypicongpu.species.util.element import Element from .interaction import InteractionInterface -from .predefinedparticletypeproperties import non_element_particle_type_properties, _PropertyTuple +from .predefinedparticletypeproperties import non_element_particle_type_properties import picmistandard @@ -33,7 +33,7 @@ class Species(picmistandard.PICMI_Species): @attention ONLY set non-element particles here, all other are handled by element """ - __non_element_particle_types: dict[str, _PropertyTuple] = __non_element_particle_type_properties.keys() + __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]) From a7cd90c119f8489f3aa24cb034e0248418d86372 Mon Sep 17 00:00:00 2001 From: mafshari Date: Wed, 31 Jul 2024 16:19:59 +0200 Subject: [PATCH 14/20] fixes --- 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 ------ .../__init__.py | 0 .../thomasfermi.py | 3 + .../ionization/fieldionization/ADK.py | 2 + .../ionization/fieldionization/BSI.py | 22 ++--- .../ionization/fieldionization/keldysh.py | 5 +- .../ionization/groundstateionizationmodel.py | 2 + .../interaction/ionization/ionizationmodel.py | 15 ++- lib/python/picongpu/picmi/simulation.py | 96 +++++++++++------- lib/python/picongpu/picmi/species.py | 86 +++++----------- .../pypicongpu/rendering/renderedobject.py | 3 +- .../pypicongpu/species/constant/constant.py | 4 +- .../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 +++++++++---------- 33 files changed, 267 insertions(+), 266 deletions(-) delete mode 100644 lib/python/picongpu/picmi/interaction/interactioninterface.py rename lib/python/picongpu/picmi/interaction/ionization/{electroniccollisonalequilibrium => electroniccollisionalequilibrium}/__init__.py (100%) rename lib/python/picongpu/picmi/interaction/ionization/{electroniccollisonalequilibrium => electroniccollisionalequilibrium}/thomasfermi.py (96%) 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 62f5e813c3..3873f2d73f 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 1a8ece0260..4951142a0a 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 053f81d670..0a34f541a6 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 e7034b1ede..a84c0f431b 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 e1088d4ca3..69896a4ddf 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 0e31f10c63..7c914be8dc 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 ae5a3f425d..0000000000 --- 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/electroniccollisonalequilibrium/__init__.py b/lib/python/picongpu/picmi/interaction/ionization/electroniccollisionalequilibrium/__init__.py similarity index 100% rename from lib/python/picongpu/picmi/interaction/ionization/electroniccollisonalequilibrium/__init__.py rename to lib/python/picongpu/picmi/interaction/ionization/electroniccollisionalequilibrium/__init__.py diff --git a/lib/python/picongpu/picmi/interaction/ionization/electroniccollisonalequilibrium/thomasfermi.py b/lib/python/picongpu/picmi/interaction/ionization/electroniccollisionalequilibrium/thomasfermi.py similarity index 96% rename from lib/python/picongpu/picmi/interaction/ionization/electroniccollisonalequilibrium/thomasfermi.py rename to lib/python/picongpu/picmi/interaction/ionization/electroniccollisionalequilibrium/thomasfermi.py index bdc7638fdd..ca11d40a7e 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/electroniccollisonalequilibrium/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 da988288f9..20a65a970e 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 60634fee8d..3eb8387433 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 @@ -30,19 +29,20 @@ class BSI(FieldIonization): MODEL_NAME: str = "BSI" - BSI_extensions: list[BSIExtension] + 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 self.BSI_extensions == [BSIExtension.StarkShift]: + if len(self.BSI_extensions) > 1: + pypicongpu.util.unsupported("more than one BSI_extension, will use first entry only") + + if self.BSI_extensions[0] is BSIExtension.StarkShift: return ionizationmodel.BSIStarkShifted(ionization_current=None_()) - if self.BSI_extensions == [BSIExtension.EffectiveZ]: + if self.BSI_extensions[0] is BSIExtension.EffectiveZ: return ionizationmodel.BSIEffectiveZ(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]}") + 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 fa470d5769..7cc79f30f4 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 a3a4c36fac..68a596be3e 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 b97405c97f..a3d772ffad 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 5f1840601c..c456c5a541 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) @@ -368,7 +398,7 @@ def write_input_file( def picongpu_add_custom_user_input(self, custom_user_input: pypicongpu.customuserinput.InterfaceCustomUserInput): """add custom user input to previously stored input""" - self.picongpu_custom_user_input = (self.picongpu_custom_user_input + []) + [custom_user_input] + self.picongpu_custom_user_input = (self.picongpu_custom_user_input or []) + [custom_user_input] def add_interaction(self, interaction) -> None: pypicongpu.util.unsupported( diff --git a/lib/python/picongpu/picmi/species.py b/lib/python/picongpu/picmi/species.py index 90a7d1fe9d..c5ade6ac2a 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 46c064fe85..1f249ca332 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/constant/constant.py b/lib/python/picongpu/pypicongpu/species/constant/constant.py index 01910842eb..9b50c52bf7 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/constant.py +++ b/lib/python/picongpu/pypicongpu/species/constant/constant.py @@ -51,8 +51,8 @@ def __eq__(self, other) -> bool: if key not in other.__dict__: return False - for key, value in self.__dict__: - if self.value != other.__dict__[key]: + for key, value in self.__dict__.items(): + if value != other.__dict__[key]: return False return True diff --git a/lib/python/picongpu/pypicongpu/species/initmanager.py b/lib/python/picongpu/pypicongpu/species/initmanager.py index 6de5f677d2..01a6873b86 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 6fd4327caf..adc75ade30 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 b743dfbd3d..53a475d5fb 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 1b21e51aab..8e66890c64 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 1a541597a4..7f69ab32cc 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 fe5e2b6757..e7fd901403 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 63d38e45fd..f3bf3e8144 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 9c932faa3a..7c23e10070 100644 --- a/share/picongpu/pypicongpu/template/include/picongpu/param/particle.param.mustache +++ b/share/picongpu/pypicongpu/template/include/picongpu/param/particle.param.mustache @@ -104,10 +104,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 e97097e8ef..15f4a730b1 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 347c685644..6e2cc8d35f 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 624784e84a..496f1189c2 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 8259819d50..9034c5ac3f 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 edf495cbc3..1876a93608 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 ace9dfee20..4cc78673d7 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"]) From 81ebc2e2f7315694bfd06a2e593f8957e5932be6 Mon Sep 17 00:00:00 2001 From: BrianMarre Date: Sat, 3 Aug 2024 20:55:01 +0200 Subject: [PATCH 15/20] add PICMI laser wakefield example --- docs/source/usage/picmi/intro.rst | 36 ++-- .../examples/laser_wakefield/.gitignore | 1 + .../examples/laser_wakefield/main.py | 202 ++++++++++++++++++ .../examples/warm_plasma/.gitignore | 2 +- .../pypicongpu/examples/warm_plasma/main.py | 11 +- 5 files changed, 233 insertions(+), 19 deletions(-) create mode 100644 share/picongpu/pypicongpu/examples/laser_wakefield/.gitignore create mode 100644 share/picongpu/pypicongpu/examples/laser_wakefield/main.py diff --git a/docs/source/usage/picmi/intro.rst b/docs/source/usage/picmi/intro.rst index 07a60af1ce..d01bc62734 100644 --- a/docs/source/usage/picmi/intro.rst +++ b/docs/source/usage/picmi/intro.rst @@ -48,16 +48,25 @@ After you have installed the dependencies you must include the PIConGPU PICMI im .. note:: Above, we used ``$PICSRC`` as a short hand for the path to picongpu's source code directory, provided from your shell environment if a pre-configured profile is used. -After you have installed all PICMI dependencies, simply create a user script, see :ref:`here `, and generate a picongpu setup, see :ref:`generating a PIConGPU setup with PICMI `. +After you have installed all PICMI dependencies, simply create a user script, see the :ref:`warm plasma ` and :ref:`laser wakefield ` examples, and generate a picongpu setup, see :ref:`generating a PIConGPU setup with PICMI `. Example User Script for a warm plasma setup: -------------------------------------- -.. _example_PICMI_setup: +.. _example_PICMI_setup_warm_plasma: .. literalinclude:: ../../../../share/picongpu/pypicongpu/examples/warm_plasma/main.py :language: python -Creates a directory ``generated_input``, where you can run ``pic-build`` and subsequently ``tbg``. +Creates a directory ``warm_plasma``, where you can run ``pic-build`` and subsequently ``tbg``. + +Example User Script for a laser wakefield setup: +-------------------------------------- +.. _example_PICMI_setup_lwfa: + +.. literalinclude:: ../../../../share/picongpu/pypicongpu/examples/laser_wakefield/main.py + :language: python + +Creates a directory ``LWFA``, where you can run ``pic-build`` and subsequently ``tbg``. Generation of PIConGPU setups with PICMI ---------------------------------------- @@ -188,24 +197,17 @@ Parameters/Methods prefixed with ``picongpu_`` are PIConGPU-exclusive. If neither is set a warning is printed prompting for either of the options above. -- **Interaction** - Configuration of the PIC-algorithm extensions, example of use as follows: - - .. code:: python +Ionization: +^^^^^^^^^^^ +The PIConGPU PICMI interface currently supports the configuration of ionization only through a picongpu specific PICMI extension, not the in the PICMI standard defined interface, due to the lack of standardization of ionization algorithm names in the PICMI standard. - from picongpu import picmi - from picongpu.interaction.ionization.fieldionization import ADK, ADKVariant - from picongpu.interaction import Interaction +Use the **Interaction** interface - e = picmi.Species(name="e", particle_type="electron") - nitrogen = picmi.Species(name="nitrogen", particle_type="N", charge_state=2) - ADK_ionization = ADK(ADK_variant = ADKVariant.LinearPolarization, ion_species = nitrogen, ionization_electron_species=e) - interaction = Interaction(ground_state_ionizaion_model_list=[ADK_Ionization]) +- **Interaction** + picongpu specific configuration of PIC-algorithm extensions. - sim = picmi.simulation(picongpu_interaction=interaction) - sim.add_species(e, ...) - sim.add_species(nitrogen, ...) + - ``__init__(ground_state_ionizaion_model_list= )`` Output ^^^^^^ diff --git a/share/picongpu/pypicongpu/examples/laser_wakefield/.gitignore b/share/picongpu/pypicongpu/examples/laser_wakefield/.gitignore new file mode 100644 index 0000000000..1142186953 --- /dev/null +++ b/share/picongpu/pypicongpu/examples/laser_wakefield/.gitignore @@ -0,0 +1 @@ +LWFA diff --git a/share/picongpu/pypicongpu/examples/laser_wakefield/main.py b/share/picongpu/pypicongpu/examples/laser_wakefield/main.py new file mode 100644 index 0000000000..efdb42ef49 --- /dev/null +++ b/share/picongpu/pypicongpu/examples/laser_wakefield/main.py @@ -0,0 +1,202 @@ +""" +This file is part of PIConGPU. +Copyright 2024 PIConGPU contributors +Authors: Masoud Afshari, Brian Edward Marre +License: GPLv3+ +""" + +from picongpu import picmi +from picongpu import pypicongpu +import numpy as np + +""" +@file PICMI user script reproducing the PIConGPU LWFA example + +This Python script is example PICMI user script reproducing the LaserWakefield example setup, based on 8.cfg. +""" + + +# generation modifiers +ENABLE_IONS = True +ENABLE_IONIZATION = True +ADD_CUSTOM_INPUT = True +OUTPUT_DIRECTORY_PATH = "LWFA" + +numberCells = np.array([192, 2048, 192]) +cellSize = np.array([0.1772e-6, 0.4430e-7, 0.1772e-6]) # unit: meter) + +# Define the simulation grid based on grid.param +grid = picmi.Cartesian3DGrid( + picongpu_n_gpus=[2, 4, 1], + number_of_cells=numberCells.tolist(), + lower_bound=[0, 0, 0], + upper_bound=(numberCells * cellSize).tolist(), + lower_boundary_conditions=["open", "open", "open"], + upper_boundary_conditions=["open", "open", "open"], +) + +gaussianProfile = picmi.distribution.GaussianDistribution( + density=1.0e25, + center_front=8.0e-5, + sigma_front=8.0e-5, + center_rear=10.0e-5, + sigma_rear=8.0e-5, + factor=-1.0, + power=4.0, + vacuum_cells_front=50, +) + +# for particle type see https://github.com/openPMD/openPMD-standard/blob/upcoming-2.0.0/EXT_SpeciesType.md +electrons = picmi.Species(particle_type="electron", name="electron", initial_distribution=gaussianProfile) + +hydrogen_ionization = picmi.Species( + particle_type="H", name="hydrogen", charge_state=0, initial_distribution=gaussianProfile +) + +hydrogen_fully_ionized = picmi.Species( + particle_type="H", name="hydrogen", picongpu_fixed_charge=True, initial_distribution=gaussianProfile +) + +solver = picmi.ElectromagneticSolver( + grid=grid, + method="Yee", +) + +laser = picmi.GaussianLaser( + wavelength=0.8e-6, + waist=5.0e-6 / 1.17741, + duration=5.0e-15, + propagation_direction=[0.0, 1.0, 0.0], + polarization_direction=[1.0, 0.0, 0.0], + focal_position=[float(numberCells[0] * cellSize[0] / 2.0), 4.62e-5, float(numberCells[2] * cellSize[2] / 2.0)], + centroid_position=[float(numberCells[0] * cellSize[0] / 2.0), 0.0, float(numberCells[2] * cellSize[2] / 2.0)], + picongpu_polarization_type=pypicongpu.laser.GaussianLaser.PolarizationType.CIRCULAR, + a0=8.0, + picongpu_phase=0.0, +) + +randomLayout = picmi.PseudoRandomLayout(n_macroparticles_per_cell=2) + +# Initialize particles based on speciesInitialization.param +# simulation schema : https://github.com/BrianMarre/picongpu/blob/2ddcdab4c1aca70e1fc0ba02dbda8bd5e29d98eb/share/picongpu/pypicongpu/schema/simulation.Simulation.json + +if not ENABLE_IONIZATION: + hydrogen = hydrogen_fully_ionized + interaction = None +else: + hydrogen = hydrogen_ionization + adk_ionization_model = picmi.ADK( + ADK_variant=picmi.ADKVariant.CircularPolarization, + ion_species=hydrogen_ionization, + ionization_electron_species=electrons, + ionization_current=None, + ) + + bsi_effectiveZ_ionization_model = picmi.BSI( + BSI_extensions=[picmi.BSIExtension.EffectiveZ], + ion_species=hydrogen_ionization, + ionization_electron_species=electrons, + ionization_current=None, + ) + + interaction = picmi.Interaction( + ground_state_ionization_model_list=[adk_ionization_model, bsi_effectiveZ_ionization_model] + ) + +sim = picmi.Simulation( + solver=solver, + max_steps=4000, + time_step_size=1.39e-16, + picongpu_moving_window_move_point=0.9, + picongpu_interaction=interaction, +) + +sim.add_species(electrons, layout=randomLayout) + +if ENABLE_IONS: + sim.add_species(hydrogen, layout=randomLayout) + +sim.add_laser(laser, None) + +# additional non standardized custom user input +# only active if custom templates are used + +# for generating setup with custom input see standard implementation, +# see https://picongpu.readthedocs.io/en/latest/usage/picmi/custom_template.html +if ADD_CUSTOM_INPUT: + min_weight_input = pypicongpu.customuserinput.CustomUserInput() + min_weight_input.addToCustomInput({"minimum_weight": 10.0}, "minimum_weight") + sim.picongpu_add_custom_user_input(min_weight_input) + + output_configuration = pypicongpu.customuserinput.CustomUserInput() + output_configuration.addToCustomInput( + { + "png_plugin_data_list": "['Ex', 'Ey', 'Ez', 'Bx', 'By', 'Bz', 'Jx', 'Jy', 'Jz']", + "png_plugin_SCALE_IMAGE": 1.0, + "png_plugin_SCALE_TO_CELLSIZE": True, + "png_plugin_WHITE_BOX_PER_GPU": False, + "png_plugin_EM_FIELD_SCALE_CHANNEL1": 7, + "png_plugin_EM_FIELD_SCALE_CHANNEL2": -1, + "png_plugin_EM_FIELD_SCALE_CHANNEL3": -1, + "png_plugin_CUSTOM_NORMALIZATION_SI": "5.0e12 / constants.c, 5.0e12, 15.0", + "png_plugin_PRE_PARTICLE_DENS_OPACITY": 0.25, + "png_plugin_PRE_CHANNEL1_OPACITY": 1.0, + "png_plugin_PRE_CHANNEL2_OPACITY": 1.0, + "png_plugin_PRE_CHANNEL3_OPACITY": 1.0, + "png_plugin_preParticleDensCol": "colorScales::grayInv", + "png_plugin_preChannel1Col": "colorScales::green", + "png_plugin_preChannel2Col": "colorScales::none", + "png_plugin_preChannel3Col": "colorScales::none", + "png_plugin_preChannel1": "field_E.x() * field_E.x();", + "png_plugin_preChannel2": "field_E.y()", + "png_plugin_preChannel3": "-1.0_X * field_E.y()", + "png_plugin_period": 100, + "png_plugin_axis": "yx", + "png_plugin_slicePoint": 0.5, + "png_plugin_species_name": "electron", + "png_plugin_folder_name": "pngElectronsYX", + }, + "png plugin configuration", + ) + + output_configuration.addToCustomInput( + { + "energy_histogram_species_name": "electron", + "energy_histogram_period": 100, + "energy_histogram_bin_count": 1024, + "energy_histogram_min_energy": 0.0, + "energy_histogram_maxEnergy": 1000.0, + "energy_histogram_filter": "all", + }, + "energy histogram plugin configuration", + ) + + output_configuration.addToCustomInput( + { + "phase_space_species_name": "electron", + "phase_space_period": 100, + "phase_space_space": "y", + "phase_space_momentum": "py", + "phase_space_min": -1.0, + "phase_space_max": 1.0, + "phase_space_filter": "all", + }, + "phase space plugin configuration", + ) + + output_configuration.addToCustomInput( + {"opnePMD_period": 100, "opnePMD_file": "simData", "opnePMD_extension": "bp"}, "openPMD plugin configuration" + ) + + output_configuration.addToCustomInput( + {"checkpoint_period": 100, "checkpoint_backend": "openPMD", "checkpoint_restart_backend": "openPMD"}, + "checkpoint configuration", + ) + + output_configuration.addToCustomInput( + {"macro_particle_count_period": 100, "macro_particle_count_species_name": "electron"}, + "macro particle count plugin configuration", + ) + sim.picongpu_add_custom_user_input(output_configuration) + +sim.write_input_file(OUTPUT_DIRECTORY_PATH) diff --git a/share/picongpu/pypicongpu/examples/warm_plasma/.gitignore b/share/picongpu/pypicongpu/examples/warm_plasma/.gitignore index 470af14ac9..e0924a2bff 100644 --- a/share/picongpu/pypicongpu/examples/warm_plasma/.gitignore +++ b/share/picongpu/pypicongpu/examples/warm_plasma/.gitignore @@ -1 +1 @@ -generated_input +warm_plasma diff --git a/share/picongpu/pypicongpu/examples/warm_plasma/main.py b/share/picongpu/pypicongpu/examples/warm_plasma/main.py index cec188ff8a..ac2f096c27 100644 --- a/share/picongpu/pypicongpu/examples/warm_plasma/main.py +++ b/share/picongpu/pypicongpu/examples/warm_plasma/main.py @@ -1,5 +1,14 @@ +""" +This file is part of PIConGPU. +Copyright 2021-2024 PIConGPU contributors +Authors: Hannes Troepgen +License: GPLv3+ +""" + from picongpu import picmi +OUTPUT_DIRECTORY_PATH = "warm_plasma" + boundary_conditions = ["periodic", "periodic", "periodic"] grid = picmi.Cartesian3DGrid( # note: [x] * 3 == [x, x, x] @@ -36,4 +45,4 @@ layout = picmi.PseudoRandomLayout(n_macroparticles_per_cell=25) sim.add_species(electron, layout) -sim.write_input_file("generated_input") +sim.write_input_file(OUTPUT_DIRECTORY_PATH) From 78080565f114e541eb1cb6d0443177b509a886ba Mon Sep 17 00:00:00 2001 From: BrianMarre Date: Sat, 3 Aug 2024 20:59:20 +0200 Subject: [PATCH 16/20] add PICMI test for PICMI examples --- lib/python/picongpu/pypicongpu/runner.py | 9 +++- .../examples/laser_wakefield/main.py | 4 +- .../pypicongpu/examples/warm_plasma/main.py | 3 +- test/python/picongpu/.gitignore | 2 + test/python/picongpu/compiling/__init__.py | 1 + .../picongpu/compiling/compileexamples.py | 45 +++++++++++++++++++ 6 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 test/python/picongpu/compiling/compileexamples.py diff --git a/lib/python/picongpu/pypicongpu/runner.py b/lib/python/picongpu/pypicongpu/runner.py index c51f42e238..478675e4f7 100644 --- a/lib/python/picongpu/pypicongpu/runner.py +++ b/lib/python/picongpu/pypicongpu/runner.py @@ -248,7 +248,7 @@ def __helper_set_scratch_dir(self, scratch_dir: typing.Optional[str]) -> None: # try to retrieve from environment var if self.SCRATCH_ENV_NAME in environ: logging.info( - "loading scratch directory (implicitly) " "from environment var ${}".format(self.SCRATCH_ENV_NAME) + "loading scratch directory (implicitly) from environment var ${}".format(self.SCRATCH_ENV_NAME) ) self.scratch_dir = path.abspath(environ[self.SCRATCH_ENV_NAME]) else: @@ -333,7 +333,12 @@ def __run(self): chdir(self.setup_dir) runArgs( "PIConGPU", - r"tbg -s bash -c etc/picongpu/N.cfg -t $PIC_SYSTEM_TEMPLATE_PATH/mpiexec.tpl".split(" ") + [self.run_dir], + ( + ("tbg -s bash -c etc/picongpu/N.cfg -t " + environ["PIC_SYSTEM_TEMPLATE_PATH"] + "/mpiexec.tpl").split( + " " + ) + + [self.run_dir] + ), ) def generate(self, printDirToConsole=False): diff --git a/share/picongpu/pypicongpu/examples/laser_wakefield/main.py b/share/picongpu/pypicongpu/examples/laser_wakefield/main.py index efdb42ef49..6cbacf3432 100644 --- a/share/picongpu/pypicongpu/examples/laser_wakefield/main.py +++ b/share/picongpu/pypicongpu/examples/laser_wakefield/main.py @@ -22,6 +22,7 @@ ADD_CUSTOM_INPUT = True OUTPUT_DIRECTORY_PATH = "LWFA" + numberCells = np.array([192, 2048, 192]) cellSize = np.array([0.1772e-6, 0.4430e-7, 0.1772e-6]) # unit: meter) @@ -199,4 +200,5 @@ ) sim.picongpu_add_custom_user_input(output_configuration) -sim.write_input_file(OUTPUT_DIRECTORY_PATH) +if __name__ == "__main__": + sim.write_input_file(OUTPUT_DIRECTORY_PATH) diff --git a/share/picongpu/pypicongpu/examples/warm_plasma/main.py b/share/picongpu/pypicongpu/examples/warm_plasma/main.py index ac2f096c27..ffed6dbb10 100644 --- a/share/picongpu/pypicongpu/examples/warm_plasma/main.py +++ b/share/picongpu/pypicongpu/examples/warm_plasma/main.py @@ -45,4 +45,5 @@ layout = picmi.PseudoRandomLayout(n_macroparticles_per_cell=25) sim.add_species(electron, layout) -sim.write_input_file(OUTPUT_DIRECTORY_PATH) +if __name__ == "__main__": + sim.write_input_file(OUTPUT_DIRECTORY_PATH) diff --git a/test/python/picongpu/.gitignore b/test/python/picongpu/.gitignore index 24f843267b..9e89b04562 100644 --- a/test/python/picongpu/.gitignore +++ b/test/python/picongpu/.gitignore @@ -1,2 +1,4 @@ # generated by coverage.py .coverage +LWFA +warm_plasma diff --git a/test/python/picongpu/compiling/__init__.py b/test/python/picongpu/compiling/__init__.py index 69c38d59bf..0f5034d2a0 100644 --- a/test/python/picongpu/compiling/__init__.py +++ b/test/python/picongpu/compiling/__init__.py @@ -3,3 +3,4 @@ from .species import * # pyflakes.ignore from .distribution import * # pyflakes.ignore from .simulation import * # pyflakes.ignore +from .compileexamples import * # pyflakes.ignore diff --git a/test/python/picongpu/compiling/compileexamples.py b/test/python/picongpu/compiling/compileexamples.py new file mode 100644 index 0000000000..6db41e8b17 --- /dev/null +++ b/test/python/picongpu/compiling/compileexamples.py @@ -0,0 +1,45 @@ +""" +This file is part of PIConGPU. +Copyright 2021-2023 PIConGPU contributors +Authors: Brian Edward Marre +License: GPLv3+ +""" + +from picongpu import pypicongpu + +import importlib.util +import os + +import unittest + + +class TestExamples(unittest.TestCase): + def load_example_script(self, path): + """load and execute example PICMI script from given path""" + module_spec = importlib.util.spec_from_file_location("example", path) + module = importlib.util.module_from_spec(module_spec) + module_spec.loader.exec_module(module) + + sim = module.sim + + return sim + + def build_simulation(self, sim): + """build the given instance of simulation""" + runner = pypicongpu.Runner(sim) + runner.generate(printDirToConsole=True) + runner.build() + + def test_LWFA_example(self): + """generate a PIConGPU setup from the laser_wakefield PICMI example and compile the setup""" + sim = self.load_example_script( + os.environ["PICSRC"] + "/share/picongpu/pypicongpu/examples/laser_wakefield/main.py" + ) + + self.build_simulation(sim) + + def test_warm_plasma_example(self): + """generate a PIConGPU setup from the warm_plasma PICMI example and compile the setup""" + sim = self.load_example_script(os.environ["PICSRC"] + "/share/picongpu/pypicongpu/examples/warm_plasma/main.py") + + self.build_simulation(sim) From bf389150982cd4d559976508e9138ac23bfade02 Mon Sep 17 00:00:00 2001 From: BrianMarre Date: Thu, 8 Aug 2024 15:39:07 +0200 Subject: [PATCH 17/20] reviewer comments --- docs/source/usage/picmi/intro.rst | 1 + .../picmi/interaction/ionization/ionizationmodel.py | 2 +- .../picmi/predefinedparticletypeproperties.py | 5 ++--- .../picongpu/pypicongpu/species/constant/constant.py | 12 ++---------- .../pypicongpu/examples/laser_wakefield/.gitignore | 1 + .../pypicongpu/examples/warm_plasma/.gitignore | 1 + .../picongpu/quick/pypicongpu/species/species.py | 2 +- 7 files changed, 9 insertions(+), 15 deletions(-) diff --git a/docs/source/usage/picmi/intro.rst b/docs/source/usage/picmi/intro.rst index d01bc62734..548978c5d0 100644 --- a/docs/source/usage/picmi/intro.rst +++ b/docs/source/usage/picmi/intro.rst @@ -58,6 +58,7 @@ Example User Script for a warm plasma setup: :language: python Creates a directory ``warm_plasma``, where you can run ``pic-build`` and subsequently ``tbg``. +You can find this and more elaborate examples in `share/picongpu/pypicongpu/examples`. Example User Script for a laser wakefield setup: -------------------------------------- diff --git a/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py b/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py index a3d772ffad..3dd2c216a8 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py +++ b/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py @@ -44,7 +44,7 @@ def __hash__(self): return hash_value def check(self): - # import here to avoid circular import + # import here to avoid circular import that stems from projecting different species types from PIConGPU onto the same `Species` type in PICMI from ... import Species assert isinstance(self.ion_species, Species), "ion_species must be an instance of the species object" diff --git a/lib/python/picongpu/picmi/predefinedparticletypeproperties.py b/lib/python/picongpu/picmi/predefinedparticletypeproperties.py index bd2cb29f6c..f74888e622 100644 --- a/lib/python/picongpu/picmi/predefinedparticletypeproperties.py +++ b/lib/python/picongpu/picmi/predefinedparticletypeproperties.py @@ -12,7 +12,7 @@ _PropertyTuple: collections.namedtuple = collections.namedtuple("_PropertyTuple", ["mass", "charge"]) -# based on 2024 Particle data Group values +# based on 2024 Particle data Group values, @todo read automatically from somewhere, BrianMarre _quarks = { "up": _PropertyTuple( mass=2.16e6 * consts.elementary_charge / consts.speed_of_light**2, charge=2.0 / 3.0 * consts.elementary_charge @@ -118,5 +118,4 @@ ), } -non_element_particle_type_properties = {} -non_element_particle_type_properties.update(**_quarks, **_leptons, **_neutrinos, **_nucleons, **_gauge_bosons) +non_element_particle_type_properties = {**_quarks, **_leptons, **_neutrinos, **_nucleons, **_gauge_bosons} diff --git a/lib/python/picongpu/pypicongpu/species/constant/constant.py b/lib/python/picongpu/pypicongpu/species/constant/constant.py index 9b50c52bf7..9b2ca87675 100644 --- a/lib/python/picongpu/pypicongpu/species/constant/constant.py +++ b/lib/python/picongpu/pypicongpu/species/constant/constant.py @@ -46,16 +46,8 @@ class Constant(RenderedObject): """ def __eq__(self, other) -> bool: - """two constants are equal if the have the same attributes values""" - for key in self.__dict__.keys(): - if key not in other.__dict__: - return False - - for key, value in self.__dict__.items(): - if value != other.__dict__[key]: - return False - - return True + """two constants are equal if they have the same attributes values""" + return set(self.__dict__.items()) == set(other.__dict__.items()) def check(self) -> None: """ diff --git a/share/picongpu/pypicongpu/examples/laser_wakefield/.gitignore b/share/picongpu/pypicongpu/examples/laser_wakefield/.gitignore index 1142186953..07cd22f55b 100644 --- a/share/picongpu/pypicongpu/examples/laser_wakefield/.gitignore +++ b/share/picongpu/pypicongpu/examples/laser_wakefield/.gitignore @@ -1 +1,2 @@ +# This folder is produced by default upon execution: LWFA diff --git a/share/picongpu/pypicongpu/examples/warm_plasma/.gitignore b/share/picongpu/pypicongpu/examples/warm_plasma/.gitignore index e0924a2bff..8015854005 100644 --- a/share/picongpu/pypicongpu/examples/warm_plasma/.gitignore +++ b/share/picongpu/pypicongpu/examples/warm_plasma/.gitignore @@ -1 +1,2 @@ +# This folder is produced by default upon execution: warm_plasma diff --git a/test/python/picongpu/quick/pypicongpu/species/species.py b/test/python/picongpu/quick/pypicongpu/species/species.py index 5105448d2d..cf9501e46d 100644 --- a/test/python/picongpu/quick/pypicongpu/species/species.py +++ b/test/python/picongpu/quick/pypicongpu/species/species.py @@ -1,4 +1,4 @@ -"""sshfs fwk394:/home/marre55 ~/mnt/fwk394 +""" This file is part of PIConGPU. Copyright 2021-2023 PIConGPU contributors Authors: Hannes Troepgen, Brian Edward Marre From f3f3e8a89115c9f8a5f04f8dce236f952c34d8a9 Mon Sep 17 00:00:00 2001 From: BrianMarre Date: Mon, 19 Aug 2024 16:47:41 +0200 Subject: [PATCH 18/20] make PICMI LWFA test compile for example script --- .../picongpu/picmi/interaction/interaction.py | 13 +++-- .../examples/laser_wakefield/main.py | 54 ++++++++++--------- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/lib/python/picongpu/picmi/interaction/interaction.py b/lib/python/picongpu/picmi/interaction/interaction.py index 7c914be8dc..722a28f706 100644 --- a/lib/python/picongpu/picmi/interaction/interaction.py +++ b/lib/python/picongpu/picmi/interaction/interaction.py @@ -123,9 +123,16 @@ def fill_in_ionization_electron_species( 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 - ] + try: + pypicongpu_ionization_electron_species = pypicongpu_by_picmi_species[ + picmi_ionization_model.ionization_electron_species + ] + except KeyError: + raise ValueError( + f"Ionization electron species of {picmi_ionization_model} not known to simulation." + + f"Please add species {picmi_ionization_model.ionization_electron_species.name} to" + + "the simulation" + ) pypicongpu_ionization_model.ionization_electron_species = pypicongpu_ionization_electron_species def __has_ground_state_ionization(self, species) -> bool: diff --git a/share/picongpu/pypicongpu/examples/laser_wakefield/main.py b/share/picongpu/pypicongpu/examples/laser_wakefield/main.py index 6cbacf3432..c3872142b8 100644 --- a/share/picongpu/pypicongpu/examples/laser_wakefield/main.py +++ b/share/picongpu/pypicongpu/examples/laser_wakefield/main.py @@ -15,14 +15,12 @@ This Python script is example PICMI user script reproducing the LaserWakefield example setup, based on 8.cfg. """ - # generation modifiers ENABLE_IONS = True ENABLE_IONIZATION = True ADD_CUSTOM_INPUT = True OUTPUT_DIRECTORY_PATH = "LWFA" - numberCells = np.array([192, 2048, 192]) cellSize = np.array([0.1772e-6, 0.4430e-7, 0.1772e-6]) # unit: meter) @@ -47,17 +45,6 @@ vacuum_cells_front=50, ) -# for particle type see https://github.com/openPMD/openPMD-standard/blob/upcoming-2.0.0/EXT_SpeciesType.md -electrons = picmi.Species(particle_type="electron", name="electron", initial_distribution=gaussianProfile) - -hydrogen_ionization = picmi.Species( - particle_type="H", name="hydrogen", charge_state=0, initial_distribution=gaussianProfile -) - -hydrogen_fully_ionized = picmi.Species( - particle_type="H", name="hydrogen", picongpu_fixed_charge=True, initial_distribution=gaussianProfile -) - solver = picmi.ElectromagneticSolver( grid=grid, method="Yee", @@ -76,27 +63,47 @@ picongpu_phase=0.0, ) -randomLayout = picmi.PseudoRandomLayout(n_macroparticles_per_cell=2) +random_layout = picmi.PseudoRandomLayout(n_macroparticles_per_cell=2) # Initialize particles based on speciesInitialization.param # simulation schema : https://github.com/BrianMarre/picongpu/blob/2ddcdab4c1aca70e1fc0ba02dbda8bd5e29d98eb/share/picongpu/pypicongpu/schema/simulation.Simulation.json +# for particle type see https://github.com/openPMD/openPMD-standard/blob/upcoming-2.0.0/EXT_SpeciesType.md +species_list = [] if not ENABLE_IONIZATION: - hydrogen = hydrogen_fully_ionized interaction = None + + electron_placed = picmi.Species(particle_type="electron", name="electron", initial_distribution=gaussianProfile) + species_list.append((electron_placed, random_layout)) + + if ENABLE_IONS: + hydrogen_fully_ionized = picmi.Species( + particle_type="H", name="hydrogen", picongpu_fixed_charge=True, initial_distribution=gaussianProfile + ) + species_list.append((hydrogen_fully_ionized, random_layout)) else: - hydrogen = hydrogen_ionization + if not ENABLE_IONS: + raise ValueError("Ions species required for ionization") + + hydrogen_with_ionization = picmi.Species( + particle_type="H", name="hydrogen", charge_state=0, initial_distribution=gaussianProfile + ) + species_list.append((hydrogen_with_ionization, random_layout)) + + electron_not_placed = picmi.Species(particle_type="electron", name="electron", initial_distribution=None) + species_list.append((electron_not_placed, None)) + adk_ionization_model = picmi.ADK( ADK_variant=picmi.ADKVariant.CircularPolarization, - ion_species=hydrogen_ionization, - ionization_electron_species=electrons, + ion_species=hydrogen_with_ionization, + ionization_electron_species=electron_not_placed, ionization_current=None, ) bsi_effectiveZ_ionization_model = picmi.BSI( BSI_extensions=[picmi.BSIExtension.EffectiveZ], - ion_species=hydrogen_ionization, - ionization_electron_species=electrons, + ion_species=hydrogen_with_ionization, + ionization_electron_species=electron_not_placed, ionization_current=None, ) @@ -111,11 +118,8 @@ picongpu_moving_window_move_point=0.9, picongpu_interaction=interaction, ) - -sim.add_species(electrons, layout=randomLayout) - -if ENABLE_IONS: - sim.add_species(hydrogen, layout=randomLayout) +for species, layout in species_list: + sim.add_species(species, layout=layout) sim.add_laser(laser, None) From 99280247ac57ef0bb308e71ee3b4f75b961ec6b8 Mon Sep 17 00:00:00 2001 From: BrianMarre Date: Tue, 24 Sep 2024 16:22:16 +0200 Subject: [PATCH 19/20] reviewer comments ci: picongpu --- .../picongpu/picmi/interaction/interaction.py | 13 +++++++------ .../interaction/ionization/fieldionization/ADK.py | 9 ++++----- .../interaction/ionization/fieldionization/BSI.py | 7 +++---- .../picmi/interaction/ionization/ionizationmodel.py | 2 +- lib/python/picongpu/picmi/simulation.py | 2 +- lib/python/picongpu/pypicongpu/runner.py | 4 +--- lib/python/picongpu/pypicongpu/simulation.py | 7 ------- .../picongpu/pypicongpu/species/util/element.py | 2 +- .../species/constant/groundstateionization.py | 2 +- 9 files changed, 19 insertions(+), 29 deletions(-) diff --git a/lib/python/picongpu/picmi/interaction/interaction.py b/lib/python/picongpu/picmi/interaction/interaction.py index 722a28f706..1d997483b6 100644 --- a/lib/python/picongpu/picmi/interaction/interaction.py +++ b/lib/python/picongpu/picmi/interaction/interaction.py @@ -54,7 +54,7 @@ def update_constant_list( if constant != constant_new: # same type of constant but conflicting values - raise ValueError(f"Constants {constant} and {constant_new} conflict with each other") + raise ValueError(f"Constants {constant} and {constant_new} conflict with each other.") if not exists_already: new_constant_list.append(constant_new) @@ -103,8 +103,9 @@ def fill_in_ionization_electron_species( """ add ionization models to pypicongpu species - in PICMI ioniaztion is defined as a list ionization models owned by the simulation, with each ionization model - storing its PICMI ion and PICMI ionization electron species. + In PICMI ionization is defined as a list ionization models owned by an interaction object which in turn is a + member of the simulation, with each ionization model storing its PICMI ion and PICMI ionization electron + species. In contrast in PyPIConGPU each ion PyPIConGPU species owns a list of ionization models, each storing its PyPIConGPU ionization electron species. @@ -115,7 +116,7 @@ def fill_in_ionization_electron_species( Therefore we leave the ionization electron unspecified upon species creation and fill it in from the PICMI simulation ionization model list later. - (An because python uses pointers, this will be applied to the existing species objects passed in + (And because python uses pointers, this will be applied to the existing species objects passed in pypicongpu_by_picmi_species) """ @@ -129,9 +130,9 @@ def fill_in_ionization_electron_species( ] except KeyError: raise ValueError( - f"Ionization electron species of {picmi_ionization_model} not known to simulation." + f"Ionization electron species of {picmi_ionization_model} not known to simulation.\n" + f"Please add species {picmi_ionization_model.ionization_electron_species.name} to" - + "the simulation" + + " the simulation." ) pypicongpu_ionization_model.ionization_electron_species = pypicongpu_ionization_electron_species diff --git a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ADK.py b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ADK.py index 20a65a970e..2a54e68ba5 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ADK.py +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/ADK.py @@ -14,7 +14,6 @@ IonizationModel, ) -from ..... import pypicongpu import enum import typeguard @@ -28,12 +27,12 @@ class ADKVariant(enum.Enum): @typeguard.typechecked class ADK(FieldIonization): - """Barrier Suppression Ioniztion model""" + """ADK Tunneling Ionization model""" MODEL_NAME: str = "ADK" ADK_variant: ADKVariant - """extension to the BSI model""" + """ADK model variant specification""" def get_as_pypicongpu(self) -> IonizationModel: self.check() @@ -43,5 +42,5 @@ def get_as_pypicongpu(self) -> IonizationModel: if self.ADK_variant is ADKVariant.CircularPolarization: return ADKCircularPolarization(ionization_current=None_()) - # unknown/unsupported ADK variant - pypicongpu.util.unsupported(f"ADKVariant {self.ADK_variant}") + # unknown ADK variant + raise ValueError(f"ADKVariant {self.ADK_variant} is not supported.") diff --git a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/BSI.py b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/BSI.py index 3eb8387433..4054a196ff 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/fieldionization/BSI.py +++ b/lib/python/picongpu/picmi/interaction/ionization/fieldionization/BSI.py @@ -19,13 +19,12 @@ class BSIExtension(enum.Enum): StarkShift = 0 EffectiveZ = 1 - # consider_excitation = 2 - # add additional features here + # add additional extensions here @typeguard.typechecked class BSI(FieldIonization): - """Barrier Suppression Ioniztion model""" + """Barrier Suppression Ionization model""" MODEL_NAME: str = "BSI" @@ -45,4 +44,4 @@ def get_as_pypicongpu(self) -> ionizationmodel.IonizationModel: return ionizationmodel.BSIStarkShifted(ionization_current=None_()) if self.BSI_extensions[0] is BSIExtension.EffectiveZ: return ionizationmodel.BSIEffectiveZ(ionization_current=None_()) - raise ValueError(f"unknown BSI_extension {self.BSI_extensions[0]}") + raise ValueError(f"unknown BSI_extension {self.BSI_extensions[0]}.") diff --git a/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py b/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py index 3dd2c216a8..b09795eae0 100644 --- a/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py +++ b/lib/python/picongpu/picmi/interaction/ionization/ionizationmodel.py @@ -50,7 +50,7 @@ def check(self): 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" + ), "ionization_electron_species must be an instance of the species object" def get_constants(self) -> list[pypicongpu.species.constant.Constant]: raise NotImplementedError("abstract base class only!") diff --git a/lib/python/picongpu/picmi/simulation.py b/lib/python/picongpu/picmi/simulation.py index c456c5a541..7f9a780e3a 100644 --- a/lib/python/picongpu/picmi/simulation.py +++ b/lib/python/picongpu/picmi/simulation.py @@ -433,7 +433,7 @@ def get_as_pypicongpu(self) -> pypicongpu.simulation.Simulation: pypicongpu.util.unsupported("verbose", self.verbose) pypicongpu.util.unsupported("particle shape", self.particle_shape, "linear") - pypicongpu.util.unsupported("gamma boost, use picongpu_moving_window_move_point instead", self.gamma_boost) + pypicongpu.util.unsupported("gamma boost", self.gamma_boost) try: s.grid = self.solver.grid.get_as_pypicongpu() diff --git a/lib/python/picongpu/pypicongpu/runner.py b/lib/python/picongpu/pypicongpu/runner.py index 478675e4f7..bcbeb37767 100644 --- a/lib/python/picongpu/pypicongpu/runner.py +++ b/lib/python/picongpu/pypicongpu/runner.py @@ -334,9 +334,7 @@ def __run(self): runArgs( "PIConGPU", ( - ("tbg -s bash -c etc/picongpu/N.cfg -t " + environ["PIC_SYSTEM_TEMPLATE_PATH"] + "/mpiexec.tpl").split( - " " - ) + ("tbg -s bash -c etc/picongpu/N.cfg -t " + environ["PIC_SYSTEM_TEMPLATE_PATH"] + "/mpiexec.tpl").split() + [self.run_dir] ), ) diff --git a/lib/python/picongpu/pypicongpu/simulation.py b/lib/python/picongpu/pypicongpu/simulation.py index f92cfb5f8f..d04196e941 100644 --- a/lib/python/picongpu/pypicongpu/simulation.py +++ b/lib/python/picongpu/pypicongpu/simulation.py @@ -99,14 +99,7 @@ def __found_custom_input(self, serialized: dict): + "\t WARNING: custom templates are required if using custom user input.\n" ) - def check(self): - """check validity of self""" - if self.typical_ppc < 1: - raise ValueError("typical_ppc must be >= 1") - def _get_serialized(self) -> dict: - self.check() - serialized = { "delta_t_si": self.delta_t_si, "time_steps": self.time_steps, diff --git a/lib/python/picongpu/pypicongpu/species/util/element.py b/lib/python/picongpu/pypicongpu/species/util/element.py index 491b3de932..83060592ac 100644 --- a/lib/python/picongpu/pypicongpu/species/util/element.py +++ b/lib/python/picongpu/pypicongpu/species/util/element.py @@ -36,7 +36,7 @@ class Element(RenderedObject, pydantic.BaseModel): @staticmethod 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") + raise ValueError("Empty string 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 diff --git a/test/python/picongpu/quick/pypicongpu/species/constant/groundstateionization.py b/test/python/picongpu/quick/pypicongpu/species/constant/groundstateionization.py index 01861e0230..b8961e07f3 100644 --- a/test/python/picongpu/quick/pypicongpu/species/constant/groundstateionization.py +++ b/test/python/picongpu/quick/pypicongpu/species/constant/groundstateionization.py @@ -1,6 +1,6 @@ """ This file is part of PIConGPU. -Copyright 2021-2023 PIConGPU contributors +Copyright 2021-2024 PIConGPU contributors Authors: Hannes Troepgen, Brian Edward Marre License: GPLv3+ """ From bf8f1d55cba7ad3b2728a2324d2eba8be0778376 Mon Sep 17 00:00:00 2001 From: BrianMarre Date: Fri, 27 Sep 2024 12:13:50 +0200 Subject: [PATCH 20/20] switch from pdg to particle in PICMI ci: picongpu --- .../picmi/predefinedparticletypeproperties.py | 78 ++++++++++--------- lib/python/picongpu/picmi/requirements.txt | 2 +- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/lib/python/picongpu/picmi/predefinedparticletypeproperties.py b/lib/python/picongpu/picmi/predefinedparticletypeproperties.py index f74888e622..dd457025d0 100644 --- a/lib/python/picongpu/picmi/predefinedparticletypeproperties.py +++ b/lib/python/picongpu/picmi/predefinedparticletypeproperties.py @@ -6,7 +6,7 @@ """ import collections -import pdg +import particle from scipy import constants as consts @@ -15,69 +15,73 @@ # based on 2024 Particle data Group values, @todo read automatically from somewhere, BrianMarre _quarks = { "up": _PropertyTuple( - mass=2.16e6 * consts.elementary_charge / consts.speed_of_light**2, charge=2.0 / 3.0 * consts.elementary_charge + mass=particle.Particle.findall("u")[0].mass * 1e6 * consts.elementary_charge / consts.speed_of_light**2, + charge=particle.Particle.findall("u")[0].charge * consts.elementary_charge, ), "charm": _PropertyTuple( - mass=1.2730e9 * consts.elementary_charge / consts.speed_of_light**2, charge=2.0 / 3.0 * consts.elementary_charge + mass=particle.Particle.findall("c")[0].mass * 1e6 * consts.elementary_charge / consts.speed_of_light**2, + charge=particle.Particle.findall("c")[0].charge * consts.elementary_charge, ), "top": _PropertyTuple( - mass=172.57e9 * consts.elementary_charge / consts.speed_of_light**2, charge=2.0 / 3.0 * consts.elementary_charge + mass=particle.Particle.findall("t")[0].mass * 1e6 * consts.elementary_charge / consts.speed_of_light**2, + charge=particle.Particle.findall("t")[0].charge * consts.elementary_charge, ), "down": _PropertyTuple( - mass=4.70e6 * consts.elementary_charge / consts.speed_of_light**2, charge=-1.0 / 3.0 * consts.elementary_charge + mass=particle.Particle.findall("d")[0].mass * 1e6 * consts.elementary_charge / consts.speed_of_light**2, + charge=particle.Particle.findall("d")[0].charge * consts.elementary_charge, ), "strange": _PropertyTuple( - mass=93.5 * consts.elementary_charge / consts.speed_of_light**2, charge=-1.0 / 3.0 * consts.elementary_charge + mass=particle.Particle.findall("s")[0].mass * 1e6 * consts.elementary_charge / consts.speed_of_light**2, + charge=particle.Particle.findall("s")[0].charge * consts.elementary_charge, ), "bottom": _PropertyTuple( - mass=4.138 * consts.elementary_charge / consts.speed_of_light**2, charge=-1.0 / 3.0 * consts.elementary_charge + mass=particle.Particle.findall("b")[0].mass * 1e6 * consts.elementary_charge / consts.speed_of_light**2, + charge=particle.Particle.findall("b")[0].charge * 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 + mass=particle.Particle.findall("u~")[0].mass * 1e6 * consts.elementary_charge / consts.speed_of_light**2, + charge=particle.Particle.findall("u~")[0].charge * 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, + mass=particle.Particle.findall("c~")[0].mass * 1e6 * consts.elementary_charge / consts.speed_of_light**2, + charge=particle.Particle.findall("c~")[0].charge * 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, + mass=particle.Particle.findall("t~")[0].mass * 1e6 * consts.elementary_charge / consts.speed_of_light**2, + charge=particle.Particle.findall("t~")[0].charge * 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 + mass=particle.Particle.findall("d~")[0].mass * 1e6 * consts.elementary_charge / consts.speed_of_light**2, + charge=particle.Particle.findall("d~")[0].charge * 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 + mass=particle.Particle.findall("s~")[0].mass * 1e6 * consts.elementary_charge / consts.speed_of_light**2, + charge=particle.Particle.findall("s~")[0].charge * 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 + mass=particle.Particle.findall("b~")[0].mass * 1e6 * consts.elementary_charge / consts.speed_of_light**2, + charge=particle.Particle.findall("b~")[0].charge * 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, + mass=particle.Particle.findall("mu-")[0].mass * 1e6 * consts.elementary_charge / consts.speed_of_light**2, + charge=particle.Particle.findall("mu-")[0].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, + mass=particle.Particle.findall("tau-")[0].mass * 1e6 * consts.elementary_charge / consts.speed_of_light**2, + charge=particle.Particle.findall("tau-")[0].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, + mass=particle.Particle.findall("mu+")[0].mass * 1e6 * consts.elementary_charge / consts.speed_of_light**2, + charge=particle.Particle.findall("mu+")[0].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, + mass=particle.Particle.findall("tau+")[0].mass * 1e6 * consts.elementary_charge / consts.speed_of_light**2, + charge=particle.Particle.findall("tau+")[0].charge * consts.elementary_charge, ), } @@ -101,20 +105,20 @@ "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, + mass=particle.Particle.findall("W+")[0].mass * 1e9 * consts.elementary_charge / consts.speed_of_light**2, + charge=particle.Particle.findall("W+")[0].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, + mass=particle.Particle.findall("W-")[0].mass * 1e9 * consts.elementary_charge / consts.speed_of_light**2, + charge=particle.Particle.findall("W-")[0].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, + mass=particle.Particle.findall("Z")[0].mass * 1e9 * consts.elementary_charge / consts.speed_of_light**2, + charge=particle.Particle.findall("Z")[0].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, + mass=particle.Particle.findall("H")[0].mass * 1e9 * consts.elementary_charge / consts.speed_of_light**2, + charge=particle.Particle.findall("H")[0].charge * consts.elementary_charge, ), } diff --git a/lib/python/picongpu/picmi/requirements.txt b/lib/python/picongpu/picmi/requirements.txt index 6135cdc3c3..9f751e927b 100644 --- a/lib/python/picongpu/picmi/requirements.txt +++ b/lib/python/picongpu/picmi/requirements.txt @@ -4,5 +4,5 @@ picmistandard >= 0.27.0 typeguard >= 4.2.1 sympy >= 1.9 pydantic >= 2.6.4 -pdg >= 0.1.2 +particle >= 0.25.1 -r ../pypicongpu/requirements.txt