Source code for joseki.core

"""Core module."""

from __future__ import annotations

import datetime
import logging
import os
import typing as t

import numpy as np
import pint
import xarray as xr

from ._version import __version__
from .profiles.core import (
    DEFAULT_METHOD,
    select_molecules,
)
from .profiles.core import regularize as _regularize
from .profiles.factory import factory
from .units import to_quantity

logger = logging.getLogger(__name__)


[docs] def make( identifier: str, z: pint.Quantity | dict | xr.DataArray | None = None, interp_method: t.Mapping[str, str] | None = DEFAULT_METHOD, conserve_column: bool = False, molecules: t.List[str] | None = None, regularize: bool | dict | None = None, rescale_to: dict | None = None, check_x_sum: bool = False, **kwargs: t.Any, ) -> xr.Dataset: """ Create a profile with the specified identifier. Args: identifier: Profile identifier. z: Altitude values. interp_method: Mapping of variable and interpolation method. conserve_column: If ``True``, ensure that column densities are conserved during interpolation. molecules: List of molecules to include in the profile. regularize: Regularize the altitude grid with specified options which are passed to [regularize](reference.md#src.joseki.profiles.core.regularize). rescale_to: Rescale molecular concentrations to the specified target values which are passed to [rescale_to](reference.md#src.joseki.accessor.JosekiAccessor.rescale_to). check_x_sum: If ``True``, check that the mole fraction sums are less or equal to 1. kwargs: Additional keyword arguments passed to the profile constructor. Returns: Profile as xarray.Dataset. See Also: * :func:`joseki.profiles.core.regularize` * :meth:`joseki.accessor.JosekiAccessor.rescale_to` """ logger.info("Creating profile %s", identifier) logger.debug("z: %s", z) logger.debug("interp_method: %s", interp_method) logger.debug("conserve_column: %s", conserve_column) logger.debug("molecules: %s", molecules) logger.debug("regularize: %s", regularize) logger.debug("rescale_to: %s", rescale_to) logger.debug("kwargs: %s", kwargs) # Convert z to pint.Quantity z = to_quantity(z) if z is not None else None profile = factory.create(identifier) logger.debug("exporting profile to xarray.Dataset") ds = profile.to_dataset( z=z, interp_method=interp_method, conserve_column=conserve_column, **kwargs, ) # Molecules selection if molecules is not None: ds = select_molecules(ds, molecules) # Altitude grid regularization if regularize: z = to_quantity(ds.z) default_num = int((z.max() - z.min()) // np.diff(z).min()) + 1 if isinstance(regularize, bool): regularize = {} ds = _regularize( ds=ds, method=regularize.get("method", DEFAULT_METHOD), conserve_column=regularize.get("conserve_column", False), options=regularize.get("options", {"num": default_num}), ) # Molecular concentration rescaling if rescale_to: ds = ds.joseki.rescale_to( target=rescale_to, check_x_sum=check_x_sum, ) return ds
[docs] def open_dataset(path: os.PathLike, *args, **kwargs) -> xr.Dataset: """ Thin wrapper around `xarray.open_dataset`. Args: path: Path to the dataset. Returns: Profile. """ return xr.open_dataset(path, *args, **kwargs)
[docs] def load_dataset(path: os.PathLike, *args, **kwargs) -> xr.Dataset: """ Thin wrapper around `xarray.load_dataset`. Args: path: Path to the dataset. Returns: Profile. """ return xr.load_dataset(path, *args, **kwargs)
[docs] def merge( datasets: t.Iterable[xr.Dataset], new_title: str | None = None, ) -> xr.Dataset: """ Merge multiple profiles into a single profile. Args: datasets: Iterable of profiles. new_title: New title for the merged profile. If `None`, the title of the first profile is used. Returns: Merged profile. Notes: The first profile in the iterable is used as the base profile; when variables with the same name are encountered in subsequent profiles, the variable from the first profile is used. """ merged = xr.merge( datasets, compat="override", # pick variable from first dataset combine_attrs="override", # copy attrs from the first dataset to the result ) # update attributes now = datetime.datetime.utcnow().replace(microsecond=0).isoformat() institutions = set([ds.attrs["institution"] for ds in datasets]) sources = set([ds.attrs["source"] for ds in datasets]) references = set([ds.attrs["references"] for ds in datasets]) urls = set([ds.attrs["url"] for ds in datasets]) urldates = set([ds.attrs["urldate"] for ds in datasets]) conventions = set([ds.attrs["Conventions"] for ds in datasets]) with_profiles = "with profile " if len(datasets) == 2 else "with profiles " with_profiles += ", ".join([f"'{ds.title}'" for ds in datasets[1:]]) merged.attrs["history"] += ( f"\n{now} - merged profile '{datasets[0].title}' {with_profiles} " f"joseki, version {__version__}" ) merged.attrs["institution"] = "\n".join(institutions) merged.attrs["source"] = "\n".join(sources) merged.attrs["references"] = "\n".join(references) merged.attrs["url"] = "\n".join(urls) merged.attrs["urldate"] = "\n".join(urldates) merged.attrs["Conventions"] = "\n".join(conventions) if new_title is not None: # pragma: no cover merged.attrs["title"] = new_title else: # pragma: no cover merged.attrs["title"] = datasets[0].title return merged
[docs] def identifiers() -> t.List[str]: """ List all registered profile identifiers. Returns: List of all registered profile identifiers. """ return factory.registered_identifiers