# Licensed under a 3-clause BSD style license - see LICENSE.rst """|Cosmology| <-> ECSV I/O, using |Cosmology.read| and |Cosmology.write|. This module provides functions to write/read a |Cosmology| object to/from an ECSV file. The functions are registered with ``readwrite_registry`` under the format name "ascii.ecsv". We assume the following setup: >>> from pathlib import Path >>> from tempfile import TemporaryDirectory >>> temp_dir = TemporaryDirectory() To see reading a Cosmology from an ECSV file, we first write a Cosmology to an ECSV file: >>> from astropy.cosmology import Cosmology, Planck18 >>> file = Path(temp_dir.name) / "file.ecsv" >>> Planck18.write(file) >>> with open(file) as f: print(f.read()) # %ECSV 1.0 # --- # datatype: # - {name: name, datatype: string} # - {name: H0, unit: km / (Mpc s), datatype: float64, description: Hubble ...} ... # meta: !!omap # - {Oc0: 0.2607} ... # schema: astropy-2.0 name H0 Om0 Tcmb0 Neff m_nu Ob0 Planck18 67.66 0.30966 2.7255 3.046 [0.0,0.0,0.06] 0.04897 Now we can read the Cosmology from the ECSV file, constructing a new cosmological instance identical to the ``Planck18`` cosmology from which it was generated. >>> cosmo = Cosmology.read(file) >>> print(cosmo) FlatLambdaCDM(name="Planck18", H0=67.66 km / (Mpc s), Om0=0.30966, Tcmb0=2.7255 K, Neff=3.046, m_nu=[0. 0. 0.06] eV, Ob0=0.04897) >>> cosmo == Planck18 True If a file already exists, attempting to write will raise an error unless ``overwrite=True``. >>> Planck18.write(file, overwrite=True) By default the cosmology class is written to the Table metadata. This can be changed to a column of the table using the ``cosmology_in_meta`` keyword argument. >>> file = Path(temp_dir.name) / "file2.ecsv" >>> Planck18.write(file, cosmology_in_meta=False) >>> with open(file) as f: print(f.read()) # %ECSV 1.0 # --- # datatype: # - {name: cosmology, datatype: string} # - {name: name, datatype: string} ... # meta: !!omap # - {Oc0: 0.2607} ... # schema: astropy-2.0 cosmology name H0 Om0 Tcmb0 Neff m_nu Ob0 FlatLambdaCDM Planck18 67.66 0.30966 2.7255 3.046 [0.0,0.0,0.06] 0.04897 The ``cosmology`` information (column or metadata) may be omitted if the cosmology class (or its string name) is passed as the ``cosmology`` keyword argument to |Cosmology.read|. Alternatively, specific cosmology classes can be used to parse the data. >>> from astropy.cosmology import FlatLambdaCDM >>> print(FlatLambdaCDM.read(file)) FlatLambdaCDM(name="Planck18", H0=67.66 km / (Mpc s), Om0=0.30966, Tcmb0=2.7255 K, Neff=3.046, m_nu=[0. 0. 0.06] eV, Ob0=0.04897) When using a specific cosmology class, the class' default parameter values are used to fill in any missing information. For files with multiple rows of cosmological parameters, the ``index`` argument is needed to select the correct row. The index can be an integer for the row number or, if the table is indexed by a column, the value of that column. If the table is not indexed and ``index`` is a string, the "name" column is used as the indexing column. Here is an example where ``index`` is needed and can be either an integer (for the row number) or the name of one of the cosmologies, e.g. 'Planck15'. >>> from astropy.cosmology import Planck13, Planck15, Planck18 >>> from astropy.table import vstack >>> cts = vstack([c.to_format("astropy.table") ... for c in (Planck13, Planck15, Planck18)], ... metadata_conflicts='silent') >>> file = Path(temp_dir.name) / "file3.ecsv" >>> cts.write(file) >>> with open(file) as f: print(f.read()) # %ECSV 1.0 # --- # datatype: # - {name: name, datatype: string} ... # meta: !!omap # - {Oc0: 0.2607} ... # schema: astropy-2.0 name H0 Om0 Tcmb0 Neff m_nu Ob0 Planck13 67.77 0.30712 2.7255 3.046 [0.0,0.0,0.06] 0.048252 Planck15 67.74 0.3075 2.7255 3.046 [0.0,0.0,0.06] 0.0486 Planck18 67.66 0.30966 2.7255 3.046 [0.0,0.0,0.06] 0.04897 >>> cosmo = Cosmology.read(file, index="Planck15", format="ascii.ecsv") >>> cosmo == Planck15 True Fields of the table in the file can be renamed to match the `~astropy.cosmology.Cosmology` class' signature using the ``rename`` argument. This is useful when the files's column names do not match the class' parameter names. >>> file = Path(temp_dir.name) / "file4.ecsv" >>> Planck18.write(file, rename={"H0": "Hubble"}) >>> with open(file) as f: print(f.read()) # %ECSV 1.0 # --- # datatype: # - {name: name, datatype: string} ... # meta: !!omap # - {Oc0: 0.2607} ... # schema: astropy-2.0 name Hubble Om0 Tcmb0 Neff m_nu Ob0 Planck18 67.66 0.30966 2.7255 3.046 [0.0,0.0,0.06] 0.04897 >>> cosmo = Cosmology.read(file, rename={"Hubble": "H0"}) >>> cosmo == Planck18 True By default :class:`~astropy.cosmology.Cosmology` instances are written using `~astropy.table.QTable` as an intermediate representation (for details see |Cosmology.to_format|, with ``format="astropy.table"``). The `~astropy.table.Table` type can be changed using the ``cls`` keyword argument. >>> from astropy.table import Table >>> file = Path(temp_dir.name) / "file5.ecsv" >>> Planck18.write(file, cls=Table) For most use cases, the default ``cls`` of :class:`~astropy.table.QTable` is recommended and will be largely indistinguishable from other table types, as the ECSV format is agnostic to the table type. An example of a difference that might necessitate using a different table type is if a different ECSV schema is desired. Additional keyword arguments are passed to ``QTable.read`` and ``QTable.write``. .. testcleanup:: >>> temp_dir.cleanup() """ from __future__ import annotations from typing import TYPE_CHECKING, Any, TypeVar import astropy.cosmology.units as cu import astropy.units as u from astropy.cosmology.connect import readwrite_registry from astropy.cosmology.core import Cosmology from astropy.table import QTable from .table import from_table, to_table if TYPE_CHECKING: from collections.abc import Mapping from astropy.cosmology._typing import _CosmoT from astropy.io.typing import PathLike, ReadableFileLike, WriteableFileLike from astropy.table import Table _TableT = TypeVar("_TableT", "Table") def read_ecsv( filename: PathLike | ReadableFileLike[Table], index: int | str | None = None, *, move_to_meta: bool = False, cosmology: str | type[_CosmoT] | None = None, rename: Mapping[str, str] | None = None, **kwargs: Any, ) -> _CosmoT: r"""Read a `~astropy.cosmology.Cosmology` from an ECSV file. Parameters ---------- filename : path-like or file-like From where to read the Cosmology. index : int, str, or None, optional Needed to select the row in tables with multiple rows. ``index`` can be an integer for the row number or, if the table is indexed by a column, the value of that column. If the table is not indexed and ``index`` is a string, the "name" column is used as the indexing column. move_to_meta : bool (optional, keyword-only) Whether to move keyword arguments that are not in the Cosmology class' signature to the Cosmology's metadata. This will only be applied if the Cosmology does NOT have a keyword-only argument (e.g. ``**kwargs``). Arguments moved to the metadata will be merged with existing metadata, preferring specified metadata in the case of a merge conflict (e.g. for ``Cosmology(meta={'key':10}, key=42)``, the ``Cosmology.meta`` will be ``{'key': 10}``). cosmology : str or type or None (optional, keyword-only) The cosmology class (or string name thereof) to use when constructing the cosmology instance. The class also provides default parameter values, filling in any non-mandatory arguments missing in 'table'. rename : dict or None (optional, keyword-only) A dictionary mapping column names in 'table' to fields of the `~astropy.cosmology.Cosmology` class. **kwargs Passed to ``QTable.read`` Returns ------- `~astropy.cosmology.Cosmology` subclass instance Examples -------- We assume the following setup: >>> from pathlib import Path >>> from tempfile import TemporaryDirectory >>> temp_dir = TemporaryDirectory() To see reading a Cosmology from an ECSV file, we first write a Cosmology to an ECSV file: >>> from astropy.cosmology import Cosmology, Planck18 >>> file = Path(temp_dir.name) / "file.ecsv" >>> Planck18.write(file) >>> with open(file) as f: print(f.read()) # %ECSV 1.0 # --- # datatype: # - {name: name, datatype: string} ... # meta: !!omap # - {Oc0: 0.2607} ... # schema: astropy-2.0 name H0 Om0 Tcmb0 Neff m_nu Ob0 Planck18 67.66 0.30966 2.7255 3.046 [0.0,0.0,0.06] 0.04897 Now we can read the Cosmology from the ECSV file, constructing a new cosmological instance identical to the ``Planck18`` cosmology from which it was generated. >>> cosmo = Cosmology.read(file) >>> print(cosmo) FlatLambdaCDM(name="Planck18", H0=67.66 km / (Mpc s), Om0=0.30966, Tcmb0=2.7255 K, Neff=3.046, m_nu=[0. 0. 0.06] eV, Ob0=0.04897) >>> cosmo == Planck18 True The ``cosmology`` information (column or metadata) may be omitted if the cosmology class (or its string name) is passed as the ``cosmology`` keyword argument to |Cosmology.read|. Alternatively, specific cosmology classes can be used to parse the data. >>> from astropy.cosmology import FlatLambdaCDM >>> print(FlatLambdaCDM.read(file)) FlatLambdaCDM(name="Planck18", H0=67.66 km / (Mpc s), Om0=0.30966, Tcmb0=2.7255 K, Neff=3.046, m_nu=[0. 0. 0.06] eV, Ob0=0.04897) When using a specific cosmology class, the class' default parameter values are used to fill in any missing information. For files with multiple rows of cosmological parameters, the ``index`` argument is needed to select the correct row. The index can be an integer for the row number or, if the table is indexed by a column, the value of that column. If the table is not indexed and ``index`` is a string, the "name" column is used as the indexing column. Here is an example where ``index`` is needed and can be either an integer (for the row number) or the name of one of the cosmologies, e.g. 'Planck15'. >>> from astropy.cosmology import Planck13, Planck15, Planck18 >>> from astropy.table import vstack >>> cts = vstack([c.to_format("astropy.table") ... for c in (Planck13, Planck15, Planck18)], ... metadata_conflicts='silent') >>> file = Path(temp_dir.name) / "file2.ecsv" >>> cts.write(file) >>> with open(file) as f: print(f.read()) # %ECSV 1.0 # --- # datatype: # - {name: name, datatype: string} ... # meta: !!omap # - {Oc0: 0.2607} ... # schema: astropy-2.0 name H0 Om0 Tcmb0 Neff m_nu Ob0 Planck13 67.77 0.30712 2.7255 3.046 [0.0,0.0,0.06] 0.048252 Planck15 67.74 0.3075 2.7255 3.046 [0.0,0.0,0.06] 0.0486 Planck18 67.66 0.30966 2.7255 3.046 [0.0,0.0,0.06] 0.04897 >>> cosmo = Cosmology.read(file, index="Planck15", format="ascii.ecsv") >>> cosmo == Planck15 True Fields of the table in the file can be renamed to match the `~astropy.cosmology.Cosmology` class' signature using the ``rename`` argument. This is useful when the files's column names do not match the class' parameter names. For this example we need to make a new file with renamed columns: >>> file = Path(temp_dir.name) / "file3.ecsv" >>> renamed_table = Planck18.to_format("astropy.table", rename={"H0": "Hubble"}) >>> renamed_table.write(file) >>> with open(file) as f: print(f.read()) # %ECSV 1.0 # --- # datatype: # - {name: name, datatype: string} ... # meta: !!omap # - {Oc0: 0.2607} ... # schema: astropy-2.0 name Hubble Om0 Tcmb0 Neff m_nu Ob0 Planck18 67.66 0.30966 2.7255 3.046 [0.0,0.0,0.06] 0.04897 Now we can read the Cosmology from the ECSV file, with the required renaming: >>> cosmo = Cosmology.read(file, rename={"Hubble": "H0"}) >>> cosmo == Planck18 True Additional keyword arguments are passed to ``QTable.read``. .. testcleanup:: >>> temp_dir.cleanup() """ kwargs["format"] = "ascii.ecsv" with u.add_enabled_units(cu): table = QTable.read(filename, **kwargs) # build cosmology from table return from_table( table, index=index, move_to_meta=move_to_meta, cosmology=cosmology, rename=rename, ) def write_ecsv( cosmology: Cosmology, file: PathLike | WriteableFileLike[_TableT], *, overwrite: bool = False, cls: type[_TableT] = QTable, cosmology_in_meta: bool = True, rename: Mapping[str, str] | None = None, **kwargs: Any, ) -> None: """Serialize the cosmology into a ECSV. Parameters ---------- cosmology : `~astropy.cosmology.Cosmology` The cosmology instance to convert to a mapping. file : path-like or file-like Location to save the serialized cosmology. overwrite : bool (optional, keyword-only) Whether to overwrite the file, if it exists. cls : type (optional, keyword-only) Astropy :class:`~astropy.table.Table` (sub)class to use when writing. Default is :class:`~astropy.table.QTable`. cosmology_in_meta : bool (optional, keyword-only) Whether to put the cosmology class in the Table metadata (if `True`, default) or as the first column (if `False`). rename : Mapping[str, str] or None (optional keyword-only) A mapping of field names on the `~astropy.cosmology.Cosmology` to column names of the table. **kwargs Passed to ``cls.write`` Raises ------ TypeError If kwarg (optional) 'cls' is not a subclass of `astropy.table.Table` Examples -------- We assume the following setup: >>> from pathlib import Path >>> from tempfile import TemporaryDirectory >>> temp_dir = TemporaryDirectory() A Cosmology can be written to an ECSV file: >>> from astropy.cosmology import Cosmology, Planck18 >>> file = Path(temp_dir.name) / "file.ecsv" >>> Planck18.write(file) >>> with open(file) as f: print(f.read()) # %ECSV 1.0 # --- # datatype: # - {name: name, datatype: string} ... # meta: !!omap # - {Oc0: 0.2607} ... # schema: astropy-2.0 name H0 Om0 Tcmb0 Neff m_nu Ob0 Planck18 67.66 0.30966 2.7255 3.046 [0.0,0.0,0.06] 0.04897 If a file already exists, attempting to write will raise an error unless ``overwrite=True``. >>> Planck18.write(file, overwrite=True) By default :class:`~astropy.cosmology.Cosmology` instances are written using `~astropy.table.QTable` as an intermediate representation (for details see |Cosmology.to_format|, with ``format="astropy.table"``). The `~astropy.table.Table` type can be changed using the ``cls`` keyword argument. >>> from astropy.table import Table >>> file = Path(temp_dir.name) / "file2.ecsv" >>> Planck18.write(file, cls=Table) For most use cases, the default ``cls`` of :class:`~astropy.table.QTable` is recommended and will be largely indistinguishable from other table types, as the ECSV format is agnostic to the table type. An example of a difference that might necessitate using a different table type is if a different ECSV schema is desired. By default the cosmology class is written to the Table metadata. This can be changed to a column of the table using the ``cosmology_in_meta`` keyword argument. >>> file = Path(temp_dir.name) / "file3.ecsv" >>> Planck18.write(file, cosmology_in_meta=False) >>> with open(file) as f: print(f.read()) # %ECSV 1.0 # --- # datatype: # - {name: cosmology, datatype: string} # - {name: name, datatype: string} ... # meta: !!omap # - {Oc0: 0.2607} ... # schema: astropy-2.0 cosmology name H0 Om0 Tcmb0 Neff m_nu Ob0 FlatLambdaCDM Planck18 67.66 0.30966 2.7255 3.046 [0.0,0.0,0.06] 0.04897 Fields of the Cosmology can be renamed to when writing to an ECSV file using the ``rename`` argument. >>> file = Path(temp_dir.name) / "file4.ecsv" >>> Planck18.write(file, rename={"H0": "Hubble"}) >>> with open(file) as f: print(f.read()) # %ECSV 1.0 # --- # datatype: # - {name: name, datatype: string} ... # meta: ... # schema: astropy-2.0 name Hubble Om0 Tcmb0 Neff m_nu Ob0 Planck18 67.66 0.30966 2.7255 3.046 [0.0,0.0,0.06] 0.04897 Additional keyword arguments are passed to :attr:`astropy.table.QTable.write`. .. testcleanup:: >>> temp_dir.cleanup() """ table = to_table( cosmology, cls=cls, cosmology_in_meta=cosmology_in_meta, rename=rename ) kwargs["format"] = "ascii.ecsv" table.write(file, overwrite=overwrite, **kwargs) def ecsv_identify( origin: object, filepath: str | None, *args: object, **kwargs: object ) -> bool: """Identify if object uses the Table format. Returns ------- bool """ return filepath is not None and filepath.endswith(".ecsv") # =================================================================== # Register readwrite_registry.register_reader("ascii.ecsv", Cosmology, read_ecsv) readwrite_registry.register_writer("ascii.ecsv", Cosmology, write_ecsv) readwrite_registry.register_identifier("ascii.ecsv", Cosmology, ecsv_identify)