diff --git a/lib/python/picongpu/picmi/diagnostics/__init__.py b/lib/python/picongpu/picmi/diagnostics/__init__.py index e54881b1a7..891dbf7ca9 100644 --- a/lib/python/picongpu/picmi/diagnostics/__init__.py +++ b/lib/python/picongpu/picmi/diagnostics/__init__.py @@ -11,7 +11,10 @@ from .macro_particle_count import MacroParticleCount from .png import Png from .timestepspec import TimeStepSpec +from .rangespec import RangeSpec from .checkpoint import Checkpoint +from .openpmd import OpenPMD +from .openpmd_sources.source_base import SourceBase __all__ = [ "Auto", @@ -20,5 +23,8 @@ "MacroParticleCount", "Png", "TimeStepSpec", + "RangeSpec", "Checkpoint", + "OpenPMD", + "SourceBase", ] diff --git a/lib/python/picongpu/picmi/diagnostics/auto.py b/lib/python/picongpu/picmi/diagnostics/auto.py index 20f0134f64..932e5aefb0 100644 --- a/lib/python/picongpu/picmi/diagnostics/auto.py +++ b/lib/python/picongpu/picmi/diagnostics/auto.py @@ -1,7 +1,7 @@ """ This file is part of PIConGPU. Copyright 2025 PIConGPU contributors -Authors: Pawel Ordyna +Authors: Pawel Ordyna, Masoud Afshari License: GPLv3+ """ @@ -42,6 +42,7 @@ def get_as_pypicongpu( dict_species_picmi_to_pypicongpu: dict[PICMISpecies, PyPIConGPUSpecies], time_step_size, num_steps, + simulation_box=None, # Added to match OpenPMD signature, not used ) -> PyPIConGPUAuto: self.check() pypicongpu_auto = PyPIConGPUAuto() diff --git a/lib/python/picongpu/picmi/diagnostics/checkpoint.py b/lib/python/picongpu/picmi/diagnostics/checkpoint.py index 160a856265..6be3a5a5e2 100644 --- a/lib/python/picongpu/picmi/diagnostics/checkpoint.py +++ b/lib/python/picongpu/picmi/diagnostics/checkpoint.py @@ -108,6 +108,7 @@ def get_as_pypicongpu( pypicongpu_by_picmi_species: Dict, time_step_size: float, num_steps: int, + simulation_box=None, # Added to match OpenPMD signature, not used ) -> PyPIConGPUCheckpoint: self.check() diff --git a/lib/python/picongpu/picmi/diagnostics/energy_histogram.py b/lib/python/picongpu/picmi/diagnostics/energy_histogram.py index 28602556f6..ae73903e7a 100644 --- a/lib/python/picongpu/picmi/diagnostics/energy_histogram.py +++ b/lib/python/picongpu/picmi/diagnostics/energy_histogram.py @@ -79,6 +79,7 @@ def get_as_pypicongpu( dict_species_picmi_to_pypicongpu: dict[PICMISpecies, PyPIConGPUSpecies], time_step_size, num_steps, + simulation_box=None, # Added to match OpenPMD signature, not used ) -> PyPIConGPUEnergyHistogram: self.check() diff --git a/lib/python/picongpu/picmi/diagnostics/macro_particle_count.py b/lib/python/picongpu/picmi/diagnostics/macro_particle_count.py index 097a4d9a6b..cf80b48e8c 100644 --- a/lib/python/picongpu/picmi/diagnostics/macro_particle_count.py +++ b/lib/python/picongpu/picmi/diagnostics/macro_particle_count.py @@ -49,6 +49,7 @@ def get_as_pypicongpu( dict_species_picmi_to_pypicongpu: dict[PICMISpecies, PyPIConGPUSpecies], time_step_size, num_steps, + simulation_box=None, # Added to match OpenPMD signature, not used ) -> PyPIConGPUMacroParticleCount: self.check() diff --git a/lib/python/picongpu/picmi/diagnostics/openpmd.py b/lib/python/picongpu/picmi/diagnostics/openpmd.py new file mode 100644 index 0000000000..b6b9255062 --- /dev/null +++ b/lib/python/picongpu/picmi/diagnostics/openpmd.py @@ -0,0 +1,109 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from ...pypicongpu.output.openpmd import OpenPMD as PyPIConGPUOpenPMD +from ...pypicongpu.output.openpmd_sources.source_base import SourceBase as PyPIConGPUSource +from .timestepspec import TimeStepSpec +from .rangespec import RangeSpec +from .openpmd_sources.source_base import SourceBase + +import typeguard +from typing import Optional, Dict, Union, List, Literal, Tuple + + +@typeguard.typechecked +class OpenPMD: + """ + openPMD diagnostic output + + This diagnostic writes simulation data (base fields, derived fields and/or particles) to disk using the openPMD + standard, with configurable periods, data sources and backend settings. + + @param period specification of the time steps for data output, outputs will always be written at the end of a PIC time step. + @param source list of data source objects to include in the dump (e.g., [ChargeDensity(filter="all")]), + Setting this to None will cause an empty dump + @param range contiguous range of cells to dump the base- and derived field for, specified as a RangeSpec object + or a string in the format "begin:end" (1D), "begin:end,begin:end" (2D), or "begin:end,begin:end,begin:end" (3D). + Example: "0:10,5:15,2:8" specifies cells 0 to 10 (x), 5 to 15 (y), 2 to 8 (z). + Notes: Values are clipped to the simulation box. Begin and/or end may be omitted (":") to indicate the full extent + of the dimension. Negative indices are supported (e.g., "-5:-1" for last 5 cells). The default ":,:,:," (3D), + ":,:" (2D), or ":" (1D) includes all cells in the simulation box. + @param file relative or absolute file path prefix for openPMD output files. Relative paths are interpreted as relative to the simulation output directory, the default value None indicates the PIC code's default. + @param ext file extension controlling the openPMD backend, options are "bp" (default backend ADIOS2), "h5" (HDF5), "sst" (ADIOS2/SST for streaming). + @param infix filename infix for the iteration layout (e.g., "_%06T"), use "NULL" for the group-based layout, ext="sst" requires infix="NULL". + @param json openPMD backend configuration as a JSON string, dictionary, or filename (filename must be prepended with "@"). + @param json_restart backend-specific parameters for restarting, as a JSON string, dictionary, or filename (filenames must be prepended with "@"). + @param data_preparation_strategy strategy for particle data preparation, options: "doubleBuffer" or "adios" (ADIOS2-based), "mappedMemory" or "hdf5" (HDF5-based), the default value None indicates the PIC code default + @param toml path to a TOML file for openPMD configuration. Replaces the JSON or keyword configuration. + @param particle_io_chunk_size size of particle data chunks used in writing (in MiB), reduces host memory footprint for certain backends, default "None" indicates the PIC code default. + @param file_writing file writing mode for writing, options: "create" (new files), "append" (for checkpoint-restart workflows). + """ + + def check(self): + """ + Validate the provided parameters. + """ + if self.particle_io_chunk_size is not None and self.particle_io_chunk_size < 1: + raise ValueError("particle_io_chunk_size (in MiB) must be positive") + if self.ext == "sst" and self.infix is not None and self.infix != "NULL": + raise ValueError("infix must be 'NULL' when ext is 'sst'") + if self.source is not None and not all(isinstance(s, SourceBase) for s in self.source): + raise ValueError("source must be a list of SourceBase objects") + + def __init__( + self, + period: TimeStepSpec, + source: Optional[List[SourceBase]] = None, + range: Optional[Union[str, RangeSpec]] = ":,:,:", + file: Optional[str] = None, + ext: Optional[Literal["bp", "h5", "sst"]] = "bp", + infix: Optional[str] = "NULL", + json: Optional[Union[str, Dict]] = None, + json_restart: Optional[Union[str, Dict]] = None, + data_preparation_strategy: Optional[Literal["doubleBuffer", "adios", "mappedMemory", "hdf5"]] = None, + toml: Optional[str] = None, + particle_io_chunk_size: Optional[int] = None, + file_writing: Optional[Literal["create", "append"]] = "create", + ): + self.period = period + self.source = source + self.range = RangeSpec(range) if isinstance(range, str) else range + self.file = file + self.ext = ext + self.infix = infix + self.json = json if json is not None else {} + self.json_restart = json_restart if json_restart is not None else {} + self.data_preparation_strategy = data_preparation_strategy + self.toml = toml + self.particle_io_chunk_size = particle_io_chunk_size + self.file_writing = file_writing + + self.check() + + def get_as_pypicongpu( + self, + pypicongpu_by_picmi_species: Dict, + time_step_size: float, + num_steps: int, + simulation_box: Tuple[int, ...], + ) -> PyPIConGPUOpenPMD: + self.check() + pypicongpu_openpmd = PyPIConGPUOpenPMD( + period=self.period.get_as_pypicongpu(time_step_size, num_steps), + source=PyPIConGPUSource([s.get_as_pypicongpu() for s in self.source]) if self.source is not None else None, + range=self.range.get_as_pypicongpu(simulation_box), + file=self.file, + ext=self.ext, + infix=self.infix, + json=self.json, + json_restart=self.json_restart, + data_preparation_strategy=self.data_preparation_strategy, + toml=self.toml, + particle_io_chunk_size=self.particle_io_chunk_size, + file_writing=self.file_writing, + ) + return pypicongpu_openpmd diff --git a/lib/python/picongpu/picmi/diagnostics/openpmd_sources/__init__.py b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/__init__.py new file mode 100644 index 0000000000..8bad5040c4 --- /dev/null +++ b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/__init__.py @@ -0,0 +1,35 @@ +from .auto import Auto +from .bound_electron_density import BoundElectronDensity +from .charge_density import ChargeDensity +from .counter import Counter +from .density import Density +from .derived_attributes import DerivedAttributes +from .energy import Energy +from .energy_density import EnergyDensity +from .energy_density_cutoff import EnergyDensityCutoff +from .larmor_power import LarmorPower +from .macro_counter import MacroCounter +from .mid_current_density_component import MidCurrentDensityComponent +from .momentum import Momentum +from .momentum_density import MomentumDensity +from .weighted_velocity import WeightedVelocity +from .source_base import SourceBase + +__all__ = [ + "Auto", + "BoundElectronDensity", + "ChargeDensity", + "Counter", + "Density", + "DerivedAttributes", + "Energy", + "EnergyDensity", + "EnergyDensityCutoff", + "LarmorPower", + "MacroCounter", + "MidCurrentDensityComponent", + "Momentum", + "MomentumDensity", + "WeightedVelocity", + "SourceBase", +] diff --git a/lib/python/picongpu/picmi/diagnostics/openpmd_sources/auto.py b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/auto.py new file mode 100644 index 0000000000..53646e90a4 --- /dev/null +++ b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/auto.py @@ -0,0 +1,49 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from .source_base import SourceBase +from ....pypicongpu.output.openpmd_sources import Auto as PyPIConGPUAuto +import typeguard +import typing + + +@typeguard.typechecked +class Auto(SourceBase): + """ + Default data source for openPMD output + + This class provides a convenient way to dump default simulation data (e.g., all + particle species and fields) using the openPMD standard, with defaults determined + by the PIC code in particle-in-cell simulations. + + @param filter Name of a filter to select data contributing to the source. + Default: None (PIC code-dependent). + """ + + # filter = util.build_typesafe_property(typing.Optional[str]) + + def __init__(self, filter: typing.Optional[str] = None): + self.filter = filter + self.check() + + def check(self) -> None: + """ + Validate the filter parameter. + + @throw ValueError If the filter is not a string or None. + """ + if self.filter is not None and not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string or None, got {type(self.filter)}") + + def get_as_pypicongpu(self) -> PyPIConGPUAuto: + """ + Convert this Auto source to a PyPIConGPU Auto source. + + @return A PyPIConGPU Auto instance with the same filter. + """ + self.check() + return PyPIConGPUAuto(filter=self.filter) diff --git a/lib/python/picongpu/picmi/diagnostics/openpmd_sources/bound_electron_density.py b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/bound_electron_density.py new file mode 100644 index 0000000000..c1dc710718 --- /dev/null +++ b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/bound_electron_density.py @@ -0,0 +1,65 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from .source_base import SourceBase +from ....pypicongpu.output.openpmd_sources import BoundElectronDensity as PyPIConGPUBoundElectronDensity +from ...species import Species as PICMISpecies +import typeguard +import typing + + +@typeguard.typechecked +class BoundElectronDensity(SourceBase): + """ + Bound electron density data source for openPMD output + + This source calculates the density of bound electrons from a specified particle species, + optionally filtered by a selection criterion, for particle-in-cell simulations. + + @param species Particle species contributing to the bound electron density (e.g., ions). + @param filter Name of a filter to select particles contributing to the source. + Default: "all" (includes all particles of the specified species). + """ + + def __init__(self, species: PICMISpecies, filter: str = "all"): + self.species = species + self.filter = filter + self.check() + + def check(self) -> None: + """ + Validate the parameters. + + @throw ValueError If filter is not a string or species is not a PICMISpecies. + """ + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, PICMISpecies): + raise ValueError(f"Species must be a PICMISpecies, got {type(self.species)}") + + def get_as_pypicongpu( + self, + dict_species_picmi_to_pypicongpu: dict[PICMISpecies, typing.Any], + ) -> PyPIConGPUBoundElectronDensity: + """ + Convert this BoundElectronDensity source to a PyPIConGPU BoundElectronDensity source. + + @param dict_species_picmi_to_pypicongpu Mapping of PICMI species to PyPIConGPU species. + @return A PyPIConGPU BoundElectronDensity instance with the same filter and species. + @throw ValueError If the species is not known to the simulation or not mapped to a PyPIConGPUSpecies. + """ + self.check() + + if self.species not in dict_species_picmi_to_pypicongpu.keys(): + raise ValueError(f"Species {self.species} is not known to Simulation") + + pypicongpu_species = dict_species_picmi_to_pypicongpu.get(self.species) + + if pypicongpu_species is None: + raise ValueError(f"Species {self.species} is not mapped to a PyPIConGPUSpecies.") + + return PyPIConGPUBoundElectronDensity(filter=self.filter, species=pypicongpu_species) diff --git a/lib/python/picongpu/picmi/diagnostics/openpmd_sources/charge_density.py b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/charge_density.py new file mode 100644 index 0000000000..3fac9e7b93 --- /dev/null +++ b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/charge_density.py @@ -0,0 +1,65 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from .source_base import SourceBase +from ....pypicongpu.output.openpmd_sources import ChargeDensity as PyPIConGPUChargeDensity +from ...species import Species as PICMISpecies +import typeguard +import typing + + +@typeguard.typechecked +class ChargeDensity(SourceBase): + """ + Charge density data source for openPMD output + + This source calculates the charge density from a specified particle species, optionally + filtered by a selection criterion, for particle-in-cell simulations. + + @param species Particle species contributing to the charge density (e.g., electrons, protons). + @param filter Name of a filter to select particles contributing to the source. + Default: "all" (includes all particles of the specified species). + """ + + def __init__(self, species: PICMISpecies, filter: str = "all"): + self.species = species + self.filter = filter + self.check() + + def check(self) -> None: + """ + Validate the parameters. + + @throw ValueError If filter is not a string or species is not a PICMISpecies. + """ + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, PICMISpecies): + raise ValueError(f"Species must be a PICMISpecies, got {type(self.species)}") + + def get_as_pypicongpu( + self, + dict_species_picmi_to_pypicongpu: dict[PICMISpecies, typing.Any], + ) -> PyPIConGPUChargeDensity: + """ + Convert this ChargeDensity source to a PyPIConGPU ChargeDensity source. + + @param dict_species_picmi_to_pypicongpu Mapping of PICMI species to PyPIConGPU species. + @return A PyPIConGPU ChargeDensity instance with the same filter and species. + @throw ValueError If the species is not known to the simulation or not mapped to a PyPIConGPUSpecies. + """ + self.check() + + if self.species not in dict_species_picmi_to_pypicongpu.keys(): + raise ValueError(f"Species {self.species} is not known to Simulation") + + pypicongpu_species = dict_species_picmi_to_pypicongpu.get(self.species) + + if pypicongpu_species is None: + raise ValueError(f"Species {self.species} is not mapped to a PyPIConGPUSpecies.") + + return PyPIConGPUChargeDensity(filter=self.filter, species=pypicongpu_species) diff --git a/lib/python/picongpu/picmi/diagnostics/openpmd_sources/counter.py b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/counter.py new file mode 100644 index 0000000000..40d2f362fb --- /dev/null +++ b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/counter.py @@ -0,0 +1,69 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from .source_base import SourceBase +from ....pypicongpu.output.openpmd_sources import Counter as PyPIConGPUCounter +from ...species import Species as PICMISpecies +import typeguard +import typing + + +@typeguard.typechecked +class Counter(SourceBase): + """ + Particle counter data source for openPMD output + + This source derives a scalar field representing the number of real particles per cell + for a specified species, optionally filtered by a selection criterion, in particle-in-cell + simulations. The particle count is based on the species' weighting attribute and assigned + directly to the cell containing each particle. Intended primarily for debugging due to + its non-physical deposition shape, which differs from standard charge or momentum-conserving + assignments. + + @param species Particle species to count (e.g., electrons, ions). Must have a weighting attribute. + @param filter Name of a filter to select particles contributing to the source. + Default: "all" (includes all particles of the specified species). + """ + + def __init__(self, species: PICMISpecies, filter: str = "all"): + self.species = species + self.filter = filter + self.check() + + def check(self) -> None: + """ + Validate the parameters. + + @throw ValueError If filter is not a string or species is not a PICMISpecies. + """ + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, PICMISpecies): + raise ValueError(f"Species must be a PICMISpecies, got {type(self.species)}") + + def get_as_pypicongpu( + self, + dict_species_picmi_to_pypicongpu: dict[PICMISpecies, typing.Any], + ) -> PyPIConGPUCounter: + """ + Convert this Counter source to a PyPIConGPU Counter source. + + @param dict_species_picmi_to_pypicongpu Mapping of PICMI species to PyPIConGPU species. + @return A PyPIConGPU Counter instance with the same filter and species. + @throw ValueError If the species is not known to the simulation or not mapped to a PyPIConGPUSpecies. + """ + self.check() + + if self.species not in dict_species_picmi_to_pypicongpu.keys(): + raise ValueError(f"Species {self.species} is not known to Simulation") + + pypicongpu_species = dict_species_picmi_to_pypicongpu.get(self.species) + + if pypicongpu_species is None: + raise ValueError(f"Species {self.species} is not mapped to a PyPIConGPUSpecies.") + + return PyPIConGPUCounter(filter=self.filter, species=pypicongpu_species) diff --git a/lib/python/picongpu/picmi/diagnostics/openpmd_sources/density.py b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/density.py new file mode 100644 index 0000000000..239467c62e --- /dev/null +++ b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/density.py @@ -0,0 +1,68 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from .source_base import SourceBase +from ....pypicongpu.output.openpmd_sources import Density as PyPIConGPUDensity +from ...species import Species as PICMISpecies +import typeguard +import typing + + +@typeguard.typechecked +class Density(SourceBase): + """ + Particle density data source for openPMD output + + This source derives a scalar field representing the number density (in m^-3) of a specified + particle species, optionally filtered by a selection criterion, in particle-in-cell simulations. + The density is calculated based on the species' weighting and position attributes and mapped to + cells according to the PIC code's spatial shape assignment. + + @param species Particle species to calculate density for (e.g., electrons, ions). + Must have weighting and position attributes. + @param filter Name of a filter to select particles contributing to the source. + Default: "all" (includes all particles of the specified species). + """ + + def __init__(self, species: PICMISpecies, filter: str = "all"): + self.species = species + self.filter = filter + self.check() + + def check(self) -> None: + """ + Validate the parameters. + + @throw ValueError If filter is not a string or species is not a PICMISpecies. + """ + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, PICMISpecies): + raise ValueError(f"Species must be a PICMISpecies, got {type(self.species)}") + + def get_as_pypicongpu( + self, + dict_species_picmi_to_pypicongpu: dict[PICMISpecies, typing.Any], + ) -> PyPIConGPUDensity: + """ + Convert this Density source to a PyPIConGPU Density source. + + @param dict_species_picmi_to_pypicongpu Mapping of PICMI species to PyPIConGPU species. + @return A PyPIConGPU Density instance with the same filter and species. + @throw ValueError If the species is not known to the simulation or not mapped to a PyPIConGPUSpecies. + """ + self.check() + + if self.species not in dict_species_picmi_to_pypicongpu.keys(): + raise ValueError(f"Species {self.species} is not known to Simulation") + + pypicongpu_species = dict_species_picmi_to_pypicongpu.get(self.species) + + if pypicongpu_species is None: + raise ValueError(f"Species {self.species} is not mapped to a PyPIConGPUSpecies.") + + return PyPIConGPUDensity(filter=self.filter, species=pypicongpu_species) diff --git a/lib/python/picongpu/picmi/diagnostics/openpmd_sources/derived_attributes.py b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/derived_attributes.py new file mode 100644 index 0000000000..7f50bb6916 --- /dev/null +++ b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/derived_attributes.py @@ -0,0 +1,45 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from .source_base import SourceBase +from ....pypicongpu.output.openpmd_sources import DerivedAttributes as PyPIConGPUDerivedAttributes +import typeguard +import typing + + +@typeguard.typechecked +class DerivedAttributes(SourceBase): + """ + Aggregated derived attributes data source for openPMD output + + Enables all particle-to-grid derived attributes (e.g., density, charge) for openPMD output + in particle-in-cell simulations, with defaults determined by the PIC code. + + @param filter Name of a filter to select data. Default: None (PIC code-dependent). + """ + + def __init__(self, filter: typing.Optional[str] = None): + self.filter = filter + self.check() + + def check(self) -> None: + """ + Validate the filter parameter. + + @throw ValueError If filter is not a string or None. + """ + if self.filter is not None and not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string or None, got {type(self.filter)}") + + def get_as_pypicongpu(self) -> PyPIConGPUDerivedAttributes: + """ + Convert to a PyPIConGPU DerivedAttributes source. + + @return A PyPIConGPU DerivedAttributes instance with the same filter. + """ + self.check() + return PyPIConGPUDerivedAttributes(filter=self.filter) diff --git a/lib/python/picongpu/picmi/diagnostics/openpmd_sources/energy.py b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/energy.py new file mode 100644 index 0000000000..73de600332 --- /dev/null +++ b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/energy.py @@ -0,0 +1,66 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from .source_base import SourceBase +from ....pypicongpu.output.openpmd_sources import Energy as PyPIConGPEnergy +from ...species import Species as PICMISpecies +import typeguard +import typing + + +@typeguard.typechecked +class Energy(SourceBase): + """ + Kinetic energy data source for openPMD output + + Derives a scalar field of summed kinetic energy (in Joules) for a specified particle species, + optionally filtered, in particle-in-cell simulations. Uses weighting, momentum, and mass attributes, + mapped to cells by the PIC code's spatial shape. + + @param species Particle species to calculate energy for (e.g., electrons, ions). + Must have weighting, momentum, and mass attributes. + @param filter Name of a filter to select particles. Default: "all". + """ + + def __init__(self, species: PICMISpecies, filter: str = "all"): + self.species = species + self.filter = filter + self.check() + + def check(self) -> None: + """ + Validate the parameters. + + @throw ValueError If filter is not a string or species is not a PICMISpecies. + """ + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, PICMISpecies): + raise ValueError(f"Species must be a PICMISpecies, got {type(self.species)}") + + def get_as_pypicongpu( + self, + dict_species_picmi_to_pypicongpu: dict[PICMISpecies, typing.Any], + ) -> PyPIConGPEnergy: + """ + Convert to a PyPIConGPU Energy source. + + @param dict_species_picmi_to_pypicongpu Mapping of PICMI to PyPIConGPU species. + @return A PyPIConGPU Energy instance with the same filter and species. + @throw ValueError If species is unknown or unmapped to a PyPIConGPUSpecies. + """ + self.check() + + if self.species not in dict_species_picmi_to_pypicongpu.keys(): + raise ValueError(f"Species {self.species} is not known to Simulation") + + pypicongpu_species = dict_species_picmi_to_pypicongpu.get(self.species) + + if pypicongpu_species is None: + raise ValueError(f"Species {self.species} is not mapped to a PyPIConGPUSpecies.") + + return PyPIConGPEnergy(filter=self.filter, species=pypicongpu_species) diff --git a/lib/python/picongpu/picmi/diagnostics/openpmd_sources/energy_density.py b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/energy_density.py new file mode 100644 index 0000000000..19ee976e4a --- /dev/null +++ b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/energy_density.py @@ -0,0 +1,66 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from .source_base import SourceBase +from ....pypicongpu.output.openpmd_sources import EnergyDensity as PyPIConGPUEnergyDensity +from ...species import Species as PICMISpecies +import typeguard +import typing + + +@typeguard.typechecked +class EnergyDensity(SourceBase): + """ + Kinetic energy density data source for openPMD output + + Derives a scalar field of kinetic energy density (in J/m^3) for a specified particle species, + optionally filtered, in particle-in-cell simulations. Uses weighting, momentum, and mass attributes, + mapped to cells by the PIC code's spatial shape. + + @param species Particle species to calculate energy density for (e.g., electrons, ions). + Must have weighting, momentum, and mass attributes. + @param filter Name of a filter to select particles. Default: "all". + """ + + def __init__(self, species: PICMISpecies, filter: str = "all"): + self.species = species + self.filter = filter + self.check() + + def check(self) -> None: + """ + Validate the parameters. + + @throw ValueError If filter is not a string or species is not a PICMISpecies. + """ + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, PICMISpecies): + raise ValueError(f"Species must be a PICMISpecies, got {type(self.species)}") + + def get_as_pypicongpu( + self, + dict_species_picmi_to_pypicongpu: dict[PICMISpecies, typing.Any], + ) -> PyPIConGPUEnergyDensity: + """ + Convert to a PyPIConGPU EnergyDensity source. + + @param dict_species_picmi_to_pypicongpu Mapping of PICMI to PyPIConGPU species. + @return A PyPIConGPU EnergyDensity instance with the same filter and species. + @throw ValueError If species is unknown or unmapped to a PyPIConGPUSpecies. + """ + self.check() + + if self.species not in dict_species_picmi_to_pypicongpu.keys(): + raise ValueError(f"Species {self.species} is not known to Simulation") + + pypicongpu_species = dict_species_picmi_to_pypicongpu.get(self.species) + + if pypicongpu_species is None: + raise ValueError(f"Species {self.species} is not mapped to a PyPIConGPUSpecies.") + + return PyPIConGPUEnergyDensity(filter=self.filter, species=pypicongpu_species) diff --git a/lib/python/picongpu/picmi/diagnostics/openpmd_sources/energy_density_cutoff.py b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/energy_density_cutoff.py new file mode 100644 index 0000000000..659d9e3c58 --- /dev/null +++ b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/energy_density_cutoff.py @@ -0,0 +1,77 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from .source_base import SourceBase +from ....pypicongpu.output.openpmd_sources import EnergyDensityCutoff as PyPIConGPUEnergyDensityCutoff +from ...species import Species as PICMISpecies +import typeguard +import typing + + +@typeguard.typechecked +class EnergyDensityCutoff(SourceBase): + """ + Kinetic energy density data source with cutoff for openPMD output + + Derives a scalar field of kinetic energy density (in J/m^3) for a specified particle species, + optionally filtered, including only particles with kinetic energy below a user-defined cutoff, + in particle-in-cell simulations. Uses weighting, momentum, and mass attributes, mapped to cells + by the PIC code's spatial shape. + + @param species Particle species to calculate energy density for (e.g., electrons, ions). + Must have weighting, momentum, and mass attributes. + @param filter Name of a filter to select particles. Default: "all". + @param cutoff_max_energy Maximum kinetic energy cutoff (in Joules). + """ + + def __init__(self, species: PICMISpecies, filter: str = "all", cutoff_max_energy: float = None): + self.species = species + self.filter = filter + self.cutoff_max_energy = cutoff_max_energy + self.check() + + def check(self) -> None: + """ + Validate the parameters. + + @throw ValueError If filter is not a string, species is not a PICMISpecies, or cutoff_max_energy is invalid. + """ + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, PICMISpecies): + raise ValueError(f"Species must be a PICMISpecies, got {type(self.species)}") + if self.cutoff_max_energy is not None and not isinstance(self.cutoff_max_energy, (int, float)): + raise ValueError(f"cutoff_max_energy must be a number or None, got {type(self.cutoff_max_energy)}") + if self.cutoff_max_energy is not None and self.cutoff_max_energy <= 0: + raise ValueError(f"cutoff_max_energy must be positive, got {self.cutoff_max_energy}") + + def get_as_pypicongpu( + self, + dict_species_picmi_to_pypicongpu: dict[PICMISpecies, typing.Any], + ) -> PyPIConGPUEnergyDensityCutoff: + """ + Convert to a PyPIConGPU EnergyDensityCutoff source. + + @param dict_species_picmi_to_pypicongpu Mapping of PICMI to PyPIConGPU species. + @return A PyPIConGPU EnergyDensityCutoff instance with the same filter, species, and cutoff. + @throw ValueError If species is unknown or unmapped to a PyPIConGPUSpecies. + """ + self.check() + + if self.species not in dict_species_picmi_to_pypicongpu.keys(): + raise ValueError(f"Species {self.species} is not known to Simulation") + + pypicongpu_species = dict_species_picmi_to_pypicongpu.get(self.species) + + if pypicongpu_species is None: + raise ValueError(f"Species {self.species} is not mapped to a PyPIConGPUSpecies.") + + return PyPIConGPUEnergyDensityCutoff( + filter=self.filter, + species=pypicongpu_species, + cutoff_max_energy=self.cutoff_max_energy, + ) diff --git a/lib/python/picongpu/picmi/diagnostics/openpmd_sources/larmor_power.py b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/larmor_power.py new file mode 100644 index 0000000000..3db7bb4628 --- /dev/null +++ b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/larmor_power.py @@ -0,0 +1,67 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from .source_base import SourceBase +from ....pypicongpu.output.openpmd_sources import LarmorPower as PyPIConGPULarmorPower +from ...species import Species as PICMISpecies +import typeguard +import typing + + +@typeguard.typechecked +class LarmorPower(SourceBase): + """ + Radiated Larmor power data source for openPMD output + + Derives a scalar field of radiated power (in Joules) for a specified particle species, + optionally filtered, using the Larmor formula in particle-in-cell simulations. Uses + weighting, position, momentum, momentumPrev1, mass, and charge attributes, mapped to + cells by the PIC code's spatial shape. + + @param species Particle species to calculate Larmor power for (e.g., electrons, ions). + Must have weighting, position, momentum, momentumPrev1, mass, and charge attributes. + @param filter Name of a filter to select particles. Default: "all". + """ + + def __init__(self, species: PICMISpecies, filter: str = "all"): + self.species = species + self.filter = filter + self.check() + + def check(self) -> None: + """ + Validate the parameters. + + @throw ValueError If filter is not a string or species is not a PICMISpecies. + """ + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, PICMISpecies): + raise ValueError(f"Species must be a PICMISpecies, got {type(self.species)}") + + def get_as_pypicongpu( + self, + dict_species_picmi_to_pypicongpu: dict[PICMISpecies, typing.Any], + ) -> PyPIConGPULarmorPower: + """ + Convert to a PyPIConGPU LarmorPower source. + + @param dict_species_picmi_to_pypicongpu Mapping of PICMI to PyPIConGPU species. + @return A PyPIConGPU LarmorPower instance with the same filter and species. + @throw ValueError If species is unknown or unmapped to a PyPIConGPUSpecies. + """ + self.check() + + if self.species not in dict_species_picmi_to_pypicongpu.keys(): + raise ValueError(f"Species {self.species} is not known to Simulation") + + pypicongpu_species = dict_species_picmi_to_pypicongpu.get(self.species) + + if pypicongpu_species is None: + raise ValueError(f"Species {self.species} is not mapped to a PyPIConGPUSpecies.") + + return PyPIConGPULarmorPower(filter=self.filter, species=pypicongpu_species) diff --git a/lib/python/picongpu/picmi/diagnostics/openpmd_sources/macro_counter.py b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/macro_counter.py new file mode 100644 index 0000000000..f9e3fb0c59 --- /dev/null +++ b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/macro_counter.py @@ -0,0 +1,65 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from .source_base import SourceBase +from ....pypicongpu.output.openpmd_sources import MacroCounter as PyPIConGPUMacroCounter +from ...species import Species as PICMISpecies +import typeguard +import typing + + +@typeguard.typechecked +class MacroCounter(SourceBase): + """ + Macro-particle counter data source for openPMD output + + Derives a scalar field counting macro-particles per cell for a specified particle species, + optionally filtered, in particle-in-cell simulations. Assigns each macro-particle directly + to its cell via floor operation. Intended for debugging (e.g., validating particle memory). + + @param species Particle species to count (e.g., electrons, ions). + @param filter Name of a filter to select particles. Default: "all". + """ + + def __init__(self, species: PICMISpecies, filter: str = "all"): + self.species = species + self.filter = filter + self.check() + + def check(self) -> None: + """ + Validate the parameters. + + @throw ValueError If filter is not a string or species is not a PICMISpecies. + """ + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, PICMISpecies): + raise ValueError(f"Species must be a PICMISpecies, got {type(self.species)}") + + def get_as_pypicongpu( + self, + dict_species_picmi_to_pypicongpu: dict[PICMISpecies, typing.Any], + ) -> PyPIConGPUMacroCounter: + """ + Convert to a PyPIConGPU MacroCounter source. + + @param dict_species_picmi_to_pypicongpu Mapping of PICMI to PyPIConGPU species. + @return A PyPIConGPU MacroCounter instance with the same filter and species. + @throw ValueError If species is unknown or unmapped to a PyPIConGPUSpecies. + """ + self.check() + + if self.species not in dict_species_picmi_to_pypicongpu.keys(): + raise ValueError(f"Species {self.species} is not known to Simulation") + + pypicongpu_species = dict_species_picmi_to_pypicongpu.get(self.species) + + if pypicongpu_species is None: + raise ValueError(f"Species {self.species} is not mapped to a PyPIConGPUSpecies.") + + return PyPIConGPUMacroCounter(filter=self.filter, species=pypicongpu_species) diff --git a/lib/python/picongpu/picmi/diagnostics/openpmd_sources/mid_current_density_component.py b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/mid_current_density_component.py new file mode 100644 index 0000000000..1c2fa7dd41 --- /dev/null +++ b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/mid_current_density_component.py @@ -0,0 +1,77 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from .source_base import SourceBase +from ....pypicongpu.output.openpmd_sources import MidCurrentDensityComponent as PyPIConGPUMidCurrentDensityComponent +from ...species import Species as PICMISpecies +import typeguard +import typing + + +@typeguard.typechecked +class MidCurrentDensityComponent(SourceBase): + """ + Current density component data source for openPMD output + + Derives a scalar field of current density (in A/m^2) in a specified direction (x=0, y=1, z=2) + for a specified particle species, optionally filtered, in particle-in-cell simulations. Uses + weighting, position, momentum, mass, and charge attributes, mapped to cells by the PIC code's + spatial shape. Intended for debugging (e.g., validating current solvers). + + @param species Particle species to calculate current density for (e.g., electrons, ions). + Must have weighting, position, momentum, mass, and charge attributes. + @param filter Name of a filter to select particles. Default: "all". + @param direction Direction of current density (0=x, 1=y, 2=z). + """ + + def __init__(self, species: PICMISpecies, filter: str = "all", direction: int = 0): + self.species = species + self.filter = filter + self.direction = direction + self.check() + + def check(self) -> None: + """ + Validate the parameters. + + @throw ValueError If filter is not a string, species is not a PICMISpecies, or direction is invalid. + """ + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, PICMISpecies): + raise ValueError(f"Species must be a PICMISpecies, got {type(self.species)}") + if not isinstance(self.direction, int): + raise ValueError(f"Direction must be an integer, got {type(self.direction)}") + if self.direction not in [0, 1, 2]: + raise ValueError(f"Direction must be 0 (x), 1 (y), or 2 (z), got {self.direction}") + + def get_as_pypicongpu( + self, + dict_species_picmi_to_pypicongpu: dict[PICMISpecies, typing.Any], + ) -> PyPIConGPUMidCurrentDensityComponent: + """ + Convert to a PyPIConGPU MidCurrentDensityComponent source. + + @param dict_species_picmi_to_pypicongpu Mapping of PICMI to PyPIConGPU species. + @return A PyPIConGPU MidCurrentDensityComponent instance with the same filter, species, and direction. + @throw ValueError If species is unknown or unmapped to a PyPIConGPUSpecies. + """ + self.check() + + if self.species not in dict_species_picmi_to_pypicongpu.keys(): + raise ValueError(f"Species {self.species} is not known to Simulation") + + pypicongpu_species = dict_species_picmi_to_pypicongpu.get(self.species) + + if pypicongpu_species is None: + raise ValueError(f"Species {self.species} is not mapped to a PyPIConGPUSpecies.") + + return PyPIConGPUMidCurrentDensityComponent( + filter=self.filter, + species=pypicongpu_species, + direction=self.direction, + ) diff --git a/lib/python/picongpu/picmi/diagnostics/openpmd_sources/momentum.py b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/momentum.py new file mode 100644 index 0000000000..2240cab6f1 --- /dev/null +++ b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/momentum.py @@ -0,0 +1,76 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from .source_base import SourceBase +from ....pypicongpu.output.openpmd_sources import Momentum as PyPIConGPUMomentum +from ...species import Species as PICMISpecies +import typeguard +import typing + + +@typeguard.typechecked +class Momentum(SourceBase): + """ + Momentum component data source for openPMD output + + Derives a scalar field of a specified momentum component (in kg·m/s, direction x=0, y=1, z=2) + for a specified particle species, optionally filtered, in particle-in-cell simulations. Uses + position and momentum attributes, mapped to cells by the PIC code's spatial shape. + + @param species Particle species to calculate momentum for (e.g., electrons, ions). + Must have position and momentum attributes. + @param filter Name of a filter to select particles. Default: "all". + @param direction Momentum component direction (0=x, 1=y, 2=z). + """ + + def __init__(self, species: PICMISpecies, filter: str = "all", direction: int = 0): + self.species = species + self.filter = filter + self.direction = direction + self.check() + + def check(self) -> None: + """ + Validate the parameters. + + @throw ValueError If filter is not a string, species is not a PICMISpecies, or direction is invalid. + """ + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, PICMISpecies): + raise ValueError(f"Species must be a PICMISpecies, got {type(self.species)}") + if not isinstance(self.direction, int): + raise ValueError(f"Direction must be an integer, got {type(self.direction)}") + if self.direction not in [0, 1, 2]: + raise ValueError(f"Direction must be 0 (x), 1 (y), or 2 (z), got {self.direction}") + + def get_as_pypicongpu( + self, + dict_species_picmi_to_pypicongpu: dict[PICMISpecies, typing.Any], + ) -> PyPIConGPUMomentum: + """ + Convert to a PyPIConGPU Momentum source. + + @param dict_species_picmi_to_pypicongpu Mapping of PICMI to PyPIConGPU species. + @return A PyPIConGPU Momentum instance with the same filter, species, and direction. + @throw ValueError If species is unknown or unmapped to a PyPIConGPUSpecies. + """ + self.check() + + if self.species not in dict_species_picmi_to_pypicongpu.keys(): + raise ValueError(f"Species {self.species} is not known to Simulation") + + pypicongpu_species = dict_species_picmi_to_pypicongpu.get(self.species) + + if pypicongpu_species is None: + raise ValueError(f"Species {self.species} is not mapped to a PyPIConGPUSpecies.") + + return PyPIConGPUMomentum( + filter=self.filter, + species=pypicongpu_species, + direction=self.direction, + ) diff --git a/lib/python/picongpu/picmi/diagnostics/openpmd_sources/momentum_density.py b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/momentum_density.py new file mode 100644 index 0000000000..0abfbca0f1 --- /dev/null +++ b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/momentum_density.py @@ -0,0 +1,76 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from .source_base import SourceBase +from ....pypicongpu.output.openpmd_sources import MomentumDensity as PyPIConGPUMomentumDensity +from ...species import Species as PICMISpecies +import typeguard +import typing + + +@typeguard.typechecked +class MomentumDensity(SourceBase): + """ + Momentum density component data source for openPMD output + + Derives a scalar field of momentum density (in kg·m/s/m^3, direction x=0, y=1, z=2) + for a specified particle species, optionally filtered, in particle-in-cell simulations. Uses + position and momentum attributes, mapped to cells by the PIC code's spatial shape. + + @param species Particle species to calculate momentum density for (e.g., electrons, ions). + Must have position and momentum attributes. + @param filter Name of a filter to select particles. Default: "all". + @param direction Momentum density component direction (0=x, 1=y, 2=z). + """ + + def __init__(self, species: PICMISpecies, filter: str = "all", direction: int = 0): + self.species = species + self.filter = filter + self.direction = direction + self.check() + + def check(self) -> None: + """ + Validate the parameters. + + @throw ValueError If filter is not a string, species is not a PICMISpecies, or direction is invalid. + """ + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, PICMISpecies): + raise ValueError(f"Species must be a PICMISpecies, got {type(self.species)}") + if not isinstance(self.direction, int): + raise ValueError(f"Direction must be an integer, got {type(self.direction)}") + if self.direction not in [0, 1, 2]: + raise ValueError(f"Direction must be 0 (x), 1 (y), or 2 (z), got {self.direction}") + + def get_as_pypicongpu( + self, + dict_species_picmi_to_pypicongpu: dict[PICMISpecies, typing.Any], + ) -> PyPIConGPUMomentumDensity: + """ + Convert to a PyPIConGPU MomentumDensity source. + + @param dict_species_picmi_to_pypicongpu Mapping of PICMI to PyPIConGPU species. + @return A PyPIConGPU MomentumDensity instance with the same filter, species, and direction. + @throw ValueError If species is unknown or unmapped to a PyPIConGPUSpecies. + """ + self.check() + + if self.species not in dict_species_picmi_to_pypicongpu.keys(): + raise ValueError(f"Species {self.species} is not known to Simulation") + + pypicongpu_species = dict_species_picmi_to_pypicongpu.get(self.species) + + if pypicongpu_species is None: + raise ValueError(f"Species {self.species} is not mapped to a PyPIConGPUSpecies.") + + return PyPIConGPUMomentumDensity( + filter=self.filter, + species=pypicongpu_species, + direction=self.direction, + ) diff --git a/lib/python/picongpu/picmi/diagnostics/openpmd_sources/source_base.py b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/source_base.py new file mode 100644 index 0000000000..c0e05046fe --- /dev/null +++ b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/source_base.py @@ -0,0 +1,59 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from abc import ABCMeta, abstractmethod +import typeguard +import typing + + +@typeguard.typechecked +class SourceBase(metaclass=ABCMeta): + """ + Abstract base class for openPMD data sources in particle-in-cell simulations. + + Defines the interface for data sources output via the openPMD standard, + such as charge density or other derived attributes. + + Subclasses must implement the filter property, check method, and get_as_pypicongpu method. + """ + + @property + @abstractmethod + def filter(self) -> typing.Optional[str]: + """ + Name of a filter to select particles contributing to the data source. + + Returns + ------- + str or None + The filter name, or None if no filter is applied. + """ + pass + + @abstractmethod + def check(self) -> None: + """ + Validate the data source parameters. + + Raises + ------ + ValueError + If the parameters (e.g., filter) are invalid. + """ + pass + + @abstractmethod + def get_as_pypicongpu(self) -> typing.Any: + """ + Convert this data source to a PyPIConGPU equivalent. + + Returns + ------- + Any + A PyPIConGPU data source object (e.g., pypicongpu.output.openpmd_source.ChargeDensity). + """ + pass diff --git a/lib/python/picongpu/picmi/diagnostics/openpmd_sources/weighted_velocity.py b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/weighted_velocity.py new file mode 100644 index 0000000000..80356d33f0 --- /dev/null +++ b/lib/python/picongpu/picmi/diagnostics/openpmd_sources/weighted_velocity.py @@ -0,0 +1,77 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from .source_base import SourceBase +from ....pypicongpu.output.openpmd_sources import WeightedVelocity as PyPIConGPUWeightedVelocity +from ...species import Species as PICMISpecies +import typeguard +import typing + + +@typeguard.typechecked +class WeightedVelocity(SourceBase): + """ + Weighted velocity component data source for openPMD output + + Derives a scalar field of a weighted velocity component (in m/s, direction x=0, y=1, z=2) + for a specified particle species, optionally filtered, in particle-in-cell simulations. Uses + position, momentum, weighting, and mass attributes, mapped to cells by the PIC code's spatial + shape. Use with AveragedAttribute to calculate average velocity. + + @param species Particle species to calculate weighted velocity for (e.g., electrons, ions). + Must have position, momentum, weighting, and mass attributes. + @param filter Name of a filter to select particles. Default: "all". + @param direction Velocity component direction (0=x, 1=y, 2=z). + """ + + def __init__(self, species: PICMISpecies, filter: str = "all", direction: int = 0): + self.species = species + self.filter = filter + self.direction = direction + self.check() + + def check(self) -> None: + """ + Validate the parameters. + + @throw ValueError If filter is not a string, species is not a PICMISpecies, or direction is invalid. + """ + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, PICMISpecies): + raise ValueError(f"Species must be a PICMISpecies, got {type(self.species)}") + if not isinstance(self.direction, int): + raise ValueError(f"Direction must be an integer, got {type(self.direction)}") + if self.direction not in [0, 1, 2]: + raise ValueError(f"Direction must be 0 (x), 1 (y), or 2 (z), got {self.direction}") + + def get_as_pypicongpu( + self, + dict_species_picmi_to_pypicongpu: dict[PICMISpecies, typing.Any], + ) -> PyPIConGPUWeightedVelocity: + """ + Convert to a PyPIConGPU WeightedVelocity source. + + @param dict_species_picmi_to_pypicongpu Mapping of PICMI to PyPIConGPU species. + @return A PyPIConGPU WeightedVelocity instance with the same filter, species, and direction. + @throw ValueError If species is unknown or unmapped to a PyPIConGPUSpecies. + """ + self.check() + + if self.species not in dict_species_picmi_to_pypicongpu.keys(): + raise ValueError(f"Species {self.species} is not known to Simulation") + + pypicongpu_species = dict_species_picmi_to_pypicongpu.get(self.species) + + if pypicongpu_species is None: + raise ValueError(f"Species {self.species} is not mapped to a PyPIConGPUSpecies.") + + return PyPIConGPUWeightedVelocity( + filter=self.filter, + species=pypicongpu_species, + direction=self.direction, + ) diff --git a/lib/python/picongpu/picmi/diagnostics/phase_space.py b/lib/python/picongpu/picmi/diagnostics/phase_space.py index ec3872906b..c58567c788 100644 --- a/lib/python/picongpu/picmi/diagnostics/phase_space.py +++ b/lib/python/picongpu/picmi/diagnostics/phase_space.py @@ -76,6 +76,7 @@ def get_as_pypicongpu( dict_species_picmi_to_pypicongpu: dict[PICMISpecies, PyPIConGPUSpecies], time_step_size, num_steps, + simulation_box=None, # Added to match OpenPMD signature, not used ) -> PyPIConGPUPhaseSpace: self.check() diff --git a/lib/python/picongpu/picmi/diagnostics/png.py b/lib/python/picongpu/picmi/diagnostics/png.py index f8626e0cf0..54ef2ce602 100644 --- a/lib/python/picongpu/picmi/diagnostics/png.py +++ b/lib/python/picongpu/picmi/diagnostics/png.py @@ -189,6 +189,7 @@ def get_as_pypicongpu( dict_species_picmi_to_pypicongpu: dict[PICMISpecies, PyPIConGPUSpecies], time_step_size, num_steps, + simulation_box=None, # Added to match OpenPMD signature, not used ) -> PyPIConGPUPNG: self.check() diff --git a/lib/python/picongpu/picmi/diagnostics/rangespec.py b/lib/python/picongpu/picmi/diagnostics/rangespec.py new file mode 100644 index 0000000000..a9fe4cac27 --- /dev/null +++ b/lib/python/picongpu/picmi/diagnostics/rangespec.py @@ -0,0 +1,157 @@ +""" +This file is part of PIConGPU. +Copyright 2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from typing import Tuple +from ...pypicongpu.output import RangeSpec as PyPIConGPURangeSpec +import re + + +class RangeSpec: + """ + A class to specify a contiguous range of cells for simulation output in 1D, 2D, or 3D. + + This class parses a string in the format "begin:end" (1D), "begin:end,begin:end" (2D), + or "begin:end,begin:end,begin:end" (3D) to define inclusive cell ranges for the simulation + dimensions. For example: + - 1D: RangeSpec("0:10") specifies cells 0 to 10 (x). + - 2D: RangeSpec("0:10,5:15") specifies cells 0 to 10 (x) and 5 to 15 (y). + - 3D: RangeSpec("0:10,5:15,2:8") specifies cells 0 to 10 (x), 5 to 15 (y), 2 to 8 (z). + The default ":", ":,:" or ":,:,:," includes all cells in the simulation box for 1D, 2D or 3D. + Values are clipped to the simulation box boundaries, and omitted bounds (":") indicate the + full extent of the dimension. Negative indices are supported, counting from the end of the + simulation box. + + Example usage: + # 1D: rs = RangeSpec("0:10") # x: cells 0 to 10 + # rs = RangeSpec("-5:-1") # x: last 5 cells (15 to 19 for 20-cell box) + # 2D: rs = RangeSpec("0:10,5:15") # x: 0 to 10, y: 5 to 15 + # rs = RangeSpec("-5:-1,0:15") # x: last 5 cells (15 to 19 for 20-cell box), y: 0 to 15 + # 3D: rs = RangeSpec("0:10,5:15,:") # x: 0 to 10, y: 5 to 15, z: full range + # rs = RangeSpec("-5:-1,-10:-2,2:8") # x: last 5 cells (15 to 19 for 20-cell box), y: last 9 to 2 cells (20 to 28 for 30-cell box), z: 2 to 8 + """ + + def __init__(self, range_str: str): + """ + Initialize a RangeSpec from a string. + + :param range_str: A string specifying cell ranges for 1 to 3 dimensions, e.g., "0:10" + (1D), "0:10,5:15" (2D), or "0:10,5:15,2:8" (3D). + :raises ValueError: If the string format is invalid or contains non-integer bounds. + """ + self.range_str = range_str + self.slices = self._parse_range(range_str) + self._validate() + + def _parse_range(self, range_str: str) -> Tuple[slice, ...]: + """ + Parse the range string into a tuple of slice objects for each dimension. + + :param range_str: Input string (e.g., "0:10,5:15,2:8"). + :return: Tuple of 1 to 3 slice objects. + :raises ValueError: If the string format is invalid or has more than 3 dimensions. + """ + # Split the string into dimension parts + parts = range_str.split(",") + if len(parts) > 3: + raise ValueError(f"Range must specify at most three dimensions, got {len(parts)}: {range_str}") + + slices = [] + for i, part in enumerate(parts): + part = part.strip() + if part == ":": + slices.append(slice(None, None, 1)) + continue + + # Match "begin:end" or single ":" using regex + match = re.match(r"^([-]?\d+)?(:([-]?\d+)?)?$", part) + if not match: + raise ValueError(f"Invalid range format for dimension {i+1}: {part}. Expected 'begin:end' or ':'") + + start, _, end = match.groups() + start = int(start) if start is not None else None + end = int(end) if end is not None else None + + # Step is always 1 for contiguous ranges + slices.append(slice(start, end, 1)) + + return tuple(slices) + + def _validate(self): + """ + Validate the parsed slices. + + :raises ValueError: If slices have invalid step values. + """ + for i, s in enumerate(self.slices): + if s.step is not None and s.step != 1: + raise ValueError( + f"Step size must be 1 in dimension {i+1} since RangeSpec only supports contiguous ranges. Got {s.step}." + ) + + def _interpret_nones(self, spec: slice, dim_size: int) -> slice: + """ + Replace None in slice bounds with simulation box limits (0 for start, dim_size-1 for stop). + + :param spec: Input slice. + :param dim_size: Size of the simulation box in the dimension. + :return: Slice with explicit bounds. + """ + return slice( + 0 if spec.start is None else spec.start, + dim_size - 1 if spec.stop is None else spec.stop, + 1, + ) + + def _interpret_negatives(self, spec: slice, dim_size: int) -> slice: + """ + Convert negative indices to positive, clipping to simulation box. + + :param spec: Input slice. + :param dim_size: Size of the simulation box in the dimension. + :return: Slice with non-negative bounds, clipped to [0, dim_size-1]. + """ + if dim_size <= 0: + raise ValueError(f"Dimension size must be positive. Got {dim_size}.") + + start = spec.start if spec.start is not None else 0 + stop = spec.stop if spec.stop is not None else dim_size - 1 + + # Convert negative indices + start = dim_size + start if start < 0 else start + stop = dim_size + stop if stop < 0 else stop + + # Clip to simulation box + start = max(0, min(start, dim_size - 1)) + stop = max(0, min(stop, dim_size - 1)) + + # Ensure start <= stop for a valid range + if start > stop: + start, stop = stop, start + + return slice(start, stop, 1) + + def get_as_pypicongpu(self, simulation_box: Tuple[int, ...]) -> PyPIConGPURangeSpec: + """ + Convert to a PyPIConGPURangeSpec object, applying simulation box clipping. + + :param simulation_box: Tuple of dimension sizes (1 to 3 dimensions). + :return: PyPIConGPURangeSpec object with clipped, non-negative slices. + :raises ValueError: If the number of ranges does not match the simulation box dimensions. + """ + if len(self.slices) != len(simulation_box): + raise ValueError( + f"Number of range specifications ({len(self.slices)}) must match " + f"simulation box dimensions ({len(simulation_box)})." + ) + + # Process each dimension + processed_slices = [ + self._interpret_negatives(self._interpret_nones(s, dim_size), dim_size) + for s, dim_size in zip(self.slices, simulation_box) + ] + + return PyPIConGPURangeSpec(processed_slices) diff --git a/lib/python/picongpu/picmi/simulation.py b/lib/python/picongpu/picmi/simulation.py index 39413d06aa..e24e19dc85 100644 --- a/lib/python/picongpu/picmi/simulation.py +++ b/lib/python/picongpu/picmi/simulation.py @@ -1,7 +1,7 @@ """ This file is part of PIConGPU. Copyright 2021-2025 PIConGPU contributors -Authors: Hannes Troepgen, Brian Edward Marre, Julian Lenz +Authors: Hannes Troepgen, Brian Edward Marre, Julian Lenz, Masoud Afshari License: GPLv3+ """ @@ -480,8 +480,14 @@ def get_as_pypicongpu(self) -> pypicongpu.simulation.Simulation: s.init_manager, pypicongpu_by_picmi_species = self.__get_init_manager() + # Extract simulation_box from grid + if isinstance(self.solver.grid, Cartesian3DGrid): + simulation_box = tuple(self.solver.grid.number_of_cells) + else: + raise ValueError("Grid must be a Cartesian3DGrid with defined number_of_cells") + s.plugins = [ - entry.get_as_pypicongpu(pypicongpu_by_picmi_species, self.time_step_size, s.time_steps) + entry.get_as_pypicongpu(pypicongpu_by_picmi_species, self.time_step_size, s.time_steps, simulation_box) for entry in self.diagnostics ] diff --git a/lib/python/picongpu/pypicongpu/__init__.py b/lib/python/picongpu/pypicongpu/__init__.py index 237dfb0293..dc7dcd7c71 100644 --- a/lib/python/picongpu/pypicongpu/__init__.py +++ b/lib/python/picongpu/pypicongpu/__init__.py @@ -1,7 +1,3 @@ -""" -internal representation of params to generate PIConGPU input files -""" - from .simulation import Simulation from .runner import Runner from .output.phase_space import PhaseSpace @@ -9,6 +5,8 @@ from .output.macro_particle_count import MacroParticleCount from .output.png import Png from .output.checkpoint import Checkpoint +from .output.openpmd import OpenPMD +from .output.openpmd_sources.source_base import SourceBase from . import laser from . import grid @@ -35,9 +33,6 @@ "MacroParticleCount", "Png", "Checkpoint", + "OpenPMD", + "SourceBase", ] - -# note: put down here b/c linter complains if imports are not at top -import sys - -assert sys.version_info.major > 3 or sys.version_info.minor >= 9, "Python 3.9 is required for PIConGPU" diff --git a/lib/python/picongpu/pypicongpu/output/__init__.py b/lib/python/picongpu/pypicongpu/output/__init__.py index 9636a64119..585025d311 100644 --- a/lib/python/picongpu/pypicongpu/output/__init__.py +++ b/lib/python/picongpu/pypicongpu/output/__init__.py @@ -4,7 +4,10 @@ from .macro_particle_count import MacroParticleCount from .png import Png from .timestepspec import TimeStepSpec +from .rangespec import RangeSpec from .checkpoint import Checkpoint +from .openpmd import OpenPMD +from .openpmd_sources.source_base import SourceBase __all__ = [ "Auto", @@ -13,5 +16,8 @@ "MacroParticleCount", "Png", "TimeStepSpec", + "RangeSpec", "Checkpoint", + "OpenPMD", + "SourceBase", ] diff --git a/lib/python/picongpu/pypicongpu/output/openpmd.py b/lib/python/picongpu/pypicongpu/output/openpmd.py new file mode 100644 index 0000000000..575bc784c4 --- /dev/null +++ b/lib/python/picongpu/pypicongpu/output/openpmd.py @@ -0,0 +1,105 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from .. import util +from .timestepspec import TimeStepSpec +from .rangespec import RangeSpec +from .plugin import Plugin +from .openpmd_sources.source_base import SourceBase + +import typeguard +import typing +from typing import Optional, List, Literal, Dict, Union + + +@typeguard.typechecked +class OpenPMD(Plugin): + period = util.build_typesafe_property(TimeStepSpec) + source = util.build_typesafe_property(Optional[List[SourceBase]]) + range = util.build_typesafe_property(Optional[RangeSpec]) + file = util.build_typesafe_property(Optional[str]) + ext = util.build_typesafe_property(Optional[Literal["bp", "h5", "sst"]]) + infix = util.build_typesafe_property(Optional[str]) + json = util.build_typesafe_property(Union[str, Dict, None]) + json_restart = util.build_typesafe_property(Union[str, Dict, None]) + data_preparation_strategy = util.build_typesafe_property( + Optional[Literal["doubleBuffer", "adios", "mappedMemory", "hdf5"]] + ) + toml = util.build_typesafe_property(Optional[str]) + particle_io_chunk_size = util.build_typesafe_property(Optional[int]) + file_writing = util.build_typesafe_property(Optional[Literal["create", "append"]]) + + _name = "openpmd" + + def __init__( + self, + period: TimeStepSpec, + source: Optional[List[SourceBase]] = None, + range: Optional[RangeSpec] = None, + file: Optional[str] = None, + ext: Optional[Literal["bp", "h5", "sst"]] = "bp", + infix: Optional[str] = "NULL", + json: Optional[Union[str, Dict]] = None, + json_restart: Optional[Union[str, Dict]] = None, + data_preparation_strategy: Optional[Literal["doubleBuffer", "adios", "mappedMemory", "hdf5"]] = None, + toml: Optional[str] = None, + particle_io_chunk_size: Optional[int] = None, + file_writing: Optional[Literal["create", "append"]] = "create", + ): + self.period = period + self.source = source + self.range = range + self.file = file + self.ext = ext + self.infix = infix + self.json = None if json is None or json == {} else json + self.json_restart = None if json_restart is None or json_restart == {} else json_restart + self.data_preparation_strategy = data_preparation_strategy + self.toml = toml + self.particle_io_chunk_size = particle_io_chunk_size + self.file_writing = file_writing + self.check() + + def check(self) -> None: + """ + Validate the provided parameters. + """ + if self.particle_io_chunk_size is not None and self.particle_io_chunk_size < 1: + raise ValueError("particle_io_chunk_size (in MiB) must be positive") + if self.ext == "sst" and self.infix is not None and self.infix != "NULL": + raise ValueError("infix must be 'NULL' when ext is 'sst'") + if self.source is not None and not all(isinstance(s, SourceBase) for s in self.source): + raise ValueError("source must be a list of SourceBase objects") + + def _get_serialized(self) -> typing.Dict: + """ + Serialize the OpenPMD object to a JSON-compatible dictionary. + """ + self.check() + + # Convert RangeSpec to string format + range_context = self.range._get_serialized() if self.range is not None else None + if range_context: + specs = range_context["specs"] + range_str = ",".join(f"{spec['start']}:{spec['stop']}" for spec in specs) + else: + range_str = None + + return { + "period": self.period._get_serialized(), + "source": [s._get_serialized() for s in self.source] if self.source is not None else None, + "range": range_str, + "file": self.file, + "ext": self.ext, + "infix": self.infix, + "json": self.json, + "json_restart": self.json_restart, + "data_preparation_strategy": self.data_preparation_strategy, + "toml": self.toml, + "particle_io_chunk_size": self.particle_io_chunk_size, + "file_writing": self.file_writing, + } diff --git a/lib/python/picongpu/pypicongpu/output/openpmd_sources/__init__.py b/lib/python/picongpu/pypicongpu/output/openpmd_sources/__init__.py new file mode 100644 index 0000000000..8bad5040c4 --- /dev/null +++ b/lib/python/picongpu/pypicongpu/output/openpmd_sources/__init__.py @@ -0,0 +1,35 @@ +from .auto import Auto +from .bound_electron_density import BoundElectronDensity +from .charge_density import ChargeDensity +from .counter import Counter +from .density import Density +from .derived_attributes import DerivedAttributes +from .energy import Energy +from .energy_density import EnergyDensity +from .energy_density_cutoff import EnergyDensityCutoff +from .larmor_power import LarmorPower +from .macro_counter import MacroCounter +from .mid_current_density_component import MidCurrentDensityComponent +from .momentum import Momentum +from .momentum_density import MomentumDensity +from .weighted_velocity import WeightedVelocity +from .source_base import SourceBase + +__all__ = [ + "Auto", + "BoundElectronDensity", + "ChargeDensity", + "Counter", + "Density", + "DerivedAttributes", + "Energy", + "EnergyDensity", + "EnergyDensityCutoff", + "LarmorPower", + "MacroCounter", + "MidCurrentDensityComponent", + "Momentum", + "MomentumDensity", + "WeightedVelocity", + "SourceBase", +] diff --git a/lib/python/picongpu/pypicongpu/output/openpmd_sources/auto.py b/lib/python/picongpu/pypicongpu/output/openpmd_sources/auto.py new file mode 100644 index 0000000000..ff395456be --- /dev/null +++ b/lib/python/picongpu/pypicongpu/output/openpmd_sources/auto.py @@ -0,0 +1,30 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from ... import util +from .source_base import SourceBase +import typeguard +import typing + + +@typeguard.typechecked +class Auto(SourceBase): + filter = util.build_typesafe_property(typing.Optional[str]) + + def __init__(self, filter: typing.Optional[str] = None): + self.filter = filter + self.check() + + def check(self) -> None: + # Validate filter parameter + if self.filter is not None and not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string or None, got {type(self.filter)}") + + def _get_serialized(self) -> typing.Dict: + # Return serialized representation + self.check() + return {"filter": self.filter} diff --git a/lib/python/picongpu/pypicongpu/output/openpmd_sources/bound_electron_density.py b/lib/python/picongpu/pypicongpu/output/openpmd_sources/bound_electron_density.py new file mode 100644 index 0000000000..37afd7773e --- /dev/null +++ b/lib/python/picongpu/pypicongpu/output/openpmd_sources/bound_electron_density.py @@ -0,0 +1,38 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from ... import util +from ...species import Species +from .source_base import SourceBase +import typeguard +import typing + + +@typeguard.typechecked +class BoundElectronDensity(SourceBase): + species = util.build_typesafe_property(Species) + filter = util.build_typesafe_property(str) + + def __init__(self, species: Species, filter: str = "all"): + self.species = species + self.filter = filter + self.check() + + def check(self) -> None: + # Validate parameters + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, Species): + raise ValueError(f"Species must be a Species, got {type(self.species)}") + + def _get_serialized(self) -> typing.Dict: + # Return serialized representation + self.check() + return { + "species": self.species.get_rendering_context(), + "filter": self.filter, + } diff --git a/lib/python/picongpu/pypicongpu/output/openpmd_sources/charge_density.py b/lib/python/picongpu/pypicongpu/output/openpmd_sources/charge_density.py new file mode 100644 index 0000000000..9052d18386 --- /dev/null +++ b/lib/python/picongpu/pypicongpu/output/openpmd_sources/charge_density.py @@ -0,0 +1,38 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from ... import util +from ...species import Species +from .source_base import SourceBase +import typeguard +import typing + + +@typeguard.typechecked +class ChargeDensity(SourceBase): + species = util.build_typesafe_property(Species) + filter = util.build_typesafe_property(str) + + def __init__(self, species: Species, filter: str = "all"): + self.species = species + self.filter = filter + self.check() + + def check(self) -> None: + # Validate parameters + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, Species): + raise ValueError(f"Species must be a Species, got {type(self.species)}") + + def _get_serialized(self) -> typing.Dict: + # Return serialized representation + self.check() + return { + "species": self.species.get_rendering_context(), + "filter": self.filter, + } diff --git a/lib/python/picongpu/pypicongpu/output/openpmd_sources/counter.py b/lib/python/picongpu/pypicongpu/output/openpmd_sources/counter.py new file mode 100644 index 0000000000..310f8a3d9b --- /dev/null +++ b/lib/python/picongpu/pypicongpu/output/openpmd_sources/counter.py @@ -0,0 +1,38 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from ... import util +from ...species import Species +from .source_base import SourceBase +import typeguard +import typing + + +@typeguard.typechecked +class Counter(SourceBase): + species = util.build_typesafe_property(Species) + filter = util.build_typesafe_property(str) + + def __init__(self, species: Species, filter: str = "all"): + self.species = species + self.filter = filter + self.check() + + def check(self) -> None: + # Validate parameters + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, Species): + raise ValueError(f"Species must be a Species, got {type(self.species)}") + + def _get_serialized(self) -> typing.Dict: + # Return serialized representation + self.check() + return { + "species": self.species.get_rendering_context(), + "filter": self.filter, + } diff --git a/lib/python/picongpu/pypicongpu/output/openpmd_sources/density.py b/lib/python/picongpu/pypicongpu/output/openpmd_sources/density.py new file mode 100644 index 0000000000..e47b798ed8 --- /dev/null +++ b/lib/python/picongpu/pypicongpu/output/openpmd_sources/density.py @@ -0,0 +1,38 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from ... import util +from ...species import Species +from .source_base import SourceBase +import typeguard +import typing + + +@typeguard.typechecked +class Density(SourceBase): + species = util.build_typesafe_property(Species) + filter = util.build_typesafe_property(str) + + def __init__(self, species: Species, filter: str = "all"): + self.species = species + self.filter = filter + self.check() + + def check(self) -> None: + # Validate parameters + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, Species): + raise ValueError(f"Species must be a Species, got {type(self.species)}") + + def _get_serialized(self) -> typing.Dict: + # Return serialized representation + self.check() + return { + "species": self.species.get_rendering_context(), + "filter": self.filter, + } diff --git a/lib/python/picongpu/pypicongpu/output/openpmd_sources/derived_attributes.py b/lib/python/picongpu/pypicongpu/output/openpmd_sources/derived_attributes.py new file mode 100644 index 0000000000..11aef5a83e --- /dev/null +++ b/lib/python/picongpu/pypicongpu/output/openpmd_sources/derived_attributes.py @@ -0,0 +1,30 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from ... import util +from .source_base import SourceBase +import typeguard +import typing + + +@typeguard.typechecked +class DerivedAttributes(SourceBase): + filter = util.build_typesafe_property(typing.Optional[str]) + + def __init__(self, filter: typing.Optional[str] = None): + self.filter = filter + self.check() + + def check(self) -> None: + # Validate filter parameter + if self.filter is not None and not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string or None, got {type(self.filter)}") + + def _get_serialized(self) -> typing.Dict: + # Return serialized representation + self.check() + return {"filter": self.filter} diff --git a/lib/python/picongpu/pypicongpu/output/openpmd_sources/energy.py b/lib/python/picongpu/pypicongpu/output/openpmd_sources/energy.py new file mode 100644 index 0000000000..7de4644aaa --- /dev/null +++ b/lib/python/picongpu/pypicongpu/output/openpmd_sources/energy.py @@ -0,0 +1,38 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from ... import util +from ...species import Species +from .source_base import SourceBase +import typeguard +import typing + + +@typeguard.typechecked +class Energy(SourceBase): + species = util.build_typesafe_property(Species) + filter = util.build_typesafe_property(str) + + def __init__(self, species: Species, filter: str = "all"): + self.species = species + self.filter = filter + self.check() + + def check(self) -> None: + # Validate parameters + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, Species): + raise ValueError(f"Species must be a Species, got {type(self.species)}") + + def _get_serialized(self) -> typing.Dict: + # Return serialized representation + self.check() + return { + "species": self.species.get_rendering_context(), + "filter": self.filter, + } diff --git a/lib/python/picongpu/pypicongpu/output/openpmd_sources/energy_density.py b/lib/python/picongpu/pypicongpu/output/openpmd_sources/energy_density.py new file mode 100644 index 0000000000..85360ea8af --- /dev/null +++ b/lib/python/picongpu/pypicongpu/output/openpmd_sources/energy_density.py @@ -0,0 +1,38 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from ... import util +from ...species import Species +from .source_base import SourceBase +import typeguard +import typing + + +@typeguard.typechecked +class EnergyDensity(SourceBase): + species = util.build_typesafe_property(Species) + filter = util.build_typesafe_property(str) + + def __init__(self, species: Species, filter: str = "all"): + self.species = species + self.filter = filter + self.check() + + def check(self) -> None: + # Validate parameters + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, Species): + raise ValueError(f"Species must be a Species, got {type(self.species)}") + + def _get_serialized(self) -> typing.Dict: + # Return serialized representation + self.check() + return { + "species": self.species.get_rendering_context(), + "filter": self.filter, + } diff --git a/lib/python/picongpu/pypicongpu/output/openpmd_sources/energy_density_cutoff.py b/lib/python/picongpu/pypicongpu/output/openpmd_sources/energy_density_cutoff.py new file mode 100644 index 0000000000..2e74b0965b --- /dev/null +++ b/lib/python/picongpu/pypicongpu/output/openpmd_sources/energy_density_cutoff.py @@ -0,0 +1,45 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from ... import util +from ...species import Species +from .source_base import SourceBase +import typeguard +import typing + + +@typeguard.typechecked +class EnergyDensityCutoff(SourceBase): + species = util.build_typesafe_property(Species) + filter = util.build_typesafe_property(str) + cutoff_max_energy = util.build_typesafe_property(typing.Optional[float]) + + def __init__(self, species: Species, filter: str = "all", cutoff_max_energy: typing.Optional[float] = None): + self.species = species + self.filter = filter + self.cutoff_max_energy = cutoff_max_energy + self.check() + + def check(self) -> None: + # Validate parameters + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, Species): + raise ValueError(f"Species must be a Species, got {type(self.species)}") + if self.cutoff_max_energy is not None and not isinstance(self.cutoff_max_energy, (int, float)): + raise ValueError(f"cutoff_max_energy must be a number or None, got {type(self.cutoff_max_energy)}") + if self.cutoff_max_energy is not None and self.cutoff_max_energy <= 0: + raise ValueError(f"cutoff_max_energy must be positive, got {self.cutoff_max_energy}") + + def _get_serialized(self) -> typing.Dict: + # Return serialized representation + self.check() + return { + "species": self.species.get_rendering_context(), + "filter": self.filter, + "cutoff_max_energy": self.cutoff_max_energy, + } diff --git a/lib/python/picongpu/pypicongpu/output/openpmd_sources/larmor_power.py b/lib/python/picongpu/pypicongpu/output/openpmd_sources/larmor_power.py new file mode 100644 index 0000000000..268ef57092 --- /dev/null +++ b/lib/python/picongpu/pypicongpu/output/openpmd_sources/larmor_power.py @@ -0,0 +1,38 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from ... import util +from ...species import Species +from .source_base import SourceBase +import typeguard +import typing + + +@typeguard.typechecked +class LarmorPower(SourceBase): + species = util.build_typesafe_property(Species) + filter = util.build_typesafe_property(str) + + def __init__(self, species: Species, filter: str = "all"): + self.species = species + self.filter = filter + self.check() + + def check(self) -> None: + # Validate parameters + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, Species): + raise ValueError(f"Species must be a Species, got {type(self.species)}") + + def _get_serialized(self) -> typing.Dict: + # Return serialized representation + self.check() + return { + "species": self.species.get_rendering_context(), + "filter": self.filter, + } diff --git a/lib/python/picongpu/pypicongpu/output/openpmd_sources/macro_counter.py b/lib/python/picongpu/pypicongpu/output/openpmd_sources/macro_counter.py new file mode 100644 index 0000000000..a71a013428 --- /dev/null +++ b/lib/python/picongpu/pypicongpu/output/openpmd_sources/macro_counter.py @@ -0,0 +1,38 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from ... import util +from ...species import Species +from .source_base import SourceBase +import typeguard +import typing + + +@typeguard.typechecked +class MacroCounter(SourceBase): + species = util.build_typesafe_property(Species) + filter = util.build_typesafe_property(str) + + def __init__(self, species: Species, filter: str = "all"): + self.species = species + self.filter = filter + self.check() + + def check(self) -> None: + # Validate parameters + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, Species): + raise ValueError(f"Species must be a Species, got {type(self.species)}") + + def _get_serialized(self) -> typing.Dict: + # Return serialized representation + self.check() + return { + "species": self.species.get_rendering_context(), + "filter": self.filter, + } diff --git a/lib/python/picongpu/pypicongpu/output/openpmd_sources/mid_current_density_component.py b/lib/python/picongpu/pypicongpu/output/openpmd_sources/mid_current_density_component.py new file mode 100644 index 0000000000..bdfa97e4a9 --- /dev/null +++ b/lib/python/picongpu/pypicongpu/output/openpmd_sources/mid_current_density_component.py @@ -0,0 +1,44 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from ... import util +from ...species import Species +from .source_base import SourceBase +import typeguard +import typing +from typing import Literal + + +@typeguard.typechecked +class MidCurrentDensityComponent(SourceBase): + species = util.build_typesafe_property(Species) + filter = util.build_typesafe_property(str) + direction = util.build_typesafe_property(Literal["x", "y", "z"]) + + def __init__(self, species: Species, filter: str = "all", direction: Literal["x", "y", "z"] = "x"): + self.species = species + self.filter = filter + self.direction = direction + self.check() + + def check(self) -> None: + # Validate parameters + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, Species): + raise ValueError(f"Species must be a Species, got {type(self.species)}") + if self.direction not in ["x", "y", "z"]: + raise ValueError(f"Direction must be 'x', 'y', or 'z', got {self.direction}") + + def _get_serialized(self) -> typing.Dict: + # Return serialized representation + self.check() + return { + "species": self.species.get_rendering_context(), + "filter": self.filter, + "direction": self.direction, + } diff --git a/lib/python/picongpu/pypicongpu/output/openpmd_sources/momentum.py b/lib/python/picongpu/pypicongpu/output/openpmd_sources/momentum.py new file mode 100644 index 0000000000..b48ddc72f2 --- /dev/null +++ b/lib/python/picongpu/pypicongpu/output/openpmd_sources/momentum.py @@ -0,0 +1,44 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from ... import util +from ...species import Species +from .source_base import SourceBase +import typeguard +import typing +from typing import Literal + + +@typeguard.typechecked +class Momentum(SourceBase): + species = util.build_typesafe_property(Species) + filter = util.build_typesafe_property(str) + direction = util.build_typesafe_property(Literal["x", "y", "z"]) + + def __init__(self, species: Species, filter: str = "all", direction: Literal["x", "y", "z"] = "x"): + self.species = species + self.filter = filter + self.direction = direction + self.check() + + def check(self) -> None: + # Validate parameters + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, Species): + raise ValueError(f"Species must be a Species, got {type(self.species)}") + if self.direction not in ["x", "y", "z"]: + raise ValueError(f"Direction must be 'x', 'y', or 'z', got {self.direction}") + + def _get_serialized(self) -> typing.Dict: + # Return serialized representation + self.check() + return { + "species": self.species.get_rendering_context(), + "filter": self.filter, + "direction": self.direction, + } diff --git a/lib/python/picongpu/pypicongpu/output/openpmd_sources/momentum_density.py b/lib/python/picongpu/pypicongpu/output/openpmd_sources/momentum_density.py new file mode 100644 index 0000000000..526354dfeb --- /dev/null +++ b/lib/python/picongpu/pypicongpu/output/openpmd_sources/momentum_density.py @@ -0,0 +1,44 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from ... import util +from ...species import Species +from .source_base import SourceBase +import typeguard +import typing +from typing import Literal + + +@typeguard.typechecked +class MomentumDensity(SourceBase): + species = util.build_typesafe_property(Species) + filter = util.build_typesafe_property(str) + direction = util.build_typesafe_property(Literal["x", "y", "z"]) + + def __init__(self, species: Species, filter: str = "all", direction: Literal["x", "y", "z"] = "x"): + self.species = species + self.filter = filter + self.direction = direction + self.check() + + def check(self) -> None: + # Validate parameters + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, Species): + raise ValueError(f"Species must be a Species, got {type(self.species)}") + if self.direction not in ["x", "y", "z"]: + raise ValueError(f"Direction must be 'x', 'y', or 'z', got {self.direction}") + + def _get_serialized(self) -> typing.Dict: + # Return serialized representation + self.check() + return { + "species": self.species.get_rendering_context(), + "filter": self.filter, + "direction": self.direction, + } diff --git a/lib/python/picongpu/pypicongpu/output/openpmd_sources/source_base.py b/lib/python/picongpu/pypicongpu/output/openpmd_sources/source_base.py new file mode 100644 index 0000000000..8a7df61288 --- /dev/null +++ b/lib/python/picongpu/pypicongpu/output/openpmd_sources/source_base.py @@ -0,0 +1,29 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from abc import ABCMeta, abstractmethod +import typeguard +import typing + + +@typeguard.typechecked +class SourceBase(metaclass=ABCMeta): + @property + @abstractmethod + def filter(self) -> typing.Optional[str]: + # Filter name for particle selection, None if no filter + pass + + @abstractmethod + def check(self) -> None: + # Validate data source parameters + pass + + @abstractmethod + def _get_serialized(self) -> typing.Dict: + # Return serialized representation for rendering + pass diff --git a/lib/python/picongpu/pypicongpu/output/openpmd_sources/weighted_velocity.py b/lib/python/picongpu/pypicongpu/output/openpmd_sources/weighted_velocity.py new file mode 100644 index 0000000000..4bb15583fb --- /dev/null +++ b/lib/python/picongpu/pypicongpu/output/openpmd_sources/weighted_velocity.py @@ -0,0 +1,44 @@ +""" +This file is part of PIConGPU. +Copyright 2025-2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from ... import util +from ...species import Species +from .source_base import SourceBase +import typeguard +import typing +from typing import Literal + + +@typeguard.typechecked +class WeightedVelocity(SourceBase): + species = util.build_typesafe_property(Species) + filter = util.build_typesafe_property(str) + direction = util.build_typesafe_property(Literal["x", "y", "z"]) + + def __init__(self, species: Species, filter: str = "all", direction: Literal["x", "y", "z"] = "x"): + self.species = species + self.filter = filter + self.direction = direction + self.check() + + def check(self) -> None: + # Validate parameters + if not isinstance(self.filter, str): + raise ValueError(f"Filter must be a string, got {type(self.filter)}") + if not isinstance(self.species, Species): + raise ValueError(f"Species must be a Species, got {type(self.species)}") + if self.direction not in ["x", "y", "z"]: + raise ValueError(f"Direction must be 'x', 'y', or 'z', got {self.direction}") + + def _get_serialized(self) -> typing.Dict: + # Return serialized representation + self.check() + return { + "species": self.species.get_rendering_context(), + "filter": self.filter, + "direction": self.direction, + } diff --git a/lib/python/picongpu/pypicongpu/output/rangespec.py b/lib/python/picongpu/pypicongpu/output/rangespec.py new file mode 100644 index 0000000000..388f70eba5 --- /dev/null +++ b/lib/python/picongpu/pypicongpu/output/rangespec.py @@ -0,0 +1,63 @@ +""" +This file is part of PIConGPU. +Copyright 2025 PIConGPU contributors +Authors: Masoud Afshari +License: GPLv3+ +""" + +from ..rendering.renderedobject import RenderedObject +from ..util import build_typesafe_property + +import typeguard + + +def _serialize(spec): + """ + Serialize a slice object to a JSON-compatible dictionary. + + :param spec: A slice object representing a range for one dimension. + :return: Dictionary with start, stop, and step keys. + :raises ValueError: If the input is not a slice. + """ + if isinstance(spec, slice): + return { + "start": spec.start if spec.start is not None else 0, + "stop": spec.stop if spec.stop is not None else -1, + "step": spec.step if spec.step is not None else 1, + } + raise ValueError(f"Unknown serialization for {spec=} as a range specifier.") + + +@typeguard.typechecked +class RangeSpec(RenderedObject): + """ + A class to specify a contiguous range of cells for simulation output in 1D, 2D, or 3D. + + This class stores a list of slices representing inclusive cell ranges for each dimension. + It is used as the output of PICMI RangeSpec.get_as_pypicongpu, with negative indices and + clipping already handled. The slices must have a step size of 1 (contiguous ranges). + + """ + + specs = build_typesafe_property(list[slice]) + + def __init__(self, specs: list[slice]): + """ + Initialize a RangeSpec with a list of slices. + + :param specs: List of 1 to 3 slice objects, one per dimension. + :raises ValueError: If specs is empty, has more than 3 slices, or contains slices with step != 1. + """ + if not specs: + raise ValueError("RangeSpec must have at least one slice.") + if len(specs) > 3: + raise ValueError(f"RangeSpec must have at most 3 slices, got {len(specs)}.") + for i, spec in enumerate(specs): + if spec.step is not None and spec.step != 1: + raise ValueError( + f"Step size must be 1 in dimension {i+1} since RangeSpec only supports contiguous ranges. Got {spec.step}." + ) + self.specs = specs + + def _get_serialized(self): + return {"specs": list(map(_serialize, self.specs))} diff --git a/lib/python/picongpu/pypicongpu/rendering/renderedobject.py b/lib/python/picongpu/pypicongpu/rendering/renderedobject.py index 89006eb9a0..409219f1b1 100644 --- a/lib/python/picongpu/pypicongpu/rendering/renderedobject.py +++ b/lib/python/picongpu/pypicongpu/rendering/renderedobject.py @@ -314,6 +314,7 @@ def get_rendering_context(self): # TODO: The schema for different registered classes are likely to be identical # upto the fact that they refer to different allowed content. # We might want to unify this in the future. + return RenderedObject.check_context_for_type( self._registered_class, { diff --git a/share/picongpu/pypicongpu/examples/laser_wakefield/main.py b/share/picongpu/pypicongpu/examples/laser_wakefield/main.py index 4047ea0232..c472efec77 100644 --- a/share/picongpu/pypicongpu/examples/laser_wakefield/main.py +++ b/share/picongpu/pypicongpu/examples/laser_wakefield/main.py @@ -192,6 +192,11 @@ picmi.diagnostics.Checkpoint( period=picmi.diagnostics.TimeStepSpec[::100], ), + picmi.diagnostics.OpenPMD( + period=picmi.diagnostics.TimeStepSpec[::100], + file="simOutput", + ext="bp", + ), ] sim.add_laser(laser, None) @@ -206,13 +211,8 @@ 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( - {"openPMD_period": 100, "openPMD_file": "simData", "openPMD_extension": "bp"}, "openPMD plugin configuration" - ) - - sim.picongpu_add_custom_user_input(output_configuration) + # output_configuration = pypicongpu.customuserinput.CustomUserInput() + # sim.picongpu_add_custom_user_input(output_configuration) if __name__ == "__main__": sim.write_input_file(OUTPUT_DIRECTORY_PATH) diff --git a/share/picongpu/pypicongpu/schema/output/openpmd.OpenPMD.json b/share/picongpu/pypicongpu/schema/output/openpmd.OpenPMD.json new file mode 100644 index 0000000000..89315acf91 --- /dev/null +++ b/share/picongpu/pypicongpu/schema/output/openpmd.OpenPMD.json @@ -0,0 +1,134 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd.OpenPMD", + "type": "object", + "description": "openPMD output configuration for PIConGPU, writing simulation data to disk using the openPMD standard.", + "unevaluatedProperties": false, + "required": [ + "period" + ], + "properties": { + "period": { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.timestepspec.TimeStepSpec", + "description": "Specification of time steps for data output." + }, + "source": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.source_base.SourceBase" + }, + "description": "List of data source objects to include in the dump (e.g., ChargeDensity, Auto)." + }, + "range": { + "type": [ + "string", + "null" + ], + "description": "Contiguous range of cells to dump fields, in 'begin:end,begin:end,begin:end' format." + }, + "file": { + "type": [ + "string", + "null" + ], + "description": "Relative or absolute file path prefix for openPMD output files." + }, + "ext": { + "type": [ + "string", + "null" + ], + "enum": [ + "bp", + "h5", + "sst", + null + ], + "description": "File extension controlling the openPMD backend (bp=ADIOS2, h5=HDF5, sst=ADIOS2/SST)." + }, + "infix": { + "type": [ + "string", + "null" + ], + "description": "Filename infix for iteration layout (e.g., '_%06T'), 'NULL' for group-based layout." + }, + "json": { + "type": [ + "string", + "object", + "null" + ], + "description": "openPMD backend configuration as JSON string or dictionary." + }, + "json_restart": { + "type": [ + "string", + "object", + "null" + ], + "description": "Backend-specific parameters for restarting, as JSON string or dictionary." + }, + "data_preparation_strategy": { + "type": [ + "string", + "null" + ], + "enum": [ + "doubleBuffer", + "adios", + "mappedMemory", + "hdf5", + null + ], + "description": "Strategy for particle data preparation (e.g., doubleBuffer, adios)." + }, + "toml": { + "type": [ + "string", + "null" + ], + "description": "Path to a TOML file for openPMD configuration." + }, + "particle_io_chunk_size": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "description": "Size of particle data chunks used in writing (in MiB)." + }, + "file_writing": { + "type": [ + "string", + "null" + ], + "enum": [ + "create", + "append", + null + ], + "description": "File access mode for writing (create=new files, append=checkpoint-restart)." + } + }, + "allOf": [ + { + "if": { + "properties": { + "ext": { + "const": "sst" + } + } + }, + "then": { + "properties": { + "infix": { + "const": "NULL" + } + } + } + } + ] +} diff --git a/share/picongpu/pypicongpu/schema/output/openpmd_sources/auto.Auto.json b/share/picongpu/pypicongpu/schema/output/openpmd_sources/auto.Auto.json new file mode 100644 index 0000000000..a22d9db0f9 --- /dev/null +++ b/share/picongpu/pypicongpu/schema/output/openpmd_sources/auto.Auto.json @@ -0,0 +1,15 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.auto.Auto", + "type": "object", + "description": "Auto data source for openPMD output in PIConGPU, enabling automatic derived attributes with an optional filter.", + "unevaluatedProperties": false, + "properties": { + "filter": { + "type": [ + "string", + "null" + ], + "description": "Name of a filter to select particles contributing to the data source." + } + } +} diff --git a/share/picongpu/pypicongpu/schema/output/openpmd_sources/bound_electron_density.BoundElectronDensity.json b/share/picongpu/pypicongpu/schema/output/openpmd_sources/bound_electron_density.BoundElectronDensity.json new file mode 100644 index 0000000000..16c29b7d8b --- /dev/null +++ b/share/picongpu/pypicongpu/schema/output/openpmd_sources/bound_electron_density.BoundElectronDensity.json @@ -0,0 +1,21 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.bound_electron_density.BoundElectronDensity", + "type": "object", + "description": "Bound electron density data source for openPMD output in PIConGPU, calculating density for bound electrons of a specified species.", + "unevaluatedProperties": false, + "required": [ + "species", + "filter" + ], + "properties": { + "species": { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species", + "description": "Particle species to calculate bound electron density for." + }, + "filter": { + "type": "string", + "description": "Name of a filter to select particles contributing to the data source.", + "default": "all" + } + } +} diff --git a/share/picongpu/pypicongpu/schema/output/openpmd_sources/charge_density.ChargeDensity.json b/share/picongpu/pypicongpu/schema/output/openpmd_sources/charge_density.ChargeDensity.json new file mode 100644 index 0000000000..3b4eb1ff44 --- /dev/null +++ b/share/picongpu/pypicongpu/schema/output/openpmd_sources/charge_density.ChargeDensity.json @@ -0,0 +1,21 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.charge_density.ChargeDensity", + "type": "object", + "description": "Charge density data source for openPMD output in PIConGPU, calculating charge density for a specified species.", + "unevaluatedProperties": false, + "required": [ + "species", + "filter" + ], + "properties": { + "species": { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species", + "description": "Particle species to calculate charge density for." + }, + "filter": { + "type": "string", + "description": "Name of a filter to select particles contributing to the data source.", + "default": "all" + } + } +} diff --git a/share/picongpu/pypicongpu/schema/output/openpmd_sources/counter.Counter.json b/share/picongpu/pypicongpu/schema/output/openpmd_sources/counter.Counter.json new file mode 100644 index 0000000000..d25e572767 --- /dev/null +++ b/share/picongpu/pypicongpu/schema/output/openpmd_sources/counter.Counter.json @@ -0,0 +1,21 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.counter.Counter", + "type": "object", + "description": "Counter data source for openPMD output in PIConGPU, counting particles of a specified species.", + "unevaluatedProperties": false, + "required": [ + "species", + "filter" + ], + "properties": { + "species": { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species", + "description": "Particle species to count." + }, + "filter": { + "type": "string", + "description": "Name of a filter to select particles contributing to the data source.", + "default": "all" + } + } +} diff --git a/share/picongpu/pypicongpu/schema/output/openpmd_sources/density.Density.json b/share/picongpu/pypicongpu/schema/output/openpmd_sources/density.Density.json new file mode 100644 index 0000000000..f4ac000d70 --- /dev/null +++ b/share/picongpu/pypicongpu/schema/output/openpmd_sources/density.Density.json @@ -0,0 +1,21 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.density.Density", + "type": "object", + "description": "Density data source for openPMD output in PIConGPU, calculating particle density for a specified species.", + "unevaluatedProperties": false, + "required": [ + "species", + "filter" + ], + "properties": { + "species": { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species", + "description": "Particle species to calculate density for." + }, + "filter": { + "type": "string", + "description": "Name of a filter to select particles contributing to the data source.", + "default": "all" + } + } +} diff --git a/share/picongpu/pypicongpu/schema/output/openpmd_sources/derived_attributes.DerivedAttributes.json b/share/picongpu/pypicongpu/schema/output/openpmd_sources/derived_attributes.DerivedAttributes.json new file mode 100644 index 0000000000..096f83fcc0 --- /dev/null +++ b/share/picongpu/pypicongpu/schema/output/openpmd_sources/derived_attributes.DerivedAttributes.json @@ -0,0 +1,15 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.derived_attributes.DerivedAttributes", + "type": "object", + "description": "Derived attributes data source for openPMD output in PIConGPU, enabling custom derived attributes with an optional filter.", + "unevaluatedProperties": false, + "properties": { + "filter": { + "type": [ + "string", + "null" + ], + "description": "Name of a filter to select particles contributing to the data source." + } + } +} diff --git a/share/picongpu/pypicongpu/schema/output/openpmd_sources/energy.Energy.json b/share/picongpu/pypicongpu/schema/output/openpmd_sources/energy.Energy.json new file mode 100644 index 0000000000..40effc992a --- /dev/null +++ b/share/picongpu/pypicongpu/schema/output/openpmd_sources/energy.Energy.json @@ -0,0 +1,21 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.energy.Energy", + "type": "object", + "description": "Energy data source for openPMD output in PIConGPU, calculating particle energy for a specified species.", + "unevaluatedProperties": false, + "required": [ + "species", + "filter" + ], + "properties": { + "species": { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species", + "description": "Particle species to calculate energy for." + }, + "filter": { + "type": "string", + "description": "Name of a filter to select particles contributing to the data source.", + "default": "all" + } + } +} diff --git a/share/picongpu/pypicongpu/schema/output/openpmd_sources/energy_density.EnergyDensity.json b/share/picongpu/pypicongpu/schema/output/openpmd_sources/energy_density.EnergyDensity.json new file mode 100644 index 0000000000..1786a3e80f --- /dev/null +++ b/share/picongpu/pypicongpu/schema/output/openpmd_sources/energy_density.EnergyDensity.json @@ -0,0 +1,21 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.energy_density.EnergyDensity", + "type": "object", + "description": "Energy density data source for openPMD output in PIConGPU, calculating energy density for a specified species.", + "unevaluatedProperties": false, + "required": [ + "species", + "filter" + ], + "properties": { + "species": { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species", + "description": "Particle species to calculate energy density for." + }, + "filter": { + "type": "string", + "description": "Name of a filter to select particles contributing to the data source.", + "default": "all" + } + } +} diff --git a/share/picongpu/pypicongpu/schema/output/openpmd_sources/energy_density_cutoff.EnergyDensityCutoff.json b/share/picongpu/pypicongpu/schema/output/openpmd_sources/energy_density_cutoff.EnergyDensityCutoff.json new file mode 100644 index 0000000000..e146915e6c --- /dev/null +++ b/share/picongpu/pypicongpu/schema/output/openpmd_sources/energy_density_cutoff.EnergyDensityCutoff.json @@ -0,0 +1,29 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.energy_density_cutoff.EnergyDensityCutoff", + "type": "object", + "description": "Energy density cutoff data source for openPMD output in PIConGPU, calculating energy density for a specified species with an optional maximum energy cutoff.", + "unevaluatedProperties": false, + "required": [ + "species", + "filter" + ], + "properties": { + "species": { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species", + "description": "Particle species to calculate energy density for." + }, + "filter": { + "type": "string", + "description": "Name of a filter to select particles contributing to the data source.", + "default": "all" + }, + "cutoff_max_energy": { + "type": [ + "number", + "null" + ], + "exclusiveMinimum": 0, + "description": "Maximum energy cutoff for particles contributing to the energy density." + } + } +} diff --git a/share/picongpu/pypicongpu/schema/output/openpmd_sources/larmor_power.LarmorPower.json b/share/picongpu/pypicongpu/schema/output/openpmd_sources/larmor_power.LarmorPower.json new file mode 100644 index 0000000000..114387edc5 --- /dev/null +++ b/share/picongpu/pypicongpu/schema/output/openpmd_sources/larmor_power.LarmorPower.json @@ -0,0 +1,21 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.larmor_power.LarmorPower", + "type": "object", + "description": "Larmor power data source for openPMD output in PIConGPU, calculating Larmor radiation power for a specified species.", + "unevaluatedProperties": false, + "required": [ + "species", + "filter" + ], + "properties": { + "species": { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species", + "description": "Particle species to calculate Larmor power for." + }, + "filter": { + "type": "string", + "description": "Name of a filter to select particles contributing to the data source.", + "default": "all" + } + } +} diff --git a/share/picongpu/pypicongpu/schema/output/openpmd_sources/macro_counter.MacroCounter.json b/share/picongpu/pypicongpu/schema/output/openpmd_sources/macro_counter.MacroCounter.json new file mode 100644 index 0000000000..23d6456681 --- /dev/null +++ b/share/picongpu/pypicongpu/schema/output/openpmd_sources/macro_counter.MacroCounter.json @@ -0,0 +1,21 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.macro_counter.MacroCounter", + "type": "object", + "description": "Macro counter data source for openPMD output in PIConGPU, counting macro-particles of a specified species.", + "unevaluatedProperties": false, + "required": [ + "species", + "filter" + ], + "properties": { + "species": { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species", + "description": "Particle species to count macro-particles for." + }, + "filter": { + "type": "string", + "description": "Name of a filter to select particles contributing to the data source.", + "default": "all" + } + } +} diff --git a/share/picongpu/pypicongpu/schema/output/openpmd_sources/mid_current_density_component.MidCurrentDensityComponent.json b/share/picongpu/pypicongpu/schema/output/openpmd_sources/mid_current_density_component.MidCurrentDensityComponent.json new file mode 100644 index 0000000000..423a839f9e --- /dev/null +++ b/share/picongpu/pypicongpu/schema/output/openpmd_sources/mid_current_density_component.MidCurrentDensityComponent.json @@ -0,0 +1,32 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.mid_current_density_component.MidCurrentDensityComponent", + "type": "object", + "description": "Mid-current density component data source for openPMD output in PIConGPU, calculating current density along a specified direction for a species.", + "unevaluatedProperties": false, + "required": [ + "species", + "filter", + "direction" + ], + "properties": { + "species": { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species", + "description": "Particle species to calculate current density for." + }, + "filter": { + "type": "string", + "description": "Name of a filter to select particles contributing to the data source.", + "default": "all" + }, + "direction": { + "type": "string", + "enum": [ + "x", + "y", + "z" + ], + "description": "Direction (x, y, or z) for the current density component.", + "default": "x" + } + } +} diff --git a/share/picongpu/pypicongpu/schema/output/openpmd_sources/momentum.Momentum.json b/share/picongpu/pypicongpu/schema/output/openpmd_sources/momentum.Momentum.json new file mode 100644 index 0000000000..e422981f0d --- /dev/null +++ b/share/picongpu/pypicongpu/schema/output/openpmd_sources/momentum.Momentum.json @@ -0,0 +1,32 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.momentum.Momentum", + "type": "object", + "description": "Momentum data source for openPMD output in PIConGPU, calculating momentum along a specified direction for a species.", + "unevaluatedProperties": false, + "required": [ + "species", + "filter", + "direction" + ], + "properties": { + "species": { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species", + "description": "Particle species to calculate momentum for." + }, + "filter": { + "type": "string", + "description": "Name of a filter to select particles contributing to the data source.", + "default": "all" + }, + "direction": { + "type": "string", + "enum": [ + "x", + "y", + "z" + ], + "description": "Direction (x, y, or z) for the momentum component.", + "default": "x" + } + } +} diff --git a/share/picongpu/pypicongpu/schema/output/openpmd_sources/momentum_density.MomentumDensity.json b/share/picongpu/pypicongpu/schema/output/openpmd_sources/momentum_density.MomentumDensity.json new file mode 100644 index 0000000000..924cdfb024 --- /dev/null +++ b/share/picongpu/pypicongpu/schema/output/openpmd_sources/momentum_density.MomentumDensity.json @@ -0,0 +1,32 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.momentum_density.MomentumDensity", + "type": "object", + "description": "Momentum density data source for openPMD output in PIConGPU, calculating momentum density along a specified direction for a species.", + "unevaluatedProperties": false, + "required": [ + "species", + "filter", + "direction" + ], + "properties": { + "species": { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species", + "description": "Particle species to calculate momentum density for." + }, + "filter": { + "type": "string", + "description": "Name of a filter to select particles contributing to the data source.", + "default": "all" + }, + "direction": { + "type": "string", + "enum": [ + "x", + "y", + "z" + ], + "description": "Direction (x, y, or z) for the momentum density component.", + "default": "x" + } + } +} diff --git a/share/picongpu/pypicongpu/schema/output/openpmd_sources/source_base.SourceBase.json b/share/picongpu/pypicongpu/schema/output/openpmd_sources/source_base.SourceBase.json new file mode 100644 index 0000000000..5caa7fccb3 --- /dev/null +++ b/share/picongpu/pypicongpu/schema/output/openpmd_sources/source_base.SourceBase.json @@ -0,0 +1,62 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.source_base.SourceBase", + "type": "object", + "description": "Abstract base class for openPMD data sources in PIConGPU, defining common properties for derived attributes.", + "unevaluatedProperties": false, + "properties": { + "filter": { + "type": [ + "string", + "null" + ], + "description": "Name of a filter to select particles contributing to the data source." + } + }, + "anyOf": [ + { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.auto.Auto" + }, + { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.bound_electron_density.BoundElectronDensity" + }, + { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.charge_density.ChargeDensity" + }, + { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.counter.Counter" + }, + { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.density.Density" + }, + { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.derived_attributes.DerivedAttributes" + }, + { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.energy.Energy" + }, + { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.energy_density.EnergyDensity" + }, + { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.energy_density_cutoff.EnergyDensityCutoff" + }, + { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.larmor_power.LarmorPower" + }, + { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.macro_counter.MacroCounter" + }, + { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.mid_current_density_component.MidCurrentDensityComponent" + }, + { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.momentum.Momentum" + }, + { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.momentum_density.MomentumDensity" + }, + { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.weighted_velocity.WeightedVelocity" + } + ] +} diff --git a/share/picongpu/pypicongpu/schema/output/openpmd_sources/weighted_velocity.WeightedVelocity.json b/share/picongpu/pypicongpu/schema/output/openpmd_sources/weighted_velocity.WeightedVelocity.json new file mode 100644 index 0000000000..924dfe9606 --- /dev/null +++ b/share/picongpu/pypicongpu/schema/output/openpmd_sources/weighted_velocity.WeightedVelocity.json @@ -0,0 +1,32 @@ +{ + "$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd_sources.weighted_velocity.WeightedVelocity", + "type": "object", + "description": "Weighted velocity data source for openPMD output in PIConGPU, calculating weighted velocity along a specified direction for a species.", + "unevaluatedProperties": false, + "required": [ + "species", + "filter", + "direction" + ], + "properties": { + "species": { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.species.species.Species", + "description": "Particle species to calculate weighted velocity for." + }, + "filter": { + "type": "string", + "description": "Name of a filter to select particles contributing to the data source.", + "default": "all" + }, + "direction": { + "type": "string", + "enum": [ + "x", + "y", + "z" + ], + "description": "Direction (x, y, or z) for the weighted velocity component.", + "default": "x" + } + } +} diff --git a/share/picongpu/pypicongpu/schema/output/plugin.Plugin.json b/share/picongpu/pypicongpu/schema/output/plugin.Plugin.json index eada288928..8fdafab27b 100644 --- a/share/picongpu/pypicongpu/schema/output/plugin.Plugin.json +++ b/share/picongpu/pypicongpu/schema/output/plugin.Plugin.json @@ -17,7 +17,8 @@ "energyhistogram", "macroparticlecount", "png", - "checkpoint" + "checkpoint", + "openpmd" ], "unevaluatedProperties": false, "properties": { @@ -38,6 +39,9 @@ }, "checkpoint": { "type": "boolean" + }, + "openpmd": { + "type": "boolean" } } }, @@ -45,10 +49,10 @@ "description": "configuration data of plugin", "anyOf": [ { - "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.phase_space.PhaseSpace" + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.auto.Auto" }, { - "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.auto.Auto" + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.phase_space.PhaseSpace" }, { "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.energy_histogram.EnergyHistogram" @@ -61,6 +65,9 @@ }, { "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.checkpoint.Checkpoint" + }, + { + "$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.output.openpmd.OpenPMD" } ] } diff --git a/share/picongpu/pypicongpu/template/etc/picongpu/N.cfg.mustache b/share/picongpu/pypicongpu/template/etc/picongpu/N.cfg.mustache index d0c57cffd5..f8d06e8af3 100644 --- a/share/picongpu/pypicongpu/template/etc/picongpu/N.cfg.mustache +++ b/share/picongpu/pypicongpu/template/etc/picongpu/N.cfg.mustache @@ -192,6 +192,24 @@ pypicongpu_output_with_newlines=" {{/openPMD}} {{/typeID.checkpoint}} +{{#typeID.openpmd}} +--openPMD.period {{#period.specs}}{{{start}}}:{{{stop}}}:{{{step}}}{{^_last}},{{/_last}}{{/period.specs}} +{{#source}} +{{#filter}}--openPMD.source.{{{species.name}}}.filter {{{filter}}}{{/filter}} +{{#direction}}--openPMD.source.{{{species.name}}}.direction {{{direction}}}{{/direction}} +{{#cutoff_max_energy}}--openPMD.source.{{{species.name}}}.cutoff_max_energy {{{cutoff_max_energy}}}{{/cutoff_max_energy}} +{{/source}} +{{#range}}--openPMD.range {{{range}}}{{/range}} +{{#file}}--openPMD.file {{{file}}}{{/file}} +{{#ext}}--openPMD.ext {{{ext}}}{{/ext}} +{{#infix}}--openPMD.infix {{{infix}}}{{/infix}} +{{#json}}--openPMD.json {{{json}}}{{/json}} +{{#json_restart}}--checkpoint.openPMD.jsonRestart {{{json_restart}}}{{/json_restart}} +{{#data_preparation_strategy}}--openPMD.dataPreparationStrategy {{{data_preparation_strategy}}}{{/data_preparation_strategy}} +{{#toml}}--openPMD.toml {{{toml}}}{{/toml}} +{{#particle_io_chunk_size}}--openPMD.particleIOChunkSize {{{particle_io_chunk_size}}}{{/particle_io_chunk_size}} +{{#file_writing}}--openPMD.writeAccess {{{file_writing}}}{{/file_writing}} +{{/typeID.openpmd}} {{/data}} {{/output}}