diff --git a/src/xclim/core/dataflags.py b/src/xclim/core/dataflags.py index eb574b91d..54bcba821 100644 --- a/src/xclim/core/dataflags.py +++ b/src/xclim/core/dataflags.py @@ -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 @@ -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") diff --git a/src/xclim/core/indicator.py b/src/xclim/core/indicator.py index 7d417e2e1..37543fec6 100644 --- a/src/xclim/core/indicator.py +++ b/src/xclim/core/indicator.py @@ -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}" elif isinstance(v, int | float): mba[k] = f"{v:g}" # TODO: What about InputKind.NUMBER_SEQUENCE diff --git a/src/xclim/core/units.py b/src/xclim/core/units.py index 2bdb233a4..036835d3e 100644 --- a/src/xclim/core/units.py +++ b/src/xclim/core/units.py @@ -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 @@ -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" +# 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", @@ -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"] @@ -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: diff --git a/src/xclim/indices/_threshold.py b/src/xclim/indices/_threshold.py index a3580b212..c027da0ea 100644 --- a/src/xclim/indices/_threshold.py +++ b/src/xclim/indices/_threshold.py @@ -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. @@ -2994,7 +2994,7 @@ 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 @@ -3002,7 +3002,7 @@ def sea_ice_area( @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. diff --git a/src/xclim/indices/fire/_cffwis.py b/src/xclim/indices/fire/_cffwis.py index e59dc2cb6..bb2c3a0c6 100644 --- a/src/xclim/indices/fire/_cffwis.py +++ b/src/xclim/indices/fire/_cffwis.py @@ -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") diff --git a/src/xclim/indices/fire/_ffdi.py b/src/xclim/indices/fire/_ffdi.py index 5c06ee0b2..f8ba04bb1 100644 --- a/src/xclim/indices/fire/_ffdi.py +++ b/src/xclim/indices/fire/_ffdi.py @@ -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( diff --git a/tests/test_indicators.py b/tests/test_indicators.py index 8df70f4dd..20f71fdac 100644 --- a/tests/test_indicators.py +++ b/tests/test_indicators.py @@ -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." ] diff --git a/tests/test_indices.py b/tests/test_indices.py index f766b7d7f..6987959a3 100644 --- a/tests/test_indices.py +++ b/tests/test_indices.py @@ -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" diff --git a/tests/test_units.py b/tests/test_units.py index a0781ad4e..fbffbf391 100644 --- a/tests/test_units.py +++ b/tests/test_units.py @@ -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 @@ -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): @@ -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" @@ -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]")