diff --git a/doc/source/_static/logo_icclim_colored__displayed.svg b/doc/source/_static/logo_icclim_colored__displayed.svg index d30226e8..c3d52e89 100644 --- a/doc/source/_static/logo_icclim_colored__displayed.svg +++ b/doc/source/_static/logo_icclim_colored__displayed.svg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19a9c7820d76480eedf72b6205666d8780e32c7abcd26edeeeebd1c3da8d124e +oid sha256:cbb3b0dc6978cb3a9b3d72fc9f27d13ceb2f593ac081bb9ff8f687535dd923e5 size 66296 diff --git a/doc/source/_static/logo_icclim_grey__displayed.svg b/doc/source/_static/logo_icclim_grey__displayed.svg index 42245633..ffff6861 100644 --- a/doc/source/_static/logo_icclim_grey__displayed.svg +++ b/doc/source/_static/logo_icclim_grey__displayed.svg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:073c4d0906e1e64bbd854ed634a637a57e212093e68aaba2ae57cf6c3bbae6b3 +oid sha256:12c01ff2f5def2cc8c950f4f822b869adceadec85f1011ae5769dff33429094a size 63678 diff --git a/doc/source/_static/logo_icclim_white__displayed.svg b/doc/source/_static/logo_icclim_white__displayed.svg index 916167f5..1b05f433 100644 --- a/doc/source/_static/logo_icclim_white__displayed.svg +++ b/doc/source/_static/logo_icclim_white__displayed.svg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f3ca8685ceee5bb58c5cbc8a8bf4463f134101308aff61782d59bb8d605cf45 +oid sha256:c78c30348dce8a5c484e4fcd697fc9b4c00816daa80789e42fb93416bc47a1b4 size 39021 diff --git a/doc/source/references/release_notes.rst b/doc/source/references/release_notes.rst index bc181501..24dc0e76 100644 --- a/doc/source/references/release_notes.rst +++ b/doc/source/references/release_notes.rst @@ -1,6 +1,13 @@ Release history =============== +6.2.0 (unreleased) +------------------ +* [maint] Upgrade and adapt to xclim 0.40. + Moved PercentileDataArray from xclim to icclim. + Adapted the unit cenversion to use the hydro context. + + 6.1.5 ----- * [fix] Bug fix: not assuming longitude and latitude are lon and lat with respect to output metadata. Fix needed to work on E-OBS and other datasets. diff --git a/environment.yml b/environment.yml index 17fb0c2f..0270b39e 100644 --- a/environment.yml +++ b/environment.yml @@ -6,7 +6,7 @@ channels: dependencies: # Core dependencies - python>=3.8 - - xclim>=0.39 + - xclim>=0.40 - numpy - xarray>=2022.6 - cf_xarray>=0.7.4 diff --git a/icclim/generic_indices/generic_indicators.py b/icclim/generic_indices/generic_indicators.py index 5dc18351..4a073519 100644 --- a/icclim/generic_indices/generic_indicators.py +++ b/icclim/generic_indices/generic_indicators.py @@ -125,7 +125,7 @@ def postprocess( out_unit: str | None, ): if out_unit is not None: - result = convert_units_to(result, out_unit) + result = convert_units_to(result, out_unit, context="hydro") if self.missing != "skip" and indexer is not None: # reference variable is a subset of the studied variable, # so no need to check it. @@ -231,9 +231,10 @@ def preprocess( # noqa signature != from super for climate_var in climate_vars: current_unit = climate_var.studied_data.attrs.get(UNITS_KEY, None) if current_unit is not None and not _is_amount_unit(current_unit): - climate_var.studied_data = rate2amount( - climate_var.studied_data, out_units=output_unit - ) + with xc_units.context("hydro"): + climate_var.studied_data = rate2amount( + climate_var.studied_data, out_units=output_unit + ) if coef is not None: for climate_var in climate_vars: climate_var.studied_data = coef * climate_var.studied_data @@ -903,7 +904,7 @@ def get_couple_of_var( ) study = climate_vars[0].studied_data ref = climate_vars[1].studied_data - study = convert_units_to(study, ref) + study = convert_units_to(study, ref, context="hydro") return study, ref diff --git a/icclim/generic_indices/threshold.py b/icclim/generic_indices/threshold.py index 774ec609..f4e3a63f 100644 --- a/icclim/generic_indices/threshold.py +++ b/icclim/generic_indices/threshold.py @@ -14,7 +14,7 @@ from xclim.core.calendar import build_climatology_bounds, percentile_doy, resample_doy from xclim.core.units import convert_units_to, str2pint from xclim.core.units import units as xc_units -from xclim.core.utils import PercentileDataArray, calc_perc +from xclim.core.utils import calc_perc from icclim.generic_indices.threshold_templates import ( EN_THRESHOLD_TEMPLATE, @@ -37,6 +37,7 @@ QuantileInterpolationRegistry, ) from icclim.pre_processing.input_parsing import ( + PercentileDataArray, build_reference_da, find_standard_vars, get_name_of_first_var, @@ -439,7 +440,9 @@ def unit(self) -> str | None: def unit(self, unit: str | xr.DataArray | pint.Quantity | pint.Unit): if self.is_ready: if self.value.attrs.get(UNITS_KEY, None) is not None and unit is not None: - self._prepared_value = convert_units_to(self._prepared_value, unit) + self._prepared_value = convert_units_to( + self._prepared_value, unit, context="hydro" + ) self.value.attrs[UNITS_KEY] = unit @property @@ -645,7 +648,7 @@ def unit(self) -> str | None: @unit.setter def unit(self, unit): if self.value.attrs.get(UNITS_KEY, None) is not None and unit is not None: - self.value = convert_units_to(self.value, unit) + self.value = convert_units_to(self.value, unit, context="hydro") self.value.attrs[UNITS_KEY] = unit def __init__( @@ -690,7 +693,7 @@ def __init__( else: raise NotImplementedError(f"Cannot build threshold from a {type(value)}.") if unit is not None: - built_value = convert_units_to(built_value, unit) + built_value = convert_units_to(built_value, unit, context="hydro") self.operator = operator self.value = built_value self.unit = unit @@ -1049,9 +1052,10 @@ def _must_build_bounded_threshold(input: ThresholdBuilderInput) -> bool: def _apply_min_value(thresh_da: DataArray, min_value: pint.Quantity | None): if min_value is not None: if min_value.dimensionless: - min_value = convert_units_to(min_value.m, thresh_da) + # We assume min_value use the same unit as thresh_da if it's dimensionless + min_value = min_value.m else: - min_value = convert_units_to(str(min_value), thresh_da) + min_value = convert_units_to(str(min_value), thresh_da, context="hydro") built_value = thresh_da.where(thresh_da > min_value, np.nan) return built_value else: @@ -1095,6 +1099,6 @@ def _build_per_thresh_from_dataset( ) if unit is not None: if built_value.attrs.get(UNITS_KEY, None) is not None: - built_value = convert_units_to(built_value, unit) + built_value = convert_units_to(built_value, unit, context="hydro") built_value.attrs[UNITS_KEY] = unit return built_value, DOY_COORDINATE in built_value.coords diff --git a/icclim/models/constants.py b/icclim/models/constants.py index afb20302..a2d683c9 100644 --- a/icclim/models/constants.py +++ b/icclim/models/constants.py @@ -3,7 +3,7 @@ # fmt: off # flake8: noqa -ICCLIM_VERSION = "6.1.5" +ICCLIM_VERSION = "6.2.0" # placeholders for user_index PERCENTILE_THRESHOLD_STAMP = "p" diff --git a/icclim/pre_processing/input_parsing.py b/icclim/pre_processing/input_parsing.py index b0628666..0b55f8b3 100644 --- a/icclim/pre_processing/input_parsing.py +++ b/icclim/pre_processing/input_parsing.py @@ -10,7 +10,6 @@ from xarray.core.dataarray import DataArray from xarray.core.dataset import Dataset from xclim.core.units import convert_units_to -from xclim.core.utils import PercentileDataArray from icclim.generic_indices.standard_variable import ( StandardVariable, @@ -26,6 +25,69 @@ DEFAULT_INPUT_FREQUENCY = "days" +class PercentileDataArray(xr.DataArray): + """Wrap xarray DataArray for percentiles values.""" + + __slots__ = () + + @classmethod + def is_compatible(cls, source: xr.DataArray) -> bool: + """Evaluate whether PecentileDataArray is conformant with expected fields. + + A PercentileDataArray must have climatology_bounds attributes and either a + quantile or percentiles coordinate, the window is not mandatory. + """ + return ( + isinstance(source, xr.DataArray) + and source.attrs.get("climatology_bounds", None) is not None + and ("quantile" in source.coords or "percentiles" in source.coords) + ) + + @classmethod + def from_da( + cls, source: xr.DataArray, climatology_bounds: list[str] = None + ) -> PercentileDataArray: + """Create a PercentileDataArray from a xarray.DataArray. + + Parameters + ---------- + source : xr.DataArray + A DataArray with its content containing percentiles values. + It must also have a coordinate variable percentiles or quantile. + climatology_bounds : list[str] + Optional. A List of size two which contains the period on which the + percentiles were computed. See + `xclim.core.calendar.build_climatology_bounds` + to build this list from a DataArray. + + Returns + ------- + PercentileDataArray + The initial `source` DataArray but wrap by PercentileDataArray class. + The data is unchanged and only climatology_bounds attributes is overridden + if q new value is given in inputs. + """ + if ( + climatology_bounds is None + and source.attrs.get("climatology_bounds", None) is None + ): + raise ValueError("PercentileDataArray needs a climatology_bounds.") + per = cls(source) + # handle case where da was created with `quantile()` method + if "quantile" in source.coords: + per = per.rename({"quantile": "percentiles"}) + per.coords["percentiles"] = per.coords["percentiles"] * 100 + clim_bounds = source.attrs.get("climatology_bounds", climatology_bounds) + per.attrs["climatology_bounds"] = clim_bounds + if "percentiles" in per.coords: + return per + raise ValueError( + f"DataArray {source.name} could not be turned into" + f" PercentileDataArray. The DataArray must have a" + f" 'percentiles' coordinate variable." + ) + + def guess_var_names( ds: Dataset, var_names: str | Sequence[str] | None, @@ -300,7 +362,9 @@ def read_threshold_DataArray( else: if threshold_min_value: if isinstance(threshold_min_value, str): - threshold_min_value = convert_units_to(threshold_min_value, thresh_da) + threshold_min_value = convert_units_to( + threshold_min_value, thresh_da, context="hydro" + ) # todo in prcptot the replacing value (np.nan) needs to be 0 built_value = thresh_da.where(thresh_da > threshold_min_value, np.nan) else: @@ -330,6 +394,8 @@ def build_reference_da( if only_leap_years: reference = reduce_only_leap_years(original_da) if percentile_min_value is not None: - percentile_min_value = convert_units_to(str(percentile_min_value), reference) + percentile_min_value = convert_units_to( + str(percentile_min_value), reference, context="hydro" + ) reference = reference.where(reference >= percentile_min_value, np.nan) return reference diff --git a/icclim/tests/test_input_parsing.py b/icclim/tests/test_input_parsing.py index bdce2a16..424dcc6f 100644 --- a/icclim/tests/test_input_parsing.py +++ b/icclim/tests/test_input_parsing.py @@ -7,12 +7,12 @@ import pandas as pd import pytest import xarray as xr -from xclim.core.utils import PercentileDataArray from icclim.ecad.ecad_indices import EcadIndexRegistry from icclim.icclim_exceptions import InvalidIcclimArgumentError from icclim.models.constants import UNITS_KEY from icclim.pre_processing.input_parsing import ( + PercentileDataArray, guess_var_names, read_dataset, update_to_standard_coords, diff --git a/icclim/tests/test_threshold.py b/icclim/tests/test_threshold.py index e752fdf1..23b7f99e 100644 --- a/icclim/tests/test_threshold.py +++ b/icclim/tests/test_threshold.py @@ -21,6 +21,7 @@ from icclim.models.constants import UNITS_KEY from icclim.models.logical_link import LogicalLinkRegistry from icclim.models.operator import OperatorRegistry +from icclim.pre_processing.input_parsing import PercentileDataArray def test_value_error(): @@ -295,6 +296,7 @@ def test_threshold_min_value__number_from_file(self): def test_build_percentile_threshold__from_file(self): doys = percentile_doy(self.data) + doys = PercentileDataArray.from_da(doys) doys.to_netcdf(path=self.IN_FILE_PATH) res = build_threshold(operator=">=", value=self.IN_FILE_PATH) assert isinstance(res, PercentileThreshold) diff --git a/requirements.txt b/requirements.txt index fa8837cd..c4efa363 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,5 +18,5 @@ sphinx_codeautolink sphinx_copybutton sphinx_lfs_content xarray>=2022.6 -xclim~=0.39.0 +xclim~=0.40 zarr diff --git a/requirements_dev.txt b/requirements_dev.txt index 459c6785..e4ae9a04 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -25,5 +25,5 @@ sphinx_copybutton sphinx_lfs_content twine xarray>=2022.6 -xclim~=0.39.0 +xclim~=0.40 zarr diff --git a/setup.py b/setup.py index 568b2fe1..763d04c7 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ MINIMAL_REQUIREMENTS = [ "numpy>=1.16", "xarray>=2022.6", - "xclim>=0.39", + "xclim>=0.40", "cf_xarray>=0.7.4", "cftime>=1.4.1", "dask[array]", @@ -21,7 +21,7 @@ setup( name="icclim", - version="6.1.5", + version="6.2.0", packages=find_packages(), author="Christian P.", author_email="christian.page@cerfacs.fr",