From 0dec48d0f0c13b348e3607276c5e7d9c8cb85a92 Mon Sep 17 00:00:00 2001 From: Travis Logan Date: Tue, 25 Jun 2019 10:18:13 -0400 Subject: [PATCH 1/2] Update HISTORY.rst --- HISTORY.rst | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 7231208e8..07a5f540f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,7 +11,6 @@ History 0.10-beta (2019-06-06) ---------------------- -* Indicators are now split into packages named by *realms*. ``import xclim.atmos`` is now the method for loading indicators related to atmospheric variables. * Removed support for Python 2 compatibility. * Added support for *period of the year* subsetting in ``checks.missing_any``. * Now allow for passing positive longitude values when subsetting data with negative longitudes. @@ -19,7 +18,17 @@ History 0.9-beta (2019-05-13) --------------------- -TODO +This is a significant jump in the release. Many modifications have been made and will be added to the documentation in the coming days. Among the many changes: + +* New indices have been added with documentation and call examples +* Run_length based operations have been optimized +* Support for CF non-standard calendars +* Automated/improved unit conversion and management via pint library +* Added ensemble utilities for creation and analysis of muti-model climate ensembles +* Added subsetting utilities for spatio-temporal subsets of xarray data objects +* Added streamflow indicators +* Refactoring of the code : separation of indices.py into a directory with sub-files (simple, threshold and multivariate); ensembles and subset utilities separated into distinct modules (pulled from utils.py) +* Indicators are now split into packages named by realms. import xclim.atmos to load indicators related to atmospheric variables. 0.8-beta (2019-02-11) --------------------- From a2a1db2b61b81157362efc3dd59e342efd84f639 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Wed, 26 Jun 2019 11:02:51 -0400 Subject: [PATCH 2/2] Pre-Commit Hooks and Black code formatting (#239) * Added pre-commit code formatting hooks * Integrated Python Black code formatting and painted the source code Black * Setup Python Black for CI services * Corrected travis build versions * Corrected conda install call * Updated HISTORY.rst --- .github/CONTRIBUTING.rst | 44 +- .pre-commit-config.yaml | 34 ++ .travis.yml | 6 +- HISTORY.rst | 4 +- requirements_dev.txt | 1 + tests/conftest.py | 38 +- tests/test_checks.py | 75 ++-- tests/test_generic.py | 39 +- tests/test_indices.py | 417 +++++++++-------- tests/test_modules.py | 30 +- tests/test_precip.py | 181 ++++---- tests/test_run_length.py | 126 ++++-- tests/test_streamflow.py | 48 +- tests/test_temperature.py | 606 ++++++++++++++----------- tests/test_utils.py | 473 +++++++++++-------- tox.ini | 10 +- xclim/__init__.py | 124 ++--- xclim/atmos/_precip.py | 179 ++++---- xclim/atmos/_temperature.py | 800 ++++++++++++++++++--------------- xclim/checks.py | 60 +-- xclim/ensembles.py | 130 ++++-- xclim/generic.py | 97 ++-- xclim/indices/_multivariate.py | 251 +++++++---- xclim/indices/_simple.py | 138 +++--- xclim/indices/_threshold.py | 178 ++++---- xclim/run_length.py | 123 ++--- xclim/streamflow.py | 124 ++--- xclim/subset.py | 25 +- xclim/testing/common.py | 68 ++- xclim/utils.py | 373 +++++++++------ 30 files changed, 2744 insertions(+), 2058 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.github/CONTRIBUTING.rst b/.github/CONTRIBUTING.rst index b4ea8764b..77d1bd7d9 100644 --- a/.github/CONTRIBUTING.rst +++ b/.github/CONTRIBUTING.rst @@ -60,38 +60,62 @@ Get Started! Ready to contribute? Here's how to set up `xclim` for local development. 1. Fork the `xclim` repo on GitHub. + 2. Clone your fork locally:: $ git clone git@github.com:Ouranosinc/xclim.git 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: + # For virtualenv environments: $ mkvirtualenv xclim + + # For Anaconda/Miniconda environments: + $ conda create -n xclim python=3.6 + $ cd xclim/ - $ pip install -e . + $ pip install -e . 4. Create a branch for local development:: $ git checkout -b name-of-your-bugfix-or-feature - Now you can make your changes locally. + Now you can make your changes locally! + +5. When you're done making changes, check that you verify your changes with `black` and run the tests, including testing other Python versions with `tox`:: + + # For virtualenv environments: + $ pip install black pytest tox -5. When you're done making changes, check that your changes pass flake8 and the - tests, including testing other Python versions with tox:: + # For Anaconda/Miniconda environments: + $ conda install -c conda-forge black pytest tox - $ flake8 xclim tests - $ python setup.py test or py.test + $ black xclim tests + $ python setup.py test OR pytest test $ tox - To get flake8 and tox, just pip install them into your virtualenv. +6. Before committing your changes, we ask that you install `pre-commit` in your virtualenv. `Pre-commit` runs git hooks that ensure that your code resembles that of the project and catches and corrects any small errors or inconsistencies when you `git commit`:: -6. Commit your changes and push your branch to GitHub:: + # For virtualenv environments: + $ pip install pre-commit + + # For Anaconda/Miniconda environments: + $ conda install -c conda-forge pre_commit + + $ pre-commit install + +7. Commit your changes and push your branch to GitHub:: + + $ git add * - $ git add . $ git commit -m "Your detailed description of your changes." + # `pre-commit` will run checks at this point: + # if no errors are found, chnages will be committed. + # if errors are found, modifications will be mades. Simply `git commit` again. + $ git push origin name-of-your-bugfix-or-feature -7. Submit a pull request through the GitHub website. +8. Submit a pull request through the GitHub website. Pull Request Guidelines ----------------------- diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..6f721ad87 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,34 @@ +default_language_version: + python: python3.6 +#exclude: '^$' +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.2.3 + hooks: + - id: trailing-whitespace + language_version: python3 + - id: end-of-file-fixer + language_version: python3 + - id: check-yaml + language_version: python3 + - id: debug-statements + language_version: python3 +- repo: https://github.com/ambv/black + rev: 19.3b0 + hooks: + - id: black + language_version: python3 +- repo: https://github.com/asottile/reorder_python_imports + rev: v1.5.0 + hooks: + - id: reorder-python-imports + language_version: python3 +- repo: https://github.com/asottile/pyupgrade + rev: v1.19.0 + hooks: + - id: pyupgrade + language_version: python3 +- repo: meta + hooks: + - id: check-hooks-apply + - id: check-useless-excludes diff --git a/.travis.yml b/.travis.yml index e19e2413f..b7751f7de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,9 +11,9 @@ matrix: fast_finish: true include: - env: TOXENV=docs - python: 3.5 - - env: TOXENV=flake8 - python: 3.5 + python: 3.6 + - env: TOXENV=black + python: 3.6 - env: TOXENV=py38-dev python: 3.8-dev dist: xenial diff --git a/HISTORY.rst b/HISTORY.rst index 07a5f540f..509313ec1 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -8,6 +8,8 @@ History * Removed attributes in netCDF output from Indicators that are not in the CF-convention. * Added `fit` indicator to fit the parameters of a distribution to a series. * Added utilities with ensemble, run length, and subset algorithms to the documentation. +* Source code development standards now implement Python Black formatting. +* Pre-commit is now used to launch code formatting inspections for local development. 0.10-beta (2019-06-06) ---------------------- @@ -74,5 +76,3 @@ Class-based indicators are new methods that allow index calculation with error-c 0.1.0-dev (2018-08-23) ---------------------- * First release on PyPI. - - diff --git a/requirements_dev.txt b/requirements_dev.txt index a31fe7849..7ab1a0def 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -8,3 +8,4 @@ coverage twine pytest pytest-runner +pre-commit diff --git a/tests/conftest.py b/tests/conftest.py index eb5b02001..1ce0c6781 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,16 +1,20 @@ -import pytest +import numpy as np import pandas as pd +import pytest import xarray as xr -import numpy as np @pytest.fixture def q_series(): - def _q_series(values, start='1/1/2000'): + def _q_series(values, start="1/1/2000"): coords = pd.date_range(start, periods=len(values), freq=pd.DateOffset(days=1)) - return xr.DataArray(values, coords=[coords, ], dims='time', name='q', - attrs={'standard_name': 'dis', - 'units': 'm3 s-1'}) + return xr.DataArray( + values, + coords=[coords], + dims="time", + name="q", + attrs={"standard_name": "dis", "units": "m3 s-1"}, + ) return _q_series @@ -21,15 +25,17 @@ def ndq_series(): x = np.arange(0, nx) y = np.arange(0, ny) - cx = xr.IndexVariable('x', x) - cy = xr.IndexVariable('y', y) - dates = pd.date_range('1900-01-01', periods=nt, freq=pd.DateOffset(days=1)) + cx = xr.IndexVariable("x", x) + cy = xr.IndexVariable("y", y) + dates = pd.date_range("1900-01-01", periods=nt, freq=pd.DateOffset(days=1)) - time = xr.IndexVariable('time', dates, attrs={'units': 'days since 1900-01-01', 'calendar': 'standard'}) + time = xr.IndexVariable( + "time", dates, attrs={"units": "days since 1900-01-01", "calendar": "standard"} + ) - return xr.DataArray(np.random.lognormal(10, 1, (nt, nx, ny)), - dims=('time', 'x', 'y'), - coords={'time': time, 'x': cx, 'y': cy}, - attrs={'units': 'm^3 s-1', - 'standard_name': 'streamflow'} - ) + return xr.DataArray( + np.random.lognormal(10, 1, (nt, nx, ny)), + dims=("time", "x", "y"), + coords={"time": time, "x": cx, "y": cy}, + attrs={"units": "m^3 s-1", "standard_name": "streamflow"}, + ) diff --git a/tests/test_checks.py b/tests/test_checks.py index 274970c58..2c7375e1d 100644 --- a/tests/test_checks.py +++ b/tests/test_checks.py @@ -7,7 +7,8 @@ from xclim import checks from xclim.atmos import tg_mean -from xclim.testing.common import tas_series, tasmin_series +from xclim.testing.common import tas_series +from xclim.testing.common import tasmin_series TAS_SERIES = tas_series TASMIN_SERIES = tasmin_series @@ -15,124 +16,122 @@ K2C = 273.15 TESTS_HOME = Path(__file__).absolute().parent -TESTS_DATA = Path(TESTS_HOME, 'testdata') +TESTS_DATA = Path(TESTS_HOME, "testdata") class TestDateHandling: - def test_assert_daily(self): n = 365 # one day short of a full year - times = pd.date_range('2000-01-01', freq='1D', periods=n) - da = xr.DataArray(np.arange(n), [('time', times)], attrs={'units': 'K'}) + times = pd.date_range("2000-01-01", freq="1D", periods=n) + da = xr.DataArray(np.arange(n), [("time", times)], attrs={"units": "K"}) tg_mean(da) # Bad frequency def test_bad_frequency(self): with pytest.raises(ValueError): n = 365 - times = pd.date_range('2000-01-01', freq='12H', periods=n) - da = xr.DataArray(np.arange(n), [('time', times)], attrs={'units': 'K'}) + times = pd.date_range("2000-01-01", freq="12H", periods=n) + da = xr.DataArray(np.arange(n), [("time", times)], attrs={"units": "K"}) tg_mean(da) # Missing one day between the two years def test_missing_one_day_between_two_years(self): with pytest.raises(ValueError): n = 365 - times = pd.date_range('2000-01-01', freq='1D', periods=n) - times = times.append(pd.date_range('2001-01-01', freq='1D', periods=n)) - da = xr.DataArray(np.arange(2 * n), [('time', times)], attrs={'units': 'K'}) + times = pd.date_range("2000-01-01", freq="1D", periods=n) + times = times.append(pd.date_range("2001-01-01", freq="1D", periods=n)) + da = xr.DataArray(np.arange(2 * n), [("time", times)], attrs={"units": "K"}) tg_mean(da) # Duplicate dates def test_duplicate_dates(self): with pytest.raises(ValueError): n = 365 - times = pd.date_range('2000-01-01', freq='1D', periods=n) - times = times.append(pd.date_range('2000-12-29', freq='1D', periods=n)) - da = xr.DataArray(np.arange(2 * n), [('time', times)], attrs={'units': 'K'}) + times = pd.date_range("2000-01-01", freq="1D", periods=n) + times = times.append(pd.date_range("2000-12-29", freq="1D", periods=n)) + da = xr.DataArray(np.arange(2 * n), [("time", times)], attrs={"units": "K"}) tg_mean(da) class TestMissingAnyFills: - def test_missing_days(self, tas_series): - a = np.arange(360.) + a = np.arange(360.0) a[5:10] = np.nan ts = tas_series(a) - out = checks.missing_any(ts, freq='MS') + out = checks.missing_any(ts, freq="MS") assert out[0] assert not out[1] def test_missing_months(self): n = 66 - times = pd.date_range('2001-12-30', freq='1D', periods=n) - da = xr.DataArray(np.arange(n), [('time', times)]) - miss = checks.missing_any(da, 'MS') + times = pd.date_range("2001-12-30", freq="1D", periods=n) + da = xr.DataArray(np.arange(n), [("time", times)]) + miss = checks.missing_any(da, "MS") np.testing.assert_array_equal(miss, [True, False, False, True]) def test_missing_years(self): n = 378 - times = pd.date_range('2001-12-31', freq='1D', periods=n) - da = xr.DataArray(np.arange(n), [('time', times)]) - miss = checks.missing_any(da, 'YS') + times = pd.date_range("2001-12-31", freq="1D", periods=n) + da = xr.DataArray(np.arange(n), [("time", times)]) + miss = checks.missing_any(da, "YS") np.testing.assert_array_equal(miss, [True, False, True]) def test_missing_season(self): n = 378 - times = pd.date_range('2001-12-31', freq='1D', periods=n) - da = xr.DataArray(np.arange(n), [('time', times)]) - miss = checks.missing_any(da, 'Q-NOV') + times = pd.date_range("2001-12-31", freq="1D", periods=n) + da = xr.DataArray(np.arange(n), [("time", times)]) + miss = checks.missing_any(da, "Q-NOV") np.testing.assert_array_equal(miss, [True, False, False, False, True]) def test_to_period_start(self, tasmin_series): a = np.zeros(365) + K2C + 5.0 a[2] -= 20 ts = tasmin_series(a) - miss = checks.missing_any(ts, freq='AS-JUL') + miss = checks.missing_any(ts, freq="AS-JUL") np.testing.assert_equal(miss, [False]) def test_to_period_end(self, tasmin_series): a = np.zeros(365) + K2C + 5.0 a[2] -= 20 ts = tasmin_series(a) - miss = checks.missing_any(ts, freq='A-JUN') + miss = checks.missing_any(ts, freq="A-JUN") np.testing.assert_equal(miss, [False]) def test_month(self, tasmin_series): ts = tasmin_series(np.zeros(36)) - miss = checks.missing_any(ts, freq='YS', month=7) + miss = checks.missing_any(ts, freq="YS", month=7) np.testing.assert_equal(miss, [False]) - miss = checks.missing_any(ts, freq='YS', month=8) + miss = checks.missing_any(ts, freq="YS", month=8) np.testing.assert_equal(miss, [True]) with pytest.raises(ValueError, match=r"No data for selected period."): - miss = checks.missing_any(ts, freq='YS', month=1) + miss = checks.missing_any(ts, freq="YS", month=1) - miss = checks.missing_any(ts, freq='YS', month=[7, 8]) + miss = checks.missing_any(ts, freq="YS", month=[7, 8]) np.testing.assert_equal(miss, [True]) ts = tasmin_series(np.zeros(76)) - miss = checks.missing_any(ts, freq='YS', month=[7, 8]) + miss = checks.missing_any(ts, freq="YS", month=[7, 8]) np.testing.assert_equal(miss, [False]) def test_season(self, tasmin_series): ts = tasmin_series(np.zeros(360)) - miss = checks.missing_any(ts, freq='YS', season='MAM') + miss = checks.missing_any(ts, freq="YS", season="MAM") np.testing.assert_equal(miss, [False]) - miss = checks.missing_any(ts, freq='YS', season='JJA') + miss = checks.missing_any(ts, freq="YS", season="JJA") np.testing.assert_array_equal(miss, [True, True]) - miss = checks.missing_any(ts, freq='YS', season='SON') + miss = checks.missing_any(ts, freq="YS", season="SON") np.testing.assert_equal(miss, [False]) def test_hydro(self): - fn = Path(TESTS_DATA, 'Raven', 'q_sim.nc') + fn = Path(TESTS_DATA, "Raven", "q_sim.nc") ds = xr.open_dataset(fn) - miss = checks.missing_any(ds.q_sim, freq='YS') + miss = checks.missing_any(ds.q_sim, freq="YS") np.testing.assert_array_equal(miss[:-1], False) np.testing.assert_array_equal(miss[-1], True) - miss = checks.missing_any(ds.q_sim, freq='YS', season='JJA') + miss = checks.missing_any(ds.q_sim, freq="YS", season="JJA") np.testing.assert_array_equal(miss, False) diff --git a/tests/test_generic.py b/tests/test_generic.py index ea4ca0010..38b040b11 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -6,58 +6,57 @@ class TestFA(object): - def setup(self): self.nx, self.ny = 2, 3 x = np.arange(0, self.nx) y = np.arange(0, self.ny) - cx = xr.IndexVariable('x', x) - cy = xr.IndexVariable('y', y) - time = xr.IndexVariable('time', np.arange(50)) + cx = xr.IndexVariable("x", x) + cy = xr.IndexVariable("y", y) + time = xr.IndexVariable("time", np.arange(50)) - self.da = xr.DataArray(np.random.lognormal(10, 1, (len(time), self.nx, self.ny)), - dims=('time', 'x', 'y'), - coords={'time': time, 'x': cx, 'y': cy} - ) + self.da = xr.DataArray( + np.random.lognormal(10, 1, (len(time), self.nx, self.ny)), + dims=("time", "x", "y"), + coords={"time": time, "x": cx, "y": cy}, + ) def test_fit(self): - p = generic.fit(self.da, 'lognorm') + p = generic.fit(self.da, "lognorm") - assert p.dims[0] == 'dparams' - assert p.get_axis_num('dparams') == 0 + assert p.dims[0] == "dparams" + assert p.get_axis_num("dparams") == 0 p0 = lognorm.fit(self.da.values[:, 0, 0]) np.testing.assert_array_equal(p[:, 0, 0], p0) # Check that we can reuse the parameters with scipy distributions - cdf = lognorm.cdf(.99, *p.values) + cdf = lognorm.cdf(0.99, *p.values) assert cdf.shape == (self.nx, self.ny) - assert p.attrs['estimator'] == 'Maximum likelihood' + assert p.attrs["estimator"] == "Maximum likelihood" def test_fa(self): T = 10 - q = generic.fa(self.da, T, 'lognorm') + q = generic.fa(self.da, T, "lognorm") p0 = lognorm.fit(self.da.values[:, 0, 0]) - q0 = lognorm.ppf(1 - 1. / T, *p0) + q0 = lognorm.ppf(1 - 1.0 / T, *p0) np.testing.assert_array_equal(q[0, 0, 0], q0) -class TestSelectResampleOp(): - +class TestSelectResampleOp: def test_month(self, q_series): q = q_series(np.arange(1000)) - o = generic.select_resample_op(q, 'count', freq='YS', month=3) + o = generic.select_resample_op(q, "count", freq="YS", month=3) np.testing.assert_array_equal(o, 31) def test_season_default(self, q_series): # Will use freq='YS', so count J, F and D of each year. q = q_series(np.arange(1000)) - o = generic.select_resample_op(q, 'min', season='DJF') + o = generic.select_resample_op(q, "min", season="DJF") assert o[0] == 0 assert o[1] == 366 def test_season(self, q_series): q = q_series(np.arange(1000)) - o = generic.select_resample_op(q, 'count', freq='AS-DEC', season='DJF') + o = generic.select_resample_op(q, "count", freq="AS-DEC", season="DJF") assert o[0] == 31 + 29 diff --git a/tests/test_indices.py b/tests/test_indices.py index 5ae63fafe..a39f74021 100644 --- a/tests/test_indices.py +++ b/tests/test_indices.py @@ -1,7 +1,5 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- - - # Tests for `xclim` package. # # We want to tests multiple things here: @@ -15,8 +13,6 @@ # For correctness, I think it would be useful to use a small dataset and run the original ICCLIM indicators on it, # saving the results in a reference netcdf dataset. We could then compare the hailstorm output to this reference as # a first line of defense. - - # import cftime import calendar import os @@ -27,7 +23,10 @@ import xarray as xr import xclim.indices as xci -from xclim.testing.common import tas_series, tasmax_series, tasmin_series, pr_series +from xclim.testing.common import pr_series +from xclim.testing.common import tas_series +from xclim.testing.common import tasmax_series +from xclim.testing.common import tasmin_series from xclim.utils import percentile_doy xr.set_options(enable_cftimeindex=True) @@ -37,20 +36,20 @@ TASMIN_SERIES = tasmin_series PR_SERIES = pr_series TESTS_HOME = os.path.abspath(os.path.dirname(__file__)) -TESTS_DATA = os.path.join(TESTS_HOME, 'testdata') +TESTS_DATA = os.path.join(TESTS_HOME, "testdata") K2C = 273.15 # PLEASE MAINTAIN ALPHABETICAL ORDER -class TestBaseFlowIndex: +class TestBaseFlowIndex: def test_simple(self, q_series): a = np.zeros(365) + 10 a[10:17] = 1 q = q_series(a) out = xci.base_flow_index(q) - np.testing.assert_array_equal(out, 1. / a.mean()) + np.testing.assert_array_equal(out, 1.0 / a.mean()) class TestMaxNDayPrecipitationAmount: @@ -66,7 +65,7 @@ def test_single_max(self, pr_series): def test_sumlength_max(self, pr_series): a = pr_series(np.array([3, 4, 20, 20, 0, 6, 9, 25, 0, 0])) rxnday = xci.max_n_day_precipitation_amount(a, len(a)) - assert rxnday == a.sum('time') * 3600 * 24 + assert rxnday == a.sum("time") * 3600 * 24 assert rxnday.time.dt.year == 2000 # test whether non-unique maxes are resolved @@ -79,14 +78,21 @@ def test_multi_max(self, pr_series): class TestMax1DayPrecipitationAmount: - @staticmethod def time_series(values): - coords = pd.date_range('7/1/2000', periods=len(values), freq=pd.DateOffset(days=1)) - return xr.DataArray(values, coords=[coords, ], dims='time', - attrs={'standard_name': 'precipitation_flux', - 'cell_methods': 'time: sum (interval: 1 day)', - 'units': 'mm/day'}) + coords = pd.date_range( + "7/1/2000", periods=len(values), freq=pd.DateOffset(days=1) + ) + return xr.DataArray( + values, + coords=[coords], + dims="time", + attrs={ + "standard_name": "precipitation_flux", + "cell_methods": "time: sum (interval: 1 day)", + "units": "mm/day", + }, + ) # test max precip def test_single_max(self): @@ -113,22 +119,24 @@ def test_uniform_max(self): class TestColdSpellDurationIndex: - def test_simple(self, tasmin_series): i = 3650 - A = 10. - tn = np.zeros(i) + A * np.sin(np.arange(i) / 365. * 2 * np.pi) + .1 * np.random.rand(i) + A = 10.0 + tn = ( + np.zeros(i) + + A * np.sin(np.arange(i) / 365.0 * 2 * np.pi) + + 0.1 * np.random.rand(i) + ) tn[10:20] -= 2 tn = tasmin_series(tn) - tn10 = percentile_doy(tn, per=.1) + tn10 = percentile_doy(tn, per=0.1) - out = xci.cold_spell_duration_index(tn, tn10, freq='YS') + out = xci.cold_spell_duration_index(tn, tn10, freq="YS") assert out[0] == 10 - assert out.units == 'days' + assert out.units == "days" class TestColdSpellDays: - def test_simple(self, tas_series): a = np.zeros(365) a[10:20] -= 15 # 10 days @@ -136,20 +144,27 @@ def test_simple(self, tas_series): a[80:100] -= 30 # at the end and beginning da = tas_series(a + K2C) - out = xci.cold_spell_days(da, thresh='-10. C', freq='M') + out = xci.cold_spell_days(da, thresh="-10. C", freq="M") np.testing.assert_array_equal(out, [10, 0, 12, 8, 0, 0, 0, 0, 0, 0, 0, 0]) - assert out.units == 'days' + assert out.units == "days" class TestConsecutiveFrostDays: - @staticmethod def time_series(values): - coords = pd.date_range('7/1/2000', periods=len(values), freq=pd.DateOffset(days=1)) - return xr.DataArray(values, coords=[coords, ], dims='time', - attrs={'standard_name': 'air_temperature', - 'cell_methods': 'time: minimum within days', - 'units': 'K'}) + coords = pd.date_range( + "7/1/2000", periods=len(values), freq=pd.DateOffset(days=1) + ) + return xr.DataArray( + values, + coords=[coords], + dims="time", + attrs={ + "standard_name": "air_temperature", + "cell_methods": "time: minimum within days", + "units": "K", + }, + ) def test_one_freeze_day(self): a = self.time_series(np.array([3, 4, 5, -1, 3]) + K2C) @@ -169,20 +184,27 @@ def test_all_year_freeze(self): class TestCoolingDegreeDays: - @staticmethod def time_series(values): - coords = pd.date_range('7/1/2000', periods=len(values), freq=pd.DateOffset(days=1)) - return xr.DataArray(values, coords=[coords, ], dims='time', - attrs={'standard_name': 'air_temperature', - 'cell_methods': 'time: mean within days', - 'units': 'K'}) + coords = pd.date_range( + "7/1/2000", periods=len(values), freq=pd.DateOffset(days=1) + ) + return xr.DataArray( + values, + coords=[coords], + dims="time", + attrs={ + "standard_name": "air_temperature", + "cell_methods": "time: mean within days", + "units": "K", + }, + ) def test_no_cdd(self): a = self.time_series(np.array([10, 15, -5, 18]) + K2C) cdd = xci.cooling_degree_days(a) assert cdd == 0 - assert cdd.units == 'C days' + assert cdd.units == "C days" def test_cdd(self): a = self.time_series(np.array([20, 25, -15, 19]) + K2C) @@ -191,7 +213,6 @@ def test_cdd(self): class TestDailyFreezeThawCycles: - def test_simple(self, tasmin_series, tasmax_series): mn = np.zeros(365) mx = np.zeros(365) @@ -206,55 +227,52 @@ def test_simple(self, tasmin_series, tasmax_series): mn = tasmin_series(mn + K2C) mx = tasmax_series(mx + K2C) - out = xci.daily_freezethaw_cycles(mx, mn, 'M') + out = xci.daily_freezethaw_cycles(mx, mn, "M") np.testing.assert_array_equal(out[:2], [5, 1]) np.testing.assert_array_equal(out[2:], 0) class TestDailyPrIntensity: - def test_simple(self, pr_series): pr = pr_series(np.zeros(365)) - pr[3:8] += [.5, 1, 2, 3, 4] - out = xci.daily_pr_intensity(pr, thresh='1 kg/m**2/s') + pr[3:8] += [0.5, 1, 2, 3, 4] + out = xci.daily_pr_intensity(pr, thresh="1 kg/m**2/s") np.testing.assert_array_equal(out[0], 2.5 * 3600 * 24) def test_mm(self, pr_series): pr = pr_series(np.zeros(365)) - pr[3:8] += [.5, 1, 2, 3, 4] - pr.attrs['units'] = 'mm/d' - out = xci.daily_pr_intensity(pr, thresh='1 mm/day') + pr[3:8] += [0.5, 1, 2, 3, 4] + pr.attrs["units"] = "mm/d" + out = xci.daily_pr_intensity(pr, thresh="1 mm/day") np.testing.assert_array_almost_equal(out[0], 2.5) class TestFreshetStart: - def test_simple(self, tas_series): tg = np.zeros(365) - 1 w = 5 i = 10 - tg[i:i + w - 1] += 6 # too short + tg[i : i + w - 1] += 6 # too short i = 20 - tg[i:i + w] += 6 # ok + tg[i : i + w] += 6 # ok i = 30 - tg[i:i + w + 1] += 6 # Second valid condition, should be ignored. + tg[i : i + w + 1] += 6 # Second valid condition, should be ignored. - tg = tas_series(tg + K2C, start='1/1/2000') + tg = tas_series(tg + K2C, start="1/1/2000") out = xci.freshet_start(tg, window=w) - assert out[0] == tg.indexes['time'][20].dayofyear + assert out[0] == tg.indexes["time"][20].dayofyear def test_no_start(self, tas_series): tg = np.zeros(365) - 1 - tg = tas_series(tg, start='1/1/2000') + tg = tas_series(tg, start="1/1/2000") out = xci.freshet_start(tg) - np.testing.assert_equal(out, [np.nan, ]) + np.testing.assert_equal(out, [np.nan]) class TestGrowingDegreeDays: - def test_simple(self, tas_series): a = np.zeros(365) a[0] = 5 # default thresh at 4 @@ -268,19 +286,19 @@ def test_simple(self, tas_series): # generate 5 years of data a = np.zeros(366 * 2 + 365 * 3) - tas = tas_series(a, start='2000/1/1') + tas = tas_series(a, start="2000/1/1") # 2000 : no growing season # 2001 : growing season all year - d1 = '27-12-2000' - d2 = '31-12-2001' + d1 = "27-12-2000" + d2 = "31-12-2001" buffer = tas.sel(time=slice(d1, d2)) tas = tas.where(~tas.time.isin(buffer.time), 280) # 2002 : growing season in June only - d1 = '6-1-2002' - d2 = '6-10-2002' + d1 = "6-1-2002" + d2 = "6-10-2002" buffer = tas.sel(time=slice(d1, d2)) tas = tas.where(~tas.time.isin(buffer.time), 280) # @@ -290,14 +308,14 @@ def test_simple(self, tas_series): # of growing season to be equal or later than July 1st. # growing season in Aug only - d1 = '8-1-2003' - d2 = '8-10-2003' + d1 = "8-1-2003" + d2 = "8-10-2003" buffer = tas.sel(time=slice(d1, d2)) tas = tas.where(~tas.time.isin(buffer.time), 280) # growing season from June to end of July - d1 = '6-1-2004' - d2 = '7-31-2004' + d1 = "6-1-2004" + d2 = "7-31-2004" buffer = tas.sel(time=slice(d1, d2)) tas = tas.where(~tas.time.isin(buffer.time), 280) @@ -308,7 +326,6 @@ def test_simple(self, tas_series): class TestHeatingDegreeDays: - def test_simple(self, tas_series): a = np.zeros(365) + 17 a[:7] += [-3, -2, -1, 0, 1, 2, 3] @@ -319,7 +336,6 @@ def test_simple(self, tas_series): class TestHeatWaveIndex: - def test_simple(self, tasmax_series): a = np.zeros(365) a[10:20] += 30 # 10 days @@ -327,88 +343,91 @@ def test_simple(self, tasmax_series): a[80:100] += 30 # at the end and beginning da = tasmax_series(a + K2C) - out = xci.heat_wave_index(da, thresh='25 C', freq='M') + out = xci.heat_wave_index(da, thresh="25 C", freq="M") np.testing.assert_array_equal(out, [10, 0, 12, 8, 0, 0, 0, 0, 0, 0, 0, 0]) class TestHeatWaveFrequency: - def test_1d(self, tasmax_series, tasmin_series): tn = tasmin_series(np.asarray([20, 23, 23, 23, 23, 22, 23, 23, 23, 23]) + K2C) tx = tasmax_series(np.asarray([29, 31, 31, 31, 29, 31, 31, 31, 31, 31]) + K2C) # some hw - hwf = xci.heat_wave_frequency(tn, tx, thresh_tasmin='22 C', - thresh_tasmax='30 C') + hwf = xci.heat_wave_frequency( + tn, tx, thresh_tasmin="22 C", thresh_tasmax="30 C" + ) np.testing.assert_allclose(hwf.values, 2) - hwf = xci.heat_wave_frequency(tn, tx, thresh_tasmin='22 C', - thresh_tasmax='30 C', window=4) + hwf = xci.heat_wave_frequency( + tn, tx, thresh_tasmin="22 C", thresh_tasmax="30 C", window=4 + ) np.testing.assert_allclose(hwf.values, 1) # one long hw - hwf = xci.heat_wave_frequency(tn, tx, thresh_tasmin='10 C', - thresh_tasmax='10 C') + hwf = xci.heat_wave_frequency( + tn, tx, thresh_tasmin="10 C", thresh_tasmax="10 C" + ) np.testing.assert_allclose(hwf.values, 1) # no hw - hwf = xci.heat_wave_frequency(tn, tx, thresh_tasmin='40 C', - thresh_tasmax='40 C') + hwf = xci.heat_wave_frequency( + tn, tx, thresh_tasmin="40 C", thresh_tasmax="40 C" + ) np.testing.assert_allclose(hwf.values, 0) class TestHeatWaveMaxLength: - def test_1d(self, tasmax_series, tasmin_series): tn = tasmin_series(np.asarray([20, 23, 23, 23, 23, 22, 23, 23, 23, 23]) + K2C) tx = tasmax_series(np.asarray([29, 31, 31, 31, 29, 31, 31, 31, 31, 31]) + K2C) # some hw - hwml = xci.heat_wave_max_length(tn, tx, thresh_tasmin='22 C', - thresh_tasmax='30 C') + hwml = xci.heat_wave_max_length( + tn, tx, thresh_tasmin="22 C", thresh_tasmax="30 C" + ) np.testing.assert_allclose(hwml.values, 4) # one long hw - hwml = xci.heat_wave_max_length(tn, tx, thresh_tasmin='10 C', - thresh_tasmax='10 C') + hwml = xci.heat_wave_max_length( + tn, tx, thresh_tasmin="10 C", thresh_tasmax="10 C" + ) np.testing.assert_allclose(hwml.values, 10) # no hw - hwml = xci.heat_wave_max_length(tn, tx, thresh_tasmin='40 C', - thresh_tasmax='40 C') + hwml = xci.heat_wave_max_length( + tn, tx, thresh_tasmin="40 C", thresh_tasmax="40 C" + ) np.testing.assert_allclose(hwml.values, 0) - hwml = xci.heat_wave_max_length(tn, tx, thresh_tasmin='22 C', - thresh_tasmax='30 C', window=5) + hwml = xci.heat_wave_max_length( + tn, tx, thresh_tasmin="22 C", thresh_tasmax="30 C", window=5 + ) np.testing.assert_allclose(hwml.values, 0) class TestTnDaysBelow: - def test_simple(self, tasmin_series): a = np.zeros(365) a[:6] -= [27, 28, 29, 30, 31, 32] # 2 above 30 mx = tasmin_series(a + K2C) - out = xci.tn_days_below(mx, thresh='-10 C') + out = xci.tn_days_below(mx, thresh="-10 C") np.testing.assert_array_equal(out[:1], [6]) np.testing.assert_array_equal(out[1:], [0]) - out = xci.tn_days_below(mx, thresh='-30 C') + out = xci.tn_days_below(mx, thresh="-30 C") np.testing.assert_array_equal(out[:1], [2]) np.testing.assert_array_equal(out[1:], [0]) class TestTxDaysAbove: - def test_simple(self, tasmax_series): a = np.zeros(365) a[:6] += [27, 28, 29, 30, 31, 32] # 2 above 30 mx = tasmax_series(a + K2C) - out = xci.tx_days_above(mx, thresh='30 C') + out = xci.tx_days_above(mx, thresh="30 C") np.testing.assert_array_equal(out[:1], [2]) np.testing.assert_array_equal(out[1:], [0]) class TestLiquidPrecipitationRatio: - def test_simple(self, pr_series, tas_series): pr = np.zeros(100) pr[10:20] = 1 @@ -419,31 +438,32 @@ def test_simple(self, pr_series, tas_series): tas[14:] += 10 tas = tas_series(tas + K2C) - out = xci.liquid_precip_ratio(pr, tas=tas, freq='M') - np.testing.assert_almost_equal(out[:1], [.6, ]) + out = xci.liquid_precip_ratio(pr, tas=tas, freq="M") + np.testing.assert_almost_equal(out[:1], [0.6]) class TestMaximumConsecutiveDryDays: - def test_simple(self, pr_series): a = np.zeros(365) + 10 a[5:15] = 0 pr = pr_series(a) - out = xci.maximum_consecutive_dry_days(pr, freq='M') + out = xci.maximum_consecutive_dry_days(pr, freq="M") assert out[0] == 10 def test_run_start_at_0(self, pr_series): a = np.zeros(365) + 10 a[:10] = 0 pr = pr_series(a) - out = xci.maximum_consecutive_dry_days(pr, freq='M') + out = xci.maximum_consecutive_dry_days(pr, freq="M") assert out[0] == 10 class TestPrecipAccumulation: # build test data for different calendar - time_std = pd.date_range('2000-01-01', '2010-12-31', freq='D') - da_std = xr.DataArray(time_std.year, coords=[time_std], dims='time', attrs={'units': 'mm d-1'}) + time_std = pd.date_range("2000-01-01", "2010-12-31", freq="D") + da_std = xr.DataArray( + time_std.year, coords=[time_std], dims="time", attrs={"units": "mm d-1"} + ) # calendar 365_day and 360_day not tested for now since xarray.resample # does not support other calendars than standard @@ -459,18 +479,19 @@ def test_simple(self, pr_series): pr[5:10] = 1 pr = pr_series(pr) - out = xci.precip_accumulation(pr, freq='M') + out = xci.precip_accumulation(pr, freq="M") np.testing.assert_array_equal(out[0], 5 * 3600 * 24) def test_yearly(self): da_std = self.da_std out_std = xci.precip_accumulation(da_std) - target = [(365 + calendar.isleap(y)) * y for y in np.unique(da_std.time.dt.year)] + target = [ + (365 + calendar.isleap(y)) * y for y in np.unique(da_std.time.dt.year) + ] np.testing.assert_allclose(out_std.values, target) class TestRainOnFrozenGround: - def test_simple(self, tas_series, pr_series): tas = np.zeros(30) - 1 pr = np.zeros(30) @@ -481,7 +502,7 @@ def test_simple(self, tas_series, pr_series): tas = tas_series(tas + K2C) pr = pr_series(pr / 3600 / 24) - out = xci.rain_on_frozen_ground_days(pr, tas, freq='MS') + out = xci.rain_on_frozen_ground_days(pr, tas, freq="MS") assert out[0] == 1 def test_small_rain(self, tas_series, pr_series): @@ -489,12 +510,12 @@ def test_small_rain(self, tas_series, pr_series): pr = np.zeros(30) tas[10] += 5 - pr[10] += .5 + pr[10] += 0.5 tas = tas_series(tas + K2C) pr = pr_series(pr / 3600 / 24) - out = xci.rain_on_frozen_ground_days(pr, tas, freq='MS') + out = xci.rain_on_frozen_ground_days(pr, tas, freq="MS") assert out[0] == 0 def test_consecutive_rain(self, tas_series, pr_series): @@ -507,79 +528,83 @@ def test_consecutive_rain(self, tas_series, pr_series): tas = tas_series(tas + K2C) pr = pr_series(pr) - out = xci.rain_on_frozen_ground_days(pr, tas, freq='MS') + out = xci.rain_on_frozen_ground_days(pr, tas, freq="MS") assert out[0] == 1 class TestTGXN10p: - def test_tg10p_simple(self, tas_series): i = 366 tas = np.array(range(i)) - tas = tas_series(tas, start='1/1/2000') - t10 = percentile_doy(tas, per=.1) + tas = tas_series(tas, start="1/1/2000") + t10 = percentile_doy(tas, per=0.1) # create cold spell in june tas[175:180] = 1 - out = xci.tg10p(tas, t10, freq='MS') + out = xci.tg10p(tas, t10, freq="MS") assert out[0] == 1 assert out[5] == 5 def test_tx10p_simple(self, tasmax_series): i = 366 tas = np.array(range(i)) - tas = tasmax_series(tas, start='1/1/2000') - t10 = percentile_doy(tas, per=.1) + tas = tasmax_series(tas, start="1/1/2000") + t10 = percentile_doy(tas, per=0.1) # create cold spell in june tas[175:180] = 1 - out = xci.tx10p(tas, t10, freq='MS') + out = xci.tx10p(tas, t10, freq="MS") assert out[0] == 1 assert out[5] == 5 def test_tn10p_simple(self, tas_series): i = 366 tas = np.array(range(i)) - tas = tas_series(tas, start='1/1/2000') - t10 = percentile_doy(tas, per=.1) + tas = tas_series(tas, start="1/1/2000") + t10 = percentile_doy(tas, per=0.1) # create cold spell in june tas[175:180] = 1 - out = xci.tn10p(tas, t10, freq='MS') + out = xci.tn10p(tas, t10, freq="MS") assert out[0] == 1 assert out[5] == 5 def test_doy_interpolation(self): - pytest.importorskip('xarray', '0.11.4') + pytest.importorskip("xarray", "0.11.4") # Just a smoke test - fn_clim = os.path.join(TESTS_DATA, 'CanESM2_365day', - 'tasmin_day_CanESM2_rcp85_r1i1p1_na10kgrid_qm-moving-50bins-detrend_2095.nc') - fn = os.path.join(TESTS_DATA, 'HadGEM2-CC_360day', - 'tasmin_day_HadGEM2-CC_rcp85_r1i1p1_na10kgrid_qm-moving-50bins-detrend_2095.nc') + fn_clim = os.path.join( + TESTS_DATA, + "CanESM2_365day", + "tasmin_day_CanESM2_rcp85_r1i1p1_na10kgrid_qm-moving-50bins-detrend_2095.nc", + ) + fn = os.path.join( + TESTS_DATA, + "HadGEM2-CC_360day", + "tasmin_day_HadGEM2-CC_rcp85_r1i1p1_na10kgrid_qm-moving-50bins-detrend_2095.nc", + ) with xr.open_dataset(fn_clim) as ds: - t10 = percentile_doy(ds.tasmin.isel(lat=0, lon=0), per=.1) + t10 = percentile_doy(ds.tasmin.isel(lat=0, lon=0), per=0.1) with xr.open_dataset(fn) as ds: - xci.tn10p(ds.tasmin.isel(lat=0, lon=0), t10, freq='MS') + xci.tn10p(ds.tasmin.isel(lat=0, lon=0), t10, freq="MS") class TestTGXN90p: - def test_tg90p_simple(self, tas_series): i = 366 tas = np.array(range(i)) - tas = tas_series(tas, start='1/1/2000') - t90 = percentile_doy(tas, per=.1) + tas = tas_series(tas, start="1/1/2000") + t90 = percentile_doy(tas, per=0.1) # create cold spell in june tas[175:180] = 1 - out = xci.tg90p(tas, t90, freq='MS') + out = xci.tg90p(tas, t90, freq="MS") assert out[0] == 30 assert out[1] == 29 assert out[5] == 25 @@ -587,13 +612,13 @@ def test_tg90p_simple(self, tas_series): def test_tx90p_simple(self, tasmax_series): i = 366 tas = np.array(range(i)) - tas = tasmax_series(tas, start='1/1/2000') - t90 = percentile_doy(tas, per=.1) + tas = tasmax_series(tas, start="1/1/2000") + t90 = percentile_doy(tas, per=0.1) # create cold spell in june tas[175:180] = 1 - out = xci.tx90p(tas, t90, freq='MS') + out = xci.tx90p(tas, t90, freq="MS") assert out[0] == 30 assert out[1] == 29 assert out[5] == 25 @@ -601,71 +626,91 @@ def test_tx90p_simple(self, tasmax_series): def test_tn90p_simple(self, tasmin_series): i = 366 tas = np.array(range(i)) - tas = tasmin_series(tas, start='1/1/2000') - t90 = percentile_doy(tas, per=.1) + tas = tasmin_series(tas, start="1/1/2000") + t90 = percentile_doy(tas, per=0.1) # create cold spell in june tas[175:180] = 1 - out = xci.tn90p(tas, t90, freq='MS') + out = xci.tn90p(tas, t90, freq="MS") assert out[0] == 30 assert out[1] == 29 assert out[5] == 25 class TestTxMin: - @staticmethod def time_series(values): - coords = pd.date_range('7/1/2000', periods=len(values), freq=pd.DateOffset(days=1)) - return xr.DataArray(values, coords=[coords, ], dims='time', - attrs={'standard_name': 'air_temperature', - 'cell_methods': 'time: maximum within days', - 'units': 'K'}) + coords = pd.date_range( + "7/1/2000", periods=len(values), freq=pd.DateOffset(days=1) + ) + return xr.DataArray( + values, + coords=[coords], + dims="time", + attrs={ + "standard_name": "air_temperature", + "cell_methods": "time: maximum within days", + "units": "K", + }, + ) class TestTxMean: - @staticmethod def time_series(values): - coords = pd.date_range('7/1/2000', periods=len(values), freq=pd.DateOffset(days=1)) - return xr.DataArray(values, coords=[coords, ], dims='time', - attrs={'standard_name': 'air_temperature', - 'cell_methods': 'time: maximum within days', - 'units': 'K'}) + coords = pd.date_range( + "7/1/2000", periods=len(values), freq=pd.DateOffset(days=1) + ) + return xr.DataArray( + values, + coords=[coords], + dims="time", + attrs={ + "standard_name": "air_temperature", + "cell_methods": "time: maximum within days", + "units": "K", + }, + ) def test_attrs(self): a = self.time_series(np.array([320, 321, 322, 323, 324])) - txm = xci.tx_mean(a, freq='YS') + txm = xci.tx_mean(a, freq="YS") assert txm == 322 - assert txm.units == 'K' + assert txm.units == "K" a = self.time_series(np.array([20, 21, 22, 23, 24])) - a.attrs['units'] = 'C' - txm = xci.tx_mean(a, freq='YS') + a.attrs["units"] = "C" + txm = xci.tx_mean(a, freq="YS") assert txm == 22 - assert txm.units == 'C' + assert txm.units == "C" class TestTxMax: - @staticmethod def time_series(values): - coords = pd.date_range('7/1/2000', periods=len(values), freq=pd.DateOffset(days=1)) - return xr.DataArray(values, coords=[coords, ], dims='time', - attrs={'standard_name': 'air_temperature', - 'cell_methods': 'time: maximum within days', - 'units': 'K'}) + coords = pd.date_range( + "7/1/2000", periods=len(values), freq=pd.DateOffset(days=1) + ) + return xr.DataArray( + values, + coords=[coords], + dims="time", + attrs={ + "standard_name": "air_temperature", + "cell_methods": "time: maximum within days", + "units": "K", + }, + ) def test_simple(self): a = self.time_series(np.array([20, 25, -15, 19])) - txm = xci.tx_max(a, freq='YS') + txm = xci.tx_max(a, freq="YS") assert txm == 25 class TestTgMaxTgMinIndices: - @staticmethod def random_tmax_tmin_setup(length, tasmax_series, tasmin_series): max_values = np.random.uniform(-20, 40, length) @@ -707,7 +752,9 @@ def test_static_daily_temperature_range(self, tasmax_series, tasmin_series): # np.testing.assert_allclose(vdtr.mean(), 20, atol=10) # np.testing.assert_array_less(-vdtr, [0, 0, 0, 0]) - def test_static_variable_daily_temperature_range(self, tasmax_series, tasmin_series): + def test_static_variable_daily_temperature_range( + self, tasmax_series, tasmin_series + ): tasmax, tasmin = self.static_tmax_tmin_setup(tasmax_series, tasmin_series) dtr = xci.daily_temperature_range_variability(tasmax, tasmin, freq="YS") @@ -721,7 +768,10 @@ def test_static_extreme_temperature_range(self, tasmax_series, tasmin_series): def test_uniform_freeze_thaw_cycles(self, tasmax_series, tasmin_series): temp_values = np.zeros(365) - tasmax, tasmin = tasmax_series(temp_values + 5 + K2C), tasmin_series(temp_values - 5 + K2C) + tasmax, tasmin = ( + tasmax_series(temp_values + 5 + K2C), + tasmin_series(temp_values - 5 + K2C), + ) ft = xci.daily_freezethaw_cycles(tasmax, tasmin, freq="YS") np.testing.assert_array_equal([np.sum(ft)], [365]) @@ -746,84 +796,82 @@ def test_static_freeze_thaw_cycles(self, tasmax_series, tasmin_series): class TestWarmDayFrequency: - def test_1d(self, tasmax_series): a = np.zeros(35) a[25:] = 31 da = tasmax_series(a + K2C) - wdf = xci.warm_day_frequency(da, freq='MS') + wdf = xci.warm_day_frequency(da, freq="MS") np.testing.assert_allclose(wdf.values, [6, 4]) - wdf = xci.warm_day_frequency(da, freq='YS') + wdf = xci.warm_day_frequency(da, freq="YS") np.testing.assert_allclose(wdf.values, [10]) - wdf = xci.warm_day_frequency(da, thresh='-1 C') + wdf = xci.warm_day_frequency(da, thresh="-1 C") np.testing.assert_allclose(wdf.values, [35]) - wdf = xci.warm_day_frequency(da, thresh='50 C') + wdf = xci.warm_day_frequency(da, thresh="50 C") np.testing.assert_allclose(wdf.values, [0]) class TestWarmNightFrequency: - def test_1d(self, tasmin_series): a = np.zeros(35) a[25:] = 23 da = tasmin_series(a + K2C) - wnf = xci.warm_night_frequency(da, freq='MS') + wnf = xci.warm_night_frequency(da, freq="MS") np.testing.assert_allclose(wnf.values, [6, 4]) - wnf = xci.warm_night_frequency(da, freq='YS') + wnf = xci.warm_night_frequency(da, freq="YS") np.testing.assert_allclose(wnf.values, [10]) - wnf = xci.warm_night_frequency(da, thresh='-1 C') + wnf = xci.warm_night_frequency(da, thresh="-1 C") np.testing.assert_allclose(wnf.values, [35]) - wnf = xci.warm_night_frequency(da, thresh='50 C') + wnf = xci.warm_night_frequency(da, thresh="50 C") np.testing.assert_allclose(wnf.values, [0]) class TestTxTnDaysAbove: - def test_1d(self, tasmax_series, tasmin_series): tn = tasmin_series(np.asarray([20, 23, 23, 23, 23, 22, 23, 23, 23, 23]) + K2C) tx = tasmax_series(np.asarray([29, 31, 31, 31, 29, 31, 31, 31, 31, 31]) + K2C) wmmtf = xci.tx_tn_days_above(tn, tx) np.testing.assert_allclose(wmmtf.values, [7]) - wmmtf = xci.tx_tn_days_above(tn, tx, thresh_tasmax='50 C') + wmmtf = xci.tx_tn_days_above(tn, tx, thresh_tasmax="50 C") np.testing.assert_allclose(wmmtf.values, [0]) - wmmtf = xci.tx_tn_days_above(tn, tx, thresh_tasmax='0 C', - thresh_tasmin='0 C') + wmmtf = xci.tx_tn_days_above(tn, tx, thresh_tasmax="0 C", thresh_tasmin="0 C") np.testing.assert_allclose(wmmtf.values, [10]) class TestWarmSpellDurationIndex: - def test_simple(self, tasmax_series): i = 3650 - A = 10. - tx = np.zeros(i) + A * np.sin(np.arange(i) / 365. * 2 * np.pi) + .1 * np.random.rand(i) + A = 10.0 + tx = ( + np.zeros(i) + + A * np.sin(np.arange(i) / 365.0 * 2 * np.pi) + + 0.1 * np.random.rand(i) + ) tx[10:20] += 2 tx = tasmax_series(tx) - tx90 = percentile_doy(tx, per=.9) + tx90 = percentile_doy(tx, per=0.9) - out = xci.warm_spell_duration_index(tx, tx90, freq='YS') + out = xci.warm_spell_duration_index(tx, tx90, freq="YS") assert out[0] == 10 class TestWinterRainRatio: - def test_simple(self, pr_series, tas_series): pr = np.ones(450) - pr = pr_series(pr, start='12/1/2000') + pr = pr_series(pr, start="12/1/2000") tas = np.zeros(450) - 1 tas[10:20] += 10 - tas = tas_series(tas + K2C, start='12/1/2000') + tas = tas_series(tas + K2C, start="12/1/2000") out = xci.winter_rain_ratio(pr, tas=tas) - np.testing.assert_almost_equal(out, [10. / (31 + 31 + 28), 0]) + np.testing.assert_almost_equal(out, [10.0 / (31 + 31 + 28), 0]) # I'd like to parametrize some of these tests so we don't have to write individual tests for each indicator. class TestTG: def test_cmip3(self, cmip3_day_tas): - pytest.importorskip('xarray', '0.11.4') + pytest.importorskip("xarray", "0.11.4") xci.tg_mean(cmip3_day_tas) def compare_against_icclim(self, cmip3_day_tas): @@ -833,7 +881,9 @@ def compare_against_icclim(self, cmip3_day_tas): @pytest.fixture(scope="session") def cmip3_day_tas(): # xr.set_options(enable_cftimeindex=False) - ds = xr.open_dataset(os.path.join(TESTS_DATA, 'cmip3', 'tas.sresb1.giss_model_e_r.run1.atm.da.nc')) + ds = xr.open_dataset( + os.path.join(TESTS_DATA, "cmip3", "tas.sresb1.giss_model_e_r.run1.atm.da.nc") + ) yield ds.tas ds.close() @@ -854,5 +904,6 @@ def test_content(response): # from bs4 import BeautifulSoup # assert 'GitHub' in BeautifulSoup(response.content).title.string + # x = Test_frost_days() # print('done') diff --git a/tests/test_modules.py b/tests/test_modules.py index efe369276..97bd844f8 100644 --- a/tests/test_modules.py +++ b/tests/test_modules.py @@ -1,36 +1,42 @@ import pytest -from xclim import build_module +from xclim import build_module -class TestBuildModules(): +class TestBuildModules: def test_nonexistent_process_build_failure(self): name = "" - objs = {'k1': 'v1', 'k2': 'v2'} + objs = {"k1": "v1", "k2": "v2"} with pytest.raises(AttributeError): - build_module(name, objs, mode='warn') + build_module(name, objs, mode="warn") def test_quiet_build_failure(self): name = None objs = {} with pytest.raises(TypeError): - build_module(name, objs, mode='ignore') + build_module(name, objs, mode="ignore") + + def test_configured_build_failure(self): + name = "" + objs = {"k1": None, "k2": None} + with pytest.raises(AttributeError): + build_module(name, objs, mode="bananas") def test_loud_build_failure(self): name = "" - objs = {'k1': None, 'k2': None} + objs = {"k1": None, "k2": None} with pytest.warns(Warning): - build_module(name, objs, mode='warn') + build_module(name, objs, mode="warn") def test_raise_build_failure(self): name = "" - objs = {'k1': None, 'k2': None} + objs = {"k1": None, "k2": None} with pytest.raises(NotImplementedError): - build_module(name, objs, mode='raise') - + build_module(name, objs, mode="raise") -class TestICCLIM(): +class TestICCLIM: def test_exists(self): from xclim import icclim - assert getattr(icclim, 'TG', None) is not None + + assert getattr(icclim, "TG", None) is not None diff --git a/tests/test_precip.py b/tests/test_precip.py index 3e091e5a9..e9e9f258c 100644 --- a/tests/test_precip.py +++ b/tests/test_precip.py @@ -5,23 +5,28 @@ import pandas as pd import pytest import xarray as xr + import xclim.atmos as atmos TESTS_HOME = os.path.abspath(os.path.dirname(__file__)) -TESTS_DATA = os.path.join(TESTS_HOME, 'testdata') +TESTS_DATA = os.path.join(TESTS_HOME, "testdata") K2C = 273.15 -class TestRainOnFrozenGround(): - nc_pr = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_pr_1990.nc') - nc_tasmax = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmax_1990.nc') - nc_tasmin = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmin_1990.nc') +class TestRainOnFrozenGround: + nc_pr = os.path.join(TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_pr_1990.nc") + nc_tasmax = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmax_1990.nc" + ) + nc_tasmin = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmin_1990.nc" + ) def test_3d_data_with_nans(self): pr = xr.open_dataset(self.nc_pr).pr prMM = pr.copy() - prMM.values *= 86400. - prMM.attrs['units'] = 'mm/day' + prMM.values *= 86400.0 + prMM.attrs["units"] = "mm/day" tasmax = xr.open_dataset(self.nc_tasmax).tasmax tasmin = xr.open_dataset(self.nc_tasmin).tasmin @@ -29,18 +34,18 @@ def test_3d_data_with_nans(self): tas.attrs = tasmax.attrs tasC = tas.copy() tasC.values -= K2C - tasC.attrs['units'] = 'C' + tasC.attrs["units"] = "C" prMM.values[10, 1, 0] = np.nan pr.values[10, 1, 0] = np.nan - out1 = atmos.rain_on_frozen_ground_days(pr, tas, freq='MS') - out2 = atmos.rain_on_frozen_ground_days(prMM, tas, freq='MS') - out3 = atmos.rain_on_frozen_ground_days(prMM, tasC, freq='MS') - out4 = atmos.rain_on_frozen_ground_days(pr, tasC, freq='MS') - pr.attrs['units'] = 'kg m-2 s-1' - out5 = atmos.rain_on_frozen_ground_days(pr, tas, freq='MS') - out6 = atmos.rain_on_frozen_ground_days(pr, tasC, freq='MS') + out1 = atmos.rain_on_frozen_ground_days(pr, tas, freq="MS") + out2 = atmos.rain_on_frozen_ground_days(prMM, tas, freq="MS") + out3 = atmos.rain_on_frozen_ground_days(prMM, tasC, freq="MS") + out4 = atmos.rain_on_frozen_ground_days(pr, tasC, freq="MS") + pr.attrs["units"] = "kg m-2 s-1" + out5 = atmos.rain_on_frozen_ground_days(pr, tas, freq="MS") + out6 = atmos.rain_on_frozen_ground_days(pr, tasC, freq="MS") np.testing.assert_array_equal(out1, out2) np.testing.assert_array_equal(out1, out3) np.testing.assert_array_equal(out1, out4) @@ -59,31 +64,31 @@ def test_3d_data_with_nans(self): tas1[10] += 5 tas1[20] += 5 - rfrz = atmos.rain_on_frozen_ground_days(pr1, tas1, freq='MS') + rfrz = atmos.rain_on_frozen_ground_days(pr1, tas1, freq="MS") np.testing.assert_array_equal(rfrz, 2) -class TestPrecipAccumulation(): +class TestPrecipAccumulation: # TODO: replace by fixture - nc_file = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_pr_1990.nc') + nc_file = os.path.join(TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_pr_1990.nc") def test_3d_data_with_nans(self): # test with 3d data pr = xr.open_dataset(self.nc_file).pr # mm/s prMM = xr.open_dataset(self.nc_file).pr prMM *= 86400 - prMM.attrs['units'] = 'mm/day' + prMM.attrs["units"] = "mm/day" # put a nan somewhere prMM.values[10, 1, 0] = np.nan pr.values[10, 1, 0] = np.nan - out1 = atmos.precip_accumulation(pr, freq='MS') - out2 = atmos.precip_accumulation(prMM, freq='MS') + out1 = atmos.precip_accumulation(pr, freq="MS") + out2 = atmos.precip_accumulation(prMM, freq="MS") # test kg m-2 s-1 - pr.attrs['units'] = 'kg m-2 s-1' - out3 = atmos.precip_accumulation(pr, freq='MS') + pr.attrs["units"] = "kg m-2 s-1" + out3 = atmos.precip_accumulation(pr, freq="MS") np.testing.assert_array_almost_equal(out1, out2, 3) np.testing.assert_array_almost_equal(out1, out3) @@ -100,26 +105,26 @@ def test_3d_data_with_nans(self): assert np.isnan(out1.values[0, -1, -1]) -class TestWetDays(): +class TestWetDays: # TODO: replace by fixture - nc_file = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_pr_1990.nc') + nc_file = os.path.join(TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_pr_1990.nc") def test_3d_data_with_nans(self): # test with 3d data pr = xr.open_dataset(self.nc_file).pr prMM = xr.open_dataset(self.nc_file).pr - prMM.values *= 86400. - prMM.attrs['units'] = 'mm/day' + prMM.values *= 86400.0 + prMM.attrs["units"] = "mm/day" # put a nan somewhere prMM.values[10, 1, 0] = np.nan pr.values[10, 1, 0] = np.nan - pr_min = '5 mm/d' - out1 = atmos.wetdays(pr, thresh=pr_min, freq='MS') - out2 = atmos.wetdays(prMM, thresh=pr_min, freq='MS') + pr_min = "5 mm/d" + out1 = atmos.wetdays(pr, thresh=pr_min, freq="MS") + out2 = atmos.wetdays(prMM, thresh=pr_min, freq="MS") # test kg m-2 s-1 - pr.attrs['units'] = 'kg m-2 s-1' - out3 = atmos.wetdays(pr, thresh=pr_min, freq='MS') + pr.attrs["units"] = "kg m-2 s-1" + out3 = atmos.wetdays(pr, thresh=pr_min, freq="MS") np.testing.assert_array_equal(out1, out2) np.testing.assert_array_equal(out1, out3) @@ -127,7 +132,7 @@ def test_3d_data_with_nans(self): # check some vector with and without a nan x1 = prMM[:31, 0, 0].values - wd1 = (x1 >= int(pr_min.split(' ')[0])).sum() + wd1 = (x1 >= int(pr_min.split(" ")[0])).sum() assert wd1 == out1.values[0, 0, 0] @@ -138,38 +143,38 @@ def test_3d_data_with_nans(self): # assert (np.isnan(wds.values[0, -1, -1])) -class TestDailyIntensity(): +class TestDailyIntensity: # testing of wet_day and daily_pr_intensity, both are related - nc_file = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_pr_1990.nc') + nc_file = os.path.join(TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_pr_1990.nc") def test_3d_data_with_nans(self): # test with 3d data pr = xr.open_dataset(self.nc_file).pr prMM = xr.open_dataset(self.nc_file).pr - prMM.values *= 86400. - prMM.attrs['units'] = 'mm/day' + prMM.values *= 86400.0 + prMM.attrs["units"] = "mm/day" # put a nan somewhere prMM.values[10, 1, 0] = np.nan pr.values[10, 1, 0] = np.nan # compute with both skipna options - pr_min = '2 mm/d' + pr_min = "2 mm/d" # dis = daily_pr_intensity(pr, pr_min=pr_min, freq='MS', skipna=True) - out1 = atmos.daily_pr_intensity(pr, thresh=pr_min, freq='MS') - out2 = atmos.daily_pr_intensity(prMM, thresh=pr_min, freq='MS') + out1 = atmos.daily_pr_intensity(pr, thresh=pr_min, freq="MS") + out2 = atmos.daily_pr_intensity(prMM, thresh=pr_min, freq="MS") # test kg m-2 s-1 - pr.attrs['units'] = 'kg m-2 s-1' - out3 = atmos.daily_pr_intensity(pr, thresh=pr_min, freq='MS') + pr.attrs["units"] = "kg m-2 s-1" + out3 = atmos.daily_pr_intensity(pr, thresh=pr_min, freq="MS") np.testing.assert_array_almost_equal(out1, out2, 3) np.testing.assert_array_almost_equal(out1, out3, 3) x1 = prMM[:31, 0, 0].values - di1 = x1[x1 >= int(pr_min.split(' ')[0])].mean() + di1 = x1[x1 >= int(pr_min.split(" ")[0])].mean() # buffer = np.ma.masked_invalid(x2) # di2 = buffer[buffer >= pr_min].mean() @@ -181,27 +186,27 @@ def test_3d_data_with_nans(self): # assert (np.isnan(dis.values[0, -1, -1])) -class TestMax1Day(): +class TestMax1Day: # testing of wet_day and daily_pr_intensity, both are related - nc_file = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_pr_1990.nc') + nc_file = os.path.join(TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_pr_1990.nc") def test_3d_data_with_nans(self): # test with 3d data pr = xr.open_dataset(self.nc_file).pr prMM = xr.open_dataset(self.nc_file).pr - prMM.values *= 86400. - prMM.attrs['units'] = 'mm/day' + prMM.values *= 86400.0 + prMM.attrs["units"] = "mm/day" # put a nan somewhere prMM.values[10, 1, 0] = np.nan pr.values[10, 1, 0] = np.nan - out1 = atmos.max_1day_precipitation_amount(pr, freq='MS') - out2 = atmos.max_1day_precipitation_amount(prMM, freq='MS') + out1 = atmos.max_1day_precipitation_amount(pr, freq="MS") + out2 = atmos.max_1day_precipitation_amount(prMM, freq="MS") # test kg m-2 s-1 - pr.attrs['units'] = 'kg m-2 s-1' - out3 = atmos.max_1day_precipitation_amount(pr, freq='MS') + pr.attrs["units"] = "kg m-2 s-1" + out3 = atmos.max_1day_precipitation_amount(pr, freq="MS") np.testing.assert_array_almost_equal(out1, out2, 3) np.testing.assert_array_almost_equal(out1, out3, 3) @@ -217,33 +222,33 @@ def test_3d_data_with_nans(self): # assert (np.isnan(dis.values[0, -1, -1])) -class TestMaxNDay(): +class TestMaxNDay: # testing of wet_day and daily_pr_intensity, both are related - nc_file = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_pr_1990.nc') + nc_file = os.path.join(TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_pr_1990.nc") def test_3d_data_with_nans(self): # test with 3d data pr = xr.open_dataset(self.nc_file).pr prMM = xr.open_dataset(self.nc_file).pr - prMM.values *= 86400. - prMM.attrs['units'] = 'mm/day' + prMM.values *= 86400.0 + prMM.attrs["units"] = "mm/day" # put a nan somewhere prMM.values[10, 1, 0] = np.nan pr.values[10, 1, 0] = np.nan wind = 3 - out1 = atmos.max_n_day_precipitation_amount(pr, window=wind, freq='MS') - out2 = atmos.max_n_day_precipitation_amount(prMM, window=wind, freq='MS') + out1 = atmos.max_n_day_precipitation_amount(pr, window=wind, freq="MS") + out2 = atmos.max_n_day_precipitation_amount(prMM, window=wind, freq="MS") # test kg m-2 s-1 - pr.attrs['units'] = 'kg m-2 s-1' - out3 = atmos.max_n_day_precipitation_amount(pr, window=wind, freq='MS') + pr.attrs["units"] = "kg m-2 s-1" + out3 = atmos.max_n_day_precipitation_amount(pr, window=wind, freq="MS") np.testing.assert_array_almost_equal(out1, out2, 3) np.testing.assert_array_almost_equal(out1, out3, 3) x1 = prMM[:31, 0, 0].values - df = pd.DataFrame({'pr': x1}) + df = pd.DataFrame({"pr": x1}) rx3 = df.rolling(wind).sum().max() assert np.allclose(rx3, out1.values[0, 0, 0]) @@ -253,36 +258,38 @@ def test_3d_data_with_nans(self): assert np.isnan(out1.values[0, -1, -1]) -@pytest.mark.skipif(sys.version_info < (3, 5), reason="too slow to evaluate on python2.7") -class TestMaxConsecWetDays(): +@pytest.mark.skipif( + sys.version_info < (3, 5), reason="too slow to evaluate on python2.7" +) +class TestMaxConsecWetDays: # TODO: replace by fixture - nc_file = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_pr_1990.nc') + nc_file = os.path.join(TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_pr_1990.nc") def test_3d_data_with_nans(self): # test with 3d data pr = xr.open_dataset(self.nc_file).pr prMM = xr.open_dataset(self.nc_file).pr - prMM.values *= 86400. - prMM.attrs['units'] = 'mm/day' + prMM.values *= 86400.0 + prMM.attrs["units"] = "mm/day" # put a nan somewhere prMM.values[10, 1, 0] = np.nan pr.values[10, 1, 0] = np.nan - pr_min = '5 mm/d' - out1 = atmos.maximum_consecutive_wet_days(pr, thresh=pr_min, freq='MS') - out2 = atmos.maximum_consecutive_wet_days(prMM, thresh=pr_min, freq='MS') + pr_min = "5 mm/d" + out1 = atmos.maximum_consecutive_wet_days(pr, thresh=pr_min, freq="MS") + out2 = atmos.maximum_consecutive_wet_days(prMM, thresh=pr_min, freq="MS") # test kg m-2 s-1 - pr.attrs['units'] = 'kg m-2 s-1' - out3 = atmos.maximum_consecutive_wet_days(pr, thresh=pr_min, freq='MS') + pr.attrs["units"] = "kg m-2 s-1" + out3 = atmos.maximum_consecutive_wet_days(pr, thresh=pr_min, freq="MS") np.testing.assert_array_equal(out1, out2) np.testing.assert_array_equal(out1, out3) # check some vector with and without a nan - x1 = prMM[:31, 0, 0] * 0. + x1 = prMM[:31, 0, 0] * 0.0 x1[5:10] = 10 - x1.attrs['units'] = 'mm/day' - cwd1 = atmos.maximum_consecutive_wet_days(x1, freq='MS') + x1.attrs["units"] = "mm/day" + cwd1 = atmos.maximum_consecutive_wet_days(x1, freq="MS") assert cwd1 == 5 @@ -293,36 +300,38 @@ def test_3d_data_with_nans(self): # assert (np.isnan(wds.values[0, -1, -1])) -@pytest.mark.skipif(sys.version_info < (3, 5), reason="too slow to evaluate on python2.7") -class TestMaxConsecDryDays(): +@pytest.mark.skipif( + sys.version_info < (3, 5), reason="too slow to evaluate on python2.7" +) +class TestMaxConsecDryDays: # TODO: replace by fixture - nc_file = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_pr_1990.nc') + nc_file = os.path.join(TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_pr_1990.nc") def test_3d_data_with_nans(self): # test with 3d data pr = xr.open_dataset(self.nc_file).pr prMM = xr.open_dataset(self.nc_file).pr - prMM.values *= 86400. - prMM.attrs['units'] = 'mm/day' + prMM.values *= 86400.0 + prMM.attrs["units"] = "mm/day" # put a nan somewhere prMM.values[10, 1, 0] = np.nan pr.values[10, 1, 0] = np.nan - pr_min = '5 mm/d' - out1 = atmos.maximum_consecutive_dry_days(pr, thresh=pr_min, freq='MS') - out2 = atmos.maximum_consecutive_dry_days(prMM, thresh=pr_min, freq='MS') + pr_min = "5 mm/d" + out1 = atmos.maximum_consecutive_dry_days(pr, thresh=pr_min, freq="MS") + out2 = atmos.maximum_consecutive_dry_days(prMM, thresh=pr_min, freq="MS") # test kg m-2 s-1 - pr.attrs['units'] = 'kg m-2 s-1' - out3 = atmos.maximum_consecutive_dry_days(pr, thresh=pr_min, freq='MS') + pr.attrs["units"] = "kg m-2 s-1" + out3 = atmos.maximum_consecutive_dry_days(pr, thresh=pr_min, freq="MS") np.testing.assert_array_equal(out1, out2) np.testing.assert_array_equal(out1, out3) # check some vector with and without a nan - x1 = prMM[:31, 0, 0] * 0. + 50.0 + x1 = prMM[:31, 0, 0] * 0.0 + 50.0 x1[5:10] = 0 - x1.attrs['units'] = 'mm/day' - cdd1 = atmos.maximum_consecutive_dry_days(x1, freq='MS') + x1.attrs["units"] = "mm/day" + cdd1 = atmos.maximum_consecutive_dry_days(x1, freq="MS") assert cdd1 == 5 diff --git a/tests/test_run_length.py b/tests/test_run_length.py index 3b24395f0..2e575c649 100644 --- a/tests/test_run_length.py +++ b/tests/test_run_length.py @@ -1,120 +1,142 @@ +import os + +import numpy as np +import pandas as pd +import xarray as xr + from xclim import run_length as rl from xclim.testing.common import tas_series -import xarray as xr -import pandas as pd -import numpy as np -import os TAS_SERIES = tas_series TESTS_HOME = os.path.abspath(os.path.dirname(__file__)) -TESTS_DATA = os.path.join(TESTS_HOME, 'testdata') +TESTS_DATA = os.path.join(TESTS_HOME, "testdata") K2C = 273.15 class TestRLE: - def test_dataarray(self): values = np.zeros(365) - time = pd.date_range('7/1/2000', periods=len(values), freq=pd.DateOffset(days=1)) + time = pd.date_range( + "7/1/2000", periods=len(values), freq=pd.DateOffset(days=1) + ) values[1:11] = 1 - da = xr.DataArray(values, coords={'time': time}, dims='time') + da = xr.DataArray(values, coords={"time": time}, dims="time") v, l, p = rl.rle_1d(da != 0) class TestLongestRun: - nc_pr = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_pr_1990.nc') + nc_pr = os.path.join(TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_pr_1990.nc") def test_simple(self): values = np.zeros(365) - time = pd.date_range('7/1/2000', periods=len(values), freq=pd.DateOffset(days=1)) + time = pd.date_range( + "7/1/2000", periods=len(values), freq=pd.DateOffset(days=1) + ) values[1:11] = 1 - da = xr.DataArray(values != 0, coords={'time': time}, dims='time') - lt = da.resample(time='M').apply(rl.longest_run_ufunc) + da = xr.DataArray(values != 0, coords={"time": time}, dims="time") + lt = da.resample(time="M").apply(rl.longest_run_ufunc) assert lt[0] == 10 np.testing.assert_array_equal(lt[1:], 0) # n-dim version versus ufunc da3d = xr.open_dataset(self.nc_pr).pr[:, 40:50, 50:68] != 0 - lt_orig = da3d.resample(time='M').apply(rl.longest_run_ufunc) + lt_orig = da3d.resample(time="M").apply(rl.longest_run_ufunc) # override 'auto' usage of ufunc for small number of gridpoints - lt_Ndim = da3d.resample(time='M').apply(rl.longest_run, dim='time', ufunc_1dim=False) + lt_Ndim = da3d.resample(time="M").apply( + rl.longest_run, dim="time", ufunc_1dim=False + ) np.testing.assert_array_equal(lt_orig, lt_Ndim) def test_start_at_0(self): values = np.zeros(365) - time = pd.date_range('7/1/2000', periods=len(values), freq=pd.DateOffset(days=1)) + time = pd.date_range( + "7/1/2000", periods=len(values), freq=pd.DateOffset(days=1) + ) values[0:10] = 1 - da = xr.DataArray(values != 0, coords={'time': time}, dims='time') - lt = da.resample(time='M').apply(rl.longest_run_ufunc) + da = xr.DataArray(values != 0, coords={"time": time}, dims="time") + lt = da.resample(time="M").apply(rl.longest_run_ufunc) assert lt[0] == 10 np.testing.assert_array_equal(lt[1:], 0) # n-dim version versus ufunc da3d = xr.open_dataset(self.nc_pr).pr[:, 40:50, 50:68] * 0 - da3d[0:10, ] = da3d[0:10, ] + 1 + da3d[0:10,] = da3d[0:10,] + 1 da3d = da3d == 1 - lt_orig = da3d.resample(time='M').apply(rl.longest_run_ufunc) + lt_orig = da3d.resample(time="M").apply(rl.longest_run_ufunc) # override 'auto' usage of ufunc for small number of gridpoints - lt_Ndim = da3d.resample(time='M').apply(rl.longest_run, dim='time', - ufunc_1dim=False) # override 'auto' for small + lt_Ndim = da3d.resample(time="M").apply( + rl.longest_run, dim="time", ufunc_1dim=False + ) # override 'auto' for small np.testing.assert_array_equal(lt_orig, lt_Ndim) def test_end_start_at_0(self): values = np.zeros(365) - time = pd.date_range('7/1/2000', periods=len(values), freq=pd.DateOffset(days=1)) + time = pd.date_range( + "7/1/2000", periods=len(values), freq=pd.DateOffset(days=1) + ) values[-10:] = 1 - da = xr.DataArray(values != 0, coords={'time': time}, dims='time') + da = xr.DataArray(values != 0, coords={"time": time}, dims="time") - lt = da.resample(time='M').apply(rl.longest_run_ufunc) + lt = da.resample(time="M").apply(rl.longest_run_ufunc) assert lt[-1] == 10 np.testing.assert_array_equal(lt[:-1], 0) # n-dim version versus ufunc da3d = xr.open_dataset(self.nc_pr).pr[:, 40:50, 50:68] * 0 - da3d[-10:, ] = da3d[-10:, ] + 1 + da3d[-10:,] = da3d[-10:,] + 1 da3d = da3d == 1 - lt_orig = da3d.resample(time='M').apply(rl.longest_run_ufunc) - lt_Ndim = da3d.resample(time='M').apply(rl.longest_run, dim='time', ufunc_1dim=False) + lt_orig = da3d.resample(time="M").apply(rl.longest_run_ufunc) + lt_Ndim = da3d.resample(time="M").apply( + rl.longest_run, dim="time", ufunc_1dim=False + ) np.testing.assert_array_equal(lt_orig, lt_Ndim) def test_all_true(self): values = np.ones(365) - time = pd.date_range('7/1/2000', periods=len(values), freq=pd.DateOffset(days=1)) - da = xr.DataArray(values != 0, coords={'time': time}, dims='time') + time = pd.date_range( + "7/1/2000", periods=len(values), freq=pd.DateOffset(days=1) + ) + da = xr.DataArray(values != 0, coords={"time": time}, dims="time") - lt = da.resample(time='M').apply(rl.longest_run_ufunc) - np.testing.assert_array_equal(lt, da.resample(time='M').count(dim='time')) + lt = da.resample(time="M").apply(rl.longest_run_ufunc) + np.testing.assert_array_equal(lt, da.resample(time="M").count(dim="time")) # n-dim version versus ufunc da3d = xr.open_dataset(self.nc_pr).pr[:, 40:50, 50:68] * 0 + 1 da3d = da3d == 1 - lt_orig = da3d.resample(time='M').apply(rl.longest_run_ufunc) - lt_Ndim = da3d.resample(time='M').apply(rl.longest_run, dim='time', ufunc_1dim=False) + lt_orig = da3d.resample(time="M").apply(rl.longest_run_ufunc) + lt_Ndim = da3d.resample(time="M").apply( + rl.longest_run, dim="time", ufunc_1dim=False + ) np.testing.assert_array_equal(lt_orig, lt_Ndim) def test_almost_all_true(self): values = np.ones(365) values[35] = 0 - time = pd.date_range('7/1/2000', periods=len(values), freq=pd.DateOffset(days=1)) - da = xr.DataArray(values != 0, coords={'time': time}, dims='time') + time = pd.date_range( + "7/1/2000", periods=len(values), freq=pd.DateOffset(days=1) + ) + da = xr.DataArray(values != 0, coords={"time": time}, dims="time") - lt = da.resample(time='M').apply(rl.longest_run_ufunc) - n = da.resample(time='M').count(dim='time') + lt = da.resample(time="M").apply(rl.longest_run_ufunc) + n = da.resample(time="M").count(dim="time") np.testing.assert_array_equal(lt[0], n[0]) np.testing.assert_array_equal(lt[1], 26) # n-dim version versus ufunc da3d = xr.open_dataset(self.nc_pr).pr[:, 40:50, 50:68] * 0 + 1 - da3d[35, ] = da3d[35, ] + 1 + da3d[35,] = da3d[35,] + 1 da3d = da3d == 1 - lt_orig = da3d.resample(time='M').apply(rl.longest_run_ufunc) - lt_Ndim = da3d.resample(time='M').apply(rl.longest_run, dim='time', ufunc_1dim=False) + lt_orig = da3d.resample(time="M").apply(rl.longest_run_ufunc) + lt_Ndim = da3d.resample(time="M").apply( + rl.longest_run, dim="time", ufunc_1dim=False + ) np.testing.assert_array_equal(lt_orig, lt_Ndim) class TestFirstRun: - nc_pr = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_pr_1990.nc') + nc_pr = os.path.join(TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_pr_1990.nc") def test_simple(self): a = np.zeros(100, bool) @@ -124,13 +146,15 @@ def test_simple(self): # n-dim version versus ufunc da3d = xr.open_dataset(self.nc_pr).pr[:, 40:50, 50:68] != 0 - lt_orig = da3d.resample(time='M').apply(rl.first_run_ufunc, window=5) - lt_Ndim = da3d.resample(time='M').apply(rl.first_run, window=5, dim='time', ufunc_1dim=False) + lt_orig = da3d.resample(time="M").apply(rl.first_run_ufunc, window=5) + lt_Ndim = da3d.resample(time="M").apply( + rl.first_run, window=5, dim="time", ufunc_1dim=False + ) np.testing.assert_array_equal(lt_orig, lt_Ndim) class TestWindowedRunEvents: - nc_pr = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_pr_1990.nc') + nc_pr = os.path.join(TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_pr_1990.nc") def test_simple(self): a = np.zeros(50, bool) @@ -140,13 +164,15 @@ def test_simple(self): # n-dim version versus ufunc da3d = xr.open_dataset(self.nc_pr).pr[:, 40:50, 50:68] != 0 - lt_orig = da3d.resample(time='M').apply(rl.windowed_run_events_ufunc, window=4) - lt_Ndim = da3d.resample(time='M').apply(rl.windowed_run_events, window=4, dim='time', ufunc_1dim=False) + lt_orig = da3d.resample(time="M").apply(rl.windowed_run_events_ufunc, window=4) + lt_Ndim = da3d.resample(time="M").apply( + rl.windowed_run_events, window=4, dim="time", ufunc_1dim=False + ) np.testing.assert_array_equal(lt_orig, lt_Ndim) class TestWindowedRunCount: - nc_pr = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_pr_1990.nc') + nc_pr = os.path.join(TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_pr_1990.nc") def test_simple(self): a = np.zeros(50, bool) @@ -156,6 +182,8 @@ def test_simple(self): # n-dim version versus ufunc da3d = xr.open_dataset(self.nc_pr).pr[:, 40:50, 50:68] != 0 - lt_orig = da3d.resample(time='M').apply(rl.windowed_run_count_ufunc, window=4) - lt_Ndim = da3d.resample(time='M').apply(rl.windowed_run_count, window=4, dim='time', ufunc_1dim=False) + lt_orig = da3d.resample(time="M").apply(rl.windowed_run_count_ufunc, window=4) + lt_Ndim = da3d.resample(time="M").apply( + rl.windowed_run_count, window=4, dim="time", ufunc_1dim=False + ) np.testing.assert_array_equal(lt_orig, lt_Ndim) diff --git a/tests/test_streamflow.py b/tests/test_streamflow.py index 90b101ab9..f04d675d4 100644 --- a/tests/test_streamflow.py +++ b/tests/test_streamflow.py @@ -1,58 +1,60 @@ +import numpy as np import pytest + from xclim import streamflow -import numpy as np def test_base_flow_index(ndq_series): - out = streamflow.base_flow_index(ndq_series, freq='YS') - assert out.attrs['units'] == '' + out = streamflow.base_flow_index(ndq_series, freq="YS") + assert out.attrs["units"] == "" -class Test_FA(): - +class Test_FA: def test_simple(self, ndq_series): - out = streamflow.freq_analysis(ndq_series, mode='max', t=[2, 5], dist='gamma', season='DJF') - assert out.long_name == 'N-year return period max winter 1-day flow' + out = streamflow.freq_analysis( + ndq_series, mode="max", t=[2, 5], dist="gamma", season="DJF" + ) + assert out.long_name == "N-year return period max winter 1-day flow" assert out.shape == (2, 2, 3) # nrt, nx, ny def test_no_indexer(self, ndq_series): - out = streamflow.freq_analysis(ndq_series, mode='max', t=[2, 5], dist='gamma') - assert out.long_name == 'N-year return period max annual 1-day flow' + out = streamflow.freq_analysis(ndq_series, mode="max", t=[2, 5], dist="gamma") + assert out.long_name == "N-year return period max annual 1-day flow" assert out.shape == (2, 2, 3) # nrt, nx, ny def test_q27(self, ndq_series): - out = streamflow.freq_analysis(ndq_series, mode='max', t=2, dist='gamma', window=7) + out = streamflow.freq_analysis( + ndq_series, mode="max", t=2, dist="gamma", window=7 + ) assert out.shape == (1, 2, 3) -class TestStats(): - +class TestStats: def test_simple(self, ndq_series): - out = streamflow.stats(ndq_series, freq='YS', op='min', season='MAM') - assert out.attrs['units'] == 'm^3 s-1' + out = streamflow.stats(ndq_series, freq="YS", op="min", season="MAM") + assert out.attrs["units"] == "m^3 s-1" def test_missing(self, ndq_series): a = ndq_series a = ndq_series.where(~((a.time.dt.dayofyear == 5) * (a.time.dt.year == 1902))) - out = streamflow.stats(a, op='max', month=1) + out = streamflow.stats(a, op="max", month=1) np.testing.assert_array_equal(out[1].isnull(), False) np.testing.assert_array_equal(out[2].isnull(), True) -class TestFit(): - +class TestFit: def test_simple(self, ndq_series): - ts = streamflow.stats(ndq_series, freq='YS', op='max') - p = streamflow.fit(ts, dist='gumbel_r') - assert p.attrs['estimator'] == 'Maximum likelihood' + ts = streamflow.stats(ndq_series, freq="YS", op="max") + p = streamflow.fit(ts, dist="gumbel_r") + assert p.attrs["estimator"] == "Maximum likelihood" def test_qdoy_max(ndq_series, q_series): - out = streamflow.doy_qmax(ndq_series, freq='YS', season='JJA') - assert out.attrs['units'] == '' + out = streamflow.doy_qmax(ndq_series, freq="YS", season="JJA") + assert out.attrs["units"] == "" a = np.ones(450) a[100] = 2 - out = streamflow.doy_qmax(q_series(a), freq='YS') + out = streamflow.doy_qmax(q_series(a), freq="YS") assert out[0] == 101 diff --git a/tests/test_temperature.py b/tests/test_temperature.py index e57ccf723..59a49067f 100644 --- a/tests/test_temperature.py +++ b/tests/test_temperature.py @@ -4,11 +4,13 @@ import xarray as xr import xclim.atmos as atmos -from xclim.testing.common import tas_series, tasmin_series, tasmax_series +from xclim.testing.common import tas_series +from xclim.testing.common import tasmax_series +from xclim.testing.common import tasmin_series from xclim.utils import percentile_doy TESTS_HOME = os.path.abspath(os.path.dirname(__file__)) -TESTS_DATA = os.path.join(TESTS_HOME, 'testdata') +TESTS_DATA = os.path.join(TESTS_HOME, "testdata") TAS_SERIES = tas_series TASMIN_SERIES = tasmin_series @@ -20,146 +22,173 @@ class TestCSDI: def test_simple(self, tasmin_series): i = 3650 - A = 10. - tn = np.zeros(i) + A * np.sin(np.arange(i) / 365. * 2 * np.pi) + .1 * np.random.rand(i) + A = 10.0 + tn = ( + np.zeros(i) + + A * np.sin(np.arange(i) / 365.0 * 2 * np.pi) + + 0.1 * np.random.rand(i) + ) tn += K2C tn[10:20] -= 2 tn = tasmin_series(tn) - tn10 = percentile_doy(tn, per=.1) + tn10 = percentile_doy(tn, per=0.1) - out = atmos.cold_spell_duration_index(tn, tn10, freq='AS-JUL') + out = atmos.cold_spell_duration_index(tn, tn10, freq="AS-JUL") assert out[0] == 10 def test_convert_units(self, tasmin_series): i = 3650 - A = 10. - tn = np.zeros(i) + A * np.sin(np.arange(i) / 365. * 2 * np.pi) + .1 * np.random.rand(i) + A = 10.0 + tn = ( + np.zeros(i) + + A * np.sin(np.arange(i) / 365.0 * 2 * np.pi) + + 0.1 * np.random.rand(i) + ) tn[10:20] -= 2 tn = tasmin_series(tn + K2C) - tn.attrs['units'] = 'C' - tn10 = percentile_doy(tn, per=.1) + tn.attrs["units"] = "C" + tn10 = percentile_doy(tn, per=0.1) - out = atmos.cold_spell_duration_index(tn, tn10, freq='AS-JUL') + out = atmos.cold_spell_duration_index(tn, tn10, freq="AS-JUL") assert out[0] == 10 def test_nan_presence(self, tasmin_series): i = 3650 - A = 10. - tn = np.zeros(i) + K2C + A * np.sin(np.arange(i) / 365. * 2 * np.pi) + .1 * np.random.rand(i) + A = 10.0 + tn = ( + np.zeros(i) + + K2C + + A * np.sin(np.arange(i) / 365.0 * 2 * np.pi) + + 0.1 * np.random.rand(i) + ) tn[10:20] -= 2 tn[9] = np.nan tn = tasmin_series(tn) - tn10 = percentile_doy(tn, per=.1) + tn10 = percentile_doy(tn, per=0.1) - out = atmos.cold_spell_duration_index(tn, tn10, freq='AS-JUL') + out = atmos.cold_spell_duration_index(tn, tn10, freq="AS-JUL") assert np.isnan(out[0]) class TestDTR: - nc_tasmax = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmax_1990.nc') - nc_tasmin = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmin_1990.nc') + nc_tasmax = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmax_1990.nc" + ) + nc_tasmin = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmin_1990.nc" + ) def test_DTR_3d_data_with_nans(self): tasmax = xr.open_dataset(self.nc_tasmax).tasmax tasmax_C = xr.open_dataset(self.nc_tasmax).tasmax tasmax_C -= K2C - tasmax_C.attrs['units'] = 'C' + tasmax_C.attrs["units"] = "C" tasmin = xr.open_dataset(self.nc_tasmin).tasmin tasmin_C = xr.open_dataset(self.nc_tasmin).tasmin tasmin_C -= K2C - tasmin_C.attrs['units'] = 'C' + tasmin_C.attrs["units"] = "C" # put a nan somewhere tasmin.values[32, 1, 0] = np.nan tasmin_C.values[32, 1, 0] = np.nan - dtr = atmos.daily_temperature_range(tasmax, tasmin, freq='MS') - dtrC = atmos.daily_temperature_range(tasmax_C, tasmin_C, freq='MS') + dtr = atmos.daily_temperature_range(tasmax, tasmin, freq="MS") + dtrC = atmos.daily_temperature_range(tasmax_C, tasmin_C, freq="MS") min1 = tasmin.values[:, 0, 0] max1 = tasmax.values[:, 0, 0] - dtr1 = (max1 - min1) + dtr1 = max1 - min1 np.testing.assert_array_equal(dtr, dtrC) - assert (np.allclose(dtr1[0:31].mean(), dtr.values[0, 0, 0], dtrC.values[0, 0, 0])) + assert np.allclose(dtr1[0:31].mean(), dtr.values[0, 0, 0], dtrC.values[0, 0, 0]) - assert (np.isnan(dtr.values[1, 1, 0])) + assert np.isnan(dtr.values[1, 1, 0]) - assert (np.isnan(dtr.values[0, -1, -1])) + assert np.isnan(dtr.values[0, -1, -1]) class TestDTRVar: - nc_tasmax = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmax_1990.nc') - nc_tasmin = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmin_1990.nc') + nc_tasmax = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmax_1990.nc" + ) + nc_tasmin = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmin_1990.nc" + ) def test_dtr_var_3d_data_with_nans(self): tasmax = xr.open_dataset(self.nc_tasmax).tasmax tasmax_C = xr.open_dataset(self.nc_tasmax).tasmax tasmax_C -= K2C - tasmax_C.attrs['units'] = 'C' + tasmax_C.attrs["units"] = "C" tasmin = xr.open_dataset(self.nc_tasmin).tasmin tasmin_C = xr.open_dataset(self.nc_tasmin).tasmin tasmin_C -= K2C - tasmin_C.attrs['units'] = 'C' + tasmin_C.attrs["units"] = "C" # put a nan somewhere tasmin.values[32, 1, 0] = np.nan tasmin_C.values[32, 1, 0] = np.nan - dtr = atmos.daily_temperature_range_variability(tasmax, tasmin, freq='MS') - dtrC = atmos.daily_temperature_range_variability(tasmax_C, tasmin_C, freq='MS') + dtr = atmos.daily_temperature_range_variability(tasmax, tasmin, freq="MS") + dtrC = atmos.daily_temperature_range_variability(tasmax_C, tasmin_C, freq="MS") min1 = tasmin.values[:, 0, 0] max1 = tasmax.values[:, 0, 0] - dtr1a = (max1 - min1) + dtr1a = max1 - min1 dtr1 = abs(np.diff(dtr1a)) np.testing.assert_array_equal(dtr, dtrC) # first month jan use 0:30 (n==30) because of day to day diff - assert (np.allclose(dtr1[0:30].mean(), dtr.values[0, 0, 0], dtrC.values[0, 0, 0])) + assert np.allclose(dtr1[0:30].mean(), dtr.values[0, 0, 0], dtrC.values[0, 0, 0]) - assert (np.isnan(dtr.values[1, 1, 0])) + assert np.isnan(dtr.values[1, 1, 0]) - assert (np.isnan(dtr.values[0, -1, -1])) + assert np.isnan(dtr.values[0, -1, -1]) class TestETR: - nc_tasmax = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmax_1990.nc') - nc_tasmin = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmin_1990.nc') + nc_tasmax = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmax_1990.nc" + ) + nc_tasmin = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmin_1990.nc" + ) def test_dtr_var_3d_data_with_nans(self): tasmax = xr.open_dataset(self.nc_tasmax).tasmax tasmax_C = xr.open_dataset(self.nc_tasmax).tasmax tasmax_C -= K2C - tasmax_C.attrs['units'] = 'C' + tasmax_C.attrs["units"] = "C" tasmin = xr.open_dataset(self.nc_tasmin).tasmin tasmin_C = xr.open_dataset(self.nc_tasmin).tasmin tasmin_C -= K2C - tasmin_C.attrs['units'] = 'C' + tasmin_C.attrs["units"] = "C" # put a nan somewhere tasmin.values[32, 1, 0] = np.nan tasmin_C.values[32, 1, 0] = np.nan - etr = atmos.extreme_temperature_range(tasmax, tasmin, freq='MS') - etrC = atmos.extreme_temperature_range(tasmax_C, tasmin_C, freq='MS') + etr = atmos.extreme_temperature_range(tasmax, tasmin, freq="MS") + etrC = atmos.extreme_temperature_range(tasmax_C, tasmin_C, freq="MS") min1 = tasmin.values[:, 0, 0] max1 = tasmax.values[:, 0, 0] np.testing.assert_array_equal(etr, etrC) etr1 = max1[0:31].max() - min1[0:31].min() - assert (np.allclose(etr1, etr.values[0, 0, 0], etrC.values[0, 0, 0])) + assert np.allclose(etr1, etr.values[0, 0, 0], etrC.values[0, 0, 0]) - assert (np.isnan(etr.values[1, 1, 0])) + assert np.isnan(etr.values[1, 1, 0]) - assert (np.isnan(etr.values[0, -1, -1])) + assert np.isnan(etr.values[0, -1, -1]) class TestTmean: - nc_file = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmax_1990.nc') + nc_file = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmax_1990.nc" + ) def test_Tmean_3d_data(self): tas = xr.open_dataset(self.nc_file).tasmax tas_C = xr.open_dataset(self.nc_file).tasmax tas_C.values -= K2C - tas_C.attrs['units'] = 'C' + tas_C.attrs["units"] = "C" # put a nan somewhere tas.values[180, 1, 0] = np.nan tas_C.values[180, 1, 0] = np.nan @@ -172,21 +201,23 @@ def test_Tmean_3d_data(self): # The conversion to K is done after / before the mean. np.testing.assert_array_almost_equal(tmmeanC, tmmean, 3) # test single point vs manual - assert (np.allclose(tmmean1, tmmean.values[0, 0, 0], tmmeanC.values[0, 0, 0])) + assert np.allclose(tmmean1, tmmean.values[0, 0, 0], tmmeanC.values[0, 0, 0]) # test single nan point - assert (np.isnan(tmmean.values[0, 1, 0])) + assert np.isnan(tmmean.values[0, 1, 0]) # test all nan point - assert (np.isnan(tmmean.values[0, -1, -1])) + assert np.isnan(tmmean.values[0, -1, -1]) class TestTx: - nc_file = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmax_1990.nc') + nc_file = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmax_1990.nc" + ) def test_TX_3d_data(self): tasmax = xr.open_dataset(self.nc_file).tasmax tasmax_C = xr.open_dataset(self.nc_file).tasmax tasmax_C.values -= K2C - tasmax_C.attrs['units'] = 'C' + tasmax_C.attrs["units"] = "C" # put a nan somewhere tasmax.values[180, 1, 0] = np.nan tasmax_C.values[180, 1, 0] = np.nan @@ -198,11 +229,14 @@ def test_TX_3d_data(self): txmaxC = atmos.tx_max(tasmax_C) txminC = atmos.tx_min(tasmax_C) - no_nan = ~np.isnan(txmean).values & ~np.isnan(txmax).values & ~np.isnan(txmin).values + no_nan = ( + ~np.isnan(txmean).values & ~np.isnan(txmax).values & ~np.isnan(txmin).values + ) # test maxes always greater than mean and mean always greater than min (non nan values only) - assert (np.all(txmax.values[no_nan] > txmean.values[no_nan]) & np.all( - txmean.values[no_nan] > txmin.values[no_nan])) + assert np.all(txmax.values[no_nan] > txmean.values[no_nan]) & np.all( + txmean.values[no_nan] > txmin.values[no_nan] + ) np.testing.assert_array_almost_equal(txmeanC, txmean, 3) np.testing.assert_array_equal(txminC, txmin) @@ -213,27 +247,29 @@ def test_TX_3d_data(self): txmax1 = x1.max() # test single point vs manual - assert (np.allclose(txmean1, txmean.values[0, 0, 0], txmeanC.values[0, 0, 0])) - assert (np.allclose(txmax1, txmax.values[0, 0, 0], txmaxC.values[0, 0, 0])) - assert (np.allclose(txmin1, txmin.values[0, 0, 0], txminC.values[0, 0, 0])) + assert np.allclose(txmean1, txmean.values[0, 0, 0], txmeanC.values[0, 0, 0]) + assert np.allclose(txmax1, txmax.values[0, 0, 0], txmaxC.values[0, 0, 0]) + assert np.allclose(txmin1, txmin.values[0, 0, 0], txminC.values[0, 0, 0]) # test single nan point - assert (np.isnan(txmean.values[0, 1, 0])) - assert (np.isnan(txmin.values[0, 1, 0])) - assert (np.isnan(txmax.values[0, 1, 0])) + assert np.isnan(txmean.values[0, 1, 0]) + assert np.isnan(txmin.values[0, 1, 0]) + assert np.isnan(txmax.values[0, 1, 0]) # test all nan point - assert (np.isnan(txmean.values[0, -1, -1])) - assert (np.isnan(txmin.values[0, -1, -1])) - assert (np.isnan(txmax.values[0, -1, -1])) + assert np.isnan(txmean.values[0, -1, -1]) + assert np.isnan(txmin.values[0, -1, -1]) + assert np.isnan(txmax.values[0, -1, -1]) class TestTn: - nc_file = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmin_1990.nc') + nc_file = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmin_1990.nc" + ) def test_TN_3d_data(self): tasmin = xr.open_dataset(self.nc_file).tasmin tasmin_C = xr.open_dataset(self.nc_file).tasmin tasmin_C.values -= K2C - tasmin_C.attrs['units'] = 'C' + tasmin_C.attrs["units"] = "C" # put a nan somewhere tasmin.values[180, 1, 0] = np.nan tasmin_C.values[180, 1, 0] = np.nan @@ -245,11 +281,14 @@ def test_TN_3d_data(self): tnmaxC = atmos.tn_max(tasmin_C) tnminC = atmos.tn_min(tasmin_C) - no_nan = ~np.isnan(tnmean).values & ~np.isnan(tnmax).values & ~np.isnan(tnmin).values + no_nan = ( + ~np.isnan(tnmean).values & ~np.isnan(tnmax).values & ~np.isnan(tnmin).values + ) # test maxes always greater than mean and mean alwyas greater than min (non nan values only) - assert (np.all(tnmax.values[no_nan] > tnmean.values[no_nan]) & np.all( - tnmean.values[no_nan] > tnmin.values[no_nan])) + assert np.all(tnmax.values[no_nan] > tnmean.values[no_nan]) & np.all( + tnmean.values[no_nan] > tnmin.values[no_nan] + ) np.testing.assert_array_almost_equal(tnmeanC, tnmean, 3) np.testing.assert_array_equal(tnminC, tnmin) @@ -261,21 +300,20 @@ def test_TN_3d_data(self): txmax1 = x1.max() # test single point vs manual - assert (np.allclose(txmean1, tnmean.values[0, 0, 0], tnmeanC.values[0, 0, 0])) - assert (np.allclose(txmax1, tnmax.values[0, 0, 0], tnmaxC.values[0, 0, 0])) - assert (np.allclose(txmin1, tnmin.values[0, 0, 0], tnminC.values[0, 0, 0])) + assert np.allclose(txmean1, tnmean.values[0, 0, 0], tnmeanC.values[0, 0, 0]) + assert np.allclose(txmax1, tnmax.values[0, 0, 0], tnmaxC.values[0, 0, 0]) + assert np.allclose(txmin1, tnmin.values[0, 0, 0], tnminC.values[0, 0, 0]) # test single nan point - assert (np.isnan(tnmean.values[0, 1, 0])) - assert (np.isnan(tnmin.values[0, 1, 0])) - assert (np.isnan(tnmax.values[0, 1, 0])) + assert np.isnan(tnmean.values[0, 1, 0]) + assert np.isnan(tnmin.values[0, 1, 0]) + assert np.isnan(tnmax.values[0, 1, 0]) # test all nan point - assert (np.isnan(tnmean.values[0, -1, -1])) - assert (np.isnan(tnmin.values[0, -1, -1])) - assert (np.isnan(tnmax.values[0, -1, -1])) + assert np.isnan(tnmean.values[0, -1, -1]) + assert np.isnan(tnmin.values[0, -1, -1]) + assert np.isnan(tnmax.values[0, -1, -1]) class TestConsecutiveFrostDays: - def test_one_freeze_day(self, tasmin_series): a = np.zeros(365) + K2C + 5.0 a[2] -= 20 @@ -313,7 +351,7 @@ def test_convert_units_freeze_day(self, tasmin_series): a[2:5] -= 20 a[6:10] -= 20 ts = tasmin_series(a) - ts.attrs['units'] = 'C' + ts.attrs["units"] = "C" out = atmos.consecutive_frost_days(ts) np.testing.assert_array_equal(out, [4]) @@ -328,14 +366,13 @@ def test_one_nan_day(self, tasmin_series): class TestColdSpellDays: - def test_simple(self, tas_series): a = np.zeros(365) + K2C a[10:20] -= 15 # 10 days a[40:43] -= 50 # too short -> 0 a[80:100] -= 30 # at the end and beginning ts = tas_series(a) - out = atmos.cold_spell_days(ts, thresh='-10 C', freq='MS') + out = atmos.cold_spell_days(ts, thresh="-10 C", freq="MS") np.testing.assert_array_equal(out, [10, 0, 12, 8, 0, 0, 0, 0, 0, 0, 0, 0]) def test_convert_units(self, tas_series): @@ -344,8 +381,8 @@ def test_convert_units(self, tas_series): a[40:43] -= 50 # too short -> 0 a[80:100] -= 30 # at the end and beginning ts = tas_series(a) - ts.attrs['units'] = 'C' - out = atmos.cold_spell_days(ts, thresh='-10 C', freq='MS') + ts.attrs["units"] = "C" + out = atmos.cold_spell_days(ts, thresh="-10 C", freq="MS") np.testing.assert_array_equal(out, [10, 0, 12, 8, 0, 0, 0, 0, 0, 0, 0, 0]) def test_nan_presence(self, tas_series): @@ -356,26 +393,28 @@ def test_nan_presence(self, tas_series): a[-1] = np.nan ts = tas_series(a) - out = atmos.cold_spell_days(ts, thresh='-10 C', freq='MS') + out = atmos.cold_spell_days(ts, thresh="-10 C", freq="MS") np.testing.assert_array_equal(out, [10, 0, 12, 8, 0, 0, 0, 0, 0, 0, 0, np.nan]) class TestFrostDays: - nc_file = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmin_1990.nc') + nc_file = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmin_1990.nc" + ) def test_3d_data_with_nans(self): # test with 3d data tasmin = xr.open_dataset(self.nc_file).tasmin tasminC = xr.open_dataset(self.nc_file).tasmin tasminC -= K2C - tasminC.attrs['units'] = 'C' + tasminC.attrs["units"] = "C" # put a nan somewhere tasmin.values[180, 1, 0] = np.nan tasminC.values[180, 1, 0] = np.nan # compute with both skipna options thresh = 273.16 - fd = atmos.frost_days(tasmin, freq='YS') - fdC = atmos.frost_days(tasminC, freq='YS') + fd = atmos.frost_days(tasmin, freq="YS") + fdC = atmos.frost_days(tasminC, freq="YS") # fds = xci.frost_days(tasmin, thresh=thresh, freq='YS', skipna=True) x1 = tasmin.values[:, 0, 0] @@ -384,30 +423,32 @@ def test_3d_data_with_nans(self): np.testing.assert_array_equal(fd, fdC) - assert (np.allclose(fd1, fd.values[0, 0, 0])) + assert np.allclose(fd1, fd.values[0, 0, 0]) # assert (np.allclose(fd1, fds.values[0, 0, 0])) - assert (np.isnan(fd.values[0, 1, 0])) + assert np.isnan(fd.values[0, 1, 0]) # assert (np.allclose(fd2, fds.values[0, 1, 0])) - assert (np.isnan(fd.values[0, -1, -1])) + assert np.isnan(fd.values[0, -1, -1]) # assert (np.isnan(fds.values[0, -1, -1])) class TestIceDays: - nc_file = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmax_1990.nc') + nc_file = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmax_1990.nc" + ) def test_3d_data_with_nans(self): # test with 3d data tas = xr.open_dataset(self.nc_file).tasmax tasC = xr.open_dataset(self.nc_file).tasmax tasC -= K2C - tasC.attrs['units'] = 'C' + tasC.attrs["units"] = "C" # put a nan somewhere tas.values[180, 1, 0] = np.nan tasC.values[180, 1, 0] = np.nan # compute with both skipna options thresh = 273.16 - fd = atmos.ice_days(tas, freq='YS') - fdC = atmos.ice_days(tasC, freq='YS') + fd = atmos.ice_days(tas, freq="YS") + fdC = atmos.ice_days(tasC, freq="YS") x1 = tas.values[:, 0, 0] @@ -415,15 +456,17 @@ def test_3d_data_with_nans(self): np.testing.assert_array_equal(fd, fdC) - assert (np.allclose(fd1, fd.values[0, 0, 0])) + assert np.allclose(fd1, fd.values[0, 0, 0]) - assert (np.isnan(fd.values[0, 1, 0])) + assert np.isnan(fd.values[0, 1, 0]) - assert (np.isnan(fd.values[0, -1, -1])) + assert np.isnan(fd.values[0, -1, -1]) class TestCoolingDegreeDays: - nc_file = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmax_1990.nc') + nc_file = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmax_1990.nc" + ) def test_3d_data_with_nans(self): # test with 3d data @@ -433,29 +476,29 @@ def test_3d_data_with_nans(self): # compute with both skipna options thresh = 18 + K2C - cdd = atmos.cooling_degree_days(tas, thresh='18 C', freq='YS') + cdd = atmos.cooling_degree_days(tas, thresh="18 C", freq="YS") x1 = tas.values[:, 0, 0] cdd1 = (x1[x1 > thresh] - thresh).sum() - assert (np.allclose(cdd1, cdd.values[0, 0, 0])) + assert np.allclose(cdd1, cdd.values[0, 0, 0]) - assert (np.isnan(cdd.values[0, 1, 0])) + assert np.isnan(cdd.values[0, 1, 0]) - assert (np.isnan(cdd.values[0, -1, -1])) + assert np.isnan(cdd.values[0, -1, -1]) def test_convert_units(self): # test with 3d data tas = xr.open_dataset(self.nc_file).tasmax tas.values -= K2C - tas.attrs['units'] = 'C' + tas.attrs["units"] = "C" # put a nan somewhere tas.values[180, 1, 0] = np.nan # compute with both skipna options thresh = 18 - cdd = atmos.cooling_degree_days(tas, thresh='18 C', freq='YS') + cdd = atmos.cooling_degree_days(tas, thresh="18 C", freq="YS") x1 = tas.values[:, 0, 0] # x2 = tas.values[:, 1, 0] @@ -463,16 +506,18 @@ def test_convert_units(self): cdd1 = (x1[x1 > thresh] - thresh).sum() # gdd2 = (x2[x2 > thresh] - thresh).sum() - assert (np.allclose(cdd1, cdd.values[0, 0, 0])) + assert np.allclose(cdd1, cdd.values[0, 0, 0]) # assert (np.allclose(gdd1, gdds.values[0, 0, 0])) - assert (np.isnan(cdd.values[0, 1, 0])) + assert np.isnan(cdd.values[0, 1, 0]) # assert (np.allclose(gdd2, gdds.values[0, 1, 0])) - assert (np.isnan(cdd.values[0, -1, -1])) + assert np.isnan(cdd.values[0, -1, -1]) # assert (np.isnan(gdds.values[0, -1, -1])) class TestHeatingDegreeDays: - nc_file = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmax_1990.nc') + nc_file = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmax_1990.nc" + ) def test_3d_data_with_nans(self): # test with 3d data @@ -482,17 +527,17 @@ def test_3d_data_with_nans(self): # compute with both skipna options thresh = 17 + K2C - hdd = atmos.heating_degree_days(tas, freq='YS') + hdd = atmos.heating_degree_days(tas, freq="YS") x1 = tas.values[:, 0, 0] hdd1 = (thresh - x1).clip(min=0).sum() - assert (np.allclose(hdd1, hdd.values[0, 0, 0])) + assert np.allclose(hdd1, hdd.values[0, 0, 0]) - assert (np.isnan(hdd.values[0, 1, 0])) + assert np.isnan(hdd.values[0, 1, 0]) - assert (np.isnan(hdd.values[0, -1, -1])) + assert np.isnan(hdd.values[0, -1, -1]) def test_convert_units(self): # test with 3d data @@ -500,24 +545,26 @@ def test_convert_units(self): # put a nan somewhere tas.values[180, 1, 0] = np.nan tas.values -= K2C - tas.attrs['units'] = 'C' + tas.attrs["units"] = "C" # compute with both skipna options thresh = 17 - hdd = atmos.heating_degree_days(tas, freq='YS') + hdd = atmos.heating_degree_days(tas, freq="YS") x1 = tas.values[:, 0, 0] hdd1 = (thresh - x1).clip(min=0).sum() - assert (np.allclose(hdd1, hdd.values[0, 0, 0])) + assert np.allclose(hdd1, hdd.values[0, 0, 0]) - assert (np.isnan(hdd.values[0, 1, 0])) + assert np.isnan(hdd.values[0, 1, 0]) - assert (np.isnan(hdd.values[0, -1, -1])) + assert np.isnan(hdd.values[0, -1, -1]) class TestGrowingDegreeDays: - nc_file = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmax_1990.nc') + nc_file = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmax_1990.nc" + ) def test_3d_data_with_nans(self): # test with 3d data @@ -527,7 +574,7 @@ def test_3d_data_with_nans(self): # compute with both skipna options thresh = K2C + 4 - gdd = atmos.growing_degree_days(tas, freq='YS') + gdd = atmos.growing_degree_days(tas, freq="YS") # gdds = xci.growing_degree_days(tas, thresh=thresh, freq='YS', skipna=True) x1 = tas.values[:, 0, 0] @@ -536,11 +583,11 @@ def test_3d_data_with_nans(self): gdd1 = (x1[x1 > thresh] - thresh).sum() # gdd2 = (x2[x2 > thresh] - thresh).sum() - assert (np.allclose(gdd1, gdd.values[0, 0, 0])) + assert np.allclose(gdd1, gdd.values[0, 0, 0]) - assert (np.isnan(gdd.values[0, 1, 0])) + assert np.isnan(gdd.values[0, 1, 0]) - assert (np.isnan(gdd.values[0, -1, -1])) + assert np.isnan(gdd.values[0, -1, -1]) class TestHeatWaveFrequency: @@ -550,31 +597,36 @@ def test_1d(self, tasmax_series, tasmin_series): tn1[:10] = np.array([20, 23, 23, 23, 23, 21, 23, 23, 23, 23]) tx1[:10] = np.array([29, 31, 31, 31, 29, 31, 31, 31, 31, 31]) - tn = tasmin_series(tn1 + K2C, start='1/1/2000') - tx = tasmax_series(tx1 + K2C, start='1/1/2000') - tnC = tasmin_series(tn1, start='1/1/2000') - tnC.attrs['units'] = 'C' - txC = tasmax_series(tx1, start='1/1/2000') - txC.attrs['units'] = 'C' - - hwf = atmos.heat_wave_frequency(tn, tx, thresh_tasmin='22 C', - thresh_tasmax='30 C') - hwfC = atmos.heat_wave_frequency(tnC, txC, thresh_tasmin='22 C', - thresh_tasmax='30 C') + tn = tasmin_series(tn1 + K2C, start="1/1/2000") + tx = tasmax_series(tx1 + K2C, start="1/1/2000") + tnC = tasmin_series(tn1, start="1/1/2000") + tnC.attrs["units"] = "C" + txC = tasmax_series(tx1, start="1/1/2000") + txC.attrs["units"] = "C" + + hwf = atmos.heat_wave_frequency( + tn, tx, thresh_tasmin="22 C", thresh_tasmax="30 C" + ) + hwfC = atmos.heat_wave_frequency( + tnC, txC, thresh_tasmin="22 C", thresh_tasmax="30 C" + ) np.testing.assert_array_equal(hwf, hwfC) np.testing.assert_allclose(hwf.values[:1], 2) - hwf = atmos.heat_wave_frequency(tn, tx, thresh_tasmin='22 C', - thresh_tasmax='30 C', window=4) + hwf = atmos.heat_wave_frequency( + tn, tx, thresh_tasmin="22 C", thresh_tasmax="30 C", window=4 + ) np.testing.assert_allclose(hwf.values[:1], 1) # one long hw - hwf = atmos.heat_wave_frequency(tn, tx, thresh_tasmin='10 C', - thresh_tasmax='10 C') + hwf = atmos.heat_wave_frequency( + tn, tx, thresh_tasmin="10 C", thresh_tasmax="10 C" + ) np.testing.assert_allclose(hwf.values[:1], 1) # no hw - hwf = atmos.heat_wave_frequency(tn, tx, thresh_tasmin='40 C', - thresh_tasmax='40 C') + hwf = atmos.heat_wave_frequency( + tn, tx, thresh_tasmin="40 C", thresh_tasmax="40 C" + ) np.testing.assert_allclose(hwf.values[:1], 0) @@ -585,64 +637,72 @@ def test_1d(self, tasmax_series, tasmin_series): tn1[:10] = np.array([20, 23, 23, 23, 23, 21, 23, 23, 23, 23]) tx1[:10] = np.array([29, 31, 31, 31, 29, 31, 31, 31, 31, 31]) - tn = tasmin_series(tn1 + K2C, start='1/1/2000') - tx = tasmax_series(tx1 + K2C, start='1/1/2000') - tnC = tasmin_series(tn1, start='1/1/2000') - tnC.attrs['units'] = 'C' - txC = tasmax_series(tx1, start='1/1/2000') - txC.attrs['units'] = 'C' - - hwf = atmos.heat_wave_max_length(tn, tx, thresh_tasmin='22 C', - thresh_tasmax='30 C') - hwfC = atmos.heat_wave_max_length(tnC, txC, thresh_tasmin='22 C', - thresh_tasmax='30 C') + tn = tasmin_series(tn1 + K2C, start="1/1/2000") + tx = tasmax_series(tx1 + K2C, start="1/1/2000") + tnC = tasmin_series(tn1, start="1/1/2000") + tnC.attrs["units"] = "C" + txC = tasmax_series(tx1, start="1/1/2000") + txC.attrs["units"] = "C" + + hwf = atmos.heat_wave_max_length( + tn, tx, thresh_tasmin="22 C", thresh_tasmax="30 C" + ) + hwfC = atmos.heat_wave_max_length( + tnC, txC, thresh_tasmin="22 C", thresh_tasmax="30 C" + ) np.testing.assert_array_equal(hwf, hwfC) np.testing.assert_allclose(hwf.values[:1], 4) - hwf = atmos.heat_wave_max_length(tn, tx, thresh_tasmin='20 C', - thresh_tasmax='30 C', window=4) + hwf = atmos.heat_wave_max_length( + tn, tx, thresh_tasmin="20 C", thresh_tasmax="30 C", window=4 + ) np.testing.assert_allclose(hwf.values[:1], 5) # one long hw - hwf = atmos.heat_wave_max_length(tn, tx, thresh_tasmin='10 C', - thresh_tasmax='10 C') + hwf = atmos.heat_wave_max_length( + tn, tx, thresh_tasmin="10 C", thresh_tasmax="10 C" + ) np.testing.assert_allclose(hwf.values[:1], 10) # no hw - hwf = atmos.heat_wave_max_length(tn, tx, thresh_tasmin='40 C', - thresh_tasmax='40 C') + hwf = atmos.heat_wave_max_length( + tn, tx, thresh_tasmin="40 C", thresh_tasmax="40 C" + ) np.testing.assert_allclose(hwf.values[:1], 0) class TestHeatWaveIndex: - def test_simple(self, tasmax_series): tx = np.zeros(366) tx[:10] = np.array([29, 31, 31, 31, 29, 31, 31, 31, 31, 31]) - tx = tasmax_series(tx + K2C, start='1/1/2000') - hwi = atmos.heat_wave_index(tx, freq='YS') + tx = tasmax_series(tx + K2C, start="1/1/2000") + hwi = atmos.heat_wave_index(tx, freq="YS") np.testing.assert_array_equal(hwi, [10]) def test_convert_units(self, tasmax_series): tx = np.zeros(366) tx[:10] = np.array([29, 31, 31, 31, 29, 31, 31, 31, 31, 31]) - tx = tasmax_series(tx, start='1/1/2000') - tx.attrs['units'] = 'C' - hwi = atmos.heat_wave_index(tx, freq='YS') + tx = tasmax_series(tx, start="1/1/2000") + tx.attrs["units"] = "C" + hwi = atmos.heat_wave_index(tx, freq="YS") np.testing.assert_array_equal(hwi, [10]) def test_nan_presence(self, tasmax_series): tx = np.zeros(366) tx[:10] = np.array([29, 31, 31, 31, 29, 31, 31, 31, 31, 31]) tx[-1] = np.nan - tx = tasmax_series(tx + K2C, start='1/1/2000') + tx = tasmax_series(tx + K2C, start="1/1/2000") - hwi = atmos.heat_wave_index(tx, freq='YS') + hwi = atmos.heat_wave_index(tx, freq="YS") np.testing.assert_array_equal(hwi, [np.nan]) class TestDailyFreezeThaw: - nc_tasmax = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmax_1990.nc') - nc_tasmin = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmin_1990.nc') + nc_tasmax = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmax_1990.nc" + ) + nc_tasmin = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmin_1990.nc" + ) def test_3d_data_with_nans(self): tasmax = xr.open_dataset(self.nc_tasmax).tasmax @@ -651,47 +711,47 @@ def test_3d_data_with_nans(self): # put a nan somewhere tasmin.values[180, 1, 0] = np.nan - frzthw = atmos.daily_freezethaw_cycles(tasmax, tasmin, freq='YS') + frzthw = atmos.daily_freezethaw_cycles(tasmax, tasmin, freq="YS") min1 = tasmin.values[:, 0, 0] max1 = tasmax.values[:, 0, 0] frzthw1 = ((min1 < K2C) * (max1 > K2C) * 1.0).sum() - assert (np.allclose(frzthw1, frzthw.values[0, 0, 0])) + assert np.allclose(frzthw1, frzthw.values[0, 0, 0]) - assert (np.isnan(frzthw.values[0, 1, 0])) + assert np.isnan(frzthw.values[0, 1, 0]) - assert (np.isnan(frzthw.values[0, -1, -1])) + assert np.isnan(frzthw.values[0, -1, -1]) def test_convert_units(self): tasmax = xr.open_dataset(self.nc_tasmax).tasmax tasmin = xr.open_dataset(self.nc_tasmin).tasmin tasmax.values -= K2C - tasmax.attrs['units'] = 'C' + tasmax.attrs["units"] = "C" tasmin.values -= K2C - tasmin.attrs['units'] = 'C' + tasmin.attrs["units"] = "C" # put a nan somewhere tasmin.values[180, 1, 0] = np.nan - frzthw = atmos.daily_freezethaw_cycles(tasmax, tasmin, freq='YS') + frzthw = atmos.daily_freezethaw_cycles(tasmax, tasmin, freq="YS") min1 = tasmin.values[:, 0, 0] max1 = tasmax.values[:, 0, 0] frzthw1 = ((min1 < 0) * (max1 > 0) * 1.0).sum() - assert (np.allclose(frzthw1, frzthw.values[0, 0, 0])) + assert np.allclose(frzthw1, frzthw.values[0, 0, 0]) - assert (np.isnan(frzthw.values[0, 1, 0])) + assert np.isnan(frzthw.values[0, 1, 0]) - assert (np.isnan(frzthw.values[0, -1, -1])) + assert np.isnan(frzthw.values[0, -1, -1]) class TestGrowingSeasonLength: def test_single_year(self, tas_series): a = np.zeros(366) + K2C - ts = tas_series(a, start='1/1/2000') + ts = tas_series(a, start="1/1/2000") tt = (ts.time.dt.month >= 5) & (ts.time.dt.month <= 8) offset = np.random.uniform(low=5.5, high=23, size=(tt.sum().values,)) ts[tt] = ts[tt] + offset @@ -703,8 +763,8 @@ def test_single_year(self, tas_series): def test_convert_units(self, tas_series): a = np.zeros(366) - ts = tas_series(a, start='1/1/2000') - ts.attrs['units'] = 'C' + ts = tas_series(a, start="1/1/2000") + ts.attrs["units"] = "C" tt = (ts.time.dt.month >= 5) & (ts.time.dt.month <= 8) offset = np.random.uniform(low=5.5, high=23, size=(tt.sum().values,)) ts[tt] = ts[tt] + offset @@ -716,8 +776,8 @@ def test_convert_units(self, tas_series): def test_nan_presence(self, tas_series): a = np.zeros(366) a[50] = np.nan - ts = tas_series(a, start='1/1/2000') - ts.attrs['units'] = 'C' + ts = tas_series(a, start="1/1/2000") + ts.attrs["units"] = "C" tt = (ts.time.dt.month >= 5) & (ts.time.dt.month <= 8) offset = np.random.uniform(low=5.5, high=23, size=(tt.sum().values,)) @@ -730,8 +790,8 @@ def test_nan_presence(self, tas_series): def test_multiyear(self, tas_series): a = np.zeros(366 * 10) - ts = tas_series(a, start='1/1/2000') - ts.attrs['units'] = 'C' + ts = tas_series(a, start="1/1/2000") + ts.attrs["units"] = "C" tt = (ts.time.dt.month >= 5) & (ts.time.dt.month <= 8) offset = np.random.uniform(low=5.5, high=23, size=(tt.sum().values,)) @@ -743,21 +803,23 @@ def test_multiyear(self, tas_series): class TestTnDaysBelow: - nc_file = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmin_1990.nc') + nc_file = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmin_1990.nc" + ) def test_3d_data_with_nans(self): # test with 3d data tas = xr.open_dataset(self.nc_file).tasmin tasC = xr.open_dataset(self.nc_file).tasmin tasC -= K2C - tasC.attrs['units'] = 'C' + tasC.attrs["units"] = "C" # put a nan somewhere tas.values[180, 1, 0] = np.nan tasC.values[180, 1, 0] = np.nan # compute with both skipna options thresh = 273.16 + -10 - fd = atmos.tn_days_below(tas, freq='YS') - fdC = atmos.tn_days_below(tasC, freq='YS') + fd = atmos.tn_days_below(tas, freq="YS") + fdC = atmos.tn_days_below(tasC, freq="YS") x1 = tas.values[:, 0, 0] @@ -765,29 +827,31 @@ def test_3d_data_with_nans(self): np.testing.assert_array_equal(fd, fdC) - assert (np.allclose(fd1, fd.values[0, 0, 0])) + assert np.allclose(fd1, fd.values[0, 0, 0]) - assert (np.isnan(fd.values[0, 1, 0])) + assert np.isnan(fd.values[0, 1, 0]) - assert (np.isnan(fd.values[0, -1, -1])) + assert np.isnan(fd.values[0, -1, -1]) class TestTxDaysAbove: - nc_file = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmax_1990.nc') + nc_file = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmax_1990.nc" + ) def test_3d_data_with_nans(self): # test with 3d data tas = xr.open_dataset(self.nc_file).tasmax tasC = xr.open_dataset(self.nc_file).tasmax tasC -= K2C - tasC.attrs['units'] = 'C' + tasC.attrs["units"] = "C" # put a nan somewhere tas.values[180, 1, 0] = np.nan tasC.values[180, 1, 0] = np.nan # compute with both skipna options thresh = 273.16 + 25 - fd = atmos.tx_days_above(tas, freq='YS') - fdC = atmos.tx_days_above(tasC, freq='YS') + fd = atmos.tx_days_above(tas, freq="YS") + fdC = atmos.tx_days_above(tasC, freq="YS") x1 = tas.values[:, 0, 0] @@ -795,29 +859,31 @@ def test_3d_data_with_nans(self): np.testing.assert_array_equal(fd, fdC) - assert (np.allclose(fd1, fd.values[0, 0, 0])) + assert np.allclose(fd1, fd.values[0, 0, 0]) - assert (np.isnan(fd.values[0, 1, 0])) + assert np.isnan(fd.values[0, 1, 0]) - assert (np.isnan(fd.values[0, -1, -1])) + assert np.isnan(fd.values[0, -1, -1]) class TestTropicalNights: - nc_file = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmin_1990.nc') + nc_file = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmin_1990.nc" + ) def test_3d_data_with_nans(self): # test with 3d data tas = xr.open_dataset(self.nc_file).tasmin tasC = xr.open_dataset(self.nc_file).tasmin tasC -= K2C - tasC.attrs['units'] = 'C' + tasC.attrs["units"] = "C" # put a nan somewhere tas.values[180, 1, 0] = np.nan tasC.values[180, 1, 0] = np.nan # compute with both skipna options thresh = 273.16 + 20 - out = atmos.tropical_nights(tas, freq='YS') - outC = atmos.tropical_nights(tasC, freq='YS') + out = atmos.tropical_nights(tas, freq="YS") + outC = atmos.tropical_nights(tasC, freq="YS") # fds = xci.frost_days(tasmin, thresh=thresh, freq='YS', skipna=True) x1 = tas.values[:, 0, 0] @@ -826,16 +892,20 @@ def test_3d_data_with_nans(self): np.testing.assert_array_equal(out, outC) - assert (np.allclose(out1, out.values[0, 0, 0])) + assert np.allclose(out1, out.values[0, 0, 0]) - assert (np.isnan(out.values[0, 1, 0])) + assert np.isnan(out.values[0, 1, 0]) - assert (np.isnan(out.values[0, -1, -1])) + assert np.isnan(out.values[0, -1, -1]) class TestTxTnDaysAbove: - nc_tasmax = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmax_1990.nc') - nc_tasmin = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmin_1990.nc') + nc_tasmax = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmax_1990.nc" + ) + nc_tasmin = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmin_1990.nc" + ) def test_3d_data_with_nans(self): tasmax = xr.open_dataset(self.nc_tasmax).tasmax @@ -844,46 +914,49 @@ def test_3d_data_with_nans(self): tasmaxC = xr.open_dataset(self.nc_tasmax).tasmax tasminC = xr.open_dataset(self.nc_tasmin).tasmin tasmaxC -= K2C - tasmaxC.attrs['units'] = 'C' + tasmaxC.attrs["units"] = "C" tasminC -= K2C - tasminC.attrs['units'] = 'C' + tasminC.attrs["units"] = "C" # put a nan somewhere tasmin.values[180, 1, 0] = np.nan tasminC.values[180, 1, 0] = np.nan - out = atmos.tx_tn_days_above(tasmin, tasmax, thresh_tasmax='25 C', thresh_tasmin='18 C') - outC = atmos.tx_tn_days_above(tasminC, tasmaxC, thresh_tasmax='25 C', thresh_tasmin='18 C') - np.testing.assert_array_equal(out, outC, ) + out = atmos.tx_tn_days_above( + tasmin, tasmax, thresh_tasmax="25 C", thresh_tasmin="18 C" + ) + outC = atmos.tx_tn_days_above( + tasminC, tasmaxC, thresh_tasmax="25 C", thresh_tasmin="18 C" + ) + np.testing.assert_array_equal(out, outC) min1 = tasmin.values[:, 53, 76] max1 = tasmax.values[:, 53, 76] out1 = ((min1 > (K2C + 18)) * (max1 > (K2C + 25)) * 1.0).sum() - assert (np.allclose(out1, out.values[0, 53, 76])) + assert np.allclose(out1, out.values[0, 53, 76]) - assert (np.isnan(out.values[0, 1, 0])) + assert np.isnan(out.values[0, 1, 0]) - assert (np.isnan(out.values[0, -1, -1])) + assert np.isnan(out.values[0, -1, -1]) class TestT90p: - def test_tg90p_simple(self, tas_series): i = 366 - arr = np.asarray(np.arange(i), 'float') - tas = tas_series(arr, start='1/1/2000') + arr = np.asarray(np.arange(i), "float") + tas = tas_series(arr, start="1/1/2000") tasC = tas.copy() tasC -= K2C - tasC.attrs['units'] = 'C' - t90 = percentile_doy(tas, per=.1) + tasC.attrs["units"] = "C" + t90 = percentile_doy(tas, per=0.1) # create cold spell in june tas[175:180] = 1 tasC[175:180] = 1 - K2C - out = atmos.tg90p(tas, t90, freq='MS') - outC = atmos.tg90p(tasC, t90, freq='MS') + out = atmos.tg90p(tas, t90, freq="MS") + outC = atmos.tg90p(tasC, t90, freq="MS") np.testing.assert_array_equal(out, outC) assert out[0] == 30 @@ -893,8 +966,8 @@ def test_tg90p_simple(self, tas_series): # nan treatment tas[33] = np.nan tasC[33] = np.nan - out = atmos.tg90p(tas, t90, freq='MS') - outC = atmos.tg90p(tasC, t90, freq='MS') + out = atmos.tg90p(tas, t90, freq="MS") + outC = atmos.tg90p(tasC, t90, freq="MS") np.testing.assert_array_equal(out, outC) assert out[0] == 30 @@ -903,18 +976,18 @@ def test_tg90p_simple(self, tas_series): def test_tn90p_simple(self, tasmin_series): i = 366 - arr = np.asarray(np.arange(i), 'float') - tas = tasmin_series(arr, start='1/1/2000') + arr = np.asarray(np.arange(i), "float") + tas = tasmin_series(arr, start="1/1/2000") tasC = tas.copy() tasC -= K2C - tasC.attrs['units'] = 'C' - t90 = percentile_doy(tas, per=.1) + tasC.attrs["units"] = "C" + t90 = percentile_doy(tas, per=0.1) # create cold spell in june tas[175:180] = 1 tasC[175:180] = 1 - K2C - out = atmos.tn90p(tas, t90, freq='MS') - outC = atmos.tn90p(tasC, t90, freq='MS') + out = atmos.tn90p(tas, t90, freq="MS") + outC = atmos.tn90p(tasC, t90, freq="MS") np.testing.assert_array_equal(out, outC) assert out[0] == 30 @@ -924,8 +997,8 @@ def test_tn90p_simple(self, tasmin_series): # nan treatment tas[33] = np.nan tasC[33] = np.nan - out = atmos.tn90p(tas, t90, freq='MS') - outC = atmos.tn90p(tasC, t90, freq='MS') + out = atmos.tn90p(tas, t90, freq="MS") + outC = atmos.tn90p(tasC, t90, freq="MS") np.testing.assert_array_equal(out, outC) assert out[0] == 30 @@ -934,18 +1007,18 @@ def test_tn90p_simple(self, tasmin_series): def test_tx90p_simple(self, tasmax_series): i = 366 - arr = np.asarray(np.arange(i), 'float') - tas = tasmax_series(arr, start='1/1/2000') + arr = np.asarray(np.arange(i), "float") + tas = tasmax_series(arr, start="1/1/2000") tasC = tas.copy() tasC -= K2C - tasC.attrs['units'] = 'C' - t90 = percentile_doy(tas, per=.1) + tasC.attrs["units"] = "C" + t90 = percentile_doy(tas, per=0.1) # create cold spell in june tas[175:180] = 1 tasC[175:180] = 1 - K2C - out = atmos.tx90p(tas, t90, freq='MS') - outC = atmos.tx90p(tasC, t90, freq='MS') + out = atmos.tx90p(tas, t90, freq="MS") + outC = atmos.tx90p(tasC, t90, freq="MS") np.testing.assert_array_equal(out, outC) assert out[0] == 30 @@ -955,8 +1028,8 @@ def test_tx90p_simple(self, tasmax_series): # nan treatment tas[33] = np.nan tasC[33] = np.nan - out = atmos.tx90p(tas, t90, freq='MS') - outC = atmos.tx90p(tasC, t90, freq='MS') + out = atmos.tx90p(tas, t90, freq="MS") + outC = atmos.tx90p(tasC, t90, freq="MS") np.testing.assert_array_equal(out, outC) assert out[0] == 30 @@ -965,21 +1038,20 @@ def test_tx90p_simple(self, tasmax_series): class TestT10p: - def test_tg10p_simple(self, tas_series): i = 366 - arr = np.asarray(np.arange(i), 'float') - tas = tas_series(arr, start='1/1/2000') + arr = np.asarray(np.arange(i), "float") + tas = tas_series(arr, start="1/1/2000") tasC = tas.copy() tasC -= K2C - tasC.attrs['units'] = 'C' - t10 = percentile_doy(tas, per=.1) + tasC.attrs["units"] = "C" + t10 = percentile_doy(tas, per=0.1) # create cold spell in june tas[175:180] = 1 tasC[175:180] = 1 - K2C - out = atmos.tg10p(tas, t10, freq='MS') - outC = atmos.tg10p(tasC, t10, freq='MS') + out = atmos.tg10p(tas, t10, freq="MS") + outC = atmos.tg10p(tasC, t10, freq="MS") np.testing.assert_array_equal(out, outC) @@ -989,8 +1061,8 @@ def test_tg10p_simple(self, tas_series): # nan treatment tas[33] = np.nan tasC[33] = np.nan - out = atmos.tg10p(tas, t10, freq='MS') - outC = atmos.tg10p(tasC, t10, freq='MS') + out = atmos.tg10p(tas, t10, freq="MS") + outC = atmos.tg10p(tasC, t10, freq="MS") np.testing.assert_array_equal(out, outC) assert out[0] == 1 @@ -999,18 +1071,18 @@ def test_tg10p_simple(self, tas_series): def test_tn10p_simple(self, tasmin_series): i = 366 - arr = np.asarray(np.arange(i), 'float') - tas = tasmin_series(arr, start='1/1/2000') + arr = np.asarray(np.arange(i), "float") + tas = tasmin_series(arr, start="1/1/2000") tasC = tas.copy() tasC -= K2C - tasC.attrs['units'] = 'C' - t10 = percentile_doy(tas, per=.1) + tasC.attrs["units"] = "C" + t10 = percentile_doy(tas, per=0.1) # create cold spell in june tas[175:180] = 1 tasC[175:180] = 1 - K2C - out = atmos.tn10p(tas, t10, freq='MS') - outC = atmos.tn10p(tasC, t10, freq='MS') + out = atmos.tn10p(tas, t10, freq="MS") + outC = atmos.tn10p(tasC, t10, freq="MS") np.testing.assert_array_equal(out, outC) assert out[0] == 1 @@ -1019,8 +1091,8 @@ def test_tn10p_simple(self, tasmin_series): # nan treatment tas[33] = np.nan tasC[33] = np.nan - out = atmos.tn10p(tas, t10, freq='MS') - outC = atmos.tn10p(tasC, t10, freq='MS') + out = atmos.tn10p(tas, t10, freq="MS") + outC = atmos.tn10p(tasC, t10, freq="MS") np.testing.assert_array_equal(out, outC) assert out[0] == 1 @@ -1029,18 +1101,18 @@ def test_tn10p_simple(self, tasmin_series): def test_tx10p_simple(self, tasmax_series): i = 366 - arr = np.asarray(np.arange(i), 'float') - tas = tasmax_series(arr, start='1/1/2000') + arr = np.asarray(np.arange(i), "float") + tas = tasmax_series(arr, start="1/1/2000") tasC = tas.copy() tasC -= K2C - tasC.attrs['units'] = 'C' - t10 = percentile_doy(tas, per=.1) + tasC.attrs["units"] = "C" + t10 = percentile_doy(tas, per=0.1) # create cold spell in june tas[175:180] = 1 tasC[175:180] = 1 - K2C - out = atmos.tx10p(tas, t10, freq='MS') - outC = atmos.tx10p(tasC, t10, freq='MS') + out = atmos.tx10p(tas, t10, freq="MS") + outC = atmos.tx10p(tasC, t10, freq="MS") np.testing.assert_array_equal(out, outC) assert out[0] == 1 @@ -1049,8 +1121,8 @@ def test_tx10p_simple(self, tasmax_series): # nan treatment tas[33] = np.nan tasC[33] = np.nan - out = atmos.tx10p(tas, t10, freq='MS') - outC = atmos.tx10p(tasC, t10, freq='MS') + out = atmos.tx10p(tas, t10, freq="MS") + outC = atmos.tx10p(tasC, t10, freq="MS") np.testing.assert_array_equal(out, outC) assert out[0] == 1 @@ -1059,5 +1131,7 @@ def test_tx10p_simple(self, tasmax_series): def test_freshet_start(tas_series): - out = atmos.freshet_start(tas_series(np.arange(-50, 350) + 274, start='1/1/2000'), freq='YS') + out = atmos.freshet_start( + tas_series(np.arange(-50, 350) + 274, start="1/1/2000"), freq="YS" + ) assert out[0] == 51 diff --git a/tests/test_utils.py b/tests/test_utils.py index e8e1f90d0..6345700bb 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,7 +1,5 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- - - # Tests for `xclim` package. # # We want to tests multiple things here: @@ -15,38 +13,47 @@ # For correctness, I think it would be useful to use a small dataset and run the original ICCLIM indicators on it, # saving the results in a reference netcdf dataset. We could then compare the hailstorm output to this reference as # a first line of defense. - - -import os import glob +import os + import cftime +import dask import numpy as np import pandas as pd import pytest import xarray as xr -import dask - +from xclim import __version__ +from xclim import atmos from xclim import ensembles from xclim import indices from xclim import subset from xclim import utils -from xclim import atmos -from xclim import __version__ -from xclim.utils import daily_downsampler, Indicator, format_kwargs, parse_doc, walk_map -from xclim.utils import infer_doy_max, adjust_doy_calendar, percentile_doy -from xclim.utils import units, pint2cfunits, units2pint -from xclim.testing.common import tas_series, pr_series +from xclim.testing.common import pr_series +from xclim.testing.common import tas_series +from xclim.utils import adjust_doy_calendar +from xclim.utils import daily_downsampler +from xclim.utils import format_kwargs +from xclim.utils import Indicator +from xclim.utils import infer_doy_max +from xclim.utils import parse_doc +from xclim.utils import percentile_doy +from xclim.utils import pint2cfunits +from xclim.utils import units +from xclim.utils import units2pint +from xclim.utils import walk_map TAS_SERIES = tas_series PR_SERIES = pr_series TESTS_HOME = os.path.abspath(os.path.dirname(__file__)) -TESTS_DATA = os.path.join(TESTS_HOME, 'testdata') +TESTS_DATA = os.path.join(TESTS_HOME, "testdata") class TestEnsembleStats: - nc_files_simple = glob.glob(os.path.join(TESTS_DATA, 'EnsembleStats', '*1950-2100*.nc')) - nc_files = glob.glob(os.path.join(TESTS_DATA, 'EnsembleStats', '*.nc')) + nc_files_simple = glob.glob( + os.path.join(TESTS_DATA, "EnsembleStats", "*1950-2100*.nc") + ) + nc_files = glob.glob(os.path.join(TESTS_DATA, "EnsembleStats", "*.nc")) def test_create_ensemble(self): ens = ensembles.create_ensemble(self.nc_files_simple) @@ -61,21 +68,27 @@ def test_create_unequal_times(self): def test_calc_perc(self): ens = ensembles.create_ensemble(self.nc_files_simple) out1 = ensembles.ensemble_percentiles(ens) - np.testing.assert_array_equal(np.percentile(ens['tg_mean'][:, 0, 5, 5], 10), out1['tg_mean_p10'][0, 5, 5]) - np.testing.assert_array_equal(np.percentile(ens['tg_mean'][:, 0, 5, 5], 50), out1['tg_mean_p50'][0, 5, 5]) - np.testing.assert_array_equal(np.percentile(ens['tg_mean'][:, 0, 5, 5], 90), out1['tg_mean_p90'][0, 5, 5]) - assert np.all(out1['tg_mean_p90'] > out1['tg_mean_p50']) - assert np.all(out1['tg_mean_p50'] > out1['tg_mean_p10']) + np.testing.assert_array_equal( + np.percentile(ens["tg_mean"][:, 0, 5, 5], 10), out1["tg_mean_p10"][0, 5, 5] + ) + np.testing.assert_array_equal( + np.percentile(ens["tg_mean"][:, 0, 5, 5], 50), out1["tg_mean_p50"][0, 5, 5] + ) + np.testing.assert_array_equal( + np.percentile(ens["tg_mean"][:, 0, 5, 5], 90), out1["tg_mean_p90"][0, 5, 5] + ) + assert np.all(out1["tg_mean_p90"] > out1["tg_mean_p50"]) + assert np.all(out1["tg_mean_p50"] > out1["tg_mean_p10"]) out1 = ensembles.ensemble_percentiles(ens, values=(25, 75)) - assert np.all(out1['tg_mean_p75'] > out1['tg_mean_p25']) + assert np.all(out1["tg_mean_p75"] > out1["tg_mean_p25"]) def test_calc_perc_blocks(self): ens = ensembles.create_ensemble(self.nc_files_simple) out1 = ensembles.ensemble_percentiles(ens) out2 = ensembles.ensemble_percentiles(ens, values=(10, 50, 90), time_block=10) - np.testing.assert_array_equal(out1['tg_mean_p10'], out2['tg_mean_p10']) - np.testing.assert_array_equal(out1['tg_mean_p50'], out2['tg_mean_p50']) - np.testing.assert_array_equal(out1['tg_mean_p90'], out2['tg_mean_p90']) + np.testing.assert_array_equal(out1["tg_mean_p10"], out2["tg_mean_p10"]) + np.testing.assert_array_equal(out1["tg_mean_p50"], out2["tg_mean_p50"]) + np.testing.assert_array_equal(out1["tg_mean_p90"], out2["tg_mean_p90"]) def test_calc_perc_nans(self): ens = ensembles.create_ensemble(self.nc_files_simple).load() @@ -83,32 +96,51 @@ def test_calc_perc_nans(self): ens.tg_mean[2, 0, 5, 5] = np.nan ens.tg_mean[2, 7, 5, 5] = np.nan out1 = ensembles.ensemble_percentiles(ens) - np.testing.assert_array_equal(np.percentile(ens['tg_mean'][:, 0, 5, 5], 10), np.nan) - np.testing.assert_array_equal(np.percentile(ens['tg_mean'][:, 7, 5, 5], 10), np.nan) - np.testing.assert_array_equal(np.nanpercentile(ens['tg_mean'][:, 0, 5, 5], 10), out1['tg_mean_p10'][0, 5, 5]) - np.testing.assert_array_equal(np.nanpercentile(ens['tg_mean'][:, 7, 5, 5], 10), out1['tg_mean_p10'][7, 5, 5]) - assert np.all(out1['tg_mean_p90'] > out1['tg_mean_p50']) - assert np.all(out1['tg_mean_p50'] > out1['tg_mean_p10']) + np.testing.assert_array_equal( + np.percentile(ens["tg_mean"][:, 0, 5, 5], 10), np.nan + ) + np.testing.assert_array_equal( + np.percentile(ens["tg_mean"][:, 7, 5, 5], 10), np.nan + ) + np.testing.assert_array_equal( + np.nanpercentile(ens["tg_mean"][:, 0, 5, 5], 10), + out1["tg_mean_p10"][0, 5, 5], + ) + np.testing.assert_array_equal( + np.nanpercentile(ens["tg_mean"][:, 7, 5, 5], 10), + out1["tg_mean_p10"][7, 5, 5], + ) + assert np.all(out1["tg_mean_p90"] > out1["tg_mean_p50"]) + assert np.all(out1["tg_mean_p50"] > out1["tg_mean_p10"]) def test_calc_mean_std_min_max(self): ens = ensembles.create_ensemble(self.nc_files_simple) out1 = ensembles.ensemble_mean_std_max_min(ens) - np.testing.assert_array_equal(ens['tg_mean'][:, 0, 5, 5].mean(dim='realization'), out1.tg_mean_mean[0, 5, 5]) - np.testing.assert_array_equal(ens['tg_mean'][:, 0, 5, 5].std(dim='realization'), out1.tg_mean_stdev[0, 5, 5]) - np.testing.assert_array_equal(ens['tg_mean'][:, 0, 5, 5].max(dim='realization'), out1.tg_mean_max[0, 5, 5]) - np.testing.assert_array_equal(ens['tg_mean'][:, 0, 5, 5].min(dim='realization'), out1.tg_mean_min[0, 5, 5]) + np.testing.assert_array_equal( + ens["tg_mean"][:, 0, 5, 5].mean(dim="realization"), + out1.tg_mean_mean[0, 5, 5], + ) + np.testing.assert_array_equal( + ens["tg_mean"][:, 0, 5, 5].std(dim="realization"), + out1.tg_mean_stdev[0, 5, 5], + ) + np.testing.assert_array_equal( + ens["tg_mean"][:, 0, 5, 5].max(dim="realization"), out1.tg_mean_max[0, 5, 5] + ) + np.testing.assert_array_equal( + ens["tg_mean"][:, 0, 5, 5].min(dim="realization"), out1.tg_mean_min[0, 5, 5] + ) class TestDailyDownsampler: - def test_std_calendar(self): # standard calendar # generate test DataArray - time_std = pd.date_range('2000-01-01', '2000-12-31', freq='D') - da_std = xr.DataArray(np.arange(time_std.size), coords=[time_std], dims='time') + time_std = pd.date_range("2000-01-01", "2000-12-31", freq="D") + da_std = xr.DataArray(np.arange(time_std.size), coords=[time_std], dims="time") - for freq in 'YS MS QS-DEC'.split(): + for freq in "YS MS QS-DEC".split(): resampler = da_std.resample(time=freq) grouper = daily_downsampler(da_std, freq=freq) @@ -117,26 +149,30 @@ def test_std_calendar(self): # add time coords to x2 and change dimension tags to time time1 = daily_downsampler(da_std.time, freq=freq).first() - x2.coords['time'] = ('tags', time1.values) - x2 = x2.swap_dims({'tags': 'time'}) - x2 = x2.sortby('time') + x2.coords["time"] = ("tags", time1.values) + x2 = x2.swap_dims({"tags": "time"}) + x2 = x2.sortby("time") # assert the values of resampler and grouper are the same - assert (np.allclose(x1.values, x2.values)) + assert np.allclose(x1.values, x2.values) @pytest.mark.skip def test_365_day(self): # 365_day calendar # generate test DataArray - units = 'days since 2000-01-01 00:00' - time_365 = cftime.num2date(np.arange(0, 1 * 365), units, '365_day') - da_365 = xr.DataArray(np.arange(time_365.size), coords=[time_365], dims='time', name='data') - units = 'days since 2001-01-01 00:00' - time_std = cftime.num2date(np.arange(0, 1 * 365), units, 'standard') - da_std = xr.DataArray(np.arange(time_std.size), coords=[time_std], dims='time', name='data') - - for freq in 'YS MS QS-DEC'.split(): + units = "days since 2000-01-01 00:00" + time_365 = cftime.num2date(np.arange(0, 1 * 365), units, "365_day") + da_365 = xr.DataArray( + np.arange(time_365.size), coords=[time_365], dims="time", name="data" + ) + units = "days since 2001-01-01 00:00" + time_std = cftime.num2date(np.arange(0, 1 * 365), units, "standard") + da_std = xr.DataArray( + np.arange(time_std.size), coords=[time_std], dims="time", name="data" + ) + + for freq in "YS MS QS-DEC".split(): resampler = da_std.resample(time=freq) grouper = daily_downsampler(da_365, freq=freq) @@ -145,51 +181,55 @@ def test_365_day(self): # add time coords to x2 and change dimension tags to time time1 = daily_downsampler(da_365.time, freq=freq).first() - x2.coords['time'] = ('tags', time1.values) - x2 = x2.swap_dims({'tags': 'time'}) - x2 = x2.sortby('time') + x2.coords["time"] = ("tags", time1.values) + x2 = x2.swap_dims({"tags": "time"}) + x2 = x2.sortby("time") # assert the values of resampler of non leap year with standard calendar # is identical to grouper - assert (np.allclose(x1.values, x2.values)) + assert np.allclose(x1.values, x2.values) def test_360_days(self): # # 360_day calendar # - units = 'days since 2000-01-01 00:00' - time_360 = cftime.num2date(np.arange(0, 360), units, '360_day') - da_360 = xr.DataArray(np.arange(1, time_360.size + 1), coords=[time_360], dims='time', name='data') + units = "days since 2000-01-01 00:00" + time_360 = cftime.num2date(np.arange(0, 360), units, "360_day") + da_360 = xr.DataArray( + np.arange(1, time_360.size + 1), coords=[time_360], dims="time", name="data" + ) - for freq in 'YS MS QS-DEC'.split(): + for freq in "YS MS QS-DEC".split(): grouper = daily_downsampler(da_360, freq=freq) x2 = grouper.mean() # add time coords to x2 and change dimension tags to time time1 = daily_downsampler(da_360.time, freq=freq).first() - x2.coords['time'] = ('tags', time1.values) - x2 = x2.swap_dims({'tags': 'time'}) - x2 = x2.sortby('time') + x2.coords["time"] = ("tags", time1.values) + x2 = x2.swap_dims({"tags": "time"}) + x2 = x2.sortby("time") # assert grouper values == expected values target_year = 180.5 target_month = [n * 30 + 15.5 for n in range(0, 12)] target_season = [30.5] + [(n - 1) * 30 + 15.5 for n in [4, 7, 10, 12]] - target = {'YS': target_year, 'MS': target_month, 'QS-DEC': target_season}[freq] - assert (np.allclose(x2.values, target)) + target = {"YS": target_year, "MS": target_month, "QS-DEC": target_season}[ + freq + ] + assert np.allclose(x2.values, target) class UniIndTemp(Indicator): - identifier = 'tmin' - var_name = 'tmin{thresh}' - units = 'K' - long_name = '{freq} mean surface temperature' - standard_name = '{freq} mean temperature' - cell_methods = 'time: mean within {freq}' + identifier = "tmin" + var_name = "tmin{thresh}" + units = "K" + long_name = "{freq} mean surface temperature" + standard_name = "{freq} mean temperature" + cell_methods = "time: mean within {freq}" @staticmethod - def compute(da, thresh=0., freq='YS'): + def compute(da, thresh=0.0, freq="YS"): """Docstring""" out = da out -= thresh @@ -197,9 +237,9 @@ def compute(da, thresh=0., freq='YS'): class UniIndPr(Indicator): - identifier = 'prmax' - units = 'mm/s' - context = 'hydro' + identifier = "prmax" + units = "mm/s" + context = "hydro" @staticmethod def compute(da, freq): @@ -208,25 +248,25 @@ def compute(da, freq): class TestIndicator: - def test_attrs(self, tas_series): import datetime as dt - a = tas_series(np.arange(360.)) + + a = tas_series(np.arange(360.0)) ind = UniIndTemp() - txm = ind(a, thresh=5, freq='YS') - assert txm.cell_methods == 'time: mean within days time: mean within years' - assert '{:%Y-%m-%d %H}'.format(dt.datetime.now()) in txm.attrs['history'] - assert "tmin(da, thresh=5, freq='YS')" in txm.attrs['history'] - assert 'xclim version: {}.'.format(__version__) in txm.attrs['history'] + txm = ind(a, thresh=5, freq="YS") + assert txm.cell_methods == "time: mean within days time: mean within years" + assert "{:%Y-%m-%d %H}".format(dt.datetime.now()) in txm.attrs["history"] + assert "tmin(da, thresh=5, freq='YS')" in txm.attrs["history"] + assert "xclim version: {}.".format(__version__) in txm.attrs["history"] assert txm.name == "tmin5" def test_temp_unit_conversion(self, tas_series): - a = tas_series(np.arange(360.)) + a = tas_series(np.arange(360.0)) ind = UniIndTemp() - txk = ind(a, freq='YS') + txk = ind(a, freq="YS") - ind.units = 'degC' - txc = ind(a, freq='YS') + ind.units = "degC" + txc = ind(a, freq="YS") np.testing.assert_array_almost_equal(txk, txc + 273.15) @@ -234,14 +274,28 @@ def test_json(self, pr_series): ind = UniIndPr() meta = ind.json() - expected = {'identifier', 'var_name', 'units', 'long_name', 'standard_name', 'cell_methods', 'keywords', - 'abstract', - 'parameters', 'description', 'history', 'references', 'comment', 'notes'} + expected = { + "identifier", + "var_name", + "units", + "long_name", + "standard_name", + "cell_methods", + "keywords", + "abstract", + "parameters", + "description", + "history", + "references", + "comment", + "notes", + } assert set(meta.keys()).issubset(expected) def test_signature(self): from inspect import signature + ind = UniIndTemp() assert signature(ind.compute) == signature(ind.__call__) @@ -250,10 +304,10 @@ def test_doc(self): assert ind.__call__.__doc__ == ind.compute.__doc__ def test_delayed(self): - fn = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmax_1990.nc') + fn = os.path.join(TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmax_1990.nc") # Load dataset as a dask array - ds = xr.open_dataset(fn, chunks={'time': 10}, cache=True) + ds = xr.open_dataset(fn, chunks={"time": 10}, cache=True) tx = UniIndTemp() txk = tx(ds.tasmax) @@ -262,8 +316,8 @@ def test_delayed(self): assert isinstance(txk.data, dask.array.core.Array) # Same with unit conversion - tx.required_units = ('C',) - tx.units = 'C' + tx.required_units = ("C",) + tx.units = "C" txc = tx(ds.tasmax) assert isinstance(txc.data, dask.array.core.Array) @@ -271,47 +325,52 @@ def test_delayed(self): def test_identifier(self): with pytest.warns(UserWarning): - UniIndPr(identifier='t_{}') + UniIndPr(identifier="t_{}") def test_formatting(self, pr_series): - out = atmos.wetdays(pr_series(np.arange(366)), thresh=1.0 * units.mm / units.day) - assert out.attrs['long_name'] == 'Number of wet days (precip >= 1 mm/day)' + out = atmos.wetdays( + pr_series(np.arange(366)), thresh=1.0 * units.mm / units.day + ) + assert out.attrs["long_name"] == "Number of wet days (precip >= 1 mm/day)" - out = atmos.wetdays(pr_series(np.arange(366)), thresh=1.5 * units.mm / units.day) - assert out.attrs['long_name'] == 'Number of wet days (precip >= 1.5 mm/day)' + out = atmos.wetdays( + pr_series(np.arange(366)), thresh=1.5 * units.mm / units.day + ) + assert out.attrs["long_name"] == "Number of wet days (precip >= 1.5 mm/day)" class TestKwargs: - def test_format_kwargs(self): - attrs = dict(standard_name='tx_min', long_name='Minimum of daily maximum temperature', - cell_methods='time: minimum within {freq}') + attrs = dict( + standard_name="tx_min", + long_name="Minimum of daily maximum temperature", + cell_methods="time: minimum within {freq}", + ) - format_kwargs(attrs, {'freq': 'YS'}) - assert attrs['cell_methods'] == 'time: minimum within years' + format_kwargs(attrs, {"freq": "YS"}) + assert attrs["cell_methods"] == "time: minimum within years" class TestParseDoc: - def test_simple(self): parse_doc(indices.tg_mean.__doc__) class TestPercentileDOY: - def test_simple(self, tas_series): - tas = tas_series(np.arange(365), start='1/1/2001') - p1 = percentile_doy(tas, window=5, per=.5) + tas = tas_series(np.arange(365), start="1/1/2001") + p1 = percentile_doy(tas, window=5, per=0.5) assert p1.sel(dayofyear=3).data == 2 - assert p1.attrs['units'] == 'K' + assert p1.attrs["units"] == "K" class TestAdjustDoyCalendar: - def test_360_to_366(self): - source = xr.DataArray(np.arange(360), coords=[np.arange(1, 361), ], dims='dayofyear') - time = pd.date_range('2000-01-01', '2001-12-31', freq='D') - target = xr.DataArray(np.arange(len(time)), coords=[time, ], dims='time') + source = xr.DataArray( + np.arange(360), coords=[np.arange(1, 361)], dims="dayofyear" + ) + time = pd.date_range("2000-01-01", "2001-12-31", freq="D") + target = xr.DataArray(np.arange(len(time)), coords=[time], dims="time") out = adjust_doy_calendar(source, target) @@ -319,122 +378,127 @@ def test_360_to_366(self): assert out.sel(dayofyear=366) == source.sel(dayofyear=360) def test_infer_doy_max(self): - fn = os.path.join(TESTS_DATA, 'CanESM2_365day', - 'pr_day_CanESM2_rcp85_r1i1p1_na10kgrid_qm-moving-50bins-detrend_2095.nc') + fn = os.path.join( + TESTS_DATA, + "CanESM2_365day", + "pr_day_CanESM2_rcp85_r1i1p1_na10kgrid_qm-moving-50bins-detrend_2095.nc", + ) with xr.open_dataset(fn) as ds: assert infer_doy_max(ds) == 365 - fn = os.path.join(TESTS_DATA, 'HadGEM2-CC_360day', - 'pr_day_HadGEM2-CC_rcp85_r1i1p1_na10kgrid_qm-moving-50bins-detrend_2095.nc') + fn = os.path.join( + TESTS_DATA, + "HadGEM2-CC_360day", + "pr_day_HadGEM2-CC_rcp85_r1i1p1_na10kgrid_qm-moving-50bins-detrend_2095.nc", + ) with xr.open_dataset(fn) as ds: assert infer_doy_max(ds) == 360 - fn = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_pr_1990.nc') + fn = os.path.join(TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_pr_1990.nc") with xr.open_dataset(fn) as ds: assert infer_doy_max(ds) == 366 class TestWalkMap: - def test_simple(self): - d = {'a': -1, 'b': {'c': -2}} + d = {"a": -1, "b": {"c": -2}} o = walk_map(d, lambda x: 0) - assert o['a'] == 0 - assert o['b']['c'] == 0 + assert o["a"] == 0 + assert o["b"]["c"] == 0 class TestUnits: - def test_temperature(self): assert 4 * units.d == 4 * units.day Q_ = units.Quantity assert Q_(1, units.C) == Q_(1, units.degC) def test_hydro(self): - with units.context('hydro'): + 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("mm/day") == q.to("mm/d") def test_lat_lon(self): assert 100 * units.degreeN == 100 * units.degree def test_pcic(self): - with units.context('hydro'): + with units.context("hydro"): fu = units.parse_units("kilogram / d / meter ** 2") tu = units.parse_units("mm/day") np.isclose(1 * fu, 1 * tu) def test_dimensionality(self): - 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') + 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") class TestConvertUnitsTo: - def test_deprecation(self, tas_series): with pytest.warns(FutureWarning): out = utils.convert_units_to(0, units.K) assert out == 273.15 - out = utils.convert_units_to(10, units.mm / units.day, context='hydro') + out = utils.convert_units_to(10, units.mm / units.day, context="hydro") assert out == 10 with pytest.warns(FutureWarning): - tas = tas_series(np.arange(365), start='1/1/2001') + tas = tas_series(np.arange(365), start="1/1/2001") out = indices.tx_days_above(tas, 30) - out1 = indices.tx_days_above(tas, '30 degC') - out2 = indices.tx_days_above(tas, '303.15 K') + out1 = indices.tx_days_above(tas, "30 degC") + out2 = indices.tx_days_above(tas, "303.15 K") np.testing.assert_array_equal(out, out1) np.testing.assert_array_equal(out, out2) class TestUnitConversion: - def test_pint2cfunits(self): - u = units('mm/d') - assert pint2cfunits(u.units) == 'mm d-1' + u = units("mm/d") + assert pint2cfunits(u.units) == "mm d-1" def test_cfunits2pint(self, pr_series): u = units2pint(pr_series([1, 2])) - assert (str(u)) == 'kilogram / meter ** 2 / second' - assert pint2cfunits(u) == 'kg m-2 s-1' + assert (str(u)) == "kilogram / meter ** 2 / second" + assert pint2cfunits(u) == "kg m-2 s-1" - u = units2pint('m^3 s-1') - assert str(u) == 'meter ** 3 / second' - assert pint2cfunits(u) == 'm^3 s-1' + u = units2pint("m^3 s-1") + assert str(u) == "meter ** 3 / second" + assert pint2cfunits(u) == "m^3 s-1" def test_pint_multiply(self, pr_series): a = pr_series([1, 2, 3]) out = utils.pint_multiply(a, 1 * units.days) assert out[0] == 1 * 60 * 60 * 24 - assert out.units == 'kg m-2' + assert out.units == "kg m-2" class TestCheckUnits: - def test_basic(self): - utils._check_units('mm/day', '[precipitation]') - utils._check_units('mm/s', '[precipitation]') - utils._check_units('kg/m2/s', '[precipitation]') - utils._check_units('kg/m2', '[length]') - utils._check_units('cms', '[discharge]') - utils._check_units('m3/s', '[discharge]') + utils._check_units("mm/day", "[precipitation]") + utils._check_units("mm/s", "[precipitation]") + utils._check_units("kg/m2/s", "[precipitation]") + utils._check_units("kg/m2", "[length]") + utils._check_units("cms", "[discharge]") + utils._check_units("m3/s", "[discharge]") with pytest.raises(AttributeError): - utils._check_units('mm', '[precipitation]') - utils._check_units('m3', '[discharge]') + utils._check_units("mm", "[precipitation]") + utils._check_units("m3", "[discharge]") class TestSubsetGridPoint: - nc_poslons = os.path.join(TESTS_DATA, 'cmip3', 'tas.sresb1.giss_model_e_r.run1.atm.da.nc') - nc_file = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmax_1990.nc') - nc_2dlonlat = os.path.join(TESTS_DATA, 'CRCM5', 'tasmax_bby_198406_se.nc') + nc_poslons = os.path.join( + TESTS_DATA, "cmip3", "tas.sresb1.giss_model_e_r.run1.atm.da.nc" + ) + nc_file = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmax_1990.nc" + ) + nc_2dlonlat = os.path.join(TESTS_DATA, "CRCM5", "tasmax_bby_198406_se.nc") def test_dataset(self): - da = xr.open_mfdataset([self.nc_file, self.nc_file.replace('tasmax', 'tasmin')]) + da = xr.open_mfdataset([self.nc_file, self.nc_file.replace("tasmax", "tasmin")]) lon = -72.4 lat = 46.1 out = subset.subset_gridpoint(da, lon=lon, lat=lat) @@ -451,11 +515,13 @@ def test_simple(self): np.testing.assert_almost_equal(out.lat, lat, 1) da = xr.open_dataset(self.nc_poslons).tas - da['lon'] -= 360 + da["lon"] -= 360 yr_st = 2050 yr_ed = 2059 - out = subset.subset_gridpoint(da, lon=lon, lat=lat, start_yr=yr_st, end_yr=yr_ed) + out = subset.subset_gridpoint( + da, lon=lon, lat=lat, start_yr=yr_st, end_yr=yr_ed + ) np.testing.assert_almost_equal(out.lon, lon, 1) np.testing.assert_almost_equal(out.lat, lat, 1) np.testing.assert_array_equal(len(np.unique(out.time.dt.year)), 10) @@ -489,56 +555,64 @@ def test_raise(self): class TestSubsetBbox: - nc_poslons = os.path.join(TESTS_DATA, 'cmip3', 'tas.sresb1.giss_model_e_r.run1.atm.da.nc') - nc_file = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmax_1990.nc') - nc_2dlonlat = os.path.join(TESTS_DATA, 'CRCM5', 'tasmax_bby_198406_se.nc') + nc_poslons = os.path.join( + TESTS_DATA, "cmip3", "tas.sresb1.giss_model_e_r.run1.atm.da.nc" + ) + nc_file = os.path.join( + TESTS_DATA, "NRCANdaily", "nrcan_canada_daily_tasmax_1990.nc" + ) + nc_2dlonlat = os.path.join(TESTS_DATA, "CRCM5", "tasmax_bby_198406_se.nc") lon = [-72.4, -60] lat = [42, 46.1] def test_dataset(self): - da = xr.open_mfdataset([self.nc_file, self.nc_file.replace('tasmax', 'tasmin')]) + da = xr.open_mfdataset([self.nc_file, self.nc_file.replace("tasmax", "tasmin")]) out = subset.subset_bbox(da, lon_bnds=self.lon, lat_bnds=self.lat) - assert (np.all(out.lon >= np.min(self.lon))) - assert (np.all(out.lon <= np.max(self.lon))) - assert (np.all(out.lat >= np.min(self.lat))) - assert (np.all(out.lat <= np.max(self.lat))) + assert np.all(out.lon >= np.min(self.lon)) + assert np.all(out.lon <= np.max(self.lon)) + assert np.all(out.lat >= np.min(self.lat)) + assert np.all(out.lat <= np.max(self.lat)) np.testing.assert_array_equal(out.tasmin.shape, out.tasmax.shape) def test_simple(self): da = xr.open_dataset(self.nc_file).tasmax out = subset.subset_bbox(da, lon_bnds=self.lon, lat_bnds=self.lat) - assert (np.all(out.lon >= np.min(self.lon))) - assert (np.all(out.lon <= np.max(self.lon))) - assert (np.all(out.lat >= np.min(self.lat))) - assert (np.all(out.lat <= np.max(self.lat))) + assert np.all(out.lon >= np.min(self.lon)) + assert np.all(out.lon <= np.max(self.lon)) + assert np.all(out.lat >= np.min(self.lat)) + assert np.all(out.lat <= np.max(self.lat)) da = xr.open_dataset(self.nc_poslons).tas - da['lon'] -= 360 + da["lon"] -= 360 yr_st = 2050 yr_ed = 2059 - out = subset.subset_bbox(da, lon_bnds=self.lon, lat_bnds=self.lat, start_yr=yr_st, end_yr=yr_ed) - assert (np.all(out.lon >= np.min(self.lon))) - assert (np.all(out.lon <= np.max(self.lon))) - assert (np.all(out.lat >= np.min(self.lat))) - assert (np.all(out.lat <= np.max(self.lat))) + out = subset.subset_bbox( + da, lon_bnds=self.lon, lat_bnds=self.lat, start_yr=yr_st, end_yr=yr_ed + ) + assert np.all(out.lon >= np.min(self.lon)) + assert np.all(out.lon <= np.max(self.lon)) + assert np.all(out.lat >= np.min(self.lat)) + assert np.all(out.lat <= np.max(self.lat)) np.testing.assert_array_equal(out.time.dt.year.max(), yr_ed) np.testing.assert_array_equal(out.time.dt.year.min(), yr_st) - out = subset.subset_bbox(da, lon_bnds=self.lon, lat_bnds=self.lat, start_yr=yr_st) - assert (np.all(out.lon >= np.min(self.lon))) - assert (np.all(out.lon <= np.max(self.lon))) - assert (np.all(out.lat >= np.min(self.lat))) - assert (np.all(out.lat <= np.max(self.lat))) + out = subset.subset_bbox( + da, lon_bnds=self.lon, lat_bnds=self.lat, start_yr=yr_st + ) + assert np.all(out.lon >= np.min(self.lon)) + assert np.all(out.lon <= np.max(self.lon)) + assert np.all(out.lat >= np.min(self.lat)) + assert np.all(out.lat <= np.max(self.lat)) np.testing.assert_array_equal(out.time.dt.year.max(), da.time.dt.year.max()) np.testing.assert_array_equal(out.time.dt.year.min(), yr_st) out = subset.subset_bbox(da, lon_bnds=self.lon, lat_bnds=self.lat, end_yr=yr_ed) - assert (np.all(out.lon >= np.min(self.lon))) - assert (np.all(out.lon <= np.max(self.lon))) - assert (np.all(out.lat >= np.min(self.lat))) - assert (np.all(out.lat <= np.max(self.lat))) + assert np.all(out.lon >= np.min(self.lon)) + assert np.all(out.lon <= np.max(self.lon)) + assert np.all(out.lat >= np.min(self.lat)) + assert np.all(out.lat <= np.max(self.lat)) np.testing.assert_array_equal(out.time.dt.year.max(), yr_ed) np.testing.assert_array_equal(out.time.dt.year.min(), da.time.dt.year.min()) @@ -552,32 +626,35 @@ def test_irregular(self): # Check only non-nans gridcells using mask mask1 = ~np.isnan(out.sel(time=out.time[0])) - assert (np.all(out.lon.values[mask1] >= np.min(self.lon))) - assert (np.all(out.lon.values[mask1] <= np.max(self.lon))) - assert (np.all(out.lat.values[mask1] >= np.min(self.lat))) - assert (np.all(out.lat.values[mask1] <= np.max(self.lat))) + assert np.all(out.lon.values[mask1] >= np.min(self.lon)) + assert np.all(out.lon.values[mask1] <= np.max(self.lon)) + assert np.all(out.lat.values[mask1] >= np.min(self.lat)) + assert np.all(out.lat.values[mask1] <= np.max(self.lat)) def test_positive_lons(self): da = xr.open_dataset(self.nc_poslons).tas out = subset.subset_bbox(da, lon_bnds=self.lon, lat_bnds=self.lat) - assert (np.all(out.lon >= np.min(np.asarray(self.lon) + 360))) - assert (np.all(out.lon <= np.max(np.asarray(self.lon) + 360))) - assert (np.all(out.lat >= np.min(self.lat))) - assert (np.all(out.lat <= np.max(self.lat))) + assert np.all(out.lon >= np.min(np.asarray(self.lon) + 360)) + assert np.all(out.lon <= np.max(np.asarray(self.lon) + 360)) + assert np.all(out.lat >= np.min(self.lat)) + assert np.all(out.lat <= np.max(self.lat)) - out = subset.subset_bbox(da, lon_bnds=np.array(self.lon) + 360, lat_bnds=self.lat) - assert (np.all(out.lon >= np.min(np.asarray(self.lon) + 360))) + out = subset.subset_bbox( + da, lon_bnds=np.array(self.lon) + 360, lat_bnds=self.lat + ) + assert np.all(out.lon >= np.min(np.asarray(self.lon) + 360)) def test_raise(self): da = xr.open_dataset(self.nc_poslons).tas with pytest.raises(ValueError): - subset.subset_bbox(da, lon_bnds=self.lon, lat_bnds=self.lat, start_yr=2056, end_yr=2055) + subset.subset_bbox( + da, lon_bnds=self.lon, lat_bnds=self.lat, start_yr=2056, end_yr=2055 + ) class TestThresholdCount: - def test_simple(self, tas_series): ts = tas_series(np.arange(365)) - out = utils.threshold_count(ts, '<', 50, 'Y') + out = utils.threshold_count(ts, "<", 50, "Y") np.testing.assert_array_equal(out, [50, 0]) diff --git a/tox.ini b/tox.ini index 0fdd3a248..cbedb011c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py35, py36, py37, py38-dev, flake8, docs +envlist = py35, py36, py37, py38-dev, black, docs [travis] python = @@ -7,14 +7,14 @@ python = 3.7: py37 3.6: py36 3.5: py35 - 3.6: flake8 + 3.6: black 3.6: docs -[testenv:flake8] +[testenv:black] basepython = python -deps = flake8 +deps = black commands = - flake8 xclim tests + black --check xclim tests [testenv:docs] deps = diff --git a/xclim/__init__.py b/xclim/__init__.py index 8b9244e62..2f742129c 100644 --- a/xclim/__init__.py +++ b/xclim/__init__.py @@ -1,20 +1,18 @@ # -*- coding: utf-8 -*- - """Top-level package for xclim.""" - +import sys from functools import partial from xclim import indices -import sys # from .stats import fit, test __author__ = """Travis Logan""" -__email__ = 'logan.travis@ouranos.ca' -__version__ = '0.10.2-beta' +__email__ = "logan.travis@ouranos.ca" +__version__ = "0.10.2-beta" -def build_module(name, objs, doc='', source=None, mode='ignore'): +def build_module(name, objs, doc="", source=None, mode="ignore"): """Create a module from imported objects. Parameters @@ -58,12 +56,15 @@ def build_module(name, objs, doc='', source=None, mode='ignore'): if module_mappings is None: msg = "{} has not been implemented.".format(obj) - if mode == 'raise': - raise NotImplementedError(msg) - elif mode == 'warn': + if mode == "ignore": + logging.info(msg) + elif mode == "warn": warnings.warn(msg) + elif mode == "raise": + raise NotImplementedError(msg) else: - logging.info(msg) + msg = "{} is not a valid missing object behaviour".format(mode) + raise AttributeError(msg) else: out.__dict__[key] = module_mappings @@ -77,7 +78,7 @@ def build_module(name, objs, doc='', source=None, mode='ignore'): return out -def __build_icclim(mode='warn'): +def __build_icclim(mode="warn"): # ['TG', 'TX', 'TN', 'TXx', 'TXn', 'TNx', 'TNn', 'SU', 'TR', 'CSU', 'GD4', 'FD', 'CFD', 'GSL', # 'ID', 'HD17', 'CDD', 'CWD', 'PRCPTOT', 'RR1', 'SDII', 'R10mm', 'R20mm', 'RX1day', 'RX5day', @@ -86,56 +87,57 @@ def __build_icclim(mode='warn'): # Use partials to specify default value ? # TODO : Complete mappings for ICCLIM indices - mapping = {'TG': indices.tg_mean, - 'TX': indices.tx_mean, - 'TN': indices.tn_mean, - 'TG90p': indices.tg90p, - 'TG10p': indices.tg10p, - 'TGx': indices.tg_max, - 'TGn': indices.tg_min, - 'TX90p': indices.tx90p, - 'TX10p': indices.tx10p, - 'TXx': indices.tx_max, - 'TXn': indices.tx_min, - 'TN90p': indices.tn90p, - 'TN10p': indices.tn10p, - 'TNx': indices.tn_max, - 'TNn': indices.tn_min, - 'SU': indices.tx_days_above, - 'TR': indices.tropical_nights, - # 'CSU': None, - 'GD4': partial(indices.growing_degree_days, thresh=4), - 'FD': indices.frost_days, - 'CFD': indices.consecutive_frost_days, - 'GSL': indices.growing_season_length, - 'ID': indices.ice_days, - 'HD17': indices.heating_degree_days, - 'CDD': indices.maximum_consecutive_dry_days, - 'CWD': indices.maximum_consecutive_wet_days, - 'PRCPTOT': indices.precip_accumulation, - 'RR1': indices.wetdays, - # 'SDII': None, - 'ETR': indices.extreme_temperature_range, - 'DTR': indices.daily_temperature_range, - 'vDTR': indices.daily_temperature_range_variability, - # 'R10mm': None, - # 'R20mm': None, - # 'RX1day': None, - # 'RX5day': None, - # 'R75p': None, - # 'R75pTOT':None, - # 'R95p': None, - # 'R95pTOT': None, - # 'R99p': None, - # 'R99pTOT': None, - # 'SD': None, - # 'SD1': None, - # 'SD5cm': None, - # 'SD50cm': None, - } - - mod = build_module('xclim.icclim', mapping, doc="""ICCLIM indices""", mode=mode) + mapping = { + "TG": indices.tg_mean, + "TX": indices.tx_mean, + "TN": indices.tn_mean, + "TG90p": indices.tg90p, + "TG10p": indices.tg10p, + "TGx": indices.tg_max, + "TGn": indices.tg_min, + "TX90p": indices.tx90p, + "TX10p": indices.tx10p, + "TXx": indices.tx_max, + "TXn": indices.tx_min, + "TN90p": indices.tn90p, + "TN10p": indices.tn10p, + "TNx": indices.tn_max, + "TNn": indices.tn_min, + "SU": indices.tx_days_above, + "TR": indices.tropical_nights, + # 'CSU': None, + "GD4": partial(indices.growing_degree_days, thresh=4), + "FD": indices.frost_days, + "CFD": indices.consecutive_frost_days, + "GSL": indices.growing_season_length, + "ID": indices.ice_days, + "HD17": indices.heating_degree_days, + "CDD": indices.maximum_consecutive_dry_days, + "CWD": indices.maximum_consecutive_wet_days, + "PRCPTOT": indices.precip_accumulation, + "RR1": indices.wetdays, + # 'SDII': None, + "ETR": indices.extreme_temperature_range, + "DTR": indices.daily_temperature_range, + "vDTR": indices.daily_temperature_range_variability, + # 'R10mm': None, + # 'R20mm': None, + # 'RX1day': None, + # 'RX5day': None, + # 'R75p': None, + # 'R75pTOT':None, + # 'R95p': None, + # 'R95pTOT': None, + # 'R99p': None, + # 'R99pTOT': None, + # 'SD': None, + # 'SD1': None, + # 'SD5cm': None, + # 'SD50cm': None, + } + + mod = build_module("xclim.icclim", mapping, doc="""ICCLIM indices""", mode=mode) return mod -icclim = __build_icclim('ignore') +ICCLIM = __build_icclim("ignore") diff --git a/xclim/atmos/_precip.py b/xclim/atmos/_precip.py index 8fb6396de..7ca25aec6 100644 --- a/xclim/atmos/_precip.py +++ b/xclim/atmos/_precip.py @@ -1,100 +1,115 @@ # -*- coding: utf-8 -*- - from xclim import indices -from xclim.utils import Indicator, Indicator2D +from xclim.utils import Indicator +from xclim.utils import Indicator2D -__all__ = ['rain_on_frozen_ground_days', 'max_1day_precipitation_amount', - 'max_n_day_precipitation_amount', 'wetdays', 'maximum_consecutive_dry_days', 'maximum_consecutive_wet_days', - 'daily_pr_intensity', 'precip_accumulation'] +__all__ = [ + "rain_on_frozen_ground_days", + "max_1day_precipitation_amount", + "max_n_day_precipitation_amount", + "wetdays", + "maximum_consecutive_dry_days", + "maximum_consecutive_wet_days", + "daily_pr_intensity", + "precip_accumulation", +] class Pr(Indicator): - context = 'hydro' + context = "hydro" class PrTas(Indicator2D): - context = 'hydro' + context = "hydro" -rain_on_frozen_ground_days = PrTas(identifier='rain_frzgr', - units='days', - standard_name='number_of_days_with_lwe_thickness_of_' - 'precipitation_amount_above_threshold', - long_name='Number of rain on frozen ground days', - description="{freq} number of days with rain above {thresh} " - "after a series of seven days " - "with average daily temperature below 0℃. " - "Precipitation is assumed to be rain when the" - "daily average temperature is above 0℃.", - cell_methods='', - compute=indices.rain_on_frozen_ground_days, - ) +rain_on_frozen_ground_days = PrTas( + identifier="rain_frzgr", + units="days", + standard_name="number_of_days_with_lwe_thickness_of_" + "precipitation_amount_above_threshold", + long_name="Number of rain on frozen ground days", + description="{freq} number of days with rain above {thresh} " + "after a series of seven days " + "with average daily temperature below 0℃. " + "Precipitation is assumed to be rain when the" + "daily average temperature is above 0℃.", + cell_methods="", + compute=indices.rain_on_frozen_ground_days, +) -max_1day_precipitation_amount = Pr(identifier='rx1day', - units='mm/day', - standard_name='lwe_thickness_of_precipitation_amount', - long_name='maximum 1-day total precipitation', - description="{freq} maximum 1-day total precipitation", - cellmethods='time: sum within days time: maximum over days', - compute=indices.max_1day_precipitation_amount, - ) +max_1day_precipitation_amount = Pr( + identifier="rx1day", + units="mm/day", + standard_name="lwe_thickness_of_precipitation_amount", + long_name="maximum 1-day total precipitation", + description="{freq} maximum 1-day total precipitation", + cellmethods="time: sum within days time: maximum over days", + compute=indices.max_1day_precipitation_amount, +) -max_n_day_precipitation_amount = Pr(identifier='max_n_day_precipitation_amount', - var_name='rx{window}day', - units='mm', - standard_name='lwe_thickness_of_precipitation_amount', - long_name='maximum {window}-day total precipitation', - description="{freq} maximum {window}-day total precipitation.", - cellmethods='time: sum within days time: maximum over days', - compute=indices.max_n_day_precipitation_amount, - ) +max_n_day_precipitation_amount = Pr( + identifier="max_n_day_precipitation_amount", + var_name="rx{window}day", + units="mm", + standard_name="lwe_thickness_of_precipitation_amount", + long_name="maximum {window}-day total precipitation", + description="{freq} maximum {window}-day total precipitation.", + cellmethods="time: sum within days time: maximum over days", + compute=indices.max_n_day_precipitation_amount, +) -wetdays = Pr(identifier='wetdays', - units='days', - standard_name='number_of_days_with_lwe_thickness_of_precipitation_amount_at_or_above_threshold', - long_name='Number of wet days (precip >= {thresh})', - description='{freq} number of days with daily precipitation over {thresh}.', - cell_methods='time: sum within days time: sum over days', - compute=indices.wetdays, - ) +wetdays = Pr( + identifier="wetdays", + units="days", + standard_name="number_of_days_with_lwe_thickness_of_precipitation_amount_at_or_above_threshold", + long_name="Number of wet days (precip >= {thresh})", + description="{freq} number of days with daily precipitation over {thresh}.", + cell_methods="time: sum within days time: sum over days", + compute=indices.wetdays, +) -maximum_consecutive_wet_days = Pr(identifier='cwd', - units='days', - standard_name='number_of_days_with_lwe_thickness_of_' - 'precipitation_amount_at_or_above_threshold', - long_name='Maximum consecutive wet days (Precip >= {thresh})', - description='{freq} maximum number of days with daily ' - 'precipitation over {thresh}.', - cell_methods='time: sum within days time: sum over days', - compute=indices.maximum_consecutive_wet_days, - ) +maximum_consecutive_wet_days = Pr( + identifier="cwd", + units="days", + standard_name="number_of_days_with_lwe_thickness_of_" + "precipitation_amount_at_or_above_threshold", + long_name="Maximum consecutive wet days (Precip >= {thresh})", + description="{freq} maximum number of days with daily " + "precipitation over {thresh}.", + cell_methods="time: sum within days time: sum over days", + compute=indices.maximum_consecutive_wet_days, +) -maximum_consecutive_dry_days = Pr(identifier='cdd', - units='days', - standard_name='number_of_days_with_lwe_thickness_of_' - 'precipitation_amount_below_threshold', - long_name='Maximum consecutive dry days (Precip < {thresh})', - description='{freq} maximum number of days with daily ' - 'precipitation below {thresh}.', - cell_methods='time: sum within days time: sum over days', - compute=indices.maximum_consecutive_dry_days, - ) +maximum_consecutive_dry_days = Pr( + identifier="cdd", + units="days", + standard_name="number_of_days_with_lwe_thickness_of_" + "precipitation_amount_below_threshold", + long_name="Maximum consecutive dry days (Precip < {thresh})", + description="{freq} maximum number of days with daily " + "precipitation below {thresh}.", + cell_methods="time: sum within days time: sum over days", + compute=indices.maximum_consecutive_dry_days, +) -daily_pr_intensity = Pr(identifier='sdii', - units='mm/day', - standard_name='lwe_thickness_of_precipitation_amount', - long_name='Average precipitation during Wet Days (SDII)', - description="{freq} Simple Daily Intensity Index (SDII) : {freq} average precipitation " - "for days with daily precipitation over {thresh}.", - cell_methods='', - compute=indices.daily_pr_intensity, - ) +daily_pr_intensity = Pr( + identifier="sdii", + units="mm/day", + standard_name="lwe_thickness_of_precipitation_amount", + long_name="Average precipitation during Wet Days (SDII)", + description="{freq} Simple Daily Intensity Index (SDII) : {freq} average precipitation " + "for days with daily precipitation over {thresh}.", + cell_methods="", + compute=indices.daily_pr_intensity, +) -precip_accumulation = Pr(identifier='prcptot', - units='mm', - standard_name='lwe_thickness_of_precipitation_amount', - long_name='Total precipitation', - description='{freq} total precipitation.', - cell_methods='time: sum within days time: sum over days', - compute=indices.precip_accumulation - ) +precip_accumulation = Pr( + identifier="prcptot", + units="mm", + standard_name="lwe_thickness_of_precipitation_amount", + long_name="Total precipitation", + description="{freq} total precipitation.", + cell_methods="time: sum within days time: sum over days", + compute=indices.precip_accumulation, +) diff --git a/xclim/atmos/_temperature.py b/xclim/atmos/_temperature.py index 20a739735..d13e8894d 100644 --- a/xclim/atmos/_temperature.py +++ b/xclim/atmos/_temperature.py @@ -1,17 +1,47 @@ # -*- coding: utf-8 -*- import abc -from xclim import indices from xclim import checks -from xclim.utils import Indicator, Indicator2D - -__all__ = ['tn_days_below', 'tx_days_above', 'tx_tn_days_above', - 'heat_wave_frequency', 'heat_wave_max_length', 'heat_wave_index', 'tg_mean', 'tg10p', 'tg90p', 'tn_min', - 'tn_max', 'tn_mean', 'tn10p', 'tn90p', 'tx_min', 'tx_max', 'tx_mean', 'tx10p', 'tx90p', - 'daily_temperature_range', 'daily_temperature_range_variability', 'extreme_temperature_range', - 'cold_spell_duration_index', 'cold_spell_days', 'daily_freezethaw_cycles', 'cooling_degree_days', - 'heating_degree_days', 'growing_degree_days', 'freshet_start', 'frost_days', 'ice_days', - 'consecutive_frost_days', 'growing_season_length', 'tropical_nights'] +from xclim import indices +from xclim.utils import Indicator +from xclim.utils import Indicator2D + +__all__ = [ + "tn_days_below", + "tx_days_above", + "tx_tn_days_above", + "heat_wave_frequency", + "heat_wave_max_length", + "heat_wave_index", + "tg_mean", + "tg10p", + "tg90p", + "tn_min", + "tn_max", + "tn_mean", + "tn10p", + "tn90p", + "tx_min", + "tx_max", + "tx_mean", + "tx10p", + "tx90p", + "daily_temperature_range", + "daily_temperature_range_variability", + "extreme_temperature_range", + "cold_spell_duration_index", + "cold_spell_days", + "daily_freezethaw_cycles", + "cooling_degree_days", + "heating_degree_days", + "growing_degree_days", + "freshet_start", + "frost_days", + "ice_days", + "consecutive_frost_days", + "growing_season_length", + "tropical_nights", +] # TODO: Should we reference the standard vocabulary we're using ? @@ -22,8 +52,8 @@ class Tas(Indicator): """Class for univariate indices using mean daily temperature as the input.""" def cfprobe(self, da): - checks.check_valid(da, 'cell_methods', 'time: mean within days') - checks.check_valid(da, 'standard_name', 'air_temperature') + checks.check_valid(da, "cell_methods", "time: mean within days") + checks.check_valid(da, "standard_name", "air_temperature") @abc.abstractmethod def compute(*args, **kwds): @@ -34,8 +64,8 @@ class Tasmin(Indicator): """Class for univariate indices using min daily temperature as the input.""" def cfprobe(self, da): - checks.check_valid(da, 'cell_methods', 'time: minimum within days') - checks.check_valid(da, 'standard_name', 'air_temperature') + checks.check_valid(da, "cell_methods", "time: minimum within days") + checks.check_valid(da, "standard_name", "air_temperature") @abc.abstractmethod def compute(*args, **kwds): @@ -46,8 +76,8 @@ class Tasmax(Indicator): """Class for univariate indices using max daily temperature as the input.""" def cfprobe(self, da): - checks.check_valid(da, 'cell_methods', 'time: maximum within days') - checks.check_valid(da, 'standard_name', 'air_temperature') + checks.check_valid(da, "cell_methods", "time: maximum within days") + checks.check_valid(da, "standard_name", "air_temperature") @abc.abstractmethod def compute(*args, **kwds): @@ -55,364 +85,396 @@ def compute(*args, **kwds): class TasminTasmax(Indicator2D): - def cfprobe(self, dan, dax): for da in (dan, dax): - checks.check_valid(da, 'cell_methods', 'time: maximum within days') - checks.check_valid(da, 'standard_name', 'air_temperature') + checks.check_valid(da, "cell_methods", "time: maximum within days") + checks.check_valid(da, "standard_name", "air_temperature") @abc.abstractmethod def compute(*args, **kwds): """The function computing the indicator.""" -tn_days_below = Tasmin(identifier='tn_days_below', - units='days', - standard_name='number_of_days_with_air_temperature_below_threshold', - long_name='Number of days with Tmin < {thresh}', - description="{freq} number of days where daily minimum temperature is below {thresh}", - cell_methods='time: minimum within days time: sum over days', - compute=indices.tn_days_below, - ) - -tx_days_above = Tasmax(identifier='tx_days_above', - units='days', - standard_name='number_of_days_with_air_temperature_above_threshold', - long_name='Number of days with Tmax > {thresh}', - description="{freq} number of days where daily maximum temperature exceeds {thresh}", - cell_methods='time: maximum within days time: sum over days', - compute=indices.tx_days_above, - ) - -tx_tn_days_above = TasminTasmax(identifier='tx_tn_days_above', - units='days', - standard_name='number_of_days_with_air_temperature_above_threshold', - long_name='Number of days with Tmax > {thresh_tasmax} and Tmin > {thresh_tasmin}', - description="{freq} number of days where daily maximum temperature exceeds" - " {thresh_tasmax} and minimum temperature exceeds {thresh_tasmin}", - cell_methods='', - compute=indices.tx_tn_days_above, - ) - -heat_wave_frequency = TasminTasmax(identifier='heat_wave_frequency', - units='', - standard_name='heat_wave_events', - long_name='Number of heat wave events (Tmin > {thresh_tasmin}' - 'and Tmax > {thresh_tasmax} for >= {window} days)', - description="{freq} number of heat wave events over a given period. " - "An event occurs when the minimum and maximum daily " - "temperature both exceeds specific thresholds : " - "(Tmin > {thresh_tasmin} and Tmax > {thresh_tasmax}) " - "over a minimum number of days ({window}).", - cell_methods='', - keywords="health,", - compute=indices.heat_wave_frequency, - ) - -heat_wave_max_length = TasminTasmax(identifier='heat_wave_max_length', - units='days', - standard_name='spell_length_of_days_with_air_temperature_above_threshold', - long_name='Maximum length of heat wave events (Tmin > {thresh_tasmin}' - 'and Tmax > {thresh_tasmax} for >= {window} days)', - description="{freq} maximum length of heat wave events occuring in a given period." - "An event occurs when the minimum and maximum daily " - "temperature both exceeds specific thresholds " - "(Tmin > {thresh_tasmin} and Tmax > {thresh_tasmax}) over " - "a minimum number of days ({window}).", - cell_methods='', - keywords="health,", - compute=indices.heat_wave_max_length, - ) - -heat_wave_index = Tasmax(identifier='heat_wave_index', - units='days', - standard_name='heat_wave_index', - long_name='Number of days that are part of a heatwave', - description='{freq} number of days that are part of a heatwave, ' - 'defined as five or more consecutive days over {thresh}.', - cell_methods='', - compute=indices.heat_wave_index, - ) - -tg_mean = Tas(identifier='tg_mean', - units='K', - standard_name="air_temperature", - long_name="Mean daily mean temperature", - description="{freq} mean of daily mean temperature.", - cell_methods='time: mean within days time: mean over days', - compute=indices.tg_mean, ) - -tx_mean = Tasmax(identifier='tx_mean', - units='K', - standard_name='air_temperature', - long_name='Mean daily maximum temperature', - description='{freq} mean of daily maximum temperature.', - cell_methods='time: maximum within days time: mean over days', - compute=indices.tx_mean, - ) - -tx_max = Tasmax(identifier='tx_max', - units='K', - standard_name='air_temperature', - long_name='Maximum daily maximum temperature', - description='{freq} maximum of daily maximum temperature.', - cell_methods='time: maximum within days time: maximum over days', - compute=indices.tx_max, - ) - -tx_min = Tasmax(identifier='tx_min', - units='K', - standard_name='air_temperature', - long_name='Minimum daily maximum temperature', - description='{freq} minimum of daily maximum temperature.', - cell_methods='time: maximum within days time: minimum over days', - compute=indices.tx_min, - ) - -tn_mean = Tasmin(identifier='tn_mean', - units='K', - standard_name='air_temperature', - long_name='Mean daily minimum temperature', - description='{freq} mean of daily minimum temperature.', - cell_methods='time: minimum within days time: mean over days', - compute=indices.tn_mean, - ) - -tn_max = Tasmin(identifier='tn_max', - units='K', - standard_name='air_temperature', - long_name='Maximum daily minimum temperature', - description='{freq} maximum of daily minimum temperature.', - cell_methods='time: minimum within days time: maximum over days', - compute=indices.tn_max, - ) - -tn_min = Tasmin(identifier='tn_min', - units='K', - standard_name='air_temperature', - long_name='Minimum daily minimum temperature', - description='{freq} minimum of daily minimum temperature.', - cell_methods='time: minimum within days time: minimum over days', - compute=indices.tn_min, - ) - -daily_temperature_range = TasminTasmax(identifier='dtr', - units='K', - standard_name='air_temperature', - long_name='Mean Diurnal Temperature Range', - description='{freq} mean diurnal temperature range', - cell_methods='time range within days time: mean over days', - compute=indices.daily_temperature_range, - ) - -daily_temperature_range_variability = TasminTasmax(identifier='dtrvar', - units='K', - standard_name='air_temperature', - long_name='Mean Diurnal Temperature Range Variability', - description='{freq} mean diurnal temparature range variability (' - 'defined as the average day-to-day variation ' - 'in daily temperature range ' - 'for the given time period)', - cell_methods='time range within days time: difference ' - 'over days time: mean over days', - compute=indices.daily_temperature_range_variability, - ) - -extreme_temperature_range = TasminTasmax(identifier='etr', - units='K', - standard_name='air_temperature', - long_name='Intra-period Extreme Temperature Range', - - description='{freq} range between the maximum of daily max temperature ' - '(tx_max) and the minimum of daily min temperature (tn_min)', - compute=indices.extreme_temperature_range, - ) - -cold_spell_duration_index = Tasmin(identifier='cold_spell_duration_index', - var_name='csdi_{window}', - units='days', - standard_name='cold_spell_duration_index', - long_name='Cold Spell Duration Index, count of days with at ' - 'least {window} consecutive days when Tmin < 10th percentile', - description='{freq} number of days with at least {window} consecutive days' - ' where the daily minimum temperature is below the 10th ' - 'percentile. The 10th percentile should be computed for ' - 'a 5-day window centred on each calendar day in the 1961-1990 period', - cell_methods='', - compute=indices.cold_spell_duration_index, - ) - -cold_spell_days = Tas(identifier='cold_spell_days', - units='days', - standard_name='cold_spell_days', - long_name='cold spell index', - description='{freq} number of days that are part of a cold spell, defined as {window} ' - 'or more consecutive days with mean daily ' - 'temperature below {thresh}.', - cell_methods='', - compute=indices.cold_spell_days, - ) - -daily_freezethaw_cycles = TasminTasmax(identifier='dlyfrzthw', - units='days', - standard_name='daily_freezethaw_cycles', - long_name='daily freezethaw cycles', - description='{freq} number of days with a diurnal freeze-thaw cycle ' - ': Tmax > 0℃ and Tmin < 0℃.', - cell_methods='', - compute=indices.daily_freezethaw_cycles, - ) - -cooling_degree_days = Tas(identifier='cooling_degree_days', - units='K days', - standard_name='integral_of_air_temperature_excess_wrt_time', - long_name='Cooling Degree Days (Tmean > {thresh})', - description='{freq} cooling degree days above {thresh}.', - cell_methods='time: mean within days time: sum over days', - compute=indices.cooling_degree_days, - ) - -heating_degree_days = Tas(identifier='heating_degree_days', - units='K days', - standard_name='integral_of_air_temperature_deficit_wrt_time', - long_name='Heating Degree Days (Tmean < {thresh})', - description='{freq} heating degree days below {thresh}.', - cell_methods='time: mean within days time: sum over days', - compute=indices.heating_degree_days, - ) - -growing_degree_days = Tas(identifier='growing_degree_days', - units='K days', - standard_name='integral_of_air_temperature_excess_wrt_time', - long_name='growing degree days above {thresh}', - description='{freq} growing degree days above {thresh}', - cell_methods='time: mean within days time: sum over days', - compute=indices.growing_degree_days, - ) - -freshet_start = Tas(identifier='freshet_start', - units='', - standard_name='day_of_year', - long_name="Day of year of spring freshet start", - description="Day of year of spring freshet start, defined as the first day a temperature " - "threshold of {thresh} is exceeded for at least {window} days.", - compute=indices.freshet_start) - -frost_days = Tasmin(identifier='frost_days', - units='days', - standard_name='days_with_air_temperature_below_threshold', - long_name='Number of Frost Days (Tmin < 0C)', - description='{freq} number of days with minimum daily ' - 'temperature below 0℃.', - cell_methods='time: minimum within days time: sum over days', - compute=indices.frost_days, - ) - -ice_days = Tasmax(identifier='ice_days', - standard_name='days_with_air_temperature_below_threshold', - units='days', - long_name='Number of Ice Days (Tmax < 0℃)', - description='{freq} number of days with maximum daily ' - 'temperature below 0℃', - cell_methods='time: maximum within days time: sum over days', - compute=indices.ice_days, - ) - -consecutive_frost_days = Tasmin(identifier='consecutive_frost_days', - units='days', - standard_name='spell_length_of_days_with_air_temperature_below_threshold', - long_name='Maximum number of consecutive days with Tmin < 0C', - description='{freq} maximum number of consecutive days with ' - 'minimum daily temperature below 0℃', - cell_methods='time: min within days time: maximum over days', - compute=indices.consecutive_frost_days, - ) - -growing_season_length = Tas(identifier='growing_season_length', - units='days', - standard_name='growing_season_length', - long_name='ETCCDI Growing Season Length (Tmean > {thresh})', - description='{freq} number of days between the first occurrence of at least ' - 'six consecutive days with mean daily temperature over {thresh} and ' - 'the first occurrence of at least {window} consecutive days with ' - 'mean daily temperature below {thresh} after July 1st in the northern ' - 'hemisphere and January 1st in the southern hemisphere', - cell_methods='', - compute=indices.growing_season_length, - ) - -tropical_nights = Tasmin(identifier='tropical_nights', - units='days', - standard_name='number_of_days_with_air_temperature_above_threshold', - long_name='Number of Tropical Nights (Tmin > {thresh})', - description='{freq} number of Tropical Nights : defined as days with minimum daily temperature' - ' above {thresh}', - cell_methods='time: minimum within days time: sum over days', - compute=indices.tropical_nights, - ) - -tg90p = Tas(identifier='tg90p', - units='days', - standard_name='days_with_air_temperature_above_threshold', - long_name='Number of days when Tmean > 90th percentile', - description='{freq} number of days with mean daily temperature above the 90th percentile.' - 'The 90th percentile is to be computed for a 5 day window centered on each calendar day ' - 'for a reference period.', - cell_methods='time: mean within days time: sum over days', - compute=indices.tg90p, - ) - -tg10p = Tas(identifier='tg10p', - units='days', - standard_name='days_with_air_temperature_below_threshold', - long_name='Number of days when Tmean < 10th percentile', - description='{freq} number of days with mean daily temperature below the 10th percentile.' - 'The 10th percentile is to be computed for a 5 day window centered on each calendar day ' - 'for a reference period.', - cell_methods='time: mean within days time: sum over days', - compute=indices.tg10p - ) - -tx90p = Tasmax(identifier='tx90p', - units='days', - standard_name='days_with_air_temperature_above_threshold', - long_name='Number of days when Tmax > 90th percentile', - description='{freq} number of days with maximum daily temperature above the 90th percentile.' - 'The 90th percentile is to be computed for a 5 day window centered on each calendar day ' - 'for a reference period.', - cell_methods='time: maximum within days time: sum over days', - compute=indices.tx90p, - ) - -tx10p = Tasmax(identifier='tx10p', - units='days', - standard_name='days_with_air_temperature_below_threshold', - long_name='Number of days when Tmax < 10th percentile', - description='{freq} number of days with maximum daily temperature below the 10th percentile.' - 'The 10th percentile is to be computed for a 5 day window centered on each calendar day ' - 'for a reference period.', - cell_methods='time: maximum within days time: sum over days', - compute=indices.tx10p - ) - -tn90p = Tasmin(identifier='tn90p', - units='days', - standard_name='days_with_air_temperature_above_threshold', - long_name='Number of days when Tmin > 90th percentile', - description='{freq} number of days with minimum daily temperature above the 90th percentile.' - 'The 90th percentile is to be computed for a 5 day window centered on each calendar day ' - 'for a reference period.', - cell_methods='time: minimum within days time: sum over days', - compute=indices.tn90p, - ) - -tn10p = Tasmin(identifier='tn10p', - units='days', - standard_name='days_with_air_temperature_below_threshold', - long_name='Number of days when Tmin < 10th percentile', - description='{freq} number of days with minimum daily temperature below the 10th percentile.' - 'The 10th percentile is to be computed for a 5 day window centered on each calendar day ' - 'for a reference period.', - cell_methods='time: minimum within days time: sum over days', - compute=indices.tn10p - ) +tn_days_below = Tasmin( + identifier="tn_days_below", + units="days", + standard_name="number_of_days_with_air_temperature_below_threshold", + long_name="Number of days with Tmin < {thresh}", + description="{freq} number of days where daily minimum temperature is below {thresh}", + cell_methods="time: minimum within days time: sum over days", + compute=indices.tn_days_below, +) + +tx_days_above = Tasmax( + identifier="tx_days_above", + units="days", + standard_name="number_of_days_with_air_temperature_above_threshold", + long_name="Number of days with Tmax > {thresh}", + description="{freq} number of days where daily maximum temperature exceeds {thresh}", + cell_methods="time: maximum within days time: sum over days", + compute=indices.tx_days_above, +) + +tx_tn_days_above = TasminTasmax( + identifier="tx_tn_days_above", + units="days", + standard_name="number_of_days_with_air_temperature_above_threshold", + long_name="Number of days with Tmax > {thresh_tasmax} and Tmin > {thresh_tasmin}", + description="{freq} number of days where daily maximum temperature exceeds" + " {thresh_tasmax} and minimum temperature exceeds {thresh_tasmin}", + cell_methods="", + compute=indices.tx_tn_days_above, +) + +heat_wave_frequency = TasminTasmax( + identifier="heat_wave_frequency", + units="", + standard_name="heat_wave_events", + long_name="Number of heat wave events (Tmin > {thresh_tasmin}" + "and Tmax > {thresh_tasmax} for >= {window} days)", + description="{freq} number of heat wave events over a given period. " + "An event occurs when the minimum and maximum daily " + "temperature both exceeds specific thresholds : " + "(Tmin > {thresh_tasmin} and Tmax > {thresh_tasmax}) " + "over a minimum number of days ({window}).", + cell_methods="", + keywords="health,", + compute=indices.heat_wave_frequency, +) + +heat_wave_max_length = TasminTasmax( + identifier="heat_wave_max_length", + units="days", + standard_name="spell_length_of_days_with_air_temperature_above_threshold", + long_name="Maximum length of heat wave events (Tmin > {thresh_tasmin}" + "and Tmax > {thresh_tasmax} for >= {window} days)", + description="{freq} maximum length of heat wave events occuring in a given period." + "An event occurs when the minimum and maximum daily " + "temperature both exceeds specific thresholds " + "(Tmin > {thresh_tasmin} and Tmax > {thresh_tasmax}) over " + "a minimum number of days ({window}).", + cell_methods="", + keywords="health,", + compute=indices.heat_wave_max_length, +) + +heat_wave_index = Tasmax( + identifier="heat_wave_index", + units="days", + standard_name="heat_wave_index", + long_name="Number of days that are part of a heatwave", + description="{freq} number of days that are part of a heatwave, " + "defined as five or more consecutive days over {thresh}.", + cell_methods="", + compute=indices.heat_wave_index, +) + +tg_mean = Tas( + identifier="tg_mean", + units="K", + standard_name="air_temperature", + long_name="Mean daily mean temperature", + description="{freq} mean of daily mean temperature.", + cell_methods="time: mean within days time: mean over days", + compute=indices.tg_mean, +) + +tx_mean = Tasmax( + identifier="tx_mean", + units="K", + standard_name="air_temperature", + long_name="Mean daily maximum temperature", + description="{freq} mean of daily maximum temperature.", + cell_methods="time: maximum within days time: mean over days", + compute=indices.tx_mean, +) + +tx_max = Tasmax( + identifier="tx_max", + units="K", + standard_name="air_temperature", + long_name="Maximum daily maximum temperature", + description="{freq} maximum of daily maximum temperature.", + cell_methods="time: maximum within days time: maximum over days", + compute=indices.tx_max, +) + +tx_min = Tasmax( + identifier="tx_min", + units="K", + standard_name="air_temperature", + long_name="Minimum daily maximum temperature", + description="{freq} minimum of daily maximum temperature.", + cell_methods="time: maximum within days time: minimum over days", + compute=indices.tx_min, +) + +tn_mean = Tasmin( + identifier="tn_mean", + units="K", + standard_name="air_temperature", + long_name="Mean daily minimum temperature", + description="{freq} mean of daily minimum temperature.", + cell_methods="time: minimum within days time: mean over days", + compute=indices.tn_mean, +) + +tn_max = Tasmin( + identifier="tn_max", + units="K", + standard_name="air_temperature", + long_name="Maximum daily minimum temperature", + description="{freq} maximum of daily minimum temperature.", + cell_methods="time: minimum within days time: maximum over days", + compute=indices.tn_max, +) + +tn_min = Tasmin( + identifier="tn_min", + units="K", + standard_name="air_temperature", + long_name="Minimum daily minimum temperature", + description="{freq} minimum of daily minimum temperature.", + cell_methods="time: minimum within days time: minimum over days", + compute=indices.tn_min, +) + +daily_temperature_range = TasminTasmax( + identifier="dtr", + units="K", + standard_name="air_temperature", + long_name="Mean Diurnal Temperature Range", + description="{freq} mean diurnal temperature range", + cell_methods="time range within days time: mean over days", + compute=indices.daily_temperature_range, +) + +daily_temperature_range_variability = TasminTasmax( + identifier="dtrvar", + units="K", + standard_name="air_temperature", + long_name="Mean Diurnal Temperature Range Variability", + description="{freq} mean diurnal temparature range variability (" + "defined as the average day-to-day variation " + "in daily temperature range " + "for the given time period)", + cell_methods="time range within days time: difference " + "over days time: mean over days", + compute=indices.daily_temperature_range_variability, +) + +extreme_temperature_range = TasminTasmax( + identifier="etr", + units="K", + standard_name="air_temperature", + long_name="Intra-period Extreme Temperature Range", + description="{freq} range between the maximum of daily max temperature " + "(tx_max) and the minimum of daily min temperature (tn_min)", + compute=indices.extreme_temperature_range, +) + +cold_spell_duration_index = Tasmin( + identifier="cold_spell_duration_index", + var_name="csdi_{window}", + units="days", + standard_name="cold_spell_duration_index", + long_name="Cold Spell Duration Index, count of days with at " + "least {window} consecutive days when Tmin < 10th percentile", + description="{freq} number of days with at least {window} consecutive days" + " where the daily minimum temperature is below the 10th " + "percentile. The 10th percentile should be computed for " + "a 5-day window centred on each calendar day in the 1961-1990 period", + cell_methods="", + compute=indices.cold_spell_duration_index, +) + +cold_spell_days = Tas( + identifier="cold_spell_days", + units="days", + standard_name="cold_spell_days", + long_name="cold spell index", + description="{freq} number of days that are part of a cold spell, defined as {window} " + "or more consecutive days with mean daily " + "temperature below {thresh}.", + cell_methods="", + compute=indices.cold_spell_days, +) + +daily_freezethaw_cycles = TasminTasmax( + identifier="dlyfrzthw", + units="days", + standard_name="daily_freezethaw_cycles", + long_name="daily freezethaw cycles", + description="{freq} number of days with a diurnal freeze-thaw cycle " + ": Tmax > 0℃ and Tmin < 0℃.", + cell_methods="", + compute=indices.daily_freezethaw_cycles, +) + +cooling_degree_days = Tas( + identifier="cooling_degree_days", + units="K days", + standard_name="integral_of_air_temperature_excess_wrt_time", + long_name="Cooling Degree Days (Tmean > {thresh})", + description="{freq} cooling degree days above {thresh}.", + cell_methods="time: mean within days time: sum over days", + compute=indices.cooling_degree_days, +) + +heating_degree_days = Tas( + identifier="heating_degree_days", + units="K days", + standard_name="integral_of_air_temperature_deficit_wrt_time", + long_name="Heating Degree Days (Tmean < {thresh})", + description="{freq} heating degree days below {thresh}.", + cell_methods="time: mean within days time: sum over days", + compute=indices.heating_degree_days, +) + +growing_degree_days = Tas( + identifier="growing_degree_days", + units="K days", + standard_name="integral_of_air_temperature_excess_wrt_time", + long_name="growing degree days above {thresh}", + description="{freq} growing degree days above {thresh}", + cell_methods="time: mean within days time: sum over days", + compute=indices.growing_degree_days, +) + +freshet_start = Tas( + identifier="freshet_start", + units="", + standard_name="day_of_year", + long_name="Day of year of spring freshet start", + description="Day of year of spring freshet start, defined as the first day a temperature " + "threshold of {thresh} is exceeded for at least {window} days.", + compute=indices.freshet_start, +) + +frost_days = Tasmin( + identifier="frost_days", + units="days", + standard_name="days_with_air_temperature_below_threshold", + long_name="Number of Frost Days (Tmin < 0C)", + description="{freq} number of days with minimum daily " "temperature below 0℃.", + cell_methods="time: minimum within days time: sum over days", + compute=indices.frost_days, +) + +ice_days = Tasmax( + identifier="ice_days", + standard_name="days_with_air_temperature_below_threshold", + units="days", + long_name="Number of Ice Days (Tmax < 0℃)", + description="{freq} number of days with maximum daily " "temperature below 0℃", + cell_methods="time: maximum within days time: sum over days", + compute=indices.ice_days, +) + +consecutive_frost_days = Tasmin( + identifier="consecutive_frost_days", + units="days", + standard_name="spell_length_of_days_with_air_temperature_below_threshold", + long_name="Maximum number of consecutive days with Tmin < 0C", + description="{freq} maximum number of consecutive days with " + "minimum daily temperature below 0℃", + cell_methods="time: min within days time: maximum over days", + compute=indices.consecutive_frost_days, +) + +growing_season_length = Tas( + identifier="growing_season_length", + units="days", + standard_name="growing_season_length", + long_name="ETCCDI Growing Season Length (Tmean > {thresh})", + description="{freq} number of days between the first occurrence of at least " + "six consecutive days with mean daily temperature over {thresh} and " + "the first occurrence of at least {window} consecutive days with " + "mean daily temperature below {thresh} after July 1st in the northern " + "hemisphere and January 1st in the southern hemisphere", + cell_methods="", + compute=indices.growing_season_length, +) + +tropical_nights = Tasmin( + identifier="tropical_nights", + units="days", + standard_name="number_of_days_with_air_temperature_above_threshold", + long_name="Number of Tropical Nights (Tmin > {thresh})", + description="{freq} number of Tropical Nights : defined as days with minimum daily temperature" + " above {thresh}", + cell_methods="time: minimum within days time: sum over days", + compute=indices.tropical_nights, +) + +tg90p = Tas( + identifier="tg90p", + units="days", + standard_name="days_with_air_temperature_above_threshold", + long_name="Number of days when Tmean > 90th percentile", + description="{freq} number of days with mean daily temperature above the 90th percentile." + "The 90th percentile is to be computed for a 5 day window centered on each calendar day " + "for a reference period.", + cell_methods="time: mean within days time: sum over days", + compute=indices.tg90p, +) + +tg10p = Tas( + identifier="tg10p", + units="days", + standard_name="days_with_air_temperature_below_threshold", + long_name="Number of days when Tmean < 10th percentile", + description="{freq} number of days with mean daily temperature below the 10th percentile." + "The 10th percentile is to be computed for a 5 day window centered on each calendar day " + "for a reference period.", + cell_methods="time: mean within days time: sum over days", + compute=indices.tg10p, +) + +tx90p = Tasmax( + identifier="tx90p", + units="days", + standard_name="days_with_air_temperature_above_threshold", + long_name="Number of days when Tmax > 90th percentile", + description="{freq} number of days with maximum daily temperature above the 90th percentile." + "The 90th percentile is to be computed for a 5 day window centered on each calendar day " + "for a reference period.", + cell_methods="time: maximum within days time: sum over days", + compute=indices.tx90p, +) + +tx10p = Tasmax( + identifier="tx10p", + units="days", + standard_name="days_with_air_temperature_below_threshold", + long_name="Number of days when Tmax < 10th percentile", + description="{freq} number of days with maximum daily temperature below the 10th percentile." + "The 10th percentile is to be computed for a 5 day window centered on each calendar day " + "for a reference period.", + cell_methods="time: maximum within days time: sum over days", + compute=indices.tx10p, +) + +tn90p = Tasmin( + identifier="tn90p", + units="days", + standard_name="days_with_air_temperature_above_threshold", + long_name="Number of days when Tmin > 90th percentile", + description="{freq} number of days with minimum daily temperature above the 90th percentile." + "The 90th percentile is to be computed for a 5 day window centered on each calendar day " + "for a reference period.", + cell_methods="time: minimum within days time: sum over days", + compute=indices.tn90p, +) + +tn10p = Tasmin( + identifier="tn10p", + units="days", + standard_name="days_with_air_temperature_below_threshold", + long_name="Number of days when Tmin < 10th percentile", + description="{freq} number of days with minimum daily temperature below the 10th percentile." + "The 10th percentile is to be computed for a 5 day window centered on each calendar day " + "for a reference period.", + cell_methods="time: minimum within days time: sum over days", + compute=indices.tn10p, +) diff --git a/xclim/checks.py b/xclim/checks.py index 8d339da56..65b3657b5 100644 --- a/xclim/checks.py +++ b/xclim/checks.py @@ -1,7 +1,8 @@ import datetime as dt +import logging from functools import wraps from warnings import warn -import logging + import numpy as np import pandas as pd import xarray as xr @@ -21,15 +22,18 @@ # TODO: Implement pandas infer_freq in xarray with CFTimeIndex. + def check_valid(var, key, expected): r"""Check that a variable's attribute has the expected value. Warn user otherwise.""" att = getattr(var, key, None) if att is None: - e = 'Variable does not have a `{}` attribute.'.format(key) + e = "Variable does not have a `{}` attribute.".format(key) warn(e) elif att != expected: - e = 'Variable has a non-conforming {}. Got `{}`, expected `{}`'.format(key, att, expected) + e = "Variable has a non-conforming {}. Got `{}`, expected `{}`".format( + key, att, expected + ) warn(e) @@ -42,7 +46,7 @@ def assert_daily(var): # This won't work for non-standard calendars. Needs to be implemented in xarray. Comment for now if isinstance(t0.values, np.datetime64): - if pd.infer_freq(var.time.to_pandas()) != 'D': + if pd.infer_freq(var.time.to_pandas()) != "D": raise ValueError("time series is not recognized as daily.") # Check that the first time step is one day. @@ -57,55 +61,55 @@ def assert_daily(var): def check_valid_temperature(var, units): r"""Check that variable is air temperature.""" - check_valid(var, 'standard_name', 'air_temperature') - check_valid(var, 'units', units) + check_valid(var, "standard_name", "air_temperature") + check_valid(var, "units", units) assert_daily(var) def check_valid_discharge(var): r"""Check that the variable is a discharge.""" # - check_valid(var, 'standard_name', 'water_volume_transport_in_river_channel') - check_valid(var, 'units', 'm3 s-1') + check_valid(var, "standard_name", "water_volume_transport_in_river_channel") + check_valid(var, "units", "m3 s-1") -def valid_daily_min_temperature(comp, units='K'): +def valid_daily_min_temperature(comp, units="K"): r"""Decorator to check that a computation runs on a valid temperature dataset.""" @wraps(comp) def func(tasmin, *args, **kwds): check_valid_temperature(tasmin, units) - check_valid(tasmin, 'cell_methods', 'time: minimum within days') + check_valid(tasmin, "cell_methods", "time: minimum within days") return comp(tasmin, **kwds) return func -def valid_daily_mean_temperature(comp, units='K'): +def valid_daily_mean_temperature(comp, units="K"): r"""Decorator to check that a computation runs on a valid temperature dataset.""" @wraps(comp) def func(tas, *args, **kwds): check_valid_temperature(tas, units) - check_valid(tas, 'cell_methods', 'time: mean within days') + check_valid(tas, "cell_methods", "time: mean within days") return comp(tas, *args, **kwds) return func -def valid_daily_max_temperature(comp, units='K'): +def valid_daily_max_temperature(comp, units="K"): r"""Decorator to check that a computation runs on a valid temperature dataset.""" @wraps(comp) def func(tasmax, *args, **kwds): check_valid_temperature(tasmax, units) - check_valid(tasmax, 'cell_methods', 'time: maximum within days') + check_valid(tasmax, "cell_methods", "time: maximum within days") return comp(tasmax, *args, **kwds) return func -def valid_daily_max_min_temperature(comp, units='K'): +def valid_daily_max_min_temperature(comp, units="K"): r"""Decorator to check that a computation runs on valid min and max temperature datasets.""" @wraps(comp) @@ -169,8 +173,8 @@ def missing_any(da, freq, **indexer): """ from . import generic - if '-' in freq: - pfreq, anchor = freq.split('-') + if "-" in freq: + pfreq, anchor = freq.split("-") else: pfreq = freq @@ -179,32 +183,32 @@ def missing_any(da, freq, **indexer): if selected.time.size == 0: raise ValueError("No data for selected period.") - c = selected.notnull().resample(time=freq).sum(dim='time') + c = selected.notnull().resample(time=freq).sum(dim="time") # Otherwise simply use the start and end dates to find the expected number of days. - if pfreq.endswith('S'): - start_time = c.indexes['time'] + if pfreq.endswith("S"): + start_time = c.indexes["time"] end_time = start_time.shift(1, freq=freq) else: - end_time = c.indexes['time'] + end_time = c.indexes["time"] start_time = end_time.shift(-1, freq=freq) if indexer: # Create a full synthetic time series and compare the number of days with the original series. t0 = str(start_time[0].date()) t1 = str(end_time[-1].date()) - if isinstance(c.indexes['time'], xr.CFTimeIndex): - cal = da.time.encoding.get('calendar') - t = xr.cftime_range(t0, t1, freq='D', calendar=cal) + if isinstance(c.indexes["time"], xr.CFTimeIndex): + cal = da.time.encoding.get("calendar") + t = xr.cftime_range(t0, t1, freq="D", calendar=cal) else: - t = pd.date_range(t0, t1, freq='D') + t = pd.date_range(t0, t1, freq="D") - sda = xr.DataArray(data=np.empty(len(t)), coords={'time': t}, dims=('time', )) + sda = xr.DataArray(data=np.empty(len(t)), coords={"time": t}, dims=("time",)) st = generic.select_time(sda, **indexer) - sn = st.notnull().resample(time=freq).sum(dim='time') + sn = st.notnull().resample(time=freq).sum(dim="time") miss = sn != c return miss n = (end_time - start_time).days - nda = xr.DataArray(n.values, coords={'time': c.time}, dims='time') + nda = xr.DataArray(n.values, coords={"time": c.time}, dims="time") return c != nda diff --git a/xclim/ensembles.py b/xclim/ensembles.py index 4ac65ac27..21c855d16 100644 --- a/xclim/ensembles.py +++ b/xclim/ensembles.py @@ -41,19 +41,23 @@ def create_ensemble(ncfiles, mf_flag=False): simulation 2 is also a list of .nc files >>> ens = utils.create_ensemble(ncfiles) """ - dim = 'realization' + dim = "realization" ds1 = [] start_end_flag = True # print('finding common time-steps') for n in ncfiles: if mf_flag: - ds = xr.open_mfdataset(n, concat_dim='time', decode_times=False, chunks={'time': 10}) - ds['time'] = xr.open_mfdataset(n).time + ds = xr.open_mfdataset( + n, concat_dim="time", decode_times=False, chunks={"time": 10} + ) + ds["time"] = xr.open_mfdataset(n).time else: ds = xr.open_dataset(n, decode_times=False) - ds['time'] = xr.decode_cf(ds).time + ds["time"] = xr.decode_cf(ds).time # get times - use common - time1 = pd.to_datetime({'year': ds.time.dt.year, 'month': ds.time.dt.month, 'day': ds.time.dt.day}) + time1 = pd.to_datetime( + {"year": ds.time.dt.year, "month": ds.time.dt.month, "day": ds.time.dt.day} + ) if start_end_flag: start1 = time1.values[0] end1 = time1.values[-1] @@ -66,17 +70,21 @@ def create_ensemble(ncfiles, mf_flag=False): for n in ncfiles: # print('accessing file ', ncfiles.index(n) + 1, ' of ', len(ncfiles)) if mf_flag: - ds = xr.open_mfdataset(n, concat_dim='time', decode_times=False, chunks={'time': 10}) - ds['time'] = xr.open_mfdataset(n).time + ds = xr.open_mfdataset( + n, concat_dim="time", decode_times=False, chunks={"time": 10} + ) + ds["time"] = xr.open_mfdataset(n).time else: - ds = xr.open_dataset(n, decode_times=False, chunks={'time': 10}) - ds['time'] = xr.decode_cf(ds).time + ds = xr.open_dataset(n, decode_times=False, chunks={"time": 10}) + ds["time"] = xr.decode_cf(ds).time - ds['time'].values = pd.to_datetime({'year': ds.time.dt.year, 'month': ds.time.dt.month, 'day': ds.time.dt.day}) + ds["time"].values = pd.to_datetime( + {"year": ds.time.dt.year, "month": ds.time.dt.month, "day": ds.time.dt.day} + ) ds = ds.where((ds.time >= start1) & (ds.time <= end1), drop=True) - ds1.append(ds.drop('time')) + ds1.append(ds.drop("time")) # print('concatenating files : adding dimension ', dim, ) ens = xr.concat(ds1, dim=dim) # assign time coords @@ -112,17 +120,21 @@ def ensemble_mean_std_max_min(ens): dsOut = ens.drop(ens.data_vars) for v in ens.data_vars: - dsOut[v + '_mean'] = ens[v].mean(dim='realization') - dsOut[v + '_stdev'] = ens[v].std(dim='realization') - dsOut[v + '_max'] = ens[v].max(dim='realization') - dsOut[v + '_min'] = ens[v].min(dim='realization') + dsOut[v + "_mean"] = ens[v].mean(dim="realization") + dsOut[v + "_stdev"] = ens[v].std(dim="realization") + dsOut[v + "_max"] = ens[v].max(dim="realization") + dsOut[v + "_min"] = ens[v].min(dim="realization") for vv in dsOut.data_vars: dsOut[vv].attrs = ens[v].attrs - if 'description' in dsOut[vv].attrs.keys(): + if "description" in dsOut[vv].attrs.keys(): vv.split() - dsOut[vv].attrs['description'] = dsOut[vv].attrs['description'] + ' : ' + vv.split('_')[ - -1] + ' of ensemble' + dsOut[vv].attrs["description"] = ( + dsOut[vv].attrs["description"] + + " : " + + vv.split("_")[-1] + + " of ensemble" + ) return dsOut @@ -169,15 +181,20 @@ def ensemble_percentiles(ens, values=(10, 50, 90), time_block=None): for v in ens.data_vars: # Percentile calculation requires load to memory : automate size for large ensemble objects if not time_block: - time_block = round(2E8 / (ens[v].size / ens[v].shape[dims.index('time')]), -1) # 2E8 + time_block = round( + 2e8 / (ens[v].size / ens[v].shape[dims.index("time")]), -1 + ) # 2E8 if time_block > len(ens[v].time): out = _calc_percentiles_simple(ens, v, values) else: # loop through blocks - Warning('large ensemble size detected : statistics will be calculated in blocks of ', int(time_block), - ' time-steps') + Warning( + "large ensemble size detected : statistics will be calculated in blocks of ", + int(time_block), + " time-steps", + ) out = _calc_percentiles_blocks(ens, v, values, time_block) for vv in out.data_vars: ds_out[vv] = out[vv] @@ -187,7 +204,7 @@ def ensemble_percentiles(ens, values=(10, 50, 90), time_block=None): def _calc_percentiles_simple(ens, v, values): ds_out = ens.drop(ens.data_vars) dims = list(ens[v].dims) - outdims = [x for x in dims if 'realization' not in x] + outdims = [x for x in dims if "realization" not in x] # print('loading ensemble data to memory') arr = ens[v].load() # percentile calc requires loading the array @@ -195,17 +212,22 @@ def _calc_percentiles_simple(ens, v, values): for c in outdims: coords[c] = arr[c] for p in values: - outvar = v + '_p' + str(p) + outvar = v + "_p" + str(p) out1 = _calc_perc(arr, p) ds_out[outvar] = xr.DataArray(out1, dims=outdims, coords=coords) ds_out[outvar].attrs = ens[v].attrs - if 'description' in ds_out[outvar].attrs.keys(): - ds_out[outvar].attrs['description'] = '{} : {}th percentile of ensemble'.format( - ds_out[outvar].attrs['description'], str(p)) + if "description" in ds_out[outvar].attrs.keys(): + ds_out[outvar].attrs[ + "description" + ] = "{} : {}th percentile of ensemble".format( + ds_out[outvar].attrs["description"], str(p) + ) else: - ds_out[outvar].attrs['description'] = '{}th percentile of ensemble'.format(str(p)) + ds_out[outvar].attrs["description"] = "{}th percentile of ensemble".format( + str(p) + ) return ds_out @@ -213,7 +235,7 @@ def _calc_percentiles_simple(ens, v, values): def _calc_percentiles_blocks(ens, v, values, time_block): ds_out = ens.drop(ens.data_vars) dims = list(ens[v].dims) - outdims = [x for x in dims if 'realization' not in x] + outdims = [x for x in dims if "realization" not in x] blocks = list(range(0, len(ens.time) + 1, int(time_block))) if blocks[-1] != len(ens[v].time): @@ -222,7 +244,9 @@ def _calc_percentiles_blocks(ens, v, values, time_block): for t in range(0, len(blocks) - 1): # print('Calculating block ', t + 1, ' of ', len(blocks) - 1) time_sel = slice(blocks[t], blocks[t + 1]) - arr = ens[v].isel(time=time_sel).load() # percentile calc requires loading the array + arr = ( + ens[v].isel(time=time_sel).load() + ) # percentile calc requires loading the array coords = {} for c in outdims: coords[c] = arr[c] @@ -233,17 +257,27 @@ def _calc_percentiles_blocks(ens, v, values, time_block): if t == 0: arr_p_all[str(p)] = xr.DataArray(out1, dims=outdims, coords=coords) else: - arr_p_all[str(p)] = xr.concat([arr_p_all[str(p)], - xr.DataArray(out1, dims=outdims, coords=coords)], dim='time') + arr_p_all[str(p)] = xr.concat( + [ + arr_p_all[str(p)], + xr.DataArray(out1, dims=outdims, coords=coords), + ], + dim="time", + ) for p in values: - outvar = v + '_p' + str(p) + outvar = v + "_p" + str(p) ds_out[outvar] = arr_p_all[str(p)] ds_out[outvar].attrs = ens[v].attrs - if 'description' in ds_out[outvar].attrs.keys(): - ds_out[outvar].attrs['description'] = '{} : {}th percentile of ensemble'.format( - ds_out[outvar].attrs['description'], str(p)) + if "description" in ds_out[outvar].attrs.keys(): + ds_out[outvar].attrs[ + "description" + ] = "{} : {}th percentile of ensemble".format( + ds_out[outvar].attrs["description"], str(p) + ) else: - ds_out[outvar].attrs['description'] = '{}th percentile of ensemble'.format(str(p)) + ds_out[outvar].attrs["description"] = "{}th percentile of ensemble".format( + str(p) + ) return ds_out @@ -251,22 +285,28 @@ def _calc_percentiles_blocks(ens, v, values, time_block): def _calc_perc(arr, p): dims = arr.dims # make sure time is the second dimension - if dims.index('time') != 1: - dims1 = [dims[dims.index('realization')], dims[dims.index('time')]] + if dims.index("time") != 1: + dims1 = [dims[dims.index("realization")], dims[dims.index("time")]] for d in dims: if d not in dims1: dims1.append(d) arr = arr.transpose(*dims1) dims = dims1 - nan_count = np.isnan(arr).sum(axis=dims.index('realization')) - out = np.percentile(arr, p, axis=dims.index('realization')) - if np.any((nan_count > 0) & (nan_count < arr.shape[dims.index('realization')])): - arr1 = arr.values.reshape(arr.shape[dims.index('realization')], - int(arr.size / arr.shape[dims.index('realization')])) + nan_count = np.isnan(arr).sum(axis=dims.index("realization")) + out = np.percentile(arr, p, axis=dims.index("realization")) + if np.any((nan_count > 0) & (nan_count < arr.shape[dims.index("realization")])): + arr1 = arr.values.reshape( + arr.shape[dims.index("realization")], + int(arr.size / arr.shape[dims.index("realization")]), + ) # only use nanpercentile where we need it (slow performace compared to standard) : - nan_index = np.where((nan_count > 0) & (nan_count < arr.shape[dims.index('realization')])) + nan_index = np.where( + (nan_count > 0) & (nan_count < arr.shape[dims.index("realization")]) + ) t = np.ravel_multi_index(nan_index, nan_count.shape) - out[np.unravel_index(t, nan_count.shape)] = np.nanpercentile(arr1[:, t], p, axis=dims.index('realization')) + out[np.unravel_index(t, nan_count.shape)] = np.nanpercentile( + arr1[:, t], p, axis=dims.index("realization") + ) return out diff --git a/xclim/generic.py b/xclim/generic.py index 1aee24f87..786113016 100644 --- a/xclim/generic.py +++ b/xclim/generic.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Note: stats.dist.shapes: comma separated names of shape parameters # The other parameters, common to all distribution, are loc and scale. - import dask import numpy as np import xarray as xr @@ -29,7 +28,7 @@ def select_time(da, **indexer): else: key, val = indexer.popitem() time_att = getattr(da.time.dt, key) - selected = da.sel(time=time_att.isin(val)).dropna(dim='time') + selected = da.sel(time=time_att.isin(val)).dropna(dim="time") return selected @@ -59,28 +58,28 @@ def select_resample_op(da, op, freq="YS", **indexer): da = select_time(da, **indexer) r = da.resample(time=freq, keep_attrs=True) if isinstance(op, str): - return getattr(r, op)(dim='time', keep_attrs=True) + return getattr(r, op)(dim="time", keep_attrs=True) return r.apply(op) def doymax(da): """Return the day of year of the maximum value.""" - i = da.argmax(dim='time') + i = da.argmax(dim="time") out = da.time.dt.dayofyear[i] - out.attrs['units'] = '' + out.attrs["units"] = "" return out def doymin(da): """Return the day of year of the minimum value.""" - i = da.argmax(dim='time') + i = da.argmax(dim="time") out = da.time.dt.dayofyear[i] - out.attrs['units'] = '' + out.attrs["units"] = "" return out -def fit(da, dist='norm'): +def fit(da, dist="norm"): """Fit an array to a univariate distribution along the time dimension. Parameters @@ -105,36 +104,44 @@ def fit(da, dist='norm'): dc = get_dist(dist) # Fit the parameters (lazy computation) - data = dask.array.apply_along_axis(dc.fit, da.get_axis_num('time'), da.dropna('time', how='all')) + data = dask.array.apply_along_axis( + dc.fit, da.get_axis_num("time"), da.dropna("time", how="all") + ) # Count the number of values used for the fit. # n = arr.count(dim='time') # Create a view to a DataArray with the desired dimensions to copy them over to the parameter array. - mean = da.mean(dim='time', keep_attrs=True) + mean = da.mean(dim="time", keep_attrs=True) # Create coordinate for the distribution parameters coords = dict(mean.coords.items()) - coords['dparams'] = ([] if dc.shapes is None else dc.shapes.split(',')) + ['loc', 'scale'] + coords["dparams"] = ([] if dc.shapes is None else dc.shapes.split(",")) + [ + "loc", + "scale", + ] # TODO: add time and time_bnds coordinates (Low will work on this) # time.attrs['climatology'] = 'climatology_bounds' # coords['time'] = # coords['climatology_bounds'] = - out = xr.DataArray(data=data, coords=coords, dims=(u'dparams',) + mean.dims) + out = xr.DataArray(data=data, coords=coords, dims=(u"dparams",) + mean.dims) out.attrs = da.attrs - out.attrs['original_name'] = getattr(da, 'standard_name', '') - out.attrs['description'] = \ - 'Parameters of the {0} distribution fitted over {1}'.format(dist, getattr(da, 'standard_name', '')) - out.attrs['estimator'] = 'Maximum likelihood' - out.attrs['scipy_dist'] = dist - out.attrs['units'] = '' + out.attrs["original_name"] = getattr(da, "standard_name", "") + out.attrs[ + "description" + ] = "Parameters of the {} distribution fitted over {}".format( + dist, getattr(da, "standard_name", "") + ) + out.attrs["estimator"] = "Maximum likelihood" + out.attrs["scipy_dist"] = dist + out.attrs["units"] = "" # out.name = 'params' return out -def fa(da, t, dist='norm', mode='high'): +def fa(da, t, dist="norm", mode="high"): """Return the value corresponding to the given return period. Parameters @@ -164,26 +171,30 @@ def fa(da, t, dist='norm', mode='high'): p = fit(da, dist) # Create a lambda function to facilitate passing arguments to dask. There is probably a better way to do this. - if mode in ['max', 'high']: + if mode in ["max", "high"]: + def func(x): - return dc.isf(1. / t, *x) - elif mode in ['min', 'low']: + return dc.isf(1.0 / t, *x) + + elif mode in ["min", "low"]: + def func(x): - return dc.ppf(1. / t, *x) + return dc.ppf(1.0 / t, *x) + else: raise ValueError("mode `{}` should be either 'max' or 'min'".format(mode)) - data = dask.array.apply_along_axis(func, p.get_axis_num('dparams'), p) + data = dask.array.apply_along_axis(func, p.get_axis_num("dparams"), p) # Create coordinate for the return periods coords = dict(p.coords.items()) - coords.pop('dparams') - coords['return_period'] = t + coords.pop("dparams") + coords["return_period"] = t # Create dimensions dims = list(p.dims) - dims.remove('dparams') - dims.insert(0, u'return_period') + dims.remove("dparams") + dims.insert(0, u"return_period") # TODO: add time and time_bnds coordinates (Low will work on this) # time.attrs['climatology'] = 'climatology_bounds' @@ -192,12 +203,18 @@ def func(x): out = xr.DataArray(data=data, coords=coords, dims=dims) out.attrs = p.attrs - out.attrs['standard_name'] = '{0} quantiles'.format(dist) - out.attrs['long_name'] = '{0} return period values for {1}'.format(dist, getattr(da, 'standard_name', '')) - out.attrs['cell_methods'] = (out.attrs.get('cell_methods', '') + ' dparams: ppf').strip() - out.attrs['units'] = da.attrs.get('units', '') - out.attrs['mode'] = mode - out.attrs['history'] = out.attrs.get('history', '') + "Compute values corresponding to return periods." + out.attrs["standard_name"] = "{} quantiles".format(dist) + out.attrs["long_name"] = "{} return period values for {}".format( + dist, getattr(da, "standard_name", "") + ) + out.attrs["cell_methods"] = ( + out.attrs.get("cell_methods", "") + " dparams: ppf" + ).strip() + out.attrs["units"] = da.attrs.get("units", "") + out.attrs["mode"] = mode + out.attrs["history"] = ( + out.attrs.get("history", "") + "Compute values corresponding to return periods." + ) return out @@ -243,7 +260,7 @@ def frequency_analysis(da, mode, t, dist, window=1, freq=None, **indexer): freq = freq or default_freq(**indexer) # Extract the time series of min or max over the period - sel = select_resample_op(da, op=mode, freq=freq, **indexer).dropna(dim='time') + sel = select_resample_op(da, op=mode, freq=freq, **indexer).dropna(dim="time") # Frequency analysis return fa(sel, t, dist, mode) @@ -251,12 +268,12 @@ def frequency_analysis(da, mode, t, dist, window=1, freq=None, **indexer): def default_freq(**indexer): """Return the default frequency.""" - freq = 'AS-JAN' + freq = "AS-JAN" if indexer: - if 'DJF' in indexer.values(): - freq = 'AS-DEC' - if 'month' in indexer and sorted(indexer.values()) != indexer.values(): - raise (NotImplementedError) + if "DJF" in indexer.values(): + freq = "AS-DEC" + if "month" in indexer and sorted(indexer.values()) != indexer.values(): + raise NotImplementedError return freq diff --git a/xclim/indices/_multivariate.py b/xclim/indices/_multivariate.py index d4d678824..9b001bc9b 100644 --- a/xclim/indices/_multivariate.py +++ b/xclim/indices/_multivariate.py @@ -3,8 +3,10 @@ import numpy as np import xarray as xr -from xclim import utils, run_length as rl -from xclim.utils import declare_units, units +from xclim import run_length as rl +from xclim import utils +from xclim.utils import declare_units +from xclim.utils import units # logging.basicConfig(level=logging.DEBUG) # logging.captureWarnings(True) @@ -19,14 +21,31 @@ # ATTENTION: ASSUME ALL INDICES WRONG UNTIL TESTED ! # # -------------------------------------------------- # -__all__ = ['cold_spell_duration_index', 'cold_and_dry_days', 'daily_freezethaw_cycles', 'daily_temperature_range', - 'daily_temperature_range_variability', 'extreme_temperature_range', 'heat_wave_frequency', - 'heat_wave_max_length', 'liquid_precip_ratio', 'rain_on_frozen_ground_days', 'tg90p', 'tg10p', - 'tn90p', 'tn10p', 'tx90p', 'tx10p', 'tx_tn_days_above', 'warm_spell_duration_index', 'winter_rain_ratio'] - - -@declare_units('days', tasmin='[temperature]', tn10='[temperature]') -def cold_spell_duration_index(tasmin, tn10, window=6, freq='YS'): +__all__ = [ + "cold_spell_duration_index", + "cold_and_dry_days", + "daily_freezethaw_cycles", + "daily_temperature_range", + "daily_temperature_range_variability", + "extreme_temperature_range", + "heat_wave_frequency", + "heat_wave_max_length", + "liquid_precip_ratio", + "rain_on_frozen_ground_days", + "tg90p", + "tg10p", + "tn90p", + "tn10p", + "tx90p", + "tx10p", + "tx_tn_days_above", + "warm_spell_duration_index", + "winter_rain_ratio", +] + + +@declare_units("days", tasmin="[temperature]", tn10="[temperature]") +def cold_spell_duration_index(tasmin, tn10, window=6, freq="YS"): r"""Cold spell duration index Number of days with at least six consecutive days where the daily minimum temperature is below the 10th @@ -70,11 +89,11 @@ def cold_spell_duration_index(tasmin, tn10, window=6, freq='YS'): >>> tn10 = percentile_doy(historical_tasmin, per=.1) >>> cold_spell_duration_index(reference_tasmin, tn10) """ - if 'dayofyear' not in tn10.coords.keys(): + if "dayofyear" not in tn10.coords.keys(): raise AttributeError("tn10 should have dayofyear coordinates.") # The day of year value of the tasmin series. - doy = tasmin.indexes['time'].dayofyear + doy = tasmin.indexes["time"].dayofyear tn10 = utils.convert_units_to(tn10, tasmin) # If calendar of `tn10` is different from `tasmin`, interpolate. @@ -84,12 +103,14 @@ def cold_spell_duration_index(tasmin, tn10, window=6, freq='YS'): thresh = xr.full_like(tasmin, np.nan) thresh.data = tn10.sel(dayofyear=doy) - below = (tasmin < thresh) + below = tasmin < thresh - return below.resample(time=freq).apply(rl.windowed_run_count, window=window, dim='time') + return below.resample(time=freq).apply( + rl.windowed_run_count, window=window, dim="time" + ) -def cold_and_dry_days(tas, tgin25, pr, wet25, freq='YS'): +def cold_and_dry_days(tas, tgin25, pr, wet25, freq="YS"): r"""Cold and dry days. Returns the total number of days where "Cold" and "Dry" conditions coincide. @@ -130,8 +151,8 @@ def cold_and_dry_days(tas, tgin25, pr, wet25, freq='YS'): # return c.resample(time=freq).sum(dim='time') -@declare_units('days', tasmax='[temperature]', tasmin='[temperature]') -def daily_freezethaw_cycles(tasmax, tasmin, freq='YS'): +@declare_units("days", tasmax="[temperature]", tasmin="[temperature]") +def daily_freezethaw_cycles(tasmax, tasmin, freq="YS"): r"""Number of days with a diurnal freeze-thaw cycle The number of days where Tmax > 0℃ and Tmin < 0℃. @@ -162,14 +183,14 @@ def daily_freezethaw_cycles(tasmax, tasmin, freq='YS'): where :math:`[P]` is 1 if :math:`P` is true, and 0 if false. """ - frz = utils.convert_units_to('0 degC', tasmax) + frz = utils.convert_units_to("0 degC", tasmax) ft = (tasmin < frz) * (tasmax > frz) * 1 - out = ft.resample(time=freq).sum(dim='time') + out = ft.resample(time=freq).sum(dim="time") return out -@declare_units('K', tasmax='[temperature]', tasmin='[temperature]') -def daily_temperature_range(tasmax, tasmin, freq='YS'): +@declare_units("K", tasmax="[temperature]", tasmin="[temperature]") +def daily_temperature_range(tasmax, tasmin, freq="YS"): r"""Mean of daily temperature range. The mean difference between the daily maximum temperature and the daily minimum temperature. @@ -199,12 +220,12 @@ def daily_temperature_range(tasmax, tasmin, freq='YS'): """ dtr = tasmax - tasmin - out = dtr.resample(time=freq).mean(dim='time', keep_attrs=True) - out.attrs['units'] = tasmax.units + out = dtr.resample(time=freq).mean(dim="time", keep_attrs=True) + out.attrs["units"] = tasmax.units return out -@declare_units('K', tasmax='[temperature]', tasmin='[temperature]') +@declare_units("K", tasmax="[temperature]", tasmin="[temperature]") def daily_temperature_range_variability(tasmax, tasmin, freq="YS"): r"""Mean absolute day-to-day variation in daily temperature range. @@ -235,14 +256,14 @@ def daily_temperature_range_variability(tasmax, tasmin, freq="YS"): vDTR_j = \frac{ \sum_{i=2}^{I} |(TX_{ij}-TN_{ij})-(TX_{i-1,j}-TN_{i-1,j})| }{I} """ - vdtr = abs((tasmax - tasmin).diff(dim='time')) - out = vdtr.resample(time=freq).mean(dim='time') - out.attrs['units'] = tasmax.units + vdtr = abs((tasmax - tasmin).diff(dim="time")) + out = vdtr.resample(time=freq).mean(dim="time") + out.attrs["units"] = tasmax.units return out -@declare_units('K', tasmax='[temperature]', tasmin='[temperature]') -def extreme_temperature_range(tasmax, tasmin, freq='YS'): +@declare_units("K", tasmax="[temperature]", tasmin="[temperature]") +def extreme_temperature_range(tasmax, tasmin, freq="YS"): r"""Extreme intra-period temperature range. The maximum of max temperature (TXx) minus the minimum of min temperature (TNn) for the given time period. @@ -271,18 +292,29 @@ def extreme_temperature_range(tasmax, tasmin, freq='YS'): ETR_j = max(TX_{ij}) - min(TN_{ij}) """ - tx_max = tasmax.resample(time=freq).max(dim='time') - tn_min = tasmin.resample(time=freq).min(dim='time') + tx_max = tasmax.resample(time=freq).max(dim="time") + tn_min = tasmin.resample(time=freq).min(dim="time") out = tx_max - tn_min - out.attrs['units'] = tasmax.units + out.attrs["units"] = tasmax.units return out -@declare_units('', tasmin='[temperature]', tasmax='[temperature]', thresh_tasmin='[temperature]', - thresh_tasmax='[temperature]') -def heat_wave_frequency(tasmin, tasmax, thresh_tasmin='22.0 degC', thresh_tasmax='30 degC', - window=3, freq='YS'): +@declare_units( + "", + tasmin="[temperature]", + tasmax="[temperature]", + thresh_tasmin="[temperature]", + thresh_tasmax="[temperature]", +) +def heat_wave_frequency( + tasmin, + tasmax, + thresh_tasmin="22.0 degC", + thresh_tasmax="30 degC", + window=3, + freq="YS", +): # Dev note : we should decide if it is deg K or C r"""Heat wave frequency @@ -334,13 +366,24 @@ def heat_wave_frequency(tasmin, tasmax, thresh_tasmin='22.0 degC', thresh_tasmax cond = (tasmin > thresh_tasmin) & (tasmax > thresh_tasmax) group = cond.resample(time=freq) - return group.apply(rl.windowed_run_events, window=window, dim='time') - - -@declare_units('days', tasmin='[temperature]', tasmax='[temperature]', thresh_tasmin='[temperature]', - thresh_tasmax='[temperature]') -def heat_wave_max_length(tasmin, tasmax, thresh_tasmin='22.0 degC', thresh_tasmax='30 degC', - window=3, freq='YS'): + return group.apply(rl.windowed_run_events, window=window, dim="time") + + +@declare_units( + "days", + tasmin="[temperature]", + tasmax="[temperature]", + thresh_tasmin="[temperature]", + thresh_tasmax="[temperature]", +) +def heat_wave_max_length( + tasmin, + tasmax, + thresh_tasmin="22.0 degC", + thresh_tasmax="30 degC", + window=3, + freq="YS", +): # Dev note : we should decide if it is deg K or C r"""Heat wave max length @@ -394,12 +437,12 @@ def heat_wave_max_length(tasmin, tasmax, thresh_tasmin='22.0 degC', thresh_tasma cond = (tasmin > thresh_tasmin) & (tasmax > thresh_tasmax) group = cond.resample(time=freq) - max_l = group.apply(rl.longest_run, dim='time') + max_l = group.apply(rl.longest_run, dim="time") return max_l.where(max_l >= window, 0) -@declare_units('', pr='[precipitation]', prsn='[precipitation]', tas='[temperature]') -def liquid_precip_ratio(pr, prsn=None, tas=None, freq='QS-DEC'): +@declare_units("", pr="[precipitation]", prsn="[precipitation]", tas="[temperature]") +def liquid_precip_ratio(pr, prsn=None, tas=None, freq="QS-DEC"): r"""Ratio of rainfall to total precipitation The ratio of total liquid precipitation over the total precipitation. If solid precipitation is not provided, @@ -439,21 +482,23 @@ def liquid_precip_ratio(pr, prsn=None, tas=None, freq='QS-DEC'): """ if prsn is None: - tu = units.parse_units(tas.attrs['units'].replace('-', '**-')) - fu = 'degC' + tu = units.parse_units(tas.attrs["units"].replace("-", "**-")) + fu = "degC" frz = 0 if fu != tu: frz = units.convert(frz, fu, tu) prsn = pr.where(tas < frz, 0) - tot = pr.resample(time=freq).sum(dim='time') - rain = tot - prsn.resample(time=freq).sum(dim='time') + tot = pr.resample(time=freq).sum(dim="time") + rain = tot - prsn.resample(time=freq).sum(dim="time") ratio = rain / tot return ratio -@declare_units('days', pr='[precipitation]', tas='[temperature]', thresh='[precipitation]') -def rain_on_frozen_ground_days(pr, tas, thresh='1 mm/d', freq='YS'): +@declare_units( + "days", pr="[precipitation]", tas="[temperature]", thresh="[precipitation]" +) +def rain_on_frozen_ground_days(pr, tas, thresh="1 mm/d", freq="YS"): """Number of rain on frozen ground events Number of days with rain above a threshold after a series of seven days below freezing temperature. @@ -494,7 +539,7 @@ def rain_on_frozen_ground_days(pr, tas, thresh='1 mm/d', freq='YS'): """ t = utils.convert_units_to(thresh, pr) - frz = utils.convert_units_to('0 C', tas) + frz = utils.convert_units_to("0 C", tas) def func(x, axis): """Check that temperature conditions are below 0 for seven days and above after.""" @@ -502,13 +547,13 @@ def func(x, axis): return frozen.all(axis=axis) tcond = (tas > frz).rolling(time=8).reduce(func) - pcond = (pr > t) + pcond = pr > t - return (tcond * pcond * 1).resample(time=freq).sum(dim='time') + return (tcond * pcond * 1).resample(time=freq).sum(dim="time") -@declare_units('days', tas='[temperature]', t90='[temperature]') -def tg90p(tas, t90, freq='YS'): +@declare_units("days", tas="[temperature]", t90="[temperature]") +def tg90p(tas, t90, freq="YS"): r"""Number of days with daily mean temperature over the 90th percentile. Number of days with daily mean temperature over the 90th percentile. @@ -536,7 +581,7 @@ def tg90p(tas, t90, freq='YS'): >>> t90 = percentile_doy(historical_tas, per=0.9) >>> hot_days = tg90p(tas, t90) """ - if 'dayofyear' not in t90.coords.keys(): + if "dayofyear" not in t90.coords.keys(): raise AttributeError("t10 should have dayofyear coordinates.") t90 = utils.convert_units_to(t90, tas) @@ -550,13 +595,13 @@ def tg90p(tas, t90, freq='YS'): thresh.data = t90.sel(dayofyear=doy) # compute the cold days - over = (tas > thresh) + over = tas > thresh - return over.resample(time=freq).sum(dim='time') + return over.resample(time=freq).sum(dim="time") -@declare_units('days', tas='[temperature]', t10='[temperature]') -def tg10p(tas, t10, freq='YS'): +@declare_units("days", tas="[temperature]", t10="[temperature]") +def tg10p(tas, t10, freq="YS"): r"""Number of days with daily mean temperature below the 10th percentile. Number of days with daily mean temperature below the 10th percentile. @@ -584,7 +629,7 @@ def tg10p(tas, t10, freq='YS'): >>> t10 = percentile_doy(historical_tas, per=0.1) >>> cold_days = tg10p(tas, t10) """ - if 'dayofyear' not in t10.coords.keys(): + if "dayofyear" not in t10.coords.keys(): raise AttributeError("t10 should have dayofyear coordinates.") t10 = utils.convert_units_to(t10, tas) @@ -598,13 +643,13 @@ def tg10p(tas, t10, freq='YS'): thresh.data = t10.sel(dayofyear=doy) # compute the cold days - below = (tas < thresh) + below = tas < thresh - return below.resample(time=freq).sum(dim='time') + return below.resample(time=freq).sum(dim="time") -@declare_units('days', tasmin='[temperature]', t90='[temperature]') -def tn90p(tasmin, t90, freq='YS'): +@declare_units("days", tasmin="[temperature]", t90="[temperature]") +def tn90p(tasmin, t90, freq="YS"): r"""Number of days with daily minimum temperature over the 90th percentile. Number of days with daily minimum temperature over the 90th percentile. @@ -632,7 +677,7 @@ def tn90p(tasmin, t90, freq='YS'): >>> t90 = percentile_doy(historical_tas, per=0.9) >>> hot_days = tg90p(tas, t90) """ - if 'dayofyear' not in t90.coords.keys(): + if "dayofyear" not in t90.coords.keys(): raise AttributeError("t10 should have dayofyear coordinates.") t90 = utils.convert_units_to(t90, tasmin) # adjustment of t90 to tas doy range @@ -644,13 +689,13 @@ def tn90p(tasmin, t90, freq='YS'): thresh.data = t90.sel(dayofyear=doy) # compute the cold days - over = (tasmin > thresh) + over = tasmin > thresh - return over.resample(time=freq).sum(dim='time') + return over.resample(time=freq).sum(dim="time") -@declare_units('days', tasmin='[temperature]', t10='[temperature]') -def tn10p(tasmin, t10, freq='YS'): +@declare_units("days", tasmin="[temperature]", t10="[temperature]") +def tn10p(tasmin, t10, freq="YS"): r"""Number of days with daily minimum temperature below the 10th percentile. Number of days with daily minimum temperature below the 10th percentile. @@ -679,7 +724,7 @@ def tn10p(tasmin, t10, freq='YS'): >>> t10 = percentile_doy(historical_tas, per=0.1) >>> cold_days = tg10p(tas, t10) """ - if 'dayofyear' not in t10.coords.keys(): + if "dayofyear" not in t10.coords.keys(): raise AttributeError("t10 should have dayofyear coordinates.") t10 = utils.convert_units_to(t10, tasmin) @@ -692,13 +737,13 @@ def tn10p(tasmin, t10, freq='YS'): thresh.data = t10.sel(dayofyear=doy) # compute the cold days - below = (tasmin < thresh) + below = tasmin < thresh - return below.resample(time=freq).sum(dim='time') + return below.resample(time=freq).sum(dim="time") -@declare_units('days', tasmax='[temperature]', t90='[temperature]') -def tx90p(tasmax, t90, freq='YS'): +@declare_units("days", tasmax="[temperature]", t90="[temperature]") +def tx90p(tasmax, t90, freq="YS"): r"""Number of days with daily maximum temperature over the 90th percentile. Number of days with daily maximum temperature over the 90th percentile. @@ -726,7 +771,7 @@ def tx90p(tasmax, t90, freq='YS'): >>> t90 = percentile_doy(historical_tas, per=0.9) >>> hot_days = tg90p(tas, t90) """ - if 'dayofyear' not in t90.coords.keys(): + if "dayofyear" not in t90.coords.keys(): raise AttributeError("t10 should have dayofyear coordinates.") t90 = utils.convert_units_to(t90, tasmax) @@ -740,13 +785,13 @@ def tx90p(tasmax, t90, freq='YS'): thresh.data = t90.sel(dayofyear=doy) # compute the cold days - over = (tasmax > thresh) + over = tasmax > thresh - return over.resample(time=freq).sum(dim='time') + return over.resample(time=freq).sum(dim="time") -@declare_units('days', tasmax='[temperature]', t10='[temperature]') -def tx10p(tasmax, t10, freq='YS'): +@declare_units("days", tasmax="[temperature]", t10="[temperature]") +def tx10p(tasmax, t10, freq="YS"): r"""Number of days with daily maximum temperature below the 10th percentile. Number of days with daily maximum temperature below the 10th percentile. @@ -774,7 +819,7 @@ def tx10p(tasmax, t10, freq='YS'): >>> t10 = percentile_doy(historical_tas, per=0.1) >>> cold_days = tg10p(tas, t10) """ - if 'dayofyear' not in t10.coords.keys(): + if "dayofyear" not in t10.coords.keys(): raise AttributeError("t10 should have dayofyear coordinates.") t10 = utils.convert_units_to(t10, tasmax) @@ -788,15 +833,21 @@ def tx10p(tasmax, t10, freq='YS'): thresh.data = t10.sel(dayofyear=doy) # compute the cold days - below = (tasmax < thresh) + below = tasmax < thresh - return below.resample(time=freq).sum(dim='time') + return below.resample(time=freq).sum(dim="time") -@declare_units('days', tasmin='[temperature]', tasmax='[temperature]', thresh_tasmin='[temperature]', - thresh_tasmax='[temperature]') -def tx_tn_days_above(tasmin, tasmax, thresh_tasmin='22 degC', - thresh_tasmax='30 degC', freq='YS'): +@declare_units( + "days", + tasmin="[temperature]", + tasmax="[temperature]", + thresh_tasmin="[temperature]", + thresh_tasmax="[temperature]", +) +def tx_tn_days_above( + tasmin, tasmax, thresh_tasmin="22 degC", thresh_tasmax="30 degC", freq="YS" +): r"""Number of days with both hot maximum and minimum daily temperatures. The number of days per period with tasmin above a threshold and tasmax above another threshold. @@ -842,11 +893,11 @@ def tx_tn_days_above(tasmin, tasmax, thresh_tasmin='22 degC', thresh_tasmax = utils.convert_units_to(thresh_tasmax, tasmax) thresh_tasmin = utils.convert_units_to(thresh_tasmin, tasmin) events = ((tasmin > (thresh_tasmin)) & (tasmax > (thresh_tasmax))) * 1 - return events.resample(time=freq).sum(dim='time') + return events.resample(time=freq).sum(dim="time") -@declare_units('days', tasmax='[temperature]', tx90='[temperature]') -def warm_spell_duration_index(tasmax, tx90, window=6, freq='YS'): +@declare_units("days", tasmax="[temperature]", tx90="[temperature]") +def warm_spell_duration_index(tasmax, tx90, window=6, freq="YS"): r"""Warm spell duration index Number of days with at least six consecutive days where the daily maximum temperature is above the 90th @@ -877,11 +928,11 @@ def warm_spell_duration_index(tasmax, tx90, window=6, freq='YS'): precipitation, J. Geophys. Res., 111, D05109, doi: 10.1029/2005JD006290. """ - if 'dayofyear' not in tx90.coords.keys(): + if "dayofyear" not in tx90.coords.keys(): raise AttributeError("tx90 should have dayofyear coordinates.") # The day of year value of the tasmax series. - doy = tasmax.indexes['time'].dayofyear + doy = tasmax.indexes["time"].dayofyear # adjustment of tx90 to tasmax doy range tx90 = utils.adjust_doy_calendar(tx90, tasmax) @@ -890,12 +941,14 @@ def warm_spell_duration_index(tasmax, tx90, window=6, freq='YS'): thresh = xr.full_like(tasmax, np.nan) thresh.data = tx90.sel(dayofyear=doy) - above = (tasmax > thresh) + above = tasmax > thresh - return above.resample(time=freq).apply(rl.windowed_run_count, window=window, dim='time') + return above.resample(time=freq).apply( + rl.windowed_run_count, window=window, dim="time" + ) -@declare_units('', pr='[precipitation]', prsn='[precipitation]', tas='[temperature]') +@declare_units("", pr="[precipitation]", prsn="[precipitation]", tas="[temperature]") def winter_rain_ratio(pr, prsn=None, tas=None): """Ratio of rainfall to total precipitation during winter @@ -918,6 +971,6 @@ def winter_rain_ratio(pr, prsn=None, tas=None): xarray.DataArray Ratio of rainfall to total precipitation during winter months (DJF) """ - ratio = liquid_precip_ratio(pr, prsn, tas, freq='QS-DEC') - winter = ratio.indexes['time'].month == 12 + ratio = liquid_precip_ratio(pr, prsn, tas, freq="QS-DEC") + winter = ratio.indexes["time"].month == 12 return ratio[winter] diff --git a/xclim/indices/_simple.py b/xclim/indices/_simple.py index 5910e7622..8c2ed32f0 100644 --- a/xclim/indices/_simple.py +++ b/xclim/indices/_simple.py @@ -2,8 +2,10 @@ import xarray as xr -from xclim import run_length as rl, utils -from xclim.utils import declare_units, units +from xclim import run_length as rl +from xclim import utils +from xclim.utils import declare_units +from xclim.utils import units # logging.basicConfig(level=logging.DEBUG) # logging.captureWarnings(True) @@ -18,13 +20,27 @@ # ATTENTION: ASSUME ALL INDICES WRONG UNTIL TESTED ! # # -------------------------------------------------- # -__all__ = ['tg_max', 'tg_mean', 'tg_min', 'tn_max', 'tn_mean', 'tn_min', 'tx_max', 'tx_mean', 'tx_min', - 'base_flow_index', 'consecutive_frost_days', 'frost_days', 'ice_days', 'max_1day_precipitation_amount', - 'precip_accumulation'] - - -@declare_units('[temperature]', tas='[temperature]') -def tg_max(tas, freq='YS'): +__all__ = [ + "tg_max", + "tg_mean", + "tg_min", + "tn_max", + "tn_mean", + "tn_min", + "tx_max", + "tx_mean", + "tx_min", + "base_flow_index", + "consecutive_frost_days", + "frost_days", + "ice_days", + "max_1day_precipitation_amount", + "precip_accumulation", +] + + +@declare_units("[temperature]", tas="[temperature]") +def tg_max(tas, freq="YS"): r"""Highest mean temperature. The maximum of daily mean temperature. @@ -51,11 +67,11 @@ def tg_max(tas, freq='YS'): TNx_j = max(TN_{ij}) """ - return tas.resample(time=freq).max(dim='time', keep_attrs=True) + return tas.resample(time=freq).max(dim="time", keep_attrs=True) -@declare_units('[temperature]', tas='[temperature]') -def tg_mean(tas, freq='YS'): +@declare_units("[temperature]", tas="[temperature]") +def tg_mean(tas, freq="YS"): r"""Mean of daily average temperature. Resample the original daily mean temperature series by taking the mean over each period. @@ -92,11 +108,11 @@ def tg_mean(tas, freq='YS'): """ arr = tas.resample(time=freq) if freq else tas - return arr.mean(dim='time', keep_attrs=True) + return arr.mean(dim="time", keep_attrs=True) -@declare_units('[temperature]', tas='[temperature]') -def tg_min(tas, freq='YS'): +@declare_units("[temperature]", tas="[temperature]") +def tg_min(tas, freq="YS"): r"""Lowest mean temperature Minimum of daily mean temperature. @@ -123,11 +139,11 @@ def tg_min(tas, freq='YS'): TGn_j = min(TG_{ij}) """ - return tas.resample(time=freq).min(dim='time', keep_attrs=True) + return tas.resample(time=freq).min(dim="time", keep_attrs=True) -@declare_units('[temperature]', tasmin='[temperature]') -def tn_max(tasmin, freq='YS'): +@declare_units("[temperature]", tasmin="[temperature]") +def tn_max(tasmin, freq="YS"): r"""Highest minimum temperature. The maximum of daily minimum temperature. @@ -154,11 +170,11 @@ def tn_max(tasmin, freq='YS'): TNx_j = max(TN_{ij}) """ - return tasmin.resample(time=freq).max(dim='time', keep_attrs=True) + return tasmin.resample(time=freq).max(dim="time", keep_attrs=True) -@declare_units('[temperature]', tasmin='[temperature]') -def tn_mean(tasmin, freq='YS'): +@declare_units("[temperature]", tasmin="[temperature]") +def tn_mean(tasmin, freq="YS"): r"""Mean minimum temperature. Mean of daily minimum temperature. @@ -186,11 +202,11 @@ def tn_mean(tasmin, freq='YS'): """ arr = tasmin.resample(time=freq) if freq else tasmin - return arr.mean(dim='time', keep_attrs=True) + return arr.mean(dim="time", keep_attrs=True) -@declare_units('[temperature]', tasmin='[temperature]') -def tn_min(tasmin, freq='YS'): +@declare_units("[temperature]", tasmin="[temperature]") +def tn_min(tasmin, freq="YS"): r"""Lowest minimum temperature Minimum of daily minimum temperature. @@ -217,11 +233,11 @@ def tn_min(tasmin, freq='YS'): TNn_j = min(TN_{ij}) """ - return tasmin.resample(time=freq).min(dim='time', keep_attrs=True) + return tasmin.resample(time=freq).min(dim="time", keep_attrs=True) -@declare_units('[temperature]', tasmax='[temperature]') -def tx_max(tasmax, freq='YS'): +@declare_units("[temperature]", tasmax="[temperature]") +def tx_max(tasmax, freq="YS"): r"""Highest max temperature The maximum value of daily maximum temperature. @@ -248,11 +264,11 @@ def tx_max(tasmax, freq='YS'): TXx_j = max(TX_{ij}) """ - return tasmax.resample(time=freq).max(dim='time', keep_attrs=True) + return tasmax.resample(time=freq).max(dim="time", keep_attrs=True) -@declare_units('[temperature]', tasmax='[temperature]') -def tx_mean(tasmax, freq='YS'): +@declare_units("[temperature]", tasmax="[temperature]") +def tx_mean(tasmax, freq="YS"): r"""Mean max temperature The mean of daily maximum temperature. @@ -280,11 +296,11 @@ def tx_mean(tasmax, freq='YS'): """ arr = tasmax.resample(time=freq) if freq else tasmax - return arr.mean(dim='time', keep_attrs=True) + return arr.mean(dim="time", keep_attrs=True) -@declare_units('[temperature]', tasmax='[temperature]') -def tx_min(tasmax, freq='YS'): +@declare_units("[temperature]", tasmax="[temperature]") +def tx_min(tasmax, freq="YS"): r"""Lowest max temperature The minimum of daily maximum temperature. @@ -311,11 +327,11 @@ def tx_min(tasmax, freq='YS'): TXn_j = min(TX_{ij}) """ - return tasmax.resample(time=freq).min(dim='time', keep_attrs=True) + return tasmax.resample(time=freq).min(dim="time", keep_attrs=True) -@declare_units('', q='[discharge]') -def base_flow_index(q, freq='YS'): +@declare_units("", q="[discharge]") +def base_flow_index(q, freq="YS"): r"""Base flow index Return the base flow index, defined as the minimum 7-day average flow divided by the mean flow. @@ -353,12 +369,12 @@ def base_flow_index(q, freq='YS'): m7 = q.rolling(time=7, center=True).mean().resample(time=freq) mq = q.resample(time=freq) - m7m = m7.min(dim='time') - return m7m / mq.mean(dim='time') + m7m = m7.min(dim="time") + return m7m / mq.mean(dim="time") -@declare_units('days', tasmin='[temperature]') -def consecutive_frost_days(tasmin, freq='AS-JUL'): +@declare_units("days", tasmin="[temperature]") +def consecutive_frost_days(tasmin, freq="AS-JUL"): r"""Maximum number of consecutive frost days (Tmin < 0℃). Resample the daily minimum temperature series by computing the maximum number @@ -390,17 +406,17 @@ def consecutive_frost_days(tasmin, freq='AS-JUL'): where :math:`[P]` is 1 if :math:`P` is true, and 0 if false. Note that this formula does not handle sequences at the start and end of the series, but the numerical algorithm does. """ - tu = units.parse_units(tasmin.attrs['units'].replace('-', '**-')) - fu = 'degC' + tu = units.parse_units(tasmin.attrs["units"].replace("-", "**-")) + fu = "degC" frz = 0 if fu != tu: frz = units.convert(frz, fu, tu) group = (tasmin < frz).resample(time=freq) - return group.apply(rl.longest_run, dim='time') + return group.apply(rl.longest_run, dim="time") -@declare_units('days', tasmin='[temperature]') -def frost_days(tasmin, freq='YS'): +@declare_units("days", tasmin="[temperature]") +def frost_days(tasmin, freq="YS"): r"""Frost days index Number of days where daily minimum temperatures are below 0℃. @@ -426,17 +442,17 @@ def frost_days(tasmin, freq='YS'): TN_{ij} < 0℃ """ - tu = units.parse_units(tasmin.attrs['units'].replace('-', '**-')) - fu = 'degC' + tu = units.parse_units(tasmin.attrs["units"].replace("-", "**-")) + fu = "degC" frz = 0 if fu != tu: frz = units.convert(frz, fu, tu) f = (tasmin < frz) * 1 - return f.resample(time=freq).sum(dim='time') + return f.resample(time=freq).sum(dim="time") -@declare_units('days', tasmax='[temperature]') -def ice_days(tasmax, freq='YS'): +@declare_units("days", tasmax="[temperature]") +def ice_days(tasmax, freq="YS"): r"""Number of ice/freezing days Number of days where daily maximum temperatures are below 0℃. @@ -462,17 +478,17 @@ def ice_days(tasmax, freq='YS'): TX_{ij} < 0℃ """ - tu = units.parse_units(tasmax.attrs['units'].replace('-', '**-')) - fu = 'degC' + tu = units.parse_units(tasmax.attrs["units"].replace("-", "**-")) + fu = "degC" frz = 0 if fu != tu: frz = units.convert(frz, fu, tu) f = (tasmax < frz) * 1 - return f.resample(time=freq).sum(dim='time') + return f.resample(time=freq).sum(dim="time") -@declare_units('mm/day', pr='[precipitation]') -def max_1day_precipitation_amount(pr, freq='YS'): +@declare_units("mm/day", pr="[precipitation]") +def max_1day_precipitation_amount(pr, freq="YS"): r"""Highest 1-day precipitation amount for a period (frequency). Resample the original daily total precipitation temperature series by taking the max over each period. @@ -506,12 +522,12 @@ def max_1day_precipitation_amount(pr, freq='YS'): >>> rx1day = max_1day_precipitation_amount(pr, freq="YS") """ - out = pr.resample(time=freq).max(dim='time', keep_attrs=True) - return utils.convert_units_to(out, 'mm/day', 'hydro') + out = pr.resample(time=freq).max(dim="time", keep_attrs=True) + return utils.convert_units_to(out, "mm/day", "hydro") -@declare_units('mm', pr='[precipitation]') -def precip_accumulation(pr, freq='YS'): +@declare_units("mm", pr="[precipitation]") +def precip_accumulation(pr, freq="YS"): r"""Accumulated total (liquid + solid) precipitation. Resample the original daily mean precipitation flux and accumulate over each period. @@ -547,5 +563,5 @@ def precip_accumulation(pr, freq='YS'): >>> prcp_tot_seasonal = precip_accumulation(pr_day, freq="QS-DEC") """ - out = pr.resample(time=freq).sum(dim='time', keep_attrs=True) - return utils.pint_multiply(out, 1 * units.day, 'mm') + out = pr.resample(time=freq).sum(dim="time", keep_attrs=True) + return utils.pint_multiply(out, 1 * units.day, "mm") diff --git a/xclim/indices/_threshold.py b/xclim/indices/_threshold.py index fc5394736..599766f3f 100644 --- a/xclim/indices/_threshold.py +++ b/xclim/indices/_threshold.py @@ -3,8 +3,10 @@ import numpy as np import xarray as xr -from xclim import utils, run_length as rl -from xclim.utils import declare_units, units +from xclim import run_length as rl +from xclim import utils +from xclim.utils import declare_units +from xclim.utils import units # logging.basicConfig(level=logging.DEBUG) # logging.captureWarnings(True) @@ -18,14 +20,29 @@ # ATTENTION: ASSUME ALL INDICES WRONG UNTIL TESTED ! # # -------------------------------------------------- # -__all__ = ['cold_spell_days', 'daily_pr_intensity', 'maximum_consecutive_wet_days', 'cooling_degree_days', - 'freshet_start', 'growing_degree_days', 'growing_season_length', 'heat_wave_index', 'heating_degree_days', - 'tn_days_below', 'tx_days_above', 'warm_day_frequency', 'warm_night_frequency', 'wetdays', - 'maximum_consecutive_dry_days', 'max_n_day_precipitation_amount', 'tropical_nights'] - - -@declare_units('days', tas='[temperature]', thresh='[temperature]') -def cold_spell_days(tas, thresh='-10 degC', window=5, freq='AS-JUL'): +__all__ = [ + "cold_spell_days", + "daily_pr_intensity", + "maximum_consecutive_wet_days", + "cooling_degree_days", + "freshet_start", + "growing_degree_days", + "growing_season_length", + "heat_wave_index", + "heating_degree_days", + "tn_days_below", + "tx_days_above", + "warm_day_frequency", + "warm_night_frequency", + "wetdays", + "maximum_consecutive_dry_days", + "max_n_day_precipitation_amount", + "tropical_nights", +] + + +@declare_units("days", tas="[temperature]", thresh="[temperature]") +def cold_spell_days(tas, thresh="-10 degC", window=5, freq="AS-JUL"): r"""Cold spell days The number of days that are part of a cold spell, defined as five or more consecutive days with mean daily @@ -63,11 +80,11 @@ def cold_spell_days(tas, thresh='-10 degC', window=5, freq='AS-JUL'): over = tas < t group = over.resample(time=freq) - return group.apply(rl.windowed_run_count, window=window, dim='time') + return group.apply(rl.windowed_run_count, window=window, dim="time") -@declare_units('mm/day', pr='[precipitation]', thresh='[precipitation]') -def daily_pr_intensity(pr, thresh='1 mm/day', freq='YS'): +@declare_units("mm/day", pr="[precipitation]", thresh="[precipitation]") +def daily_pr_intensity(pr, thresh="1 mm/day", freq="YS"): r"""Average daily precipitation intensity Return the average precipitation over wet days. @@ -108,23 +125,23 @@ def daily_pr_intensity(pr, thresh='1 mm/day', freq='YS'): >>> daily_int = daily_pr_intensity(pr, thresh='5 mm/day', freq="QS-DEC") """ - t = utils.convert_units_to(thresh, pr, 'hydro') + t = utils.convert_units_to(thresh, pr, "hydro") # put pr=0 for non wet-days pr_wd = xr.where(pr >= t, pr, 0) - pr_wd.attrs['units'] = pr.units + pr_wd.attrs["units"] = pr.units # sum over wanted period - s = pr_wd.resample(time=freq).sum(dim='time', keep_attrs=True) - sd = utils.pint_multiply(s, 1 * units.day, 'mm') + s = pr_wd.resample(time=freq).sum(dim="time", keep_attrs=True) + sd = utils.pint_multiply(s, 1 * units.day, "mm") # get number of wetdays over period wd = wetdays(pr, thresh=thresh, freq=freq) return sd / wd -@declare_units('days', pr='[precipitation]', thresh='[precipitation]') -def maximum_consecutive_wet_days(pr, thresh='1 mm/day', freq='YS'): +@declare_units("days", pr="[precipitation]", thresh="[precipitation]") +def maximum_consecutive_wet_days(pr, thresh="1 mm/day", freq="YS"): r"""Consecutive wet days. Returns the maximum number of consecutive wet days. @@ -158,14 +175,14 @@ def maximum_consecutive_wet_days(pr, thresh='1 mm/day', freq='YS'): where :math:`[P]` is 1 if :math:`P` is true, and 0 if false. Note that this formula does not handle sequences at the start and end of the series, but the numerical algorithm does. """ - thresh = utils.convert_units_to(thresh, pr, 'hydro') + thresh = utils.convert_units_to(thresh, pr, "hydro") group = (pr > thresh).resample(time=freq) - return group.apply(rl.longest_run, dim='time') + return group.apply(rl.longest_run, dim="time") -@declare_units('C days', tas='[temperature]', thresh='[temperature]') -def cooling_degree_days(tas, thresh='18 degC', freq='YS'): +@declare_units("C days", tas="[temperature]", thresh="[temperature]") +def cooling_degree_days(tas, thresh="18 degC", freq="YS"): r"""Cooling degree days Sum of degree days above the temperature threshold at which spaces are cooled. @@ -197,14 +214,13 @@ def cooling_degree_days(tas, thresh='18 degC', freq='YS'): """ thresh = utils.convert_units_to(thresh, tas) - return tas.pipe(lambda x: x - thresh) \ - .clip(min=0) \ - .resample(time=freq) \ - .sum(dim='time') + return ( + tas.pipe(lambda x: x - thresh).clip(min=0).resample(time=freq).sum(dim="time") + ) -@declare_units('', tas='[temperature]', thresh='[temperature]') -def freshet_start(tas, thresh='0 degC', window=5, freq='YS'): +@declare_units("", tas="[temperature]", thresh="[temperature]") +def freshet_start(tas, thresh="0 degC", window=5, freq="YS"): r"""First day consistently exceeding threshold temperature. Returns first day of period where a temperature threshold is exceeded @@ -240,13 +256,13 @@ def freshet_start(tas, thresh='0 degC', window=5, freq='YS'): 1 if :math:`P` is true, and 0 if false. """ thresh = utils.convert_units_to(thresh, tas) - over = (tas > thresh) + over = tas > thresh group = over.resample(time=freq) - return group.apply(rl.first_run_ufunc, window=window, index='dayofyear') + return group.apply(rl.first_run_ufunc, window=window, index="dayofyear") -@declare_units('C days', tas='[temperature]', thresh='[temperature]') -def growing_degree_days(tas, thresh='4.0 degC', freq='YS'): +@declare_units("C days", tas="[temperature]", thresh="[temperature]") +def growing_degree_days(tas, thresh="4.0 degC", freq="YS"): r"""Growing degree-days over threshold temperature value [℃]. The sum of degree-days over the threshold temperature. @@ -275,14 +291,13 @@ def growing_degree_days(tas, thresh='4.0 degC', freq='YS'): GD4_j = \sum_{i=1}^I (TG_{ij}-{4} | TG_{ij} > {4}℃) """ thresh = utils.convert_units_to(thresh, tas) - return tas.pipe(lambda x: x - thresh) \ - .clip(min=0) \ - .resample(time=freq) \ - .sum(dim='time') + return ( + tas.pipe(lambda x: x - thresh).clip(min=0).resample(time=freq).sum(dim="time") + ) -@declare_units('days', tas='[temperature]', thresh='[temperature]') -def growing_season_length(tas, thresh='5.0 degC', window=6, freq='YS'): +@declare_units("days", tas="[temperature]", thresh="[temperature]") +def growing_season_length(tas, thresh="5.0 degC", window=6, freq="YS"): r"""Growing season length. The number of days between the first occurrence of at least @@ -353,14 +368,14 @@ def growing_season_length(tas, thresh='5.0 degC', window=6, freq='YS'): def compute_gsl(c): nt = c.time.size - i = xr.DataArray(np.arange(nt), dims='time').chunk({'time': 1}) + i = xr.DataArray(np.arange(nt), dims="time").chunk({"time": 1}) ind = xr.broadcast(i, c)[0].chunk(c.chunks) - i1 = ind.where(c == window).min(dim='time') + i1 = ind.where(c == window).min(dim="time") i1 = xr.where(np.isnan(i1), nt, i1) - i11 = i1.reindex_like(c, method='ffill') + i11 = i1.reindex_like(c, method="ffill") i2 = ind.where((c == 0) & (ind > i11)).where(c.time.dt.month >= 7) i2 = xr.where(np.isnan(i2), nt, i2) - d = (i2 - i1).min(dim='time') + d = (i2 - i1).min(dim="time") return d gsl = c.resample(time=freq).apply(compute_gsl) @@ -368,8 +383,8 @@ def compute_gsl(c): return gsl -@declare_units('days', tasmax='[temperature]', thresh='[temperature]') -def heat_wave_index(tasmax, thresh='25.0 degC', window=5, freq='YS'): +@declare_units("days", tasmax="[temperature]", thresh="[temperature]") +def heat_wave_index(tasmax, thresh="25.0 degC", window=5, freq="YS"): r"""Heat wave index. Number of days that are part of a heatwave, defined as five or more consecutive days over 25℃. @@ -394,11 +409,11 @@ def heat_wave_index(tasmax, thresh='25.0 degC', window=5, freq='YS'): over = tasmax > thresh group = over.resample(time=freq) - return group.apply(rl.windowed_run_count, window=window, dim='time') + return group.apply(rl.windowed_run_count, window=window, dim="time") -@declare_units('C days', tas='[temperature]', thresh='[temperature]') -def heating_degree_days(tas, thresh='17.0 degC', freq='YS'): +@declare_units("C days", tas="[temperature]", thresh="[temperature]") +def heating_degree_days(tas, thresh="17.0 degC", freq="YS"): r"""Heating degree days Sum of degree days below the temperature threshold at which spaces are heated. @@ -428,14 +443,11 @@ def heating_degree_days(tas, thresh='17.0 degC', freq='YS'): """ thresh = utils.convert_units_to(thresh, tas) - return tas.pipe(lambda x: thresh - x) \ - .clip(0) \ - .resample(time=freq) \ - .sum(dim='time') + return tas.pipe(lambda x: thresh - x).clip(0).resample(time=freq).sum(dim="time") -@declare_units('days', tasmin='[temperature]', thresh='[temperature]') -def tn_days_below(tasmin, thresh='-10.0 degC', freq='YS'): +@declare_units("days", tasmin="[temperature]", thresh="[temperature]") +def tn_days_below(tasmin, thresh="-10.0 degC", freq="YS"): r"""Number of days with tmin below a threshold in Number of days where daily minimum temperature is below a threshold. @@ -464,12 +476,12 @@ def tn_days_below(tasmin, thresh='-10.0 degC', freq='YS'): TX_{ij} < Threshold [℃] """ thresh = utils.convert_units_to(thresh, tasmin) - f1 = utils.threshold_count(tasmin, '<', thresh, freq) + f1 = utils.threshold_count(tasmin, "<", thresh, freq) return f1 -@declare_units('days', tasmax='[temperature]', thresh='[temperature]') -def tx_days_above(tasmax, thresh='25.0 degC', freq='YS'): +@declare_units("days", tasmax="[temperature]", thresh="[temperature]") +def tx_days_above(tasmax, thresh="25.0 degC", freq="YS"): r"""Number of summer days Number of days where daily maximum temperature exceed a threshold. @@ -499,11 +511,11 @@ def tx_days_above(tasmax, thresh='25.0 degC', freq='YS'): """ thresh = utils.convert_units_to(thresh, tasmax) f = (tasmax > (thresh)) * 1 - return f.resample(time=freq).sum(dim='time') + return f.resample(time=freq).sum(dim="time") -@declare_units('days', tasmax='[temperature]', thresh='[temperature]') -def warm_day_frequency(tasmax, thresh='30 degC', freq='YS'): +@declare_units("days", tasmax="[temperature]", thresh="[temperature]") +def warm_day_frequency(tasmax, thresh="30 degC", freq="YS"): r"""Frequency of extreme warm days Return the number of days with tasmax > thresh per period @@ -533,11 +545,11 @@ def warm_day_frequency(tasmax, thresh='30 degC', freq='YS'): """ thresh = utils.convert_units_to(thresh, tasmax) events = (tasmax > thresh) * 1 - return events.resample(time=freq).sum(dim='time') + return events.resample(time=freq).sum(dim="time") -@declare_units('days', tasmin='[temperature]', thresh='[temperature]') -def warm_night_frequency(tasmin, thresh='22 degC', freq='YS'): +@declare_units("days", tasmin="[temperature]", thresh="[temperature]") +def warm_night_frequency(tasmin, thresh="22 degC", freq="YS"): r"""Frequency of extreme warm nights Return the number of days with tasmin > thresh per period @@ -556,13 +568,13 @@ def warm_night_frequency(tasmin, thresh='22 degC', freq='YS'): xarray.DataArray The number of days with tasmin > thresh per period """ - thresh = utils.convert_units_to(thresh, tasmin, ) + thresh = utils.convert_units_to(thresh, tasmin) events = (tasmin > thresh) * 1 - return events.resample(time=freq).sum(dim='time') + return events.resample(time=freq).sum(dim="time") -@declare_units('days', pr='[precipitation]', thresh='[precipitation]') -def wetdays(pr, thresh='1.0 mm/day', freq='YS'): +@declare_units("days", pr="[precipitation]", thresh="[precipitation]") +def wetdays(pr, thresh="1.0 mm/day", freq="YS"): r"""Wet days Return the total number of days during period with precipitation over threshold. @@ -590,14 +602,14 @@ def wetdays(pr, thresh='1.0 mm/day', freq='YS'): >>> pr = xr.open_dataset('pr.day.nc') >>> wd = wetdays(pr, pr_min = 5., freq="QS-DEC") """ - thresh = utils.convert_units_to(thresh, pr, 'hydro') + thresh = utils.convert_units_to(thresh, pr, "hydro") wd = (pr >= thresh) * 1 - return wd.resample(time=freq).sum(dim='time') + return wd.resample(time=freq).sum(dim="time") -@declare_units('days', pr='[precipitation]', thresh='[precipitation]') -def maximum_consecutive_dry_days(pr, thresh='1 mm/day', freq='YS'): +@declare_units("days", pr="[precipitation]", thresh="[precipitation]") +def maximum_consecutive_dry_days(pr, thresh="1 mm/day", freq="YS"): r"""Maximum number of consecutive dry days Return the maximum number of consecutive days within the period where precipitation @@ -631,14 +643,14 @@ def maximum_consecutive_dry_days(pr, thresh='1 mm/day', freq='YS'): where :math:`[P]` is 1 if :math:`P` is true, and 0 if false. Note that this formula does not handle sequences at the start and end of the series, but the numerical algorithm does. """ - t = utils.convert_units_to(thresh, pr, 'hydro') + t = utils.convert_units_to(thresh, pr, "hydro") group = (pr < t).resample(time=freq) - return group.apply(rl.longest_run, dim='time') + return group.apply(rl.longest_run, dim="time") -@declare_units('mm', pr='[precipitation]') -def max_n_day_precipitation_amount(pr, window=1, freq='YS'): +@declare_units("mm", pr="[precipitation]") +def max_n_day_precipitation_amount(pr, window=1, freq="YS"): r"""Highest precipitation amount cumulated over a n-day moving window. Calculate the n-day rolling sum of the original daily total precipitation series @@ -670,15 +682,15 @@ def max_n_day_precipitation_amount(pr, window=1, freq='YS'): # rolling sum of the values arr = pr.rolling(time=window, center=False).sum() - out = arr.resample(time=freq).max(dim='time', keep_attrs=True) + out = arr.resample(time=freq).max(dim="time", keep_attrs=True) - out.attrs['units'] = pr.units + out.attrs["units"] = pr.units # Adjust values and units to make sure they are daily - return utils.pint_multiply(out, 1 * units.day, 'mm') + return utils.pint_multiply(out, 1 * units.day, "mm") -@declare_units('days', tasmin='[temperature]', thresh='[temperature]') -def tropical_nights(tasmin, thresh='20.0 degC', freq='YS'): +@declare_units("days", tasmin="[temperature]", thresh="[temperature]") +def tropical_nights(tasmin, thresh="20.0 degC", freq="YS"): r"""Tropical nights The number of days with minimum daily temperature above threshold. @@ -707,6 +719,6 @@ def tropical_nights(tasmin, thresh='20.0 degC', freq='YS'): TN_{ij} > Threshold [℃] """ thresh = utils.convert_units_to(thresh, tasmin) - return tasmin.pipe(lambda x: (tasmin > thresh) * 1) \ - .resample(time=freq) \ - .sum(dim='time') + return ( + tasmin.pipe(lambda x: (tasmin > thresh) * 1).resample(time=freq).sum(dim="time") + ) diff --git a/xclim/run_length.py b/xclim/run_length.py index bca23573c..49f03f3e8 100644 --- a/xclim/run_length.py +++ b/xclim/run_length.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- """Run length algorithms module""" +import logging +from warnings import warn import numpy as np import xarray as xr -import logging -from warnings import warn logging.captureWarnings(True) npts_opt = 9000 @@ -25,20 +25,24 @@ def get_npts(da): """ coords = list(da.coords) - coords.remove('time') + coords.remove("time") npts = 1 for c in coords: npts *= da[c].size return npts -def rle(da, dim='time', max_chunk=1000000): +def rle(da, dim="time", max_chunk=1000000): n = len(da[dim]) - i = xr.DataArray(np.arange(da[dim].size), dims=dim).chunk({'time': 1}) + i = xr.DataArray(np.arange(da[dim].size), dims=dim).chunk({"time": 1}) ind = xr.broadcast(i, da)[0].chunk(da.chunks) b = ind.where(~da) # find indexes where false - end1 = da.where(b[dim] == b[dim][-1], drop=True) * 0 + n # add additional end value index (deal with end cases) - start1 = da.where(b[dim] == b[dim][0], drop=True) * 0 - 1 # add additional start index (deal with end cases) + end1 = ( + da.where(b[dim] == b[dim][-1], drop=True) * 0 + n + ) # add additional end value index (deal with end cases) + start1 = ( + da.where(b[dim] == b[dim][0], drop=True) * 0 - 1 + ) # add additional start index (deal with end cases) b = xr.concat([start1, b, end1], dim) # Ensure bfill operates on entire (unchunked) time dimension @@ -64,7 +68,7 @@ def rle(da, dim='time', max_chunk=1000000): return d -def longest_run(da, dim='time', ufunc_1dim='auto'): +def longest_run(da, dim="time", ufunc_1dim="auto"): """Return the length of the longest consecutive run of True values. Parameters @@ -82,7 +86,7 @@ def longest_run(da, dim='time', ufunc_1dim='auto'): N-dimensional array (int) Length of longest run of True values along dimension """ - if ufunc_1dim == 'auto': + if ufunc_1dim == "auto": npts = get_npts(da) ufunc_1dim = npts <= npts_opt @@ -95,7 +99,7 @@ def longest_run(da, dim='time', ufunc_1dim='auto'): return rl_long -def windowed_run_events(da, window, dim='time', ufunc_1dim='auto'): +def windowed_run_events(da, window, dim="time", ufunc_1dim="auto"): """Return the number of runs of a minimum length. Parameters @@ -115,7 +119,7 @@ def windowed_run_events(da, window, dim='time', ufunc_1dim='auto'): out : N-dimensional xarray data array (int) Number of distinct runs of a minimum length. """ - if ufunc_1dim == 'auto': + if ufunc_1dim == "auto": npts = get_npts(da) ufunc_1dim = npts <= npts_opt @@ -127,7 +131,7 @@ def windowed_run_events(da, window, dim='time', ufunc_1dim='auto'): return out -def windowed_run_count(da, window, dim='time', ufunc_1dim='auto'): +def windowed_run_count(da, window, dim="time", ufunc_1dim="auto"): """Return the number of consecutive true values in array for runs at least as long as given duration. Parameters @@ -148,7 +152,7 @@ def windowed_run_count(da, window, dim='time', ufunc_1dim='auto'): out : N-dimensional xarray data array (int) Total number of true values part of a consecutive runs of at least `window` long. """ - if ufunc_1dim == 'auto': + if ufunc_1dim == "auto": npts = get_npts(da) ufunc_1dim = npts <= npts_opt @@ -160,7 +164,7 @@ def windowed_run_count(da, window, dim='time', ufunc_1dim='auto'): return out -def first_run(da, window, dim='time', ufunc_1dim='auto'): +def first_run(da, window, dim="time", ufunc_1dim="auto"): """Return the index of the first item of a run of at least a given length. Parameters @@ -182,7 +186,7 @@ def first_run(da, window, dim='time', ufunc_1dim='auto'): out : N-dimensional xarray data array (int) Index of first item in first valid run. Returns np.nan if there are no valid run. """ - if ufunc_1dim == 'auto': + if ufunc_1dim == "auto": npts = get_npts(da) ufunc_1dim = npts <= npts_opt @@ -191,15 +195,16 @@ def first_run(da, window, dim='time', ufunc_1dim='auto'): else: dims = list(da.dims) - if 'time' not in dims: - da['time'] = da[dim] - da.swap_dims({dim: 'time'}) - da = da.astype('int') - i = xr.DataArray(np.arange(da[dim].size), dims=dim).chunk({'time': 1}) + if "time" not in dims: + da["time"] = da[dim] + da.swap_dims({dim: "time"}) + da = da.astype("int") + i = xr.DataArray(np.arange(da[dim].size), dims=dim).chunk({"time": 1}) ind = xr.broadcast(i, da)[0].chunk(da.chunks) wind_sum = da.rolling(time=window).sum() out = ind.where(wind_sum >= window).min(dim=dim) - ( - window - 1) # remove window -1 as rolling result index is last element of the moving window + window - 1 + ) # remove window -1 as rolling result index is last element of the moving window return out @@ -232,7 +237,7 @@ def rle_1d(arr): n = len(ia) if n == 0: - e = 'run length array empty' + e = "run length array empty" warn(e) return None, None, None @@ -339,14 +344,16 @@ def windowed_run_count_ufunc(x, window): out : func A function operating along the time dimension of a dask-array. """ - return xr.apply_ufunc(windowed_run_count_1d, - x, - input_core_dims=[['time'], ], - vectorize=True, - dask='parallelized', - output_dtypes=[np.int, ], - keep_attrs=True, - kwargs={'window': window}) + return xr.apply_ufunc( + windowed_run_count_1d, + x, + input_core_dims=[["time"]], + vectorize=True, + dask="parallelized", + output_dtypes=[np.int], + keep_attrs=True, + kwargs={"window": window}, + ) def windowed_run_events_ufunc(x, window): @@ -364,14 +371,16 @@ def windowed_run_events_ufunc(x, window): out : func A function operating along the time dimension of a dask-array. """ - return xr.apply_ufunc(windowed_run_events_1d, - x, - input_core_dims=[['time'], ], - vectorize=True, - dask='parallelized', - output_dtypes=[np.int, ], - keep_attrs=True, - kwargs={'window': window}) + return xr.apply_ufunc( + windowed_run_events_1d, + x, + input_core_dims=[["time"]], + vectorize=True, + dask="parallelized", + output_dtypes=[np.int], + keep_attrs=True, + kwargs={"window": window}, + ) def longest_run_ufunc(x): @@ -388,29 +397,31 @@ def longest_run_ufunc(x): out : func A function operating along the time dimension of a dask-array. """ - return xr.apply_ufunc(longest_run_1d, - x, - input_core_dims=[['time'], ], - vectorize=True, - dask='parallelized', - output_dtypes=[np.int, ], - keep_attrs=True, - ) + return xr.apply_ufunc( + longest_run_1d, + x, + input_core_dims=[["time"]], + vectorize=True, + dask="parallelized", + output_dtypes=[np.int], + keep_attrs=True, + ) def first_run_ufunc(x, window, index=None): - ind = xr.apply_ufunc(first_run_1d, - x, - input_core_dims=[['time'], ], - vectorize=True, - dask='parallelized', - output_dtypes=[np.float, ], - keep_attrs=True, - kwargs={'window': window} - ) + ind = xr.apply_ufunc( + first_run_1d, + x, + input_core_dims=[["time"]], + vectorize=True, + dask="parallelized", + output_dtypes=[np.float], + keep_attrs=True, + kwargs={"window": window}, + ) if index is not None and ~np.isnan(ind): - val = getattr(x.indexes['time'], index) + val = getattr(x.indexes["time"], index) i = int(ind.data) ind.data = val[i] diff --git a/xclim/streamflow.py b/xclim/streamflow.py index c4e8d05a0..89e0ca98d 100644 --- a/xclim/streamflow.py +++ b/xclim/streamflow.py @@ -1,25 +1,27 @@ # -*- coding: utf-8 -*- import numpy as np -from xclim.indices import base_flow_index from xclim import checks -from xclim.utils import Indicator, wrapped_partial from xclim import generic +from xclim.indices import base_flow_index +from xclim.utils import Indicator +from xclim.utils import wrapped_partial + # from boltons.funcutils import FunctionBuilder # import calendar class Streamflow(Indicator): - units = 'm^3 s-1' - context = 'hydro' - standard_name = 'discharge' + units = "m^3 s-1" + context = "hydro" + standard_name = "discharge" @staticmethod def compute(*args, **kwds): pass def cfprobe(self, q): - checks.check_valid(q, 'standard_name', 'streamflow') + checks.check_valid(q, "standard_name", "streamflow") class Stats(Streamflow): @@ -27,8 +29,8 @@ def missing(self, *args, **kwds): """Return whether an output is considered missing or not.""" from functools import reduce - indexer = kwds['indexer'] - freq = kwds['freq'] or generic.default_freq(**indexer) + indexer = kwds["indexer"] + freq = kwds["freq"] or generic.default_freq(**indexer) miss = (checks.missing_any(da, freq, **indexer) for da in args) return reduce(np.logical_or, miss) @@ -47,50 +49,62 @@ def validate(self, da): pass -base_flow_index = Streamflow(identifier='base_flow_index', - units='', - long_name="Base flow index", - compute=base_flow_index) - - -freq_analysis = FA(identifier='freq_analysis', - var_name='q{window}{mode}{indexer}', - long_name='N-year return period {mode} {indexer} {window}-day flow', - description="Streamflow frequency analysis for the {mode} {indexer} {window}-day flow " - "estimated using the {dist} distribution.", - compute=generic.frequency_analysis) - - -stats = Stats(identifier='stats', - var_name='q{indexer}{op}', - long_name='{freq} {op} of {indexer} daily flow ', - description="{freq} {op} of {indexer} daily flow", - compute=generic.select_resample_op) - - -fit = Fit(identifier='fit', - var_name='params', - units='', - standard_name='{dist} parameters', - long_name="{dist} distribution parameters", - description="Parameters of the {dist} distribution fitted ", - cell_methods='time: fit', - compute=generic.fit) - - -doy_qmax = Streamflow(identifier='doy_qmax', - var_name='q{indexer}_doy_qmax', - long_name='Day of the year of the maximum over {indexer}', - description='Day of the year of the maximum over {indexer}', - units='', - _partial=True, - compute=wrapped_partial(generic.select_resample_op, op=generic.doymax)) - - -doy_qmin = Streamflow(identifier='doy_qmin', - var_name='q{indexer}_doy_qmin', - long_name='Day of the year of the minimum over {indexer}', - description='Day of the year of the minimum over {indexer}', - units='', - _partial=True, - compute=wrapped_partial(generic.select_resample_op, op=generic.doymin)) +base_flow_index = Streamflow( + identifier="base_flow_index", + units="", + long_name="Base flow index", + compute=base_flow_index, +) + + +freq_analysis = FA( + identifier="freq_analysis", + var_name="q{window}{mode}{indexer}", + long_name="N-year return period {mode} {indexer} {window}-day flow", + description="Streamflow frequency analysis for the {mode} {indexer} {window}-day flow " + "estimated using the {dist} distribution.", + compute=generic.frequency_analysis, +) + + +stats = Stats( + identifier="stats", + var_name="q{indexer}{op}", + long_name="{freq} {op} of {indexer} daily flow ", + description="{freq} {op} of {indexer} daily flow", + compute=generic.select_resample_op, +) + + +fit = Fit( + identifier="fit", + var_name="params", + units="", + standard_name="{dist} parameters", + long_name="{dist} distribution parameters", + description="Parameters of the {dist} distribution fitted ", + cell_methods="time: fit", + compute=generic.fit, +) + + +doy_qmax = Streamflow( + identifier="doy_qmax", + var_name="q{indexer}_doy_qmax", + long_name="Day of the year of the maximum over {indexer}", + description="Day of the year of the maximum over {indexer}", + units="", + _partial=True, + compute=wrapped_partial(generic.select_resample_op, op=generic.doymax), +) + + +doy_qmin = Streamflow( + identifier="doy_qmin", + var_name="q{indexer}_doy_qmin", + long_name="Day of the year of the minimum over {indexer}", + description="Day of the year of the minimum over {indexer}", + units="", + _partial=True, + compute=wrapped_partial(generic.select_resample_op, op=generic.doymin), +) diff --git a/xclim/subset.py b/xclim/subset.py index 5e6ad21dc..b4335c345 100644 --- a/xclim/subset.py +++ b/xclim/subset.py @@ -47,11 +47,15 @@ def subset_bbox(da, lon_bnds=None, lat_bnds=None, start_yr=None, end_yr=None): lon_bnds[lon_bnds < 0] += 360 if np.all(da.lon < 0) and np.any(lon_bnds > 0): lon_bnds[lon_bnds < 0] -= 360 - da = da.where((da.lon >= lon_bnds.min()) & (da.lon <= lon_bnds.max()), drop=True) + da = da.where( + (da.lon >= lon_bnds.min()) & (da.lon <= lon_bnds.max()), drop=True + ) if lat_bnds is not None: lat_bnds = np.asarray(lat_bnds) - da = da.where((da.lat >= lat_bnds.min()) & (da.lat <= lat_bnds.max()), drop=True) + da = da.where( + (da.lat >= lat_bnds.min()) & (da.lat <= lat_bnds.max()), drop=True + ) if start_yr or end_yr: if not start_yr: @@ -63,7 +67,10 @@ def subset_bbox(da, lon_bnds=None, lat_bnds=None, start_yr=None, end_yr=None): raise ValueError("Start date is after end date.") year_bnds = np.asarray([start_yr, end_yr]) - da = da.where((da.time.dt.year >= year_bnds.min()) & (da.time.dt.year <= year_bnds.max()), drop=True) + da = da.where( + (da.time.dt.year >= year_bnds.min()) & (da.time.dt.year <= year_bnds.max()), + drop=True, + ) return da @@ -107,7 +114,7 @@ def subset_gridpoint(da, lon, lat, start_yr=None, end_yr=None): >>> dsSub = subset.subset_gridpoint(ds, lon=-75,lat=45,start_yr=1990,end_yr=1999) """ - g = Geod(ellps='WGS84') # WGS84 ellipsoid - decent globaly + g = Geod(ellps="WGS84") # WGS84 ellipsoid - decent globaly # adjust negative/positive longitudes if necessary if np.all(da.lon > 0) and lon < 0: lon += 360 @@ -125,11 +132,13 @@ def subset_gridpoint(da, lon, lat, start_yr=None, end_yr=None): lon1 = np.reshape(lon1, lon1.size) lat1 = np.reshape(lat1, lat1.size) # calculate geodesic distance between grid points and point of interest - az12, az21, dist = g.inv(lon1, lat1, np.broadcast_to(lon, lon1.shape), np.broadcast_to(lat, lat1.shape)) + az12, az21, dist = g.inv( + lon1, lat1, np.broadcast_to(lon, lon1.shape), np.broadcast_to(lat, lat1.shape) + ) dist = dist.reshape(shp_orig) iy, ix = np.unravel_index(np.argmin(dist, axis=None), dist.shape) - xydims = [x for x in da.dims if 'time' not in x] + xydims = [x for x in da.dims if "time" not in x] args = dict() args[xydims[0]] = iy @@ -149,7 +158,9 @@ def subset_gridpoint(da, lon, lat, start_yr=None, end_yr=None): if len(year_bnds) == 1: time_cond = da.time.dt.year == year_bnds else: - time_cond = (da.time.dt.year >= year_bnds.min()) & (da.time.dt.year <= year_bnds.max()) + time_cond = (da.time.dt.year >= year_bnds.min()) & ( + da.time.dt.year <= year_bnds.max() + ) out = out.where(time_cond, drop=True) return out diff --git a/xclim/testing/common.py b/xclim/testing/common.py index b4b70bbef..b482ed663 100644 --- a/xclim/testing/common.py +++ b/xclim/testing/common.py @@ -5,47 +5,75 @@ @pytest.fixture def tas_series(): - def _tas_series(values, start='7/1/2000'): + def _tas_series(values, start="7/1/2000"): coords = pd.date_range(start, periods=len(values), freq=pd.DateOffset(days=1)) - return xr.DataArray(values, coords=[coords, ], dims='time', name='tas', - attrs={'standard_name': 'air_temperature', - 'cell_methods': 'time: mean within days', - 'units': 'K'}) + return xr.DataArray( + values, + coords=[coords], + dims="time", + name="tas", + attrs={ + "standard_name": "air_temperature", + "cell_methods": "time: mean within days", + "units": "K", + }, + ) return _tas_series @pytest.fixture def tasmax_series(): - def _tasmax_series(values, start='7/1/2000'): + def _tasmax_series(values, start="7/1/2000"): coords = pd.date_range(start, periods=len(values), freq=pd.DateOffset(days=1)) - return xr.DataArray(values, coords=[coords, ], dims='time', name='tasmax', - attrs={'standard_name': 'air_temperature', - 'cell_methods': 'time: maximum within days', - 'units': 'K'}) + return xr.DataArray( + values, + coords=[coords], + dims="time", + name="tasmax", + attrs={ + "standard_name": "air_temperature", + "cell_methods": "time: maximum within days", + "units": "K", + }, + ) return _tasmax_series @pytest.fixture def tasmin_series(): - def _tasmin_series(values, start='7/1/2000'): + def _tasmin_series(values, start="7/1/2000"): coords = pd.date_range(start, periods=len(values), freq=pd.DateOffset(days=1)) - return xr.DataArray(values, coords=[coords, ], dims='time', name='tasmin', - attrs={'standard_name': 'air_temperature', - 'cell_methods': 'time: minimum within days', - 'units': 'K'}) + return xr.DataArray( + values, + coords=[coords], + dims="time", + name="tasmin", + attrs={ + "standard_name": "air_temperature", + "cell_methods": "time: minimum within days", + "units": "K", + }, + ) return _tasmin_series @pytest.fixture def pr_series(): - def _pr_series(values, start='7/1/2000'): + def _pr_series(values, start="7/1/2000"): coords = pd.date_range(start, periods=len(values), freq=pd.DateOffset(days=1)) - return xr.DataArray(values, coords=[coords, ], dims='time', name='pr', - attrs={'standard_name': 'precipitation_flux', - 'cell_methods': 'time: sum over day', - 'units': 'kg m-2 s-1'}) + return xr.DataArray( + values, + coords=[coords], + dims="time", + name="pr", + attrs={ + "standard_name": "precipitation_flux", + "cell_methods": "time: sum over day", + "units": "kg m-2 s-1", + }, + ) return _pr_series diff --git a/xclim/utils.py b/xclim/utils.py index df4d99938..28a4e337b 100644 --- a/xclim/utils.py +++ b/xclim/utils.py @@ -1,16 +1,15 @@ # -*- coding: utf-8 -*- - """ xclim xarray.DataArray utilities module """ - -import abc +# import abc import calendar import datetime as dt import functools import re import warnings -from collections import defaultdict, OrderedDict +from collections import defaultdict +from collections import OrderedDict from inspect import signature import numpy as np @@ -18,22 +17,29 @@ import xarray as xr from boltons.funcutils import wraps -from . import checks import xclim +from . import checks units = pint.UnitRegistry(autoconvert_offset_to_baseunit=True) -units.define(pint.unit.UnitDefinition('percent', '%', (), - pint.converters.ScaleConverter(0.01))) +units.define( + pint.unit.UnitDefinition("percent", "%", (), pint.converters.ScaleConverter(0.01)) +) # Define commonly encountered units not defined by pint -units.define('degrees_north = degree = degrees_N = degreesN = degree_north = degree_N ' - '= degreeN') -units.define('degrees_east = degree = degrees_E = degreesE = degree_east = degree_E = degreeE') -units.define("degC = kelvin; offset: 273.15 = celsius = C") # add 'C' as an abbrev for celsius (default Coulomb) +units.define( + "degrees_north = degree = degrees_N = degreesN = degree_north = degree_N " + "= degreeN" +) +units.define( + "degrees_east = degree = degrees_E = degreesE = degree_east = degree_E = degreeE" +) +units.define( + "degC = kelvin; offset: 273.15 = celsius = C" +) # add 'C' as an abbrev for celsius (default Coulomb) units.define("d = day") # Default context. -null = pint.Context('none') +null = pint.Context("none") units.add_context(null) # Precipitation units. This is an artificial unit that we're using to verify that a given unit can be converted into @@ -44,12 +50,22 @@ units.define("[discharge] = [length] ** 3 / [time]") units.define("cms = meter ** 3 / second") -hydro = pint.Context('hydro') -hydro.add_transformation('[mass] / [length]**2', '[length]', lambda ureg, x: x / (1000 * ureg.kg / ureg.m ** 3)) -hydro.add_transformation('[mass] / [length]**2 / [time]', '[length] / [time]', - lambda ureg, x: x / (1000 * ureg.kg / ureg.m ** 3)) -hydro.add_transformation('[length] / [time]', '[mass] / [length]**2 / [time]', - lambda ureg, x: x * (1000 * ureg.kg / ureg.m ** 3)) +hydro = pint.Context("hydro") +hydro.add_transformation( + "[mass] / [length]**2", + "[length]", + lambda ureg, x: x / (1000 * ureg.kg / ureg.m ** 3), +) +hydro.add_transformation( + "[mass] / [length]**2 / [time]", + "[length] / [time]", + lambda ureg, x: x / (1000 * ureg.kg / ureg.m ** 3), +) +hydro.add_transformation( + "[length] / [time]", + "[mass] / [length]**2 / [time]", + lambda ureg, x: x * (1000 * ureg.kg / ureg.m ** 3), +) units.add_context(hydro) units.enable_contexts(hydro) @@ -64,19 +80,21 @@ # [mass] / [length]**2 / [time] -> [length] / [time] : value / 1000 / kg * m ** 3 # [length] / [time] -> [mass] / [length]**2 / [time] : value * 1000 * kg / m ** 3 # @end -binary_ops = {'>': 'gt', '<': 'lt', '>=': 'ge', '<=': 'le'} +binary_ops = {">": "gt", "<": "lt", ">=": "ge", "<=": "le"} # Maximum day of year in each calendar. -calendars = {'standard': 366, - 'gregorian': 366, - 'proleptic_gregorian': 366, - 'julian': 366, - 'no_leap': 365, - '365_day': 365, - 'all_leap': 366, - '366_day': 366, - 'uniform30day': 360, - '360_day': 360} +calendars = { + "standard": 366, + "gregorian": 366, + "proleptic_gregorian": 366, + "julian": 366, + "no_leap": 365, + "365_day": 365, + "all_leap": 366, + "366_day": 366, + "uniform30day": 360, + "360_day": 360, +} def units2pint(value): @@ -96,12 +114,12 @@ def units2pint(value): def _transform(s): """Convert a CF-unit string to a pint expression.""" - return re.subn(r'\^?(-?\d)', r'**\g<1>', s)[0] + return re.subn(r"\^?(-?\d)", r"**\g<1>", s)[0] if isinstance(value, str): unit = value elif isinstance(value, xr.DataArray): - unit = value.attrs['units'] + unit = value.attrs["units"] elif isinstance(value, units.Quantity): return value.units else: @@ -109,7 +127,10 @@ def _transform(s): try: # Pint compatible return units.parse_expression(unit).units - except (pint.UndefinedUnitError, pint.DimensionalityError): # Convert from CF-units to pint-compatible + except ( + pint.UndefinedUnitError, + pint.DimensionalityError, + ): # Convert from CF-units to pint-compatible return units.parse_expression(_transform(unit)).units @@ -130,12 +151,12 @@ def pint2cfunits(value): s = "{:~}".format(value) # Search and replace patterns - pat = r'(?P/ )?(?P\w+)(?: \*\* (?P\d))?' + pat = r"(?P/ )?(?P\w+)(?: \*\* (?P\d))?" def repl(m): i, u, p = m.groups() - p = p or (1 if i else '') - neg = '-' if i else ('^' if p else '') + p = p or (1 if i else "") + neg = "-" if i else ("^" if p else "") return "{}{}{}".format(u, neg, p) @@ -160,7 +181,7 @@ def pint_multiply(da, q, out_units=None): if out_units: f = f.to(out_units) out = da * f.magnitude - out.attrs['units'] = pint2cfunits(f.units) + out.attrs["units"] = pint2cfunits(f.units) return out @@ -206,21 +227,26 @@ def convert_units_to(source, target, context=None): return source tu_u = pint2cfunits(tu) - with units.context(context or 'none'): + with units.context(context or "none"): out = units.convert(source, fu, tu) - out.attrs['units'] = tu_u + out.attrs["units"] = tu_u return out # TODO remove backwards compatibility of int/float thresholds after v1.0 release if isinstance(source, (float, int)): - if context == 'hydro': + if context == "hydro": fu = units.mm / units.day else: fu = units.degC - warnings.warn("Future versions of XCLIM will require explicit unit specifications.", FutureWarning) + warnings.warn( + "Future versions of XCLIM will require explicit unit specifications.", + FutureWarning, + ) return (source * fu).to(tu).m - raise NotImplementedError("source of type {} is not supported.".format(type(source))) + raise NotImplementedError( + "source of type {} is not supported.".format(type(source)) + ) def _check_units(val, dim): @@ -231,7 +257,7 @@ def _check_units(val, dim): if isinstance(val, (int, float)): return - expected = units.get_dimensionality(dim.replace('dimensionless', '')) + expected = units.get_dimensionality(dim.replace("dimensionless", "")) val_dim = units2pint(val).dimensionality if val_dim == expected: return @@ -243,19 +269,23 @@ def _check_units(val, dim): if pint.util.find_shortest_path(graph, start, end): return - if dim == '[precipitation]': - tu = 'mmday' - elif dim == '[discharge]': - tu = 'cms' - elif dim == '[length]': - tu = 'm' + if dim == "[precipitation]": + tu = "mmday" + elif dim == "[discharge]": + tu = "cms" + elif dim == "[length]": + tu = "m" else: raise NotImplementedError try: - (1 * units2pint(val)).to(tu, 'hydro') + (1 * units2pint(val)).to(tu, "hydro") except (pint.UndefinedUnitError, pint.DimensionalityError): - raise AttributeError("Value's dimension {} does not match expected units {}.".format(val_dim, expected)) + raise AttributeError( + "Value's dimension {} does not match expected units {}.".format( + val_dim, expected + ) + ) def declare_units(out_units, **units_by_name): @@ -276,12 +306,12 @@ def wrapper(*args, **kwargs): out = func(*args, **kwargs) # In the generic case, we use the default units that should have been propagated by the computation. - if '[' in out_units: + if "[" in out_units: _check_units(out, out_units) # Otherwise, we specify explicitly the units. else: - out.attrs['units'] = out_units + out.attrs["units"] = out_units return out return wrapper @@ -318,12 +348,12 @@ def threshold_count(da, op, thresh, freq): else: raise ValueError("Operation `{}` not recognized.".format(op)) - func = getattr(da, '_binary_op')(get_op(op)) + func = getattr(da, "_binary_op")(get_op(op)) c = func(da, thresh) * 1 - return c.resample(time=freq).sum(dim='time') + return c.resample(time=freq).sum(dim="time") -def percentile_doy(arr, window=5, per=.1): +def percentile_doy(arr, window=5, per=0.1): """Percentile value for each day of the year Return the climatological percentile over a moving window around each day of the year. @@ -344,12 +374,12 @@ def percentile_doy(arr, window=5, per=.1): """ # TODO: Support percentile array, store percentile in coordinates. # This is supported by DataArray.quantile, but not by groupby.reduce. - rr = arr.rolling(min_periods=1, center=True, time=window).construct('window') + rr = arr.rolling(min_periods=1, center=True, time=window).construct("window") # Create empty percentile array - g = rr.groupby('time.dayofyear') + g = rr.groupby("time.dayofyear") - p = g.reduce(np.nanpercentile, dim=('time', 'window'), q=per * 100) + p = g.reduce(np.nanpercentile, dim=("time", "window"), q=per * 100) # The percentile for the 366th day has a sample size of 1/4 of the other days. # To have the same sample size, we interpolate the percentile from 1-365 doy range to 1-366 @@ -373,7 +403,7 @@ def infer_doy_max(arr): int The largest day of the year found in calendar. """ - cal = arr.time.encoding.get('calendar', None) + cal = arr.time.encoding.get("calendar", None) if cal in calendars: doy_max = calendars[cal] else: @@ -381,7 +411,9 @@ def infer_doy_max(arr): # then this inference could be wrong ( doy_max = arr.time.dt.dayofyear.max().data if len(arr.time) < 360: - raise ValueError("Cannot infer the calendar from a series less than a year long.") + raise ValueError( + "Cannot infer the calendar from a series less than a year long." + ) if doy_max not in [360, 365, 366]: raise ValueError("The target array's calendar is not recognized") @@ -407,17 +439,17 @@ def _interpolate_doy_calendar(source, doy_max): Interpolated source array over coordinates spanning the target `dayofyear` range. """ - if 'dayofyear' not in source.coords.keys(): + if "dayofyear" not in source.coords.keys(): raise AttributeError("source should have dayofyear coordinates.") # Interpolation of source to target dayofyear range doy_max_source = source.dayofyear.max() # Interpolate to fill na values - tmp = source.interpolate_na(dim='dayofyear') + tmp = source.interpolate_na(dim="dayofyear") # Interpolate to target dayofyear range - tmp.coords['dayofyear'] = np.linspace(start=1, stop=doy_max, num=doy_max_source) + tmp.coords["dayofyear"] = np.linspace(start=1, stop=doy_max, num=doy_max_source) return tmp.interp(dayofyear=range(1, doy_max + 1)) @@ -472,11 +504,11 @@ def get_daily_events(da, da_value, operator): """ events = operator(da, da_value) * 1 events = events.where(~np.isnan(da)) - events = events.rename('events') + events = events.rename("events") return events -def daily_downsampler(da, freq='YS'): +def daily_downsampler(da, freq="YS"): r"""Daily climate data downsampler Parameters @@ -506,22 +538,25 @@ def daily_downsampler(da, freq='YS'): # generate tags from da.time and freq if isinstance(da.time.values[0], np.datetime64): - years = ['{:04d}'.format(y) for y in da.time.dt.year.values] - months = ['{:02d}'.format(m) for m in da.time.dt.month.values] + years = ["{:04d}".format(y) for y in da.time.dt.year.values] + months = ["{:02d}".format(m) for m in da.time.dt.month.values] else: # cannot use year, month, season attributes, not available for all calendars ... - years = ['{:04d}'.format(v.year) for v in da.time.values] - months = ['{:02d}'.format(v.month) for v in da.time.values] - seasons = ['DJF DJF MAM MAM MAM JJA JJA JJA SON SON SON DJF'.split()[int(m) - 1] for m in months] + years = ["{:04d}".format(v.year) for v in da.time.values] + months = ["{:02d}".format(v.month) for v in da.time.values] + seasons = [ + "DJF DJF MAM MAM MAM JJA JJA JJA SON SON SON DJF".split()[int(m) - 1] + for m in months + ] n_t = da.time.size - if freq == 'YS': + if freq == "YS": # year start frequency l_tags = years - elif freq == 'MS': + elif freq == "MS": # month start frequency l_tags = [years[i] + months[i] for i in range(n_t)] - elif freq == 'QS-DEC': + elif freq == "QS-DEC": # DJF, MAM, JJA, SON seasons # construct tags from list of season+year, increasing year for December ys = [] @@ -529,19 +564,19 @@ def daily_downsampler(da, freq='YS'): m = months[i] s = seasons[i] y = years[i] - if m == '12': + if m == "12": y = str(int(y) + 1) ys.append(y + s) l_tags = ys else: - raise RuntimeError('freqency {:s} not implemented'.format(freq)) + raise RuntimeError("freqency {:s} not implemented".format(freq)) # add tags to buffer DataArray buffer = da.copy() - buffer.coords['tags'] = ('time', l_tags) + buffer.coords["tags"] = ("time", l_tags) # return groupby according to tags - return buffer.groupby('tags') + return buffer.groupby("tags") def walk_map(d, func): @@ -570,44 +605,73 @@ def walk_map(d, func): # This class needs to be subclassed by individual indicator classes defining metadata information, compute and # missing functions. It can handle indicators with any number of forcing fields. -class Indicator(): +class Indicator: r"""Climate indicator based on xarray """ # Unique ID for function registry. - identifier = '' + identifier = "" # Output variable name. May use tags {} that will be formatted at runtime. - var_name = '' + var_name = "" _nvar = 1 # CF-Convention metadata to be attributed to the output variable. May use tags {} formatted at runtime. - standard_name = '' # The set of permissible standard names is contained in the standard name table. - long_name = '' # Parsed. - units = '' # Representative units of the physical quantity. - cell_methods = '' # List of blank-separated words of the form "name: method" - description = '' # The description is meant to clarify the qualifiers of the fundamental quantities, such as which + standard_name = ( + "" + ) # The set of permissible standard names is contained in the standard name table. + long_name = "" # Parsed. + units = "" # Representative units of the physical quantity. + cell_methods = "" # List of blank-separated words of the form "name: method" + description = ( + "" + ) # The description is meant to clarify the qualifiers of the fundamental quantities, such as which # surface a quantity is defined on or what the flux sign conventions are. # The `pint` unit context. Use 'hydro' to allow conversion from kg m-2 s-1 to mm/day. - context = 'none' + context = "none" # Additional information that can be used by third party libraries or to describe the file content. - title = '' # A succinct description of what is in the dataset. Default parsed from compute.__doc__ - abstract = '' # Parsed - keywords = '' # Comma separated list of keywords - references = '' # Published or web-based references that describe the data or methods used to produce it. Parsed. - comment = '' # Miscellaneous information about the data or methods used to produce it. - notes = '' # Mathematical formulation. Parsed. + title = ( + "" + ) # A succinct description of what is in the dataset. Default parsed from compute.__doc__ + abstract = "" # Parsed + keywords = "" # Comma separated list of keywords + references = ( + "" + ) # Published or web-based references that describe the data or methods used to produce it. Parsed. + comment = ( + "" + ) # Miscellaneous information about the data or methods used to produce it. + notes = "" # Mathematical formulation. Parsed. # Tag mappings between keyword arguments and long-form text. - months = {'m{}'.format(i): calendar.month_name[i].lower() for i in range(1, 13)} - _attrs_mapping = {'cell_methods': {'YS': 'years', 'MS': 'months'}, # I don't think this is necessary. - 'long_name': {'YS': 'Annual', 'MS': 'Monthly', 'QS-DEC': 'Seasonal', 'DJF': 'winter', - 'MAM': 'spring', 'JJA': 'summer', 'SON': 'fall'}, - 'description': {'YS': 'Annual', 'MS': 'Monthly', 'QS-DEC': 'Seasonal', 'DJF': 'winter', - 'MAM': 'spring', 'JJA': 'summer', 'SON': 'fall'}, - 'var_name': {'DJF': 'winter', 'MAM': 'spring', 'JJA': 'summer', 'SON': 'fall'}} + months = {"m{}".format(i): calendar.month_name[i].lower() for i in range(1, 13)} + _attrs_mapping = { + "cell_methods": { + "YS": "years", + "MS": "months", + }, # I don't think this is necessary. + "long_name": { + "YS": "Annual", + "MS": "Monthly", + "QS-DEC": "Seasonal", + "DJF": "winter", + "MAM": "spring", + "JJA": "summer", + "SON": "fall", + }, + "description": { + "YS": "Annual", + "MS": "Monthly", + "QS-DEC": "Seasonal", + "DJF": "winter", + "MAM": "spring", + "JJA": "summer", + "SON": "fall", + }, + "var_name": {"DJF": "winter", "MAM": "spring", "JJA": "summer", "SON": "fall"}, + } for k, v in _attrs_mapping.items(): v.update(months) @@ -625,12 +689,15 @@ def __init__(self, **kwds): setattr(self, key, val) # Verify that the identifier is a proper slug - if not re.match(r'^[-\w]+$', self.identifier): - warnings.warn("The identifier contains non-alphanumeric characters. It could make life " - "difficult for downstream software reusing this class.", UserWarning) + if not re.match(r"^[-\w]+$", self.identifier): + warnings.warn( + "The identifier contains non-alphanumeric characters. It could make life " + "difficult for downstream software reusing this class.", + UserWarning, + ) # Default value for `var_name` is the `identifier`. - if self.var_name == '': + if self.var_name == "": self.var_name = self.identifier # Extract information from the `compute` function. @@ -649,8 +716,8 @@ def __init__(self, **kwds): # Fill in missing metadata from the doc meta = parse_doc(self.compute.__doc__) - for key in ['abstract', 'title', 'notes', 'references']: - setattr(self, key, getattr(self, key) or meta.get(key, '')) + for key in ["abstract", "title", "notes", "references"]: + setattr(self, key, getattr(self, key) or meta.get(key, "")) def __call__(self, *args, **kwds): # Bind call arguments. We need to use the class signature, not the instance, otherwise it removes the first @@ -668,15 +735,15 @@ def __call__(self, *args, **kwds): attrs = defaultdict(str) for i in range(self._nvar): p = self._parameters[i] - for attr in ['history', 'cell_methods']: + for attr in ["history", "cell_methods"]: attrs[attr] += "{}: ".format(p) if self._nvar > 1 else "" - attrs[attr] += getattr(ba.arguments[p], attr, '') + attrs[attr] += getattr(ba.arguments[p], attr, "") if attrs[attr]: - attrs[attr] += "\n" if attr == 'history' else " " + attrs[attr] += "\n" if attr == "history" else " " # Update attributes out_attrs = self.format(self.cf_attrs, ba.arguments) - vname = self.format({'var_name': self.var_name}, ba.arguments)['var_name'] + vname = self.format({"var_name": self.var_name}, ba.arguments)["var_name"] # Update the signature with the values of the actual call. cp = OrderedDict() @@ -686,13 +753,20 @@ def __call__(self, *args, **kwds): else: cp[k] = v - attrs['history'] += '[{:%Y-%m-%d %H:%M:%S}] {}: {}{} - xclim version: {}.'.format( - dt.datetime.now(), vname, self.identifier, ba.signature.replace(parameters=cp.values()), xclim.__version__) - attrs['cell_methods'] += out_attrs.pop('cell_methods', '') + attrs[ + "history" + ] += "[{:%Y-%m-%d %H:%M:%S}] {}: {}{} - xclim version: {}.".format( + dt.datetime.now(), + vname, + self.identifier, + ba.signature.replace(parameters=cp.values()), + xclim.__version__, + ) + attrs["cell_methods"] += out_attrs.pop("cell_methods", "") attrs.update(out_attrs) # Assume the first arguments are always the DataArray. - das = tuple((ba.arguments.pop(self._parameters[i]) for i in range(self._nvar))) + das = tuple(ba.arguments.pop(self._parameters[i]) for i in range(self._nvar)) # Pre-computation validation checks for da in das: @@ -720,8 +794,15 @@ def __call__(self, *args, **kwds): @property def cf_attrs(self): """CF-Convention attributes of the output value.""" - names = ['standard_name', 'long_name', 'units', 'cell_methods', 'description', 'comment', - 'references'] + names = [ + "standard_name", + "long_name", + "units", + "cell_methods", + "description", + "comment", + "references", + ] return {k: getattr(self, k) for k in names if getattr(self, k)} def json(self, args=None): @@ -732,15 +813,22 @@ def json(self, args=None): This is meant to be used by a third-party library wanting to wrap this class into another interface. """ - names = ['identifier', 'var_name', 'abstract', 'keywords'] + names = ["identifier", "var_name", "abstract", "keywords"] out = {key: getattr(self, key) for key in names} out.update(self.cf_attrs) out = self.format(out, args) - out['notes'] = self.notes + out["notes"] = self.notes - out['parameters'] = str({key: {'default': p.default if p.default != p.empty else None, 'desc': ''} - for (key, p) in self._sig.parameters.items()}) + out["parameters"] = str( + { + key: { + "default": p.default if p.default != p.empty else None, + "desc": "", + } + for (key, p) in self._sig.parameters.items() + } + ) # if six.PY2: # out = walk_map(out, lambda x: x.decode('utf8') if isinstance(x, six.string_types) else x) @@ -771,21 +859,21 @@ def format(self, attrs, args=None): out = {} for key, val in attrs.items(): - mba = {'indexer': 'annual'} + mba = {"indexer": "annual"} # Add formatting {} around values to be able to replace them with _attrs_mapping using format. for k, v in args.items(): if isinstance(v, str) and v in self._attrs_mapping.get(key, {}).keys(): - mba[k] = '{{{}}}'.format(v) + mba[k] = "{{{}}}".format(v) elif isinstance(v, dict): if v: dk, dv = v.copy().popitem() - if dk == 'month': - dv = 'm{}'.format(dv) - mba[k] = '{{{}}}'.format(dv) + if dk == "month": + dv = "m{}".format(dv) + mba[k] = "{{{}}}".format(dv) elif isinstance(v, units.Quantity): - mba[k] = '{:g~P}'.format(v) + mba[k] = "{:g~P}".format(v) elif isinstance(v, (int, float)): - mba[k] = '{:g}'.format(v) + mba[k] = "{:g}".format(v) else: mba[k] = v @@ -798,7 +886,7 @@ def missing(*args, **kwds): """Return whether an output is considered missing or not.""" from functools import reduce - freq = kwds.get('freq') + freq = kwds.get("freq") miss = (checks.missing_any(da, freq) for da in args) return reduce(np.logical_or, miss) @@ -819,26 +907,26 @@ def parse_doc(doc): out = {} - sections = re.split(r'(\w+)\n\s+-{4,50}', doc) # obj.__doc__.split('\n\n') + sections = re.split(r"(\w+)\n\s+-{4,50}", doc) # obj.__doc__.split('\n\n') intro = sections.pop(0) if intro: - content = list(map(str.strip, intro.strip().split('\n\n'))) + content = list(map(str.strip, intro.strip().split("\n\n"))) if len(content) == 1: - out['title'] = content[0] + out["title"] = content[0] elif len(content) == 2: - out['title'], out['abstract'] = content + out["title"], out["abstract"] = content for i in range(0, len(sections), 2): - header, content = sections[i:i + 2] + header, content = sections[i : i + 2] - if header in ['Notes', 'References']: - out[header.lower()] = content.replace('\n ', '\n') - elif header == 'Parameters': + if header in ["Notes", "References"]: + out[header.lower()] = content.replace("\n ", "\n") + elif header == "Parameters": pass - elif header == 'Returns': - match = re.search(r'xarray\.DataArray\s*(.*)', content) + elif header == "Returns": + match = re.search(r"xarray\.DataArray\s*(.*)", content) if match: - out['long_name'] = match.groups()[0] + out["long_name"] = match.groups()[0] return out @@ -854,15 +942,17 @@ def format_kwargs(attrs, params): params : dict A BoundArguments.arguments dictionary storing a function's arguments. """ - attrs_mapping = {'cell_methods': {'YS': 'years', 'MS': 'months'}, - 'long_name': {'YS': 'Annual', 'MS': 'Monthly'}} + attrs_mapping = { + "cell_methods": {"YS": "years", "MS": "months"}, + "long_name": {"YS": "Annual", "MS": "Monthly"}, + } for key, val in attrs.items(): mba = {} # Add formatting {} around values to be able to replace them with _attrs_mapping using format. for k, v in params.items(): if isinstance(v, str) and v in attrs_mapping.get(key, {}).keys(): - mba[k] = '{' + v + '}' + mba[k] = "{" + v + "}" else: mba[k] = v @@ -871,6 +961,7 @@ def format_kwargs(attrs, params): def wrapped_partial(func, *args, **kwargs): from functools import partial, update_wrapper + partial_func = partial(func, *args, **kwargs) update_wrapper(partial_func, func) return partial_func