Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
opencosmo 0.9.4 (2025-09-26)
============================

Bugfixes
--------

- Fix a bug that could cause opening halos and galaxies only to fail


opencosmo 0.9.3 (2025-09-25)
============================

Bugfixes
--------

- Fix an issue that could cause opens with multiple files to fail in MPI contexts


opencosmo 0.9.2 (2025-09-25)
============================

Bugfixes
--------

- Fix a but that could cause opening properties and profiles without particles to fail.


opencosmo 0.9.1 (2025-09-16)
============================

Expand All @@ -7,7 +34,7 @@ Bugfixes
- Re-add license file to package for conda-forge compatability


Opencosmo 0.9.0 (2025-09-15)
opencosmo 0.9.0 (2025-09-15)
============================

Features
Expand Down Expand Up @@ -50,7 +77,7 @@ Misc
- Update evaluate to respect default parameters


Opencosmo 0.8.0 (2025-07-22)
opencosmo 0.8.0 (2025-07-22)
============================

Features
Expand Down
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
project = "OpenCosmo"
copyright = "2025, OpenCosmo Team"
author = "OpenCosmo Team"
release = "0.9.1"
release = "0.9.4"

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
Expand Down
10 changes: 9 additions & 1 deletion src/opencosmo/collection/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def get_collection_type(
2. The files contain a single non-lightcone datatype.
3. The files are linked together into a structure collection
"""

handles_by_type = {target.data_type: target.group for target in targets}
is_lightcone = [target.header.file.is_lightcone for target in targets]
unique_data_types = set(handles_by_type.keys())
Expand All @@ -60,9 +61,16 @@ def get_collection_type(

elif len(unique_data_types) == 1 and all(not il for il in is_lightcone):
return oc.SimulationCollection
elif unique_file_types == set([FILE_TYPE.PARTICLES, FILE_TYPE.DATASET]):

elif FILE_TYPE.HALO_PROPERTIES in unique_file_types and set(
[FILE_TYPE.HALO_PARTICLES, FILE_TYPE.SOD_BINS, FILE_TYPE.GALAXY_PROPERTIES]
).intersection(unique_file_types):
validate_linked_groups(handles_by_type)
return oc.StructureCollection

elif unique_file_types == {FILE_TYPE.GALAXY_PROPERTIES, FILE_TYPE.GALAXY_PARTICLES}:
return oc.StructureCollection

elif (
len(unique_file_types) == 1
and unique_file_types.pop() == FILE_TYPE.STRUCTURE_COLLECTION
Expand Down
10 changes: 10 additions & 0 deletions src/opencosmo/collection/structure/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,16 @@ def build_structure_collection(targets: list[io.io.OpenTarget], ignore_empty: bo
else:
return collection

if (
link_sources["halo_properties"]
and len(link_sources["galaxy_properties"]) == 1
and not link_targets["galaxy_targets"]
):
galaxy_properties = io.io.open_single_dataset(
link_sources["galaxy_properties"][0]
)
link_targets["halo_targets"]["galaxy_properties"] = galaxy_properties

if len(link_sources["halo_properties"]) == 1 and link_targets["halo_targets"]:
handlers = get_link_handlers(
link_sources["halo_properties"][0].group,
Expand Down
48 changes: 30 additions & 18 deletions src/opencosmo/io/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,15 @@


class FILE_TYPE(Enum):
DATASET = 0
PARTICLES = 1
LIGHTCONE = 2
STRUCTURE_COLLECTION = 3
SIMULATION_COLLECTION = 4
HALO_PROPERTIES = 0
HALO_PARTICLES = 1
GALAXY_PROPERTIES = 2
GALAXY_PARTICLES = 3
SOD_BINS = 4
LIGHTCONE = 5
STRUCTURE_COLLECTION = 6
SIMULATION_COLLECTION = 7
SYNTHETIC_CATALOG = 8


class COLLECTION_TYPE(Enum):
Expand All @@ -83,17 +87,22 @@ def data_type(self):


def get_file_type(file: h5py.File) -> FILE_TYPE:
file_keys = set(file.keys())
if len(set(["header", "data"]).intersection(file_keys)) == 2:
return FILE_TYPE.DATASET

elif all("particles" in fk or fk == "header" for fk in file_keys):
return FILE_TYPE.PARTICLES

elif "header" in file.keys():
groups = {name: group for name, group in file.items() if name != "header"}
collection.structure.io.validate_linked_groups(groups)
return FILE_TYPE.STRUCTURE_COLLECTION
if "header" in file.keys():
dtype = file["header"]["file"].attrs["data_type"]
if dtype == "halo_particles":
return FILE_TYPE.HALO_PARTICLES
elif dtype == "halo_profiles":
return FILE_TYPE.SOD_BINS
elif dtype == "halo_properties":
return FILE_TYPE.HALO_PROPERTIES
elif dtype == "galaxy_properties":
return FILE_TYPE.GALAXY_PROPERTIES
elif dtype == "galaxy_particles":
return FILE_TYPE.GALAXY_PARTICLES
elif dtype == "diffsky_fits":
return FILE_TYPE.SYNTHETIC_CATALOG
else:
raise ValueError(f"Unknown file type {dtype}")

if not all("header" in group.keys() for group in file.values()):
for subgroup in file.values():
Expand Down Expand Up @@ -206,8 +215,11 @@ def open(

"""
if len(files) == 1 and isinstance(files[0], list):
return oc.open(*files[0], **open_kwargs)
handles = [h5py.File(f) for f in files]
file_list = files[0]
else:
file_list = list(files)
file_list.sort()
handles = [h5py.File(f) for f in file_list]
file_types = list(map(get_file_type, handles))
targets = make_all_targets(handles)
targets = evaluate_load_conditions(targets, open_kwargs)
Expand Down
20 changes: 20 additions & 0 deletions test/parallel/test_mpi.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import mpi4py
import numpy as np
import pytest
from mpi4py import MPI
from pytest_mpi.parallel_assert import parallel_assert

import opencosmo as oc
Expand All @@ -22,6 +23,11 @@ def particle_path(snapshot_path):
return snapshot_path / "haloparticles.hdf5"


@pytest.fixture
def profile_path(snapshot_path):
return snapshot_path / "sodproperties.hdf5"


@pytest.fixture
def malformed_header_path(input_path, tmp_path):
update = {"n_dm": "foo"}
Expand Down Expand Up @@ -72,6 +78,20 @@ def test_mpi(input_path):
parallel_assert(len(data) != 0)


@pytest.mark.parallel(nprocs=4)
def test_structure_collection_open(input_path, profile_path):
oc.open(input_path, profile_path)


@pytest.mark.parallel(nprocs=4)
def test_structure_collection_open_2(input_path, profile_path):
comm = MPI.COMM_WORLD
if comm.Get_rank() in [0, 2]:
oc.open(profile_path, input_path)
else:
oc.open(input_path, profile_path)


@pytest.mark.parallel(nprocs=4)
def test_partitioning_includes_all(input_path):
with oc.open(input_path) as f:
Expand Down
37 changes: 36 additions & 1 deletion test/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def multi_path(snapshot_path):

@pytest.fixture
def halo_paths(snapshot_path: Path):
files = ["haloparticles.hdf5", "haloproperties.hdf5", "sodproperties.hdf5"]
files = ["haloproperties.hdf5", "haloparticles.hdf5", "sodproperties.hdf5"]
hdf_files = [snapshot_path / file for file in files]
return list(hdf_files)

Expand Down Expand Up @@ -48,6 +48,27 @@ def conditional_path(multi_path, tmp_path):
return path


def test_open_structures(halo_paths, galaxy_paths):
c1 = oc.open(galaxy_paths)
assert isinstance(c1, oc.StructureCollection)
c2 = oc.open(halo_paths[0], galaxy_paths[1])
assert isinstance(c2, oc.StructureCollection)
c3 = oc.open(halo_paths[0], *galaxy_paths)
assert isinstance(c3, oc.StructureCollection)
c3 = oc.open(halo_paths[0], halo_paths[1])
assert isinstance(c3, oc.StructureCollection)
c3 = oc.open(halo_paths[0], halo_paths[2])
assert isinstance(c3, oc.StructureCollection)
c3 = oc.open(*halo_paths)
assert isinstance(c3, oc.StructureCollection)
c3 = oc.open(*halo_paths, galaxy_paths[1])
assert isinstance(c3, oc.StructureCollection)
c3 = oc.open(*halo_paths, *galaxy_paths)
assert isinstance(c3, oc.StructureCollection)
c3 = oc.open(halo_paths[0], halo_paths[1], *galaxy_paths)
assert isinstance(c3, oc.StructureCollection)


def test_multi_filter(multi_path):
collection = oc.open(multi_path)
collection = collection.filter(oc.col("sod_halo_mass") > 0)
Expand All @@ -56,6 +77,20 @@ def test_multi_filter(multi_path):
assert all(ds.data["sod_halo_mass"] > 0)


def test_link_particles_only(halo_paths):
collection = oc.open(halo_paths[0], halo_paths[1])
assert isinstance(collection, oc.StructureCollection)
for key in collection.keys():
assert "particles" in key or key == "halo_properties"


def test_link_profiles_only(halo_paths):
print(halo_paths)
collection = oc.open(halo_paths[0], halo_paths[2])
assert isinstance(collection, oc.StructureCollection)
assert set(collection.keys()) == {"halo_properties", "halo_profiles"}


def test_galaxy_alias_fails_for_halos(halo_paths):
ds = oc.open(halo_paths)
with pytest.raises(AttributeError):
Expand Down
Loading