diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8b3debf3..4e879058 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,7 +14,7 @@ on: jobs: black: name: Code Style Compliance - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest defaults: run: shell: bash -l {0} @@ -35,39 +35,57 @@ jobs: tox -e black testing: - name: Smoke Test with Python${{ matrix.python-version }} (${{ matrix.tox-build }}) + name: Smoke Test with Python${{ matrix.python-version }} needs: black - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - python-version: "3.9" - tox-build: "py39" +# tox-build: "py39" - python-version: "3.10" - tox-build: "py310" +# tox-build: "py310" defaults: run: shell: bash -l {0} steps: - uses: actions/checkout@v3 - name: Setup Conda (Micromamba) with Python ${{ matrix.python-version }} - uses: mamba-org/provision-with-micromamba@main + uses: mamba-org/setup-micromamba@v1 with: cache-downloads: true environment-file: environment.yml - extra-specs: | + create-args: >- + coveralls mamba python=${{ matrix.python-version }} pytest pytest-cov xdoctest - - name: Conda and mamba versions + - name: Conda and Mamba versions run: | mamba --version + echo "micromamba $(micromamba --version)" + - name: Install xscen + run: | + pip install --editable . + - name: Check versions + run: | + conda list + pip check - name: Test with pytest run: | pytest --cov xscen + - name: Report coverage + run: | + coveralls + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_FLAG_NAME: run-Python${{ matrix.python-version }} + COVERALLS_PARALLEL: true + COVERALLS_SERVICE_NAME: github + # - name: Install tox-current-env # run: | # pip install tox tox-conda tox-current-env @@ -77,3 +95,17 @@ jobs: # env: # CONDA_EXE: mamba # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + finish: + needs: + - testing + runs-on: ubuntu-latest + container: python:3-slim + steps: + - name: Coveralls Finished + run: | + pip install --upgrade coveralls + coveralls --finish + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_SERVICE_NAME: github diff --git a/HISTORY.rst b/HISTORY.rst index 8da75443..025722f2 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -4,7 +4,7 @@ History v0.7.0 (unreleased) ------------------- -Contributors to this version: Gabriel Rondeau-Genesse (:user:`RondeauG`). +Contributors to this version: Pascal Bourgault (:user:`aulemahal`), Gabriel Rondeau-Genesse (:user:`RondeauG`), Trevor James Smith (:user:`Zeitsperre`). Announcements ^^^^^^^^^^^^^ @@ -12,7 +12,7 @@ Announcements New features and enhancements ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -* N/A +* `xscen` now tracks code coverage using `coveralls `_. (:pull:`187`). Breaking changes ^^^^^^^^^^^^^^^^ @@ -21,11 +21,14 @@ Breaking changes Bug fixes ^^^^^^^^^ * Fix bug in ``unstack_dates`` with seasonal climatological mean. (:issue:`202`, :pull:`202`). +* Added NotImplemented errors when trying to call `climatological_mean` and `compute_deltas` with daily data. (:pull:`187`). Internal changes ^^^^^^^^^^^^^^^^ * Removed the pin on xarray's version. (:issue:`175`, :pull:`199`). * Updated ReadTheDocs configuration to prevent ``--eager`` installation of xscen (:pull:`209`). +* Implemented a template to be used for unit tests. (:pull:`187`). +* Updated GitHub Actions to remove deprecation warnings. (:pull:`187`). v0.6.0 (2023-05-04) ------------------- diff --git a/README.rst b/README.rst index 25bb90f4..01e5bbb4 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ xscen |logo| ============ -|pypi| |status| |build| |docs| |black| |pre-commit| |versions| +|pypi| |status| |build| |coverage| |docs| |black| |pre-commit| |versions| A climate change scenario-building analysis framework, built with Intake-esm catalogs and xarray-based packages such as xclim and xESMF. @@ -32,11 +32,16 @@ This package was created with Cookiecutter_ and the `Ouranosinc/cookiecutter-pyp .. |logo| image:: https://raw.githubusercontent.com/Ouranosinc/xscen/main/docs/_static/_images/xscen-logo-small.png :target: https://github.com/Ouranosinc/xscen + :alt: xscen Logo .. |build| image:: https://github.com/Ouranosinc/xscen/actions/workflows/main.yml/badge.svg :target: https://github.com/Ouranosinc/xscen/actions/workflows/main.yml :alt: Build Status +.. |coverage| image:: https://coveralls.io/repos/github/Ouranosinc/xscen/badge.svg + :target: https://coveralls.io/github/Ouranosinc/xscen + :alt: Code Coverage + .. |pypi| image:: https://img.shields.io/pypi/v/xscen.svg :target: https://pypi.python.org/pypi/xscen :alt: Python Package Index Build diff --git a/environment-dev.yml b/environment-dev.yml index b9336a99..f0668285 100644 --- a/environment-dev.yml +++ b/environment-dev.yml @@ -26,7 +26,7 @@ dependencies: - rechunker - shapely - xarray - - xclim >=0.37 + - xclim >=0.43.0 - xesmf >=0.7 - zarr # Opt @@ -34,6 +34,7 @@ dependencies: - pyarrow >=1.0.0 # For lighter in-memory catalogs # Dev - bumpversion + - coveralls - ipykernel - ipython - jupyter_client diff --git a/environment.yml b/environment.yml index 1f36ca72..51adcb00 100644 --- a/environment.yml +++ b/environment.yml @@ -25,7 +25,7 @@ dependencies: - rechunker - shapely - xarray - - xclim >=0.37 + - xclim >=0.43.0 - xesmf >=0.7 - zarr # Opt diff --git a/tests/test_aggregate.py b/tests/test_aggregate.py new file mode 100644 index 00000000..39b9c353 --- /dev/null +++ b/tests/test_aggregate.py @@ -0,0 +1,140 @@ +import numpy as np +import pytest +import xarray as xr +from xclim.testing.helpers import test_timeseries as timeseries + +import xscen as xs + + +class TestClimatologicalMean: + def test_daily(self): + ds = timeseries( + np.tile(np.arange(1, 13), 3), + variable="tas", + start="2001-01-01", + freq="D", + as_dataset=True, + ) + with pytest.raises(NotImplementedError): + xs.climatological_mean(ds) + + @pytest.mark.parametrize("xrfreq", ["MS", "AS-JAN"]) + def test_all_default(self, xrfreq): + o = 12 if xrfreq == "MS" else 1 + + ds = timeseries( + np.tile(np.arange(1, o + 1), 30), + variable="tas", + start="2001-01-01", + freq=xrfreq, + as_dataset=True, + ) + out = xs.climatological_mean(ds) + + # Test output values + np.testing.assert_array_equal(out.tas, np.arange(1, o + 1)) + assert len(out.time) == (o * len(np.unique(out.horizon.values))) + np.testing.assert_array_equal(out.time[0], ds.time[0]) + assert (out.horizon == "2001-2030").all() + # Test metadata + assert ( + out.tas.attrs["description"] + == f"30-year mean of {ds.tas.attrs['description']}" + ) + assert ( + "30-year rolling average (non-centered) with a minimum of 30 years of data" + in out.tas.attrs["history"] + ) + assert out.attrs["cat:processing_level"] == "climatology" + + @pytest.mark.parametrize("xrfreq", ["MS", "AS-JAN"]) + def test_options(self, xrfreq): + o = 12 if xrfreq == "MS" else 1 + + ds = timeseries( + np.tile(np.arange(1, o + 1), 30), + variable="tas", + start="2001-01-01", + freq=xrfreq, + as_dataset=True, + ) + out = xs.climatological_mean(ds, window=15, interval=5, to_level="for_testing") + + # Test output values + np.testing.assert_array_equal( + out.tas, + np.tile(np.arange(1, o + 1), len(np.unique(out.horizon.values))), + ) + assert len(out.time) == (o * len(np.unique(out.horizon.values))) + np.testing.assert_array_equal(out.time[0], ds.time[0]) + assert {"2001-2015", "2006-2020", "2011-2025", "2016-2030"}.issubset( + out.horizon.values + ) + # Test metadata + assert ( + out.tas.attrs["description"] + == f"15-year mean of {ds.tas.attrs['description']}" + ) + assert ( + "15-year rolling average (non-centered) with a minimum of 15 years of data" + in out.tas.attrs["history"] + ) + assert out.attrs["cat:processing_level"] == "for_testing" + + def test_minperiods(self): + ds = timeseries( + np.tile(np.arange(1, 5), 30), + variable="tas", + start="2001-03-01", + freq="QS-DEC", + as_dataset=True, + ) + ds = ds.where(ds["time"].dt.strftime("%Y-%m-%d") != "2030-12-01") + + out = xs.climatological_mean(ds, window=30) + assert all(np.isreal(out.tas)) + assert len(out.time) == 4 + np.testing.assert_array_equal(out.tas, np.arange(1, 5)) + + out = xs.climatological_mean(ds, window=30, min_periods=30) + assert np.sum(np.isnan(out.tas)) == 1 + + with pytest.raises(ValueError): + xs.climatological_mean(ds, window=5, min_periods=6) + + def test_periods(self): + ds1 = timeseries( + np.tile(np.arange(1, 2), 10), + variable="tas", + start="2001-01-01", + freq="AS-JAN", + as_dataset=True, + ) + ds2 = timeseries( + np.tile(np.arange(1, 2), 10), + variable="tas", + start="2021-01-01", + freq="AS-JAN", + as_dataset=True, + ) + + ds = xr.concat([ds1, ds2], dim="time") + with pytest.raises(ValueError): + xs.climatological_mean(ds) + + out = xs.climatological_mean(ds, periods=[["2001", "2010"], ["2021", "2030"]]) + assert len(out.time) == 2 + assert {"2001-2010", "2021-2030"}.issubset(out.horizon.values) + + @pytest.mark.parametrize("cal", ["proleptic_gregorian", "noleap", "360_day"]) + def test_calendars(self, cal): + ds = timeseries( + np.tile(np.arange(1, 2), 30), + variable="tas", + start="2001-01-01", + freq="AS-JAN", + as_dataset=True, + ) + + out = xs.climatological_mean(ds.convert_calendar(cal, align_on="date")) + assert out.time.dt.calendar == cal diff --git a/xscen/aggregate.py b/xscen/aggregate.py index 1e6e238a..70b8600b 100644 --- a/xscen/aggregate.py +++ b/xscen/aggregate.py @@ -73,6 +73,11 @@ def climatological_mean( Returns a Dataset of the climatological mean """ + if xr.infer_freq(ds.time) == "D": + raise NotImplementedError( + "xs.climatological_mean does not currently support daily data." + ) + # there is one less occurrence when a period crosses years freq_across_year = [ f"{f}-{mon}" @@ -100,9 +105,17 @@ def climatological_mean( window = window or int(periods[0][1]) - int(periods[0][0]) + 1 - if ds.attrs.get("cat:xrfreq") in freq_across_year and min_periods is None: + if ( + any( + x in freq_across_year + for x in [ds.attrs.get("cat:xrfreq"), xr.infer_freq(ds.time)] + ) + and min_periods is None + ): min_periods = window - 1 min_periods = min_periods or window + if min_periods > window: + raise ValueError("'min_periods' should be smaller or equal to 'window'") for period in periods: # Rolling average @@ -174,7 +187,6 @@ def climatological_mean( else new_history ) ds_rolling[vv].attrs["history"] = history - if to_level is not None: ds_rolling.attrs["cat:processing_level"] = to_level @@ -234,6 +246,11 @@ def compute_deltas( ) if "time" in ds: + if xr.infer_freq(ds.time) == "D": + raise NotImplementedError( + "xs.climatological_mean does not currently support daily data." + ) + # Remove references to 'year' in REF ind = pd.MultiIndex.from_arrays( [ref.time.dt.month.values, ref.time.dt.day.values], names=["month", "day"]