Source code for pisces.extensions.simulation.gadget.frontends

"""
Frontends for Gadget simulations codes.

This module provides frontend classes for working with Gadget-4 simulation codes.
These frontends facilitate the generation of initial conditions and particle datasets
compatible with Gadget-4, leveraging the underlying infrastructure provided by
the Pisces framework.
"""

from pathlib import Path

import numpy as np

from pisces.particles.gadget import Gadget4ParticleDataset

from ..core import GadgetLikeFrontend, InitialConditions3DCartesian

# Construct the local path to this file for looking up
# the default configuration file(s).
file_path = Path(__file__).absolute().parent


# --------------------------------------- #
# FRONTEND CLASSES                        #
# --------------------------------------- #
[docs] class Gadget4Frontend(GadgetLikeFrontend): """ Frontend for generating initial conditions compatible with Gadget-4. This class provides frontend access for generating initial conditions which can be fed directly to Gadget-4 simulations. Detailed notes on the usage of this frontend and the relevant settings / configuration options found in Gadget-4 can be found on the associated documentation page: :ref:`simulations_gadget`. """ # --------------------------------------- # # Class Variables and Constants # # --------------------------------------- # # These overwrite the baseclass implementations to customize # for Gadget-4. __default_configuration_path__: Path = file_path / "gadget_4_default_config.yaml" __particle_dataset_type__ = Gadget4ParticleDataset __frontend_name__ = "Gadget4Frontend" _simulation_types_map: dict[type, str] = {InitialConditions3DCartesian: "3D"} _allowed_ic_types = (InitialConditions3DCartesian,) # --------------------------------------- # # IC Generation Methods # # --------------------------------------- # # Most of the detail here is handled by the superclass GadgetLikeFrontend. # We only need to implement the method that generates the particle dataset as this # will determine some details about the file header and other relevant structures. def _generate_particle_dataset(self, *args, **kwargs): """ Generate the particle dataset for the entire simulation. Notes ----- For Gadget-like frontends, this method constructs the complete particle dataset by iterating over each model in the initial conditions, mapping their native particle types and fields to the Gadget-4 equivalents, and writing the data into the appropriate locations in the overall particle dataset. It will need to be reimplemented in subclasses if the particle dataset construction requires different parameters or additional steps. """ # Parse out the arguments that are relevant to the generation of # the particle dataset. path, *args = args # Begin by creating the IC particles dataset skeleton so that we can # populate it with data from each of the models. # # THIS MAY NEED MODIFICATION in subclasses to handle new / different # inputs to the particle dataset builder. # noinspection PyArgumentList ic_particles = self.__class__.__particle_dataset_type__.build_particle_dataset( path, # The path we generate the particles at. self._count_particles(), # The number of particles. self.config["parameters.boxsize"], # The box size. ntypes=self.config["makefile.number_of_particle_types"], # Number of particle types. unit_system=self.unit_system, # The unit system to use. double_precision=True, # Use double precision for positions / velocities. id_precision=self.config["makefile.nbits_id"], # The particle ID precision. ) # With the skeleton written, we now cycle through each of the models, # extract their particle datasets, cast the names to the right things, and # then proceed to write the data into the file. _particle_count_offsets = np.zeros(self.config["makefile.number_of_particle_types"], dtype=np.uint64) for model_name in self.initial_conditions.list_models(): # Extract the model's configuration data from the frontend # configuration file and load the particle dataset that we # are going to be using. model_config = self.config[f"models.{model_name}"] particle_dataset = self.initial_conditions.load_particles(model_name) # We iterate through each of the initial conditions' particle # types and map them to Gadget-4 types. for gptype in range(self.config["makefile.number_of_particle_types"]): # Extract relevant metadata. gpkey = f"ParticleType{gptype}" ipkey = model_config[gpkey]["name"] # Check if the model even includes this particle type. if ipkey not in particle_dataset.particle_groups: self.logger.debug( f"Model `{model_name}` does not have particles of type `{ipkey}`!" f" Skipping {self.__frontend_name__} type `{gpkey}`." ) continue for gfield, ifield in model_config[gpkey]["fields"].items(): # Skip the ID column because we are going to handle # that ourselves at the very end once all the # particles have been written. if gfield == "ParticleIDs": continue # Extract the particle array from the particle file # for this model. if f"{ipkey}.{ifield}" not in particle_dataset: raise RuntimeError( f"Model `{model_name}` is missing required field" f" `{ifield}` for particle type `{ipkey}`!\n" f"HINT: If it exists, is it named something different? Modify the" f" configuration file to match the field name in the particle dataset.\n" f"HINT: If it doesn't exist, you may need to derive it manually." ) # Access the dataset in the Gadget-4 particle dataset and # dump our data into it. field_handle = ic_particles.get_particle_field_handle(gpkey, gfield) field_unit = ic_particles.get_field_units(gpkey, gfield) # Now we write the array data into the dataset by virtue of # the tracked slicing. _slc = slice( int(_particle_count_offsets[gptype]), int(_particle_count_offsets[gptype] + particle_dataset.num_particles[ipkey]), ) field_handle[_slc, ...] = particle_dataset.get_particle_field(ipkey, ifield).to_value(field_unit) # After writing all the fields, we need to increment the particle offsets _particle_count_offsets[gptype] += particle_dataset.num_particles[ipkey] self.logger.info( f"[{self.__class__.__name__}]: Model `{model_name}` wrote" f" {particle_dataset.num_particles[ipkey]} particles of type" f" `{ipkey}` to {self.__frontend_name__} type `{gpkey}`." ) return ic_particles
[docs] def generate_initial_conditions(self, filename: str, *args, overwrite: bool = False, **kwargs): """ Generate Gadget-4–compatible initial condition files for this frontend. This method wraps the base :meth:`~pisces.extensions.simulation.core.frontends.GadgetLikeFrontend.generate_initial_conditions` lifecycle, providing the standard Gadget-4 interface for writing fully formatted HDF5 initial condition files. It performs a complete validation and generation sequence: 1. **Runtime validation** — Ensures that all models have generated particles and that the current configuration is consistent with the Gadget-4 compile-time and runtime settings. 2. **Initial condition generation** — Constructs and writes the Gadget-4 particle dataset, including all particle types, field mappings, and metadata. Parameters ---------- filename : str Name of the output HDF5 file to generate within the initial conditions directory (e.g., ``"ClusterICs.hdf5"``). The path is resolved automatically relative to :attr:`ic_directory`. overwrite : bool, default=False If ``True``, any existing file with the same name will be replaced. If ``False``, a :class:`FileExistsError` is raised when the file exists. Notes ----- - This is the preferred entry point for creating Gadget-4 initial conditions. It provides automatic logging, file overwrite handling, and lifecycle consistency. - Subclasses should not override this method unless a different output mechanism is required. - All generated files conform to the standard Gadget-4 ``ICFormat=3`` HDF5 layout. """ super().generate_initial_conditions(filename, *args, overwrite=overwrite, **kwargs)