diff --git a/.circleci/config.yml b/.circleci/config.yml index 8e5f38b84f..975a1ae50a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2.1 orbs: - coverage-reporter: codacy/coverage-reporter@13.13.7 + coverage-reporter: codacy/coverage-reporter@13.16.5 codecov: codecov/codecov@3.2.5 commands: diff --git a/.github/workflows/run-tests-monitor.yml b/.github/workflows/run-tests-monitor.yml index 63f4d312ed..f0d0b625a3 100644 --- a/.github/workflows/run-tests-monitor.yml +++ b/.github/workflows/run-tests-monitor.yml @@ -75,6 +75,7 @@ jobs: - run: python -V 2>&1 | tee test_osx_artifacts_python_${{ matrix.python-version }}/python_version.txt - run: pip install pytest-monitor - run: pip install -e .[develop] 2>&1 | tee test_osx_artifacts_python_${{ matrix.python-version }}/install.txt + - run: conda list - run: pytest -n 2 -m "not installation" --db ../.pymon 2>&1 | tee test_osx_artifacts_python_${{ matrix.python-version }}/test_report.txt - run: python tests/parse_pymon.py - name: Upload artifacts diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index aa80283eb9..ce057f1ef5 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -58,6 +58,7 @@ jobs: - run: conda --version 2>&1 | tee test_linux_artifacts_python_${{ matrix.python-version }}/conda_version.txt - run: python -V 2>&1 | tee test_linux_artifacts_python_${{ matrix.python-version }}/python_version.txt - run: pip install -e .[develop] 2>&1 | tee test_linux_artifacts_python_${{ matrix.python-version }}/install.txt + - run: conda list - run: flake8 - run: pytest -n 2 -m "not installation" 2>&1 | tee test_linux_artifacts_python_${{ matrix.python-version }}/test_report.txt - name: Upload artifacts @@ -90,6 +91,7 @@ jobs: - run: conda --version 2>&1 | tee test_osx_artifacts_python_${{ matrix.python-version }}/conda_version.txt - run: python -V 2>&1 | tee test_osx_artifacts_python_${{ matrix.python-version }}/python_version.txt - run: pip install -e .[develop] 2>&1 | tee test_osx_artifacts_python_${{ matrix.python-version }}/install.txt + - run: conda list - run: flake8 - run: pytest -n 2 -m "not installation" 2>&1 | tee test_osx_artifacts_python_${{ matrix.python-version }}/test_report.txt - name: Upload artifacts diff --git a/.zenodo.json b/.zenodo.json index df7a49977d..bb00d5471d 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -200,6 +200,10 @@ "affiliation": "BSC, Spain", "name": "Martin-Martinez, Eneko", "orcid": "0000-0002-9213-7818" + }, + { + "affiliation": "DLR, Germany", + "name": "Cammarano, Diego" } ], "description": "ESMValCore: A community tool for pre-processing data from Earth system models in CMIP and running analysis scripts.", diff --git a/CITATION.cff b/CITATION.cff index d188af5d39..b7c116e3e5 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -204,6 +204,10 @@ authors: family-names: Martin-Martinez given-names: Eneko orcid: "https://orcid.org/0000-0002-9213-7818" + - + affiliation: "DLR, Germany" + family-names: Cammarano + given-names: Diego cff-version: 1.2.0 date-released: 2023-12-19 diff --git a/README.md b/README.md index f2039f5e8b..b256b0be40 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Chat on Matrix](https://matrix.to/img/matrix-badge.svg)](https://matrix.to/#/#ESMValGroup_Lobby:gitter.im) [![CircleCI](https://circleci.com/gh/ESMValGroup/ESMValCore/tree/main.svg?style=svg)](https://circleci.com/gh/ESMValGroup/ESMValCore/tree/main) [![codecov](https://codecov.io/gh/ESMValGroup/ESMValCore/branch/main/graph/badge.svg?token=wQnDzguwq6)](https://codecov.io/gh/ESMValGroup/ESMValCore) -[![Codacy Badge](https://app.codacy.com/project/badge/Grade/5d496dea9ef64ec68e448a6df5a65783)](https://www.codacy.com/gh/ESMValGroup/ESMValCore?utm_source=github.com&utm_medium=referral&utm_content=ESMValGroup/ESMValCore&utm_campaign=Badge_Grade) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/5d496dea9ef64ec68e448a6df5a65783)](https://app.codacy.com/gh/ESMValGroup/ESMValCore/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![Docker Build Status](https://img.shields.io/docker/cloud/build/esmvalgroup/esmvalcore)](https://hub.docker.com/r/esmvalgroup/esmvalcore/) [![Anaconda-Server Badge](https://img.shields.io/conda/vn/conda-forge/ESMValCore?color=blue&label=conda-forge&logo=conda-forge&logoColor=white)](https://anaconda.org/conda-forge/esmvalcore) [![Github Actions Test](https://github.com/ESMValGroup/ESMValCore/actions/workflows/run-tests.yml/badge.svg)](https://github.com/ESMValGroup/ESMValCore/actions/workflows/run-tests.yml) diff --git a/conda-linux-64.lock b/conda-linux-64.lock index c2d8d02ef5..68041614c5 100644 --- a/conda-linux-64.lock +++ b/conda-linux-64.lock @@ -1,6 +1,6 @@ # Generated by conda-lock. # platform: linux-64 -# input_hash: 69ce949889818c504d81d521be29fb77d725352f185b4be94a01411d8bff16a5 +# input_hash: 8401422fe0aac12c225a68ec94d2e6160210768cc54652a8965089c05890477e @EXPLICIT https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca054_0.conda#2f4327a1cbe7f022401b236e915a5fef @@ -8,52 +8,52 @@ https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2#34893075a5c9e55cdafac56607368fc6 https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_1.conda#6185f640c43843e5ad6fd1c5372c3f80 -https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-2.6.32-he073ed8_16.conda#7ca122655873935e02c91279c5b03c8c +https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-2.6.32-he073ed8_17.conda#d731b543793afc0433c4fd593e693fce https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h41732ed_0.conda#7aca3059a1729aa76c597603f10b0dd3 -https://conda.anaconda.org/conda-forge/linux-64/libboost-headers-1.84.0-ha770c72_0.conda#9c595e87653a36aa4d8c71b4e2f7e586 +https://conda.anaconda.org/conda-forge/linux-64/libboost-headers-1.84.0-ha770c72_2.conda#85d30a3fcc0f1cfc252776208af546a1 https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-12.3.0-h8bca6fd_105.conda#e12ce6b051085b8f27e239f5e5f5bce5 https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-12.3.0-h8bca6fd_105.conda#b3c6062c84a8e172555ee104ea6a01ab https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h7e041cc_5.conda#f6f6600d18a4047b54f803cf708b868a -https://conda.anaconda.org/conda-forge/linux-64/pandoc-3.1.11.1-ha770c72_0.conda#0e2f14aff42adf4675bcd5335d644a5f +https://conda.anaconda.org/conda-forge/linux-64/pandoc-3.1.12.3-ha770c72_0.conda#cdea66892b19a454f939487318b6c517 https://conda.anaconda.org/conda-forge/noarch/poppler-data-0.4.12-hd8ed1ab_0.conda#d8d7293c5b37f39b2ac32940621c6592 https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.11-4_cp311.conda#d786502c97404c94d7d58d258a445a65 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-h807b86a_5.conda#d211c42b9ce49aee3734fdc828731689 -https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.12-he073ed8_16.conda#071ea8dceff4d30ac511f4a2f8437cd1 +https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.12-he073ed8_17.conda#595db67e32b276298ff3d94d07d47fbf https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.40-hf600244_0.conda#33084421a8c0af6aef1b439707f7662a https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab https://conda.anaconda.org/conda-forge/linux-64/binutils-2.40-hdd6e379_0.conda#ccc940fddbc3fcd3d79cd4c654c4b5c4 -https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.40-hbdbef99_2.conda#adfebae9fdc63a598495dfe3b006973a +https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.40-hdade7a5_3.conda#2d9a60578bc28469d9aeef9aea5520c3 https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h807b86a_5.conda#d4ff227c46917d3b4565302a2bbb276b -https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.12-hd590300_0.conda#7dbb94ffb9df66406f3101625807cac1 +https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.14-hd590300_0.conda#d44fe0d9a6971a4fb245be0055775d9d https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda#69b8b6202a07720f448be700e300ccf4 -https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.26.0-hd590300_0.conda#a86d90025198fd411845fc245ebc06c8 +https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.28.1-hd590300_0.conda#dcde58ff9a1f30b0037a2315d1846d1f +https://conda.anaconda.org/conda-forge/linux-64/fmt-10.2.1-h00ab1b0_0.conda#35ef8bc24bd34074ebae3c943d551728 https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.10-h36c2ea0_0.tar.bz2#ac7bc6a654f8f41b352b38f4051135f8 https://conda.anaconda.org/conda-forge/linux-64/geos-3.12.1-h59595ed_0.conda#8c0f4f71f5a59ceb0c6fa9f51501066d https://conda.anaconda.org/conda-forge/linux-64/gettext-0.21.1-h27087fc_0.tar.bz2#14947d8770185e5153fdd04d4673ed37 https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-he1b5a44_1004.tar.bz2#cddaf2c63ea4a5901cf09524c490ecdc https://conda.anaconda.org/conda-forge/linux-64/giflib-5.2.1-h0b41bf4_3.conda#96f3b11872ef6fad973eac856cd2624f -https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.13-h58526e2_1001.tar.bz2#8c54672728e8ec6aa6db90cf2806d220 +https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.13-h59595ed_1003.conda#f87c7b7c2cb45f323ffbce941c78ab7c https://conda.anaconda.org/conda-forge/linux-64/icu-73.2-h59595ed_0.conda#cc47e1facc155f91abd89b11e48e72ff https://conda.anaconda.org/conda-forge/linux-64/json-c-0.17-h7ab15ed_0.conda#9961b1f100c3b6852bd97c9233d06979 https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2#30186d27e2c9fa62b45fb1476b7200e3 https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h27087fc_0.tar.bz2#76bbff344f0134279f225174e9064c8f -https://conda.anaconda.org/conda-forge/linux-64/libabseil-20230802.1-cxx17_h59595ed_0.conda#2785ddf4cb0e7e743477991d64353947 -https://conda.anaconda.org/conda-forge/linux-64/libaec-1.1.2-h59595ed_1.conda#127b0be54c1c90760d7fe02ea7a56426 +https://conda.anaconda.org/conda-forge/linux-64/libabseil-20240116.1-cxx17_h59595ed_2.conda#75648bc5dd3b8eab22406876c24d81ec +https://conda.anaconda.org/conda-forge/linux-64/libaec-1.1.3-h59595ed_0.conda#5e97e271911b8b2001a8b71860c32faa https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.1.0-hd590300_1.conda#aec6c91c7371c26392a06708a73c70e5 https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2#c965a5aa0d5c1c37ffc62dff36e28400 -https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.19-hd590300_0.conda#1635570038840ee3f9c71d22aa5b8b6d +https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.20-hd590300_0.conda#8e88f9389f1165d7c0936fe40d9a9a79 https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda#172bf1cd1ff8629f2b1179945ed45055 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.5.0-hcb278e6_1.conda#6305a3dd2752c76335295da4e581f2fd +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.2-h59595ed_0.conda#e7ba12deb7020dd080c6c70e7b6f6a3d https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2#d645c6d2ac96843a2bfaccd2d62b3ac3 https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-ha4646dd_5.conda#7a6bd7a12a4bd359e2afe6c0fa1acace https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.17-hd590300_2.conda#d66573916ffcf376178462f1b61c941e https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.0.0-hd590300_1.conda#ea25936bb4080d843790b586850f82b8 https://conda.anaconda.org/conda-forge/linux-64/libnl-3.9.0-hd590300_0.conda#d27c451db4f1d3c983c78167d2fdabc2 https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda#30fd6e37fe21f86f4bd26d6ee73eeec7 -https://conda.anaconda.org/conda-forge/linux-64/libnuma-2.0.16-h0b41bf4_1.conda#28bfe2cb11357ccc5be21101a6b7ce86 https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.3.0-h0f45ef3_5.conda#11d1ceacff40054d5a74b12975d76f20 https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.18-h36c2ea0_1.tar.bz2#c3788462a6fbddafdb413a9f9053e58d https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.8.0-h166bdaf_0.tar.bz2#ede4266dc02e875fe1ea77b25dd43747 @@ -63,14 +63,14 @@ https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.cond https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda#f36c115f1ee199da648e0597ec2047ad https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.9.4-hcb278e6_0.conda#318b08df404f9c9be5712aaa5a6f0bb0 https://conda.anaconda.org/conda-forge/linux-64/lzo-2.10-h516909a_1000.tar.bz2#bb14fcb13341b81d5eb386423b9d2bac -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4-h59595ed_2.conda#7dbaa197d7ba6032caf7ae7f32c1efa0 +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4.20240210-h59595ed_0.conda#97da8860a0da5413c7c98a3b3838a645 https://conda.anaconda.org/conda-forge/linux-64/nspr-4.35-h27087fc_0.conda#da0ec11a6454ae19bff5b02ed881a2b1 -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.1-hd590300_0.conda#51a753e64a3027bd7e23a189b1f6e91e +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.1-hd590300_1.conda#9d731343cff6ee2e5a25c4a091bf8e2a https://conda.anaconda.org/conda-forge/linux-64/pixman-0.43.2-h59595ed_0.conda#71004cbf7924e19c02746ccde9fd7123 https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-h36c2ea0_1001.tar.bz2#22dad4df6e8630e8dff2428f6f6a7036 https://conda.anaconda.org/conda-forge/linux-64/snappy-1.1.10-h9fff704_0.conda#e6d228cd0bb74a51dd18f5bfce0b4115 https://conda.anaconda.org/conda-forge/linux-64/tzcode-2024a-h3f72095_0.conda#32146e34aaec3745a08b6f49af3f41b0 -https://conda.anaconda.org/conda-forge/linux-64/uriparser-0.9.7-hcb278e6_1.conda#2c46deb08ba9b10e90d0a6401ad65deb +https://conda.anaconda.org/conda-forge/linux-64/uriparser-0.9.7-h59595ed_1.conda#c5edf07141147789784f89d5b4e4a9ad https://conda.anaconda.org/conda-forge/linux-64/xorg-kbproto-1.0.7-h7f98852_1002.tar.bz2#4b230e8381279d76131116660f5a241a https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.1-hd590300_0.conda#b462a33c0be1421532f28bfe8f4a7514 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.11-hd590300_0.conda#2c80dc38fface310c9bd81b17037fee5 @@ -81,13 +81,13 @@ https://conda.anaconda.org/conda-forge/linux-64/xorg-xproto-7.0.31-h7f98852_1007 https://conda.anaconda.org/conda-forge/linux-64/xxhash-0.8.2-hd590300_0.conda#f08fb5c89edfc4aadee1c81d4cfb1fa1 https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2#2161070d867d1b1204ea749c8eec4ef0 https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2#4cb3ad778ec2d5a7acbdf254eb1c42ae -https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.6.9-h14ec70c_3.conda#7da4b84275e63f56d158d6250727a70f -https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.17-h572eabf_8.conda#cc6630010cb1211cc15fb348f7c7eb70 -https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.14-h572eabf_0.conda#42db61eee93a2c0f918d18bd4422d331 -https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.17-h572eabf_7.conda#f7323eedc2685a24661cd6b57d7ed321 -https://conda.anaconda.org/conda-forge/linux-64/expat-2.5.0-hcb278e6_1.conda#8b9b5aca60558d02ddaa09d599e55920 +https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.6.10-ha9bf9b1_2.conda#ce2471034f5459a39636aacc292c96b6 +https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.18-h4466546_2.conda#b0d9153fc7cfa8dc36b8703e1a59f5f3 +https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.15-h4466546_2.conda#258194cedccd33fd8a7b95a8aa105015 +https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.18-h4466546_2.conda#8a04fc5a5ecaba31f66904b47dcc7797 +https://conda.anaconda.org/conda-forge/linux-64/expat-2.6.2-h59595ed_0.conda#53fb86322bdb89496d7579fe3f02fd61 https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.3.0-he2b93b0_5.conda#e89827619e73df59496c708b94f6f3d5 -https://conda.anaconda.org/conda-forge/linux-64/glog-0.6.0-h6f12383_0.tar.bz2#b31f3565cb84435407594e548a2fb7b2 +https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.0-hed5481d_0.conda#a9ea19c48e11754899299f8123070f4e https://conda.anaconda.org/conda-forge/linux-64/hdf4-4.2.15-h2a13503_7.conda#bd77f8da987968ec3927990495dc22e4 https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hd590300_1.conda#f07002e225d7a60a694d42a7bf5ff53f https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.1.0-hd590300_1.conda#5fc11c6020d421960607d821310fcd4d @@ -96,56 +96,57 @@ https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_5.conda#e73e9cfd1191783392131e6238bdb3e9 https://conda.anaconda.org/conda-forge/linux-64/libkml-1.3.0-h01aab08_1018.conda#3eb5f16bcc8a02892199aa63555c731f https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.58.0-h47da74e_1.conda#700ac6ea6d53d5510591c4344d5c989a -https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.42-h2797004_0.conda#d67729828dc6ff7ba44a61062ad79880 -https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-4.25.1-hf27288f_1.conda#78ad06185133494138cd5e922ed73ac7 -https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2023.06.02-h7a70373_0.conda#c0e7eacd9694db3ef5ef2979a7deea70 +https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.43-h2797004_0.conda#009981dd9cfcaa4dbfa25ffaed86bcae +https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-4.25.3-h08a7969_0.conda#6945825cebd2aeb16af4c69d97c32c13 +https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2023.09.01-h5a48ba9_2.conda#41c69fba59d495e8cf5ffda48a607e35 https://conda.anaconda.org/conda-forge/linux-64/librttopo-1.1.0-h8917695_15.conda#20c3c14bc491f30daecaa6f73e2223ae -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.1-h2797004_0.conda#fc4ccadfbf6d4784de88c41704792562 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.2-h2797004_0.conda#866983a220e27a80cb75e85cb30466a1 https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.0-h0841786_0.conda#1f5a58e686b13bcfde88b93f547d23fe https://conda.anaconda.org/conda-forge/linux-64/libudunits2-2.2.28-h40f5838_3.conda#4bdace082e911a3e1f1f0b721bed5b56 https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.15-h0b41bf4_0.conda#33277193f5b92bad9fdd230eb700929c -https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.5-h232c23b_0.conda#c442ebfda7a475f5e78f1c8e45f1e919 +https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.6-h232c23b_1.conda#6853448e9ca1cfd5f15382afd2a6d123 https://conda.anaconda.org/conda-forge/linux-64/libzip-1.10.1-h2629f0a_3.conda#ac79812548e7e8cf61f7b0abdef01d3b -https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.42-hcad00b1_0.conda#679c8961826aa4b50653bce17ee52abe -https://conda.anaconda.org/conda-forge/linux-64/rdma-core-50.0-hd3aeb46_0.conda#4594b391274e38f07c668acb45285a1f +https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.43-hcad00b1_0.conda#8292dea9e022d9610a11fce5e0896ed8 +https://conda.anaconda.org/conda-forge/linux-64/rdma-core-50.0-hd3aeb46_1.conda#f462219598fcf46c0cdfb985c3482b4f https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda#47d31b792659ce70f470b5c82fdfb7a4 -https://conda.anaconda.org/conda-forge/linux-64/s2n-1.4.3-h06160fa_0.conda#860332295eef2ef9fef370f365ea78b4 +https://conda.anaconda.org/conda-forge/linux-64/s2n-1.4.8-h06160fa_0.conda#0240a49dffea6daea27aa388663edcab +https://conda.anaconda.org/conda-forge/linux-64/spdlog-1.12.0-hd2e6256_2.conda#f37afc6ce10d45b9fae2f55ddc635b9f https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda#d453b98d9c83e71da0741bb0ff4d76bc https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.4-h7391055_0.conda#93ee23f12bc2e684548181256edd2cf6 -https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h59595ed_0.conda#8851084c192dbc56215ac4e3c9aa30fa +https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h59595ed_1.conda#7fc9d3288d2420bb3637647621018000 https://conda.anaconda.org/conda-forge/linux-64/zlib-1.2.13-hd590300_5.conda#68c34ec6149623be41a1933ab996a209 https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.5-hfc55251_0.conda#04b88013080254850d6c01ed54810589 -https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.14.3-h3c8c088_1.conda#12af79204e13550614bc51bb380c32e5 +https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.14.6-h96cd748_2.conda#cbf8138080ea12e9d9d66cf7c8bee325 https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.5-h0f2a231_0.conda#009521b7ed97cca25f8f997f9e745976 https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.1.0-hd590300_1.conda#39f910d205726805a958da408ca194ba https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda#9ae35c3d96db2c94ce0cef86efdfa2cb -https://conda.anaconda.org/conda-forge/linux-64/gcc-12.3.0-h8d2909c_2.conda#e2f2f81f367e14ca1f77a870bda2fe59 -https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-12.3.0-h76fc315_2.conda#11517e7b5c910c5b5d6985c0c7eb7f50 +https://conda.anaconda.org/conda-forge/linux-64/gcc-12.3.0-h95e488c_3.conda#413e326f8a01d041ffbfbb51cea46a93 +https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-12.3.0-h6477408_3.conda#7a53f84c45bdf4656ba27b9e9ed68b3d https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-12.3.0-hfcedea8_5.conda#4d72ee7c82f8a9b2ecef4fcefa9acd19 https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.3.0-he2b93b0_5.conda#cddba8fd94e52012abea1caad722b9c2 https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda#cd95826dbd331ed1be26bdf401432844 https://conda.anaconda.org/conda-forge/linux-64/libarchive-3.7.2-h2aa1ff5_1.conda#3bf887827d1968275978361a6e405e4f -https://conda.anaconda.org/conda-forge/linux-64/libglib-2.78.3-h783c2da_0.conda#9bd06b12bbfa6fd1740fd23af4b0f0c7 -https://conda.anaconda.org/conda-forge/linux-64/libllvm15-15.0.7-hb3ce162_4.conda#8a35df3cbc0c8b12cc8af9473ae75eef +https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.0-hf2295e7_1.conda#0725f6081030c29b109088639824ff90 +https://conda.anaconda.org/conda-forge/linux-64/libllvm16-16.0.6-hb3ce162_3.conda#a4d48c40dd5c60edbab7fd69c9a88967 https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.26-pthreads_h413a1c8_0.conda#760ae35415f5ba8b15d09df5afe8b23a https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.19.0-hb90f79a_1.conda#8cdb7d41faa0260875ba92414c487e2d -https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-ha9c0a0a_2.conda#55ed21669b2015f77c180feb1dd41930 +https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-h1dd3fc0_3.conda#66f03896ffbe1a110ffda05c7a856504 https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.39-h76b75d6_0.conda#e71f31f8cfb0a91439f2086fc8aa0461 -https://conda.anaconda.org/conda-forge/linux-64/minizip-4.0.4-h0ab5242_0.conda#813bc75d9c33ddd9c9d5b8d9c560e152 -https://conda.anaconda.org/conda-forge/linux-64/nss-3.97-h1d7d5a4_0.conda#b916d71a3032416e3f9136090d814472 -https://conda.anaconda.org/conda-forge/linux-64/orc-1.9.2-h7829240_1.conda#306ffb76ce3cdfc539d29fa5b8dd716c -https://conda.anaconda.org/conda-forge/linux-64/python-3.11.7-hab00c5b_1_cpython.conda#27cf681282c11dba7b0b1fd266e8f289 -https://conda.anaconda.org/conda-forge/linux-64/re2-2023.06.02-h2873b5e_0.conda#bb2d5e593ef13fe4aff0bc9440f945ae -https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.45.1-h2c6b66d_0.conda#93acf31b379acebada263b9bce3dc6ed -https://conda.anaconda.org/conda-forge/linux-64/ucx-1.15.0-h75e419f_3.conda#5baf4efbca923cdf73490c62cc7de1e2 +https://conda.anaconda.org/conda-forge/linux-64/minizip-4.0.5-h0ab5242_0.conda#557396140c71eba588e96d597e0c61aa +https://conda.anaconda.org/conda-forge/linux-64/nss-3.98-h1d7d5a4_0.conda#54b56c2fdf973656b748e0378900ec13 +https://conda.anaconda.org/conda-forge/linux-64/orc-2.0.0-h1e5e2c1_0.conda#53e8f030579d34e1a36a735d527c021f +https://conda.anaconda.org/conda-forge/linux-64/python-3.11.8-hab00c5b_0_cpython.conda#2fdc314ee058eda0114738a9309d3683 +https://conda.anaconda.org/conda-forge/linux-64/re2-2023.09.01-h7f4b329_2.conda#8f70e36268dea8eb666ef14c29bd3cda +https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.45.2-h2c6b66d_0.conda#1423efca06ed343c1da0fc429bae0779 +https://conda.anaconda.org/conda-forge/linux-64/ucx-1.15.0-h11edf95_7.conda#20a94f617ad76922f8737ad1fe317f4d https://conda.anaconda.org/conda-forge/linux-64/udunits2-2.2.28-h40f5838_3.conda#6bb8deb138f87c9d48320ac21b87e7a1 https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.7-h8ee46fc_0.conda#49e482d882669206653b095f5206c05b https://conda.anaconda.org/conda-forge/noarch/alabaster-0.7.16-pyhd8ed1ab_0.conda#def531a3ac77b7fb8c21d17bb5d0badb https://conda.anaconda.org/conda-forge/noarch/antlr-python-runtime-4.11.1-pyhd8ed1ab_0.tar.bz2#15109c4977d39ad7aa3423f57243e286 https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.38.0-hd4edc92_1.tar.bz2#6c72ec3e660a51736913ef6ea68c454b https://conda.anaconda.org/conda-forge/noarch/attrs-23.2.0-pyh71513ae_0.conda#5e4c0743c70186509d1412e03c2d8dfa -https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.4.1-h17cd1f3_5.conda#65d1aabc7656d7c08585efd584332235 -https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.8.0-hc6da83f_5.conda#a257c3335609a22036947f99a87ca024 +https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.4.2-he635cd5_6.conda#58fc78e523e35a08423c913751a51fde +https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.8.1-hbfc29b2_7.conda#8476ec099649e9a6de52f7f4d916cd2a https://conda.anaconda.org/conda-forge/linux-64/backports.zoneinfo-0.2.1-py311h38be061_8.conda#5384590f14dfe6ccd02811236afc9f8e https://conda.anaconda.org/conda-forge/linux-64/brotli-1.1.0-hd590300_1.conda#f27a24d46e3ea7b70a1f98e50c62508f https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py311hb755f60_1.conda#cce9e7c3f1c307f2a5fb08a2922d6164 @@ -158,7 +159,7 @@ https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.0.0-pyhd8ed1ab_0.con https://conda.anaconda.org/conda-forge/noarch/codespell-2.2.6-pyhd8ed1ab_0.conda#a206349b7bb7475ae580f987cb425bdd https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2#3faab06a954c2a04039983f2c4a50d99 https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_0.conda#5cd86562580f274031ede6aa6aa24441 -https://conda.anaconda.org/conda-forge/linux-64/cython-3.0.8-py311hb755f60_0.conda#28778bfea41b0f34141208783882649b +https://conda.anaconda.org/conda-forge/linux-64/cython-3.0.10-py311hb755f60_0.conda#f3a8a500a2e743ff92f418f0eaf9bf71 https://conda.anaconda.org/conda-forge/noarch/decorator-5.1.1-pyhd8ed1ab_0.tar.bz2#43afe5ab04e35e17ba28649471dd7364 https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2#961b3a227b437d82ad7054484cfa71b2 https://conda.anaconda.org/conda-forge/noarch/dill-0.3.8-pyhd8ed1ab_0.conda#78745f157d56877a2c6e7b386f66f3e2 @@ -169,17 +170,17 @@ https://conda.anaconda.org/conda-forge/noarch/entrypoints-0.4-pyhd8ed1ab_0.tar.b https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda#8d652ea2ee8eaee02ed8dc820bc794aa https://conda.anaconda.org/conda-forge/noarch/execnet-2.0.2-pyhd8ed1ab_0.conda#67de0d8241e1060a479e3c37793e26f9 https://conda.anaconda.org/conda-forge/noarch/executing-2.0.1-pyhd8ed1ab_0.conda#e16be50e378d8a4533b989035b196ab8 -https://conda.anaconda.org/conda-forge/noarch/filelock-3.13.1-pyhd8ed1ab_0.conda#0c1729b74a8152fde6a38ba0a2ab9f45 +https://conda.anaconda.org/conda-forge/noarch/filelock-3.13.3-pyhd8ed1ab_0.conda#ff15f46b0d34308f4d40c1c51df07592 https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.14.2-h14ed4e7_0.conda#0f69b688f52ff6da70bccb7ff7001d1d https://conda.anaconda.org/conda-forge/linux-64/freexl-2.0.0-h743c826_0.conda#12e6988845706b2cfbc3bc35c9a61a95 -https://conda.anaconda.org/conda-forge/noarch/fsspec-2024.2.0-pyhca7485f_0.conda#fad86b90138cf5d82c6f5a2ed6e683d9 -https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.42.10-h829c605_4.conda#252a696860674caf7a855e16f680d63a -https://conda.anaconda.org/conda-forge/noarch/geographiclib-1.52-pyhd8ed1ab_0.tar.bz2#6880e7100ebae550a33ce26663316d85 -https://conda.anaconda.org/conda-forge/linux-64/gfortran-12.3.0-h499e0f7_2.conda#0558a8c44eb7a18e6682bd3a8ae6dcab -https://conda.anaconda.org/conda-forge/linux-64/gfortran_linux-64-12.3.0-h7fe76b4_2.conda#3a749210487c0358b6f135a648cbbf60 +https://conda.anaconda.org/conda-forge/noarch/fsspec-2024.3.1-pyhca7485f_0.conda#b7f0662ef2c9d4404f0af9eef5ed2fde +https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.42.10-h829c605_5.conda#8fdb82e5d9694dd8e9ed9ac8fdf48a26 +https://conda.anaconda.org/conda-forge/noarch/geographiclib-2.0-pyhd8ed1ab_0.tar.bz2#6b1f32359fc5d2ab7b491d0029bfffeb +https://conda.anaconda.org/conda-forge/linux-64/gfortran-12.3.0-h7389182_3.conda#6b0b27394cf439d0540f949190556860 +https://conda.anaconda.org/conda-forge/linux-64/gfortran_linux-64-12.3.0-h617cb40_3.conda#3a9e5b8a6f651ff14e74d896d8f04ab6 https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h977cf35_4.conda#4d8df0b0db060d33c9a702ada998a8fe -https://conda.anaconda.org/conda-forge/linux-64/gxx-12.3.0-h8d2909c_2.conda#673bac341be6b90ef9e8abae7e52ca46 -https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-12.3.0-h8a814eb_2.conda#f517b1525e9783849bd56a5dc45a9960 +https://conda.anaconda.org/conda-forge/linux-64/gxx-12.3.0-h95e488c_3.conda#8c50a4d15a8d4812af563a684d598910 +https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-12.3.0-h4a1b8e8_3.conda#9ec22c7c544f4a4f6d660f0a3b0fd15c https://conda.anaconda.org/conda-forge/noarch/humanfriendly-10.0-pyhd8ed1ab_6.conda#2ed1fe4b9079da97c44cfe9c2e5078fd https://conda.anaconda.org/conda-forge/noarch/idna-3.6-pyhd8ed1ab_0.conda#1a76f09108576397c41c0b0c5bd84134 https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2#7de5386c8fea29e76b303f37dde4c352 @@ -189,9 +190,9 @@ https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.5-py311h9547e67_1 https://conda.anaconda.org/conda-forge/linux-64/lazy-object-proxy-1.10.0-py311h459d7ec_0.conda#d39020c78fd00ed774ff9c876e8aba07 https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.16-hb7c19ff_0.conda#51bb7010fc86f70eee639b4bb7a894f5 https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-21_linux64_openblas.conda#0ac9f44fc096772b0aa092119b00c3ca -https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.5.0-hca28451_0.conda#7144d5a828e2cae218e0e3c98d8a0aeb -https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.60.0-h74775cd_1.conda#e5dac7b919ed16dbcf9dc0f512cb68c0 -https://conda.anaconda.org/conda-forge/linux-64/libpq-16.2-h33b98f1_0.conda#fe0e297faf462ee579c95071a5211665 +https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.7.1-hca28451_0.conda#755c7f876815003337d2c61ff5d047e5 +https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.62.1-h15f2491_0.conda#564517a8cbd095cff75eb996d33d2b7e +https://conda.anaconda.org/conda-forge/linux-64/libpq-16.2-h33b98f1_1.conda#9e49ec2a61d02623b379dc332eb6889d https://conda.anaconda.org/conda-forge/linux-64/libwebp-1.3.2-h658648e_1.conda#0ebb65e8d86843865796c7c95a941f34 https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2#91e27ef3d05cc772ce627e51cff111c4 https://conda.anaconda.org/conda-forge/linux-64/lxml-5.1.0-py311h9691dec_0.conda#cee803b62c62e5f3326be31e57161ff5 @@ -203,8 +204,8 @@ https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.0.7-py311h9547e https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda#4eccaeba205f0aed9ac3a9ea58568ca3 https://conda.anaconda.org/conda-forge/noarch/networkx-3.2.1-pyhd8ed1ab_0.conda#425fce3b531bed6ec3c74fab3e5f0a1c -https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.0-h488ebb8_3.conda#128c25b7fe6a25286a48f3a6a9b5b6f3 -https://conda.anaconda.org/conda-forge/noarch/packaging-23.2-pyhd8ed1ab_0.conda#79002079284aa895f883c6b7f3f88fd6 +https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.2-h488ebb8_0.conda#7f2e286780f072ed750df46dc2631138 +https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda#248f521b64ce055e7feae3105e7abeb8 https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2#457c2c8c08e54905d6954e79cb5b5db9 https://conda.anaconda.org/conda-forge/noarch/parso-0.8.3-pyhd8ed1ab_0.tar.bz2#17a565a0c3899244e938cdf417e7b094 https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_0.conda#17064acba08d3686f1135b5ec1b32b12 @@ -216,22 +217,22 @@ https://conda.anaconda.org/conda-forge/linux-64/psutil-5.9.8-py311h459d7ec_0.con https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd3deb0d_0.tar.bz2#359eeb6536da0e687af562ed265ec263 https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.2-pyhd8ed1ab_0.tar.bz2#6784285c7e55cb7212efabc79e4c2883 https://conda.anaconda.org/conda-forge/noarch/pycodestyle-2.9.1-pyhd8ed1ab_0.tar.bz2#0191dd7efe1a94262812770183b68892 -https://conda.anaconda.org/conda-forge/noarch/pycparser-2.21-pyhd8ed1ab_0.tar.bz2#076becd9e05608f8dc72757d5f3a91ff +https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyhd8ed1ab_0.conda#844d9eb3b43095b031874477f7d70088 https://conda.anaconda.org/conda-forge/noarch/pyflakes-2.5.0-pyhd8ed1ab_0.tar.bz2#1b3bef4313288ae8d35b1dfba4cd84a3 https://conda.anaconda.org/conda-forge/noarch/pygments-2.17.2-pyhd8ed1ab_0.conda#140a7f159396547e9799aa98f9f0742e -https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.1-pyhd8ed1ab_0.conda#176f7d56f0cfe9008bdf1bccd7de02fb +https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda#b9a4dacf97241704529131a0dfc0494f https://conda.anaconda.org/conda-forge/noarch/pyshp-2.3.1-pyhd8ed1ab_0.tar.bz2#92a889dc236a5197612bc85bee6d7174 https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2#2a7de29fb590ca14b5243c4c812c8025 https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.19.1-pyhd8ed1ab_0.conda#4d3ceee3af4b0f9a1f48f57176bf8625 -https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2023.4-pyhd8ed1ab_0.conda#c79cacf8a06a51552fc651652f170208 +https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2024.1-pyhd8ed1ab_0.conda#98206ea9954216ee7540f0c773f2104d https://conda.anaconda.org/conda-forge/linux-64/python-xxhash-3.4.1-py311h459d7ec_0.conda#60b5332b3989fda37884b92c7afd6a91 https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda#3eeeeb9e4827ace8c0c1419c85d590ad https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.1-py311h459d7ec_1.conda#52719a74ad130de8fb5d047dc91f247a https://conda.anaconda.org/conda-forge/linux-64/pyzmq-25.1.2-py311h34ded2d_0.conda#819aa640a0493d4b52faf938e94d129e -https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.17.1-py311h46250e7_0.conda#a206e8c500a27fa82adae7c2f1929675 +https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.18.0-py311h46250e7_0.conda#688a1190531dc4e8c00e25d0d1de4135 https://conda.anaconda.org/conda-forge/noarch/semver-3.0.2-pyhd8ed1ab_0.conda#5efb3fccda53974aed800b6d575f72ed https://conda.anaconda.org/conda-forge/noarch/setoptconf-tmp-0.3.1-pyhd8ed1ab_0.tar.bz2#af3e36d4effb85b9b9f93cd1db0963df -https://conda.anaconda.org/conda-forge/noarch/setuptools-69.0.3-pyhd8ed1ab_0.conda#40695fdfd15a92121ed2922900d0308b +https://conda.anaconda.org/conda-forge/noarch/setuptools-69.2.0-pyhd8ed1ab_0.conda#da214ecd521a720a9d521c68047682dc https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.0-pyhd8ed1ab_0.tar.bz2#62f26a3d1387acee31322208f0cfa3e0 https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2#4d22a9315e78c6827f806065957d566e @@ -243,18 +244,18 @@ https://conda.anaconda.org/conda-forge/noarch/tblib-3.0.0-pyhd8ed1ab_0.conda#04e https://conda.anaconda.org/conda-forge/noarch/termcolor-2.4.0-pyhd8ed1ab_0.conda#a5033708ad9283907c3b1bc1f90d0d0d https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2#f832c45a477c78bebd107098db465095 https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2#5844808ffab9ebdb694585b50ba02a96 -https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda#074d0ce7a6261ab8b497c3518796ef3e +https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.4-pyha770c72_0.conda#37c47ea93ef00dd80d880fc4ba21256a https://conda.anaconda.org/conda-forge/noarch/toolz-0.12.1-pyhd8ed1ab_0.conda#2fcb582444635e2c402e8569bb94e039 -https://conda.anaconda.org/conda-forge/linux-64/tornado-6.3.3-py311h459d7ec_1.conda#a700fcb5cedd3e72d0c75d095c7a6eda -https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.1-pyhd8ed1ab_0.conda#1c6acfdc7ecbfe09954c4216da99c146 -https://conda.anaconda.org/conda-forge/noarch/types-pyyaml-6.0.12.12-pyhd8ed1ab_0.conda#0cb14c80f66937df894d60626dd1921f -https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.9.0-pyha770c72_0.conda#a92a6440c3fe7052d63244f3aba2a4a7 +https://conda.anaconda.org/conda-forge/linux-64/tornado-6.4-py311h459d7ec_0.conda#cc7727006191b8f3630936b339a76cd0 +https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.2-pyhd8ed1ab_0.conda#af5fa2d2186003472e766a23c46cae04 +https://conda.anaconda.org/conda-forge/noarch/types-pyyaml-6.0.12.20240311-pyhd8ed1ab_0.conda#df5d4b66033ecb54c7a4040627215529 +https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.10.0-pyha770c72_0.conda#16ae769069b380646c47142d719ef466 https://conda.anaconda.org/conda-forge/linux-64/ujson-5.9.0-py311hb755f60_0.conda#36dda52dc99a4fb9cadd3b738ec24848 https://conda.anaconda.org/conda-forge/noarch/untokenize-0.1.1-py_0.tar.bz2#1447ead40f2a01733a9c8dfc32988375 https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_0.conda#68f0738df502a14213624b288c60c9ad https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_2.conda#daf5160ff9cde3a468556965329085b9 https://conda.anaconda.org/conda-forge/noarch/webob-1.8.7-pyhd8ed1ab_0.tar.bz2#a8192f3585f341ea66c60c189580ac67 -https://conda.anaconda.org/conda-forge/noarch/wheel-0.42.0-pyhd8ed1ab_0.conda#1cdea58981c5cbc17b51973bcaddcea7 +https://conda.anaconda.org/conda-forge/noarch/wheel-0.43.0-pyhd8ed1ab_1.conda#0b5293a157c2b5cd513dd1b03d8d3aae https://conda.anaconda.org/conda-forge/linux-64/wrapt-1.16.0-py311h459d7ec_0.conda#6669b5529d206c1f880b642cdd17ae05 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.4-h0b41bf4_2.conda#82b6df12252e6f32402b96dacc656fec https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.11-hd590300_0.conda#ed67c36f215b310412b2af935bf3e530 @@ -262,168 +263,170 @@ https://conda.anaconda.org/conda-forge/noarch/xyzservices-2023.10.1-pyhd8ed1ab_0 https://conda.anaconda.org/conda-forge/noarch/zict-3.0.0-pyhd8ed1ab_0.conda#cf30c2c15b82aacb07f9c09e28ff2275 https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda#2e4d6bc0b14e10f895fc6791a7d9b26a https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.4-pyhd8ed1ab_0.conda#46a2e6e3dfa718ce3492018d5a110dd6 -https://conda.anaconda.org/conda-forge/noarch/asgiref-3.7.2-pyhd8ed1ab_0.conda#596932155bf88bb6837141550cb721b0 +https://conda.anaconda.org/conda-forge/noarch/asgiref-3.8.1-pyhd8ed1ab_0.conda#b5c2e1034ccc76fb14031637924880eb https://conda.anaconda.org/conda-forge/linux-64/astroid-2.15.8-py311h38be061_0.conda#46d70fcb74472aab178991f0231ee3c6 https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda#5f25798dcefd8252ce5f9dc494d5f571 -https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.15-h70caa3e_0.conda#ac982c47c6439386e10afb4e0fe0d3e0 -https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.10.1-h0ef3971_3.conda#5f80f11865fad4cc684f1007170df6ec -https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.10.3-h91d86a7_1.conda#c05a913b8203d14b4a91c54d57b52282 +https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.16-haed3651_8.conda#ce96c083829ab2727c942243ac93ffe0 +https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.10.3-hffff1cc_2.conda#14ad8defb307e1edb293c3fc9da8648f +https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.11.1-h91d86a7_1.conda#2dbab1d281b7e1da05eee544cbdc8af6 https://conda.anaconda.org/conda-forge/noarch/babel-2.14.0-pyhd8ed1ab_0.conda#9669586875baeced8fc30c0826c3270e https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.12.3-pyha770c72_0.conda#332493000404d8411859539a5a630865 https://conda.anaconda.org/conda-forge/noarch/bleach-6.1.0-pyhd8ed1ab_0.conda#0ed9d7c0e9afa7c025807a9a8136ea3e https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda#f907bb958910dc404647326ca80c263e https://conda.anaconda.org/conda-forge/linux-64/cffi-1.16.0-py311hb3a22ac_0.conda#b3469563ac5e808b0cd92810d0697043 -https://conda.anaconda.org/conda-forge/linux-64/cfitsio-4.3.1-hbdc6101_0.conda#dcea02841b33a9c49f74ca9328de919a +https://conda.anaconda.org/conda-forge/linux-64/cfitsio-4.4.0-hbdc6101_0.conda#446ac3db6cb017e3dd067cc35cf51442 https://conda.anaconda.org/conda-forge/noarch/click-plugins-1.1.1-py_0.tar.bz2#4fd2c6b53934bd7d96d1f3fdaf99b79f https://conda.anaconda.org/conda-forge/noarch/cligj-0.7.2-pyhd8ed1ab_1.tar.bz2#a29b7c141d6b2de4bb67788a5f107734 -https://conda.anaconda.org/conda-forge/linux-64/coverage-7.4.1-py311h459d7ec_0.conda#9caf3270065a2d40fd9a443ba1568e96 +https://conda.anaconda.org/conda-forge/linux-64/coverage-7.4.4-py311h459d7ec_0.conda#1aa22cb84e68841ec206ee066457bdf0 https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.7.0-h00ab1b0_0.conda#b4537c98cb59f8725b0e1e65816b4a28 https://conda.anaconda.org/conda-forge/linux-64/cytoolz-0.12.3-py311h459d7ec_0.conda#13d385f635d7fbe9acc93600f67a6cb4 https://conda.anaconda.org/conda-forge/noarch/docformatter-1.7.5-pyhd8ed1ab_0.conda#3a941b6083e945aa87e739a9b85c82e9 -https://conda.anaconda.org/conda-forge/noarch/fire-0.5.0-pyhd8ed1ab_0.conda#9fd22aae8d2f319e80f68b295ab91d64 -https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.48.1-py311h459d7ec_0.conda#36363685b6e56682b1b256eb0ad503f6 +https://conda.anaconda.org/conda-forge/noarch/fire-0.6.0-pyhd8ed1ab_0.conda#e9ed10aa8fa1dd6782940b95c942a6ae +https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.50.0-py311h459d7ec_0.conda#fcdef52b45265eece45de756b164a9a7 https://conda.anaconda.org/conda-forge/linux-64/fortran-compiler-1.7.0-heb67821_0.conda#7ef7c0f111dad1c8006504a0f1ccd820 -https://conda.anaconda.org/conda-forge/noarch/geopy-2.4.1-pyhd8ed1ab_0.conda#c75621ce68f6570fff9a6734cf21c9a7 +https://conda.anaconda.org/conda-forge/noarch/geopy-2.4.1-pyhd8ed1ab_1.conda#358c17429c97883b2cb9ab5f64bc161b https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.11-pyhd8ed1ab_0.conda#623b19f616f2ca0c261441067e18ae40 https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.3-nompi_h4f84152_100.conda#d471a5c3abc984b662d9bae3bb7fd8a5 -https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-7.0.1-pyha770c72_0.conda#746623a787e06191d80a2133e5daff17 -https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.1.1-pyhd8ed1ab_0.conda#3d5fa25cf42f3f32a12b2d874ace8574 +https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-7.1.0-pyha770c72_0.conda#0896606848b2dc5cebdf111b6543aa04 +https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.0-pyhd8ed1ab_0.conda#c5d3907ad8bd7bf557521a1833cf7e6d https://conda.anaconda.org/conda-forge/noarch/isodate-0.6.1-pyhd8ed1ab_0.tar.bz2#4a62c93c1b5c0b920508ae3fd285eaf5 https://conda.anaconda.org/conda-forge/noarch/isort-5.13.2-pyhd8ed1ab_0.conda#1d25ed2b95b92b026aaa795eabec8d91 https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.1-pyhd8ed1ab_0.conda#81a3be0b2023e1ea8555781f0ad904a2 https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.3-pyhd8ed1ab_0.conda#e7d8df6509ba635247ff9aea31134262 -https://conda.anaconda.org/conda-forge/linux-64/jupyter_core-5.7.1-py311h38be061_0.conda#175a430872841f7c351879f4c4c85b9e +https://conda.anaconda.org/conda-forge/linux-64/jupyter_core-5.7.2-py311h38be061_0.conda#f85e78497dfed6f6a4b865191f42de2e https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_1.conda#afcd1b53bcac8844540358e33f33d28f https://conda.anaconda.org/conda-forge/noarch/latexcodec-2.0.1-pyh9f0ad1d_0.tar.bz2#8d67904973263afd2985ba56aa2d6bb4 https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-21_linux64_openblas.conda#4a3816d06451c4946e2db26b86472cb6 https://conda.anaconda.org/conda-forge/linux-64/libgd-2.3.3-h119a65a_9.conda#cfebc557e54905dadc355c0e9f003004 -https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.12.0-hef10d8f_5.conda#055e2266d27f0e2290cf0a6ad668a225 +https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.22.0-h9be4e54_1.conda#4b4e36a91e7dabf7345b82d85767a7c3 https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-21_linux64_openblas.conda#1a42f305615c3867684e049e85927531 https://conda.anaconda.org/conda-forge/noarch/logilab-common-1.7.3-py_0.tar.bz2#6eafcdf39a7eb90b6d951cfff59e8d3b https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.6-pyhd8ed1ab_0.tar.bz2#b21613793fcc81d944c76c9f2864a7de -https://conda.anaconda.org/conda-forge/linux-64/mypy-1.8.0-py311h459d7ec_0.conda#93b7b2391a045cea0d97772f550f1d77 +https://conda.anaconda.org/conda-forge/linux-64/mypy-1.9.0-py311h459d7ec_0.conda#e58bef0cd6e90e074db57962be787e13 https://conda.anaconda.org/conda-forge/noarch/nested-lookup-0.2.25-pyhd8ed1ab_1.tar.bz2#2f59daeb14581d41b1e2dda0895933b2 https://conda.anaconda.org/conda-forge/noarch/nodeenv-1.8.0-pyhd8ed1ab_0.conda#2a75b296096adabbabadd5e9782e5fcc https://conda.anaconda.org/conda-forge/noarch/partd-1.4.1-pyhd8ed1ab_0.conda#acf4b7c0bcd5fa3b0e05801c4d2accd6 https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_0.conda#629f3203c99b32e0988910c93e77f3b6 https://conda.anaconda.org/conda-forge/linux-64/pillow-10.2.0-py311ha6c5da5_0.conda#a5ccd7f2271f28b7d2de0b02b64e3796 https://conda.anaconda.org/conda-forge/noarch/pip-24.0-pyhd8ed1ab_0.conda#f586ac1e56c8638b64f9c8122a7b8a67 -https://conda.anaconda.org/conda-forge/linux-64/postgresql-16.2-h7387d8b_0.conda#4e86738066b4966f0357f661b3691cae +https://conda.anaconda.org/conda-forge/linux-64/postgresql-16.2-h82ecc9d_1.conda#7a5806219d0f77ce8393375d040df065 https://conda.anaconda.org/conda-forge/linux-64/proj-9.3.1-h1d62c97_0.conda#44ec51d0857d9be26158bb85caa74fdb https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.42-pyha770c72_0.conda#0bf64bf10eee21f46ac83c161917fa86 https://conda.anaconda.org/conda-forge/noarch/pydocstyle-6.3.0-pyhd8ed1ab_0.conda#7e23a61a7fbaedfef6eb0e1ac775c8e5 -https://conda.anaconda.org/conda-forge/noarch/pytest-8.0.0-pyhd8ed1ab_0.conda#5ba1cc5b924226349d4a49fb547b7579 -https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.8.2-pyhd8ed1ab_0.tar.bz2#dd999d1cc9f79e67dbb855c8924c7984 -https://conda.anaconda.org/conda-forge/noarch/referencing-0.33.0-pyhd8ed1ab_0.conda#bc415a1c6cf049166215d6b596e0fcbe +https://conda.anaconda.org/conda-forge/noarch/pytest-8.1.1-pyhd8ed1ab_0.conda#94ff09cdedcb7b17e9cd5097ee2cfcff +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda#2cf4264fffb9e6eff6031c5b6884d61c +https://conda.anaconda.org/conda-forge/noarch/referencing-0.34.0-pyhd8ed1ab_0.conda#e4492c22e314be5c75db3469e3bbf3d9 https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.2.1-pyhd8ed1ab_0.tar.bz2#7234c9eefff659501cd2fe0d2ede4d48 -https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.9.0-hd8ed1ab_0.conda#c16524c1b7227dc80b36b4fa6f77cc86 +https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.10.0-hd8ed1ab_0.conda#091683b9150d2ebaa62fd7e2c86433da https://conda.anaconda.org/conda-forge/noarch/url-normalize-1.4.3-pyhd8ed1ab_0.tar.bz2#7c4076e494f0efe76705154ac9302ba6 -https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.0-pyhd8ed1ab_0.conda#6a7e0694921f668a030d52f0c47baebd -https://conda.anaconda.org/conda-forge/noarch/virtualenv-20.25.0-pyhd8ed1ab_0.conda#c119653cba436d8183c27bf6d190e587 +https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.1-pyhd8ed1ab_0.conda#08807a87fa7af10754d46f63b368e016 +https://conda.anaconda.org/conda-forge/noarch/virtualenv-20.25.1-pyhd8ed1ab_0.conda#8797a4e26be36880a603aba29c785352 https://conda.anaconda.org/conda-forge/linux-64/xerces-c-3.2.5-hac6953d_0.conda#63b80ca78d29380fe69e69412dcbe4ac https://conda.anaconda.org/conda-forge/noarch/yamale-4.0.4-pyh6c4a22f_0.tar.bz2#cc9f59f147740d88679bf1bd94dbe588 -https://conda.anaconda.org/conda-forge/noarch/yamllint-1.34.0-pyhd8ed1ab_0.conda#262273faaed1e4bdf0c61209c38abd15 -https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.5.0-h1b46bed_2.conda#cbbdaaec72d302636a64a3fcaa3a72c7 -https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.5.0-hb858b4b_2.conda#19f23b45d1925a9a8f701a3f6f9cce4f +https://conda.anaconda.org/conda-forge/noarch/yamllint-1.35.1-pyhd8ed1ab_0.conda#a1240b99a7ccd953879dc63111823986 +https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.5.4-h4893938_0.conda#4ccee5dfb44ad34d8bb30429f62273cc +https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.5.0-h94269e2_4.conda#f364272cb4c2f4ce2341067107b82865 https://conda.anaconda.org/conda-forge/noarch/cattrs-23.2.3-pyhd8ed1ab_0.conda#91fc4700dcce4a46d439900a132fe4e5 https://conda.anaconda.org/conda-forge/linux-64/compilers-1.7.0-ha770c72_0.conda#81458b3aed8ab8711951ec3c0c04e097 -https://conda.anaconda.org/conda-forge/linux-64/cryptography-42.0.2-py311hcb13ee4_0.conda#c61fd9e9fcfa599ea5a8b1de42b147a8 -https://conda.anaconda.org/conda-forge/noarch/django-5.0.2-pyhd8ed1ab_0.conda#596031b6473e1fead388589348472748 +https://conda.anaconda.org/conda-forge/linux-64/cryptography-42.0.5-py311h63ff55d_0.conda#76909c8c7b915f0af4f35e80da5f9a87 +https://conda.anaconda.org/conda-forge/noarch/django-5.0.3-pyhd8ed1ab_0.conda#5242811441d7edca3fa3026693bd9cd8 https://conda.anaconda.org/conda-forge/noarch/flake8-5.0.4-pyhd8ed1ab_0.tar.bz2#8079ea7dec0a917dd0cb6c257f7ea9ea https://conda.anaconda.org/conda-forge/linux-64/geotiff-1.7.1-h6b2125f_15.conda#218a726155bd9ae1787b26054eed8566 -https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.41-pyhd8ed1ab_0.conda#84874a90c312088f7b5e63402fc44a58 +https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.42-pyhd8ed1ab_0.conda#6bc8e496351bafd761c0922c3ebd989a https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.3.0-h3d44ed6_0.conda#5a6f6c00ef982a9bc83558d9ac8f64a0 -https://conda.anaconda.org/conda-forge/noarch/importlib_metadata-7.0.1-hd8ed1ab_0.conda#4a2f43a20fa404b998859c6a470ba316 +https://conda.anaconda.org/conda-forge/noarch/importlib_metadata-7.1.0-hd8ed1ab_0.conda#6ef2b72d291b39e479d7694efa2b2b98 https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2023.12.1-pyhd8ed1ab_0.conda#a0e4efb5f35786a05af4809a2fb1f855 https://conda.anaconda.org/conda-forge/linux-64/kealib-1.5.3-h2f55d51_0.conda#f7e7077802927590efc8bf7328208f12 +https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.22.0-hc7a4891_1.conda#7811f043944e010e54640918ea82cecd https://conda.anaconda.org/conda-forge/linux-64/libnetcdf-4.9.2-nompi_h9612171_113.conda#b2414908e43c442ddc68e6148774a304 https://conda.anaconda.org/conda-forge/linux-64/libspatialite-5.1.0-h7bd4643_4.conda#127d36f9ee392fa81b45e81867ce30ab https://conda.anaconda.org/conda-forge/linux-64/numpy-1.26.4-py311h64a7726_0.conda#a502d7aad449a1206efb366d6a12c52d -https://conda.anaconda.org/conda-forge/linux-64/poppler-24.02.0-h590f24d_0.conda#7e715c1572de09d6106c5a31fa70ffca +https://conda.anaconda.org/conda-forge/linux-64/poppler-24.03.0-h590f24d_0.conda#c688853df9dcfed47200d0e28e5dfe11 https://conda.anaconda.org/conda-forge/noarch/pybtex-0.24.0-pyhd8ed1ab_2.tar.bz2#2099b86a7399c44c0c61cdb6de6915ba https://conda.anaconda.org/conda-forge/noarch/pylint-2.17.7-pyhd8ed1ab_0.conda#3cab6aee60038b3f621bce3e50f52bed https://conda.anaconda.org/conda-forge/linux-64/pyproj-3.6.1-py311hca0b8b9_5.conda#cac429fcb9126d5e6f02c8ba61c2a811 -https://conda.anaconda.org/conda-forge/noarch/pytest-cov-4.1.0-pyhd8ed1ab_0.conda#06eb685a3a0b146347a58dda979485da +https://conda.anaconda.org/conda-forge/noarch/pytest-cov-5.0.0-pyhd8ed1ab_0.conda#c54c0107057d67ddf077751339ec2c63 https://conda.anaconda.org/conda-forge/noarch/pytest-env-1.1.3-pyhd8ed1ab_0.conda#1dbdf019d740419852c4a7803fff49d9 -https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.0-pyhd8ed1ab_0.conda#d87474b01a3e2b8e919a24b922463056 -https://conda.anaconda.org/conda-forge/noarch/pytest-mock-3.12.0-pyhd8ed1ab_0.conda#ac9fedc9a0c397f2318e82525491dd83 +https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.1-pyhd8ed1ab_0.conda#52b91ecba854d55b28ad916a8b10da24 +https://conda.anaconda.org/conda-forge/noarch/pytest-mock-3.14.0-pyhd8ed1ab_0.conda#4b9b5e086812283c052a9105ab1e254e https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.5.0-pyhd8ed1ab_0.conda#d5f595da2daead898ca958ac62f0307b https://conda.anaconda.org/conda-forge/noarch/rdflib-7.0.0-pyhd8ed1ab_0.conda#44d14ef95495b3d4438f28998e0296a9 https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda#a30144e4156cdbb236f99ebb49828f8b https://conda.anaconda.org/conda-forge/noarch/requirements-detector-1.2.2-pyhd8ed1ab_0.conda#6626918380d99292df110f3c91b6e5ec https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda#e7df0fdd404616638df5ece6e69ba7af -https://conda.anaconda.org/conda-forge/noarch/types-requests-2.31.0.20240125-pyhd8ed1ab_0.conda#0200f9f9bb98c12a172ae49ef7d83d60 +https://conda.anaconda.org/conda-forge/noarch/types-requests-2.31.0.20240311-pyhd8ed1ab_0.conda#3ee81b9baa97edc84698b0689e320278 https://conda.anaconda.org/conda-forge/linux-64/ukkonen-1.0.1-py311h9547e67_4.conda#586da7df03b68640de14dc3e8bcbf76f https://conda.anaconda.org/conda-forge/noarch/yapf-0.40.1-pyhd8ed1ab_0.conda#f269942e802d5e148632143d4c37acc9 -https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.26.1-h33f84b2_9.conda#feff02dd51629ad703677c28eaf03a1e -https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.10.0-h00ab1b0_0.conda#64eec459779f01803594f5272cdde23c +https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.26.4-hba3594f_2.conda#d464ebd32bea6638216bae1d406e2b15 +https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.10.0-h00ab1b0_1.conda#1e63d3866554a4d2e3d1cba5f21a2841 https://conda.anaconda.org/conda-forge/linux-64/cftime-1.6.3-py311h1f0f07a_0.conda#b7e6d52b39e199238c3400cafaabafb3 https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.2.0-py311h9547e67_0.conda#40828c5b36ef52433e21f89943e09f33 -https://conda.anaconda.org/conda-forge/noarch/dask-core-2024.2.0-pyhd8ed1ab_0.conda#5973bc565e2aea620c3a431cafdde032 +https://conda.anaconda.org/conda-forge/noarch/dask-core-2024.3.1-pyhd8ed1ab_0.conda#52dd56ce3afa6a52c2f3d3116875ff32 https://conda.anaconda.org/conda-forge/noarch/flake8-polyfill-1.0.2-py_0.tar.bz2#a53db35e3d07f0af2eccd59c2a00bffe -https://conda.anaconda.org/conda-forge/noarch/identify-2.5.34-pyhd8ed1ab_0.conda#048ba98aa7b16ef0d8866f8c87d7c5b8 -https://conda.anaconda.org/conda-forge/noarch/ipython-8.21.0-pyh707e725_0.conda#371344fdbdf9c70cfe9adb512a8cbca6 +https://conda.anaconda.org/conda-forge/noarch/identify-2.5.35-pyhd8ed1ab_0.conda#9472bfd206a2b7bb8143835e37667054 +https://conda.anaconda.org/conda-forge/noarch/ipython-8.22.2-pyh707e725_0.conda#f0abe827c8a7c6d91bccdf90cb1fbee3 https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.21.1-pyhd8ed1ab_0.conda#8a3a3d01629da20befa340919e3dd2c4 -https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.0-pyhd8ed1ab_0.conda#6bd3f1069cdebb44c7ae9efb900e312d +https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.1-pyhd8ed1ab_0.conda#c03972cfce69ad913d520c652e5ed908 https://conda.anaconda.org/conda-forge/linux-64/netcdf-fortran-4.6.1-nompi_hacb5139_103.conda#50f05f98d084805642d24dff910e11e8 https://conda.anaconda.org/conda-forge/linux-64/pandas-2.1.4-py311h320fe9a_0.conda#e44ccb61b6621bf3f8053ae66eba7397 -https://conda.anaconda.org/conda-forge/linux-64/pango-1.50.14-ha41ecd1_2.conda#1a66c10f6a0da3dbd2f3a68127e7f6a0 -https://conda.anaconda.org/conda-forge/noarch/pooch-1.8.0-pyhd8ed1ab_0.conda#134b2b57b7865d2316a7cce1915a51ed +https://conda.anaconda.org/conda-forge/linux-64/pango-1.52.1-ha41ecd1_0.conda#5c0cc002bf4eaa56448b0729efd6e96c +https://conda.anaconda.org/conda-forge/noarch/pooch-1.8.1-pyhd8ed1ab_0.conda#d15917f33140f8d2ac9ca44db7ec8a25 https://conda.anaconda.org/conda-forge/noarch/pylint-plugin-utils-0.7-pyhd8ed1ab_0.tar.bz2#1657976383aee04dbb3ae3bdf654bb58 https://conda.anaconda.org/conda-forge/noarch/pyopenssl-24.0.0-pyhd8ed1ab_0.conda#b50aec2c744a5c493c09cce9e2e7533e https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.1.1-pyhd8ed1ab_0.conda#4d2040212307d18392a2687772b3a96d -https://conda.anaconda.org/conda-forge/noarch/requests-cache-1.1.1-pyhd8ed1ab_0.conda#29bf13210ee541c59166cea092b91080 +https://conda.anaconda.org/conda-forge/noarch/requests-cache-1.2.0-pyhd8ed1ab_0.conda#f9a382d30405f6c874edf866eb814e7c https://conda.anaconda.org/conda-forge/linux-64/scipy-1.12.0-py311h64a7726_2.conda#24ca5107ab75c5521067b8ba505dfae5 -https://conda.anaconda.org/conda-forge/linux-64/shapely-2.0.2-py311h2032efe_1.conda#4ba860ff851768615b1a25b788022750 -https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.242-h65f022c_0.conda#09b53fbd76044de441d25261840821ac -https://conda.anaconda.org/conda-forge/noarch/bokeh-3.3.4-pyhd8ed1ab_0.conda#6cc92bba68b7bb5a3b180e96508f9480 +https://conda.anaconda.org/conda-forge/linux-64/shapely-2.0.3-py311h2032efe_0.conda#e982956906078eeac9feb3b8db10d011 +https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.267-hb1af6a8_4.conda#3e735ae06073894080acd78365e78936 +https://conda.anaconda.org/conda-forge/noarch/bokeh-3.4.0-pyhd8ed1ab_0.conda#eebbbfdb7eb885ddc751c790c3d0ad64 https://conda.anaconda.org/conda-forge/linux-64/cf-units-3.2.0-py311h1f0f07a_4.conda#1e105c1a8ea2163507726144b401eb1b -https://conda.anaconda.org/conda-forge/noarch/distributed-2024.2.0-pyhd8ed1ab_0.conda#81c14e12f44f94613fe5922403e32341 -https://conda.anaconda.org/conda-forge/linux-64/esmf-8.4.2-nompi_h9e768e6_3.conda#c330e87e698bae8e7381c0315cf25dd0 -https://conda.anaconda.org/conda-forge/linux-64/gtk2-2.24.33-h7f000aa_3.conda#0abfa7f9241a0f4fd732bc15773cfb0c +https://conda.anaconda.org/conda-forge/noarch/distributed-2024.3.1-pyhd8ed1ab_0.conda#b0ad5ef44595ef37c3008fc04ecd2abf +https://conda.anaconda.org/conda-forge/linux-64/esmf-8.6.0-nompi_h7b237b1_0.conda#a5f1925a75d9fcf0bffd07a194f83895 +https://conda.anaconda.org/conda-forge/linux-64/gtk2-2.24.33-h280cfa0_4.conda#410f86e58e880dcc7b0e910a8e89c05c https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.56.3-he3f83f7_1.conda#03bd1ddcc942867a19528877143b9852 -https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.2-py311h54ef318_0.conda#9f80753bc008bfc9b95f39d9ff9f1694 +https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.3-py311h54ef318_0.conda#014c115be880802d2372ac6ed665f526 https://conda.anaconda.org/conda-forge/noarch/myproxyclient-2.1.1-pyhd8ed1ab_0.conda#bcdbeb2b693eba886583a907840c6421 -https://conda.anaconda.org/conda-forge/noarch/nbformat-5.9.2-pyhd8ed1ab_0.conda#61ba076de6530d9301a0053b02f093d2 +https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.3-pyhd8ed1ab_0.conda#ca3d437c0ef2e87f63d085822c74c49a https://conda.anaconda.org/conda-forge/linux-64/netcdf4-1.6.5-nompi_py311he8ad708_100.conda#597b1ad6cb7011b7561c20ea30295cae https://conda.anaconda.org/conda-forge/noarch/pep8-naming-0.10.0-pyh9f0ad1d_0.tar.bz2#b3c5536e4f9f58a4b16adb6f1e11732d -https://conda.anaconda.org/conda-forge/noarch/pre-commit-3.6.1-pyha770c72_0.conda#4efd2c755bf2079e5651e57e1999db6c +https://conda.anaconda.org/conda-forge/noarch/pre-commit-3.7.0-pyha770c72_0.conda#846ba0877cda9c4f11e13720cacd1968 https://conda.anaconda.org/conda-forge/noarch/pylint-celery-0.3-py_1.tar.bz2#e29456a611a62d3f26105a2f9c68f759 https://conda.anaconda.org/conda-forge/noarch/pylint-django-2.5.3-pyhd8ed1ab_0.tar.bz2#00d8853fb1f87195722ea6a582cc9b56 https://conda.anaconda.org/conda-forge/noarch/pylint-flask-0.6-py_0.tar.bz2#5a9afd3d0a61b08d59eed70fab859c1b https://conda.anaconda.org/conda-forge/linux-64/python-stratify-0.3.0-py311h1f0f07a_1.conda#cd36a89a048ad2bcc6d8b43f648fb1d0 -https://conda.anaconda.org/conda-forge/linux-64/tiledb-2.19.1-h4386cac_0.conda#8d16e7b2529607a12aa6722c7a7c7356 -https://conda.anaconda.org/conda-forge/noarch/xarray-2024.1.1-pyhd8ed1ab_0.conda#38b5de3877d1f28089b231d24622dd64 +https://conda.anaconda.org/conda-forge/noarch/xarray-2024.3.0-pyhd8ed1ab_0.conda#772d7ee42b65d0840130eabd5bd3fc17 https://conda.anaconda.org/conda-forge/linux-64/cartopy-0.22.0-py311h320fe9a_1.conda#10d1806e20da040c58c36deddf51c70c -https://conda.anaconda.org/conda-forge/noarch/cf_xarray-0.8.9-pyhd8ed1ab_0.conda#f9f82c2c3d1b3588e8ab34976f98af91 -https://conda.anaconda.org/conda-forge/noarch/dask-jobqueue-0.8.2-pyhd8ed1ab_0.conda#cc344a296a41369bcb05f7216661cec8 +https://conda.anaconda.org/conda-forge/noarch/cf_xarray-0.9.0-pyhd8ed1ab_0.conda#33070a578d45591f242a254f78f86f10 +https://conda.anaconda.org/conda-forge/noarch/dask-jobqueue-0.8.5-pyhd8ed1ab_0.conda#abfb434fb6654f83d740428863ec85a8 https://conda.anaconda.org/conda-forge/noarch/esgf-pyclient-0.3.1-pyhca7485f_3.conda#1d43833138d38ad8324700ce45a7099a -https://conda.anaconda.org/conda-forge/noarch/esmpy-8.4.2-pyhc1e730c_4.conda#ddcf387719b2e44df0cc4dd467643951 +https://conda.anaconda.org/conda-forge/noarch/esmpy-8.6.0-pyhc1e730c_0.conda#60404b48ef1ccfb92cfd055f8844b700 https://conda.anaconda.org/conda-forge/linux-64/graphviz-9.0.0-h78e8752_1.conda#a3f4cd4a512ec5db35ffbf25ba11f537 -https://conda.anaconda.org/conda-forge/linux-64/libarrow-15.0.0-he2c5238_2_cpu.conda#cd7cd1c21dc42befdbb44b5afe2cd048 -https://conda.anaconda.org/conda-forge/linux-64/libgdal-3.8.3-h80d7d79_2.conda#b0c8bd68d3119c8e3a355ff9417ceff1 -https://conda.anaconda.org/conda-forge/noarch/nbclient-0.8.0-pyhd8ed1ab_0.conda#e78da91cf428faaf05701ce8cc8f2f9b +https://conda.anaconda.org/conda-forge/linux-64/libarrow-15.0.2-hb86450c_1_cpu.conda#b68f648f3e2f60755adaa5bfb93287d0 +https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.0-pyhd8ed1ab_0.conda#15b51397e0fe8ea7d7da60d83eb76ebc https://conda.anaconda.org/conda-forge/noarch/nc-time-axis-1.4.1-pyhd8ed1ab_0.tar.bz2#281b58948bf60a2582de9e548bcc5369 https://conda.anaconda.org/conda-forge/noarch/prospector-1.10.3-pyhd8ed1ab_0.conda#f551d4d859a1d70c6abff8310a655481 -https://conda.anaconda.org/conda-forge/linux-64/gdal-3.8.3-py311h8be719e_2.conda#4b587b3a15d00f6ec7fdecfe2d805140 -https://conda.anaconda.org/conda-forge/noarch/iris-3.7.0-pyha770c72_0.conda#dccc1f660bf455c239adaabf56b91dc9 -https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-15.0.0-h59595ed_2_cpu.conda#85d3e05ea2b427e879e486f09fb8cf54 -https://conda.anaconda.org/conda-forge/linux-64/libarrow-flight-15.0.0-hdc44a87_2_cpu.conda#41f4c79b79d6c13ffb7abc71ab4f0c54 -https://conda.anaconda.org/conda-forge/linux-64/libarrow-gandiva-15.0.0-hacb8726_2_cpu.conda#d8415318348d02dad131144d6fc151ec -https://conda.anaconda.org/conda-forge/linux-64/libparquet-15.0.0-h352af49_2_cpu.conda#8d99909e413b67872996d46093dda024 -https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.0-pyhd8ed1ab_0.conda#de2255e7a38fad6eaf457739c6599413 -https://conda.anaconda.org/conda-forge/noarch/py-cordex-0.6.6-pyhd8ed1ab_0.conda#255f9eac03143526c8aed41d1d091c63 +https://conda.anaconda.org/conda-forge/linux-64/tiledb-2.21.1-ha9641ad_1.conda#0453962dfed0265093929b52f885b190 +https://conda.anaconda.org/conda-forge/noarch/iris-3.8.1-pyha770c72_0.conda#b08a116ef1607e7e960a4caa902e3a90 +https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-15.0.2-h59595ed_1_cpu.conda#b9423f0ec36b99f729aa890b6fb3c98d +https://conda.anaconda.org/conda-forge/linux-64/libarrow-flight-15.0.2-hc6145d9_1_cpu.conda#a8166c3e9ff1222307cdd86af0234dbe +https://conda.anaconda.org/conda-forge/linux-64/libarrow-gandiva-15.0.2-hb016d2e_1_cpu.conda#c595407620b1688599908bdc1c17fd74 +https://conda.anaconda.org/conda-forge/linux-64/libgdal-3.8.4-h7c88fdf_5.conda#750bfb344a8690e7089c8c2b303f252a +https://conda.anaconda.org/conda-forge/linux-64/libparquet-15.0.2-h352af49_1_cpu.conda#9c9171bf3a477a585d08a7979f84c3b8 +https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.3-pyhd8ed1ab_0.conda#0cab42b4917e71df9dc2224b9940ef19 +https://conda.anaconda.org/conda-forge/noarch/py-cordex-0.7.0-pyhd8ed1ab_0.conda#7ad60b498674a9bff3ba8f3fb335e4f0 https://conda.anaconda.org/conda-forge/linux-64/pydot-2.0.0-py311h38be061_0.conda#cdfd23a54a18f3c8d5320d7717f4ed52 -https://conda.anaconda.org/conda-forge/linux-64/fiona-1.9.5-py311hf8e0aa6_3.conda#a5277325e005e9d014eca37187b3f4a2 -https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-15.0.0-h59595ed_2_cpu.conda#3cda69f7af9b2341e3ee0fb602861726 -https://conda.anaconda.org/conda-forge/linux-64/libarrow-flight-sql-15.0.0-hfbc7f12_2_cpu.conda#d779a1334ac200d6e9c04ee6bfa2a6af -https://conda.anaconda.org/conda-forge/noarch/nbconvert-pandoc-7.16.0-pyhd8ed1ab_0.conda#28dde45c295b3f110bc6bb425472137b +https://conda.anaconda.org/conda-forge/linux-64/gdal-3.8.4-py311h8be719e_5.conda#de467dcf47e6877fabff111dbe98b4b3 +https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-15.0.2-h59595ed_1_cpu.conda#a921e87ad731a7cde36a016233c1b80b +https://conda.anaconda.org/conda-forge/linux-64/libarrow-flight-sql-15.0.2-h757c851_1_cpu.conda#b59b90d6c8d2e072890f5d289f9ba36f +https://conda.anaconda.org/conda-forge/noarch/nbconvert-pandoc-7.16.3-hd8ed1ab_0.conda#1344bbd74e8bcd1acdd8ec0824e9840c https://conda.anaconda.org/conda-forge/noarch/prov-2.0.0-pyhd3deb0d_0.tar.bz2#aa9b3ad140f6c0668c646f32e20ccf82 -https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-15.0.0-hfbc7f12_2_cpu.conda#21de7bd8fd4568ccb232bc7bfbf3d112 -https://conda.anaconda.org/conda-forge/noarch/nbconvert-7.16.0-pyhd8ed1ab_0.conda#342ba1099325da21a811e80397006461 -https://conda.anaconda.org/conda-forge/linux-64/pyarrow-15.0.0-py311h39c9aba_2_cpu.conda#6c70dd512802dfc4114b2df223a883bd +https://conda.anaconda.org/conda-forge/linux-64/fiona-1.9.6-py311hf8e0aa6_0.conda#80e9901639787044e91155e9a99d706d +https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-15.0.2-h757c851_1_cpu.conda#802e115e2c489e1c76c0fe809e766ccd +https://conda.anaconda.org/conda-forge/noarch/nbconvert-7.16.3-hd8ed1ab_0.conda#b0c9bbbe54a11a6db3bec51eb0ef0281 +https://conda.anaconda.org/conda-forge/linux-64/pyarrow-15.0.2-py311h39c9aba_1_cpu.conda#9279ae695726f9217888f9845f578f2f +https://conda.anaconda.org/conda-forge/noarch/dask-expr-1.0.5-pyhd8ed1ab_0.conda#d1e973e2e617f806194f4e664c2d3d33 https://conda.anaconda.org/conda-forge/noarch/pyarrow-hotfix-0.6-pyhd8ed1ab_0.conda#ccc06e6ef2064ae129fab3286299abda -https://conda.anaconda.org/conda-forge/noarch/dask-2024.2.0-pyhd8ed1ab_0.conda#085d464298ca31a98193af99ee5e75e7 +https://conda.anaconda.org/conda-forge/noarch/dask-2024.3.1-pyhd8ed1ab_0.conda#e3f23f17022881c62e75ddbab7a61f9e https://conda.anaconda.org/conda-forge/noarch/iris-esmf-regrid-0.9.0-pyhd8ed1ab_0.conda#570f2c6e387fd6dac5356a5152f91b3f https://conda.anaconda.org/conda-forge/noarch/autodocsumm-0.2.6-pyhd8ed1ab_0.tar.bz2#4409dd7e06a62c3b2aa9e96782c49c6d https://conda.anaconda.org/conda-forge/noarch/nbsphinx-0.9.3-pyhd8ed1ab_0.conda#0dbaa7d08d3d79b2a1a4dd6a02cc4581 diff --git a/doc/quickstart/find_data.rst b/doc/quickstart/find_data.rst index 31ee262ef5..131bf8f99e 100644 --- a/doc/quickstart/find_data.rst +++ b/doc/quickstart/find_data.rst @@ -388,7 +388,9 @@ This grid file can either be specified as absolute or relative (to with the facet ``horizontal_grid`` in the recipe or the extra facets (see below), or retrieved automatically from the `grid_file_uri` attribute of the input files. -In the latter case, the file is downloaded once and then cached. +In the latter case, ESMValCore first searches the input directories specified +for ICON for a grid file with that name, and if that was not successful, tries +to download the file and cache it. The cached file is valid for 7 days. ESMValCore can automatically make native ICON data `UGRID @@ -467,7 +469,7 @@ Key Description Default value if not specif =================== ================================ =================================== ``horizontal_grid`` Absolute or relative (to If not given, use file attribute ``auxiliary_data_dir`` defined ``grid_file_uri`` to retrieve ICON - in the grid file + in the grid file (see details above) :ref:`user configuration file`) path to the ICON grid file ``latitude`` Standard name of the latitude ``latitude`` diff --git a/doc/recipe/preprocessor.rst b/doc/recipe/preprocessor.rst index afc7aad6f4..3595741708 100644 --- a/doc/recipe/preprocessor.rst +++ b/doc/recipe/preprocessor.rst @@ -177,12 +177,12 @@ Calculate the global non-weighted root mean square: global_mean: area_statistics: operator: rms - weighted: false + weights: false .. warning:: The disabling of weights by specifying the keyword argument ``weights: - False`` needs to be used with great care; from a scientific standpoint, we + false`` needs to be used with great care; from a scientific standpoint, we strongly recommend to **not** use it! @@ -307,7 +307,7 @@ Preprocessor Variable short na :ref:`area_statistics` ``areacella``, ``areacello`` cell_area :ref:`mask_landsea` ``sftlf``, ``sftof`` land_area_fraction, sea_area_fraction :ref:`mask_landseaice` ``sftgif`` land_ice_area_fraction -:ref:`volume_statistics` ``volcello`` ocean_volume +:ref:`volume_statistics` ``volcello``, ``areacello`` ocean_volume, cell_area :ref:`weighting_landsea_fraction` ``sftlf``, ``sftof`` land_area_fraction, sea_area_fraction ============================================================== ============================== ===================================== @@ -2135,12 +2135,16 @@ but maintains the time dimension. By default, the `mean` operation is weighted by the grid cell volumes. For weighted statistics, this function requires a cell volume `cell measure`_, -unless the coordinates of the input data are regular 1D latitude and longitude -coordinates so the cell volumes can be computed internally. -The required supplementary variable ``volcello`` can be attached to the main -dataset as described in :ref:`supplementary_variables`. +unless it has a cell_area `cell measure`_ or the coordinates of the input data +are regular 1D latitude and longitude coordinates so the cell volumes can be +computed internally. +The required supplementary variable ``volcello``, or ``areacello`` in its +absence, can be attached to the main dataset as described in +:ref:`supplementary_variables`. -No depth coordinate is required as this is determined by Iris. +No depth coordinate is required as this is determined by Iris. However, to +compute the volume automatically when ``volcello`` is not provided, the depth +coordinate units should be convertible to meters. Parameters: * `operator`: Operation to apply. diff --git a/environment.yml b/environment.yml index f84372d197..c3c33e248e 100644 --- a/environment.yml +++ b/environment.yml @@ -30,7 +30,7 @@ dependencies: - netcdf4 - numpy !=1.24.3 - packaging - - pandas !=2.2.0,!=2.2.1 # github.com/ESMValGroup/ESMValCore/pull/2305 and #2349 + - pandas !=2.2.0,!=2.2.1,!=2.2.2 # github.com/ESMValGroup/ESMValCore/pull/2305 and #2349 - pillow - pip !=21.3 - prov diff --git a/esmvalcore/_recipe/recipe.py b/esmvalcore/_recipe/recipe.py index 4369a7d4ac..ea42869375 100644 --- a/esmvalcore/_recipe/recipe.py +++ b/esmvalcore/_recipe/recipe.py @@ -50,7 +50,7 @@ from .to_datasets import ( _derive_needed, _get_input_datasets, - _representative_dataset, + _representative_datasets, ) logger = logging.getLogger(__name__) @@ -116,7 +116,7 @@ def _update_target_levels(dataset, datasets, settings): del settings['extract_levels'] else: target_ds = _select_dataset(dataset_name, datasets) - representative_ds = _representative_dataset(target_ds) + representative_ds = _representative_datasets(target_ds)[0] check.data_availability(representative_ds) settings['extract_levels']['levels'] = get_reference_levels( representative_ds) @@ -133,8 +133,8 @@ def _update_target_grid(dataset, datasets, settings): if dataset.facets['dataset'] == grid: del settings['regrid'] elif any(grid == d.facets['dataset'] for d in datasets): - representative_ds = _representative_dataset( - _select_dataset(grid, datasets)) + representative_ds = _representative_datasets( + _select_dataset(grid, datasets))[0] check.data_availability(representative_ds) settings['regrid']['target_grid'] = representative_ds else: diff --git a/esmvalcore/_recipe/to_datasets.py b/esmvalcore/_recipe/to_datasets.py index fd286ea81b..962d732a9d 100644 --- a/esmvalcore/_recipe/to_datasets.py +++ b/esmvalcore/_recipe/to_datasets.py @@ -404,53 +404,57 @@ def datasets_from_recipe( def _dataset_from_files(dataset: Dataset) -> list[Dataset]: """Replace facet values of '*' based on available files.""" result: list[Dataset] = [] - errors = [] + errors: list[str] = [] if any(_isglob(f) for f in dataset.facets.values()): logger.debug( "Expanding dataset globs for dataset %s, " "this may take a while..", dataset.summary(shorten=True)) - repr_dataset = _representative_dataset(dataset) - for repr_ds in repr_dataset.from_files(): - updated_facets = {} - failed = {} - for key, value in dataset.facets.items(): - if _isglob(value): - if key in repr_ds.facets and not _isglob(repr_ds[key]): - updated_facets[key] = repr_ds.facets[key] - else: - failed[key] = value - - if failed: - msg = ("Unable to replace " + - ", ".join(f"{k}={v}" for k, v in failed.items()) + - f" by a value for\n{dataset}") - # Set supplementaries to [] to avoid searching for supplementary - # files. - repr_ds.supplementaries = [] - if repr_ds.files: - paths_msg = "paths to " if any( - isinstance(f, LocalFile) for f in repr_ds.files) else "" - msg = (f"{msg}\nDo the {paths_msg}the files:\n" + - "\n".join(f"{f} with facets: {f.facets}" - for f in repr_ds.files) + - "\nprovide the missing facet values?") - else: - timerange = repr_ds.facets.get('timerange') - patterns = repr_ds._file_globs - msg = ( - f"{msg}\nNo files found matching:\n" + - "\n".join(str(p) for p in patterns) + # type:ignore - (f"\nwithin the requested timerange {timerange}." - if timerange else "")) - errors.append(msg) - continue - - new_ds = dataset.copy() - new_ds.facets.update(updated_facets) - new_ds.supplementaries = repr_ds.supplementaries - result.append(new_ds) + representative_datasets = _representative_datasets(dataset) + + # For derived variables, representative_datasets might contain more than + # one element + all_datasets: list[list[tuple[dict, Dataset]]] = [] + for representative_dataset in representative_datasets: + all_datasets.append([]) + for expanded_ds in representative_dataset.from_files(): + updated_facets = {} + unexpanded_globs = {} + for key, value in dataset.facets.items(): + if _isglob(value): + if (key in expanded_ds.facets and + not _isglob(expanded_ds[key])): + updated_facets[key] = expanded_ds.facets[key] + else: + unexpanded_globs[key] = value + + if unexpanded_globs: + msg = _report_unexpanded_globs( + dataset, expanded_ds, unexpanded_globs + ) + errors.append(msg) + continue + + new_ds = dataset.copy() + new_ds.facets.update(updated_facets) + new_ds.supplementaries = expanded_ds.supplementaries + + all_datasets[-1].append((updated_facets, new_ds)) + + # If globs have been expanded, only consider those datasets that contain + # all necessary input variables if derivation is necessary + for (updated_facets, new_ds) in all_datasets[0]: + other_facets = [[d[0] for d in ds] for ds in all_datasets[1:]] + if all(updated_facets in facets for facets in other_facets): + result.append(new_ds) + else: + logger.debug( + "Not all necessary input variables to derive '%s' are " + "available for dataset %s", + dataset['short_name'], + updated_facets, + ) if errors: raise RecipeError("\n".join(errors)) @@ -458,6 +462,47 @@ def _dataset_from_files(dataset: Dataset) -> list[Dataset]: return result +def _report_unexpanded_globs( + unexpanded_ds: Dataset, + expanded_ds: Dataset, + unexpanded_globs: dict, +) -> str: + """Get error message for unexpanded globs.""" + msg = ( + "Unable to replace " + + ", ".join(f"{k}={v}" for k, v in unexpanded_globs.items()) + + f" by a value for\n{unexpanded_ds}" + ) + + # Set supplementaries to [] to avoid searching for supplementary files + expanded_ds.supplementaries = [] + + if expanded_ds.files: + if any(isinstance(f, LocalFile) for f in expanded_ds.files): + paths_msg = "paths to the " + else: + paths_msg = "" + msg = ( + f"{msg}\nDo the {paths_msg}files:\n" + + "\n".join( + f"{f} with facets: {f.facets}" for f in expanded_ds.files + ) + + "\nprovide the missing facet values?" + ) + else: + timerange = expanded_ds.facets.get('timerange') + patterns = expanded_ds._file_globs + msg = ( + f"{msg}\nNo files found matching:\n" + + "\n".join(str(p) for p in patterns) + ( # type:ignore + f"\nwithin the requested timerange {timerange}." + if timerange else "" + ) + ) + + return msg + + def _derive_needed(dataset: Dataset) -> bool: """Check if dataset needs to be derived from other datasets.""" if not dataset.facets.get('derive'): @@ -512,11 +557,11 @@ def _get_input_datasets(dataset: Dataset) -> list[Dataset]: return datasets -def _representative_dataset(dataset: Dataset) -> Dataset: - """Find a representative dataset that has files available.""" +def _representative_datasets(dataset: Dataset) -> list[Dataset]: + """Find representative datasets for all input variables.""" copy = dataset.copy() copy.supplementaries = [] - datasets = _get_input_datasets(copy) - representative_dataset = datasets[0] - representative_dataset.supplementaries = dataset.supplementaries - return representative_dataset + representative_datasets = _get_input_datasets(copy) + for representative_dataset in representative_datasets: + representative_dataset.supplementaries = dataset.supplementaries + return representative_datasets diff --git a/esmvalcore/cmor/_fixes/cmip6/gfdl_cm4.py b/esmvalcore/cmor/_fixes/cmip6/gfdl_cm4.py index 274a495bc7..4034186d7b 100644 --- a/esmvalcore/cmor/_fixes/cmip6/gfdl_cm4.py +++ b/esmvalcore/cmor/_fixes/cmip6/gfdl_cm4.py @@ -76,10 +76,6 @@ class Tasmax(Tas): """Fixes for tasmax.""" -class SfcWind(Tas): - """Fixes for sfcWind.""" - - class Hurs(Tas): """Fixes for hurs.""" @@ -110,6 +106,10 @@ class Vas(Uas): """Fixes for vas.""" +class SfcWind(Uas): + """Fixes for sfcWind.""" + + Omon = BaseOmon diff --git a/esmvalcore/cmor/_fixes/cordex/cnrm_cerfacs_cnrm_cm5/wrf381p.py b/esmvalcore/cmor/_fixes/cordex/cnrm_cerfacs_cnrm_cm5/wrf381p.py new file mode 100644 index 0000000000..26fab6a87c --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/cnrm_cerfacs_cnrm_cm5/wrf381p.py @@ -0,0 +1,36 @@ +"""Fixes for rcm WRF381P driven by CNRM-CERFACS-CNRM-CM5.""" +from esmvalcore.cmor._fixes.shared import add_scalar_height_coord +from esmvalcore.cmor.fix import Fix + + +class Tas(Fix): + """Fixes for tas.""" + + def fix_metadata(self, cubes): + """Add height (2m) coordinate and correct long_name for time. + + Parameters + ---------- + cubes : iris.cube.CubeList + Input cubes. + + Returns + ------- + iris.cube.CubeList + """ + cube = self.get_cube_from_list(cubes) + add_scalar_height_coord(cube) + + return cubes + + +Tasmin = Tas + + +Tasmax = Tas + + +Hurs = Tas + + +Huss = Tas diff --git a/esmvalcore/cmor/_fixes/cordex/ichec_ec_earth/wrf381p.py b/esmvalcore/cmor/_fixes/cordex/ichec_ec_earth/wrf381p.py new file mode 100644 index 0000000000..509b0d3290 --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/ichec_ec_earth/wrf381p.py @@ -0,0 +1,36 @@ +"""Fixes for rcm WRF381P driven by ICHEC-EC-EARTH.""" +from esmvalcore.cmor._fixes.shared import add_scalar_height_coord +from esmvalcore.cmor.fix import Fix + + +class Tas(Fix): + """Fixes for tas.""" + + def fix_metadata(self, cubes): + """Add height (2m) coordinate and correct long_name for time. + + Parameters + ---------- + cubes : iris.cube.CubeList + Input cubes. + + Returns + ------- + iris.cube.CubeList + """ + cube = self.get_cube_from_list(cubes) + add_scalar_height_coord(cube) + + return cubes + + +Tasmin = Tas + + +Tasmax = Tas + + +Hurs = Tas + + +Huss = Tas diff --git a/esmvalcore/cmor/_fixes/cordex/ipsl_ipsl_cm5a_mr/__init__.py b/esmvalcore/cmor/_fixes/cordex/ipsl_ipsl_cm5a_mr/__init__.py new file mode 100644 index 0000000000..093969370f --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/ipsl_ipsl_cm5a_mr/__init__.py @@ -0,0 +1 @@ +"""Fixes for CORDEX data.""" diff --git a/esmvalcore/cmor/_fixes/cordex/ipsl_ipsl_cm5a_mr/wrf381p.py b/esmvalcore/cmor/_fixes/cordex/ipsl_ipsl_cm5a_mr/wrf381p.py new file mode 100644 index 0000000000..86da149533 --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/ipsl_ipsl_cm5a_mr/wrf381p.py @@ -0,0 +1,36 @@ +"""Fixes for rcm WRF381P driven by IPSL-IPSL-CM5A-MR.""" +from esmvalcore.cmor._fixes.shared import add_scalar_height_coord +from esmvalcore.cmor.fix import Fix + + +class Tas(Fix): + """Fixes for tas.""" + + def fix_metadata(self, cubes): + """Add height (2m) coordinate and correct long_name for time. + + Parameters + ---------- + cubes : iris.cube.CubeList + Input cubes. + + Returns + ------- + iris.cube.CubeList + """ + cube = self.get_cube_from_list(cubes) + add_scalar_height_coord(cube) + + return cubes + + +Tasmin = Tas + + +Tasmax = Tas + + +Hurs = Tas + + +Huss = Tas diff --git a/esmvalcore/cmor/_fixes/cordex/mohc_hadgem2_es/wrf381p.py b/esmvalcore/cmor/_fixes/cordex/mohc_hadgem2_es/wrf381p.py new file mode 100644 index 0000000000..4e4d3a316f --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/mohc_hadgem2_es/wrf381p.py @@ -0,0 +1,36 @@ +"""Fixes for rcm WRF381P driven by MOHC-HadGEM2-ES.""" +from esmvalcore.cmor._fixes.shared import add_scalar_height_coord +from esmvalcore.cmor.fix import Fix + + +class Tas(Fix): + """Fixes for tas.""" + + def fix_metadata(self, cubes): + """Add height (2m) coordinate and correct long_name for time. + + Parameters + ---------- + cubes : iris.cube.CubeList + Input cubes. + + Returns + ------- + iris.cube.CubeList + """ + cube = self.get_cube_from_list(cubes) + add_scalar_height_coord(cube) + + return cubes + + +Tasmin = Tas + + +Tasmax = Tas + + +Hurs = Tas + + +Huss = Tas diff --git a/esmvalcore/cmor/_fixes/cordex/ncc_noresm1_m/wrf381p.py b/esmvalcore/cmor/_fixes/cordex/ncc_noresm1_m/wrf381p.py new file mode 100644 index 0000000000..d373d9ddd2 --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/ncc_noresm1_m/wrf381p.py @@ -0,0 +1,36 @@ +"""Fixes for rcm WRF381P driven by NCC-NorESM1-M.""" +from esmvalcore.cmor._fixes.shared import add_scalar_height_coord +from esmvalcore.cmor.fix import Fix + + +class Tas(Fix): + """Fixes for tas.""" + + def fix_metadata(self, cubes): + """Add height (2m) coordinate and correct long_name for time. + + Parameters + ---------- + cubes : iris.cube.CubeList + Input cubes. + + Returns + ------- + iris.cube.CubeList + """ + cube = self.get_cube_from_list(cubes) + add_scalar_height_coord(cube) + + return cubes + + +Tasmin = Tas + + +Tasmax = Tas + + +Hurs = Tas + + +Huss = Tas diff --git a/esmvalcore/cmor/_fixes/icon/_base_fixes.py b/esmvalcore/cmor/_fixes/icon/_base_fixes.py index 94ada2e25b..e4196b0355 100644 --- a/esmvalcore/cmor/_fixes/icon/_base_fixes.py +++ b/esmvalcore/cmor/_fixes/icon/_base_fixes.py @@ -1,4 +1,5 @@ """Fix base classes for ICON on-the-fly CMORizer.""" +from __future__ import annotations import logging import os @@ -14,9 +15,11 @@ import numpy as np import requests from iris import NameConstraint +from iris.cube import Cube, CubeList from iris.experimental.ugrid import Connectivity, Mesh -from ..native_datasets import NativeDatasetFix +from esmvalcore.cmor._fixes.native_datasets import NativeDatasetFix +from esmvalcore.local import _get_rootpath, _replace_tags, _select_drs logger = logging.getLogger(__name__) @@ -240,7 +243,7 @@ def add_additional_cubes(self, cubes): 'zghalf_file', ] for facet in facets_to_consider: - if facet not in self.extra_facets: + if self.extra_facets.get(facet) is None: continue path_to_add = self._get_path_from_facet(facet) logger.debug("Adding cubes from %s", path_to_add) @@ -271,7 +274,7 @@ def _tmp_local_file(local_file: Path) -> Path: with NamedTemporaryFile(prefix=f"{local_file}.") as file: return Path(file.name) - def _get_grid_from_cube_attr(self, cube: iris.cube.Cube) -> iris.cube.Cube: + def _get_grid_from_cube_attr(self, cube: Cube) -> Cube: """Get horizontal grid from `grid_file_uri` attribute of cube.""" (grid_url, grid_name) = self._get_grid_url(cube) @@ -279,10 +282,50 @@ def _get_grid_from_cube_attr(self, cube: iris.cube.Cube) -> iris.cube.Cube: if grid_name in self._horizontal_grids: return self._horizontal_grids[grid_name] - # Check if grid file has recently been downloaded and load it if - # possible + # First, check if the grid file is available in the ICON rootpath + grid = self._get_grid_from_rootpath(grid_name) + + # Second, if that didn't work, try to download grid (or use cached + # version of it if possible) + if grid is None: + grid = self._get_downloaded_grid(grid_url, grid_name) + + # Cache grid for later use + self._horizontal_grids[grid_name] = grid + + return grid + + def _get_grid_from_rootpath(self, grid_name: str) -> CubeList | None: + """Try to get grid from the ICON rootpath.""" + rootpaths = _get_rootpath('ICON') + dirname_template = _select_drs('input_dir', 'ICON') + dirname_globs = _replace_tags(dirname_template, self.extra_facets) + possible_grid_paths = [ + r / d / grid_name for r in rootpaths for d in dirname_globs + ] + for grid_path in possible_grid_paths: + if grid_path.is_file(): + logger.debug("Using ICON grid file '%s'", grid_path) + cubes = self._load_cubes(grid_path) + return cubes + return None + + def _get_downloaded_grid(self, grid_url: str, grid_name: str) -> CubeList: + """Get downloaded horizontal grid. + + Check if grid file has recently been downloaded. If not, download grid + file here. + + Note + ---- + In order to make this function thread-safe, the downloaded grid file is + first saved to a temporary location, then copied to the actual location + later. + + """ grid_path = self.CACHE_DIR / grid_name + # Check cache valid_cache = False if grid_path.exists(): mtime = grid_path.stat().st_mtime @@ -295,6 +338,7 @@ def _get_grid_from_cube_attr(self, cube: iris.cube.Cube) -> iris.cube.Cube: logger.debug("Existing cached ICON grid file '%s' is outdated", grid_path) + # File is not present in cache or too old -> download it if not valid_cache: self.CACHE_DIR.mkdir(parents=True, exist_ok=True) tmp_path = self._tmp_local_file(grid_path) @@ -320,9 +364,8 @@ def _get_grid_from_cube_attr(self, cube: iris.cube.Cube) -> iris.cube.Cube: grid_path, ) - self._horizontal_grids[grid_name] = self._load_cubes(grid_path) - - return self._horizontal_grids[grid_name] + cubes = self._load_cubes(grid_path) + return cubes def get_horizontal_grid(self, cube): """Get copy of ICON horizontal grid. @@ -361,7 +404,7 @@ def get_horizontal_grid(self, cube): file. """ - if 'horizontal_grid' in self.extra_facets: + if self.extra_facets.get('horizontal_grid') is not None: grid = self._get_grid_from_facet() else: grid = self._get_grid_from_cube_attr(cube) @@ -402,7 +445,7 @@ def get_mesh(self, cube): """ # If specified by the user, use `horizontal_grid` facet to determine # grid name; otherwise, use the `grid_file_uri` attribute of the cube - if 'horizontal_grid' in self.extra_facets: + if self.extra_facets.get('horizontal_grid') is not None: grid_path = self._get_path_from_facet( 'horizontal_grid', 'Horizontal grid file' ) @@ -410,7 +453,7 @@ def get_mesh(self, cube): else: (_, grid_name) = self._get_grid_url(cube) - # Re-use mesh if possible + # Reuse mesh if possible if grid_name in self._meshes: logger.debug("Reusing ICON mesh for grid %s", grid_name) else: @@ -436,7 +479,7 @@ def _get_start_index(horizontal_grid): return np.int32(np.min(vertex_index.data)) @staticmethod - def _load_cubes(path): + def _load_cubes(path: Path | str) -> CubeList: """Load cubes and ignore certain warnings.""" with warnings.catch_warnings(): warnings.filterwarnings( @@ -444,7 +487,13 @@ def _load_cubes(path): message="Ignoring netCDF variable .* invalid units .*", category=UserWarning, module='iris', - ) + ) # iris < 3.8 + warnings.filterwarnings( + 'ignore', + message="Ignoring invalid units .* on netCDF variable .*", + category=UserWarning, + module='iris', + ) # iris >= 3.8 warnings.filterwarnings( 'ignore', message="Failed to create 'height' dimension coordinate: The " @@ -453,7 +502,7 @@ def _load_cubes(path): category=UserWarning, module='iris', ) - cubes = iris.load(str(path)) + cubes = iris.load(path) return cubes @staticmethod @@ -462,3 +511,12 @@ def _set_range_in_0_360(lon_coord): lon_coord.points = (lon_coord.core_points() + 360.0) % 360.0 if lon_coord.has_bounds(): lon_coord.bounds = (lon_coord.core_bounds() + 360.0) % 360.0 + + +class NegateData(IconFix): + """Base fix to negate data.""" + + def fix_data(self, cube): + """Fix data.""" + cube.data = -cube.core_data() + return cube diff --git a/esmvalcore/cmor/_fixes/icon/icon.py b/esmvalcore/cmor/_fixes/icon/icon.py index 2e1dfd3bb3..707a47f20c 100644 --- a/esmvalcore/cmor/_fixes/icon/icon.py +++ b/esmvalcore/cmor/_fixes/icon/icon.py @@ -1,6 +1,7 @@ """On-the-fly CMORizer for ICON.""" import logging +import warnings from datetime import datetime, timedelta import dask.array as da @@ -15,7 +16,7 @@ from esmvalcore.iris_helpers import add_leading_dim_to_cube, date2num -from ._base_fixes import IconFix +from ._base_fixes import IconFix, NegateData logger = logging.getLogger(__name__) @@ -493,7 +494,15 @@ def _fix_invalid_time_units(time_coord): # this results in times that are off by 1s (e.g., 13:59:59 instead of # 14:00:00). rounded_datetimes = (year_month_day + day_float).round('s') - new_datetimes = np.array(rounded_datetimes.dt.to_pydatetime()) + with warnings.catch_warnings(): + # We already fixed the deprecated code as recommended in the + # warning, but it still shows up -> ignore it + warnings.filterwarnings( + 'ignore', + message="The behavior of DatetimeProperties.to_pydatetime .*", + category=FutureWarning, + ) + new_datetimes = np.array(rounded_datetimes.dt.to_pydatetime()) new_dt_points = date2num(np.array(new_datetimes), new_t_units) # Modify time coordinate in place @@ -512,3 +521,26 @@ def fix_metadata(self, cubes): ) cube.var_name = self.vardef.short_name return CubeList([cube]) + + +class Rtmt(IconFix): + """Fixes for ``rtmt``.""" + + def fix_metadata(self, cubes): + """Fix metadata.""" + cube = ( + self.get_cube(cubes, var_name='rsdt') - + self.get_cube(cubes, var_name='rsut') - + self.get_cube(cubes, var_name='rlut') + ) + cube.var_name = self.vardef.short_name + return CubeList([cube]) + + +Hfls = NegateData + + +Hfss = NegateData + + +Rtnt = Rtmt diff --git a/esmvalcore/cmor/tables/custom/CMOR_clhmtisccp.dat b/esmvalcore/cmor/tables/custom/CMOR_clhmtisccp.dat index 504b69164f..3c8b049ec0 100644 --- a/esmvalcore/cmor/tables/custom/CMOR_clhmtisccp.dat +++ b/esmvalcore/cmor/tables/custom/CMOR_clhmtisccp.dat @@ -17,6 +17,5 @@ comment: at the top of the atmosphere (to be compared with satellite m !---------------------------------- dimensions: longitude latitude time type: real -positive: up !---------------------------------- ! diff --git a/esmvalcore/cmor/tables/custom/CMOR_clhtkisccp.dat b/esmvalcore/cmor/tables/custom/CMOR_clhtkisccp.dat index 6ecb43a913..d34d39782f 100644 --- a/esmvalcore/cmor/tables/custom/CMOR_clhtkisccp.dat +++ b/esmvalcore/cmor/tables/custom/CMOR_clhtkisccp.dat @@ -17,6 +17,5 @@ comment: at the top of the atmosphere (to be compared with satellite m !---------------------------------- dimensions: longitude latitude time type: real -positive: up !---------------------------------- ! diff --git a/esmvalcore/cmor/tables/custom/CMOR_cllmtisccp.dat b/esmvalcore/cmor/tables/custom/CMOR_cllmtisccp.dat index a16279a424..86438496bc 100644 --- a/esmvalcore/cmor/tables/custom/CMOR_cllmtisccp.dat +++ b/esmvalcore/cmor/tables/custom/CMOR_cllmtisccp.dat @@ -17,6 +17,5 @@ comment: at the top of the atmosphere (to be compared with satellite m !---------------------------------- dimensions: longitude latitude time type: real -positive: up !---------------------------------- ! diff --git a/esmvalcore/cmor/tables/custom/CMOR_clltkisccp.dat b/esmvalcore/cmor/tables/custom/CMOR_clltkisccp.dat index c15c19d12e..a1b989f98b 100644 --- a/esmvalcore/cmor/tables/custom/CMOR_clltkisccp.dat +++ b/esmvalcore/cmor/tables/custom/CMOR_clltkisccp.dat @@ -17,6 +17,5 @@ comment: at the top of the atmosphere (to be compared with satellite m !---------------------------------- dimensions: longitude latitude time type: real -positive: up !---------------------------------- ! diff --git a/esmvalcore/cmor/tables/custom/CMOR_clmmtisccp.dat b/esmvalcore/cmor/tables/custom/CMOR_clmmtisccp.dat index 707709843b..a7482c24bf 100644 --- a/esmvalcore/cmor/tables/custom/CMOR_clmmtisccp.dat +++ b/esmvalcore/cmor/tables/custom/CMOR_clmmtisccp.dat @@ -17,6 +17,5 @@ comment: at the top of the atmosphere (to be compared with satellite m !---------------------------------- dimensions: longitude latitude time type: real -positive: up !---------------------------------- ! diff --git a/esmvalcore/cmor/tables/custom/CMOR_clmtkisccp.dat b/esmvalcore/cmor/tables/custom/CMOR_clmtkisccp.dat index 7ec19cf1a4..1bdfe4133d 100644 --- a/esmvalcore/cmor/tables/custom/CMOR_clmtkisccp.dat +++ b/esmvalcore/cmor/tables/custom/CMOR_clmtkisccp.dat @@ -17,6 +17,5 @@ comment: at the top of the atmosphere (to be compared with satellite m !---------------------------------- dimensions: longitude latitude time type: real -positive: up !---------------------------------- ! diff --git a/esmvalcore/cmor/tables/custom/CMOR_lwcre.dat b/esmvalcore/cmor/tables/custom/CMOR_lwcre.dat index 67a613e5b8..6818af1b91 100644 --- a/esmvalcore/cmor/tables/custom/CMOR_lwcre.dat +++ b/esmvalcore/cmor/tables/custom/CMOR_lwcre.dat @@ -17,6 +17,6 @@ comment: at the top of the atmosphere (to be compared with satellite m !---------------------------------- dimensions: longitude latitude time type: real -positive: up +positive: down !---------------------------------- ! diff --git a/esmvalcore/cmor/tables/custom/CMOR_netcre.dat b/esmvalcore/cmor/tables/custom/CMOR_netcre.dat index 689afdf8a9..6bce0c491f 100644 --- a/esmvalcore/cmor/tables/custom/CMOR_netcre.dat +++ b/esmvalcore/cmor/tables/custom/CMOR_netcre.dat @@ -17,6 +17,6 @@ comment: at the top of the atmosphere (to be compared with satellite m !---------------------------------- dimensions: longitude latitude time type: real -positive: up +positive: down !---------------------------------- ! diff --git a/esmvalcore/cmor/tables/custom/CMOR_rsnt.dat b/esmvalcore/cmor/tables/custom/CMOR_rsnt.dat index f3c84d1d70..38b43c7f4a 100644 --- a/esmvalcore/cmor/tables/custom/CMOR_rsnt.dat +++ b/esmvalcore/cmor/tables/custom/CMOR_rsnt.dat @@ -17,6 +17,6 @@ comment: at the top of the atmosphere (to be compared with satellite m !---------------------------------- dimensions: longitude latitude time type: real -positive: up +positive: down !---------------------------------- ! diff --git a/esmvalcore/cmor/tables/custom/CMOR_rtnt.dat b/esmvalcore/cmor/tables/custom/CMOR_rtnt.dat index 3cbd45543c..c8d7053bb9 100644 --- a/esmvalcore/cmor/tables/custom/CMOR_rtnt.dat +++ b/esmvalcore/cmor/tables/custom/CMOR_rtnt.dat @@ -17,6 +17,6 @@ comment: at the top of the atmosphere (to be compared with satellite m !---------------------------------- dimensions: longitude latitude time type: real -positive: up +positive: down !---------------------------------- ! diff --git a/esmvalcore/cmor/tables/custom/CMOR_swcre.dat b/esmvalcore/cmor/tables/custom/CMOR_swcre.dat index 38caf0a638..9cfe7df0e2 100644 --- a/esmvalcore/cmor/tables/custom/CMOR_swcre.dat +++ b/esmvalcore/cmor/tables/custom/CMOR_swcre.dat @@ -17,6 +17,6 @@ comment: at the top of the atmosphere (to be compared with satellite m !---------------------------------- dimensions: longitude latitude time type: real -positive: up +positive: down !---------------------------------- ! diff --git a/esmvalcore/config/extra_facets/icon-mappings.yml b/esmvalcore/config/extra_facets/icon-mappings.yml index 9ada69335f..f05035df1c 100644 --- a/esmvalcore/config/extra_facets/icon-mappings.yml +++ b/esmvalcore/config/extra_facets/icon-mappings.yml @@ -24,13 +24,16 @@ ICON: raw_name: cell_area # 2D dynamical/meteorological variables + asr: {var_type: atm_2d_ml} clivi: {var_type: atm_2d_ml} clt: {var_type: atm_2d_ml} clwvi: {var_type: atm_2d_ml} evspsbl: {var_type: atm_2d_ml} hfls: {var_type: atm_2d_ml} hfss: {var_type: atm_2d_ml} + lwcre: {var_type: atm_2d_ml} lwp: {raw_name: cllvi, var_type: atm_2d_ml} + netcre: {var_type: atm_2d_ml} pr: {var_type: atm_2d_ml} prw: {var_type: atm_2d_ml} ps: {var_type: atm_2d_ml} @@ -43,12 +46,16 @@ ICON: rsds: {var_type: atm_2d_ml} rsdscs: {var_type: atm_2d_ml} rsdt: {var_type: atm_2d_ml} + rsnt: {var_type: atm_2d_ml} rsus: {var_type: atm_2d_ml} rsuscs: {var_type: atm_2d_ml} rsut: {var_type: atm_2d_ml} rsutcs: {var_type: atm_2d_ml} + rtmt: {var_type: atm_2d_ml} + rtnt: {var_type: atm_2d_ml} siconc: {raw_name: sic, raw_units: '1', var_type: atm_2d_ml} siconca: {raw_name: sic, raw_units: '1', var_type: atm_2d_ml} + swcre: {var_type: atm_2d_ml} tas: {var_type: atm_2d_ml} tasmax: {var_type: atm_2d_ml} tasmin: {var_type: atm_2d_ml} @@ -68,3 +75,4 @@ ICON: ua: {var_type: atm_3d_ml} va: {var_type: atm_3d_ml} wap: {var_type: atm_3d_ml} + zg: {var_type: atm_3d_ml} diff --git a/esmvalcore/experimental/recipe_output.py b/esmvalcore/experimental/recipe_output.py index 9765d15c77..6e4ea9b277 100644 --- a/esmvalcore/experimental/recipe_output.py +++ b/esmvalcore/experimental/recipe_output.py @@ -1,7 +1,9 @@ """API for handing recipe output.""" import base64 +import getpass import logging import os.path +import sys from collections.abc import Mapping, Sequence from pathlib import Path from typing import Optional, Tuple, Type @@ -228,6 +230,37 @@ def from_core_recipe_output(cls, recipe_output: dict): return cls(task_output, session=session, info=info) + def _log_ssh_html_info(self): + """Log information about accessing index.html on an SSH server.""" + if 'SSH_CONNECTION' not in os.environ: + return + server_ip = os.environ['SSH_CONNECTION'].split()[2] + server_ip_env = '${server}' + server = f'{getpass.getuser()}@{server_ip_env}' + port = '31415' + port_env = '${port}' + command = ( + f'server={server_ip} && port={port} && ' + f'ssh -t -L {port_env}:localhost:{port_env} {server} ' + f'{sys.executable} -m http.server {port_env} -d ' + f'{self.session.session_dir}' + ) + logger.info( + "It looks like you are connected to a remote machine via SSH. To " + "show the output html file, you can try the following command:" + "\n%s\nThen visit http://localhost:%s in your browser", + command, + port, + ) + logger.info( + "If the port %s is already in use, you can replace it with any " + "other free one (e.g., 12789). If you are connected through a " + "jump host, replace the server IP address %s with your SSH server " + "name", + port, + server_ip, + ) + def write_html(self): """Write output summary to html document. @@ -242,6 +275,7 @@ def write_html(self): file.write(html_dump) logger.info("Wrote recipe output to:\nfile://%s", filename) + self._log_ssh_html_info() def render(self, template=None): """Render output as html. diff --git a/esmvalcore/preprocessor/_area.py b/esmvalcore/preprocessor/_area.py index 7d7bbe0c91..61f0402b39 100644 --- a/esmvalcore/preprocessor/_area.py +++ b/esmvalcore/preprocessor/_area.py @@ -938,6 +938,23 @@ def _mask_cube(cube: Cube, masks: dict[str, np.ndarray]) -> Cube: result = fix_coordinate_ordering(cubelist.merge_cube()) if cube.cell_measures(): for measure in cube.cell_measures(): + # Cell measures that are time-dependent, with 4 dimension and + # an original shape of (time, depth, lat, lon), need to be + # broadcasted to the cube with 5 dimensions and shape + # (time, shape_id, depth, lat, lon) + if measure.ndim > 3 and result.ndim > 4: + data = measure.core_data() + data = da.expand_dims(data, axis=(1,)) + data = da.broadcast_to(data, result.shape) + measure = iris.coords.CellMeasure( + data, + standard_name=measure.standard_name, + long_name=measure.long_name, + units=measure.units, + measure=measure.measure, + var_name=measure.var_name, + attributes=measure.attributes, + ) add_cell_measure(result, measure, measure.measure) if cube.ancillary_variables(): for ancillary_variable in cube.ancillary_variables(): diff --git a/esmvalcore/preprocessor/_derive/lwcre.py b/esmvalcore/preprocessor/_derive/lwcre.py index ed7b1ada77..7189560289 100644 --- a/esmvalcore/preprocessor/_derive/lwcre.py +++ b/esmvalcore/preprocessor/_derive/lwcre.py @@ -31,5 +31,6 @@ def calculate(cubes): lwcre_cube = rlutcs_cube - rlut_cube lwcre_cube.units = rlut_cube.units + lwcre_cube.attributes['positive'] = 'down' return lwcre_cube diff --git a/esmvalcore/preprocessor/_derive/netcre.py b/esmvalcore/preprocessor/_derive/netcre.py index 806a79000b..9c65e0ec7e 100644 --- a/esmvalcore/preprocessor/_derive/netcre.py +++ b/esmvalcore/preprocessor/_derive/netcre.py @@ -43,5 +43,6 @@ def calculate(cubes): netcre_cube = lwcre_cube + swcre_cube netcre_cube.units = lwcre_cube.units + netcre_cube.attributes['positive'] = 'down' return netcre_cube diff --git a/esmvalcore/preprocessor/_derive/rsnt.py b/esmvalcore/preprocessor/_derive/rsnt.py index 7838c3a0b1..dbd04ca2ca 100644 --- a/esmvalcore/preprocessor/_derive/rsnt.py +++ b/esmvalcore/preprocessor/_derive/rsnt.py @@ -30,5 +30,7 @@ def calculate(cubes): Constraint(name='toa_outgoing_shortwave_flux')) rsnt_cube = rsdt_cube - rsut_cube + rsnt_cube.units = rsdt_cube.units + rsnt_cube.attributes['positive'] = 'down' return rsnt_cube diff --git a/esmvalcore/preprocessor/_derive/swcre.py b/esmvalcore/preprocessor/_derive/swcre.py index 9fcf056f4f..c1873f622d 100644 --- a/esmvalcore/preprocessor/_derive/swcre.py +++ b/esmvalcore/preprocessor/_derive/swcre.py @@ -30,5 +30,7 @@ def calculate(cubes): Constraint(name='toa_outgoing_shortwave_flux_assuming_clear_sky')) swcre_cube = rsutcs_cube - rsut_cube + swcre_cube.units = rsut_cube.units + swcre_cube.attributes['positive'] = 'down' return swcre_cube diff --git a/esmvalcore/preprocessor/_io.py b/esmvalcore/preprocessor/_io.py index d53955b132..2c3bd761f0 100644 --- a/esmvalcore/preprocessor/_io.py +++ b/esmvalcore/preprocessor/_io.py @@ -36,55 +36,6 @@ } -def _fix_aux_factories(cube): - """Fix :class:`iris.aux_factory.AuxCoordFactory` after concatenation. - - Necessary because of bug in :mod:`iris` (see issue #2478). - """ - coord_names = [coord.name() for coord in cube.coords()] - - # Hybrid sigma pressure coordinate - # TODO possibly add support for other hybrid coordinates - if 'atmosphere_hybrid_sigma_pressure_coordinate' in coord_names: - new_aux_factory = iris.aux_factory.HybridPressureFactory( - delta=cube.coord(var_name='ap'), - sigma=cube.coord(var_name='b'), - surface_air_pressure=cube.coord(var_name='ps'), - ) - for aux_factory in cube.aux_factories: - if isinstance(aux_factory, iris.aux_factory.HybridPressureFactory): - break - else: - cube.add_aux_factory(new_aux_factory) - - # Hybrid sigma height coordinate - if 'atmosphere_hybrid_height_coordinate' in coord_names: - new_aux_factory = iris.aux_factory.HybridHeightFactory( - delta=cube.coord(var_name='lev'), - sigma=cube.coord(var_name='b'), - orography=cube.coord(var_name='orog'), - ) - for aux_factory in cube.aux_factories: - if isinstance(aux_factory, iris.aux_factory.HybridHeightFactory): - break - else: - cube.add_aux_factory(new_aux_factory) - - # Atmosphere sigma coordinate - if 'atmosphere_sigma_coordinate' in coord_names: - new_aux_factory = iris.aux_factory.AtmosphereSigmaFactory( - pressure_at_top=cube.coord(var_name='ptop'), - sigma=cube.coord(var_name='lev'), - surface_air_pressure=cube.coord(var_name='ps'), - ) - for aux_factory in cube.aux_factories: - if isinstance(aux_factory, - iris.aux_factory.AtmosphereSigmaFactory): - break - else: - cube.add_aux_factory(new_aux_factory) - - def _get_attr_from_field_coord(ncfield, coord_name, attr): if coord_name is not None: attrs = ncfield.cf_group[coord_name].cf_attrs() @@ -161,7 +112,12 @@ def load( 'message': "Ignoring netCDF variable '.*' invalid units '.*'", 'category': UserWarning, 'module': 'iris', - }) + }) # iris < 3.8 + ignore_warnings.append({ + 'message': "Ignoring invalid units .* on netCDF variable .*", + 'category': UserWarning, + 'module': 'iris', + }) # iris >= 3.8 # Filter warnings with catch_warnings(): @@ -384,8 +340,6 @@ def concatenate(cubes, check_level=CheckLevels.DEFAULT): else: _get_concatenation_error(result) - _fix_aux_factories(result) - return result diff --git a/esmvalcore/preprocessor/_multimodel.py b/esmvalcore/preprocessor/_multimodel.py index b74c6950cc..5390517a26 100644 --- a/esmvalcore/preprocessor/_multimodel.py +++ b/esmvalcore/preprocessor/_multimodel.py @@ -702,8 +702,8 @@ def multi_model_statistics( Desired statistics need to be given as a list, e.g., ``statistics: ['mean', 'median']``. For some statistics like percentiles, it is also possible to pass additional keyword arguments, for example ``statistics: [{'operator': - 'mean', 'percent': 20}]``. A full list of supported statistics is available - in the section on :ref:`stat_preprocs`. + 'percentile', 'percent': 20}]``. A full list of supported statistics is + available in the section on :ref:`stat_preprocs`. This function can handle cubes with differing metadata: @@ -758,9 +758,9 @@ def multi_model_statistics( statistics: Statistical operations to be computed, e.g., ``['mean', 'median']``. For some statistics like percentiles, it is also possible to pass - additional keyword arguments, e.g., ``[{'operator': 'mean', 'percent': - 20}]``. All supported options are are given in :ref:`this table - `. + additional keyword arguments, e.g., ``[{'operator': 'percentile', + 'percent': 20}]``. All supported options are are given in + :ref:`this table `. output_products: dict For internal use only. A dict with statistics names as keys and preprocessorfiles as values. If products are passed as input, the @@ -845,9 +845,9 @@ def ensemble_statistics( statistics: Statistical operations to be computed, e.g., ``['mean', 'median']``. For some statistics like percentiles, it is also possible to pass - additional keyword arguments, e.g., ``[{'operator': 'mean', 'percent': - 20}]``. All supported options are are given in :ref:`this table - `. + additional keyword arguments, e.g., ``[{'operator': 'percentile', + 'percent': 20}]``. All supported options are are given in + :ref:`this table `. output_products: dict For internal use only. A dict with statistics names as keys and preprocessorfiles as values. If products are passed as input, the diff --git a/esmvalcore/preprocessor/_time.py b/esmvalcore/preprocessor/_time.py index ee11e1146d..2466e5a064 100644 --- a/esmvalcore/preprocessor/_time.py +++ b/esmvalcore/preprocessor/_time.py @@ -1233,7 +1233,7 @@ def resample_hours(cube: Cube, interval: int, offset: int = 0) -> Cube: f'the interval ({interval})') time = cube.coord('time') cube_period = time.cell(1).point - time.cell(0).point - if cube_period.total_seconds() / 3600 >= interval: + if cube_period.total_seconds() / 3600 > interval: raise ValueError(f"Data period ({cube_period}) should be lower than " f"the interval ({interval})") hours = [PartialDateTime(hour=h) for h in range(0 + offset, 24, interval)] @@ -1436,9 +1436,9 @@ def _get_time_index_and_mask( def _transform_to_lst_eager( data: np.ndarray, - *, time_index: np.ndarray, mask: np.ndarray, + *, time_dim: int, lon_dim: int, **__, @@ -1479,9 +1479,9 @@ def _transform_to_lst_eager( def _transform_to_lst_lazy( data: da.core.Array, - *, time_index: np.ndarray, mask: np.ndarray, + *, time_dim: int, lon_dim: int, output_dtypes: DTypeLike, @@ -1505,28 +1505,25 @@ def _transform_to_lst_lazy( `mask` is 2D with shape (time, lon) that will be applied to the final data. """ - _transform_chunk_to_lst = partial( - _transform_to_lst_eager, - time_index=time_index, - mask=mask, - time_dim=-2, # this is ensured by da.apply_gufunc - lon_dim=-1, # this is ensured by da.apply_gufunc - ) new_data = da.apply_gufunc( - _transform_chunk_to_lst, - '(t,y)->(t,y)', + _transform_to_lst_eager, + '(t,x),(t,x),(t,x)->(t,x)', data, - axes=[(time_dim, lon_dim), (time_dim, lon_dim)], + time_index, + mask, + axes=[(time_dim, lon_dim), (0, 1), (0, 1), (time_dim, lon_dim)], output_dtypes=output_dtypes, + time_dim=-2, # this is ensured by da.apply_gufunc + lon_dim=-1, # this is ensured by da.apply_gufunc ) return new_data def _transform_arr_to_lst( data: np.ndarray | da.core.Array, - *, time_index: np.ndarray, mask: np.ndarray, + *, time_dim: int, lon_dim: int, output_dtypes: DTypeLike, @@ -1545,8 +1542,8 @@ def _transform_arr_to_lst( func = _transform_to_lst_lazy # type: ignore new_data = func( data, # type: ignore - time_index=time_index, - mask=mask, + time_index, + mask, time_dim=time_dim, lon_dim=lon_dim, output_dtypes=output_dtypes, @@ -1571,13 +1568,10 @@ def _transform_cube_to_lst(cube: Cube) -> Cube: # Transform cube data (time_index, mask) = _get_time_index_and_mask(time_coord, lon_coord) - _transform_arr = partial( - _transform_arr_to_lst, - time_index=time_index, - mask=mask, - ) - cube.data = _transform_arr( + cube.data = _transform_arr_to_lst( cube.core_data(), + time_index, + mask, time_dim=time_dim, lon_dim=lon_dim, output_dtypes=cube.dtype, @@ -1589,15 +1583,19 @@ def _transform_cube_to_lst(cube: Cube) -> Cube: if time_dim in dims and lon_dim in dims: time_dim_ = dims.index(time_dim) lon_dim_ = dims.index(lon_dim) - coord.points = _transform_arr( + coord.points = _transform_arr_to_lst( coord.core_points(), + time_index, + mask, time_dim=time_dim_, lon_dim=lon_dim_, output_dtypes=coord.dtype, ) if coord.has_bounds(): - coord.bounds = _transform_arr( + coord.bounds = _transform_arr_to_lst( coord.core_bounds(), + time_index, + mask, time_dim=time_dim_, lon_dim=lon_dim_, output_dtypes=coord.bounds_dtype, @@ -1609,8 +1607,10 @@ def _transform_cube_to_lst(cube: Cube) -> Cube: if time_dim in dims and lon_dim in dims: time_dim_ = dims.index(time_dim) lon_dim_ = dims.index(lon_dim) - cell_measure.data = _transform_arr( + cell_measure.data = _transform_arr_to_lst( cell_measure.core_data(), + time_index, + mask, time_dim=time_dim_, lon_dim=lon_dim_, output_dtypes=cell_measure.dtype, @@ -1622,8 +1622,10 @@ def _transform_cube_to_lst(cube: Cube) -> Cube: if time_dim in dims and lon_dim in dims: time_dim_ = dims.index(time_dim) lon_dim_ = dims.index(lon_dim) - anc_var.data = _transform_arr( + anc_var.data = _transform_arr_to_lst( anc_var.core_data(), + time_index, + mask, time_dim=time_dim_, lon_dim=lon_dim_, output_dtypes=anc_var.dtype, diff --git a/esmvalcore/preprocessor/_volume.py b/esmvalcore/preprocessor/_volume.py index 878bc03c68..e956b2c35c 100644 --- a/esmvalcore/preprocessor/_volume.py +++ b/esmvalcore/preprocessor/_volume.py @@ -14,9 +14,8 @@ import numpy as np from iris.coords import AuxCoord, CellMeasure from iris.cube import Cube -from iris.exceptions import CoordinateMultiDimError -from ._area import compute_area_weights +from ._area import _try_adding_calculated_cell_area from ._shared import ( get_iris_aggregator, get_normalized_cube, @@ -105,12 +104,14 @@ def calculate_volume(cube: Cube) -> da.core.Array: """Calculate volume from a cube. This function is used when the 'ocean_volume' cell measure can't be found. + The output data will be given in cubic meters (m3). Note ---- - This only works if the grid cell areas can be calculated (i.e., latitude - and longitude are 1D) and if the depth coordinate is 1D or 4D with first - dimension 1. + It gets the cell_area from the cube if it is available. If not, it + calculates it from the grid. This only works if the grid cell areas can + be calculated (i.e., latitude and longitude are 1D). The depth coordinate + units should be convertible to meters. Parameters ---------- @@ -125,33 +126,50 @@ def calculate_volume(cube: Cube) -> da.core.Array: """ # Load depth field and figure out which dim is which depth = cube.coord(axis='z') - z_dim = cube.coord_dims(depth)[0] + z_dim = cube.coord_dims(depth) + depth = depth.copy() + + # Assert z has length > 0 + if not z_dim: + raise ValueError("Cannot compute volume with scalar Z-axis") + + # Guess bounds if missing + if not depth.has_bounds(): + depth.guess_bounds() + if depth.core_bounds().shape[-1] != 2: + raise ValueError( + f"Z axis bounds shape found {depth.core_bounds().shape}. " + "Bounds should be 2 in the last dimension to compute the " + "thickness.") + + # Convert units to get the thickness in meters + try: + depth.convert_units('m') + except ValueError as err: + raise ValueError( + f'Cannot compute volume using the Z-axis. {err}') from err # Calculate Z-direction thickness thickness = depth.core_bounds()[..., 1] - depth.core_bounds()[..., 0] - # Try to calculate grid cell area - try: - area = da.array(compute_area_weights(cube)) - except CoordinateMultiDimError: - logger.error( - "Supplementary variables are needed to calculate grid cell " - "areas for irregular grid of cube %s", - cube.summary(shorten=True), - ) - raise + # Get or calculate the horizontal areas of the cube + has_cell_measure = bool(cube.cell_measures('cell_area')) + _try_adding_calculated_cell_area(cube) + area = cube.cell_measure('cell_area').copy() + area_dim = cube.cell_measure_dims(area) - # Try to calculate grid cell volume as area * thickness - if thickness.ndim == 1 and z_dim == 1: - grid_volume = area * thickness[None, :, None, None] - elif thickness.ndim == 4 and z_dim == 1: - grid_volume = area * thickness[:, :] - else: - raise ValueError( - f"Supplementary variables are needed to calculate grid cell " - f"volumes for cubes with {thickness.ndim:d}D depth coordinate, " - f"got cube {cube.summary(shorten=True)}" - ) + # Ensure cell area is in square meters as the units + area.convert_units('m2') + + # Make sure input cube has not been modified + if not has_cell_measure: + cube.remove_cell_measure('cell_area') + + area_arr = iris.util.broadcast_to_shape( + area.core_data(), cube.shape, area_dim) + thickness_arr = iris.util.broadcast_to_shape( + thickness, cube.shape, z_dim) + grid_volume = area_arr * thickness_arr return grid_volume @@ -180,7 +198,7 @@ def _try_adding_calculated_ocean_volume(cube: Cube) -> None: @register_supplementaries( - variables=['volcello'], + variables=['volcello', 'areacello'], required='prefer_at_least_one', ) def volume_statistics( @@ -198,10 +216,13 @@ def volume_statistics( cube: Input cube. The input cube should have a :class:`iris.coords.CellMeasure` named ``'ocean_volume'``, unless it - has regular 1D latitude and longitude coordinates so the cell volumes - can be computed by using :func:`iris.analysis.cartography.area_weights` - to compute the cell areas and multiplying those by the cell thickness, - computed from the bounds of the vertical coordinate. + has a :class:`iris.coords.CellMeasure` named ``'cell_area'`` or + regular 1D latitude and longitude coordinates so the cell areas + can be computed using :func:`iris.analysis.cartography.area_weights`. + The volume will be computed from the area multiplied by the + thickness, computed from the bounds of the vertical coordinate. + In that case, vertical coordinate units should be convertible to + meters. operator: The operation. Used to determine the :class:`iris.analysis.Aggregator` object used to calculate the statistics. Currently, only `mean` is @@ -215,6 +236,11 @@ def volume_statistics( Optional keyword arguments for the :class:`iris.analysis.Aggregator` object defined by `operator`. + Note + ---- + This preprocessor has been designed for oceanic variables, but it might + be applicable to atmospheric data as well. + Returns ------- iris.cube.Cube @@ -227,6 +253,20 @@ def volume_statistics( # TODO: Add other operations. if operator != 'mean': raise ValueError(f"Volume operator {operator} not recognised.") + # get z, y, x coords + z_axis = cube.coord(axis='Z') + y_axis = cube.coord(axis='Y') + x_axis = cube.coord(axis='X') + + # assert z axis only uses 1 dimension more than x, y axis + xy_dims = tuple({*cube.coord_dims(y_axis), *cube.coord_dims(x_axis)}) + xyz_dims = tuple({*cube.coord_dims(z_axis), *xy_dims}) + if len(xyz_dims) > len(xy_dims) + 1: + raise ValueError( + f"X and Y axis coordinates depend on {xy_dims} dimensions, " + f"while X, Y, and Z axis depends on {xyz_dims} dimensions. " + "This may indicate Z axis depending on other dimension than " + "space that could provoke invalid aggregation...") (agg, agg_kwargs) = get_iris_aggregator(operator, **operator_kwargs) agg_kwargs = update_weights_kwargs( @@ -237,11 +277,7 @@ def volume_statistics( _try_adding_calculated_ocean_volume, ) - result = cube.collapsed( - [cube.coord(axis='Z'), cube.coord(axis='Y'), cube.coord(axis='X')], - agg, - **agg_kwargs, - ) + result = cube.collapsed([z_axis, y_axis, x_axis], agg, **agg_kwargs) if normalize is not None: result = get_normalized_cube(cube, result, normalize) diff --git a/setup.py b/setup.py index fc6cdb2678..72e82e27e5 100755 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ 'netCDF4', 'numpy!=1.24.3', 'packaging', - 'pandas!=2.2.0,!=2.2.1', # GH ESMValCore #2305 #2349 + 'pandas!=2.2.0,!=2.2.1,!=2.2.2', # GH #2305 #2349 etc 'pillow', 'prov', 'psutil', diff --git a/tests/integration/cmor/_fixes/cordex/test_cnrm_cerfacs_cnrm_cm5.py b/tests/integration/cmor/_fixes/cordex/test_cnrm_cerfacs_cnrm_cm5.py index 47b0cb81f4..63428e913c 100644 --- a/tests/integration/cmor/_fixes/cordex/test_cnrm_cerfacs_cnrm_cm5.py +++ b/tests/integration/cmor/_fixes/cordex/test_cnrm_cerfacs_cnrm_cm5.py @@ -2,8 +2,11 @@ import iris import pytest -from esmvalcore.cmor._fixes.cordex.cnrm_cerfacs_cnrm_cm5 import aladin63 +from esmvalcore.cmor._fixes.cordex.cnrm_cerfacs_cnrm_cm5 import ( + aladin63, + wrf381p,) from esmvalcore.cmor.fix import Fix +from esmvalcore.cmor.table import get_var_info @pytest.fixture @@ -59,3 +62,32 @@ def test_aladin63_height_fix(cubes): assert cubes is out_cubes for cube in out_cubes: assert cube.coord('height').points == 2.0 + + +@pytest.mark.parametrize( + 'short_name', + ['tasmax', 'tasmin', 'tas', 'hurs', 'huss']) +def test_get_wrf381p_fix(short_name): + fix = Fix.get_fixes( + 'CORDEX', + 'WRF381P', + 'Amon', + short_name, + extra_facets={'driver': 'CNRM-CERFACS-CNRM-CM5'}) + assert isinstance(fix[0], Fix) + + +def test_wrf381p_height_fix(): + time_coord = iris.coords.DimCoord([0.0], + var_name='time', + standard_name='time', + long_name='time') + cube = iris.cube.Cube( + [10.0], + var_name='tas', + dim_coords_and_dims=[(time_coord, 0)], + ) + vardef = get_var_info('CMIP6', 'Amon', 'tas') + fix = wrf381p.Tas(vardef) + out_cubes = fix.fix_metadata([cube]) + assert out_cubes[0].coord('height').points == 2.0 diff --git a/tests/integration/cmor/_fixes/cordex/test_ichec_ec_earth.py b/tests/integration/cmor/_fixes/cordex/test_ichec_ec_earth.py index 8b888a0b79..1314dc5341 100644 --- a/tests/integration/cmor/_fixes/cordex/test_ichec_ec_earth.py +++ b/tests/integration/cmor/_fixes/cordex/test_ichec_ec_earth.py @@ -1,7 +1,10 @@ """Tests for the fixes for driver ICHEC-EC-Earth.""" +import iris import pytest +from esmvalcore.cmor._fixes.cordex.ichec_ec_earth import wrf381p from esmvalcore.cmor.fix import Fix +from esmvalcore.cmor.table import get_var_info def test_get_remo2015_fix(): @@ -44,3 +47,32 @@ def test_get_rca4_fix(short_name): short_name, extra_facets={'driver': 'ICHEC-EC-Earth'}) assert isinstance(fix[0], Fix) + + +@pytest.mark.parametrize( + 'short_name', + ['tasmax', 'tasmin', 'tas', 'hurs', 'huss']) +def test_get_wrf381p_fix(short_name): + fix = Fix.get_fixes( + 'CORDEX', + 'WRF381P', + 'Amon', + short_name, + extra_facets={'driver': 'ICHEC-EC-Earth'}) + assert isinstance(fix[0], Fix) + + +def test_wrf381p_height_fix(): + time_coord = iris.coords.DimCoord([0.0], + var_name='time', + standard_name='time', + long_name='time') + cube = iris.cube.Cube( + [10.0], + var_name='tas', + dim_coords_and_dims=[(time_coord, 0)], + ) + vardef = get_var_info('CMIP6', 'Amon', 'tas') + fix = wrf381p.Tas(vardef) + out_cubes = fix.fix_metadata([cube]) + assert out_cubes[0].coord('height').points == 2.0 diff --git a/tests/integration/cmor/_fixes/cordex/test_ipsl_ipsl_cm5a_mr.py b/tests/integration/cmor/_fixes/cordex/test_ipsl_ipsl_cm5a_mr.py new file mode 100644 index 0000000000..5f879dfe9c --- /dev/null +++ b/tests/integration/cmor/_fixes/cordex/test_ipsl_ipsl_cm5a_mr.py @@ -0,0 +1,36 @@ +"""Tests for the fixes of driver IPSL-CM5A-MR.""" +import iris +import pytest + +from esmvalcore.cmor._fixes.cordex.ipsl_ipsl_cm5a_mr import wrf381p +from esmvalcore.cmor.fix import Fix +from esmvalcore.cmor.table import get_var_info + + +@pytest.mark.parametrize( + 'short_name', + ['tasmax', 'tasmin', 'tas', 'hurs', 'huss']) +def test_get_wrf381p_fix(short_name): + fix = Fix.get_fixes( + 'CORDEX', + 'WRF381P', + 'Amon', + short_name, + extra_facets={'driver': 'IPSL-CM5A-MR'}) + assert isinstance(fix[0], Fix) + + +def test_wrf381p_height_fix(): + time_coord = iris.coords.DimCoord([0.0], + var_name='time', + standard_name='time', + long_name='time') + cube = iris.cube.Cube( + [10.0], + var_name='tas', + dim_coords_and_dims=[(time_coord, 0)], + ) + vardef = get_var_info('CMIP6', 'Amon', 'tas') + fix = wrf381p.Tas(vardef) + out_cubes = fix.fix_metadata([cube]) + assert out_cubes[0].coord('height').points == 2.0 diff --git a/tests/integration/cmor/_fixes/cordex/test_mohc_hadgem2_es.py b/tests/integration/cmor/_fixes/cordex/test_mohc_hadgem2_es.py index f504b81aec..0584165e9e 100644 --- a/tests/integration/cmor/_fixes/cordex/test_mohc_hadgem2_es.py +++ b/tests/integration/cmor/_fixes/cordex/test_mohc_hadgem2_es.py @@ -2,8 +2,9 @@ import iris import pytest -from esmvalcore.cmor._fixes.cordex.mohc_hadgem2_es import hirham5 +from esmvalcore.cmor._fixes.cordex.mohc_hadgem2_es import hirham5, wrf381p from esmvalcore.cmor.fix import Fix +from esmvalcore.cmor.table import get_var_info @pytest.fixture @@ -103,3 +104,32 @@ def test_hirham5_fix(cubes): for cube in out_cubes: assert cube.coord('latitude').attributes == {} assert cube.coord('longitude').attributes == {} + + +@pytest.mark.parametrize( + 'short_name', + ['tasmax', 'tasmin', 'tas', 'hurs', 'huss']) +def test_get_wrf381p_fix(short_name): + fix = Fix.get_fixes( + 'CORDEX', + 'WRF381P', + 'Amon', + short_name, + extra_facets={'driver': 'MOHC-HadGEM2-ES'}) + assert isinstance(fix[0], Fix) + + +def test_wrf381p_height_fix(): + time_coord = iris.coords.DimCoord([0.0], + var_name='time', + standard_name='time', + long_name='time') + cube = iris.cube.Cube( + [10.0], + var_name='tas', + dim_coords_and_dims=[(time_coord, 0)], + ) + vardef = get_var_info('CMIP6', 'Amon', 'tas') + fix = wrf381p.Tas(vardef) + out_cubes = fix.fix_metadata([cube]) + assert out_cubes[0].coord('height').points == 2.0 diff --git a/tests/integration/cmor/_fixes/cordex/test_ncc_noresm1_m.py b/tests/integration/cmor/_fixes/cordex/test_ncc_noresm1_m.py index 209c2311fd..8b5c4ab084 100644 --- a/tests/integration/cmor/_fixes/cordex/test_ncc_noresm1_m.py +++ b/tests/integration/cmor/_fixes/cordex/test_ncc_noresm1_m.py @@ -1,7 +1,10 @@ """Tests for the fixes of driver NCC-NorESM1-M.""" +import iris import pytest +from esmvalcore.cmor._fixes.cordex.ncc_noresm1_m import wrf381p from esmvalcore.cmor.fix import Fix +from esmvalcore.cmor.table import get_var_info def test_get_remo2015_fix(): @@ -44,3 +47,32 @@ def test_get_rca4_fix(short_name): short_name, extra_facets={'driver': 'NCC-NorESM1-M'}) assert isinstance(fix[0], Fix) + + +@pytest.mark.parametrize( + 'short_name', + ['tasmax', 'tasmin', 'tas', 'hurs', 'huss']) +def test_get_wrf381p_fix(short_name): + fix = Fix.get_fixes( + 'CORDEX', + 'WRF381P', + 'Amon', + short_name, + extra_facets={'driver': 'NCC-NorESM1-M'}) + assert isinstance(fix[0], Fix) + + +def test_wrf381p_height_fix(): + time_coord = iris.coords.DimCoord([0.0], + var_name='time', + standard_name='time', + long_name='time') + cube = iris.cube.Cube( + [10.0], + var_name='tas', + dim_coords_and_dims=[(time_coord, 0)], + ) + vardef = get_var_info('CMIP6', 'Amon', 'tas') + fix = wrf381p.Tas(vardef) + out_cubes = fix.fix_metadata([cube]) + assert out_cubes[0].coord('height').points == 2.0 diff --git a/tests/integration/cmor/_fixes/icon/test_icon.py b/tests/integration/cmor/_fixes/icon/test_icon.py index ab823cb846..24fd1d4bac 100644 --- a/tests/integration/cmor/_fixes/icon/test_icon.py +++ b/tests/integration/cmor/_fixes/icon/test_icon.py @@ -1,4 +1,5 @@ """Tests for the ICON on-the-fly CMORizer.""" +from copy import deepcopy from datetime import datetime from pathlib import Path from unittest import mock @@ -14,7 +15,14 @@ import esmvalcore.cmor._fixes.icon.icon from esmvalcore.cmor._fixes.fix import GenericFix from esmvalcore.cmor._fixes.icon._base_fixes import IconFix -from esmvalcore.cmor._fixes.icon.icon import AllVars, Clwvi +from esmvalcore.cmor._fixes.icon.icon import ( + AllVars, + Clwvi, + Hfls, + Hfss, + Rtmt, + Rtnt, +) from esmvalcore.cmor.fix import Fix from esmvalcore.cmor.table import CoordinateInfo, get_var_info from esmvalcore.config import CFG @@ -121,6 +129,7 @@ def _get_fix(mip, short_name, fix_name, session=None): ) extra_facets = get_extra_facets(dataset, ()) extra_facets['frequency'] = 'mon' + extra_facets['exp'] = 'amip' vardef = get_var_info(project='ICON', mip=mip, short_name=short_name) cls = getattr(esmvalcore.cmor._fixes.icon.icon, fix_name) fix = cls(vardef, extra_facets=extra_facets, session=session) @@ -147,6 +156,15 @@ def fix_metadata(cubes, mip, short_name, session=None): return cubes +def fix_data(cube, mip, short_name, session=None): + """Fix data of cube.""" + fix = get_fix(mip, short_name, session=session) + cube = fix.fix_data(cube) + fix = get_allvars_fix(mip, short_name, session=session) + cube = fix.fix_data(cube) + return cube + + def check_ta_metadata(cubes): """Check ta metadata.""" assert len(cubes) == 1 @@ -1202,6 +1220,33 @@ def test_get_horizontal_grid_from_attr_cached_in_dict( mock_get_grid_from_facet.assert_not_called() +@mock.patch.object(IconFix, '_get_grid_from_facet', autospec=True) +def test_get_horizontal_grid_from_attr_rootpath( + mock_get_grid_from_facet, monkeypatch, tmp_path +): + """Test fix.""" + rootpath = deepcopy(CFG['rootpath']) + rootpath['ICON'] = str(tmp_path) + monkeypatch.setitem(CFG, 'rootpath', rootpath) + cube = Cube(0, attributes={'grid_file_uri': 'grid.nc'}) + grid_cube = Cube(0, var_name='test_grid_cube') + (tmp_path / 'amip').mkdir(parents=True, exist_ok=True) + iris.save(grid_cube, tmp_path / 'amip' / 'grid.nc') + + fix = get_allvars_fix('Amon', 'tas') + fix._horizontal_grids['grid_from_facet.nc'] = mock.sentinel.wrong_grid + + grid = fix.get_horizontal_grid(cube) + assert len(fix._horizontal_grids) == 2 + assert 'grid.nc' in fix._horizontal_grids + assert 'grid_from_facet.nc' in fix._horizontal_grids # has not been used + assert fix._horizontal_grids['grid.nc'] == grid + assert len(grid) == 1 + assert grid[0].var_name == 'test_grid_cube' + assert grid[0].shape == () + mock_get_grid_from_facet.assert_not_called() + + @mock.patch.object(IconFix, '_get_grid_from_facet', autospec=True) @mock.patch('esmvalcore.cmor._fixes.icon._base_fixes.requests', autospec=True) def test_get_horizontal_grid_from_attr_cached_in_file( @@ -1223,6 +1268,7 @@ def test_get_horizontal_grid_from_attr_cached_in_file( assert isinstance(grid, CubeList) assert len(grid) == 1 assert grid[0].var_name == 'grid' + assert grid[0].shape == () assert len(fix._horizontal_grids) == 1 assert 'grid_file.nc' in fix._horizontal_grids assert fix._horizontal_grids['grid_file.nc'] == grid @@ -2209,3 +2255,136 @@ def test_fix_height_alt16(bounds, simple_unstructured_cube): np.testing.assert_allclose(alt16.bounds, expected_bnds) else: assert alt16.bounds is None + + +# Test hfls (for extra fix) + + +def test_get_hfls_fix(): + """Test getting of fix.""" + fix = Fix.get_fixes('ICON', 'ICON', 'Amon', 'hfls') + assert fix == [Hfls(None), AllVars(None), GenericFix(None)] + + +def test_hfls_fix(cubes_regular_grid): + """Test fix.""" + cubes = CubeList([cubes_regular_grid[0].copy()]) + cubes[0].var_name = 'hfls' + cubes[0].units = 'W m-2' + + fixed_cubes = fix_metadata(cubes, 'Amon', 'hfls') + + assert len(fixed_cubes) == 1 + cube = fixed_cubes[0] + assert cube.var_name == 'hfls' + assert cube.standard_name == 'surface_upward_latent_heat_flux' + assert cube.long_name == 'Surface Upward Latent Heat Flux' + assert cube.units == 'W m-2' + assert cube.attributes['positive'] == 'up' + + fixed_cube = fix_data(cube, 'Amon', 'hfls') + + np.testing.assert_allclose(fixed_cube.data, [[[0.0, -1.0], [-2.0, -3.0]]]) + + +# Test hfss (for extra fix) + + +def test_get_hfss_fix(): + """Test getting of fix.""" + fix = Fix.get_fixes('ICON', 'ICON', 'Amon', 'hfss') + assert fix == [Hfss(None), AllVars(None), GenericFix(None)] + + +def test_hfss_fix(cubes_regular_grid): + """Test fix.""" + cubes = CubeList([cubes_regular_grid[0].copy()]) + cubes[0].var_name = 'hfss' + cubes[0].units = 'W m-2' + + fixed_cubes = fix_metadata(cubes, 'Amon', 'hfss') + + assert len(fixed_cubes) == 1 + cube = fixed_cubes[0] + assert cube.var_name == 'hfss' + assert cube.standard_name == 'surface_upward_sensible_heat_flux' + assert cube.long_name == 'Surface Upward Sensible Heat Flux' + assert cube.units == 'W m-2' + assert cube.attributes['positive'] == 'up' + + fixed_cube = fix_data(cube, 'Amon', 'hfss') + + np.testing.assert_allclose(fixed_cube.data, [[[0.0, -1.0], [-2.0, -3.0]]]) + + +# Test rtnt (for extra fix) + + +def test_get_rtnt_fix(): + """Test getting of fix.""" + fix = Fix.get_fixes('ICON', 'ICON', 'Amon', 'rtnt') + assert fix == [Rtnt(None), AllVars(None), GenericFix(None)] + + +def test_rtnt_fix(cubes_regular_grid): + """Test fix.""" + cubes = CubeList([ + cubes_regular_grid[0].copy(), + cubes_regular_grid[0].copy(), + cubes_regular_grid[0].copy() + ]) + cubes[0].var_name = 'rsdt' + cubes[1].var_name = 'rsut' + cubes[2].var_name = 'rlut' + cubes[0].units = 'W m-2' + cubes[1].units = 'W m-2' + cubes[2].units = 'W m-2' + + fixed_cubes = fix_metadata(cubes, 'Amon', 'rtnt') + + assert len(fixed_cubes) == 1 + cube = fixed_cubes[0] + assert cube.var_name == 'rtnt' + assert cube.standard_name is None + assert cube.long_name == 'TOA Net downward Total Radiation' + assert cube.units == 'W m-2' + assert cube.attributes['positive'] == 'down' + + np.testing.assert_allclose(cube.data, [[[0.0, -1.0], [-2.0, -3.0]]]) + + +# Test rtmt (for extra fix) + + +def test_get_rtmt_fix(): + """Test getting of fix.""" + fix = Fix.get_fixes('ICON', 'ICON', 'Amon', 'rtmt') + assert fix == [Rtmt(None), AllVars(None), GenericFix(None)] + + +def test_rtmt_fix(cubes_regular_grid): + """Test fix.""" + cubes = CubeList([ + cubes_regular_grid[0].copy(), + cubes_regular_grid[0].copy(), + cubes_regular_grid[0].copy() + ]) + cubes[0].var_name = 'rsdt' + cubes[1].var_name = 'rsut' + cubes[2].var_name = 'rlut' + cubes[0].units = 'W m-2' + cubes[1].units = 'W m-2' + cubes[2].units = 'W m-2' + + fixed_cubes = fix_metadata(cubes, 'Amon', 'rtmt') + + assert len(fixed_cubes) == 1 + cube = fixed_cubes[0] + assert cube.var_name == 'rtmt' + assert cube.standard_name == ('net_downward_radiative_flux_at_top_of' + '_atmosphere_model') + assert cube.long_name == 'Net Downward Radiative Flux at Top of Model' + assert cube.units == 'W m-2' + assert cube.attributes['positive'] == 'down' + + np.testing.assert_allclose(cube.data, [[[0.0, -1.0], [-2.0, -3.0]]]) diff --git a/tests/integration/cmor/_fixes/test_common.py b/tests/integration/cmor/_fixes/test_common.py index 809c1fbfd2..8eedfa084a 100644 --- a/tests/integration/cmor/_fixes/test_common.py +++ b/tests/integration/cmor/_fixes/test_common.py @@ -141,6 +141,7 @@ def hybrid_height_coord_fix_metadata(nc_path, short_name, fix): assert air_pressure_coord.units == 'Pa' +@pytest.mark.xfail(reason="github.com/SciTools/iris/issues/5806") def test_cl_hybrid_height_coord_fix_metadata(test_data_path): """Test ``fix_metadata`` for ``cl``.""" vardef = get_var_info('CMIP6', 'Amon', 'cl') diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 2771db32c0..89be978730 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -40,43 +40,72 @@ def create_test_file(filename, tracking_id=None): def _get_files(root_path, facets, tracking_id): + """Return dummy files. + + Wildcards are only supported for `dataset` and `institute`; in this case + return files for the two "models" AAA and BBB. + + """ + if facets['dataset'] == '*': + all_facets = [ + {**facets, 'dataset': 'AAA', 'institute': 'A'}, + {**facets, 'dataset': 'BBB', 'institute': 'B'}, + ] + else: + all_facets = [facets] + + # Globs without expanded facets + dir_template = _select_drs('input_dir', facets['project']) file_template = _select_drs('input_file', facets['project']) + dir_globs = _replace_tags(dir_template, facets) file_globs = _replace_tags(file_template, facets) - filename = Path(file_globs[0]).name - filename = str(root_path / 'input' / filename) - filenames = [] - if filename.endswith('[_.]*nc'): - # Restore when we support filenames with no dates - # filenames.append(filename.replace('[_.]*nc', '.nc')) - filename = filename.replace('[_.]*nc', '_*.nc') - if filename.endswith('*.nc'): - filename = filename[:-len('*.nc')] + '_' - if facets['frequency'] == 'fx': - intervals = [''] + globs = sorted( + root_path / 'input' / d / f for d in dir_globs for f in file_globs + ) + + files = [] + for expanded_facets in all_facets: + filenames = [] + dir_template = _select_drs('input_dir', expanded_facets['project']) + file_template = _select_drs('input_file', expanded_facets['project']) + dir_globs = _replace_tags(dir_template, expanded_facets) + file_globs = _replace_tags(file_template, expanded_facets) + filename = ( + str(root_path / 'input' / dir_globs[0] / Path(file_globs[0]).name) + ) + + if filename.endswith('[_.]*nc'): + # Restore when we support filenames with no dates + # filenames.append(filename.replace('[_.]*nc', '.nc')) + filename = filename.replace('[_.]*nc', '_*.nc') + + if filename.endswith('*.nc'): + filename = filename[:-len('*.nc')] + '_' + if facets['frequency'] == 'fx': + intervals = [''] + else: + intervals = [ + '1990_1999', + '2000_2009', + '2010_2019', + ] + for interval in intervals: + filenames.append(filename + interval + '.nc') else: - intervals = [ - '1990_1999', - '2000_2009', - '2010_2019', - ] - for interval in intervals: - filenames.append(filename + interval + '.nc') - else: - filenames.append(filename) + filenames.append(filename) - if 'timerange' in facets: - filenames = _select_files(filenames, facets['timerange']) + if 'timerange' in facets: + filenames = _select_files(filenames, facets['timerange']) - for filename in filenames: - create_test_file(filename, next(tracking_id)) + for filename in filenames: + create_test_file(filename, next(tracking_id)) - files = [] - for filename in filenames: - file = LocalFile(filename) - file.facets = facets - files.append(file) + for filename in filenames: + file = LocalFile(filename) + file.facets = expanded_facets + files.append(file) - return files, file_globs + return files, globs @pytest.fixture @@ -100,6 +129,15 @@ def find_files(*, debug: bool = False, **facets): @pytest.fixture def patched_failing_datafinder(tmp_path, monkeypatch): + """Failing data finder. + + Do not return files for: + - fx files + - Variable rsutcs for model AAA + + Otherwise, return files just like `patched_datafinder`. + + """ def tracking_ids(i=0): while True: @@ -112,8 +150,12 @@ def find_files(*, debug: bool = False, **facets): files, file_globs = _get_files(tmp_path, facets, tracking_id) if 'fx' == facets['frequency']: files = [] + returned_files = [] + for file in files: + if not ('AAA' in file.name and 'rsutcs' in file.name): + returned_files.append(file) if debug: - return files, file_globs - return files + return returned_files, file_globs + return returned_files monkeypatch.setattr(esmvalcore.local, 'find_files', find_files) diff --git a/tests/integration/preprocessor/_io/test_concatenate.py b/tests/integration/preprocessor/_io/test_concatenate.py index 3171c27a25..d8d5d680c1 100644 --- a/tests/integration/preprocessor/_io/test_concatenate.py +++ b/tests/integration/preprocessor/_io/test_concatenate.py @@ -1,17 +1,11 @@ """Integration tests for :func:`esmvalcore.preprocessor._io.concatenate`.""" import unittest -import warnings -from unittest.mock import call import numpy as np import pytest from cf_units import Unit -from iris.aux_factory import ( - AtmosphereSigmaFactory, - HybridHeightFactory, - HybridPressureFactory, -) +from iris.aux_factory import HybridPressureFactory from iris.coords import AuxCoord, DimCoord from iris.cube import Cube, CubeList @@ -64,98 +58,6 @@ def get_time_coord(time_point): units='days since 6453-2-1') -@pytest.fixture -def mock_empty_cube(): - """Return mocked cube with irrelevant coordinates.""" - cube = unittest.mock.create_autospec(Cube, spec_set=True, instance=True) - a_coord = AuxCoord(0.0, var_name='a') - b_coord = AuxCoord(0.0, var_name='b') - cube.coords.return_value = [a_coord, b_coord] - return cube - - -@pytest.fixture -def mock_atmosphere_sigma_cube(): - """Return mocked cube with atmosphere sigma coordinate.""" - cube = unittest.mock.create_autospec(Cube, spec_set=True, instance=True) - ptop_coord = AuxCoord([1.0], var_name='ptop', units='Pa') - lev_coord = AuxCoord([0.0], - bounds=[[-0.5, 1.5]], - var_name='lev', - units='1') - ps_coord = AuxCoord([[[100000]]], var_name='ps', units='Pa') - cube.coord.side_effect = [ - ptop_coord, lev_coord, ps_coord, ptop_coord, lev_coord, ps_coord - ] - cube.coords.return_value = [ - ptop_coord, - lev_coord, - ps_coord, - AuxCoord(0.0, standard_name='atmosphere_sigma_coordinate'), - ] - aux_factory = AtmosphereSigmaFactory( - pressure_at_top=ptop_coord, - sigma=lev_coord, - surface_air_pressure=ps_coord, - ) - cube.aux_factories = ['dummy', aux_factory] - return cube - - -@pytest.fixture -def mock_hybrid_height_cube(): - """Return mocked cube with hybrid height coordinate.""" - cube = unittest.mock.create_autospec(Cube, spec_set=True, instance=True) - lev_coord = AuxCoord([1.0], bounds=[[0.0, 2.0]], var_name='lev', units='m') - b_coord = AuxCoord([0.0], bounds=[[-0.5, 1.5]], var_name='b') - orog_coord = AuxCoord([[[100000]]], var_name='orog', units='m') - cube.coord.side_effect = [ - lev_coord, b_coord, orog_coord, lev_coord, b_coord, orog_coord - ] - cube.coords.return_value = [ - lev_coord, - b_coord, - orog_coord, - AuxCoord(0.0, standard_name='atmosphere_hybrid_height_coordinate'), - ] - aux_factory = HybridHeightFactory( - delta=lev_coord, - sigma=b_coord, - orography=orog_coord, - ) - cube.aux_factories = ['dummy', aux_factory] - return cube - - -@pytest.fixture -def mock_hybrid_pressure_cube(): - """Return mocked cube with hybrid pressure coordinate.""" - cube = unittest.mock.create_autospec(Cube, spec_set=True, instance=True) - ap_coord = AuxCoord([1.0], bounds=[[0.0, 2.0]], var_name='ap', units='Pa') - b_coord = AuxCoord([0.0], - bounds=[[-0.5, 1.5]], - var_name='b', - units=Unit('1')) - ps_coord = AuxCoord([[[100000]]], var_name='ps', units='Pa') - cube.coord.side_effect = [ - ap_coord, b_coord, ps_coord, ap_coord, b_coord, ps_coord - ] - cube.coords.return_value = [ - ap_coord, - b_coord, - ps_coord, - AuxCoord(0.0, - standard_name='atmosphere_hybrid_sigma_pressure_coordinate'), - ] - aux_factory = HybridPressureFactory( - delta=ap_coord, - sigma=b_coord, - surface_air_pressure=ps_coord, - ) - cube.aux_factories = ['dummy', aux_factory] - return cube - - @pytest.fixture def real_hybrid_pressure_cube(): """Return real cube with hybrid pressure coordinate.""" @@ -168,114 +70,6 @@ def real_hybrid_pressure_cube_list(): return get_hybrid_pressure_cube_list() -def check_if_fix_aux_factories_is_necessary(): - """Check if _fix_aux_factories() is necessary (i.e. iris bug is fixed).""" - cubes = get_hybrid_pressure_cube_list() - cube = cubes.concatenate_cube() - coords = [coord.name() for coord in cube.coords()] - msg = ("Apparently concatenation of cubes that have a derived variable " - "is now possible in iris (i.e. issue #2478 has been fixed). Thus, " - "this test and ALL appearances of the function " - "'_fix_aux_factories' can safely be removed!") - if 'air_pressure' in coords: - warnings.warn(msg) - - -def test_fix_aux_factories_empty_cube(mock_empty_cube): - """Test fixing with empty cube.""" - check_if_fix_aux_factories_is_necessary() - _io._fix_aux_factories(mock_empty_cube) - assert mock_empty_cube.mock_calls == [call.coords()] - - -def test_fix_aux_factories_atmosphere_sigma(mock_atmosphere_sigma_cube): - """Test fixing of atmosphere sigma coordinate.""" - check_if_fix_aux_factories_is_necessary() - - # Test with aux_factory object - _io._fix_aux_factories(mock_atmosphere_sigma_cube) - mock_atmosphere_sigma_cube.coords.assert_called_once_with() - mock_atmosphere_sigma_cube.coord.assert_has_calls( - [call(var_name='ptop'), - call(var_name='lev'), - call(var_name='ps')]) - mock_atmosphere_sigma_cube.add_aux_factory.assert_not_called() - - # Test without aux_factory object - mock_atmosphere_sigma_cube.reset_mock() - mock_atmosphere_sigma_cube.aux_factories = ['dummy'] - _io._fix_aux_factories(mock_atmosphere_sigma_cube) - mock_atmosphere_sigma_cube.coords.assert_called_once_with() - mock_atmosphere_sigma_cube.coord.assert_has_calls( - [call(var_name='ptop'), - call(var_name='lev'), - call(var_name='ps')]) - mock_atmosphere_sigma_cube.add_aux_factory.assert_called_once() - - -def test_fix_aux_factories_hybrid_height(mock_hybrid_height_cube): - """Test fixing of hybrid height coordinate.""" - check_if_fix_aux_factories_is_necessary() - - # Test with aux_factory object - _io._fix_aux_factories(mock_hybrid_height_cube) - mock_hybrid_height_cube.coords.assert_called_once_with() - mock_hybrid_height_cube.coord.assert_has_calls( - [call(var_name='lev'), - call(var_name='b'), - call(var_name='orog')]) - mock_hybrid_height_cube.add_aux_factory.assert_not_called() - - # Test without aux_factory object - mock_hybrid_height_cube.reset_mock() - mock_hybrid_height_cube.aux_factories = ['dummy'] - _io._fix_aux_factories(mock_hybrid_height_cube) - mock_hybrid_height_cube.coords.assert_called_once_with() - mock_hybrid_height_cube.coord.assert_has_calls( - [call(var_name='lev'), - call(var_name='b'), - call(var_name='orog')]) - mock_hybrid_height_cube.add_aux_factory.assert_called_once() - - -def test_fix_aux_factories_hybrid_pressure(mock_hybrid_pressure_cube): - """Test fixing of hybrid pressure coordinate.""" - check_if_fix_aux_factories_is_necessary() - - # Test with aux_factory object - _io._fix_aux_factories(mock_hybrid_pressure_cube) - mock_hybrid_pressure_cube.coords.assert_called_once_with() - mock_hybrid_pressure_cube.coord.assert_has_calls( - [call(var_name='ap'), - call(var_name='b'), - call(var_name='ps')]) - mock_hybrid_pressure_cube.add_aux_factory.assert_not_called() - - # Test without aux_factory object - mock_hybrid_pressure_cube.reset_mock() - mock_hybrid_pressure_cube.aux_factories = ['dummy'] - _io._fix_aux_factories(mock_hybrid_pressure_cube) - mock_hybrid_pressure_cube.coords.assert_called_once_with() - mock_hybrid_pressure_cube.coord.assert_has_calls( - [call(var_name='ap'), - call(var_name='b'), - call(var_name='ps')]) - mock_hybrid_pressure_cube.add_aux_factory.assert_called_once() - - -def test_fix_aux_factories_real_cube(real_hybrid_pressure_cube): - """Test fixing of hybrid pressure coordinate on real cube.""" - check_if_fix_aux_factories_is_necessary() - assert not real_hybrid_pressure_cube.coords('air_pressure') - _io._fix_aux_factories(real_hybrid_pressure_cube) - air_pressure_coord = real_hybrid_pressure_cube.coord('air_pressure') - expected_coord = AuxCoord([[[[1.0]]]], - bounds=[[[[[-50000., 150002.]]]]], - standard_name='air_pressure', - units='Pa') - assert air_pressure_coord == expected_coord - - def test_concatenation_with_aux_factory(real_hybrid_pressure_cube_list): """Test actual concatenation of a cube with a derived coordinate.""" concatenated = _io.concatenate(real_hybrid_pressure_cube_list) diff --git a/tests/integration/recipe/test_recipe.py b/tests/integration/recipe/test_recipe.py index 38e015078f..9e2b7ca924 100644 --- a/tests/integration/recipe/test_recipe.py +++ b/tests/integration/recipe/test_recipe.py @@ -16,7 +16,7 @@ import esmvalcore._task from esmvalcore._recipe.recipe import ( _get_input_datasets, - _representative_dataset, + _representative_datasets, read_recipe_file, ) from esmvalcore._task import DiagnosticTask @@ -1490,8 +1490,8 @@ def test_alias_generation(tmp_path, patched_datafinder, session): - {dataset: EC-EARTH, ensemble: r1i1p1} - {dataset: EC-EARTH, ensemble: r2i1p1} - {dataset: EC-EARTH, ensemble: r3i1p1, alias: my_alias} - - {dataset: FGOALS-g3, sub_experiment: s1960, ensemble: r1} - - {dataset: FGOALS-g3, sub_experiment: s1961, ensemble: r1} + - {dataset: FGOALS-g3, sub_experiment: s1960, ensemble: r1, institute: CAS} + - {dataset: FGOALS-g3, sub_experiment: s1961, ensemble: r1, institute: CAS} - {project: OBS, dataset: ERA-Interim, version: 1} - {project: OBS, dataset: ERA-Interim, version: 2} - {project: CMIP6, activity: CMP, dataset: GF3, ensemble: r1, institute: fake} @@ -2360,7 +2360,9 @@ def test_representative_dataset_regular_var(patched_datafinder, session): } dataset = Dataset(**variable) dataset.session = session - filename = _representative_dataset(dataset).files[0] + datasets = _representative_datasets(dataset) + assert len(datasets) == 1 + filename = datasets[0].files[0] path = Path(filename) assert path.name == 'atm_amip-rad_R2B4_r1i1p1f1_atm_2d_ml_1990_1999.nc' @@ -2384,11 +2386,9 @@ def test_representative_dataset_derived_var(patched_datafinder, session, } dataset = Dataset(**variable) dataset.session = session - representative_dataset = _representative_dataset(dataset) + representative_datasets = _representative_datasets(dataset) - expect_required_var = { - # Added by get_required - 'short_name': 'rsdscs', + expected_facets = { # Already present in variable 'dataset': 'ICON', 'derive': True, @@ -2399,22 +2399,40 @@ def test_representative_dataset_derived_var(patched_datafinder, session, 'project': 'ICON', 'timerange': '1990/2000', # Added by _add_cmor_info - 'long_name': 'Surface Downwelling Clear-Sky Shortwave Radiation', 'modeling_realm': ['atmos'], - 'original_short_name': 'rsdscs', - 'standard_name': - 'surface_downwelling_shortwave_flux_in_air_assuming_clear_sky', 'units': 'W m-2', # Added by _add_extra_facets 'var_type': 'atm_2d_ml', } if force_derivation: - expected_dataset = Dataset(**expect_required_var) - expected_dataset.session = session + expected_datasets = [ + Dataset( + short_name='rsdscs', + long_name='Surface Downwelling Clear-Sky Shortwave Radiation', + original_short_name='rsdscs', + standard_name=( + 'surface_downwelling_shortwave_flux_in_air_assuming_clear_' + 'sky' + ), + **expected_facets, + ), + Dataset( + short_name='rsuscs', + long_name='Surface Upwelling Clear-Sky Shortwave Radiation', + original_short_name='rsuscs', + standard_name=( + 'surface_upwelling_shortwave_flux_in_air_assuming_clear_' + 'sky' + ), + **expected_facets, + ), + ] else: - expected_dataset = dataset + expected_datasets = [dataset] + for dataset in expected_datasets: + dataset.session = session - assert representative_dataset == expected_dataset + assert representative_datasets == expected_datasets def test_get_derive_input_variables(patched_datafinder, session): @@ -3140,3 +3158,29 @@ def test_deprecated_unstructured_nearest_scheme( scripts: null """) get_recipe(tmp_path, content, session) + + +def test_wildcard_derived_var( + tmp_path, patched_failing_datafinder, session +): + content = dedent(""" + diagnostics: + diagnostic_name: + variables: + swcre: + mip: Amon + derive: true + force_derivation: true + timerange: '2000/2010' + additional_datasets: + - {project: CMIP5, dataset: '*', institute: '*', exp: amip, + ensemble: r1i1p1} + scripts: null + """) + recipe = get_recipe(tmp_path, content, session) + + assert len(recipe.datasets) == 1 + dataset = recipe.datasets[0] + assert dataset.facets['dataset'] == 'BBB' + assert dataset.facets['institute'] == 'B' + assert dataset.facets['short_name'] == 'swcre' diff --git a/tests/sample_data/experimental/test_run_recipe.py b/tests/sample_data/experimental/test_run_recipe.py index 8dadb73805..771c572a77 100644 --- a/tests/sample_data/experimental/test_run_recipe.py +++ b/tests/sample_data/experimental/test_run_recipe.py @@ -3,6 +3,8 @@ Runs recipes using :meth:`esmvalcore.experimental.Recipe.run`. """ +import logging +import os from contextlib import contextmanager from pathlib import Path @@ -55,12 +57,20 @@ def recipe(): @pytest.mark.use_sample_data +@pytest.mark.parametrize('ssh', (True, False)) @pytest.mark.parametrize('task', (None, 'example/ta')) -def test_run_recipe(monkeypatch, task, recipe, tmp_path): +def test_run_recipe(monkeypatch, task, ssh, recipe, tmp_path, caplog): """Test running a basic recipe using sample data. Recipe contains no provenance and no diagnostics. """ + caplog.set_level(logging.INFO) + caplog.clear() + if ssh: + monkeypatch.setitem(os.environ, 'SSH_CONNECTION', '0.0 0 1.1 1') + else: + monkeypatch.delitem(os.environ, 'SSH_CONNECTION', raising=False) + TAGS.set_tag_values(AUTHOR_TAGS) assert isinstance(recipe, Recipe) @@ -97,6 +107,12 @@ def test_run_recipe(monkeypatch, task, recipe, tmp_path): cube = data_file.load_iris() assert isinstance(cube, iris.cube.CubeList) + msg = "It looks like you are connected to a remote machine via SSH." + if ssh: + assert msg in caplog.text + else: + assert msg not in caplog.text + @pytest.mark.use_sample_data def test_run_recipe_diagnostic_failing(monkeypatch, recipe, tmp_path): diff --git a/tests/unit/preprocessor/_area/test_area.py b/tests/unit/preprocessor/_area/test_area.py index 8835014ff2..f7f507e496 100644 --- a/tests/unit/preprocessor/_area/test_area.py +++ b/tests/unit/preprocessor/_area/test_area.py @@ -1399,5 +1399,51 @@ def test_meridional_statistics_invalid_norm_fail(make_testcube): meridional_statistics(make_testcube, 'sum', normalize='x') +def test_time_dependent_volcello(): + coord_sys = iris.coord_systems.GeogCS(iris.fileformats.pp.EARTH_RADIUS) + data = np.ma.ones((2, 3, 2, 2)) + + time = iris.coords.DimCoord([15, 45], + standard_name='time', + bounds=[[1., 30.], [30., 60.]], + units=Unit('days since 1950-01-01', + calendar='gregorian')) + + zcoord = iris.coords.DimCoord([0.5, 5., 50.], + long_name='zcoord', + bounds=[[0., 2.5], [2.5, 25.], + [25., 250.]], + units='m', + attributes={'positive': 'down'}) + lons = iris.coords.DimCoord([1.5, 2.5], + standard_name='longitude', + bounds=[[1., 2.], [2., 3.]], + units='degrees_east', + coord_system=coord_sys) + lats = iris.coords.DimCoord([1.5, 2.5], + standard_name='latitude', + bounds=[[1., 2.], [2., 3.]], + units='degrees_north', + coord_system=coord_sys) + coords_spec4 = [(time, 0), (zcoord, 1), (lats, 2), (lons, 3)] + cube = iris.cube.Cube(data, dim_coords_and_dims=coords_spec4) + volcello = iris.coords.CellMeasure( + data, + standard_name='ocean_volume', + units='m3', + measure='volume') + cube.add_cell_measure(volcello, range(0, volcello.ndim)) + cube = extract_shape( + cube, + 'AR6', + method='contains', + crop=False, + decomposed=True, + ids={'Acronym': ['EAO', 'WAF']}, + ) + + assert cube.shape == cube.cell_measure('ocean_volume').shape + + if __name__ == '__main__': unittest.main() diff --git a/tests/unit/preprocessor/_derive/test_lwcre.py b/tests/unit/preprocessor/_derive/test_lwcre.py new file mode 100644 index 0000000000..c70807a149 --- /dev/null +++ b/tests/unit/preprocessor/_derive/test_lwcre.py @@ -0,0 +1,28 @@ +"""Test derivation of `lwcre`.""" +import numpy as np +import pytest +from iris.cube import Cube, CubeList + +import esmvalcore.preprocessor._derive.lwcre as lwcre + + +@pytest.fixture +def cubes(): + rlut_cube = Cube( + 3, standard_name='toa_outgoing_longwave_flux', units='W m-2' + ) + rlutcs_cube = Cube( + 1, + standard_name='toa_outgoing_longwave_flux_assuming_clear_sky', + units='W m-2', + ) + return CubeList([rlut_cube, rlutcs_cube]) + + +def test_lwcre_calculation(cubes): + """Test calculation of `lwcre`.""" + derived_var = lwcre.DerivedVariable() + out_cube = derived_var.calculate(cubes) + np.testing.assert_equal(out_cube.data, -2) + assert out_cube.units == 'W m-2' + assert out_cube.attributes['positive'] == 'down' diff --git a/tests/unit/preprocessor/_derive/test_netcre.py b/tests/unit/preprocessor/_derive/test_netcre.py new file mode 100644 index 0000000000..f520b1bc13 --- /dev/null +++ b/tests/unit/preprocessor/_derive/test_netcre.py @@ -0,0 +1,36 @@ +"""Test derivation of `netcre`.""" +import numpy as np +import pytest +from iris.cube import Cube, CubeList + +import esmvalcore.preprocessor._derive.netcre as netcre + + +@pytest.fixture +def cubes(): + rlut_cube = Cube( + 3, standard_name='toa_outgoing_longwave_flux', units='W m-2' + ) + rlutcs_cube = Cube( + 1, + standard_name='toa_outgoing_longwave_flux_assuming_clear_sky', + units='W m-2', + ) + rsut_cube = Cube( + 3, standard_name='toa_outgoing_shortwave_flux', units='W m-2' + ) + rsutcs_cube = Cube( + 1, + standard_name='toa_outgoing_shortwave_flux_assuming_clear_sky', + units='W m-2', + ) + return CubeList([rlut_cube, rlutcs_cube, rsut_cube, rsutcs_cube]) + + +def test_netcre_calculation(cubes): + """Test calculation of `netcre`.""" + derived_var = netcre.DerivedVariable() + out_cube = derived_var.calculate(cubes) + np.testing.assert_equal(out_cube.data, -4) + assert out_cube.units == 'W m-2' + assert out_cube.attributes['positive'] == 'down' diff --git a/tests/unit/preprocessor/_derive/test_rsnt.py b/tests/unit/preprocessor/_derive/test_rsnt.py new file mode 100644 index 0000000000..4cc16f1709 --- /dev/null +++ b/tests/unit/preprocessor/_derive/test_rsnt.py @@ -0,0 +1,26 @@ +"""Test derivation of `rsnt`.""" +import numpy as np +import pytest +from iris.cube import Cube, CubeList + +import esmvalcore.preprocessor._derive.rsnt as rsnt + + +@pytest.fixture +def cubes(): + rsdt_cube = Cube( + 3, standard_name='toa_incoming_shortwave_flux', units='W m-2' + ) + rsut_cube = Cube( + 1, standard_name='toa_outgoing_shortwave_flux', units='W m-2' + ) + return CubeList([rsdt_cube, rsut_cube]) + + +def test_rsnt_calculation(cubes): + """Test calculation of `rsnt`.""" + derived_var = rsnt.DerivedVariable() + out_cube = derived_var.calculate(cubes) + np.testing.assert_equal(out_cube.data, 2) + assert out_cube.units == 'W m-2' + assert out_cube.attributes['positive'] == 'down' diff --git a/tests/unit/preprocessor/_derive/test_swcre.py b/tests/unit/preprocessor/_derive/test_swcre.py new file mode 100644 index 0000000000..165477061a --- /dev/null +++ b/tests/unit/preprocessor/_derive/test_swcre.py @@ -0,0 +1,28 @@ +"""Test derivation of `swcre`.""" +import numpy as np +import pytest +from iris.cube import Cube, CubeList + +import esmvalcore.preprocessor._derive.swcre as swcre + + +@pytest.fixture +def cubes(): + rsut_cube = Cube( + 3, standard_name='toa_outgoing_shortwave_flux', units='W m-2' + ) + rsutcs_cube = Cube( + 1, + standard_name='toa_outgoing_shortwave_flux_assuming_clear_sky', + units='W m-2', + ) + return CubeList([rsut_cube, rsutcs_cube]) + + +def test_swcre_calculation(cubes): + """Test calculation of `swcre`.""" + derived_var = swcre.DerivedVariable() + out_cube = derived_var.calculate(cubes) + np.testing.assert_equal(out_cube.data, -2) + assert out_cube.units == 'W m-2' + assert out_cube.attributes['positive'] == 'down' diff --git a/tests/unit/preprocessor/_time/test_time.py b/tests/unit/preprocessor/_time/test_time.py index a1ae135c25..03ca53dd24 100644 --- a/tests/unit/preprocessor/_time/test_time.py +++ b/tests/unit/preprocessor/_time/test_time.py @@ -2082,8 +2082,9 @@ def test_resample_same_interval(self): times = np.arange(0, 48, 12) cube = self._create_cube(data, times) - with self.assertRaises(ValueError): - resample_hours(cube, interval=12) + result = resample_hours(cube, interval=12) + expected = np.arange(0, 48, 12) + assert_array_equal(result.data, expected) def test_resample_nodata(self): """Test average of a 1D field.""" diff --git a/tests/unit/preprocessor/_volume/test_volume.py b/tests/unit/preprocessor/_volume/test_volume.py index fccd065a36..e962e062dd 100644 --- a/tests/unit/preprocessor/_volume/test_volume.py +++ b/tests/unit/preprocessor/_volume/test_volume.py @@ -56,6 +56,16 @@ def setUp(self): [25., 250.]], units='m', attributes={'positive': 'down'}) + scoord = iris.coords.DimCoord([36., 36.5, 37.], + long_name='ocean_sigma_coordinate', + bounds=[[35.5, 36.25], [36.25, 36.75], + [36.75, 37.5]], + units='kg m-3', + attributes={'positive': 'down'}) + zcoord_nobounds = iris.coords.DimCoord([0.5, 5., 50.], + long_name='zcoord', + units='m', + attributes={'positive': 'down'}) zcoord_4d = iris.coords.AuxCoord( np.broadcast_to([[[[0.5]], [[5.]], [[50.]]]], (2, 3, 2, 2)), long_name='zcoord', @@ -66,6 +76,18 @@ def setUp(self): units='m', attributes={'positive': 'down'}, ) + zcoord_3d_invalid_bounds = iris.coords.AuxCoord( + np.broadcast_to([[[0.5]], [[5.]], [[50.]]], (3, 2, 2)), + long_name='zcoord', + bounds=np.broadcast_to( + [[[[0., 2.5, 2.5, 3.]]], + [[[2.5, 25., 25., 30.]]], + [[[25., 250., 250., 300.]]]], + (3, 2, 2, 4), + ), + units='m', + attributes={'positive': 'down'}, + ) lons2 = iris.coords.DimCoord([1.5, 2.5], standard_name='longitude', bounds=[[1., 2.], [2., 3.]], @@ -77,6 +99,15 @@ def setUp(self): units='degrees_north', coord_system=coord_sys) + lons2d = iris.coords.AuxCoord([[1.5, 2.5], [1.2, 2.7]], + standard_name='longitude', + units='degrees_east', + coord_system=coord_sys) + lats2d = iris.coords.AuxCoord([[1.5, 2.5], [1.2, 2.7]], + standard_name='latitude', + units='degrees_north', + coord_system=coord_sys) + coords_spec3 = [(zcoord, 0), (lats2, 1), (lons2, 2)] self.grid_3d = iris.cube.Cube(data1, dim_coords_and_dims=coords_spec3) @@ -87,6 +118,13 @@ def setUp(self): units='kg m-3', ) + coords_spec4_sigma = [(time, 0), (scoord, 1), (lats2, 2), (lons2, 3)] + self.grid_4d_sigma_space = iris.cube.Cube( + data2, + dim_coords_and_dims=coords_spec4_sigma, + units='kg m-3', + ) + coords_spec5 = [(time2, 0), (zcoord, 1), (lats2, 2), (lons2, 3)] self.grid_4d_2 = iris.cube.Cube( data3, @@ -101,6 +139,28 @@ def setUp(self): units='kg m-3', ) + self.grid_4d_znobounds = iris.cube.Cube( + data2, + dim_coords_and_dims=[ + (time, 0), (zcoord_nobounds, 1), (lats2, 2), (lons2, 3) + ], + units='kg m-3', + ) + + self.grid_4d_irregular = iris.cube.Cube( + data2, + dim_coords_and_dims=[(time, 0), (zcoord, 1)], + aux_coords_and_dims=[(lats2d, (2, 3)), (lons2d, (2, 3))], + units='kg m-3', + ) + + self.grid_invalid_z_bounds = iris.cube.Cube( + data2, + dim_coords_and_dims=[(time, 0), (lats2, 2), (lons2, 3)], + aux_coords_and_dims=[(zcoord_3d_invalid_bounds, (1, 2, 3))], + units='kg m-3', + ) + # allow iris to figure out the axis='z' coordinate iris.util.guess_coord_axis(self.grid_3d.coord('zcoord')) iris.util.guess_coord_axis(self.grid_4d.coord('zcoord')) @@ -368,6 +428,20 @@ def test_volume_statistics(self): self.assertFalse(self.grid_4d.cell_measures('ocean_volume')) self.assertFalse(result.cell_measures('ocean_volume')) + def test_volume_nolevbounds(self): + """Test to take the volume weighted average of a cube with no bounds + in the z axis. + """ + + self.assertFalse(self.grid_4d_znobounds.coord(axis='z').has_bounds()) + result = volume_statistics(self.grid_4d_znobounds, 'mean') + + expected = np.ma.array([1., 1.], mask=False) + self.assert_array_equal(result.data, expected) + self.assertEqual(result.units, 'kg m-3') + self.assertFalse(self.grid_4d.cell_measures('ocean_volume')) + self.assertFalse(result.cell_measures('ocean_volume')) + def test_volume_statistics_cell_measure(self): """Test to take the volume weighted average of a (2,3,2,2) cube. @@ -467,49 +541,110 @@ def test_volume_statistics_wrong_operator_fail(self): str(err.exception)) def test_volume_statistics_2d_lat_fail(self): - # Create dummy 2D latitude from depth - new_lat_coord = self.grid_4d_z.coord('zcoord')[0, 0, :, :] - new_lat_coord.rename('latitude') - self.grid_4d_z.remove_coord('latitude') - self.grid_4d_z.add_aux_coord(new_lat_coord, (2, 3)) with self.assertRaises(CoordinateMultiDimError): - volume_statistics(self.grid_4d_z, 'mean') + volume_statistics(self.grid_4d_irregular, 'mean') + + def test_volume_statistics_2d_lat_cellarea(self): + measure = iris.coords.CellMeasure(np.arange(1, 5).reshape(2, 2), + standard_name='cell_area', + units='m2', + measure='area') + self.grid_4d_irregular.add_cell_measure(measure, (2, 3)) - def test_volume_statistics_4d_depth_fail(self): - # Fails because depth coord dims are (0, ...), but must be (1, ...) + result = volume_statistics(self.grid_4d_irregular, 'mean') + expected = np.ma.array([1., 1.], mask=False) + self.assert_array_equal(result.data, expected) + self.assertEqual(result.units, 'kg m-3') + + data = np.ma.arange(1, 25).reshape(2, 3, 2, 2) + self.grid_4d_irregular.data = data + + result = volume_statistics(self.grid_4d_irregular, 'mean') + expected = np.ma.array([10.56, 22.56], mask=False) + self.assert_array_equal(result.data, expected) + self.assertEqual(result.units, 'kg m-3') + + def test_volume_statistics_invalid_bounds(self): + """Test z-axis bounds is not 2 in last dimension""" + + with self.assertRaises(ValueError) as err: + volume_statistics(self.grid_invalid_z_bounds, 'mean') + self.assertIn( + "Z axis bounds shape found (3, 2, 2, 4). Bounds should be " + "2 in the last dimension to compute the thickness.", + str(err.exception) + ) + + def test_volume_statistics_invalid_units(self): + """Test z-axis units cannot be converted to m""" + + with self.assertRaises(ValueError) as err: + volume_statistics(self.grid_4d_sigma_space, 'mean') + self.assertIn( + "Cannot compute volume using the Z-axis. " + "Unable to convert from 'Unit('kg m-3')' to 'Unit('m')'.", + str(err.exception) + ) + + def test_volume_statistics_z_axis_time_error(self): + # Fails because depth z-axis coord depends on time dimensions + # which would aggregate also along that dimension with self.assertRaises(ValueError) as err: volume_statistics(self.grid_4d_z, 'mean') self.assertIn( - "Supplementary variables are needed to calculate grid cell " - "volumes for cubes with 4D depth coordinate, got cube ", - str(err.exception), + "X and Y axis coordinates depend on (2, 3) dimensions, " + "while X, Y, and Z axis depends on (0, 1, 2, 3) dimensions. " + "This may indicate Z axis depending on other dimension than " + "space that could provoke invalid aggregation...", + str(err.exception) + ) + + grid_3d_no_x = self.grid_4d_z[..., 0] + with self.assertRaises(ValueError) as err: + volume_statistics(grid_3d_no_x, 'mean') + self.assertIn( + "X and Y axis coordinates depend on (2,) dimensions, " + "while X, Y, and Z axis depends on (0, 1, 2) dimensions. " + "This may indicate Z axis depending on other dimension than " + "space that could provoke invalid aggregation...", + str(err.exception) ) - def test_volume_statistics_2d_depth_fail(self): + def test_volume_statistics_missing_axis(self): + # x axis is missing + grid_no_x = self.grid_4d[..., 0] + volume_statistics(grid_no_x, 'mean') + + # y axis is missing + grid_no_y = self.grid_4d[..., 0, :] + volume_statistics(grid_no_y, 'mean') + + # z axis is missing + grid_no_z = self.grid_4d[:, 0] + with self.assertRaises(ValueError) as err: + volume_statistics(grid_no_z, 'mean') + self.assertIn("Cannot compute volume with scalar Z-axis", + str(err.exception)) + + def test_volume_statistics_2d_depth(self): # Create new 2D depth coord new_z_coord = self.grid_4d_z.coord('zcoord')[0, :, :, 0] self.grid_4d_z.remove_coord('zcoord') self.grid_4d_z.add_aux_coord(new_z_coord, (1, 2)) - with self.assertRaises(ValueError) as err: - volume_statistics(self.grid_4d_z, 'mean') - self.assertIn( - "Supplementary variables are needed to calculate grid cell " - "volumes for cubes with 2D depth coordinate, got cube ", - str(err.exception), - ) + result = volume_statistics(self.grid_4d, 'mean') + expected = np.ma.array([1., 1.], mask=False) + self.assert_array_equal(result.data, expected) def test_depth_integration_1d(self): """Test to take the depth integration of a 3 layer cube.""" result = depth_integration(self.grid_3d[:, 0, 0]) expected = np.ones((1, 1)) * 250. - print(result.data, expected.data) self.assert_array_equal(result.data, expected) def test_depth_integration_3d(self): """Test to take the depth integration of a 3 layer cube.""" result = depth_integration(self.grid_3d) expected = np.ones((2, 2)) * 250. - print(result.data, expected.data) self.assert_array_equal(result.data, expected) def test_extract_transect_latitude(self): diff --git a/tests/unit/recipe/test_to_datasets.py b/tests/unit/recipe/test_to_datasets.py index ac68dd2f41..d20f2c0d85 100644 --- a/tests/unit/recipe/test_to_datasets.py +++ b/tests/unit/recipe/test_to_datasets.py @@ -411,3 +411,28 @@ def test_append_missing_supplementaries(): short_names = {f['short_name'] for f in supplementaries} assert short_names == {'areacella', 'sftlf'} + + +def test_report_unexpanded_globs(mocker): + dataset = Dataset( + alias='CMIP5', + dataset='*', + diagnostic='diagnostic1', + ensemble='r1i1p1', + exp='historical', + mip='Amon', + preprocessor='preprocessor1', + project='CMIP5', + recipe_dataset_index=1, + short_name='ta', + variable_group='ta850', + ) + file = mocker.Mock(facets={'dataset': '*'}) + dataset.files = [file] + unexpanded_globs = {'dataset': '*'} + + msg = to_datasets._report_unexpanded_globs( + dataset, dataset, unexpanded_globs + ) + + assert 'paths to the' not in msg