diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d1acec8ff..92b406b3d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ env: concurrency: # For a given workflow, if we push to the same branch, cancel all previous builds on that branch except on main. - group: ${{ github.workflow }}-${{ github.ref }} + group: "${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} permissions: @@ -57,8 +57,9 @@ jobs: uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 with: python-version: ${{ matrix.python-version }} - - name: Install pylint and tox - run: pip install pylint tox~=4.0 + - name: Install pip, pylint, and tox + run: | + python -m pip install flit pip~=24.0 pylint tox~=4.0 - name: Run pylint run: | python -m pylint --rcfile=.pylintrc.toml --disable=import-error --exit-zero xclim @@ -97,7 +98,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install tox run: | - python -m pip install tox~=4.0 + python -m pip install flit pip~=24.0 tox~=4.0 - name: Test with tox run: | python -m tox -e ${{ matrix.tox-env }} @@ -118,17 +119,17 @@ jobs: matrix: include: # Windows builds - - tox-env: py39-prefetch-coverage + - tox-env: py39-coverage-prefetch python-version: "3.9" markers: -m 'not slow' os: windows-latest # macOS builds - - tox-env: py310-coverage + - tox-env: py310-coverage-lmoments-doctest python-version: "3.10" markers: -m 'not slow' os: macos-latest # Linux builds - - tox-env: py39-coverage-sbck + - tox-env: py39-coverage-sbck-doctest python-version: "3.9" markers: -m 'not slow' os: ubuntu-latest @@ -139,11 +140,11 @@ jobs: python-version: "3.11" markers: -m 'not slow' os: ubuntu-latest - - tox-env: py312-coverage-numba + - tox-env: py312-coverage-lmoments-doctest python-version: "3.12" markers: -m 'not slow' os: ubuntu-latest - - tox-env: notebooks_doctests + - tox-env: notebooks python-version: "3.10" os: ubuntu-latest - tox-env: offline-prefetch @@ -180,7 +181,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install tox run: | - python -m pip install tox~=4.0 + python -m pip install flit pip~=24.0 tox~=4.0 tox-gh - name: Test with tox run: | python -m tox -e ${{ matrix.tox-env }} -- ${{ matrix.markers }} @@ -192,17 +193,16 @@ jobs: test-conda: needs: lint - name: test-conda-Python${{ matrix.python-version }} + name: Python${{ matrix.python-version }} (Conda, ${{ matrix.os }}) if: | contains(github.event.pull_request.labels.*.name, 'approved') || (github.event.review.state == 'approved') || (github.event_name == 'push') - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: matrix: - include: - - python-version: "3.9" - - python-version: "3.12" + os: [ubuntu-latest] + python-version: ["3.9", "3.12"] defaults: run: shell: bash -l {0} diff --git a/CHANGES.rst b/CHANGES.rst index 05989cbbe..65ac93e40 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -19,12 +19,15 @@ Bug fixes * Fixed an bug in sdba's ``map_groups`` that prevented passing DataArrays with cftime coordinates if the ``sdba_encode_cf`` option was True. (:issue:`1673`, :pull:`1674`). * Fixed bug (:issue:`1678`, :pull:`1679`) in sdba where a loaded training dataset could not be used for adjustment * Fixed bug with loess smoothing for an array full of NaNs. (:pull:`1699`). -* Fixed and adapted ``time_bnds`` to the newest xarray. (:pull:`1700`) +* Fixed and adapted ``time_bnds`` to the newest xarray. (:pull:`1700`). +* Fixed "agreement fraction" in ``robustness_fractions`` to distinguish between negative change and no change. Added "negative" and "changed negative" fractions (:issue:`1690`, :pull:`1711`). Internal changes ^^^^^^^^^^^^^^^^ * Added "doymin" and "doymax" to the possible operations of ``generic.stats``. Fixed a warning issue when ``op`` was "integral". (:pull:`1672`). - +* Reorganized GitHub CI build matrices to run the doctests more consistently. (:pull:`1709`). +* Removed the experimental `numba` and `llvm` dependency installation steps in the `tox.ini` file. Added `numba@main` to the upstream dependencies. (:pull:`1709`). +* Added the `tox-gh` dependency to the development installation recipe. This will soon be required for running the `tox` test ensemble on GitHub Workflows. (:pull:`1709`). v0.48.2 (2024-02-26) -------------------- diff --git a/pyproject.toml b/pyproject.toml index 20ea95e52..9f8ec848d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,7 @@ dev = [ "tokenize-rt", "tox >=4.0", # "tox-conda", # Will be added when a tox@v4.0+ compatible plugin is released. + "tox-gh >=1.3.1", "xdoctest", "yamllint", # Documentation and examples @@ -123,7 +124,7 @@ target-version = [ ] [tool.bumpversion] -current_version = "0.48.3-dev.5" +current_version = "0.48.3-dev.6" commit = true commit_args = "--no-verify" tag = false diff --git a/requirements_upstream.txt b/requirements_upstream.txt index 4758fded4..71145c0ce 100644 --- a/requirements_upstream.txt +++ b/requirements_upstream.txt @@ -1,4 +1,5 @@ bottleneck @ git+https://github.com/pydata/bottleneck.git@master cftime @ git+https://github.com/Unidata/cftime.git@master flox @ git+https://github.com/xarray-contrib/flox.git@main +numba @ git+https://github.com/numba/numba.git@main xarray @ git+https://github.com/pydata/xarray.git@main diff --git a/tests/test_ensembles.py b/tests/test_ensembles.py index 67aac38aa..63d62e6d4 100644 --- a/tests/test_ensembles.py +++ b/tests/test_ensembles.py @@ -735,13 +735,14 @@ def test_robustness_fractions_weighted(robust_data): def test_robustness_fractions_delta(robust_data): - delta = xr.DataArray([-2, 1, -2, -1], dims=("realization",)) + delta = xr.DataArray([-2, 1, -2, -1, 0, 0], dims=("realization",)) fracs = ensembles.robustness_fractions(delta, test="threshold", abs_thresh=1.5) - np.testing.assert_array_equal(fracs.changed, [0.5]) + np.testing.assert_array_equal(fracs.changed, [2 / 6]) np.testing.assert_array_equal(fracs.changed_positive, [0.0]) - np.testing.assert_array_equal(fracs.positive, [0.25]) - np.testing.assert_array_equal(fracs.agree, [0.75]) + np.testing.assert_array_equal(fracs.positive, [1 / 6]) + np.testing.assert_array_equal(fracs.agree, [3 / 6]) + 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 diff --git a/tox.ini b/tox.ini index 264aee431..afbc68cd5 100644 --- a/tox.ini +++ b/tox.ini @@ -3,18 +3,26 @@ min_version = 4.0 env_list = lint docs - notebooks_doctests + notebooks offline-prefetch py39-upstream-doctest - py310 + py310-doctest py311-lmoments - py312-numba + py312-lmoments-doctest labels = test = py39, py310-upstream-doctest, py311, notebooks_doctests, offline-prefetch requires = - pip >= 23.0 + pip >= 24.0 + flit opts = -vv +[gh] +python = + 3.12 = py312-coverage-lmoments-doctest + 3.11 = py311-coverage-lmoments-sbck-doctest, offline-coverage-prefetch + 3.10 = py310-coverage-lmoments-doctest, notebooks + 3.9 = py39-coverage-sbck-doctest, lint, docs + [testenv:lint] description = Run code quality compliance tests under {basepython} skip_install = True @@ -65,16 +73,15 @@ allowlist_externals = ;deps = ;extras = -[testenv:notebooks_doctests{-coverage,}] -description = Run notebooks and doctests with pytest under {basepython} +[testenv:notebooks{-prefetch,}] +description = Run notebooks with pytest under {basepython} commands = pytest --no-cov --nbval --dist=loadscope --rootdir=tests/ --ignore=docs/notebooks/example.ipynb docs/notebooks - pytest --rootdir=tests/ --xdoctest xclim +commands_post = [testenv:offline{-prefetch,}{-coverage,}] description = Run tests with pytest under {basepython}, preventing socket connections (except for unix sockets for async support) commands: - prefetch: xclim prefetch_testing_data python -c 'print("Running offline tests with positional arguments: --disable-socket --allow-unix-socket --m \"not requires_internet\"")' python -c 'print("These can be overwritten with: tox -e offline -- -m \"some other marker statement\"")' pytest --disable-socket --allow-unix-socket {posargs:-m 'not requires_internet'} @@ -99,14 +106,11 @@ passenv = XCLIM_* extras = dev deps = - # FIXME: Remove when numba 0.59.0 is released - numba: numba==0.59.0rc1 - numba: llvmlite==0.42.0rc1 coverage: coveralls upstream: -rrequirements_upstream.txt sbck: pybind11 lmoments: lmoments3 - notebooks_doctests: lmoments3 + notebooks: lmoments3 install_command = python -m pip install --no-user {opts} {packages} download = True commands_pre = @@ -115,11 +119,13 @@ commands_pre = xclim show_version_info python -m pip check xclim --help + prefetch: xclim prefetch_testing_data commands = prefetch: xclim prefetch_testing_data - doctest: pytest --no-cov --rootdir=tests/ --xdoctest xclim pytest {posargs} + doctest: pytest --rootdir=tests/ --xdoctest xclim commands_post = coverage: - coveralls allowlist_externals = git + xclim diff --git a/xclim/__init__.py b/xclim/__init__.py index ccc4ad28f..e33337d19 100644 --- a/xclim/__init__.py +++ b/xclim/__init__.py @@ -16,7 +16,7 @@ __author__ = """Travis Logan""" __email__ = "logan.travis@ouranos.ca" -__version__ = "0.48.3-dev.5" +__version__ = "0.48.3-dev.6" _module_data = _files("xclim.data") diff --git a/xclim/ensembles/_robustness.py b/xclim/ensembles/_robustness.py index 00e9f1b1b..9c1603336 100644 --- a/xclim/ensembles/_robustness.py +++ b/xclim/ensembles/_robustness.py @@ -91,11 +91,15 @@ def robustness_fractions( # noqa: C901 The weighted fraction of valid members showing significant change. Passing `test=None` yields change_frac = 1 everywhere. Same type as `fut`. positive : - The weighted fraction of valid members showing 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 (]0, 1]). + 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. + 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 and 1 - positive. + 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`. pvals : @@ -106,7 +110,7 @@ def robustness_fractions( # noqa: C901 The table below shows the coefficient needed to retrieve the number of members that have the indicated characteristics, by multiplying it by the total number of members (`fut.realization.size`) and by `valid_frac`, assuming uniform weights. - For compactness, we rename the outputs cf, cpf and pf. + For compactness, we rename the outputs cf, pf, cpf, nf and cnf. +-----------------+--------------------+------------------------+------------+ | | Significant change | Non-significant change | Any change | @@ -115,9 +119,11 @@ def robustness_fractions( # noqa: C901 +-----------------+--------------------+------------------------+------------+ | Positive change | cpf | pf - cpf | pf | +-----------------+--------------------+------------------------+------------+ - | Negative change | (cf - cpf) | 1 - pf - (cf -cpf) | 1 - pf | + | Negative change | cnf | nf - cnf | nf | +-----------------+--------------------+------------------------+------------+ + And members showing absolutely no change are ``1 - nf - pf``. + Available statistical tests are : {tests_doc} @@ -213,10 +219,16 @@ def robustness_fractions( # noqa: C901 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 - agree_frac = xr.concat((pos_frac, 1 - pos_frac), "sign").max("sign") + 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] @@ -233,7 +245,7 @@ def robustness_fractions( # noqa: C901 test=str(test), ), "positive": pos_frac.assign_attrs( - description="Fraction of valid members showing positive change.", + description="Fraction of valid members showing strictly positive change.", units="", ), "changed_positive": change_pos_frac.assign_attrs( @@ -242,12 +254,25 @@ def robustness_fractions( # noqa: C901 units="", test=str(test), ), + "negative": neg_frac.assign_attrs( + description="Fraction of valid members showing strictly negative change.", + units="", + ), + "changed_negative": change_neg_frac.assign_attrs( + description="Fraction of valid members showing significant and negative change. " + + test_str, + units="", + test=str(test), + ), "valid": valid_frac.assign_attrs( description="Fraction of valid members (No missing values along time).", units="", ), "agree": agree_frac.assign_attrs( - description="Fraction of valid members agreeing on the sign of change. Maximum between pos_frac and 1 - pos_frac.", + description=( + "Fraction of valid members agreeing on the sign of change. " + "Maximum between the positive, negative and no change fractions." + ), units="", ), },