Skip to content

Commit c5dc0f8

Browse files
authored
Merge branch 'main' into release
2 parents 337cf40 + 5b301a6 commit c5dc0f8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1787
-871
lines changed

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@
77
</h1><br>
88

99
[![CI](https://github.com/ArgonneCPAC/OpenCosmo/actions/workflows/merge.yaml/badge.svg)](https://github.com/ArgonneCPAC/OpenCosmo/actions/workflows/merge.yaml)
10-
![PyPI - Version](https://img.shields.io/pypi/v/opencosmo)
11-
![Conda Version](https://img.shields.io/conda/vn/conda-forge/opencosmo)
12-
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/opencosmo)
13-
![GitHub License](https://img.shields.io/github/license/ArgonneCPAC/opencosmo)
10+
[![PyPI - Version](https://img.shields.io/pypi/v/opencosmo)](https://pypi.org/project/opencosmo/)
11+
[![Conda Version](https://img.shields.io/conda/vn/conda-forge/opencosmo)](https://anaconda.org/conda-forge/opencosmo)
12+
[![GitHub License](https://img.shields.io/github/license/ArgonneCPAC/opencosmo)](https://github.com/ArgonneCPAC/OpenCosmo/blob/main/LICENSE.md)
1413

1514

1615
The OpenCosmo Python Toolkit provides utilities for reading, writing and manipulating data from cosmological simulations produced by the Cosmolgical Physics and Advanced Computing (CPAC) group at Argonne National Laboratory. It can be used to work with smaller quantities data retrieved with the CosmoExplorer, as well as the much larget datasets these queries draw from. The OpenCosmo toolkit integrates with standard tools such as AstroPy, and allows you to manipulate data in a fully-consistent cosmological context.

changes/+f722df63.misc.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Unit handling has been fully rewritten, making it much more flexible for backend work.

changes/+fa256c74.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:code:`with_new_columns` now accepts a :code:`descriptions` argument for providing column descriptions

changes/122.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Descriptions of columns can now be accessed with :py:class:`Dataset.descriptions <opencosmo.Dataset.descriptions>`

changes/43.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:code:`with_units` can now be used to provide unit conversions, in addition to changing conventions

docs/source/units.rst

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,132 @@ If you change unit conventions after performing a filter, the filter will still
5757
5858
should output a value of ~1.5 x 10^10 Msun, which is about 1e10/0.67.
5959

60+
Converting Columns to an Equivalent Unit
61+
----------------------------------------
62+
63+
You can convert all columns with a given unit into a different unit with the :code:`conversions` argument:
64+
65+
66+
.. code-block:: python
67+
68+
import opencosmo as oc
6069
70+
ds = oc.read("haloproperties.hdf5")
71+
conversions = {u.Mpc: u.lyr}
72+
ds = ds.with_units(conversions=conversions)
73+
74+
In the new dataset, all columns that originally had units of megaparsecs will be converted to lightyears. Composite units including megaparsec (e.g. km / s / Mpc, Mpc^2) will *not* be converted. All-column conversions are always peformed after a change of unit conventions. Changing units *after* doing a conversion always clears the conversions.
75+
76+
.. code-block:: python
77+
78+
import opencosmo as oc
79+
80+
ds = oc.read("haloproperties.hdf5")
81+
conversions = {u.Mpc: u.lyr}
82+
ds = ds.with_units(conversions=conversions)
83+
84+
85+
86+
Single-Column Conversions
87+
-------------------------
88+
89+
You can also use :code:`with_units` to convert the values in individual columns to their values in an equivalent unit:
90+
91+
.. code-block:: python
92+
93+
import astropy.units as u
94+
95+
dataset = oc.read("haloproperties.hdf5").with_units(
96+
fof_halo_center_x = u.lyr,
97+
fof_halo_center_y = u.lyr,
98+
fof_halo_center_z = u.lyr,
99+
)
100+
101+
Unit conversions like these are always performed *after* any change in unit convention, and changing unit conventions clears any existing unit conversions:
102+
103+
.. code-block:: python
104+
105+
# this works
106+
dataset = dataset.with_units(fof_halo_mass=u.kg)
107+
108+
# this clears the previous conversion,
109+
# the masses are now in Msun / h
110+
dataset = dataset.with_units("scalefree")
111+
112+
# This now fails, because the units of masses
113+
# are Msun / h, which cannot be converted to kg
114+
dataset = dataset.with_units(fof_halo_mass=u.kg)
115+
116+
# this will work, the units of halo mass in the "physical"
117+
# convention are Msun (no h), and the change of convention
118+
# happens before the conversions
119+
dataset = dataset.with_units("physical", fof_halo_mass=u.kg, fof_halo_center_x=u.lyr)
120+
121+
# reset all units
122+
dataset = dataset.with_units("physical")
123+
124+
125+
Unit conversions on :py:class:`Lightcones <opencosmo.Lightcone>` and :py:class:`SimulationCollections <opencosmo.SimulationCollection>` behave identically to single datasets. In :py:class:`StructureCollections <opencosmo.StructureCollections>`, unit conversions must be passed on a per-dataset basis:
126+
127+
.. code-block:: python
128+
129+
import astropy.units as u
130+
131+
structures = oc.open("haloproperties.hdf5", "haloparticles.hdf5")
132+
structures = structures.with_units(
133+
halo_properties={"fof_halo_mass": u.kg},
134+
dm_particles={"mass": u.kg}
135+
)
136+
137+
As with all-column conversions, composite units that include the target unit will not be converted. If you want to convert a composite unit, the conversion must be stated seperately.
138+
139+
Conversion Precedence
140+
---------------------
141+
142+
In cases where a blanket conversion is provided alongside a conversion for a specific column, the specific conversion always take precedence:
143+
144+
.. code-block:: python
145+
146+
import astropy.units as u
147+
148+
conversions = {u.Mpc: u.lyr}
149+
ds = ds.with_units(conversions=conversions, fof_halo_center_x=u.km)
150+
151+
All columns with units of megaparsecs will be converted to lightyears, except for the :code:`fof_halo_center_x` column which will be converted to kilometers.
152+
153+
Structure Collection Conversions
154+
--------------------------------
155+
156+
When working with a structure collection, you can provide conversions that apply to the entire collection, as single dataset inside the collection, or individual columns within a given dataset. As you might expect, conversions on an individual dataset takes precedence over those that apply to all datasets.
157+
158+
.. code-block:: python
159+
160+
import astropy.units as u
161+
162+
conversions = {u.Mpc: u.lyr}
163+
structures = structures.with_units(
164+
conversions=conversions
165+
halo_properties = {
166+
"conversions": {u.Mpc: u.km},
167+
"fof_halo_center_x": u.m
168+
}
169+
)
170+
171+
In this example, all values in Mpc will be converted to lightyears, except in the "halo_properties" dataset, where they will be converted to kilometers. The column "fof_halo_center_x" in "halo_properties" will be converted to meters instead.
172+
173+
Clearing Conversions
174+
--------------------
175+
176+
Conversions are always cleared when changing unit conventions, or you can also clear them by calling :code:`with_units` with no arguments.
177+
178+
.. code-block:: python
179+
180+
dataset = oc.read("haloproperties.hdf5").with_units(
181+
conversions={u.Mpc: u.lyr},
182+
fof_halo_center_x = u.lyr,
183+
)
184+
185+
dataset = dataset.with_units()
186+
# all unit conversion reset
187+
188+

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "opencosmo"
3-
version = "0.9.4"
3+
version = "0.10.0a1"
44
description = "OpenCosmo Python Toolkit"
55
authors = [
66
{ name = "Patrick Wells", email = "pwells@anl.gov" },

src/opencosmo/collection/lightcone/lightcone.py

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from functools import reduce
1+
from functools import cached_property, reduce
22
from itertools import chain
33
from typing import Any, Callable, Generator, Iterable, Optional, Self
44

@@ -203,6 +203,25 @@ def columns(self) -> list[str]:
203203
cols = list(filter(lambda col: col not in self.__hidden, cols))
204204
return cols
205205

206+
@cached_property
207+
def descriptions(self) -> dict[str, Optional[str]]:
208+
"""
209+
Return the descriptions (if any) of the columns in this lightcone as a dictonary.
210+
Columns without a description will be included in the dictionary with a value
211+
of None
212+
213+
Returns
214+
-------
215+
216+
descriptions : dict[str, str | None]
217+
The column descriptions
218+
"""
219+
descriptions = next(iter(self.values())).descriptions
220+
descriptions = dict(
221+
filter(lambda kv: kv[0] not in self.__hidden, descriptions.items())
222+
)
223+
return descriptions
224+
206225
@property
207226
def cosmology(self) -> Cosmology:
208227
"""
@@ -747,7 +766,11 @@ def take(self, n: int, at: str = "random") -> "Lightcone":
747766
output = {k: v for k, v in reversed(output.items())}
748767
return Lightcone(output, self.z_range, self.__hidden, self.__ordered_by)
749768

750-
def with_new_columns(self, **columns: DerivedColumn | np.ndarray | u.Quantity):
769+
def with_new_columns(
770+
self,
771+
descriptions: str | dict[str, str] = {},
772+
**columns: DerivedColumn | np.ndarray | u.Quantity,
773+
):
751774
"""
752775
Create a new dataset with additional columns. These new columns can be derived
753776
from columns already in the dataset, a numpy array, or an Astropy quantity
@@ -758,7 +781,13 @@ def with_new_columns(self, **columns: DerivedColumn | np.ndarray | u.Quantity):
758781
759782
Parameters
760783
----------
761-
** columns : opencosmo.DerivedColumn
784+
descriptions : str | dict[str, str], optional
785+
A description for the new columns. These descriptions will be accessible through
786+
:py:attr:`Lightcone.descriptions <opencosmo.Lighcone.descriptions>`. If a dictionary,
787+
should have keys matching the column names.
788+
789+
** columns : opencosmo.DerivedColumn | np.ndarray | u.quantity
790+
The new columns
762791
763792
Returns
764793
-------
@@ -786,7 +815,7 @@ def with_new_columns(self, **columns: DerivedColumn | np.ndarray | u.Quantity):
786815
for i, (ds_name, ds) in enumerate(self.items()):
787816
raw_columns = {name: arrs[i] for name, arrs in raw_split.items()}
788817
columns_input = raw_columns | derived
789-
new_dataset = ds.with_new_columns(**columns_input)
818+
new_dataset = ds.with_new_columns(descriptions, **columns_input)
790819
new_datasets[ds_name] = new_dataset
791820
return Lightcone(new_datasets, self.z_range, self.__hidden, self.__ordered_by)
792821

@@ -828,23 +857,69 @@ def sort_by(self, column: str, invert: bool = False):
828857
raise ValueError(f"Column {column} does not exist in this dataset!")
829858
return Lightcone(dict(self), self.z_range, self.__hidden, (column, invert))
830859

831-
def with_units(self, convention: str) -> Self:
832-
"""
833-
Create a new dataset from this one with a different unit convention.
860+
def with_units(
861+
self,
862+
convention: Optional[str] = None,
863+
conversions: dict[u.Unit, u.Unit] = {},
864+
**columns: u.Unit,
865+
) -> Self:
866+
r"""
867+
Create a new lightcone from this one with a different unit convention or
868+
with certain columns converted to a different compatible unit.
869+
870+
Unit conversions are always performed after a change of convention, and
871+
changing conventions clears any existing unit conversions.
872+
873+
For more, see :doc:`units`.
874+
875+
.. code-block:: python
876+
877+
import astropy.units as u
878+
879+
# this works
880+
lc = lc.with_units(fof_halo_mass=u.kg)
881+
882+
# this clears the previous conversion
883+
lc = lc.with_units("scalefree")
884+
885+
# This now fails, because the units of masses
886+
# are Msun / h, which cannot be converted to kg
887+
lc = lc.with_units(fof_halo_mass=u.kg)
888+
889+
# this will now work, wince the units of halo mass in the "physical"
890+
# convention are Msun (no h).
891+
lc = lc.with_units("physical", fof_halo_mass=u.kg, fof_halo_center_x=u.lyr)
892+
893+
# Suppose you want your distances in lightyears, but the x coordinate of your
894+
# halo center in kilometers, for some reason ¯\_(ツ)_/¯
895+
blanket_conversions = {u.Mpc: u.lyr}
896+
lc = lc.with_units(conversions = blanket_conversions, fof_halo_center_x = u.km)
834897
835898
Parameters
836899
----------
837-
convention : str
900+
convention : str, optional
838901
The unit convention to use. One of "physical", "comoving",
839902
"scalefree", or "unitless".
840903
904+
conversions: dict[astropy.units.Unit, astropy.units.Unit]
905+
Conversions that apply to all columns in the lightcone with the
906+
unit given by the key.
907+
908+
**column_conversions: astropy.units.Unit
909+
Custom unit conversions for specific columns
910+
in this dataset.
911+
841912
Returns
842913
-------
843-
dataset : Dataset
844-
The new dataset with the requested unit convention.
845-
846-
"""
847-
return self.__map("with_units", convention)
914+
lightcone : Lightcone
915+
The new lightcone with the requested unit convention and/or conversions.
916+
"""
917+
return self.__map(
918+
"with_units",
919+
convention=convention,
920+
conversions=conversions,
921+
**columns,
922+
)
848923

849924
def collect(self) -> "Lightcone":
850925
"""

src/opencosmo/collection/simulation/simulation.py

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Callable, Iterable, Mapping, Optional, Self
22

3+
import astropy.units as u
34
import h5py
45
from astropy.cosmology import Cosmology # type: ignore
56

@@ -276,7 +277,11 @@ def take(self, n: int, at: str = "random") -> Self:
276277
return self.__map("take", n, at)
277278

278279
def with_new_columns(
279-
self, *args, datasets: Optional[str | Iterable[str]] = None, **kwargs
280+
self,
281+
*args,
282+
datasets: Optional[str | Iterable[str]] = None,
283+
descriptions: str | dict[str, str] = {},
284+
**kwargs,
280285
):
281286
"""
282287
Update the datasets within this collection with a set of new columns.
@@ -294,6 +299,11 @@ def with_new_columns(
294299
datasets: str | list[str], optional
295300
The datasets to add the columns to.
296301
302+
descriptions : str | dict[str, str], optional
303+
A description for the new columns. These descriptions will be accessible through
304+
:py:attr:`SimulationCollection(datasets).descriptions <opencosmo.SimulationCollection.descriptions>`.
305+
If a dictionary, should have keys matching the column names.
306+
297307
** columns : opencosmo.DerivedColumn | np.ndarray | units.Quantity
298308
The new columns
299309
"""
@@ -305,10 +315,14 @@ def with_new_columns(
305315

306316
output = {name: ds for name, ds in self.items()}
307317
for ds_name in datasets:
308-
output[ds_name] = output[ds_name].with_new_columns(*args, **kwargs)
318+
output[ds_name] = output[ds_name].with_new_columns(
319+
*args, descriptions=descriptions, **kwargs
320+
)
309321
return SimulationCollection(output)
310322

311-
return self.__map("with_new_columns", *args, **kwargs)
323+
return self.__map(
324+
"with_new_columns", *args, descriptions=descriptions, **kwargs
325+
)
312326

313327
def evaluate(
314328
self,
@@ -389,16 +403,31 @@ def sort_by(self, column: str, invert: bool = False):
389403
"""
390404
return self.__map("sort_by", column=column, invert=invert)
391405

392-
def with_units(self, convention: str) -> Self:
406+
def with_units(
407+
self,
408+
convention: Optional[str] = None,
409+
conversions: dict[u.Unit, u.Unit] = {},
410+
**columns: u.Unit,
411+
) -> Self:
393412
"""
394-
Transform all datasets or collections to use the given unit convention. This
395-
method behaves exactly like :meth:`opencosmo.Dataset.with_units`.
413+
Transform all datasets or collections to use the given unit convention, convert
414+
all columns with a given unit into a different unit, and/or convert specific column(s)
415+
to a compatible unit. This method behaves exactly like :meth:`opencosmo.Dataset.with_units`.
396416
397417
Parameters
398418
----------
399419
convention: str
400420
The unit convention to use. One of "unitless",
401421
"scalefree", "comoving", or "physical".
402422
423+
conversions: dict[astropy.units.Unit, astropy.units.Unit]
424+
Conversions that apply to all columns in the collection with the
425+
unit given by the key.
426+
427+
**column_conversions: astropy.units.Unit
428+
Custom unit conversions for any column with a specific
429+
name in the datasets in this collection.
430+
431+
403432
"""
404-
return self.__map("with_units", convention)
433+
return self.__map("with_units", convention, conversions=conversions, **columns)

0 commit comments

Comments
 (0)