Developer Guide: Initial Conditions#
This page is for Pisces contributors who are extending or subclassing the
InitialConditions
base class used to define, store, and manipulate
simulation initial conditions (ICs). It provides a detailed overview of the
class’s goals, responsibilities, lifecycle, extension points, and best practices for
subclassing. For a high-level user overview, see the Initial Conditions for Simulations section.
Subclassing#
The InitialConditions
class is explicitly designed for extension.
If you are writing a new frontend or simulation interface, you will almost
certainly want to define a subclass with custom metadata rules, additional
processing hooks, or specialized helper methods.
Best practices:
Module placement Define your subclass alongside the rest of your simulation or frontend code. This keeps the implementation localized and avoids cluttering the core Pisces namespace. For example:
pisces/ <simulation_code>/ __init__.py initial_conditions.py # Your subclass here frontend.py # Frontend implementation
Override minimally Subclasses should override only the parts they need:
Validation hooks (
_validate_configuration()
,_validate_model()
)Processing methods (
_process_models()
,_process_metadata()
,_process_particle_files()
or equivalents for new structures)New accessors or physics helpers
Avoid rewriting core methods like
__init__()
orcreate_ics()
unless absolutely necessary. These contain structural invariants that guarantee compatibility with the Pisces ecosystem.Preserve base contracts Always call
super()
when overriding validation or processing methods to keep the base guarantees intact. This ensures that IC directories created with your subclass can still be loaded and validated by the base class or other tools.Additive, not destructive Focus on adding capabilities rather than changing the meaning of existing methods. If you need fundamentally different semantics (e.g., an entirely new file layout), consider building a separate abstraction instead of subclassing
InitialConditions
.
Note
Place your subclass in the same module as your frontend or simulation code. This makes the dependency explicit: anyone using your frontend should import your specialized IC subclass directly.
Class Creation#
Class creation is mediated via the InitialConditions.create_ics()
class method, which
builds a new IC directory from scratch. This method handles:
Validating and processing model tuples (name, model, position, velocity, etc.)
Copying or moving model files into the IC directory
Writing the authoritative configuration file
IC_CONFIG.yaml
with metadata and modelsOptionally attaching particle datasets
Returning a ready‑to‑use
InitialConditions
instance
various elements of this process are extensible / modifiable while others are fixed to
ensure consistency and compatibility with the Pisces ecosystem. Formally, the InitialConditions.create_ics()
method proceeds as follows:
@classmethod
def create_ics(cls, directory, *models, particle_files=None, **kwargs):
"""
Create a new IC directory with the given models and optional particle files.
Returns an initialized InitialConditions instance.
"""
# --- DIRECTORY SETUP --- #
# Process the provided directory. We check that it is a valid directory
# and that it doesn't contain any existing files that need to be overwritten.
#
# This is a structural invariant of this class and should NOT be overwritten
# by subclasses to ensure that the structure is contiguous.
...
# --- MODEL PROCESSING --- #
# Validate and process the models, ensuring they conform to the expected
# structure and dimensionality. This includes copying/moving files and
# normalizing orientations.
#
# This part of the process is NOT STRUCTURALLY INVARIANT and may be modified
# by altering the ``cls._process_models`` method.
...
# --- ADDITIONAL PROCESSING --- #
# This section handles optional particle files, metadata processing,
# and any other additional processing required for the ICs.
# The methods called here are also NOT STRUCTURALLY INVARIANT and may be
# modified directly by altering the create_ics method.
...
# --- IC CONFIG GENERATION --- #
# Finally, we write the IC_CONFIG.yaml file with all the metadata and models.
...
return cls(directory)
In editing this procedure, the following key points should be kept in mind:
Structural Invariants: The directory structure and its handling (the first half of the create_ics method) should not be altered. This ensures that the ICs can be loaded and validated correctly by the base class and other Pisces components. Additionally, ALL initial conditions directories must contain an
IC_CONFIG.yaml
file, which is the authoritative source of metadata and model information.Additional metadata may be added to the
IC_CONFIG.yaml
file, but the resulting YAML file must contain themodels
key in order to ensure that the rest of the class structure can be loaded correctly.Note
Additional metadata may be added to the
IC_CONFIG.yaml
file in other groups or sections, but themodels
key must always be present in order to ensure that the rest of the class structure can be loaded correctly. You can also edit the metadata attached to the models.Additional Processing: The second half of the create_ics method is where you can extend or modify the behavior of the IC creation process. This includes processing models, handling particle files, and writing metadata. You can override methods like _process_models, _process_particle_files, and _process_metadata to customize how these elements are handled.
If you modify the
_process_metadata
method, ensure that it still returns a dictionary that contains themetadata
key with all the contents.If you modify the
_process_models
method, ensure that it returns a dictionary with the relevant metadata. The following are required pieces of metadata for each model:path
: The path to the model file.position
: A unyt array with length units.velocity
: A unyt array with velocity units.orientation
: A normalized vector of length(ndim,)
.spin
: A float representing the spin of the model.
You may choose to add additional metadata to the models, but the above keys must always be present in order to ensure that the rest of the class structure can be loaded correctly.
Initialization#
Initialization is handled through the InitialConditions.__init__()
constructor,
which loads an existing IC directory and ensures it is valid. This method
performs a series of checks and setup steps to guarantee that the instance is
ready for use:
Normalize and store the target directory path
Verify the directory exists on disk
Locate and load the authoritative
IC_CONFIG.yaml
fileInitialize the internal
ConfigManager
for autosaved config accessRun
_validate_configuration()
on the loaded configRun
_validate_model()
on each stored model
Formally, the initialization procedure is:
def __init__(self, directory: Union[str, Path]):
"""
Load an existing IC directory and validate its contents.
"""
# --- DIRECTORY VALIDATION --- #
# Ensure the provided directory exists on disk.
# This structural invariant guarantees we always work with a real folder.
...
# --- CONFIG FILE LOADING --- #
# Require that the IC_CONFIG.yaml file is present in the directory.
# Load it via ConfigManager with autosave=True.
...
# --- CONFIG VALIDATION --- #
# Delegate to self._validate_configuration() to ensure metadata consistency
# and presence of required keys.
...
# --- MODEL VALIDATION --- #
# Iterate through all stored models, calling _validate_model on each.
...
return self
Key extension points:
Structural Invariants: - The directory must exist and contain
IC_CONFIG.yaml
. - The YAML must have at least two top-level keys:metadata
andmodels
. These invariants should not be altered by subclasses, as they are critical to compatibility across Pisces components.Subclass Validation Hooks:
_validate_configuration()
performs base checks on metadata (class name, ndim, required sections). Subclasses may override to enforce extra metadata rules (e.g., requiring box size, simulation code version). Always callsuper()._validate_configuration()
first to preserve base guarantees._validate_model()
enforces presence of required fields (path, position, velocity, orientation, spin), correct dimensionality of vectors, and particle file existence. Subclasses can extend this to require additional per-model metadata.
Note
When extending validation, remember that the base checks ensure structural safety. Your overrides should add domain-specific constraints without breaking compatibility with the rest of the Pisces ecosystem.
In short, initialization guarantees that every InitialConditions
instance
represents a valid, fully consistent IC directory. Developers may tighten or
extend validation rules, but they must preserve the base invariants so that all
Pisces tools can rely on consistent structure and metadata.
Method Modification#
In addition to validation and creation hooks, subclasses are free to add new
methods or override existing ones to extend the functionality of
InitialConditions
. For example, you might add a helper that computes
angular momentum from positions and velocities, or override particle handling to
support a new file format.
That said, there are important guidelines:
Preserve behavioral contracts: Existing methods such as
add_model()
,update_model()
,remove_model()
, andcreate_ics()
have clear expectations about their inputs, side-effects, and outputs. Subclasses should not change these contracts in ways that would surprise downstream users or break other Pisces components.If you override a method, call
super()
unless you are intentionally replacing its functionality.Do not alter return types (e.g., always return a dict for model listings, always return a ready-to-use
InitialConditions
instance fromcreate_ics()
).Do not silently skip validation or autosaving — these are core guarantees of the base class.
Additive extensions are preferred: Rather than modifying existing logic heavily, consider:
Adding new methods with specialized behavior.
Wrapping or composing base methods, e.g. calling
update_model()
and then applying domain-specific adjustments.Providing subclass-only helpers for new metadata or file formats.
Consistency and compatibility: The base methods ensure IC directories remain self-contained and portable across the Pisces ecosystem. Any modifications should keep directories valid (i.e., still containing a correct
IC_CONFIG.yaml
and valid model files). This ensures that ICs created with your subclass can still be loaded by the baseInitialConditions
or by other tools.
Note
If you need radically different semantics (e.g., a completely different on-disk
layout), it is better to create a new abstraction outside of
InitialConditions
. Subclasses are intended to extend, not replace,
the guarantees of the base class.
Common Extension Patterns#
Adding new metadata#
A common subclassing task is to enrich ICs with additional metadata. This can be done at two levels:
Global metadata (applies to the IC directory as a whole) Override
_process_metadata()
to inject extra keys into themetadata
section ofIC_CONFIG.yaml
. For example, you might record a simulation code identifier, cosmological parameters, or a random seed. Always callsuper()._process_metadata
first to retain the required fields (class_name
,directory
,ndim
,created_at
).class MyICs(InitialConditions): @classmethod def _process_metadata(cls, directory, **kwargs): md = super()._process_metadata(directory, **kwargs) md["metadata"]["box_size"] = kwargs.get("box_size", 100.0) # add box size md["metadata"]["frontend"] = kwargs.get("frontend", "gadget") return md
Per-model metadata (applies to individual models) Extend
_process_models()
to add new keys into each model’s dictionary. For example, you might attach a bulk temperature, metallicity, or softening length. Make sure the returned dict for each model still contains all required fields (path
,position
,velocity
,orientation
,spin
).class MyICs(InitialConditions): @classmethod def _process_models(cls, directory, *models, **kwargs): processed = super()._process_models(directory, *models, **kwargs) for name, info in processed.items(): info["bulk_temperature"] = kwargs.get("bulk_temperature", 1e6) return processed
Accessors for new metadata After storing additional metadata, provide convenience accessors so downstream code can query them easily. For example:
class MyICs(InitialConditions): @property def box_size(self): return self.metadata.get("box_size") def model_temperatures(self): return {name: info["bulk_temperature"] for name, info in self.models.items()}
Note
You may add any additional metadata you need, but the core keys must remain
in both metadata
and models
for compatibility. These include
metadata.ndim
and, per model, the fields path
, position
,
velocity
, orientation
, and spin
.
Adding new data structures#
Sometimes models require more than particle datasets. For example, you might
want to attach merger trees, halo catalogs, or radiative transfer grids. The
recommended approach is to follow the pattern of
_process_particle_files()
:
Introduce a new processing method Define a method like
_process_trees
or_process_catalogs
that takes paths to the new data structure(s), validates them, and copies/moves them into the IC directory. Extend themodels
dictionary with a new key (e.g.,trees
orcatalog
) that stores the path.Record relational metadata Ensure your metadata clearly links the new data structure to its model(s). For example, store it under
models.<name>.trees
if each model gets its own, or create a new top-level section if the data applies globally.Provide accessors and checks Add convenience methods to query and load the new data structure, similar to
has_particles
,get_particle_path
, andload_particles
. This makes it easy for downstream users to check whether a model has an attached dataset and to load it consistently.Support lifecycle operations Offer methods to generate, add, remove, and overwrite the new data structure. For example:
add_trees_to_model(name, path, overwrite=False)
remove_trees_from_model(name, delete_file=True)
generate_trees(name, **kwargs)
These should mirror the semantics of particle handling: validate existence, manage file copy/move, update
IC_CONFIG.yaml
, and autosave.
Note
The key requirement is that your new data structure integrates cleanly with the existing config system. That means:
All references must be stored in
IC_CONFIG.yaml
under a stable key.The file(s) must be physically present inside the IC directory (or linked in a reproducible way).
Any model with an attached structure should be discoverable via accessors and listings, not hidden behind ad hoc conventions.