Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clean and export the units registry #2040

Merged
merged 7 commits into from
Jan 20, 2025
Merged
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
3 changes: 1 addition & 2 deletions src/xclim/core/dataflags.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from __future__ import annotations

from collections.abc import Callable, Sequence
from decimal import Decimal
from functools import reduce
from inspect import signature

Expand Down Expand Up @@ -674,7 +673,7 @@ def _get_variable_name(function, _kwargs):
format_args[arg] = "array"
else:
val = str2pint(val).magnitude
if Decimal(val) % 1 == 0:
if val == int(val):
val = str(int(val))
else:
val = str(val).replace(".", "point")
Expand Down
2 changes: 1 addition & 1 deletion src/xclim/core/indicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1311,7 +1311,7 @@ def _format(
# Add formatting {} around values to be able to replace them with _attrs_mapping using format.
for k, v in args.items():
if isinstance(v, units.Quantity):
mba[k] = f"{v:g~P}"
mba[k] = f"{v:gcf}"
Zeitsperre marked this conversation as resolved.
Show resolved Hide resolved
elif isinstance(v, int | float):
mba[k] = f"{v:g}"
# TODO: What about InputKind.NUMBER_SEQUENCE
Expand Down
37 changes: 14 additions & 23 deletions src/xclim/core/units.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import pint
import xarray as xr
from boltons.funcutils import wraps
from pint import UndefinedUnitError
from yaml import safe_load

from xclim.core._exceptions import ValidationError
Expand Down Expand Up @@ -64,28 +63,21 @@
units = deepcopy(cf_xarray.units.units)
# Changing the default string format for units/quantities.
# CF is implemented by cf-xarray, g is the most versatile float format.
# The following try/except logic can be removed when xclim drops support numpy <2.0.
try:
units.formatter.default_format = "gcf"
except UndefinedUnitError:
units.default_format = "gcf"
# Switch this flag back to False. Not sure what that implies, but it breaks some tests.
units.force_ndarray_like = False # noqa: F841
# Another alias not included by cf_xarray
units.define("@alias percent = pct")

# Default context.
null = pint.Context("none")
units.add_context(null)
units.formatter.default_format = "gcf"
Zeitsperre marked this conversation as resolved.
Show resolved Hide resolved
# CF-xarray forces numpy arrays even for scalar values, not sure why.
# We don't want that in xclim, the magnitude of a scalar is a scalar (float).
units.force_ndarray_like = False

# Precipitation units. This is an artificial unit that we're using to verify that a given unit can be converted into
# a precipitation unit. Ideally this could be checked through the `dimensionality`, but I can't get it to work.
# Define dimensionalities for convenience with the `declare_units` decorator
units.define("[precipitation] = [mass] / [length] ** 2 / [time]")
units.define("mmday = 1 kg / meter ** 2 / day")

units.define("[discharge] = [length] ** 3 / [time]")
units.define("cms = meter ** 3 / second")
units.define("[radiation] = [power] / [length]**2")

# Default context. This is essentially a convenience, so that we can pass a context systemtically to pint's methods.
null = pint.Context("none")
units.add_context(null)

# Convenience context for common transformation involving liquid water
hydro = pint.Context("hydro")
hydro.add_transformation(
"[mass] / [length]**2",
Expand All @@ -109,6 +101,9 @@
)
units.add_context(hydro)

# Set as application registry
pint.set_application_registry(units)


with (files("xclim.data") / "variables.yml").open() as variables:
CF_CONVERSIONS = safe_load(variables)["conversions"]
Expand Down Expand Up @@ -136,10 +131,6 @@ def _func_register(func: Callable) -> Callable:
return _func_register


# Radiation units
units.define("[radiation] = [power] / [length]**2")


def units2pint(
value: xr.DataArray | units.Unit | units.Quantity | dict | str,
) -> pint.Unit:
Expand Down
6 changes: 3 additions & 3 deletions src/xclim/indices/_threshold.py
Original file line number Diff line number Diff line change
Expand Up @@ -2963,7 +2963,7 @@ def maximum_consecutive_tx_days(

@declare_units(siconc="[]", areacello="[area]", thresh="[]")
def sea_ice_area(
siconc: xarray.DataArray, areacello: xarray.DataArray, thresh: Quantified = "15 pct"
siconc: xarray.DataArray, areacello: xarray.DataArray, thresh: Quantified = "15 %"
) -> xarray.DataArray:
"""
Total sea ice area.
Expand Down Expand Up @@ -2994,15 +2994,15 @@ def sea_ice_area(
"What is the difference between sea ice area and extent?" - :cite:cts:`nsidc_frequently_2008`
"""
t = convert_units_to(thresh, siconc)
factor = convert_units_to("100 pct", siconc)
factor = convert_units_to("100 %", siconc)
sia = xarray.dot(siconc.where(siconc >= t, 0), areacello) / factor
sia = sia.assign_attrs(units=areacello.units)
return sia


@declare_units(siconc="[]", areacello="[area]", thresh="[]")
def sea_ice_extent(
siconc: xarray.DataArray, areacello: xarray.DataArray, thresh: Quantified = "15 pct"
siconc: xarray.DataArray, areacello: xarray.DataArray, thresh: Quantified = "15 %"
) -> xarray.DataArray:
"""
Total sea ice extent.
Expand Down
2 changes: 1 addition & 1 deletion src/xclim/indices/fire/_cffwis.py
Original file line number Diff line number Diff line change
Expand Up @@ -1397,7 +1397,7 @@ def cffwis_indices(
tas = convert_units_to(tas, "C")
pr = convert_units_to(pr, "mm/day")
sfcWind = convert_units_to(sfcWind, "km/h")
hurs = convert_units_to(hurs, "pct")
hurs = convert_units_to(hurs, "%")
if snd is not None:
snd = convert_units_to(snd, "m")

Expand Down
2 changes: 1 addition & 1 deletion src/xclim/indices/fire/_ffdi.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ def mcarthur_forest_fire_danger_index(
:cite:cts:`ffdi-noble_1980,ffdi-dowdy_2018,ffdi-holgate_2017`
"""
tasmax = convert_units_to(tasmax, "C")
hurs = convert_units_to(hurs, "pct")
hurs = convert_units_to(hurs, "%")
sfcWind = convert_units_to(sfcWind, "km/h")

ffdi = drought_factor**0.987 * np.exp(
Expand Down
8 changes: 4 additions & 4 deletions tests/test_indicators.py
Original file line number Diff line number Diff line change
Expand Up @@ -564,18 +564,18 @@ def test_formatting(pr_series):
# pint 0.10 now pretty print day as d.
assert (
out.attrs["long_name"]
== "Number of days with daily precipitation at or above 1 mm/d"
== "Number of days with daily precipitation at or above 1 mm d-1"
)
assert out.attrs["description"] in [
"Annual number of days with daily precipitation at or above 1 mm/d."
"Annual number of days with daily precipitation at or above 1 mm d-1."
]
out = atmos.wetdays(pr_series(np.arange(366)), thresh=1.5 * units.mm / units.day)
assert (
out.attrs["long_name"]
== "Number of days with daily precipitation at or above 1.5 mm/d"
== "Number of days with daily precipitation at or above 1.5 mm d-1"
)
assert out.attrs["description"] in [
"Annual number of days with daily precipitation at or above 1.5 mm/d."
"Annual number of days with daily precipitation at or above 1.5 mm d-1."
]


Expand Down
6 changes: 3 additions & 3 deletions tests/test_indices.py
Original file line number Diff line number Diff line change
Expand Up @@ -3332,19 +3332,19 @@ def test_simple(self):
np.arange(4).reshape(1, 2, 2),
dims=["time", "lat", "lon"],
coords={"time": [1], "lat": [-45, 45], "lon": [30, 60]},
attrs={"units": "mmday"},
attrs={"units": "mm/day"},
)
tas_baseline = xr.DataArray(
np.arange(4).reshape(1, 2, 2),
dims=["time", "lat", "lon"],
coords={"time": [1], "lat": [-45, 45], "lon": [30, 60]},
attrs={"units": "C"},
attrs={"units": "degC"},
)
tas_future = xr.DataArray(
np.arange(40).reshape(10, 2, 2),
dims=["time_fut", "lat", "lon"],
coords={"time_fut": np.arange(10), "lat": [-45, 45], "lon": [30, 60]},
attrs={"units": "C"},
attrs={"units": "degC"},
)
delta_tas = tas_future - tas_baseline
delta_tas.attrs["units"] = "delta_degC"
Expand Down
14 changes: 2 additions & 12 deletions tests/test_units.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ def test_hydro(self):
with units.context("hydro"):
q = 1 * units.kg / units.m**2 / units.s
assert q.to("mm/day") == q.to("mm/d")
assert q.to("mmday").magnitude == 24 * 60**2

def test_lat_lon(self):
assert 100 * units.degreeN == 100 * units.degree
Expand All @@ -55,19 +54,15 @@ def test_pcic(self):
np.isclose(1 * fu, 1 * tu)

def test_dimensionality(self):
# Check that the hydro context allows flux to rate conversion
with units.context("hydro"):
fu = 1 * units.parse_units("kg / m**2 / s")
tu = 1 * units.parse_units("mm / d")
fu.to("mmday")
tu.to("mmday")
fu.to("mm/day")

def test_fraction(self):
q = 5 * units.percent
assert q.to("dimensionless") == 0.05

q = 5 * units.parse_units("pct")
assert q.to("dimensionless") == 0.05


class TestConvertUnitsTo:
def test_deprecation(self, tas_series):
Expand Down Expand Up @@ -135,9 +130,6 @@ def test_pint2cfunits(self):
u = units("percent")
assert pint2cfunits(u.units) == "%"

u = units("pct")
assert pint2cfunits(u.units) == "%"

def test_units2pint(self, pr_series):
u = units2pint(pr_series([1, 2]))
assert pint2cfunits(u) == "kg m-2 s-1"
Expand Down Expand Up @@ -168,11 +160,9 @@ def test_str2pint(self):
class TestCheckUnits:
def test_basic(self):
check_units("%", "[]")
check_units("pct", "[]")
check_units("mm/day", "[precipitation]")
check_units("mm/s", "[precipitation]")
check_units("kg/m2/s", "[precipitation]")
check_units("cms", "[discharge]")
check_units("m3/s", "[discharge]")
check_units("m/s", "[speed]")
check_units("km/h", "[speed]")
Expand Down
Loading