From cc69858b6687ab323de06858823d5e2ef47e476d Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Tue, 18 Feb 2025 13:22:00 -0500 Subject: [PATCH 01/26] drop Python3.10 conventions --- .github/workflows/main.yml | 11 +++-------- .pre-commit-config.yaml | 10 +++++----- environment.yml | 13 ++++++------- pyproject.toml | 10 ++++------ tox.ini | 39 +++++++++++++++++++------------------- 5 files changed, 37 insertions(+), 46 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5ca3856de..536ec5cd6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -111,7 +111,7 @@ jobs: strategy: matrix: os: [ 'ubuntu-latest' ] - python-version: [ "3.10" ] + python-version: [ "3.11" ] testdata-cache: [ '~/.cache/xclim-testdata' ] steps: - name: Harden Runner @@ -195,11 +195,6 @@ jobs: matrix: include: # Linux builds - - os: 'ubuntu-latest' - testdata-cache: '~/.cache/xclim-testdata' - markers: -m 'not slow' - python-version: "3.10" - tox-env: standard - os: 'ubuntu-latest' testdata-cache: '~/.cache/xclim-testdata' markers: -m 'not slow' @@ -237,7 +232,7 @@ jobs: - os: 'ubuntu-latest' testdata-cache: '~/.cache/xclim-testdata' markers: '' # No markers for notebooks - python-version: "3.10" + python-version: "3.11" tox-env: notebooks - os: 'ubuntu-latest' testdata-cache: '~/.cache/xclim-testdata' @@ -346,7 +341,7 @@ jobs: strategy: matrix: os: [ 'ubuntu-latest' ] - python-version: [ "3.10", "3.12" ] + python-version: [ "3.11", "3.13" ] testdata-cache: [ '~/.cache/xclim-testdata' ] defaults: run: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 82440c49e..f19c100c1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ repos: rev: v3.19.1 hooks: - id: pyupgrade - args: ['--py310-plus'] + args: ['--py311-plus'] exclude: 'src/xclim/core/indicator.py' - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 @@ -37,7 +37,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.4 + rev: v0.9.6 hooks: - id: ruff args: [ '--fix', '--show-fixes' ] @@ -47,7 +47,7 @@ repos: - id: pylint args: [ '--rcfile=.pylintrc.toml', '--errors-only', '--jobs=0', '--disable=import-error' ] - repo: https://github.com/pycqa/flake8 - rev: 7.1.1 + rev: 7.1.2 hooks: - id: flake8 additional_dependencies: [ 'flake8-rst-docstrings '] @@ -61,9 +61,9 @@ repos: hooks: - id: nbqa-pyupgrade args: [ '--py310-plus' ] - additional_dependencies: [ 'pyupgrade==3.19.0' ] + additional_dependencies: [ 'pyupgrade==3.19.1' ] - id: nbqa-black - additional_dependencies: [ 'black==24.10.0' ] + additional_dependencies: [ 'black==25.1.0' ] - repo: https://github.com/kynan/nbstripout rev: 0.8.1 hooks: diff --git a/environment.yml b/environment.yml index 91b01d7e6..1074d241d 100644 --- a/environment.yml +++ b/environment.yml @@ -2,7 +2,7 @@ name: xclim channels: - conda-forge dependencies: - - python >=3.10,<3.14 + - python >=3.11,<3.14 - boltons >=20.1 - bottleneck >=1.3.1 - cf_xarray >=0.9.3 @@ -29,23 +29,22 @@ dependencies: - lmoments3 >=1.0.7 # Required for some Jupyter notebooks - pot >=0.9.4 # Testing and development dependencies - - black =24.10.0 + - black =25.1.0 - blackdoc =0.3.9 - bump-my-version >=0.28.1 - cairosvg >=2.6.0 - - codespell =2.3.0 + - codespell >=2.4.1 - coverage >=7.5.0 - coveralls >=4.0.1 # Note: coveralls is not yet compatible with Python 3.13 - deptry =0.21.2 - distributed >=2.0 - flake8 >=7.1.1 - flake8-rst-docstrings >=0.3.0 - - flit >=3.9.0 + - flit >=3.10.1,<4.0 - furo >=2023.9.10 - h5netcdf >=1.3.0 - ipykernel - ipython >=8.5.0 - - isort =5.13.2 - matplotlib >=3.6.0 - mypy >=1.10.0 - nbconvert >=7.16.4 @@ -57,7 +56,7 @@ dependencies: - notebook - numpydoc >=1.8.0 - pandas-stubs >=2.2 - - pip >=24.2.0 + - pip >=25.0 - pooch >=1.8.0 - pre-commit >=3.7 - pybtex >=0.24.0 @@ -66,7 +65,7 @@ dependencies: - pytest-cov >=5.0.0 - pytest-socket >=0.6.0 - pytest-xdist >=3.2 - - ruff >=0.7.0 + - ruff >=0.9.6 - sphinx >=7.0.0 - sphinx-autobuild >=2024.4.16 - sphinx-autodoc-typehints diff --git a/pyproject.toml b/pyproject.toml index 7fe3705e7..7099b4b3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ maintainers = [ {name = "Pascal Bourgault", email = "bourgault.pascal@ouranos.ca"} ] readme = {file = "README.rst", content-type = "text/x-rst"} -requires-python = ">=3.10.0" +requires-python = ">=3.11.0" keywords = ["xclim", "xarray", "climate", "climatology", "bias correction", "ensemble", "indicators", "analysis"] license = {file = "LICENSE"} classifiers = [ @@ -23,7 +23,6 @@ classifiers = [ "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -69,7 +68,6 @@ dev = [ "flake8-rst-docstrings >=0.3.0", "h5netcdf>=1.3.0", "ipython >=8.5.0", - "isort ==6.0.0", "mypy >=1.10.0", "nbconvert >=7.16.4", "nbqa >=1.8.2", @@ -131,9 +129,9 @@ xclim = "xclim.cli:cli" [tool.black] target-version = [ - "py310", "py311", - "py312" + "py312", + "py313" ] [tool.bumpversion] @@ -225,7 +223,7 @@ exclude = [ ] [tool.mypy] -python_version = 3.10 +python_version = 3.11 show_error_codes = true enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] plugins = ["numpy.typing.mypy_plugin"] diff --git a/tox.ini b/tox.ini index 8914e8437..0a5a41fb1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,47 +1,46 @@ [tox] -min_version = 4.18.1 +min_version = 4.24.1 env_list = lint docs notebooks doctests - py310 py311-extras-numpy py312-extras-sbck py313-extras-lmoments labels = static = lint - test = py310, py311-extras-numpy, py312-extras-sbck, py313-extras-lmoments + test = py311-extras-numpy, py312-extras-sbck, py313-extras-lmoments special = docs, notebooks, doctests requires = - pip >= 24.2.0 - flit >=3.9,<4.0 -opts = -vv + pip >= 25.0 + flit >=3.10.1,<4.0 +opts = + --verbose [gh] python = - 3.10 = py310-coverage-lmoments - 3.11 = py311-coverage-extras-sbck + 3.11 = py311-coverage-extras-sbck-lmoments 3.12 = py312-coverage-extras-numpy # coveralls is not yet supported for Python3.13; Adjust this build when coveralls>4.0.1 is released. - 3.13 = py313-extras-lmoments + 3.13 = py313-extras-sbck [testenv:lint] description = Run code quality compliance tests under {basepython} skip_install = True extras = deps = - codespell ==2.3.0 - deptry==0.16.1 - flake8==7.1.1 - flake8-rst-docstrings==0.3.0 - black[jupyter]==24.10.0 - blackdoc==0.3.9 - nbqa==1.8.2 - numpydoc==1.8.0 - ruff==0.7.0 - vulture==2.11 - yamllint==1.35.1 + black[jupyter] ==25.1.0 + blackdoc ==0.3.9 + codespell >=2.4.1 + deptry >=0.23.0 + flake8 >=7.1.1 + flake8-rst-docstrings ==0.3.0 + nbqa >=1.8.2 + numpydoc >=1.8.0 + ruff >=0.9.6 + vulture >=2.11 + yamllint >=1.35.1 commands_pre = commands = make lint From 3803fba5b139ac14b273c5948b20c69fedaa0524 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Tue, 18 Feb 2025 13:47:32 -0500 Subject: [PATCH 02/26] remove black/isort formatter for code base, enable a few checks in pre-commit-hooks, remove nbqa hook --- .pre-commit-config.yaml | 20 +++++++------------- Makefile | 2 -- README.rst | 10 +++------- environment.yml | 3 +-- pyproject.toml | 8 +++----- tests/test_cffwis.py | 6 +++--- tox.ini | 1 - 7 files changed, 17 insertions(+), 33 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f19c100c1..6a782edc7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,6 +14,12 @@ repos: - id: trailing-whitespace - id: end-of-file-fixer exclude: '.ipynb|.github/publish-mastodon-template.md' + - id: fix-byte-order-marker + - id: name-tests-test + args: [ '--pytest-test-first' ] + - id: no-commit-to-branch + args: [ '--branch', 'main' ] + - id: check-merge-conflict - id: check-json - id: check-toml - id: check-yaml @@ -32,10 +38,6 @@ repos: hooks: - id: yamllint args: [ '--config-file=.yamllint.yaml' ] - - repo: https://github.com/psf/black-pre-commit-mirror - rev: 25.1.0 - hooks: - - id: black - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.9.6 hooks: @@ -56,14 +58,6 @@ repos: rev: 'v2.14' hooks: - id: vulture - - repo: https://github.com/nbQA-dev/nbQA - rev: 1.9.1 - hooks: - - id: nbqa-pyupgrade - args: [ '--py310-plus' ] - additional_dependencies: [ 'pyupgrade==3.19.1' ] - - id: nbqa-black - additional_dependencies: [ 'black==25.1.0' ] - repo: https://github.com/kynan/nbstripout rev: 0.8.1 hooks: @@ -92,7 +86,7 @@ repos: - id: blackdoc additional_dependencies: [ 'black==25.1.0' ] exclude: '(src/xclim/indices/__init__.py|docs/installation.rst)' - - id: blackdoc-autoupdate-black + # - id: blackdoc-autoupdate-black - repo: https://github.com/codespell-project/codespell rev: v2.4.1 hooks: diff --git a/Makefile b/Makefile index 73629adf1..fc616f6ce 100644 --- a/Makefile +++ b/Makefile @@ -53,11 +53,9 @@ clean-test: ## remove test and coverage artifacts rm -fr .pytest_cache lint: ## check style with flake8 and black - python -m black --check src/xclim tests python -m ruff check src/xclim tests python -m flake8 --config=.flake8 src/xclim tests python -m vulture src/xclim tests - python -m nbqa black --check docs python -m blackdoc --check --exclude=src/xclim/indices/__init__.py src/xclim python -m blackdoc --check docs codespell src/xclim tests docs diff --git a/README.rst b/README.rst index 05a2369a9..01b0999f6 100644 --- a/README.rst +++ b/README.rst @@ -9,7 +9,7 @@ xclim: Climate services library |logo| |logo-dark| |logo-light| +----------------------------+-----------------------------------------------------+ | Open Source | |license| |ossf-score| |zenodo| |pyOpenSci| |joss| | +----------------------------+-----------------------------------------------------+ -| Coding Standards | |black| |ruff| |pre-commit| |ossf-bp| |fossa| | +| Coding Standards | |ruff| |pre-commit| |ossf-bp| |fossa| | +----------------------------+-----------------------------------------------------+ | Development Status | |status| |build| |coveralls| | +----------------------------+-----------------------------------------------------+ @@ -68,13 +68,13 @@ Conventions ----------- In order to provide a coherent interface, `xclim` tries to follow different sets of conventions. In particular, input data should follow the `CF conventions`_ whenever possible for variable attributes. Variable names are usually the ones used in `CMIP6`_, when they exist. -However, xclim will *always* assume the temporal coordinate is named "time". If your data uses another name (for example: "T"), you can rename the variable with: +However, `xclim` will *always* assume the temporal coordinate is named "time". If your data uses another name (for example: "T"), you can rename the variable with: .. code-block:: python ds = ds.rename(T="time") -`xclim` code uses the `black`_ formatter, a modified `ruff`_ linting configuration, and (mostly) adheres to the `NumPy docstring`_ style. For more information on coding and development conventions, see the `Contributing Guidelines`_. +`xclim` employs `black`_-like code formatting style, a modified `ruff`_ linting configuration, and (mostly) adheres to the `NumPy docstring`_ style. For more information on coding and development conventions, see the `Contributing Guidelines`_. .. _black: https://black.readthedocs.io/en/stable/ .. _ruff: https://docs.astral.sh/ruff/ @@ -191,10 +191,6 @@ This package was created with Cookiecutter_ and the `audreyfeldroy/cookiecutter- :target: https://app.fossa.com/projects/git%2Bgithub.com%2FOuranosinc%2Fxclim?ref=badge_shield :alt: FOSSA -.. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/psf/black - :alt: Python Black - .. |logo| image:: https://raw.githubusercontent.com/Ouranosinc/xclim/main/docs/logos/xclim-logo-small-light.png :target: https://github.com/Ouranosinc/xclim :alt: Xclim diff --git a/environment.yml b/environment.yml index 1074d241d..a8f1d59c9 100644 --- a/environment.yml +++ b/environment.yml @@ -29,9 +29,8 @@ dependencies: - lmoments3 >=1.0.7 # Required for some Jupyter notebooks - pot >=0.9.4 # Testing and development dependencies - - black =25.1.0 - blackdoc =0.3.9 - - bump-my-version >=0.28.1 + - bump-my-version >=0.32.1 - cairosvg >=2.6.0 - codespell >=2.4.1 - coverage >=7.5.0 diff --git a/pyproject.toml b/pyproject.toml index 7099b4b3e..718645409 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,6 @@ dependencies = [ [project.optional-dependencies] dev = [ # Dev tools and testing - "black[jupyter] ==25.1.0", "blackdoc ==0.3.9", "bump-my-version ==0.32.1", "codespell ==2.4.1", @@ -82,7 +81,7 @@ dev = [ "pytest-cov >=5.0.0", "pytest-socket >=0.6.0", "pytest-xdist[psutil] >=3.2", - "ruff >=0.7.0", + "ruff >=0.9.6", "tokenize-rt >=5.2.0", "tox >=4.21.2", "tox-gh >=1.4.4", @@ -308,9 +307,9 @@ xfail_strict = true src = ["xclim"] line-length = 150 exclude = [ + ".eggs", ".git", - "build", - ".eggs" + "build" ] extend-include = [ "*.ipynb" # Include notebooks @@ -364,7 +363,6 @@ case-sensitive = true detect-same-package = false known-first-party = ["xclim"] no-lines-before = ["future"] -required-imports = ["from __future__ import annotations"] [tool.ruff.lint.mccabe] max-complexity = 20 diff --git a/tests/test_cffwis.py b/tests/test_cffwis.py index fd7cddad7..b677d2d4d 100644 --- a/tests/test_cffwis.py +++ b/tests/test_cffwis.py @@ -309,9 +309,9 @@ def test_fire_weather_ufunc_errors( snd = xr.full_like(tas, 0) lat = xr.full_like(tas.isel(time=0), 45) - DC0 = xr.full_like(tas.isel(time=0), np.nan) # noqa - DMC0 = xr.full_like(tas.isel(time=0), np.nan) # noqa - FFMC0 = xr.full_like(tas.isel(time=0), np.nan) # noqa + DC0 = xr.full_like(tas.isel(time=0), np.nan) + DMC0 = xr.full_like(tas.isel(time=0), np.nan) + FFMC0 = xr.full_like(tas.isel(time=0), np.nan) # Test invalid combination with pytest.raises(TypeError): diff --git a/tox.ini b/tox.ini index 0a5a41fb1..f7f7c2c8c 100644 --- a/tox.ini +++ b/tox.ini @@ -30,7 +30,6 @@ description = Run code quality compliance tests under {basepython} skip_install = True extras = deps = - black[jupyter] ==25.1.0 blackdoc ==0.3.9 codespell >=2.4.1 deptry >=0.23.0 From d88bcd8d2490043e7142b90c17d4865be37e10b4 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:04:59 -0500 Subject: [PATCH 03/26] update dependencies --- environment.yml | 21 ++++++++++----------- pyproject.toml | 34 +++++++++++++++++----------------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/environment.yml b/environment.yml index a8f1d59c9..a980170c0 100644 --- a/environment.yml +++ b/environment.yml @@ -11,15 +11,15 @@ dependencies: - dask >=2024.8.1 - filelock >=3.14.0 - jsonpickle >=3.1.0 - - numba >=0.54.1 - - numpy >=1.23.0 + - numba >=0.57.0 + - numpy >=1.24.0 - packaging >=24.0 - pandas >=2.2.0 - pint >=0.24.4 - pyarrow >=15.0.0 # Strongly encouraged for Pandas v2.2.0+ - pyyaml >=6.0.1 - - scikit-learn >=1.1.0 - - scipy >=1.9.0 + - scikit-learn >=1.2.0 + - scipy >=1.11.0 - statsmodels >=0.14.2 - xarray >=2023.11.0,!=2024.10.0 - yamale >=5.0.0 @@ -35,7 +35,7 @@ dependencies: - codespell >=2.4.1 - coverage >=7.5.0 - coveralls >=4.0.1 # Note: coveralls is not yet compatible with Python 3.13 - - deptry =0.21.2 + - deptry =0.23.0 - distributed >=2.0 - flake8 >=7.1.1 - flake8-rst-docstrings >=0.3.0 @@ -43,11 +43,10 @@ dependencies: - furo >=2023.9.10 - h5netcdf >=1.3.0 - ipykernel - - ipython >=8.5.0 - - matplotlib >=3.6.0 - - mypy >=1.10.0 + - ipython >=8.10.0 + - matplotlib >=3.7.0 + - mypy >=1.14.1 - nbconvert >=7.16.4 - - nbqa >=1.8.2 - nbsphinx >=0.9.5 - nbval >=0.11.0 - nc-time-axis >=1.4.1 @@ -74,8 +73,8 @@ dependencies: - sphinxcontrib-bibtex - sphinxcontrib-svg2pdfconverter - tokenize-rt >=5.2.0 - - tox >=4.21.2 - - tox-gh >=1.4.4 + - tox >=4.24.1 + - tox-gh >=1.5.0 - vulture =2.14 - xdoctest >=1.1.5 - yamllint >=1.35.1 diff --git a/pyproject.toml b/pyproject.toml index 718645409..a01ea89e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,15 +41,15 @@ dependencies = [ "dask[array] >=2024.8.1", "filelock >=3.14.0", "jsonpickle >=3.1.0", - "numba >=0.54.1", - "numpy >=1.23.0", + "numba >=0.57.0", + "numpy >=1.24.0", "packaging >=24.0", - "pandas >=2.2", + "pandas >=2.2.0", "pint >=0.24.4", - "pyarrow >=10.0.1", # Strongly encouraged for pandas v2.2.0+ + "pyarrow >=15.0", # Strongly encouraged for pandas v2.2.0+ "pyyaml >=6.0.1", - "scikit-learn >=1.1.0", - "scipy >=1.9.0", + "scikit-learn >=1.2.0", + "scipy >=1.11.0", "statsmodels >=0.14.2", "xarray >=2023.11.0,!=2024.10.0", "yamale >=5.0.0" @@ -59,21 +59,21 @@ dependencies = [ dev = [ # Dev tools and testing "blackdoc ==0.3.9", - "bump-my-version ==0.32.1", - "codespell ==2.4.1", + "bump-my-version >=0.32.1", + "codespell >=2.4.1", "coverage[toml] >=7.5.0", "deptry ==0.23.0", "flake8 >=7.1.1", - "flake8-rst-docstrings >=0.3.0", + "flake8-rst-docstrings ==0.3.0", "h5netcdf>=1.3.0", - "ipython >=8.5.0", - "mypy >=1.10.0", + "ipython >=8.10.0", + "mypy >=1.14.1", "nbconvert >=7.16.4", "nbqa >=1.8.2", "nbval >=0.11.0", "numpydoc >=1.8.0", "pandas-stubs >=2.2", - "pip >=24.2.0", + "pip >=25.0", "pooch >=1.8.0", "pre-commit >=3.7", "pylint >=3.3.1", @@ -83,11 +83,11 @@ dev = [ "pytest-xdist[psutil] >=3.2", "ruff >=0.9.6", "tokenize-rt >=5.2.0", - "tox >=4.21.2", - "tox-gh >=1.4.4", - "vulture ==2.14", + "tox >=4.24.1", + "tox-gh >=1.5.0", + "vulture >=2.14", "xdoctest >=1.1.5", - "yamllint ==1.35.1" + "yamllint >=1.35.1" ] docs = [ # Documentation and examples @@ -95,7 +95,7 @@ docs = [ "distributed >=2.0", "furo >=2023.9.10", "ipykernel", - "matplotlib >=3.6.0", + "matplotlib >=3.7.0", "nbsphinx >=0.9.5", "nc-time-axis >=1.4.1", "pooch >=1.8.0", From e6319f310d6834bdf2d8ceeb2452e18a31ea09f4 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:01:23 -0500 Subject: [PATCH 04/26] enable ruff formatting --- .pre-commit-config.yaml | 9 ++------- pyproject.toml | 10 +++++++++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6a782edc7..75e38f585 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,6 +43,8 @@ repos: hooks: - id: ruff args: [ '--fix', '--show-fixes' ] + - id: ruff-format + exclude: '(src/xclim/indices/__init__.py|docs/installation.rst)' - repo: https://github.com/pylint-dev/pylint rev: v3.3.4 hooks: @@ -80,13 +82,6 @@ repos: hooks: - id: mdformat exclude: '.github/\w+.md|.github/publish-mastodon-template.md|docs/paper/paper.md' - - repo: https://github.com/keewis/blackdoc - rev: v0.3.9 - hooks: - - id: blackdoc - additional_dependencies: [ 'black==25.1.0' ] - exclude: '(src/xclim/indices/__init__.py|docs/installation.rst)' - # - id: blackdoc-autoupdate-black - repo: https://github.com/codespell-project/codespell rev: v2.4.1 hooks: diff --git a/pyproject.toml b/pyproject.toml index a01ea89e8..b94c567b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -316,7 +316,12 @@ extend-include = [ ] [tool.ruff.format] -line-ending = "auto" +exclude = [ + "docs/notebooks/xclim_training/*.ipynb" +] +line-ending = "lf" +docstring-code-format = true +docstring-code-line-length = "dynamic" [tool.ruff.lint] exclude = [ @@ -358,6 +363,9 @@ pandas = "pd" scipy = "sp" xarray = "xr" +[tool.ruff.lint.flake8-quotes] +docstring-quotes = "double" + [tool.ruff.lint.isort] case-sensitive = true detect-same-package = false From 6e25ac437f0e4c5882da0500e4d98945ada86ec0 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:04:20 -0500 Subject: [PATCH 05/26] lower pin on deptry --- environment.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/environment.yml b/environment.yml index a980170c0..b32a4f3a2 100644 --- a/environment.yml +++ b/environment.yml @@ -35,7 +35,7 @@ dependencies: - codespell >=2.4.1 - coverage >=7.5.0 - coveralls >=4.0.1 # Note: coveralls is not yet compatible with Python 3.13 - - deptry =0.23.0 + - deptry >=0.22.0 # Version is lagging on conda-forge - distributed >=2.0 - flake8 >=7.1.1 - flake8-rst-docstrings >=0.3.0 diff --git a/pyproject.toml b/pyproject.toml index a01ea89e8..be3ec9a8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ dev = [ "bump-my-version >=0.32.1", "codespell >=2.4.1", "coverage[toml] >=7.5.0", - "deptry ==0.23.0", + "deptry >=0.23.0", "flake8 >=7.1.1", "flake8-rst-docstrings ==0.3.0", "h5netcdf>=1.3.0", From 346c61728dd509cb67919981ee555ef245aeb3f1 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:10:20 -0500 Subject: [PATCH 06/26] remove black config --- pyproject.toml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index be3ec9a8a..1f2b4a502 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -126,13 +126,6 @@ xclim = "xclim.cli:cli" [tool] -[tool.black] -target-version = [ - "py311", - "py312", - "py313" -] - [tool.bumpversion] current_version = "0.55.1-dev.0" commit = true From 3613282a7c42c12b6b9c0777a1d02cb176ecc60e Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Tue, 18 Feb 2025 16:49:55 -0500 Subject: [PATCH 07/26] format for ruff --- docs/conf.py | 4 +- docs/notebooks/analogs.ipynb | 8 +- docs/notebooks/benchmarks/sdba_quantile.ipynb | 8 +- docs/notebooks/customize.ipynb | 27 +- docs/notebooks/ensembles-advanced.ipynb | 46 +- docs/notebooks/ensembles.ipynb | 84 +--- docs/notebooks/example.ipynb | 25 +- docs/notebooks/extendxclim.ipynb | 12 +- docs/notebooks/frequency_analysis.ipynb | 4 +- docs/notebooks/partitioning.ipynb | 10 +- docs/notebooks/sdba-advanced.ipynb | 114 ++--- docs/notebooks/sdba.ipynb | 68 +-- docs/notebooks/units.ipynb | 21 +- docs/notebooks/usage.ipynb | 12 +- pyproject.toml | 13 +- src/xclim/analog.py | 27 +- src/xclim/cli.py | 87 +--- src/xclim/core/bootstrapping.py | 23 +- src/xclim/core/calendar.py | 176 ++----- src/xclim/core/cfchecks.py | 8 +- src/xclim/core/datachecks.py | 28 +- src/xclim/core/dataflags.py | 59 +-- src/xclim/core/formatting.py | 39 +- src/xclim/core/indicator.py | 190 +++----- src/xclim/core/locales.py | 17 +- src/xclim/core/missing.py | 76 +-- src/xclim/core/options.py | 7 +- src/xclim/core/units.py | 111 ++--- src/xclim/core/utils.py | 66 +-- src/xclim/ensembles/_base.py | 48 +- src/xclim/ensembles/_filters.py | 4 +- src/xclim/ensembles/_partitioning.py | 43 +- src/xclim/ensembles/_reduce.py | 96 ++-- src/xclim/ensembles/_robustness.py | 87 ++-- src/xclim/indicators/atmos/_conversion.py | 4 +- src/xclim/indicators/atmos/_temperature.py | 103 ++-- src/xclim/indices/_agro.py | 108 ++--- src/xclim/indices/_anuclim.py | 52 +- src/xclim/indices/_conversion.py | 146 ++---- src/xclim/indices/_hydrology.py | 24 +- src/xclim/indices/_multivariate.py | 46 +- src/xclim/indices/_simple.py | 38 +- src/xclim/indices/_synoptic.py | 30 +- src/xclim/indices/_threshold.py | 118 ++--- src/xclim/indices/fire/_cffwis.py | 167 +++---- src/xclim/indices/fire/_ffdi.py | 12 +- src/xclim/indices/generic.py | 135 ++---- src/xclim/indices/helpers.py | 102 ++-- src/xclim/indices/run_length.py | 144 +++--- src/xclim/indices/stats.py | 121 ++--- src/xclim/testing/conftest.py | 12 +- src/xclim/testing/diagnostics.py | 21 +- src/xclim/testing/helpers.py | 24 +- src/xclim/testing/utils.py | 74 +-- tests/conftest.py | 40 +- tests/test_analog.py | 37 +- tests/test_atmos.py | 108 ++--- tests/test_bootstrapping.py | 38 +- tests/test_calendar.py | 50 +- tests/test_cffwis.py | 39 +- tests/test_checks.py | 8 +- tests/test_cli.py | 16 +- tests/test_ensembles.py | 148 ++---- tests/test_ffdi.py | 20 +- tests/test_flags.py | 37 +- tests/test_formatting.py | 5 +- tests/test_generic.py | 191 ++------ tests/test_generic_indicators.py | 15 +- tests/test_helpers.py | 50 +- tests/test_indicators.py | 94 +--- tests/test_indices.py | 456 ++++-------------- tests/test_locales.py | 17 +- tests/test_modules.py | 58 +-- tests/test_partitioning.py | 43 +- tests/test_precip.py | 132 ++--- tests/test_run_length.py | 88 +--- tests/test_sdba/conftest.py | 4 +- tests/test_sdba/test_adjustment.py | 137 ++---- tests/test_sdba/test_base.py | 15 +- tests/test_sdba/test_detrending.py | 10 +- tests/test_sdba/test_measures.py | 38 +- tests/test_sdba/test_nbutils.py | 4 +- tests/test_sdba/test_processing.py | 44 +- tests/test_sdba/test_properties.py | 204 ++------ tests/test_sdba/test_sdba_utils.py | 28 +- tests/test_snow.py | 23 +- tests/test_stats.py | 36 +- tests/test_temperature.py | 204 ++------ tests/test_testing_utils.py | 1 - tests/test_units.py | 20 +- tests/test_utils.py | 8 +- tests/test_wind.py | 4 +- 92 files changed, 1609 insertions(+), 3990 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 0a1ed342b..6096ef093 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -240,9 +240,7 @@ class XCStyle(AlphaStyle): # General information about the project. project = "xclim" -copyright = ( - f"2018-{datetime.datetime.now().year}, Ouranos Inc., Travis Logan, and contributors" -) +copyright = f"2018-{datetime.datetime.now().year}, Ouranos Inc., Travis Logan, and contributors" author = "xclim Project Development Team" # The version info for the project you're documenting, acts as replacement diff --git a/docs/notebooks/analogs.ipynb b/docs/notebooks/analogs.ipynb index 0afc21a40..325292ff6 100644 --- a/docs/notebooks/analogs.ipynb +++ b/docs/notebooks/analogs.ipynb @@ -133,9 +133,7 @@ "metadata": {}, "outputs": [], "source": [ - "results = analog.spatial_analogs(\n", - " sim[[\"tg_mean\"]], obs[[\"tg_mean\"]], method=\"seuclidean\"\n", - ")\n", + "results = analog.spatial_analogs(sim[[\"tg_mean\"]], obs[[\"tg_mean\"]], method=\"seuclidean\")\n", "\n", "results.plot()\n", "plt.plot(sim.lon, sim.lat, \"ro\", label=\"Target\")\n", @@ -169,9 +167,7 @@ "metadata": {}, "outputs": [], "source": [ - "results = analog.spatial_analogs(\n", - " sim[[\"tg_mean\"]], obs[[\"tg_mean\"]], method=\"zech_aslan\"\n", - ")\n", + "results = analog.spatial_analogs(sim[[\"tg_mean\"]], obs[[\"tg_mean\"]], method=\"zech_aslan\")\n", "\n", "results.plot(center=False)\n", "plt.plot(sim.lon, sim.lat, \"ro\", label=\"Target\")\n", diff --git a/docs/notebooks/benchmarks/sdba_quantile.ipynb b/docs/notebooks/benchmarks/sdba_quantile.ipynb index a244fd2b1..43b60f881 100644 --- a/docs/notebooks/benchmarks/sdba_quantile.ipynb +++ b/docs/notebooks/benchmarks/sdba_quantile.ipynb @@ -100,9 +100,7 @@ "for use_fnq in [True, False]:\n", " sdba.nbutils.USE_FASTNANQUANTILE = use_fnq\n", " # heat-up the jit\n", - " sdba.nbutils.quantile(\n", - " xr.DataArray(np.array([0, 1.5])), dim=\"dim_0\", q=np.array([0.5])\n", - " )\n", + " sdba.nbutils.quantile(xr.DataArray(np.array([0, 1.5])), dim=\"dim_0\", q=np.array([0.5]))\n", " for size in np.arange(250, 2000 + 250, 250):\n", " da = tx.isel(time=slice(0, size))\n", " t0 = time.time()\n", @@ -110,9 +108,7 @@ " sdba.nbutils.quantile(da, **kws).compute()\n", " timed[use_fnq].append([size, time.time() - t0])\n", "\n", - "for k, lab in zip(\n", - " [True, False], [\"xclim.core.utils.nan_quantile\", \"fastnanquantile\"], strict=False\n", - "):\n", + "for k, lab in zip([True, False], [\"xclim.core.utils.nan_quantile\", \"fastnanquantile\"], strict=False):\n", " arr = np.array(timed[k])\n", " plt.plot(arr[:, 0], arr[:, 1] / num_tests, label=lab)\n", "plt.legend()\n", diff --git a/docs/notebooks/customize.ipynb b/docs/notebooks/customize.ipynb index 6d1abf138..e715ebd50 100644 --- a/docs/notebooks/customize.ipynb +++ b/docs/notebooks/customize.ipynb @@ -35,11 +35,7 @@ "metadata": {}, "outputs": [], "source": [ - "tasmax = (\n", - " xr.tutorial.load_dataset(\"air_temperature\")\n", - " .air.resample(time=\"D\")\n", - " .max(keep_attrs=True)\n", - ")\n", + "tasmax = xr.tutorial.load_dataset(\"air_temperature\").air.resample(time=\"D\").max(keep_attrs=True)\n", "tasmax = tasmax.where(tasmax.time.dt.day % 10 != 0)" ] }, @@ -137,9 +133,7 @@ "outputs": [], "source": [ "with xclim.set_options(check_missing=\"wmo\"):\n", - " tx_mean = xclim.atmos.tx_mean(\n", - " tasmax=tasmax, freq=\"MS\"\n", - " ) # compute monthly max tasmax\n", + " tx_mean = xclim.atmos.tx_mean(tasmax=tasmax, freq=\"MS\") # compute monthly max tasmax\n", "tx_mean.sel(time=\"2013\", lat=75, lon=200)" ] }, @@ -165,7 +159,7 @@ "\n", "To add additional arguments, one should override the `__init__` (receiving those arguments) and the `validate` static method, which validates them. The options are then stored in the `options` property of the instance. See example below and the docstrings in the module.\n", "\n", - "When registering the class with the `xclim.core.checks.register_missing_method` decorator, the keyword arguments will be registered as options for the missing method. " + "When registering the class with the `xclim.core.checks.register_missing_method` decorator, the keyword arguments will be registered as options for the missing method." ] }, { @@ -186,12 +180,9 @@ " super().__init__(max_n=max_n)\n", "\n", " def is_missing(self, valid, count, freq):\n", - " \"\"\"Return a boolean mask where True values are for elements that are considered missing and masked on the output.\"\"\"\n", + " \"\"\"Return a boolean mask for elements that are considered missing and masked on the output.\"\"\"\n", " null = ~valid\n", - " return (\n", - " null.resample(time=freq).map(longest_run, dim=\"time\")\n", - " >= self.options[\"max_n\"]\n", - " )\n", + " return null.resample(time=freq).map(longest_run, dim=\"time\") >= self.options[\"max_n\"]\n", "\n", " @staticmethod\n", " def validate(max_n):\n", @@ -212,12 +203,8 @@ "metadata": {}, "outputs": [], "source": [ - "with xclim.set_options(\n", - " check_missing=\"consecutive\", missing_options={\"consecutive\": {\"max_n\": 2}}\n", - "):\n", - " tx_mean = xclim.atmos.tx_mean(\n", - " tasmax=tasmax, freq=\"MS\"\n", - " ) # compute monthly max tasmax\n", + "with xclim.set_options(check_missing=\"consecutive\", missing_options={\"consecutive\": {\"max_n\": 2}}):\n", + " tx_mean = xclim.atmos.tx_mean(tasmax=tasmax, freq=\"MS\") # compute monthly max tasmax\n", "tx_mean.sel(time=\"2013\", lat=75, lon=200)" ] }, diff --git a/docs/notebooks/ensembles-advanced.ipynb b/docs/notebooks/ensembles-advanced.ipynb index 394bb25d0..35ef8d11c 100644 --- a/docs/notebooks/ensembles-advanced.ipynb +++ b/docs/notebooks/ensembles-advanced.ipynb @@ -3,9 +3,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "nbsphinx": "hidden" - }, + "metadata": {}, "outputs": [], "source": [ "# This cell is not visible when the documentation is built.\n", @@ -19,15 +17,15 @@ "np.random.normal(loc=3.5, scale=1.5, size=50)\n", "# crit['delta_annual_tavg']\n", "np.random.seed(0)\n", - "test = xr.DataArray(\n", - " np.random.normal(loc=3, scale=1.5, size=50), dims=[\"realization\"]\n", - ").assign_coords(horizon=\"2041-2070\")\n", + "test = xr.DataArray(np.random.normal(loc=3, scale=1.5, size=50), dims=[\"realization\"]).assign_coords(\n", + " horizon=\"2041-2070\"\n", + ")\n", "test = xr.concat(\n", " (\n", " test,\n", - " xr.DataArray(\n", - " np.random.normal(loc=5.34, scale=2, size=50), dims=[\"realization\"]\n", - " ).assign_coords(horizon=\"2071-2100\"),\n", + " xr.DataArray(np.random.normal(loc=5.34, scale=2, size=50), dims=[\"realization\"]).assign_coords(\n", + " horizon=\"2071-2100\"\n", + " ),\n", " ),\n", " dim=\"horizon\",\n", ")\n", @@ -35,28 +33,24 @@ "ds_crit = xr.Dataset()\n", "\n", "ds_crit[\"delta_annual_tavg\"] = test\n", - "test = xr.DataArray(\n", - " np.random.normal(loc=5, scale=5, size=50), dims=[\"realization\"]\n", - ").assign_coords(horizon=\"2041-2070\")\n", + "test = xr.DataArray(np.random.normal(loc=5, scale=5, size=50), dims=[\"realization\"]).assign_coords(horizon=\"2041-2070\")\n", "test = xr.concat(\n", " (\n", " test,\n", - " xr.DataArray(\n", - " np.random.normal(loc=10, scale=8, size=50), dims=[\"realization\"]\n", - " ).assign_coords(horizon=\"2071-2100\"),\n", + " xr.DataArray(np.random.normal(loc=10, scale=8, size=50), dims=[\"realization\"]).assign_coords(\n", + " horizon=\"2071-2100\"\n", + " ),\n", " ),\n", " dim=\"horizon\",\n", ")\n", "ds_crit[\"delta_annual_prtot\"] = test\n", - "test = xr.DataArray(\n", - " np.random.normal(loc=0, scale=3, size=50), dims=[\"realization\"]\n", - ").assign_coords(horizon=\"2041-2070\")\n", + "test = xr.DataArray(np.random.normal(loc=0, scale=3, size=50), dims=[\"realization\"]).assign_coords(horizon=\"2041-2070\")\n", "test = xr.concat(\n", " (\n", " test,\n", - " xr.DataArray(\n", - " np.random.normal(loc=2, scale=4, size=50), dims=[\"realization\"]\n", - " ).assign_coords(horizon=\"2071-2100\"),\n", + " xr.DataArray(np.random.normal(loc=2, scale=4, size=50), dims=[\"realization\"]).assign_coords(\n", + " horizon=\"2071-2100\"\n", + " ),\n", " ),\n", " dim=\"horizon\",\n", ")\n", @@ -242,7 +236,7 @@ "\n", "`xclim` also makes available a similar ensemble reduction algorithm, `ensembles.kkz_reduce_ensemble`. See: https://doi.org/10.1175/JCLI-D-14-00636.1\n", "\n", - "The advantage of this algorithm is largely that fewer realizations are needed in order to reach the same level of representative members than the K-means clustering algorithm, as the KKZ methods tends towards identifying members that fall towards the extremes of criteria values. \n", + "The advantage of this algorithm is largely that fewer realizations are needed in order to reach the same level of representative members than the K-means clustering algorithm, as the KKZ methods tends towards identifying members that fall towards the extremes of criteria values.\n", "\n", "This technique also produces nested selection results, where an additional increase in desired selection size does not alter the previous choices, which is not the case for the K-means algorithm." ] @@ -306,11 +300,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "# UNNESTED results using k-means\n", @@ -331,7 +321,7 @@ "\n", "The **KKZ** algorithm iteratively maximizes distance from previous selected candidates - potentially resulting in 'off-center' results versus the original ensemble\n", "\n", - "The **K-means** algorithm will redivide the data space with each iteration, producing results that are consistently centered on the original ensemble but lacking coverage in the extremes " + "The **K-means** algorithm will redivide the data space with each iteration, producing results that are consistently centered on the original ensemble but lacking coverage in the extremes" ] }, { diff --git a/docs/notebooks/ensembles.ipynb b/docs/notebooks/ensembles.ipynb index 664a36f6f..5600b5c97 100644 --- a/docs/notebooks/ensembles.ipynb +++ b/docs/notebooks/ensembles.ipynb @@ -3,14 +3,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "editable": true, - "nbsphinx": "hidden", - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, + "metadata": {}, "outputs": [], "source": [ "# This cell is not visible when the documentation is built.\n", @@ -55,9 +48,7 @@ "# write 10 members adding cubic-smoothed gaussian noise of wave number 43 and amplitude 20\n", "# resulting temp will oscillate between -18 and 38\n", "for i in range(10):\n", - " tasi = tas + 20 * interp1d(\n", - " np.arange(43), np.random.random((43,)), kind=\"quadratic\"\n", - " )(np.linspace(0, 42, tas.size))\n", + " tasi = tas + 20 * interp1d(np.arange(43), np.random.random((43,)), kind=\"quadratic\")(np.linspace(0, 42, tas.size))\n", " tasi.name = \"tas\"\n", " tasi.attrs.update(tas.attrs)\n", " tasi.attrs[\"title\"] = f\"tas of member {i:02d}\"\n", @@ -67,15 +58,15 @@ "np.random.normal(loc=3.5, scale=1.5, size=50)\n", "# crit['delta_annual_tavg']\n", "np.random.seed(0)\n", - "test = xr.DataArray(\n", - " np.random.normal(loc=3, scale=1.5, size=100), dims=[\"realization\"]\n", - ").assign_coords(horizon=\"2041-2070\")\n", + "test = xr.DataArray(np.random.normal(loc=3, scale=1.5, size=100), dims=[\"realization\"]).assign_coords(\n", + " horizon=\"2041-2070\"\n", + ")\n", "test = xr.concat(\n", " (\n", " test,\n", - " xr.DataArray(\n", - " np.random.normal(loc=5.34, scale=2, size=100), dims=[\"realization\"]\n", - " ).assign_coords(horizon=\"2071-2100\"),\n", + " xr.DataArray(np.random.normal(loc=5.34, scale=2, size=100), dims=[\"realization\"]).assign_coords(\n", + " horizon=\"2071-2100\"\n", + " ),\n", " ),\n", " dim=\"horizon\",\n", ")\n", @@ -83,28 +74,24 @@ "ds_crit = xr.Dataset()\n", "\n", "ds_crit[\"delta_annual_tavg\"] = test\n", - "test = xr.DataArray(\n", - " np.random.normal(loc=5, scale=5, size=100), dims=[\"realization\"]\n", - ").assign_coords(horizon=\"2041-2070\")\n", + "test = xr.DataArray(np.random.normal(loc=5, scale=5, size=100), dims=[\"realization\"]).assign_coords(horizon=\"2041-2070\")\n", "test = xr.concat(\n", " (\n", " test,\n", - " xr.DataArray(\n", - " np.random.normal(loc=10, scale=8, size=100), dims=[\"realization\"]\n", - " ).assign_coords(horizon=\"2071-2100\"),\n", + " xr.DataArray(np.random.normal(loc=10, scale=8, size=100), dims=[\"realization\"]).assign_coords(\n", + " horizon=\"2071-2100\"\n", + " ),\n", " ),\n", " dim=\"horizon\",\n", ")\n", "ds_crit[\"delta_annual_prtot\"] = test\n", - "test = xr.DataArray(\n", - " np.random.normal(loc=0, scale=3, size=100), dims=[\"realization\"]\n", - ").assign_coords(horizon=\"2041-2070\")\n", + "test = xr.DataArray(np.random.normal(loc=0, scale=3, size=100), dims=[\"realization\"]).assign_coords(horizon=\"2041-2070\")\n", "test = xr.concat(\n", " (\n", " test,\n", - " xr.DataArray(\n", - " np.random.normal(loc=2, scale=4, size=100), dims=[\"realization\"]\n", - " ).assign_coords(horizon=\"2071-2100\"),\n", + " xr.DataArray(np.random.normal(loc=2, scale=4, size=100), dims=[\"realization\"]).assign_coords(\n", + " horizon=\"2071-2100\"\n", + " ),\n", " ),\n", " dim=\"horizon\",\n", ")\n", @@ -146,13 +133,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, + "metadata": {}, "outputs": [], "source": [ "import matplotlib as mpl\n", @@ -238,13 +219,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, + "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", @@ -265,9 +240,7 @@ "ax._get_lines.get_next_color() # Hack to get different line\n", "ax._get_lines.get_next_color()\n", "ax.plot(ens_stats.time.values, ens_stats.tas_mean, linewidth=2, label=\"Mean\")\n", - "ax.plot(\n", - " ens_perc.time.values, ens_perc.tas.sel(percentiles=50), linewidth=2, label=\"Median\"\n", - ")\n", + "ax.plot(ens_perc.time.values, ens_perc.tas.sel(percentiles=50), linewidth=2, label=\"Median\")\n", "ax.legend()\n", "plt.show()" ] @@ -278,12 +251,12 @@ "source": [ "### Change significance and model agreement\n", "\n", - "When communicating climate change through plots of projected change, it is often useful to add information on the statistical significance of the values. A common way to represent this information without overloading the figures is through hatching patterns superimposed on the primary data. Two aspects are usually shown: \n", + "When communicating climate change through plots of projected change, it is often useful to add information on the statistical significance of the values. A common way to represent this information without overloading the figures is through hatching patterns superimposed on the primary data. Two aspects are usually shown:\n", "\n", "- change significance: whether most of the ensemble members project a statistically significant climate change signal, in comparison to their internal variability.\n", "- model agreement: whether the different ensemble members agree on the sign of the change.\n", "\n", - "We can then divide the plotted points into categories each with its own hatching pattern, usually leaving the robust data (models agree and enough show a significant change) without hatching. \n", + "We can then divide the plotted points into categories each with its own hatching pattern, usually leaving the robust data (models agree and enough show a significant change) without hatching.\n", "\n", "Xclim provides some tools to help in generating these hatching masks. First is [xc.ensembles.robustness_fractions](../apidoc/xclim.ensembles.rst#xclim.ensembles._robustness.robustness_fractions) that can characterize the change significance and sign agreement across ensemble members. To demonstrate its usage, we'll first generate some fake annual mean temperature data. Here, `ref` is the data on the reference period and `fut` is a future projection. There are five (5) different members in the ensemble. We tweaked the generation so that all models agree on significant change in the \"South\" while agreement and significance of change decreases as we go North and East." ] @@ -325,16 +298,11 @@ ")\n", "# Add change.\n", "fut = fut + xr.concat(\n", - " [\n", - " xr.DataArray(np.linspace(15, north_delta, num=10), dims=(\"lat\",))\n", - " for north_delta in [15, 10, 0, -7, -10]\n", - " ],\n", + " [xr.DataArray(np.linspace(15, north_delta, num=10), dims=(\"lat\",)) for north_delta in [15, 10, 0, -7, -10]],\n", " \"realization\",\n", ")\n", "\n", - "deltas = (fut.mean(\"time\") - ref.mean(\"time\")).assign_attrs(\n", - " long_name=\"Temperature change\"\n", - ")\n", + "deltas = (fut.mean(\"time\") - ref.mean(\"time\")).assign_attrs(long_name=\"Temperature change\")\n", "mean_delta = deltas.mean(\"realization\")\n", "deltas.plot(col=\"realization\")" ] @@ -403,7 +371,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The output is a categorical map following the \"flag variables\" CF conventions. Parameters needed for plotting are found in the attributes. " + "The output is a categorical map following the \"flag variables\" CF conventions. Parameters needed for plotting are found in the attributes." ] }, { @@ -445,9 +413,7 @@ "ax.legend(\n", " handles=[\n", " Rectangle((0, 0), 2, 2, fill=False, hatch=h, label=lbl)\n", - " for h, lbl in zip(\n", - " [\"\\\\\\\\\\\\\", \"xxx\"], robustness.flag_descriptions[1:], strict=False\n", - " )\n", + " for h, lbl in zip([\"\\\\\\\\\\\\\", \"xxx\"], robustness.flag_descriptions[1:], strict=False)\n", " ],\n", " bbox_to_anchor=(0.0, 1.1),\n", " loc=\"upper left\",\n", diff --git a/docs/notebooks/example.ipynb b/docs/notebooks/example.ipynb index 21c913684..a94f6bca0 100644 --- a/docs/notebooks/example.ipynb +++ b/docs/notebooks/example.ipynb @@ -325,7 +325,8 @@ }, "outputs": [], "source": [ - "# We have created an xarray data-array - We can insert this into an output xr.Dataset object with a copy of the original dataset global attrs\n", + "# We have created an xarray data-array.\n", + "# We can insert this into an output xr.Dataset object with a copy of the original dataset global attrs\n", "ds_out = xr.Dataset(attrs=ds2.attrs)\n", "\n", "# Add our climate index as a data variable to the dataset\n", @@ -375,11 +376,7 @@ }, "outputs": [], "source": [ - "ds_reduced = (\n", - " ds.sel(lat=slice(44.5, 43))\n", - " .sel(lon=slice(-117.5, -116))\n", - " .sel(time=slice(\"2010-05-01\", \"2011-08-31\"))\n", - ")\n", + "ds_reduced = ds.sel(lat=slice(44.5, 43)).sel(lon=slice(-117.5, -116)).sel(time=slice(\"2010-05-01\", \"2011-08-31\"))\n", "tn, tx = ds_reduced.tasmin, ds_reduced.tasmax\n", "freq = \"MS\"\n", "\n", @@ -387,13 +384,9 @@ "thresh_tn = \"20.0 degC\"\n", "\n", "# Computing index by resampling **before** run length algorithm (default value)\n", - "hw_before = xclim.indices.heat_wave_max_length(\n", - " tn, tx, freq=freq, thresh_tasmin=thresh_tn, resample_before_rl=True\n", - ")\n", + "hw_before = xclim.indices.heat_wave_max_length(tn, tx, freq=freq, thresh_tasmin=thresh_tn, resample_before_rl=True)\n", "# Computing index by resampling **after** run length algorithm\n", - "hw_after = xclim.indices.heat_wave_max_length(\n", - " tn, tx, freq=freq, thresh_tasmin=thresh_tn, resample_before_rl=False\n", - ")\n", + "hw_after = xclim.indices.heat_wave_max_length(tn, tx, freq=freq, thresh_tasmin=thresh_tn, resample_before_rl=False)\n", "\n", "hw_before.sel(time=\"2010-07-01\").plot(vmin=0, vmax=7)\n", "plt.title(\"Resample, then run length\")\n", @@ -655,13 +648,9 @@ "source": [ "# The tasmin threshold is 7°C for the northern half of the domain and 11°C for the southern half.\n", "# (notice that the lat coordinate is in decreasing order : from north to south)\n", - "thresh_tasmin = xr.DataArray(\n", - " [7] * 24 + [11] * 24, dims=(\"lat\",), coords={\"lat\": ds5.lat}, attrs={\"units\": \"°C\"}\n", - ")\n", + "thresh_tasmin = xr.DataArray([7] * 24 + [11] * 24, dims=(\"lat\",), coords={\"lat\": ds5.lat}, attrs={\"units\": \"°C\"})\n", "# The tasmax threshold is 17°C for the western half of the domain and 21°C for the eastern half.\n", - "thresh_tasmax = xr.DataArray(\n", - " [17] * 24 + [21] * 24, dims=(\"lon\",), coords={\"lon\": ds5.lon}, attrs={\"units\": \"°C\"}\n", - ")\n", + "thresh_tasmax = xr.DataArray([17] * 24 + [21] * 24, dims=(\"lon\",), coords={\"lon\": ds5.lon}, attrs={\"units\": \"°C\"})\n", "\n", "out_hw2d = xclim.atmos.heat_wave_total_length(\n", " tasmin=ds5.tasmin,\n", diff --git a/docs/notebooks/extendxclim.ipynb b/docs/notebooks/extendxclim.ipynb index 5fa869a51..540aba4f7 100644 --- a/docs/notebooks/extendxclim.ipynb +++ b/docs/notebooks/extendxclim.ipynb @@ -104,9 +104,7 @@ "\n", "\n", "@declare_units(tasmax=\"[temperature]\", thresh=\"[temperature]\")\n", - "def tx_days_compare(\n", - " tasmax: xr.DataArray, thresh: str = \"0 degC\", op: str = \">\", freq: str = \"YS\"\n", - "):\n", + "def tx_days_compare(tasmax: xr.DataArray, thresh: str = \"0 degC\", op: str = \">\", freq: str = \"YS\"):\n", " r\"\"\"\n", " Number of days where maximum daily temperature is above or under a threshold.\n", "\n", @@ -457,9 +455,7 @@ "source": [ "import xclim as xc\n", "\n", - "example = xc.core.indicator.build_indicator_module_from_yaml(\n", - " example_dir / \"example\", mode=\"raise\"\n", - ")" + "example = xc.core.indicator.build_indicator_module_from_yaml(example_dir / \"example\", mode=\"raise\")" ] }, { @@ -494,9 +490,7 @@ "ds = open_dataset(\"ERA5/daily_surface_cancities_1990-1993.nc\")\n", "with xr.set_options(keep_attrs=True):\n", " ds2 = ds.assign(\n", - " pr_per=xc.core.calendar.percentile_doy(ds.pr, window=5, per=75).isel(\n", - " percentiles=0\n", - " ),\n", + " pr_per=xc.core.calendar.percentile_doy(ds.pr, window=5, per=75).isel(percentiles=0),\n", " prveg=ds.pr * 1.1, # Very realistic\n", " )\n", " ds2.prveg.attrs[\"standard_name\"] = \"precipitation_flux_onto_canopy\"\n", diff --git a/docs/notebooks/frequency_analysis.ipynb b/docs/notebooks/frequency_analysis.ipynb index 0e68b1a18..e092ebbb5 100644 --- a/docs/notebooks/frequency_analysis.ipynb +++ b/docs/notebooks/frequency_analysis.ipynb @@ -75,9 +75,7 @@ "outputs": [], "source": [ "# Compute the design value\n", - "frequency_analysis(\n", - " pr, t=20, dist=\"genextreme\", mode=\"max\", freq=\"YS\", month=[5, 6, 7, 8, 9, 10]\n", - ")" + "frequency_analysis(pr, t=20, dist=\"genextreme\", mode=\"max\", freq=\"YS\", month=[5, 6, 7, 8, 9, 10])" ] }, { diff --git a/docs/notebooks/partitioning.ipynb b/docs/notebooks/partitioning.ipynb index 23601dc97..e50a4c192 100644 --- a/docs/notebooks/partitioning.ipynb +++ b/docs/notebooks/partitioning.ipynb @@ -64,11 +64,7 @@ " # Fetch data using pandas\n", " df = pd.read_csv(url, index_col=0, comment=\"#\", parse_dates=True)[\"world\"]\n", " # Convert to a DataArray, complete with coordinates.\n", - " da = (\n", - " xr.DataArray(df)\n", - " .expand_dims(model=[model], scenario=[scenario])\n", - " .rename(date=\"time\")\n", - " )\n", + " da = xr.DataArray(df).expand_dims(model=[model], scenario=[scenario]).rename(date=\"time\")\n", " data.append(da)" ] }, @@ -107,9 +103,7 @@ "plt.figure(figsize=(10, 4))\n", "for scenario in ens.scenario:\n", " for model in ens.model:\n", - " ens.sel(scenario=scenario, model=model).plot(\n", - " color=ssp_col[str(scenario.data)], alpha=0.8, lw=1\n", - " )\n", + " ens.sel(scenario=scenario, model=model).plot(color=ssp_col[str(scenario.data)], alpha=0.8, lw=1)\n", "plt.title(\"Projected mean global temperature\");" ] }, diff --git a/docs/notebooks/sdba-advanced.ipynb b/docs/notebooks/sdba-advanced.ipynb index 3e55fe3c8..b2a4eca7c 100644 --- a/docs/notebooks/sdba-advanced.ipynb +++ b/docs/notebooks/sdba-advanced.ipynb @@ -96,9 +96,7 @@ "# Plot nearest neighbors and weighing function\n", "wax = ax.twinx()\n", "wax.plot(tas.time, weights, color=\"indianred\")\n", - "ax.plot(\n", - " tas.time, tas.where(tas * weights > 0), \"o\", color=\"lightcoral\", fillstyle=\"none\"\n", - ")\n", + "ax.plot(tas.time, tas.where(tas * weights > 0), \"o\", color=\"lightcoral\", fillstyle=\"none\")\n", "\n", "ax.plot(ti, ys[366], \"ko\")\n", "wax.set_ylabel(\"Weights\")\n", @@ -238,9 +236,7 @@ "source": [ "from xclim.sdba.adjustment import QuantileDeltaMapping\n", "\n", - "QDM = QuantileDeltaMapping.train(\n", - " ref, hist, nquantiles=15, kind=\"+\", group=\"time.dayofyear\"\n", - ")\n", + "QDM = QuantileDeltaMapping.train(ref, hist, nquantiles=15, kind=\"+\", group=\"time.dayofyear\")\n", "QDM" ] }, @@ -327,9 +323,7 @@ "from xclim import set_options\n", "\n", "with set_options(sdba_extra_output=True):\n", - " QDM = QuantileDeltaMapping.train(\n", - " ref, hist, nquantiles=15, kind=\"+\", group=\"time.dayofyear\"\n", - " )\n", + " QDM = QuantileDeltaMapping.train(ref, hist, nquantiles=15, kind=\"+\", group=\"time.dayofyear\")\n", " out = QDM.adjust(sim)\n", "\n", "out.sim_q" @@ -369,9 +363,7 @@ "metadata": {}, "outputs": [], "source": [ - "QDM = QuantileDeltaMapping.train(\n", - " ref, hist, nquantiles=15, kind=\"+\", group=\"time.dayofyear\"\n", - ")\n", + "QDM = QuantileDeltaMapping.train(ref, hist, nquantiles=15, kind=\"+\", group=\"time.dayofyear\")\n", "\n", "scen_nowin = QDM.adjust(sim)" ] @@ -432,9 +424,7 @@ "\n", "group = sdba.Grouper(\"time.dayofyear\", window=31)\n", "\n", - "dref = convert_calendar(open_dataset(\"sdba/ahccd_1950-2013.nc\"), \"noleap\").sel(\n", - " time=slice(\"1981\", \"2010\")\n", - ")\n", + "dref = convert_calendar(open_dataset(\"sdba/ahccd_1950-2013.nc\"), \"noleap\").sel(time=slice(\"1981\", \"2010\"))\n", "dsim = open_dataset(\"sdba/CanESM2_1950-2100.nc\")\n", "\n", "dref = dref.assign(\n", @@ -502,9 +492,7 @@ "outputs": [], "source": [ "ref_res, ref_norm = sdba.processing.normalize(ref, group=group, kind=\"+\")\n", - "hist_res, hist_norm = sdba.processing.normalize(\n", - " sim.sel(time=slice(\"1981\", \"2010\")), group=group, kind=\"+\"\n", - ")\n", + "hist_res, hist_norm = sdba.processing.normalize(sim.sel(time=slice(\"1981\", \"2010\")), group=group, kind=\"+\")\n", "scaling = sdba.utils.get_correction(hist_norm, ref_norm, kind=\"+\")" ] }, @@ -514,9 +502,7 @@ "metadata": {}, "outputs": [], "source": [ - "sim_scaled = sdba.utils.apply_correction(\n", - " sim, sdba.utils.broadcast(scaling, sim, group=group), kind=\"+\"\n", - ")\n", + "sim_scaled = sdba.utils.apply_correction(sim, sdba.utils.broadcast(scaling, sim, group=group), kind=\"+\")\n", "\n", "loess = sdba.detrending.LoessDetrend(group=group, f=0.2, d=0, kind=\"+\", niter=1)\n", "simfit = loess.fit(sim_scaled)\n", @@ -588,15 +574,9 @@ "metadata": {}, "outputs": [], "source": [ - "dref.tasmax.sel(time=slice(\"1981\", \"2010\"), location=\"Vancouver\").groupby(\n", - " \"time.dayofyear\"\n", - ").mean().plot(label=\"obs\")\n", - "dsim.tasmax.sel(time=slice(\"1981\", \"2010\"), location=\"Vancouver\").groupby(\n", - " \"time.dayofyear\"\n", - ").mean().plot(label=\"raw\")\n", - "dscen.tasmax.sel(time=slice(\"1981\", \"2010\"), location=\"Vancouver\").groupby(\n", - " \"time.dayofyear\"\n", - ").mean().plot(label=\"scen\")\n", + "dref.tasmax.sel(time=slice(\"1981\", \"2010\"), location=\"Vancouver\").groupby(\"time.dayofyear\").mean().plot(label=\"obs\")\n", + "dsim.tasmax.sel(time=slice(\"1981\", \"2010\"), location=\"Vancouver\").groupby(\"time.dayofyear\").mean().plot(label=\"raw\")\n", + "dscen.tasmax.sel(time=slice(\"1981\", \"2010\"), location=\"Vancouver\").groupby(\"time.dayofyear\").mean().plot(label=\"scen\")\n", "plt.legend()" ] }, @@ -606,15 +586,9 @@ "metadata": {}, "outputs": [], "source": [ - "dref.pr.sel(time=slice(\"1981\", \"2010\"), location=\"Vancouver\").groupby(\n", - " \"time.dayofyear\"\n", - ").mean().plot(label=\"obs\")\n", - "dsim.pr.sel(time=slice(\"1981\", \"2010\"), location=\"Vancouver\").groupby(\n", - " \"time.dayofyear\"\n", - ").mean().plot(label=\"raw\")\n", - "dscen.pr.sel(time=slice(\"1981\", \"2010\"), location=\"Vancouver\").groupby(\n", - " \"time.dayofyear\"\n", - ").mean().plot(label=\"scen\")\n", + "dref.pr.sel(time=slice(\"1981\", \"2010\"), location=\"Vancouver\").groupby(\"time.dayofyear\").mean().plot(label=\"obs\")\n", + "dsim.pr.sel(time=slice(\"1981\", \"2010\"), location=\"Vancouver\").groupby(\"time.dayofyear\").mean().plot(label=\"raw\")\n", + "dscen.pr.sel(time=slice(\"1981\", \"2010\"), location=\"Vancouver\").groupby(\"time.dayofyear\").mean().plot(label=\"scen\")\n", "plt.legend()" ] }, @@ -645,19 +619,11 @@ "\n", "vals = np.random.randint(0, 1000, size=(t.size,)) / 100\n", "vals_ref = (4 ** np.where(vals < 9, vals / 100, vals)) / 3e6\n", - "vals_sim = (\n", - " (1 + 0.1 * np.random.random_sample((t.size,)))\n", - " * (4 ** np.where(vals < 9.5, vals / 100, vals))\n", - " / 3e6\n", - ")\n", + "vals_sim = (1 + 0.1 * np.random.random_sample((t.size,))) * (4 ** np.where(vals < 9.5, vals / 100, vals)) / 3e6\n", "\n", - "pr_ref = xr.DataArray(\n", - " vals_ref, coords={\"time\": t}, dims=(\"time\",), attrs={\"units\": \"mm/day\"}\n", - ")\n", + "pr_ref = xr.DataArray(vals_ref, coords={\"time\": t}, dims=(\"time\",), attrs={\"units\": \"mm/day\"})\n", "pr_ref = pr_ref.sel(time=slice(\"2000\", \"2015\"))\n", - "pr_sim = xr.DataArray(\n", - " vals_sim, coords={\"time\": t}, dims=(\"time\",), attrs={\"units\": \"mm/day\"}\n", - ")\n", + "pr_sim = xr.DataArray(vals_sim, coords={\"time\": t}, dims=(\"time\",), attrs={\"units\": \"mm/day\"})\n", "pr_hist = pr_sim.sel(time=slice(\"2000\", \"2015\"))" ] }, @@ -680,12 +646,8 @@ "from xclim import sdba\n", "\n", "group = sdba.Grouper(\"time.dayofyear\", window=31)\n", - "hist_ad, pth, dP0 = sdba.processing.adapt_freq(\n", - " pr_ref, pr_hist, thresh=\"0.05 mm d-1\", group=group\n", - ")\n", - "QM_ad = sdba.EmpiricalQuantileMapping.train(\n", - " pr_ref, hist_ad, nquantiles=15, kind=\"*\", group=group\n", - ")\n", + "hist_ad, pth, dP0 = sdba.processing.adapt_freq(pr_ref, pr_hist, thresh=\"0.05 mm d-1\", group=group)\n", + "QM_ad = sdba.EmpiricalQuantileMapping.train(pr_ref, hist_ad, nquantiles=15, kind=\"*\", group=group)\n", "scen_ad = QM_ad.adjust(pr_sim)\n", "\n", "pr_ref.sel(time=\"2010\").plot(alpha=0.9, label=\"Reference\")\n", @@ -757,20 +719,14 @@ "# load test data\n", "hist = open_dataset(\"sdba/CanESM2_1950-2100.nc\").sel(time=slice(\"1950\", \"1980\")).tasmax\n", "ref = open_dataset(\"sdba/nrcan_1950-2013.nc\").sel(time=slice(\"1950\", \"1980\")).tasmax\n", - "sim = (\n", - " open_dataset(\"sdba/CanESM2_1950-2100.nc\").sel(time=slice(\"1980\", \"2010\")).tasmax\n", - ") # biased\n", + "sim = open_dataset(\"sdba/CanESM2_1950-2100.nc\").sel(time=slice(\"1980\", \"2010\")).tasmax # biased\n", "\n", "# learn the bias in historical simulation compared to reference\n", - "QM = sdba.EmpiricalQuantileMapping.train(\n", - " ref, hist, nquantiles=50, group=\"time\", kind=\"+\"\n", - ")\n", + "QM = sdba.EmpiricalQuantileMapping.train(ref, hist, nquantiles=50, group=\"time\", kind=\"+\")\n", "\n", "# correct the bias in the future\n", "scen = QM.adjust(sim, extrapolation=\"constant\", interp=\"nearest\")\n", - "ref_future = (\n", - " open_dataset(\"sdba/nrcan_1950-2013.nc\").sel(time=slice(\"1980\", \"2010\")).tasmax\n", - ") # truth\n", + "ref_future = open_dataset(\"sdba/nrcan_1950-2013.nc\").sel(time=slice(\"1980\", \"2010\")).tasmax # truth\n", "\n", "plt.figure(figsize=(15, 5))\n", "lw = 0.3\n", @@ -789,18 +745,12 @@ "outputs": [], "source": [ "# calculate the mean warm Spell Length Distribution\n", - "sim_prop = sdba.properties.spell_length_distribution(\n", - " da=sim, thresh=\"28 degC\", op=\">\", stat=\"mean\", group=\"time\"\n", - ")\n", + "sim_prop = sdba.properties.spell_length_distribution(da=sim, thresh=\"28 degC\", op=\">\", stat=\"mean\", group=\"time\")\n", "\n", "\n", - "scen_prop = sdba.properties.spell_length_distribution(\n", - " da=scen, thresh=\"28 degC\", op=\">\", stat=\"mean\", group=\"time\"\n", - ")\n", + "scen_prop = sdba.properties.spell_length_distribution(da=scen, thresh=\"28 degC\", op=\">\", stat=\"mean\", group=\"time\")\n", "\n", - "ref_prop = sdba.properties.spell_length_distribution(\n", - " da=ref_future, thresh=\"28 degC\", op=\">\", stat=\"mean\", group=\"time\"\n", - ")\n", + "ref_prop = sdba.properties.spell_length_distribution(da=ref_future, thresh=\"28 degC\", op=\">\", stat=\"mean\", group=\"time\")\n", "# measure the difference between the prediction and the reference with an absolute bias of the properties\n", "measure_sim = sdba.measures.bias(sim_prop, ref_prop)\n", "measure_scen = sdba.measures.bias(scen_prop, ref_prop)\n", @@ -808,9 +758,7 @@ "plt.figure(figsize=(5, 3))\n", "plt.plot(measure_sim.location, measure_sim.values, \".\", label=\"biased model (sim)\")\n", "plt.plot(measure_scen.location, measure_scen.values, \".\", label=\"adjusted model (scen)\")\n", - "plt.title(\n", - " \"Bias of the mean of the warm spell \\n length distribution compared to observations\"\n", - ")\n", + "plt.title(\"Bias of the mean of the warm spell \\n length distribution compared to observations\")\n", "plt.legend()\n", "plt.ylim(-2.5, 2.5)" ] @@ -830,9 +778,7 @@ "outputs": [], "source": [ "# calculate the mean warm Spell Length Distribution\n", - "sim_prop = sdba.properties.spell_length_distribution(\n", - " da=sim, thresh=\"28 degC\", op=\">\", stat=\"mean\", group=\"time.season\"\n", - ")\n", + "sim_prop = sdba.properties.spell_length_distribution(da=sim, thresh=\"28 degC\", op=\">\", stat=\"mean\", group=\"time.season\")\n", "\n", "scen_prop = sdba.properties.spell_length_distribution(\n", " da=scen, thresh=\"28 degC\", op=\">\", stat=\"mean\", group=\"time.season\"\n", @@ -849,9 +795,7 @@ "fig, axs = plt.subplots(2, 2, figsize=(9, 6))\n", "axs = axs.ravel()\n", "for i in range(4):\n", - " axs[i].plot(\n", - " measure_sim.location, measure_sim.values[:, i], \".\", label=\"biased model (sim)\"\n", - " )\n", + " axs[i].plot(measure_sim.location, measure_sim.values[:, i], \".\", label=\"biased model (sim)\")\n", " axs[i].plot(\n", " measure_scen.location,\n", " measure_scen.isel(season=i).values,\n", @@ -861,9 +805,7 @@ " axs[i].set_title(measure_scen.season.values[i])\n", " axs[i].legend(loc=\"lower right\")\n", " axs[i].set_ylim(-2.5, 2.5)\n", - "fig.suptitle(\n", - " \"Bias of the mean of the warm spell length distribution compared to observations\"\n", - ")\n", + "fig.suptitle(\"Bias of the mean of the warm spell length distribution compared to observations\")\n", "plt.tight_layout()" ] } diff --git a/docs/notebooks/sdba.ipynb b/docs/notebooks/sdba.ipynb index 04015c0cd..e13289cf4 100644 --- a/docs/notebooks/sdba.ipynb +++ b/docs/notebooks/sdba.ipynb @@ -72,9 +72,7 @@ "source": [ "from xclim import sdba\n", "\n", - "QM = sdba.EmpiricalQuantileMapping.train(\n", - " ref, hist, nquantiles=15, group=\"time\", kind=\"+\"\n", - ")\n", + "QM = sdba.EmpiricalQuantileMapping.train(ref, hist, nquantiles=15, group=\"time\", kind=\"+\")\n", "scen = QM.adjust(sim, extrapolation=\"constant\", interp=\"nearest\")\n", "\n", "ref.groupby(\"time.dayofyear\").mean().plot(label=\"Reference\")\n", @@ -103,9 +101,7 @@ "metadata": {}, "outputs": [], "source": [ - "QM_mo = sdba.EmpiricalQuantileMapping.train(\n", - " ref, hist, nquantiles=15, group=\"time.month\", kind=\"+\"\n", - ")\n", + "QM_mo = sdba.EmpiricalQuantileMapping.train(ref, hist, nquantiles=15, group=\"time.month\", kind=\"+\")\n", "scen = QM_mo.adjust(sim, extrapolation=\"constant\", interp=\"linear\")\n", "\n", "ref.groupby(\"time.dayofyear\").mean().plot(label=\"Reference\")\n", @@ -227,19 +223,11 @@ "source": [ "vals = np.random.randint(0, 1000, size=(t.size,)) / 100\n", "vals_ref = (4 ** np.where(vals < 9, vals / 100, vals)) / 3e6\n", - "vals_sim = (\n", - " (1 + 0.1 * np.random.random_sample((t.size,)))\n", - " * (4 ** np.where(vals < 9.5, vals / 100, vals))\n", - " / 3e6\n", - ")\n", + "vals_sim = (1 + 0.1 * np.random.random_sample((t.size,))) * (4 ** np.where(vals < 9.5, vals / 100, vals)) / 3e6\n", "\n", - "pr_ref = xr.DataArray(\n", - " vals_ref, coords={\"time\": t}, dims=(\"time\",), attrs={\"units\": \"mm/day\"}\n", - ")\n", + "pr_ref = xr.DataArray(vals_ref, coords={\"time\": t}, dims=(\"time\",), attrs={\"units\": \"mm/day\"})\n", "pr_ref = pr_ref.sel(time=slice(\"2000\", \"2015\"))\n", - "pr_sim = xr.DataArray(\n", - " vals_sim, coords={\"time\": t}, dims=(\"time\",), attrs={\"units\": \"mm/day\"}\n", - ")\n", + "pr_sim = xr.DataArray(vals_sim, coords={\"time\": t}, dims=(\"time\",), attrs={\"units\": \"mm/day\"})\n", "pr_hist = pr_sim.sel(time=slice(\"2000\", \"2015\"))\n", "\n", "pr_ref.plot(alpha=0.9, label=\"Reference\")\n", @@ -254,9 +242,7 @@ "outputs": [], "source": [ "# 1st try without adapt_freq\n", - "QM = sdba.EmpiricalQuantileMapping.train(\n", - " pr_ref, pr_hist, nquantiles=15, kind=\"*\", group=\"time\"\n", - ")\n", + "QM = sdba.EmpiricalQuantileMapping.train(pr_ref, pr_hist, nquantiles=15, kind=\"*\", group=\"time\")\n", "scen = QM.adjust(pr_sim)\n", "\n", "pr_ref.sel(time=\"2010\").plot(alpha=0.9, label=\"Reference\")\n", @@ -279,12 +265,8 @@ "outputs": [], "source": [ "# 2nd try with adapt_freq\n", - "hist_ad, pth, dP0 = sdba.processing.adapt_freq(\n", - " pr_ref, pr_hist, thresh=\"0.05 mm d-1\", group=\"time\"\n", - ")\n", - "QM_ad = sdba.EmpiricalQuantileMapping.train(\n", - " pr_ref, hist_ad, nquantiles=15, kind=\"*\", group=\"time\"\n", - ")\n", + "hist_ad, pth, dP0 = sdba.processing.adapt_freq(pr_ref, pr_hist, thresh=\"0.05 mm d-1\", group=\"time\")\n", + "QM_ad = sdba.EmpiricalQuantileMapping.train(pr_ref, hist_ad, nquantiles=15, kind=\"*\", group=\"time\")\n", "scen_ad = QM_ad.adjust(pr_sim)\n", "\n", "pr_ref.sel(time=\"2010\").plot(alpha=0.9, label=\"Reference\")\n", @@ -321,9 +303,7 @@ "ref_n, _ = sdba.processing.normalize(ref, group=doy_win31, kind=\"+\")\n", "hist_n, _ = sdba.processing.normalize(hist, group=doy_win31, kind=\"+\")\n", "\n", - "QM = sdba.EmpiricalQuantileMapping.train(\n", - " ref_n, hist_n, nquantiles=15, group=\"time.month\", kind=\"+\"\n", - ")\n", + "QM = sdba.EmpiricalQuantileMapping.train(ref_n, hist_n, nquantiles=15, group=\"time.month\", kind=\"+\")\n", "scen_detrended = QM.adjust(sim_detrended, extrapolation=\"constant\", interp=\"nearest\")\n", "scen = sim_fit.retrend(scen_detrended)\n", "\n", @@ -380,9 +360,7 @@ "scen1 = PCA.adjust(simt)\n", "\n", "# QM, no grouping, 20 quantiles and additive adjustment\n", - "EQM = sdba.EmpiricalQuantileMapping.train(\n", - " reft, scen1, group=\"time\", nquantiles=50, kind=\"+\"\n", - ")\n", + "EQM = sdba.EmpiricalQuantileMapping.train(reft, scen1, group=\"time\", nquantiles=50, kind=\"+\")\n", "scen2 = EQM.adjust(scen1)" ] }, @@ -419,9 +397,7 @@ " axQM.plot(refQ.isel(lon=i), simQ.isel(lon=i), label=\"No adj\")\n", " axQM.plot(refQ.isel(lon=i), scen1Q.isel(lon=i), label=\"PCA\")\n", " axQM.plot(refQ.isel(lon=i), scen2Q.isel(lon=i), label=\"PCA+EQM\")\n", - " axQM.plot(\n", - " refQ.isel(lon=i), refQ.isel(lon=i), color=\"k\", linestyle=\":\", label=\"Ideal\"\n", - " )\n", + " axQM.plot(refQ.isel(lon=i), refQ.isel(lon=i), color=\"k\", linestyle=\":\", label=\"Ideal\")\n", " axQM.set_title(f\"QQ plot - Point {i + 1}\")\n", " axQM.set_xlabel(\"Reference\")\n", " axQM.set_xlabel(\"Model\")\n", @@ -459,9 +435,7 @@ "from xclim.core.units import convert_units_to\n", "from xclim.testing import open_dataset\n", "\n", - "dref = open_dataset(\"sdba/ahccd_1950-2013.nc\", drop_variables=[\"lat\", \"lon\"]).sel(\n", - " time=slice(\"1981\", \"2010\")\n", - ")\n", + "dref = open_dataset(\"sdba/ahccd_1950-2013.nc\", drop_variables=[\"lat\", \"lon\"]).sel(time=slice(\"1981\", \"2010\"))\n", "\n", "# Fix the standard name of the `pr` variable.\n", "# This allows the convert_units_to below to infer the correct CF transformation (precip rate to flux)\n", @@ -510,9 +484,7 @@ " ref,\n", " hist,\n", " sim,\n", - " kind={\n", - " \"pr\": \"*\"\n", - " }, # Since this bias correction method is multivariate, `kind` must be specified per variable\n", + " kind={\"pr\": \"*\"}, # Since this bias correction method is multivariate, `kind` must be specified per variable\n", " adapt_freq_thresh={\"pr\": \"3.5e-4 kg m-2 s-1\"}, # Idem\n", ")" ] @@ -644,9 +616,9 @@ "from xclim.core.units import convert_units_to\n", "from xclim.testing import open_dataset\n", "\n", - "dref = open_dataset(\n", - " \"sdba/ahccd_1950-2013.nc\", chunks={\"location\": 1}, drop_variables=[\"lat\", \"lon\"]\n", - ").sel(time=slice(\"1981\", \"2010\"))\n", + "dref = open_dataset(\"sdba/ahccd_1950-2013.nc\", chunks={\"location\": 1}, drop_variables=[\"lat\", \"lon\"]).sel(\n", + " time=slice(\"1981\", \"2010\")\n", + ")\n", "\n", "# Fix the standard name of the `pr` variable.\n", "# This allows the convert_units_to below to infer the correct CF transformation (precip rate to flux)\n", @@ -657,9 +629,7 @@ " tasmax=convert_units_to(dref.tasmax, \"K\"),\n", " pr=convert_units_to(dref.pr, \"kg m-2 s-1\"),\n", ")\n", - "dsim = open_dataset(\n", - " \"sdba/CanESM2_1950-2100.nc\", chunks={\"location\": 1}, drop_variables=[\"lat\", \"lon\"]\n", - ")\n", + "dsim = open_dataset(\"sdba/CanESM2_1950-2100.nc\", chunks={\"location\": 1}, drop_variables=[\"lat\", \"lon\"])\n", "\n", "dhist = dsim.sel(time=slice(\"1981\", \"2010\"))\n", "dsim = dsim.sel(time=slice(\"2041\", \"2070\"))\n", @@ -750,9 +720,7 @@ "outputs": [], "source": [ "fig, axs = plt.subplots(1, 2, figsize=(16, 4))\n", - "for da, label in zip(\n", - " (ref, scenh, hist), (\"Reference\", \"Adjusted\", \"Simulated\"), strict=False\n", - "):\n", + "for da, label in zip((ref, scenh, hist), (\"Reference\", \"Adjusted\", \"Simulated\"), strict=False):\n", " ds = sdba.unstack_variables(da).isel(location=2)\n", " # time series - tasmax\n", " ds.tasmax.plot(ax=axs[0], label=label, alpha=0.65 if label == \"Adjusted\" else 1)\n", diff --git a/docs/notebooks/units.ipynb b/docs/notebooks/units.ipynb index b8c4072b8..7ff9d63e2 100644 --- a/docs/notebooks/units.ipynb +++ b/docs/notebooks/units.ipynb @@ -49,11 +49,7 @@ "source": [ "# See the Usage page for details on opening datasets, subsetting and resampling.\n", "ds = xr.tutorial.load_dataset(\"air_temperature\")\n", - "tas = (\n", - " ds.air.sel(lat=40, lon=270, method=\"nearest\")\n", - " .resample(time=\"D\")\n", - " .mean(keep_attrs=True)\n", - ")\n", + "tas = ds.air.sel(lat=40, lon=270, method=\"nearest\").resample(time=\"D\").mean(keep_attrs=True)\n", "print(tas.attrs[\"units\"])" ] }, @@ -194,9 +190,7 @@ "outputs": [], "source": [ "ds = xr.tutorial.load_dataset(\"air_temperature\")\n", - "tas_6h = ds.air.sel(\n", - " lat=40, lon=270, method=\"nearest\"\n", - ") # no resampling, original data is 6-hourly\n", + "tas_6h = ds.air.sel(lat=40, lon=270, method=\"nearest\") # no resampling, original data is 6-hourly\n", "tas_D = tas_6h.resample(time=\"D\").mean()\n", "out1_h = xclim.indices.tx_days_above(tasmax=tas_6h, thresh=\"20 C\", freq=\"MS\")\n", "out2_D = xclim.indices.tx_days_above(tasmax=tas_D, thresh=\"20 C\", freq=\"MS\")\n", @@ -222,7 +216,7 @@ "source": [ "## Temperature differences vs absolute temperature\n", "\n", - "Temperature anomalies and biases as well as degree-days indicators are all *differences* between temperatures. If we assign those differences units of degrees Celsius, then converting to Kelvins or Fahrenheits will yield nonsensical values. ``pint`` solves this using *delta* units such as ``delta_degC`` and ``delta_degF``. \n" + "Temperature anomalies and biases as well as degree-days indicators are all *differences* between temperatures. If we assign those differences units of degrees Celsius, then converting to Kelvins or Fahrenheits will yield nonsensical values. ``pint`` solves this using *delta* units such as ``delta_degC`` and ``delta_degF``.\n" ] }, { @@ -231,7 +225,8 @@ "metadata": {}, "outputs": [], "source": [ - "# If we have a DataArray storing a temperature anomaly of 2°C, converting to Kelvin will yield a nonsensical value 0f 275.15.\n", + "# If we have a DataArray storing a temperature anomaly of 2°C,\n", + "# converting to Kelvin will yield a nonsensical value 0f 275.15.\n", "# Fortunately, pint has delta units to represent temperature differences.\n", "display(units.convert_units_to(xr.DataArray([2], attrs={\"units\": \"delta_degC\"}), \"K\"))" ] @@ -240,7 +235,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The issue for ``xclim`` is that there are no equivalent delta units in the CF convention. To resolve this ambiguity, the [CF convention](https://cfconventions.org/Data/cf-conventions/cf-conventions-1.11/cf-conventions.html#temperature-units) recommends including a ``units_metadata`` attribute set to ``\"temperature: difference\"``, and this is supported in ``xclim`` as of version 0.52. The function ``units2pint`` interprets the ``units_metadata`` attribute and returns a ``pint`` delta unit as needed. To convert a ``pint`` delta unit to CF attributes, use the function ``pint2cfattrs``, which returns a dictionary with the ``units`` and ``units_metadata`` attributes (``pint2cfunits`` cannot support the convention because it only returns the unit string). " + "The issue for ``xclim`` is that there are no equivalent delta units in the CF convention. To resolve this ambiguity, the [CF convention](https://cfconventions.org/Data/cf-conventions/cf-conventions-1.11/cf-conventions.html#temperature-units) recommends including a ``units_metadata`` attribute set to ``\"temperature: difference\"``, and this is supported in ``xclim`` as of version 0.52. The function ``units2pint`` interprets the ``units_metadata`` attribute and returns a ``pint`` delta unit as needed. To convert a ``pint`` delta unit to CF attributes, use the function ``pint2cfattrs``, which returns a dictionary with the ``units`` and ``units_metadata`` attributes (``pint2cfunits`` cannot support the convention because it only returns the unit string)." ] }, { @@ -249,9 +244,7 @@ "metadata": {}, "outputs": [], "source": [ - "delta = xr.DataArray(\n", - " [2], attrs={\"units\": \"K\", \"units_metadata\": \"temperature: difference\"}\n", - ")\n", + "delta = xr.DataArray([2], attrs={\"units\": \"K\", \"units_metadata\": \"temperature: difference\"})\n", "units.convert_units_to(delta, \"delta_degC\")" ] }, diff --git a/docs/notebooks/usage.ipynb b/docs/notebooks/usage.ipynb index 175b66016..03544b3a5 100644 --- a/docs/notebooks/usage.ipynb +++ b/docs/notebooks/usage.ipynb @@ -218,9 +218,7 @@ "outputs": [], "source": [ "with xclim.set_options(cf_compliance=\"log\"):\n", - " gdd = xclim.atmos.growing_degree_days(\n", - " tas=daily_ds.air, thresh=\"10 degC\", freq=\"YS\", date_bounds=(\"04-01\", \"09-30\")\n", - " )\n", + " gdd = xclim.atmos.growing_degree_days(tas=daily_ds.air, thresh=\"10 degC\", freq=\"YS\", date_bounds=(\"04-01\", \"09-30\"))\n", "gdd" ] }, @@ -238,15 +236,11 @@ "outputs": [], "source": [ "with xclim.set_options(cf_compliance=\"log\"):\n", - " gdd = xclim.atmos.growing_degree_days(\n", - " tas=\"air\", thresh=\"10.0 degC\", freq=\"MS\", ds=daily_ds\n", - " )\n", + " gdd = xclim.atmos.growing_degree_days(tas=\"air\", thresh=\"10.0 degC\", freq=\"MS\", ds=daily_ds)\n", "\n", " # variable names default to xclim names, so we can even do this:\n", " renamed_daily_ds = daily_ds.rename(air=\"tas\")\n", - " gdd = xclim.atmos.growing_degree_days(\n", - " thresh=\"10.0 degC\", freq=\"MS\", ds=renamed_daily_ds\n", - " )\n", + " gdd = xclim.atmos.growing_degree_days(thresh=\"10.0 degC\", freq=\"MS\", ds=renamed_daily_ds)\n", "gdd" ] }, diff --git a/pyproject.toml b/pyproject.toml index b94c567b2..342d63703 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -305,7 +305,7 @@ xfail_strict = true [tool.ruff] src = ["xclim"] -line-length = 150 +line-length = 120 exclude = [ ".eggs", ".git", @@ -317,7 +317,8 @@ extend-include = [ [tool.ruff.format] exclude = [ - "docs/notebooks/xclim_training/*.ipynb" + "docs/notebooks/xclim_training/*.ipynb", + "src/xclim/sdba/*.py" ] line-ending = "lf" docstring-code-format = true @@ -325,7 +326,8 @@ docstring-code-line-length = "dynamic" [tool.ruff.lint] exclude = [ - "docs/notebooks/xclim_training/*.ipynb" + "docs/notebooks/xclim_training/*.ipynb", + "src/xclim/sdba/*.py" ] extend-select = [ "D213", # multi-line-summary-second-line @@ -377,15 +379,16 @@ max-complexity = 20 [tool.ruff.lint.per-file-ignores] "docs/*.py" = ["D100", "D101", "D102", "D103"] -"docs/notebooks/*.ipynb" = ["E225", "E226", "E231"] +"docs/notebooks/*.ipynb" = ["E225", "E226", "E231", "E501"] "src/xclim/**/__init__.py" = ["F401", "F403"] +"src/xclim/analog.py" = ["E501"] "src/xclim/core/indicator.py" = ["D214", "D405", "D406", "D407", "D411"] "src/xclim/core/locales.py" = ["E501", "W505"] "src/xclim/core/missing" = ["D103"] "src/xclim/indices/_agro.py" = ["E501"] "src/xclim/indices/fire/_cffwis.py" = ["D103"] "src/xclim/sdba/utils.py" = ["D103"] -"tests/**/*test*.py" = ["D100", "D101", "D102", "D103", "N802", "S101"] +"tests/**/*test*.py" = ["D100", "D101", "D102", "D103", "E501", "N802", "S101"] [tool.ruff.lint.pycodestyle] max-doc-length = 180 diff --git a/src/xclim/analog.py b/src/xclim/analog.py index 239856463..7911a264d 100644 --- a/src/xclim/analog.py +++ b/src/xclim/analog.py @@ -61,9 +61,7 @@ def spatial_analogs( # Create the target DataArray # drop any (sub-)index along "dist_dim" that could conflict with target, and rename it. # The drop is the simplest solution that is compatible with both xarray <=2022.3.0 and >2022.3.1 - candidate_array = candidates.to_array("_indices", "candidates").rename( - {dist_dim: "_dist_dim"} - ) + candidate_array = candidates.to_array("_indices", "candidates").rename({dist_dim: "_dist_dim"}) if isinstance(candidate_array.indexes["_dist_dim"], pd.MultiIndex): candidate_array = candidate_array.drop_vars( ["_dist_dim"] + candidate_array.indexes["_dist_dim"].names, @@ -432,7 +430,7 @@ def friedman_rafsky(x: np.ndarray, y: np.ndarray) -> float: @metric -def kolmogorov_smirnov(x: np.ndarray, y: np.ndarray) -> float: +def kolmogorov_smirnov(x: np.ndarray, y: np.ndarray) -> float: # noqa: E501 """ Compute the Kolmogorov-Smirnov statistic applied to two multivariate samples as described by Fasano and Franceschini. @@ -497,9 +495,7 @@ def pivot(_x: np.ndarray, _y: np.ndarray) -> float: @metric -def kldiv( - x: np.ndarray, y: np.ndarray, *, k: int | Sequence[int] = 1 -) -> float | Sequence[float]: +def kldiv(x: np.ndarray, y: np.ndarray, *, k: int | Sequence[int] = 1) -> float | Sequence[float]: r""" Compute the Kullback-Leibler divergence between two multivariate samples. @@ -529,9 +525,10 @@ def kldiv( Notes ----- - In information theory, the Kullback–Leibler divergence :cite:p:`perez-cruz_kullback-leibler_2008` is a non-symmetric - measure of the difference between two probability distributions P and Q, where P is the "true" distribution and Q an - approximation. This nuance is important because :math:`D(P||Q)` is not equal to :math:`D(Q||P)`. + In information theory, the Kullback–Leibler divergence :cite:p:`perez-cruz_kullback-leibler_2008` is + a non-symmetric measure of the difference between two probability distributions P and Q, where P is the + "true" distribution and Q an approximation. + This nuance is important because :math:`D(P||Q)` is not equal to :math:`D(Q||P)`. For probability distributions P and Q of a continuous random variable, the K–L divergence is defined as: @@ -542,9 +539,9 @@ def kldiv( This formula assumes we have a representation of the probability densities :math:`p(x)` and :math:`q(x)`. In many cases, we only have samples from the distribution, and most methods first estimate the densities from the samples and then proceed to compute the K-L divergence. In :cite:t:`perez-cruz_kullback-leibler_2008`, the author - proposes an algorithm to estimate the K-L divergence directly from the sample using an empirical CDF. Even though the - CDFs do not converge to their true values, the paper proves that the K-L divergence almost surely does converge to - its true value. + proposes an algorithm to estimate the K-L divergence directly from the sample using an empirical CDF. + Even though the CDFs do not converge to their true values, the paper proves that the K-L divergence almost + surely does converge to its true value. References ---------- @@ -582,9 +579,7 @@ def kldiv( # Hence, we take the k'th + 1, which in 0-based indexing is given by # index k. with np.errstate(divide="ignore"): - ki_calc = -np.log(r[:, ki] / s[:, ki - 1]).sum() * d / nx + np.log( - ny / (nx - 1.0) - ) + ki_calc = -np.log(r[:, ki] / s[:, ki - 1]).sum() * d / nx + np.log(ny / (nx - 1.0)) out.append(ki_calc) if mk: diff --git a/src/xclim/cli.py b/src/xclim/cli.py index 9b5fb4eb2..3e62ac243 100644 --- a/src/xclim/cli.py +++ b/src/xclim/cli.py @@ -44,9 +44,7 @@ def _get_indicator(indicator_name): try: return xc.core.indicator.registry[indicator_name.upper()].get_instance() # noqa except KeyError as e: - raise click.BadArgumentUsage( - f"Indicator '{indicator_name}' not found in xclim." - ) from e + raise click.BadArgumentUsage(f"Indicator '{indicator_name}' not found in xclim.") from e def _get_input(ctx): @@ -81,9 +79,7 @@ def _get_output(ctx): ds_in = _get_input(ctx) ctx.obj["ds_out"] = xr.Dataset(attrs=ds_in.attrs) if ctx.obj["output"] is None: - raise click.BadOptionUsage( - "output", "No output file name given.", ctx.parent - ) + raise click.BadOptionUsage("output", "No output file name given.", ctx.parent) return ctx.obj["ds_out"] @@ -200,38 +196,26 @@ def prefetch_testing_data(ctx, repo, branch, cache_dir): # numpydoc ignore=PR01 testdata_cache_dir = TESTDATA_CACHE_DIR click.echo(f"Gathering testing data from {testdata_repo}/{testdata_branch} ...") - click.echo( - populate_testing_data( - repo=testdata_repo, branch=testdata_branch, local_cache=testdata_cache_dir - ) - ) + click.echo(populate_testing_data(repo=testdata_repo, branch=testdata_branch, local_cache=testdata_cache_dir)) click.echo(f"Testing data saved to `{testdata_cache_dir}`.") ctx.exit() @click.command(short_help="Print history for publishing purposes.") @click.option("-m", "--md", is_flag=True, help="Prints the history in Markdown format.") -@click.option( - "-r", "--rst", is_flag=True, help="Prints the history in ReStructuredText format." -) -@click.option( - "-c", "--changes", help="Pass a custom changelog file to be used instead." -) +@click.option("-r", "--rst", is_flag=True, help="Prints the history in ReStructuredText format.") +@click.option("-c", "--changes", help="Pass a custom changelog file to be used instead.") @click.pass_context def release_notes(ctx, md, rst, changes): # numpydoc ignore=PR01 """Generate the release notes history for publishing purposes.""" if md and rst: - raise click.BadArgumentUsage( - "Cannot return both Markdown and ReStructuredText in same release_notes call." - ) + raise click.BadArgumentUsage("Cannot return both Markdown and ReStructuredText in same release_notes call.") if md: style = "md" elif rst: style = "rst" else: - raise click.BadArgumentUsage( - "Must specify Markdown (-m) or ReStructuredText (-r)." - ) + raise click.BadArgumentUsage("Must specify Markdown (-m) or ReStructuredText (-r).") if changes: click.echo(f"{publish_release_notes(style, changes=changes)}") @@ -296,9 +280,7 @@ def dataflags(ctx, variables, raise_flags, append, dims, freq): # numpydoc igno exit_code = 0 for v in variables: try: - flagged_var = data_flags( - ds[v], ds, dims=dims, freq=freq, raise_flags=raise_flags - ) + flagged_var = data_flags(ds[v], ds, dims=dims, freq=freq, raise_flags=raise_flags) if output: flagged = xr.merge([flagged, flagged_var]) except DataQualityException as e: @@ -309,9 +291,7 @@ def dataflags(ctx, variables, raise_flags, append, dims, freq): # numpydoc igno ctx.exit(exit_code) else: try: - flagged = ecad_compliant( - ds, dims=dims, raise_flags=raise_flags, append=append - ) + flagged = ecad_compliant(ds, dims=dims, raise_flags=raise_flags, append=append) if raise_flags: click.echo("Dataset passes quality control checks!") ctx.exit() @@ -325,9 +305,7 @@ def dataflags(ctx, variables, raise_flags, append, dims, freq): # numpydoc igno @click.command(short_help="List indicators.") -@click.option( - "-i", "--info", is_flag=True, help="Prints more details for each indicator." -) +@click.option("-i", "--info", is_flag=True, help="Prints more details for each indicator.") def indices(info): # numpydoc ignore=PR01 """List all indicators.""" formatter = click.HelpFormatter() @@ -335,13 +313,9 @@ def indices(info): # numpydoc ignore=PR01 rows = [] for name, indcls in xc.core.indicator.registry.items(): # noqa left = click.style(name.lower(), fg="yellow") - right = ", ".join( - [var.get("long_name", var["var_name"]) for var in indcls.cf_attrs] - ) + right = ", ".join([var.get("long_name", var["var_name"]) for var in indcls.cf_attrs]) if indcls.cf_attrs[0]["var_name"] != name.lower(): - right += ( - " (" + ", ".join([var["var_name"] for var in indcls.cf_attrs]) + ")" - ) + right += " (" + ", ".join([var["var_name"] for var in indcls.cf_attrs]) + ")" if info: right += "\n" + indcls.abstract rows.append((left, right)) @@ -359,10 +333,7 @@ def info(ctx, indicator): # numpydoc ignore=PR01 ind = _get_indicator(indname) command = _create_command(indname) formatter = click.HelpFormatter() - with formatter.section( - click.style("Indicator", fg="blue") - + click.style(f" {indname}", fg="yellow") - ): + with formatter.section(click.style("Indicator", fg="blue") + click.style(f" {indname}", fg="yellow")): data = ind.json() data.pop("parameters") _format_dict(data, formatter, key_fg="blue", spaces=2) @@ -376,25 +347,19 @@ def _format_dict(data, formatter, key_fg="blue", spaces=2): for attr, val in data.items(): if isinstance(val, list): for isub, sub in enumerate(val): - formatter.write_text( - click.style(" " * spaces + f"{attr} (#{isub + 1})", fg=key_fg) - ) + formatter.write_text(click.style(" " * spaces + f"{attr} (#{isub + 1})", fg=key_fg)) _format_dict(sub, formatter, key_fg=key_fg, spaces=spaces + 2) elif isinstance(val, dict): formatter.write_text(click.style(" " * spaces + f"{attr}:", fg=key_fg)) _format_dict(val, formatter, key_fg=key_fg, spaces=spaces + 2) else: - formatter.write_text( - click.style(" " * spaces + attr + " :", fg=key_fg) + " " + str(val) - ) + formatter.write_text(click.style(" " * spaces + attr + " :", fg=key_fg) + " " + str(val)) class XclimCli(click.MultiCommand): """Main cli class.""" - def list_commands( - self, ctx - ) -> tuple[str, str, str, str, str, str]: # numpydoc ignore=PR01,RT01 + def list_commands(self, ctx) -> tuple[str, str, str, str, str, str]: # numpydoc ignore=PR01,RT01 """Return the available commands (other than the indicators).""" return ( "indices", @@ -435,12 +400,8 @@ def get_command(self, ctx, cmd_name) -> click.Command: # numpydoc ignore=PR01,R multiple=True, ) @click.option("-o", "--output", help="Output filepath. A new file will be created") -@click.option( - "-v", "--verbose", help="Print details about context and progress.", count=True -) -@click.option( - "-V", "--version", is_flag=True, help="Prints xclim's version number and exits" -) +@click.option("-v", "--verbose", help="Print details about context and progress.", count=True) +@click.option("-V", "--version", is_flag=True, help="Prints xclim's version number and exits") @click.option( "--dask-nthreads", type=int, @@ -459,8 +420,7 @@ def get_command(self, ctx, cmd_name) -> click.Command: # numpydoc ignore=PR01,R ) @click.option( "--engine", - help="Engine to use when opening the input dataset(s). " - "If not specified, xarray decides.", + help="Engine to use when opening the input dataset(s). If not specified, xarray decides.", ) @click.pass_context def cli(ctx, **kwargs): # numpydoc ignore=PR01 @@ -508,10 +468,7 @@ def cli(ctx, **kwargs): # numpydoc ignore=PR01 f"{client.scheduler_info()['services']['dashboard']}/status" ) if kwargs["chunks"] is not None: - kwargs["chunks"] = { - dim: int(num) - for dim, num in map(lambda x: x.split(":"), kwargs["chunks"].split(",")) - } + kwargs["chunks"] = {dim: int(num) for dim, num in map(lambda x: x.split(":"), kwargs["chunks"].split(","))} kwargs["xr_kwargs"] = { "chunks": kwargs["chunks"] or {}, @@ -527,9 +484,7 @@ def write_file(ctx, *_, **kwargs): # numpydoc ignore=PR01 if ctx.obj["verbose"]: click.echo(f"Writing to file {ctx.obj['output']}") with ProgressBar(): - r = ctx.obj["ds_out"].to_netcdf( - ctx.obj["output"], engine=kwargs["engine"], compute=False - ) + r = ctx.obj["ds_out"].to_netcdf(ctx.obj["output"], engine=kwargs["engine"], compute=False) if ctx.obj["dask_nthreads"] is not None: progress(r.data) r.compute() diff --git a/src/xclim/core/bootstrapping.py b/src/xclim/core/bootstrapping.py index 81fe85e88..0334dc063 100644 --- a/src/xclim/core/bootstrapping.py +++ b/src/xclim/core/bootstrapping.py @@ -134,17 +134,13 @@ def bootstrap_func(compute_index_func: Callable, **kwargs) -> xarray.DataArray: else: da_key = name if da_key is None or per_key is None: - raise KeyError( - "The input data and the percentile DataArray must be provided as named arguments." - ) + raise KeyError("The input data and the percentile DataArray must be provided as named arguments.") # Extract the DataArray inputs from the arguments da: DataArray = kwargs.pop(da_key) per_da: DataArray | None = kwargs.pop(per_key, None) if per_da is None: # per may be empty on non doy percentiles - raise KeyError( - "`bootstrap` can only be used with percentiles computed using `percentile_doy`" - ) + raise KeyError("`bootstrap` can only be used with percentiles computed using `percentile_doy`") # Boundary years of reference period clim = per_da.attrs["climatology_bounds"] if xclim.core.utils.uses_dask(da) and len(da.chunks[da.get_axis_num("time")]) > 1: @@ -189,13 +185,10 @@ def bootstrap_func(compute_index_func: Callable, **kwargs) -> xarray.DataArray: # If the group year is in both reference and studied periods, run the bootstrap bda = build_bootstrap_year_da(overlap_da, overlap_years_groups, year_key) if BOOTSTRAP_DIM not in per_template.dims: - per_template = per_template.expand_dims( - {BOOTSTRAP_DIM: np.arange(len(bda._bootstrap))} - ) + per_template = per_template.expand_dims({BOOTSTRAP_DIM: np.arange(len(bda._bootstrap))}) if xclim.core.utils.uses_dask(bda): chunking = { - d: bda.chunks[bda.get_axis_num(d)] - for d in set(bda.dims).intersection(set(per_template.dims)) + d: bda.chunks[bda.get_axis_num(d)] for d in set(bda.dims).intersection(set(per_template.dims)) } per_template = per_template.chunk(chunking) per = xarray.map_blocks( @@ -239,9 +232,7 @@ def _get_year_label(year_dt) -> str: # TODO: Return a generator instead and assess performance -def build_bootstrap_year_da( - da: DataArray, groups: dict[Any, slice], label: Any, dim: str = "time" -) -> DataArray: +def build_bootstrap_year_da(da: DataArray, groups: dict[Any, slice], label: Any, dim: str = "time") -> DataArray: """ Return an array where a group in the original is replaced by every other groups along a new dimension. @@ -281,9 +272,7 @@ def build_bootstrap_year_da( elif len(bloc) == 365: out_view.loc[{dim: bloc}] = source.convert_calendar("noleap").data elif len(bloc) == 366: - out_view.loc[{dim: bloc}] = source.convert_calendar( - "366_day", missing=np.nan - ).data + out_view.loc[{dim: bloc}] = source.convert_calendar("366_day", missing=np.nan).data elif len(bloc) < 365: # 360 days calendar case or anchored years for both source[dim] and bloc case out_view.loc[{dim: bloc}] = source.data[: len(bloc)] diff --git a/src/xclim/core/calendar.py b/src/xclim/core/calendar.py index 6883198e1..f1187602e 100644 --- a/src/xclim/core/calendar.py +++ b/src/xclim/core/calendar.py @@ -230,8 +230,8 @@ def convert_doy( Calendar the doys are in. If not given, uses the "calendar" attribute of `source` or, if absent, the calendar of its `dim` axis. align_on : {'date', 'year'} - If 'year' (default), the doy is seen as a "percentage" of the year and is simply rescaled unto the new doy range. - This always result in floating point data, changing the decimal part of the value. + If 'year' (default), the doy is seen as a "percentage" of the year and is simply rescaled onto + the new doy range. This always result in floating point data, changing the decimal part of the value. If 'date', the doy is seen as a specific date. See notes. This never changes the decimal part of the value. missing : Any If `align_on` is "date" and the new doy doesn't exist in the new calendar, this value is used. @@ -335,9 +335,7 @@ def ensure_cftime_array(time: Sequence) -> np.ndarray | Sequence[cftime.datetime if isinstance(time[0], cftime.datetime): return time if isinstance(time[0], pydt.datetime): - return np.array( - [cftime.DatetimeGregorian(*ele.timetuple()[:6]) for ele in time] - ) + return np.array([cftime.DatetimeGregorian(*ele.timetuple()[:6]) for ele in time]) raise ValueError("Unable to cast array to cftime dtype") @@ -570,9 +568,7 @@ def construct_offset(mult: int, base: str, start_anchored: bool, anchor: str | N start = ("S" if start_anchored else "E") if base in "YAQM" else "" if anchor is None and base in "AQY": anchor = "JAN" if start_anchored else "DEC" - return ( - f"{mult if mult > 1 else ''}{base}{start}{'-' if anchor else ''}{anchor or ''}" - ) + return f"{mult if mult > 1 else ''}{base}{start}{'-' if anchor else ''}{anchor or ''}" def is_offset_divisor(divisor: str, offset: str): @@ -627,9 +623,7 @@ def is_offset_divisor(divisor: str, offset: str): # Simple length comparison is sufficient for submonthly freqs # In case one of bA or bB is > W, we test many to be sure. tA = pd.date_range("1970-01-01T00:00:00", freq=offAs, periods=13) - return bool( - np.all((np.diff(tB)[:, np.newaxis] / np.diff(tA)[np.newaxis, :]) % 1 == 0) - ) + return bool(np.all((np.diff(tB)[:, np.newaxis] / np.diff(tA)[np.newaxis, :]) % 1 == 0)) # else, we test alignment with some real dates # If both fall on offAs, then is means divisor is aligned with offset at those dates @@ -638,9 +632,7 @@ def is_offset_divisor(divisor: str, offset: str): return all(offAs.is_on_offset(d) for d in tB) -def _interpolate_doy_calendar( - source: xr.DataArray, doy_max: int, doy_min: int = 1 -) -> xr.DataArray: +def _interpolate_doy_calendar(source: xr.DataArray, doy_max: int, doy_min: int = 1) -> xr.DataArray: """ Interpolate from one set of dayofyear range to another. @@ -674,16 +666,12 @@ def _interpolate_doy_calendar( filled_na = da.interpolate_na(dim="dayofyear") # Interpolate to target dayofyear range - filled_na.coords["dayofyear"] = np.linspace( - start=doy_min, stop=doy_max, num=len(filled_na.coords["dayofyear"]) - ) + filled_na.coords["dayofyear"] = np.linspace(start=doy_min, stop=doy_max, num=len(filled_na.coords["dayofyear"])) return filled_na.interp(dayofyear=range(doy_min, doy_max + 1)) -def adjust_doy_calendar( - source: xr.DataArray, target: xr.DataArray | xr.Dataset -) -> xr.DataArray: +def adjust_doy_calendar(source: xr.DataArray, target: xr.DataArray | xr.Dataset) -> xr.DataArray: """ Interpolate from one set of dayofyear range to another calendar. @@ -708,18 +696,11 @@ def has_same_calendar(_source, _target): # numpydoc ignore=GL08 # case of full year (doys between 1 and 360|365|366) return _source.dayofyear.max() == max_doy[get_calendar(_target)] - def has_similar_doys( - _source, _min_target_doy, _max_target_doy - ): # numpydoc ignore=GL08 + def has_similar_doys(_source, _min_target_doy, _max_target_doy): # numpydoc ignore=GL08 # case of partial year (e.g. JJA, doys between 152|153 and 243|244) - return ( - _source.dayofyear.min == _min_target_doy - and _source.dayofyear.max == _max_target_doy - ) + return _source.dayofyear.min == _min_target_doy and _source.dayofyear.max == _max_target_doy - if has_same_calendar(source, target) or has_similar_doys( - source, min_target_doy, max_target_doy - ): + if has_same_calendar(source, target) or has_similar_doys(source, min_target_doy, max_target_doy): return source return _interpolate_doy_calendar(source, max_target_doy, min_target_doy) @@ -755,14 +736,7 @@ def resample_doy(doy: xr.DataArray, arr: xr.DataArray | xr.Dataset) -> xr.DataAr def time_bnds( # noqa: C901 - time: ( - xr.DataArray - | xr.Dataset - | CFTimeIndex - | pd.DatetimeIndex - | DataArrayResample - | DatasetResample - ), + time: (xr.DataArray | xr.Dataset | CFTimeIndex | pd.DatetimeIndex | DataArrayResample | DatasetResample), freq: str | None = None, precision: str | None = None, ): @@ -816,9 +790,7 @@ def time_bnds( # noqa: C901 break else: - raise ValueError( - 'Got object resampled along another dimension than "time".' - ) + raise ValueError('Got object resampled along another dimension than "time".') if freq is None and hasattr(time, "freq"): freq = time.freq @@ -873,14 +845,10 @@ def shift_time(t): # numpydoc ignore=GL08 cls([t - period + day for t in time_real]), cls([t + day - eps for t in time_real]), ] - return xr.DataArray( - tbnds, dims=("bnds", "time"), coords={"time": time}, name="time_bnds" - ).transpose() + return xr.DataArray(tbnds, dims=("bnds", "time"), coords={"time": time}, name="time_bnds").transpose() -def climatological_mean_doy( - arr: xr.DataArray, window: int = 5 -) -> tuple[xr.DataArray, xr.DataArray]: +def climatological_mean_doy(arr: xr.DataArray, window: int = 5) -> tuple[xr.DataArray, xr.DataArray]: """ Calculate the climatological mean and standard deviation for each day of the year. @@ -907,9 +875,7 @@ def climatological_mean_doy( return m, s -def within_bnds_doy( - arr: xr.DataArray, *, low: xr.DataArray, high: xr.DataArray -) -> xr.DataArray: +def within_bnds_doy(arr: xr.DataArray, *, low: xr.DataArray, high: xr.DataArray) -> xr.DataArray: """ Return whether array values are within bounds for each day of the year. @@ -1110,9 +1076,7 @@ def days_since_to_doy( out = dac + start_doy out = xr.where(out > doy_max, out - doy_max, out) - out.attrs.update( - {k: v for k, v in da.attrs.items() if k not in ["units", "calendar"]} - ) + out.attrs.update({k: v for k, v in da.attrs.items() if k not in ["units", "calendar"]}) out.attrs.update(calendar=calendar, is_dayofyear=1) return out.convert_calendar(base_calendar).rename(da.name) @@ -1162,11 +1126,12 @@ def mask_between_doys( The bounds as (start, end) of the period of interest expressed in day-of-year, integers going from 1 (January 1st) to 365 or 366 (December 31st). If DataArrays are passed, they must have the same coordinates on the dimensions they share. - They may have a time dimension, in which case the masking is done independently for each period defined by the coordinate, - which means the time coordinate must have an inferable frequency (see :py:func:`xr.infer_freq`). - Timesteps of the input not appearing in the time coordinate of the bounds are masked as "outside the bounds". - Missing values (nan) in the start and end bounds default to 1 and 366 respectively in the non-temporal case - and to open bounds (the start and end of the period) in the temporal case. + They may have a time dimension, in which case the masking is done independently for each period + defined by the coordinate, which means the time coordinate must have an inferable frequency + (see :py:func:`xr.infer_freq`). Timesteps of the input not appearing in the time coordinate of the + bounds are masked as "outside the bounds". Missing values (nan) in the start and end bounds default + to 1 and 366 respectively in the non-temporal case and to open bounds (the start and end of the period) + in the temporal case. include_bounds : 2-tuple of booleans Whether the bounds of `doy_bounds` should be inclusive or not. @@ -1196,12 +1161,11 @@ def mask_between_doys( if "time" in start.dims: freq = xr.infer_freq(start.time) - # Convert the doy bounds to a duration since the beginning of each period defined in the bound's time coordinate - # Also ensures the bounds share the sime time calendar as the input - # Any missing value is replaced with the min/max of possible values - calkws = dict( - calendar=da.time.dt.calendar, use_cftime=(da.time.dtype == "O") - ) + # Convert the doy bounds to a duration since the beginning of each period defined + # in the bound's time coordinate. + # Also ensures the bounds share the same time calendar as the input. + # Any missing value is replaced with the min/max of possible values. + calkws = dict(calendar=da.time.dt.calendar, use_cftime=(da.time.dtype == "O")) start = doy_to_days_since(start.convert_calendar(**calkws)).fillna(0) end = doy_to_days_since(end.convert_calendar(**calkws)).fillna(366) @@ -1220,9 +1184,7 @@ def mask_between_doys( mask = (days >= start_d) & (days <= end_d) else: # This group has no defined bounds : put False in the mask # Array with the same shape as the "mask" in the other case : broadcast of time and bounds dims - template = xr.broadcast( - group.time.dt.day, start.isel(time=0, drop=True) - )[0] + template = xr.broadcast(group.time.dt.day, start.isel(time=0, drop=True))[0] mask = xr.full_like(template, False, dtype="bool") out.append(mask) mask = xr.concat(out, dim="time") @@ -1327,14 +1289,9 @@ def select_time( mask = da.time.dt.month.isin(month) elif doy_bounds is not None: - if ( - not (isinstance(doy_bounds[0], int) and isinstance(doy_bounds[1], int)) - and drop - ): + if not (isinstance(doy_bounds[0], int) and isinstance(doy_bounds[1], int)) and drop: # At least one of those is an array, this drop won't work - raise ValueError( - "Passing array-like doy bounds is incompatible with drop=True." - ) + raise ValueError("Passing array-like doy bounds is incompatible with drop=True.") mask = mask_between_doys(da, doy_bounds, include_bounds) elif date_bounds is not None: @@ -1361,9 +1318,7 @@ def select_time( mask["time"] = da.time else: - raise ValueError( - "Must provide either `season`, `month`, `doy_bounds` or `date_bounds`." - ) + raise ValueError("Must provide either `season`, `month`, `doy_bounds` or `date_bounds`.") return da.where(mask, drop=drop) @@ -1416,14 +1371,14 @@ def stack_periods( The length of the moving window as a multiple of ``freq``. stride : int, optional At which interval to take the windows, as a multiple of ``freq``. - For the operation to be reversible with :py:func:`unstack_periods`, it must divide `window` into an odd number of parts. - Default is `window` (no overlap between periods). + For the operation to be reversible with :py:func:`unstack_periods`, it must divide `window` into an + odd number of parts. Default is `window` (no overlap between periods). min_length : int, optional Windows shorter than this are not included in the output. Given as a multiple of ``freq``. Default is ``window`` (every window must be complete). Similar to the ``min_periods`` argument of ``da.rolling``. - If ``freq`` is annual or quarterly and ``min_length == ``window``, the first period is considered complete - if the first timestep is in the first month of the period. + If ``freq`` is annual or quarterly and ``min_length == ``window``, + the first period is considered complete if the first timestep is in the first month of the period. freq : str Units of ``window``, ``stride`` and ``min_length``, as a frequency string. Must be larger or equal to the data's sampling frequency. @@ -1451,10 +1406,10 @@ def stack_periods( A DataArray with a new `period` dimension and a `time` dimension with the length of the longest window. The new time coordinate has the same frequency as the input data but is generated using :py:func:`xarray.date_range` with the given `start` value. - That coordinate is the same for all periods, depending on the choice of ``window`` and ``freq``, it might make sense. - But for unequal periods or non-uniform calendars, it will certainly not. - If ``stride`` is a divisor of ``window``, the correct timeseries can be reconstructed with :py:func:`unstack_periods`. - The coordinate of `period` is the first timestep of each window. + That coordinate is the same for all periods, depending on the choice of ``window`` and ``freq``, + it might make sense. But for unequal periods or non-uniform calendars, it will certainly not. + If ``stride`` is a divisor of ``window``, the correct timeseries can be reconstructed with + :py:func:`unstack_periods`. The coordinate of `period` is the first timestep of each window. """ # Import in function to avoid cyclical imports from xclim.core.units import ( # pylint: disable=import-outside-toplevel @@ -1465,9 +1420,7 @@ def stack_periods( stride = stride or window min_length = min_length or window if stride > window: - raise ValueError( - f"Stride must be less than or equal to window. Got {stride} > {window}." - ) + raise ValueError(f"Stride must be less than or equal to window. Got {stride} > {window}.") srcfreq = xr.infer_freq(da.time) cal = da.time.dt.calendar @@ -1520,9 +1473,7 @@ def stack_periods( win_slc = list(win_resamp.groups.values())[0] if min_length < window: # If we ask for a min_length period instead is it complete ? - min_resamp = time2.isel(time=slice(strd_slc.start, None)).resample( - time=minl_frq - ) + min_resamp = time2.isel(time=slice(strd_slc.start, None)).resample(time=minl_frq) min_slc = list(min_resamp.groups.values())[0] open_ended = min_slc.stop is None else: @@ -1545,11 +1496,7 @@ def stack_periods( periods.append( slice( strd_slc.start + win_slc.start, - ( - (strd_slc.start + win_slc.stop) - if win_slc.stop is not None - else da.time.size - ), + ((strd_slc.start + win_slc.stop) if win_slc.stop is not None else da.time.size), ) ) @@ -1578,17 +1525,10 @@ def stack_periods( }, ) # The "fake" axis that all periods share - fake_time = xr.date_range( - start, periods=longest, freq=srcfreq, calendar=cal, use_cftime=use_cftime - ) + fake_time = xr.date_range(start, periods=longest, freq=srcfreq, calendar=cal, use_cftime=use_cftime) # Slice and concat along new dim. We drop the index and add a new one so that xarray can concat them together. out = xr.concat( - [ - da.isel(time=slc) - .drop_vars("time") - .assign_coords(time=np.arange(slc.stop - slc.start)) - for slc in periods - ], + [da.isel(time=slc).drop_vars("time").assign_coords(time=np.arange(slc.stop - slc.start)) for slc in periods], dim, join="outer", fill_value=pad_value, @@ -1601,9 +1541,7 @@ def stack_periods( return out -def unstack_periods( - da: xr.DataArray | xr.Dataset, dim: str = "period" -) -> xr.DataArray | xr.Dataset: +def unstack_periods(da: xr.DataArray | xr.Dataset, dim: str = "period") -> xr.DataArray | xr.Dataset: """ Unstack an array constructed with :py:func:`stack_periods`. @@ -1661,9 +1599,7 @@ def unstack_periods( try: lengths = da[f"{dim}_length"] except KeyError as err: - raise ValueError( - f"`unstack_periods` can't find the `{dim}_length` coordinate." - ) from err + raise ValueError(f"`unstack_periods` can't find the `{dim}_length` coordinate.") from err # Get length as number of points m, _ = infer_sampling_units(da.time) lengths = lengths // m @@ -1675,9 +1611,7 @@ def unstack_periods( time_as_delta = da.time - da.time[0] if da.time.dtype == "O": # cftime can't add with np.timedelta64 (restriction comes from numpy which refuses to add O with m8) - time_as_delta = pd.TimedeltaIndex( - time_as_delta - ).to_pytimedelta() # this array is O, numpy complies + time_as_delta = pd.TimedeltaIndex(time_as_delta).to_pytimedelta() # this array is O, numpy complies else: # Xarray will return int when iterating over datetime values, this returns timestamps starts = pd.DatetimeIndex(starts) @@ -1690,9 +1624,7 @@ def _reconstruct_time(_time_as_delta, _start): if window == stride: # just concat them all periods = [] - for i, (start, length) in enumerate( - zip(starts.values, lengths.values, strict=False) - ): + for i, (start, length) in enumerate(zip(starts.values, lengths.values, strict=False)): real_time = _reconstruct_time(time_as_delta, start) periods.append( da.isel(**{dim: i}, drop=True) @@ -1716,9 +1648,7 @@ def _reconstruct_time(_time_as_delta, _start): strd_frq = construct_offset(mult * stride, *args) periods = [] - for i, (start, length) in enumerate( - zip(starts.values, lengths.values, strict=False) - ): + for i, (start, length) in enumerate(zip(starts.values, lengths.values, strict=False)): real_time = _reconstruct_time(time_as_delta, start) slices = list(real_time.resample(time=strd_frq).groups.values()) if i == 0: @@ -1727,10 +1657,6 @@ def _reconstruct_time(_time_as_delta, _start): slc = slice(slices[mid].start, min(slices[Nwin - 1].stop or length, length)) else: slc = slice(slices[mid].start, min(slices[mid].stop, length)) - periods.append( - da.isel(**{dim: i}, drop=True) - .isel(time=slc) - .assign_coords(time=real_time.isel(time=slc)) - ) + periods.append(da.isel(**{dim: i}, drop=True).isel(time=slc).assign_coords(time=real_time.isel(time=slc))) return xr.concat(periods, "time") diff --git a/src/xclim/core/cfchecks.py b/src/xclim/core/cfchecks.py index 8e648fe98..e7707c1a9 100644 --- a/src/xclim/core/cfchecks.py +++ b/src/xclim/core/cfchecks.py @@ -51,9 +51,7 @@ def check_valid(var: xr.DataArray, key: str, expected: str | Sequence[str]): ) -def cfcheck_from_name( - varname: str, vardata: xr.DataArray, attrs: list[str] | None = None -): +def cfcheck_from_name(varname: str, vardata: xr.DataArray, attrs: list[str] | None = None): """ Perform cfchecks on a DataArray using specifications from xclim's default variables. @@ -76,9 +74,7 @@ def cfcheck_from_name( data = VARIABLES[varname] if "cell_methods" in data and "cell_methods" in attrs: - _check_cell_methods( - getattr(vardata, "cell_methods", None), data["cell_methods"] - ) + _check_cell_methods(getattr(vardata, "cell_methods", None), data["cell_methods"]) if "standard_name" in data and "standard_name" in attrs: check_valid(vardata, "standard_name", data["standard_name"]) diff --git a/src/xclim/core/datachecks.py b/src/xclim/core/datachecks.py index dd9316a06..c707c8500 100644 --- a/src/xclim/core/datachecks.py +++ b/src/xclim/core/datachecks.py @@ -17,9 +17,7 @@ @datacheck -def check_freq( - var: xr.DataArray, freq: str | Sequence[str], strict: bool = True -) -> None: +def check_freq(var: xr.DataArray, freq: str | Sequence[str], strict: bool = True) -> None: """ Raise an error if not series has not the expected temporal frequency or is not monotonically increasing. @@ -28,8 +26,9 @@ def check_freq( var : xr.DataArray Input array. freq : str or sequence of str - The expected temporal frequencies, using Pandas frequency terminology ({'Y', 'M', 'D', 'h', 'min', 's', 'ms', 'us'}) - and multiples thereof. To test strictly for 'W', pass '7D' with `strict=True`. + The expected temporal frequencies, using Pandas frequency terminology + (e.g. {'Y', 'M', 'D', 'h', 'min', 's', 'ms', 'us'}) and multiples thereof. + To test strictly for 'W', pass '7D' with `strict=True`. This ignores the start/end flag and the anchor (ex: 'YS-JUL' will validate against 'Y'). strict : bool Whether multiples of the frequencies are considered invalid or not. With `strict` set to False, a '3h' series @@ -47,13 +46,10 @@ def check_freq( v_freq = xr.infer_freq(var.time) if v_freq is None: raise ValidationError( - "Unable to infer the frequency of the time series. " - "To mute this, set xclim's option data_validation='log'." + "Unable to infer the frequency of the time series. To mute this, set xclim's option data_validation='log'." ) v_base = parse_offset(v_freq)[1] - if v_base not in exp_base or ( - strict and all(compare_offsets(v_freq, "!=", frq) for frq in freq) - ): + if v_base not in exp_base or (strict and all(compare_offsets(v_freq, "!=", frq) for frq in freq)): raise ValidationError( f"Frequency of time series not {'strictly' if strict else ''} in {freq}. " "To mute this, set xclim's option data_validation='log'." @@ -97,13 +93,11 @@ def check_common_time(inputs: Sequence[xr.DataArray]) -> None: freqs = [xr.infer_freq(da.time) for da in inputs] if None in freqs: raise ValidationError( - "Unable to infer the frequency of the time series. " - "To mute this, set xclim's option data_validation='log'." + "Unable to infer the frequency of the time series. To mute this, set xclim's option data_validation='log'." ) if len(set(freqs)) != 1: raise ValidationError( - f"Inputs have different frequencies. Got : {freqs}." - "To mute this, set xclim's option data_validation='log'." + f"Inputs have different frequencies. Got : {freqs}.To mute this, set xclim's option data_validation='log'." ) # Check if anchor is the same @@ -114,7 +108,7 @@ def check_common_time(inputs: Sequence[xr.DataArray]) -> None: outs = {da.indexes["time"][0].strftime(fmt[base]) for da in inputs} if len(outs) > 1: raise ValidationError( - f"All inputs have the same frequency ({freq}), but they are not anchored on the same minutes (got {outs}). " - f"xarray's alignment would silently fail. You can try to fix this with `da.resample('{freq}').mean()`." - "To mute this, set xclim's option data_validation='log'." + f"All inputs have the same frequency ({freq}), but they are not anchored on the same " + f"minutes (got {outs}). xarray's alignment would silently fail. You can try to fix this " + f"with `da.resample('{freq}').mean()`. To mute this, set xclim's option data_validation='log'." ) diff --git a/src/xclim/core/dataflags.py b/src/xclim/core/dataflags.py index 54bcba821..fa287994c 100644 --- a/src/xclim/core/dataflags.py +++ b/src/xclim/core/dataflags.py @@ -194,9 +194,7 @@ def tas_exceeds_tasmax( @register_methods() @update_xclim_history @declare_units(tas="[temperature]", tasmin="[temperature]") -def tas_below_tasmin( - tas: xarray.DataArray, tasmin: xarray.DataArray -) -> xarray.DataArray: +def tas_below_tasmin(tas: xarray.DataArray, tasmin: xarray.DataArray) -> xarray.DataArray: """ Check if tas values are below tasmin values for any given day. @@ -230,9 +228,7 @@ def tas_below_tasmin( @register_methods() @update_xclim_history @declare_units(da="[temperature]", thresh="[temperature]") -def temperature_extremely_low( - da: xarray.DataArray, *, thresh: Quantified = "-90 degC" -) -> xarray.DataArray: +def temperature_extremely_low(da: xarray.DataArray, *, thresh: Quantified = "-90 degC") -> xarray.DataArray: """ Check if temperatures values are below -90 degrees Celsius for any given day. @@ -269,9 +265,7 @@ def temperature_extremely_low( @register_methods() @update_xclim_history @declare_units(da="[temperature]", thresh="[temperature]") -def temperature_extremely_high( - da: xarray.DataArray, *, thresh: Quantified = "60 degC" -) -> xarray.DataArray: +def temperature_extremely_high(da: xarray.DataArray, *, thresh: Quantified = "60 degC") -> xarray.DataArray: """ Check if temperatures values exceed 60 degrees Celsius for any given day. @@ -280,7 +274,8 @@ def temperature_extremely_high( da : xarray.DataArray Temperature. thresh : str - Threshold above which temperatures are considered problematic and a flag is raised. Default is 60 degrees Celsius. + Threshold above which temperatures are considered problematic and a flag is raised. + Default is 60 degrees Celsius. Returns ------- @@ -340,9 +335,7 @@ def negative_accumulation_values( @register_methods() @update_xclim_history @declare_units(da="[precipitation]", thresh="[precipitation]") -def very_large_precipitation_events( - da: xarray.DataArray, *, thresh: Quantified = "300 mm d-1" -) -> xarray.DataArray: +def very_large_precipitation_events(da: xarray.DataArray, *, thresh: Quantified = "300 mm d-1") -> xarray.DataArray: """ Check if precipitation values exceed 300 mm/day for any given day. @@ -408,18 +401,12 @@ def values_op_thresh_repeating_for_n_or_more_days( >>> units = "5 mm d-1" >>> days = 5 >>> comparison = "eq" - >>> flagged = values_op_thresh_repeating_for_n_or_more_days( - ... ds.pr, n=days, thresh=units, op=comparison - ... ) + >>> flagged = values_op_thresh_repeating_for_n_or_more_days(ds.pr, n=days, thresh=units, op=comparison) """ - thresh = convert_units_to( - thresh, da, context=infer_context(standard_name=da.attrs.get("standard_name")) - ) + thresh = convert_units_to(thresh, da, context=infer_context(standard_name=da.attrs.get("standard_name"))) repetitions = _sanitize_attrs(suspicious_run(da, window=n, op=op, thresh=thresh)) - description = ( - f"Repetitive values at {thresh} for at least {n} days found for {da.name}." - ) + description = f"Repetitive values at {thresh} for at least {n} days found for {da.name}." repetitions.attrs["description"] = description repetitions.attrs["units"] = "" return repetitions @@ -457,9 +444,7 @@ def wind_values_outside_of_bounds( >>> from xclim.core.dataflags import wind_values_outside_of_bounds >>> ceiling, floor = "46 m s-1", "0 m s-1" - >>> flagged = wind_values_outside_of_bounds( - ... sfcWind_dataset, upper=ceiling, lower=floor - ... ) + >>> flagged = wind_values_outside_of_bounds(sfcWind_dataset, upper=ceiling, lower=floor) """ lower, upper = convert_units_to(lower, da), convert_units_to(upper, da) unbounded_percentages = _sanitize_attrs((da < lower) | (da > upper)) @@ -499,7 +484,8 @@ def outside_n_standard_deviations_of_climatology( Notes ----- - A moving window of five (5) days is suggested for `tas` data flag calculations according to ICCLIM data quality standards. + A moving window of five (5) days is suggested for `tas` data flag calculations according to + ICCLIM data quality standards. References ---------- @@ -513,14 +499,10 @@ def outside_n_standard_deviations_of_climatology( >>> ds = xr.open_dataset(path_to_tas_file) >>> std_devs = 5 >>> average_over = 5 - >>> flagged = outside_n_standard_deviations_of_climatology( - ... ds.tas, n=std_devs, window=average_over - ... ) + >>> flagged = outside_n_standard_deviations_of_climatology(ds.tas, n=std_devs, window=average_over) """ mu, sig = climatological_mean_doy(da, window=window) - within_bounds = _sanitize_attrs( - within_bnds_doy(da, high=(mu + n * sig), low=(mu - n * sig)) - ) + within_bounds = _sanitize_attrs(within_bnds_doy(da, high=(mu + n * sig), low=(mu - n * sig))) description = ( f"Values outside of {n} standard deviations from climatology found for {da.name} " f"with moving window of {window} days." @@ -532,9 +514,7 @@ def outside_n_standard_deviations_of_climatology( @register_methods("values_repeating_for_{n}_or_more_days") @update_xclim_history -def values_repeating_for_n_or_more_days( - da: xarray.DataArray, *, n: int -) -> xarray.DataArray: +def values_repeating_for_n_or_more_days(da: xarray.DataArray, *, n: int) -> xarray.DataArray: """ Check if exact values are found to be repeating for at least 5 or more days. @@ -704,9 +684,7 @@ def _missing_vars(function, dataset: xarray.Dataset, var_provided: str): # Thus, a single dimension name, we allow this option to mirror xarray. dims = {dims} if freq is not None and dims is not None: - dims = ( - set(dims) - {"time"} - ) or None # Will return None if the only dimension was "time". + dims = (set(dims) - {"time"}) or None # Will return None if the only dimension was "time". if flags is None: try: @@ -799,10 +777,7 @@ def ecad_compliant( for flag_name, flag_data in df.data_vars.items(): flags = flags.assign({f"{var}_{flag_name}": flag_data}) - if ( - "history" in flag_data.attrs.keys() - and np.all(flag_data.values) is not None - ): + if "history" in flag_data.attrs.keys() and np.all(flag_data.values) is not None: # The extra `split("\n") should be removed when merge_attributes(missing_str=None) history_elems = flag_data.attrs["history"].split("\n")[-1].split(" ") if not history: diff --git a/src/xclim/core/formatting.py b/src/xclim/core/formatting.py index de09f3e15..bd1aafafe 100644 --- a/src/xclim/core/formatting.py +++ b/src/xclim/core/formatting.py @@ -160,12 +160,8 @@ def format_field(self, value, format_spec: str) -> str: """ baseval = self._match_value(value) if baseval is None: # Not something we know how to translate - if format_spec in self.modifiers + [ - "r" - ]: # Woops, however a known format spec was asked - warnings.warn( - f"Requested formatting `{format_spec}` for unknown string `{value}`." - ) + if format_spec in self.modifiers + ["r"]: # Woops, however a known format spec was asked + warnings.warn(f"Requested formatting `{format_spec}` for unknown string `{value}`.") format_spec = "" return super().format_field(value, format_spec) # Thus, known value @@ -384,9 +380,7 @@ def merge_attributes( if attribute in in_ds.attrs or missing_str is not None: if in_name is not None and len(inputs) > 1: merged_attr += f"{in_name}: " - merged_attr += in_ds.attrs.get( - attribute, "" if in_name is None else missing_str - ) + merged_attr += in_ds.attrs.get(attribute, "" if in_name is None else missing_str) merged_attr += new_line if len(new_line) > 0: @@ -441,8 +435,7 @@ def update_history( if len(merged_history) > 0 and not merged_history.endswith("\n"): merged_history += "\n" merged_history += ( - f"[{dt.datetime.now():%Y-%m-%d %H:%M:%S}] {new_name or ''}: " - f"{hist_str} - xclim version: {__version__}" + f"[{dt.datetime.now():%Y-%m-%d %H:%M:%S}] {new_name or ''}: {hist_str} - xclim version: {__version__}" ) return merged_history @@ -477,14 +470,10 @@ def _call_and_add_history(*args, **kwargs): out = outs if not isinstance(out, xr.DataArray | xr.Dataset): - raise TypeError( - f"Decorated `update_xclim_history` received a non-xarray output from {func.__name__}." - ) + raise TypeError(f"Decorated `update_xclim_history` received a non-xarray output from {func.__name__}.") da_list = [arg for arg in args if isinstance(arg, xr.DataArray)] - da_dict = { - name: arg for name, arg in kwargs.items() if isinstance(arg, xr.DataArray) - } + da_dict = {name: arg for name, arg in kwargs.items() if isinstance(arg, xr.DataArray)} # The wrapper hides how the user passed the arguments (positional or keyword) # Instead of having it all position, we have it all keyword-like for explicitness. @@ -625,9 +614,7 @@ def unprefix_attrs(source: dict, keys: Sequence, prefix: str) -> dict: } -def _gen_parameters_section( - parameters: dict[str, dict[str, Any]], allowed_periods: list[str] | None = None -) -> str: +def _gen_parameters_section(parameters: dict[str, dict[str, Any]], allowed_periods: list[str] | None = None) -> str: """ Generate the "parameters" section of the indicator docstring. @@ -652,9 +639,7 @@ def _gen_parameters_section( "-aliases for available options." ) if allowed_periods is not None: - desc_str += ( - f" Restricted to frequencies equivalent to one of {allowed_periods}" - ) + desc_str += f" Restricted to frequencies equivalent to one of {allowed_periods}" if param.kind == InputKind.VARIABLE: defstr = f"Default : `ds.{param.default}`. " elif param.kind == InputKind.OPTIONAL_VARIABLE: @@ -735,16 +720,12 @@ def generate_indicator_docstring(ind) -> str: special += f"Based on indice :py:func:`~{ind.compute.__module__}.{ind.compute.__name__}`.\n" if ind.injected_parameters: special += "With injected parameters: " - special += ", ".join( - [f"{k}={v}" for k, v in ind.injected_parameters.items()] - ) + special += ", ".join([f"{k}={v}" for k, v in ind.injected_parameters.items()]) special += ".\n" if ind.keywords: special += f"Keywords : {ind.keywords}.\n" - parameters = _gen_parameters_section( - ind.parameters, getattr(ind, "allowed_periods", None) - ) + parameters = _gen_parameters_section(ind.parameters, getattr(ind, "allowed_periods", None)) returns = _gen_returns_section(ind.cf_attrs) diff --git a/src/xclim/core/indicator.py b/src/xclim/core/indicator.py index 365fa1983..8e0568232 100644 --- a/src/xclim/core/indicator.py +++ b/src/xclim/core/indicator.py @@ -62,7 +62,8 @@ allowed_periods: [, , , ] # Compute function - compute: # Referring to a function in `Indices` module (xclim.indices.generic or xclim.indices) + compute: # Referring to a function in `Indices` module + # (xclim.indices.generic or xclim.indices) input: # When "compute" is a generic function, this is a mapping from argument name to the expected variable. # This will allow the input units and CF metadata checks to run on the inputs. # Can also be used to modify the expected variable, as long as it has the same dimensionality @@ -77,8 +78,8 @@ default : description: name : # Change the name of the parameter (similar to what `input` does for variables) - kind: # Override the parameter kind. - # This is mostly useful for transforming an optional variable into a required one by passing ``kind: 0``. + kind: # Override the parameter kind. This is mostly useful for transforming an + # optional variable into a required one by passing ``kind: 0``. ... ... # and so on. @@ -86,7 +87,8 @@ In the following, the section under `` is referred to as `data`. When creating indicators from a dictionary, with :py:meth:`Indicator.from_dict`, the input dict must follow the same structure of `data`. -Note that kwargs-like parameters like ``indexer`` must be injected as a dictionary (``param data`` above should be a dictionary). +Note that kwargs-like parameters like ``indexer`` must be injected as a dictionary +(``param data`` above should be a dictionary). When a module is built from a yaml file, the yaml is first validated against the schema (see xclim/data/schema.yml) using the YAMALE library (:cite:p:`lopker_yamale_2022`). See the "Extending xclim" notebook for more info. @@ -239,9 +241,7 @@ def is_parameter_dict(cls, other: dict) -> bool: """ # Passing compute_name is forbidden. # name is valid, but is handled by the indicator - return set(other.keys()).issubset( - {"kind", "default", "description", "units", "choices", "value", "name"} - ) + return set(other.keys()).issubset({"kind", "default", "description", "units", "choices", "value", "name"}) def __contains__(self, key) -> bool: """Imitate previous behaviour where "units" and "choices" were missing, instead of being "_empty".""" @@ -286,9 +286,7 @@ def __new__(cls): else: name = f"{module}.{name}" if name in registry: - warnings.warn( - f"Class {name} already exists and will be overwritten.", stacklevel=1 - ) + warnings.warn(f"Class {name} already exists and will be overwritten.", stacklevel=1) registry[name] = cls cls._registry_id = name return super().__new__(cls) @@ -350,26 +348,30 @@ class Indicator(IndicatorRegistrar): Parameters ---------- identifier : str - Unique ID for class registry, should be a valid slug. + Unique ID for class registry. Should be a valid slug. realm : {'atmos', 'seaIce', 'land', 'ocean'} - General domain of validity of the indicator. Indicators created outside ``xclim.indicators`` must set this attribute. + General domain of validity of the indicator. + Indicators created outside ``xclim.indicators`` must set this attribute. compute : func The function computing the indicators. It should return one or more DataArray. cf_attrs : list of dicts Attributes to be formatted and added to the computation's output. See :py:attr:`xclim.core.indicator.Indicator.cf_attrs`. title : str - A succinct description of what is in the computed outputs. Parsed from `compute` docstring if None (first paragraph). + A succinct description of what is in the computed outputs. + Parsed from `compute` docstring if None (first paragraph). abstract : str - A long description of what is in the computed outputs. Parsed from `compute` docstring if None (second paragraph). + A long description of what is in the computed outputs. + Parsed from `compute` docstring if None (second paragraph). keywords : str - Comma separated list of keywords. Parsed from `compute` docstring if None (from a "Keywords" section). + Comma separated list of keywords. + Parsed from `compute` docstring if None (from a "Keywords" section). references : str - Published or web-based references that describe the data or methods used to produce it. Parsed from - `compute` docstring if None (from the "References" section). + Published or web-based references that describe the data or methods used to produce it. + Parsed from `compute` docstring if None (from the "References" section). notes : str - Notes regarding computing function, for example the mathematical formulation. Parsed from `compute` - docstring if None (form the "Notes" section). + Notes regarding computing function, for example the mathematical formulation. + Parsed from `compute` docstring if None (form the "Notes" section). src_freq : str, sequence of strings, optional The expected frequency of the input data. Can be a list for multiple frequencies, or None if irrelevant. context : str @@ -460,9 +462,7 @@ def __new__(cls, **kwds): # noqa: C901 if "compute" in kwds: # Parsed parameters and metadata override parent's params entirely. - parameters, docmeta = cls._parse_indice( - kwds["compute"], kwds.get("parameters", {}) - ) + parameters, docmeta = cls._parse_indice(kwds["compute"], kwds.get("parameters", {})) for name, value in docmeta.items(): # title, abstract, references, notes, long_name kwds.setdefault(name, value) @@ -607,7 +607,8 @@ def _update_parameters(cls, parameters, passed): new_key = val.pop("name") if new_key in parameters: raise ValueError( - f"Cannot rename a parameter or variable with the same name as another parameter. '{new_key}' is already a parameter." + "Cannot rename a parameter or variable with the same name as another parameter. " + f"'{new_key}' is already a parameter." ) parameters[new_key] = parameters.pop(key) key = new_key @@ -706,9 +707,7 @@ def _parse_output_attrs( # noqa: C901 # a single string or callable, same for all outputs values = [values] * n_outs elif len(values) != n_outs: # A sequence of the wrong length. - raise ValueError( - f"Attribute {name} has {len(values)} elements but xclim expected {n_outs}." - ) + raise ValueError(f"Attribute {name} has {len(values)} elements but xclim expected {n_outs}.") for attrs, value in zip(cf_attrs, values, strict=False): if value: # Skip the empty ones (None or "") attrs[name] = value @@ -783,14 +782,9 @@ def from_dict( # data.compute refers to a function in xclim.indices.generic or xclim.indices (in this order of priority). # It can also directly be a function (like if a module was passed to build_indicator_module_from_yaml) if isinstance(compute, str): - compute_func = getattr( - indices.generic, compute, getattr(indices, compute, None) - ) + compute_func = getattr(indices.generic, compute, getattr(indices, compute, None)) if compute_func is None: - raise ImportError( - f"Indice function {compute} not found in xclim.indices or " - "xclim.indices.generic." - ) + raise ImportError(f"Indice function {compute} not found in xclim.indices or xclim.indices.generic.") data["compute"] = compute_func return cls(identifier=identifier, module=module, **data) @@ -866,12 +860,9 @@ def __call__(self, *args, **kwds): das, params, dsattrs = self._parse_variables_from_call(args, kwds) if OPTIONS[KEEP_ATTRS] is True or ( - OPTIONS[KEEP_ATTRS] == "xarray" - and xarray.core.options._get_keep_attrs(False) + OPTIONS[KEEP_ATTRS] == "xarray" and xarray.core.options._get_keep_attrs(False) ): - out_attrs = xarray.core.merge.merge_attrs( - [da.attrs for da in das.values()], "drop_conflicts" - ) + out_attrs = xarray.core.merge.merge_attrs([da.attrs for da in das.values()], "drop_conflicts") out_attrs.pop("units", None) else: out_attrs = {} @@ -889,8 +880,7 @@ def __call__(self, *args, **kwds): if len(outs) != self.n_outs: raise ValueError( - f"Indicator {self.identifier} was wrongly defined. Expected " - f"{self.n_outs} outputs, got {len(outs)}." + f"Indicator {self.identifier} was wrongly defined. Expected {self.n_outs} outputs, got {len(outs)}." ) # Metadata attributes from templates @@ -913,10 +903,7 @@ def __call__(self, *args, **kwds): ) # Convert to output units - outs = [ - convert_units_to(out, attrs, self.context) - for out, attrs in zip(outs, out_attrs, strict=False) - ] + outs = [convert_units_to(out, attrs, self.context) for out, attrs in zip(outs, out_attrs, strict=False)] outs = self._postprocess(outs, das, params) @@ -929,8 +916,7 @@ def __call__(self, *args, **kwds): if OPTIONS[AS_DATASET]: out = Dataset({o.name: o for o in outs}) if OPTIONS[KEEP_ATTRS] is True or ( - OPTIONS[KEEP_ATTRS] == "xarray" - and xarray.core.options._get_keep_attrs(False) + OPTIONS[KEEP_ATTRS] == "xarray" and xarray.core.options._get_keep_attrs(False) ): out.attrs.update(dsattrs) out.attrs["history"] = update_history( @@ -945,9 +931,7 @@ def __call__(self, *args, **kwds): return outs[0] return tuple(outs) - def _parse_variables_from_call( - self, args, kwds - ) -> tuple[OrderedDict, OrderedDict, OrderedDict | dict]: + def _parse_variables_from_call(self, args, kwds) -> tuple[OrderedDict, OrderedDict, OrderedDict | dict]: """Extract variable and optional variables from call arguments.""" # Bind call arguments to `compute` arguments and set defaults. ba = self.__signature__.bind(*args, **kwds) @@ -987,8 +971,7 @@ def _assign_named_args(self, ba): if kind <= InputKind.OPTIONAL_VARIABLE: if isinstance(val, str) and ds is None: raise ValueError( - "Passing variable names as string requires giving the `ds` " - f"dataset (got {name}='{val}')" + f"Passing variable names as string requires giving the `ds` dataset (got {name}='{val}')" ) if (isinstance(val, str) or val is None) and ds is not None: # Set default name for DataArray @@ -998,8 +981,7 @@ def _assign_named_args(self, ba): ba.arguments[name] = ds[key] elif kind == InputKind.VARIABLE: raise MissingVariableError( - f"For input '{name}', variable '{key}' " - "was not found in the input dataset." + f"For input '{name}', variable '{key}' was not found in the input dataset." ) def _preprocess_and_checks(self, das, params): @@ -1063,9 +1045,7 @@ def _bind_call(self, func, **das): return func(*ba.args, **ba.kwargs) @classmethod - def _get_translated_metadata( - cls, locale, var_id=None, names=None, append_locale_name=True - ): + def _get_translated_metadata(cls, locale, var_id=None, names=None, append_locale_name=True): """ Get raw translated metadata for the current indicator and a given locale. @@ -1131,9 +1111,7 @@ def _update_attrs( for locale in OPTIONS[METADATA_LOCALES]: out.update( self._format( - self._get_translated_metadata( - locale, var_id=var_id, names=names or list(attrs.keys()) - ), + self._get_translated_metadata(locale, var_id=var_id, names=names or list(attrs.keys())), args=args, formatter=get_local_formatter(locale), ) @@ -1142,9 +1120,7 @@ def _update_attrs( # Get history and cell method attributes from source data attrs = defaultdict(str) if names is None or "cell_methods" in names: - attrs["cell_methods"] = merge_attributes( - "cell_methods", new_line=" ", missing_str=None, **das - ) + attrs["cell_methods"] = merge_attributes("cell_methods", new_line=" ", missing_str=None, **das) if "cell_methods" in out: attrs["cell_methods"] += " " + out.pop("cell_methods") @@ -1179,9 +1155,7 @@ def _check_identifier(identifier: str) -> None: ) @classmethod - def translate_attrs( - cls, locale: str | Sequence[str], fill_missing: bool = True - ) -> dict: + def translate_attrs(cls, locale: str | Sequence[str], fill_missing: bool = True) -> dict: """ Return a dictionary of unformatted translated translatable attributes. @@ -1266,8 +1240,7 @@ def json(cls, args: dict | None = None) -> dict: # We need to deepcopy, otherwise empty defaults get overwritten! # All those tweaks are to ensure proper serialization of the returned dictionary. out["parameters"] = { - k: p.asdict() if not p.injected else deepcopy(p.value) - for k, p in cls._all_parameters.items() + k: p.asdict() if not p.injected else deepcopy(p.value) for k, p in cls._all_parameters.items() } for name, param in list(out["parameters"].items()): if not cls._all_parameters[name].injected: @@ -1306,10 +1279,7 @@ def _format( """ # Use defaults if args is None: - args = { - k: p.default if not p.injected else p.value - for k, p in cls._all_parameters.items() - } + args = {k: p.default if not p.injected else p.value for k, p in cls._all_parameters.items()} # Prepare arguments mba = {} @@ -1332,10 +1302,7 @@ def _format( mba["indexer"] = args.get("freq") or "YS" elif is_percentile_dataarray(v): mba.update(get_percentile_metadata(v, k)) - elif ( - isinstance(v, DataArray) - and cls._all_parameters[k].kind == InputKind.QUANTIFIED - ): + elif isinstance(v, DataArray) and cls._all_parameters[k].kind == InputKind.QUANTIFIED: mba[k] = "" else: mba[k] = v @@ -1414,11 +1381,7 @@ def datacheck(self, **das) -> None: datachecks.check_freq(da, self.src_freq, strict=True) datachecks.check_common_time( - [ - da - for da in das.values() - if "time" in da.coords and da.time.ndim == 1 and len(da.time) > 3 - ] + [da for da in das.values() if "time" in da.coords and da.time.ndim == 1 and len(da.time) > 3] ) def __getattr__(self, attr): @@ -1454,11 +1417,7 @@ def parameters(self) -> dict: dict A dictionary of controllable parameters. """ - return { - name: param - for name, param in self._all_parameters.items() - if not param.injected - } + return {name: param for name, param in self._all_parameters.items() if not param.injected} @property def injected_parameters(self) -> dict: @@ -1472,11 +1431,7 @@ def injected_parameters(self) -> dict: dict A dictionary of all injected parameters. """ - return { - name: param.value - for name, param in self._all_parameters.items() - if param.injected - } + return {name: param.value for name, param in self._all_parameters.items() if param.injected} @property def is_generic(self) -> bool: @@ -1525,9 +1480,7 @@ class CheckMissingIndicator(Indicator): # numpydoc ignore=PR01,PR02 def __init__(self, **kwds): if self.missing == "from_context" and self.missing_options is not None: - raise ValueError( - "Cannot set `missing_options` with `missing` method being from context." - ) + raise ValueError("Cannot set `missing_options` with `missing` method being from context.") super().__init__(**kwds) @@ -1556,9 +1509,7 @@ def _postprocess(self, outs, das, params): outs = super()._postprocess(outs, das, params) freq = self._get_missing_freq(params) - method = ( - self.missing if self.missing != "from_context" else OPTIONS[CHECK_MISSING] - ) + method = self.missing if self.missing != "from_context" else OPTIONS[CHECK_MISSING] if method != "skip" and freq is not False: # Mask results that do not meet criteria defined by the `missing` method. # This means all outputs must have the same dimensions as the broadcasted inputs (excluding time) @@ -1568,9 +1519,7 @@ def _postprocess(self, outs, das, params): # We flag periods according to the missing method. skip variables without a time coordinate. src_freq = self.src_freq if isinstance(self.src_freq, str) else None miss = ( - misser(da, freq, src_freq, **params.get("indexer", {})) - for da in das.values() - if "time" in da.coords + misser(da, freq, src_freq, **params.get("indexer", {})) for da in das.values() if "time" in da.coords ) # Reduce by or and broadcast to ensure the same length in time # When indexing is used and there are no valid points in the last period, mask will not include it @@ -1772,9 +1721,7 @@ def build_indicator_module( out: ModuleType if hasattr(indicators, name): if doc is not None: - warnings.warn( - "Passed docstring ignored when extending existing module.", stacklevel=1 - ) + warnings.warn("Passed docstring ignored when extending existing module.", stacklevel=1) out = getattr(indicators, name) if reload: for n, ind in list(out.iter_indicators()): @@ -1816,13 +1763,14 @@ def build_indicator_module_from_yaml( # noqa: C901 Parameters ---------- filename : PathLike - Path to a YAML file or to the stem of all module files. See Notes for behaviour when passing a basename only. + Path to a YAML file or to the stem of all module files. + See Notes for behaviour when passing a basename only. name : str, optional The name of the new or existing module, defaults to the basename of the file (e.g: `atmos.yml` -> `atmos`). indices : Mapping of callables or module or path, optional - A mapping or module of indice functions or a python file declaring such a file. - When creating the indicator, the name in the `index_function` field is first sought - here, then the indicator class will search in :py:mod:`xclim.indices.generic` and finally in :py:mod:`xclim.indices`. + A mapping or module of indice functions or a python file declaring such a file. When creating the indicator, + the name in the `index_function` field is first sought here, then the indicator class will search + in :py:mod:`xclim.indices.generic` and finally in :py:mod:`xclim.indices`. translations : Mapping of dicts or path, optional Translated metadata for the new indicators. Keys of the mapping must be two-character language tags. Values can be translations dictionaries as defined in :ref:`internationalization:Internationalization`. @@ -1880,28 +1828,18 @@ def build_indicator_module_from_yaml( # noqa: C901 if validate is not True: schema = yamale.make_schema(validate) else: - schema = yamale.make_schema( - Path(__file__).parent.parent / "data" / "schema.yml" - ) + schema = yamale.make_schema(Path(__file__).parent.parent / "data" / "schema.yml") # Validate - a YamaleError will be raised if the module does not comply with the schema. - yamale.validate( - schema, yamale.make_data(content=ymlpath.read_text(encoding=encoding)) - ) + yamale.validate(schema, yamale.make_data(content=ymlpath.read_text(encoding=encoding))) # Load values from top-level in yml. # Priority of arguments differ. module_name = name or yml.get("module", filepath.stem) - default_base = registry.get( - yml.get("base"), base_registry.get(yml.get("base"), Daily) - ) + default_base = registry.get(yml.get("base"), base_registry.get(yml.get("base"), Daily)) doc = yml.get("doc") - if ( - not filepath.suffix - and indices is None - and (indfile := filepath.with_suffix(".py")).is_file() - ): + if not filepath.suffix and indices is None and (indfile := filepath.with_suffix(".py")).is_file(): # No suffix means we try to automatically detect the python file indices = indfile @@ -1913,9 +1851,7 @@ def build_indicator_module_from_yaml( # noqa: C901 # No suffix mean we try to automatically detect the json files. for locfile in filepath.parent.glob(f"{filepath.stem}.*.json"): locale = locfile.suffixes[0][1:] - _translations[locale] = read_locale_file( - locfile, module=module_name, encoding=encoding - ) + _translations[locale] = read_locale_file(locfile, module=module_name, encoding=encoding) elif translations is not None: # A mapping was passed, we read paths is any. _translations = { @@ -1989,14 +1925,10 @@ def _merge_attrs(dbase, dextra, attr, sep): if data.get("realm") is None and defkwargs.get("realm") is not None: data["realm"] = defkwargs["realm"] - mapping[identifier] = Indicator.from_dict( - data, identifier=identifier, module=module_name - ) + mapping[identifier] = Indicator.from_dict(data, identifier=identifier, module=module_name) except Exception as err: # pylint: disable=broad-except - raise_warn_or_log( - err, mode, msg=f"Constructing {identifier} failed with {err!r}" - ) + raise_warn_or_log(err, mode, msg=f"Constructing {identifier} failed with {err!r}") # Construct module mod = build_indicator_module(module_name, objs=mapping, doc=doc, reload=reload) diff --git a/src/xclim/core/locales.py b/src/xclim/core/locales.py index a3526bedb..e99c604cb 100644 --- a/src/xclim/core/locales.py +++ b/src/xclim/core/locales.py @@ -183,9 +183,7 @@ def get_local_attrs( indicator = [indicator] if not append_locale_name and len(locales) > 1: - raise ValueError( - "`append_locale_name` cannot be False if multiple locales are requested." - ) + raise ValueError("`append_locale_name` cannot be False if multiple locales are requested.") attrs = {} for locale in locales: @@ -229,9 +227,7 @@ def get_local_formatter( mods = attrs_mapping.pop("modifiers") return AttrFormatter(attrs_mapping, mods) - warnings.warn( - "No `attrs_mapping` entry found for locale {loc_name}, using default (english) formatter." - ) + warnings.warn("No `attrs_mapping` entry found for locale {loc_name}, using default (english) formatter.") return default_formatter @@ -251,9 +247,7 @@ def __init__(self, locale): ) -def read_locale_file( - filename, module: str | None = None, encoding: str = "UTF8" -) -> dict[str, dict]: +def read_locale_file(filename, module: str | None = None, encoding: str = "UTF8") -> dict[str, dict]: """ Read a locale file (.json) and return its dictionary. @@ -278,10 +272,7 @@ def read_locale_file( locdict = json.load(f) if module is not None: - locdict = { - (k if k == "attrs_mapping" else f"{module}.{k}"): v - for k, v in locdict.items() - } + locdict = {(k if k == "attrs_mapping" else f"{module}.{k}"): v for k, v in locdict.items()} return locdict diff --git a/src/xclim/core/missing.py b/src/xclim/core/missing.py index f5c5ee197..f97095f8b 100644 --- a/src/xclim/core/missing.py +++ b/src/xclim/core/missing.py @@ -9,13 +9,13 @@ consecutive and overall missing values per month. `xclim` has a registry of missing value detection algorithms that can be extended by users to customize the behavior -of indicators. Once registered, algorithms can be used by setting the global option as ``xc.set_options(check_missing="method")`` -or within indicators by setting the `missing` attribute of an `Indicator` subclass. -By default, `xclim` registers the following algorithms: +of indicators. Once registered, algorithms can be used by setting the global option as +``xc.set_options(check_missing="method")`` or within indicators by setting the `missing` attribute of an +`Indicator` subclass. By default, `xclim` registers the following algorithms: * `any`: A result is missing if any input value is missing. * `at_least_n`: A result is missing if less than a given number of valid values are present. - * `pct`: A result is missing if more than a given fraction of values are missing. + * `pct`: A result is missing if more than a given fraction of its values are missing. * `wmo`: A result is missing if 11 days are missing, or 5 consecutive values are missing in a month. To define another missing value algorithm, subclass :py:class:`MissingBase` and decorate it with @@ -95,9 +95,7 @@ def expected_count( if src_timestep is None: src_timestep = xr.infer_freq(time) if src_timestep is None: - raise ValueError( - "A src_timestep must be passed when it can't be inferred from the data." - ) + raise ValueError("A src_timestep must be passed when it can't be inferred from the data.") if freq is not None and not is_offset_divisor(src_timestep, freq): raise NotImplementedError( @@ -106,9 +104,7 @@ def expected_count( ) # Ensure a DataArray constructed like we expect - time = xr.DataArray( - time.values, dims=("time",), coords={"time": time.values}, name="time" - ) + time = xr.DataArray(time.values, dims=("time",), coords={"time": time.values}, name="time") if freq: resamp = time.resample(time=freq).first() @@ -141,9 +137,7 @@ def expected_count( if "doy_bounds" in indexer: # This is the only case supported by select_time where DataArrays are supported # TODO: What happens when this new dim makes sda too large ? How do we involve dask here ? - da_bnds = [ - bnd for bnd in indexer["doy_bounds"] if isinstance(bnd, xr.DataArray) - ] + da_bnds = [bnd for bnd in indexer["doy_bounds"] if isinstance(bnd, xr.DataArray)] sda = xr.broadcast(sda, *da_bnds, exclude=("time",))[0] st = select_time(sda, **indexer) @@ -180,9 +174,7 @@ class MissingBase: def __init__(self, **options): if not self.validate(**options): - raise ValueError( - f"Options {options} are not valid for {self.__class__.__name__}." - ) + raise ValueError(f"Options {options} are not valid for {self.__class__.__name__}.") self.options = options @staticmethod @@ -295,8 +287,7 @@ def __call__( if not self._validate_src_timestep(src_timestep): raise ValueError( - f"Input source timestep {src_timestep} is invalid " - f"for missing method {self.__class__.__name__}." + f"Input source timestep {src_timestep} is invalid for missing method {self.__class__.__name__}." ) count = expected_count(da.time, freq=freq, src_timestep=src_timestep, **indexer) @@ -321,9 +312,7 @@ def __init__(self): """Create a MissingAny object.""" super().__init__() - def is_missing( - self, valid: xr.DataArray, count: xr.DataArray, freq: str | None - ) -> xr.DataArray: + def is_missing(self, valid: xr.DataArray, count: xr.DataArray, freq: str | None) -> xr.DataArray: if freq is not None: valid = valid.resample(time=freq) # The number of valid values should fit the expected count. @@ -333,8 +322,7 @@ def is_missing( # TODO: Make coarser method controllable. class MissingTwoSteps(MissingBase): r""" - Base class used to determined where Indicator outputs should be masked, - in a two step process. + Base class used to determined where Indicator outputs should be masked in a two-step process. In addition to what :py:class:`MissingBase` does, subclasses first perform the mask determination at some frequency and then resample at the (coarser) target frequency. @@ -377,20 +365,14 @@ def __call__( True on the periods that should be considered missing or invalid. """ subfreq = self.options["subfreq"] or freq - if ( - subfreq is not None - and freq is not None - and compare_offsets(freq, "<", subfreq) - ): + if subfreq is not None and freq is not None and compare_offsets(freq, "<", subfreq): raise ValueError( "The target resampling frequency cannot be finer than the first-step " f"frequency. Got : {subfreq} > {freq}." ) miss = super().__call__(da, freq=subfreq, src_timestep=src_timestep, **indexer) if subfreq != freq: - miss = MissingAny()( - miss.where(~miss), freq, src_timestep=subfreq, **indexer - ) + miss = MissingAny()(miss.where(~miss), freq, src_timestep=subfreq, **indexer) return miss @@ -434,9 +416,7 @@ def validate(nm: int, nc: int, subfreq: str | None = None): def _validate_src_timestep(self, src_timestep): return src_timestep == "D" - def is_missing( - self, valid: xr.DataArray, count: xr.DataArray, freq: str - ) -> xr.DataArray: + def is_missing(self, valid: xr.DataArray, count: xr.DataArray, freq: str) -> xr.DataArray: from xclim.indices import run_length as rl from xclim.indices.helpers import resample_map @@ -449,9 +429,7 @@ def is_missing( # Check for consecutive invalid values # FIXME: This does not take holes in consideration - longest_run = resample_map( - ~valid, "time", freq, rl.longest_run, map_blocks=True - ) + longest_run = resample_map(~valid, "time", freq, rl.longest_run, map_blocks=True) cond2 = longest_run >= self.options["nc"] return cond1 | cond2 @@ -459,7 +437,7 @@ def is_missing( @register_missing_method("pct") class MissingPct(MissingTwoSteps): - """Mask periods as missing when there are more then a given percentage of missing days.""" + """Mask periods as missing when there are more than a given percentage of missing days.""" def __init__(self, tolerance: float = 0.1, subfreq: str | None = None): """ @@ -480,9 +458,7 @@ def __init__(self, tolerance: float = 0.1, subfreq: str | None = None): def validate(tolerance: float, subfreq: str | None = None): return 0 <= tolerance <= 1 - def is_missing( - self, valid: xr.DataArray, count: xr.DataArray, freq: str | None - ) -> xr.DataArray: + def is_missing(self, valid: xr.DataArray, count: xr.DataArray, freq: str | None) -> xr.DataArray: if freq is not None: valid = valid.resample(time=freq) @@ -493,7 +469,11 @@ def is_missing( @register_missing_method("at_least_n") class AtLeastNValid(MissingTwoSteps): - r"""Mask periods as missing if they don't have at least a given number of valid values (ignoring the expected count of elements).""" + r""" + Mask periods as missing if they don't have at least a given number of valid values. + + Ignores the expected count of elements. + """ def __init__(self, n: int = 20, subfreq: str | None = None): """ @@ -513,9 +493,7 @@ def __init__(self, n: int = 20, subfreq: str | None = None): def validate(n: int, subfreq: str | None = None): return n > 0 - def is_missing( - self, valid: xr.DataArray, count: xr.DataArray, freq: str | None - ) -> xr.DataArray: + def is_missing(self, valid: xr.DataArray, count: xr.DataArray, freq: str | None) -> xr.DataArray: if freq is not None: valid = valid.resample(time=freq) nvalid = valid.sum(dim="time") @@ -555,9 +533,7 @@ def missing_pct( # noqa: D103 # numpydoc ignore=GL08 subfreq: str | None = None, **indexer, ) -> xr.DataArray: - return MissingPct(tolerance=tolerance, subfreq=subfreq)( - da, freq, src_timestep, **indexer - ) + return MissingPct(tolerance=tolerance, subfreq=subfreq)(da, freq, src_timestep, **indexer) def at_least_n_valid( # noqa: D103 # numpydoc ignore=GL08 @@ -571,9 +547,7 @@ def at_least_n_valid( # noqa: D103 # numpydoc ignore=GL08 return AtLeastNValid(n=n, subfreq=subfreq)(da, freq, src_timestep, **indexer) -def missing_from_context( - da: xr.DataArray, freq: str, src_timestep: str | None = None, **indexer -) -> xr.DataArray: +def missing_from_context(da: xr.DataArray, freq: str, src_timestep: str | None = None, **indexer) -> xr.DataArray: """ Mask periods as missing according to the algorithm and options set in xclim's global options. diff --git a/src/xclim/core/options.py b/src/xclim/core/options.py index 9a718c4c9..63fece6dc 100644 --- a/src/xclim/core/options.py +++ b/src/xclim/core/options.py @@ -237,13 +237,15 @@ class set_options: # numpydoc ignore=PR01,PR02 Controls attributes handling in indicators. If True, attributes from all inputs are merged using the `drop_conflicts` strategy and then updated with xclim-provided attributes. If ``as_dataset`` is also True and a dataset was passed to the ``ds`` argument of the Indicator, - the dataset's attributes are copied to the indicator's output. If False, attributes from the inputs are ignored. + the dataset's attributes are copied to the indicator's output. + If False, attributes from the inputs are ignored. If "xarray", xclim will use xarray's `keep_attrs` option. Note that xarray's "default" is equivalent to False. Default: ``"xarray"``. as_dataset : bool If True, indicators output datasets. If False, they output DataArrays. Default :``False``. resample_map_blocks : bool - If True, some indicators will wrap their resampling operations with `xr.map_blocks`, using :py:func:`xclim.indices.helpers.resample_map`. + If True, some indicators will wrap their resampling operations with `xr.map_blocks`, + using :py:func:`xclim.indices.helpers.resample_map`. This requires `flox` to be installed in order to ensure the chunking is appropriate. Examples @@ -254,7 +256,6 @@ class set_options: # numpydoc ignore=PR01,PR02 >>> ds = xr.open_dataset(path_to_tas_file).tas >>> with xclim.set_options(metadata_locales=["fr"]): ... out = xclim.atmos.tg_mean(ds) - ... Or to set global options: diff --git a/src/xclim/core/units.py b/src/xclim/core/units.py index 036835d3e..284888f19 100644 --- a/src/xclim/core/units.py +++ b/src/xclim/core/units.py @@ -188,9 +188,7 @@ def units2pint( ] possibilities = [f"{d} {u}" for d in degree_ex for u in unit_ex] if unit.strip() in possibilities: - raise ValidationError( - "Remove white space from temperature units, e.g. use `degC`." - ) + raise ValidationError("Remove white space from temperature units, e.g. use `degC`.") pu = units.parse_units(unit) if metadata == "temperature: difference": @@ -273,9 +271,7 @@ def ensure_cf_units(ustr: str) -> str: return pint2cfunits(units2pint(ustr)) -def pint_multiply( - da: xr.DataArray, q: Any, out_units: str | None = None -) -> xr.DataArray: +def pint_multiply(da: xr.DataArray, q: Any, out_units: str | None = None) -> xr.DataArray: """ Multiply xarray.DataArray by pint.Quantity. @@ -401,10 +397,7 @@ def convert_units_to( # noqa: C901 # Automatic pre-conversions based on the dimensionalities and CF standard names standard_name = source.attrs.get("standard_name") - if ( - standard_name is not None - and source_unit.dimensionality != target_unit.dimensionality - ): + if standard_name is not None and source_unit.dimensionality != target_unit.dimensionality: dim_order_diff = source_unit.dimensionality / target_unit.dimensionality for convname, convconf in CF_CONVERSIONS.items(): for direction, sign in [("to", 1), ("from", -1)]: @@ -444,9 +437,7 @@ def convert_units_to( # noqa: C901 raise NotImplementedError(f"Source of type `{type(source)}` is not supported.") -def cf_conversion( - standard_name: str, conversion: str, direction: Literal["to", "from"] -) -> str | None: +def cf_conversion(standard_name: str, conversion: str, direction: Literal["to", "from"]) -> str | None: """ Get the standard name of the specific conversion for the given standard name. @@ -483,7 +474,8 @@ def cf_conversion( """ Resampling frequency units for :py:func:`xclim.core.units.infer_sampling_units`. -Mapping from offset base to CF-compliant unit. Only constant-length frequencies that are not also pint units are included. +Mapping from offset base to CF-compliant unit. Only constant-length frequencies that are +not also pint units are included. """ @@ -525,9 +517,7 @@ def infer_sampling_units( try: out = multi, FREQ_UNITS.get(base, base) except KeyError as err: - raise ValueError( - f"Sampling frequency {freq} has no corresponding units." - ) from err + raise ValueError(f"Sampling frequency {freq} has no corresponding units.") from err if out == (7, "d"): # Special case for weekly frequency. xarray's CFTimeOffsets do not have "W". return 1, "week" @@ -599,9 +589,7 @@ def ensure_delta(unit: xr.DataArray | str | units.Quantity) -> str: return delta_unit -def to_agg_units( - out: xr.DataArray, orig: xr.DataArray, op: str, dim: str = "time" -) -> xr.DataArray: +def to_agg_units(out: xr.DataArray, orig: xr.DataArray, op: str, dim: str = "time") -> xr.DataArray: """ Set and convert units of an array after an aggregation operation along the sampling dimension (time). @@ -652,9 +640,7 @@ def to_agg_units( ... dims=("time",), ... coords={"time": time}, ... ) - >>> dt = (tas - 16).assign_attrs( - ... units="degC", units_metadata="temperature: difference" - ... ) + >>> dt = (tas - 16).assign_attrs(units="degC", units_metadata="temperature: difference") >>> degdays = dt.clip(0).sum("time") # Integral of temperature above a threshold >>> degdays = to_agg_units(degdays, dt, op="integral") >>> degdays.units @@ -675,9 +661,7 @@ def to_agg_units( out.attrs["units"] = pint2cfunits(str2pint(orig.units) ** 2) elif op in ["doymin", "doymax"]: - out.attrs.update( - units="1", is_dayofyear=np.int32(1), calendar=get_calendar(orig) - ) + out.attrs.update(units="1", is_dayofyear=np.int32(1), calendar=get_calendar(orig)) elif op in ["count", "integral"]: m, freq_u_raw = infer_sampling_units(orig[dim]) @@ -725,9 +709,7 @@ def _rate_and_amount_converter( label: Literal["lower", "upper"] = "lower" # Default to "lower" label for diff if isinstance(dim, str): if not isinstance(da, xr.DataArray): - raise ValueError( - "If `dim` is a string, the data to convert must be a DataArray." - ) + raise ValueError("If `dim` is a string, the data to convert must be a DataArray.") time = da[dim] else: time = dim @@ -789,11 +771,7 @@ def _rate_and_amount_converter( # In the case with no freq, last period as the same length as the one before. # In the case with freq in M, Q, A, this has been dealt with above in `time` # and `label` has been updated accordingly. - dt = ( - time.diff(dim, label=label) - .reindex({dim: time}, method="ffill") - .astype(float) - ) + dt = time.diff(dim, label=label).reindex({dim: time}, method="ffill").astype(float) dt = dt / 1e9 # Convert to seconds if to == "amount": @@ -816,11 +794,7 @@ def _rate_and_amount_converter( raise ValueError("Argument `to` must be one of 'amount' or 'rate'.") old_name = da.attrs.get("standard_name") - if old_name and ( - new_name := cf_conversion( - old_name, "amount2rate", "to" if to == "rate" else "from" - ) - ): + if old_name and (new_name := cf_conversion(old_name, "amount2rate", "to" if to == "rate" else "from")): out = out.assign_attrs(standard_name=new_name) if out_units: @@ -841,9 +815,9 @@ def rate2amount( """ Convert a rate variable to an amount by multiplying by the sampling period length. - If the sampling period length cannot be inferred, the rate values - are multiplied by the duration between their time coordinate and the next one. The last period - is estimated with the duration of the one just before. + If the sampling period length cannot be inferred, the rate values are multiplied by the duration + between their time coordinate and the next one. The last period is estimated with the duration of + the one just before. This is the inverse operation of :py:func:`xclim.core.units.amount2rate`. @@ -854,8 +828,9 @@ def rate2amount( dim : str or DataArray The name of time dimension or the coordinate itself. sampling_rate_from_coord : bool - For data with irregular time coordinates. If True, the diff of the time coordinate will be used as the sampling rate, - meaning each data point will be assumed to apply for the interval ending at the next point. See notes. + For data with irregular time coordinates. + If True, the diff of the time coordinate will be used as the sampling rate, meaning each data point + will be assumed to apply for the interval ending at the next point. See notes. Defaults to False, which raises an error if the time coordinate is irregular. out_units : str, optional Specific output units, if needed. @@ -879,22 +854,18 @@ def rate2amount( The following converts a daily array of precipitation in mm/h to the daily amounts in mm: >>> time = xr.cftime_range("2001-01-01", freq="D", periods=365) - >>> pr = xr.DataArray( - ... [1] * 365, dims=("time",), coords={"time": time}, attrs={"units": "mm/h"} - ... ) + >>> pr = xr.DataArray([1] * 365, dims=("time",), coords={"time": time}, attrs={"units": "mm/h"}) >>> pram = rate2amount(pr) >>> pram.units 'mm' >>> float(pram[0]) 24.0 - Also works if the time axis is irregular : the rates are assumed constant for the whole period - starting on the values timestamp to the next timestamp. This option is activated with `sampling_rate_from_coord=True`. + Also works if the time axis is irregular : the rates are assumed constant for the whole period starting on + the values timestamp to the next timestamp. This option is activated with `sampling_rate_from_coord=True`. >>> time = time[[0, 9, 30]] # The time axis is Jan 1st, Jan 10th, Jan 31st - >>> pr = xr.DataArray( - ... [1] * 3, dims=("time",), coords={"time": time}, attrs={"units": "mm/h"} - ... ) + >>> pr = xr.DataArray([1] * 3, dims=("time",), coords={"time": time}, attrs={"units": "mm/h"}) >>> pram = rate2amount(pr, sampling_rate_from_coord=True) >>> pram.values array([216., 504., 504.]) @@ -969,9 +940,7 @@ def amount2rate( @_register_conversion("amount2lwethickness", "to") -def amount2lwethickness( - amount: xr.DataArray, out_units: str | None = None -) -> xr.DataArray | Quantified: +def amount2lwethickness(amount: xr.DataArray, out_units: str | None = None) -> xr.DataArray | Quantified: """ Convert a liquid water amount (mass over area) to its equivalent area-averaged thickness (length). @@ -1007,9 +976,7 @@ def amount2lwethickness( @_register_conversion("amount2lwethickness", "from") -def lwethickness2amount( - thickness: xr.DataArray, out_units: str | None = None -) -> xr.DataArray | Quantified: +def lwethickness2amount(thickness: xr.DataArray, out_units: str | None = None) -> xr.DataArray | Quantified: """ Convert a liquid water thickness (length) to its equivalent amount (mass over area). @@ -1036,9 +1003,7 @@ def lwethickness2amount( water_density = str2pint("1000 kg m-3") out = pint_multiply(thickness, water_density) old_name = thickness.attrs.get("standard_name") - if old_name and ( - new_name := cf_conversion(old_name, "amount2lwethickness", "from") - ): + if old_name and (new_name := cf_conversion(old_name, "amount2lwethickness", "from")): out.attrs["standard_name"] = new_name if out_units: out = cast(xr.DataArray, convert_units_to(out, out_units)) @@ -1124,9 +1089,7 @@ def rate2flux( assuming a density of 100 kg m-3: >>> time = xr.cftime_range("2001-01-01", freq="D", periods=365) - >>> prsnd = xr.DataArray( - ... [1] * 365, dims=("time",), coords={"time": time}, attrs={"units": "mm/s"} - ... ) + >>> prsnd = xr.DataArray([1] * 365, dims=("time",), coords={"time": time}, attrs={"units": "mm/s"}) >>> prsn = rate2flux(prsnd, density="100 kg m-3", out_units="kg m-2 s-1") >>> prsn.units 'kg m-2 s-1' @@ -1197,9 +1160,7 @@ def flux2rate( @datacheck -def check_units( - val: str | xr.DataArray | None, dim: str | xr.DataArray | None = None -) -> None: +def check_units(val: str | xr.DataArray | None, dim: str | xr.DataArray | None = None) -> None: """ Check that units are compatible with dimensions, otherwise raise a `ValidationError`. @@ -1273,9 +1234,7 @@ def check_units( # Should be resolved in pint v0.24. See: https://github.com/hgrecco/pint/issues/1913 with warnings.catch_warnings(): warnings.simplefilter("ignore", category=DeprecationWarning) - raise ValidationError( - f"Data units {val_units} are not compatible with requested {dim}." - ) + raise ValidationError(f"Data units {val_units} are not compatible with requested {dim}.") def _check_output_has_units( @@ -1391,9 +1350,7 @@ def wrapper(*args, **kwargs): # numpydoc ignore=GL08 # but here we pass a real unit. It will also check the standard name of the arg, # but we give it another chance by checking the ref arg. context = context or infer_context( - standard_name=getattr(refvar, "attrs", {}).get( - "standard_name" - ) + standard_name=getattr(refvar, "attrs", {}).get("standard_name") ) with units.context(context): check_units(bound_args.arguments.get(name), dim) @@ -1468,9 +1425,7 @@ def dec(func): # numpydoc ignore=GL08 # Check that all Quantified parameters have their dimension declared. sig = signature(func) for name, param in sig.parameters.items(): - if infer_kind_from_parameter(param) == InputKind.QUANTIFIED and ( - name not in units_by_name - ): + if infer_kind_from_parameter(param) == InputKind.QUANTIFIED and (name not in units_by_name): raise ValueError(f"Argument {name} has no declared dimensions.") @wraps(func) @@ -1492,9 +1447,7 @@ def wrapper(*args, **kwargs): # numpydoc ignore=GL08 return dec -def infer_context( - standard_name: str | None = None, dimension: str | None = None -) -> str: +def infer_context(standard_name: str | None = None, dimension: str | None = None) -> str: """ Return units context based on either the variable's standard name or the pint dimension. diff --git a/src/xclim/core/utils.py b/src/xclim/core/utils.py index 4b7461b64..df0495677 100644 --- a/src/xclim/core/utils.py +++ b/src/xclim/core/utils.py @@ -157,9 +157,7 @@ def ensure_chunk_size(da: xr.DataArray, **minchunks: int) -> xr.DataArray: if toosmall.sum() > 1: # Many chunks are too small, merge them by groups fac = np.ceil(minchunk / min(chunks)).astype(int) - chunking[dim] = tuple( - sum(chunks[i : i + fac]) for i in range(0, len(chunks), fac) - ) + chunking[dim] = tuple(sum(chunks[i : i + fac]) for i in range(0, len(chunks), fac)) # Reset counter is case the last chunks are still too small chunks = chunking[dim] toosmall = np.array(chunks) < minchunk @@ -292,9 +290,7 @@ def nan_calc_percentiles( return _nan_quantile(arr, quantiles, axis, alpha, beta) -def _compute_virtual_index( - n: np.ndarray, quantiles: np.ndarray, alpha: float, beta: float -): +def _compute_virtual_index(n: np.ndarray, quantiles: np.ndarray, alpha: float, beta: float): """ Compute the floating point indexes of an array for the linear interpolation of quantiles. @@ -412,9 +408,7 @@ def _linear_interpolation( """ diff_b_a = np.subtract(right, left) lerp_interpolation = np.asanyarray(np.add(left, diff_b_a * gamma)) - np.subtract( - right, diff_b_a * (1 - gamma), out=lerp_interpolation, where=gamma >= 0.5 - ) + np.subtract(right, diff_b_a * (1 - gamma), out=lerp_interpolation, where=gamma >= 0.5) if lerp_interpolation.ndim == 0: lerp_interpolation = lerp_interpolation[()] # unpack 0d arrays return lerp_interpolation @@ -462,9 +456,7 @@ def _nan_quantile( valid_values_count = valid_values_count[..., np.newaxis] virtual_indexes = _compute_virtual_index(valid_values_count, quantiles, alpha, beta) virtual_indexes = np.asanyarray(virtual_indexes) - previous_indexes, next_indexes = _get_indexes( - arr, virtual_indexes, valid_values_count - ) + previous_indexes, next_indexes = _get_indexes(arr, virtual_indexes, valid_values_count) # --- Sorting arr.sort(axis=DATA_AXIS) # --- Get values from indexes @@ -516,8 +508,8 @@ class InputKind(IntEnum): QUANTIFIED = 2 """A quantity with units, either as a string (scalar), a pint.Quantity (scalar) or a DataArray (with units set). - Annotation : ``xclim.core.utils.Quantified`` and an entry in the :py:func:`xclim.core.units.declare_units` decorator. - "Quantified" translates to ``str | xr.DataArray | pint.util.Quantity``. + Annotation : ``xclim.core.utils.Quantified`` and an entry in the :py:func:`xclim.core.units.declare_units` + decorator. "Quantified" translates to ``str | xr.DataArray | pint.util.Quantity``. """ FREQ_STR = 3 """A string representing an "offset alias", as defined by pandas. @@ -600,9 +592,7 @@ def infer_kind_from_parameter(param) -> InputKind: The correspondence between parameters and kinds is documented in :py:class:`xclim.core.utils.InputKind`. """ if param.annotation is not _empty: - annot = set( - param.annotation.replace("xarray.", "").replace("xr.", "").split(" | ") - ) + annot = set(param.annotation.replace("xarray.", "").replace("xr.", "").split(" | ")) else: annot = {"no_annotation"} @@ -675,9 +665,7 @@ def adapt_clix_meta_yaml( # noqa: C901 yml = safe_load(raw) yml["realm"] = "atmos" - yml[ - "doc" - ] = """ =================== + yml["doc"] = """ =================== CF Standard indices =================== @@ -708,14 +696,10 @@ def adapt_clix_meta_yaml( # noqa: C901 data["compute"] = index_function["name"] if getattr(generic, data["compute"], None) is None: remove_ids.append(cmid) - print( - f"Indicator {cmid} uses non-implemented function {data['compute']}, removing." - ) + print(f"Indicator {cmid} uses non-implemented function {data['compute']}, removing.") continue - if (data["output"].get("standard_name") or "").startswith( - "number_of_days" - ) or cmid == "nzero": + if (data["output"].get("standard_name") or "").startswith("number_of_days") or cmid == "nzero": remove_ids.append(cmid) print( f"Indicator {cmid} has a 'number_of_days' standard name" @@ -751,15 +735,11 @@ def adapt_clix_meta_yaml( # noqa: C901 data["parameters"][name] = { "description": param.get( "long_name", - param.get( - "proposed_standard_name", param.get("standard_name") - ).replace("_", " "), + param.get("proposed_standard_name", param.get("standard_name")).replace("_", " "), ), "units": param["units"], } - rename_params[f"{{{name}}}"] = ( - f"{{{list(param['data'].keys())[0]}}}" - ) + rename_params[f"{{{name}}}"] = f"{{{list(param['data'].keys())[0]}}}" else: # Value data["parameters"][name] = f"{param['data']} {param['units']}" @@ -777,9 +757,7 @@ def adapt_clix_meta_yaml( # noqa: C901 methods = [] for i, cell_method in enumerate(val): # Construct cell_method string - cm = "".join( - [f"{dim}: {meth}" for dim, meth in cell_method.items()] - ) + cm = "".join([f"{dim}: {meth}" for dim, meth in cell_method.items()]) # If cell_method seems to be describing input data, and not the operation, skip. if i == 0: @@ -848,16 +826,12 @@ def _chunk_like(*inputs: xr.DataArray | xr.Dataset, chunks: dict[str, int] | Non outputs = [] for da in inputs: - if isinstance(da, xr.DataArray) and isinstance( - da.variable, xr.core.variable.IndexVariable - ): + if isinstance(da, xr.DataArray) and isinstance(da.variable, xr.core.variable.IndexVariable): da = xr.DataArray(da, dims=da.dims, coords=da.coords, name=da.name) if not isinstance(da, xr.DataArray | xr.Dataset): outputs.append(da) else: - outputs.append( - da.chunk(**{d: c for d, c in chunks.items() if d in da.dims}) - ) + outputs.append(da.chunk(**{d: c for d, c in chunks.items() if d in da.dims})) return tuple(outputs) @@ -867,9 +841,9 @@ def split_auxiliary_coordinates( """ Split auxiliary coords from the dataset. - An auxiliary coordinate is a coordinate variable that does not define a dimension and thus is not necessarily needed for dataset alignment. - Any coordinate that has a name different from its dimension(s) is flagged as auxiliary. - All scalar coordinates are flagged as auxiliary. + An auxiliary coordinate is a coordinate variable that does not define a dimension and thus + is not necessarily needed for dataset alignment. Any coordinate that has a name different from + its dimension(s) is flagged as auxiliary. All scalar coordinates are flagged as auxiliary. Parameters ---------- @@ -896,9 +870,7 @@ def split_auxiliary_coordinates( >>> merged = clean.assign_coords(da.coords) >>> merged.identical(ds) # True """ - aux_crd_names = [ - nm for nm, crd in obj.coords.items() if len(crd.dims) != 1 or crd.dims[0] != nm - ] + aux_crd_names = [nm for nm, crd in obj.coords.items() if len(crd.dims) != 1 or crd.dims[0] != nm] aux_crd_ds = obj.coords.to_dataset()[aux_crd_names] clean_obj = obj.drop_vars(aux_crd_names) return clean_obj, aux_crd_ds diff --git a/src/xclim/ensembles/_base.py b/src/xclim/ensembles/_base.py index 64ea60bd9..37ade39f2 100644 --- a/src/xclim/ensembles/_base.py +++ b/src/xclim/ensembles/_base.py @@ -60,11 +60,13 @@ def create_ensemble( If True, climate simulations are treated as xarray multifile Datasets before concatenation. Only applicable when "datasets" is sequence of list of file paths. Default: False. resample_freq : Optional[str] - If the members of the ensemble have the same frequency but not the same offset, they cannot be properly aligned. + If the members of the ensemble have the same frequency but not the same offset, + they cannot be properly aligned. If resample_freq is set, the time coordinate of each member will be modified to fit this frequency. calendar : str, optional The calendar of the time coordinate of the ensemble. - By default, the smallest common calendar is chosen. For example, a mixed input of "noleap" and "360_day" will default to "noleap". + By default, the smallest common calendar is chosen. + For example, a mixed input of "noleap" and "360_day" will default to "noleap". 'default' is the standard calendar using np.datetime64 objects (xarray's "standard" with `use_cftime=False`). realizations : sequence, optional The coordinate values for the new `realization` axis. @@ -74,7 +76,8 @@ def create_ensemble( Additional arguments to pass to py:func:`xclim.core.calendar.convert_calendar`. For conversions involving '360_day', the align_on='date' option is used by default. **xr_kwargs : dict - Any keyword arguments to be given to `xr.open_dataset` when opening the files (or to `xr.open_mfdataset` if `multifile` is True). + Any keyword arguments to be given to `xr.open_dataset` when opening the files + (or to `xr.open_mfdataset` if `multifile` is True). Returns ------- @@ -110,8 +113,7 @@ def create_ensemble( datasets = datasets.values() elif isinstance(datasets, str) and realizations is not None: raise ValueError( - "Passing `realizations` is not supported when `datasets` " - "is a glob pattern, as the final order is random." + "Passing `realizations` is not supported when `datasets` is a glob pattern, as the final order is random." ) ds = _ens_align_datasets( @@ -200,10 +202,7 @@ def ensemble_mean_std_max_min( if "description" in ds_out[vv].attrs.keys(): vv.split() ds_out[vv].attrs["description"] = ( - ds_out[vv].attrs["description"] - + " : " - + vv.split("_")[-1] - + " of ensemble" + ds_out[vv].attrs["description"] + " : " + vv.split("_")[-1] + " of ensemble" ) ds_out.attrs["history"] = update_history( @@ -262,7 +261,8 @@ def ensemble_percentiles( Returns ------- xr.Dataset or xr.DataArray - If split is True, same type as ens; dataset otherwise, containing data variable(s) of requested ensemble statistics. + If split is True, same type as ens; + Otherwise, a dataset containing data variable(s) of requested ensemble statistics. Examples -------- @@ -315,21 +315,14 @@ def ensemble_percentiles( if ens.chunks and len(ens.chunks[ens.get_axis_num("realization")]) > 1: if keep_chunk_size is None: # Enable smart rechunking is chunksize exceed 2E8 elements after merging along realization - keep_chunk_size = ( - np.prod(ens.isel(realization=0).data.chunksize) * ens.realization.size - > 2e8 - ) + keep_chunk_size = np.prod(ens.isel(realization=0).data.chunksize) * ens.realization.size > 2e8 if keep_chunk_size: # Smart rechunk on dimension where chunks are the largest chk_dim, chks = max( enumerate(ens.chunks), - key=lambda kv: ( - 0 if kv[0] == ens.get_axis_num("realization") else max(kv[1]) - ), - ) - ens = ens.chunk( - {"realization": -1, ens.dims[chk_dim]: len(chks) * ens.realization.size} + key=lambda kv: (0 if kv[0] == ens.get_axis_num("realization") else max(kv[1])), ) + ens = ens.chunk({"realization": -1, ens.dims[chk_dim]: len(chks) * ens.realization.size}) else: ens = ens.chunk({"realization": -1}) @@ -349,9 +342,7 @@ def ensemble_percentiles( ) else: if method != "linear": - raise ValueError( - "Only the 'linear' method is supported when using weights." - ) + raise ValueError("Only the 'linear' method is supported when using weights.") with xr.set_options(keep_attrs=True): # xclim's calc_perc does not support weighted arrays, so xarray's native function is used instead. @@ -364,17 +355,13 @@ def ensemble_percentiles( if min_members != 1: out = out.where(ens.notnull().sum("realization") >= min_members) - out = out.assign_coords( - percentiles=xr.DataArray(list(values), dims=("percentiles",)) - ) + out = out.assign_coords(percentiles=xr.DataArray(list(values), dims=("percentiles",))) if split: out = out.to_dataset(dim="percentiles") for p, perc in out.data_vars.items(): perc.attrs.update(ens.attrs) - perc.attrs["description"] = ( - perc.attrs.get("description", "") + f" {p}th percentile of ensemble." - ) + perc.attrs["description"] = perc.attrs.get("description", "") + f" {p}th percentile of ensemble." out[p] = perc out = out.rename(name_dict={p: f"{ens.name}_p{int(p):02d}"}) @@ -450,8 +437,7 @@ def _ens_align_datasets( counts = time.astype(bool).resample(time=resample_freq).count() if any(counts > 1): raise ValueError( - f"Alignment of dataset #{i:02d} failed: " - f"Time axis cannot be resampled to freq {resample_freq}." + f"Alignment of dataset #{i:02d} failed: Time axis cannot be resampled to freq {resample_freq}." ) time = counts.time diff --git a/src/xclim/ensembles/_filters.py b/src/xclim/ensembles/_filters.py index bd5d352d0..d8039071f 100644 --- a/src/xclim/ensembles/_filters.py +++ b/src/xclim/ensembles/_filters.py @@ -65,9 +65,7 @@ def _concat_hist(da: xr.DataArray, **hist) -> xr.DataArray: return xr.concat([h, bare], dim="time").assign_coords({dim: index}) -def _model_in_all_scens( - da: xr.DataArray, dimensions: dict | None = None -) -> xr.DataArray: +def _model_in_all_scens(da: xr.DataArray, dimensions: dict | None = None) -> xr.DataArray: """ Return data with only simulations that have at least one member in each scenario. diff --git a/src/xclim/ensembles/_partitioning.py b/src/xclim/ensembles/_partitioning.py index e759a6899..16984c0b7 100644 --- a/src/xclim/ensembles/_partitioning.py +++ b/src/xclim/ensembles/_partitioning.py @@ -6,7 +6,6 @@ This module implements methods and tools meant to partition climate projection uncertainties into different components. """ - from __future__ import annotations import numpy as np @@ -106,9 +105,7 @@ def hawkins_sutton( raise ValueError("This algorithm expects annual time series.") if not {"time", "scenario", "model"}.issubset(da.dims): - raise ValueError( - "DataArray dimensions should include 'time', 'scenario' and 'model'." - ) + raise ValueError("DataArray dimensions should include 'time', 'scenario' and 'model'.") # Confirm the same models have data for all scenarios check = da.notnull().any("time").all("scenario") @@ -122,21 +119,14 @@ def hawkins_sutton( # Fit 4th order polynomial to smooth natural fluctuations # Note that the order of the polynomial has a substantial influence on the results. fit = da.polyfit(dim="time", deg=4, skipna=True) - sm = xr.polyval(coord=da.time, coeffs=fit.polyfit_coefficients).where( - da.notnull() - ) + sm = xr.polyval(coord=da.time, coeffs=fit.polyfit_coefficients).where(da.notnull()) # Decadal mean residuals res = (da - sm).rolling(time=10, center=True).mean() # Individual model variance after 2000: V # Note that the historical data is the same for all scenarios. - nv_u = ( - res.sel(time=slice("2000", None)) - .var(dim=("scenario", "time")) - .weighted(weights) - .mean("model") - ) + nv_u = res.sel(time=slice("2000", None)).var(dim=("scenario", "time")).weighted(weights).mean("model") # Compute baseline average ref = sm.sel(time=slice(*baseline)).mean(dim="time") @@ -241,25 +231,16 @@ def lafferty_sriver( raise ValueError("This algorithm expects annual time series.") if not {"time", "scenario", "model", "downscaling"}.issubset(da.dims): - raise ValueError( - "DataArray dimensions should include 'time', 'scenario', 'downscaling' and 'model'." - ) + raise ValueError("DataArray dimensions should include 'time', 'scenario', 'downscaling' and 'model'.") if sm is None: # Fit a 4th order polynomial fit = da.polyfit(dim="time", deg=4, skipna=True) - sm = xr.polyval(coord=da.time, coeffs=fit.polyfit_coefficients).where( - da.notnull() - ) + sm = xr.polyval(coord=da.time, coeffs=fit.polyfit_coefficients).where(da.notnull()) # "Interannual variability is then estimated as the centered rolling 11-year variance of the difference # between the extracted forced response and the raw outputs, averaged over all outputs." - nv_u = ( - (da - sm) - .rolling(time=11, center=True) - .var() - .mean(dim=["scenario", "model", "downscaling"]) - ) + nv_u = (da - sm).rolling(time=11, center=True).var().mean(dim=["scenario", "model", "downscaling"]) # Scenario uncertainty: U_s(t) if bb13: @@ -278,17 +259,13 @@ def lafferty_sriver( # Downscaling uncertainty: U_d(t) dw = sm.count("downscaling") - downscaling_u = ( - sm.var(dim="downscaling").weighted(dw).mean(dim=["scenario", "model"]) - ) + downscaling_u = sm.var(dim="downscaling").weighted(dw).mean(dim=["scenario", "model"]) # Total uncertainty: T(t) total = nv_u + scenario_u + model_u + downscaling_u # Create output array with the uncertainty components - u = pd.Index( - ["model", "scenario", "downscaling", "variability", "total"], name="uncertainty" - ) + u = pd.Index(["model", "scenario", "downscaling", "variability", "total"], name="uncertainty") uncertainty = xr.concat([model_u, scenario_u, downscaling_u, nv_u, total], dim=u) # Keep a trace of the elements for each uncertainty component @@ -365,9 +342,7 @@ def general_partition( if sm == "poly": # Fit a 4th order polynomial fit = da.polyfit(dim="time", deg=4, skipna=True) - sm = xr.polyval(coord=da.time, coeffs=fit.polyfit_coefficients).where( - da.notnull() - ) + sm = xr.polyval(coord=da.time, coeffs=fit.polyfit_coefficients).where(da.notnull()) elif isinstance(sm, xr.DataArray): sm = sm else: diff --git a/src/xclim/ensembles/_reduce.py b/src/xclim/ensembles/_reduce.py index 72d501184..f8b9436b1 100644 --- a/src/xclim/ensembles/_reduce.py +++ b/src/xclim/ensembles/_reduce.py @@ -58,10 +58,10 @@ def make_criteria(ds: xarray.Dataset | xarray.DataArray): ds2 = crit.unstack("criteria").to_dataset("variables") `ds2` will have all variables with the same dimensions, so if the original dataset had variables with different - dimensions, the added dimensions are filled with NaNs. - Also, note that criteria that are all NaN (such as lat/lon coordinates with no data) are dropped from `crit` to avoid issues with - the clustering algorithms, so the original dataset might not be able to be fully reconstructed. - The `to_dataset` part can be skipped if the original input was a DataArray. + dimensions, the added dimensions are filled with NaNs. Also, note that criteria that are all NaN (such as lat/lon + coordinates with no data) are dropped from `crit` to avoid issues with the clustering algorithms, so the original + dataset might not be able to be fully reconstructed. The `to_dataset` part can be skipped if the original input + was a DataArray. """ def _make_crit(da): @@ -69,22 +69,14 @@ def _make_crit(da): return da.stack(criteria=set(da.dims) - {"realization"}) if isinstance(ds, xarray.Dataset): - # When variables have different set of dims, missing dims on one variable results in duplicated values when a simple stack is done. - # To avoid that: stack each variable independently add a new "variables" dim - stacked = { - da.name: _make_crit(da.expand_dims(variables=[da.name])) - for da in ds.data_vars.values() - } + # When variables have different set of dims, missing dims on one variable results in duplicated values + # when a simple stack is done. To avoid that: stack each variable independently add a new "variables" dim + stacked = {da.name: _make_crit(da.expand_dims(variables=[da.name])) for da in ds.data_vars.values()} # Get name of all stacked coords - stacked_coords = set.union( - *[set(da.indexes["criteria"].names) for da in stacked.values()] - ) + stacked_coords = set.union(*[set(da.indexes["criteria"].names) for da in stacked.values()]) # Concat the variables by dropping old stacked index and related coords crit = xarray.concat( - [ - da.reset_index("criteria").drop_vars(stacked_coords, errors="ignore") - for k, da in stacked.items() - ], + [da.reset_index("criteria").drop_vars(stacked_coords, errors="ignore") for k, da in stacked.items()], "criteria", ) # Reconstruct proper stacked coordinates. When a variable is missing one of the coords, give NaNss @@ -92,23 +84,14 @@ def _make_crit(da): ( crd, np.concatenate( - [ - ( - da[crd].values - if crd in da.coords - else [np.nan] * da.criteria.size - ) - for da in stacked.values() - ], + [(da[crd].values if crd in da.coords else [np.nan] * da.criteria.size) for da in stacked.values()], ), ) for crd in stacked_coords ] crit = crit.assign_coords( xarray.Coordinates.from_pandas_multiindex( - pd.MultiIndex.from_arrays( - [arr for name, arr in coords], names=[name for name, arr in coords] - ), + pd.MultiIndex.from_arrays([arr for name, arr in coords], names=[name for name, arr in coords]), "criteria", ) ) @@ -152,7 +135,8 @@ def kkz_reduce_ensemble( Any distance metric name accepted by `scipy.spatial.distance.cdist`. standardize : bool Whether to standardize the input before running the selection or not. - Standardization consists in translation as to have a zero mean and scaling as to have a unit standard deviation. + Standardization consists in translation as to have a zero mean and scaling as to have a unit + standard deviation. **cdist_kwargs : Any All extra arguments are passed as-is to `scipy.spatial.distance.cdist`, see its docs for more information. @@ -211,8 +195,8 @@ def kmeans_reduce_ensemble( The algorithm attempts to reduce the total number of ensemble members while maintaining adequate coverage of the ensemble uncertainty in an N-dimensional data space. K-Means clustering is carried out on the input - selection criteria data-array in order to group individual ensemble members into a reduced number of similar groups. - Subsequently, a single representative simulation is retained from each group. + selection criteria data-array in order to group individual ensemble members into a reduced number + of similar groups. Subsequently, a single representative simulation is retained from each group. Parameters ---------- @@ -227,11 +211,12 @@ def kmeans_reduce_ensemble( Defaults to True if matplotlib is installed in the runtime environment. max_clusters : int, optional Maximum number of members to include in the output ensemble selection. - When using 'rsq_optimize' or 'rsq_cutoff' methods, limit the final selection to a maximum number even if method - results indicate a higher value. Defaults to N. + When using 'rsq_optimize' or 'rsq_cutoff' methods, limit the final selection to a maximum number + even if method results indicate a higher value. Defaults to N. variable_weights : np.ndarray, optional An array of size P. - This weighting can be used to influence of weight of the climate indices (criteria dimension) on the clustering itself. + This weighting can be used to influence of weight of the climate indices (criteria dimension) + on the clustering itself. model_weights : np.ndarray, optional An array of size N. This weighting can be used to influence which realization is selected from within each cluster. This parameter has no influence on the clustering itself. @@ -240,7 +225,8 @@ def kmeans_reduce_ensemble( This weighting can be used to influence of weight of simulations on the clustering itself. See: https://scikit-learn.org/stable/modules/generated/sklearn.cluster.KMeans.html. random_state : int or np.random.RandomState, optional - A sklearn.cluster.KMeans() random_state parameter. Determines random number generation for centroid initialization. + A sklearn.cluster.KMeans() random_state parameter. + Determines random number generation for centroid initialization. Use to make the randomness deterministic. See: https://scikit-learn.org/stable/modules/generated/sklearn.cluster.KMeans.html. @@ -363,9 +349,7 @@ def kmeans_reduce_ensemble( z = z * variable_weights rsq = _calc_rsq(z, method, make_graph, n_sim, random_state, sample_weights) - n_clusters = _get_nclust( - method=method, n_sim=n_sim, rsq=rsq, max_clusters=max_clusters - ) + n_clusters = _get_nclust(method=method, n_sim=n_sim, rsq=rsq, max_clusters=max_clusters) if make_graph: fig_data["method"] = method @@ -374,27 +358,19 @@ def kmeans_reduce_ensemble( fig_data["realizations"] = n_sim # Final k-means clustering with 1000 iterations to avoid instabilities in the choice of final scenarios - kmeans = KMeans( - n_clusters=n_clusters, n_init=1000, max_iter=600, random_state=random_state - ) + kmeans = KMeans(n_clusters=n_clusters, n_init=1000, max_iter=600, random_state=random_state) # we use 'fit_' only once, otherwise it computes everything again clusters = kmeans.fit_predict(z, sample_weight=sample_weights) # squared distance to centroids - d = np.square( - kmeans.transform(z) - ) # squared distance between each point and each centroid + d = np.square(kmeans.transform(z)) # squared distance between each point and each centroid - out = np.empty( - shape=n_clusters - ) # prepare an empty array in which to store the results + out = np.empty(shape=n_clusters) # prepare an empty array in which to store the results r = np.arange(n_sim) # in each cluster, find the closest (weighted) simulation and select it for i in range(n_clusters): - d_i = d[ - clusters == i, i - ] # distance to the centroid for all simulations within the cluster 'i' + d_i = d[clusters == i, i] # distance to the centroid for all simulations within the cluster 'i' if d_i.shape[0] >= 2: if d_i.shape[0] == 2: sig = 1 @@ -403,18 +379,14 @@ def kmeans_reduce_ensemble( d_i, ddof=1 ) # standard deviation of those distances (ddof = 1 gives the same as Matlab's std function) - like = ( - scipy.stats.norm.pdf(d_i, 0, sig) * model_weights[clusters == i] - ) # weighted likelihood + like = scipy.stats.norm.pdf(d_i, 0, sig) * model_weights[clusters == i] # weighted likelihood argmax = np.argmax(like) # index of the maximum likelihood else: argmax = 0 - r_clust = r[ - clusters == i - ] # index of the cluster simulations within the full ensemble + r_clust = r[clusters == i] # index of the cluster simulations within the full ensemble out[i] = r_clust[argmax] @@ -424,9 +396,7 @@ def kmeans_reduce_ensemble( return out, clusters, fig_data -def _calc_rsq( - z, method: dict, make_graph: bool, n_sim: np.ndarray, random_state, sample_weights -): +def _calc_rsq(z, method: dict, make_graph: bool, n_sim: np.ndarray, random_state, sample_weights): """ Sub-function to kmeans_reduce_ensemble. @@ -464,9 +434,7 @@ def _get_nclust(method: dict, n_sim: int, rsq: float, max_clusters: int): elif list(method.keys())[0] == "rsq_optimize": # create constant benefits curve (one to one) - onetoone = -1 * (1.0 / (n_sim - 1)) + np.arange(1, n_sim + 1) * ( - 1.0 / (n_sim - 1) - ) + onetoone = -1 * (1.0 / (n_sim - 1)) + np.arange(1, n_sim + 1) * (1.0 / (n_sim - 1)) n_clusters = np.argmax(rsq - onetoone) + 1 @@ -547,9 +515,7 @@ def plot_rsqprofile(fig_data: dict) -> None: ) plt.legend(loc="lower right") elif "rsq_optimize" in fig_data["method"].keys(): - onetoone = -1 * (1.0 / (n_sim - 1)) + np.arange(1, n_sim + 1) * ( - 1.0 / (n_sim - 1) - ) + onetoone = -1 * (1.0 / (n_sim - 1)) + np.arange(1, n_sim + 1) * (1.0 / (n_sim - 1)) plt.plot( range(1, n_sim + 1), onetoone, diff --git a/src/xclim/ensembles/_robustness.py b/src/xclim/ensembles/_robustness.py index 4328060cc..a175fb461 100644 --- a/src/xclim/ensembles/_robustness.py +++ b/src/xclim/ensembles/_robustness.py @@ -33,12 +33,13 @@ New tests must be decorated with :py:func:`significance_test` and fulfill the following requirements: -- Function name should begin by "_", registered test name is the function name without its first character and with _ replaced by -. -- Function must accept 2 positional arguments : fut and ref (see :py:func:`robustness_fractions` for definitions) +- Function name should begin with "_", test name is the function name without its first character, "_" replaced by "-". +- Function must accept 2 positional arguments : fut and ref (see :py:func:`robustness_fractions` for definitions). - Function may accept other keyword-only arguments. -- Function must return 2 values : +- Function must return two (2) values : + `changed` : 1D boolean array along `realization`. True for realization with significant change. - + `pvals` : 1D float array along `realization`. P-values of the statistical test. Should be `None` for test where is doesn't apply. + + `pvals` : 1D float array along `realization`. + P-values of the statistical test. Should be `None` for test where is doesn't apply. """ @@ -76,7 +77,9 @@ def robustness_fractions( # noqa: C901 **kwargs, ) -> xr.Dataset: r""" - Calculate robustness statistics qualifying how members of an ensemble agree on the existence of change and on its sign. + Calculate robustness statistics. + + The metric for qualifying how members of an ensemble agree on the existence of change and on its sign. Parameters ---------- @@ -106,25 +109,30 @@ def robustness_fractions( # noqa: C901 Passing `test=None` yields change_frac = 1 everywhere. Same type as `fut`. - positive - - The weighted fraction of valid members showing strictly positive change, no matter if it is significant or not. + - The weighted fraction of valid members showing strictly positive change, + no matter if it is significant or not. - changed_positive - The weighted fraction of valid members showing significant and positive change. - negative - - The weighted fraction of valid members showing strictly negative change, no matter if it is significant or not. + - The weighted fraction of valid members showing strictly negative change, + no matter if it is significant or not. - changed_negative - The weighted fraction of valid members showing significant and negative change. - agree - - The weighted fraction of valid members agreeing on the sign of change. It is the maximum between positive, negative and the rest. + - The weighted fraction of valid members agreeing on the sign of change. + It is the maximum between positive, negative and the rest. - valid - - The weighted fraction of valid members. A member is valid is there are no NaNs along the time axes of `fut` and `ref`. + - The weighted fraction of valid members. + A member is valid is there are no NaNs along the time axes of `fut` and `ref`. - pvals - - The p-values estimated by the significance tests. Only returned if the test uses `pvals`. Has the `realization` dimension. + - The p-values estimated by the significance tests. + Only returned if the test uses `pvals`. Has the `realization` dimension. Notes ----- @@ -195,9 +203,7 @@ def robustness_fractions( # noqa: C901 delta = fut valid = delta.notnull() if test not in [None, "threshold"]: - raise ValueError( - "When deltas are given (ref=None), 'test' must be None or 'threshold'." - ) + raise ValueError("When deltas are given (ref=None), 'test' must be None or 'threshold'.") else: delta = fut.mean("time") - ref.mean("time") valid = fut.notnull().all("time") & ref.notnull().all("time") @@ -216,9 +222,7 @@ def robustness_fractions( # noqa: C901 changed = abs(delta / ref.mean("time")) > rel_thresh test_params = {"rel_thresh": rel_thresh} else: - raise ValueError( - "One and only one of abs_thresh or rel_thresh must be given if test='threshold'." - ) + raise ValueError("One and only one of abs_thresh or rel_thresh must be given if test='threshold'.") pvals = None elif test in SIGNIFICANCE_TESTS: test_func = SIGNIFICANCE_TESTS[test] @@ -230,36 +234,25 @@ def robustness_fractions( # noqa: C901 changed, pvals = test_func(fut, ref, **test_params) else: - raise ValueError( - f"Statistical test {test} must be one of {', '.join(SIGNIFICANCE_TESTS.keys())}." - ) + raise ValueError(f"Statistical test {test} must be one of {', '.join(SIGNIFICANCE_TESTS.keys())}.") valid_frac = valid.weighted(w).sum(realization) / fut[realization].size n_valid = valid.weighted(w).sum(realization) change_frac = changed.where(valid).weighted(w).sum(realization) / n_valid pos_frac = (delta > 0).where(valid).weighted(w).sum(realization) / n_valid neg_frac = (delta < 0).where(valid).weighted(w).sum(realization) / n_valid - change_pos_frac = ((delta > 0) & changed).where(valid).weighted(w).sum( - realization - ) / n_valid - change_neg_frac = ((delta < 0) & changed).where(valid).weighted(w).sum( - realization - ) / n_valid - agree_frac = xr.concat((pos_frac, neg_frac, 1 - pos_frac - neg_frac), "sign").max( - "sign" - ) + change_pos_frac = ((delta > 0) & changed).where(valid).weighted(w).sum(realization) / n_valid + change_neg_frac = ((delta < 0) & changed).where(valid).weighted(w).sum(realization) / n_valid + agree_frac = xr.concat((pos_frac, neg_frac, 1 - pos_frac - neg_frac), "sign").max("sign") # Metadata kwargs_str = gen_call_string("", **test_params)[1:-1] - test_str = ( - f"Significant change was tested with test {test} and parameters {kwargs_str}." - ) + test_str = f"Significant change was tested with test {test} and parameters {kwargs_str}." out = xr.Dataset( { "changed": change_frac.assign_attrs( - description="Fraction of members showing significant change. " - + test_str, + description="Fraction of members showing significant change. " + test_str, units="", test=str(test), ), @@ -268,8 +261,7 @@ def robustness_fractions( # noqa: C901 units="", ), "changed_positive": change_pos_frac.assign_attrs( - description="Fraction of valid members showing significant and positive change. " - + test_str, + description="Fraction of valid members showing significant and positive change. " + test_str, units="", test=str(test), ), @@ -278,8 +270,7 @@ def robustness_fractions( # noqa: C901 units="", ), "changed_negative": change_neg_frac.assign_attrs( - description="Fraction of valid members showing significant and negative change. " - + test_str, + description="Fraction of valid members showing significant and negative change. " + test_str, units="", test=str(test), ), @@ -386,26 +377,20 @@ def robustness_categories( elif not chg_op: cond = compare(agree, agr_op, agr_thresh) else: - cond = compare(changed, chg_op, chg_thresh) & compare( - agree, agr_op, agr_thresh - ) + cond = compare(changed, chg_op, chg_thresh) & compare(agree, agr_op, agr_thresh) robustness = xr.where(~cond, robustness, i, keep_attrs=True) robustness = robustness.assign_attrs( flag_values=list(range(1, len(categories) + 1)), _FillValue=99, flag_descriptions=categories, - flag_meanings=" ".join( - map(lambda cat: cat.casefold().replace(" ", "_"), categories) - ), + flag_meanings=" ".join(map(lambda cat: cat.casefold().replace(" ", "_"), categories)), ) return robustness @update_xclim_history -def robustness_coefficient( - fut: xr.DataArray | xr.Dataset, ref: xr.DataArray | xr.Dataset -) -> xr.DataArray | xr.Dataset: +def robustness_coefficient(fut: xr.DataArray | xr.Dataset, ref: xr.DataArray | xr.Dataset) -> xr.DataArray | xr.Dataset: """ Calculate the robustness coefficient quantifying the robustness of a climate change signal in an ensemble. @@ -483,7 +468,8 @@ def diff_cdf_sq_area_int(x1, x2): name="R", long_name="Ensemble robustness coefficient", description="Ensemble robustness coefficient as defined by Knutti and Sedláček (2013).", - reference="Knutti, R. and Sedláček, J. (2013) Robustness and uncertainties in the new CMIP5 climate model projections. Nat. Clim. Change.", + reference="Knutti, R. and Sedláček, J. (2013) Robustness and uncertainties in the new CMIP5 climate " + "model projections. Nat. Clim. Change.", units="", ) return R @@ -496,7 +482,8 @@ def _ttest(fut, ref, *, p_change=0.05): The future values are compared against the reference mean (over 'time'). - Accepts argument p_change (float, default : 0.05) the p-value threshold for rejecting the hypothesis of no significant change. + Accepts argument p_change (float, default : 0.05) the p-value threshold for rejecting the hypothesis + of no significant change. """ def _ttest_func(f, r): @@ -627,9 +614,7 @@ def _ipcc_ar6_c(fut, ref, *, ref_pi=None): gamma = np.sqrt(2 / 20) * 1.645 * ref_detrended.std("time") else: ref_detrended = detrend(refy, dim="time", deg=2) - gamma = ( - np.sqrt(2) * 1.645 * ref_detrended.resample(time="20YS").mean().std("time") - ) + gamma = np.sqrt(2) * 1.645 * ref_detrended.resample(time="20YS").mean().std("time") delta = fut.mean("time") - ref.mean("time") changed = abs(delta) > gamma diff --git a/src/xclim/indicators/atmos/_conversion.py b/src/xclim/indicators/atmos/_conversion.py index 66f83cd8e..5888344e9 100644 --- a/src/xclim/indicators/atmos/_conversion.py +++ b/src/xclim/indicators/atmos/_conversion.py @@ -390,9 +390,7 @@ def cfcheck(self, **das) -> None: description=( "Precipitation minus potential evapotranspiration as a measure of an approximated surface water budget." ), - abstract=( - "Precipitation minus potential evapotranspiration as a measure of an approximated surface water budget." - ), + abstract=("Precipitation minus potential evapotranspiration as a measure of an approximated surface water budget."), compute=indices.water_budget, parameters={"method": "dummy"}, ) diff --git a/src/xclim/indicators/atmos/_temperature.py b/src/xclim/indicators/atmos/_temperature.py index cc1d529f7..af33b0704 100644 --- a/src/xclim/indicators/atmos/_temperature.py +++ b/src/xclim/indicators/atmos/_temperature.py @@ -215,8 +215,8 @@ class TempHourlyWithIndexing(ResamplingIndicatorWithIndexing): standard_name="number_of_days_with_air_temperature_above_threshold", long_name="Number of days with daily minimum above {thresh_tasmin} " "and daily maximum temperatures above {thresh_tasmax}", - description="{freq} number of days where daily maximum temperature exceeds {thresh_tasmax} and minimum temperature " - "exceeds {thresh_tasmin}.", + description="{freq} number of days where daily maximum temperature exceeds {thresh_tasmax} and minimum " + "temperature exceeds {thresh_tasmin}.", abstract="Number of days with daily maximum and minimum temperatures above given thresholds.", cell_methods="", compute=indices.tx_tn_days_above, @@ -242,11 +242,12 @@ class TempHourlyWithIndexing(ResamplingIndicatorWithIndexing): title="Hot spell maximum magnitude", identifier="hot_spell_max_magnitude", units="K d", - long_name="Maximum cumulative difference between daily maximum temperature and {thresh} for days within a heat wave. " - "A heat wave is defined as a series of at least {window} consecutive days with daily maximum temperature above {thresh}.", - description="Magnitude of the most intensive heat wave per {freq}. The magnitude is the cumulative exceedance of daily " - "maximum temperature over {thresh}. A heat wave is defined as a series of at least {window} consecutive days with daily " - "maximum temperature above {thresh}", + long_name="Maximum cumulative difference between daily maximum temperature and {thresh} for days within " + "a heat wave. A heat wave is defined as a series of at least {window} consecutive days with daily maximum " + "temperature above {thresh}.", + description="Magnitude of the most intensive heat wave per {freq}. The magnitude is the cumulative " + "exceedance of daily maximum temperature over {thresh}. A heat wave is defined as a series of at least " + "{window} consecutive days with daily maximum temperature above {thresh}", abstract="Magnitude of the most intensive heat wave per {freq}. A heat wave occurs when daily maximum " "temperatures exceed given thresholds for a number of days.", cell_methods="", @@ -309,10 +310,10 @@ class TempHourlyWithIndexing(ResamplingIndicatorWithIndexing): long_name="Number of heat spells", description="{freq} number of heat spells events. A heat spell occurs when the {window}-day " "averages of daily minimum and maximum temperatures each exceed {thresh_tasmin} and {thresh_tasmax}. " - "All days of the {window}-day period are considered part of the spell. Gaps of fewer than {min_gap} day(s) are allowed " - "within a spell.", - abstract="Number of heat spells. A heat spell occurs when rolling averages of daily minimum and maximum temperatures exceed given " - "thresholds for a number of days.", + "All days of the {window}-day period are considered part of the spell. Gaps of fewer than {min_gap} day(s) " + "are allowed within a spell.", + abstract="Number of heat spells. A heat spell occurs when rolling averages of daily minimum and maximum" + "temperatures exceed given thresholds for a number of days.", cell_methods="", keywords="health,", compute=indices.generic.bivariate_spell_length_statistics, @@ -344,10 +345,10 @@ class TempHourlyWithIndexing(ResamplingIndicatorWithIndexing): long_name="Longest heat spell", description="{freq} maximum length of heat spells. A heat spell occurs when the {window}-day " "averages of daily minimum and maximum temperatures each exceed {thresh_tasmin} and {thresh_tasmax}. " - "All days of the {window}-day period are considered part of the spell. Gaps of fewer than {min_gap} day(s) are allowed " - "within a spell.", - abstract="The longest heat spell of a period. A heat spell occurs when rolling averages of daily minimum and maximum temperatures exceed given " - "thresholds for a number of days.", + "All days of the {window}-day period are considered part of the spell. " + "Gaps of fewer than {min_gap} day(s) are allowed within a spell.", + abstract="The longest heat spell of a period. A heat spell occurs when rolling averages of daily minimum " + "and maximum temperatures exceed given thresholds for a number of days.", compute=indices.generic.bivariate_spell_length_statistics, input={"data1": "tasmin", "data2": "tasmax"}, parameters=dict( @@ -377,10 +378,10 @@ class TempHourlyWithIndexing(ResamplingIndicatorWithIndexing): long_name="Total length of heat spells.", description="{freq} total length of heat spell events. " "A heat spell occurs when the {window}-day averages of daily minimum and maximum temperatures " - "each exceed {thresh_tasmin} and {thresh_tasmax}. All days of the {window}-day period are considered part of the spell." - "Gaps of fewer than {min_gap} day(s) are allowed within a spell.", - abstract="Total length of heat spells. A heat spell occurs when rolling averages of daily minimum and maximum temperatures exceed given " - "thresholds for a number of days.", + "each exceed {thresh_tasmin} and {thresh_tasmax}. All days of the {window}-day period are considered part " + "of the spell. Gaps of fewer than {min_gap} day(s) are allowed within a spell.", + abstract="Total length of heat spells. A heat spell occurs when rolling averages of daily minimum and " + "maximum temperatures exceed given thresholds for a number of days.", compute=indices.generic.bivariate_spell_length_statistics, input={"data1": "tasmin", "data2": "tasmax"}, parameters=dict( @@ -696,11 +697,11 @@ class TempHourlyWithIndexing(ResamplingIndicatorWithIndexing): units="days", long_name="Number of days where maximum daily temperatures are above {thresh_tasmax} " "and minimum daily temperatures are at or below {thresh_tasmin}", - description="{freq} number of days with a diurnal freeze-thaw cycle, where maximum daily temperatures are above " - "{thresh_tasmax} and minimum daily temperatures are at or below {thresh_tasmin}.", - abstract="The number of days with a freeze-thaw cycle. A freeze-thaw cycle is defined as a day where maximum daily " - "temperature is above a given threshold and minimum daily temperature is at or below a given threshold, " - "usually 0°C for both.", + description="{freq} number of days with a diurnal freeze-thaw cycle, where maximum daily temperatures " + "are above {thresh_tasmax} and minimum daily temperatures are at or below {thresh_tasmin}.", + abstract="The number of days with a freeze-thaw cycle. A freeze-thaw cycle is defined as a day where " + "maximum daily temperature is above a given threshold and minimum daily temperature is at or below a " + "given threshold, usually 0°C for both.", cell_methods="", compute=indices.multiday_temperature_swing, parameters={ @@ -744,11 +745,11 @@ class TempHourlyWithIndexing(ResamplingIndicatorWithIndexing): long_name="Average length of events where maximum daily temperatures are above {thresh_tasmax} " "and minimum daily temperatures are at or below {thresh_tasmin} for at least {window} consecutive day(s).", description="{freq} average length of freeze-thaw spells, where maximum daily temperatures are above " - "{thresh_tasmax} and minimum daily temperatures are at or below {thresh_tasmin} for at least {window} consecutive " - "day(s).", - abstract="Average length of daily freeze-thaw spells. A freeze-thaw spell is defined as a number of consecutive " - "days where maximum daily temperatures are above a given threshold and minimum daily temperatures are at or below " - "a given threshold, usually 0°C for both.", + "{thresh_tasmax} and minimum daily temperatures are at or below {thresh_tasmin} for at least {window} " + "consecutive day(s).", + abstract="Average length of daily freeze-thaw spells. A freeze-thaw spell is defined as a number of " + "consecutive days where maximum daily temperatures are above a given threshold and minimum daily temperatures " + "are at or below a given threshold, usually 0°C for both.", cell_methods="", compute=indices.multiday_temperature_swing, parameters={ @@ -768,11 +769,11 @@ class TempHourlyWithIndexing(ResamplingIndicatorWithIndexing): long_name="Maximal length of events where maximum daily temperatures are above {thresh_tasmax} " "and minimum daily temperatures are at or below {thresh_tasmin} for at least {window} consecutive day(s).", description="{freq} maximal length of freeze-thaw spells, where maximum daily temperatures are above " - "{thresh_tasmax} and minimum daily temperatures are at or below {thresh_tasmin} for at least {window} consecutive " - "day(s).", - abstract="Maximal length of daily freeze-thaw spells. A freeze-thaw spell is defined as a number of consecutive " - "days where maximum daily temperatures are above a given threshold and minimum daily temperatures are at or below " - "a threshold, usually 0°C for both.", + "{thresh_tasmax} and minimum daily temperatures are at or below {thresh_tasmin} for at least {window} " + "consecutive day(s).", + abstract="Maximal length of daily freeze-thaw spells. A freeze-thaw spell is defined as a number of " + "consecutive days where maximum daily temperatures are above a given threshold and minimum daily " + "temperatures are at or below a threshold, usually 0°C for both.", cell_methods="", compute=indices.multiday_temperature_swing, parameters={ @@ -860,10 +861,10 @@ class TempHourlyWithIndexing(ResamplingIndicatorWithIndexing): units="", standard_name="day_of_year", long_name="First day where temperature threshold of {thresh} is exceeded for at least {window} days", - description="Day of year of the spring freshet start, defined as the first day a temperature threshold of {thresh} " - "is exceeded for at least {window} days.", - abstract="Day of year of the spring freshet start, defined as the first day when the temperature exceeds a certain " - "threshold for a given number of consecutive days.", + description="Day of year of the spring freshet start, defined as the first day a temperature threshold " + "of {thresh} is exceeded for at least {window} days.", + abstract="Day of year of the spring freshet start, defined as the first day when the temperature exceeds " + "a certain threshold for a given number of consecutive days.", compute=indices.first_day_temperature_above, parameters={"thresh": {"default": "0 degC"}, "window": {"default": 5}}, ) @@ -1039,8 +1040,8 @@ class TempHourlyWithIndexing(ResamplingIndicatorWithIndexing): description="{freq} number of days between the first occurrence of at least {window} consecutive days " "with minimum daily temperature at or above {thresh} and the first occurrence of at least " "{window} consecutive days with minimum daily temperature below {thresh} after {mid_date}.", - abstract="Duration of the frost free season, defined as the period when the minimum daily temperature is above 0°C " - "without a freezing window of `N` days, with freezing occurring after a median calendar date.", + abstract="Duration of the frost free season, defined as the period when the minimum daily temperature " + "is above 0°C without a freezing window of `N` days, with freezing occurring after a median calendar date.", cell_methods="time: sum over days", compute=indices.frost_free_season_length, parameters={"thresh": {"default": "0 degC"}}, @@ -1052,9 +1053,10 @@ class TempHourlyWithIndexing(ResamplingIndicatorWithIndexing): units="", standard_name="day_of_year", long_name="First day following a period of {window} days with minimum daily temperature at or above {thresh}", - description="Day of the year of the beginning of the frost-free season, defined as the {window}th consecutive day " - "when minimum daily temperature exceeds {thresh}.", - abstract="First day when minimum daily temperature exceeds a given threshold for a given number of consecutive days", + description="Day of the year of the beginning of the frost-free season, defined as the {window}th consecutive " + "day when minimum daily temperature exceeds {thresh}.", + abstract="First day when minimum daily temperature exceeds a given threshold for a given number of " + "consecutive days", compute=indices.frost_free_season_start, parameters={"thresh": {"default": "0 degC"}}, ) @@ -1269,11 +1271,7 @@ class TempHourlyWithIndexing(ResamplingIndicatorWithIndexing): long_name="Day of year when the integral of mean daily temperature {op} {thresh} exceeds {sum_thresh}", description=lambda **kws: "Day of year when the integral of degree days (mean daily temperature {op} {thresh}) " "exceeds {sum_thresh}" - + ( - ", with the cumulative sum starting from {after_date}." - if kws["after_date"] is not None - else "." - ), + + (", with the cumulative sum starting from {after_date}." if kws["after_date"] is not None else "."), abstract="The day of the year when the sum of degree days exceeds a threshold, occurring after a given date. " "Degree days are calculated above or below a given temperature threshold.", cell_methods="", @@ -1447,8 +1445,7 @@ def cfcheck(self, tas: DataArray, snd: DataArray = None): units="days", standard_name="days_with_air_temperature_below_threshold", long_name="Number of days where the daily minimum temperature is below {thresh}", - description="{freq} number of days where the daily minimum temperature is below {thresh}" - "over the period {indexer}.", + description="{freq} number of days where the daily minimum temperature is below {thresh}over the period {indexer}.", abstract="Number of days where the daily minimum temperature is below a given threshold between a given" "start date and a given end date.", cell_methods="time: sum over days", @@ -1524,9 +1521,9 @@ def cfcheck(self, tas: DataArray, snd: DataArray = None): identifier="cu", units="", cell_methods="time: sum", - description="Chill units are a measure to estimate the bud breaking potential of different crops based on the Utah model developed in " - "Richardson et al. (1974). The Utah model assigns a weight to each hour depending on the temperature recognising that high temperatures can " - "actually decrease the potential for bud breaking.", + description="Chill units are a measure to estimate the bud breaking potential of different crops based on the " + "Utah model developed in Richardson et al. (1974). The Utah model assigns a weight to each hour depending on " + "the temperature recognising that high temperatures can actually decrease the potential for bud breaking.", long_name="Chill units after the Utah Model", allowed_periods=["Y"], compute=indices.chill_units, diff --git a/src/xclim/indices/_agro.py b/src/xclim/indices/_agro.py index e155f0c9b..6f6246e3f 100644 --- a/src/xclim/indices/_agro.py +++ b/src/xclim/indices/_agro.py @@ -260,9 +260,7 @@ def huglin_index( xarray.where( (46 < abs(lat)) & (abs(lat) <= 48), k_f[4], - xarray.where( - (48 < abs(lat)) & (abs(lat) <= 50), k_f[5], np.nan - ), + xarray.where((48 < abs(lat)) & (abs(lat) <= 50), k_f[5], np.nan), ), ), ), @@ -286,11 +284,7 @@ def huglin_index( hi: xarray.DataArray = (((tas + tasmax) / 2) - thresh).clip(min=0) * k hi = ( - select_time( - hi, date_bounds=(start_date, end_date), include_bounds=(True, False) - ) - .resample(time=freq) - .sum() + select_time(hi, date_bounds=(start_date, end_date), include_bounds=(True, False)).resample(time=freq).sum() * k_aggregated ) hi = hi.assign_attrs(units="") @@ -426,9 +420,7 @@ def biologically_effective_degree_days( if isinstance(lat, int | float): lat = xarray.DataArray(lat) lat_mask = abs(lat) <= 50 - k = 1 + xarray.where( - lat_mask, ((abs(lat) - 40) * 0.06 / 10).clip(0, None), 0 - ) + k = 1 + xarray.where(lat_mask, ((abs(lat) - 40) * 0.06 / 10).clip(0, None), 0) k_aggregated = 1 else: day_length = ( @@ -450,16 +442,12 @@ def biologically_effective_degree_days( else: raise NotImplementedError() - bedd: xarray.DataArray = ( - (((tasmin + tasmax) / 2) - thresh_tasmin).clip(min=0) * k + tr_adj - ).clip(max=max_daily_degree_days) + bedd: xarray.DataArray = ((((tasmin + tasmax) / 2) - thresh_tasmin).clip(min=0) * k + tr_adj).clip( + max=max_daily_degree_days + ) bedd = ( - select_time( - bedd, date_bounds=(start_date, end_date), include_bounds=(True, False) - ) - .resample(time=freq) - .sum() + select_time(bedd, date_bounds=(start_date, end_date), include_bounds=(True, False)).resample(time=freq).sum() * k_aggregated ) @@ -657,11 +645,7 @@ def dryness_index( # numpydoc ignore=SS05 raise ValueError(f"Freq not allowed: {freq}. Must be `YS` or `YS-JAN`") # Resample all variables to monthly totals in mm units. - evspsblpot = ( - amount2lwethickness(rate2amount(evspsblpot), out_units="mm") - .resample(time="MS") - .sum() - ) + evspsblpot = amount2lwethickness(rate2amount(evspsblpot), out_units="mm").resample(time="MS").sum() pr = amount2lwethickness(rate2amount(pr), out_units="mm").resample(time="MS").sum() wo = convert_units_to(wo, "mm") @@ -1023,9 +1007,7 @@ def rain_season( def _get_first_run(run_positions, start_date, end_date): run_positions = select_time(run_positions, date_bounds=(start_date, end_date)) first_start = run_positions.argmax("time") - return xarray.where( - first_start != run_positions.argmin("time"), first_start, np.nan - ) + return xarray.where(first_start != run_positions.argmin("time"), first_start, np.nan) # Find the start of the rain season def _get_first_run_start(_pram): @@ -1040,9 +1022,7 @@ def _get_first_run_start(_pram): da_stop = _pram <= thresh_dry_start window_dry = window_dry_start elif method_dry_start == "total": - da_stop = ( - _pram.rolling({"time": window_dry_start}).sum() <= thresh_dry_start - ) + da_stop = _pram.rolling({"time": window_dry_start}).sum() <= thresh_dry_start # equivalent to rolling forward in time instead, i.e. end date will be at beginning of dry run da_stop = da_stop.shift({"time": -(window_dry_start - 1)}, fill_value=False) window_dry = 1 @@ -1062,9 +1042,7 @@ def _get_first_run_end(_pram): da_stop = _pram <= thresh_dry_end run_positions = rl.rle(da_stop) >= window_dry_end elif method_dry_end == "total": - run_positions = ( - _pram.rolling({"time": window_dry_end}).sum() <= thresh_dry_end - ) + run_positions = _pram.rolling({"time": window_dry_end}).sum() <= thresh_dry_end else: raise ValueError(f"Unknown method_dry_end: {method_dry_end}.") return _get_first_run(run_positions, date_min_end, date_max_end) @@ -1105,12 +1083,8 @@ def _get_rain_season(_pram): # Compute rain season, attribute units out = cast(xarray.Dataset, pram.resample(time=freq).map(_get_rain_season)) - rain_season_start = out.rain_season_start.assign_attrs( - units="", is_dayofyear=np.int32(1) - ) - rain_season_end = out.rain_season_end.assign_attrs( - units="", is_dayofyear=np.int32(1) - ) + rain_season_start = out.rain_season_start.assign_attrs(units="", is_dayofyear=np.int32(1)) + rain_season_end = out.rain_season_end.assign_attrs(units="", is_dayofyear=np.int32(1)) rain_season_length = out.rain_season_length.assign_attrs(units="days") return rain_season_start, rain_season_end, rain_season_length @@ -1229,9 +1203,7 @@ def standardized_precipitation_index( if isinstance(dist, str): if dist in dist_methods: if method not in dist_methods[dist]: - raise NotImplementedError( - f"{method} method is not implemented for {dist} distribution" - ) + raise NotImplementedError(f"{method} method is not implemented for {dist} distribution") else: raise NotImplementedError(f"{dist} distribution is not yet implemented.") @@ -1326,9 +1298,7 @@ def standardized_precipitation_evapotranspiration_index( if isinstance(dist, str): if dist in dist_methods: if method not in dist_methods[dist]: - raise NotImplementedError( - f"{method} method is not implemented for {dist} distribution" - ) + raise NotImplementedError(f"{method} method is not implemented for {dist} distribution") else: raise NotImplementedError(f"{dist} distribution is not yet implemented.") @@ -1352,9 +1322,7 @@ def standardized_precipitation_evapotranspiration_index( @declare_units(tas="[temperature]") -def qian_weighted_mean_average( - tas: xarray.DataArray, dim: str = "time" -) -> xarray.DataArray: +def qian_weighted_mean_average(tas: xarray.DataArray, dim: str = "time") -> xarray.DataArray: r""" Binomial smoothed, five-day weighted mean average temperature. @@ -1391,9 +1359,7 @@ def qian_weighted_mean_average( units = tas.attrs["units"] weights = xarray.DataArray([0.0625, 0.25, 0.375, 0.25, 0.0625], dims=["window"]) - weighted_mean: xarray.DataArray = ( - tas.rolling({dim: 5}, center=True).construct("window").dot(weights) - ) + weighted_mean: xarray.DataArray = tas.rolling({dim: 5}, center=True).construct("window").dot(weights) weighted_mean = weighted_mean.assign_attrs(units=units) return weighted_mean @@ -1472,15 +1438,11 @@ def effective_growing_degree_days( tas.attrs["units"] = "degC" if method.lower() == "bootsma": - fda = first_day_temperature_above( - tas=tas, thresh=thresh_with_units, window=1, freq=freq - ) + fda = first_day_temperature_above(tas=tas, thresh=thresh_with_units, window=1, freq=freq) start = fda + 10 elif method.lower() == "qian": tas_weighted = qian_weighted_mean_average(tas=tas, dim=dim) - start = first_day_temperature_above( - tas_weighted, thresh=thresh_with_units, window=5, freq=freq - ) + start = first_day_temperature_above(tas_weighted, thresh=thresh_with_units, window=5, freq=freq) else: raise NotImplementedError(f"Method: {method}.") @@ -1497,9 +1459,7 @@ def effective_growing_degree_days( ) deg_days = (tas - thresh).clip(min=0) - egdd: xarray.DataArray = aggregate_between_dates( - deg_days, start=start, end=end, freq=freq - ) + egdd: xarray.DataArray = aggregate_between_dates(deg_days, start=start, end=end, freq=freq) egdd = to_agg_units(egdd, tas, op="integral") return egdd @@ -1544,14 +1504,10 @@ def hardiness_zones( # numpydoc ignore=SS05 zone_min, zone_max, zone_step = "-15 degC", "20 degC", "5 degC" else: - raise NotImplementedError( - f"Method must be one of `usda` or `anbg`. Got {method}." - ) + raise NotImplementedError(f"Method must be one of `usda` or `anbg`. Got {method}.") tn_min_rolling = tn_min(tasmin, freq=freq).rolling(time=window).mean() - zones: xarray.DataArray = get_zones( - tn_min_rolling, zone_min=zone_min, zone_max=zone_max, zone_step=zone_step - ) + zones: xarray.DataArray = get_zones(tn_min_rolling, zone_min=zone_min, zone_max=zone_max, zone_step=zone_step) zones = zones.assign_attrs(units="") return zones @@ -1583,9 +1539,7 @@ def _chill_portion_one_season(tas_K): inter_E = np.zeros_like(tas_K) for i in range(1, tas_K.shape[-1]): - inter_E[..., i] = _accumulate_intermediate( - inter_E[..., i - 1], xi[..., i - 1], xs[..., i], ak1[..., i] - ) + inter_E[..., i] = _accumulate_intermediate(inter_E[..., i - 1], xi[..., i - 1], xs[..., i], ak1[..., i]) delta = np.where(inter_E >= 1, inter_E * xi, 0) return delta @@ -1606,9 +1560,7 @@ def _apply_chill_portion_one_season(tas_K): @declare_units(tas="[temperature]") -def chill_portions( - tas: xarray.DataArray, freq: str = "YS", **indexer -) -> xarray.DataArray: +def chill_portions(tas: xarray.DataArray, freq: str = "YS", **indexer) -> xarray.DataArray: r""" Chill portion based on the dynamic model. @@ -1658,18 +1610,12 @@ def chill_portions( >>> tas_hourly = make_hourly_temperature(tasmin, tasmax) >>> cp = chill_portions(tasmin) """ - tas_K: xarray.DataArray = select_time( - convert_units_to(tas, "K"), drop=True, **indexer - ) - return resample_map( - tas_K, "time", freq, _apply_chill_portion_one_season - ).assign_attrs(units="") + tas_K: xarray.DataArray = select_time(convert_units_to(tas, "K"), drop=True, **indexer) + return resample_map(tas_K, "time", freq, _apply_chill_portion_one_season).assign_attrs(units="") @declare_units(tas="[temperature]") -def chill_units( - tas: xarray.DataArray, positive_only: bool = False, freq: str = "YS" -) -> xarray.DataArray: +def chill_units(tas: xarray.DataArray, positive_only: bool = False, freq: str = "YS") -> xarray.DataArray: """ Chill units using the Utah model. diff --git a/src/xclim/indices/_anuclim.py b/src/xclim/indices/_anuclim.py index 46d75d51c..5083256d7 100644 --- a/src/xclim/indices/_anuclim.py +++ b/src/xclim/indices/_anuclim.py @@ -63,9 +63,7 @@ @declare_units(tasmin="[temperature]", tasmax="[temperature]") -def isothermality( - tasmin: xarray.DataArray, tasmax: xarray.DataArray, freq: str = "YS" -) -> xarray.DataArray: +def isothermality(tasmin: xarray.DataArray, tasmax: xarray.DataArray, freq: str = "YS") -> xarray.DataArray: r""" Isothermality. @@ -104,9 +102,7 @@ def isothermality( @declare_units(tas="[temperature]") -def temperature_seasonality( - tas: xarray.DataArray, freq: str = "YS" -) -> xarray.DataArray: +def temperature_seasonality(tas: xarray.DataArray, freq: str = "YS") -> xarray.DataArray: r""" Temperature seasonality (coefficient of variation). @@ -265,9 +261,7 @@ def tg_mean_warmcold_quarter( out = _to_quarter(tas=tas) if op not in ["warmest", "coldest"]: - raise NotImplementedError( - f'op parameter ({op}) may only be one of "warmest", "coldest"' - ) + raise NotImplementedError(f'op parameter ({op}) may only be one of "warmest", "coldest"') oper = _np_ops[op] out = select_resample_op(out, oper, freq) @@ -322,9 +316,7 @@ def tg_mean_wetdry_quarter( pr_qrt = _to_quarter(pr=pr) if op not in ["wettest", "driest", "dryest"]: - raise NotImplementedError( - f'op parameter ({op}) may only be one of "wettest" or "driest"' - ) + raise NotImplementedError(f'op parameter ({op}) may only be one of "wettest" or "driest"') xr_op = _xr_argops[op] out = _from_other_arg(criteria=pr_qrt, output=tas_qrt, op=xr_op, freq=freq) @@ -332,9 +324,7 @@ def tg_mean_wetdry_quarter( @declare_units(pr="[precipitation]") -def prcptot_wetdry_quarter( - pr: xarray.DataArray, op: str, freq: str = "YS" -) -> xarray.DataArray: +def prcptot_wetdry_quarter(pr: xarray.DataArray, op: str, freq: str = "YS") -> xarray.DataArray: r""" Total precipitation of wettest/driest quarter. @@ -379,9 +369,7 @@ def prcptot_wetdry_quarter( pr_qrt = _to_quarter(pr=pr) if op not in ["wettest", "driest", "dryest"]: - raise NotImplementedError( - f'op parameter ({op}) may only be one of "wettest" or "driest"' - ) + raise NotImplementedError(f'op parameter ({op}) may only be one of "wettest" or "driest"') op = _np_ops[op] out = select_resample_op(pr_qrt, op, freq) @@ -436,9 +424,7 @@ def prcptot_warmcold_quarter( pr_qrt = _to_quarter(pr=pr) if op not in ["warmest", "coldest"]: - raise NotImplementedError( - f'op parameter ({op}) may only be one of "warmest", "coldest"' - ) + raise NotImplementedError(f'op parameter ({op}) may only be one of "warmest", "coldest"') xr_op = _xr_argops[op] out = _from_other_arg(criteria=tas_qrt, output=pr_qrt, op=xr_op, freq=freq) @@ -447,9 +433,7 @@ def prcptot_warmcold_quarter( @declare_units(pr="[precipitation]", thresh="[precipitation]") -def prcptot( - pr: xarray.DataArray, thresh: Quantified = "0 mm/d", freq: str = "YS" -) -> xarray.DataArray: +def prcptot(pr: xarray.DataArray, thresh: Quantified = "0 mm/d", freq: str = "YS") -> xarray.DataArray: r""" Accumulated total precipitation. @@ -477,9 +461,7 @@ def prcptot( @declare_units(pr="[precipitation]") -def prcptot_wetdry_period( - pr: xarray.DataArray, *, op: str, freq: str = "YS" -) -> xarray.DataArray: +def prcptot_wetdry_period(pr: xarray.DataArray, *, op: str, freq: str = "YS") -> xarray.DataArray: r""" Precipitation of the wettest/driest day, week, or month, depending on the time step. @@ -513,9 +495,7 @@ def prcptot_wetdry_period( pram = rate2amount(pr) if op not in ["wettest", "driest", "dryest"]: - raise NotImplementedError( - f'op parameter ({op}) may only be one of "wettest" or "driest"' - ) + raise NotImplementedError(f'op parameter ({op}) may only be one of "wettest" or "driest"') op = _np_ops[op] pwp: xarray.DataArray = getattr(pram.resample(time=freq), op)(dim="time") @@ -530,9 +510,7 @@ def _anuclim_coeff_var(arr: xarray.DataArray, freq: str = "YS") -> xarray.DataAr return std / mu -def _from_other_arg( - criteria: xarray.DataArray, output: xarray.DataArray, op: Callable, freq: str -) -> xarray.DataArray: +def _from_other_arg(criteria: xarray.DataArray, output: xarray.DataArray, op: Callable, freq: str) -> xarray.DataArray: """ Pick values from output based on operation returning an index from criteria. @@ -607,18 +585,14 @@ def _to_quarter( # Accumulate on a week # Ensure units are back to a "rate" for rate2amount below ts_var = precip_accumulation(ts_var, freq="7D") - ts_var = convert_units_to(ts_var, "mm", context="hydro").assign_attrs( - units="mm/week" - ) + ts_var = convert_units_to(ts_var, "mm", context="hydro").assign_attrs(units="mm/week") freq_upper = "W" if freq_upper.startswith("W"): window = 13 elif freq_upper.startswith("M"): window = 3 else: - raise NotImplementedError( - f'Unknown input time frequency "{freq}": must be one of "D", "W" or "M".' - ) + raise NotImplementedError(f'Unknown input time frequency "{freq}": must be one of "D", "W" or "M".') ts_var = ensure_chunk_size(ts_var, time=np.ceil(window / 2)) if tas is not None: diff --git a/src/xclim/indices/_conversion.py b/src/xclim/indices/_conversion.py index 98a4eb0ee..8f8c0fc7d 100644 --- a/src/xclim/indices/_conversion.py +++ b/src/xclim/indices/_conversion.py @@ -78,8 +78,8 @@ def humidex( tdps : xarray.DataArray, optional Dewpoint Temperature, used to compute the vapour pressure. hurs : xarray.DataArray, optional - Relative Humidity, used as an alternative way to compute the vapour pressure if the dewpoint temperature is not - available. + Relative Humidity, used as an alternative way to compute the vapour pressure if the dewpoint + temperature is not available. Returns ------- @@ -103,8 +103,9 @@ def humidex( e = 6.112 \times \exp(5417.7530\left({\frac {1}{273.16}}-{\frac {1}{T_{\text{dewpoint}}}}\right) where the constant 5417.753 reflects the molecular weight of water, latent heat of vaporization, - and the universal gas constant :cite:p:`mekis_observed_2015`. Alternatively, the term :math:`e` can also be computed - from the relative humidity `h` expressed in percent using :cite:t:`sirangelo_combining_2020`: + and the universal gas constant :cite:p:`mekis_observed_2015`. + Alternatively, the term :math:`e` can also be computed from the relative humidity `h` expressed + in percent using :cite:t:`sirangelo_combining_2020`: .. math:: @@ -263,18 +264,18 @@ def uas_vas_2_sfcwind( wind : xr.DataArray, [m s-1] Wind Velocity. wind_from_dir : xr.DataArray, [°] - Direction from which the wind blows, following the meteorological convention where 360 stands for North and 0 for calm winds. + Direction from which the wind blows, following the meteorological convention where 360 stands + for North and 0 for calm winds. Notes ----- - Winds with a velocity less than `calm_wind_thresh` are given a wind direction of 0°, while stronger northerly winds are set to 360°. + Winds with a velocity less than `calm_wind_thresh` are given a wind direction of 0°, + while stronger northerly winds are set to 360°. Examples -------- >>> from xclim.indices import uas_vas_2_sfcwind - >>> sfcWind = uas_vas_2_sfcwind( - ... uas=uas_dataset, vas=vas_dataset, calm_wind_thresh="0.5 m/s" - ... ) + >>> sfcWind = uas_vas_2_sfcwind(uas=uas_dataset, vas=vas_dataset, calm_wind_thresh="0.5 m/s") """ # Converts the wind speed to m s-1 uas = convert_units_to(uas, "m/s") @@ -302,7 +303,8 @@ def uas_vas_2_sfcwind( @declare_units(sfcWind="[speed]", sfcWindfromdir="[]") def sfcwind_2_uas_vas( - sfcWind: xr.DataArray, sfcWindfromdir: xr.DataArray # noqa + sfcWind: xr.DataArray, + sfcWindfromdir: xr.DataArray, # noqa ) -> tuple[xr.DataArray, xr.DataArray]: """ Eastward and northward wind components from the wind speed and direction. @@ -326,9 +328,7 @@ def sfcwind_2_uas_vas( Examples -------- >>> from xclim.indices import sfcwind_2_uas_vas - >>> uas, vas = sfcwind_2_uas_vas( - ... sfcWind=sfcWind_dataset, sfcWindfromdir=sfcWindfromdir_dataset - ... ) + >>> uas, vas = sfcwind_2_uas_vas(sfcWind=sfcWind_dataset, sfcWindfromdir=sfcWindfromdir_dataset) """ # Converts the wind speed to m s-1 sfcWind = convert_units_to(sfcWind, "m/s") # noqa @@ -358,7 +358,7 @@ def saturation_vapor_pressure( tas: xr.DataArray, ice_thresh: Quantified | None = None, method: str = "sonntag90", -) -> xr.DataArray: +) -> xr.DataArray: # noqa: E501 """ Saturation vapour pressure from temperature. @@ -382,9 +382,11 @@ def saturation_vapor_pressure( In all cases implemented here :math:`log(e_{sat})` is an empirically fitted function (usually a polynomial) where coefficients can be different when ice is taken as reference instead of water. Available methods are: - - "goffgratch46" or "GG46", based on :cite:t:`goff_low-pressure_1946`, values and equation taken from :cite:t:`vomel_saturation_2016`. + - "goffgratch46" or "GG46", based on :cite:t:`goff_low-pressure_1946`, + values and equation taken from :cite:t:`vomel_saturation_2016`. - "sonntag90" or "SO90", taken from :cite:t:`sonntag_important_1990`. - - "tetens30" or "TE30", based on :cite:t:`tetens_uber_1930`, values and equation taken from :cite:t:`vomel_saturation_2016`. + - "tetens30" or "TE30", based on :cite:t:`tetens_uber_1930`, + values and equation taken from :cite:t:`vomel_saturation_2016`. - "wmo08" or "WMO08", taken from :cite:t:`world_meteorological_organization_guide_2008`. - "its90" or "ITS90", taken from :cite:t:`hardy_its-90_1998`. @@ -395,9 +397,7 @@ def saturation_vapor_pressure( Examples -------- >>> from xclim.indices import saturation_vapor_pressure - >>> rh = saturation_vapor_pressure( - ... tas=tas_dataset, ice_thresh="0 degC", method="wmo08" - ... ) + >>> rh = saturation_vapor_pressure(tas=tas_dataset, ice_thresh="0 degC", method="wmo08") """ if ice_thresh is not None: thresh = convert_units_to(ice_thresh, "K") @@ -485,9 +485,7 @@ def saturation_vapor_pressure( ), ) else: - raise ValueError( - f"Method {method} is not in ['sonntag90', 'tetens30', 'goffgratch46', 'wmo08', 'its90']" - ) + raise ValueError(f"Method {method} is not in ['sonntag90', 'tetens30', 'goffgratch46', 'wmo08', 'its90']") e_sat = e_sat.assign_attrs(units="Pa") return e_sat @@ -554,8 +552,8 @@ def relative_humidity( r""" Relative humidity. - Compute relative humidity from temperature and either dewpoint temperature or specific humidity and pressure through - the saturation vapour pressure. + Compute relative humidity from temperature and either dewpoint temperature or specific humidity + and pressure through the saturation vapour pressure. Parameters ---------- @@ -615,7 +613,8 @@ def relative_humidity( w = \frac{q}{1-q} w_{sat} = 0.622\frac{e_{sat}}{P - e_{sat}} - The methods differ by how :math:`e_{sat}` is computed. See the doc of :py:func:`xclim.core.utils.saturation_vapor_pressure`. + The methods differ by how :math:`e_{sat}` is computed. + See the doc of :py:func:`xclim.core.utils.saturation_vapor_pressure`. References ---------- @@ -644,12 +643,8 @@ def relative_humidity( Rw = (461.5,) hurs = 100 * np.exp(-L * (tas - tdps) / (Rw * tas * tdps)) # type: ignore elif tdps is not None: - e_sat_dt = saturation_vapor_pressure( - tas=tdps, ice_thresh=ice_thresh, method=method - ) - e_sat_t = saturation_vapor_pressure( - tas=tas, ice_thresh=ice_thresh, method=method - ) + e_sat_dt = saturation_vapor_pressure(tas=tdps, ice_thresh=ice_thresh, method=method) + e_sat_t = saturation_vapor_pressure(tas=tas, ice_thresh=ice_thresh, method=method) hurs = 100 * e_sat_dt / e_sat_t # type: ignore elif huss is not None and ps is not None: ps = convert_units_to(ps, "Pa") @@ -898,9 +893,7 @@ def snowfall_approximation( thresh = convert_units_to(thresh, tas) # Interpolate fraction over temperature (in units of tas) - t = xr.DataArray( - [-np.inf, thresh, upper, np.inf], dims=("tas",), attrs={"units": "degC"} - ) + t = xr.DataArray([-np.inf, thresh, upper, np.inf], dims=("tas",), attrs={"units": "degC"}) fraction = xr.DataArray([1.0, 1.0, 0.0, 0.0], dims=("tas",), coords={"tas": t}) # Multiply precip by snowfall fraction @@ -910,9 +903,7 @@ def snowfall_approximation( dtas = convert_units_to(tas, "K") - convert_units_to(thresh, "K") # Create nodes for the snowfall fraction: -inf, thresh, ..., thresh+6, inf [degC] - t = np.concatenate( - [[-273.15], np.linspace(0, 6, 100, endpoint=False), [6, 1e10]] - ) + t = np.concatenate([[-273.15], np.linspace(0, 6, 100, endpoint=False), [6, 1e10]]) t = xr.DataArray(t, dims="tas", name="tas", coords={"tas": t}) # The polynomial coefficients, valid between thresh and thresh + 6 (defined in CLASS) @@ -974,9 +965,7 @@ def rain_approximation( This method computes the snowfall approximation and subtracts it from the total precipitation to estimate the liquid rain precipitation. """ - prra: xr.DataArray = pr - snowfall_approximation( - pr, tas, thresh=thresh, method=method - ) + prra: xr.DataArray = pr - snowfall_approximation(pr, tas, thresh=thresh, method=method) prra = prra.assign_attrs(units=pr.attrs["units"]) return prra @@ -1018,9 +1007,7 @@ def snd_to_snw( :cite:cts:`sturm_swe_2010` """ density = snr if (snr is not None) else const - snw: xr.DataArray = rate2flux(snd, density=density, out_units=out_units).rename( - "snw" - ) + snw: xr.DataArray = rate2flux(snd, density=density, out_units=out_units).rename("snw") # TODO: Leave this operation to rate2flux? Maybe also the variable renaming above? snw = snw.assign_attrs(standard_name="surface_snow_amount") return snw @@ -1062,16 +1049,12 @@ def snw_to_snd( :cite:cts:`sturm_swe_2010` """ density = snr if (snr is not None) else const - snd: xr.DataArray = flux2rate(snw, density=density, out_units=out_units).rename( - "snd" - ) + snd: xr.DataArray = flux2rate(snw, density=density, out_units=out_units).rename("snd") snd = snd.assign_attrs(standard_name="surface_snow_thickness") return snd -@declare_units( - prsn="[mass]/[area]/[time]", snr="[mass]/[volume]", const="[mass]/[volume]" -) +@declare_units(prsn="[mass]/[area]/[time]", snr="[mass]/[volume]", const="[mass]/[volume]") def prsn_to_prsnd( prsn: xr.DataArray, snr: xr.DataArray | None = None, @@ -1108,9 +1091,7 @@ def prsn_to_prsnd( :cite:cts:`frei_snowfall_2018, cbcl_climate_2020` """ density = snr if snr else const - prsnd: xr.DataArray = flux2rate(prsn, density=density, out_units=out_units).rename( - "prsnd" - ) + prsnd: xr.DataArray = flux2rate(prsn, density=density, out_units=out_units).rename("prsnd") return prsnd @@ -1150,17 +1131,13 @@ def prsnd_to_prsn( :cite:cts:`frei_snowfall_2018, cbcl_climate_2020` """ density = snr if snr else const - prsn: xr.DataArray = rate2flux(prsnd, density=density, out_units=out_units).rename( - "prsn" - ) + prsn: xr.DataArray = rate2flux(prsnd, density=density, out_units=out_units).rename("prsn") prsn = prsn.assign_attrs(standard_name="snowfall_flux") return prsn @declare_units(rls="[radiation]", rlds="[radiation]") -def longwave_upwelling_radiation_from_net_downwelling( - rls: xr.DataArray, rlds: xr.DataArray -) -> xr.DataArray: +def longwave_upwelling_radiation_from_net_downwelling(rls: xr.DataArray, rlds: xr.DataArray) -> xr.DataArray: """ Calculate upwelling thermal radiation from net thermal radiation and downwelling thermal radiation. @@ -1183,9 +1160,7 @@ def longwave_upwelling_radiation_from_net_downwelling( @declare_units(rss="[radiation]", rsds="[radiation]") -def shortwave_upwelling_radiation_from_net_downwelling( - rss: xr.DataArray, rsds: xr.DataArray -) -> xr.DataArray: +def shortwave_upwelling_radiation_from_net_downwelling(rss: xr.DataArray, rsds: xr.DataArray) -> xr.DataArray: """ Calculate upwelling solar radiation from net solar radiation and downwelling solar radiation. @@ -1552,15 +1527,11 @@ def potential_evapotranspiration( _tasmin = convert_units_to(tasmin, "degF") _tasmax = convert_units_to(tasmax, "degF") - re = extraterrestrial_solar_radiation( - _tasmin.time, _lat, chunks=_tasmin.chunksizes - ) + re = extraterrestrial_solar_radiation(_tasmin.time, _lat, chunks=_tasmin.chunksizes) re = convert_units_to(re, "cal cm-2 day-1") # Baier et Robertson(1965) formula - pet = 0.094 * ( - -87.03 + 0.928 * _tasmax + 0.933 * (_tasmax - _tasmin) + 0.0486 * re - ) + pet = 0.094 * (-87.03 + 0.928 * _tasmax + 0.933 * (_tasmax - _tasmin) + 0.0486 * re) pet = pet.clip(0) elif method in ["hargreaves85", "HG85"]: @@ -1571,9 +1542,7 @@ def potential_evapotranspiration( else: _tas = convert_units_to(tas, "degC") - ra = extraterrestrial_solar_radiation( - _tasmin.time, _lat, chunks=_tasmin.chunksizes - ) + ra = extraterrestrial_solar_radiation(_tasmin.time, _lat, chunks=_tasmin.chunksizes) ra = convert_units_to(ra, "MJ m-2 d-1") # Is used to convert the radiation to evaporation equivalents in mm (kg/MJ) @@ -1625,9 +1594,7 @@ def potential_evapotranspiration( tasK = convert_units_to(_tas, "K") - ext_rad = extraterrestrial_solar_radiation( - _tas.time, _lat, solar_constant="1367 W m-2", chunks=_tas.chunksizes - ) + ext_rad = extraterrestrial_solar_radiation(_tas.time, _lat, solar_constant="1367 W m-2", chunks=_tas.chunksizes) latentH = 4185.5 * (751.78 - 0.5655 * tasK) radDIVlat = ext_rad / latentH @@ -1687,9 +1654,7 @@ def potential_evapotranspiration( # mean temperature [degC] tas_m = (_tasmax + _tasmin) / 2 # mean saturation vapour pressure [kPa] - es = (1 / 2) * ( - saturation_vapor_pressure(_tasmax) + saturation_vapor_pressure(_tasmin) - ) + es = (1 / 2) * (saturation_vapor_pressure(_tasmax) + saturation_vapor_pressure(_tasmin)) es = convert_units_to(es, "kPa") # mean actual vapour pressure [kPa] ea = es * _hurs @@ -1999,8 +1964,8 @@ def universal_thermal_climate_index( - -30°C < tas - mrt < 30°C. - 0.5 m/s < sfcWind < 17.0 m/s. wind_cap_min : bool - If True, wind velocities are capped to a minimum of 0.5 m/s following :cite:t:`brode_utci_2012` usage guidelines. - This ensures UTCI calculation for low winds. Default value False. + If True, wind velocities are capped to a minimum of 0.5 m/s following :cite:t:`brode_utci_2012` + usage guidelines. This ensures UTCI calculation for low winds. Default value False. Returns ------- @@ -2026,9 +1991,7 @@ def universal_thermal_climate_index( if wind_cap_min: sfcWind = sfcWind.clip(0.5, None) if mrt is None: - mrt = mean_radiant_temperature( - rsds=rsds, rsus=rsus, rlds=rlds, rlus=rlus, stat=stat - ) + mrt = mean_radiant_temperature(rsds=rsds, rsus=rsus, rlds=rlds, rlus=rlus, stat=stat) mrt = convert_units_to(mrt, "degC") delta = mrt - tas pa = convert_units_to(e_sat, "kPa") * convert_units_to(hurs, "1") @@ -2047,12 +2010,7 @@ def universal_thermal_climate_index( utci = utci.assign_attrs({"units": "degC"}) if mask_invalid: utci = utci.where( - (-50.0 < tas) - & (tas < 50.0) - & (-30 < delta) - & (delta < 30) - & (0.5 <= sfcWind) - & (sfcWind < 17.0) + (-50.0 < tas) & (tas < 50.0) & (-30 < delta) & (delta < 30) & (0.5 <= sfcWind) & (sfcWind < 17.0) ) return utci @@ -2102,9 +2060,7 @@ def _fdir_ratio( ) -@declare_units( - rsds="[radiation]", rsus="[radiation]", rlds="[radiation]", rlus="[radiation]" -) +@declare_units(rsds="[radiation]", rsus="[radiation]", rlds="[radiation]", rlus="[radiation]") def mean_radiant_temperature( rsds: xr.DataArray, rsus: xr.DataArray, @@ -2180,9 +2136,7 @@ def mean_radiant_temperature( chunks=rsds.chunksizes, ) else: - raise NotImplementedError( - "Argument 'stat' must be one of 'instant' or 'sunlit'." - ) + raise NotImplementedError("Argument 'stat' must be one of 'instant' or 'sunlit'.") fdir_ratio = _fdir_ratio(dates, csza, rsds) @@ -2198,11 +2152,7 @@ def mean_radiant_temperature( np.power( ( (1 / 5.67e-8) # Stefan-Boltzmann constant - * ( - 0.5 * rlds - + 0.5 * rlus - + (0.7 / 0.97) * (0.5 * rsds_diffuse + 0.5 * rsus + fp * i_star) - ) + * (0.5 * rlds + 0.5 * rlus + (0.7 / 0.97) * (0.5 * rsds_diffuse + 0.5 * rsus + fp * i_star)) ), 0.25, ), diff --git a/src/xclim/indices/_hydrology.py b/src/xclim/indices/_hydrology.py index e01377b1a..13758e62f 100644 --- a/src/xclim/indices/_hydrology.py +++ b/src/xclim/indices/_hydrology.py @@ -154,9 +154,7 @@ def snd_max_doy(snd: xr.DataArray, freq: str = "YS-JUL") -> xr.DataArray: valid = at_least_n_valid(snd.where(snd > 0), n=1, freq=freq) # Compute doymax. Will return first time step if all snow depths are 0. - out = generic.select_resample_op( - snd.where(snd > 0, 0), op=generic.doymax, freq=freq - ) + out = generic.select_resample_op(snd.where(snd > 0, 0), op=generic.doymax, freq=freq) out.attrs.update(units="", is_dayofyear=np.int32(1), calendar=get_calendar(snd)) # Mask arrays that miss at least one non-null snd. @@ -208,9 +206,7 @@ def snw_max_doy(snw: xr.DataArray, freq: str = "YS-JUL") -> xr.DataArray: valid = at_least_n_valid(snw.where(snw > 0), n=1, freq=freq) # Compute doymax. Will return first time step if all snow depths are 0. - out = generic.select_resample_op( - snw.where(snw > 0, 0), op=generic.doymax, freq=freq - ) + out = generic.select_resample_op(snw.where(snw > 0, 0), op=generic.doymax, freq=freq) out.attrs.update(units="", is_dayofyear=np.int32(1), calendar=get_calendar(snw)) # Mask arrays that miss at least one non-null snd. @@ -218,9 +214,7 @@ def snw_max_doy(snw: xr.DataArray, freq: str = "YS-JUL") -> xr.DataArray: @declare_units(snw="[mass]/[area]") -def snow_melt_we_max( - snw: xr.DataArray, window: int = 3, freq: str = "YS-JUL" -) -> xr.DataArray: +def snow_melt_we_max(snw: xr.DataArray, window: int = 3, freq: str = "YS-JUL") -> xr.DataArray: """ Maximum snow melt. @@ -253,9 +247,7 @@ def snow_melt_we_max( @declare_units(snw="[mass]/[area]", pr="[precipitation]") -def melt_and_precip_max( - snw: xr.DataArray, pr: xr.DataArray, window: int = 3, freq: str = "YS-JUL" -) -> xr.DataArray: +def melt_and_precip_max(snw: xr.DataArray, pr: xr.DataArray, window: int = 3, freq: str = "YS-JUL") -> xr.DataArray: """ Maximum snow melt and precipitation. @@ -323,9 +315,7 @@ def flow_index(q: xr.DataArray, p: float = 0.95) -> xr.DataArray: @declare_units(q="[discharge]") -def high_flow_frequency( - q: xr.DataArray, threshold_factor: int = 9, freq: str = "YS-OCT" -) -> xr.DataArray: +def high_flow_frequency(q: xr.DataArray, threshold_factor: int = 9, freq: str = "YS-OCT") -> xr.DataArray: """ High flow frequency. @@ -358,9 +348,7 @@ def high_flow_frequency( @declare_units(q="[discharge]") -def low_flow_frequency( - q: xr.DataArray, threshold_factor: float = 0.2, freq: str = "YS-OCT" -) -> xr.DataArray: +def low_flow_frequency(q: xr.DataArray, threshold_factor: float = 0.2, freq: str = "YS-OCT") -> xr.DataArray: """ Low flow frequency. diff --git a/src/xclim/indices/_multivariate.py b/src/xclim/indices/_multivariate.py index 8303e37e9..22d8bfbe8 100644 --- a/src/xclim/indices/_multivariate.py +++ b/src/xclim/indices/_multivariate.py @@ -598,9 +598,7 @@ def daily_temperature_range_variability( @declare_units(tasmax="[temperature]", tasmin="[temperature]") -def extreme_temperature_range( - tasmin: xarray.DataArray, tasmax: xarray.DataArray, freq: str = "YS" -) -> xarray.DataArray: +def extreme_temperature_range(tasmin: xarray.DataArray, tasmax: xarray.DataArray, freq: str = "YS") -> xarray.DataArray: r""" Extreme intra-period temperature range. @@ -693,7 +691,8 @@ def heat_wave_frequency( characterize the occurrence of hot weather events that can result in adverse health outcomes for Canadian communities :cite:p:`casati_regional_2013`. - In :cite:t:`robinson_definition_2001`, the parameters would be `thresh_tasmin=27.22, thresh_tasmax=39.44, window=2` (81F, 103F). + In :cite:t:`robinson_definition_2001`, the parameters would be ``thresh_tasmin=27.22, + thresh_tasmax=39.44, window=2`` (81F, 103F). References ---------- @@ -703,9 +702,7 @@ def heat_wave_frequency( thresh_tasmin = convert_units_to(thresh_tasmin, tasmin) constrain = (">", ">=") - cond = (compare(tasmin, op, thresh_tasmin, constrain)) & ( - compare(tasmax, op, thresh_tasmax, constrain) - ) + cond = (compare(tasmin, op, thresh_tasmin, constrain)) & (compare(tasmax, op, thresh_tasmax, constrain)) out = rl.resample_and_rl( cond, @@ -785,9 +782,7 @@ def heat_wave_max_length( thresh_tasmin = convert_units_to(thresh_tasmin, tasmin) constrain = (">", ">=") - cond = (compare(tasmin, op, thresh_tasmin, constrain)) & ( - compare(tasmax, op, thresh_tasmax, constrain) - ) + cond = (compare(tasmin, op, thresh_tasmin, constrain)) & (compare(tasmax, op, thresh_tasmax, constrain)) out = rl.resample_and_rl( cond, resample_before_rl, @@ -855,9 +850,7 @@ def heat_wave_total_length( thresh_tasmin = convert_units_to(thresh_tasmin, tasmin) constrain = (">", ">=") - cond = compare(tasmin, op, thresh_tasmin, constrain) & compare( - tasmax, op, thresh_tasmax, constrain - ) + cond = compare(tasmin, op, thresh_tasmin, constrain) & compare(tasmax, op, thresh_tasmax, constrain) out = rl.resample_and_rl( cond, resample_before_rl, @@ -1165,9 +1158,7 @@ def high_precip_low_temp( To compute the number of days with intense rainfall while minimum temperatures dip below -0.2C: >>> pr = xr.open_dataset(path_to_pr_file).pr >>> tasmin = xr.open_dataset(path_to_tasmin_file).tasmin - >>> high_precip_low_temp( - ... pr, tas=tasmin, pr_thresh="10 mm/d", tas_thresh="-0.2 degC" - ... ) + >>> high_precip_low_temp(pr, tas=tasmin, pr_thresh="10 mm/d", tas_thresh="-0.2 degC") """ pr_thresh = convert_units_to(pr_thresh, pr, context="hydro") tas_thresh = convert_units_to(tas_thresh, tas) @@ -1291,16 +1282,10 @@ def fraction_over_precip_thresh( constrain = (">", ">=") # Total precip during wet days over period - total = ( - pr.where(compare(pr, op, thresh, constrain), 0) - .resample(time=freq) - .sum(dim="time") - ) + total = pr.where(compare(pr, op, thresh, constrain), 0).resample(time=freq).sum(dim="time") # Compute the days when precip is both over the wet day threshold and the percentile threshold. - over = ( - pr.where(compare(pr, op, tp, constrain), 0).resample(time=freq).sum(dim="time") - ) + over = pr.where(compare(pr, op, tp, constrain), 0).resample(time=freq).sum(dim="time") out = over / total out.attrs["units"] = "" @@ -1721,10 +1706,7 @@ def tx_tn_days_above( thresh_tasmin = convert_units_to(thresh_tasmin, tasmin) constrain = (">", ">=") - events = ( - compare(tasmin, op, thresh_tasmin, constrain) - & compare(tasmax, op, thresh_tasmax, constrain) - ) * 1 + events = (compare(tasmin, op, thresh_tasmin, constrain) & compare(tasmax, op, thresh_tasmax, constrain)) * 1 out = events.resample(time=freq).sum(dim="time") return to_agg_units(out, tasmin, "count") @@ -1844,9 +1826,7 @@ def winter_rain_ratio( return ratio -@declare_units( - snd="[length]", sfcWind="[speed]", snd_thresh="[length]", sfcWind_thresh="[speed]" -) +@declare_units(snd="[length]", sfcWind="[speed]", snd_thresh="[length]", sfcWind_thresh="[speed]") def blowing_snow( snd: xarray.DataArray, sfcWind: xarray.DataArray, # noqa @@ -1895,9 +1875,7 @@ def blowing_snow( @declare_units(pr="[precipitation]", evspsbl="[precipitation]") -def water_cycle_intensity( - pr: xarray.DataArray, evspsbl: xarray.DataArray, freq="YS" -) -> xarray.DataArray: +def water_cycle_intensity(pr: xarray.DataArray, evspsbl: xarray.DataArray, freq="YS") -> xarray.DataArray: """ Water cycle intensity. diff --git a/src/xclim/indices/_simple.py b/src/xclim/indices/_simple.py index 61a7e4312..7714219c6 100644 --- a/src/xclim/indices/_simple.py +++ b/src/xclim/indices/_simple.py @@ -371,9 +371,7 @@ def frost_days( @declare_units(tasmax="[temperature]", thresh="[temperature]") -def ice_days( - tasmax: xarray.DataArray, thresh: Quantified = "0 degC", freq: str = "YS" -) -> xarray.DataArray: +def ice_days(tasmax: xarray.DataArray, thresh: Quantified = "0 degC", freq: str = "YS") -> xarray.DataArray: r""" Number of ice/freezing days. @@ -408,9 +406,7 @@ def ice_days( @declare_units(pr="[precipitation]") -def max_1day_precipitation_amount( - pr: xarray.DataArray, freq: str = "YS" -) -> xarray.DataArray: +def max_1day_precipitation_amount(pr: xarray.DataArray, freq: str = "YS") -> xarray.DataArray: r""" Highest 1-day precipitation amount for a period (frequency). @@ -448,9 +444,7 @@ def max_1day_precipitation_amount( @declare_units(pr="[precipitation]") -def max_n_day_precipitation_amount( - pr: xarray.DataArray, window: int = 1, freq: str = "YS" -) -> xarray.DataArray: +def max_n_day_precipitation_amount(pr: xarray.DataArray, window: int = 1, freq: str = "YS") -> xarray.DataArray: r""" Highest precipitation amount cumulated over a n-day moving window. @@ -486,9 +480,7 @@ def max_n_day_precipitation_amount( @declare_units(pr="[precipitation]") -def max_pr_intensity( - pr: xarray.DataArray, window: int = 1, freq: str = "YS" -) -> xarray.DataArray: +def max_pr_intensity(pr: xarray.DataArray, window: int = 1, freq: str = "YS") -> xarray.DataArray: r""" Highest precipitation intensity over a n-hour moving window. @@ -631,9 +623,7 @@ def sfcWind_mean( # noqa: N802 >>> fg = xr.open_dataset(path_to_sfcWind_file).sfcWind >>> fg_mean = sfcWind_mean(fg, freq="QS-DEC") """ - return ( - sfcWind.resample(time=freq).mean(dim="time").assign_attrs(units=sfcWind.units) - ) + return sfcWind.resample(time=freq).mean(dim="time").assign_attrs(units=sfcWind.units) @declare_units(sfcWind="[speed]") @@ -716,11 +706,7 @@ def sfcWindmax_max( # noqa: N802 >>> from xclim.indices import sfcWindmax_max >>> max_sfcWindmax = sfcWindmax_max(sfcWindmax_dataset, freq="QS-DEC") """ - return ( - sfcWindmax.resample(time=freq) - .max(dim="time") - .assign_attrs(units=sfcWindmax.units) - ) + return sfcWindmax.resample(time=freq).max(dim="time").assign_attrs(units=sfcWindmax.units) @declare_units(sfcWindmax="[speed]") @@ -761,11 +747,7 @@ def sfcWindmax_mean( # noqa: N802 >>> from xclim.indices import sfcWindmax_mean >>> mean_sfcWindmax = sfcWindmax_mean(sfcWindmax_dataset, freq="QS-DEC") """ - return ( - sfcWindmax.resample(time=freq) - .mean(dim="time") - .assign_attrs(units=sfcWindmax.units) - ) + return sfcWindmax.resample(time=freq).mean(dim="time").assign_attrs(units=sfcWindmax.units) @declare_units(sfcWindmax="[speed]") @@ -806,8 +788,4 @@ def sfcWindmax_min( # noqa: N802 >>> from xclim.indices import sfcWindmax_min >>> min_sfcWindmax = sfcWindmax_min(sfcWindmax_dataset, freq="QS-DEC") """ - return ( - sfcWindmax.resample(time=freq) - .min(dim="time") - .assign_attrs(units=sfcWindmax.units) - ) + return sfcWindmax.resample(time=freq).min(dim="time").assign_attrs(units=sfcWindmax.units) diff --git a/src/xclim/indices/_synoptic.py b/src/xclim/indices/_synoptic.py index 419f055bf..625cfc06b 100644 --- a/src/xclim/indices/_synoptic.py +++ b/src/xclim/indices/_synoptic.py @@ -53,17 +53,13 @@ def jetstream_metric_woollings( lon = ua.cf["longitude"] ilon = (lon >= 300) * (lon <= 360) + (lon >= -60) * (lon <= 0) if not ilon.any(): - raise ValueError( - "Make sure the grid includes longitude values in a range between -60 and 0°E." - ) + raise ValueError("Make sure the grid includes longitude values in a range between -60 and 0°E.") # Select latitudes in the 15 to 75°N range lat = ua.cf["latitude"] ilat = (lat >= 15) * (lat <= 75) if not ilat.any(): - raise ValueError( - "Make sure the grid includes latitude values in a range between 15 and 75°N." - ) + raise ValueError("Make sure the grid includes latitude values in a range between 15 and 75°N.") # Select levels between 750 and 950 hPa pmin = convert_units_to("750 hPa", ua.cf["vertical"]) @@ -72,9 +68,7 @@ def jetstream_metric_woollings( p = ua.cf["vertical"] ip = (p >= pmin) * (p <= pmax) if not ip.any(): - raise ValueError( - "Make sure the grid includes pressure values in a range between 750 and 950 hPa." - ) + raise ValueError("Make sure the grid includes pressure values in a range between 750 and 950 hPa.") ua = ua.cf.sel( vertical=ip, @@ -89,20 +83,12 @@ def jetstream_metric_woollings( window_size = 61 cutoff = 1 / filter_freq if ua.time.size <= filter_freq or ua.time.size <= window_size: - raise ValueError( - f"Time series is too short to apply 61-day Lanczos filter (got a length of {ua.time.size})" - ) + raise ValueError(f"Time series is too short to apply 61-day Lanczos filter (got a length of {ua.time.size})") # compute low-pass filter weights - lanczos_weights = _compute_low_pass_filter_weights( - window_size=window_size, cutoff=cutoff - ) + lanczos_weights = _compute_low_pass_filter_weights(window_size=window_size, cutoff=cutoff) # apply the filter - ua_lf = ( - zonal_mean.rolling(time=window_size, center=True) - .construct("window") - .dot(lanczos_weights) - ) + ua_lf = zonal_mean.rolling(time=window_size, center=True).construct("window").dot(lanczos_weights) # Get latitude & eastward wind component units lat_name = ua.cf["latitude"].name @@ -114,9 +100,7 @@ def jetstream_metric_woollings( return jetlat, jetstr -def _compute_low_pass_filter_weights( - window_size: int, cutoff: float -) -> xarray.DataArray: +def _compute_low_pass_filter_weights(window_size: int, cutoff: float) -> xarray.DataArray: order = ((window_size - 1) // 2) + 1 nwts = 2 * order + 1 w = np.zeros([nwts]) diff --git a/src/xclim/indices/_threshold.py b/src/xclim/indices/_threshold.py index c027da0ea..2e0f43328 100644 --- a/src/xclim/indices/_threshold.py +++ b/src/xclim/indices/_threshold.py @@ -115,9 +115,7 @@ @declare_units(sfcWind="[speed]", thresh="[speed]") -def calm_days( - sfcWind: xarray.DataArray, thresh: Quantified = "2 m s-1", freq: str = "MS" -) -> xarray.DataArray: +def calm_days(sfcWind: xarray.DataArray, thresh: Quantified = "2 m s-1", freq: str = "MS") -> xarray.DataArray: r""" Calm days. @@ -223,7 +221,8 @@ def cold_spell_frequency( r""" Cold spell frequency. - The number of cold spell events, defined as a sequence of consecutive {window} days with mean daily temperature below a {thresh}. + The number of cold spell events, defined as a sequence of consecutive {window} days with mean daily + temperature below a {thresh}. Parameters ---------- @@ -593,9 +592,7 @@ def snw_season_length( @declare_units(snd="[length]", thresh="[length]") -def snd_storm_days( - snd: xarray.DataArray, thresh: Quantified = "25 cm", freq: str = "YS-JUL" -) -> xarray.DataArray: +def snd_storm_days(snd: xarray.DataArray, thresh: Quantified = "25 cm", freq: str = "YS-JUL") -> xarray.DataArray: """ Days with snowfall over threshold. @@ -635,9 +632,7 @@ def snd_storm_days( @declare_units(snw="[mass]/[area]", thresh="[mass]/[area]") -def snw_storm_days( - snw: xarray.DataArray, thresh: Quantified = "10 kg m-2", freq: str = "YS-JUL" -) -> xarray.DataArray: +def snw_storm_days(snw: xarray.DataArray, thresh: Quantified = "10 kg m-2", freq: str = "YS-JUL") -> xarray.DataArray: """ Days with snowfall over threshold. @@ -747,9 +742,7 @@ def daily_pr_intensity( # Should be resolved in pint v0.24. See: https://github.com/hgrecco/pint/issues/1913 with warnings.catch_warnings(): warnings.simplefilter("ignore", category=DeprecationWarning) - dpr_int = dpr_int.assign_attrs( - units=f"{str2pint(pram.units) / str2pint(wd.units):~}" - ) + dpr_int = dpr_int.assign_attrs(units=f"{str2pint(pram.units) / str2pint(wd.units):~}") return dpr_int @@ -853,9 +846,7 @@ def maximum_consecutive_wet_days( @declare_units(tas="[temperature]", thresh="[temperature]") -def cooling_degree_days( - tas: xarray.DataArray, thresh: Quantified = "18 degC", freq: str = "YS" -) -> xarray.DataArray: +def cooling_degree_days(tas: xarray.DataArray, thresh: Quantified = "18 degC", freq: str = "YS") -> xarray.DataArray: r""" Cooling degree days. @@ -891,9 +882,7 @@ def cooling_degree_days( @declare_units(tas="[temperature]", thresh="[temperature]") -def growing_degree_days( - tas: xarray.DataArray, thresh: Quantified = "4.0 degC", freq: str = "YS" -) -> xarray.DataArray: +def growing_degree_days(tas: xarray.DataArray, thresh: Quantified = "4.0 degC", freq: str = "YS") -> xarray.DataArray: r""" Growing degree-days over threshold temperature value. @@ -2768,10 +2757,10 @@ def maximum_consecutive_frost_days( Notes ----- - Let :math:`\mathbf{t}=t_0, t_1, \ldots, t_n` be a minimum daily temperature series and :math:`thresh` the threshold - below which a day is considered a frost day. Let :math:`\mathbf{s}` be the sorted vector of indices :math:`i` - where :math:`[t_i < thresh] \neq [t_{i+1} < thresh]`, that is, the days where the temperature crosses the threshold. - Then the maximum number of consecutive frost days is given by: + Let :math:`\mathbf{t}=t_0, t_1, \ldots, t_n` be a minimum daily temperature series and :math:`thresh` + the threshold below which a day is considered a frost day. Let :math:`\mathbf{s}` be the sorted vector + of indices :math:`i` where :math:`[t_i < thresh] \neq [t_{i+1} < thresh]`, that is, the days where the + temperature crosses the threshold. Then the maximum number of consecutive frost days is given by: .. math:: @@ -2883,10 +2872,11 @@ def maximum_consecutive_frost_free_days( Notes ----- - Let :math:`\mathbf{t}=t_0, t_1, \ldots, t_n` be a daily minimum temperature series and :math:`thresh` the threshold - above or equal to which a day is considered a frost free day. Let :math:`\mathbf{s}` be the sorted vector of - indices :math:`i` where :math:`[t_i <= thresh] \neq [t_{i+1} <= thresh]`, that is, the days where the temperature - crosses the threshold. Then the maximum number of consecutive frost free days is given by: + Let :math:`\mathbf{t}=t_0, t_1, \ldots, t_n` be a daily minimum temperature series and :math:`thresh` + the threshold above or equal to which a day is considered a frost free day. Let :math:`\mathbf{s}` + be the sorted vector of indices :math:`i` where :math:`[t_i <= thresh] \neq [t_{i+1} <= thresh]`, + that is, the days where the temperature crosses the threshold. + Then the maximum number of consecutive frost free days is given by: .. math:: @@ -2938,10 +2928,10 @@ def maximum_consecutive_tx_days( Notes ----- - Let :math:`\mathbf{t}=t_0, t_1, \ldots, t_n` be a daily maximum temperature series and :math:`thresh` the threshold - above which a day is considered a summer day. Let :math:`\mathbf{s}` be the sorted vector of indices :math:`i` - where :math:`[t_i < thresh] \neq [t_{i+1} < thresh]`, that is, the days where the temperature crosses the threshold. - Then the maximum number of consecutive tx_days (summer days) is given by: + Let :math:`\mathbf{t}=t_0, t_1, \ldots, t_n` be a daily maximum temperature series and :math:`thresh` + the threshold above which a day is considered a summer day. Let :math:`\mathbf{s}` be the sorted vector + of indices :math:`i` where :math:`[t_i < thresh] \neq [t_{i+1} < thresh]`, that is, the days where the + temperature crosses the threshold. Then the maximum number of consecutive tx_days (summer days) is given by: .. math:: @@ -3039,9 +3029,7 @@ def sea_ice_extent( @declare_units(sfcWind="[speed]", thresh="[speed]") -def windy_days( - sfcWind: xarray.DataArray, thresh: Quantified = "10.8 m s-1", freq: str = "MS" -) -> xarray.DataArray: +def windy_days(sfcWind: xarray.DataArray, thresh: Quantified = "10.8 m s-1", freq: str = "MS") -> xarray.DataArray: r""" Windy days. @@ -3179,7 +3167,7 @@ def degree_days_exceedance_date( The resulting :math:`k` is expressed as a day of year. Cumulated degree days have numerous applications including plant and insect phenology. - See https://en.wikipedia.org/wiki/Growing_degree-day for examples (:cite:t:`wikipedia_contributors_growing_2021`). + See: https://en.wikipedia.org/wiki/Growing_degree-day for examples (:cite:t:`wikipedia_contributors_growing_2021`). """ thresh = convert_units_to(thresh, "K") tas = convert_units_to(tas, "K") @@ -3194,9 +3182,7 @@ def degree_days_exceedance_date( def _exceedance_date(grp): strt_idx = rl.index_of_date(grp.time, after_date, max_idxs=1, default=0) - if ( - strt_idx.size == 0 - ): # The date is not within the group. Happens at boundaries. + if strt_idx.size == 0: # The date is not within the group. Happens at boundaries. return xarray.full_like(grp.isel(time=0), np.nan, float).drop_vars("time") # type: ignore cumsum = grp.where(grp.time >= grp.time[strt_idx][0]).cumsum("time") @@ -3209,17 +3195,13 @@ def _exceedance_date(grp): # This is slightly faster in numpy and generates fewer tasks in dask return out if isinstance(never_reached, str): - never_reached_val = doy_from_string( - DayOfYearStr(never_reached), grp.time.dt.year[0], grp.time.dt.calendar - ) + never_reached_val = doy_from_string(DayOfYearStr(never_reached), grp.time.dt.year[0], grp.time.dt.calendar) else: never_reached_val = never_reached return xarray.where((cumsum <= sum_thresh).all("time"), never_reached_val, out) dded = resample_map(c.clip(0), "time", freq, _exceedance_date) - dded = dded.assign_attrs( - units="", is_dayofyear=np.int32(1), calendar=get_calendar(tas) - ) + dded = dded.assign_attrs(units="", is_dayofyear=np.int32(1), calendar=get_calendar(tas)) return dded @@ -3250,11 +3232,12 @@ def dry_spell_frequency( freq : str Resampling frequency. resample_before_rl : bool - Determines if the resampling should take place before or after the run length encoding (or a similar algorithm) is applied to runs. + Determines if the resampling should take place before or after the run length encoding + (or a similar algorithm) is applied to runs. op : {"sum", "max", "min", "mean"} Operation to perform on the window. - Default is "sum", which checks that the sum of accumulated precipitation over the whole window is less than the - threshold. + Default is "sum", which checks that the sum of accumulated precipitation over the whole window + is less than the threshold. "max" checks that the maximal daily precipitation amount within the window is less than the threshold. This is the same as verifying that each individual day is below the threshold. **indexer : {dim: indexer}, optional @@ -3318,14 +3301,15 @@ def dry_spell_total_length( Number of days when the maximum or accumulated precipitation is under threshold. op : {"sum", "max", "min", "mean"} Operation to perform on the window. - Default is "sum", which checks that the sum of accumulated precipitation over the whole window is less than the - threshold. + Default is "sum", which checks that the sum of accumulated precipitation over the whole window + is less than the threshold. "max" checks that the maximal daily precipitation amount within the window is less than the threshold. This is the same as verifying that each individual day is below the threshold. freq : str Resampling frequency. resample_before_rl : bool - Determines if the resampling should take place before or after the run length encoding (or a similar algorithm) is applied to runs. + Determines if the resampling should take place before or after the run length encoding + (or a similar algorithm) is applied to runs. **indexer : {dim: indexer}, optional Indexing parameters to compute the indicator on a temporal subset of the data. It accepts the same arguments as :py:func:`xclim.indices.generic.select_time`. @@ -3411,11 +3395,11 @@ def dry_spell_max_length( Notes ----- The algorithm assumes days before and after the timeseries are "wet", meaning that the condition for being - considered part of a dry spell is stricter on the edges. For example, with `window=3` and `op='sum'`, the first day - of the series is considered part of a dry spell only if the accumulated precipitation within the first three days is - under the threshold. In comparison, a day in the middle of the series is considered part of a dry spell if any of - the three 3-day periods of which it is part are considered dry (so a total of five days are included in the - computation, compared to only three). + considered part of a dry spell is stricter on the edges. For example, with `window=3` and `op='sum'`, + the first day of the series is considered part of a dry spell only if the accumulated precipitation within + the first three days is under the threshold. In comparison, a day in the middle of the series is considered + part of a dry spell if any of the three 3-day periods of which it is part are considered dry + (so a total of five days are included in the computation, compared to only three). """ pram = rate2amount(convert_units_to(pr, "mm/d", context="hydro"), out_units="mm") return spell_length_statistics( @@ -3444,7 +3428,8 @@ def wet_spell_frequency( r""" Return the number of wet periods of n days and more. - Periods during which the accumulated, minimal, or maximal daily precipitation amount on a window of n days is over threshold. + Periods during which the accumulated, minimal, or maximal daily precipitation amount on a window + of n days is over threshold. Parameters ---------- @@ -3458,11 +3443,12 @@ def wet_spell_frequency( freq : str Resampling frequency. resample_before_rl : bool - Determines if the resampling should take place before or after the run length encoding (or a similar algorithm) is applied to runs. + Determines if the resampling should take place before or after the run length encoding + (or a similar algorithm) is applied to runs. op : {"sum", "min", "max", "mean"} Operation to perform on the window. - Default is "sum", which checks that the sum of accumulated precipitation over the whole window is more than the - threshold. + Default is "sum", which checks that the sum of accumulated precipitation over the whole window is + more than the threshold. "min" checks that the maximal daily precipitation amount within the window is more than the threshold. This is the same as verifying that each individual day is above the threshold. **indexer : {dim: indexer}, optional @@ -3532,7 +3518,8 @@ def wet_spell_total_length( freq : str Resampling frequency. resample_before_rl : bool - Determines if the resampling should take place before or after the run length encoding (or a similar algorithm) is applied to runs. + Determines if the resampling should take place before or after the run length encoding + (or a similar algorithm) is applied to runs. **indexer : {dim: indexer}, optional Indexing parameters to compute the indicator on a temporal subset of the data. It accepts the same arguments as :py:func:`xclim.indices.generic.select_time`. @@ -3602,7 +3589,8 @@ def wet_spell_max_length( freq : str Resampling frequency. resample_before_rl : bool - Determines if the resampling should take place before or after the run length encoding (or a similar algorithm) is applied to runs. + Determines if the resampling should take place before or after the run length encoding + (or a similar algorithm) is applied to runs. **indexer : {dim: indexer}, optional Indexing parameters to compute the indicator on a temporal subset of the data. It accepts the same arguments as :py:func:`xclim.indices.generic.select_time`. @@ -3690,9 +3678,7 @@ def holiday_snow_days( date_bounds=(date_start, date_start if date_end is None else date_end), ) - xmas_days = count_occurrences( - snd_constrained, snd_thresh, freq, op, constrain=[">=", ">"] - ) + xmas_days = count_occurrences(snd_constrained, snd_thresh, freq, op, constrain=[">=", ">"]) xmas_days = to_agg_units(xmas_days, snd, "count") return xmas_days @@ -3757,9 +3743,7 @@ def holiday_snow_and_snowfall_days( date_bounds=(date_start, date_start if date_end is None else date_end), ) - prsn_mm = rate2amount( - convert_units_to(prsn, "mm day-1", context="hydro"), out_units="mm" - ) + prsn_mm = rate2amount(convert_units_to(prsn, "mm day-1", context="hydro"), out_units="mm") prsn_mm_constrained = select_time( prsn_mm, date_bounds=(date_start, date_start if date_end is None else date_end), diff --git a/src/xclim/indices/fire/_cffwis.py b/src/xclim/indices/fire/_cffwis.py index bb2c3a0c6..090b684b2 100644 --- a/src/xclim/indices/fire/_cffwis.py +++ b/src/xclim/indices/fire/_cffwis.py @@ -44,12 +44,12 @@ Finally, a mechanism for dry spring starts is implemented. For now, it is slightly different from what the GFWED, uses, but seems to agree with the state of the science of the CFS. When activated, the drought code and Duff-moisture codes -are started in spring with a value that is function of the number of days since the last significant precipitation event. -The conventional start value increased by that number of days times a "dry start" factor. Parameters are controlled in -the call of the indices and :py:func:`fire_weather_ufunc`. Overwintering of the drought code overrides this mechanism if -both are activated. GFWED use a more complex approach with an added check on the previous day's snow cover for -determining "dry" points. Moreover, there, the start values are only the multiplication of a factor to the number of dry -days. +are started in spring with a value that is function of the number of days since the last significant precipitation +event. The conventional start value increased by that number of days times a "dry start" factor. Parameters are +controlled in the call of the indices and :py:func:`fire_weather_ufunc`. Overwintering of the drought code overrides +this mechanism if both are activated. GFWED use a more complex approach with an added check on the previous day's +snow cover for determining "dry" points. Moreover, there, the start values are only the multiplication of a factor +to the number of dry days. Examples -------- @@ -268,22 +268,17 @@ def _fine_fuel_moisture_code(t, p, w, h, ffmc0): # pragma: no cover if p > 0.5: rf = p - 0.5 # *Eq.2*# if mo > 150.0: - mo = ( - mo - + 42.5 * rf * np.exp(-100.0 / (251.0 - mo)) * (1.0 - np.exp(-6.93 / rf)) - ) + (0.0015 * (mo - 150.0) ** 2) * np.sqrt(rf) + mo = (mo + 42.5 * rf * np.exp(-100.0 / (251.0 - mo)) * (1.0 - np.exp(-6.93 / rf))) + ( + 0.0015 * (mo - 150.0) ** 2 + ) * np.sqrt(rf) # *Eq.3b*# elif mo <= 150.0: - mo = mo + 42.5 * rf * np.exp(-100.0 / (251.0 - mo)) * ( - 1.0 - np.exp(-6.93 / rf) - ) + mo = mo + 42.5 * rf * np.exp(-100.0 / (251.0 - mo)) * (1.0 - np.exp(-6.93 / rf)) # *Eq.3a*# mo = min(mo, 250.0) ed = ( - 0.942 * (h**0.679) - + (11.0 * np.exp((h - 100.0) / 10.0)) - + 0.18 * (21.1 - t) * (1.0 - 1.0 / np.exp(0.1150 * h)) + 0.942 * (h**0.679) + (11.0 * np.exp((h - 100.0) / 10.0)) + 0.18 * (21.1 - t) * (1.0 - 1.0 / np.exp(0.1150 * h)) ) # *Eq.4*# if mo < ed: @@ -294,9 +289,9 @@ def _fine_fuel_moisture_code(t, p, w, h, ffmc0): # pragma: no cover ) # *Eq.5*# if mo < ew: # *Eq.7a*# - kl = 0.424 * (1.0 - ((100.0 - h) / 100.0) ** 1.7) + ( - 0.0694 * np.sqrt(w) - ) * (1.0 - ((100.0 - h) / 100.0) ** 8) + kl = 0.424 * (1.0 - ((100.0 - h) / 100.0) ** 1.7) + (0.0694 * np.sqrt(w)) * ( + 1.0 - ((100.0 - h) / 100.0) ** 8 + ) kw = kl * (0.581 * np.exp(0.0365 * t)) # *Eq.7b*# m = ew - (ew - mo) / 10.0**kw # *Eq.9*# elif mo > ew: @@ -306,9 +301,7 @@ def _fine_fuel_moisture_code(t, p, w, h, ffmc0): # pragma: no cover elif mo == ed: m = mo else: - kl = 0.424 * (1.0 - (h / 100.0) ** 1.7) + (0.0694 * np.sqrt(w)) * ( - 1.0 - (h / 100.0) ** 8 - ) # *Eq.6a*# + kl = 0.424 * (1.0 - (h / 100.0) ** 1.7) + (0.0694 * np.sqrt(w)) * (1.0 - (h / 100.0) ** 8) # *Eq.6a*# kw = kl * (0.581 * np.exp(0.0365 * t)) # *Eq.6b*# m = ed + (mo - ed) / 10.0**kw # *Eq.8*# @@ -366,9 +359,7 @@ def _duff_moisture_code( if p > 1.5: ra = p rw = 0.92 * ra - 1.27 # *Eq.11*# - wmi = 20.0 + 280.0 / np.exp( - 0.023 * dmc0 - ) # *Eq.12*# This line replicates cffdrs (R code from CFS) + wmi = 20.0 + 280.0 / np.exp(0.023 * dmc0) # *Eq.12*# This line replicates cffdrs (R code from CFS) # wmi = 20.0 + np.exp(5.6348 - dmc0 / 43.43) # *Eq.12*# This line replicates GFWED (Matlab code) if dmc0 <= 33.0: b = 100.0 / (0.5 + 0.3 * dmc0) # *Eq.13a*# @@ -534,7 +525,9 @@ def _overwintering_drought_code( DCf: np.ndarray, wpr: np.ndarray, a: float, b: float, minDC: int ) -> np.ndarray | np.nan: # pragma: no cover """ - Compute the season-starting drought code based on the previous season's last drought code and the total winter precipitation. + Compute the season-starting drought code. + + Calculation based on the previous season's last drought code and the total winter precipitation. Parameters ---------- @@ -631,9 +624,7 @@ def _fire_season( # Start up when the last X days including today have no snow on the ground. start_up = np.all(snow <= snow_thresh, axis=-1) # Shut down when today has snow OR the last X days (including today) were all below a threshold. - shut_down = (snd[..., it] > snow_thresh) | np.all( - temp < temp_end_thresh, axis=-1 - ) + shut_down = (snd[..., it] > snow_thresh) | np.all(temp < temp_end_thresh, axis=-1) elif method == "GFWED": msnow = np.mean(snd[..., it - snow_condition_days + 1 : it + 1], axis=-1) @@ -718,9 +709,7 @@ def _fire_weather_calc( # noqa: C901 if dry_start: ow_DC = dc0.copy() ow_DMC = dmc0.copy() - start_up_wet = np.zeros_like( - dmc0, dtype=bool - ) # Pre allocate to avoid "unboundlocalerror" + start_up_wet = np.zeros_like(dmc0, dtype=bool) # Pre allocate to avoid "unboundlocalerror" # Iterate on all days. for it in range(tas.shape[-1]): @@ -751,24 +740,14 @@ def _fire_weather_calc( # noqa: C901 if "SNOW" in dry_start and it >= params["snow_cover_days"]: # This is for the GFWED mode with snow - snow_cover_history = snd[ - ..., it - params["snow_cover_days"] + 1 : it + 1 - ] - snow_days = np.count_nonzero( - snow_cover_history > params["snow_thresh"], axis=-1 - ) + snow_cover_history = snd[..., it - params["snow_cover_days"] + 1 : it + 1] + snow_days = np.count_nonzero(snow_cover_history > params["snow_thresh"], axis=-1) # Points where the snow cover is enough to trigger a "wet" start-up. start_up_wet = ( start_up - & ( - snow_days / params["snow_cover_days"] - >= params["snow_min_cover_frac"] - ) - & ( - snow_cover_history.mean(axis=-1) - >= params["snow_min_mean_depth"] - ) + & (snow_days / params["snow_cover_days"] >= params["snow_min_cover_frac"]) + & (snow_cover_history.mean(axis=-1) >= params["snow_min_mean_depth"]) ) if "DC" in ind_prevs: @@ -805,14 +784,11 @@ def _fire_weather_calc( # noqa: C901 # The GFWED includes the current day in the "wet points" check. ow_DC[(start_up | winter) & wetpts] = 0 ow_DC[(start_up | winter) & ~wetpts] = ( - ow_DC[(start_up | winter) & ~wetpts] - + params["dc_dry_factor"] + ow_DC[(start_up | winter) & ~wetpts] + params["dc_dry_factor"] ) else: # "CFS" ow_DC[winter & wetpts] = params["dc_start"] - ow_DC[winter & ~wetpts] = ( - ow_DC[winter & ~wetpts] + params["dc_dry_factor"] - ) + ow_DC[winter & ~wetpts] = ow_DC[winter & ~wetpts] + params["dc_dry_factor"] if "SNOW" in dry_start: # Points where we have start-up and where snow cover was enough @@ -832,14 +808,11 @@ def _fire_weather_calc( # noqa: C901 # The GFWED includes the current day in the "wet points" check. ow_DMC[(start_up | winter) & wetpts] = 0 ow_DMC[(start_up | winter) & ~wetpts] = ( - ow_DMC[(start_up | winter) & ~wetpts] - + params["dmc_dry_factor"] + ow_DMC[(start_up | winter) & ~wetpts] + params["dmc_dry_factor"] ) else: # "CFS" ow_DMC[winter & wetpts] = params["dmc_start"] - ow_DMC[winter & ~wetpts] = ( - ow_DMC[winter & ~wetpts] + params["dmc_dry_factor"] - ) + ow_DMC[winter & ~wetpts] = ow_DMC[winter & ~wetpts] + params["dmc_dry_factor"] if "SNOW" in dry_start: # Points where we have start-up and where snow cover was enough @@ -857,9 +830,7 @@ def _fire_weather_calc( # noqa: C901 # Main computation if "DC" in outputs: - out["DC"][..., it] = _drought_code( - tas[..., it], pr[..., it], mth[..., it], lat, ind_prevs["DC"] - ) + out["DC"][..., it] = _drought_code(tas[..., it], pr[..., it], mth[..., it], lat, ind_prevs["DC"]) if "DMC" in outputs: out["DMC"][..., it] = _duff_moisture_code( tas[..., it], @@ -879,17 +850,11 @@ def _fire_weather_calc( # noqa: C901 ind_prevs["FFMC"], ) if "ISI" in outputs: - out["ISI"][..., it] = initial_spread_index( - ws[..., it], out["FFMC"][..., it] - ) + out["ISI"][..., it] = initial_spread_index(ws[..., it], out["FFMC"][..., it]) if "BUI" in outputs: - out["BUI"][..., it] = build_up_index( - out["DMC"][..., it], out["DC"][..., it] - ) + out["BUI"][..., it] = build_up_index(out["DMC"][..., it], out["DC"][..., it]) if "FWI" in outputs: - out["FWI"][..., it] = fire_weather_index( - out["ISI"][..., it], out["BUI"][..., it] - ) + out["FWI"][..., it] = fire_weather_index(out["ISI"][..., it], out["BUI"][..., it]) if "DSR" in outputs: out["DSR"][..., it] = daily_severity_rating(out["FWI"][..., it]) @@ -974,10 +939,12 @@ def fire_weather_ufunc( # noqa: C901 # numpydoc ignore=PR01,PR02 Whether to activate DC overwintering or not. If True, either season_method or season_mask must be given. dry_start : {None, 'CFS', 'GFWED'} Whether to activate the DC and DMC "dry start" mechanism and which method to use. See Notes. - If overwintering is activated, it overrides this parameter : only DMC is handled through the dry start mechanism. + If overwintering is activated, it overrides this parameter; + Only DMC is handled through the dry start mechanism. initial_start_up : bool - If True (default), grid points where the fire season is active on the first timestep go through a start-up phase - for that time step. Otherwise, previous codes must be given as a continuing fire season is assumed for those points. + If True (default), grid points where the fire season is active on the first timestep go through a + start-up phase for that time step. + Otherwise, previous codes must be given as a continuing fire season is assumed for those points. carry_over_fraction : float Carry over fraction. wetting_efficiency_fraction : float @@ -1132,9 +1099,7 @@ def fire_weather_ufunc( # noqa: C901 # numpydoc ignore=PR01,PR02 if overwintering: # Overwintering code activated if season_method is None and season_mask is None: - raise ValueError( - "If overwintering is activated, either `season_method` or `season_mask` must be given." - ) + raise ValueError("If overwintering is activated, either `season_method` or `season_mask` must be given.") # Last winter PR is 0 by default if winter_pr is None: @@ -1148,9 +1113,7 @@ def fire_weather_ufunc( # noqa: C901 # numpydoc ignore=PR01,PR02 output_dtypes.append(pr.dtype) # Kwargs from default parameters. take the value when it is a tuple. - kwargs = { - k: v if not isinstance(v, tuple) else v[0] for k, v in default_params.items() - } + kwargs = {k: v if not isinstance(v, tuple) else v[0] for k, v in default_params.items()} kwargs.update(**params) kwargs.update( season_method=season_method, @@ -1175,9 +1138,7 @@ def fire_weather_ufunc( # noqa: C901 # numpydoc ignore=PR01,PR02 input_core_dims=input_core_dims, output_core_dims=output_core_dims, dask="parallelized", - dask_gufunc_kwargs={ - "meta": tuple(np.array((), dtype=dtype) for dtype in output_dtypes) - }, + dask_gufunc_kwargs={"meta": tuple(np.array((), dtype=dtype) for dtype in output_dtypes)}, ) if tas.ndim == 1: @@ -1197,9 +1158,7 @@ def overwintering_drought_code( last_dc: xr.DataArray, winter_pr: xr.DataArray, carry_over_fraction: xr.DataArray | float = default_params["carry_over_fraction"], - wetting_efficiency_fraction: xr.DataArray | float = default_params[ - "wetting_efficiency_fraction" - ], + wetting_efficiency_fraction: xr.DataArray | float = default_params["wetting_efficiency_fraction"], min_dc: xr.DataArray | float = default_params["dc_start"], ) -> xr.DataArray: """ @@ -1250,8 +1209,8 @@ def overwintering_drought_code( - 0.9, Poorly drained, boggy sites with deep organic layers - 0.75, Deep ground frost does not occur until late fall, if at all; moderately drained sites that allow infiltration of most of the melting snowpack - - 0.5, Chinook-prone areas and areas subject to early and deep ground frost; well-drained soils favoring rapid - percolation or topography favoring rapid runoff before melting of ground frost + - 0.5, Chinook-prone areas and areas subject to early and deep ground frost; well-drained soils favoring + rapid percolation or topography favoring rapid runoff before melting of ground frost Source: :cite:cts:`drought-lawson_weather_2008` - Table 9. @@ -1283,7 +1242,8 @@ def _convert_parameters( for param, value in params.copy().items(): if param not in default_params: raise ValueError( - f"{param} is not a valid parameter for {funcname}. See the docstring of the function and the list in xc.indices.fire.default_params." + f"{param} is not a valid parameter for {funcname}. " + "See the docstring of the function and the list in xc.indices.fire.default_params." ) if isinstance(default_params[param], tuple): params[param] = convert_units_to(value, default_params[param][1]) @@ -1318,9 +1278,7 @@ def cffwis_indices( dry_start: str | None = None, initial_start_up: bool = True, **params, -) -> tuple[ - xr.DataArray, xr.DataArray, xr.DataArray, xr.DataArray, xr.DataArray, xr.DataArray -]: +) -> tuple[xr.DataArray, xr.DataArray, xr.DataArray, xr.DataArray, xr.DataArray, xr.DataArray]: r""" Canadian Fire Weather Index System indices. @@ -1387,8 +1345,8 @@ def cffwis_indices( Notes ----- See :cite:t:`code-natural_resources_canada_data_nodate`, the :py:mod:`xclim.indices.fire` module documentation, - and the docstring of :py:func:`fire_weather_ufunc` for more information. This algorithm follows the official R code - released by the CFS, which contains revisions from the original 1982 Fortran code. + and the docstring of :py:func:`fire_weather_ufunc` for more information. This algorithm follows the + official R code released by the CFS, which contains revisions from the original 1982 Fortran code. References ---------- @@ -1475,11 +1433,12 @@ def drought_code( Whether to activate the DC and DMC "dry start" mechanism and which method to use. See :py:func:`fire_weather_ufunc`. initial_start_up : bool - If True (default), grid points where the fire season is active on the first timestep go through a start_up phase - for that time step. Otherwise, previous codes must be given as a continuing fire season is assumed for those - points. + If True (default), grid points where the fire season is active on the first timestep go through + a start_up phase for that time step. Otherwise, previous codes must be given as a continuing fire + season is assumed for those points. **params : dict - Any other keyword parameters as defined in `xclim.indices.fire.fire_weather_ufunc` and in :py:data:`default_params`. + Any other keyword parameters as defined in `xclim.indices.fire.fire_weather_ufunc` + and in :py:data:`default_params`. Returns ------- @@ -1571,11 +1530,12 @@ def duff_moisture_code( Whether to activate the DC and DMC "dry start" mechanism and which method to use. See :py:func:`fire_weather_ufunc`. initial_start_up : bool - If True (default), grid points where the fire season is active on the first timestep go through a start_up phase - for that time step. Otherwise, previous codes must be given as a continuing fire season is assumed for those - points. + If True (default), grid points where the fire season is active on the first timestep go through a start_up + phase for that time step. Otherwise, previous codes must be given as a continuing fire season is assumed + for those points. **params : dict - Any other keyword parameters as defined in `xclim.indices.fire.fire_weather_ufunc` and in :py:data:`default_params`. + Any other keyword parameters as defined in `xclim.indices.fire.fire_weather_ufunc` + and in :py:data:`default_params`. Returns ------- @@ -1636,7 +1596,8 @@ def fire_season( """ Fire season mask. - Binary mask of the active fire season, defined by conditions on consecutive daily temperatures and, optionally, snow depths. + Binary mask of the active fire season, defined by conditions on consecutive daily temperatures + and, optionally, snow depths. Parameters ---------- @@ -1671,10 +1632,8 @@ def fire_season( ---------- :cite:cts:`fire-wotton_length_1993,fire-lawson_weather_2008` """ - # TODO: `map_blocks` as currently implemented, does not mix well with non-scalar thresholds, as they are being passed through kwargs - if not all( - np.isscalar(v) for v in [temp_start_thresh, temp_end_thresh, snow_thresh] - ): + # TODO: `map_blocks` as currently implemented, does not mix well with non-scalar thresholds passed via kwargs. + if not all(np.isscalar(v) for v in [temp_start_thresh, temp_end_thresh, snow_thresh]): raise ValueError("Thresholds must be scalar.") kwargs = { diff --git a/src/xclim/indices/fire/_ffdi.py b/src/xclim/indices/fire/_ffdi.py index c9d2ac3bc..09c757da8 100644 --- a/src/xclim/indices/fire/_ffdi.py +++ b/src/xclim/indices/fire/_ffdi.py @@ -12,6 +12,7 @@ this module and consult :cite:t:`ffdi-finkele_2006` for a full description of the methods used to calculate each index. """ + # This file is structured in the following way: # Section 1: individual codes, numba-accelerated and vectorized functions. # Section 2: Exposed methods and indices. @@ -155,12 +156,7 @@ def _griffiths_drought_factor(p, smd, lim, df): # pragma: no cover xlim = 75 / (270.525 - 1.267 * smd[d]) x = min(x, xlim) - dfw = ( - 10.5 - * (1 - np.exp(-(smd[d] + 30) / 40)) - * (41 * x**2 + x) - / (40 * x**2 + x + 1) - ) + dfw = 10.5 * (1 - np.exp(-(smd[d] + 30) / 40)) * (41 * x**2 + x) / (40 * x**2 + x + 1) if lim == 1: if smd[d] < 25.0: @@ -401,8 +397,6 @@ def mcarthur_forest_fire_danger_index( hurs = convert_units_to(hurs, "%") sfcWind = convert_units_to(sfcWind, "km/h") - ffdi = drought_factor**0.987 * np.exp( - 0.0338 * tasmax - 0.0345 * hurs + 0.0234 * sfcWind + 0.243147 - ) + ffdi = drought_factor**0.987 * np.exp(0.0338 * tasmax - 0.0345 * hurs + 0.0234 * sfcWind + 0.243147) ffdi.attrs["units"] = "" return ffdi diff --git a/src/xclim/indices/generic.py b/src/xclim/indices/generic.py index f8ead5977..373019c04 100644 --- a/src/xclim/indices/generic.py +++ b/src/xclim/indices/generic.py @@ -84,11 +84,12 @@ def select_resample_op( freq : str Resampling frequency defining the periods as defined in :ref:`timeseries.resampling`. out_units : str, optional - Output units to assign. Only necessary if `op` is function not supported by :py:func:`xclim.core.units.to_agg_units`. + Output units to assign. + Only necessary if `op` is function not supported by :py:func:`xclim.core.units.to_agg_units`. **indexer : {dim: indexer, }, optional - Time attribute and values over which to subset the array. For example, use season='DJF' to select winter values, - month=1 to select January, or month=[6,7,8] to select summer months. If not indexer is given, all values are - considered. + Time attribute and values over which to subset the array. + For example, use season='DJF' to select winter values, month=1 to select January, or month=[6,7,8] + to select summer months. If not indexer is given, all values are considered. Returns ------- @@ -99,9 +100,7 @@ def select_resample_op( if isinstance(op, str): op = _xclim_ops.get(op, op) if isinstance(op, str): - out = getattr(da.resample(time=freq), op.replace("integral", "sum"))( - dim="time", keep_attrs=True - ) + out = getattr(da.resample(time=freq), op.replace("integral", "sum"))(dim="time", keep_attrs=True) else: with xr.set_options(keep_attrs=True): out = resample_map(da, "time", freq, op) @@ -110,9 +109,7 @@ def select_resample_op( return out.assign_attrs(units=out_units) if op in ["std", "var"]: - out.attrs.update( - pint2cfattrs(units2pint(out.attrs["units"]), is_difference=True) - ) + out.attrs.update(pint2cfattrs(units2pint(out.attrs["units"]), is_difference=True)) return to_agg_units(out, da, op) @@ -143,9 +140,11 @@ def select_rolling_resample_op( window_op : str {'min', 'max', 'mean', 'std', 'var', 'count', 'sum', 'integral'} Operation to apply to the rolling window. Default: 'mean'. freq : str - Resampling frequency defining the periods as defined in :ref:`timeseries.resampling`. Applied after the rolling window. + Resampling frequency defining the periods as defined in :ref:`timeseries.resampling`. + Applied after the rolling window. out_units : str, optional - Output units to assign. Only necessary if `op` is function not supported by :py:func:`xclim.core.units.to_agg_units`. + Output units to assign. + Only necessary if `op` is function not supported by :py:func:`xclim.core.units.to_agg_units`. **indexer : {dim: indexer, }, optional Time attribute and values over which to subset the array. For example, use season='DJF' to select winter values, month=1 to select January, or month=[6,7,8] to select summer months. If not indexer is given, all values are @@ -465,9 +464,7 @@ def spell_mask( if not isinstance(data, xr.DataArray): # thus a sequence if np.isscalar(thresh) or len(data) != len(thresh): - raise ValueError( - "When ``data`` is given as a list, ``threshold`` must be a sequence of the same length." - ) + raise ValueError("When ``data`` is given as a list, ``threshold`` must be a sequence of the same length.") data = xr.concat(data, "variable") if isinstance(thresh[0], xr.DataArray): thresh = xr.concat(thresh, "variable") @@ -475,13 +472,9 @@ def spell_mask( thresh = xr.DataArray(thresh, dims=("variable",)) if weights is not None: if win_reducer != "mean": - raise ValueError( - f"Argument 'weights' is only supported if 'win_reducer' is 'mean'. Got : {win_reducer}" - ) + raise ValueError(f"Argument 'weights' is only supported if 'win_reducer' is 'mean'. Got : {win_reducer}") elif len(weights) != window: - raise ValueError( - f"Weights have a different length ({len(weights)}) than the window ({window})." - ) + raise ValueError(f"Weights have a different length ({len(weights)}) than the window ({window}).") weights = xr.DataArray(weights, dims=("window",)) if window == 1: # Fast path @@ -507,7 +500,8 @@ def spell_mask( else: data_pad = data.pad(time=(0, window)) # The spell-wise value to test - # For example "window_reducer='sum'", we want the sum over the minimum spell length (window) to be above the thresh + # For example "window_reducer='sum'", + # we want the sum over the minimum spell length (window) to be above the thresh if weights is not None: spell_value = data_pad.rolling(time=window).construct("window").dot(weights) else: @@ -517,16 +511,12 @@ def spell_mask( if not np.isscalar(thresh): mask = getattr(mask, var_reducer)("variable") # True for all days part of a spell that respected the condition (shift because of the two rollings) - is_in_spell = (mask.rolling(time=window).sum() >= 1).shift( - time=-(window - 1), fill_value=False - ) + is_in_spell = (mask.rolling(time=window).sum() >= 1).shift(time=-(window - 1), fill_value=False) # Cut back to the original size is_in_spell = is_in_spell.isel(time=slice(0, data.time.size)) if min_gap > 1: - is_in_spell = rl.runs_with_holes(is_in_spell, 1, ~is_in_spell, min_gap).astype( - bool - ) + is_in_spell = rl.runs_with_holes(is_in_spell, 1, ~is_in_spell, min_gap).astype(bool) return is_in_spell @@ -545,9 +535,7 @@ def _spell_length_statistics( ) -> xr.DataArray | Sequence[xr.DataArray]: if isinstance(spell_reducer, str): spell_reducer = [spell_reducer] - is_in_spell = spell_mask( - data, window, win_reducer, op, thresh, min_gap=min_gap - ).astype(np.float32) + is_in_spell = spell_mask(data, window, win_reducer, op, thresh, min_gap=min_gap).astype(np.float32) is_in_spell = select_time(is_in_spell, **indexer) outs = [] @@ -594,8 +582,8 @@ def spell_length_statistics( r""" Generate statistic on spells lengths. - A spell is when a statistic (`win_reducer`) over a minimum number (`window`) of consecutive timesteps respects a condition (`op` `thresh`). - This returns a statistic over the spells count or lengths. + A spell is when a statistic (`win_reducer`) over a minimum number (`window`) of consecutive timesteps + respects a condition (`op` `thresh`). This returns a statistic over the spells count or lengths. Parameters ---------- @@ -661,8 +649,8 @@ def spell_length_statistics( ... freq="YS", ... ) - Here, a day is part of a spell if it is in any five (5) day period where the total accumulated precipitation reaches - or exceeds 20 mm. We then return the length of the longest of such spells. + Here, a day is part of a spell if it is in any five (5) day period where the total accumulated precipitation + reaches or exceeds 20 mm. We then return the length of the longest of such spells. """ thresh = convert_units_to(threshold, data, context="infer") return _spell_length_statistics( @@ -697,8 +685,9 @@ def bivariate_spell_length_statistics( r""" Generate statistic on spells lengths based on two variables. - A spell is when a statistic (`win_reducer`) over a minimum number (`window`) of consecutive timesteps respects a condition (`op` `thresh`). - This returns a statistic over the spells count or lengths. In this bivariate version, conditions on both variables must be fulfilled. + A spell is when a statistic (`win_reducer`) over a minimum number (`window`) of consecutive timesteps + respects a condition (`op` `thresh`). This returns a statistic over the spells count or lengths. + In this bivariate version, conditions on both variables must be fulfilled. Parameters ---------- @@ -812,7 +801,8 @@ def season( >>> season(tas, thresh="0 °C", window=5, op=">", stat="start", freq="YS") Returns the start of the "frost-free" season. The season starts with 5 consecutive days with mean temperature - above 0°C and ends with as many days under or equal to 0°C. And end does not need to be found for a start to be valid. + above 0°C and ends with as many days under or equal to 0°C, and end does not need to be found for a + start to be valid. >>> season( ... pr, @@ -824,9 +814,10 @@ def season( ... freq="YS", ... ) - Returns the length of the "dry" season. The season starts with 7 consecutive days with precipitation under or equal to - 2 mm/d and ends with as many days above 2 mm/d. If no start is found before the first of august, the season is invalid. - If a start is found but no end, the end is set to the last day of the period (December 31st if the dataset is complete). + Returns the length of the "dry" season. The season starts with 7 consecutive days with precipitation under or + equal to 2 mm/d and ends with as many days above 2 mm/d. If no start is found before the first of august, + the season is invalid. If a start is found but no end, the end is set to the last day of the period + (December 31st if the dataset is complete). """ thresh = convert_units_to(thresh, data, context="infer") cond = compare(data, op, thresh, constrain=constrain) @@ -975,7 +966,8 @@ def bivariate_count_occurrences( op_var2 : {">", "gt", "<", "lt", ">=", "ge", "<=", "le", "==", "eq", "!=", "ne"} Logical operator for data variable 2. e.g. arr > thresh. var_reducer : {"all", "any"} - The condition must either be fulfilled on *all* or *any* variables for the period to be considered an occurrence. + The condition must either be fulfilled on *all* or *any* variables + for the period to be considered an occurrence. constrain_var1 : sequence of str, optional Optionally allowed comparison operators for variable 1. constrain_var2 : sequence of str, optional @@ -1008,9 +1000,7 @@ def bivariate_count_occurrences( return to_agg_units(out, data_var1, "count", dim="time") -def diurnal_temperature_range( - low_data: xr.DataArray, high_data: xr.DataArray, reducer: str, freq: str -) -> xr.DataArray: +def diurnal_temperature_range(low_data: xr.DataArray, high_data: xr.DataArray, reducer: str, freq: str) -> xr.DataArray: """ Calculate the diurnal temperature range and reduce according to a statistic. @@ -1137,9 +1127,7 @@ def last_occurrence( @declare_relative_units(threshold="") -def spell_length( - data: xr.DataArray, threshold: Quantified, reducer: str, freq: str, op: str -) -> xr.DataArray: +def spell_length(data: xr.DataArray, threshold: Quantified, reducer: str, freq: str, op: str) -> xr.DataArray: """ Calculate statistics on lengths of spells. @@ -1252,9 +1240,7 @@ def thresholded_statistics( @declare_relative_units(threshold="") -def temperature_sum( - data: xr.DataArray, op: str, threshold: Quantified, freq: str -) -> xr.DataArray: +def temperature_sum(data: xr.DataArray, op: str, threshold: Quantified, freq: str) -> xr.DataArray: """ Calculate the temperature sum above/below a threshold. @@ -1290,9 +1276,7 @@ def temperature_sum( return to_agg_units(out, data, "integral") -def interday_diurnal_temperature_range( - low_data: xr.DataArray, high_data: xr.DataArray, freq: str -) -> xr.DataArray: +def interday_diurnal_temperature_range(low_data: xr.DataArray, high_data: xr.DataArray, freq: str) -> xr.DataArray: """ Calculate the average absolute day-to-day difference in diurnal temperature range. @@ -1320,11 +1304,11 @@ def interday_diurnal_temperature_range( return out -def extreme_temperature_range( - low_data: xr.DataArray, high_data: xr.DataArray, freq: str -) -> xr.DataArray: +def extreme_temperature_range(low_data: xr.DataArray, high_data: xr.DataArray, freq: str) -> xr.DataArray: """ - Calculate the extreme temperature range as the maximum of daily maximum temperature minus the minimum of daily minimum temperature. + Calculate the extreme daily temperature range. + + The maximum of daily maximum temperature minus the minimum of daily minimum temperature. Parameters ---------- @@ -1433,9 +1417,7 @@ def _get_days(_bound, _group, _base_time): masked = group.where((days >= start_d) & (days <= end_d - 1)) res = getattr(masked, op)(dim="time", skipna=True) - res = xr.where( - ((start_d > end_d) | (start_d.isnull()) | (end_d.isnull())), np.nan, res - ) + res = xr.where(((start_d > end_d) | (start_d.isnull()) | (end_d.isnull())), np.nan, res) # Re-add the time dimension with the period's base time. res = res.expand_dims(time=[base_time]) out.append(res) @@ -1449,9 +1431,7 @@ def _get_days(_bound, _group, _base_time): @declare_relative_units(threshold="") -def cumulative_difference( - data: xr.DataArray, threshold: Quantified, op: str, freq: str | None = None -) -> xr.DataArray: +def cumulative_difference(data: xr.DataArray, threshold: Quantified, op: str, freq: str | None = None) -> xr.DataArray: """ Calculate the cumulative difference below/above a given value threshold. @@ -1568,14 +1548,10 @@ def _get_zone_bins( Array of values corresponding to each zone: [zone_min, zone_min+step, ..., zone_max]. """ units = pint2cfunits(str2pint(zone_step)) - mn, mx, step = ( - convert_units_to(str2pint(z), units) for z in [zone_min, zone_max, zone_step] - ) + mn, mx, step = (convert_units_to(str2pint(z), units) for z in [zone_min, zone_max, zone_step]) bins = np.arange(mn, mx + step, step) if (mx - mn) % step != 0: - warnings.warn( - "`zone_max` - `zone_min` is not an integer multiple of `zone_step`. Last zone will be smaller." - ) + warnings.warn("`zone_max` - `zone_min` is not an integer multiple of `zone_step`. Last zone will be smaller.") bins[-1] = mx return xr.DataArray(bins, attrs={"units": units}) @@ -1608,7 +1584,8 @@ def get_zones( bins : xr.DataArray or list of Quantity, optional Zones to be used, either as a DataArray with appropriate units or a list of Quantity. exclude_boundary_zones : bool - Determines whether a zone value is attributed for values in ]`-np.inf`, `zone_min`[ and [`zone_max`, `np.inf`\ [. + Determines whether a zone value is attributed for values in ]`-np.inf`, + `zone_min`[ and [`zone_max`, `np.inf`\ [. close_last_zone_right_boundary : bool Determines if the right boundary of the last zone is closed. @@ -1626,10 +1603,7 @@ def get_zones( "Expected defined parameters in one of these cases." ) elif set(zone_params) != {None}: - warnings.warn( - "Expected either `bins` or [`zone_min`, `zone_max`, `zone_step`], got both. " - "`bins` will be used." - ) + warnings.warn("Expected either `bins` or [`zone_min`, `zone_max`, `zone_step`], got both. `bins` will be used.") # Get zone bins (if necessary) bins = bins if bins is not None else _get_zone_bins(zone_min, zone_max, zone_step) @@ -1646,16 +1620,12 @@ def _get_zone(_da): if close_last_zone_right_boundary: zones = zones.where(da != bins[-1], _get_zone(bins[-2])) if exclude_boundary_zones: - zones = zones.where( - (zones != _get_zone(bins[0] - 1)) & (zones != _get_zone(bins[-1])) - ) + zones = zones.where((zones != _get_zone(bins[0] - 1)) & (zones != _get_zone(bins[-1]))) return zones -def detrend( - ds: xr.DataArray | xr.Dataset, dim="time", deg=1 -) -> xr.DataArray | xr.Dataset: +def detrend(ds: xr.DataArray | xr.Dataset, dim="time", deg=1) -> xr.DataArray | xr.Dataset: """ Detrend data along a given dimension computing a polynomial trend of a given order. @@ -1697,7 +1667,8 @@ def thresholded_events( r""" Thresholded events. - Finds all events along the time dimension. An event starts if the start condition is fulfilled for a given number of consecutive time steps. + Finds all events along the time dimension. + An event starts if the start condition is fulfilled for a given number of consecutive time steps. It ends when the end condition is fulfilled for a given number of consecutive time steps. Conditions are simple comparison of the data with a threshold: ``cond = data op thresh``. diff --git a/src/xclim/indices/helpers.py b/src/xclim/indices/helpers.py index 4475830fc..ee9aba3c4 100644 --- a/src/xclim/indices/helpers.py +++ b/src/xclim/indices/helpers.py @@ -64,9 +64,7 @@ def distance_from_sun(dates: xr.DataArray) -> xr.DataArray: cal = get_calendar(dates) if cal == "default": cal = "standard" - days_since = cftime.date2num( - ensure_cftime_array(dates), "days since 2000-01-01 12:00:00", calendar=cal - ) + days_since = cftime.date2num(ensure_cftime_array(dates), "days since 2000-01-01 12:00:00", calendar=cal) g = ((357.528 + 0.9856003 * days_since) % 360) * np.pi / 180 sun_earth = 1.00014 - 0.01671 * np.cos(g) - 0.00014 * np.cos(2.0 * g) return xr.DataArray(sun_earth, coords=dates.coords, dims=dates.dims) @@ -140,9 +138,7 @@ def solar_declination(time: xr.DataArray, method="spencer") -> xr.DataArray: + 0.001480 * np.sin(3 * da) ) else: - raise NotImplementedError( - f"Method {method} must be one of 'simple' or 'spencer'" - ) + raise NotImplementedError(f"Method {method} must be one of 'simple' or 'spencer'") return _wrap_radians(sd).assign_attrs(units="rad").rename("declination") @@ -169,19 +165,13 @@ def time_correction_for_solar_angle(time: xr.DataArray) -> xr.DataArray: """ da = convert_units_to(day_angle(time), "rad") tc = ( - 0.004297 - + 0.107029 * np.cos(da) - - 1.837877 * np.sin(da) - - 0.837378 * np.cos(2 * da) - - 2.340475 * np.sin(2 * da) + 0.004297 + 0.107029 * np.cos(da) - 1.837877 * np.sin(da) - 0.837378 * np.cos(2 * da) - 2.340475 * np.sin(2 * da) ) tc = tc.assign_attrs(units="degrees") return _wrap_radians(convert_units_to(tc, "rad")) -def eccentricity_correction_factor( - time: xr.DataArray, method: str = "spencer" -) -> xr.DataArray: +def eccentricity_correction_factor(time: xr.DataArray, method: str = "spencer") -> xr.DataArray: """ Eccentricity correction factor of the Earth's orbit. @@ -239,14 +229,15 @@ def cosine_of_solar_zenith_angle( Cosine of the solar zenith angle. The solar zenith angle is the angle between a vertical line (perpendicular to the ground) and the sun rays. - This function computes a statistic of its cosine : its instantaneous value, the integral from sunrise to sunset or the average over - the same period or over a subdaily interval. + This function computes a statistic of its cosine : its instantaneous value, + the integral from sunrise to sunset or the average over the same period or over a subdaily interval. Based on :cite:t:`kalogirou_chapter_2014` and :cite:t:`di_napoli_mean_2020`. Parameters ---------- time : xr.DataArray - The UTC time. If not daily and `stat` is "integral" or "average", the timestamp is taken as the start of interval. + The UTC time. If not daily and `stat` is "integral" or "average", + the timestamp is taken as the start of interval. If daily, the interval is assumed to be centered on Noon. If fewer than three timesteps are given, a daily frequency is assumed. declination : xr.DataArray @@ -297,19 +288,13 @@ def cosine_of_solar_zenith_angle( h_e = np.pi - 1e-9 # just below pi else: if time.dtype == "O": # cftime - time_as_s = time.copy( - data=xr.CFTimeIndex(cast(time.values, np.ndarray)).asi8 / 1e6 - ) + time_as_s = time.copy(data=xr.CFTimeIndex(cast(time.values, np.ndarray)).asi8 / 1e6) else: # numpy time_as_s = time.copy(data=time.astype(float) / 1e9) - h_s_utc = (((time_as_s % S_IN_D) / S_IN_D) * 2 * np.pi + np.pi).assign_attrs( - units="rad" - ) + h_s_utc = (((time_as_s % S_IN_D) / S_IN_D) * 2 * np.pi + np.pi).assign_attrs(units="rad") h_s = h_s_utc + lon - interval_as_s = time.diff("time").dt.seconds.reindex( - time=time.time, method="bfill" - ) + interval_as_s = time.diff("time").dt.seconds.reindex(time=time.time, method="bfill") h_e = h_s + 2 * np.pi * interval_as_s / S_IN_D if stat == "instant": @@ -317,13 +302,10 @@ def cosine_of_solar_zenith_angle( return cast( xr.DataArray, - np.sin(declination) * np.sin(lat) - + np.cos(declination) * np.cos(lat) * np.cos(h_s), + np.sin(declination) * np.sin(lat) + np.cos(declination) * np.cos(lat) * np.cos(h_s), ).clip(0, None) if stat not in {"average", "integral"}: - raise NotImplementedError( - "Argument 'stat' must be one of 'integral', 'average' or 'instant'." - ) + raise NotImplementedError("Argument 'stat' must be one of 'integral', 'average' or 'instant'.") if sunlit: # hour angle of sunset (eq. 2.15), with NaNs inside the polar day/night tantan = cast(xr.DataArray, -np.tan(lat) * np.tan(declination)) @@ -346,9 +328,7 @@ def cosine_of_solar_zenith_angle( @nb.vectorize -def _sunlit_integral_of_cosine_of_solar_zenith_angle( - declination, lat, h_sunset, h_start, h_end, average -): +def _sunlit_integral_of_cosine_of_solar_zenith_angle(declination, lat, h_sunset, h_start, h_end, average): """Integral of the cosine of the solar zenith angle over the sunlit part of the interval.""" # Code inspired by PyWBGT h_sunrise = -h_sunset @@ -388,10 +368,7 @@ def _sunlit_integral_of_cosine_of_solar_zenith_angle( h2 = min(h_sunset, h_end) num = np.sin(h2) - np.sin(h1) denum = h2 - h1 - out = ( - np.sin(declination) * np.sin(lat) * denum - + np.cos(declination) * np.cos(lat) * num - ) + out = np.sin(declination) * np.sin(lat) * denum + np.cos(declination) * np.cos(lat) * num if average: out = out / denum return out @@ -442,9 +419,7 @@ def extraterrestrial_solar_radiation( return ( gsc * rad_to_day - * cosine_of_solar_zenith_angle( - times, ds, lat, stat="integral", sunlit=True, chunks=chunks - ) + * cosine_of_solar_zenith_angle(times, ds, lat, stat="integral", sunlit=True, chunks=chunks) * dr ).assign_attrs(units="J m-2 d-1") @@ -485,9 +460,7 @@ def day_lengths( # arccos gives the hour-angle at sunset, multiply by 24 / 2π to get hours. # The day length is twice that. with np.errstate(invalid="ignore"): - day_length_hours = ( - (24 / np.pi) * np.arccos(-np.tan(lat) * np.tan(declination)) - ).assign_attrs(units="h") + day_length_hours = ((24 / np.pi) * np.arccos(-np.tan(lat) * np.tan(declination))).assign_attrs(units="h") return day_length_hours @@ -526,7 +499,8 @@ def wind_speed_height_conversion( if method == "log": if min(h_source, h_target) < 1 + 5.42 / 67.8: raise ValueError( - f"The height {min(h_source, h_target)}m is too small for method {method}. Heights must be greater than {1 + 5.42 / 67.8}" + f"The height {min(h_source, h_target)}m is too small for method {method}. " + f"Heights must be greater than {1 + 5.42 / 67.8}" ) with xr.set_options(keep_attrs=True): return ua * np.log(67.8 * h_target - 5.42) / np.log(67.8 * h_source - 5.42) @@ -553,10 +527,7 @@ def _gather_lat(da: xr.DataArray) -> xr.DataArray: return lat except KeyError as err: n_func = stack()[1].function - msg = ( - f"{n_func} could not find latitude coordinate in DataArray. " - "Try passing it explicitly (`lat=ds.lat`)." - ) + msg = f"{n_func} could not find latitude coordinate in DataArray. Try passing it explicitly (`lat=ds.lat`)." raise ValueError(msg) from err @@ -579,10 +550,7 @@ def _gather_lon(da: xr.DataArray) -> xr.DataArray: return lat except KeyError as err: n_func = stack()[1].function - msg = ( - f"{n_func} could not find longitude coordinate in DataArray. " - "Try passing it explicitly (`lon=ds.lon`)." - ) + msg = f"{n_func} could not find longitude coordinate in DataArray. Try passing it explicitly (`lon=ds.lon`)." raise ValueError(msg) from err @@ -596,7 +564,9 @@ def resample_map( map_kwargs: dict | None = None, ) -> xr.DataArray | xr.Dataset: r""" - Wrap xarray's resample(...).map() with a :py:func:`xarray.map_blocks`, ensuring the chunking is appropriate using flox. + Wrap xarray's resample(...).map() with a :py:func:`xarray.map_blocks`. + + Ensures that the chunking is appropriate using `flox`. Parameters ---------- @@ -662,9 +632,7 @@ def _resample_map(obj_chnk, dm, frq, rs_kws, fun, mp_kws): i += chunksize template = template.chunk({dim: tuple(new_chunks)}) - return obj_rechunked.map_blocks( - _resample_map, (dim, freq, resample_kwargs, func, map_kwargs), template=template - ) + return obj_rechunked.map_blocks(_resample_map, (dim, freq, resample_kwargs, func, map_kwargs), template=template) def _compute_daytime_temperature( @@ -694,9 +662,7 @@ def _compute_daytime_temperature( xarray.DataArray Hourly daytime temperature. """ - return (tasmax - tasmin) * np.sin( - (np.pi * hour_after_sunrise) / (daylength + 4) - ) + tasmin + return (tasmax - tasmin) * np.sin((np.pi * hour_after_sunrise) / (daylength + 4)) + tasmin def _compute_nighttime_temperature( @@ -727,9 +693,7 @@ def _compute_nighttime_temperature( xarray.DataArray Hourly nighttime temperature. """ - return tas_sunset - ((tas_sunset - tasmin) / np.log(24 - daylength)) * np.log( - hours_after_sunset - ) + return tas_sunset - ((tas_sunset - tasmin) / np.log(24 - daylength)) * np.log(hours_after_sunset) def _add_one_day(time: xr.DataArray) -> xr.DataArray: @@ -783,9 +747,7 @@ def make_hourly_temperature(tasmin: xr.DataArray, tasmax: xr.DataArray) -> xr.Da data = xr.concat( [ data, - data.isel(time=-1).assign_coords( - time=_add_one_day(data.isel(time=-1).time) - ), + data.isel(time=-1).assign_coords(time=_add_one_day(data.isel(time=-1).time)), ], dim="time", ) @@ -794,9 +756,7 @@ def make_hourly_temperature(tasmin: xr.DataArray, tasmax: xr.DataArray) -> xr.Da # Create daily chunks to avoid memory issues after the resampling data = data.assign( daylength=daylength, - sunset_temp=_compute_daytime_temperature( - daylength, data.tasmin, data.tasmax, daylength - ), + sunset_temp=_compute_daytime_temperature(daylength, data.tasmin, data.tasmax, daylength), next_tasmin=data.tasmin.shift(time=-1), ) # Compute hourly data by resampling and remove the last time stamp that was added earlier @@ -807,9 +767,7 @@ def make_hourly_temperature(tasmin: xr.DataArray, tasmax: xr.DataArray) -> xr.Da return xr.where( hourly.time.dt.hour < hourly.daylength, - _compute_daytime_temperature( - hourly.time.dt.hour, hourly.tasmin, hourly.tasmax, hourly.daylength - ), + _compute_daytime_temperature(hourly.time.dt.hour, hourly.tasmin, hourly.tasmax, hourly.daylength), _compute_nighttime_temperature( nighttime_hours, hourly.next_tasmin, diff --git a/src/xclim/indices/run_length.py b/src/xclim/indices/run_length.py index 894a08902..230ae2a32 100644 --- a/src/xclim/indices/run_length.py +++ b/src/xclim/indices/run_length.py @@ -65,9 +65,7 @@ def use_ufunc( Otherwise, returns ufunc_1dim. """ if ufunc_1dim is True and freq is not None: - raise ValueError( - "Resampling after run length operations is not implemented for 1d method" - ) + raise ValueError("Resampling after run length operations is not implemented for 1d method") if ufunc_1dim == "from_context": ufunc_1dim = OPTIONS[RUN_LENGTH_UFUNC] @@ -161,9 +159,7 @@ def _cumsum_reset( # Example: da == 100110111 -> cs_s == 100120123 cs = da.cumsum(dim=dim) # cumulative sum e.g. 111233456 cond = da == 0 if reset_on_zero else da.isnull() # reset condition - cs2 = cs.where( - cond - ) # keep only numbers at positions of zeroes e.g. N11NN3NNN (default) + cs2 = cs.where(cond) # keep only numbers at positions of zeroes e.g. N11NN3NNN (default) cs2[{dim: 0}] = 0 # put a zero in front e.g. 011NN3NNN cs2 = cs2.ffill(dim=dim) # e.g. 011113333 out = cs - cs2 @@ -188,8 +184,8 @@ def rle( of each run of consecutive > 0 values, where it is set to the sum of the elements within the run. For an actual run length encoder, see :py:func:`rle_1d`. - Usually, the input would be a boolean mask and the first element of each run would then be set to the run's length (thus the name). - But the function also accepts int and float inputs. + Usually, the input would be a boolean mask and the first element of each run would then be set to + the run's length (thus the name), but the function also accepts int and float inputs. Parameters ---------- @@ -558,9 +554,7 @@ def find_boundary_run(runs, position): da = da.fillna(0) # We expect a boolean array, but there could be NaNs nonetheless if window == 1: if freq is not None: - out = resample_map( - da, dim, freq, find_boundary_run, map_kwargs=dict(position=position) - ) + out = resample_map(da, dim, freq, find_boundary_run, map_kwargs=dict(position=position)) else: out = find_boundary_run(da, position) @@ -579,9 +573,7 @@ def find_boundary_run(runs, position): d = xr.where(d >= window, 1, 0) # for "first" run, return "first" element in the run (and conversely for "last" run) if freq is not None: - out = resample_map( - d, dim, freq, find_boundary_run, map_kwargs=dict(position=position) - ) + out = resample_map(d, dim, freq, find_boundary_run, map_kwargs=dict(position=position)) else: out = find_boundary_run(d, position) @@ -710,13 +702,9 @@ def run_bounds(mask: xr.DataArray, dim: str = "time", coord: bool | str = True): With ``dim`` reduced to "events" and "bounds". The events dim is as long as needed, padded with NaN or NaT. """ if uses_dask(mask): - raise NotImplementedError( - "Dask arrays not supported as we can't know the final event number before computing." - ) + raise NotImplementedError("Dask arrays not supported as we can't know the final event number before computing.") - diff = xr.concat( - (mask.isel({dim: [0]}).astype(int), mask.astype(int).diff(dim)), dim - ) + diff = xr.concat((mask.isel({dim: [0]}).astype(int), mask.astype(int).diff(dim)), dim) nstarts = (diff == 1).sum(dim).max().item() @@ -754,9 +742,7 @@ def _get_indices(arr, *, N): return xr.concat((starts, ends), "bounds") -def keep_longest_run( - da: xr.DataArray, dim: str = "time", freq: str | None = None -) -> xr.DataArray: +def keep_longest_run(da: xr.DataArray, dim: str = "time", freq: str | None = None) -> xr.DataArray: """ Keep the longest run along a dimension. @@ -825,8 +811,9 @@ def runs_with_holes( Notes ----- - A season (as defined in ``season``) could be considered as an event with `window_stop == window_start` and `da_stop == 1 - da_start`, - although it has more constraints on when to start and stop a run through the `date` argument and only one season can be found. + A season (as defined in ``season``) could be considered as an event with ``window_stop == window_start`` + and ``da_stop == 1 - da_start``, although it has more constraints on when to start and stop a run through + the `date` argument and only one season can be found. """ da_start = da_start.astype(int).fillna(0) da_stop = da_stop.astype(int).fillna(0) @@ -934,9 +921,7 @@ def season_end( # Invert the condition and mask all values after beginning # we fillna(0) as so to differentiate series with no runs and all-nan series not_da = (~da).where(da[dim].copy(data=np.arange(da[dim].size)) >= beg.fillna(0)) - end = first_run_after_date( - not_da, window=window, dim=dim, date=mid_date, coord=False - ) + end = first_run_after_date(not_da, window=window, dim=dim, date=mid_date, coord=False) if _beg is None: # Where end is null and beg is not null (valid data, no end detected), put the last index # Don't do this in the fast path, so that the length can use this information @@ -964,7 +949,8 @@ def season( A "season" is a run of True values that may include breaks under a given length (`window`). The start is computed as the first run of `window` True values, and the end as the first subsequent run of `window` False values. The end element is the first element after the season. - If a date is given, it must be included in the season, i.e. the start cannot occur later and the end cannot occur earlier. + If a date is given, it must be included in the season + (i.e. the start cannot occur later and the end cannot occur earlier). Parameters ---------- @@ -1005,24 +991,24 @@ def season( If a date is given, the season start and end are forced to be on each side of this date. This means that even if the "real" season has been over for a long time, this is the date used in the length calculation. e.g. Length of the "warm season", where T > 25°C, with date = 1st August. Let's say the temperature is over - 25 for all June, but July and august have very cold temperatures. Instead of returning 30 days (June), the function - will return 61 days (July + June). + 25 for all June, but July and august have very cold temperatures. Instead of returning 30 days (June), + the function will return 61 days (July + June). - The season's length is always the difference between the end and the start. Except if no - season end was found before the end of the data. In that case the end is set to last element and - the length is set to the data size minus the start index. Thus, for the specific case, :math:`length = end - start + 1`, + The season's length is always the difference between the end and the start. Except if no season end was + found before the end of the data. In that case the end is set to last element and the length is set to + the data size minus the start index. Thus, for the specific case, :math:`length = end - start + 1`, because the end falls on the last element of the season instead of the subsequent one. """ beg = season_start(da, window=window, dim=dim, mid_date=mid_date, coord=False) - # Use fast path in season_end : no recomputing of start, no masking of end where beg.isnull() and don't set end if none found - end = season_end( - da, window=window, dim=dim, mid_date=mid_date, _beg=beg, coord=False - ) + # Use fast path in season_end : no recomputing of start, + # no masking of end where beg.isnull(), and don't set end if none found + end = season_end(da, window=window, dim=dim, mid_date=mid_date, _beg=beg, coord=False) # Three cases : # start no start # end e - s 0 # no end size - s 0 - # da is boolean, so we have no way of knowing if the absence of season is due to missing values or to an actual absence. + # da is boolean, so we have no way of knowing if the absence of season + # is due to missing values or to an actual absence. length = xr.where( beg.isnull(), 0, @@ -1271,9 +1257,7 @@ def first_run_before_date( """ if date is not None: mid_idx = index_of_date(da[dim], date, max_idxs=1, default=0) - if ( - mid_idx.size == 0 - ): # The date is not within the group. Happens at boundaries. + if mid_idx.size == 0: # The date is not within the group. Happens at boundaries. return xr.full_like(da.isel({dim: 0}), np.nan, float).drop_vars(dim) # Mask anything after the mid_date + window - 1 # Thus, the latest run possible can begin on the day just before mid_idx @@ -1427,11 +1411,11 @@ def windowed_run_events_1d(arr: Sequence[bool], window: int) -> xr.DataArray: return (v * rl >= window).sum() -def windowed_run_count_ufunc( - x: xr.DataArray | Sequence[bool], window: int, dim: str -) -> xr.DataArray: +def windowed_run_count_ufunc(x: xr.DataArray | Sequence[bool], window: int, dim: str) -> xr.DataArray: """ - Dask-parallel version of windowed_run_count_1d, i.e. the number of consecutive true values in array for runs at least as long as given duration. + Dask-parallel version of windowed_run_count_1d. + + The number of consecutive true values in array for runs at least as long as given duration. Parameters ---------- @@ -1459,11 +1443,11 @@ def windowed_run_count_ufunc( ) -def windowed_run_events_ufunc( - x: xr.DataArray | Sequence[bool], window: int, dim: str -) -> xr.DataArray: +def windowed_run_events_ufunc(x: xr.DataArray | Sequence[bool], window: int, dim: str) -> xr.DataArray: """ - Dask-parallel version of windowed_run_events_1d, i.e. the number of runs at least as long as given duration. + Dask-parallel version of windowed_run_events_1d. + + The number of runs at least as long as given duration. Parameters ---------- @@ -1498,7 +1482,9 @@ def statistics_run_ufunc( dim: str = "time", ) -> xr.DataArray: """ - Dask-parallel version of statistics_run_1d, i.e. the {reducer} number of consecutive true values in array. + Dask-parallel version of statistics_run_1d. + + The {reducer} number of consecutive true values in array. Parameters ---------- @@ -1534,7 +1520,9 @@ def first_run_ufunc( dim: str, ) -> xr.DataArray: """ - Dask-parallel version of first_run_1d, i.e. the first entry in array of consecutive true values. + Dask-parallel version of first_run_1d. + + The first entry in array of consecutive true values. Parameters ---------- @@ -1564,9 +1552,7 @@ def first_run_ufunc( return ind -def lazy_indexing( - da: xr.DataArray, index: xr.DataArray, dim: str | None = None -) -> xr.DataArray: +def lazy_indexing(da: xr.DataArray, index: xr.DataArray, dim: str | None = None) -> xr.DataArray: """ Get values of `da` at indices `index` in a NaN-aware and lazy manner. @@ -1609,13 +1595,9 @@ def _index_from_1d_array(indices, array): # for each chunk of index, take corresponding values from da out = index.map_blocks(_index_from_1d_array, args=(da2,)).rename(da.name) # mask where index was NaN. Drop any auxiliary coord, they are already on `out`. - # Chunked aux coord would have the same name on both sides and xarray will want to check if they are equal, which means loading them - # making lazy_indexing not lazy. same issue as above - out = out.where( - ~invalid.drop_vars( - [crd for crd in invalid.coords if crd not in invalid.dims] - ) - ) + # Chunked aux coord would have the same name on both sides and xarray will want to check if they are equal, + # which means loading them making lazy_indexing not lazy. same issue as above + out = out.where(~invalid.drop_vars([crd for crd in invalid.coords if crd not in invalid.dims])) out = out.assign_coords(auxcrd.coords) if idx_ndim == 0: # 0-D case, drop useless coords and dummy dim @@ -1626,9 +1608,7 @@ def _index_from_1d_array(indices, array): if dim is None: diff_dims = set(da.dims) - set(index.dims) if len(diff_dims) == 0: - raise ValueError( - "da must have at least one dimension more than index for lazy_indexing." - ) + raise ValueError("da must have at least one dimension more than index for lazy_indexing.") if len(diff_dims) > 1: raise ValueError( "If da has more than one dimension more than index, the indexing dim must be given through `dim`" @@ -1689,13 +1669,9 @@ def index_of_date( date = datetime.strptime(date, "%m-%d") year_cond = True - idxs = np.where( - year_cond & (time.dt.month == date.month) & (time.dt.day == date.day) - )[0] + idxs = np.where(year_cond & (time.dt.month == date.month) & (time.dt.day == date.day))[0] if max_idxs is not None and idxs.size > max_idxs: - raise ValueError( - f"More than {max_idxs} instance of date {date} found in the coordinate array." - ) + raise ValueError(f"More than {max_idxs} instance of date {date} found in the coordinate array.") return idxs @@ -1806,15 +1782,13 @@ def _find_events(da_start, da_stop, data, window_start, window_stop): ds = rle(runs).fillna(0).astype(np.int16).to_dataset(name="event_length") # Time duration where the precipitation threshold is exceeded during an event # (duration of complete run - duration of holes in the run ) - ds["event_effective_length"] = _cumsum_reset( - da_start.where(runs == 1), index="first", reset_on_zero=False - ).astype(np.int16) + ds["event_effective_length"] = _cumsum_reset(da_start.where(runs == 1), index="first", reset_on_zero=False).astype( + np.int16 + ) if data is not None: # Ex: Cumulated precipitation in a given freezing rain event - ds["event_sum"] = _cumsum_reset( - data.where(runs == 1), index="first", reset_on_zero=False - ) + ds["event_sum"] = _cumsum_reset(data.where(runs == 1), index="first", reset_on_zero=False) # Keep time as a variable, it will be used to keep start of events ds["event_start"] = ds["time"].broadcast_like(ds) # .astype(int) @@ -1863,9 +1837,7 @@ def _get_start_cftime(deltas, time_min=None): output_dtypes=[time_min.dtype], ) else: - ds["event_start"] = ds.event_start.copy( - data=time_min.values + ds.event_start.data.astype("timedelta64[s]") - ) + ds["event_start"] = ds.event_start.copy(data=time_min.values + ds.event_start.data.astype("timedelta64[s]")) ds["event"] = np.arange(1, ds.event.size + 1) ds["event_length"].attrs["units"] = "1" @@ -1891,8 +1863,8 @@ def find_events( An event starts with a run of ``window`` consecutive True values in the condition and stops with ``window_stop`` consecutive True values in the stop condition. - This returns a Dataset with each event along an `event` dimension. It does not - perform statistics over the events like other function in this module do. + This returns a Dataset with each event along an `event` dimension. + It does not perform statistics over the events like other function in this module do. Parameters ---------- @@ -1914,12 +1886,12 @@ def find_events( Returns ------- - xr.Dataset, same shape as the data it has a new "event" dimension (and the time dimension is resample or removed, according to ``freq``). + xr.Dataset, same shape as the data (and the time dimension is resample or removed, according to ``freq``). The Dataset has the following variables: event_length: The number of time steps in each event event_effective_length: The number of time steps of even event where the start condition is true. event_start: The datetime of the start of the run. - event_sum: The sum within each event, only considering the steps where start condition is true. Only present if ``data`` is given. + event_sum: The sum within each event, only considering steps where start condition is true (if ``data``). """ if condition_stop is None: condition_stop = ~condition @@ -1931,7 +1903,5 @@ def find_events( if data is not None: ds = ds.assign(data=data) return ds.resample(time=freq).map( - lambda grp: _find_events( - grp.da_start, grp.da_stop, grp.get("data", None), window, window_stop - ) + lambda grp: _find_events(grp.da_start, grp.da_stop, grp.get("data", None), window, window_stop) ) diff --git a/src/xclim/indices/stats.py b/src/xclim/indices/stats.py index 9aef2699c..d563fb7f3 100644 --- a/src/xclim/indices/stats.py +++ b/src/xclim/indices/stats.py @@ -61,17 +61,14 @@ def _fitfunc_1d(arr, *, dist, nparams, method, **fitkwargs): for i, arg in enumerate(args): guess[param_info[i]] = arg - fitresult = scipy.stats.fit( - dist=dist, data=x, method="mse", guess=guess, **fitkwargs - ) + fitresult = scipy.stats.fit(dist=dist, data=x, method="mse", guess=guess, **fitkwargs) params = fitresult.params elif method == "PWM": # lmoments3 will raise an error if only dist.numargs + 2 values are provided if len(x) <= dist.numargs + 2: return np.asarray([np.nan] * nparams) if (type(dist).__name__ != "GammaGen" and len(fitkwargs.keys()) != 0) or ( - type(dist).__name__ == "GammaGen" - and set(fitkwargs.keys()) - {"floc"} != set() + type(dist).__name__ == "GammaGen" and set(fitkwargs.keys()) - {"floc"} != set() ): raise ValueError( "Lmoments3 does not use `fitkwargs` arguments, except for `floc` with the Gamma distribution." @@ -118,13 +115,13 @@ def fit( Name of the univariate distribution, such as beta, expon, genextreme, gamma, gumbel_r, lognorm, norm (see :py:mod:scipy.stats for full list) or the distribution object itself. method : {"ML", "MLE", "MM", "PWM", "APP", "MSE", "MPS"} - Fitting method, either maximum likelihood (ML or MLE), method of moments (MM), maximum product of spacings (MSE or MPS) - or approximate method (APP). - Can also be the probability weighted moments (PWM), also called L-Moments, if a compatible `dist` object is passed. - The PWM method is usually more robust to outliers. The MSE method is more consistent than the MLE method, although - it can be more sensitive to repeated data. + Fitting method, either maximum likelihood (ML or MLE), method of moments (MM), + maximum product of spacings (MSE or MPS) or approximate method (APP). + If `dist` is an instance from the lmoments3 library, accepts probability weighted moments (PWM; "L-Moments"). + The PWM method is usually more robust to outliers. + The MSE method is more consistent than the MLE method, although it can be more sensitive to repeated data. For the MSE method, each variable parameter must be given finite bounds - (provided with keyword argument bounds={'param_name':(min,max),...}). + (provided with keyword argument `bounds={'param_name':(min,max),...}`). dim : str The dimension upon which to perform the indexing (default: "time"). **fitkwargs : dict @@ -158,7 +155,8 @@ def fit( if method == "PWM" and not hasattr(dist, "lmom_fit"): raise ValueError( - f"The given distribution {dist} does not implement the PWM fitting method. Please pass an instance from the lmoments3 package." + f"The given distribution {dist} does not implement the PWM fitting method. " + "Please pass an instance from the lmoments3 package." ) shape_params = [] if dist.shapes is None else dist.shapes.split(",") @@ -187,9 +185,7 @@ def fit( dims = [d if d != dim else "dparams" for d in da.dims] out = data.assign_coords(dparams=dist_params).transpose(*dims) - out.attrs = prefix_attrs( - da.attrs, ["standard_name", "long_name", "units", "description"], "original_" - ) + out.attrs = prefix_attrs(da.attrs, ["standard_name", "long_name", "units", "description"], "original_") attrs = { "long_name": f"{dist.name} parameters", "description": f"Parameters of the {dist.name} distribution", @@ -369,7 +365,7 @@ def fa( Whether we are looking for a probability of exceedance (max) or a probability of non-exceedance (min). method : {"ML", "MLE", "MOM", "PWM", "APP"} Fitting method, either maximum likelihood (ML or MLE), method of moments (MOM) or approximate method (APP). - Also accepts probability weighted moments (PWM), also called L-Moments, if `dist` is an instance from the lmoments3 library. + If `dist` is an instance from the lmoments3 library, accepts probability weighted moments (PWM; "L-Moments"). The PWM method is usually more robust to outliers. Returns @@ -395,11 +391,7 @@ def fa( raise ValueError(f"Mode `{mode}` should be either 'max' or 'min'.") # Compute the quantiles - out = ( - parametric_quantile(p, q, dist) - .rename({"quantile": "return_period"}) - .assign_coords(return_period=t) - ) + out = parametric_quantile(p, q, dist).rename({"quantile": "return_period"}).assign_coords(return_period=t) out.attrs["mode"] = mode return out @@ -434,9 +426,9 @@ def frequency_analysis( freq : str, optional Resampling frequency. If None, the frequency is assumed to be 'YS' unless the indexer is season='DJF', in which case `freq` would be set to `YS-DEC`. - method : {"ML" or "MLE", "MOM", "PWM", "APP"} + method : {"ML", "MLE", "MOM", "PWM", "APP"} Fitting method, either maximum likelihood (ML or MLE), method of moments (MOM) or approximate method (APP). - Also accepts probability weighted moments (PWM), also called L-Moments, if `dist` is an instance from the lmoments3 library. + If `dist` is an instance from the lmoments3 library, accepts probability weighted moments (PWM; "L-Moments"). The PWM method is usually more robust to outliers. **indexer : {dim: indexer, }, optional Time attribute and values over which to subset the array. For example, use season='DJF' to select winter values, @@ -667,9 +659,7 @@ def dist_method( # Typically the data to be transformed arg = [arg] if arg is not None else [] if function == "nnlf": - raise ValueError( - "This method is not supported because it reduces the dimensionality of the data." - ) + raise ValueError("This method is not supported because it reduces the dimensionality of the data.") # We don't need to set `input_core_dims` because we're explicitly splitting the parameters here. args = arg + [fit_params.sel(dparams=dp) for dp in fit_params.dparams.values] @@ -687,9 +677,7 @@ def dist_method( ) -def preprocess_standardized_index( - da: xr.DataArray, freq: str | None, window: int, **indexer -): +def preprocess_standardized_index(da: xr.DataArray, freq: str | None, window: int, **indexer): r""" Perform resample and roll operations involved in computing a standardized index. @@ -698,8 +686,9 @@ def preprocess_standardized_index( da : xarray.DataArray Input array. freq : {'D', 'MS'}, optional - Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes that desired resampling - has already been applied input dataset and will skip the resampling step. + Resampling frequency. A monthly or daily frequency is expected. + Option `None` assumes that desired resampling has already been applied input dataset + and will skip the resampling step. window : int Averaging window length relative to the resampling frequency. For example, if `freq="MS"`, i.e. a monthly resampling, the window is an integer number of months. @@ -730,7 +719,8 @@ def preprocess_standardized_index( ) else: warnings.warn( - "No resampling frequency was specified and a frequency for the dataset could not be identified with ``xr.infer_freq``" + "No resampling frequency was specified and a frequency " + "for the dataset could not be identified with ``xr.infer_freq``" ) group = "time.dayofyear" @@ -806,13 +796,14 @@ def standardized_index_fit_params( Supported combinations of `dist` and `method` are: * Gamma ("gamma") : "ML", "APP" * Log-logistic ("fisk") : "ML", "APP" - * "APP" method only supports two-parameter distributions. Parameter `loc` will be set to 0 (setting `floc=0` in `fitkwargs`). - * Otherwise, generic `rv_continuous` methods can be used. This includes distributions from `lmoments3` which should be used with - `method="PWM"`. + * "APP" method only supports two-parameter distributions. Parameter `loc` will be set to 0 + (setting `floc=0` in `fitkwargs`). + * Otherwise, generic `rv_continuous` methods can be used. This includes distributions from `lmoments3` + which should be used with `method="PWM"`. - When using the zero inflated option, : A probability density function :math:`\texttt{pdf}_0(X)` is fitted for :math:`X \neq 0` - and a supplementary parameter :math:`\pi` takes into account the probability of :math:`X = 0`. The full probability density - function is a piecewise function: + When using the zero inflated option, : A probability density function :math:`\texttt{pdf}_0(X)` is fitted + for :math:`X \neq 0` and a supplementary parameter :math:`\pi` takes into account the probability of + :math:`X = 0`. The full probability density function is a piecewise function: .. math:: @@ -822,31 +813,21 @@ def standardized_index_fit_params( if method == "APP": if "floc" not in fitkwargs.keys(): raise ValueError( - "The APP method is only supported for two-parameter distributions with `gamma` or `fisk` with `loc` being fixed." - "Pass a value for `floc` in `fitkwargs`." + "The APP method is only supported for two-parameter distributions with `gamma` or `fisk` " + "with `loc` being fixed. Pass a value for `floc` in `fitkwargs`." ) dist_and_methods = {"gamma": ["ML", "APP"], "fisk": ["ML", "APP"]} dist = get_dist(dist) if method != "PWM": if dist.name not in dist_and_methods: - raise NotImplementedError( - f"The distribution `{dist.name}` is not supported." - ) + raise NotImplementedError(f"The distribution `{dist.name}` is not supported.") if method not in dist_and_methods[dist.name]: - raise NotImplementedError( - f"The method `{method}` is not supported for distribution `{dist.name}`." - ) + raise NotImplementedError(f"The method `{method}` is not supported for distribution `{dist.name}`.") da, group = preprocess_standardized_index(da, freq, window, **indexer) if zero_inflated: - prob_of_zero = da.groupby(group).map( - lambda x: (x == 0).sum("time") / x.notnull().sum("time") - ) - params = ( - da.where(da != 0) - .groupby(group) - .map(fit, dist=dist, method=method, **fitkwargs) - ) + prob_of_zero = da.groupby(group).map(lambda x: (x == 0).sum("time") / x.notnull().sum("time")) + params = da.where(da != 0).groupby(group).map(fit, dist=dist, method=method, **fitkwargs) params["prob_of_zero"] = prob_of_zero else: params = da.groupby(group).map(fit, dist=dist, method=method, **fitkwargs) @@ -933,16 +914,18 @@ def standardized_index( Notes ----- - * The standardized index is bounded by ±8.21. 8.21 is the largest standardized index as constrained by the float64 precision in - the inversion to the normal distribution. - * ``window``, ``dist``, ``method``, ``zero_inflated`` are only optional if ``params`` is given. If `params` is given as input, - it overrides the `cal_start`, `cal_end`, `freq` and `window`, `dist` and `method` options. + * The standardized index is bounded by ±8.21. 8.21 is the largest standardized index as constrained by + the float64 precision in the inversion to the normal distribution. + * ``window``, ``dist``, ``method``, ``zero_inflated`` are only optional if ``params`` is given. + If `params` is given as input, it overrides the `cal_start`, `cal_end`, `freq` and `window`, + `dist` and `method` options. * Supported combinations of `dist` and `method` are: * Gamma ("gamma") : "ML", "APP" * Log-logistic ("fisk") : "ML", "APP" - * "APP" method only supports two-parameter distributions. Parameter `loc` will be set to 0 (setting `floc=0` in `fitkwargs`). - * Otherwise, generic `rv_continuous` methods can be used. This includes distributions from `lmoments3` which should be used with - `method="PWM"`. + * "APP" method only supports two-parameter distributions. + Parameter `loc` will be set to 0 (setting `floc=0` in `fitkwargs`). + * Otherwise, generic `rv_continuous` methods can be used. + This includes distributions from `lmoments3` which should be used with `method="PWM"`. References ---------- @@ -950,9 +933,7 @@ def standardized_index( """ # use input arguments from ``params`` if it is given if params is not None: - freq, window, dist, indexer = ( - params.attrs[s] for s in ["freq", "window", "scipy_dist", "time_indexer"] - ) + freq, window, dist, indexer = (params.attrs[s] for s in ["freq", "window", "scipy_dist", "time_indexer"]) # Unpack attrs to None and {} if needed freq = None if freq == "" else freq indexer = json.loads(indexer) @@ -965,9 +946,7 @@ def standardized_index( else: for p in [window, dist, method, zero_inflated]: if p is None: - raise ValueError( - "If `params` is `None`, `window`, `dist`, `method` and `zero_inflated` must be given." - ) + raise ValueError("If `params` is `None`, `window`, `dist`, `method` and `zero_inflated` must be given.") # apply resampling and rolling operations da, _ = preprocess_standardized_index(da, freq=freq, window=window, **indexer) if params is None: @@ -989,18 +968,14 @@ def standardized_index( if paramsd != template.sizes: params = params.broadcast_like(template) - def reindex_time( - _da: xr.DataArray, _da_ref: xr.DataArray, _group: str - ): # numpydoc ignore=GL08 + def reindex_time(_da: xr.DataArray, _da_ref: xr.DataArray, _group: str): # numpydoc ignore=GL08 if group == "time.dayofyear": _da = resample_doy(_da, _da_ref) elif group == "time.month": _da = _da.rename(month="time").reindex(time=_da_ref.time.dt.month) _da["time"] = _da_ref.time elif group == "time.week": - _da = _da.rename(week="time").reindex( - time=_da_ref.time.dt.isocalendar().week - ) + _da = _da.rename(week="time").reindex(time=_da_ref.time.dt.isocalendar().week) _da["time"] = _da_ref.time # I don't think rechunking is necessary here, need to check return _da if not uses_dask(_da) else _da.chunk({"time": -1}) diff --git a/src/xclim/testing/conftest.py b/src/xclim/testing/conftest.py index 15ae34a5c..05388a7ea 100644 --- a/src/xclim/testing/conftest.py +++ b/src/xclim/testing/conftest.py @@ -53,9 +53,7 @@ def nimbus(threadsafe_data_dir, worker_id): # numpydoc ignore=PR01 return _nimbus( repo=TESTDATA_REPO_URL, branch=TESTDATA_BRANCH, - cache_dir=( - TESTDATA_CACHE_DIR if worker_id == "master" else threadsafe_data_dir - ), + cache_dir=(TESTDATA_CACHE_DIR if worker_id == "master" else threadsafe_data_dir), ) @@ -99,15 +97,11 @@ def _is_matplotlib_installed(): @pytest.fixture(scope="session", autouse=True) -def doctest_setup( - xdoctest_namespace, nimbus, worker_id, open_dataset -) -> None: # numpydoc ignore=PR01 +def doctest_setup(xdoctest_namespace, nimbus, worker_id, open_dataset) -> None: # numpydoc ignore=PR01 """Gather testing data on doctest run.""" testing_setup_warnings() gather_testing_data(worker_cache_dir=nimbus.path, worker_id=worker_id) - xdoctest_namespace.update( - generate_atmos(branch=TESTDATA_BRANCH, cache_dir=nimbus.path) - ) + xdoctest_namespace.update(generate_atmos(branch=TESTDATA_BRANCH, cache_dir=nimbus.path)) class AttrDict(dict): # numpydoc ignore=PR01 """A dictionary that allows access to its keys as attributes.""" diff --git a/src/xclim/testing/diagnostics.py b/src/xclim/testing/diagnostics.py index 0b28c9a47..f5f6d0c96 100644 --- a/src/xclim/testing/diagnostics.py +++ b/src/xclim/testing/diagnostics.py @@ -6,6 +6,7 @@ This module is meant to compare results with those expected from papers, or create figures illustrating the behavior of sdba methods and utilities. """ + from __future__ import annotations import warnings @@ -31,9 +32,7 @@ __all__ = ["adapt_freq_graph", "cannon_2015_figure_2", "synth_rainfall"] -def synth_rainfall( - shape: float, scale: float = 1.0, wet_freq: float = 0.25, size: int = 1 -) -> np.ndarray: +def synth_rainfall(shape: float, scale: float = 1.0, wet_freq: float = 0.25, size: int = 1) -> np.ndarray: r""" Return gamma distributed rainfall values for wet days. @@ -113,18 +112,10 @@ def cannon_2015_figure_2() -> plt.Figure: ax1.set_ylabel("Density") tau = np.array([0.25, 0.5, 0.75, 0.95, 0.99]) * 100 - bc_gcm = ( - scoreatpercentile(sim, tau) - scoreatpercentile(hist, tau) - ) / scoreatpercentile(hist, tau) - bc_qdm = ( - scoreatpercentile(sim_qdm, tau) - scoreatpercentile(ref, tau) - ) / scoreatpercentile(ref, tau) - bc_eqm = ( - scoreatpercentile(sim_eqm, tau) - scoreatpercentile(ref, tau) - ) / scoreatpercentile(ref, tau) - bc_dqm = ( - scoreatpercentile(sim_dqm, tau) - scoreatpercentile(ref, tau) - ) / scoreatpercentile(ref, tau) + bc_gcm = (scoreatpercentile(sim, tau) - scoreatpercentile(hist, tau)) / scoreatpercentile(hist, tau) + bc_qdm = (scoreatpercentile(sim_qdm, tau) - scoreatpercentile(ref, tau)) / scoreatpercentile(ref, tau) + bc_eqm = (scoreatpercentile(sim_eqm, tau) - scoreatpercentile(ref, tau)) / scoreatpercentile(ref, tau) + bc_dqm = (scoreatpercentile(sim_dqm, tau) - scoreatpercentile(ref, tau)) / scoreatpercentile(ref, tau) ax2.plot([0, 1], [0, 1], ls=":", color="blue") ax2.plot(bc_gcm, bc_gcm, "-", color="blue", label="GCM") diff --git a/src/xclim/testing/helpers.py b/src/xclim/testing/helpers.py index 7b1df4a90..370aa4811 100644 --- a/src/xclim/testing/helpers.py +++ b/src/xclim/testing/helpers.py @@ -80,9 +80,7 @@ def generate_atmos( ds.to_netcdf(atmos_file, engine="h5netcdf") # Give access to dataset variables by name in namespace - with xtu.open_dataset( - atmos_file, branch=branch, cache_dir=cache_dir, engine="h5netcdf" - ) as ds: + with xtu.open_dataset(atmos_file, branch=branch, cache_dir=cache_dir, engine="h5netcdf") as ds: namespace = {f"{var}_dataset": ds[var] for var in ds.data_vars} return namespace @@ -127,12 +125,7 @@ def add_example_file_paths() -> dict[str, str | list[xr.DataArray]]: "path_to_tas_file": "ERA5/daily_surface_cancities_1990-1993.nc", "path_to_tasmax_file": "NRCANdaily/nrcan_canada_daily_tasmax_1990.nc", "path_to_tasmin_file": "NRCANdaily/nrcan_canada_daily_tasmin_1990.nc", - "path_to_example_py": ( - Path(__file__).parent.parent.parent.parent - / "docs" - / "notebooks" - / "example.py" - ), + "path_to_example_py": (Path(__file__).parent.parent.parent.parent / "docs" / "notebooks" / "example.py"), } # For core.utils.load_module example @@ -220,17 +213,12 @@ def test_timeseries( A DataArray or Dataset with time, lon and lat dimensions. """ if calendar or cftime: - coords = xr.cftime_range( - start, periods=len(values), freq=freq, calendar=calendar or "standard" - ) + coords = xr.cftime_range(start, periods=len(values), freq=freq, calendar=calendar or "standard") else: coords = pd.date_range(start, periods=len(values), freq=freq) if variable in VARIABLES: - attrs = { - a: VARIABLES[variable].get(a, "") - for a in ["description", "standard_name", "cell_methods"] - } + attrs = {a: VARIABLES[variable].get(a, "") for a in ["description", "standard_name", "cell_methods"]} attrs["units"] = VARIABLES[variable]["canonical_units"] else: @@ -261,9 +249,7 @@ def _raise_on_compute(dsk: dict): AssertionError If the dask computation is triggered. """ - raise AssertionError( - f"Not lazy. Computation was triggered with a graph of {len(dsk)} tasks." - ) + raise AssertionError(f"Not lazy. Computation was triggered with a graph of {len(dsk)} tasks.") assert_lazy = Callback(start=_raise_on_compute) diff --git a/src/xclim/testing/utils.py b/src/xclim/testing/utils.py index a06add28d..fff7e0cb6 100644 --- a/src/xclim/testing/utils.py +++ b/src/xclim/testing/utils.py @@ -43,10 +43,7 @@ try: import pooch except ImportError: - warnings.warn( - "The `pooch` library is not installed. " - "The default cache directory for testing data will not be set." - ) + warnings.warn("The `pooch` library is not installed. The default cache directory for testing data will not be set.") pooch = None @@ -75,9 +72,7 @@ default_testdata_version = "v2025.1.8" """Default version of the testing data to use when fetching datasets.""" -default_testdata_repo_url = ( - "https://raw.githubusercontent.com/Ouranosinc/xclim-testdata/" -) +default_testdata_repo_url = "https://raw.githubusercontent.com/Ouranosinc/xclim-testdata/" """Default URL of the testing data repository to use when fetching datasets.""" try: @@ -146,9 +141,7 @@ """ -def list_input_variables( - submodules: Sequence[str] | None = None, realms: Sequence[str] | None = None -) -> dict: +def list_input_variables(submodules: Sequence[str] | None = None, realms: Sequence[str] | None = None) -> dict: """ List all possible variables names used in xclim's indicators. @@ -173,9 +166,7 @@ def list_input_variables( from xclim.core.indicator import registry # pylint: disable=import-outside-toplevel from xclim.core.utils import InputKind # pylint: disable=import-outside-toplevel - submodules = submodules or [ - sub for sub in dir(indicators) if not sub.startswith("__") - ] + submodules = submodules or [sub for sub in dir(indicators) if not sub.startswith("__")] realms = realms or ["atmos", "ocean", "land", "seaIce"] variables = defaultdict(list) @@ -269,9 +260,7 @@ def publish_release_notes( for title_expression, level in titles.items(): found = re.findall(title_expression, changes) for grouping in found: - fixed_grouping = ( - str(grouping[0]).replace("(", r"\(").replace(")", r"\)") - ) + fixed_grouping = str(grouping[0]).replace("(", r"\(").replace(")", r"\)") search = rf"({fixed_grouping})\n([\{level}]{'{' + str(len(grouping[1])) + '}'})" replacement = f"{'##' if level == '-' else '###'} {grouping[0]}" changes = re.sub(search, replacement, changes) @@ -328,7 +317,8 @@ def show_versions( file : {os.PathLike, StringIO, TextIO}, optional If provided, prints to the given file-like object. Otherwise, returns a string. deps : list of str, optional - A list of dependencies to gather and print version information from. Otherwise, prints `xclim` dependencies. + A list of dependencies to gather and print version information from. + Otherwise, prints `xclim` dependencies. Returns ------- @@ -404,15 +394,13 @@ def run_doctests(): def testing_setup_warnings(): """Warn users about potential incompatibilities between xclim and xclim-testdata versions.""" - if ( - re.match(r"^\d+\.\d+\.\d+$", __xclim_version__) - and TESTDATA_BRANCH != default_testdata_version - ): + if re.match(r"^\d+\.\d+\.\d+$", __xclim_version__) and TESTDATA_BRANCH != default_testdata_version: # This does not need to be emitted on GitHub Workflows and ReadTheDocs if not os.getenv("CI") and not os.getenv("READTHEDOCS"): warnings.warn( - f"`xclim` stable ({__xclim_version__}) is running tests against a non-default branch of the testing data. " - "It is possible that changes to the testing data may be incompatible with some assertions in this version. " + f"`xclim` stable ({__xclim_version__}) is running tests against a non-default " + f"branch of the testing data. It is possible that changes to the testing data may " + f"be incompatible with some assertions in this version. " f"Please be sure to check {TESTDATA_REPO_URL} for more information.", ) @@ -422,9 +410,7 @@ def testing_setup_warnings(): time.ctime(os.path.getmtime(xclim.__file__)), "%a %b %d %H:%M:%S %Y", ) - install_calendar_version = ( - f"{install_date.year}.{install_date.month}.{install_date.day}" - ) + install_calendar_version = f"{install_date.year}.{install_date.month}.{install_date.day}" if Version(TESTDATA_BRANCH) > Version(install_calendar_version): warnings.warn( @@ -434,9 +420,7 @@ def testing_setup_warnings(): ) -def load_registry( - branch: str = TESTDATA_BRANCH, repo: str = TESTDATA_REPO_URL -) -> dict[str, str]: +def load_registry(branch: str = TESTDATA_BRANCH, repo: str = TESTDATA_REPO_URL) -> dict[str, str]: """ Load the registry file for the test data. @@ -465,18 +449,12 @@ def load_registry( external_repo_name = urlparse(repo).path.split("/")[-2] external_branch_name = branch.split("/")[-1] registry_file = Path( - str( - ilr.files("xclim").joinpath( - f"testing/registry.{external_repo_name}.{external_branch_name}.txt" - ) - ) + str(ilr.files("xclim").joinpath(f"testing/registry.{external_repo_name}.{external_branch_name}.txt")) ) urlretrieve(remote_registry, registry_file) # noqa: S310 elif branch != default_testdata_version: - custom_registry_folder = Path( - str(ilr.files("xclim").joinpath(f"testing/{branch}")) - ) + custom_registry_folder = Path(str(ilr.files("xclim").joinpath(f"testing/{branch}"))) custom_registry_folder.mkdir(parents=True, exist_ok=True) registry_file = custom_registry_folder.joinpath("registry.txt") urlretrieve(remote_registry, registry_file) # noqa: S310 @@ -521,13 +499,14 @@ def nimbus( # noqa: PR01 Notes ----- There are three environment variables that can be used to control the behaviour of this registry: - - ``XCLIM_TESTDATA_CACHE_DIR``: If this environment variable is set, it will be used as the base directory to - store the data files. The directory should be an absolute path (i.e., it should start with ``/``). + - ``XCLIM_TESTDATA_CACHE_DIR``: If this environment variable is set, it will be used as the + base directory to store the data files. + The directory should be an absolute path (i.e., it should start with ``/``). Otherwise,the default location will be used (based on ``platformdirs``, see :py:func:`pooch.os_cache`). - - ``XCLIM_TESTDATA_REPO_URL``: If this environment variable is set, it will be used as the URL of the repository - to use when fetching datasets. Otherwise, the default repository will be used. - - ``XCLIM_TESTDATA_BRANCH``: If this environment variable is set, it will be used as the branch of the repository - to use when fetching datasets. Otherwise, the default branch will be used. + - ``XCLIM_TESTDATA_REPO_URL``: If this environment variable is set, it will be used as the URL of + the repository to use when fetching datasets. Otherwise, the default repository will be used. + - ``XCLIM_TESTDATA_BRANCH``: If this environment variable is set, it will be used as the branch of + the repository to use when fetching datasets. Otherwise, the default branch will be used. Examples -------- @@ -548,9 +527,7 @@ def nimbus( # noqa: PR01 ) if not repo.endswith("/"): repo = f"{repo}/" - remote = audit_url( - urljoin(urljoin(repo, branch if branch.endswith("/") else f"{branch}/"), "data") - ) + remote = audit_url(urljoin(urljoin(repo, branch if branch.endswith("/") else f"{branch}/"), "data")) _nimbus = pooch.create( path=cache_dir, @@ -569,7 +546,6 @@ def nimbus( # noqa: PR01 # Overload the fetch method to add user-agent headers @wraps(_nimbus.fetch_diversion) def _fetch(*args: str, **kwargs: bool | Callable) -> str: # numpydoc ignore=GL08 - def _downloader( url: str, output_file: str | IO, @@ -654,9 +630,7 @@ def open_dataset( local_file = Path(cache_dir).joinpath(name) if not local_file.exists(): try: - local_file = nimbus(branch=branch, repo=repo, cache_dir=cache_dir).fetch( - name - ) + local_file = nimbus(branch=branch, repo=repo, cache_dir=cache_dir).fetch(name) except OSError as err: msg = f"File not found locally. Verify that the testing data is available in remote: {local_file}" raise OSError(msg) from err diff --git a/tests/conftest.py b/tests/conftest.py index 59de0249a..3b357ce5a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -112,9 +112,7 @@ def prsnd_series(): @pytest.fixture def pr_hr_series(): """Return precipitation hourly time series.""" - _pr_hr_series = partial( - test_timeseries, start="1/1/2000", variable="pr", units="kg m-2 s-1", freq="h" - ) + _pr_hr_series = partial(test_timeseries, start="1/1/2000", variable="pr", units="kg m-2 s-1", freq="h") return _pr_hr_series @@ -168,9 +166,7 @@ def ndq_series(random): cy = xr.IndexVariable("y", y) dates = pd.date_range("1900-01-01", periods=nt, freq="D") - time_range = xr.IndexVariable( - "time", dates, attrs={"units": "days since 1900-01-01", "calendar": "standard"} - ) + time_range = xr.IndexVariable("time", dates, attrs={"units": "days since 1900-01-01", "calendar": "standard"}) return xr.DataArray( random.lognormal(10, 1, (nt, nx, ny)), @@ -195,13 +191,9 @@ def per_doy(): def _per_doy(values, calendar="standard", units="kg m-2 s-1"): n = max_doy[calendar] if len(values) != n: - raise ValueError( - "Values must be same length as number of days in calendar." - ) + raise ValueError("Values must be same length as number of days in calendar.") coords = xr.IndexVariable("dayofyear", np.arange(1, n + 1)) - return xr.DataArray( - values, coords=[coords], attrs={"calendar": calendar, "units": units} - ) + return xr.DataArray(values, coords=[coords], attrs={"calendar": calendar, "units": units}) return _per_doy @@ -216,13 +208,7 @@ def areacella() -> xr.DataArray: d_lat = np.diff(lat_bnds) lon = np.convolve(lon_bnds, [0.5, 0.5], "valid") lat = np.convolve(lat_bnds, [0.5, 0.5], "valid") - area = ( - r - * np.radians(d_lat)[:, np.newaxis] - * r - * np.cos(np.radians(lat)[:, np.newaxis]) - * np.radians(d_lon) - ) + area = r * np.radians(d_lat)[:, np.newaxis] * r * np.cos(np.radians(lat)[:, np.newaxis]) * np.radians(d_lon) return xr.DataArray( data=area, dims=("lat", "lon"), @@ -321,9 +307,7 @@ def nimbus(threadsafe_data_dir, worker_id): return _nimbus( repo=TESTDATA_REPO_URL, branch=TESTDATA_BRANCH, - cache_dir=( - TESTDATA_CACHE_DIR if worker_id == "master" else threadsafe_data_dir - ), + cache_dir=(TESTDATA_CACHE_DIR if worker_id == "master" else threadsafe_data_dir), ) @@ -366,14 +350,10 @@ def lafferty_sriver_ds(nimbus) -> xr.Dataset: "uncertainty_partitioning/seattle_avg_tas.csv", ) - df = pd.read_csv(fn, parse_dates=["time"]).rename( - columns={"ssp": "scenario", "ensemble": "downscaling"} - ) + df = pd.read_csv(fn, parse_dates=["time"]).rename(columns={"ssp": "scenario", "ensemble": "downscaling"}) # Make xarray dataset - return xr.Dataset.from_dataframe( - df.set_index(["scenario", "model", "downscaling", "time"]) - ) + return xr.Dataset.from_dataframe(df.set_index(["scenario", "model", "downscaling", "time"])) @pytest.fixture @@ -417,9 +397,7 @@ def remove_data_written_flag(): try: flag.unlink() except FileNotFoundError: - logging.info( - "Teardown race condition occurred: .data_written flag already removed. Lucky!" - ) + logging.info("Teardown race condition occurred: .data_written flag already removed. Lucky!") pass request.addfinalizer(remove_data_written_flag) diff --git a/tests/test_analog.py b/tests/test_analog.py index 37d124612..56eb7e3c8 100644 --- a/tests/test_analog.py +++ b/tests/test_analog.py @@ -97,16 +97,12 @@ def test_spatial_analogs_multi_index(open_dataset): candidates_stacked = candidates.stack(sample=["time"]) method = "seuclidean" - out = xca.spatial_analogs( - target_stacked, candidates_stacked, dist_dim="sample", method=method - ) + out = xca.spatial_analogs(target_stacked, candidates_stacked, dist_dim="sample", method=method) np.testing.assert_allclose(diss[method], out, rtol=1e-3, atol=1e-3) # Check that it works as well when time dimensions don't have the same length. candidates = data.sel(time=slice("1970", "1991")) - xca.spatial_analogs( - target_stacked, candidates_stacked, dist_dim="sample", method=method - ) + xca.spatial_analogs(target_stacked, candidates_stacked, dist_dim="sample", method=method) class TestSEuclidean: @@ -258,11 +254,7 @@ def test_accuracy(self, random): out = [] n = 500 for _i in range(500): - out.append( - xca.kldiv( - p.rvs(n, random_state=random), q.rvs(n, random_state=random), k=k - ) - ) + out.append(xca.kldiv(p.rvs(n, random_state=random), q.rvs(n, random_state=random), k=k)) out = np.array(out) # Compare with analytical value @@ -280,23 +272,14 @@ def test_different_sample_size(self, random): n = 6000 # Same sample size for x and y - re = [ - xca.kldiv(p.rvs(n, random_state=random), q.rvs(n, random_state=random)) - for i in range(30) - ] + re = [xca.kldiv(p.rvs(n, random_state=random), q.rvs(n, random_state=random)) for i in range(30)] assert_almost_equal(np.mean(re), ra, 2) # Different sample sizes - re = [ - xca.kldiv(p.rvs(n * 2, random_state=random), q.rvs(n, random_state=random)) - for i in range(30) - ] + re = [xca.kldiv(p.rvs(n * 2, random_state=random), q.rvs(n, random_state=random)) for i in range(30)] assert_almost_equal(np.mean(re), ra, 2) - re = [ - xca.kldiv(p.rvs(n, random_state=random), q.rvs(n * 2, random_state=random)) - for i in range(30) - ] + re = [xca.kldiv(p.rvs(n, random_state=random), q.rvs(n * 2, random_state=random)) for i in range(30)] assert_almost_equal(np.mean(re), ra, 2) # @@ -317,14 +300,10 @@ def test_szekely_rizzo(): x = iris.iloc[:80, :].to_xarray().to_array().T y = iris.iloc[80:, :].to_xarray().to_array().T - np.testing.assert_allclose( - xca.szekely_rizzo(x, y, standardize=False), 116.1987, atol=5e-5 - ) + np.testing.assert_allclose(xca.szekely_rizzo(x, y, standardize=False), 116.1987, atol=5e-5) # first 50 against last 100 x = iris.iloc[:50, :].to_xarray().to_array().T y = iris.iloc[50:, :].to_xarray().to_array().T - np.testing.assert_allclose( - xca.szekely_rizzo(x, y, standardize=False), 199.6205, atol=5e-5 - ) + np.testing.assert_allclose(xca.szekely_rizzo(x, y, standardize=False), 199.6205, atol=5e-5) diff --git a/tests/test_atmos.py b/tests/test_atmos.py index 6bcd289c0..e418efaf1 100644 --- a/tests/test_atmos.py +++ b/tests/test_atmos.py @@ -32,9 +32,7 @@ def test_wind_speed_from_vectors(): uas[:] = 0 vas[0] = 0.9 vas[1] = -1.1 - wind, winddir = atmos.wind_speed_from_vector( - uas=uas, vas=vas, calm_wind_thresh="1 m/s" - ) + wind, winddir = atmos.wind_speed_from_vector(uas=uas, vas=vas, calm_wind_thresh="1 m/s") np.testing.assert_array_equal(wind, [0.9, 1.1]) np.testing.assert_allclose(winddir, [0.0, 360.0]) @@ -45,18 +43,14 @@ def test_wind_vector_from_speed(): sfcWindfromdir = xr.DataArray(np.array([360.0, 36.86989764584402, 0.0]), dims=["x"]) sfcWindfromdir.attrs["units"] = "degree" - uas, vas = atmos.wind_vector_from_speed( - sfcWind=sfcWind, sfcWindfromdir=sfcWindfromdir - ) + uas, vas = atmos.wind_vector_from_speed(sfcWind=sfcWind, sfcWindfromdir=sfcWindfromdir) np.testing.assert_allclose(uas, [0.0, -3.0, 0.0], atol=1e-14) np.testing.assert_allclose(vas, [-3.0, -4.0, -0.2], atol=1e-14) # missing values sfcWind[0] = np.nan sfcWindfromdir[1] = np.nan - uas, vas = atmos.wind_vector_from_speed( - sfcWind=sfcWind, sfcWindfromdir=sfcWindfromdir - ) + uas, vas = atmos.wind_vector_from_speed(sfcWind=sfcWind, sfcWindfromdir=sfcWindfromdir) np.testing.assert_array_equal(uas.isnull(), [True, True, False]) np.testing.assert_array_equal(vas.isnull(), [True, True, False]) @@ -64,12 +58,8 @@ def test_wind_vector_from_speed(): def test_relative_humidity_dewpoint(timeseries): np.testing.assert_allclose( atmos.relative_humidity_from_dewpoint( - tas=timeseries( - np.array([-20, -10, -1, 10, 20, 25, 30, 40, 60]) + K2C, "tas" - ), - tdps=timeseries( - np.array([-15, -10, -2, 5, 10, 20, 29, 20, 30]) + K2C, "tdps" - ), + tas=timeseries(np.array([-20, -10, -1, 10, 20, 25, 30, 40, 60]) + K2C, "tas"), + tdps=timeseries(np.array([-15, -10, -2, 5, 10, 20, 29, 20, 30]) + K2C, "tdps"), ), timeseries([np.nan, 100, 93, 71, 52, 73, 94, 31, 20], "hurs"), rtol=0.02, @@ -164,9 +154,7 @@ def test_specific_humidity(tas_series, hurs_series, huss_series, ps_series): tas = tas_series(np.array([20, -10, 10, 20, 35, 50, 75, 95]) + K2C) hurs = hurs_series([150, 10, 90, 20, 80, 50, 70, 40, 30]) ps = ps_series(1000 * np.array([100] * 4 + [101] * 4)) - huss_exp = huss_series( - [np.nan, 1.6e-4, 6.9e-3, 3.0e-3, 2.9e-2, 4.1e-2, 2.1e-1, 5.7e-1] - ) + huss_exp = huss_series([np.nan, 1.6e-4, 6.9e-3, 3.0e-3, 2.9e-2, 4.1e-2, 2.1e-1, 5.7e-1]) huss = atmos.specific_humidity( tas=tas, @@ -202,13 +190,9 @@ def test_snowfall_approximation(pr_series, tasmax_series): pr = pr_series(np.ones(10)) tasmax = tasmax_series(np.arange(10) + K2C) - prsn = atmos.snowfall_approximation( - pr, tas=tasmax, thresh="5 degC", method="binary" - ) + prsn = atmos.snowfall_approximation(pr, tas=tasmax, thresh="5 degC", method="binary") - np.testing.assert_allclose( - prsn, [1, 1, 1, 1, 1, 0, 0, 0, 0, 0], atol=1e-5, rtol=1e-3 - ) + np.testing.assert_allclose(prsn, [1, 1, 1, 1, 1, 0, 0, 0, 0, 0], atol=1e-5, rtol=1e-3) def test_rain_approximation(pr_series, tas_series): @@ -217,9 +201,7 @@ def test_rain_approximation(pr_series, tas_series): prlp = atmos.rain_approximation(pr, tas=tas, thresh="5 degC", method="binary") - np.testing.assert_allclose( - prlp, [0, 0, 0, 0, 0, 1, 1, 1, 1, 1], atol=1e-5, rtol=1e-3 - ) + np.testing.assert_allclose(prlp, [0, 0, 0, 0, 0, 1, 1, 1, 1, 1], atol=1e-5, rtol=1e-3) def test_high_precip_low_temp(pr_series, tasmin_series): @@ -232,30 +214,22 @@ def test_high_precip_low_temp(pr_series, tasmin_series): tas += K2C tas = tasmin_series(tas, start="1999-01-01") - out = atmos.high_precip_low_temp( - pr, tas, pr_thresh="1 kg m-2 s-1", tas_thresh="1 C" - ) + out = atmos.high_precip_low_temp(pr, tas, pr_thresh="1 kg m-2 s-1", tas_thresh="1 C") np.testing.assert_array_equal(out, [1]) def test_wind_chill_index(atmosds): out = atmos.wind_chill_index(ds=atmosds) - np.testing.assert_allclose( - out.isel(time=0), [np.nan, -6.716, -35.617, -8.486, np.nan], rtol=1e-3 - ) + np.testing.assert_allclose(out.isel(time=0), [np.nan, -6.716, -35.617, -8.486, np.nan], rtol=1e-3) out_us = atmos.wind_chill_index(ds=atmosds, method="US") - np.testing.assert_allclose( - out_us.isel(time=0), [-1.429, -6.716, -35.617, -8.486, 2.781], rtol=1e-3 - ) + np.testing.assert_allclose(out_us.isel(time=0), [-1.429, -6.716, -35.617, -8.486, 2.781], rtol=1e-3) def test_wind_profile(atmosds): - out = atmos.wind_profile( - wind_speed=atmosds.sfcWind, h_r="10 m", h="100 m", alpha=1 / 7 - ) + out = atmos.wind_profile(wind_speed=atmosds.sfcWind, h_r="10 m", h="100 m", alpha=1 / 7) assert out.attrs["units"] == "m s-1" assert (out > atmosds.sfcWind).all() @@ -273,17 +247,13 @@ def test_wind_power_potential_from_3h_series(): from xclim.indices.generic import select_resample_op from xclim.testing.helpers import test_timeseries - w = test_timeseries( - np.ones(96) * 15, variable="sfcWind", start="7/1/2000", units="m s-1", freq="3h" - ) + w = test_timeseries(np.ones(96) * 15, variable="sfcWind", start="7/1/2000", units="m s-1", freq="3h") out = atmos.wind_power_potential(wind_speed=w) # Multiply with nominal capacity power = out * 100 power.attrs["units"] = "MW" - annual_power = convert_units_to( - select_resample_op(power, op="integral", freq="D"), "MWh" - ) + annual_power = convert_units_to(select_resample_op(power, op="integral", freq="D"), "MWh") assert (annual_power == 100 * 24).all() @@ -295,9 +265,7 @@ def test_simple(self, atmosds): evspsblpot = ds.evspsblpot di = atmos.dryness_index(pr, evspsblpot) - np.testing.assert_allclose( - di, np.array([13.355, 102.426, 65.576, 158.078]), rtol=1e-03 - ) + np.testing.assert_allclose(di, np.array([13.355, 102.426, 65.576, 158.078]), rtol=1e-03) assert di.attrs["long_name"] == "Growing season humidity" def test_variable_initial_conditions(self, atmosds): @@ -432,12 +400,8 @@ def test_nan_values(self, atmosds): pet_tw48 = atmos.potential_evapotranspiration(tas=tm, method="TW48") - np.testing.assert_allclose( - pet_br65.isel(location=0, time=slice(100, 102)), [np.nan, np.nan] - ) - np.testing.assert_allclose( - pet_hg85.isel(location=0, time=slice(100, 102)), [np.nan, np.nan] - ) + np.testing.assert_allclose(pet_br65.isel(location=0, time=slice(100, 102)), [np.nan, np.nan]) + np.testing.assert_allclose(pet_hg85.isel(location=0, time=slice(100, 102)), [np.nan, np.nan]) np.testing.assert_allclose( pet_fao_pm98.isel(location=0, time=slice(100, 102)), [np.nan, np.nan], @@ -471,24 +435,12 @@ def test_convert_units(self, atmosds): petR = pet * 86400 petR.attrs["units"] = "mm/day" - p_pet_br65 = atmos.water_budget_from_tas( - pr, tasmin=tn, tasmax=tx, method="BR65" - ) - p_pet_br65C = atmos.water_budget_from_tas( - prR, tasmin=tnC, tasmax=tx, method="BR65" - ) - p_pet_hg85 = atmos.water_budget_from_tas( - pr, tasmin=tn, tasmax=tx, method="HG85" - ) - p_pet_hg85C = atmos.water_budget_from_tas( - prR, tasmin=tnC, tasmax=tx, method="HG85" - ) - p_pet_tw48 = atmos.water_budget_from_tas( - pr, tasmin=tn, tasmax=tx, method="TW48" - ) - p_pet_tw48C = atmos.water_budget_from_tas( - prR, tasmin=tnC, tasmax=tx, method="TW48" - ) + p_pet_br65 = atmos.water_budget_from_tas(pr, tasmin=tn, tasmax=tx, method="BR65") + p_pet_br65C = atmos.water_budget_from_tas(prR, tasmin=tnC, tasmax=tx, method="BR65") + p_pet_hg85 = atmos.water_budget_from_tas(pr, tasmin=tn, tasmax=tx, method="HG85") + p_pet_hg85C = atmos.water_budget_from_tas(prR, tasmin=tnC, tasmax=tx, method="HG85") + p_pet_tw48 = atmos.water_budget_from_tas(pr, tasmin=tn, tasmax=tx, method="TW48") + p_pet_tw48C = atmos.water_budget_from_tas(prR, tasmin=tnC, tasmax=tx, method="TW48") p_pet_fao_pm98 = atmos.water_budget_from_tas( pr=pr, @@ -545,12 +497,8 @@ def test_nan_values(self, atmosds): tn[0, 100] = np.nan tx[0, 101] = np.nan - p_pet_br65 = atmos.water_budget_from_tas( - pr, tasmin=tn, tasmax=tx, method="BR65" - ) - p_pet_hg85 = atmos.water_budget_from_tas( - pr, tasmin=tn, tasmax=tx, method="HG85" - ) + p_pet_br65 = atmos.water_budget_from_tas(pr, tasmin=tn, tasmax=tx, method="BR65") + p_pet_hg85 = atmos.water_budget_from_tas(pr, tasmin=tn, tasmax=tx, method="HG85") p_pet_fao_pm98 = atmos.water_budget_from_tas( pr=pr, tasmin=tn, @@ -585,9 +533,7 @@ def test_universal_thermal_climate_index(self, atmosds): tas = dataset.tas hurs = dataset.hurs - sfcWind, _sfcWindfromdir = atmos.wind_speed_from_vector( - uas=dataset.uas, vas=dataset.vas - ) + sfcWind, _sfcWindfromdir = atmos.wind_speed_from_vector(uas=dataset.uas, vas=dataset.vas) rsds = dataset.rsds rsus = dataset.rsus rlds = dataset.rlds diff --git a/tests/test_bootstrapping.py b/tests/test_bootstrapping.py index fb98eaf34..662b12fbd 100644 --- a/tests/test_bootstrapping.py +++ b/tests/test_bootstrapping.py @@ -42,12 +42,8 @@ class Test_bootstrap: ) def test_bootstrap(self, var, p, index, freq, calendar, use_dask, random): # -- GIVEN - arr = self.ar1( - alpha=0.8, n=int(4 * 365.25), random=random, positive_values=(var == "pr") - ) - climate_var = _test_timeseries( - arr, start="2000-01-01", variable=var, calendar=calendar - ) + arr = self.ar1(alpha=0.8, n=int(4 * 365.25), random=random, positive_values=(var == "pr")) + climate_var = _test_timeseries(arr, start="2000-01-01", variable=var, calendar=calendar) if use_dask: climate_var = climate_var.chunk(dict(time=50)) in_base_slice = slice("2000-01-01", "2001-12-31") @@ -70,9 +66,9 @@ def test_bootstrap(self, var, p, index, freq, calendar, use_dask, random): # the closer the target percentile is to the median the less bootstrapping is # necessary. # Following assertions may even fail if chosen percentile is close to 50. - assert np.count_nonzero( - bootstrapped_in_base > no_bs_in_base - ) > np.count_nonzero(bootstrapped_in_base < no_bs_in_base) + assert np.count_nonzero(bootstrapped_in_base > no_bs_in_base) > np.count_nonzero( + bootstrapped_in_base < no_bs_in_base + ) # bootstrapping should leave the out of base unchanged, # but precision above 15th decimal might differ. np.testing.assert_array_almost_equal(no_bs_out_base, bs_out_base, 15) @@ -81,16 +77,12 @@ def test_bootstrap(self, var, p, index, freq, calendar, use_dask, random): def test_bootstrap_fraction_over_precip_error_no_doy(self, pr_series): with pytest.raises(KeyError): # no "dayofyear" coords on per - fraction_over_precip_thresh( - pr_series([1, 2]), pr_series([1, 2]), bootstrap=True - ) + fraction_over_precip_thresh(pr_series([1, 2]), pr_series([1, 2]), bootstrap=True) def test_bootstrap_days_over_precip_thresh_error_no_doy(self, pr_series): with pytest.raises(KeyError): # no "dayofyear" coords on per - days_over_precip_thresh( - pr_series([1, 2]), pr_series([1, 2]), bootstrap=True - ) + days_over_precip_thresh(pr_series([1, 2]), pr_series([1, 2]), bootstrap=True) def test_bootstrap_no_doy(self, tas_series): # no "dayofyear" coords on per @@ -100,9 +92,7 @@ def test_bootstrap_no_doy(self, tas_series): def test_bootstrap_full_overlap(self, tas_series, random): # bootstrap is unnecessary when in base and out of base fully overlap # -- GIVEN - tas = tas_series( - self.ar1(alpha=0.8, n=int(4 * 365.25), random=random), start="2000-01-01" - ) + tas = tas_series(self.ar1(alpha=0.8, n=int(4 * 365.25), random=random), start="2000-01-01") per = percentile_doy(tas, per=90) # -- THEN with pytest.raises(KeyError): @@ -113,9 +103,7 @@ def test_bootstrap_full_overlap(self, tas_series, random): def test_bootstrap_no_overlap(self, tas_series, random): # bootstrap is unnecessary when in base and out of base fully overlap # -- GIVEN - tas = tas_series( - self.ar1(alpha=0.8, n=int(4 * 365.25), random=random), start="2000-01-01" - ) + tas = tas_series(self.ar1(alpha=0.8, n=int(4 * 365.25), random=random), start="2000-01-01") tas_in_base = tas.sel(time=slice("2000-01-01", "2001-12-31")) tas_out_base = tas.sel(time=slice("2002-01-01", "2001-12-31")) per = percentile_doy(tas_in_base, per=90) @@ -127,9 +115,7 @@ def test_bootstrap_no_overlap(self, tas_series, random): @pytest.mark.slow def test_multi_per(self, open_dataset): tas = open_dataset("ERA5/daily_surface_cancities_1990-1993.nc").tas - t90 = percentile_doy( - tas.sel(time=slice("1990-01-01", "1991-12-31")), window=5, per=[90, 91] - ) + t90 = percentile_doy(tas.sel(time=slice("1990-01-01", "1991-12-31")), window=5, per=[90, 91]) res = tg90p(tas=tas, tas_per=t90, freq="YS", bootstrap=True) np.testing.assert_array_equal([90, 91], res.percentiles) @@ -137,9 +123,7 @@ def test_multi_per(self, open_dataset): def test_doctest_ndims(self, open_dataset): """Replicates doctest to facilitate debugging.""" tas = open_dataset("ERA5/daily_surface_cancities_1990-1993.nc").tas - t90 = percentile_doy( - tas.sel(time=slice("1990-01-01", "1991-12-31")), window=5, per=90 - ) + t90 = percentile_doy(tas.sel(time=slice("1990-01-01", "1991-12-31")), window=5, per=90) tg90p(tas=tas, tas_per=t90.isel(percentiles=0), freq="YS", bootstrap=True) tg90p(tas=tas, tas_per=t90, freq="YS", bootstrap=True) diff --git a/tests/test_calendar.py b/tests/test_calendar.py index 7d40e75c9..54f53c7f4 100644 --- a/tests/test_calendar.py +++ b/tests/test_calendar.py @@ -30,9 +30,7 @@ ) -@pytest.fixture( - params=[dict(start="2004-01-01T12:07:01", periods=27, freq="3MS")], ids=["3MS"] -) +@pytest.fixture(params=[dict(start="2004-01-01T12:07:01", periods=27, freq="3MS")], ids=["3MS"]) def time_range_kwargs(request): return request.param @@ -48,9 +46,7 @@ def cftime_index(time_range_kwargs): def da(index): - return xr.DataArray( - np.arange(100.0, 100.0 + index.size), coords=[index], dims=["time"] - ) + return xr.DataArray(np.arange(100.0, 100.0 + index.size), coords=[index], dims=["time"]) @pytest.mark.parametrize("freq", ["6480h", "302431min", "23144781s"]) @@ -79,9 +75,7 @@ def test_time_bnds_irregular(typ): if typ == "xr": start = xr.cftime_range("1990-01-01", periods=24, freq="MS") # Well. xarray string parsers do not support sub-second resolution, but cftime does. - end = xr.cftime_range( - "1990-01-01T23:59:59", periods=24, freq="ME" - ) + pd.Timedelta(0.999999, "s") + end = xr.cftime_range("1990-01-01T23:59:59", periods=24, freq="ME") + pd.Timedelta(0.999999, "s") elif typ == "pd": start = pd.date_range("1990-01-01", periods=24, freq="MS") end = pd.date_range("1990-01-01 23:59:59.999999999", periods=24, freq="ME") @@ -265,22 +259,14 @@ def test_convert_calendar_and_doy(): doy = xr.DataArray( [31, 32, 336, 364.23, 365], dims=("time",), - coords={ - "time": xr.date_range("2000-01-01", periods=5, freq="YS", calendar="noleap") - }, + coords={"time": xr.date_range("2000-01-01", periods=5, freq="YS", calendar="noleap")}, attrs={"is_dayofyear": 1, "calendar": "noleap"}, ) - out = convert_doy(doy, target_cal="360_day").convert_calendar( - "360_day", align_on="date" - ) + out = convert_doy(doy, target_cal="360_day").convert_calendar("360_day", align_on="date") # out = convert_calendar(doy, "360_day", align_on="date", doy=True) - np.testing.assert_allclose( - out, [30.575342, 31.561644, 331.39726, 359.240548, 360.0] - ) + np.testing.assert_allclose(out, [30.575342, 31.561644, 331.39726, 359.240548, 360.0]) assert out.time.dt.calendar == "360_day" - out = convert_doy(doy, target_cal="360_day", align_on="date").convert_calendar( - "360_day", align_on="date" - ) + out = convert_doy(doy, target_cal="360_day", align_on="date").convert_calendar("360_day", align_on="date") np.testing.assert_array_equal(out, [np.nan, 31, 332, 360.23, np.nan]) assert out.time.dt.calendar == "360_day" @@ -449,9 +435,7 @@ def test_convert_doy(): doy = xr.DataArray( [31, 32, 336, 364.23, 365], dims=("time",), - coords={ - "time": xr.date_range("2000-01-01", periods=5, freq="YS", calendar="noleap") - }, + coords={"time": xr.date_range("2000-01-01", periods=5, freq="YS", calendar="noleap")}, attrs={"is_dayofyear": 1, "calendar": "noleap"}, ) @@ -459,27 +443,19 @@ def test_convert_doy(): np.testing.assert_array_equal(out, [np.nan, 31, 332, 360.23, np.nan]) assert out.time.dt.calendar == "noleap" out = convert_doy(doy, "360_day", align_on="year") - np.testing.assert_allclose( - out, [30.575342, 31.561644, 331.39726, 359.240548, 360.0] - ) + np.testing.assert_allclose(out, [30.575342, 31.561644, 331.39726, 359.240548, 360.0]) doy = xr.DataArray( [31, 200.48, 190, 60, 300.54], dims=("time",), - coords={ - "time": xr.date_range( - "2000-01-01", periods=5, freq="YS-JUL", calendar="standard" - ) - }, + coords={"time": xr.date_range("2000-01-01", periods=5, freq="YS-JUL", calendar="standard")}, attrs={"is_dayofyear": 1, "calendar": "standard"}, ).expand_dims(lat=[10, 20, 30]) out = convert_doy(doy, "noleap", align_on="date") np.testing.assert_array_equal(out.isel(lat=0), [31, 200.48, 190, np.nan, 299.54]) out = convert_doy(doy, "noleap", align_on="year") - np.testing.assert_allclose( - out.isel(lat=0), [31.0, 200.48, 190.0, 59.83607, 299.71885] - ) + np.testing.assert_allclose(out.isel(lat=0), [31.0, 200.48, 190.0, 59.83607, 299.71885]) @pytest.mark.parametrize("calendar", ["standard", None]) @@ -490,9 +466,7 @@ def test_convert_doy(): def test_stack_periods(tas_series, calendar, w, s, m, f, ss): da = tas_series(np.arange(365 * 50), start="2000-01-01", calendar=calendar) - da_stck = stack_periods( - da, window=w, stride=s, min_length=m, freq=f, align_days=False - ) + da_stck = stack_periods(da, window=w, stride=s, min_length=m, freq=f, align_days=False) assert "period_length" in da_stck.coords diff --git a/tests/test_cffwis.py b/tests/test_cffwis.py index b677d2d4d..a9e2d508d 100644 --- a/tests/test_cffwis.py +++ b/tests/test_cffwis.py @@ -47,9 +47,7 @@ class TestCFFWIS: @classmethod def _get_cffdrs_fire_season(cls, key=None): def to_xr(arr): - return xr.DataArray( - np.array(arr, dtype=np.datetime64), dims=("bounds", "events") - ) + return xr.DataArray(np.array(arr, dtype=np.datetime64), dims=("bounds", "events")) if key: return to_xr(cls.cffdrs_fire_season[key]) @@ -236,12 +234,7 @@ def test_fire_weather_ufunc_overwintering(self, atmosds): # Overwintering # Get last season's DC (from previous comp) and mask Saskatoon and Victoria - dc0 = ( - out2["DC"] - .ffill("time") - .isel(time=-1) - .where([True, True, True, False, False]) - ) + dc0 = out2["DC"].ffill("time").isel(time=-1).where([True, True, True, False, False]) winter_pr = out2["winter_pr"] out3 = fire_weather_ufunc( @@ -254,9 +247,7 @@ def test_fire_weather_ufunc_overwintering(self, atmosds): overwintering=True, indexes=["DC"], ) - np.testing.assert_allclose( - out3["winter_pr"].isel(location=0), 262.67575, rtol=1e-6 - ) + np.testing.assert_allclose(out3["winter_pr"].isel(location=0), 262.67575, rtol=1e-6) np.testing.assert_array_equal(out3["DC"].notnull(), season_mask_yr) def test_fire_weather_ufunc_drystart(self, atmosds): @@ -299,9 +290,7 @@ def test_fire_weather_ufunc_drystart(self, atmosds): out_no["DMC"].sel(location="Montréal", time="1992"), ) - def test_fire_weather_ufunc_errors( - self, tas_series, pr_series, hurs_series, sfcWind_series - ): + def test_fire_weather_ufunc_errors(self, tas_series, pr_series, hurs_series, sfcWind_series): tas = tas_series(np.ones(100), start="2017-01-01") pr = pr_series(np.ones(100), start="2017-01-01") hurs = hurs_series(np.ones(100), start="2017-01-01") @@ -418,12 +407,8 @@ def test_gfwed_and_indicators(self, open_dataset): temp_end_thresh="6 degC", ) - for exp, out in zip( - [ds.DC, ds.DMC, ds.FFMC, ds.ISI, ds.BUI, ds.FWI], outs, strict=False - ): - np.testing.assert_allclose( - out.isel(loc=[0, 1]), exp.isel(loc=[0, 1]), rtol=0.03 - ) + for exp, out in zip([ds.DC, ds.DMC, ds.FFMC, ds.ISI, ds.BUI, ds.FWI], outs, strict=False): + np.testing.assert_allclose(out.isel(loc=[0, 1]), exp.isel(loc=[0, 1]), rtol=0.03) ds2 = ds.isel(time=slice(1, None)) @@ -456,9 +441,7 @@ def test_gfwed_and_indicators(self, open_dataset): initial_start_up=False, ) - for exp, out in zip( - [ds2.DC, ds2.DMC, ds2.FFMC, ds2.ISI, ds2.BUI, ds2.FWI], outs, strict=False - ): + for exp, out in zip([ds2.DC, ds2.DMC, ds2.FFMC, ds2.ISI, ds2.BUI, ds2.FWI], outs, strict=False): np.testing.assert_allclose(out, exp, rtol=0.03) def test_gfwed_drought_code(self, open_dataset): @@ -479,9 +462,7 @@ def test_gfwed_drought_code(self, open_dataset): temp_end_thresh="6 degC", ) - np.testing.assert_allclose( - out.isel(loc=[0, 1]), ds.DC.isel(loc=[0, 1]), rtol=0.03 - ) + np.testing.assert_allclose(out.isel(loc=[0, 1]), ds.DC.isel(loc=[0, 1]), rtol=0.03) def test_gfwed_duff_moisture_code(self, open_dataset): # Also tests passing parameters as quantity strings @@ -501,6 +482,4 @@ def test_gfwed_duff_moisture_code(self, open_dataset): temp_end_thresh="6 degC", ) - np.testing.assert_allclose( - out.isel(loc=[0, 1]), ds.DMC.isel(loc=[0, 1]), rtol=0.03 - ) + np.testing.assert_allclose(out.isel(loc=[0, 1]), ds.DMC.isel(loc=[0, 1]), rtol=0.03) diff --git a/tests/test_checks.py b/tests/test_checks.py index 9677e7601..bafa0b9f1 100644 --- a/tests/test_checks.py +++ b/tests/test_checks.py @@ -116,9 +116,7 @@ def test_decreasing_index(self, date_range): with pytest.raises(ValidationError): n = 365 times = date_range("2000-01-01", freq="12h", periods=n) - da = xr.DataArray( - np.arange(n), [("time", times[::-1])], attrs=self.tas_attrs - ) + da = xr.DataArray(np.arange(n), [("time", times[::-1])], attrs=self.tas_attrs) tg_mean(da) # Missing one day between the two years @@ -180,9 +178,7 @@ def test_common_time(self, tas_series, date_range, random): # No freq db = da[np.array([0, 1, 4, 6, 10])] - with pytest.raises( - ValidationError, match="Unable to infer the frequency of the time series." - ): + with pytest.raises(ValidationError, match="Unable to infer the frequency of the time series."): datachecks.check_common_time([db, da]) # Not same freq diff --git a/tests/test_cli.py b/tests/test_cli.py index c88625354..e748ecdc9 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -72,9 +72,7 @@ def test_indicator_help(indicator, indname): ("solidprcptot", 31622400.0, ["tas", "pr"]), ], ) -def test_normal_computation( - tasmin_series, tasmax_series, pr_series, tmp_path, indicator, expected, varnames -): +def test_normal_computation(tasmin_series, tasmax_series, pr_series, tmp_path, indicator, expected, varnames): tasmin = tasmin_series(np.ones(366) + 270.15, start="1/1/2000") tasmax = tasmax_series(np.ones(366) + 272.15, start="1/1/2000") pr = pr_series(np.ones(366), start="1/1/2000") @@ -219,9 +217,7 @@ def test_missing_variable(tas_series, tmp_path): tas.to_netcdf(input_file, engine="h5netcdf") runner = CliRunner() - results = runner.invoke( - cli, ["-i", str(input_file), "-o", str(output_file), "tn_mean"] - ) + results = runner.invoke(cli, ["-i", str(input_file), "-o", str(output_file), "tn_mean"]) assert results.exit_code == 2 assert "'tasmin' was not found in the input dataset." in results.output @@ -267,9 +263,7 @@ def test_suspicious_precipitation_flags(pr_series, tmp_path): bad_pr.to_netcdf(input_file) runner = CliRunner() - runner.invoke( - cli, ["-i", str(input_file), "-o", str(output_file), "dataflags", "pr"] - ) + runner.invoke(cli, ["-i", str(input_file), "-o", str(output_file), "dataflags", "pr"]) with xr.open_dataset(output_file) as ds: for var in ds.data_vars: assert var @@ -278,9 +272,7 @@ def test_suspicious_precipitation_flags(pr_series, tmp_path): @pytest.mark.slow def test_dataflags_output(tmp_path, tas_series, tasmax_series, tasmin_series): ds = xr.Dataset() - for series, val in zip( - [tas_series, tasmax_series, tasmin_series], [0, 10, -10], strict=False - ): + for series, val in zip([tas_series, tasmax_series, tasmin_series], [0, 10, -10], strict=False): vals = val + K2C + np.sin(np.pi * np.arange(366 * 3) / 366) arr = series(vals, start="1971-01-01") ds = xr.merge([ds, arr]) diff --git a/tests/test_ensembles.py b/tests/test_ensembles.py index abd8e25f3..83d3a2610 100644 --- a/tests/test_ensembles.py +++ b/tests/test_ensembles.py @@ -48,14 +48,9 @@ def test_create_ensemble(self, open_dataset, ensemble_dataset_objects, nimbus): assert len(ens.realization) == len(ensemble_dataset_objects["nc_files_simple"]) assert len(ens.time) == 151 for i in np.arange(0, len(ens.realization)): - np.testing.assert_array_equal( - ens.isel(realization=i).tg_mean.values, ds_all[i].tg_mean.values - ) + np.testing.assert_array_equal(ens.isel(realization=i).tg_mean.values, ds_all[i].tg_mean.values) - reals = [ - "_".join(Path(f).name.split("_")[1:4:2]) - for f in ensemble_dataset_objects["nc_files_simple"] - ] + reals = ["_".join(Path(f).name.split("_")[1:4:2]) for f in ensemble_dataset_objects["nc_files_simple"]] ens1 = ensembles.create_ensemble(ds_all, realizations=reals) # Kinda a hack? Alternative is to open and rewrite in a temp folder. @@ -95,26 +90,14 @@ def test_create_unequal_times(self, ensemble_dataset_objects, open_dataset): assert ens.time.dt.year.max() == 2100 assert len(ens.time) == 151 - ii = [ - i - for i, s in enumerate(ensemble_dataset_objects["nc_files"]) - if "1970-2050" in s - ] + ii = [i for i, s in enumerate(ensemble_dataset_objects["nc_files"]) if "1970-2050" in s] # assert padded with nans - assert np.all( - np.isnan(ens.tg_mean.isel(realization=ii).sel(time=ens.time.dt.year < 1970)) - ) - assert np.all( - np.isnan(ens.tg_mean.isel(realization=ii).sel(time=ens.time.dt.year > 2050)) - ) + assert np.all(np.isnan(ens.tg_mean.isel(realization=ii).sel(time=ens.time.dt.year < 1970))) + assert np.all(np.isnan(ens.tg_mean.isel(realization=ii).sel(time=ens.time.dt.year > 2050))) ens_mean = ens.tg_mean.mean(dim=["realization", "lon", "lat"], skipna=False) - assert ( - ens_mean.where(~(np.isnan(ens_mean)), drop=True).time.dt.year.min() == 1970 - ) - assert ( - ens_mean.where(~(np.isnan(ens_mean)), drop=True).time.dt.year.max() == 2050 - ) + assert ens_mean.where(~(np.isnan(ens_mean)), drop=True).time.dt.year.min() == 1970 + assert ens_mean.where(~(np.isnan(ens_mean)), drop=True).time.dt.year.max() == 2050 @pytest.mark.parametrize( "timegen,calkw", @@ -124,12 +107,8 @@ def test_create_unaligned_times(self, timegen, calkw): t1 = timegen("2000-01-01", periods=24, freq="ME", **calkw) t2 = timegen("2000-01-01", periods=24, freq="MS", **calkw) - d1 = xr.DataArray( - np.arange(24), dims=("time",), coords={"time": t1}, name="tas" - ) - d2 = xr.DataArray( - np.arange(24), dims=("time",), coords={"time": t2}, name="tas" - ) + d1 = xr.DataArray(np.arange(24), dims=("time",), coords={"time": t1}, name="tas") + d2 = xr.DataArray(np.arange(24), dims=("time",), coords={"time": t2}, name="tas") if t1.dtype != "O": ens = ensembles.create_ensemble((d1, d2)) @@ -153,21 +132,15 @@ def test_calc_perc(self, transpose, ensemble_dataset_objects, open_dataset): out1 = ensembles.ensemble_percentiles(ens, split=True) np.testing.assert_array_almost_equal( - mquantiles( - ens["tg_mean"].isel(time=0, lon=5, lat=5), 0.1, alphap=1, betap=1 - ), + mquantiles(ens["tg_mean"].isel(time=0, lon=5, lat=5), 0.1, alphap=1, betap=1), out1["tg_mean_p10"].isel(time=0, lon=5, lat=5), ) np.testing.assert_array_almost_equal( - mquantiles( - ens["tg_mean"].isel(time=0, lon=5, lat=5), alphap=1, betap=1, prob=0.50 - ), + mquantiles(ens["tg_mean"].isel(time=0, lon=5, lat=5), alphap=1, betap=1, prob=0.50), out1["tg_mean_p50"].isel(time=0, lon=5, lat=5), ) np.testing.assert_array_almost_equal( - mquantiles( - ens["tg_mean"].isel(time=0, lon=5, lat=5), alphap=1, betap=1, prob=0.90 - ), + mquantiles(ens["tg_mean"].isel(time=0, lon=5, lat=5), alphap=1, betap=1, prob=0.90), out1["tg_mean_p90"].isel(time=0, lon=5, lat=5), ) @@ -179,9 +152,7 @@ def test_calc_perc(self, transpose, ensemble_dataset_objects, open_dataset): betap=0.5, prob=0.90, ), - ensembles.ensemble_percentiles( - ens.isel(time=0, lon=5, lat=5), values=[90], method="hazen" - ).tg_mean_p90, + ensembles.ensemble_percentiles(ens.isel(time=0, lon=5, lat=5), values=[90], method="hazen").tg_mean_p90, ) assert np.all(out1["tg_mean_p90"] > out1["tg_mean_p50"]) @@ -192,13 +163,9 @@ def test_calc_perc(self, transpose, ensemble_dataset_objects, open_dataset): assert "Computation of the percentiles on" in out1.attrs["history"] out3 = ensembles.ensemble_percentiles(ens, split=False) - xr.testing.assert_equal( - out1["tg_mean_p10"], out3.tg_mean.sel(percentiles=10, drop=True) - ) + xr.testing.assert_equal(out1["tg_mean_p10"], out3.tg_mean.sel(percentiles=10, drop=True)) - weights = xr.DataArray( - [1, 0.1, 3.5, 5], coords={"realization": ens.realization} - ) + weights = xr.DataArray([1, 0.1, 3.5, 5], coords={"realization": ens.realization}) out4 = ensembles.ensemble_percentiles(ens, weights=weights) np.testing.assert_array_almost_equal( ens["tg_mean"].isel(time=0, lon=5, lat=5).weighted(weights).quantile(0.5), @@ -215,18 +182,14 @@ def test_calc_perc(self, transpose, ensemble_dataset_objects, open_dataset): assert np.all(out4["tg_mean_p90"] > out4["tg_mean_p10"]) @pytest.mark.parametrize("keep_chunk_size", [False, True, None]) - def test_calc_perc_dask( - self, keep_chunk_size, ensemble_dataset_objects, open_dataset - ): + def test_calc_perc_dask(self, keep_chunk_size, ensemble_dataset_objects, open_dataset): ds_all = [] for n in ensemble_dataset_objects["nc_files_simple"]: ds = open_dataset(n) ds_all.append(ds) ens = ensembles.create_ensemble(ds_all) - out2 = ensembles.ensemble_percentiles( - ens.chunk({"time": 2}), keep_chunk_size=keep_chunk_size, split=False - ) + out2 = ensembles.ensemble_percentiles(ens.chunk({"time": 2}), keep_chunk_size=keep_chunk_size, split=False) out1 = ensembles.ensemble_percentiles(ens.load(), split=False) np.testing.assert_array_equal(out1["tg_mean"], out2["tg_mean"]) @@ -269,17 +232,11 @@ def test_calc_mean_std_min_max(self, ensemble_dataset_objects, open_dataset): 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].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]) assert "Computation of statistics on" in out1.attrs["history"] - weights = xr.DataArray( - [1, 0.1, 3.5, 5], coords={"realization": ens.realization} - ) + weights = xr.DataArray([1, 0.1, 3.5, 5], coords={"realization": ens.realization}) out2 = ensembles.ensemble_mean_std_max_min(ens, weights=weights) values = ens["tg_mean"][:, 0, 5, 5] # Explicit float64 so numpy does the expected datatype promotion (change in numpy 2) @@ -297,16 +254,10 @@ def test_calc_mean_std_min_max(self, ensemble_dataset_objects, open_dataset): ens["tg_mean"][:, 0, 5, 5].weighted(weights).std(dim="realization"), out2.tg_mean_stdev[0, 5, 5], ) - np.testing.assert_array_equal( - out1.tg_mean_max[0, 5, 5], out2.tg_mean_max[0, 5, 5] - ) - np.testing.assert_array_equal( - out1.tg_mean_min[0, 5, 5], out2.tg_mean_min[0, 5, 5] - ) + np.testing.assert_array_equal(out1.tg_mean_max[0, 5, 5], out2.tg_mean_max[0, 5, 5]) + np.testing.assert_array_equal(out1.tg_mean_min[0, 5, 5], out2.tg_mean_min[0, 5, 5]) - @pytest.mark.parametrize( - "aggfunc", [ensembles.ensemble_percentiles, ensembles.ensemble_mean_std_max_min] - ) + @pytest.mark.parametrize("aggfunc", [ensembles.ensemble_percentiles, ensembles.ensemble_mean_std_max_min]) def test_stats_min_members(self, ensemble_dataset_objects, open_dataset, aggfunc): ds_all = [open_dataset(n) for n in ensemble_dataset_objects["nc_files_simple"]] ens = ensembles.create_ensemble(ds_all).isel(lat=0, lon=0) @@ -323,9 +274,7 @@ def first(ds): # A number out = first(aggfunc(ens, min_members=3)) # Only 1950 is null - np.testing.assert_array_equal( - out.isnull(), [True] + [False] * (ens.time.size - 1) - ) + np.testing.assert_array_equal(out.isnull(), [True] + [False] * (ens.time.size - 1)) # Special value out = first(aggfunc(ens, min_members=None)) @@ -476,9 +425,7 @@ def test_kmeans_modelweights(self, open_dataset, random_state): if np.sum(cluster == cluster[i]) > 1: assert i not in ids - @pytest.mark.skipif( - "matplotlib.pyplot" not in sys.modules, reason="matplotlib.pyplot is required" - ) + @pytest.mark.skipif("matplotlib.pyplot" not in sys.modules, reason="matplotlib.pyplot is required") def test_kmeans_rsqcutoff_with_graphs(self, open_dataset, random_state): pytest.importorskip("sklearn", minversion="0.24.1") ds = open_dataset(self.nc_file) @@ -535,9 +482,7 @@ def test_kkz_change_metric(self, open_dataset): data = ens.data.isel(criteria=[1, 3, 5]) sel_euc = ensembles.kkz_reduce_ensemble(data, 4, dist_method="euclidean") - sel_mah = ensembles.kkz_reduce_ensemble( - data, 4, dist_method="mahalanobis", VI=np.arange(24) - ) + sel_mah = ensembles.kkz_reduce_ensemble(data, 4, dist_method="mahalanobis", VI=np.arange(24)) assert sel_euc == [23, 10, 19, 14] assert sel_mah == [5, 3, 4, 0] @@ -546,15 +491,9 @@ def test_standardize_seuclidean(self, open_dataset): ens = open_dataset(self.nc_file) data = ens.data for n in np.arange(1, len(data)): - sel1 = ensembles.kkz_reduce_ensemble( - data, n, dist_method="seuclidean", standardize=True - ) - sel2 = ensembles.kkz_reduce_ensemble( - data, n, dist_method="seuclidean", standardize=False - ) - sel3 = ensembles.kkz_reduce_ensemble( - data, n, dist_method="euclidean", standardize=True - ) + sel1 = ensembles.kkz_reduce_ensemble(data, n, dist_method="seuclidean", standardize=True) + sel2 = ensembles.kkz_reduce_ensemble(data, n, dist_method="seuclidean", standardize=False) + sel3 = ensembles.kkz_reduce_ensemble(data, n, dist_method="euclidean", standardize=True) assert sel1 == sel2 assert sel1 == sel3 @@ -595,20 +534,12 @@ def test_make_criteria(self, tas_series): def robust_data(random): norm = get_dist("norm") ref = np.tile( - np.array( - [ - norm.rvs(loc=274, scale=0.8, size=(40,), random_state=random) - for r in range(4) - ] - ), + np.array([norm.rvs(loc=274, scale=0.8, size=(40,), random_state=random) for r in range(4)]), (4, 1, 1), ) fut = np.array( [ - [ - norm.rvs(loc=loc, scale=sc, size=(40,), random_state=random) - for loc, sc in shps - ] + [norm.rvs(loc=loc, scale=sc, size=(40,), random_state=random) for loc, sc in shps] for shps in ( [ (274.0, 0.7), @@ -725,9 +656,7 @@ def robust_data(random): ), ], ) -def test_robustness_fractions( - robust_data, test, exp_chng_frac, exp_pos_frac, exp_changed, kws -): +def test_robustness_fractions(robust_data, test, exp_chng_frac, exp_pos_frac, exp_changed, kws): ref, fut = robust_data fracs = ensembles.robustness_fractions(fut, ref, test=test, **kws) @@ -752,9 +681,7 @@ def test_robustness_fractions_weighted(robust_data): assert fracs.changed.attrs["test"] == "None" np.testing.assert_array_equal(fracs.changed, [1, 1, 1, 1]) - np.testing.assert_array_almost_equal( - fracs.changed_positive, [0.53125, 0.88541667, 1.0, 1.0] - ) + np.testing.assert_array_almost_equal(fracs.changed_positive, [0.53125, 0.88541667, 1.0, 1.0]) def test_robustness_fractions_delta(robust_data): @@ -767,9 +694,7 @@ def test_robustness_fractions_delta(robust_data): delta = xr.DataArray([-2, 1, -2, -1], dims=("realization",)) weights = xr.DataArray([4, 3, 2, 1], dims=("realization",)) - fracs = ensembles.robustness_fractions( - delta, test="threshold", abs_thresh=1.5, weights=weights - ) + fracs = ensembles.robustness_fractions(delta, test="threshold", abs_thresh=1.5, weights=weights) np.testing.assert_array_equal(fracs.changed, [0.6]) np.testing.assert_array_equal(fracs.positive, [0.3]) np.testing.assert_array_equal(fracs.agree, [0.7]) @@ -795,10 +720,7 @@ def test_robustness_categories(): categories = ensembles.robustness_categories(changed, agree) np.testing.assert_array_equal(categories, [2, 3, 3, 1]) assert categories.flag_values == [1, 2, 3] - assert ( - categories.flag_meanings - == "robust_signal no_change_or_no_signal conflicting_signal" - ) + assert categories.flag_meanings == "robust_signal no_change_or_no_signal conflicting_signal" assert categories.lat.attrs["axis"] == "Y" diff --git a/tests/test_ffdi.py b/tests/test_ffdi.py index 1449f0453..32667e1fc 100644 --- a/tests/test_ffdi.py +++ b/tests/test_ffdi.py @@ -52,18 +52,14 @@ class TestFFDI: ), ], ) - def test_keetch_byram_drought_index( - self, p, t, pa, k0, exp, pr_series, tasmax_series - ): + def test_keetch_byram_drought_index(self, p, t, pa, k0, exp, pr_series, tasmax_series): """Compare output to calculation by hand""" pr = pr_series(p, units="mm/day") tasmax = tasmax_series(t, units="degC") pr_annual = xr.DataArray(pa, attrs={"units": "mm/year"}) kbdi0 = xr.DataArray(k0, attrs={"units": "mm/day"}) - kbdi_final = keetch_byram_drought_index(pr, tasmax, pr_annual, kbdi0).isel( - time=-1 - ) + kbdi_final = keetch_byram_drought_index(pr, tasmax, pr_annual, kbdi0).isel(time=-1) np.testing.assert_allclose(kbdi_final, exp, atol=1e-5) @pytest.mark.parametrize( @@ -120,9 +116,7 @@ def test_griffiths_drought_factor_sliding(self, pr_series): df = griffiths_drought_factor(pr, smd, "xlim").isel(time=slice(19, None)) np.testing.assert_allclose(df, exp, atol=1e-5) - def test_mcarthur_forest_fire_danger_index( - self, pr_series, tasmax_series, hurs_series, sfcWind_series - ): + def test_mcarthur_forest_fire_danger_index(self, pr_series, tasmax_series, hurs_series, sfcWind_series): """Compare output to calculation by hand""" D = pr_series(range(1, 11), units="") # This is probably not good practice? T = tasmax_series(range(30, 40), units="degC") @@ -130,9 +124,7 @@ def test_mcarthur_forest_fire_danger_index( V = sfcWind_series(range(10, 20)) # Compare FFDI to values calculated using original arrangement of the FFDI: - exp = 2.0 * np.exp( - -0.450 + 0.987 * np.log(D) - 0.0345 * H + 0.0338 * T + 0.0234 * V - ) + exp = 2.0 * np.exp(-0.450 + 0.987 * np.log(D) - 0.0345 * H + 0.0338 * T + 0.0234 * V) ffdi = mcarthur_forest_fire_danger_index(D, T, H, V) np.testing.assert_allclose(ffdi, exp, rtol=1e-6) @@ -158,9 +150,7 @@ def test_ffdi_indicators(self, open_dataset, init_kbdi, limiting_func): else: kbdi0 = None - kbdi = atmos.keetch_byram_drought_index( - test_data["pr"], test_data["tasmax"], pr_annual, kbdi0 - ) + kbdi = atmos.keetch_byram_drought_index(test_data["pr"], test_data["tasmax"], pr_annual, kbdi0) assert (kbdi >= 0).all() assert (kbdi <= 203.2).all() assert kbdi.shape == test_data["pr"].shape diff --git a/tests/test_flags.py b/tests/test_flags.py index 75c729f5c..1fb071535 100644 --- a/tests/test_flags.py +++ b/tests/test_flags.py @@ -18,13 +18,9 @@ class TestDataFlags: ([], dict(tas_exceeds_tasmax=False, tas_below_tasmin=False)), ], ) - def test_tas_temperature_flags( - self, vars_dropped, flags, tas_series, tasmax_series, tasmin_series - ): + def test_tas_temperature_flags(self, vars_dropped, flags, tas_series, tasmax_series, tasmin_series): ds = xr.Dataset() - for series, val in zip( - [tas_series, tasmax_series, tasmin_series], [0, 10, -10], strict=False - ): + for series, val in zip([tas_series, tasmax_series, tasmin_series], [0, 10, -10], strict=False): vals = val + K2C + np.sin(2 * np.pi * np.arange(366 * 3) / 366) arr = series(vals, start="1971-01-01") ds = xr.merge([ds, arr]) @@ -34,12 +30,8 @@ def test_tas_temperature_flags( np.testing.assert_equal(flagged_ds.temperature_extremely_high.values, False) np.testing.assert_equal(flagged_ds.temperature_extremely_low.values, False) - np.testing.assert_equal( - flagged_ds.values_repeating_for_5_or_more_days.values, False - ) - np.testing.assert_equal( - flagged_ds.outside_5_standard_deviations_of_climatology.values, False - ) + np.testing.assert_equal(flagged_ds.values_repeating_for_5_or_more_days.values, False) + np.testing.assert_equal(flagged_ds.outside_5_standard_deviations_of_climatology.values, False) for flag, val in flags.items(): np.testing.assert_equal(getattr(flagged_ds, flag).values, val) @@ -74,18 +66,12 @@ def test_suspicious_pr_data(self, pr_series): flagged = df.data_flags(bad_pr) np.testing.assert_equal(flagged.negative_accumulation_values.values, True) np.testing.assert_equal(flagged.very_large_precipitation_events.values, True) - np.testing.assert_equal( - flagged.values_eq_1_repeating_for_10_or_more_days.values, True - ) - np.testing.assert_equal( - flagged.values_eq_5_repeating_for_5_or_more_days.values, True - ) + np.testing.assert_equal(flagged.values_eq_1_repeating_for_10_or_more_days.values, True) + np.testing.assert_equal(flagged.values_eq_5_repeating_for_5_or_more_days.values, True) def test_suspicious_tas_data(self, tas_series, tasmax_series, tasmin_series): bad_ds = xr.Dataset() - for series, val in zip( - [tas_series, tasmax_series, tasmin_series], [0, 10, -10], strict=False - ): + for series, val in zip([tas_series, tasmax_series, tasmin_series], [0, 10, -10], strict=False): vals = val + K2C + np.sin(2 * np.pi * np.arange(366 * 7) / 366) arr = series(vals, start="1971-01-01") bad_ds = xr.merge([bad_ds, arr]) @@ -106,9 +92,7 @@ def test_suspicious_tas_data(self, tas_series, tasmax_series, tasmin_series): flagged = df.data_flags(bad_ds.tas, bad_ds) np.testing.assert_equal(flagged.temperature_extremely_high.values, True) np.testing.assert_equal(flagged.temperature_extremely_low.values, True) - np.testing.assert_equal( - flagged.values_repeating_for_5_or_more_days.values, True - ) + np.testing.assert_equal(flagged.values_repeating_for_5_or_more_days.values, True) np.testing.assert_equal( flagged.outside_5_standard_deviations_of_climatology.values, True, @@ -164,7 +148,4 @@ def test_names(self, pr_series): } }, ) - assert ( - list(flgs.data_vars.keys())[0] - == "values_eq_minus5point1_repeating_for_5_or_more_days" - ) + assert list(flgs.data_vars.keys())[0] == "values_eq_minus5point1_repeating_for_5_or_more_days" diff --git a/tests/test_formatting.py b/tests/test_formatting.py index 7f58c2a67..cd91c8cc9 100644 --- a/tests/test_formatting.py +++ b/tests/test_formatting.py @@ -26,10 +26,7 @@ def test_prefix_attrs(): def test_indicator_docstring(): doc = heat_wave_frequency.__doc__.split("\n") assert doc[0] == "Heat wave frequency (realm: atmos)" - assert ( - doc[5] - == "Based on indice :py:func:`~xclim.indices._multivariate.heat_wave_frequency`." - ) + assert doc[5] == "Based on indice :py:func:`~xclim.indices._multivariate.heat_wave_frequency`." assert doc[6] == "Keywords : temperature health,." assert doc[12] == " Default : `ds.tasmin`. [Required units : [temperature]]" assert ( diff --git a/tests/test_generic.py b/tests/test_generic.py index 3b4524e33..91317e768 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -35,9 +35,7 @@ def test_season(self, q_series): class TestSelectRollingResampleOp: def test_rollingmax(self, q_series): q = q_series(np.arange(1, 366 + 365 + 365 + 1)) # 1st year is leap - o = generic.select_rolling_resample_op( - q, "max", window=14, window_center=False, window_op="mean" - ) + o = generic.select_rolling_resample_op(q, "max", window=14, window_center=False, window_op="mean") np.testing.assert_array_equal( [ np.mean(np.arange(353, 366 + 1)), @@ -50,9 +48,7 @@ def test_rollingmax(self, q_series): def test_rollingmaxindexer(self, q_series): q = q_series(np.arange(1, 366 + 365 + 365 + 1)) # 1st year is leap - o = generic.select_rolling_resample_op( - q, "min", window=14, window_center=False, window_op="max", season="DJF" - ) + o = generic.select_rolling_resample_op(q, "min", window=14, window_center=False, window_op="max", season="DJF") np.testing.assert_array_equal( [14, 367, 367 + 365], o.values ) # 14th day for 1st year, then Jan 1st for the next two @@ -60,9 +56,7 @@ def test_rollingmaxindexer(self, q_series): def test_freq(self, q_series): q = q_series(np.arange(1, 366 + 365 + 365 + 1)) # 1st year is leap - o = generic.select_rolling_resample_op( - q, "max", window=3, window_center=True, window_op="integral", freq="MS" - ) + o = generic.select_rolling_resample_op(q, "max", window=3, window_center=True, window_op="integral", freq="MS") np.testing.assert_array_equal( [ np.sum([30, 31, 32]) * 86400, @@ -110,12 +104,8 @@ def test_doyminmax(self, q_series): class TestAggregateBetweenDates: def test_calendars(self): # generate test DataArray - time_std = xr.date_range( - "1991-07-01", "1993-06-30", freq="D", calendar="standard" - ) - time_365 = xr.date_range( - "1991-07-01", "1993-06-30", freq="D", calendar="noleap" - ) + time_std = xr.date_range("1991-07-01", "1993-06-30", freq="D", calendar="standard") + time_365 = xr.date_range("1991-07-01", "1993-06-30", freq="D", calendar="noleap") data_std = xr.DataArray( np.ones((time_std.size, 4)), dims=("time", "lon"), @@ -144,9 +134,7 @@ def test_calendars(self): attrs={"calendar": "noleap", "is_dayofyear": 1}, ) - out = generic.aggregate_between_dates( - data_std, start_std, end_std, op="sum", freq="YS-JUL" - ) + out = generic.aggregate_between_dates(data_std, start_std, end_std, op="sum", freq="YS-JUL") # expected output s = doy_to_days_since(start_std) @@ -157,23 +145,15 @@ def test_calendars(self): np.testing.assert_allclose(out, expected) # check calendar conversion - out_noleap = generic.aggregate_between_dates( - data_std, start_std, end_noleap, op="sum", freq="YS-JUL" - ) + out_noleap = generic.aggregate_between_dates(data_std, start_std, end_noleap, op="sum", freq="YS-JUL") np.testing.assert_allclose(out, out_noleap) def test_time_length(self): # generate test DataArray - time_data = xr.date_range( - "1991-01-01", "1993-12-31", freq="D", calendar="standard" - ) - time_start = xr.date_range( - "1990-01-01", "1992-12-31", freq="D", calendar="standard" - ) - time_end = xr.date_range( - "1991-01-01", "1993-12-31", freq="D", calendar="standard" - ) + time_data = xr.date_range("1991-01-01", "1993-12-31", freq="D", calendar="standard") + time_start = xr.date_range("1990-01-01", "1992-12-31", freq="D", calendar="standard") + time_end = xr.date_range("1991-01-01", "1993-12-31", freq="D", calendar="standard") data = xr.DataArray( np.ones((time_data.size, 4)), dims=("time", "lon"), @@ -214,9 +194,7 @@ def test_time_length(self): def test_frequency(self): # generate test DataArray - time_data = xr.date_range( - "1991-01-01", "1992-05-31", freq="D", calendar="standard" - ) + time_data = xr.date_range("1991-01-01", "1992-05-31", freq="D", calendar="standard") data = xr.DataArray( np.ones((time_data.size, 2)), dims=("time", "lon"), @@ -288,9 +266,7 @@ def test_frequency(self): def test_day_of_year_strings(self): # generate test DataArray - time_data = xr.date_range( - "1990-08-01", "1995-06-01", freq="D", calendar="standard" - ) + time_data = xr.date_range("1990-08-01", "1995-06-01", freq="D", calendar="standard") data = xr.DataArray( np.ones(time_data.size), dims="time", @@ -415,9 +391,7 @@ class TestGenericCountingIndices: "op_high, op_low, expected", [(">", "<", 1), (">", "<=", 2), (">=", "<", 3), (">=", "<=", 4)], ) - def test_simple_count_level_crossings( - self, tasmin_series, tasmax_series, op_high, op_low, expected - ): + def test_simple_count_level_crossings(self, tasmin_series, tasmax_series, op_high, op_low, expected): tasmin = tasmin_series(np.array([-1, -3, 0, 5, 9, 1, 3]) + K2C) tasmax = tasmax_series(np.array([5, 7, 3, 6, 13, 5, 4]) + K2C) @@ -431,9 +405,7 @@ def test_simple_count_level_crossings( ) np.testing.assert_array_equal(crossings, [expected]) - @pytest.mark.parametrize( - "op_high, op_low", [("<=", "<="), (">=", ">="), ("<", ">"), ("==", "!=")] - ) + @pytest.mark.parametrize("op_high, op_low", [("<=", "<="), (">=", ">="), ("<", ">"), ("==", "!=")]) def test_forbidden_op(self, tasmin_series, tasmax_series, op_high, op_low): tasmin = tasmin_series(np.zeros(7) + K2C) tasmax = tasmax_series(np.ones(7) + K2C) @@ -466,13 +438,9 @@ def test_count_occurrences(self, tas_series, op, constrain, expected, should_fai if should_fail: with pytest.raises(ValueError): - generic.count_occurrences( - tas, "4 degC", freq="YS", op=op, constrain=constrain - ) + generic.count_occurrences(tas, "4 degC", freq="YS", op=op, constrain=constrain) else: - occurrences = generic.count_occurrences( - tas, "4 degC", freq="YS", op=op, constrain=constrain - ) + occurrences = generic.count_occurrences(tas, "4 degC", freq="YS", op=op, constrain=constrain) np.testing.assert_array_equal(occurrences, [expected]) @pytest.mark.parametrize( @@ -486,19 +454,13 @@ def test_count_occurrences(self, tas_series, op, constrain, expected, should_fai ], ) def test_first_occurrence(self, tas_series, op, constrain, expected, should_fail): - tas = tas_series( - np.array([15, 12, 11, 12, 14, 13, 18, 11, 13]) + K2C, start="1/1/2000" - ) + tas = tas_series(np.array([15, 12, 11, 12, 14, 13, 18, 11, 13]) + K2C, start="1/1/2000") if should_fail: with pytest.raises(ValueError): - generic.first_occurrence( - tas, threshold="11 degC", freq="YS", op=op, constrain=constrain - ) + generic.first_occurrence(tas, threshold="11 degC", freq="YS", op=op, constrain=constrain) else: - first = generic.first_occurrence( - tas, threshold="11 degC", freq="YS", op=op, constrain=constrain - ) + first = generic.first_occurrence(tas, threshold="11 degC", freq="YS", op=op, constrain=constrain) np.testing.assert_array_equal(first, [expected]) @@ -513,19 +475,13 @@ def test_first_occurrence(self, tas_series, op, constrain, expected, should_fail ], ) def test_last_occurrence(self, tas_series, op, constrain, expected, should_fail): - tas = tas_series( - np.array([15, 12, 11, 12, 14, 13, 18, 11, 13]) + K2C, start="1/1/2000" - ) + tas = tas_series(np.array([15, 12, 11, 12, 14, 13, 18, 11, 13]) + K2C, start="1/1/2000") if should_fail: with pytest.raises(ValueError): - generic.last_occurrence( - tas, threshold="11 degC", freq="YS", op=op, constrain=constrain - ) + generic.last_occurrence(tas, threshold="11 degC", freq="YS", op=op, constrain=constrain) else: - first = generic.last_occurrence( - tas, threshold="11 degC", freq="YS", op=op, constrain=constrain - ) + first = generic.last_occurrence(tas, threshold="11 degC", freq="YS", op=op, constrain=constrain) np.testing.assert_array_equal(first, [expected]) @@ -627,32 +583,22 @@ def test_select_time_doys(self): @pytest.mark.parametrize("include_bounds", [True, False]) def test_select_time_doys_2D_spatial(self, include_bounds): # first doy of da is 44, last is 366 - da = self.series("2003-02-13", "2004-12-31", "default").expand_dims( - lat=[0, 10, 15, 20, 25] - ) + da = self.series("2003-02-13", "2004-12-31", "default").expand_dims(lat=[0, 10, 15, 20, 25]) # 5 cases # normal : start < end # over NYE : end < start # end is nan (i.e. 366) # start is nan (i.e. 1) # both are nan (no drop) - start = xr.DataArray( - [50, 340, 100, np.nan, np.nan], dims=("lat",), coords={"lat": da.lat} - ) - end = xr.DataArray( - [200, 20, np.nan, 200, np.nan], dims=("lat",), coords={"lat": da.lat} - ) + start = xr.DataArray([50, 340, 100, np.nan, np.nan], dims=("lat",), coords={"lat": da.lat}) + end = xr.DataArray([200, 20, np.nan, 200, np.nan], dims=("lat",), coords={"lat": da.lat}) out = select_time(da, doy_bounds=(start, end), include_bounds=include_bounds) exp = [151 * 2, 26 + 20 + 27, 266 + 267, 200 - 43 + 200, 365 - 43 + 366] if not include_bounds: exp[0] = exp[0] - 4 # 2 years * 2 - exp[1] = ( - exp[1] - 3 - ) # 2 on 1st year, 1 on 2nd (end bnd is after end of data) - exp[2] = ( - exp[2] - 2 - ) # "Open" bound so always included, 1 real bnd on each year + exp[1] = exp[1] - 3 # 2 on 1st year, 1 on 2nd (end bnd is after end of data) + exp[2] = exp[2] - 2 # "Open" bound so always included, 1 real bnd on each year exp[3] = exp[3] - 2 # Same # No real bound on exp[4] np.testing.assert_array_equal(out.notnull().sum("time"), exp) @@ -670,12 +616,8 @@ def test_select_time_doys_2D_temporal(self, include_bounds): # 06-07 : start is nan (i.e. start of period) # 07- : both are nan (no drop) time = xr.date_range("2003-07-01", freq="YS-JUL", periods=5) - start = xr.DataArray( - [50, 340, 100, np.nan, np.nan], dims=("time",), coords={"time": time} - ) - end = xr.DataArray( - [100, 20, np.nan, 200, np.nan], dims=("time",), coords={"time": time} - ) + start = xr.DataArray([50, 340, 100, np.nan, np.nan], dims=("time",), coords={"time": time}) + end = xr.DataArray([100, 20, np.nan, 200, np.nan], dims=("time",), coords={"time": time}) out = select_time(da, doy_bounds=(start, end), include_bounds=include_bounds) exp = [0, 51, 47, 82, 19, 184] @@ -721,9 +663,7 @@ def test_select_time_errors(self): xr.testing.assert_identical(da, select_time(da)) - with pytest.raises( - ValueError, match="Only one method of indexing may be given" - ): + with pytest.raises(ValueError, match="Only one method of indexing may be given"): select_time(da, season="DJF", month=[3, 4, 5]) with pytest.raises(ValueError, match="invalid day number provided in cftime."): @@ -741,52 +681,32 @@ def test_single_variable(self): data = xr.DataArray([0, 1, 2, 3, 2, 1, 0, 0], dims=("time",)) out = generic.spell_mask(data, 3, "min", ">=", 2) - np.testing.assert_array_equal( - out, np.array([0, 0, 1, 1, 1, 0, 0, 0]).astype(bool) - ) + np.testing.assert_array_equal(out, np.array([0, 0, 1, 1, 1, 0, 0, 0]).astype(bool)) out = generic.spell_mask(data, 3, "max", ">=", 2) - np.testing.assert_array_equal( - out, np.array([1, 1, 1, 1, 1, 1, 1, 0]).astype(bool) - ) + np.testing.assert_array_equal(out, np.array([1, 1, 1, 1, 1, 1, 1, 0]).astype(bool)) out = generic.spell_mask(data, 2, "mean", ">=", 2) - np.testing.assert_array_equal( - out, np.array([0, 0, 1, 1, 1, 0, 0, 0]).astype(bool) - ) + np.testing.assert_array_equal(out, np.array([0, 0, 1, 1, 1, 0, 0, 0]).astype(bool)) out = generic.spell_mask(data, 3, "mean", ">", 2, weights=[0.2, 0.4, 0.4]) - np.testing.assert_array_equal( - out, np.array([0, 1, 1, 1, 1, 0, 0, 0]).astype(bool) - ) + np.testing.assert_array_equal(out, np.array([0, 1, 1, 1, 1, 0, 0, 0]).astype(bool)) def test_multiple_variables(self): data1 = xr.DataArray([0, 1, 2, 3, 2, 1, 0, 0], dims=("time",)) data2 = xr.DataArray([1, 2, 3, 2, 1, 0, 0, 0], dims=("time",)) out = generic.spell_mask([data1, data2], 3, "min", ">=", [2, 2]) - np.testing.assert_array_equal( - out, np.array([0, 0, 0, 0, 0, 0, 0, 0]).astype(bool) - ) + np.testing.assert_array_equal(out, np.array([0, 0, 0, 0, 0, 0, 0, 0]).astype(bool)) - out = generic.spell_mask( - [data1, data2], 3, "min", ">=", [2, 2], var_reducer="any" - ) - np.testing.assert_array_equal( - out, np.array([0, 1, 1, 1, 1, 0, 0, 0]).astype(bool) - ) + out = generic.spell_mask([data1, data2], 3, "min", ">=", [2, 2], var_reducer="any") + np.testing.assert_array_equal(out, np.array([0, 1, 1, 1, 1, 0, 0, 0]).astype(bool)) out = generic.spell_mask([data1, data2], 2, "mean", ">=", [2, 2]) - np.testing.assert_array_equal( - out, np.array([0, 0, 1, 1, 0, 0, 0, 0]).astype(bool) - ) + np.testing.assert_array_equal(out, np.array([0, 0, 1, 1, 0, 0, 0, 0]).astype(bool)) - out = generic.spell_mask( - [data1, data2], 3, "mean", ">", [2, 1.5], weights=[0.2, 0.4, 0.4] - ) - np.testing.assert_array_equal( - out, np.array([0, 1, 1, 1, 1, 0, 0, 0]).astype(bool) - ) + out = generic.spell_mask([data1, data2], 3, "mean", ">", [2, 1.5], weights=[0.2, 0.4, 0.4]) + np.testing.assert_array_equal(out, np.array([0, 1, 1, 1, 1, 0, 0, 0]).astype(bool)) def test_errors(self): data = xr.DataArray([0, 1, 2, 3, 2, 1, 0, 0], dims=("time",)) @@ -800,9 +720,7 @@ def test_errors(self): generic.spell_mask([data, data], 3, "min", "<=", [2]) # Weights must have win_reducer = 'mean' - with pytest.raises( - ValueError, match="is only supported if 'win_reducer' is 'mean'" - ): + with pytest.raises(ValueError, match="is only supported if 'win_reducer' is 'mean'"): generic.spell_mask(data, 3, "min", "<=", 2, weights=[1, 2, 3]) # Weights must have same length as window @@ -842,7 +760,6 @@ def test_spell_length_statistics_multi(tasmin_series, tasmax_series): class TestThresholdedEvents: - @pytest.mark.parametrize("use_dask", [True, False]) def test_simple(self, pr_series, use_dask): arr = np.array([0, 0, 0, 1, 2, 3, 0, 3, 3, 10, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0, 0, 0, 1, 3, 3, 2, 0, 0, 0, 2, 0, 0, 0, 0]) # fmt: skip @@ -881,9 +798,7 @@ def test_diff_windows(self, pr_series, use_dask): # different window stop out = ( - generic.thresholded_events( - pr, thresh="2 mm", op=">=", window=3, window_stop=4 - ) + generic.thresholded_events(pr, thresh="2 mm", op=">=", window=3, window_stop=4) .load() .dropna("event", how="all") ) @@ -893,9 +808,7 @@ def test_diff_windows(self, pr_series, use_dask): np.testing.assert_array_equal(out.event_sum, [16, 6, 10]) np.testing.assert_array_equal( out.event_start, - np.array( - ["2000-01-08", "2000-01-17", "2000-01-27"], dtype="datetime64[ns]" - ), + np.array(["2000-01-08", "2000-01-17", "2000-01-27"], dtype="datetime64[ns]"), ) @pytest.mark.parametrize("use_dask", [True, False]) @@ -922,15 +835,9 @@ def test_cftime(self, pr_series, use_dask): exp = xr.DataArray( [1, 2, 3], dims=("time",), - coords={ - "time": np.array( - ["2000-01-04", "2000-01-16", "2000-01-26"], dtype="datetime64[ns]" - ) - }, - ) - np.testing.assert_array_equal( - out.event_start, exp.convert_calendar("noleap").time + coords={"time": np.array(["2000-01-04", "2000-01-16", "2000-01-26"], dtype="datetime64[ns]")}, ) + np.testing.assert_array_equal(out.event_start, exp.convert_calendar("noleap").time) @pytest.mark.parametrize("use_dask", [True, False]) def test_freq(self, pr_series, use_dask): @@ -941,16 +848,12 @@ def test_freq(self, pr_series, use_dask): pr = pr.chunk(-1) with assert_lazy: - out = generic.thresholded_events( - pr, thresh="1 mm", op=">=", window=3, freq="MS", window_stop=3 - ) + out = generic.thresholded_events(pr, thresh="1 mm", op=">=", window=3, freq="MS", window_stop=3) assert out.event_length.shape == (2, 6) out = out.load().dropna("event", how="all") np.testing.assert_array_equal(out.event_length, [[7, 6, 4], [3, 5, np.nan]]) - np.testing.assert_array_equal( - out.event_effective_length, [[6, 6, 4], [3, 5, np.nan]] - ) + np.testing.assert_array_equal(out.event_effective_length, [[6, 6, 4], [3, 5, np.nan]]) np.testing.assert_array_equal(out.event_sum, [[22, 12, 10], [5, 17, np.nan]]) np.testing.assert_array_equal( out.event_start, diff --git a/tests/test_generic_indicators.py b/tests/test_generic_indicators.py index 6411e1479..c0d5e6c66 100644 --- a/tests/test_generic_indicators.py +++ b/tests/test_generic_indicators.py @@ -43,13 +43,10 @@ def test_options(self, q_series, random): class TestReturnLevel: def test_seasonal(self, ndq_series): - out = generic.return_level( - ndq_series, mode="max", t=[2, 5], dist="gamma", season="DJF" - ) + out = generic.return_level(ndq_series, mode="max", t=[2, 5], dist="gamma", season="DJF") assert out.description == ( - "Frequency analysis for the maximal winter 1-day value estimated using the " - "gamma distribution." + "Frequency analysis for the maximal winter 1-day value estimated using the gamma distribution." ) assert out.name == "fa_1maxwinter" assert out.shape == (2, 2, 3) # nrt, nx, ny @@ -76,9 +73,7 @@ def test_q27(self, ndq_series): def test_empty(self, ndq_series): q = ndq_series.copy() q[:, 0, 0] = np.nan - out = generic.return_level( - q, mode="max", t=2, dist="genextreme", window=6, freq="YS" - ) + out = generic.return_level(q, mode="max", t=2, dist="genextreme", window=6, freq="YS") assert np.isnan(out.values[:, 0, 0]).all() @@ -99,9 +94,7 @@ def test_ndq(self, ndq_series): assert out.attrs["units"] == "m3 s-1" def test_missing(self, ndq_series): - a = ndq_series.where( - ~((ndq_series.time.dt.dayofyear == 5) & (ndq_series.time.dt.year == 1902)) - ) + a = ndq_series.where(~((ndq_series.time.dt.dayofyear == 5) & (ndq_series.time.dt.year == 1902))) assert a.shape == (5000, 2, 3) out = generic.stats(a, op="max", month=1) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 064227052..953fffaba 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -16,9 +16,7 @@ def test_solar_declinaton(method, rtol): # Expected values from https://gml.noaa.gov/grad/solcalc/azel.html times = xr.DataArray( - pd.to_datetime( - ["1793-01-21T10:22:00", "1969-07-20T20:17:40", "2022-05-20T16:55:48"] - ), + pd.to_datetime(["1793-01-21T10:22:00", "1969-07-20T20:17:40", "2022-05-20T16:55:48"]), dims=("time",), ) exp = [-19.83, 20.64, 20.00] @@ -46,9 +44,7 @@ def test_extraterrestrial_radiation(method): ) exp = [99.06, 239.98, 520.01] np.testing.assert_allclose( - convert_units_to( - helpers.extraterrestrial_solar_radiation(times, lat, method=method), "W m-2" - ), + convert_units_to(helpers.extraterrestrial_solar_radiation(times, lat, method=method), "W m-2"), exp, rtol=3e-2, ) @@ -80,29 +76,19 @@ def test_day_lengths(method): for event, evaluations in events.items(): for e in evaluations: if event == "solstice": - np.testing.assert_array_almost_equal( - dl.sel(time=e[0]).transpose(), np.array(e[1]), 2 - ) + np.testing.assert_array_almost_equal(dl.sel(time=e[0]).transpose(), np.array(e[1]), 2) elif event == "equinox": - np.testing.assert_allclose( - dl.sel(time=e[0]).transpose(), np.array(e[1]), rtol=2e-1 - ) + np.testing.assert_allclose(dl.sel(time=e[0]).transpose(), np.array(e[1]), rtol=2e-1) def test_cosine_of_solar_zenith_angle(): time = xr.date_range("1900-01-01T00:30", "1900-01-03", freq="h") time = xr.DataArray(time, dims=("time",), coords={"time": time}, name="time") - lat = xr.DataArray( - [0, 45, 70], dims=("site",), name="lat", attrs={"units": "degree_north"} - ) - lon = xr.DataArray( - [-40, 0, 80], dims=("site",), name="lon", attrs={"units": "degree_east"} - ) + lat = xr.DataArray([0, 45, 70], dims=("site",), name="lat", attrs={"units": "degree_north"}) + lon = xr.DataArray([-40, 0, 80], dims=("site",), name="lon", attrs={"units": "degree_east"}) dec = helpers.solar_declination(time) - czda = helpers.cosine_of_solar_zenith_angle( - time, dec, lat, lon, stat="average", sunlit=True - ) + czda = helpers.cosine_of_solar_zenith_angle(time, dec, lat, lon, stat="average", sunlit=True) # Data Generated with PyWGBT # raw = coszda( # (time + pd.Timedelta('30 m')).data, @@ -110,7 +96,11 @@ def test_cosine_of_solar_zenith_angle(): # convert_units_to(lon, 'rad').data[np.newaxis, :], # 1 # ) - # exp_cza = xr.DataArray(raw, dims=('time', 'd', 'site'), coords={'lat': lat, 'lon': lon, 'time': time}).squeeze('d') + # exp_cza = xr.DataArray( + # raw, + # dims=('time', 'd', 'site'), + # coords={'lat': lat, 'lon': lon, 'time': time} + # ).squeeze('d') exp_czda = np.array( [ [0.0, 0.0610457, 0.0], @@ -123,9 +113,7 @@ def test_cosine_of_solar_zenith_angle(): np.testing.assert_allclose(czda[7:12, :], exp_czda, rtol=1e-3) # Same code as above, but with function "cosza". - cza = helpers.cosine_of_solar_zenith_angle( - time, dec, lat, lon, stat="average", sunlit=False - ) + cza = helpers.cosine_of_solar_zenith_angle(time, dec, lat, lon, stat="average", sunlit=False) exp_cza = np.array( [ [-0.83153798, -0.90358335, -0.34065474], @@ -141,16 +129,12 @@ def _test_function(da, op, dim): return getattr(da, op)(dim) -@pytest.mark.parametrize( - ["in_chunks", "exp_chunks"], [(60, 6 * (2,)), (30, 12 * (1,)), (-1, (12,))] -) +@pytest.mark.parametrize(["in_chunks", "exp_chunks"], [(60, 6 * (2,)), (30, 12 * (1,)), (-1, (12,))]) def test_resample_map(tas_series, in_chunks, exp_chunks): pytest.importorskip("flox") tas = tas_series(365 * [1]).chunk(time=in_chunks) with assert_lazy: - out = helpers.resample_map( - tas, "time", "MS", lambda da: da.mean("time"), map_blocks=True - ) + out = helpers.resample_map(tas, "time", "MS", lambda da: da.mean("time"), map_blocks=True) assert out.chunks[0] == exp_chunks out.load() # Trigger compute to see if it actually works @@ -182,9 +166,7 @@ def test_resample_map_passthrough(tas_series): @pytest.mark.parametrize("calendar", [None, "standard"]) def test_make_hourly_temperature(tasmax_series, tasmin_series, calendar): tasmax = tasmax_series(np.array([20]), units="degC", calendar=calendar) - tasmin = tasmin_series(np.array([0]), units="degC", calendar=calendar).expand_dims( - lat=[0] - ) + tasmin = tasmin_series(np.array([0]), units="degC", calendar=calendar).expand_dims(lat=[0]) tasmin.lat.attrs["units"] = "degree_north" tas_hourly = helpers.make_hourly_temperature(tasmax, tasmin) diff --git a/tests/test_indicators.py b/tests/test_indicators.py index 7af88b5b1..d5bab40a5 100644 --- a/tests/test_indicators.py +++ b/tests/test_indicators.py @@ -152,10 +152,7 @@ def test_attrs(tas_series): txm = uniIndTemp(a, thresh="5 degC", freq="YS") assert txm.cell_methods == "time: mean time: mean within years" assert f"{dt.datetime.now():%Y-%m-%d %H}" in txm.attrs["history"] - assert ( - "TMIN(da=tas, thresh='5 degC', freq='YS') with options check_missing=any" - in txm.attrs["history"] - ) + assert "TMIN(da=tas, thresh='5 degC', freq='YS') with options check_missing=any" in txm.attrs["history"] assert f"xclim version: {__version__}" in txm.attrs["history"] assert txm.name == "tmin5 degC" assert uniIndTemp.standard_name == "{freq} mean temperature" @@ -169,10 +166,7 @@ def test_attrs(tas_series): name="TT", ) txm = uniIndTemp(a, thresh=thresh, freq="YS") - assert ( - "TMIN(da=tas, thresh=TT, freq='YS') with options check_missing=any" - in txm.attrs["history"] - ) + assert "TMIN(da=tas, thresh=TT, freq='YS') with options check_missing=any" in txm.attrs["history"] assert txm.attrs["long_name"].endswith("with threshold.") @@ -266,9 +260,7 @@ def test_module(): assert atmos.tg_mean.__module__.split(".")[2] == "atmos" # Virtual module also are stored under xclim.indicators assert xclim.indicators.cf.fg.__module__ == "xclim.indicators.cf" # noqa: F821 - assert ( - xclim.indicators.icclim.GD4.__module__ == "xclim.indicators.icclim" - ) # noqa: F821 + assert xclim.indicators.icclim.GD4.__module__ == "xclim.indicators.icclim" # noqa: F821 def test_temp_unit_conversion(tas_series): @@ -292,9 +284,7 @@ def test_temp_diff_unit_conversion(tasmax_series, tasmin_series): txC = convert_units_to(tx, "degC") tnC = convert_units_to(tn, "degC") - ind = xclim.atmos.daily_temperature_range.from_dict( - {"units": "degC"}, "dtr_degC", "test" - ) + ind = xclim.atmos.daily_temperature_range.from_dict({"units": "degC"}, "dtr_degC", "test") out = ind(tasmax=txC, tasmin=tnC) assert out.attrs["units"] == "degC" assert out.attrs["units_metadata"] == "temperature: difference" @@ -410,9 +400,7 @@ def test_missing(tas_series): m = uniIndTemp(a, freq="MS") assert m[0].isnull() - with xclim.set_options( - check_missing="pct", missing_options={"pct": {"tolerance": 0.05}} - ): + with xclim.set_options(check_missing="pct", missing_options={"pct": {"tolerance": 0.05}}): m = uniIndTemp(a, freq="MS") assert not m[0].isnull() assert "check_missing=pct, missing_options={'tolerance': 0.05}" in m.history @@ -482,9 +470,7 @@ def test_all_jsonable(official_indicators): problems.append(identifier) err = e if problems: - raise ValueError( - f"Indicators {problems} provide problematic json serialization.: {err}" - ) + raise ValueError(f"Indicators {problems} provide problematic json serialization.: {err}") def test_all_parameters_understood(official_indicators): @@ -502,9 +488,7 @@ def test_all_parameters_understood(official_indicators): ("GROWING_SEASON_END", "op"), ("GROWING_SEASON_START", "op"), }: - raise ValueError( - f"The following indicator/parameter couple {problems} use types not listed in InputKind." - ) + raise ValueError(f"The following indicator/parameter couple {problems} use types not listed in InputKind.") def test_signature(): @@ -531,17 +515,12 @@ def test_doc(): doc = xclim.atmos.cffwis_indices.__doc__ assert doc.startswith("Canadian Fire Weather Index System indices. (realm: atmos)") assert "This indicator will check for missing values according to the method" in doc - assert ( - "Based on indice :py:func:`~xclim.indices.fire._cffwis.cffwis_indices`." in doc - ) + assert "Based on indice :py:func:`~xclim.indices.fire._cffwis.cffwis_indices`." in doc assert "ffmc0 : str or DataArray, optional" in doc assert "Returns\n-------" in doc assert "See :cite:t:`code-natural_resources_canada_data_nodate`, " in doc assert "the :py:mod:`xclim.indices.fire` module documentation," in doc - assert ( - "and the docstring of :py:func:`fire_weather_ufunc` for more information." - in doc - ) + assert "and the docstring of :py:func:`fire_weather_ufunc` for more information." in doc def test_delayed(tasmax_series): @@ -558,30 +537,17 @@ def test_identifier(): def test_formatting(pr_series): out = atmos.wetdays(pr_series(np.arange(366)), thresh=1.0 * units.mm / units.day) # pint 0.10 now pretty print day as d. - assert ( - out.attrs["long_name"] - == "Number of days with daily precipitation at or above 1 mm d-1" - ) - assert out.attrs["description"] in [ - "Annual number of days with daily precipitation at or above 1 mm d-1." - ] + assert out.attrs["long_name"] == "Number of days with daily precipitation at or above 1 mm d-1" + assert out.attrs["description"] in ["Annual number of days with daily precipitation at or above 1 mm d-1."] out = atmos.wetdays(pr_series(np.arange(366)), thresh=1.5 * units.mm / units.day) - assert ( - out.attrs["long_name"] - == "Number of days with daily precipitation at or above 1.5 mm d-1" - ) - assert out.attrs["description"] in [ - "Annual number of days with daily precipitation at or above 1.5 mm d-1." - ] + assert out.attrs["long_name"] == "Number of days with daily precipitation at or above 1.5 mm d-1" + assert out.attrs["description"] in ["Annual number of days with daily precipitation at or above 1.5 mm d-1."] def test_parse_doc(): doc = parse_doc(tg_mean.__doc__) assert doc["title"] == "Mean of daily average temperature." - assert ( - doc["abstract"] - == "Resample the original daily mean temperature series by taking the mean over each period." - ) + assert doc["abstract"] == "Resample the original daily mean temperature series by taking the mean over each period." assert doc["parameters"]["tas"]["description"] == "Mean daily temperature." assert doc["parameters"]["freq"]["description"] == "Resampling frequency." assert doc["notes"].startswith("Let") @@ -644,9 +610,7 @@ def test_merge_attributes(missing_str, new_line): b = xr.DataArray([0], attrs={}) c = xr.Dataset(attrs={"text": "Text3"}) - merged = merge_attributes( - "text", a, missing_str=missing_str, new_line=new_line, b=b, c=c - ) + merged = merge_attributes("text", a, missing_str=missing_str, new_line=new_line, b=b, c=c) assert merged.startswith("a: Text1") @@ -849,28 +813,18 @@ def test_resampling_indicator_with_indexing(tas_series): out = xclim.atmos.tx_days_above(tas, thresh="0 degC", freq="YS", month=2) np.testing.assert_allclose(out, [28, 29]) - out = xclim.atmos.tx_days_above( - tas, thresh="0 degC", freq="YS-JUL", doy_bounds=(1, 50) - ) + out = xclim.atmos.tx_days_above(tas, thresh="0 degC", freq="YS-JUL", doy_bounds=(1, 50)) np.testing.assert_allclose(out, [50, 50, np.nan]) - out = xclim.atmos.tx_days_above( - tas, thresh="0 degC", freq="YS", date_bounds=("02-29", "04-01") - ) + out = xclim.atmos.tx_days_above(tas, thresh="0 degC", freq="YS", date_bounds=("02-29", "04-01")) np.testing.assert_allclose(out, [32, 33]) def test_indicator_indexing_doy_bounds_spatial(tasmin_series): - da = tasmin_series(np.ones(730), start="2005-01-01", units="°C").expand_dims( - lat=[0, 10, 15, 20, 25] - ) + da = tasmin_series(np.ones(730), start="2005-01-01", units="°C").expand_dims(lat=[0, 10, 15, 20, 25]) - start = xr.DataArray( - [50, 340, 100, np.nan, np.nan], dims=("lat",), coords={"lat": da.lat} - ) - end = xr.DataArray( - [200, 20, np.nan, 200, np.nan], dims=("lat",), coords={"lat": da.lat} - ) + start = xr.DataArray([50, 340, 100, np.nan, np.nan], dims=("lat",), coords={"lat": da.lat}) + end = xr.DataArray([200, 20, np.nan, 200, np.nan], dims=("lat",), coords={"lat": da.lat}) out = atmos.tn_days_above(da, thresh="0 °C", doy_bounds=(start, end)) np.testing.assert_array_equal( @@ -883,12 +837,8 @@ def test_indicator_indexing_doy_bounds_temporal(tasmin_series): da = tasmin_series(np.ones(365 * 5 + 1), start="2005-01-01", units="°C") time = xr.date_range("2005-01-01", freq="YS", periods=5) - start = xr.DataArray( - [50, 340, 100, np.nan, np.nan], dims=("time",), coords={"time": time} - ) - end = xr.DataArray( - [200, 20, np.nan, 200, np.nan], dims=("time",), coords={"time": time} - ) + start = xr.DataArray([50, 340, 100, np.nan, np.nan], dims=("time",), coords={"time": time}) + end = xr.DataArray([200, 20, np.nan, 200, np.nan], dims=("time",), coords={"time": time}) out = atmos.tn_days_above(da, thresh="0 °C", doy_bounds=(start, end)) # 340, 20 is an invalid indexer for freq YS. diff --git a/tests/test_indices.py b/tests/test_indices.py index 32c61b501..98d61008b 100644 --- a/tests/test_indices.py +++ b/tests/test_indices.py @@ -105,11 +105,7 @@ class TestColdSpellDurationIndex: def test_simple(self, tasmin_series, random): i = 3650 A = 10.0 - tn = ( - np.zeros(i) - + A * np.sin(np.arange(i) / 365.0 * 2 * np.pi) - + 0.1 * random.random(i) - ) + tn = np.zeros(i) + A * np.sin(np.arange(i) / 365.0 * 2 * np.pi) + 0.1 * random.random(i) tn[10:20] -= 2 tn = tasmin_series(tn) tn10 = percentile_doy(tn, per=10).sel(percentiles=10) @@ -254,9 +250,7 @@ def test_corn_heat_units(self, tasmin_series, tasmax_series): tn = tasmin_series(np.array([-10, 5, 4, 3, 10]) + K2C) tx = tasmax_series(np.array([-5, 9, 10, 16, 20]) + K2C) - out = xci.corn_heat_units( - tn, tx, thresh_tasmin="4.44 degC", thresh_tasmax="10 degC" - ) + out = xci.corn_heat_units(tn, tx, thresh_tasmin="4.44 degC", thresh_tasmax="10 degC") np.testing.assert_allclose(out, [0, 0.504, 0, 8.478, 17.454]) @pytest.mark.parametrize( @@ -268,9 +262,7 @@ def test_corn_heat_units(self, tasmin_series, tasmax_series): ], ) def test_bedd(self, method, end_date, deg_days, max_deg_days): - time_data = xr.date_range( - "1992-01-01", "1995-06-01", freq="D", calendar="standard" - ) + time_data = xr.date_range("1992-01-01", "1995-06-01", freq="D", calendar="standard") tn = xr.DataArray( np.zeros(time_data.size) + 10 + K2C, dims="time", @@ -292,9 +284,7 @@ def test_bedd(self, method, end_date, deg_days, max_deg_days): attrs=dict(units="K"), ) - lat = xr.DataArray( - [35, 45], dims=("lat",), name="lat", attrs={"units": "degrees_north"} - ) + lat = xr.DataArray([35, 45], dims=("lat",), name="lat", attrs={"units": "degrees_north"}) high_lat = xr.DataArray(48, name="lat", attrs={"units": "degrees_north"}) bedd = xci.biologically_effective_degree_days( @@ -326,23 +316,13 @@ def test_bedd(self, method, end_date, deg_days, max_deg_days): ) if method == "jones": - np.testing.assert_array_less( - bedd[1], bedd[0] - ) # Leap-year has slightly higher values - np.testing.assert_allclose( - bedd[:3], np.array([deg_days, deg_days, deg_days]), rtol=6e-4 - ) - np.testing.assert_allclose( - bedd_hot[:3], [max_deg_days, max_deg_days, max_deg_days], rtol=0.15 - ) + np.testing.assert_array_less(bedd[1], bedd[0]) # Leap-year has slightly higher values + np.testing.assert_allclose(bedd[:3], np.array([deg_days, deg_days, deg_days]), rtol=6e-4) + np.testing.assert_allclose(bedd_hot[:3], [max_deg_days, max_deg_days, max_deg_days], rtol=0.15) else: - np.testing.assert_allclose( - bedd[:3], np.array([deg_days, deg_days, deg_days]) - ) - np.testing.assert_array_equal( - bedd_hot[:3], [max_deg_days, max_deg_days, max_deg_days] - ) + np.testing.assert_allclose(bedd[:3], np.array([deg_days, deg_days, deg_days])) + np.testing.assert_array_equal(bedd_hot[:3], [max_deg_days, max_deg_days, max_deg_days]) if method == "gladstones": np.testing.assert_array_less(bedd, bedd_high_lat) if method == "icclim": @@ -362,11 +342,7 @@ def test_chill_units(self, tas_series): tas = tas_series( np.array( - num_cu_0 * [1.1] - + num_cu_05 * [2.0] - + num_cu_1 * [5.6] - + num_cu_min_05 * [16.0] - + num_cu_min_1 * [20.0] + num_cu_0 * [1.1] + num_cu_05 * [2.0] + num_cu_1 * [5.6] + num_cu_min_05 * [16.0] + num_cu_min_1 * [20.0] ) + K2C, freq="h", @@ -415,9 +391,7 @@ def test_lat_temperature_index(self, open_dataset, lat_factor, values): lti = xci.latitude_temperature_index(tas=ds.tas, lat_factor=lat_factor) assert lti.where(abs(lti.lat) > lat_factor).sum() == 0 - lti = lti.where(abs(lti.lat) <= lat_factor, drop=True).where( - lti.lon <= 35, drop=True - ) + lti = lti.where(abs(lti.lat) <= lat_factor, drop=True).where(lti.lon <= 35, drop=True) lti = lti.groupby_bins(lti.lon, 1).mean().groupby_bins(lti.lat, 5).mean() np.testing.assert_array_almost_equal(lti[0].squeeze(), np.array(values), 2) @@ -464,17 +438,13 @@ def test_qian_weighted_mean_average(self, tas_series): mg = tas_series(mg + K2C) out = xci.qian_weighted_mean_average(mg, dim="time") - np.testing.assert_array_equal( - out[7:12], [273.15, 273.2125, 273.525, 274.3375, 275.775] - ) + np.testing.assert_array_equal(out[7:12], [273.15, 273.2125, 273.525, 274.3375, 275.775]) assert out[50].values < (10 + K2C) assert out[51].values > K2C assert out.attrs["units"] == "K" @pytest.mark.parametrize("method,expected", [("bootsma", 2267), ("qian", 2252.0)]) - def test_effective_growing_degree_days( - self, tasmax_series, tasmin_series, method, expected - ): + def test_effective_growing_degree_days(self, tasmax_series, tasmin_series, method, expected): mg = np.zeros(547) # False start @@ -490,9 +460,7 @@ def test_effective_growing_degree_days( mx = tasmax_series(mg + K2C + 10) mn = tasmin_series(mg + K2C - 10) - out = xci.effective_growing_degree_days( - tasmax=mx, tasmin=mn, method=method, freq="YS" - ) + out = xci.effective_growing_degree_days(tasmax=mx, tasmin=mn, method=method, freq="YS") np.testing.assert_array_equal(out, np.array([np.nan, expected])) @@ -696,14 +664,8 @@ class TestStandardizedIndices: ), ], ) - def test_standardized_precipitation_index( - self, open_dataset, freq, window, dist, method, values, diff_tol - ): - if ( - method == "ML" - and freq == "D" - and Version(__numpy_version__) < Version("2.0.0") - ): + def test_standardized_precipitation_index(self, open_dataset, freq, window, dist, method, values, diff_tol): + if method == "ML" and freq == "D" and Version(__numpy_version__) < Version("2.0.0"): pytest.skip("Skipping SPI/ML/D on older numpy") # change `dist` to a lmoments3 object if needed @@ -718,9 +680,7 @@ def test_standardized_precipitation_index( ds = ds.convert_calendar("366_day", missing=np.nan) elif freq == "W": # only standard calendar supported with freq="W" - ds = ds.convert_calendar( - "standard", missing=np.nan, align_on="year", use_cftime=False - ) + ds = ds.convert_calendar("standard", missing=np.nan, align_on="year", use_cftime=False) pr = ds.pr.sel(time=slice("1998", "2000")) pr_cal = ds.pr.sel(time=slice("1950", "1980")) fitkwargs = {} @@ -844,18 +804,10 @@ def test_str_vs_rv_continuous(self, open_dataset, dist): def test_standardized_precipitation_evapotranspiration_index( self, open_dataset, freq, window, dist, method, values, diff_tol ): - if ( - method == "ML" - and freq == "D" - and Version(__numpy_version__) < Version("2.0.0") - ): + if method == "ML" and freq == "D" and Version(__numpy_version__) < Version("2.0.0"): pytest.skip("Skipping SPI/ML/D on older numpy") - ds = ( - open_dataset("sdba/CanESM2_1950-2100.nc") - .isel(location=1) - .sel(time=slice("1950", "2000")) - ) + ds = open_dataset("sdba/CanESM2_1950-2100.nc").isel(location=1).sel(time=slice("1950", "2000")) pr = ds.pr # generate water budget with xr.set_options(keep_attrs=True): @@ -899,11 +851,7 @@ def test_standardized_precipitation_evapotranspiration_index( ) def test_standardized_index_modularity(self, open_dataset, tmp_path, indexer): freq, window, dist, method = "MS", 6, "gamma", "APP" - ds = ( - open_dataset("sdba/CanESM2_1950-2100.nc") - .isel(location=1) - .sel(time=slice("1950", "2000")) - ) + ds = open_dataset("sdba/CanESM2_1950-2100.nc").isel(location=1).sel(time=slice("1950", "2000")) pr = ds.pr # generate water budget with xr.set_options(keep_attrs=True): @@ -947,12 +895,10 @@ def test_standardized_index_modularity(self, open_dataset, tmp_path, indexer): **indexer, ).sel(time=slice("1998", "2000")) - # In the previous computation, the first {window-1} values are NaN because the rolling is performed on the period [1998,2000]. - # Here, the computation is performed on the period [1950,2000], *then* subsetted to [1998,2000], so it doesn't have NaNs - # for the first values - nan_window = xr.cftime_range( - spei1.time.values[0], periods=window - 1, freq=freq - ) + # In the previous computation, the first {window-1} values are NaN because the rolling is performed + # on the period [1998,2000]. Here, the computation is performed on the period [1950,2000], + # *then* subsetted to [1998,2000], so it doesn't have NaNs for the first values + nan_window = xr.cftime_range(spei1.time.values[0], periods=window - 1, freq=freq) spei2.loc[{"time": spei2.time.isin(nan_window)}] = ( np.nan ) # select time based on the window is necessary when `drop=True` @@ -961,11 +907,7 @@ def test_standardized_index_modularity(self, open_dataset, tmp_path, indexer): def test_zero_inflated(self, open_dataset): # This tests that the zero_inflated option makes a difference with zero inflated data - pr = ( - open_dataset("sdba/CanESM2_1950-2100.nc") - .isel(location=1) - .sel(time=slice("1950", "1980")) - ).pr + pr = (open_dataset("sdba/CanESM2_1950-2100.nc").isel(location=1).sel(time=slice("1950", "1980"))).pr # july 1st (doy=180) with 10 years with zero precipitation pr[{"time": slice(179, 365 * 11, 365)}] = 0 spid = {} @@ -984,19 +926,11 @@ def test_zero_inflated(self, open_dataset): pr, params=params, cal_start=None, cal_end=None, **input_params ) # drop doys other than 180 that will be NaNs - spid[zero_inflated] = spid[zero_inflated].where( - spid[zero_inflated].notnull(), drop=True - ) - np.testing.assert_equal( - np.all(np.not_equal(spid[False].values, spid[True].values)), True - ) + spid[zero_inflated] = spid[zero_inflated].where(spid[zero_inflated].notnull(), drop=True) + np.testing.assert_equal(np.all(np.not_equal(spid[False].values, spid[True].values)), True) def test_PWM_and_fitkwargs(self, open_dataset): - pr = ( - open_dataset("sdba/CanESM2_1950-2100.nc") - .isel(location=1) - .sel(time=slice("1950", "1980")) - ).pr + pr = (open_dataset("sdba/CanESM2_1950-2100.nc").isel(location=1).sel(time=slice("1950", "1980"))).pr lmom = pytest.importorskip("lmoments3.distr") # for now, only one function used @@ -1011,12 +945,8 @@ def test_PWM_and_fitkwargs(self, open_dataset): fitkwargs=fitkwargs, ) # this should not cause a problem - params_d0 = xci.stats.standardized_index_fit_params(pr, **input_params).isel( - dayofyear=0 - ) - np.testing.assert_allclose( - params_d0, np.array([5.63e-01, 0, 3.37e-05]), rtol=0, atol=2e-2 - ) + params_d0 = xci.stats.standardized_index_fit_params(pr, **input_params).isel(dayofyear=0) + np.testing.assert_allclose(params_d0, np.array([5.63e-01, 0, 3.37e-05]), rtol=0, atol=2e-2) # this should cause a problem fitkwargs["fscale"] = 1 input_params["fitkwargs"] = fitkwargs @@ -1049,9 +979,7 @@ def test_simple(self, tasmin_series, tasmax_series, thresholds): mn = tasmin_series(mn + K2C) mx = tasmax_series(mx + K2C) - out = xci.multiday_temperature_swing( - mn, mx, **thresholds, op="sum", window=1, freq="ME" - ) + out = xci.multiday_temperature_swing(mn, mx, **thresholds, op="sum", window=1, freq="ME") np.testing.assert_array_equal(out[:2], [5, 1]) np.testing.assert_array_equal(out[2:], 0) @@ -1206,9 +1134,7 @@ def test_simple(self, pr_series, per_doy): np.testing.assert_array_almost_equal(out[0], 4) out = xci.fraction_over_precip_thresh(pr, per, thresh="2 kg/m**2/s") - np.testing.assert_array_almost_equal( - out[0], (3 + 4 + 6 + 7) / (3 + 4 + 5 + 6 + 7) - ) + np.testing.assert_array_almost_equal(out[0], (3 + 4 + 6 + 7) / (3 + 4 + 5 + 6 + 7)) def test_quantile(self, pr_series): a = np.zeros(365) @@ -1221,9 +1147,7 @@ def test_quantile(self, pr_series): per.attrs["units"] = "kg m-2 s-1" # This won't be needed with xarray 0.13 out = xci.days_over_precip_thresh(pr, per, thresh="2 kg/m**2/s") - np.testing.assert_array_almost_equal( - out[0], 2 - ) # Only days 6 and 7 meet criteria. + np.testing.assert_array_almost_equal(out[0], 2) # Only days 6 and 7 meet criteria. def test_nd(self, pr_ndseries): pr = pr_ndseries(np.ones((300, 2, 3))) @@ -1573,7 +1497,6 @@ def test_1d( class TestHolidayIndices: - def test_xmas_days_simple(self, snd_series): # 5ish years of data, starting from 2000-07-01 snd = snd_series(np.zeros(365 * 5), units="cm") @@ -1632,9 +1555,7 @@ def test_perfect_xmas_days(self, snd_series, prsn_series): out1 = xci.holiday_snow_and_snowfall_days(snd, prsn) np.testing.assert_array_equal(out1, [1, 0, 0, 0, 1]) - out2 = xci.holiday_snow_and_snowfall_days( - snd, prsn, snd_thresh="15 mm", prsn_thresh="0.5 mm" - ) + out2 = xci.holiday_snow_and_snowfall_days(snd, prsn, snd_thresh="15 mm", prsn_thresh="0.5 mm") np.testing.assert_array_equal(out2, [1, 1, 1, 0, 1]) out3 = xci.holiday_snow_and_snowfall_days( @@ -1678,9 +1599,7 @@ def test_resampling_order(self, tasmax_series, resample_before_rl, expected): a[5:35] = 31 tx = tasmax_series(a + K2C).chunk() - hsf = xci.hot_spell_frequency( - tx, resample_before_rl=resample_before_rl, freq="MS" - ).load() + hsf = xci.hot_spell_frequency(tx, resample_before_rl=resample_before_rl, freq="MS").load() assert hsf[1] == expected @pytest.mark.parametrize("resample_map", [True, False]) @@ -1980,9 +1899,7 @@ def test_resampling_order(self, pr_series, resample_before_rl, expected): a = np.zeros(365) + 10 a[5:35] = 0 pr = pr_series(a).chunk() - out = xci.maximum_consecutive_dry_days( - pr, freq="ME", resample_before_rl=resample_before_rl - ).load() + out = xci.maximum_consecutive_dry_days(pr, freq="ME", resample_before_rl=resample_before_rl).load() assert out[0] == expected @@ -1999,9 +1916,7 @@ def test_simple(self, tasmax_series): 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"} - ) + 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 @@ -2023,9 +1938,7 @@ def test_simple(self, pr_series): 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) def test_mixed_phases(self, pr_series, tas_series): @@ -2039,9 +1952,7 @@ def test_mixed_phases(self, pr_series, tas_series): tas = tas_series(tas) outsn = xci.precip_accumulation(pr, tas=tas, phase="solid", freq="ME") - outsn2 = xci.precip_accumulation( - pr, tas=tas, phase="solid", thresh="269 K", freq="ME" - ) + outsn2 = xci.precip_accumulation(pr, tas=tas, phase="solid", thresh="269 K", freq="ME") outrn = xci.precip_accumulation(pr, tas=tas, phase="liquid", freq="ME") np.testing.assert_array_equal(outsn[0], 10 * 3600 * 24) @@ -2052,9 +1963,7 @@ def test_mixed_phases(self, pr_series, tas_series): class TestPrecipAverage: # 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"} - ) + da_std = xr.DataArray(time_std.year, coords=[time_std], dims="time", attrs={"units": "mm d-1"}) # "365_day" and "360_day" calendars not tested for now since xarray.resample # does not support calendars other than "standard" and "*gregorian" @@ -2090,9 +1999,7 @@ def test_mixed_phases(self, pr_series, tas_series): tas = tas_series(tas) outsn = xci.precip_average(pr, tas=tas, phase="solid", freq="ME") - outsn2 = xci.precip_average( - pr, tas=tas, phase="solid", thresh="269 K", freq="ME" - ) + outsn2 = xci.precip_average(pr, tas=tas, phase="solid", thresh="269 K", freq="ME") outrn = xci.precip_average(pr, tas=tas, phase="liquid", freq="ME") np.testing.assert_array_equal(outsn[0], 10 * 3600 * 24 / 31) @@ -2238,9 +2145,7 @@ def test_tn90p_simple(self, tasmin_series): class TestTas: @pytest.mark.parametrize("tasmin_units", ["K", "°C"]) @pytest.mark.parametrize("tasmax_units", ["K", "°C"]) - def test_tas( - self, tasmin_series, tasmax_series, tas_series, tasmin_units, tasmax_units - ): + def test_tas(self, tasmin_series, tasmax_series, tas_series, tasmin_units, tasmax_units): tas = tas_series(np.ones(10) + (K2C if tasmin_units == "K" else 0)) tas.attrs["units"] = tasmin_units tasmin = tasmin_series(np.zeros(10) + (K2C if tasmin_units == "K" else 0)) @@ -2319,9 +2224,7 @@ def static_tmin_tmax_setup(tasmin_series, tasmax_series): (np.std, 2.72913233), ], ) - def test_static_reduce_daily_temperature_range( - self, tasmin_series, tasmax_series, op, expected - ): + def test_static_reduce_daily_temperature_range(self, tasmin_series, tasmax_series, op, expected): tasmin, tasmax = self.static_tmin_tmax_setup(tasmin_series, tasmax_series) dtr = xci.daily_temperature_range(tasmin, tasmax, freq="YS", op=op) assert dtr.units == "K" @@ -2350,9 +2253,7 @@ def test_static_daily_temperature_range(self, tasmin_series, tasmax_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, tasmin_series, tasmax_series - ): + def test_static_variable_daily_temperature_range(self, tasmin_series, tasmax_series): tasmin, tasmax = self.static_tmin_tmax_setup(tasmin_series, tasmax_series) dtr = xci.daily_temperature_range_variability(tasmin, tasmax, freq="YS") @@ -2412,7 +2313,6 @@ def test_static_freeze_thaw_cycles(self, tasmin_series, tasmax_series): class TestTemperatureSeasonality: - def test_simple(self, tas_series): a = np.zeros(365) a = tas_series(a + K2C, start="1971-01-01") @@ -2523,16 +2423,12 @@ def get_data(tas_series, pr_series, random): annual_cycle = np.sin(2 * np.pi * (times.dayofyear.values / 365.25 - 0.28)) base = 10 + 15 * annual_cycle.reshape(-1, 1) values = base + 3 * random.standard_normal((annual_cycle.size, 1)) + K2C - tas = tas_series(values.squeeze(), start="2001-01-01").sel( - time=slice("2001", "2002") - ) + tas = tas_series(values.squeeze(), start="2001-01-01").sel(time=slice("2001", "2002")) base = 15 * annual_cycle.reshape(-1, 1) values = base + 10 + 10 * random.standard_normal((annual_cycle.size, 1)) values = values / 3600 / 24 values[values < 0] = 0 - pr = pr_series(values.squeeze(), start="2001-01-01").sel( - time=slice("2001", "2002") - ) + pr = pr_series(values.squeeze(), start="2001-01-01").sel(time=slice("2001", "2002")) return tas, pr @pytest.mark.parametrize( @@ -2547,9 +2443,7 @@ def get_data(tas_series, pr_series, random): ], ) @pytest.mark.parametrize("use_dask", [True, False]) - def test_tg_wetdry( - self, tas_series, pr_series, use_dask, freq, op, expected, random - ): + def test_tg_wetdry(self, tas_series, pr_series, use_dask, freq, op, expected, random): tas, pr = self.get_data(tas_series, pr_series, random) pr = pr.resample(time=freq).mean(keep_attrs=True) @@ -2579,9 +2473,7 @@ def test_tg_wetdry( ) def test_pr_warmcold(self, tas_series, pr_series, freq, op, expected, random): tas, pr = self.get_data(tas_series, pr_series, random) - pr = convert_units_to( - pr.resample(time=freq).mean(keep_attrs=True), "mm/d", context="hydro" - ) + pr = convert_units_to(pr.resample(time=freq).mean(keep_attrs=True), "mm/d", context="hydro") tas = xci.tg_mean(tas, freq=freq) @@ -2593,9 +2485,7 @@ class TestTempWarmestColdestQuarter: @staticmethod def get_data(tas_series, units="K"): a = np.zeros(365 * 2) - a = tas_series( - a + (K2C if units == "K" else 0), start="1971-01-01", units=units - ) + a = tas_series(a + (K2C if units == "K" else 0), start="1971-01-01", units=units) a[(a.time.dt.season == "JJA") & (a.time.dt.year == 1971)] += 22 a[(a.time.dt.season == "SON") & (a.time.dt.year == 1972)] += 25 return a @@ -2622,9 +2512,7 @@ def test_simple(self, tas_series): def test_celsius(self, tas_series): a = self.get_data(tas_series, units="°C") - a[ - (a.time.dt.month >= 1) & (a.time.dt.month <= 3) & (a.time.dt.year == 1971) - ] += -15 + a[(a.time.dt.month >= 1) & (a.time.dt.month <= 3) & (a.time.dt.year == 1971)] += -15 a[(a.time.dt.season == "MAM") & (a.time.dt.year == 1972)] += -10 out = xci.tg_mean_warmcold_quarter(a, op="warmest") @@ -2703,13 +2591,9 @@ def test_simple(self, tasmax_series, tasmin_series, freq, expected, random): annual_cycle = np.sin(2 * np.pi * (times.dayofyear.values / 365.25 - 0.28)) base = 10 + 15 * annual_cycle.reshape(-1, 1) values = base + 3 * random.standard_normal((annual_cycle.size, 1)) + K2C - tasmin = tasmin_series(values.squeeze(), start="2001-01-01").sel( - time=slice("2001", "2002") - ) + tasmin = tasmin_series(values.squeeze(), start="2001-01-01").sel(time=slice("2001", "2002")) values = base + 10 + 3 * random.standard_normal((annual_cycle.size, 1)) + K2C - tasmax = tasmax_series(values.squeeze(), start="2001-01-01").sel( - time=slice("2001", "2002") - ) + tasmax = tasmax_series(values.squeeze(), start="2001-01-01").sel(time=slice("2001", "2002")) # weekly tmin = tasmin.resample(time=freq).mean(dim="time", keep_attrs=True) @@ -2799,11 +2683,7 @@ class TestWarmSpellDurationIndex: def test_simple(self, tasmax_series, random): i = 3650 A = 10.0 - tx = ( - np.zeros(i) - + A * np.sin(np.arange(i) / 365.0 * 2 * np.pi) - + 0.1 * random.random(i) - ) + tx = np.zeros(i) + A * np.sin(np.arange(i) / 365.0 * 2 * np.pi) + 0.1 * random.random(i) tx[10:20] += 2 tx = tasmax_series(tx) tx90 = percentile_doy(tx, per=90).sel(percentiles=90) @@ -2879,14 +2759,8 @@ class TestWindConversion: def test_uas_vas_2_sfcwind(self): wind, windfromdir = xci.uas_vas_2_sfcwind(self.da_uas, self.da_vas) - assert np.all( - np.around(wind.values, decimals=10) - == np.around(self.da_wind.values / 3.6, decimals=10) - ) - assert np.all( - np.around(windfromdir.values, decimals=10) - == np.around(self.da_windfromdir.values, decimals=10) - ) + assert np.all(np.around(wind.values, decimals=10) == np.around(self.da_wind.values / 3.6, decimals=10)) + assert np.all(np.around(windfromdir.values, decimals=10) == np.around(self.da_windfromdir.values, decimals=10)) def test_sfcwind_2_uas_vas(self): uas, vas = xci.sfcwind_2_uas_vas(self.da_wind, self.da_windfromdir) @@ -2898,15 +2772,9 @@ def test_sfcwind_2_uas_vas(self): ) -@pytest.mark.parametrize( - "method", ["bohren98", "tetens30", "sonntag90", "goffgratch46", "wmo08"] -) -@pytest.mark.parametrize( - "invalid_values,exp0", [("clip", 100), ("mask", np.nan), (None, 151)] -) -def test_relative_humidity_dewpoint( - tas_series, hurs_series, method, invalid_values, exp0 -): +@pytest.mark.parametrize("method", ["bohren98", "tetens30", "sonntag90", "goffgratch46", "wmo08"]) +@pytest.mark.parametrize("invalid_values,exp0", [("clip", 100), ("mask", np.nan), (None, 151)]) +def test_relative_humidity_dewpoint(tas_series, hurs_series, method, invalid_values, exp0): np.testing.assert_allclose( xci.relative_humidity( tas=tas_series(np.array([-20, -10, -1, 10, 20, 25, 30, 40, 60]) + K2C), @@ -2934,12 +2802,8 @@ def test_specific_humidity_from_dewpoint(tas_series, ps_series): np.testing.assert_allclose(q, 0.012, 3) -@pytest.mark.parametrize( - "method", ["tetens30", "sonntag90", "goffgratch46", "wmo08", "its90"] -) -@pytest.mark.parametrize( - "ice_thresh,exp0", [(None, [125, 286, 568]), ("0 degC", [103, 260, 563])] -) +@pytest.mark.parametrize("method", ["tetens30", "sonntag90", "goffgratch46", "wmo08", "its90"]) +@pytest.mark.parametrize("ice_thresh,exp0", [(None, [125, 286, 568]), ("0 degC", [103, 260, 563])]) @pytest.mark.parametrize("temp_units", ["degC", "degK"]) def test_saturation_vapor_pressure(tas_series, method, ice_thresh, exp0, temp_units): tas = tas_series(np.array([-20, -10, -1, 10, 20, 25, 30, 40, 60]) + K2C) @@ -2956,9 +2820,7 @@ def test_saturation_vapor_pressure(tas_series, method, ice_thresh, exp0, temp_un np.testing.assert_allclose(e_sat, e_sat_exp, atol=0.5, rtol=0.005) -@pytest.mark.parametrize( - "method", ["tetens30", "sonntag90", "goffgratch46", "wmo08", "its90"] -) +@pytest.mark.parametrize("method", ["tetens30", "sonntag90", "goffgratch46", "wmo08", "its90"]) def test_vapor_pressure_deficit(tas_series, hurs_series, method): tas = tas_series(np.array([-1, 10, 20, 25, 30, 40, 60]) + K2C) hurs = hurs_series(np.array([0, 0.5, 0.8, 0.9, 0.95, 0.99, 1])) @@ -2975,12 +2837,8 @@ def test_vapor_pressure_deficit(tas_series, hurs_series, method): @pytest.mark.parametrize("method", ["tetens30", "sonntag90", "goffgratch46", "wmo08"]) -@pytest.mark.parametrize( - "invalid_values,exp0", [("clip", 100), ("mask", np.nan), (None, 188)] -) -def test_relative_humidity( - tas_series, hurs_series, huss_series, ps_series, method, invalid_values, exp0 -): +@pytest.mark.parametrize("invalid_values,exp0", [("clip", 100), ("mask", np.nan), (None, 188)]) +def test_relative_humidity(tas_series, hurs_series, huss_series, ps_series, method, invalid_values, exp0): tas = tas_series(np.array([-10, -10, 10, 20, 35, 50, 75, 95]) + K2C) # Expected values obtained with the Sonntag90 method @@ -3000,19 +2858,13 @@ def test_relative_humidity( @pytest.mark.parametrize("method", ["tetens30", "sonntag90", "goffgratch46", "wmo08"]) -@pytest.mark.parametrize( - "invalid_values,exp0", [("clip", 1.4e-2), ("mask", np.nan), (None, 2.2e-2)] -) -def test_specific_humidity( - tas_series, hurs_series, huss_series, ps_series, method, invalid_values, exp0 -): +@pytest.mark.parametrize("invalid_values,exp0", [("clip", 1.4e-2), ("mask", np.nan), (None, 2.2e-2)]) +def test_specific_humidity(tas_series, hurs_series, huss_series, ps_series, method, invalid_values, exp0): tas = tas_series(np.array([20, -10, 10, 20, 35, 50, 75, 95]) + K2C) hurs = hurs_series([150, 10, 90, 20, 80, 50, 70, 40, 30]) ps = ps_series(1000 * np.array([100] * 4 + [101] * 4)) # Expected values obtained with the Sonntag90 method - huss_exp = huss_series( - [exp0, 1.6e-4, 6.9e-3, 3.0e-3, 2.9e-2, 4.1e-2, 2.1e-1, 5.7e-1] - ) + huss_exp = huss_series([exp0, 1.6e-4, 6.9e-3, 3.0e-3, 2.9e-2, 4.1e-2, 2.1e-1, 5.7e-1]) huss = xci.specific_humidity( tas=tas, @@ -3028,19 +2880,13 @@ def test_specific_humidity( def test_degree_days_exceedance_date(tas_series): tas = tas_series(np.ones(366) + K2C, start="2000-01-01") - out = xci.degree_days_exceedance_date( - tas, thresh="0 degC", op=">", sum_thresh="150 K days" - ) + out = xci.degree_days_exceedance_date(tas, thresh="0 degC", op=">", sum_thresh="150 K days") assert out[0] == 151 - out = xci.degree_days_exceedance_date( - tas, thresh="2 degC", op="<", sum_thresh="150 degC days" - ) + out = xci.degree_days_exceedance_date(tas, thresh="2 degC", op="<", sum_thresh="150 degC days") assert out[0] == 151 - out = xci.degree_days_exceedance_date( - tas, thresh="2 degC", op="<", sum_thresh="150 K days", after_date="04-15" - ) + out = xci.degree_days_exceedance_date(tas, thresh="2 degC", op="<", sum_thresh="150 K days", after_date="04-15") assert out[0] == 256 for attr in ["units", "is_dayofyear", "calendar"]: @@ -3079,9 +2925,7 @@ def test_rain_approximation(pr_series, tas_series, method, exp): def test_first_snowfall(prsn_series, prsnd_series): # test with prsnd [mm day-1] - prsnd = prsnd_series( - (30 - abs(np.arange(366) - 180)), start="2000-01-01", units="mm day-1" - ) + prsnd = prsnd_series((30 - abs(np.arange(366) - 180)), start="2000-01-01", units="mm day-1") out = xci.first_snowfall(prsnd, thresh="15 mm/day", freq="YS") assert out[0] == 166 for attr in ["units", "is_dayofyear", "calendar"]: @@ -3099,9 +2943,7 @@ def test_first_snowfall(prsn_series, prsnd_series): assert out.attrs["is_dayofyear"] == 1 # test with prsn [kg m-2 s-1] - prsn = prsn_series( - (30 - abs(np.arange(366) - 180)), start="2000-01-01", units="mm day-1" - ) + prsn = prsn_series((30 - abs(np.arange(366) - 180)), start="2000-01-01", units="mm day-1") prsn = convert_units_to(prsn, "kg m-2 s-1", context="hydro") out = xci.first_snowfall(prsn, thresh="15 mm/day", freq="YS") assert out[0] == 166 @@ -3114,9 +2956,7 @@ def test_first_snowfall(prsn_series, prsnd_series): def test_last_snowfall(prsn_series, prsnd_series): # test with prsnd [mm day-1] - prsnd = prsnd_series( - (30 - abs(np.arange(366) - 180)), start="2000-01-01", units="mm day-1" - ) + prsnd = prsnd_series((30 - abs(np.arange(366) - 180)), start="2000-01-01", units="mm day-1") out = xci.last_snowfall(prsnd, thresh="15 mm/day", freq="YS") assert out[0] == 196 @@ -3126,9 +2966,7 @@ def test_last_snowfall(prsn_series, prsnd_series): assert out[0] == 196 # test with prsn [kg m-2 s-1] - prsn = prsn_series( - (30 - abs(np.arange(366) - 180)), start="2000-01-01", units="mm day-1" - ) + prsn = prsn_series((30 - abs(np.arange(366) - 180)), start="2000-01-01", units="mm day-1") prsn = convert_units_to(prsn, "kg m-2 s-1", context="hydro") out = xci.last_snowfall(prsn, thresh="15 mm/day", freq="YS") assert out[0] == 196 @@ -3323,9 +3161,7 @@ def test_rain_season(pr_series, result_type, method_dry_start): date_min_end="01-01", method_dry_start=method_dry_start, ) - out_arr = np.array( - [out[var].values for var in ["start", "end", "length"]] - ).flatten() + out_arr = np.array([out[var].values for var in ["start", "end", "length"]]).flatten() np.testing.assert_array_equal(out_arr, out_exp) @@ -3395,9 +3231,7 @@ def test_heat_index(tas_series, hurs_series): hurs = hurs_series([5, 5, 0, 25, 25, 50, 25, 50, 25, 50, 25, 50, 25, 50]) - expected = ( - np.array([np.nan, np.nan, 24, 25, 28, 31, 34, 41, 41, 55, 50, 73]) * units.degC - ) + expected = np.array([np.nan, np.nan, 24, 25, 28, 31, 34, 41, 41, 55, 50, 73]) * units.degC # Celsius hc = xci.heat_index(tas, hurs) @@ -3412,9 +3246,7 @@ def test_heat_index(tas_series, hurs_series): np.testing.assert_array_almost_equal(hf, expected.to("fahrenheit"), 0) -@pytest.mark.parametrize( - "op,exp", [("max", 11), ("sum", 21), ("count", 3), ("mean", 7)] -) +@pytest.mark.parametrize("op,exp", [("max", 11), ("sum", 21), ("count", 3), ("mean", 7)]) def test_freezethaw_spell(tasmin_series, tasmax_series, op, exp): tmin = np.ones(365) tmax = np.ones(365) @@ -3427,9 +3259,7 @@ def test_freezethaw_spell(tasmin_series, tasmax_series, op, exp): tasmax = tasmax_series(tmax + K2C) tasmin = tasmin_series(tmin + K2C) - out = xci.multiday_temperature_swing( - tasmin=tasmin, tasmax=tasmax, freq="YS-JUL", window=3, op=op - ) + out = xci.multiday_temperature_swing(tasmin=tasmin, tasmax=tasmax, freq="YS-JUL", window=3, op=op) np.testing.assert_array_equal(out, exp) @@ -3558,9 +3388,7 @@ def test_baier_robertson(self, tasmin_series, tasmax_series, lat_series): tx = tasmax_series(np.array([10, 15, 20]) + 273.15).expand_dims(lat=lat) out = xci.potential_evapotranspiration(tn, tx, lat=lat, method="BR65") - np.testing.assert_allclose( - out.isel(lat=0, time=2), [3.861079 / 86400], rtol=1e-2 - ) + np.testing.assert_allclose(out.isel(lat=0, time=2), [3.861079 / 86400], rtol=1e-2) def test_hargreaves(self, tasmin_series, tasmax_series, tas_series, lat_series): lat = lat_series([45]) @@ -3569,33 +3397,17 @@ def test_hargreaves(self, tasmin_series, tasmax_series, tas_series, lat_series): tm = tas_series(np.array([5, 10, 15]) + 273.15).expand_dims(lat=lat) out = xci.potential_evapotranspiration(tn, tx, tm, lat=lat, method="HG85") - np.testing.assert_allclose( - out.isel(lat=0, time=2), [4.030339 / 86400], rtol=1e-2 - ) + np.testing.assert_allclose(out.isel(lat=0, time=2), [4.030339 / 86400], rtol=1e-2) - def test_droogersallen02( - self, tasmin_series, tasmax_series, tas_series, pr_series, lat_series - ): + def test_droogersallen02(self, tasmin_series, tasmax_series, tas_series, pr_series, lat_series): lat = lat_series([45]) - tn = tasmin_series( - np.array([0, 5, 10]), start="1990-01-01", freq="MS", units="degC" - ).expand_dims(lat=lat) - tx = tasmax_series( - np.array([10, 15, 20]), start="1990-01-01", freq="MS", units="degC" - ).expand_dims(lat=lat) - tg = tas_series( - np.array([5, 10, 15]), start="1990-01-01", freq="MS", units="degC" - ).expand_dims(lat=lat) - pr = pr_series( - np.array([30, 0, 60]), start="1990-01-01", freq="MS", units="mm/month" - ).expand_dims(lat=lat) + tn = tasmin_series(np.array([0, 5, 10]), start="1990-01-01", freq="MS", units="degC").expand_dims(lat=lat) + tx = tasmax_series(np.array([10, 15, 20]), start="1990-01-01", freq="MS", units="degC").expand_dims(lat=lat) + tg = tas_series(np.array([5, 10, 15]), start="1990-01-01", freq="MS", units="degC").expand_dims(lat=lat) + pr = pr_series(np.array([30, 0, 60]), start="1990-01-01", freq="MS", units="mm/month").expand_dims(lat=lat) - out = xci.potential_evapotranspiration( - tasmin=tn, tasmax=tx, tas=tg, pr=pr, lat=lat, method="DA02" - ) - np.testing.assert_allclose( - out.isel(lat=0, time=2), [2.32659206 / 86400], rtol=1e-2 - ) + out = xci.potential_evapotranspiration(tasmin=tn, tasmax=tx, tas=tg, pr=pr, lat=lat, method="DA02") + np.testing.assert_allclose(out.isel(lat=0, time=2), [2.32659206 / 86400], rtol=1e-2) def test_thornthwaite(self, tas_series, lat_series): lat = lat_series([45]) @@ -3607,9 +3419,7 @@ def test_thornthwaite(self, tas_series, lat_series): # find lat implicitly out = xci.potential_evapotranspiration(tas=tm, method="TW48") - np.testing.assert_allclose( - out.isel(lat=0, time=1), [42.7619242 / (86400 * 30)], rtol=1e-1 - ) + np.testing.assert_allclose(out.isel(lat=0, time=1), [42.7619242 / (86400 * 30)], rtol=1e-1) def test_mcguinnessbordne(self, tasmin_series, tasmax_series, lat_series): lat = lat_series([45]) @@ -3617,9 +3427,7 @@ def test_mcguinnessbordne(self, tasmin_series, tasmax_series, lat_series): tx = tasmax_series(np.array([10, 15, 20]) + 273.15).expand_dims(lat=lat) out = xci.potential_evapotranspiration(tn, tx, lat=lat, method="MB05") - np.testing.assert_allclose( - out.isel(lat=0, time=2), [2.78253138816 / 86400], rtol=1e-2 - ) + np.testing.assert_allclose(out.isel(lat=0, time=2), [2.78253138816 / 86400], rtol=1e-2) def test_allen( self, @@ -3657,27 +3465,15 @@ def test_allen( sfcWind=sfcWind, method="FAO_PM98", ) - np.testing.assert_allclose( - out.isel(lat=0, time=2), [1.208832768 / 86400], rtol=1e-2 - ) + np.testing.assert_allclose(out.isel(lat=0, time=2), [1.208832768 / 86400], rtol=1e-2) -def test_water_budget_from_tas( - pr_series, tasmin_series, tasmax_series, tas_series, lat_series -): +def test_water_budget_from_tas(pr_series, tasmin_series, tasmax_series, tas_series, lat_series): lat = lat_series([45]) pr = pr_series(np.array([10, 10, 10])).expand_dims(lat=lat) pr.attrs["units"] = "mm/day" - tn = ( - tasmin_series(np.array([0, 5, 10]) + K2C) - .expand_dims(lat=lat) - .assign_coords(lat=lat) - ) - tx = ( - tasmax_series(np.array([10, 15, 20]) + K2C) - .expand_dims(lat=lat) - .assign_coords(lat=lat) - ) + tn = tasmin_series(np.array([0, 5, 10]) + K2C).expand_dims(lat=lat).assign_coords(lat=lat) + tx = tasmax_series(np.array([10, 15, 20]) + K2C).expand_dims(lat=lat).assign_coords(lat=lat) out = xci.water_budget(pr, tasmin=tn, tasmax=tx, lat=lat, method="BR65") np.testing.assert_allclose(out[0, 2], 6.138921 / 86400, rtol=2e-3) @@ -3686,9 +3482,7 @@ def test_water_budget_from_tas( np.testing.assert_allclose(out[0, 2], 5.969661 / 86400, rtol=2e-3) tm = ( - tas_series(np.ones(12), start="1990-01-01", freq="MS", units="degC") - .expand_dims(lat=lat) - .assign_coords(lat=lat) + tas_series(np.ones(12), start="1990-01-01", freq="MS", units="degC").expand_dims(lat=lat).assign_coords(lat=lat) ) prm = ( pr_series(np.ones(12) * 10, start="1990-01-01", freq="MS", units="mm/day") @@ -3715,26 +3509,14 @@ def test_water_budget(pr_series, evspsblpot_series): "pr,thresh1,thresh2,window,outs", [ ( - [1.01] * 6 - + [0.01] * 3 - + [0.51] * 2 - + [0.75] * 2 - + [0.51] - + [0.01] * 3 - + [1.01] * 3, + [1.01] * 6 + [0.01] * 3 + [0.51] * 2 + [0.75] * 2 + [0.51] + [0.01] * 3 + [1.01] * 3, 3, 3, 7, (1, 12, 20, 12, 20), ), ( - [0.01] * 6 - + [1.01] * 3 - + [0.51] * 2 - + [0.75] * 2 - + [0.51] - + [0.01] * 3 - + [0.01] * 3, + [0.01] * 6 + [1.01] * 3 + [0.51] * 2 + [0.75] * 2 + [0.51] + [0.01] * 3 + [0.01] * 3, 3, 3, 7, @@ -3748,9 +3530,7 @@ def test_dry_spell(pr_series, pr, thresh1, thresh2, window, outs): out_events, out_total_d_sum, out_total_d_max, out_max_d_sum, out_max_d_max = outs - events = xci.dry_spell_frequency( - pr, thresh=f"{thresh1} mm", window=window, freq="YS" - ) + events = xci.dry_spell_frequency(pr, thresh=f"{thresh1} mm", window=window, freq="YS") total_d_sum = xci.dry_spell_total_length( pr, thresh=f"{thresh2} mm", @@ -3758,9 +3538,7 @@ def test_dry_spell(pr_series, pr, thresh1, thresh2, window, outs): op="sum", freq="YS", ) - total_d_max = xci.dry_spell_total_length( - pr, thresh=f"{thresh1} mm", window=window, op="max", freq="YS" - ) + total_d_max = xci.dry_spell_total_length(pr, thresh=f"{thresh1} mm", window=window, op="max", freq="YS") max_d_sum = xci.dry_spell_max_length( pr, thresh=f"{thresh2} mm", @@ -3768,9 +3546,7 @@ def test_dry_spell(pr_series, pr, thresh1, thresh2, window, outs): op="sum", freq="YS", ) - max_d_max = xci.dry_spell_max_length( - pr, thresh=f"{thresh1} mm", window=window, op="max", freq="YS" - ) + max_d_max = xci.dry_spell_max_length(pr, thresh=f"{thresh1} mm", window=window, op="max", freq="YS") np.testing.assert_allclose(events[0], [out_events], rtol=1e-1) np.testing.assert_allclose(total_d_sum[0], [out_total_d_sum], rtol=1e-1) np.testing.assert_allclose(total_d_max[0], [out_total_d_max], rtol=1e-1) @@ -3780,17 +3556,13 @@ def test_dry_spell(pr_series, pr, thresh1, thresh2, window, outs): def test_dry_spell_total_length_indexer(pr_series): pr = pr_series([1] * 5 + [0] * 10 + [1] * 350, start="1900-01-01", units="mm/d") - out = xci.dry_spell_total_length( - pr, window=7, op="sum", thresh="3 mm", freq="MS", date_bounds=("01-10", "12-31") - ) + out = xci.dry_spell_total_length(pr, window=7, op="sum", thresh="3 mm", freq="MS", date_bounds=("01-10", "12-31")) np.testing.assert_allclose(out, [9] + [0] * 11) def test_dry_spell_max_length_indexer(pr_series): pr = pr_series([1] * 5 + [0] * 10 + [1] * 350, start="1900-01-01", units="mm/d") - out = xci.dry_spell_max_length( - pr, window=7, op="sum", thresh="3 mm", freq="MS", date_bounds=("01-10", "12-31") - ) + out = xci.dry_spell_max_length(pr, window=7, op="sum", thresh="3 mm", freq="MS", date_bounds=("01-10", "12-31")) np.testing.assert_allclose(out, [9] + [0] * 11) @@ -3981,9 +3753,7 @@ def test_dryness_index(self, atmosds): di = xci.dryness_index(pr, evspsblpot) di_wet = xci.dryness_index(pr, evspsblpot, wo="300 mm") di_plus_100 = di + 100 - np.testing.assert_allclose( - di, np.array([13.355, 102.426, 65.576, 158.078]), rtol=1e-03 - ) + np.testing.assert_allclose(di, np.array([13.355, 102.426, 65.576, 158.078]), rtol=1e-03) np.testing.assert_allclose(di_wet, di_plus_100) @@ -4019,13 +3789,7 @@ def test_hardiness_zones(tasmin_series, tmin, meth, zone): "pr,threshmin,threshsum,window,outs", [ ( - [1.01] * 6 - + [0.01] * 3 - + [0.51] * 2 - + [0.75] * 2 - + [0.51] - + [0.01] * 3 - + [1.01] * 3, + [1.01] * 6 + [0.01] * 3 + [0.51] * 2 + [0.75] * 2 + [0.51] + [0.01] * 3 + [1.01] * 3, 3, 3, 7, @@ -4052,9 +3816,7 @@ def test_wet_spell(pr_series, pr, threshmin, threshsum, window, outs): out_events, out_total_d_sum, out_total_d_min, out_max_d_sum, out_max_d_min = outs - events = xci.wet_spell_frequency( - pr, thresh=f"{threshsum} mm", window=window, freq="YS", op="sum" - ) + events = xci.wet_spell_frequency(pr, thresh=f"{threshsum} mm", window=window, freq="YS", op="sum") total_d_sum = xci.wet_spell_total_length( pr, thresh=f"{threshsum} mm", @@ -4062,9 +3824,7 @@ def test_wet_spell(pr_series, pr, threshmin, threshsum, window, outs): op="sum", freq="YS", ) - total_d_min = xci.wet_spell_total_length( - pr, thresh=f"{threshmin} mm", window=window, op="min", freq="YS" - ) + total_d_min = xci.wet_spell_total_length(pr, thresh=f"{threshmin} mm", window=window, op="min", freq="YS") max_d_sum = xci.wet_spell_max_length( pr, thresh=f"{threshsum} mm", @@ -4072,9 +3832,7 @@ def test_wet_spell(pr_series, pr, threshmin, threshsum, window, outs): op="sum", freq="YS", ) - max_d_min = xci.wet_spell_max_length( - pr, thresh=f"{threshmin} mm", window=window, op="min", freq="YS" - ) + max_d_min = xci.wet_spell_max_length(pr, thresh=f"{threshmin} mm", window=window, op="min", freq="YS") np.testing.assert_allclose(events[0], [out_events], rtol=1e-1) np.testing.assert_allclose(total_d_sum[0], [out_total_d_sum], rtol=1e-1) np.testing.assert_allclose(total_d_min[0], [out_total_d_min], rtol=1e-1) @@ -4226,9 +3984,7 @@ def test_simple(self, sfcWind_series): class TestWindPowerPotential: def test_simple(self, sfcWind_series): v = [2, 6, 20, 30] - p = xci.wind_power_potential( - sfcWind_series(v, units="m/s"), cut_in="4 m/s", rated="8 m/s" - ) + p = xci.wind_power_potential(sfcWind_series(v, units="m/s"), cut_in="4 m/s", rated="8 m/s") np.testing.assert_allclose(p, [0, (6**3 - 4**3) / (8**3 - 4**3), 1, 0]) # Test discontinuities at the default thresholds diff --git a/tests/test_locales.py b/tests/test_locales.py index 24187dc5a..3a49d3855 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -42,9 +42,7 @@ def test_local_dict(tmp_path): loc, dic = xloc.get_local_dict("fr") assert loc == "fr" - assert ( - dic["TG_MEAN"]["long_name"] == "Moyenne de la température moyenne quotidienne" - ) + assert dic["TG_MEAN"]["long_name"] == "Moyenne de la température moyenne quotidienne" loc, dic = xloc.get_local_dict(esperanto) assert loc == "eo" @@ -63,15 +61,11 @@ def test_local_dict(tmp_path): loc, dic = xloc.get_local_dict(("fr", {"TX_MAX": {"long_name": "Fait chaud."}})) assert loc == "fr" assert dic["TX_MAX"]["long_name"] == "Fait chaud." - assert ( - dic["TG_MEAN"]["long_name"] == "Moyenne de la température moyenne quotidienne" - ) + assert dic["TG_MEAN"]["long_name"] == "Moyenne de la température moyenne quotidienne" def test_local_attrs_sing(): - attrs = xloc.get_local_attrs( - atmos.tg_mean.__class__.__name__, esperanto, append_locale_name=False - ) + attrs = xloc.get_local_attrs(atmos.tg_mean.__class__.__name__, esperanto, append_locale_name=False) assert "description" not in attrs with pytest.raises(ValueError): @@ -108,10 +102,7 @@ def test_indicator_output(tas_series): tgmean = atmos.tg_mean(tas, freq="YS") assert "long_name_fr" in tgmean.attrs - assert ( - tgmean.attrs["description_fr"] - == "Moyenne annuelle de la température quotidienne." - ) + assert tgmean.attrs["description_fr"] == "Moyenne annuelle de la température quotidienne." def test_indicator_integration(): diff --git a/tests/test_modules.py b/tests/test_modules.py index 970e2a1ed..2190949bc 100644 --- a/tests/test_modules.py +++ b/tests/test_modules.py @@ -53,8 +53,7 @@ def test_virtual_modules(virtual_indicator, atmosds): mod, indname, ind = virtual_indicator for name, param in ind.parameters.items(): if param.kind not in [InputKind.DATASET, InputKind.KWARGS] and ( - param.default in (None, _empty) - or (param.default == name and name not in atmosds) + param.default in (None, _empty) or (param.default == name and name not in atmosds) ): pytest.skip(f"Indicator {mod}.{indname} has no default for {name}.") ind(ds=atmosds) @@ -71,20 +70,14 @@ def test_custom_indices(open_dataset): example = load_module(example_path / "example.py") # From module - ex1 = build_indicator_module_from_yaml( - example_path / "example.yml", name="ex1", indices=example - ) + ex1 = build_indicator_module_from_yaml(example_path / "example.yml", name="ex1", indices=example) # Did this register the new variable? assert "prveg" in VARIABLES # From mapping - extreme_inds = { - "extreme_precip_accumulation_and_days": example.extreme_precip_accumulation_and_days - } - ex2 = build_indicator_module_from_yaml( - example_path / "example.yml", name="ex2", indices=extreme_inds - ) + extreme_inds = {"extreme_precip_accumulation_and_days": example.extreme_precip_accumulation_and_days} + ex2 = build_indicator_module_from_yaml(example_path / "example.yml", name="ex2", indices=extreme_inds) assert ex1.R95p.__doc__ == ex2.R95p.__doc__ # noqa @@ -94,23 +87,16 @@ def test_custom_indices(open_dataset): xr.testing.assert_equal(out1[0], out2[0]) # Check that missing was not modified even with injecting `freq`. - assert ( - ex1.RX5day_canopy.missing - == indicators.atmos.max_n_day_precipitation_amount.missing - ) + assert ex1.RX5day_canopy.missing == indicators.atmos.max_n_day_precipitation_amount.missing # Error when missing with pytest.raises(ImportError, match="extreme_precip_accumulation_and_days"): build_indicator_module_from_yaml(example_path / "example.yml", name="ex3") - build_indicator_module_from_yaml( - example_path / "example.yml", name="ex4", mode="ignore" - ) + build_indicator_module_from_yaml(example_path / "example.yml", name="ex4", mode="ignore") # Check that indexer was added and injected correctly assert "indexer" not in ex1.RX1day_summer.parameters - assert ex1.RX1day_summer.injected_parameters["indexer"] == { - "month": [5, 6, 7, 8, 9] - } + assert ex1.RX1day_summer.injected_parameters["indexer"] == {"month": [5, 6, 7, 8, 9]} @pytest.mark.requires_docs @@ -119,21 +105,17 @@ def test_indicator_module_translations(): example_path = Path(__file__).parent.parent / "docs" / "notebooks" / "example" ex = build_indicator_module_from_yaml(example_path / "example", name="ex_trans") - assert ex.RX5day_canopy.translate_attrs("fr")["cf_attrs"][0][ - "long_name" - ].startswith("Cumul maximal") - assert indicators.atmos.max_n_day_precipitation_amount.translate_attrs("fr")[ - "cf_attrs" - ][0]["long_name"].startswith("Maximum du cumul") + assert ex.RX5day_canopy.translate_attrs("fr")["cf_attrs"][0]["long_name"].startswith("Cumul maximal") + assert indicators.atmos.max_n_day_precipitation_amount.translate_attrs("fr")["cf_attrs"][0]["long_name"].startswith( + "Maximum du cumul" + ) @pytest.mark.requires_docs def test_indicator_module_input_mapping(atmosds): example_path = Path(__file__).parent.parent / "docs" / "notebooks" / "example" ex = build_indicator_module_from_yaml(example_path / "example", name="ex_input") - prveg = atmosds.pr.rename("prveg").assign_attrs( - standard_name="precipitation_flux_onto_canopy" - ) + prveg = atmosds.pr.rename("prveg").assign_attrs(standard_name="precipitation_flux_onto_canopy") out = ex.RX5day_canopy(prveg=prveg) assert "RX5DAY_CANOPY(prveg=prveg)" in out.attrs["history"] @@ -156,15 +138,9 @@ def test_build_indicator_module_from_yaml_edge_cases(): name="ex5", ) assert hasattr(indicators, "ex5") - assert ex5.R95p.translate_attrs("fr")["cf_attrs"][0]["description"].startswith( - "Épaisseur équivalente" - ) - assert ex5.R95p.translate_attrs("ru")["cf_attrs"][0]["description"].startswith( - "Épaisseur équivalente" - ) - assert ex5.R95p.translate_attrs("eo")["cf_attrs"][0]["description"].startswith( - "Épaisseur équivalente" - ) + assert ex5.R95p.translate_attrs("fr")["cf_attrs"][0]["description"].startswith("Épaisseur équivalente") + assert ex5.R95p.translate_attrs("ru")["cf_attrs"][0]["description"].startswith("Épaisseur équivalente") + assert ex5.R95p.translate_attrs("eo")["cf_attrs"][0]["description"].startswith("Épaisseur équivalente") class TestClixMeta: @@ -275,9 +251,7 @@ def test_all(self): @pytest.mark.xfail(reason="This test is relatively unstable.", strict=False) -@pytest.mark.skipif( - platform.system() == "Windows", reason="nl_langinfo not available on Windows." -) +@pytest.mark.skipif(platform.system() == "Windows", reason="nl_langinfo not available on Windows.") def test_encoding(): import _locale import sys diff --git a/tests/test_partitioning.py b/tests/test_partitioning.py index 21ffe9995..79f4dcbfe 100644 --- a/tests/test_partitioning.py +++ b/tests/test_partitioning.py @@ -15,11 +15,7 @@ def test_hawkins_sutton_smoke(open_dataset): """Just a smoke test.""" dims = {"run": "member", "scen": "scenario"} - da = ( - open_dataset("uncertainty_partitioning/cmip5_pr_global_mon.nc") - .pr.sel(time=slice("1950", None)) - .rename(dims) - ) + da = open_dataset("uncertainty_partitioning/cmip5_pr_global_mon.nc").pr.sel(time=slice("1950", None)).rename(dims) da1 = _model_in_all_scens(da) dac = _concat_hist(da1, scenario="historical") das = _single_member(dac) @@ -68,10 +64,7 @@ def test_hawkins_sutton_synthetic(random): # We expect the scenario uncertainty to grow over time # The scenarios all have the same absolute slope, but since their reference mean is different, the relative increase # is not the same and this creates a spread over time across "relative" scenarios. - assert ( - su.sel(time=slice("2020", None)).mean() - > su.sel(time=slice("2000", "2010")).mean() - ) + assert su.sel(time=slice("2020", None)).mean() > su.sel(time=slice("2000", "2010")).mean() def test_lafferty_sriver_synthetic(random): @@ -82,20 +75,14 @@ def test_lafferty_sriver_synthetic(random): sm = np.arange(10, 41, 10) # Scenario mean (4) mm = np.arange(-6, 7, 1) # Model mean (13) dm = np.arange(-2, 3, 1) # Downscaling mean (5) - mean = ( - dm[np.newaxis, np.newaxis, :] - + mm[np.newaxis, :, np.newaxis] - + sm[:, np.newaxis, np.newaxis] - ) + mean = dm[np.newaxis, np.newaxis, :] + mm[np.newaxis, :, np.newaxis] + sm[:, np.newaxis, np.newaxis] # Natural variability r = random.standard_normal((4, 13, 5, 60)) x = r + mean[:, :, :, np.newaxis] time = xr.date_range("1970-01-01", periods=60, freq="YE") - da = xr.DataArray( - x, dims=("scenario", "model", "downscaling", "time"), coords={"time": time} - ) + da = xr.DataArray(x, dims=("scenario", "model", "downscaling", "time"), coords={"time": time}) m, v = lafferty_sriver(da) # Mean uncertainty over time vm = v.mean(dim="time") @@ -119,20 +106,10 @@ def test_lafferty_sriver(lafferty_sriver_ds): # Assertions based on expected results from # https://github.com/david0811/lafferty-sriver_2023_npjCliAtm/blob/main/unit_test/unit_test_check.ipynb - assert fu.sel(time="2020", uncertainty="downscaling") > fu.sel( - time="2020", uncertainty="model" - ) - assert fu.sel(time="2020", uncertainty="variability") > fu.sel( - time="2020", uncertainty="scenario" - ) - assert ( - fu.sel(time="2090", uncertainty="scenario").data - > fu.sel(time="2020", uncertainty="scenario").data - ) - assert ( - fu.sel(time="2090", uncertainty="downscaling").data - < fu.sel(time="2020", uncertainty="downscaling").data - ) + assert fu.sel(time="2020", uncertainty="downscaling") > fu.sel(time="2020", uncertainty="model") + assert fu.sel(time="2020", uncertainty="variability") > fu.sel(time="2020", uncertainty="scenario") + assert fu.sel(time="2090", uncertainty="scenario").data > fu.sel(time="2020", uncertainty="scenario").data + assert fu.sel(time="2090", uncertainty="downscaling").data < fu.sel(time="2020", uncertainty="downscaling").data def graph(): """Return graphic like in https://github.com/david0811/lafferty-sriver_2023_npjCliAtm/blob/main/unit_test/unit_test_check.ipynb""" @@ -174,9 +151,7 @@ def test_general_partition(lafferty_sriver_ds): sm="poly", ) # fix order - u2 = u2.sel( - uncertainty=["model", "scenario", "downscaling", "variability", "total"] - ) + u2 = u2.sel(uncertainty=["model", "scenario", "downscaling", "variability", "total"]) assert u1.equals(u2) np.testing.assert_allclose(g1.values, g2.values, atol=0.1) diff --git a/tests/test_precip.py b/tests/test_precip.py index 68b3b1bb7..4419b6fbc 100644 --- a/tests/test_precip.py +++ b/tests/test_precip.py @@ -106,12 +106,8 @@ def test_with_different_phases(self, open_dataset): assert out_sol.standard_name == "lwe_thickness_of_snowfall_amount" # With a non-default threshold - out_sol = atmos.solid_precip_accumulation( - pr, tas=tasmin, thresh="40 degF", freq="MS" - ) - out_liq = atmos.liquid_precip_accumulation( - pr, tas=tasmin, thresh="40 degF", freq="MS" - ) + out_sol = atmos.solid_precip_accumulation(pr, tas=tasmin, thresh="40 degF", freq="MS") + out_liq = atmos.liquid_precip_accumulation(pr, tas=tasmin, thresh="40 degF", freq="MS") np.testing.assert_array_almost_equal(out_liq + out_sol, out_tot, 4) @@ -167,12 +163,8 @@ def test_with_different_phases(self, open_dataset): assert out_sol.standard_name == "lwe_average_of_snowfall_amount" # With a non-default threshold - out_sol = atmos.solid_precip_average( - pr, tas=tasmin, thresh="40 degF", freq="MS" - ) - out_liq = atmos.liquid_precip_average( - pr, tas=tasmin, thresh="40 degF", freq="MS" - ) + out_sol = atmos.solid_precip_average(pr, tas=tasmin, thresh="40 degF", freq="MS") + out_liq = atmos.liquid_precip_average(pr, tas=tasmin, thresh="40 degF", freq="MS") np.testing.assert_array_almost_equal(out_liq + out_sol, out_tot, 4) @@ -483,15 +475,11 @@ class TestSnowfallDate: @classmethod def get_snowfall(cls, open_dataset): dnr = xr.merge((open_dataset(cls.pr_file), open_dataset(cls.tasmin_file))) - return atmos.snowfall_approximation( - dnr.pr, tas=dnr.tasmin, thresh="-0.5 degC", method="binary" - ) + return atmos.snowfall_approximation(dnr.pr, tas=dnr.tasmin, thresh="-0.5 degC", method="binary") def test_first_snowfall(self, open_dataset): with set_options(check_missing="skip"): - fs = atmos.first_snowfall( - prsn=self.get_snowfall(open_dataset), thresh="0.5 mm/day" - ) + fs = atmos.first_snowfall(prsn=self.get_snowfall(open_dataset), thresh="0.5 mm/day") np.testing.assert_array_equal( fs[:, [0, 45, 82], [10, 105, 155]], @@ -505,9 +493,7 @@ def test_first_snowfall(self, open_dataset): def test_last_snowfall(self, open_dataset): with set_options(check_missing="skip"): - ls = atmos.last_snowfall( - prsn=self.get_snowfall(open_dataset), thresh="0.5 mm/day" - ) + ls = atmos.last_snowfall(prsn=self.get_snowfall(open_dataset), thresh="0.5 mm/day") np.testing.assert_array_equal( ls[:, [0, 45, 82], [10, 105, 155]], @@ -550,9 +536,7 @@ def test_days_over_precip_thresh(open_dataset): out = atmos.days_over_precip_thresh(pr, per) - np.testing.assert_allclose( - out[1, :], np.array([80.0, 64.0, 65.0, 83.0]), atol=0.001 - ) + np.testing.assert_allclose(out[1, :], np.array([80.0, 64.0, 65.0, 83.0]), atol=0.001) assert "80.0th percentile" in out.attrs["description"] assert "['1990-01-01', '1993-12-31'] period" in out.attrs["description"] @@ -562,9 +546,7 @@ def test_days_over_precip_thresh__seasonal_indexer(open_dataset): pr = open_dataset("ERA5/daily_surface_cancities_1990-1993.nc").pr per = pr.quantile(0.8, "time", keep_attrs=True) # WHEN - out = atmos.days_over_precip_thresh( - pr, per, freq="YS", date_bounds=("01-10", "12-31") - ) + out = atmos.days_over_precip_thresh(pr, per, freq="YS", date_bounds=("01-10", "12-31")) # THEN np.testing.assert_almost_equal(out[0], np.array([81.0, 66.0, 66.0, 75.0])) @@ -574,14 +556,10 @@ def test_fraction_over_precip_doy_thresh(open_dataset): per = percentile_doy(pr, window=5, per=80) out = atmos.fraction_over_precip_doy_thresh(pr, per) - np.testing.assert_allclose( - out[1, :, 0], np.array([0.803, 0.747, 0.745, 0.806]), atol=0.001 - ) + np.testing.assert_allclose(out[1, :, 0], np.array([0.803, 0.747, 0.745, 0.806]), atol=0.001) out = atmos.fraction_over_precip_doy_thresh(pr, per, thresh="0.002 m/d") - np.testing.assert_allclose( - out[1, :, 0], np.array([0.822, 0.780, 0.771, 0.829]), atol=0.001 - ) + np.testing.assert_allclose(out[1, :, 0], np.array([0.822, 0.780, 0.771, 0.829]), atol=0.001) assert "only days with at least 0.002 m/d are included" in out.description assert "[80]th percentile" in out.attrs["description"] @@ -596,9 +574,7 @@ def test_fraction_over_precip_thresh(open_dataset): out = atmos.fraction_over_precip_thresh(pr, per) - np.testing.assert_allclose( - out[1, :], np.array([0.839, 0.812, 0.776, 0.864]), atol=0.001 - ) + np.testing.assert_allclose(out[1, :], np.array([0.839, 0.812, 0.776, 0.864]), atol=0.001) assert "80.0th percentile" in out.attrs["description"] assert "['1990-01-01', '1993-12-31'] period" in out.attrs["description"] @@ -608,18 +584,12 @@ def test_liquid_precip_ratio(open_dataset): ds = open_dataset("ERA5/daily_surface_cancities_1990-1993.nc") out = atmos.liquid_precip_ratio(pr=ds.pr, tas=ds.tas, thresh="0 degC", freq="YS") - np.testing.assert_allclose( - out[:, 0], np.array([0.919, 0.805, 0.525, 0.740, 0.993]), atol=1e3 - ) + np.testing.assert_allclose(out[:, 0], np.array([0.919, 0.805, 0.525, 0.740, 0.993]), atol=1e3) with set_options(cf_compliance="raise"): # Test if tasmax is allowed - out = atmos.liquid_precip_ratio( - pr=ds.pr, tas=ds.tasmax, thresh="33 degF", freq="YS" - ) - np.testing.assert_allclose( - out[:, 0], np.array([0.975, 0.921, 0.547, 0.794, 0.999]), atol=1e3 - ) + out = atmos.liquid_precip_ratio(pr=ds.pr, tas=ds.tasmax, thresh="33 degF", freq="YS") + np.testing.assert_allclose(out[:, 0], np.array([0.975, 0.921, 0.547, 0.794, 0.999]), atol=1e3) assert "where temperature is above 33 degf." in out.description @@ -627,18 +597,10 @@ def test_dry_spell(atmosds): pr = atmosds.pr events = atmos.dry_spell_frequency(pr, thresh="3 mm", window=7, freq="YS") - total_d_sum = atmos.dry_spell_total_length( - pr, thresh="3 mm", window=7, op="sum", freq="YS" - ) - total_d_max = atmos.dry_spell_total_length( - pr, thresh="3 mm", window=7, op="max", freq="YS" - ) - max_d_sum = atmos.dry_spell_max_length( - pr, thresh="3 mm", window=7, op="sum", freq="YS" - ) - max_d_max = atmos.dry_spell_max_length( - pr, thresh="3 mm", window=7, op="max", freq="YS" - ) + total_d_sum = atmos.dry_spell_total_length(pr, thresh="3 mm", window=7, op="sum", freq="YS") + total_d_max = atmos.dry_spell_total_length(pr, thresh="3 mm", window=7, op="max", freq="YS") + max_d_sum = atmos.dry_spell_max_length(pr, thresh="3 mm", window=7, op="sum", freq="YS") + max_d_max = atmos.dry_spell_max_length(pr, thresh="3 mm", window=7, op="max", freq="YS") total_d_sum = total_d_sum.sel(location="Halifax", drop=True).isel(time=slice(0, 2)) total_d_max = total_d_max.sel(location="Halifax", drop=True).isel(time=slice(0, 2)) max_d_sum = max_d_sum.sel(location="Halifax", drop=True).isel(time=slice(0, 2)) @@ -654,28 +616,14 @@ def test_dry_spell(atmosds): "The annual number of dry periods of 7 day(s) or more, " "during which the total precipitation on a window of 7 day(s) is below 3 mm." ) in events.description - assert ( - "The annual number of days in dry periods of 7 day(s) or more" - in total_d_sum.description - ) - assert ( - "The annual number of days in dry periods of 7 day(s) or more" - in total_d_max.description - ) - assert ( - "The maximum annual number of consecutive days in a dry period of 7 day(s) or more" - in max_d_sum.description - ) - assert ( - "The maximum annual number of consecutive days in a dry period of 7 day(s) or more" - in max_d_max.description - ) + assert "The annual number of days in dry periods of 7 day(s) or more" in total_d_sum.description + assert "The annual number of days in dry periods of 7 day(s) or more" in total_d_max.description + assert "The maximum annual number of consecutive days in a dry period of 7 day(s) or more" in max_d_sum.description + assert "The maximum annual number of consecutive days in a dry period of 7 day(s) or more" in max_d_max.description def test_dry_spell_total_length_indexer(pr_series): - pr = pr_series( - [np.nan] + [1] * 4 + [0] * 10 + [1] * 350, start="1900-01-01", units="mm/d" - ) + pr = pr_series([np.nan] + [1] * 4 + [0] * 10 + [1] * 350, start="1900-01-01", units="mm/d") out = atmos.dry_spell_total_length( pr, window=7, @@ -685,16 +633,12 @@ def test_dry_spell_total_length_indexer(pr_series): ) np.testing.assert_allclose(out, [np.nan] + [0] * 11) - out = atmos.dry_spell_total_length( - pr, window=7, op="sum", thresh="3 mm", freq="MS", date_bounds=("01-10", "12-31") - ) + out = atmos.dry_spell_total_length(pr, window=7, op="sum", thresh="3 mm", freq="MS", date_bounds=("01-10", "12-31")) np.testing.assert_allclose(out, [9] + [0] * 11) def test_dry_spell_max_length_indexer(pr_series): - pr = pr_series( - [np.nan] + [1] * 4 + [0] * 10 + [1] * 350, start="1900-01-01", units="mm/d" - ) + pr = pr_series([np.nan] + [1] * 4 + [0] * 10 + [1] * 350, start="1900-01-01", units="mm/d") out = atmos.dry_spell_max_length( pr, window=7, @@ -704,27 +648,17 @@ def test_dry_spell_max_length_indexer(pr_series): ) np.testing.assert_allclose(out, [np.nan] + [0] * 11) - out = atmos.dry_spell_total_length( - pr, window=7, op="sum", thresh="3 mm", freq="MS", date_bounds=("01-10", "12-31") - ) + out = atmos.dry_spell_total_length(pr, window=7, op="sum", thresh="3 mm", freq="MS", date_bounds=("01-10", "12-31")) np.testing.assert_allclose(out, [9] + [0] * 11) def test_dry_spell_frequency_op(open_dataset): pr = open_dataset("ERA5/daily_surface_cancities_1990-1993.nc").pr - test_sum = atmos.dry_spell_frequency( - pr, thresh="3 mm", window=7, freq="MS", op="sum" - ) - test_max = atmos.dry_spell_frequency( - pr, thresh="3 mm", window=7, freq="MS", op="max" - ) + test_sum = atmos.dry_spell_frequency(pr, thresh="3 mm", window=7, freq="MS", op="sum") + test_max = atmos.dry_spell_frequency(pr, thresh="3 mm", window=7, freq="MS", op="max") - np.testing.assert_allclose( - test_sum[0, :14], [1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0], rtol=1e-1 - ) - np.testing.assert_allclose( - test_max[0, :14], [1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 2, 1], rtol=1e-1 - ) + np.testing.assert_allclose(test_sum[0, :14], [1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0], rtol=1e-1) + np.testing.assert_allclose(test_max[0, :14], [1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 2, 1], rtol=1e-1) assert ( "The monthly number of dry periods of 7 day(s) or more, " "during which the total precipitation on a window of 7 day(s) is below 3 mm." @@ -742,9 +676,7 @@ class TestSnowfallMeteoSwiss: @classmethod def get_snowfall(cls, open_dataset): dnr = xr.merge((open_dataset(cls.pr_file), open_dataset(cls.tasmin_file))) - return atmos.snowfall_approximation( - dnr.pr, tas=dnr.tasmin, thresh="-0.5 degC", method="binary" - ) + return atmos.snowfall_approximation(dnr.pr, tas=dnr.tasmin, thresh="-0.5 degC", method="binary") def test_snowfall_frequency(self, open_dataset): prsn = self.get_snowfall(open_dataset) diff --git a/tests/test_run_length.py b/tests/test_run_length.py index 0c64e3a84..aabb9305f 100644 --- a/tests/test_run_length.py +++ b/tests/test_run_length.py @@ -255,9 +255,7 @@ def test_other_stats(self): lt = rl.rle_statistics(da, freq="YS", reducer="min", window=1, ufunc_1dim=False) assert lt == 35 - lt = rl.rle_statistics( - da, freq="YS", reducer="mean", window=36, ufunc_1dim=False - ) + lt = rl.rle_statistics(da, freq="YS", reducer="mean", window=36, ufunc_1dim=False) assert lt == 329 lt = rl.rle_statistics(da, freq="YS", reducer="std", window=1, ufunc_1dim=False) @@ -269,24 +267,16 @@ def test_resampling_order(self, op): values[35:45] = 0 time = pd.date_range("1/1/2000", periods=len(values), freq="D") da = xr.DataArray(values != 0, coords={"time": time}, dims="time") - lt_resample_before = da.resample(time="MS").map( - rl.rle_statistics, reducer=op, window=1, ufunc_1dim=False - ) - lt_resample_after = rl.rle_statistics( - da, freq="MS", reducer=op, window=1, ufunc_1dim=False - ) + lt_resample_before = da.resample(time="MS").map(rl.rle_statistics, reducer=op, window=1, ufunc_1dim=False) + lt_resample_after = rl.rle_statistics(da, freq="MS", reducer=op, window=1, ufunc_1dim=False) assert (lt_resample_before != lt_resample_after).any() values = np.zeros(365) values[0:-1:31] = 1 time = pd.date_range("1/1/2000", periods=len(values), freq="D") da = xr.DataArray(values != 0, coords={"time": time}, dims="time") - lt_resample_before = da.resample(time="MS").map( - rl.rle_statistics, reducer=op, window=1, ufunc_1dim=False - ) - lt_resample_after = rl.rle_statistics( - da, freq="MS", reducer=op, window=1, ufunc_1dim=False - ) + lt_resample_before = da.resample(time="MS").map(rl.rle_statistics, reducer=op, window=1, ufunc_1dim=False) + lt_resample_after = rl.rle_statistics(da, freq="MS", reducer=op, window=1, ufunc_1dim=False) assert (lt_resample_before == lt_resample_after).any() @@ -343,9 +333,7 @@ def test_resample_after(self, tas_series, coord, expected, use_dask): if use_dask: runs = runs.chunk({"time": -1 if ufunc else 10, "dim0": 1}) - out = rl.first_run( - runs, window=1, dim="time", coord=coord, freq="MS", ufunc_1dim=False - ) + out = rl.first_run(runs, window=1, dim="time", coord=coord, freq="MS", ufunc_1dim=False) np.testing.assert_array_equal(out.load(), np.array([expected, expected])) @@ -364,9 +352,7 @@ def test_simple(self, index): a = xr.DataArray(np.zeros(50, bool), dims=("time",)) a[4:7] = True a[34:45] = True - assert rl.windowed_run_count(a, 3, dim="time", index=index) == len( - a[4:7] - ) + len(a[34:45]) + assert rl.windowed_run_count(a, 3, dim="time", index=index) == len(a[4:7]) + len(a[34:45]) class TestWindowedMaxRunSum: @@ -418,16 +404,12 @@ def test_resample_after(self, tas_series, coord, expected, use_dask): if use_dask: runs = runs.chunk({"time": -1 if ufunc else 10, "dim0": 1}) - out = rl.last_run( - runs, window=1, dim="time", coord=coord, freq="MS", ufunc_1dim=False - ) + out = rl.last_run(runs, window=1, dim="time", coord=coord, freq="MS", ufunc_1dim=False) np.testing.assert_array_equal(out.load(), np.array([expected, expected])) def test_run_bounds_synthetic(): - run = xr.DataArray( - [0, 1, 1, 1, 0, 0, 1, 1, 1, 0], dims="x", coords={"x": np.arange(10) ** 2} - ) + run = xr.DataArray([0, 1, 1, 1, 0, 0, 1, 1, 1, 0], dims="x", coords={"x": np.arange(10) ** 2}) bounds = rl.run_bounds(run, "x", coord=True) np.testing.assert_array_equal(bounds, [[1, 36], [16, 81]]) @@ -453,9 +435,7 @@ def test_run_bounds_data(open_dataset): def test_keep_longest_run_synthetic(): runs = xr.DataArray([0, 1, 1, 1, 0, 0, 1, 1, 1, 0], dims="time").astype(bool) lrun = rl.keep_longest_run(runs, "time") - np.testing.assert_array_equal( - lrun, np.array([0, 1, 1, 1, 0, 0, 0, 0, 0, 0], dtype=bool) - ) + np.testing.assert_array_equal(lrun, np.array([0, 1, 1, 1, 0, 0, 0, 0, 0, 0], dtype=bool)) def test_keep_longest_run_data(open_dataset): @@ -515,9 +495,7 @@ def test_season_length(self, tas_series, date, end, expected, use_dask, ufunc): ], ) @pytest.mark.parametrize("use_dask", [True, False]) - def test_run_end_after_date( - self, tas_series, coord, date, end, expected, use_dask, ufunc - ): + def test_run_end_after_date(self, tas_series, coord, date, end, expected, use_dask, ufunc): # if use_dask and ufunc: # pytest.xfail("Ufunc run length algorithms not implemented for dask arrays.") @@ -543,9 +521,7 @@ def test_run_end_after_date( ], ) @pytest.mark.parametrize("use_dask", [True, False]) - def test_first_run_after_date( - self, tas_series, coord, date, beg, expected, use_dask, ufunc - ): + def test_first_run_after_date(self, tas_series, coord, date, beg, expected, use_dask, ufunc): # if use_dask and ufunc: # pytest.xfail("Ufunc run length algorithms not implemented for dask arrays.") t = np.zeros(365) @@ -558,9 +534,7 @@ def test_first_run_after_date( if use_dask: runs = runs.chunk({"time": -1 if ufunc else 10, "dim0": 1}) - out = rl.first_run_after_date( - runs, window=1, date=date, dim="time", coord=coord - ) + out = rl.first_run_after_date(runs, window=1, date=date, dim="time", coord=coord) np.testing.assert_array_equal(np.mean(out.load()), expected) @pytest.mark.parametrize( @@ -573,9 +547,7 @@ def test_first_run_after_date( ], ) @pytest.mark.parametrize("use_dask", [True, False]) - def test_last_run_before_date( - self, tas_series, coord, date, end, expected, use_dask, ufunc - ): + def test_last_run_before_date(self, tas_series, coord, date, end, expected, use_dask, ufunc): # if use_dask and ufunc: # pytest.xfail("Ufunc run length algorithms not implemented for dask arrays.") t = np.zeros(360) @@ -587,9 +559,7 @@ def test_last_run_before_date( if use_dask: runs = runs.chunk({"time": -1 if ufunc else 10, "dim0": 1}) - out = rl.last_run_before_date( - runs, window=1, date=date, dim="time", coord=coord - ) + out = rl.last_run_before_date(runs, window=1, date=date, dim="time", coord=coord) np.testing.assert_array_equal(np.mean(out.load()), expected) @pytest.mark.parametrize( @@ -616,40 +586,22 @@ def test_run_with_dates_no_date(self, tas_series, use_dask, func, ufunc): [("standard", [61, 60]), ("365_day", [60, 60]), ("366_day", [61, 61])], ) def test_run_with_dates_different_calendars(self, calendar, expected): - time = xr.cftime_range( - "2004-01-01", end="2005-12-31", freq="D", calendar=calendar - ) + time = xr.cftime_range("2004-01-01", end="2005-12-31", freq="D", calendar=calendar) tas = np.zeros(time.size) start = np.where((time.day == 1) & (time.month == 3))[0] tas[start[0] : start[0] + 250] = 5 tas[start[1] : start[1] + 250] = 5 tas = xr.DataArray(tas, coords={"time": time}, dims=("time",)) - out = ( - (tas > 0) - .resample(time="YS-MAR") - .map(rl.first_run_after_date, date="03-01", window=2) - ) + out = (tas > 0).resample(time="YS-MAR").map(rl.first_run_after_date, date="03-01", window=2) np.testing.assert_array_equal(out.values[1:], expected) - out = ( - (tas > 0) - .resample(time="YS-MAR") - .map(rl.season_length, mid_date="03-02", window=2) - ) + out = (tas > 0).resample(time="YS-MAR").map(rl.season_length, mid_date="03-02", window=2) np.testing.assert_array_equal(out.values[1:], [250, 250]) - out = ( - (tas > 0) - .resample(time="YS-MAR") - .map(rl.run_end_after_date, date="03-03", window=2) - ) + out = (tas > 0).resample(time="YS-MAR").map(rl.run_end_after_date, date="03-03", window=2) np.testing.assert_array_equal(out.values[1:], np.array(expected) + 250) - out = ( - (tas > 0) - .resample(time="YS-MAR") - .map(rl.last_run_before_date, date="03-02", window=2) - ) + out = (tas > 0).resample(time="YS-MAR").map(rl.last_run_before_date, date="03-02", window=2) np.testing.assert_array_equal(out.values[1:], np.array(expected) + 1) @pytest.mark.parametrize("func", [rl.first_run_after_date, rl.run_end_after_date]) diff --git a/tests/test_sdba/conftest.py b/tests/test_sdba/conftest.py index 9f38e9dfe..d6001089a 100644 --- a/tests/test_sdba/conftest.py +++ b/tests/test_sdba/conftest.py @@ -113,9 +113,7 @@ def _ref_hist_sim_tuto(sim_offset=3, delta=0.1, smth_win=3, trend=True): ref = ds.air.resample(time="D").mean(keep_attrs=True) hist = ref.rolling(time=smth_win, min_periods=1).mean(keep_attrs=True) + delta hist.attrs["units"] = ref.attrs["units"] - sim_time = hist.time + np.timedelta64(730 + sim_offset * 365, "D").astype( - " 1], q_thresh) - base[base > qv] = genpareto.rvs( - c, loc=qv, scale=s, size=base[base > qv].shape, random_state=random - ) + base[base > qv] = genpareto.rvs(c, loc=qv, scale=s, size=base[base > qv].shape, random_state=random) return xr.DataArray( base, dims=("time",), - coords={ - "time": xr.cftime_range("1990-01-01", periods=n, calendar="noleap") - }, + coords={"time": xr.cftime_range("1990-01-01", periods=n, calendar="noleap")}, attrs={"units": "mm/day", "thresh": qv}, ) @@ -804,9 +765,7 @@ def gen_testdata(c, s): hist = jitter_under_thresh(gen_testdata(-0.1, 2), "1e-3 mm/d") sim = gen_testdata(-0.15, 2.5) - EQM = EmpiricalQuantileMapping.train( - ref, hist, group="time.dayofyear", nquantiles=15, kind="*" - ) + EQM = EmpiricalQuantileMapping.train(ref, hist, group="time.dayofyear", nquantiles=15, kind="*") scen = EQM.adjust(sim) @@ -819,21 +778,15 @@ def gen_testdata(c, s): # What to test??? # Test if extreme values of sim are still extreme exval = sim > EX.ds.thresh - assert (scen2.where(exval) > EX.ds.thresh).sum() > ( - scen.where(exval) > EX.ds.thresh - ).sum() + assert (scen2.where(exval) > EX.ds.thresh).sum() > (scen.where(exval) > EX.ds.thresh).sum() @pytest.mark.slow def test_real_data(self, open_dataset): dsim = open_dataset("sdba/CanESM2_1950-2100.nc").chunk() dref = open_dataset("sdba/ahccd_1950-2013.nc").chunk() - ref = convert_units_to( - dref.sel(time=slice("1950", "2009")).pr, "mm/d", context="hydro" - ) - hist = convert_units_to( - dsim.sel(time=slice("1950", "2009")).pr, "mm/d", context="hydro" - ) + ref = convert_units_to(dref.sel(time=slice("1950", "2009")).pr, "mm/d", context="hydro") + hist = convert_units_to(dsim.sel(time=slice("1950", "2009")).pr, "mm/d", context="hydro") quantiles = np.linspace(0.01, 0.99, num=50) @@ -907,9 +860,7 @@ def test_compare_sbck(self, random, series): scen = OTC.adjust(ref, hist, bin_width=bin_width, jitter_inside_bins=False) otc_sbck = adjustment.SBCK_OTC - scen_sbck = otc_sbck.adjust( - ref, hist, hist, multi_dim="multivar", bin_width=bin_width - ) + scen_sbck = otc_sbck.adjust(ref, hist, hist, multi_dim="multivar", bin_width=bin_width) scen = scen.to_numpy().T scen_sbck = scen_sbck.to_numpy() @@ -1007,23 +958,15 @@ def test_different_times(self, tasmax_series, tasmin_series): # `sim` has a different time than `ref,hist` (but same size) ref = xr.merge( [ - tasmax_series(np.arange(730).astype(float), start="2000-01-01").chunk( - {"time": -1} - ), - tasmin_series(np.arange(730).astype(float), start="2000-01-01").chunk( - {"time": -1} - ), + tasmax_series(np.arange(730).astype(float), start="2000-01-01").chunk({"time": -1}), + tasmin_series(np.arange(730).astype(float), start="2000-01-01").chunk({"time": -1}), ] ) hist = ref.copy() sim = xr.merge( [ - tasmax_series(np.arange(730).astype(float), start="2020-01-01").chunk( - {"time": -1} - ), - tasmin_series(np.arange(730).astype(float), start="2020-01-01").chunk( - {"time": -1} - ), + tasmax_series(np.arange(730).astype(float), start="2020-01-01").chunk({"time": -1}), + tasmin_series(np.arange(730).astype(float), start="2020-01-01").chunk({"time": -1}), ] ) ref, hist, sim = (stack_variables(arr) for arr in [ref, hist, sim]) @@ -1065,36 +1008,24 @@ def test_sbck(self, method, use_dask, random): ref = xr.Dataset( { - "tasmin": xr.DataArray( - ref_x, dims=("lon", "time"), attrs={"units": "degC"} - ), - "tasmax": xr.DataArray( - ref_y, dims=("lon", "time"), attrs={"units": "degC"} - ), + "tasmin": xr.DataArray(ref_x, dims=("lon", "time"), attrs={"units": "degC"}), + "tasmax": xr.DataArray(ref_y, dims=("lon", "time"), attrs={"units": "degC"}), } ) ref["time"] = xr.cftime_range("1990-01-01", periods=n, calendar="noleap") hist = xr.Dataset( { - "tasmin": xr.DataArray( - hist_x, dims=("lon", "time"), attrs={"units": "degC"} - ), - "tasmax": xr.DataArray( - hist_y, dims=("lon", "time"), attrs={"units": "degC"} - ), + "tasmin": xr.DataArray(hist_x, dims=("lon", "time"), attrs={"units": "degC"}), + "tasmax": xr.DataArray(hist_y, dims=("lon", "time"), attrs={"units": "degC"}), } ) hist["time"] = ref["time"] sim = xr.Dataset( { - "tasmin": xr.DataArray( - sim_x, dims=("lon", "time"), attrs={"units": "degC"} - ), - "tasmax": xr.DataArray( - sim_y, dims=("lon", "time"), attrs={"units": "degC"} - ), + "tasmin": xr.DataArray(sim_x, dims=("lon", "time"), attrs={"units": "degC"}), + "tasmax": xr.DataArray(sim_y, dims=("lon", "time"), attrs={"units": "degC"}), } ) sim["time"] = xr.cftime_range("2090-01-01", periods=n, calendar="noleap") diff --git a/tests/test_sdba/test_base.py b/tests/test_sdba/test_base.py index 5619b5d47..b942d5aa3 100644 --- a/tests/test_sdba/test_base.py +++ b/tests/test_sdba/test_base.py @@ -16,16 +16,13 @@ class ATestSubClass(Parametrizable): def test_param_class(): gr = Grouper(group="time.month") - in_params = dict( - anint=4, abool=True, astring="a string", adict={"key": "val"}, group=gr - ) + in_params = dict(anint=4, abool=True, astring="a string", adict={"key": "val"}, group=gr) obj = Parametrizable(**in_params) assert obj.parameters == in_params assert repr(obj).startswith( - "Parametrizable(anint=4, abool=True, astring='a string', adict={'key': 'val'}, " - "group=Grouper(" + "Parametrizable(anint=4, abool=True, astring='a string', adict={'key': 'val'}, group=Grouper(" ) s = jsonpickle.encode(obj) @@ -119,9 +116,7 @@ def test_grouper_apply(tas_series, use_dask, group, n): # With window win_grouper = Grouper(group, window=5) out = win_grouper.apply("mean", tas) - rolld = tas.rolling({win_grouper.dim: 5}, center=True).construct( - window_dim="window" - ) + rolld = tas.rolling({win_grouper.dim: 5}, center=True).construct(window_dim="window") if grouper.prop != "group": exp = rolld.groupby(group).mean(dim=[win_grouper.dim, "window"]) else: @@ -162,9 +157,7 @@ def mixed_reduce(grdds, dim=None): def normalize_from_precomputed(grpds, dim=None): return (grpds.tas / grpds.tas1_mean).mean(dim=dim) - out = grouper.apply( - normalize_from_precomputed, {"tas": tas, "tas1_mean": out.tas1_mean} - ).isel(lat=0) + out = grouper.apply(normalize_from_precomputed, {"tas": tas, "tas1_mean": out.tas1_mean}).isel(lat=0) if grouper.prop == "group": exp = normed.mean("time").isel(lat=0) else: diff --git a/tests/test_sdba/test_detrending.py b/tests/test_sdba/test_detrending.py index 99a7cca4f..b4693e07c 100644 --- a/tests/test_sdba/test_detrending.py +++ b/tests/test_sdba/test_detrending.py @@ -79,16 +79,10 @@ def test_rollingmean_detrend(series): x = xr.DataArray( np.sin(2 * np.pi * np.arange(11 * 365) / 365), dims=("time",), - coords={ - "time": xr.cftime_range( - "2010-01-01", periods=11 * 365, freq="D", calendar="noleap" - ) - }, + coords={"time": xr.cftime_range("2010-01-01", periods=11 * 365, freq="D", calendar="noleap")}, ) w = windows.get_window("triang", 11, False) - det = RollingMeanDetrend( - group=Grouper("time.dayofyear", window=3), win=11, weights=w - ) + det = RollingMeanDetrend(group=Grouper("time.dayofyear", window=3), win=11, weights=w) fx = det.fit(x) assert fx.ds.trend.notnull().sum() == 365 diff --git a/tests/test_sdba/test_measures.py b/tests/test_sdba/test_measures.py index 2235251d6..cd75c3155 100644 --- a/tests/test_sdba/test_measures.py +++ b/tests/test_sdba/test_measures.py @@ -22,12 +22,8 @@ def test_relative_bias(open_dataset): def test_circular_bias(): - sim = xr.DataArray( - data=np.array([1, 1, 1, 2, 365, 300]), attrs={"units": "", "long_name": "test"} - ) - ref = xr.DataArray( - data=np.array([2, 365, 300, 1, 1, 1]), attrs={"units": "", "long_name": "test"} - ) + sim = xr.DataArray(data=np.array([1, 1, 1, 2, 365, 300]), attrs={"units": "", "long_name": "test"}) + ref = xr.DataArray(data=np.array([2, 365, 300, 1, 1, 1]), attrs={"units": "", "long_name": "test"}) test = sdba.measures.circular_bias(sim, ref).values np.testing.assert_array_almost_equal(test, [1, 1, 66, -1, -1, -66]) @@ -40,33 +36,23 @@ def test_ratio(open_dataset): def test_rmse(open_dataset): - sim = ( - open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1953")).tasmax - ) + sim = open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1953")).tasmax ref = open_dataset("sdba/nrcan_1950-2013.nc").sel(time=slice("1950", "1953")).tasmax test = sdba.measures.rmse(sim, ref).values np.testing.assert_array_almost_equal(test, [5.4499755, 18.124086, 12.387193], 4) def test_mae(open_dataset): - sim = ( - open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1953")).tasmax - ) + sim = open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1953")).tasmax ref = open_dataset("sdba/nrcan_1950-2013.nc").sel(time=slice("1950", "1953")).tasmax test = sdba.measures.mae(sim, ref).values np.testing.assert_array_almost_equal(test, [4.159672, 14.2148, 9.768536], 4) def test_annual_cycle_correlation(open_dataset): - sim = ( - open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1953")).tasmax - ) + sim = open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1953")).tasmax ref = open_dataset("sdba/nrcan_1950-2013.nc").sel(time=slice("1950", "1953")).tasmax - test = ( - sdba.measures.annual_cycle_correlation(sim, ref, window=31) - .sel(location="Vancouver") - .values - ) + test = sdba.measures.annual_cycle_correlation(sim, ref, window=31).sel(location="Vancouver").values np.testing.assert_array_almost_equal(test, [0.94580488], 4) @@ -80,16 +66,8 @@ def test_scorr(open_dataset): def test_taylordiagram(open_dataset): - sim = ( - open_dataset("sdba/CanESM2_1950-2100.nc") - .sel(time=slice("1950", "1953"), location="Amos") - .tasmax - ) - ref = ( - open_dataset("sdba/nrcan_1950-2013.nc") - .sel(time=slice("1950", "1953"), location="Amos") - .tasmax - ) + sim = open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1953"), location="Amos").tasmax + ref = open_dataset("sdba/nrcan_1950-2013.nc").sel(time=slice("1950", "1953"), location="Amos").tasmax test = sdba.measures.taylordiagram(sim, ref).values np.testing.assert_array_almost_equal(test, [13.12244701, 6.76166582, 0.73230199], 4) diff --git a/tests/test_sdba/test_nbutils.py b/tests/test_sdba/test_nbutils.py index 20ece1e21..d229f8f70 100644 --- a/tests/test_sdba/test_nbutils.py +++ b/tests/test_sdba/test_nbutils.py @@ -10,9 +10,7 @@ class TestQuantiles: @pytest.mark.parametrize("uses_dask", [True, False]) def test_quantile(self, open_dataset, uses_dask): - da = ( - open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1955")).pr - ).load() + da = (open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1955")).pr).load() if uses_dask: da = da.chunk({"location": 1}) else: diff --git a/tests/test_sdba/test_processing.py b/tests/test_sdba/test_processing.py index efc1078e4..9096a9192 100644 --- a/tests/test_sdba/test_processing.py +++ b/tests/test_sdba/test_processing.py @@ -47,8 +47,7 @@ def test_jitter_under_thresh(): assert da[0] > 0 np.testing.assert_allclose(da[1:], out[1:]) assert ( - "jitter(x=, lower='1 K', upper=None, minimum=None, maximum=None) - xclim version" - in out.attrs["history"] + "jitter(x=, lower='1 K', upper=None, minimum=None, maximum=None) - xclim version" in out.attrs["history"] ) @@ -90,12 +89,7 @@ def test_adapt_freq(use_dask, random): np.testing.assert_allclose(dP0_out, 0.5, atol=0.1) # Assert that corrected values were generated in the range ]1, 20 + tol[ - corrected = ( - input_zeros.where(input_zeros > 1) - .stack(flat=["lat", "time"]) - .reset_index("flat") - .dropna("flat") - ) + corrected = input_zeros.where(input_zeros > 1).stack(flat=["lat", "time"]).reset_index("flat").dropna("flat") assert ((corrected < 20.1) & (corrected > 1)).all() # Assert that non-corrected values are untouched @@ -186,9 +180,9 @@ def test_reordering(): def test_reordering_with_window(): - time = list( - xr.date_range("2000-01-01", "2000-01-04", freq="D", calendar="noleap") - ) + list(xr.date_range("2001-01-01", "2001-01-04", freq="D", calendar="noleap")) + time = list(xr.date_range("2000-01-01", "2000-01-04", freq="D", calendar="noleap")) + list( + xr.date_range("2001-01-01", "2001-01-04", freq="D", calendar="noleap") + ) x = xr.DataArray( np.arange(1, 9, 1), @@ -230,23 +224,15 @@ def test_to_additive(pr_series, hurs_series): # logit hurs = hurs_series(np.array([0, 1e-3, 90, 100])) - hurslogit = to_additive_space( - hurs, lower_bound="0 %", trans="logit", upper_bound="100 %" - ) - np.testing.assert_allclose( - hurslogit, [-np.inf, -11.5129154649, 2.197224577, np.inf] - ) + hurslogit = to_additive_space(hurs, lower_bound="0 %", trans="logit", upper_bound="100 %") + np.testing.assert_allclose(hurslogit, [-np.inf, -11.5129154649, 2.197224577, np.inf]) assert hurslogit.attrs["sdba_transform"] == "logit" assert hurslogit.attrs["sdba_transform_units"] == "%" with xr.set_options(keep_attrs=True): hursscl = hurs * 4 + 200 - hurslogit2 = to_additive_space( - hursscl, trans="logit", lower_bound="2", upper_bound="6" - ) - np.testing.assert_allclose( - hurslogit2, [-np.inf, -11.5129154649, 2.197224577, np.inf] - ) + hurslogit2 = to_additive_space(hursscl, trans="logit", lower_bound="2", upper_bound="6") + np.testing.assert_allclose(hurslogit2, [-np.inf, -11.5129154649, 2.197224577, np.inf]) assert hurslogit2.attrs["sdba_transform_lower"] == 200.0 assert hurslogit2.attrs["sdba_transform_upper"] == 600.0 @@ -255,25 +241,19 @@ def test_from_additive(pr_series, hurs_series): # log pr = pr_series(np.array([0, 1e-5, 1, np.e**10])) with units.context("hydro"): - pr2 = from_additive_space( - to_additive_space(pr, lower_bound="0 mm/d", trans="log") - ) + pr2 = from_additive_space(to_additive_space(pr, lower_bound="0 mm/d", trans="log")) np.testing.assert_allclose(pr[1:], pr2[1:]) pr2.attrs.pop("history") assert pr.attrs == pr2.attrs # logit hurs = hurs_series(np.array([0, 1e-5, 0.9, 1])) - hurs2 = from_additive_space( - to_additive_space(hurs, lower_bound="0 %", trans="logit", upper_bound="100 %") - ) + hurs2 = from_additive_space(to_additive_space(hurs, lower_bound="0 %", trans="logit", upper_bound="100 %")) np.testing.assert_allclose(hurs[1:-1], hurs2[1:-1]) def test_normalize(tas_series, random): - tas = tas_series( - random.standard_normal((int(365.25 * 36),)) + 273.15, start="2000-01-01" - ) + tas = tas_series(random.standard_normal((int(365.25 * 36),)) + 273.15, start="2000-01-01") xp, norm = normalize(tas, group="time.dayofyear") np.testing.assert_allclose(norm, 273.15, atol=1) diff --git a/tests/test_sdba/test_properties.py b/tests/test_sdba/test_properties.py index 1caadeb1a..670137c20 100644 --- a/tests/test_sdba/test_properties.py +++ b/tests/test_sdba/test_properties.py @@ -13,9 +13,7 @@ class TestProperties: def test_mean(self, open_dataset): sim = ( - open_dataset("sdba/CanESM2_1950-2100.nc") - .sel(time=slice("1950", "1980"), location="Vancouver") - .pr + open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1980"), location="Vancouver").pr ).load() out_year = sdba.properties.mean(sim) @@ -31,9 +29,7 @@ def test_mean(self, open_dataset): def test_var(self, open_dataset): sim = ( - open_dataset("sdba/CanESM2_1950-2100.nc") - .sel(time=slice("1950", "1980"), location="Vancouver") - .pr + open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1980"), location="Vancouver").pr ).load() out_year = sdba.properties.var(sim) @@ -49,9 +45,7 @@ def test_var(self, open_dataset): def test_std(self, open_dataset): sim = ( - open_dataset("sdba/CanESM2_1950-2100.nc") - .sel(time=slice("1950", "1980"), location="Vancouver") - .pr + open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1980"), location="Vancouver").pr ).load() out_year = sdba.properties.std(sim) @@ -67,9 +61,7 @@ def test_std(self, open_dataset): def test_skewness(self, open_dataset): sim = ( - open_dataset("sdba/CanESM2_1950-2100.nc") - .sel(time=slice("1950", "1980"), location="Vancouver") - .pr + open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1980"), location="Vancouver").pr ).load() out_year = sdba.properties.skewness(sim) @@ -90,9 +82,7 @@ def test_skewness(self, open_dataset): def test_quantile(self, open_dataset): sim = ( - open_dataset("sdba/CanESM2_1950-2100.nc") - .sel(time=slice("1950", "1980"), location="Vancouver") - .pr + open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1980"), location="Vancouver").pr ).load() out_year = sdba.properties.quantile(sim, q=0.2) @@ -112,37 +102,25 @@ def test_quantile(self, open_dataset): # TODO: test theshold_count? it's the same a test_spell_length_distribution def test_spell_length_distribution(self, open_dataset): - ds = ( - open_dataset("sdba/CanESM2_1950-2100.nc") - .sel(time=slice("1950", "1952"), location="Vancouver") - .load() - ) + ds = open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1952"), location="Vancouver").load() # test pr, with amount method sim = ds.pr kws = {"op": "<", "group": "time.month"} outd = { - stat: sdba.properties.spell_length_distribution(da=sim, **kws, stat=stat) - .sel(month=1) - .values + stat: sdba.properties.spell_length_distribution(da=sim, **kws, stat=stat).sel(month=1).values for stat in ["mean", "max", "min"] } - np.testing.assert_array_almost_equal( - [outd[k] for k in ["mean", "max", "min"]], [2.44127, 10, 1] - ) + np.testing.assert_array_almost_equal([outd[k] for k in ["mean", "max", "min"]], [2.44127, 10, 1]) # test tasmax, with quantile method simt = ds.tasmax kws = {"thresh": 0.9, "op": ">=", "method": "quantile", "group": "time.month"} outd = { - stat: sdba.properties.spell_length_distribution( - da=simt, **kws, stat=stat - ).sel(month=6) + stat: sdba.properties.spell_length_distribution(da=simt, **kws, stat=stat).sel(month=6) for stat in ["mean", "max", "min"] } - np.testing.assert_array_almost_equal( - [outd[k].values for k in ["mean", "max", "min"]], [3.0, 6, 1] - ) + np.testing.assert_array_almost_equal([outd[k].values for k in ["mean", "max", "min"]], [3.0, 6, 1]) # test varia with pytest.raises( @@ -157,7 +135,6 @@ def test_spell_length_distribution(self, open_dataset): ) def test_spell_length_distribution_mixed_stat(self, open_dataset): - time = pd.date_range("2000-01-01", periods=2 * 365, freq="D") tas = xr.DataArray( np.array([0] * 365 + [40] * 365), @@ -166,13 +143,9 @@ def test_spell_length_distribution_mixed_stat(self, open_dataset): attrs={"units": "degC"}, ) - kws_sum = dict( - thresh="30 degC", op=">=", stat="sum", stat_resample="sum", group="time" - ) + kws_sum = dict(thresh="30 degC", op=">=", stat="sum", stat_resample="sum", group="time") out_sum = sdba.properties.spell_length_distribution(tas, **kws_sum).values - kws_mixed = dict( - thresh="30 degC", op=">=", stat="mean", stat_resample="sum", group="time" - ) + kws_mixed = dict(thresh="30 degC", op=">=", stat="mean", stat_resample="sum", group="time") out_mixed = sdba.properties.spell_length_distribution(tas, **kws_mixed).values assert out_sum == 365 @@ -185,14 +158,8 @@ def test_spell_length_distribution_mixed_stat(self, open_dataset): (3, [1.333333, 4, 0], [2, 6, 0]), ], ) - def test_bivariate_spell_length_distribution( - self, open_dataset, window, expected_amount, expected_quantile - ): - ds = ( - open_dataset("sdba/CanESM2_1950-2100.nc").sel( - time=slice("1950", "1952"), location="Vancouver" - ) - ).load() + def test_bivariate_spell_length_distribution(self, open_dataset, window, expected_amount, expected_quantile): + ds = (open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1952"), location="Vancouver")).load() tx = ds.tasmax with set_options(keep_attrs=True): tn = tx - 5 @@ -207,16 +174,12 @@ def test_bivariate_spell_length_distribution( "window": window, } outd = { - stat: sdba.properties.bivariate_spell_length_distribution( - da1=tx, da2=tn, **kws, stat=stat - ) + stat: sdba.properties.bivariate_spell_length_distribution(da1=tx, da2=tn, **kws, stat=stat) .sel(month=1) .values for stat in ["mean", "max", "min"] } - np.testing.assert_array_almost_equal( - [outd[k] for k in ["mean", "max", "min"]], expected_amount - ) + np.testing.assert_array_almost_equal([outd[k] for k in ["mean", "max", "min"]], expected_amount) # test with quantile method kws = { @@ -230,22 +193,16 @@ def test_bivariate_spell_length_distribution( "window": window, } outd = { - stat: sdba.properties.bivariate_spell_length_distribution( - da1=tx, da2=tn, **kws, stat=stat - ) + stat: sdba.properties.bivariate_spell_length_distribution(da1=tx, da2=tn, **kws, stat=stat) .sel(month=6) .values for stat in ["mean", "max", "min"] } - np.testing.assert_array_almost_equal( - [outd[k] for k in ["mean", "max", "min"]], expected_quantile - ) + np.testing.assert_array_almost_equal([outd[k] for k in ["mean", "max", "min"]], expected_quantile) def test_acf(self, open_dataset): sim = ( - open_dataset("sdba/CanESM2_1950-2100.nc") - .sel(time=slice("1950", "1952"), location="Vancouver") - .pr + open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1952"), location="Vancouver").pr ).load() out = sdba.properties.acf(sim, lag=1, group="time.month").sel(month=1) @@ -259,9 +216,7 @@ def test_acf(self, open_dataset): def test_annual_cycle(self, open_dataset): simt = ( - open_dataset("sdba/CanESM2_1950-2100.nc") - .sel(time=slice("1950", "1952"), location="Vancouver") - .tasmax + open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1952"), location="Vancouver").tasmax ).load() amp = sdba.properties.annual_cycle_amplitude(simt) @@ -294,9 +249,7 @@ def test_annual_cycle(self, open_dataset): def test_annual_range(self, open_dataset): simt = ( - open_dataset("sdba/CanESM2_1950-2100.nc") - .sel(time=slice("1950", "1952"), location="Vancouver") - .tasmax + open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1952"), location="Vancouver").tasmax ).load() # Initial annual cycle was this with window = 1 @@ -338,28 +291,18 @@ def test_annual_range(self, open_dataset): def test_corr_btw_var(self, open_dataset): simt = ( - open_dataset("sdba/CanESM2_1950-2100.nc") - .sel(time=slice("1950", "1952"), location="Vancouver") - .tasmax + open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1952"), location="Vancouver").tasmax ).load() sim = ( - open_dataset("sdba/CanESM2_1950-2100.nc") - .sel(time=slice("1950", "1952"), location="Vancouver") - .pr + open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1952"), location="Vancouver").pr ).load() pc = sdba.properties.corr_btw_var(simt, sim, corr_type="Pearson") - pp = sdba.properties.corr_btw_var( - simt, sim, corr_type="Pearson", output="pvalue" - ).values + pp = sdba.properties.corr_btw_var(simt, sim, corr_type="Pearson", output="pvalue").values sc = sdba.properties.corr_btw_var(simt, sim).values sp = sdba.properties.corr_btw_var(simt, sim, output="pvalue").values - sc_jan = ( - sdba.properties.corr_btw_var(simt, sim, group="time.month") - .sel(month=1) - .values - ) + sc_jan = sdba.properties.corr_btw_var(simt, sim, group="time.month").sel(month=1).values sim[0] = np.nan pc_nan = sdba.properties.corr_btw_var(sim, simt, corr_type="Pearson").values @@ -385,48 +328,31 @@ def test_corr_btw_var(self, open_dataset): def test_relative_frequency(self, open_dataset): sim = ( - open_dataset("sdba/CanESM2_1950-2100.nc") - .sel(time=slice("1950", "1952"), location="Vancouver") - .pr + open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1952"), location="Vancouver").pr ).load() test = sdba.properties.relative_frequency(sim, thresh="25 mm d-1", op=">=") testjan = ( - sdba.properties.relative_frequency( - sim, thresh="25 mm d-1", op=">=", group="time.month" - ) - .sel(month=1) - .values - ) - np.testing.assert_array_almost_equal( - [test.values, testjan], [0.0045662100456621, 0.010752688172043012] + sdba.properties.relative_frequency(sim, thresh="25 mm d-1", op=">=", group="time.month").sel(month=1).values ) + np.testing.assert_array_almost_equal([test.values, testjan], [0.0045662100456621, 0.010752688172043012]) assert test.long_name == "Relative frequency of values >= 25 mm d-1." assert test.units == "" def test_transition(self, open_dataset): sim = ( - open_dataset("sdba/CanESM2_1950-2100.nc") - .sel(time=slice("1950", "1952"), location="Vancouver") - .pr + open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1952"), location="Vancouver").pr ).load() - test = sdba.properties.transition_probability( - da=sim, initial_op="<", final_op=">=" - ) + test = sdba.properties.transition_probability(da=sim, initial_op="<", final_op=">=") np.testing.assert_array_almost_equal([test.values], [0.14076782449725778]) - assert ( - test.long_name - == "Transition probability of values < 1 mm d-1 to values >= 1 mm d-1." - ) + assert test.long_name == "Transition probability of values < 1 mm d-1 to values >= 1 mm d-1." assert test.units == "" def test_trend(self, open_dataset): simt = ( - open_dataset("sdba/CanESM2_1950-2100.nc") - .sel(time=slice("1950", "1952"), location="Vancouver") - .tasmax + open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "1952"), location="Vancouver").tasmax ).load() slope = sdba.properties.trend(simt).values @@ -450,30 +376,12 @@ def test_trend(self, open_dataset): ) slope = sdba.properties.trend(simt, group="time.month").sel(month=1) - intercept = ( - sdba.properties.trend(simt, output="intercept", group="time.month") - .sel(month=1) - .values - ) - rvalue = ( - sdba.properties.trend(simt, output="rvalue", group="time.month") - .sel(month=1) - .values - ) - pvalue = ( - sdba.properties.trend(simt, output="pvalue", group="time.month") - .sel(month=1) - .values - ) - stderr = ( - sdba.properties.trend(simt, output="stderr", group="time.month") - .sel(month=1) - .values - ) + intercept = sdba.properties.trend(simt, output="intercept", group="time.month").sel(month=1).values + rvalue = sdba.properties.trend(simt, output="rvalue", group="time.month").sel(month=1).values + pvalue = sdba.properties.trend(simt, output="pvalue", group="time.month").sel(month=1).values + stderr = sdba.properties.trend(simt, output="stderr", group="time.month").sel(month=1).values intercept_stderr = ( - sdba.properties.trend(simt, output="intercept_stderr", group="time.month") - .sel(month=1) - .values + sdba.properties.trend(simt, output="intercept_stderr", group="time.month").sel(month=1).values ) np.testing.assert_array_almost_equal( @@ -494,42 +402,28 @@ def test_trend(self, open_dataset): def test_return_value(self, open_dataset): simt = ( - open_dataset("sdba/CanESM2_1950-2100.nc") - .sel(time=slice("1950", "2010"), location="Vancouver") - .tasmax + open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1950", "2010"), location="Vancouver").tasmax ).load() out_y = sdba.properties.return_value(simt) - out_djf = ( - sdba.properties.return_value(simt, op="min", group="time.season") - .sel(season="DJF") - .values - ) + out_djf = sdba.properties.return_value(simt, op="min", group="time.season").sel(season="DJF").values - np.testing.assert_array_almost_equal( - [out_y.values, out_djf], [313.154, 278.072], 3 - ) + np.testing.assert_array_almost_equal([out_y.values, out_djf], [313.154, 278.072], 3) assert out_y.long_name.startswith("20-year maximal return level") @pytest.mark.slow def test_spatial_correlogram(self, open_dataset): # This also tests sdba.utils._pairwise_spearman and sdba.nbutils._pairwise_haversine_and_bins # Test 1, does it work with 1D data? - sim = ( - open_dataset("sdba/CanESM2_1950-2100.nc") - .sel(time=slice("1981", "2010")) - .tasmax - ).load() + sim = (open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1981", "2010")).tasmax).load() out = sdba.properties.spatial_correlogram(sim, dims=["location"], bins=3) np.testing.assert_allclose(out, [-1, np.nan, 0], atol=1e-6) # Test 2, not very exhaustive, this is more of a detect-if-we-break-it test. sim = open_dataset("NRCANdaily/nrcan_canada_daily_tasmax_1990.nc").tasmax - out = sdba.properties.spatial_correlogram( - sim.isel(lon=slice(0, 50)), dims=["lon", "lat"], bins=20 - ) + out = sdba.properties.spatial_correlogram(sim.isel(lon=slice(0, 50)), dims=["lon", "lat"], bins=20) np.testing.assert_allclose( out[:5], [0.95099902, 0.83028772, 0.66874473, 0.48893958, 0.30915054], @@ -548,9 +442,7 @@ def test_decorrelation_length(self, open_dataset): .load() ) - out = sdba.properties.decorrelation_length( - sim, dims=["lat", "lon"], bins=10, radius=30 - ) + out = sdba.properties.decorrelation_length(sim, dims=["lat", "lon"], bins=10, radius=30) np.testing.assert_allclose( out[0], [4.5, 4.5, 4.5, 4.5, 10.5], @@ -558,16 +450,10 @@ def test_decorrelation_length(self, open_dataset): def test_get_measure(self, open_dataset): sim = ( - open_dataset("sdba/CanESM2_1950-2100.nc") - .sel(time=slice("1981", "2010"), location="Vancouver") - .pr + open_dataset("sdba/CanESM2_1950-2100.nc").sel(time=slice("1981", "2010"), location="Vancouver").pr ).load() - ref = ( - open_dataset("sdba/ahccd_1950-2013.nc") - .sel(time=slice("1981", "2010"), location="Vancouver") - .pr - ).load() + ref = (open_dataset("sdba/ahccd_1950-2013.nc").sel(time=slice("1981", "2010"), location="Vancouver").pr).load() sim = convert_units_to(sim, ref, context="hydro") sim_var = sdba.properties.var(sim) diff --git a/tests/test_sdba/test_sdba_utils.py b/tests/test_sdba/test_sdba_utils.py index 48fe0bdcc..6c08820fd 100644 --- a/tests/test_sdba/test_sdba_utils.py +++ b/tests/test_sdba/test_sdba_utils.py @@ -65,9 +65,7 @@ def test_equally_spaced_nodes(): np.testing.assert_almost_equal(x[0], 0.5) -@pytest.mark.parametrize( - "interp,expi", [("nearest", 2.9), ("linear", 2.95), ("cubic", 2.95)] -) +@pytest.mark.parametrize("interp,expi", [("nearest", 2.9), ("linear", 2.95), ("cubic", 2.95)]) @pytest.mark.parametrize("extrap,expe", [("constant", 4.4), ("nan", np.nan)]) def test_interp_on_quantiles_constant(interp, expi, extrap, expe): quantiles = np.linspace(0, 1, num=25) @@ -94,9 +92,7 @@ def test_interp_on_quantiles_constant(interp, expi, extrap, expe): yq = yq.expand_dims(lat=[1, 2, 3]) newx = newx.expand_dims(lat=[1, 2, 3]) - out = u.interp_on_quantiles( - newx, xq, yq, group="time", method=interp, extrapolation=extrap - ) + out = u.interp_on_quantiles(newx, xq, yq, group="time", method=interp, extrapolation=extrap) if np.isnan(expe): assert out.isel(time=0).isnull().all() @@ -107,9 +103,7 @@ def test_interp_on_quantiles_constant(interp, expi, extrap, expe): xq = xq.where(xq != 220) yq = yq.where(yq != 3) - out = u.interp_on_quantiles( - newx, xq, yq, group="time", method=interp, extrapolation=extrap - ) + out = u.interp_on_quantiles(newx, xq, yq, group="time", method=interp, extrapolation=extrap) if np.isnan(expe): assert out.isel(time=0).isnull().all() @@ -154,15 +148,11 @@ def test_interp_on_quantiles_monthly(random): af = u.get_correction(hist_q, ref_q, "+") for interp in ["nearest", "linear", "cubic"]: - afi = u.interp_on_quantiles( - sim, hist_q, af, group="time.month", method=interp, extrapolation="constant" - ) + afi = u.interp_on_quantiles(sim, hist_q, af, group="time.month", method=interp, extrapolation="constant") assert afi.isnull().sum("time") == 0, interp -@pytest.mark.parametrize( - "interp,expi", [("nearest", 2.9), ("linear", 2.95), ("cubic", 2.95)] -) +@pytest.mark.parametrize("interp,expi", [("nearest", 2.9), ("linear", 2.95), ("cubic", 2.95)]) @pytest.mark.parametrize("extrap,expe", [("constant", 4.4), ("nan", np.nan)]) def test_interp_on_quantiles_constant_with_nan(interp, expi, extrap, expe): quantiles = np.linspace(0, 1, num=30) @@ -189,9 +179,7 @@ def test_interp_on_quantiles_constant_with_nan(interp, expi, extrap, expe): yq = yq.expand_dims(lat=[1, 2, 3]) newx = newx.expand_dims(lat=[1, 2, 3]) - out = u.interp_on_quantiles( - newx, xq, yq, group="time", method=interp, extrapolation=extrap - ) + out = u.interp_on_quantiles(newx, xq, yq, group="time", method=interp, extrapolation=extrap) if np.isnan(expe): assert out.isel(time=0).isnull().all() @@ -202,9 +190,7 @@ def test_interp_on_quantiles_constant_with_nan(interp, expi, extrap, expe): xq = xq.where(xq != 220) yq = yq.where(yq != 3) - out = u.interp_on_quantiles( - newx, xq, yq, group="time", method=interp, extrapolation=extrap - ) + out = u.interp_on_quantiles(newx, xq, yq, group="time", method=interp, extrapolation=extrap) if np.isnan(expe): assert out.isel(time=0).isnull().all() diff --git a/tests/test_snow.py b/tests/test_snow.py index 395faadbc..f17e69c61 100644 --- a/tests/test_snow.py +++ b/tests/test_snow.py @@ -26,9 +26,7 @@ def test_simple(self, snd_series): class TestSnowWaterCoverDuration: - @pytest.mark.parametrize( - "factor,exp", ([1000, [31, 28, 31, np.nan]], [0, [0, 0, 0, np.nan]]) - ) + @pytest.mark.parametrize("factor,exp", ([1000, [31, 28, 31, np.nan]], [0, [0, 0, 0, np.nan]])) def test_simple(self, snw_series, factor, exp): snw = snw_series(np.ones(110) * factor, start="2001-01-01") out = land.snw_days_above(snw, freq="ME") @@ -124,13 +122,8 @@ def test_simple(self, snw_series): class TestHolidaySnowIndicators: - def test_xmas_days_simple(self, nimbus): - ds = xr.open_dataset( - nimbus.fetch( - "cmip6/snw_day_CanESM5_historical_r1i1p1f1_gn_19910101-20101231.nc" - ) - ) + ds = xr.open_dataset(nimbus.fetch("cmip6/snw_day_CanESM5_historical_r1i1p1f1_gn_19910101-20101231.nc")) snd = land.snw_to_snd(ds.snw) out = land.holiday_snow_days(snd) @@ -150,16 +143,8 @@ def test_xmas_days_simple(self, nimbus): ) def test_perfect_xmas_days_simple(self, nimbus): - ds_snw = xr.open_dataset( - nimbus.fetch( - "cmip6/snw_day_CanESM5_historical_r1i1p1f1_gn_19910101-20101231.nc" - ) - ) - ds_prsn = xr.open_dataset( - nimbus.fetch( - "cmip6/prsn_day_CanESM5_historical_r1i1p1f1_gn_19910101-20101231.nc" - ) - ) + ds_snw = xr.open_dataset(nimbus.fetch("cmip6/snw_day_CanESM5_historical_r1i1p1f1_gn_19910101-20101231.nc")) + ds_prsn = xr.open_dataset(nimbus.fetch("cmip6/prsn_day_CanESM5_historical_r1i1p1f1_gn_19910101-20101231.nc")) snd = land.snw_to_snd(ds_snw.snw) prsn = ds_prsn.prsn diff --git a/tests/test_stats.py b/tests/test_stats.py index 96289d717..f3da4700a 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -79,9 +79,7 @@ def weibull_min(request): ], dims=("time",), ) - da = da.assign_coords( - time=xr.cftime_range("2045-02-02", periods=da.time.size, freq="D") - ) + da = da.assign_coords(time=xr.cftime_range("2045-02-02", periods=da.time.size, freq="D")) if request.param: da = da.chunk() @@ -115,9 +113,7 @@ def genextreme(request): ], dims=("time",), ) - da = da.assign_coords( - time=xr.cftime_range("2045-02-02", periods=da.time.size, freq="D") - ) + da = da.assign_coords(time=xr.cftime_range("2045-02-02", periods=da.time.size, freq="D")) if request.param: da = da.chunk() @@ -173,9 +169,7 @@ def test_mse_fit(genextreme): bounds=dict(c=(0, 1), scale=(0, 100), loc=(200, 400)), optimizer=optimizer, ) - np.testing.assert_allclose( - p, (0.18435517630019815, 293.61049928703073, 86.70937297745427), 1e-3 - ) + np.testing.assert_allclose(p, (0.18435517630019815, 293.61049928703073, 86.70937297745427), 1e-3) def test_fa(fitda): @@ -293,9 +287,7 @@ def test_frequency_analysis(ndq_series, use_dask): if use_dask: q = q.chunk() - out = stats.frequency_analysis( - q, mode="max", t=2, dist="genextreme", window=6, freq="YS" - ) + out = stats.frequency_analysis(q, mode="max", t=2, dist="genextreme", window=6, freq="YS") assert out.dims == ("return_period", "x", "y") assert out.shape == (1, 2, 3) v = out.values @@ -305,9 +297,7 @@ def test_frequency_analysis(ndq_series, use_dask): assert out.units == "m3 s-1" # smoke test when time is not the first dimension - stats.frequency_analysis( - q.transpose(), mode="max", t=2, dist="genextreme", window=6, freq="YS" - ) + stats.frequency_analysis(q.transpose(), mode="max", t=2, dist="genextreme", window=6, freq="YS") @pytest.mark.parametrize("use_dask", [True, False]) @@ -319,12 +309,8 @@ def test_frequency_analysis_lmoments(ndq_series, use_dask): if use_dask: q = q.chunk() - out = stats.frequency_analysis( - q, mode="max", t=2, dist="genextreme", window=6, freq="YS" - ) - out1 = stats.frequency_analysis( - q, mode="max", t=2, dist=lmom.gev, window=6, freq="YS", method="PWM" - ) + out = stats.frequency_analysis(q, mode="max", t=2, dist="genextreme", window=6, freq="YS") + out1 = stats.frequency_analysis(q, mode="max", t=2, dist=lmom.gev, window=6, freq="YS", method="PWM") np.testing.assert_allclose( out1, out, @@ -381,12 +367,8 @@ def test_parametric_cdf(use_dask, random): def test_dist_method(fitda): params = stats.fit(fitda, "lognorm") - cdf = stats.dist_method( - "cdf", fit_params=params, arg=xr.DataArray([0.2, 0.8], dims="val") - ) + cdf = stats.dist_method("cdf", fit_params=params, arg=xr.DataArray([0.2, 0.8], dims="val")) assert tuple(cdf.dims) == ("val", "x", "y") with pytest.raises(ValueError): - stats.dist_method( - "nnlf", fit_params=params, dims="val", x=xr.DataArray([0.2, 0.8]) - ) + stats.dist_method("nnlf", fit_params=params, dims="val", x=xr.DataArray([0.2, 0.8])) diff --git a/tests/test_temperature.py b/tests/test_temperature.py index e0585bf3d..f0fa2c038 100644 --- a/tests/test_temperature.py +++ b/tests/test_temperature.py @@ -21,11 +21,7 @@ class TestCSDI: def test_simple(self, tasmin_series, random): i = 3650 A = 10.0 - tn = ( - np.zeros(i) - + A * np.sin(np.arange(i) / 365.0 * 2 * np.pi) - + 0.1 * random.random(i) - ) + tn = np.zeros(i) + A * np.sin(np.arange(i) / 365.0 * 2 * np.pi) + 0.1 * random.random(i) tn += K2C tn[10:20] -= 2 tn = tasmin_series(tn) @@ -37,11 +33,7 @@ def test_simple(self, tasmin_series, random): def test_convert_units(self, tasmin_series, random): i = 3650 A = 10.0 - tn = ( - np.zeros(i) - + A * np.sin(np.arange(i) / 365.0 * 2 * np.pi) - + 0.1 * random.random(i) - ) + tn = np.zeros(i) + A * np.sin(np.arange(i) / 365.0 * 2 * np.pi) + 0.1 * random.random(i) tn[10:20] -= 2 tn = tasmin_series(tn + K2C) tn.attrs["units"] = "C" @@ -53,12 +45,7 @@ def test_convert_units(self, tasmin_series, random): def test_nan_presence(self, tasmin_series, random): i = 3650 A = 10.0 - tn = ( - np.zeros(i) - + K2C - + A * np.sin(np.arange(i) / 365.0 * 2 * np.pi) - + 0.1 * random.random(i) - ) + tn = np.zeros(i) + K2C + A * np.sin(np.arange(i) / 365.0 * 2 * np.pi) + 0.1 * random.random(i) tn[10:20] -= 2 tn[9] = np.nan tn = tasmin_series(tn) @@ -224,9 +211,7 @@ def test_TX_3d_data(self, open_dataset): 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( @@ -274,9 +259,7 @@ def test_TN_3d_data(self, open_dataset): 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 always greater than min (non nan values only) assert np.all(tnmax.values[no_nan] > tnmean.values[no_nan]) & np.all( @@ -629,15 +612,11 @@ def test_1d(self, tasmax_series, tasmin_series): ) np.testing.assert_allclose(hsf.values[:1], 2) - hsf = atmos.heat_spell_frequency( - tn, tx, thresh_tasmin="22 C", thresh_tasmax="30 C", window=5, freq="YS" - ) + hsf = atmos.heat_spell_frequency(tn, tx, thresh_tasmin="22 C", thresh_tasmax="30 C", window=5, freq="YS") np.testing.assert_allclose(hsf.values[:1], 1) # no hs - hsf = atmos.heat_spell_frequency( - tn, tx, thresh_tasmin="40 C", thresh_tasmax="40 C", freq="YS" - ) + hsf = atmos.heat_spell_frequency(tn, tx, thresh_tasmin="40 C", thresh_tasmax="40 C", freq="YS") np.testing.assert_allclose(hsf.values[:1], 0) def test_gap(self, tasmax_series, tasmin_series): @@ -649,9 +628,7 @@ def test_gap(self, tasmax_series, tasmin_series): tn = tasmin_series(tn1 + K2C, start="1/1/2000") tx = tasmax_series(tx1 + K2C, start="1/1/2000") - hsf = atmos.heat_spell_frequency( - tn, tx, thresh_tasmin="22.1 C", thresh_tasmax="30.1 C", freq="YS", min_gap=3 - ) + hsf = atmos.heat_spell_frequency(tn, tx, thresh_tasmin="22.1 C", thresh_tasmax="30.1 C", freq="YS", min_gap=3) np.testing.assert_allclose(hsf.values[:1], 1) @@ -685,9 +662,7 @@ def test_1d(self, tasmax_series, tasmin_series): np.testing.assert_allclose(hsf.values[:1], 5) # no hs - hsf = atmos.heat_spell_max_length( - tn, tx, thresh_tasmin="40 C", thresh_tasmax="40 C", freq="YS" - ) + hsf = atmos.heat_spell_max_length(tn, tx, thresh_tasmin="40 C", thresh_tasmax="40 C", freq="YS") np.testing.assert_allclose(hsf.values[:1], 0) @@ -701,20 +676,14 @@ def test_1d(self, tasmax_series, tasmin_series): tn = tasmin_series(tn1 + K2C, start="1/1/2000") tx = tasmax_series(tx1 + K2C, start="1/1/2000") - hsf = atmos.heat_spell_total_length( - tn, tx, thresh_tasmin="22.1 C", thresh_tasmax="30.1 C", freq="YS" - ) + hsf = atmos.heat_spell_total_length(tn, tx, thresh_tasmin="22.1 C", thresh_tasmax="30.1 C", freq="YS") np.testing.assert_allclose(hsf.values[:1], 7) - hsf = atmos.heat_spell_total_length( - tn, tx, thresh_tasmin="22 C", thresh_tasmax="30 C", window=5, freq="YS" - ) + hsf = atmos.heat_spell_total_length(tn, tx, thresh_tasmin="22 C", thresh_tasmax="30 C", window=5, freq="YS") np.testing.assert_allclose(hsf.values[:1], 5) # no hs - hsf = atmos.heat_spell_total_length( - tn, tx, thresh_tasmin="40 C", thresh_tasmax="40 C", freq="YS" - ) + hsf = atmos.heat_spell_total_length(tn, tx, thresh_tasmin="40 C", thresh_tasmax="40 C", freq="YS") np.testing.assert_allclose(hsf.values[:1], 0) @@ -732,29 +701,19 @@ def test_1d(self, tasmax_series, tasmin_series): 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", freq="YS" - ) - hwfC = atmos.heat_wave_frequency( - tnC, txC, thresh_tasmin="22 C", thresh_tasmax="30 C", freq="YS" - ) + hwf = atmos.heat_wave_frequency(tn, tx, thresh_tasmin="22 C", thresh_tasmax="30 C", freq="YS") + hwfC = atmos.heat_wave_frequency(tnC, txC, thresh_tasmin="22 C", thresh_tasmax="30 C", freq="YS") 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, freq="YS" - ) + hwf = atmos.heat_wave_frequency(tn, tx, thresh_tasmin="22 C", thresh_tasmax="30 C", window=4, freq="YS") 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", freq="YS" - ) + hwf = atmos.heat_wave_frequency(tn, tx, thresh_tasmin="10 C", thresh_tasmax="10 C", freq="YS") 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", freq="YS" - ) + hwf = atmos.heat_wave_frequency(tn, tx, thresh_tasmin="40 C", thresh_tasmax="40 C", freq="YS") np.testing.assert_allclose(hwf.values[:1], 0) @@ -772,29 +731,19 @@ def test_1d(self, tasmax_series, tasmin_series): 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", freq="YS" - ) - hwfC = atmos.heat_wave_max_length( - tnC, txC, thresh_tasmin="22 C", thresh_tasmax="30 C", freq="YS" - ) + hwf = atmos.heat_wave_max_length(tn, tx, thresh_tasmin="22 C", thresh_tasmax="30 C", freq="YS") + hwfC = atmos.heat_wave_max_length(tnC, txC, thresh_tasmin="22 C", thresh_tasmax="30 C", freq="YS") 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, freq="YS" - ) + hwf = atmos.heat_wave_max_length(tn, tx, thresh_tasmin="20 C", thresh_tasmax="30 C", window=4, freq="YS") 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", freq="YS" - ) + hwf = atmos.heat_wave_max_length(tn, tx, thresh_tasmin="10 C", thresh_tasmax="10 C", freq="YS") 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", freq="YS" - ) + hwf = atmos.heat_wave_max_length(tn, tx, thresh_tasmin="40 C", thresh_tasmax="40 C", freq="YS") np.testing.assert_allclose(hwf.values[:1], 0) @@ -812,38 +761,24 @@ def test_1d(self, tasmax_series, tasmin_series): txC = tasmax_series(tx1, start="1/1/2000") txC.attrs["units"] = "C" - hwf = atmos.heat_wave_total_length( - tn, tx, thresh_tasmin="22 C", thresh_tasmax="30 C", freq="YS" - ) - hwfC = atmos.heat_wave_total_length( - tnC, txC, thresh_tasmin="22 C", thresh_tasmax="30 C", freq="YS" - ) + hwf = atmos.heat_wave_total_length(tn, tx, thresh_tasmin="22 C", thresh_tasmax="30 C", freq="YS") + hwfC = atmos.heat_wave_total_length(tnC, txC, thresh_tasmin="22 C", thresh_tasmax="30 C", freq="YS") np.testing.assert_array_equal(hwf, hwfC) np.testing.assert_allclose(hwf.values[:1], 7) - hwf = atmos.heat_wave_total_length( - tn, tx, thresh_tasmin="20 C", thresh_tasmax="30 C", window=4, freq="YS" - ) + hwf = atmos.heat_wave_total_length(tn, tx, thresh_tasmin="20 C", thresh_tasmax="30 C", window=4, freq="YS") np.testing.assert_allclose(hwf.values[:1], 5) # one long hw - hwf = atmos.heat_wave_total_length( - tn, tx, thresh_tasmin="10 C", thresh_tasmax="10 C", freq="YS" - ) + hwf = atmos.heat_wave_total_length(tn, tx, thresh_tasmin="10 C", thresh_tasmax="10 C", freq="YS") np.testing.assert_allclose(hwf.values[:1], 10) # no hw - hwf = atmos.heat_wave_total_length( - tn, tx, thresh_tasmin="40 C", thresh_tasmax="40 C", freq="YS" - ) + hwf = atmos.heat_wave_total_length(tn, tx, thresh_tasmin="40 C", thresh_tasmax="40 C", freq="YS") np.testing.assert_allclose(hwf.values[:1], 0) def test_2dthresholds(self, tasmax_series, tasmin_series): - tasmax = tasmax_series(np.arange(365) + 3, start="1/1/2001").expand_dims( - lat=np.arange(20), lon=np.arange(20) - ) - tasmin = tasmin_series(np.arange(365) + 2, start="1/1/2001").expand_dims( - lat=np.arange(20), lon=np.arange(20) - ) + tasmax = tasmax_series(np.arange(365) + 3, start="1/1/2001").expand_dims(lat=np.arange(20), lon=np.arange(20)) + tasmin = tasmin_series(np.arange(365) + 2, start="1/1/2001").expand_dims(lat=np.arange(20), lon=np.arange(20)) thresh_tasmin = xr.DataArray( 10 * np.arange(20) + 100, @@ -872,9 +807,7 @@ def test_2dthresholds(self, tasmax_series, tasmin_series): dims=("lon", "lat"), coords={"lon": hwtl.lon[:3], "lat": hwtl.lat[:3]}, ) - np.testing.assert_array_equal( - exp, hwtl.isel(time=3, lon=slice(None, 3), lat=slice(None, 3)) - ) + np.testing.assert_array_equal(exp, hwtl.isel(time=3, lon=slice(None, 3), lat=slice(None, 3))) class TestHeatWaveIndex: @@ -1130,12 +1063,8 @@ def test_3d_data_with_nans(self, open_dataset): 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" - ) + 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] @@ -1351,17 +1280,13 @@ 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 def test_degree_days_exceedance_date(open_dataset): tas = open_dataset("FWI/GFWED_sample_2017.nc").tas - tas.attrs.update( - cell_methods="time: mean within days", standard_name="air_temperature" - ) + tas.attrs.update(cell_methods="time: mean within days", standard_name="air_temperature") out = atmos.degree_days_exceedance_date( tas=tas, @@ -1381,8 +1306,7 @@ def test_degree_days_exceedance_date(open_dataset): np.testing.assert_array_equal(out, np.array([[199, 193, 190, 190]]).T) assert ( "Day of year when the integral of degree days (mean daily temperature > 4 degc) " - "exceeds 200 k days, with the cumulative sum starting from 07-01." - in out.attrs["description"] + "exceeds 200 k days, with the cumulative sum starting from 07-01." in out.attrs["description"] ) with set_options(check_missing="skip"): @@ -1397,14 +1321,10 @@ def test_degree_days_exceedance_date(open_dataset): np.testing.assert_array_equal(out, np.array([[np.nan, 280, 241, 244]]).T) -@pytest.mark.parametrize( - "never_reached,exp", [(None, np.nan), (300, 300), ("12-01", 335)] -) +@pytest.mark.parametrize("never_reached,exp", [(None, np.nan), (300, 300), ("12-01", 335)]) def test_degree_days_exceedance_date_never_reached(open_dataset, never_reached, exp): tas = open_dataset("FWI/GFWED_sample_2017.nc").tas - tas.attrs.update( - cell_methods="time: mean within days", standard_name="air_temperature" - ) + tas.attrs.update(cell_methods="time: mean within days", standard_name="air_temperature") # Default -> NaN out = atmos.degree_days_exceedance_date( tas=tas, @@ -1423,15 +1343,9 @@ def test_warm_spell_duration_index(self, open_dataset): tasmax = open_dataset("ERA5/daily_surface_cancities_1990-1993.nc").tasmax tx90 = percentile_doy(tasmax, window=5, per=90) - out = atmos.warm_spell_duration_index( - tasmax=tasmax, tasmax_per=tx90, window=3, freq="YS-JUL" - ) - np.testing.assert_array_equal( - out.isel(location=0, percentiles=0), np.array([np.nan, 4, 0, 0, np.nan]) - ) - assert ( - "Annual number of days with at least 3 consecutive days" in out.description - ) + out = atmos.warm_spell_duration_index(tasmax=tasmax, tasmax_per=tx90, window=3, freq="YS-JUL") + np.testing.assert_array_equal(out.isel(location=0, percentiles=0), np.array([np.nan, 4, 0, 0, np.nan])) + assert "Annual number of days with at least 3 consecutive days" in out.description def test_wsdi_custom_percentiles_parameters(self, open_dataset): # GIVEN @@ -1463,10 +1377,7 @@ def test_maximum_consecutive_warm_days(open_dataset): tasmax = open_dataset("ERA5/daily_surface_cancities_1990-1993.nc").tasmax out = atmos.maximum_consecutive_warm_days(tasmax) np.testing.assert_array_equal(out[1, :], np.array([13, 21, 6, 10])) - assert ( - "Annual longest spell of consecutive days with maximum daily temperature above 25 degc." - in out.description - ) + assert "Annual longest spell of consecutive days with maximum daily temperature above 25 degc." in out.description def test_corn_heat_units(open_dataset): @@ -1477,32 +1388,21 @@ def test_corn_heat_units(open_dataset): tnC = tn - K2C tnC.attrs["units"] = "C" - chu = atmos.corn_heat_units( - tasmin=tn, tasmax=tx, thresh_tasmin="4.44 degC", thresh_tasmax="10 degC" - ) - chuC = atmos.corn_heat_units( - tasmin=tnC, tasmax=tx, thresh_tasmin="4.44 degC", thresh_tasmax="10 degC" - ) + chu = atmos.corn_heat_units(tasmin=tn, tasmax=tx, thresh_tasmin="4.44 degC", thresh_tasmax="10 degC") + chuC = atmos.corn_heat_units(tasmin=tnC, tasmax=tx, thresh_tasmin="4.44 degC", thresh_tasmax="10 degC") np.testing.assert_allclose(chu, chuC, rtol=1e-3) - np.testing.assert_allclose( - chu[0, 180:185], np.array([12.933, 11.361, 11.1365, 13.419, 15.569]), rtol=1e-4 - ) + np.testing.assert_allclose(chu[0, 180:185], np.array([12.933, 11.361, 11.1365, 13.419, 15.569]), rtol=1e-4) - assert ( - "minimum and maximum daily temperatures both exceed 4.44 degc and 10 degc, respectively." - in chu.description - ) + assert "minimum and maximum daily temperatures both exceed 4.44 degc and 10 degc, respectively." in chu.description class TestFreezeThawSpell: def test_freezethaw_spell_frequency(self, open_dataset): ds = open_dataset("ERA5/daily_surface_cancities_1990-1993.nc") - out = atmos.freezethaw_spell_frequency( - tasmin=ds.tasmin, tasmax=ds.tasmax, freq="YS" - ) + out = atmos.freezethaw_spell_frequency(tasmin=ds.tasmin, tasmax=ds.tasmax, freq="YS") np.testing.assert_array_equal(out.isel(location=0), [34.0, 37.0, 36.0, 30.0]) # At location -1, year 2 has no spells of length >=2 @@ -1526,12 +1426,8 @@ def test_freezethaw_spell_frequency(self, open_dataset): def test_freezethaw_spell_mean_length(self, open_dataset): ds = open_dataset("ERA5/daily_surface_cancities_1990-1993.nc") - out = atmos.freezethaw_spell_mean_length( - tasmin=ds.tasmin, tasmax=ds.tasmax, freq="YS" - ) - np.testing.assert_allclose( - out.isel(location=0), [1.911765, 2.027027, 1.888889, 1.733333], rtol=1e-06 - ) + out = atmos.freezethaw_spell_mean_length(tasmin=ds.tasmin, tasmax=ds.tasmax, freq="YS") + np.testing.assert_allclose(out.isel(location=0), [1.911765, 2.027027, 1.888889, 1.733333], rtol=1e-06) # At location -1, year 2 has no spells of length >=2 out = atmos.freezethaw_spell_mean_length( @@ -1554,9 +1450,7 @@ def test_freezethaw_spell_mean_length(self, open_dataset): def test_freezethaw_spell_max_length(self, open_dataset): ds = open_dataset("ERA5/daily_surface_cancities_1990-1993.nc") - out = atmos.freezethaw_spell_max_length( - tasmin=ds.tasmin, tasmax=ds.tasmax, freq="YS" - ) + out = atmos.freezethaw_spell_max_length(tasmin=ds.tasmin, tasmax=ds.tasmax, freq="YS") np.testing.assert_array_equal(out.isel(location=0), [12, 7, 7, 4]) # At location -1, year 2 has no spells of length >=2 diff --git a/tests/test_testing_utils.py b/tests/test_testing_utils.py index 6bf6005a9..0e60b3f72 100644 --- a/tests/test_testing_utils.py +++ b/tests/test_testing_utils.py @@ -27,7 +27,6 @@ def test_timeseries_made_up_variable(self): class TestFileRequests: - @staticmethod def file_md5_checksum(f_name): import hashlib diff --git a/tests/test_units.py b/tests/test_units.py index 946e8ab71..463e593b7 100644 --- a/tests/test_units.py +++ b/tests/test_units.py @@ -85,9 +85,7 @@ def test_lazy(self, pr_series): out = convert_units_to(pr, "mm/day", context="hydro") assert isinstance(out.data, dsk.Array) - @pytest.mark.parametrize( - "alias", [units("Celsius"), units("degC"), units("C"), units("deg_C")] - ) + @pytest.mark.parametrize("alias", [units("Celsius"), units("degC"), units("C"), units("deg_C")]) def test_temperature_aliases(self, alias): assert alias == units("celsius") @@ -114,9 +112,7 @@ def test_cf_conversion_amount2lwethickness_amount2rate(self): assert out.attrs["standard_name"] == "rainfall_flux" def test_temperature_difference(self): - delta = xr.DataArray( - [2], attrs={"units": "K", "units_metadata": "temperature: difference"} - ) + delta = xr.DataArray([2], attrs={"units": "K", "units_metadata": "temperature: difference"}) out = convert_units_to(source=delta, target="delta_degC") assert out == 2 assert out.attrs["units"] == "degC" @@ -224,9 +220,7 @@ def test_rate2amount(pr_series): np.testing.assert_array_equal(am_ys, 86400 * np.array([365, 366, 365])) -@pytest.mark.parametrize( - "srcfreq, exp", [("h", 3600), ("min", 60), ("s", 1), ("ns", 1e-9)] -) +@pytest.mark.parametrize("srcfreq, exp", [("h", 3600), ("min", 60), ("s", 1), ("ns", 1e-9)]) def test_rate2amount_subdaily(srcfreq, exp): pr = xr.DataArray( np.ones(1000), @@ -304,13 +298,13 @@ def dryness_index( def test_declare_relative_units(): def index( - data: xr.DataArray, thresh: Quantified, dthreshdt: Quantified # noqa: F841 + data: xr.DataArray, + thresh: Quantified, + dthreshdt: Quantified, # noqa: F841 ): return xr.DataArray(1, attrs={"units": "rad"}) - index_relative = declare_relative_units(thresh="", dthreshdt="/[time]")( - index - ) + index_relative = declare_relative_units(thresh="", dthreshdt="/[time]")(index) assert hasattr(index_relative, "relative_units") index_full_mm = declare_units(data="mm")(index_relative) diff --git a/tests/test_utils.py b/tests/test_utils.py index 8e2dad369..dafc02c15 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -35,9 +35,7 @@ def test_calc_perc_type7(self): def test_calc_perc_type8(self): # Example array from: https://en.wikipedia.org/wiki/Percentile#The_nearest-rank_method - arr = np.asarray( - [[15.0, 20.0, 35.0, 40.0, 50.0], [15.0, 20.0, 35.0, 40.0, 50.0]] - ) + arr = np.asarray([[15.0, 20.0, 35.0, 40.0, 50.0], [15.0, 20.0, 35.0, 40.0, 50.0]]) res = nan_calc_percentiles( arr, percentiles=[40.0], @@ -50,9 +48,7 @@ def test_calc_perc_type8(self): def test_calc_perc_2d(self): # Example array from: https://en.wikipedia.org/wiki/Percentile#The_nearest-rank_method - arr = np.asarray( - [[15.0, 20.0, 35.0, 40.0, 50.0], [15.0, 20.0, 35.0, 40.0, 50.0]] - ) + arr = np.asarray([[15.0, 20.0, 35.0, 40.0, 50.0], [15.0, 20.0, 35.0, 40.0, 50.0]]) res = nan_calc_percentiles(arr, percentiles=[40.0]) # The expected is from R ` quantile(c(15.0, 20.0, 35.0, 40.0, 50.0), probs=0.4)` assert np.all(res[0][0] == 29) diff --git a/tests/test_wind.py b/tests/test_wind.py index 5e352be13..253fd6ab6 100644 --- a/tests/test_wind.py +++ b/tests/test_wind.py @@ -8,9 +8,7 @@ class TestWindSpeedIndicators: def test_calm_windy_days(self, open_dataset): ds = open_dataset("ERA5/daily_surface_cancities_1990-1993.nc") - sfcwind, _ = atmos.wind_speed_from_vector( - ds.uas, ds.vas, calm_wind_thresh="0 m/s" - ) + sfcwind, _ = atmos.wind_speed_from_vector(ds.uas, ds.vas, calm_wind_thresh="0 m/s") calm = atmos.calm_days(sfcwind, thresh="5 m/s") windy = atmos.windy_days(sfcwind, thresh="5 m/s") From 7ab3551d59df366383c37d87046b9827cad4d583 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Tue, 18 Feb 2025 17:00:44 -0500 Subject: [PATCH 08/26] reimplement blackdoc for non-python files --- .pre-commit-config.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 75e38f585..d2ab41e2b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -82,6 +82,12 @@ repos: hooks: - id: mdformat exclude: '.github/\w+.md|.github/publish-mastodon-template.md|docs/paper/paper.md' + - repo: https://github.com/keewis/blackdoc + rev: v0.3.9 + hooks: + - id: blackdoc + additional_dependencies: [ 'black==25.1.0' ] + exclude: '(.py|docs/installation.rst)' - repo: https://github.com/codespell-project/codespell rev: v2.4.1 hooks: From 2ec47a202d79cbf614bc5b642b7738530f11d448 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Tue, 18 Feb 2025 17:09:50 -0500 Subject: [PATCH 09/26] exclude py files --- Makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Makefile b/Makefile index fc616f6ce..b4d4751fa 100644 --- a/Makefile +++ b/Makefile @@ -56,8 +56,7 @@ lint: ## check style with flake8 and black python -m ruff check src/xclim tests python -m flake8 --config=.flake8 src/xclim tests python -m vulture src/xclim tests - python -m blackdoc --check --exclude=src/xclim/indices/__init__.py src/xclim - python -m blackdoc --check docs + python -m blackdoc --check README.rst CHANGELOG.rst CONTRIBUTING.rst docs --exclude=".py" codespell src/xclim tests docs python -m numpydoc lint src/xclim/*.py src/xclim/ensembles/*.py src/xclim/indices/*.py src/xclim/indicators/*.py src/xclim/testing/*.py python -m deptry src From 13572e3293395dc42868e1cd551daa3cb70aef2d Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Tue, 18 Feb 2025 17:11:34 -0500 Subject: [PATCH 10/26] drop nbqa --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index f7f7c2c8c..745918043 100644 --- a/tox.ini +++ b/tox.ini @@ -35,7 +35,6 @@ deps = deptry >=0.23.0 flake8 >=7.1.1 flake8-rst-docstrings ==0.3.0 - nbqa >=1.8.2 numpydoc >=1.8.0 ruff >=0.9.6 vulture >=2.11 From d06327e720c1653ec3d9f776b7322a08c78c53d2 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Wed, 19 Feb 2025 09:33:13 -0500 Subject: [PATCH 11/26] remove coveralls package from conda env --- .github/workflows/main.yml | 12 +++++------- environment.yml | 3 +-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 536ec5cd6..445ec0205 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -408,13 +408,11 @@ jobs: - name: Test with pytest run: | python -m pytest --numprocesses=logical --durations=10 --cov=xclim --cov-report=term-missing - - name: Report coverage - run: | - coveralls - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COVERALLS_FLAG_NAME: run-{{ matrix.python-version }}-conda - COVERALLS_PARALLEL: true + - name: Report Coverage + uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6 + with: + flag-name: run-{{ matrix.python-version }}-conda + parallel: true - name: Tests measurement uses: green-coding-solutions/eco-ci-energy-estimation@7ff5628108e21227662ce881f10156eb9deab891 # v4.4 with: diff --git a/environment.yml b/environment.yml index b32a4f3a2..026e50f23 100644 --- a/environment.yml +++ b/environment.yml @@ -34,8 +34,7 @@ dependencies: - cairosvg >=2.6.0 - codespell >=2.4.1 - coverage >=7.5.0 - - coveralls >=4.0.1 # Note: coveralls is not yet compatible with Python 3.13 - - deptry >=0.22.0 # Version is lagging on conda-forge + - deptry >=0.23.0 - distributed >=2.0 - flake8 >=7.1.1 - flake8-rst-docstrings >=0.3.0 From 032664e21d760703de5e6299a0458f24dca8e64b Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Wed, 19 Feb 2025 14:06:01 -0500 Subject: [PATCH 12/26] use lcov format --- .github/workflows/main.yml | 2 +- .gitignore | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 445ec0205..cdb632c05 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -407,7 +407,7 @@ jobs: continue-on-error: true - name: Test with pytest run: | - python -m pytest --numprocesses=logical --durations=10 --cov=xclim --cov-report=term-missing + python -m pytest --numprocesses=logical --durations=10 --cov=xclim --cov-report=lcov - name: Report Coverage uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6 with: diff --git a/.gitignore b/.gitignore index a92c3a916..d1d6d1966 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ htmlcov/ .coverage.* .cache nosetests.xml +coverage.lcov coverage.xml *.cover .hypothesis/ From 4bc30658ad59e314200608bc8e0e61bec419333d Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Wed, 19 Feb 2025 14:23:04 -0500 Subject: [PATCH 13/26] do not use Python3.13 for docs --- .readthedocs.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 1f9c01886..c57f3a8cf 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -8,10 +8,12 @@ sphinx: # - pdf build: - os: ubuntu-22.04 + os: ubuntu-24.04 tools: - python: "mambaforge-22.9" + python: "mambaforge-23.11" jobs: + pre_create_environment: + - sed -i 's/python\ >=3.11,<3.14/python\ >=3.11,<3.13/' environment.yml pre_build: - sphinx-apidoc -o docs/apidoc/ --private --module-first src/xclim src/xclim/testing/tests src/xclim/indicators src/xclim/indices - rm docs/apidoc/xclim.rst @@ -31,7 +33,6 @@ python: search: ranking: - notebooks/*: 2 api_indicators.html: 1 indices.html: -1 From 4c78389b3064b6cbb3543599d86cdbae007a3430 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Wed, 19 Feb 2025 14:40:43 -0500 Subject: [PATCH 14/26] use double-quotes --- .readthedocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index c57f3a8cf..1088e05d5 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -13,7 +13,7 @@ build: python: "mambaforge-23.11" jobs: pre_create_environment: - - sed -i 's/python\ >=3.11,<3.14/python\ >=3.11,<3.13/' environment.yml + - sed -i "s/python >=3.11,<3.14/python >=3.11,<3.13/" environment.yml pre_build: - sphinx-apidoc -o docs/apidoc/ --private --module-first src/xclim src/xclim/testing/tests src/xclim/indicators src/xclim/indices - rm docs/apidoc/xclim.rst From fa10cf666e4d579dda93ad7e1170661f9f597ce1 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Wed, 19 Feb 2025 14:54:34 -0500 Subject: [PATCH 15/26] update sphinx and sphinx-autodoc-typehints --- environment.yml | 4 ++-- pyproject.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/environment.yml b/environment.yml index 026e50f23..fe119d324 100644 --- a/environment.yml +++ b/environment.yml @@ -63,9 +63,9 @@ dependencies: - pytest-socket >=0.6.0 - pytest-xdist >=3.2 - ruff >=0.9.6 - - sphinx >=7.0.0 + - sphinx >=8.2.0 - sphinx-autobuild >=2024.4.16 - - sphinx-autodoc-typehints + - sphinx-autodoc-typehints >=3.1.0 - sphinx-codeautolink >=0.16.2 - sphinx-copybutton - sphinx-mdinclude diff --git a/pyproject.toml b/pyproject.toml index 1f2b4a502..6e3c87d6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,9 +100,9 @@ docs = [ "nc-time-axis >=1.4.1", "pooch >=1.8.0", "pybtex >=0.24.0", - "sphinx >=7.0.0", + "sphinx >=8.2.0", "sphinx-autobuild >=2024.4.16", - "sphinx-autodoc-typehints", + "sphinx-autodoc-typehints >=3.1.0", "sphinx-codeautolink >=0.16.2", "sphinx-copybutton", "sphinx-mdinclude", From 63cbc08cd0166225d7c6a9a80f79849588ee4450 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Thu, 20 Feb 2025 10:42:22 -0500 Subject: [PATCH 16/26] release pin on conda --- environment.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/environment.yml b/environment.yml index fe119d324..28e3b4aeb 100644 --- a/environment.yml +++ b/environment.yml @@ -63,9 +63,9 @@ dependencies: - pytest-socket >=0.6.0 - pytest-xdist >=3.2 - ruff >=0.9.6 - - sphinx >=8.2.0 + - sphinx >=7.1.0,<8.2 - sphinx-autobuild >=2024.4.16 - - sphinx-autodoc-typehints >=3.1.0 + - sphinx-autodoc-typehints - sphinx-codeautolink >=0.16.2 - sphinx-copybutton - sphinx-mdinclude From b1e2a8552bdde3cb02184eb09e39b97784b7e2ff Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Thu, 20 Feb 2025 10:52:04 -0500 Subject: [PATCH 17/26] squelch warning on linkcheck --- docs/references.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/references.rst b/docs/references.rst index 31f02b0c3..22ba91d32 100644 --- a/docs/references.rst +++ b/docs/references.rst @@ -1,4 +1,4 @@ -.. only:: html +.. only:: not latex ============ Bibliography From 688c8407aa14866349427afdcc54fcccabd8af49 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Thu, 20 Feb 2025 11:00:34 -0500 Subject: [PATCH 18/26] update CHANGELOG.rst --- CHANGELOG.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9051f3b51..ad695f56d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,19 @@ Changelog ========= +v0.56.0 (unreleased) +-------------------- +Contributors to this version: Trevor James Smith (:user:`Zeitsperre`). + +Breaking changes +^^^^^^^^^^^^^^^^ +* `xclim` no longer supports Python 3.10. The minimum required version is now Python 3.11. (:pull:`2082`). +* The minimum versions of several key dependencies have been raised (`numpy` >=1.24.0; `scikit-learn` >=1.2.0; `scipy` >=1.11.0). (:pull:`2082`). + +Internal changes +^^^^^^^^^^^^^^^^ +* `black`, `isort`, and `nbqa` have all been dropped from the development dependencies. (:pull:`2082`). + v0.55.0 (2025-02-17) -------------------- Contributors to this version: Juliette Lavoie (:user:`juliettelavoie`), Trevor James Smith (:user:`Zeitsperre`), Sascha Hofmann (:user:`saschahofmann`), Pascal Bourgault (:user:`aulemahal`), Éric Dupuis (:user:`coxipi`), Baptiste Hamon (:user:`baptistehamon`), Sarah Gammon (:user:`SarahG-579462`). From 2093fb028ebc3e9048e1d9eac0773040901b77f3 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Thu, 20 Feb 2025 11:01:18 -0500 Subject: [PATCH 19/26] update CHANGELOG.rst --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ad695f56d..10b1543e5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,7 +13,7 @@ Breaking changes Internal changes ^^^^^^^^^^^^^^^^ -* `black`, `isort`, and `nbqa` have all been dropped from the development dependencies. (:pull:`2082`). +* `black`, `isort`, and `nbqa` have all been dropped from the development dependencies. (:issue:`1805`, :pull:`2082`). v0.55.0 (2025-02-17) -------------------- From a7aeaba6bcdd665ab9f59ededce29d23ec6db628 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Fri, 21 Feb 2025 10:25:22 -0500 Subject: [PATCH 20/26] fix eigen3 configuration --- .github/workflows/main.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cdb632c05..7d165d2bc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -200,17 +200,18 @@ jobs: markers: -m 'not slow' python-version: "3.11" tox-env: standard + eigen3: true - os: 'ubuntu-latest' testdata-cache: '~/.cache/xclim-testdata' markers: -m 'not slow' python-version: "3.12" tox-env: standard - # Coverage is not yet supported on Python3.13 . See `tox.ini`. - os: 'ubuntu-latest' testdata-cache: '~/.cache/xclim-testdata' markers: -m 'not slow' python-version: "3.13" tox-env: standard + eigen3: true # Windows builds - os: 'windows-latest' testdata-cache: 'C:\Users\runneradmin\AppData\Local\xclim-testdata\xclim-testdata\Cache' @@ -222,7 +223,7 @@ jobs: testdata-cache: '~/Library/Caches/xclim-testdata' markers: '' # Slow tests python-version: "3.11" - tox-env: py311-coverage-extras + tox-env: py311-coverage-extras-lmoments # Specialized tests - os: 'ubuntu-latest' testdata-cache: '~/.cache/xclim-testdata' @@ -272,7 +273,7 @@ jobs: with: persist-credentials: false - name: Install Eigen3 (SBCK) - if: ${{ matrix.python-version == '3.11' && matrix.os == 'ubuntu-latest' }} + if: ${{ matrix.eigen3 == true }} run: | sudo apt-get update sudo apt-get install libeigen3-dev From 94c93b4f71d349da5793f0d248254536458ed828 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Fri, 21 Feb 2025 10:34:49 -0500 Subject: [PATCH 21/26] sbck not supported on Python3.13 --- .github/workflows/main.yml | 4 +--- tox.ini | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7d165d2bc..531b56dc6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -200,7 +200,6 @@ jobs: markers: -m 'not slow' python-version: "3.11" tox-env: standard - eigen3: true - os: 'ubuntu-latest' testdata-cache: '~/.cache/xclim-testdata' markers: -m 'not slow' @@ -211,7 +210,6 @@ jobs: markers: -m 'not slow' python-version: "3.13" tox-env: standard - eigen3: true # Windows builds - os: 'windows-latest' testdata-cache: 'C:\Users\runneradmin\AppData\Local\xclim-testdata\xclim-testdata\Cache' @@ -273,7 +271,7 @@ jobs: with: persist-credentials: false - name: Install Eigen3 (SBCK) - if: ${{ matrix.eigen3 == true }} + if: ${{ matrix.python-version == '3.11' && matrix.os == 'ubuntu-latest' }} run: | sudo apt-get update sudo apt-get install libeigen3-dev diff --git a/tox.ini b/tox.ini index f7f7c2c8c..0c15a69e5 100644 --- a/tox.ini +++ b/tox.ini @@ -22,8 +22,7 @@ opts = python = 3.11 = py311-coverage-extras-sbck-lmoments 3.12 = py312-coverage-extras-numpy - # coveralls is not yet supported for Python3.13; Adjust this build when coveralls>4.0.1 is released. - 3.13 = py313-extras-sbck + 3.13 = py313-extras-lmoments [testenv:lint] description = Run code quality compliance tests under {basepython} From 8e5f84809669abdcd169ca39cfac17ac9829378d Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Fri, 21 Feb 2025 10:46:07 -0500 Subject: [PATCH 22/26] drop Python3.10 on Windows --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 531b56dc6..bf4c02bf1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -214,8 +214,8 @@ jobs: - os: 'windows-latest' testdata-cache: 'C:\Users\runneradmin\AppData\Local\xclim-testdata\xclim-testdata\Cache' markers: -m 'not slow' - python-version: "3.10" - tox-env: py310-coverage-prefetch # Test data prefetch is needed for Windows + python-version: "3.12" + tox-env: py312-coverage-prefetch # Test data prefetch is needed for Windows # macOS builds - os: 'macos-latest' testdata-cache: '~/Library/Caches/xclim-testdata' From d806c7d294bb6074714fafcfb540f03ff1547a1d Mon Sep 17 00:00:00 2001 From: Ouranos Helper Bot Date: Fri, 21 Feb 2025 16:04:27 +0000 Subject: [PATCH 23/26] =?UTF-8?q?Bump=20version:=200.55.1-dev.0=20?= =?UTF-8?q?=E2=86=92=200.55.1-dev.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Ouranos Helper Bot --- pyproject.toml | 2 +- src/xclim/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6e3c87d6b..3eda91829 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,7 +127,7 @@ xclim = "xclim.cli:cli" [tool] [tool.bumpversion] -current_version = "0.55.1-dev.0" +current_version = "0.55.1-dev.1" commit = true commit_args = "--no-verify --signoff" tag = false diff --git a/src/xclim/__init__.py b/src/xclim/__init__.py index 4bc11d562..3e165ff88 100644 --- a/src/xclim/__init__.py +++ b/src/xclim/__init__.py @@ -13,7 +13,7 @@ __author__ = """Travis Logan""" __email__ = "logan.travis@ouranos.ca" -__version__ = "0.55.1-dev.0" +__version__ = "0.55.1-dev.1" with _resources.as_file(_resources.files("xclim.data")) as _module_data: From 8b6f7abe0f4726acace1bc4652288954f8d22286 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:26:48 -0500 Subject: [PATCH 24/26] update tox config --- tox.ini | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/tox.ini b/tox.ini index b728a8c18..35dbf8c9d 100644 --- a/tox.ini +++ b/tox.ini @@ -60,15 +60,6 @@ allowlist_externals = env make -# Requires tox-conda compatible with tox@v4.0 -;[testenv:conda] -;description = Run tests with pytest under {basepython} (Anaconda distribution) -;commands_pre = -;conda_channels = conda-forge -;conda_env = environment-dev.yml -;deps = -;extras = - [testenv:notebooks{-prefetch,}] description = Run notebooks with pytest under {basepython} extras = @@ -106,8 +97,7 @@ extras = deps = coverage: coveralls>=4.0.1 lmoments: lmoments3 - numpy: numpy>=1.23,<2.0 - numpy: pint>=0.18,<0.24.0 + numpy: numpy>=1.24,<2.0 upstream: -r CI/requirements_upstream.txt install_command = python -m pip install --no-user {opts} {packages} download = True From 31c7d4a81eac3806a382688e514daee37d2a043f Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:26:58 -0500 Subject: [PATCH 25/26] update CHANGELOG.rst --- CHANGELOG.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 10b1543e5..c9f69e58d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,11 @@ Breaking changes Internal changes ^^^^^^^^^^^^^^^^ * `black`, `isort`, and `nbqa` have all been dropped from the development dependencies. (:issue:`1805`, :pull:`2082`). +* `ruff` has been configured to provide code formatting. (:pull:`2083`): + * The maximum line-length is now 120 characters. + * Docstring formatting is now enabled. + * Line endings in files now must be `Unix`-compatible (`LF`). +* The `blackdoc` pre-commit hook now only examines `.rst` and `.md` files. (:pull:`2083`). v0.55.0 (2025-02-17) -------------------- From d327e600a4691e7b38144fd2dd075ad2d3699691 Mon Sep 17 00:00:00 2001 From: Ouranos Helper Bot Date: Fri, 21 Feb 2025 16:42:01 +0000 Subject: [PATCH 26/26] =?UTF-8?q?Bump=20version:=200.55.1-dev.1=20?= =?UTF-8?q?=E2=86=92=200.55.1-dev.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Ouranos Helper Bot --- pyproject.toml | 2 +- src/xclim/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1b06894b1..2cf17554c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,7 +127,7 @@ xclim = "xclim.cli:cli" [tool] [tool.bumpversion] -current_version = "0.55.1-dev.1" +current_version = "0.55.1-dev.2" commit = true commit_args = "--no-verify --signoff" tag = false diff --git a/src/xclim/__init__.py b/src/xclim/__init__.py index 3e165ff88..bf487dd16 100644 --- a/src/xclim/__init__.py +++ b/src/xclim/__init__.py @@ -13,7 +13,7 @@ __author__ = """Travis Logan""" __email__ = "logan.travis@ouranos.ca" -__version__ = "0.55.1-dev.1" +__version__ = "0.55.1-dev.2" with _resources.as_file(_resources.files("xclim.data")) as _module_data: