Source code for grid_process.units_conv
# Copyright (c) 2026 MeteoSwiss, contributors listed in AUTHORS
#
# Distributed under the terms of the BSD 3-Clause License.
#
# SPDX-License-Identifier: BSD-3-Clause
"""
Module to control, parse and convert units
"""
import logging
import re
from functools import partial
from collections.abc import Sequence, Hashable
import numpy as np
import xarray as xr
from pint import UnitRegistry
from grid_process.helpers import make_accessor
logger = logging.getLogger(__name__)
[docs]
class UnitsError(Exception):
"""
Class to raise units errors
"""
# Register xarray accessor so module functions can also be called as methods
# on xr.DataArray objects
[docs]
@xr.register_dataarray_accessor("units_conv")
class DaUnits:
"""
Xarray accessors class for units utility functions on DataArray
"""
def __init__(self, da: xr.DataArray):
self._data = da
[docs]
@xr.register_dataset_accessor("units_conv")
class DsUnits:
"""
Xarray accessors class for units utility functions on Dataset
"""
def __init__(self, ds: xr.Dataset):
self._data = ds
dataarray_accessor = partial(make_accessor, accessor_class=DaUnits)
dataset_accessor = partial(make_accessor, accessor_class=DsUnits)
ureg: UnitRegistry = UnitRegistry(
autoconvert_offset_to_baseunit=True,
preprocessors=[
partial(
re.sub,
r"(?<=[A-Za-z])(?![A-Za-z])(?<![0-9\-][eE])(?<![0-9\-])(?=[0-9\-])",
"**",
),
partial(re.sub, r"\b[h]\b", "hour"), # avoid parsing of Plank constant for h
],
)
[docs]
@dataarray_accessor
def da_to(da: xr.DataArray, dst_units: str) -> xr.DataArray:
"""
Convert units in a xarray.DataArray.
Parameters
----------
da : xarray.DataArray
with attribute "units"
dst_units : str
destination units
Returns
-------
da_out : xarray.DataArray
contains the data array with converted units
Raises
------
pint.errors.DimensionalityError
if the conversion is not possible
"""
da = da.copy()
src_units_parsed = ureg.parse_units(da.units)
dst_units_parsed = ureg.parse_units(dst_units)
if src_units_parsed == dst_units_parsed:
pass
else:
da_quantity = da.values * src_units_parsed
da.values = da_quantity.to(dst_units_parsed).magnitude
da.attrs["units"] = dst_units
return da
[docs]
@dataset_accessor
def ds_to(ds: xr.Dataset, dst_units: str | dict[Hashable, str]) -> xr.Dataset:
"""
Convert units in a xarray.Dataset.
Parameters
----------
ds : xarray.Dataset
with variable attributes "units"
dst_units : str or dict
destination units for each data variable to be converted.
Variable names not specified in ``dst_units`` are kept in the same units.
If passed as a single string, all variables in the dataset are converted to the
same units
Returns
-------
ds_out : xarray.Dataset
contains the dataset with converted units
Raises
------
pint.errors.DimensionalityError
if the conversion is not possible
"""
if isinstance(dst_units, str):
dst_units = {var: dst_units for var in ds}
ds_out = ds.copy()
for var, dst_unit in dst_units.items():
ds_out[var] = da_to(ds[var], dst_unit)
return ds_out
[docs]
def consistency_check(datasets: Sequence[xr.Dataset], check_vars: bool = True) -> dict[str, dict[str, str]]:
"""
Checks that the input datasets have same data variables and units. If it is
the case, returns the 'units' attributes of each data variable otherwise
raises an error.
Parameters
----------
datasets : sequence of xarray.Dataset
each data variable in the datasets must have a 'units' attributes
check_vars : bool, optional
if False, allows datasets to have different variables, but common ones
must have same units. Default is True
Returns
-------
out : dict
contains the 'units' attribute of the data variables in the input
datasets. In case same units are specified in various format, the
dominant format is kept if there is one otherwise the format from the
first dataset in the input sequence
Raises
------
UnitsError
if same variables across the input datasets have different units
ValueError
when ``check_vars=True`` and the input datasets do not have exact same
set of variables
"""
vars_attrs = np.array([{varname: da.units for varname, da in ds.data_vars.items()} for ds in datasets])
if check_vars:
if not np.all([dic.keys() == vars_attrs[0].keys() for dic in vars_attrs]):
raise ValueError("The datasets to combine must have matching variables, which was not the case")
unique = {key: {"units": value} for elem in vars_attrs[::-1] for key, value in elem.items()}
if np.all(
[
ureg.parse_units(value) == ureg.parse_units(unique[key]["units"])
for dic in vars_attrs
for key, value in dic.items()
]
):
return unique
raise UnitsError("Same variables among the datasets to combine have different units. Please convert units first")
[docs]
def units_check(datasets: Sequence[xr.Dataset]) -> None:
"""
Check that the data variables in the datasets all have same single units.
If not, it raises an error.
Parameters
----------
datasets : sequence of xarray.Dataset
each data variable in the datasets must have a 'units' attributes
Raises
------
UnitsError
if any variable in any of the input datasets have different units
"""
units = [ureg.parse_units(da.units) for ds in datasets for _, da in ds.data_vars.items()]
if len(set(units)) != 1:
raise UnitsError("Variables in the datasets do not all have same single units")