diff --git a/CHANGES.rst b/CHANGES.rst index 99c165ec3..cb7c9db91 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,7 +4,11 @@ Changelog v0.51.0 (unreleased) -------------------- -Contributors to this version: Trevor James Smith (:user:`Zeitsperre`). +Contributors to this version: Trevor James Smith (:user:`Zeitsperre`), Pascal Bourgault (:user:`aulemahal`). + +Bug fixes +^^^^^^^^^ +* Units of degree-days computations with Fahrenheit input fixed to yield "°R d". Added a new ``xclim.core.units.ensure_absolute_temperature`` method to convert from delta to absolute temperatures. (:issue:`1789`, :pull:`1804`). New features and enhancements ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/test_units.py b/tests/test_units.py index e94db9140..b59626fde 100644 --- a/tests/test_units.py +++ b/tests/test_units.py @@ -332,6 +332,8 @@ def index( ("", "sum", "count", 365, "d"), ("kg m-2", "var", "var", 0, "kg2 m-4"), ("°C", "argmax", "doymax", 0, ""), + ("°C", "sum", "integral", 365, "K d"), + ("°F", "sum", "integral", 365, "d °R"), # not sure why the order is different ], ) def test_to_agg_units(in_u, opfunc, op, exp, exp_u): diff --git a/xclim/core/units.py b/xclim/core/units.py index 3282de6f8..1c6ec4b62 100644 --- a/xclim/core/units.py +++ b/xclim/core/units.py @@ -463,6 +463,25 @@ def infer_sampling_units( return out +DELTA_ABSOLUTE_TEMP = { + units.delta_degC: units.kelvin, + units.delta_degF: units.rankine, +} + + +def ensure_absolute_temperature(units: str): + """Convert temperature units to their absolute counterpart, assuming they represented a difference (delta). + + Celsius becomes Kelvin, Fahrenheit becomes Rankine. Does nothing for other units. + """ + a = str2pint(units) + # ensure a delta pint unit + a = a - 0 * a + if a.units in DELTA_ABSOLUTE_TEMP: + return pint2cfunits(DELTA_ABSOLUTE_TEMP[a.units]) + return units + + def to_agg_units( out: xr.DataArray, orig: xr.DataArray, op: str, dim: str = "time" ) -> xr.DataArray: @@ -518,7 +537,7 @@ def to_agg_units( >>> degdays = dt.clip(0).sum("time") # Integral of temperature above a threshold >>> degdays = to_agg_units(degdays, dt, op="integral") >>> degdays.units - 'week delta_degC' + 'K week' Which we can always convert to the more common "K days": @@ -526,11 +545,16 @@ def to_agg_units( >>> degdays.units 'K d' """ - if op in ["amin", "min", "amax", "max", "mean", "std", "sum"]: + if op in ["amin", "min", "amax", "max", "mean", "sum"]: out.attrs["units"] = orig.attrs["units"] + elif op in ["std"]: + out.attrs["units"] = ensure_absolute_temperature(orig.attrs["units"]) + elif op in ["var"]: - out.attrs["units"] = pint2cfunits(str2pint(orig.units) ** 2) + out.attrs["units"] = pint2cfunits( + str2pint(ensure_absolute_temperature(orig.units)) ** 2 + ) elif op in ["doymin", "doymax"]: out.attrs.update( @@ -539,7 +563,7 @@ def to_agg_units( elif op in ["count", "integral"]: m, freq_u_raw = infer_sampling_units(orig[dim]) - orig_u = str2pint(orig.units) + orig_u = str2pint(ensure_absolute_temperature(orig.units)) freq_u = str2pint(freq_u_raw) out = out * m